diff --git a/serverless/cloudflare/src/index.test.ts b/serverless/cloudflare/src/index.test.ts index 380ef62..0956c48 100644 --- a/serverless/cloudflare/src/index.test.ts +++ b/serverless/cloudflare/src/index.test.ts @@ -1,17 +1,51 @@ import { test } from "zora"; -import { pmtiles_path } from "./index"; +import { pmtiles_path, tile_path } from "./index"; test("pmtiles path", (assertion) => { - let result = pmtiles_path(undefined, "foo"); + let result = pmtiles_path("foo", undefined); assertion.equal(result, "foo.pmtiles"); }); test("pmtiles path", (assertion) => { - let result = pmtiles_path("folder/{name}/file.pmtiles", "foo"); + let result = pmtiles_path("foo","folder/{name}/file.pmtiles"); assertion.equal(result, "folder/foo/file.pmtiles"); }); test("pmtiles path with slash", (assertion) => { - let result = pmtiles_path("folder/{name}/file.pmtiles", "foo/bar"); + let result = pmtiles_path("foo/bar","folder/{name}/file.pmtiles"); assertion.equal(result, "folder/foo/bar/file.pmtiles"); }); + +test("parse tile default", (assertion) => { + let {ok, name, tile, ext} = tile_path("abcd"); + assertion.equal(ok, false); + + ({name, tile} = tile_path("/foo/11/22/33.pbf")); + assertion.equal(name,"foo"); + assertion.equal(tile[0],11); + assertion.equal(tile[1],22); + assertion.equal(tile[2],33); +}); + +test("parse tile path setting", (assertion) => { + let {ok, name, tile, ext} = tile_path("/foo/11/22/33.pbf","/{name}/{z}/{y}/{x}.{ext}"); + assertion.equal(tile[1],33); + assertion.equal(tile[2],22); + assertion.equal(ext,"pbf"); + ({ok, name, tile, ext} = tile_path("/tiles/foo/4/2/3.mvt","/tiles/{name}/{z}/{x}/{y}.{ext}")); + assertion.equal(name,"foo"); + assertion.equal(tile[0],4); + assertion.equal(tile[1],2); + assertion.equal(tile[2],3); + assertion.equal(ext,"mvt"); +}); + +test("parse tile path setting special chars", (assertion) => { + let {ok, name, tile, ext} = tile_path("/folder(new/foo/11/22/33.pbf","/folder(new/{name}/{z}/{y}/{x}.{ext}"); + assertion.equal(name,"foo"); +}); + +test("parse tile path setting slash", (assertion) => { + let {ok, name, tile, ext} = tile_path("/foo/bar/11/22/33.pbf","/{name}/{z}/{y}/{x}.{ext}"); + assertion.equal(name,"foo/bar"); +}); diff --git a/serverless/cloudflare/src/index.ts b/serverless/cloudflare/src/index.ts index 499ba9d..dfc4b67 100644 --- a/serverless/cloudflare/src/index.ts +++ b/serverless/cloudflare/src/index.ts @@ -15,6 +15,7 @@ import { interface Env { BUCKET: R2Bucket; PMTILES_PATH?: string; + TILE_PATH?: string; } class KeyNotFoundError extends Error { @@ -23,17 +24,46 @@ class KeyNotFoundError extends Error { } } -const TILE = new RegExp( - /^\/([0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(\d+)\/(\d+)\/(\d+).([a-z]+)$/ -); - -export const pmtiles_path = (p: string | undefined, name: string): string => { - if (p) { - return p.replace("{name}", name); +export const pmtiles_path = (name: string, setting?: string): string => { + if (setting) { + return setting.replace("{name}", name); } return name + ".pmtiles"; }; +const TILE = + /^\/(?[0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(?\d+)\/(?\d+)\/(?\d+).(?[a-z]+)$/; + +export const tile_path = ( + path: string, + setting?: string +): { + ok: boolean; + name: string; + tile: [number, number, number]; + ext: string; +} => { + let pattern = TILE; + if (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); + } + + let match = path.match(pattern); + + if (match) { + const g = match.groups!; + return { ok: true, name: g.NAME, tile: [+g.Z, +g.X, +g.Y], ext: g.EXT }; + } + return { ok: false, name: "", tile: [0, 0, 0], ext: "" }; +}; + const CACHE = new ResolvedValueCache(); class R2Source implements Source { @@ -51,7 +81,7 @@ class R2Source implements Source { async getBytes(offset: number, length: number): Promise { const resp = await this.env.BUCKET.get( - pmtiles_path(this.env.PMTILES_PATH, this.archive_name), + pmtiles_path(this.archive_name, this.env.PMTILES_PATH), { range: { offset: offset, length: length }, } @@ -72,15 +102,10 @@ export default { ctx: ExecutionContext ): Promise { const url = new URL(request.url); - const match = url.pathname.match(TILE)!; + const {ok, name, tile, ext} = tile_path(url.pathname, env.TILE_PATH); - if (match) { - const archive_name = match[1]; - const z = +match[2]; - const x = +match[3]; - const y = +match[4]; - const ext = match[5]; - const source = new R2Source(env, archive_name); + if (ok) { + const source = new R2Source(env, name); const p = new PMTiles(source, CACHE); let header = await p.getHeader(); @@ -100,7 +125,7 @@ export default { // TODO: optimize by checking header min/maxzoom try { - const tile = await p.getZxy(z, x, y); + const tiledata = await p.getZxy(tile[0], tile[1], tile[2]); const headers = new Headers(); headers.set("Access-Control-Allow-Origin", "*"); // TODO: make configurable @@ -120,8 +145,8 @@ export default { } // TODO: optimize by making decompression optional - if (tile) { - return new Response(tile.data, { headers: headers, status: 200 }); + if (tiledata) { + return new Response(tiledata.data, { headers: headers, status: 200 }); } else { return new Response(undefined, { headers: headers, status: 204 }); }