maplibre-tilepackage-protocol 1.0.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.
@@ -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