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