kepler.gl
Version:
kepler.gl is a webgl based application to visualize large scale location data in the browser
564 lines (515 loc) • 16.5 kB
JavaScript
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import Task, {withTask} from 'react-palm/tasks';
import cloneDeep from 'lodash.clonedeep';
// Utils
import {
getDefaultLayerGroupVisibility,
isValidStyleUrl,
getStyleDownloadUrl,
mergeLayerGroupVisibility,
editTopMapStyle,
editBottomMapStyle,
getStyleImageIcon
} from 'utils/map-style-utils/mapbox-gl-style-editor';
import {
DEFAULT_MAP_STYLES,
DEFAULT_LAYER_GROUPS,
DEFAULT_MAPBOX_API_URL
} from 'constants/default-settings';
import {generateHashId} from 'utils/utils';
import {LOAD_MAP_STYLE_TASK} from 'tasks/tasks';
import {loadMapStyles, loadMapStyleErr} from 'actions/map-style-actions';
import {rgb} from 'd3-color';
import {hexToRgb} from 'utils/color-utils';
const DEFAULT_BLDG_COLOR = '#D1CEC7';
const getDefaultState = () => {
const visibleLayerGroups = {};
const styleType = 'dark';
const topLayerGroups = {};
return {
styleType,
visibleLayerGroups,
topLayerGroups,
mapStyles: DEFAULT_MAP_STYLES.reduce(
(accu, curr) => ({
...accu,
[curr.id]: curr
}),
{}
),
// save mapbox access token
mapboxApiAccessToken: null,
mapboxApiUrl: DEFAULT_MAPBOX_API_URL,
mapStylesReplaceDefault: false,
inputStyle: getInitialInputStyle(),
threeDBuildingColor: hexToRgb(DEFAULT_BLDG_COLOR),
custom3DBuildingColor: false
};
};
/**
* Updaters for `mapStyle`. Can be used in your root reducer to directly modify kepler.gl's state.
* Read more about [Using updaters](../advanced-usage/using-updaters.md)
* @public
* @example
*
* import keplerGlReducer, {mapStyleUpdaters} from 'kepler.gl/reducers';
* // Root Reducer
* const reducers = combineReducers({
* keplerGl: keplerGlReducer,
* app: appReducer
* });
*
* const composedReducer = (state, action) => {
* switch (action.type) {
* // click button to hide label from background map
* case 'CLICK_BUTTON':
* return {
* ...state,
* keplerGl: {
* ...state.keplerGl,
* foo: {
* ...state.keplerGl.foo,
* mapStyle: mapStyleUpdaters.mapConfigChangeUpdater(
* mapStyle,
* {payload: {visibleLayerGroups: {label: false, road: true, background: true}}}
* )
* }
* }
* };
* }
* return reducers(state, action);
* };
*
* export default composedReducer;
*/
/* eslint-disable no-unused-vars */
const mapStyleUpdaters = null;
/* eslint-enable no-unused-vars */
/**
* Default initial `mapStyle`
* @memberof mapStyleUpdaters
* @constant
* @property {string} styleType - Default: `'dark'`
* @property {Object} visibleLayerGroups - Default: `{}`
* @property {Object} topLayerGroups - Default: `{}`
* @property {Object} mapStyles - mapping from style key to style object
* @property {string} mapboxApiAccessToken - Default: `null`
* @Property {string} mapboxApiUrl - Default null
* @Property {Boolean} mapStylesReplaceDefault - Default: `false`
* @property {Object} inputStyle - Default: `{}`
* @property {Array} threeDBuildingColor - Default: `[r, g, b]`
* @public
*/
export const INITIAL_MAP_STYLE = getDefaultState();
/**
* Create two map styles from preset map style, one for top map one for bottom
*
* @param {string} styleType - current map style
* @param {Object} visibleLayerGroups - visible layers of bottom map
* @param {Object} topLayerGroups - visible layers of top map
* @param {Object} mapStyles - a dictionary of all map styles
* @returns {Object} bottomMapStyle | topMapStyle | isRaster
*/
export function getMapStyles({styleType, visibleLayerGroups, topLayerGroups, mapStyles}) {
const mapStyle = mapStyles[styleType];
// style might not be loaded yet
if (!mapStyle || !mapStyle.style) {
return {};
}
const editable = Object.keys(visibleLayerGroups).length;
const bottomMapStyle = !editable
? mapStyle.style
: editBottomMapStyle({
id: styleType,
mapStyle,
visibleLayerGroups
});
const hasTopLayer = editable && Object.values(topLayerGroups).some(v => v);
// mute top layer if not visible in bottom layer
const topLayers =
hasTopLayer &&
Object.keys(topLayerGroups).reduce(
(accu, key) => ({
...accu,
[key]: topLayerGroups[key] && visibleLayerGroups[key]
}),
{}
);
const topMapStyle = hasTopLayer
? editTopMapStyle({
id: styleType,
mapStyle,
visibleLayerGroups: topLayers
})
: null;
return {bottomMapStyle, topMapStyle, editable};
}
function findLayerFillColor(layer) {
return layer && layer.paint && layer.paint['background-color'];
}
function get3DBuildingColor(style) {
// set building color to be the same as the background color.
if (!style.style) {
return hexToRgb(DEFAULT_BLDG_COLOR);
}
const backgroundLayer = (style.style.layers || []).find(({id}) => id === 'background');
const buildingLayer = (style.style.layers || []).find(({id}) => id.match(/building/));
const buildingColor =
findLayerFillColor(buildingLayer) || findLayerFillColor(backgroundLayer) || DEFAULT_BLDG_COLOR;
// brighten or darken building based on style
const operation = style.id.match(/(?=(dark|night))/) ? 'brighter' : 'darker';
const alpha = 0.2;
const rgbObj = rgb(buildingColor)[operation]([alpha]);
return [rgbObj.r, rgbObj.g, rgbObj.b];
}
function getLayerGroupsFromStyle(style) {
return Array.isArray(style.layers)
? DEFAULT_LAYER_GROUPS.filter(lg => style.layers.filter(lg.filter).length)
: [];
}
// Updaters
/**
* Propagate `mapStyle` reducer with `mapboxApiAccessToken` and `mapStylesReplaceDefault`.
* if mapStylesReplaceDefault is true mapStyles is emptied; loadMapStylesUpdater() will
* populate mapStyles.
*
* @memberof mapStyleUpdaters
* @param {Object} state
* @param {Object} action
* @param {Object} action.payload
* @param {string} action.payload.mapboxApiAccessToken
* @returns {Object} nextState
* @public
*/
export const initMapStyleUpdater = (state, action) => ({
...state,
// save mapbox access token to map style state
mapboxApiAccessToken: (action.payload || {}).mapboxApiAccessToken,
mapboxApiUrl: (action.payload || {}).mapboxApiUrl || state.mapboxApiUrl,
mapStyles: action.payload && !action.payload.mapStylesReplaceDefault ? state.mapStyles : {},
mapStylesReplaceDefault: action.payload.mapStylesReplaceDefault || false
});
// });
/**
* Update `visibleLayerGroups`to change layer group visibility
* @memberof mapStyleUpdaters
* @param {Object} state `mapStyle`
* @param {Object} action
* @param {Object} action.payload new config `{visibleLayerGroups: {label: false, road: true, background: true}}`
* @returns {Object} nextState
* @public
*/
export const mapConfigChangeUpdater = (state, action) => ({
...state,
...action.payload,
...getMapStyles({
...state,
...action.payload
})
});
/**
* Change to another map style. The selected style should already been loaded into `mapStyle.mapStyles`
* @memberof mapStyleUpdaters
* @param {Object} state `mapStyle`
* @param {Object} action
* @param {string} action.payload
* @returns {Object} nextState
* @public
*/
export const mapStyleChangeUpdater = (state, {payload: styleType}) => {
if (!state.mapStyles[styleType]) {
// we might not have received the style yet
return state;
}
const defaultLGVisibility = getDefaultLayerGroupVisibility(state.mapStyles[styleType]);
const visibleLayerGroups = mergeLayerGroupVisibility(
defaultLGVisibility,
state.visibleLayerGroups
);
const threeDBuildingColor = state.custom3DBuildingColor
? state.threeDBuildingColor
: get3DBuildingColor(state.mapStyles[styleType]);
return {
...state,
styleType,
visibleLayerGroups,
threeDBuildingColor,
...getMapStyles({
...state,
visibleLayerGroups,
styleType
})
};
};
/**
* Callback when load map style success
* @memberof mapStyleUpdaters
* @param {Object} state `mapStyle`
* @param {Object} action
* @param {Object} action.payload a `{[id]: style}` mapping
* @return {Object} nextState
* @public
*/
export const loadMapStylesUpdater = (state, action) => {
const newStyles = action.payload || {};
const addLayerGroups = Object.keys(newStyles).reduce(
(accu, id) => ({
...accu,
[id]: {
...newStyles[id],
layerGroups: newStyles[id].layerGroups || getLayerGroupsFromStyle(newStyles[id].style)
}
}),
{}
);
// add new styles to state
const newState = {
...state,
mapStyles: {
...state.mapStyles,
...addLayerGroups
}
};
return newStyles[state.styleType]
? mapStyleChangeUpdater(newState, {payload: state.styleType})
: newState;
};
/**
* Callback when load map style error
* @memberof mapStyleUpdaters
* @param {Object} state `mapStyle`
* @param {Object} action
* @param {*} action.payload error
* @returns {Object} nextState
* @public
*/
// do nothing for now, if didn't load, skip it
export const loadMapStyleErrUpdater = state => state;
export const requestMapStylesUpdater = (state, {payload: mapStyles}) => {
const loadMapStyleTasks = getLoadMapStyleTasks(
mapStyles,
state.mapboxApiAccessToken,
state.mapboxApiUrl
);
return withTask(state, loadMapStyleTasks);
};
/**
* Load map style object when pass in saved map config
* @memberof mapStyleUpdaters
* @param {Object} state `mapStyle`
* @param {Object} action
* @param {Object} action.payload saved map config `{mapStyle, visState, mapState}`
* @returns {Object} nextState or `react-pam` tasks to load map style object
*/
export const receiveMapConfigUpdater = (state, {payload: {config = {}}}) => {
const {mapStyle} = config || {};
if (!mapStyle) {
return state;
}
// if saved custom mapStyles load the style object
const loadMapStyleTasks = mapStyle.mapStyles
? getLoadMapStyleTasks(mapStyle.mapStyles, state.mapboxApiAccessToken, state.mapboxApiUrl)
: null;
// merge default mapStyles
const merged = mapStyle.mapStyles
? {
...mapStyle,
mapStyles: {
...mapStyle.mapStyles,
...state.mapStyles
}
}
: mapStyle;
// set custom3DBuildingColor: true if mapStyle contains threeDBuildingColor
merged.custom3DBuildingColor =
Boolean(mapStyle.threeDBuildingColor) || merged.custom3DBuildingColor;
const newState = mapConfigChangeUpdater(state, {payload: merged});
return loadMapStyleTasks ? withTask(newState, loadMapStyleTasks) : newState;
};
function getLoadMapStyleTasks(mapStyles, mapboxApiAccessToken, mapboxApiUrl) {
return [
Task.all(
Object.values(mapStyles)
.map(({id, url, accessToken}) => ({
id,
url: isValidStyleUrl(url)
? getStyleDownloadUrl(url, accessToken || mapboxApiAccessToken, mapboxApiUrl)
: url
}))
.map(LOAD_MAP_STYLE_TASK)
).bimap(
// success
results =>
loadMapStyles(
results.reduce(
(accu, {id, style}) => ({
...accu,
[id]: {
...mapStyles[id],
style
}
}),
{}
)
),
// error
loadMapStyleErr
)
];
}
/**
* Reset map style config to initial state
* @memberof mapStyleUpdaters
* @param {Object} state `mapStyle`
* @returns {Object} nextState
* @public
*/
export const resetMapConfigMapStyleUpdater = state => {
const emptyConfig = {
...INITIAL_MAP_STYLE,
mapboxApiAccessToken: state.mapboxApiAccessToken,
mapboxApiUrl: state.mapboxApiUrl,
mapStylesReplaceDefault: state.mapStylesReplaceDefault,
...state.initialState,
mapStyles: state.mapStyles,
initialState: state.initialState
};
return mapStyleChangeUpdater(emptyConfig, {payload: emptyConfig.styleType});
};
/**
* Callback when a custom map style object is received
* @memberof mapStyleUpdaters
* @param {Object} state `mapStyle`
* @param {Object} action
* @param {Object} action.payload
* @param {string} action.payload.icon
* @param {Object} action.payload.style
* @param {*} action.payload.error
* @returns {Object} nextState
* @public
*/
export const loadCustomMapStyleUpdater = (state, {payload: {icon, style, error}}) => ({
...state,
inputStyle: {
...state.inputStyle,
// style json and icon will load asynchronously
...(style
? {
id: style.id || generateHashId(),
// make a copy of the style object
style: cloneDeep(style),
label: style.name,
// gathering layer group info from style json
layerGroups: getLayerGroupsFromStyle(style)
}
: {}),
...(icon ? {icon} : {}),
...(error !== undefined ? {error} : {})
}
});
/**
* Input a custom map style object
* @memberof mapStyleUpdaters
* @param {Object} state `mapStyle`
* @param {Object} action action object
* @param {Object} action.payload inputStyle
* @param {string} action.payload.url style url e.g. `'mapbox://styles/heshan/xxxxxyyyyzzz'`
* @param {string} action.payload.id style url e.g. `'custom_style_1'`
* @param {Object} action.payload.style actual mapbox style json
* @param {string} action.payload.name style name
* @param {Object} action.payload.layerGroups layer groups that can be used to set map layer visibility
* @param {Object} action.payload.icon icon image data url
* @returns {Object} nextState
* @public
*/
export const inputMapStyleUpdater = (state, {payload: {inputStyle, mapState}}) => {
const updated = {
...state.inputStyle,
...inputStyle
};
const isValid = isValidStyleUrl(updated.url);
const icon = isValid
? getStyleImageIcon({
mapState,
styleUrl: updated.url,
mapboxApiAccessToken: updated.accessToken || state.mapboxApiAccessToken,
mapboxApiUrl: state.mapboxApiUrl || DEFAULT_MAPBOX_API_URL
})
: state.inputStyle.icon;
return {
...state,
inputStyle: {
...updated,
isValid,
icon
}
};
};
/**
* Add map style from user input to reducer and set it to current style
* This action is called when user click confirm after putting in a valid style url in the custom map style dialog.
* It should not be called from outside kepler.gl without a valid `inputStyle` in the `mapStyle` reducer.
* @memberof mapStyleUpdaters
* @param {Object} state `mapStyle`
* @returns {Object} nextState
*/
export const addCustomMapStyleUpdater = state => {
const styleId = state.inputStyle.id;
const newState = {
...state,
mapStyles: {
...state.mapStyles,
[styleId]: state.inputStyle
},
// set to default
inputStyle: getInitialInputStyle()
};
// set new style
return mapStyleChangeUpdater(newState, {payload: styleId});
};
/**
* Updates 3d building color
* @memberof mapStyleUpdaters
* @param state
* @param color
* @return {Object} nextState
*/
export const set3dBuildingColorUpdater = (state, {payload: color}) => ({
...state,
threeDBuildingColor: color,
custom3DBuildingColor: true
});
/**
* Return the initial input style
* @return Object
*/
export function getInitialInputStyle() {
return {
accessToken: null,
error: false,
isValid: false,
label: null,
style: null,
url: null,
icon: null,
custom: true
};
}