feat(map): add reference layers and stabilize workspace controls

This commit is contained in:
Codex
2026-07-25 13:12:24 +03:00
parent c859ae4db0
commit eaecce9f56
29 changed files with 1593 additions and 247 deletions
+87 -2
View File
@@ -55,6 +55,32 @@ if (mapPresentationProfileRegistry?.schemaVersion !== "nodedc.map-presentation-p
throw new Error("map_presentation_profile_registry_invalid");
}
const canonicalMapPresentationProfiles = normalizeMapPresentationProfiles(mapPresentationProfileRegistry.profiles);
const canonicalMapReferencePresentationProfiles = canonicalMapPresentationProfiles.filter((profile) => (
profile.id.startsWith("map.reference.transport.")
));
const canonicalMapReferenceLayers = Object.freeze([
Object.freeze({
id: "reference.transport.metro",
referenceProfileId: "transport-stations.v1",
category: "metro",
presentationProfileId: "map.reference.transport.metro.v1",
visible: true,
}),
Object.freeze({
id: "reference.transport.railway_terminal",
referenceProfileId: "transport-stations.v1",
category: "railway_terminal",
presentationProfileId: "map.reference.transport.terminal.v1",
visible: true,
}),
Object.freeze({
id: "reference.transport.railway_station",
referenceProfileId: "transport-stations.v1",
category: "railway_station",
presentationProfileId: "map.reference.transport.railway-station.v1",
visible: true,
}),
]);
const mapSubjectDetailProfileRegistry = JSON.parse(await readFile(join(root, "registry", "map-subject-detail-profiles.json"), "utf8"));
if (mapSubjectDetailProfileRegistry?.schemaVersion !== "nodedc.map-subject-detail-profiles/v1") {
throw new Error("map_subject_detail_profile_registry_invalid");
@@ -458,6 +484,33 @@ function validateMapSubjectStates(value, dataProductBindings) {
return dataProductBindings.map((binding, index) => states.get(binding.id) ?? defaultMapSubjectState(binding.id, index));
}
function validateMapReferenceLayers(value) {
if (!Array.isArray(value) || value.length > canonicalMapReferenceLayers.length) {
throw applicationError("invalid_map_reference_layers");
}
const canonicalById = new Map(canonicalMapReferenceLayers.map((layer) => [layer.id, layer]));
const visibilityById = new Map();
for (const raw of value) {
if (!isObject(raw)) throw applicationError("invalid_map_reference_layer");
if (Object.keys(raw).some((key) => !new Set([
"id", "referenceProfileId", "category", "presentationProfileId", "visible",
]).has(key))) throw applicationError("invalid_map_reference_layer_field");
const canonical = canonicalById.get(raw.id);
if (!canonical
|| raw.referenceProfileId !== canonical.referenceProfileId
|| raw.category !== canonical.category
|| raw.presentationProfileId !== canonical.presentationProfileId) {
throw applicationError("map_reference_layer_not_registered");
}
if (visibilityById.has(raw.id)) throw applicationError("duplicate_map_reference_layer_id");
visibilityById.set(raw.id, requireBoolean(raw.visible, "invalid_map_reference_layer_visibility"));
}
return canonicalMapReferenceLayers.map((layer) => ({
...layer,
visible: visibilityById.get(layer.id) ?? layer.visible,
}));
}
const MAP_PAGE_SETTING_KEYS = new Set([
"imagerySource", "imageryVisible", "cacheEnabled", "cacheNoOverwrite", "terrainEnabled",
"terrainExaggeration", "monochrome", "monochromeColor", "imageryGamma", "imageryHue",
@@ -477,6 +530,21 @@ function validateMapPageSettingsPatch(value) {
return value;
}
function validateMapInspectorOpenSections(value) {
if (!Array.isArray(value) || value.length > 1) {
throw applicationError("invalid_map_inspector_open_sections");
}
const sections = value.map((section) => requireNonEmptyString(
section,
"invalid_map_inspector_open_section",
256,
));
if (new Set(sections).size !== sections.length) {
throw applicationError("duplicate_map_inspector_open_section");
}
return sections;
}
function validateMapPageLayout(value) {
if (!isObject(value)) throw applicationError("invalid_map_page_layout");
if (value.schemaVersion !== 1 || value.pageId !== "map") throw applicationError("unsupported_map_page_layout");
@@ -501,7 +569,12 @@ function validateMapPageLayout(value) {
for (const key of ["longitude", "latitude", "height", "heading", "pitch", "roll"]) {
requireNumber(camera[key], -1_000_000_000, 1_000_000_000, `invalid_map_page_camera_${key}`);
}
const presentationProfiles = normalizeMapPresentationProfiles(value.presentationProfiles === undefined ? [] : value.presentationProfiles);
const suppliedPresentationProfiles = normalizeMapPresentationProfiles(value.presentationProfiles === undefined ? [] : value.presentationProfiles);
const suppliedPresentationProfileIds = new Set(suppliedPresentationProfiles.map((profile) => profile.id));
const presentationProfiles = normalizeMapPresentationProfiles([
...suppliedPresentationProfiles,
...canonicalMapReferencePresentationProfiles.filter((profile) => !suppliedPresentationProfileIds.has(profile.id)),
]);
const subjectDetailProfiles = normalizeMapSubjectDetailProfiles(
value.subjectDetailProfiles === undefined
? structuredClone(canonicalMapSubjectDetailProfiles)
@@ -509,6 +582,7 @@ function validateMapPageLayout(value) {
);
const dataProductBindings = validateMapDataProductBindings(value.dataProductBindings === undefined ? [] : value.dataProductBindings);
const subjectStates = validateMapSubjectStates(value.subjectStates === undefined ? [] : value.subjectStates, dataProductBindings);
const referenceLayers = validateMapReferenceLayers(value.referenceLayers === undefined ? [] : value.referenceLayers);
const presentationProfileIds = new Set(presentationProfiles.map((profile) => profile.id));
if (dataProductBindings.some((binding) => binding.presentationProfileId && !presentationProfileIds.has(binding.presentationProfileId))) {
throw applicationError("map_data_product_presentation_profile_not_found");
@@ -571,6 +645,10 @@ function validateMapPageLayout(value) {
subjectDetailProfiles,
dataProductBindings,
subjectStates,
referenceLayers,
inspectorOpenSections: validateMapInspectorOpenSections(
value.inspectorOpenSections === undefined ? ["map-base"] : value.inspectorOpenSections,
),
};
}
@@ -663,6 +741,8 @@ function defaultMapPageLayout() {
subjectDetailProfiles: structuredClone(canonicalMapSubjectDetailProfiles),
dataProductBindings: [],
subjectStates: [],
referenceLayers: structuredClone(canonicalMapReferenceLayers),
inspectorOpenSections: ["map-base"],
};
}
@@ -2159,6 +2239,9 @@ const foundryMcpOperations = {
mapHeight: input.viewState.mapHeight,
camera: input.viewState.camera,
subjectStates: input.viewState.subjectStates,
inspectorOpenSections: input.viewState.inspectorOpenSections === undefined
? layout.inspectorOpenSections
: input.viewState.inspectorOpenSections,
}) },
};
return updatedPage;
@@ -2170,6 +2253,7 @@ const foundryMcpOperations = {
mapHeight: updatedPage.layout.map.mapHeight,
camera: updatedPage.layout.map.camera,
subjectStates: updatedPage.layout.map.subjectStates,
inspectorOpenSections: updatedPage.layout.map.inspectorOpenSections,
} };
},
});
@@ -2703,7 +2787,8 @@ const server = createServer(async (request, response) => {
const pageLayoutMatch = url.pathname.match(/^\/api\/page-layouts\/([a-z0-9-]+)$/i);
if (pageLayoutMatch && request.method === "GET") {
try {
json(response, 200, JSON.parse(await readFile(pageLayoutPath(pageLayoutMatch[1]), "utf8")));
const stored = JSON.parse(await readFile(pageLayoutPath(pageLayoutMatch[1]), "utf8"));
json(response, 200, pageLayoutMatch[1] === "map" ? validateMapPageLayout(stored) : stored);
} catch (error) {
if (error?.code === "ENOENT") return json(response, 200, null);
throw error;
+3 -1
View File
@@ -132,7 +132,7 @@ test("one setup command provisions independent Foundry and Ontology MCP transpor
assert.deepEqual(subjectDetailTool.inputSchema.properties.profile.required, ["id", "version", "title", "semanticTypes", "defaultTabId", "tabs"]);
assert.deepEqual(
subjectDetailTool.inputSchema.properties.profile.properties.tabs.items.properties.sections.items.properties.fields.items.properties.format.enum,
["text", "number", "timestamp", "boolean", "coordinate", "signal_state", "movement_state", "telemetry_readings"],
["text", "number", "timestamp", "boolean", "coordinate", "signal_state", "movement_state", "string_list", "telemetry_readings"],
);
const mapSettingsTool = foundryList.result.tools.find((tool) => tool.name === "foundry_update_map_page_settings");
assert.ok(mapSettingsTool);
@@ -140,6 +140,8 @@ test("one setup command provisions independent Foundry and Ontology MCP transpor
assert.equal(Object.hasOwn(mapSettingsTool.inputSchema.properties.settings.properties, "providerUrl"), false);
const mapViewStateTool = foundryList.result.tools.find((tool) => tool.name === "foundry_save_map_page_view_state");
assert.ok(mapViewStateTool);
assert.equal(mapViewStateTool.inputSchema.properties.viewState.properties.inspectorOpenSections.maxItems, 1);
assert.equal(mapViewStateTool.inputSchema.properties.viewState.properties.inspectorOpenSections.uniqueItems, true);
const subjectStateSchema = mapViewStateTool.inputSchema.properties.viewState.properties.subjectStates.items;
assert.deepEqual(subjectStateSchema.required, ["bindingId", "visible", "filters", "window"]);
assert.equal(subjectStateSchema.properties.filters.additionalProperties.items.type, "string");
+7
View File
@@ -688,6 +688,13 @@ const tools = [
},
},
subjectStates: { type: "array", maxItems: 64, items: mapSubjectStateInputSchema },
inspectorOpenSections: {
type: "array",
maxItems: 1,
uniqueItems: true,
description: "Exact expanded Map Inspector section ids. Empty means every section is collapsed.",
items: { type: "string", minLength: 1, maxLength: 256 },
},
},
},
},