feat(map): add functional sector grid v2

This commit is contained in:
Codex
2026-08-06 11:22:45 +03:00
parent 6f1fcb1eb0
commit d4827009d4
15 changed files with 2902 additions and 76 deletions
+406
View File
@@ -12,16 +12,44 @@ import {
alignedGridValues,
boundedAngularParts,
fixedGridOrigin,
geodeticRectangleAreaSquareMeters,
graticuleGranularity,
graticuleLinePlan,
graticuleMajorTileAt,
graticuleMajorTileBounds,
graticuleMajorTileChildren,
graticuleMajorTileForSector,
graticuleMajorTileNeighbors,
graticuleMajorTileSummary,
graticuleSectorAreaSquareMeters,
graticuleSectorAt,
graticuleSectorBounds,
graticuleSectorNeighbors,
graticuleSectorSummary,
isGraticuleMajorLineIndex,
isGraticuleMajorLineValue,
isLocalMajorLineIndex,
localGridPlan,
localMajorTileAt,
localMajorTileBounds,
localMajorTileChildren,
localMajorTileForSector,
localMajorTileId,
localMajorTileNeighbors,
localMajorTileSummary,
MAX_GRID_CHILD_PAGE_SIZE,
MAX_LOCAL_GRID_INDEX,
localParentSector,
localSectorAreaSquareMeters,
localSectorAt,
localSectorBounds,
localSectorNeighbors,
localSectorSummary,
localVolumeAt,
localVolumeBounds,
localVolumeId,
localVolumeNeighbors,
localVolumeSummary,
normalizeLongitudeDegrees,
splitLongitudeRange,
} from "../apps/catalog/src/mapSectorGrid.mjs";
@@ -33,6 +61,18 @@ const localDefinition = {
stepMeters: 1_000,
};
const localHierarchyDefinition = {
...localDefinition,
tileSizeMeters: 10_000,
};
const volumeDefinition = {
...localDefinition,
altitudeFloorMeters: -50,
altitudeCeilingMeters: 225,
altitudeBandMeters: 100,
};
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 }), {
@@ -108,6 +148,230 @@ test("local sector neighbors and integer-ratio parents preserve signed addressin
);
});
test("local major tiles keep a fixed phase across positive and negative ENU boundaries", () => {
const samples = [
[-10_000.001, -2],
[-10_000, -1],
[-0.001, -1],
[0, 0],
[9_999.999, 0],
[10_000, 1],
];
assert.deepEqual(
samples.map(([eastMeters]) => localMajorTileAt(
{ eastMeters, northMeters: 9_999.999 },
localHierarchyDefinition,
).eastIndex),
samples.map(([, expected]) => expected),
);
const tile = localMajorTileAt(
{ eastMeters: -0.001, northMeters: 9_999.999 },
localHierarchyDefinition,
);
assert.equal(tile.id, "grid/local/55.755800,37.617300/l1/s1000.000/t10000.000/e-1/n+0");
assert.equal(tile.minorPerSide, 10);
assert.deepEqual(localMajorTileBounds(tile, localHierarchyDefinition.tileSizeMeters), {
west: -10_000,
east: 0,
south: 0,
north: 10_000,
});
});
test("local major parent, children and neighbors are exact inverse hierarchy operations", () => {
const tile = localMajorTileAt(
{ eastMeters: -1, northMeters: 1 },
localHierarchyDefinition,
);
const children = localMajorTileChildren(tile, localHierarchyDefinition);
assert.equal(children.length, 100);
assert.deepEqual(
[children[0].eastIndex, children[0].northIndex, children.at(-1).eastIndex, children.at(-1).northIndex],
[-10, 0, -1, 9],
);
for (const child of children) {
assert.equal(localMajorTileForSector(child, localHierarchyDefinition).id, tile.id);
}
const neighbors = localMajorTileNeighbors(tile, localHierarchyDefinition);
assert.deepEqual(
Object.fromEntries(Object.entries(neighbors).map(([direction, address]) => [
direction,
[address.eastIndex, address.northIndex],
])),
{ north: [-1, 1], east: [0, 0], south: [-1, -1], west: [-2, 0] },
);
assert.equal(isLocalMajorLineIndex(-10, localHierarchyDefinition), true);
assert.equal(isLocalMajorLineIndex(-1, localHierarchyDefinition), false);
assert.equal(isLocalMajorLineIndex(0, localHierarchyDefinition), true);
});
test("large local major tiles require bounded child pages", () => {
const largeDefinition = { ...localDefinition, stepMeters: 100, tileSizeMeters: 50_000 };
const tile = localMajorTileAt({ eastMeters: 0, northMeters: 0 }, largeDefinition);
assert.equal(tile.minorPerSide, 500);
assert.throws(
() => localMajorTileChildren(tile, largeDefinition),
/grid_child_page_limit_exceeded/,
);
const tail = localMajorTileChildren(tile, largeDefinition, { offset: 249_998, limit: 2 });
assert.deepEqual(tail.map(({ eastIndex, northIndex }) => [eastIndex, northIndex]), [
[498, 499],
[499, 499],
]);
assert.throws(
() => localMajorTileChildren(tile, largeDefinition, { limit: MAX_GRID_CHILD_PAGE_SIZE + 1 }),
/grid_child_page_limit_exceeded/,
);
});
test("local hierarchy rejects non-integral, non-positive and unsafe definitions", () => {
assert.throws(
() => localMajorTileAt({ eastMeters: 0, northMeters: 0 }, {
...localDefinition,
tileSizeMeters: 2_500,
}),
/grid_tile_size_must_be_integer_multiple_of_step/,
);
assert.throws(
() => localMajorTileAt({ eastMeters: 0, northMeters: 0 }, {
...localDefinition,
tileSizeMeters: 0,
}),
/grid_tile_size_must_be_positive/,
);
assert.throws(
() => localMajorTileAt({ eastMeters: 0, northMeters: 0 }, {
...localDefinition,
stepMeters: Number.NaN,
tileSizeMeters: 10_000,
}),
/grid_step_must_be_positive/,
);
assert.throws(
() => localMajorTileId(localHierarchyDefinition, 0.5, 0),
/grid_index_must_be_safe_integer/,
);
assert.doesNotThrow(() => localMajorTileAt(
{ eastMeters: -0.001, northMeters: 0 },
{ ...localDefinition, stepMeters: 0.1, tileSizeMeters: 1 },
));
});
test("local summaries are UI-ready and expose neighbor and major-parent geometry", () => {
const sector = localSectorAt({ eastMeters: -1, northMeters: 2_500 }, localDefinition);
const summary = localSectorSummary(sector, localHierarchyDefinition);
assert.equal(summary.id, sector.id);
assert.deepEqual(summary.center, { eastMeters: -500, northMeters: 2_500 });
assert.equal(summary.areaSquareMeters, 1_000_000);
assert.equal(localSectorAreaSquareMeters(sector, 1_000), 1_000_000);
assert.equal(summary.neighbors.west.address.id, localSectorNeighbors(sector, localDefinition).west.id);
assert.deepEqual(summary.neighbors.north.center, { eastMeters: -500, northMeters: 3_500 });
assert.equal(summary.majorTile?.id, summary.parentMajorTile?.id);
assert.equal(summary.majorTile?.minorPerSide, 10);
assert.deepEqual(summary.majorTile?.center, { eastMeters: -5_000, northMeters: 5_000 });
assert.equal(summary.majorTile?.areaSquareMeters, 100_000_000);
const standalone = localSectorSummary(sector, localDefinition);
assert.equal(standalone.majorTile, null);
assert.equal(standalone.parentMajorTile, null);
const majorSummary = localMajorTileSummary(summary.majorTile.address, localHierarchyDefinition);
assert.equal(majorSummary.childCount, 100);
assert.equal(majorSummary.neighbors.east.address.eastIndex, 0);
});
test("local volume addressing is half-open and clips an incomplete last band", () => {
const samples = [
[-50, 0],
[49.999, 0],
[50, 1],
[149.999, 1],
[150, 2],
[224.999, 2],
];
for (const [altitudeMeters, expectedBand] of samples) {
assert.equal(
localVolumeAt({ eastMeters: -1, northMeters: 2_500, altitudeMeters }, volumeDefinition)?.bandIndex,
expectedBand,
);
}
assert.equal(localVolumeAt({ eastMeters: 0, northMeters: 0, altitudeMeters: -50.001 }, volumeDefinition), null);
assert.equal(localVolumeAt({ eastMeters: 0, northMeters: 0, altitudeMeters: 225 }, volumeDefinition), null);
const last = localVolumeAt({ eastMeters: -1, northMeters: 2_500, altitudeMeters: 200 }, volumeDefinition);
assert.ok(last);
assert.equal(last.id, "grid/local-volume/55.755800,37.617300/l1/s1000.000/f-50.000/c225.000/h100.000/e-1/n+2/z+2");
assert.equal(last.altitudeBandMeters, 75);
assert.deepEqual(localVolumeBounds(last, volumeDefinition), {
west: -1_000,
east: 0,
south: 2_000,
north: 3_000,
altitudeFloorMeters: 150,
altitudeCeilingMeters: 225,
});
const summary = localVolumeSummary(last, volumeDefinition);
assert.equal(summary.address.id, last.id);
assert.deepEqual(summary.center, {
eastMeters: -500,
northMeters: 2_500,
altitudeMeters: 187.5,
});
assert.equal(summary.footprintAreaSquareMeters, 1_000_000);
assert.equal(summary.volumeCubicMeters, 75_000_000);
});
test("local volume neighbors stop at vertical limits but remain unbounded horizontally", () => {
const first = localVolumeAt({ eastMeters: 0, northMeters: 0, altitudeMeters: 0 }, volumeDefinition);
assert.ok(first);
const firstNeighbors = localVolumeNeighbors(first, volumeDefinition);
assert.equal(firstNeighbors.below, null);
assert.equal(firstNeighbors.above?.bandIndex, 1);
assert.equal(firstNeighbors.west.eastIndex, -1);
const last = localVolumeAt({ eastMeters: 0, northMeters: 0, altitudeMeters: 200 }, volumeDefinition);
assert.ok(last);
const lastNeighbors = localVolumeNeighbors(last, volumeDefinition);
assert.equal(lastNeighbors.above, null);
assert.equal(lastNeighbors.below?.bandIndex, 1);
});
test("local volume contract rejects degenerate ranges, bands and invalid addresses", () => {
assert.throws(
() => localVolumeAt({ eastMeters: 0, northMeters: 0, altitudeMeters: 0 }, {
...volumeDefinition,
altitudeBandMeters: 0,
}),
/grid_altitude_band_must_be_positive/,
);
assert.throws(
() => localVolumeAt({ eastMeters: 0, northMeters: 0, altitudeMeters: 0 }, {
...volumeDefinition,
altitudeCeilingMeters: -50,
}),
/grid_altitude_ceiling_must_exceed_floor/,
);
assert.throws(
() => localVolumeAt({ eastMeters: 0, northMeters: 0, altitudeMeters: 0 }, {
...volumeDefinition,
altitudeFloorMeters: Number.NaN,
}),
/grid_altitude_floor_must_be_finite/,
);
assert.equal(
localVolumeAt({ eastMeters: 0, northMeters: 0, altitudeMeters: Number.NaN }, volumeDefinition),
null,
);
assert.throws(
() => localVolumeId(volumeDefinition, 0, 0, -1),
/grid_altitude_band_index_out_of_range/,
);
assert.throws(
() => localVolumeId(volumeDefinition, 0, 0, 3),
/grid_altitude_band_index_out_of_range/,
);
});
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]), [
@@ -229,3 +493,145 @@ test("graticule sector IDs, negative boundaries and bounds use the same global p
});
assert.equal(seamEast.id, seamWest.id);
});
test("graticule minor neighbors wrap at the date line and stop at both poles", () => {
const definition = { lod: 4, stepDegrees: 2 };
const northEast = graticuleSectorAt({ longitude: 179.999, latitude: 89.999 }, definition);
const northEastNeighbors = graticuleSectorNeighbors(northEast, definition);
assert.equal(northEastNeighbors.north, null);
assert.deepEqual(
[northEastNeighbors.east.longitudeIndex, northEastNeighbors.east.latitudeIndex],
[-90, 44],
);
assert.deepEqual(
[northEastNeighbors.west.longitudeIndex, northEastNeighbors.west.latitudeIndex],
[88, 44],
);
const southWest = graticuleSectorAt({ longitude: -180, latitude: -90 }, definition);
const southWestNeighbors = graticuleSectorNeighbors(southWest, definition);
assert.equal(southWestNeighbors.south, null);
assert.equal(southWestNeighbors.west.longitudeIndex, 89);
const unevenDefinition = { lod: 4, stepDegrees: 7 };
const unevenWest = graticuleSectorAt({ longitude: -180, latitude: 0 }, unevenDefinition);
const unevenNeighbors = graticuleSectorNeighbors(unevenWest, unevenDefinition);
assert.equal(unevenNeighbors.west.longitudeIndex, 25);
assert.throws(
() => graticuleSectorNeighbors({ ...unevenWest, latitudeIndex: 13 }, unevenDefinition),
/grid_graticule_latitude_index_out_of_range/,
);
});
test("graticule major hierarchy has stable IDs, exact children and seam-safe neighbors", () => {
const definition = { lod: 4, stepDegrees: 2, majorStepDegrees: 10 };
const tile = graticuleMajorTileAt({ longitude: -0.001, latitude: 9.999 }, definition);
assert.equal(tile.id, "grid/wgs84/l4/s2.000000/m10.000000/x-1/y+0");
assert.equal(tile.minorPerSide, 5);
assert.deepEqual(graticuleMajorTileBounds(tile, definition.majorStepDegrees), {
west: -10,
east: 0,
south: 0,
north: 10,
});
const children = graticuleMajorTileChildren(tile, definition);
assert.equal(children.length, 25);
assert.deepEqual(
[children[0].longitudeIndex, children[0].latitudeIndex, children.at(-1).longitudeIndex, children.at(-1).latitudeIndex],
[-5, 0, -1, 4],
);
for (const child of children) {
assert.equal(graticuleMajorTileForSector(child, definition).id, tile.id);
}
const seam = graticuleMajorTileAt({ longitude: 179.999, latitude: 89.999 }, definition);
const seamNeighbors = graticuleMajorTileNeighbors(seam, definition);
assert.equal(seam.longitudeIndex, 17);
assert.equal(seam.latitudeIndex, 8);
assert.equal(seamNeighbors.north, null);
assert.equal(seamNeighbors.east.longitudeIndex, -18);
assert.equal(graticuleMajorTileAt({ longitude: 180, latitude: 0 }, definition).longitudeIndex, -18);
assert.equal(graticuleMajorTileAt({ longitude: -180, latitude: 0 }, definition).id,
graticuleMajorTileAt({ longitude: 180, latitude: 0 }, definition).id);
assert.equal(
graticuleMajorTileAt({ longitude: -180, latitude: 0 }, definition).id,
graticuleMajorTileAt({ longitude: 540, latitude: 0 }, definition).id,
);
assert.equal(isGraticuleMajorLineIndex(-5, definition), true);
assert.equal(isGraticuleMajorLineIndex(-4, definition), false);
assert.equal(isGraticuleMajorLineValue(-10, definition), true);
assert.equal(isGraticuleMajorLineValue(-9.999, definition), false);
});
test("large graticule major tiles expose children through bounded pages", () => {
const definition = { lod: 4, stepDegrees: 0.1, majorStepDegrees: 90 };
const tile = graticuleMajorTileAt({ longitude: 0, latitude: 0 }, definition);
assert.equal(tile.minorPerSide, 900);
assert.throws(
() => graticuleMajorTileChildren(tile, definition),
/grid_child_page_limit_exceeded/,
);
const tail = graticuleMajorTileChildren(tile, definition, { offset: 809_999, limit: 1 });
assert.deepEqual(
[tail[0].longitudeIndex, tail[0].latitudeIndex],
[899, 899],
);
});
test("graticule hierarchy rejects ambiguous global partitions and malformed indices", () => {
assert.throws(
() => graticuleMajorTileAt({ longitude: 0, latitude: 0 }, {
lod: 4,
stepDegrees: 2,
majorStepDegrees: 5,
}),
/grid_graticule_major_step_must_be_integer_multiple_of_step/,
);
assert.throws(
() => graticuleMajorTileAt({ longitude: 0, latitude: 0 }, {
lod: 4,
stepDegrees: 1,
majorStepDegrees: 7,
}),
/grid_graticule_major_step_must_partition_hemisphere/,
);
assert.throws(
() => graticuleMajorTileAt({ longitude: 0, latitude: 0 }, {
lod: 4,
stepDegrees: 1,
majorStepDegrees: 0,
}),
/grid_graticule_major_step_must_be_positive/,
);
assert.throws(
() => graticuleMajorTileBounds({ longitudeIndex: 0, latitudeIndex: 9.5 }, 10),
/grid_index_must_be_safe_integer/,
);
});
test("WGS84 summaries expose ellipsoidal area, centers, neighbors and major parents", () => {
const definition = { lod: 4, stepDegrees: 2, majorStepDegrees: 10 };
const sector = graticuleSectorAt({ longitude: -0.001, latitude: -0.001 }, definition);
const summary = graticuleSectorSummary(sector, definition);
assert.deepEqual(summary.center, { longitude: -1, latitude: -1 });
assert.equal(summary.address.id, sector.id);
assert.equal(summary.neighbors.east.address.id,
graticuleSectorAt({ longitude: 0, latitude: -0.001 }, definition).id);
assert.equal(summary.majorTile?.id, summary.parentMajorTile?.id);
assert.deepEqual(summary.majorTile?.bounds, { west: -10, east: 0, south: -10, north: 0 });
assert.equal(summary.majorTile?.childCount, 25);
assert.ok(summary.areaSquareMeters > 49_000_000_000 && summary.areaSquareMeters < 50_000_000_000);
const equator = graticuleSectorAt({ longitude: 0, latitude: 0 }, definition);
const polar = graticuleSectorAt({ longitude: 0, latitude: 89 }, definition);
assert.ok(
graticuleSectorAreaSquareMeters(equator, definition.stepDegrees)
> graticuleSectorAreaSquareMeters(polar, definition.stepDegrees) * 20,
);
const worldArea = geodeticRectangleAreaSquareMeters({ west: -180, east: 180, south: -90, north: 90 });
assert.ok(worldArea > 5.10e14 && worldArea < 5.11e14);
const majorSummary = graticuleMajorTileSummary(summary.majorTile.address, definition);
assert.equal(majorSummary.neighbors.east.address.longitudeIndex, 0);
assert.equal(graticuleSectorSummary(sector, { lod: 4, stepDegrees: 2 }).majorTile, null);
});