Skip to content

Commit

Permalink
Maps API v2 at @deck.gl/carto (#5053)
Browse files Browse the repository at this point in the history
  • Loading branch information
alasarr committed Oct 24, 2020
1 parent 8a9c838 commit 8db4485
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 162 deletions.
21 changes: 7 additions & 14 deletions docs/api-reference/carto/carto-sql-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
22 changes: 22 additions & 0 deletions docs/api-reference/carto/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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://<domain>/user/{user}/api/v1/map',
sqlUrl: 'https://<domain>/user/{user}/api/v2/sql',
});
```

138 changes: 38 additions & 100 deletions modules/carto/src/api/maps-api-client.js
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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);
}

/**
Expand Down
18 changes: 0 additions & 18 deletions modules/carto/src/auth.js

This file was deleted.

33 changes: 33 additions & 0 deletions modules/carto/src/config.js
Original file line number Diff line number Diff line change
@@ -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';
}
2 changes: 1 addition & 1 deletion modules/carto/src/index.js
Original file line number Diff line number Diff line change
@@ -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';
26 changes: 13 additions & 13 deletions modules/carto/src/layers/carto-bqtiler-layer.js
Original file line number Diff line number Diff line change
@@ -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
}
}
]
};
}
}

Expand Down
Loading

0 comments on commit 8db4485

Please sign in to comment.