pmtiles-swarm 0.54.2 → 0.55.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,36 @@
7
7
  ### 🐞 Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.55.0
11
+ ### ✨ Features and improvements
12
+ - **The TileJSON now carries a `raster-dem` archive's `encoding`**, read out of the archive's own
13
+ metadata the same way `sparse` already is. Nothing in a PMTiles header carries this — the header
14
+ knows the tile is WebP, not what its three channels mean — so the metadata is the only place it can
15
+ come from, and without it a consumer falls back to a default that is wrong for exactly the archives
16
+ that most need to speak up: a terrarium-packed DEM read as `mapbox` decodes every mountain into
17
+ noise, silently, with a plausible-looking map on screen.
18
+
19
+ Same key and meaning as tileserver-gl, which reads `tileJSON.encoding` and accepts `terrarium` or
20
+ `mapbox`. The difference is where it comes from — there it is configuration, set per layer beside
21
+ the server; here it travels with the archive, so a mirror reads the same answer without being
22
+ configured again and a style no longer has to restate what the archive already knows. Restating it
23
+ is how a style and its data drift into disagreeing. MapLibre applies TileJSON members to the source
24
+ after construction, so this overrides an encoding written in the style, which is the intended
25
+ direction.
26
+
27
+ `custom` brings `redFactor`, `greenFactor`, `blueFactor` and `baseShift` with it, and only when all
28
+ four are present: `custom` means "the channels mean what these numbers say", so the word without
29
+ the numbers is an archive nobody can read. Anything outside the three values the style
30
+ specification defines is dropped rather than passed on — a client handed an encoding it does not
31
+ recognise is worse off than one handed nothing.
32
+
33
+ Carried through the feed as `<pmtiles:encoding>` too, so a subscriber serves elevation correctly
34
+ from the moment it joins rather than reading noise until it has probed the header itself. The
35
+ custom factors deliberately are not: an archive needing four numbers to be legible is one a mirror
36
+ should read for itself rather than take on trust from somebody else's document.
37
+
38
+ ### 🐞 Bug fixes
39
+
10
40
  ## 0.54.2
11
41
  ### ✨ Features and improvements
12
42
  - **Written down and tested: a range read is cached exactly as a tile read is**, because it is the
