worldmap-viewer

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

master

raw · 3838 bytes

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