pmtiles-swarm 0.89.0 β†’ 0.90.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,82 @@
7
7
  ### 🐞 Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.90.0
11
+ ### ✨ Features and improvements
12
+ - **A stack source may name a URL instead of a category, archive or stack.** `{ "url": "https://…" }`,
13
+ read straight over HTTP with no torrent involved β€” for an archive published as a plain download,
14
+ like Mapterhorn's terrain: a global base plus hundreds of regional patches, 11.8 TiB in total and
15
+ never meant to be downloaded whole. `FetchSource`, from the `pmtiles` package this project already
16
+ depends on, asks for byte ranges the way a swarm-backed source does; a tile costs the header once,
17
+ the directory once, and the tile itself.
18
+
19
+ Parent climbing works the same way it does for a category source β€” a shallow archive upscales for a
20
+ deeper request through the same code, since reading one is now only a question of which store method
21
+ answers. New per-source `minzoom` and `maxzoom` fields, checked before anything is opened, are what
22
+ make hundreds of these practical: a tile request outside a source's stated box or zoom range skips it
23
+ without a request leaving this node, which for a stack built from a provider's whole file list is
24
+ most of them, on every tile. A URL source always decodes rather than passing through raw, having no
25
+ infohash to answer that question with, and is not seeded, retired or rebuilt β€” none of the mechanisms
26
+ built for an archive this node actually holds apply to one it does not.
27
+
28
+ - **A provider's list of PMTiles URLs can be imported as sources.** **Stacks β†’ Import URL list…**,
29
+ or `POST /api/stacks/<id>/import`. Mapterhorn's `download_urls.json` names 458 files with the box
30
+ and zoom range of each; naming those by hand is not work anybody should do once, let alone again
31
+ when the provider adds one. An index with an `items` list is read, and so is a plain list of
32
+ addresses for a provider that publishes no index β€” which shape it is is worked out from the
33
+ document, since somebody pasting an address has no reason to know.
34
+
35
+ The global file becomes `sources[0]` and `required`: a stack is painted bottom-first, so the thing
36
+ covering everywhere has to sit under everything patching it, and the index does not list it first.
37
+ Every other entry keeps its box and zoom range, which is what lets a tile outside one skip it
38
+ without a request leaving the node. The encoding is asked for rather than read β€” an index rarely
39
+ states it, Mapterhorn's files are all terrarium and its JSON never says so, and a terrain source
40
+ read with the wrong one is a cliff face.
41
+
42
+ The list is fetched **by the node**, not the browser: the console is often on a different network,
43
+ and what matters is that the machine which will read the archives can reach them. **Check** shows
44
+ what an import would write before it writes anything.
45
+
46
+ In the editor an imported batch is one row rather than several hundred cards, with **Re-import**
47
+ and **Remove all**. A re-import replaces only what came from that same address β€” anything typed by
48
+ hand is left alone β€” and puts the batch back **where it already was** rather than on the end:
49
+ painting order is the whole meaning of a stack, and a batch that moved each time would quietly bury
50
+ a local override, a day later, on a schedule, with nothing to say why the map had changed.
51
+
52
+
53
+ - **A stack can follow a provider's file list on a timer.** A stack feed row with **Into stack** set
54
+ reads a URL list rather than another node's recipes, and keeps the named stack level with it.
55
+ Mapterhorn's index has grown through several versions, and a node that imported it once is a node
56
+ serving whenever that was. The row says which of the two it is by whether **Into stack** is filled
57
+ in β€” a feed of recipes names its own stacks and an index cannot, being a list of files with no
58
+ opinion about what they are for, so the row supplies the stack and the encoding as well.
59
+
60
+ It reconciles exactly as **Re-import** does: hand-written sources untouched, the batch back where it
61
+ was, a withdrawn file dropped rather than left to 404 per tile. A poll where nothing changed writes
62
+ nothing at all β€” not the same recipe again, which would move its revision and with it every tile
63
+ cached against it. A stack that does not exist yet is created; one that does keeps everything it had.
64
+
65
+
66
+ ### 🐞 Bug fixes
67
+ - **A stack reported no encoding unless its recipe stated one outright.** Reading `output.encoding`
68
+ alone reports null for the ordinary recipe β€” the one that re-encodes to nothing and writes whatever
69
+ its base source is written in, which is every imported stack, since an import sets the encoding per
70
+ source. Three places read it that way and all three were wrong about terrain: the console offered no
71
+ terrain preview, the TileJSON told clients nothing so terrarium heights rendered through the mapbox
72
+ formula, and a baked archive was written with no encoding metadata at all β€” an export that had to be
73
+ corrected by hand afterwards to be readable.
74
+
75
+ - **A stack's TileJSON listed every source.** For a stack imported from a provider's index that is 458
76
+ addresses and 55 KB, in a document every map load fetches, describing files a client cannot use and
77
+ will never ask for β€” it reads tiles from the XYZ endpoint. It now names 25 with a count of the rest.
78
+ The extension it advertises is the one the endpoint actually serves, too: a stack whose sources state
79
+ no format has no coverage format to read, and the document said `.bin` for tiles answered as webp.
80
+
81
+ - **Cancel asked you to fill the form in first.** A `<button>` in a form submits, and a submit is
82
+ checked against the form's required fields before it goes anywhere β€” so cancelling the import or
83
+ warm dialog answered "Please fill out this field" about a field that was about to be discarded.
84
+
85
+
10
86
  ## 0.89.0
