From 8db4485e8f534e230006ef0e0dcf0fc567bbc66b Mon Sep 17 00:00:00 2001 From: Alberto Asuero <1161870+alasarr@users.noreply.github.com> Date: Sat, 24 Oct 2020 07:35:13 +0200 Subject: [PATCH] Maps API v2 at @deck.gl/carto (#5053) --- docs/api-reference/carto/carto-sql-layer.md | 21 +-- docs/api-reference/carto/overview.md | 22 +++ modules/carto/src/api/maps-api-client.js | 138 +++++------------- modules/carto/src/auth.js | 18 --- modules/carto/src/config.js | 33 +++++ modules/carto/src/index.js | 2 +- .../carto/src/layers/carto-bqtiler-layer.js | 26 ++-- modules/carto/src/layers/carto-layer.js | 12 +- modules/carto/src/layers/carto-sql-layer.js | 54 ++++++- .../carto/{auth.spec.js => config.spec.js} | 16 +- test/modules/carto/index.js | 2 +- 11 files changed, 182 insertions(+), 162 deletions(-) delete mode 100644 modules/carto/src/auth.js create mode 100644 modules/carto/src/config.js rename test/modules/carto/{auth.spec.js => config.spec.js} (60%) diff --git a/docs/api-reference/carto/carto-sql-layer.md b/docs/api-reference/carto/carto-sql-layer.md index 1ac679eb88d..73b01fd3dfa 100644 --- a/docs/api-reference/carto/carto-sql-layer.md +++ b/docs/api-reference/carto/carto-sql-layer.md @@ -84,22 +84,15 @@ Optional. Object with the credentials to connect with CARTO. ```js { username: 'public', - apiKey: 'default_public', - serverUrlTemplate: 'https://{user}.carto.com' + apiKey: 'default_public' } ``` ##### `bufferSize` (Number) -Optional. MVT BufferSize in pixels - -* Default: `1` - -##### `version` (String) - -Optional. MapConfig version +Optional. MVT BufferSize in tile coordinate space as defined by MVT specification -* Default: `1.3.1` +* Default: `16` ##### `tileExtent` (String) @@ -114,21 +107,21 @@ Optional. Tile extent in tile coordinate space as defined by MVT specification. `onDataLoad` is called when the request to the CARTO tiler was completed successfully. -- Default: `tilejson => {}` +* Default: `tilejson => {}` Receives arguments: -- `tilejson` (Object) - the response from the tiler service +* `tilejson` (Object) - the response from the tiler service ##### `onDataError` (Function, optional) `onDataError` is called when the request to the CARTO tiler failed. -- Default: `console.error` +* Default: `console.error` Receives arguments: -- `error` (`Error`) +* `error` (`Error`) ## Source diff --git a/docs/api-reference/carto/overview.md b/docs/api-reference/carto/overview.md index cc04be6d508..c27a7edc607 100644 --- a/docs/api-reference/carto/overview.md +++ b/docs/api-reference/carto/overview.md @@ -54,3 +54,25 @@ You can see real examples for the following: * [Pure JS](https://github.com/CartoDB/viz-doc/tree/master/deck.gl/examples/pure-js): integrate in a pure js application, using webpack. + +### CARTO credentials + +This is an object to define the connection to CARTO, including the credentials (and optionally the parameters to point to specific api endpoints): + +* username (required): unique username in the platform +* apiKey (optional): api key. default to `public_user` +* region (optional): region of the user, posible values are `us` or `eu`. Only need to be specified if you've specifically requested an account in `eu`. +* sqlUrl (optional): SQL API URL Template. Default to `https://{user}.carto.com/api/v2/sql`, +* mapsUrl (optional): MAPS API URL Template. Default to `https://{user}.carto.com/api/v1/map` + +If you're an on-premise user or you're running CARTO from [Google's Market place](https://console.cloud.google.com/marketplace/details/cartodb-public/carto-enterprise-payg), you need to set the URLs to point to your instance. + +```js +setDefaultCredentials({ + username: 'public', + apiKey: 'default_public', + mapsUrl: 'https:///user/{user}/api/v1/map', + sqlUrl: 'https:///user/{user}/api/v2/sql', +}); +``` + diff --git a/modules/carto/src/api/maps-api-client.js b/modules/carto/src/api/maps-api-client.js index db07253451c..5dc1ca905dd 100644 --- a/modules/carto/src/api/maps-api-client.js +++ b/modules/carto/src/api/maps-api-client.js @@ -1,76 +1,61 @@ -import {getDefaultCredentials} from '../auth'; +import {getDefaultCredentials, getMapsVersion} from '../config'; const DEFAULT_USER_COMPONENT_IN_URL = '{user}'; -const REQUEST_GET_MAX_URL_LENGTH = 2048; +const DEFAULT_REGION_COMPONENT_IN_URL = '{region}'; /** - * Obtain a TileJson from Maps API v1 + * Obtain a TileJson from Maps API v1 and v2 */ -export async function getMapTileJSON(props) { - const {data, bufferSize, version, tileExtent, credentials} = props; +export async function getTileJSON(mapConfig, credentials) { const creds = {...getDefaultCredentials(), ...credentials}; + switch (getMapsVersion(creds)) { + case 'v1': + // Maps API v1 + const layergroup = await instantiateMap({mapConfig, credentials: creds}); + return layergroup.metadata.tilejson.vector; - const mapConfig = createMapConfig({data, bufferSize, version, tileExtent}); - const layergroup = await instantiateMap({mapConfig, credentials: creds}); + case 'v2': + // Maps API v2 + return await instantiateMap({mapConfig, credentials: creds}); - const tiles = layergroup.metadata.tilejson.vector; - return tiles; -} - -/** - * Create a mapConfig for Maps API - */ -function createMapConfig({data, bufferSize, version, tileExtent}) { - const isSQL = data.search(' ') > -1; - const sql = isSQL ? data : `SELECT * FROM ${data}`; - - const mapConfig = { - version, - buffersize: { - mvt: bufferSize - }, - layers: [ - { - type: 'mapnik', - options: { - sql: sql.trim(), - vector_extent: tileExtent - } - } - ] - }; - return mapConfig; + default: + throw new Error('Invalid maps API version. It shoud be v1 or v2'); + } } /** - * Instantiate a map, either by GET or POST, using Maps API + * Instantiate a map using Maps API */ async function instantiateMap({mapConfig, credentials}) { + const url = buildURL({mapConfig, credentials}); + let response; try { - const config = JSON.stringify(mapConfig); - const request = createMapsApiRequest({config, credentials}); /* global fetch */ /* eslint no-undef: "error" */ - response = await fetch(request); + response = await fetch(url, { + headers: { + Accept: 'application/json' + } + }); } catch (error) { throw new Error(`Failed to connect to Maps API: ${error}`); } - const layergroup = await response.json(); + const json = await response.json(); if (!response.ok) { - dealWithWindshaftError({response, layergroup, credentials}); + dealWithError({response, json, credentials}); } - return layergroup; + return json; } /** * Display proper message from Maps API error */ -function dealWithWindshaftError({response, layergroup, credentials}) { +function dealWithError({response, json, credentials}) { switch (response.status) { case 401: throw new Error( @@ -84,78 +69,31 @@ function dealWithWindshaftError({response, layergroup, credentials}) { credentials.apiKey }') doesn't provide access to the requested data` ); + default: - throw new Error(`${JSON.stringify(layergroup.errors)}`); + const e = getMapsVersion() === 'v1' ? JSON.stringify(json.errors) : json.error; + throw new Error(e); } } /** - * Create a GET or POST request, with all required parameters + * Build a URL with all required parameters */ -function createMapsApiRequest({config, credentials}) { +function buildURL({mapConfig, credentials}) { + const cfg = JSON.stringify(mapConfig); const encodedApiKey = encodeParameter('api_key', credentials.apiKey); const encodedClient = encodeParameter('client', `deck-gl-carto`); const parameters = [encodedApiKey, encodedClient]; - const url = generateMapsApiUrl(parameters, credentials); - - const getUrl = `${url}&${encodeParameter('config', config)}`; - if (getUrl.length < REQUEST_GET_MAX_URL_LENGTH) { - return getRequest(getUrl); - } - - return postRequest(url, config); -} - -/** - * Generate a Maps API url for the request - */ -function generateMapsApiUrl(parameters, credentials) { - const base = `${serverURL(credentials)}api/v1/map`; - return `${base}?${parameters.join('&')}`; + return `${mapsUrl(credentials)}?${parameters.join('&')}&${encodeParameter('config', cfg)}`; } /** * Prepare a url valid for the specified user */ -function serverURL(credentials) { - let url = credentials.serverUrlTemplate.replace( - DEFAULT_USER_COMPONENT_IN_URL, - credentials.username - ); - - if (!url.endsWith('/')) { - url += '/'; - } - - return url; -} - -/** - * Simple GET request - */ -function getRequest(url) { - /* global Request */ - /* eslint no-undef: "error" */ - return new Request(url, { - method: 'GET', - headers: { - Accept: 'application/json' - } - }); -} - -/** - * Simple POST request - */ -function postRequest(url, payload) { - return new Request(url, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: payload - }); +function mapsUrl(credentials) { + return credentials.mapsUrl + .replace(DEFAULT_USER_COMPONENT_IN_URL, credentials.username) + .replace(DEFAULT_REGION_COMPONENT_IN_URL, credentials.region); } /** diff --git a/modules/carto/src/auth.js b/modules/carto/src/auth.js deleted file mode 100644 index 4d03292d9f2..00000000000 --- a/modules/carto/src/auth.js +++ /dev/null @@ -1,18 +0,0 @@ -const defaultCredentials = { - username: 'public', - apiKey: 'default_public', - serverUrlTemplate: 'https://{user}.carto.com' -}; - -let credentials = defaultCredentials; - -export function setDefaultCredentials(opts) { - credentials = { - ...credentials, - ...opts - }; -} - -export function getDefaultCredentials() { - return credentials; -} diff --git a/modules/carto/src/config.js b/modules/carto/src/config.js new file mode 100644 index 00000000000..e45c045883d --- /dev/null +++ b/modules/carto/src/config.js @@ -0,0 +1,33 @@ +const defaultCredentials = { + username: 'public', + apiKey: 'default_public', + region: 'us', + // Set to null to guess from mapsUrl attribute. Other values are 'v1' or 'v2' + mapsVersion: null, + // SQL API URL + sqlUrl: 'https://{user}.carto.com/api/v2/sql', + // Maps API URL + mapsUrl: 'https://maps-api-v2.{region}.carto.com/user/{user}/map' +}; + +let credentials = defaultCredentials; + +export function setDefaultCredentials(opts) { + credentials = { + ...credentials, + ...opts + }; +} + +export function getDefaultCredentials() { + return credentials; +} + +export function getMapsVersion(creds) { + const localCreds = creds || credentials; + if (localCreds.mapsVersion) { + return localCreds.mapsVersion; + } + + return localCreds.mapsUrl.includes('api/v1/map') ? 'v1' : 'v2'; +} diff --git a/modules/carto/src/index.js b/modules/carto/src/index.js index de683136e5d..d59f9342fbb 100644 --- a/modules/carto/src/index.js +++ b/modules/carto/src/index.js @@ -1,4 +1,4 @@ -export {getDefaultCredentials, setDefaultCredentials} from './auth.js'; +export {getDefaultCredentials, setDefaultCredentials} from './config.js'; export {default as CartoSQLLayer} from './layers/carto-sql-layer'; export {default as CartoBQTilerLayer} from './layers/carto-bqtiler-layer'; export {default as BASEMAP} from './basemap'; diff --git a/modules/carto/src/layers/carto-bqtiler-layer.js b/modules/carto/src/layers/carto-bqtiler-layer.js index cec8d5f3c1a..6973a239e95 100644 --- a/modules/carto/src/layers/carto-bqtiler-layer.js +++ b/modules/carto/src/layers/carto-bqtiler-layer.js @@ -1,19 +1,19 @@ import CartoLayer from './carto-layer'; -const BQ_TILEJSON_ENDPOINT = 'https://us-central1-cartobq.cloudfunctions.net/tilejson'; - export default class CartoBQTilerLayer extends CartoLayer { - async _updateTileJSON() { - /* global fetch */ - /* eslint no-undef: "error" */ - const response = await fetch(`${BQ_TILEJSON_ENDPOINT}?t=${this.props.data}`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - } - }); - const tilejson = await response.json(); - return tilejson; + buildMapConfig() { + return { + version: '2.0.0', + layers: [ + { + type: 'tileset', + source: 'bigquery', + options: { + tileset: this.props.data + } + } + ] + }; } } diff --git a/modules/carto/src/layers/carto-layer.js b/modules/carto/src/layers/carto-layer.js index 2f25312005f..251dd8e4b09 100644 --- a/modules/carto/src/layers/carto-layer.js +++ b/modules/carto/src/layers/carto-layer.js @@ -1,12 +1,13 @@ import {CompositeLayer} from '@deck.gl/core'; import {MVTLayer} from '@deck.gl/geo-layers'; +import {getTileJSON} from '../api/maps-api-client'; const defaultProps = { data: null, credentials: null, onDataLoad: {type: 'function', value: tilejson => {}, compare: false}, // eslint-disable-next-line - onDataLoadError: {type: 'function', value: err => console.error(err), compare: false} + onDataError: {type: 'function', value: err => console.error(err), compare: false} }; export default class CartoLayer extends CompositeLayer { @@ -33,8 +34,13 @@ export default class CartoLayer extends CompositeLayer { } } + buildMapConfig() { + throw new Error('You must use one of the specific carto layers: BQ or SQL type'); + } + async _updateTileJSON() { - throw new Error(`You must use one of the specific carto layers: BQ or SQL type`); + const tilejson = await getTileJSON(this.buildMapConfig(this.props), this.props.credentials); + return tilejson; } onHover(info, pickingEvent) { @@ -51,7 +57,7 @@ export default class CartoLayer extends CompositeLayer { this.props, this.getSubLayerProps({ id: 'mvt', - data: this.state.tilejson.tiles, + data: this.state.tilejson, updateTriggers }) ); diff --git a/modules/carto/src/layers/carto-sql-layer.js b/modules/carto/src/layers/carto-sql-layer.js index 326e91b4f25..11b9b9a063b 100644 --- a/modules/carto/src/layers/carto-sql-layer.js +++ b/modules/carto/src/layers/carto-sql-layer.js @@ -1,17 +1,59 @@ import CartoLayer from './carto-layer'; -import {getMapTileJSON} from '../api/maps-api-client'; +import {getMapsVersion} from '../config'; const defaultProps = { - version: '1.3.1', // MapConfig Version (Maps API) - bufferSize: 1, // MVT buffersize in pixels, + bufferSize: 16, // MVT buffersize in pixels, tileExtent: 4096, // Tile extent in tile coordinate space (MVT spec.) uniqueIdProperty: 'cartodb_id' }; export default class CartoSQLLayer extends CartoLayer { - async _updateTileJSON() { - const tilejson = await getMapTileJSON(this.props); - return tilejson; + buildMapConfig() { + const {data, bufferSize, tileExtent} = this.props; + + const version = getMapsVersion(this.props.creds); + const isSQL = data.search(' ') > -1; + const sql = isSQL ? data : `SELECT * FROM ${data}`; + + switch (version) { + case 'v1': + // Map config v1 + return { + version: '1.3.1', + buffersize: { + mvt: bufferSize + }, + layers: [ + { + type: 'mapnik', + options: { + sql: sql.trim(), + vector_extent: tileExtent + } + } + ] + }; + + case 'v2': + // Map config v2 + return { + version: '2.0.0', + buffer_size: bufferSize, + tile_extent: tileExtent, + layers: [ + { + type: 'query', + source: 'postgres', + options: { + sql: sql.trim(), + vector_extent: tileExtent + } + } + ] + }; + default: + throw new Error(`Cannot build MapConfig for unmatching version ${version}`); + } } } diff --git a/test/modules/carto/auth.spec.js b/test/modules/carto/config.spec.js similarity index 60% rename from test/modules/carto/auth.spec.js rename to test/modules/carto/config.spec.js index 2cd0a9d3786..c2dc6751348 100644 --- a/test/modules/carto/auth.spec.js +++ b/test/modules/carto/config.spec.js @@ -1,35 +1,39 @@ import test from 'tape-catch'; import {getDefaultCredentials, setDefaultCredentials} from '@deck.gl/carto'; -test('auth#getDefaultCredentials', t => { +test('config#getDefaultCredentials', t => { const credentials = getDefaultCredentials(); t.notOk(credentials === null, 'default credentials available'); t.end(); }); -test('auth#getDefaultCredentials', t => { +test('config#getDefaultCredentials', t => { // partial (keeping other params) setDefaultCredentials({username: 'a-new-username'}); let credentials = getDefaultCredentials(); t.ok(credentials.username === 'a-new-username', 'user update'); t.ok(credentials.apiKey === 'default_public', 'keep default apiKey'); t.ok( - credentials.serverUrlTemplate === 'https://{user}.carto.com', - 'keep default serverUrlTemplate' + credentials.mapsUrl === 'https://maps-api-v2.{region}.carto.com/user/{user}/map', + 'keep default mapsUrl' ); + t.ok(credentials.sqlUrl === 'https://{user}.carto.com/api/v2/sql', 'keep default sqlUrl'); + const baseUrl = 'https://a-custom-{user}.carto.com'; // full setDefaultCredentials({ username: 'a-new-username', apiKey: 'a-new-key', - serverUrlTemplate: 'https://a-custom-{user}.carto.com' + sqlUrl: `${baseUrl}/sql`, + mapsUrl: `${baseUrl}/map` }); credentials = getDefaultCredentials(); t.ok(credentials.username === 'a-new-username', ''); t.ok(credentials.apiKey === 'a-new-key'); - t.ok(credentials.serverUrlTemplate === 'https://a-custom-{user}.carto.com'); + t.ok(credentials.sqlUrl === `${baseUrl}/sql`); + t.ok(credentials.mapsUrl === `${baseUrl}/map`); t.end(); }); diff --git a/test/modules/carto/index.js b/test/modules/carto/index.js index 2330e3c943f..16ef0620981 100644 --- a/test/modules/carto/index.js +++ b/test/modules/carto/index.js @@ -1,3 +1,3 @@ import './carto-sql-layer.spec'; import './carto-bqtiler-layer.spec'; -import './auth.spec'; +import './config.spec';