@@ -95,6 +95,16 @@ function _patchStyleWater(styleObj) {
9595 styleObj . layers = styleObj . layers . filter ( l => l . id !== 'natural_earth' ) ;
9696 if ( styleObj . sources ) delete styleObj . sources [ 'ne2_shaded' ] ;
9797 }
98+ // Apple Maps Dark palette — neutral dark land, distinctly blue water.
99+ // Colors pre-compensated for canvas filter brightness(1.8) contrast(0.9).
100+ if ( _mapStyle === 'dark' ) {
101+ for ( const layer of styleObj . layers ) {
102+ if ( ! layer . paint ) layer . paint = { } ;
103+ if ( layer . type === 'fill' && / ^ w a t e r / . test ( layer . id ) ) {
104+ layer . paint [ 'fill-color' ] = '#131619' ;
105+ }
106+ }
107+ }
98108 const color = _mapStyle === 'dark' ? '#6a9fd8' : '#2c5f8a' ;
99109 let hasPointLabel = false ;
100110 for ( const layer of styleObj . layers ) {
@@ -108,10 +118,15 @@ function _patchStyleWater(styleObj) {
108118 }
109119 // Hide major city labels at zoom 10+ so they don't mislead when right-clicking returns a sub-area
110120 const cityLayers = [ 'place_city' , 'place_city_large' , 'label_city' , 'label_city_capital' ] ;
121+ // Show state/province labels only at zoom 2.5+
122+ const stateLayers = [ 'place_state' , 'label_state' ] ;
111123 for ( const layer of styleObj . layers ) {
112124 if ( cityLayers . includes ( layer . id ) ) {
113125 layer . maxzoom = 10 ;
114126 }
127+ if ( stateLayers . includes ( layer . id ) ) {
128+ layer . minzoom = 2.5 ;
129+ }
115130 }
116131
117132 // Dark style lacks a point-based water label layer — add one for ocean names
@@ -331,7 +346,10 @@ async function _doStyleSwap(style) {
331346 styleObj = JSON . parse ( JSON . stringify ( _styleJsonCache [ style ] ) ) ;
332347 } else {
333348 try {
334- const r = await fetch ( style ) ;
349+ const ac = new AbortController ( ) ;
350+ const timer = setTimeout ( ( ) => ac . abort ( ) , 5000 ) ;
351+ const r = await fetch ( style , { signal : ac . signal } ) ;
352+ clearTimeout ( timer ) ;
335353 const json = await r . json ( ) ;
336354 _styleJsonCache [ style ] = json ;
337355 styleObj = JSON . parse ( JSON . stringify ( json ) ) ;
@@ -369,6 +387,8 @@ async function _doStyleSwap(style) {
369387 } else {
370388 buildClusterIndex ( ) ;
371389 }
390+ // Update dark-map CSS class after pin icons are re-added with correct compensation
391+ document . getElementById ( 'map' ) ?. classList . toggle ( 'dark-map' , _mapStyle === 'dark' ) ;
372392 } ;
373393 map . once ( 'styledata' , ( ) => setTimeout ( restore , 100 ) ) ;
374394 setTimeout ( restore , 600 ) ;
@@ -382,7 +402,10 @@ async function initMap() {
382402 const styleUrl = _styleUrl ( ) ;
383403 let initStyle ;
384404 try {
385- const r = await fetch ( styleUrl ) ;
405+ const ac = new AbortController ( ) ;
406+ const timer = setTimeout ( ( ) => ac . abort ( ) , 5000 ) ;
407+ const r = await fetch ( styleUrl , { signal : ac . signal } ) ;
408+ clearTimeout ( timer ) ;
386409 const json = await r . json ( ) ;
387410 _styleJsonCache [ styleUrl ] = json ; // seed cache so first style switch is instant
388411 initStyle = JSON . parse ( JSON . stringify ( json ) ) ;
@@ -394,6 +417,24 @@ async function initMap() {
394417 fetch ( otherUrl ) . then ( r => r . json ( ) ) . then ( j => { _styleJsonCache [ otherUrl ] = j ; } ) . catch ( ( ) => { } ) ;
395418 map = new maplibregl . Map ( { container :'map' , style : initStyle , center :[ 0 , 20 ] , zoom :1.8 , attributionControl :false , preserveDrawingBuffer :true } ) ;
396419 map . addControl ( new maplibregl . NavigationControl ( { showCompass :false } ) , 'bottom-right' ) ;
420+ // Recover from WebGL context loss (Safari loses context after sleep or memory pressure)
421+ const canvas = map . getCanvas ( ) ;
422+ canvas . addEventListener ( 'webglcontextlost' , ( e ) => {
423+ e . preventDefault ( ) ; // allow context to be restored
424+ console . warn ( 'WebGL context lost — waiting for restore' ) ;
425+ } ) ;
426+ canvas . addEventListener ( 'webglcontextrestored' , ( ) => {
427+ console . log ( 'WebGL context restored — reinitializing map' ) ;
428+ map . triggerRepaint ( ) ;
429+ } ) ;
430+ // Safety net: if map is still blank after 8 seconds, show a reload prompt
431+ setTimeout ( ( ) => {
432+ const gl = canvas . getContext ( 'webgl2' ) || canvas . getContext ( 'webgl' ) ;
433+ if ( ! gl || gl . isContextLost ( ) ) {
434+ console . warn ( 'Map canvas has no WebGL context — prompting reload' ) ;
435+ showToast ( 'Map failed to load — please refresh the page' , 'error' ) ;
436+ }
437+ } , 8000 ) ;
397438 // Provide a transparent 1x1 placeholder for any missing sprite images (e.g. POI icons)
398439 map . on ( 'styleimagemissing' , ( e ) => {
399440 if ( ! map . hasImage ( e . id ) ) {
@@ -439,13 +480,18 @@ async function initMap() {
439480 // Tile loading spinner
440481 const tileSpinner = document . getElementById ( 'tile-spinner' ) ;
441482 map . on ( 'dataloading' , ( ) => { tileSpinner ?. classList . add ( 'active' ) ; } ) ;
442- map . on ( 'idle' , ( ) => { tileSpinner ?. classList . remove ( 'active' ) ; } ) ;
483+ map . on ( 'idle' , ( ) => {
484+ tileSpinner ?. classList . remove ( 'active' ) ;
485+ const mapLoading = document . getElementById ( 'map-loading' ) ;
486+ if ( mapLoading ) { mapLoading . classList . add ( 'done' ) ; setTimeout ( ( ) => mapLoading . remove ( ) , 400 ) ; }
487+ } ) ;
443488 } ) ;
444489 map . on ( 'movestart' , ( ) => { _mapBusy = true ; } ) ;
445490 map . on ( 'moveend' , ( ) => { _mapBusy = false ; } ) ;
446491
447492 // Right-click on map to pin a location
448493 map . on ( 'contextmenu' , async ( e ) => {
494+ try {
449495 e . preventDefault ( ) ;
450496 // Detect water vs land early — water clicks are allowed at any zoom,
451497 // land clicks require zoom >= 7 for meaningful Nominatim results.
@@ -513,6 +559,7 @@ async function initMap() {
513559 popup . on ( 'close' , ( ) => { if ( destMarkerObj ) { destMarkerObj . marker . remove ( ) ; destMarkerObj = null ; } } ) ;
514560
515561 destMarkerObj = { marker, popup } ;
562+ } catch ( err ) { console . error ( '[right-click] ERROR in handler:' , err ) ; }
516563 } ) ;
517564
518565 // Window-level capture handler — fires before anything else can intercept.
@@ -783,13 +830,15 @@ function toggleStyleMenu(e) {
783830}
784831
785832function setMapStyle ( mode ) {
833+ const wasDark = _mapStyle === 'dark' ;
786834 _mapStyle = mode ;
787835 // Persist style preference (satellite resets to previous on reload)
788836 if ( mode !== 'satellite' ) localStorage . setItem ( 'matrix-theme' , mode ) ;
789837
790- // Update CSS classes
838+ // Defer dark-map CSS class removal until pin icons are re-added with correct
839+ // compensation (otherwise pre-darkened images render without the CSS filter)
791840 const mapEl = document . getElementById ( 'map' ) ;
792- mapEl . classList . toggle ( 'dark-map' , _mapStyle === 'dark' ) ;
841+ if ( _mapStyle === 'dark' ) mapEl . classList . add ( 'dark-map ') ;
793842 mapEl . classList . toggle ( 'sat-mode' , _mapStyle === 'satellite' ) ;
794843
795844 // Labels toggle visibility
0 commit comments