@@ -107,9 +107,9 @@ function _patchStyleWater(styleObj) {
107107 }
108108 // Apple Maps Dark palette — neutral dark land, distinctly blue water.
109109 // Colors pre-compensated for canvas filter brightness(1.8) contrast(0.9).
110- if ( _mapStyle === 'dark' || _mapStyle === 'terrain3d' ) {
111- // Darker water for both Dark Map and 3D Terrain modes
112- const waterColor = _mapStyle === 'terrain3d' ? '#1a2e3d' : '#131619' ;
110+ if ( _mapStyle === 'dark' ) {
111+ // Darker water for Dark Map
112+ const waterColor = '#131619' ;
113113 for ( const layer of styleObj . layers ) {
114114 if ( ! layer . paint ) layer . paint = { } ;
115115 if ( layer . type === 'fill' && / ^ w a t e r / . test ( layer . id ) ) {
@@ -273,6 +273,120 @@ function toggleLabels() {
273273 }
274274}
275275
276+ // Apply vivid parks, airport runways, mountain peaks, and optional 3D buildings
277+ function applyExtraLayers ( ) {
278+ const hasOmt = map . getSource && map . getSource ( 'openmaptiles' ) ;
279+ if ( ! hasOmt || [ 'satellite' , 'globe' ] . includes ( _mapStyle ) ) return ;
280+
281+ // Vivid park fill — more saturated green over the base park layer
282+ if ( ! map . getLayer ( 'matrix-park-vivid' ) ) {
283+ map . addLayer ( {
284+ id : 'matrix-park-vivid' ,
285+ type : 'fill' ,
286+ source : 'openmaptiles' ,
287+ 'source-layer' : 'park' ,
288+ minzoom : 5 ,
289+ paint : {
290+ 'fill-color' : '#5aab5a' ,
291+ 'fill-opacity' : [ 'interpolate' , [ 'linear' ] , [ 'zoom' ] , 5 , 0.08 , 12 , 0.18 ]
292+ }
293+ } , 'park_outline' ) ;
294+ }
295+
296+ // Airport runway & taxiway fill — visible from zoom 9+
297+ if ( ! map . getLayer ( 'matrix-aeroway-fill' ) ) {
298+ map . addLayer ( {
299+ id : 'matrix-aeroway-fill' ,
300+ type : 'fill' ,
301+ source : 'openmaptiles' ,
302+ 'source-layer' : 'aeroway' ,
303+ minzoom : 9 ,
304+ filter : [ 'match' , [ 'geometry-type' ] , [ 'Polygon' , 'MultiPolygon' ] , true , false ] ,
305+ paint : {
306+ 'fill-color' : '#d8d4cb' ,
307+ 'fill-opacity' : 0.6
308+ }
309+ } , 'aeroway_fill' ) ;
310+ }
311+ if ( ! map . getLayer ( 'matrix-aeroway-runway' ) ) {
312+ map . addLayer ( {
313+ id : 'matrix-aeroway-runway' ,
314+ type : 'line' ,
315+ source : 'openmaptiles' ,
316+ 'source-layer' : 'aeroway' ,
317+ minzoom : 10 ,
318+ filter : [ '==' , [ 'get' , 'class' ] , 'runway' ] ,
319+ paint : {
320+ 'line-color' : '#c8c4bb' ,
321+ 'line-width' : [ 'interpolate' , [ 'exponential' , 1.2 ] , [ 'zoom' ] , 10 , 1 , 18 , 12 ]
322+ }
323+ } ) ;
324+ }
325+
326+ // Mountain peak labels — shows summit names at zoom 10+
327+ if ( ! map . getLayer ( 'matrix-mountain-peaks' ) ) {
328+ map . addLayer ( {
329+ id : 'matrix-mountain-peaks' ,
330+ type : 'symbol' ,
331+ source : 'openmaptiles' ,
332+ 'source-layer' : 'poi' ,
333+ minzoom : 10 ,
334+ filter : [ '==' , [ 'get' , 'class' ] , 'mountain' ] ,
335+ layout : {
336+ 'text-field' : [ 'coalesce' , [ 'get' , 'name_en' ] , [ 'get' , 'name' ] ] ,
337+ 'text-font' : [ 'Noto Sans Bold' ] ,
338+ 'text-size' : 11 ,
339+ 'text-anchor' : 'top' ,
340+ 'text-offset' : [ 0 , 0.5 ] ,
341+ 'icon-image' : 'mountain_11' ,
342+ 'icon-size' : 0.8 ,
343+ 'icon-allow-overlap' : false ,
344+ 'text-allow-overlap' : false
345+ } ,
346+ paint : {
347+ 'text-color' : _mapStyle === 'dark' ? '#a0b8c0' : '#5a7a8a' ,
348+ 'text-halo-color' : _mapStyle === 'dark' ? '#1a2530' : '#ffffff' ,
349+ 'text-halo-width' : 1.5
350+ }
351+ } ) ;
352+ }
353+
354+ // 3D buildings — only when toggled on, zoom 15+
355+ apply3DBuildings ( ) ;
356+ }
357+
358+ function apply3DBuildings ( ) {
359+ if ( ! map . getSource || ! map . getSource ( 'openmaptiles' ) ) return ;
360+ const shouldShow = buildings3DVisible && ! [ 'satellite' , 'terrain3d' , 'globe' ] . includes ( _mapStyle ) ;
361+ if ( shouldShow && ! map . getLayer ( 'matrix-buildings-3d' ) ) {
362+ // Insert below the first symbol layer so road/POI labels render on top of buildings
363+ const firstSymbol = map . getStyle ( ) ?. layers ?. find ( l => l . type === 'symbol' ) ;
364+ map . addLayer ( {
365+ id : 'matrix-buildings-3d' ,
366+ type : 'fill-extrusion' ,
367+ source : 'openmaptiles' ,
368+ 'source-layer' : 'building' ,
369+ minzoom : 15 ,
370+ paint : {
371+ 'fill-extrusion-color' : _mapStyle === 'dark' ? '#2a2e3a' : '#d9d6d0' ,
372+ 'fill-extrusion-height' : [ 'get' , 'render_height' ] ,
373+ 'fill-extrusion-base' : [ 'get' , 'render_min_height' ] ,
374+ 'fill-extrusion-opacity' : 0.75
375+ }
376+ } , firstSymbol ?. id ) ;
377+ } else if ( ! shouldShow && map . getLayer ( 'matrix-buildings-3d' ) ) {
378+ map . removeLayer ( 'matrix-buildings-3d' ) ;
379+ }
380+ const btn = document . getElementById ( 'tb-3d-buildings' ) ;
381+ if ( btn ) btn . style . opacity = buildings3DVisible ? '1' : '.4' ;
382+ }
383+
384+ function toggle3DBuildings ( ) {
385+ buildings3DVisible = ! buildings3DVisible ;
386+ localStorage . setItem ( 'matrix-3d-buildings' , buildings3DVisible ? 'visible' : 'hidden' ) ;
387+ apply3DBuildings ( ) ;
388+ }
389+
276390function addPinLayers ( ) {
277391 if ( ! map . getSource ( 'photo-pins' ) ) {
278392 map . addSource ( 'photo-pins' , { type : 'geojson' , data : { type : 'FeatureCollection' , features : [ ] } } ) ;
@@ -383,6 +497,7 @@ async function _doStyleSwap(style) {
383497 raiseLabelsAboveRoads ( ) ;
384498 applyLabelScale ( ) ;
385499 applyLabelVisibility ( ) ;
500+ applyExtraLayers ( ) ;
386501 // Re-add pin icons from pixel cache (fast) and refresh clusters
387502 // without rebuilding the Supercluster index (unchanged)
388503 // Update dark-map CSS class after pin icons are re-added with correct compensation
@@ -547,9 +662,26 @@ async function initMap() {
547662 btn . addEventListener ( 'click' , toggleLabels ) ;
548663 wrap . appendChild ( btn ) ;
549664 navGroup . after ( wrap ) ;
665+
666+ // 3D Buildings toggle — inserted before navGroup so it appears above zoom controls
667+ const bldgWrap = document . createElement ( 'div' ) ;
668+ bldgWrap . className = 'maplibregl-ctrl maplibregl-ctrl-group' ;
669+ bldgWrap . id = 'buildings-toggle-wrap' ;
670+ const bldgBtn = document . createElement ( 'button' ) ;
671+ bldgBtn . id = 'tb-3d-buildings' ;
672+ bldgBtn . type = 'button' ;
673+ bldgBtn . title = '3D Buildings (zoom 15+)' ;
674+ bldgBtn . setAttribute ( 'aria-label' , 'Toggle 3D buildings' ) ;
675+ bldgBtn . style . opacity = buildings3DVisible ? '1' : '.4' ;
676+ bldgBtn . innerHTML = '<span style="font-size:11px;font-weight:700;line-height:29px;display:block;color:var(--text);opacity:.7;font-family:var(--font)">3D</span>' ;
677+ bldgBtn . addEventListener ( 'click' , toggle3DBuildings ) ;
678+ bldgWrap . appendChild ( bldgBtn ) ;
679+ if ( [ 'satellite' , 'terrain3d' , 'globe' ] . includes ( _mapStyle ) ) bldgWrap . style . display = 'none' ;
680+ navGroup . before ( bldgWrap ) ;
550681 }
551682 applyLabelScale ( ) ;
552683 applyLabelVisibility ( ) ;
684+ applyExtraLayers ( ) ;
553685 _applyTerrainAndProjection ( ) ;
554686 // Tile loading spinner
555687 const tileSpinner = document . getElementById ( 'tile-spinner' ) ;
@@ -751,6 +883,9 @@ function setMapStyle(mode) {
751883 // Labels toggle visibility
752884 const labelsWrap = document . getElementById ( 'labels-toggle-wrap' ) ;
753885 if ( labelsWrap ) labelsWrap . style . visibility = _mapStyle === 'satellite' ? 'hidden' : 'visible' ;
886+ // Hide 3D buildings toggle in modes where buildings don't make sense
887+ const bldgWrap = document . getElementById ( 'buildings-toggle-wrap' ) ;
888+ if ( bldgWrap ) bldgWrap . style . display = [ 'satellite' , 'terrain3d' , 'globe' ] . includes ( _mapStyle ) ? 'none' : '' ;
754889
755890 // Disable Export Video in Globe mode (flyTo animation doesn't translate to globe projection)
756891 const exportBtn = document . getElementById ( 'tb-export-video' ) ;
0 commit comments