baldav/tools/brouter-web/baldav-custom.js
4aiman d504315229 brouter-web: white+black casing under the slope-colored route
leaflet-hotline has a single outlineColor, so render the colored core thin
(outlineWidth 0) and add two plain polylines beneath it in a dedicated pane
just under the hotline pane: a wide black one and a narrower white one — black
rim, white rim, colored core, legible on any map background. Map captured via
L.Map addInitHook to build the casing pane.
2026-09-10 00:17:42 +03:00

228 lines
15 KiB
JavaScript

(function () {
var LS_DARK = "baldav_bw_dark";
function isDark() { try { return localStorage.getItem(LS_DARK) === "1"; } catch (e) { return false; } }
// Inject the dark rules as a <style> in <head> (no dependency on an external CSS file,
// and it covers elements created later — e.g. the elevation chart, drawn only once a
// route exists). Toggling body.bw-dark activates/deactivates everything.
function injectStyle() {
if (document.getElementById("baldav-dark-style")) return;
var st = document.createElement("style");
st.id = "baldav-dark-style";
st.textContent = [
"body.bw-dark .leaflet-tile-pane{filter:invert(1) hue-rotate(180deg) brightness(1.5)}",
"body.bw-dark .leaflet-container{background:#1b1f24}",
// right sidebar / layers panel
"body.bw-dark #sidebar,body.bw-dark #sidebar .leaflet-sidebar-content,body.bw-dark .leaflet-sidebar-pane,body.bw-dark .leaflet-control-layers,body.bw-dark .leaflet-control-layers-expanded{background:#20242a!important;color:#dfe3e8!important}",
"body.bw-dark #sidebar label,body.bw-dark #sidebar span,body.bw-dark #sidebar a,body.bw-dark #sidebar h1,body.bw-dark #sidebar h2{color:#dfe3e8!important}",
"body.bw-dark #sidebar input[type=text],body.bw-dark #sidebar select{background:#2b3038!important;color:#dfe3e8!important;border-color:#454b54!important}",
// left toolbar + all leaflet control buttons (zoom, draw, etc.)
"body.bw-dark .leaflet-bar a,body.bw-dark .leaflet-control-zoom a,body.bw-dark .leaflet-control a,body.bw-dark .leaflet-control button{background:#2b3038!important;color:#fff!important;border-color:#454b54!important}",
"body.bw-dark .leaflet-bar a:hover,body.bw-dark .leaflet-control a:hover{background:#3a4048!important}",
// bottom elevation profile panel + footer/stats
"body.bw-dark #elevation-chart,body.bw-dark #elevation-chart svg,body.bw-dark .elevation,body.bw-dark .elevation svg{background:#20242a!important;color:#dfe3e8!important}",
"body.bw-dark .elevation .background{background-color:#20242a!important}",
"body.bw-dark .elevation rect.background,body.bw-dark .elevation .background rect{fill:#20242a!important}",
"body.bw-dark footer,body.bw-dark #stats-info,body.bw-dark #stats-container,body.bw-dark #stats,body.bw-dark #track_statistics{background:#20242a!important;color:#dfe3e8!important}",
"body.bw-dark .elevation text,body.bw-dark .elevation .axis text{fill:#dfe3e8!important}",
"body.bw-dark .elevation .domain,body.bw-dark .elevation .tick line,body.bw-dark .elevation .grid line{stroke:#55606a!important}",
"body.bw-dark .elevation path.area,body.bw-dark .elevation .area{fill:#2b3038!important;opacity:.6}",
// elevation SVG axis text (was black on dark)
"body.bw-dark #elevation-chart text,body.bw-dark #elevation-chart .tick text,body.bw-dark #elevation-chart .axis text{fill:#dfe3e8!important}",
"body.bw-dark #elevation-chart .tick line,body.bw-dark #elevation-chart .domain,body.bw-dark #elevation-chart path.grid,body.bw-dark #elevation-chart line{stroke:#3a4048!important}",
// hover tooltip on the elevation chart (.focusbox): its rect is white and our
// .elevation text rule turned the text light — light-on-light, unreadable. Give the
// box a dark ground and keep the text light. Same for the on-line height label.
"body.bw-dark .focusbox rect{fill:rgba(32,36,42,.92)!important;stroke:#667!important}",
"body.bw-dark .focusbox,body.bw-dark .focusbox text{fill:#dfe3e8!important}",
"body.bw-dark .height-focus.label rect{fill:rgba(32,36,42,.9)!important;stroke:#667!important}",
"body.bw-dark .height-focus.label text{fill:#dfe3e8!important}",
// guide lines/markers were near-black (#141414) — invisible on the dark chart
"body.bw-dark .focusLine line,body.bw-dark .height-focus.line{stroke:#aab4be!important}",
"body.bw-dark .height-focus.circle{stroke:#20242a!important}",
// lift the on-map hover box above the route so it doesn't cover the line (both themes)
".height-focus.label{transform:translateY(-42px)}",
// right vertical sidebar tab strip (layers/settings/data/stats icons)
"body.bw-dark .leaflet-sidebar-tabs,body.bw-dark .leaflet-sidebar-tabs>ul,body.bw-dark .leaflet-sidebar-tabs li,body.bw-dark .leaflet-sidebar-tabs li a{background:#20242a!important;color:#dfe3e8!important}",
"body.bw-dark .leaflet-sidebar-tabs li.active a,body.bw-dark .leaflet-sidebar-tabs li a:hover{background:#2b3038!important;color:#fff!important}",
"body.bw-dark .leaflet-sidebar.collapsed,body.bw-dark .leaflet-sidebar-header{background:#20242a!important;color:#dfe3e8!important}",
// Bootstrap form controls: search fields, the profile dropdown/select, etc.
"body.bw-dark input,body.bw-dark textarea,body.bw-dark select,body.bw-dark .form-control,body.bw-dark .custom-select{background:#2b3038!important;color:#dfe3e8!important;border-color:#454b54!important}",
"body.bw-dark ::placeholder{color:#9aa4ae!important}",
// dropdown menus (Load, profile chooser)
"body.bw-dark .dropdown-menu{background:#20242a!important;color:#dfe3e8!important;border-color:#454b54!important}",
"body.bw-dark .dropdown-item{color:#dfe3e8!important}",
"body.bw-dark .dropdown-item:hover,body.bw-dark .dropdown-item:focus{background:#2b3038!important;color:#fff!important}",
// light-styled buttons (incl. those above the elevation chart) → dark; keep coloured btns
"body.bw-dark .btn-light,body.bw-dark .btn-outline-secondary,body.bw-dark .btn-secondary,body.bw-dark .btn-default,body.bw-dark button.btn:not([class*=btn-primary]):not([class*=btn-success]):not([class*=btn-danger]){background:#2b3038!important;color:#dfe3e8!important;border-color:#454b54!important}",
"body.bw-dark .card,body.bw-dark .modal-content,body.bw-dark .list-group-item,body.bw-dark .popover,body.bw-dark .popover-body{background:#20242a!important;color:#dfe3e8!important}",
// brouter-web bottom footer buttons / export-image control
"body.bw-dark footer .btn,body.bw-dark footer button,body.bw-dark #export-to-image,body.bw-dark .leaflet-control button{background:#2b3038!important;color:#dfe3e8!important;border-color:#454b54!important}"
].join("\n");
(document.head || document.documentElement).appendChild(st);
}
function apply() {
document.body.classList.toggle("bw-dark", isDark());
var b = document.getElementById("baldav-daynight");
if (b) b.textContent = isDark() ? "☀ День" : "☾ Ночь";
}
function toggle() {
try { localStorage.setItem(LS_DARK, isDark() ? "0" : "1"); } catch (e) {}
apply();
}
// Standalone floating button — independent of the navbar so it can't be invisible or
// zero-width. Fixed bottom-right, explicit colours/size.
function addButton() {
if (document.getElementById("baldav-daynight")) return true;
var a = document.createElement("button");
a.id = "baldav-daynight";
a.type = "button";
a.textContent = isDark() ? "☀ День" : "☾ Ночь";
a.style.cssText = "position:fixed;bottom:24px;right:24px;z-index:100000;" +
"padding:8px 12px;font:600 14px sans-serif;background:#2b3038;color:#fff;" +
"border:1px solid #667;border-radius:8px;cursor:pointer;box-shadow:0 2px 6px rgba(0,0,0,.5)";
a.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); toggle(); });
document.body.appendChild(a);
return true;
}
var WANT = ["Cycling","Hiking","Bench","Toilets","Bank","ATM","Bar","BBQ","Cafe","Drinking water","Fast food","Food court","Pub","Restaurant","Bicycle parking","Bus station","Bakery","Beverages","Coffee","Supermarket","Picnic site","Viewpoint","Shelter","Route quality"];
function setDefaultOverlays() {
try { if (localStorage.getItem("baldav_bw_overlays_v1") === "1") return true; } catch (e) {}
var labels = document.querySelectorAll("#layers-control-wrapper label, .leaflet-control-layers-overlays label");
if (!labels.length) return false;
labels.forEach(function (lab) {
var t = (lab.textContent || "").trim();
if (!WANT.some(function (w) { return t.indexOf(w) !== -1; })) return;
var cb = lab.querySelector("input[type=checkbox]") || lab.control || (lab.htmlFor && document.getElementById(lab.htmlFor));
if (cb && cb.type === "checkbox" && !cb.checked) cb.click();
});
try { localStorage.setItem("baldav_bw_overlays_v1", "1"); } catch (e) {}
return true;
}
// --- Route colored by slope, BalDav palette (task: paint the route by gradient, not
// elevation). brouter-web's "Route quality" overlay uses a HotLineQualityProvider; its
// `incline` provider colors by SIGNED incline in degrees (blue→green→red). We repoint it
// at |grade %| with BalDav's discrete bands (green→yellow→red→brown→black, ≥10% black).
// The provider reads hotlineOptions.{min,max,palette} and valueFunction directly at draw
// time (computeLayers → L.hotline), so mutating them is enough. Registered via addInitHook
// BEFORE the control is constructed, so the very first draw already uses our palette.
var BANDS = ["#2e7d32","#7cb342","#aeea00","#ffeb3b","#ffc107","#ff9800","#ff5722","#e53935","#b71c1c","#6d4c41","#000000"];
function slopePalette() {
// step function over normalized [0,1] == grade 0..10%: band k holds across [k,k+1)%
var p = {}, eps = 0.0009;
for (var k = 0; k < 10; k++) { p[k / 10] = BANDS[k]; p[(k + 1) / 10 - eps] = BANDS[k]; }
p[1] = BANDS[10];
return p;
}
function slopeValue(cur, prev) {
if (!cur || !prev) return 0;
var d = prev.distanceTo(cur); if (!d) return 0;
return Math.abs(100 * (((cur.alt) || 0) - ((prev.alt) || 0)) / d); // |grade %|
}
// 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
// black one and a narrower white one — black rim, white rim, colored core.
var CASE_PANE = "baldavCasingPane";
function ensureCasingPane() {
var m = window.BR && BR.__map;
if (!m) return null;
if (!m.getPane(CASE_PANE)) {
m.createPane(CASE_PANE);
var p = m.getPane(CASE_PANE);
if (p) p.style.zIndex = 449; // routingQualityPane is 450 → casings sit just under it
}
return m.getPane(CASE_PANE) ? CASE_PANE : null;
}
function makeCasings(segments) {
var pane = ensureCasingPane(), out = [];
for (var i = 0; segments && i < segments.length; i++) {
var seg = segments[i], r;
if (!seg || !seg.getLatLngs) continue;
if (null !== (r = seg._routing) && void 0 !== r && r.beeline) continue; // skip beelines, as brouter does
var lls = seg.getLatLngs();
var base = { opacity: 1, interactive: false, lineCap: "round", lineJoin: "round" };
if (pane) base.pane = pane;
out.push(L.polyline(lls, L.extend({}, base, { color: "#000000", weight: 12 })));
out.push(L.polyline(lls, L.extend({}, base, { color: "#ffffff", weight: 8 })));
}
return out;
}
function applySlopeProvider(ctrl) {
try {
var pr = ctrl && ctrl.providers && ctrl.providers.incline && ctrl.providers.incline.provider;
if (!pr || !pr.hotlineOptions) return false;
pr.hotlineOptions.min = 0;
pr.hotlineOptions.max = 10;
pr.hotlineOptions.palette = slopePalette();
pr.hotlineOptions.weight = 4; // thin colored core over the casings
pr.hotlineOptions.outlineWidth = 0; // no built-in outline; casings provide it
pr.valueFunction = slopeValue;
ctrl._initialProvider = "incline";
ctrl.selectedProvider = "incline";
// 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; }
};
pr.__baldavCased = true;
}
return true;
} catch (e) { return false; }
}
var rqHooked = false, mapHooked = false;
function registerRouteQuality() {
// Capture the map instance as it's created (needed to make the casing pane).
if (!mapHooked && window.L && L.Map && L.Map.addInitHook) {
mapHooked = true;
L.Map.addInitHook(function () { window.BR = window.BR || {}; BR.__map = this; });
}
if (rqHooked) return;
if (!(window.BR && BR.RoutingPathQuality && BR.RoutingPathQuality.addInitHook)) return;
rqHooked = true;
BR.RoutingPathQuality.addInitHook(function () { applySlopeProvider(this); BR.__quality = this; });
}
// Best-effort: auto-enable the "Route quality" overlay so the route shows slope out of the
// box. Its checkbox label is localized (locale files), so match a few languages; if it
// isn't found the palette override still applies once the user turns it on (button / key C).
function enableRouteQuality() {
try { if (localStorage.getItem("baldav_bw_rq_v1") === "1") return true; } catch (e) {}
var labels = document.querySelectorAll("#layers-control-wrapper label, .leaflet-control-layers-overlays label");
if (!labels.length) return false;
var re = /route.?quality|streckenqualit|качеств/i, done = false;
labels.forEach(function (lab) {
if (!re.test((lab.textContent || ""))) return;
var cb = lab.querySelector("input[type=checkbox]") || lab.control || (lab.htmlFor && document.getElementById(lab.htmlFor));
if (cb && cb.type === "checkbox" && !cb.checked) { cb.click(); }
done = true;
});
if (done) { try { localStorage.setItem("baldav_bw_rq_v1", "1"); } catch (e) {} }
return done;
}
function init() {
registerRouteQuality();
injectStyle();
apply();
var tries = 0;
var iv = setInterval(function () {
tries++;
var b = addButton(); var o = setDefaultOverlays();
// If the control got built before our hook (shouldn't, but be safe), patch it live.
if (window.BR && BR.__quality && !BR.__quality.__baldavSlope) {
if (applySlopeProvider(BR.__quality)) BR.__quality.__baldavSlope = true;
}
var q = enableRouteQuality();
if ((b && o && q) || tries > 60) clearInterval(iv);
}, 500);
}
// Register the route-quality hook AT SCRIPT-EXECUTION TIME (bw.js has already defined
// BR.RoutingPathQuality by now, and the control is only constructed later on DOM ready) —
// so the slope palette is in place before its first draw.
registerRouteQuality();
if (document.readyState !== "loading") init(); else document.addEventListener("DOMContentLoaded", init);
})();