feat(map): add fixed ENU and WGS84 sector grid

This commit is contained in:
Codex
2026-08-06 10:31:03 +03:00
parent 80e948c018
commit 6f1fcb1eb0
15 changed files with 1806 additions and 362 deletions
+231
View File
@@ -0,0 +1,231 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
ArcType,
ApproximateTerrainHeights,
Cartesian3,
GroundPolylineGeometry,
Math as CesiumMath,
PolylineGeometry,
} from "cesium";
import {
alignedGridValues,
boundedAngularParts,
fixedGridOrigin,
graticuleGranularity,
graticuleLinePlan,
graticuleSectorAt,
graticuleSectorBounds,
localGridPlan,
MAX_LOCAL_GRID_INDEX,
localParentSector,
localSectorAt,
localSectorBounds,
localSectorNeighbors,
normalizeLongitudeDegrees,
splitLongitudeRange,
} from "../apps/catalog/src/mapSectorGrid.mjs";
const localDefinition = {
lod: 1,
originLatitude: 55.7558,
originLongitude: 37.6173,
stepMeters: 1_000,
};
test("fixed origin normalizes WGS84 coordinates without consulting the camera", () => {
assert.deepEqual(fixedGridOrigin({}), { latitude: 55.7558, longitude: 37.6173 });
assert.deepEqual(fixedGridOrigin({ gridCenterLatitude: 100, gridCenterLongitude: 540 }), {
latitude: 89.9,
longitude: -180,
});
assert.equal(normalizeLongitudeDegrees(-540), -180);
assert.equal(normalizeLongitudeDegrees(360), 0);
assert.equal(Object.is(normalizeLongitudeDegrees(-360), -0), false);
});
test("local sectors use half-open floor boundaries on both sides of the ENU origin", () => {
assert.deepEqual(
[
[0, 0],
[999.999, 999.999],
[1_000, 1_000],
[-0.001, -0.001],
[-1_000, -1_000],
[-1_000.001, -1_000.001],
].map(([eastMeters, northMeters]) => {
const sector = localSectorAt({ eastMeters, northMeters }, localDefinition);
return [sector.eastIndex, sector.northIndex];
}),
[
[0, 0],
[0, 0],
[1, 1],
[-1, -1],
[-1, -1],
[-2, -2],
],
);
});
test("local sector IDs and bounds stay stable across calls and normalized equivalent origins", () => {
const first = localSectorAt({ eastMeters: -1, northMeters: 2_500 }, localDefinition);
const second = localSectorAt({ eastMeters: -1, northMeters: 2_500 }, {
...localDefinition,
originLongitude: localDefinition.originLongitude + 360,
});
assert.equal(first.id, "grid/local/55.755800,37.617300/l1/s1000.000/e-1/n+2");
assert.equal(second.id, first.id);
assert.deepEqual(localSectorBounds(first, localDefinition.stepMeters), {
west: -1_000,
east: 0,
south: 2_000,
north: 3_000,
});
});
test("local sector neighbors and integer-ratio parents preserve signed addressing", () => {
const child = localSectorAt({ eastMeters: -1, northMeters: 2_500 }, localDefinition);
const neighbors = localSectorNeighbors(child, localDefinition);
assert.deepEqual(
Object.fromEntries(Object.entries(neighbors).map(([direction, address]) => [
direction,
[address.eastIndex, address.northIndex],
])),
{
north: [-1, 3],
east: [0, 2],
south: [-1, 1],
west: [-2, 2],
},
);
const parent = localParentSector(child, 1_000, { ...localDefinition, lod: 2, stepMeters: 5_000 });
assert.deepEqual([parent.eastIndex, parent.northIndex], [-1, 0]);
assert.equal(parent.id, "grid/local/55.755800,37.617300/l2/s5000.000/e-1/n+0");
assert.throws(
() => localParentSector(child, 1_000, { ...localDefinition, lod: 2, stepMeters: 2_500 }),
/grid_parent_step_must_be_integer_multiple/,
);
});
test("local grid plans are symmetric about the immutable origin and cap marker density", () => {
const plan = localGridPlan({ stepMeters: 1_000, radiusMeters: 2_000, maximumMarkers: 4 });
assert.deepEqual(plan.lines.map(({ index, offsetMeters }) => [index, offsetMeters]), [
[-2, -2_000],
[-1, -1_000],
[0, 0],
[1, 1_000],
[2, 2_000],
]);
assert.equal(plan.lines[0].extentMeters, 0);
assert.equal(plan.lines[2].extentMeters, 2_000);
assert.equal(plan.markerStride, 2);
const bounded = localGridPlan({ stepMeters: 100, radiusMeters: 100_000_000 });
assert.equal(bounded.maximumIndex, MAX_LOCAL_GRID_INDEX);
assert.equal(bounded.clipped, true);
assert.ok(bounded.lines.length <= MAX_LOCAL_GRID_INDEX * 2 + 1);
});
test("bounded RHUMB parts compile through Cesium at the equator, poles and date line", () => {
const compile = (positions) => PolylineGeometry.createGeometry(new PolylineGeometry({
positions,
width: 1,
arcType: ArcType.RHUMB,
granularity: CesiumMath.toRadians(2),
}));
for (const part of boundedAngularParts(-180, 180)) {
assert.ok(compile([
Cartesian3.fromDegrees(part.start, 0, 500),
Cartesian3.fromDegrees(part.end, 0, 500),
]));
}
for (const part of boundedAngularParts(-89.9, 89.9)) {
assert.ok(compile([
Cartesian3.fromDegrees(180, part.start, 500),
Cartesian3.fromDegrees(180, part.end, 500),
]));
}
});
test("ground graticule uses metre granularity and compiles without explosive subdivision", () => {
const stepDegrees = 2;
const granularity = graticuleGranularity(stepDegrees, true);
assert.ok(granularity > 200_000 && granularity < 225_000);
assert.equal(graticuleGranularity(stepDegrees, false), CesiumMath.toRadians(stepDegrees));
const positions = boundedAngularParts(-180, 180)
.reduce((values, part, index) => [
...values,
...(index === 0 ? [part.start] : []),
part.end,
], [])
.map((longitude) => Cartesian3.fromDegrees(longitude, 0));
const previousTerrainHeights = ApproximateTerrainHeights._terrainHeights;
try {
ApproximateTerrainHeights._terrainHeights = {};
const source = new GroundPolylineGeometry({
positions,
width: 1,
arcType: ArcType.RHUMB,
granularity,
});
assert.equal(source.granularity, granularity);
assert.ok(GroundPolylineGeometry.createGeometry(source));
} finally {
ApproximateTerrainHeights._terrainHeights = previousTerrainHeights;
}
});
test("anti-meridian ranges split explicitly while whole-world ranges keep one interval", () => {
assert.deepEqual(splitLongitudeRange(170, -170), [
{ west: 170, east: 180 },
{ west: -180, east: -170 },
]);
assert.deepEqual(splitLongitudeRange(-170, 170), [{ west: -170, east: 170 }]);
assert.deepEqual(splitLongitudeRange(-180, 180), [{ west: -180, east: 180 }]);
assert.deepEqual(splitLongitudeRange(10, 370), [{ west: -180, east: 180 }]);
});
test("graticule lines retain a global zero phase and never duplicate the date-line meridian", () => {
const longitudeIntervals = splitLongitudeRange(170, -170);
const plan = graticuleLinePlan({
south: -3.7,
north: 3.7,
longitudeIntervals,
stepDegrees: 2,
});
assert.deepEqual(plan.parallels, [-2, 0, 2]);
assert.deepEqual(plan.meridians.map(({ longitude }) => longitude), [
170, 172, 174, 176, 178,
-180, -178, -176, -174, -172, -170,
]);
assert.equal(new Set(plan.meridians.map(({ longitude }) => longitude)).size, plan.meridians.length);
assert.deepEqual(alignedGridValues(1, 7, 2), [2, 4, 6]);
const world = graticuleLinePlan({
south: -2,
north: 2,
longitudeIntervals: [{ west: -180, east: 180 }],
stepDegrees: 2,
});
assert.equal(world.meridians.some(({ longitude }) => longitude === 180), false);
assert.equal(world.meridians.filter(({ longitude }) => longitude === -180).length, 1);
});
test("graticule sector IDs, negative boundaries and bounds use the same global phase", () => {
const definition = { lod: 4, stepDegrees: 2 };
const negative = graticuleSectorAt({ longitude: -0.0001, latitude: -0.0001 }, definition);
const zero = graticuleSectorAt({ longitude: 0, latitude: 0 }, definition);
const seamWest = graticuleSectorAt({ longitude: -180, latitude: 0 }, definition);
const seamEast = graticuleSectorAt({ longitude: 180, latitude: 0 }, definition);
assert.deepEqual([negative.longitudeIndex, negative.latitudeIndex], [-1, -1]);
assert.equal(negative.id, "grid/wgs84/l4/s2.000000/x-1/y-1");
assert.equal(zero.id, "grid/wgs84/l4/s2.000000/x+0/y+0");
assert.deepEqual(graticuleSectorBounds(negative, definition.stepDegrees), {
west: -2,
east: 0,
south: -2,
north: 0,
});
assert.equal(seamEast.id, seamWest.id);
});