mirror of
https://github.com/protomaps/PMTiles.git
synced 2026-02-04 02:41:09 +00:00
TileJSON support for Cloudflare and AWS [#169]
* Remove TILE_PATH configuration as this makes supporting non-tile paths difficult * create shared/ dir in serverless for common code * linting fixes
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -1,78 +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");
|
||||
});
|
||||
|
||||
test("parse tileset default", () => {
|
||||
let { ok, name, tile, ext } = tile_path("/abcd.json");
|
||||
assert.strictEqual(ok, true);
|
||||
assert.strictEqual("abcd", name);
|
||||
});
|
||||
@@ -1,25 +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;
|
||||
TILESET_PATH?: string;
|
||||
BUCKET: R2Bucket;
|
||||
CACHE_MAX_AGE?: number;
|
||||
PMTILES_PATH?: string;
|
||||
PUBLIC_HOSTNAME?: string;
|
||||
}
|
||||
|
||||
class KeyNotFoundError extends Error {
|
||||
@@ -28,86 +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]+)$/;
|
||||
|
||||
const TILESET = /^\/(?<NAME>[0-9a-zA-Z\/!\-_\.\*\'\(\)]+).json$/;
|
||||
|
||||
export const tile_path = (
|
||||
path: string,
|
||||
tile_setting?: string,
|
||||
tileset_setting?: string
|
||||
): {
|
||||
ok: boolean;
|
||||
name: string;
|
||||
tile?: [number, number, number];
|
||||
ext: string;
|
||||
} => {
|
||||
let tile_pattern = TILE;
|
||||
if (tile_setting) {
|
||||
// escape regex
|
||||
tile_setting = tile_setting.replace(/[.*+?^$()|[\]\\]/g, "\\$&");
|
||||
tile_setting = tile_setting.replace(
|
||||
"{name}",
|
||||
"(?<NAME>[0-9a-zA-Z/!-_.*'()]+)"
|
||||
);
|
||||
tile_setting = tile_setting.replace("{z}", "(?<Z>\\d+)");
|
||||
tile_setting = tile_setting.replace("{x}", "(?<X>\\d+)");
|
||||
tile_setting = tile_setting.replace("{y}", "(?<Y>\\d+)");
|
||||
tile_setting = tile_setting.replace("{ext}", "(?<EXT>[a-z]+)");
|
||||
tile_pattern = new RegExp(tile_setting);
|
||||
}
|
||||
|
||||
let tile_match = path.match(tile_pattern);
|
||||
|
||||
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}",
|
||||
"(?<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: "" };
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -153,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");
|
||||
@@ -187,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,
|
||||
});
|
||||
@@ -197,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");
|
||||
@@ -206,54 +120,21 @@ 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) {
|
||||
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
|
||||
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) {
|
||||
@@ -310,7 +191,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: metadata responses, tileJSON
|
||||
return new Response("Invalid URL", { status: 404 });
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user