maplibre-tilepackage-protocol 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -2,7 +2,6 @@ import { VectorTile, VectorTileFeature, VectorTileLayer } from '@mapbox/vector-t
2
2
  import Protobuf from 'pbf';
3
3
  import Point from '@mapbox/point-geometry';
4
4
  import { fromVectorTileJs } from '@maplibre/vt-pbf';
5
- import protocolGlyphs from 'maplibre-local-glyphs';
6
5
 
7
6
  function stringEndsIn(string, endings) {
8
7
  for (let i = 0; i < endings.length; i++) {
@@ -175,14 +174,6 @@ class Protocol {
175
174
  };
176
175
  }
177
176
 
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
177
  return { data: null };
187
178
  }
188
179
  }
@@ -281,9 +272,11 @@ class FileSource extends Source {
281
272
  constructor(file, options) {
282
273
  super();
283
274
  this.file = file;
275
+ this.coverageCheck = 1;
284
276
  if (options && options.coverageCheck) {
285
277
  this.coverageCheck = options.coverageCheck;
286
278
  }
279
+ //console.log("Tilepackage FileSource initialized", this.file);
287
280
  }
288
281
 
289
282
  getKey() {
@@ -316,18 +309,9 @@ class FetchSource extends Source {
316
309
  this.url = url;
317
310
  this.customHeaders = customHeaders;
318
311
  this.mustReload = false;
319
- // Safe global detection for legacy environments without globalThis
320
- const g =
321
- typeof globalThis !== "undefined"
322
- ? globalThis
323
- : typeof window !== "undefined"
324
- ? window
325
- : typeof self !== "undefined"
326
- ? self
327
- : {};
328
312
  let userAgent = "";
329
- if (g && typeof g.navigator !== "undefined") {
330
- userAgent = g.navigator.userAgent || "";
313
+ if ("navigator" in globalThis) {
314
+ userAgent = globalThis.navigator.userAgent || "";
331
315
  }
332
316
  const isWindows = userAgent.indexOf("Windows") > -1;
333
317
  const isChromiumBased = /Chrome|Chromium|Edg|OPR|Brave/.test(userAgent);
@@ -335,6 +319,7 @@ class FetchSource extends Source {
335
319
  if (isWindows && isChromiumBased) {
336
320
  this.chromeWindowsNoCache = true;
337
321
  }
322
+ this.coverageCheck = 1;
338
323
  if (options && options.coverageCheck) {
339
324
  this.coverageCheck = options.coverageCheck;
340
325
  }
@@ -902,7 +887,7 @@ var ec = [
902
887
  'invalid distance',
903
888
  'stream finished',
904
889
  'no stream handler',
905
- ,
890
+ , // determined by compression function
906
891
  'no callback',
907
892
  'invalid UTF-8 data',
908
893
  'extra field too long',
@@ -1130,33 +1115,15 @@ var zls = function (d, dict) {
1130
1115
  err(6, 'invalid zlib data: ' + (d[1] & 32 ? 'need' : 'unexpected') + ' dictionary');
1131
1116
  return (d[1] >> 3 & 4) + 2;
1132
1117
  };
1133
- /**
1134
- * Expands DEFLATE data with no wrapper
1135
- * @param data The data to decompress
1136
- * @param opts The decompression options
1137
- * @returns The decompressed version of the data
1138
- */
1139
1118
  function inflateSync(data, opts) {
1140
1119
  return inflt(data, { i: 2 }, opts, opts);
1141
1120
  }
1142
- /**
1143
- * Expands GZIP data
1144
- * @param data The data to decompress
1145
- * @param opts The decompression options
1146
- * @returns The decompressed version of the data
1147
- */
1148
1121
  function gunzipSync(data, opts) {
1149
1122
  var st = gzs(data);
1150
1123
  if (st + 8 > data.length)
1151
1124
  err(6, 'invalid gzip data');
1152
1125
  return inflt(data.subarray(st, -8), { i: 2 }, new u8(gzl(data)), opts);
1153
1126
  }
1154
- /**
1155
- * Expands Zlib data
1156
- * @param data The data to decompress
1157
- * @param opts The decompression options
1158
- * @returns The decompressed version of the data
1159
- */
1160
1127
  function unzlibSync(data, opts) {
1161
1128
  return inflt(data.subarray(zls(data), -4), { i: 2 }, opts, opts);
1162
1129
  }
@@ -1196,41 +1163,420 @@ async function defaultDecompress(buf, compression) {
1196
1163
  return buf;
1197
1164
  }
1198
1165
  if (compression === "gzip") {
1199
- // Safe global detection for DecompressionStream feature test
1200
- const g =
1201
- typeof globalThis !== "undefined"
1202
- ? globalThis
1203
- : typeof window !== "undefined"
1204
- ? window
1205
- : typeof self !== "undefined"
1206
- ? self
1207
- : {};
1208
- if (typeof g.DecompressionStream === "undefined") {
1166
+ if (typeof globalThis.DecompressionStream === "undefined") {
1209
1167
  return decompressSync(new Uint8Array(buf));
1210
1168
  }
1211
1169
  const stream = new Response(buf).body;
1212
1170
  if (!stream) {
1213
1171
  throw new Error("Failed to read response stream");
1214
1172
  }
1215
- const result = stream.pipeThrough(new g.DecompressionStream("gzip"));
1173
+ const result = stream.pipeThrough(
1174
+ new globalThis.DecompressionStream("gzip"),
1175
+ );
1216
1176
  return new Response(result).arrayBuffer();
1217
1177
  }
1218
1178
  throw new Error("Compression method not supported");
1219
1179
  }
1220
1180
 
1221
- async function getJsonFromFile(file, tilePackageFiles, source) {
1181
+ async function getJsonFromZip(file, zipFiles, source) {
1182
+ //console.log("getJsonFromZip:", file);
1222
1183
  const decoder = new TextDecoder("utf-8");
1223
- if (tilePackageFiles[file]) {
1224
- const fileOffset = tilePackageFiles[file].absoluteOffset;
1225
- const fileJSON = await source.getBytes(
1226
- fileOffset,
1227
- tilePackageFiles[file].size,
1228
- );
1229
- return JSON.parse(decoder.decode(fileJSON.data));
1184
+ if (zipFiles[file]) {
1185
+ const fileOffset = zipFiles[file].absoluteOffset;
1186
+ const fileJSON = await source.getBytes(fileOffset, zipFiles[file].size);
1187
+ let text;
1188
+ try {
1189
+ text = decoder.decode(fileJSON.data);
1190
+ } catch (error) {
1191
+ throw new Error(
1192
+ "Failed to decode JSON bytes from " + file + ": " + error.message,
1193
+ );
1194
+ }
1195
+
1196
+ const trimmed = text.trim();
1197
+ if (!trimmed) {
1198
+ throw new Error("JSON file " + file + " is empty or whitespace only");
1199
+ }
1200
+
1201
+ const firstChar = trimmed[0];
1202
+ const lastChar = trimmed[trimmed.length - 1];
1203
+ const looksLikeJsonObject = firstChar === "{" && lastChar === "}";
1204
+ const looksLikeJsonArray = firstChar === "[" && lastChar === "]";
1205
+ if (!looksLikeJsonObject && !looksLikeJsonArray) {
1206
+ throw new Error(
1207
+ "File " +
1208
+ file +
1209
+ " does not appear to contain JSON (starts with '" +
1210
+ firstChar +
1211
+ "' and ends with '" +
1212
+ lastChar +
1213
+ "')",
1214
+ );
1215
+ }
1216
+
1217
+ try {
1218
+ return JSON.parse(trimmed);
1219
+ } catch (error) {
1220
+ throw new Error(
1221
+ "Failed to parse JSON from " + file + ": " + error.message,
1222
+ );
1223
+ }
1230
1224
  }
1225
+ console.warn(`File ${file} not found in Zip file`);
1231
1226
  return {};
1232
1227
  }
1233
1228
 
1229
+ const ZIP64_EOCD_SIGNATURE = 0x06064b50;
1230
+ const EOCD_SIGNATURE = 0x06054b50;
1231
+ const CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
1232
+ const LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
1233
+ const MIN_EOCD_SIZE = 22;
1234
+ const ZIP64_EOCD_SIZE = 56;
1235
+ const MAX_TAIL_SCAN = 65536; // 64KB
1236
+ const MAX_CENTRAL_DIR_SIZE = 500 * 1024 * 1024; // 500MB
1237
+ const MAX_DIRECTORY_ENTRIES = 1000000;
1238
+ const LOCAL_FILE_HEADER_BASE = 30;
1239
+
1240
+ function scanForSignature(view, signature, minSize) {
1241
+ for (let i = view.byteLength - minSize; i >= 0; i--) {
1242
+ if (view.getUint32(i, true) === signature) {
1243
+ return i;
1244
+ }
1245
+ }
1246
+ return -1;
1247
+ }
1248
+
1249
+ function parseZip64EndOfCentralDirectory(view, position) {
1250
+ return {
1251
+ entries: Number(view.getBigUint64(position + 32, true)),
1252
+ size: Number(view.getBigUint64(position + 40, true)),
1253
+ offset: Number(view.getBigUint64(position + 48, true)),
1254
+ };
1255
+ }
1256
+
1257
+ function parseEndOfCentralDirectory(view, position) {
1258
+ return {
1259
+ entries: view.getUint16(position + 10, true),
1260
+ size: view.getUint32(position + 12, true),
1261
+ offset: view.getUint32(position + 16, true),
1262
+ };
1263
+ }
1264
+
1265
+ async function readLocalFileHeader(source, offset, decoder) {
1266
+ const headerResp = await source.getBytes(offset, LOCAL_FILE_HEADER_BASE);
1267
+ if (headerResp.data.byteLength < LOCAL_FILE_HEADER_BASE) {
1268
+ throw new Error("Local file header truncated before 30 bytes");
1269
+ }
1270
+
1271
+ const headerView = new DataView(headerResp.data, 0, LOCAL_FILE_HEADER_BASE);
1272
+ const signature = headerView.getUint32(0, true);
1273
+ if (signature !== LOCAL_FILE_HEADER_SIGNATURE) {
1274
+ throw new Error(
1275
+ `Local file header signature mismatch (0x${signature.toString(16)})`,
1276
+ );
1277
+ }
1278
+
1279
+ const filenameLength = headerView.getUint16(26, true);
1280
+ const extraFieldLength = headerView.getUint16(28, true);
1281
+ const trailingLength = filenameLength + extraFieldLength;
1282
+ let filename = "";
1283
+
1284
+ if (filenameLength > 0) {
1285
+ const trailingResp = await source.getBytes(
1286
+ offset + LOCAL_FILE_HEADER_BASE,
1287
+ trailingLength,
1288
+ );
1289
+ if (trailingResp.data.byteLength < trailingLength) {
1290
+ throw new Error("Local header filename/extra data truncated");
1291
+ }
1292
+ const filenameView = new DataView(trailingResp.data, 0, filenameLength);
1293
+ filename = decoder.decode(filenameView);
1294
+ }
1295
+
1296
+ return {
1297
+ filename,
1298
+ filenameLength,
1299
+ extraFieldLength,
1300
+ dataOffset:
1301
+ offset + LOCAL_FILE_HEADER_BASE + filenameLength + extraFieldLength,
1302
+ };
1303
+ }
1304
+
1305
+ async function verifyLocalHeaderOffset({
1306
+ source,
1307
+ filename,
1308
+ relativeOffset,
1309
+ computedOffset,
1310
+ decoder,
1311
+ logger,
1312
+ }) {
1313
+ try {
1314
+ const localHeader = await readLocalFileHeader(
1315
+ source,
1316
+ relativeOffset,
1317
+ decoder,
1318
+ );
1319
+ if (localHeader.filename !== filename) {
1320
+ logger(
1321
+ `Local header filename mismatch for ${filename}: found ${localHeader.filename} at offset ${relativeOffset}`,
1322
+ );
1323
+ }
1324
+ if (localHeader.dataOffset !== computedOffset) {
1325
+ logger(
1326
+ `Data offset mismatch for ${filename}: calculated ${computedOffset}, actual ${localHeader.dataOffset}`,
1327
+ );
1328
+ }
1329
+ return localHeader;
1330
+ } catch (error) {
1331
+ logger(
1332
+ `Failed to verify local header for ${filename} at ${relativeOffset}: ${error.message}`,
1333
+ );
1334
+ return null;
1335
+ }
1336
+ }
1337
+
1338
+ async function getFilelistFromZip(source, options) {
1339
+ const resolvedOptions = {};
1340
+ const verifyOffsets =
1341
+ typeof resolvedOptions.verifyOffsets === "boolean"
1342
+ ? resolvedOptions.verifyOffsets
1343
+ : false;
1344
+ const verificationLogger =
1345
+ typeof resolvedOptions.verificationLogger === "function"
1346
+ ? resolvedOptions.verificationLogger
1347
+ : console.warn;
1348
+ const fileSize = await source.getSize();
1349
+ if (fileSize < MIN_EOCD_SIZE) {
1350
+ throw new Error(
1351
+ "File too small to be a valid ZIP archive (minimum 22 bytes)",
1352
+ );
1353
+ }
1354
+
1355
+ const tailReadSize = Math.min(MAX_TAIL_SCAN, fileSize);
1356
+ const tailOffset = fileSize - tailReadSize;
1357
+ const tailResp = await source.getBytes(tailOffset, tailReadSize);
1358
+ const tailView = new DataView(tailResp.data, 0, tailReadSize);
1359
+
1360
+ let entriesCentralDirectory;
1361
+ let sizeCentralDirectory;
1362
+ let offsetCentralDirectory;
1363
+
1364
+ const zip64Pos = scanForSignature(
1365
+ tailView,
1366
+ ZIP64_EOCD_SIGNATURE,
1367
+ ZIP64_EOCD_SIZE,
1368
+ );
1369
+ if (zip64Pos !== -1) {
1370
+ const zip64Data = parseZip64EndOfCentralDirectory(tailView, zip64Pos);
1371
+ entriesCentralDirectory = zip64Data.entries;
1372
+ sizeCentralDirectory = zip64Data.size;
1373
+ offsetCentralDirectory = zip64Data.offset;
1374
+ } else {
1375
+ const eocdPos = scanForSignature(tailView, EOCD_SIGNATURE, MIN_EOCD_SIZE);
1376
+ if (eocdPos === -1) {
1377
+ throw new Error("End of Central Directory record not found in zip");
1378
+ }
1379
+ const eocdData = parseEndOfCentralDirectory(tailView, eocdPos);
1380
+ entriesCentralDirectory = eocdData.entries;
1381
+ sizeCentralDirectory = eocdData.size;
1382
+ offsetCentralDirectory = eocdData.offset;
1383
+ }
1384
+
1385
+ if (sizeCentralDirectory > MAX_CENTRAL_DIR_SIZE) {
1386
+ throw new Error("Central directory size exceeds maximum allowed (500MB)");
1387
+ }
1388
+ if (entriesCentralDirectory > MAX_DIRECTORY_ENTRIES) {
1389
+ throw new Error("Too many entries in central directory (max 1000000)");
1390
+ }
1391
+
1392
+ if (offsetCentralDirectory < 0) {
1393
+ throw new Error("Central directory offset is negative, file is corrupted");
1394
+ }
1395
+
1396
+ const availableBytes = fileSize - offsetCentralDirectory;
1397
+ if (availableBytes < 0) {
1398
+ throw new Error("Central directory offset is beyond file size");
1399
+ }
1400
+
1401
+ let safeCentralDirectorySize = sizeCentralDirectory;
1402
+ if (safeCentralDirectorySize > availableBytes) {
1403
+ console.warn(
1404
+ "Central directory size extends past end of file, clamping to available bytes",
1405
+ );
1406
+ safeCentralDirectorySize = availableBytes;
1407
+ }
1408
+ if (safeCentralDirectorySize <= 0) {
1409
+ throw new Error(
1410
+ "Central directory size is zero or negative after validation",
1411
+ );
1412
+ }
1413
+
1414
+ const centralDirectory = await source.getBytes(
1415
+ offsetCentralDirectory,
1416
+ safeCentralDirectorySize,
1417
+ );
1418
+ const v = new DataView(centralDirectory.data, 0, safeCentralDirectorySize);
1419
+
1420
+ if (v.getUint32(0, true) !== CENTRAL_DIRECTORY_SIGNATURE) {
1421
+ throw new Error("Wrong magic number for Central Directory archive");
1422
+ }
1423
+
1424
+ const zipFiles = {};
1425
+ let entryStart = 0;
1426
+ const decoder = new TextDecoder("utf-8");
1427
+
1428
+ for (let i = 0; i < entriesCentralDirectory; i++) {
1429
+ if (entryStart + 46 > safeCentralDirectorySize) {
1430
+ break;
1431
+ }
1432
+
1433
+ const generalPurposeFlag = v.getUint16(entryStart + 8, true);
1434
+ const compressionMethod = v.getUint16(entryStart + 10, true);
1435
+ if (compressionMethod !== 0 && compressionMethod !== 8) {
1436
+ throw new Error(
1437
+ "Unsupported compression method: " +
1438
+ compressionMethod +
1439
+ " only STORE(0) and DEFLATE(8) are supported",
1440
+ );
1441
+ }
1442
+ let sizeFile = v.getUint32(entryStart + 20, true);
1443
+ const uncompressedSize = v.getUint32(entryStart + 24, true);
1444
+ const sizeFileName = v.getUint16(entryStart + 28, true);
1445
+ const sizeExtraField = v.getUint16(entryStart + 30, true);
1446
+ const sizeComment = v.getUint16(entryStart + 32, true);
1447
+ let relativeOffset = v.getUint32(entryStart + 42, true);
1448
+
1449
+ const headerLength = 46 + sizeFileName + sizeExtraField + sizeComment;
1450
+ if (entryStart + headerLength > safeCentralDirectorySize) {
1451
+ break;
1452
+ }
1453
+
1454
+ const filenameView = new DataView(
1455
+ centralDirectory.data,
1456
+ entryStart + 46,
1457
+ sizeFileName,
1458
+ );
1459
+ let filename;
1460
+ try {
1461
+ filename = decoder.decode(filenameView);
1462
+ } catch (error) {
1463
+ throw new Error(
1464
+ "Failed to decode filename in ZIP entry: " + error.message,
1465
+ );
1466
+ }
1467
+
1468
+ const zip64SizeOverflow = sizeFile === 0xffffffff;
1469
+ const zip64UncompressedOverflow = uncompressedSize === 0xffffffff;
1470
+ const zip64OffsetOverflow = relativeOffset === 0xffffffff;
1471
+ const hasZip64Marker =
1472
+ zip64SizeOverflow || zip64UncompressedOverflow || zip64OffsetOverflow;
1473
+ let localExtraFieldZip64Length = 0;
1474
+
1475
+ if (hasZip64Marker && sizeExtraField > 0) {
1476
+ const extraStart = entryStart + 46 + sizeFileName;
1477
+ const extraEnd = extraStart + sizeExtraField;
1478
+ if (extraEnd <= safeCentralDirectorySize) {
1479
+ const extraView = new DataView(
1480
+ centralDirectory.data,
1481
+ extraStart,
1482
+ sizeExtraField,
1483
+ );
1484
+ let extraOffset = 0;
1485
+ let iterations = 100;
1486
+ while (extraOffset + 4 <= sizeExtraField && iterations > 0) {
1487
+ iterations--;
1488
+ const headerId = extraView.getUint16(extraOffset, true);
1489
+ const dataSize = extraView.getUint16(extraOffset + 2, true);
1490
+
1491
+ if (headerId === 0x0001) {
1492
+ // APPNOTE.TXT (4.5.3) – local header ZIP64 field only stores
1493
+ // the compressed/uncompressed sizes when a 64-bit value is needed.
1494
+ const dataStart = extraOffset + 4;
1495
+ const dataEnd = dataStart + dataSize;
1496
+ let cursor = dataStart;
1497
+ let localZip64DataSize = 0;
1498
+
1499
+ if (zip64UncompressedOverflow) {
1500
+ if (cursor + 8 > dataEnd) {
1501
+ throw new Error(
1502
+ "ZIP64 extra field truncated while reading uncompressed size",
1503
+ );
1504
+ }
1505
+ extraView.getBigUint64(cursor, true);
1506
+ cursor += 8;
1507
+ localZip64DataSize += 8;
1508
+ }
1509
+
1510
+ if (zip64SizeOverflow) {
1511
+ if (cursor + 8 > dataEnd) {
1512
+ throw new Error(
1513
+ "ZIP64 extra field truncated while reading compressed size",
1514
+ );
1515
+ }
1516
+ sizeFile = Number(extraView.getBigUint64(cursor, true));
1517
+ cursor += 8;
1518
+ localZip64DataSize += 8;
1519
+ }
1520
+
1521
+ if (zip64OffsetOverflow) {
1522
+ if (cursor + 8 > dataEnd) {
1523
+ throw new Error(
1524
+ "ZIP64 extra field truncated while reading relative offset",
1525
+ );
1526
+ }
1527
+ relativeOffset = Number(extraView.getBigUint64(cursor, true));
1528
+ cursor += 8;
1529
+ }
1530
+
1531
+ if (localZip64DataSize > 0) {
1532
+ localExtraFieldZip64Length = 4 + localZip64DataSize;
1533
+ }
1534
+ break;
1535
+ }
1536
+
1537
+ if (dataSize === 0) {
1538
+ extraOffset += 4;
1539
+ } else {
1540
+ extraOffset += 4 + dataSize;
1541
+ }
1542
+ }
1543
+ }
1544
+ }
1545
+ let absoluteOffset =
1546
+ relativeOffset + 30 + sizeFileName + localExtraFieldZip64Length;
1547
+
1548
+ if (verifyOffsets) {
1549
+ const verification = await verifyLocalHeaderOffset({
1550
+ source,
1551
+ filename,
1552
+ relativeOffset,
1553
+ computedOffset: absoluteOffset,
1554
+ decoder,
1555
+ logger: verificationLogger,
1556
+ });
1557
+ if (verification && typeof verification.dataOffset === "number") {
1558
+ absoluteOffset = verification.dataOffset;
1559
+ }
1560
+ }
1561
+ zipFiles[filename] = {
1562
+ filename,
1563
+ size: sizeFile,
1564
+ generalPurposeFlag,
1565
+ compressionMethod,
1566
+ uncompressedSize,
1567
+ relativeOffset,
1568
+ absoluteOffset,
1569
+ };
1570
+
1571
+ entryStart += headerLength;
1572
+ }
1573
+
1574
+ return {
1575
+ files: zipFiles,
1576
+ etag: tailResp.etag,
1577
+ };
1578
+ }
1579
+
1234
1580
  function xmlToJson(xmlString) {
1235
1581
  const parser = new DOMParser();
1236
1582
  const xmlDoc = parser.parseFromString(xmlString, "application/xml");
@@ -1302,137 +1648,14 @@ function jsonToHeader(json, files, etag) {
1302
1648
  */
1303
1649
  async function getHeaderAndFileList(source) {
1304
1650
  const key = source.getKey();
1305
- const fileSize = await source.getSize();
1306
- const resp = await source.getBytes(fileSize - 98, 98);
1307
- let v = new DataView(resp.data, 0, 98);
1308
- let entriesCentralDirectory, sizeCentralDirectory, offsetCentralDirectory;
1309
- //let bigZip64 = false;
1310
- if (v.getUint32(0, true) === 0x06064b50) {
1311
- // This is a ZIP64 tilepackage
1312
- //bigZip64 = true;
1313
- entriesCentralDirectory = Number(v.getBigUint64(32, true));
1314
- sizeCentralDirectory = Number(v.getBigUint64(40, true));
1315
- offsetCentralDirectory = Number(v.getBigUint64(48, true));
1316
- } else {
1317
- v = new DataView(resp.data, 98 - 22, 22);
1318
- if (v.getUint32(0, true) === 0x06054b50) {
1319
- // This is a ordinary zip archive
1320
- entriesCentralDirectory = Number(v.getUint16(10, true));
1321
- sizeCentralDirectory = Number(v.getUint32(12, true));
1322
- offsetCentralDirectory = Number(v.getUint32(16, true));
1323
- } else {
1324
- throw new Error("Wrong magic number for Zip archive");
1325
- }
1326
- }
1327
-
1328
- const centralDirectory = await source.getBytes(
1329
- offsetCentralDirectory,
1330
- sizeCentralDirectory,
1331
- );
1332
-
1333
- v = new DataView(centralDirectory.data, 0, sizeCentralDirectory);
1334
- if (v.getUint32(0, true) !== 0x02014b50) {
1335
- throw new Error("Wrong magic number for Central Directory archive");
1336
- }
1337
-
1338
- let entryStart = 0;
1339
- const tilePackageFiles = {};
1340
- for (let i = 0; i < entriesCentralDirectory; i++) {
1341
- if (entryStart >= sizeCentralDirectory) break;
1342
- /*
1343
- central file header signature 4 bytes (0x02014b50)
1344
- version made by 2 bytes
1345
- version needed to extract 2 bytes
1346
- general purpose bit flag 2 bytes
1347
- compression method 2 bytes
1348
- last mod file time 2 bytes
1349
- last mod file date 2 bytes
1350
- crc-32 4 bytes
1351
- compressed size 4 bytes
1352
- uncompressed size 4 bytes
1353
- file name length 2 bytes
1354
- extra field length 2 bytes
1355
- file comment length 2 bytes
1356
- disk number start 2 bytes
1357
- internal file attributes 2 bytes
1358
- external file attributes 4 bytes
1359
- relative offset of local header 4 bytes
1360
-
1361
- file name (variable size)
1362
- extra field (variable size)
1363
- file comment (variable size)
1364
- */
1365
- let sizeFile = v.getUint32(entryStart + 20, true);
1366
-
1367
- const sizeFileName = v.getUint16(entryStart + 28, true);
1368
-
1369
- const sizeExtraField = v.getUint16(entryStart + 30, true);
1370
-
1371
- const sizeComment = v.getUint16(entryStart + 32, true);
1372
-
1373
- let relativeOffset = v.getUint32(entryStart + 42, true);
1374
-
1375
- const vFilename = new DataView(
1376
- centralDirectory.data,
1377
- entryStart + 46,
1378
- sizeFileName,
1379
- );
1380
- const decoder = new TextDecoder("utf-8");
1381
- const filename = decoder.decode(vFilename);
1382
-
1383
- if (
1384
- (sizeFile == 0xffffffff || relativeOffset == 0xffffffff) &&
1385
- sizeExtraField > 0
1386
- ) {
1387
- const vExtended = new DataView(
1388
- centralDirectory.data,
1389
- entryStart + 46 + sizeFileName,
1390
- 32,
1391
- );
1392
- if (vExtended.getUint16(0, true) == 0x0001) {
1393
- /*
1394
- Value Size Description
1395
- ----- ---- -----------
1396
- (ZIP64) 0x0001 2 bytes Tag for this "extra" block type
1397
- Size 2 bytes Size of this "extra" block
1398
- Original
1399
- Size 8 bytes Original uncompressed file size
1400
- Compressed
1401
- Size 8 bytes Size of compressed data
1402
- Relative Header
1403
- Offset 8 bytes Offset of local header record
1404
- Disk Start
1405
- Number 4 bytes Number of the disk on which
1406
- this file starts
1407
- */
1408
-
1409
- let j = 0;
1410
- if (sizeFile == 0xffffffff) {
1411
- sizeFile = Number(vExtended.getBigUint64(4, true));
1412
- j += 8;
1413
- }
1414
- if (relativeOffset == 0xffffffff) {
1415
- relativeOffset = Number(vExtended.getBigUint64(4 + j, true));
1416
- }
1417
- }
1418
- }
1419
-
1420
- tilePackageFiles[filename] = {
1421
- filename: filename,
1422
- size: sizeFile,
1423
- relativeOffset: relativeOffset,
1424
- absoluteOffset: relativeOffset + 30 + filename.length,
1425
- };
1426
- entryStart += 46 + sizeFileName + sizeExtraField + sizeComment;
1427
- }
1428
-
1651
+ const { files: tilePackageFiles, etag } =
1652
+ await getFilelistFromZip(source);
1429
1653
  let root = {};
1430
-
1431
1654
  if (key.indexOf(".tpkx") !== -1) {
1432
1655
  // TPKX
1433
- root = await getJsonFromFile("root.json", tilePackageFiles, source);
1656
+ root = await getJsonFromZip("root.json", tilePackageFiles, source);
1434
1657
  root.type = "tpkx";
1435
- const iteminfo = await getJsonFromFile(
1658
+ const iteminfo = await getJsonFromZip(
1436
1659
  "iteminfo.json",
1437
1660
  tilePackageFiles,
1438
1661
  source,
@@ -1444,7 +1667,7 @@ async function getHeaderAndFileList(source) {
1444
1667
  });
1445
1668
  } else {
1446
1669
  // VTPK
1447
- root = await getJsonFromFile("p12/root.json", tilePackageFiles, source);
1670
+ root = await getJsonFromZip("p12/root.json", tilePackageFiles, source);
1448
1671
  root.type = "vtpk";
1449
1672
 
1450
1673
  const iteminfoFile = "esriinfo/iteminfo.xml";
@@ -1498,7 +1721,7 @@ async function getHeaderAndFileList(source) {
1498
1721
  }
1499
1722
  // Get and calculate the tilemap
1500
1723
  if (source.coverageCheck > 0 && tilePackageFiles["p12/tilemap/root.json"]) {
1501
- const tilemap = await getJsonFromFile(
1724
+ const tilemap = await getJsonFromZip(
1502
1725
  "p12/tilemap/root.json",
1503
1726
  tilePackageFiles,
1504
1727
  source,
@@ -1512,7 +1735,7 @@ async function getHeaderAndFileList(source) {
1512
1735
  }
1513
1736
  }
1514
1737
 
1515
- const header = jsonToHeader(root, tilePackageFiles, resp.etag);
1738
+ const header = jsonToHeader(root, tilePackageFiles, etag);
1516
1739
  return [header, ""];
1517
1740
  }
1518
1741
 
@@ -1733,23 +1956,18 @@ class SharedPromiseCache {
1733
1956
  }
1734
1957
  }
1735
1958
 
1736
- /**
1737
- * Escape literal newlines in JSON text.
1738
- * Replaces actual newline characters with the JSON escape sequence.
1739
- */
1740
- function escapeJsonNewlines(text) {
1741
- // Replace literal newlines with JSON escape sequences
1742
- // This handles \r\n, \n, and \r
1743
- return text.replace(/\r\n/g, "\\n").replace(/[\r\n]/g, "\\n");
1744
- }
1745
-
1746
1959
  function calculateFilename(z, x, y, header) {
1747
1960
  const zoom = z.toString().padStart(2, "0");
1748
1961
  const baseRow = Math.floor(y / 128) * 128;
1749
1962
  const baseCol = Math.floor(x / 128) * 128;
1750
1963
  const rowHex = baseRow.toString(16).padStart(4, "0");
1751
1964
  const colHex = baseCol.toString(16).padStart(4, "0");
1752
- const basePath = header.type === "tpkx" ? "tile" : "p12/tile";
1965
+ let basePath;
1966
+ if (header.type === "tpkx") {
1967
+ basePath = `tile`;
1968
+ } else {
1969
+ basePath = `p12/tile`;
1970
+ }
1753
1971
  return `${basePath}/L${zoom}/R${rowHex}C${colHex}.bundle`;
1754
1972
  }
1755
1973
 
@@ -1759,6 +1977,10 @@ function getTileInfoFromTileIndex(tileIndex, z, x, y) {
1759
1977
  return tileIndex[row][col];
1760
1978
  }
1761
1979
 
1980
+ /**
1981
+ * Error thrown when a response for TilePackage over HTTP does not match previous, cached parts of the archive.
1982
+ * The default TilePackage implementation will catch this error once internally and retry a request.
1983
+ */
1762
1984
  class EtagMismatch extends Error {
1763
1985
  constructor(message) {
1764
1986
  super(message);
@@ -1771,25 +1993,23 @@ class TilePackage {
1771
1993
  // Default coverageCheck enabled; disable with coverageCheck:false
1772
1994
  this.coverageCheck = 1;
1773
1995
  if (options && options.coverageCheck === false) this.coverageCheck = 0;
1774
- // Max dz guard (skip subdivision beyond this): default 8
1775
- this.maxDz =
1776
- options && typeof options.maxDz === "number" ? options.maxDz : 8;
1777
1996
  if (typeof source === "string") {
1778
- const opts = this.coverageCheck
1779
- ? { coverageCheck: this.coverageCheck }
1780
- : undefined;
1781
- this.source = new FetchSource(source, opts);
1997
+ this.source = new FetchSource(source, this.coverageCheck);
1782
1998
  } else {
1783
1999
  this.source = source;
1784
- // Propagate default coverageCheck to existing Source instances if undefined
1785
- if (this.source && this.source.coverageCheck === undefined) {
1786
- this.source.coverageCheck = this.coverageCheck;
1787
- }
1788
2000
  }
1789
- this.decompress =
1790
- options && options.decompress ? options.decompress : defaultDecompress;
1791
- this.cache =
1792
- options && options.cache ? options.cache : new SharedPromiseCache();
2001
+
2002
+ if (options && options.decompress) {
2003
+ this.decompress = options.decompress;
2004
+ } else {
2005
+ this.decompress = defaultDecompress;
2006
+ }
2007
+
2008
+ if (options && options.cache) {
2009
+ this.cache = options.cache;
2010
+ } else {
2011
+ this.cache = new SharedPromiseCache();
2012
+ }
1793
2013
  }
1794
2014
 
1795
2015
  async getHeader() {
@@ -1906,21 +2126,21 @@ class TilePackage {
1906
2126
 
1907
2127
  async getMetadataAttempt() {
1908
2128
  const header = await this.cache.getHeader(this.source);
2129
+
1909
2130
  let metadata = {};
1910
2131
  if (header.packageType === "vtpk") {
1911
2132
  const resp = await this.source.getBytes(
1912
- header.metadataOffset,
1913
- header.metadataLength,
2133
+ header.jsonMetadataOffset,
2134
+ header.jsonMetadataLength,
1914
2135
  undefined,
1915
2136
  header.etag,
1916
2137
  );
2138
+
1917
2139
  const decoder = new TextDecoder("utf-8");
1918
- const jsonText = decoder.decode(resp.data);
1919
- console.log("test", jsonText);
1920
- console.log("escaped text", escapeJsonNewlines(jsonText));
1921
- metadata = JSON.parse(escapeJsonNewlines(jsonText));
2140
+ metadata = JSON.parse(decoder.decode(resp.data));
1922
2141
  }
1923
2142
  metadata.name = header.name;
2143
+
1924
2144
  return metadata;
1925
2145
  }
1926
2146
 
@@ -1938,7 +2158,10 @@ class TilePackage {
1938
2158
 
1939
2159
  async getResourceAttempt(file, signal) {
1940
2160
  const header = await this.cache.getHeader(this.source);
1941
- if (!header.files[file]) return undefined;
2161
+ if (!header.files[file]) {
2162
+ return undefined;
2163
+ }
2164
+
1942
2165
  const resource = await this.cache.getResource(
1943
2166
  this.source,
1944
2167
  file,
@@ -1968,59 +2191,46 @@ class TilePackage {
1968
2191
  const header = await this.cache.getHeader(this.source);
1969
2192
  const sourceKey = this.source.getKey();
1970
2193
  if (header.packageType === "vtpk") {
1971
- const metadata = await getJsonFromFile(
2194
+ const metadata = await getJsonFromZip(
1972
2195
  "p12/root.json",
1973
2196
  header.files,
1974
2197
  this.source,
1975
2198
  );
1976
- const style = await getJsonFromFile(
2199
+ const style = await getJsonFromZip(
1977
2200
  "p12/resources/styles/root.json",
1978
2201
  header.files,
1979
2202
  this.source,
1980
2203
  );
1981
- if (style.sources && style.sources.esri) {
1982
- delete style.sources.esri.url;
1983
- style.sources.esri.tiles = [`tilepackage://${sourceKey}/{z}/{x}/{y}`];
1984
- style.sources.esri.type = style.sources.esri.type || "vector";
1985
- style.sources.esri.minzoom = header.minZoom || 0;
1986
- style.sources.esri.maxzoom = header.maxZoom || 22;
1987
- }
1988
- for (const k in style.sources) {
1989
- const src = style.sources[k];
1990
- if (
1991
- src &&
1992
- typeof src.url === "string" &&
1993
- src.url.startsWith("tilepackage://")
1994
- ) {
1995
- delete src.url;
1996
- if (!src.tiles)
1997
- src.tiles = [`tilepackage://${sourceKey}/{z}/{x}/{y}`];
1998
- }
1999
- }
2000
- if (metadata.copyrightText)
2204
+ style.sources.esri.url = `tilepackage://${sourceKey}`;
2205
+ style.sources.esri.minzoom = 0; //header.maxZoom || 22;
2206
+ style.sources.esri.maxzoom = header.maxZoom || 22;
2207
+ if (metadata.copyrightText) {
2001
2208
  style.sources.esri.attribution = metadata.copyrightText;
2209
+ }
2002
2210
  style.glyphs = `tilepackage://${sourceKey}/{fontstack}/{range}`;
2003
2211
  style.sprite = `tilepackage://${sourceKey}/sprite`;
2004
- if (this.debug)
2005
- console.debug(
2006
- "[tilepackage style] rewritten esri source",
2007
- style.sources.esri,
2008
- );
2009
2212
  return style;
2010
- }
2011
- return {
2012
- version: 8,
2013
- sources: {
2014
- esri: {
2015
- type: "raster",
2016
- tileSize: header.tileSize || 256,
2017
- tiles: [`tilepackage://${sourceKey}/{z}/{x}/{y}`],
2018
- maxzoom: header.maxZoom || 22,
2019
- minzoom: header.minZoom || 0,
2213
+ } else {
2214
+ const style = {
2215
+ version: 8,
2216
+ sources: {
2217
+ esri: {
2218
+ type: "raster",
2219
+ tileSize: header.tileSize || 256,
2220
+ url: `tilepackage://${sourceKey}`,
2221
+ maxzoom: header.maxZoom || 22,
2222
+ },
2020
2223
  },
2021
- },
2022
- layers: [{ id: "tilepackageraster", type: "raster", source: "esri" }],
2023
- };
2224
+ layers: [
2225
+ {
2226
+ id: "tilepackageraster",
2227
+ type: "raster",
2228
+ source: "esri",
2229
+ },
2230
+ ],
2231
+ };
2232
+ return style;
2233
+ }
2024
2234
  }
2025
2235
 
2026
2236
  async getStyle() {
@@ -2039,6 +2249,7 @@ class TilePackage {
2039
2249
  const header = await this.getHeader();
2040
2250
  const metadata = await this.getMetadata();
2041
2251
  const ext = header.tileType;
2252
+
2042
2253
  const tileJson = {
2043
2254
  tilejson: "3.0.0",
2044
2255
  scheme: "xyz",
@@ -2047,11 +2258,21 @@ class TilePackage {
2047
2258
  version: header.version,
2048
2259
  bounds: [header.minLon, header.minLat, header.maxLon, header.maxLat],
2049
2260
  };
2050
- if (metadata.vector_layers) tileJson.vector_layers = metadata.vector_layers;
2051
- if (metadata.attribution) tileJson.attribution = metadata.attribution;
2052
- if (metadata.description) tileJson.description = metadata.description;
2053
- if (header.minZoom) tileJson.minzoom = header.minZoom;
2054
- if (header.maxZoom) tileJson.maxzoom = header.maxZoom;
2261
+ if (metadata.vector_layers) {
2262
+ tileJson.vector_layers = metadata.vector_layers;
2263
+ }
2264
+ if (metadata.attribution) {
2265
+ tileJson.attribution = metadata.attribution;
2266
+ }
2267
+ if (metadata.description) {
2268
+ tileJson.description = metadata.description;
2269
+ }
2270
+ if (header.minZoom) {
2271
+ tileJson.minzoom = header.minZoom;
2272
+ }
2273
+ if (header.maxZoom) {
2274
+ tileJson.maxzoom = header.maxZoom;
2275
+ }
2055
2276
  return tileJson;
2056
2277
  }
2057
2278
  }