feat(map3d): stabilize globe overlays and hover-highlight sync
This commit is contained in:
부모
b944887430
커밋
05b0c6b881
@ -188,12 +188,17 @@ export function computeFleetCircles(vessels: DerivedLegacyVessel[]): FleetCircle
|
|||||||
const out: FleetCircle[] = [];
|
const out: FleetCircle[] = [];
|
||||||
for (const [ownerKey, vs] of groups.entries()) {
|
for (const [ownerKey, vs] of groups.entries()) {
|
||||||
if (vs.length < 3) continue;
|
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 lon = vs.reduce((sum, v) => sum + v.lon, 0) / vs.length;
|
||||||
const lat = vs.reduce((sum, v) => sum + v.lat, 0) / vs.length;
|
const lat = vs.reduce((sum, v) => sum + v.lat, 0) / vs.length;
|
||||||
let radiusNm = 0;
|
let radiusNm = 0;
|
||||||
for (const v of vs) radiusNm = Math.max(radiusNm, haversineNm(lat, lon, v.lat, v.lon));
|
for (const v of vs) radiusNm = Math.max(radiusNm, haversineNm(lat, lon, v.lat, v.lon));
|
||||||
out.push({
|
out.push({
|
||||||
ownerKey,
|
ownerKey,
|
||||||
|
ownerLabel,
|
||||||
center: [lon, lat],
|
center: [lon, lat],
|
||||||
radiusNm: Math.max(0.2, radiusNm),
|
radiusNm: Math.max(0.2, radiusNm),
|
||||||
count: vs.length,
|
count: vs.length,
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export type FcLink = {
|
|||||||
|
|
||||||
export type FleetCircle = {
|
export type FleetCircle = {
|
||||||
ownerKey: string;
|
ownerKey: string;
|
||||||
|
ownerLabel: string;
|
||||||
center: [number, number];
|
center: [number, number];
|
||||||
radiusNm: number;
|
radiusNm: number;
|
||||||
count: number;
|
count: number;
|
||||||
@ -71,4 +72,3 @@ export type LegacyAlarm = {
|
|||||||
text: string;
|
text: string;
|
||||||
relatedMmsi: number[];
|
relatedMmsi: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -610,6 +610,10 @@ export function DashboardPage() {
|
|||||||
fleetCircles={fleetCirclesForMap}
|
fleetCircles={fleetCirclesForMap}
|
||||||
fleetFocus={fleetFocus}
|
fleetFocus={fleetFocus}
|
||||||
onProjectionLoadingChange={setIsProjectionLoading}
|
onProjectionLoadingChange={setIsProjectionLoading}
|
||||||
|
onHoverMmsi={(mmsis) => setHoveredMmsiSet(setUniqueSorted(mmsis))}
|
||||||
|
onClearMmsiHover={() => setHoveredMmsiSet((prev) => (prev.length === 0 ? prev : []))}
|
||||||
|
onHoverPair={(pairMmsis) => setHoveredPairMmsiSet(setUniqueSorted(pairMmsis))}
|
||||||
|
onClearPairHover={() => setHoveredPairMmsiSet([])}
|
||||||
onHoverFleet={(ownerKey, fleetMmsis) => {
|
onHoverFleet={(ownerKey, fleetMmsis) => {
|
||||||
setHoveredFleetOwnerKey(ownerKey);
|
setHoveredFleetOwnerKey(ownerKey);
|
||||||
setHoveredFleetMmsiSet(setSortedIfChanged(fleetMmsis));
|
setHoveredFleetMmsiSet(setSortedIfChanged(fleetMmsis));
|
||||||
|
|||||||
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
@ -1,17 +1,38 @@
|
|||||||
import { VESSEL_TYPES } from "../../entities/vessel/model/meta";
|
import { VESSEL_TYPES } from "../../entities/vessel/model/meta";
|
||||||
import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types";
|
import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types";
|
||||||
|
import type { MouseEvent } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
vessels: DerivedLegacyVessel[];
|
vessels: DerivedLegacyVessel[];
|
||||||
selectedMmsi: number | null;
|
selectedMmsi: number | null;
|
||||||
|
highlightedMmsiSet?: number[];
|
||||||
|
onToggleHighlightMmsi: (mmsi: number) => void;
|
||||||
onSelectMmsi: (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);
|
return typeof x === "number" && Number.isFinite(x);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VesselList({ vessels, selectedMmsi, onSelectMmsi }: Props) {
|
|
||||||
const sorted = vessels
|
const sorted = vessels
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => (isFiniteNumber(b.sog) ? b.sog : -1) - (isFiniteNumber(a.sog) ? a.sog : -1))
|
.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 speedColor = inRange ? "#22C55E" : (v.sog ?? 0) > 5 ? "#3B82F6" : "var(--muted)";
|
||||||
const hasPair = v.pairPermitNo ? "⛓" : "";
|
const hasPair = v.pairPermitNo ? "⛓" : "";
|
||||||
const sel = selectedMmsi === v.mmsi;
|
const sel = selectedMmsi === v.mmsi;
|
||||||
|
const hl = highlightedMmsiSet.includes(v.mmsi);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={v.mmsi}
|
key={v.mmsi}
|
||||||
className="vi"
|
className={`vi ${sel ? "sel" : ""} ${hl ? "hl" : ""}`}
|
||||||
onClick={() => onSelectMmsi(v.mmsi)}
|
onClick={(e) => handlePrimaryAction(e, v.mmsi)}
|
||||||
style={sel ? { background: "rgba(59,130,246,.12)", border: "1px solid rgba(59,130,246,.45)" } : undefined}
|
onMouseEnter={() => onHoverMmsi?.(v.mmsi)}
|
||||||
|
onMouseLeave={() => onClearHover?.()}
|
||||||
title={v.name}
|
title={v.name}
|
||||||
>
|
>
|
||||||
<div className="dot" style={{ background: meta.color, boxShadow: v.state.isFishing ? `0 0 3px ${meta.color}` : undefined }} />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user