refactor(map): split foundry workspace and cesium runtime
This commit is contained in:
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user