Files
NODEDC_DESIGN_GUIDELINE/scripts/map-workspace-model.test.mjs
T

236 lines
7.9 KiB
JavaScript

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);
});