pmtiles-swarm 0.63.0 → 0.64.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 +159 -0
- package/NOTICE.md +23 -0
- package/README.md +7 -5
- package/docs/tile-stacks.md +244 -11
- package/package.json +1 -1
- package/src/api.js +119 -323
- package/src/bake-jobs.js +282 -0
- package/src/bake.js +466 -0
- package/src/index.js +9 -0
- package/src/pmtiles-scan.js +254 -0
- package/src/pmtiles-write.js +517 -0
- package/src/stack-tile.js +443 -0
- package/src/web/index.html +125 -6
- package/src/web/public.html +28 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,165 @@
|
|
|
4
4
|
### ✨ Features and improvements
|
|
5
5
|
- _...Add new stuff here..._
|
|
6
6
|
|
|
7
|
+
### 🐞 Bug fixes
|
|
8
|
+
- _...Add new stuff here..._
|
|
9
|
+
|
|
10
|
+
## 0.64.0
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
- **Export a stack to an archive, from the console.** **Export to archive** sits beside Edit and
|
|
13
|
+
Delete on a stack. It runs the recipe over its sources, writes a real `.pmtiles`, and hands the
|
|
14
|
+
file to the library — which hashes it, makes a torrent and puts it in the catalog, so what comes
|
|
15
|
+
out has an infohash and is seeded like anything else here. That is what makes the live endpoint
|
|
16
|
+
the preview of something rather than an endpoint on its own.
|
|
17
|
+
|
|
18
|
+
**Two halves, watched in two places.** Merging is about a stack, so tiles written, tiles skipped
|
|
19
|
+
and the zoom it is working through are reported on the stack. What follows is an archive being
|
|
20
|
+
added, which this node already reports on the archives view through the library's in-progress
|
|
21
|
+
list — so the second half is handed over rather than drawn twice, and the line on the stack says
|
|
22
|
+
where to look.
|
|
23
|
+
|
|
24
|
+
Stopping keeps the work. The checkpoint is the hours already spent, so **Stop export** leaves it
|
|
25
|
+
where the next run picks it up rather than throwing it away. One bake per stack at a time: two
|
|
26
|
+
runs of one recipe write the same checkpoint files over each other, and the second would resume
|
|
27
|
+
the first's work believing it were its own.
|
|
28
|
+
|
|
29
|
+
A bake is identified by the recipe's revision **and what each source resolved to**. The recipe
|
|
30
|
+
alone cannot see the difference: a stack naming a category resolves to whichever build is
|
|
31
|
+
current, so the same recipe over a rebuilt source is a different bake, and a checkpoint that
|
|
32
|
+
could not tell would resume across the change and produce an archive half of one map and half of
|
|
33
|
+
another.
|
|
34
|
+
|
|
35
|
+
The file is dated — `Terrain-20260822.pmtiles` — and the archive's **name** is not. A rebuild
|
|
36
|
+
here keeps its name and mints a new infohash, which is what lets `/latest/<category>/` follow it;
|
|
37
|
+
dating the name would make every build a different map. The date goes in `description`. `name` is
|
|
38
|
+
always written, because these get converted to mbtiles by other tools and a nameless metadata
|
|
39
|
+
block is not valid there.
|
|
40
|
+
|
|
41
|
+
The codec is required for a recipe that asks for pixel work, and refused when the button is
|
|
42
|
+
pressed rather than an hour in. **A cache-mode source is allowed**, which reverses what the
|
|
43
|
+
design first said: reading one pulls pieces through the swarm, and the tile store already holds
|
|
44
|
+
those to a byte budget and drops what it stops using. Slow is the operator's call to make. The
|
|
45
|
+
sources are scanned through the store for the same reason, so a cache-mode archive's directories
|
|
46
|
+
come out of the swarm the way its tiles do.
|
|
47
|
+
|
|
48
|
+
- **The per-tile merge is one function, shared by the route and the bake.** `src/stack-tile.js`.
|
|
49
|
+
The tile route was 380 lines of Express with the whole merge inline — reading each source,
|
|
50
|
+
climbing to a parent, stitching children where tile sizes differ, both pixel spaces, the cache —
|
|
51
|
+
and none of it callable from anywhere else. A bake had to produce exactly what a request would,
|
|
52
|
+
and two implementations of that disagree eventually.
|
|
53
|
+
|
|
54
|
+
What is left in the route is what is genuinely about HTTP: parsing, status codes, the
|
|
55
|
+
abort-on-close wiring, the stats hook. Behaviour is unchanged and the existing stack suites say
|
|
56
|
+
so.
|
|
57
|
+
|
|
58
|
+
- **The bake driver, with checkpointing and cancellation.** `src/bake.js` runs a stack over its
|
|
59
|
+
sources and writes the result as a real archive: union the sources' coverage, merge in tile-id
|
|
60
|
+
order, write, checkpoint, stop when told. `src/pmtiles-scan.js` is the other new piece — it
|
|
61
|
+
reads back *which* tiles an archive holds, which the `pmtiles` package does not offer because
|
|
62
|
+
a tile server never needs to ask.
|
|
63
|
+
|
|
64
|
+
It iterates coverage rather than a zoom range, and that is what decides whether the job is
|
|
65
|
+
possible: a planet at z16 is 5.7 billion coordinates, and asking each one whether a source
|
|
66
|
+
covers it does not finish. The union of what the sources actually hold skips most of the
|
|
67
|
+
pyramid without a single decode.
|
|
68
|
+
|
|
69
|
+
**Sparseness is a consequence rather than a setting.** A tile no source covered is never
|
|
70
|
+
written, so the archive is sparse by construction — and the baked metadata says so, under the
|
|
71
|
+
key tileserver-gl reads and this project's prober reads. `encoding` travels the same way, with
|
|
72
|
+
its four factors when it is `custom`, because a terrain archive that does not say how to read
|
|
73
|
+
its pixels is an image of nothing in particular.
|
|
74
|
+
|
|
75
|
+
**A cancelled bake keeps its work.** Deleting it would make stopping and failing the same
|
|
76
|
+
thing, and this is a job somebody may have been running since yesterday. The checkpoint is the
|
|
77
|
+
buffered tile data, the entries as `serializeDirectory` writes them, and a small JSON state —
|
|
78
|
+
the entries reuse the archive's own serialization rather than inventing a second format for
|
|
79
|
+
the same array. A checkpoint belongs to one revision of one recipe; a checkpoint for anything
|
|
80
|
+
else is discarded rather than continued, because resuming a changed recipe produces an archive
|
|
81
|
+
that is half one map and half another and nothing downstream could tell.
|
|
82
|
+
|
|
83
|
+
The merge is handed in rather than reached for. That keeps the design's own constraint honest
|
|
84
|
+
— one per-tile answer serves both a request and a bake — and it is also what is left to do:
|
|
85
|
+
the orchestration still lives inline in the stack tile route, and extracting it is the next
|
|
86
|
+
step.
|
|
87
|
+
|
|
88
|
+
- **This node can write a PMTiles file.** `src/pmtiles-write.js`: varints, directories, the
|
|
89
|
+
fixed header and a writer that takes tiles one at a time and finalises an archive. The
|
|
90
|
+
`pmtiles` package everything here reads with is read-only, so this is the other half of it,
|
|
91
|
+
ported from the reference implementation in protomaps/PMTiles.
|
|
92
|
+
|
|
93
|
+
It exists for stage 8 of [tile stacks](docs/tile-stacks.md) — baking a stack into a real
|
|
94
|
+
archive with a real infohash — but it is a module on its own and tested as one. Every test
|
|
95
|
+
reads the result back through the same `pmtiles` library that answers served tiles, and one
|
|
96
|
+
reads it through this project's own prober, so an archive written here is one the node can
|
|
97
|
+
take into its catalog.
|
|
98
|
+
|
|
99
|
+
Three deliberate departures from the reference, all of them written down in
|
|
100
|
+
[docs/tile-stacks.md](docs/tile-stacks.md#writing-a-pmtiles-file):
|
|
101
|
+
|
|
102
|
+
Deduplication hashes with a full-length digest rather than the reference's 64 bits. A
|
|
103
|
+
collision does not raise anything — it points one tile at another tile's bytes, in a file
|
|
104
|
+
that is then hashed, torrented and served to other people. At a billion distinct tiles a
|
|
105
|
+
64-bit hash collides about 2.7% of the time, and at five billion about half the time.
|
|
106
|
+
|
|
107
|
+
Varints are built by division rather than by shifting. JavaScript's bitwise operators are
|
|
108
|
+
32-bit, so `value >>= 7` mangles any offset past 4 GiB — which in an archive worth baking
|
|
109
|
+
arrives early. There is a test at 6 GiB.
|
|
110
|
+
|
|
111
|
+
Deduplication can be turned off. It is the one part whose memory grows with the archive, one
|
|
112
|
+
entry per distinct tile, and go-pmtiles makes it a flag for the same reason. Run-length
|
|
113
|
+
encoding still applies without it, and for terrain — long runs of identical ocean and
|
|
114
|
+
identical nodata — that is most of the saving.
|
|
115
|
+
|
|
116
|
+
`clustered` is reported honestly rather than assumed. Tiles written out of tile-id order
|
|
117
|
+
still produce a valid archive, and the header says it is unclustered, because an unclustered
|
|
118
|
+
archive cannot answer a range read in one seek — which is the reason this project serves
|
|
119
|
+
PMTiles rather than MBTiles at all.
|
|
120
|
+
|
|
121
|
+
- **The bake and the sync are designed.** [docs/tile-stacks.md](docs/tile-stacks.md) gained
|
|
122
|
+
three sections: what a bake iterates and why it is the sources' own coverage rather than the
|
|
123
|
+
zoom range (a planet at z16 is 5.7 billion tiles, and asking each one whether a source covers
|
|
124
|
+
it is the difference between a job that finishes and one that does not), what running one
|
|
125
|
+
requires, and what syncing a stack between nodes would have to answer.
|
|
126
|
+
|
|
127
|
+
The sync section is the useful half. A stack is a mutable document whose sources are named by
|
|
128
|
+
category or infohash, so the same recipe means different things on different nodes — sent
|
|
129
|
+
somewhere missing a source it either breaks outright or silently resolves to a different map.
|
|
130
|
+
A feed is the wrong shape for that. Baking sidesteps it: a baked stack is an ordinary archive
|
|
131
|
+
with an infohash, and archives already sync.
|
|
132
|
+
|
|
133
|
+
The stage list also said the console reads stacks but does not edit them, which stopped being
|
|
134
|
+
true when the editor shipped.
|
|
135
|
+
|
|
136
|
+
- **A terrain archive says so before you open it.** The console detail, the stacks list and
|
|
137
|
+
the public catalogue now offer two preview buttons where the encoding is one the preview can
|
|
138
|
+
draw as a DEM: the raster keeps its name, and **Terrain** sits beside it. Before this the
|
|
139
|
+
terrain view existed but nothing pointed at it — you had to open the preview and notice a
|
|
140
|
+
link in the header — and nothing on a listing said which archives were terrain at all.
|
|
141
|
+
|
|
142
|
+
The raster stays first and keeps the plain name deliberately. It is the view that shows a
|
|
143
|
+
hole, because a missing DEM tile hillshades as flat ground rather than as missing.
|
|
144
|
+
|
|
145
|
+
`/api/stacks` and the categories payload now carry `encoding` for the same reason, so a page
|
|
146
|
+
can decide without fetching a TileJSON per row.
|
|
147
|
+
|
|
148
|
+
### 🐞 Bug fixes
|
|
149
|
+
- **A catalogue helper declared in the wrong scope.** The public page renders archives and
|
|
150
|
+
categories from two separate functions, and the terrain check went inside the first — a
|
|
151
|
+
`ReferenceError` the moment a terrain category was drawn. `node --check` accepts it and so
|
|
152
|
+
does a scope check, because the name exists; it is only wrong from the other function.
|
|
153
|
+
|
|
154
|
+
The scope checks that exist for exactly this were only ever run against the console. They now
|
|
155
|
+
run against the catalogue and the preview too, and the helper both renderers share is
|
|
156
|
+
asserted to be declared where both can reach it.
|
|
157
|
+
|
|
158
|
+
- **Three copies of one rule, none of them checked against each other.** The preview decides
|
|
159
|
+
what to draw as terrain, and the console and the catalogue decide whether to offer the
|
|
160
|
+
button. A button offered on something that does not render as terrain is worse than no
|
|
161
|
+
button, so the three are now lifted out of their pages and asserted to agree across eight
|
|
162
|
+
cases — `mlt`, an unknown encoding, and `custom` with none, half and all four of its factors
|
|
163
|
+
among them.
|
|
164
|
+
|
|
165
|
+
|
|
7
166
|
### 🐞 Bug fixes
|
|
8
167
|
- _...Add new stuff here..._
|
|
9
168
|
|
package/NOTICE.md
CHANGED
|
@@ -57,6 +57,29 @@ The prebuilt `@img/sharp-*` packages it resolves carry libvips (LGPL-3.0-or-late
|
|
|
57
57
|
dependencies, each under their own terms; sharp links to libvips dynamically and ships it
|
|
58
58
|
unmodified.
|
|
59
59
|
|
|
60
|
+
## rio-rgbify — MIT
|
|
61
|
+
|
|
62
|
+
> Copyright (c) 2016 Mapbox
|
|
63
|
+
> https://github.com/mapbox/rio-rgbify
|
|
64
|
+
|
|
65
|
+
The original terrain-RGB encoder: it is what established the `base` and `interval` packing that
|
|
66
|
+
`src/elevation.js` decodes and encodes, and the defaults there — base `-10000`, interval `0.1` —
|
|
67
|
+
are Mapbox's Terrain-RGB format rather than a choice made here.
|
|
68
|
+
|
|
69
|
+
[rio-rgbify-merge](https://github.com/TechIdiots-LLC/rio-rgbify-merge) is a fork of it by
|
|
70
|
+
TechIdiots LLC, under the same MIT license and carrying the same copyright line. Tile stacks are
|
|
71
|
+
the on-the-fly counterpart to that fork's offline merge, and the pixel maths is deliberately kept
|
|
72
|
+
the same so a stack previewed live and a stack baked to a file do not disagree — see
|
|
73
|
+
[docs/tile-stacks.md](docs/tile-stacks.md).
|
|
74
|
+
|
|
75
|
+
Which half is whose is worth stating, because the two are credited differently. The encoder traces
|
|
76
|
+
to Mapbox in 2016. The **merge** behaviour this project follows — layer priority, masking by height
|
|
77
|
+
and by colour, sparse tiles — was added in the fork in 2026 and is TechIdiots LLC's own work, not
|
|
78
|
+
Mapbox's.
|
|
79
|
+
|
|
80
|
+
No code is copied either way. `src/elevation.js` and `src/rgba.js` are JavaScript written against
|
|
81
|
+
the same rules, and `docs/tile-stacks.md` records where those rules differ from the fork's and why.
|
|
82
|
+
|
|
60
83
|
## qBittorrent — GPL-2.0-or-later
|
|
61
84
|
|
|
62
85
|
> https://github.com/qbittorrent/qBittorrent
|
package/README.md
CHANGED
|
@@ -804,11 +804,13 @@ which the endpoint answers 501.
|
|
|
804
804
|
| `GET` | `/api/stacks` | Every stack, with what each source resolved to and why one cannot be served yet |
|
|
805
805
|
| `GET` | `/api/torrents/:infoHash/stacks` | Which stacks would break if this archive were removed, and how |
|
|
806
806
|
| `GET`, `PUT`, `DELETE` | `/api/stacks/:id/raw`, `/api/stacks/:id` | Read a recipe as written, save one, or remove it. `/raw` is the recipe; `/api/stacks` is what it resolved to |
|
|
807
|
-
|
|
808
|
-
| `
|
|
809
|
-
| `GET`
|
|
810
|
-
| `GET`
|
|
811
|
-
| `GET`
|
|
807
|
+
|
|
808
|
+
| `POST`, `DELETE` | `/api/stacks/:id/bake` | Run the stack over its sources and write the result as an archive, or stop one that is running. Answers as soon as the job starts; watch it on `/api/stacks` |
|
|
809
|
+
| `GET` | `/stacks/:id/preview` | A map of a stack, for looking at it — **public** |
|
|
810
|
+
| `GET` | `/stacks/:id/tiles.json` | TileJSON for a stack — **public**. `maxzoom` is the maximum over its sources, not the minimum |
|
|
811
|
+
| `GET` | `/stacks/:id/:size/:z/:x/:y.:ext` | The same tile at 256 or 512 px. A tile's coordinates are an extent rather than a pixel count, so this is the same ground on a finer or coarser grid |
|
|
812
|
+
| `GET` | `/stacks/:id/:z/:x/:y.:ext` | One tile of a stack — **public**. Answered by the topmost source holding it; `X-Stack-Sources` says which were asked and what each said |
|
|
813
|
+
| `GET` | `/feed.xml`, `/feed/:category.xml`, `/latest/:category.xml` | RSS — **public** |
|
|
812
814
|
|
|
813
815
|
Everything under `/api/` is guarded once a credential is configured; tiles, TileJSON and the feeds
|
|
814
816
|
never are. A `peer` token may read but not change, and may be narrowed to some categories. See
|
package/docs/tile-stacks.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Tile stacks
|
|
2
2
|
|
|
3
|
-
**Status:
|
|
3
|
+
**Status: all eight stages are implemented.** Elevation stacks work: a recipe is
|
|
4
4
|
defined, resolved and served, and a source is masked, shifted, resampled from a
|
|
5
5
|
parent and painted in order. Merged tiles are cached on disk against a
|
|
6
6
|
byte budget, and image stacks composite with opacity and blend modes. The
|
|
7
|
-
console lists stacks
|
|
8
|
-
still serves the passthrough case and answers 501 for the rest.
|
|
7
|
+
console lists stacks, diagnoses them and edits them. A node with no codec
|
|
8
|
+
installed still serves the passthrough case and answers 501 for the rest.
|
|
9
9
|
|
|
10
10
|
A stack is a recipe for combining several archives into one tile endpoint,
|
|
11
11
|
evaluated per request rather than baked into a file. Where
|
|
@@ -15,7 +15,9 @@ regional lidar patch over a global DEM.
|
|
|
15
15
|
|
|
16
16
|
This is the on-the-fly counterpart to the offline merge in
|
|
17
17
|
[rio-rgbify-merge](https://github.com/TechIdiots-LLC/rio-rgbify-merge)
|
|
18
|
-
(`pip install rio-rgbify-merge`)
|
|
18
|
+
(`pip install rio-rgbify-merge`), a fork of
|
|
19
|
+
[mapbox/rio-rgbify](https://github.com/mapbox/rio-rgbify) which added the merge
|
|
20
|
+
the encoder never had. The pixel maths is the same and is
|
|
19
21
|
deliberately kept the same, so a stack can be previewed live and then baked into
|
|
20
22
|
a real archive without the two disagreeing. What the swarm adds is that a source is named by
|
|
21
23
|
_category_ rather than by path, so a stack keeps working across a rebuild of any
|
|
@@ -39,6 +41,14 @@ of its parts.
|
|
|
39
41
|
- [TileJSON for a stack](#tilejson-for-a-stack)
|
|
40
42
|
- [When a source will not answer](#when-a-source-will-not-answer)
|
|
41
43
|
- [Baking a stack into an archive](#baking-a-stack-into-an-archive)
|
|
44
|
+
- [Writing a PMTiles file](#writing-a-pmtiles-file)
|
|
45
|
+
- [What to iterate](#what-to-iterate)
|
|
46
|
+
- [Running it](#running-it)
|
|
47
|
+
- [What a baked archive says about itself](#what-a-baked-archive-says-about-itself)
|
|
48
|
+
- [What identifies a bake](#what-identifies-a-bake)
|
|
49
|
+
- [Starting one, and watching it](#starting-one-and-watching-it)
|
|
50
|
+
- [What exists now](#what-exists-now)
|
|
51
|
+
- [Syncing a stack to another node](#syncing-a-stack-to-another-node)
|
|
42
52
|
- [What the offline merge got wrong](#what-the-offline-merge-got-wrong)
|
|
43
53
|
- [The stack editor](#the-stack-editor)
|
|
44
54
|
- [Staging](#staging)
|
|
@@ -523,9 +533,227 @@ and the merge code is shared. It also inverts the cost argument completely: the
|
|
|
523
533
|
swarm-read latency that makes on-the-fly stacking painful is irrelevant to a
|
|
524
534
|
batch job that runs overnight.
|
|
525
535
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
536
|
+
That constraint was honoured: `mergeElevation` and the RGBA equivalent are pure
|
|
537
|
+
functions of their contributions, so the bake is a different driver over the
|
|
538
|
+
same core rather than a second implementation of it.
|
|
539
|
+
|
|
540
|
+
Most of what a bake needs is already here. `stackCoverage` says what zooms and
|
|
541
|
+
bounds a stack reaches. The merge path answers a tile. `#running` in the library
|
|
542
|
+
already registers a long job with an `AbortController`, a name, a start time and
|
|
543
|
+
a received/total pair, and the console already draws that list, so a bake is
|
|
544
|
+
another entry in it rather than new machinery. `settleFromStaging` exists for
|
|
545
|
+
precisely this shape: work into a staging directory, hash, then move into place
|
|
546
|
+
once the infohash exists, because an infohash cannot be known before the bytes
|
|
547
|
+
do. The last step is `createTorrentFromFile`, which is what every other archive
|
|
548
|
+
built here goes through.
|
|
549
|
+
|
|
550
|
+
One piece is genuinely missing: nothing in this project writes a PMTiles file.
|
|
551
|
+
|
|
552
|
+
### Writing a PMTiles file
|
|
553
|
+
|
|
554
|
+
The `pmtiles` npm package is read-only. It exports `bytesToHeader`, `findTile`,
|
|
555
|
+
`readVarint` and `zxyToTileId`, and nothing that serialises. So
|
|
556
|
+
`src/pmtiles-write.js` is new, but it is a port of a small, well-understood
|
|
557
|
+
reference rather than a design problem. The Python implementation in
|
|
558
|
+
protomaps/PMTiles is 124 lines for the writer, plus about 70 for
|
|
559
|
+
`serialize_directory` and `serialize_header`; the Hilbert maths does not need
|
|
560
|
+
porting at all, because `zxyToTileId` already ships.
|
|
561
|
+
|
|
562
|
+
The reader shipping in the same package is what makes this cheap to trust: a
|
|
563
|
+
round trip is write it, read it back with `bytesToHeader` and `findTile`, and
|
|
564
|
+
probe it with `pmtiles-probe.js` - the same prober every other archive here goes
|
|
565
|
+
through.
|
|
566
|
+
|
|
567
|
+
Three things the Python reference does that a port must not copy.
|
|
568
|
+
|
|
569
|
+
**It deduplicates on a 64-bit hash.** go-pmtiles uses FNV-128a for the same job,
|
|
570
|
+
and at the scale a bake works at that is not a stylistic difference. A collision
|
|
571
|
+
does not raise anything: it points one tile at another tile's bytes, in a file
|
|
572
|
+
that is then hashed, torrented and served to other people.
|
|
573
|
+
|
|
574
|
+
| distinct tiles | 64-bit | 128-bit |
|
|
575
|
+
| -------------- | -------- | ------- |
|
|
576
|
+
| 10^7 | 3e-06 | ~0 |
|
|
577
|
+
| 10^8 | 3e-04 | ~0 |
|
|
578
|
+
| 10^9 | **2.7%** | ~0 |
|
|
579
|
+
| 5 x 10^9 | **49%** | ~0 |
|
|
580
|
+
|
|
581
|
+
Use a 128-bit digest, and compare the bytes on a hit rather than trusting the
|
|
582
|
+
digest alone. The cost is a comparison against a tile already in hand; the
|
|
583
|
+
failure it prevents is silent and permanent.
|
|
584
|
+
|
|
585
|
+
**It buffers tile data in a temporary file and copies it in at the end**,
|
|
586
|
+
because `tile_data_offset` is not known until the directories have been sized.
|
|
587
|
+
Peak disk is then twice the tile bytes, which for a planet bake is a terabyte of
|
|
588
|
+
transient space. The format does not require it: `tile_data_offset` may be
|
|
589
|
+
anything, so tile data can be written straight into the output at a generous
|
|
590
|
+
fixed offset, leaving a small hole rather than making a second copy.
|
|
591
|
+
|
|
592
|
+
**It detects `clustered` rather than requiring it.** Writing tiles in ascending
|
|
593
|
+
tile-id order is what makes an archive answer a range read in one seek, and
|
|
594
|
+
range reads over HTTP are the reason this project serves PMTiles rather than
|
|
595
|
+
MBTiles at all. An unclustered bake is valid and bad at the only thing it exists
|
|
596
|
+
for, so ordering is a requirement of the bake here, not an outcome to be
|
|
597
|
+
reported.
|
|
598
|
+
|
|
599
|
+
The run-length encoding in `write_tile` is worth keeping exactly as it is:
|
|
600
|
+
consecutive tile ids sharing an offset collapse into one entry, and for terrain,
|
|
601
|
+
with its long runs of identical ocean and identical nodata, that is most of the
|
|
602
|
+
saving.
|
|
603
|
+
|
|
604
|
+
### What to iterate
|
|
605
|
+
|
|
606
|
+
Not the zoom range. A planet at z0-z14 is around 350 million tiles and at z16
|
|
607
|
+
around 5.7 billion, and enumerating that to ask each one whether any source
|
|
608
|
+
covers it is the difference between a job that finishes and one that does not.
|
|
609
|
+
|
|
610
|
+
Iterate the sources' own coverage instead. Every source is a PMTiles archive
|
|
611
|
+
whose directories already say which tiles it has; the union of those, in tile-id
|
|
612
|
+
order, is exactly the set a stack can answer for. For terrain over ocean that
|
|
613
|
+
skips most of the pyramid without a single decode.
|
|
614
|
+
|
|
615
|
+
`sparse` follows from the same fact and needs no separate decision: a tile no
|
|
616
|
+
source covered is not written, and the baked archive is sparse for the same
|
|
617
|
+
reason the live stack answers 404.
|
|
618
|
+
|
|
619
|
+
### Running it
|
|
620
|
+
|
|
621
|
+
- **The codec is required**, and refused up front. A bake that is not pure
|
|
622
|
+
passthrough decodes and re-encodes every tile, so `sharp` stops being optional
|
|
623
|
+
for it — and a node without one should find out when it presses the button,
|
|
624
|
+
not an hour in.
|
|
625
|
+
- **A cache-mode source is allowed.** An earlier draft here said to refuse one.
|
|
626
|
+
That was wrong: reading a cache-mode archive pulls pieces through the swarm,
|
|
627
|
+
and the tile store already holds those to a byte budget and drops what it
|
|
628
|
+
stops using. A long bake against a cached source is therefore slow, not
|
|
629
|
+
unbounded — and slow is the operator's call to make, not this document's. The
|
|
630
|
+
sources are scanned through the store for the same reason, so a cache-mode
|
|
631
|
+
archive's directories come out of the swarm the way its tiles do.
|
|
632
|
+
- **Checkpointing.** Hours to days means the process will be interrupted, and
|
|
633
|
+
resume state is the part of this project that has already gone subtly wrong
|
|
634
|
+
once - see `tools/resume-doctor.py` and what it exists to diagnose. Design it
|
|
635
|
+
rather than discovering it.
|
|
636
|
+
- **Cancellable**, and stopping keeps the work: realising it is the wrong recipe
|
|
637
|
+
should not mean waiting it out, and it should not mean starting over either.
|
|
638
|
+
|
|
639
|
+
### What identifies a bake
|
|
640
|
+
|
|
641
|
+
`bakeRevision` is the recipe's revision and what each source resolved to,
|
|
642
|
+
hashed together. The recipe alone is not enough. A stack naming a category
|
|
643
|
+
resolves to whichever build is current, so the same recipe over a rebuilt source
|
|
644
|
+
is a different bake — and a checkpoint that could not tell would resume across
|
|
645
|
+
the change and produce an archive that is half one map and half another.
|
|
646
|
+
|
|
647
|
+
The file is dated: `Terrain-20260822.pmtiles`. The archive's **name** is not.
|
|
648
|
+
A rebuild here keeps its name and mints a new infohash, which is what lets
|
|
649
|
+
`/latest/<category>/` follow it; dating the name would make every build a
|
|
650
|
+
different map. The date goes in `description` instead, so an archive read out of
|
|
651
|
+
context still says what produced it and when.
|
|
652
|
+
|
|
653
|
+
`name` is always written, because these archives get converted to mbtiles by
|
|
654
|
+
other tools and a nameless metadata block is not valid there.
|
|
655
|
+
|
|
656
|
+
### What a baked archive says about itself
|
|
657
|
+
|
|
658
|
+
Two metadata keys beyond the name, and both are the keys tileserver-gl reads and
|
|
659
|
+
this project's own prober reads, so a baked archive is understood wherever it
|
|
660
|
+
lands.
|
|
661
|
+
|
|
662
|
+
`sparse` is true unless the recipe says otherwise. For a bake this is not a
|
|
663
|
+
claim, it is a description: a tile no source covered is never written, so the
|
|
664
|
+
archive is sparse by construction. The flag is what makes a client overzoom the
|
|
665
|
+
parent rather than draw nothing, and without it a terrain map is full of holes
|
|
666
|
+
that render as sea.
|
|
667
|
+
|
|
668
|
+
`encoding` says how to read the pixels. A terrain-RGB archive without it is an
|
|
669
|
+
image of nothing in particular, and `custom` carries its four factors or is not
|
|
670
|
+
worth writing at all.
|
|
671
|
+
|
|
672
|
+
### Starting one, and watching it
|
|
673
|
+
|
|
674
|
+
**Export to archive**, on the stack, beside Edit and Delete. `POST
|
|
675
|
+
/api/stacks/<id>/bake` starts it and answers as soon as the job is running: a
|
|
676
|
+
planet bake is hours, and a request that waited for the archive is a request
|
|
677
|
+
nothing could hold open. `DELETE` on the same address stops it.
|
|
678
|
+
|
|
679
|
+
A bake has two halves and they are watched in two places, deliberately. Merging
|
|
680
|
+
is about a stack, so it is reported on the stack — tiles written, tiles skipped,
|
|
681
|
+
the zoom it is working through. What happens afterwards is an archive being
|
|
682
|
+
added, and this node already reports that on the archives view through the
|
|
683
|
+
library's own in-progress list, so the second half is handed over rather than
|
|
684
|
+
drawn twice. The line on the stack says where to look.
|
|
685
|
+
|
|
686
|
+
One bake per stack at a time. Two runs of one recipe write the same checkpoint
|
|
687
|
+
files over each other, and the second would resume the first's work believing it
|
|
688
|
+
were its own.
|
|
689
|
+
|
|
690
|
+
### What exists now
|
|
691
|
+
|
|
692
|
+
`src/pmtiles-write.js` writes archives, `src/pmtiles-scan.js` reads back what one
|
|
693
|
+
holds, and `src/bake.js` is the driver: union the sources' coverage, merge in
|
|
694
|
+
tile-id order, write, checkpoint, stop when told.
|
|
695
|
+
|
|
696
|
+
The merge itself is handed in as a function, and `mergeTileFor` is what supplies
|
|
697
|
+
it: the same `answerStackTile` the tile route calls, so a baked tile and a served
|
|
698
|
+
one come out of one implementation and cannot drift. The cache is bypassed there
|
|
699
|
+
— it is sized for tiles people ask for twice, and a whole-pyramid run would evict
|
|
700
|
+
all of those in favour of tiles nobody will ask for again.
|
|
701
|
+
|
|
702
|
+
The checkpoint is three files in a working directory: the buffered tile data,
|
|
703
|
+
the entries as `serializeDirectory` writes them, and a small JSON state. Entries
|
|
704
|
+
go through the same serialization the archive itself uses rather than inventing
|
|
705
|
+
a second format for the same array. A checkpoint belongs to one revision of one
|
|
706
|
+
recipe; anything else is discarded rather than continued, because resuming a
|
|
707
|
+
changed recipe produces an archive that is half one map and half another and
|
|
708
|
+
nothing downstream could tell.
|
|
709
|
+
|
|
710
|
+
The deduplication map is deliberately not part of a checkpoint. Rebuilding it
|
|
711
|
+
means re-hashing everything already buffered, and the cost of starting it empty
|
|
712
|
+
is that a tile identical to one from before the interruption is stored twice.
|
|
713
|
+
The archive is correct either way; it is a little larger.
|
|
714
|
+
|
|
715
|
+
## Syncing a stack to another node
|
|
716
|
+
|
|
717
|
+
The question is whether a stack can travel between nodes the way an archive
|
|
718
|
+
does, and the answer is that it is a different kind of thing.
|
|
719
|
+
|
|
720
|
+
An archive is immutable and content-addressed: an infohash either matches or it
|
|
721
|
+
does not, and two nodes converge because they are fetching the same bytes. A
|
|
722
|
+
stack is a mutable document that gets edited, so syncing it is a question about
|
|
723
|
+
conflicts and clobbering rather than about missing pieces. A feed is the wrong
|
|
724
|
+
shape for it.
|
|
725
|
+
|
|
726
|
+
There is a sharper problem underneath. A recipe names its sources by category or
|
|
727
|
+
by infohash, so **the same recipe means different things on different nodes**.
|
|
728
|
+
Sent to a node missing one source, an infohash-pinned stack is permanently
|
|
729
|
+
broken; a category-named one silently resolves to that node's newest build of
|
|
730
|
+
that category, which may be a different map entirely. Either way the recipe
|
|
731
|
+
travels and the meaning does not.
|
|
732
|
+
|
|
733
|
+
Three shapes, in the order they are worth considering:
|
|
734
|
+
|
|
735
|
+
**Pull into a namespace the receiver owns.** `/api/stacks/<id>/raw` already
|
|
736
|
+
returns the recipe as written. A subscribing node polls a peer's stack list the
|
|
737
|
+
way it already polls for archives, and adopts what it finds under `<peer>:<id>`,
|
|
738
|
+
so an adopted stack can never overwrite a local one and it is obvious on the
|
|
739
|
+
screen which node a recipe came from. This reuses the remote-node machinery
|
|
740
|
+
rather than adding a feed to it. A source that does not resolve locally is
|
|
741
|
+
reported through `problems`, which the console already separates from an invalid
|
|
742
|
+
recipe and from one needing a codec; an adopted stack with a missing source is a
|
|
743
|
+
normal thing to look at, not an error to refuse.
|
|
744
|
+
|
|
745
|
+
**Export a bundle.** The recipe plus the resolved infohashes of its sources, as
|
|
746
|
+
one file. Unambiguous and reproducible, and the right answer when what is wanted
|
|
747
|
+
is _this exact map_. It pins, so it does not follow a rebuild, which is either
|
|
748
|
+
the point or the problem depending on why it was sent.
|
|
749
|
+
|
|
750
|
+
**Nothing automatic.** Copy the recipe and let the operator resolve the sources.
|
|
751
|
+
Honest, and possibly right for as long as stacks are few.
|
|
752
|
+
|
|
753
|
+
Baking sidesteps the question rather than answering it. A baked stack is an
|
|
754
|
+
ordinary archive with an infohash, and archives already sync, so where what is
|
|
755
|
+
wanted is the _output_ on another node rather than the _recipe_, that is the
|
|
756
|
+
mechanism, and it carries no ambiguity about what the sources resolved to.
|
|
529
757
|
|
|
530
758
|
## What the offline merge got wrong
|
|
531
759
|
|
|
@@ -743,10 +971,15 @@ still requested and still composited.
|
|
|
743
971
|
with a byte budget, LRU eviction and single-flight.
|
|
744
972
|
6. ~~**RGBA space.**~~ Done. `src/rgba.js`: opacity, the separable blend
|
|
745
973
|
modes, colour masking and alpha-correct resampling.
|
|
746
|
-
7.
|
|
747
|
-
resolved to and why one cannot be served
|
|
748
|
-
|
|
749
|
-
8.
|
|
974
|
+
7. ~~**Console.**~~ Done. A Stacks view listing every stack with what each
|
|
975
|
+
source resolved to and why one cannot be served, and an editor that adds,
|
|
976
|
+
changes and removes them.
|
|
977
|
+
8. ~~**Bake.**~~ Done. A coverage-driven run to a new `.pmtiles`, hashed and
|
|
978
|
+
registered like any other archive. `src/pmtiles-write.js` writes archives,
|
|
979
|
+
`src/pmtiles-scan.js` says what one holds, `src/stack-tile.js` is the
|
|
980
|
+
per-tile merge shared with the tile route, `src/bake.js` drives the run with
|
|
981
|
+
checkpointing and cancellation, and `src/bake-jobs.js` is the job the console
|
|
982
|
+
starts and watches.
|
|
750
983
|
|
|
751
984
|
Stages 1 and 2 are worth doing on their own even if the rest waits: a
|
|
752
985
|
category-resolved passthrough endpoint that picks whichever source has the tile
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.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",
|