import './_planet.mjs'; import { test } from 'node:test'; import assert from 'node:assert/strict'; const fc = { type: 'FeatureCollection', features: [ { type: 'Feature', properties: { order: 1 }, geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] } }, { type: 'Feature', properties: { order: 4 }, geometry: { type: 'LineString', coordinates: [[5, 5], [6, 6]] } }, ] }; let fetches = 0; globalThis.fetch = async () => { fetches++; return new Response(JSON.stringify(fc), { status: 200 }); }; const { Overlays } = await import('../js/overlays.js'); const flush = () => new Promise(r => setTimeout(r, 0)); const box = { replaceChildren() {} }; const defs = [{ id: 'coast', name: 'Coastline', file: '/data/geo/coast.geojson' }, { id: 'rivers', name: 'Rivers', file: '/data/geo/rivers.geojson' }]; const fakeView = () => ({ lines: new Map(), setLines(id, l) { this.lines.set(id, l); }, removeLines(id) { this.lines.delete(id); }, toScreen: () => null }); test('enabled overlays reach the view; rivers show the orders that stay apart at the scale', async () => { const o = new Overlays(defs, box), v = fakeView(); o.setEnabled(['coast', 'rivers', 'graticule']); o.attach(v); await flush(); assert.deepEqual([...v.lines.keys()].sort(), ['coast', 'graticule', 'rivers']); assert.equal(v.lines.get('rivers').length, 1); // no scale yet: the largest order only o.setScale(0.01); // a first-order segment (≈ 157 km) spans far more than 8 px await flush(); assert.equal(v.lines.get('rivers').length, 2); o.setScale(1000); await flush(); assert.equal(v.lines.get('rivers').length, 1); assert.equal(v.lines.get('graticule').length, 18); o.setEnabled(['coast']); await flush(); assert.deepEqual([...v.lines.keys()], ['coast']); }); test('GeoJSON fetched once; a newly attached view gets everything; extras always shown', async () => { fetches = 0; const o = new Overlays(defs, box), a = fakeView(), b = fakeView(); o.setEnabled(['coast']); o.attach(a); await flush(); o.setExtra('measure', [[{ lat: 0, lon: 0 }, { lat: 1, lon: 1 }]]); o.attach(b); await flush(); assert.equal(fetches, 1); assert.deepEqual([...b.lines.keys()].sort(), ['coast', 'measure']); o.setExtra('measure', []); assert.deepEqual(b.lines.get('measure'), []); }); test('list offers meta overlays plus graticule, refined regions, vents and pins', () => { assert.deepEqual(new Overlays(defs, box).list().map(o => o.id), ['coast', 'rivers', 'graticule', 'regions', 'vents', 'pins']); }); test('vent markers carry a title and class, show only with their overlay, and restyle when re-clustered', async () => { const { FakeEl, installFakeDom } = await import('./fakedom.mjs'); installFakeDom(); const mbox = new FakeEl('div'), o = new Overlays(defs, mbox); o.attach({ ...fakeView(), toScreen: () => ({ x: 1, y: 2 }) }); o.setMarkers('vent', [{ lat: 1, lon: 2, label: '', cls: 't0', title: 'black smoker' }]); const [m] = mbox.kids; assert.match(m.className, /marker vent t0/); assert.equal(m.attrs.title, 'black smoker'); assert.equal(m.style.display, 'none', 'vents overlay off'); o.setEnabled(['vents']); assert.equal(m.style.display, ''); o.setMarkers('vent', [{ lat: 1, lon: 2, label: '', cls: 't2', title: 'diffuse' }]); assert.match(mbox.kids[0].className, /t2/); }); test('refined-region outlines draw while their overlay is on', () => { const o = new Overlays(defs, box), v = fakeView(); o.attach(v); const line = [{ lat: 1, lon: 2 }, { lat: 3, lon: 4 }, { lat: 1, lon: 2 }]; o.setRegions([line]); assert.ok(!v.lines.has('regions'), 'off: nothing drawn'); o.setEnabled(['regions']); assert.deepEqual(v.lines.get('regions'), [line]); o.setEnabled([]); assert.ok(!v.lines.has('regions')); }); test('asks the app to redraw after lines change, including after async loads', async () => { let n = 0; const o = new Overlays(defs, box, () => n++), v = fakeView(); o.setEnabled(['coast']); o.attach(v); const before = n; await flush(); assert.ok(v.lines.has('coast')); assert.ok(n > before, 'redraw requested after the GeoJSON arrived'); const k = n; o.setEnabled([]); assert.ok(n > k, 'redraw requested after removal'); }); test('markers with onClick are clickable; the rest let the pointer through', async () => { const { FakeEl, installFakeDom } = await import('./fakedom.mjs'); installFakeDom(); let hit = null; const mbox = new FakeEl('div'), o = new Overlays(defs, mbox); o.attach(fakeView()); o.setMarkers('pin', [{ lat: 1, lon: 2, label: 'A', onClick: () => { hit = 'A'; } }]); o.setMarkers('measure', [{ lat: 3, lon: 4, label: '1' }]); const [pin, meas] = mbox.kids; assert.match(pin.className, /clickable/); assert.doesNotMatch(meas.className, /clickable/); pin.fire('click'); assert.equal(hit, 'A'); }); test('suppressed overlays leave the view and come back', async () => { const o = new Overlays(defs, box), v = fakeView(); o.setEnabled(['coast']); o.attach(v); await flush(); o.setSuppressed(['coast', 'lakes']); assert.equal(v.lines.has('coast'), false); o.setSuppressed([]); await flush(); assert.equal(v.lines.has('coast'), true); }); test('moving a marker keeps its DOM node (sky play moves the sun 20× a second)', async () => { const { FakeEl, installFakeDom } = await import('./fakedom.mjs'); installFakeDom(); const mbox = new FakeEl('div'), o = new Overlays(defs, mbox); o.attach(fakeView()); o.setMarkers('sun', [{ lat: 1, lon: 2, label: '☉' }]); const node = mbox.kids[0]; o.setMarkers('sun', [{ lat: 3, lon: 4, label: '☉' }]); assert.equal(mbox.kids[0], node); assert.deepEqual([o.markers[0].lat, o.markers[0].lon], [3, 4]); }); test('new definitions drop the loaded overlays (another era)', async () => { const { FakeEl, installFakeDom } = await import('./fakedom.mjs'); installFakeDom(); const o = new Overlays([{ id: 'coast', name: 'Coast', file: '/data/geo/coast.geojson?era=a' }], new FakeEl('div')); o.cache.set('coast', Promise.resolve([])); o.setDefs([{ id: 'coast', name: 'Coast', file: '/data/geo/coast.geojson?era=b' }]); assert.equal(o.cache.size, 0); assert.match(o.defs[0].file, /era=b$/); });