fix(ais,map): 2-stage bootstrap and globe overlay refresh
This commit is contained in:
부모
b8ccef23ca
커밋
c31d26124c
@ -18,6 +18,7 @@ export type AisPollingSnapshot = {
|
|||||||
|
|
||||||
export type AisPollingOptions = {
|
export type AisPollingOptions = {
|
||||||
initialMinutes?: number;
|
initialMinutes?: number;
|
||||||
|
bootstrapMinutes?: number;
|
||||||
incrementalMinutes?: number;
|
incrementalMinutes?: number;
|
||||||
intervalMs?: number;
|
intervalMs?: number;
|
||||||
retentionMinutes?: number;
|
retentionMinutes?: number;
|
||||||
@ -43,10 +44,10 @@ function upsertByMmsi(store: Map<number, AisTarget>, rows: AisTarget[]) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer newer records if the upstream ever returns stale items.
|
// Keep newer rows only. If backend returns same/older timestamp, skip.
|
||||||
const prevTs = prev.messageTimestamp ?? "";
|
const prevTs = Date.parse(prev.messageTimestamp || "");
|
||||||
const nextTs = r.messageTimestamp ?? "";
|
const nextTs = Date.parse(r.messageTimestamp || "");
|
||||||
if (nextTs && prevTs && nextTs < prevTs) continue;
|
if (Number.isFinite(prevTs) && Number.isFinite(nextTs) && nextTs <= prevTs) continue;
|
||||||
|
|
||||||
store.set(r.mmsi, r);
|
store.set(r.mmsi, r);
|
||||||
upserted += 1;
|
upserted += 1;
|
||||||
@ -112,6 +113,7 @@ function pruneStore(store: Map<number, AisTarget>, retentionMinutes: number, bbo
|
|||||||
|
|
||||||
export function useAisTargetPolling(opts: AisPollingOptions = {}) {
|
export function useAisTargetPolling(opts: AisPollingOptions = {}) {
|
||||||
const initialMinutes = opts.initialMinutes ?? 60;
|
const initialMinutes = opts.initialMinutes ?? 60;
|
||||||
|
const bootstrapMinutes = opts.bootstrapMinutes ?? initialMinutes;
|
||||||
const incrementalMinutes = opts.incrementalMinutes ?? 1;
|
const incrementalMinutes = opts.incrementalMinutes ?? 1;
|
||||||
const intervalMs = opts.intervalMs ?? 60_000;
|
const intervalMs = opts.intervalMs ?? 60_000;
|
||||||
const retentionMinutes = opts.retentionMinutes ?? initialMinutes;
|
const retentionMinutes = opts.retentionMinutes ?? initialMinutes;
|
||||||
@ -122,7 +124,7 @@ export function useAisTargetPolling(opts: AisPollingOptions = {}) {
|
|||||||
const radiusMeters = opts.radiusMeters;
|
const radiusMeters = opts.radiusMeters;
|
||||||
|
|
||||||
const storeRef = useRef<Map<number, AisTarget>>(new Map());
|
const storeRef = useRef<Map<number, AisTarget>>(new Map());
|
||||||
const inFlightRef = useRef(false);
|
const generationRef = useRef(0);
|
||||||
|
|
||||||
const [rev, setRev] = useState(0);
|
const [rev, setRev] = useState(0);
|
||||||
const [snapshot, setSnapshot] = useState<AisPollingSnapshot>({
|
const [snapshot, setSnapshot] = useState<AisPollingSnapshot>({
|
||||||
@ -142,10 +144,9 @@ export function useAisTargetPolling(opts: AisPollingOptions = {}) {
|
|||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
const generation = ++generationRef.current;
|
||||||
|
|
||||||
async function run(minutes: number) {
|
async function run(minutes: number, context: "bootstrap" | "initial" | "incremental") {
|
||||||
if (inFlightRef.current) return;
|
|
||||||
inFlightRef.current = true;
|
|
||||||
try {
|
try {
|
||||||
setSnapshot((s) => ({ ...s, status: s.status === "idle" ? "loading" : s.status, error: null }));
|
setSnapshot((s) => ({ ...s, status: s.status === "idle" ? "loading" : s.status, error: null }));
|
||||||
|
|
||||||
@ -159,7 +160,7 @@ export function useAisTargetPolling(opts: AisPollingOptions = {}) {
|
|||||||
},
|
},
|
||||||
controller.signal,
|
controller.signal,
|
||||||
);
|
);
|
||||||
if (cancelled) return;
|
if (cancelled || generation !== generationRef.current) return;
|
||||||
|
|
||||||
const { inserted, upserted } = upsertByMmsi(storeRef.current, res.data);
|
const { inserted, upserted } = upsertByMmsi(storeRef.current, res.data);
|
||||||
const deleted = pruneStore(storeRef.current, retentionMinutes, bbox);
|
const deleted = pruneStore(storeRef.current, retentionMinutes, bbox);
|
||||||
@ -179,14 +180,12 @@ export function useAisTargetPolling(opts: AisPollingOptions = {}) {
|
|||||||
});
|
});
|
||||||
setRev((r) => r + 1);
|
setRev((r) => r + 1);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (cancelled) return;
|
if (cancelled || generation !== generationRef.current) return;
|
||||||
setSnapshot((s) => ({
|
setSnapshot((s) => ({
|
||||||
...s,
|
...s,
|
||||||
status: "error",
|
status: context === "incremental" ? s.status : "error",
|
||||||
error: e instanceof Error ? e.message : String(e),
|
error: e instanceof Error ? e.message : String(e),
|
||||||
}));
|
}));
|
||||||
} finally {
|
|
||||||
inFlightRef.current = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,15 +204,30 @@ export function useAisTargetPolling(opts: AisPollingOptions = {}) {
|
|||||||
});
|
});
|
||||||
setRev((r) => r + 1);
|
setRev((r) => r + 1);
|
||||||
|
|
||||||
void run(initialMinutes);
|
void run(bootstrapMinutes, "bootstrap");
|
||||||
const id = window.setInterval(() => void run(incrementalMinutes), intervalMs);
|
if (bootstrapMinutes !== initialMinutes) {
|
||||||
|
void run(initialMinutes, "initial");
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = window.setInterval(() => void run(incrementalMinutes, "incremental"), intervalMs);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
controller.abort();
|
controller.abort();
|
||||||
window.clearInterval(id);
|
window.clearInterval(id);
|
||||||
};
|
};
|
||||||
}, [initialMinutes, incrementalMinutes, intervalMs, retentionMinutes, bbox, centerLon, centerLat, radiusMeters, enabled]);
|
}, [
|
||||||
|
initialMinutes,
|
||||||
|
bootstrapMinutes,
|
||||||
|
incrementalMinutes,
|
||||||
|
intervalMs,
|
||||||
|
retentionMinutes,
|
||||||
|
bbox,
|
||||||
|
centerLon,
|
||||||
|
centerLat,
|
||||||
|
radiusMeters,
|
||||||
|
enabled,
|
||||||
|
]);
|
||||||
|
|
||||||
const targets = useMemo(() => {
|
const targets = useMemo(() => {
|
||||||
// `rev` is a version counter so we recompute the array snapshot when the store changes.
|
// `rev` is a version counter so we recompute the array snapshot when the store changes.
|
||||||
|
|||||||
@ -76,6 +76,7 @@ export function DashboardPage() {
|
|||||||
|
|
||||||
const { targets, snapshot } = useAisTargetPolling({
|
const { targets, snapshot } = useAisTargetPolling({
|
||||||
initialMinutes: 60,
|
initialMinutes: 60,
|
||||||
|
bootstrapMinutes: 10,
|
||||||
incrementalMinutes: 2,
|
incrementalMinutes: 2,
|
||||||
intervalMs: 60_000,
|
intervalMs: 60_000,
|
||||||
retentionMinutes: 90,
|
retentionMinutes: 90,
|
||||||
|
|||||||
@ -87,10 +87,84 @@ function kickRepaint(map: maplibregl.Map | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onMapStyleReady(map: maplibregl.Map, callback: () => void) {
|
||||||
|
if (map.isStyleLoaded()) {
|
||||||
|
callback();
|
||||||
|
return () => {
|
||||||
|
// noop
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let fired = false;
|
||||||
|
const runOnce = () => {
|
||||||
|
if (fired || !map.isStyleLoaded()) return;
|
||||||
|
fired = true;
|
||||||
|
callback();
|
||||||
|
try {
|
||||||
|
map.off("style.load", runOnce);
|
||||||
|
map.off("styledata", runOnce);
|
||||||
|
map.off("idle", runOnce);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
map.on("style.load", runOnce);
|
||||||
|
map.on("styledata", runOnce);
|
||||||
|
map.on("idle", runOnce);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (fired) return;
|
||||||
|
fired = true;
|
||||||
|
try {
|
||||||
|
map.off("style.load", runOnce);
|
||||||
|
map.off("styledata", runOnce);
|
||||||
|
map.off("idle", runOnce);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const DEG2RAD = Math.PI / 180;
|
const DEG2RAD = Math.PI / 180;
|
||||||
|
|
||||||
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 rgbToHex(rgb: [number, number, number]) {
|
||||||
|
const toHex = (v: number) => {
|
||||||
|
const clamped = Math.max(0, Math.min(255, Math.round(v)));
|
||||||
|
return clamped.toString(16).padStart(2, "0");
|
||||||
|
};
|
||||||
|
|
||||||
|
return `#${toHex(rgb[0])}${toHex(rgb[1])}${toHex(rgb[2])}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lightenColor(rgb: [number, number, number], ratio = 0.32) {
|
||||||
|
const out = rgb.map((v) => Math.round(v + (255 - v) * ratio) as number) as [number, number, number];
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGlobeShipColor({
|
||||||
|
selected,
|
||||||
|
legacy,
|
||||||
|
sog,
|
||||||
|
}: {
|
||||||
|
selected: boolean;
|
||||||
|
legacy: string | null;
|
||||||
|
sog: number | null;
|
||||||
|
}) {
|
||||||
|
if (selected) return "rgba(255,255,255,0.98)";
|
||||||
|
if (legacy) {
|
||||||
|
const rgb = LEGACY_CODE_COLORS[legacy];
|
||||||
|
if (rgb) return rgbToHex(lightenColor(rgb, 0.38));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFiniteNumber(sog)) return "rgba(100,116,139,0.75)";
|
||||||
|
if (sog >= 10) return "#3b82f6";
|
||||||
|
if (sog >= 1) return "#22c55e";
|
||||||
|
return "rgba(100,116,139,0.75)";
|
||||||
|
}
|
||||||
|
|
||||||
const LEGACY_CODE_COLORS: Record<string, [number, number, number]> = {
|
const LEGACY_CODE_COLORS: Record<string, [number, number, number]> = {
|
||||||
PT: [30, 64, 175], // #1e40af
|
PT: [30, 64, 175], // #1e40af
|
||||||
"PT-S": [234, 88, 12], // #ea580c
|
"PT-S": [234, 88, 12], // #ea580c
|
||||||
@ -100,15 +174,6 @@ const LEGACY_CODE_COLORS: Record<string, [number, number, number]> = {
|
|||||||
FC: [245, 158, 11], // #f59e0b
|
FC: [245, 158, 11], // #f59e0b
|
||||||
};
|
};
|
||||||
|
|
||||||
const LEGACY_CODE_HEX: Record<string, string> = {
|
|
||||||
PT: "#1e40af",
|
|
||||||
"PT-S": "#ea580c",
|
|
||||||
GN: "#10b981",
|
|
||||||
OT: "#8b5cf6",
|
|
||||||
PS: "#ef4444",
|
|
||||||
FC: "#f59e0b",
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEPTH_DISABLED_PARAMS = {
|
const DEPTH_DISABLED_PARAMS = {
|
||||||
// In MapLibre+Deck (interleaved), the map often leaves a depth buffer populated.
|
// In MapLibre+Deck (interleaved), the map often leaves a depth buffer populated.
|
||||||
// For 2D overlays like zones/icons/halos we want stable painter's-order rendering
|
// For 2D overlays like zones/icons/halos we want stable painter's-order rendering
|
||||||
@ -480,6 +545,14 @@ export function Map3D({
|
|||||||
const projectionRef = useRef<MapProjectionId>(projection);
|
const projectionRef = useRef<MapProjectionId>(projection);
|
||||||
const [mapSyncEpoch, setMapSyncEpoch] = useState(0);
|
const [mapSyncEpoch, setMapSyncEpoch] = useState(0);
|
||||||
|
|
||||||
|
const pulseMapSync = () => {
|
||||||
|
setMapSyncEpoch((prev) => prev + 1);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
kickRepaint(mapRef.current);
|
||||||
|
setMapSyncEpoch((prev) => prev + 1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
showSeamarkRef.current = settings.showSeamark;
|
showSeamarkRef.current = settings.showSeamark;
|
||||||
}, [settings.showSeamark]);
|
}, [settings.showSeamark]);
|
||||||
@ -561,7 +634,7 @@ export function Map3D({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the seamark raster overlay exists even when using MapTiler vector styles.
|
// Ensure the seamark raster overlay exists even when using MapTiler vector styles.
|
||||||
map.on("style.load", () => {
|
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)) {
|
if (projectionRef.current === "globe" && globeDeckLayerRef.current && !map!.getLayer(globeDeckLayerRef.current.id)) {
|
||||||
@ -740,20 +813,20 @@ export function Map3D({
|
|||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
pulseMapSync();
|
||||||
setMapSyncEpoch((prev) => prev + 1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (map.isStyleLoaded()) syncProjectionAndDeck();
|
if (map.isStyleLoaded()) syncProjectionAndDeck();
|
||||||
else map.once("style.load", syncProjectionAndDeck);
|
else {
|
||||||
|
const stop = onMapStyleReady(map, syncProjectionAndDeck);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
stop();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
try {
|
|
||||||
map.off("style.load", syncProjectionAndDeck);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [projection]);
|
}, [projection]);
|
||||||
|
|
||||||
@ -764,6 +837,7 @@ export function Map3D({
|
|||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
let stop: (() => void) | null = null;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@ -772,10 +846,10 @@ export function Map3D({
|
|||||||
// Disable style diff to avoid warnings with custom layers (Deck MapboxOverlay) and
|
// Disable style diff to avoid warnings with custom layers (Deck MapboxOverlay) and
|
||||||
// to ensure a clean rebuild when switching between very different styles.
|
// to ensure a clean rebuild when switching between very different styles.
|
||||||
map.setStyle(style, { diff: false });
|
map.setStyle(style, { diff: false });
|
||||||
map.once("style.load", () => {
|
stop = onMapStyleReady(map, () => {
|
||||||
kickRepaint(map);
|
kickRepaint(map);
|
||||||
requestAnimationFrame(() => kickRepaint(map));
|
requestAnimationFrame(() => kickRepaint(map));
|
||||||
setMapSyncEpoch((prev) => prev + 1);
|
pulseMapSync();
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
@ -786,6 +860,7 @@ export function Map3D({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
controller.abort();
|
controller.abort();
|
||||||
|
stop?.();
|
||||||
};
|
};
|
||||||
}, [baseMap]);
|
}, [baseMap]);
|
||||||
|
|
||||||
@ -845,14 +920,9 @@ export function Map3D({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (map.isStyleLoaded()) apply();
|
const stop = onMapStyleReady(map, apply);
|
||||||
map.on("style.load", apply);
|
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
stop();
|
||||||
map.off("style.load", apply);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [projection, baseMap, mapSyncEpoch]);
|
}, [projection, baseMap, mapSyncEpoch]);
|
||||||
|
|
||||||
@ -975,14 +1045,9 @@ export function Map3D({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (map.isStyleLoaded()) ensure();
|
const stop = onMapStyleReady(map, ensure);
|
||||||
map.on("style.load", ensure);
|
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
stop();
|
||||||
map.off("style.load", ensure);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [zones, overlays.zones, projection, baseMap, mapSyncEpoch]);
|
}, [zones, overlays.zones, projection, baseMap, mapSyncEpoch]);
|
||||||
|
|
||||||
@ -1040,22 +1105,9 @@ export function Map3D({
|
|||||||
ctx.fillRect(size / 2 - 8, 34, 16, 18);
|
ctx.fillRect(size / 2 - 8, 34, 16, 18);
|
||||||
|
|
||||||
const img = ctx.getImageData(0, 0, size, size);
|
const img = ctx.getImageData(0, 0, size, size);
|
||||||
map.addImage(imgId, img, { pixelRatio: 2 });
|
map.addImage(imgId, img, { pixelRatio: 2, sdf: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const speedColorExpr: unknown[] = [
|
|
||||||
"case",
|
|
||||||
[">=", ["to-number", ["get", "sog"]], 10],
|
|
||||||
"#3b82f6",
|
|
||||||
[">=", ["to-number", ["get", "sog"]], 1],
|
|
||||||
"#22c55e",
|
|
||||||
"#64748b",
|
|
||||||
];
|
|
||||||
|
|
||||||
const codeColorExpr: unknown[] = ["match", ["get", "code"]];
|
|
||||||
for (const [k, hex] of Object.entries(LEGACY_CODE_HEX)) codeColorExpr.push(k, hex);
|
|
||||||
codeColorExpr.push(speedColorExpr);
|
|
||||||
|
|
||||||
const ensure = () => {
|
const ensure = () => {
|
||||||
if (!map.isStyleLoaded()) return;
|
if (!map.isStyleLoaded()) return;
|
||||||
|
|
||||||
@ -1099,6 +1151,11 @@ export function Map3D({
|
|||||||
name: t.name || "",
|
name: t.name || "",
|
||||||
cog: cogNorm,
|
cog: cogNorm,
|
||||||
sog: isFiniteNumber(t.sog) ? t.sog : 0,
|
sog: isFiniteNumber(t.sog) ? t.sog : 0,
|
||||||
|
shipColor: getGlobeShipColor({
|
||||||
|
selected,
|
||||||
|
legacy: legacy?.shipCode || null,
|
||||||
|
sog: isFiniteNumber(t.sog) ? t.sog : null,
|
||||||
|
}),
|
||||||
iconSize3,
|
iconSize3,
|
||||||
iconSize7,
|
iconSize7,
|
||||||
iconSize10,
|
iconSize10,
|
||||||
@ -1149,7 +1206,7 @@ export function Map3D({
|
|||||||
layout: { visibility },
|
layout: { visibility },
|
||||||
paint: {
|
paint: {
|
||||||
"circle-radius": circleRadius as never,
|
"circle-radius": circleRadius as never,
|
||||||
"circle-color": codeColorExpr as never,
|
"circle-color": ["coalesce", ["get", "shipColor"], "#64748b"] as never,
|
||||||
"circle-opacity": 0.22,
|
"circle-opacity": 0.22,
|
||||||
},
|
},
|
||||||
} as unknown as LayerSpecification,
|
} as unknown as LayerSpecification,
|
||||||
@ -1177,7 +1234,7 @@ export function Map3D({
|
|||||||
paint: {
|
paint: {
|
||||||
"circle-radius": circleRadius as never,
|
"circle-radius": circleRadius as never,
|
||||||
"circle-color": "rgba(0,0,0,0)",
|
"circle-color": "rgba(0,0,0,0)",
|
||||||
"circle-stroke-color": codeColorExpr as never,
|
"circle-stroke-color": ["coalesce", ["get", "shipColor"], "#64748b"] as never,
|
||||||
"circle-stroke-width": [
|
"circle-stroke-width": [
|
||||||
"case",
|
"case",
|
||||||
["boolean", ["get", "permitted"], false],
|
["boolean", ["get", "permitted"], false],
|
||||||
@ -1229,10 +1286,13 @@ export function Map3D({
|
|||||||
"icon-rotate": ["to-number", ["get", "cog"], 0],
|
"icon-rotate": ["to-number", ["get", "cog"], 0],
|
||||||
// Keep the icon on the sea surface.
|
// Keep the icon on the sea surface.
|
||||||
"icon-rotation-alignment": "map",
|
"icon-rotation-alignment": "map",
|
||||||
"icon-pitch-alignment": "map",
|
"icon-pitch-alignment": "viewport",
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
|
"icon-color": ["coalesce", ["get", "shipColor"], "#64748b"] as never,
|
||||||
"icon-opacity": ["case", ["==", ["get", "selected"], 1], 1.0, 0.92],
|
"icon-opacity": ["case", ["==", ["get", "selected"], 1], 1.0, 0.92],
|
||||||
|
"icon-halo-color": "rgba(15,23,42,0.25)",
|
||||||
|
"icon-halo-width": 1,
|
||||||
},
|
},
|
||||||
} as unknown as LayerSpecification,
|
} as unknown as LayerSpecification,
|
||||||
before,
|
before,
|
||||||
@ -1252,14 +1312,9 @@ export function Map3D({
|
|||||||
kickRepaint(map);
|
kickRepaint(map);
|
||||||
};
|
};
|
||||||
|
|
||||||
ensure();
|
const stop = onMapStyleReady(map, ensure);
|
||||||
map.on("style.load", ensure);
|
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
stop();
|
||||||
map.off("style.load", ensure);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [projection, settings.showShips, targets, legacyHits, selectedMmsi, mapSyncEpoch]);
|
}, [projection, settings.showShips, targets, legacyHits, selectedMmsi, mapSyncEpoch]);
|
||||||
|
|
||||||
@ -1277,9 +1332,19 @@ export function Map3D({
|
|||||||
const onClick = (e: maplibregl.MapMouseEvent) => {
|
const onClick = (e: maplibregl.MapMouseEvent) => {
|
||||||
try {
|
try {
|
||||||
const layerIds = [symbolId, haloId, outlineId].filter((id) => map.getLayer(id));
|
const layerIds = [symbolId, haloId, outlineId].filter((id) => map.getLayer(id));
|
||||||
const feats = layerIds.length > 0 ? map.queryRenderedFeatures(e.point, { layers: layerIds }) : [];
|
let feats: unknown[] = [];
|
||||||
|
if (layerIds.length > 0) {
|
||||||
|
try {
|
||||||
|
feats = map.queryRenderedFeatures(e.point, { layers: layerIds }) as unknown[];
|
||||||
|
} catch {
|
||||||
|
feats = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
const f = feats?.[0];
|
const f = feats?.[0];
|
||||||
const props = (f?.properties || {}) as Record<string, unknown>;
|
const props = ((f as { properties?: Record<string, unknown> } | undefined)?.properties || {}) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
const mmsi = Number(props.mmsi);
|
const mmsi = Number(props.mmsi);
|
||||||
if (Number.isFinite(mmsi)) {
|
if (Number.isFinite(mmsi)) {
|
||||||
onSelectMmsi(mmsi);
|
onSelectMmsi(mmsi);
|
||||||
@ -1399,14 +1464,10 @@ export function Map3D({
|
|||||||
kickRepaint(map);
|
kickRepaint(map);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stop = onMapStyleReady(map, ensure);
|
||||||
ensure();
|
ensure();
|
||||||
map.on("style.load", ensure);
|
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
stop();
|
||||||
map.off("style.load", ensure);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
remove();
|
remove();
|
||||||
};
|
};
|
||||||
}, [projection, overlays.pairLines, pairLinks, mapSyncEpoch]);
|
}, [projection, overlays.pairLines, pairLinks, mapSyncEpoch]);
|
||||||
@ -1495,14 +1556,10 @@ export function Map3D({
|
|||||||
kickRepaint(map);
|
kickRepaint(map);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stop = onMapStyleReady(map, ensure);
|
||||||
ensure();
|
ensure();
|
||||||
map.on("style.load", ensure);
|
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
stop();
|
||||||
map.off("style.load", ensure);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
remove();
|
remove();
|
||||||
};
|
};
|
||||||
}, [projection, overlays.fcLines, fcLinks, mapSyncEpoch]);
|
}, [projection, overlays.fcLines, fcLinks, mapSyncEpoch]);
|
||||||
@ -1582,14 +1639,10 @@ export function Map3D({
|
|||||||
kickRepaint(map);
|
kickRepaint(map);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stop = onMapStyleReady(map, ensure);
|
||||||
ensure();
|
ensure();
|
||||||
map.on("style.load", ensure);
|
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
stop();
|
||||||
map.off("style.load", ensure);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
remove();
|
remove();
|
||||||
};
|
};
|
||||||
}, [projection, overlays.fleetCircles, fleetCircles, mapSyncEpoch]);
|
}, [projection, overlays.fleetCircles, fleetCircles, mapSyncEpoch]);
|
||||||
@ -1684,14 +1737,10 @@ export function Map3D({
|
|||||||
kickRepaint(map);
|
kickRepaint(map);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stop = onMapStyleReady(map, ensure);
|
||||||
ensure();
|
ensure();
|
||||||
map.on("style.load", ensure);
|
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
stop();
|
||||||
map.off("style.load", ensure);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
remove();
|
remove();
|
||||||
};
|
};
|
||||||
}, [projection, overlays.pairRange, pairLinks, mapSyncEpoch]);
|
}, [projection, overlays.pairRange, pairLinks, mapSyncEpoch]);
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user