pmtiles-swarm 0.65.0 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/docs/tile-stacks.md +33 -6
- package/package.json +1 -1
- package/src/api.js +2 -0
- package/src/bake-jobs.js +44 -8
- package/src/bake.js +34 -8
- package/src/web/index.html +175 -21
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,62 @@
|
|
|
4
4
|
### ✨ Features and improvements
|
|
5
5
|
- _...Add new stuff here..._
|
|
6
6
|
|
|
7
|
+
### 🐞 Bug fixes
|
|
8
|
+
- _...Add new stuff here..._
|
|
9
|
+
|
|
10
|
+
## 0.67.0
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
- **An exported archive is dated, and the filename is its own field.** Both the archive's name and
|
|
13
|
+
the file it lands in now carry the date by default, and both can be changed — separately. They
|
|
14
|
+
answer different questions: `Terrain-20260822.pmtiles` is what somebody finds on disk,
|
|
15
|
+
`Terrain 20260822` is what a map client shows, and tying one to the other only guarantees that
|
|
16
|
+
one of them is wrong whenever they should differ.
|
|
17
|
+
|
|
18
|
+
A filename given by hand is reduced to a single path segment before it is used, because it is
|
|
19
|
+
joined to a save path and a filename is exactly the kind of field somebody puts a slash in.
|
|
20
|
+
`../../etc/passwd` is tested.
|
|
21
|
+
|
|
22
|
+
The description starts empty and stays empty unless something is typed. It used to be
|
|
23
|
+
prefilled from the recipe, which is a different thing — a recipe describes how tiles are
|
|
24
|
+
combined, an archive describes what it is, and only the person exporting it knows that. The
|
|
25
|
+
server no longer falls back to the recipe either: filling in a field the dialog showed as
|
|
26
|
+
blank is a worse surprise than having no description. The date is recorded there regardless,
|
|
27
|
+
because a name can be changed to anything and then nothing else says when the archive was
|
|
28
|
+
made.
|
|
29
|
+
|
|
30
|
+
This corrects something 0.64.0 asserted and this project does not do. The name was left undated
|
|
31
|
+
on the reasoning that `/latest/<category>/` follows a rebuild by name. It does not: it resolves a
|
|
32
|
+
category and takes the newest by date, and nothing here looks an archive up by name at all — the
|
|
33
|
+
only name comparison in the codebase refuses two archives the same *file* path. The
|
|
34
|
+
documentation said so as well, and now says what is true.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### 🐞 Bug fixes
|
|
39
|
+
- _...Add new stuff here..._
|
|
40
|
+
|
|
41
|
+
## 0.66.0
|
|
42
|
+
### ✨ Features and improvements
|
|
43
|
+
- **An export asks where it should go, and what to call it.** **Export to archive** now opens a
|
|
44
|
+
dialog instead of a confirmation, because there is something to decide. The save location uses
|
|
45
|
+
the same picker every other destination in this console uses, so a location named once under
|
|
46
|
+
Settings is offered here too — a baked planet is hundreds of gigabytes and which disk it lands on
|
|
47
|
+
is not a detail. The name and description default to the stack's and can be changed.
|
|
48
|
+
|
|
49
|
+
The filename is shown as the name is typed. A name is not a filename — separators and the
|
|
50
|
+
characters Windows refuses become dashes, and the date is appended so successive builds do not
|
|
51
|
+
collide — and a preview promising one thing while the server writes another would be worse than
|
|
52
|
+
no preview at all. A test asserts the browser's rule and `safeSegment` agree.
|
|
53
|
+
|
|
54
|
+
### 🐞 Bug fixes
|
|
55
|
+
- **A save location the node did not know was silently ignored.** The export resolved it after the
|
|
56
|
+
merge and swallowed the error, so naming a location that did not exist, or one that could not be
|
|
57
|
+
written, meant an hour of merging answered with a shrug and the archive on the default disk. It
|
|
58
|
+
is resolved before anything starts and the error is allowed through, which is what lets the
|
|
59
|
+
dialog put it beside the field that caused it.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
7
63
|
### 🐞 Bug fixes
|
|
8
64
|
- _...Add new stuff here..._
|
|
9
65
|
|
package/docs/tile-stacks.md
CHANGED
|
@@ -698,14 +698,26 @@ resolves to whichever build is current, so the same recipe over a rebuilt source
|
|
|
698
698
|
is a different bake — and a checkpoint that could not tell would resume across
|
|
699
699
|
the change and produce an archive that is half one map and half another.
|
|
700
700
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
701
|
+
Both the file and the archive's name are dated, and both can be changed —
|
|
702
|
+
separately, because they answer different questions. `Terrain-20260822.pmtiles`
|
|
703
|
+
is what somebody finds on disk; `Terrain 20260822` is what a map client shows.
|
|
704
|
+
Tying one to the other only guarantees that one of them is wrong whenever they
|
|
705
|
+
should differ.
|
|
706
|
+
|
|
707
|
+
An earlier version of this document said the name had to stay undated so
|
|
708
|
+
`/latest/<category>/` could follow a rebuild. That was wrong. `/latest/`
|
|
709
|
+
resolves a category and takes the newest by date; nothing in this project looks
|
|
710
|
+
an archive up by name at all, and the only name comparison there is refuses two
|
|
711
|
+
archives the same _file_ path. A dated name is free, and it answers the question
|
|
712
|
+
somebody holding two builds actually has.
|
|
706
713
|
|
|
707
714
|
`name` is always written, because these archives get converted to mbtiles by
|
|
708
|
-
other tools and a nameless metadata block is not valid there.
|
|
715
|
+
other tools and a nameless metadata block is not valid there. The date also goes
|
|
716
|
+
in `description`, so an archive read out of context says what produced it.
|
|
717
|
+
|
|
718
|
+
A chosen filename is reduced to one path segment before it is used. That is not
|
|
719
|
+
politeness — the name is joined to a save path, and a filename is exactly the
|
|
720
|
+
kind of field somebody puts a slash in.
|
|
709
721
|
|
|
710
722
|
### What a baked archive says about itself
|
|
711
723
|
|
|
@@ -730,6 +742,21 @@ worth writing at all.
|
|
|
730
742
|
planet bake is hours, and a request that waited for the archive is a request
|
|
731
743
|
nothing could hold open. `DELETE` on the same address stops it.
|
|
732
744
|
|
|
745
|
+
The button opens a dialog rather than a confirmation, because there is
|
|
746
|
+
something to decide. **Where it lands** uses the same picker every other
|
|
747
|
+
destination in this console uses, so a location named once under Settings is
|
|
748
|
+
offered here too — and a baked planet is hundreds of gigabytes, so which disk it
|
|
749
|
+
goes on is not a detail. The location is resolved _before_ anything is merged:
|
|
750
|
+
a name this node does not know, or a path it cannot write, is the caller's
|
|
751
|
+
mistake and they can fix it, but only if they are told now rather than an hour
|
|
752
|
+
later.
|
|
753
|
+
|
|
754
|
+
**What it is called** is two fields, both dated by default and both editable:
|
|
755
|
+
the archive's name, and the filename. The dialog says what a filename will
|
|
756
|
+
actually become where sanitising would change it, using the same rule the server
|
|
757
|
+
applies — a field that shows one filename while the server writes another is
|
|
758
|
+
worse than a field that shows nothing.
|
|
759
|
+
|
|
733
760
|
A bake has two halves and they are watched in two places, deliberately. Merging
|
|
734
761
|
is about a stack, so it is reported on the stack — tiles written, tiles skipped,
|
|
735
762
|
the zoom it is working through. What happens afterwards is an archive being
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.0",
|
|
4
4
|
"description": "BitTorrent distribution for PMTiles map archives: create torrents, watch folders, publish and subscribe to RSS feeds, and seed through qBittorrent or an embedded client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/api.js
CHANGED
|
@@ -3181,6 +3181,8 @@ export function createApp({
|
|
|
3181
3181
|
categories: req.body?.categories,
|
|
3182
3182
|
location: req.body?.location,
|
|
3183
3183
|
savePath: req.body?.savePath,
|
|
3184
|
+
name: req.body?.name,
|
|
3185
|
+
description: req.body?.description,
|
|
3184
3186
|
});
|
|
3185
3187
|
return res.status(202).json({ bake: job });
|
|
3186
3188
|
} catch (error) {
|
package/src/bake-jobs.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
assertBakeable,
|
|
4
4
|
bakeRevision,
|
|
5
5
|
bakeStack,
|
|
6
|
+
bakedArchiveName,
|
|
6
7
|
bakedName,
|
|
7
8
|
mergeTileFor,
|
|
8
9
|
} from './bake.js';
|
|
@@ -113,9 +114,27 @@ export class BakeManager {
|
|
|
113
114
|
throw error;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
// Resolved before anything starts, and allowed to throw. A location that
|
|
118
|
+
// does not exist or cannot be written is the caller's mistake and they can
|
|
119
|
+
// fix it -- but only if they are told now. Checked at the end instead, it
|
|
120
|
+
// is an hour of merging answered with a shrug and the default disk.
|
|
121
|
+
const publishDir =
|
|
122
|
+
options.publishDir ?? (await this.#savePath(options)) ?? undefined;
|
|
123
|
+
|
|
124
|
+
// Both dated by default and both overridable, separately: the archive's
|
|
125
|
+
// name is what a map client shows, and the filename is what somebody finds
|
|
126
|
+
// on disk. Tying them together only means one of the two is wrong whenever
|
|
127
|
+
// they should differ.
|
|
128
|
+
const when = new Date();
|
|
129
|
+
const archiveName = bakedArchiveName(resolved, {
|
|
130
|
+
name: options.name,
|
|
131
|
+
when,
|
|
132
|
+
});
|
|
133
|
+
|
|
116
134
|
const job = {
|
|
117
135
|
stackId,
|
|
118
136
|
title: resolved.stack.title ?? stackId,
|
|
137
|
+
archiveName,
|
|
119
138
|
phase: 'merging',
|
|
120
139
|
startedAt: new Date().toISOString(),
|
|
121
140
|
written: 0,
|
|
@@ -126,7 +145,8 @@ export class BakeManager {
|
|
|
126
145
|
finishedAt: null,
|
|
127
146
|
error: null,
|
|
128
147
|
infoHash: null,
|
|
129
|
-
name: bakedName(resolved, {
|
|
148
|
+
name: bakedName(resolved, { filename: options.filename, when }),
|
|
149
|
+
publishDir,
|
|
130
150
|
};
|
|
131
151
|
this.#jobs.set(stackId, job);
|
|
132
152
|
|
|
@@ -171,6 +191,7 @@ export class BakeManager {
|
|
|
171
191
|
workDir,
|
|
172
192
|
destination,
|
|
173
193
|
format,
|
|
194
|
+
options,
|
|
174
195
|
);
|
|
175
196
|
} finally {
|
|
176
197
|
await pixels?.close().catch(() => {});
|
|
@@ -185,7 +206,7 @@ export class BakeManager {
|
|
|
185
206
|
// Moved out of the working directory as it is taken on, so a finished
|
|
186
207
|
// archive does not live among the checkpoint files of the job that made
|
|
187
208
|
// it.
|
|
188
|
-
publishDir:
|
|
209
|
+
publishDir: job.publishDir,
|
|
189
210
|
mode: 'mirror',
|
|
190
211
|
});
|
|
191
212
|
|
|
@@ -205,7 +226,16 @@ export class BakeManager {
|
|
|
205
226
|
* @param {string} format - The output format.
|
|
206
227
|
* @returns {Promise<void>} - Resolves when the file is written.
|
|
207
228
|
*/
|
|
208
|
-
async #merge(
|
|
229
|
+
async #merge(
|
|
230
|
+
job,
|
|
231
|
+
resolved,
|
|
232
|
+
codec,
|
|
233
|
+
pixels,
|
|
234
|
+
workDir,
|
|
235
|
+
destination,
|
|
236
|
+
format,
|
|
237
|
+
options,
|
|
238
|
+
) {
|
|
209
239
|
// Read through the tile store rather than off the disk, so a cache-mode
|
|
210
240
|
// source is scanned the same way it is served: its directories come out of
|
|
211
241
|
// the swarm, and the store holds them to its own byte budget.
|
|
@@ -249,8 +279,11 @@ export class BakeManager {
|
|
|
249
279
|
header: { format },
|
|
250
280
|
pauseMs: this.#config.stacks?.bakePauseMs ?? 0,
|
|
251
281
|
metadata: {
|
|
252
|
-
name:
|
|
253
|
-
|
|
282
|
+
name: job.archiveName,
|
|
283
|
+
// Only what was asked for. Falling back to the recipe's own
|
|
284
|
+
// description would fill in a field the dialog showed as empty, which
|
|
285
|
+
// is a worse surprise than having no description at all.
|
|
286
|
+
description: options.description,
|
|
254
287
|
attribution: resolved.stack.attribution,
|
|
255
288
|
encoding: resolved.stack.output?.encoding,
|
|
256
289
|
encodingFactors: resolved.stack.output,
|
|
@@ -273,9 +306,10 @@ export class BakeManager {
|
|
|
273
306
|
* @returns {Promise<string|undefined>} - A path, or undefined for the default.
|
|
274
307
|
*/
|
|
275
308
|
async #savePath(options) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
309
|
+
// Not caught. `resolveSavePath` refuses a name this node does not know and
|
|
310
|
+
// a path it cannot write, and both are worth refusing rather than quietly
|
|
311
|
+
// putting hundreds of gigabytes somewhere else.
|
|
312
|
+
const explicit = await this.#library.resolveSavePath(options);
|
|
279
313
|
return explicit ?? this.#config.savePath ?? undefined;
|
|
280
314
|
}
|
|
281
315
|
|
|
@@ -295,6 +329,8 @@ export class BakeManager {
|
|
|
295
329
|
skipped: job.skipped,
|
|
296
330
|
zoom: job.zoom,
|
|
297
331
|
name: job.name,
|
|
332
|
+
archiveName: job.archiveName,
|
|
333
|
+
publishDir: job.publishDir ?? null,
|
|
298
334
|
infoHash: job.infoHash,
|
|
299
335
|
error: job.error,
|
|
300
336
|
};
|
package/src/bake.js
CHANGED
|
@@ -228,19 +228,45 @@ function stamp(when = new Date()) {
|
|
|
228
228
|
/**
|
|
229
229
|
* What to call the file a bake writes.
|
|
230
230
|
*
|
|
231
|
-
* Dated, because successive bakes of one stack are successive builds
|
|
232
|
-
* map and two files cannot share a path.
|
|
233
|
-
*
|
|
234
|
-
*
|
|
231
|
+
* Dated by default, because successive bakes of one stack are successive builds
|
|
232
|
+
* of one map and two files cannot share a path.
|
|
233
|
+
*
|
|
234
|
+
* A caller may name it instead, and what they ask for is reduced to a single
|
|
235
|
+
* path segment before it is used. That is not politeness: this name is joined
|
|
236
|
+
* to a directory, and a filename is exactly the kind of field somebody puts a
|
|
237
|
+
* slash in.
|
|
235
238
|
* @param {object} resolved - The resolved stack.
|
|
236
|
-
* @param {object} [options] - `
|
|
237
|
-
* @returns {string} - A filename
|
|
239
|
+
* @param {object} [options] - `filename` to choose one outright, and `when`.
|
|
240
|
+
* @returns {string} - A filename, always ending `.pmtiles`.
|
|
238
241
|
*/
|
|
239
242
|
export function bakedName(resolved, options = {}) {
|
|
243
|
+
const requested = String(options.filename ?? '').trim();
|
|
244
|
+
if (requested) {
|
|
245
|
+
const stem = safeSegment(requested.replace(/\.pmtiles$/i, ''));
|
|
246
|
+
if (stem) return `${stem}.pmtiles`;
|
|
247
|
+
}
|
|
248
|
+
|
|
240
249
|
const title = resolved.stack.title ?? resolved.stack.id;
|
|
241
250
|
const slug = safeSegment(title) || 'stack';
|
|
242
|
-
|
|
243
|
-
|
|
251
|
+
return `${slug}-${stamp(options.when)}.pmtiles`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* What to call the archive itself.
|
|
256
|
+
*
|
|
257
|
+
* Dated too, and separately from the file. Nothing in this project looks an
|
|
258
|
+
* archive up by name -- `/latest/<category>/` follows a category and takes the
|
|
259
|
+
* newest by date -- so a dated name costs nothing and says which build you are
|
|
260
|
+
* looking at, which is the question somebody holding two of them has.
|
|
261
|
+
* @param {object} resolved - The resolved stack.
|
|
262
|
+
* @param {object} [options] - `name` to choose one outright, and `when`.
|
|
263
|
+
* @returns {string} - The name.
|
|
264
|
+
*/
|
|
265
|
+
export function bakedArchiveName(resolved, options = {}) {
|
|
266
|
+
const explicit = String(options.name ?? '').trim();
|
|
267
|
+
if (explicit) return explicit;
|
|
268
|
+
const title = resolved.stack.title ?? resolved.stack.id;
|
|
269
|
+
return `${title} ${stamp(options.when)}`;
|
|
244
270
|
}
|
|
245
271
|
|
|
246
272
|
/**
|
package/src/web/index.html
CHANGED
|
@@ -644,6 +644,68 @@
|
|
|
644
644
|
</form>
|
|
645
645
|
</dialog>
|
|
646
646
|
|
|
647
|
+
<dialog id="bake-dialog">
|
|
648
|
+
<form method="dialog" id="bake-form">
|
|
649
|
+
<h2>Export to archive</h2>
|
|
650
|
+
<div class="sub" style="margin-bottom:0.8rem">
|
|
651
|
+
Runs the recipe over its sources and writes the result as a real
|
|
652
|
+
archive, with its own infohash, seeded like any other. It reads every
|
|
653
|
+
tile the sources hold, so it can take hours — and it can be stopped,
|
|
654
|
+
which keeps the work for the next run to carry on from.
|
|
655
|
+
</div>
|
|
656
|
+
|
|
657
|
+
<div class="field">
|
|
658
|
+
<label>Stack</label>
|
|
659
|
+
<div class="sub"><code id="bake-stack"></code></div>
|
|
660
|
+
</div>
|
|
661
|
+
|
|
662
|
+
<div class="field">
|
|
663
|
+
<label for="bake-name">Name</label>
|
|
664
|
+
<input id="bake-name" placeholder="Terrain 20260101" />
|
|
665
|
+
<div class="sub">
|
|
666
|
+
What the archive calls itself, and what a map client shows. Dated by
|
|
667
|
+
default, because two builds of one map are easier to tell apart that
|
|
668
|
+
way — nothing here looks an archive up by name, so it is free to say
|
|
669
|
+
whatever is useful.
|
|
670
|
+
</div>
|
|
671
|
+
</div>
|
|
672
|
+
|
|
673
|
+
<div class="field">
|
|
674
|
+
<label for="bake-file">Filename</label>
|
|
675
|
+
<input id="bake-file" placeholder="Terrain-20260101.pmtiles" />
|
|
676
|
+
<div class="sub" id="bake-filename"></div>
|
|
677
|
+
</div>
|
|
678
|
+
|
|
679
|
+
<div class="field">
|
|
680
|
+
<label for="bake-description">Description</label>
|
|
681
|
+
<input id="bake-description" placeholder="" />
|
|
682
|
+
<div class="sub">
|
|
683
|
+
Optional, and empty by default — what a stack merges is the
|
|
684
|
+
operator's business and not something to guess at. The date this was
|
|
685
|
+
baked is recorded here whether or not anything else is.
|
|
686
|
+
</div>
|
|
687
|
+
</div>
|
|
688
|
+
|
|
689
|
+
<div id="bake-location"></div>
|
|
690
|
+
|
|
691
|
+
<div class="field">
|
|
692
|
+
<label for="bake-categories">Categories</label>
|
|
693
|
+
<input id="bake-categories" placeholder="terrain, weekly" />
|
|
694
|
+
<div class="sub">
|
|
695
|
+
Comma separated. What the archive is filed under once it is added,
|
|
696
|
+
which is what <code>/latest/<category>/</code> follows. Left
|
|
697
|
+
empty, it takes the stack's own.
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
|
|
701
|
+
<div class="sub" id="bake-error" style="color: var(--danger)"></div>
|
|
702
|
+
<menu>
|
|
703
|
+
<button value="cancel">Cancel</button>
|
|
704
|
+
<button id="bake-go" value="default">Export</button>
|
|
705
|
+
</menu>
|
|
706
|
+
</form>
|
|
707
|
+
</dialog>
|
|
708
|
+
|
|
647
709
|
<dialog id="adopt-dialog">
|
|
648
710
|
<form method="dialog" id="adopt-form">
|
|
649
711
|
<h2>Adopt existing torrents</h2>
|
|
@@ -7678,6 +7740,114 @@ Every piece is hashed against the ` +
|
|
|
7678
7740
|
$('stacks-add').onclick = () =>
|
|
7679
7741
|
openStackDialog().catch((e) => toast(e.message));
|
|
7680
7742
|
|
|
7743
|
+
/**
|
|
7744
|
+
* Asks where an export should land before starting it.
|
|
7745
|
+
*
|
|
7746
|
+
* A dialog rather than a confirm, because there is something to decide:
|
|
7747
|
+
* a baked planet is hundreds of gigabytes and the disk it goes on is not
|
|
7748
|
+
* a detail. The same picker every other destination in this console
|
|
7749
|
+
* uses, so a location named once under Settings is offered here too.
|
|
7750
|
+
* @param {string} id - The stack to export.
|
|
7751
|
+
* @returns {Promise<void>} - Resolves once it is asked for or dismissed.
|
|
7752
|
+
*/
|
|
7753
|
+
async function openBakeDialog(id, stack) {
|
|
7754
|
+
const title = stack?.title ?? id;
|
|
7755
|
+
const day = new Date().toISOString().slice(0, 10).replaceAll('-', '');
|
|
7756
|
+
|
|
7757
|
+
$('bake-stack').textContent = id;
|
|
7758
|
+
$('bake-error').textContent = '';
|
|
7759
|
+
$('bake-name').value = `${title} ${day}`;
|
|
7760
|
+
$('bake-file').value = `${bakeSlug(title)}-${day}.pmtiles`;
|
|
7761
|
+
// Blank rather than the stack's own description. A recipe describes
|
|
7762
|
+
// how tiles are combined; an archive describes what it is, and only
|
|
7763
|
+
// the person exporting it knows that.
|
|
7764
|
+
$('bake-description').value = '';
|
|
7765
|
+
$('bake-categories').value = (stack?.categories ?? []).join(', ');
|
|
7766
|
+
showBakeFilename();
|
|
7767
|
+
$('bake-location').innerHTML = locationPicker('bake-loc');
|
|
7768
|
+
await fillLocations('bake-loc');
|
|
7769
|
+
$('bake-dialog').showModal();
|
|
7770
|
+
}
|
|
7771
|
+
|
|
7772
|
+
/**
|
|
7773
|
+
* A name reduced to something usable as one path segment.
|
|
7774
|
+
*
|
|
7775
|
+
* The same rules `safeSegment` applies on the server, and a test holds
|
|
7776
|
+
* the two together — a field that shows one filename while the server
|
|
7777
|
+
* writes another is worse than a field that shows nothing.
|
|
7778
|
+
* @param {string} typed - What somebody entered.
|
|
7779
|
+
* @returns {string} - A safe segment, never empty.
|
|
7780
|
+
*/
|
|
7781
|
+
function bakeSlug(typed) {
|
|
7782
|
+
const slug = typed
|
|
7783
|
+
.replace(/[/\\<>:"|?*\u0000-\u001f\u007f -]/g, '-')
|
|
7784
|
+
.replace(/-{2,}/g, '-')
|
|
7785
|
+
.replace(/^[.\s-]+/, '')
|
|
7786
|
+
.replace(/[.\s-]+$/, '')
|
|
7787
|
+
.slice(0, 120)
|
|
7788
|
+
.replace(/[.\s-]+$/, '');
|
|
7789
|
+
return slug || 'stack';
|
|
7790
|
+
}
|
|
7791
|
+
|
|
7792
|
+
/**
|
|
7793
|
+
* Says what the file will actually be called, where that differs.
|
|
7794
|
+
*
|
|
7795
|
+
* A filename is exactly the kind of field somebody puts a slash in, and
|
|
7796
|
+
* the server reduces it to one path segment before joining it to a
|
|
7797
|
+
* directory. Saying so here means it is not a surprise.
|
|
7798
|
+
* @returns {void}
|
|
7799
|
+
*/
|
|
7800
|
+
function showBakeFilename() {
|
|
7801
|
+
const typed = $('bake-file').value.trim();
|
|
7802
|
+
const note = $('bake-filename');
|
|
7803
|
+
if (!typed) {
|
|
7804
|
+
note.textContent = 'Left empty, it is named after the stack and dated.';
|
|
7805
|
+
return;
|
|
7806
|
+
}
|
|
7807
|
+
const settled = `${bakeSlug(typed.replace(/\.pmtiles$/i, ''))}.pmtiles`;
|
|
7808
|
+
note.textContent =
|
|
7809
|
+
settled === typed ? '' : `Saved as ${settled}`;
|
|
7810
|
+
}
|
|
7811
|
+
|
|
7812
|
+
$('bake-file').addEventListener('input', showBakeFilename);
|
|
7813
|
+
|
|
7814
|
+
$('bake-form').addEventListener('submit', async (event) => {
|
|
7815
|
+
if (event.submitter?.value === 'cancel') return;
|
|
7816
|
+
event.preventDefault();
|
|
7817
|
+
|
|
7818
|
+
const id = $('bake-stack').textContent;
|
|
7819
|
+
const categories = $('bake-categories')
|
|
7820
|
+
.value.split(',')
|
|
7821
|
+
.map((one) => one.trim())
|
|
7822
|
+
.filter(Boolean);
|
|
7823
|
+
const name = $('bake-name').value.trim();
|
|
7824
|
+
const filename = $('bake-file').value.trim();
|
|
7825
|
+
const description = $('bake-description').value.trim();
|
|
7826
|
+
|
|
7827
|
+
try {
|
|
7828
|
+
await api(`/api/stacks/${encodeURIComponent(id)}/bake`, {
|
|
7829
|
+
method: 'POST',
|
|
7830
|
+
body: {
|
|
7831
|
+
...chosenLocation('bake-loc'),
|
|
7832
|
+
...(categories.length > 0 ? { categories } : {}),
|
|
7833
|
+
...(name ? { name } : {}),
|
|
7834
|
+
...(filename ? { filename } : {}),
|
|
7835
|
+
...(description ? { description } : {}),
|
|
7836
|
+
},
|
|
7837
|
+
});
|
|
7838
|
+
} catch (error) {
|
|
7839
|
+
// Kept open, because the answer is usually something to change here
|
|
7840
|
+
// -- a location that does not exist, or a codec that is not
|
|
7841
|
+
// installed -- rather than something to go away and think about.
|
|
7842
|
+
$('bake-error').textContent = error.message;
|
|
7843
|
+
return;
|
|
7844
|
+
}
|
|
7845
|
+
|
|
7846
|
+
$('bake-dialog').close();
|
|
7847
|
+
toast(`Exporting ${id}`);
|
|
7848
|
+
loadStacks().catch((e) => toast(e.message));
|
|
7849
|
+
});
|
|
7850
|
+
|
|
7681
7851
|
// Editing and deleting hang off the list, which is redrawn on every
|
|
7682
7852
|
// change, so they are delegated the same way the source rows are.
|
|
7683
7853
|
$('stacks-list').addEventListener('click', async (event) => {
|
|
@@ -7686,27 +7856,11 @@ Every piece is hashed against the ` +
|
|
|
7686
7856
|
const bake = event.target.dataset.stackBake;
|
|
7687
7857
|
const stopBake = event.target.dataset.stackBakeStop;
|
|
7688
7858
|
if (bake !== undefined) {
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
'archive?\n\nThis reads every tile the sources hold and can ' +
|
|
7695
|
-
'take hours. It can be stopped, and stopping keeps the work.',
|
|
7696
|
-
)
|
|
7697
|
-
) {
|
|
7698
|
-
return;
|
|
7699
|
-
}
|
|
7700
|
-
try {
|
|
7701
|
-
await api(`/api/stacks/${encodeURIComponent(bake)}/bake`, {
|
|
7702
|
-
method: 'POST',
|
|
7703
|
-
body: {},
|
|
7704
|
-
});
|
|
7705
|
-
toast(`Exporting ${bake}`);
|
|
7706
|
-
} catch (error) {
|
|
7707
|
-
toast(error.message);
|
|
7708
|
-
}
|
|
7709
|
-
loadStacks().catch((e) => toast(e.message));
|
|
7859
|
+
const { stacks } = await api('/api/stacks');
|
|
7860
|
+
await openBakeDialog(
|
|
7861
|
+
bake,
|
|
7862
|
+
stacks.find((one) => one.id === bake),
|
|
7863
|
+
).catch((e) => toast(e.message));
|
|
7710
7864
|
} else if (stopBake !== undefined) {
|
|
7711
7865
|
try {
|
|
7712
7866
|
await api(`/api/stacks/${encodeURIComponent(stopBake)}/bake`, {
|