worldmap-viewer

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

master

raw · 4800 bytes

import './_planet.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { closesOutline, crossesItself, regionPayload, statusText, outlineLines } from '../js/regions.js';

test('clicking near the first point closes the outline', () => {
  assert.equal(closesOutline({ x: 100, y: 100 }, { x: 108, y: 105 }), true);
  assert.equal(closesOutline({ x: 100, y: 100 }, { x: 130, y: 100 }), false);
  assert.equal(closesOutline(null, { x: 1, y: 1 }), false);
});

test('self-crossing outlines are refused; unwraps across ±180°', () => {
  const sq = [[0, 0], [0, 2], [2, 2], [2, 0]].map(([lat, lon]) => ({ lat, lon }));
  const bow = [[0, 0], [2, 2], [0, 2], [2, 0]].map(([lat, lon]) => ({ lat, lon }));
  assert.equal(crossesItself(sq), false);
  assert.equal(crossesItself(bow), true);
  const dateline = [[0, 179], [0, -179], [2, -179], [2, 179]].map(([lat, lon]) => ({ lat, lon }));
  assert.equal(crossesItself(dateline), false);
});

test('payload, status text, outline lines', () => {
  assert.deepEqual(regionPayload('  Gorge ', [{ lat: 1, lon: 2 }, { lat: 3, lon: 4 }, { lat: 5, lon: 6 }]),
    { name: 'Gorge', outline: [[1, 2], [3, 4], [5, 6]] });
  assert.equal(statusText({ state: 'building', stage: 'erosion', fraction: 0.456 }), 'Refining… 46% (erosion)');
  assert.equal(statusText({ state: 'ready' }), 'Refined areas are up to date');
  assert.match(statusText({ state: 'failed', error: 'boom' }), /failed.*boom/i);
  assert.equal(statusText({ state: 'queued' }), 'Queued…');
  const lines = outlineLines([{ outline: [[1, 2], [3, 4], [5, 6]] }]);
  assert.equal(lines.length, 1);
  assert.deepEqual(lines[0].at(-1), { lat: 1, lon: 2 }, 'closed');
});

test('status shows the tiles being saved ahead after a build', async () => {
  const { statusText } = await import('../js/regions.js');
  assert.equal(statusText({ state: 'ready', tiles: { done: 45, total: 100 } }), 'Refined areas are up to date · saving tiles 45%');
  assert.equal(statusText({ state: 'ready', tiles: { done: 100, total: 100 } }), 'Refined areas are up to date');
  assert.equal(statusText({ state: 'idle', tiles: { done: 3, total: 10 } }), 'Saving region tiles 30%');
});

test('outline area on a 2× Earth-radius world, also across ±180°', async () => {
  const { outlineAreaKm2 } = await import('../js/regions.js');
  const box = (la0, lo0, d) => [{ lat: la0, lon: lo0 }, { lat: la0, lon: lo0 + d }, { lat: la0 + d, lon: lo0 + d }, { lat: la0 + d, lon: lo0 }];
  const want = 12742 ** 2 * (10 * Math.PI / 180) * Math.sin(10 * Math.PI / 180);
  assert.ok(Math.abs(outlineAreaKm2(box(0, 20, 10)) / want - 1) < 1e-9);
  assert.ok(Math.abs(outlineAreaKm2(box(0, 175, 10).map(p => ({ ...p, lon: p.lon > 180 ? p.lon - 360 : p.lon }))) / want - 1) < 1e-9);
  assert.ok(Math.abs(outlineAreaKm2([...box(0, 20, 10)].reverse()) / want - 1) < 1e-9, 'either winding');
});

test('tiles switch whenever the server names a different URL', async () => {
  const { needsTileSwitch } = await import('../js/regions.js');
  assert.equal(needsTileSwitch({ url: '/tiles/a/x' }, { url: '/tiles/b/x' }), true);
  assert.equal(needsTileSwitch({ url: '/tiles/a/x' }, { url: '/tiles/a/x' }), false);
  assert.equal(needsTileSwitch({ url: '/tiles/a/x' }, undefined), false);
});

test('outline clicks: add, close on the first point, and a double-click (a second click on the last point) closes', async () => {
  const { outlineClick } = await import('../js/regions.js');
  const first = { x: 100, y: 100 }, last = { x: 300, y: 200 };
  assert.equal(outlineClick(null, null, { x: 5, y: 5 }, 0), 'add');
  assert.equal(outlineClick(first, last, { x: 400, y: 400 }, 3), 'add');
  assert.equal(outlineClick(first, last, { x: 103, y: 99 }, 3), 'close');
  assert.equal(outlineClick(first, last, { x: 301, y: 202 }, 3), 'close', 'double-click on the last point');
  assert.equal(outlineClick(first, last, { x: 301, y: 202 }, 2), 'ignore', 'too few points: no duplicate either');
});

test('a poll tells which era it asked about: an answer for the era before a switch is not taken for this one', async () => {
  const { FakeEl, installFakeDom } = await import('./fakedom.mjs');
  installFakeDom();
  const { RegionsPanel } = await import('../js/regionspanel.js');
  let q = '?era=a', release;
  const seen = [];
  globalThis.fetch = () => new Promise(r => { release = () => r({ ok: true, status: 200,
    json: async () => ({ regions: [], status: { state: 'ready' }, tiles: { url: 'a' } }) }); });
  const p = new RegionsPanel(new FakeEl('section'), { onDraft() {}, onDrawing() {}, onRegions() {},
    onStatus: (s, t, was, asked) => seen.push(asked), eraQuery: () => q });
  const done = p.poll();
  q = '?era=b';
  release();
  await done;
  clearTimeout(p.timer);
  assert.deepEqual(seen, ['?era=a']);
});