feat(foundry): render geozones as independent map layer

This commit is contained in:
Codex
2026-07-20 21:48:27 +03:00
parent a02c3ff3dd
commit aa681ca069
12 changed files with 418 additions and 90 deletions
+48 -13
View File
@@ -1166,11 +1166,53 @@ async function inspectFoundryConsumerReaderGrant(target, { generation = 1 } = {}
return { product: planned.product, readerGrantAction: "ensure", readerGrantGeneration: generation };
}
function runtimePointGeometry(value) {
if (!isObject(value) || value.type !== "Point" || !Array.isArray(value.coordinates) || value.coordinates.length !== 2) return null;
const [longitude, latitude] = value.coordinates;
if (typeof longitude !== "number" || !Number.isFinite(longitude) || typeof latitude !== "number" || !Number.isFinite(latitude)) return null;
return { type: "Point", coordinates: [longitude, latitude] };
function runtimePosition(value) {
if (!Array.isArray(value) || value.length !== 2) return null;
const [longitude, latitude] = value;
if (
typeof longitude !== "number"
|| !Number.isFinite(longitude)
|| longitude < -180
|| longitude > 180
|| typeof latitude !== "number"
|| !Number.isFinite(latitude)
|| latitude < -90
|| latitude > 90
) return null;
return [longitude, latitude];
}
function runtimeLinearRing(value) {
if (!Array.isArray(value) || value.length < 4 || value.length > 10_000) return null;
const positions = value.map(runtimePosition);
if (positions.some((position) => !position)) return null;
const first = positions[0];
const last = positions.at(-1);
if (!last || first[0] !== last[0] || first[1] !== last[1]) return null;
return positions;
}
function runtimePolygonCoordinates(value) {
if (!Array.isArray(value) || value.length < 1 || value.length > 256) return null;
const rings = value.map(runtimeLinearRing);
return rings.some((ring) => !ring) ? null : rings;
}
function runtimeGeometry(value) {
if (!isObject(value) || JSON.stringify(value).length > 196_608) return null;
if (value.type === "Point") {
const coordinates = runtimePosition(value.coordinates);
return coordinates ? { type: "Point", coordinates } : null;
}
if (value.type === "Polygon") {
const coordinates = runtimePolygonCoordinates(value.coordinates);
return coordinates ? { type: "Polygon", coordinates } : null;
}
if (value.type === "MultiPolygon" && Array.isArray(value.coordinates) && value.coordinates.length >= 1 && value.coordinates.length <= 256) {
const coordinates = value.coordinates.map(runtimePolygonCoordinates);
return coordinates.some((polygon) => !polygon) ? null : { type: "MultiPolygon", coordinates };
}
return null;
}
const RUNTIME_SECRET_LIKE_KEY = /(token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key)/i;
@@ -1203,7 +1245,7 @@ function sanitizeRuntimeFact(value, binding) {
observedAt: value.observedAt,
receivedAt: value.receivedAt,
attributes,
geometry: runtimePointGeometry(value.geometry),
geometry: runtimeGeometry(value.geometry),
};
}
@@ -1285,23 +1327,16 @@ function sanitizeRuntimePatch(value, binding) {
const fact = sanitizeRuntimeFact(operation.fact, binding);
return fact ? [{ op: "upsert", fact }] : [];
}
// Current EDP v1 emits only upserts. Foundry already understands the
// canonical removal shape so a future versioned product can revoke a
// subject without turning a transient stream failure into deletion.
if (
operation.op === "remove"
&& isRuntimeIdentifier(operation.sourceId)
&& isRuntimeIdentifier(operation.semanticType)
&& binding.semanticTypes.includes(operation.semanticType)
&& isRuntimeTimestamp(operation.removedAt)
&& ["tombstone", "revoked"].includes(operation.reason)
) {
return [{
op: "remove",
sourceId: operation.sourceId,
semanticType: operation.semanticType,
removedAt: operation.removedAt,
reason: operation.reason,
}];
}
return [];
+11 -2
View File
@@ -49,7 +49,7 @@ function safeStatusFromFact(fact, policy, nowMs) {
if (typeof sourceStatus !== "string" || !statusContract.allowedValues.includes(sourceStatus)) {
throw consumerError("data_product_consumer_fact_status_invalid", 502);
}
if (statusContract.freshness === "none" || policy.terminalStatuses.includes(sourceStatus)) {
if (policy.freshness === "none" || policy.terminalStatuses.includes(sourceStatus)) {
return sourceStatus;
}
const observedAt = Date.parse(String(fact?.observedAt || ""));
@@ -60,6 +60,7 @@ function safeStatusFromFact(fact, policy, nowMs) {
.find((value) => typeof value === "string" && value.trim());
const normalized = String(sourceStatus || "active").trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").slice(0, 64) || "active";
if (policy.terminalStatuses.includes(normalized)) return normalized;
if (policy.freshness === "none") return normalized;
const observedAt = Date.parse(String(fact?.observedAt || ""));
if (Number.isFinite(observedAt) && nowMs - observedAt > policy.staleAfterMs) return "stale";
return normalized;
@@ -144,7 +145,14 @@ function validatePolicy(policy, product) {
const statusContract = policy.statusContract === undefined
? null
: validateStatusContract(policy.statusContract);
const staleDisabled = statusContract?.freshness === "none";
const freshness = policy.freshness ?? statusContract?.freshness ?? "observed-at";
if (!["none", "observed-at"].includes(freshness)) {
throw consumerError("data_product_consumer_policy_invalid", 500);
}
if (statusContract && statusContract.freshness !== freshness) {
throw consumerError("data_product_consumer_policy_invalid", 500);
}
const staleDisabled = freshness === "none";
if (
(staleDisabled && policy.staleAfterMs !== null)
|| (!staleDisabled && (!Number.isInteger(policy.staleAfterMs) || policy.staleAfterMs < 1_000 || policy.staleAfterMs > 7 * 24 * 60 * 60 * 1000))
@@ -166,6 +174,7 @@ function validatePolicy(policy, product) {
version: String(policy.version || ""),
dataProductId: product.id,
productVersion: product.version,
freshness,
staleAfterMs: policy.staleAfterMs,
terminalStatuses: [...terminalStatuses],
...(statusContract ? { statusContract } : {}),
+35 -19
View File
@@ -174,26 +174,42 @@ const mapPresentationProfileInputSchema = {
},
},
target: {
type: "object",
additionalProperties: false,
required: [
"variant", "stemHeightMeters", "headSizePx", "stemWidthPx", "outlineColor",
"outlineOpacity", "outlineWidthPx", "hideCameraHeightMeters",
],
properties: {
variant: {
type: "string",
enum: ["elevated-spike"],
description: "Renderer-neutral presentation preset interpreted by the active Map adapter.",
oneOf: [
{
type: "object",
additionalProperties: false,
required: [
"variant", "stemHeightMeters", "headSizePx", "stemWidthPx", "outlineColor",
"outlineOpacity", "outlineWidthPx", "hideCameraHeightMeters",
],
properties: {
variant: { type: "string", enum: ["elevated-spike"] },
stemHeightMeters: { type: "number", minimum: 1, maximum: 100000 },
headSizePx: { type: "number", minimum: 1, maximum: 64 },
stemWidthPx: { type: "number", minimum: 0.25, maximum: 16 },
outlineColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
outlineOpacity: { type: "number", minimum: 0, maximum: 1 },
outlineWidthPx: { type: "number", minimum: 0, maximum: 8 },
hideCameraHeightMeters: { type: "number", minimum: 1, maximum: 100000000 },
},
},
stemHeightMeters: { type: "number", minimum: 1, maximum: 100000 },
headSizePx: { type: "number", minimum: 1, maximum: 64 },
stemWidthPx: { type: "number", minimum: 0.25, maximum: 16 },
outlineColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
outlineOpacity: { type: "number", minimum: 0, maximum: 1 },
outlineWidthPx: { type: "number", minimum: 0, maximum: 8 },
hideCameraHeightMeters: { type: "number", minimum: 1, maximum: 100000000 },
},
{
type: "object",
additionalProperties: false,
required: ["variant", "outlineColor", "outlineOpacity", "outlineWidthPx", "hideCameraHeightMeters"],
properties: {
variant: {
type: "string",
enum: ["surface-fill"],
description: "Provider-neutral filled surface interpreted by the active Map adapter.",
},
outlineColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
outlineOpacity: { type: "number", minimum: 0, maximum: 1 },
outlineWidthPx: { type: "number", minimum: 0, maximum: 8 },
hideCameraHeightMeters: { type: "number", minimum: 1, maximum: 100000000 },
},
},
],
},
facets: {
type: "array",
+13 -4
View File
@@ -92,16 +92,25 @@ function normalizeLabel(value) {
function normalizeTarget(value) {
object(value, "invalid_map_presentation_profile_target");
onlyKeys(value, TARGET_KEYS, "invalid_map_presentation_profile_target_fields");
const shared = {
outlineColor: hex(value.outlineColor, "invalid_map_presentation_profile_target_outline"),
outlineOpacity: number(value.outlineOpacity, 0, 1, "invalid_map_presentation_profile_target_outline_opacity"),
outlineWidthPx: number(value.outlineWidthPx, 0, 8, "invalid_map_presentation_profile_target_outline_width"),
hideCameraHeightMeters: number(value.hideCameraHeightMeters, 1, 100_000_000, "invalid_map_presentation_profile_target_lod"),
};
if (value.variant === "surface-fill") {
if (value.stemHeightMeters !== undefined || value.headSizePx !== undefined || value.stemWidthPx !== undefined) {
fail("invalid_map_presentation_profile_surface_target_fields");
}
return { variant: "surface-fill", ...shared };
}
if (value.variant !== "elevated-spike") fail("invalid_map_presentation_profile_target_variant");
return {
variant: "elevated-spike",
stemHeightMeters: number(value.stemHeightMeters, 1, 100_000, "invalid_map_presentation_profile_target_stem_height"),
headSizePx: number(value.headSizePx, 1, 64, "invalid_map_presentation_profile_target_head_size"),
stemWidthPx: number(value.stemWidthPx, 0.25, 16, "invalid_map_presentation_profile_target_stem_width"),
outlineColor: hex(value.outlineColor, "invalid_map_presentation_profile_target_outline"),
outlineOpacity: number(value.outlineOpacity, 0, 1, "invalid_map_presentation_profile_target_outline_opacity"),
outlineWidthPx: number(value.outlineWidthPx, 0, 8, "invalid_map_presentation_profile_target_outline_width"),
hideCameraHeightMeters: number(value.hideCameraHeightMeters, 1, 100_000_000, "invalid_map_presentation_profile_target_lod"),
...shared,
};
}
+15 -2
View File
@@ -10,8 +10,9 @@ const catalogStyles = await readFile(new URL("../apps/catalog/src/styles.css", i
test("canonical moving-object profile is provider-neutral and internally consistent", () => {
assert.equal(registry.schemaVersion, "nodedc.map-presentation-profiles/v1");
const profiles = normalizeMapPresentationProfiles(registry.profiles);
assert.equal(profiles.length, 1);
const profile = profiles[0];
assert.equal(profiles.length, 2);
const profile = profiles.find((item) => item.id === "map.moving-object.operational.default");
assert.ok(profile);
assert.equal(profile.id, "map.moving-object.operational.default");
assert.deepEqual(profile.semanticTypes, ["map.moving_object"]);
assert.equal(profile.target.variant, "elevated-spike");
@@ -37,6 +38,18 @@ test("canonical moving-object profile is provider-neutral and internally consist
assert.equal(JSON.stringify(profile).includes("Неизвестно"), false);
});
test("canonical zone profile renders provider-neutral surfaces", () => {
const profiles = normalizeMapPresentationProfiles(registry.profiles);
const profile = profiles.find((item) => item.id === "map.zone.operational.default");
assert.ok(profile);
assert.deepEqual(profile.semanticTypes, ["map.zone"]);
assert.equal(profile.target.variant, "surface-fill");
assert.equal(profile.label.mode, "attributes");
assert.deepEqual(profile.facets.map((facet) => facet.field), ["geometry_kind", "source_kind"]);
assert.equal(JSON.stringify(profile).toLowerCase().includes("gelios"), false);
assert.equal(JSON.stringify(profile).includes("http"), false);
});
test("fleet filter window is one compact vertical list without group rows or All", () => {
assert.match(mapFixtureSource, /catalog-map-fixture__target-filter-list/);
assert.doesNotMatch(mapFixtureSource, /catalog-map-fixture__target-profile/);