From a885f4098a3bc44eea065c67d5264c815bf99f67 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Wed, 3 May 2023 23:58:22 +0800 Subject: [PATCH] implement metadata and TileJSON-like responses for Cloudflare Workers [#169] --- serverless/cloudflare/src/index.test.ts | 22 ++++-- serverless/cloudflare/src/index.ts | 100 ++++++++++++++++++++---- 2 files changed, 100 insertions(+), 22 deletions(-) diff --git a/serverless/cloudflare/src/index.test.ts b/serverless/cloudflare/src/index.test.ts index 64fe012..5e9865e 100644 --- a/serverless/cloudflare/src/index.test.ts +++ b/serverless/cloudflare/src/index.test.ts @@ -31,9 +31,9 @@ test("parse tile default", () => { ({ name, tile } = tile_path("/foo/11/22/33.pbf")); assert.strictEqual(name, "foo"); - assert.strictEqual(tile[0], 11); - assert.strictEqual(tile[1], 22); - assert.strictEqual(tile[2], 33); + assert.strictEqual(tile![0], 11); + assert.strictEqual(tile![1], 22); + assert.strictEqual(tile![2], 33); }); test("parse tile path setting", () => { @@ -41,17 +41,17 @@ test("parse tile path setting", () => { "/foo/11/22/33.pbf", "/{name}/{z}/{y}/{x}.{ext}" ); - assert.strictEqual(tile[1], 33); - assert.strictEqual(tile[2], 22); + assert.strictEqual(tile![1], 33); + assert.strictEqual(tile![2], 22); assert.strictEqual(ext, "pbf"); ({ ok, name, tile, ext } = tile_path( "/tiles/foo/4/2/3.mvt", "/tiles/{name}/{z}/{x}/{y}.{ext}" )); assert.strictEqual(name, "foo"); - assert.strictEqual(tile[0], 4); - assert.strictEqual(tile[1], 2); - assert.strictEqual(tile[2], 3); + assert.strictEqual(tile![0], 4); + assert.strictEqual(tile![1], 2); + assert.strictEqual(tile![2], 3); assert.strictEqual(ext, "mvt"); }); @@ -70,3 +70,9 @@ test("parse tile path setting slash", () => { ); assert.strictEqual(name, "foo/bar"); }); + +test("parse tileset default", () => { + let { ok, name, tile, ext } = tile_path("/abcd.json"); + assert.strictEqual(ok, true); + assert.strictEqual("abcd", name); +}); \ No newline at end of file diff --git a/serverless/cloudflare/src/index.ts b/serverless/cloudflare/src/index.ts index 2a33f45..b67b60e 100644 --- a/serverless/cloudflare/src/index.ts +++ b/serverless/cloudflare/src/index.ts @@ -18,6 +18,7 @@ interface Env { ALLOWED_ORIGINS?: string; PMTILES_PATH?: string; TILE_PATH?: string; + TILESET_PATH?: string; CACHE_MAX_AGE?: number; } @@ -37,33 +38,57 @@ export const pmtiles_path = (name: string, setting?: string): string => { const TILE = /^\/(?[0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(?\d+)\/(?\d+)\/(?\d+).(?[a-z]+)$/; +const TILESET = /^\/(?[0-9a-zA-Z\/!\-_\.\*\'\(\)]+).json$/; + export const tile_path = ( path: string, - setting?: string + tile_setting?: string, + tileset_setting?: string ): { ok: boolean; name: string; - tile: [number, number, number]; + tile?: [number, number, number]; ext: string; } => { - let pattern = TILE; - if (setting) { + let tile_pattern = TILE; + if (tile_setting) { // escape regex - setting = setting.replace(/[.*+?^$()|[\]\\]/g, "\\$&"); - setting = setting.replace("{name}", "(?[0-9a-zA-Z/!-_.*'()]+)"); - setting = setting.replace("{z}", "(?\\d+)"); - setting = setting.replace("{x}", "(?\\d+)"); - setting = setting.replace("{y}", "(?\\d+)"); - setting = setting.replace("{ext}", "(?[a-z]+)"); - pattern = new RegExp(setting); + tile_setting = tile_setting.replace(/[.*+?^$()|[\]\\]/g, "\\$&"); + tile_setting = tile_setting.replace( + "{name}", + "(?[0-9a-zA-Z/!-_.*'()]+)" + ); + tile_setting = tile_setting.replace("{z}", "(?\\d+)"); + tile_setting = tile_setting.replace("{x}", "(?\\d+)"); + tile_setting = tile_setting.replace("{y}", "(?\\d+)"); + tile_setting = tile_setting.replace("{ext}", "(?[a-z]+)"); + tile_pattern = new RegExp(tile_setting); } - let match = path.match(pattern); + let tile_match = path.match(tile_pattern); - if (match) { - const g = match.groups!; + if (tile_match) { + const g = tile_match.groups!; return { ok: true, name: g.NAME, tile: [+g.Z, +g.X, +g.Y], ext: g.EXT }; } + + let tileset_pattern = TILESET; + if (tileset_setting) { + tileset_setting = tileset_setting.replace(/[.*+?^$()|[\]\\]/g, "\\$&"); + tileset_setting = tileset_setting.replace( + "{name}", + "(?[0-9a-zA-Z/!-_.*'()]+)" + ); + tileset_pattern = new RegExp(tileset_setting); + } + + let tileset_match = path.match(tileset_pattern); + + if (tileset_match) { + const g = tileset_match.groups!; + return { ok: true, name: g.NAME, ext: "json" }; + } + return { ok: false, name: "", tile: [0, 0, 0], ext: "" }; }; @@ -184,6 +209,53 @@ export default { const p = new PMTiles(source, CACHE, nativeDecompress); try { const p_header = await p.getHeader(); + + if (!tile) { + const metadata = await p.getMetadata(); + cacheable_headers.set("Content-Type", "application/json"); + + let ext = ""; + if (p_header.tileType === TileType.Mvt) { + ext = ".mvt"; + } else if (p_header.tileType === TileType.Png) { + ext = ".png"; + } else if (p_header.tileType === TileType.Jpeg) { + ext = ".jpg"; + } else if (p_header.tileType === TileType.Webp) { + ext = ".webp"; + } + + // TODO: this needs to be based on the TILE_PATH setting + metadata.tiles = [ + url.protocol + + "//" + + url.hostname + + "/" + + name + + "/{z}/{x}/{y}" + + ext, + ]; + metadata.bounds = [ + p_header.minLon, + p_header.minLat, + p_header.maxLon, + p_header.maxLat, + ]; + metadata.center = [ + p_header.centerLon, + p_header.centerLat, + p_header.centerZoom, + ]; + metadata.maxzoom = p_header.maxZoom; + metadata.minzoom = p_header.minZoom; + metadata.scheme = "xyz"; + return cacheableResponse( + JSON.stringify(metadata), + cacheable_headers, + 200 + ); + } + if (tile[0] < p_header.minZoom || tile[0] > p_header.maxZoom) { return cacheableResponse(undefined, cacheable_headers, 404); }