refactor: OpenLayers → MapLibre GL JS 완전 전환 (Phase 3 Step 2) #1

병합
htlee refactor/maplibre-migration 에서 develop 로 8 commits 를 머지했습니다 2026-02-15 17:52:59 +09:00
34개의 변경된 파일1256개의 추가작업 그리고 802개의 파일을 삭제
Showing only changes of commit 4c7bd42b42 - Show all commits

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

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