Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add URL/tiles distinction and PM Tiles support #91

Merged
merged 6 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions examples/pmtiles-rasterDem.jGIS
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"layerTree": [
"ee4ddf21-31f1-4a71-befd-c7a456628db6",
"e6a856f0-3883-4d10-9e9f-04132b3eb7f1"
],
"layers": {
"e6a856f0-3883-4d10-9e9f-04132b3eb7f1": {
"name": "Custom Hillshade Layer Layer",
"parameters": {
"shadowColor": "#473B24",
"source": "7b6789c2-74cd-4a74-ad4d-ceef22d1eab8"
},
"type": "HillshadeLayer",
"visible": true
},
"ee4ddf21-31f1-4a71-befd-c7a456628db6": {
"name": "OpenStreetMap.Mapnik Layer",
"parameters": {
"source": "d6afefa4-6dcc-4a24-88ad-f7af0f6d76f3"
},
"type": "RasterLayer",
"visible": true
}
},
"options": {
"bearing": 0.0,
"latitude": 2.842170943040401e-14,
"longitude": -14.148151338180469,
"pitch": 0.0,
"zoom": 1.2095796395452967
},
"sources": {
"7b6789c2-74cd-4a74-ad4d-ceef22d1eab8": {
"name": "PMTile",
"parameters": {
"encoding": "terrarium",
"tileSize": 512.0,
"url": "pmtiles://https://r2-public.protomaps.com/protomaps-sample-datasets/terrarium_z9.pmtiles",
"urlParameters": {}
},
"type": "RasterDemSource"
},
"d6afefa4-6dcc-4a24-88ad-f7af0f6d76f3": {
"name": "OpenStreetMap.Mapnik",
"parameters": {
"attribution": "(C) OpenStreetMap contributors",
"maxZoom": 19.0,
"minZoom": 0.0,
"provider": "OpenStreetMap",
"url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
"urlParameters": {}
},
"type": "RasterSource"
}
},
"terrain": {
"exaggeration": 0.0,
"source": ""
}
}
87 changes: 87 additions & 0 deletions examples/pmtiles-vector.jGIS
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"layerTree": [
"34e3dbaa-f1df-4add-af4b-8f8bec9bc0c8",
{
"layers": [
"3c9de1bc-8a95-4913-a3a7-c76ea54146e6",
"2171c5a1-b237-4413-a358-07ea8bcc67d3"
],
"name": "Firenze"
}
],
"layers": {
"2171c5a1-b237-4413-a358-07ea8bcc67d3": {
"name": "Roads Layer",
"parameters": {
"color": "#241f31",
"opacity": 1.0,
"source": "c09567a9-2c88-4818-ad40-536e005e9496",
"sourceLayer": "roads",
"type": "line"
},
"type": "VectorLayer",
"visible": true
},
"34e3dbaa-f1df-4add-af4b-8f8bec9bc0c8": {
"name": "OpenStreetMap.Mapnik Layer",
"parameters": {
"source": "4cc5d853-cb4a-4d05-9560-0c5e7de54fb0"
},
"type": "RasterLayer",
"visible": true
},
"3c9de1bc-8a95-4913-a3a7-c76ea54146e6": {
"name": "Land Use",
"parameters": {
"color": "#1a5fb4",
"opacity": 1.0,
"source": "c09567a9-2c88-4818-ad40-536e005e9496",
"sourceLayer": "landuse",
"type": "fill"
},
"type": "VectorLayer",
"visible": true
}
},
"options": {
"bearing": 0.0,
"latitude": 43.769833024178524,
"longitude": 11.243657520618626,
"pitch": 9.886721549625761,
"zoom": 11.30647314935016
},
"sources": {
"4cc5d853-cb4a-4d05-9560-0c5e7de54fb0": {
"name": "OpenStreetMap.Mapnik",
"parameters": {
"attribution": "(C) OpenStreetMap contributors",
"maxZoom": 19.0,
"minZoom": 0.0,
"provider": "OpenStreetMap",
"url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
"urlParameters": {}
},
"type": "RasterSource"
},
"c09567a9-2c88-4818-ad40-536e005e9496": {
"name": "PMTiles",
"parameters": {
"bounds": [
-180.0,
-85.051129,
180.0,
85.051129
],
"maxZoom": 24.0,
"minZoom": 0.0,
"scheme": "xyz",
"url": "pmtiles://https://pmtiles.io/protomaps(vector)ODbL_firenze.pmtiles"
},
"type": "VectorTileSource"
}
},
"terrain": {
"exaggeration": 0.0,
"source": ""
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "latest"
"@fortawesome/react-fontawesome": "latest",
"pmtiles": "^3.0.7"
}
}
160 changes: 131 additions & 29 deletions packages/base/src/mainview/mainview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import * as React from 'react';

