refactor(map): split foundry workspace and cesium runtime

This commit is contained in:
Codex
2026-08-09 19:22:25 +03:00
parent 6c1265b8b6
commit b70e165910
39 changed files with 5959 additions and 4225 deletions
+4 -4
View File
@@ -48,14 +48,14 @@ test("Inspector hard-forces a raw integrated Select to the split presentation",
});
test("Foundry Inspector screens consume the semantic selection field", async () => {
const [mapPreview, catalog, coreStyles] = await Promise.all([
readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8"),
const [mapInspector, catalog, coreStyles] = await Promise.all([
readFile(new URL("../apps/catalog/src/mapInspectorSections.tsx", import.meta.url), "utf8"),
readFile(new URL("../apps/catalog/src/CatalogApp.tsx", import.meta.url), "utf8"),
readFile(new URL("../packages/ui-core/styles.css", import.meta.url), "utf8"),
]);
assert.match(mapPreview, /<InspectorSelectField\s+label="Профиль покрытия"/);
assert.doesNotMatch(mapPreview, /<ControlRow label="Профиль покрытия">/);
assert.match(mapInspector, /<InspectorSelectField\s+label="Профиль покрытия"/);
assert.doesNotMatch(mapInspector, /<ControlRow label="Профиль покрытия">/);
assert.equal((catalog.match(/<InspectorSelectField/g) ?? []).length, 2);
assert.match(coreStyles, /\.nodedc-inspector \.nodedc-control-row:has\(\.nodedc-select-anchor\)/);
});
+28 -25
View File
@@ -315,29 +315,32 @@ test("grid center is fixed and camera-independent, including for legacy camera-m
});
test("renderer uses fixed ENU sectors, angular graticules and a non-blank double-buffer swap", async () => {
const source = await readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8");
assert.match(source, /class GridLayerController/);
assert.match(source, /dataSourceDisplay\.ready/);
assert.match(source, /fixedGridOrigin\(presentation\)/);
assert.match(source, /Transforms\.eastNorthUpToFixedFrame\(anchor\)/);
assert.match(source, /localSectorAt\(/);
assert.match(source, /gridController\?\.pick\(worldPosition\)/);
assert.match(source, /lod\.stepKm \* 1_000/);
assert.match(source, /lod\.graticuleStepDegrees/);
assert.match(source, /lod\.graticuleLineWidthPx/);
assert.match(source, /arcType: ArcType\.RHUMB/);
assert.match(source, /clampToGround: false/);
assert.match(source, /majorLineWidthMultiplier/);
assert.match(source, /isLocalMajorLineIndex/);
assert.match(source, /isGraticuleMajorLineValue/);
assert.match(source, /new LabelCollection\(\)/);
assert.match(source, /const maximumLabels = 48/);
assert.match(source, /materializeLocalSelection/);
assert.match(source, /localVolumeAt\(/);
assert.match(source, /focusGridSector/);
assert.match(source, /selectedGridSector/);
assert.match(source, /mountResources\(resources\)[\s\S]*?const previous = this\.current;[\s\S]*?removeResources\(previous\.resources\)/);
assert.doesNotMatch(source, /function rebuildElevatedGrid[\s\S]*?entities\.removeAll\(\)/);
assert.doesNotMatch(source, /Math\.min\(40,[\s\S]*?safeRadiusKm \/ safeStepKm/);
assert.doesNotMatch(source, /snapGridCenter\(/);
const [renderer, gridLayer] = await Promise.all([
readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8"),
readFile(new URL("../apps/catalog/src/mapCesiumGridLayer.ts", import.meta.url), "utf8"),
]);
assert.match(gridLayer, /class GridLayerController/);
assert.match(gridLayer, /dataSourceDisplay\.ready/);
assert.match(gridLayer, /fixedGridOrigin\(presentation\)/);
assert.match(gridLayer, /Transforms\.eastNorthUpToFixedFrame\(anchor\)/);
assert.match(gridLayer, /localSectorAt\(/);
assert.match(renderer, /gridController\?\.pick\(worldPosition\)/);
assert.match(gridLayer, /lod\.stepKm \* 1_000/);
assert.match(gridLayer, /lod\.graticuleStepDegrees/);
assert.match(gridLayer, /lod\.graticuleLineWidthPx/);
assert.match(gridLayer, /arcType: ArcType\.RHUMB/);
assert.match(gridLayer, /clampToGround: false/);
assert.match(gridLayer, /majorLineWidthMultiplier/);
assert.match(gridLayer, /isLocalMajorLineIndex/);
assert.match(gridLayer, /isGraticuleMajorLineValue/);
assert.match(gridLayer, /new LabelCollection\(\)/);
assert.match(gridLayer, /const maximumLabels = 48/);
assert.match(gridLayer, /materializeLocalSelection/);
assert.match(gridLayer, /localVolumeAt\(/);
assert.match(renderer, /focusGridSector/);
assert.match(renderer, /selectedGridSector/);
assert.match(gridLayer, /mountResources\(resources\)[\s\S]*?const previous = this\.current;[\s\S]*?removeResources\(previous\.resources\)/);
assert.doesNotMatch(gridLayer, /function rebuildElevatedGrid[\s\S]*?entities\.removeAll\(\)/);
assert.doesNotMatch(gridLayer, /Math\.min\(40,[\s\S]*?safeRadiusKm \/ safeStepKm/);
assert.doesNotMatch(gridLayer, /snapGridCenter\(/);
});
+8 -8
View File
@@ -3,17 +3,17 @@ import { readFile } from "node:fs/promises";
import test from "node:test";
test("Cesium elevated targets keep their profile border while labels occlude them inside the scene overlay", async () => {
const [renderer, cesiumDisplay] = await Promise.all([
readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8"),
const [runtimeLayers, cesiumDisplay] = await Promise.all([
readFile(new URL("../apps/catalog/src/mapCesiumRuntimeLayers.ts", import.meta.url), "utf8"),
readFile(new URL("../node_modules/@cesium/engine/Source/DataSources/DataSourceDisplay.js", import.meta.url), "utf8"),
]);
assert.match(renderer, /BillboardGraphics/);
assert.match(renderer, /elevatedTargetImage\(\s*color,\s*Color\.fromCssColorString\(target\.outlineColor\)\.withAlpha\(target\.outlineOpacity\),\s*target\.outlineWidthPx,\s*target\.headSizePx/);
assert.match(renderer, /entity\.point = undefined;\s*entity\.billboard = new BillboardGraphics/);
assert.match(renderer, /disableDepthTestDistance: Number\.POSITIVE_INFINITY/);
assert.match(renderer, /outlineWidth: 0,\s*style: LabelStyle\.FILL/);
assert.doesNotMatch(renderer, /style: 2/);
assert.match(runtimeLayers, /BillboardGraphics/);
assert.match(runtimeLayers, /elevatedTargetImage\(\s*color,\s*Color\.fromCssColorString\(target\.outlineColor\)\.withAlpha\(target\.outlineOpacity\),\s*target\.outlineWidthPx,\s*target\.headSizePx/);
assert.match(runtimeLayers, /entity\.point = undefined;\s*entity\.billboard = new BillboardGraphics/);
assert.match(runtimeLayers, /disableDepthTestDistance: Number\.POSITIVE_INFINITY/);
assert.match(runtimeLayers, /outlineWidth: 0,\s*style: LabelStyle\.FILL/);
assert.doesNotMatch(runtimeLayers, /style: 2/);
assert.ok(cesiumDisplay.indexOf("new BillboardVisualizer") < cesiumDisplay.indexOf("new LabelVisualizer"));
});
+107 -80
View File
@@ -2,71 +2,92 @@ import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
test("Objects menu opens controllable groups from the row and keeps plain bindings as visibility layers", async () => {
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
const source = (path) => readFile(new URL(path, import.meta.url), "utf8");
assert.match(preview, /const toggleSubjectVisibility = \(bindingId: string\)/);
assert.match(preview, /role=\{hasControls \? "menuitem" : "menuitemcheckbox"\}/);
assert.match(preview, /if \(hasControls\) \{\s*openSubjectWindow\(summary\.bindingId\);\s*close\(\)/);
assert.match(preview, /toggleSubjectVisibility\(summary\.bindingId\)/);
assert.match(preview, /visible: !state\.visible/);
assert.match(preview, /слой скрыт · \$\{summary\.total\} объектов/);
assert.doesNotMatch(preview, /Фильтры и счётчики: \$\{summary\.displayName\}/);
test("Objects menu delegates controlled layers to the workspace reducer", async () => {
const [preview, toolbar, state] = await Promise.all([
source("../apps/catalog/src/MapFixturePreview.tsx"),
source("../apps/catalog/src/MapWorkspaceToolbar.tsx"),
source("../apps/catalog/src/mapWorkspaceState.mjs"),
]);
assert.match(toolbar, /role=\{hasControls \? "menuitem" : "menuitemcheckbox"\}/);
assert.match(toolbar, /onOpenSubjectWindow\(summary\.bindingId\)/);
assert.match(toolbar, /onToggleSubjectVisibility\(summary\.bindingId\)/);
assert.match(toolbar, /слой скрыт · \$\{summary\.total\} объектов/);
assert.match(preview, /type: "toggle-subject-visibility", bindingId/);
assert.match(state, /visible: !subject\.visible/);
});
test("a subject window exists only when the presentation profile exposes controls", async () => {
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
test("a subject window exists only when the MCP presentation profile exposes controls", async () => {
const [windows, model, state] = await Promise.all([
source("../apps/catalog/src/MapSubjectWorkspaceWindows.tsx"),
source("../apps/catalog/src/mapWorkspaceModel.mjs"),
source("../apps/catalog/src/mapWorkspaceState.mjs"),
]);
assert.match(preview, /function hasSubjectWindowControls\(profile: MapPresentationProfile\)/);
assert.match(preview, /if \(!state\?\.window\.open \|\| !hasSubjectWindowControls\(summary\.profile\)\) return null/);
assert.match(preview, /window: \{ \.\.\.state\.window, open: false \}/);
assert.match(model, /export function mapProfileHasSubjectWindowControls/);
assert.match(windows, /!state\?\.window\.open \|\| !mapProfileHasSubjectWindowControls\(summary\.profile\)/);
assert.match(windows, /type: "close-subject-window"/);
assert.match(state, /window: \{ \.\.\.subject\.window, open: false \}/);
});
test("hidden and visible layers have an explicit visual state", async () => {
const styles = await readFile(new URL("../apps/catalog/src/styles.css", import.meta.url), "utf8");
const styles = await source("../apps/catalog/src/styles.css");
assert.match(styles, /\.catalog-map-fixture__objects-menu-item\[data-visible\]/);
assert.match(styles, /\.catalog-map-fixture__objects-menu-item:not\(\[data-visible\]\)/);
});
test("facet controls and renderer consume the same canonical enabled-value state", async () => {
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
test("facet controls and renderer consume one canonical enabled-value state", async () => {
const [preview, windows, state] = await Promise.all([
source("../apps/catalog/src/MapFixturePreview.tsx"),
source("../apps/catalog/src/MapSubjectWorkspaceWindows.tsx"),
source("../apps/catalog/src/mapWorkspaceState.mjs"),
]);
assert.match(preview, /initialSubjectState\(initialLayout\?\.dataProductBindings[\s\S]*?presentationProfiles\)/);
assert.match(preview, /facets: profile \? normalizeMapPresentationFacetSelections\(state\.filters, profile\) : state\.filters/);
assert.match(preview, /filters: normalizeMapPresentationFacetSelections\(state\.filters, summary\.profile\)/);
assert.match(preview, /const active = mapPresentationFacetValueIsEnabled\(state\.filters, facet\.field, item\.value\)/);
assert.match(preview, /toggleMapPresentationFacetSelection\(filters, field, value, availableValues\)/);
assert.match(preview, /facet\.values\.map\(\(value\) => value\.value\)/);
assert.match(preview, /normalizeMapPresentationFacetSelections\(state\.filters, profile\)/);
assert.match(preview, /presentationFilters=\{rendererPresentationFilters\}/);
assert.match(windows, /mapPresentationFacetValueIsEnabled\(state\.filters, facet\.field, item\.value\)/);
assert.match(windows, /facet\.values\.map\(\(value\) => value\.value\)/);
assert.match(state, /toggleMapPresentationFacetSelection\(/);
});
test("joined detail aspects do not become independent map layers", async () => {
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
test("joined detail aspects enrich subjects but never become independent renderer layers", async () => {
const [preview, model] = await Promise.all([
source("../apps/catalog/src/MapFixturePreview.tsx"),
source("../apps/catalog/src/mapWorkspaceModel.mjs"),
]);
assert.match(preview, /dataProductBindings\.filter\(\(binding\) => !binding\.joinToBindingId\)/);
assert.match(preview, /runtimeBindings\.filter\(\(binding\) => primaryBindingIds\.has\(binding\.bindingId\)\)/);
assert.match(model, /filter\(\(binding\) => !binding\.joinToBindingId\)/);
assert.match(model, /primaryMapBindingConfigs\(bindingConfigs\)/);
assert.match(preview, /runtimeBindings=\{\[\.\.\.sectorScopedPrimaryRuntimeBindings, \.\.\.referenceRuntimeBindings\]\}/);
});
test("bounded map windows keep one stack while layers and settings use their canonical surfaces", async () => {
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
test("bounded map windows share one reducer-owned z-stack while settings and layers keep canonical surfaces", async () => {
const [preview, toolbar, windows, sectorWindow, state, contract] = await Promise.all([
source("../apps/catalog/src/MapFixturePreview.tsx"),
source("../apps/catalog/src/MapWorkspaceToolbar.tsx"),
source("../apps/catalog/src/MapSubjectWorkspaceWindows.tsx"),
source("../apps/catalog/src/MapSectorWorkspaceWindow.tsx"),
source("../apps/catalog/src/mapWorkspaceState.mjs"),
source("../apps/catalog/src/mapPageContract.ts"),
]);
assert.match(preview, /type MapWorkspaceWindowId = "sector" \| "subject-card" \| `binding:\$\{string\}`/);
assert.match(preview, /const activateWorkspaceWindow = useCallback/);
assert.match(preview, /onActivate=\{\(\) => activateWorkspaceWindow\(`binding:\$\{summary\.bindingId\}`\)\}/);
assert.doesNotMatch(preview, /onActivate=\{\(\) => openSubjectWindow\(summary\.bindingId\)\}/);
assert.match(preview, /sectorWindowZIndex[\s\S]*subjectCardZIndex/);
assert.match(contract, /type MapWorkspaceWindowId = "sector" \| "subject-card" \| `binding:\$\{string\}`/);
assert.match(state, /function activateWindow\(state, windowId\)/);
assert.match(state, /Math\.max\([\s\S]*state\.sector\.window\.zIndex[\s\S]*state\.subjectCard\.zIndex/);
assert.match(windows, /type: "activate-window", windowId: `binding:\$\{summary\.bindingId\}`/);
assert.match(sectorWindow, /onActivate=\{onActivate\}/);
assert.match(preview, /<ApplicationSidePanel[\s\S]*title="Настройки карты"[\s\S]*onClose=\{closeSettingsPanel\}/);
assert.match(preview, /createPortal\(headerActions, headerActionsHost\)/);
assert.doesNotMatch(preview, /settingsPanelPinned|onPinnedChange/);
assert.match(preview, /surfaceClassName="catalog-map-fixture__objects-menu catalog-map-fixture__layers-menu nodedc-map-glass"/);
assert.match(preview, /onOpenChange=\{setLayersOpen\}/);
assert.doesNotMatch(preview, /settingsWindowZIndex|layersWindowZIndex/);
assert.doesNotMatch(preview, /<Window\b/);
assert.match(toolbar, /surfaceClassName="catalog-map-fixture__objects-menu catalog-map-fixture__layers-menu nodedc-map-glass"/);
assert.match(toolbar, /onOpenChange=\{onOpenChange\}/);
assert.doesNotMatch(preview, /settingsPanelPinned|settingsWindowZIndex|layersWindowZIndex/);
});
test("selecting a point or HGeoZone preserves the active grid sector", async () => {
const renderer = await readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8");
const renderer = await source("../apps/catalog/src/CesiumMapRenderer.tsx");
const pointBranch = renderer.match(/if \(pickedId instanceof Entity[\s\S]*?return;\s*\}/)?.[0] ?? "";
const zoneBranch = renderer.match(/if \(\s*pickedId[\s\S]*?HGeoZonePickId\)\.entityId\);\s*return;\s*\}/)?.[0] ?? "";
@@ -77,59 +98,65 @@ test("selecting a point or HGeoZone preserves the active grid sector", async ()
assert.match(renderer, /gridController\?\.setSelection\(gridSelection\);\s*onGridSectorSelectRef\.current\?\.\(gridSelection\)/);
});
test("an active sector scopes point facts, exposes data facets and deactivates on close", async () => {
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
assert.match(preview, /function mapFactInsideGridSector/);
assert.match(preview, /localSectorAtGeodetic/);
assert.match(preview, /label="Скрыть объекты за сектором"/);
assert.match(preview, />Домены данных</);
assert.match(preview, />Провайдеры</);
assert.match(preview, />Типы объектов</);
assert.match(preview, />Объекты сектора</);
assert.match(preview, /onClose=\{deactivateGridSector\}/);
assert.match(preview, />\s*Деактивировать сектор\s*</);
assert.match(preview, /setSelectedGridSector\(null\)/);
});
test("reference stations are first-class Objects menu layers without provider settings actions", async () => {
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
assert.match(preview, /const referenceObjectSummaries = useMemo/);
assert.match(preview, /const objectLayerCount = presentationSummaries\.length \+ referenceObjectSummaries\.length/);
assert.match(preview, /referenceObjectSummaries\.map\(\(\{ layer, displayName, total \}\)/);
assert.match(preview, /candidate\.id === layer\.id \? \{ \.\.\.candidate, visible: !candidate\.visible \} : candidate/);
assert.match(preview, /role="menuitemcheckbox"/);
});
test("facet window auto-fits expanded and collapsed content inside map workspace", async () => {
const [preview, workspace] = await Promise.all([
readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8"),
readFile(new URL("../packages/ui-react/src/WorkspaceWindow.tsx", import.meta.url), "utf8"),
test("active sector scope is geometric, filterable and reducer-deactivated", async () => {
const [runtime, sectorWindow, state] = await Promise.all([
source("../apps/catalog/src/mapSectorRuntime.mjs"),
source("../apps/catalog/src/MapSectorWorkspaceWindow.tsx"),
source("../apps/catalog/src/mapWorkspaceState.mjs"),
]);
assert.match(preview, /minHeight=\{220\}\s*autoHeight/);
assert.match(runtime, /export function mapFactInsideGridSector/);
assert.match(runtime, /localSectorAtGeodetic/);
assert.match(sectorWindow, /label="Скрыть объекты за сектором"/);
assert.match(sectorWindow, /title="Домены данных"/);
assert.match(sectorWindow, /title="Провайдеры"/);
assert.match(sectorWindow, /title="Типы объектов"/);
assert.match(sectorWindow, /id="map-sector-objects-title">Объекты сектора/);
assert.match(sectorWindow, /onClose=\{onDeactivate\}/);
assert.match(sectorWindow, />\s*Деактивировать сектор\s*</);
assert.match(state, /case "deactivate-sector"/);
assert.match(state, /selectedSector: null/);
});
test("reference stations remain first-class visibility layers without provider settings actions", async () => {
const [preview, toolbar] = await Promise.all([
source("../apps/catalog/src/MapFixturePreview.tsx"),
source("../apps/catalog/src/MapWorkspaceToolbar.tsx"),
]);
assert.match(preview, /const referenceObjectSummaries = useMemo/);
assert.match(toolbar, /const objectLayerCount = summaries\.length \+ referenceSummaries\.length/);
assert.match(toolbar, /referenceSummaries\.map\(\(\{ layer, displayName, total \}\)/);
assert.match(toolbar, /onToggleReferenceLayer\(layer\.id\)/);
assert.match(toolbar, /role="menuitemcheckbox"/);
});
test("facet windows auto-fit expanded and collapsed content inside the map workspace", async () => {
const [windows, workspace] = await Promise.all([
source("../apps/catalog/src/MapSubjectWorkspaceWindows.tsx"),
source("../packages/ui-react/src/WorkspaceWindow.tsx"),
]);
assert.match(windows, /minHeight=\{220\}\s*autoHeight/);
assert.match(workspace, /autoHeight\?: boolean/);
assert.match(workspace, /content\.scrollHeight/);
assert.match(workspace, /availableHeight = Math\.max\(0, nextBounds\.height - normalized\.y\)/);
assert.match(workspace, /new ResizeObserver\(scheduleFit\)/);
});
test("facet tree selection focuses the subject without resetting a compatible detail tab", async () => {
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
const renderer = await readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8");
test("facet and search selection focus the subject while the reducer preserves a valid detail tab", async () => {
const [preview, renderer, state] = await Promise.all([
source("../apps/catalog/src/MapFixturePreview.tsx"),
source("../apps/catalog/src/CesiumMapRenderer.tsx"),
source("../apps/catalog/src/mapWorkspaceState.mjs"),
]);
assert.match(preview, /const \[expandedFacetRows, setExpandedFacetRows\]/);
assert.match(preview, /const handleSelectAndFocus = useCallback/);
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, /profile\?\.tabs\.some\(\(tab\) => tab\.id === current\)/);
assert.match(preview, /\(profile\?\.defaultTabId \?\? "overview"\)/);
assert.match(preview, /validTabIds: profile\?\.tabs\.map\(\(tab\) => tab\.id\) \?\? \["overview"\]/);
assert.match(state, /action\.validTabIds\.includes\(state\.subjectCard\.tabId\)/);
assert.match(renderer, /void viewer\.flyTo\(entity, \{/);
assert.match(renderer, /const focusSubjectCoordinates = useCallback/);
assert.match(renderer, /viewer\.camera\.flyToBoundingSphere\(new BoundingSphere\(target, 1\), \{/);
+1 -9
View File
@@ -1,13 +1,5 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import ts from "typescript";
const source = await readFile(new URL("../apps/catalog/src/mapPresentationProfile.ts", import.meta.url), "utf8");
const transpiled = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
}).outputText;
const moduleUrl = `data:text/javascript;base64,${Buffer.from(transpiled).toString("base64")}`;
const {
mapFactMatchesFilters,
mapPresentationBindingIsAll,
@@ -15,7 +7,7 @@ const {
mapPresentationFacetValueIsEnabled,
normalizeMapPresentationFacetSelections,
toggleMapPresentationFacetSelection,
} = await import(moduleUrl);
} = await import("../apps/catalog/src/mapPresentationProfile.mjs");
const profile = {
id: "map.moving-object.operational.default",
+2 -2
View File
@@ -28,12 +28,12 @@ test("Map Page registers a provider-neutral reference-points slot and three stat
test("existing layouts upgrade reference layers on read and renderer accepts their slot", async () => {
const server = await readFile(new URL("server/catalog-server.mjs", root), "utf8");
const renderer = await readFile(new URL("apps/catalog/src/CesiumMapRenderer.tsx", root), "utf8");
const runtimeLayers = await readFile(new URL("apps/catalog/src/mapCesiumRuntimeLayers.ts", root), "utf8");
const runtime = await readFile(new URL("apps/catalog/src/useMapReferenceRuntime.ts", root), "utf8");
assert.match(server, /pageLayoutMatch\[1\] === "map" \? validateMapPageLayout\(stored\) : stored/);
assert.match(server, /referenceLayers: structuredClone\(canonicalMapReferenceLayers\)/);
assert.match(renderer, /binding\.slotId === "reference-points"/);
assert.match(runtimeLayers, /binding\.slotId === "reference-points"/);
assert.match(runtime, /\/api\/map-gateway\/api\/map\/reference-sources\/v1\/profiles\//);
assert.match(runtime, /allowedAttributes = new Set\(\["name", "category", "network", "operator", "official_name", "local_name", "uic_ref", "wheelchair"\]\)/);
assert.doesNotMatch(runtime, /credential|accessToken|providerEndpoint|rawPayload/);
+8 -5
View File
@@ -275,17 +275,20 @@ test("restricted identity aspects render full identifiers and bounded string lis
});
test("Cesium pin and label share one entity selection that opens the subject card", async () => {
const [renderer, preview] = await Promise.all([
const [renderer, preview, windows, state] = await Promise.all([
readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8"),
readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8"),
readFile(new URL("../apps/catalog/src/MapSubjectWorkspaceWindows.tsx", import.meta.url), "utf8"),
readFile(new URL("../apps/catalog/src/mapWorkspaceState.mjs", import.meta.url), "utf8"),
]);
assert.match(renderer, /viewer\?\.scene\.pick/);
assert.match(renderer, /pickedId instanceof Entity/);
assert.match(renderer, /onSelectRef\.current\?\.\(pickedId\.id\)/);
assert.match(preview, /const handleSelect = useCallback/);
assert.match(preview, /setSubjectCardOpen\(true\)/);
assert.match(preview, /title=\{selectedSubjectCard\.title\}/);
assert.match(preview, /Карточка объекта:/);
assert.match(preview, /SegmentedControl/);
assert.match(preview, /type: "select-entity"/);
assert.match(state, /subjectCard: \{ \.\.\.state\.subjectCard, open: true, tabId \}/);
assert.match(windows, /title=\{selectedSubjectCard\.title\}/);
assert.match(windows, /Карточка объекта:/);
assert.match(windows, /SegmentedControl/);
});
+235
View File
@@ -0,0 +1,235 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
buildFilteredMapTargets,
buildMapPresentationFilters,
buildSelectableMapEntities,
planMapSubjectReveal,
primaryMapRuntimeBindings,
} from "../apps/catalog/src/mapWorkspaceModel.mjs";
import { mapRuntimeEntityId, mapRuntimeFactKey } from "../apps/catalog/src/mapRuntimeIdentity.mjs";
import { localSectorAtGeodetic, localSectorSummary } from "../apps/catalog/src/mapSectorGrid.mjs";
const origin = { latitude: 55.7558, longitude: 37.6173 };
const gridProfile = {
mode: "3d",
stepKm: 1,
tileSizeKm: 5,
graticuleStepDegrees: 1,
};
const primaryBinding = {
id: "positions",
order: 1,
dataProductId: "fleet.positions.current.v5",
slotId: "points",
delivery: "snapshot+patch",
semanticTypes: ["map.moving_object"],
fieldProjection: ["display_name", "signal_state", "position_source", "object_kind"],
presentationProfileId: "moving",
};
const joinedBinding = {
...primaryBinding,
id: "identity",
order: 2,
dataProductId: "fleet.units.identity.current.v1",
joinToBindingId: "positions",
};
const profile = {
id: "moving",
title: "Current positions",
semanticTypes: ["map.moving_object"],
label: { mode: "attributes", fields: ["display_name"], maxLength: 80 },
target: { variant: "elevated-spike" },
facets: [{
id: "signal",
field: "signal_state",
filterable: true,
counter: true,
values: [
{ value: "active", label: "Online", order: 1 },
{ value: "inactive", label: "Offline", order: 2 },
],
}],
styles: [{ id: "active", color: "#fff", opacity: 1 }],
classes: [{ id: "active", label: "Online", priority: 1, match: [], styleId: "active", renderable: true }],
defaultClassId: "active",
sort: [{ field: "signal_state", order: ["active", "inactive"] }],
};
function pointFact(sourceId, signalState, coordinates, displayName = sourceId) {
return {
sourceId,
semanticType: "map.moving_object",
observedAt: "2026-08-09T12:00:00.000Z",
receivedAt: "2026-08-09T12:00:01.000Z",
attributes: {
display_name: displayName,
signal_state: signalState,
position_source: "gelios",
object_kind: "tracked_unit",
},
geometry: { type: "Point", coordinates },
presentationStatus: signalState,
};
}
const inactive = pointFact("unit-offline", "inactive", [37.6173, 55.7558], "Альфа");
const active = pointFact("unit-online", "active", [37.618, 55.756], "Янтарь");
const runtimeBindings = [{
bindingId: "positions",
dataProductId: primaryBinding.dataProductId,
slotId: "points",
presentationProfileId: "moving",
facts: [inactive, active],
cursor: "1",
state: "ready",
}, {
bindingId: "identity",
dataProductId: joinedBinding.dataProductId,
slotId: "subject-aspect",
presentationProfileId: "moving",
facts: [active],
cursor: "1",
state: "ready",
}];
test("stable runtime identity uses binding, semantic type and source id only", () => {
const moved = { ...active, geometry: { type: "Point", coordinates: [40, 60] } };
assert.equal(mapRuntimeFactKey(active), "map.moving_object\u0000unit-online");
assert.equal(mapRuntimeEntityId("positions", active), mapRuntimeEntityId("positions", moved));
assert.equal(mapRuntimeEntityId("positions", active), "nodedc-runtime:positions:map.moving_object:unit-online");
});
test("joined aspects enrich a primary subject and never become map entities", () => {
assert.deepEqual(
primaryMapRuntimeBindings(runtimeBindings, [joinedBinding, primaryBinding]).map((binding) => binding.bindingId),
["positions"],
);
assert.deepEqual(
buildSelectableMapEntities(runtimeBindings, [joinedBinding, primaryBinding], [profile]).map((entity) => entity.fact.sourceId),
["unit-online", "unit-offline"],
);
});
test("object overview preserves the same MCP profile sort as facets and renderer", () => {
const filters = { positions: { visible: true, facets: {} } };
const selectable = buildSelectableMapEntities(runtimeBindings, [primaryBinding, joinedBinding], [profile]);
const targets = buildFilteredMapTargets(runtimeBindings, [primaryBinding, joinedBinding], [profile], filters);
// Alphabetical title order would be Альфа, Янтарь. Declared state order is
// active, inactive and must remain authoritative everywhere.
assert.deepEqual(selectable.map((entity) => entity.title), ["Янтарь", "Альфа"]);
assert.deepEqual(targets.map((target) => target.title), ["Янтарь", "Альфа"]);
});
test("explicit empty facets remain match-nothing through workspace projection", () => {
const subjectStates = {
positions: {
bindingId: "positions",
visible: true,
filters: { signal_state: [] },
window: { open: false, rect: { x: 0, y: 0, width: 280, height: 260 }, maximized: false, zIndex: 20 },
},
};
const filters = buildMapPresentationFilters(subjectStates, [primaryBinding], [profile]);
assert.deepEqual(filters.positions.facets, { signal_state: [] });
assert.deepEqual(buildFilteredMapTargets(runtimeBindings, [primaryBinding], [profile], filters), []);
});
test("search reveal removes only constraints hiding the selected subject", () => {
const address = localSectorAtGeodetic(origin, {
lod: 1,
originLatitude: origin.latitude,
originLongitude: origin.longitude,
stepMeters: 1_000,
});
const selectedSector = {
...localSectorSummary(address, {
lod: 1,
originLatitude: origin.latitude,
originLongitude: origin.longitude,
stepMeters: 1_000,
tileSizeMeters: 5_000,
}),
mode: "3d",
units: "meters-enu",
volume: null,
};
const entity = buildSelectableMapEntities(runtimeBindings, [primaryBinding], [profile])
.find((candidate) => candidate.fact.sourceId === "unit-offline");
const subjectState = {
bindingId: "positions",
visible: false,
filters: { signal_state: ["active"] },
window: { open: true, rect: { x: 10, y: 20, width: 280, height: 260 }, maximized: false, zIndex: 42 },
};
const reveal = planMapSubjectReveal({
entity,
subjectState,
profile,
selectedSector,
gridProfiles: [gridProfile],
origin,
hideOutsideSector: true,
scope: {
excludedBindingIds: ["positions", "other"],
excludedProviders: ["gelios", "other"],
excludedObjectKinds: ["tracked_unit", "other"],
},
});
assert.equal(reveal.subjectState.visible, true);
assert.deepEqual(reveal.subjectState.filters, {});
assert.deepEqual(reveal.subjectState.window, subjectState.window);
assert.equal(reveal.hideOutsideSector, true);
assert.deepEqual(reveal.scope, {
excludedBindingIds: ["other"],
excludedProviders: ["other"],
excludedObjectKinds: ["other"],
});
});
test("search keeps the active sector but relaxes outside clipping for an outside subject", () => {
const address = localSectorAtGeodetic(origin, {
lod: 1,
originLatitude: origin.latitude,
originLongitude: origin.longitude,
stepMeters: 1_000,
});
const selectedSector = {
...localSectorSummary(address, {
lod: 1,
originLatitude: origin.latitude,
originLongitude: origin.longitude,
stepMeters: 1_000,
tileSizeMeters: 5_000,
}),
mode: "3d",
units: "meters-enu",
volume: null,
};
const outsideEntity = {
...buildSelectableMapEntities(runtimeBindings, [primaryBinding], [profile])[0],
fact: pointFact("far-away", "active", [50, 60]),
};
const subjectState = {
bindingId: "positions",
visible: true,
filters: {},
window: { open: false, rect: { x: 0, y: 0, width: 280, height: 260 }, maximized: false, zIndex: 20 },
};
const reveal = planMapSubjectReveal({
entity: outsideEntity,
subjectState,
profile,
selectedSector,
gridProfiles: [gridProfile],
origin,
hideOutsideSector: true,
scope: { excludedBindingIds: [], excludedProviders: [], excludedObjectKinds: [] },
});
assert.equal(reveal.hideOutsideSector, false);
assert.equal(selectedSector.id, reveal ? selectedSector.id : null);
});
+102
View File
@@ -0,0 +1,102 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createMapWorkspaceState, mapWorkspaceReducer } from "../apps/catalog/src/mapWorkspaceState.mjs";
const rect = (x, y, width = 280, height = 260) => ({ x, y, width, height });
const subjectStates = {
positions: {
bindingId: "positions",
visible: true,
filters: {},
window: { open: false, rect: rect(24, 56), maximized: false, zIndex: 20 },
},
zones: {
bindingId: "zones",
visible: true,
filters: {},
window: { open: false, rect: rect(52, 84), maximized: false, zIndex: 21 },
},
};
function initialState() {
return createMapWorkspaceState({
subjectStates: structuredClone(subjectStates),
sectorWindowRect: rect(24, 72, 380, 530),
subjectCardRect: rect(940, 72, 390, 520),
});
}
test("one reducer owns the complete floating-window stack", () => {
const opened = mapWorkspaceReducer(initialState(), { type: "open-subject-window", bindingId: "positions" });
assert.equal(opened.subjectStates.positions.window.open, true);
assert.equal(opened.activeWindowId, "binding:positions");
assert.ok(opened.subjectStates.positions.window.zIndex > opened.sector.window.zIndex);
const activatedCard = mapWorkspaceReducer(opened, {
type: "select-entity",
entityId: "nodedc-runtime:positions:map.moving_object:unit-1",
validTabIds: ["overview", "position"],
defaultTabId: "overview",
});
assert.equal(activatedCard.subjectCard.open, true);
assert.equal(activatedCard.activeWindowId, "subject-card");
assert.ok(activatedCard.subjectCard.zIndex > opened.subjectStates.positions.window.zIndex);
// Re-activating the active window is stable and does not inflate z-index.
assert.equal(
mapWorkspaceReducer(activatedCard, { type: "activate-window", windowId: "subject-card" }),
activatedCard,
);
});
test("selecting a domain subject never clears the active grid sector", () => {
const sector = { id: "grid/local/sector", lod: 2, mode: "3d" };
const withSector = mapWorkspaceReducer(initialState(), { type: "select-sector", selection: sector });
const withSubject = mapWorkspaceReducer(withSector, {
type: "select-entity",
entityId: "nodedc-runtime:positions:map.moving_object:unit-1",
validTabIds: ["overview"],
defaultTabId: "overview",
});
assert.equal(withSubject.selectedSector, sector);
assert.equal(withSubject.selectedEntityId, "nodedc-runtime:positions:map.moving_object:unit-1");
});
test("sector deactivation clears only sector scope and keeps subject UI state", () => {
let state = mapWorkspaceReducer(initialState(), { type: "select-sector", selection: { id: "sector", lod: 1, mode: "3d" } });
state = mapWorkspaceReducer(state, { type: "set-sector-scope-enabled", dimension: "binding", value: "positions", enabled: false });
state = mapWorkspaceReducer(state, { type: "open-subject-window", bindingId: "positions" });
state = mapWorkspaceReducer(state, { type: "deactivate-sector" });
assert.equal(state.selectedSector, null);
assert.deepEqual(state.sector.scope, {
excludedBindingIds: [],
excludedProviders: [],
excludedObjectKinds: [],
});
assert.equal(state.subjectStates.positions.window.open, true);
assert.equal(state.subjectStates.positions.visible, true);
});
test("search reveal state is applied atomically without replacing window geometry", () => {
const state = initialState();
const originalWindow = state.subjectStates.positions.window;
const revealedSubject = {
...state.subjectStates.positions,
visible: true,
filters: { signal_state: ["inactive"] },
};
const next = mapWorkspaceReducer(state, {
type: "reveal-subject",
bindingId: "positions",
subjectState: revealedSubject,
hideOutsideSector: false,
scope: { excludedBindingIds: [], excludedProviders: ["other"], excludedObjectKinds: [] },
});
assert.deepEqual(next.subjectStates.positions.window, originalWindow);
assert.deepEqual(next.subjectStates.positions.filters, { signal_state: ["inactive"] });
assert.deepEqual(next.sector.scope.excludedProviders, ["other"]);
});