Skip to content

Commit

Permalink
TileLayer API audit
Browse files Browse the repository at this point in the history
  • Loading branch information
Xiaoji Chen committed Feb 6, 2020
1 parent efe8c59 commit b752b25
Show file tree
Hide file tree
Showing 12 changed files with 529 additions and 328 deletions.
194 changes: 163 additions & 31 deletions docs/layers/tile-layer.md
Expand Up @@ -7,7 +7,9 @@

# TileLayer

This TileLayer takes in a function `getTileData` that fetches tiles, and renders it in a GeoJsonLayer or with the layer returned in `renderSubLayers`.
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`.

```js
import DeckGL from '@deck.gl/react';
Expand Down Expand Up @@ -104,12 +106,12 @@ new deck.TileLayer({});

The `tile` argument 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, see `tileToBoundingBox`.
- `x` (Number) - x of [the OSM tile index](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames)
- `y` (Number) - y of [the OSM tile index](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames)
- `z` (Number) - z of [the OSM tile index](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames)
- `bbox` (Object) - bounding box of the tile, in the shape of `{west: <longitude>, north: <latitude>, east: <longitude>, south: <latitude>}`.

By default, the `TileLayer` loads tiles defined by [the OSM tile index](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames). You may override this by implementing `getTileIndices`.
You may also use a different indexing system by implementing your own tiling logic. See the `Tileset2D` documentation below.


##### `maxZoom` (Number|Null, optional)
Expand All @@ -126,39 +128,163 @@ Hide tiles when under-zoomed.
- Default: 0


##### `maxCacheSize` (Number|Null, optional)
##### `maxCacheSize` (Number, optional)

The maximum number of tiles that can be cached. The tile cache keeps loaded tiles in memory even if they are no longer visible. It reduces the need to re-download the same data over and over again when the user pan/zooms around the map, providing a smoother experience.

The maximum cache size for a tile layer. If not defined, it is calculated using the number of tiles in the current viewport times multiplied by `5`.
If not supplied, the `maxCacheSize` is calculated as `5` times the number of tiles in the current viewport.

- Default: `null`


##### `strategy` (Enum, optional)
##### `maxCacheByteSize` (Number, optional)

The maximum memory used for caching tiles. If this limit is supplied, `getTileData` must return an object that contains a `byteLength` field.

- Default: `null`


How the tile layer determines the visibility of tiles. One of the following:
##### `refinementStrategy` (Enum, optional)

How the tile layer refines the visibility of tiles. One of the following:

* `'best-available'`: If a tile in the current viewport is waiting for its data to load, use cached content from the closest zoom level to fill the empty space. This approach minimizes the visual flashing due to missing content.
* `'no-overlap'`: Avoid showing overlapping tiles when backfilling with cached content. This is usually favorable when tiles do not have opaque backgrounds.
* `'never'`: Do not display any tile that is not selected.

- Default: `'best-available'`


##### `tileToBoundingBox` (Function, optional)
##### `Tileset` (Function, optional)

**Advanced** Converts from `x, y, z` tile indices to a bounding box in the global coordinates. The default implementation converts an OSM tile index to `{west: <longitude>, north: <latitude>, east: <longitude>, south: <latitude>}`.
A custom implemetation of the `Tileset2D` interface. This can be used to override some of the default behaviors of the `TileLayer`. See the `Tileset2D` documentation below.


### Render Options

##### `renderSubLayers` (Function, optional))

Renders one or an array of Layer instances with all the `TileLayer` props and the following props:

* `id`: An unique id for this sublayer
* `data`: Resolved from `getTileData`
* `tile`: An object containing tile index `x`, `y`, `z`, and `bbox` of the tile.

- Default: `props => new GeoJsonLayer(props)`

Receives arguments:

### Callbacks

##### `onViewportLoad` (Function, optional)

`onViewportLoad` is a function that is called when all tiles in the current viewport are loaded. The loaded content (as returned by `getTileData`) for each visible tile is passed as an array to this callback function.

- Default: `data => null`


##### `onTileError` (Function, optional)

`onTileError` called when a tile failed to load.

- Default: `console.error`


## Tileset2D

> This section describes advanced customization of the TileLayer.
`Tileset2D` is a class that manages the request and traversal of tiles. Based on the current viewport and existing cached tiles, it determines which tiles need to be rendered, whether to fetch new data, and grooms the cache if becomes too big.

