chore: develop 병합으로 충돌 해결

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
htlee 2026-02-16 22:21:15 +09:00
커밋 c09211d5ed
8개의 변경된 파일88개의 추가작업 그리고 352개의 파일을 삭제

파일 보기

@ -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
파일 보기

@ -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": {