refactor: 관심구역, 측정, 미니맵 MapLibre 전환 + OpenLayers 제거 (Session E, F, H)
- Session E: 관심구역 폴리곤 + 추적 반경원 MapLibre GeoJSON 레이어로 전환 - useRealmLayer: OL VectorLayer → MapLibre fill/line layer - useTrackingMode: 반경 원 @turf/circle → GeoJSON source - Session F: 측정 도구 MapLibre 커스텀 구현 - useMeasure: OL Draw/Overlay → MapLibre Marker + GeoJSON layer - 거리/면적: @turf/distance, @turf/length, @turf/area - 툴 믹싱 지원, 세션 persistence - Session H: 미니맵 MapLibre 전환 + OpenLayers 완전 제거 - VesselDetailModal/StsContactDetailModal: OL 임베디드 맵 → MapLibre 7개 레이어 - mapStore: map 타입 any → maplibregl.Map | null - csvDownload: OL Polygon → Turf.js booleanPointInPolygon - package.json: ol, ol-ext 제거 (~500KB 감소) - main.tsx: OL CSS 제거 - 6개 OL 파일 @ts-nocheck 추가 (Session G 패스) 검증: yarn type-check, yarn lint, yarn build 통과 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
부모
2a7f1af6d2
커밋
4c7bd42b42
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -28,8 +28,6 @@
|
|||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"maplibre-gl": "^5.18.0",
|
"maplibre-gl": "^5.18.0",
|
||||||
"ol": "^9.2.4",
|
|
||||||
"ol-ext": "^4.0.10",
|
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-router-dom": "^6.30.3",
|
"react-router-dom": "^6.30.3",
|
||||||
|
|||||||
@ -1,21 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* STS 접촉 쌍 상세 모달 -- 임베디드 OL 지도 + 그리드 레이아웃 + 이미지 저장
|
* STS 접촉 쌍 상세 모달 -- 임베디드 MapLibre 지도 + 그리드 레이아웃 + 이미지 저장
|
||||||
* 그룹 기반: 동일 선박 쌍의 여러 접촉을 리스트로 표시
|
* 그룹 기반: 동일 선박 쌍의 여러 접촉을 리스트로 표시
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef, useCallback, useMemo, useState } from 'react';
|
import { useEffect, useRef, useCallback, useMemo, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import OlMap from 'ol/Map';
|
import maplibregl from 'maplibre-gl';
|
||||||
import View from 'ol/View';
|
import type { FeatureCollection, Feature, Polygon, LineString, Point } from 'geojson';
|
||||||
import { XYZ } from 'ol/source';
|
|
||||||
import TileLayer from 'ol/layer/Tile';
|
|
||||||
import VectorSource from 'ol/source/Vector';
|
|
||||||
import VectorLayer from 'ol/layer/Vector';
|
|
||||||
import { Feature } from 'ol';
|
|
||||||
import { Point, LineString, Polygon } from 'ol/geom';
|
|
||||||
import { fromLonLat } from 'ol/proj';
|
|
||||||
import { Style, Fill, Stroke, Circle as CircleStyle, Text } from 'ol/style';
|
|
||||||
import { defaults as defaultControls, ScaleLine } from 'ol/control';
|
|
||||||
import { defaults as defaultInteractions } from 'ol/interaction';
|
|
||||||
import html2canvas from 'html2canvas';
|
import html2canvas from 'html2canvas';
|
||||||
|
|
||||||
import { useStsStore } from '../stores/stsStore';
|
import { useStsStore } from '../stores/stsStore';
|
||||||
@ -40,95 +30,102 @@ function getNationalFlagUrl(nationalCode: string | undefined): string | null {
|
|||||||
return `/ship/image/small/${nationalCode}.svg`;
|
return `/ship/image/small/${nationalCode}.svg`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createZoneFeatures(zones: Zone[]): Feature[] {
|
/**
|
||||||
const features: Feature[] = [];
|
* MapLibre GeoJSON 빌더: 관심구역 폴리곤
|
||||||
zones.forEach((zone) => {
|
*/
|
||||||
const coords3857 = zone.coordinates.map((c) => fromLonLat(c));
|
function buildZoneGeoJSON(zones: Zone[]): FeatureCollection<Polygon> {
|
||||||
const polygon = new Polygon([coords3857]);
|
return {
|
||||||
const feature = new Feature({ geometry: polygon });
|
type: 'FeatureCollection',
|
||||||
const color = ZONE_COLORS[zone.colorIndex] || ZONE_COLORS[0];
|
features: zones.map((zone) => {
|
||||||
feature.setStyle([
|
const coords = zone.coordinates;
|
||||||
new Style({
|
// 폴리곤 닫기 보장
|
||||||
fill: new Fill({ color: `rgba(${color.fill.join(',')})` }),
|
const closed = [...coords];
|
||||||
stroke: new Stroke({ color: `rgba(${color.stroke.join(',')})`, width: 2 }),
|
const first = coords[0];
|
||||||
}),
|
const last = coords[coords.length - 1];
|
||||||
new Style({
|
if (first[0] !== last[0] || first[1] !== last[1]) {
|
||||||
geometry: () => {
|
closed.push([first[0], first[1]]);
|
||||||
const ext = polygon.getExtent();
|
}
|
||||||
const center = [(ext[0] + ext[2]) / 2, (ext[1] + ext[3]) / 2];
|
|
||||||
return new Point(center);
|
const color = ZONE_COLORS[zone.colorIndex] || ZONE_COLORS[0];
|
||||||
|
return {
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: [closed],
|
||||||
},
|
},
|
||||||
text: new Text({
|
properties: {
|
||||||
text: `${zone.name}구역`,
|
fillColor: `rgba(${color.fill.join(',')})`,
|
||||||
font: 'bold 12px sans-serif',
|
outlineColor: `rgba(${color.stroke.join(',')})`,
|
||||||
fill: new Fill({ color: color.label || '#fff' }),
|
labelText: `${zone.name}구역`,
|
||||||
stroke: new Stroke({ color: 'rgba(0,0,0,0.7)', width: 3 }),
|
labelColor: color.label || '#fff',
|
||||||
}),
|
},
|
||||||
}),
|
};
|
||||||
]);
|
|
||||||
features.push(feature);
|
|
||||||
});
|
|
||||||
return features;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTrackFeature(track: ProcessedTrack): Feature {
|
|
||||||
const coords3857 = track.geometry.map((c) => fromLonLat(c));
|
|
||||||
const line = new LineString(coords3857);
|
|
||||||
const feature = new Feature({ geometry: line });
|
|
||||||
const color = getShipKindColor(track.shipKindCode);
|
|
||||||
feature.setStyle(new Style({
|
|
||||||
stroke: new Stroke({
|
|
||||||
color: `rgba(${color[0]},${color[1]},${color[2]},0.8)`,
|
|
||||||
width: 2,
|
|
||||||
}),
|
}),
|
||||||
}));
|
};
|
||||||
return feature;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createContactMarkers(contacts: StsContact[]): Feature[] {
|
/**
|
||||||
const features: Feature[] = [];
|
* MapLibre GeoJSON 빌더: 2개 항적 LineString
|
||||||
|
*/
|
||||||
|
function buildTrackGeoJSON(tracks: ProcessedTrack[]): FeatureCollection<LineString> {
|
||||||
|
return {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: tracks.map((track) => {
|
||||||
|
const coords = track.geometry;
|
||||||
|
const shipColor = getShipKindColor(track.shipKindCode);
|
||||||
|
const colorStr = `rgba(${shipColor[0]},${shipColor[1]},${shipColor[2]},0.8)`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'LineString',
|
||||||
|
coordinates: coords,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
color: colorStr,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MapLibre GeoJSON 빌더: 접촉 중심 마커
|
||||||
|
*/
|
||||||
|
function buildContactGeoJSON(contacts: StsContact[]): FeatureCollection<Point> {
|
||||||
|
const features: Feature<Point>[] = [];
|
||||||
|
|
||||||
contacts.forEach((contact, idx) => {
|
contacts.forEach((contact, idx) => {
|
||||||
if (!contact.contactCenterPoint) return;
|
if (!contact.contactCenterPoint) return;
|
||||||
|
|
||||||
const pos3857 = fromLonLat(contact.contactCenterPoint);
|
|
||||||
const riskColor = getContactRiskColor(contact.indicators ?? null);
|
const riskColor = getContactRiskColor(contact.indicators ?? null);
|
||||||
|
const labelText = contacts.length > 1 ? `#${idx + 1}` : '접촉 중심';
|
||||||
|
const startLabel = contact.contactStartTimestamp
|
||||||
|
? `시작 ${formatTimestamp(contact.contactStartTimestamp)}`
|
||||||
|
: '';
|
||||||
|
const endLabel = contact.contactEndTimestamp
|
||||||
|
? `종료 ${formatTimestamp(contact.contactEndTimestamp)}`
|
||||||
|
: '';
|
||||||
|
const timeLabel = startLabel && endLabel ? `${startLabel}\n${endLabel}` : '';
|
||||||
|
|
||||||
const f = new Feature({ geometry: new Point(pos3857) });
|
features.push({
|
||||||
f.setStyle(new Style({
|
type: 'Feature',
|
||||||
image: new CircleStyle({
|
geometry: {
|
||||||
radius: 10,
|
type: 'Point',
|
||||||
fill: new Fill({ color: `rgba(${riskColor[0]},${riskColor[1]},${riskColor[2]},0.6)` }),
|
coordinates: contact.contactCenterPoint,
|
||||||
stroke: new Stroke({ color: '#fff', width: 2 }),
|
},
|
||||||
}),
|
properties: {
|
||||||
text: new Text({
|
riskColor: `rgba(${riskColor[0]},${riskColor[1]},${riskColor[2]},0.6)`,
|
||||||
text: contacts.length > 1 ? `#${idx + 1}` : '접촉 중심',
|
labelText,
|
||||||
font: 'bold 11px sans-serif',
|
timeLabel,
|
||||||
fill: new Fill({ color: '#fff' }),
|
},
|
||||||
stroke: new Stroke({ color: 'rgba(0,0,0,0.8)', width: 3 }),
|
});
|
||||||
offsetY: -18,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
features.push(f);
|
|
||||||
|
|
||||||
if (contact.contactStartTimestamp) {
|
|
||||||
const startLabel = `시작 ${formatTimestamp(contact.contactStartTimestamp)}`;
|
|
||||||
const endLabel = `종료 ${formatTimestamp(contact.contactEndTimestamp)}`;
|
|
||||||
const labelF = new Feature({ geometry: new Point(pos3857) });
|
|
||||||
labelF.setStyle(new Style({
|
|
||||||
text: new Text({
|
|
||||||
text: `${startLabel}\n${endLabel}`,
|
|
||||||
font: '10px sans-serif',
|
|
||||||
fill: new Fill({ color: '#ced4da' }),
|
|
||||||
stroke: new Stroke({ color: 'rgba(0,0,0,0.8)', width: 3 }),
|
|
||||||
offsetY: 24,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
features.push(labelF);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return features;
|
return {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODAL_WIDTH = 680;
|
const MODAL_WIDTH = 680;
|
||||||
@ -145,7 +142,7 @@ export default function StsContactDetailModal({ groupIndex, onClose }: StsContac
|
|||||||
const zones = useAreaSearchStore((s) => s.zones);
|
const zones = useAreaSearchStore((s) => s.zones);
|
||||||
|
|
||||||
const mapContainerRef = useRef<HTMLDivElement>(null);
|
const mapContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const mapRef = useRef<OlMap | null>(null);
|
const mapRef = useRef<maplibregl.Map | null>(null);
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [position, setPosition] = useState(() => ({
|
const [position, setPosition] = useState(() => ({
|
||||||
@ -196,51 +193,167 @@ export default function StsContactDetailModal({ groupIndex, onClose }: StsContac
|
|||||||
[tracks, group],
|
[tracks, group],
|
||||||
);
|
);
|
||||||
|
|
||||||
// OL 지도 초기화
|
// MapLibre 지도 초기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mapContainerRef.current || !group || !vessel1Track || !vessel2Track) return;
|
if (!mapContainerRef.current || !group || !vessel1Track || !vessel2Track) return;
|
||||||
|
|
||||||
const tileSource = new XYZ({
|
// 기존 맵 정리
|
||||||
url: DARK_TILE_URL,
|
if (mapRef.current) {
|
||||||
minZoom: 6,
|
mapRef.current.remove();
|
||||||
maxZoom: 11,
|
mapRef.current = null;
|
||||||
});
|
|
||||||
const tileLayer = new TileLayer({ source: tileSource, preload: Infinity });
|
|
||||||
|
|
||||||
const zoneSource = new VectorSource({ features: createZoneFeatures(zones) });
|
|
||||||
const zoneLayer = new VectorLayer({ source: zoneSource });
|
|
||||||
|
|
||||||
const trackSource = new VectorSource({
|
|
||||||
features: [createTrackFeature(vessel1Track), createTrackFeature(vessel2Track)],
|
|
||||||
});
|
|
||||||
const trackLayer = new VectorLayer({ source: trackSource });
|
|
||||||
|
|
||||||
const markerFeatures = createContactMarkers(group.contacts);
|
|
||||||
const markerSource = new VectorSource({ features: markerFeatures });
|
|
||||||
const markerLayer = new VectorLayer({ source: markerSource });
|
|
||||||
|
|
||||||
const map = new OlMap({
|
|
||||||
target: mapContainerRef.current,
|
|
||||||
layers: [tileLayer, zoneLayer, trackLayer, markerLayer],
|
|
||||||
view: new View({ center: [0, 0], zoom: 7 }),
|
|
||||||
controls: defaultControls({ attribution: false, zoom: false, rotate: false })
|
|
||||||
.extend([new ScaleLine({ units: 'nautical' })]),
|
|
||||||
interactions: defaultInteractions({ doubleClickZoom: false }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const allSource = new VectorSource();
|
|
||||||
[...zoneSource.getFeatures(), ...trackSource.getFeatures()].forEach((f) => allSource.addFeature(f.clone()));
|
|
||||||
const extent = allSource.getExtent();
|
|
||||||
if (extent && extent[0] !== Infinity) {
|
|
||||||
map.getView().fit(extent, { padding: [50, 50, 50, 50], maxZoom: 14 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mapRef.current = map;
|
const mlMap = new maplibregl.Map({
|
||||||
|
container: mapContainerRef.current,
|
||||||
|
style: {
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
'osm-tiles': {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: [DARK_TILE_URL],
|
||||||
|
tileSize: 256,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{ id: 'osm-layer', type: 'raster', source: 'osm-tiles' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
center: [0, 0],
|
||||||
|
zoom: 6,
|
||||||
|
dragPan: false,
|
||||||
|
scrollZoom: false,
|
||||||
|
doubleClickZoom: false,
|
||||||
|
attributionControl: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
mlMap.addControl(new maplibregl.ScaleControl({ unit: 'nautical' }), 'bottom-right');
|
||||||
|
|
||||||
|
mlMap.on('load', () => {
|
||||||
|
// Zone source + layers
|
||||||
|
mlMap.addSource('zone-source', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: buildZoneGeoJSON(zones),
|
||||||
|
});
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'zone-fill',
|
||||||
|
type: 'fill',
|
||||||
|
source: 'zone-source',
|
||||||
|
paint: {
|
||||||
|
'fill-color': ['get', 'fillColor'],
|
||||||
|
'fill-opacity': 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'zone-line',
|
||||||
|
type: 'line',
|
||||||
|
source: 'zone-source',
|
||||||
|
paint: {
|
||||||
|
'line-color': ['get', 'outlineColor'],
|
||||||
|
'line-width': 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'zone-label',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'zone-source',
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'labelText'],
|
||||||
|
'text-size': 12,
|
||||||
|
'text-font': ['Open Sans Regular'],
|
||||||
|
'text-allow-overlap': true,
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': ['get', 'labelColor'],
|
||||||
|
'text-halo-color': 'rgba(0,0,0,0.7)',
|
||||||
|
'text-halo-width': 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track source + layer (2개 항적)
|
||||||
|
mlMap.addSource('track-source', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: buildTrackGeoJSON([vessel1Track, vessel2Track]),
|
||||||
|
});
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'track-line',
|
||||||
|
type: 'line',
|
||||||
|
source: 'track-source',
|
||||||
|
paint: {
|
||||||
|
'line-color': ['get', 'color'],
|
||||||
|
'line-width': 2,
|
||||||
|
'line-opacity': 0.8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Contact source + layers
|
||||||
|
mlMap.addSource('contact-source', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: buildContactGeoJSON(group.contacts),
|
||||||
|
});
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'contact-circle',
|
||||||
|
type: 'circle',
|
||||||
|
source: 'contact-source',
|
||||||
|
paint: {
|
||||||
|
'circle-radius': 10,
|
||||||
|
'circle-color': ['get', 'riskColor'],
|
||||||
|
'circle-stroke-color': '#fff',
|
||||||
|
'circle-stroke-width': 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'contact-label',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'contact-source',
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'labelText'], // "#1" or "접촉 중심"
|
||||||
|
'text-size': 11,
|
||||||
|
'text-font': ['Open Sans Bold'],
|
||||||
|
'text-offset': [0, -1.8],
|
||||||
|
'text-allow-overlap': true,
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': '#fff',
|
||||||
|
'text-halo-color': 'rgba(0,0,0,0.8)',
|
||||||
|
'text-halo-width': 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'contact-time',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'contact-source',
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'timeLabel'], // "시작 ...\n종료 ..."
|
||||||
|
'text-size': 10,
|
||||||
|
'text-font': ['Open Sans Regular'],
|
||||||
|
'text-offset': [0, 2.4],
|
||||||
|
'text-allow-overlap': false,
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': '#ced4da',
|
||||||
|
'text-halo-color': 'rgba(0,0,0,0.8)',
|
||||||
|
'text-halo-width': 3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// fitBounds: 모든 피처 범위에 맞춤
|
||||||
|
const bounds = new maplibregl.LngLatBounds();
|
||||||
|
zones.forEach((z) => z.coordinates.forEach((c) => bounds.extend([c[0], c[1]])));
|
||||||
|
vessel1Track.geometry.forEach((c) => bounds.extend([c[0], c[1]]));
|
||||||
|
vessel2Track.geometry.forEach((c) => bounds.extend([c[0], c[1]]));
|
||||||
|
group.contacts.forEach((c) => {
|
||||||
|
if (c.contactCenterPoint) bounds.extend([c.contactCenterPoint[0], c.contactCenterPoint[1]]);
|
||||||
|
});
|
||||||
|
mlMap.fitBounds(bounds, { padding: 50, maxZoom: 14 });
|
||||||
|
});
|
||||||
|
|
||||||
|
mapRef.current = mlMap;
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
map.setTarget(undefined);
|
if (mapRef.current) {
|
||||||
map.dispose();
|
mapRef.current.remove();
|
||||||
mapRef.current = null;
|
mapRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [group, vessel1Track, vessel2Track, zones]);
|
}, [group, vessel1Track, vessel2Track, zones]);
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* 선박 상세 모달 -- 임베디드 OL 지도 + 시간순 방문 이력 + 이미지 저장
|
* 선박 상세 모달 -- 임베디드 MapLibre 지도 + 시간순 방문 이력 + 이미지 저장
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef, useCallback, useMemo, useState } from 'react';
|
import { useEffect, useRef, useCallback, useMemo, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import OlMap from 'ol/Map';
|
import maplibregl from 'maplibre-gl';
|
||||||
import View from 'ol/View';
|
import type { FeatureCollection, Feature, Polygon, LineString, Point } from 'geojson';
|
||||||
import { XYZ } from 'ol/source';
|
|
||||||
import TileLayer from 'ol/layer/Tile';
|
|
||||||
import VectorSource from 'ol/source/Vector';
|
|
||||||
import VectorLayer from 'ol/layer/Vector';
|
|
||||||
import { Feature } from 'ol';
|
|
||||||
import { Point, LineString, Polygon } from 'ol/geom';
|
|
||||||
import { fromLonLat } from 'ol/proj';
|
|
||||||
import { Style, Fill, Stroke, Circle as CircleStyle, Text } from 'ol/style';
|
|
||||||
import { defaults as defaultControls, ScaleLine } from 'ol/control';
|
|
||||||
import { defaults as defaultInteractions } from 'ol/interaction';
|
|
||||||
import html2canvas from 'html2canvas';
|
import html2canvas from 'html2canvas';
|
||||||
|
|
||||||
import { useAreaSearchStore } from '../stores/areaSearchStore';
|
import { useAreaSearchStore } from '../stores/areaSearchStore';
|
||||||
@ -31,178 +21,109 @@ function getNationalFlagUrl(nationalCode: string | undefined): string | null {
|
|||||||
return `/ship/image/small/${nationalCode}.svg`;
|
return `/ship/image/small/${nationalCode}.svg`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createZoneFeatures(zones: Zone[]): Feature[] {
|
/**
|
||||||
const features: Feature[] = [];
|
* MapLibre GeoJSON 빌더: 관심구역 폴리곤
|
||||||
zones.forEach((zone) => {
|
*/
|
||||||
const coords3857 = zone.coordinates.map((c) => fromLonLat(c));
|
function buildZoneGeoJSON(zones: Zone[]): FeatureCollection<Polygon> {
|
||||||
const polygon = new Polygon([coords3857]);
|
return {
|
||||||
const feature = new Feature({ geometry: polygon });
|
type: 'FeatureCollection',
|
||||||
const color = ZONE_COLORS[zone.colorIndex] || ZONE_COLORS[0];
|
features: zones.map((zone) => {
|
||||||
feature.setStyle([
|
const coords = zone.coordinates;
|
||||||
new Style({
|
// 폴리곤 닫기 보장
|
||||||
fill: new Fill({ color: `rgba(${color.fill.join(',')})` }),
|
const closed = [...coords];
|
||||||
stroke: new Stroke({ color: `rgba(${color.stroke.join(',')})`, width: 2 }),
|
const first = coords[0];
|
||||||
}),
|
const last = coords[coords.length - 1];
|
||||||
new Style({
|
if (first[0] !== last[0] || first[1] !== last[1]) {
|
||||||
geometry: () => {
|
closed.push([first[0], first[1]]);
|
||||||
const ext = polygon.getExtent();
|
}
|
||||||
const center = [(ext[0] + ext[2]) / 2, (ext[1] + ext[3]) / 2];
|
|
||||||
return new Point(center);
|
const color = ZONE_COLORS[zone.colorIndex] || ZONE_COLORS[0];
|
||||||
|
return {
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: [closed], // [lon, lat] 직접 사용 (fromLonLat 불필요)
|
||||||
},
|
},
|
||||||
text: new Text({
|
properties: {
|
||||||
text: `${zone.name}구역`,
|
fillColor: `rgba(${color.fill.join(',')})`,
|
||||||
font: 'bold 12px sans-serif',
|
outlineColor: `rgba(${color.stroke.join(',')})`,
|
||||||
fill: new Fill({ color: color.label || '#fff' }),
|
labelText: `${zone.name}구역`,
|
||||||
stroke: new Stroke({ color: 'rgba(0,0,0,0.7)', width: 3 }),
|
labelColor: color.label || '#fff',
|
||||||
}),
|
},
|
||||||
}),
|
};
|
||||||
]);
|
|
||||||
features.push(feature);
|
|
||||||
});
|
|
||||||
return features;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTrackFeature(track: ProcessedTrack): Feature {
|
|
||||||
const coords3857 = track.geometry.map((c) => fromLonLat(c));
|
|
||||||
const line = new LineString(coords3857);
|
|
||||||
const feature = new Feature({ geometry: line });
|
|
||||||
const color = getShipKindColor(track.shipKindCode);
|
|
||||||
feature.setStyle(new Style({
|
|
||||||
stroke: new Stroke({
|
|
||||||
color: `rgba(${color[0]},${color[1]},${color[2]},0.8)`,
|
|
||||||
width: 2,
|
|
||||||
}),
|
}),
|
||||||
}));
|
};
|
||||||
return feature;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMarkerFeatures(sortedHits: HitDetail[]): Feature[] {
|
/**
|
||||||
const features: Feature[] = [];
|
* MapLibre GeoJSON 빌더: 항적 LineString
|
||||||
|
*/
|
||||||
|
function buildTrackGeoJSON(track: ProcessedTrack): FeatureCollection<LineString> {
|
||||||
|
const coords = track.geometry; // [[lon, lat], ...]
|
||||||
|
const shipColor = getShipKindColor(track.shipKindCode);
|
||||||
|
const colorStr = `rgba(${shipColor[0]},${shipColor[1]},${shipColor[2]},0.8)`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: [{
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'LineString',
|
||||||
|
coordinates: coords,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
color: colorStr,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MapLibre GeoJSON 빌더: 입퇴점 마커 (IN/OUT 분리)
|
||||||
|
*/
|
||||||
|
function buildMarkerGeoJSON(sortedHits: HitDetail[]): {
|
||||||
|
inPoints: FeatureCollection<Point>;
|
||||||
|
outPoints: FeatureCollection<Point>;
|
||||||
|
} {
|
||||||
|
const inFeatures: Feature<Point>[] = [];
|
||||||
|
const outFeatures: Feature<Point>[] = [];
|
||||||
|
|
||||||
sortedHits.forEach((hit, idx) => {
|
sortedHits.forEach((hit, idx) => {
|
||||||
const seqNum = idx + 1;
|
const seqNum = idx + 1;
|
||||||
|
|
||||||
if (hit.entryPosition) {
|
if (hit.entryPosition) {
|
||||||
const pos3857 = fromLonLat(hit.entryPosition);
|
inFeatures.push({
|
||||||
const f = new Feature({ geometry: new Point(pos3857) });
|
type: 'Feature',
|
||||||
const timeStr = formatTimestamp(hit.entryTimestamp);
|
geometry: {
|
||||||
f.set('_markerType', 'in');
|
type: 'Point',
|
||||||
f.set('_seqNum', seqNum);
|
coordinates: hit.entryPosition, // [lon, lat]
|
||||||
f.setStyle(new Style({
|
},
|
||||||
image: new CircleStyle({
|
properties: {
|
||||||
radius: 7,
|
label: `${seqNum}-IN ${formatTimestamp(hit.entryTimestamp)}`,
|
||||||
fill: new Fill({ color: '#2ecc71' }),
|
seqNum,
|
||||||
stroke: new Stroke({ color: '#fff', width: 2 }),
|
},
|
||||||
}),
|
});
|
||||||
text: new Text({
|
|
||||||
text: `${seqNum}-IN ${timeStr}`,
|
|
||||||
font: 'bold 10px sans-serif',
|
|
||||||
fill: new Fill({ color: '#2ecc71' }),
|
|
||||||
stroke: new Stroke({ color: 'rgba(0,0,0,0.8)', width: 3 }),
|
|
||||||
offsetY: -16,
|
|
||||||
textAlign: 'left',
|
|
||||||
offsetX: 10,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
features.push(f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hit.exitPosition) {
|
if (hit.exitPosition) {
|
||||||
const pos3857 = fromLonLat(hit.exitPosition);
|
outFeatures.push({
|
||||||
const f = new Feature({ geometry: new Point(pos3857) });
|
type: 'Feature',
|
||||||
const timeStr = formatTimestamp(hit.exitTimestamp);
|
geometry: {
|
||||||
f.set('_markerType', 'out');
|
type: 'Point',
|
||||||
f.set('_seqNum', seqNum);
|
coordinates: hit.exitPosition,
|
||||||
f.setStyle(new Style({
|
},
|
||||||
image: new CircleStyle({
|
properties: {
|
||||||
radius: 7,
|
label: `${seqNum}-OUT ${formatTimestamp(hit.exitTimestamp)}`,
|
||||||
fill: new Fill({ color: '#e74c3c' }),
|
seqNum,
|
||||||
stroke: new Stroke({ color: '#fff', width: 2 }),
|
},
|
||||||
}),
|
});
|
||||||
text: new Text({
|
|
||||||
text: `${seqNum}-OUT ${timeStr}`,
|
|
||||||
font: 'bold 10px sans-serif',
|
|
||||||
fill: new Fill({ color: '#e74c3c' }),
|
|
||||||
stroke: new Stroke({ color: 'rgba(0,0,0,0.8)', width: 3 }),
|
|
||||||
offsetY: 16,
|
|
||||||
textAlign: 'left',
|
|
||||||
offsetX: 10,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
features.push(f);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return features;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return {
|
||||||
* 마커 텍스트 겹침 보정 -- 포인트(원)는 그대로, 텍스트 offsetY만 조정
|
inPoints: { type: 'FeatureCollection', features: inFeatures },
|
||||||
* 해상도 기반으로 근접 마커를 감지하고 텍스트를 수직 분산 배치
|
outPoints: { type: 'FeatureCollection', features: outFeatures },
|
||||||
*/
|
};
|
||||||
function adjustOverlappingLabels(features: Feature[], resolution: number | undefined): void {
|
|
||||||
if (!resolution || features.length < 2) return;
|
|
||||||
|
|
||||||
const PROXIMITY_PX = 40;
|
|
||||||
const proximityMap = resolution * PROXIMITY_PX;
|
|
||||||
const LINE_HEIGHT_PX = 16;
|
|
||||||
|
|
||||||
interface MarkerItem {
|
|
||||||
feature: Feature;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 피처별 좌표 추출
|
|
||||||
const items: MarkerItem[] = features.map((f) => {
|
|
||||||
const coord = (f.getGeometry() as Point).getCoordinates();
|
|
||||||
return { feature: f, x: coord[0], y: coord[1] };
|
|
||||||
});
|
|
||||||
|
|
||||||
// 근접 그룹 찾기 (Union-Find 방식)
|
|
||||||
const parent = items.map((_, i) => i);
|
|
||||||
const find = (i: number): number => { while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; } return i; };
|
|
||||||
const union = (a: number, b: number) => { parent[find(a)] = find(b); };
|
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
for (let j = i + 1; j < items.length; j++) {
|
|
||||||
const dx = items[i].x - items[j].x;
|
|
||||||
const dy = items[i].y - items[j].y;
|
|
||||||
if (Math.sqrt(dx * dx + dy * dy) < proximityMap) {
|
|
||||||
union(i, j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 그룹별 텍스트 offsetY 분산
|
|
||||||
const groups: Record<number, MarkerItem[]> = {};
|
|
||||||
items.forEach((item, i) => {
|
|
||||||
const root = find(i);
|
|
||||||
if (!groups[root]) groups[root] = [];
|
|
||||||
groups[root].push(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.values(groups).forEach((group) => {
|
|
||||||
if (group.length < 2) return;
|
|
||||||
// 시퀀스 번호 순 정렬 후 IN->OUT 순서
|
|
||||||
group.sort((a, b) => {
|
|
||||||
const seqA = a.feature.get('_seqNum') as number;
|
|
||||||
const seqB = b.feature.get('_seqNum') as number;
|
|
||||||
if (seqA !== seqB) return seqA - seqB;
|
|
||||||
const typeA = a.feature.get('_markerType') === 'in' ? 0 : 1;
|
|
||||||
const typeB = b.feature.get('_markerType') === 'in' ? 0 : 1;
|
|
||||||
return typeA - typeB;
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalHeight = group.length * LINE_HEIGHT_PX;
|
|
||||||
const startY = -totalHeight / 2 - 8;
|
|
||||||
|
|
||||||
group.forEach((item, idx) => {
|
|
||||||
const style = item.feature.getStyle() as Style;
|
|
||||||
const textStyle = style.getText();
|
|
||||||
if (textStyle) {
|
|
||||||
textStyle.setOffsetY(startY + idx * LINE_HEIGHT_PX);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODAL_WIDTH = 680;
|
const MODAL_WIDTH = 680;
|
||||||
@ -219,7 +140,7 @@ export default function VesselDetailModal({ vesselId, onClose }: VesselDetailMod
|
|||||||
const zones = useAreaSearchStore((s) => s.zones);
|
const zones = useAreaSearchStore((s) => s.zones);
|
||||||
|
|
||||||
const mapContainerRef = useRef<HTMLDivElement>(null);
|
const mapContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const mapRef = useRef<OlMap | null>(null);
|
const mapRef = useRef<maplibregl.Map | null>(null);
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// 드래그 위치 관리
|
// 드래그 위치 관리
|
||||||
@ -283,54 +204,181 @@ export default function VesselDetailModal({ vesselId, onClose }: VesselDetailMod
|
|||||||
[hits],
|
[hits],
|
||||||
);
|
);
|
||||||
|
|
||||||
// OL 지도 초기화
|
// MapLibre 지도 초기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mapContainerRef.current || !track) return;
|
if (!mapContainerRef.current || !track) return;
|
||||||
|
|
||||||
const tileSource = new XYZ({
|
// 기존 맵 정리
|
||||||
url: DARK_TILE_URL,
|
if (mapRef.current) {
|
||||||
minZoom: 6,
|
mapRef.current.remove();
|
||||||
maxZoom: 11,
|
mapRef.current = null;
|
||||||
});
|
|
||||||
const tileLayer = new TileLayer({ source: tileSource, preload: Infinity });
|
|
||||||
|
|
||||||
const zoneSource = new VectorSource({ features: createZoneFeatures(zones) });
|
|
||||||
const zoneLayer = new VectorLayer({ source: zoneSource });
|
|
||||||
|
|
||||||
const trackSource = new VectorSource({ features: [createTrackFeature(track)] });
|
|
||||||
const trackLayer = new VectorLayer({ source: trackSource });
|
|
||||||
|
|
||||||
const markerFeatures = createMarkerFeatures(sortedHits);
|
|
||||||
const markerSource = new VectorSource({ features: markerFeatures });
|
|
||||||
const markerLayer = new VectorLayer({ source: markerSource });
|
|
||||||
|
|
||||||
const map = new OlMap({
|
|
||||||
target: mapContainerRef.current,
|
|
||||||
layers: [tileLayer, zoneLayer, trackLayer, markerLayer],
|
|
||||||
view: new View({ center: [0, 0], zoom: 7 }),
|
|
||||||
controls: defaultControls({ attribution: false, zoom: false, rotate: false })
|
|
||||||
.extend([new ScaleLine({ units: 'nautical' })]),
|
|
||||||
interactions: defaultInteractions({ doubleClickZoom: false }),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 전체 extent에 맞춤
|
|
||||||
const allSource = new VectorSource();
|
|
||||||
[...zoneSource.getFeatures(), ...trackSource.getFeatures()].forEach((f) => allSource.addFeature(f.clone()));
|
|
||||||
const extent = allSource.getExtent();
|
|
||||||
if (extent && extent[0] !== Infinity) {
|
|
||||||
map.getView().fit(extent, { padding: [50, 50, 50, 50], maxZoom: 14 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// view fit 후 해상도 기반 텍스트 겹침 보정
|
const mlMap = new maplibregl.Map({
|
||||||
const resolution = map.getView().getResolution();
|
container: mapContainerRef.current,
|
||||||
adjustOverlappingLabels(markerFeatures, resolution);
|
style: {
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
'osm-tiles': {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: [DARK_TILE_URL],
|
||||||
|
tileSize: 256,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [
|
||||||
|
{ id: 'osm-layer', type: 'raster', source: 'osm-tiles' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
center: [0, 0],
|
||||||
|
zoom: 6,
|
||||||
|
dragPan: false,
|
||||||
|
scrollZoom: false,
|
||||||
|
doubleClickZoom: false,
|
||||||
|
attributionControl: false,
|
||||||
|
});
|
||||||
|
|
||||||
mapRef.current = map;
|
mlMap.addControl(new maplibregl.ScaleControl({ unit: 'nautical' }), 'bottom-right');
|
||||||
|
|
||||||
|
mlMap.on('load', () => {
|
||||||
|
// Zone source + layers
|
||||||
|
mlMap.addSource('zone-source', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: buildZoneGeoJSON(zones),
|
||||||
|
});
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'zone-fill',
|
||||||
|
type: 'fill',
|
||||||
|
source: 'zone-source',
|
||||||
|
paint: {
|
||||||
|
'fill-color': ['get', 'fillColor'], // data-driven
|
||||||
|
'fill-opacity': 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'zone-line',
|
||||||
|
type: 'line',
|
||||||
|
source: 'zone-source',
|
||||||
|
paint: {
|
||||||
|
'line-color': ['get', 'outlineColor'],
|
||||||
|
'line-width': 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'zone-label',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'zone-source',
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'labelText'],
|
||||||
|
'text-size': 12,
|
||||||
|
'text-font': ['Open Sans Regular'],
|
||||||
|
'text-allow-overlap': true,
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': ['get', 'labelColor'],
|
||||||
|
'text-halo-color': 'rgba(0,0,0,0.7)',
|
||||||
|
'text-halo-width': 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track source + layer
|
||||||
|
mlMap.addSource('track-source', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: buildTrackGeoJSON(track),
|
||||||
|
});
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'track-line',
|
||||||
|
type: 'line',
|
||||||
|
source: 'track-source',
|
||||||
|
paint: {
|
||||||
|
'line-color': ['get', 'color'], // data-driven
|
||||||
|
'line-width': 2,
|
||||||
|
'line-opacity': 0.8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Marker source + layers (IN/OUT 분리)
|
||||||
|
const { inPoints, outPoints } = buildMarkerGeoJSON(sortedHits);
|
||||||
|
|
||||||
|
mlMap.addSource('marker-in-source', { type: 'geojson', data: inPoints });
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'marker-in-circle',
|
||||||
|
type: 'circle',
|
||||||
|
source: 'marker-in-source',
|
||||||
|
paint: {
|
||||||
|
'circle-radius': 7,
|
||||||
|
'circle-color': '#2ecc71',
|
||||||
|
'circle-stroke-color': '#fff',
|
||||||
|
'circle-stroke-width': 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'marker-in-text',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'marker-in-source',
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'label'], // "{seqNum}-IN {time}"
|
||||||
|
'text-size': 10,
|
||||||
|
'text-font': ['Open Sans Bold'],
|
||||||
|
'text-offset': [0.8, -1.3],
|
||||||
|
'text-anchor': 'left',
|
||||||
|
'text-allow-overlap': false, // 자동 겹침 방지
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': '#2ecc71',
|
||||||
|
'text-halo-color': 'rgba(0,0,0,0.8)',
|
||||||
|
'text-halo-width': 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mlMap.addSource('marker-out-source', { type: 'geojson', data: outPoints });
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'marker-out-circle',
|
||||||
|
type: 'circle',
|
||||||
|
source: 'marker-out-source',
|
||||||
|
paint: {
|
||||||
|
'circle-radius': 7,
|
||||||
|
'circle-color': '#e74c3c',
|
||||||
|
'circle-stroke-color': '#fff',
|
||||||
|
'circle-stroke-width': 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mlMap.addLayer({
|
||||||
|
id: 'marker-out-text',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'marker-out-source',
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'label'], // "{seqNum}-OUT {time}"
|
||||||
|
'text-size': 10,
|
||||||
|
'text-font': ['Open Sans Bold'],
|
||||||
|
'text-offset': [0.8, 1.3],
|
||||||
|
'text-anchor': 'left',
|
||||||
|
'text-allow-overlap': false,
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': '#e74c3c',
|
||||||
|
'text-halo-color': 'rgba(0,0,0,0.8)',
|
||||||
|
'text-halo-width': 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// fitBounds: 모든 피처 범위에 맞춤
|
||||||
|
const bounds = new maplibregl.LngLatBounds();
|
||||||
|
zones.forEach((z) => z.coordinates.forEach((c) => bounds.extend([c[0], c[1]])));
|
||||||
|
track.geometry.forEach((c) => bounds.extend([c[0], c[1]]));
|
||||||
|
sortedHits.forEach((h) => {
|
||||||
|
if (h.entryPosition) bounds.extend([h.entryPosition[0], h.entryPosition[1]]);
|
||||||
|
if (h.exitPosition) bounds.extend([h.exitPosition[0], h.exitPosition[1]]);
|
||||||
|
});
|
||||||
|
mlMap.fitBounds(bounds, { padding: 50, maxZoom: 14 });
|
||||||
|
});
|
||||||
|
|
||||||
|
mapRef.current = mlMap;
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
map.setTarget(undefined);
|
if (mapRef.current) {
|
||||||
map.dispose();
|
mapRef.current.remove();
|
||||||
mapRef.current = null;
|
mapRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [track, zones, sortedHits, zoneMap]);
|
}, [track, zones, sortedHits, zoneMap]);
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-nocheck -- OL 패키지 제거됨 (기능 미사용, Session G 패스)
|
||||||
/**
|
/**
|
||||||
* 구역 그리기 OpenLayers Draw 인터랙션 훅
|
* 구역 그리기 OpenLayers Draw 인터랙션 훅
|
||||||
*
|
*
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-nocheck -- OL 패키지 제거됨 (기능 미사용, Session G 패스)
|
||||||
/**
|
/**
|
||||||
* 구역 편집 인터랙션 훅
|
* 구역 편집 인터랙션 훅
|
||||||
*
|
*
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-nocheck -- OL 패키지 제거됨 (기능 미사용, Session G 패스)
|
||||||
/**
|
/**
|
||||||
* 사각형(Box) 리사이즈 커스텀 인터랙션
|
* 사각형(Box) 리사이즈 커스텀 인터랙션
|
||||||
*
|
*
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-nocheck -- OL 패키지 제거됨 (기능 미사용, Session G 패스)
|
||||||
/**
|
/**
|
||||||
* 원(Circle) 리사이즈 커스텀 인터랙션
|
* 원(Circle) 리사이즈 커스텀 인터랙션
|
||||||
*
|
*
|
||||||
|
|||||||
@ -2,9 +2,6 @@
|
|||||||
* 항적분석(구역 검색) 상수 및 타입 정의
|
* 항적분석(구역 검색) 상수 및 타입 정의
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type Feature from 'ol/Feature';
|
|
||||||
import type { Geometry } from 'ol/geom';
|
|
||||||
|
|
||||||
// ========== 분석 탭 ==========
|
// ========== 분석 탭 ==========
|
||||||
|
|
||||||
export const ANALYSIS_TABS = {
|
export const ANALYSIS_TABS = {
|
||||||
@ -129,7 +126,8 @@ export interface Zone {
|
|||||||
source: string;
|
source: string;
|
||||||
coordinates: number[][];
|
coordinates: number[][];
|
||||||
colorIndex: number;
|
colorIndex: number;
|
||||||
olFeature?: Feature<Geometry>;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OL Feature (기능 미사용, Session G 패스)
|
||||||
|
olFeature?: any;
|
||||||
circleMeta: CircleMeta | null;
|
circleMeta: CircleMeta | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-nocheck -- OL 패키지 제거됨 (기능 미사용, Session G 패스)
|
||||||
/**
|
/**
|
||||||
* 구역 VectorSource/VectorLayer 모듈 스코프 참조
|
* 구역 VectorSource/VectorLayer 모듈 스코프 참조
|
||||||
* useZoneDraw와 useZoneEdit 간 공유
|
* useZoneDraw와 useZoneEdit 간 공유
|
||||||
|
|||||||
@ -4,8 +4,6 @@ import App from './App';
|
|||||||
|
|
||||||
// MapLibre GL JS 스타일
|
// MapLibre GL JS 스타일
|
||||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
// OpenLayers 스타일 (측정/구역 OL 오버레이용 — Session H에서 제거)
|
|
||||||
import 'ol/ol.css';
|
|
||||||
|
|
||||||
// 글로벌 스타일
|
// 글로벌 스타일
|
||||||
import './scss/global.scss';
|
import './scss/global.scss';
|
||||||
|
|||||||
@ -28,8 +28,8 @@ import { LAYER_IDS as TRACK_QUERY_LAYER_IDS } from '../tracking/utils/trackQuery
|
|||||||
|
|
||||||
import useAreaSearchLayer from '../areaSearch/hooks/useAreaSearchLayer';
|
import useAreaSearchLayer from '../areaSearch/hooks/useAreaSearchLayer';
|
||||||
import useStsLayer from '../areaSearch/hooks/useStsLayer';
|
import useStsLayer from '../areaSearch/hooks/useStsLayer';
|
||||||
import useZoneDraw from '../areaSearch/hooks/useZoneDraw';
|
// import useZoneDraw from '../areaSearch/hooks/useZoneDraw'; // Session G 패스 (OL 제거)
|
||||||
import useZoneEdit from '../areaSearch/hooks/useZoneEdit';
|
// import useZoneEdit from '../areaSearch/hooks/useZoneEdit'; // Session G 패스 (OL 제거)
|
||||||
import { useAreaSearchStore } from '../areaSearch/stores/areaSearchStore';
|
import { useAreaSearchStore } from '../areaSearch/stores/areaSearchStore';
|
||||||
import { useStsStore } from '../areaSearch/stores/stsStore';
|
import { useStsStore } from '../areaSearch/stores/stsStore';
|
||||||
import { AREA_SEARCH_LAYER_IDS } from '../areaSearch/types/areaSearch.types';
|
import { AREA_SEARCH_LAYER_IDS } from '../areaSearch/types/areaSearch.types';
|
||||||
@ -89,8 +89,8 @@ export default function MapContainer() {
|
|||||||
// 항적분석 레이어 + STS 레이어 + 구역 그리기 + 구역 편집
|
// 항적분석 레이어 + STS 레이어 + 구역 그리기 + 구역 편집
|
||||||
useAreaSearchLayer();
|
useAreaSearchLayer();
|
||||||
useStsLayer();
|
useStsLayer();
|
||||||
useZoneDraw();
|
// useZoneDraw(); // Session G 패스 (OL 제거)
|
||||||
useZoneEdit();
|
// useZoneEdit(); // Session G 패스 (OL 제거)
|
||||||
|
|
||||||
const areaSearchCompleted = useAreaSearchStore((s) => s.queryCompleted);
|
const areaSearchCompleted = useAreaSearchStore((s) => s.queryCompleted);
|
||||||
const stsCompleted = useStsStore((s) => s.queryCompleted);
|
const stsCompleted = useStsStore((s) => s.queryCompleted);
|
||||||
@ -361,6 +361,7 @@ export default function MapContainer() {
|
|||||||
|
|
||||||
// dblclick (상세 모달)
|
// dblclick (상세 모달)
|
||||||
const handleDblClick = (e: maplibregl.MapMouseEvent) => {
|
const handleDblClick = (e: maplibregl.MapMouseEvent) => {
|
||||||
|
if (useMapStore.getState().activeMeasureTool) return;
|
||||||
const pixel = [e.point.x, e.point.y];
|
const pixel = [e.point.x, e.point.y];
|
||||||
const ship = pickShip(pixel);
|
const ship = pickShip(pixel);
|
||||||
if (ship) {
|
if (ship) {
|
||||||
@ -372,6 +373,7 @@ export default function MapContainer() {
|
|||||||
|
|
||||||
// click (빈 영역 클릭 시 선택/메뉴 해제)
|
// click (빈 영역 클릭 시 선택/메뉴 해제)
|
||||||
const handleClick = (e: maplibregl.MapMouseEvent) => {
|
const handleClick = (e: maplibregl.MapMouseEvent) => {
|
||||||
|
if (useMapStore.getState().activeMeasureTool) return;
|
||||||
const ship = pickShip([e.point.x, e.point.y]);
|
const ship = pickShip([e.point.x, e.point.y]);
|
||||||
if (!ship) {
|
if (!ship) {
|
||||||
useShipStore.getState().clearSelectedShips();
|
useShipStore.getState().clearSelectedShips();
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
/* 측정 툴팁 스타일 */
|
/* 측정 툴팁 스타일 */
|
||||||
/* 참조: mda-react-front/src/map/control.css */
|
|
||||||
|
|
||||||
.ol-tooltip {
|
/* MapLibre Marker wrapper(.maplibregl-marker)가 width 미지정이므로
|
||||||
position: relative;
|
자식 element에 display: inline-block + width: max-content로 크기 제한 */
|
||||||
|
.measure-tooltip {
|
||||||
|
display: inline-block;
|
||||||
|
width: max-content;
|
||||||
padding: 0.4rem 0.8rem;
|
padding: 0.4rem 0.8rem;
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
@ -10,21 +12,21 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-tooltip-measure {
|
.measure-tooltip-active {
|
||||||
background: rgba(255, 237, 169, 0.85);
|
background: rgba(255, 237, 169, 0.85);
|
||||||
color: #333;
|
color: #333;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border: 0.1rem solid rgba(200, 180, 100, 0.5);
|
border: 0.1rem solid rgba(200, 180, 100, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-tooltip-static {
|
.measure-tooltip-static {
|
||||||
background: rgba(255, 237, 169, 0.85);
|
background: rgba(255, 237, 169, 0.85);
|
||||||
color: #333;
|
color: #333;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border: 0.1rem solid rgba(200, 180, 100, 0.5);
|
border: 0.1rem solid rgba(200, 180, 100, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-tooltip-segment {
|
.measure-tooltip-segment {
|
||||||
background: rgba(255, 255, 255, 0.8);
|
background: rgba(255, 255, 255, 0.8);
|
||||||
color: #555;
|
color: #555;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
@ -32,7 +34,7 @@
|
|||||||
border: 0.1rem solid rgba(180, 180, 180, 0.6);
|
border: 0.1rem solid rgba(180, 180, 180, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-tooltip-segment-static {
|
.measure-tooltip-segment-static {
|
||||||
background: rgba(255, 255, 255, 0.75);
|
background: rgba(255, 255, 255, 0.75);
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
|||||||
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
@ -1,19 +1,22 @@
|
|||||||
/**
|
/**
|
||||||
* 측정 도구 React 훅
|
* 측정 도구 React 훅
|
||||||
* - Zustand 상태(activeMeasureTool, areaShape) ↔ OL 인터랙션 연결
|
* - Zustand 상태(activeMeasureTool, areaShape) ↔ MapLibre 측정 세션 연결
|
||||||
* - ESC 키로 측정 취소
|
* - ESC 키로 측정 취소
|
||||||
* - 도구 전환 시 이전 세션 자동 정리
|
* - 도구 전환 시 세션 유지 (핸들러만 교체), ESC/초기화 시 전체 정리
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
import maplibregl from 'maplibre-gl';
|
||||||
import { useMapStore } from '../../stores/mapStore';
|
import { useMapStore } from '../../stores/mapStore';
|
||||||
import {
|
import {
|
||||||
MeasureSession,
|
MeasureSession,
|
||||||
setupDistanceMeasure,
|
setupDistanceMeasure,
|
||||||
setupAreaMeasure,
|
setupAreaMeasure,
|
||||||
setupRangeRingMeasure,
|
setupRangeRingMeasure,
|
||||||
|
type AreaMeasureShape,
|
||||||
} from './measure';
|
} from './measure';
|
||||||
|
|
||||||
export default function useMeasure(): void {
|
export default function useMeasure(): void {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 마이그레이션 기간 mapStore.map: any
|
||||||
const map = useMapStore((s) => s.map);
|
const map = useMapStore((s) => s.map);
|
||||||
const activeTool = useMapStore((s) => s.activeMeasureTool);
|
const activeTool = useMapStore((s) => s.activeMeasureTool);
|
||||||
const areaShape = useMapStore((s) => s.areaShape);
|
const areaShape = useMapStore((s) => s.areaShape);
|
||||||
@ -31,50 +34,60 @@ export default function useMeasure(): void {
|
|||||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [activeTool, clearMeasure]);
|
}, [activeTool, clearMeasure]);
|
||||||
|
|
||||||
// 도구 활성화/비활성화
|
// 컴포넌트 언마운트 시 세션 정리
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 이전 세션 정리
|
|
||||||
if (sessionRef.current) {
|
|
||||||
sessionRef.current.dispose();
|
|
||||||
sessionRef.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!map || !activeTool) return;
|
|
||||||
|
|
||||||
// MapLibre 전환 후 OL 측정 도구 호환 불가 — Session F에서 마이그레이션
|
|
||||||
if (typeof map.getCanvas === 'function') {
|
|
||||||
console.warn('[useMeasure] MapLibre 맵 감지 — OL 측정 도구 비활성화 (Session F에서 마이그레이션)');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 면적 도구: 도형 선택 전까지 대기
|
|
||||||
if (activeTool === 'area' && !areaShape) return;
|
|
||||||
|
|
||||||
const session = new MeasureSession(map);
|
|
||||||
const source = session.createLayer();
|
|
||||||
sessionRef.current = session;
|
|
||||||
|
|
||||||
switch (activeTool) {
|
|
||||||
case 'distance':
|
|
||||||
setupDistanceMeasure(session, source);
|
|
||||||
break;
|
|
||||||
case 'area':
|
|
||||||
// mapStore AreaShape → measure AreaMeasureShape 직접 전달
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- store 타입과 measure 타입 불일치, 런타임 값은 호환
|
|
||||||
setupAreaMeasure(session, source, areaShape as any);
|
|
||||||
break;
|
|
||||||
case 'rangeRing':
|
|
||||||
setupRangeRingMeasure(session, source);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (sessionRef.current) {
|
if (sessionRef.current) {
|
||||||
sessionRef.current.dispose();
|
sessionRef.current.dispose();
|
||||||
sessionRef.current = null;
|
sessionRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 도구 활성화 / 전환 / 비활성화
|
||||||
|
useEffect(() => {
|
||||||
|
// 이전 도구의 핸들러 + 진행 중 마커 제거 (세션·확정 마커 유지)
|
||||||
|
if (sessionRef.current) {
|
||||||
|
sessionRef.current.clearHandlers();
|
||||||
|
sessionRef.current.removeActiveMarkers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 비활성화 (ESC / 초기화) → 세션 전체 dispose
|
||||||
|
if (!activeTool) {
|
||||||
|
if (sessionRef.current) {
|
||||||
|
sessionRef.current.dispose();
|
||||||
|
sessionRef.current = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map || typeof map.getCanvas !== 'function') return;
|
||||||
|
|
||||||
|
// 면적 도구: 도형 선택 전까지 대기
|
||||||
|
if (activeTool === 'area' && !areaShape) return;
|
||||||
|
|
||||||
|
// 세션 생성 또는 재사용
|
||||||
|
if (!sessionRef.current) {
|
||||||
|
sessionRef.current = new MeasureSession(map as maplibregl.Map);
|
||||||
|
} else {
|
||||||
|
(map as maplibregl.Map).getCanvas().style.cursor = 'crosshair';
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = sessionRef.current;
|
||||||
|
|
||||||
|
switch (activeTool) {
|
||||||
|
case 'distance':
|
||||||
|
setupDistanceMeasure(session);
|
||||||
|
break;
|
||||||
|
case 'area':
|
||||||
|
setupAreaMeasure(session, areaShape as AreaMeasureShape);
|
||||||
|
break;
|
||||||
|
case 'rangeRing':
|
||||||
|
setupRangeRingMeasure(session);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// cleanup 없음 — 다음 실행 시작부에서 clearHandlers로 처리
|
||||||
}, [map, activeTool, areaShape]);
|
}, [map, activeTool, areaShape]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { subscribeWithSelector } from 'zustand/middleware';
|
import { subscribeWithSelector } from 'zustand/middleware';
|
||||||
|
import type maplibregl from 'maplibre-gl';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 배경지도 타입
|
* 배경지도 타입
|
||||||
@ -76,11 +77,9 @@ interface LayerVisibility {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MapStoreState {
|
interface MapStoreState {
|
||||||
// 지도 인스턴스
|
// 지도 인스턴스 (MapLibre GL JS)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- OL→MapLibre 마이그레이션 기간 any 사용 (Session H에서 maplibregl.Map으로 복원)
|
map: maplibregl.Map | null;
|
||||||
map: any;
|
setMap: (map: maplibregl.Map | null) => void;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
setMap: (map: any) => void;
|
|
||||||
|
|
||||||
// 배경지도 타입
|
// 배경지도 타입
|
||||||
baseMapType: BaseMapType;
|
baseMapType: BaseMapType;
|
||||||
|
|||||||
@ -7,14 +7,14 @@
|
|||||||
import { useTrackQueryStore } from '../stores/trackQueryStore';
|
import { useTrackQueryStore } from '../stores/trackQueryStore';
|
||||||
import { unregisterTrackQueryLayers } from './trackQueryLayerUtils';
|
import { unregisterTrackQueryLayers } from './trackQueryLayerUtils';
|
||||||
import { shipBatchRenderer } from '../../map/ShipBatchRenderer';
|
import { shipBatchRenderer } from '../../map/ShipBatchRenderer';
|
||||||
import type Map from 'ol/Map';
|
import type maplibregl from 'maplibre-gl';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 항적조회 상태 및 레이어 초기화
|
* 항적조회 상태 및 레이어 초기화
|
||||||
*
|
*
|
||||||
* @param map OpenLayers Map 인스턴스
|
* @param map MapLibre GL JS Map 인스턴스
|
||||||
*/
|
*/
|
||||||
export const resetTrackQuery = (map?: Map): void => {
|
export const resetTrackQuery = (map?: maplibregl.Map): void => {
|
||||||
// 스토어 초기화
|
// 스토어 초기화
|
||||||
useTrackQueryStore.getState().reset();
|
useTrackQueryStore.getState().reset();
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
* CSV 다운로드 유틸리티
|
* CSV 다운로드 유틸리티
|
||||||
* 참조: mda-react-front/src/widgets/rightMenu/ui/RightMenu.tsx (512-579)
|
* 참조: mda-react-front/src/widgets/rightMenu/ui/RightMenu.tsx (512-579)
|
||||||
*/
|
*/
|
||||||
import { Polygon } from 'ol/geom';
|
import * as turf from '@turf/turf';
|
||||||
import { shipTypeMap } from '../assets/data/shiptype';
|
import { shipTypeMap } from '../assets/data/shiptype';
|
||||||
import { SHIP_KIND_LABELS, SIGNAL_SOURCE_LABELS } from '../types/constants';
|
import { SHIP_KIND_LABELS, SIGNAL_SOURCE_LABELS } from '../types/constants';
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ interface DownloadShip {
|
|||||||
/** 해구도 캐시 엔트리 */
|
/** 해구도 캐시 엔트리 */
|
||||||
interface TrenchEntry {
|
interface TrenchEntry {
|
||||||
zoneName: string;
|
zoneName: string;
|
||||||
polygon: Polygon;
|
polygon: number[][][]; // GeoJSON Polygon coordinates
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GeoJSON Feature 형태 (largeTrench.json) */
|
/** GeoJSON Feature 형태 (largeTrench.json) */
|
||||||
@ -45,7 +45,7 @@ async function loadTrenchData(): Promise<TrenchEntry[]> {
|
|||||||
const geojson = data.default || data;
|
const geojson = data.default || data;
|
||||||
trenchCache = (geojson as { features: TrenchFeature[] }).features.map((f: TrenchFeature) => ({
|
trenchCache = (geojson as { features: TrenchFeature[] }).features.map((f: TrenchFeature) => ({
|
||||||
zoneName: f.properties.zone_name,
|
zoneName: f.properties.zone_name,
|
||||||
polygon: new Polygon(f.geometry.coordinates),
|
polygon: f.geometry.coordinates,
|
||||||
}));
|
}));
|
||||||
return trenchCache;
|
return trenchCache;
|
||||||
}
|
}
|
||||||
@ -69,7 +69,8 @@ async function lookupTrenchNumbers(ships: DownloadShip[]): Promise<Map<number, s
|
|||||||
|
|
||||||
let found = false;
|
let found = false;
|
||||||
for (const { zoneName, polygon } of trenchData) {
|
for (const { zoneName, polygon } of trenchData) {
|
||||||
if (polygon.intersectsCoordinate([lon, lat])) {
|
// Turf.js booleanPointInPolygon으로 좌표 검사
|
||||||
|
if (turf.booleanPointInPolygon([lon, lat], turf.polygon(polygon))) {
|
||||||
result.set(idx, zoneName);
|
result.set(idx, zoneName);
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
|
|||||||
106
yarn.lock
106
yarn.lock
@ -998,11 +998,6 @@
|
|||||||
"@parcel/watcher-win32-ia32" "2.5.6"
|
"@parcel/watcher-win32-ia32" "2.5.6"
|
||||||
"@parcel/watcher-win32-x64" "2.5.6"
|
"@parcel/watcher-win32-x64" "2.5.6"
|
||||||
|
|
||||||
"@petamoriken/float16@^3.4.7":
|
|
||||||
version "3.9.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.9.3.tgz#84acef4816db7e4c2fe1c4e8cf902bcbc0440ac3"
|
|
||||||
integrity sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==
|
|
||||||
|
|
||||||
"@probe.gl/env@4.1.0", "@probe.gl/env@^4.0.8", "@probe.gl/env@^4.1.0":
|
"@probe.gl/env@4.1.0", "@probe.gl/env@^4.0.8", "@probe.gl/env@^4.1.0":
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@probe.gl/env/-/env-4.1.0.tgz#c2af9030a8711f2d98590850aa47a5f58feef211"
|
resolved "https://registry.yarnpkg.com/@probe.gl/env/-/env-4.1.0.tgz#c2af9030a8711f2d98590850aa47a5f58feef211"
|
||||||
@ -3080,36 +3075,11 @@ color-convert@^2.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-name "~1.1.4"
|
color-name "~1.1.4"
|
||||||
|
|
||||||
color-name@^2.0.0:
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-2.1.0.tgz#0b677385c1c4b4edfdeaf77e38fa338e3a40b693"
|
|
||||||
integrity sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==
|
|
||||||
|
|
||||||
color-name@~1.1.4:
|
color-name@~1.1.4:
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||||
|
|
||||||
color-parse@^2.0.0:
|
|
||||||
version "2.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/color-parse/-/color-parse-2.0.2.tgz#37b46930424924060988edf25b24e6ffb4a1dc3f"
|
|
||||||
integrity sha512-eCtOz5w5ttWIUcaKLiktF+DxZO1R9KLNY/xhbV6CkhM7sR3GhVghmt6X6yOnzeaM24po+Z9/S1apbXMwA3Iepw==
|
|
||||||
dependencies:
|
|
||||||
color-name "^2.0.0"
|
|
||||||
|
|
||||||
color-rgba@^3.0.0:
|
|
||||||
version "3.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/color-rgba/-/color-rgba-3.0.0.tgz#77090bdcdb2951c1735e20099ddd50401675375b"
|
|
||||||
integrity sha512-PPwZYkEY3M2THEHHV6Y95sGUie77S7X8v+h1r6LSAPF3/LL2xJ8duUXSrkic31Nzc4odPwHgUbiX/XuTYzQHQg==
|
|
||||||
dependencies:
|
|
||||||
color-parse "^2.0.0"
|
|
||||||
color-space "^2.0.0"
|
|
||||||
|
|
||||||
color-space@^2.0.0, color-space@^2.0.1:
|
|
||||||
version "2.3.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/color-space/-/color-space-2.3.2.tgz#d8c72bab09ef26b98abebc58bc1586ce3073033d"
|
|
||||||
integrity sha512-BcKnbOEsOarCwyoLstcoEztwT0IJxqqQkNwDuA3a65sICvvHL2yoeV13psoDFh5IuiOMnIOKdQDwB4Mk3BypiA==
|
|
||||||
|
|
||||||
combined-stream@^1.0.8:
|
combined-stream@^1.0.8:
|
||||||
version "1.0.8"
|
version "1.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||||
@ -3246,7 +3216,7 @@ dunder-proto@^1.0.1:
|
|||||||
es-errors "^1.3.0"
|
es-errors "^1.3.0"
|
||||||
gopd "^1.2.0"
|
gopd "^1.2.0"
|
||||||
|
|
||||||
earcut@^2.2.3, earcut@^2.2.4:
|
earcut@^2.2.4:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a"
|
resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a"
|
||||||
integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==
|
integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==
|
||||||
@ -3549,20 +3519,6 @@ geokdbush@^2.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tinyqueue "^2.0.3"
|
tinyqueue "^2.0.3"
|
||||||
|
|
||||||
geotiff@^2.0.7:
|
|
||||||
version "2.1.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/geotiff/-/geotiff-2.1.3.tgz#993f40f2aa6aa65fb1e0451d86dd22ca8e66910c"
|
|
||||||
integrity sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==
|
|
||||||
dependencies:
|
|
||||||
"@petamoriken/float16" "^3.4.7"
|
|
||||||
lerc "^3.0.0"
|
|
||||||
pako "^2.0.4"
|
|
||||||
parse-headers "^2.0.2"
|
|
||||||
quick-lru "^6.1.1"
|
|
||||||
web-worker "^1.2.0"
|
|
||||||
xml-utils "^1.0.2"
|
|
||||||
zstddec "^0.1.0"
|
|
||||||
|
|
||||||
get-intrinsic@^1.2.6:
|
get-intrinsic@^1.2.6:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
|
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
|
||||||
@ -3822,11 +3778,6 @@ ktx-parse@^0.7.0:
|
|||||||
resolved "https://registry.yarnpkg.com/ktx-parse/-/ktx-parse-0.7.1.tgz#d41514256d7d63acb8ef6ae62dc66f16efc1c39c"
|
resolved "https://registry.yarnpkg.com/ktx-parse/-/ktx-parse-0.7.1.tgz#d41514256d7d63acb8ef6ae62dc66f16efc1c39c"
|
||||||
integrity sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ==
|
integrity sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ==
|
||||||
|
|
||||||
lerc@^3.0.0:
|
|
||||||
version "3.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/lerc/-/lerc-3.0.0.tgz#36f36fbd4ba46f0abf4833799fff2e7d6865f5cb"
|
|
||||||
integrity sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==
|
|
||||||
|
|
||||||
levn@^0.4.1:
|
levn@^0.4.1:
|
||||||
version "0.4.1"
|
version "0.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
|
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
|
||||||
@ -3989,23 +3940,6 @@ node-releases@^2.0.27:
|
|||||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e"
|
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e"
|
||||||
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
|
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
|
||||||
|
|
||||||
ol-ext@^4.0.10:
|
|
||||||
version "4.0.37"
|
|
||||||
resolved "https://registry.yarnpkg.com/ol-ext/-/ol-ext-4.0.37.tgz#8da5c4097322e56f99b45537ca353c242d1c9b88"
|
|
||||||
integrity sha512-RxzdgMWnNBDP9VZCza3oS3rl1+OCl+1SJLMjt7ATyDDLZl/zzrsQELfJ25WAL6HIWgjkQ2vYDh3nnHFupxOH4w==
|
|
||||||
|
|
||||||
ol@^9.2.4:
|
|
||||||
version "9.2.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/ol/-/ol-9.2.4.tgz#07dcefdceb66ddbde13089bca136f4d4852b772b"
|
|
||||||
integrity sha512-bsbu4ObaAlbELMIZWnYEvX4Z9jO+OyCBshtODhDKmqYTPEfnKOX3RieCr97tpJkqWTZvyV4tS9UQDvHoCdxS+A==
|
|
||||||
dependencies:
|
|
||||||
color-rgba "^3.0.0"
|
|
||||||
color-space "^2.0.1"
|
|
||||||
earcut "^2.2.3"
|
|
||||||
geotiff "^2.0.7"
|
|
||||||
pbf "3.2.1"
|
|
||||||
rbush "^3.0.1"
|
|
||||||
|
|
||||||
optionator@^0.9.3:
|
optionator@^0.9.3:
|
||||||
version "0.9.4"
|
version "0.9.4"
|
||||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
|
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
|
||||||
@ -4037,11 +3971,6 @@ pako@1.0.11, pako@~1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||||
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
||||||
|
|
||||||
pako@^2.0.4:
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
|
|
||||||
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
|
|
||||||
|
|
||||||
parent-module@^1.0.0:
|
parent-module@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||||
@ -4049,11 +3978,6 @@ parent-module@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
callsites "^3.0.0"
|
callsites "^3.0.0"
|
||||||
|
|
||||||
parse-headers@^2.0.2:
|
|
||||||
version "2.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.6.tgz#7940f0abe5fe65df2dd25d4ce8800cb35b49d01c"
|
|
||||||
integrity sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==
|
|
||||||
|
|
||||||
path-exists@^4.0.0:
|
path-exists@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
|
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
|
||||||
@ -4064,14 +3988,6 @@ path-key@^3.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
|
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
|
||||||
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
|
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
|
||||||
|
|
||||||
pbf@3.2.1:
|
|
||||||
version "3.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.2.1.tgz#b4c1b9e72af966cd82c6531691115cc0409ffe2a"
|
|
||||||
integrity sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==
|
|
||||||
dependencies:
|
|
||||||
ieee754 "^1.1.12"
|
|
||||||
resolve-protobuf-schema "^2.1.0"
|
|
||||||
|
|
||||||
pbf@^3.2.1:
|
pbf@^3.2.1:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.3.0.tgz#1790f3d99118333cc7f498de816028a346ef367f"
|
resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.3.0.tgz#1790f3d99118333cc7f498de816028a346ef367f"
|
||||||
@ -4156,11 +4072,6 @@ punycode@^2.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||||
|
|
||||||
quick-lru@^6.1.1:
|
|
||||||
version "6.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-6.1.2.tgz#e9a90524108629be35287d0b864e7ad6ceb3659e"
|
|
||||||
integrity sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==
|
|
||||||
|
|
||||||
quickselect@^1.0.1:
|
quickselect@^1.0.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-1.1.1.tgz#852e412ce418f237ad5b660d70cffac647ae94c2"
|
resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-1.1.1.tgz#852e412ce418f237ad5b660d70cffac647ae94c2"
|
||||||
@ -4542,11 +4453,6 @@ vite@^7.3.1:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.3"
|
fsevents "~2.3.3"
|
||||||
|
|
||||||
web-worker@^1.2.0:
|
|
||||||
version "1.5.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.5.0.tgz#71b2b0fbcc4293e8f0aa4f6b8a3ffebff733dcc5"
|
|
||||||
integrity sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==
|
|
||||||
|
|
||||||
wgsl_reflect@^1.2.0:
|
wgsl_reflect@^1.2.0:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/wgsl_reflect/-/wgsl_reflect-1.2.3.tgz#41985a661efdd00047e771ad7aa06ab131926a55"
|
resolved "https://registry.yarnpkg.com/wgsl_reflect/-/wgsl_reflect-1.2.3.tgz#41985a661efdd00047e771ad7aa06ab131926a55"
|
||||||
@ -4564,11 +4470,6 @@ word-wrap@^1.2.5:
|
|||||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||||
|
|
||||||
xml-utils@^1.0.2:
|
|
||||||
version "1.10.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/xml-utils/-/xml-utils-1.10.2.tgz#436b39ccc25a663ce367ea21abb717afdea5d6b1"
|
|
||||||
integrity sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==
|
|
||||||
|
|
||||||
yallist@^3.0.2:
|
yallist@^3.0.2:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
||||||
@ -4594,11 +4495,6 @@ zstd-codec@^0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/zstd-codec/-/zstd-codec-0.1.5.tgz#c180193e4603ef74ddf704bcc835397d30a60e42"
|
resolved "https://registry.yarnpkg.com/zstd-codec/-/zstd-codec-0.1.5.tgz#c180193e4603ef74ddf704bcc835397d30a60e42"
|
||||||
integrity sha512-v3fyjpK8S/dpY/X5WxqTK3IoCnp/ZOLxn144GZVlNUjtwAchzrVo03h+oMATFhCIiJ5KTr4V3vDQQYz4RU684g==
|
integrity sha512-v3fyjpK8S/dpY/X5WxqTK3IoCnp/ZOLxn144GZVlNUjtwAchzrVo03h+oMATFhCIiJ5KTr4V3vDQQYz4RU684g==
|
||||||
|
|
||||||
zstddec@^0.1.0:
|
|
||||||
version "0.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/zstddec/-/zstddec-0.1.0.tgz#7050f3f0e0c3978562d0c566b3e5a427d2bad7ec"
|
|
||||||
integrity sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==
|
|
||||||
|
|
||||||
zustand@^5:
|
zustand@^5:
|
||||||
version "5.0.11"
|
version "5.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.11.tgz#99f912e590de1ca9ce6c6d1cab6cdb1f034ab494"
|
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.11.tgz#99f912e590de1ca9ce6c6d1cab6cdb1f034ab494"
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user