diff --git a/package-lock.json b/package-lock.json index 85f6f768..6d8b8bf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "maputnik-design": "github:maputnik/design#172b06c", "ol": "^6.14.1", "ol-mapbox-style": "^7.1.1", + "pmtiles": "^3.2.0", "prop-types": "^15.8.1", "react": "^18.2.0", "react-accessible-accordion": "^5.0.0", @@ -2131,6 +2132,15 @@ "integrity": "sha512-131wOmuwDg8ypYCSQ437bGdP+K2lJ8GJUu+ng4iQQxAc3irRnb7mGHbexsPChBcKWLctTR9V5LJdX5A8WWk44A==", "dev": true }, + "node_modules/@types/leaflet": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.12.tgz", + "integrity": "sha512-BK7XS+NyRI291HIo0HCfE18Lp8oA30H1gpi1tf0mF3TgiCEzanQjOqNZ4x126SXzzi2oNSZhZ5axJp1k0iM6jg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/lodash": { "version": "4.17.0", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", @@ -5169,6 +5179,12 @@ "pend": "~1.2.0" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -8836,6 +8852,16 @@ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.3.tgz", "integrity": "sha512-VJK1SRmXBpjwsB4YOHYSturx48rLKMzHgCqDH2ZDa6ZbMS/N5huoNqyQdK5Fj/xayu3fqbXckn5SeCS1EbMDZg==" }, + "node_modules/pmtiles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-3.2.0.tgz", + "integrity": "sha512-4v3Nw5xeMxaUReLZQTz3PyM4VM/Lx/Xp/rc2GGEWMl0nqAmcb+gjyi+eOTwfPu8LnB0ash36hz0dV76uYvih5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/leaflet": "^1.9.8", + "fflate": "^0.8.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", diff --git a/package.json b/package.json index fd307127..616f4dca 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "maputnik-design": "github:maputnik/design#172b06c", "ol": "^6.14.1", "ol-mapbox-style": "^7.1.1", + "pmtiles": "^3.2.0", "prop-types": "^15.8.1", "react": "^18.2.0", "react-accessible-accordion": "^5.0.0", diff --git a/src/components/App.tsx b/src/components/App.tsx index 3c5509cf..73b8adc7 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -8,6 +8,7 @@ import get from 'lodash.get' import {unset} from 'lodash' import {arrayMoveMutable} from 'array-move' import hash from "string-hash"; +import { PMTiles } from "pmtiles"; import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl' import {latest, validateStyleMin} from '@maplibre/maplibre-gl-style-spec' @@ -638,33 +639,42 @@ export default class App extends React.Component { console.warn("Failed to setFetchAccessToken: ", err); } - fetch(url!, { - mode: 'cors', - }) - .then(response => response.json()) - .then(json => { + const setVectorLayers = (json:any) => { + if(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) { + return; + } - if(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) { - return; - } + // Create new objects before setState + const sources = Object.assign({}, { + [key]: this.state.sources[key], + }); - // Create new objects before setState - const sources = Object.assign({}, { - [key]: this.state.sources[key], - }); + for(const layer of json.vector_layers) { + (sources[key] as any).layers.push(layer.id) + } - for(const layer of json.vector_layers) { - (sources[key] as any).layers.push(layer.id) - } + console.debug("Updating source: "+key); + this.setState({ + sources: sources + }); + }; - console.debug("Updating source: "+key); - this.setState({ - sources: sources + if (url!.startsWith("pmtiles://")) { + (new PMTiles(url!.substr(10))).getTileJson("") + .then(json => setVectorLayers(json)) + .catch(err => { + console.error("Failed to process sources for '%s'", url, err); }); + } else { + fetch(url!, { + mode: 'cors', }) - .catch(err => { - console.error("Failed to process sources for '%s'", url, err); - }); + .then(response => response.json()) + .then(json => setVectorLayers(json)) + .catch(err => { + console.error("Failed to process sources for '%s'", url, err); + }); + } } else { sourceList[key] = this.state.sources[key] || this.state.mapStyle.sources[key]; diff --git a/src/components/MapMaplibreGl.tsx b/src/components/MapMaplibreGl.tsx index 87a2ad23..833aace3 100644 --- a/src/components/MapMaplibreGl.tsx +++ b/src/components/MapMaplibreGl.tsx @@ -15,6 +15,7 @@ import MaplibreGeocoder, { MaplibreGeocoderApi, MaplibreGeocoderApiConfig } from import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css'; import { withTranslation, WithTranslation } from 'react-i18next' import i18next from 'i18next' +import { Protocol } from "pmtiles"; function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container): HTMLElement { ReactDOM.render(popup, mountNode); @@ -148,6 +149,8 @@ class MapMaplibreGlInternal extends React.Component { diff --git a/src/components/ModalSources.tsx b/src/components/ModalSources.tsx index eabb5a8b..fccda23b 100644 --- a/src/components/ModalSources.tsx +++ b/src/components/ModalSources.tsx @@ -51,6 +51,7 @@ function editorMode(source: SourceSpecification) { } if(source.type === 'vector') { if(source.tiles) return 'tile_vector' + if(source.url?.startsWith("pmtiles://")) return 'pmtiles_vector' return 'tilejson_vector' } if(source.type === 'geojson') { @@ -129,6 +130,10 @@ class AddSource extends React.Component { const {protocol} = window.location; switch(mode) { + case 'pmtiles_vector': return { + type: 'vector', + url: `${protocol}//localhost:3000/file.pmtiles` + } case 'geojson_url': return { type: 'geojson', data: `${protocol}//localhost:3000/geojson.json` @@ -240,6 +245,7 @@ class AddSource extends React.Component { ['tile_raster', t('Raster (Tile URLs)')], ['tilejson_raster-dem', t('Raster DEM (TileJSON URL)')], ['tilexyz_raster-dem', t('Raster DEM (XYZ URLs)')], + ['pmtiles_vector', 'Vector (PMTiles)'], ['image', t('Image')], ['video', t('Video')], ]} diff --git a/src/components/ModalSourcesTypeEditor.tsx b/src/components/ModalSourcesTypeEditor.tsx index da4f7a3c..d5b8794b 100644 --- a/src/components/ModalSourcesTypeEditor.tsx +++ b/src/components/ModalSourcesTypeEditor.tsx @@ -11,7 +11,7 @@ import FieldCheckbox from './FieldCheckbox' import { WithTranslation, withTranslation } from 'react-i18next'; import { TFunction } from 'i18next' -export type EditorMode = "video" | "image" | "tilejson_vector" | "tile_raster" | "tilejson_raster" | "tilexyz_raster-dem" | "tilejson_raster-dem" | "tile_vector" | "geojson_url" | "geojson_json" | null; +export type EditorMode = "video" | "image" | "tilejson_vector" | "tile_raster" | "tilejson_raster" | "tilexyz_raster-dem" | "tilejson_raster-dem" | "pmtiles_vector" | "tile_vector" | "geojson_url" | "geojson_json" | null; type TileJSONSourceEditorProps = { source: { @@ -286,6 +286,32 @@ class GeoJSONSourceFieldJsonEditor extends React.Component { + render() { + const t = this.props.t; + return
+ this.props.onChange({ + ...this.props.source, + url: `pmtiles://${url}` + })} + /> + {this.props.children} +
+ } +} + type ModalSourcesTypeEditorInternalProps = { mode: EditorMode source: any @@ -343,6 +369,7 @@ class ModalSourcesTypeEditorInternal extends React.Component + case 'pmtiles_vector': return case 'image': return case 'video': return default: return null