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 +30 -0
- package/docs/tilejson.md +43 -0
- package/package.json +1 -1
- package/src/feed.js +14 -0
- package/src/pmtiles-probe.js +53 -0
- package/src/tilejson.js +10 -0
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.
|
|
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.
|
package/src/pmtiles-probe.js
CHANGED
|
@@ -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
|
|