import './_planet.mjs'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import * as G from '../js/geo.js'; const near = (a, b, eps = 1e-9) => assert.ok(Math.abs(a - b) <= eps, `${a} vs ${b}`); test('lat/lon <-> vector round trip incl. poles and seam', () => { for (const [lat, lon] of [[0, 0], [45, 120], [-60, -170], [89.999, 10], [-89.999, -100], [10, 179.999], [10, -179.999]]) { const p = G.toLatLon(G.toVec(lat, lon)); near(p.lat, lat, 1e-9); near(p.lon, lon, 1e-7); } const pole = G.toLatLon(G.toVec(90, 0)); near(pole.lat, 90, 1e-9); assert.ok(Number.isFinite(pole.lon)); }); test('distances on the 12,742 km sphere', () => { near(G.distanceKm({ lat: 0, lon: 0 }, { lat: 0, lon: 90 }), (Math.PI / 2) * 12742, 1e-6); near(G.distanceKm({ lat: 90, lon: 0 }, { lat: -90, lon: 0 }), Math.PI * 12742, 1e-6); near(G.distanceKm({ lat: 10, lon: 179 }, { lat: 10, lon: -179 }), G.distanceKm({ lat: 10, lon: 0 }, { lat: 10, lon: 2 }), 1e-6); }); test('interpolate follows the great circle', () => { const pts = G.interpolate({ lat: 0, lon: 0 }, { lat: 0, lon: 90 }, 10); assert.equal(pts.length, 10); near(pts[5].lon, 50, 1e-9); near(pts[5].lat, 0, 1e-9); }); test('spherical polygon area', () => { near(G.polygonAreaKm2([{ lat: 0, lon: 0 }, { lat: 0, lon: 90 }, { lat: 90, lon: 0 }]), (4 * Math.PI * 12742 ** 2) / 8, 1); const cell = ((12742 * Math.PI) / 180) ** 2; near(G.polygonAreaKm2([{ lat: 0, lon: 0 }, { lat: 0, lon: 1 }, { lat: 1, lon: 1 }, { lat: 1, lon: 0 }]), cell, cell * 0.01); }); test('equal earth matches mapgen/projections.py', () => { const fx = JSON.parse(readFileSync(new URL('./fixtures/projections.json', import.meta.url))); near(G.EE_XMAX, fx.ee_xmax); near(G.EE_YMAX, fx.ee_ymax); for (const p of fx.equal_earth) { const q = G.project('equal_earth', p.lat, p.lon); near(q.x, p.x); near(q.y, p.y); const r = G.unproject('equal_earth', p.x, p.y); near(r.lat, p.lat, 1e-6); near(r.lon, p.lon, 1e-6); } assert.equal(G.unproject('equal_earth', G.EE_XMAX, G.EE_YMAX), null); near(G.unproject('equal_earth', 0, G.EE_YMAX).lat, 90, 1e-6); // pole survives rounding }); test('plate carree projection', () => { const q = G.project('plate_carree', 45, -90); near(q.x, -Math.PI / 2); near(q.y, Math.PI / 4); assert.equal(G.unproject('plate_carree', 4, 0), null); }); test('sphere mesh: uv follows lat/lon and faces point outward', () => { const m = G.sphereMesh(8, 16); for (let i = 0; i < m.uvs.length / 2; i++) { const ll = G.toLatLon([m.positions[3 * i], m.positions[3 * i + 1], m.positions[3 * i + 2]]); const u = m.uvs[2 * i], v = m.uvs[2 * i + 1]; near((ll.lat + 90) / 180, v, 1e-6); if (Math.abs(ll.lat) < 89.9 && u > 1e-6 && u < 1 - 1e-6) near((ll.lon + 180) / 360, u, 1e-6); } assert.equal(m.indices.length, 8 * 16 * 6); const q = 3 * 16 * 6 + 5 * 6; // a quad just north of the equator const P = k => [m.positions[3 * k], m.positions[3 * k + 1], m.positions[3 * k + 2]]; const [a, b, c] = [P(m.indices[q]), P(m.indices[q + 1]), P(m.indices[q + 2])]; const u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]], w = [c[0] - a[0], c[1] - a[1], c[2] - a[2]]; const n = [u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]]; assert.ok(n[0] * a[0] + n[1] * a[1] + n[2] * a[2] > 0, 'triangles wind counter-clockwise seen from outside'); });