change ol-pmtiles to TypeScript [#312] (#444)

* change ol-pmtiles to TypeScript [#444]

* olpmtiles: 1.0.0
* accept either a string or pmtiles.Source for the url option
* package.json works for ESM/CJS/IIFE [#312, #443]
* replace npm install with npm ci on github actions
This commit is contained in:
Brandon Liu
2024-09-11 16:36:48 +08:00
committed by GitHub
parent 089d13d637
commit ab5534df7e
11 changed files with 2429 additions and 580 deletions

13
openlayers/CHANGELOG.md Normal file
View File

@@ -0,0 +1,13 @@
## 1.0.0
* Port code to TypeScript.
* add proper CJS, ESM and IIFE build artifacts.
* `url` option to `PMTilesVectorSource`/`PMTilesRasterSource` is either string URL or a `pmtiles.Source`.
* remove option `headers`, instead create a `FetchSource` and specify custom headers:
```js
const fetchSource = new pmtiles.FetchSource(
"https://r2-public.protomaps.com/protomaps-sample-datasets/nz-buildings-v3.pmtiles",
new Headers({'X-Abc':'Def'}),
);
```

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8"/>
<script src="https://cdn.jsdelivr.net/npm/ol@v9.0.0/dist/ol.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@v9.0.0/ol.css">
<script src="https://unpkg.com/ol-pmtiles@0.5.0/dist/olpmtiles.js"></script>
<script src="https://unpkg.com/ol-pmtiles@1.0.0/dist/olpmtiles.js"></script>
<style>
body, #map {
height:100vh;

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8"/>
<script src="https://cdn.jsdelivr.net/npm/ol@v9.0.0/dist/ol.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@v9.0.0/ol.css">
<script src="https://unpkg.com/ol-pmtiles@0.5.0/dist/olpmtiles.js"></script>
<script src="https://unpkg.com/ol-pmtiles@1.0.0/dist/olpmtiles.js"></script>
<style>
body, #map {
height:100vh;

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,27 @@
{
"name": "ol-pmtiles",
"version": "0.5.0",
"version": "1.0.0",
"description": "PMTiles sources for OpenLayers",
"type": "module",
"exports": [
"./src/index.js"
],
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/cjs/index.d.cts",
"default": "./dist/cjs/index.cjs"
}
}
},
"files": [
"./dist/olpmtiles.js",
"./src/index.js"
"dist",
"src"
],
"repository": {
"type": "git",
@@ -23,15 +36,17 @@
],
"license": "BSD-3-Clause",
"scripts": {
"build-iife": "esbuild src/script_includes.js --outfile=dist/olpmtiles.js --target=es6 --global-name=olpmtiles --bundle --format=iife",
"build": "tsup",
"tsc": "tsc --noEmit --skipLibCheck",
"prettier": "prettier --write *.js",
"prettier-check": "prettier --check *.js"
},
"dependencies": {
"pmtiles": "^3.0.4"
"pmtiles": "^3.0.7"
},
"devDependencies": {
"esbuild": "^0.15.11"
"tsup": "^8.2.3",
"typescript": "^4.5.5"
},
"peerDependencies": {
"ol": ">=9.0.0"

View File

@@ -1,102 +0,0 @@
import DataTile from "ol/source/DataTile.js";
import VectorTile from "ol/source/VectorTile.js";
import TileState from "ol/TileState.js";
import { MVT } from "ol/format.js";
import * as pmtiles from "pmtiles";
export class PMTilesRasterSource extends DataTile {
loadImage = (src) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.addEventListener("load", () => resolve(img));
img.addEventListener("error", () => reject(new Error("load failed")));
img.src = src;
});
};
constructor(options) {
super({
...options,
...{
state: "loading",
},
});
const fetchSource = new pmtiles.FetchSource(
options.url,
new Headers(options.headers),
);
const p = new pmtiles.PMTiles(fetchSource);
p.getHeader().then((h) => {
this.tileGrid.minZoom = h.minZoom;
this.tileGrid.maxZoom = h.maxZoom;
this.setLoader(async (z, x, y) => {
const response = await p.getZxy(z, x, y);
const src = URL.createObjectURL(new Blob([response.data]));
const image = await this.loadImage(src);
URL.revokeObjectURL(src);
return image;
});
this.setState("ready");
});
}
}
export class PMTilesVectorSource extends VectorTile {
tileLoadFunction = (tile, url) => {
// the URL construction is done internally by OL, so we need to parse it
// back out here using a hacky regex
const re = new RegExp(/pmtiles:\/\/(.+)\/(\d+)\/(\d+)\/(\d+)/);
const result = url.match(re);
const z = +result[2];
const x = +result[3];
const y = +result[4];
tile.setLoader((extent, resolution, projection) => {
this.pmtiles_
.getZxy(z, x, y)
.then((tile_result) => {
if (tile_result) {
const format = tile.getFormat();
tile.setFeatures(
format.readFeatures(tile_result.data, {
extent: extent,
featureProjection: projection,
}),
);
tile.setState(TileState.LOADED);
} else {
tile.setFeatures([]);
tile.setState(TileState.EMPTY);
}
})
.catch((err) => {
tile.setFeatures([]);
tile.setState(TileState.ERROR);
});
});
};
constructor(options) {
super({
...options,
...{
state: "loading",
url: "pmtiles://" + options.url + "/{z}/{x}/{y}",
format: options.format || new MVT(),
},
});
const fetchSource = new pmtiles.FetchSource(
options.url,
new Headers(options.headers),
);
this.pmtiles_ = new pmtiles.PMTiles(fetchSource);
this.pmtiles_.getHeader().then((h) => {
this.tileGrid.minZoom = h.minZoom;
this.tileGrid.maxZoom = h.maxZoom;
this.setTileLoadFunction(this.tileLoadFunction);
this.setState("ready");
});
}
}