```js
import {TileLayer, _Tileset2D as Tileset2D} from '@deck.gl/geo-layers';

class NonGeospatialTileset extends Tileset2D {
// viewport is an OrthographicViewport
getTileIndicesInViewport({viewport}) {
const z = Math.ceil(viewport);
const TILE_SIZE = 256;
const scale = Math.pow(2, z);
const topLeft = viewport.unproject([0, 0]);
const bottomRight = viewport.unproject([viewport.width, viewport.height]);

const minX = Math.floor(topLeft[0] * scale / TILE_SIZE);
const minY = Math.floor(topLeft[1] * scale / TILE_SIZE);
const maxX = Math.ceil(bottomRight[0] * scale / TILE_SIZE);
const maxY = Math.ceil(bottomRight[1] * scale / TILE_SIZE);

const result = [];
for (let x = minX; x < maxX; x++) {
for (let y = minY; y < maxY; y++) {
indices.push({x, y, z});
}
}
return result;
}

// Add custom metadata to tiles
getTileMetadata({x, y, z}) {
const TILE_SIZE = 256;
const scale = Math.pow(2, z);
return {
id: `${x}_${y}_${z}`,
bbox: [
x * TILE_SIZE / scale,
y * TILE_SIZE / scale,
(x + 1) * TILE_SIZE / scale,
(y + 1) * TILE_SIZE / scale
];
}
}
}

new TileLayer({
Tileset: NonGeospatialTileset,
...
});
```

### Tileset2D:Members

##### `opts` (Object)

User options, contains the following fields:

* `maxCacheSize` (Number, optional)
* `maxCacheByteSize` (Number, optional)
* `maxZoom` (Number, optional)
* `minZoom` (Number, optional)
* `refinementStrategy` (Enum, optional)

##### `tiles` (Array)

All tiles in the current cache.
Each `tile` contains the following properties:

- `x` (Number)
- `y` (Number)
- `z` (Number)
- `parent` (Tile): the parent tile
- `children` (Array): children tiles
- `isLoaded` (Boolean): whether the tile's data has been loaded
- `content` (Any): resolved from `getTileData`

##### `selectedTiles` (Array)

All tiles that should be displayed in the current viewport.

##### `isLoaded` (Boolean)

The returned value will be available via `tile.bbox`.
`true` if all selected tiles are loaded.

### Tileset2D:Methods

##### `getTileIndices` (Function, optional)
The following methods can be overriden by a subclass in order to customize the `TileLayer`'s behavior.

**Advanced** This function converts a given viewport to the indices needed to fetch tiles contained in the viewport. The default implementation returns visible tiles defined by [the OSM tile index](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames).
##### `getTileIndices`

`getTileIndices({viewport, maxZoom, minZoom})`

This function converts a given viewport to the indices needed to fetch tiles contained in the viewport. The default implementation returns visible tiles defined by [the OSM tile index](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames).

Receives arguments:

Expand All @@ -171,35 +297,41 @@ Returns:
An array of objects in the shape of `{x, y, z}`.


### Render Options
##### `getTileMetadata`

##### `renderSubLayers` (Function, optional))
`getTileMetadata({x, y, z})`

Renders one or an array of Layer instances with all the `TileLayer` props and the following props:
Generates additional metadata for a tile. The metadata is then stored as properties in a `tile` instance. The default implementation adds a `bbox`.

* `id`: An unique id for this sublayer
* `data`: Resolved from `getTileData`
* `tile`: An object containing tile index `x`, `y`, `z`, and `bbox` of the tile.
Receives arguments:

- Default: `props => new GeoJsonLayer(props)`
- `x` (Number)
- `y` (Number)
- `z` (Number)

Returns:

### Callbacks
An object that contains the fields that should be added to the `tile` instance.

##### `onViewportLoad` (Function, optional)

`onViewportLoad` is a function that is called when all tiles in the current viewport are loaded. The loaded content (as returned by `getTileData`) for each visible tile is passed as an array to this callback function.
##### `getParentIndex`

- Default: `data => null`
`getParentIndex({x, y, z})`

Retrives the parent of a tile. The parent tile is usually a tile in the previous zoom level (`z - 1`) and contains the given tile. This method is called to populate the `parent` and `children` fields of a tile.

##### `onTileError` (Function, optional)
Returns:

`onTileError` called when a tile failed to load.
`{x, y, z}` of the parent tile.

- Default: `console.error`

##### `updateTileStates`

`updateTileStates()`

Updates the visibility of all tiles. This method is called when the viewport changes, or new tile content has been loaded. It is called after the tiles are selected by `getTileIndicesInViewport`. This method can access all tiles and selected tiles via `this.tiles` and `this.selectedTiles`, and is expected to update the `isVisible` property of each tile.


# Source
## Source

