pmtiles-swarm 0.98.0 → 0.98.1

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,26 @@
7
7
  ### 🐞 Bug fixes
8
8
  - _...Add new stuff here..._
9
9
 
10
+ ## 0.98.1
11
+ ### ✨ Features and improvements
12
+ - **The two archive readers no longer import each other.** Summarising an MBTiles archive reached
13
+ into `pmtiles-probe.js` for `summarize`, which put one format's reader inside the other's module
14
+ and dragged the `pmtiles` package in behind it. The half neither format owns — the summary shape,
15
+ its version, the metadata readers and the tile-type table — is now `archive-summary.js`, which
16
+ imports nothing and opens nothing: it takes a header and a metadata document and returns a
17
+ summary. `pmtiles-probe.js` keeps the PMTiles half and `mbtiles.js` the SQLite half, and neither
18
+ reaches the other.
19
+
20
+ Nothing about what the node serves changes; this is where the code lives.
21
+
22
+ ### 🐞 Bug fixes
23
+ - **An MBTiles archive holding MLT tiles was summarised as an unknown format.** The table mapping a
24
+ format name onto a tile type had been written twice, in opposite directions, and the copy beside
25
+ the MBTiles reader was written by hand and had no entry for `mlt` — so an archive declaring it
26
+ came back as `unknown`, which is what the catalog then reported and what a stack over it read.
27
+ There is one table now, with the reverse lookup derived from it, so the two halves cannot drift
28
+ apart again.
29
+
10
30
  ## 0.98.0
11
31
  ### ✨ Features and improvements
12
32
  - **A finished MBTiles archive works as a stack layer, and now says what it holds.** It always read:
@@ -469,9 +469,11 @@ This is the one thing that stops a stack being a small feature, and it is
469
469
  narrower than "the node cannot handle tiles". It already does, in most of the
470
470
  ways a stack needs:
471
471
 
472
- - `identify.js` reads an archive's magic bytes; the prober reads PMTiles
473
- headers, directories and metadata; `mbtiles.js` queries tile rows out of
474
- SQLite.
472
+ - `identify.js` reads an archive's magic bytes; `pmtiles-probe.js` reads PMTiles
473
+ headers, directories and metadata; `mbtiles.js` queries tile rows and the
474
+ metadata table out of SQLite. Neither format's reader imports the other's:
475
+ what they share is `archive-summary.js`, which turns a header and a metadata
476
+ document into the summary the catalog keeps and opens nothing itself.
475
477
  - `TileStore.getTile` resolves an archive, reads a tile through the local file
476
478
  or the swarm, and knows its format from the header rather than by guessing.
477
479
  - The tile route already gzips vector tiles through `node:zlib`, abandons a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmtiles-swarm",
3
- "version": "0.98.0",
3
+ "version": "0.98.1",
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/api.js CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  } from './sources.js';
39
39
  import { limitFor, remaining } from './seeding.js';
40
40
  import { buildTileJson, extensionMatches, tileExtension } from './tilejson.js';
41
- import { SUMMARY_VERSION } from './pmtiles-probe.js';
41
+ import { SUMMARY_VERSION } from './archive-summary.js';
42
42
  import { TileReadError } from './tiles.js';
43
43
  import { loadCodec } from './codec.js';
44
44
  import { answerStackTile, outputFormat, outputSize } from './stack-tile.js';
