Skip to content

Commit

Permalink
feat(carto): Support custom basemaps in fetchMap (#8856)
Browse files Browse the repository at this point in the history
  • Loading branch information
zbigg committed May 14, 2024
1 parent d504327 commit 341b753
Show file tree
Hide file tree
Showing 13 changed files with 740 additions and 57 deletions.
19 changes: 16 additions & 3 deletions docs/api-reference/carto/basemap.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,34 @@ To use pre-bundled scripts:
<script src="https://unpkg.com/@deck.gl/carto@^9.0.0/dist.min.js"></script>

<!-- or -->
<script src="https://unpkg.com/@deck.gl/core@^9.0.0/dist.min.js"></script>
<script src="https://unpkg.com/@deck.gl/core/@^9.0.0/dist.min.js"></script>
<script src="https://unpkg.com/@deck.gl/layers@^9.0.0/dist.min.js"></script>
<script src="https://unpkg.com/@deck.gl/mesh-layers@^9.0.0/dist.min.js"></script>
<script src="https://unpkg.com/@deck.gl/geo-layers@^9.0.0/dist.min.js"></script>
<script src="https://unpkg.com/@deck.gl/extensions@^9.0.0/dist.min.js"></script>
<script src="https://unpkg.com/@deck.gl/carto@^9.0.0/dist.min.js"></script>

<!-- basemap provider -->
<script src="https://unpkg.com/maplibre-gl/dist/maplibre-gl.js"></script>
```

```js
const deckgl = new deck.DeckGL({
const map = new maplibregl.Map({
container: 'map',
mapStyle: deck.carto.BASEMAP.POSITRON,
style: deck.carto.BASEMAP.POSITRON,
interactive: false
})
const deckgl = new deck.DeckGL({
canvas: 'deck-canvas',
initialViewState: {
latitude: 0,
longitude: 0,
zoom: 1
},
onViewStateChange: ({viewState}) => {
const {longitude, latitude, ...rest} = viewState;
map.jumpTo({center: [longitude, latitude], ...rest});
}
controller: true
});
```
Expand Down
52 changes: 38 additions & 14 deletions docs/api-reference/carto/fetch-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,21 @@ fetchMap({cartoMapId}).then(map => new Deck(map));

### Integration with CARTO basemaps


```js
fetchMap({cartoMapId}).then(({initialViewState, mapStyle, layers}) => {
const deckgl = new deck.DeckGL({
container: 'container',
controller: true,
// (Optional) Include a basemap.
mapStyle: `https://basemaps.cartocdn.com/gl/${mapStyle.styleType}-gl-style/style.json`,
initialViewState,
layers
});
});
import { fetchMap } from '@deck.gl/carto';
import { MapboxOverlay } from '@deck.gl/mapbox';
import maplibregl from 'maplibre-gl';

fetchMap({ cartoMapId }).then(({ basemap, layers }) => {
const map = new maplibregl.Map({
container: '...',
...basemap?.props, // basemap.props contain all props required to setup basemap
interactive: true
})
const overlay = new MapboxOverlay({layers: result.layers});
map.addControl(overlay);
})
```

## Parameters
Expand Down Expand Up @@ -90,14 +94,34 @@ When the map was last updated.

The [view state](../../developer-guide/views.md#view-state).

#### `mapStyle` (string) {#mapstyle}

An identifier describing the [basemap](../../api-reference/carto/basemap.md#supported-basemaps) configured in CARTO Builder.

#### `layers` (Layer[]) {#layers}

A collection of deck.gl [layers](../core/layer.md).

#### `basemap` (object) {#basemap}

An object describing the [basemap](../../api-reference/carto/basemap.md#supported-basemaps) configured in CARTO Builder.

Properties:
* `type` **(string)** - type of basemap: `'maplibre'` or `'google-maps'`
* `props` **(string or object)** - props that should be passed to basemap implementation
* if `type` is `'maplibre'` then it contains
* `style` **(string or object)** - URL of basemap style or style object if custom basemap is configured
* `center` **([number, number])** - center of map as `[latitude, longitude]`
* `zoom` **(number)** - zoom level
* `pitch` **(number)**
* `bearing` **(number)**
* if `type` is `'google-maps'`, then it contains those props
* `mapTypeId` **(string)** - type id of map
* `mapId` **(string, optional)** - map id
* `center` **(object)** - center of map as `{lat: number; lng: number}`
* `zoom`: **(number)** - zoom level (note, it has +1 offset applied versus deck.gl zoom)
* `tilt`: **(number)** - tilt, same as `pitch` in deck.gl API
* `heading`: **(number)** - heading, same as `bearing` in deck.gl API
* `rawStyle` **(string or object)** - for `maplibre` basemaps, original `style` before applying layer filtering
* `visibleLayerGroups` **(object, optional)** - layer groups to be displayed in the basemap.
* `attribution` **(string, optional)** - custom attribution HTML for this basemap

#### `stopAutoRefresh` (Function) {#stopautorefresh}

A function to invoke to stop auto-refreshing. Only present if `autoRefresh` option was provided to `fetchMap`.
Expand Down
100 changes: 100 additions & 0 deletions modules/carto/src/api/basemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {MapViewState} from '@deck.gl/core';
import {
GOOGLE_BASEMAPS,
CARTO_MAP_STYLES,
applyLayerGroupFilters,
fetchStyle,
getStyleUrl,
someLayerGroupsDisabled
} from '../basemap';
import {APIErrorContext, Basemap, KeplerMapConfig, MapLibreBasemapProps} from './types';

const CUSTOM_STYLE_ID_PREFIX = 'custom:';
const DEFAULT_CARTO_STYLE = 'positron';

function mapLibreViewpros(config: KeplerMapConfig): Omit<MapLibreBasemapProps, 'style'> {
const {longitude, latitude, ...rest} = config.mapState as MapViewState;
return {
center: [longitude, latitude],
...rest
};
}

/**
* Get basemap properties for Carto map.
*
* For maplibre-based basemaps it returns style or style URL that can be used with `maplibregl.Map` compatible component.
* * style url is returned for non-filtered standard Carto basemaps or if user used style URL directly in configuration
* * filtered style object returned for Carto basemaps with layer groups filtered
*
* For Google-maps base maps, it returns options that can be used with `google.maps.Map` constructor.
*/
export async function fetchBasemapProps({
config,
errorContext,

applyLayerFilters = true
}: {
config: KeplerMapConfig;

/** By default `fetchBasemapProps` applies layers filters to style. Set this to `false` to disable it. */
applyLayerFilters?: boolean;
errorContext?: APIErrorContext;
}): Promise<Basemap | null> {
const {mapStyle} = config;
const styleType = mapStyle.styleType || DEFAULT_CARTO_STYLE;
if (styleType.startsWith(CUSTOM_STYLE_ID_PREFIX)) {
const currentCustomStyle = config.customBaseMaps?.customStyle;
if (currentCustomStyle) {
return {
type: 'maplibre',
props: {
style: currentCustomStyle.style || currentCustomStyle.url,
...mapLibreViewpros(config)
},
attribution: currentCustomStyle.customAttribution
};
}
}

if (CARTO_MAP_STYLES.includes(styleType)) {
const {visibleLayerGroups} = mapStyle;
const styleUrl = getStyleUrl(styleType);
let style = styleUrl;
let rawStyle = styleUrl;
if (applyLayerFilters && visibleLayerGroups && someLayerGroupsDisabled(visibleLayerGroups)) {
rawStyle = await fetchStyle({styleUrl, errorContext});
style = applyLayerGroupFilters(rawStyle, visibleLayerGroups);
}
return {
type: 'maplibre',
props: {
style,
...mapLibreViewpros(config)
},
visibleLayerGroups,
rawStyle
};
}
const googleBasemapDef = GOOGLE_BASEMAPS[styleType];
if (googleBasemapDef) {
const {mapState} = config;
return {
type: 'google-maps',
props: {
...googleBasemapDef,
center: {lat: mapState.latitude, lng: mapState.longitude},
zoom: mapState.zoom + 1,
tilt: mapState.pitch,
heading: mapState.bearing
}
};
}
return {
type: 'maplibre',
props: {
style: getStyleUrl(DEFAULT_CARTO_STYLE),
...mapLibreViewpros(config)
}
};
}
26 changes: 20 additions & 6 deletions modules/carto/src/api/fetch-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ import {
vectorTableSource,
vectorTilesetSource
} from '../sources/index';
import {parseMap} from './parse-map';
import {ParseMapResult, parseMap} from './parse-map';
import {requestWithParameters} from './request-with-parameters';
import {assert} from '../utils';
import type {APIErrorContext, Format, MapType, QueryParameters} from './types';
import type {APIErrorContext, Basemap, Format, MapType, QueryParameters} from './types';
import {fetchBasemapProps} from './basemap';

type Dataset = {
id: string;
Expand Down Expand Up @@ -213,6 +214,14 @@ export type FetchMapOptions = {
onNewData?: (map: any) => void;
};

export type FetchMapResult = ParseMapResult & {
/**
* Basemap properties.
*/
basemap: Basemap | null;
stopAutoRefresh?: () => void;
};

/* eslint-disable max-statements */
export async function fetchMap({
apiBaseUrl = DEFAULT_API_BASE_URL,
Expand All @@ -221,7 +230,7 @@ export async function fetchMap({
headers = {},
autoRefresh,
onNewData
}: FetchMapOptions) {
}: FetchMapOptions): Promise<FetchMapResult> {
assert(cartoMapId, 'Must define CARTO map id: fetchMap({cartoMapId: "XXXX-XXXX-XXXX"})');
assert(apiBaseUrl, 'Must define apiBaseUrl');

Expand Down Expand Up @@ -272,12 +281,17 @@ export async function fetchMap({
}
});

// Mutates map.datasets so that dataset.data contains data
await fillInMapDatasets(map, clientId, apiBaseUrl, headers);
const [basemap] = await Promise.all([
fetchBasemapProps({config: map.keplerMapConfig.config, errorContext}),

// Mutates map.datasets so that dataset.data contains data
fillInMapDatasets(map, clientId, apiBaseUrl, headers)
]);

// Mutates attributes in visualChannels to contain tile stats
await fillInTileStats(map, apiBaseUrl);
const out = {...parseMap(map), ...{stopAutoRefresh}};

const out = {...parseMap(map), basemap, ...{stopAutoRefresh}};

const textLayers = out.layers.filter(layer => {
const pointType = layer.props.pointType || '';
Expand Down
13 changes: 11 additions & 2 deletions modules/carto/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
export {CartoAPIError} from './carto-api-error';
export {fetchMap} from './fetch-map';
export type {FetchMapOptions} from './fetch-map';
export type {APIErrorContext, Format, MapType, RequestType, QueryParameters} from './types';
export type {FetchMapOptions, FetchMapResult} from './fetch-map';
export type {
APIErrorContext,
Format,
MapType,
RequestType,
QueryParameters,
Basemap,
MapLibreBasemap,
GoogleBasemap
} from './types';
export {query} from './query';
export type {QueryOptions} from './query';
27 changes: 24 additions & 3 deletions modules/carto/src/api/parse-map.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {GL} from '@luma.gl/constants';
import {log} from '@deck.gl/core';
import {Layer, log} from '@deck.gl/core';
import {
AGGREGATION,
getLayer,
Expand All @@ -16,14 +16,34 @@ import {
import PointLabelLayer from '../layers/point-label-layer';
import {CollisionFilterExtension} from '@deck.gl/extensions';
import {assert} from '../utils';
import {MapDataset, MapLayerConfig, VisualChannels} from './types';
import {KeplerMapConfig, MapDataset, MapLayerConfig, VisualChannels} from './types';

const collisionFilterExtension = new CollisionFilterExtension();

export type ParseMapResult = {
/** Map id. */
id: string;

/** Title of map. */
title: string;

/** Description of map. */
description?: string;
createdAt: string;
updatedAt: string;
initialViewState: any;

/** @deprecated Use `basemap`. */
mapStyle: any;
token: string;

layers: Layer[];
};

export function parseMap(json) {
const {keplerMapConfig, datasets, token} = json;
assert(keplerMapConfig.version === 'v1', 'Only support Kepler v1');
const {mapState, mapStyle} = keplerMapConfig.config;
const {mapState, mapStyle} = keplerMapConfig.config as KeplerMapConfig;
const {layers, layerBlending, interactionConfig} = keplerMapConfig.config.visState;

return {
Expand All @@ -33,6 +53,7 @@ export function parseMap(json) {
createdAt: json.createdAt,
updatedAt: json.updatedAt,
initialViewState: mapState,
/** @deprecated Use `basemap`. */
mapStyle,
token,
layers: layers.reverse().map(({id, type, config, visualChannels}) => {
Expand Down

0 comments on commit 341b753

Please sign in to comment.