fix(map): restore subject flight and search inset
This commit is contained in:
@@ -375,6 +375,7 @@ export type CesiumMapRendererHandle = {
|
|||||||
getCameraView: () => MapCameraView | null;
|
getCameraView: () => MapCameraView | null;
|
||||||
fitRuntimeEntities: (entityIds?: string[]) => boolean;
|
fitRuntimeEntities: (entityIds?: string[]) => boolean;
|
||||||
focusRuntimeEntity: (entityId: string) => boolean;
|
focusRuntimeEntity: (entityId: string) => boolean;
|
||||||
|
focusSubjectCoordinates: (longitude: number, latitude: number) => boolean;
|
||||||
focusCoordinates: (longitude: number, latitude: number) => boolean;
|
focusCoordinates: (longitude: number, latitude: number) => boolean;
|
||||||
focusGridSector: (sector: GridSectorSelection) => boolean;
|
focusGridSector: (sector: GridSectorSelection) => boolean;
|
||||||
focusGridMajorTile: (tile: GridMajorTileSelection) => boolean;
|
focusGridMajorTile: (tile: GridMajorTileSelection) => boolean;
|
||||||
@@ -2744,6 +2745,25 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
|||||||
return true;
|
return true;
|
||||||
}, [runtimeEntities]);
|
}, [runtimeEntities]);
|
||||||
|
|
||||||
|
const focusSubjectCoordinates = useCallback((longitude: number, latitude: number) => {
|
||||||
|
const viewer = viewerRef.current;
|
||||||
|
if (!viewer || viewer.isDestroyed()
|
||||||
|
|| !Number.isFinite(longitude) || longitude < -180 || longitude > 180
|
||||||
|
|| !Number.isFinite(latitude) || latitude < -90 || latitude > 90) return false;
|
||||||
|
|
||||||
|
// Selection can make a filtered subject visible and request navigation in
|
||||||
|
// the same React tick. In that case Cesium has not rebuilt its data source
|
||||||
|
// yet, so entity lookup cannot be the only way to fly to the subject.
|
||||||
|
// Coordinates from the authorized search/runtime fact provide the same
|
||||||
|
// explicit, inspectable camera composition without clearing user filters.
|
||||||
|
const target = Cartesian3.fromDegrees(longitude, latitude, 0, viewer.scene.globe.ellipsoid);
|
||||||
|
viewer.camera.flyToBoundingSphere(new BoundingSphere(target, 1), {
|
||||||
|
duration: 0.45,
|
||||||
|
offset: new HeadingPitchRange(0, -0.9, 8_000),
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
startSpiralAnimation,
|
startSpiralAnimation,
|
||||||
stopSpiralAnimation,
|
stopSpiralAnimation,
|
||||||
@@ -2754,9 +2774,10 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
|||||||
fitRuntimeEntities,
|
fitRuntimeEntities,
|
||||||
focusCoordinates,
|
focusCoordinates,
|
||||||
focusRuntimeEntity,
|
focusRuntimeEntity,
|
||||||
|
focusSubjectCoordinates,
|
||||||
focusGridSector,
|
focusGridSector,
|
||||||
focusGridMajorTile,
|
focusGridMajorTile,
|
||||||
}), [fitRuntimeEntities, focusCoordinates, focusGridMajorTile, focusGridSector, focusRuntimeEntity, startSpiralAnimation, stopSpiralAnimation]);
|
}), [fitRuntimeEntities, focusCoordinates, focusGridMajorTile, focusGridSector, focusRuntimeEntity, focusSubjectCoordinates, startSpiralAnimation, stopSpiralAnimation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stopForPageLeave = () => stopSpiralAnimation("stopped");
|
const stopForPageLeave = () => stopSpiralAnimation("stopped");
|
||||||
|
|||||||
@@ -332,6 +332,15 @@ function mapFactSectorScopeValue(fact: MapRuntimeFact, field: string) {
|
|||||||
return normalizedSectorScopeValue(fact.attributes[field]) ?? MAP_SCOPE_MISSING_VALUE;
|
return normalizedSectorScopeValue(fact.attributes[field]) ?? MAP_SCOPE_MISSING_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapFactPointCoordinates(fact: MapRuntimeFact | undefined): [number, number] | null {
|
||||||
|
if (fact?.geometry?.type !== "Point") return null;
|
||||||
|
const [longitude, latitude] = fact.geometry.coordinates;
|
||||||
|
return Number.isFinite(longitude) && longitude >= -180 && longitude <= 180
|
||||||
|
&& Number.isFinite(latitude) && latitude >= -90 && latitude <= 90
|
||||||
|
? [longitude, latitude]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
function mapFactInsideGridSector(
|
function mapFactInsideGridSector(
|
||||||
fact: MapRuntimeFact,
|
fact: MapRuntimeFact,
|
||||||
selection: GridSectorSelection,
|
selection: GridSectorSelection,
|
||||||
@@ -1477,10 +1486,21 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
|||||||
activateWorkspaceWindow("subject-card");
|
activateWorkspaceWindow("subject-card");
|
||||||
}, [activateWorkspaceWindow, dataProductBindings, selectable, subjectDetailProfiles]);
|
}, [activateWorkspaceWindow, dataProductBindings, selectable, subjectDetailProfiles]);
|
||||||
|
|
||||||
|
const focusSubject = useCallback((entityId: string, coordinates?: readonly [number, number]) => {
|
||||||
|
const renderer = mapRendererRef.current;
|
||||||
|
if (!renderer) return false;
|
||||||
|
if (renderer.focusRuntimeEntity(entityId)) return true;
|
||||||
|
const fallbackCoordinates = coordinates
|
||||||
|
?? mapFactPointCoordinates(selectable.find((entity) => entity.id === entityId)?.fact);
|
||||||
|
return fallbackCoordinates
|
||||||
|
? renderer.focusSubjectCoordinates(fallbackCoordinates[0], fallbackCoordinates[1])
|
||||||
|
: false;
|
||||||
|
}, [selectable]);
|
||||||
|
|
||||||
const handleSelectAndFocus = useCallback((entityId: string) => {
|
const handleSelectAndFocus = useCallback((entityId: string) => {
|
||||||
handleSelect(entityId);
|
handleSelect(entityId);
|
||||||
mapRendererRef.current?.focusRuntimeEntity(entityId);
|
focusSubject(entityId);
|
||||||
}, [handleSelect]);
|
}, [focusSubject, handleSelect]);
|
||||||
|
|
||||||
const handleSearchResult = useCallback((result: (typeof mapSearchResults)[number]) => {
|
const handleSearchResult = useCallback((result: (typeof mapSearchResults)[number]) => {
|
||||||
if (result.selectable) {
|
if (result.selectable) {
|
||||||
@@ -1494,14 +1514,12 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
|||||||
layer.id === result.bindingId ? { ...layer, visible: true } : layer
|
layer.id === result.bindingId ? { ...layer, visible: true } : layer
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
if (!mapRendererRef.current?.focusRuntimeEntity(result.entityId)) {
|
focusSubject(result.entityId, result.coordinates);
|
||||||
mapRendererRef.current?.focusCoordinates(result.coordinates[0], result.coordinates[1]);
|
|
||||||
}
|
|
||||||
setSearchOpen(false);
|
setSearchOpen(false);
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setRemoteSearchQuery("");
|
setRemoteSearchQuery("");
|
||||||
setSearchActiveIndex(0);
|
setSearchActiveIndex(0);
|
||||||
}, [handleSelect, mapSearchResults]);
|
}, [focusSubject, handleSelect]);
|
||||||
|
|
||||||
const handleSearchKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {
|
const handleSearchKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
|
|||||||
@@ -842,7 +842,6 @@ textarea {
|
|||||||
|
|
||||||
.catalog-map-fixture__toolbar[data-search-open] {
|
.catalog-map-fixture__toolbar[data-search-open] {
|
||||||
width: min(42rem, calc(100% - 2rem));
|
width: min(42rem, calc(100% - 2rem));
|
||||||
padding-right: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.catalog-map-search {
|
.catalog-map-search {
|
||||||
|
|||||||
@@ -108,11 +108,18 @@ test("facet tree selection focuses the subject without resetting a compatible de
|
|||||||
|
|
||||||
assert.match(preview, /const \[expandedFacetRows, setExpandedFacetRows\]/);
|
assert.match(preview, /const \[expandedFacetRows, setExpandedFacetRows\]/);
|
||||||
assert.match(preview, /const handleSelectAndFocus = useCallback/);
|
assert.match(preview, /const handleSelectAndFocus = useCallback/);
|
||||||
assert.match(preview, /mapRendererRef\.current\?\.focusRuntimeEntity\(entityId\)/);
|
assert.match(preview, /const focusSubject = useCallback/);
|
||||||
|
assert.match(preview, /if \(renderer\.focusRuntimeEntity\(entityId\)\) return true/);
|
||||||
|
assert.match(preview, /mapFactPointCoordinates\(selectable\.find/);
|
||||||
|
assert.match(preview, /renderer\.focusSubjectCoordinates\(fallbackCoordinates\[0\], fallbackCoordinates\[1\]\)/);
|
||||||
|
assert.match(preview, /focusSubject\(entityId\)/);
|
||||||
|
assert.match(preview, /focusSubject\(result\.entityId, result\.coordinates\)/);
|
||||||
assert.match(preview, /setSubjectCardTabId\(\(current\) =>/);
|
assert.match(preview, /setSubjectCardTabId\(\(current\) =>/);
|
||||||
assert.match(preview, /profile\?\.tabs\.some\(\(tab\) => tab\.id === current\)/);
|
assert.match(preview, /profile\?\.tabs\.some\(\(tab\) => tab\.id === current\)/);
|
||||||
assert.match(preview, /\(profile\?\.defaultTabId \?\? "overview"\)/);
|
assert.match(preview, /\(profile\?\.defaultTabId \?\? "overview"\)/);
|
||||||
assert.match(renderer, /void viewer\.flyTo\(entity, \{/);
|
assert.match(renderer, /void viewer\.flyTo\(entity, \{/);
|
||||||
|
assert.match(renderer, /const focusSubjectCoordinates = useCallback/);
|
||||||
|
assert.match(renderer, /viewer\.camera\.flyToBoundingSphere\(new BoundingSphere\(target, 1\), \{/);
|
||||||
assert.match(renderer, /duration: 0\.45/);
|
assert.match(renderer, /duration: 0\.45/);
|
||||||
assert.match(renderer, /offset: new HeadingPitchRange\(0, -0\.9, 8_000\)/);
|
assert.match(renderer, /offset: new HeadingPitchRange\(0, -0\.9, 8_000\)/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
|
||||||
import { buildMapSearchIndex, searchMapSubjects } from "../apps/catalog/src/mapSearch.mjs";
|
import { buildMapSearchIndex, searchMapSubjects } from "../apps/catalog/src/mapSearch.mjs";
|
||||||
|
|
||||||
const now = "2026-07-25T08:00:00.000Z";
|
const now = "2026-07-25T08:00:00.000Z";
|
||||||
@@ -101,3 +103,12 @@ test("reference subjects remain searchable by profile labels without a provider
|
|||||||
assert.equal(searchMapSubjects(index, "osm.node.1").at(0)?.groupTitle, "Метро");
|
assert.equal(searchMapSubjects(index, "osm.node.1").at(0)?.groupTitle, "Метро");
|
||||||
assert.deepEqual(searchMapSubjects(index, "provider_note"), []);
|
assert.deepEqual(searchMapSubjects(index, "provider_note"), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("expanded search keeps the canonical toolbar inset on every edge", async () => {
|
||||||
|
const styles = await readFile(new URL("../apps/catalog/src/styles.css", import.meta.url), "utf8");
|
||||||
|
const baseToolbar = styles.match(/\.catalog-map-fixture__toolbar\s*\{(?<body>[\s\S]*?)\n\}/)?.groups?.body ?? "";
|
||||||
|
const expandedToolbar = styles.match(/\.catalog-map-fixture__toolbar\[data-search-open\]\s*\{(?<body>[\s\S]*?)\n\}/)?.groups?.body ?? "";
|
||||||
|
|
||||||
|
assert.match(baseToolbar, /padding: 0\.4rem/);
|
||||||
|
assert.doesNotMatch(expandedToolbar, /padding-right:\s*0/);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user