worldmap-viewer

git clone https://git.godosa.eu/worldmap-viewer

master

raw ยท 3508 bytes

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