Currently it is a bit difficult to guess the slope of the route for planning purposes, since even for routes in very hilly terrain the colors hardly change. Only extremely steep hills are indicated, and there is no visual difference between going uphill or downhill. By cutting off earlier, more of the route will show meaningful differences in color. Note that BRouter's gradients are already averaged compared to the maximum gradients shown on road signs, so 15% should be a good compromise (anything steeper is difficult to ride on for longer periods anyway, and rightly deserves to be colored with an alarming red). By using different colors for the min and max parameters, uphill and downhill sections should now be easy to distinguish. By introducing two more color stops, the gradient becomes much smoother, with flat sections featuring a distinctive green, where they were drawn in a muddy dark green before. Note that this approach had been implemented like this in QLandkarte GT for several years now, with great success.
254 lines
8.5 KiB
JavaScript
254 lines
8.5 KiB
JavaScript
BR.RoutingPathQuality = L.Control.extend({
|
|
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;
|
|
map.getPane('routingQualityPane').style.pointerEvents = 'none';
|
|
var renderer = new L.Hotline.Renderer({ pane: 'routingQualityPane' });
|
|
|
|
this._routingSegments = L.featureGroup();
|
|
this._routingSegments.id = 'route-quality'; // for URL hash instead of language name
|
|
layersControl.addOverlay(this._routingSegments, i18next.t('map.layer.route-quality'));
|
|
|
|
this.providers = {
|
|
incline: {
|
|
title: i18next.t('map.route-quality-incline'),
|
|
icon: 'fa-line-chart',
|
|
provider: new HotLineQualityProvider({
|
|
hotlineOptions: {
|
|
min: -8.5,
|
|
max: 8.5, // angle in degree, == 15% incline
|
|
palette: {
|
|
0.0: '#0000ff', // blue
|
|
0.25: '#00ffff', // cyan
|
|
0.5: '#00ff00', // green
|
|
0.75: '#ffff00', // yellow
|
|
1.0: '#ff0000' // red
|
|
},
|
|
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;
|
|
}
|
|
})
|
|
},
|
|
altitude: {
|
|
title: i18next.t('map.route-quality-altitude'),
|
|
icon: 'fa-area-chart',
|
|
provider: new HotLineQualityProvider({
|
|
hotlineOptions: {
|
|
renderer: renderer
|
|
},
|
|
valueFunction: function(latLng) {
|
|
return latLng.alt;
|
|
}
|
|
})
|
|
},
|
|
cost: {
|
|
title: i18next.t('map.route-quality-cost'),
|
|
icon: 'fa-usd',
|
|
provider: new HotLineQualityProvider({
|
|
hotlineOptions: {
|
|
renderer: renderer
|
|
},
|
|
valueFunction: function(latLng) {
|
|
var feature = latLng.feature;
|
|
return (
|
|
feature.cost.perKm +
|
|
feature.cost.elev +
|
|
feature.cost.turn +
|
|
feature.cost.node +
|
|
feature.cost.initial
|
|
);
|
|
}
|
|
})
|
|
}
|
|
};
|
|
this._initialProvider = this.options.initialProvider || 'incline';
|
|
this.selectedProvider = this._initialProvider;
|
|
|
|
this._active = false;
|
|
},
|
|
|
|
onAdd: function(map) {
|
|
this._map = map;
|
|
|
|
map.on(
|
|
'overlayadd',
|
|
function(evt) {
|
|
if (evt.layer === this._routingSegments) {
|
|
this._activate(this.routingPathButton);
|
|
}
|
|
},
|
|
this
|
|
);
|
|
map.on(
|
|
'overlayremove',
|
|
function(evt) {
|
|
if (evt.layer === this._routingSegments) {
|
|
this._deactivate(this.routingPathButton);
|
|
}
|
|
},
|
|
this
|
|
);
|
|
|
|
var states = [],
|
|
i,
|
|
keys = Object.keys(this.providers),
|
|
l = keys.length;
|
|
|
|
for (i = 0; i < l; ++i) {
|
|
var provider = this.providers[keys[i]];
|
|
var nextState = keys[(i + 1) % l];
|
|
states.push({
|
|
stateName: keys[i],
|
|
icon: provider.icon,
|
|
title: provider.title,
|
|
onClick: L.bind(function(state) {
|
|
return L.bind(function(btn) {
|
|
if (this._active) {
|
|
btn.state(state);
|
|
this.setProvider(state);
|
|
|
|
if (state === this._initialProvider) {
|
|
this._deactivate(btn);
|
|
} else {
|
|
this._getIcon(btn).classList.add('active');
|
|
}
|
|
} else {
|
|
this._activate(btn);
|
|
}
|
|
}, this);
|
|
}, this)(nextState)
|
|
});
|
|
}
|
|
|
|
this.routingPathButton = new L.easyButton({
|
|
states: states
|
|
}).addTo(map);
|
|
return new L.DomUtil.create('div');
|
|
},
|
|
|
|
_activate: function(btn) {
|
|
this._active = true;
|
|
this._getIcon(btn).classList.add('active');
|
|
this._routingSegments.addTo(this._map);
|
|
},
|
|
|
|
_deactivate: function(btn) {
|
|
this._active = false;
|
|
this._getIcon(btn).classList.remove('active');
|
|
this._map.removeLayer(this._routingSegments);
|
|
},
|
|
|
|
_getIcon: function(btn) {
|
|
return btn.button.firstChild.firstChild;
|
|
},
|
|
|
|
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);
|
|
},
|
|
|
|
_update: function(segments) {
|
|
this._routingSegments.clearLayers();
|
|
var layers = this.providers[this.selectedProvider].provider.computeLayers(segments);
|
|
if (layers) {
|
|
for (var i = 0; i < layers.length; i++) {
|
|
this._routingSegments.addLayer(layers[i]);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
var HotLineQualityProvider = L.Class.extend({
|
|
initialize: function(options) {
|
|
this.hotlineOptions = options.hotlineOptions;
|
|
this.valueFunction = options.valueFunction;
|
|
},
|
|
|
|
computeLayers: function(segments) {
|
|
var layers = [];
|
|
if (segments) {
|
|
var segmentLatLngs = [];
|
|
var flatLines = [];
|
|
for (var i = 0; segments && i < segments.length; i++) {
|
|
var segment = segments[i];
|
|
var vals = this._computeLatLngVals(segment);
|
|
segmentLatLngs.push(vals);
|
|
Array.prototype.push.apply(flatLines, vals);
|
|
}
|
|
|
|
if (flatLines.length > 0) {
|
|
var hotlineOptions = L.extend({}, this.hotlineOptions);
|
|
if (!hotlineOptions.min && !hotlineOptions.max) {
|
|
var minMax = this._calcMinMaxValues(flatLines);
|
|
hotlineOptions.min = minMax.min;
|
|
hotlineOptions.max = minMax.max;
|
|
}
|
|
|
|
for (var i = 0; i < segmentLatLngs.length; i++) {
|
|
var line = segmentLatLngs[i];
|
|
var hotline = L.hotline(line, hotlineOptions);
|
|
layers.push(hotline);
|
|
}
|
|
}
|
|
}
|
|
return layers;
|
|
},
|
|
|
|
_computeLatLngVals: function(segment) {
|
|
var latLngVals = [],
|
|
segmentLatLngs = segment.getLatLngs(),
|
|
segmentLength = segmentLatLngs.length;
|
|
|
|
for (var 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: function(latLng, val) {
|
|
return [latLng.lat, latLng.lng, val];
|
|
},
|
|
|
|
_calcMinMaxValues: function(lines) {
|
|
var min = lines[0][2],
|
|
max = min;
|
|
for (var i = 1; lines && i < lines.length; i++) {
|
|
var 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
|
|
};
|
|
}
|
|
});
|