fix(foundry): isolate HGeoZone projection faults
This commit is contained in:
parent
309cddef73
commit
ca26702047
|
|
@ -58,6 +58,7 @@ import {
|
||||||
type MapPresentationFilters,
|
type MapPresentationFilters,
|
||||||
type MapPresentationProfile,
|
type MapPresentationProfile,
|
||||||
} from "./mapPresentationProfile.js";
|
} from "./mapPresentationProfile.js";
|
||||||
|
import { normalizeHGeoZoneRing } from "./hGeoZoneProjection.mjs";
|
||||||
|
|
||||||
const MAX_SPIRAL_SUBSTEPS_PER_FRAME = 300;
|
const MAX_SPIRAL_SUBSTEPS_PER_FRAME = 300;
|
||||||
const TERRAIN_SAMPLE_TIMEOUT_MS = 12_000;
|
const TERRAIN_SAMPLE_TIMEOUT_MS = 12_000;
|
||||||
|
|
@ -109,7 +110,7 @@ export type MapProviderStatus = {
|
||||||
imagery: MapProviderState;
|
imagery: MapProviderState;
|
||||||
terrain: MapProviderState;
|
terrain: MapProviderState;
|
||||||
buildings: MapProviderState;
|
buildings: MapProviderState;
|
||||||
errors: Partial<Record<"imagery" | "terrain" | "buildings", string>>;
|
errors: Partial<Record<"imagery" | "terrain" | "buildings" | "projection", string>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type IonAssetEndpoint = {
|
type IonAssetEndpoint = {
|
||||||
|
|
@ -366,19 +367,18 @@ type HGeoZoneProjectionLayer = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function hGeoZoneRingPositions(ring: Array<[number, number]>) {
|
function hGeoZoneRingPositions(ring: Array<[number, number]>) {
|
||||||
const first = ring[0];
|
return normalizeHGeoZoneRing(ring)
|
||||||
const last = ring[ring.length - 1];
|
.map(([longitude, latitude]) => Cartesian3.fromDegrees(longitude, latitude, 0));
|
||||||
const openRing = first && last && first[0] === last[0] && first[1] === last[1]
|
|
||||||
? ring.slice(0, -1)
|
|
||||||
: ring;
|
|
||||||
return openRing.map(([longitude, latitude]) => Cartesian3.fromDegrees(longitude, latitude, 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hGeoZoneHierarchy(polygon: Array<Array<[number, number]>>) {
|
function hGeoZoneHierarchy(polygon: Array<Array<[number, number]>>) {
|
||||||
return new PolygonHierarchy(
|
const outer = hGeoZoneRingPositions(polygon[0]);
|
||||||
hGeoZoneRingPositions(polygon[0]),
|
if (outer.length < 3) return null;
|
||||||
polygon.slice(1).map((ring) => new PolygonHierarchy(hGeoZoneRingPositions(ring))),
|
const holes = polygon.slice(1)
|
||||||
);
|
.map((ring) => hGeoZoneRingPositions(ring))
|
||||||
|
.filter((ring) => ring.length >= 3)
|
||||||
|
.map((ring) => new PolygonHierarchy(ring));
|
||||||
|
return new PolygonHierarchy(outer, holes);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hGeoZoneBatches<T>(items: T[]) {
|
function hGeoZoneBatches<T>(items: T[]) {
|
||||||
|
|
@ -395,6 +395,29 @@ function removeHGeoZoneLayer(viewer: Viewer, layer: HGeoZoneProjectionLayer) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hGeoZoneFaultKey(bindingId: string, geometryKey: string) {
|
||||||
|
return `${bindingId}\u0000${geometryKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function quarantineHGeoZoneLayers(
|
||||||
|
viewer: Viewer,
|
||||||
|
layers: Map<string, HGeoZoneProjectionLayer>,
|
||||||
|
faultedGeometryKeys: Set<string>,
|
||||||
|
) {
|
||||||
|
if (!layers.size) return false;
|
||||||
|
const hasPendingGeometry = [...layers.values()].some((layer) => (
|
||||||
|
[...layer.fills, ...layer.outlines].some((batch) => !batch.primitive.ready)
|
||||||
|
));
|
||||||
|
if (!hasPendingGeometry) return false;
|
||||||
|
for (const [bindingId, layer] of layers) {
|
||||||
|
faultedGeometryKeys.add(hGeoZoneFaultKey(bindingId, layer.geometryKey));
|
||||||
|
removeHGeoZoneLayer(viewer, layer);
|
||||||
|
}
|
||||||
|
layers.clear();
|
||||||
|
viewer.scene.requestRender();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function syncHGeoZoneVisibility(viewer: Viewer, layers: Map<string, HGeoZoneProjectionLayer>) {
|
function syncHGeoZoneVisibility(viewer: Viewer, layers: Map<string, HGeoZoneProjectionLayer>) {
|
||||||
const cameraHeight = Number(viewer.camera.positionCartographic?.height || 0);
|
const cameraHeight = Number(viewer.camera.positionCartographic?.height || 0);
|
||||||
for (const layer of layers.values()) {
|
for (const layer of layers.values()) {
|
||||||
|
|
@ -477,6 +500,7 @@ function syncHGeoZoneLayers(
|
||||||
bindings: MapRuntimeBinding[],
|
bindings: MapRuntimeBinding[],
|
||||||
presentationProfiles: MapPresentationProfile[],
|
presentationProfiles: MapPresentationProfile[],
|
||||||
presentationFilters: MapPresentationFilters,
|
presentationFilters: MapPresentationFilters,
|
||||||
|
faultedGeometryKeys: Set<string>,
|
||||||
) {
|
) {
|
||||||
const activeBindings = new Set(bindings.filter((binding) => binding.slotId === "zones").map((binding) => binding.bindingId));
|
const activeBindings = new Set(bindings.filter((binding) => binding.slotId === "zones").map((binding) => binding.bindingId));
|
||||||
for (const [bindingId, layer] of layers) {
|
for (const [bindingId, layer] of layers) {
|
||||||
|
|
@ -520,22 +544,26 @@ function syncHGeoZoneLayers(
|
||||||
const polygons = fact.geometry.type === "Polygon" ? [fact.geometry.coordinates] : fact.geometry.coordinates;
|
const polygons = fact.geometry.type === "Polygon" ? [fact.geometry.coordinates] : fact.geometry.coordinates;
|
||||||
const baseEntityId = mapRuntimeEntityId(binding.bindingId, fact);
|
const baseEntityId = mapRuntimeEntityId(binding.bindingId, fact);
|
||||||
for (const [polygonIndex, polygon] of polygons.entries()) {
|
for (const [polygonIndex, polygon] of polygons.entries()) {
|
||||||
|
const hierarchy = hGeoZoneHierarchy(polygon);
|
||||||
|
if (!hierarchy) continue;
|
||||||
const instanceBase = `${baseEntityId}:part:${polygonIndex}`;
|
const instanceBase = `${baseEntityId}:part:${polygonIndex}`;
|
||||||
const fillPickId: HGeoZonePickId = {
|
const fillPickId: HGeoZonePickId = {
|
||||||
kind: "nodedc-hgeozone",
|
kind: "nodedc-hgeozone",
|
||||||
entityId: baseEntityId,
|
entityId: baseEntityId,
|
||||||
instanceId: `${instanceBase}:fill`,
|
instanceId: `${instanceBase}:fill`,
|
||||||
};
|
};
|
||||||
fillParts.push({ pickId: fillPickId, hierarchy: hGeoZoneHierarchy(polygon), color: fillColor });
|
fillParts.push({ pickId: fillPickId, hierarchy, color: fillColor });
|
||||||
geometryMembers.push(fillPickId.instanceId);
|
geometryMembers.push(fillPickId.instanceId);
|
||||||
styleMembers.push(`${fillPickId.instanceId}:${resolvedStyle?.id ?? "default"}:${fillColor.toCssHexString()}:${fillColor.alpha}`);
|
styleMembers.push(`${fillPickId.instanceId}:${resolvedStyle?.id ?? "default"}:${fillColor.toCssHexString()}:${fillColor.alpha}`);
|
||||||
for (const [ringIndex, ring] of polygon.entries()) {
|
for (const [ringIndex, ring] of polygon.entries()) {
|
||||||
|
const positions = hGeoZoneRingPositions(ring);
|
||||||
|
if (positions.length < 3) continue;
|
||||||
const outlinePickId: HGeoZonePickId = {
|
const outlinePickId: HGeoZonePickId = {
|
||||||
kind: "nodedc-hgeozone",
|
kind: "nodedc-hgeozone",
|
||||||
entityId: baseEntityId,
|
entityId: baseEntityId,
|
||||||
instanceId: `${instanceBase}:ring:${ringIndex}`,
|
instanceId: `${instanceBase}:ring:${ringIndex}`,
|
||||||
};
|
};
|
||||||
outlineParts.push({ pickId: outlinePickId, positions: hGeoZoneRingPositions(ring), color: outlineColor });
|
outlineParts.push({ pickId: outlinePickId, positions, color: outlineColor });
|
||||||
geometryMembers.push(outlinePickId.instanceId);
|
geometryMembers.push(outlinePickId.instanceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -549,6 +577,11 @@ function syncHGeoZoneLayers(
|
||||||
layers.delete(binding.bindingId);
|
layers.delete(binding.bindingId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (faultedGeometryKeys.has(hGeoZoneFaultKey(binding.bindingId, geometryKey))) {
|
||||||
|
if (current) removeHGeoZoneLayer(viewer, current);
|
||||||
|
layers.delete(binding.bindingId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (current?.geometryKey === geometryKey) {
|
if (current?.geometryKey === geometryKey) {
|
||||||
current.hideCameraHeightMeters = hideCameraHeightMeters;
|
current.hideCameraHeightMeters = hideCameraHeightMeters;
|
||||||
if (current.styleKey !== styleKey) {
|
if (current.styleKey !== styleKey) {
|
||||||
|
|
@ -586,6 +619,7 @@ function syncRuntimeDataSources(
|
||||||
bindings: MapRuntimeBinding[],
|
bindings: MapRuntimeBinding[],
|
||||||
presentationProfiles: MapPresentationProfile[],
|
presentationProfiles: MapPresentationProfile[],
|
||||||
presentationFilters: MapPresentationFilters,
|
presentationFilters: MapPresentationFilters,
|
||||||
|
faultedHGeoZoneGeometryKeys: Set<string>,
|
||||||
) {
|
) {
|
||||||
const activeBindings = new Map(bindings
|
const activeBindings = new Map(bindings
|
||||||
.filter((binding) => binding.slotId === "points" || binding.slotId === "zones")
|
.filter((binding) => binding.slotId === "points" || binding.slotId === "zones")
|
||||||
|
|
@ -714,7 +748,7 @@ function syncRuntimeDataSources(
|
||||||
|| (profile && profile.target.variant !== "surface-fill")
|
|| (profile && profile.target.variant !== "surface-fill")
|
||||||
) continue;
|
) continue;
|
||||||
const polygons = fact.geometry.type === "Polygon" ? [fact.geometry.coordinates] : fact.geometry.coordinates;
|
const polygons = fact.geometry.type === "Polygon" ? [fact.geometry.coordinates] : fact.geometry.coordinates;
|
||||||
const outerRing = polygons[0]?.[0] ?? [];
|
const outerRing = normalizeHGeoZoneRing(polygons[0]?.[0] ?? []);
|
||||||
if (!outerRing.length) continue;
|
if (!outerRing.length) continue;
|
||||||
wanted.add(baseEntityId);
|
wanted.add(baseEntityId);
|
||||||
const labelAnchor = outerRing.reduce(
|
const labelAnchor = outerRing.reduce(
|
||||||
|
|
@ -753,7 +787,14 @@ function syncRuntimeDataSources(
|
||||||
if (typeof entity.id === "string" && !wanted.has(entity.id)) dataSource.entities.remove(entity);
|
if (typeof entity.id === "string" && !wanted.has(entity.id)) dataSource.entities.remove(entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
syncHGeoZoneLayers(viewer, hGeoZoneLayers, bindings, presentationProfiles, presentationFilters);
|
syncHGeoZoneLayers(
|
||||||
|
viewer,
|
||||||
|
hGeoZoneLayers,
|
||||||
|
bindings,
|
||||||
|
presentationProfiles,
|
||||||
|
presentationFilters,
|
||||||
|
faultedHGeoZoneGeometryKeys,
|
||||||
|
);
|
||||||
viewer.scene.requestRender();
|
viewer.scene.requestRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -912,6 +953,7 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||||
const rebuildGridRef = useRef<(() => void) | null>(null);
|
const rebuildGridRef = useRef<(() => void) | null>(null);
|
||||||
const runtimeDataSourcesRef = useRef(new Map<string, CustomDataSource>());
|
const runtimeDataSourcesRef = useRef(new Map<string, CustomDataSource>());
|
||||||
const hGeoZoneLayersRef = useRef(new Map<string, HGeoZoneProjectionLayer>());
|
const hGeoZoneLayersRef = useRef(new Map<string, HGeoZoneProjectionLayer>());
|
||||||
|
const faultedHGeoZoneGeometryKeysRef = useRef(new Set<string>());
|
||||||
const presentationRef = useRef(presentation);
|
const presentationRef = useRef(presentation);
|
||||||
const runtimeBindingsRef = useRef(runtimeBindings);
|
const runtimeBindingsRef = useRef(runtimeBindings);
|
||||||
const presentationProfilesRef = useRef(presentationProfiles);
|
const presentationProfilesRef = useRef(presentationProfiles);
|
||||||
|
|
@ -1354,6 +1396,7 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||||
runtimeBindings,
|
runtimeBindings,
|
||||||
presentationProfiles,
|
presentationProfiles,
|
||||||
presentationFilters,
|
presentationFilters,
|
||||||
|
faultedHGeoZoneGeometryKeysRef.current,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [presentationFilters, presentationProfiles, runtimeBindings]);
|
}, [presentationFilters, presentationProfiles, runtimeBindings]);
|
||||||
|
|
@ -1433,14 +1476,6 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||||
const terrain = { world: null as CesiumTerrainProvider | null, ellipsoid: new EllipsoidTerrainProvider() };
|
const terrain = { world: null as CesiumTerrainProvider | null, ellipsoid: new EllipsoidTerrainProvider() };
|
||||||
viewer.terrainProvider = terrain.ellipsoid;
|
viewer.terrainProvider = terrain.ellipsoid;
|
||||||
viewer.scene.globe.depthTestAgainstTerrain = true;
|
viewer.scene.globe.depthTestAgainstTerrain = true;
|
||||||
syncRuntimeDataSources(
|
|
||||||
viewer,
|
|
||||||
runtimeDataSourcesRef.current,
|
|
||||||
hGeoZoneLayersRef.current,
|
|
||||||
runtimeBindingsRef.current,
|
|
||||||
presentationProfilesRef.current,
|
|
||||||
presentationFiltersRef.current,
|
|
||||||
);
|
|
||||||
viewerRef.current = viewer;
|
viewerRef.current = viewer;
|
||||||
terrainRef.current = terrain;
|
terrainRef.current = terrain;
|
||||||
const rebuildGrid = () => rebuildElevatedGrid(viewer!, gridDataSource, presentationRef.current);
|
const rebuildGrid = () => rebuildElevatedGrid(viewer!, gridDataSource, presentationRef.current);
|
||||||
|
|
@ -1487,9 +1522,22 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||||
stopSpiralAnimation("render_error");
|
stopSpiralAnimation("render_error");
|
||||||
const raw = error instanceof Error ? error.message : "cesium_render_error";
|
const raw = error instanceof Error ? error.message : "cesium_render_error";
|
||||||
const safe = raw.replace(/[^A-Za-z0-9_.:-]/g, "_").slice(0, 120) || "cesium_render_error";
|
const safe = raw.replace(/[^A-Za-z0-9_.:-]/g, "_").slice(0, 120) || "cesium_render_error";
|
||||||
reportProvider("imagery", "error", safe);
|
const projectionQuarantined = quarantineHGeoZoneLayers(
|
||||||
reportProvider("terrain", "error", safe);
|
viewer!,
|
||||||
reportProvider("buildings", "error", safe);
|
hGeoZoneLayersRef.current,
|
||||||
|
faultedHGeoZoneGeometryKeysRef.current,
|
||||||
|
);
|
||||||
|
if (projectionQuarantined) {
|
||||||
|
// A domain projection is not a live-provider or TileCache failure.
|
||||||
|
// Quarantine the exact binding/cursor geometry and resume the base
|
||||||
|
// scene; a new cursor or renderer restart may try a corrected set.
|
||||||
|
providerStatus.errors.projection = `hgeozone_quarantined:${safe}`;
|
||||||
|
if (!cancelled) onProviderStatus?.({ ...providerStatus, errors: { ...providerStatus.errors } });
|
||||||
|
} else {
|
||||||
|
reportProvider("imagery", "error", safe);
|
||||||
|
reportProvider("terrain", "error", safe);
|
||||||
|
reportProvider("buildings", "error", safe);
|
||||||
|
}
|
||||||
if (renderRecoveryScheduled) return;
|
if (renderRecoveryScheduled) return;
|
||||||
renderRecoveryScheduled = true;
|
renderRecoveryScheduled = true;
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
|
|
@ -1501,6 +1549,18 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||||
viewer.scene.requestRender();
|
viewer.scene.requestRender();
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
// Projection primitives are created only after the scene-level fault
|
||||||
|
// boundary is active. Their asynchronous Cesium workers must never be
|
||||||
|
// able to fail before the offending domain layer can be quarantined.
|
||||||
|
syncRuntimeDataSources(
|
||||||
|
viewer,
|
||||||
|
runtimeDataSourcesRef.current,
|
||||||
|
hGeoZoneLayersRef.current,
|
||||||
|
runtimeBindingsRef.current,
|
||||||
|
presentationProfilesRef.current,
|
||||||
|
presentationFiltersRef.current,
|
||||||
|
faultedHGeoZoneGeometryKeysRef.current,
|
||||||
|
);
|
||||||
|
|
||||||
if (config?.gatewayReady) {
|
if (config?.gatewayReady) {
|
||||||
// Do not serialize provider startup. A failure in Bing imagery is
|
// Do not serialize provider startup. A failure in Bing imagery is
|
||||||
|
|
@ -1648,6 +1708,7 @@ export const CesiumMapRenderer = forwardRef<CesiumMapRendererHandle, {
|
||||||
rebuildGridRef.current = null;
|
rebuildGridRef.current = null;
|
||||||
runtimeDataSourcesRef.current.clear();
|
runtimeDataSourcesRef.current.clear();
|
||||||
hGeoZoneLayersRef.current.clear();
|
hGeoZoneLayersRef.current.clear();
|
||||||
|
faultedHGeoZoneGeometryKeysRef.current.clear();
|
||||||
};
|
};
|
||||||
}, [onGatewayHealth, onProviderStatus, stopSpiralAnimation]);
|
}, [onGatewayHealth, onProviderStatus, stopSpiralAnimation]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export type HGeoZoneCoordinate = [number, number];
|
||||||
|
|
||||||
|
export declare function normalizeHGeoZoneRing(
|
||||||
|
ring: HGeoZoneCoordinate[],
|
||||||
|
): HGeoZoneCoordinate[];
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Canonicalize a provider-neutral GeoJSON linear ring before it reaches a
|
||||||
|
* Cesium geometry worker. GeoJSON closes a ring by repeating its first
|
||||||
|
* coordinate, while GroundPolylineGeometry with `loop: true` expects an open
|
||||||
|
* sequence. Some source snapshots contain both adjacent duplicates and more
|
||||||
|
* than one closing coordinate; forwarding either shape can create a
|
||||||
|
* zero-length segment and stop Cesium's render loop.
|
||||||
|
*
|
||||||
|
* Invalid coordinates invalidate the complete ring. Silently retaining a
|
||||||
|
* partial polygon would display a boundary the provider never published.
|
||||||
|
*
|
||||||
|
* @param {Array<[number, number]>} ring
|
||||||
|
* @returns {Array<[number, number]>}
|
||||||
|
*/
|
||||||
|
export function normalizeHGeoZoneRing(ring) {
|
||||||
|
if (!Array.isArray(ring)) return [];
|
||||||
|
/** @type {Array<[number, number]>} */
|
||||||
|
const normalized = [];
|
||||||
|
for (const coordinate of ring) {
|
||||||
|
if (
|
||||||
|
!Array.isArray(coordinate)
|
||||||
|
|| coordinate.length < 2
|
||||||
|
|| !Number.isFinite(coordinate[0])
|
||||||
|
|| !Number.isFinite(coordinate[1])
|
||||||
|
|| coordinate[0] < -180
|
||||||
|
|| coordinate[0] > 180
|
||||||
|
|| coordinate[1] < -90
|
||||||
|
|| coordinate[1] > 90
|
||||||
|
) return [];
|
||||||
|
const next = /** @type {[number, number]} */ ([coordinate[0], coordinate[1]]);
|
||||||
|
if (!sameCoordinate(normalized.at(-1), next)) normalized.push(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (normalized.length > 1 && sameCoordinate(normalized[0], normalized.at(-1))) {
|
||||||
|
normalized.pop();
|
||||||
|
}
|
||||||
|
const distinct = new Set(normalized.map(([longitude, latitude]) => `${longitude}:${latitude}`));
|
||||||
|
return distinct.size >= 3 ? normalized : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {[number, number] | undefined} left
|
||||||
|
* @param {[number, number] | undefined} right
|
||||||
|
*/
|
||||||
|
function sameCoordinate(left, right) {
|
||||||
|
return Boolean(left && right && left[0] === right[0] && left[1] === right[1]);
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build --workspace @nodedc/ui-core && npm run build --workspace @nodedc/ui-dom && npm run build --workspace @nodedc/ui-react && npm run build --workspace @nodedc/page-patterns && npm run build --workspace @nodedc/ui-catalog",
|
"build": "npm run build --workspace @nodedc/ui-core && npm run build --workspace @nodedc/ui-dom && npm run build --workspace @nodedc/ui-react && npm run build --workspace @nodedc/page-patterns && npm run build --workspace @nodedc/ui-catalog",
|
||||||
"build:packages": "npm run build --workspace @nodedc/ui-core && npm run build --workspace @nodedc/ui-dom && npm run build --workspace @nodedc/ui-react && npm run build --workspace @nodedc/page-patterns",
|
"build:packages": "npm run build --workspace @nodedc/ui-core && npm run build --workspace @nodedc/ui-dom && npm run build --workspace @nodedc/ui-react && npm run build --workspace @nodedc/page-patterns",
|
||||||
"check": "npm run build:packages && npm run typecheck --workspaces --if-present && npm run validate:registry && npm run test:inspector-select",
|
"check": "npm run build:packages && npm run typecheck --workspaces --if-present && npm run validate:registry && npm run test:inspector-select && npm run test:hgeozone-projection",
|
||||||
"dev": "npm run build:packages && npm run dev --workspace @nodedc/ui-catalog",
|
"dev": "npm run build:packages && npm run dev --workspace @nodedc/ui-catalog",
|
||||||
"serve": "node server/catalog-server.mjs",
|
"serve": "node server/catalog-server.mjs",
|
||||||
"validate:registry": "node scripts/validate-registry.mjs",
|
"validate:registry": "node scripts/validate-registry.mjs",
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
"test:foundry-agent": "node --test server/foundry-agent-store.test.mjs server/foundry-agent-gateway.test.mjs scripts/foundry-agent-installer.test.mjs",
|
"test:foundry-agent": "node --test server/foundry-agent-store.test.mjs server/foundry-agent-gateway.test.mjs scripts/foundry-agent-installer.test.mjs",
|
||||||
"test:map-animation": "node --test scripts/map-spiral.test.mjs scripts/map-camera-presets.test.mjs",
|
"test:map-animation": "node --test scripts/map-spiral.test.mjs scripts/map-camera-presets.test.mjs",
|
||||||
"test:map-filters": "node --test scripts/map-presentation-filters.test.mjs",
|
"test:map-filters": "node --test scripts/map-presentation-filters.test.mjs",
|
||||||
|
"test:hgeozone-projection": "node --test scripts/hgeozone-projection.test.mjs",
|
||||||
"test:map-cache-contract": "node --test scripts/map-cache-resource-contract.test.mjs",
|
"test:map-cache-contract": "node --test scripts/map-cache-resource-contract.test.mjs",
|
||||||
"test:inspector-select": "node --test scripts/inspector-select-contract.test.mjs"
|
"test:inspector-select": "node --test scripts/inspector-select-contract.test.mjs"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import test from "node:test";
|
||||||
|
import ApproximateTerrainHeights from "@cesium/engine/Source/Core/ApproximateTerrainHeights.js";
|
||||||
|
import Cartesian3 from "@cesium/engine/Source/Core/Cartesian3.js";
|
||||||
|
import GroundPolylineGeometry from "@cesium/engine/Source/Core/GroundPolylineGeometry.js";
|
||||||
|
import { normalizeHGeoZoneRing } from "../apps/catalog/src/hGeoZoneProjection.mjs";
|
||||||
|
|
||||||
|
test("normalizes GeoJSON closure and adjacent duplicates without changing the polygon", () => {
|
||||||
|
assert.deepEqual(normalizeHGeoZoneRing([
|
||||||
|
[37, 55],
|
||||||
|
[37, 55],
|
||||||
|
[38, 55],
|
||||||
|
[38, 56],
|
||||||
|
[37, 55],
|
||||||
|
[37, 55],
|
||||||
|
]), [
|
||||||
|
[37, 55],
|
||||||
|
[38, 55],
|
||||||
|
[38, 56],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects an invalid or degenerate ring instead of publishing partial geometry", () => {
|
||||||
|
assert.deepEqual(normalizeHGeoZoneRing([[37, 55], [38, 55], [37, 55]]), []);
|
||||||
|
assert.deepEqual(normalizeHGeoZoneRing([[37, 55], [38, Number.NaN], [38, 56], [37, 55]]), []);
|
||||||
|
assert.deepEqual(normalizeHGeoZoneRing([[37, 55], [181, 55], [38, 56], [37, 55]]), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalized double-closed ring is accepted by Cesium GroundPolylineGeometry", async () => {
|
||||||
|
ApproximateTerrainHeights._terrainHeights = JSON.parse(await readFile(
|
||||||
|
new URL("../node_modules/@cesium/engine/Source/Assets/approximateTerrainHeights.json", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
));
|
||||||
|
const positions = normalizeHGeoZoneRing([
|
||||||
|
[37.55, 55.75],
|
||||||
|
[37.56, 55.75],
|
||||||
|
[37.56, 55.76],
|
||||||
|
[37.55, 55.75],
|
||||||
|
[37.55, 55.75],
|
||||||
|
]).map(([longitude, latitude]) => Cartesian3.fromDegrees(longitude, latitude));
|
||||||
|
const geometry = GroundPolylineGeometry.createGeometry(new GroundPolylineGeometry({
|
||||||
|
positions,
|
||||||
|
width: 1.5,
|
||||||
|
loop: true,
|
||||||
|
}));
|
||||||
|
assert.ok(geometry);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue