maplibre-tilepackage-protocol 1.1.0 → 1.2.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/README.md +112 -47
- package/dist/bundle.js +5628 -0
- package/dist/bundle.js.map +1 -0
- package/dist/index.esm.js +2031 -0
- package/dist/index.esm.js.map +1 -0
- package/package.json +43 -23
- package/rollup.config.mjs +45 -0
- package/src/get-header-and-filelist.js +42 -3
- package/src/index.js +159 -100
- package/src/maplibre-gl-js-protocol.js +28 -12
- package/src/shared-promise-cache.js +23 -0
- package/src/source.js +8 -2
- package/src/tilecutter/IMPLEMENTATION_PLAN.md +135 -0
- package/src/tilecutter/subdivide.js +247 -0
|
@@ -0,0 +1,2031 @@
|
|
|
1
|
+
import { VectorTile, VectorTileFeature, VectorTileLayer } from '@mapbox/vector-tile';
|
|
2
|
+
import Protobuf from 'pbf';
|
|
3
|
+
import Point from '@mapbox/point-geometry';
|
|
4
|
+
import { fromVectorTileJs } from '@maplibre/vt-pbf';
|
|
5
|
+
import protocolGlyphs from 'maplibre-local-glyphs';
|
|
6
|
+
|
|
7
|
+
function stringEndsIn(string, endings) {
|
|
8
|
+
for (let i = 0; i < endings.length; i++) {
|
|
9
|
+
if (string.slice(-endings[i].length) == endings[i]) return endings[i];
|
|
10
|
+
}
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const converter = (getData) => (requestParameters, arg2) => {
|
|
15
|
+
if (arg2 instanceof AbortController) {
|
|
16
|
+
return getData(requestParameters, arg2);
|
|
17
|
+
}
|
|
18
|
+
const abortController = new AbortController();
|
|
19
|
+
getData(requestParameters, abortController)
|
|
20
|
+
.then(
|
|
21
|
+
(result) => {
|
|
22
|
+
return arg2(
|
|
23
|
+
undefined,
|
|
24
|
+
result.data,
|
|
25
|
+
result.cacheControl || "",
|
|
26
|
+
result.expires || "",
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
(err) => {
|
|
30
|
+
return arg2(err);
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
.catch((e) => {
|
|
34
|
+
return arg2(e);
|
|
35
|
+
});
|
|
36
|
+
return { cancel: () => abortController.abort() };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* MapLibre GL JS protocol. Must be added once globally.
|
|
41
|
+
*/
|
|
42
|
+
class Protocol {
|
|
43
|
+
/**
|
|
44
|
+
* Initialize the MapLibre TilePackage protocol.
|
|
45
|
+
*
|
|
46
|
+
* * metadata: also load the metadata section of the TilePackage. required for some "inspect" functionality
|
|
47
|
+
* and to automatically populate the map attribution. Requires an extra HTTP request.
|
|
48
|
+
* * errorOnMissingTile: When a vector MVT tile is missing from the archive, raise an error instead of
|
|
49
|
+
* returning the empty array. Not recommended. This is only to reproduce the behavior of ZXY tile APIs
|
|
50
|
+
* which some applications depend on when overzooming.
|
|
51
|
+
*/
|
|
52
|
+
constructor(options) {
|
|
53
|
+
this.tiles = new Map();
|
|
54
|
+
this.metadata = options ? options.metadata || false : false;
|
|
55
|
+
this.errorOnMissingTile = options
|
|
56
|
+
? options.errorOnMissingTile || false
|
|
57
|
+
: false;
|
|
58
|
+
this.debug = options ? options.debug || false : false;
|
|
59
|
+
this.getData = async (params, abortController) => {
|
|
60
|
+
if (params.type === "json") {
|
|
61
|
+
let tilePackageUrl = params.url.substr(14); // TODO fix this to be more robust
|
|
62
|
+
// at the moment we depend on that the tilePackage URL starts with tilepackage://
|
|
63
|
+
|
|
64
|
+
let spriteCheck = stringEndsIn(params.url, [
|
|
65
|
+
"/sprite",
|
|
66
|
+
"/sprite.json",
|
|
67
|
+
"/sprite@2x.json",
|
|
68
|
+
]);
|
|
69
|
+
if (spriteCheck) {
|
|
70
|
+
// For some reason spirit and glyphs urls might loose the : in http://
|
|
71
|
+
tilePackageUrl = tilePackageUrl.replace(/(https?)\/\/?/g, "$1://");
|
|
72
|
+
tilePackageUrl = tilePackageUrl.slice(0, -spriteCheck.length);
|
|
73
|
+
if (spriteCheck == "/sprite") {
|
|
74
|
+
spriteCheck = "/sprite.json";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let instance = this.tiles.get(tilePackageUrl);
|
|
79
|
+
if (!instance) {
|
|
80
|
+
instance = new TilePackage(tilePackageUrl);
|
|
81
|
+
this.tiles.set(tilePackageUrl, instance);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (spriteCheck) {
|
|
85
|
+
// This is a request for a sprite json
|
|
86
|
+
const file = `p12/resources/sprites${spriteCheck}`;
|
|
87
|
+
const resp = await instance.getResource(file, abortController.signal);
|
|
88
|
+
if (resp) {
|
|
89
|
+
const decoder = new TextDecoder("utf-8");
|
|
90
|
+
const json = JSON.parse(decoder.decode(resp.data));
|
|
91
|
+
return {
|
|
92
|
+
data: json,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// This is a request for a tile json
|
|
97
|
+
if (this.metadata) {
|
|
98
|
+
const tj = await instance.getTileJson(params.url);
|
|
99
|
+
if (this.debug)
|
|
100
|
+
console.debug("[tilepackage] tilejson (metadata)", tj);
|
|
101
|
+
return { data: tj };
|
|
102
|
+
}
|
|
103
|
+
const h = await instance.getHeader();
|
|
104
|
+
if (h.minLon >= h.maxLon || h.minLat >= h.maxLat) {
|
|
105
|
+
console.error(
|
|
106
|
+
`Bounds of TilePackage archive ${h.minLon},${h.minLat},${h.maxLon},${h.maxLat} are not valid.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const synthesized = {
|
|
111
|
+
tiles: [`${params.url}/{z}/{x}/{y}`],
|
|
112
|
+
minzoom: h.minZoom,
|
|
113
|
+
maxzoom: h.maxZoom,
|
|
114
|
+
bounds: [h.minLon, h.minLat, h.maxLon, h.maxLat],
|
|
115
|
+
};
|
|
116
|
+
if (this.debug)
|
|
117
|
+
console.debug("[tilepackage] tilejson (synthesized)", synthesized);
|
|
118
|
+
return { data: synthesized };
|
|
119
|
+
//}
|
|
120
|
+
}
|
|
121
|
+
// TODO handle other paths ???
|
|
122
|
+
let re = new RegExp(/tilepackage:\/\/(.+)\/(\d+)\/(\d+)\/(\d+)/);
|
|
123
|
+
let result = params.url.match(re);
|
|
124
|
+
if (!result) {
|
|
125
|
+
// This might be a request for glyphs or spirit
|
|
126
|
+
let tilePackageUrl = params.url.substr(14);
|
|
127
|
+
// For some reason spirit and glyphs urls might loose the : in http://
|
|
128
|
+
tilePackageUrl = tilePackageUrl.replace(/(https?)\/\/?/g, "$1://");
|
|
129
|
+
let spriteCheck = stringEndsIn(params.url, [
|
|
130
|
+
"/sprite",
|
|
131
|
+
"/sprite.png",
|
|
132
|
+
"/sprite@2x.png",
|
|
133
|
+
]);
|
|
134
|
+
if (spriteCheck) {
|
|
135
|
+
// It looks like we are looking for a spirit png
|
|
136
|
+
tilePackageUrl = tilePackageUrl.slice(0, -spriteCheck.length);
|
|
137
|
+
if (spriteCheck == "/sprite") {
|
|
138
|
+
spriteCheck = "/sprite.png";
|
|
139
|
+
}
|
|
140
|
+
let instance = this.tiles.get(tilePackageUrl);
|
|
141
|
+
if (!instance) {
|
|
142
|
+
instance = new TilePackage(tilePackageUrl);
|
|
143
|
+
this.tiles.set(tilePackageUrl, instance);
|
|
144
|
+
}
|
|
145
|
+
const file = `p12/resources/sprites${spriteCheck}`;
|
|
146
|
+
const resp = await instance.getResource(file, abortController.signal);
|
|
147
|
+
if (resp) {
|
|
148
|
+
return {
|
|
149
|
+
data: new Uint8Array(resp.data),
|
|
150
|
+
cacheControl: resp.cacheControl,
|
|
151
|
+
expires: resp.expires,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// This is more likely a glyph url
|
|
156
|
+
re = new RegExp(/^tilepackage:\/\/(.+?)\/([^/]+)\/([^/]+)$/);
|
|
157
|
+
result = params.url.match(re);
|
|
158
|
+
if (result && isNaN(result[2])) {
|
|
159
|
+
const tilePackageUrl = result[1];
|
|
160
|
+
let instance = this.tiles.get(tilePackageUrl);
|
|
161
|
+
if (!instance) {
|
|
162
|
+
instance = new TilePackage(tilePackageUrl);
|
|
163
|
+
this.tiles.set(tilePackageUrl, instance);
|
|
164
|
+
}
|
|
165
|
+
const file = `p12/resources/fonts/${result[2]}/${result[3]}.pbf`;
|
|
166
|
+
const resp = await instance.getResource(
|
|
167
|
+
file,
|
|
168
|
+
abortController.signal,
|
|
169
|
+
);
|
|
170
|
+
if (resp) {
|
|
171
|
+
return {
|
|
172
|
+
data: new Uint8Array(resp.data),
|
|
173
|
+
cacheControl: resp.cacheControl,
|
|
174
|
+
expires: resp.expires,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Fallback to local glyphs since we couldn't load the coorect from the vtpk
|
|
179
|
+
const fallback = await protocolGlyphs({
|
|
180
|
+
url: `glyphs://${result[2]}/${result[3]}`,
|
|
181
|
+
});
|
|
182
|
+
if (fallback) {
|
|
183
|
+
return fallback;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { data: null };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!result) {
|
|
192
|
+
throw new Error("Invalid TilePackage protocol URL");
|
|
193
|
+
}
|
|
194
|
+
const tilePackageUrl = result[1];
|
|
195
|
+
|
|
196
|
+
let instance = this.tiles.get(tilePackageUrl);
|
|
197
|
+
if (!instance) {
|
|
198
|
+
instance = new TilePackage(tilePackageUrl);
|
|
199
|
+
this.tiles.set(tilePackageUrl, instance);
|
|
200
|
+
}
|
|
201
|
+
const z = result[2];
|
|
202
|
+
const x = result[3];
|
|
203
|
+
const y = result[4];
|
|
204
|
+
const header = await instance.getHeader();
|
|
205
|
+
const resp = await instance.getZxy(+z, +x, +y, abortController.signal);
|
|
206
|
+
if (this.debug)
|
|
207
|
+
console.debug("[tilepackage] tile fetch", {
|
|
208
|
+
z: +z,
|
|
209
|
+
x: +x,
|
|
210
|
+
y: +y,
|
|
211
|
+
found: !!resp,
|
|
212
|
+
});
|
|
213
|
+
if (resp) {
|
|
214
|
+
return {
|
|
215
|
+
data: new Uint8Array(resp.data),
|
|
216
|
+
cacheControl: resp.cacheControl,
|
|
217
|
+
expires: resp.expires,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (header.tileType === "pbf") {
|
|
221
|
+
if (this.errorOnMissingTile) {
|
|
222
|
+
//*
|
|
223
|
+
const e = new Error(
|
|
224
|
+
`Tile [${z},${x},${y}] not found in Tile Package, normal for VTPK with variable depth.`,
|
|
225
|
+
);
|
|
226
|
+
e.name = "TileError";
|
|
227
|
+
if (this.debug)
|
|
228
|
+
console.debug("[tilepackage] missing tile", { z, x, y });
|
|
229
|
+
throw e;
|
|
230
|
+
}
|
|
231
|
+
return { data: new Uint8Array() };
|
|
232
|
+
} else {
|
|
233
|
+
return { data: null };
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
this.package = converter(this.getData);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Add a {@link TilePackage} instance to the global protocol instance.
|
|
242
|
+
*
|
|
243
|
+
* For remote fetch sources, references in MapLibre styles like tilePackage://http://...
|
|
244
|
+
* will resolve to the same instance if the URLs match.
|
|
245
|
+
*/
|
|
246
|
+
add(p) {
|
|
247
|
+
this.tiles.set(p.source.getKey(), p);
|
|
248
|
+
if (this.debug)
|
|
249
|
+
console.debug("[tilepackage] add instance", p.source.getKey());
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Fetch a {@link TilePackage} instance by URL, for remote TilePackage instances.
|
|
254
|
+
*/
|
|
255
|
+
get(url) {
|
|
256
|
+
if (this.debug) console.debug("[tilepackage] get instance", url);
|
|
257
|
+
return this.tiles.get(url);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* global globalThis */
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Interface for retrieving an archive from remote or local storage.
|
|
265
|
+
*/
|
|
266
|
+
class Source {
|
|
267
|
+
async getBytes() {
|
|
268
|
+
throw new Error("Not implemented");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
getKey() {
|
|
272
|
+
throw new Error("Not implemented");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Use the Browser's File API, which is different from the NodeJS file API.
|
|
278
|
+
* see https://developer.mozilla.org/en-US/docs/Web/API/File_API
|
|
279
|
+
*/
|
|
280
|
+
class FileSource extends Source {
|
|
281
|
+
constructor(file, options) {
|
|
282
|
+
super();
|
|
283
|
+
this.file = file;
|
|
284
|
+
if (options && options.coverageCheck) {
|
|
285
|
+
this.coverageCheck = options.coverageCheck;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
getKey() {
|
|
290
|
+
return this.file.name;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async getSize() {
|
|
294
|
+
return this.file.size;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// eslint-disable-next-line no-unused-vars
|
|
298
|
+
async getBytes(offset, length, passedSignal, etag) {
|
|
299
|
+
const blob = this.file.slice(offset, offset + length);
|
|
300
|
+
const a = await blob.arrayBuffer();
|
|
301
|
+
return { data: a };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Uses the browser Fetch API to make tile requests via HTTP.
|
|
307
|
+
*
|
|
308
|
+
* This method does not send conditional request headers If-Match because of CORS.
|
|
309
|
+
* Instead, it detects ETag mismatches via the response ETag or the 416 response code.
|
|
310
|
+
*
|
|
311
|
+
* This also works around browser and storage-specific edge cases.
|
|
312
|
+
*/
|
|
313
|
+
class FetchSource extends Source {
|
|
314
|
+
constructor(url, options, customHeaders = new Headers()) {
|
|
315
|
+
super();
|
|
316
|
+
this.url = url;
|
|
317
|
+
this.customHeaders = customHeaders;
|
|
318
|
+
this.mustReload = false;
|
|
319
|
+
let userAgent = "";
|
|
320
|
+
if ("navigator" in globalThis) {
|
|
321
|
+
userAgent = globalThis.navigator.userAgent || "";
|
|
322
|
+
}
|
|
323
|
+
const isWindows = userAgent.indexOf("Windows") > -1;
|
|
324
|
+
const isChromiumBased = /Chrome|Chromium|Edg|OPR|Brave/.test(userAgent);
|
|
325
|
+
this.chromeWindowsNoCache = false;
|
|
326
|
+
if (isWindows && isChromiumBased) {
|
|
327
|
+
this.chromeWindowsNoCache = true;
|
|
328
|
+
}
|
|
329
|
+
if (options && options.coverageCheck) {
|
|
330
|
+
this.coverageCheck = options.coverageCheck;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
getKey() {
|
|
335
|
+
return this.url;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// eslint-disable-next-line no-unused-vars
|
|
339
|
+
async getSize(passedSignal, etag) {
|
|
340
|
+
if (this.size) return this.size;
|
|
341
|
+
|
|
342
|
+
let controller;
|
|
343
|
+
let signal;
|
|
344
|
+
if (passedSignal) {
|
|
345
|
+
signal = passedSignal;
|
|
346
|
+
} else {
|
|
347
|
+
controller = new AbortController();
|
|
348
|
+
signal = controller.signal;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const requestHeaders = new Headers(this.customHeaders);
|
|
352
|
+
requestHeaders.set("range", `bytes=${0}-${4}`);
|
|
353
|
+
|
|
354
|
+
let cache;
|
|
355
|
+
if (this.mustReload) {
|
|
356
|
+
cache = "reload";
|
|
357
|
+
} else if (this.chromeWindowsNoCache) {
|
|
358
|
+
cache = "no-store";
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const resp = await fetch(this.url, {
|
|
362
|
+
signal: signal,
|
|
363
|
+
cache: cache,
|
|
364
|
+
headers: requestHeaders,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
//Making sure that the server supports content-range and getting the file size
|
|
368
|
+
const contentRange = resp.headers.get("Content-Range");
|
|
369
|
+
if (resp.status === 206 && contentRange) {
|
|
370
|
+
const fileSize = contentRange.split("/")[1];
|
|
371
|
+
if (fileSize) {
|
|
372
|
+
this.size = fileSize;
|
|
373
|
+
return fileSize;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (resp.status === 206 && !contentRange) {
|
|
377
|
+
// We didn't get a content-range response, getting head instead
|
|
378
|
+
// Getting head is much slower but works. We need to get the size
|
|
379
|
+
// of the file in some way since zip files has the catalog
|
|
380
|
+
// directory at the end.
|
|
381
|
+
const response = await fetch(this.url, {
|
|
382
|
+
method: "HEAD",
|
|
383
|
+
});
|
|
384
|
+
if (response.ok) {
|
|
385
|
+
const fileSize = response.headers.get("Content-Length");
|
|
386
|
+
this.size = fileSize;
|
|
387
|
+
return fileSize;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (resp.status === 200 && !contentRange) {
|
|
392
|
+
if (controller) controller.abort();
|
|
393
|
+
throw new Error(
|
|
394
|
+
"Server returned no content-length header or content-length exceeding request. Check that your storage backend supports HTTP Byte Serving.",
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
//const requestHeaders = new Headers(this.customHeaders);
|
|
398
|
+
//requestHeaders.set("method", "HEAD");
|
|
399
|
+
|
|
400
|
+
/*
|
|
401
|
+
let controller;
|
|
402
|
+
let signal;
|
|
403
|
+
if (passedSignal) {
|
|
404
|
+
signal = passedSignal;
|
|
405
|
+
} else {
|
|
406
|
+
controller = new AbortController();
|
|
407
|
+
signal = controller.signal;
|
|
408
|
+
}
|
|
409
|
+
*/
|
|
410
|
+
/*
|
|
411
|
+
try {
|
|
412
|
+
const response = await fetch(this.url, { method: "HEAD" });
|
|
413
|
+
if (!response.ok) {
|
|
414
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
415
|
+
}
|
|
416
|
+
const contentLength = response.headers.get("content-length");
|
|
417
|
+
if (contentLength) {
|
|
418
|
+
console.log(contentLength);
|
|
419
|
+
this.size = contentLength;
|
|
420
|
+
return contentLength;
|
|
421
|
+
} else {
|
|
422
|
+
console.error("Content-Length header is not available.");
|
|
423
|
+
}
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.error("Error fetching headers:", error);
|
|
426
|
+
}*/
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
setHeaders(customHeaders) {
|
|
430
|
+
this.customHeaders = customHeaders;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async getBytes(offset, length, passedSignal, etag) {
|
|
434
|
+
let controller;
|
|
435
|
+
let signal;
|
|
436
|
+
if (passedSignal) {
|
|
437
|
+
signal = passedSignal;
|
|
438
|
+
} else {
|
|
439
|
+
controller = new AbortController();
|
|
440
|
+
signal = controller.signal;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const requestHeaders = new Headers(this.customHeaders);
|
|
444
|
+
requestHeaders.set("range", `bytes=${offset}-${offset + length - 1}`);
|
|
445
|
+
|
|
446
|
+
let cache;
|
|
447
|
+
if (this.mustReload) {
|
|
448
|
+
cache = "reload";
|
|
449
|
+
} else if (this.chromeWindowsNoCache) {
|
|
450
|
+
cache = "no-store";
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
let resp = await fetch(this.url, {
|
|
454
|
+
signal: signal,
|
|
455
|
+
cache: cache,
|
|
456
|
+
headers: requestHeaders,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (offset === 0 && resp.status === 416) {
|
|
460
|
+
const contentRange = resp.headers.get("Content-Range");
|
|
461
|
+
if (!contentRange || !contentRange.startsWith("bytes */")) {
|
|
462
|
+
throw new Error("Missing content-length on 416 response");
|
|
463
|
+
}
|
|
464
|
+
const actualLength = +contentRange.substr(8);
|
|
465
|
+
resp = await fetch(this.url, {
|
|
466
|
+
signal: signal,
|
|
467
|
+
cache: "reload",
|
|
468
|
+
headers: { range: `bytes=0-${actualLength - 1}` },
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let newEtag = resp.headers.get("Etag");
|
|
473
|
+
if (newEtag && newEtag.startsWith("W/")) {
|
|
474
|
+
newEtag = null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (resp.status === 416 || (etag && newEtag && newEtag !== etag)) {
|
|
478
|
+
this.mustReload = true;
|
|
479
|
+
throw new Error(
|
|
480
|
+
`Server returned non-matching ETag ${etag} after one retry. Check browser extensions and servers for issues that may affect correct ETag headers.`,
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (resp.status >= 300) {
|
|
485
|
+
throw new Error(`Bad response code: ${resp.status}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const contentLength = resp.headers.get("Content-Length");
|
|
489
|
+
if (resp.status === 200 && (!contentLength || +contentLength > length)) {
|
|
490
|
+
if (controller) controller.abort();
|
|
491
|
+
throw new Error(
|
|
492
|
+
"Server returned no content-length header or content-length exceeding request. Check that your storage backend supports HTTP Byte Serving.",
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
const a = await resp.arrayBuffer();
|
|
496
|
+
return {
|
|
497
|
+
data: a,
|
|
498
|
+
etag: newEtag || undefined,
|
|
499
|
+
cacheControl: resp.headers.get("Cache-Control") || undefined,
|
|
500
|
+
expires: resp.headers.get("Expires") || undefined,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Phase A: Subdivide a parent vector tile into a higher zoom target tile.
|
|
506
|
+
// Minimal dependency implementation using @mapbox/vector-tile for decode and @maplibre/vt-pbf for encode.
|
|
507
|
+
// Public API: subdivideVectorTile(parentPbf, parentZ, parentX, parentY, targetZ, targetX, targetY, options)
|
|
508
|
+
// Returns { data: Uint8Array, stats }
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Subdivide parent tile PBF into higher zoom target tile.
|
|
513
|
+
* @param {ArrayBuffer|Uint8Array} parentPbf - Raw parent tile PBF bytes.
|
|
514
|
+
* @param {number} parentZ
|
|
515
|
+
* @param {number} parentX
|
|
516
|
+
* @param {number} parentY
|
|
517
|
+
* @param {number} targetZ
|
|
518
|
+
* @param {number} targetX
|
|
519
|
+
* @param {number} targetY
|
|
520
|
+
* @param {object} options
|
|
521
|
+
* @param {number} [options.buffer=128] - Clip buffer.
|
|
522
|
+
* @param {string[]|RegExp} [options.includeLayers] - Layer filter.
|
|
523
|
+
* @param {number} [options.maxDzWarn=8] - Warn if dz larger.
|
|
524
|
+
* @returns {{ data: Uint8Array, stats: any }}
|
|
525
|
+
*/
|
|
526
|
+
function subdivideVectorTile(
|
|
527
|
+
parentPbf,
|
|
528
|
+
parentZ,
|
|
529
|
+
parentX,
|
|
530
|
+
parentY,
|
|
531
|
+
targetZ,
|
|
532
|
+
targetX,
|
|
533
|
+
targetY,
|
|
534
|
+
options = {},
|
|
535
|
+
) {
|
|
536
|
+
const t0 = performance.now ? performance.now() : Date.now();
|
|
537
|
+
const { buffer = 128, includeLayers, maxDzWarn = 8 } = options;
|
|
538
|
+
const dz = targetZ - parentZ;
|
|
539
|
+
if (dz <= 0) {
|
|
540
|
+
// Nothing to subdivide, caller should just use original bytes.
|
|
541
|
+
return { data: toUint8(parentPbf), stats: { dz, passthrough: true } };
|
|
542
|
+
}
|
|
543
|
+
if (dz > maxDzWarn && console && console.warn) {
|
|
544
|
+
console.warn(
|
|
545
|
+
`subdivideVectorTile: high dz=${dz}, performance may degrade.`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
// Validate containment.
|
|
549
|
+
const scale = Math.pow(2, dz);
|
|
550
|
+
const expectedParentX = Math.floor(targetX / scale);
|
|
551
|
+
const expectedParentY = Math.floor(targetY / scale);
|
|
552
|
+
if (expectedParentX !== parentX || expectedParentY !== parentY) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
"Target tile not contained within parent tile coordinates.",
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const vt = new VectorTile(new Protobuf(toUint8(parentPbf)));
|
|
559
|
+
|
|
560
|
+
const overzoomed = new OverzoomedVectorTile();
|
|
561
|
+
let featuresIn = 0;
|
|
562
|
+
let featuresOut = 0;
|
|
563
|
+
for (const layerName in vt.layers) {
|
|
564
|
+
if (!Object.prototype.hasOwnProperty.call(vt.layers, layerName)) continue;
|
|
565
|
+
if (includeLayers) {
|
|
566
|
+
if (Array.isArray(includeLayers) && !includeLayers.includes(layerName))
|
|
567
|
+
continue;
|
|
568
|
+
if (includeLayers instanceof RegExp && !includeLayers.test(layerName))
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const sourceLayer = vt.layers[layerName];
|
|
572
|
+
const extent = sourceLayer.extent;
|
|
573
|
+
const offsetX = (targetX - parentX * scale) * extent;
|
|
574
|
+
const offsetY = (targetY - parentY * scale) * extent;
|
|
575
|
+
|
|
576
|
+
const featureWrappers = [];
|
|
577
|
+
for (let i = 0; i < sourceLayer.length; i++) {
|
|
578
|
+
const feature = sourceLayer.feature(i);
|
|
579
|
+
featuresIn++;
|
|
580
|
+
let geometry = feature.loadGeometry(); // Array of rings (Point[])
|
|
581
|
+
// Transform
|
|
582
|
+
for (const ring of geometry) {
|
|
583
|
+
for (const pt of ring) {
|
|
584
|
+
pt.x = pt.x * scale - offsetX;
|
|
585
|
+
pt.y = pt.y * scale - offsetY;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// Clip
|
|
589
|
+
geometry = clipGeometry(geometry, feature.type, -buffer, extent + buffer);
|
|
590
|
+
if (!geometry || geometry.length === 0) continue;
|
|
591
|
+
featureWrappers.push(
|
|
592
|
+
new OverzoomedFeatureWrapper(
|
|
593
|
+
feature.type,
|
|
594
|
+
geometry,
|
|
595
|
+
feature.properties,
|
|
596
|
+
feature.id,
|
|
597
|
+
extent,
|
|
598
|
+
),
|
|
599
|
+
);
|
|
600
|
+
featuresOut++;
|
|
601
|
+
}
|
|
602
|
+
if (featureWrappers.length > 0) {
|
|
603
|
+
overzoomed.addLayer(
|
|
604
|
+
new OverzoomedTileLayer(featureWrappers, layerName, extent),
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const pbf = fromVectorTileJs(overzoomed);
|
|
609
|
+
const data =
|
|
610
|
+
pbf.byteOffset === 0 && pbf.byteLength === pbf.buffer.byteLength
|
|
611
|
+
? pbf
|
|
612
|
+
: new Uint8Array(pbf);
|
|
613
|
+
const t1 = performance.now ? performance.now() : Date.now();
|
|
614
|
+
return { data, stats: { dz, featuresIn, featuresOut, ms: t1 - t0 } };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// --- Internal wrappers (adapted from overzoomed-geojson-vector-tile.ts) ---
|
|
618
|
+
class OverzoomedFeatureWrapper extends VectorTileFeature {
|
|
619
|
+
constructor(type, geometry, properties, id, extent) {
|
|
620
|
+
super(new Protobuf(), 0, extent, [], []);
|
|
621
|
+
this.type = type; // 1=Point,2=LineString,3=Polygon
|
|
622
|
+
this.properties = properties ? properties : {};
|
|
623
|
+
this.extent = extent;
|
|
624
|
+
this.myGeometry = geometry;
|
|
625
|
+
this.id = id;
|
|
626
|
+
}
|
|
627
|
+
loadGeometry() {
|
|
628
|
+
return this.myGeometry.map((ring) => ring.map((p) => new Point(p.x, p.y)));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
class OverzoomedTileLayer extends VectorTileLayer {
|
|
633
|
+
constructor(features, name, extent) {
|
|
634
|
+
super(new Protobuf());
|
|
635
|
+
this._myFeatures = features;
|
|
636
|
+
this.name = name;
|
|
637
|
+
this.length = features.length;
|
|
638
|
+
this.extent = extent;
|
|
639
|
+
this.version = 2;
|
|
640
|
+
}
|
|
641
|
+
feature(i) {
|
|
642
|
+
return this._myFeatures[i];
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
class OverzoomedVectorTile {
|
|
647
|
+
constructor() {
|
|
648
|
+
this.layers = {};
|
|
649
|
+
}
|
|
650
|
+
addLayer(layer) {
|
|
651
|
+
this.layers[layer.name] = layer;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// --- Clipping helpers ---
|
|
656
|
+
function clipGeometry(geometry, type, min, max) {
|
|
657
|
+
switch (type) {
|
|
658
|
+
case 1:
|
|
659
|
+
return clipPoints(geometry, min, max); // POINT
|
|
660
|
+
case 2:
|
|
661
|
+
return clipLinesWrapper(geometry, min, max, false); // LINESTRING
|
|
662
|
+
case 3:
|
|
663
|
+
return clipLinesWrapper(geometry, min, max, true); // POLYGON
|
|
664
|
+
default:
|
|
665
|
+
return [];
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function clipPoints(geometry, min, max) {
|
|
669
|
+
const out = [];
|
|
670
|
+
for (const ring of geometry) {
|
|
671
|
+
for (const p of ring) {
|
|
672
|
+
if (p.x >= min && p.x <= max && p.y >= min && p.y <= max) {
|
|
673
|
+
out.push([p]);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return out;
|
|
678
|
+
}
|
|
679
|
+
function clipLinesWrapper(geometry, min, max, isPolygon) {
|
|
680
|
+
const out = [];
|
|
681
|
+
for (const line of geometry) {
|
|
682
|
+
const clipped = clipAxis(line, min, max, 0, isPolygon);
|
|
683
|
+
for (const seg of clipped) {
|
|
684
|
+
const clippedY = clipAxis(seg, min, max, 1, isPolygon);
|
|
685
|
+
for (const finalSeg of clippedY) out.push(finalSeg);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return out;
|
|
689
|
+
}
|
|
690
|
+
function clipAxis(line, start, end, axis, isPolygon) {
|
|
691
|
+
const intersect = axis === 0 ? intersectX : intersectY;
|
|
692
|
+
let slice = [];
|
|
693
|
+
const out = [];
|
|
694
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
695
|
+
const p1 = line[i];
|
|
696
|
+
const p2 = line[i + 1];
|
|
697
|
+
const a = axis === 0 ? p1.x : p1.y;
|
|
698
|
+
const b = axis === 0 ? p2.x : p2.y;
|
|
699
|
+
let exited = false;
|
|
700
|
+
if (a < start && b > start) slice.push(intersect(p1, p2, start));
|
|
701
|
+
else if (a > end && b < end) slice.push(intersect(p1, p2, end));
|
|
702
|
+
else if (a >= start && a <= end) slice.push(p1);
|
|
703
|
+
if (b < start && a >= start) {
|
|
704
|
+
slice.push(intersect(p1, p2, start));
|
|
705
|
+
exited = true;
|
|
706
|
+
}
|
|
707
|
+
if (b > end && a <= end) {
|
|
708
|
+
slice.push(intersect(p1, p2, end));
|
|
709
|
+
exited = true;
|
|
710
|
+
}
|
|
711
|
+
if (!isPolygon && exited) {
|
|
712
|
+
out.push(slice);
|
|
713
|
+
slice = [];
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const last = line[line.length - 1];
|
|
717
|
+
const lastVal = axis === 0 ? last.x : last.y;
|
|
718
|
+
if (lastVal >= start && lastVal <= end) slice.push(last);
|
|
719
|
+
if (
|
|
720
|
+
isPolygon &&
|
|
721
|
+
slice.length > 0 &&
|
|
722
|
+
!slice[0].equals(slice[slice.length - 1])
|
|
723
|
+
) {
|
|
724
|
+
slice.push(new Point(slice[0].x, slice[0].y));
|
|
725
|
+
}
|
|
726
|
+
if (slice.length > 0) out.push(slice);
|
|
727
|
+
return out;
|
|
728
|
+
}
|
|
729
|
+
function intersectX(p1, p2, x) {
|
|
730
|
+
const t = (x - p1.x) / (p2.x - p1.x);
|
|
731
|
+
return new Point(x, p1.y + (p2.y - p1.y) * t);
|
|
732
|
+
}
|
|
733
|
+
function intersectY(p1, p2, y) {
|
|
734
|
+
const t = (y - p1.y) / (p2.y - p1.y);
|
|
735
|
+
return new Point(p1.x + (p2.x - p1.x) * t, y);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function toUint8(data) {
|
|
739
|
+
if (data instanceof Uint8Array) return data;
|
|
740
|
+
return new Uint8Array(data);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// DEFLATE is a complex format; to read this code, you should probably check the RFC first:
|
|
744
|
+
// https://tools.ietf.org/html/rfc1951
|
|
745
|
+
// You may also wish to take a look at the guide I made about this program:
|
|
746
|
+
// https://gist.github.com/101arrowz/253f31eb5abc3d9275ab943003ffecad
|
|
747
|
+
// Some of the following code is similar to that of UZIP.js:
|
|
748
|
+
// https://github.com/photopea/UZIP.js
|
|
749
|
+
// However, the vast majority of the codebase has diverged from UZIP.js to increase performance and reduce bundle size.
|
|
750
|
+
// Sometimes 0 will appear where -1 would be more appropriate. This is because using a uint
|
|
751
|
+
// is better for memory in most engines (I *think*).
|
|
752
|
+
|
|
753
|
+
// aliases for shorter compressed code (most minifers don't do this)
|
|
754
|
+
var u8 = Uint8Array, u16 = Uint16Array, i32 = Int32Array;
|
|
755
|
+
// fixed length extra bits
|
|
756
|
+
var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, /* unused */ 0, 0, /* impossible */ 0]);
|
|
757
|
+
// fixed distance extra bits
|
|
758
|
+
var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, /* unused */ 0, 0]);
|
|
759
|
+
// code length index map
|
|
760
|
+
var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
|
|
761
|
+
// get base, reverse index map from extra bits
|
|
762
|
+
var freb = function (eb, start) {
|
|
763
|
+
var b = new u16(31);
|
|
764
|
+
for (var i = 0; i < 31; ++i) {
|
|
765
|
+
b[i] = start += 1 << eb[i - 1];
|
|
766
|
+
}
|
|
767
|
+
// numbers here are at max 18 bits
|
|
768
|
+
var r = new i32(b[30]);
|
|
769
|
+
for (var i = 1; i < 30; ++i) {
|
|
770
|
+
for (var j = b[i]; j < b[i + 1]; ++j) {
|
|
771
|
+
r[j] = ((j - b[i]) << 5) | i;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return { b: b, r: r };
|
|
775
|
+
};
|
|
776
|
+
var _a = freb(fleb, 2), fl = _a.b, revfl = _a.r;
|
|
777
|
+
// we can ignore the fact that the other numbers are wrong; they never happen anyway
|
|
778
|
+
fl[28] = 258, revfl[258] = 28;
|
|
779
|
+
var _b = freb(fdeb, 0), fd = _b.b;
|
|
780
|
+
// map of value to reverse (assuming 16 bits)
|
|
781
|
+
var rev = new u16(32768);
|
|
782
|
+
for (var i = 0; i < 32768; ++i) {
|
|
783
|
+
// reverse table algorithm from SO
|
|
784
|
+
var x = ((i & 0xAAAA) >> 1) | ((i & 0x5555) << 1);
|
|
785
|
+
x = ((x & 0xCCCC) >> 2) | ((x & 0x3333) << 2);
|
|
786
|
+
x = ((x & 0xF0F0) >> 4) | ((x & 0x0F0F) << 4);
|
|
787
|
+
rev[i] = (((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8)) >> 1;
|
|
788
|
+
}
|
|
789
|
+
// create huffman tree from u8 "map": index -> code length for code index
|
|
790
|
+
// mb (max bits) must be at most 15
|
|
791
|
+
// TODO: optimize/split up?
|
|
792
|
+
var hMap = (function (cd, mb, r) {
|
|
793
|
+
var s = cd.length;
|
|
794
|
+
// index
|
|
795
|
+
var i = 0;
|
|
796
|
+
// u16 "map": index -> # of codes with bit length = index
|
|
797
|
+
var l = new u16(mb);
|
|
798
|
+
// length of cd must be 288 (total # of codes)
|
|
799
|
+
for (; i < s; ++i) {
|
|
800
|
+
if (cd[i])
|
|
801
|
+
++l[cd[i] - 1];
|
|
802
|
+
}
|
|
803
|
+
// u16 "map": index -> minimum code for bit length = index
|
|
804
|
+
var le = new u16(mb);
|
|
805
|
+
for (i = 1; i < mb; ++i) {
|
|
806
|
+
le[i] = (le[i - 1] + l[i - 1]) << 1;
|
|
807
|
+
}
|
|
808
|
+
var co;
|
|
809
|
+
if (r) {
|
|
810
|
+
// u16 "map": index -> number of actual bits, symbol for code
|
|
811
|
+
co = new u16(1 << mb);
|
|
812
|
+
// bits to remove for reverser
|
|
813
|
+
var rvb = 15 - mb;
|
|
814
|
+
for (i = 0; i < s; ++i) {
|
|
815
|
+
// ignore 0 lengths
|
|
816
|
+
if (cd[i]) {
|
|
817
|
+
// num encoding both symbol and bits read
|
|
818
|
+
var sv = (i << 4) | cd[i];
|
|
819
|
+
// free bits
|
|
820
|
+
var r_1 = mb - cd[i];
|
|
821
|
+
// start value
|
|
822
|
+
var v = le[cd[i] - 1]++ << r_1;
|
|
823
|
+
// m is end value
|
|
824
|
+
for (var m = v | ((1 << r_1) - 1); v <= m; ++v) {
|
|
825
|
+
// every 16 bit value starting with the code yields the same result
|
|
826
|
+
co[rev[v] >> rvb] = sv;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
co = new u16(s);
|
|
833
|
+
for (i = 0; i < s; ++i) {
|
|
834
|
+
if (cd[i]) {
|
|
835
|
+
co[i] = rev[le[cd[i] - 1]++] >> (15 - cd[i]);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return co;
|
|
840
|
+
});
|
|
841
|
+
// fixed length tree
|
|
842
|
+
var flt = new u8(288);
|
|
843
|
+
for (var i = 0; i < 144; ++i)
|
|
844
|
+
flt[i] = 8;
|
|
845
|
+
for (var i = 144; i < 256; ++i)
|
|
846
|
+
flt[i] = 9;
|
|
847
|
+
for (var i = 256; i < 280; ++i)
|
|
848
|
+
flt[i] = 7;
|
|
849
|
+
for (var i = 280; i < 288; ++i)
|
|
850
|
+
flt[i] = 8;
|
|
851
|
+
// fixed distance tree
|
|
852
|
+
var fdt = new u8(32);
|
|
853
|
+
for (var i = 0; i < 32; ++i)
|
|
854
|
+
fdt[i] = 5;
|
|
855
|
+
// fixed length map
|
|
856
|
+
var flrm = /*#__PURE__*/ hMap(flt, 9, 1);
|
|
857
|
+
// fixed distance map
|
|
858
|
+
var fdrm = /*#__PURE__*/ hMap(fdt, 5, 1);
|
|
859
|
+
// find max of array
|
|
860
|
+
var max = function (a) {
|
|
861
|
+
var m = a[0];
|
|
862
|
+
for (var i = 1; i < a.length; ++i) {
|
|
863
|
+
if (a[i] > m)
|
|
864
|
+
m = a[i];
|
|
865
|
+
}
|
|
866
|
+
return m;
|
|
867
|
+
};
|
|
868
|
+
// read d, starting at bit p and mask with m
|
|
869
|
+
var bits = function (d, p, m) {
|
|
870
|
+
var o = (p / 8) | 0;
|
|
871
|
+
return ((d[o] | (d[o + 1] << 8)) >> (p & 7)) & m;
|
|
872
|
+
};
|
|
873
|
+
// read d, starting at bit p continuing for at least 16 bits
|
|
874
|
+
var bits16 = function (d, p) {
|
|
875
|
+
var o = (p / 8) | 0;
|
|
876
|
+
return ((d[o] | (d[o + 1] << 8) | (d[o + 2] << 16)) >> (p & 7));
|
|
877
|
+
};
|
|
878
|
+
// get end of byte
|
|
879
|
+
var shft = function (p) { return ((p + 7) / 8) | 0; };
|
|
880
|
+
// typed array slice - allows garbage collector to free original reference,
|
|
881
|
+
// while being more compatible than .slice
|
|
882
|
+
var slc = function (v, s, e) {
|
|
883
|
+
if (e == null || e > v.length)
|
|
884
|
+
e = v.length;
|
|
885
|
+
// can't use .constructor in case user-supplied
|
|
886
|
+
return new u8(v.subarray(s, e));
|
|
887
|
+
};
|
|
888
|
+
// error codes
|
|
889
|
+
var ec = [
|
|
890
|
+
'unexpected EOF',
|
|
891
|
+
'invalid block type',
|
|
892
|
+
'invalid length/literal',
|
|
893
|
+
'invalid distance',
|
|
894
|
+
'stream finished',
|
|
895
|
+
'no stream handler',
|
|
896
|
+
,
|
|
897
|
+
'no callback',
|
|
898
|
+
'invalid UTF-8 data',
|
|
899
|
+
'extra field too long',
|
|
900
|
+
'date not in range 1980-2099',
|
|
901
|
+
'filename too long',
|
|
902
|
+
'stream finishing',
|
|
903
|
+
'invalid zip data'
|
|
904
|
+
// determined by unknown compression method
|
|
905
|
+
];
|
|
906
|
+
var err = function (ind, msg, nt) {
|
|
907
|
+
var e = new Error(msg || ec[ind]);
|
|
908
|
+
e.code = ind;
|
|
909
|
+
if (Error.captureStackTrace)
|
|
910
|
+
Error.captureStackTrace(e, err);
|
|
911
|
+
if (!nt)
|
|
912
|
+
throw e;
|
|
913
|
+
return e;
|
|
914
|
+
};
|
|
915
|
+
// expands raw DEFLATE data
|
|
916
|
+
var inflt = function (dat, st, buf, dict) {
|
|
917
|
+
// source length dict length
|
|
918
|
+
var sl = dat.length, dl = 0;
|
|
919
|
+
if (!sl || st.f && !st.l)
|
|
920
|
+
return buf || new u8(0);
|
|
921
|
+
var noBuf = !buf;
|
|
922
|
+
// have to estimate size
|
|
923
|
+
var resize = noBuf || st.i != 2;
|
|
924
|
+
// no state
|
|
925
|
+
var noSt = st.i;
|
|
926
|
+
// Assumes roughly 33% compression ratio average
|
|
927
|
+
if (noBuf)
|
|
928
|
+
buf = new u8(sl * 3);
|
|
929
|
+
// ensure buffer can fit at least l elements
|
|
930
|
+
var cbuf = function (l) {
|
|
931
|
+
var bl = buf.length;
|
|
932
|
+
// need to increase size to fit
|
|
933
|
+
if (l > bl) {
|
|
934
|
+
// Double or set to necessary, whichever is greater
|
|
935
|
+
var nbuf = new u8(Math.max(bl * 2, l));
|
|
936
|
+
nbuf.set(buf);
|
|
937
|
+
buf = nbuf;
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
// last chunk bitpos bytes
|
|
941
|
+
var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
|
|
942
|
+
// total bits
|
|
943
|
+
var tbts = sl * 8;
|
|
944
|
+
do {
|
|
945
|
+
if (!lm) {
|
|
946
|
+
// BFINAL - this is only 1 when last chunk is next
|
|
947
|
+
final = bits(dat, pos, 1);
|
|
948
|
+
// type: 0 = no compression, 1 = fixed huffman, 2 = dynamic huffman
|
|
949
|
+
var type = bits(dat, pos + 1, 3);
|
|
950
|
+
pos += 3;
|
|
951
|
+
if (!type) {
|
|
952
|
+
// go to end of byte boundary
|
|
953
|
+
var s = shft(pos) + 4, l = dat[s - 4] | (dat[s - 3] << 8), t = s + l;
|
|
954
|
+
if (t > sl) {
|
|
955
|
+
if (noSt)
|
|
956
|
+
err(0);
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
// ensure size
|
|
960
|
+
if (resize)
|
|
961
|
+
cbuf(bt + l);
|
|
962
|
+
// Copy over uncompressed data
|
|
963
|
+
buf.set(dat.subarray(s, t), bt);
|
|
964
|
+
// Get new bitpos, update byte count
|
|
965
|
+
st.b = bt += l, st.p = pos = t * 8, st.f = final;
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
else if (type == 1)
|
|
969
|
+
lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
|
|
970
|
+
else if (type == 2) {
|
|
971
|
+
// literal lengths
|
|
972
|
+
var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
|
|
973
|
+
var tl = hLit + bits(dat, pos + 5, 31) + 1;
|
|
974
|
+
pos += 14;
|
|
975
|
+
// length+distance tree
|
|
976
|
+
var ldt = new u8(tl);
|
|
977
|
+
// code length tree
|
|
978
|
+
var clt = new u8(19);
|
|
979
|
+
for (var i = 0; i < hcLen; ++i) {
|
|
980
|
+
// use index map to get real code
|
|
981
|
+
clt[clim[i]] = bits(dat, pos + i * 3, 7);
|
|
982
|
+
}
|
|
983
|
+
pos += hcLen * 3;
|
|
984
|
+
// code lengths bits
|
|
985
|
+
var clb = max(clt), clbmsk = (1 << clb) - 1;
|
|
986
|
+
// code lengths map
|
|
987
|
+
var clm = hMap(clt, clb, 1);
|
|
988
|
+
for (var i = 0; i < tl;) {
|
|
989
|
+
var r = clm[bits(dat, pos, clbmsk)];
|
|
990
|
+
// bits read
|
|
991
|
+
pos += r & 15;
|
|
992
|
+
// symbol
|
|
993
|
+
var s = r >> 4;
|
|
994
|
+
// code length to copy
|
|
995
|
+
if (s < 16) {
|
|
996
|
+
ldt[i++] = s;
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
// copy count
|
|
1000
|
+
var c = 0, n = 0;
|
|
1001
|
+
if (s == 16)
|
|
1002
|
+
n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1];
|
|
1003
|
+
else if (s == 17)
|
|
1004
|
+
n = 3 + bits(dat, pos, 7), pos += 3;
|
|
1005
|
+
else if (s == 18)
|
|
1006
|
+
n = 11 + bits(dat, pos, 127), pos += 7;
|
|
1007
|
+
while (n--)
|
|
1008
|
+
ldt[i++] = c;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
// length tree distance tree
|
|
1012
|
+
var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
|
|
1013
|
+
// max length bits
|
|
1014
|
+
lbt = max(lt);
|
|
1015
|
+
// max dist bits
|
|
1016
|
+
dbt = max(dt);
|
|
1017
|
+
lm = hMap(lt, lbt, 1);
|
|
1018
|
+
dm = hMap(dt, dbt, 1);
|
|
1019
|
+
}
|
|
1020
|
+
else
|
|
1021
|
+
err(1);
|
|
1022
|
+
if (pos > tbts) {
|
|
1023
|
+
if (noSt)
|
|
1024
|
+
err(0);
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
// Make sure the buffer can hold this + the largest possible addition
|
|
1029
|
+
// Maximum chunk size (practically, theoretically infinite) is 2^17
|
|
1030
|
+
if (resize)
|
|
1031
|
+
cbuf(bt + 131072);
|
|
1032
|
+
var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
|
|
1033
|
+
var lpos = pos;
|
|
1034
|
+
for (;; lpos = pos) {
|
|
1035
|
+
// bits read, code
|
|
1036
|
+
var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
|
|
1037
|
+
pos += c & 15;
|
|
1038
|
+
if (pos > tbts) {
|
|
1039
|
+
if (noSt)
|
|
1040
|
+
err(0);
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
if (!c)
|
|
1044
|
+
err(2);
|
|
1045
|
+
if (sym < 256)
|
|
1046
|
+
buf[bt++] = sym;
|
|
1047
|
+
else if (sym == 256) {
|
|
1048
|
+
lpos = pos, lm = null;
|
|
1049
|
+
break;
|
|
1050
|
+
}
|
|
1051
|
+
else {
|
|
1052
|
+
var add = sym - 254;
|
|
1053
|
+
// no extra bits needed if less
|
|
1054
|
+
if (sym > 264) {
|
|
1055
|
+
// index
|
|
1056
|
+
var i = sym - 257, b = fleb[i];
|
|
1057
|
+
add = bits(dat, pos, (1 << b) - 1) + fl[i];
|
|
1058
|
+
pos += b;
|
|
1059
|
+
}
|
|
1060
|
+
// dist
|
|
1061
|
+
var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
|
|
1062
|
+
if (!d)
|
|
1063
|
+
err(3);
|
|
1064
|
+
pos += d & 15;
|
|
1065
|
+
var dt = fd[dsym];
|
|
1066
|
+
if (dsym > 3) {
|
|
1067
|
+
var b = fdeb[dsym];
|
|
1068
|
+
dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
|
|
1069
|
+
}
|
|
1070
|
+
if (pos > tbts) {
|
|
1071
|
+
if (noSt)
|
|
1072
|
+
err(0);
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
if (resize)
|
|
1076
|
+
cbuf(bt + 131072);
|
|
1077
|
+
var end = bt + add;
|
|
1078
|
+
if (bt < dt) {
|
|
1079
|
+
var shift = dl - dt, dend = Math.min(dt, end);
|
|
1080
|
+
if (shift + bt < 0)
|
|
1081
|
+
err(3);
|
|
1082
|
+
for (; bt < dend; ++bt)
|
|
1083
|
+
buf[bt] = dict[shift + bt];
|
|
1084
|
+
}
|
|
1085
|
+
for (; bt < end; ++bt)
|
|
1086
|
+
buf[bt] = buf[bt - dt];
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
st.l = lm, st.p = lpos, st.b = bt, st.f = final;
|
|
1090
|
+
if (lm)
|
|
1091
|
+
final = 1, st.m = lbt, st.d = dm, st.n = dbt;
|
|
1092
|
+
} while (!final);
|
|
1093
|
+
// don't reallocate for streams or user buffers
|
|
1094
|
+
return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
|
|
1095
|
+
};
|
|
1096
|
+
// empty
|
|
1097
|
+
var et = /*#__PURE__*/ new u8(0);
|
|
1098
|
+
// gzip footer: -8 to -4 = CRC, -4 to -0 is length
|
|
1099
|
+
// gzip start
|
|
1100
|
+
var gzs = function (d) {
|
|
1101
|
+
if (d[0] != 31 || d[1] != 139 || d[2] != 8)
|
|
1102
|
+
err(6, 'invalid gzip data');
|
|
1103
|
+
var flg = d[3];
|
|
1104
|
+
var st = 10;
|
|
1105
|
+
if (flg & 4)
|
|
1106
|
+
st += (d[10] | d[11] << 8) + 2;
|
|
1107
|
+
for (var zs = (flg >> 3 & 1) + (flg >> 4 & 1); zs > 0; zs -= !d[st++])
|
|
1108
|
+
;
|
|
1109
|
+
return st + (flg & 2);
|
|
1110
|
+
};
|
|
1111
|
+
// gzip length
|
|
1112
|
+
var gzl = function (d) {
|
|
1113
|
+
var l = d.length;
|
|
1114
|
+
return (d[l - 4] | d[l - 3] << 8 | d[l - 2] << 16 | d[l - 1] << 24) >>> 0;
|
|
1115
|
+
};
|
|
1116
|
+
// zlib start
|
|
1117
|
+
var zls = function (d, dict) {
|
|
1118
|
+
if ((d[0] & 15) != 8 || (d[0] >> 4) > 7 || ((d[0] << 8 | d[1]) % 31))
|
|
1119
|
+
err(6, 'invalid zlib data');
|
|
1120
|
+
if ((d[1] >> 5 & 1) == 1)
|
|
1121
|
+
err(6, 'invalid zlib data: ' + (d[1] & 32 ? 'need' : 'unexpected') + ' dictionary');
|
|
1122
|
+
return (d[1] >> 3 & 4) + 2;
|
|
1123
|
+
};
|
|
1124
|
+
/**
|
|
1125
|
+
* Expands DEFLATE data with no wrapper
|
|
1126
|
+
* @param data The data to decompress
|
|
1127
|
+
* @param opts The decompression options
|
|
1128
|
+
* @returns The decompressed version of the data
|
|
1129
|
+
*/
|
|
1130
|
+
function inflateSync(data, opts) {
|
|
1131
|
+
return inflt(data, { i: 2 }, opts, opts);
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Expands GZIP data
|
|
1135
|
+
* @param data The data to decompress
|
|
1136
|
+
* @param opts The decompression options
|
|
1137
|
+
* @returns The decompressed version of the data
|
|
1138
|
+
*/
|
|
1139
|
+
function gunzipSync(data, opts) {
|
|
1140
|
+
var st = gzs(data);
|
|
1141
|
+
if (st + 8 > data.length)
|
|
1142
|
+
err(6, 'invalid gzip data');
|
|
1143
|
+
return inflt(data.subarray(st, -8), { i: 2 }, new u8(gzl(data)), opts);
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Expands Zlib data
|
|
1147
|
+
* @param data The data to decompress
|
|
1148
|
+
* @param opts The decompression options
|
|
1149
|
+
* @returns The decompressed version of the data
|
|
1150
|
+
*/
|
|
1151
|
+
function unzlibSync(data, opts) {
|
|
1152
|
+
return inflt(data.subarray(zls(data), -4), { i: 2 }, opts, opts);
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Expands compressed GZIP, Zlib, or raw DEFLATE data, automatically detecting the format
|
|
1156
|
+
* @param data The data to decompress
|
|
1157
|
+
* @param opts The decompression options
|
|
1158
|
+
* @returns The decompressed version of the data
|
|
1159
|
+
*/
|
|
1160
|
+
function decompressSync(data, opts) {
|
|
1161
|
+
return (data[0] == 31 && data[1] == 139 && data[2] == 8)
|
|
1162
|
+
? gunzipSync(data, opts)
|
|
1163
|
+
: ((data[0] & 15) != 8 || (data[0] >> 4) > 7 || ((data[0] << 8 | data[1]) % 31))
|
|
1164
|
+
? inflateSync(data, opts)
|
|
1165
|
+
: unzlibSync(data, opts);
|
|
1166
|
+
}
|
|
1167
|
+
// text decoder
|
|
1168
|
+
var td = typeof TextDecoder != 'undefined' && /*#__PURE__*/ new TextDecoder();
|
|
1169
|
+
// text decoder stream
|
|
1170
|
+
var tds = 0;
|
|
1171
|
+
try {
|
|
1172
|
+
td.decode(et, { stream: true });
|
|
1173
|
+
tds = 1;
|
|
1174
|
+
}
|
|
1175
|
+
catch (e) { }
|
|
1176
|
+
|
|
1177
|
+
/* global globalThis */
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Provide a decompression implementation that acts on `buf` and returns decompressed data.
|
|
1181
|
+
*
|
|
1182
|
+
* Should use the native DecompressionStream on browsers, zlib on node.
|
|
1183
|
+
* Should throw if the compression algorithm is not supported.
|
|
1184
|
+
*/
|
|
1185
|
+
async function defaultDecompress(buf, compression) {
|
|
1186
|
+
if (compression === "none" || compression === undefined) {
|
|
1187
|
+
return buf;
|
|
1188
|
+
}
|
|
1189
|
+
if (compression === "gzip") {
|
|
1190
|
+
if (typeof globalThis.DecompressionStream === "undefined") {
|
|
1191
|
+
return decompressSync(new Uint8Array(buf));
|
|
1192
|
+
}
|
|
1193
|
+
const stream = new Response(buf).body;
|
|
1194
|
+
if (!stream) {
|
|
1195
|
+
throw new Error("Failed to read response stream");
|
|
1196
|
+
}
|
|
1197
|
+
const result = stream.pipeThrough(
|
|
1198
|
+
new globalThis.DecompressionStream("gzip"),
|
|
1199
|
+
);
|
|
1200
|
+
return new Response(result).arrayBuffer();
|
|
1201
|
+
}
|
|
1202
|
+
throw new Error("Compression method not supported");
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
async function getJsonFromFile(file, tilePackageFiles, source) {
|
|
1206
|
+
const decoder = new TextDecoder("utf-8");
|
|
1207
|
+
if (tilePackageFiles[file]) {
|
|
1208
|
+
const fileOffset = tilePackageFiles[file].absoluteOffset;
|
|
1209
|
+
const fileJSON = await source.getBytes(
|
|
1210
|
+
fileOffset,
|
|
1211
|
+
tilePackageFiles[file].size,
|
|
1212
|
+
);
|
|
1213
|
+
return JSON.parse(decoder.decode(fileJSON.data));
|
|
1214
|
+
}
|
|
1215
|
+
return {};
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function xmlToJson(xmlString) {
|
|
1219
|
+
const parser = new DOMParser();
|
|
1220
|
+
const xmlDoc = parser.parseFromString(xmlString, "application/xml");
|
|
1221
|
+
const obj = {};
|
|
1222
|
+
|
|
1223
|
+
function traverse(node, obj) {
|
|
1224
|
+
if (node.nodeType === 1) {
|
|
1225
|
+
// element
|
|
1226
|
+
const nodeName = node.nodeName;
|
|
1227
|
+
if (!obj[nodeName] && node.childNodes.length > 0) {
|
|
1228
|
+
obj[nodeName] = {};
|
|
1229
|
+
}
|
|
1230
|
+
if (node.childNodes.length == 1 && node.childNodes[0].nodeType == 3) {
|
|
1231
|
+
obj[nodeName] = node.childNodes[0].nodeValue.trim();
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
1235
|
+
traverse(node.childNodes[i], obj[nodeName]);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
traverse(xmlDoc.documentElement, obj);
|
|
1241
|
+
return obj;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Parse json into a Header object.
|
|
1246
|
+
*/
|
|
1247
|
+
function jsonToHeader(json, files, etag) {
|
|
1248
|
+
let spatialReference;
|
|
1249
|
+
try {
|
|
1250
|
+
spatialReference = json.tileInfo.spatialReference.latestWkid;
|
|
1251
|
+
// eslint-disable-next-line no-unused-vars
|
|
1252
|
+
} catch (e) {
|
|
1253
|
+
spatialReference = undefined;
|
|
1254
|
+
}
|
|
1255
|
+
return {
|
|
1256
|
+
type: json.type,
|
|
1257
|
+
name: json.name,
|
|
1258
|
+
description: json.serviceDescription,
|
|
1259
|
+
attribution: json.copyrightText,
|
|
1260
|
+
version: json.version,
|
|
1261
|
+
metadataOffset: json.metadataOffset || 0,
|
|
1262
|
+
metadataLength: json.metadataLength || 0,
|
|
1263
|
+
packageType: json.tileInfo.format ? "vtpk" : "tpkx",
|
|
1264
|
+
spatialReference: spatialReference,
|
|
1265
|
+
tileCompression: json.resourceInfo
|
|
1266
|
+
? json.resourceInfo.tileCompression || "none"
|
|
1267
|
+
: "none",
|
|
1268
|
+
tileType: json.tileInfo.format || json.tileImageInfo.format,
|
|
1269
|
+
tileSize: json.tileInfo.rows,
|
|
1270
|
+
minZoom: json.minZoom || json.minLOD,
|
|
1271
|
+
maxZoom: json.maxZoom || json.maxLOD,
|
|
1272
|
+
minLon: Number(json.extent.xmin),
|
|
1273
|
+
minLat: Number(json.extent.ymin),
|
|
1274
|
+
maxLon: Number(json.extent.xmax),
|
|
1275
|
+
maxLat: Number(json.extent.ymax),
|
|
1276
|
+
files: files,
|
|
1277
|
+
coverageMap: json.coverageMap,
|
|
1278
|
+
etag: etag,
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* Retrieve the header and root directory of a TilePackage archive.
|
|
1284
|
+
*
|
|
1285
|
+
* @param {Source} source - The source of the TilePackage archive.
|
|
1286
|
+
*/
|
|
1287
|
+
async function getHeaderAndFileList(source) {
|
|
1288
|
+
const key = source.getKey();
|
|
1289
|
+
const fileSize = await source.getSize();
|
|
1290
|
+
const resp = await source.getBytes(fileSize - 98, 98);
|
|
1291
|
+
let v = new DataView(resp.data, 0, 98);
|
|
1292
|
+
let entriesCentralDirectory, sizeCentralDirectory, offsetCentralDirectory;
|
|
1293
|
+
//let bigZip64 = false;
|
|
1294
|
+
if (v.getUint32(0, true) === 0x06064b50) {
|
|
1295
|
+
// This is a ZIP64 tilepackage
|
|
1296
|
+
//bigZip64 = true;
|
|
1297
|
+
entriesCentralDirectory = Number(v.getBigUint64(32, true));
|
|
1298
|
+
sizeCentralDirectory = Number(v.getBigUint64(40, true));
|
|
1299
|
+
offsetCentralDirectory = Number(v.getBigUint64(48, true));
|
|
1300
|
+
} else {
|
|
1301
|
+
v = new DataView(resp.data, 98 - 22, 22);
|
|
1302
|
+
if (v.getUint32(0, true) === 0x06054b50) {
|
|
1303
|
+
// This is a ordinary zip archive
|
|
1304
|
+
entriesCentralDirectory = Number(v.getUint16(10, true));
|
|
1305
|
+
sizeCentralDirectory = Number(v.getUint32(12, true));
|
|
1306
|
+
offsetCentralDirectory = Number(v.getUint32(16, true));
|
|
1307
|
+
} else {
|
|
1308
|
+
throw new Error("Wrong magic number for Zip archive");
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const centralDirectory = await source.getBytes(
|
|
1313
|
+
offsetCentralDirectory,
|
|
1314
|
+
sizeCentralDirectory,
|
|
1315
|
+
);
|
|
1316
|
+
|
|
1317
|
+
v = new DataView(centralDirectory.data, 0, sizeCentralDirectory);
|
|
1318
|
+
if (v.getUint32(0, true) !== 0x02014b50) {
|
|
1319
|
+
throw new Error("Wrong magic number for Central Directory archive");
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
let entryStart = 0;
|
|
1323
|
+
const tilePackageFiles = {};
|
|
1324
|
+
for (let i = 0; i < entriesCentralDirectory; i++) {
|
|
1325
|
+
if (entryStart >= sizeCentralDirectory) break;
|
|
1326
|
+
/*
|
|
1327
|
+
central file header signature 4 bytes (0x02014b50)
|
|
1328
|
+
version made by 2 bytes
|
|
1329
|
+
version needed to extract 2 bytes
|
|
1330
|
+
general purpose bit flag 2 bytes
|
|
1331
|
+
compression method 2 bytes
|
|
1332
|
+
last mod file time 2 bytes
|
|
1333
|
+
last mod file date 2 bytes
|
|
1334
|
+
crc-32 4 bytes
|
|
1335
|
+
compressed size 4 bytes
|
|
1336
|
+
uncompressed size 4 bytes
|
|
1337
|
+
file name length 2 bytes
|
|
1338
|
+
extra field length 2 bytes
|
|
1339
|
+
file comment length 2 bytes
|
|
1340
|
+
disk number start 2 bytes
|
|
1341
|
+
internal file attributes 2 bytes
|
|
1342
|
+
external file attributes 4 bytes
|
|
1343
|
+
relative offset of local header 4 bytes
|
|
1344
|
+
|
|
1345
|
+
file name (variable size)
|
|
1346
|
+
extra field (variable size)
|
|
1347
|
+
file comment (variable size)
|
|
1348
|
+
*/
|
|
1349
|
+
let sizeFile = v.getUint32(entryStart + 20, true);
|
|
1350
|
+
|
|
1351
|
+
const sizeFileName = v.getUint16(entryStart + 28, true);
|
|
1352
|
+
|
|
1353
|
+
const sizeExtraField = v.getUint16(entryStart + 30, true);
|
|
1354
|
+
|
|
1355
|
+
const sizeComment = v.getUint16(entryStart + 32, true);
|
|
1356
|
+
|
|
1357
|
+
let relativeOffset = v.getUint32(entryStart + 42, true);
|
|
1358
|
+
|
|
1359
|
+
const vFilename = new DataView(
|
|
1360
|
+
centralDirectory.data,
|
|
1361
|
+
entryStart + 46,
|
|
1362
|
+
sizeFileName,
|
|
1363
|
+
);
|
|
1364
|
+
const decoder = new TextDecoder("utf-8");
|
|
1365
|
+
const filename = decoder.decode(vFilename);
|
|
1366
|
+
|
|
1367
|
+
if (
|
|
1368
|
+
(sizeFile == 0xffffffff || relativeOffset == 0xffffffff) &&
|
|
1369
|
+
sizeExtraField > 0
|
|
1370
|
+
) {
|
|
1371
|
+
const vExtended = new DataView(
|
|
1372
|
+
centralDirectory.data,
|
|
1373
|
+
entryStart + 46 + sizeFileName,
|
|
1374
|
+
32,
|
|
1375
|
+
);
|
|
1376
|
+
if (vExtended.getUint16(0, true) == 0x0001) {
|
|
1377
|
+
/*
|
|
1378
|
+
Value Size Description
|
|
1379
|
+
----- ---- -----------
|
|
1380
|
+
(ZIP64) 0x0001 2 bytes Tag for this "extra" block type
|
|
1381
|
+
Size 2 bytes Size of this "extra" block
|
|
1382
|
+
Original
|
|
1383
|
+
Size 8 bytes Original uncompressed file size
|
|
1384
|
+
Compressed
|
|
1385
|
+
Size 8 bytes Size of compressed data
|
|
1386
|
+
Relative Header
|
|
1387
|
+
Offset 8 bytes Offset of local header record
|
|
1388
|
+
Disk Start
|
|
1389
|
+
Number 4 bytes Number of the disk on which
|
|
1390
|
+
this file starts
|
|
1391
|
+
*/
|
|
1392
|
+
|
|
1393
|
+
let j = 0;
|
|
1394
|
+
if (sizeFile == 0xffffffff) {
|
|
1395
|
+
sizeFile = Number(vExtended.getBigUint64(4, true));
|
|
1396
|
+
j += 8;
|
|
1397
|
+
}
|
|
1398
|
+
if (relativeOffset == 0xffffffff) {
|
|
1399
|
+
relativeOffset = Number(vExtended.getBigUint64(4 + j, true));
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
tilePackageFiles[filename] = {
|
|
1405
|
+
filename: filename,
|
|
1406
|
+
size: sizeFile,
|
|
1407
|
+
relativeOffset: relativeOffset,
|
|
1408
|
+
absoluteOffset: relativeOffset + 30 + filename.length,
|
|
1409
|
+
};
|
|
1410
|
+
entryStart += 46 + sizeFileName + sizeExtraField + sizeComment;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
let root = {};
|
|
1414
|
+
|
|
1415
|
+
if (key.indexOf(".tpkx") !== -1) {
|
|
1416
|
+
// TPKX
|
|
1417
|
+
root = await getJsonFromFile("root.json", tilePackageFiles, source);
|
|
1418
|
+
root.type = "tpkx";
|
|
1419
|
+
const iteminfo = await getJsonFromFile(
|
|
1420
|
+
"iteminfo.json",
|
|
1421
|
+
tilePackageFiles,
|
|
1422
|
+
source,
|
|
1423
|
+
);
|
|
1424
|
+
Object.keys(iteminfo).forEach((key) => {
|
|
1425
|
+
if (key !== "type") {
|
|
1426
|
+
root[key] = iteminfo[key];
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
} else {
|
|
1430
|
+
// VTPK
|
|
1431
|
+
root = await getJsonFromFile("p12/root.json", tilePackageFiles, source);
|
|
1432
|
+
root.type = "vtpk";
|
|
1433
|
+
|
|
1434
|
+
const iteminfoFile = "esriinfo/iteminfo.xml";
|
|
1435
|
+
if (tilePackageFiles[iteminfoFile]) {
|
|
1436
|
+
const iteminfoOffset = tilePackageFiles[iteminfoFile].absoluteOffset;
|
|
1437
|
+
|
|
1438
|
+
const iteminfoData = await source.getBytes(
|
|
1439
|
+
iteminfoOffset,
|
|
1440
|
+
tilePackageFiles[iteminfoFile].size,
|
|
1441
|
+
);
|
|
1442
|
+
const decoder = new TextDecoder("utf-8");
|
|
1443
|
+
const iteminfoString = decoder.decode(iteminfoData.data);
|
|
1444
|
+
const iteminfo = xmlToJson(iteminfoString);
|
|
1445
|
+
Object.keys(iteminfo.ESRI_ItemInformation).forEach((key) => {
|
|
1446
|
+
if (key !== "type") {
|
|
1447
|
+
root[key] = iteminfo.ESRI_ItemInformation[key];
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
if (tilePackageFiles["p12/metadata.json"]) {
|
|
1453
|
+
const metadataOffset =
|
|
1454
|
+
tilePackageFiles["p12/metadata.json"].absoluteOffset;
|
|
1455
|
+
root.metadataOffset = metadataOffset;
|
|
1456
|
+
root.metadataLength = tilePackageFiles["p12/metadata.json"].size;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
async function calculateCoverage(z, x, y, tilemap, coverageMap) {
|
|
1460
|
+
z = z + 1;
|
|
1461
|
+
x = x * 2;
|
|
1462
|
+
y = y * 2;
|
|
1463
|
+
if (coverageMap[z] == undefined) coverageMap[z] = {};
|
|
1464
|
+
if (coverageMap[z][x] == undefined) coverageMap[z][x] = {};
|
|
1465
|
+
if (coverageMap[z][x + 1] == undefined) coverageMap[z][x + 1] = {};
|
|
1466
|
+
coverageMap[z][x][y] = tilemap[0];
|
|
1467
|
+
if (isNaN(tilemap[0])) {
|
|
1468
|
+
calculateCoverage(z, x, y, tilemap[0], coverageMap);
|
|
1469
|
+
}
|
|
1470
|
+
coverageMap[z][x + 1][y] = tilemap[1];
|
|
1471
|
+
if (isNaN(tilemap[1])) {
|
|
1472
|
+
calculateCoverage(z, x + 1, y, tilemap[1], coverageMap);
|
|
1473
|
+
}
|
|
1474
|
+
coverageMap[z][x][y + 1] = tilemap[2];
|
|
1475
|
+
if (isNaN(tilemap[2])) {
|
|
1476
|
+
calculateCoverage(z, x, y + 1, tilemap[2], coverageMap);
|
|
1477
|
+
}
|
|
1478
|
+
coverageMap[z][x + 1][y + 1] = tilemap[3];
|
|
1479
|
+
if (isNaN(tilemap[3])) {
|
|
1480
|
+
calculateCoverage(z, x + 1, y + 1, tilemap[3], coverageMap);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
// Get and calculate the tilemap
|
|
1484
|
+
if (source.coverageCheck > 0 && tilePackageFiles["p12/tilemap/root.json"]) {
|
|
1485
|
+
const tilemap = await getJsonFromFile(
|
|
1486
|
+
"p12/tilemap/root.json",
|
|
1487
|
+
tilePackageFiles,
|
|
1488
|
+
source,
|
|
1489
|
+
);
|
|
1490
|
+
const coverageMap = {};
|
|
1491
|
+
coverageMap[0] = {};
|
|
1492
|
+
coverageMap[0][0] = {};
|
|
1493
|
+
coverageMap[0][0][0] = "blob";
|
|
1494
|
+
calculateCoverage(0, 0, 0, tilemap.index, coverageMap);
|
|
1495
|
+
root.coverageMap = coverageMap;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
const header = jsonToHeader(root, tilePackageFiles, resp.etag);
|
|
1500
|
+
return [header, ""];
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function deserializeIndex(dataView) {
|
|
1504
|
+
const index = [];
|
|
1505
|
+
|
|
1506
|
+
//const M = BigInt(Math.pow(2, 40));
|
|
1507
|
+
for (let row = 0; row < 128; row++) {
|
|
1508
|
+
index[row] = [];
|
|
1509
|
+
for (let col = 0; col < 128; col++) {
|
|
1510
|
+
const tileIndexOffset = 8 * (128 * (row % 128) + (col % 128));
|
|
1511
|
+
// eslint-disable-next-line no-undef
|
|
1512
|
+
let tileOffset = BigInt(0);
|
|
1513
|
+
for (let i = 4; i >= 0; i--) {
|
|
1514
|
+
// Start from the last byte for little-endian
|
|
1515
|
+
tileOffset =
|
|
1516
|
+
// eslint-disable-next-line no-undef
|
|
1517
|
+
(tileOffset << BigInt(8)) |
|
|
1518
|
+
// eslint-disable-next-line no-undef
|
|
1519
|
+
BigInt(dataView.getUint8(tileIndexOffset + i));
|
|
1520
|
+
}
|
|
1521
|
+
// eslint-disable-next-line no-undef
|
|
1522
|
+
let tileSize = BigInt(0);
|
|
1523
|
+
for (let i = 2; i >= 0; i--) {
|
|
1524
|
+
// Start from the last byte for little-endian
|
|
1525
|
+
tileSize =
|
|
1526
|
+
// eslint-disable-next-line no-undef
|
|
1527
|
+
(tileSize << BigInt(8)) |
|
|
1528
|
+
// eslint-disable-next-line no-undef
|
|
1529
|
+
BigInt(dataView.getUint8(tileIndexOffset + 5 + i));
|
|
1530
|
+
}
|
|
1531
|
+
if (tileSize > 0) {
|
|
1532
|
+
index[row][col] = {
|
|
1533
|
+
row: row,
|
|
1534
|
+
col: col,
|
|
1535
|
+
tileOffset: Number(tileOffset),
|
|
1536
|
+
tileSize: Number(tileSize),
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
return index;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
async function getResource(source, file, header, signal) {
|
|
1546
|
+
const resp = await source.getBytes(
|
|
1547
|
+
header.files[file].absoluteOffset,
|
|
1548
|
+
header.files[file].size,
|
|
1549
|
+
signal,
|
|
1550
|
+
header.etag,
|
|
1551
|
+
);
|
|
1552
|
+
return resp;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
async function getTileIndex(source, file, header, signal) {
|
|
1556
|
+
const offset = header.files[file].absoluteOffset + 64; // File header 64bytes
|
|
1557
|
+
const resp = await source.getBytes(
|
|
1558
|
+
offset,
|
|
1559
|
+
128 * 128 * 8,
|
|
1560
|
+
signal,
|
|
1561
|
+
header.etag,
|
|
1562
|
+
);
|
|
1563
|
+
const dataView = new DataView(resp.data, 0, 128 * 128 * 8);
|
|
1564
|
+
const directory = deserializeIndex(dataView);
|
|
1565
|
+
if (directory.length === 0) {
|
|
1566
|
+
throw new Error("Empty directory is invalid");
|
|
1567
|
+
}
|
|
1568
|
+
return directory;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/**
|
|
1572
|
+
* A cache for parts of a TilePackage archive where promises can be shared between requests.
|
|
1573
|
+
*
|
|
1574
|
+
* Only caches headers, resource files, and directories, not individual tile contents.
|
|
1575
|
+
*/
|
|
1576
|
+
class SharedPromiseCache {
|
|
1577
|
+
constructor(maxCacheEntries = 100) {
|
|
1578
|
+
this.cache = new Map();
|
|
1579
|
+
this.invalidations = new Map();
|
|
1580
|
+
this.maxCacheEntries = maxCacheEntries;
|
|
1581
|
+
this.counter = 1;
|
|
1582
|
+
//this.decompress = decompress;
|
|
1583
|
+
this.subdivided = new Map(); // key: sourceKey|z|x|y -> Uint8Array
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
async getHeader(source) {
|
|
1587
|
+
const cacheKey = source.getKey();
|
|
1588
|
+
const cacheValue = this.cache.get(cacheKey);
|
|
1589
|
+
if (cacheValue) {
|
|
1590
|
+
cacheValue.lastUsed = this.counter++;
|
|
1591
|
+
const data = await cacheValue.data;
|
|
1592
|
+
return data;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
const p = new Promise((resolve, reject) => {
|
|
1596
|
+
getHeaderAndFileList(source)
|
|
1597
|
+
.then((res) => {
|
|
1598
|
+
/*
|
|
1599
|
+
if (res[1]) {
|
|
1600
|
+
this.cache.set(res[1][0], {
|
|
1601
|
+
lastUsed: this.counter++,
|
|
1602
|
+
data: Promise.resolve(res[1][2]),
|
|
1603
|
+
});
|
|
1604
|
+
}*/
|
|
1605
|
+
resolve(res[0]);
|
|
1606
|
+
this.prune();
|
|
1607
|
+
})
|
|
1608
|
+
.catch((e) => {
|
|
1609
|
+
reject(e);
|
|
1610
|
+
});
|
|
1611
|
+
});
|
|
1612
|
+
this.cache.set(cacheKey, { lastUsed: this.counter++, data: p });
|
|
1613
|
+
return p;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
async getResource(source, file, header, signal) {
|
|
1617
|
+
const cacheKey = `${source.getKey()}|${header.etag || ""}|${file}|Resource`;
|
|
1618
|
+
const cacheValue = this.cache.get(cacheKey);
|
|
1619
|
+
if (cacheValue) {
|
|
1620
|
+
cacheValue.lastUsed = this.counter++;
|
|
1621
|
+
const data = await cacheValue.data;
|
|
1622
|
+
return data;
|
|
1623
|
+
}
|
|
1624
|
+
const p = new Promise((resolve, reject) => {
|
|
1625
|
+
getResource(source, file, header, signal)
|
|
1626
|
+
.then((resource) => {
|
|
1627
|
+
resolve(resource);
|
|
1628
|
+
this.prune();
|
|
1629
|
+
})
|
|
1630
|
+
.catch((e) => {
|
|
1631
|
+
reject(e);
|
|
1632
|
+
});
|
|
1633
|
+
});
|
|
1634
|
+
this.cache.set(cacheKey, { lastUsed: this.counter++, data: p });
|
|
1635
|
+
return p;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
async getTileIndex(source, file, header, signal) {
|
|
1639
|
+
const cacheKey = `${source.getKey()}|${
|
|
1640
|
+
header.etag || ""
|
|
1641
|
+
}|${file}|TileIndex`;
|
|
1642
|
+
const cacheValue = this.cache.get(cacheKey);
|
|
1643
|
+
if (cacheValue) {
|
|
1644
|
+
cacheValue.lastUsed = this.counter++;
|
|
1645
|
+
const data = await cacheValue.data;
|
|
1646
|
+
return data;
|
|
1647
|
+
}
|
|
1648
|
+
const p = new Promise((resolve, reject) => {
|
|
1649
|
+
getTileIndex(source, file, header, signal)
|
|
1650
|
+
.then((directory) => {
|
|
1651
|
+
resolve(directory);
|
|
1652
|
+
this.prune();
|
|
1653
|
+
})
|
|
1654
|
+
.catch((e) => {
|
|
1655
|
+
reject(e);
|
|
1656
|
+
});
|
|
1657
|
+
});
|
|
1658
|
+
this.cache.set(cacheKey, { lastUsed: this.counter++, data: p });
|
|
1659
|
+
return p;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
prune() {
|
|
1663
|
+
if (this.cache.size >= this.maxCacheEntries) {
|
|
1664
|
+
let minUsed = Infinity;
|
|
1665
|
+
let minKey = undefined;
|
|
1666
|
+
this.cache.forEach((cacheValue, key) => {
|
|
1667
|
+
if (cacheValue.lastUsed < minUsed) {
|
|
1668
|
+
minUsed = cacheValue.lastUsed;
|
|
1669
|
+
minKey = key;
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
if (minKey) {
|
|
1673
|
+
this.cache.delete(minKey);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
getSubdivided(source, z, x, y) {
|
|
1679
|
+
const key = `${source.getKey()}|${z}|${x}|${y}|Subdivided`;
|
|
1680
|
+
return this.subdivided.get(key);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
setSubdivided(source, z, x, y, bytes) {
|
|
1684
|
+
const key = `${source.getKey()}|${z}|${x}|${y}|Subdivided`;
|
|
1685
|
+
if (!this.subdivided.has(key)) {
|
|
1686
|
+
this.subdivided.set(key, bytes);
|
|
1687
|
+
// Optional lightweight prune if huge:
|
|
1688
|
+
if (this.subdivided.size > this.maxCacheEntries * 2) {
|
|
1689
|
+
// Remove oldest half (no usage tracking kept; iterate arbitrary)
|
|
1690
|
+
let toRemove = Math.floor(this.subdivided.size / 2);
|
|
1691
|
+
for (const k of this.subdivided.keys()) {
|
|
1692
|
+
this.subdivided.delete(k);
|
|
1693
|
+
toRemove--;
|
|
1694
|
+
if (toRemove <= 0) break;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
async invalidate(source) {
|
|
1701
|
+
const key = source.getKey();
|
|
1702
|
+
if (this.invalidations.get(key)) {
|
|
1703
|
+
return await this.invalidations.get(key);
|
|
1704
|
+
}
|
|
1705
|
+
this.cache.delete(source.getKey());
|
|
1706
|
+
const p = new Promise((resolve, reject) => {
|
|
1707
|
+
this.getHeader(source)
|
|
1708
|
+
.then(() => {
|
|
1709
|
+
resolve();
|
|
1710
|
+
this.invalidations.delete(key);
|
|
1711
|
+
})
|
|
1712
|
+
.catch((e) => {
|
|
1713
|
+
reject(e);
|
|
1714
|
+
});
|
|
1715
|
+
});
|
|
1716
|
+
this.invalidations.set(key, p);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function calculateFilename(z, x, y, header) {
|
|
1721
|
+
const zoom = z.toString().padStart(2, "0");
|
|
1722
|
+
const baseRow = Math.floor(y / 128) * 128;
|
|
1723
|
+
const baseCol = Math.floor(x / 128) * 128;
|
|
1724
|
+
const rowHex = baseRow.toString(16).padStart(4, "0");
|
|
1725
|
+
const colHex = baseCol.toString(16).padStart(4, "0");
|
|
1726
|
+
let basePath = header.type === "tpkx" ? "tile" : "p12/tile";
|
|
1727
|
+
return `${basePath}/L${zoom}/R${rowHex}C${colHex}.bundle`;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function getTileInfoFromTileIndex(tileIndex, z, x, y) {
|
|
1731
|
+
const row = y % 128;
|
|
1732
|
+
const col = x % 128;
|
|
1733
|
+
return tileIndex[row][col];
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
class EtagMismatch extends Error {
|
|
1737
|
+
constructor(message) {
|
|
1738
|
+
super(message);
|
|
1739
|
+
this.name = "EtagMismatch";
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
class TilePackage {
|
|
1744
|
+
constructor(source, options) {
|
|
1745
|
+
// Default coverageCheck enabled; disable with coverageCheck:false
|
|
1746
|
+
this.coverageCheck = 1;
|
|
1747
|
+
if (options && options.coverageCheck === false) this.coverageCheck = 0;
|
|
1748
|
+
// Max dz guard (skip subdivision beyond this): default 8
|
|
1749
|
+
this.maxDz =
|
|
1750
|
+
options && typeof options.maxDz === "number" ? options.maxDz : 8;
|
|
1751
|
+
if (typeof source === "string") {
|
|
1752
|
+
const opts = this.coverageCheck
|
|
1753
|
+
? { coverageCheck: this.coverageCheck }
|
|
1754
|
+
: undefined;
|
|
1755
|
+
this.source = new FetchSource(source, opts);
|
|
1756
|
+
} else {
|
|
1757
|
+
this.source = source;
|
|
1758
|
+
// Propagate default coverageCheck to existing Source instances if undefined
|
|
1759
|
+
if (this.source && this.source.coverageCheck === undefined) {
|
|
1760
|
+
this.source.coverageCheck = this.coverageCheck;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
this.decompress =
|
|
1764
|
+
options && options.decompress ? options.decompress : defaultDecompress;
|
|
1765
|
+
this.cache =
|
|
1766
|
+
options && options.cache ? options.cache : new SharedPromiseCache();
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
async getHeader() {
|
|
1770
|
+
return await this.cache.getHeader(this.source);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
async getZxyAttempt(z, x, y, signal) {
|
|
1774
|
+
const header = await this.cache.getHeader(this.source);
|
|
1775
|
+
if (z < header.minZoom || z > header.maxZoom) return undefined;
|
|
1776
|
+
const file = calculateFilename(z, x, y, header);
|
|
1777
|
+
let tileInfo = null;
|
|
1778
|
+
if (header.files[file]) {
|
|
1779
|
+
const tileIndex = await this.cache.getTileIndex(
|
|
1780
|
+
this.source,
|
|
1781
|
+
file,
|
|
1782
|
+
header,
|
|
1783
|
+
signal,
|
|
1784
|
+
);
|
|
1785
|
+
tileInfo = getTileInfoFromTileIndex(tileIndex, z, x, y);
|
|
1786
|
+
if (tileInfo && tileInfo.tileSize > 0) {
|
|
1787
|
+
const tileOffset =
|
|
1788
|
+
header.files[file].absoluteOffset + tileInfo.tileOffset;
|
|
1789
|
+
const resp = await this.source.getBytes(
|
|
1790
|
+
tileOffset,
|
|
1791
|
+
tileInfo.tileSize,
|
|
1792
|
+
signal,
|
|
1793
|
+
header.etag,
|
|
1794
|
+
);
|
|
1795
|
+
const data = await this.decompress(resp.data, header.tileCompression);
|
|
1796
|
+
return { data, cacheControl: resp.cacheControl, expires: resp.expires };
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
if (header.packageType === "vtpk" && header.coverageMap) {
|
|
1800
|
+
// Ascend to find parent with value 1
|
|
1801
|
+
let pz = z,
|
|
1802
|
+
px = x,
|
|
1803
|
+
py = y,
|
|
1804
|
+
foundParent = null;
|
|
1805
|
+
while (pz > header.minZoom) {
|
|
1806
|
+
pz -= 1;
|
|
1807
|
+
px = Math.floor(px / 2);
|
|
1808
|
+
py = Math.floor(py / 2);
|
|
1809
|
+
const level = header.coverageMap[pz];
|
|
1810
|
+
const val = level && level[px] ? level[px][py] : undefined;
|
|
1811
|
+
if (val === 1) {
|
|
1812
|
+
foundParent = { pz, px, py };
|
|
1813
|
+
break;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (foundParent) {
|
|
1817
|
+
const parent = await this.getZxyAttempt(
|
|
1818
|
+
foundParent.pz,
|
|
1819
|
+
foundParent.px,
|
|
1820
|
+
foundParent.py,
|
|
1821
|
+
signal,
|
|
1822
|
+
);
|
|
1823
|
+
if (parent && parent.data) {
|
|
1824
|
+
const cached = this.cache.getSubdivided(this.source, z, x, y);
|
|
1825
|
+
if (cached) {
|
|
1826
|
+
return {
|
|
1827
|
+
data: cached,
|
|
1828
|
+
cacheControl: parent.cacheControl,
|
|
1829
|
+
expires: parent.expires,
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
const dz = z - foundParent.pz;
|
|
1833
|
+
if (dz > this.maxDz) {
|
|
1834
|
+
return undefined; // exceed hard cap
|
|
1835
|
+
}
|
|
1836
|
+
try {
|
|
1837
|
+
const { data: overzoomed } = subdivideVectorTile(
|
|
1838
|
+
parent.data,
|
|
1839
|
+
foundParent.pz,
|
|
1840
|
+
foundParent.px,
|
|
1841
|
+
foundParent.py,
|
|
1842
|
+
z,
|
|
1843
|
+
x,
|
|
1844
|
+
y,
|
|
1845
|
+
{ buffer: 128, maxDzWarn: this.maxDz - 1 },
|
|
1846
|
+
);
|
|
1847
|
+
this.cache.setSubdivided(this.source, z, x, y, overzoomed);
|
|
1848
|
+
return {
|
|
1849
|
+
data: overzoomed,
|
|
1850
|
+
cacheControl: parent.cacheControl,
|
|
1851
|
+
expires: parent.expires,
|
|
1852
|
+
};
|
|
1853
|
+
} catch (e) {
|
|
1854
|
+
console.warn(
|
|
1855
|
+
`[tilepackage subdivide] failed for z=${z} x=${x} y=${y}:`,
|
|
1856
|
+
e,
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
} else {
|
|
1860
|
+
console.warn(
|
|
1861
|
+
`[tilepackage subdivide] parent tile missing at z=${foundParent.pz} x=${foundParent.px} y=${foundParent.py}`,
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return undefined;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
async getZxy(z, x, y, signal) {
|
|
1870
|
+
try {
|
|
1871
|
+
return await this.getZxyAttempt(z, x, y, signal);
|
|
1872
|
+
} catch (e) {
|
|
1873
|
+
if (e instanceof EtagMismatch) {
|
|
1874
|
+
this.cache.invalidate(this.source);
|
|
1875
|
+
return await this.getZxyAttempt(z, x, y, signal);
|
|
1876
|
+
}
|
|
1877
|
+
throw e;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
async getMetadataAttempt() {
|
|
1882
|
+
const header = await this.cache.getHeader(this.source);
|
|
1883
|
+
let metadata = {};
|
|
1884
|
+
if (header.packageType === "vtpk") {
|
|
1885
|
+
const resp = await this.source.getBytes(
|
|
1886
|
+
header.jsonMetadataOffset,
|
|
1887
|
+
header.jsonMetadataLength,
|
|
1888
|
+
undefined,
|
|
1889
|
+
header.etag,
|
|
1890
|
+
);
|
|
1891
|
+
const decoder = new TextDecoder("utf-8");
|
|
1892
|
+
metadata = JSON.parse(decoder.decode(resp.data));
|
|
1893
|
+
}
|
|
1894
|
+
metadata.name = header.name;
|
|
1895
|
+
return metadata;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
async getMetadata() {
|
|
1899
|
+
try {
|
|
1900
|
+
return await this.getMetadataAttempt();
|
|
1901
|
+
} catch (e) {
|
|
1902
|
+
if (e instanceof EtagMismatch) {
|
|
1903
|
+
this.cache.invalidate(this.source);
|
|
1904
|
+
return await this.getMetadataAttempt();
|
|
1905
|
+
}
|
|
1906
|
+
throw e;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
async getResourceAttempt(file, signal) {
|
|
1911
|
+
const header = await this.cache.getHeader(this.source);
|
|
1912
|
+
if (!header.files[file]) return undefined;
|
|
1913
|
+
const resource = await this.cache.getResource(
|
|
1914
|
+
this.source,
|
|
1915
|
+
file,
|
|
1916
|
+
header,
|
|
1917
|
+
signal,
|
|
1918
|
+
);
|
|
1919
|
+
return {
|
|
1920
|
+
data: resource.data,
|
|
1921
|
+
cacheControl: resource.cacheControl,
|
|
1922
|
+
expires: resource.expires,
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
async getResource(file, signal) {
|
|
1927
|
+
try {
|
|
1928
|
+
return await this.getResourceAttempt(file, signal);
|
|
1929
|
+
} catch (e) {
|
|
1930
|
+
if (e instanceof EtagMismatch) {
|
|
1931
|
+
this.cache.invalidate(this.source);
|
|
1932
|
+
return await this.getResourceAttempt(file, signal);
|
|
1933
|
+
}
|
|
1934
|
+
throw e;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
async getStyleAttempt() {
|
|
1939
|
+
const header = await this.cache.getHeader(this.source);
|
|
1940
|
+
const sourceKey = this.source.getKey();
|
|
1941
|
+
if (header.packageType === "vtpk") {
|
|
1942
|
+
const metadata = await getJsonFromFile(
|
|
1943
|
+
"p12/root.json",
|
|
1944
|
+
header.files,
|
|
1945
|
+
this.source,
|
|
1946
|
+
);
|
|
1947
|
+
const style = await getJsonFromFile(
|
|
1948
|
+
"p12/resources/styles/root.json",
|
|
1949
|
+
header.files,
|
|
1950
|
+
this.source,
|
|
1951
|
+
);
|
|
1952
|
+
if (style.sources && style.sources.esri) {
|
|
1953
|
+
delete style.sources.esri.url;
|
|
1954
|
+
style.sources.esri.tiles = [`tilepackage://${sourceKey}/{z}/{x}/{y}`];
|
|
1955
|
+
style.sources.esri.type = style.sources.esri.type || "vector";
|
|
1956
|
+
style.sources.esri.minzoom = header.minZoom || 0;
|
|
1957
|
+
style.sources.esri.maxzoom = header.maxZoom || 22;
|
|
1958
|
+
}
|
|
1959
|
+
for (const k in style.sources) {
|
|
1960
|
+
const src = style.sources[k];
|
|
1961
|
+
if (
|
|
1962
|
+
src &&
|
|
1963
|
+
typeof src.url === "string" &&
|
|
1964
|
+
src.url.startsWith("tilepackage://")
|
|
1965
|
+
) {
|
|
1966
|
+
delete src.url;
|
|
1967
|
+
if (!src.tiles)
|
|
1968
|
+
src.tiles = [`tilepackage://${sourceKey}/{z}/{x}/{y}`];
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
if (metadata.copyrightText)
|
|
1972
|
+
style.sources.esri.attribution = metadata.copyrightText;
|
|
1973
|
+
style.glyphs = `tilepackage://${sourceKey}/{fontstack}/{range}`;
|
|
1974
|
+
style.sprite = `tilepackage://${sourceKey}/sprite`;
|
|
1975
|
+
if (this.debug)
|
|
1976
|
+
console.debug(
|
|
1977
|
+
"[tilepackage style] rewritten esri source",
|
|
1978
|
+
style.sources.esri,
|
|
1979
|
+
);
|
|
1980
|
+
return style;
|
|
1981
|
+
}
|
|
1982
|
+
return {
|
|
1983
|
+
version: 8,
|
|
1984
|
+
sources: {
|
|
1985
|
+
esri: {
|
|
1986
|
+
type: "raster",
|
|
1987
|
+
tileSize: header.tileSize || 256,
|
|
1988
|
+
tiles: [`tilepackage://${sourceKey}/{z}/{x}/{y}`],
|
|
1989
|
+
maxzoom: header.maxZoom || 22,
|
|
1990
|
+
minzoom: header.minZoom || 0,
|
|
1991
|
+
},
|
|
1992
|
+
},
|
|
1993
|
+
layers: [{ id: "tilepackageraster", type: "raster", source: "esri" }],
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
async getStyle() {
|
|
1998
|
+
try {
|
|
1999
|
+
return await this.getStyleAttempt();
|
|
2000
|
+
} catch (e) {
|
|
2001
|
+
if (e instanceof EtagMismatch) {
|
|
2002
|
+
this.cache.invalidate(this.source);
|
|
2003
|
+
return await this.getStyleAttempt();
|
|
2004
|
+
}
|
|
2005
|
+
throw e;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
async getTileJson(baseTilesUrl) {
|
|
2010
|
+
const header = await this.getHeader();
|
|
2011
|
+
const metadata = await this.getMetadata();
|
|
2012
|
+
const ext = header.tileType;
|
|
2013
|
+
const tileJson = {
|
|
2014
|
+
tilejson: "3.0.0",
|
|
2015
|
+
scheme: "xyz",
|
|
2016
|
+
tiles: [`${baseTilesUrl}/{z}/{x}/{y}${ext}`],
|
|
2017
|
+
name: metadata.name,
|
|
2018
|
+
version: header.version,
|
|
2019
|
+
bounds: [header.minLon, header.minLat, header.maxLon, header.maxLat],
|
|
2020
|
+
};
|
|
2021
|
+
if (metadata.vector_layers) tileJson.vector_layers = metadata.vector_layers;
|
|
2022
|
+
if (metadata.attribution) tileJson.attribution = metadata.attribution;
|
|
2023
|
+
if (metadata.description) tileJson.description = metadata.description;
|
|
2024
|
+
if (header.minZoom) tileJson.minzoom = header.minZoom;
|
|
2025
|
+
if (header.maxZoom) tileJson.maxzoom = header.maxZoom;
|
|
2026
|
+
return tileJson;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
export { FetchSource, FileSource, Protocol, Source, TilePackage };
|
|
2031
|
+
//# sourceMappingURL=index.esm.js.map
|