UNPKG

@deck.gl/carto

Version:

CARTO official integration with Deck.gl. Build geospatial applications using CARTO and Deck.gl.

262 lines (225 loc) 8.11 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import {registerLoaders} from '@loaders.gl/core'; import {BinaryPointFeature} from '@loaders.gl/schema'; import CartoPropertiesTileLoader from './schema/carto-properties-tile-loader'; import CartoVectorTileLoader from './schema/carto-vector-tile-loader'; registerLoaders([CartoPropertiesTileLoader, CartoVectorTileLoader]); import {DefaultProps, Layer, LayersList} from '@deck.gl/core'; import {ClipExtension, CollisionFilterExtension} from '@deck.gl/extensions'; import { MVTLayer, MVTLayerProps, _getURLFromTemplate, _Tile2DHeader, _TileLoadProps as TileLoadProps, GeoBoundingBox } from '@deck.gl/geo-layers'; import {GeoJsonLayer} from '@deck.gl/layers'; import type {TilejsonResult} from '@carto/api-client'; import {TilejsonPropType, mergeLoadOptions, mergeBoundaryData} from './utils'; import {DEFAULT_TILE_SIZE} from '../constants'; import {createPointsFromLines, createPointsFromPolygons} from './label-utils'; import {createEmptyBinary} from '../utils'; import PointLabelLayer from './point-label-layer'; const MVT_BBOX: GeoBoundingBox = {west: 0, east: 1, south: 0, north: 1}; const defaultProps: DefaultProps<VectorTileLayerProps> = { ...MVTLayer.defaultProps, autoLabels: false, data: TilejsonPropType, dataComparator: TilejsonPropType.equal, tileSize: DEFAULT_TILE_SIZE }; /** All properties supported by VectorTileLayer. */ export type VectorTileLayerProps<FeaturePropertiesT = unknown> = _VectorTileLayerProps & Omit<MVTLayerProps<FeaturePropertiesT>, 'data'>; /** Properties added by VectorTileLayer. */ type _VectorTileLayerProps = { data: null | TilejsonResult | Promise<TilejsonResult>; /** * If true, create labels for lines and polygons. * Specify uniqueIdProperty to only create a single label for each unique feature. */ autoLabels?: boolean | {uniqueIdProperty: string}; }; // @ts-ignore export default class VectorTileLayer< FeaturePropertiesT = any, ExtraProps extends {} = {} > extends MVTLayer<FeaturePropertiesT, Required<_VectorTileLayerProps> & ExtraProps> { static layerName = 'VectorTileLayer'; static defaultProps = defaultProps; state!: MVTLayer['state'] & { mvt: boolean; }; constructor(...propObjects: VectorTileLayerProps<FeaturePropertiesT>[]) { // Force externally visible props type, as it is not possible modify via extension // @ts-ignore super(...propObjects); } initializeState(): void { super.initializeState(); this.setState({binary: true}); } updateState(parameters) { const {props} = parameters; if (props.data) { super.updateState(parameters); const formatTiles = new URL(props.data.tiles[0]).searchParams.get('formatTiles'); const mvt = formatTiles === 'mvt'; this.setState({mvt}); } } getLoadOptions(): any { const tileJSON = this.props.data as TilejsonResult; return mergeLoadOptions(super.getLoadOptions(), { fetch: {headers: {Authorization: `Bearer ${tileJSON.accessToken}`}}, gis: {format: 'binary'} // Use binary for MVT loading }); } /* eslint-disable camelcase */ async getTileData(tile: TileLoadProps) { const tileJSON = this.props.data as TilejsonResult; const {tiles, properties_tiles} = tileJSON; const url = _getURLFromTemplate(tiles, tile); if (!url) { return Promise.reject('Invalid URL'); } const loadOptions = this.getLoadOptions(); const {fetch} = this.props; const {signal} = tile; // Fetch geometry and attributes separately const geometryFetch = fetch(url, {propName: 'data', layer: this, loadOptions, signal}); if (!properties_tiles) { return await geometryFetch; } const propertiesUrl = _getURLFromTemplate(properties_tiles, tile); if (!propertiesUrl) { return Promise.reject('Invalid properties URL'); } const attributesFetch = fetch(propertiesUrl, { propName: 'data', layer: this, loadOptions, signal }); const [geometry, attributes] = await Promise.all([geometryFetch, attributesFetch]); if (!geometry) return null; return attributes ? mergeBoundaryData(geometry, attributes) : geometry; } /* eslint-enable camelcase */ renderSubLayers( props: VectorTileLayer['props'] & { id: string; data: any; _offset: number; tile: _Tile2DHeader; } ): GeoJsonLayer | GeoJsonLayer[] | null { if (props.data === null) { return null; } const tileBbox = props.tile.bbox as GeoBoundingBox; const subLayers: GeoJsonLayer[] = []; const defaultToPointLabelLayer = { 'points-text': { type: PointLabelLayer, ...props?._subLayerProps?.['points-text'], extensions: [ new CollisionFilterExtension(), ...(props.extensions || []), ...(props?._subLayerProps?.['points-text']?.extensions || []) ] } }; if (this.state.mvt) { const subLayerProps = { ...props, _subLayerProps: { ...props._subLayerProps, ...defaultToPointLabelLayer } }; subLayers.push(super.renderSubLayers(subLayerProps) as GeoJsonLayer); } else { const {west, south, east, north} = tileBbox; const extensions = [new ClipExtension(), ...(props.extensions || [])]; const clipProps = { clipBounds: [west, south, east, north] }; const applyClipExtensionToSublayerProps = (subLayerId: string) => { return { [subLayerId]: { ...clipProps, ...props?._subLayerProps?.[subLayerId], extensions: [...extensions, ...(props?._subLayerProps?.[subLayerId]?.extensions || [])] } }; }; const subLayerProps = { ...props, data: {...props.data, tileBbox}, autoHighlight: false, // Do not perform clipping on points (#9059) _subLayerProps: { ...props._subLayerProps, ...defaultToPointLabelLayer, ...applyClipExtensionToSublayerProps('polygons-fill'), ...applyClipExtensionToSublayerProps('polygons-stroke'), ...applyClipExtensionToSublayerProps('linestrings') } }; subLayers.push(new GeoJsonLayer(subLayerProps)); } // Add labels if (subLayers[0] && props.autoLabels) { const labelData = createEmptyBinary(); if (props.data.lines && props.data.lines.positions.value.length > 0) { labelData.points = createPointsFromLines( props.data.lines, typeof props.autoLabels === 'object' ? props.autoLabels.uniqueIdProperty : undefined ) as BinaryPointFeature; } if (props.data.polygons && props.data.polygons.positions.value.length > 0) { labelData.points = createPointsFromPolygons( props.data.polygons, this.state.mvt ? MVT_BBOX : tileBbox, props ); } subLayers.push( subLayers[0].clone({ id: `${props.id}-labels`, data: labelData, pickable: false, autoHighlight: false }) ); } return subLayers; } renderLayers(): Layer | null | LayersList { const layers = super.renderLayers() as LayersList; if (!this.props.autoLabels) { return layers; } // Sort layers so that label layers are rendered after the main layer const validLayers = (layers || []).flat().filter(Boolean) as Layer[]; validLayers.sort((a: Layer, b: Layer) => { const aHasLabel = a.id.includes('labels'); const bHasLabel = b.id.includes('labels'); if (aHasLabel && !bHasLabel) return 1; if (!aHasLabel && bHasLabel) return -1; return 0; }); return validLayers.map(l => l.id.includes('labels') ? l.clone({highlightedObjectIndex: -1}) : l ); } protected override _isWGS84(): boolean { // CARTO binary tile coordinates are [lng, lat], not tile-relative like MVT. if (this.state.mvt) return super._isWGS84(); return true; } }