From 9eeca7e2d58eccb2c58d58d2974f981aa07da66a Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 1 Oct 2019 15:46:09 +0200 Subject: [PATCH 1/5] Added hotline based route overlay Added a overlay which reflects the quality of the route based on either: * cost * altitude * incline --- js/index.js | 4 + js/plugin/RoutingPathQuality.js | 201 ++++++++++++++++++++++++++++++++ js/router/BRouter.js | 51 +++++++- locales/de.json | 4 + locales/en.json | 4 + package.json | 1 + yarn.lock | 5 + 7 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 js/plugin/RoutingPathQuality.js diff --git a/js/index.js b/js/index.js index 21c9b6a..4e28628 100644 --- a/js/index.js +++ b/js/index.js @@ -199,6 +199,8 @@ requestUpdate: requestUpdate }); + routingPathQuality = new BR.RoutingPathQuality(map, layersControl); + routing = new BR.Routing({ routing: { router: L.bind(router.getRouteSegment, router) @@ -233,6 +235,7 @@ segmentsLayer = routing._segments; elevation.update(track, segmentsLayer); + routingPathQuality.update(track, segmentsLayer); if (BR.conf.transit) { itinerary.update(track, segments); } else { @@ -244,6 +247,7 @@ } routing.addTo(map); + routingPathQuality.addTo(map); elevation.addBelow(map); sidebar = BR.sidebar({ diff --git a/js/plugin/RoutingPathQuality.js b/js/plugin/RoutingPathQuality.js new file mode 100644 index 0000000..645cfa1 --- /dev/null +++ b/js/plugin/RoutingPathQuality.js @@ -0,0 +1,201 @@ +BR.RoutingPathQuality = L.Control.extend({ + initialize: function(map, layersControl) { + // hotline uses canvas and cannot be moved in front of the svg, so we create another pane + map.createPane('routingQualityPane'); + map.getPane('routingQualityPane').style.zIndex = 450; + map.getPane('routingQualityPane').style.pointerEvents = 'none'; + var renderer = new L.Hotline.Renderer({ pane: 'routingQualityPane' }); + + this._routingSegments = L.featureGroup(); + layersControl.addOverlay(this._routingSegments, i18next.t('map.layer.route-quality')); + + this.providers = { + cost: { + title: i18next.t('map.route-quality-cost'), + icon: 'fa-usd', + provider: new HotlineProvider({ + hotlineOptions: { + renderer: renderer + }, + valueFunction: function(latLng) { + let feature = latLng.feature; + return ( + feature.cost.perKm + + feature.cost.elev + + feature.cost.turn + + feature.cost.node + + feature.cost.initial + ); + } + }) + }, + altitude: { + title: i18next.t('map.route-quality-altitude'), + icon: 'fa-area-chart', + provider: new HotlineProvider({ + hotlineOptions: { + renderer: renderer + }, + valueFunction: function(latLng) { + feature = latLng.feature; + return latLng.alt; + } + }) + }, + incline: { + title: i18next.t('map.route-quality-incline'), + icon: 'fa-line-chart', + provider: new HotlineProvider({ + hotlineOptions: { + min: -15, + max: 15, + palette: { + 0.0: '#ff0000', + 0.5: '#00ff00', + 1.0: '#ff0000' + }, + renderer: renderer + }, + valueFunction: function(latLng, prevLatLng) { + var deltaAltitude = latLng.alt - prevLatLng.alt, // in m + distance = prevLatLng.distanceTo(latLng); // in m + if (distance === 0) { + return 0; + } + return (Math.atan(deltaAltitude / distance) * 180) / Math.PI; + } + }) + } + }; + this.selectedProvider = this.options.initialProvider || 'cost'; + }, + + onAdd: function(map) { + var self = this; + this._map = map; + this._routingSegments.addTo(map); + + let states = []; + var i, + keys = Object.keys(this.providers), + l = keys.length; + + for (i = 0; i < l; ++i) { + let provider = this.providers[keys[i]]; + let nextState = keys[(i + 1) % l]; + states.push({ + stateName: keys[i], + icon: provider.icon, + title: provider.title, + onClick: function(btn) { + btn.state(nextState); + self.setProvider(nextState); + } + }); + } + + this.routingPathButton = new L.easyButton({ + states: states + }).addTo(map); + return new L.DomUtil.create('div'); + }, + + update: function(track, layer) { + var segments = []; + layer.eachLayer(function(layer) { + segments.push(layer); + }); + this.segments = segments; + this._update(this.segments); + }, + + setProvider: function(provider) { + this.selectedProvider = provider; + this._update(this.segments); + this._routingSegments.addTo(this._map); + }, + + _update: function(segments) { + this._routingSegments.clearLayers(); + let layers = this.providers[this.selectedProvider].provider.computeLayers(segments); + if (layers) { + for (let i = 0; i < layers.length; i++) { + this._routingSegments.addLayer(layers[i]); + } + } + } +}); + +class HotlineProvider { + constructor(options) { + this.hotlineOptions = options.hotlineOptions; + this.valueFunction = options.valueFunction; + } + + computeLayers(segments) { + let layers = []; + if (segments) { + let segmentLatLngs = []; + for (let i = 0; segments && i < segments.length; i++) { + let segment = segments[i]; + segmentLatLngs.push(this._computeLatLngVals(segment)); + } + let flatLines = segmentLatLngs.flat(); + + if (flatLines.length > 0) { + let hotlineOptions = Object.assign(new Object(), this.hotlineOptions); + if (!hotlineOptions.min && !hotlineOptions.max) { + let minMax = this._calcMinMaxValues(flatLines); + hotlineOptions.min = minMax.min; + hotlineOptions.max = minMax.max; + } + + for (let i = 0; i < segmentLatLngs.length; i++) { + const line = segmentLatLngs[i]; + let hotline = L.hotline(line, hotlineOptions); + layers.push(hotline); + } + } + } + return layers; + } + + _computeLatLngVals(segment) { + let latLngVals = [], + segmentLatLngs = segment.getLatLngs(), + segmentLength = segmentLatLngs.length; + + for (let i = 0; i < segmentLength; i++) { + var val = this.valueFunction.call( + this, + segmentLatLngs[i], + segmentLatLngs[Math.max(i - 1, 0)], + i, + segmentLatLngs + ); + latLngVals.push(this._convertToArray(segmentLatLngs[i], val)); + } + return latLngVals; + } + + _convertToArray(latLng, val) { + return [latLng.lat, latLng.lng, val]; + } + + _calcMinMaxValues(lines) { + let min = lines[0][2], + max = min; + for (let i = 1; lines && i < lines.length; i++) { + let line = lines[i]; + max = Math.max(max, line[2]); + min = Math.min(min, line[2]); + } + if (min === max) { + max = min + 1; + } + return { + min: min, + max: max + }; + } +} diff --git a/js/router/BRouter.js b/js/router/BRouter.js index 71469fb..b6e52da 100644 --- a/js/router/BRouter.js +++ b/js/router/BRouter.js @@ -154,7 +154,7 @@ L.BRouter = L.Class.extend({ try { geojson = JSON.parse(xhr.responseText); - layer = L.geoJSON(geojson).getLayers()[0]; + layer = this._assignFeatures(L.geoJSON(geojson).getLayers()[0]); return cb(null, layer); } catch (e) { @@ -190,6 +190,55 @@ L.BRouter = L.Class.extend({ xhr.send(profileText); }, + _assignFeatures: function(segment) { + if (segment.feature.properties.messages) { + let featureMessages = segment.feature.properties.messages, + segmentLatLngs = segment.getLatLngs(), + segmentLength = segmentLatLngs.length, + featureSegmentIndex = 0; + + for (let mi = 1; mi < featureMessages.length; mi++) { + var featureLatLng = this._getFeatureLatLng(featureMessages[mi]); + + for (let fi = featureSegmentIndex; fi < segmentLength; fi++) { + let segmentLatLng = segmentLatLngs[fi], + featureMessage = featureMessages[mi]; + + segmentLatLng.feature = this._getFeature(featureMessage); + segmentLatLng.message = featureMessage; + + if (featureLatLng.equals(segmentLatLngs[fi])) { + featureSegmentIndex = fi + 1; + break; + } + } + } + } + return segment; + }, + + _getFeature: function(featureMessage) { + //["Longitude", "Latitude", "Elevation", "Distance", "CostPerKm", "ElevCost", "TurnCost", "NodeCost", "InitialCost", "WayTags", "NodeTags"] + return { + cost: { + perKm: parseInt(featureMessage[4]), + elev: parseInt(featureMessage[5]), + turn: parseInt(featureMessage[6]), + node: parseInt(featureMessage[7]), + initial: parseInt(featureMessage[8]) + }, + wayTags: featureMessage[9], + nodeTags: featureMessage[10] + }; + }, + + _getFeatureLatLng: function(message) { + var lon = message[0] / 1000000, + lat = message[1] / 1000000; + + return L.latLng(lat, lon); + }, + _handleProfileResponse: function(xhr, cb) { var response; diff --git a/locales/de.json b/locales/de.json index 3c85020..d53e30f 100644 --- a/locales/de.json +++ b/locales/de.json @@ -97,6 +97,7 @@ "osm": "OpenStreetMap", "osmde": "OpenStreetMap.de", "outdoors": "Outdoor (Thunderforest)", + "route-quality": "Routenqualitätscodierung", "stamen-terrain": "Terrain (Stamen)", "strava-segments": "Strava Segmente", "topo": "OpenTopoMap" @@ -113,6 +114,9 @@ "opacity-slider": "Transparenz von Route und Markern anpassen", "privacy": "Datenschutz", "reverse-route": "Route umkehren", + "route-quality-altitude": "Höhencodierung", + "route-quality-cost": "Kostencodierung", + "route-quality-incline": "Steigungscodierung", "strava-biking": "Zeige Strava Radfahrsegmente", "strava-running": "Zeige Strava Läufersegmente", "zoomInTitle": "Hineinzoomen", diff --git a/locales/en.json b/locales/en.json index 0c4777e..f6eda4b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -97,6 +97,7 @@ "osm": "OpenStreetMap", "osmde": "OpenStreetMap.de", "outdoors": "Outdoors (Thunderforest)", + "route-quality": "Route quality coding", "stamen-terrain": "Terrain (Stamen)", "strava-segments": "Strava segments", "topo": "OpenTopoMap" @@ -113,6 +114,9 @@ "opacity-slider": "Set transparency of route track and markers", "privacy": "Privacy", "reverse-route": "Reverse route", + "route-quality-altitude": "Altitude coding", + "route-quality-cost": "Cost coding", + "route-quality-incline": "Incline coding", "strava-biking": "Show Strava biking segments", "strava-running": "Show Strava running segments", "zoomInTitle": "Zoom in", diff --git a/package.json b/package.json index 379e6e0..63a0e6f 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "leaflet-easybutton": "*", "leaflet-editable": "^1.1.0", "leaflet-elevation": "nrenner/Leaflet.Elevation#dev", + "leaflet-hotline": "^0.4.0", "leaflet-filelayer": "^1.2.0", "leaflet-plugins": "~3.0.0", "leaflet-providers": "^1.5.0", diff --git a/yarn.lock b/yarn.lock index 1e93e8a..f4a658b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4453,6 +4453,11 @@ leaflet-filelayer@^1.2.0: resolved "https://registry.yarnpkg.com/leaflet-filelayer/-/leaflet-filelayer-1.2.0.tgz#9f822e68a06072b0b0a8f328ba9419ba96bbccb1" integrity sha512-H3HrOOM9bpkrRUacdnWISV0MKZXLBYsX24H4XV+55QbcGCvd9In6oPzANEnhsokHAwNWd9qP6GfiHEFCfn+qkA== +leaflet-hotline@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/leaflet-hotline/-/leaflet-hotline-0.4.0.tgz#e01069836a9d2e2c78b1fa1db2013bd03c8ff8d9" + integrity sha1-4BBpg2qdLix4sfodsgE70DyP+Nk= + leaflet-plugins@~3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/leaflet-plugins/-/leaflet-plugins-3.0.3.tgz#7c727ac79a37636b245dd1adc64e10c61b425864" From 8d0dda51751bece46f85054e436288216d7e47f0 Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 1 Oct 2019 15:58:27 +0200 Subject: [PATCH 2/5] Added license information --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d2abddb..f5876c5 100644 --- a/README.md +++ b/README.md @@ -184,3 +184,5 @@ Copyright (c) 2018 Norbert Renner and [contributors](https://github.com/nrenner/ Copyright (c) 2012 Makina Corpus; [MIT License](https://github.com/makinacorpus/Leaflet.FileLayer/blob/master/LICENSE) - [togeojson](https://github.com/mapbox/togeojson) Copyright (c) 2016 Mapbox All rights reserved.; [2-clause BSD License](https://github.com/mapbox/togeojson/blob/master/LICENSE) +- [Leaflet.hotline](https://github.com/iosphere/Leaflet.hotline) + Copyright (c) 2015, iosphere GmbH, Jonas Coch; [Leaflet.hotline](https://github.com/iosphere/Leaflet.hotline/blob/master/LICENSE) From 023e30073d43f808bf56b0c92fe2b7eb5c2f5764 Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 1 Oct 2019 21:09:45 +0200 Subject: [PATCH 3/5] Incline is now the first option, cost the last --- js/plugin/RoutingPathQuality.js | 70 +++++++++++++++++---------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/js/plugin/RoutingPathQuality.js b/js/plugin/RoutingPathQuality.js index 645cfa1..f3e3430 100644 --- a/js/plugin/RoutingPathQuality.js +++ b/js/plugin/RoutingPathQuality.js @@ -1,5 +1,7 @@ BR.RoutingPathQuality = L.Control.extend({ - initialize: function(map, layersControl) { + initialize: function(map, layersControl, options) { + L.setOptions(this, options); + // hotline uses canvas and cannot be moved in front of the svg, so we create another pane map.createPane('routingQualityPane'); map.getPane('routingQualityPane').style.zIndex = 450; @@ -10,38 +12,6 @@ BR.RoutingPathQuality = L.Control.extend({ layersControl.addOverlay(this._routingSegments, i18next.t('map.layer.route-quality')); this.providers = { - cost: { - title: i18next.t('map.route-quality-cost'), - icon: 'fa-usd', - provider: new HotlineProvider({ - hotlineOptions: { - renderer: renderer - }, - valueFunction: function(latLng) { - let feature = latLng.feature; - return ( - feature.cost.perKm + - feature.cost.elev + - feature.cost.turn + - feature.cost.node + - feature.cost.initial - ); - } - }) - }, - altitude: { - title: i18next.t('map.route-quality-altitude'), - icon: 'fa-area-chart', - provider: new HotlineProvider({ - hotlineOptions: { - renderer: renderer - }, - valueFunction: function(latLng) { - feature = latLng.feature; - return latLng.alt; - } - }) - }, incline: { title: i18next.t('map.route-quality-incline'), icon: 'fa-line-chart', @@ -65,9 +35,41 @@ BR.RoutingPathQuality = L.Control.extend({ return (Math.atan(deltaAltitude / distance) * 180) / Math.PI; } }) + }, + altitude: { + title: i18next.t('map.route-quality-altitude'), + icon: 'fa-area-chart', + provider: new HotlineProvider({ + hotlineOptions: { + renderer: renderer + }, + valueFunction: function(latLng) { + feature = latLng.feature; + return latLng.alt; + } + }) + }, + cost: { + title: i18next.t('map.route-quality-cost'), + icon: 'fa-usd', + provider: new HotlineProvider({ + hotlineOptions: { + renderer: renderer + }, + valueFunction: function(latLng) { + let feature = latLng.feature; + return ( + feature.cost.perKm + + feature.cost.elev + + feature.cost.turn + + feature.cost.node + + feature.cost.initial + ); + } + }) } }; - this.selectedProvider = this.options.initialProvider || 'cost'; + this.selectedProvider = this.options.initialProvider || 'incline'; }, onAdd: function(map) { From 544aab07163fb5cc5984bd73594f6654103176ff Mon Sep 17 00:00:00 2001 From: Matzepan Date: Tue, 8 Oct 2019 19:30:29 +0200 Subject: [PATCH 4/5] Fixed problems with gulp-uglify --- js/plugin/RoutingPathQuality.js | 75 ++++++++++++++++----------------- js/router/BRouter.js | 14 +++--- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/js/plugin/RoutingPathQuality.js b/js/plugin/RoutingPathQuality.js index f3e3430..9c7af1d 100644 --- a/js/plugin/RoutingPathQuality.js +++ b/js/plugin/RoutingPathQuality.js @@ -15,7 +15,7 @@ BR.RoutingPathQuality = L.Control.extend({ incline: { title: i18next.t('map.route-quality-incline'), icon: 'fa-line-chart', - provider: new HotlineProvider({ + provider: new HotLineQualityProvider({ hotlineOptions: { min: -15, max: 15, @@ -27,7 +27,7 @@ BR.RoutingPathQuality = L.Control.extend({ renderer: renderer }, valueFunction: function(latLng, prevLatLng) { - var deltaAltitude = latLng.alt - prevLatLng.alt, // in m + const deltaAltitude = latLng.alt - prevLatLng.alt, // in m distance = prevLatLng.distanceTo(latLng); // in m if (distance === 0) { return 0; @@ -39,12 +39,11 @@ BR.RoutingPathQuality = L.Control.extend({ altitude: { title: i18next.t('map.route-quality-altitude'), icon: 'fa-area-chart', - provider: new HotlineProvider({ + provider: new HotLineQualityProvider({ hotlineOptions: { renderer: renderer }, valueFunction: function(latLng) { - feature = latLng.feature; return latLng.alt; } }) @@ -52,12 +51,12 @@ BR.RoutingPathQuality = L.Control.extend({ cost: { title: i18next.t('map.route-quality-cost'), icon: 'fa-usd', - provider: new HotlineProvider({ + provider: new HotLineQualityProvider({ hotlineOptions: { renderer: renderer }, valueFunction: function(latLng) { - let feature = latLng.feature; + const feature = latLng.feature; return ( feature.cost.perKm + feature.cost.elev + @@ -77,14 +76,14 @@ BR.RoutingPathQuality = L.Control.extend({ this._map = map; this._routingSegments.addTo(map); - let states = []; - var i, + var states = [], + i, keys = Object.keys(this.providers), l = keys.length; for (i = 0; i < l; ++i) { - let provider = this.providers[keys[i]]; - let nextState = keys[(i + 1) % l]; + const provider = this.providers[keys[i]]; + const nextState = keys[(i + 1) % l]; states.push({ stateName: keys[i], icon: provider.icon, @@ -119,56 +118,56 @@ BR.RoutingPathQuality = L.Control.extend({ _update: function(segments) { this._routingSegments.clearLayers(); - let layers = this.providers[this.selectedProvider].provider.computeLayers(segments); + const layers = this.providers[this.selectedProvider].provider.computeLayers(segments); if (layers) { - for (let i = 0; i < layers.length; i++) { + for (var i = 0; i < layers.length; i++) { this._routingSegments.addLayer(layers[i]); } } } }); -class HotlineProvider { - constructor(options) { +var HotLineQualityProvider = L.Class.extend({ + initialize: function(options) { this.hotlineOptions = options.hotlineOptions; this.valueFunction = options.valueFunction; - } + }, - computeLayers(segments) { - let layers = []; + computeLayers: function(segments) { + var layers = []; if (segments) { - let segmentLatLngs = []; - for (let i = 0; segments && i < segments.length; i++) { - let segment = segments[i]; + var segmentLatLngs = []; + for (var i = 0; segments && i < segments.length; i++) { + const segment = segments[i]; segmentLatLngs.push(this._computeLatLngVals(segment)); } - let flatLines = segmentLatLngs.flat(); + const flatLines = segmentLatLngs.flat(); if (flatLines.length > 0) { - let hotlineOptions = Object.assign(new Object(), this.hotlineOptions); + const hotlineOptions = Object.assign(new Object(), this.hotlineOptions); if (!hotlineOptions.min && !hotlineOptions.max) { - let minMax = this._calcMinMaxValues(flatLines); + const minMax = this._calcMinMaxValues(flatLines); hotlineOptions.min = minMax.min; hotlineOptions.max = minMax.max; } - for (let i = 0; i < segmentLatLngs.length; i++) { + for (var i = 0; i < segmentLatLngs.length; i++) { const line = segmentLatLngs[i]; - let hotline = L.hotline(line, hotlineOptions); + const hotline = L.hotline(line, hotlineOptions); layers.push(hotline); } } } return layers; - } + }, - _computeLatLngVals(segment) { - let latLngVals = [], + _computeLatLngVals: function(segment) { + var latLngVals = [], segmentLatLngs = segment.getLatLngs(), segmentLength = segmentLatLngs.length; - for (let i = 0; i < segmentLength; i++) { - var val = this.valueFunction.call( + for (var i = 0; i < segmentLength; i++) { + const val = this.valueFunction.call( this, segmentLatLngs[i], segmentLatLngs[Math.max(i - 1, 0)], @@ -178,17 +177,17 @@ class HotlineProvider { latLngVals.push(this._convertToArray(segmentLatLngs[i], val)); } return latLngVals; - } + }, - _convertToArray(latLng, val) { + _convertToArray: function(latLng, val) { return [latLng.lat, latLng.lng, val]; - } + }, - _calcMinMaxValues(lines) { - let min = lines[0][2], + _calcMinMaxValues: function(lines) { + var min = lines[0][2], max = min; - for (let i = 1; lines && i < lines.length; i++) { - let line = lines[i]; + for (var i = 1; lines && i < lines.length; i++) { + const line = lines[i]; max = Math.max(max, line[2]); min = Math.min(min, line[2]); } @@ -200,4 +199,4 @@ class HotlineProvider { max: max }; } -} +}); diff --git a/js/router/BRouter.js b/js/router/BRouter.js index b6e52da..1b65d2f 100644 --- a/js/router/BRouter.js +++ b/js/router/BRouter.js @@ -192,16 +192,16 @@ L.BRouter = L.Class.extend({ _assignFeatures: function(segment) { if (segment.feature.properties.messages) { - let featureMessages = segment.feature.properties.messages, + const featureMessages = segment.feature.properties.messages, segmentLatLngs = segment.getLatLngs(), - segmentLength = segmentLatLngs.length, - featureSegmentIndex = 0; + segmentLength = segmentLatLngs.length; + var featureSegmentIndex = 0; - for (let mi = 1; mi < featureMessages.length; mi++) { - var featureLatLng = this._getFeatureLatLng(featureMessages[mi]); + for (var mi = 1; mi < featureMessages.length; mi++) { + const featureLatLng = this._getFeatureLatLng(featureMessages[mi]); - for (let fi = featureSegmentIndex; fi < segmentLength; fi++) { - let segmentLatLng = segmentLatLngs[fi], + for (var fi = featureSegmentIndex; fi < segmentLength; fi++) { + const segmentLatLng = segmentLatLngs[fi], featureMessage = featureMessages[mi]; segmentLatLng.feature = this._getFeature(featureMessage); From 3a0a4498c2b15b5387d35604b9cb9afddea96a89 Mon Sep 17 00:00:00 2001 From: Matzepan Date: Thu, 10 Oct 2019 19:25:51 +0200 Subject: [PATCH 5/5] moved button above opacity slider --- js/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/index.js b/js/index.js index 4e28628..3e82757 100644 --- a/js/index.js +++ b/js/index.js @@ -247,7 +247,7 @@ } routing.addTo(map); - routingPathQuality.addTo(map); + elevation.addBelow(map); sidebar = BR.sidebar({ @@ -277,6 +277,8 @@ BR.tracksLoader(map, layersControl, routing); + routingPathQuality.addTo(map); + map.addControl( new BR.OpacitySliderControl({ id: 'route',