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