Skip to content

Commit 8ac5e36

Browse files
committed
Add PMTiles vector support
* add PMTiles mode to source editing dialog * App.tsx detects PMTiles sources for fetching vector_layers (inspect does not yet work)
1 parent 0f1000c commit 8ac5e36

File tree

6 files changed

+95
-22
lines changed

6 files changed

+95
-22
lines changed

package-lock.json

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"maputnik-design": "github:maputnik/design#172b06c",
5858
"ol": "^6.14.1",
5959
"ol-mapbox-style": "^7.1.1",
60+
"pmtiles": "^3.1.0",
6061
"prop-types": "^15.8.1",
6162
"react": "^18.2.0",
6263
"react-accessible-accordion": "^5.0.0",

src/components/App.tsx

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import get from 'lodash.get'
88
import {unset} from 'lodash'
99
import {arrayMoveMutable} from 'array-move'
1010
import hash from "string-hash";
11+
import { PMTiles } from "pmtiles";
1112
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
1213
import {latest, validateStyleMin} from '@maplibre/maplibre-gl-style-spec'
1314

@@ -638,33 +639,42 @@ export default class App extends React.Component<any, AppState> {
638639
console.warn("Failed to setFetchAccessToken: ", err);
639640
}
640641

641-
fetch(url!, {
642-
mode: 'cors',
643-
})
644-
.then(response => response.json())
645-
.then(json => {
642+
const setVectorLayers = (json:any) => {
643+
if(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) {
644+
return;
645+
}
646646

647-
if(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) {
648-
return;
649-
}
647+
// Create new objects before setState
648+
const sources = Object.assign({}, {
649+
[key]: this.state.sources[key],
650+
});
650651

651-
// Create new objects before setState
652-
const sources = Object.assign({}, {
653-
[key]: this.state.sources[key],
654-
});
652+
for(const layer of json.vector_layers) {
653+
(sources[key] as any).layers.push(layer.id)
654+
}
655655

656-
for(const layer of json.vector_layers) {
657-
(sources[key] as any).layers.push(layer.id)
658-
}
656+
console.debug("Updating source: "+key);
657+
this.setState({
658+
sources: sources
659+
});
660+
};
659661

660-
console.debug("Updating source: "+key);
661-
this.setState({
662-
sources: sources
662+
if (url!.startsWith("pmtiles://")) {
663+
(new PMTiles(url!.substr(10))).getTileJson("")
664+
.then(json => setVectorLayers(json))
665+
.catch(err => {
666+
console.error("Failed to process sources for '%s'", url, err);
663667
});
668+
} else {
669+
fetch(url!, {
670+
mode: 'cors',
664671
})
665-
.catch(err => {
666-
console.error("Failed to process sources for '%s'", url, err);
667-
});
672+
.then(response => response.json())
673+
.then(json => setVectorLayers(json))
674+
.catch(err => {
675+
console.error("Failed to process sources for '%s'", url, err);
676+
});
677+
}
668678
}
669679
else {
670680
sourceList[key] = this.state.sources[key] || this.state.mapStyle.sources[key];

src/components/MapMaplibreGl.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import MaplibreGeocoder, { MaplibreGeocoderApi, MaplibreGeocoderApiConfig } from
1515
import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css';
1616
import { withTranslation, WithTranslation } from 'react-i18next'
1717
import i18next from 'i18next'
18+
import { Protocol } from "pmtiles";
1819

1920
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container): HTMLElement {
2021
ReactDOM.render(popup, mountNode);
@@ -148,6 +149,8 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
148149
localIdeographFontFamily: false
149150
} satisfies MapOptions;
150151

152+
let protocol = new Protocol();
153+
MapLibreGl.addProtocol("pmtiles",protocol.tile);
151154
const map = new MapLibreGl.Map(mapOpts);
152155

153156
const mapViewChange = () => {

src/components/ModalSources.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ function editorMode(source: SourceSpecification) {
5151
}
5252
if(source.type === 'vector') {
5353
if(source.tiles) return 'tilexyz_vector'
54+
if(source.url!.startsWith("pmtiles://")) return 'pmtiles_vector'
5455
return 'tilejson_vector'
5556
}
5657
if(source.type === 'geojson') {
@@ -129,6 +130,10 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
129130
const {protocol} = window.location;
130131

131132
switch(mode) {
133+
case 'pmtiles_vector': return {
134+
type: 'vector',
135+
url: `${protocol}//localhost:3000/file.pmtiles`
136+
}
132137
case 'geojson_url': return {
133138
type: 'geojson',
134139
data: `${protocol}//localhost:3000/geojson.json`
@@ -235,6 +240,7 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
235240
['tilexyz_raster', t('Raster (XYZ URL)')],
236241
['tilejson_raster-dem', t('Raster DEM (TileJSON URL)')],
237242
['tilexyz_raster-dem', t('Raster DEM (XYZ URLs)')],
243+
['pmtiles_vector', 'Vector (PMTiles)'],
238244
['image', t('Image')],
239245
['video', t('Video')],
240246
]}

src/components/ModalSourcesTypeEditor.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import FieldCheckbox from './FieldCheckbox'
1111
import { WithTranslation, withTranslation } from 'react-i18next';
1212
import { TFunction } from 'i18next'
1313

14-
export type EditorMode = "video" | "image" | "tilejson_vector" | "tilexyz_raster" | "tilejson_raster" | "tilexyz_raster-dem" | "tilejson_raster-dem" | "tilexyz_vector" | "geojson_url" | "geojson_json" | null;
14+
export type EditorMode = "video" | "image" | "tilejson_vector" | "tilexyz_raster" | "tilejson_raster" | "tilexyz_raster-dem" | "tilejson_raster-dem" | "pmtiles_vector" | "tilexyz_vector" | "geojson_url" | "geojson_json" | null;
1515

1616
type TileJSONSourceEditorProps = {
1717
source: {
@@ -271,6 +271,32 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
271271
}
272272
}
273273

274+
type PMTilesSourceEditorProps = {
275+
source: {
276+
url: string
277+
}
278+
onChange(...args: unknown[]): unknown
279+
children?: React.ReactNode
280+
} & WithTranslation;
281+
282+
class PMTilesSourceEditor extends React.Component<PMTilesSourceEditorProps> {
283+
render() {
284+
const t = this.props.t;
285+
return <div>
286+
<FieldUrl
287+
label={t("PMTiles URL")}
288+
fieldSpec={latest.source_vector.url}
289+
value={this.props.source.url}
290+
onChange={url => this.props.onChange({
291+
...this.props.source,
292+
url: `pmtiles://${url}`
293+
})}
294+
/>
295+
{this.props.children}
296+
</div>
297+
}
298+
}
299+
274300
type ModalSourcesTypeEditorInternalProps = {
275301
mode: EditorMode
276302
source: any
@@ -307,6 +333,7 @@ class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEdi
307333
value={this.props.source.encoding || latest.source_raster_dem.encoding.default}
308334
/>
309335
</TileURLSourceEditor>
336+
case 'pmtiles_vector': return <PMTilesSourceEditor {...commonProps} />
310337
case 'image': return <ImageSourceEditor {...commonProps} />
311338
case 'video': return <VideoSourceEditor {...commonProps} />
312339
default: return null

0 commit comments

Comments
 (0)