pmtiles-swarm 0.78.0 → 0.79.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 +30 -0
- package/docs/tile-stacks.md +23 -0
- package/package.json +1 -1
- package/src/api.js +13 -0
- package/src/web/index.html +94 -10
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.79.0
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
- **Duplicate a stack.** A button beside Edit on every row, which opens the editor on a copy of that
|
|
13
|
+
recipe: the same sources in the same order, the same masks, the same output, under a name of its
|
|
14
|
+
own and saved only when you save it. Most stacks after the first are a variation on one that
|
|
15
|
+
already works, and rebuilding that by hand is where a source gets left out.
|
|
16
|
+
|
|
17
|
+
Two things are changed for the copy and nothing else is. The name becomes `<name>-copy`, counting
|
|
18
|
+
up until it is one nothing is using, and the title gains `copy` — two stacks under one title are
|
|
19
|
+
two rows nobody can tell apart in the list they both appear in. Both are editable before saving.
|
|
20
|
+
|
|
21
|
+
It copies the **recipe** rather than the row. The list holds what each source resolved to, so a
|
|
22
|
+
copy taken from it would pin the infohashes the original follows by category and stop following
|
|
23
|
+
rebuilds from the moment it was made.
|
|
24
|
+
|
|
25
|
+
### 🐞 Bug fixes
|
|
26
|
+
- **Saving a stack with no name reported that the reply was not JSON.** `Unexpected token '<',
|
|
27
|
+
"<!DOCTYPE "... is not valid JSON`, which says nothing about a missing name. A stack with no name
|
|
28
|
+
is a `PUT /api/stacks/`, and that matches no route at all — `:id` needs a segment to be — so what
|
|
29
|
+
came back was express's own HTML error page, which the console then tried to parse.
|
|
30
|
+
|
|
31
|
+
Both halves are fixed. Every unmatched path under `/api` answers JSON now, so any future typo says
|
|
32
|
+
`no route for PUT /api/stacks/` rather than arriving as a parse error; and the dialog refuses an
|
|
33
|
+
empty or malformed name itself, since it is the one that knows what the box is for.
|
|
34
|
+
|
|
35
|
+
- **A new stack could be saved over an existing one without a word.** `PUT` upserts, which is right
|
|
36
|
+
for editing and wrong for naming: typing a name already in use replaced that stack rather than
|
|
37
|
+
refusing. It is refused now when naming a new stack or a copy — where the stack it would have
|
|
38
|
+
replaced is usually the one being copied. Editing is unaffected; a stack keeps the name it has.
|
|
39
|
+
|
|
10
40
|
## 0.78.0
|
|
11
41
|
### ✨ Features and improvements
|
|
12
42
|
- **Somewhere to clear the caches and the files nothing is waiting for.** A merged tile cache with
|
package/docs/tile-stacks.md
CHANGED
|
@@ -1586,10 +1586,33 @@ editor exists for — an editor whose primary action only works with a mouse is
|
|
|
1586
1586
|
an editor half the people cannot use. The console has no drag-and-drop anywhere
|
|
1587
1587
|
yet, so this is the first, and the buttons are what make it safe to add.
|
|
1588
1588
|
|
|
1589
|
+
### Starting from a stack that already works
|
|
1590
|
+
|
|
1591
|
+
**Duplicate**, beside Edit on every row. It opens the editor on a copy of that
|
|
1592
|
+
recipe: the same sources in the same order, the same masks, the same output —
|
|
1593
|
+
under a name of its own, and saved only when it is saved. Most stacks after the
|
|
1594
|
+
first are a variation on one that already works, and rebuilding that by hand is
|
|
1595
|
+
where a source gets left out.
|
|
1596
|
+
|
|
1597
|
+
Two things are changed for the copy and nothing else is. The name becomes
|
|
1598
|
+
`<name>-copy`, counting up until it is one nothing is using; the title gains
|
|
1599
|
+
`copy`, because two stacks under one title are two rows nobody can tell apart in
|
|
1600
|
+
the list they both appear in. Both are editable before saving — the point is
|
|
1601
|
+
that neither is left matching by accident.
|
|
1602
|
+
|
|
1603
|
+
It copies the **recipe** rather than the row. The list holds what each source
|
|
1604
|
+
resolved to, so a copy taken from it would pin the infohashes the original
|
|
1605
|
+
follows by category, and stop following rebuilds from the moment it was made.
|
|
1606
|
+
The editor reads `/api/stacks/<id>/raw` for the same reason.
|
|
1607
|
+
|
|
1589
1608
|
### What the editor refuses, and what it only warns about
|
|
1590
1609
|
|
|
1591
1610
|
Refuses, because the stack cannot work:
|
|
1592
1611
|
|
|
1612
|
+
- A name that is already a stack's, when naming a new one or a copy. `PUT`
|
|
1613
|
+
upserts, so this is the difference between adding a stack and replacing one —
|
|
1614
|
+
and for a copy, the stack it would replace is usually the one being copied.
|
|
1615
|
+
Editing an existing stack is unaffected: it keeps the name it has.
|
|
1593
1616
|
- A recipe naming an `output.tileSize` that is not 256 or 512.
|
|
1594
1617
|
- A source that resolves to nothing — an empty category, or an archive that
|
|
1595
1618
|
retention has removed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.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
|
@@ -3758,6 +3758,19 @@ export function createApp({
|
|
|
3758
3758
|
}
|
|
3759
3759
|
}
|
|
3760
3760
|
|
|
3761
|
+
// An API path no route claimed. Without this express's own handler answers
|
|
3762
|
+
// with an HTML error page, and a caller that parses every reply as JSON --
|
|
3763
|
+
// the console does -- reports `Unexpected token '<'`, which says nothing at
|
|
3764
|
+
// all about what was wrong with the request.
|
|
3765
|
+
//
|
|
3766
|
+
// The one that produced it was a stack saved with no name: `PUT
|
|
3767
|
+
// /api/stacks/` matches no route, because `:id` needs a segment to be, and
|
|
3768
|
+
// the reply came back as the start of an HTML document.
|
|
3769
|
+
app.use('/api', (req, res) => {
|
|
3770
|
+
const where = req.originalUrl.split('?')[0];
|
|
3771
|
+
res.status(404).json({ error: `no route for ${req.method} ${where}` });
|
|
3772
|
+
});
|
|
3773
|
+
|
|
3761
3774
|
app.use(express.static(path.join(here, 'web')));
|
|
3762
3775
|
|
|
3763
3776
|
// Four parameters, two of them unused: express identifies an error handler
|
package/src/web/index.html
CHANGED
|
@@ -840,7 +840,8 @@
|
|
|
840
840
|
|
|
841
841
|
<div class="field">
|
|
842
842
|
<label for="stack-id">Name</label>
|
|
843
|
-
<input id="stack-id" placeholder="planet-terrain" autocomplete="off"
|
|
843
|
+
<input id="stack-id" placeholder="planet-terrain" autocomplete="off"
|
|
844
|
+
required pattern="[A-Za-z0-9][A-Za-z0-9._-]*" />
|
|
844
845
|
<div class="sub">
|
|
845
846
|
Used in the URL: <code id="stack-url-preview">/stacks/…/tiles.json</code>
|
|
846
847
|
</div>
|
|
@@ -7342,6 +7343,8 @@ Every piece is hashed against the ` +
|
|
|
7342
7343
|
: `<button type="button" data-stack-bake="${escapeHtml(stack.id)}" title="Run this stack over its sources and write the result as a real archive, with its own infohash, seeded like any other.">Export to archive</button>`
|
|
7343
7344
|
}
|
|
7344
7345
|
<button type="button" data-stack-edit="${escapeHtml(stack.id)}">Edit</button>
|
|
7346
|
+
<button type="button" data-stack-copy="${escapeHtml(stack.id)}"
|
|
7347
|
+
title="Opens the editor on a copy of this recipe, under a name of its own. Nothing is saved until you save it.">Duplicate</button>
|
|
7345
7348
|
<button type="button" data-stack-delete="${escapeHtml(stack.id)}">Delete</button>`;
|
|
7346
7349
|
|
|
7347
7350
|
return `
|
|
@@ -7431,6 +7434,26 @@ Every piece is hashed against the ` +
|
|
|
7431
7434
|
// space, so a form that re-derived itself from its own inputs would have
|
|
7432
7435
|
// to know which of them are currently real.
|
|
7433
7436
|
let stackDraft = null;
|
|
7437
|
+
// Whether the dialog is naming a stack rather than editing one that is
|
|
7438
|
+
// already named, and what names are already spoken for. Held here
|
|
7439
|
+
// because the save is the only place it can be checked: `PUT` upserts,
|
|
7440
|
+
// so a name that is taken replaces what is there rather than refusing.
|
|
7441
|
+
let stackNaming = false;
|
|
7442
|
+
let stackNamesTaken = new Set();
|
|
7443
|
+
|
|
7444
|
+
/**
|
|
7445
|
+
* A name like this one that nothing is using.
|
|
7446
|
+
* @param {string} base - The name being copied.
|
|
7447
|
+
* @param {Set<string>} taken - What is spoken for.
|
|
7448
|
+
* @returns {string} - A free name.
|
|
7449
|
+
*/
|
|
7450
|
+
const freeStackId = (base, taken) => {
|
|
7451
|
+
let candidate = `${base}-copy`;
|
|
7452
|
+
for (let n = 2; taken.has(candidate); n += 1) {
|
|
7453
|
+
candidate = `${base}-copy-${n}`;
|
|
7454
|
+
}
|
|
7455
|
+
return candidate;
|
|
7456
|
+
};
|
|
7434
7457
|
|
|
7435
7458
|
/** Forty characters of hex is not a label. */
|
|
7436
7459
|
const shortHash = (hash) => (hash ? `${hash.slice(0, 12)}…` : hash);
|
|
@@ -7439,24 +7462,35 @@ Every piece is hashed against the ` +
|
|
|
7439
7462
|
const draftIsRgba = () => stackDraft?.space === 'rgba';
|
|
7440
7463
|
|
|
7441
7464
|
/**
|
|
7442
|
-
* Opens the dialog on a new stack,
|
|
7443
|
-
*
|
|
7465
|
+
* Opens the dialog on a new stack, on one that already exists, or on a
|
|
7466
|
+
* copy of one.
|
|
7467
|
+
* @param {object} [existing] - The stack to edit, or the copy to name.
|
|
7468
|
+
* @param {object} [options] - `copy` when the draft is a duplicate, and
|
|
7469
|
+
* `taken` the names already in use.
|
|
7444
7470
|
* @returns {Promise<void>} - Resolves once the dialog is open.
|
|
7445
7471
|
*/
|
|
7446
|
-
const openStackDialog = async (existing) => {
|
|
7472
|
+
const openStackDialog = async (existing, options = {}) => {
|
|
7447
7473
|
stackDraft = existing
|
|
7448
7474
|
? JSON.parse(JSON.stringify(existing))
|
|
7449
7475
|
: { id: '', title: '', space: 'elevation', sources: [], output: {} };
|
|
7450
7476
|
stackDraft.sources ??= [];
|
|
7451
7477
|
stackDraft.output ??= {};
|
|
7452
7478
|
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7479
|
+
// A copy arrives filled in but unnamed as far as the node is
|
|
7480
|
+
// concerned, so it is edited like a new stack rather than like the one
|
|
7481
|
+
// it came from.
|
|
7482
|
+
stackNaming = Boolean(options.copy) || !existing;
|
|
7483
|
+
stackNamesTaken = new Set(options.taken ?? []);
|
|
7484
|
+
|
|
7485
|
+
$('stack-dialog-title').textContent = options.copy
|
|
7486
|
+
? 'Duplicate stack'
|
|
7487
|
+
: existing
|
|
7488
|
+
? 'Edit stack'
|
|
7489
|
+
: 'Add stack';
|
|
7456
7490
|
// An id is what the URL is made of, so changing one would silently
|
|
7457
7491
|
// orphan whatever points at the old address.
|
|
7458
7492
|
$('stack-id').value = stackDraft.id ?? '';
|
|
7459
|
-
$('stack-id').disabled =
|
|
7493
|
+
$('stack-id').disabled = !stackNaming;
|
|
7460
7494
|
$('stack-title').value = stackDraft.title ?? '';
|
|
7461
7495
|
for (const radio of document.querySelectorAll(
|
|
7462
7496
|
'input[name="stack-space"]',
|
|
@@ -8021,6 +8055,30 @@ Every piece is hashed against the ` +
|
|
|
8021
8055
|
$('stack-form').addEventListener('submit', async (event) => {
|
|
8022
8056
|
event.preventDefault();
|
|
8023
8057
|
const id = $('stack-id').value.trim();
|
|
8058
|
+
// Caught here because the node cannot catch it: a stack with no name
|
|
8059
|
+
// is a `PUT /api/stacks/`, which matches no route at all, and what
|
|
8060
|
+
// comes back says only that the reply was not JSON.
|
|
8061
|
+
if (!id) {
|
|
8062
|
+
$('stack-error').textContent =
|
|
8063
|
+
'A name is needed. It is what the stack is served under, so ' +
|
|
8064
|
+
'there is nowhere to save one without it.';
|
|
8065
|
+
return;
|
|
8066
|
+
}
|
|
8067
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(id)) {
|
|
8068
|
+
$('stack-error').textContent =
|
|
8069
|
+
'A name may hold letters, digits, dots, dashes and underscores, ' +
|
|
8070
|
+
'and has to start with a letter or a digit — it goes in a URL.';
|
|
8071
|
+
return;
|
|
8072
|
+
}
|
|
8073
|
+
// `PUT` upserts, so a name already in use is replaced rather than
|
|
8074
|
+
// refused -- which for a duplicate means quietly overwriting the very
|
|
8075
|
+
// stack it was copied from.
|
|
8076
|
+
if (stackNaming && stackNamesTaken.has(id)) {
|
|
8077
|
+
$('stack-error').textContent =
|
|
8078
|
+
`There is already a stack called "${id}". A name is what its URL ` +
|
|
8079
|
+
'is made of, so saving would replace it rather than add this one.';
|
|
8080
|
+
return;
|
|
8081
|
+
}
|
|
8024
8082
|
const body = {
|
|
8025
8083
|
title: $('stack-title').value.trim() || undefined,
|
|
8026
8084
|
space: stackDraft.space,
|
|
@@ -8086,8 +8144,16 @@ Every piece is hashed against the ` +
|
|
|
8086
8144
|
loadStacks().catch((e) => toast(e.message));
|
|
8087
8145
|
});
|
|
8088
8146
|
|
|
8089
|
-
$('stacks-add').onclick = () =>
|
|
8090
|
-
|
|
8147
|
+
$('stacks-add').onclick = async () => {
|
|
8148
|
+
try {
|
|
8149
|
+
const { stacks } = await api('/api/stacks');
|
|
8150
|
+
await openStackDialog(undefined, {
|
|
8151
|
+
taken: stacks.map((one) => one.id),
|
|
8152
|
+
});
|
|
8153
|
+
} catch (error) {
|
|
8154
|
+
toast(error.message);
|
|
8155
|
+
}
|
|
8156
|
+
};
|
|
8091
8157
|
|
|
8092
8158
|
/**
|
|
8093
8159
|
* Asks where an export should land before starting it.
|
|
@@ -8231,6 +8297,7 @@ Every piece is hashed against the ` +
|
|
|
8231
8297
|
// change, so they are delegated the same way the source rows are.
|
|
8232
8298
|
$('stacks-list').addEventListener('click', async (event) => {
|
|
8233
8299
|
const edit = event.target.dataset.stackEdit;
|
|
8300
|
+
const copy = event.target.dataset.stackCopy;
|
|
8234
8301
|
const drop = event.target.dataset.stackDelete;
|
|
8235
8302
|
const bake = event.target.dataset.stackBake;
|
|
8236
8303
|
const stopBake = event.target.dataset.stackBakeStop;
|
|
@@ -8260,6 +8327,23 @@ Every piece is hashed against the ` +
|
|
|
8260
8327
|
const raw = await api(`/api/stacks/${encodeURIComponent(edit)}/raw`);
|
|
8261
8328
|
await openStackDialog(raw.stack);
|
|
8262
8329
|
}
|
|
8330
|
+
} else if (copy !== undefined) {
|
|
8331
|
+
const { stacks } = await api('/api/stacks');
|
|
8332
|
+
// The recipe, not the report -- the same reason editing reads it.
|
|
8333
|
+
// A report holds what each source resolved to, so a copy made from
|
|
8334
|
+
// one would pin infohashes the original follows by category.
|
|
8335
|
+
const raw = await api(`/api/stacks/${encodeURIComponent(copy)}/raw`);
|
|
8336
|
+
const taken = stacks.map((one) => one.id);
|
|
8337
|
+
await openStackDialog(
|
|
8338
|
+
{
|
|
8339
|
+
...raw.stack,
|
|
8340
|
+
id: freeStackId(copy, new Set(taken)),
|
|
8341
|
+
// Named apart on purpose: two stacks under one title are two
|
|
8342
|
+
// rows nobody can tell apart in the list they both appear in.
|
|
8343
|
+
title: `${raw.stack.title ?? copy} copy`,
|
|
8344
|
+
},
|
|
8345
|
+
{ copy: true, taken },
|
|
8346
|
+
);
|
|
8263
8347
|
} else if (drop !== undefined) {
|
|
8264
8348
|
if (!confirm(`Delete the stack "${drop}"?`)) return;
|
|
8265
8349
|
try {
|