├── .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 | } --------------------------------------------------------------------------------