1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
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$/);
});
|