diff --git a/openlayers/README.md b/openlayers/README.md new file mode 100644 index 0000000..7ed11ba --- /dev/null +++ b/openlayers/README.md @@ -0,0 +1,75 @@ +# PMTiles for OpenLayers + +## Example Usage + +Based on the [OpenLayers Quick Start](https://openlayers.org/doc/quickstart.html) + +`npm install ol-pmtiles` + +### Raster tiles + +```js +import "./style.css"; +import { Map, View } from "ol"; +import WebGLTile from "ol/layer/WebGLTile"; +import { PMTilesRasterSource } from "ol-pmtiles"; +import { useGeographic } from 'ol/proj'; + +const rasterLayer = new WebGLTile({ + source: new PMTilesRasterSource({ + url:"https://r2-public.protomaps.com/protomaps-sample-datasets/terrarium_z9.pmtiles", + attributions:["https://github.com/tilezen/joerd/blob/master/docs/attribution.md"], + tileSize: [512,512] + }) +}); + +useGeographic(); + +const map = new Map({ + target: "map", + layers: [rasterLayer], + view: new View({ + center: [0,0], + zoom: 1, + }), +}); +``` + +### Vector tiles + +```js +import "./style.css"; +import { Map, View } from "ol"; +import VectorTile from "ol/layer/VectorTile"; +import { PMTilesVectorSource } from "ol-pmtiles"; +import { Style, Stroke, Fill } from 'ol/style'; +import { useGeographic } from 'ol/proj'; + +const vectorLayer = new VectorTile({ + declutter: true, + source: new PMTilesVectorSource({ + url: "https://r2-public.protomaps.com/protomaps-sample-datasets/nz-buildings-v3.pmtiles", + attributions: ["© Land Information New Zealand"], + }), + style: new Style({ + stroke: new Stroke({ + color: "gray", + width: 1, + }), + fill: new Fill({ + color: "rgba(20,20,20,0.9)", + }), + }), +}); + +useGeographic(); + +const map = new Map({ + target: "map", + layers: [vectorLayer], + view: new View({ + center: [172.606201,-43.556510], + zoom: 7, + }), +}); +``` diff --git a/openlayers/package-lock.json b/openlayers/package-lock.json new file mode 100644 index 0000000..565f0ce --- /dev/null +++ b/openlayers/package-lock.json @@ -0,0 +1,302 @@ +{ + "name": "ol-pmtiles", + "version": "0.0.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ol-pmtiles", + "version": "0.0.2", + "license": "BSD-3-Clause", + "dependencies": { + "pmtiles": "^2.7.0" + }, + "devDependencies": {}, + "peerDependencies": { + "ol": ">=7.3.0" + } + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/mapbox-gl-style-spec": { + "version": "13.28.0", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.28.0.tgz", + "integrity": "sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg==", + "peer": true, + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/unitbezier": "^0.0.0", + "csscolorparser": "~1.0.2", + "json-stringify-pretty-compact": "^2.0.0", + "minimist": "^1.2.6", + "rw": "^1.3.3", + "sort-object": "^0.3.2" + }, + "bin": { + "gl-style-composite": "bin/gl-style-composite.js", + "gl-style-format": "bin/gl-style-format.js", + "gl-style-migrate": "bin/gl-style-migrate.js", + "gl-style-validate": "bin/gl-style-validate.js" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", + "peer": true + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", + "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==", + "peer": true + }, + "node_modules/@petamoriken/float16": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.8.0.tgz", + "integrity": "sha512-AhVAm6SQ+zgxIiOzwVdUcDmKlu/qU39FiYD2UD6kQQaVenrn0dGZewIghWAENGQsvC+1avLCuT+T2/3Gsp/W3w==", + "peer": true + }, + "node_modules/csscolorparser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", + "peer": true + }, + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", + "peer": true + }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==" + }, + "node_modules/geotiff": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.0.7.tgz", + "integrity": "sha512-FKvFTNowMU5K6lHYY2f83d4lS2rsCNdpUC28AX61x9ZzzqPNaWFElWv93xj0eJFaNyOYA63ic5OzJ88dHpoA5Q==", + "peer": true, + "dependencies": { + "@petamoriken/float16": "^3.4.7", + "lerc": "^3.0.0", + "pako": "^2.0.4", + "parse-headers": "^2.0.2", + "quick-lru": "^6.1.1", + "web-worker": "^1.2.0", + "xml-utils": "^1.0.2" + }, + "engines": { + "node": ">=10.19" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "peer": true + }, + "node_modules/json-stringify-pretty-compact": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz", + "integrity": "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==", + "peer": true + }, + "node_modules/lerc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz", + "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==", + "peer": true + }, + "node_modules/mapbox-to-css-font": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-2.4.2.tgz", + "integrity": "sha512-f+NBjJJY4T3dHtlEz1wCG7YFlkODEjFIYlxDdLIDMNpkSksqTt+l/d4rjuwItxuzkuMFvPyrjzV2lxRM4ePcIA==", + "peer": true + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ol": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ol/-/ol-7.3.0.tgz", + "integrity": "sha512-08vJE4xITKPazQ9qJjeqYjRngnM9s+1eSv219Pdlrjj3LpLqjEH386ncq+76Dw1oGPGR8eLVEePk7FEd9XqqMw==", + "peer": true, + "dependencies": { + "earcut": "^2.2.3", + "geotiff": "^2.0.7", + "ol-mapbox-style": "^9.2.0", + "pbf": "3.2.1", + "rbush": "^3.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/openlayers" + } + }, + "node_modules/ol-mapbox-style": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-9.7.0.tgz", + "integrity": "sha512-YX3u8FBJHsRHaoGxmd724Mp5WPTuV7wLQW6zZhcihMuInsSdCX1EiZfU+8IAL7jG0pbgl5YgC0aWE/MXJcUXxg==", + "peer": true, + "dependencies": { + "@mapbox/mapbox-gl-style-spec": "^13.23.1", + "mapbox-to-css-font": "^2.4.1" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "peer": true + }, + "node_modules/parse-headers": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", + "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==", + "peer": true + }, + "node_modules/pbf": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", + "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", + "peer": true, + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/pmtiles": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-2.7.2.tgz", + "integrity": "sha512-YRdfLlvN5z94F5/e3cnEFE0TymKx6egi0vDx6NJF83rYGNLV6Lr2PsdpWkSo3oVl4e6LVj8RvaWLfpT874QH+Q==", + "dependencies": { + "fflate": "^0.7.3" + } + }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "peer": true + }, + "node_modules/quick-lru": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.1.tgz", + "integrity": "sha512-S27GBT+F0NTRiehtbrgaSE1idUAJ5bX8dPAQTdylEyNlrdcH5X4Lz7Edz3DYzecbsCluD5zO8ZNEe04z3D3u6Q==", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "peer": true + }, + "node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "peer": true, + "dependencies": { + "quickselect": "^2.0.0" + } + }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "peer": true, + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "peer": true + }, + "node_modules/sort-asc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz", + "integrity": "sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-desc": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.1.1.tgz", + "integrity": "sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-object": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-0.3.2.tgz", + "integrity": "sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA==", + "peer": true, + "dependencies": { + "sort-asc": "^0.1.0", + "sort-desc": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/web-worker": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", + "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==", + "peer": true + }, + "node_modules/xml-utils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.7.0.tgz", + "integrity": "sha512-bWB489+RQQclC7A9OW8e5BzbT8Tu//jtAOvkYwewFr+Q9T9KDGvfzC1lp0pYPEQPEoPQLDkmxkepSC/2gIAZGw==", + "peer": true + } + } +} diff --git a/openlayers/package.json b/openlayers/package.json index e69de29..77452a7 100644 --- a/openlayers/package.json +++ b/openlayers/package.json @@ -0,0 +1,34 @@ +{ + "name": "ol-pmtiles", + "version": "0.0.2", + "description": "PMTiles sources for OpenLayers", + "type": "module", + "main": "src/index.js", + "files": [ + "src/*" + ], + "repository": { + "type": "git", + "url": "git://github.com/protomaps/PMTiles.git" + }, + "bugs": { + "url": "https://github.com/protomaps/PMTiles/issues" + }, + "keywords": [ + "openlayers", + "pmtiles" + ], + "license": "BSD-3-Clause", + "scripts": { + "prettier": "prettier --write *.js", + "prettier-check": "prettier --check *.js" + }, + "dependencies": { + "pmtiles": "^2.7.0" + }, + "devDependencies": { + }, + "peerDependencies": { + "ol": ">=7.3.0" + } +} diff --git a/openlayers/src/PMTilesRasterSource.js b/openlayers/src/PMTilesRasterSource.js new file mode 100644 index 0000000..bb53707 --- /dev/null +++ b/openlayers/src/PMTilesRasterSource.js @@ -0,0 +1,38 @@ +import DataTile from "ol/source/DataTile"; +import * as pmtiles from "pmtiles"; + +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({ + state: "loading", + attributions: options.attributions, + tileSize: options.tileSize, + }); + + const p = new pmtiles.PMTiles(options.url); + 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 blob = new Blob([response.data]); + const src = URL.createObjectURL(blob); + const image = await this.loadImage(src); + URL.revokeObjectURL(src); + return image; + }); + this.setState("ready"); + }); + } +} + +export default PMTilesRasterSource; diff --git a/openlayers/src/PMTilesVectorSource.js b/openlayers/src/PMTilesVectorSource.js new file mode 100644 index 0000000..557f645 --- /dev/null +++ b/openlayers/src/PMTilesVectorSource.js @@ -0,0 +1,53 @@ +import VectorTile from "ol/source/VectorTile"; +import TileState from "ol/TileState"; +import { MVT } from "ol/format"; +import * as pmtiles from "pmtiles"; + +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) => { + tile.setState(TileState.LOADING); + this._p.getZxy(z, x, y).then((tile_result) => { + if (tile_result) { + const format = tile.getFormat(); + const features = format.readFeatures(tile_result.data.buffer, { + extent: extent, + featureProjection: projection, + }); + tile.setFeatures(features); + tile.setState(TileState.LOADED); + } else { + tile.setFeatures([]); + tile.setState(TileState.EMPTY); + } // TODO error state + }); + }); + }; + + constructor(options) { + super({ + state: "loading", + url: "pmtiles://" + options.url + "/{z}/{x}/{y}", + format: new MVT(), + attributions: options.attributions, + }); + + this._p = new pmtiles.PMTiles(options.url); + this._p.getHeader().then((h) => { + this.tileGrid.minZoom = h.minZoom; + this.tileGrid.maxZoom = h.maxZoom; + this.setTileLoadFunction(this.tileLoadFunction); + this.setState("ready"); + }); + } +} + +export default PMTilesVectorSource; diff --git a/openlayers/src/index.js b/openlayers/src/index.js new file mode 100644 index 0000000..08ee71e --- /dev/null +++ b/openlayers/src/index.js @@ -0,0 +1,2 @@ +export { default as PMTilesRasterSource } from './PMTilesRasterSource.js'; +export { default as PMTilesVectorSource } from './PMTilesVectorSource.js'; \ No newline at end of file