brouter-web: dual-sided route coloring (slope offset-left, surface offset-right)

Per-segment offset polylines in pixels, rebuilt on zoom; surface from per-segment WayTags
(messages[i][9]), slope from point altitudes. Centre keeps the neutral black/white casing.
v1 — offset width/colours to tune visually.
This commit is contained in:
4aiman 2026-10-27 14:28:02 +03:00
commit 1df97d3e73

View file

@ -131,6 +131,68 @@
var d = prev.distanceTo(cur); if (!d) return 0;
return 100 * (((cur.alt) || 0) - ((prev.alt) || 0)) / d; // SIGNED grade %
}
// --- Dual-sided route coloring: SLOPE offset to the LEFT, SURFACE offset to the RIGHT ---
// (task: see gradient AND surface at once). Surface comes from the per-segment WayTags in
// feature.properties.messages[i][9]; offset is in pixels, recomputed on zoom.
var SURF_COL = {
asphalt: "#455a64", concrete: "#455a64", paved: "#455a64", paving_stones: "#455a64",
compacted: "#6d4c41", ground: "#8d6e63", dirt: "#8d6e63", earth: "#8d6e63", soil: "#8d6e63",
cobblestone: "#78909c", sett: "#78909c", gravel: "#b0bec5", fine_gravel: "#b0bec5", pebblestone: "#b0bec5",
clay: "#a1887f", grass: "#7cb342", grass_paver: "#7cb342", wood: "#a1887f",
sand: "#ffca28", mud: "#ff7043"
};
function surfColor(wayTags) {
var m = /(?:^|\s)surface=([a-z_]+)/.exec(wayTags || "");
return (m && SURF_COL[m[1]]) || "#9e9e9e"; // grey = unknown surface
}
function slopeBandColor(gpct) { var i = Math.min(10, Math.floor(Math.abs(gpct || 0))); return gpct < 0 ? DOWN[i] : UP[i]; }
function offsetLL(map, lls, px) {
var pts = [], i;
for (i = 0; i < lls.length; i++) pts.push(map.latLngToLayerPoint(lls[i]));
var out = [];
for (i = 0; i < pts.length; i++) {
var a = pts[Math.max(0, i - 1)], b = pts[Math.min(pts.length - 1, i + 1)];
var dx = b.x - a.x, dy = b.y - a.y, len = Math.sqrt(dx * dx + dy * dy) || 1;
out.push(map.layerPointToLatLng(L.point(pts[i].x + (-dy / len) * px, pts[i].y + (dx / len) * px)));
}
return out;
}
var dualData = null, dualGroup = null, dualHooked = false;
function buildDual() {
try {
var map = window.BR && BR.__map;
if (!map || !dualData || !dualData.length) return;
if (dualGroup) { map.removeLayer(dualGroup); dualGroup = null; }
var g = L.layerGroup(), OFF = 5;
dualData.forEach(function (seg) {
var lls = seg.lls, msgs = seg.msgs;
if (!lls || lls.length < 2) return;
var L1 = offsetLL(map, lls, -OFF), R1 = offsetLL(map, lls, OFF);
for (var i = 0; i < lls.length - 1; i++) {
var d = lls[i].distanceTo(lls[i + 1]) || 1;
var a0 = lls[i].alt, a1 = lls[i + 1].alt;
var gpct = (a0 == null || a1 == null) ? 0 : 100 * (a1 - a0) / d;
L.polyline([L1[i], L1[i + 1]], { color: slopeBandColor(gpct), weight: 3, opacity: 0.95, interactive: false, lineCap: "butt" }).addTo(g);
var wt = (msgs && msgs[i]) ? msgs[i][9] : "";
L.polyline([R1[i], R1[i + 1]], { color: surfColor(wt), weight: 3, opacity: 0.95, interactive: false, lineCap: "butt" }).addTo(g);
}
});
g.addTo(map); dualGroup = g;
} catch (e) {}
}
function captureDual(segments) {
dualData = [];
for (var i = 0; segments && i < segments.length; i++) {
var seg = segments[i], r = seg && seg._routing;
if (r && r.beeline) continue;
if (!seg || !seg.getLatLngs) continue;
var msgs = (seg.feature && seg.feature.properties && seg.feature.properties.messages) || null;
dualData.push({ lls: seg.getLatLngs(), msgs: msgs ? msgs.slice(1) : null }); // drop header row
}
if (!dualHooked && window.BR && BR.__map) { dualHooked = true; BR.__map.on("zoomend", buildDual); }
buildDual();
}
// White+black casing under the colored line so it reads on any background. leaflet-hotline
// has only ONE outlineColor, so we render the colored core thin (no built-in outline) and
// add two plain polylines beneath it in a dedicated pane below the hotline pane: a wide
@ -175,10 +237,11 @@
// Prepend black+white casings to whatever hotlines the provider computes (added first
// = under the colored line). Wrap once.
if (!pr.__baldavCased && typeof pr.computeLayers === "function") {
var orig = pr.computeLayers.bind(pr);
pr.computeLayers = function (segments) {
var hotlines = orig(segments);
try { return makeCasings(segments).concat(hotlines); } catch (e) { return hotlines; }
// dual-sided: slope offset-left + surface offset-right (managed on the map itself,
// rebuilt on zoom); the overlay keeps only the neutral black/white centre casing.
try { captureDual(segments); } catch (e) {}
try { return makeCasings(segments); } catch (e) { return []; }
};
pr.__baldavCased = true;
}