90 lines
3.7 KiB
JavaScript
90 lines
3.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import test from "node:test";
|
|
import ts from "typescript";
|
|
|
|
const source = await readFile(new URL("../apps/catalog/src/mapSpiralMath.ts", import.meta.url), "utf8");
|
|
const { outputText } = ts.transpileModule(source, {
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.ES2022,
|
|
target: ts.ScriptTarget.ES2022,
|
|
},
|
|
fileName: "mapSpiralMath.ts",
|
|
});
|
|
const spiralMath = await import(`data:text/javascript;base64,${Buffer.from(outputText).toString("base64")}`);
|
|
const {
|
|
MAX_SPIRAL_RADIUS_METERS,
|
|
directGeodesicDestination,
|
|
normalizeRadians,
|
|
spiralAngleAtArcLength,
|
|
spiralArcLengthAtAngle,
|
|
spiralRadiusAtAngle,
|
|
spiralSurfaceFrame,
|
|
} = spiralMath;
|
|
|
|
const twoPi = Math.PI * 2;
|
|
|
|
test("one complete spiral turn increases radius by exactly one pitch", () => {
|
|
assert.equal(spiralRadiusAtAngle(twoPi, 10_000), 10_000);
|
|
});
|
|
|
|
test("arc-length inversion keeps configured surface distance across the route", () => {
|
|
for (const distance of [0, 1, 20, 1_000, 25_000, 1_000_000]) {
|
|
const angle = spiralAngleAtArcLength(distance, 10_000);
|
|
const restored = spiralArcLengthAtAngle(angle, 10_000);
|
|
assert.ok(Math.abs(restored - distance) <= Math.max(1e-6, distance * 1e-10), `${distance} → ${restored}`);
|
|
}
|
|
});
|
|
|
|
test("equal elapsed time produces equal traveled surface distance", () => {
|
|
const speed = 250;
|
|
const pitch = 10_000;
|
|
const atOneSecond = spiralAngleAtArcLength(speed, pitch);
|
|
const atTenSeconds = spiralAngleAtArcLength(speed * 10, pitch);
|
|
assert.ok(atTenSeconds > atOneSecond);
|
|
assert.ok(spiralRadiusAtAngle(atTenSeconds, pitch) > spiralRadiusAtAngle(atOneSecond, pitch));
|
|
assert.ok(Math.abs(spiralArcLengthAtAngle(atTenSeconds, pitch) - speed * 10) < 1e-6);
|
|
});
|
|
|
|
test("WGS84 direct destination remains finite across dateline and high latitude", () => {
|
|
const destination = directGeodesicDestination({
|
|
longitude: 179.9 * Math.PI / 180,
|
|
latitude: 84 * Math.PI / 180,
|
|
}, Math.PI / 3, 500_000);
|
|
assert.ok(Number.isFinite(destination.longitude));
|
|
assert.ok(Number.isFinite(destination.latitude));
|
|
assert.ok(destination.longitude >= -Math.PI && destination.longitude <= Math.PI);
|
|
assert.ok(destination.latitude >= -Math.PI / 2 && destination.latitude <= Math.PI / 2);
|
|
});
|
|
|
|
test("eastbound WGS84 destination at equator matches the ellipsoid circumference", () => {
|
|
const destination = directGeodesicDestination({ longitude: 0, latitude: 0 }, Math.PI / 2, 100_000);
|
|
assert.ok(Math.abs(destination.latitude) < 1e-12);
|
|
assert.ok(Math.abs(destination.longitude - 100_000 / 6_378_137) < 1e-10);
|
|
});
|
|
|
|
test("spiral frame starts at the current point and follows the current heading", () => {
|
|
const origin = { longitude: 37.6173 * Math.PI / 180, latitude: 55.7558 * Math.PI / 180 };
|
|
const heading = 0.7;
|
|
const frame = spiralSurfaceFrame(origin, heading, 0, 10_000);
|
|
assert.ok(Math.abs(normalizeRadians(frame.longitude - origin.longitude)) < 1e-12);
|
|
assert.ok(Math.abs(frame.latitude - origin.latitude) < 1e-12);
|
|
assert.equal(frame.radiusMeters, 0);
|
|
assert.ok(Math.abs(normalizeRadians(frame.tangentHeading - heading)) < 1e-12);
|
|
});
|
|
|
|
test("route stops at the documented regional WGS84 safety boundary", () => {
|
|
const pitch = 100_000;
|
|
const angle = (MAX_SPIRAL_RADIUS_METERS + pitch) * twoPi / pitch;
|
|
const distance = spiralArcLengthAtAngle(angle, pitch);
|
|
assert.throws(
|
|
() => spiralSurfaceFrame({ longitude: 0, latitude: 0 }, 0, distance, pitch),
|
|
/spiral_extent_limit/,
|
|
);
|
|
});
|
|
|
|
test("invalid animation inputs fail closed", () => {
|
|
assert.throws(() => spiralAngleAtArcLength(100, 0), /spiral_pitch_invalid/);
|
|
assert.throws(() => directGeodesicDestination({ longitude: 0, latitude: Number.NaN }, 0, 10), /spiral_origin_invalid/);
|
|
});
|