2026-02-16 23:55:58 +09:00
|
|
|
import { useCallback, useMemo } from "react";
|
2026-02-16 08:44:25 +09:00
|
|
|
import { useAuth } from "../../shared/auth";
|
2026-02-17 06:22:49 +09:00
|
|
|
import { useTheme } from "../../shared/hooks";
|
2026-02-15 11:22:38 +09:00
|
|
|
import { useAisTargetPolling } from "../../features/aisPolling/useAisTargetPolling";
|
2026-02-16 23:55:58 +09:00
|
|
|
import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types";
|
|
|
|
|
import { LEGACY_ALARM_KINDS } from "../../features/legacyDashboard/model/types";
|
2026-02-15 11:22:38 +09:00
|
|
|
import { useLegacyVessels } from "../../entities/legacyVessel/api/useLegacyVessels";
|
|
|
|
|
import { buildLegacyVesselIndex } from "../../entities/legacyVessel/lib";
|
|
|
|
|
import type { LegacyVesselDataset } from "../../entities/legacyVessel/model/types";
|
2026-02-16 23:55:58 +09:00
|
|
|
import type { VesselTypeCode } from "../../entities/vessel/model/types";
|
|
|
|
|
import { VESSEL_TYPE_ORDER } from "../../entities/vessel/model/meta";
|
2026-02-15 11:22:38 +09:00
|
|
|
import { useZones } from "../../entities/zone/api/useZones";
|
2026-02-16 02:11:37 +09:00
|
|
|
import { useSubcables } from "../../entities/subcable/api/useSubcables";
|
2026-02-15 11:22:38 +09:00
|
|
|
import { AisInfoPanel } from "../../widgets/aisInfo/AisInfoPanel";
|
|
|
|
|
import { MapLegend } from "../../widgets/legend/MapLegend";
|
2026-02-16 23:55:58 +09:00
|
|
|
import { Map3D } from "../../widgets/map3d/Map3D";
|
2026-02-15 11:22:38 +09:00
|
|
|
import { Topbar } from "../../widgets/topbar/Topbar";
|
|
|
|
|
import { VesselInfoPanel } from "../../widgets/info/VesselInfoPanel";
|
2026-02-16 02:11:37 +09:00
|
|
|
import { SubcableInfoPanel } from "../../widgets/subcableInfo/SubcableInfoPanel";
|
2026-02-16 06:17:20 +09:00
|
|
|
import { DepthLegend } from "../../widgets/legend/DepthLegend";
|
2026-02-16 22:43:08 +09:00
|
|
|
import { queryTrackByMmsi } from "../../features/trackReplay/services/trackQueryService";
|
|
|
|
|
import { useTrackQueryStore } from "../../features/trackReplay/stores/trackQueryStore";
|
|
|
|
|
import { GlobalTrackReplayPanel } from "../../widgets/trackReplay/GlobalTrackReplayPanel";
|
|
|
|
|
import { useWeatherPolling } from "../../features/weatherOverlay/useWeatherPolling";
|
|
|
|
|
import { useWeatherOverlay } from "../../features/weatherOverlay/useWeatherOverlay";
|
|
|
|
|
import { WeatherPanel } from "../../widgets/weatherPanel/WeatherPanel";
|
|
|
|
|
import { WeatherOverlayPanel } from "../../widgets/weatherOverlay/WeatherOverlayPanel";
|
2026-02-16 23:55:58 +09:00
|
|
|
import { MapSettingsPanel } from "../../features/mapSettings/MapSettingsPanel";
|
2026-02-15 11:22:38 +09:00
|
|
|
import {
|
|
|
|
|
buildLegacyHitMap,
|
|
|
|
|
computeCountsByType,
|
|
|
|
|
computeFcLinks,
|
|
|
|
|
computeFleetCircles,
|
|
|
|
|
computeLegacyAlarms,
|
|
|
|
|
computePairLinks,
|
|
|
|
|
deriveLegacyVessels,
|
|
|
|
|
filterByShipCodes,
|
|
|
|
|
} from "../../features/legacyDashboard/model/derive";
|
2026-02-16 23:55:58 +09:00
|
|
|
import { useDashboardState } from "./useDashboardState";
|
|
|
|
|
import type { Bbox } from "./useDashboardState";
|
|
|
|
|
import { DashboardSidebar } from "./DashboardSidebar";
|
2026-02-15 11:22:38 +09:00
|
|
|
|
2026-02-15 13:58:07 +09:00
|
|
|
const AIS_CENTER = {
|
|
|
|
|
lon: 126.95,
|
|
|
|
|
lat: 35.95,
|
|
|
|
|
radiusMeters: 2_000_000,
|
|
|
|
|
};
|
2026-02-15 11:22:38 +09:00
|
|
|
|
|
|
|
|
function inBbox(lon: number, lat: number, bbox: Bbox) {
|
|
|
|
|
const [lonMin, latMin, lonMax, latMax] = bbox;
|
|
|
|
|
if (lat < latMin || lat > latMax) return false;
|
|
|
|
|
if (lonMin <= lonMax) return lon >= lonMin && lon <= lonMax;
|
|
|
|
|
return lon >= lonMin || lon <= lonMax;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 23:55:58 +09:00
|
|
|
function useLegacyIndex(data: LegacyVesselDataset | null) {
|
2026-02-15 11:22:38 +09:00
|
|
|
return useMemo(() => (data ? buildLegacyVesselIndex(data.vessels) : null), [data]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function DashboardPage() {
|
2026-02-16 08:44:25 +09:00
|
|
|
const { user, logout } = useAuth();
|
2026-02-17 06:22:49 +09:00
|
|
|
const { theme, toggleTheme } = useTheme();
|
2026-02-16 23:55:58 +09:00
|
|
|
const uid = user?.id ?? null;
|
|
|
|
|
|
|
|
|
|
// ── Data fetching ──
|
2026-02-15 11:22:38 +09:00
|
|
|
const { data: zones, error: zonesError } = useZones();
|
|
|
|
|
const { data: legacyData, error: legacyError } = useLegacyVessels();
|
2026-02-16 02:11:37 +09:00
|
|
|
const { data: subcableData } = useSubcables();
|
2026-02-15 11:22:38 +09:00
|
|
|
const legacyIndex = useLegacyIndex(legacyData);
|
2026-02-16 22:43:08 +09:00
|
|
|
|
2026-02-16 23:55:58 +09:00
|
|
|
// ── UI state ──
|
|
|
|
|
const state = useDashboardState(uid);
|
|
|
|
|
const {
|
|
|
|
|
mapInstance, handleMapReady,
|
|
|
|
|
viewBbox, setViewBbox,
|
|
|
|
|
useViewportFilter,
|
|
|
|
|
useApiBbox, apiBbox,
|
|
|
|
|
selectedMmsi, setSelectedMmsi,
|
|
|
|
|
highlightedMmsiSet,
|
|
|
|
|
hoveredMmsiSet, setHoveredMmsiSet,
|
|
|
|
|
hoveredFleetMmsiSet, setHoveredFleetMmsiSet,
|
|
|
|
|
hoveredPairMmsiSet, setHoveredPairMmsiSet,
|
|
|
|
|
hoveredFleetOwnerKey, setHoveredFleetOwnerKey,
|
|
|
|
|
typeEnabled,
|
|
|
|
|
showTargets, showOthers,
|
|
|
|
|
baseMap, projection,
|
|
|
|
|
mapStyleSettings, setMapStyleSettings,
|
|
|
|
|
overlays, settings,
|
|
|
|
|
mapView, setMapView,
|
|
|
|
|
fleetFocus, setFleetFocus,
|
|
|
|
|
hoveredCableId, setHoveredCableId,
|
|
|
|
|
selectedCableId, setSelectedCableId,
|
|
|
|
|
trackContextMenu, handleOpenTrackMenu, handleCloseTrackMenu,
|
|
|
|
|
handleProjectionLoadingChange,
|
|
|
|
|
setIsGlobeShipsReady,
|
|
|
|
|
showMapLoader,
|
|
|
|
|
clock, adminMode, onLogoClick,
|
|
|
|
|
setUniqueSorted, setSortedIfChanged, toggleHighlightedMmsi,
|
|
|
|
|
alarmKindEnabled,
|
|
|
|
|
} = state;
|
|
|
|
|
|
|
|
|
|
// ── Weather ──
|
2026-02-16 21:45:07 +09:00
|
|
|
const weather = useWeatherPolling(zones);
|
|
|
|
|
const weatherOverlay = useWeatherOverlay(mapInstance);
|
2026-02-15 11:22:38 +09:00
|
|
|
|
2026-02-16 23:55:58 +09:00
|
|
|
// ── AIS polling ──
|
2026-02-15 11:22:38 +09:00
|
|
|
const { targets, snapshot } = useAisTargetPolling({
|
2026-02-16 22:12:48 +09:00
|
|
|
chnprmshipMinutes: 120,
|
2026-02-15 11:22:38 +09:00
|
|
|
incrementalMinutes: 2,
|
|
|
|
|
intervalMs: 60_000,
|
2026-02-16 22:12:48 +09:00
|
|
|
retentionMinutes: 120,
|
2026-02-15 11:22:38 +09:00
|
|
|
bbox: useApiBbox ? apiBbox : undefined,
|
2026-02-15 13:58:07 +09:00
|
|
|
centerLon: useApiBbox ? undefined : AIS_CENTER.lon,
|
|
|
|
|
centerLat: useApiBbox ? undefined : AIS_CENTER.lat,
|
|
|
|
|
radiusMeters: useApiBbox ? undefined : AIS_CENTER.radiusMeters,
|
2026-02-15 11:22:38 +09:00
|
|
|
});
|
|
|
|
|
|
2026-02-16 23:55:58 +09:00
|
|
|
// ── Track request ──
|
2026-02-16 22:12:48 +09:00
|
|
|
const handleRequestTrack = useCallback(async (mmsi: number, minutes: number) => {
|
2026-02-16 22:43:08 +09:00
|
|
|
const trackStore = useTrackQueryStore.getState();
|
|
|
|
|
const queryKey = `${mmsi}:${minutes}:${Date.now()}`;
|
|
|
|
|
trackStore.beginQuery(queryKey);
|
|
|
|
|
|
2026-02-16 22:12:48 +09:00
|
|
|
try {
|
2026-02-16 22:43:08 +09:00
|
|
|
const target = targets.find((item) => item.mmsi === mmsi);
|
|
|
|
|
const tracks = await queryTrackByMmsi({
|
|
|
|
|
mmsi,
|
|
|
|
|
minutes,
|
|
|
|
|
shipNameHint: target?.name,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (tracks.length > 0) {
|
|
|
|
|
trackStore.applyTracksSuccess(tracks, queryKey);
|
2026-02-16 22:12:48 +09:00
|
|
|
} else {
|
2026-02-16 22:43:08 +09:00
|
|
|
trackStore.applyQueryError('항적 데이터가 없습니다.', queryKey);
|
2026-02-16 22:12:48 +09:00
|
|
|
}
|
|
|
|
|
} catch (e) {
|
2026-02-16 22:43:08 +09:00
|
|
|
trackStore.applyQueryError(e instanceof Error ? e.message : '항적 조회에 실패했습니다.', queryKey);
|
2026-02-16 22:12:48 +09:00
|
|
|
}
|
2026-02-16 22:43:08 +09:00
|
|
|
}, [targets]);
|
2026-02-16 22:12:48 +09:00
|
|
|
|
2026-02-16 23:55:58 +09:00
|
|
|
// ── Derived data ──
|
2026-02-15 11:22:38 +09:00
|
|
|
const targetsInScope = useMemo(() => {
|
|
|
|
|
if (!useViewportFilter || !viewBbox) return targets;
|
|
|
|
|
return targets.filter((t) => typeof t.lon === "number" && typeof t.lat === "number" && inBbox(t.lon, t.lat, viewBbox));
|
|
|
|
|
}, [targets, useViewportFilter, viewBbox]);
|
|
|
|
|
|
|
|
|
|
const legacyHits = useMemo(() => buildLegacyHitMap(targetsInScope, legacyIndex), [targetsInScope, legacyIndex]);
|
|
|
|
|
const legacyVesselsAll = useMemo(() => deriveLegacyVessels({ targets: targetsInScope, legacyHits, zones }), [targetsInScope, legacyHits, zones]);
|
|
|
|
|
const legacyCounts = useMemo(() => computeCountsByType(legacyVesselsAll), [legacyVesselsAll]);
|
|
|
|
|
|
|
|
|
|
const legacyVesselsFiltered = useMemo(() => {
|
|
|
|
|
if (!showTargets) return [];
|
|
|
|
|
return filterByShipCodes(legacyVesselsAll, typeEnabled);
|
|
|
|
|
}, [legacyVesselsAll, showTargets, typeEnabled]);
|
|
|
|
|
|
|
|
|
|
const legacyMmsiForMap = useMemo(() => new Set(legacyVesselsFiltered.map((v) => v.mmsi)), [legacyVesselsFiltered]);
|
|
|
|
|
|
|
|
|
|
const targetsForMap = useMemo(() => {
|
|
|
|
|
const out = [];
|
|
|
|
|
for (const t of targetsInScope) {
|
|
|
|
|
const mmsi = t.mmsi;
|
|
|
|
|
if (typeof mmsi !== "number") continue;
|
|
|
|
|
const isLegacy = legacyHits.has(mmsi);
|
|
|
|
|
if (isLegacy) {
|
|
|
|
|
if (!showTargets) continue;
|
|
|
|
|
if (!legacyMmsiForMap.has(mmsi)) continue;
|
|
|
|
|
out.push(t);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (showOthers) out.push(t);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}, [targetsInScope, legacyHits, showTargets, showOthers, legacyMmsiForMap]);
|
|
|
|
|
|
|
|
|
|
const pairLinksAll = useMemo(() => computePairLinks(legacyVesselsAll), [legacyVesselsAll]);
|
|
|
|
|
const fcLinksAll = useMemo(() => computeFcLinks(legacyVesselsAll), [legacyVesselsAll]);
|
|
|
|
|
const alarms = useMemo(() => computeLegacyAlarms({ vessels: legacyVesselsAll, pairLinks: pairLinksAll, fcLinks: fcLinksAll }), [legacyVesselsAll, pairLinksAll, fcLinksAll]);
|
|
|
|
|
|
2026-02-15 18:42:49 +09:00
|
|
|
const alarmKindCounts = useMemo(() => {
|
2026-02-16 23:55:58 +09:00
|
|
|
const base = Object.fromEntries(LEGACY_ALARM_KINDS.map((k) => [k, 0])) as Record<typeof LEGACY_ALARM_KINDS[number], number>;
|
2026-02-15 18:42:49 +09:00
|
|
|
for (const a of alarms) {
|
|
|
|
|
base[a.kind] = (base[a.kind] ?? 0) + 1;
|
|
|
|
|
}
|
|
|
|
|
return base;
|
|
|
|
|
}, [alarms]);
|
|
|
|
|
|
2026-02-16 23:55:58 +09:00
|
|
|
const enabledAlarmKinds = useMemo(() => LEGACY_ALARM_KINDS.filter((k) => alarmKindEnabled[k]), [alarmKindEnabled]);
|
2026-02-15 18:42:49 +09:00
|
|
|
const allAlarmKindsEnabled = enabledAlarmKinds.length === LEGACY_ALARM_KINDS.length;
|
|
|
|
|
|
|
|
|
|
const filteredAlarms = useMemo(() => {
|
|
|
|
|
if (allAlarmKindsEnabled) return alarms;
|
|
|
|
|
const enabled = new Set(enabledAlarmKinds);
|
|
|
|
|
return alarms.filter((a) => enabled.has(a.kind));
|
|
|
|
|
}, [alarms, enabledAlarmKinds, allAlarmKindsEnabled]);
|
|
|
|
|
|
2026-02-15 11:22:38 +09:00
|
|
|
const pairLinksForMap = useMemo(() => computePairLinks(legacyVesselsFiltered), [legacyVesselsFiltered]);
|
|
|
|
|
const fcLinksForMap = useMemo(() => computeFcLinks(legacyVesselsFiltered), [legacyVesselsFiltered]);
|
|
|
|
|
const fleetCirclesForMap = useMemo(() => computeFleetCircles(legacyVesselsFiltered), [legacyVesselsFiltered]);
|
|
|
|
|
|
|
|
|
|
const selectedLegacyVessel = useMemo(() => {
|
|
|
|
|
if (!selectedMmsi) return null;
|
|
|
|
|
return legacyVesselsAll.find((v) => v.mmsi === selectedMmsi) ?? null;
|
|
|
|
|
}, [legacyVesselsAll, selectedMmsi]);
|
|
|
|
|
|
|
|
|
|
const selectedTarget = useMemo(() => {
|
|
|
|
|
if (!selectedMmsi) return null;
|
|
|
|
|
return targetsInScope.find((t) => t.mmsi === selectedMmsi) ?? null;
|
|
|
|
|
}, [targetsInScope, selectedMmsi]);
|
|
|
|
|
|
|
|
|
|
const selectedLegacyInfo = useMemo(() => {
|
|
|
|
|
if (!selectedMmsi) return null;
|
|
|
|
|
return legacyHits.get(selectedMmsi) ?? null;
|
|
|
|
|
}, [selectedMmsi, legacyHits]);
|
|
|
|
|
|
2026-02-15 15:17:48 +09:00
|
|
|
const availableTargetMmsiSet = useMemo(
|
|
|
|
|
() => new Set(targetsInScope.map((t) => t.mmsi).filter((mmsi) => Number.isFinite(mmsi))),
|
|
|
|
|
[targetsInScope],
|
|
|
|
|
);
|
|
|
|
|
const activeHighlightedMmsiSet = useMemo(
|
|
|
|
|
() => highlightedMmsiSet.filter((mmsi) => availableTargetMmsiSet.has(mmsi)),
|
|
|
|
|
[highlightedMmsiSet, availableTargetMmsiSet],
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-16 23:55:58 +09:00
|
|
|
const fishingCount = legacyVesselsAll.filter((v) => v.state.isFishing).length;
|
|
|
|
|
const transitCount = legacyVesselsAll.filter((v) => v.state.isTransit).length;
|
2026-02-15 15:17:48 +09:00
|
|
|
|
2026-02-16 23:55:58 +09:00
|
|
|
const enabledTypes = useMemo(() => VESSEL_TYPE_ORDER.filter((c) => typeEnabled[c]), [typeEnabled]);
|
|
|
|
|
const speedPanelType = (selectedLegacyVessel?.shipCode ?? (enabledTypes.length === 1 ? enabledTypes[0] : "PT")) as VesselTypeCode;
|
|
|
|
|
const alarmFilterSummary = allAlarmKindsEnabled ? "전체" : `${enabledAlarmKinds.length}/${LEGACY_ALARM_KINDS.length}`;
|
2026-02-15 15:17:48 +09:00
|
|
|
|
|
|
|
|
const handleFleetContextMenu = (ownerKey: string, mmsis: number[]) => {
|
|
|
|
|
if (!mmsis.length) return;
|
|
|
|
|
const members = mmsis
|
|
|
|
|
.map((mmsi) => legacyVesselsFiltered.find((v): v is DerivedLegacyVessel => v.mmsi === mmsi))
|
|
|
|
|
.filter(
|
|
|
|
|
(v): v is DerivedLegacyVessel & { lat: number; lon: number } =>
|
|
|
|
|
v != null && typeof v.lat === "number" && typeof v.lon === "number" && Number.isFinite(v.lat) && Number.isFinite(v.lon),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (members.length === 0) return;
|
|
|
|
|
const sumLon = members.reduce((acc, v) => acc + v.lon, 0);
|
|
|
|
|
const sumLat = members.reduce((acc, v) => acc + v.lat, 0);
|
|
|
|
|
const center: [number, number] = [sumLon / members.length, sumLat / members.length];
|
2026-02-16 23:55:58 +09:00
|
|
|
setFleetFocus({ id: `${ownerKey}-${Date.now()}`, center, zoom: 9 });
|
2026-02-15 15:17:48 +09:00
|
|
|
};
|
|
|
|
|
|
2026-02-16 23:55:58 +09:00
|
|
|
// ── Render ──
|
2026-02-15 11:22:38 +09:00
|
|
|
return (
|
2026-02-17 06:15:32 +09:00
|
|
|
<div className="grid h-screen grid-cols-[310px_1fr] grid-rows-[44px_1fr] max-md:grid-cols-[1fr]">
|
2026-02-15 11:22:38 +09:00
|
|
|
<Topbar
|
|
|
|
|
total={legacyVesselsAll.length}
|
|
|
|
|
fishing={fishingCount}
|
|
|
|
|
transit={transitCount}
|
|
|
|
|
pairLinks={pairLinksAll.length}
|
|
|
|
|
alarms={alarms.length}
|
|
|
|
|
pollingStatus={snapshot.status}
|
|
|
|
|
lastFetchMinutes={snapshot.lastFetchMinutes}
|
|
|
|
|
clock={clock}
|
|
|
|
|
adminMode={adminMode}
|
|
|
|
|
onLogoClick={onLogoClick}
|
2026-02-16 08:44:25 +09:00
|
|
|
userName={user?.name}
|
|
|
|
|
onLogout={logout}
|
2026-02-17 06:22:49 +09:00
|
|
|
theme={theme}
|
|
|
|
|
onToggleTheme={toggleTheme}
|
2026-02-15 11:22:38 +09:00
|
|
|
/>
|
|
|
|
|
|
2026-02-16 23:55:58 +09:00
|
|
|
<DashboardSidebar
|
|
|
|
|
state={state}
|
|
|
|
|
legacyVesselsAll={legacyVesselsAll}
|
|
|
|
|
legacyVesselsFiltered={legacyVesselsFiltered}
|
|
|
|
|
legacyCounts={legacyCounts}
|
|
|
|
|
selectedLegacyVessel={selectedLegacyVessel}
|
|
|
|
|
activeHighlightedMmsiSet={activeHighlightedMmsiSet}
|
|
|
|
|
legacyHits={legacyHits}
|
|
|
|
|
filteredAlarms={filteredAlarms}
|
|
|
|
|
alarms={alarms}
|
|
|
|
|
alarmKindCounts={alarmKindCounts}
|
|
|
|
|
allAlarmKindsEnabled={allAlarmKindsEnabled}
|
|
|
|
|
alarmFilterSummary={alarmFilterSummary}
|
|
|
|
|
speedPanelType={speedPanelType}
|
|
|
|
|
onFleetContextMenu={handleFleetContextMenu}
|
|
|
|
|
snapshot={snapshot}
|
|
|
|
|
legacyError={legacyError}
|
|
|
|
|
legacyData={legacyData}
|
|
|
|
|
targetsInScope={targetsInScope}
|
|
|
|
|
zonesError={zonesError}
|
|
|
|
|
zones={zones}
|
|
|
|
|
legacyIndex={legacyIndex}
|
|
|
|
|
/>
|
2026-02-15 11:22:38 +09:00
|
|
|
|
2026-02-17 06:15:32 +09:00
|
|
|
<div className="relative bg-[#010610]">
|
2026-02-16 22:12:48 +09:00
|
|
|
{showMapLoader ? (
|
2026-02-15 14:42:07 +09:00
|
|
|
<div className="map-loader-overlay" role="status" aria-live="polite">
|
|
|
|
|
<div className="map-loader-overlay__panel">
|
|
|
|
|
<div className="map-loader-overlay__spinner" />
|
|
|
|
|
<div className="map-loader-overlay__text">지도 모드 동기화 중...</div>
|
|
|
|
|
<div className="map-loader-overlay__bar">
|
|
|
|
|
<div className="map-loader-overlay__fill" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
2026-02-15 11:22:38 +09:00
|
|
|
<Map3D
|
|
|
|
|
targets={targetsForMap}
|
|
|
|
|
zones={zones}
|
|
|
|
|
selectedMmsi={selectedMmsi}
|
2026-02-15 15:17:48 +09:00
|
|
|
highlightedMmsiSet={activeHighlightedMmsiSet}
|
|
|
|
|
hoveredMmsiSet={hoveredMmsiSet}
|
|
|
|
|
hoveredFleetMmsiSet={hoveredFleetMmsiSet}
|
|
|
|
|
hoveredPairMmsiSet={hoveredPairMmsiSet}
|
|
|
|
|
hoveredFleetOwnerKey={hoveredFleetOwnerKey}
|
2026-02-15 11:22:38 +09:00
|
|
|
settings={settings}
|
|
|
|
|
baseMap={baseMap}
|
|
|
|
|
projection={projection}
|
|
|
|
|
overlays={overlays}
|
|
|
|
|
onSelectMmsi={setSelectedMmsi}
|
2026-02-15 15:17:48 +09:00
|
|
|
onToggleHighlightMmsi={toggleHighlightedMmsi}
|
2026-02-15 11:22:38 +09:00
|
|
|
onViewBboxChange={setViewBbox}
|
|
|
|
|
legacyHits={legacyHits}
|
|
|
|
|
pairLinks={pairLinksForMap}
|
|
|
|
|
fcLinks={fcLinksForMap}
|
|
|
|
|
fleetCircles={fleetCirclesForMap}
|
2026-02-15 15:17:48 +09:00
|
|
|
fleetFocus={fleetFocus}
|
2026-02-16 22:12:48 +09:00
|
|
|
onProjectionLoadingChange={handleProjectionLoadingChange}
|
|
|
|
|
onGlobeShipsReady={setIsGlobeShipsReady}
|
2026-02-15 16:09:21 +09:00
|
|
|
onHoverMmsi={(mmsis) => setHoveredMmsiSet(setUniqueSorted(mmsis))}
|
|
|
|
|
onClearMmsiHover={() => setHoveredMmsiSet((prev) => (prev.length === 0 ? prev : []))}
|
|
|
|
|
onHoverPair={(pairMmsis) => setHoveredPairMmsiSet(setUniqueSorted(pairMmsis))}
|
|
|
|
|
onClearPairHover={() => setHoveredPairMmsiSet([])}
|
2026-02-15 15:17:48 +09:00
|
|
|
onHoverFleet={(ownerKey, fleetMmsis) => {
|
|
|
|
|
setHoveredFleetOwnerKey(ownerKey);
|
|
|
|
|
setHoveredFleetMmsiSet(setSortedIfChanged(fleetMmsis));
|
|
|
|
|
}}
|
|
|
|
|
onClearFleetHover={() => {
|
|
|
|
|
setHoveredFleetOwnerKey(null);
|
|
|
|
|
setHoveredFleetMmsiSet((prev) => (prev.length === 0 ? prev : []));
|
|
|
|
|
}}
|
2026-02-16 02:11:37 +09:00
|
|
|
subcableGeo={subcableData?.geo ?? null}
|
|
|
|
|
hoveredCableId={hoveredCableId}
|
|
|
|
|
onHoverCable={setHoveredCableId}
|
|
|
|
|
onClickCable={(id) => setSelectedCableId((prev) => (prev === id ? null : id))}
|
2026-02-16 06:17:20 +09:00
|
|
|
mapStyleSettings={mapStyleSettings}
|
2026-02-16 22:12:48 +09:00
|
|
|
initialView={mapView}
|
|
|
|
|
onViewStateChange={setMapView}
|
2026-02-16 22:43:08 +09:00
|
|
|
activeTrack={null}
|
2026-02-16 22:12:48 +09:00
|
|
|
trackContextMenu={trackContextMenu}
|
|
|
|
|
onRequestTrack={handleRequestTrack}
|
|
|
|
|
onCloseTrackMenu={handleCloseTrackMenu}
|
|
|
|
|
onOpenTrackMenu={handleOpenTrackMenu}
|
2026-02-16 22:43:08 +09:00
|
|
|
onMapReady={handleMapReady}
|
2026-02-15 11:22:38 +09:00
|
|
|
/>
|
2026-02-16 22:43:08 +09:00
|
|
|
<GlobalTrackReplayPanel />
|
2026-02-16 21:45:07 +09:00
|
|
|
<WeatherPanel
|
|
|
|
|
snapshot={weather.snapshot}
|
|
|
|
|
isLoading={weather.isLoading}
|
|
|
|
|
error={weather.error}
|
|
|
|
|
onRefresh={weather.refresh}
|
|
|
|
|
/>
|
|
|
|
|
<WeatherOverlayPanel {...weatherOverlay} />
|
2026-02-16 22:43:08 +09:00
|
|
|
<MapSettingsPanel value={mapStyleSettings} onChange={setMapStyleSettings} />
|
2026-02-16 06:17:20 +09:00
|
|
|
<DepthLegend depthStops={mapStyleSettings.depthStops} />
|
2026-02-15 11:22:38 +09:00
|
|
|
<MapLegend />
|
|
|
|
|
{selectedLegacyVessel ? (
|
|
|
|
|
<VesselInfoPanel vessel={selectedLegacyVessel} allVessels={legacyVesselsAll} onClose={() => setSelectedMmsi(null)} onSelectMmsi={setSelectedMmsi} />
|
|
|
|
|
) : selectedTarget ? (
|
|
|
|
|
<AisInfoPanel target={selectedTarget} legacy={selectedLegacyInfo} onClose={() => setSelectedMmsi(null)} />
|
|
|
|
|
) : null}
|
2026-02-16 02:11:37 +09:00
|
|
|
{selectedCableId && subcableData?.details.get(selectedCableId) ? (
|
|
|
|
|
<SubcableInfoPanel
|
|
|
|
|
detail={subcableData.details.get(selectedCableId)!}
|
|
|
|
|
color={subcableData.geo.features.find((f) => f.properties.id === selectedCableId)?.properties.color}
|
|
|
|
|
onClose={() => setSelectedCableId(null)}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
2026-02-15 11:22:38 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|