[modules/geo-layers/src/tile-layer](https://github.com/uber/deck.gl/tree/master/modules/geo-layers/src/tile-layer)
3 changes: 3 additions & 0 deletions modules/geo-layers/src/index.js
Expand Up @@ -26,3 +26,6 @@ export {default as TripsLayer} from './trips-layer/trips-layer';
export {default as H3ClusterLayer} from './h3-layers/h3-cluster-layer';
export {default as H3HexagonLayer} from './h3-layers/h3-hexagon-layer';
export {default as Tile3DLayer} from './tile-3d-layer/tile-3d-layer';

// experimental export
export {default as _Tileset2D} from './tile-layer/tileset-2d';
Expand Up @@ -31,6 +31,20 @@ function getTileIndex(lngLat, scale) {
return [x, y];
}

// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_2
function tile2lngLat(x, y, z) {
const lng = (x / Math.pow(2, z)) * 360 - 180;
const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z);
const lat = (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
return [lng, lat];
}

export function tileToBoundingBox(x, y, z) {
const [west, north] = tile2lngLat(x, y, z);
const [east, south] = tile2lngLat(x + 1, y + 1, z);
return {west, north, east, south};
}

/**
* Returns all tile indices in the current viewport. If the current zoom level is smaller
* than minZoom, return an empty array. If the current zoom level is greater than maxZoom,
Expand Down
@@ -1,10 +1,11 @@
import {log} from '@deck.gl/core';

export default class Tile2DHeader {
constructor({getTileData, x, y, z, onTileLoad, onTileError, tileToBoundingBox}) {
constructor({getTileData, x, y, z, onTileLoad, onTileError}) {
this.x = x;
this.y = y;
this.z = z;
this.bbox = tileToBoundingBox(this.x, this.y, this.z);
this.selected = true;
this.isVisible = false;
this.parent = null;
this.children = [];

Expand All @@ -17,13 +18,21 @@ export default class Tile2DHeader {
}

get data() {
return this.content || this._loader;
return this._isLoaded ? this.content : this._loader;
}

get isLoaded() {
return this._isLoaded;
}

get byteLength() {
const result = this.content ? this.content.byteLength : 0;
if (!Number.isFinite(result)) {
log.error('byteLength not defined in tile data')();
}
return result;
}

_loadData(getTileData) {
const {x, y, z, bbox} = this;
if (!getTileData) {
Expand Down
34 changes: 11 additions & 23 deletions modules/geo-layers/src/tile-layer/tile-layer.js
@@ -1,23 +1,21 @@
import {CompositeLayer} from '@deck.gl/core';
import {GeoJsonLayer} from '@deck.gl/layers';

import Tileset2D, {STRATEGY_DEFAULT} from './utils/tileset-2d';
import {tileToBoundingBox as defaultTile2Bbox} from './utils/tile-util';
import {getTileIndices as defaultGetIndices} from './utils/viewport-util';
import Tileset2D, {STRATEGY_DEFAULT} from './tileset-2d';

const defaultProps = {
renderSubLayers: {type: 'function', value: props => new GeoJsonLayer(props), compare: false},
getTileData: {type: 'function', value: ({x, y, z}) => null, compare: false},
tileToBoundingBox: {type: 'function', value: defaultTile2Bbox, compare: false},
getTileIndices: {type: 'function', value: defaultGetIndices, compare: false},
Tileset: Tileset2D,
// TODO - change to onViewportLoad to align with Tile3DLayer
onViewportLoad: {type: 'function', optional: true, value: null, compare: false},
// eslint-disable-next-line
onTileError: {type: 'function', value: err => console.error(err), compare: false},
maxZoom: null,
minZoom: 0,
maxCacheSize: null,
strategy: STRATEGY_DEFAULT
maxCacheByteSize: null,
refinementStrategy: STRATEGY_DEFAULT
};

export default class TileLayer extends CompositeLayer {
Expand All @@ -36,35 +34,25 @@ export default class TileLayer extends CompositeLayer {
let {tileset} = this.state;
const createTileCache =
!tileset ||
props.Tileset !== oldProps.Tileset ||
(changeFlags.updateTriggersChanged &&
(changeFlags.updateTriggersChanged.all || changeFlags.updateTriggersChanged.getTileData));

if (createTileCache) {
const {
getTileData,
maxZoom,
minZoom,
const {Tileset, maxZoom, minZoom, maxCacheSize, maxCacheByteSize, refinementStrategy} = props;
tileset = new Tileset({
getTileData: props.getTileData,
maxCacheSize,
strategy,
getTileIndices,
tileToBoundingBox
} = props;
if (tileset) {
tileset.finalize();
}
tileset = new Tileset2D({
getTileData,
getTileIndices,
tileToBoundingBox,
maxSize: maxCacheSize,
maxCacheByteSize,
maxZoom,
minZoom,
strategy,
refinementStrategy,
onTileLoad: this._updateTileset.bind(this),
onTileError: this._onTileError.bind(this)
});
this.setState({tileset});
} else if (changeFlags.propsChanged) {
tileset.setOptions(props);
// if any props changed, delete the cached layers
this.state.tileset.tiles.forEach(tile => {
tile.layer = null;
Expand Down

0 comments on commit b752b25

Please sign in to comment.