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.
228 lines
15 KiB
JavaScript
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);
|
|
})();
|