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);
});