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));
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<string, unknown>)["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<GeoJSON.Point> = {
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(() => {