UNPKG

@deck.gl/carto

Version:

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

219 lines 9.42 kB
// deck.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors /* eslint-disable @typescript-eslint/no-shadow */ import { DEFAULT_API_BASE_URL, CartoAPIError, buildPublicMapUrl, buildStatsUrl, h3QuerySource, h3TableSource, quadbinQuerySource, quadbinTableSource, vectorQuerySource, vectorTableSource, vectorTilesetSource, requestWithParameters } from '@carto/api-client'; import { parseMap } from "./parse-map.js"; import { assert } from "../utils.js"; import { fetchBasemapProps } from "./basemap.js"; import { DEFAULT_AGGREGATION_EXP } from "../constants.js"; /* global clearInterval, setInterval, URL */ /* eslint-disable complexity, max-statements, max-params */ async function _fetchMapDataset(dataset, context) { const { aggregationExp, aggregationResLevel, connectionName, columns, format, geoColumn, source, type, queryParameters } = dataset; const cache = {}; const globalOptions = { ...context, cache, connectionName, format }; if (type === 'tileset') { // TODO do we want a generic tilesetSource? // @ts-ignore dataset.data = await vectorTilesetSource({ ...globalOptions, tableName: source }); } else { const [spatialDataType, spatialDataColumn] = geoColumn ? geoColumn.split(':') : ['geom']; if (spatialDataType === 'geom') { const options = { ...globalOptions, spatialDataColumn }; if (type === 'table') { dataset.data = await vectorTableSource({ ...options, columns, tableName: source }); } else if (type === 'query') { dataset.data = await vectorQuerySource({ ...options, columns, sqlQuery: source, queryParameters }); } } else if (spatialDataType === 'h3') { const options = { ...globalOptions, aggregationExp: aggregationExp || DEFAULT_AGGREGATION_EXP, aggregationResLevel, spatialDataColumn }; if (type === 'table') { dataset.data = await h3TableSource({ ...options, tableName: source }); } else if (type === 'query') { dataset.data = await h3QuerySource({ ...options, sqlQuery: source, queryParameters }); } } else if (spatialDataType === 'quadbin') { const options = { ...globalOptions, aggregationExp: aggregationExp || DEFAULT_AGGREGATION_EXP, aggregationResLevel, spatialDataColumn }; if (type === 'table') { dataset.data = await quadbinTableSource({ ...options, tableName: source }); } else if (type === 'query') { dataset.data = await quadbinQuerySource({ ...options, sqlQuery: source, queryParameters }); } } } let cacheChanged = true; if (cache.value) { cacheChanged = dataset.cache !== cache.value; dataset.cache = cache.value; } return cacheChanged; } async function _fetchTilestats(attribute, dataset, context) { const { connectionName, data, id, source, type, queryParameters } = dataset; const { apiBaseUrl } = context; const errorContext = { requestType: 'Tile stats', connection: connectionName, type, source }; if (!('tilestats' in data)) { throw new CartoAPIError(new Error(`Invalid dataset for tilestats: ${id}`), errorContext); } const baseUrl = buildStatsUrl({ attribute, apiBaseUrl, ...dataset }); const client = new URLSearchParams(data.tiles[0]).get('client'); const headers = { Authorization: `Bearer ${context.accessToken}` }; const parameters = {}; if (client) { parameters.client = client; } if (type === 'query') { parameters.q = source; if (queryParameters) { parameters.queryParameters = JSON.stringify(queryParameters); } } const stats = await requestWithParameters({ baseUrl, headers, parameters, errorContext, maxLengthURL: context.maxLengthURL }); // Replace tilestats for attribute with value from API const { attributes } = data.tilestats.layers[0]; const index = attributes.findIndex(d => d.attribute === attribute); attributes[index] = stats; return true; } async function fillInMapDatasets({ datasets }, context) { const promises = datasets.map(dataset => _fetchMapDataset(dataset, context)); return await Promise.all(promises); } async function fillInTileStats({ datasets, keplerMapConfig }, context) { const attributes = []; const { layers } = keplerMapConfig.config.visState; for (const layer of layers) { for (const channel of Object.keys(layer.visualChannels)) { const attribute = layer.visualChannels[channel]?.name; if (attribute) { const dataset = datasets.find(d => d.id === layer.config.dataId); if (dataset && dataset.type !== 'tileset' && dataset.data.tilestats) { // Only fetch stats for QUERY & TABLE map types attributes.push({ attribute, dataset }); } } } } // Remove duplicates to avoid repeated requests const filteredAttributes = []; for (const a of attributes) { if (!filteredAttributes.find(({ attribute, dataset }) => attribute === a.attribute && dataset === a.dataset)) { filteredAttributes.push(a); } } const promises = filteredAttributes.map(({ attribute, dataset }) => _fetchTilestats(attribute, dataset, context)); return await Promise.all(promises); } /* eslint-disable max-statements */ export async function fetchMap({ accessToken, apiBaseUrl = DEFAULT_API_BASE_URL, cartoMapId, clientId, headers, autoRefresh, onNewData, maxLengthURL }) { assert(cartoMapId, 'Must define CARTO map id: fetchMap({cartoMapId: "XXXX-XXXX-XXXX"})'); if (accessToken) { headers = { Authorization: `Bearer ${accessToken}`, ...headers }; } if (autoRefresh || onNewData) { assert(onNewData, 'Must define `onNewData` when using autoRefresh'); assert(typeof onNewData === 'function', '`onNewData` must be a function'); assert(typeof autoRefresh === 'number' && autoRefresh > 0, '`autoRefresh` must be a positive number'); } const baseUrl = buildPublicMapUrl({ apiBaseUrl, cartoMapId }); const errorContext = { requestType: 'Public map', mapId: cartoMapId }; const map = await requestWithParameters({ baseUrl, headers, errorContext, maxLengthURL }); const context = { accessToken: map.token || accessToken, apiBaseUrl, clientId, headers, maxLengthURL }; // Periodically check if the data has changed. Note that this // will not update when a map is published. let stopAutoRefresh; if (autoRefresh) { // eslint-disable-next-line @typescript-eslint/no-misused-promises const intervalId = setInterval(async () => { const changed = await fillInMapDatasets(map, { ...context, headers: { ...headers, 'If-Modified-Since': new Date().toUTCString() } }); if (onNewData && changed.some(v => v === true)) { onNewData(parseMap(map)); } }, autoRefresh * 1000); stopAutoRefresh = () => { clearInterval(intervalId); }; } const geojsonLayers = map.keplerMapConfig.config.visState.layers.filter(({ type }) => type === 'geojson' || type === 'point'); const geojsonDatasetIds = geojsonLayers.map(({ config }) => config.dataId); map.datasets.forEach(dataset => { if (geojsonDatasetIds.includes(dataset.id)) { const { config } = geojsonLayers.find(({ config }) => config.dataId === dataset.id); dataset.format = 'geojson'; // Support for very old maps. geoColumn was not stored in the past if (!dataset.geoColumn && config.columns.geojson) { dataset.geoColumn = config.columns.geojson; } } }); const [basemap] = await Promise.all([ fetchBasemapProps({ config: map.keplerMapConfig.config, errorContext }), // Mutates map.datasets so that dataset.data contains data fillInMapDatasets(map, context) ]); // Mutates attributes in visualChannels to contain tile stats await fillInTileStats(map, context); const out = { ...parseMap(map), basemap, ...{ stopAutoRefresh } }; const textLayers = out.layers.filter(layer => { const pointType = layer.props.pointType || ''; return pointType.includes('text'); }); /* global FontFace, window, document */ if (textLayers.length && window.FontFace && !document.fonts.check('12px Inter')) { // Fetch font needed for labels const font = new FontFace('Inter', 'url(https://fonts.gstatic.com/s/inter/v12/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2)'); await font.load().then(f => document.fonts.add(f)); } return out; } //# sourceMappingURL=fetch-map.js.map