├── .babelrc
├── .editorconfig
├── .eslintrc.js
├── .gitattributes
├── .gitignore
├── .npmrc
├── README.md
├── example
├── .gitignore
├── index.html
├── package.json
├── public
│ ├── output.pmtiles
│ ├── percapita_income.csv
│ └── pmt-worker.js
├── src
│ ├── App.css
│ ├── App.tsx
│ ├── assets
│ │ └── react.svg
│ ├── index.css
│ ├── main.tsx
│ └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
├── package.json
├── rollup.config.js
├── src
├── index.ts
├── join-loader
│ ├── bundle.ts
│ ├── index.ts
│ ├── join-loader.ts
│ └── workers
│ │ └── join-worker.ts
├── pmt-layer
│ ├── index.ts
│ └── pmt-layer.ts
├── pmt-loader
│ ├── bundle.js
│ ├── index.ts
│ ├── pmt-loader.ts
│ └── workers
│ │ └── pmt-worker.ts
└── useJoinLoader
│ ├── types.ts
│ └── useJoinLoader.ts
├── tsconfig.json
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "modules": false
7 | }
8 | ],
9 | "@babel/preset-react"
10 | ],
11 | "plugins": ["react-hot-loader/babel"]
12 | }
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 |
7 | [*.{js,json,yml}]
8 | charset = utf-8
9 | indent_style = space
10 | indent_size = 2
11 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended",],
7 | "overrides": [],
8 | "parser": "@typescript-eslint/parser",
9 | "parserOptions": {
10 | "ecmaVersion": "latest",
11 | "sourceType": "module"
12 | },
13 | "plugins": ["react", "@typescript-eslint"],
14 | "rules": {}
15 | };
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .yarn/*
2 | !.yarn/patches
3 | !.yarn/plugins
4 | !.yarn/releases
5 | !.yarn/sdks
6 | !.yarn/versions
7 |
8 | # Swap the comments on the following lines if you don't wish to use zero-installs
9 | # Documentation here: https://yarnpkg.com/features/zero-installs
10 | !.yarn/cache
11 | #.pnp.*
12 |
13 | node_modules
14 | dist
15 | .yarn
16 | lib
17 | */**/.DS_Store
18 | .DS_Store
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | legacy-peer-deps=true
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @maticoapp/deck.gl-pmtiles
2 |
3 | This repo provides a typed Deck.gl layer and loader for PMTiles data. PMTiles data provides a serverless and compact way to store tile geospatial data. Combined with Deck.gl's rendering, this provides a flexible and powerful way to cheaply manage geospatial data.
4 |
5 | Learn more about Brandon Liu's [PMTiles and ProtoMaps work](https://github.com/protomaps/PMTiles).
6 |
7 | ## Breaking Changes
8 | Version 0.0.3 breaks support for V2 tiles. Legacy support will be coming back soon. If you need to use legacy v2 spec PMtiles, use version 0.0.14 of this package.
9 | ## Roadmap
10 | - Raster tile support
11 | - V2 legacy support
12 | ## Repo Scripts:
13 |
14 | ```js
15 | yarn build-dev
16 | // builds the loader/layer for development and listens for changes
17 |
18 |
19 | yarn build-prod
20 | // builds the loader/layer for production
21 |
22 | yarn dev
23 | // runs the development server for the loader/layer and the frontend example
24 | ```
25 |
26 |
27 | ## Example Usage:
28 |
29 | ```typescript
30 | import React from 'react'
31 | import { PMTLayer } from "@maticoapp/deck.gl-pmtiles";
32 | import DeckGL from "@deck.gl/react/typed";
33 |
34 | const INITIAL_VIEW_STATE = {
35 | longitude: -80,
36 | latitude: 37,
37 | zoom: 6,
38 | pitch: 0,
39 | bearing: 0,
40 | };
41 |
42 | const Example: React.FC = () => {
43 | const Layers = [
44 | new PMTLayer({
45 | id: "pmtiles-layer",
46 | data: "https://protomaps-static.sfo3.digitaloceanspaces.com/mantle-trial.pmtiles",
47 | getFillColor: [255, 120, 120],
48 | stroked: true,
49 | getLineColor: [8, 8, 8],
50 | lineWidthMinPixels: 1
51 | })
52 | ]
53 |
54 | return
59 | }
60 |
61 | export default Example
62 |
63 | ```
64 |
65 | ## Peer Dependencies
66 |
67 | You must also install these packages in your app:
68 |
69 | - deck.gl
70 | - pmtiles
71 |
72 | ## Other Libraries and References
73 |
74 |
75 | This repo's code builds on existing work adapted from:
76 |
77 | - [Deckgl MVT Layer (MIT Licensed)](https://github.com/visgl/deck.gl/blob/master/modules/geo-layers/src/mvt-layer/mvt-layer.ts)
78 | - [@jtmiclat/deck.gl-pmtiles (MIT Licensed)](https://github.com/jtmiclat/deck.gl-pmtiles)
79 | - [PMtiles Leaflet and Maplibre implementations (BSD-3 license)](https://github.com/protomaps/PMTiles/tree/master/js)
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PMtiles Demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-pmtiles",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@adobe/react-spectrum": "^3.21.1",
13 | "@loaders.gl/core": "^3.2.9",
14 | "@loaders.gl/csv": "^3.2.9",
15 | "@react-spectrum/color": "^3.0.0-beta.15",
16 | "@tanstack/react-query": "^4.2.3",
17 | "deck.gl": "^8.8.9",
18 | "fflate": "^0.7.3",
19 | "pmtiles": "^1.1.0",
20 | "react": "^18.2.0",
21 | "react-dom": "^18.2.0"
22 | },
23 | "devDependencies": {
24 | "@types/react": "^18.0.17",
25 | "@types/react-dom": "^18.0.6",
26 | "@vitejs/plugin-react": "^2.1.0",
27 | "typescript": "^4.6.4",
28 | "vite": "^3.1.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/example/public/output.pmtiles:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Matico-Platform/deck.gl-pmtiles/483799485770df53ce0e5a5953598a8c3841b0db/example/public/output.pmtiles
--------------------------------------------------------------------------------
/example/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding: 2rem;
5 | text-align: center;
6 | }
7 |
8 | .logo {
9 | height: 6em;
10 | padding: 1.5em;
11 | will-change: filter;
12 | }
13 | .logo:hover {
14 | filter: drop-shadow(0 0 2em #646cffaa);
15 | }
16 | .logo.react:hover {
17 | filter: drop-shadow(0 0 2em #61dafbaa);
18 | }
19 |
20 | @keyframes logo-spin {
21 | from {
22 | transform: rotate(0deg);
23 | }
24 | to {
25 | transform: rotate(360deg);
26 | }
27 | }
28 |
29 | @media (prefers-reduced-motion: no-preference) {
30 | a:nth-of-type(2) .logo {
31 | animation: logo-spin infinite 20s linear;
32 | }
33 | }
34 |
35 | .card {
36 | padding: 2em;
37 | }
38 |
39 | .read-the-docs {
40 | color: #888;
41 | }
42 |
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { PMTLayer } from "../../src";
3 | import "./App.css";
4 | import DeckGL from "@deck.gl/react/typed";
5 | import { BitmapLayer, GeoJsonLayer } from "@deck.gl/layers/typed";
6 | import { TileLayer } from "@deck.gl/geo-layers/typed";
7 | import {
8 | RangeSlider,
9 | TextField,
10 | Heading,
11 | Flex,
12 | ProgressCircle,
13 | } from "@adobe/react-spectrum";
14 | import { parseColor } from "@react-stately/color";
15 | import { useQuery } from "@tanstack/react-query";
16 | import { CSVLoader } from "@loaders.gl/csv";
17 | import { load } from "@loaders.gl/core";
18 |
19 | const INITIAL_VIEW_STATE = {
20 | longitude: -90,
21 | latitude: 42,
22 | zoom: 7,
23 | pitch: 0,
24 | bearing: 0,
25 | };
26 |
27 | export default function App() {
28 | const [dataSource, setDataSource] = useState(
29 | "/output.pmtiles"
30 | );
31 | const [zoomRange, setZoomRange] = useState<{ start: number; end: number }>({
32 | start: 5,
33 | end: 10,
34 | });
35 | const {
36 | isLoading,
37 | error,
38 | data: tableData,
39 | } = useQuery(["tableData"], () =>
40 | load("/percapita_income.csv", CSVLoader, {
41 | csv: { header: true, dynamicTyping: false },
42 | })
43 | );
44 |
45 | if (isLoading) {
46 | return (
47 |
55 |
61 |
62 | Loading...
63 |
64 |
65 | );
66 | }
67 |
68 | const layers = [
69 | new TileLayer({
70 | data: "https://c.tile.openstreetmap.org/{z}/{x}/{y}.png",
71 | minZoom: 0,
72 | maxZoom: 19,
73 | tileSize: 256,
74 | renderSubLayers: (props) => {
75 | // console.log(props)
76 | const {
77 | // @ts-ignore
78 | bbox: { west, south, east, north },
79 | } = props.tile;
80 |
81 | return new BitmapLayer(props, {
82 | data: null,
83 | image: props.data,
84 | bounds: [west, south, east, north],
85 | });
86 | },
87 | }),
88 | new PMTLayer({
89 | id: "pmtiles-layer",
90 | data: dataSource,
91 | onClick: (info) => {
92 | console.log(info);
93 | },
94 | maxZoom: zoomRange.end,
95 | minZoom: zoomRange.start,
96 | getFillColor: (d: any) => [255 * (+d.properties.STATEFP / 90), 0, 0],
97 | pickable: true,
98 | }),
99 | ];
100 |
101 | return (
102 |
103 |
109 |
122 |
123 | PMTiles Layer
124 |
129 |
136 |
137 |
138 |
139 | );
140 | }
141 |
--------------------------------------------------------------------------------
/example/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
3 | font-size: 16px;
4 | line-height: 24px;
5 | font-weight: 400;
6 |
7 | color-scheme: light dark;
8 | color: rgba(255, 255, 255, 0.87);
9 | background-color: #242424;
10 |
11 | font-synthesis: none;
12 | text-rendering: optimizeLegibility;
13 | -webkit-font-smoothing: antialiased;
14 | -moz-osx-font-smoothing: grayscale;
15 | -webkit-text-size-adjust: 100%;
16 | }
17 |
18 | a {
19 | font-weight: 500;
20 | color: #646cff;
21 | text-decoration: inherit;
22 | }
23 | a:hover {
24 | color: #535bf2;
25 | }
26 |
27 | body {
28 | margin: 0;
29 | display: flex;
30 | place-items: center;
31 | min-width: 320px;
32 | min-height: 100vh;
33 | }
34 |
35 | h1 {
36 | font-size: 3.2em;
37 | line-height: 1.1;
38 | }
39 |
40 | button {
41 | border-radius: 8px;
42 | border: 1px solid transparent;
43 | padding: 0.6em 1.2em;
44 | font-size: 1em;
45 | font-weight: 500;
46 | font-family: inherit;
47 | background-color: #1a1a1a;
48 | cursor: pointer;
49 | transition: border-color 0.25s;
50 | }
51 | button:hover {
52 | border-color: #646cff;
53 | }
54 | button:focus,
55 | button:focus-visible {
56 | outline: 4px auto -webkit-focus-ring-color;
57 | }
58 |
59 | @media (prefers-color-scheme: light) {
60 | :root {
61 | color: #213547;
62 | background-color: #ffffff;
63 | }
64 | a:hover {
65 | color: #747bff;
66 | }
67 | button {
68 | background-color: #f9f9f9;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/example/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App";
4 | import "./index.css";
5 | import { darkTheme, Provider } from "@adobe/react-spectrum";
6 | import {
7 | QueryClient,
8 | QueryClientProvider,
9 | } from "@tanstack/react-query";
10 |
11 | const queryClient = new QueryClient();
12 |
13 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/example/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src"],
20 | "references": [{ "path": "./tsconfig.node.json" }]
21 | }
22 |
--------------------------------------------------------------------------------
/example/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/example/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()]
7 | })
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@maticoapp/deck.gl-pmtiles",
3 | "author": "Dylan Halpern",
4 | "packageManager": "yarn@3.2.2",
5 | "types": "dist/index.d.ts",
6 | "module": "dist/index.js",
7 | "main": "dist/index.js",
8 | "version": "0.0.33",
9 | "license": "MIT",
10 | "private": true,
11 | "workspaces": [
12 | "example",
13 | "deck.gl-pmtiles"
14 | ],
15 | "devDependencies": {
16 | "@babel/core": "^7.18.13",
17 | "@babel/preset-env": "^7.18.10",
18 | "@types/ms": "^0.7.30",
19 | "@typescript-eslint/eslint-plugin": "latest",
20 | "@typescript-eslint/parser": "latest",
21 | "babel-loader": "^8.2.5",
22 | "concurrently": "^7.3.0",
23 | "deck.gl": "^8.8.9",
24 | "esbuild": "^0.15.7",
25 | "eslint": "^8.23.0",
26 | "eslint-plugin-react": "latest",
27 | "geojson": "^0.5.0",
28 | "rollup-plugin-dts": "^4.2.2",
29 | "rollup-plugin-esbuild": "^4.10.1",
30 | "tslib": "^2.4.0"
31 | },
32 | "dependencies": {
33 | "typescript": "^4.8.2",
34 | "deck.gl": "^8.8.9",
35 | "pmtiles": "^2.4.0"
36 | },
37 | "scripts": {
38 | "lint": "eslint --fix 'src/**/*.ts'",
39 | "test": "echo \"Error: no test specified\" && exit 1",
40 | "example": "cd example && yarn dev",
41 | "dev": "concurrently \"yarn build-dev\" \"yarn workspace example-pmtiles dev\"",
42 | "build": "tsc && yarn pre-build && rollup -c",
43 | "pre-build": "tsc && yarn build-worker && yarn build-bundle",
44 | "build-bundle": "esbuild src/pmt-loader/bundle.js --bundle --outfile=dist/dist.min.js",
45 | "build-worker": "esbuild src/pmt-loader/workers/pmt-worker.ts --bundle --outfile=dist/pmt-worker.js --define:__VERSION__=\\\"$npm_package_version\\\"",
46 | "build-dev": "tsc && yarn pre-build && rollup -c",
47 | "type-check": "tsc --noEmit"
48 | },
49 | "description": "This repo provides a typed Deck.gl layer and loader for PMTiles data. PMTiles data provides a serverless and compact way to store tile geospatial data. Combined with Deck.gl's rendering, this provides a flexible and powerful way to cheaply manage geospatial data.",
50 | "directories": {
51 | "example": "example"
52 | },
53 | "repository": {
54 | "type": "git",
55 | "url": "git+https://github.com/Matico-Platform/deck.gl-pmtiles.git"
56 | },
57 | "keywords": [
58 | "pmtiles",
59 | "geo",
60 | "deck.gl"
61 | ],
62 | "bugs": {
63 | "url": "https://github.com/Matico-Platform/deck.gl-pmtiles/issues"
64 | },
65 | "homepage": "https://github.com/Matico-Platform/deck.gl-pmtiles#readme"
66 | }
67 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import dts from 'rollup-plugin-dts'
2 | import esbuild from 'rollup-plugin-esbuild'
3 |
4 | export default [
5 | {
6 | input: `src/index.ts`,
7 | plugins: [esbuild()],
8 | output: [
9 | {
10 | file: `dist/index.js`,
11 | format: 'cjs',
12 | sourcemap: true,
13 | },
14 | ]
15 | },
16 | {
17 | input: `src/index.ts`,
18 | plugins: [dts()],
19 | output: {
20 | file: `dist/index.d.ts`,
21 | format: 'es',
22 | },
23 | }
24 | ]
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import PMTLayer from "./pmt-layer/pmt-layer";
2 | import { PMTLoader, PMTWorkerLoader } from "./pmt-loader";
3 | import { useJoinData, useJoinLoader } from "./useJoinLoader/useJoinLoader";
4 |
5 | export {
6 | PMTLayer,
7 | PMTWorkerLoader,
8 | PMTLoader,
9 | useJoinLoader,
10 | useJoinData,
11 | }
--------------------------------------------------------------------------------
/src/join-loader/bundle.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | const moduleExports = require('./index');
3 | globalThis.loaders = globalThis.loaders || {};
4 | module.exports = Object.assign(globalThis.loaders, moduleExports);
--------------------------------------------------------------------------------
/src/join-loader/index.ts:
--------------------------------------------------------------------------------
1 | export {JoinLoader, JoinWorkerLoader} from './join-loader';
--------------------------------------------------------------------------------
/src/join-loader/join-loader.ts:
--------------------------------------------------------------------------------
1 | import type { Loader, LoaderWithParser, LoaderOptions } from "@loaders.gl/loader-utils";
2 | //@ts-ignore
3 | import { decompressSync } from "fflate";
4 | import ParseMVT from "@loaders.gl/mvt/dist/lib/parse-mvt";
5 | import { MVTLoaderOptions } from "@loaders.gl/mvt/dist/lib/types";
6 | import parseImage from '@loaders.gl/images/dist/lib/parsers/parse-image'
7 | import { ImageLoaderOptions } from "@loaders.gl/images/dist/image-loader";
8 | // __VERSION__ is injected by babel-plugin-version-inline
9 | // @ts-ignore TS2304: Cannot find name '__VERSION__'.
10 | const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "latest";
11 |
12 | type JoinLoaderOptions = LoaderOptions & {
13 | join?: {
14 |
15 | }
16 | }
17 |
18 | const DEFAULT_PMT_LOADER_OPTIONS: JoinLoaderOptions = {
19 | join: {
20 | }
21 | };
22 |
23 | /**
24 | * Worker loader for the Dataset join Loader
25 | */
26 | export const JoinWorkerLoader: Loader = {
27 | id: "join",
28 | module: "join",
29 | name: "Join",
30 | version: VERSION,
31 | extensions: ["*"],
32 | mimeTypes: ["application/x-www-form-urlencoded"],
33 | worker: true,
34 | category: "utility",
35 | options: DEFAULT_PMT_LOADER_OPTIONS,
36 | };
37 |
38 | /**
39 | * Loader for the Mapbox Vector Tile format
40 | */
41 | export const JoinLoader: LoaderWithParser = {
42 | ...JoinWorkerLoader,
43 | parse: async (data, options?: JoinLoaderOptions) =>
44 | parseJoin(data, options),
45 | binary: true,
46 | worker: false,
47 | };
48 |
49 | /**
50 | * Parse PMT arrayBuffer and return GeoJSON.
51 | *
52 | * @param arrayBuffer A MVT arrayBuffer
53 | * @param options
54 | * @returns A GeoJSON geometry object or a binary representation
55 | */
56 | async function parseJoin(data: ArrayBuffer, options?: JoinLoaderOptions) {
57 | console.log(data, options)
58 | return data
59 | }
--------------------------------------------------------------------------------
/src/join-loader/workers/join-worker.ts:
--------------------------------------------------------------------------------
1 | import {JoinLoader} from '../join-loader';
2 | import {createLoaderWorker} from '@loaders.gl/loader-utils';
3 |
4 | createLoaderWorker(JoinLoader);
--------------------------------------------------------------------------------
/src/pmt-layer/index.ts:
--------------------------------------------------------------------------------
1 | import { PMTLayer } from "./pmt-layer";
2 |
3 | export {
4 | PMTLayer
5 | }
--------------------------------------------------------------------------------
/src/pmt-layer/pmt-layer.ts:
--------------------------------------------------------------------------------
1 | import { type TileLayerProps, MVTLayer } from "@deck.gl/geo-layers/typed";
2 | import { type DefaultProps } from "@deck.gl/core/typed";
3 | import { GeoJsonLayer, type GeoJsonLayerProps } from "@deck.gl/layers/typed";
4 |
5 | import { findTile, PMTiles, zxyToTileId } from "pmtiles";
6 | import type { BinaryFeatures } from "@loaders.gl/schema";
7 | import type { Feature } from "geojson";
8 |
9 | import type {Loader} from '@loaders.gl/loader-utils';
10 |
11 | import { PMTWorkerLoader } from "../pmt-loader";
12 |
13 |
14 | // from @deck.gl/geo-layers/src/mvt-layer/mvt-layer
15 |
16 |
17 | export type TileJson = {
18 | tilejson: string;
19 | tiles: string[];
20 | // eslint-disable-next-line camelcase
21 | vector_layers: any[];
22 | attribution?: string;
23 | scheme?: string;
24 | maxzoom?: number;
25 | minzoom?: number;
26 | version?: string;
27 | };
28 | /** Props added by the MVTLayer */
29 | export type _MVTLayerProps = {
30 | /** Called if `data` is a TileJSON URL when it is successfully fetched. */
31 | onDataLoad?: ((tilejson: TileJson | null) => void) | null;
32 |
33 | /** Needed for highlighting a feature split across two or more tiles. */
34 | uniqueIdProperty?: string;
35 |
36 | /** A feature with ID corresponding to the supplied value will be highlighted. */
37 | highlightedFeatureId?: string | null;
38 |
39 | /**
40 | * Use tile data in binary format.
41 | *
42 | * @default true
43 | */
44 | binary?: boolean;
45 |
46 | /**
47 | * Loaders used to transform tiles into `data` property passed to `renderSubLayers`.
48 | *
49 | * @default [MVTWorkerLoader] from `@loaders.gl/mvt`
50 | */
51 | loaders?: Loader[];
52 | };
53 |
54 | // From @deck.gl/geo-layers/typed/tile-layer/types
55 | export type GeoBoundingBox = {west: number; north: number; east: number; south: number};
56 | export type NonGeoBoundingBox = {left: number; top: number; right: number; bottom: number};
57 |
58 | export type TileBoundingBox = NonGeoBoundingBox | GeoBoundingBox;
59 |
60 | export type TileIndex = {x: number; y: number; z: number};
61 |
62 | export type TileLoadProps = {
63 | index: TileIndex;
64 | id: string;
65 | bbox: TileBoundingBox;
66 | url?: string | null;
67 | signal?: AbortSignal;
68 | userData?: Record;
69 | zoom?: number;
70 | };
71 |
72 |
73 | export type ParsedPmTile = Feature[] | BinaryFeatures;
74 |
75 | export type ExtraProps = {
76 | raster?: boolean;
77 | };
78 |
79 | export type _PMTLayerProps = _MVTLayerProps & ExtraProps;
80 |
81 | export type PmtLayerProps = _PMTLayerProps &
82 | GeoJsonLayerProps &
83 | TileLayerProps;
84 |
85 | const defaultProps: DefaultProps = {
86 | ...GeoJsonLayer.defaultProps,
87 | onDataLoad: { type: "function", value: null, optional: true, compare: false },
88 | uniqueIdProperty: "",
89 | highlightedFeatureId: null,
90 | binary: true,
91 | raster: false,
92 | loaders: [PMTWorkerLoader]
93 | };
94 |
95 | type ZxyOffset = { offset: number; length: number };
96 | export class DeckglPmtiles extends PMTiles {
97 | async getZxyOffset(
98 | z: number,
99 | x: number,
100 | y: number,
101 | signal?: AbortSignal
102 | ): Promise {
103 | const tile_id = zxyToTileId(z, x, y);
104 | const header = await this.cache.getHeader(this.source);
105 | // V2 COMPATIBILITY
106 | // if (header.specVersion < 3) {
107 | // return v2.getZxy(header, this.source, this.cache, z, x, y, signal);
108 | // }
109 |
110 | if (z < header.minZoom || z > header.maxZoom) {
111 | return undefined;
112 | }
113 |
114 | let d_o = header.rootDirectoryOffset;
115 | let d_l = header.rootDirectoryLength;
116 | for (let depth = 0; depth <= 3; depth++) {
117 | const directory = await this.cache.getDirectory(
118 | this.source,
119 | d_o,
120 | d_l,
121 | header
122 | );
123 | const entry = findTile(directory, tile_id);
124 | if (entry) {
125 | if (entry.runLength > 0) {
126 | return {
127 | offset: entry.offset,
128 | length: entry.length,
129 | };
130 | } else {
131 | d_o = header.leafDirectoryOffset + entry.offset;
132 | d_l = entry.length;
133 | }
134 | } else {
135 | return undefined;
136 | }
137 | }
138 | throw Error("Maximum directory depth exceeded");
139 | }
140 | }
141 |
142 | export class PMTLayer<
143 | DataT extends Feature = Feature,
144 | ExtraProps = {}
145 | > extends MVTLayer {
146 | static layerName = "PMTilesLayer";
147 | static defaultProps = defaultProps;
148 |
149 | initializeState(): void {
150 | super.initializeState();
151 | // GlobeView doesn't work well with binary data
152 | const binary =
153 | this.context.viewport.resolution !== undefined
154 | ? false
155 | : this.props.binary;
156 |
157 | // @ts-ignore
158 | const raster = this.props.raster;
159 |
160 | (this as any)._updateTileData = async (): Promise => {
161 | const data = this.props.data;
162 | // @ts-ignore
163 | const raster = this.props.raster;
164 | const pmtiles = new DeckglPmtiles(data as string);
165 | const header = await pmtiles.getHeader();
166 | this.setState({ data, pmtiles, raster, header });
167 | };
168 |
169 | this.setState({
170 | binary,
171 | raster,
172 | data: null,
173 | });
174 | }
175 |
176 | getTileData(loadProps: TileLoadProps, iter?: number): Promise {
177 | const { index, signal } = loadProps;
178 | const { data, binary, raster, pmtiles, header } = this.state;
179 | const { x, y, z } = index;
180 | let loadOptions = this.getLoadOptions();
181 | const { fetch } = this.props;
182 |
183 | return pmtiles
184 | .getZxyOffset(z, x, y, signal)
185 | .then((entry: Awaited) => {
186 | if (!entry) {
187 | return new Promise((resolve) => resolve(null));
188 | }
189 | const tileOffset = entry.offset + header.tileDataOffset;
190 | const tileLength = entry.length;
191 |
192 | loadOptions = {
193 | ...loadOptions,
194 | mimeType: "application/x-protobuf",
195 | pmt: {
196 | workerUrl: "https://unpkg.com/@maticoapp/deck.gl-pmtiles@latest/dist/pmt-worker.js",
197 | coordinates: this.context.viewport.resolution ? "wgs84" : "local",
198 | tileIndex: index,
199 | raster: raster,
200 | tileCompression: header.tileCompression,
201 | ...loadOptions?.pmt,
202 | },
203 | gis: binary ? { format: "binary" } : {},
204 | fetch: {
205 | headers: {
206 | Range: `bytes=${tileOffset}-${tileOffset + tileLength - 1}`,
207 | },
208 | },
209 | };
210 | return fetch(data, {
211 | propName: "data",
212 | layer: this,
213 | loadOptions,
214 | signal,
215 | });
216 | });
217 | }
218 | }
219 | export default PMTLayer;
220 |
221 | // code adapted from
222 | // Deckgl MVT Layer (MIT) https://github.com/visgl/deck.gl/blob/master/modules/geo-layers/src/mvt-layer/mvt-layer.ts
223 | // @jtmiclat/deck.gl-pmtiles (MIT) https://github.com/jtmiclat/deck.gl-pmtiles
224 |
--------------------------------------------------------------------------------
/src/pmt-loader/bundle.js:
--------------------------------------------------------------------------------
1 | const moduleExports = require('./index');
2 | globalThis.loaders = globalThis.loaders || {};
3 | module.exports = Object.assign(globalThis.loaders, moduleExports);
--------------------------------------------------------------------------------
/src/pmt-loader/index.ts:
--------------------------------------------------------------------------------
1 | export {PMTLoader, PMTWorkerLoader} from './pmt-loader';
--------------------------------------------------------------------------------
/src/pmt-loader/pmt-loader.ts:
--------------------------------------------------------------------------------
1 | import type { Loader, LoaderWithParser, LoaderOptions } from "@loaders.gl/loader-utils";
2 | import { decompressSync } from "fflate";
3 | import ParseMVT from "@loaders.gl/mvt/dist/lib/parse-mvt";
4 | import { MVTLoaderOptions } from "@loaders.gl/mvt/dist/lib/types";
5 | import { ImageLoaderOptions } from "@loaders.gl/images/dist/image-loader";
6 | import { Compression } from "pmtiles";
7 | // __VERSION__ is injected by babel-plugin-version-inline
8 | // @ts-ignore TS2304: Cannot find name '__VERSION__'.
9 | const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "latest";
10 |
11 | type PMTLoaderOptions = LoaderOptions & {
12 | pmt?: {
13 | raster?: boolean;
14 | tileCompression?: Compression;
15 | }
16 | };
17 |
18 | const DEFAULT_PMT_LOADER_OPTIONS: PMTLoaderOptions & MVTLoaderOptions & ImageLoaderOptions = {
19 | pmt: {
20 | raster: false
21 | },
22 | // @ts-ignore
23 | mvt: {
24 | },
25 | image: {}
26 | };
27 | /**
28 | * Worker loader for the Mapbox Vector Tile format
29 | */
30 | export const PMTWorkerLoader: Loader = {
31 | id: "pmt",
32 | module: "pmt",
33 | name: "PMTiles",
34 | version: VERSION,
35 | extensions: ["pmtiles"],
36 | mimeTypes: ["application/x-protobuf"],
37 | worker: true,
38 | category: "geometry",
39 | options: DEFAULT_PMT_LOADER_OPTIONS,
40 | };
41 |
42 | /**
43 | * Loader for the Mapbox Vector Tile format
44 | */
45 | export const PMTLoader: LoaderWithParser = {
46 | ...PMTWorkerLoader,
47 | // @ts-ignore
48 | parse: async (arrayBuffer, options?: PMTLoaderOptions) => {
49 | return parsePMT(arrayBuffer, options)
50 | },
51 | // @ts-ignore
52 | parseSync: (arrayBuffer, options?: PMTLoaderOptions) => {
53 | return parsePMT(arrayBuffer, options)
54 | },
55 | binary: true,
56 | };
57 |
58 | /**
59 | * Parse PMT arrayBuffer and return GeoJSON.
60 | *
61 | * @param arrayBuffer A MVT arrayBuffer
62 | * @param options
63 | * @returns A GeoJSON geometry object or a binary representation
64 | */
65 | function parsePMT(arrayBuffer: ArrayBuffer, options?: PMTLoaderOptions) {
66 | if (options && options?.pmt?.raster){
67 | const blob = new Blob([arrayBuffer], {type: "image/png"});
68 | const url = URL.createObjectURL(blob);
69 | // const url = window.URL.createObjectURL(blob);
70 | return createImageBitmap(blob);
71 | } else if (options?.pmt?.tileCompression) {
72 | const decompressed = fflateDecompress(arrayBuffer, options.pmt.tileCompression)
73 | const tiledata = ParseMVT(decompressed, options);
74 | return tiledata;
75 | }
76 | }
77 |
78 | function fflateDecompress(
79 | buf: ArrayBuffer,
80 | compression: Compression
81 | ): ArrayBuffer {
82 | if (compression === Compression.None || compression === Compression.Unknown) {
83 | return buf;
84 | } else if (compression === Compression.Gzip) {
85 | return decompressSync(new Uint8Array(buf));
86 | } else {
87 | throw Error("Compression method not supported");
88 |
89 | }
90 | }
--------------------------------------------------------------------------------
/src/pmt-loader/workers/pmt-worker.ts:
--------------------------------------------------------------------------------
1 | import {PMTLoader} from '../pmt-loader';
2 | import {createLoaderWorker} from '@loaders.gl/loader-utils';
3 |
4 | createLoaderWorker(PMTLoader);
--------------------------------------------------------------------------------
/src/useJoinLoader/types.ts:
--------------------------------------------------------------------------------
1 |
2 | import { LoaderWithParser } from "@loaders.gl/loader-utils";
3 | import type { BinaryFeatures } from "@loaders.gl/schema";
4 |
5 | export type DataShapeNames = keyof DataShapes;
6 | export type DataShapes = {
7 | "binary": BinaryFeatures,
8 | "binary-geometry": BinaryFeatures
9 | "columnar-table": {'shape': "columnar-table", 'data': BinaryFeatures},
10 | "geojson": GeoJSON.FeatureCollection,
11 | "geojson-row-table": {'shape': "geojson-row-table", 'data': GeoJSON.FeatureCollection},
12 |
13 | }
14 | export type BinaryEntries = [keyof BinaryFeatures, any];
15 | export type JoinLoaderProps = {
16 | loader: LoaderWithParser;
17 | shape: "binary";
18 | leftId: string;
19 | rightId: string;
20 | tableData?: {[key: string]: any}[];
21 | dataDict?: {[key: string]: object};
22 | dataMap?: Map;
23 | updateTriggers?: any[];
24 | }
25 | export type JoinLoader = (props: JoinLoaderProps) => LoaderWithParser;
26 |
--------------------------------------------------------------------------------
/src/useJoinLoader/useJoinLoader.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { LoaderWithParser } from "@loaders.gl/loader-utils";
3 | import type { BinaryFeatures } from "@loaders.gl/schema";
4 |
5 | type DataShapeNames = keyof DataShapes;
6 | type DataShapes = {
7 | binary: BinaryFeatures;
8 | "binary-geometry": BinaryFeatures;
9 | "columnar-table": { shape: "columnar-table"; data: BinaryFeatures };
10 | geojson: GeoJSON.FeatureCollection;
11 | "geojson-row-table": {
12 | shape: "geojson-row-table";
13 | data: GeoJSON.FeatureCollection;
14 | };
15 | };
16 |
17 | type BinaryEntries = [keyof BinaryFeatures, any];
18 |
19 | function join({
20 | mapData,
21 | shape,
22 | leftId,
23 | dataAccessor,
24 | }: {
25 | mapData: DataShapes[T];
26 | shape: T;
27 | leftId: string;
28 | dataAccessor: (key: string) => object;
29 | }): DataShapes[T] {
30 | switch (shape) {
31 | case "columnar-table":
32 | case "binary-geometry":
33 | case "binary": {
34 | const isColumnar = shape === "columnar-table";
35 | const dataInner = isColumnar
36 | ? (mapData as DataShapes["columnar-table"])['data']
37 | : mapData as DataShapes["binary"];
38 | // @ts-ignore
39 | Object.entries(dataInner).forEach(
40 | // @ts-ignore
41 | ([featureType, { properties }]: BinaryEntries) => {
42 | properties &&
43 | properties.forEach((entry: { [key: string]: any }, i: number) => {
44 | const id = entry[leftId];
45 | const data = dataAccessor(id);
46 | if (data && dataInner?.[featureType]) { // @ts-ignore
47 | dataInner[featureType].properties[i] = {
48 | ...entry,
49 | ...data,
50 | };
51 | }
52 | });
53 | }
54 | );
55 | break;
56 | }
57 | case "geojson-row-table":
58 | case "geojson": {
59 | const isRowTable = shape === "geojson-row-table";
60 | const dataInner = isRowTable
61 | ? (mapData as DataShapes["geojson-row-table"])['data']
62 | : mapData as DataShapes["geojson"];
63 |
64 | dataInner.features.forEach((entry: { [key: string]: any }) => {
65 | const id = entry.properties[leftId];
66 | const data = dataAccessor(id);
67 | if (data) {
68 | entry.properties = {
69 | ...entry.properties,
70 | ...data,
71 | };
72 | }
73 | });
74 | break;
75 | }
76 | default:
77 | break;
78 | }
79 | return mapData as DataShapes[T];
80 | }
81 |
82 | export const useJoinLoader = ({
83 | loader,
84 | shape,
85 | leftId,
86 | rightId,
87 | tableData,
88 | dataDict,
89 | dataMap,
90 | updateTriggers,
91 | }: {
92 | loader: LoaderWithParser;
93 | shape: keyof DataShapes;
94 | leftId: string;
95 | rightId: string;
96 | tableData?: { [key: string]: any }[];
97 | dataDict?: { [key: string]: object };
98 | dataMap?: Map;
99 | updateTriggers?: any[];
100 | }): LoaderWithParser => {
101 | const dataAccessor = useMemo(() => {
102 | if (!dataDict && !dataMap) {
103 | const tempMap = new Map();
104 | tableData &&
105 | tableData.forEach((entry) => tempMap.set(entry[rightId], entry));
106 | return (key: string) => tempMap.get(key);
107 | } else if (dataMap) {
108 | return (key: string) => dataMap.get(key);
109 | } else if (dataDict) {
110 | return (key: string) => dataDict[key];
111 | } else {
112 | return (_key: string) => {};
113 | }
114 | }, [rightId, updateTriggers || tableData]);
115 |
116 | const injectedParse: typeof loader.parse = async (arrayBuffer, options) => {
117 | const mapData = await loader.parse(arrayBuffer, options);
118 | return join({
119 | shape,
120 | mapData,
121 | leftId,
122 | dataAccessor,
123 | });
124 | };
125 |
126 | const injectedLoader = {
127 | ...loader,
128 | parse: injectedParse,
129 | };
130 |
131 | return injectedLoader;
132 | };
133 |
134 | export function useJoinData({
135 | shape,
136 | leftId,
137 | rightId,
138 | tableData,
139 | dataDict,
140 | dataMap,
141 | updateTriggers,
142 | }: {
143 | shape: T;
144 | leftId: string;
145 | rightId: string;
146 | tableData?: { [key: string]: any }[];
147 | dataDict?: { [key: string]: object };
148 | dataMap?: Map;
149 | updateTriggers?: any[];
150 | }): (data: DataShapes[T]) => DataShapes[T] {
151 | /**
152 | * Returns a function that can be used to join data in a given format with another table.
153 | *
154 | *
155 | * @shape x - The first input number
156 | * @param y - The second input number
157 | * @returns The arithmetic mean of `x` and `y`
158 | *
159 | * @beta
160 | */
161 | const dataAccessor = useMemo(() => {
162 | if (!dataDict && !dataMap) {
163 | const tempMap = new Map();
164 | tableData &&
165 | tableData.forEach((entry) => tempMap.set(entry[rightId], entry));
166 | return (key: string) => tempMap.get(key);
167 | } else if (dataMap) {
168 | return (key: string) => dataMap.get(key);
169 | } else if (dataDict) {
170 | return (key: string) => dataDict[key];
171 | } else {
172 | return (_key: string) => {};
173 | }
174 | }, [rightId, updateTriggers || tableData]);
175 |
176 | const doJoin: (data: DataShapes[T]) => DataShapes[T] = (mapData) =>
177 | join({
178 | shape,
179 | mapData,
180 | leftId,
181 | dataAccessor,
182 | });
183 |
184 | return doJoin;
185 | }
186 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "emitDecoratorMetadata": true,
4 | "experimentalDecorators": true,
5 | "forceConsistentCasingInFileNames": true,
6 | "jsx": "react",
7 | "module": "esnext",
8 | "moduleResolution": "node",
9 | "noImplicitAny": false,
10 | "allowJs": true,
11 | "outDir": "example/public/dist",
12 | "preserveConstEnums": true,
13 | "baseUrl": "./src/",
14 | "allowSyntheticDefaultImports":true,
15 | "target": "es6",
16 | "declaration": true,
17 | "declarationDir": "example/public/dist",
18 | "skipLibCheck": true,
19 | "strict": true,
20 | "checkJs": false,
21 | "esModuleInterop": true,
22 | "noEmit": true,
23 | "resolveJsonModule": true,
24 | },
25 | "exclude": [
26 | "node_modules",
27 | "example/public/dist/**/*",
28 | "example/*"
29 | ]
30 | }
--------------------------------------------------------------------------------