feat(map): 지도 설정 패널 + 수심 범례 구현

- 나침반/줌 컨트롤 분리, 기어 버튼으로 설정 패널 토글
- 설정 항목: 레이블 언어, 육지/물/수심 색상, 수심 폰트 크기/색상
- 런타임 map.setPaintProperty/setLayoutProperty로 즉시 적용
- 수심 색상 범례 (좌하단 그라데이션 바 + 눈금)
- 초기화 버튼으로 디폴트 복원

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
htlee 2026-02-16 06:17:20 +09:00
부모 4c257a2883
커밋 650888adb7
10개의 변경된 파일584개의 추가작업 그리고 3개의 파일을 삭제

파일 보기

@ -921,6 +921,182 @@ body {
border-radius: 8px; border-radius: 8px;
} }
/* ── Map Settings Panel ────────────────────────────────────────────── */
.map-settings-gear {
position: absolute;
top: 95px;
left: 10px;
z-index: 850;
width: 29px;
height: 29px;
border-radius: 4px;
border: 1px solid var(--border);
background: rgba(15, 23, 42, 0.92);
backdrop-filter: blur(8px);
color: var(--muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: color 0.15s, border-color 0.15s;
user-select: none;
padding: 0;
}
.map-settings-gear:hover {
color: var(--text);
border-color: var(--accent);
}
.map-settings-gear.open {
color: var(--accent);
border-color: var(--accent);
}
.map-settings-panel {
position: absolute;
top: 10px;
left: 48px;
z-index: 850;
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(8px);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
width: 240px;
max-height: calc(100vh - 80px);
overflow-y: auto;
}
.map-settings-panel .ms-title {
font-size: 10px;
font-weight: 700;
color: var(--text);
letter-spacing: 1px;
margin-bottom: 10px;
}
.map-settings-panel .ms-section {
margin-bottom: 10px;
}
.map-settings-panel .ms-label {
font-size: 8px;
font-weight: 700;
color: var(--muted);
letter-spacing: 1px;
margin-bottom: 4px;
}
.map-settings-panel .ms-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.map-settings-panel .ms-color-input {
width: 24px;
height: 24px;
border: 1px solid var(--border);
border-radius: 4px;
padding: 0;
cursor: pointer;
background: transparent;
flex-shrink: 0;
}
.map-settings-panel .ms-color-input::-webkit-color-swatch-wrapper {
padding: 1px;
}
.map-settings-panel .ms-color-input::-webkit-color-swatch {
border: none;
border-radius: 2px;
}
.map-settings-panel .ms-hex {
font-size: 9px;
color: var(--muted);
font-family: monospace;
}
.map-settings-panel .ms-depth-label {
font-size: 9px;
color: var(--text);
min-width: 48px;
text-align: right;
}
.map-settings-panel select {
font-size: 10px;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--card);
color: var(--text);
cursor: pointer;
outline: none;
width: 100%;
}
.map-settings-panel select:focus {
border-color: var(--accent);
}
.map-settings-panel .ms-reset {
width: 100%;
font-size: 9px;
padding: 5px 8px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--card);
color: var(--muted);
cursor: pointer;
transition: all 0.15s;
margin-top: 4px;
}
.map-settings-panel .ms-reset:hover {
color: var(--text);
border-color: var(--accent);
}
/* ── Depth Legend ──────────────────────────────────────────────────── */
.depth-legend {
position: absolute;
bottom: 44px;
left: 10px;
z-index: 800;
background: rgba(15, 23, 42, 0.92);
backdrop-filter: blur(8px);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px;
display: flex;
gap: 6px;
align-items: stretch;
}
.depth-legend__bar {
width: 14px;
border-radius: 3px;
min-height: 120px;
}
.depth-legend__ticks {
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 8px;
color: var(--muted);
font-family: monospace;
padding: 1px 0;
}
@media (max-width: 920px) { @media (max-width: 920px) {
.app { .app {
grid-template-columns: 1fr; grid-template-columns: 1fr;

파일 보기

@ -0,0 +1,157 @@
import { useState } from 'react';
import type { MapStyleSettings, MapLabelLanguage, DepthFontSize } from './types';
import { DEFAULT_MAP_STYLE_SETTINGS } from './types';
interface MapSettingsPanelProps {
value: MapStyleSettings;
onChange: (next: MapStyleSettings) => void;
}
const LANGUAGES: { value: MapLabelLanguage; label: string }[] = [
{ value: 'ko', label: '한국어' },
{ value: 'en', label: 'English' },
{ value: 'ja', label: '日本語' },
{ value: 'zh', label: '中文' },
{ value: 'local', label: '현지어' },
];
const FONT_SIZES: { value: DepthFontSize; label: string }[] = [
{ value: 'small', label: '소' },
{ value: 'medium', label: '중' },
{ value: 'large', label: '대' },
];
function depthLabel(depth: number): string {
return `${Math.abs(depth).toLocaleString()}m`;
}
export function MapSettingsPanel({ value, onChange }: MapSettingsPanelProps) {
const [open, setOpen] = useState(false);
const update = <K extends keyof MapStyleSettings>(key: K, val: MapStyleSettings[K]) => {
onChange({ ...value, [key]: val });
};
const updateDepthStop = (index: number, color: string) => {
const next = value.depthStops.map((s, i) => (i === index ? { ...s, color } : s));
update('depthStops', next);
};
return (
<>
<button
className={`map-settings-gear${open ? ' open' : ''}`}
onClick={() => setOpen((p) => !p)}
title="지도 설정"
type="button"
>
</button>
{open && (
<div className="map-settings-panel">
<div className="ms-title"> </div>
{/* ── Language ──────────────────────────────────── */}
<div className="ms-section">
<div className="ms-label"> </div>
<select
value={value.labelLanguage}
onChange={(e) => update('labelLanguage', e.target.value as MapLabelLanguage)}
>
{LANGUAGES.map((l) => (
<option key={l.value} value={l.value}>
{l.label}
</option>
))}
</select>
</div>
{/* ── Land color ────────────────────────────────── */}
<div className="ms-section">
<div className="ms-label"> </div>
<div className="ms-row">
<input
type="color"
className="ms-color-input"
value={value.landColor}
onChange={(e) => update('landColor', e.target.value)}
/>
<span className="ms-hex">{value.landColor}</span>
</div>
</div>
{/* ── Water color ───────────────────────────────── */}
<div className="ms-section">
<div className="ms-label"> </div>
<div className="ms-row">
<input
type="color"
className="ms-color-input"
value={value.waterBaseColor}
onChange={(e) => update('waterBaseColor', e.target.value)}
/>
<span className="ms-hex">{value.waterBaseColor}</span>
</div>
</div>
{/* ── Depth gradient ────────────────────────────── */}
<div className="ms-section">
<div className="ms-label"> </div>
{value.depthStops.map((stop, i) => (
<div className="ms-row" key={stop.depth}>
<span className="ms-depth-label">{depthLabel(stop.depth)}</span>
<input
type="color"
className="ms-color-input"
value={stop.color}
onChange={(e) => updateDepthStop(i, e.target.value)}
/>
<span className="ms-hex">{stop.color}</span>
</div>
))}
</div>
{/* ── Depth font size ───────────────────────────── */}
<div className="ms-section">
<div className="ms-label"> </div>
<div className="tog" style={{ gap: 3 }}>
{FONT_SIZES.map((fs) => (
<div
key={fs.value}
className={`tog-btn${value.depthFontSize === fs.value ? ' on' : ''}`}
onClick={() => update('depthFontSize', fs.value)}
>
{fs.label}
</div>
))}
</div>
</div>
{/* ── Depth font color ──────────────────────────── */}
<div className="ms-section">
<div className="ms-label"> </div>
<div className="ms-row">
<input
type="color"
className="ms-color-input"
value={value.depthFontColor}
onChange={(e) => update('depthFontColor', e.target.value)}
/>
<span className="ms-hex">{value.depthFontColor}</span>
</div>
</div>
{/* ── Reset ─────────────────────────────────────── */}
<button
className="ms-reset"
type="button"
onClick={() => onChange(DEFAULT_MAP_STYLE_SETTINGS)}
>
</button>
</div>
)}
</>
);
}

파일 보기

@ -0,0 +1,32 @@
export type MapLabelLanguage = 'ko' | 'en' | 'ja' | 'zh' | 'local';
export type DepthFontSize = 'small' | 'medium' | 'large';
export interface DepthColorStop {
depth: number;
color: string;
}
export interface MapStyleSettings {
labelLanguage: MapLabelLanguage;
landColor: string;
waterBaseColor: string;
depthStops: DepthColorStop[];
depthFontSize: DepthFontSize;
depthFontColor: string;
}
export const DEFAULT_MAP_STYLE_SETTINGS: MapStyleSettings = {
labelLanguage: 'ko',
landColor: '#1a1a2e',
waterBaseColor: '#14606e',
depthStops: [
{ depth: -8000, color: '#010610' },
{ depth: -4000, color: '#030c1c' },
{ depth: -2000, color: '#041022' },
{ depth: -1000, color: '#051529' },
{ depth: -500, color: '#061a30' },
{ depth: -100, color: '#08263d' },
],
depthFontSize: 'medium',
depthFontColor: '#e2e8f0',
};

파일 보기

@ -24,6 +24,10 @@ import { Topbar } from "../../widgets/topbar/Topbar";
import { VesselInfoPanel } from "../../widgets/info/VesselInfoPanel"; import { VesselInfoPanel } from "../../widgets/info/VesselInfoPanel";
import { SubcableInfoPanel } from "../../widgets/subcableInfo/SubcableInfoPanel"; import { SubcableInfoPanel } from "../../widgets/subcableInfo/SubcableInfoPanel";
import { VesselList } from "../../widgets/vesselList/VesselList"; import { VesselList } from "../../widgets/vesselList/VesselList";
import { MapSettingsPanel } from "../../features/mapSettings/MapSettingsPanel";
import { DepthLegend } from "../../widgets/legend/DepthLegend";
import { DEFAULT_MAP_STYLE_SETTINGS } from "../../features/mapSettings/types";
import type { MapStyleSettings } from "../../features/mapSettings/types";
import { import {
buildLegacyHitMap, buildLegacyHitMap,
computeCountsByType, computeCountsByType,
@ -111,6 +115,7 @@ export function DashboardPage() {
const [baseMap, setBaseMap] = useState<BaseMapId>("enhanced"); const [baseMap, setBaseMap] = useState<BaseMapId>("enhanced");
const [projection, setProjection] = useState<MapProjectionId>("mercator"); const [projection, setProjection] = useState<MapProjectionId>("mercator");
const [mapStyleSettings, setMapStyleSettings] = useState<MapStyleSettings>(DEFAULT_MAP_STYLE_SETTINGS);
const [overlays, setOverlays] = useState<MapToggleState>({ const [overlays, setOverlays] = useState<MapToggleState>({
pairLines: true, pairLines: true,
@ -722,7 +727,10 @@ export function DashboardPage() {
hoveredCableId={hoveredCableId} hoveredCableId={hoveredCableId}
onHoverCable={setHoveredCableId} onHoverCable={setHoveredCableId}
onClickCable={(id) => setSelectedCableId((prev) => (prev === id ? null : id))} onClickCable={(id) => setSelectedCableId((prev) => (prev === id ? null : id))}
mapStyleSettings={mapStyleSettings}
/> />
<MapSettingsPanel value={mapStyleSettings} onChange={setMapStyleSettings} />
<DepthLegend depthStops={mapStyleSettings.depthStops} />
<MapLegend /> <MapLegend />
{selectedLegacyVessel ? ( {selectedLegacyVessel ? (
<VesselInfoPanel vessel={selectedLegacyVessel} allVessels={legacyVesselsAll} onClose={() => setSelectedMmsi(null)} onSelectMmsi={setSelectedMmsi} /> <VesselInfoPanel vessel={selectedLegacyVessel} allVessels={legacyVesselsAll} onClose={() => setSelectedMmsi(null)} onSelectMmsi={setSelectedMmsi} />

파일 보기

@ -0,0 +1,33 @@
import { useMemo } from 'react';
import type { DepthColorStop } from '../../features/mapSettings/types';
interface DepthLegendProps {
depthStops: DepthColorStop[];
}
export function DepthLegend({ depthStops }: DepthLegendProps) {
const sorted = useMemo(
() => [...depthStops].sort((a, b) => a.depth - b.depth),
[depthStops],
);
const gradient = useMemo(() => {
if (sorted.length === 0) return 'transparent';
const stops = sorted.map((s, i) => {
const pct = (i / (sorted.length - 1)) * 100;
return `${s.color} ${pct.toFixed(0)}%`;
});
return `linear-gradient(to bottom, ${stops.join(', ')})`;
}, [sorted]);
return (
<div className="depth-legend">
<div className="depth-legend__bar" style={{ background: gradient }} />
<div className="depth-legend__ticks">
{sorted.map((s) => (
<span key={s.depth}>{Math.abs(s.depth).toLocaleString()}m</span>
))}
</div>
</div>
);
}

파일 보기

@ -26,6 +26,7 @@ import { useGlobeOverlays } from './hooks/useGlobeOverlays';
import { useGlobeInteraction } from './hooks/useGlobeInteraction'; import { useGlobeInteraction } from './hooks/useGlobeInteraction';
import { useDeckLayers } from './hooks/useDeckLayers'; import { useDeckLayers } from './hooks/useDeckLayers';
import { useSubcablesLayer } from './hooks/useSubcablesLayer'; import { useSubcablesLayer } from './hooks/useSubcablesLayer';
import { useMapStyleSettings } from './hooks/useMapStyleSettings';
export type { Map3DSettings, BaseMapId, MapProjectionId } from './types'; export type { Map3DSettings, BaseMapId, MapProjectionId } from './types';
@ -64,6 +65,7 @@ export function Map3D({
hoveredCableId = null, hoveredCableId = null,
onHoverCable, onHoverCable,
onClickCable, onClickCable,
mapStyleSettings,
}: Props) { }: Props) {
void onHoverFleet; void onHoverFleet;
void onClearFleetHover; void onClearFleetHover;
@ -448,6 +450,8 @@ export function Map3D({
{ baseMap, projection, showSeamark: settings.showSeamark, mapSyncEpoch, pulseMapSync }, { baseMap, projection, showSeamark: settings.showSeamark, mapSyncEpoch, pulseMapSync },
); );
useMapStyleSettings(mapRef, mapStyleSettings, { baseMap, mapSyncEpoch });
useZonesLayer( useZonesLayer(
mapRef, projectionBusyRef, reorderGlobeFeatureLayers, mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
{ zones, overlays, projection, baseMap, hoveredZoneId, mapSyncEpoch }, { zones, overlays, projection, baseMap, hoveredZoneId, mapSyncEpoch },

파일 보기

@ -91,7 +91,8 @@ export function useMapInit(
scrollZoom: { around: 'center' }, scrollZoom: { around: 'center' },
}); });
map.addControl(new maplibregl.NavigationControl({ showZoom: true, showCompass: true }), 'top-left'); map.addControl(new maplibregl.NavigationControl({ showZoom: true, showCompass: false }), 'top-left');
map.addControl(new maplibregl.NavigationControl({ showZoom: false, showCompass: true }), 'top-left');
map.addControl(new maplibregl.ScaleControl({ maxWidth: 120, unit: 'metric' }), 'bottom-left'); map.addControl(new maplibregl.ScaleControl({ maxWidth: 120, unit: 'metric' }), 'bottom-left');
mapRef.current = map; mapRef.current = map;

파일 보기

@ -0,0 +1,165 @@
import { useEffect, useRef, type MutableRefObject } from 'react';
import maplibregl from 'maplibre-gl';
import type { MapStyleSettings, MapLabelLanguage, DepthColorStop, DepthFontSize } from '../../../features/mapSettings/types';
import type { BaseMapId } from '../types';
import { kickRepaint, onMapStyleReady } from '../lib/mapCore';
/* ── Depth font size presets ──────────────────────────────────────── */
const DEPTH_FONT_SIZE_MAP: Record<DepthFontSize, unknown[]> = {
small: ['interpolate', ['linear'], ['zoom'], 7, 8, 9, 9, 11, 11, 13, 13],
medium: ['interpolate', ['linear'], ['zoom'], 7, 10, 9, 12, 11, 14, 13, 16],
large: ['interpolate', ['linear'], ['zoom'], 7, 12, 9, 15, 11, 18, 13, 20],
};
/* ── Helpers ──────────────────────────────────────────────────────── */
function darkenHex(hex: string, factor = 0.85): string {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `#${[r, g, b].map((c) => Math.round(c * factor).toString(16).padStart(2, '0')).join('')}`;
}
function lightenHex(hex: string, factor = 1.3): string {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `#${[r, g, b].map((c) => Math.min(255, Math.round(c * factor)).toString(16).padStart(2, '0')).join('')}`;
}
/* ── Apply functions ──────────────────────────────────────────────── */
function applyLabelLanguage(map: maplibregl.Map, lang: MapLabelLanguage) {
const style = map.getStyle();
if (!style?.layers) return;
for (const layer of style.layers) {
if (layer.type !== 'symbol') continue;
const layout = (layer as { layout?: Record<string, unknown> }).layout;
if (!layout?.['text-field']) continue;
if (layer.id === 'bathymetry-labels') continue;
const textField =
lang === 'local'
? ['get', 'name']
: ['coalesce', ['get', `name:${lang}`], ['get', 'name']];
try {
map.setLayoutProperty(layer.id, 'text-field', textField);
} catch {
// ignore
}
}
}
function applyLandColor(map: maplibregl.Map, color: string) {
const style = map.getStyle();
if (!style?.layers) return;
const landRegex = /(land|landcover|landuse|earth|continent|terrain|park)/i;
for (const layer of style.layers) {
if (layer.type !== 'fill') continue;
const id = layer.id;
const sourceLayer = String((layer as Record<string, unknown>)['source-layer'] ?? '');
if (!landRegex.test(id) && !landRegex.test(sourceLayer)) continue;
try {
map.setPaintProperty(id, 'fill-color', color);
} catch {
// ignore
}
}
}
function applyWaterBaseColor(map: maplibregl.Map, fillColor: string) {
const style = map.getStyle();
if (!style?.layers) return;
const waterRegex = /(water|sea|ocean|river|lake|coast|bay)/i;
const lineColor = darkenHex(fillColor, 0.85);
for (const layer of style.layers) {
const id = layer.id;
if (id.startsWith('bathymetry-')) continue;
const sourceLayer = String((layer as Record<string, unknown>)['source-layer'] ?? '');
if (!waterRegex.test(id) && !waterRegex.test(sourceLayer)) continue;
try {
if (layer.type === 'fill') {
map.setPaintProperty(id, 'fill-color', fillColor);
} else if (layer.type === 'line') {
map.setPaintProperty(id, 'line-color', lineColor);
}
} catch {
// ignore
}
}
}
function applyDepthGradient(map: maplibregl.Map, stops: DepthColorStop[]) {
if (!map.getLayer('bathymetry-fill')) return;
const depth = ['to-number', ['get', 'depth']];
const sorted = [...stops].sort((a, b) => a.depth - b.depth);
const expr: unknown[] = ['interpolate', ['linear'], depth];
const deepest = sorted[0];
if (deepest) expr.push(-11000, darkenHex(deepest.color, 0.5));
for (const s of sorted) {
expr.push(s.depth, s.color);
}
const shallowest = sorted[sorted.length - 1];
if (shallowest) expr.push(0, lightenHex(shallowest.color, 1.8));
try {
map.setPaintProperty('bathymetry-fill', 'fill-color', expr as never);
} catch {
// ignore
}
}
function applyDepthFontSize(map: maplibregl.Map, size: DepthFontSize) {
const expr = DEPTH_FONT_SIZE_MAP[size];
for (const layerId of ['bathymetry-labels', 'bathymetry-landforms']) {
if (!map.getLayer(layerId)) continue;
try {
map.setLayoutProperty(layerId, 'text-size', expr);
} catch {
// ignore
}
}
}
function applyDepthFontColor(map: maplibregl.Map, color: string) {
for (const layerId of ['bathymetry-labels', 'bathymetry-landforms']) {
if (!map.getLayer(layerId)) continue;
try {
map.setPaintProperty(layerId, 'text-color', color);
} catch {
// ignore
}
}
}
/* ── Hook ──────────────────────────────────────────────────────────── */
export function useMapStyleSettings(
mapRef: MutableRefObject<maplibregl.Map | null>,
settings: MapStyleSettings | undefined,
opts: { baseMap: BaseMapId; mapSyncEpoch: number },
) {
const settingsRef = useRef(settings);
useEffect(() => {
settingsRef.current = settings;
});
const { baseMap, mapSyncEpoch } = opts;
useEffect(() => {
const map = mapRef.current;
const s = settingsRef.current;
if (!map || !s) return;
const stop = onMapStyleReady(map, () => {
applyLabelLanguage(map, s.labelLanguage);
applyLandColor(map, s.landColor);
applyWaterBaseColor(map, s.waterBaseColor);
if (baseMap === 'enhanced') {
applyDepthGradient(map, s.depthStops);
applyDepthFontSize(map, s.depthFontSize);
applyDepthFontColor(map, s.depthFontColor);
}
kickRepaint(map);
});
return () => stop();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings, baseMap, mapSyncEpoch]);
}

파일 보기

@ -6,6 +6,9 @@ import maplibregl, {
import type { BaseMapId, BathyZoomRange, MapProjectionId } from '../types'; import type { BaseMapId, BathyZoomRange, MapProjectionId } from '../types';
import { getLayerId, getMapTilerKey } from '../lib/mapCore'; import { getLayerId, getMapTilerKey } from '../lib/mapCore';
export const SHALLOW_WATER_FILL_DEFAULT = '#14606e';
export const SHALLOW_WATER_LINE_DEFAULT = '#114f5c';
const BATHY_ZOOM_RANGES: BathyZoomRange[] = [ const BATHY_ZOOM_RANGES: BathyZoomRange[] = [
{ id: 'bathymetry-fill', mercator: [6, 24], globe: [8, 24] }, { id: 'bathymetry-fill', mercator: [6, 24], globe: [8, 24] },
{ id: 'bathymetry-borders', mercator: [6, 24], globe: [8, 24] }, { id: 'bathymetry-borders', mercator: [6, 24], globe: [8, 24] },
@ -209,8 +212,8 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK
// Brighten base-map water fills so shallow coasts, rivers & lakes blend naturally // Brighten base-map water fills so shallow coasts, rivers & lakes blend naturally
// with the bathymetry gradient instead of appearing as near-black voids. // with the bathymetry gradient instead of appearing as near-black voids.
const waterRegex = /(water|sea|ocean|river|lake|coast|bay)/i; const waterRegex = /(water|sea|ocean|river|lake|coast|bay)/i;
const SHALLOW_WATER_FILL = '#14606e'; const SHALLOW_WATER_FILL = SHALLOW_WATER_FILL_DEFAULT;
const SHALLOW_WATER_LINE = '#114f5c'; const SHALLOW_WATER_LINE = SHALLOW_WATER_LINE_DEFAULT;
for (const layer of layers) { for (const layer of layers) {
const id = getLayerId(layer); const id = getLayerId(layer);
if (!id) continue; if (!id) continue;

파일 보기

@ -4,6 +4,7 @@ import type { SubcableGeoJson } from '../../entities/subcable/model/types';
import type { ZonesGeoJson } from '../../entities/zone/api/useZones'; import type { ZonesGeoJson } from '../../entities/zone/api/useZones';
import type { MapToggleState } from '../../features/mapToggles/MapToggles'; import type { MapToggleState } from '../../features/mapToggles/MapToggles';
import type { FcLink, FleetCircle, PairLink } from '../../features/legacyDashboard/model/types'; import type { FcLink, FleetCircle, PairLink } from '../../features/legacyDashboard/model/types';
import type { MapStyleSettings } from '../../features/mapSettings/types';
export type Map3DSettings = { export type Map3DSettings = {
showSeamark: boolean; showSeamark: boolean;
@ -50,6 +51,7 @@ export interface Map3DProps {
hoveredCableId?: string | null; hoveredCableId?: string | null;
onHoverCable?: (cableId: string | null) => void; onHoverCable?: (cableId: string | null) => void;
onClickCable?: (cableId: string | null) => void; onClickCable?: (cableId: string | null) => void;
mapStyleSettings?: MapStyleSettings;
} }
export type DashSeg = { export type DashSeg = {