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
|
// regionspanel.js — the Regions tab: list refined regions, draw a new outline by clicking the map, save, delete,
// and show the background refinement's progress
import { api, el, toast } from './ui.js';
import { WARN_KM2, crossesItself, outlineAreaKm2, regionPayload, statusText } from './regions.js';
export class RegionsPanel {
constructor(box, { onDraft, onDrawing, onRegions, onStatus, eraQuery = () => '' }) {
Object.assign(this, { box, onDraft, onDrawing, onRegions, onStatus, eraQuery });
this.regions = [];
this.status = null;
this.points = null; // the outline being drawn
this.closed = false;
this.timer = null;
}
async load() {
try {
const q = this.eraQuery(), r = await api(`api/regions${q}`);
this.regions = r.regions;
this.#status(r.status, r.tiles, q);
} catch (e) {
toast(`Regions: ${e.message}`);
}
this.onRegions(this.regions);
this.render();
}
#status(s, tiles, q = this.eraQuery()) {
const was = this.status?.state;
this.status = s;
this.onStatus(s, tiles, was, q);
clearTimeout(this.timer);
const saving = s?.tiles && s.tiles.done < s.tiles.total; // tiles being saved ahead: keep the percentage fresh
const busy = s && ['queued', 'building'].includes(s.state);
this.timer = setTimeout(() => this.poll(), busy ? 700 : saving ? 3000 : 30000); // idle: another tab may build
}
async poll() {
try {
const q = this.eraQuery(), r = await api(`api/regions${q}`);
this.regions = r.regions;
this.#status(r.status, r.tiles, q);
this.onRegions(this.regions);
if (!this.points) this.render(); // never wipe a name being typed
} catch {
this.timer = setTimeout(() => this.poll(), 2000);
}
}
startDraw() {
this.points = [];
this.closed = false;
this.onDrawing(true);
this.render();
}
cancel() {
this.points = null;
this.closed = false;
this.onDraft(null);
this.onDrawing(false);
this.render();
}
addPoint(p, action) { // action: 'add' | 'close' | 'ignore' (regions.outlineClick)
if (!this.points || this.closed || action === 'ignore') return;
if (action === 'close' && this.points.length >= 3) {
this.closed = true;
this.onDrawing(false);
} else this.points.push(p);
this.onDraft({ points: this.points, closed: this.closed });
this.render();
}
async save(name) {
if (crossesItself(this.points)) return toast('The outline crosses itself: redraw it');
const km2 = outlineAreaKm2(this.points);
if (km2 > WARN_KM2 && !confirm(`This region is ≈ ${Math.round(km2).toLocaleString()} km². Regions over `
+ `${WARN_KM2.toLocaleString()} km² take minutes to refine and a lot of memory. Refine it anyway?`)) return;
try {
await api('api/regions', { method: 'POST', body: regionPayload(name, this.points) });
this.points = null;
this.closed = false;
this.onDraft(null);
await this.load();
} catch (e) {
toast(`Could not save the region: ${e.message}`);
}
}
async remove(r) {
if (!confirm(`Delete region “${r.name}”? Its refined terrain goes too.`)) return;
try {
await api(`api/regions/${r.id}`, { method: 'DELETE' });
await this.load();
} catch (e) {
toast(`Could not delete the region: ${e.message}`);
}
}
render() {
const st = el('p', { class: this.status?.state === 'failed' ? 'warn' : 'muted' }, statusText(this.status));
if (this.points) {
const name = el('input', { required: true, maxlength: 80, placeholder: 'Region name' });
const form = el('form', { class: 'region' },
el('p', { class: 'muted' }, this.closed ? `${this.points.length} points — name it and save`
: `Click the map to outline the region (${this.points.length} points); click the first point to close. Esc cancels.`),
this.closed ? el('label', {}, 'Name', name) : null,
el('div', {}, this.closed ? el('button', { type: 'submit' }, 'Save and refine') : null, ' ',
el('button', { type: 'button', onclick: () => this.cancel() }, 'Cancel')));
form.addEventListener('submit', e => { e.preventDefault(); this.save(name.value); });
if (this.closed) queueMicrotask(() => name.focus());
return this.box.replaceChildren(form, st);
}
const item = r => el('div', { class: 'pin-item' }, el('b', {}, r.name), el('br'),
el('span', { class: 'muted' }, `${Math.round(r.area_km2
?? outlineAreaKm2(r.outline.map(([lat, lon]) => ({ lat, lon })))).toLocaleString()} km² · `
+ (r.refined ? 'refined' : 'not refined yet')), ' ',
el('button', { onclick: () => this.remove(r) }, 'Delete'));
this.box.replaceChildren(
el('div', {}, el('button', { onclick: () => this.startDraw() }, '+ Draw region')),
el('p', { class: 'muted' }, 'Refined regions re-run erosion, rivers, lakes and life zones at ≈ 5 km (generated terrain: idea).'),
st, ...(this.regions.length ? this.regions.map(item) : [el('p', { class: 'muted' }, 'No regions yet.')]));
}
}
|