-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1cbb2b6
commit 6ed31bf
Showing
8 changed files
with
403 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
import {GeoJsonLayer, GeoJsonLayerProps} from '@deck.gl/layers'; | ||
import { | ||
TileLayer, | ||
_Tile2DHeader as Tile2DHeader, | ||
TileLayerProps, | ||
TileLayerPickingInfo | ||
} from '@deck.gl/geo-layers'; | ||
import {registerLoaders} from '@loaders.gl/core'; | ||
import {binaryToGeojson} from '@loaders.gl/gis'; | ||
import {BinaryFeatureCollection} from '@loaders.gl/schema'; | ||
import type {Feature, Geometry} from 'geojson'; | ||
|
||
import { | ||
Accessor, | ||
DefaultProps, | ||
CompositeLayer, | ||
_deepEqual as deepEqual, | ||
GetPickingInfoParams, | ||
Layer, | ||
LayersList, | ||
PickingInfo | ||
} from '@deck.gl/core'; | ||
|
||
import { | ||
aggregateTile, | ||
ClusteredFeaturePropertiesT, | ||
clustersToBinary, | ||
computeAggregationStats, | ||
extractAggregationProperties, | ||
ParsedQuadbinCell, | ||
ParsedQuadbinTile | ||
} from './cluster-utils'; | ||
import {DEFAULT_TILE_SIZE} from '../constants'; | ||
import QuadbinTileset2D from './quadbin-tileset-2d'; | ||
import {getQuadbinPolygon} from './quadbin-utils'; | ||
import CartoSpatialTileLoader from './schema/carto-spatial-tile-loader'; | ||
import {injectAccessToken, TilejsonPropType} from './utils'; | ||
import type {TilejsonResult} from '../sources/types'; | ||
|
||
registerLoaders([CartoSpatialTileLoader]); | ||
|
||
const defaultProps: DefaultProps<ClusterTileLayerProps> = { | ||
data: TilejsonPropType, | ||
clusterLevel: {type: 'number', value: 5, min: 1}, | ||
getPosition: { | ||
type: 'accessor', | ||
value: ({id}) => getQuadbinPolygon(id, 0.5).slice(2, 4) as [number, number] | ||
}, | ||
getWeight: {type: 'accessor', value: 100}, | ||
refinementStrategy: 'no-overlap', | ||
tileSize: DEFAULT_TILE_SIZE | ||
}; | ||
|
||
export type ClusterTileLayerPickingInfo<FeaturePropertiesT = {}> = TileLayerPickingInfo< | ||
ParsedQuadbinTile<FeaturePropertiesT>, | ||
PickingInfo<Feature<Geometry, FeaturePropertiesT>> | ||
>; | ||
|
||
/** All properties supported by ClusterTileLayer. */ | ||
export type ClusterTileLayerProps<FeaturePropertiesT = unknown> = | ||
_ClusterTileLayerProps<FeaturePropertiesT> & | ||
Omit<TileLayerProps<ParsedQuadbinTile<FeaturePropertiesT>>, 'data'>; | ||
|
||
/** Properties added by ClusterTileLayer. */ | ||
type _ClusterTileLayerProps<FeaturePropertiesT> = Omit< | ||
GeoJsonLayerProps<ClusteredFeaturePropertiesT<FeaturePropertiesT>>, | ||
'data' | ||
> & { | ||
data: null | TilejsonResult | Promise<TilejsonResult>; | ||
|
||
/** | ||
* The number of aggregation levels to cluster cells by. Larger values increase | ||
* the clustering radius, an increment of `clusterLevel` doubling the radius. | ||
* | ||
* @default 5 | ||
*/ | ||
clusterLevel?: number; | ||
|
||
/** | ||
* The (average) position of points in a cell used for clustering. | ||
* If not supplied the center of the quadbin cell is used. | ||
* | ||
* @default cell center | ||
*/ | ||
getPosition?: Accessor<ParsedQuadbinCell<FeaturePropertiesT>, [number, number]>; | ||
|
||
/** | ||
* The weight of each cell used for clustering. | ||
* | ||
* @default 1 | ||
*/ | ||
getWeight?: Accessor<ParsedQuadbinCell<FeaturePropertiesT>, number>; | ||
}; | ||
|
||
class ClusterGeoJsonLayer< | ||
FeaturePropertiesT extends {} = {}, | ||
ExtraProps extends {} = {} | ||
> extends TileLayer< | ||
ParsedQuadbinTile<FeaturePropertiesT>, | ||
ExtraProps & Required<_ClusterTileLayerProps<FeaturePropertiesT>> | ||
> { | ||
static layerName = 'ClusterGeoJsonLayer'; | ||
static defaultProps = defaultProps; | ||
state!: TileLayer<FeaturePropertiesT>['state'] & { | ||
data: BinaryFeatureCollection; | ||
clusterIds: bigint[]; | ||
hoveredFeatureId: bigint | number | null; | ||
highlightColor: number[]; | ||
}; | ||
|
||
renderLayers(): Layer | null | LayersList { | ||
const visibleTiles = this.state.tileset?.tiles.filter((tile: Tile2DHeader) => { | ||
return tile.isLoaded && tile.content && this.state.tileset!.isTileVisible(tile); | ||
}) as Tile2DHeader<ParsedQuadbinTile<FeaturePropertiesT>>[]; | ||
if (!visibleTiles?.length) { | ||
return null; | ||
} | ||
visibleTiles.sort((a, b) => b.zoom - a.zoom); | ||
|
||
const {zoom} = this.context.viewport; | ||
const {clusterLevel, getPosition, getWeight} = this.props; | ||
|
||
const properties = extractAggregationProperties(visibleTiles[0]); | ||
const data = [] as ClusteredFeaturePropertiesT<FeaturePropertiesT>[]; | ||
for (const tile of visibleTiles) { | ||
// Calculate aggregation based on viewport zoom | ||
const overZoom = Math.round(zoom - tile.zoom); | ||
const aggregationLevels = Math.round(clusterLevel) - overZoom; | ||
aggregateTile(tile, aggregationLevels, properties, getPosition, getWeight); | ||
data.push(...tile.userData![aggregationLevels]); | ||
} | ||
|
||
data.sort((a, b) => Number(b.count - a.count)); | ||
|
||
const clusterIds = data?.map((tile: any) => tile.id); | ||
const needsUpdate = !deepEqual(clusterIds, this.state.clusterIds, 1); | ||
this.setState({clusterIds}); | ||
|
||
if (needsUpdate) { | ||
const stats = computeAggregationStats(data, properties); | ||
for (const d of data) { | ||
d.stats = stats; | ||
} | ||
this.setState({data: clustersToBinary(data)}); | ||
} | ||
|
||
const props = { | ||
...this.props, | ||
id: 'clusters', | ||
data: this.state.data, | ||
dataComparator: (data?: BinaryFeatureCollection, oldData?: BinaryFeatureCollection) => { | ||
const newIds = data?.points?.properties?.map((tile: any) => tile.id); | ||
const oldIds = oldData?.points?.properties?.map((tile: any) => tile.id); | ||
return deepEqual(newIds, oldIds, 1); | ||
} | ||
} as GeoJsonLayerProps<ClusteredFeaturePropertiesT<FeaturePropertiesT>>; | ||
|
||
return new GeoJsonLayer(this.getSubLayerProps(props)); | ||
} | ||
|
||
getPickingInfo(params: GetPickingInfoParams): ClusterTileLayerPickingInfo<FeaturePropertiesT> { | ||
const info = params.info as TileLayerPickingInfo<ParsedQuadbinTile<FeaturePropertiesT>>; | ||
|
||
if (info.index !== -1) { | ||
const {data} = params.sourceLayer!.props; | ||
info.object = binaryToGeojson(data as BinaryFeatureCollection, { | ||
globalFeatureId: info.index | ||
}) as Feature; | ||
} | ||
|
||
return info; | ||
} | ||
|
||
protected _updateAutoHighlight(info: PickingInfo): void { | ||
for (const layer of this.getSubLayers()) { | ||
layer.updateAutoHighlight(info); | ||
} | ||
} | ||
|
||
filterSubLayer() { | ||
return true; | ||
} | ||
} | ||
|
||
// Adapter layer around ClusterLayer that converts tileJSON into TileLayer API | ||
export default class ClusterTileLayer< | ||
FeaturePropertiesT = any, | ||
ExtraProps extends {} = {} | ||
> extends CompositeLayer<ExtraProps & Required<_ClusterTileLayerProps<FeaturePropertiesT>>> { | ||
static layerName = 'ClusterTileLayer'; | ||
static defaultProps = defaultProps; | ||
|
||
getLoadOptions(): any { | ||
const loadOptions = super.getLoadOptions() || {}; | ||
const tileJSON = this.props.data as TilejsonResult; | ||
injectAccessToken(loadOptions, tileJSON.accessToken); | ||
loadOptions.cartoSpatialTile = {...loadOptions.cartoSpatialTile, scheme: 'quadbin'}; | ||
return loadOptions; | ||
} | ||
|
||
renderLayers(): Layer | null | LayersList { | ||
const tileJSON = this.props.data as TilejsonResult; | ||
if (!tileJSON) return null; | ||
|
||
const {tiles: data, maxresolution: maxZoom} = tileJSON; | ||
return [ | ||
// @ts-ignore | ||
new ClusterGeoJsonLayer(this.props, { | ||
id: `cluster-geojson-layer-${this.props.id}`, | ||
data, | ||
// TODO: Tileset2D should be generic over TileIndex type | ||
TilesetClass: QuadbinTileset2D as any, | ||
maxZoom, | ||
loadOptions: this.getLoadOptions() | ||
}) | ||
]; | ||
} | ||
} |
Oops, something went wrong.