maplibre-tilepackage-protocol 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +4 -1
- package/dist/bundle.js +4314 -5313
- package/dist/bundle.js.map +1 -1
- package/dist/index.esm.js +478 -228
- package/dist/index.esm.js.map +1 -1
- package/eslint.config.mjs +28 -0
- package/package.json +42 -43
- package/rollup.config.mjs +0 -1
- package/src/get-header-and-filelist.js +5 -129
- package/src/get-json-from-file.js +1 -12
- package/src/index.js +72 -61
- package/src/maplibre-gl-js-protocol.js +0 -9
- package/src/source.js +3 -0
- package/src/zip/get-filelist.js +350 -0
- package/src/zip/get-json-from-file.js +47 -0
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() {
|
|
@@ -326,6 +319,7 @@ class FetchSource extends Source {
|
|
|
326
319
|
if (isWindows && isChromiumBased) {
|
|
327
320
|
this.chromeWindowsNoCache = true;
|
|
328
321
|
}
|
|
322
|
+
this.coverageCheck = 1;
|
|
329
323
|
if (options && options.coverageCheck) {
|
|
330
324
|
this.coverageCheck = options.coverageCheck;
|
|
331
325
|
}
|
|
@@ -893,7 +887,7 @@ var ec = [
|
|
|
893
887
|
'invalid distance',
|
|
894
888
|
'stream finished',
|
|
895
889
|
'no stream handler',
|
|
896
|
-
,
|
|
890
|
+
, // determined by compression function
|
|
897
891
|
'no callback',
|
|
898
892
|
'invalid UTF-8 data',
|
|
899
893
|
'extra field too long',
|
|
@@ -1121,33 +1115,15 @@ var zls = function (d, dict) {
|
|
|
1121
1115
|
err(6, 'invalid zlib data: ' + (d[1] & 32 ? 'need' : 'unexpected') + ' dictionary');
|
|
1122
1116
|
return (d[1] >> 3 & 4) + 2;
|
|
1123
1117
|
};
|
|
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
1118
|
function inflateSync(data, opts) {
|
|
1131
1119
|
return inflt(data, { i: 2 }, opts, opts);
|
|
1132
1120
|
}
|
|
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
1121
|
function gunzipSync(data, opts) {
|
|
1140
1122
|
var st = gzs(data);
|
|
1141
1123
|
if (st + 8 > data.length)
|
|
1142
1124
|
err(6, 'invalid gzip data');
|
|
1143
1125
|
return inflt(data.subarray(st, -8), { i: 2 }, new u8(gzl(data)), opts);
|
|
1144
1126
|
}
|
|
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
1127
|
function unzlibSync(data, opts) {
|
|
1152
1128
|
return inflt(data.subarray(zls(data), -4), { i: 2 }, opts, opts);
|
|
1153
1129
|
}
|
|
@@ -1202,19 +1178,405 @@ async function defaultDecompress(buf, compression) {
|
|
|
1202
1178
|
throw new Error("Compression method not supported");
|
|
1203
1179
|
}
|
|
1204
1180
|
|
|
1205
|
-
async function
|
|
1181
|
+
async function getJsonFromZip(file, zipFiles, source) {
|
|
1182
|
+
//console.log("getJsonFromZip:", file);
|
|
1206
1183
|
const decoder = new TextDecoder("utf-8");
|
|
1207
|
-
if (
|
|
1208
|
-
const fileOffset =
|
|
1209
|
-
const fileJSON = await source.getBytes(
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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
|
+
}
|
|
1214
1224
|
}
|
|
1225
|
+
console.warn(`File ${file} not found in Zip file`);
|
|
1215
1226
|
return {};
|
|
1216
1227
|
}
|
|
1217
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
|
+
|
|
1218
1580
|
function xmlToJson(xmlString) {
|
|
1219
1581
|
const parser = new DOMParser();
|
|
1220
1582
|
const xmlDoc = parser.parseFromString(xmlString, "application/xml");
|
|
@@ -1286,137 +1648,14 @@ function jsonToHeader(json, files, etag) {
|
|
|
1286
1648
|
*/
|
|
1287
1649
|
async function getHeaderAndFileList(source) {
|
|
1288
1650
|
const key = source.getKey();
|
|
1289
|
-
const
|
|
1290
|
-
|
|
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
|
-
|
|
1651
|
+
const { files: tilePackageFiles, etag } =
|
|
1652
|
+
await getFilelistFromZip(source);
|
|
1413
1653
|
let root = {};
|
|
1414
|
-
|
|
1415
1654
|
if (key.indexOf(".tpkx") !== -1) {
|
|
1416
1655
|
// TPKX
|
|
1417
|
-
root = await
|
|
1656
|
+
root = await getJsonFromZip("root.json", tilePackageFiles, source);
|
|
1418
1657
|
root.type = "tpkx";
|
|
1419
|
-
const iteminfo = await
|
|
1658
|
+
const iteminfo = await getJsonFromZip(
|
|
1420
1659
|
"iteminfo.json",
|
|
1421
1660
|
tilePackageFiles,
|
|
1422
1661
|
source,
|
|
@@ -1428,7 +1667,7 @@ async function getHeaderAndFileList(source) {
|
|
|
1428
1667
|
});
|
|
1429
1668
|
} else {
|
|
1430
1669
|
// VTPK
|
|
1431
|
-
root = await
|
|
1670
|
+
root = await getJsonFromZip("p12/root.json", tilePackageFiles, source);
|
|
1432
1671
|
root.type = "vtpk";
|
|
1433
1672
|
|
|
1434
1673
|
const iteminfoFile = "esriinfo/iteminfo.xml";
|
|
@@ -1482,7 +1721,7 @@ async function getHeaderAndFileList(source) {
|
|
|
1482
1721
|
}
|
|
1483
1722
|
// Get and calculate the tilemap
|
|
1484
1723
|
if (source.coverageCheck > 0 && tilePackageFiles["p12/tilemap/root.json"]) {
|
|
1485
|
-
const tilemap = await
|
|
1724
|
+
const tilemap = await getJsonFromZip(
|
|
1486
1725
|
"p12/tilemap/root.json",
|
|
1487
1726
|
tilePackageFiles,
|
|
1488
1727
|
source,
|
|
@@ -1496,7 +1735,7 @@ async function getHeaderAndFileList(source) {
|
|
|
1496
1735
|
}
|
|
1497
1736
|
}
|
|
1498
1737
|
|
|
1499
|
-
const header = jsonToHeader(root, tilePackageFiles,
|
|
1738
|
+
const header = jsonToHeader(root, tilePackageFiles, etag);
|
|
1500
1739
|
return [header, ""];
|
|
1501
1740
|
}
|
|
1502
1741
|
|
|
@@ -1723,7 +1962,12 @@ function calculateFilename(z, x, y, header) {
|
|
|
1723
1962
|
const baseCol = Math.floor(x / 128) * 128;
|
|
1724
1963
|
const rowHex = baseRow.toString(16).padStart(4, "0");
|
|
1725
1964
|
const colHex = baseCol.toString(16).padStart(4, "0");
|
|
1726
|
-
let basePath
|
|
1965
|
+
let basePath;
|
|
1966
|
+
if (header.type === "tpkx") {
|
|
1967
|
+
basePath = `tile`;
|
|
1968
|
+
} else {
|
|
1969
|
+
basePath = `p12/tile`;
|
|
1970
|
+
}
|
|
1727
1971
|
return `${basePath}/L${zoom}/R${rowHex}C${colHex}.bundle`;
|
|
1728
1972
|
}
|
|
1729
1973
|
|
|
@@ -1733,6 +1977,10 @@ function getTileInfoFromTileIndex(tileIndex, z, x, y) {
|
|
|
1733
1977
|
return tileIndex[row][col];
|
|
1734
1978
|
}
|
|
1735
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
|
+
*/
|
|
1736
1984
|
class EtagMismatch extends Error {
|
|
1737
1985
|
constructor(message) {
|
|
1738
1986
|
super(message);
|
|
@@ -1745,25 +1993,23 @@ class TilePackage {
|
|
|
1745
1993
|
// Default coverageCheck enabled; disable with coverageCheck:false
|
|
1746
1994
|
this.coverageCheck = 1;
|
|
1747
1995
|
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
1996
|
if (typeof source === "string") {
|
|
1752
|
-
|
|
1753
|
-
? { coverageCheck: this.coverageCheck }
|
|
1754
|
-
: undefined;
|
|
1755
|
-
this.source = new FetchSource(source, opts);
|
|
1997
|
+
this.source = new FetchSource(source, this.coverageCheck);
|
|
1756
1998
|
} else {
|
|
1757
1999
|
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
2000
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
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
|
+
}
|
|
1767
2013
|
}
|
|
1768
2014
|
|
|
1769
2015
|
async getHeader() {
|
|
@@ -1880,6 +2126,7 @@ class TilePackage {
|
|
|
1880
2126
|
|
|
1881
2127
|
async getMetadataAttempt() {
|
|
1882
2128
|
const header = await this.cache.getHeader(this.source);
|
|
2129
|
+
|
|
1883
2130
|
let metadata = {};
|
|
1884
2131
|
if (header.packageType === "vtpk") {
|
|
1885
2132
|
const resp = await this.source.getBytes(
|
|
@@ -1888,10 +2135,12 @@ class TilePackage {
|
|
|
1888
2135
|
undefined,
|
|
1889
2136
|
header.etag,
|
|
1890
2137
|
);
|
|
2138
|
+
|
|
1891
2139
|
const decoder = new TextDecoder("utf-8");
|
|
1892
2140
|
metadata = JSON.parse(decoder.decode(resp.data));
|
|
1893
2141
|
}
|
|
1894
2142
|
metadata.name = header.name;
|
|
2143
|
+
|
|
1895
2144
|
return metadata;
|
|
1896
2145
|
}
|
|
1897
2146
|
|
|
@@ -1909,7 +2158,10 @@ class TilePackage {
|
|
|
1909
2158
|
|
|
1910
2159
|
async getResourceAttempt(file, signal) {
|
|
1911
2160
|
const header = await this.cache.getHeader(this.source);
|
|
1912
|
-
if (!header.files[file])
|
|
2161
|
+
if (!header.files[file]) {
|
|
2162
|
+
return undefined;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
1913
2165
|
const resource = await this.cache.getResource(
|
|
1914
2166
|
this.source,
|
|
1915
2167
|
file,
|
|
@@ -1939,59 +2191,46 @@ class TilePackage {
|
|
|
1939
2191
|
const header = await this.cache.getHeader(this.source);
|
|
1940
2192
|
const sourceKey = this.source.getKey();
|
|
1941
2193
|
if (header.packageType === "vtpk") {
|
|
1942
|
-
const metadata = await
|
|
2194
|
+
const metadata = await getJsonFromZip(
|
|
1943
2195
|
"p12/root.json",
|
|
1944
2196
|
header.files,
|
|
1945
2197
|
this.source,
|
|
1946
2198
|
);
|
|
1947
|
-
const style = await
|
|
2199
|
+
const style = await getJsonFromZip(
|
|
1948
2200
|
"p12/resources/styles/root.json",
|
|
1949
2201
|
header.files,
|
|
1950
2202
|
this.source,
|
|
1951
2203
|
);
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
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)
|
|
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) {
|
|
1972
2208
|
style.sources.esri.attribution = metadata.copyrightText;
|
|
2209
|
+
}
|
|
1973
2210
|
style.glyphs = `tilepackage://${sourceKey}/{fontstack}/{range}`;
|
|
1974
2211
|
style.sprite = `tilepackage://${sourceKey}/sprite`;
|
|
1975
|
-
if (this.debug)
|
|
1976
|
-
console.debug(
|
|
1977
|
-
"[tilepackage style] rewritten esri source",
|
|
1978
|
-
style.sources.esri,
|
|
1979
|
-
);
|
|
1980
2212
|
return style;
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
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
|
+
},
|
|
1991
2223
|
},
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2224
|
+
layers: [
|
|
2225
|
+
{
|
|
2226
|
+
id: "tilepackageraster",
|
|
2227
|
+
type: "raster",
|
|
2228
|
+
source: "esri",
|
|
2229
|
+
},
|
|
2230
|
+
],
|
|
2231
|
+
};
|
|
2232
|
+
return style;
|
|
2233
|
+
}
|
|
1995
2234
|
}
|
|
1996
2235
|
|
|
1997
2236
|
async getStyle() {
|
|
@@ -2010,6 +2249,7 @@ class TilePackage {
|
|
|
2010
2249
|
const header = await this.getHeader();
|
|
2011
2250
|
const metadata = await this.getMetadata();
|
|
2012
2251
|
const ext = header.tileType;
|
|
2252
|
+
|
|
2013
2253
|
const tileJson = {
|
|
2014
2254
|
tilejson: "3.0.0",
|
|
2015
2255
|
scheme: "xyz",
|
|
@@ -2018,11 +2258,21 @@ class TilePackage {
|
|
|
2018
2258
|
version: header.version,
|
|
2019
2259
|
bounds: [header.minLon, header.minLat, header.maxLon, header.maxLat],
|
|
2020
2260
|
};
|
|
2021
|
-
if (metadata.vector_layers)
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
if (
|
|
2025
|
-
|
|
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
|
+
}
|
|
2026
2276
|
return tileJson;
|
|
2027
2277
|
}
|
|
2028
2278
|
}
|