diff --git a/css/style.css b/css/style.css index 669f12b..957375d 100644 --- a/css/style.css +++ b/css/style.css @@ -567,6 +567,7 @@ table.dataTable thead .sorting { table.track-analysis-table tbody tr:hover, table.dataTable.hover tbody tr:hover, +table.dataTable.hover tbody tr.hoverRoute, table.dataTable.display tbody tr:hover { background-color: rgba(255, 255, 0, 0.3); } diff --git a/js/control/TrackMessages.js b/js/control/TrackMessages.js index ed2f435..6dfa140 100644 --- a/js/control/TrackMessages.js +++ b/js/control/TrackMessages.js @@ -36,6 +36,8 @@ BR.TrackMessages = L.Class.extend({ */ trackPolyline: null, + segments: null, + initialize: function (map, options) { L.setOptions(this, options); this._map = map; @@ -46,9 +48,12 @@ BR.TrackMessages = L.Class.extend({ var syncButton = document.getElementById('data-sync-map'); L.DomEvent.on(syncButton, 'click', this._toggleSyncMap, this); + + this._mapMouseMoveHandlerBound = this.mapMouseMoveHandler.bind(this); + this._mapMouseOutHandlerBound = this.mapMouseOutHandler.bind(this); }, - update: function (polyline, segments) { + update: function (polyline, segments, layer) { var i, messages, columns, @@ -56,11 +61,13 @@ BR.TrackMessages = L.Class.extend({ data = []; if (!this.active) { + this.listenMapEvents(layer, false); return; } this.trackPolyline = polyline; this.trackEdges = new BR.TrackEdges(segments); + this.segments = segments; for (i = 0; segments && i < segments.length; i++) { messages = segments[i].feature.properties.messages; @@ -72,6 +79,7 @@ BR.TrackMessages = L.Class.extend({ this._destroyTable(); if (data.length === 0) { + this.listenMapEvents(layer, false); return; } @@ -96,6 +104,20 @@ BR.TrackMessages = L.Class.extend({ $('#datatable tbody tr').hover(L.bind(this._handleHover, this), L.bind(this._handleHoverOut, this)); $('#datatable tbody').on('click', 'tr', L.bind(this._toggleSelected, this)); + + this.listenMapEvents(layer, true); + }, + + listenMapEvents: function (layer, on) { + if (layer) { + if (on) { + layer.on('mousemove', this._mapMouseMoveHandlerBound); + layer.on('mouseout', this._mapMouseOutHandlerBound); + } else { + layer.off('mousemove', this._mapMouseMoveHandlerBound); + layer.off('mouseout', this._mapMouseOutHandlerBound); + } + } }, show: function () { @@ -209,4 +231,58 @@ BR.TrackMessages = L.Class.extend({ button.classList.toggle('active'); this.options.syncMap = !this.options.syncMap; }, + + mapMouseMoveHandler: function (evt) { + // initialize the vars for the closest item calculation + let closestPointIdx = null; + // large enough to be trumped by any point on the chart + let closestDistance = 2 * Math.pow(100, 2); + // consider a good enough match if the given point (lat and lng) is within + // 1.1 meters of a point on the chart (there are 111,111 meters in a degree) + const exactMatchRounding = 1.1 / 111111; + + const point = turf.point([evt.latlng.lng, evt.latlng.lat]); + let idxOffset = 0; + outer: for (let segment of this.segments) { + const bestPointForSegement = turf.nearestPointOnLine(segment.feature, point); + if (bestPointForSegement.properties.dist < closestDistance) { + closestPointIdx = idxOffset + bestPointForSegement.properties.index; + closestDistance = bestPointForSegement.properties.dist; + } + idxOffset += segment.feature.geometry.coordinates.length; + } + + if (closestPointIdx !== null) { + // Now map point to next data row + let rowIdx = -1; + for (let i = 0; i < this.trackEdges.edges.length; i++) { + if (closestPointIdx < this.trackEdges.edges[i]) { + rowIdx = i; + break; + } + } + if (rowIdx != -1) { + // highlight found row + const rowObj = this._table.row(rowIdx); + if (rowObj && rowObj != this._mapHoveredRow) { + if (this._mapHoveredRow) { + this._mapHoveredRow.classList.remove('hoverRoute'); + } + this._mapHoveredRow = rowObj.node(); + this._mapHoveredRow.classList.add('hoverRoute'); + this._mapHoveredRow.scrollIntoView(false); + } + } + } else { + if (this._mapHoveredRow) { + this._mapHoveredRow.classList.remove('hoverRoute'); + } + } + }, + + mapMouseOutHandler: function () { + if (this._mapHoveredRow) { + this._mapHoveredRow.classList.remove('hoverRoute'); + } + }, }); diff --git a/js/index.js b/js/index.js index db574d5..e5f75d1 100644 --- a/js/index.js +++ b/js/index.js @@ -36,7 +36,6 @@ pois, circlego, urlHash; - // By default bootstrap-select use glyphicons $('.selectpicker').selectpicker({ iconBase: 'fa', @@ -223,9 +222,10 @@ function requestUpdate(updatable) { var track = routing.toPolyline(), - segments = routing.getSegments(); + segments = routing.getSegments(), + segmentsLayer = routing._segments; - updatable.update(track, segments); + updatable.update(track, segments, segmentsLayer); } routingOptions = new BR.RoutingOptions(); @@ -342,7 +342,7 @@ } else { stats.update(track, segments); } - trackMessages.update(track, segments); + trackMessages.update(track, segments, segmentsLayer); trackAnalysis.update(track, segments); exportRoute.update(latLngs, segments); diff --git a/js/plugin/RoutingPathQuality.js b/js/plugin/RoutingPathQuality.js index 0ff67e0..cdea8d7 100644 --- a/js/plugin/RoutingPathQuality.js +++ b/js/plugin/RoutingPathQuality.js @@ -60,6 +60,162 @@ BR.RoutingPathQuality = L.Control.extend({ }, }), }, + surface: { + title: i18next.t('map.route-quality-surface'), + icon: 'fa-road', + provider: new HotLineQualityProvider({ + hotlineOptions: { + renderer: renderer, + palette: { + // normal range + 0.0: 'red', + 0.45: 'yellow', + 0.9: 'green', + // special value for unknown + 1.0: '#888888', + }, + // note: without this the lib will get min/max from the actual + // values rendering the special values moot + min: 0, + max: 1, + discreteStrokes: true, + }, + valueFunction: (function () { + let cache = []; + return function (latLng) { + var feature = latLng.feature; + if (!feature.wayTags) { + return 1.0; + } else if (cache[feature.wayTags]) { + return cache[feature.wayTags]; + } + let data = new URLSearchParams(feature.wayTags.replace(/\s+/g, '&')); // eslint-disable-line compat/compat + let surface = null; + switch (data.get('surface')) { + case 'paved': + case 'chipseal': + surface = 0.8; + break; + case 'asphalt': + case 'concrete': + surface = 1; + break; + case 'concrete:lanes': + case 'concrete:plates': + surface = 0.6; + case 'sett': + case 'gravel': + case 'pebblestone': + case 'unpaved': + surface = 0.5; + break; + case 'paving_stones': + case 'compacted': + case 'fine_gravel': + surface = 0.7; + break; + case 'cobblestone': + case 'dirt': + case 'grass': + surface = 0.2; + break; + case 'unhewn_cobblestone': + surface = 0.01; + break; + case 'ground': + case 'earth': + surface = 0.3; + break; + case 'mud': + case 'sand': + surface = 0.01; + break; + case null: + break; + /*default: + console.warn('unhandled surface type', data.get('surface')); + break;*/ + } + + // modifier tracktype; also sometimes only tracktype is available + if (data.get('highway') === 'track') { + switch (data.get('tracktype') || 'unknown') { + case 'grade1': + if (surface === null) { + surface = 0.9; + } /* else { + don't change + } */ + break; + case 'grade2': + if (surface === null) { + surface = 0.7; + } else { + surface *= 0.9; + } + break; + case 'grade3': + if (surface === null) { + surface = 0.4; + } else { + surface *= 0.8; + } + break; + case 'grade4': + if (surface === null) { + surface = 0.1; + } else { + surface *= 0.6; + } + break; + case 'grade5': + if (surface === null) { + surface = 0.01; + } else { + surface *= 0.4; + } + break; + } + } + + if (surface !== null) { + // modifier for surface quality + switch (data.get('smoothness')) { + case 'excellent': + surface = Math.min(surface * 1.1, 1.0); + break; + case 'good': + surface = Math.min(surface * 1.05, 1.0); + break; + case 'intermediate': + surface *= 0.9; + break; + case 'bad': + surface *= 0.7; + break; + case 'very_bad': + surface *= 0.5; + break; + case 'horrible': + surface *= 0.4; + break; + case 'very_horrible': + surface *= 0.2; + break; + case 'impassable': + surface *= 0.01; + break; + } + } + + // limit normal values 0-0.9 so 1.0 can be unknown + const final = surface === null ? 1.0 : surface * 0.9; + cache[feature.wayTags] = final; + return final; + }; + })(), + }), + }, cost: { title: i18next.t('map.route-quality-shortcut', { action: '$t(map.route-quality-cost)', key: 'C' }), icon: 'fa-usd', diff --git a/locales/en.json b/locales/en.json index 3985a38..c533888 100644 --- a/locales/en.json +++ b/locales/en.json @@ -165,6 +165,7 @@ "route-quality-altitude": "Altitude coding", "route-quality-cost": "Cost coding", "route-quality-incline": "Incline coding", + "route-quality-surface": "Road surface/quality", "route-quality-shortcut": "{{action}} ({{key}} key to toggle)", "route-tooltip-segment": "Drag to create a new waypoint. Click to toggle straight line.", "route-tooltip-waypoint": "Waypoint. Drag to move; Click to remove.", diff --git a/package.json b/package.json index 8d04ea4..7dce75d 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "leaflet-editable": "1.2.0", "leaflet-filelayer": "1.2.0", "leaflet-geometryutil": "0.10.1", - "leaflet-hotline": "0.4.0", + "leaflet-hotline": "tbsmark86/Leaflet.hotline#25b2457", "leaflet-osm-notes": "osmlab/leaflet-osm-notes#af2aa811", "leaflet-plugins": "3.4.0", "leaflet-providers": "1.13.0", diff --git a/yarn.lock b/yarn.lock index e274f97..156f14d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8515,10 +8515,9 @@ leaflet-geometryutil@0.10.1: dependencies: leaflet "^1.6.0" -leaflet-hotline@0.4.0: +leaflet-hotline@tbsmark86/Leaflet.hotline#25b2457: version "0.4.0" - resolved "https://registry.yarnpkg.com/leaflet-hotline/-/leaflet-hotline-0.4.0.tgz#e01069836a9d2e2c78b1fa1db2013bd03c8ff8d9" - integrity sha512-+In6c8WxMsRKMmwQ1m2GmbNxbXvA3WsrOilJGK7l4Sj+mUDh1gdyGMYCIoRBtUeX7lMvBc4KKeEVAlwQERKpxg== + resolved "https://codeload.github.com/tbsmark86/Leaflet.hotline/tar.gz/25b24572b99ac66203d857e0fb27f430e2f68448" leaflet-osm-notes@osmlab/leaflet-osm-notes#af2aa811: version "0.0.1"