Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

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

Merged
merged 4 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions js/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,20 +164,31 @@ const v3compat =
* MapLibre GL JS protocol. Must be added once globally.
*/
export class Protocol {
/** @hidden */
tiles: Map<string, PMTiles>;

constructor() {
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) {
this.tiles.set(p.source.getKey(), p);
}

/**
* Fetch a {@link PMTiles} instance by URL, for remote PMTiles instances.
*/
get(url: string) {
return this.tiles.get(url);
}

/** @hidden */
tilev4 = async (
params: RequestParameters,
abortController: AbortController
Expand Down
61 changes: 60 additions & 1 deletion js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ export interface Entry {
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.
* 0 = unknown compression, for if you must use a different or unspecified algorithm.
Expand Down Expand Up @@ -212,6 +221,15 @@ export enum TileType {
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;

/**
Expand Down Expand Up @@ -327,10 +345,19 @@ export class FileSource implements Source {
*
* 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.
*
* This also works around browser and storage-specific edge cases.
*/
export class FetchSource implements Source {
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;
/** @hidden */
mustReload: boolean;

constructor(url: string, customHeaders: Headers = new Headers()) {
Expand All @@ -343,6 +370,9 @@ export class FetchSource implements Source {
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) {
this.customHeaders = customHeaders;
}
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -1056,4 +1086,33 @@ export class PMTiles {
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,
};
}
}
39 changes: 39 additions & 0 deletions js/test/v3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import {
RangeResponse,
SharedPromiseCache,
Source,
TileType,
findTile,
getUint64,
readVarint,
tileIdToZxy,
tileTypeExt,
zxyToTileId,
} from "../index";

Expand Down Expand Up @@ -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

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);
});
14 changes: 6 additions & 8 deletions serverless/aws/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
Source,
TileType,
} 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 zlib from "zlib";
Expand Down Expand Up @@ -177,15 +177,13 @@ export const handlerRaw = async (
}
headers["Content-Type"] = "application/json";

const t = tileJSON(
header,
await p.getMetadata(),
process.env.PUBLIC_HOSTNAME ||
const t = await p.getTileJson(
`https://${
process.env.PUBLIC_HOSTNAME ||
event.headers["x-distribution-domain-name"] ||
"",
name
""
}/${name}`
);

return apiResp(200, JSON.stringify(t), false, headers);
}

Expand Down
2 changes: 1 addition & 1 deletion serverless/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"deploy": "wrangler deploy",
"test": "tsx ../shared/index.test.ts",
"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-check": "biome check --config-path=../../js src/index.ts"
}
Expand Down
11 changes: 3 additions & 8 deletions serverless/cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
Source,
TileType,
} from "../../../js/index";
import { pmtiles_path, tileJSON, tile_path } from "../../shared/index";
import { pmtiles_path, tile_path } from "../../shared/index";

interface Env {
// biome-ignore lint: config name
Expand Down Expand Up @@ -159,14 +159,9 @@ export default {

if (!tile) {
cacheableHeaders.set("Content-Type", "application/json");

const t = tileJSON(
pHeader,
await p.getMetadata(),
env.PUBLIC_HOSTNAME || url.hostname,
name
const t = await p.getTileJson(
`https://${env.PUBLIC_HOSTNAME || url.hostname}/${name}`
);

return cacheableResponse(JSON.stringify(t), cacheableHeaders, 200);
}

Expand Down
39 changes: 1 addition & 38 deletions serverless/shared/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { Header, TileType } from "../../js/index";

export const pmtiles_path = (name: string, setting?: string): string => {
if (setting) {
return setting.replaceAll("{name}", name);
Expand Down Expand Up @@ -35,39 +33,4 @@ export const tile_path = (
}

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,
};
};
};
Loading