feat(map): add fixed ENU and WGS84 sector grid
This commit is contained in:
+229
-36
@@ -2,6 +2,8 @@ import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import {
|
||||
DEFAULT_GRID_LOD_PROFILES,
|
||||
gridLodProfile,
|
||||
gridShouldBeVisible,
|
||||
resolveGridMode,
|
||||
selectGridLod,
|
||||
@@ -11,68 +13,259 @@ import {
|
||||
const settings = {
|
||||
gridVisible: true,
|
||||
gridLodEnabled: true,
|
||||
gridLod1MaxHeightKm: 10,
|
||||
gridLod1StepKm: 1,
|
||||
gridLod1Mode: "3d",
|
||||
gridLod2MaxHeightKm: 50,
|
||||
gridLod2StepKm: 5,
|
||||
gridLod2Mode: "3d",
|
||||
gridLod3MaxHeightKm: 180,
|
||||
gridLod3StepKm: 25,
|
||||
gridLod3Mode: "3d",
|
||||
gridLod4MaxHeightKm: 700,
|
||||
gridLod4StepKm: 100,
|
||||
gridLod4Mode: "graticule",
|
||||
gridLod5StepKm: 500,
|
||||
gridLod5Mode: "graticule",
|
||||
grid3dEnabled: true,
|
||||
gridGraticuleEnabled: true,
|
||||
gridMax3dViewAngleDegrees: 30,
|
||||
gridAutoDisableHeightKm: 10_000,
|
||||
gridCenterMode: "camera",
|
||||
gridTileSizeKm: 10,
|
||||
gridCenterMode: "fixed",
|
||||
gridCenterLatitude: 55.7558,
|
||||
gridCenterLongitude: 37.6173,
|
||||
gridLodProfiles: DEFAULT_GRID_LOD_PROFILES,
|
||||
};
|
||||
|
||||
test("five grid LODs preserve the close 3D and distant graticule contract", () => {
|
||||
test("canonical defaults preserve the exact effective MMAP/MOSCOWMAP five-LOD donor profile", () => {
|
||||
assert.deepEqual(DEFAULT_GRID_LOD_PROFILES, [
|
||||
{
|
||||
maxHeightKm: 10,
|
||||
stepKm: 1,
|
||||
mode: "3d",
|
||||
heightMeters: 300,
|
||||
max3dViewAngleDegrees: 30,
|
||||
tileSizeKm: 10,
|
||||
radiusKm: 50,
|
||||
lineDiameterMeters: 4,
|
||||
lineColor: "#f5f5f5",
|
||||
lineOpacity: 12,
|
||||
dotsEnabled: true,
|
||||
dotsDiameterMeters: 7,
|
||||
dotsColor: "#ffffff",
|
||||
dotsOpacity: 58,
|
||||
crossesEnabled: false,
|
||||
crossesLengthMeters: 60,
|
||||
crossesWidthMeters: 10,
|
||||
crossesColor: "#9c9c9c",
|
||||
crossesOpacity: 46,
|
||||
graticuleStepDegrees: 0.25,
|
||||
graticuleLineWidthPx: 1,
|
||||
graticuleColor: "#f5f5f5",
|
||||
graticuleOpacity: 12,
|
||||
},
|
||||
{
|
||||
maxHeightKm: 50,
|
||||
stepKm: 5,
|
||||
mode: "3d",
|
||||
heightMeters: 500,
|
||||
max3dViewAngleDegrees: 30,
|
||||
tileSizeKm: 10,
|
||||
radiusKm: 1_000,
|
||||
lineDiameterMeters: 10,
|
||||
lineColor: "#f8fbfc",
|
||||
lineOpacity: 12,
|
||||
dotsEnabled: true,
|
||||
dotsDiameterMeters: 80,
|
||||
dotsColor: "#ffffff",
|
||||
dotsOpacity: 22,
|
||||
crossesEnabled: false,
|
||||
crossesLengthMeters: 60,
|
||||
crossesWidthMeters: 10,
|
||||
crossesColor: "#9c9c9c",
|
||||
crossesOpacity: 46,
|
||||
graticuleStepDegrees: 0.5,
|
||||
graticuleLineWidthPx: 1,
|
||||
graticuleColor: "#f8fbfc",
|
||||
graticuleOpacity: 12,
|
||||
},
|
||||
{
|
||||
maxHeightKm: 200,
|
||||
stepKm: 25,
|
||||
mode: "3d",
|
||||
heightMeters: 500,
|
||||
max3dViewAngleDegrees: 30,
|
||||
tileSizeKm: 25,
|
||||
radiusKm: 1_000,
|
||||
lineDiameterMeters: 7,
|
||||
lineColor: "#fafafa",
|
||||
lineOpacity: 12,
|
||||
dotsEnabled: true,
|
||||
dotsDiameterMeters: 10,
|
||||
dotsColor: "#ffffff",
|
||||
dotsOpacity: 58,
|
||||
crossesEnabled: false,
|
||||
crossesLengthMeters: 60,
|
||||
crossesWidthMeters: 10,
|
||||
crossesColor: "#9c9c9c",
|
||||
crossesOpacity: 46,
|
||||
graticuleStepDegrees: 1,
|
||||
graticuleLineWidthPx: 1,
|
||||
graticuleColor: "#fafafa",
|
||||
graticuleOpacity: 12,
|
||||
},
|
||||
{
|
||||
maxHeightKm: 800,
|
||||
stepKm: 50,
|
||||
mode: "graticule",
|
||||
heightMeters: 500,
|
||||
max3dViewAngleDegrees: 30,
|
||||
tileSizeKm: 10,
|
||||
radiusKm: 1_000,
|
||||
lineDiameterMeters: 10,
|
||||
lineColor: "#fcfdfd",
|
||||
lineOpacity: 12,
|
||||
dotsEnabled: true,
|
||||
dotsDiameterMeters: 10,
|
||||
dotsColor: "#fcfdfd",
|
||||
dotsOpacity: 58,
|
||||
crossesEnabled: false,
|
||||
crossesLengthMeters: 60,
|
||||
crossesWidthMeters: 10,
|
||||
crossesColor: "#9c9c9c",
|
||||
crossesOpacity: 46,
|
||||
graticuleStepDegrees: 2,
|
||||
graticuleLineWidthPx: 1,
|
||||
graticuleColor: "#fcfdfd",
|
||||
graticuleOpacity: 12,
|
||||
},
|
||||
{
|
||||
maxHeightKm: 3_000,
|
||||
stepKm: 50,
|
||||
mode: "graticule",
|
||||
heightMeters: 500,
|
||||
max3dViewAngleDegrees: 30,
|
||||
tileSizeKm: 10,
|
||||
radiusKm: 100,
|
||||
lineDiameterMeters: 7,
|
||||
lineColor: "#9c9c9c",
|
||||
lineOpacity: 12,
|
||||
dotsEnabled: true,
|
||||
dotsDiameterMeters: 10,
|
||||
dotsColor: "#9c9c9c",
|
||||
dotsOpacity: 58,
|
||||
crossesEnabled: false,
|
||||
crossesLengthMeters: 60,
|
||||
crossesWidthMeters: 10,
|
||||
crossesColor: "#9c9c9c",
|
||||
crossesOpacity: 46,
|
||||
graticuleStepDegrees: 2,
|
||||
graticuleLineWidthPx: 1,
|
||||
graticuleColor: "#9c9c9c",
|
||||
graticuleOpacity: 8,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("LOD selection keeps three spatial bands, two graticule bands and the independent 10,000 km cutoff", () => {
|
||||
assert.deepEqual(
|
||||
[5, 40, 120, 500, 2_000].map((height) => {
|
||||
[0, 10, 10.001, 50, 50.001, 200, 200.001, 800, 800.001, 3_000, 9_999].map((height) => {
|
||||
const band = selectGridLod(settings, height);
|
||||
return [band.id, band.stepKm, band.mode];
|
||||
}),
|
||||
[
|
||||
["lod-1", 1, "3d"],
|
||||
["lod-1", 1, "3d"],
|
||||
["lod-2", 5, "3d"],
|
||||
["lod-2", 5, "3d"],
|
||||
["lod-3", 25, "3d"],
|
||||
["lod-4", 100, "graticule"],
|
||||
["lod-5", 500, "graticule"],
|
||||
["lod-3", 25, "3d"],
|
||||
["lod-4", 50, "graticule"],
|
||||
["lod-4", 50, "graticule"],
|
||||
["lod-5", 50, "graticule"],
|
||||
["lod-5", 50, "graticule"],
|
||||
["lod-5", 50, "graticule"],
|
||||
],
|
||||
);
|
||||
assert.equal(gridShouldBeVisible(settings, 10_000), true);
|
||||
assert.equal(gridShouldBeVisible(settings, 10_000.001), false);
|
||||
});
|
||||
|
||||
test("LOD hysteresis holds the previous band around a threshold", () => {
|
||||
assert.equal(selectGridLod(settings, 10.5, 0).id, "lod-1");
|
||||
assert.equal(selectGridLod(settings, 10.9, 0).id, "lod-2");
|
||||
assert.equal(selectGridLod(settings, 9.5, 1).id, "lod-2");
|
||||
assert.equal(selectGridLod(settings, 9.1, 1).id, "lod-1");
|
||||
test("every LOD owns independent metric and angular spacing and visual fields", () => {
|
||||
const profiles = DEFAULT_GRID_LOD_PROFILES.map((profile, index) => ({
|
||||
...profile,
|
||||
stepKm: 11 + index,
|
||||
lineDiameterMeters: 21 + index,
|
||||
graticuleStepDegrees: 0.125 * (index + 1),
|
||||
graticuleLineWidthPx: 1 + (index % 3),
|
||||
graticuleColor: `#00000${index}`,
|
||||
graticuleOpacity: 30 + index,
|
||||
}));
|
||||
const profile = gridLodProfile({ ...settings, gridLodProfiles: profiles }, 3);
|
||||
assert.equal(profile.stepKm, 14);
|
||||
assert.equal(profile.lineDiameterMeters, 24);
|
||||
assert.equal(profile.graticuleStepDegrees, 0.5);
|
||||
assert.equal(profile.graticuleLineWidthPx, 1);
|
||||
assert.equal(profile.graticuleColor, "#000003");
|
||||
assert.equal(profile.graticuleOpacity, 33);
|
||||
});
|
||||
|
||||
test("oblique close view falls back to the graticule without dropping the grid", () => {
|
||||
assert.equal(resolveGridMode(settings, "3d", 12), "3d");
|
||||
assert.equal(resolveGridMode(settings, "3d", 48), "graticule");
|
||||
assert.equal(resolveGridMode({ ...settings, gridGraticuleEnabled: false }, "3d", 48), "hidden");
|
||||
test("legacy flat visuals migrate while angular graticule spacing stays independent", () => {
|
||||
const profile = gridLodProfile({
|
||||
...settings,
|
||||
gridLodProfiles: undefined,
|
||||
gridRadiusKm: 1_234,
|
||||
gridHeightMeters: 640,
|
||||
gridLineDiameterMeters: 17,
|
||||
gridColor: "#abcdef",
|
||||
gridOpacity: 37,
|
||||
gridDotsEnabled: false,
|
||||
gridLod2StepKm: 7,
|
||||
}, 1);
|
||||
assert.equal(profile.maxHeightKm, 50);
|
||||
assert.equal(profile.stepKm, 7);
|
||||
assert.equal(profile.radiusKm, 1_234);
|
||||
assert.equal(profile.heightMeters, 640);
|
||||
assert.equal(profile.lineDiameterMeters, 17);
|
||||
assert.equal(profile.lineColor, "#abcdef");
|
||||
assert.equal(profile.lineOpacity, 37);
|
||||
assert.equal(profile.dotsEnabled, false);
|
||||
assert.equal(profile.graticuleStepDegrees, 0.5);
|
||||
assert.equal(profile.graticuleLineWidthPx, 1);
|
||||
assert.equal(profile.graticuleColor, "#abcdef");
|
||||
assert.equal(profile.graticuleOpacity, 37);
|
||||
});
|
||||
|
||||
test("grid auto-disable and stable camera tile center are deterministic", () => {
|
||||
assert.equal(gridShouldBeVisible(settings, 9_999), true);
|
||||
assert.equal(gridShouldBeVisible(settings, 10_001), false);
|
||||
test("LOD hysteresis holds the previous band for eight percent on either side of a threshold", () => {
|
||||
assert.equal(selectGridLod(settings, 10.8, 0).id, "lod-1");
|
||||
assert.equal(selectGridLod(settings, 10.8001, 0).id, "lod-2");
|
||||
assert.equal(selectGridLod(settings, 9.2001, 1).id, "lod-2");
|
||||
assert.equal(selectGridLod(settings, 9.1999, 1).id, "lod-1");
|
||||
assert.equal(selectGridLod(settings, 864, 3).id, "lod-4");
|
||||
assert.equal(selectGridLod(settings, 864.001, 3).id, "lod-5");
|
||||
});
|
||||
|
||||
test("mode fallback preserves a usable grid when one renderer is disabled", () => {
|
||||
assert.equal(resolveGridMode(settings, "3d"), "3d");
|
||||
assert.equal(resolveGridMode(settings, "graticule"), "graticule");
|
||||
assert.equal(resolveGridMode({ ...settings, grid3dEnabled: false }, "3d"), "graticule");
|
||||
assert.equal(resolveGridMode({ ...settings, gridGraticuleEnabled: false }, "graticule"), "3d");
|
||||
assert.equal(resolveGridMode({ ...settings, grid3dEnabled: false, gridGraticuleEnabled: false }, "3d"), "hidden");
|
||||
});
|
||||
|
||||
test("grid center is fixed and camera-independent, including for legacy camera-mode payloads", () => {
|
||||
const first = snapGridCenter({ latitude: 55.7558, longitude: 37.6173 }, settings);
|
||||
const second = snapGridCenter({ latitude: 55.76, longitude: 37.62 }, settings);
|
||||
assert.deepEqual(first, second);
|
||||
const second = snapGridCenter({ latitude: -22, longitude: -140 }, settings);
|
||||
assert.deepEqual(first, { latitude: 55.7558, longitude: 37.6173 });
|
||||
assert.deepEqual(second, first);
|
||||
assert.deepEqual(snapGridCenter({ latitude: 0, longitude: 0 }, {
|
||||
...settings,
|
||||
gridCenterMode: "camera",
|
||||
gridCenterLatitude: 12.5,
|
||||
gridCenterLongitude: 190,
|
||||
}), { latitude: 12.5, longitude: -170 });
|
||||
});
|
||||
|
||||
test("renderer swaps double-buffered data sources and never clears the live grid first", async () => {
|
||||
test("renderer uses fixed ENU sectors, angular graticules and a non-blank double-buffer swap", async () => {
|
||||
const source = await readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8");
|
||||
assert.match(source, /class GridLayerController/);
|
||||
assert.match(source, /dataSourceDisplay\.ready/);
|
||||
assert.match(source, /fixedGridOrigin\(presentation\)/);
|
||||
assert.match(source, /Transforms\.eastNorthUpToFixedFrame\(anchor\)/);
|
||||
assert.match(source, /localSectorAt\(/);
|
||||
assert.match(source, /gridController\?\.pick\(worldPosition\)/);
|
||||
assert.match(source, /lod\.stepKm \* 1_000/);
|
||||
assert.match(source, /lod\.graticuleStepDegrees/);
|
||||
assert.match(source, /lod\.graticuleLineWidthPx/);
|
||||
assert.match(source, /arcType: ArcType\.RHUMB/);
|
||||
assert.match(source, /clampToGround: false/);
|
||||
assert.match(source, /mountResources\(resources\)[\s\S]*?const previous = this\.current;[\s\S]*?removeResources\(previous\.resources\)/);
|
||||
assert.doesNotMatch(source, /function rebuildElevatedGrid[\s\S]*?entities\.removeAll\(\)/);
|
||||
assert.doesNotMatch(source, /Math\.min\(40,[\s\S]*?safeRadiusKm \/ safeStepKm/);
|
||||
assert.doesNotMatch(source, /snapGridCenter\(/);
|
||||
});
|
||||
|
||||
@@ -70,10 +70,7 @@ test("facet tree selection focuses the subject without resetting a compatible de
|
||||
assert.match(preview, /setSubjectCardTabId\(\(current\) =>/);
|
||||
assert.match(preview, /profile\?\.tabs\.some\(\(tab\) => tab\.id === current\)/);
|
||||
assert.match(preview, /\(profile\?\.defaultTabId \?\? "overview"\)/);
|
||||
assert.match(renderer, /camera\.pickEllipsoid/);
|
||||
assert.match(renderer, /Cartesian3\.subtract\(camera\.position, viewportCenter/);
|
||||
assert.match(renderer, /heading: camera\.heading/);
|
||||
assert.match(renderer, /pitch: camera\.pitch/);
|
||||
assert.match(renderer, /roll: camera\.roll/);
|
||||
assert.match(renderer, /void viewer\.flyTo\(entity, \{/);
|
||||
assert.match(renderer, /duration: 0\.45/);
|
||||
assert.match(renderer, /offset: new HeadingPitchRange\(0, -0\.9, 8_000\)/);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user