diff options
| author | godosa <godosa@godosa.eu> | 2026-10-07 00:14:38 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-07 00:14:38 +0200 |
| commit | 3443c1c65e9f1753e1e656b35d08416c1fa298f2 (patch) | |
| tree | 4e43236f460145a4d75d1b4616dcb7aa6ef08f51 /viewer/js/exportpanel.js | |
| download | worldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.tar.gz worldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.zip | |
worldmap-viewer: initial public history
Diffstat (limited to 'viewer/js/exportpanel.js')
| -rw-r--r-- | viewer/js/exportpanel.js | 103 |
1 files changed, 103 insertions, 0 deletions
diff --git a/viewer/js/exportpanel.js b/viewer/js/exportpanel.js new file mode 100644 index 0000000..9af9526 --- /dev/null +++ b/viewer/js/exportpanel.js @@ -0,0 +1,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(); + } +} |
