feat(map3d): stabilize globe overlays and hover-highlight sync

This commit is contained in:
htlee 2026-02-15 16:09:21 +09:00
부모 b944887430
커밋 05b0c6b881
5개의 변경된 파일516개의 추가작업 그리고 163개의 파일을 삭제

파일 보기

@ -188,12 +188,17 @@ export function computeFleetCircles(vessels: DerivedLegacyVessel[]): FleetCircle
const out: FleetCircle[] = [];
for (const [ownerKey, vs] of groups.entries()) {
if (vs.length < 3) continue;
const ownerLabel =
vs.find((v) => v.ownerCn)?.ownerCn ??
vs.find((v) => v.ownerRoman)?.ownerRoman ??
ownerKey;
const lon = vs.reduce((sum, v) => sum + v.lon, 0) / vs.length;
const lat = vs.reduce((sum, v) => sum + v.lat, 0) / vs.length;
let radiusNm = 0;
for (const v of vs) radiusNm = Math.max(radiusNm, haversineNm(lat, lon, v.lat, v.lon));
out.push({
ownerKey,
ownerLabel,
center: [lon, lat],
radiusNm: Math.max(0.2, radiusNm),
count: vs.length,

파일 보기

@ -56,6 +56,7 @@ export type FcLink = {
export type FleetCircle = {
ownerKey: string;
ownerLabel: string;
center: [number, number];
radiusNm: number;
count: number;
@ -71,4 +72,3 @@ export type LegacyAlarm = {
text: string;
relatedMmsi: number[];
};

파일 보기

@ -610,6 +610,10 @@ export function DashboardPage() {
fleetCircles={fleetCirclesForMap}
fleetFocus={fleetFocus}
onProjectionLoadingChange={setIsProjectionLoading}
onHoverMmsi={(mmsis) => setHoveredMmsiSet(setUniqueSorted(mmsis))}
onClearMmsiHover={() => setHoveredMmsiSet((prev) => (prev.length === 0 ? prev : []))}
onHoverPair={(pairMmsis) => setHoveredPairMmsiSet(setUniqueSorted(pairMmsis))}
onClearPairHover={() => setHoveredPairMmsiSet([])}
onHoverFleet={(ownerKey, fleetMmsis) => {
setHoveredFleetOwnerKey(ownerKey);
setHoveredFleetMmsiSet(setSortedIfChanged(fleetMmsis));

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다. Load Diff

파일 보기

@ -1,17 +1,38 @@
import { VESSEL_TYPES } from "../../entities/vessel/model/meta";
import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types";
import type { MouseEvent } from "react";
type Props = {
vessels: DerivedLegacyVessel[];
selectedMmsi: number | null;
highlightedMmsiSet?: number[];
onToggleHighlightMmsi: (mmsi: number) => void;
onSelectMmsi: (mmsi: number) => void;
onHoverMmsi?: (mmsi: number) => void;
onClearHover?: () => void;
};
function isFiniteNumber(x: unknown): x is number {
export function VesselList({
vessels,
selectedMmsi,
highlightedMmsiSet = [],
onToggleHighlightMmsi,
onSelectMmsi,
onHoverMmsi,
onClearHover,
}: Props) {
const handlePrimaryAction = (e: MouseEvent, mmsi: number) => {
if (e.shiftKey || e.ctrlKey || e.metaKey) {
onToggleHighlightMmsi(mmsi);
return;
}
onSelectMmsi(mmsi);
};
function isFiniteNumber(x: unknown): x is number {
return typeof x === "number" && Number.isFinite(x);
}
export function VesselList({ vessels, selectedMmsi, onSelectMmsi }: Props) {
const sorted = vessels
.slice()
.sort((a, b) => (isFiniteNumber(b.sog) ? b.sog : -1) - (isFiniteNumber(a.sog) ? a.sog : -1))
@ -29,13 +50,15 @@ export function VesselList({ vessels, selectedMmsi, onSelectMmsi }: Props) {
const speedColor = inRange ? "#22C55E" : (v.sog ?? 0) > 5 ? "#3B82F6" : "var(--muted)";
const hasPair = v.pairPermitNo ? "⛓" : "";
const sel = selectedMmsi === v.mmsi;
const hl = highlightedMmsiSet.includes(v.mmsi);
return (
<div
key={v.mmsi}
className="vi"
onClick={() => onSelectMmsi(v.mmsi)}
style={sel ? { background: "rgba(59,130,246,.12)", border: "1px solid rgba(59,130,246,.45)" } : undefined}
className={`vi ${sel ? "sel" : ""} ${hl ? "hl" : ""}`}
onClick={(e) => handlePrimaryAction(e, v.mmsi)}
onMouseEnter={() => onHoverMmsi?.(v.mmsi)}
onMouseLeave={() => onClearHover?.()}
title={v.name}
>
<div className="dot" style={{ background: meta.color, boxShadow: v.state.isFishing ? `0 0 3px ${meta.color}` : undefined }} />
@ -56,4 +79,3 @@ export function VesselList({ vessels, selectedMmsi, onSelectMmsi }: Props) {
</div>
);
}