maplibre-tilepackage-protocol 1.2.1 → 1.2.2

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/package.json CHANGED
@@ -1,43 +1,42 @@
1
- {
2
- "name": "maplibre-tilepackage-protocol",
3
- "version": "1.2.1",
4
- "description": "Maplibre-gl-js protocol for reading VTPK and TPKX files",
5
- "homepage": "https://github.com/spatialillusions/maplibre-tilepackage-protocol#readme",
6
- "bugs": {
7
- "url": "https://github.com/spatialillusions/maplibre-tilepackage-protocol/issues"
8
- },
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/spatialillusions/maplibre-tilepackage-protocol.git"
12
- },
13
- "license": "MIT",
14
- "author": "Måns Beckman",
15
- "type": "module",
16
- "main": "src/index.js",
17
- "scripts": {
18
- "test": "echo \"Error: no test specified\" && exit 1",
19
- "build": "rollup -c",
20
- "build:esm": "rollup -c --configPlugin @rollup/plugin-node-resolve",
21
- "demo": "npm run build"
22
- },
23
- "dependencies": {
24
- "@mapbox/point-geometry": "^0.1.0",
25
- "@mapbox/vector-tile": "^1.3.1",
26
- "@maplibre/vt-pbf": "^4.1.0",
27
- "fflate": "^0.8.2",
28
- "maplibre-local-glyphs": "^1.2.0",
29
- "pbf": "^3.2.1"
30
- },
31
- "devDependencies": {
32
- "@rollup/plugin-commonjs": "^26.0.3",
33
- "@rollup/plugin-node-resolve": "^16.0.3",
34
- "rollup": "^4.53.3"
35
- },
36
- "module": "dist/index.esm.js",
37
- "exports": {
38
- ".": {
39
- "import": "./dist/index.esm.js",
40
- "require": "./src/index.js"
41
- }
42
- }
43
- }
1
+ {
2
+ "name": "maplibre-tilepackage-protocol",
3
+ "version": "1.2.2",
4
+ "description": "Maplibre-gl-js protocol for reading VTPK and TPKX files",
5
+ "homepage": "https://github.com/spatialillusions/maplibre-tilepackage-protocol#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/spatialillusions/maplibre-tilepackage-protocol/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/spatialillusions/maplibre-tilepackage-protocol.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Måns Beckman",
15
+ "type": "module",
16
+ "main": "src/index.js",
17
+ "scripts": {
18
+ "test": "echo \"Error: no test specified\" && exit 1",
19
+ "build": "rollup -c",
20
+ "build:esm": "rollup -c --configPlugin @rollup/plugin-node-resolve",
21
+ "demo": "npm run build"
22
+ },
23
+ "dependencies": {
24
+ "@mapbox/point-geometry": "^0.1.0",
25
+ "@mapbox/vector-tile": "^1.3.1",
26
+ "@maplibre/vt-pbf": "^4.1.0",
27
+ "fflate": "^0.8.3",
28
+ "pbf": "^3.2.1"
29
+ },
30
+ "devDependencies": {
31
+ "@rollup/plugin-commonjs": "^26.0.3",
32
+ "@rollup/plugin-node-resolve": "^16.0.3",
33
+ "rollup": "^4.59.0"
34
+ },
35
+ "module": "dist/index.esm.js",
36
+ "exports": {
37
+ ".": {
38
+ "import": "./dist/index.esm.js",
39
+ "require": "./src/index.js"
40
+ }
41
+ }
42
+ }
package/rollup.config.mjs CHANGED
@@ -8,7 +8,6 @@ const externals = [
8
8
  "pbf",
9
9
  "@mapbox/point-geometry",
10
10
  "@maplibre/vt-pbf",
11
- "maplibre-local-glyphs",
12
11
  ];
13
12
 
