From a2b5690192cd11134511cc568b1d97f6c51d9782 Mon Sep 17 00:00:00 2001 From: felixpalmer Date: Thu, 5 Oct 2023 10:38:11 +0200 Subject: [PATCH] CARTO: Initial implementation of v9 API (#8167) --- modules/carto/src/api/layer-map.ts | 12 +- modules/carto/src/api/maps-api-common.ts | 1 - modules/carto/src/api/maps-v3-client.ts | 9 +- modules/carto/src/index.ts | 39 +++- modules/carto/src/layers/h3-tile-layer.ts | 56 ++---- .../carto/src/layers/quadbin-tile-layer.ts | 51 ++--- modules/carto/src/layers/raster-tile-layer.ts | 48 ++--- modules/carto/src/layers/utils.ts | 11 + ...rto-tile-layer.ts => vector-tile-layer.ts} | 47 ++++- modules/carto/src/sources/base-source.ts | 55 +++++ modules/carto/src/sources/common.ts | 132 ++++++++++++ modules/carto/src/sources/h3-query-source.ts | 45 +++++ modules/carto/src/sources/h3-table-source.ts | 46 +++++ .../carto/src/sources/h3-tileset-source.ts | 16 ++ modules/carto/src/sources/index.ts | 32 +++ .../carto/src/sources/quadbin-query-source.ts | 46 +++++ .../carto/src/sources/quadbin-table-source.ts | 46 +++++ .../src/sources/quadbin-tileset-source.ts | 16 ++ modules/carto/src/sources/raster-source.ts | 16 ++ modules/carto/src/sources/utils.ts | 63 ++++++ .../carto/src/sources/vector-query-source.ts | 23 +++ .../carto/src/sources/vector-table-source.ts | 23 +++ .../src/sources/vector-tileset-source.ts | 16 ++ test/apps/carto-dynamic-tile/app.jsx | 190 ------------------ test/apps/carto-dynamic-tile/app.tsx | 183 +++++++++++++++++ test/apps/carto-dynamic-tile/datasets.ts | 118 +++++++++++ test/apps/carto-dynamic-tile/index.html | 2 +- .../modules/carto/api/maps-api-client.spec.ts | 7 +- test/modules/carto/index.ts | 6 +- .../carto/layers/h3-tile-layer.spec.ts | 2 +- .../carto/layers/quadbin-tile-layer.spec.ts | 2 +- .../carto/layers/raster-tile-layer.spec.ts | 2 +- .../carto/layers/vector-tile-layer.spec.ts | 84 ++++++++ 33 files changed, 1125 insertions(+), 320 deletions(-) create mode 100644 modules/carto/src/layers/utils.ts rename modules/carto/src/layers/{carto-tile-layer.ts => vector-tile-layer.ts} (63%) create mode 100644 modules/carto/src/sources/base-source.ts create mode 100644 modules/carto/src/sources/common.ts create mode 100644 modules/carto/src/sources/h3-query-source.ts create mode 100644 modules/carto/src/sources/h3-table-source.ts create mode 100644 modules/carto/src/sources/h3-tileset-source.ts create mode 100644 modules/carto/src/sources/index.ts create mode 100644 modules/carto/src/sources/quadbin-query-source.ts create mode 100644 modules/carto/src/sources/quadbin-table-source.ts create mode 100644 modules/carto/src/sources/quadbin-tileset-source.ts create mode 100644 modules/carto/src/sources/raster-source.ts create mode 100644 modules/carto/src/sources/utils.ts create mode 100644 modules/carto/src/sources/vector-query-source.ts create mode 100644 modules/carto/src/sources/vector-table-source.ts create mode 100644 modules/carto/src/sources/vector-tileset-source.ts delete mode 100644 test/apps/carto-dynamic-tile/app.jsx create mode 100644 test/apps/carto-dynamic-tile/app.tsx create mode 100644 test/apps/carto-dynamic-tile/datasets.ts create mode 100644 test/modules/carto/layers/vector-tile-layer.spec.ts diff --git a/modules/carto/src/api/layer-map.ts b/modules/carto/src/api/layer-map.ts index d8385630532..4af6a6ab36a 100644 --- a/modules/carto/src/api/layer-map.ts +++ b/modules/carto/src/api/layer-map.ts @@ -16,12 +16,12 @@ import moment from 'moment-timezone'; import {Accessor, Layer, _ConstructorOf as ConstructorOf} from '@deck.gl/core'; import {CPUGridLayer, HeatmapLayer, HexagonLayer} from '@deck.gl/aggregation-layers'; import {GeoJsonLayer} from '@deck.gl/layers'; -import {H3HexagonLayer, MVTLayer} from '@deck.gl/geo-layers'; +import {H3HexagonLayer} from '@deck.gl/geo-layers'; -import CartoTileLayer from '../layers/carto-tile-layer'; import H3TileLayer from '../layers/h3-tile-layer'; import QuadbinTileLayer from '../layers/quadbin-tile-layer'; import RasterTileLayer from '../layers/raster-tile-layer'; +import VectorTileLayer from '../layers/vector-tile-layer'; import {MapType, TILE_FORMATS, TileFormat} from './maps-api-common'; import {assert, createBinaryProxy} from '../utils'; import { @@ -204,7 +204,7 @@ export function layerFromTileDataset( formatTiles: TileFormat | null = TILE_FORMATS.MVT, scheme: string, type?: MapType -): typeof CartoTileLayer | typeof H3TileLayer | typeof MVTLayer | typeof QuadbinTileLayer { +): typeof VectorTileLayer | typeof H3TileLayer | typeof QuadbinTileLayer { if (type === 'raster') { return RasterTileLayer; } @@ -214,12 +214,8 @@ export function layerFromTileDataset( if (scheme === 'quadbin') { return QuadbinTileLayer; } - if (formatTiles === 'mvt') { - return MVTLayer; - } - // formatTiles === BINARY|JSON|GEOJSON - return CartoTileLayer; + return VectorTileLayer; } function getTileLayer(dataset: MapDataset, basePropMap) { diff --git a/modules/carto/src/api/maps-api-common.ts b/modules/carto/src/api/maps-api-common.ts index fe09f34f64d..0572d5108d0 100644 --- a/modules/carto/src/api/maps-api-common.ts +++ b/modules/carto/src/api/maps-api-common.ts @@ -27,7 +27,6 @@ export const GEO_COLUMN_SUPPORT: MapType[] = [MAP_TYPES.QUERY, MAP_TYPES.TABLE]; // AVAILABLE FORMATS export const FORMATS = { GEOJSON: 'geojson', - NDJSON: 'ndjson', TILEJSON: 'tilejson', JSON: 'json' } as const; diff --git a/modules/carto/src/api/maps-v3-client.ts b/modules/carto/src/api/maps-v3-client.ts index edb16b17b38..771dcc3b7d5 100644 --- a/modules/carto/src/api/maps-v3-client.ts +++ b/modules/carto/src/api/maps-v3-client.ts @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ /** * Maps API Client for Carto 3 */ @@ -112,9 +113,9 @@ async function requestData({ }: RequestParams & { format: Format; }): Promise { - if (format === FORMATS.NDJSON) { - return request({method, url, accessToken, body, errorContext}); - } + // if (format === FORMATS.NDJSON) { + // return request({method, url, accessToken, body, errorContext}); + // } const data = await requestJson({method, url, accessToken, body, errorContext}); return data.rows ? data.rows : data; @@ -395,7 +396,7 @@ async function _fetchDataUrl({ assert(url, `Format ${format} not available`); } else { // guess map format - const prioritizedFormats = [FORMATS.GEOJSON, FORMATS.JSON, FORMATS.NDJSON, FORMATS.TILEJSON]; + const prioritizedFormats = [FORMATS.GEOJSON, FORMATS.JSON, FORMATS.TILEJSON]; for (const f of prioritizedFormats) { url = getUrlFromMetadata(metadata, f); if (url) { diff --git a/modules/carto/src/index.ts b/modules/carto/src/index.ts index 90b51f60fee..674096ebac6 100644 --- a/modules/carto/src/index.ts +++ b/modules/carto/src/index.ts @@ -1,10 +1,10 @@ export {getDefaultCredentials, setDefaultCredentials} from './config'; -export {default as CartoLayer} from './layers/carto-layer'; -export {default as _CartoTileLayer} from './layers/carto-tile-layer'; -export {default as _H3TileLayer} from './layers/h3-tile-layer'; +export {default as CartoLayer} from './layers/carto-layer'; // <-- REMOVE in v9 +export {default as VectorTileLayer} from './layers/vector-tile-layer'; +export {default as H3TileLayer} from './layers/h3-tile-layer'; export {default as _PointLabelLayer} from './layers/point-label-layer'; -export {default as _QuadbinTileLayer} from './layers/quadbin-tile-layer'; -export {default as _RasterTileLayer} from './layers/raster-tile-layer'; +export {default as QuadbinTileLayer} from './layers/quadbin-tile-layer'; +export {default as RasterTileLayer} from './layers/raster-tile-layer'; export {default as BASEMAP} from './basemap'; export {default as colorBins} from './style/color-bins-style'; export {default as colorCategories} from './style/color-categories-style'; @@ -22,3 +22,32 @@ export { } from './api'; export type {APIErrorContext, QueryParameters} from './api'; export type {CartoLayerProps} from './layers/carto-layer'; + +export { + cartoH3QuerySource, + cartoH3TableSource, + cartoH3TilesetSource, + cartoRasterSource, + cartoQuadbinQuerySource, + cartoQuadbinTableSource, + cartoQuadbinTilesetSource, + cartoVectorQuerySource, + cartoVectorTableSource, + cartoVectorTilesetSource, + SOURCE_DEFAULTS +} from './sources'; +export type { + CartoTilejsonResult, + CartoH3QuerySourceOptions, + CartoH3TableSourceOptions, + CartoH3TilesetSourceOptions, + CartoRasterSourceOptions, + CartoQuadbinQuerySourceOptions, + CartoQuadbinTableSourceOptions, + CartoQuadbinTilesetSourceOptions, + CartoVectorQuerySourceOptions, + CartoVectorTableSourceOptions, + CartoVectorTilesetSourceOptions, + GeojsonResult, + JsonResult +} from './sources'; diff --git a/modules/carto/src/layers/h3-tile-layer.ts b/modules/carto/src/layers/h3-tile-layer.ts index 94caaef5ec5..7d4771eaf3d 100644 --- a/modules/carto/src/layers/h3-tile-layer.ts +++ b/modules/carto/src/layers/h3-tile-layer.ts @@ -1,16 +1,11 @@ -import { - CompositeLayer, - CompositeLayerProps, - Layer, - LayersList, - UpdateParameters, - DefaultProps -} from '@deck.gl/core'; +import {CompositeLayer, CompositeLayerProps, Layer, LayersList, DefaultProps} from '@deck.gl/core'; import {H3HexagonLayer} from '@deck.gl/geo-layers'; import H3Tileset2D, {getHexagonResolution} from './h3-tileset-2d'; import SpatialIndexTileLayer from './spatial-index-tile-layer'; +import {TilejsonPropType, CartoTilejsonResult} from '../sources/common'; +import {injectAccessToken} from './utils'; -const renderSubLayers = props => { +const renderSubLayers = (props: H3HexagonLayerProps) => { const {data} = props; const {index} = props.tile; if (!data || !data.length) return null; @@ -23,7 +18,8 @@ const renderSubLayers = props => { }; const defaultProps: DefaultProps = { - aggregationResLevel: 4 + aggregationResLevel: 4, + data: TilejsonPropType }; /** All properties supported by H3TileLayer. */ @@ -34,8 +30,8 @@ export type H3TileLayerProps = _H3TileLayerProps & Composite type H3HexagonLayerProps = Record; /** Properties added by H3TileLayer. */ -type _H3TileLayerProps = H3HexagonLayerProps & { - data: string; +type _H3TileLayerProps = Omit, 'data'> & { + data: null | CartoTilejsonResult | Promise; aggregationResLevel?: number; }; @@ -45,30 +41,24 @@ export default class H3TileLayer exten static layerName = 'H3TileLayer'; static defaultProps = defaultProps; - state!: { - tileJSON: any; - data: any; - }; - initializeState(): void { H3HexagonLayer._checkH3Lib(); - this.setState({data: null, tileJSON: null}); } - updateState({changeFlags}: UpdateParameters): void { - if (changeFlags.dataChanged) { - let {data} = this.props; - const tileJSON = data; - data = (tileJSON as any).tiles; - this.setState({data, tileJSON}); - } + getLoadOptions(): any { + const loadOptions = super.getLoadOptions() || {}; + const tileJSON = this.props.data as CartoTilejsonResult; + injectAccessToken(loadOptions, tileJSON.accessToken); + loadOptions.cartoSpatialTile = {...loadOptions.cartoSpatialTile, scheme: 'h3'}; + return loadOptions; } renderLayers(): Layer | null | LayersList { - const {data, tileJSON} = this.state; - let minresolution = parseInt(tileJSON.minresolution); - let maxresolution = parseInt(tileJSON.maxresolution); + const tileJSON = this.props.data as CartoTilejsonResult; + if (!tileJSON) return null; + const {tiles: data} = tileJSON; + let {minresolution, maxresolution} = tileJSON; // Convert Mercator zooms provided in props into H3 res levels // and clip into valid range provided from the tilejson if (this.props.minZoom) { @@ -87,19 +77,17 @@ export default class H3TileLayer exten // The naming is unfortunate, but minZoom & maxZoom in the context // of a Tileset2D refer to the resolution levels, not the Mercator zooms return [ + // @ts-ignore new SpatialIndexTileLayer(this.props, { id: `h3-tile-layer-${this.props.id}`, data, - // @ts-expect-error Tileset2D should be generic over TileIndex - TilesetClass: H3Tileset2D, + // TODO: Tileset2D should be generic over TileIndex type + TilesetClass: H3Tileset2D as any, renderSubLayers, // minZoom and maxZoom are H3 resolutions, however we must use this naming as that is what the Tileset2D class expects minZoom: minresolution, maxZoom: maxresolution, - loadOptions: { - ...this.getLoadOptions(), - cartoSpatialTile: {scheme: 'h3'} - } + loadOptions: this.getLoadOptions() }) ]; } diff --git a/modules/carto/src/layers/quadbin-tile-layer.ts b/modules/carto/src/layers/quadbin-tile-layer.ts index e62b09b16f3..061bea899eb 100644 --- a/modules/carto/src/layers/quadbin-tile-layer.ts +++ b/modules/carto/src/layers/quadbin-tile-layer.ts @@ -1,15 +1,10 @@ -import { - CompositeLayer, - CompositeLayerProps, - Layer, - LayersList, - UpdateParameters, - DefaultProps -} from '@deck.gl/core'; +import {CompositeLayer, CompositeLayerProps, Layer, LayersList, DefaultProps} from '@deck.gl/core'; import QuadbinLayer, {QuadbinLayerProps} from './quadbin-layer'; import QuadbinTileset2D from './quadbin-tileset-2d'; import SpatialIndexTileLayer from './spatial-index-tile-layer'; import {hexToBigInt} from 'quadbin'; +import {TilejsonPropType, CartoTilejsonResult} from '../sources/common'; +import {injectAccessToken} from './utils'; export const renderSubLayers = props => { const {data} = props; @@ -21,7 +16,8 @@ export const renderSubLayers = props => { }; const defaultProps: DefaultProps = { - aggregationResLevel: 6 + aggregationResLevel: 6, + data: TilejsonPropType }; /** All properties supported by QuadbinTileLayer. */ @@ -29,8 +25,8 @@ export type QuadbinTileLayerProps = _QuadbinTileLayerProps & CompositeLayerProps; /** Properties added by QuadbinTileLayer. */ -type _QuadbinTileLayerProps = QuadbinLayerProps & { - data: string; +type _QuadbinTileLayerProps = Omit, 'data'> & { + data: null | CartoTilejsonResult | Promise; aggregationResLevel?: number; }; @@ -41,27 +37,21 @@ export default class QuadbinTileLayer< static layerName = 'QuadbinTileLayer'; static defaultProps = defaultProps; - state!: { - tileJSON: any; - data: any; - }; - initializeState(): void { - this.setState({data: null, tileJSON: null}); - } - - updateState({changeFlags}: UpdateParameters): void { - if (changeFlags.dataChanged) { - let {data} = this.props; - const tileJSON = data; - data = (tileJSON as any).tiles; - this.setState({data, tileJSON}); - } + getLoadOptions(): any { + const loadOptions = super.getLoadOptions() || {}; + const tileJSON = this.props.data as CartoTilejsonResult; + injectAccessToken(loadOptions, tileJSON.accessToken); + loadOptions.cartoSpatialTile = {...loadOptions.cartoSpatialTile, scheme: 'quadbin'}; + return loadOptions; } renderLayers(): Layer | null | LayersList { - const {data, tileJSON} = this.state; - const maxZoom = parseInt(tileJSON?.maxresolution); + const tileJSON = this.props.data as CartoTilejsonResult; + if (!tileJSON) return null; + + const {tiles: data, maxresolution: maxZoom} = tileJSON; return [ + // @ts-ignore new SpatialIndexTileLayer(this.props, { id: `quadbin-tile-layer-${this.props.id}`, data, @@ -69,10 +59,7 @@ export default class QuadbinTileLayer< TilesetClass: QuadbinTileset2D as any, renderSubLayers, maxZoom, - loadOptions: { - ...this.getLoadOptions(), - cartoSpatialTile: {scheme: 'quadbin'} - } + loadOptions: this.getLoadOptions() }) ]; } diff --git a/modules/carto/src/layers/raster-tile-layer.ts b/modules/carto/src/layers/raster-tile-layer.ts index 4003f344952..0e6ef20e6fc 100644 --- a/modules/carto/src/layers/raster-tile-layer.ts +++ b/modules/carto/src/layers/raster-tile-layer.ts @@ -1,13 +1,9 @@ -import { - CompositeLayer, - CompositeLayerProps, - Layer, - LayersList, - UpdateParameters -} from '@deck.gl/core'; +import {CompositeLayer, CompositeLayerProps, DefaultProps, Layer, LayersList} from '@deck.gl/core'; import RasterLayer, {RasterLayerProps} from './raster-layer'; import QuadbinTileset2D from './quadbin-tileset-2d'; import SpatialIndexTileLayer from './spatial-index-tile-layer'; +import {TilejsonPropType, CartoTilejsonResult} from '../sources/common'; +import {injectAccessToken} from './utils'; export const renderSubLayers = props => { const tileIndex = props.tile?.index?.q; @@ -15,12 +11,16 @@ export const renderSubLayers = props => { return new RasterLayer(props, {tileIndex}); }; +const defaultProps: DefaultProps = { + data: TilejsonPropType +}; + /** All properties supported by RasterTileLayer. */ export type RasterTileLayerProps = _RasterTileLayerProps & CompositeLayerProps; /** Properties added by RasterTileLayer. */ -type _RasterTileLayerProps = RasterLayerProps & { - data: string; +type _RasterTileLayerProps = Omit, 'data'> & { + data: null | CartoTilejsonResult | Promise; }; export default class RasterTileLayer< @@ -28,27 +28,22 @@ export default class RasterTileLayer< ExtraProps extends {} = {} > extends CompositeLayer>> { static layerName = 'RasterTileLayer'; - static defaultProps = {}; + static defaultProps = defaultProps; - state!: {tileJSON: any; data: any}; - initializeState(): void { - this.setState({data: null, tileJSON: null}); - } - - updateState({changeFlags}: UpdateParameters): void { - if (changeFlags.dataChanged) { - let {data} = this.props; - const tileJSON = data; - data = (tileJSON as any).tiles; - this.setState({data, tileJSON}); - } + getLoadOptions(): any { + const loadOptions = super.getLoadOptions() || {}; + const tileJSON = this.props.data as CartoTilejsonResult; + injectAccessToken(loadOptions, tileJSON.accessToken); + return loadOptions; } renderLayers(): Layer | null | LayersList { - const {data, tileJSON} = this.state; - const minZoom = parseInt(tileJSON?.minzoom); - const maxZoom = parseInt(tileJSON?.maxzoom); + const tileJSON = this.props.data as CartoTilejsonResult; + if (!tileJSON) return null; + + const {tiles: data, minzoom: minZoom, maxzoom: maxZoom} = tileJSON; return [ + // @ts-ignore new SpatialIndexTileLayer(this.props, { id: `raster-tile-layer-${this.props.id}`, data, @@ -56,7 +51,8 @@ export default class RasterTileLayer< TilesetClass: QuadbinTileset2D as any, renderSubLayers, minZoom, - maxZoom + maxZoom, + loadOptions: this.getLoadOptions() }) ]; } diff --git a/modules/carto/src/layers/utils.ts b/modules/carto/src/layers/utils.ts new file mode 100644 index 00000000000..548498d08ae --- /dev/null +++ b/modules/carto/src/layers/utils.ts @@ -0,0 +1,11 @@ +/** + * Adds access token to Authorization header in loadOptions + */ +export function injectAccessToken(loadOptions: any, accessToken: string): void { + if (!loadOptions?.fetch?.headers?.Authorization) { + loadOptions.fetch = { + ...loadOptions.fetch, + headers: {...loadOptions.fetch?.headers, Authorization: `Bearer ${accessToken}`} + }; + } +} diff --git a/modules/carto/src/layers/carto-tile-layer.ts b/modules/carto/src/layers/vector-tile-layer.ts similarity index 63% rename from modules/carto/src/layers/carto-tile-layer.ts rename to modules/carto/src/layers/vector-tile-layer.ts index c727a7e0e43..10ab173e013 100644 --- a/modules/carto/src/layers/carto-tile-layer.ts +++ b/modules/carto/src/layers/vector-tile-layer.ts @@ -17,19 +17,23 @@ import {binaryToGeojson} from '@loaders.gl/gis'; import type {BinaryFeatures} from '@loaders.gl/schema'; import {TileFormat, TILE_FORMATS} from '../api/maps-api-common'; import type {Feature} from 'geojson'; +import {TilejsonPropType, CartoTilejsonResult} from '../sources/common'; +import {injectAccessToken} from './utils'; const defaultTileFormat = TILE_FORMATS.BINARY; -const defaultProps: DefaultProps = { +const defaultProps: DefaultProps = { ...MVTLayer.defaultProps, + data: TilejsonPropType, formatTiles: defaultTileFormat }; -/** All properties supported by CartoTileLayer. */ -export type CartoTileLayerProps = _CartoTileLayerProps & MVTLayerProps; +/** All properties supported by VectorTileLayer. */ +export type VectorTileLayerProps = _VectorTileLayerProps & Omit; -/** Properties added by CartoTileLayer. */ -type _CartoTileLayerProps = { +/** Properties added by VectorTileLayer. */ +type _VectorTileLayerProps = { + data: null | CartoTilejsonResult | Promise; /** Use to override the default tile data format. * * Possible values are: `TILE_FORMATS.BINARY`, `TILE_FORMATS.GEOJSON` and `TILE_FORMATS.MVT`. @@ -39,18 +43,39 @@ type _CartoTileLayerProps = { formatTiles?: TileFormat; }; -export default class CartoTileLayer extends MVTLayer< - Required<_CartoTileLayerProps> & ExtraProps +// TODO Perhaps we can't subclass MVTLayer and keep types. Better to subclass TileLayer instead? +// @ts-ignore +export default class VectorTileLayer extends MVTLayer< + Required<_VectorTileLayerProps> & ExtraProps > { - static layerName = 'CartoTileLayer'; + static layerName = 'VectorTileLayer'; static defaultProps = defaultProps; initializeState(): void { super.initializeState(); - const binary = this.props.formatTiles === TILE_FORMATS.BINARY; + const binary = this.props.formatTiles === TILE_FORMATS.BINARY || TILE_FORMATS.MVT; this.setState({binary}); } + 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 === TILE_FORMATS.MVT; + this.setState({mvt}); + } + } + + getLoadOptions(): any { + const loadOptions = super.getLoadOptions() || {}; + const tileJSON = this.props.data as CartoTilejsonResult; + injectAccessToken(loadOptions, tileJSON.accessToken); + loadOptions.gis = {format: 'binary'}; // Use binary for MVT loading + return loadOptions; + } + getTileData(tile: TileLoadProps) { const url = _getURLFromTemplate(this.state.data, tile); if (!url) { @@ -75,6 +100,10 @@ export default class CartoTileLayer extends MVTLayer return null; } + if (this.state.mvt) { + return super.renderSubLayers(props) as GeoJsonLayer; + } + const tileBbox = props.tile.bbox as any; const {west, south, east, north} = tileBbox; diff --git a/modules/carto/src/sources/base-source.ts b/modules/carto/src/sources/base-source.ts new file mode 100644 index 00000000000..4c5feb44794 --- /dev/null +++ b/modules/carto/src/sources/base-source.ts @@ -0,0 +1,55 @@ +import type {MapType} from '../api/maps-api-common'; +import {APIErrorContext} from '../api/carto-api-error'; +import { + CartoSourceOptionalOptions, + CartoSourceRequiredOptions, + CartoTilejsonResult, + GeojsonResult, + JsonResult, + SOURCE_DEFAULTS, + Tilejson, + TilejsonMapInstantiation +} from './common'; +import {buildApiEndpoint, requestWithParameters} from './utils'; + +export async function cartoBaseSource>( + endpoint: MapType, + options: Partial & CartoSourceRequiredOptions, + urlParameters: UrlParameters +): Promise { + const mergedOptions = {...SOURCE_DEFAULTS, ...options, endpoint}; + const baseUrl = buildApiEndpoint(mergedOptions); + const {accessToken, format} = mergedOptions; + const headers = {Authorization: `Bearer ${options.accessToken}`, ...options.headers}; + + const errorContext: APIErrorContext = { + requestType: 'Map instantiation', + connection: options.connectionName, + type: endpoint, + source: JSON.stringify(urlParameters, undefined, 2) + }; + const mapInstantiation = await requestWithParameters({ + baseUrl, + parameters: urlParameters, + headers, + errorContext + }); + + const dataUrl = mapInstantiation[format].url[0]; + errorContext.requestType = 'Map data'; + + if (format === 'tilejson') { + const tilejson = await requestWithParameters({ + baseUrl: dataUrl, + headers, + errorContext + }); + return {...tilejson, accessToken}; + } + + return await requestWithParameters({ + baseUrl: dataUrl, + headers, + errorContext + }); +} diff --git a/modules/carto/src/sources/common.ts b/modules/carto/src/sources/common.ts new file mode 100644 index 00000000000..f1314d03a73 --- /dev/null +++ b/modules/carto/src/sources/common.ts @@ -0,0 +1,132 @@ +/* eslint-disable camelcase */ +import type {Feature} from 'geojson'; +import {Format, MapInstantiation, TileFormat, QueryParameters} from '../api/maps-api-common'; + +export type CartoSourceRequiredOptions = { + accessToken: string; + connectionName: string; +}; + +export type CartoSourceOptionalOptions = { + apiBaseUrl: string; + clientId: string; + format: Format; + formatTiles: TileFormat; + headers: Record; + mapsUrl?: string; +}; + +export type CartoSourceOptions = CartoSourceRequiredOptions & Partial; + +export type CartoAggregationOptions = { + aggregationExp: string; + aggregationResLevel?: number; +}; + +export type CartoQuerySourceOptions = { + spatialDataColumn?: string; + sqlQuery: string; + queryParameters?: QueryParameters; +}; + +export type CartoTableSourceOptions = { + columns?: string[]; + spatialDataColumn?: string; + tableName: string; +}; + +export type CartoTilesetSourceOptions = { + tableName: string; +}; + +export const SOURCE_DEFAULTS: CartoSourceOptionalOptions = { + apiBaseUrl: 'https://gcp-us-east1.api.carto.com', + clientId: 'deck-gl-carto', + format: 'tilejson', + formatTiles: 'binary', + headers: {} +}; + +export type TilejsonMapInstantiation = MapInstantiation & { + tilejson: {url: string[]}; +}; + +export interface Tilejson { + tilejson: string; + name: string; + description: string; + version: string; + attribution: string; + scheme: string; + tiles: string[]; + properties_tiles: string[]; + minresolution: number; + maxresolution: number; + minzoom: number; + maxzoom: number; + bounds: [number, number, number, number]; + center: [number, number, number]; + vector_layers: VectorLayer[]; + tilestats: Tilestats; +} + +export interface Tilestats { + layerCount: number; + layers: Layer[]; +} + +export interface Layer { + layer: string; + count: number; + attributeCount: number; + attributes: Attribute[]; +} + +export interface Attribute { + attribute: string; + type: string; +} + +export interface VectorLayer { + id: string; + minzoom: number; + maxzoom: number; + fields: Record; +} + +export type CartoTilejsonResult = Tilejson & {accessToken: string}; +export type GeojsonResult = {type: 'FeatureCollection'; features: Feature[]}; +export type JsonResult = any[]; +export interface TilejsonSource { + (options: T & {format?: 'tilejson'}): Promise; +} +export interface TypedSource extends TilejsonSource { + (options: T & {format: 'geojson'}): Promise; + (options: T & {format: 'json'}): Promise; +} + +export const DEFAULT_CLIENT = 'deck-gl-carto'; +export const V3_MINOR_VERSION = '3.2'; +export const MAX_GET_LENGTH = 8192; + +export const DEFAULT_PARAMETERS = { + client: DEFAULT_CLIENT, + v: V3_MINOR_VERSION +}; + +export const DEFAULT_HEADERS = { + Accept: 'application/json', + 'Content-Type': 'application/json' +}; + +export const TilejsonPropType = { + type: 'object' as const, + value: null as null | CartoTilejsonResult, + validate: (value: CartoTilejsonResult, propType) => + (propType.optional && value === null) || + (typeof value === 'object' && + Array.isArray(value.tiles) && + value.tiles.every(url => typeof url === 'string')), + compare: 2, + async: true +}; diff --git a/modules/carto/src/sources/h3-query-source.ts b/modules/carto/src/sources/h3-query-source.ts new file mode 100644 index 00000000000..d1ded1115d9 --- /dev/null +++ b/modules/carto/src/sources/h3-query-source.ts @@ -0,0 +1,45 @@ +/* eslint-disable camelcase */ +import {cartoBaseSource} from './base-source'; +import { + CartoAggregationOptions, + CartoQuerySourceOptions, + CartoSourceOptions, + TilejsonSource +} from './common'; + +export type CartoH3QuerySourceOptions = CartoSourceOptions & + CartoQuerySourceOptions & + CartoAggregationOptions; +type UrlParameters = { + aggregationExp: string; + aggregationResLevel?: string; + geo_column?: string; + q: string; + queryParameters?: string; +}; + +const cartoH3QuerySource: TilejsonSource = async function ( + options: CartoH3QuerySourceOptions +): Promise { + const { + aggregationExp, + aggregationResLevel = 4, + sqlQuery, + spatialDataColumn = 'h3:h3', + queryParameters + } = options; + const urlParameters: UrlParameters = {aggregationExp, q: sqlQuery}; + + if (aggregationResLevel) { + urlParameters.aggregationResLevel = String(aggregationResLevel); + } + if (spatialDataColumn) { + urlParameters.geo_column = spatialDataColumn; + } + if (queryParameters) { + urlParameters.queryParameters = JSON.stringify(queryParameters); + } + return cartoBaseSource('query', options, urlParameters); +}; + +export {cartoH3QuerySource}; diff --git a/modules/carto/src/sources/h3-table-source.ts b/modules/carto/src/sources/h3-table-source.ts new file mode 100644 index 00000000000..aee4a82777c --- /dev/null +++ b/modules/carto/src/sources/h3-table-source.ts @@ -0,0 +1,46 @@ +/* eslint-disable camelcase */ +import {cartoBaseSource} from './base-source'; +import { + CartoAggregationOptions, + CartoSourceOptions, + CartoTableSourceOptions, + TilejsonSource +} from './common'; + +export type CartoH3TableSourceOptions = CartoSourceOptions & + CartoTableSourceOptions & + CartoAggregationOptions; + +type UrlParameters = { + aggregationExp: string; + aggregationResLevel?: string; + columns?: string; + geo_column?: string; + name: string; +}; + +const cartoH3TableSource: TilejsonSource = async function ( + options: CartoH3TableSourceOptions +): Promise { + const { + aggregationExp, + aggregationResLevel = 4, + columns, + spatialDataColumn = 'h3:h3', + tableName + } = options; + const urlParameters: UrlParameters = {aggregationExp, name: tableName}; + + if (aggregationResLevel) { + urlParameters.aggregationResLevel = String(aggregationResLevel); + } + if (columns) { + urlParameters.columns = columns.join(','); + } + if (spatialDataColumn) { + urlParameters.geo_column = spatialDataColumn; + } + return cartoBaseSource('table', options, urlParameters); +}; + +export {cartoH3TableSource}; diff --git a/modules/carto/src/sources/h3-tileset-source.ts b/modules/carto/src/sources/h3-tileset-source.ts new file mode 100644 index 00000000000..30a3d6fb169 --- /dev/null +++ b/modules/carto/src/sources/h3-tileset-source.ts @@ -0,0 +1,16 @@ +import {cartoBaseSource} from './base-source'; +import {CartoSourceOptions, CartoTilesetSourceOptions, TilejsonSource} from './common'; + +export type CartoH3TilesetSourceOptions = CartoSourceOptions & CartoTilesetSourceOptions; +type UrlParameters = {name: string}; + +const cartoH3TilesetSource: TilejsonSource = async function ( + options: CartoH3TilesetSourceOptions +): Promise { + const {tableName} = options; + const urlParameters: UrlParameters = {name: tableName}; + + return cartoBaseSource('tileset', options, urlParameters); +}; + +export {cartoH3TilesetSource}; diff --git a/modules/carto/src/sources/index.ts b/modules/carto/src/sources/index.ts new file mode 100644 index 00000000000..02a24f48015 --- /dev/null +++ b/modules/carto/src/sources/index.ts @@ -0,0 +1,32 @@ +export {SOURCE_DEFAULTS} from './common'; +export type {CartoTilejsonResult, GeojsonResult, JsonResult} from './common'; + +export {cartoH3QuerySource} from './h3-query-source'; +export type {CartoH3QuerySourceOptions} from './h3-query-source'; + +export {cartoH3TableSource} from './h3-table-source'; +export type {CartoH3TableSourceOptions} from './h3-table-source'; + +export {cartoH3TilesetSource} from './h3-tileset-source'; +export type {CartoH3TilesetSourceOptions} from './h3-tileset-source'; + +export {cartoRasterSource} from './raster-source'; +export type {CartoRasterSourceOptions} from './raster-source'; + +export {cartoQuadbinQuerySource} from './quadbin-query-source'; +export type {CartoQuadbinQuerySourceOptions} from './quadbin-query-source'; + +export {cartoQuadbinTableSource} from './quadbin-table-source'; +export type {CartoQuadbinTableSourceOptions} from './quadbin-table-source'; + +export {cartoQuadbinTilesetSource} from './quadbin-tileset-source'; +export type {CartoQuadbinTilesetSourceOptions} from './quadbin-tileset-source'; + +export {cartoVectorQuerySource} from './vector-query-source'; +export type {CartoVectorQuerySourceOptions} from './vector-query-source'; + +export {cartoVectorTableSource} from './vector-table-source'; +export type {CartoVectorTableSourceOptions} from './vector-table-source'; + +export {cartoVectorTilesetSource} from './vector-tileset-source'; +export type {CartoVectorTilesetSourceOptions} from './vector-tileset-source'; diff --git a/modules/carto/src/sources/quadbin-query-source.ts b/modules/carto/src/sources/quadbin-query-source.ts new file mode 100644 index 00000000000..13d4c28d901 --- /dev/null +++ b/modules/carto/src/sources/quadbin-query-source.ts @@ -0,0 +1,46 @@ +/* eslint-disable camelcase */ +import {cartoBaseSource} from './base-source'; +import { + CartoAggregationOptions, + CartoQuerySourceOptions, + CartoSourceOptions, + TilejsonSource +} from './common'; + +export type CartoQuadbinQuerySourceOptions = CartoSourceOptions & + CartoQuerySourceOptions & + CartoAggregationOptions; + +type UrlParameters = { + aggregationExp: string; + aggregationResLevel?: string; + geo_column?: string; + q: string; + queryParameters?: string; +}; + +const cartoQuadbinQuerySource: TilejsonSource = async function ( + options: CartoQuadbinQuerySourceOptions +): Promise { + const { + aggregationExp, + aggregationResLevel = 6, + sqlQuery, + spatialDataColumn = 'quadbin:quadbin', + queryParameters + } = options; + const urlParameters: UrlParameters = {aggregationExp, q: sqlQuery}; + + if (aggregationResLevel) { + urlParameters.aggregationResLevel = String(aggregationResLevel); + } + if (spatialDataColumn) { + urlParameters.geo_column = spatialDataColumn; + } + if (queryParameters) { + urlParameters.queryParameters = JSON.stringify(queryParameters); + } + return cartoBaseSource('query', options, urlParameters); +}; + +export {cartoQuadbinQuerySource}; diff --git a/modules/carto/src/sources/quadbin-table-source.ts b/modules/carto/src/sources/quadbin-table-source.ts new file mode 100644 index 00000000000..542b754a993 --- /dev/null +++ b/modules/carto/src/sources/quadbin-table-source.ts @@ -0,0 +1,46 @@ +/* eslint-disable camelcase */ +import {cartoBaseSource} from './base-source'; +import { + CartoAggregationOptions, + CartoSourceOptions, + CartoTableSourceOptions, + TilejsonSource +} from './common'; + +export type CartoQuadbinTableSourceOptions = CartoSourceOptions & + CartoTableSourceOptions & + CartoAggregationOptions; + +type UrlParameters = { + aggregationExp: string; + aggregationResLevel?: string; + columns?: string; + geo_column?: string; + name: string; +}; + +const cartoQuadbinTableSource: TilejsonSource = async function ( + options: CartoQuadbinTableSourceOptions +): Promise { + const { + aggregationExp, + aggregationResLevel = 6, + columns, + spatialDataColumn = 'quadbin:quadbin', + tableName + } = options; + const urlParameters: UrlParameters = {aggregationExp, name: tableName}; + + if (aggregationResLevel) { + urlParameters.aggregationResLevel = String(aggregationResLevel); + } + if (columns) { + urlParameters.columns = columns.join(','); + } + if (spatialDataColumn) { + urlParameters.geo_column = spatialDataColumn; + } + return cartoBaseSource('table', options, urlParameters); +}; + +export {cartoQuadbinTableSource}; diff --git a/modules/carto/src/sources/quadbin-tileset-source.ts b/modules/carto/src/sources/quadbin-tileset-source.ts new file mode 100644 index 00000000000..bd819943e45 --- /dev/null +++ b/modules/carto/src/sources/quadbin-tileset-source.ts @@ -0,0 +1,16 @@ +import {cartoBaseSource} from './base-source'; +import {CartoSourceOptions, CartoTilesetSourceOptions, TilejsonSource} from './common'; + +export type CartoQuadbinTilesetSourceOptions = CartoSourceOptions & CartoTilesetSourceOptions; +type UrlParameters = {name: string}; + +const cartoQuadbinTilesetSource: TilejsonSource = async function ( + options: CartoQuadbinTilesetSourceOptions +): Promise { + const {tableName} = options; + const urlParameters: UrlParameters = {name: tableName}; + + return cartoBaseSource('tileset', options, urlParameters); +}; + +export {cartoQuadbinTilesetSource}; diff --git a/modules/carto/src/sources/raster-source.ts b/modules/carto/src/sources/raster-source.ts new file mode 100644 index 00000000000..db37cb202df --- /dev/null +++ b/modules/carto/src/sources/raster-source.ts @@ -0,0 +1,16 @@ +import {cartoBaseSource} from './base-source'; +import {CartoSourceOptions, CartoTilesetSourceOptions, TypedSource} from './common'; + +export type CartoRasterSourceOptions = CartoSourceOptions & CartoTilesetSourceOptions; +type UrlParameters = {name: string}; + +const cartoRasterSource: TypedSource = async function ( + options: CartoRasterSourceOptions +): Promise { + const {tableName} = options; + const urlParameters: UrlParameters = {name: tableName}; + + return cartoBaseSource('raster', options, urlParameters); +}; + +export {cartoRasterSource}; diff --git a/modules/carto/src/sources/utils.ts b/modules/carto/src/sources/utils.ts new file mode 100644 index 00000000000..1b2cd280e65 --- /dev/null +++ b/modules/carto/src/sources/utils.ts @@ -0,0 +1,63 @@ +import {APIErrorContext, CartoAPIError} from '../api/carto-api-error'; +import {encodeParameter, MapType} from '../api/maps-api-common'; +import {DEFAULT_HEADERS, DEFAULT_PARAMETERS, MAX_GET_LENGTH} from './common'; +import {buildMapsUrlFromBase} from '../config'; + +export async function requestWithParameters({ + baseUrl, + parameters, + headers: customHeaders, + errorContext +}: { + baseUrl: string; + parameters?: Record; + headers: Record; + errorContext: APIErrorContext; +}): Promise { + let url = baseUrl; + if (parameters) { + const allParameters = {...DEFAULT_PARAMETERS, ...parameters}; + const encodedParameters = Object.entries(allParameters).map(([key, value]) => { + return encodeParameter(key, value); + }); + url += `?${encodedParameters.join('&')}`; + } + + const headers = {...DEFAULT_HEADERS, ...customHeaders}; + try { + /* global fetch */ + let response: Response; + if (url.length > MAX_GET_LENGTH) { + response = await fetch(url, {method: 'POST', body: JSON.stringify(parameters), headers}); + } else { + response = await fetch(url, {headers}); + } + let json: any; + try { + json = await response.json(); + } catch { + json = {error: ''}; + } + if (!response.ok) { + throw new CartoAPIError(json.error, errorContext, response); + } + + return json; + } catch (error) { + throw new CartoAPIError(error as Error, errorContext); + } +} + +export function buildApiEndpoint({ + apiBaseUrl, + connectionName, + endpoint, + mapsUrl +}: { + apiBaseUrl: string; + connectionName: string; + endpoint: MapType; + mapsUrl?: string; +}): string { + return `${mapsUrl || buildMapsUrlFromBase(apiBaseUrl)}/${connectionName}/${endpoint}`; +} diff --git a/modules/carto/src/sources/vector-query-source.ts b/modules/carto/src/sources/vector-query-source.ts new file mode 100644 index 00000000000..d08ce67eb12 --- /dev/null +++ b/modules/carto/src/sources/vector-query-source.ts @@ -0,0 +1,23 @@ +/* eslint-disable camelcase */ +import {cartoBaseSource} from './base-source'; +import {CartoSourceOptions, CartoQuerySourceOptions, TypedSource} from './common'; + +export type CartoVectorQuerySourceOptions = CartoSourceOptions & CartoQuerySourceOptions; +type UrlParameters = {geo_column?: string; q: string; queryParameters?: string}; + +const cartoVectorQuerySource: TypedSource = async function ( + options: CartoVectorQuerySourceOptions +): Promise { + const {spatialDataColumn, sqlQuery, queryParameters} = options; + const urlParameters: UrlParameters = {q: sqlQuery}; + + if (spatialDataColumn) { + urlParameters.geo_column = spatialDataColumn; + } + if (queryParameters) { + urlParameters.queryParameters = JSON.stringify(queryParameters); + } + return cartoBaseSource('query', options, urlParameters); +}; + +export {cartoVectorQuerySource}; diff --git a/modules/carto/src/sources/vector-table-source.ts b/modules/carto/src/sources/vector-table-source.ts new file mode 100644 index 00000000000..9c1066e9503 --- /dev/null +++ b/modules/carto/src/sources/vector-table-source.ts @@ -0,0 +1,23 @@ +/* eslint-disable camelcase */ +import {cartoBaseSource} from './base-source'; +import {CartoSourceOptions, CartoTableSourceOptions, TypedSource} from './common'; + +export type CartoVectorTableSourceOptions = CartoSourceOptions & CartoTableSourceOptions; +type UrlParameters = {columns?: string; geo_column?: string; name: string}; + +const cartoVectorTableSource: TypedSource = async function ( + options: CartoVectorTableSourceOptions +): Promise { + const {columns, spatialDataColumn, tableName} = options; + const urlParameters: UrlParameters = {name: tableName}; + + if (columns) { + urlParameters.columns = columns.join(','); + } + if (spatialDataColumn) { + urlParameters.geo_column = spatialDataColumn; + } + return cartoBaseSource('table', options, urlParameters); +}; + +export {cartoVectorTableSource}; diff --git a/modules/carto/src/sources/vector-tileset-source.ts b/modules/carto/src/sources/vector-tileset-source.ts new file mode 100644 index 00000000000..f4cc5970c0a --- /dev/null +++ b/modules/carto/src/sources/vector-tileset-source.ts @@ -0,0 +1,16 @@ +import {cartoBaseSource} from './base-source'; +import {CartoSourceOptions, CartoTilesetSourceOptions, TilejsonSource} from './common'; + +export type CartoVectorTilesetSourceOptions = CartoSourceOptions & CartoTilesetSourceOptions; +type UrlParameters = {name: string}; + +const cartoVectorTilesetSource: TilejsonSource = async function ( + options: CartoVectorTilesetSourceOptions +): Promise { + const {tableName} = options; + const urlParameters: UrlParameters = {name: tableName}; + + return cartoBaseSource('tileset', options, urlParameters); +}; + +export {cartoVectorTilesetSource}; diff --git a/test/apps/carto-dynamic-tile/app.jsx b/test/apps/carto-dynamic-tile/app.jsx deleted file mode 100644 index 53666fc99fa..00000000000 --- a/test/apps/carto-dynamic-tile/app.jsx +++ /dev/null @@ -1,190 +0,0 @@ -/* global document */ -/* eslint-disable no-console */ -import React, {useState} from 'react'; -import {createRoot} from 'react-dom/client'; -import DeckGL from '@deck.gl/react'; -import {CartoLayer, FORMATS, MAP_TYPES} from '@deck.gl/carto'; -import {GeoJsonLayer} from '@deck.gl/layers'; - -const ZOOMS = {3: 3, 4: 4, 5: 5, 6: 6}; -const FORMATTILES = {binary: 'binary', json: 'json'}; -const INITIAL_VIEW_STATE = {longitude: 8, latitude: 47, zoom: 6}; -const COUNTRIES = - 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_0_scale_rank.geojson'; - -// Skip CDN -// const apiBaseUrl = 'https://direct-gcp-us-east1.api.carto.com'; -// PROD US GCP -const apiBaseUrl = 'https://gcp-us-east1.api.carto.com'; -// const apiBaseUrl = 'https://gcp-us-east1-06.dev.api.carto.com'; -// Localhost -// const apiBaseUrl = 'http://localhost:8002' - -const config = { - bigquery: { - h3: 'carto-dev-data.public.derived_spatialfeatures_che_h3res8_v1_yearly_v2', - h3int: 'carto-dev-data.public.derived_spatialfeatures_che_h3int_res8_v1_yearly_v2', - quadbin: 'carto-dev-data.public.derived_spatialfeatures_che_quadgrid15_v1_yearly_v2_quadbin' - }, - snowflake: { - h3: 'carto_dev_data.public.derived_spatialfeatures_che_h3res8_v1_yearly_v2', - h3int: 'carto_dev_data.public.derived_spatialfeatures_che_h3int_res8_v1_yearly_v2', - quadbin: 'carto_dev_data.public.derived_spatialfeatures_che_quadgrid15_v1_yearly_v2_quadbin' - }, - redshift: { - h3: 'carto_dev_data.public.derived_spatialfeatures_che_h3res8_v1_yearly_v2', - h3int: 'carto_dev_data.public.derived_spatialfeatures_che_h3int_res8_v1_yearly_v2', - quadbin: 'carto_dev_data.public.derived_spatialfeatures_che_quadgrid15_v1_yearly_v2_quadbin' - }, - postgres: { - h3: 'carto_dev_data.public.derived_spatialfeatures_esp_h3res8_v1_yearly_v2', - h3int: 'carto_dev_data.public.derived_spatialfeatures_esp_h3int_res8_v1_yearly_v2', - quadbin: 'carto_dev_data.public.derived_spatialfeatures_che_quadgrid15_v1_yearly_v2_quadbin' - }, - databricks: { - h3: 'cluster.carto_dev_data.derived_spatialfeatures_che_h3res8_v1_yearly_v2', - h3int: 'cluster.carto_dev_data.derived_spatialfeatures_che_h3int_res8_v1_yearly_v2' - } -}; - -const accessToken = 'XXX'; - -const showBasemap = true; -const showCarto = true; - -function Root() { - const [connection, setConnection] = useState('bigquery'); - const [dataset, setDataset] = useState('h3'); - const [zoom, setZoom] = useState(5); - const [formatTiles, setFormatTiles] = useState('binary'); - const table = config[connection][dataset]; - return ( - <> - - - - { - setConnection(c); - if (!config[c][dataset]) { - setDataset(Object.keys(config[c])[0]); - } - }} - /> - - - ); -} - -function createBasemap() { - return new GeoJsonLayer({ - id: 'base-map', - data: COUNTRIES, - // Styles - stroked: true, - filled: true, - lineWidthMinPixels: 2, - opacity: 0.4, - getLineColor: [60, 60, 60], - getFillColor: [200, 200, 200] - }); -} - -// Add aggregation expressions -function createCarto(connection, zoom, table, formatTiles) { - const isH3 = table.includes('h3'); - const isQuadbin = table.includes('quadbin'); - const geoColumn = isH3 - ? 'h3' - : isQuadbin - ? 'quadbin' - : table.endsWith('_quadkey') - ? 'quadkey' - : 'quadint'; - return new CartoLayer({ - id: 'carto', - connection, - data: table, - credentials: {accessToken, apiBaseUrl}, - - // Dynamic tiling. Request TILEJSON format with TABLE - type: MAP_TYPES.TABLE, - format: FORMATS.TILEJSON, - - // tile data format - formatTiles, - - // Aggregation - aggregationExp: 'avg(population) as value, 0.1*avg(population) as elevation, "test" as str', - aggregationResLevel: zoom, - geoColumn, - getQuadkey: d => d.id, - - // Visibilty (will be converted to H3 levels in the case of H3 tiles) - minZoom: 5, - maxZoom: 9, - - // autohighlight - pickable: true, - autoHighlight: true, - highlightColor: [33, 77, 255, 255], - - // Styling - getFillColor: d => [ - Math.pow((d.properties.value || d.properties.VALUE) / 200, 0.1) * 255, - 255 - (d.properties.value || d.properties.VALUE), - 79 - ], - getElevation: d => - 'elevation' in d.properties - ? d.properties.elevation - : d.properties.value || d.properties.VALUE, - extruded: true, - opacity: 0.3, - elevationScale: 100 - }); -} - -function ObjectSelect({title, obj, value, onSelect}) { - const keys = Object.values(obj).sort(); - return ( - <> - -

- - ); -} - -const container = document.body.appendChild(document.createElement('div')); -createRoot(container).render(); diff --git a/test/apps/carto-dynamic-tile/app.tsx b/test/apps/carto-dynamic-tile/app.tsx new file mode 100644 index 00000000000..65527ce3cd2 --- /dev/null +++ b/test/apps/carto-dynamic-tile/app.tsx @@ -0,0 +1,183 @@ +/* global document */ +/* eslint-disable no-console */ +import React, {useMemo, useState} from 'react'; +import {createRoot} from 'react-dom/client'; +import {StaticMap} from 'react-map-gl'; +import DeckGL from '@deck.gl/react'; +import { + CartoTilejsonResult, + cartoVectorTableSource, + H3TileLayer, + RasterTileLayer, + QuadbinTileLayer, + VectorTileLayer +} from '@deck.gl/carto'; +import datasets from './datasets'; +import {Layer} from '@deck.gl/core'; + +const MAP_STYLE = 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json'; +const INITIAL_VIEW_STATE = {longitude: -87.65, latitude: 41.82, zoom: 10}; + +const apiBaseUrl = 'https://gcp-us-east1.api.carto.com'; +const connectionName = 'bigquery'; + +const accessToken = 'XXX'; + +const globalOptions = {accessToken, apiBaseUrl, connectionName}; // apiBaseUrl not required + +function Root() { + const [dataset, setDataset] = useState(Object.keys(datasets)[0]); + const datasource = datasets[dataset]; + let layers: Layer[] = []; + + if (dataset.includes('h3')) { + layers = [useH3Layer(datasource)]; + } else if (dataset.includes('raster')) { + layers = [useRasterLayer(datasource)]; + } else if (dataset.includes('quadbin')) { + layers = [useQuadbinLayer(datasource)]; + } else if (dataset.includes('vector')) { + layers = [useVectorLayer(datasource)]; + } + + return ( + <> + { + const properties = object?.properties; + if (!properties) return null; + return Object.entries(properties) + .map(([k, v]) => `${k}: ${v}\n`) + .join(''); + }} + > + + + + + ); +} + +function useH3Layer(datasource) { + const {getFillColor, source, aggregationExp, columns, spatialDataColumn, sqlQuery, tableName} = + datasource; + // useMemo to avoid a map instantiation on every re-render + const tilejson = useMemo>(() => { + return source({ + ...globalOptions, + aggregationExp, + columns, + spatialDataColumn, + sqlQuery, + tableName + }); + }, [source, aggregationExp, columns, spatialDataColumn, sqlQuery, tableName]); + + return new H3TileLayer({ + id: 'carto', + data: tilejson, + pickable: true, + stroked: false, + getFillColor + }); +} + +function useRasterLayer(datasource) { + const {getFillColor, source, tableName} = datasource; + // useMemo to avoid a map instantiation on every re-render + const tilejson = useMemo>(() => { + return source({...globalOptions, tableName}); + }, [source, null, null, null, null, tableName]); + + return new RasterTileLayer({ + id: 'carto', + data: tilejson, // TODO how to correctly specify data type? + pickable: true, + getFillColor + }); +} + +function useQuadbinLayer(datasource) { + const {getFillColor, source, aggregationExp, columns, spatialDataColumn, sqlQuery, tableName} = + datasource; + // useMemo to avoid a map instantiation on every re-render + const tilejson = useMemo>(() => { + return source({ + ...globalOptions, + aggregationExp, + columns, + spatialDataColumn, + sqlQuery, + tableName + }); + }, [source, aggregationExp, columns, spatialDataColumn, sqlQuery, tableName]); + + return new QuadbinTileLayer({ + id: 'carto', + data: tilejson, + pickable: true, + stroked: false, + getFillColor + }); +} + +function useVectorLayer(datasource) { + const {getFillColor, source, columns, spatialDataColumn, sqlQuery, tableName} = datasource; + // useMemo to avoid a map instantiation on every re-render + const tilejson = useMemo>(() => { + return source({...globalOptions, columns, spatialDataColumn, sqlQuery, tableName}); + }, [source, null, columns, spatialDataColumn, sqlQuery, tableName]); + + return new VectorTileLayer({ + id: 'carto', + // @ts-ignore + data: tilejson, // TODO how to correctly specify data type? + pickable: true, + pointRadiusMinPixels: 5, + getFillColor + }); +} + +async function fetchLayerData() { + const data = await cartoVectorTableSource({ + ...globalOptions, + tableName: 'carto-demo-data.demo_tables.chicago_crime_sample', + format: 'geojson' + }); + // console.log(data.tiles); // <- Typescript error + console.log(data.features); // <- type: GeoJSON + console.log(data); +} +fetchLayerData(); + +function ObjectSelect({title, obj, value, onSelect}) { + const keys = Object.values(obj).sort() as string[]; + return ( + <> + +