140
openlayers/src/index.ts Normal file
View File

@@ -0,0 +1,140 @@
import { type Data } from "ol/DataTile";
import {
type Options as DataTileSourceOptions,
default as DataTileSource,
} from "ol/source/DataTile";
import TileState from "ol/TileState";
import { MVT } from "ol/format";
import type TileSource from "ol/source/Tile";
import { type Extent } from "ol/extent";
import type Projection from "ol/proj/Projection";
import type Tile from "ol/Tile";
import type VectorTile from "ol/VectorTile";
import {
type Options as VectorTileSourceOptions,
default as VectorTileSource,
} from "ol/source/VectorTile";
import type RenderFeature from "ol/render/Feature";
import { createXYZ, extentFromProjection } from "ol/tilegrid";
import { PMTiles, Header, Source } from "pmtiles";
export class PMTilesRasterSource extends DataTileSource {
loadImage = (src: string): Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => {
const img = new Image();
img.addEventListener("load", () => resolve(img));
img.addEventListener("error", () => reject(new Error("load failed")));
img.src = src;
});
};
constructor(options: DataTileSourceOptions & { url: string | Source }) {
super({
...options,
...{
state: "loading",
},
});
const p = new PMTiles(options.url);
p.getHeader().then((h: Header) => {
const projection =
options.projection === undefined ? "EPSG:3857" : options.projection;
this.tileGrid =
options.tileGrid ||
createXYZ({
extent: extentFromProjection(projection),
maxResolution: options.maxResolution,
minZoom: h.minZoom,
maxZoom: h.maxZoom,
tileSize: options.tileSize,
});
this.setLoader(async (z, x, y): Promise<Data> => {
const response = await p.getZxy(z, x, y);
if (!response) {
return new Uint8Array();
}
const src = URL.createObjectURL(new Blob([response.data]));
const image = await this.loadImage(src);
URL.revokeObjectURL(src);
return image;
});
this.setState("ready");
});
}
}
export class PMTilesVectorSource extends VectorTileSource {
pmtiles_: PMTiles;
tileLoadFunction = (tile: Tile, url: string) => {
const vtile = tile as VectorTile;
// the URL construction is done internally by OL, so we need to parse it
// back out here using a hacky regex
const re = new RegExp(/pmtiles:\/\/(\d+)\/(\d+)\/(\d+)/);
const result = url.match(re);
if (!(result && result.length >= 4)) {
throw Error("Could not parse tile URL");
}
const z = +result[1];
const x = +result[2];
const y = +result[3];
vtile.setLoader(
(extent: Extent, resolution: number, projection: Projection) => {
this.pmtiles_
.getZxy(z, x, y)
.then((tile_result) => {
if (tile_result) {
const format = vtile.getFormat();
vtile.setFeatures(
format.readFeatures(tile_result.data, {
extent: extent,
featureProjection: projection,
}),
);
vtile.setState(TileState.LOADED);
} else {
vtile.setFeatures([]);
vtile.setState(TileState.EMPTY);
}
})
.catch((err) => {
vtile.setFeatures([]);
vtile.setState(TileState.ERROR);
});
},
);
};
constructor(
options: VectorTileSourceOptions<RenderFeature> & { url: string | Source },
) {
super({
...options,
...{
state: "loading",
url: "pmtiles://{z}/{x}/{y}",
format: options.format || new MVT(),
},
});
this.pmtiles_ = new PMTiles(options.url);
this.pmtiles_.getHeader().then((h: Header) => {
const projection = options.projection || "EPSG:3857";
const extent = options.extent || extentFromProjection(projection);
this.tileGrid =
options.tileGrid ||
createXYZ({
extent: extent,
maxResolution: options.maxResolution,
maxZoom: options.maxZoom !== undefined ? options.maxZoom : h.maxZoom,
minZoom: h.minZoom,
tileSize: options.tileSize || 512,
});
this.setTileLoadFunction(this.tileLoadFunction);
this.setState("ready");
});
}
}

