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 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 |
@@ -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).
@@ -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. Exactly one of the two. |
262
- | `required` | A tile fails rather than being served without this source. Defaults true for the bottom-most, false above. |
263
- | `encoding` | `mapbox` or `terrarium`. Elevation space only. |
264
- | `baseVal` / `interval` | Mapbox decode offset and step. Default `-10000` and `0.1`. |
265
- | `maskValues` | Decoded heights meaning "no data here". Elevation space only. |
266
- | `maskRange` | `[low, high]` in metres, or a list of them. Everything inside is nodata. Elevation space only. |
267
- | `heightAdjustment` | Metres, added **after** masking. Elevation space only. |
268
- | `feather` | Pixels to fade in over wherever the source stops: a `cutline`, `bounds`, or the holes a mask leaves. Max 64. |
269
- | `featherMetres` | The same fade written as metres of ground, worked out per tile. Usually the one to reach for. |
270
- | `opacity` | `0`–`1`, scales the source alpha. RGBA space only. |
271
- | `blend` | `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`. RGBA space only. |
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.89.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",