package/docs/tilejson.md CHANGED
@@ -16,6 +16,7 @@ page is what they mean.
16
16
  - [One string, both kinds of client](#one-string-both-kinds-of-client)
17
17
  - [Where this magnet shows up](#where-this-magnet-shows-up)
18
18
  - [`sparse`](#sparse)
19
+ - [`encoding`](#encoding)
19
20
  - [A complete example](#a-complete-example)
20
21
  - [What a plain client sees](#what-a-plain-client-sees)
21
22
 
@@ -145,6 +146,48 @@ reads the same answer rather than falling back to guessing from the tile format.
145
146
  See
146
147
  [internals.md](internals.md#answering-for-a-tile-that-is-not-there).
147
148
 
149
+ ## `encoding`
150
+
151
+ How a `raster-dem` archive packs elevation into pixels: `terrarium`, `mapbox` or
152
+ `custom`. Present only when the archive itself said so in its metadata.
153
+
154
+ Nothing in a PMTiles header carries this — the header knows the tile is WebP,
155
+ not what its three channels mean — so the metadata is the only place it can come
156
+ from, and the PMTiles specification explicitly allows metadata beyond the keys
157
+ it names. Without it a consumer falls back to a default, and the default is
158
+ wrong for exactly the archives that most need to speak up: a terrarium-packed
159
+ DEM read as `mapbox` decodes every mountain into noise, silently, with a
160
+ plausible-looking map on screen and nothing to point at.
161
+
162
+ Same key and same meaning as tileserver-gl, which reads `tileJSON.encoding` and
163
+ accepts `terrarium` or `mapbox`. The difference is where it comes from: there it
164
+ is configuration, set per layer in the server's own config, so the answer lives
165
+ beside the server rather than beside the data. Here it travels with the archive,
166
+ which is the point — a node mirroring it reads the same answer without being
167
+ configured again, and a style pointing at this TileJSON no longer has to restate
168
+ an encoding the archive already knows. Restating it is how a style and its data
169
+ drift into disagreeing.
170
+
171
+ MapLibre applies every TileJSON member to the source after the source is
172
+ constructed, so an `encoding` here **overrides** one written in the style. That
173
+ is the intended direction: the archive is the thing that knows.
174
+
175
+ Anything other than the three values the style specification defines is dropped
176
+ rather than passed on. A client handed an encoding it does not recognise is
177
+ worse off than one handed nothing, because nothing at least leaves it free to
178
+ use its own default.
179
+
180
+ ### `redFactor`, `greenFactor`, `blueFactor`, `baseShift`
181
+
182
+ Carried only alongside `encoding: "custom"`, and only when all four are present.
183
+ `custom` means "the channels mean what these numbers say", so publishing the
184
+ word without the numbers publishes an archive nobody can read — and three
185
+ numbers with one missing is no better, so the whole claim is dropped instead.
186
+
187
+ The feed carries `<pmtiles:encoding>` but not these: an archive that needs four
188
+ numbers to be legible is one a mirror should read for itself rather than take on
189
+ trust from somebody else's document.
190
+
148
191
  ## A complete example
149
192
 
150
193
  ```json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.54.2",
3
+ "version": "0.55.0",
4
4
  "description": "BitTorrent distribution for PMTiles map archives: create torrents, watch folders, publish and subscribe to RSS feeds, and seed through qBittorrent or an embedded client",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/feed.js CHANGED
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  import { mutableMagnet } from './mutable.js';
21
+ import { metadataEncoding } from './pmtiles-probe.js';
21
22
 
22
23
  const PMTILES_NS = 'https://github.com/TechIdiots-LLC/pmtiles-swarm/ns/1.0';
23
24
 
@@ -119,6 +120,14 @@ function renderItem(entry, baseUrl) {
119
120
  map.attribution
120
121
  ? ` <pmtiles:attribution>${xml(map.attribution)}</pmtiles:attribution>`
121
122
  : '',
123
+ // So a subscriber can serve elevation correctly from the moment it
124
+ // joins, rather than reading noise until it has probed the header
125
+ // itself. The custom factors are deliberately not carried: an archive
126
+ // that needs four numbers to be legible is one a mirror should read
127
+ // for itself rather than take on trust from a feed.
128
+ map.encoding
129
+ ? ` <pmtiles:encoding>${xml(map.encoding)}</pmtiles:encoding>`
130
+ : '',
122
131
  ]
123
132
  .filter(Boolean)
124
133
  .join('\n')
@@ -233,6 +242,11 @@ function mapSummary(block) {
233
242
  bounds: bounds.length === 4 ? bounds : undefined,
234
243
  tileCount: number('pmtiles:tiles'),
235
244
  attribution: tag(block, 'pmtiles:attribution'),
245
+ // Carried because a subscriber serves tiles from this summary before it
246
+ // has read a byte of the archive, and for a raster-dem archive the
247
+ // encoding is the difference between elevation and noise. Validated on the
248
+ // way in, since a feed is somebody else's document.
249
+ encoding: metadataEncoding(tag(block, 'pmtiles:encoding')),
236
250
  // Says where this came from, because a summary taken on trust from another
237
251
  // node is not the same fact as one read off the archive's own header, and
238
252
  // the difference matters when the two disagree.
@@ -58,6 +58,49 @@ export function metadataFlag(value) {
58
58
  return undefined;
59
59
  }
60
60
 
61
+ /**
62
+ * What a `raster-dem` archive says about how its elevations are packed.
63
+ *
64
+ * Nothing in a PMTiles header says this — the header knows the tile is WebP,
65
+ * not what the three channels mean — so the only place it can come from is the
66
+ * archive's own metadata. Without it a consumer falls back to a default, and
67
+ * the default is wrong for exactly the archives that most need to say
68
+ * something: a terrarium-packed DEM read as `mapbox` decodes every mountain
69
+ * into noise, silently, with a plausible-looking map on screen.
70
+ *
71
+ * The values are the ones the style specification defines, and anything else
72
+ * is dropped rather than passed on. A tile client handed an encoding it does
73
+ * not recognise is worse off than one handed nothing, because nothing at least
74
+ * leaves it free to use its own default.
75
+ * @param {unknown} value - Whatever the metadata held.
76
+ * @returns {string | undefined} - A known encoding, or undefined.
77
+ */
78
+ export function metadataEncoding(value) {
79
+ if (typeof value !== 'string') return undefined;
80
+ const text = value.trim().toLowerCase();
81
+ return ['terrarium', 'mapbox', 'custom'].includes(text) ? text : undefined;
82
+ }
83
+
84
+ /**
85
+ * The numbers a `custom` encoding is meaningless without.
86
+ *
87
+ * `encoding: "custom"` says "the channels mean what these four factors say",
88
+ * so carrying the word and not the factors publishes an archive nobody can
89
+ * read. They travel together or not at all.
90
+ * @param {object} metadata - The archive's metadata.
91
+ * @returns {object | undefined} - The four factors, or undefined.
92
+ */
93
+ export function customEncodingFactors(metadata) {
94
+ const named = ['redFactor', 'greenFactor', 'blueFactor', 'baseShift'];
95
+ const out = {};
96
+ for (const name of named) {
97
+ const value = Number(metadata[name]);
98
+ if (!Number.isFinite(value)) return undefined;
99
+ out[name] = value;
100
+ }
101
+ return out;
102
+ }
103
+
61
104
  /**
62
105
  * Reads an archive's header and metadata.
63
106
  *
@@ -103,6 +146,16 @@ export function summarize(header, metadata = {}) {
103
146
  // the same key, so an archive built to be served there carries the answer
104
147
  // with it and does not have to be configured again here.
105
148
  sparse: metadataFlag(metadata.sparse),
149
+ // How a raster-dem archive packs elevation into pixels: terrarium, mapbox
150
+ // or custom. Read from the archive rather than configured here, for the
151
+ // same reason `sparse` is — the archive knows, and a mirror of it should
152
+ // not have to be told again.
153
+ encoding: metadataEncoding(metadata.encoding),
154
+ // Only where the encoding is custom, since they mean nothing otherwise.
155
+ encodingFactors:
156
+ metadataEncoding(metadata.encoding) === 'custom'
157
+ ? customEncodingFactors(metadata)
158
+ : undefined,
106
159
  };
107
160
  }
108
161
 
package/src/tilejson.js CHANGED
@@ -55,6 +55,16 @@ export function buildTileJson(entry, baseUrl) {
55
55
  // Passed on so the next node to mirror this archive reads the same answer we
56
56
  // did, rather than falling back to a guess from the tile format.
57
57
  if (summary.sparse !== undefined) doc.sparse = summary.sparse;
58
+ // How to read the pixels of a raster-dem archive. tileserver-gl reads the
59
+ // same key, so an archive built to be served there carries the answer with
60
+ // it — and a style pointing at this TileJSON no longer has to restate an
61
+ // encoding that the archive already knows, which is how a style and its data
62
+ // drift into disagreeing.
63
+ if (summary.encoding) doc.encoding = summary.encoding;
64
+ // `custom` is unreadable without them, so they go wherever it does.
65
+ if (summary.encoding === 'custom' && summary.encodingFactors) {
66
+ Object.assign(doc, summary.encodingFactors);
67
+ }
58
68
  if (summary.format === 'pbf') doc.format = 'pbf';
59
69
  else if (summary.format) doc.format = summary.format;
60
70