add getTileJson method to PMTiles class [#239, #247] (#453)

* add getTileJson method to PMTiles class [#239, #247]
* update docs related to FetchSource and headers [#397]
This commit is contained in:
Brandon Liu
2024-09-18 15:57:14 +08:00
committed by GitHub
parent 6ed85a28fb
commit 6f5479439b
7 changed files with 121 additions and 56 deletions

View File

@@ -164,20 +164,31 @@ const v3compat =
* MapLibre GL JS protocol. Must be added once globally. * MapLibre GL JS protocol. Must be added once globally.
*/ */
export class Protocol { export class Protocol {
/** @hidden */
tiles: Map<string, PMTiles>; tiles: Map<string, PMTiles>;
constructor() { constructor() {
this.tiles = new Map<string, PMTiles>(); this.tiles = new Map<string, PMTiles>();
} }
/**
* Add a {@link PMTiles} instance to the global protocol instance.
*
* For remote fetch sources, references in MapLibre styles like pmtiles://http://...
* will resolve to the same instance if the URLs match.
*/
add(p: PMTiles) { add(p: PMTiles) {
this.tiles.set(p.source.getKey(), p); this.tiles.set(p.source.getKey(), p);
} }
/**
* Fetch a {@link PMTiles} instance by URL, for remote PMTiles instances.
*/
get(url: string) { get(url: string) {
return this.tiles.get(url); return this.tiles.get(url);
} }
/** @hidden */
tilev4 = async ( tilev4 = async (
params: RequestParameters, params: RequestParameters,
abortController: AbortController abortController: AbortController

View File

@@ -150,6 +150,15 @@ export interface Entry {
runLength: number; runLength: number;
} }
interface MetadataLike {
attribution?: string;
name?: string;
version?: string;
// biome-ignore lint: TileJSON spec
vector_layers?: string;
description?: string;
}
/** /**
* Enum representing a compression algorithm used. * Enum representing a compression algorithm used.
* 0 = unknown compression, for if you must use a different or unspecified algorithm. * 0 = unknown compression, for if you must use a different or unspecified algorithm.
@@ -212,6 +221,15 @@ export enum TileType {
Avif = 5, Avif = 5,
} }
export function tileTypeExt(t: TileType): string {
if (t === TileType.Mvt) return ".mvt";
if (t === TileType.Png) return ".png";
if (t === TileType.Jpeg) return ".jpg";
if (t === TileType.Webp) return ".webp";
if (t === TileType.Avif) return ".avif";
return "";
}
const HEADER_SIZE_BYTES = 127; const HEADER_SIZE_BYTES = 127;
/** /**
@@ -327,10 +345,19 @@ export class FileSource implements Source {
* *
* This method does not send conditional request headers If-Match because of CORS. * This method does not send conditional request headers If-Match because of CORS.
* Instead, it detects ETag mismatches via the response ETag or the 416 response code. * Instead, it detects ETag mismatches via the response ETag or the 416 response code.
*
* This also works around browser and storage-specific edge cases.
*/ */
export class FetchSource implements Source { export class FetchSource implements Source {
url: string; url: string;
/**
* A [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object, specfying custom [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) set for all requests to the remote archive.
*
* This should be used instead of maplibre's [transformRequest](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/#example) for PMTiles archives.
*/
customHeaders: Headers; customHeaders: Headers;
/** @hidden */
mustReload: boolean; mustReload: boolean;
constructor(url: string, customHeaders: Headers = new Headers()) { constructor(url: string, customHeaders: Headers = new Headers()) {
@@ -343,6 +370,9 @@ export class FetchSource implements Source {
return this.url; return this.url;
} }
/**
* Mutate the custom [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) set for all requests to the remote archive.
*/
setHeaders(customHeaders: Headers) { setHeaders(customHeaders: Headers) {
this.customHeaders = customHeaders; this.customHeaders = customHeaders;
} }
@@ -1003,7 +1033,7 @@ export class PMTiles {
} }
/** /**
* Primary method to get a single tile bytes from an archive. * Primary method to get a single tile's bytes from an archive.
* *
* Returns undefined if the tile does not exist in the archive. * Returns undefined if the tile does not exist in the archive.
*/ */
@@ -1056,4 +1086,33 @@ export class PMTiles {
throw e; throw e;
} }
} }
/**
* Construct a [TileJSON](https://github.com/mapbox/tilejson-spec) object.
*
* baseTilesUrl is the desired tiles URL, excluding the suffix `/{z}/{x}/{y}.{ext}`.
* For example, if the desired URL is `http://example.com/tileset/{z}/{x}/{y}.mvt`,
* the baseTilesUrl should be `https://example.com/tileset`.
*/
async getTileJson(baseTilesUrl: string): Promise<unknown> {
const header = await this.getHeader();
const metadata = (await this.getMetadata()) as MetadataLike;
const ext = tileTypeExt(header.tileType);
return {
tilejson: "3.0.0",
scheme: "xyz",
tiles: [`${baseTilesUrl}/{z}/{x}/{y}${ext}`],
// biome-ignore lint: TileJSON spec
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,
};
}
} }

View File

@@ -11,10 +11,12 @@ import {
RangeResponse, RangeResponse,
SharedPromiseCache, SharedPromiseCache,
Source, Source,
TileType,
findTile, findTile,
getUint64, getUint64,
readVarint, readVarint,
tileIdToZxy, tileIdToZxy,
tileTypeExt,
zxyToTileId, zxyToTileId,
} from "../index"; } from "../index";
@@ -376,3 +378,40 @@ test("pmtiles get metadata", async () => {
}); });
// echo '{"type":"Polygon","coordinates":[[[0,0],[0,1],[1,0],[0,0]]]}' | ./tippecanoe -zg -o test_fixture_2.pmtiles // echo '{"type":"Polygon","coordinates":[[[0,0],[0,1],[1,0],[0,0]]]}' | ./tippecanoe -zg -o test_fixture_2.pmtiles
test("get file extension", async () => {
assert.equal("", tileTypeExt(TileType.Unknown));
assert.equal(".mvt", tileTypeExt(TileType.Mvt));
assert.equal(".png", tileTypeExt(TileType.Png));
assert.equal(".jpg", tileTypeExt(TileType.Jpeg));
assert.equal(".webp", tileTypeExt(TileType.Webp));
assert.equal(".avif", tileTypeExt(TileType.Avif));
});
interface TileJsonLike {
tilejson: string;
scheme: string;
tiles: string[];
description?: string;
name?: string;
attribution?: string;
version?: string;
}
test("pmtiles get TileJSON", async () => {
const source = new TestNodeFileSource(
"test/data/test_fixture_1.pmtiles",
"1"
);
const p = new PMTiles(source);
const tilejson = (await p.getTileJson(
"https://example.com/foo"
)) as TileJsonLike;
assert.equal("3.0.0", tilejson.tilejson);
assert.equal("xyz", tilejson.scheme);
assert.equal("https://example.com/foo/{z}/{x}/{y}.mvt", tilejson.tiles[0]);
assert.equal(undefined, tilejson.attribution);
assert.equal("test_fixture_1.pmtiles", tilejson.description);
assert.equal("test_fixture_1.pmtiles", tilejson.name);
assert.equal("2", tilejson.version);
});