import * as MapLibre from 'maplibre-gl';

import { Protocol } from 'pmtiles';
import { isLightTheme } from '../tools';
import { MainViewModel } from './mainviewmodel';
import { Spinner } from './spinner';
Expand Down Expand Up @@ -145,6 +146,10 @@ export class MainView extends React.Component<IProps, IStates> {
});
});

// PM tile stuff
this._protocol = new Protocol();
MapLibre.addProtocol('pmtiles', this._protocol.tile);

// Workaround for broken intialization of maplibre
this._Map._lazyInitEmptyStyle();

Expand All @@ -170,27 +175,40 @@ export class MainView extends React.Component<IProps, IStates> {
switch (source.type) {
case 'RasterSource': {
const mapSource = this._Map.getSource(id) as MapLibre.RasterTileSource;

if (!mapSource) {
this._Map.addSource(id, {
type: 'raster',
attribution: source.parameters?.attribution || '',
tiles: [this.computeSourceUrl(source)],
tileSize: 256
});
const parameters = source.parameters as IRasterSource;
const sourceSpec = this.configureTileSource(
{
type: 'raster',
minzoom: parameters.minZoom,
maxzoom: parameters.maxZoom,
attribution: parameters.attribution || '',
tileSize: 256
},
this.computeSourceUrl(source)
);

this._Map.addSource(id, sourceSpec);
}
break;
}
case 'VectorTileSource': {
const mapSource = this._Map.getSource(id) as MapLibre.VectorTileSource;

if (!mapSource) {
const parameters = source.parameters as IVectorTileSource;
this._Map.addSource(id, {
type: 'vector',
minzoom: parameters.minZoom,
maxzoom: parameters.maxZoom,
attribution: parameters.attribution || '',
tiles: [this.computeSourceUrl(source)]
});
const sourceSpec = this.configureTileSource(
{
type: 'vector',
minzoom: parameters.minZoom,
maxzoom: parameters.maxZoom,
attribution: parameters.attribution || ''
},
this.computeSourceUrl(source)
);

this._Map.addSource(id, sourceSpec);
}
break;
}
Expand All @@ -211,13 +229,19 @@ export class MainView extends React.Component<IProps, IStates> {
const mapSource = this._Map.getSource(
id
) as MapLibre.RasterDEMTileSource;

if (!mapSource) {
const parameters = source.parameters as IRasterDemSource;
this._Map.addSource(id, {
type: 'raster-dem',
tileSize: parameters.tileSize,
url: parameters.url
});
const sourceSpec = this.configureTileSource(
{
type: 'raster-dem',
tileSize: parameters.tileSize,
encoding: parameters.encoding
},
this.computeSourceUrl(source)
);

this._Map.addSource(id, sourceSpec);
}
break;
}
Expand Down Expand Up @@ -294,15 +318,21 @@ export class MainView extends React.Component<IProps, IStates> {

switch (source.type) {
case 'RasterSource': {
(mapSource as MapLibre.RasterTileSource).setTiles([
this.computeSourceUrl(source)
]);
const sourceCast = mapSource as MapLibre.RasterTileSource;
const url = this.computeSourceUrl(source);
this._handleSourceUpdate(sourceCast, url);
break;
}
case 'VectorTileSource': {
(mapSource as MapLibre.RasterTileSource).setTiles([
this.computeSourceUrl(source)
]);
const sourceCast = mapSource as MapLibre.VectorTileSource;
const url = this.computeSourceUrl(source);
this._handleSourceUpdate(sourceCast, url);
break;
}
case 'RasterDemSource': {
const sourceCast = mapSource as MapLibre.RasterDEMTileSource;
const url = this.computeSourceUrl(source);
this._handleSourceUpdate(sourceCast, url);
break;
}
case 'GeoJSONSource': {
Expand All @@ -312,11 +342,6 @@ export class MainView extends React.Component<IProps, IStates> {
(mapSource as MapLibre.GeoJSONSource).setData(data);
break;
}
case 'RasterDemSource': {
const parameters = source.parameters as IRasterDemSource;
(mapSource as MapLibre.RasterDEMTileSource).setTiles([parameters.url]);
break;
}
case 'ImageSource': {
const parameters = source.parameters as IImageSource;
(mapSource as MapLibre.ImageSource).updateImage({
Expand Down Expand Up @@ -445,6 +470,7 @@ export class MainView extends React.Component<IProps, IStates> {
if (index < currentLayerIds.length && index !== -1) {
beforeId = currentLayerIds[index];
}

switch (layer.type) {
case 'RasterLayer': {
this._Map.addLayer(
Expand Down Expand Up @@ -665,6 +691,81 @@ export class MainView extends React.Component<IProps, IStates> {
}
}

/**
* Determines whether to use tiles or a URL for a given source based on the presence of replaceable parameters in the URL.
*
* This method checks if the provided URL contains patterns that indicate it can be broken down into tiles
* (e.g., {z}/{x}/{y}, {ratio}, etc.).
* If such patterns are found, it suggests that tiles should be used. Otherwise, the URL itself should be used.
*
* @param url - The URL to check for replaceable parameters.
* @returns True if the URL contains replaceable parameters indicating the use of tiles, false otherwise.
*/
private _shouldUseTiles(url: string) {
const regexPatterns = [
/\{z\}/,
/\{x\}/,
/\{y\}/,
/\{ratio\}/,
/\{quadkey\}/,
/\{bbox-epsg-3857\}/
];

let result = false;

for (const pattern of regexPatterns) {
if (pattern.test(url)) {
result = true;
break;
}
}

return result;
}

/**
* Updates the given source with either a new URL or tiles based on the result of `_shouldUseTiles`.
*
* @param source - The source to update.
* @param url - The URL to set for the source.
*/
private _handleSourceUpdate(
source:
| MapLibre.RasterTileSource
| MapLibre.VectorTileSource
| MapLibre.RasterDEMTileSource,
url: string
) {
const result = this._shouldUseTiles(url);

result ? source.setTiles([url]) : source.setUrl(url);
}

/**
* Configures a source specification, setting either the `tiles` or `url` property based on the provided URL.
*
* This method uses the `_shouldUseTiles` method to determine whether the source should be configured with tiles or a direct URL. It then modifies the `sourceSpec` object accordingly.
*
* @param sourceSpec - The source specification object to configure. This object is modified in place.
* @param url - The URL to check for replaceable parameters and use in configuring the source.
* @returns The modified source specification object.
*/
private configureTileSource(
sourceSpec:
| MapLibre.RasterSourceSpecification
| MapLibre.RasterDEMSourceSpecification
| MapLibre.VectorSourceSpecification,
url: string
) {
const result = this._shouldUseTiles(url);

result
? (sourceSpec = { tiles: [url], ...sourceSpec })
: (sourceSpec = { url, ...sourceSpec });

return sourceSpec;
}

private _onClientSharedStateChanged = (
sender: IJupyterGISModel,
clients: Map<number, IJupyterGISClientState>
Expand Down Expand Up @@ -825,4 +926,5 @@ export class MainView extends React.Component<IProps, IStates> {
private _ready = false;
private _terrainControl: MapLibre.TerrainControl | null;
private _videoPlaying = false;
private _protocol: Protocol;
}
Loading