pmtiles-swarm 0.89.0 β 0.91.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 +132 -0
- package/README.md +1 -0
- package/docs/configuration.md +91 -0
- package/docs/tile-stacks.md +237 -14
- package/package.json +1 -1
- package/src/api.js +220 -103
- package/src/bake-jobs.js +27 -12
- package/src/config.js +23 -0
- package/src/index.js +4 -0
- package/src/pmtiles-write.js +8 -3
- package/src/s3-source.js +302 -0
- package/src/stack-feed.js +68 -1
- package/src/stack-tile.js +83 -22
- package/src/stacks.js +178 -7
- package/src/tiles.js +163 -1
- package/src/url-sources.js +249 -0
- package/src/web/index.html +521 -42
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,138 @@
|
|
|
7
7
|
### π Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.91.0
|
|
11
|
+
### β¨ Features and improvements
|
|
12
|
+
- **A source at a URL can be added by hand, not only imported.** **Add source β an address you
|
|
13
|
+
typeβ¦** in the stack editor, writing the same source the importer does. The card asks for the
|
|
14
|
+
address and for the zoom range the archive holds β a catalog archive states its range in its own
|
|
15
|
+
header and this has no header anybody has read, so leaving it empty means every tile asks it.
|
|
16
|
+
Anything published as a plain HTTPS download works, an S3 object or presigned URL included, and so
|
|
17
|
+
does `s3://bucket/key.pmtiles` for a private bucket β see below.
|
|
18
|
+
|
|
19
|
+
- **A stack's source list folds up past five sources.** An imported list is several hundred rows,
|
|
20
|
+
which buried every other stack on the page under one of them. The fold says how many there are and
|
|
21
|
+
names the base; a stack of a base and a layer or two stays open, since folding that hides nothing
|
|
22
|
+
worth a click.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
- **A stack source may be an object in a private S3 bucket.**
|
|
26
|
+
`s3://bucket/terrain.pmtiles`, read with a signed request per byte range. Only for a bucket that is
|
|
27
|
+
not public β a public object or a presigned URL is an ordinary HTTPS address and always worked.
|
|
28
|
+
`endpoint` is what makes it S3-compatible rather than S3: MinIO, Ceph, Garage, R2, Wasabi and B2
|
|
29
|
+
all answer the same protocol at their own address, and path-style addressing is the default because
|
|
30
|
+
it is what they speak. Credentials go under **Settings β Feeds β S3 buckets**, per bucket or once
|
|
31
|
+
for an account; with none configured the standard `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`,
|
|
32
|
+
`AWS_REGION` and `AWS_S3_ENDPOINT` variables are read, so a machine already set up to reach a
|
|
33
|
+
bucket needs nothing typed in.
|
|
34
|
+
|
|
35
|
+
Signed here rather than by an SDK: SigV4 for a GET is a hash, four HMACs and a string, all of which
|
|
36
|
+
`node:crypto` has. An AWS client would be a large dependency for one signature, and an optional one
|
|
37
|
+
would make reading a bucket work on some installs and not on others for no reason the operator
|
|
38
|
+
could see β so it is neither optional nor a dependency. The signer is checked against AWS's own
|
|
39
|
+
published test vectors rather than against itself.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### π Bug fixes
|
|
43
|
+
- **A stack of remote sources could be served but not exported.** The bake refused any stack whose
|
|
44
|
+
sources resolved to no archive on this node, which is every stack read from URLs β refusing exactly
|
|
45
|
+
the export worth having, since baking is how terrain that lives somewhere else becomes something
|
|
46
|
+
this node holds and can seed. It also had no way to walk a remote archive's directories, which is
|
|
47
|
+
how a bake finds which tiles exist at all.
|
|
48
|
+
|
|
49
|
+
- **An archive whose deepest tiles repeat said it stopped a zoom short.** Identical tiles collapse
|
|
50
|
+
into one directory entry covering a range of ids, so the last entry's own id can sit at a shallower
|
|
51
|
+
zoom than the tiles it addresses β and the header was read off that id. A client asks for nothing
|
|
52
|
+
past a header's `maxZoom`, so the deepest zoom of such an archive was unreachable. Found baking a
|
|
53
|
+
stack over flat terrain, where every tile of the last zoom was the same tile.
|
|
54
|
+
|
|
55
|
+
- **A stack used as a source of another stack was reported as unresolved.** It resolves to a recipe
|
|
56
|
+
rather than to bytes, and the console's reader looked only for a catalog entry or an address β so a
|
|
57
|
+
working nested stack showed "does not resolve", with no zoom range and no box, and put a "sources
|
|
58
|
+
missing" badge on the stack above it. It now says what it stands for and how deep it reaches,
|
|
59
|
+
worked out from the sources that stack would serve.
|
|
60
|
+
|
|
61
|
+
- **A stack's advertised box ignored any source that stated none.** The union was taken over
|
|
62
|
+
whichever sources had a box, so a stack of a global base plus regional patches β an imported list,
|
|
63
|
+
exactly β advertised the patches as its extent and left the base out of it. A source with no box
|
|
64
|
+
is not one covering nothing, so it is now every box or none, and none means the world.
|
|
65
|
+
|
|
66
|
+
## 0.90.0
|
|
67
|
+
### β¨ Features and improvements
|
|
68
|
+
- **A stack source may name a URL instead of a category, archive or stack.** `{ "url": "https://β¦" }`,
|
|
69
|
+
read straight over HTTP with no torrent involved β for an archive published as a plain download,
|
|
70
|
+
like Mapterhorn's terrain: a global base plus hundreds of regional patches, 11.8 TiB in total and
|
|
71
|
+
never meant to be downloaded whole. `FetchSource`, from the `pmtiles` package this project already
|
|
72
|
+
depends on, asks for byte ranges the way a swarm-backed source does; a tile costs the header once,
|
|
73
|
+
the directory once, and the tile itself.
|
|
74
|
+
|
|
75
|
+
Parent climbing works the same way it does for a category source β a shallow archive upscales for a
|
|
76
|
+
deeper request through the same code, since reading one is now only a question of which store method
|
|
77
|
+
answers. New per-source `minzoom` and `maxzoom` fields, checked before anything is opened, are what
|
|
78
|
+
make hundreds of these practical: a tile request outside a source's stated box or zoom range skips it
|
|
79
|
+
without a request leaving this node, which for a stack built from a provider's whole file list is
|
|
80
|
+
most of them, on every tile. A URL source always decodes rather than passing through raw, having no
|
|
81
|
+
infohash to answer that question with, and is not seeded, retired or rebuilt β none of the mechanisms
|
|
82
|
+
built for an archive this node actually holds apply to one it does not.
|
|
83
|
+
|
|
84
|
+
- **A provider's list of PMTiles URLs can be imported as sources.** **Stacks β Import URL listβ¦**,
|
|
85
|
+
or `POST /api/stacks/<id>/import`. Mapterhorn's `download_urls.json` names 458 files with the box
|
|
86
|
+
and zoom range of each; naming those by hand is not work anybody should do once, let alone again
|
|
87
|
+
when the provider adds one. An index with an `items` list is read, and so is a plain list of
|
|
88
|
+
addresses for a provider that publishes no index β which shape it is is worked out from the
|
|
89
|
+
document, since somebody pasting an address has no reason to know.
|
|
90
|
+
|
|
91
|
+
The global file becomes `sources[0]` and `required`: a stack is painted bottom-first, so the thing
|
|
92
|
+
covering everywhere has to sit under everything patching it, and the index does not list it first.
|
|
93
|
+
Every other entry keeps its box and zoom range, which is what lets a tile outside one skip it
|
|
94
|
+
without a request leaving the node. The encoding is asked for rather than read β an index rarely
|
|
95
|
+
states it, Mapterhorn's files are all terrarium and its JSON never says so, and a terrain source
|
|
96
|
+
read with the wrong one is a cliff face.
|
|
97
|
+
|
|
98
|
+
The list is fetched **by the node**, not the browser: the console is often on a different network,
|
|
99
|
+
and what matters is that the machine which will read the archives can reach them. **Check** shows
|
|
100
|
+
what an import would write before it writes anything.
|
|
101
|
+
|
|
102
|
+
In the editor an imported batch is one row rather than several hundred cards, with **Re-import**
|
|
103
|
+
and **Remove all**. A re-import replaces only what came from that same address β anything typed by
|
|
104
|
+
hand is left alone β and puts the batch back **where it already was** rather than on the end:
|
|
105
|
+
painting order is the whole meaning of a stack, and a batch that moved each time would quietly bury
|
|
106
|
+
a local override, a day later, on a schedule, with nothing to say why the map had changed.
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
- **A stack can follow a provider's file list on a timer.** A stack feed row with **Into stack** set
|
|
110
|
+
reads a URL list rather than another node's recipes, and keeps the named stack level with it.
|
|
111
|
+
Mapterhorn's index has grown through several versions, and a node that imported it once is a node
|
|
112
|
+
serving whenever that was. The row says which of the two it is by whether **Into stack** is filled
|
|
113
|
+
in β a feed of recipes names its own stacks and an index cannot, being a list of files with no
|
|
114
|
+
opinion about what they are for, so the row supplies the stack and the encoding as well.
|
|
115
|
+
|
|
116
|
+
It reconciles exactly as **Re-import** does: hand-written sources untouched, the batch back where it
|
|
117
|
+
was, a withdrawn file dropped rather than left to 404 per tile. A poll where nothing changed writes
|
|
118
|
+
nothing at all β not the same recipe again, which would move its revision and with it every tile
|
|
119
|
+
cached against it. A stack that does not exist yet is created; one that does keeps everything it had.
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
### π Bug fixes
|
|
123
|
+
- **A stack reported no encoding unless its recipe stated one outright.** Reading `output.encoding`
|
|
124
|
+
alone reports null for the ordinary recipe β the one that re-encodes to nothing and writes whatever
|
|
125
|
+
its base source is written in, which is every imported stack, since an import sets the encoding per
|
|
126
|
+
source. Three places read it that way and all three were wrong about terrain: the console offered no
|
|
127
|
+
terrain preview, the TileJSON told clients nothing so terrarium heights rendered through the mapbox
|
|
128
|
+
formula, and a baked archive was written with no encoding metadata at all β an export that had to be
|
|
129
|
+
corrected by hand afterwards to be readable.
|
|
130
|
+
|
|
131
|
+
- **A stack's TileJSON listed every source.** For a stack imported from a provider's index that is 458
|
|
132
|
+
addresses and 55 KB, in a document every map load fetches, describing files a client cannot use and
|
|
133
|
+
will never ask for β it reads tiles from the XYZ endpoint. It now names 25 with a count of the rest.
|
|
134
|
+
The extension it advertises is the one the endpoint actually serves, too: a stack whose sources state
|
|
135
|
+
no format has no coverage format to read, and the document said `.bin` for tiles answered as webp.
|
|
136
|
+
|
|
137
|
+
- **Cancel asked you to fill the form in first.** A `<button>` in a form submits, and a submit is
|
|
138
|
+
checked against the form's required fields before it goes anywhere β so cancelling the import or
|
|
139
|
+
warm dialog answered "Please fill out this field" about a field that was about to be discarded.
|
|
140
|
+
|
|
141
|
+
|
|
10
142
|
## 0.89.0
|
|
11
143
|
### β¨ Features and improvements
|
|
12
144
|
- **Both feeds are linked at the foot of the public page.** `RSS feed` is `archive RSS feed` now and
|
package/README.md
CHANGED
|
@@ -807,6 +807,7 @@ which the endpoint answers 501.
|
|
|
807
807
|
| `GET` | `/api/torrents/:infoHash/stacks` | Which stacks would break if this archive were removed, and how |
|
|
808
808
|
| `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 |
|
|
809
809
|
|
|
810
|
+
| `POST` | `/api/stacks/:id/import` | Import a provider's list of PMTiles URLs as sources on this stack. An index with an `items` list, or a plain list of addresses; `dryRun` says what it would write without writing it |
|
|
810
811
|
| `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` |
|
|
811
812
|
| `GET` | `/stacks/:id/preview` | A map of a stack, for looking at it β **public** |
|
|
812
813
|
| `GET` | `/stacks/:id/tiles.json` | TileJSON for a stack β **public**. `maxzoom` is the maximum over its sources, not the minimum |
|
package/docs/configuration.md
CHANGED
|
@@ -20,6 +20,7 @@ Some settings only take effect on restart. Those are marked **restart**.
|
|
|
20
20
|
- [Scheduled sources](#scheduled-sources)
|
|
21
21
|
- [Subscriptions](#subscriptions)
|
|
22
22
|
- [Serving tiles](#serving-tiles)
|
|
23
|
+
- [S3 buckets](#s3-buckets)
|
|
23
24
|
- [Mutable publishing](#mutable-publishing)
|
|
24
25
|
- [Authentication](#authentication)
|
|
25
26
|
- [Statistics](#statistics)
|
|
@@ -776,6 +777,96 @@ connection, a piece already in flight. Ten attempts later it is one piece at the
|
|
|
776
777
|
far end of an archive nobody has finished, and asking every fifteen seconds
|
|
777
778
|
achieves nothing but log lines.
|
|
778
779
|
|
|
780
|
+
## S3 buckets
|
|
781
|
+
|
|
782
|
+
A stack source may be an address rather than an archive this node holds, and
|
|
783
|
+
that address may name an object in a bucket:
|
|
784
|
+
|
|
785
|
+
```json
|
|
786
|
+
{
|
|
787
|
+
"sources": [{ "url": "s3://terrain/planet.pmtiles", "encoding": "terrarium" }]
|
|
788
|
+
}
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
Only for a bucket that is **not public**. A public object, or a presigned URL,
|
|
792
|
+
is an ordinary HTTPS address: write it as one and none of this applies.
|
|
793
|
+
|
|
794
|
+
```json
|
|
795
|
+
{
|
|
796
|
+
"s3": [
|
|
797
|
+
{
|
|
798
|
+
"bucket": "terrain",
|
|
799
|
+
"endpoint": "https://minio.lan:9000",
|
|
800
|
+
"region": "us-east-1",
|
|
801
|
+
"accessKeyId": "β¦",
|
|
802
|
+
"secretAccessKey": "β¦"
|
|
803
|
+
}
|
|
804
|
+
]
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
| field | default | |
|
|
809
|
+
| ----------------- | ------------------ | ---------------------------------------------------- |
|
|
810
|
+
| `bucket` | any | which bucket this row is for; unset matches all |
|
|
811
|
+
| `endpoint` | AWS for the region | what makes it S3-compatible rather than S3 |
|
|
812
|
+
| `region` | `us-east-1` | signed into every request, whether or not it is used |
|
|
813
|
+
| `accessKeyId` | β | |
|
|
814
|
+
| `secretAccessKey` | β | |
|
|
815
|
+
| `sessionToken` | unset | for temporary credentials |
|
|
816
|
+
| `pathStyle` | path, unless AWS | `false` puts the bucket in the hostname |
|
|
817
|
+
|
|
818
|
+
A source is read through the row naming its bucket, or through whichever row
|
|
819
|
+
names none: one set of credentials for a whole account is the ordinary case,
|
|
820
|
+
and writing the same keys once per bucket is busywork with a copy-paste
|
|
821
|
+
mistake in it.
|
|
822
|
+
|
|
823
|
+
### The endpoint is the whole point
|
|
824
|
+
|
|
825
|
+
MinIO, Ceph, Garage, Cloudflare R2, Wasabi and Backblaze B2 all answer the
|
|
826
|
+
same protocol at their own address, so naming that address is the difference
|
|
827
|
+
between "S3" and "S3-compatible". Path-style addressing is what they speak and
|
|
828
|
+
is the default here; AWS withdrew it for buckets made after September 2020, so
|
|
829
|
+
an `amazonaws.com` endpoint puts the bucket in the hostname instead. Set
|
|
830
|
+
`pathStyle` only for a server that insists on the other one.
|
|
831
|
+
|
|
832
|
+
### Credentials from the environment
|
|
833
|
+
|
|
834
|
+
With no rows configured at all, the standard variables are read β the same
|
|
835
|
+
ones the AWS CLI, rclone, go-pmtiles and tileserver-gl use, so a machine
|
|
836
|
+
already set up to reach a bucket needs nothing written here:
|
|
837
|
+
|
|
838
|
+
| variable | |
|
|
839
|
+
| -------------------------------------------- | ------------------------------- |
|
|
840
|
+
| `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` | required for any of it to apply |
|
|
841
|
+
| `AWS_SESSION_TOKEN` | temporary credentials |
|
|
842
|
+
| `AWS_REGION`, `AWS_DEFAULT_REGION` | region |
|
|
843
|
+
| `AWS_S3_ENDPOINT`, `AWS_ENDPOINT_URL_S3` | endpoint |
|
|
844
|
+
| `AWS_S3_FORCE_PATH_STYLE` | addressing |
|
|
845
|
+
|
|
846
|
+
### Signed here, not by an SDK
|
|
847
|
+
|
|
848
|
+
SigV4 for a GET is a hash, four HMACs and a string, all of which `node:crypto`
|
|
849
|
+
already has. An AWS client would be a large dependency for one signature, and
|
|
850
|
+
an optional one would make reading a bucket work on some installs and not on
|
|
851
|
+
others for no reason the operator could see β so it is neither optional nor a
|
|
852
|
+
dependency. The signer is checked against AWS's own published test vectors
|
|
853
|
+
rather than against itself.
|
|
854
|
+
|
|
855
|
+
Every request is signed as it goes out, including the range: the header, each
|
|
856
|
+
directory and each tile. Nothing is presigned and nothing is cached but the
|
|
857
|
+
bytes, so revoking a key stops reads at the next request rather than at the
|
|
858
|
+
next restart. Changing the settings drops the open readers for the same
|
|
859
|
+
reason.
|
|
860
|
+
|
|
861
|
+
### What a bucket source is, and is not
|
|
862
|
+
|
|
863
|
+
It is a URL source in every other respect β see
|
|
864
|
+
[tile-stacks.md](tile-stacks.md), "A source read straight from a URL". It
|
|
865
|
+
carries its own `minzoom`, `maxzoom` and `bounds`, it is never seeded or
|
|
866
|
+
retired, and it can be baked: a stack over private buckets exports to an
|
|
867
|
+
ordinary archive with an infohash, which is the path from "terrain we hold in
|
|
868
|
+
a bucket" to "terrain we publish".
|
|
869
|
+
|
|
779
870
|
## Mutable publishing
|
|
780
871
|
|
|
781
872
|
Announcing the current build of each category over the DHT (BEP 46).
|
package/docs/tile-stacks.md
CHANGED
|
@@ -53,6 +53,8 @@ of its parts.
|
|
|
53
53
|
- [What a mask has to match](#what-a-mask-has-to-match)
|
|
54
54
|
- [Feathering a seam](#feathering-a-seam)
|
|
55
55
|
- [A stack as a source](#a-stack-as-a-source)
|
|
56
|
+
- [A source read straight from a URL](#a-source-read-straight-from-a-url)
|
|
57
|
+
- [Importing a list of URLs](#importing-a-list-of-urls)
|
|
56
58
|
- [Exporting on a schedule](#exporting-on-a-schedule)
|
|
57
59
|
- [Finding a stack](#finding-a-stack)
|
|
58
60
|
- [Syncing a stack to another node](#syncing-a-stack-to-another-node)
|
|
@@ -255,20 +257,23 @@ it differs from the snake_case rio-rgbify-merge uses.
|
|
|
255
257
|
|
|
256
258
|
### Source fields
|
|
257
259
|
|
|
258
|
-
| Field | Meaning
|
|
259
|
-
| ---------------------- |
|
|
260
|
-
| `category` | Resolve to the newest build in this category.
|
|
261
|
-
| `archive` | Or pin one infohash.
|
|
262
|
-
| `
|
|
263
|
-
| `
|
|
264
|
-
| `
|
|
265
|
-
| `
|
|
266
|
-
| `
|
|
267
|
-
| `
|
|
268
|
-
| `
|
|
269
|
-
| `
|
|
270
|
-
| `
|
|
271
|
-
| `
|
|
260
|
+
| Field | Meaning |
|
|
261
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
262
|
+
| `category` | Resolve to the newest build in this category. |
|
|
263
|
+
| `archive` | Or pin one infohash. |
|
|
264
|
+
| `stack` | Or another stack, merged as heights. See "A stack as a source". |
|
|
265
|
+
| `url` | Or an archive read straight from an http(s) address, no torrent involved. Exactly one of the four. |
|
|
266
|
+
| `minzoom` / `maxzoom` | Skip a `url` source outside this zoom range before opening it. Optional; the header answers anyway without it. |
|
|
267
|
+
| `required` | A tile fails rather than being served without this source. Defaults true for the bottom-most, false above. |
|
|
268
|
+
| `encoding` | `mapbox` or `terrarium`. Elevation space only. |
|
|
269
|
+
| `baseVal` / `interval` | Mapbox decode offset and step. Default `-10000` and `0.1`. |
|
|
270
|
+
| `maskValues` | Decoded heights meaning "no data here". Elevation space only. |
|
|
271
|
+
| `maskRange` | `[low, high]` in metres, or a list of them. Everything inside is nodata. Elevation space only. |
|
|
272
|
+
| `heightAdjustment` | Metres, added **after** masking. Elevation space only. |
|
|
273
|
+
| `feather` | Pixels to fade in over wherever the source stops: a `cutline`, `bounds`, or the holes a mask leaves. Max 64. |
|
|
274
|
+
| `featherMetres` | The same fade written as metres of ground, worked out per tile. Usually the one to reach for. |
|
|
275
|
+
| `opacity` | `0`β`1`, scales the source alpha. RGBA space only. |
|
|
276
|
+
| `blend` | `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`. RGBA space only. |
|
|
272
277
|
|
|
273
278
|
`attribution` is not optional in practice. A stack is a derived work of every
|
|
274
279
|
source in it, and the thing that reliably gets lost when tiles are combined is
|
|
@@ -1631,6 +1636,224 @@ would wait a day before showing it had ever run.
|
|
|
1631
1636
|
That is what a second node serving the same stacks wants: only one of them
|
|
1632
1637
|
should be the node that bakes.
|
|
1633
1638
|
|
|
1639
|
+
## A source read straight from a URL
|
|
1640
|
+
|
|
1641
|
+
A source may name a URL instead of a category, an archive or a stack:
|
|
1642
|
+
|
|
1643
|
+
```json
|
|
1644
|
+
{
|
|
1645
|
+
"id": "mapterhorn",
|
|
1646
|
+
"sources": [
|
|
1647
|
+
{
|
|
1648
|
+
"url": "https://download.mapterhorn.com/planet.pmtiles",
|
|
1649
|
+
"encoding": "terrarium",
|
|
1650
|
+
"minzoom": 0,
|
|
1651
|
+
"maxzoom": 12
|
|
1652
|
+
},
|
|
1653
|
+
{
|
|
1654
|
+
"url": "https://download.mapterhorn.com/6-32-31.pmtiles",
|
|
1655
|
+
"encoding": "terrarium",
|
|
1656
|
+
"bounds": [0, 0, 5.625, 5.625],
|
|
1657
|
+
"minzoom": 13,
|
|
1658
|
+
"maxzoom": 13
|
|
1659
|
+
}
|
|
1660
|
+
]
|
|
1661
|
+
}
|
|
1662
|
+
```
|
|
1663
|
+
|
|
1664
|
+
Mapterhorn publishes terrain this way: a global base to about z12 and several
|
|
1665
|
+
hundred regional patches reaching higher, each one a plain HTTPS download and
|
|
1666
|
+
none of it in a swarm this node is part of. Downloading 11.8 TiB of it first,
|
|
1667
|
+
to get it into a category the ordinary way, is not a step anybody wants β the
|
|
1668
|
+
whole point is to read a tile at a time, from wherever it already is.
|
|
1669
|
+
|
|
1670
|
+
### Read the same way a swarm archive is
|
|
1671
|
+
|
|
1672
|
+
`FetchSource`, from the `pmtiles` package this project already depends on,
|
|
1673
|
+
asks for byte ranges the way `TorrentSource` does β a request for a tile costs
|
|
1674
|
+
the header once, the directory once, and the tile itself, not the file. A URL
|
|
1675
|
+
source keeps its own small cache of open readers, separate from the one
|
|
1676
|
+
catalog archives share, so a stack naming hundreds of them is not competing
|
|
1677
|
+
with the archives this node actually seeds for the same budget.
|
|
1678
|
+
|
|
1679
|
+
Parent climbing works the same way it does for a category source: a shallow
|
|
1680
|
+
archive β Mapterhorn's own base stops at z12 β is upscaled for a deeper
|
|
1681
|
+
request exactly as GEBCO is, through the same code, because reading one is now
|
|
1682
|
+
a question of which store method answers and nothing else.
|
|
1683
|
+
|
|
1684
|
+
### Adding one by hand
|
|
1685
|
+
|
|
1686
|
+
**Add source β an address you typeβ¦** in the stack editor, which is the same
|
|
1687
|
+
source the importer writes and edits the same way. The card asks for two
|
|
1688
|
+
things nothing else can know: the address, and the zoom range the archive
|
|
1689
|
+
holds. A catalog archive states its range in its own header and a URL source
|
|
1690
|
+
has no header this node has read, so an unstated range means every tile asks
|
|
1691
|
+
it β correct, and the thing worth avoiding once there are several.
|
|
1692
|
+
|
|
1693
|
+
Anything published as a plain HTTPS download works, which includes an S3
|
|
1694
|
+
bucket that serves range requests: an object URL, or a presigned one, is an
|
|
1695
|
+
address like any other, and this asks for byte ranges the way any PMTiles
|
|
1696
|
+
reader does. What it cannot do is a private bucket named `s3://β¦`, which is
|
|
1697
|
+
not a URL a browser or a fetch can follow β that needs a signed request and
|
|
1698
|
+
somewhere to keep the credentials, and neither exists here yet.
|
|
1699
|
+
|
|
1700
|
+
### Cheap to have hundreds of
|
|
1701
|
+
|
|
1702
|
+
The one thing that makes hundreds of these practical rather than merely
|
|
1703
|
+
possible: `bounds` and the two new per-source fields, `minzoom` and `maxzoom`,
|
|
1704
|
+
are checked **before** anything is opened. A tile request outside a source's
|
|
1705
|
+
stated box, or at a zoom no climb from here could reach into its stated range,
|
|
1706
|
+
skips that source without a request leaving this node. For a stack built from
|
|
1707
|
+
Mapterhorn's own file list β one tile mostly intersects a global base and one
|
|
1708
|
+
or two regional patches β that is most of several hundred sources, on every
|
|
1709
|
+
tile, settled from the recipe alone.
|
|
1710
|
+
|
|
1711
|
+
Without a stated zoom range this still works, just later: `FetchSource`'s own
|
|
1712
|
+
header read answers "no tile" cheaply once opened, the same way a catalog
|
|
1713
|
+
archive's does. The static check is what avoids opening most of them at all,
|
|
1714
|
+
not what makes opening one safe.
|
|
1715
|
+
|
|
1716
|
+
### What it does not do
|
|
1717
|
+
|
|
1718
|
+
**Never passed through raw.** A URL source has no infohash to answer the
|
|
1719
|
+
byte-for-byte question with, so a stack containing one always decodes β
|
|
1720
|
+
`needsCodec` says so from the recipe, the same answer a nested stack gets and
|
|
1721
|
+
for the same reason.
|
|
1722
|
+
|
|
1723
|
+
**Not seeded, not retired, not rebuilt.** This node does not hold a copy, so
|
|
1724
|
+
none of the mechanisms that apply to an archive apply here. The URL is not
|
|
1725
|
+
content-addressed either β nothing stops whoever publishes it editing the file
|
|
1726
|
+
in place β so it is treated as unstable for caching, the same as a category
|
|
1727
|
+
and for a related reason: a category is unstable because the build under it
|
|
1728
|
+
moves, a URL because the operator does not control what is at the other end
|
|
1729
|
+
of it.
|
|
1730
|
+
|
|
1731
|
+
**Feathering a mask edge only partly.** A URL source that both masks and fades
|
|
1732
|
+
needs its parents read to measure the ramp, exactly as a catalog source does β
|
|
1733
|
+
and that path is wired up. What is not: a parent read that fails is caught and
|
|
1734
|
+
skipped silently, the same forgiving rule `readMaskEdges` already applies to a
|
|
1735
|
+
catalog source whose parent cannot be reached, so a patchy host degrades the
|
|
1736
|
+
ramp rather than failing the tile.
|
|
1737
|
+
|
|
1738
|
+
## Importing a list of URLs
|
|
1739
|
+
|
|
1740
|
+
Naming 458 sources by hand is not work anybody should do once, let alone again
|
|
1741
|
+
when the provider adds a file. A provider that publishes terrain as many
|
|
1742
|
+
separate archives generally publishes an index of them as well, and that index
|
|
1743
|
+
already carries exactly what the merge needs in order to skip a source without
|
|
1744
|
+
opening it.
|
|
1745
|
+
|
|
1746
|
+
**Stacks β Import URL listβ¦** takes the address of one. Mapterhorn's is
|
|
1747
|
+
`https://download.mapterhorn.com/download_urls.json`.
|
|
1748
|
+
|
|
1749
|
+
### Two shapes, detected rather than declared
|
|
1750
|
+
|
|
1751
|
+
An **index**: an object with an `items` list, or a bare array of the same
|
|
1752
|
+
entries. Each entry needs a `url` and the six numbers Mapterhorn's carries β
|
|
1753
|
+
`min_lon`, `min_lat`, `max_lon`, `max_lat`, `min_zoom`, `max_zoom` β which
|
|
1754
|
+
become the source's `bounds` and its `minzoom`/`maxzoom`.
|
|
1755
|
+
|
|
1756
|
+
A **plain list** of addresses, one per line or as a JSON array of strings, for
|
|
1757
|
+
a provider that publishes no index. Blank lines and `#` comments are ignored.
|
|
1758
|
+
Nothing states a box or a zoom range, so every source is opened for every tile
|
|
1759
|
+
it might cover β which is the cost of having no index, and the reason the index
|
|
1760
|
+
path is worth preferring.
|
|
1761
|
+
|
|
1762
|
+
Which one it is is worked out from the document, because somebody pasting an
|
|
1763
|
+
address has no reason to know which their provider answers with, and guessing
|
|
1764
|
+
wrong would be a silent import of nothing rather than an error.
|
|
1765
|
+
|
|
1766
|
+
### The global file becomes the base
|
|
1767
|
+
|
|
1768
|
+
An entry whose box covers the world β Mapterhorn's `planet.pmtiles` says -180
|
|
1769
|
+
to 180 and Β±85.0511 β is made `sources[0]` and marked `required`. A stack is
|
|
1770
|
+
painted bottom-first, so the thing covering everywhere has to sit underneath
|
|
1771
|
+
everything patching it, and the index does not list it first.
|
|
1772
|
+
|
|
1773
|
+
It is given no `bounds`: a clip excluding nothing is a rasterise on every
|
|
1774
|
+
partial tile for an answer that was never in doubt. Where several entries claim
|
|
1775
|
+
to be global, the one reaching deepest wins and the rest become ordinary
|
|
1776
|
+
layers.
|
|
1777
|
+
|
|
1778
|
+
### The encoding is the importer's to state
|
|
1779
|
+
|
|
1780
|
+
An index rarely says. Mapterhorn's files are all terrarium and its JSON never
|
|
1781
|
+
mentions it, so the dialog asks and sets it on every imported source β a
|
|
1782
|
+
terrain source read with the wrong encoding is a cliff face rather than a
|
|
1783
|
+
mistake anybody has to guess at.
|
|
1784
|
+
|
|
1785
|
+
### Re-importing
|
|
1786
|
+
|
|
1787
|
+
Every imported source carries `importedFrom`, naming the list it came from.
|
|
1788
|
+
That is what lets the editor draw one row for several hundred sources, and what
|
|
1789
|
+
a re-import uses to know which are its to replace. A source typed by hand
|
|
1790
|
+
carries none and is never touched.
|
|
1791
|
+
|
|
1792
|
+
The batch also goes back **where it already was**, rather than on the end.
|
|
1793
|
+
Painting order is the whole meaning of a stack: a batch that moved on every
|
|
1794
|
+
re-import would quietly bury a local override somebody had deliberately placed
|
|
1795
|
+
above it β a day later, on a schedule, with nothing to say why the map changed.
|
|
1796
|
+
|
|
1797
|
+
### Fetched by the node, not the browser
|
|
1798
|
+
|
|
1799
|
+
The console is often on a different network from the node, and a list is only
|
|
1800
|
+
useful if the machine that will actually read those archives can reach them. A
|
|
1801
|
+
458-entry index is also not something a browser should be parsing and posting
|
|
1802
|
+
back a megabyte of.
|
|
1803
|
+
|
|
1804
|
+
`POST /api/stacks/<id>/import` takes `{ url, encoding }`. With `dryRun` it
|
|
1805
|
+
answers what it would write without writing it, which is what **Check** in the
|
|
1806
|
+
dialog shows and what the editor's **Re-import** uses to update an unsaved
|
|
1807
|
+
draft. A draft can send its own `sources` along, so the batch comes back merged
|
|
1808
|
+
into the order the operator is holding rather than into the stored one.
|
|
1809
|
+
|
|
1810
|
+
### Following the list instead of importing it once
|
|
1811
|
+
|
|
1812
|
+
Mapterhorn's index has grown through several versions, and a node that imported
|
|
1813
|
+
it in March is a node serving March. The same address goes in **Settings β
|
|
1814
|
+
Feeds β Stack feeds** with **Into stack** naming the stack to keep level, and
|
|
1815
|
+
the poll that already checks other nodes for recipes checks this for files.
|
|
1816
|
+
|
|
1817
|
+
The row says which of the two it is by whether **Into stack** is filled in. A
|
|
1818
|
+
feed of recipes names its own stacks and this one cannot: an index is a list of
|
|
1819
|
+
files with no opinion about what they are for, so the row supplies the stack
|
|
1820
|
+
and the encoding the index does not state.
|
|
1821
|
+
|
|
1822
|
+
Reconciliation is the same one **Re-import** does, for the same reasons and
|
|
1823
|
+
with the same guarantees: hand-written sources are left alone, the batch stays
|
|
1824
|
+
where it was put, and a file the provider withdrew stops being asked for. A
|
|
1825
|
+
poll where nothing changed writes nothing at all β not the same recipe again,
|
|
1826
|
+
which would move its revision and with it every tile cached against it.
|
|
1827
|
+
|
|
1828
|
+
A stack named here that does not exist yet is created. One that does keeps
|
|
1829
|
+
everything it had; following a list never amounts to taking a recipe over.
|
|
1830
|
+
|
|
1831
|
+
### What a bulk import cannot say
|
|
1832
|
+
|
|
1833
|
+
Every imported source gets a box and a zoom range, because the index states
|
|
1834
|
+
them. It gets no **mask** β no per-source clip beyond its bounding box, no
|
|
1835
|
+
feathered edge β because there is nowhere in an index for one to come from.
|
|
1836
|
+
|
|
1837
|
+
That is fine where the provider has already merged for you. Mapterhorn's files
|
|
1838
|
+
are cut so that a patch and the planet agree at the seam, so a rectangular clip
|
|
1839
|
+
is the right clip. It is not fine where two sources genuinely overlap at
|
|
1840
|
+
different quality and the edge between them matters, and there the answer is to
|
|
1841
|
+
import the batch and then write the overlapping source by hand, above it.
|
|
1842
|
+
|
|
1843
|
+
Note what happens if you edit an imported source instead: a mask typed onto one
|
|
1844
|
+
is wiped by the next re-import, which replaces the batch entire. On a schedule
|
|
1845
|
+
that is a map changing at 4am with nothing to say why. A hand-written source
|
|
1846
|
+
carries no `importedFrom`, is never replaced, and is the place for anything the
|
|
1847
|
+
index could not have told us.
|
|
1848
|
+
|
|
1849
|
+
### Seeing them in the console
|
|
1850
|
+
|
|
1851
|
+
A stack's source list collapses behind a summary once there are more than
|
|
1852
|
+
five, with the count and the base named on the fold. A stack imported from a
|
|
1853
|
+
provider's list is several hundred rows and drawing them flat buries every
|
|
1854
|
+
other stack on the page under one of them; a stack of a base and a layer or
|
|
1855
|
+
two stays open, since folding that hides nothing worth a click.
|
|
1856
|
+
|
|
1634
1857
|
## Finding a stack
|
|
1635
1858
|
|
|
1636
1859
|
A stack has no infohash and appears in no feed, so nothing about it is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.91.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",
|