pmtiles-swarm 0.97.0 → 0.98.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 +29 -0
- package/docs/tile-stacks.md +30 -0
- package/package.json +1 -1
- package/src/library.js +38 -6
- package/src/mbtiles.js +41 -0
- package/src/prewarm.js +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.98.0
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
- **A finished MBTiles archive works as a stack layer, and now says what it holds.** It always read:
|
|
13
|
+
the tile store opens one from a complete local copy, and a stack asks the store for tiles like
|
|
14
|
+
anything else. What it could not do was describe itself. Only PMTiles was ever probed for a
|
|
15
|
+
summary — right when only PMTiles could be served, and never revisited once MBTiles became
|
|
16
|
+
servable — so those entries carried none, and a summary is what `stackCoverage` reads. A stack
|
|
17
|
+
naming one advertised the fallbacks in its TileJSON, z0–z14 over the whole world, however narrow
|
|
18
|
+
the archive actually was. Nothing refused it and nothing warned: the recipe was valid, the tiles
|
|
19
|
+
were right, and only the coverage was a fiction.
|
|
20
|
+
|
|
21
|
+
Both formats that can be served are read for a summary now, through the same summariser —
|
|
22
|
+
`probeMbtiles` off the adapter's `getHeader` and `getMetadata`, which is what that adapter exists
|
|
23
|
+
for. Most of the MBTiles spec is optional, so where an archive states nothing the reader derives
|
|
24
|
+
what it can: the zoom range comes from the tiles table when `minzoom` and `maxzoom` are absent.
|
|
25
|
+
|
|
26
|
+
The metadata reader also carries `encoding` through, with the four factors a custom packing is
|
|
27
|
+
unreadable without. An elevation stack decodes the pixels rather than passing them through, so an
|
|
28
|
+
archive that lost its encoding was not a tile that failed but a tile of wrong heights.
|
|
29
|
+
|
|
30
|
+
Archives already in the catalog are summarised by the head warmer on its next pass, once complete
|
|
31
|
+
— an MBTiles has no head to pull out of a swarm, so there is nothing to be due until the file is
|
|
32
|
+
whole, and then the read is local and instant. Unchanged: `/latest/<category>` still offers no
|
|
33
|
+
tile endpoint for an MBTiles, because a category is a promise to every node and this one is only
|
|
34
|
+
keepable on a node holding the whole file. See [tile-stacks.md](docs/tile-stacks.md) — "An
|
|
35
|
+
MBTiles archive as a source".
|
|
36
|
+
|
|
37
|
+
### 🐞 Bug fixes
|
|
38
|
+
|
|
10
39
|
## 0.97.0
|
|
11
40
|
### ✨ Features and improvements
|
|
12
41
|
- **A stack can be told to forget what it has merged, and forgets it by itself when its recipe
|
package/docs/tile-stacks.md
CHANGED
|
@@ -29,6 +29,7 @@ of its parts.
|
|
|
29
29
|
- [Painting order](#painting-order)
|
|
30
30
|
- [The two pixel spaces](#the-two-pixel-spaces)
|
|
31
31
|
- [Naming a source](#naming-a-source)
|
|
32
|
+
- [An MBTiles archive as a source](#an-mbtiles-archive-as-a-source)
|
|
32
33
|
- [The config file](#the-config-file)
|
|
33
34
|
- [Translating a rio-rgbify-merge config](#translating-a-rio-rgbify-merge-config)
|
|
34
35
|
- [Evaluating one tile](#evaluating-one-tile)
|
|
@@ -174,6 +175,35 @@ something else.
|
|
|
174
175
|
A stack may mix the two. Whether _any_ source is category-resolved decides the
|
|
175
176
|
whole stack's caching headers, below.
|
|
176
177
|
|
|
178
|
+
### An MBTiles archive as a source
|
|
179
|
+
|
|
180
|
+
Either form may name an MBTiles archive, with one condition: **this node must
|
|
181
|
+
hold the complete file.** MBTiles is SQLite, whose pages are scattered rather
|
|
182
|
+
than spatially clustered, so it cannot be read a byte range at a time out of a
|
|
183
|
+
swarm the way PMTiles can — a source still arriving answers 503, and no amount
|
|
184
|
+
of waiting for the right pieces changes that. A stack with an MBTiles source is
|
|
185
|
+
therefore only servable on nodes that have finished downloading it.
|
|
186
|
+
|
|
187
|
+
Everything else is the same. It resolves, merges, clips, feathers and bakes
|
|
188
|
+
like any other source, because the tile store presents it through the same
|
|
189
|
+
three methods a PMTiles archive answers.
|
|
190
|
+
|
|
191
|
+
What it covers comes from the metadata table, read at import by `probeMbtiles`
|
|
192
|
+
and summarised by the same `summarize` the PMTiles path uses. Most of the
|
|
193
|
+
MBTiles spec is optional, so where an archive states nothing the reader derives
|
|
194
|
+
what it can — the zoom range comes from the tiles table itself when `minzoom`
|
|
195
|
+
and `maxzoom` are absent, and bounds default to the whole world.
|
|
196
|
+
|
|
197
|
+
Two keys outside the spec are read, both because this is where every tool that
|
|
198
|
+
needs them puts them: `sparse`, which tileserver-gl reads the same way, and
|
|
199
|
+
`encoding` — with `redFactor`, `greenFactor`, `blueFactor` and `baseShift` for
|
|
200
|
+
a custom packing. An elevation stack decodes the pixels rather than passing
|
|
201
|
+
them through, so an archive that loses its encoding is not a tile that fails but
|
|
202
|
+
a tile of wrong heights.
|
|
203
|
+
|
|
204
|
+
An archive already in the catalog from before any of this was probed is
|
|
205
|
+
summarised by the head warmer on its next pass, once it is complete.
|
|
206
|
+
|
|
177
207
|
## The config file
|
|
178
208
|
|
|
179
209
|
Stacks live in `data/stacks.json`, beside `data/catalog.json` and for the same
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.98.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/library.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { publishingBase, publishingFor, reachability } from './catalog.js';
|
|
15
15
|
import { linkLatest } from './latest-link.js';
|
|
16
16
|
import { checkOrigin, fingerprintOrigin } from './origin.js';
|
|
17
|
+
import { probeMbtiles } from './mbtiles.js';
|
|
17
18
|
import { probePMTiles } from './pmtiles-probe.js';
|
|
18
19
|
import { archiveDirName } from './savepath.js';
|
|
19
20
|
import {
|
|
@@ -303,11 +304,12 @@ export class Library {
|
|
|
303
304
|
});
|
|
304
305
|
|
|
305
306
|
try {
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
307
|
+
// Both formats that can be served get read for one. The kind comes from
|
|
308
|
+
// the content rather than the extension, which is what identify exists
|
|
309
|
+
// for.
|
|
310
|
+
const summary = await probeArchive(absolute, identified.kind).catch(
|
|
311
|
+
() => undefined,
|
|
312
|
+
);
|
|
311
313
|
|
|
312
314
|
const created = await createTorrentFromFile(absolute, {
|
|
313
315
|
creator: this.#creator(),
|
|
@@ -967,6 +969,11 @@ export class Library {
|
|
|
967
969
|
// progress through runningAdds() and says "watch the log" as it closes.
|
|
968
970
|
options.onValidated?.({ url, kind: identified.kind });
|
|
969
971
|
|
|
972
|
+
// PMTiles only, and not an oversight: this reads the archive over range
|
|
973
|
+
// requests before anything has been downloaded, and there is no partial
|
|
974
|
+
// read of a SQLite file that answers anything. An MBTiles mirrored from a
|
|
975
|
+
// URL is summarised by the warmer once the download finishes, which is the
|
|
976
|
+
// first moment it can be.
|
|
970
977
|
const summary =
|
|
971
978
|
identified.kind === 'pmtiles'
|
|
972
979
|
? await probePMTiles(url).catch(() => undefined)
|
|
@@ -1605,8 +1612,9 @@ export class Library {
|
|
|
1605
1612
|
// facts, and probing on the second one's behalf would hang.
|
|
1606
1613
|
let summary;
|
|
1607
1614
|
if (readable && torrent.progress === 1 && torrent.savePath) {
|
|
1608
|
-
summary = await
|
|
1615
|
+
summary = await probeArchive(
|
|
1609
1616
|
path.join(torrent.savePath, torrent.name),
|
|
1617
|
+
guessKind(torrent.name ?? ''),
|
|
1610
1618
|
).catch(() => undefined);
|
|
1611
1619
|
}
|
|
1612
1620
|
|
|
@@ -3383,6 +3391,30 @@ export function guessKind(name) {
|
|
|
3383
3391
|
return undefined;
|
|
3384
3392
|
}
|
|
3385
3393
|
|
|
3394
|
+
/**
|
|
3395
|
+
* Reads the summary out of whichever kind of archive this is.
|
|
3396
|
+
*
|
|
3397
|
+
* Only PMTiles was ever probed, which was right when only PMTiles could be
|
|
3398
|
+
* served. MBTiles became servable from a complete local copy and this did not
|
|
3399
|
+
* follow, so those entries carried no summary at all -- and a summary is what
|
|
3400
|
+
* everything downstream reads coverage from. A stack naming one advertised the
|
|
3401
|
+
* fallbacks in its TileJSON, z0-z14 over the whole world, rather than the zoom
|
|
3402
|
+
* range and bounds the archive states about itself.
|
|
3403
|
+
*
|
|
3404
|
+
* Anything else is left alone rather than guessed at. A .osm.pbf being mirrored
|
|
3405
|
+
* has no summary to read and asking for one is how it ends up reported as a
|
|
3406
|
+
* broken map instead of a file being distributed.
|
|
3407
|
+
* @param {string} location - Local path, or a URL for PMTiles.
|
|
3408
|
+
* @param {string} [kind] - What it was identified as; the name decides if not.
|
|
3409
|
+
* @returns {Promise<object|undefined>} - The summary, where there is one.
|
|
3410
|
+
*/
|
|
3411
|
+
export async function probeArchive(location, kind) {
|
|
3412
|
+
const format = kind ?? guessKind(location);
|
|
3413
|
+
if (format === 'mbtiles') return probeMbtiles(location);
|
|
3414
|
+
if (format === 'pmtiles') return probePMTiles(location);
|
|
3415
|
+
return undefined;
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3386
3418
|
/**
|
|
3387
3419
|
* Moves a file, whether or not the two paths share a filesystem.
|
|
3388
3420
|
*
|
package/src/mbtiles.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { summarize } from './pmtiles-probe.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Reading tiles out of a completed MBTiles archive.
|
|
@@ -199,6 +200,17 @@ export class MbtilesArchive {
|
|
|
199
200
|
// Same key tileserver-gl reads, carried through so an MBTiles archive
|
|
200
201
|
// gets the same treatment a PMTiles one does.
|
|
201
202
|
sparse: meta.sparse,
|
|
203
|
+
// How heights are packed into the pixels, and the four factors a custom
|
|
204
|
+
// packing is unreadable without. Not in the MBTiles spec -- neither is
|
|
205
|
+
// `sparse` -- but this is where every tool that needs it puts it, and a
|
|
206
|
+
// terrain archive that loses it is decoded as whatever the reader
|
|
207
|
+
// assumes. That is not a tile that fails; it is a tile of wrong heights,
|
|
208
|
+
// which is worse.
|
|
209
|
+
encoding: meta.encoding,
|
|
210
|
+
redFactor: meta.redFactor,
|
|
211
|
+
greenFactor: meta.greenFactor,
|
|
212
|
+
blueFactor: meta.blueFactor,
|
|
213
|
+
baseShift: meta.baseShift,
|
|
202
214
|
};
|
|
203
215
|
}
|
|
204
216
|
|
|
@@ -254,3 +266,32 @@ export class MbtilesArchive {
|
|
|
254
266
|
}
|
|
255
267
|
}
|
|
256
268
|
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Reads an MBTiles archive's summary, the way probePMTiles reads a PMTiles one.
|
|
272
|
+
*
|
|
273
|
+
* The same summariser, off the same two methods -- which is the whole point of
|
|
274
|
+
* the adapter above: MBTiles keeps in a metadata table what PMTiles keeps in a
|
|
275
|
+
* fixed header, and everything upstream of `summarize` should not have to know
|
|
276
|
+
* which it was reading. `getHeader` even derives the zoom range from the tiles
|
|
277
|
+
* table where the metadata omits it, so a summary comes back for an archive
|
|
278
|
+
* that declares almost nothing about itself.
|
|
279
|
+
*
|
|
280
|
+
* Local paths only, and not by oversight. probePMTiles takes a URL because
|
|
281
|
+
* PMTiles is read a byte range at a time, and SQLite is not: there is no
|
|
282
|
+
* partial read of an MBTiles that answers anything, which is the same reason
|
|
283
|
+
* it is served only from a complete local copy.
|
|
284
|
+
* @param {string} file - Path to the .mbtiles file.
|
|
285
|
+
* @returns {Promise<object>} - The summary, in the shape the catalog keeps.
|
|
286
|
+
*/
|
|
287
|
+
export async function probeMbtiles(file) {
|
|
288
|
+
const archive = await MbtilesArchive.open(file);
|
|
289
|
+
try {
|
|
290
|
+
return summarize(
|
|
291
|
+
await archive.getHeader(),
|
|
292
|
+
(await archive.getMetadata()) ?? {},
|
|
293
|
+
);
|
|
294
|
+
} finally {
|
|
295
|
+
archive.close();
|
|
296
|
+
}
|
|
297
|
+
}
|
package/src/prewarm.js
CHANGED
|
@@ -156,7 +156,14 @@ export class HeadWarmer {
|
|
|
156
156
|
// not, since an archive joined by magnet has no kind until its metadata
|
|
157
157
|
// arrives.
|
|
158
158
|
const kind = entry.kind ?? guessKind(entry.name ?? '');
|
|
159
|
-
if (kind !== 'pmtiles') return false;
|
|
159
|
+
if (kind !== 'pmtiles' && kind !== 'mbtiles') return false;
|
|
160
|
+
|
|
161
|
+
// MBTiles has no head to pull out of the swarm -- it is SQLite, read whole
|
|
162
|
+
// or not at all -- so there is nothing here to be due until the file is.
|
|
163
|
+
// Once it is, the read is local and instant, and it is the only thing that
|
|
164
|
+
// gives an archive already in the catalog the summary this did not use to
|
|
165
|
+
// record for it.
|
|
166
|
+
if (kind === 'mbtiles' && entry.complete !== true) return false;
|
|
160
167
|
|
|
161
168
|
// A summary is only an answer about *this disk* if a header on this disk
|
|
162
169
|
// produced it. `format` was standing in for that and does not mean it.
|
|
@@ -289,6 +296,11 @@ export class HeadWarmer {
|
|
|
289
296
|
`[warm] ${entry.name}: header read; its metadata is at the far end ` +
|
|
290
297
|
'of the archive and has not arrived yet',
|
|
291
298
|
);
|
|
299
|
+
} else if ((entry.kind ?? guessKind(entry.name ?? '')) === 'mbtiles') {
|
|
300
|
+
// Not a header: MBTiles keeps in a metadata table what PMTiles keeps
|
|
301
|
+
// in a fixed header, and a log saying otherwise sends whoever reads it
|
|
302
|
+
// looking for the wrong thing.
|
|
303
|
+
console.log(`[warm] ${entry.name}: metadata read`);
|
|
292
304
|
} else {
|
|
293
305
|
console.log(`[warm] ${entry.name}: header read`);
|
|
294
306
|
}
|