View File

@@ -1,108 +0,0 @@
// IMPORTANT: this file is manually edited!
// copy any changes made from src/index.js to here
// to automate this we need a rollup or esbuild script
// to resolve the imports to the ol global correctly
// as well as get the enum values of TileState, which is elided
// import DataTile from "ol/source/DataTile";
// import VectorTile from "ol/source/VectorTile";
// import TileState from "ol/TileState";
// import { MVT } from "ol/format";
import * as pmtiles from "pmtiles";
export class PMTilesRasterSource extends ol.source.DataTile {
loadImage = (src) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.addEventListener("load", () => resolve(img));
img.addEventListener("error", () => reject(new Error("load failed")));
img.src = src;
});
};
constructor(options) {
super({
...options,
...{
state: "loading",
},
});
const fetchSource = new pmtiles.FetchSource(
options.url,
new Headers(options.headers),
);
const p = new pmtiles.PMTiles(fetchSource);
p.getHeader().then((h) => {
this.tileGrid.minZoom = h.minZoom;
this.tileGrid.maxZoom = h.maxZoom;
this.setLoader(async (z, x, y) => {
const response = await p.getZxy(z, x, y);
const src = URL.createObjectURL(new Blob([response.data]));
const image = await this.loadImage(src);
URL.revokeObjectURL(src);
return image;
});
this.setState("ready");
});
}
}
export class PMTilesVectorSource extends ol.source.VectorTile {
tileLoadFunction = (tile, url) => {
// the URL construction is done internally by OL, so we need to parse it
// back out here using a hacky regex
const re = new RegExp(/pmtiles:\/\/(.+)\/(\d+)\/(\d+)\/(\d+)/);
const result = url.match(re);
const z = +result[2];
const x = +result[3];
const y = +result[4];
tile.setLoader((extent, resolution, projection) => {
this.pmtiles_
.getZxy(z, x, y)
.then((tile_result) => {
if (tile_result) {
const format = tile.getFormat();
tile.setFeatures(
format.readFeatures(tile_result.data, {
extent: extent,
featureProjection: projection,
}),
);
tile.setState(2);
} else {
tile.setFeatures([]);
tile.setState(4);
}
})
.catch((err) => {
tile.setFeatures([]);
tile.setState(3);
});
});
};
constructor(options) {
super({
...options,
...{
state: "loading",
url: "pmtiles://" + options.url + "/{z}/{x}/{y}",
format: options.format || new ol.format.MVT(),
},
});
const fetchSource = new pmtiles.FetchSource(
options.url,
new Headers(options.headers),
);
this.pmtiles_ = new pmtiles.PMTiles(fetchSource);
this.pmtiles_.getHeader().then((h) => {
this.tileGrid.minZoom = h.minZoom;
this.tileGrid.maxZoom = h.maxZoom;
this.setTileLoadFunction(this.tileLoadFunction);
this.setState("ready");
});
}
}

13
openlayers/tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es2021",
"lib": ["es2021", "dom"],
"strict": true,
"moduleResolution": "node",
"paths": {
},
"types": [],
"allowSyntheticDefaultImports": true
},
"include": ["src/*.ts"]
}

42
openlayers/tsup.config.ts Normal file
View File

@@ -0,0 +1,42 @@
import { defineConfig, type Options } from "tsup";
const baseOptions: Options = {
clean: true,
minify: false,
skipNodeModulesBundle: true,
sourcemap: true,
target: "es6",
tsconfig: "./tsconfig.json",
keepNames: true,
cjsInterop: true,
splitting: true,
};
export default [
defineConfig({
...baseOptions,
entry: ["src/index.ts"],
outDir: "dist/cjs",
format: "cjs",
dts: true,
}),
defineConfig({
...baseOptions,
entry: ["src/index.ts"],
outDir: "dist/esm",
format: "esm",
dts: true,
}),
defineConfig({
...baseOptions,
outDir: "dist",
format: "iife",
globalName: "olpmtiles",
entry: {
"olpmtiles": "src/index.ts",
},
outExtension: () => {
return { js: ".js" };
},
}),
];