kepler.gl
Version:
kepler.gl is a webgl based application to visualize large scale location data in the browser
281 lines (248 loc) • 7.75 kB
JavaScript
// Copyright (c) 2018 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 Immutable from 'immutable';
import Task, {withTask} from 'react-palm/tasks';
// Utils
import {
getDefaultLayerGroupVisibility,
isValidStyleUrl,
getStyleDownloadUrl,
mergeLayerGroupVisibility,
editTopMapStyle,
editBottomMapStyle
} from 'utils/map-style-utils/mapbox-gl-style-editor';
import {DEFAULT_MAP_STYLES, DEFAULT_LAYER_GROUPS} 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';
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,
inputStyle: getInitialInputStyle()
};
};
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
*/
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
? Immutable.fromJS(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 getLayerGroupsFromStyle(style) {
return DEFAULT_LAYER_GROUPS.filter(lg => style.layers.filter(lg.filter).length);
}
// Updaters
export const initMapStyleUpdater = (state, action) => ({
...state,
// save mapbox access token to map style state
mapboxApiAccessToken: (action.payload || {}).mapboxApiAccessToken
});
export const mapConfigChangeUpdater = (state, action) => ({
...state,
...action.payload,
...getMapStyles({
...state,
...action.payload
})
});
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);
return {
...state,
styleType,
visibleLayerGroups,
...getMapStyles({
...state,
visibleLayerGroups,
styleType
})
};
};
export const loadMapStylesUpdater = (state, action) => {
const newStyles = action.payload;
// add new styles to state
const newState = {
...state,
mapStyles: {
...state.mapStyles,
...newStyles
}
};
return newStyles[state.styleType]
? mapStyleChangeUpdater(newState, {payload: state.styleType})
: newState;
};
// do nothing for now, if didn't load, skip it
export const loadMapStyleErrUpdater = (state, action) => state;
export const receiveMapConfigUpdater = (state, {payload: {mapStyle}}) => {
if (!mapStyle) {
return state;
}
// if saved custom mapStyles load the style object
const loadMapStyleTasks = mapStyle.mapStyles ? [
Task.all(
Object.values(mapStyle.mapStyles)
.map(({id, url, accessToken}) => ({
id, url: getStyleDownloadUrl(url, accessToken || state.mapboxApiAccessToken)
}))
.map(LOAD_MAP_STYLE_TASK))
.bimap(
// success
results => (
loadMapStyles(
results.reduce((accu, {id, style}) => ({
...accu,
[id]: {
...mapStyle.mapStyles[id],
layerGroups: getLayerGroupsFromStyle(style),
style
}
}), {})
)
),
// error
error => loadMapStyleErr(error)
)
] : null;
const newState = mapConfigChangeUpdater(state, {payload: mapStyle});
return loadMapStyleTasks ? withTask(
newState,
loadMapStyleTasks
) : newState;
};
export const resetMapConfigMapStyleUpdater = (state) => {
const emptyConfig = {
...INITIAL_MAP_STYLE,
mapboxApiAccessToken: state.mapboxApiAccessToken,
...state.initialState,
mapStyles: state.mapStyles,
initialState: state.initialState
};
return mapStyleChangeUpdater(emptyConfig, {payload: emptyConfig.styleType});
};
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: JSON.parse(JSON.stringify(style)),
label: style.name,
// gathering layer group info from style json
layerGroups: getLayerGroupsFromStyle(style)
} : {}),
...(icon ? {icon} : {}),
...(error !== undefined ? {error} : {})
}
});
export const inputMapStyleUpdater = (state, {payload: inputStyle}) => ({
...state,
inputStyle: {
...inputStyle,
isValid: isValidStyleUrl(inputStyle.url)
}
});
export const addCustomMapStyleUpdater = (state, action) => {
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});
};
export function getInitialInputStyle() {
return {
accessToken: null,
error: false,
isValid: false,
label: null,
style: null,
url: null,
custom: true
};
}