@@ -0,0 +1,183 @@
1
+ /**
2
+ * The summary this project keeps about a map archive, and the vocabulary for
3
+ * building one.
4
+ *
5
+ * What a subscriber needs in order to decide whether they want an archive:
6
+ * coverage, zoom range, tile type, size. This is what makes a map RSS feed more
7
+ * useful than a generic torrent feed — an item can say "raster webp, z0-14,
8
+ * covering Switzerland, 36 GiB" instead of just a filename, so a consumer can
9
+ * filter before committing to a download.
10
+ *
11
+ * Here rather than beside either reader because both formats produce one, and
12
+ * neither should have to import the other to do it. Nothing in this file opens
13
+ * a file or a socket: it takes a header and a metadata document and returns the
14
+ * summary, which is why it can be shared without dragging one format's
15
+ * dependencies into the other's module.
16
+ */
17
+
18
+ /**
19
+ * Maps tile type numbers onto names and content types.
20
+ *
21
+ * The numbering is PMTiles', because a number was needed and that one is
22
+ * written down. An MBTiles archive names its format in a metadata row instead,
23
+ * and `tileTypeFor` is how it says the same thing in the same terms.
24
+ */
25
+ const TILE_TYPES = {
26
+ 0: { format: 'unknown', contentType: 'application/octet-stream' },
27
+ 1: { format: 'pbf', contentType: 'application/x-protobuf' },
28
+ 2: { format: 'png', contentType: 'image/png' },
29
+ 3: { format: 'jpeg', contentType: 'image/jpeg' },
30
+ 4: { format: 'webp', contentType: 'image/webp' },
31
+ 5: { format: 'avif', contentType: 'image/avif' },
32
+ // No MIME is registered for MLT, and nothing selects a tile by content type.
33
+ 6: { format: 'mlt', contentType: 'application/octet-stream' },
34
+ };
35
+
36
+ /**
37
+ * The tile type number for a format an archive names in words.
38
+ *
39
+ * The inverse of the table above, kept beside it so the two halves cannot
40
+ * drift. `mvt` and `jpg` are spellings the same formats appear under in the
41
+ * wild; anything unrecognised is 0, which reads back as "unknown" rather than
42
+ * as a format nothing can serve.
43
+ * @param {string} [format] - What the archive calls its tiles.
44
+ * @returns {number} - The tile type number.
45
+ */
46
+ export function tileTypeFor(format) {
47
+ const named = String(format ?? '').toLowerCase();
48
+ const found = Object.entries(TILE_TYPES).find(
49
+ ([, type]) => type.format === named,
50
+ );
51
+ if (found) return Number(found[0]);
52
+ return { mvt: 1, jpg: 3 }[named] ?? 0;
53
+ }
54
+
55
+ /**
56
+ * Summary of an archive, as stored in the catalog and published in the feed.
57
+ * @typedef {object} ArchiveSummary
58
+ * @property {number} specVersion - PMTiles spec version.
59
+ * @property {string} format - Tile format: pbf, png, jpeg, webp, avif.
60
+ * @property {string} contentType - Matching content type.
61
+ * @property {number} minZoom - Lowest zoom present.
62
+ * @property {number} maxZoom - Highest zoom present.
63
+ * @property {number[]} bounds - [minLon, minLat, maxLon, maxLat].
64
+ * @property {number[]} center - [lon, lat, zoom].
65
+ * @property {number} tileCount - Addressed tile count.
66
+ * @property {boolean} clustered - Whether tiles are stored in Hilbert order.
67
+ * @property {string} [name] - Name from the archive metadata.
68
+ * @property {string} [description] - Description from the archive metadata.
69
+ * @property {string} [attribution] - Attribution from the archive metadata.
70
+ * @property {object[]} [vectorLayers] - Vector layer definitions, for pbf archives.
71
+ * @property {boolean} [sparse] - What the archive says about missing tiles, if it says anything.
72
+ */
73
+
74
+ /**
75
+ * Reads a flag out of archive metadata, which is not reliably typed.
76
+ *
77
+ * A PMTiles JSON blob carries a real boolean, but the same metadata routinely
78
+ * arrives having been round-tripped through MBTiles, where every value is TEXT
79
+ * — so the honest reading of `"false"` is false, not "a non-empty string".
80
+ * @param {unknown} value - Whatever the metadata held.
81
+ * @returns {boolean | undefined} - The flag, or undefined if it said nothing.
82
+ */
83
+ export function metadataFlag(value) {
84
+ if (value === undefined || value === null || value === '') return undefined;
85
+ if (typeof value === 'boolean') return value;
86
+ if (typeof value === 'number') return value !== 0;
87
+ const text = String(value).trim().toLowerCase();
88
+ if (['true', '1', 'yes'].includes(text)) return true;
89
+ if (['false', '0', 'no'].includes(text)) return false;
90
+ return undefined;
91
+ }
92
+
93
+ /**
94
+ * The tile encoding an archive declares, if it is one MapLibre understands.
95
+ *
96
+ * See docs/tilejson.md — `encoding`.
97
+ * @param {unknown} value - Whatever the metadata held.
98
+ * @returns {string | undefined} - A known encoding, or undefined.
99
+ */
100
+ export function metadataEncoding(value) {
101
+ if (typeof value !== 'string') return undefined;
102
+ const text = value.trim().toLowerCase();
103
+ // An unrecognised value is dropped: it would cost a client its own default.
104
+ return ['terrarium', 'mapbox', 'custom', 'mlt'].includes(text)
105
+ ? text
106
+ : undefined;
107
+ }
108
+
109
+ /**
110
+ * The four factors a `custom` encoding is unreadable without. All or nothing.
111
+ * @param {object} metadata - The archive's metadata.
112
+ * @returns {object | undefined} - The four factors, or undefined.
113
+ */
114
+ export function customEncodingFactors(metadata) {
115
+ const named = ['redFactor', 'greenFactor', 'blueFactor', 'baseShift'];
116
+ const out = {};
117
+ for (const name of named) {
118
+ const value = Number(metadata[name]);
119
+ if (!Number.isFinite(value)) return undefined;
120
+ out[name] = value;
121
+ }
122
+ return out;
123
+ }
124
+
125
+ /**
126
+ * What this prober reads. Raise it whenever a field is added to the summary,
127
+ * and archives probed by an older build are re-read once. See
128
+ * docs/internals.md — "Re-reading a summary an older prober wrote".
129
+ */
130
+ export const SUMMARY_VERSION = 3;
131
+
132
+ /**
133
+ * What the catalog keeps about an archive, from its header and metadata.
134
+ * @param {object} header - A parsed PMTiles v3 header.
135
+ * @param {object} [metadata] - The archive's own metadata document.
136
+ * @returns {object} - The summary, at `SUMMARY_VERSION`.
137
+ */
138
+ export function summarize(header, metadata = {}) {
139
+ const type = TILE_TYPES[header.tileType] ?? TILE_TYPES[0];
140
+
141
+ // An archive with no bounds set reports all zeroes; treat that as global
142
+ // rather than as a point at null island.
143
+ const hasBounds = !(
144
+ header.minLon === 0 &&
145
+ header.minLat === 0 &&
146
+ header.maxLon === 0 &&
147
+ header.maxLat === 0
148
+ );
149
+
150
+ return {
151
+ summaryVersion: SUMMARY_VERSION,
152
+ specVersion: header.specVersion,
153
+ format: type.format,
154
+ contentType: type.contentType,
155
+ minZoom: header.minZoom,
156
+ maxZoom: header.maxZoom,
157
+ bounds: hasBounds
158
+ ? [header.minLon, header.minLat, header.maxLon, header.maxLat]
159
+ : [-180, -85.051129, 180, 85.051129],
160
+ center: [
161
+ header.centerLon,
162
+ header.centerLat,
163
+ header.centerZoom || Math.round(header.maxZoom / 2),
164
+ ],
165
+ tileCount: header.numAddressedTiles,
166
+ clustered: header.clustered,
167
+ name: metadata.name,
168
+ description: metadata.description,
169
+ attribution: metadata.attribution,
170
+ vectorLayers: metadata.vector_layers,
171
+ // What the archive says about its own missing tiles. tileserver-gl reads
172
+ // the same key, so an archive built to be served there carries the answer
173
+ // with it and does not have to be configured again here.
174
+ sparse: metadataFlag(metadata.sparse),
175
+ // The header settles MLT; only the metadata can settle elevation packing.
176
+ encoding:
177
+ type.format === 'mlt' ? 'mlt' : metadataEncoding(metadata.encoding),
178
+ encodingFactors:
179
+ metadataEncoding(metadata.encoding) === 'custom'
180
+ ? customEncodingFactors(metadata)
181
+ : undefined,
182
+ };
183
+ }
package/src/feed.js CHANGED
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import { mutableMagnet } from './mutable.js';
21
- import { metadataEncoding } from './pmtiles-probe.js';
21
+ import { metadataEncoding } from './archive-summary.js';
22
22
 
