mirror of
https://github.com/protomaps/PMTiles.git
synced 2026-02-04 02:41:09 +00:00
* add getTileJson method to PMTiles class [#239, #247] * update docs related to FetchSource and headers [#397]
This commit is contained in:
@@ -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
|
||||||
|
|||||||
61
js/index.ts
61
js/index.ts
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user