diff --git a/apps/web/src/widgets/map3d/Map3D.tsx b/apps/web/src/widgets/map3d/Map3D.tsx index 453a8e2..e194216 100644 --- a/apps/web/src/widgets/map3d/Map3D.tsx +++ b/apps/web/src/widgets/map3d/Map3D.tsx @@ -1630,6 +1630,7 @@ export function Map3D({ zoneLabelExpr.push(["coalesce", ["get", "zoneName"], ["get", "zoneLabel"], ["get", "NAME"], "수역"]); const ensure = () => { + if (projectionBusyRef.current) return; // Always update visibility if the layers exist. const visibility = overlays.zones ? "visible" : "none"; try { @@ -1913,6 +1914,7 @@ export function Map3D({ }; const ensure = () => { + if (projectionBusyRef.current) return; if (!map.isStyleLoaded()) return; if (projection !== "globe" || !settings.showShips) { @@ -2414,6 +2416,7 @@ export function Map3D({ }; const ensure = () => { + if (projectionBusyRef.current) return; if (!map.isStyleLoaded()) return; if (projection !== "globe" || !overlays.pairLines || (pairLinks?.length ?? 0) === 0) { remove(); @@ -2515,6 +2518,7 @@ export function Map3D({ }; const ensure = () => { + if (projectionBusyRef.current) return; if (!map.isStyleLoaded()) return; if (projection !== "globe" || !overlays.fcLines) { remove(); @@ -2630,6 +2634,7 @@ export function Map3D({ }; const ensure = () => { + if (projectionBusyRef.current) return; if (!map.isStyleLoaded()) return; if (projection !== "globe" || !overlays.fleetCircles || (fleetCircles?.length ?? 0) === 0) { remove(); @@ -2790,6 +2795,7 @@ export function Map3D({ }; const ensure = () => { + if (projectionBusyRef.current) return; if (!map.isStyleLoaded()) return; if (projection !== "globe" || !overlays.pairRange) { remove(); @@ -3028,6 +3034,11 @@ export function Map3D({ resetGlobeHoverStates(); return; } + if (projectionBusyRef.current) { + resetGlobeHoverStates(); + clearGlobeTooltip(); + return; + } if (!map.isStyleLoaded()) { clearDeckGlobeHoverState(); clearGlobeTooltip(); @@ -3248,6 +3259,8 @@ export function Map3D({ useEffect(() => { const map = mapRef.current; if (!map) return; + if (projectionBusyRef.current) return; + let deckTarget = projection === "globe" ? globeDeckLayerRef.current : overlayRef.current; if (projection === "mercator") { diff --git a/apps/web/src/widgets/relations/RelationsPanel.tsx b/apps/web/src/widgets/relations/RelationsPanel.tsx index 4cbc3cc..1572940 100644 --- a/apps/web/src/widgets/relations/RelationsPanel.tsx +++ b/apps/web/src/widgets/relations/RelationsPanel.tsx @@ -1,3 +1,4 @@ +import { useMemo, type MouseEvent } from "react"; import { VESSEL_TYPES } from "../../entities/vessel/model/meta"; import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types"; import { haversineNm } from "../../shared/lib/geo/haversineNm"; @@ -7,9 +8,56 @@ type Props = { vessels: DerivedLegacyVessel[]; fleetVessels: DerivedLegacyVessel[]; onSelectMmsi: (mmsi: number) => void; + onToggleHighlightMmsi: (mmsi: number) => void; + onHoverMmsi: (mmsis: number[]) => void; + onClearHover: () => void; + onHoverPair: (mmsis: number[]) => void; + onClearPairHover: () => void; + onHoverFleet: (ownerKey: string | null, mmsis: number[]) => void; + onClearFleetHover: () => void; + hoveredFleetOwnerKey?: string | null; + hoveredFleetMmsiSet?: number[]; + onContextMenuFleet?: (ownerKey: string, mmsis: number[]) => void; }; -export function RelationsPanel({ selectedVessel, vessels, fleetVessels, onSelectMmsi }: Props) { +export function RelationsPanel({ + selectedVessel, + vessels, + fleetVessels, + onSelectMmsi, + onToggleHighlightMmsi, + onHoverMmsi, + onClearHover, + onHoverPair, + onClearPairHover, + onHoverFleet, + onClearFleetHover, + hoveredFleetOwnerKey, + hoveredFleetMmsiSet, + onContextMenuFleet, +}: Props) { + const handlePrimaryAction = (e: MouseEvent, mmsi: number) => { + if (e.shiftKey || e.ctrlKey || e.metaKey) { + onToggleHighlightMmsi(mmsi); + return; + } + onSelectMmsi(mmsi); + }; + + const clearAllHovers = () => { + onClearHover(); + onClearPairHover(); + onClearFleetHover(); + }; + + const hoveredFleetMmsis = useMemo(() => new Set(hoveredFleetMmsiSet ?? []), [hoveredFleetMmsiSet]); + + const isFleetHighlightByOwner = (ownerKey: string | null) => + hoveredFleetOwnerKey != null && ownerKey != null && hoveredFleetOwnerKey === ownerKey; + + const isVesselHighlight = (mmsi: number, ownerKey: string | null) => + hoveredFleetMmsis.has(mmsi) || isFleetHighlightByOwner(ownerKey); + if (selectedVessel) { const v = selectedVessel; const meta = VESSEL_TYPES[v.shipCode]; @@ -19,7 +67,7 @@ export function RelationsPanel({ selectedVessel, vessels, fleetVessels, onSelect const fcNearby = vessels.filter((fc) => fc.shipCode === "FC" && fc.mmsi !== v.mmsi && haversineNm(fc.lat, fc.lon, v.lat, v.lon) < 5); return ( -