diff --git a/dev-docs/RFCs/v3.4/data-source-rfc.md b/dev-docs/RFCs/v3.4/data-source-rfc.md
new file mode 100644
index 0000000000..52e8ba707a
--- /dev/null
+++ b/dev-docs/RFCs/v3.4/data-source-rfc.md
@@ -0,0 +1,41 @@
+# Data Sources API
+
+Build a data source API that can encompass services such
+- loaded data
+- URLS
+- tile service
+- WMS
+- Incremental fetch with range requests etc.
+- programmatic data generation
+- ...
+
+### Related
+
+- deck.gl has a semi-internal data source API.
+-
+
+
+
+## Main problems
+
+### Refresh / Dirty state handling.
+
+How does the application (typically deck.gl)) know when to redraw?
+
+```typescript
+DataSource.setNeedsRefresh();
+DataSource.getNeedsRefresh(clear: boolean = true);
+```
+
+## Updates
+
+`DataSource.setProps()`
+
+Typing is a bit messy when overriding child class definitions.
+
+## Declarative usage
+
+Fully declarative usage requires a lifecycle management system, which seems too heavy.
+
+
+
diff --git a/docs/modules/pmtiles/README.md b/docs/modules/pmtiles/README.md
new file mode 100644
index 0000000000..489f7ea2ff
--- /dev/null
+++ b/docs/modules/pmtiles/README.md
@@ -0,0 +1,5 @@
+# @loaders.gl/pmtiles
+
+> The pmtiles loaders are still under development and are not yet considered ready for use.
+
+Support for loading and traversing [pmtiles](http://potree.org/) format point clouds.
diff --git a/docs/modules/pmtiles/api-reference/pmtiles-loader.md b/docs/modules/pmtiles/api-reference/pmtiles-loader.md
new file mode 100644
index 0000000000..7c0d5f2579
--- /dev/null
+++ b/docs/modules/pmtiles/api-reference/pmtiles-loader.md
@@ -0,0 +1,27 @@
+# PMTilesLoader
+
+The `PMTilesLoader` parses header/metadata from a pmtiles archive
+
+| Loader | Characteristic |
+| --------------------- | --------------------------------------------- |
+| File Extension | `.pmtiles` |
+| File Type | Binary/Text |
+| File Format | [PLY](http://paulbourke.net/dataformats/ply/) |
+| Data Format | [Mesh](/docs/specifications/category-mesh) |
+| Decoder Type | Synchronous |
+| Worker Thread Support | Yes |
+| Streaming Support | No |
+
+## Usage
+
+```typescript
+import {PMTilesLoader} from '@loaders.gl/ply';
+import {load} from '@loaders.gl/core';
+
+const data = await load(url, PMTilesLoader, options);
+```
+
+## Options
+
+| Option | Type | Default | Description |
+| ------ | ---- | ------- | ----------- |
diff --git a/docs/modules/pmtiles/api-reference/pmtiles-source.md b/docs/modules/pmtiles/api-reference/pmtiles-source.md
new file mode 100644
index 0000000000..dc6ab52a72
--- /dev/null
+++ b/docs/modules/pmtiles/api-reference/pmtiles-source.md
@@ -0,0 +1,27 @@
+# PMTilesSource
+
+The `PMTilesSource` parses simple meshes in the Polygon File Format or the Stanford Triangle Format.
+
+| Loader | Characteristic |
+| --------------------- | --------------------------------------------- |
+| File Extension | `.ply` |
+| File Type | Binary/Text |
+| File Format | [PLY](http://paulbourke.net/dataformats/ply/) |
+| Data Format | [Mesh](/docs/specifications/category-mesh) |
+| Decoder Type | Synchronous |
+| Worker Thread Support | Yes |
+| Streaming Support | No |
+
+## Usage
+
+```typescript
+import {PMTilesSource} from '@loaders.gl/ply';
+import {load} from '@loaders.gl/core';
+
+const data = await load(url, PMTilesSource, options);
+```
+
+## Options
+
+| Option | Type | Default | Description |
+| ------ | ---- | ------- | ----------- |
diff --git a/docs/modules/pmtiles/formats/pmtiles.md b/docs/modules/pmtiles/formats/pmtiles.md
new file mode 100644
index 0000000000..b0ab32770c
--- /dev/null
+++ b/docs/modules/pmtiles/formats/pmtiles.md
@@ -0,0 +1,161 @@
+# PMTiles
+
+PMTiles is a single-file archive format for tiled data. A PMTiles archive can be hosted on a commodity storage platform such as Amazon S3.
+
+- https://github.com/protomaps/PMTiles
+
+
+## Versions
+
+## Version 3
+
+- File Structure
+97% smaller overhead - Spec version 2 would always issue a 512 kilobyte initial request; version 3 reduces this to 16 kilobytes. What remains the same is that nearly any map tile can be retrieved in at most two additional requests.
+- Unlimited metadata - version 2 had a hard cap on the amount of JSON metadata of about 300 kilobytes; version 3 removes this limit. This is essential for tools like tippecanoe to store detailed column statistics. Essential archive information, such as tile type and compression methods, are stored in a binary header separate from application metadata.
+- Hilbert tile IDs - tiles internally are addressed by a single 64-bit Hilbert tile ID instead of Z/X/Y. See the blog post on Tile IDs for details.
+- Archive ordering - An optional clustered mode enforces that tile contents are laid out in Tile ID order.
+- Compressed directories and metadata - Directories used to fetch offsets of tile data consume about 10% the space of those in version 2. See the blog post on compressed directories for details.
+- JavaScript
+Compression - The TypeScript pmtiles library now includes a decompressor - fflate - to allow reading compressed vector tile archives directly in the browser. This reduces the size and latency of vector tiles by as much as 70%.
+- Tile Cancellation - All JavaScript plugins now support tile cancellation, meaning quick zooming across many levels will interrupt the loading of tiles that are never shown. This has a significant effect on the perceived user experience, as tiles at the end of a animation will appear earlier.
+- ETag support - clients can detect when files change on static storage by reading the ETag HTTP header. This means that PMTiles-based map applications can update datasets in place at low frequency without running into caching problems.
+
+## Version 3 Specification
+
+### File structure
+
+A PMTiles archive is a single-file archive of square tiles with five main sections:
+
+1. A fixed-size, 127-byte **Header** starting with `PMTiles` and then the spec version - currently `3` - that contains offsets to the next sections.
+2. A root **Directory**, described below. The Header and Root combined must be less than 16,384 bytes.
+3. JSON metadata.
+4. Optionally, a section of **Leaf Directories**, encoded the same way as the root.
+5. The tile data.
+
+### Entries
+
+A Directory is a list of `Entries`, in ascending order by `TileId`:
+
+ Entry = (TileId uint64, Offset uint64, Length uint32, RunLength uint32)
+
+* `TileId` starts at 0 and corresponds to a cumulative position on the series of square Hilbert curves starting at z=0.
+* `Offset` is the position of the tile in the file relative to the start of the data section.
+* `Length` is the size of the tile in bytes.
+* `RunLength` is how many times this tile is repeated: the `TileId=5,RunLength=2` means that tile is present at IDs 5 and 6.
+* If `RunLength=0`, the offset/length points to a Leaf Directory where `TileId` is the first entry.
+
+### Directory Serialization
+
+Entries are stored in memory as integers, but serialized to disk using these compression steps:
+
+1. A little-endian varint indicating the # of entries
+2. Delta encoding of `TileId`
+3. Zeroing of `Offset`:
+ * `0` if it is equal to the `Offset` + `Length` of the previous entry
+ * `Offset+1` otherwise
+4. Varint encoding of all numbers
+5. Columnar ordering: all `TileId`s, all `RunLength`s, all `Length`s, then all `Offset`s
+6. Finally, general purpose compression as described by the `Header`'s `InternalCompression` field
+
+##3 Directory Hierarchy
+
+* The number of entries in the root directory and leaf directories is up to the implementation.
+* However, the compressed size of the header plus root directory is required in v3 to be under **16,384 bytes**. This is to allow latency-optimized clients to prefetch the root directory and guarantee it is complete. A sophisticated writer might need several attempts to optimize this.
+* Root size, leaf sizes and depth should be configurable by the user to optimize for different trade-offs: cost, bandwidth, latency.
+
+### Header Design
+
+*Certain fields belonging to metadata in v2 are promoted to fixed-size header fields. This allows a map container to be initialized to the desired extent or center without blocking on the JSON metadata, and allows proxies to return well-defined HTTP headers.*
+
+The `Header` is 127 bytes, with little-endian integer values:
+
+| offset | description | width |
+| ------ | ----------------------------------------------------------------------------------------- | ----- |
+| 0 | magic number `PMTiles` | 7 |
+| 7 | spec version, currently `3` | 1 |
+| 8 | offset of root directory | 8 |
+| 16 | length of root directory | 8 |
+| 24 | offset of JSON metadata, possibly compressed by `InternalCompression` | 8 |
+| 32 | length of JSON metadata | 8 |
+| 40 | offset of leaf directories | 8 |
+| 48 | length of leaf directories | 8 |
+| 56 | offset of tile data | 8 |
+| 64 | length of tile data | 8 |
+| 72 | # of addressed tiles, 0 if unknown | 8 |
+| 80 | # of tile entries, 0 if unknown | 8 |
+| 88 | # of tile contents, 0 if unknown | 8 |
+| 96 | boolean clustered flag, `0x1` if true | 1 |
+| 97 | `InternalCompression` enum (0 = Unknown, 1 = None, 2 = Gzip, 3 = Brotli, 4 = Zstd) | 1 |
+| 98 | `TileCompression` enum | 1 |
+| 99 | tile type enum (0 = Unknown/Other, 1 = MVT (PBF Vector Tile), 2 = PNG, 3 = JPEG, 4 = WEBP | 1 |
+| 100 | min zoom | 1 |
+| 101 | max zoom | 1 |
+| 102 | min longitude (signed 32-bit integer: longitude * 10,000,000) | 4 |
+| 106 | min latitude | 4 |
+| 110 | max longitude | 4 |
+| 114 | max latitude | 4 |
+| 118 | center zoom | 1 |
+| 119 | center longitude | 4 |
+| 123 | center latitude | 4 |
+
+### Notes
+
+* **# of addressed tiles**: the total number of tiles before run-length encoding, i.e. `Sum(RunLength)` over all entries.
+* **# of tile entries**: the total number of entries across all directories where `RunLength > 0`.
+* **# # of tile contents**: the number of referenced blobs in the tile section, or the unique # of offsets. If the archive is completely deduplicated, this is equal to the # of unique tile contents. If there is no deduplication, this is equal to the number of tile entries above.
+* **boolean clustered flag**: if true, blobs in the data section are ordered by Hilbert `TileId`. When writing with deduplication, this means that offsets are either contiguous with the previous offset+length, or refer to a lesser offset.
+* **compression enum**: Mandatory, tells the client how to decompress contents as well as provide correct `Content-Encoding` headers to browsers.
+* **tile type**: A hint as to the tile contents. Clients and proxies may use this to:
+ * Automatically determine a visualization method
+ * provide a conventional MIME type `Content-Type` HTTP header
+ * Enforce a canonical extension e.g. `.mvt`, `png`, `jpeg`, `.webp` to prevent duplication in caches
+
+### Organization
+
+In most cases, the archive should be in the order `Header`, Root Directory, JSON Metadata, Leaf Directories, Tile Data. It is possible to relocate sections other than `Header` arbitrarily, but no current writers/readers take advantage of this. A future design may allow for reverse-ordered archives to enable single-pass writing.
+
+
+## Version 2
+
+*Note: this is deprecated in favor of spec version 3.*
+
+PMTiles is a binary serialization format designed for two main access patterns: over the network, via HTTP 1.1 Byte Serving (`Range:` requests), or via memory-mapped files on disk. **All integer values are little-endian.**
+
+A PMTiles archive is composed of:
+* a fixed-size 512,000 byte header section
+* Followed by any number of tiles in arbitrary format
+* Optionally followed by any number of *leaf directories*
+
+### Header
+* The header begins with a 2-byte magic number, "PM"
+* Followed by 2 bytes, the PMTiles specification version (currently 2).
+* Followed by 4 bytes, the length of metadata (M bytes)
+* Followed by 2 bytes, the number of entries in the *root directory* (N entries)
+* Followed by M bytes of metadata, which **must be a JSON string with bounds, minzoom and maxzoom properties (new in v2)**
+* Followed by N * 17 bytes, the root directory.
+
+### Directory structure
+
+A directory is a contiguous sequence of 17 byte entries. A directory can have at most 21,845 entries. **A directory must be sorted by Z, X and then Y order (new in v2).**
+
+An entry consists of:
+* 1 byte: the zoom level (Z) of the entry, with the top bit set to 1 instead of 0 to indicate the offset/length points to a leaf directory and not a tile.
+* 3 bytes: the X (column) of the entry.
+* 3 bytes: the Y (row) of the entry.
+* 6 bytes: the offset of where the tile begins in the archive.
+* 4 bytes: the length of the tile, in bytes.
+
+**All leaf directory entries follow non-leaf entries. All leaf directories in a single directory must have the same Z value. (new in v2).**
+
+### Notes
+* A full directory of 21,845 entries holds exactly a complete pyramid with 8 levels, or 1+4+16+64+256+1024+4096+16384.
+* A PMTiles archive with less than 21,845 tiles should have a root directory and no leaf directories.
+* Multiple tile entries can point to the same offset; this is useful for de-duplicating certain tiles, such as an empty "ocean" tile.
+* Analogously, multiple leaf directory entries can point to the same offset; this can avoid inefficiently-packed small leaf directories.
+* The tentative media type for PMTiles archives is `application/vnd.pmtiles`.
+
+### Implementation suggestions
+
+* PMTiles is designed to make implementing a writer simple. Reserve 512KB, then write all tiles, recording their entry information; then write all leaf directories; finally, rewind to 0 and write the header.
+* The order of tile data in the archive is unspecified; an optimized implementation should arrange tiles on a 2D space-filling curve.
+* PMTiles readers should cache directory entries by byte offset, not by Z/X/Y. This means that deduplicated leaf directories result in cache hits.
\ No newline at end of file
diff --git a/examples/website/geospatial/package.json b/examples/website/geospatial/package.json
index 4eef7b48fa..8c1235211d 100644
--- a/examples/website/geospatial/package.json
+++ b/examples/website/geospatial/package.json
@@ -16,6 +16,7 @@
"@loaders.gl/core": "4.0.0-alpha.23",
"@loaders.gl/flatgeobuf": "4.0.0-alpha.23",
"@loaders.gl/geopackage": "4.0.0-alpha.23",
+ "@loaders.gl/parquet": "4.0.0-alpha.23",
"mapbox-gl": "npm:empty-npm-package@^1.0.0",
"maplibre-gl": "^2.4.0",
"react": "^18.0.0",
diff --git a/examples/website/tiles/app.tsx b/examples/website/tiles/app.tsx
new file mode 100644
index 0000000000..e2807052b0
--- /dev/null
+++ b/examples/website/tiles/app.tsx
@@ -0,0 +1,131 @@
+// loaders.gl, MIT license
+
+import React from 'react';
+import {createRoot} from 'react-dom/client';
+
+import DeckGL from '@deck.gl/react/typed';
+import {MapView} from '@deck.gl/core/typed';
+import {TileLayer} from '@deck.gl/geo-layers/typed';
+import {BitmapLayer, PathLayer} from '@deck.gl/layers/typed';
+import {PMTilesImageSource} from '@loaders.gl/pmtiles';
+
+const INITIAL_VIEW_STATE = {
+ latitude: 47.65,
+ longitude: 7,
+ zoom: 4.5,
+ maxZoom: 20,
+ maxPitch: 89,
+ bearing: 0
+};
+
+const COPYRIGHT_LICENSE_STYLE = {
+ position: 'absolute',
+ right: 0,
+ bottom: 0,
+ backgroundColor: 'hsla(0,0%,100%,.5)',
+ padding: '0 5px',
+ font: '12px/20px Helvetica Neue,Arial,Helvetica,sans-serif'
+};
+
+const LINK_STYLE = {
+ textDecoration: 'none',
+ color: 'rgba(0,0,0,.75)',
+ cursor: 'grab'
+};
+
+/* global window */
+const devicePixelRatio = (typeof window !== 'undefined' && window.devicePixelRatio) || 1;
+
+function getTooltip({tile}) {
+ if (tile) {
+ const {x, y, z} = tile.index;
+ return `tile: x: ${x}, y: ${y}, z: ${z}`;
+ }
+ return null;
+}
+
+// const vectorTileSource = new PMTilesVectorSource({
+// url: "https://r2-public.protomaps.com/protomaps-sample-datasets/nz-buildings-v3.pmtiles",
+// attributions: ["© Land Information New Zealand"],
+// });
+
+const rasterTileSource = new PMTilesImageSource({
+ 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]
+});
+
+export default function App({showBorder = false, onTilesLoad = null}) {
+ const tileLayer = new TileLayer({
+ // https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
+ data: [
+ 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png',
+ 'https://b.tile.openstreetmap.org/{z}/{x}/{y}.png',
+ 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png'
+ ],
+ getTileData: rasterTileSource.getTileData,
+ // Since these OSM tiles support HTTP/2, we can make many concurrent requests
+ // and we aren't limited by the browser to a certain number per domain.
+ maxRequests: 20,
+
+ pickable: true,
+ onViewportLoad: onTilesLoad,
+ autoHighlight: showBorder,
+ highlightColor: [60, 60, 60, 40],
+ // https://wiki.openstreetmap.org/wiki/Zoom_levels
+ minZoom: 0,
+ maxZoom: 19,
+ tileSize: 256,
+ zoomOffset: devicePixelRatio === 1 ? -1 : 0,
+ renderSubLayers: props => {
+ const {
+ bbox: {west, south, east, north}
+ } = props.tile;
+
+ return [
+ new BitmapLayer(props, {
+ data: null,
+ image: props.data,
+ bounds: [west, south, east, north]
+ }),
+ // showBorder &&
+ // new PathLayer({
+ // id: `${props.id}-border`,
+ // data: [
+ // [
+ // [west, north],
+ // [west, south],
+ // [east, south],
+ // [east, north],
+ // [west, north]
+ // ]
+ // ],
+ // getPath: d => d,
+ // getColor: [255, 0, 0],
+ // widthMinPixels: 4
+ // })
+ ];
+ }
+ });
+
+ return (
+
+
+
+ );
+}
+
+export function renderToDOM(container) {
+ createRoot(container).render();
+}
diff --git a/examples/website/tiles/index.html b/examples/website/tiles/index.html
new file mode 100644
index 0000000000..b09a17fedd
--- /dev/null
+++ b/examples/website/tiles/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+ Tiled Loaders
+
+
+
+
+
+
+
+
diff --git a/examples/website/tiles/package.json b/examples/website/tiles/package.json
new file mode 100644
index 0000000000..f5d6a3529d
--- /dev/null
+++ b/examples/website/tiles/package.json
@@ -0,0 +1,31 @@
+{
+ "private": true,
+ "name": "geospatial-loaders-example",
+ "description": "Minimal example of using loaders.gl with vite.",
+ "version": "0.0.0",
+ "license": "MIT",
+ "scripts": {
+ "start": "vite",
+ "build": "tsc && vite build",
+ "serve": "vite preview"
+ },
+ "dependencies": {
+ "@deck.gl/core": "^8.9.28",
+ "@deck.gl/layers": "^8.9.28",
+ "@deck.gl/geo-layers": "^8.9.28",
+ "@deck.gl/mesh-layers": "^8.9.28",
+ "@deck.gl/extensions": "8.9.28",
+ "@deck.gl/react": "^8.9.28",
+ "@loaders.gl/core": "4.0.0-alpha.26",
+ "mapbox-gl": "npm:empty-npm-package@^1.0.0",
+ "maplibre-gl": "^2.4.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0",
+ "react-map-gl": "^7.0.0",
+ "styled-components": "^4.2.0"
+ },
+ "devDependencies": {
+ "typescript": "^5.0.4",
+ "vite": "^4.4.9"
+ }
+}
diff --git a/examples/website/geospatial/tsconfig.json b/examples/website/tiles/tsconfig.json
similarity index 94%
rename from examples/website/geospatial/tsconfig.json
rename to examples/website/tiles/tsconfig.json
index 49a2fb115e..f614f74e8a 100644
--- a/examples/website/geospatial/tsconfig.json
+++ b/examples/website/tiles/tsconfig.json
@@ -16,5 +16,5 @@
"noEmit": true,
"jsx": "react-jsx"
},
- "include": ["./app.js"]
+ "include": ["."]
}
diff --git a/examples/website/geospatial/vite.config.ts b/examples/website/tiles/vite.config.ts
similarity index 100%
rename from examples/website/geospatial/vite.config.ts
rename to examples/website/tiles/vite.config.ts
diff --git a/modules/core/src/lib/filesystems/read-array-buffer.ts b/modules/core/src/lib/filesystems/read-array-buffer.ts
index 6ac17582cd..0cdc1cd4a6 100644
--- a/modules/core/src/lib/filesystems/read-array-buffer.ts
+++ b/modules/core/src/lib/filesystems/read-array-buffer.ts
@@ -1,5 +1,11 @@
// Random-Access read
+/**
+ * Read a slice of a Blob or File, without loading the entire file into memory
+ * The trick when reading File objects is to read successive "slices" of the File
+ * Per spec https://w3c.github.io/FileAPI/, slicing a File only updates the start and end fields
+ * @param file to read
+ */
export async function readArrayBuffer(
file: Blob | ArrayBuffer | any,
start: number,
@@ -11,21 +17,3 @@ export async function readArrayBuffer(
}
return await file.read(start, start + length);
}
-
-/**
- * Read a slice of a Blob or File, without loading the entire file into memory
- * The trick when reading File objects is to read successive "slices" of the File
- * Per spec https://w3c.github.io/FileAPI/, slicing a File only updates the start and end fields
- * Actually reading from file happens in `readAsArrayBuffer`
- * @param blob to read
- export async function readBlob(blob: Blob): Promise {
- return await new Promise((resolve, reject) => {
- const fileReader = new FileReader();
- fileReader.onload = (event: ProgressEvent) =>
- resolve(event?.target?.result as ArrayBuffer);
- // TODO - reject with a proper Error
- fileReader.onerror = (error: ProgressEvent) => reject(error);
- fileReader.readAsArrayBuffer(blob);
- });
-}
-*/
diff --git a/modules/loader-utils/src/index.ts b/modules/loader-utils/src/index.ts
index ac914d5aa8..7828c7527b 100644
--- a/modules/loader-utils/src/index.ts
+++ b/modules/loader-utils/src/index.ts
@@ -118,7 +118,8 @@ export {fs};
import * as stream from './lib/node/stream';
export {stream};
-// EXPERIMENTAL
+// EXPERIMENTAL: FILE SYSTEMS
+
export type {FileSystem, RandomAccessReadFileSystem} from './lib/filesystems/filesystem';
export {NodeFileSystem as _NodeFileSystem} from './lib/filesystems/node-filesystem';
@@ -134,3 +135,22 @@ export {makeReadableFile} from './lib/filesystems/readable-file';
export type {WritableFile} from './lib/filesystems/writable-file';
export {makeWritableFile} from './lib/filesystems/writable-file';
+
+// EXPERIMENTAL: DATA SOURCES
+export type {DataSourceProps} from './lib/sources/data-source';
+export {DataSource} from './lib/sources/data-source';
+
+export type {ImageType} from './lib/sources/image-source';
+export type {ImageSourceProps, ImageSourceMetadata} from './lib/sources/image-source';
+export type {GetImageParameters} from './lib/sources/image-source';
+export {ImageSource} from './lib/sources/image-source';
+
+export type {
+ TileSourceProps,
+ TileSourceMetadata,
+ GetTileParameters
+} from './lib/sources/tile-source';
+export {TileSource} from './lib/sources/tile-source';
+
+export type {ImageTileSourceProps} from './lib/sources/image-tile-source';
+export {ImageTileSource} from './lib/sources/image-tile-source';
diff --git a/modules/wms/src/lib/sources/data-source.ts b/modules/loader-utils/src/lib/sources/data-source.ts
similarity index 97%
rename from modules/wms/src/lib/sources/data-source.ts
rename to modules/loader-utils/src/lib/sources/data-source.ts
index 7283d2e3ea..ddc2b52c94 100644
--- a/modules/wms/src/lib/sources/data-source.ts
+++ b/modules/loader-utils/src/lib/sources/data-source.ts
@@ -2,6 +2,7 @@
import type {LoaderOptions} from '@loaders.gl/loader-utils';
+/** Common properties for all data sources */
export type DataSourceProps = {
/** LoaderOptions provide an option to override `fetch`. Will also be passed to any sub loaders */
loadOptions?: LoaderOptions;
diff --git a/modules/wms/src/lib/sources/image-source.ts b/modules/loader-utils/src/lib/sources/image-source.ts
similarity index 88%
rename from modules/wms/src/lib/sources/image-source.ts
rename to modules/loader-utils/src/lib/sources/image-source.ts
index 92505715db..4f8bf7b7d6 100644
--- a/modules/wms/src/lib/sources/image-source.ts
+++ b/modules/loader-utils/src/lib/sources/image-source.ts
@@ -1,9 +1,19 @@
// loaders.gl, MIT license
-import type {ImageType} from '@loaders.gl/images';
import type {DataSourceProps} from './data-source';
import {DataSource} from './data-source';
+/** data images @note duplicates definition in images/schema to avoid circular deps */
+export type ImageDataType = {
+ data: Uint8Array;
+ width: number;
+ height: number;
+ compressed?: boolean;
+};
+
+/** Supported Image Types @note duplicates definition in images/schema to avoid circular deps */
+export type ImageType = ImageBitmap | ImageDataType | HTMLImageElement;
+
/**
* Normalized capabilities of an Image service
* @example
diff --git a/modules/loader-utils/src/lib/sources/image-tile-source.ts b/modules/loader-utils/src/lib/sources/image-tile-source.ts
new file mode 100644
index 0000000000..5d9f6133c2
--- /dev/null
+++ b/modules/loader-utils/src/lib/sources/image-tile-source.ts
@@ -0,0 +1,22 @@
+// loaders.gl, MIT license
+
+import type {ImageType} from './image-source';
+import {TileSource, TileSourceProps} from './tile-source';
+import {GetTileParameters, TileLoadParameters} from './tile-source';
+
+export type ImageTileSourceProps = TileSourceProps;
+
+/**
+ * MapTileSource - data sources that allow data to be queried by (geospatial) tile
+ * @note If geospatial, bounding box is expected to be in web mercator coordinates
+ */
+export abstract class ImageTileSource<
+ PropsT extends ImageTileSourceProps = ImageTileSourceProps
+> extends TileSource {
+ constructor(props: PropsT) {
+ super(props);
+ this.getTileData = this.getTileData.bind(this);
+ }
+ abstract getTile(parameters: GetTileParameters): Promise;
+ abstract getTileData(props: TileLoadParameters): Promise;
+}
diff --git a/modules/wms/src/lib/sources/tile-source.ts b/modules/loader-utils/src/lib/sources/tile-source.ts
similarity index 56%
rename from modules/wms/src/lib/sources/tile-source.ts
rename to modules/loader-utils/src/lib/sources/tile-source.ts
index e7ec113662..82e21a0c4a 100644
--- a/modules/wms/src/lib/sources/tile-source.ts
+++ b/modules/loader-utils/src/lib/sources/tile-source.ts
@@ -1,12 +1,13 @@
// loaders.gl, MIT license
-import type {ImageType} from '@loaders.gl/images';
+// import type {ImageType} from '@loaders.gl/images';
import {DataSource, DataSourceProps} from './data-source';
/**
* Normalized capabilities of an Image service
+ * Sources are expected to normalize the response to capabilities
* @example
- * The WMSService will normalize the response to the WMS `GetCapabilities` data structure extracted from WMS XML response
+ * A WMS service would normalize the response to the WMS `GetCapabilities` data structure extracted from WMS XML response
* into an TileSourceMetadata.
*/
export type TileSourceMetadata = {
@@ -23,6 +24,9 @@ export type TileSourceMetadata = {
};
};
+/**
+ * Description of one data layer in the image source
+ */
export type TileSourceLayer = {
name: string;
title?: string;
@@ -31,6 +35,9 @@ export type TileSourceLayer = {
layers: TileSourceLayer[];
};
+/**
+ * Generic parameters for requesting an image from an image source
+ */
export type GetTileParameters = {
/** Layers to render */
layers: string | string[];
@@ -48,7 +55,25 @@ export type GetTileParameters = {
format?: 'image/png';
};
-type TileSourceProps = DataSourceProps;
+export type TileLoadParameters = {
+ index: {x: number; y: number; z: number};
+ id: string;
+ bbox: TileBoundingBox;
+ zoom?: number;
+ url?: string | null;
+ signal?: AbortSignal;
+ userData?: Record;
+};
+
+/** deck.gl compatible bounding box */
+export type TileBoundingBox = NonGeoBoundingBox | GeoBoundingBox;
+export type GeoBoundingBox = {west: number; north: number; east: number; south: number};
+export type NonGeoBoundingBox = {left: number; top: number; right: number; bottom: number};
+
+/**
+ * Props for a TileSource
+ */
+export type TileSourceProps = DataSourceProps;
/**
* MapTileSource - data sources that allow data to be queried by (geospatial) extents
@@ -57,5 +82,8 @@ type TileSourceProps = DataSourceProps;
*/
export abstract class TileSource extends DataSource {
abstract getMetadata(): Promise;
- abstract getTile(parameters: GetTileParameters): Promise;
+ /** Flat parameters */
+ abstract getTile(parameters: GetTileParameters): Promise;
+ /** deck.gl compatible method */
+ abstract getTileData(parameters: TileLoadParameters): Promise;
}
diff --git a/modules/wms/src/lib/sources/utils/utils.ts b/modules/loader-utils/src/lib/sources/utils/utils.ts
similarity index 100%
rename from modules/wms/src/lib/sources/utils/utils.ts
rename to modules/loader-utils/src/lib/sources/utils/utils.ts
diff --git a/modules/loader-utils/test/lib/file-provider/file-handle-file.spec.js b/modules/loader-utils/test/lib/file-provider/file-handle-file.spec.js
index d76da23ab5..fe497aea08 100644
--- a/modules/loader-utils/test/lib/file-provider/file-handle-file.spec.js
+++ b/modules/loader-utils/test/lib/file-provider/file-handle-file.spec.js
@@ -9,7 +9,7 @@ const SLPKUrl = 'modules/i3s/test/data/DA12_subset.slpk';
test('FileHandleFile#slice', async (t) => {
if (!isBrowser) {
const provider = await FileHandleFile.from(SLPKUrl);
- t.equals(Buffer.from(await provider.slice(0n, 4n)).compare(signature), 0);
+ t.equals(Buffer.from(await provider.slice(0n, 4n)).compare(Buffer.from(signature)), 0);
}
t.end();
});
diff --git a/modules/pmtiles/README.md b/modules/pmtiles/README.md
new file mode 100644
index 0000000000..b4ce9f83aa
--- /dev/null
+++ b/modules/pmtiles/README.md
@@ -0,0 +1,7 @@
+# @loaders.gl/pmtiles
+
+[loaders.gl](https://loaders.gl/docs) is a collection of framework-independent 3D and geospatial parsers and encoders.
+
+This module contains loaders for the pmtiles format.
+
+For documentation please visit the [website](https://loaders.gl).
diff --git a/modules/pmtiles/package.json b/modules/pmtiles/package.json
new file mode 100644
index 0000000000..92dbf57f81
--- /dev/null
+++ b/modules/pmtiles/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@loaders.gl/pmtiles",
+ "version": "3.4.0-alpha.3",
+ "description": "Framework-independent loader for the pmtiles format",
+ "license": "MIT",
+ "publishConfig": {
+ "access": "public"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/visgl/loaders.gl"
+ },
+ "keywords": [
+ "webgl",
+ "loader",
+ "3d",
+ "mesh",
+ "point cloud",
+ "PCD"
+ ],
+ "types": "dist/index.d.ts",
+ "main": "dist/es5/index.js",
+ "module": "dist/esm/index.js",
+ "sideEffects": false,
+ "files": [
+ "src",
+ "dist",
+ "README.md"
+ ],
+ "scripts": {
+ "pre-build": "npm run build-bundle",
+ "build-bundle": "esbuild src/bundle.ts --bundle --outfile=dist/dist.min.js"
+ },
+ "dependencies": {
+ "@loaders.gl/loader-utils": "3.4.0-alpha.3",
+ "@loaders.gl/schema": "3.4.0-alpha.3",
+ "pmtiles": "^2.7.2"
+ },
+ "gitHead": "c95a4ff72512668a93d9041ce8636bac09333fd5"
+}
diff --git a/modules/pmtiles/src/bundle.ts b/modules/pmtiles/src/bundle.ts
new file mode 100644
index 0000000000..0db0c48b55
--- /dev/null
+++ b/modules/pmtiles/src/bundle.ts
@@ -0,0 +1,4 @@
+// @ts-nocheck
+const moduleExports = require('./index');
+globalThis.loaders = globalThis.loaders || {};
+module.exports = Object.assign(globalThis.loaders, moduleExports);
diff --git a/modules/pmtiles/src/index.ts b/modules/pmtiles/src/index.ts
new file mode 100644
index 0000000000..576669128e
--- /dev/null
+++ b/modules/pmtiles/src/index.ts
@@ -0,0 +1,7 @@
+// loaders.gl, MIT license
+
+export type {PMTilesImageSourceProps} from './pmtiles-source';
+export {PMTilesImageSource} from './pmtiles-source';
+
+// EXPERIMENTAL
+export {PMTilesLoader} from './pmtiles-loader';
diff --git a/modules/pmtiles/src/lib/parse-pmtiles.ts b/modules/pmtiles/src/lib/parse-pmtiles.ts
new file mode 100644
index 0000000000..f15b662462
--- /dev/null
+++ b/modules/pmtiles/src/lib/parse-pmtiles.ts
@@ -0,0 +1,87 @@
+import {Source, PMTiles, Header, TileType} from 'pmtiles';
+// import {Source, RangeResponse, PMTiles, Header, Compression, TileType} from 'pmtiles';
+
+// export enum Compression {
+// Unknown = 0,
+// None = 1,
+// Gzip = 2,
+// Brotli = 3,
+// Zstd = 4,
+// }
+
+// export enum TileType {
+// Unknown = 0,
+// Mvt = 1,
+// Png = 2,
+// Jpeg = 3,
+// Webp = 4,
+// }
+
+export interface PMTilesMetadata {
+ format: 'pmtiles';
+ /** PMTiles format specific header */
+ formatHeader?: Header;
+ version: number;
+ tileType: TileType;
+ minZoom: number;
+ maxZoom: number;
+ minLon: number;
+ minLat: number;
+ maxLon: number;
+ maxLat: number;
+ centerZoom: number;
+ centerLon: number;
+ centerLat: number;
+ etag?: string;
+ // Addition
+ metadata: unknown;
+}
+
+export type ParsePMTilesOptions = {
+ tileZxy?: [number, number, number];
+};
+
+export async function loadPMTilesHeader(source: Source): Promise {
+ const pmTiles = new PMTiles(source);
+ const header = await pmTiles.getHeader();
+ const metadata = await pmTiles.getMetadata();
+ return parsePMTilesMetadata(header, metadata);
+}
+
+export async function loadPMTile(
+ source: Source,
+ options: ParsePMTilesOptions
+): Promise {
+ const pmTiles = new PMTiles(source);
+ if (!options.tileZxy) {
+ throw new Error('tile zxy missing');
+ }
+ const [z, x, y] = options.tileZxy;
+ const tile = await pmTiles.getZxy(z, x, y);
+ return tile?.data;
+}
+
+export async function parsePMTilesMetadata(
+ header: Header,
+ metadata: unknown
+): Promise {
+ return {
+ // The assumption is that this is a tileJSON style metadata generated by e.g. tippecanone
+ metadata,
+ format: 'pmtiles',
+ version: header.specVersion,
+ tileType: header.tileType,
+ minZoom: header.minZoom,
+ maxZoom: header.maxZoom,
+ minLon: header.minLon,
+ minLat: header.minLat,
+ maxLon: header.maxLon,
+ maxLat: header.maxLat,
+ centerZoom: header.centerZoom,
+ centerLon: header.centerLon,
+ centerLat: header.centerLat,
+ etag: header.etag,
+
+ formatHeader: header
+ };
+}
diff --git a/modules/pmtiles/src/lib/sources.ts b/modules/pmtiles/src/lib/sources.ts
new file mode 100644
index 0000000000..ab45a1f30d
--- /dev/null
+++ b/modules/pmtiles/src/lib/sources.ts
@@ -0,0 +1,150 @@
+/*
+import {fetchFile} from '@loaders.gl/core';
+
+import {Source as PMTilesSource, RangeResponse} from 'pmtiles';
+
+/** @note "source" here is a PMTiles library type, referring to *
+export function makeSource(data: string | Blob, fetch?) {
+ if (typeof data === 'string') {
+ return new FetchSource(data, fetch);
+ }
+ if (data instanceof Blob) {
+ const url = '';
+ return new BlobSource(data, url);
+ }
+}
+
+export class BlobSource implements PMTilesSource {
+ blob: Blob;
+ key: string;
+
+ constructor(blob: Blob, key: string) {
+ this.blob = blob;
+ this.key = key;
+ }
+
+ // TODO - how is this used?
+ getKey() {
+ return this.blob.url || '';
+ }
+
+ async getBytes(offset: number, length: number, signal?: AbortSignal): Promise {
+ const data = await this.blob.arrayBuffer();
+ return {
+ data
+ // etag: response.headers.get('ETag') || undefined,
+ // cacheControl: response.headers.get('Cache-Control') || undefined,
+ // expires: response.headers.get('Expires') || undefined
+ };
+ }
+}
+
+export class FetchSource implements PMTilesSource {
+ url: string;
+ fetch;
+
+ constructor(url: string, fetch = fetchFile) {
+ this.url = url;
+ this.fetch = fetch;
+ }
+
+ // TODO - how is this used?
+ getKey() {
+ return this.url;
+ }
+
+ async getBytes(offset: number, length: number, signal?: AbortSignal): Promise {
+ let controller;
+ if (!signal) {
+ // ToDO why is it so important to abort in case 200?
+ // TODO check this works or assert 206
+ controller = new AbortController();
+ signal = controller.signal;
+ }
+
+ let response = await fetch(this.url, {
+ signal,
+ headers: {Range: `bytes=${offset}-${offset + length - 1}`}
+ });
+
+ switch (response.status) {
+ case 206: // Partial Content success
+ // This is the expected success code for a range request
+ break;
+
+ case 200:
+ // some well-behaved backends, e.g. DigitalOcean CDN, respond with 200 instead of 206
+ // but we also need to detect no support for Byte Serving which is returning the whole file
+ const content_length = response.headers.get('Content-Length');
+ if (!content_length || Number(content_length) > length) {
+ if (controller) {
+ controller.abort();
+ }
+ throw Error(
+ 'content-length header missing or exceeding request. Server must support HTTP Byte Serving.'
+ );
+ }
+
+ case 416: // "Range Not Satisfiable"
+ // some HTTP servers don't accept ranges beyond the end of the resource.
+ // Retry with the exact length
+ // TODO: can return 416 with offset > 0 if content changed, which will have a blank etag.
+ // See https://github.com/protomaps/PMTiles/issues/90
+ if (offset === 0) {
+ const content_range = response.headers.get('Content-Range');
+ if (!content_range || !content_range.startsWith('bytes *')) {
+ throw Error('Missing content-length on 416 response');
+ }
+ const actual_length = Number(content_range.substr(8));
+ response = await fetch(this.url, {
+ signal,
+ headers: {Range: `bytes=0-${actual_length - 1}`}
+ });
+ }
+ break;
+
+ default:
+ if (response.status >= 300) {
+ throw Error(`Bad response code: ${response.status}`);
+ }
+ }
+
+ const data = await response.arrayBuffer();
+ return {
+ data,
+ etag: response.headers.get('ETag') || undefined,
+ cacheControl: response.headers.get('Cache-Control') || undefined,
+ expires: response.headers.get('Expires') || undefined
+ };
+ }
+}
+
+/*
+class TestNodeFileSource implements Source {
+ buffer: ArrayBuffer;
+ path: string;
+ key: string;
+ etag?: string;
+
+ constructor(path: string, key: string) {
+ this.path = path;
+ this.buffer = fs.readFileSync(path);
+ this.key = key;
+ }
+
+ getKey() {
+ return this.key;
+ }
+
+ replaceData(path: string) {
+ this.path = path;
+ this.buffer = fs.readFileSync(path);
+ }
+
+ async getBytes(offset: number, length: number): Promise {
+ const slice = new Uint8Array(this.buffer.slice(offset, offset + length))
+ .buffer;
+ return { data: slice, etag: this.etag };
+ }
+}
+*/
diff --git a/modules/pmtiles/src/pmtiles-loader.ts b/modules/pmtiles/src/pmtiles-loader.ts
new file mode 100644
index 0000000000..c46dc2991f
--- /dev/null
+++ b/modules/pmtiles/src/pmtiles-loader.ts
@@ -0,0 +1,23 @@
+import type {Loader} from '@loaders.gl/loader-utils';
+
+// __VERSION__ is injected by babel-plugin-version-inline
+// @ts-ignore TS2304: Cannot find name '__VERSION__'.
+const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
+
+/**
+ * Worker loader for PCD - Point Cloud Data
+ */
+export const PMTilesLoader = {
+ name: 'PMTiles',
+ id: 'pmtiles',
+ module: 'pmtiles',
+ version: VERSION,
+ worker: true,
+ extensions: ['pmtilse'],
+ mimeTypes: ['application/octet-stream'],
+ options: {
+ pmtiles: {}
+ }
+};
+
+export const _typecheckPCDLoader: Loader = PMTilesLoader;
diff --git a/modules/pmtiles/src/pmtiles-source.ts b/modules/pmtiles/src/pmtiles-source.ts
new file mode 100644
index 0000000000..64add62d34
--- /dev/null
+++ b/modules/pmtiles/src/pmtiles-source.ts
@@ -0,0 +1,42 @@
+import {GetTileParameters, ImageType, ImageTileSourceProps} from '@loaders.gl/loader-utils';
+import {ImageTileSource} from '@loaders.gl/loader-utils';
+import {ImageLoader} from '@loaders.gl/images';
+
+import {PMTiles} from 'pmtiles';
+// import {PMTiles, DecompressFunc} from 'pmtiles';
+import {parsePMTilesMetadata} from './lib/parse-pmtiles';
+import {TileLoadParameters} from 'modules/loader-utils/src/lib/sources/tile-source';
+
+export type PMTilesImageSourceProps = ImageTileSourceProps & {
+ url: string;
+};
+
+export class PMTilesImageSource extends ImageTileSource {
+ pmtiles: PMTiles;
+
+ constructor(props: PMTilesImageSourceProps) {
+ super(props);
+ this.pmtiles = new PMTiles(props.url);
+ }
+
+ async getMetadata(): Promise {
+ const header = await this.pmtiles.getHeader();
+ const metadata = await this.pmtiles.getMetadata();
+ return parsePMTilesMetadata(header, metadata);
+ }
+
+ async getTile(tileParams: GetTileParameters): Promise {
+ const {x, y, zoom: z} = tileParams;
+ const rangeResponse = await this.pmtiles.getZxy(z, x, y);
+ const arrayBuffer = rangeResponse?.data;
+ if (!arrayBuffer) {
+ return null;
+ }
+ return ImageLoader.parse(arrayBuffer, this.loadOptions);
+ }
+
+ async getTileData(tileParams: TileLoadParameters): Promise {
+ const {x, y, z} = tileParams.index;
+ return await this.getTile({x, y, zoom: z, layers: []});
+ }
+}
diff --git a/modules/pmtiles/test/data/README.md b/modules/pmtiles/test/data/README.md
new file mode 100644
index 0000000000..316f59bc65
--- /dev/null
+++ b/modules/pmtiles/test/data/README.md
@@ -0,0 +1,5 @@
+# Attributions for Sample Files
+
+## pmtiles
+
+https://github.com/protomaps/PMTiles/tree/main/js/test/data under BSD-3 license
\ No newline at end of file
diff --git a/modules/pmtiles/test/data/empty.pmtiles b/modules/pmtiles/test/data/empty.pmtiles
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/modules/pmtiles/test/data/invalid.pmtiles b/modules/pmtiles/test/data/invalid.pmtiles
new file mode 100644
index 0000000000..f2b720ba30
--- /dev/null
+++ b/modules/pmtiles/test/data/invalid.pmtiles
@@ -0,0 +1 @@
+This is an invalid tile archive, a test case to make sure that the code throws an error, but it needs to be the minimum size to pass the first test
diff --git a/modules/pmtiles/test/data/invalid_v4.pmtiles b/modules/pmtiles/test/data/invalid_v4.pmtiles
new file mode 100644
index 0000000000..1871cb2759
Binary files /dev/null and b/modules/pmtiles/test/data/invalid_v4.pmtiles differ
diff --git a/modules/pmtiles/test/data/test_fixture_1.pmtiles b/modules/pmtiles/test/data/test_fixture_1.pmtiles
new file mode 100644
index 0000000000..c86db1f27b
Binary files /dev/null and b/modules/pmtiles/test/data/test_fixture_1.pmtiles differ
diff --git a/modules/pmtiles/test/data/test_fixture_2.pmtiles b/modules/pmtiles/test/data/test_fixture_2.pmtiles
new file mode 100644
index 0000000000..cb19dd5f11
Binary files /dev/null and b/modules/pmtiles/test/data/test_fixture_2.pmtiles differ
diff --git a/modules/pmtiles/test/data/v3/stamen_toner(raster)CC-BY+ODbL_z3.pmtiles b/modules/pmtiles/test/data/v3/stamen_toner(raster)CC-BY+ODbL_z3.pmtiles
new file mode 100644
index 0000000000..8d170ca89e
Binary files /dev/null and b/modules/pmtiles/test/data/v3/stamen_toner(raster)CC-BY+ODbL_z3.pmtiles differ
diff --git a/modules/pmtiles/test/index.ts b/modules/pmtiles/test/index.ts
new file mode 100644
index 0000000000..527e907dd8
--- /dev/null
+++ b/modules/pmtiles/test/index.ts
@@ -0,0 +1,2 @@
+import './pmtiles-loader.spec';
+import './pmtiles-source.spec';
diff --git a/modules/pmtiles/test/pmtiles-loader.spec.ts b/modules/pmtiles/test/pmtiles-loader.spec.ts
new file mode 100644
index 0000000000..1ee4b790f0
--- /dev/null
+++ b/modules/pmtiles/test/pmtiles-loader.spec.ts
@@ -0,0 +1,186 @@
+/* eslint-disable max-len */
+import test from 'tape-promise/tape';
+import {validateLoader} from 'test/common/conformance';
+
+import {PMTilesLoader} from '@loaders.gl/pmtiles';
+// import {PMTiles, SharedPromiseCache} from 'pmtiles';
+
+test('PMTilesLoader#loader conformance', (t) => {
+ validateLoader(t, PMTilesLoader, 'PMTilesLoader');
+ t.end();
+});
+
+/*
+// echo '{"type":"Polygon","coordinates":[[[0,0],[0,1],[1,1],[1,0],[0,0]]]}' | ./tippecanoe -zg -o test_fixture_1.pmtiles
+test('cache getHeader', async (t) => {
+ const source = new TestFileSource('@loaders.gl/pmtiles/test/data/test_fixture_1.pmtiles', '1');
+ const cache = new SharedPromiseCache();
+ const header = await cache.getHeader(source);
+ t.strictEqual(header.rootDirectoryOffset, 127);
+ t.strictEqual(header.rootDirectoryLength, 25);
+ t.strictEqual(header.jsonMetadataOffset, 152);
+ t.strictEqual(header.jsonMetadataLength, 247);
+ t.strictEqual(header.leafDirectoryOffset, 0);
+ t.strictEqual(header.leafDirectoryLength, 0);
+ t.strictEqual(header.tileDataOffset, 399);
+ t.strictEqual(header.tileDataLength, 69);
+ t.strictEqual(header.numAddressedTiles, 1);
+ t.strictEqual(header.numTileEntries, 1);
+ t.strictEqual(header.numTileContents, 1);
+ t.strictEqual(header.clustered, false);
+ t.strictEqual(header.internalCompression, 2);
+ t.strictEqual(header.tileCompression, 2);
+ t.strictEqual(header.tileType, 1);
+ t.strictEqual(header.minZoom, 0);
+ t.strictEqual(header.maxZoom, 0);
+ t.strictEqual(header.minLon, 0);
+ t.strictEqual(header.minLat, 0);
+ // t.strictEqual(header.maxLon,1); // TODO fix me
+ t.strictEqual(header.maxLat, 1);
+});
+
+test('cache check against empty', async (t) => {
+ const source = new TestFileSource('@loaders.gl/pmtiles/test/data/empty.pmtiles', '1');
+ const cache = new SharedPromiseCache();
+ t.rejects(async () => {
+ await cache.getHeader(source);
+ });
+});
+
+test('cache check magic number', async (t) => {
+ const source = new TestFileSource('@loaders.gl/pmtiles/test/data/invalid.pmtiles', '1');
+ const cache = new SharedPromiseCache();
+ t.rejects(async () => {
+ await cache.getHeader(source);
+ });
+});
+
+test('cache check future spec version', async (t) => {
+ const source = new TestFileSource('@loaders.gl/pmtiles/test/data/invalid_v4.pmtiles', '1');
+ const cache = new SharedPromiseCache();
+ t.rejects(async () => {
+ await cache.getHeader(source);
+ });
+});
+
+test('cache getDirectory', async (t) => {
+ const source = new TestFileSource('@loaders.gl/pmtiles/test/data/test_fixture_1.pmtiles', '1');
+
+ let cache = new SharedPromiseCache(6400, false);
+ let header = await cache.getHeader(source);
+ t.strictEqual(cache.cache.size, 1);
+
+ cache = new SharedPromiseCache(6400, true);
+ header = await cache.getHeader(source);
+
+ // prepopulates the root directory
+ t.strictEqual(cache.cache.size, 2);
+
+ const directory = await cache.getDirectory(
+ source,
+ header.rootDirectoryOffset,
+ header.rootDirectoryLength,
+ header
+ );
+ t.strictEqual(directory.length, 1);
+ t.strictEqual(directory[0].tileId, 0);
+ t.strictEqual(directory[0].offset, 0);
+ t.strictEqual(directory[0].length, 69);
+ t.strictEqual(directory[0].runLength, 1);
+
+ for (const v of cache.cache.values()) {
+ t.ok(v.lastUsed > 0);
+ }
+});
+
+test('multiple sources in a single cache', async (t) => {
+ const cache = new SharedPromiseCache();
+ const source1 = new TestFileSource('@loaders.gl/pmtiles/test/data/test_fixture_1.pmtiles', '1');
+ const source2 = new TestFileSource('@loaders.gl/pmtiles/test/data/test_fixture_1.pmtiles', '2');
+ await cache.getHeader(source1);
+ t.strictEqual(cache.cache.size, 2);
+ await cache.getHeader(source2);
+ t.strictEqual(cache.cache.size, 4);
+});
+
+test('etags are part of key', async (t) => {
+ const cache = new SharedPromiseCache(6400, false);
+ const source = new TestFileSource('@loaders.gl/pmtiles/test/data/test_fixture_1.pmtiles', '1');
+ source.etag = 'etag_1';
+ let header = await cache.getHeader(source);
+ t.strictEqual(header.etag, 'etag_1');
+
+ source.etag = 'etag_2';
+
+ t.rejects(async () => {
+ await cache.getDirectory(
+ source,
+ header.rootDirectoryOffset,
+ header.rootDirectoryLength,
+ header
+ );
+ });
+
+ cache.invalidate(source, 'etag_2');
+ header = await cache.getHeader(source);
+ t.ok(
+ await cache.getDirectory(source, header.rootDirectoryOffset, header.rootDirectoryLength, header)
+ );
+});
+
+test.skip('soft failure on etag weirdness', async (t) => {
+ const cache = new SharedPromiseCache(6400, false);
+ const source = new TestFileSource('@loaders.gl/pmtiles/test/data/test_fixture_1.pmtiles', '1');
+ source.etag = 'etag_1';
+ let header = await cache.getHeader(source);
+ t.strictEqual(header.etag, 'etag_1');
+
+ source.etag = 'etag_2';
+
+ t.rejects(async () => {
+ await cache.getDirectory(
+ source,
+ header.rootDirectoryOffset,
+ header.rootDirectoryLength,
+ header
+ );
+ });
+
+ source.etag = 'etag_1';
+ cache.invalidate(source, 'etag_2');
+
+ header = await cache.getHeader(source);
+ t.strictEqual(header.etag, undefined);
+});
+
+test('cache pruning by byte size', async (t) => {
+ const cache = new SharedPromiseCache(2, false);
+ cache.cache.set('0', {lastUsed: 0, data: Promise.resolve([])});
+ cache.cache.set('1', {lastUsed: 1, data: Promise.resolve([])});
+ cache.cache.set('2', {lastUsed: 2, data: Promise.resolve([])});
+ cache.prune();
+ t.strictEqual(cache.cache.size, 2);
+ t.ok(cache.cache.get('2'));
+ t.ok(cache.cache.get('1'));
+ t.ok(!cache.cache.get('0'));
+});
+
+test('pmtiles get metadata', async (t) => {
+ const source = new TestFileSource('@loaders.gl/pmtiles/test/data/test_fixture_1.pmtiles', '1');
+ const p = new PMTiles(source);
+ const metadata = await p.getMetadata();
+ t.ok(metadata.name);
+});
+
+// echo '{"type":"Polygon","coordinates":[[[0,0],[0,1],[1,0],[0,0]]]}' | ./tippecanoe -zg -o test_fixture_2.pmtiles
+test('pmtiles handle retries', async (t) => {
+ const source = new TestFileSource('@loaders.gl/pmtiles/test/data/test_fixture_1.pmtiles', '1');
+ source.etag = '1';
+ const p = new PMTiles(source);
+ const metadata = await p.getMetadata();
+ t.ok(metadata.name);
+ source.etag = '2';
+ source.replaceData('@loaders.gl/pmtiles/test/data/test_fixture_2.pmtiles');
+ t.ok(await p.getZxy(0, 0, 0));
+});
+*/
diff --git a/modules/pmtiles/test/pmtiles-source.spec.ts b/modules/pmtiles/test/pmtiles-source.spec.ts
new file mode 100644
index 0000000000..563bc314b5
--- /dev/null
+++ b/modules/pmtiles/test/pmtiles-source.spec.ts
@@ -0,0 +1,12 @@
+/* eslint-disable max-len */
+import test from 'tape-promise/tape';
+import {PMTilesImageSource} from '@loaders.gl/pmtiles';
+
+test('cache getHeader', async (t) => {
+ const source = new PMTilesImageSource({
+ url: '@loaders.gl/pmtiles/test/data/test_fixture_1.pmtiles'
+ }); // , '1');
+ t.ok(source);
+});
+
+// TBA
diff --git a/modules/pmtiles/tsconfig.json b/modules/pmtiles/tsconfig.json
new file mode 100644
index 0000000000..4e11b7c942
--- /dev/null
+++ b/modules/pmtiles/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../tsconfig.module.json",
+ "include": ["src/**/*"],
+ "exclude": ["node_modules"],
+ "compilerOptions": {
+ "composite": true,
+ "rootDir": "src",
+ "outDir": "dist"
+ },
+ "references": [
+ {"path": "../loader-utils"},
+ {"path": "../schema"}
+ ]
+}
diff --git a/modules/schema/src/types/category-image.ts b/modules/schema/src/types/category-image.ts
index ee327abc25..8f8acab8f7 100644
--- a/modules/schema/src/types/category-image.ts
+++ b/modules/schema/src/types/category-image.ts
@@ -11,7 +11,7 @@ export type ImageDataType = {
/**
* Supported Image Types
*/
-export type ImageType = ImageBitmap | typeof Image | ImageDataType;
+export type ImageType = ImageBitmap | ImageDataType | HTMLImageElement;
/**
* Image type string used to control or determine the type of images returned from ImageLoader
diff --git a/modules/wms/src/index.ts b/modules/wms/src/index.ts
index 0bfa30359d..ccfeaffab8 100644
--- a/modules/wms/src/index.ts
+++ b/modules/wms/src/index.ts
@@ -54,8 +54,6 @@ export {GMLLoader as _GMLLoader} from './gml-loader';
// EXPERIMENTAL: DATA SOURCES
export type {ImageType} from '@loaders.gl/images';
-export type {ImageSourceProps, ImageSourceMetadata} from './lib/sources/image-source';
-export {ImageSource} from './lib/sources/image-source';
export type {ImageServiceType} from './lib/create-image-source';
export {createImageSource} from './lib/create-image-source';
@@ -82,3 +80,6 @@ export {ArcGISImageServer as _ArcGISImageServer} from './lib/services/arcgis/arc
/** @deprecated Use WMSCapabilitiesLoaderOptions */
export type {WMSCapabilitiesLoaderOptions as WMSLoaderOptions} from './wms-capabilities-loader';
+
+// TODO - restore once deck.gl has been udpated
+export {ImageSource} from '@loaders.gl/loader-utils';
diff --git a/modules/wms/src/lib/create-image-source.ts b/modules/wms/src/lib/create-image-source.ts
index b8bd402c72..12fd2abc15 100644
--- a/modules/wms/src/lib/create-image-source.ts
+++ b/modules/wms/src/lib/create-image-source.ts
@@ -1,6 +1,6 @@
// loaders.gl, MIT license
-import {ImageSource} from './sources/image-source';
+import {ImageSource} from '@loaders.gl/loader-utils';
import {ImageService, ImageServiceProps} from './services/generic/image-service';
import type {WMSServiceProps} from './services/ogc/wms-service';
import {WMSService} from './services/ogc/wms-service';
diff --git a/modules/wms/src/lib/services/arcgis/arcgis-image-service.ts b/modules/wms/src/lib/services/arcgis/arcgis-image-service.ts
index ac63f9109c..7a4d090f19 100644
--- a/modules/wms/src/lib/services/arcgis/arcgis-image-service.ts
+++ b/modules/wms/src/lib/services/arcgis/arcgis-image-service.ts
@@ -1,9 +1,9 @@
// loaders.gl, MIT license
import {ImageType} from '@loaders.gl/images';
-import type {ImageSourceMetadata, GetImageParameters} from '../../sources/image-source';
-import type {ImageSourceProps} from '../../sources/image-source';
-import {ImageSource} from '../../sources/image-source';
+import type {ImageSourceMetadata, GetImageParameters} from '@loaders.gl/loader-utils';
+import type {ImageSourceProps} from '@loaders.gl/loader-utils';
+import {ImageSource} from '@loaders.gl/loader-utils';
export type ArcGISImageServerProps = ImageSourceProps & {
url: string;
diff --git a/modules/wms/src/lib/services/generic/image-service.ts b/modules/wms/src/lib/services/generic/image-service.ts
index 9d26be6b08..ac9d023e3d 100644
--- a/modules/wms/src/lib/services/generic/image-service.ts
+++ b/modules/wms/src/lib/services/generic/image-service.ts
@@ -4,8 +4,8 @@ import {LoaderOptions} from '@loaders.gl/loader-utils';
import type {ImageType} from '@loaders.gl/images';
import {ImageLoader} from '@loaders.gl/images';
-import type {ImageSourceMetadata, GetImageParameters} from '../../sources/image-source';
-import {ImageSource} from '../../sources/image-source';
+import type {ImageSourceMetadata, GetImageParameters} from '@loaders.gl/loader-utils';
+import {ImageSource} from '@loaders.gl/loader-utils';
/** Template URL string should contain `${width}` etc which will be substituted. */
export type ImageServiceProps = {
diff --git a/modules/wms/src/lib/services/ogc/csw-service.ts b/modules/wms/src/lib/services/ogc/csw-service.ts
index d4c98bba83..7826b71a1a 100644
--- a/modules/wms/src/lib/services/ogc/csw-service.ts
+++ b/modules/wms/src/lib/services/ogc/csw-service.ts
@@ -2,8 +2,8 @@
/* eslint-disable camelcase */
-import type {DataSourceProps} from '../../sources/data-source';
-import {DataSource} from '../../sources/data-source';
+import type {DataSourceProps} from '@loaders.gl/loader-utils';
+import {DataSource} from '@loaders.gl/loader-utils';
import type {CSWCapabilities} from '../../../csw-capabilities-loader';
import {CSWCapabilitiesLoader} from '../../../csw-capabilities-loader';
diff --git a/modules/wms/src/lib/services/ogc/wms-service.ts b/modules/wms/src/lib/services/ogc/wms-service.ts
index ed3e5de3bd..e50ac59b56 100644
--- a/modules/wms/src/lib/services/ogc/wms-service.ts
+++ b/modules/wms/src/lib/services/ogc/wms-service.ts
@@ -5,9 +5,9 @@ import type {ImageType} from '@loaders.gl/images';
import {ImageLoader} from '@loaders.gl/images';
import {mergeLoaderOptions} from '@loaders.gl/loader-utils';
-import type {ImageSourceMetadata, GetImageParameters} from '../../sources/image-source';
-import type {ImageSourceProps} from '../../sources/image-source';
-import {ImageSource} from '../../sources/image-source';
+import type {ImageSourceMetadata, GetImageParameters} from '@loaders.gl/loader-utils';
+import type {ImageSourceProps} from '@loaders.gl/loader-utils';
+import {ImageSource} from '@loaders.gl/loader-utils';
import type {WMSCapabilities} from '../../../wms-capabilities-loader';
import type {WMSFeatureInfo} from '../../../wip/wms-feature-info-loader';
diff --git a/modules/zip/src/parse-zip/end-of-central-directory.ts b/modules/zip/src/parse-zip/end-of-central-directory.ts
index 808bcfec77..ff40a359dd 100644
--- a/modules/zip/src/parse-zip/end-of-central-directory.ts
+++ b/modules/zip/src/parse-zip/end-of-central-directory.ts
@@ -13,8 +13,8 @@ export type ZipEoCDRecord = {
};
const eoCDSignature: ZipSignature = [0x50, 0x4b, 0x05, 0x06];
-const zip64EoCDLocatorSignature = Buffer.from([0x50, 0x4b, 0x06, 0x07]);
-const zip64EoCDSignature = Buffer.from([0x50, 0x4b, 0x06, 0x06]);
+const zip64EoCDLocatorSignature = [0x50, 0x4b, 0x06, 0x07];
+const zip64EoCDSignature = [0x50, 0x4b, 0x06, 0x06];
// offsets accroding to https://en.wikipedia.org/wiki/ZIP_(file_format)
const CD_RECORDS_NUMBER_OFFSET = 8n;
@@ -42,7 +42,7 @@ export const parseEoCDRecord = async (fileProvider: FileProvider): Promise => {
- if (Buffer.from(await buffer.slice(headerOffset, headerOffset + 4n)).compare(signature) !== 0) {
+ if (
+ Buffer.from(await buffer.slice(headerOffset, headerOffset + 4n)).compare(
+ Buffer.from(signature)
+ ) !== 0
+ ) {
return null;
}
diff --git a/test/aliases.js b/test/aliases.js
index e0eaba7f38..3c8fb05b55 100644
--- a/test/aliases.js
+++ b/test/aliases.js
@@ -32,6 +32,7 @@ function makeAliases(basename = __dirname) {
'@loaders.gl/parquet/test': resolve(basename, '../modules/parquet/test'),
'@loaders.gl/pcd/test': resolve(basename, '../modules/pcd/test'),
'@loaders.gl/ply/test': resolve(basename, '../modules/ply/test'),
+ '@loaders.gl/pmtiles/test': resolve(basename, '../modules/pmtiles/test'),
'@loaders.gl/polyfills/test': resolve(basename, '../modules/polyfills/test'),
'@loaders.gl/potree/test': resolve(basename, '../modules/potree/test'),
'@loaders.gl/shapefile/test': resolve(basename, '../modules/shapefile/test'),
diff --git a/test/modules.js b/test/modules.js
index 63eaec83a3..2e45cc8b1f 100644
--- a/test/modules.js
+++ b/test/modules.js
@@ -50,11 +50,15 @@ import '@loaders.gl/tiles/test';
import '@loaders.gl/geopackage/test';
import '@loaders.gl/gis/test';
import '@loaders.gl/kml/test';
-import '@loaders.gl/mvt/test';
import '@loaders.gl/shapefile/test';
import '@loaders.gl/wkt/test';
import '@loaders.gl/wms/test';
+import '@loaders.gl/mvt/test';
+
+// Range request archive style formats
+import '@loaders.gl/pmtiles/test';
+
// Table Formats
import '@loaders.gl/schema/test';
import '@loaders.gl/arrow/test';
diff --git a/tsconfig.json b/tsconfig.json
index 0f9883540b..8ab303f6f2 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -78,6 +78,8 @@
"@loaders.gl/parquet/test": ["modules/parquet/test"],
"@loaders.gl/pcd": ["modules/pcd/src"],
"@loaders.gl/pcd/test": ["modules/pcd/test"],
+ "@loaders.gl/pmtiles": ["modules/pmtiles/src"],
+ "@loaders.gl/pmtiles/test": ["modules/pmtiles/test"],
"@loaders.gl/ply": ["modules/ply/src"],
"@loaders.gl/ply/test": ["modules/ply/test"],
"@loaders.gl/potree": ["modules/potree/src"],
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index 951dd514ee..e37458f0b4 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -78,6 +78,7 @@ const config = {
'@loaders.gl/parquet': resolve('../modules/parquet/src'),
'@loaders.gl/pcd': resolve('../modules/pcd/src'),
'@loaders.gl/ply': resolve('../modules/ply/src'),
+ '@loaders.gl/pmtiles': resolve('../modules/pmtiles/src'),
'@loaders.gl/polyfills': resolve('../modules/polyfills/src'),
'@loaders.gl/potree': resolve('../modules/potree/src'),
'@loaders.gl/schema': resolve('../modules/schema/src'),
diff --git a/website/src/examples-sidebar.js b/website/src/examples-sidebar.js
index 2b744815f6..15bd50d16b 100644
--- a/website/src/examples-sidebar.js
+++ b/website/src/examples-sidebar.js
@@ -20,6 +20,7 @@
label: 'Geospatial Loaders',
items: [
'geospatial',
+ 'pmtiles',
'wms'
],
},
diff --git a/website/src/examples/pmtiles.mdx b/website/src/examples/pmtiles.mdx
new file mode 100644
index 0000000000..7ef312e666
--- /dev/null
+++ b/website/src/examples/pmtiles.mdx
@@ -0,0 +1,7 @@
+# PMTiles
+
+import Demo from 'examples/website/tiles/app';
+
+
+
+
diff --git a/website/static/images/examples/pmtiles.jpg b/website/static/images/examples/pmtiles.jpg
new file mode 100644
index 0000000000..516accdf6f
Binary files /dev/null and b/website/static/images/examples/pmtiles.jpg differ
diff --git a/yarn.lock b/yarn.lock
index c3422c95fa..75cd70c9da 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1961,6 +1961,15 @@
dependencies:
"@loaders.gl/loader-utils" "3.4.14"
+"@loaders.gl/loader-utils@3.4.0-alpha.3":
+ version "3.4.0-alpha.3"
+ resolved "https://registry.yarnpkg.com/@loaders.gl/loader-utils/-/loader-utils-3.4.0-alpha.3.tgz#a4a6e5acc1075988b3b88fb6a889f8cd71c39fc4"
+ integrity sha512-/7si5ZbeuB2DX7kuZ2Egir/0OAL4qbIqYOs90vj+jarEbEflsedete/TPrjsdVwKLFT7RfDQPohTqMVEy9hdEg==
+ dependencies:
+ "@babel/runtime" "^7.3.1"
+ "@loaders.gl/worker-utils" "3.4.0-alpha.3"
+ "@probe.gl/stats" "^4.0.1"
+
"@loaders.gl/loader-utils@3.4.14":
version "3.4.14"
resolved "https://registry.yarnpkg.com/@loaders.gl/loader-utils/-/loader-utils-3.4.14.tgz#d94decc279fd2304b8762c87d8d9626058d91f21"
@@ -1970,6 +1979,20 @@
"@loaders.gl/worker-utils" "3.4.14"
"@probe.gl/stats" "^4.0.1"
+"@loaders.gl/schema@3.4.0-alpha.3":
+ version "3.4.0-alpha.3"
+ resolved "https://registry.yarnpkg.com/@loaders.gl/schema/-/schema-3.4.0-alpha.3.tgz#c97fa3c553e0fbddb6ff6497cc59e78f9781181d"
+ integrity sha512-syMpg7GHGS1o82PH884AhV9RualUfXs7Hul0B3y/T3jBv2Z60nzxpsRlQcqJ9r1vJE0q8ZimQGpAsccQcSdMRA==
+ dependencies:
+ "@types/geojson" "^7946.0.7"
+
+"@loaders.gl/worker-utils@3.4.0-alpha.3":
+ version "3.4.0-alpha.3"
+ resolved "https://registry.yarnpkg.com/@loaders.gl/worker-utils/-/worker-utils-3.4.0-alpha.3.tgz#15deba3310b1bbe2933b2816b08c26e5fe211888"
+ integrity sha512-Ifxvm6g8v5KdJk5mwN5xG2lYNRozB9svgRcnJQyoyf3NJd1K+Dq4SRatAHxzGzbmpTa6dm8TLgK8cYF/78++BA==
+ dependencies:
+ "@babel/runtime" "^7.3.1"
+
"@loaders.gl/worker-utils@3.4.14":
version "3.4.14"
resolved "https://registry.yarnpkg.com/@loaders.gl/worker-utils/-/worker-utils-3.4.14.tgz#5391a416a3d60e03b9edcedb285af44312d40d2e"
@@ -6264,6 +6287,11 @@ fflate@0.7.4:
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.7.4.tgz#61587e5d958fdabb5a9368a302c25363f4f69f50"
integrity sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==
+fflate@^0.8.0:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.1.tgz#1ed92270674d2ad3c73f077cd0acf26486dae6c9"
+ integrity sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==
+
figgy-pudding@^3.4.1, figgy-pudding@^3.5.1:
version "3.5.2"
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
@@ -10185,6 +10213,13 @@ pkg-dir@^4.1.0:
dependencies:
find-up "^4.0.0"
+pmtiles@^2.7.2:
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/pmtiles/-/pmtiles-2.10.0.tgz#894e2954723924add7dd9637c89c0329ffe62501"
+ integrity sha512-X+s6JyperpcAkKwv55MKx72ckOUB0ZjcfK4929iM0SS0MkLydEi2FSW1E8YTE1E2XaZ2TVk/MIUrbsZuXV7K2g==
+ dependencies:
+ fflate "^0.8.0"
+
pngjs-nozlib@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/pngjs-nozlib/-/pngjs-nozlib-1.0.0.tgz#9e64d602cfe9cce4d9d5997d0687429a73f0b7d7"