maplibre-tilepackage-protocol 1.2.0 → 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.
@@ -0,0 +1,28 @@
1
+ import globals from "globals";
2
+ import pluginJs from "@eslint/js";
3
+
4
+ /** @type {import('eslint').Linter.Config[]} */
5
+ export default [
6
+ {
7
+ rules: {
8
+ semi: "error",
9
+ "no-var": "error",
10
+ "prefer-const": "error",
11
+ },
12
+ languageOptions: {
13
+ ecmaVersion: 2017,
14
+ sourceType: "module",
15
+ globals: globals.browser,
16
+ },
17
+ plugins: {
18
+ prettier: {
19
+ "prettier/prettier": "error",
20
+ //"linebreak-style": ["error", "windows"],
21
+ "no-console": ["error", { allow: ["info", "warn", "error"] }],
22
+ "no-unused-vars": ["error", { vars: "all", args: "none" }],
23
+ semi: ["error", "always"],
24
+ },
25
+ },
26
+ },
27
+ pluginJs.configs.recommended,
28
+ ];
package/package.json CHANGED
@@ -1,43 +1,42 @@
1
- {
2
- "name": "maplibre-tilepackage-protocol",
3
- "version": "1.2.0",
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) {
@@ -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,7 +3,7 @@ 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
9
  function calculateFilename(z, x, y, header) {
@@ -12,7 +12,12 @@ function calculateFilename(z, x, y, header) {
12
12
  const baseCol = Math.floor(x / 128) * 128;
13
13
  const rowHex = baseRow.toString(16).padStart(4, "0");
14
14
  const colHex = baseCol.toString(16).padStart(4, "0");
15
- let 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
+ }
16
21
  return `${basePath}/L${zoom}/R${rowHex}C${colHex}.bundle`;
17
22
  }
18
23
 
@@ -22,6 +27,10 @@ function getTileInfoFromTileIndex(tileIndex, z, x, y) {
22
27
  return tileIndex[row][col];
23
28
  }
24
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
+ */
25
34
  class EtagMismatch extends Error {
26
35
  constructor(message) {
27
36
  super(message);
@@ -34,25 +43,23 @@ export class TilePackage {
34
43
  // Default coverageCheck enabled; disable with coverageCheck:false
35
44
  this.coverageCheck = 1;
36
45
  if (options && options.coverageCheck === false) this.coverageCheck = 0;
37
- // Max dz guard (skip subdivision beyond this): default 8
38
- this.maxDz =
39
- options && typeof options.maxDz === "number" ? options.maxDz : 8;
40
46
  if (typeof source === "string") {
41
- const opts = this.coverageCheck
42
- ? { coverageCheck: this.coverageCheck }
43
- : undefined;
44
- this.source = new FetchSource(source, opts);
47
+ this.source = new FetchSource(source, this.coverageCheck);
45
48
  } else {
46
49
  this.source = source;
47
- // Propagate default coverageCheck to existing Source instances if undefined
48
- if (this.source && this.source.coverageCheck === undefined) {
49
- this.source.coverageCheck = this.coverageCheck;
50
- }
51
50
  }
52
- this.decompress =
53
- options && options.decompress ? options.decompress : defaultDecompress;
54
- this.cache =
55
- 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
+ }
56
63
  }
57
64
 
58
65
  async getHeader() {
@@ -169,6 +176,7 @@ export class TilePackage {
169
176
 
170
177
  async getMetadataAttempt() {
171
178
  const header = await this.cache.getHeader(this.source);
179
+
172
180
  let metadata = {};
173
181
  if (header.packageType === "vtpk") {
174
182
  const resp = await this.source.getBytes(
@@ -177,10 +185,12 @@ export class TilePackage {
177
185
  undefined,
178
186
  header.etag,
179
187
  );
188
+
180
189
  const decoder = new TextDecoder("utf-8");
181
190
  metadata = JSON.parse(decoder.decode(resp.data));
182
191
  }
183
192
  metadata.name = header.name;
193
+
184
194
  return metadata;
185
195
  }
186
196
 
@@ -198,7 +208,10 @@ export class TilePackage {
198
208
 
199
209
  async getResourceAttempt(file, signal) {
200
210
  const header = await this.cache.getHeader(this.source);
201
- if (!header.files[file]) return undefined;
211
+ if (!header.files[file]) {
212
+ return undefined;
213
+ }
214
+
202
215
  const resource = await this.cache.getResource(
203
216
  this.source,
204
217
  file,
@@ -238,49 +251,36 @@ export class TilePackage {
238
251
  header.files,
239
252
  this.source,
240
253
  );
241
- if (style.sources && style.sources.esri) {
242
- delete style.sources.esri.url;
243
- style.sources.esri.tiles = [`tilepackage://${sourceKey}/{z}/{x}/{y}`];
244
- style.sources.esri.type = style.sources.esri.type || "vector";
245
- style.sources.esri.minzoom = header.minZoom || 0;
246
- style.sources.esri.maxzoom = header.maxZoom || 22;
247
- }
248
- for (const k in style.sources) {
249
- const src = style.sources[k];
250
- if (
251
- src &&
252
- typeof src.url === "string" &&
253
- src.url.startsWith("tilepackage://")
254
- ) {
255
- delete src.url;
256
- if (!src.tiles)
257
- src.tiles = [`tilepackage://${sourceKey}/{z}/{x}/{y}`];
258
- }
259
- }
260
- 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) {
261
258
  style.sources.esri.attribution = metadata.copyrightText;
259
+ }
262
260
  style.glyphs = `tilepackage://${sourceKey}/{fontstack}/{range}`;
263
261
  style.sprite = `tilepackage://${sourceKey}/sprite`;
264
- if (this.debug)
265
- console.debug(
266
- "[tilepackage style] rewritten esri source",
267
- style.sources.esri,
268
- );
269
262
  return style;
270
- }
271
- return {
272
- version: 8,
273
- sources: {
274
- esri: {
275
- type: "raster",
276
- tileSize: header.tileSize || 256,
277
- tiles: [`tilepackage://${sourceKey}/{z}/{x}/{y}`],
278
- maxzoom: header.maxZoom || 22,
279
- 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
+ },
280
273
  },
281
- },
282
- layers: [{ id: "tilepackageraster", type: "raster", source: "esri" }],
283
- };
274
+ layers: [
275
+ {
276
+ id: "tilepackageraster",
277
+ type: "raster",
278
+ source: "esri",
279
+ },
280
+ ],
281
+ };
282
+ return style;
283
+ }
284
284
  }
285
285
 
286
286
  async getStyle() {
@@ -299,6 +299,7 @@ export class TilePackage {
299
299
  const header = await this.getHeader();
300
300
  const metadata = await this.getMetadata();
301
301
  const ext = header.tileType;
302
+
302
303
  const tileJson = {
303
304
  tilejson: "3.0.0",
304
305
  scheme: "xyz",
@@ -307,11 +308,21 @@ export class TilePackage {
307
308
  version: header.version,
308
309
  bounds: [header.minLon, header.minLat, header.maxLon, header.maxLat],
309
310
  };
310
- if (metadata.vector_layers) tileJson.vector_layers = metadata.vector_layers;
311
- if (metadata.attribution) tileJson.attribution = metadata.attribution;
312
- if (metadata.description) tileJson.description = metadata.description;
313
- if (header.minZoom) tileJson.minzoom = header.minZoom;
314
- 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
+ }
315
326
  return tileJson;
316
327
  }
317
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() {
@@ -66,6 +68,7 @@ export class FetchSource extends Source {
66
68
  if (isWindows && isChromiumBased) {
67
69
  this.chromeWindowsNoCache = true;
68
70
  }
71
+ this.coverageCheck = 1;
69
72
  if (options && options.coverageCheck) {
70
73
  this.coverageCheck = options.coverageCheck;
71
74
  }