44function _styleUrl ( ) {
55 if ( _mapStyle === 'satellite' ) return STYLE_SAT ;
66 if ( _mapStyle === 'dark' ) return STYLE_DARK ;
7+ if ( _mapStyle === 'bright' ) return STYLE_BRIGHT ;
78 return STYLE_STREET ; // light, enriched, terrain3d, globe all use Liberty as base
89}
910const STYLE_STREET = 'https://tiles.openfreemap.org/styles/liberty' ;
11+ const STYLE_BRIGHT = 'https://tiles.openfreemap.org/styles/bright' ;
1012const STYLE_DARK = 'https://tiles.openfreemap.org/styles/dark' ;
1113const STYLE_SAT = {
1214 version :8 ,
@@ -94,18 +96,24 @@ function _patchStyleBoundaries(styleObj) {
9496// - Sets ocean fill color for light mode
9597function _patchStyleWater ( styleObj ) {
9698 if ( ! styleObj || ! styleObj . layers ) return styleObj ;
99+ // Remove unsupported layer types that cause MapLibre v5 validation errors
100+ styleObj . layers = styleObj . layers . filter ( l => l . type !== 'sky' ) ;
101+ if ( styleObj . sky ) delete styleObj . sky ;
102+ if ( styleObj . terrain ) delete styleObj . terrain ;
97103 // Strip natural-earth terrain raster in Light Map (not enriched) to speed up rendering
98104 if ( _mapStyle === 'light' ) {
99105 styleObj . layers = styleObj . layers . filter ( l => l . id !== 'natural_earth' ) ;
100106 if ( styleObj . sources ) delete styleObj . sources [ 'ne2_shaded' ] ;
101107 }
102108 // Apple Maps Dark palette — neutral dark land, distinctly blue water.
103109 // Colors pre-compensated for canvas filter brightness(1.8) contrast(0.9).
104- if ( _mapStyle === 'dark' ) {
110+ if ( _mapStyle === 'dark' || _mapStyle === 'terrain3d' ) {
111+ // Darker water for both Dark Map and 3D Terrain modes
112+ const waterColor = _mapStyle === 'terrain3d' ? '#1a2e3d' : '#131619' ;
105113 for ( const layer of styleObj . layers ) {
106114 if ( ! layer . paint ) layer . paint = { } ;
107115 if ( layer . type === 'fill' && / ^ w a t e r / . test ( layer . id ) ) {
108- layer . paint [ 'fill-color' ] = '#131619' ;
116+ layer . paint [ 'fill-color' ] = waterColor ;
109117 }
110118 }
111119 }
@@ -299,12 +307,18 @@ function addPinLayers() {
299307 try { openPinPopup ( lat , lng ) ; } catch ( err ) { console . error ( err ) ; }
300308 const isEmpty = f . properties . iconId === 'pin-empty' ;
301309 if ( ! isEmpty ) {
302- const targetZoom = Math . max ( map . getZoom ( ) , 14 ) ;
310+ // Zoom in by at most 4 levels from current zoom, capped at 14
311+ // Prevents a jarring jump from low zoom levels (e.g. zoom 3 → 14)
312+ const targetZoom = Math . min ( Math . max ( map . getZoom ( ) , map . getZoom ( ) + 4 ) , 14 ) ;
303313 const needsZoom = map . getZoom ( ) < targetZoom ;
304314 const dist = Math . hypot ( map . getCenter ( ) . lng - lng , map . getCenter ( ) . lat - lat ) ;
305315 const alreadyThere = dist < 0.005 && ! needsZoom ;
306316 if ( ! alreadyThere ) {
317+ // In 3D Terrain (pitched view), offset the focal point downward in screen space
318+ // so the pin appears in the visible ground area rather than drifting off-screen
319+ const pitchOffset = _mapStyle === 'terrain3d' ? [ 0 , Math . round ( map . transform ?. height * 0.15 || 100 ) ] : [ 0 , 0 ] ;
307320 map . flyTo ( { center : [ lng , lat ] , zoom : targetZoom , speed : 0.8 , curve : 1.0 , essential : true ,
321+ offset : pitchOffset ,
308322 easing : t => t < 0.5 ? 4 * t * t * t : 1 - Math . pow ( - 2 * t + 2 , 3 ) / 2 } ) ;
309323 }
310324 }
@@ -318,7 +332,7 @@ function addPinLayers() {
318332// ═══════════════════════════════════════
319333function initTheme ( ) {
320334 const stored = localStorage . getItem ( 'matrix-theme' ) ;
321- if ( stored && [ 'dark' , 'light' , 'enriched' , 'terrain3d' , 'globe' ] . includes ( stored ) ) {
335+ if ( stored && [ 'dark' , 'light' , 'bright' , ' enriched', 'terrain3d' , 'globe' ] . includes ( stored ) ) {
322336 _mapStyle = stored ;
323337 } else {
324338 _mapStyle = window . matchMedia ( '(prefers-color-scheme: light)' ) . matches ? 'light' : 'dark' ;
@@ -330,8 +344,9 @@ function applyTheme() {
330344 _tileTemplatesCache = null ;
331345 // Set initial button label
332346 const btn = document . getElementById ( 'tb-style-btn' ) ;
333- const labels = { light : 'Light Map' , enriched : 'Terrain' , dark : 'Dark Map' , satellite : 'Satellite' , terrain3d : '3D Terrain' , globe : 'Globe' } ;
347+ const labels = { light : 'Light Map' , bright : 'Bright Map' , enriched : 'Terrain' , dark : 'Dark Map' , satellite : 'Satellite' , terrain3d : '3D Terrain' , globe : 'Globe' } ;
334348 if ( btn ) btn . textContent = ( labels [ _mapStyle ] || _mapStyle ) + ' ▾' ;
349+
335350 // Set initial active state in menu
336351 document . querySelectorAll ( '.style-menu-item' ) . forEach ( el => el . classList . toggle ( 'active' , el . dataset . style === _mapStyle ) ) ;
337352 // Disable Export Video in Globe mode
@@ -429,7 +444,13 @@ async function initMap() {
429444 const otherUrl = styleUrl === STYLE_DARK ? STYLE_STREET : STYLE_DARK ;
430445 fetch ( otherUrl ) . then ( r => r . json ( ) ) . then ( j => { _styleJsonCache [ otherUrl ] = j ; } ) . catch ( ( ) => { } ) ;
431446 map = new maplibregl . Map ( { container :'map' , style : initStyle , center :[ 0 , 20 ] , zoom :1.8 , attributionControl :false , maxTileCacheSize :200 , canvasContextAttributes :{ preserveDrawingBuffer :true } } ) ;
432- map . on ( 'error' , ( e ) => console . error ( 'MapLibre error:' , e . error ?. message || e . message || e ) ) ;
447+ // Suppress non-critical style validation errors (sky/terrain-dem references in
448+ // unpatched style JSON when the pre-fetch falls back to URL loading)
449+ const _suppressedErrors = / s k y | t e r r a i n - d e m | n o n - e x i s t i n g l a y e r | s a m e s o u r c e .* h i l l s h a d e / i;
450+ map . on ( 'error' , ( e ) => {
451+ const msg = e . error ?. message || e . message || '' ;
452+ if ( ! _suppressedErrors . test ( msg ) ) console . error ( 'MapLibre error:' , msg ) ;
453+ } ) ;
433454 map . addControl ( new maplibregl . NavigationControl ( { showCompass :false } ) , 'bottom-right' ) ;
434455 // Recover from WebGL context loss (Safari loses context after sleep or memory pressure)
435456 const canvas = map . getCanvas ( ) ;
@@ -464,15 +485,16 @@ async function initMap() {
464485 zoomEl . style . cssText = 'position:absolute;bottom:24px;left:8px;background:rgba(0,0,0,.6);color:#fff;font-size:11px;padding:2px 6px;border-radius:4px;z-index:10;pointer-events:none;font-family:monospace' ;
465486 document . getElementById ( 'map' ) . appendChild ( zoomEl ) ;
466487 const pitchWrap = document . createElement ( 'div' ) ;
467- pitchWrap . style . cssText = 'position:absolute;bottom:24px;left:70px;display:none;align-items:center;gap:4px;z-index:10' ;
468- const pitchEl = document . createElement ( 'div' ) ;
488+ pitchWrap . id = 'pitch-wrap' ;
489+ pitchWrap . style . cssText = 'position:absolute;bottom:24px;left:70px;display:none;align-items:center;z-index:10;background:rgba(0,0,0,.6);border-radius:4px;font-family:monospace;font-size:11px;color:#fff' ;
490+ const pitchEl = document . createElement ( 'span' ) ;
469491 pitchEl . id = 'pitch-debug' ;
470- pitchEl . style . cssText = 'background:rgba(0,0,0,.6);color:#fff;font-size:11px; padding:2px 6px;border-radius:4px; pointer-events:none;font-family:monospace ' ;
492+ pitchEl . style . cssText = 'padding:2px 6px;pointer-events:none' ;
471493 const resetViewEl = document . createElement ( 'button' ) ;
472494 resetViewEl . id = 'reset-view-btn' ;
473- resetViewEl . title = 'Reset to top-down view ' ;
495+ resetViewEl . title = 'Reset north ' ;
474496 resetViewEl . textContent = '⊙' ;
475- resetViewEl . style . cssText = 'background:rgba(0,0,0,.6) ;color:#fff;font-size:11px ;border:none;border-radius:4px ;cursor:pointer;padding:3px 6px;font-family:monospace;display:none;box-sizing:border-box; height:19px ' ;
497+ resetViewEl . style . cssText = 'background:none ;color:#fff;font-size:15px ;border:none;border-left:1px solid rgba(255,255,255,.2) ;cursor:pointer;padding:0 6px;font-family:monospace;display:none;line- height:1 ' ;
476498 resetViewEl . addEventListener ( 'click' , ( ) => { map . easeTo ( { bearing : 0 , duration : 500 } ) ; } ) ;
477499 pitchWrap . appendChild ( pitchEl ) ;
478500 pitchWrap . appendChild ( resetViewEl ) ;
@@ -487,6 +509,16 @@ async function initMap() {
487509 document . getElementById ( 'exaggeration-val' ) . textContent = val . toFixed ( 1 ) + '×' ;
488510 if ( map . getTerrain ( ) ) map . setTerrain ( { source : 'terrain-dem' , exaggeration : val } ) ;
489511 } ) ;
512+ // Live GPS coordinates on mouse move
513+ const coordsEl = document . createElement ( 'div' ) ;
514+ coordsEl . id = 'coords-debug' ;
515+ coordsEl . style . cssText = 'position:absolute;bottom:24px;right:50px;background:rgba(0,0,0,.6);color:#fff;font-size:11px;padding:2px 6px;border-radius:4px;z-index:10;pointer-events:none;font-family:monospace;display:none' ;
516+ document . getElementById ( 'map' ) . appendChild ( coordsEl ) ;
517+ map . on ( 'mousemove' , ( e ) => {
518+ coordsEl . textContent = `${ e . lngLat . lat . toFixed ( 4 ) } °, ${ e . lngLat . lng . toFixed ( 4 ) } °` ;
519+ coordsEl . style . display = '' ;
520+ } ) ;
521+ map . getCanvas ( ) . addEventListener ( 'mouseout' , ( ) => { coordsEl . style . display = 'none' ; } ) ;
490522 const updateZoom = ( ) => {
491523 zoomEl . textContent = 'z' + map . getZoom ( ) . toFixed ( 2 ) ;
492524 const is3D = _mapStyle === 'terrain3d' ;
@@ -740,7 +772,7 @@ function setMapStyle(mode) {
740772 }
741773
742774 // Update button label
743- const labels = { light : 'Light Map' , enriched : 'Terrain' , dark : 'Dark Map' , satellite : 'Satellite' , terrain3d : '3D Terrain' , globe : 'Globe' } ;
775+ const labels = { light : 'Light Map' , bright : 'Bright Map' , enriched : 'Terrain' , dark : 'Dark Map' , satellite : 'Satellite' , terrain3d : '3D Terrain' , globe : 'Globe' } ;
744776 const btn = document . getElementById ( 'tb-style-btn' ) ;
745777 if ( btn ) btn . textContent = ( labels [ mode ] || mode ) + ' ▾' ;
746778
@@ -778,11 +810,19 @@ function _applyTerrainAndProjection() {
778810
779811 // Terrain — AWS Terrain Tiles (free, no API key, terrarium encoding)
780812 if ( is3D ) {
813+ // Use separate sources for terrain mesh and hillshade to improve rendering quality
781814 if ( ! map . getSource ( 'terrain-dem' ) ) {
782815 map . addSource ( 'terrain-dem' , {
783816 type : 'raster-dem' ,
784817 tiles : [ 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png' ] ,
785- tileSize : 512 , maxzoom : 17 , encoding : 'terrarium'
818+ tileSize : 512 , maxzoom : 15 , encoding : 'terrarium'
819+ } ) ;
820+ }
821+ if ( ! map . getSource ( 'hillshade-dem' ) ) {
822+ map . addSource ( 'hillshade-dem' , {
823+ type : 'raster-dem' ,
824+ tiles : [ 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png' ] ,
825+ tileSize : 512 , maxzoom : 15 , encoding : 'terrarium'
786826 } ) ;
787827 }
788828 const sliderEl = document . getElementById ( 'exaggeration-slider' ) ;
@@ -799,7 +839,7 @@ function _applyTerrainAndProjection() {
799839 map . addLayer ( {
800840 id : 'terrain-hillshade' ,
801841 type : 'hillshade' ,
802- source : 'terrain -dem' ,
842+ source : 'hillshade -dem' ,
803843 paint : {
804844 'hillshade-exaggeration' : 1.0 ,
805845 'hillshade-illumination-direction' : 270 ,
@@ -835,7 +875,7 @@ function _applyTerrainAndProjection() {
835875 map . flyTo ( { center : [ 0 , 0 ] , zoom : 2 , pitch : 0 , bearing : 0 , duration : 800 } ) ;
836876 } else {
837877 map . setMinZoom ( - 2 ) ;
838- map . easeTo ( { pitch : 0 , bearing : 0 , duration : 600 } ) ;
878+ map . jumpTo ( { pitch : 0 , bearing : 0 } ) ;
839879 }
840880 }
841881}
0 commit comments