fix(map): resolve initialization crashes and boundary rendering

Fixes three critical bugs:

1. CRASH: Guard addSource/addLayer calls with existence checks to
   prevent "Source already exists" errors in React strict mode
   double-mount scenarios

2. BOUNDARY: Wrap boundary update logic in a function and properly
   handle async style loading - check isStyleLoaded() and use
   map.once('load') as fallback

3. FONTS: Use 'Noto Sans Regular' for highlight layers instead of
   'Noto Sans Medium'/'Noto Sans Bold' which 404 on protomaps CDN

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-04-29 22:47:24 +00:00
commit 472ef4d0e8

View file

@ -57,13 +57,13 @@ function setupHighlightLayers(map, isDark) {
map.addLayer({
id: 'hover-hl-' + sourceLayer, type: 'symbol', source: 'protomaps', 'source-layer': sourceLayer,
filter: EMPTY_FILTER,
layout: { 'text-field': ['coalesce', ['get', 'name:en'], ['get', 'name']], 'text-font': ['Noto Sans Medium'], 'text-size': ['interpolate', ['linear'], ['zoom'], 4, 10, 10, 14, 16, 18], 'text-allow-overlap': true, 'text-ignore-placement': true },
layout: { 'text-field': ['coalesce', ['get', 'name:en'], ['get', 'name']], 'text-font': ['Noto Sans Regular'], 'text-size': ['interpolate', ['linear'], ['zoom'], 4, 10, 10, 14, 16, 18], 'text-allow-overlap': true, 'text-ignore-placement': true },
paint: { 'text-color': isDark ? '#ffffff' : '#000000', 'text-halo-color': isDark ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)', 'text-halo-width': 2.5 },
})
map.addLayer({
id: 'selected-hl-' + sourceLayer, type: 'symbol', source: 'protomaps', 'source-layer': sourceLayer,
filter: EMPTY_FILTER,
layout: { 'text-field': ['coalesce', ['get', 'name:en'], ['get', 'name']], 'text-font': ['Noto Sans Bold'], 'text-size': ['interpolate', ['linear'], ['zoom'], 4, 10, 10, 14, 16, 18], 'text-allow-overlap': true, 'text-ignore-placement': true },
layout: { 'text-field': ['coalesce', ['get', 'name:en'], ['get', 'name']], 'text-font': ['Noto Sans Regular'], 'text-size': ['interpolate', ['linear'], ['zoom'], 4, 10, 10, 14, 16, 18], 'text-allow-overlap': true, 'text-ignore-placement': true },
paint: { 'text-color': accentColor, 'text-halo-color': isDark ? 'rgba(122,154,107,0.5)' : 'rgba(122,154,107,0.3)', 'text-halo-width': 3 },
})
})
@ -1386,13 +1386,18 @@ const MapView = forwardRef(function MapView(_, ref) {
})
map.on('load', () => {
// Guard against double-mount in React strict mode
if (!map.getSource(ROUTE_SOURCE)) {
map.addSource(ROUTE_SOURCE, {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] },
})
}
// Boundary polygon layer for selected places
if (!map.getLayer(BOUNDARY_LAYER)) {
addBoundaryLayer(map)
}
// Restore overlay layers from localStorage prefs
try {
@ -1544,13 +1549,18 @@ const MapView = forwardRef(function MapView(_, ref) {
// Re-add sources/layers after style swap
map.once('style.load', () => {
// Guard against source already existing
if (!map.getSource(ROUTE_SOURCE)) {
map.addSource(ROUTE_SOURCE, {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] },
})
}
// Boundary polygon layer
if (!map.getLayer(BOUNDARY_LAYER)) {
addBoundaryLayer(map)
}
// Re-add active overlay layers
if (activeLayersRef.current.hillshade) addHillshade(map)
@ -1618,8 +1628,9 @@ const MapView = forwardRef(function MapView(_, ref) {
// Boundary polygon and zoom-to-feature
useEffect(() => {
const map = mapInstance.current
if (!map || !map.isStyleLoaded()) return
if (!map) return
const updateBoundary = () => {
const source = map.getSource(BOUNDARY_SOURCE)
if (!source) return
@ -1688,6 +1699,15 @@ const MapView = forwardRef(function MapView(_, ref) {
}
}
}
}
// If style is loaded, update immediately; otherwise wait for load event
if (map.isStyleLoaded()) {
updateBoundary()
} else {
map.once('load', updateBoundary)
return () => map.off('load', updateBoundary)
}
}, [selectedPlace])
// Update route polyline when route changes