Added hotline based route overlay

Added a overlay which reflects the quality of the route based on either:
* cost
* altitude
* incline
This commit is contained in:
Unknown 2019-10-01 15:46:09 +02:00 committed by Matzepan
parent 90dfa5b7f3
commit 9eeca7e2d5
7 changed files with 269 additions and 1 deletions

View file

@ -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({

View file

@ -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
};
}
}

View file

@ -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;

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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"