+ + ); +} + +const container = document.body.appendChild(document.createElement('div')); +createRoot(container).render(); diff --git a/test/apps/carto-dynamic-tile/datasets.ts b/test/apps/carto-dynamic-tile/datasets.ts new file mode 100644 index 00000000000..8156dc1dfa0 --- /dev/null +++ b/test/apps/carto-dynamic-tile/datasets.ts @@ -0,0 +1,118 @@ +import { + cartoH3TilesetSource, + cartoH3TableSource, + cartoH3QuerySource, + cartoRasterSource, + cartoQuadbinTableSource, + cartoQuadbinTilesetSource, + cartoQuadbinQuerySource, + cartoVectorTableSource, + cartoVectorTilesetSource, + cartoVectorQuerySource, + colorBins +} from '@deck.gl/carto'; + +export default { + 'h3-query': { + source: cartoH3QuerySource, + sqlQuery: + 'select h3, population from carto-demo-data.demo_tables.derived_spatialfeatures_usa_h3res8_v1_yearly_v2', + aggregationExp: 'min(population) as population_min', + getFillColor: colorBins({ + attr: 'population_min', + domain: [10, 50, 100, 250, 500, 1000], + colors: 'BrwnYl' + }) + }, + 'h3-table': { + source: cartoH3TableSource, + tableName: 'carto-demo-data.demo_tables.derived_spatialfeatures_usa_h3res8_v1_yearly_v2', + aggregationExp: 'avg(population) as population_average', + getFillColor: colorBins({ + attr: 'population_average', + domain: [10, 50, 100, 250, 500, 1000], + colors: 'SunsetDark' + }) + }, + 'h3-tileset': { + source: cartoH3TilesetSource, + tableName: + 'carto-demo-data.demo_tilesets.derived_spatialfeatures_usa_h3res8_v1_yearly_v2_tileset', + getFillColor: colorBins({ + attr: 'retail', + domain: [1, 2, 3, 5, 8, 11], + colors: 'Earth' + }) + }, + 'quadbin-query': { + source: cartoQuadbinQuerySource, + sqlQuery: + 'select quadbin, population from carto-demo-data.demo_tables.derived_spatialfeatures_usa_quadbin15_v1_yearly_v2', + aggregationExp: 'min(population) as population_min', + getFillColor: colorBins({ + attr: 'population_min', + domain: [10, 50, 100, 250, 500, 1000], + colors: 'BrwnYl' + }) + }, + 'quadbin-table': { + source: cartoQuadbinTableSource, + tableName: 'carto-demo-data.demo_tables.derived_spatialfeatures_usa_quadbin15_v1_yearly_v2', + aggregationExp: 'avg(population) as population_average', + getFillColor: colorBins({ + attr: 'population_average', + domain: [10, 50, 100, 250, 500, 1000], + colors: 'SunsetDark' + }) + }, + 'quadbin-tileset': { + source: cartoQuadbinTilesetSource, + tableName: + 'carto-demo-data.demo_tilesets.derived_spatialfeatures_usa_quadbin15_v1_yearly_v2_tileset', + getFillColor: colorBins({ + attr: 'avg_retail', + domain: [1, 2, 3, 5, 8, 11], + colors: 'Earth' + }) + }, + raster: { + source: cartoRasterSource, + tableName: 'cartodb-data-engineering-team.jarroyo_raster.sdsc23_5_quadbin', + getFillColor: colorBins({ + attr: 'band_1', + domain: [0, 5, 10, 15, 20, 25, 30], + colors: 'Temps' + }) + }, + 'vector-query': { + source: cartoVectorQuerySource, + sqlQuery: + 'select geom, district from carto-demo-data.demo_tables.chicago_crime_sample where year > 2016', + getFillColor: [255, 0, 0] + }, + 'vector-table': { + source: cartoVectorTableSource, + tableName: 'carto-demo-data.demo_tables.chicago_crime_sample', + columns: ['date', 'year'], + getFillColor: colorBins({ + attr: 'year', + domain: [2002, 2006, 2010, 2016, 2020], + colors: 'Magenta' + }) + }, + 'vector-table-dynamic': { + source: cartoVectorTableSource, + tableName: 'carto-demo-data.demo_tables.osm_buildings_usa', + spatialDataColumn: 'geog', + getFillColor: [131, 44, 247] + }, + 'vector-tileset': { + source: cartoVectorTilesetSource, + tableName: 'carto-demo-data.demo_tilesets.sociodemographics_usa_blockgroup', + getFillColor: colorBins({ + attr: 'income_per_capita', + domain: [15000, 25000, 35000, 45000, 60000], + colors: 'OrYel' + }) + } +}; diff --git a/test/apps/carto-dynamic-tile/index.html b/test/apps/carto-dynamic-tile/index.html index 967ce3f7aab..774628d4126 100644 --- a/test/apps/carto-dynamic-tile/index.html +++ b/test/apps/carto-dynamic-tile/index.html @@ -8,6 +8,6 @@ - + diff --git a/test/modules/carto/api/maps-api-client.spec.ts b/test/modules/carto/api/maps-api-client.spec.ts index e8ef469485b..d4629cd3b48 100644 --- a/test/modules/carto/api/maps-api-client.spec.ts +++ b/test/modules/carto/api/maps-api-client.spec.ts @@ -8,7 +8,6 @@ import { MAP_TYPES, _getDataV2, fetchLayerData, - fetchMap, getDefaultCredentials, setDefaultCredentials } from '@deck.gl/carto'; @@ -19,7 +18,7 @@ import { TILESTATS_RESPONSE, mockFetchMapsV3 } from '../mock-fetch'; -import {EMPTY_KEPLER_MAP_CONFIG} from './parseMap.spec'; +// import {EMPTY_KEPLER_MAP_CONFIG} from './parseMap.spec'; for (const useSetDefaultCredentials of [true, false]) { test(`getDataV2#v1#setDefaultCredentials(${String(useSetDefaultCredentials)})`, async t => { @@ -609,7 +608,7 @@ test('fetchLayerData#post', async t => { t.end(); }); -test('fetchMap#no datasets', async t => { +/* test('fetchMap#no datasets', async t => { const cartoMapId = 'abcd-1234'; const mapUrl = `http://carto-api/v3/maps/public/${cartoMapId}`; const mapResponse = {id: cartoMapId, datasets: [], keplerMapConfig: EMPTY_KEPLER_MAP_CONFIG}; @@ -861,4 +860,4 @@ test('fetchMap#geoColumn', async t => { globalThis.fetch = fetch; t.end(); -}); +}); */ diff --git a/test/modules/carto/index.ts b/test/modules/carto/index.ts index 64d08503cdf..a363443a14b 100644 --- a/test/modules/carto/index.ts +++ b/test/modules/carto/index.ts @@ -1,11 +1,10 @@ import './api/carto-api-error.spec'; import './api/layer-map.spec'; -import './api/parseMap.spec'; +// import './api/parseMap.spec'; import './api/maps-api-client.spec'; -import './carto-layer.spec'; +// import './carto-layer.spec'; import './config.spec'; import './utils.spec'; -import './layers/carto-tile-layer.spec'; import './layers/carto-vector-tile.spec'; import './layers/h3-tile-layer.spec'; import './layers/h3-tileset-2d.spec'; @@ -15,6 +14,7 @@ import './layers/point-label-layer.spec'; import './layers/quadbin-layer.spec'; import './layers/quadbin-tile-layer.spec'; import './layers/quadbin-tileset-2d.spec'; +// import './layers/vector-tile-layer.spec'; import './style/carto-color-bins.spec'; import './style/carto-color-categories.spec'; import './style/carto-color-continuous.spec'; diff --git a/test/modules/carto/layers/h3-tile-layer.spec.ts b/test/modules/carto/layers/h3-tile-layer.spec.ts index fed73213db6..c57233c1ad4 100644 --- a/test/modules/carto/layers/h3-tile-layer.spec.ts +++ b/test/modules/carto/layers/h3-tile-layer.spec.ts @@ -1,7 +1,7 @@ import {getResolution, cellToChildren} from 'h3-js'; import test from 'tape-promise/tape'; import {generateLayerTests, testLayerAsync} from '@deck.gl/test-utils'; -import {_H3TileLayer as H3TileLayer} from '@deck.gl/carto'; +import {H3TileLayer} from '@deck.gl/carto'; import {WebMercatorViewport} from '@deck.gl/core'; import {testPickingLayer} from '../../layers/test-picking-layer'; diff --git a/test/modules/carto/layers/quadbin-tile-layer.spec.ts b/test/modules/carto/layers/quadbin-tile-layer.spec.ts index ebd6f4eb361..0f8e5c9b515 100644 --- a/test/modules/carto/layers/quadbin-tile-layer.spec.ts +++ b/test/modules/carto/layers/quadbin-tile-layer.spec.ts @@ -1,6 +1,6 @@ import test from 'tape-promise/tape'; import {generateLayerTests, testLayerAsync} from '@deck.gl/test-utils'; -import {_QuadbinTileLayer as QuadbinTileLayer} from '@deck.gl/carto'; +import {QuadbinTileLayer} from '@deck.gl/carto'; import {renderSubLayers} from '@deck.gl/carto/layers/quadbin-tile-layer'; import {WebMercatorViewport} from '@deck.gl/core'; import {testPickingLayer} from '../../layers/test-picking-layer'; diff --git a/test/modules/carto/layers/raster-tile-layer.spec.ts b/test/modules/carto/layers/raster-tile-layer.spec.ts index 45e70bf295b..bde9f218b79 100644 --- a/test/modules/carto/layers/raster-tile-layer.spec.ts +++ b/test/modules/carto/layers/raster-tile-layer.spec.ts @@ -1,6 +1,6 @@ import test from 'tape-promise/tape'; import {generateLayerTests, testLayerAsync} from '@deck.gl/test-utils'; -import {_RasterTileLayer as RasterTileLayer} from '@deck.gl/carto'; +import {RasterTileLayer} from '@deck.gl/carto'; import RasterLayer from '@deck.gl/carto/layers/raster-layer'; import binaryRasterTileData from '../data/binaryRasterTile.json'; // tile 487624ffffffffff diff --git a/test/modules/carto/layers/vector-tile-layer.spec.ts b/test/modules/carto/layers/vector-tile-layer.spec.ts new file mode 100644 index 00000000000..70dd2a1ce90 --- /dev/null +++ b/test/modules/carto/layers/vector-tile-layer.spec.ts @@ -0,0 +1,84 @@ +import test from 'tape-promise/tape'; +import {VectorTileLayer} from '@deck.gl/carto'; +import {geojsonToBinary} from '@loaders.gl/gis'; +import {testPickingLayer} from '../../layers/test-picking-layer'; +import {WebMercatorViewport} from '@deck.gl/core'; + +const geoJSONData = [ + { + id: 1, + type: 'Feature', + geometry: { + type: 'Point', + // Unlike MVT, coordinates are not relative to the tile, but [lng, lat] + coordinates: [-123, 45] + }, + properties: { + cartodb_id: 148 + } + } +]; + +const geoJSONBinaryData = geojsonToBinary(JSON.parse(JSON.stringify(geoJSONData))); + +test(`VectorTileLayer#picking`, async t => { + class TestVectorTileLayer extends VectorTileLayer { + getTileData() { + return geoJSONBinaryData; + } + } + + TestVectorTileLayer.layerName = 'TestVectorTileLayer'; + + await testPickingLayer({ + layer: new TestVectorTileLayer({ + id: 'mvt', + binary: true, + data: ['https://json_2/{z}/{x}/{y}.mvt'], + uniqueIdProperty: 'cartodb_id', + autoHighlight: true + }), + viewport: new WebMercatorViewport({ + latitude: 0, + longitude: 0, + zoom: 1 + }), + testCases: [ + { + pickedColor: new Uint8Array([1, 0, 0, 0]), + pickedLayerId: 'mvt-0-0-1-points-circle', + mode: 'hover', + onAfterUpdate: ({layer, subLayers, info}) => { + t.comment('hover over polygon'); + t.ok(info.object, 'info.object is populated'); + t.ok(info.object.properties, 'info.object.properties is populated'); + t.ok(info.object.geometry, 'info.object.geometry is populated'); + t.deepEqual( + info.object.geometry.coordinates, + [-123, 45], + 'picked coordinates are correct' + ); + t.ok( + subLayers.every(l => l.props.highlightedObjectIndex === 0), + 'set sub layers highlightedObjectIndex' + ); + } + }, + { + pickedColor: new Uint8Array([0, 0, 0, 0]), + pickedLayerId: '', + mode: 'hover', + onAfterUpdate: ({layer, subLayers, info}) => { + t.comment('pointer leave'); + t.notOk(info.object, 'info.object is not populated'); + t.ok( + subLayers.every(l => l.props.highlightedObjectIndex === -1), + 'cleared sub layers highlightedObjectIndex' + ); + } + } + ] + }); + + t.end(); +});