mirror of
https://github.com/protomaps/PMTiles.git
synced 2026-02-04 10:51:07 +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:
88
serverless/aws/package-lock.json
generated
88
serverless/aws/package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"@types/aws-lambda": "^8.10.108",
|
"@types/aws-lambda": "^8.10.108",
|
||||||
"@types/node": "^18.11.2",
|
"@types/node": "^18.11.2",
|
||||||
"esbuild": "^0.15.11",
|
"esbuild": "^0.15.11",
|
||||||
|
"esbuild-runner": "^2.2.2",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1351,6 +1352,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
|
||||||
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
|
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-from": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.15.11",
|
"version": "0.15.11",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.11.tgz",
|
||||||
@@ -1644,6 +1651,28 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/esbuild-runner": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-runner/-/esbuild-runner-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-fRFVXcmYVmSmtYm2mL8RlUASt2TDkGh3uRcvHFOKNr/T58VrfVeKD9uT9nlgxk96u0LS0ehS/GY7Da/bXWKkhw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"source-map-support": "0.5.21",
|
||||||
|
"tslib": "2.4.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"esr": "bin/esr.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"esbuild": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-runner/node_modules/tslib": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/esbuild-sunos-64": {
|
"node_modules/esbuild-sunos-64": {
|
||||||
"version": "0.15.11",
|
"version": "0.15.11",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.11.tgz",
|
||||||
@@ -1723,6 +1752,25 @@
|
|||||||
"url": "https://paypal.me/naturalintelligence"
|
"url": "https://paypal.me/naturalintelligence"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map-support": {
|
||||||
|
"version": "0.5.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||||
|
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"source-map": "^0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strnum": {
|
"node_modules/strnum": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
|
||||||
@@ -2870,6 +2918,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
|
||||||
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
|
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
|
||||||
},
|
},
|
||||||
|
"buffer-from": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"esbuild": {
|
"esbuild": {
|
||||||
"version": "0.15.11",
|
"version": "0.15.11",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.11.tgz",
|
||||||
@@ -3012,6 +3066,24 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"esbuild-runner": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-runner/-/esbuild-runner-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-fRFVXcmYVmSmtYm2mL8RlUASt2TDkGh3uRcvHFOKNr/T58VrfVeKD9uT9nlgxk96u0LS0ehS/GY7Da/bXWKkhw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"source-map-support": "0.5.21",
|
||||||
|
"tslib": "2.4.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"esbuild-sunos-64": {
|
"esbuild-sunos-64": {
|
||||||
"version": "0.15.11",
|
"version": "0.15.11",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.11.tgz",
|
||||||
@@ -3048,6 +3120,22 @@
|
|||||||
"strnum": "^1.0.5"
|
"strnum": "^1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"source-map-support": {
|
||||||
|
"version": "0.5.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||||
|
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"source-map": "^0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"strnum": {
|
"strnum": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
|
||||||
|
|||||||
@@ -5,12 +5,14 @@
|
|||||||
"@types/aws-lambda": "^8.10.108",
|
"@types/aws-lambda": "^8.10.108",
|
||||||
"@types/node": "^18.11.2",
|
"@types/node": "^18.11.2",
|
||||||
"esbuild": "^0.15.11",
|
"esbuild": "^0.15.11",
|
||||||
|
"esbuild-runner": "^2.2.2",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.8.4"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tsc": "tsc --noEmit --watch",
|
"tsc": "tsc --noEmit --watch",
|
||||||
"build": "esbuild src/index.ts --target=es2020 --outfile=dist/index.mjs --format=esm --bundle --platform=node --target=node18 --external:@aws-sdk/client-s3 --external:@aws-sdk/node-http-handler --banner:js=//$(git describe --always) && cd dist && zip lambda_function.zip index.mjs"
|
"build": "esbuild src/index.ts --target=es2020 --outfile=dist/index.mjs --format=esm --bundle --platform=node --target=node18 --external:@aws-sdk/client-s3 --external:@aws-sdk/node-http-handler --banner:js=//$(git describe --always) && cd dist && zip lambda_function.zip index.mjs",
|
||||||
|
"test": "node -r esbuild-runner/register ../shared/index.test.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.213.0",
|
"@aws-sdk/client-s3": "^3.213.0",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Readable } from "stream";
|
|
||||||
import {
|
import {
|
||||||
Context,
|
Context,
|
||||||
APIGatewayProxyResult,
|
APIGatewayProxyResult,
|
||||||
@@ -12,8 +11,8 @@ import {
|
|||||||
Compression,
|
Compression,
|
||||||
TileType,
|
TileType,
|
||||||
} from "../../../js/index";
|
} from "../../../js/index";
|
||||||
|
import { pmtiles_path, tile_path, tileJSON } from "../../shared/index";
|
||||||
|
|
||||||
import https from "https";
|
|
||||||
import zlib from "zlib";
|
import zlib from "zlib";
|
||||||
|
|
||||||
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
||||||
@@ -43,47 +42,6 @@ async function nativeDecompress(
|
|||||||
// Lambda needs to run with 512MB, empty function takes about 70
|
// Lambda needs to run with 512MB, empty function takes about 70
|
||||||
const CACHE = new ResolvedValueCache(undefined, undefined, nativeDecompress);
|
const CACHE = new ResolvedValueCache(undefined, undefined, nativeDecompress);
|
||||||
|
|
||||||
// duplicated code below
|
|
||||||
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: "" };
|
|
||||||
};
|
|
||||||
|
|
||||||
class S3Source implements Source {
|
class S3Source implements Source {
|
||||||
archive_name: string;
|
archive_name: string;
|
||||||
|
|
||||||
@@ -138,11 +96,11 @@ const apiResp = (
|
|||||||
// Does not work with CloudFront events/Lambda@Edge; see README
|
// Does not work with CloudFront events/Lambda@Edge; see README
|
||||||
export const handlerRaw = async (
|
export const handlerRaw = async (
|
||||||
event: APIGatewayProxyEventV2,
|
event: APIGatewayProxyEventV2,
|
||||||
context: Context,
|
_context: Context,
|
||||||
tilePostprocess?: (a: ArrayBuffer, t: TileType) => ArrayBuffer
|
tilePostprocess?: (a: ArrayBuffer, t: TileType) => ArrayBuffer
|
||||||
): Promise<APIGatewayProxyResult> => {
|
): Promise<APIGatewayProxyResult> => {
|
||||||
var path;
|
let path;
|
||||||
var is_api_gateway;
|
let is_api_gateway;
|
||||||
if (event.pathParameters) {
|
if (event.pathParameters) {
|
||||||
is_api_gateway = true;
|
is_api_gateway = true;
|
||||||
if (event.pathParameters.proxy) {
|
if (event.pathParameters.proxy) {
|
||||||
@@ -158,14 +116,13 @@ export const handlerRaw = async (
|
|||||||
return apiResp(500, "Invalid event configuration");
|
return apiResp(500, "Invalid event configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
var headers: Headers = {};
|
const headers: Headers = {};
|
||||||
// TODO: metadata and TileJSON
|
|
||||||
|
|
||||||
if (process.env.CORS) {
|
if (process.env.CORS) {
|
||||||
headers["Access-Control-Allow-Origin"] = process.env.CORS;
|
headers["Access-Control-Allow-Origin"] = process.env.CORS;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ok, name, tile, ext } = tile_path(path, process.env.TILE_PATH);
|
const { ok, name, tile, ext } = tile_path(path);
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return apiResp(400, "Invalid tile URL", false, headers);
|
return apiResp(400, "Invalid tile URL", false, headers);
|
||||||
@@ -175,6 +132,28 @@ export const handlerRaw = async (
|
|||||||
const p = new PMTiles(source, CACHE, nativeDecompress);
|
const p = new PMTiles(source, CACHE, nativeDecompress);
|
||||||
try {
|
try {
|
||||||
const header = await p.getHeader();
|
const header = await p.getHeader();
|
||||||
|
|
||||||
|
if (!tile) {
|
||||||
|
if (!process.env.PUBLIC_HOSTNAME) {
|
||||||
|
return apiResp(
|
||||||
|
400,
|
||||||
|
"PUBLIC_HOSTNAME must be set for TileJSON",
|
||||||
|
false,
|
||||||
|
headers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
|
||||||
|
const t = tileJSON(
|
||||||
|
header,
|
||||||
|
await p.getMetadata(),
|
||||||
|
process.env.PUBLIC_HOSTNAME,
|
||||||
|
name
|
||||||
|
);
|
||||||
|
|
||||||
|
return apiResp(200, JSON.stringify(t), false, headers);
|
||||||
|
}
|
||||||
|
|
||||||
if (tile[0] < header.minZoom || tile[0] > header.maxZoom) {
|
if (tile[0] < header.minZoom || tile[0] > header.maxZoom) {
|
||||||
return apiResp(404, "", false, headers);
|
return apiResp(404, "", false, headers);
|
||||||
}
|
}
|
||||||
@@ -259,5 +238,5 @@ export const handler = async (
|
|||||||
event: APIGatewayProxyEventV2,
|
event: APIGatewayProxyEventV2,
|
||||||
context: Context
|
context: Context
|
||||||
): Promise<APIGatewayProxyResult> => {
|
): Promise<APIGatewayProxyResult> => {
|
||||||
return handlerRaw(event, context);
|
return await handlerRaw(event, context);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "wrangler dev",
|
"start": "wrangler dev",
|
||||||
"deploy": "wrangler publish",
|
"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",
|
"tsc": "tsc --watch",
|
||||||
"build": "wrangler publish --outdir dist --dry-run"
|
"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 {
|
import {
|
||||||
PMTiles,
|
PMTiles,
|
||||||
Source,
|
Source,
|
||||||
RangeResponse,
|
RangeResponse,
|
||||||
ResolvedValueCache,
|
ResolvedValueCache,
|
||||||
TileType,
|
TileType,
|
||||||
Compression,
|
|
||||||
} from "../../../js/index";
|
} from "../../../js/index";
|
||||||
|
import { pmtiles_path, tile_path, tileJSON } from "../../shared/index";
|
||||||
|
|
||||||
interface Env {
|
interface Env {
|
||||||
BUCKET: R2Bucket;
|
|
||||||
ALLOWED_ORIGINS?: string;
|
ALLOWED_ORIGINS?: string;
|
||||||
PMTILES_PATH?: string;
|
BUCKET: R2Bucket;
|
||||||
TILE_PATH?: string;
|
|
||||||
TILESET_PATH?: string;
|
|
||||||
CACHE_MAX_AGE?: number;
|
CACHE_MAX_AGE?: number;
|
||||||
|
PMTILES_PATH?: string;
|
||||||
|
PUBLIC_HOSTNAME?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class KeyNotFoundError extends Error {
|
class KeyNotFoundError extends Error {
|
||||||
@@ -28,86 +21,7 @@ class KeyNotFoundError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pmtiles_path = (name: string, setting?: string): string => {
|
const CACHE = new ResolvedValueCache(25, undefined);
|
||||||
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);
|
|
||||||
|
|
||||||
class R2Source implements Source {
|
class R2Source implements Source {
|
||||||
env: Env;
|
env: Env;
|
||||||
@@ -153,23 +67,23 @@ export default {
|
|||||||
return new Response(undefined, { status: 405 });
|
return new Response(undefined, { status: 405 });
|
||||||
|
|
||||||
const url = new URL(request.url);
|
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;
|
const cache = caches.default;
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
let allowed_origin = "";
|
let allowed_origin = "";
|
||||||
if (typeof env.ALLOWED_ORIGINS !== "undefined") {
|
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 === "*") {
|
if (o === request.headers.get("Origin") || o === "*") {
|
||||||
allowed_origin = o;
|
allowed_origin = o;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cached = await cache.match(request.url);
|
const cached = await cache.match(request.url);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
let resp_headers = new Headers(cached.headers);
|
const resp_headers = new Headers(cached.headers);
|
||||||
if (allowed_origin)
|
if (allowed_origin)
|
||||||
resp_headers.set("Access-Control-Allow-Origin", allowed_origin);
|
resp_headers.set("Access-Control-Allow-Origin", allowed_origin);
|
||||||
resp_headers.set("Vary", "Origin");
|
resp_headers.set("Vary", "Origin");
|
||||||
@@ -187,9 +101,9 @@ export default {
|
|||||||
) => {
|
) => {
|
||||||
cacheable_headers.set(
|
cacheable_headers.set(
|
||||||
"Cache-Control",
|
"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,
|
headers: cacheable_headers,
|
||||||
status: status,
|
status: status,
|
||||||
});
|
});
|
||||||
@@ -197,7 +111,7 @@ export default {
|
|||||||
// normalize HEAD requests
|
// normalize HEAD requests
|
||||||
ctx.waitUntil(cache.put(request.url, cacheable));
|
ctx.waitUntil(cache.put(request.url, cacheable));
|
||||||
|
|
||||||
let resp_headers = new Headers(cacheable_headers);
|
const resp_headers = new Headers(cacheable_headers);
|
||||||
if (allowed_origin)
|
if (allowed_origin)
|
||||||
resp_headers.set("Access-Control-Allow-Origin", allowed_origin);
|
resp_headers.set("Access-Control-Allow-Origin", allowed_origin);
|
||||||
resp_headers.set("Vary", "Origin");
|
resp_headers.set("Vary", "Origin");
|
||||||
@@ -206,54 +120,21 @@ export default {
|
|||||||
|
|
||||||
const cacheable_headers = new Headers();
|
const cacheable_headers = new Headers();
|
||||||
const source = new R2Source(env, name);
|
const source = new R2Source(env, name);
|
||||||
const p = new PMTiles(source, CACHE, nativeDecompress);
|
const p = new PMTiles(source, CACHE);
|
||||||
try {
|
try {
|
||||||
const p_header = await p.getHeader();
|
const p_header = await p.getHeader();
|
||||||
|
|
||||||
if (!tile) {
|
if (!tile) {
|
||||||
const metadata = await p.getMetadata();
|
|
||||||
cacheable_headers.set("Content-Type", "application/json");
|
cacheable_headers.set("Content-Type", "application/json");
|
||||||
|
|
||||||
let ext = "";
|
const t = tileJSON(
|
||||||
if (p_header.tileType === TileType.Mvt) {
|
p_header,
|
||||||
ext = ".mvt";
|
await p.getMetadata(),
|
||||||
} else if (p_header.tileType === TileType.Png) {
|
env.PUBLIC_HOSTNAME || url.hostname,
|
||||||
ext = ".png";
|
name
|
||||||
} 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
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return cacheableResponse(JSON.stringify(t), cacheable_headers, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tile[0] < p_header.minZoom || tile[0] > p_header.maxZoom) {
|
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 });
|
return new Response("Invalid URL", { status: 404 });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
27
serverless/shared/index.test.ts
Normal file
27
serverless/shared/index.test.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
71
serverless/shared/index.ts
Normal file
71
serverless/shared/index.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { Header, TileType } from "../../js/index";
|
||||||
|
|
||||||
|
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
|
||||||
|
): {
|
||||||
|
ok: boolean;
|
||||||
|
name: string;
|
||||||
|
tile?: [number, number, number];
|
||||||
|
ext: string;
|
||||||
|
} => {
|
||||||
|
const tile_match = path.match(TILE);
|
||||||
|
|
||||||
|
if (tile_match) {
|
||||||
|
const g = tile_match.groups!;
|
||||||
|
return { ok: true, name: g.NAME, tile: [+g.Z, +g.X, +g.Y], ext: g.EXT };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tileset_match = path.match(TILESET);
|
||||||
|
|
||||||
|
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: "" };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tileJSON = (
|
||||||
|
header: Header,
|
||||||
|
metadata: unknown,
|
||||||
|
hostname: string,
|
||||||
|
tileset_name: string
|
||||||
|
) => {
|
||||||
|
let ext = "";
|
||||||
|
if (header.tileType === TileType.Mvt) {
|
||||||
|
ext = ".mvt";
|
||||||
|
} else if (header.tileType === TileType.Png) {
|
||||||
|
ext = ".png";
|
||||||
|
} else if (header.tileType === TileType.Jpeg) {
|
||||||
|
ext = ".jpg";
|
||||||
|
} else if (header.tileType === TileType.Webp) {
|
||||||
|
ext = ".webp";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tilejson: "3.0.0",
|
||||||
|
scheme: "xyz",
|
||||||
|
tiles: ["https://" + hostname + "/" + tileset_name + "/{z}/{x}/{y}" + ext],
|
||||||
|
vector_layers: metadata.vector_layers,
|
||||||
|
attribution: metadata.attribution,
|
||||||
|
description: metadata.description,
|
||||||
|
name: metadata.name,
|
||||||
|
version: metadata.version,
|
||||||
|
bounds: [header.minLon, header.minLat, header.maxLon, header.maxLat],
|
||||||
|
center: [header.centerLon, header.centerLat, header.centerZoom],
|
||||||
|
minzoom: header.minZoom,
|
||||||
|
maxzoom: header.maxZoom,
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user