Merge pull request #170 from protomaps/cloudflare-tilejson

Cloudflare TileJSON
This commit is contained in:
Brandon Liu
2023-06-21 18:34:28 +08:00
committed by GitHub
8 changed files with 246 additions and 199 deletions

View File

@@ -11,7 +11,7 @@
"scripts": {
"start": "wrangler dev",
"deploy": "wrangler publish",
"test": "node -r esbuild-runner/register src/index.test.ts",
"test": "node -r esbuild-runner/register ../shared/index.test.ts",
"tsc": "tsc --watch",
"build": "wrangler publish --outdir dist --dry-run"
}

View File

@@ -1,72 +0,0 @@
import { test } from "node:test";
import assert from "node:assert";
import { pmtiles_path, tile_path } from "./index";
test("pmtiles path", () => {
let result = pmtiles_path("foo", undefined);
assert.strictEqual(result, "foo.pmtiles");
});
test("pmtiles path", () => {
let result = pmtiles_path("foo", "folder/{name}/file.pmtiles");
assert.strictEqual(result, "folder/foo/file.pmtiles");
});
test("pmtiles path with slash", () => {
let result = pmtiles_path("foo/bar", "folder/{name}/file.pmtiles");
assert.strictEqual(result, "folder/foo/bar/file.pmtiles");
});
test("pmtiles path with multiple names", () => {
let result = pmtiles_path("slug", "folder/{name}/{name}.pmtiles");
assert.strictEqual(result, "folder/slug/slug.pmtiles");
result = pmtiles_path("foo/bar", "folder/{name}/{name}.pmtiles");
assert.strictEqual(result, "folder/foo/bar/foo/bar.pmtiles");
});
test("parse tile default", () => {
let { ok, name, tile, ext } = tile_path("abcd");
assert.strictEqual(ok, false);
({ 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);
});
test("parse tile path setting", () => {
let { ok, name, tile, ext } = tile_path(
"/foo/11/22/33.pbf",
"/{name}/{z}/{y}/{x}.{ext}"
);
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(ext, "mvt");
});
test("parse tile path setting special chars", () => {
let { ok, name, tile, ext } = tile_path(
"/folder(new/foo/11/22/33.pbf",
"/folder(new/{name}/{z}/{y}/{x}.{ext}"
);
assert.strictEqual(name, "foo");
});
test("parse tile path setting slash", () => {
let { ok, name, tile, ext } = tile_path(
"/foo/bar/11/22/33.pbf",
"/{name}/{z}/{y}/{x}.{ext}"
);
assert.strictEqual(name, "foo/bar");
});

View File

@@ -1,24 +1,18 @@
/**
* - Run `wrangler dev src/index.ts` in your terminal to start a development server
* - Open a browser tab at http://localhost:8787/ to see your worker in action
* - Run `wrangler publish src/index.ts --name my-worker` to publish your worker
*/
import {
PMTiles,
Source,
RangeResponse,
ResolvedValueCache,
TileType,
Compression,
} from "../../../js/index";
import { pmtiles_path, tile_path, tileJSON } from "../../shared/index";
interface Env {
BUCKET: R2Bucket;
ALLOWED_ORIGINS?: string;
PMTILES_PATH?: string;
TILE_PATH?: string;
BUCKET: R2Bucket;
CACHE_MAX_AGE?: number;
PMTILES_PATH?: string;
PUBLIC_HOSTNAME?: string;
}
class KeyNotFoundError extends Error {
@@ -27,62 +21,7 @@ class KeyNotFoundError extends Error {
}
}
export const pmtiles_path = (name: string, setting?: string): string => {
if (setting) {
return setting.replaceAll("{name}", name);
}
return name + ".pmtiles";
};
const TILE =
/^\/(?<NAME>[0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(?<Z>\d+)\/(?<X>\d+)\/(?<Y>\d+).(?<EXT>[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}", "(?<NAME>[0-9a-zA-Z/!-_.*'()]+)");
setting = setting.replace("{z}", "(?<Z>\\d+)");
setting = setting.replace("{x}", "(?<X>\\d+)");
setting = setting.replace("{y}", "(?<Y>\\d+)");
setting = setting.replace("{ext}", "(?<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: "" };
};
async function nativeDecompress(
buf: ArrayBuffer,
compression: Compression
): Promise<ArrayBuffer> {
if (compression === Compression.None || compression === Compression.Unknown) {
return buf;
} else if (compression === Compression.Gzip) {
let stream = new Response(buf).body!;
let result = stream.pipeThrough(new DecompressionStream("gzip"));
return new Response(result).arrayBuffer();
} else {
throw Error("Compression method not supported");
}
}
const CACHE = new ResolvedValueCache(25, undefined, nativeDecompress);
const CACHE = new ResolvedValueCache(25, undefined);
class R2Source implements Source {
env: Env;
@@ -128,23 +67,23 @@ export default {
return new Response(undefined, { status: 405 });
const url = new URL(request.url);
const { ok, name, tile, ext } = tile_path(url.pathname, env.TILE_PATH);
const { ok, name, tile, ext } = tile_path(url.pathname);
const cache = caches.default;
if (ok) {
let allowed_origin = "";
if (typeof env.ALLOWED_ORIGINS !== "undefined") {
for (let o of env.ALLOWED_ORIGINS.split(",")) {
for (const o of env.ALLOWED_ORIGINS.split(",")) {
if (o === request.headers.get("Origin") || o === "*") {
allowed_origin = o;
}
}
}
let cached = await cache.match(request.url);
const cached = await cache.match(request.url);
if (cached) {
let resp_headers = new Headers(cached.headers);
const resp_headers = new Headers(cached.headers);
if (allowed_origin)
resp_headers.set("Access-Control-Allow-Origin", allowed_origin);
resp_headers.set("Vary", "Origin");
@@ -162,9 +101,9 @@ export default {
) => {
cacheable_headers.set(
"Cache-Control",
"max-age=" + (env.CACHE_MAX_AGE | 86400)
"max-age=" + (env.CACHE_MAX_AGE || 86400)
);
let cacheable = new Response(body, {
const cacheable = new Response(body, {
headers: cacheable_headers,
status: status,
});
@@ -172,7 +111,7 @@ export default {
// normalize HEAD requests
ctx.waitUntil(cache.put(request.url, cacheable));
let resp_headers = new Headers(cacheable_headers);
const resp_headers = new Headers(cacheable_headers);
if (allowed_origin)
resp_headers.set("Access-Control-Allow-Origin", allowed_origin);
resp_headers.set("Vary", "Origin");
@@ -181,9 +120,23 @@ export default {
const cacheable_headers = new Headers();
const source = new R2Source(env, name);
const p = new PMTiles(source, CACHE, nativeDecompress);
const p = new PMTiles(source, CACHE);
try {
const p_header = await p.getHeader();
if (!tile) {
cacheable_headers.set("Content-Type", "application/json");
const t = tileJSON(
p_header,
await p.getMetadata(),
env.PUBLIC_HOSTNAME || url.hostname,
name
);
return cacheableResponse(JSON.stringify(t), cacheable_headers, 200);
}
if (tile[0] < p_header.minZoom || tile[0] > p_header.maxZoom) {
return cacheableResponse(undefined, cacheable_headers, 404);
}
@@ -238,7 +191,6 @@ export default {
}
}
// TODO: metadata responses, tileJSON
return new Response("Invalid URL", { status: 404 });
},
};