chore: develop 병합으로 충돌 해결
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
커밋
c09211d5ed
@ -27,6 +27,8 @@ import { Topbar } from "../../widgets/topbar/Topbar";
|
|||||||
import { VesselInfoPanel } from "../../widgets/info/VesselInfoPanel";
|
import { VesselInfoPanel } from "../../widgets/info/VesselInfoPanel";
|
||||||
import { SubcableInfoPanel } from "../../widgets/subcableInfo/SubcableInfoPanel";
|
import { SubcableInfoPanel } from "../../widgets/subcableInfo/SubcableInfoPanel";
|
||||||
import { VesselList } from "../../widgets/vesselList/VesselList";
|
import { VesselList } from "../../widgets/vesselList/VesselList";
|
||||||
|
import type { ActiveTrack } from "../../entities/vesselTrack/model/types";
|
||||||
|
import { fetchVesselTrack } from "../../entities/vesselTrack/api/fetchTrack";
|
||||||
import { MapSettingsPanel } from "../../features/mapSettings/MapSettingsPanel";
|
import { MapSettingsPanel } from "../../features/mapSettings/MapSettingsPanel";
|
||||||
import { useWeatherPolling } from "../../features/weatherOverlay/useWeatherPolling";
|
import { useWeatherPolling } from "../../features/weatherOverlay/useWeatherPolling";
|
||||||
import { useWeatherOverlay } from "../../features/weatherOverlay/useWeatherOverlay";
|
import { useWeatherOverlay } from "../../features/weatherOverlay/useWeatherOverlay";
|
||||||
@ -36,9 +38,6 @@ import { DepthLegend } from "../../widgets/legend/DepthLegend";
|
|||||||
import { DEFAULT_MAP_STYLE_SETTINGS } from "../../features/mapSettings/types";
|
import { DEFAULT_MAP_STYLE_SETTINGS } from "../../features/mapSettings/types";
|
||||||
import type { MapStyleSettings } from "../../features/mapSettings/types";
|
import type { MapStyleSettings } from "../../features/mapSettings/types";
|
||||||
import { fmtDateTimeFull, fmtIsoFull } from "../../shared/lib/datetime";
|
import { fmtDateTimeFull, fmtIsoFull } from "../../shared/lib/datetime";
|
||||||
import { queryTrackByMmsi } from "../../features/trackReplay/services/trackQueryService";
|
|
||||||
import { useTrackQueryStore } from "../../features/trackReplay/stores/trackQueryStore";
|
|
||||||
import { GlobalTrackReplayPanel } from "../../widgets/trackReplay/GlobalTrackReplayPanel";
|
|
||||||
import {
|
import {
|
||||||
buildLegacyHitMap,
|
buildLegacyHitMap,
|
||||||
computeCountsByType,
|
computeCountsByType,
|
||||||
@ -143,33 +142,27 @@ export function DashboardPage() {
|
|||||||
const [selectedCableId, setSelectedCableId] = useState<string | null>(null);
|
const [selectedCableId, setSelectedCableId] = useState<string | null>(null);
|
||||||
|
|
||||||
// 항적 (vessel track)
|
// 항적 (vessel track)
|
||||||
|
const [activeTrack, setActiveTrack] = useState<ActiveTrack | null>(null);
|
||||||
const [trackContextMenu, setTrackContextMenu] = useState<{ x: number; y: number; mmsi: number; vesselName: string } | null>(null);
|
const [trackContextMenu, setTrackContextMenu] = useState<{ x: number; y: number; mmsi: number; vesselName: string } | null>(null);
|
||||||
const handleOpenTrackMenu = useCallback((info: { x: number; y: number; mmsi: number; vesselName: string }) => {
|
const handleOpenTrackMenu = useCallback((info: { x: number; y: number; mmsi: number; vesselName: string }) => {
|
||||||
setTrackContextMenu(info);
|
setTrackContextMenu(info);
|
||||||
}, []);
|
}, []);
|
||||||
const handleCloseTrackMenu = useCallback(() => setTrackContextMenu(null), []);
|
const handleCloseTrackMenu = useCallback(() => setTrackContextMenu(null), []);
|
||||||
const handleRequestTrack = useCallback(async (mmsi: number, minutes: number) => {
|
const handleRequestTrack = useCallback(async (mmsi: number, minutes: number) => {
|
||||||
const trackStore = useTrackQueryStore.getState();
|
|
||||||
const queryKey = `${mmsi}:${minutes}:${Date.now()}`;
|
|
||||||
trackStore.beginQuery(queryKey);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const target = targets.find((item) => item.mmsi === mmsi);
|
const res = await fetchVesselTrack(mmsi, minutes);
|
||||||
const tracks = await queryTrackByMmsi({
|
if (res.success && res.data.length > 0) {
|
||||||
mmsi,
|
const sorted = [...res.data].sort(
|
||||||
minutes,
|
(a, b) => new Date(a.messageTimestamp).getTime() - new Date(b.messageTimestamp).getTime(),
|
||||||
shipNameHint: target?.name,
|
);
|
||||||
});
|
setActiveTrack({ mmsi, minutes, points: sorted, fetchedAt: Date.now() });
|
||||||
|
|
||||||
if (tracks.length > 0) {
|
|
||||||
trackStore.applyTracksSuccess(tracks, queryKey);
|
|
||||||
} else {
|
} else {
|
||||||
trackStore.applyQueryError('항적 데이터가 없습니다.', queryKey);
|
console.warn('Track: no data', res.message);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
trackStore.applyQueryError(e instanceof Error ? e.message : '항적 조회에 실패했습니다.', queryKey);
|
console.warn('Track fetch failed:', e);
|
||||||
}
|
}
|
||||||
}, [targets]);
|
}, []);
|
||||||
|
|
||||||
const [settings, setSettings] = usePersistedState<Map3DSettings>(uid, 'map3dSettings', {
|
const [settings, setSettings] = usePersistedState<Map3DSettings>(uid, 'map3dSettings', {
|
||||||
showShips: true, showDensity: false, showSeamark: false,
|
showShips: true, showDensity: false, showSeamark: false,
|
||||||
@ -772,13 +765,12 @@ export function DashboardPage() {
|
|||||||
onMapReady={handleMapReady}
|
onMapReady={handleMapReady}
|
||||||
initialView={mapView}
|
initialView={mapView}
|
||||||
onViewStateChange={setMapView}
|
onViewStateChange={setMapView}
|
||||||
activeTrack={null}
|
activeTrack={activeTrack}
|
||||||
trackContextMenu={trackContextMenu}
|
trackContextMenu={trackContextMenu}
|
||||||
onRequestTrack={handleRequestTrack}
|
onRequestTrack={handleRequestTrack}
|
||||||
onCloseTrackMenu={handleCloseTrackMenu}
|
onCloseTrackMenu={handleCloseTrackMenu}
|
||||||
onOpenTrackMenu={handleOpenTrackMenu}
|
onOpenTrackMenu={handleOpenTrackMenu}
|
||||||
/>
|
/>
|
||||||
<GlobalTrackReplayPanel />
|
|
||||||
<MapSettingsPanel value={mapStyleSettings} onChange={setMapStyleSettings} />
|
<MapSettingsPanel value={mapStyleSettings} onChange={setMapStyleSettings} />
|
||||||
<WeatherPanel
|
<WeatherPanel
|
||||||
snapshot={weather.snapshot}
|
snapshot={weather.snapshot}
|
||||||
|
|||||||
@ -26,13 +26,9 @@ import { useGlobeOverlays } from './hooks/useGlobeOverlays';
|
|||||||
import { useGlobeInteraction } from './hooks/useGlobeInteraction';
|
import { useGlobeInteraction } from './hooks/useGlobeInteraction';
|
||||||
import { useDeckLayers } from './hooks/useDeckLayers';
|
import { useDeckLayers } from './hooks/useDeckLayers';
|
||||||
import { useSubcablesLayer } from './hooks/useSubcablesLayer';
|
import { useSubcablesLayer } from './hooks/useSubcablesLayer';
|
||||||
import { useTrackReplayLayer } from './hooks/useTrackReplayLayer';
|
import { useVesselTrackLayer } from './hooks/useVesselTrackLayer';
|
||||||
import { useMapStyleSettings } from './hooks/useMapStyleSettings';
|
import { useMapStyleSettings } from './hooks/useMapStyleSettings';
|
||||||
import { VesselContextMenu } from './components/VesselContextMenu';
|
import { VesselContextMenu } from './components/VesselContextMenu';
|
||||||
import { useLiveShipAdapter } from '../../features/liveRenderer/hooks/useLiveShipAdapter';
|
|
||||||
import { useLiveShipBatchRender } from '../../features/liveRenderer/hooks/useLiveShipBatchRender';
|
|
||||||
import { useTrackQueryStore } from '../../features/trackReplay/stores/trackQueryStore';
|
|
||||||
import { useTrackReplayDeckLayers } from '../../features/trackReplay/hooks/useTrackReplayDeckLayers';
|
|
||||||
|
|
||||||
export type { Map3DSettings, BaseMapId, MapProjectionId } from './types';
|
export type { Map3DSettings, BaseMapId, MapProjectionId } from './types';
|
||||||
|
|
||||||
@ -82,6 +78,13 @@ export function Map3D({
|
|||||||
onCloseTrackMenu,
|
onCloseTrackMenu,
|
||||||
onOpenTrackMenu,
|
onOpenTrackMenu,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
void onHoverFleet;
|
||||||
|
void onClearFleetHover;
|
||||||
|
void onHoverMmsi;
|
||||||
|
void onClearMmsiHover;
|
||||||
|
void onHoverPair;
|
||||||
|
void onClearPairHover;
|
||||||
|
|
||||||
// ── Shared refs ──────────────────────────────────────────────────────
|
// ── Shared refs ──────────────────────────────────────────────────────
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const mapRef = useRef<maplibregl.Map | null>(null);
|
const mapRef = useRef<maplibregl.Map | null>(null);
|
||||||
@ -198,38 +201,20 @@ export function Map3D({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ── Ship data memos ──────────────────────────────────────────────────
|
// ── Ship data memos ──────────────────────────────────────────────────
|
||||||
const rawShipData = useMemo(() => {
|
const shipData = useMemo(() => {
|
||||||
return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon) && isFiniteNumber(t.mmsi));
|
return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon) && isFiniteNumber(t.mmsi));
|
||||||
}, [targets]);
|
}, [targets]);
|
||||||
|
|
||||||
const hideLiveShips = useTrackQueryStore((state) => state.hideLiveShips);
|
|
||||||
|
|
||||||
const liveShipFeatures = useLiveShipAdapter(rawShipData, legacyHits ?? null);
|
|
||||||
const { renderedTargets: batchRenderedTargets } = useLiveShipBatchRender(
|
|
||||||
mapRef,
|
|
||||||
liveShipFeatures,
|
|
||||||
rawShipData,
|
|
||||||
mapSyncEpoch,
|
|
||||||
);
|
|
||||||
|
|
||||||
const shipData = useMemo(
|
|
||||||
() => (hideLiveShips ? [] : rawShipData),
|
|
||||||
[hideLiveShips, rawShipData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const shipByMmsi = useMemo(() => {
|
const shipByMmsi = useMemo(() => {
|
||||||
const byMmsi = new Map<number, AisTarget>();
|
const byMmsi = new Map<number, AisTarget>();
|
||||||
for (const t of rawShipData) byMmsi.set(t.mmsi, t);
|
for (const t of shipData) byMmsi.set(t.mmsi, t);
|
||||||
return byMmsi;
|
return byMmsi;
|
||||||
}, [rawShipData]);
|
}, [shipData]);
|
||||||
|
|
||||||
const shipLayerData = useMemo(() => {
|
const shipLayerData = useMemo(() => {
|
||||||
if (hideLiveShips) return [];
|
if (shipData.length === 0) return shipData;
|
||||||
// Fallback to raw targets when batch result is temporarily empty
|
return [...shipData];
|
||||||
// (e.g. overlay update race or viewport sync delay).
|
}, [shipData]);
|
||||||
if (batchRenderedTargets.length === 0) return rawShipData;
|
|
||||||
return [...batchRenderedTargets];
|
|
||||||
}, [hideLiveShips, batchRenderedTargets, rawShipData]);
|
|
||||||
|
|
||||||
const shipHighlightSet = useMemo(() => {
|
const shipHighlightSet = useMemo(() => {
|
||||||
const out = new Set(highlightedMmsiSetForShips);
|
const out = new Set(highlightedMmsiSetForShips);
|
||||||
@ -251,8 +236,6 @@ export function Map3D({
|
|||||||
return shipLayerData.filter((target) => shipHighlightSet.has(target.mmsi));
|
return shipLayerData.filter((target) => shipHighlightSet.has(target.mmsi));
|
||||||
}, [shipHighlightSet, shipLayerData]);
|
}, [shipHighlightSet, shipLayerData]);
|
||||||
|
|
||||||
const trackReplayRenderState = useTrackReplayDeckLayers();
|
|
||||||
|
|
||||||
// ── Deck hover management ────────────────────────────────────────────
|
// ── Deck hover management ────────────────────────────────────────────
|
||||||
const hasAuxiliarySelectModifier = useCallback(
|
const hasAuxiliarySelectModifier = useCallback(
|
||||||
(ev?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null): boolean => {
|
(ev?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null): boolean => {
|
||||||
@ -312,51 +295,22 @@ export function Map3D({
|
|||||||
ownerKey: null,
|
ownerKey: null,
|
||||||
vesselMmsis: [],
|
vesselMmsis: [],
|
||||||
});
|
});
|
||||||
const mapDrivenMmsiHoverRef = useRef(false);
|
|
||||||
const mapDrivenPairHoverRef = useRef(false);
|
|
||||||
const mapDrivenFleetHoverRef = useRef(false);
|
|
||||||
|
|
||||||
const clearMapFleetHoverState = useCallback(() => {
|
const clearMapFleetHoverState = useCallback(() => {
|
||||||
const prev = mapFleetHoverStateRef.current;
|
|
||||||
mapFleetHoverStateRef.current = { ownerKey: null, vesselMmsis: [] };
|
mapFleetHoverStateRef.current = { ownerKey: null, vesselMmsis: [] };
|
||||||
setHoveredDeckFleetOwner(null);
|
setHoveredDeckFleetOwner(null);
|
||||||
setHoveredDeckFleetMmsis([]);
|
setHoveredDeckFleetMmsis([]);
|
||||||
if (
|
}, [setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis]);
|
||||||
mapDrivenFleetHoverRef.current &&
|
|
||||||
(prev.ownerKey != null || prev.vesselMmsis.length > 0) &&
|
|
||||||
hoveredFleetOwnerKey === prev.ownerKey &&
|
|
||||||
equalNumberArrays(hoveredFleetMmsiSet, prev.vesselMmsis)
|
|
||||||
) {
|
|
||||||
onClearFleetHover?.();
|
|
||||||
}
|
|
||||||
mapDrivenFleetHoverRef.current = false;
|
|
||||||
}, [
|
|
||||||
setHoveredDeckFleetOwner,
|
|
||||||
setHoveredDeckFleetMmsis,
|
|
||||||
onClearFleetHover,
|
|
||||||
hoveredFleetOwnerKey,
|
|
||||||
hoveredFleetMmsiSet,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const clearDeckHoverPairs = useCallback(() => {
|
const clearDeckHoverPairs = useCallback(() => {
|
||||||
const prev = mapDeckPairHoverRef.current;
|
|
||||||
mapDeckPairHoverRef.current = [];
|
mapDeckPairHoverRef.current = [];
|
||||||
setHoveredDeckPairMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
|
setHoveredDeckPairMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
|
||||||
if (mapDrivenPairHoverRef.current && prev.length > 0 && equalNumberArrays(hoveredPairMmsiSet, prev)) {
|
}, [setHoveredDeckPairMmsiSet]);
|
||||||
onClearPairHover?.();
|
|
||||||
}
|
|
||||||
mapDrivenPairHoverRef.current = false;
|
|
||||||
}, [setHoveredDeckPairMmsiSet, onClearPairHover, hoveredPairMmsiSet]);
|
|
||||||
|
|
||||||
const clearDeckHoverMmsi = useCallback(() => {
|
const clearDeckHoverMmsi = useCallback(() => {
|
||||||
const prev = mapDeckMmsiHoverRef.current;
|
|
||||||
mapDeckMmsiHoverRef.current = [];
|
mapDeckMmsiHoverRef.current = [];
|
||||||
setHoveredDeckMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
|
setHoveredDeckMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
|
||||||
if (mapDrivenMmsiHoverRef.current && prev.length > 0 && equalNumberArrays(hoveredMmsiSet, prev)) {
|
}, [setHoveredDeckMmsiSet]);
|
||||||
onClearMmsiHover?.();
|
|
||||||
}
|
|
||||||
mapDrivenMmsiHoverRef.current = false;
|
|
||||||
}, [setHoveredDeckMmsiSet, onClearMmsiHover, hoveredMmsiSet]);
|
|
||||||
|
|
||||||
const scheduleDeckHoverResolve = useCallback(() => {
|
const scheduleDeckHoverResolve = useCallback(() => {
|
||||||
if (deckHoverRafRef.current != null) return;
|
if (deckHoverRafRef.current != null) return;
|
||||||
@ -382,41 +336,21 @@ export function Map3D({
|
|||||||
const setDeckHoverMmsi = useCallback(
|
const setDeckHoverMmsi = useCallback(
|
||||||
(next: number[]) => {
|
(next: number[]) => {
|
||||||
const normalized = makeUniqueSorted(next);
|
const normalized = makeUniqueSorted(next);
|
||||||
const prev = mapDeckMmsiHoverRef.current;
|
|
||||||
touchDeckHoverState(normalized.length > 0);
|
touchDeckHoverState(normalized.length > 0);
|
||||||
setHoveredDeckMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
setHoveredDeckMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
||||||
mapDeckMmsiHoverRef.current = normalized;
|
mapDeckMmsiHoverRef.current = normalized;
|
||||||
if (!equalNumberArrays(prev, normalized)) {
|
|
||||||
if (normalized.length > 0) {
|
|
||||||
mapDrivenMmsiHoverRef.current = true;
|
|
||||||
onHoverMmsi?.(normalized);
|
|
||||||
} else if (mapDrivenMmsiHoverRef.current && prev.length > 0) {
|
|
||||||
onClearMmsiHover?.();
|
|
||||||
mapDrivenMmsiHoverRef.current = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[setHoveredDeckMmsiSet, touchDeckHoverState, onHoverMmsi, onClearMmsiHover],
|
[setHoveredDeckMmsiSet, touchDeckHoverState],
|
||||||
);
|
);
|
||||||
|
|
||||||
const setDeckHoverPairs = useCallback(
|
const setDeckHoverPairs = useCallback(
|
||||||
(next: number[]) => {
|
(next: number[]) => {
|
||||||
const normalized = makeUniqueSorted(next);
|
const normalized = makeUniqueSorted(next);
|
||||||
const prev = mapDeckPairHoverRef.current;
|
|
||||||
touchDeckHoverState(normalized.length > 0);
|
touchDeckHoverState(normalized.length > 0);
|
||||||
setHoveredDeckPairMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
setHoveredDeckPairMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
||||||
mapDeckPairHoverRef.current = normalized;
|
mapDeckPairHoverRef.current = normalized;
|
||||||
if (!equalNumberArrays(prev, normalized)) {
|
|
||||||
if (normalized.length > 0) {
|
|
||||||
mapDrivenPairHoverRef.current = true;
|
|
||||||
onHoverPair?.(normalized);
|
|
||||||
} else if (mapDrivenPairHoverRef.current && prev.length > 0) {
|
|
||||||
onClearPairHover?.();
|
|
||||||
mapDrivenPairHoverRef.current = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[setHoveredDeckPairMmsiSet, touchDeckHoverState, onHoverPair, onClearPairHover],
|
[setHoveredDeckPairMmsiSet, touchDeckHoverState],
|
||||||
);
|
);
|
||||||
|
|
||||||
const setMapFleetHoverState = useCallback(
|
const setMapFleetHoverState = useCallback(
|
||||||
@ -430,21 +364,8 @@ export function Map3D({
|
|||||||
setHoveredDeckFleetOwner(ownerKey);
|
setHoveredDeckFleetOwner(ownerKey);
|
||||||
setHoveredDeckFleetMmsis(normalized);
|
setHoveredDeckFleetMmsis(normalized);
|
||||||
mapFleetHoverStateRef.current = { ownerKey, vesselMmsis: normalized };
|
mapFleetHoverStateRef.current = { ownerKey, vesselMmsis: normalized };
|
||||||
if (ownerKey != null || normalized.length > 0) {
|
|
||||||
mapDrivenFleetHoverRef.current = true;
|
|
||||||
onHoverFleet?.(ownerKey, normalized);
|
|
||||||
} else if (mapDrivenFleetHoverRef.current) {
|
|
||||||
onClearFleetHover?.();
|
|
||||||
mapDrivenFleetHoverRef.current = false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[
|
[setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis, touchDeckHoverState],
|
||||||
setHoveredDeckFleetOwner,
|
|
||||||
setHoveredDeckFleetMmsis,
|
|
||||||
touchDeckHoverState,
|
|
||||||
onHoverFleet,
|
|
||||||
onClearFleetHover,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// hover RAF cleanup
|
// hover RAF cleanup
|
||||||
@ -492,37 +413,37 @@ export function Map3D({
|
|||||||
}, [pairLinks]);
|
}, [pairLinks]);
|
||||||
|
|
||||||
const pairLinksInteractive = useMemo(() => {
|
const pairLinksInteractive = useMemo(() => {
|
||||||
if ((pairLinks?.length ?? 0) === 0) return [];
|
if (!overlays.pairLines || (pairLinks?.length ?? 0) === 0) return [];
|
||||||
if (effectiveHoveredPairMmsiSet.size < 2) return [];
|
if (hoveredPairMmsiSetRef.size < 2) return [];
|
||||||
const links = pairLinks || [];
|
const links = pairLinks || [];
|
||||||
return links.filter((link) =>
|
return links.filter((link) =>
|
||||||
effectiveHoveredPairMmsiSet.has(link.aMmsi) && effectiveHoveredPairMmsiSet.has(link.bMmsi),
|
hoveredPairMmsiSetRef.has(link.aMmsi) && hoveredPairMmsiSetRef.has(link.bMmsi),
|
||||||
);
|
);
|
||||||
}, [pairLinks, effectiveHoveredPairMmsiSet]);
|
}, [pairLinks, hoveredPairMmsiSetRef, overlays.pairLines]);
|
||||||
|
|
||||||
const pairRangesInteractive = useMemo(() => {
|
const pairRangesInteractive = useMemo(() => {
|
||||||
if (pairRanges.length === 0) return [];
|
if (!overlays.pairRange || pairRanges.length === 0) return [];
|
||||||
if (effectiveHoveredPairMmsiSet.size < 2) return [];
|
if (hoveredPairMmsiSetRef.size < 2) return [];
|
||||||
return pairRanges.filter((range) =>
|
return pairRanges.filter((range) =>
|
||||||
effectiveHoveredPairMmsiSet.has(range.aMmsi) && effectiveHoveredPairMmsiSet.has(range.bMmsi),
|
hoveredPairMmsiSetRef.has(range.aMmsi) && hoveredPairMmsiSetRef.has(range.bMmsi),
|
||||||
);
|
);
|
||||||
}, [pairRanges, effectiveHoveredPairMmsiSet]);
|
}, [pairRanges, hoveredPairMmsiSetRef, overlays.pairRange]);
|
||||||
|
|
||||||
const fcLinesInteractive = useMemo(() => {
|
const fcLinesInteractive = useMemo(() => {
|
||||||
if (fcDashed.length === 0) return [];
|
if (!overlays.fcLines || fcDashed.length === 0) return [];
|
||||||
if (highlightedMmsiSetCombined.size === 0) return [];
|
if (highlightedMmsiSetCombined.size === 0) return [];
|
||||||
return fcDashed.filter(
|
return fcDashed.filter(
|
||||||
(line) =>
|
(line) =>
|
||||||
[line.fromMmsi, line.toMmsi].some((mmsi) => (mmsi == null ? false : highlightedMmsiSetCombined.has(mmsi))),
|
[line.fromMmsi, line.toMmsi].some((mmsi) => (mmsi == null ? false : highlightedMmsiSetCombined.has(mmsi))),
|
||||||
);
|
);
|
||||||
}, [fcDashed, highlightedMmsiSetCombined]);
|
}, [fcDashed, overlays.fcLines, highlightedMmsiSetCombined]);
|
||||||
|
|
||||||
const fleetCirclesInteractive = useMemo(() => {
|
const fleetCirclesInteractive = useMemo(() => {
|
||||||
if ((fleetCircles?.length ?? 0) === 0) return [];
|
if (!overlays.fleetCircles || (fleetCircles?.length ?? 0) === 0) return [];
|
||||||
if (hoveredFleetOwnerKeys.size === 0 && highlightedMmsiSetCombined.size === 0) return [];
|
if (hoveredFleetOwnerKeys.size === 0 && highlightedMmsiSetCombined.size === 0) return [];
|
||||||
const circles = fleetCircles || [];
|
const circles = fleetCircles || [];
|
||||||
return circles.filter((item) => isHighlightedFleet(item.ownerKey, item.vesselMmsis));
|
return circles.filter((item) => isHighlightedFleet(item.ownerKey, item.vesselMmsis));
|
||||||
}, [fleetCircles, hoveredFleetOwnerKeys, isHighlightedFleet, highlightedMmsiSetCombined]);
|
}, [fleetCircles, hoveredFleetOwnerKeys, isHighlightedFleet, overlays.fleetCircles, highlightedMmsiSetCombined]);
|
||||||
|
|
||||||
// ── Hook orchestration ───────────────────────────────────────────────
|
// ── Hook orchestration ───────────────────────────────────────────────
|
||||||
const { ensureMercatorOverlay, pulseMapSync } = useMapInit(
|
const { ensureMercatorOverlay, pulseMapSync } = useMapInit(
|
||||||
@ -559,9 +480,9 @@ export function Map3D({
|
|||||||
useGlobeShips(
|
useGlobeShips(
|
||||||
mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
|
mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
|
||||||
{
|
{
|
||||||
projection, settings, shipData: shipLayerData, shipHighlightSet, shipHoverOverlaySet,
|
projection, settings, shipData, shipHighlightSet, shipHoverOverlaySet,
|
||||||
shipOverlayLayerData, shipLayerData, shipByMmsi, mapSyncEpoch,
|
shipOverlayLayerData, shipLayerData, shipByMmsi, mapSyncEpoch,
|
||||||
onSelectMmsi, onToggleHighlightMmsi, targets: shipLayerData, overlays,
|
onSelectMmsi, onToggleHighlightMmsi, targets, overlays,
|
||||||
legacyHits, selectedMmsi, isBaseHighlightedMmsi, hasAuxiliarySelectModifier,
|
legacyHits, selectedMmsi, isBaseHighlightedMmsi, hasAuxiliarySelectModifier,
|
||||||
onGlobeShipsReady,
|
onGlobeShipsReady,
|
||||||
},
|
},
|
||||||
@ -588,7 +509,7 @@ export function Map3D({
|
|||||||
useDeckLayers(
|
useDeckLayers(
|
||||||
mapRef, overlayRef, globeDeckLayerRef, projectionBusyRef,
|
mapRef, overlayRef, globeDeckLayerRef, projectionBusyRef,
|
||||||
{
|
{
|
||||||
projection, settings, trackReplayDeckLayers: trackReplayRenderState.trackReplayDeckLayers, shipLayerData, shipOverlayLayerData, shipData,
|
projection, settings, trackReplayDeckLayers: [], shipLayerData, shipOverlayLayerData, shipData,
|
||||||
legacyHits, pairLinks, fcLinks, fcDashed, fleetCircles, pairRanges,
|
legacyHits, pairLinks, fcLinks, fcDashed, fleetCircles, pairRanges,
|
||||||
pairLinksInteractive, pairRangesInteractive, fcLinesInteractive, fleetCirclesInteractive,
|
pairLinksInteractive, pairRangesInteractive, fcLinesInteractive, fleetCirclesInteractive,
|
||||||
overlays, shipByMmsi, selectedMmsi, shipHighlightSet,
|
overlays, shipByMmsi, selectedMmsi, shipHighlightSet,
|
||||||
@ -615,9 +536,9 @@ export function Map3D({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
useTrackReplayLayer(
|
useVesselTrackLayer(
|
||||||
mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
|
mapRef, overlayRef, projectionBusyRef, reorderGlobeFeatureLayers,
|
||||||
{ activeTrack, projection, mapSyncEpoch, renderState: trackReplayRenderState },
|
{ activeTrack, projection, mapSyncEpoch },
|
||||||
);
|
);
|
||||||
|
|
||||||
// 우클릭 컨텍스트 메뉴 — 대상선박(legacyHits)만 허용
|
// 우클릭 컨텍스트 메뉴 — 대상선박(legacyHits)만 허용
|
||||||
@ -640,7 +561,7 @@ export function Map3D({
|
|||||||
// Globe: MapLibre 네이티브 레이어에서 쿼리
|
// Globe: MapLibre 네이티브 레이어에서 쿼리
|
||||||
const point: [number, number] = [e.offsetX, e.offsetY];
|
const point: [number, number] = [e.offsetX, e.offsetY];
|
||||||
const shipLayerIds = [
|
const shipLayerIds = [
|
||||||
'ships-globe', 'ships-globe-lite', 'ships-globe-halo', 'ships-globe-outline',
|
'ships-globe', 'ships-globe-halo', 'ships-globe-outline',
|
||||||
].filter((id) => map.getLayer(id));
|
].filter((id) => map.getLayer(id));
|
||||||
|
|
||||||
let features: maplibregl.MapGeoJSONFeature[] = [];
|
let features: maplibregl.MapGeoJSONFeature[] = [];
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import {
|
|||||||
PAIR_RANGE_NORMAL_ML_HL, PAIR_RANGE_WARN_ML_HL,
|
PAIR_RANGE_NORMAL_ML_HL, PAIR_RANGE_WARN_ML_HL,
|
||||||
FC_LINE_NORMAL_ML, FC_LINE_SUSPICIOUS_ML,
|
FC_LINE_NORMAL_ML, FC_LINE_SUSPICIOUS_ML,
|
||||||
FC_LINE_NORMAL_ML_HL, FC_LINE_SUSPICIOUS_ML_HL,
|
FC_LINE_NORMAL_ML_HL, FC_LINE_SUSPICIOUS_ML_HL,
|
||||||
FLEET_FILL_ML_HL,
|
|
||||||
FLEET_LINE_ML, FLEET_LINE_ML_HL,
|
FLEET_LINE_ML, FLEET_LINE_ML_HL,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import { makeUniqueSorted } from '../lib/setUtils';
|
import { makeUniqueSorted } from '../lib/setUtils';
|
||||||
@ -67,8 +66,7 @@ export function useGlobeOverlays(
|
|||||||
const ensure = () => {
|
const ensure = () => {
|
||||||
if (projectionBusyRef.current) return;
|
if (projectionBusyRef.current) return;
|
||||||
if (!map.isStyleLoaded()) return;
|
if (!map.isStyleLoaded()) return;
|
||||||
const pairHoverActive = hoveredPairMmsiList.length >= 2;
|
if (projection !== 'globe' || !overlays.pairLines || (pairLinks?.length ?? 0) === 0) {
|
||||||
if (projection !== 'globe' || (!overlays.pairLines && !pairHoverActive) || (pairLinks?.length ?? 0) === 0) {
|
|
||||||
remove();
|
remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -142,7 +140,7 @@ export function useGlobeOverlays(
|
|||||||
return () => {
|
return () => {
|
||||||
stop();
|
stop();
|
||||||
};
|
};
|
||||||
}, [projection, overlays.pairLines, pairLinks, hoveredPairMmsiList, mapSyncEpoch, reorderGlobeFeatureLayers]);
|
}, [projection, overlays.pairLines, pairLinks, mapSyncEpoch, reorderGlobeFeatureLayers]);
|
||||||
|
|
||||||
// FC lines
|
// FC lines
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -159,9 +157,7 @@ export function useGlobeOverlays(
|
|||||||
const ensure = () => {
|
const ensure = () => {
|
||||||
if (projectionBusyRef.current) return;
|
if (projectionBusyRef.current) return;
|
||||||
if (!map.isStyleLoaded()) return;
|
if (!map.isStyleLoaded()) return;
|
||||||
const fleetAwarePairMmsiList = makeUniqueSorted([...hoveredPairMmsiList, ...hoveredFleetMmsiList]);
|
if (projection !== 'globe' || !overlays.fcLines) {
|
||||||
const fcHoverActive = fleetAwarePairMmsiList.length > 0;
|
|
||||||
if (projection !== 'globe' || (!overlays.fcLines && !fcHoverActive)) {
|
|
||||||
remove();
|
remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -239,15 +235,7 @@ export function useGlobeOverlays(
|
|||||||
return () => {
|
return () => {
|
||||||
stop();
|
stop();
|
||||||
};
|
};
|
||||||
}, [
|
}, [projection, overlays.fcLines, fcLinks, mapSyncEpoch, reorderGlobeFeatureLayers]);
|
||||||
projection,
|
|
||||||
overlays.fcLines,
|
|
||||||
fcLinks,
|
|
||||||
hoveredPairMmsiList,
|
|
||||||
hoveredFleetMmsiList,
|
|
||||||
mapSyncEpoch,
|
|
||||||
reorderGlobeFeatureLayers,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Fleet circles
|
// Fleet circles
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -255,35 +243,26 @@ export function useGlobeOverlays(
|
|||||||
if (!map) return;
|
if (!map) return;
|
||||||
|
|
||||||
const srcId = 'fleet-circles-ml-src';
|
const srcId = 'fleet-circles-ml-src';
|
||||||
const fillSrcId = 'fleet-circles-ml-fill-src';
|
|
||||||
const layerId = 'fleet-circles-ml';
|
const layerId = 'fleet-circles-ml';
|
||||||
const fillLayerId = 'fleet-circles-ml-fill';
|
|
||||||
|
|
||||||
// fill layer 제거됨 — globe tessellation에서 vertex 65535 초과 경고 원인
|
// fill layer 제거됨 — globe tessellation에서 vertex 65535 초과 경고 원인
|
||||||
// 라인만으로 fleet circle 시각화 충분
|
// 라인만으로 fleet circle 시각화 충분
|
||||||
|
|
||||||
const remove = () => {
|
const remove = () => {
|
||||||
guardedSetVisibility(map, layerId, 'none');
|
guardedSetVisibility(map, layerId, 'none');
|
||||||
guardedSetVisibility(map, fillLayerId, 'none');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensure = () => {
|
const ensure = () => {
|
||||||
if (projectionBusyRef.current) return;
|
if (projectionBusyRef.current) return;
|
||||||
if (!map.isStyleLoaded()) return;
|
if (!map.isStyleLoaded()) return;
|
||||||
const fleetHoverActive = hoveredFleetOwnerKeyList.length > 0 || hoveredFleetMmsiList.length > 0;
|
if (projection !== 'globe' || !overlays.fleetCircles || (fleetCircles?.length ?? 0) === 0) {
|
||||||
if (projection !== 'globe' || (!overlays.fleetCircles && !fleetHoverActive) || (fleetCircles?.length ?? 0) === 0) {
|
|
||||||
remove();
|
remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const circles = fleetCircles || [];
|
|
||||||
const isHighlightedFleet = (ownerKey: string, vesselMmsis: number[]) =>
|
|
||||||
hoveredFleetOwnerKeyList.includes(ownerKey) ||
|
|
||||||
(hoveredFleetMmsiList.length > 0 && vesselMmsis.some((mmsi) => hoveredFleetMmsiList.includes(mmsi)));
|
|
||||||
|
|
||||||
const fcLine: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
|
const fcLine: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: circles.map((c) => {
|
features: (fleetCircles || []).map((c) => {
|
||||||
const ring = circleRingLngLat(c.center, c.radiusNm * 1852);
|
const ring = circleRingLngLat(c.center, c.radiusNm * 1852);
|
||||||
return {
|
return {
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
@ -301,23 +280,6 @@ export function useGlobeOverlays(
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const fcFill: GeoJSON.FeatureCollection<GeoJSON.Polygon> = {
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: circles
|
|
||||||
.filter((c) => isHighlightedFleet(c.ownerKey, c.vesselMmsis))
|
|
||||||
.map((c) => ({
|
|
||||||
type: 'Feature',
|
|
||||||
id: makeFleetCircleFeatureId(`${c.ownerKey}-fill`),
|
|
||||||
geometry: {
|
|
||||||
type: 'Polygon',
|
|
||||||
coordinates: [circleRingLngLat(c.center, c.radiusNm * 1852, 24)],
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
ownerKey: c.ownerKey,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existing = map.getSource(srcId) as GeoJSONSource | undefined;
|
const existing = map.getSource(srcId) as GeoJSONSource | undefined;
|
||||||
if (existing) existing.setData(fcLine);
|
if (existing) existing.setData(fcLine);
|
||||||
@ -327,14 +289,6 @@ export function useGlobeOverlays(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const existingFill = map.getSource(fillSrcId) as GeoJSONSource | undefined;
|
|
||||||
if (existingFill) existingFill.setData(fcFill);
|
|
||||||
else map.addSource(fillSrcId, { type: 'geojson', data: fcFill } as GeoJSONSourceSpecification);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Fleet circles fill source setup failed:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!map.getLayer(layerId)) {
|
if (!map.getLayer(layerId)) {
|
||||||
try {
|
try {
|
||||||
map.addLayer(
|
map.addLayer(
|
||||||
@ -358,27 +312,6 @@ export function useGlobeOverlays(
|
|||||||
guardedSetVisibility(map, layerId, 'visible');
|
guardedSetVisibility(map, layerId, 'visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!map.getLayer(fillLayerId)) {
|
|
||||||
try {
|
|
||||||
map.addLayer(
|
|
||||||
{
|
|
||||||
id: fillLayerId,
|
|
||||||
type: 'fill',
|
|
||||||
source: fillSrcId,
|
|
||||||
layout: { visibility: fcFill.features.length > 0 ? 'visible' : 'none' },
|
|
||||||
paint: {
|
|
||||||
'fill-color': FLEET_FILL_ML_HL,
|
|
||||||
},
|
|
||||||
} as unknown as LayerSpecification,
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Fleet circles fill layer add failed:', e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
guardedSetVisibility(map, fillLayerId, fcFill.features.length > 0 ? 'visible' : 'none');
|
|
||||||
}
|
|
||||||
|
|
||||||
reorderGlobeFeatureLayers();
|
reorderGlobeFeatureLayers();
|
||||||
kickRepaint(map);
|
kickRepaint(map);
|
||||||
};
|
};
|
||||||
@ -388,15 +321,7 @@ export function useGlobeOverlays(
|
|||||||
return () => {
|
return () => {
|
||||||
stop();
|
stop();
|
||||||
};
|
};
|
||||||
}, [
|
}, [projection, overlays.fleetCircles, fleetCircles, mapSyncEpoch, reorderGlobeFeatureLayers]);
|
||||||
projection,
|
|
||||||
overlays.fleetCircles,
|
|
||||||
fleetCircles,
|
|
||||||
hoveredFleetOwnerKeyList,
|
|
||||||
hoveredFleetMmsiList,
|
|
||||||
mapSyncEpoch,
|
|
||||||
reorderGlobeFeatureLayers,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Pair range
|
// Pair range
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -413,8 +338,7 @@ export function useGlobeOverlays(
|
|||||||
const ensure = () => {
|
const ensure = () => {
|
||||||
if (projectionBusyRef.current) return;
|
if (projectionBusyRef.current) return;
|
||||||
if (!map.isStyleLoaded()) return;
|
if (!map.isStyleLoaded()) return;
|
||||||
const pairHoverActive = hoveredPairMmsiList.length >= 2;
|
if (projection !== 'globe' || !overlays.pairRange) {
|
||||||
if (projection !== 'globe' || (!overlays.pairRange && !pairHoverActive)) {
|
|
||||||
remove();
|
remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -503,7 +427,7 @@ export function useGlobeOverlays(
|
|||||||
return () => {
|
return () => {
|
||||||
stop();
|
stop();
|
||||||
};
|
};
|
||||||
}, [projection, overlays.pairRange, pairLinks, hoveredPairMmsiList, mapSyncEpoch, reorderGlobeFeatureLayers]);
|
}, [projection, overlays.pairRange, pairLinks, mapSyncEpoch, reorderGlobeFeatureLayers]);
|
||||||
|
|
||||||
// Paint state updates for hover highlights
|
// Paint state updates for hover highlights
|
||||||
// eslint-disable-next-line react-hooks/preserve-manual-memoization
|
// eslint-disable-next-line react-hooks/preserve-manual-memoization
|
||||||
|
|||||||
@ -270,14 +270,13 @@ export function useGlobeShips(
|
|||||||
const srcId = 'ships-globe-src';
|
const srcId = 'ships-globe-src';
|
||||||
const haloId = 'ships-globe-halo';
|
const haloId = 'ships-globe-halo';
|
||||||
const outlineId = 'ships-globe-outline';
|
const outlineId = 'ships-globe-outline';
|
||||||
const symbolLiteId = 'ships-globe-lite';
|
|
||||||
const symbolId = 'ships-globe';
|
const symbolId = 'ships-globe';
|
||||||
const labelId = 'ships-globe-label';
|
const labelId = 'ships-globe-label';
|
||||||
|
|
||||||
// 레이어를 제거하지 않고 visibility만 'none'으로 설정
|
// 레이어를 제거하지 않고 visibility만 'none'으로 설정
|
||||||
// guardedSetVisibility로 현재 값과 동일하면 호출 생략 (style._changed 방지)
|
// guardedSetVisibility로 현재 값과 동일하면 호출 생략 (style._changed 방지)
|
||||||
const hide = () => {
|
const hide = () => {
|
||||||
for (const id of [labelId, symbolId, symbolLiteId, outlineId, haloId]) {
|
for (const id of [labelId, symbolId, outlineId, haloId]) {
|
||||||
guardedSetVisibility(map, id, 'none');
|
guardedSetVisibility(map, id, 'none');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -301,12 +300,10 @@ export function useGlobeShips(
|
|||||||
// → style._changed 방지 → 불필요한 symbol placement 재계산 방지 → 라벨 사라짐 방지
|
// → style._changed 방지 → 불필요한 symbol placement 재계산 방지 → 라벨 사라짐 방지
|
||||||
const visibility: 'visible' | 'none' = projection === 'globe' ? 'visible' : 'none';
|
const visibility: 'visible' | 'none' = projection === 'globe' ? 'visible' : 'none';
|
||||||
const labelVisibility: 'visible' | 'none' = projection === 'globe' && overlays.shipLabels ? 'visible' : 'none';
|
const labelVisibility: 'visible' | 'none' = projection === 'globe' && overlays.shipLabels ? 'visible' : 'none';
|
||||||
if (map.getLayer(symbolId) || map.getLayer(symbolLiteId)) {
|
if (map.getLayer(symbolId)) {
|
||||||
const changed =
|
const changed = map.getLayoutProperty(symbolId, 'visibility') !== visibility;
|
||||||
map.getLayoutProperty(symbolId, 'visibility') !== visibility ||
|
|
||||||
map.getLayoutProperty(symbolLiteId, 'visibility') !== visibility;
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
for (const id of [haloId, outlineId, symbolLiteId, symbolId]) {
|
for (const id of [haloId, outlineId, symbolId]) {
|
||||||
guardedSetVisibility(map, id, visibility);
|
guardedSetVisibility(map, id, visibility);
|
||||||
}
|
}
|
||||||
if (projection === 'globe') kickRepaint(map);
|
if (projection === 'globe') kickRepaint(map);
|
||||||
@ -345,18 +342,6 @@ export function useGlobeShips(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const before = undefined;
|
const before = undefined;
|
||||||
const priorityFilter = [
|
|
||||||
'any',
|
|
||||||
['==', ['to-number', ['get', 'permitted'], 0], 1],
|
|
||||||
['==', ['to-number', ['get', 'selected'], 0], 1],
|
|
||||||
['==', ['to-number', ['get', 'highlighted'], 0], 1],
|
|
||||||
] as unknown as unknown[];
|
|
||||||
const nonPriorityFilter = [
|
|
||||||
'all',
|
|
||||||
['==', ['to-number', ['get', 'permitted'], 0], 0],
|
|
||||||
['==', ['to-number', ['get', 'selected'], 0], 0],
|
|
||||||
['==', ['to-number', ['get', 'highlighted'], 0], 0],
|
|
||||||
] as unknown as unknown[];
|
|
||||||
|
|
||||||
if (!map.getLayer(haloId)) {
|
if (!map.getLayer(haloId)) {
|
||||||
try {
|
try {
|
||||||
@ -443,76 +428,6 @@ export function useGlobeShips(
|
|||||||
}
|
}
|
||||||
// outline: data-driven expressions are static — visibility handled by fast toggle
|
// outline: data-driven expressions are static — visibility handled by fast toggle
|
||||||
|
|
||||||
if (!map.getLayer(symbolLiteId)) {
|
|
||||||
try {
|
|
||||||
map.addLayer(
|
|
||||||
{
|
|
||||||
id: symbolLiteId,
|
|
||||||
type: 'symbol',
|
|
||||||
source: srcId,
|
|
||||||
minzoom: 6.5,
|
|
||||||
filter: nonPriorityFilter as never,
|
|
||||||
layout: {
|
|
||||||
visibility,
|
|
||||||
'symbol-sort-key': 40 as never,
|
|
||||||
'icon-image': [
|
|
||||||
'case',
|
|
||||||
['==', ['to-number', ['get', 'isAnchored'], 0], 1],
|
|
||||||
anchoredImgId,
|
|
||||||
imgId,
|
|
||||||
] as never,
|
|
||||||
'icon-size': [
|
|
||||||
'interpolate',
|
|
||||||
['linear'],
|
|
||||||
['zoom'],
|
|
||||||
6.5,
|
|
||||||
['*', ['to-number', ['get', 'iconSize7'], 0.45], 0.45],
|
|
||||||
8,
|
|
||||||
['*', ['to-number', ['get', 'iconSize7'], 0.45], 0.62],
|
|
||||||
10,
|
|
||||||
['*', ['to-number', ['get', 'iconSize10'], 0.58], 0.72],
|
|
||||||
14,
|
|
||||||
['*', ['to-number', ['get', 'iconSize14'], 0.85], 0.78],
|
|
||||||
18,
|
|
||||||
['*', ['to-number', ['get', 'iconSize18'], 2.5], 0.78],
|
|
||||||
] as unknown as number[],
|
|
||||||
'icon-allow-overlap': true,
|
|
||||||
'icon-ignore-placement': true,
|
|
||||||
'icon-anchor': 'center',
|
|
||||||
'icon-rotate': [
|
|
||||||
'case',
|
|
||||||
['==', ['to-number', ['get', 'isAnchored'], 0], 1],
|
|
||||||
0,
|
|
||||||
['to-number', ['get', 'heading'], 0],
|
|
||||||
] as never,
|
|
||||||
'icon-rotation-alignment': 'map',
|
|
||||||
'icon-pitch-alignment': 'map',
|
|
||||||
},
|
|
||||||
paint: {
|
|
||||||
'icon-color': ['coalesce', ['get', 'shipColor'], '#64748b'] as never,
|
|
||||||
'icon-opacity': [
|
|
||||||
'interpolate',
|
|
||||||
['linear'],
|
|
||||||
['zoom'],
|
|
||||||
6.5,
|
|
||||||
0.16,
|
|
||||||
8,
|
|
||||||
0.34,
|
|
||||||
11,
|
|
||||||
0.54,
|
|
||||||
14,
|
|
||||||
0.68,
|
|
||||||
] as never,
|
|
||||||
},
|
|
||||||
} as unknown as LayerSpecification,
|
|
||||||
before,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Ship lite symbol layer add failed:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// lite symbol: lower LOD for non-priority vessels in low zoom
|
|
||||||
|
|
||||||
if (!map.getLayer(symbolId)) {
|
if (!map.getLayer(symbolId)) {
|
||||||
try {
|
try {
|
||||||
map.addLayer(
|
map.addLayer(
|
||||||
@ -520,7 +435,6 @@ export function useGlobeShips(
|
|||||||
id: symbolId,
|
id: symbolId,
|
||||||
type: 'symbol',
|
type: 'symbol',
|
||||||
source: srcId,
|
source: srcId,
|
||||||
filter: priorityFilter as never,
|
|
||||||
layout: {
|
layout: {
|
||||||
visibility,
|
visibility,
|
||||||
'symbol-sort-key': [
|
'symbol-sort-key': [
|
||||||
@ -561,10 +475,10 @@ export function useGlobeShips(
|
|||||||
'icon-color': ['coalesce', ['get', 'shipColor'], '#64748b'] as never,
|
'icon-color': ['coalesce', ['get', 'shipColor'], '#64748b'] as never,
|
||||||
'icon-opacity': [
|
'icon-opacity': [
|
||||||
'case',
|
'case',
|
||||||
['==', ['get', 'selected'], 1], 1,
|
['==', ['get', 'permitted'], 1], 1,
|
||||||
['==', ['get', 'highlighted'], 1], 0.95,
|
['==', ['get', 'selected'], 1], 0.86,
|
||||||
['==', ['get', 'permitted'], 1], 0.93,
|
['==', ['get', 'highlighted'], 1], 0.82,
|
||||||
0.9,
|
0.66,
|
||||||
] as never,
|
] as never,
|
||||||
},
|
},
|
||||||
} as unknown as LayerSpecification,
|
} as unknown as LayerSpecification,
|
||||||
@ -906,14 +820,13 @@ export function useGlobeShips(
|
|||||||
if (projection !== 'globe' || !settings.showShips) return;
|
if (projection !== 'globe' || !settings.showShips) return;
|
||||||
|
|
||||||
const symbolId = 'ships-globe';
|
const symbolId = 'ships-globe';
|
||||||
const symbolLiteId = 'ships-globe-lite';
|
|
||||||
const haloId = 'ships-globe-halo';
|
const haloId = 'ships-globe-halo';
|
||||||
const outlineId = 'ships-globe-outline';
|
const outlineId = 'ships-globe-outline';
|
||||||
const clickedRadiusDeg2 = Math.pow(0.08, 2);
|
const clickedRadiusDeg2 = Math.pow(0.08, 2);
|
||||||
|
|
||||||
const onClick = (e: maplibregl.MapMouseEvent) => {
|
const onClick = (e: maplibregl.MapMouseEvent) => {
|
||||||
try {
|
try {
|
||||||
const layerIds = [symbolId, symbolLiteId, haloId, outlineId].filter((id) => map.getLayer(id));
|
const layerIds = [symbolId, haloId, outlineId].filter((id) => map.getLayer(id));
|
||||||
let feats: unknown[] = [];
|
let feats: unknown[] = [];
|
||||||
if (layerIds.length > 0) {
|
if (layerIds.length > 0) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -151,7 +151,6 @@ export function useMapInit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
mapRef.current = map;
|
mapRef.current = map;
|
||||||
setMapSyncEpoch((prev) => prev + 1);
|
|
||||||
|
|
||||||
// 양쪽 overlay를 모두 초기화 — projection 전환 시 재생성 비용 제거
|
// 양쪽 overlay를 모두 초기화 — projection 전환 시 재생성 비용 제거
|
||||||
ensureMercatorOverlay();
|
ensureMercatorOverlay();
|
||||||
|
|||||||
@ -99,10 +99,6 @@ export function useProjectionToggle(
|
|||||||
'vessel-track-arrow',
|
'vessel-track-arrow',
|
||||||
'vessel-track-pts',
|
'vessel-track-pts',
|
||||||
'vessel-track-pts-highlight',
|
'vessel-track-pts-highlight',
|
||||||
'track-replay-globe-path',
|
|
||||||
'track-replay-globe-points',
|
|
||||||
'track-replay-globe-virtual-ship',
|
|
||||||
'track-replay-globe-virtual-label',
|
|
||||||
'zones-fill',
|
'zones-fill',
|
||||||
'zones-line',
|
'zones-line',
|
||||||
'zones-label',
|
'zones-label',
|
||||||
@ -112,7 +108,6 @@ export function useProjectionToggle(
|
|||||||
'predict-vectors-hl',
|
'predict-vectors-hl',
|
||||||
'ships-globe-halo',
|
'ships-globe-halo',
|
||||||
'ships-globe-outline',
|
'ships-globe-outline',
|
||||||
'ships-globe-lite',
|
|
||||||
'ships-globe',
|
'ships-globe',
|
||||||
'ships-globe-label',
|
'ships-globe-label',
|
||||||
'ships-globe-hover-halo',
|
'ships-globe-hover-halo',
|
||||||
@ -121,7 +116,6 @@ export function useProjectionToggle(
|
|||||||
'pair-lines-ml',
|
'pair-lines-ml',
|
||||||
'fc-lines-ml',
|
'fc-lines-ml',
|
||||||
'pair-range-ml',
|
'pair-range-ml',
|
||||||
'fleet-circles-ml-fill',
|
|
||||||
'fleet-circles-ml',
|
'fleet-circles-ml',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -107,7 +107,6 @@ const GLOBE_LAYERS: NativeLayerSpec[] = [
|
|||||||
const ANIM_CYCLE_SEC = 20;
|
const ANIM_CYCLE_SEC = 20;
|
||||||
|
|
||||||
/* ── Hook ──────────────────────────────────────────────────────────── */
|
/* ── Hook ──────────────────────────────────────────────────────────── */
|
||||||
/** @deprecated trackReplay store 엔진으로 이관 완료. 유지보수 호환 용도로만 남겨둔다. */
|
|
||||||
export function useVesselTrackLayer(
|
export function useVesselTrackLayer(
|
||||||
mapRef: MutableRefObject<maplibregl.Map | null>,
|
mapRef: MutableRefObject<maplibregl.Map | null>,
|
||||||
overlayRef: MutableRefObject<MapboxOverlay | null>,
|
overlayRef: MutableRefObject<MapboxOverlay | null>,
|
||||||
|
|||||||
42
package-lock.json
generated
42
package-lock.json
generated
@ -1406,18 +1406,6 @@
|
|||||||
"@loaders.gl/core": "^4.3.0"
|
"@loaders.gl/core": "^4.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@loaders.gl/compression/node_modules/fflate": {
|
|
||||||
"version": "0.7.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz",
|
|
||||||
"integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@loaders.gl/compression/node_modules/pako": {
|
|
||||||
"version": "1.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
|
||||||
"license": "(MIT AND Zlib)"
|
|
||||||
},
|
|
||||||
"node_modules/@loaders.gl/core": {
|
"node_modules/@loaders.gl/core": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-4.3.4.tgz",
|
||||||
@ -3981,9 +3969,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fflate": {
|
"node_modules/fflate": {
|
||||||
"version": "0.8.2",
|
"version": "0.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz",
|
||||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
"integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
@ -4111,6 +4099,12 @@
|
|||||||
"node": ">=10.19"
|
"node": ">=10.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/geotiff/node_modules/pako": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/geotiff/node_modules/quick-lru": {
|
"node_modules/geotiff/node_modules/quick-lru": {
|
||||||
"version": "6.1.2",
|
"version": "6.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz",
|
||||||
@ -4466,12 +4460,6 @@
|
|||||||
"setimmediate": "^1.0.5"
|
"setimmediate": "^1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jszip/node_modules/pako": {
|
|
||||||
"version": "1.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
|
||||||
"license": "(MIT AND Zlib)"
|
|
||||||
},
|
|
||||||
"node_modules/kdbush": {
|
"node_modules/kdbush": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
||||||
@ -4752,6 +4740,12 @@
|
|||||||
"fflate": "^0.8.0"
|
"fflate": "^0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/numcodecs/node_modules/fflate": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ol": {
|
"node_modules/ol": {
|
||||||
"version": "10.8.0",
|
"version": "10.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/ol/-/ol-10.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/ol/-/ol-10.8.0.tgz",
|
||||||
@ -4836,9 +4830,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pako": {
|
"node_modules/pako": {
|
||||||
"version": "2.1.0",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
"license": "(MIT AND Zlib)"
|
"license": "(MIT AND Zlib)"
|
||||||
},
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user