@ -0,0 +1,528 @@
( 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 = [
// POI emoji markers (no default divIcon box); white halo so they read on any background
".baldav-poi-icon{background:none;border:0}" ,
".baldav-poi-icon span{display:block;width:22px;height:22px;line-height:22px;text-align:center;font-size:15px;filter:drop-shadow(0 0 1px #fff) drop-shadow(0 0 1px #fff)}" ,
".baldav-ov{display:inline-block;width:1.2em;text-align:center}" ,
"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.
// Directional: uphill through a WARM fan, downhill through a COOL fan (green->cyan->navy),
// like BalDav. Hotline value is SIGNED grade over normalized [0,1] with 0.5 = flat.
var UP = [ "#2e7d32" , "#7cb342" , "#aeea00" , "#ffeb3b" , "#ffc107" , "#ff9800" , "#ff5722" , "#e53935" , "#b71c1c" , "#6d4c41" , "#000000" ] ;
var DOWN = [ "#2e7d32" , "#2e9e5b" , "#26a69a" , "#00bcd4" , "#29b6f6" , "#1e88e5" , "#1976d2" , "#1565c0" , "#0d47a1" , "#0a357a" , "#001f5c" ] ;
function slopePalette ( ) {
// normalized n in [0,1] maps to grade g=(n-0.5)*20, so n=0 is -10% (steep descent),
// n=0.5 flat, n=1 +10% (steep climb). Step bands of 1% each side.
var p = { } , eps = 0.0004 ;
for ( var k = 0 ; k < 10 ; k ++ ) { p [ 0.5 + k / 20 ] = UP [ k ] ; p [ 0.5 + ( k + 1 ) / 20 - eps ] = UP [ k ] ; } // climb
p [ 1 ] = UP [ 10 ] ;
for ( var k = 0 ; k < 10 ; k ++ ) { p [ 0.5 - ( k + 1 ) / 20 + eps ] = DOWN [ k ] ; p [ 0.5 - k / 20 ] = DOWN [ k ] ; } // descent
p [ 0 ] = DOWN [ 10 ] ;
p [ 0.5 ] = "#2e7d32" ; // flat = green
return p ;
}
function slopeValue ( cur , prev ) {
if ( ! cur || ! prev ) return 0 ;
var d = prev . distanceTo ( cur ) ; if ( ! d ) return 0 ;
return 100 * ( ( ( cur . alt ) || 0 ) - ( ( prev . alt ) || 0 ) ) / d ; // SIGNED 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 = - 10 ;
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 ;
// Capture the native layers control as it is added to the map (addTo → addControl),
// so we can addOverlay() into brouter-web's own checkbox list instead of adding
// separate corner buttons. Any control exposing addOverlay()+_layers is the one.
var origAdd = this . addControl ;
this . addControl = function ( control ) {
try {
if ( control && typeof control . addOverlay === "function" && control . _layers && ! BR . _ _layersControl ) {
BR . _ _layersControl = control ;
}
} catch ( e ) { }
return origAdd . call ( this , control ) ;
} ;
} ) ;
}
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 ;
}
// --- copy coordinates of the map centre --------------------------------------------
// The map's own left-click adds a routing waypoint and right-click opens its menu, so we
// can't hijack either. Instead: a centre crosshair + a box showing the centre's lat,lon;
// click the box to copy. Pan the feature under the crosshair, click, paste into osm.org.
var coordHooked = false ;
function bwToast ( msg ) {
var d = document . createElement ( "div" ) ;
d . textContent = msg ;
d . style . cssText = "position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:100000;" +
"background:#222;color:#fff;padding:8px 14px;border-radius:6px;font:14px sans-serif;" +
"box-shadow:0 2px 8px rgba(0,0,0,.4);pointer-events:none;" ;
document . body . appendChild ( d ) ;
setTimeout ( function ( ) { d . remove ( ) ; } , 1800 ) ;
}
function copyCoord ( latlng ) {
var s = latlng . lat . toFixed ( 6 ) + ", " + latlng . lng . toFixed ( 6 ) ;
if ( navigator . clipboard && navigator . clipboard . writeText ) {
navigator . clipboard . writeText ( s ) . then (
function ( ) { bwToast ( "Скопировано: " + s ) ; } ,
function ( ) { window . prompt ( "Координаты (Ctrl+C):" , s ) ; } ) ;
} else { window . prompt ( "Координаты (Ctrl+C):" , s ) ; }
}
function hookCoordControl ( ) {
if ( coordHooked ) return true ;
var m = window . BR && BR . _ _map ;
if ( ! m || ! window . L ) return false ;
var cross = document . createElement ( "div" ) ;
cross . style . cssText = "position:absolute;left:50%;top:50%;width:16px;height:16px;margin:-8px 0 0 -8px;" +
"z-index:600;pointer-events:none;background:" +
"linear-gradient(#e33,#e33) no-repeat center/2px 16px, linear-gradient(#e33,#e33) no-repeat center/16px 2px;" ;
m . getContainer ( ) . appendChild ( cross ) ;
var Ctl = L . Control . extend ( {
options : { position : "bottomleft" } ,
onAdd : function ( ) {
var d = L . DomUtil . create ( "div" , "leaflet-bar" ) ;
d . style . cssText = "background:#fff;color:#111;padding:4px 8px;font:12px/1.3 monospace;cursor:pointer;" ;
d . title = "Клик — скопировать координаты центра (перекрестья)" ;
function upd ( ) { var c = m . getCenter ( ) ; d . textContent = "📍 " + c . lat . toFixed ( 6 ) + ", " + c . lng . toFixed ( 6 ) ; }
upd ( ) ; m . on ( "move" , upd ) ;
L . DomEvent . on ( d , "click" , function ( ev ) { L . DomEvent . stop ( ev ) ; copyCoord ( m . getCenter ( ) ) ; } ) ;
return d ;
}
} ) ;
m . addControl ( new Ctl ( ) ) ;
// Right-click / long-press copies the coordinate at that spot. Capture phase + stop so the
// map's own context menu and the browser's don't fire. NOTE: if the browser forbids JS from
// overriding the context menu (some Firefox configs), this can't work - use the box above.
m . getContainer ( ) . addEventListener ( "contextmenu" , function ( ev ) {
ev . preventDefault ( ) ; ev . stopPropagation ( ) ;
try { copyCoord ( m . mouseEventToLatLng ( ev ) ) ; } catch ( e ) { }
} , true ) ;
coordHooked = true ;
return true ;
}
// --- ridden-track + offline-POI overlays, registered in brouter-web's OWN layers control
// (the native checkbox list in the sidebar) rather than as separate corner buttons. All
// lazy: the several-MB GeoJSON / mesh files load only when a layer is first switched on.
var layersHooked = false ;
// "Покрытие" (accel-coloured ridden track): an empty layerGroup acts as the native-overlay
// handle; its add/remove (fired by the checkbox) lazy-load/attach the real canvas GeoJSON.
function surfaceOverlay ( m ) {
var st = { layer : null , loading : false } ;
var proxy = L . layerGroup ( ) ;
proxy . on ( "add" , function ( ) {
if ( st . layer ) { st . layer . addTo ( m ) ; return ; }
if ( st . loading ) return ;
st . loading = true ; bwToast ( "Загружаю «Покрытие»…" ) ;
fetch ( "track-surface.geojson" ) . then ( function ( r ) { return r . json ( ) ; } ) . then ( function ( gj ) {
st . layer = L . geoJSON ( gj , { renderer : L . canvas ( ) ,
style : function ( f ) { return { color : f . properties . color , weight : 3 , opacity : 0.85 } ; } } ) ;
st . loading = false ; if ( m . hasLayer ( proxy ) ) st . layer . addTo ( m ) ;
} ) . catch ( function ( ) { st . loading = false ; bwToast ( "Н е удалось загрузить «Покрытие»" ) ; } ) ;
} ) ;
proxy . on ( "remove" , function ( ) { if ( st . layer ) m . removeLayer ( st . layer ) ; } ) ;
return proxy ;
}
// --- elevation surface (hmview's WebGL TIN from your own height map) ------------------
// Lazy: loads cells.js (~5 MB), mesh.js (~3 MB), mesh_layer.js, then draws makeMeshLayer.
// Regenerate the data with tools/track pipeline (pull.sh, heightmap.py, hm_mesh.py,
// hm_export.py) and re-copy the 3 files into this folder.
var mesh = { ml : null , loading : false } ;
// Slider state persists across toggles. maxEdge is capped at 1000 m: hm_mesh.py exports
// only triangles with the longest side <= MAX_EDGE (1000 m), so a larger value shows
// nothing new — the beige gaps are stretches wider than 1 km that were never ridden.
var hmState = { alpha : 1 , edge : 1000 , pass : 0 } ;
// Turbo colour ramp for the legend (same 7 stops as the shader in mesh_layer.js).
function hmRamp ( t ) {
var T = [ [ 48 , 18 , 59 ] , [ 70 , 107 , 227 ] , [ 54 , 183 , 170 ] , [ 172 , 220 , 66 ] , [ 254 , 194 , 59 ] , [ 232 , 97 , 27 ] , [ 122 , 4 , 3 ] ] ;
t = Math . max ( 0 , Math . min ( 1 , t ) ) * ( T . length - 1 ) ;
var i = Math . floor ( t ) , f = t - i , a = T [ i ] , b = T [ Math . min ( i + 1 , T . length - 1 ) ] ;
return [ a [ 0 ] + ( b [ 0 ] - a [ 0 ] ) * f , a [ 1 ] + ( b [ 1 ] - a [ 1 ] ) * f , a [ 2 ] + ( b [ 2 ] - a [ 2 ] ) * f ] ;
}
// Surface needs only mesh.js (~3 MB) + mesh_layer.js; cells.js (~5 MB, points) is skipped.
// Range from MESH.e percentiles (1..99) so outlier vertices don't flatten the colour scale.
function loadMesh ( m , onReady ) {
if ( mesh . ml ) { onReady ( ) ; return ; }
if ( mesh . loading ) return ;
mesh . loading = true ; bwToast ( "Загружаю карту высот…" ) ;
[ "mesh.js" , "mesh_layer.js" ] . forEach ( function ( src ) {
if ( document . querySelector ( 'script[data-hm="' + src + '"]' ) ) return ;
var s = document . createElement ( "script" ) ; s . src = src ; s . setAttribute ( "data-hm" , src ) ; document . head . appendChild ( s ) ;
} ) ;
var t0 = Date . now ( ) ;
var iv = setInterval ( function ( ) {
if ( window . MESH && window . makeMeshLayer ) {
clearInterval ( iv ) ; mesh . loading = false ;
var es = [ ] . slice . call ( window . MESH . e ) . sort ( function ( a , b ) { return a - b ; } ) ;
var LO = es [ Math . floor ( es . length * 0.01 ) ] , HI = es [ Math . floor ( es . length * 0.99 ) ] ;
var ml = window . makeMeshLayer ( m , window . MESH , { } ) ;
if ( ml . error ) { bwToast ( "Высоты: " + ml . error ) ; return ; }
ml . state . lo = LO ; ml . state . hi = HI ; ml . setMaxEdge ( hmState . edge ) ; ml . draw ( ) ;
mesh . ml = ml ; onReady ( ) ;
} else if ( Date . now ( ) - t0 > 25000 ) {
clearInterval ( iv ) ; mesh . loading = false ; bwToast ( "Карта высот: данные не загрузились" ) ;
}
} , 300 ) ;
}
// "Высоты (заливка)": a native-overlay handle (empty layerGroup). Turning it on lazy-loads
// the WebGL mesh and shows a small slider panel; turning it off hides both. The panel is a
// transient settings surface tied to the active layer, not a permanent button.
var hmPanel = null ;
function heightsOverlay ( m ) {
var proxy = L . layerGroup ( ) ;
proxy . on ( "add" , function ( ) {
loadMesh ( m , function ( ) {
mesh . ml . state . visible = true ; mesh . ml . state . alpha = hmState . alpha ;
mesh . ml . setMinPass ( hmState . pass ) ; mesh . ml . setMaxEdge ( hmState . edge ) ; mesh . ml . draw ( ) ;
showHmPanel ( m ) ;
} ) ;
} ) ;
proxy . on ( "remove" , function ( ) {
if ( mesh . ml ) { mesh . ml . state . visible = false ; mesh . ml . draw ( ) ; }
if ( hmPanel ) hmPanel . style . display = "none" ;
} ) ;
return proxy ;
}
function showHmPanel ( m ) {
if ( ! hmPanel ) {
hmPanel = L . DomUtil . create ( "div" , "leaflet-bar" ) ;
hmPanel . style . cssText = "position:absolute;top:10px;right:54px;z-index:1000;background:#fff;color:#111;" +
"padding:6px 8px;font:12px sans-serif;width:190px;line-height:1.5;" ;
hmPanel . innerHTML =
'<b>Высоты (заливка)</b>'
+ '<div style="margin-top:4px">непрозрачность: <b id="hm-la">100</b>%<input type="range" id="hm-alpha" min="0" max="100" value="100" style="width:100%"></div>'
+ 'макс. сторона: <b id="hm-le">1000</b> м<input type="range" id="hm-edge" min="20" max="1000" step="20" value="1000" style="width:100%">'
+ 'мин. проездов: <b id="hm-lp">0</b><input type="range" id="hm-pass" min="0" max="10" value="0" style="width:100%">'
+ '<div style="margin-top:4px">высота: <span id="hm-lo"></span>..<span id="hm-hi"></span> м</div>'
+ '<div id="hm-leg" style="height:10px;border:1px solid #bbb"></div>'
+ '<div id="hm-stat" style="color:#888;font-size:10px;margin-top:2px"></div>' ;
m . getContainer ( ) . appendChild ( hmPanel ) ;
L . DomEvent . disableClickPropagation ( hmPanel ) ; L . DomEvent . disableScrollPropagation ( hmPanel ) ;
var $ = function ( id ) { return hmPanel . querySelector ( "#" + id ) ; } ;
var stat = function ( ) { if ( mesh . ml ) $ ( "hm-stat" ) . textContent = mesh . ml . count ( ) + " треугольников" ; } ;
$ ( "hm-alpha" ) . oninput = function ( ) { $ ( "hm-la" ) . textContent = this . value ; hmState . alpha = + this . value / 100 ; if ( mesh . ml ) { mesh . ml . state . alpha = hmState . alpha ; mesh . ml . draw ( ) ; } } ;
$ ( "hm-edge" ) . oninput = function ( ) { $ ( "hm-le" ) . textContent = this . value ; hmState . edge = + this . value ; if ( mesh . ml ) { mesh . ml . setMaxEdge ( hmState . edge ) ; stat ( ) ; } } ;
$ ( "hm-pass" ) . oninput = function ( ) { $ ( "hm-lp" ) . textContent = this . value ; hmState . pass = + this . value ; if ( mesh . ml ) { mesh . ml . setMinPass ( hmState . pass ) ; stat ( ) ; } } ;
}
hmPanel . style . display = "block" ;
var ml = mesh . ml ; if ( ! ml ) return ;
var $ = function ( id ) { return hmPanel . querySelector ( "#" + id ) ; } ;
$ ( "hm-lo" ) . textContent = ml . state . lo . toFixed ( 0 ) ;
$ ( "hm-hi" ) . textContent = ml . state . hi . toFixed ( 0 ) ;
var g = [ ] ; for ( var i = 0 ; i <= 10 ; i ++ ) { var c = hmRamp ( i / 10 ) ; g . push ( "rgb(" + ( c [ 0 ] | 0 ) + "," + ( c [ 1 ] | 0 ) + "," + ( c [ 2 ] | 0 ) + ")" ) ; }
$ ( "hm-leg" ) . style . background = "linear-gradient(90deg," + g . join ( "," ) + ")" ;
$ ( "hm-stat" ) . textContent = ml . count ( ) + " треугольников" ;
}
// --- offline POI (radius 150 km around НЭВЗ, from the Geofabrik Южный ФО extract) -----
// brouter-web's own POI overlays query live Overpass, which is unreachable from this
// network. These are static points (tools/poi/make_poi.sh), each category a NATIVE overlay
// checkbox with an emoji icon marker. The 4 MB GeoJSON loads once, on first category on.
// All off by default (native convention). Regenerate: rerun make_poi.sh, re-copy the file.
// z = минимальный зум, с которого категорию показываем (плотные - только вблизи, чтобы не
// захламлять карту и не плодить тысячи маркеров при отдалении).
var POI _CATS = [
{ k : "water" , i : "💧" , t : "вода" , z : 12 } ,
{ k : "food" , i : "🍴" , t : "еда" , z : 13 } ,
{ k : "fuel" , i : "⛽" , t : "А З С " , z : 12 } ,
{ k : "toilets" , i : "🚻" , t : "туалеты" , z : 13 } ,
{ k : "pharmacy" , i : "💊" , t : "аптеки" , z : 13 } ,
{ k : "shelter" , i : "⛺" , t : "навесы" , z : 12 } ,
{ k : "bike" , i : "🚲" , t : "веломаг" , z : 11 } ,
{ k : "shop" , i : "🛒" , t : "магазины" , z : 14 } ,
{ k : "sight" , i : "⭐" , t : "интересное" , z : 13 }
] ;
var poi = { data : null , loading : false , cbs : [ ] , enabled : { } , groups : { } , moveHooked : false } ;
function debounce ( fn , ms ) { var t ; return function ( ) { var a = arguments , c = this ; clearTimeout ( t ) ; t = setTimeout ( function ( ) { fn . apply ( c , a ) ; } , ms ) ; } ; }
function ensurePoiData ( cb ) {
if ( poi . data ) { cb ( ) ; return ; }
poi . cbs . push ( cb ) ;
if ( poi . loading ) return ;
poi . loading = true ; bwToast ( "Загружаю точки…" ) ;
fetch ( "poi-nevz.geojson" ) . then ( function ( r ) { return r . json ( ) ; } ) . then ( function ( gj ) {
var g = { } ; POI _CATS . forEach ( function ( x ) { g [ x . k ] = [ ] ; } ) ;
( gj . features || [ ] ) . forEach ( function ( f ) {
var c = f . properties && f . properties . cat ; if ( g [ c ] ) g [ c ] . push ( f ) ;
} ) ;
poi . data = g ; poi . loading = false ;
var cbs = poi . cbs ; poi . cbs = [ ] ; cbs . forEach ( function ( fn ) { fn ( ) ; } ) ;
} ) . catch ( function ( ) { poi . loading = false ; poi . cbs = [ ] ; bwToast ( "Н е удалось загрузить точки" ) ; } ) ;
}
function poiIcon ( k ) {
return L . divIcon ( { className : "baldav-poi-icon" , html : "<span>" + POI _LABEL [ k ] . i + "</span>" ,
iconSize : [ 22 , 22 ] , iconAnchor : [ 11 , 11 ] , popupAnchor : [ 0 , - 10 ] } ) ;
}
// Viewport culling: a category's layerGroup holds ONLY the markers inside the current view
// (+margin). On every pan/zoom we rebuild the visible markers, so even 17k shops cost only a
// screenful of DOM nodes instead of all at once.
function cullCat ( m , cat , bounds ) {
var group = poi . groups [ cat . k ] ;
if ( ! group ) return ;
group . clearLayers ( ) ;
if ( ! poi . enabled [ cat . k ] || ! poi . data ) return ;
if ( m . getZoom ( ) < cat . z ) return ; // too far out for this category — show nothing
var ic = poiIcon ( cat . k ) , feats = poi . data [ cat . k ] , added = 0 ;
for ( var i = 0 ; i < feats . length ; i ++ ) {
var c = feats [ i ] . geometry . coordinates ; // [lon, lat]
if ( ! bounds . contains ( [ c [ 1 ] , c [ 0 ] ] ) ) continue ;
var nm = ( feats [ i ] . properties && feats [ i ] . properties . name ) || "(без названия)" ;
L . marker ( [ c [ 1 ] , c [ 0 ] ] , { icon : ic } ) . bindPopup ( "<b>" + nm + "</b><br>" + cat . t ) . addTo ( group ) ;
if ( ++ added > 800 ) break ; // backstop for dense in-view areas
}
}
function cullPoi ( m ) {
if ( ! poi . data ) return ;
var b = m . getBounds ( ) . pad ( 0.25 ) ;
POI _CATS . forEach ( function ( cat ) { if ( poi . enabled [ cat . k ] ) cullCat ( m , cat , b ) ; } ) ;
}
// Each category is an empty layerGroup; enabling it marks the category on and culls to view.
function poiOverlay ( m , cat ) {
var group = L . layerGroup ( ) ;
poi . groups [ cat . k ] = group ;
if ( ! poi . moveHooked ) { poi . moveHooked = true ; m . on ( "moveend zoomend" , debounce ( function ( ) { cullPoi ( m ) ; } , 150 ) ) ; }
group . on ( "add" , function ( ) {
poi . enabled [ cat . k ] = true ;
ensurePoiData ( function ( ) { cullCat ( m , cat , m . getBounds ( ) . pad ( 0.25 ) ) ; } ) ;
} ) ;
group . on ( "remove" , function ( ) { poi . enabled [ cat . k ] = false ; group . clearLayers ( ) ; } ) ;
return group ;
}
var POI _LABEL = { } ; POI _CATS . forEach ( function ( x ) { POI _LABEL [ x . k ] = x ; } ) ;
// brouter-web attaches its layers control into the sidebar without map.addControl, so the
// addControl hook can miss it. Fallback: the control listens to map events, so recover it
// from any handler whose context exposes addOverlay()+_layers (the standard L.Control.Layers).
function findLayersControl ( m ) {
if ( window . BR && BR . _ _layersControl ) return BR . _ _layersControl ;
var ev = m . _events || { } ;
for ( var name in ev ) {
var arr = ev [ name ] ; if ( ! arr || ! arr . length ) continue ;
for ( var i = 0 ; i < arr . length ; i ++ ) {
var ctx = arr [ i ] && arr [ i ] . ctx ;
if ( ctx && typeof ctx . addOverlay === "function" && ctx . _layers ) { BR . _ _layersControl = ctx ; return ctx ; }
}
}
return null ;
}
function hookLayers ( ) {
if ( layersHooked ) return true ;
var m = window . BR && BR . _ _map ;
if ( ! m || ! window . L ) return false ;
var lc = findLayersControl ( m ) ;
if ( ! lc || typeof lc . addOverlay !== "function" ) return false ;
lc . addOverlay ( surfaceOverlay ( m ) , '<span class="baldav-ov">🛣️</span> Покрытие (трек)' ) ;
lc . addOverlay ( heightsOverlay ( m ) , '<span class="baldav-ov">⛰️</span> Высоты (заливка)' ) ;
POI _CATS . forEach ( function ( cat ) {
lc . addOverlay ( poiOverlay ( m , cat ) , '<span class="baldav-ov">' + cat . i + '</span> ' + cat . t ) ;
} ) ;
layersHooked = true ;
return true ;
}
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 ( ) ;
var cc = hookCoordControl ( ) ;
var sl = hookLayers ( ) ;
if ( ( b && o && q && cc && sl ) || 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 ) ;
} ) ( ) ;