Skip to content

Commit

Permalink
carto: basemap support for google maps
Browse files Browse the repository at this point in the history
  • Loading branch information
zbigg committed May 9, 2024
1 parent 202cce7 commit 6125cba
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 36 deletions.
124 changes: 120 additions & 4 deletions modules/carto/src/api/basemap.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,111 @@
import {getBasemapStyle, getStyleUrl} from '../basemap';
import {
applyLayerGroupFilters,
fetchBasemapStyle,
getStyleUrl,
someLayerGroupsDisabled
} from '../basemap';
import {APIErrorContext, KeplerMapConfig} from './types';

const CUSTOM_STYLE_ID_PREFIX = 'custom:';
const DEFAULT_CARTO_STYLE = 'positron';
const CARTO_MAP_STYLES = ['positron', 'dark-matter', 'voyager'];

type BasemapProps = {
style: string | unknown;
export type BasemapType = 'maplibre' | 'google-maps';

export type BasemapProps = MaplibreBasemapProps | GoogleBasemapProps;

export type MaplibreBasemapProps = {
/**
* Type of basemap.
*/
type: 'maplibre';

/**
* Basemap style URL or style object.
*
* Note, layers in this style may be filtered by `visibleLayerGroups`.
*
* Non-filtered pristine version is stored in `rawStyle` property.
*/
style: string | Record<string, any>;

/**
* Layer groups to be displayed in the basemap.
*/
visibleLayerGroups?: Record<string, boolean>;

/** If `style` has been filtered by `visibleLayerGroups` then this property contains original style object, so user
* can use `applyLayerGroupFilters` again with new settings.
*/
rawStyle?: string | Record<string, any>;

/**
* Custom attribution for style data if not provided by style definition.
*/
attribution?: string;
};

export type GoogleBasemapProps = {
/**
* Type of basemap.
*/
type: 'google-maps';

/**
* Google map options.
*/
options: Record<string, any>;
};

const googleBasemapTypes = [
{
id: 'roadmap',
options: {
mapTypeId: 'roadmap',
mapId: '3754c817b510f791'
}
},
{
id: 'google-positron',
options: {
mapTypeId: 'roadmap',
mapId: 'ea84ae4203ef21cd'
}
},
{
id: 'google-dark-matter',
options: {
mapTypeId: 'roadmap',
mapId: '2fccc3b36c22a0e2'
}
},
{
id: 'google-voyager',
options: {
mapTypeId: 'roadmap',
mapId: '885caf1e15bb9ef2'
}
},
{
id: 'satellite',
options: {
mapTypeId: 'satellite'
}
},
{
id: 'hybrid',
options: {
mapTypeId: 'hybrid'
}
},
{
id: 'terrain',
options: {
mapTypeId: 'terrain'
}
}
];

export async function fetchBasemapProps({
config,
errorContext
Expand All @@ -23,18 +119,38 @@ export async function fetchBasemapProps({
const currentCustomStyle = config.customBaseMaps?.customStyle;
if (currentCustomStyle) {
return {
type: 'maplibre',
style: currentCustomStyle.style || currentCustomStyle.url,
attribution: currentCustomStyle.customAttribution
};
}
}

if (CARTO_MAP_STYLES.includes(styleType)) {
const {visibleLayerGroups} = mapStyle;
const styleUrl = getStyleUrl(styleType);
let style = styleUrl;
let rawStyle;
if (visibleLayerGroups && someLayerGroupsDisabled(visibleLayerGroups)) {
rawStyle = await fetchBasemapStyle({styleUrl, errorContext});
style = applyLayerGroupFilters(rawStyle, visibleLayerGroups);
}
return {
type: 'maplibre',
visibleLayerGroups,
style,
rawStyle
};
}
const googleBasemapDef = googleBasemapTypes.find(b => b.id === styleType);
if (googleBasemapDef) {
return {
style: await getBasemapStyle({styleType, visibleLayerGroups, errorContext})
type: 'google-maps',
options: googleBasemapDef.options
};
}
return {
type: 'maplibre',
style: getStyleUrl(DEFAULT_CARTO_STYLE)
};
}
14 changes: 11 additions & 3 deletions modules/carto/src/api/fetch-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +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 {fetchBasemapProps} from './basemap';
import {BasemapProps, fetchBasemapProps} from './basemap';

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

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

/* eslint-disable max-statements */
export async function fetchMap({
apiBaseUrl = DEFAULT_API_BASE_URL,
Expand All @@ -222,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
22 changes: 21 additions & 1 deletion 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 @@ -20,6 +20,26 @@ import {KeplerMapConfig, MapDataset, MapLayerConfig, VisualChannels} from './typ

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');
Expand Down
39 changes: 15 additions & 24 deletions modules/carto/src/basemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,43 +70,34 @@ export function applyLayerGroupFilters(
};
}

function someLayerGroupsDisabled(visibleLayerGroups?: Record<StyleLayerGroupSlug, boolean>) {
export function someLayerGroupsDisabled(visibleLayerGroups?: Record<StyleLayerGroupSlug, boolean>) {
return visibleLayerGroups && Object.values(visibleLayerGroups).every(Boolean) === false;
}

export function getStyleUrl(styleType: string) {
return baseUrl.replace('{basemap}', styleType);
}

export async function getBasemapStyle({
styleType,
visibleLayerGroups,
export async function fetchBasemapStyle({
styleUrl,
errorContext
}: {
styleType: string;
visibleLayerGroups?: Record<StyleLayerGroupSlug, boolean>;
styleUrl: string;
errorContext?: APIErrorContext;
}) {
/* global fetch */
const styleUrl = getStyleUrl(styleType);
let style = styleUrl;

if (visibleLayerGroups && someLayerGroupsDisabled(visibleLayerGroups)) {
let response: Response | undefined;
const originalStyle = await fetch(styleUrl, {
mode: 'cors',
credentials: 'omit'
let response: Response | undefined;
return await fetch(styleUrl, {
mode: 'cors',
credentials: 'omit'
})
.then(res => {
response = res;
return res.json();
})
.then(res => {
response = res;
return res.json();
})
.catch(error => {
throw new CartoAPIError(error, {...errorContext, requestType: 'Basemap style'}, response);
});
style = applyLayerGroupFilters(originalStyle, visibleLayerGroups);
}
return style;
.catch(error => {
throw new CartoAPIError(error, {...errorContext, requestType: 'Basemap style'}, response);
});
}

export default {
Expand Down
3 changes: 2 additions & 1 deletion modules/carto/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export {

export {
default as BASEMAP,
getBasemapStyle as _getBasemapStyle,
getStyleUrl as _getStyleUrl,
fetchBasemapStyle as _getBasemapStyle,
STYLE_LAYER_GROUPS as _STYLE_LAYER_GROUPS
} from './basemap';
export {default as colorBins} from './style/color-bins-style';
Expand Down
40 changes: 37 additions & 3 deletions test/modules/carto/api/basemap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,30 @@ test('fetchBasemapProps#carto - no filters', async t =>

const r = await fetchBasemapProps({config: mockedMapConfig});
t.equals(calls.length, 0, 'no style loaded, when there are no filters');
t.deepEquals(r, {style: BASEMAP.POSITRON}, 'style is just positron URL');
t.deepEquals(
r,
{type: 'maplibre', style: BASEMAP.POSITRON, visibleLayerGroups: {}, rawStyle: undefined},
'style is just positron URL'
);
t.end();
}, responseFunc));

test('fetchBasemapProps#carto - with filters', async t =>
withMockFetchMapsV3(async calls => {
const visibleLayerGroups = {label: false, road: true, border: true, water: true};
const r = await fetchBasemapProps({
config: {
...mockedMapConfig,
mapStyle: {
styleType: 'voyager',
visibleLayerGroups: {label: false, road: true, border: true, water: true}
visibleLayerGroups
}
}
});
t.equals(calls.length, 1, 'should call fetch only once');
t.equals(calls[0].url, BASEMAP.VOYAGER, 'should request voyager style');
t.equals(r.type, 'maplibre', 'proper basemap type is returned');
t.equals(r.rawStyle, mockedCartoStyle, 'raw style is returned');
t.deepEquals(
r.style,
{
Expand All @@ -67,6 +74,7 @@ test('fetchBasemapProps#carto - with filters', async t =>
},
'actual style is loaded with layers filtered-out'
);
t.deepEquals(r.visibleLayerGroups, visibleLayerGroups, 'visibleLayerGroups are passed');
t.end();
}, responseFunc));

Expand All @@ -90,12 +98,38 @@ test('fetchBasemapProps#custom', async t =>
t.equals(calls.length, 0, `shouldn't make any fetch requests`);
t.deepEquals(
r,
{style: 'http://example.com/style.json', attribution: 'custom attribution'},
{type: 'maplibre', style: 'http://example.com/style.json', attribution: 'custom attribution'},
'should return proper basemap settings'
);
t.end();
}, responseFunc));

test('fetchBasemapProps#google', async t =>
withMockFetchMapsV3(async calls => {
const r = await fetchBasemapProps({
config: {
...mockedMapConfig,
mapStyle: {
styleType: 'google-voyager'
}
}
});

t.equals(calls.length, 0, 'should fetch anything');
t.deepEquals(
r,
{
type: 'google-maps',
options: {
mapTypeId: 'roadmap',
mapId: '885caf1e15bb9ef2'
}
},
'should return proper google map options'
);
t.end();
}, responseFunc));

test('fetchBasemapProps#carto - error handling', async t =>
withMockFetchMapsV3(async calls => {
const expectedError = await fetchBasemapProps({
Expand Down

0 comments on commit 6125cba

Please sign in to comment.