fix: guard map style and ship layer ids during rendering

This commit is contained in:
htlee 2026-02-15 15:27:57 +09:00
부모 96d8a03f93
커밋 b883c4113b

파일 보기

@ -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)); 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 { function normalizeAngleDeg(value: number, offset = 0): number {
const v = value + offset; const v = value + offset;
return ((v % 360) + 360) % 360; return ((v % 360) + 360) % 360;
@ -676,7 +682,11 @@ function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: str
} as unknown as LayerSpecification; } as unknown as LayerSpecification;
// Insert before the first symbol layer (keep labels on top), otherwise append. // 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 symbolIndex = layers.findIndex((l) => l.type === "symbol");
const insertAt = symbolIndex >= 0 ? symbolIndex : layers.length; const insertAt = symbolIndex >= 0 ? symbolIndex : layers.length;
@ -1077,7 +1087,8 @@ export function Map3D({
projectionRef.current = projection; projectionRef.current = projection;
}, [projection]); }, [projection]);
const removeLayerIfExists = useCallback((map: maplibregl.Map, layerId: string) => { const removeLayerIfExists = useCallback((map: maplibregl.Map, layerId: string | null | undefined) => {
if (!layerId) return;
try { try {
if (map.getLayer(layerId)) { if (map.getLayer(layerId)) {
map.removeLayer(layerId); map.removeLayer(layerId);
@ -1218,9 +1229,10 @@ export function Map3D({
onMapStyleReady(map, () => { onMapStyleReady(map, () => {
applyProjection(); applyProjection();
// Globe deck layer lives inside the style and must be re-added after any style swap. // 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 { try {
map!.addLayer(globeDeckLayerRef.current); map!.addLayer(deckLayer);
} catch { } catch {
// ignore // ignore
} }
@ -1431,13 +1443,14 @@ export function Map3D({
} }
const layer = globeDeckLayerRef.current; 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 { try {
map.addLayer(layer); map.addLayer(layer);
} catch { } catch {
// ignore // ignore
} }
if (!map.getLayer(layer.id) && !cancelled && retries < maxRetries) { if (!map.getLayer(layerId) && !cancelled && retries < maxRetries) {
retries += 1; retries += 1;
window.requestAnimationFrame(() => syncProjectionAndDeck()); window.requestAnimationFrame(() => syncProjectionAndDeck());
return; return;
@ -1554,7 +1567,7 @@ export function Map3D({
const style = map.getStyle(); const style = map.getStyle();
const styleLayers = style && Array.isArray(style.layers) ? style.layers : []; const styleLayers = style && Array.isArray(style.layers) ? style.layers : [];
for (const layer of styleLayers) { for (const layer of styleLayers) {
const id = String(layer.id ?? ""); const id = getLayerId(layer);
if (!id) continue; if (!id) continue;
const sourceLayer = String((layer as Record<string, unknown>)["source-layer"] ?? "").toLowerCase(); const sourceLayer = String((layer as Record<string, unknown>)["source-layer"] ?? "").toLowerCase();
const source = String((layer as { source?: unknown }).source ?? "").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); 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<GeoJSON.Point> = { const geojson: GeoJSON.FeatureCollection<GeoJSON.Point> = {
type: "FeatureCollection", type: "FeatureCollection",
features: globeShipData.map((t) => { features: globeShipData.map((t) => {
@ -1956,7 +1969,7 @@ export function Map3D({
const iconSize14 = clampNumber(0.72 * sizeScale * selectedScale, 0.45, 2.1); const iconSize14 = clampNumber(0.72 * sizeScale * selectedScale, 0.45, 2.1);
return { return {
type: "Feature", type: "Feature",
id: t.mmsi, ...(isFiniteNumber(t.mmsi) ? { id: Math.trunc(t.mmsi) } : {}),
geometry: { type: "Point", coordinates: [t.lon, t.lat] }, geometry: { type: "Point", coordinates: [t.lon, t.lat] },
properties: { properties: {
mmsi: t.mmsi, mmsi: t.mmsi,
@ -2890,7 +2903,7 @@ export function Map3D({
}, [projection, overlays.pairRange, pairLinks, mapSyncEpoch, hoveredShipSignature, hoveredPairSignature, hoveredFleetSignature, isHighlightedPair]); }, [projection, overlays.pairRange, pairLinks, mapSyncEpoch, hoveredShipSignature, hoveredPairSignature, hoveredFleetSignature, isHighlightedPair]);
const shipData = useMemo(() => { 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]); }, [targets]);
const shipByMmsi = useMemo(() => { const shipByMmsi = useMemo(() => {