View File

@@ -12,7 +12,7 @@ import {
Source, Source,
TileType, TileType,
} from "../../../js/index"; } from "../../../js/index";
import { pmtiles_path, tileJSON, tile_path } from "../../shared/index"; import { pmtiles_path, tile_path } from "../../shared/index";
import { createHash } from "crypto"; import { createHash } from "crypto";
import zlib from "zlib"; import zlib from "zlib";
@@ -177,15 +177,13 @@ export const handlerRaw = async (
} }
headers["Content-Type"] = "application/json"; headers["Content-Type"] = "application/json";
const t = tileJSON( const t = await p.getTileJson(
header, `https://${
await p.getMetadata(),
process.env.PUBLIC_HOSTNAME || process.env.PUBLIC_HOSTNAME ||
event.headers["x-distribution-domain-name"] || event.headers["x-distribution-domain-name"] ||
"", ""
name }/${name}`
); );
return apiResp(200, JSON.stringify(t), false, headers); return apiResp(200, JSON.stringify(t), false, headers);
} }

View File

@@ -14,7 +14,7 @@
"deploy": "wrangler deploy", "deploy": "wrangler deploy",
"test": "tsx ../shared/index.test.ts", "test": "tsx ../shared/index.test.ts",
"tsc": "tsc --watch", "tsc": "tsc --watch",
"build": "wrangler publish --outdir dist --dry-run", "build": "wrangler deploy --outdir dist --dry-run",
"biome": "biome check --config-path=../../js/ src/index.ts --apply", "biome": "biome check --config-path=../../js/ src/index.ts --apply",
"biome-check": "biome check --config-path=../../js src/index.ts" "biome-check": "biome check --config-path=../../js src/index.ts"
} }

View File

@@ -7,7 +7,7 @@ import {
Source, Source,
TileType, TileType,
} from "../../../js/index"; } from "../../../js/index";
import { pmtiles_path, tileJSON, tile_path } from "../../shared/index"; import { pmtiles_path, tile_path } from "../../shared/index";
interface Env { interface Env {
// biome-ignore lint: config name // biome-ignore lint: config name
@@ -159,14 +159,9 @@ export default {
if (!tile) { if (!tile) {
cacheableHeaders.set("Content-Type", "application/json"); cacheableHeaders.set("Content-Type", "application/json");
const t = await p.getTileJson(
const t = tileJSON( `https://${env.PUBLIC_HOSTNAME || url.hostname}/${name}`
pHeader,
await p.getMetadata(),
env.PUBLIC_HOSTNAME || url.hostname,
name
); );
return cacheableResponse(JSON.stringify(t), cacheableHeaders, 200); return cacheableResponse(JSON.stringify(t), cacheableHeaders, 200);
} }

View File

@@ -1,5 +1,3 @@
import { Header, TileType } from "../../js/index";
export const pmtiles_path = (name: string, setting?: string): string => { export const pmtiles_path = (name: string, setting?: string): string => {
if (setting) { if (setting) {
return setting.replaceAll("{name}", name); return setting.replaceAll("{name}", name);
@@ -36,38 +34,3 @@ export const tile_path = (
return { ok: false, name: "", tile: [0, 0, 0], ext: "" }; return { ok: false, name: "", tile: [0, 0, 0], ext: "" };
}; };
export const tileJSON = (
header: Header,
metadata: any,
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";
} else if (header.tileType === TileType.Avif) {
ext = ".avif";
}
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,
};
};