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));
|
||||||
|
|||||||
@ -56,6 +56,10 @@ type Props = {
|
|||||||
};
|
};
|
||||||
onHoverFleet?: (ownerKey: string | null, fleetMmsiSet: number[]) => void;
|
onHoverFleet?: (ownerKey: string | null, fleetMmsiSet: number[]) => void;
|
||||||
onClearFleetHover?: () => void;
|
onClearFleetHover?: () => void;
|
||||||
|
onHoverMmsi?: (mmsiList: number[]) => void;
|
||||||
|
onClearMmsiHover?: () => void;
|
||||||
|
onHoverPair?: (mmsiList: number[]) => void;
|
||||||
|
onClearPairHover?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function toNumberSet(values: number[] | undefined | null) {
|
function toNumberSet(values: number[] | undefined | null) {
|
||||||
@ -141,6 +145,49 @@ function makeFleetCircleFeatureId(ownerKey: string) {
|
|||||||
return `fleet-${ownerKey}`;
|
return `fleet-${ownerKey}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeMmsiPairHighlightExpr(aField: string, bField: string, hoveredMmsiList: number[]) {
|
||||||
|
if (!Array.isArray(hoveredMmsiList) || hoveredMmsiList.length < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const inA = ["in", ["to-number", ["get", aField]], ["literal", hoveredMmsiList]] as unknown[];
|
||||||
|
const inB = ["in", ["to-number", ["get", bField]], ["literal", hoveredMmsiList]] as unknown[];
|
||||||
|
return ["all", inA, inB] as unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMmsiAnyEndpointExpr(aField: string, bField: string, hoveredMmsiList: number[]) {
|
||||||
|
if (!Array.isArray(hoveredMmsiList) || hoveredMmsiList.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const literal = ["literal", hoveredMmsiList] as unknown[];
|
||||||
|
return [
|
||||||
|
"any",
|
||||||
|
["in", ["to-number", ["get", aField]], literal],
|
||||||
|
["in", ["to-number", ["get", bField]], literal],
|
||||||
|
] as unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeFleetOwnerMatchExpr(hoveredOwnerKeys: string[]) {
|
||||||
|
if (!Array.isArray(hoveredOwnerKeys) || hoveredOwnerKeys.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const expr = ["match", ["to-string", ["coalesce", ["get", "ownerKey"], ""]]] as unknown[];
|
||||||
|
for (const ownerKey of hoveredOwnerKeys) {
|
||||||
|
expr.push(String(ownerKey), true);
|
||||||
|
}
|
||||||
|
expr.push(false);
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeFleetMemberMatchExpr(hoveredFleetMmsiList: number[]) {
|
||||||
|
if (!Array.isArray(hoveredFleetMmsiList) || hoveredFleetMmsiList.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const clauses = hoveredFleetMmsiList.map((mmsi) =>
|
||||||
|
["in", mmsi, ["coalesce", ["get", "vesselMmsis"], []]] as unknown[],
|
||||||
|
);
|
||||||
|
return ["any", ...clauses] as unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
const SHIP_ICON_MAPPING = {
|
const SHIP_ICON_MAPPING = {
|
||||||
ship: {
|
ship: {
|
||||||
x: 0,
|
x: 0,
|
||||||
@ -922,7 +969,37 @@ type PairRangeCircle = {
|
|||||||
distanceNm: number;
|
distanceNm: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeUniqueSorted = (values: number[]) => Array.from(new Set(values.filter((v) => Number.isFinite(v)))).sort((a, b) => a - b);
|
const toNumberArray = (values: unknown): number[] => {
|
||||||
|
if (values == null) return [];
|
||||||
|
if (Array.isArray(values)) {
|
||||||
|
return values as unknown as number[];
|
||||||
|
}
|
||||||
|
if (typeof values === "number" && Number.isFinite(values)) {
|
||||||
|
return [values];
|
||||||
|
}
|
||||||
|
if (typeof values === "string") {
|
||||||
|
const value = toSafeNumber(Number(values));
|
||||||
|
return value == null ? [] : [value];
|
||||||
|
}
|
||||||
|
if (typeof values === "object") {
|
||||||
|
if (typeof (values as { [Symbol.iterator]?: unknown })?.[Symbol.iterator] === "function") {
|
||||||
|
try {
|
||||||
|
return Array.from(values as Iterable<unknown>) as number[];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeUniqueSorted = (values: unknown) => {
|
||||||
|
const maybeArray = toNumberArray(values);
|
||||||
|
const normalized = Array.isArray(maybeArray) ? maybeArray : [];
|
||||||
|
const unique = Array.from(new Set(normalized.filter((value) => Number.isFinite(value))));
|
||||||
|
unique.sort((a, b) => a - b);
|
||||||
|
return unique;
|
||||||
|
};
|
||||||
|
|
||||||
const equalNumberArrays = (a: number[], b: number[]) => {
|
const equalNumberArrays = (a: number[], b: number[]) => {
|
||||||
if (a.length !== b.length) return false;
|
if (a.length !== b.length) return false;
|
||||||
@ -959,6 +1036,10 @@ export function Map3D({
|
|||||||
fleetFocus,
|
fleetFocus,
|
||||||
onHoverFleet,
|
onHoverFleet,
|
||||||
onClearFleetHover,
|
onClearFleetHover,
|
||||||
|
onHoverMmsi,
|
||||||
|
onClearMmsiHover,
|
||||||
|
onHoverPair,
|
||||||
|
onClearPairHover,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const mapRef = useRef<maplibregl.Map | null>(null);
|
const mapRef = useRef<maplibregl.Map | null>(null);
|
||||||
@ -973,6 +1054,8 @@ export function Map3D({
|
|||||||
const projectionBusyTimerRef = useRef<ReturnType<typeof window.setTimeout> | null>(null);
|
const projectionBusyTimerRef = useRef<ReturnType<typeof window.setTimeout> | null>(null);
|
||||||
const projectionPrevRef = useRef<MapProjectionId>(projection);
|
const projectionPrevRef = useRef<MapProjectionId>(projection);
|
||||||
const mapTooltipRef = useRef<maplibregl.Popup | null>(null);
|
const mapTooltipRef = useRef<maplibregl.Popup | null>(null);
|
||||||
|
const deckHoverRafRef = useRef<number | null>(null);
|
||||||
|
const deckHoverHasHitRef = useRef(false);
|
||||||
const [hoveredDeckMmsiSet, setHoveredDeckMmsiSet] = useState<number[]>([]);
|
const [hoveredDeckMmsiSet, setHoveredDeckMmsiSet] = useState<number[]>([]);
|
||||||
const [hoveredDeckPairMmsiSet, setHoveredDeckPairMmsiSet] = useState<number[]>([]);
|
const [hoveredDeckPairMmsiSet, setHoveredDeckPairMmsiSet] = useState<number[]>([]);
|
||||||
const [hoveredDeckFleetOwnerKey, setHoveredDeckFleetOwnerKey] = useState<string | null>(null);
|
const [hoveredDeckFleetOwnerKey, setHoveredDeckFleetOwnerKey] = useState<string | null>(null);
|
||||||
@ -991,13 +1074,16 @@ export function Map3D({
|
|||||||
if (hoveredDeckFleetOwnerKey) keys.add(hoveredDeckFleetOwnerKey);
|
if (hoveredDeckFleetOwnerKey) keys.add(hoveredDeckFleetOwnerKey);
|
||||||
return keys;
|
return keys;
|
||||||
}, [hoveredFleetOwnerKey, hoveredDeckFleetOwnerKey]);
|
}, [hoveredFleetOwnerKey, hoveredDeckFleetOwnerKey]);
|
||||||
|
const fleetFocusId = fleetFocus?.id;
|
||||||
|
const fleetFocusLon = fleetFocus?.center?.[0];
|
||||||
|
const fleetFocusLat = fleetFocus?.center?.[1];
|
||||||
|
const fleetFocusZoom = fleetFocus?.zoom;
|
||||||
|
|
||||||
const reorderGlobeFeatureLayers = useCallback((options?: { shipTop?: boolean }) => {
|
const reorderGlobeFeatureLayers = useCallback(() => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map || projectionRef.current !== "globe") return;
|
if (!map || projectionRef.current !== "globe") return;
|
||||||
if (projectionBusyRef.current) return;
|
if (projectionBusyRef.current) return;
|
||||||
|
|
||||||
const shipTop = options?.shipTop === true;
|
|
||||||
const ordering = [
|
const ordering = [
|
||||||
"zones-fill",
|
"zones-fill",
|
||||||
"zones-line",
|
"zones-line",
|
||||||
@ -1020,17 +1106,6 @@ export function Map3D({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shipTop) return;
|
|
||||||
|
|
||||||
const shipOrdering = ["ships-globe-halo", "ships-globe-outline", "ships-globe"];
|
|
||||||
for (const layerId of shipOrdering) {
|
|
||||||
try {
|
|
||||||
if (map.getLayer(layerId)) map.moveLayer(layerId);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kickRepaint(map);
|
kickRepaint(map);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -1078,6 +1153,9 @@ export function Map3D({
|
|||||||
[effectiveHoveredFleetMmsiSet, hoveredFleetOwnerKeys],
|
[effectiveHoveredFleetMmsiSet, hoveredFleetOwnerKeys],
|
||||||
);
|
);
|
||||||
const hoveredPairSignature = useMemo(() => makeSetSignature(effectiveHoveredPairMmsiSet), [effectiveHoveredPairMmsiSet]);
|
const hoveredPairSignature = useMemo(() => makeSetSignature(effectiveHoveredPairMmsiSet), [effectiveHoveredPairMmsiSet]);
|
||||||
|
const hoveredPairMmsiList = useMemo(() => makeUniqueSorted(Array.from(effectiveHoveredPairMmsiSet)), [effectiveHoveredPairMmsiSet]);
|
||||||
|
const hoveredFleetOwnerKeyList = useMemo(() => Array.from(hoveredFleetOwnerKeys).sort(), [hoveredFleetOwnerKeys]);
|
||||||
|
const hoveredFleetMmsiList = useMemo(() => makeUniqueSorted(Array.from(effectiveHoveredFleetMmsiSet)), [effectiveHoveredFleetMmsiSet]);
|
||||||
|
|
||||||
const isHighlightedMmsi = useCallback(
|
const isHighlightedMmsi = useCallback(
|
||||||
(mmsi: number) => highlightedMmsiSetCombined.has(mmsi),
|
(mmsi: number) => highlightedMmsiSetCombined.has(mmsi),
|
||||||
@ -1100,6 +1178,16 @@ export function Map3D({
|
|||||||
[hoveredFleetOwnerKeys, isHighlightedMmsi],
|
[hoveredFleetOwnerKeys, isHighlightedMmsi],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const shipData = useMemo(() => {
|
||||||
|
return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon) && isFiniteNumber(t.mmsi));
|
||||||
|
}, [targets]);
|
||||||
|
|
||||||
|
const shipByMmsi = useMemo(() => {
|
||||||
|
const byMmsi = new Map<number, AisTarget>();
|
||||||
|
for (const t of shipData) byMmsi.set(t.mmsi, t);
|
||||||
|
return byMmsi;
|
||||||
|
}, [shipData]);
|
||||||
|
|
||||||
const hasAuxiliarySelectModifier = (ev?: {
|
const hasAuxiliarySelectModifier = (ev?: {
|
||||||
shiftKey?: boolean;
|
shiftKey?: boolean;
|
||||||
ctrlKey?: boolean;
|
ctrlKey?: boolean;
|
||||||
@ -1109,21 +1197,6 @@ export function Map3D({
|
|||||||
return !!(ev.shiftKey || ev.ctrlKey || ev.metaKey);
|
return !!(ev.shiftKey || ev.ctrlKey || ev.metaKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setHoveredMmsiList = useCallback((next: number[]) => {
|
|
||||||
const normalized = makeUniqueSorted(next);
|
|
||||||
setHoveredDeckMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setHoveredDeckMmsiSingle = useCallback((mmsi: number | null) => {
|
|
||||||
const normalized = mmsi == null ? [] : [mmsi];
|
|
||||||
setHoveredDeckMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setHoveredDeckPairs = useCallback((next: number[]) => {
|
|
||||||
const normalized = makeUniqueSorted(next);
|
|
||||||
setHoveredDeckPairMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setHoveredDeckFleetMmsis = useCallback((next: number[]) => {
|
const setHoveredDeckFleetMmsis = useCallback((next: number[]) => {
|
||||||
const normalized = makeUniqueSorted(next);
|
const normalized = makeUniqueSorted(next);
|
||||||
setHoveredDeckFleetMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
setHoveredDeckFleetMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
||||||
@ -1135,6 +1208,12 @@ export function Map3D({
|
|||||||
|
|
||||||
const onHoverFleetRef = useRef(onHoverFleet);
|
const onHoverFleetRef = useRef(onHoverFleet);
|
||||||
const onClearFleetHoverRef = useRef(onClearFleetHover);
|
const onClearFleetHoverRef = useRef(onClearFleetHover);
|
||||||
|
const onHoverMmsiRef = useRef(onHoverMmsi);
|
||||||
|
const onClearMmsiHoverRef = useRef(onClearMmsiHover);
|
||||||
|
const onHoverPairRef = useRef(onHoverPair);
|
||||||
|
const onClearPairHoverRef = useRef(onClearPairHover);
|
||||||
|
const mapDeckMmsiHoverRef = useRef<number[]>([]);
|
||||||
|
const mapDeckPairHoverRef = useRef<number[]>([]);
|
||||||
const mapFleetHoverStateRef = useRef<{
|
const mapFleetHoverStateRef = useRef<{
|
||||||
ownerKey: string | null;
|
ownerKey: string | null;
|
||||||
vesselMmsis: number[];
|
vesselMmsis: number[];
|
||||||
@ -1143,22 +1222,11 @@ export function Map3D({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onHoverFleetRef.current = onHoverFleet;
|
onHoverFleetRef.current = onHoverFleet;
|
||||||
onClearFleetHoverRef.current = onClearFleetHover;
|
onClearFleetHoverRef.current = onClearFleetHover;
|
||||||
}, [onHoverFleet, onClearFleetHover]);
|
onHoverMmsiRef.current = onHoverMmsi;
|
||||||
|
onClearMmsiHoverRef.current = onClearMmsiHover;
|
||||||
const setMapFleetHoverState = useCallback(
|
onHoverPairRef.current = onHoverPair;
|
||||||
(ownerKey: string | null, vesselMmsis: number[]) => {
|
onClearPairHoverRef.current = onClearPairHover;
|
||||||
const normalized = makeUniqueSorted(vesselMmsis);
|
}, [onHoverFleet, onClearFleetHover, onHoverMmsi, onClearMmsiHover, onHoverPair, onClearPairHover]);
|
||||||
const prev = mapFleetHoverStateRef.current;
|
|
||||||
if (prev.ownerKey === ownerKey && equalNumberArrays(prev.vesselMmsis, normalized)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setHoveredDeckFleetOwner(ownerKey);
|
|
||||||
setHoveredDeckFleetMmsis(normalized);
|
|
||||||
mapFleetHoverStateRef.current = { ownerKey, vesselMmsis: normalized };
|
|
||||||
onHoverFleetRef.current?.(ownerKey, normalized);
|
|
||||||
},
|
|
||||||
[setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis],
|
|
||||||
);
|
|
||||||
|
|
||||||
const clearMapFleetHoverState = useCallback(() => {
|
const clearMapFleetHoverState = useCallback(() => {
|
||||||
const nextOwner = null;
|
const nextOwner = null;
|
||||||
@ -1172,6 +1240,97 @@ export function Map3D({
|
|||||||
}
|
}
|
||||||
}, [setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis]);
|
}, [setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis]);
|
||||||
|
|
||||||
|
const clearDeckHoverPairs = useCallback(() => {
|
||||||
|
const prev = mapDeckPairHoverRef.current;
|
||||||
|
mapDeckPairHoverRef.current = [];
|
||||||
|
setHoveredDeckPairMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
|
||||||
|
if (prev.length > 0) {
|
||||||
|
onClearPairHoverRef.current?.();
|
||||||
|
}
|
||||||
|
}, [setHoveredDeckPairMmsiSet]);
|
||||||
|
|
||||||
|
const clearDeckHoverMmsi = useCallback(() => {
|
||||||
|
const prev = mapDeckMmsiHoverRef.current;
|
||||||
|
mapDeckMmsiHoverRef.current = [];
|
||||||
|
setHoveredDeckMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
|
||||||
|
if (prev.length > 0) {
|
||||||
|
onClearMmsiHoverRef.current?.();
|
||||||
|
}
|
||||||
|
}, [setHoveredDeckMmsiSet]);
|
||||||
|
|
||||||
|
const scheduleDeckHoverResolve = useCallback(() => {
|
||||||
|
if (deckHoverRafRef.current != null) return;
|
||||||
|
deckHoverRafRef.current = window.requestAnimationFrame(() => {
|
||||||
|
deckHoverRafRef.current = null;
|
||||||
|
if (!deckHoverHasHitRef.current) {
|
||||||
|
clearDeckHoverMmsi();
|
||||||
|
clearDeckHoverPairs();
|
||||||
|
clearMapFleetHoverState();
|
||||||
|
}
|
||||||
|
deckHoverHasHitRef.current = false;
|
||||||
|
});
|
||||||
|
}, [clearDeckHoverMmsi, clearDeckHoverPairs, clearMapFleetHoverState]);
|
||||||
|
|
||||||
|
const touchDeckHoverState = useCallback(
|
||||||
|
(isHover: boolean) => {
|
||||||
|
if (isHover) deckHoverHasHitRef.current = true;
|
||||||
|
scheduleDeckHoverResolve();
|
||||||
|
},
|
||||||
|
[scheduleDeckHoverResolve],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setDeckHoverMmsi = useCallback(
|
||||||
|
(next: number[]) => {
|
||||||
|
const normalized = makeUniqueSorted(next);
|
||||||
|
touchDeckHoverState(normalized.length > 0);
|
||||||
|
setHoveredDeckMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
||||||
|
if (!equalNumberArrays(mapDeckMmsiHoverRef.current, normalized)) {
|
||||||
|
mapDeckMmsiHoverRef.current = normalized;
|
||||||
|
onHoverMmsiRef.current?.(normalized);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setHoveredDeckMmsiSet, touchDeckHoverState],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setDeckHoverPairs = useCallback(
|
||||||
|
(next: number[]) => {
|
||||||
|
const normalized = makeUniqueSorted(next);
|
||||||
|
touchDeckHoverState(normalized.length > 0);
|
||||||
|
setHoveredDeckPairMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
||||||
|
if (!equalNumberArrays(mapDeckPairHoverRef.current, normalized)) {
|
||||||
|
mapDeckPairHoverRef.current = normalized;
|
||||||
|
onHoverPairRef.current?.(normalized);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setHoveredDeckPairMmsiSet, touchDeckHoverState],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setMapFleetHoverState = useCallback(
|
||||||
|
(ownerKey: string | null, vesselMmsis: number[]) => {
|
||||||
|
const normalized = makeUniqueSorted(vesselMmsis);
|
||||||
|
const prev = mapFleetHoverStateRef.current;
|
||||||
|
if (prev.ownerKey === ownerKey && equalNumberArrays(prev.vesselMmsis, normalized)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
touchDeckHoverState(!!ownerKey || normalized.length > 0);
|
||||||
|
setHoveredDeckFleetOwner(ownerKey);
|
||||||
|
setHoveredDeckFleetMmsis(normalized);
|
||||||
|
mapFleetHoverStateRef.current = { ownerKey, vesselMmsis: normalized };
|
||||||
|
onHoverFleetRef.current?.(ownerKey, normalized);
|
||||||
|
},
|
||||||
|
[setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis, touchDeckHoverState],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (deckHoverRafRef.current != null) {
|
||||||
|
window.cancelAnimationFrame(deckHoverRafRef.current);
|
||||||
|
deckHoverRafRef.current = null;
|
||||||
|
}
|
||||||
|
deckHoverHasHitRef.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mapFleetHoverStateRef.current = {
|
mapFleetHoverStateRef.current = {
|
||||||
ownerKey: hoveredFleetOwnerKey,
|
ownerKey: hoveredFleetOwnerKey,
|
||||||
@ -2155,19 +2314,7 @@ export function Map3D({
|
|||||||
|
|
||||||
const visibility = settings.showShips ? "visible" : "none";
|
const visibility = settings.showShips ? "visible" : "none";
|
||||||
|
|
||||||
// Put ships at the top so they're always visible (especially important under globe projection).
|
|
||||||
const before = undefined;
|
const before = undefined;
|
||||||
const bringShipLayersToFront = () => {
|
|
||||||
const ids = [haloId, outlineId, symbolId];
|
|
||||||
for (const id of ids) {
|
|
||||||
if (!map.getLayer(id)) continue;
|
|
||||||
try {
|
|
||||||
map.moveLayer(id);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!map.getLayer(haloId)) {
|
if (!map.getLayer(haloId)) {
|
||||||
try {
|
try {
|
||||||
@ -2181,10 +2328,12 @@ export function Map3D({
|
|||||||
"circle-sort-key": [
|
"circle-sort-key": [
|
||||||
"case",
|
"case",
|
||||||
["==", ["get", "selected"], 1],
|
["==", ["get", "selected"], 1],
|
||||||
30,
|
90,
|
||||||
["==", ["get", "highlighted"], 1],
|
["==", ["get", "highlighted"], 1],
|
||||||
25,
|
80,
|
||||||
10,
|
["==", ["get", "permitted"], 1],
|
||||||
|
60,
|
||||||
|
20,
|
||||||
] as never,
|
] as never,
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
@ -2208,7 +2357,20 @@ export function Map3D({
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
map.setLayoutProperty(haloId, "visibility", visibility);
|
map.setLayoutProperty(haloId, "visibility", visibility);
|
||||||
map.setPaintProperty(haloId, "circle-color", ["case", ["==", ["get", "highlighted"], 1], ["coalesce", ["get", "shipColor"], "#64748b"], ["coalesce", ["get", "shipColor"], "#64748b"]] as never);
|
map.setPaintProperty(
|
||||||
|
haloId,
|
||||||
|
"circle-color",
|
||||||
|
[
|
||||||
|
"case",
|
||||||
|
["==", ["get", "selected"], 1],
|
||||||
|
"rgba(14,234,255,1)",
|
||||||
|
["==", ["get", "highlighted"], 1],
|
||||||
|
"rgba(245,158,11,1)",
|
||||||
|
["==", ["get", "permitted"], 1],
|
||||||
|
"rgba(125,211,252,0.95)",
|
||||||
|
"rgba(59,130,246,1)",
|
||||||
|
] as never,
|
||||||
|
);
|
||||||
map.setPaintProperty(haloId, "circle-opacity", [
|
map.setPaintProperty(haloId, "circle-opacity", [
|
||||||
"case",
|
"case",
|
||||||
["==", ["get", "selected"], 1],
|
["==", ["get", "selected"], 1],
|
||||||
@ -2239,6 +2401,8 @@ export function Map3D({
|
|||||||
"rgba(14,234,255,0.95)",
|
"rgba(14,234,255,0.95)",
|
||||||
["==", ["get", "highlighted"], 1],
|
["==", ["get", "highlighted"], 1],
|
||||||
"rgba(245,158,11,0.95)",
|
"rgba(245,158,11,0.95)",
|
||||||
|
["==", ["get", "permitted"], 1],
|
||||||
|
"rgba(125,211,252,0.95)",
|
||||||
"rgba(59,130,246,0.75)",
|
"rgba(59,130,246,0.75)",
|
||||||
] as never,
|
] as never,
|
||||||
"circle-stroke-width": [
|
"circle-stroke-width": [
|
||||||
@ -2247,6 +2411,8 @@ export function Map3D({
|
|||||||
3.4,
|
3.4,
|
||||||
["==", ["get", "highlighted"], 1],
|
["==", ["get", "highlighted"], 1],
|
||||||
2.7,
|
2.7,
|
||||||
|
["==", ["get", "permitted"], 1],
|
||||||
|
1.8,
|
||||||
0.0,
|
0.0,
|
||||||
] as never,
|
] as never,
|
||||||
"circle-stroke-opacity": 0.85,
|
"circle-stroke-opacity": 0.85,
|
||||||
@ -2256,10 +2422,12 @@ export function Map3D({
|
|||||||
"circle-sort-key": [
|
"circle-sort-key": [
|
||||||
"case",
|
"case",
|
||||||
["==", ["get", "selected"], 1],
|
["==", ["get", "selected"], 1],
|
||||||
40,
|
100,
|
||||||
["==", ["get", "highlighted"], 1],
|
["==", ["get", "highlighted"], 1],
|
||||||
35,
|
90,
|
||||||
15,
|
["==", ["get", "permitted"], 1],
|
||||||
|
70,
|
||||||
|
30,
|
||||||
] as never,
|
] as never,
|
||||||
},
|
},
|
||||||
} as unknown as LayerSpecification,
|
} as unknown as LayerSpecification,
|
||||||
@ -2280,6 +2448,8 @@ export function Map3D({
|
|||||||
"rgba(14,234,255,0.95)",
|
"rgba(14,234,255,0.95)",
|
||||||
["==", ["get", "highlighted"], 1],
|
["==", ["get", "highlighted"], 1],
|
||||||
"rgba(245,158,11,0.95)",
|
"rgba(245,158,11,0.95)",
|
||||||
|
["==", ["get", "permitted"], 1],
|
||||||
|
"rgba(125,211,252,0.95)",
|
||||||
"rgba(59,130,246,0.75)",
|
"rgba(59,130,246,0.75)",
|
||||||
] as never,
|
] as never,
|
||||||
);
|
);
|
||||||
@ -2292,6 +2462,8 @@ export function Map3D({
|
|||||||
3.4,
|
3.4,
|
||||||
["==", ["get", "highlighted"], 1],
|
["==", ["get", "highlighted"], 1],
|
||||||
2.7,
|
2.7,
|
||||||
|
["==", ["get", "permitted"], 1],
|
||||||
|
1.8,
|
||||||
0.0,
|
0.0,
|
||||||
] as never,
|
] as never,
|
||||||
);
|
);
|
||||||
@ -2312,10 +2484,12 @@ export function Map3D({
|
|||||||
"symbol-sort-key": [
|
"symbol-sort-key": [
|
||||||
"case",
|
"case",
|
||||||
["==", ["get", "selected"], 1],
|
["==", ["get", "selected"], 1],
|
||||||
50,
|
95,
|
||||||
["==", ["get", "highlighted"], 1],
|
["==", ["get", "highlighted"], 1],
|
||||||
|
85,
|
||||||
|
["==", ["get", "permitted"], 1],
|
||||||
|
65,
|
||||||
45,
|
45,
|
||||||
20,
|
|
||||||
] as never,
|
] as never,
|
||||||
"icon-image": imgId,
|
"icon-image": imgId,
|
||||||
"icon-size": [
|
"icon-size": [
|
||||||
@ -2346,6 +2520,8 @@ export function Map3D({
|
|||||||
"rgba(14,234,255,1)",
|
"rgba(14,234,255,1)",
|
||||||
["==", ["get", "highlighted"], 1],
|
["==", ["get", "highlighted"], 1],
|
||||||
"rgba(245,158,11,1)",
|
"rgba(245,158,11,1)",
|
||||||
|
["==", ["get", "permitted"], 1],
|
||||||
|
"rgba(125,211,252,1)",
|
||||||
"rgba(59,130,246,1)",
|
"rgba(59,130,246,1)",
|
||||||
] as never,
|
] as never,
|
||||||
"icon-opacity": [
|
"icon-opacity": [
|
||||||
@ -2391,6 +2567,8 @@ export function Map3D({
|
|||||||
"rgba(14,234,255,1)",
|
"rgba(14,234,255,1)",
|
||||||
["==", ["get", "highlighted"], 1],
|
["==", ["get", "highlighted"], 1],
|
||||||
"rgba(245,158,11,1)",
|
"rgba(245,158,11,1)",
|
||||||
|
["==", ["get", "permitted"], 1],
|
||||||
|
"rgba(125,211,252,1)",
|
||||||
["coalesce", ["get", "shipColor"], "#64748b"],
|
["coalesce", ["get", "shipColor"], "#64748b"],
|
||||||
] as never,
|
] as never,
|
||||||
);
|
);
|
||||||
@ -2415,6 +2593,8 @@ export function Map3D({
|
|||||||
"rgba(14,234,255,0.68)",
|
"rgba(14,234,255,0.68)",
|
||||||
["==", ["get", "highlighted"], 1],
|
["==", ["get", "highlighted"], 1],
|
||||||
"rgba(245,158,11,0.72)",
|
"rgba(245,158,11,0.72)",
|
||||||
|
["==", ["get", "permitted"], 1],
|
||||||
|
"rgba(125,211,252,0.58)",
|
||||||
"rgba(15,23,42,0.25)",
|
"rgba(15,23,42,0.25)",
|
||||||
] as never,
|
] as never,
|
||||||
);
|
);
|
||||||
@ -2436,8 +2616,7 @@ export function Map3D({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Selection and highlight are now source-data driven.
|
// Selection and highlight are now source-data driven.
|
||||||
bringShipLayersToFront();
|
reorderGlobeFeatureLayers();
|
||||||
reorderGlobeFeatureLayers({ shipTop: true });
|
|
||||||
kickRepaint(map);
|
kickRepaint(map);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2448,7 +2627,7 @@ export function Map3D({
|
|||||||
}, [
|
}, [
|
||||||
projection,
|
projection,
|
||||||
settings.showShips,
|
settings.showShips,
|
||||||
targets,
|
shipData,
|
||||||
legacyHits,
|
legacyHits,
|
||||||
selectedMmsi,
|
selectedMmsi,
|
||||||
hoveredMmsiSetRef,
|
hoveredMmsiSetRef,
|
||||||
@ -2572,7 +2751,6 @@ export function Map3D({
|
|||||||
bMmsi: p.bMmsi,
|
bMmsi: p.bMmsi,
|
||||||
distanceNm: p.distanceNm,
|
distanceNm: p.distanceNm,
|
||||||
warn: p.warn,
|
warn: p.warn,
|
||||||
highlighted: isHighlightedPair(p.aMmsi, p.bMmsi) ? 1 : 0,
|
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
@ -2643,9 +2821,6 @@ export function Map3D({
|
|||||||
overlays.pairLines,
|
overlays.pairLines,
|
||||||
pairLinks,
|
pairLinks,
|
||||||
mapSyncEpoch,
|
mapSyncEpoch,
|
||||||
hoveredShipSignature,
|
|
||||||
hoveredPairSignature,
|
|
||||||
isHighlightedPair,
|
|
||||||
reorderGlobeFeatureLayers,
|
reorderGlobeFeatureLayers,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -2693,7 +2868,6 @@ export function Map3D({
|
|||||||
distanceNm: s.distanceNm,
|
distanceNm: s.distanceNm,
|
||||||
fcMmsi: s.fromMmsi ?? -1,
|
fcMmsi: s.fromMmsi ?? -1,
|
||||||
otherMmsi: s.toMmsi ?? -1,
|
otherMmsi: s.toMmsi ?? -1,
|
||||||
highlighted: s.fromMmsi != null && isHighlightedMmsi(s.fromMmsi) ? 1 : 0,
|
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
@ -2757,8 +2931,6 @@ export function Map3D({
|
|||||||
overlays.fcLines,
|
overlays.fcLines,
|
||||||
fcLinks,
|
fcLinks,
|
||||||
mapSyncEpoch,
|
mapSyncEpoch,
|
||||||
hoveredShipSignature,
|
|
||||||
isHighlightedMmsi,
|
|
||||||
reorderGlobeFeatureLayers,
|
reorderGlobeFeatureLayers,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -2806,10 +2978,10 @@ export function Map3D({
|
|||||||
ownerLabel: c.ownerLabel,
|
ownerLabel: c.ownerLabel,
|
||||||
count: c.count,
|
count: c.count,
|
||||||
vesselMmsis: c.vesselMmsis,
|
vesselMmsis: c.vesselMmsis,
|
||||||
highlighted:
|
// Kept for backward compatibility with existing paint expressions.
|
||||||
isHighlightedFleet(c.ownerKey, c.vesselMmsis) || c.vesselMmsis.some((m) => isHighlightedMmsi(m))
|
// Actual hover-state highlighting is now handled in
|
||||||
? 1
|
// updateGlobeOverlayPaintStates.
|
||||||
: 0,
|
highlighted: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@ -2829,10 +3001,10 @@ export function Map3D({
|
|||||||
ownerLabel: c.ownerLabel,
|
ownerLabel: c.ownerLabel,
|
||||||
count: c.count,
|
count: c.count,
|
||||||
vesselMmsis: c.vesselMmsis,
|
vesselMmsis: c.vesselMmsis,
|
||||||
highlighted:
|
// Kept for backward compatibility with existing paint expressions.
|
||||||
isHighlightedFleet(c.ownerKey, c.vesselMmsis) || c.vesselMmsis.some((m) => isHighlightedMmsi(m))
|
// Actual hover-state highlighting is now handled in
|
||||||
? 1
|
// updateGlobeOverlayPaintStates.
|
||||||
: 0,
|
highlighted: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@ -2930,11 +3102,6 @@ export function Map3D({
|
|||||||
overlays.fleetCircles,
|
overlays.fleetCircles,
|
||||||
fleetCircles,
|
fleetCircles,
|
||||||
mapSyncEpoch,
|
mapSyncEpoch,
|
||||||
hoveredShipSignature,
|
|
||||||
hoveredFleetSignature,
|
|
||||||
hoveredFleetOwnerKey,
|
|
||||||
isHighlightedFleet,
|
|
||||||
isHighlightedMmsi,
|
|
||||||
reorderGlobeFeatureLayers,
|
reorderGlobeFeatureLayers,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -2992,7 +3159,10 @@ export function Map3D({
|
|||||||
aMmsi: c.aMmsi,
|
aMmsi: c.aMmsi,
|
||||||
bMmsi: c.bMmsi,
|
bMmsi: c.bMmsi,
|
||||||
distanceNm: c.distanceNm,
|
distanceNm: c.distanceNm,
|
||||||
highlighted: isHighlightedPair(c.aMmsi, c.bMmsi) ? 1 : 0,
|
// Kept for backward compatibility with existing paint expressions.
|
||||||
|
// Actual hover-state highlighting is now handled in
|
||||||
|
// updateGlobeOverlayPaintStates.
|
||||||
|
highlighted: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@ -3056,22 +3226,148 @@ export function Map3D({
|
|||||||
overlays.pairRange,
|
overlays.pairRange,
|
||||||
pairLinks,
|
pairLinks,
|
||||||
mapSyncEpoch,
|
mapSyncEpoch,
|
||||||
hoveredShipSignature,
|
|
||||||
hoveredPairSignature,
|
|
||||||
hoveredFleetSignature,
|
|
||||||
isHighlightedPair,
|
|
||||||
reorderGlobeFeatureLayers,
|
reorderGlobeFeatureLayers,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const shipData = useMemo(() => {
|
const updateGlobeOverlayPaintStates = useCallback(() => {
|
||||||
return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon) && isFiniteNumber(t.mmsi));
|
if (projection !== "globe" || projectionBusyRef.current) return;
|
||||||
}, [targets]);
|
|
||||||
|
|
||||||
const shipByMmsi = useMemo(() => {
|
const map = mapRef.current;
|
||||||
const byMmsi = new Map<number, AisTarget>();
|
if (!map || !map.isStyleLoaded()) return;
|
||||||
for (const t of shipData) byMmsi.set(t.mmsi, t);
|
|
||||||
return byMmsi;
|
const fleetAwarePairMmsiList = makeUniqueSorted([...hoveredPairMmsiList, ...hoveredFleetMmsiList]);
|
||||||
}, [shipData]);
|
|
||||||
|
const pairHighlightExpr = hoveredPairMmsiList.length >= 2
|
||||||
|
? makeMmsiPairHighlightExpr("aMmsi", "bMmsi", hoveredPairMmsiList)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const fcEndpointHighlightExpr = fleetAwarePairMmsiList.length > 0
|
||||||
|
? makeMmsiAnyEndpointExpr("fcMmsi", "otherMmsi", fleetAwarePairMmsiList)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const fleetOwnerMatchExpr =
|
||||||
|
hoveredFleetOwnerKeyList.length > 0 ? makeFleetOwnerMatchExpr(hoveredFleetOwnerKeyList) : false;
|
||||||
|
const fleetMemberExpr =
|
||||||
|
hoveredFleetMmsiList.length > 0 ? makeFleetMemberMatchExpr(hoveredFleetMmsiList) : false;
|
||||||
|
const fleetHighlightExpr =
|
||||||
|
hoveredFleetOwnerKeyList.length > 0 || hoveredFleetMmsiList.length > 0
|
||||||
|
? (["any", fleetOwnerMatchExpr, fleetMemberExpr] as never)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (map.getLayer("pair-lines-ml")) {
|
||||||
|
map.setPaintProperty(
|
||||||
|
"pair-lines-ml",
|
||||||
|
"line-color",
|
||||||
|
["case", pairHighlightExpr, "rgba(245,158,11,0.98)", ["boolean", ["get", "warn"], false], "rgba(59,130,246,0.55)", "rgba(59,130,246,0.55)"] as never,
|
||||||
|
);
|
||||||
|
map.setPaintProperty(
|
||||||
|
"pair-lines-ml",
|
||||||
|
"line-width",
|
||||||
|
["case", pairHighlightExpr, 2.8, ["boolean", ["get", "warn"], false], 2.2, 1.4] as never,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (map.getLayer("fc-lines-ml")) {
|
||||||
|
map.setPaintProperty(
|
||||||
|
"fc-lines-ml",
|
||||||
|
"line-color",
|
||||||
|
["case", fcEndpointHighlightExpr, "rgba(245,158,11,0.98)", ["boolean", ["get", "suspicious"], false], "rgba(239,68,68,0.95)", "rgba(217,119,6,0.92)"] as never,
|
||||||
|
);
|
||||||
|
map.setPaintProperty(
|
||||||
|
"fc-lines-ml",
|
||||||
|
"line-width",
|
||||||
|
["case", fcEndpointHighlightExpr, 2.0, 1.3] as never,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (map.getLayer("pair-range-ml")) {
|
||||||
|
map.setPaintProperty(
|
||||||
|
"pair-range-ml",
|
||||||
|
"line-color",
|
||||||
|
[
|
||||||
|
"case",
|
||||||
|
pairHighlightExpr,
|
||||||
|
"rgba(245,158,11,0.92)",
|
||||||
|
["boolean", ["get", "warn"], false],
|
||||||
|
"rgba(245,158,11,0.75)",
|
||||||
|
"rgba(59,130,246,0.45)",
|
||||||
|
] as never,
|
||||||
|
);
|
||||||
|
map.setPaintProperty(
|
||||||
|
"pair-range-ml",
|
||||||
|
"line-width",
|
||||||
|
["case", pairHighlightExpr, 1.6, 1.0] as never,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (map.getLayer("fleet-circles-ml-fill")) {
|
||||||
|
map.setPaintProperty(
|
||||||
|
"fleet-circles-ml-fill",
|
||||||
|
"fill-color",
|
||||||
|
[
|
||||||
|
"case",
|
||||||
|
fleetHighlightExpr,
|
||||||
|
"rgba(245,158,11,0.24)",
|
||||||
|
"rgba(245,158,11,0.02)",
|
||||||
|
] as never,
|
||||||
|
);
|
||||||
|
map.setPaintProperty(
|
||||||
|
"fleet-circles-ml-fill",
|
||||||
|
"fill-opacity",
|
||||||
|
["case", fleetHighlightExpr, 0.7, 0.28] as never,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (map.getLayer("fleet-circles-ml")) {
|
||||||
|
map.setPaintProperty(
|
||||||
|
"fleet-circles-ml",
|
||||||
|
"line-color",
|
||||||
|
["case", fleetHighlightExpr, "rgba(245,158,11,0.95)", "rgba(245,158,11,0.65)"] as never,
|
||||||
|
);
|
||||||
|
map.setPaintProperty(
|
||||||
|
"fleet-circles-ml",
|
||||||
|
"line-width",
|
||||||
|
["case", fleetHighlightExpr, 2, 1.1] as never,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}, [projection, hoveredFleetMmsiList, hoveredFleetOwnerKeyList, hoveredPairMmsiList]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map) return;
|
||||||
|
const stop = onMapStyleReady(map, updateGlobeOverlayPaintStates);
|
||||||
|
updateGlobeOverlayPaintStates();
|
||||||
|
return () => {
|
||||||
|
stop();
|
||||||
|
};
|
||||||
|
}, [mapSyncEpoch, hoveredFleetMmsiList, hoveredFleetOwnerKeyList, hoveredPairMmsiList, projection, updateGlobeOverlayPaintStates]);
|
||||||
|
|
||||||
|
const shipLayerData = useMemo(() => {
|
||||||
|
if (shipData.length === 0) return shipData;
|
||||||
|
const layer = [...shipData];
|
||||||
|
layer.sort((a, b) => {
|
||||||
|
const aPriority = a.mmsi === selectedMmsi ? 3 : isHighlightedMmsi(a.mmsi) ? 2 : 0;
|
||||||
|
const bPriority = b.mmsi === selectedMmsi ? 3 : isHighlightedMmsi(b.mmsi) ? 2 : 0;
|
||||||
|
if (aPriority !== bPriority) return aPriority - bPriority;
|
||||||
|
return a.mmsi - b.mmsi;
|
||||||
|
});
|
||||||
|
return layer;
|
||||||
|
}, [shipData, isHighlightedMmsi, selectedMmsi]);
|
||||||
|
|
||||||
const clearGlobeTooltip = useCallback(() => {
|
const clearGlobeTooltip = useCallback(() => {
|
||||||
if (!mapTooltipRef.current) return;
|
if (!mapTooltipRef.current) return;
|
||||||
@ -3178,15 +3474,15 @@ export function Map3D({
|
|||||||
if (!map) return;
|
if (!map) return;
|
||||||
|
|
||||||
const clearDeckGlobeHoverState = () => {
|
const clearDeckGlobeHoverState = () => {
|
||||||
setHoveredDeckMmsiSet((prev) => (prev.length === 0 ? prev : []));
|
clearDeckHoverMmsi();
|
||||||
setHoveredDeckPairMmsiSet((prev) => (prev.length === 0 ? prev : []));
|
clearDeckHoverPairs();
|
||||||
setHoveredZoneId((prev) => (prev === null ? prev : null));
|
setHoveredZoneId((prev) => (prev === null ? prev : null));
|
||||||
clearMapFleetHoverState();
|
clearMapFleetHoverState();
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetGlobeHoverStates = () => {
|
const resetGlobeHoverStates = () => {
|
||||||
setHoveredDeckMmsiSet((prev) => (prev.length === 0 ? prev : []));
|
clearDeckHoverMmsi();
|
||||||
setHoveredDeckPairMmsiSet((prev) => (prev.length === 0 ? prev : []));
|
clearDeckHoverPairs();
|
||||||
setHoveredZoneId((prev) => (prev === null ? prev : null));
|
setHoveredZoneId((prev) => (prev === null ? prev : null));
|
||||||
clearMapFleetHoverState();
|
clearMapFleetHoverState();
|
||||||
};
|
};
|
||||||
@ -3287,36 +3583,36 @@ export function Map3D({
|
|||||||
|
|
||||||
if (isShipLayer) {
|
if (isShipLayer) {
|
||||||
const mmsi = toIntMmsi(props.mmsi);
|
const mmsi = toIntMmsi(props.mmsi);
|
||||||
setHoveredDeckMmsiSingle(mmsi);
|
setDeckHoverMmsi(mmsi == null ? [] : [mmsi]);
|
||||||
setHoveredDeckPairMmsiSet((prev) => (prev.length === 0 ? prev : []));
|
clearDeckHoverPairs();
|
||||||
clearMapFleetHoverState();
|
clearMapFleetHoverState();
|
||||||
setHoveredZoneId((prev) => (prev === null ? prev : null));
|
setHoveredZoneId((prev) => (prev === null ? prev : null));
|
||||||
} else if (isPairLayer) {
|
} else if (isPairLayer) {
|
||||||
const aMmsi = toIntMmsi(props.aMmsi);
|
const aMmsi = toIntMmsi(props.aMmsi);
|
||||||
const bMmsi = toIntMmsi(props.bMmsi);
|
const bMmsi = toIntMmsi(props.bMmsi);
|
||||||
setHoveredDeckPairs([...(aMmsi == null ? [] : [aMmsi]), ...(bMmsi == null ? [] : [bMmsi])]);
|
setDeckHoverPairs([...(aMmsi == null ? [] : [aMmsi]), ...(bMmsi == null ? [] : [bMmsi])]);
|
||||||
setHoveredDeckMmsiSet((prev) => (prev.length === 0 ? prev : []));
|
clearDeckHoverMmsi();
|
||||||
clearMapFleetHoverState();
|
clearMapFleetHoverState();
|
||||||
setHoveredZoneId((prev) => (prev === null ? prev : null));
|
setHoveredZoneId((prev) => (prev === null ? prev : null));
|
||||||
} else if (isFcLayer) {
|
} else if (isFcLayer) {
|
||||||
const from = toIntMmsi(props.fcMmsi);
|
const from = toIntMmsi(props.fcMmsi);
|
||||||
const to = toIntMmsi(props.otherMmsi);
|
const to = toIntMmsi(props.otherMmsi);
|
||||||
const fromTo = [from, to].filter((v): v is number => v != null);
|
const fromTo = [from, to].filter((v): v is number => v != null);
|
||||||
setHoveredDeckPairs(fromTo);
|
setDeckHoverPairs(fromTo);
|
||||||
setHoveredDeckMmsiSet((prev) => (equalNumberArrays(prev, fromTo) ? prev : fromTo));
|
setDeckHoverMmsi(fromTo);
|
||||||
clearMapFleetHoverState();
|
clearMapFleetHoverState();
|
||||||
setHoveredZoneId((prev) => (prev === null ? prev : null));
|
setHoveredZoneId((prev) => (prev === null ? prev : null));
|
||||||
} else if (isFleetLayer) {
|
} else if (isFleetLayer) {
|
||||||
const ownerKey = String(props.ownerKey ?? "");
|
const ownerKey = String(props.ownerKey ?? "");
|
||||||
const list = normalizeMmsiList(props.vesselMmsis);
|
const list = normalizeMmsiList(props.vesselMmsis);
|
||||||
setMapFleetHoverState(ownerKey || null, list);
|
setMapFleetHoverState(ownerKey || null, list);
|
||||||
setHoveredDeckMmsiSet((prev) => (prev.length === 0 ? prev : []));
|
clearDeckHoverMmsi();
|
||||||
setHoveredDeckPairMmsiSet((prev) => (prev.length === 0 ? prev : []));
|
clearDeckHoverPairs();
|
||||||
setHoveredZoneId((prev) => (prev === null ? prev : null));
|
setHoveredZoneId((prev) => (prev === null ? prev : null));
|
||||||
} else if (isZoneLayer) {
|
} else if (isZoneLayer) {
|
||||||
clearMapFleetHoverState();
|
clearMapFleetHoverState();
|
||||||
setHoveredDeckMmsiSet((prev) => (prev.length === 0 ? prev : []));
|
clearDeckHoverMmsi();
|
||||||
setHoveredDeckPairMmsiSet((prev) => (prev.length === 0 ? prev : []));
|
clearDeckHoverPairs();
|
||||||
const zoneId = getZoneIdFromProps(props);
|
const zoneId = getZoneIdFromProps(props);
|
||||||
setHoveredZoneId(zoneId || null);
|
setHoveredZoneId(zoneId || null);
|
||||||
} else {
|
} else {
|
||||||
@ -3358,8 +3654,10 @@ export function Map3D({
|
|||||||
buildGlobeFeatureTooltip,
|
buildGlobeFeatureTooltip,
|
||||||
clearGlobeTooltip,
|
clearGlobeTooltip,
|
||||||
clearMapFleetHoverState,
|
clearMapFleetHoverState,
|
||||||
setHoveredDeckPairs,
|
clearDeckHoverPairs,
|
||||||
setHoveredDeckMmsiSingle,
|
clearDeckHoverMmsi,
|
||||||
|
setDeckHoverPairs,
|
||||||
|
setDeckHoverMmsi,
|
||||||
setMapFleetHoverState,
|
setMapFleetHoverState,
|
||||||
setGlobeTooltip,
|
setGlobeTooltip,
|
||||||
]);
|
]);
|
||||||
@ -3369,6 +3667,18 @@ export function Map3D({
|
|||||||
return shipData.filter((t) => legacyHits.has(t.mmsi));
|
return shipData.filter((t) => legacyHits.has(t.mmsi));
|
||||||
}, [shipData, legacyHits]);
|
}, [shipData, legacyHits]);
|
||||||
|
|
||||||
|
const legacyTargetsOrdered = useMemo(() => {
|
||||||
|
if (legacyTargets.length === 0) return legacyTargets;
|
||||||
|
const layer = [...legacyTargets];
|
||||||
|
layer.sort((a, b) => {
|
||||||
|
const aPriority = a.mmsi === selectedMmsi ? 3 : isHighlightedMmsi(a.mmsi) ? 2 : 0;
|
||||||
|
const bPriority = b.mmsi === selectedMmsi ? 3 : isHighlightedMmsi(b.mmsi) ? 2 : 0;
|
||||||
|
if (aPriority !== bPriority) return aPriority - bPriority;
|
||||||
|
return a.mmsi - b.mmsi;
|
||||||
|
});
|
||||||
|
return layer;
|
||||||
|
}, [legacyTargets, isHighlightedMmsi, selectedMmsi]);
|
||||||
|
|
||||||
const fcDashed = useMemo(() => {
|
const fcDashed = useMemo(() => {
|
||||||
const segs: DashSeg[] = [];
|
const segs: DashSeg[] = [];
|
||||||
for (const l of fcLinks || []) {
|
for (const l of fcLinks || []) {
|
||||||
@ -3405,14 +3715,16 @@ export function Map3D({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map || !fleetFocus) return;
|
if (!map || fleetFocusLon == null || fleetFocusLat == null || !Number.isFinite(fleetFocusLon) || !Number.isFinite(fleetFocusLat))
|
||||||
const [lon, lat] = fleetFocus.center;
|
return;
|
||||||
if (!Number.isFinite(lon) || !Number.isFinite(lat)) return;
|
const lon = fleetFocusLon;
|
||||||
|
const lat = fleetFocusLat;
|
||||||
|
const zoom = fleetFocusZoom ?? 10;
|
||||||
|
|
||||||
const apply = () => {
|
const apply = () => {
|
||||||
map.easeTo({
|
map.easeTo({
|
||||||
center: [lon, lat],
|
center: [lon, lat],
|
||||||
zoom: fleetFocus.zoom ?? 10,
|
zoom,
|
||||||
duration: 700,
|
duration: 700,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -3426,7 +3738,7 @@ export function Map3D({
|
|||||||
return () => {
|
return () => {
|
||||||
stop();
|
stop();
|
||||||
};
|
};
|
||||||
}, [fleetFocus?.id, fleetFocus?.center?.[0], fleetFocus?.center?.[1], fleetFocus?.zoom]);
|
}, [fleetFocusId, fleetFocusLon, fleetFocusLat, fleetFocusZoom]);
|
||||||
|
|
||||||
// Update Deck.gl layers
|
// Update Deck.gl layers
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -3452,9 +3764,7 @@ export function Map3D({
|
|||||||
const overlayParams = projection === "globe" ? GLOBE_OVERLAY_PARAMS : DEPTH_DISABLED_PARAMS;
|
const overlayParams = projection === "globe" ? GLOBE_OVERLAY_PARAMS : DEPTH_DISABLED_PARAMS;
|
||||||
const layers = [];
|
const layers = [];
|
||||||
const clearDeckHover = () => {
|
const clearDeckHover = () => {
|
||||||
setHoveredMmsiList([]);
|
touchDeckHoverState(false);
|
||||||
setHoveredDeckPairs([]);
|
|
||||||
clearMapFleetHoverState();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const toFleetMmsiList = (value: unknown) => {
|
const toFleetMmsiList = (value: unknown) => {
|
||||||
@ -3488,7 +3798,7 @@ export function Map3D({
|
|||||||
layers.push(
|
layers.push(
|
||||||
new HexagonLayer<AisTarget>({
|
new HexagonLayer<AisTarget>({
|
||||||
id: "density",
|
id: "density",
|
||||||
data: shipData,
|
data: shipLayerData,
|
||||||
pickable: true,
|
pickable: true,
|
||||||
extruded: true,
|
extruded: true,
|
||||||
radius: 2500,
|
radius: 2500,
|
||||||
@ -3525,11 +3835,12 @@ export function Map3D({
|
|||||||
clearDeckHover();
|
clearDeckHover();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
touchDeckHoverState(true);
|
||||||
const p = info.object as PairRangeCircle;
|
const p = info.object as PairRangeCircle;
|
||||||
const aMmsi = p.aMmsi;
|
const aMmsi = p.aMmsi;
|
||||||
const bMmsi = p.bMmsi;
|
const bMmsi = p.bMmsi;
|
||||||
setHoveredDeckPairs([aMmsi, bMmsi]);
|
setDeckHoverPairs([aMmsi, bMmsi]);
|
||||||
setHoveredMmsiList([aMmsi, bMmsi]);
|
setDeckHoverMmsi([aMmsi, bMmsi]);
|
||||||
clearMapFleetHoverState();
|
clearMapFleetHoverState();
|
||||||
},
|
},
|
||||||
onClick: (info) => {
|
onClick: (info) => {
|
||||||
@ -3574,9 +3885,10 @@ export function Map3D({
|
|||||||
clearDeckHover();
|
clearDeckHover();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
touchDeckHoverState(true);
|
||||||
const obj = info.object as PairLink;
|
const obj = info.object as PairLink;
|
||||||
setHoveredDeckPairs([obj.aMmsi, obj.bMmsi]);
|
setDeckHoverPairs([obj.aMmsi, obj.bMmsi]);
|
||||||
setHoveredMmsiList([obj.aMmsi, obj.bMmsi]);
|
setDeckHoverMmsi([obj.aMmsi, obj.bMmsi]);
|
||||||
clearMapFleetHoverState();
|
clearMapFleetHoverState();
|
||||||
},
|
},
|
||||||
onClick: (info) => {
|
onClick: (info) => {
|
||||||
@ -3622,15 +3934,16 @@ export function Map3D({
|
|||||||
clearDeckHover();
|
clearDeckHover();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
touchDeckHoverState(true);
|
||||||
const obj = info.object as DashSeg;
|
const obj = info.object as DashSeg;
|
||||||
const aMmsi = obj.fromMmsi;
|
const aMmsi = obj.fromMmsi;
|
||||||
const bMmsi = obj.toMmsi;
|
const bMmsi = obj.toMmsi;
|
||||||
if (aMmsi == null || bMmsi == null) {
|
if (aMmsi == null || bMmsi == null) {
|
||||||
setHoveredMmsiList([]);
|
clearDeckHover();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setHoveredDeckPairs([aMmsi, bMmsi]);
|
setDeckHoverPairs([aMmsi, bMmsi]);
|
||||||
setHoveredMmsiList([aMmsi, bMmsi]);
|
setDeckHoverMmsi([aMmsi, bMmsi]);
|
||||||
clearMapFleetHoverState();
|
clearMapFleetHoverState();
|
||||||
},
|
},
|
||||||
onClick: (info) => {
|
onClick: (info) => {
|
||||||
@ -3679,11 +3992,12 @@ export function Map3D({
|
|||||||
clearDeckHover();
|
clearDeckHover();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
touchDeckHoverState(true);
|
||||||
const obj = info.object as FleetCircle;
|
const obj = info.object as FleetCircle;
|
||||||
const list = toFleetMmsiList(obj.vesselMmsis);
|
const list = toFleetMmsiList(obj.vesselMmsis);
|
||||||
setMapFleetHoverState(obj.ownerKey || null, list);
|
setMapFleetHoverState(obj.ownerKey || null, list);
|
||||||
setHoveredMmsiList(list);
|
setDeckHoverMmsi(list);
|
||||||
setHoveredDeckPairs([]);
|
clearDeckHoverPairs();
|
||||||
},
|
},
|
||||||
onClick: (info) => {
|
onClick: (info) => {
|
||||||
if (!info.object) return;
|
if (!info.object) return;
|
||||||
@ -3735,7 +4049,7 @@ export function Map3D({
|
|||||||
layers.push(
|
layers.push(
|
||||||
new ScatterplotLayer<AisTarget>({
|
new ScatterplotLayer<AisTarget>({
|
||||||
id: "legacy-halo",
|
id: "legacy-halo",
|
||||||
data: legacyTargets,
|
data: legacyTargetsOrdered,
|
||||||
pickable: false,
|
pickable: false,
|
||||||
billboard: false,
|
billboard: false,
|
||||||
// This ring is most prone to z-fighting, so force it into pure painter's-order rendering.
|
// This ring is most prone to z-fighting, so force it into pure painter's-order rendering.
|
||||||
@ -3778,7 +4092,7 @@ export function Map3D({
|
|||||||
layers.push(
|
layers.push(
|
||||||
new IconLayer<AisTarget>({
|
new IconLayer<AisTarget>({
|
||||||
id: "ships",
|
id: "ships",
|
||||||
data: shipData,
|
data: shipLayerData,
|
||||||
pickable: true,
|
pickable: true,
|
||||||
// Keep icons horizontal on the sea surface when view is pitched/rotated.
|
// Keep icons horizontal on the sea surface when view is pitched/rotated.
|
||||||
billboard: false,
|
billboard: false,
|
||||||
@ -3810,9 +4124,10 @@ export function Map3D({
|
|||||||
clearDeckHover();
|
clearDeckHover();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
touchDeckHoverState(true);
|
||||||
const obj = info.object as AisTarget;
|
const obj = info.object as AisTarget;
|
||||||
setHoveredMmsiList([obj.mmsi]);
|
setDeckHoverMmsi([obj.mmsi]);
|
||||||
setHoveredDeckPairs([]);
|
clearDeckHoverPairs();
|
||||||
clearMapFleetHoverState();
|
clearMapFleetHoverState();
|
||||||
},
|
},
|
||||||
onClick: (info) => {
|
onClick: (info) => {
|
||||||
@ -3969,7 +4284,8 @@ export function Map3D({
|
|||||||
applyDeckProps();
|
applyDeckProps();
|
||||||
}, [
|
}, [
|
||||||
projection,
|
projection,
|
||||||
shipData,
|
shipLayerData,
|
||||||
|
legacyTargetsOrdered,
|
||||||
baseMap,
|
baseMap,
|
||||||
zones,
|
zones,
|
||||||
selectedMmsi,
|
selectedMmsi,
|
||||||
@ -3993,12 +4309,18 @@ export function Map3D({
|
|||||||
hoveredFleetSignature,
|
hoveredFleetSignature,
|
||||||
hoveredPairSignature,
|
hoveredPairSignature,
|
||||||
hoveredFleetOwnerKey,
|
hoveredFleetOwnerKey,
|
||||||
highlightedMmsiSet,
|
highlightedMmsiSetCombined,
|
||||||
|
onToggleHighlightMmsi,
|
||||||
isHighlightedMmsi,
|
isHighlightedMmsi,
|
||||||
isHighlightedFleet,
|
isHighlightedFleet,
|
||||||
isHighlightedPair,
|
isHighlightedPair,
|
||||||
|
setDeckHoverMmsi,
|
||||||
|
clearDeckHoverMmsi,
|
||||||
|
setDeckHoverPairs,
|
||||||
|
clearDeckHoverPairs,
|
||||||
clearMapFleetHoverState,
|
clearMapFleetHoverState,
|
||||||
setMapFleetHoverState,
|
setMapFleetHoverState,
|
||||||
|
touchDeckHoverState,
|
||||||
ensureMercatorOverlay,
|
ensureMercatorOverlay,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -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