Skip to content

Commit

Permalink
TileLayer and MVTLayer API audit
Browse files Browse the repository at this point in the history
  • Loading branch information
Xiaoji Chen committed Mar 10, 2020
1 parent 4f70efb commit 5e817e2
Show file tree
Hide file tree
Showing 16 changed files with 305 additions and 180 deletions.
39 changes: 24 additions & 15 deletions docs/layers/mvt-layer.md
Expand Up @@ -9,7 +9,7 @@

`MVTLayer` is a derived `TileLayer` that makes it possible to visualize very large datasets through MVTs ([Mapbox Vector Tiles](https://docs.mapbox.com/vector-tiles/specification/)). Behaving like `TileLayer`, it will only load, decode and render MVTs containing features that are visible within the current viewport.

MVTs are loaded from a network server, so you need to specify your own tile server URLs in `urlTemplates` property. You can provide as many templates as you want for a unique set of tiles. All URL templates will be balanced by [this algorithm](https://github.com/uber/deck.gl/blob/58f8b848f3ccf1676e90c7810e1b6d115a9d53d0/modules/geo-layers/src/mvt-layer/mvt-layer.js#L51-L53), ensuring that tiles will be always requested to the same server depending on their index.
Data is loaded from URL templates in the `data` property.

This layer also handles feature clipping so that there are no features divided by tile divisions.

Expand All @@ -19,11 +19,24 @@ import {MVTLayer} from '@deck.gl/geo-layers';

export const App = ({viewport}) => {
const layer = new MVTLayer({
urlTemplates: [
`https://a.tiles.mapbox.com/v4/mapbox.boundaries-adm0-v3/{z}/{x}/{y}.vector.pbf?access_token=${MapboxAccessToken}`
],

getFillColor: [140, 170, 180]
data: `https://a.tiles.mapbox.com/v4/mapbox.mapbox-streets-v7/{z}/{x}/{y}.vector.pbf?access_token=${MAPBOX_TOKEN}`,

minZoom: 0,
maxZoom: 23,
getLineColor: [192, 192, 192],
getFillColor: [140, 170, 180],

getLineWidth: f => {
switch (f.properties.class) {
case 'street':
return 6;
case 'motorway':
return 10;
default:
return 1;
}
},
lineWidthMinPixels: 1
});

return <DeckGL {...viewport} layers={[layer]} />;
Expand Down Expand Up @@ -63,21 +76,17 @@ new deck.MVTLayer({});

## Properties

Inherits all properties from [`TileLayer`](/docs/layers/tile-layer.md) and [base `Layer`](/docs/api-reference/layer.md), and you can use [`GeoJSONLayer`](/docs/layers/geojson-layer.md) properties to style features.

It also adds a custom property:
Inherits all properties from [`TileLayer`](/docs/layers/tile-layer.md) and [base `Layer`](/docs/api-reference/layer.md), with exceptions indicated below.

##### `urlTemplates` (Array)
If using the default `renderSubLayers`, supports all [`GeoJSONLayer`](/docs/layers/geojson-layer.md) properties to style features.

- Default `[]`

URL templates to load tiles from network. Templates don't require a specific format. The layer will replace `{x}`, `{y}`, `{z}` ocurrences to the proper tile index before requesting it to the server.
##### `data` (String|Array)

When specifiying more than one URL template, all templates will be balanced by [this algorithm](https://github.com/uber/deck.gl/blob/58f8b848f3ccf1676e90c7810e1b6d115a9d53d0/modules/geo-layers/src/mvt-layer/mvt-layer.js#L51-L53), ensuring that tiles will be always requested to the same server depending on their index.
Required. Either a URL template or an array of URL templates from which the MVT data should be loaded. See `TileLayer`'s `data` prop documentation for the templating syntax.

### Callbacks
The `getTileData` prop from the `TileLayer` class will not be called.

Inherits all callbacks from [`TileLayer`](/docs/layers/tile-layer.md) and [base `Layer`](/docs/api-reference/layer.md).

## Source

Expand Down
98 changes: 50 additions & 48 deletions docs/layers/tile-layer.md
Expand Up @@ -9,54 +9,34 @@

The `TileLayer` is a composite layer that makes it possible to visualize very large datasets. Instead of fetching the entire dataset, it only loads and renders what's visible in the current viewport.

To use this layer, the data must be sliced into "tiles". Each tile has a pre-defined bounding box and level of detail. The layer takes in a function `getTileData` that fetches tiles when they are needed, and renders the loaded data in a GeoJsonLayer or with the layer returned in `renderSubLayers`.
To use this layer, the data must be sliced into "tiles". Each tile has a pre-defined bounding box and level of detail.
Users have the option to load each tile from a unique URL, defined by a template in the `data` property.
The layer can also supply a callback `getTileData` that does custom fetching when a tile is requested.
The loaded tile data is then rendered with the layer(s) returned by `renderSubLayers`.

```js
import DeckGL from '@deck.gl/react';
import {TileLayer} from '@deck.gl/geo-layers';
import {VectorTile} from '@mapbox/vector-tile';
import Protobuf from 'pbf';

export const App = ({viewport}) => {

const layer = new TileLayer({
stroked: false,

getLineColor: [192, 192, 192],
getFillColor: [140, 170, 180],

getLineWidth: f => {
if (f.properties.layer === 'transportation') {
switch (f.properties.class) {
case 'primary':
return 12;
case 'motorway':
return 16;
default:
return 6;
}
}
return 1;
},
lineWidthMinPixels: 1,

getTileData: ({x, y, z}) => {
const mapSource = `https://a.tiles.mapbox.com/v4/mapbox.mapbox-streets-v7/${z}/${x}/${y}.vector.pbf?access_token=${MapboxAccessToken}`;
return fetch(mapSource)
.then(response => response.arrayBuffer())
.then(buffer => {
const tile = new VectorTile(new Protobuf(buffer));
const features = [];
for (const layerName in tile.layers) {
const vectorTileLayer = tile.layers[layerName];
for (let i = 0; i < vectorTileLayer.length; i++) {
const vectorTileFeature = vectorTileLayer.feature(i);
const feature = vectorTileFeature.toGeoJSON(x, y, z);
features.push(feature);
}
}
return features;
});
// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
data: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png',

minZoom: 0,
maxZoom: 19,

renderSubLayers: props => {
const {
bbox: {west, south, east, north}
} = props.tile;

return new BitmapLayer(props, {
data: null,
image: props.data,
bounds: [west, south, east, north]
});
}
});
return <DeckGL {...viewport} layers={[layer]} />;
Expand Down Expand Up @@ -93,27 +73,49 @@ To use pre-bundled scripts:
new deck.TileLayer({});
```

## Indexing System

At each integer zoom level (`z`), the XY plane in the view space is divided into square tiles of the same size, each uniquely identified by their `x` and `y` index. When `z` increases by 1, the view space is scaled by 2, meaning that one tile at `z` covers the same area as four tiles at `z+1`.

When the `TileLayer` is used with a geospatial view such as the [MapView](/docs/api-reference/map-view.md), x, y, and z are determined from [the OSM tile index](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames).

When the `TileLayer` is used used with a non-geospatial view such as the [OrthographicView](/docs/api-reference/orthographic-view.md) or the [OrbitView](/docs/api-reference/orbit-view.md), `x` and `y` increment from the world origin, and each tile's width and height match that defined by the `tileSize` prop. For example, the tile `x: 0, y: 0` occupies the square between `[0, 0]` and `[tileSize, tileSize]`.


## Properties

Inherits all properties from [base `Layer`](/docs/api-reference/layer.md).

If using the default `renderSubLayers`, supports all [`GeoJSONLayer`](/docs/layers/geojson-layer.md) properties to style features.

### Data Options

##### `getTileData` (Function)
##### `data` (String|Array, optional)

`getTileData` given x, y, z indices of the tile, returns the tile data or a Promise that resolves to the tile data.
- Default: `[]`

- Default: `tile => Promise.resolve(null)`
Either a URL template or an array of URL templates from which the tile data should be loaded.

The `tile` argument contains the following fields:
If the value is a string: a URL template. Substrings `{x}` `{y}` and `{z}`, if present, will be replaced with a tile's actual index when it is requested.

If the value is an array: multiple URL templates. Each endpoint must return the same content for the same tile index. This can be used to work around [domain sharding](https://developer.mozilla.org/en-US/docs/Glossary/Domain_sharding), allowing browsers to download more resources simultaneously. Requests made are balanced among the endpoints, based on the tile index.


##### `getTileData` (Function, optional)

- Default: `tile => load(tile.url)`

If supplied, `getTileData` is called to retrieve the data of each tile. It receives one argument `tile` which contains the following fields:

- `x` (Number) - x index of the tile
- `y` (Number) - y index of the tile
- `z` (Number) - z index of the tile
- `bbox` (Object) - bounding box of the tile

When used with a geospatial view such as the [MapView](/docs/api-reference/map-view.md), x, y, and z are determined from [the OSM tile index](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames), and `bbox` is in the shape of `{west: <longitude>, north: <latitude>, east: <longitude>, south: <latitude>}`.
- `url` (String) - resolved url of the tile if the `data` prop is provided, otherwise `null`
- `bbox` (Object) - bounding box of the tile. When used with a geospatial view, `bbox` is in the shape of `{west: <longitude>, north: <latitude>, east: <longitude>, south: <latitude>}`. When used used with a non-geospatial view, `bbox` is in the shape of `{left, top, right, bottom}`.

It should return either the tile data or a Promise that resolves to the tile data.

When used with a non-geospatial view such as the [OrthographicView](/docs/api-reference/orthographic-view.md) or the [OrbitView](/docs/api-reference/orbit-view.md), x, y, and z are determined by dividing the view space in to square tiles (see `tileSize` prop). `bbox` is in the shape of `{left, top, right, bottom}`.
This prop is not required if `data` points to a supported format (JSON or image by default). Additional formats may be added by registering [loaders.gl](https://loaders.gl/modules/core/docs/api-reference/register-loaders) modules.


##### `tileSize` (Number, optional)
Expand Down
7 changes: 2 additions & 5 deletions examples/website/map-tile/app.js
Expand Up @@ -4,7 +4,6 @@ import {render} from 'react-dom';
import DeckGL from '@deck.gl/react';
import {TileLayer} from '@deck.gl/geo-layers';
import {BitmapLayer} from '@deck.gl/layers';
import {load} from '@loaders.gl/core';

const INITIAL_VIEW_STATE = {
latitude: 47.65,
Expand All @@ -14,9 +13,6 @@ const INITIAL_VIEW_STATE = {
bearing: 0
};

// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
const tileServer = 'https://c.tile.openstreetmap.org/';

export default class App extends PureComponent {
constructor(props) {
super(props);
Expand Down Expand Up @@ -55,7 +51,8 @@ export default class App extends PureComponent {
minZoom: 0,
maxZoom: 19,

getTileData: ({x, y, z}) => load(`${tileServer}/${z}/${x}/${y}.png`),
// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
url: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png',

renderSubLayers: props => {
const {
Expand Down
58 changes: 20 additions & 38 deletions modules/geo-layers/src/mvt-layer/mvt-layer.js
Expand Up @@ -2,58 +2,40 @@ import {Matrix4} from 'math.gl';
import {MVTLoader} from '@loaders.gl/mvt';
import {load} from '@loaders.gl/core';
import {COORDINATE_SYSTEM} from '@deck.gl/core';
import {GeoJsonLayer} from '@deck.gl/layers';

import TileLayer from '../tile-layer/tile-layer';
import {getTileURL} from '../tile-layer/utils';
import ClipExtension from './clip-extension';

const defaultProps = Object.assign({}, TileLayer.defaultProps, {
renderSubLayers: {type: 'function', value: renderSubLayers, compare: false},
urlTemplates: {type: 'array', value: [], compare: true}
});
const WORLD_SIZE = 512;

export default class MVTLayer extends TileLayer {
async getTileData(tileProperties) {
const {urlTemplates} = this.getCurrentLayer().props;

if (!urlTemplates || !urlTemplates.length) {
return Promise.reject('Invalid urlTemplates');
getTileData(tile) {
const url = getTileURL(this.props.data, tile);
if (!url) {
return Promise.reject('Invalid URL');
}

const templateReplacer = (_, property) => tileProperties[property];
const tileURLIndex = getTileURLIndex(tileProperties, urlTemplates.length);
const tileURL = urlTemplates[tileURLIndex].replace(/\{ *([\w_-]+) *\}/g, templateReplacer);

return await load(tileURL, MVTLoader);
return load(url, MVTLoader);
}
}

function renderSubLayers({data, tile, extensions = [], ...otherProperties}) {
return new GeoJsonLayer({
...otherProperties,
data,
modelMatrix: getModelMatrix(tile),
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
extensions: [...extensions, new ClipExtension()]
});
}
renderSubLayers(props) {
const {tile} = props;
const worldScale = Math.pow(2, tile.z);

function getModelMatrix(tile) {
const WORLD_SIZE = 512;
const worldScale = Math.pow(2, tile.z);
const xScale = WORLD_SIZE / worldScale;
const yScale = -xScale;

const xScale = WORLD_SIZE / worldScale;
const yScale = -xScale;
const xOffset = (WORLD_SIZE * tile.x) / worldScale;
const yOffset = WORLD_SIZE * (1 - tile.y / worldScale);

const xOffset = (WORLD_SIZE * tile.x) / worldScale;
const yOffset = WORLD_SIZE * (1 - tile.y / worldScale);
const modelMatrix = new Matrix4().translate([xOffset, yOffset, 0]).scale([xScale, yScale, 1]);

return new Matrix4().translate([xOffset, yOffset, 0]).scale([xScale, yScale, 1]);
}
props.modelMatrix = modelMatrix;
props.coordinateSystem = COORDINATE_SYSTEM.CARTESIAN;
props.extensions = [...(props.extensions || []), new ClipExtension()];

function getTileURLIndex({x, y}, templatesLength) {
return Math.abs(x + y) % templatesLength;
return super.renderSubLayers(props);
}
}

MVTLayer.layerName = 'MVTLayer';
MVTLayer.defaultProps = defaultProps;
30 changes: 25 additions & 5 deletions modules/geo-layers/src/tile-layer/tile-layer.js
Expand Up @@ -2,10 +2,13 @@ import {CompositeLayer, _flatten as flatten} from '@deck.gl/core';
import {GeoJsonLayer} from '@deck.gl/layers';

import Tileset2D, {STRATEGY_DEFAULT} from './tileset-2d';
import {urlType, getTileURL} from './utils';

const defaultProps = {
data: [],
dataComparator: urlType.equals,
renderSubLayers: {type: 'function', value: props => new GeoJsonLayer(props), compare: false},
getTileData: {type: 'function', value: ({x, y, z}) => null, compare: false},
getTileData: {type: 'function', optional: true, value: null, compare: false},
// TODO - change to onViewportLoad to align with Tile3DLayer
onViewportLoad: {type: 'function', optional: true, value: null, compare: false},
onTileLoad: {type: 'function', value: tile => {}, compare: false},
Expand Down Expand Up @@ -42,6 +45,7 @@ export default class TileLayer extends CompositeLayer {
let {tileset} = this.state;
const createTileCache =
!tileset ||
changeFlags.dataChanged ||
(changeFlags.updateTriggersChanged &&
(changeFlags.updateTriggersChanged.all || changeFlags.updateTriggersChanged.getTileData));

Expand Down Expand Up @@ -113,8 +117,24 @@ export default class TileLayer extends CompositeLayer {
layer._updateTileset();
}

getTileData(tilePosition) {
return this.props.getTileData(tilePosition);
// Methods for subclass to override

getTileData(tile) {
const {getTileData, fetch, data} = this.props;

tile.url = getTileURL(data, tile);

if (getTileData) {
return getTileData(tile);
}
if (tile.url) {
return fetch(tile.url, {layer: this});
}
return null;
}

renderSubLayers(props) {
return this.props.renderSubLayers(props);
}

getPickingInfo({info, sourceLayer}) {
Expand All @@ -124,15 +144,15 @@ export default class TileLayer extends CompositeLayer {
}

renderLayers() {
const {renderSubLayers, visible} = this.props;
const {visible} = this.props;
return this.state.tileset.tiles.map(tile => {
// For a tile to be visible:
// - parent layer must be visible
// - tile must be visible in the current viewport
const isVisible = visible && tile.isVisible;
// cache the rendered layer in the tile
if (!tile.layers) {
const layers = renderSubLayers(
const layers = this.renderSubLayers(
Object.assign({}, this.props, {
id: `${this.id}-${tile.x}-${tile.y}-${tile.z}`,
data: tile.data,
Expand Down

0 comments on commit 5e817e2

Please sign in to comment.