fix(globe): force repaints; maplibre overlays; disable bathy raster
This commit is contained in:
부모
dc0729fc5f
커밋
7f72ab651d
@ -6,7 +6,6 @@ import maplibregl, {
|
||||
type GeoJSONSource,
|
||||
type GeoJSONSourceSpecification,
|
||||
type LayerSpecification,
|
||||
type RasterDEMSourceSpecification,
|
||||
type StyleSpecification,
|
||||
type VectorSourceSpecification,
|
||||
} from "maplibre-gl";
|
||||
@ -61,6 +60,33 @@ function isFiniteNumber(x: unknown): x is number {
|
||||
return typeof x === "number" && Number.isFinite(x);
|
||||
}
|
||||
|
||||
function kickRepaint(map: maplibregl.Map | null) {
|
||||
if (!map) return;
|
||||
try {
|
||||
map.triggerRepaint();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
map.triggerRepaint();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
map.triggerRepaint();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// ignore (e.g., non-browser env)
|
||||
}
|
||||
}
|
||||
|
||||
const EARTH_RADIUS_M = 6371008.8; // MapLibre's `earthRadius`
|
||||
const DEG2RAD = Math.PI / 180;
|
||||
|
||||
@ -142,8 +168,10 @@ function ensureSeamarkOverlay(map: maplibregl.Map, beforeLayerId?: string) {
|
||||
}
|
||||
|
||||
function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: string) {
|
||||
// NOTE: Vector-only bathymetry injection.
|
||||
// Raster/DEM hillshade was intentionally removed for now because it caused ocean flicker
|
||||
// and extra PNG tile traffic under globe projection in our setup.
|
||||
const oceanSourceId = "maptiler-ocean";
|
||||
const terrainSourceId = "maptiler-terrain";
|
||||
|
||||
if (!style.sources) style.sources = {} as StyleSpecification["sources"];
|
||||
if (!style.layers) style.layers = [];
|
||||
@ -155,15 +183,6 @@ function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: str
|
||||
} satisfies VectorSourceSpecification as unknown as StyleSpecification["sources"][string];
|
||||
}
|
||||
|
||||
if (!style.sources[terrainSourceId]) {
|
||||
style.sources[terrainSourceId] = {
|
||||
type: "raster-dem",
|
||||
url: `https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=${encodeURIComponent(maptilerKey)}`,
|
||||
tileSize: 512,
|
||||
encoding: "mapbox",
|
||||
} satisfies RasterDEMSourceSpecification as unknown as StyleSpecification["sources"][string];
|
||||
}
|
||||
|
||||
const depth = ["to-number", ["get", "depth"]] as unknown as number[];
|
||||
const depthLabel = ["concat", ["to-string", ["*", depth, -1]], "m"] as unknown as string[];
|
||||
|
||||
@ -197,22 +216,6 @@ function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: str
|
||||
"#0b3a53",
|
||||
] as const;
|
||||
|
||||
const bathyHillshade: LayerSpecification = {
|
||||
id: "bathymetry-hillshade",
|
||||
type: "hillshade",
|
||||
source: terrainSourceId,
|
||||
paint: {
|
||||
"hillshade-illumination-anchor": "viewport",
|
||||
"hillshade-illumination-direction": 315,
|
||||
"hillshade-illumination-altitude": 45,
|
||||
"hillshade-exaggeration": ["interpolate", ["linear"], ["zoom"], 0, 0.15, 6, 0.25, 10, 0.32],
|
||||
// Dark-mode tuned shading. Alpha is baked into the colors.
|
||||
"hillshade-shadow-color": "rgba(0,0,0,0.45)",
|
||||
"hillshade-highlight-color": "rgba(255,255,255,0.18)",
|
||||
"hillshade-accent-color": "rgba(255,255,255,0.06)",
|
||||
},
|
||||
} as unknown as LayerSpecification;
|
||||
|
||||
const bathyFill: LayerSpecification = {
|
||||
id: "bathymetry-fill",
|
||||
type: "fill",
|
||||
@ -383,7 +386,6 @@ function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: str
|
||||
|
||||
const toInsert = [
|
||||
bathyFill,
|
||||
bathyHillshade,
|
||||
bathyExtrusion,
|
||||
bathyBandBorders,
|
||||
bathyBandBordersMajor,
|
||||
@ -452,6 +454,24 @@ function dashifyLine(from: [number, number], to: [number, number], suspicious: b
|
||||
return segs;
|
||||
}
|
||||
|
||||
function circleRingLngLat(center: [number, number], radiusMeters: number, steps = 72): [number, number][] {
|
||||
const [lon0, lat0] = center;
|
||||
const latRad = lat0 * DEG2RAD;
|
||||
const cosLat = Math.max(1e-6, Math.cos(latRad));
|
||||
const r = Math.max(0, radiusMeters);
|
||||
|
||||
const ring: [number, number][] = [];
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const a = (i / steps) * Math.PI * 2;
|
||||
const dy = r * Math.sin(a);
|
||||
const dx = r * Math.cos(a);
|
||||
const dLat = (dy / EARTH_RADIUS_M) / DEG2RAD;
|
||||
const dLon = (dx / (EARTH_RADIUS_M * cosLat)) / DEG2RAD;
|
||||
ring.push([lon0 + dLon, lat0 + dLat]);
|
||||
}
|
||||
return ring;
|
||||
}
|
||||
|
||||
type PairRangeCircle = {
|
||||
center: [number, number]; // [lon, lat]
|
||||
radiusNm: number;
|
||||
@ -708,6 +728,10 @@ export function Map3D({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MapLibre may not schedule a frame immediately after projection swaps if the map is idle.
|
||||
// Kick a few repaints so overlay sources (ships/zones) appear instantly.
|
||||
kickRepaint(map);
|
||||
};
|
||||
|
||||
if (map.isStyleLoaded()) syncProjectionAndDeck();
|
||||
@ -738,6 +762,7 @@ export function Map3D({
|
||||
// Disable style diff to avoid warnings with custom layers (Deck MapboxOverlay) and
|
||||
// to ensure a clean rebuild when switching between very different styles.
|
||||
map.setStyle(style, { diff: false });
|
||||
map.once("style.load", () => kickRepaint(map));
|
||||
} catch (e) {
|
||||
if (cancelled) return;
|
||||
console.warn("Base map switch failed:", e);
|
||||
@ -904,6 +929,8 @@ export function Map3D({
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Zones layer setup failed:", e);
|
||||
} finally {
|
||||
kickRepaint(map);
|
||||
}
|
||||
};
|
||||
|
||||
@ -916,7 +943,7 @@ export function Map3D({
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
}, [zones, overlays.zones]);
|
||||
}, [zones, overlays.zones, projection, baseMap]);
|
||||
|
||||
// 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.
|
||||
@ -944,6 +971,7 @@ export function Map3D({
|
||||
// ignore
|
||||
}
|
||||
prevGlobeSelectedRef.current = null;
|
||||
kickRepaint(map);
|
||||
};
|
||||
|
||||
const ensureImage = () => {
|
||||
@ -1007,6 +1035,9 @@ export function Map3D({
|
||||
type: "FeatureCollection",
|
||||
features: globeShipData.map((t) => {
|
||||
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;
|
||||
return {
|
||||
type: "Feature",
|
||||
id: t.mmsi,
|
||||
@ -1014,8 +1045,11 @@ export function Map3D({
|
||||
properties: {
|
||||
mmsi: t.mmsi,
|
||||
name: t.name || "",
|
||||
cog: isFiniteNumber(t.cog) ? t.cog : 0,
|
||||
cog,
|
||||
cog4,
|
||||
sog: isFiniteNumber(t.sog) ? t.sog : 0,
|
||||
length: isFiniteNumber(t.length) ? t.length : 0,
|
||||
width: isFiniteNumber(t.width) ? t.width : 0,
|
||||
permitted: !!legacy,
|
||||
code: legacy?.shipCode || "",
|
||||
},
|
||||
@ -1115,6 +1149,27 @@ 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[] = ["clamp", ["+", 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,
|
||||
@ -1123,15 +1178,27 @@ export function Map3D({
|
||||
layout: {
|
||||
visibility,
|
||||
"icon-image": imgId,
|
||||
"icon-size": ["interpolate", ["linear"], ["zoom"], 3, 0.32, 7, 0.42, 10, 0.52, 14, 0.72],
|
||||
"icon-size": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
3,
|
||||
["*", 0.32, sizeFactor],
|
||||
7,
|
||||
["*", 0.42, sizeFactor],
|
||||
10,
|
||||
["*", 0.52, sizeFactor],
|
||||
14,
|
||||
["*", 0.72, sizeFactor],
|
||||
] as unknown as number[],
|
||||
"icon-allow-overlap": true,
|
||||
"icon-ignore-placement": true,
|
||||
"icon-anchor": "center",
|
||||
"icon-rotate": ["get", "cog"],
|
||||
// Keep rotation relative to the map (true-north), but billboard to camera so it
|
||||
// doesn't look like it's pointing into the sky/ground on globe.
|
||||
// Debug-friendly: quantize heading to N/E/S/W while we validate globe alignment.
|
||||
"icon-rotate": ["get", "cog4"],
|
||||
// Keep the icon on the sea surface.
|
||||
"icon-rotation-alignment": "map",
|
||||
"icon-pitch-alignment": "viewport",
|
||||
"icon-pitch-alignment": "map",
|
||||
},
|
||||
paint: {
|
||||
"icon-opacity": ["case", ["boolean", ["feature-state", "selected"], false], 1.0, 0.92],
|
||||
@ -1163,6 +1230,7 @@ export function Map3D({
|
||||
// ignore
|
||||
}
|
||||
prevGlobeSelectedRef.current = selectedMmsi;
|
||||
kickRepaint(map);
|
||||
};
|
||||
|
||||
ensure();
|
||||
@ -1210,6 +1278,382 @@ export function Map3D({
|
||||
};
|
||||
}, [projection, settings.showShips, onSelectMmsi]);
|
||||
|
||||
// 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.
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
||||
const srcId = "pair-lines-ml-src";
|
||||
const layerId = "pair-lines-ml";
|
||||
|
||||
const remove = () => {
|
||||
try {
|
||||
if (map.getLayer(layerId)) map.removeLayer(layerId);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
if (map.getSource(srcId)) map.removeSource(srcId);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const ensure = () => {
|
||||
if (!map.isStyleLoaded()) return;
|
||||
if (projection !== "globe" || !overlays.pairLines || (pairLinks?.length ?? 0) === 0) {
|
||||
remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const fc: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
|
||||
type: "FeatureCollection",
|
||||
features: (pairLinks || []).map((p, idx) => ({
|
||||
type: "Feature",
|
||||
id: `${p.aMmsi}-${p.bMmsi}-${idx}`,
|
||||
geometry: { type: "LineString", coordinates: [p.from, p.to] },
|
||||
properties: { warn: p.warn },
|
||||
})),
|
||||
};
|
||||
|
||||
try {
|
||||
const existing = map.getSource(srcId) as GeoJSONSource | undefined;
|
||||
if (existing) existing.setData(fc);
|
||||
else map.addSource(srcId, { type: "geojson", data: fc } as GeoJSONSourceSpecification);
|
||||
} catch (e) {
|
||||
console.warn("Pair lines source setup failed:", e);
|
||||
return;
|
||||
}
|
||||
|
||||
const before = map.getLayer("ships-globe-halo") ? "ships-globe-halo" : undefined;
|
||||
|
||||
if (!map.getLayer(layerId)) {
|
||||
try {
|
||||
map.addLayer(
|
||||
{
|
||||
id: layerId,
|
||||
type: "line",
|
||||
source: srcId,
|
||||
layout: { "line-cap": "round", "line-join": "round", visibility: "visible" },
|
||||
paint: {
|
||||
"line-color": [
|
||||
"case",
|
||||
["boolean", ["get", "warn"], false],
|
||||
"rgba(245,158,11,0.95)",
|
||||
"rgba(59,130,246,0.55)",
|
||||
] as never,
|
||||
"line-width": ["case", ["boolean", ["get", "warn"], false], 2.2, 1.4] as never,
|
||||
"line-opacity": 0.9,
|
||||
},
|
||||
} as unknown as LayerSpecification,
|
||||
before,
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("Pair lines layer add failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
kickRepaint(map);
|
||||
};
|
||||
|
||||
ensure();
|
||||
map.on("style.load", ensure);
|
||||
return () => {
|
||||
try {
|
||||
map.off("style.load", ensure);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
remove();
|
||||
};
|
||||
}, [projection, overlays.pairLines, pairLinks]);
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
||||
const srcId = "fc-lines-ml-src";
|
||||
const layerId = "fc-lines-ml";
|
||||
|
||||
const remove = () => {
|
||||
try {
|
||||
if (map.getLayer(layerId)) map.removeLayer(layerId);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
if (map.getSource(srcId)) map.removeSource(srcId);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const ensure = () => {
|
||||
if (!map.isStyleLoaded()) return;
|
||||
if (projection !== "globe" || !overlays.fcLines) {
|
||||
remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const segs: DashSeg[] = [];
|
||||
for (const l of fcLinks || []) segs.push(...dashifyLine(l.from, l.to, l.suspicious));
|
||||
if (segs.length === 0) {
|
||||
remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const fc: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
|
||||
type: "FeatureCollection",
|
||||
features: segs.map((s, idx) => ({
|
||||
type: "Feature",
|
||||
id: `fc-${idx}`,
|
||||
geometry: { type: "LineString", coordinates: [s.from, s.to] },
|
||||
properties: { suspicious: s.suspicious },
|
||||
})),
|
||||
};
|
||||
|
||||
try {
|
||||
const existing = map.getSource(srcId) as GeoJSONSource | undefined;
|
||||
if (existing) existing.setData(fc);
|
||||
else map.addSource(srcId, { type: "geojson", data: fc } as GeoJSONSourceSpecification);
|
||||
} catch (e) {
|
||||
console.warn("FC lines source setup failed:", e);
|
||||
return;
|
||||
}
|
||||
|
||||
const before = map.getLayer("ships-globe-halo") ? "ships-globe-halo" : undefined;
|
||||
|
||||
if (!map.getLayer(layerId)) {
|
||||
try {
|
||||
map.addLayer(
|
||||
{
|
||||
id: layerId,
|
||||
type: "line",
|
||||
source: srcId,
|
||||
layout: { "line-cap": "round", "line-join": "round", visibility: "visible" },
|
||||
paint: {
|
||||
"line-color": [
|
||||
"case",
|
||||
["boolean", ["get", "suspicious"], false],
|
||||
"rgba(239,68,68,0.95)",
|
||||
"rgba(217,119,6,0.92)",
|
||||
] as never,
|
||||
"line-width": 1.3,
|
||||
"line-opacity": 0.9,
|
||||
},
|
||||
} as unknown as LayerSpecification,
|
||||
before,
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("FC lines layer add failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
kickRepaint(map);
|
||||
};
|
||||
|
||||
ensure();
|
||||
map.on("style.load", ensure);
|
||||
return () => {
|
||||
try {
|
||||
map.off("style.load", ensure);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
remove();
|
||||
};
|
||||
}, [projection, overlays.fcLines, fcLinks]);
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
||||
const srcId = "fleet-circles-ml-src";
|
||||
const layerId = "fleet-circles-ml";
|
||||
|
||||
const remove = () => {
|
||||
try {
|
||||
if (map.getLayer(layerId)) map.removeLayer(layerId);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
if (map.getSource(srcId)) map.removeSource(srcId);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const ensure = () => {
|
||||
if (!map.isStyleLoaded()) return;
|
||||
if (projection !== "globe" || !overlays.fleetCircles || (fleetCircles?.length ?? 0) === 0) {
|
||||
remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const fc: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
|
||||
type: "FeatureCollection",
|
||||
features: (fleetCircles || []).map((c, idx) => {
|
||||
const ring = circleRingLngLat(c.center, c.radiusNm * 1852);
|
||||
return {
|
||||
type: "Feature",
|
||||
id: `fleet-${c.ownerKey}-${idx}`,
|
||||
geometry: { type: "LineString", coordinates: ring },
|
||||
properties: { count: c.count },
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
try {
|
||||
const existing = map.getSource(srcId) as GeoJSONSource | undefined;
|
||||
if (existing) existing.setData(fc);
|
||||
else map.addSource(srcId, { type: "geojson", data: fc } as GeoJSONSourceSpecification);
|
||||
} catch (e) {
|
||||
console.warn("Fleet circles source setup failed:", e);
|
||||
return;
|
||||
}
|
||||
|
||||
const before = map.getLayer("ships-globe-halo") ? "ships-globe-halo" : undefined;
|
||||
|
||||
if (!map.getLayer(layerId)) {
|
||||
try {
|
||||
map.addLayer(
|
||||
{
|
||||
id: layerId,
|
||||
type: "line",
|
||||
source: srcId,
|
||||
layout: { "line-cap": "round", "line-join": "round", visibility: "visible" },
|
||||
paint: {
|
||||
"line-color": "rgba(245,158,11,0.65)",
|
||||
"line-width": 1.1,
|
||||
"line-opacity": 0.85,
|
||||
},
|
||||
} as unknown as LayerSpecification,
|
||||
before,
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("Fleet circles layer add failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
kickRepaint(map);
|
||||
};
|
||||
|
||||
ensure();
|
||||
map.on("style.load", ensure);
|
||||
return () => {
|
||||
try {
|
||||
map.off("style.load", ensure);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
remove();
|
||||
};
|
||||
}, [projection, overlays.fleetCircles, fleetCircles]);
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
||||
const srcId = "pair-range-ml-src";
|
||||
const layerId = "pair-range-ml";
|
||||
|
||||
const remove = () => {
|
||||
try {
|
||||
if (map.getLayer(layerId)) map.removeLayer(layerId);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
if (map.getSource(srcId)) map.removeSource(srcId);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const ensure = () => {
|
||||
if (!map.isStyleLoaded()) return;
|
||||
if (projection !== "globe" || !overlays.pairRange) {
|
||||
remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const ranges: PairRangeCircle[] = [];
|
||||
for (const p of pairLinks || []) {
|
||||
const center: [number, number] = [(p.from[0] + p.to[0]) / 2, (p.from[1] + p.to[1]) / 2];
|
||||
ranges.push({ center, radiusNm: Math.max(0.05, p.distanceNm / 2), warn: p.warn });
|
||||
}
|
||||
if (ranges.length === 0) {
|
||||
remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const fc: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
|
||||
type: "FeatureCollection",
|
||||
features: ranges.map((c, idx) => {
|
||||
const ring = circleRingLngLat(c.center, c.radiusNm * 1852);
|
||||
return {
|
||||
type: "Feature",
|
||||
id: `pair-range-${idx}`,
|
||||
geometry: { type: "LineString", coordinates: ring },
|
||||
properties: { warn: c.warn },
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
try {
|
||||
const existing = map.getSource(srcId) as GeoJSONSource | undefined;
|
||||
if (existing) existing.setData(fc);
|
||||
else map.addSource(srcId, { type: "geojson", data: fc } as GeoJSONSourceSpecification);
|
||||
} catch (e) {
|
||||
console.warn("Pair range source setup failed:", e);
|
||||
return;
|
||||
}
|
||||
|
||||
const before = map.getLayer("ships-globe-halo") ? "ships-globe-halo" : undefined;
|
||||
|
||||
if (!map.getLayer(layerId)) {
|
||||
try {
|
||||
map.addLayer(
|
||||
{
|
||||
id: layerId,
|
||||
type: "line",
|
||||
source: srcId,
|
||||
layout: { "line-cap": "round", "line-join": "round", visibility: "visible" },
|
||||
paint: {
|
||||
"line-color": [
|
||||
"case",
|
||||
["boolean", ["get", "warn"], false],
|
||||
"rgba(245,158,11,0.75)",
|
||||
"rgba(59,130,246,0.45)",
|
||||
] as never,
|
||||
"line-width": 1.0,
|
||||
"line-opacity": 0.85,
|
||||
},
|
||||
} as unknown as LayerSpecification,
|
||||
before,
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("Pair range layer add failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
kickRepaint(map);
|
||||
};
|
||||
|
||||
ensure();
|
||||
map.on("style.load", ensure);
|
||||
return () => {
|
||||
try {
|
||||
map.off("style.load", ensure);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
remove();
|
||||
};
|
||||
}, [projection, overlays.pairRange, pairLinks]);
|
||||
|
||||
const shipData = useMemo(() => {
|
||||
return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon));
|
||||
}, [targets]);
|
||||
@ -1321,15 +1765,15 @@ export function Map3D({
|
||||
);
|
||||
}
|
||||
|
||||
if (overlays.pairLines && (pairLinks?.length ?? 0) > 0) {
|
||||
if (overlays.pairLines && projection !== "globe" && (pairLinks?.length ?? 0) > 0) {
|
||||
layers.push(
|
||||
new LineLayer<PairLink>({
|
||||
id: "pair-lines",
|
||||
data: pairLinks,
|
||||
pickable: false,
|
||||
parameters: overlayParams,
|
||||
getSourcePosition: (d) => (projection === "globe" ? lngLatToUnitSphere(d.from[0], d.from[1]) : d.from),
|
||||
getTargetPosition: (d) => (projection === "globe" ? lngLatToUnitSphere(d.to[0], d.to[1]) : d.to),
|
||||
getSourcePosition: (d) => d.from,
|
||||
getTargetPosition: (d) => d.to,
|
||||
getColor: (d) => (d.warn ? [245, 158, 11, 220] : [59, 130, 246, 85]),
|
||||
getWidth: (d) => (d.warn ? 2.2 : 1.4),
|
||||
widthUnits: "pixels",
|
||||
@ -1337,15 +1781,15 @@ export function Map3D({
|
||||
);
|
||||
}
|
||||
|
||||
if (overlays.fcLines && fcDashed.length > 0) {
|
||||
if (overlays.fcLines && projection !== "globe" && fcDashed.length > 0) {
|
||||
layers.push(
|
||||
new LineLayer<DashSeg>({
|
||||
id: "fc-lines",
|
||||
data: fcDashed,
|
||||
pickable: false,
|
||||
parameters: overlayParams,
|
||||
getSourcePosition: (d) => (projection === "globe" ? lngLatToUnitSphere(d.from[0], d.from[1]) : d.from),
|
||||
getTargetPosition: (d) => (projection === "globe" ? lngLatToUnitSphere(d.to[0], d.to[1]) : d.to),
|
||||
getSourcePosition: (d) => d.from,
|
||||
getTargetPosition: (d) => d.to,
|
||||
getColor: (d) => (d.suspicious ? [239, 68, 68, 220] : [217, 119, 6, 200]),
|
||||
getWidth: () => 1.3,
|
||||
widthUnits: "pixels",
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user