aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/viewer/tests/overlay_geom.test.mjs
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
committergodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
commit3443c1c65e9f1753e1e656b35d08416c1fa298f2 (patch)
tree4e43236f460145a4d75d1b4616dcb7aa6ef08f51 /viewer/tests/overlay_geom.test.mjs
downloadworldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.tar.gz
worldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.zip
worldmap-viewer: initial public history
Diffstat (limited to 'viewer/tests/overlay_geom.test.mjs')
-rw-r--r--viewer/tests/overlay_geom.test.mjs69
1 files changed, 69 insertions, 0 deletions
diff --git a/viewer/tests/overlay_geom.test.mjs b/viewer/tests/overlay_geom.test.mjs
new file mode 100644
index 0000000..c4c6c6e
--- /dev/null
+++ b/viewer/tests/overlay_geom.test.mjs
@@ -0,0 +1,69 @@
+import './_planet.mjs';
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import * as O from '../js/overlay_geom.js';
+import { isClick } from '../js/input.js';
+
+const fc = { type: 'FeatureCollection', features: [
+ { type: 'Feature', properties: { order: 4 }, geometry: { type: 'LineString', coordinates: [[0, 0], [10, 0]] } },
+ { type: 'Feature', properties: { order: 1 }, geometry: { type: 'LineString', coordinates: [[0, 5], [1, 5]] } },
+ { type: 'Feature', properties: {}, geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]], [[0.2, 0.2], [0.4, 0.2], [0.2, 0.4], [0.2, 0.2]]] } },
+ { type: 'Feature', properties: {}, geometry: { type: 'MultiPolygon', coordinates: [[[[5, 5], [6, 5], [6, 6], [5, 5]]]] } },
+] };
+
+test('linesFromGeoJSON reads all geometry kinds and filters', () => {
+ assert.equal(O.linesFromGeoJSON(fc).length, 5);
+ const big = O.linesFromGeoJSON(fc, p => (p.order ?? 0) >= 3);
+ assert.equal(big.length, 1);
+ assert.deepEqual(big[0][1], { lat: 0, lon: 10 });
+});
+
+test('polygon rings are split where they run along the antimeridian cut', () => {
+ const cut = { type: 'FeatureCollection', features: [{ type: 'Feature', properties: {}, geometry: { type: 'Polygon',
+ coordinates: [[[170, 0], [180, 0], [180, 10], [170, 10], [170, 0]]] } }] };
+ const lines = O.linesFromGeoJSON(cut);
+ assert.equal(lines.length, 2);
+ for (const l of lines) for (let i = 0; i + 1 < l.length; i++)
+ assert.ok(!(Math.abs(l[i].lon) === 180 && Math.abs(l[i + 1].lon) === 180), 'no segment along lon ±180');
+});
+
+test('globe segments lie just above the unit sphere and follow great circles', () => {
+ const pos = O.globeSegments([[{ lat: 0, lon: 0 }, { lat: 0, lon: 10 }]], 1.002, 1);
+ assert.equal(pos.length, 10 * 6);
+ for (let i = 0; i < pos.length; i += 3) assert.ok(Math.abs(Math.hypot(pos[i], pos[i + 1], pos[i + 2]) - 1.002) < 1e-6);
+});
+
+test('flat segments drop antimeridian jumps', () => {
+ for (const view of ['equal_earth', 'plate_carree']) {
+ assert.equal(O.flatSegments([[{ lat: 10, lon: 179 }, { lat: 10, lon: -179 }]], view).length, 0);
+ assert.equal(O.flatSegments([[{ lat: 10, lon: 170 }, { lat: 10, lon: 179 }]], view).length, 6);
+ }
+});
+
+test('densify and graticule', () => {
+ const d = O.densify([[{ lat: 0, lon: 0 }, { lat: 0, lon: 3 }, { lat: 0, lon: 5 }]], 1);
+ assert.equal(d[0].length, 6);
+ assert.equal(O.graticuleLines(30).length, 13 + 5);
+});
+
+test('clicks vs drags', () => {
+ assert.equal(isClick({ x: 10, y: 10, t: 0 }, { x: 12, y: 11, t: 200 }), true);
+ assert.equal(isClick({ x: 10, y: 10, t: 0 }, { x: 40, y: 10, t: 200 }), false);
+ assert.equal(isClick({ x: 10, y: 10, t: 0 }, { x: 10, y: 10, t: 2000 }), false);
+});
+
+test('drapedSegments: lines near the camera lie on the ground, finer close up; far ones are skipped', async () => {
+ const { drapedSegments } = await import('../js/overlay_geom.js');
+ const { toVec, R_KM } = await import('../js/geo.js');
+ const R_M = R_KM * 1000, eyeDir = toVec(40, -10), eye = eyeDir.map(v => v * (1 + 1500 / R_M)); // above the ground
+ const opts = { eye, n: eyeDir, cosCap: Math.cos(0.01), origin: eyeDir, heightM: lat => (lat > 40 ? 1000 : 200) };
+ const near = [[{ lat: 40.001, lon: -10 }, { lat: 40.03, lon: -10 }]]; // ≈ 200 m … 4 km from the camera
+ const far = [[{ lat: 60, lon: 50 }, { lat: 61, lon: 50 }]];
+ const p = drapedSegments([...near, ...far], opts);
+ assert.ok(p.length / 6 > 10, `near segment cut into pieces: ${p.length / 6}`);
+ for (let i = 0; i < p.length; i += 3) {
+ const r = Math.hypot(p[i] + eyeDir[0], p[i + 1] + eyeDir[1], p[i + 2] + eyeDir[2]);
+ assert.ok(r > 1 + 1000 / R_M && r < 1 + 1020 / R_M, `vertex ${i / 3} at ${(r - 1) * R_M} m`); // ground + 1 m, pulled ≤ 1% up
+ }
+ assert.equal(drapedSegments(far, opts).length, 0);
+});