11
87
  ### ✨ Features and improvements
12
88
  - **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 |
@@ -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,200 @@ 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
+ ### Cheap to have hundreds of
1685
+
1686
+ The one thing that makes hundreds of these practical rather than merely
1687
+ possible: `bounds` and the two new per-source fields, `minzoom` and `maxzoom`,
1688
+ are checked **before** anything is opened. A tile request outside a source's
1689
+ stated box, or at a zoom no climb from here could reach into its stated range,
1690
+ skips that source without a request leaving this node. For a stack built from
1691
+ Mapterhorn's own file list β€” one tile mostly intersects a global base and one
1692
+ or two regional patches β€” that is most of several hundred sources, on every
1693
+ tile, settled from the recipe alone.
1694
+
1695
+ Without a stated zoom range this still works, just later: `FetchSource`'s own
1696
+ header read answers "no tile" cheaply once opened, the same way a catalog
1697
+ archive's does. The static check is what avoids opening most of them at all,
1698
+ not what makes opening one safe.
1699
+
1700
+ ### What it does not do
1701
+
1702
+ **Never passed through raw.** A URL source has no infohash to answer the
1703
+ byte-for-byte question with, so a stack containing one always decodes β€”
1704
+ `needsCodec` says so from the recipe, the same answer a nested stack gets and
1705
+ for the same reason.
1706
+
1707
+ **Not seeded, not retired, not rebuilt.** This node does not hold a copy, so
1708
+ none of the mechanisms that apply to an archive apply here. The URL is not
1709
+ content-addressed either β€” nothing stops whoever publishes it editing the file
1710
+ in place β€” so it is treated as unstable for caching, the same as a category
1711
+ and for a related reason: a category is unstable because the build under it
1712
+ moves, a URL because the operator does not control what is at the other end
1713
+ of it.
1714
+
1715
+ **Feathering a mask edge only partly.** A URL source that both masks and fades
1716
+ needs its parents read to measure the ramp, exactly as a catalog source does β€”
1717
+ and that path is wired up. What is not: a parent read that fails is caught and
1718
+ skipped silently, the same forgiving rule `readMaskEdges` already applies to a
1719
+ catalog source whose parent cannot be reached, so a patchy host degrades the
1720
+ ramp rather than failing the tile.
1721
+
1722
+ ## Importing a list of URLs
1723
+
1724
+ Naming 458 sources by hand is not work anybody should do once, let alone again
1725
+ when the provider adds a file. A provider that publishes terrain as many
1726
+ separate archives generally publishes an index of them as well, and that index
1727
+ already carries exactly what the merge needs in order to skip a source without
1728
+ opening it.
1729
+
1730
+ **Stacks β†’ Import URL list…** takes the address of one. Mapterhorn's is
1731
+ `https://download.mapterhorn.com/download_urls.json`.
1732
+
1733
+ ### Two shapes, detected rather than declared
1734
+
1735
+ An **index**: an object with an `items` list, or a bare array of the same
1736
+ entries. Each entry needs a `url` and the six numbers Mapterhorn's carries β€”
1737
+ `min_lon`, `min_lat`, `max_lon`, `max_lat`, `min_zoom`, `max_zoom` β€” which
1738
+ become the source's `bounds` and its `minzoom`/`maxzoom`.
1739
+
1740
+ A **plain list** of addresses, one per line or as a JSON array of strings, for
1741
+ a provider that publishes no index. Blank lines and `#` comments are ignored.
1742
+ Nothing states a box or a zoom range, so every source is opened for every tile
1743
+ it might cover β€” which is the cost of having no index, and the reason the index
1744
+ path is worth preferring.
1745
+
1746
+ Which one it is is worked out from the document, because somebody pasting an
1747
+ address has no reason to know which their provider answers with, and guessing
1748
+ wrong would be a silent import of nothing rather than an error.
1749
+
1750
+ ### The global file becomes the base
1751
+
1752
+ An entry whose box covers the world β€” Mapterhorn's `planet.pmtiles` says -180
1753
+ to 180 and Β±85.0511 β€” is made `sources[0]` and marked `required`. A stack is
1754
+ painted bottom-first, so the thing covering everywhere has to sit underneath
1755
+ everything patching it, and the index does not list it first.
1756
+
1757
+ It is given no `bounds`: a clip excluding nothing is a rasterise on every
1758
+ partial tile for an answer that was never in doubt. Where several entries claim
1759
+ to be global, the one reaching deepest wins and the rest become ordinary
1760
+ layers.
1761
+
1762
+ ### The encoding is the importer's to state
1763
+
1764
+ An index rarely says. Mapterhorn's files are all terrarium and its JSON never
1765
+ mentions it, so the dialog asks and sets it on every imported source β€” a
1766
+ terrain source read with the wrong encoding is a cliff face rather than a
1767
+ mistake anybody has to guess at.
1768
+
1769
+ ### Re-importing
1770
+
1771
+ Every imported source carries `importedFrom`, naming the list it came from.
1772
+ That is what lets the editor draw one row for several hundred sources, and what
1773
+ a re-import uses to know which are its to replace. A source typed by hand
1774
+ carries none and is never touched.
1775
+
1776
+ The batch also goes back **where it already was**, rather than on the end.
1777
+ Painting order is the whole meaning of a stack: a batch that moved on every
1778
+ re-import would quietly bury a local override somebody had deliberately placed
1779
+ above it β€” a day later, on a schedule, with nothing to say why the map changed.
1780
+
1781
+ ### Fetched by the node, not the browser
1782
+
1783
+ The console is often on a different network from the node, and a list is only
1784
+ useful if the machine that will actually read those archives can reach them. A
1785
+ 458-entry index is also not something a browser should be parsing and posting
1786
+ back a megabyte of.
1787
+
1788
+ `POST /api/stacks/<id>/import` takes `{ url, encoding }`. With `dryRun` it
1789
+ answers what it would write without writing it, which is what **Check** in the
1790
+ dialog shows and what the editor's **Re-import** uses to update an unsaved
1791
+ draft. A draft can send its own `sources` along, so the batch comes back merged
1792
+ into the order the operator is holding rather than into the stored one.
1793
+
1794
+ ### Following the list instead of importing it once
1795
+
1796
+ Mapterhorn's index has grown through several versions, and a node that imported
1797
+ it in March is a node serving March. The same address goes in **Settings β†’
1798
+ Feeds β†’ Stack feeds** with **Into stack** naming the stack to keep level, and
1799
+ the poll that already checks other nodes for recipes checks this for files.
1800
+
1801
+ The row says which of the two it is by whether **Into stack** is filled in. A
1802
+ feed of recipes names its own stacks and this one cannot: an index is a list of
1803
+ files with no opinion about what they are for, so the row supplies the stack
1804
+ and the encoding the index does not state.
1805
+
1806
+ Reconciliation is the same one **Re-import** does, for the same reasons and
1807
+ with the same guarantees: hand-written sources are left alone, the batch stays
1808
+ where it was put, and a file the provider withdrew stops being asked for. A
1809
+ poll where nothing changed writes nothing at all β€” not the same recipe again,
1810
+ which would move its revision and with it every tile cached against it.
1811
+
1812
+ A stack named here that does not exist yet is created. One that does keeps
1813
+ everything it had; following a list never amounts to taking a recipe over.
1814
+
1815
+ ### What a bulk import cannot say
1816
+
1817
+ Every imported source gets a box and a zoom range, because the index states
1818
+ them. It gets no **mask** β€” no per-source clip beyond its bounding box, no
1819
+ feathered edge β€” because there is nowhere in an index for one to come from.
1820
+
1821
+ That is fine where the provider has already merged for you. Mapterhorn's files
1822
+ are cut so that a patch and the planet agree at the seam, so a rectangular clip
1823
+ is the right clip. It is not fine where two sources genuinely overlap at
1824
+ different quality and the edge between them matters, and there the answer is to
1825
+ import the batch and then write the overlapping source by hand, above it.
1826
+
1827
+ Note what happens if you edit an imported source instead: a mask typed onto one
1828
+ is wiped by the next re-import, which replaces the batch entire. On a schedule
1829
+ that is a map changing at 4am with nothing to say why. A hand-written source
1830
+ carries no `importedFrom`, is never replaced, and is the place for anything the
1831
+ index could not have told us.
1832
+
1634
1833
  ## Finding a stack
1635
1834
 
1636
1835
  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.90.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",