From b883c4113b4ffa38aa63f1b94e54e32cf70babbe Mon Sep 17 00:00:00 2001 From: htlee Date: Sun, 15 Feb 2026 15:27:57 +0900 Subject: [PATCH] fix: guard map style and ship layer ids during rendering --- apps/web/src/widgets/map3d/Map3D.tsx | 33 +++++++++++++++++++--------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/apps/web/src/widgets/map3d/Map3D.tsx b/apps/web/src/widgets/map3d/Map3D.tsx index e194216..c082500 100644 --- a/apps/web/src/widgets/map3d/Map3D.tsx +++ b/apps/web/src/widgets/map3d/Map3D.tsx @@ -189,6 +189,12 @@ const MAP_DEFAULT_SHIP_RGB: [number, number, number] = [100, 116, 139]; const clampNumber = (value: number, minValue: number, maxValue: number) => Math.max(minValue, Math.min(maxValue, value)); +function getLayerId(value: unknown): string | null { + if (!value || typeof value !== "object") return null; + const candidate = (value as { id?: unknown }).id; + return typeof candidate === "string" ? candidate : null; +} + function normalizeAngleDeg(value: number, offset = 0): number { const v = value + offset; return ((v % 360) + 360) % 360; @@ -676,7 +682,11 @@ function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: str } as unknown as LayerSpecification; // Insert before the first symbol layer (keep labels on top), otherwise append. - const layers = style.layers as LayerSpecification[]; + const rawLayers = Array.isArray(style.layers) ? style.layers : []; + const layers = rawLayers.filter((layer): layer is LayerSpecification => { + if (!layer || typeof layer !== "object") return false; + return typeof (layer as { id?: unknown }).id === "string"; + }); const symbolIndex = layers.findIndex((l) => l.type === "symbol"); const insertAt = symbolIndex >= 0 ? symbolIndex : layers.length; @@ -1077,7 +1087,8 @@ export function Map3D({ projectionRef.current = projection; }, [projection]); - const removeLayerIfExists = useCallback((map: maplibregl.Map, layerId: string) => { + const removeLayerIfExists = useCallback((map: maplibregl.Map, layerId: string | null | undefined) => { + if (!layerId) return; try { if (map.getLayer(layerId)) { map.removeLayer(layerId); @@ -1218,9 +1229,10 @@ export function Map3D({ onMapStyleReady(map, () => { applyProjection(); // Globe deck layer lives inside the style and must be re-added after any style swap. - if (projectionRef.current === "globe" && globeDeckLayerRef.current && !map!.getLayer(globeDeckLayerRef.current.id)) { + const deckLayer = globeDeckLayerRef.current; + if (projectionRef.current === "globe" && deckLayer && !map!.getLayer(deckLayer.id)) { try { - map!.addLayer(globeDeckLayerRef.current); + map!.addLayer(deckLayer); } catch { // ignore } @@ -1431,13 +1443,14 @@ export function Map3D({ } const layer = globeDeckLayerRef.current; - if (layer && map.isStyleLoaded() && !map.getLayer(layer.id)) { + const layerId = layer?.id; + if (layer && layerId && map.isStyleLoaded() && !map.getLayer(layerId)) { try { map.addLayer(layer); } catch { // ignore } - if (!map.getLayer(layer.id) && !cancelled && retries < maxRetries) { + if (!map.getLayer(layerId) && !cancelled && retries < maxRetries) { retries += 1; window.requestAnimationFrame(() => syncProjectionAndDeck()); return; @@ -1554,7 +1567,7 @@ export function Map3D({ const style = map.getStyle(); const styleLayers = style && Array.isArray(style.layers) ? style.layers : []; for (const layer of styleLayers) { - const id = String(layer.id ?? ""); + const id = getLayerId(layer); if (!id) continue; const sourceLayer = String((layer as Record)["source-layer"] ?? "").toLowerCase(); const source = String((layer as { source?: unknown }).source ?? "").toLowerCase(); @@ -1933,7 +1946,7 @@ export function Map3D({ console.warn("Ship icon image setup failed:", e); } - const globeShipData = targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon)); + const globeShipData = shipData; const geojson: GeoJSON.FeatureCollection = { type: "FeatureCollection", features: globeShipData.map((t) => { @@ -1956,7 +1969,7 @@ export function Map3D({ const iconSize14 = clampNumber(0.72 * sizeScale * selectedScale, 0.45, 2.1); return { type: "Feature", - id: t.mmsi, + ...(isFiniteNumber(t.mmsi) ? { id: Math.trunc(t.mmsi) } : {}), geometry: { type: "Point", coordinates: [t.lon, t.lat] }, properties: { mmsi: t.mmsi, @@ -2890,7 +2903,7 @@ export function Map3D({ }, [projection, overlays.pairRange, pairLinks, mapSyncEpoch, hoveredShipSignature, hoveredPairSignature, hoveredFleetSignature, isHighlightedPair]); const shipData = useMemo(() => { - return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon)); + return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon) && isFiniteNumber(t.mmsi)); }, [targets]); const shipByMmsi = useMemo(() => {