14
13
  function onwarn(warning) {
@@ -12,23 +12,16 @@ export default async function defaultDecompress(buf, compression) {
12
12
  return buf;
13
13
  }
14
14
  if (compression === "gzip") {
15
- // Safe global detection for DecompressionStream feature test
16
- const g =
17
- typeof globalThis !== "undefined"
18
- ? globalThis
19
- : typeof window !== "undefined"
20
- ? window
21
- : typeof self !== "undefined"
22
- ? self
23
- : {};
24
- if (typeof g.DecompressionStream === "undefined") {
15
+ if (typeof globalThis.DecompressionStream === "undefined") {
25
16
  return fflate.decompressSync(new Uint8Array(buf));
26
17
  }
27
18
  const stream = new Response(buf).body;
28
19
  if (!stream) {
29
20
  throw new Error("Failed to read response stream");
30
21
  }
31
- const result = stream.pipeThrough(new g.DecompressionStream("gzip"));
22
+ const result = stream.pipeThrough(
23
+ new globalThis.DecompressionStream("gzip"),
24
+ );
32
25
  return new Response(result).arrayBuffer();
33
26
  }
34
27
  throw new Error("Compression method not supported");
@@ -1,4 +1,5 @@
1
- import getJsonFromFile from "./get-json-from-file.js";
1
+ import getJsonFromFile from "./zip/get-json-from-file.js";
2
+ import getFilelistFromTilePackage from "./zip/get-filelist.js";
2
3
 
3
4
  function xmlToJson(xmlString) {
4
5
  const parser = new DOMParser();
@@ -71,132 +72,9 @@ export function jsonToHeader(json, files, etag) {
71
72
  */
72
73
  export default async function getHeaderAndFileList(source) {
73
74
  const key = source.getKey();
74
- const fileSize = await source.getSize();
75
- const resp = await source.getBytes(fileSize - 98, 98);
76
- let v = new DataView(resp.data, 0, 98);
77
- let entriesCentralDirectory, sizeCentralDirectory, offsetCentralDirectory;
78
- //let bigZip64 = false;
79
- if (v.getUint32(0, true) === 0x06064b50) {
80
- // This is a ZIP64 tilepackage
81
- //bigZip64 = true;
82
- entriesCentralDirectory = Number(v.getBigUint64(32, true));
83
- sizeCentralDirectory = Number(v.getBigUint64(40, true));
84
- offsetCentralDirectory = Number(v.getBigUint64(48, true));
85
- } else {
86
- v = new DataView(resp.data, 98 - 22, 22);
87
- if (v.getUint32(0, true) === 0x06054b50) {
88
- // This is a ordinary zip archive
89
- entriesCentralDirectory = Number(v.getUint16(10, true));
90
- sizeCentralDirectory = Number(v.getUint32(12, true));
91
- offsetCentralDirectory = Number(v.getUint32(16, true));
92
- } else {
93
- throw new Error("Wrong magic number for Zip archive");
94
- }
95
- }
96
-
97
- const centralDirectory = await source.getBytes(
98
- offsetCentralDirectory,
99
- sizeCentralDirectory,
100
- );
101
-
102
- v = new DataView(centralDirectory.data, 0, sizeCentralDirectory);
103
- if (v.getUint32(0, true) !== 0x02014b50) {
104
- throw new Error("Wrong magic number for Central Directory archive");
105
- }
106
-
107
- let entryStart = 0;
108
- const tilePackageFiles = {};
109
- for (let i = 0; i < entriesCentralDirectory; i++) {
110
- if (entryStart >= sizeCentralDirectory) break;
111
- /*
112
- central file header signature 4 bytes (0x02014b50)
113
- version made by 2 bytes
114
- version needed to extract 2 bytes
115
- general purpose bit flag 2 bytes
116
- compression method 2 bytes
117
- last mod file time 2 bytes
118
- last mod file date 2 bytes
119
- crc-32 4 bytes
120
- compressed size 4 bytes
121
- uncompressed size 4 bytes
122
- file name length 2 bytes
123
- extra field length 2 bytes
124
- file comment length 2 bytes
125
- disk number start 2 bytes
126
- internal file attributes 2 bytes
127
- external file attributes 4 bytes
128
- relative offset of local header 4 bytes
129
-
130
- file name (variable size)
131
- extra field (variable size)
132
- file comment (variable size)
133
- */
134
- let sizeFile = v.getUint32(entryStart + 20, true);
135
-
136
- const sizeFileName = v.getUint16(entryStart + 28, true);
137
-
138
- const sizeExtraField = v.getUint16(entryStart + 30, true);
139
-
140
- const sizeComment = v.getUint16(entryStart + 32, true);
141
-
142
- let relativeOffset = v.getUint32(entryStart + 42, true);
143
-
144
- const vFilename = new DataView(
145
- centralDirectory.data,
146
- entryStart + 46,
147
- sizeFileName,
148
- );
149
- const decoder = new TextDecoder("utf-8");
150
- const filename = decoder.decode(vFilename);
151
-
152
- if (
153
- (sizeFile == 0xffffffff || relativeOffset == 0xffffffff) &&
154
- sizeExtraField > 0
155
- ) {
156
- const vExtended = new DataView(
157
- centralDirectory.data,
158
- entryStart + 46 + sizeFileName,
159
- 32,
160
- );
161
- if (vExtended.getUint16(0, true) == 0x0001) {
162
- /*
163
- Value Size Description
164
- ----- ---- -----------
165
- (ZIP64) 0x0001 2 bytes Tag for this "extra" block type
166
- Size 2 bytes Size of this "extra" block
167
- Original
168
- Size 8 bytes Original uncompressed file size
169
- Compressed
170
- Size 8 bytes Size of compressed data
171
- Relative Header
172
- Offset 8 bytes Offset of local header record
173
- Disk Start
174
- Number 4 bytes Number of the disk on which
175
- this file starts
176
- */
177
-
178
- let j = 0;
179
- if (sizeFile == 0xffffffff) {
180
- sizeFile = Number(vExtended.getBigUint64(4, true));
181
- j += 8;
182
- }
183
- if (relativeOffset == 0xffffffff) {
184
- relativeOffset = Number(vExtended.getBigUint64(4 + j, true));
185
- }
186
- }
187
- }
188
-
189
- tilePackageFiles[filename] = {
190
- filename: filename,
191
- size: sizeFile,
192
- relativeOffset: relativeOffset,
193
- absoluteOffset: relativeOffset + 30 + filename.length,
194
- };
195
- entryStart += 46 + sizeFileName + sizeExtraField + sizeComment;
196
- }
197
-
75
+ const { files: tilePackageFiles, etag } =
76
+ await getFilelistFromTilePackage(source);
198
77
  let root = {};
199
-
200
78
  if (key.indexOf(".tpkx") !== -1) {
201
79
  // TPKX
202
80
  root = await getJsonFromFile("root.json", tilePackageFiles, source);
@@ -278,11 +156,9 @@ export default async function getHeaderAndFileList(source) {
278
156
  coverageMap[0][0][0] = "blob";
279
157
  calculateCoverage(0, 0, 0, tilemap.index, coverageMap);
280
158
  root.coverageMap = coverageMap;
281
- } else {
282
- // Coverage map unavailable (either disabled or missing tilemap/root.json)
283
159
  }
284
160
  }
285
161
 
286
- const header = jsonToHeader(root, tilePackageFiles, resp.etag);
162
+ const header = jsonToHeader(root, tilePackageFiles, etag);
287
163
  return [header, ""];
288
164
  }
@@ -1,12 +1 @@
1
- export default async function getJsonFromFile(file, tilePackageFiles, source) {
2
- const decoder = new TextDecoder("utf-8");
3
- if (tilePackageFiles[file]) {
4
- const fileOffset = tilePackageFiles[file].absoluteOffset;
5
- const fileJSON = await source.getBytes(
6
- fileOffset,
7
- tilePackageFiles[file].size,
8
- );
9
- return JSON.parse(decoder.decode(fileJSON.data));
10
- }
11
- return {};
12
- }
1
+ export { default } from "./zip/get-json-from-file.js";
package/src/index.js CHANGED
@@ -3,26 +3,21 @@ export * from "./source.js";
3
3
  import { FetchSource } from "./source.js";
4
4
  import subdivideVectorTile from "./tilecutter/subdivide.js";
5
5
  import defaultDecompress from "./default-decompress.js";
6
- import getJsonFromFile from "./get-json-from-file.js";
6
+ import getJsonFromFile from "./zip/get-json-from-file.js";
7
7
  import SharedPromiseCache from "./shared-promise-cache.js";
8
8
 
9
- /**
10
- * Escape literal newlines in JSON text.
11
- * Replaces actual newline characters with the JSON escape sequence.
12
- */
13
- function escapeJsonNewlines(text) {
14
- // Replace literal newlines with JSON escape sequences
15
- // This handles \r\n, \n, and \r
16
- return text.replace(/\r\n/g, "\\n").replace(/[\r\n]/g, "\\n");
17
- }
18
-
19
9
  function calculateFilename(z, x, y, header) {
20
10
  const zoom = z.toString().padStart(2, "0");
21
11
  const baseRow = Math.floor(y / 128) * 128;
22
12
  const baseCol = Math.floor(x / 128) * 128;
23
13
  const rowHex = baseRow.toString(16).padStart(4, "0");
24
14
  const colHex = baseCol.toString(16).padStart(4, "0");
25
- const basePath = header.type === "tpkx" ? "tile" : "p12/tile";
15
+ let basePath;
16
+ if (header.type === "tpkx") {
17
+ basePath = `tile`;
18
+ } else {
19
+ basePath = `p12/tile`;
20
+ }
26
21
  return `${basePath}/L${zoom}/R${rowHex}C${colHex}.bundle`;
27
22
  }
28
23
 
@@ -32,6 +27,10 @@ function getTileInfoFromTileIndex(tileIndex, z, x, y) {
32
27
  return tileIndex[row][col];
33
28
  }
34
29
 
30
+ /**
31
+ * Error thrown when a response for TilePackage over HTTP does not match previous, cached parts of the archive.
32
+ * The default TilePackage implementation will catch this error once internally and retry a request.
33
+ */
35
34
  class EtagMismatch extends Error {
36
35
  constructor(message) {
37
36
  super(message);
@@ -44,25 +43,23 @@ export class TilePackage {
44
43
  // Default coverageCheck enabled; disable with coverageCheck:false
45
44
  this.coverageCheck = 1;
46
45
  if (options && options.coverageCheck === false) this.coverageCheck = 0;
47
- // Max dz guard (skip subdivision beyond this): default 8
48
- this.maxDz =
49
- options && typeof options.maxDz === "number" ? options.maxDz : 8;
50
46
  if (typeof source === "string") {
51
- const opts = this.coverageCheck
52
- ? { coverageCheck: this.coverageCheck }
53
- : undefined;
54
- this.source = new FetchSource(source, opts);
47
+ this.source = new FetchSource(source, this.coverageCheck);
55
48
  } else {
56
49
  this.source = source;
57
- // Propagate default coverageCheck to existing Source instances if undefined
58
- if (this.source && this.source.coverageCheck === undefined) {
59
- this.source.coverageCheck = this.coverageCheck;
60
- }
61
50
  }
62
- this.decompress =
63
- options && options.decompress ? options.decompress : defaultDecompress;
64
- this.cache =
65
- options && options.cache ? options.cache : new SharedPromiseCache();
51
+
52
+ if (options && options.decompress) {
53
+ this.decompress = options.decompress;
54
+ } else {
55
+ this.decompress = defaultDecompress;
56
+ }
57
+
58
+ if (options && options.cache) {
59
+ this.cache = options.cache;
60
+ } else {
61
+ this.cache = new SharedPromiseCache();
62
+ }
66
63
  }
67
64
 
68
65
  async getHeader() {
@@ -179,21 +176,21 @@ export class TilePackage {
179
176
 
180
177
  async getMetadataAttempt() {
181
178
  const header = await this.cache.getHeader(this.source);
179
+
182
180
  let metadata = {};
183
181
  if (header.packageType === "vtpk") {
184
182
  const resp = await this.source.getBytes(
185
- header.metadataOffset,
186
- header.metadataLength,
183
+ header.jsonMetadataOffset,
184
+ header.jsonMetadataLength,
187
185
  undefined,
188
186
  header.etag,
189
187
  );
188
+
190
189
  const decoder = new TextDecoder("utf-8");
191
- const jsonText = decoder.decode(resp.data);
192
- console.log("test", jsonText);
193
- console.log("escaped text", escapeJsonNewlines(jsonText));
194
- metadata = JSON.parse(escapeJsonNewlines(jsonText));
190
+ metadata = JSON.parse(decoder.decode(resp.data));
195
191
  }
196
192
  metadata.name = header.name;
193
+
197
194
  return metadata;
198
195
  }
199
196
 
@@ -211,7 +208,10 @@ export class TilePackage {
211
208
 
212
209
  async getResourceAttempt(file, signal) {
213
210
  const header = await this.cache.getHeader(this.source);
214
- if (!header.files[file]) return undefined;
211
+ if (!header.files[file]) {
212
+ return undefined;
213
+ }
214
+
215
215
  const resource = await this.cache.getResource(
216
216
  this.source,
217
217
  file,
@@ -251,49 +251,36 @@ export class TilePackage {
251
251
  header.files,
252
252
  this.source,
253
253
  );
254
- if (style.sources && style.sources.esri) {
255
- delete style.sources.esri.url;
256
- style.sources.esri.tiles = [`tilepackage://${sourceKey}/{z}/{x}/{y}`];
257
- style.sources.esri.type = style.sources.esri.type || "vector";
258
- style.sources.esri.minzoom = header.minZoom || 0;
259
- style.sources.esri.maxzoom = header.maxZoom || 22;
260
- }
261
- for (const k in style.sources) {
262
- const src = style.sources[k];
263
- if (
264
- src &&
265
- typeof src.url === "string" &&
266
- src.url.startsWith("tilepackage://")
267
- ) {
268
- delete src.url;
269
- if (!src.tiles)
270
- src.tiles = [`tilepackage://${sourceKey}/{z}/{x}/{y}`];
271
- }
272
- }
273
- if (metadata.copyrightText)
254
+ style.sources.esri.url = `tilepackage://${sourceKey}`;
255
+ style.sources.esri.minzoom = 0; //header.maxZoom || 22;
256
+ style.sources.esri.maxzoom = header.maxZoom || 22;
257
+ if (metadata.copyrightText) {
274
258
  style.sources.esri.attribution = metadata.copyrightText;
259
+ }
275
260
  style.glyphs = `tilepackage://${sourceKey}/{fontstack}/{range}`;
276
261
  style.sprite = `tilepackage://${sourceKey}/sprite`;
277
- if (this.debug)
278
- console.debug(
279
- "[tilepackage style] rewritten esri source",
280
- style.sources.esri,
281
- );
282
262
  return style;
283
- }
284
- return {
285
- version: 8,
286
- sources: {
287
- esri: {
288
- type: "raster",
289
- tileSize: header.tileSize || 256,
290
- tiles: [`tilepackage://${sourceKey}/{z}/{x}/{y}`],
291
- maxzoom: header.maxZoom || 22,
292
- minzoom: header.minZoom || 0,
263
+ } else {
264
+ const style = {
265
+ version: 8,
266
+ sources: {
267
+ esri: {
268
+ type: "raster",
269
+ tileSize: header.tileSize || 256,
270
+ url: `tilepackage://${sourceKey}`,
271
+ maxzoom: header.maxZoom || 22,
272
+ },
293
273
  },
294
- },
295
- layers: [{ id: "tilepackageraster", type: "raster", source: "esri" }],
296
- };
274
+ layers: [
275
+ {
276
+ id: "tilepackageraster",
277
+ type: "raster",
278
+ source: "esri",
279
+ },
280
+ ],
281
+ };
282
+ return style;
283
+ }
297
284
  }
298
285
 
299
286
  async getStyle() {
@@ -312,6 +299,7 @@ export class TilePackage {
312
299
  const header = await this.getHeader();
313
300
  const metadata = await this.getMetadata();
314
301
  const ext = header.tileType;
302
+
315
303
  const tileJson = {
316
304
  tilejson: "3.0.0",
317
305
  scheme: "xyz",
@@ -320,11 +308,21 @@ export class TilePackage {
320
308
  version: header.version,
321
309
  bounds: [header.minLon, header.minLat, header.maxLon, header.maxLat],
322
310
  };
323
- if (metadata.vector_layers) tileJson.vector_layers = metadata.vector_layers;
324
- if (metadata.attribution) tileJson.attribution = metadata.attribution;
325
- if (metadata.description) tileJson.description = metadata.description;
326
- if (header.minZoom) tileJson.minzoom = header.minZoom;
327
- if (header.maxZoom) tileJson.maxzoom = header.maxZoom;
311
+ if (metadata.vector_layers) {
312
+ tileJson.vector_layers = metadata.vector_layers;
313
+ }
314
+ if (metadata.attribution) {
315
+ tileJson.attribution = metadata.attribution;
316
+ }
317
+ if (metadata.description) {
318
+ tileJson.description = metadata.description;
319
+ }
320
+ if (header.minZoom) {
321
+ tileJson.minzoom = header.minZoom;
322
+ }
323
+ if (header.maxZoom) {
324
+ tileJson.maxzoom = header.maxZoom;
325
+ }
328
326
  return tileJson;
329
327
  }
330
328
  }
@@ -1,5 +1,4 @@
1
1
  import { TilePackage } from "./index.js";
2
- import protocolGlyphs from "maplibre-local-glyphs";
3
2
 
4
3
  function stringEndsIn(string, endings) {
5
4
  for (let i = 0; i < endings.length; i++) {
@@ -172,14 +171,6 @@ export class Protocol {
172
171
  };
173
172
  }
174
173
 
175
- // Fallback to local glyphs since we couldn't load the coorect from the vtpk
176
- const fallback = await protocolGlyphs({
177
- url: `glyphs://${result[2]}/${result[3]}`,
178
- });
179
- if (fallback) {
180
- return fallback;
181
- }
182
-
183
174
  return { data: null };
184
175
  }
185
176
  }
package/src/source.js CHANGED
@@ -21,9 +21,11 @@ export class FileSource extends Source {
21
21
  constructor(file, options) {
22
22
  super();
23
23
  this.file = file;
24
+ this.coverageCheck = 1;
24
25
  if (options && options.coverageCheck) {
25
26
  this.coverageCheck = options.coverageCheck;
26
27
  }
28
+ //console.log("Tilepackage FileSource initialized", this.file);
27
29
  }
28
30
 
29
31
  getKey() {
@@ -56,18 +58,9 @@ export class FetchSource extends Source {
56
58
  this.url = url;
57
59
  this.customHeaders = customHeaders;
58
60
  this.mustReload = false;
59
- // Safe global detection for legacy environments without globalThis
60
- const g =
61
- typeof globalThis !== "undefined"
62
- ? globalThis
63
- : typeof window !== "undefined"
64
- ? window
65
- : typeof self !== "undefined"
66
- ? self
67
- : {};
68
61
  let userAgent = "";
69
- if (g && typeof g.navigator !== "undefined") {
70
- userAgent = g.navigator.userAgent || "";
62
+ if ("navigator" in globalThis) {
63
+ userAgent = globalThis.navigator.userAgent || "";
71
64
  }
72
65
  const isWindows = userAgent.indexOf("Windows") > -1;
73
66
  const isChromiumBased = /Chrome|Chromium|Edg|OPR|Brave/.test(userAgent);
@@ -75,6 +68,7 @@ export class FetchSource extends Source {
75
68
  if (isWindows && isChromiumBased) {
76
69
  this.chromeWindowsNoCache = true;
77
70
  }
71
+ this.coverageCheck = 1;
78
72
  if (options && options.coverageCheck) {
79
73
  this.coverageCheck = options.coverageCheck;
80
74
  }