Skip to content

Commit e106c19

Browse files
authored
[feat] vector tile layer (#2839)
- This PR adds Vector Tile layer to Kepler with support initial support of mapbox vector tiles and remote .pmtiles (with mimeType application/vnd.mapbox-vector-tile)
1 parent a2abbf7 commit e106c19

File tree

66 files changed

+4317
-209
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+4317
-209
lines changed

docs/user-guides/c-types-of-layers/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,13 @@ To use H3 layer, you need a `hex_id` in your dataset, which can be generated usi
9292

9393
To use S2 layer, you need to assign a column containing S2 tokens.
9494

95+
## Vector Tile Layer
96+
97+
Vector Tile Layer makes it possible to visualize very large datasets through MVTs (Mapbox Vector Tiles). To optimize performance, the layer only loads and renders tiles containing features that are visible within the current viewport.
98+
99+
Supported URL templates:
100+
101+
- MVT (https://api.mapbox.com/v4/mapbox.mapbox-streets-v8/{z}/{x}/{y}.mvt?access_token=your-mapbox-acceess-token)
102+
- pmtiles (https://your-cdn/filename.pmtiles)
95103

96104
[Back to table of contents](../README.md)

examples/demo-app/src/actions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {push} from 'react-router-redux';
55
import {fetch} from 'global';
66

77
import {loadFiles, toggleModal} from '@kepler.gl/actions';
8+
import {parseUri} from '@kepler.gl/common-utils';
89
import {load} from '@loaders.gl/core';
910
import {CSVLoader} from '@loaders.gl/csv';
1011
import {GeoArrowLoader} from '@loaders.gl/arrow';
@@ -16,7 +17,6 @@ import {
1617
LOADING_SAMPLE_LIST_ERROR_MESSAGE,
1718
MAP_CONFIG_URL
1819
} from './constants/default-settings';
19-
import {parseUri} from './utils/url';
2020

2121
// CONSTANTS
2222
export const INIT = 'INIT';

examples/demo-app/src/app.tsx

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,76 @@ const App = props => {
194194
);
195195
}, [dispatch]);
196196

197+
const _loadVectorTileData = useCallback(() => {
198+
dispatch(
199+
addDataToMap({
200+
datasets: [
201+
{
202+
info: {
203+
label: 'Railroads',
204+
id: 'railroads.pmtiles',
205+
color: [255, 0, 0],
206+
type: 'vectorTile'
207+
},
208+
data: {
209+
rows: [],
210+
fields: [
211+
{
212+
name: 'continent',
213+
type: 'string',
214+
format: '',
215+
analyzerType: 'STRING'
216+
}
217+
]
218+
},
219+
metadata: {
220+
name: 'output.pmtiles',
221+
description: 'output.pmtiles',
222+
type: 'pmtiles',
223+
tilesetDataUrl:
224+
'https://4sq-studio-public.s3.us-west-2.amazonaws.com/pmtiles-test/161727fe-7952-4e57-aa05-850b3086b0b2.pmtiles',
225+
tilesetMetadataUrl:
226+
'https://4sq-studio-public.s3.us-west-2.amazonaws.com/pmtiles-test/161727fe-7952-4e57-aa05-850b3086b0b2.pmtiles',
227+
id: 'sz6uy1xtj',
228+
format: 'rows',
229+
label: 'output.pmtiles',
230+
metaJson: null,
231+
bounds: [-150.1122219, -51.8952777, 179.3577783, 69.6043747],
232+
center: [14.0625, 50.7026397, 6],
233+
maxZoom: 6,
234+
minZoom: 0,
235+
fields: [
236+
{
237+
name: 'continent',
238+
id: 'continent',
239+
format: '',
240+
filterProps: {
241+
domain: [
242+
'Africa',
243+
'Asia',
244+
'Europe',
245+
'North America',
246+
'Oceania',
247+
'South America'
248+
],
249+
value: [],
250+
type: 'multiSelect',
251+
gpu: false
252+
},
253+
type: 'string',
254+
analyzerType: 'STRING'
255+
}
256+
]
257+
}
258+
}
259+
],
260+
options: {
261+
autoCreateLayers: true
262+
}
263+
})
264+
);
265+
}, [dispatch]);
266+
197267
const _loadPointData = useCallback(() => {
198268
dispatch(
199269
addDataToMap({
@@ -427,6 +497,7 @@ const App = props => {
427497
// _loadScenegraphLayer();
428498
// _loadGpsData();
429499
// _loadRowData();
500+
// _loadVectorTileData();
430501
// eslint-disable-next-line react-hooks/exhaustive-deps
431502
}, [
432503
_loadPointData,
@@ -438,7 +509,8 @@ const App = props => {
438509
_loadScenegraphLayer,
439510
_loadGpsData,
440511
_loadRowData,
441-
_replaceData
512+
_replaceData,
513+
_loadVectorTileData
442514
]);
443515

444516
return (

examples/demo-app/src/factories/load-data-modal.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const CustomLoadDataModalFactory = (...deps) => {
2929
// add more loading methods
3030
const loadingMethods = [
3131
defaultLoadingMethods.find(lm => lm.id === 'upload'),
32+
defaultLoadingMethods.find(lm => lm.id === 'tileset'),
3233
additionalMethods.remote,
3334
defaultLoadingMethods.find(lm => lm.id === 'storage'),
3435
additionalMethods.sample

examples/demo-app/src/utils/url.js

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,6 @@
11
// SPDX-License-Identifier: MIT
22
// Copyright contributors to the kepler.gl project
33

4-
// from http://blog.stevenlevithan.com/archives/parseuri
5-
/**
6-
* Allows to break down a url into multiple params
7-
* @param str
8-
*/
9-
export function parseUri(str) {
10-
const o = parseUri.options;
11-
const m = o.parser[o.strictMode ? 'strict' : 'loose'].exec(str);
12-
const uri = {};
13-
let i = 14;
14-
15-
while (i--) uri[o.key[i]] = m[i] || '';
16-
17-
uri[o.q.name] = {};
18-
uri[o.key[12]].replace(o.q.parser, ($0, $1, $2) => {
19-
if ($1) uri[o.q.name][$1] = $2;
20-
});
21-
22-
return uri;
23-
}
24-
25-
parseUri.options = {
26-
strictMode: false,
27-
key: [
28-
'source',
29-
'protocol',
30-
'authority',
31-
'userInfo',
32-
'user',
33-
'password',
34-
'host',
35-
'port',
36-
'relative',
37-
'path',
38-
'directory',
39-
'file',
40-
'query',
41-
'anchor'
42-
],
43-
q: {
44-
name: 'queryKey',
45-
parser: /(?:^|&)([^&=]*)=?([^&]*)/g
46-
},
47-
parser: {
48-
strict:
49-
/^(?:([^:/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:/?#]*)(?::(\d*))?))?((((?:[^?#/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
50-
loose:
51-
/^(?:(?![^:@]+:[^:@/]*@)([^:/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#/]*\.[^?#/.]+(?:[?#]|$)))*\/?)?([^?#/]*))(?:\?([^#]*))?(?:#(.*))?)/
52-
}
53-
};
54-
554
/**
565
* Validates an url
576
* @param str

src/common-utils/src/data.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// SPDX-License-Identifier: MIT
22
// Copyright contributors to the kepler.gl project
33

4+
import {Analyzer, DATA_TYPES} from 'type-analyzer';
5+
6+
import {Field} from '@kepler.gl/types';
7+
48
export function notNullorUndefined<T extends NonNullable<any>>(d: T | null | undefined): d is T {
59
return d !== undefined && d !== null;
610
}
@@ -45,3 +49,23 @@ export function toArray<T>(item: T | T[]): T[] {
4549

4650
return [item];
4751
}
52+
53+
/**
54+
* Check whether geojson linestring's 4th coordinate is 1) not timestamp 2) unix time stamp 3) real date time
55+
* @param timestamps array to be tested if its elements are timestamp
56+
* @returns the type of timestamp: unix/datetime/invalid(not timestamp)
57+
*/
58+
export function containValidTime(timestamps: string[]): Field | null {
59+
const formattedTimeStamps = timestamps.map(ts => ({ts}));
60+
const ignoredDataTypes = Object.keys(DATA_TYPES).filter(
61+
type => ![DATA_TYPES.TIME, DATA_TYPES.DATETIME, DATA_TYPES.DATE].includes(type)
62+
);
63+
64+
// ignore all types but TIME to improve performance
65+
const analyzedType = Analyzer.computeColMeta(formattedTimeStamps, [], {ignoredDataTypes})[0];
66+
67+
if (!analyzedType || analyzedType.category !== 'TIME') {
68+
return null;
69+
}
70+
return analyzedType;
71+
}

src/common-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
export * from './data';
55
export * from './data-type';
66
export * from './string';
7+
export * from './url';
78

89
export {getCentroid, getHexFields, h3IsValid, idToPolygonGeo} from './h3-utils';
910
export type {Centroid} from './h3-utils';

src/common-utils/src/url.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright contributors to the kepler.gl project
3+
4+
/**
5+
* Allows to break down a url into multiple params
6+
* from http://blog.stevenlevithan.com/archives/parseuri
7+
*/
8+
export function parseUri(str: string): {[key: string]: any} {
9+
const o = parseUri.options;
10+
const m = o.parser[o.strictMode ? 'strict' : 'loose'].exec(str);
11+
const uri = {};
12+
let i = 14;
13+
14+
while (i--) uri[o.key[i]] = m?.[i] || '';
15+
16+
uri[o.q.name] = {};
17+
uri[o.key[12]].replace(o.q.parser, ($0, $1, $2) => {
18+
if ($1) uri[o.q.name][$1] = $2;
19+
});
20+
21+
return uri;
22+
}
23+
24+
parseUri.options = {
25+
strictMode: false,
26+
key: [
27+
'source',
28+
'protocol',
29+
'authority',
30+
'userInfo',
31+
'user',
32+
'password',
33+
'host',
34+
'port',
35+
'relative',
36+
'path',
37+
'directory',
38+
'file',
39+
'query',
40+
'anchor'
41+
],
42+
q: {
43+
name: 'queryKey',
44+
parser: /(?:^|&)([^&=]*)=?([^&]*)/g
45+
},
46+
parser: {
47+
strict:
48+
// eslint-disable-next-line no-useless-escape
49+
/^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
50+
loose:
51+
// eslint-disable-next-line no-useless-escape
52+
/^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
53+
}
54+
};
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright contributors to the kepler.gl project
3+
4+
import {useCallback, useEffect, useState} from 'react';
5+
6+
import {MVTSource, TileJSON} from '@loaders.gl/mvt';
7+
import {PMTilesSource, PMTilesMetadata} from '@loaders.gl/pmtiles';
8+
9+
import {VectorTileMetadata, VectorTileType} from '@kepler.gl/layers';
10+
11+
type FetchVectorTileMetadataProps = {
12+
url: string | null;
13+
type: VectorTileType;
14+
process?: (json: PMTilesMetadata | TileJSON) => VectorTileMetadata | Error | null;
15+
};
16+
17+
const DEFAULT_PROCESS_FUNCTION = (json: PMTilesMetadata | TileJSON): VectorTileMetadata => {
18+
return {
19+
metaJson: null,
20+
bounds: null,
21+
center: null,
22+
maxZoom: null,
23+
minZoom: null,
24+
fields: [],
25+
...json
26+
};
27+
};
28+
29+
type FetchVectorTileMetadataReturn = {
30+
data: VectorTileMetadata | null;
31+
loading: boolean;
32+
error: Error | null;
33+
};
34+
35+
/** Hook to fetch and return mvt or pmtiles metadata. */
36+
export default function useFetchVectorTileMetadata({
37+
type,
38+
url,
39+
process = DEFAULT_PROCESS_FUNCTION
40+
}: FetchVectorTileMetadataProps): FetchVectorTileMetadataReturn {
41+
const [error, setError] = useState<Error | null>(null);
42+
const [data, setData] = useState<VectorTileMetadata | null>(null);
43+
const [loading, setLoading] = useState<boolean>(false);
44+
45+
const setProcessedData = useCallback(
46+
(value: PMTilesMetadata | TileJSON | null) => {
47+
if (!value) {
48+
return setError(value);
49+
}
50+
const processedData = process(value);
51+
if (processedData instanceof Error) {
52+
setError(processedData);
53+
} else {
54+
setData(processedData);
55+
}
56+
},
57+
[setError, setData, process]
58+
);
59+
60+
useEffect(() => {
61+
const getAndProcessMetadata = async () => {
62+
setError(null);
63+
setData(null);
64+
if (url) {
65+
setLoading(true);
66+
67+
try {
68+
const tileSource =
69+
type === VectorTileType.MVT
70+
? MVTSource.createDataSource('', {
71+
mvt: {
72+
metadataUrl: decodeURIComponent(url)
73+
}
74+
})
75+
: PMTilesSource.createDataSource(decodeURIComponent(url), {});
76+
77+
const metadata = await tileSource.metadata;
78+
setProcessedData(metadata);
79+
} catch (metadataError) {
80+
setError(metadataError as any);
81+
}
82+
setLoading(false);
83+
}
84+
};
85+
86+
getAndProcessMetadata();
87+
}, [url, type, setProcessedData]);
88+
89+
return {data, loading, error};
90+
}

src/components/src/hooks/use-on-click-outside.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// SPDX-License-Identifier: MIT
22
// Copyright contributors to the kepler.gl project
33

4-
// Copyright 2022 Foursquare Labs, Inc. All Rights Reserved.
5-
64
import document from 'global/document';
75
import {useCallback, useEffect, useRef, MutableRefObject} from 'react';
86
export default function useOnClickOutside<T extends HTMLElement>(

0 commit comments

Comments
 (0)