refactor(map3d): isolate ship hover overlay for icon flicker reduction
This commit is contained in:
부모
54d33a8670
커밋
30e6e584ee
@ -575,6 +575,7 @@ const FLAT_SHIP_ICON_SIZE_HIGHLIGHTED = 25;
|
|||||||
const FLAT_LEGACY_HALO_RADIUS = 14;
|
const FLAT_LEGACY_HALO_RADIUS = 14;
|
||||||
const FLAT_LEGACY_HALO_RADIUS_SELECTED = 18;
|
const FLAT_LEGACY_HALO_RADIUS_SELECTED = 18;
|
||||||
const FLAT_LEGACY_HALO_RADIUS_HIGHLIGHTED = 16;
|
const FLAT_LEGACY_HALO_RADIUS_HIGHLIGHTED = 16;
|
||||||
|
const EMPTY_MMSI_SET = new Set<number>();
|
||||||
|
|
||||||
const GLOBE_OVERLAY_PARAMS = {
|
const GLOBE_OVERLAY_PARAMS = {
|
||||||
// In globe mode we want depth-testing against the globe so features on the far side don't draw through.
|
// In globe mode we want depth-testing against the globe so features on the far side don't draw through.
|
||||||
@ -1079,6 +1080,13 @@ export function Map3D({
|
|||||||
onHoverPair,
|
onHoverPair,
|
||||||
onClearPairHover,
|
onClearPairHover,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
void onHoverFleet;
|
||||||
|
void onClearFleetHover;
|
||||||
|
void onHoverMmsi;
|
||||||
|
void onClearMmsiHover;
|
||||||
|
void onHoverPair;
|
||||||
|
void onClearPairHover;
|
||||||
|
|
||||||
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);
|
||||||
const overlayRef = useRef<MapboxOverlay | null>(null);
|
const overlayRef = useRef<MapboxOverlay | null>(null);
|
||||||
@ -1196,6 +1204,18 @@ export function Map3D({
|
|||||||
(mmsi: number) => highlightedMmsiSetCombined.has(mmsi),
|
(mmsi: number) => highlightedMmsiSetCombined.has(mmsi),
|
||||||
[highlightedMmsiSetCombined],
|
[highlightedMmsiSetCombined],
|
||||||
);
|
);
|
||||||
|
const baseHighlightedMmsiSet = useMemo(() => {
|
||||||
|
const out = new Set<number>();
|
||||||
|
if (selectedMmsi != null) out.add(selectedMmsi);
|
||||||
|
externalHighlightedSetRef.forEach((value) => {
|
||||||
|
out.add(value);
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}, [selectedMmsi, externalHighlightedSetRef]);
|
||||||
|
const isBaseHighlightedMmsi = useCallback(
|
||||||
|
(mmsi: number) => baseHighlightedMmsiSet.has(mmsi),
|
||||||
|
[baseHighlightedMmsiSet],
|
||||||
|
);
|
||||||
|
|
||||||
const isHighlightedPair = useCallback(
|
const isHighlightedPair = useCallback(
|
||||||
(aMmsi: number, bMmsi: number) =>
|
(aMmsi: number, bMmsi: number) =>
|
||||||
@ -1274,12 +1294,6 @@ export function Map3D({
|
|||||||
setHoveredDeckFleetOwnerKey((prev) => (prev === ownerKey ? prev : ownerKey));
|
setHoveredDeckFleetOwnerKey((prev) => (prev === ownerKey ? prev : ownerKey));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onHoverFleetRef = useRef(onHoverFleet);
|
|
||||||
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 mapDeckMmsiHoverRef = useRef<number[]>([]);
|
||||||
const mapDeckPairHoverRef = useRef<number[]>([]);
|
const mapDeckPairHoverRef = useRef<number[]>([]);
|
||||||
const mapFleetHoverStateRef = useRef<{
|
const mapFleetHoverStateRef = useRef<{
|
||||||
@ -1287,43 +1301,20 @@ export function Map3D({
|
|||||||
vesselMmsis: number[];
|
vesselMmsis: number[];
|
||||||
}>({ ownerKey: null, vesselMmsis: [] });
|
}>({ ownerKey: null, vesselMmsis: [] });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onHoverFleetRef.current = onHoverFleet;
|
|
||||||
onClearFleetHoverRef.current = onClearFleetHover;
|
|
||||||
onHoverMmsiRef.current = onHoverMmsi;
|
|
||||||
onClearMmsiHoverRef.current = onClearMmsiHover;
|
|
||||||
onHoverPairRef.current = onHoverPair;
|
|
||||||
onClearPairHoverRef.current = onClearPairHover;
|
|
||||||
}, [onHoverFleet, onClearFleetHover, onHoverMmsi, onClearMmsiHover, onHoverPair, onClearPairHover]);
|
|
||||||
|
|
||||||
const clearMapFleetHoverState = useCallback(() => {
|
const clearMapFleetHoverState = useCallback(() => {
|
||||||
const nextOwner = null;
|
mapFleetHoverStateRef.current = { ownerKey: null, vesselMmsis: [] };
|
||||||
const prev = mapFleetHoverStateRef.current;
|
|
||||||
const shouldNotify = prev.ownerKey !== null || prev.vesselMmsis.length !== 0;
|
|
||||||
mapFleetHoverStateRef.current = { ownerKey: nextOwner, vesselMmsis: [] };
|
|
||||||
setHoveredDeckFleetOwner(null);
|
setHoveredDeckFleetOwner(null);
|
||||||
setHoveredDeckFleetMmsis([]);
|
setHoveredDeckFleetMmsis([]);
|
||||||
if (shouldNotify) {
|
|
||||||
onClearFleetHoverRef.current?.();
|
|
||||||
}
|
|
||||||
}, [setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis]);
|
}, [setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis]);
|
||||||
|
|
||||||
const clearDeckHoverPairs = useCallback(() => {
|
const clearDeckHoverPairs = useCallback(() => {
|
||||||
const prev = mapDeckPairHoverRef.current;
|
|
||||||
mapDeckPairHoverRef.current = [];
|
mapDeckPairHoverRef.current = [];
|
||||||
setHoveredDeckPairMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
|
setHoveredDeckPairMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
|
||||||
if (prev.length > 0) {
|
|
||||||
onClearPairHoverRef.current?.();
|
|
||||||
}
|
|
||||||
}, [setHoveredDeckPairMmsiSet]);
|
}, [setHoveredDeckPairMmsiSet]);
|
||||||
|
|
||||||
const clearDeckHoverMmsi = useCallback(() => {
|
const clearDeckHoverMmsi = useCallback(() => {
|
||||||
const prev = mapDeckMmsiHoverRef.current;
|
|
||||||
mapDeckMmsiHoverRef.current = [];
|
mapDeckMmsiHoverRef.current = [];
|
||||||
setHoveredDeckMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
|
setHoveredDeckMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
|
||||||
if (prev.length > 0) {
|
|
||||||
onClearMmsiHoverRef.current?.();
|
|
||||||
}
|
|
||||||
}, [setHoveredDeckMmsiSet]);
|
}, [setHoveredDeckMmsiSet]);
|
||||||
|
|
||||||
const scheduleDeckHoverResolve = useCallback(() => {
|
const scheduleDeckHoverResolve = useCallback(() => {
|
||||||
@ -1352,10 +1343,7 @@ export function Map3D({
|
|||||||
const normalized = makeUniqueSorted(next);
|
const normalized = makeUniqueSorted(next);
|
||||||
touchDeckHoverState(normalized.length > 0);
|
touchDeckHoverState(normalized.length > 0);
|
||||||
setHoveredDeckMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
setHoveredDeckMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
||||||
if (!equalNumberArrays(mapDeckMmsiHoverRef.current, normalized)) {
|
mapDeckMmsiHoverRef.current = normalized;
|
||||||
mapDeckMmsiHoverRef.current = normalized;
|
|
||||||
onHoverMmsiRef.current?.(normalized);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[setHoveredDeckMmsiSet, touchDeckHoverState],
|
[setHoveredDeckMmsiSet, touchDeckHoverState],
|
||||||
);
|
);
|
||||||
@ -1365,10 +1353,7 @@ export function Map3D({
|
|||||||
const normalized = makeUniqueSorted(next);
|
const normalized = makeUniqueSorted(next);
|
||||||
touchDeckHoverState(normalized.length > 0);
|
touchDeckHoverState(normalized.length > 0);
|
||||||
setHoveredDeckPairMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
setHoveredDeckPairMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
||||||
if (!equalNumberArrays(mapDeckPairHoverRef.current, normalized)) {
|
mapDeckPairHoverRef.current = normalized;
|
||||||
mapDeckPairHoverRef.current = normalized;
|
|
||||||
onHoverPairRef.current?.(normalized);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[setHoveredDeckPairMmsiSet, touchDeckHoverState],
|
[setHoveredDeckPairMmsiSet, touchDeckHoverState],
|
||||||
);
|
);
|
||||||
@ -1384,7 +1369,6 @@ export function Map3D({
|
|||||||
setHoveredDeckFleetOwner(ownerKey);
|
setHoveredDeckFleetOwner(ownerKey);
|
||||||
setHoveredDeckFleetMmsis(normalized);
|
setHoveredDeckFleetMmsis(normalized);
|
||||||
mapFleetHoverStateRef.current = { ownerKey, vesselMmsis: normalized };
|
mapFleetHoverStateRef.current = { ownerKey, vesselMmsis: normalized };
|
||||||
onHoverFleetRef.current?.(ownerKey, normalized);
|
|
||||||
},
|
},
|
||||||
[setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis, touchDeckHoverState],
|
[setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis, touchDeckHoverState],
|
||||||
);
|
);
|
||||||
@ -2360,7 +2344,7 @@ export function Map3D({
|
|||||||
const hull = clampNumber((isFiniteNumber(t.length) ? t.length : 0) + (isFiniteNumber(t.width) ? t.width : 0) * 3, 50, 420);
|
const hull = clampNumber((isFiniteNumber(t.length) ? t.length : 0) + (isFiniteNumber(t.width) ? t.width : 0) * 3, 50, 420);
|
||||||
const sizeScale = clampNumber(0.85 + (hull - 50) / 420, 0.82, 1.85);
|
const sizeScale = clampNumber(0.85 + (hull - 50) / 420, 0.82, 1.85);
|
||||||
const selected = t.mmsi === selectedMmsi;
|
const selected = t.mmsi === selectedMmsi;
|
||||||
const highlighted = isHighlightedMmsi(t.mmsi);
|
const highlighted = isBaseHighlightedMmsi(t.mmsi);
|
||||||
const selectedScale = selected ? 1.08 : 1;
|
const selectedScale = selected ? 1.08 : 1;
|
||||||
const highlightScale = highlighted ? 1.06 : 1;
|
const highlightScale = highlighted ? 1.06 : 1;
|
||||||
const iconScale = selected ? selectedScale : highlightScale;
|
const iconScale = selected ? selectedScale : highlightScale;
|
||||||
@ -2639,10 +2623,7 @@ export function Map3D({
|
|||||||
shipData,
|
shipData,
|
||||||
legacyHits,
|
legacyHits,
|
||||||
selectedMmsi,
|
selectedMmsi,
|
||||||
hoveredMmsiSetRef,
|
isBaseHighlightedMmsi,
|
||||||
hoveredFleetMmsiSetRef,
|
|
||||||
hoveredPairMmsiSetRef,
|
|
||||||
isHighlightedMmsi,
|
|
||||||
mapSyncEpoch,
|
mapSyncEpoch,
|
||||||
reorderGlobeFeatureLayers,
|
reorderGlobeFeatureLayers,
|
||||||
]);
|
]);
|
||||||
@ -3368,10 +3349,8 @@ export function Map3D({
|
|||||||
|
|
||||||
const shipLayerData = useMemo(() => {
|
const shipLayerData = useMemo(() => {
|
||||||
if (shipData.length === 0) return shipData;
|
if (shipData.length === 0) return shipData;
|
||||||
const layer = [...shipData];
|
return [...shipData];
|
||||||
layer.sort((a, b) => a.mmsi - b.mmsi);
|
}, [shipData]);
|
||||||
return layer;
|
|
||||||
}, [shipData, isHighlightedMmsi, selectedMmsi]);
|
|
||||||
|
|
||||||
const shipHighlightSet = useMemo(() => {
|
const shipHighlightSet = useMemo(() => {
|
||||||
const out = new Set(highlightedMmsiSetCombined);
|
const out = new Set(highlightedMmsiSetCombined);
|
||||||
@ -3380,9 +3359,9 @@ export function Map3D({
|
|||||||
}, [highlightedMmsiSetCombined, selectedMmsi]);
|
}, [highlightedMmsiSetCombined, selectedMmsi]);
|
||||||
|
|
||||||
const shipOverlayLayerData = useMemo(() => {
|
const shipOverlayLayerData = useMemo(() => {
|
||||||
if (shipHighlightSet.size === 0) return [];
|
if (shipLayerData.length === 0) return shipLayerData;
|
||||||
return shipLayerData.filter((target) => shipHighlightSet.has(target.mmsi));
|
return shipLayerData;
|
||||||
}, [shipLayerData, shipHighlightSet]);
|
}, [shipLayerData]);
|
||||||
|
|
||||||
const clearGlobeTooltip = useCallback(() => {
|
const clearGlobeTooltip = useCallback(() => {
|
||||||
if (!mapTooltipRef.current) return;
|
if (!mapTooltipRef.current) return;
|
||||||
@ -4029,7 +4008,7 @@ export function Map3D({
|
|||||||
d,
|
d,
|
||||||
null,
|
null,
|
||||||
legacyHits?.get(d.mmsi)?.shipCode ?? null,
|
legacyHits?.get(d.mmsi)?.shipCode ?? null,
|
||||||
new Set(),
|
EMPTY_MMSI_SET,
|
||||||
),
|
),
|
||||||
onHover: (info) => {
|
onHover: (info) => {
|
||||||
if (!info.object) {
|
if (!info.object) {
|
||||||
@ -4337,14 +4316,20 @@ export function Map3D({
|
|||||||
heading: d.heading,
|
heading: d.heading,
|
||||||
}),
|
}),
|
||||||
sizeUnits: "pixels",
|
sizeUnits: "pixels",
|
||||||
getSize: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? FLAT_SHIP_ICON_SIZE_SELECTED : FLAT_SHIP_ICON_SIZE_HIGHLIGHTED),
|
getSize: (d) => {
|
||||||
getColor: (d) =>
|
if (selectedMmsi != null && d.mmsi === selectedMmsi) return FLAT_SHIP_ICON_SIZE_SELECTED;
|
||||||
getShipColor(
|
if (shipHighlightSet.has(d.mmsi)) return FLAT_SHIP_ICON_SIZE_HIGHLIGHTED;
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
getColor: (d) => {
|
||||||
|
if (!shipHighlightSet.has(d.mmsi) && !(selectedMmsi != null && d.mmsi === selectedMmsi)) return [0, 0, 0, 0];
|
||||||
|
return getShipColor(
|
||||||
d,
|
d,
|
||||||
selectedMmsi,
|
selectedMmsi,
|
||||||
legacyHits?.get(d.mmsi)?.shipCode ?? null,
|
legacyHits?.get(d.mmsi)?.shipCode ?? null,
|
||||||
shipHighlightSet,
|
shipHighlightSet,
|
||||||
),
|
);
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -4603,11 +4588,15 @@ export function Map3D({
|
|||||||
sizeUnits: "pixels",
|
sizeUnits: "pixels",
|
||||||
getSize: (d) => {
|
getSize: (d) => {
|
||||||
if (selectedMmsi && d.mmsi === selectedMmsi) return FLAT_SHIP_ICON_SIZE_SELECTED;
|
if (selectedMmsi && d.mmsi === selectedMmsi) return FLAT_SHIP_ICON_SIZE_SELECTED;
|
||||||
if (isHighlightedMmsi(d.mmsi)) return FLAT_SHIP_ICON_SIZE_HIGHLIGHTED;
|
|
||||||
return FLAT_SHIP_ICON_SIZE;
|
return FLAT_SHIP_ICON_SIZE;
|
||||||
},
|
},
|
||||||
getColor: (d) =>
|
getColor: (d) =>
|
||||||
getShipColor(d, selectedMmsi, legacyHits?.get(d.mmsi)?.shipCode ?? null, highlightedMmsiSetCombined),
|
getShipColor(
|
||||||
|
d,
|
||||||
|
selectedMmsi,
|
||||||
|
legacyHits?.get(d.mmsi)?.shipCode ?? null,
|
||||||
|
EMPTY_MMSI_SET,
|
||||||
|
),
|
||||||
onHover: (info) => {
|
onHover: (info) => {
|
||||||
if (!info.object) {
|
if (!info.object) {
|
||||||
clearDeckHoverPairs();
|
clearDeckHoverPairs();
|
||||||
@ -4626,6 +4615,43 @@ export function Map3D({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings.showShips) {
|
||||||
|
globeLayers.push(
|
||||||
|
new IconLayer<AisTarget>({
|
||||||
|
id: "ships-globe-hover",
|
||||||
|
data: shipLayerData,
|
||||||
|
pickable: false,
|
||||||
|
billboard: false,
|
||||||
|
parameters: overlayParams,
|
||||||
|
iconAtlas: "/assets/ship.svg",
|
||||||
|
iconMapping: SHIP_ICON_MAPPING,
|
||||||
|
getIcon: () => "ship",
|
||||||
|
getPosition: (d) => [d.lon, d.lat] as [number, number],
|
||||||
|
getAngle: (d) =>
|
||||||
|
getDisplayHeading({
|
||||||
|
cog: d.cog,
|
||||||
|
heading: d.heading,
|
||||||
|
}),
|
||||||
|
sizeUnits: "pixels",
|
||||||
|
getSize: (d) => {
|
||||||
|
if (selectedMmsi != null && d.mmsi === selectedMmsi) return FLAT_SHIP_ICON_SIZE_SELECTED;
|
||||||
|
if (shipHighlightSet.has(d.mmsi)) return FLAT_SHIP_ICON_SIZE_HIGHLIGHTED;
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
getColor: (d) => {
|
||||||
|
if (!shipHighlightSet.has(d.mmsi) && !(selectedMmsi != null && d.mmsi === selectedMmsi)) return [0, 0, 0, 0];
|
||||||
|
return getShipColor(
|
||||||
|
d,
|
||||||
|
selectedMmsi,
|
||||||
|
legacyHits?.get(d.mmsi)?.shipCode ?? null,
|
||||||
|
shipHighlightSet,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
alphaCutoff: 0.05,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedLayers = sanitizeDeckLayerList(globeLayers);
|
const normalizedLayers = sanitizeDeckLayerList(globeLayers);
|
||||||
const globeDeckProps = {
|
const globeDeckProps = {
|
||||||
layers: normalizedLayers,
|
layers: normalizedLayers,
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user