fix(globe): stabilize ship symbols and deck rendering
This commit is contained in:
부모
bcd4a77f47
커밋
b8ccef23ca
@ -9,7 +9,7 @@ import maplibregl, {
|
||||
type StyleSpecification,
|
||||
type VectorSourceSpecification,
|
||||
} from "maplibre-gl";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { AisTarget } from "../../entities/aisTarget/model/types";
|
||||
import type { LegacyVesselInfo } from "../../entities/legacyVessel/model/types";
|
||||
import type { ZonesGeoJson } from "../../entities/zone/api/useZones";
|
||||
@ -87,21 +87,9 @@ function kickRepaint(map: maplibregl.Map | null) {
|
||||
}
|
||||
}
|
||||
|
||||
const EARTH_RADIUS_M = 6371008.8; // MapLibre's `earthRadius`
|
||||
const DEG2RAD = Math.PI / 180;
|
||||
|
||||
function clampExpr(inputExpr: unknown, minValue: number, maxValue: number): unknown[] {
|
||||
return ["min", ["max", inputExpr, minValue], maxValue];
|
||||
}
|
||||
|
||||
function lngLatToUnitSphere(lon: number, lat: number, altitudeMeters = 0): [number, number, number] {
|
||||
const lambda = lon * DEG2RAD;
|
||||
const phi = lat * DEG2RAD;
|
||||
const cosPhi = Math.cos(phi);
|
||||
const s = 1 + altitudeMeters / EARTH_RADIUS_M;
|
||||
// MapLibre globe space: x = east, y = north, z = lon=0 at equator.
|
||||
return [Math.sin(lambda) * cosPhi * s, Math.sin(phi) * s, Math.cos(lambda) * cosPhi * s];
|
||||
}
|
||||
const clampNumber = (value: number, minValue: number, maxValue: number) => Math.max(minValue, Math.min(maxValue, value));
|
||||
|
||||
const LEGACY_CODE_COLORS: Record<string, [number, number, number]> = {
|
||||
PT: [30, 64, 175], // #1e40af
|
||||
@ -228,7 +216,8 @@ function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: str
|
||||
// Very low zoom tiles can contain extremely complex polygons (coastline/detail),
|
||||
// which may exceed MapLibre's per-segment 16-bit vertex limit and render incorrectly.
|
||||
// We keep the fill starting at a more reasonable zoom.
|
||||
minzoom: 4,
|
||||
minzoom: 6,
|
||||
maxzoom: 12,
|
||||
paint: {
|
||||
// Dark-mode friendly palette (shallow = slightly brighter; deep = near-black).
|
||||
"fill-color": bathyFillColor,
|
||||
@ -236,38 +225,15 @@ function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: str
|
||||
},
|
||||
} as unknown as LayerSpecification;
|
||||
|
||||
const bathyExtrusion: LayerSpecification = {
|
||||
id: "bathymetry-extrusion",
|
||||
type: "fill-extrusion",
|
||||
source: oceanSourceId,
|
||||
"source-layer": "contour",
|
||||
minzoom: 6,
|
||||
paint: {
|
||||
"fill-extrusion-color": bathyFillColor,
|
||||
// MapLibre fill-extrusion cannot go below 0m, so we exaggerate the "relative seabed height"
|
||||
// (shallow areas higher, deep areas lower) to create a stepped relief.
|
||||
"fill-extrusion-base": 0,
|
||||
// NOTE: `zoom` can only appear as the input to a top-level `step`/`interpolate`.
|
||||
"fill-extrusion-height": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
6,
|
||||
["*", ["+", depth, 12000], 0.002], // depth is negative; -> range [0..12000]
|
||||
10,
|
||||
["*", ["+", depth, 12000], 0.01],
|
||||
],
|
||||
"fill-extrusion-opacity": ["interpolate", ["linear"], ["zoom"], 6, 0.0, 7, 0.25, 10, 0.55],
|
||||
"fill-extrusion-vertical-gradient": true,
|
||||
},
|
||||
} as unknown as LayerSpecification;
|
||||
|
||||
|
||||
const bathyBandBorders: LayerSpecification = {
|
||||
id: "bathymetry-borders",
|
||||
type: "line",
|
||||
source: oceanSourceId,
|
||||
"source-layer": "contour",
|
||||
minzoom: 4,
|
||||
minzoom: 6,
|
||||
maxzoom: 14,
|
||||
paint: {
|
||||
"line-color": "rgba(255,255,255,0.06)",
|
||||
"line-opacity": ["interpolate", ["linear"], ["zoom"], 4, 0.12, 8, 0.18, 12, 0.22],
|
||||
@ -304,10 +270,9 @@ function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: str
|
||||
|
||||
const majorDepths = [-50, -100, -200, -500, -1000, -2000, -4000, -6000, -8000, -9500];
|
||||
const bathyMajorDepthFilter: unknown[] = [
|
||||
"match",
|
||||
"in",
|
||||
["to-number", ["get", "depth"]],
|
||||
...majorDepths.map((v) => [v, true]).flat(),
|
||||
false,
|
||||
["literal", majorDepths],
|
||||
] as unknown[];
|
||||
|
||||
const bathyLinesMajor: LayerSpecification = {
|
||||
@ -316,6 +281,7 @@ function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: str
|
||||
source: oceanSourceId,
|
||||
"source-layer": "contour_line",
|
||||
minzoom: 8,
|
||||
maxzoom: 14,
|
||||
filter: bathyMajorDepthFilter as unknown as unknown[],
|
||||
paint: {
|
||||
"line-color": "rgba(255,255,255,0.16)",
|
||||
@ -331,6 +297,7 @@ function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: str
|
||||
source: oceanSourceId,
|
||||
"source-layer": "contour",
|
||||
minzoom: 4,
|
||||
maxzoom: 14,
|
||||
filter: bathyMajorDepthFilter as unknown as unknown[],
|
||||
paint: {
|
||||
"line-color": "rgba(255,255,255,0.14)",
|
||||
@ -394,7 +361,6 @@ function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: str
|
||||
|
||||
const toInsert = [
|
||||
bathyFill,
|
||||
bathyExtrusion,
|
||||
bathyBandBorders,
|
||||
bathyBandBordersMajor,
|
||||
bathyLinesMinor,
|
||||
@ -486,6 +452,7 @@ type PairRangeCircle = {
|
||||
warn: boolean;
|
||||
};
|
||||
|
||||
const EARTH_RADIUS_M = 6371008.8; // MapLibre's `earthRadius`
|
||||
const DECK_VIEW_ID = "mapbox";
|
||||
|
||||
export function Map3D({
|
||||
@ -507,10 +474,11 @@ export function Map3D({
|
||||
const mapRef = useRef<maplibregl.Map | null>(null);
|
||||
const overlayRef = useRef<MapboxOverlay | null>(null);
|
||||
const globeDeckLayerRef = useRef<MaplibreDeckCustomLayer | null>(null);
|
||||
const prevGlobeSelectedRef = useRef<number | null>(null);
|
||||
const globeShipsEpochRef = useRef(-1);
|
||||
const showSeamarkRef = useRef(settings.showSeamark);
|
||||
const baseMapRef = useRef<BaseMapId>(baseMap);
|
||||
const projectionRef = useRef<MapProjectionId>(projection);
|
||||
const [mapSyncEpoch, setMapSyncEpoch] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
showSeamarkRef.current = settings.showSeamark;
|
||||
@ -675,29 +643,56 @@ export function Map3D({
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
let cancelled = false;
|
||||
let retries = 0;
|
||||
const maxRetries = 6;
|
||||
|
||||
const syncProjectionAndDeck = () => {
|
||||
if (cancelled) return;
|
||||
|
||||
if (!map.isStyleLoaded()) {
|
||||
if (!cancelled && retries < maxRetries) {
|
||||
retries += 1;
|
||||
window.requestAnimationFrame(() => syncProjectionAndDeck());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const next = projection;
|
||||
try {
|
||||
map.setProjection({ type: projection });
|
||||
map.setRenderWorldCopies(projection !== "globe");
|
||||
map.setProjection({ type: next });
|
||||
map.setRenderWorldCopies(next !== "globe");
|
||||
} catch (e) {
|
||||
if (!cancelled && retries < maxRetries) {
|
||||
retries += 1;
|
||||
window.requestAnimationFrame(() => syncProjectionAndDeck());
|
||||
return;
|
||||
}
|
||||
console.warn("Projection switch failed:", e);
|
||||
}
|
||||
|
||||
if (projection === "globe") {
|
||||
// Tear down MapboxOverlay (mercator) and use a MapLibre custom layer that renders Deck
|
||||
// with MapLibre's globe MVP matrix. This avoids the Deck <-> MapLibre globe mismatch.
|
||||
const old = overlayRef.current;
|
||||
if (old) {
|
||||
const oldOverlay = overlayRef.current;
|
||||
if (projection === "globe" && oldOverlay) {
|
||||
// Globe mode uses custom MapLibre deck layers and should fully replace Mercator overlays.
|
||||
try {
|
||||
old.finalize();
|
||||
oldOverlay.finalize();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
overlayRef.current = null;
|
||||
}
|
||||
|
||||
if (projection === "globe") {
|
||||
// Ensure any stale layer from old mode is dropped then re-added on this projection.
|
||||
if (globeDeckLayerRef.current) {
|
||||
try {
|
||||
if (map.getLayer(globeDeckLayerRef.current.id)) {
|
||||
map.removeLayer(globeDeckLayerRef.current.id);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (!globeDeckLayerRef.current) {
|
||||
globeDeckLayerRef.current = new MaplibreDeckCustomLayer({
|
||||
id: "deck-globe",
|
||||
@ -745,6 +740,8 @@ export function Map3D({
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
setMapSyncEpoch((prev) => prev + 1);
|
||||
};
|
||||
|
||||
if (map.isStyleLoaded()) syncProjectionAndDeck();
|
||||
@ -778,6 +775,7 @@ export function Map3D({
|
||||
map.once("style.load", () => {
|
||||
kickRepaint(map);
|
||||
requestAnimationFrame(() => kickRepaint(map));
|
||||
setMapSyncEpoch((prev) => prev + 1);
|
||||
});
|
||||
} catch (e) {
|
||||
if (cancelled) return;
|
||||
@ -801,7 +799,7 @@ export function Map3D({
|
||||
if (!map.isStyleLoaded()) return;
|
||||
const disableBathyHeavy = projection === "globe" && baseMap === "enhanced";
|
||||
const visHeavy = disableBathyHeavy ? "none" : "visible";
|
||||
const disableBaseMapSea = projection === "globe" && baseMap === "enhanced";
|
||||
const disableBaseMapSea = projection === "globe";
|
||||
const seaVisibility = disableBaseMapSea ? "none" : "visible";
|
||||
const seaRegex = /(water|sea|ocean|river|lake|coast|bay)/i;
|
||||
|
||||
@ -822,17 +820,20 @@ export function Map3D({
|
||||
}
|
||||
}
|
||||
|
||||
// Vector basemap water-style layers can flicker on globe with dense symbols/fills in this stack.
|
||||
// Hide them only in globe/enhanced mode and restore on return.
|
||||
// Vector basemap water/raster layers can flicker on globe with dense symbols/fills in this stack.
|
||||
// Hide them only in globe mode and restore on return.
|
||||
try {
|
||||
for (const layer of map.getStyle().layers || []) {
|
||||
const id = String(layer.id ?? "");
|
||||
if (!id) continue;
|
||||
const sourceLayer = String((layer as Record<string, unknown>)["source-layer"] ?? "").toLowerCase();
|
||||
const source = String((layer as { source?: unknown }).source ?? "").toLowerCase();
|
||||
const type = String((layer as { type?: unknown }).type ?? "").toLowerCase();
|
||||
const isSea = seaRegex.test(id) || seaRegex.test(sourceLayer) || seaRegex.test(source);
|
||||
if (!isSea) continue;
|
||||
const isRaster = type === "raster";
|
||||
if (!isSea && !isRaster) continue;
|
||||
if (!map.getLayer(id)) continue;
|
||||
if (isRaster && id === "seamark") continue;
|
||||
try {
|
||||
map.setLayoutProperty(id, "visibility", seaVisibility);
|
||||
} catch {
|
||||
@ -853,7 +854,7 @@ export function Map3D({
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
}, [projection, baseMap]);
|
||||
}, [projection, baseMap, mapSyncEpoch]);
|
||||
|
||||
// seamark toggle
|
||||
useEffect(() => {
|
||||
@ -983,7 +984,7 @@ export function Map3D({
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
}, [zones, overlays.zones, projection, baseMap]);
|
||||
}, [zones, overlays.zones, projection, baseMap, mapSyncEpoch]);
|
||||
|
||||
// Ships in globe mode: render with MapLibre symbol layers so icons can be aligned to the globe surface.
|
||||
// Deck IconLayer billboards or uses a fixed plane, which looks like ships are pointing into the sky/ground on globe.
|
||||
@ -1010,7 +1011,6 @@ export function Map3D({
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
prevGlobeSelectedRef.current = null;
|
||||
kickRepaint(map);
|
||||
};
|
||||
|
||||
@ -1064,6 +1064,11 @@ export function Map3D({
|
||||
return;
|
||||
}
|
||||
|
||||
if (globeShipsEpochRef.current !== mapSyncEpoch) {
|
||||
remove();
|
||||
globeShipsEpochRef.current = mapSyncEpoch;
|
||||
}
|
||||
|
||||
try {
|
||||
ensureImage();
|
||||
} catch (e) {
|
||||
@ -1077,7 +1082,14 @@ export function Map3D({
|
||||
const legacy = legacyHits?.get(t.mmsi) ?? null;
|
||||
const cog = isFiniteNumber(t.cog) ? t.cog : 0;
|
||||
const cogNorm = ((cog % 360) + 360) % 360;
|
||||
const cog4 = (Math.round(cogNorm / 90) % 4) * 90;
|
||||
const hull = clampNumber((isFiniteNumber(t.length) ? t.length : 0) + (isFiniteNumber(t.width) ? t.width : 0) * 3, 50, 420);
|
||||
const sizeScale = clampNumber(0.85 + (hull - 50) / 420, 0.82, 1.85);
|
||||
const selected = t.mmsi === selectedMmsi;
|
||||
const selectedScale = selected ? 1.08 : 1;
|
||||
const iconSize3 = clampNumber(0.35 * sizeScale * selectedScale, 0.25, 1.3);
|
||||
const iconSize7 = clampNumber(0.45 * sizeScale * selectedScale, 0.3, 1.45);
|
||||
const iconSize10 = clampNumber(0.56 * sizeScale * selectedScale, 0.35, 1.7);
|
||||
const iconSize14 = clampNumber(0.72 * sizeScale * selectedScale, 0.45, 2.1);
|
||||
return {
|
||||
type: "Feature",
|
||||
id: t.mmsi,
|
||||
@ -1085,11 +1097,14 @@ export function Map3D({
|
||||
properties: {
|
||||
mmsi: t.mmsi,
|
||||
name: t.name || "",
|
||||
cog,
|
||||
cog4,
|
||||
cog: cogNorm,
|
||||
sog: isFiniteNumber(t.sog) ? t.sog : 0,
|
||||
length: isFiniteNumber(t.length) ? t.length : 0,
|
||||
width: isFiniteNumber(t.width) ? t.width : 0,
|
||||
iconSize3,
|
||||
iconSize7,
|
||||
iconSize10,
|
||||
iconSize14,
|
||||
sizeScale,
|
||||
selected: selected ? 1 : 0,
|
||||
permitted: !!legacy,
|
||||
code: legacy?.shipCode || "",
|
||||
},
|
||||
@ -1107,20 +1122,18 @@ export function Map3D({
|
||||
}
|
||||
|
||||
const visibility = settings.showShips ? "visible" : "none";
|
||||
const isSelected = ["boolean", ["feature-state", "selected"], false] as const;
|
||||
// Style-spec restriction: only one zoom-based step/interpolate is allowed in an expression.
|
||||
const circleRadius = [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
3,
|
||||
["case", isSelected, 5, 4],
|
||||
4,
|
||||
7,
|
||||
["case", isSelected, 8, 6],
|
||||
6,
|
||||
10,
|
||||
["case", isSelected, 10, 8],
|
||||
8,
|
||||
14,
|
||||
["case", isSelected, 14, 11],
|
||||
11,
|
||||
] as const;
|
||||
|
||||
// Put ships at the top so they're always visible (especially important under globe projection).
|
||||
@ -1168,8 +1181,8 @@ export function Map3D({
|
||||
"circle-stroke-width": [
|
||||
"case",
|
||||
["boolean", ["get", "permitted"], false],
|
||||
["case", ["boolean", ["feature-state", "selected"], false], 2.5, 1.6],
|
||||
["case", ["boolean", ["feature-state", "selected"], false], 2.0, 0.0],
|
||||
["case", ["==", ["get", "selected"], 1], 2.5, 1.6],
|
||||
["case", ["==", ["get", "selected"], 1], 2.0, 0.0],
|
||||
] as unknown as number[],
|
||||
"circle-stroke-opacity": 0.8,
|
||||
},
|
||||
@ -1189,27 +1202,6 @@ export function Map3D({
|
||||
|
||||
if (!map.getLayer(symbolId)) {
|
||||
try {
|
||||
const lengthExpr: unknown[] = ["to-number", ["get", "length"], 0];
|
||||
const widthExpr: unknown[] = ["to-number", ["get", "width"], 0];
|
||||
const hullExpr: unknown[] = clampExpr(["+", lengthExpr, ["*", 3, widthExpr]], 0, 420);
|
||||
const sizeFactor: unknown[] = [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
hullExpr,
|
||||
0,
|
||||
0.85,
|
||||
40,
|
||||
0.95,
|
||||
80,
|
||||
1.0,
|
||||
160,
|
||||
1.25,
|
||||
260,
|
||||
1.55,
|
||||
350,
|
||||
1.85,
|
||||
];
|
||||
|
||||
map.addLayer(
|
||||
{
|
||||
id: symbolId,
|
||||
@ -1223,25 +1215,24 @@ export function Map3D({
|
||||
["linear"],
|
||||
["zoom"],
|
||||
3,
|
||||
["*", 0.32, sizeFactor],
|
||||
["to-number", ["get", "iconSize3"], 0.35],
|
||||
7,
|
||||
["*", 0.42, sizeFactor],
|
||||
["to-number", ["get", "iconSize7"], 0.45],
|
||||
10,
|
||||
["*", 0.52, sizeFactor],
|
||||
["to-number", ["get", "iconSize10"], 0.56],
|
||||
14,
|
||||
["*", 0.72, sizeFactor],
|
||||
["to-number", ["get", "iconSize14"], 0.72],
|
||||
] as unknown as number[],
|
||||
"icon-allow-overlap": true,
|
||||
"icon-ignore-placement": true,
|
||||
"icon-anchor": "center",
|
||||
// Debug-friendly: quantize heading to N/E/S/W while we validate globe alignment.
|
||||
"icon-rotate": ["get", "cog4"],
|
||||
"icon-rotate": ["to-number", ["get", "cog"], 0],
|
||||
// Keep the icon on the sea surface.
|
||||
"icon-rotation-alignment": "map",
|
||||
"icon-pitch-alignment": "map",
|
||||
},
|
||||
paint: {
|
||||
"icon-opacity": ["case", ["boolean", ["feature-state", "selected"], false], 1.0, 0.92],
|
||||
"icon-opacity": ["case", ["==", ["get", "selected"], 1], 1.0, 0.92],
|
||||
},
|
||||
} as unknown as LayerSpecification,
|
||||
before,
|
||||
@ -1257,19 +1248,7 @@ export function Map3D({
|
||||
}
|
||||
}
|
||||
|
||||
// Apply selection state for highlight.
|
||||
try {
|
||||
const prev = prevGlobeSelectedRef.current;
|
||||
if (prev && prev !== selectedMmsi) map.setFeatureState({ source: srcId, id: prev }, { selected: false });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
if (selectedMmsi) map.setFeatureState({ source: srcId, id: selectedMmsi }, { selected: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
prevGlobeSelectedRef.current = selectedMmsi;
|
||||
// Selection is now source-data driven (`selected` property), no per-feature state update needed.
|
||||
kickRepaint(map);
|
||||
};
|
||||
|
||||
@ -1282,7 +1261,7 @@ export function Map3D({
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
}, [projection, settings.showShips, targets, legacyHits, selectedMmsi]);
|
||||
}, [projection, settings.showShips, targets, legacyHits, selectedMmsi, mapSyncEpoch]);
|
||||
|
||||
// Globe ship click selection (MapLibre-native ships layer)
|
||||
useEffect(() => {
|
||||
@ -1291,14 +1270,14 @@ export function Map3D({
|
||||
if (projection !== "globe" || !settings.showShips) return;
|
||||
|
||||
const symbolId = "ships-globe";
|
||||
const haloId = "ships-globe-halo";
|
||||
const outlineId = "ships-globe-outline";
|
||||
const clickedRadiusDeg2 = Math.pow(0.08, 2);
|
||||
|
||||
const onClick = (e: maplibregl.MapMouseEvent) => {
|
||||
try {
|
||||
if (!map.getLayer(symbolId)) {
|
||||
onSelectMmsi(null);
|
||||
return;
|
||||
}
|
||||
const feats = map.queryRenderedFeatures(e.point, { layers: [symbolId] });
|
||||
const layerIds = [symbolId, haloId, outlineId].filter((id) => map.getLayer(id));
|
||||
const feats = layerIds.length > 0 ? map.queryRenderedFeatures(e.point, { layers: layerIds }) : [];
|
||||
const f = feats?.[0];
|
||||
const props = (f?.properties || {}) as Record<string, unknown>;
|
||||
const mmsi = Number(props.mmsi);
|
||||
@ -1306,6 +1285,25 @@ export function Map3D({
|
||||
onSelectMmsi(mmsi);
|
||||
return;
|
||||
}
|
||||
|
||||
const clicked = { lat: e.lngLat.lat, lon: e.lngLat.lng };
|
||||
const cosLat = Math.cos(clicked.lat * DEG2RAD);
|
||||
let bestMmsi: number | null = null;
|
||||
let bestD2 = Number.POSITIVE_INFINITY;
|
||||
for (const t of targets) {
|
||||
if (!isFiniteNumber(t.lat) || !isFiniteNumber(t.lon)) continue;
|
||||
const dLon = (clicked.lon - t.lon) * cosLat;
|
||||
const dLat = clicked.lat - t.lat;
|
||||
const d2 = dLon * dLon + dLat * dLat;
|
||||
if (d2 <= clickedRadiusDeg2 && d2 < bestD2) {
|
||||
bestD2 = d2;
|
||||
bestMmsi = t.mmsi;
|
||||
}
|
||||
}
|
||||
if (bestMmsi != null) {
|
||||
onSelectMmsi(bestMmsi);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@ -1320,7 +1318,7 @@ export function Map3D({
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
}, [projection, settings.showShips, onSelectMmsi]);
|
||||
}, [projection, settings.showShips, onSelectMmsi, mapSyncEpoch, targets]);
|
||||
|
||||
// Globe overlays (pair links / FC links / ranges) rendered as MapLibre GeoJSON layers.
|
||||
// Deck custom layers are more fragile under globe projection; MapLibre-native rendering stays aligned like zones.
|
||||
@ -1411,7 +1409,7 @@ export function Map3D({
|
||||
}
|
||||
remove();
|
||||
};
|
||||
}, [projection, overlays.pairLines, pairLinks]);
|
||||
}, [projection, overlays.pairLines, pairLinks, mapSyncEpoch]);
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
@ -1507,7 +1505,7 @@ export function Map3D({
|
||||
}
|
||||
remove();
|
||||
};
|
||||
}, [projection, overlays.fcLines, fcLinks]);
|
||||
}, [projection, overlays.fcLines, fcLinks, mapSyncEpoch]);
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
@ -1594,7 +1592,7 @@ export function Map3D({
|
||||
}
|
||||
remove();
|
||||
};
|
||||
}, [projection, overlays.fleetCircles, fleetCircles]);
|
||||
}, [projection, overlays.fleetCircles, fleetCircles, mapSyncEpoch]);
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
@ -1696,22 +1694,12 @@ export function Map3D({
|
||||
}
|
||||
remove();
|
||||
};
|
||||
}, [projection, overlays.pairRange, pairLinks]);
|
||||
}, [projection, overlays.pairRange, pairLinks, mapSyncEpoch]);
|
||||
|
||||
const shipData = useMemo(() => {
|
||||
return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon));
|
||||
}, [targets]);
|
||||
|
||||
const globePosByMmsi = useMemo(() => {
|
||||
if (projection !== "globe") return null;
|
||||
const m = new Map<number, [number, number, number]>();
|
||||
for (const t of shipData) {
|
||||
// Slightly above the sea surface to keep the icon readable and avoid depth-fighting.
|
||||
m.set(t.mmsi, lngLatToUnitSphere(t.lon, t.lat, 12));
|
||||
}
|
||||
return m;
|
||||
}, [projection, shipData]);
|
||||
|
||||
const legacyTargets = useMemo(() => {
|
||||
if (!legacyHits) return [];
|
||||
return shipData.filter((t) => legacyHits.has(t.mmsi));
|
||||
@ -1965,6 +1953,7 @@ export function Map3D({
|
||||
}, [
|
||||
projection,
|
||||
shipData,
|
||||
baseMap,
|
||||
zones,
|
||||
selectedMmsi,
|
||||
overlays.zones,
|
||||
@ -1981,7 +1970,7 @@ export function Map3D({
|
||||
pairRanges,
|
||||
fcDashed,
|
||||
fleetCircles,
|
||||
globePosByMmsi,
|
||||
mapSyncEpoch,
|
||||
]);
|
||||
|
||||
return <div ref={containerRef} style={{ width: "100%", height: "100%" }} />;
|
||||
|
||||
@ -169,9 +169,18 @@ export class MaplibreDeckCustomLayer implements maplibregl.CustomLayerInterface
|
||||
deck.setProps({ viewState: { [this._viewId]: { projectionMatrix, viewMatrix: IDENTITY_4x4 } } });
|
||||
}
|
||||
|
||||
try {
|
||||
deck._drawLayers("maplibre-custom", {
|
||||
clearCanvas: false,
|
||||
clearStack: true,
|
||||
});
|
||||
} catch (e) {
|
||||
// Rendering can fail transiently during style/projection transitions.
|
||||
// Keep the map responsive and request a clean pass on next frame.
|
||||
console.warn("Deck render sync failed, skipping frame:", e);
|
||||
requestAnimationFrame(() => {
|
||||
this._map?.triggerRepaint();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user