Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Carto: fetchMap to support custom basemaps #8856

Merged
merged 17 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old mapStyle was actually an object with various internal properties (like styleType - an actual id of style).

New api is more robust and thus i propose to completly hide mapStyle in documentation.

(it's still returned for backwards compatibility, but it can be considered deprecated now)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good hiding it! Shouldn't we also remove it or comment it from example in line 27?

I understand the API still returns it for compatibility but using it in the example without context looks confusing to me


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) {
donmccurdy marked this conversation as resolved.
Show resolved Hide resolved
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)
},
donmccurdy marked this conversation as resolved.
Show resolved Hide resolved
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)
]);
donmccurdy marked this conversation as resolved.
Show resolved Hide resolved

// 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;
donmccurdy marked this conversation as resolved.
Show resolved Hide resolved
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
Loading
Loading