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
|
// exportpanel.js — the Export tab: freeze the square around the map centre into game-engine files (map/exports/)
import { api, el, toast } from './ui.js';
import { exportSizeMB, exportStatusText, samplesFor, squareOutline } from './exports.js';
import { fmtLatLon } from './units.js';
const SIZES = [5, 10, 20, 40, 80, 160]; // km
const RESOLUTIONS = [1, 2, 5, 10, 20, 50, 100]; // m per sample
const MAX_SAMPLES = 8193; // as the server (serve.EXPORT_MAX_SAMPLES)
export class ExportPanel {
constructor(box, { getCenter, onSquare, getEra = () => undefined }) {
Object.assign(this, { box, getCenter, onSquare, getEra });
this.size = 20;
this.res = 10;
this.list = [];
this.status = null;
this.visible = false;
this.timer = null;
}
async load() {
try {
const r = await api('api/exports');
this.list = r.exports;
this.status = r.status;
} catch (e) {
toast(`Exports: ${e.message}`);
}
clearTimeout(this.timer);
if (this.status?.state === 'running') this.timer = setTimeout(() => this.load(), 700);
if (this.statusEl) this.refresh(); // a poll: never rebuild what is being typed or picked
else this.render();
}
refresh() { // the parts that change while an export runs: status line, button, list
const busy = this.status?.state === 'running', tooMany = samplesFor(this.size, this.res) > MAX_SAMPLES;
this.statusEl.textContent = exportStatusText(this.status);
this.statusEl.setAttribute('class', this.status?.state === 'failed' ? 'warn' : 'muted');
if (busy || tooMany) this.button.setAttribute('disabled', 'true');
else this.button.removeAttribute?.('disabled');
this.listEl.replaceChildren(...(this.list.length ? this.list.map(e => this.#item(e)) : [el('p', { class: 'muted' }, 'No exports yet.')]));
}
#item(e) {
return el('div', { class: 'pin-item' }, el('b', {}, e.name), el('br'),
el('span', { class: 'muted' }, `${(e.size_m / 1000).toLocaleString()} km · ${e.res_m} m · ${e.samples} × ${e.samples}`), el('br'),
el('span', { class: 'muted small' }, e.path));
}
show() {
this.visible = true;
this.render();
this.update();
this.load();
}
hide() {
this.visible = false;
this.onSquare(null);
}
update() { // the view moved or the size changed: the square and the centre line follow
if (!this.visible) return;
const c = this.getCenter();
this.onSquare(squareOutline(c.lat, c.lon, this.size));
if (this.centre) this.centre.textContent = `Centre (map centre): ${fmtLatLon(c.lat, c.lon)}`;
}
async start(name) {
const c = this.getCenter();
try {
await api('api/exports', { method: 'POST', body: { lat: c.lat, lon: c.lon, size_km: this.size, res_m: this.res,
era: this.getEra() || undefined, ...(name.trim() ? { name: name.trim() } : {}) } });
} catch (e) {
return toast(`Could not export: ${e.message}`);
}
await this.load();
}
render() {
const n = samplesFor(this.size, this.res), tooMany = n > MAX_SAMPLES;
const select = (key, values, unit) => {
const s = el('select', { name: key }, ...values.map(v => el('option', { value: String(v), selected: v === this[key] ? true : null }, `${v} ${unit}`)));
s.addEventListener('change', () => { this[key] = Number(s.value); this.render(); this.update(); });
return s;
};
const name = el('input', { maxlength: 64, placeholder: 'name (optional: a–z 0–9 . _ -)' });
name.addEventListener('input', () => { const v = name.value.toLowerCase().replace(/[^a-z0-9._-]/g, '-'); if (v !== name.value) name.value = v; });
this.centre = el('p', { class: 'muted' }, '');
this.button = el('button', { onclick: () => this.start(name.value) }, 'Export here (map centre)');
this.statusEl = el('p', { class: 'muted' }, '');
this.listEl = el('div', {});
this.box.replaceChildren(
el('p', { class: 'muted' }, 'Freeze the square around the map centre into files for a game engine: heights, water, '
+ 'biomes, rivers, pins (map/exports/, see its README.txt). Generated terrain: idea.'),
el('label', { class: 'row' }, 'Size ', select('size', SIZES, 'km'), ' Resolution ', select('res', RESOLUTIONS, 'm')),
el('p', { class: tooMany ? 'warn' : 'muted' }, `${n} × ${n} samples · ≈ ${Math.round(exportSizeMB(n))} MB`
+ (tooMany ? ' — too many: choose a coarser resolution or a smaller square' : '')),
this.centre, el('label', { class: 'row' }, name), el('div', {}, this.button), this.statusEl, this.listEl);
this.refresh();
this.update();
}
}
|