23
23
  const PMTILES_NS = 'https://github.com/TechIdiots-LLC/pmtiles-swarm/ns/1.0';
24
24
 
package/src/mbtiles.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { summarize } from './pmtiles-probe.js';
2
+ import { summarize, tileTypeFor } from './archive-summary.js';
3
3
 
4
4
  /**
5
5
  * Reading tiles out of a completed MBTiles archive.
@@ -21,17 +21,6 @@ import { summarize } from './pmtiles-probe.js';
21
21
  * MBTiles 1.3: https://github.com/mapbox/mbtiles-spec/blob/master/1.3/spec.md
22
22
  */
23
23
 
24
- /** Maps an MBTiles `format` onto the PMTiles tile-type number summarize expects. */
25
- const TILE_TYPES = {
26
- pbf: 1,
27
- mvt: 1,
28
- png: 2,
29
- jpg: 3,
30
- jpeg: 3,
31
- webp: 4,
32
- avif: 5,
33
- };
34
-
35
24
  /** The whole world, for an archive that declares no bounds. */
36
25
  const WHOLE_WORLD = [-180, -85.051129, 180, 85.051129];
37
26
 
@@ -166,7 +155,7 @@ export class MbtilesArchive {
166
155
  return {
167
156
  // MBTiles has no equivalent, and nothing reads this except to report it.
168
157
  specVersion: 3,
169
- tileType: TILE_TYPES[format] ?? 0,
158
+ tileType: tileTypeFor(format),
170
159
  minZoom,
171
160
  maxZoom,
172
161
  minLon: bounds[0],
@@ -1,97 +1,15 @@
1
1
  import { FetchSource, PMTiles } from 'pmtiles';
2
+ import { summarize } from './archive-summary.js';
2
3
  import { NodeFileSource } from './file-source.js';
3
4
 
4
5
  /**
5
- * Reads the facts about an archive that a subscriber needs in order to decide
6
- * whether they want it: coverage, zoom range, tile type, size.
6
+ * Reading a PMTiles archive's summary, from a local path or over HTTP.
7
7
  *
8
- * This is what makes a map RSS feed more useful than a generic torrent feed —
9
- * an item can say "raster webp, z0-14, covering Switzerland, 36 GiB" instead of
10
- * just a filename, so a consumer can filter before committing to a download.
8
+ * The summary itself is built in archive-summary.js, which knows nothing about
9
+ * either format. What is here is the half that is PMTiles: opening one, and
10
+ * where its header and metadata are kept.
11
11
  */
12
12
 
13
- /** Maps PMTiles tile type numbers onto names and content types. */
14
- const TILE_TYPES = {
15
- 0: { format: 'unknown', contentType: 'application/octet-stream' },
16
- 1: { format: 'pbf', contentType: 'application/x-protobuf' },
17
- 2: { format: 'png', contentType: 'image/png' },
18
- 3: { format: 'jpeg', contentType: 'image/jpeg' },
19
- 4: { format: 'webp', contentType: 'image/webp' },
20
- 5: { format: 'avif', contentType: 'image/avif' },
21
- // No MIME is registered for MLT, and nothing selects a tile by content type.
22
- 6: { format: 'mlt', contentType: 'application/octet-stream' },
23
- };
24
-
25
- /**
26
- * Summary of an archive, as stored in the catalog and published in the feed.
27
- * @typedef {object} PMTilesSummary
28
- * @property {number} specVersion - PMTiles spec version.
29
- * @property {string} format - Tile format: pbf, png, jpeg, webp, avif.
30
- * @property {string} contentType - Matching content type.
31
- * @property {number} minZoom - Lowest zoom present.
32
- * @property {number} maxZoom - Highest zoom present.
33
- * @property {number[]} bounds - [minLon, minLat, maxLon, maxLat].
34
- * @property {number[]} center - [lon, lat, zoom].
35
- * @property {number} tileCount - Addressed tile count.
36
- * @property {boolean} clustered - Whether tiles are stored in Hilbert order.
37
- * @property {string} [name] - Name from the archive metadata.
38
- * @property {string} [description] - Description from the archive metadata.
39
- * @property {string} [attribution] - Attribution from the archive metadata.
40
- * @property {object[]} [vectorLayers] - Vector layer definitions, for pbf archives.
41
- * @property {boolean} [sparse] - What the archive says about missing tiles, if it says anything.
42
- */
43
-
44
- /**
45
- * Reads a flag out of archive metadata, which is not reliably typed.
46
- *
47
- * A PMTiles JSON blob carries a real boolean, but the same metadata routinely
48
- * arrives having been round-tripped through MBTiles, where every value is TEXT
49
- * — so the honest reading of `"false"` is false, not "a non-empty string".
50
- * @param {unknown} value - Whatever the metadata held.
51
- * @returns {boolean | undefined} - The flag, or undefined if it said nothing.
52
- */
53
- export function metadataFlag(value) {
54
- if (value === undefined || value === null || value === '') return undefined;
55
- if (typeof value === 'boolean') return value;
56
- if (typeof value === 'number') return value !== 0;
57
- const text = String(value).trim().toLowerCase();
58
- if (['true', '1', 'yes'].includes(text)) return true;
59
- if (['false', '0', 'no'].includes(text)) return false;
60
- return undefined;
61
- }
62
-
63
- /**
64
- * The tile encoding an archive declares, if it is one MapLibre understands.
65
- *
66
- * See docs/tilejson.md — `encoding`.
67
- * @param {unknown} value - Whatever the metadata held.
68
- * @returns {string | undefined} - A known encoding, or undefined.
69
- */
70
- export function metadataEncoding(value) {
71
- if (typeof value !== 'string') return undefined;
72
- const text = value.trim().toLowerCase();
73
- // An unrecognised value is dropped: it would cost a client its own default.
74
- return ['terrarium', 'mapbox', 'custom', 'mlt'].includes(text)
75
- ? text
76
- : undefined;
77
- }
78
-
79
- /**
80
- * The four factors a `custom` encoding is unreadable without. All or nothing.
81
- * @param {object} metadata - The archive's metadata.
82
- * @returns {object | undefined} - The four factors, or undefined.
83
- */
84
- export function customEncodingFactors(metadata) {
85
- const named = ['redFactor', 'greenFactor', 'blueFactor', 'baseShift'];
86
- const out = {};
87
- for (const name of named) {
88
- const value = Number(metadata[name]);
89
- if (!Number.isFinite(value)) return undefined;
90
- out[name] = value;
91
- }
92
- return out;
93
- }
94
-
95
13
  /**
96
14
  * Reads an archive's header and metadata.
97
15
  *
@@ -99,72 +17,7 @@ export function customEncodingFactors(metadata) {
99
17
  * even for a multi-terabyte archive — and it works against an HTTP URL without
100
18
  * downloading it.
101
19
  * @param {string} location - Local path or http(s) URL.
102
- * @returns {Promise<PMTilesSummary>} - The summary.
103
- */
104
- /**
105
- * What this prober reads. Raise it whenever a field is added to the summary,
106
- * and archives probed by an older build are re-read once. See
107
- * docs/internals.md — "Re-reading a summary an older prober wrote".
108
- */
109
- export const SUMMARY_VERSION = 3;
110
-
111
- /**
112
- * What the catalog keeps about an archive, from its header and metadata.
113
- * @param {object} header - A parsed PMTiles v3 header.
114
- * @param {object} [metadata] - The archive's own metadata document.
115
- * @returns {object} - The summary, at `SUMMARY_VERSION`.
116
- */
117
- export function summarize(header, metadata = {}) {
118
- const type = TILE_TYPES[header.tileType] ?? TILE_TYPES[0];
119
-
120
- // An archive with no bounds set reports all zeroes; treat that as global
121
- // rather than as a point at null island.
122
- const hasBounds = !(
123
- header.minLon === 0 &&
124
- header.minLat === 0 &&
125
- header.maxLon === 0 &&
126
- header.maxLat === 0
127
- );
128
-
129
- return {
130
- summaryVersion: SUMMARY_VERSION,
131
- specVersion: header.specVersion,
132
- format: type.format,
133
- contentType: type.contentType,
134
- minZoom: header.minZoom,
135
- maxZoom: header.maxZoom,
136
- bounds: hasBounds
137
- ? [header.minLon, header.minLat, header.maxLon, header.maxLat]
138
- : [-180, -85.051129, 180, 85.051129],
139
- center: [
140
- header.centerLon,
141
- header.centerLat,
142
- header.centerZoom || Math.round(header.maxZoom / 2),
143
- ],
144
- tileCount: header.numAddressedTiles,
145
- clustered: header.clustered,
146
- name: metadata.name,
147
- description: metadata.description,
148
- attribution: metadata.attribution,
149
- vectorLayers: metadata.vector_layers,
150
- // What the archive says about its own missing tiles. tileserver-gl reads
151
- // the same key, so an archive built to be served there carries the answer
152
- // with it and does not have to be configured again here.
153
- sparse: metadataFlag(metadata.sparse),
154
- // The header settles MLT; only the metadata can settle elevation packing.
155
- encoding:
156
- type.format === 'mlt' ? 'mlt' : metadataEncoding(metadata.encoding),
157
- encodingFactors:
158
- metadataEncoding(metadata.encoding) === 'custom'
159
- ? customEncodingFactors(metadata)
160
- : undefined,
161
- };
162
- }
163
-
164
- /**
165
- * Reads an archive's header and metadata.
166
- * @param {string} location - Local path or http(s) URL.
167
- * @returns {Promise<PMTilesSummary>} - The summary.
20
+ * @returns {Promise<ArchiveSummary>} - The summary.
168
21
  */
169
22
  export async function probePMTiles(location) {
170
23
  const isHttp = /^https?:\/\//i.test(location);
package/src/tiles.js CHANGED
@@ -7,7 +7,7 @@ import { TorrentSource } from 'pmtiles-torrent';
7
7
  import { NodeFileSource } from './file-source.js';
8
8
  import { onDiskPath } from './incomplete.js';
9
9
  import { MbtilesArchive, isMbtiles } from './mbtiles.js';
10
- import { summarize } from './pmtiles-probe.js';
10
+ import { summarize } from './archive-summary.js';
11
11
  import { LibtorrentReadEngine } from './read-engine.js';
12
12
 
13
13
  /**