kepler.gl
Version:
kepler.gl is a webgl based application to visualize large scale location data in the browser
250 lines (224 loc) • 8.44 kB
text/typescript
// SPDX-License-Identifier: MIT
// Copyright contributors to the kepler.gl project
import {
ActionTypes,
keplerGlInit,
_actionFor,
_updateProperty,
RegisterEntryUpdaterAction,
RenameEntryUpdaterAction
} from '@kepler.gl/actions';
import {handleActions} from 'redux-actions';
import {coreReducerFactory, KeplerGlState} from './core';
type KeplerGlStateMap = {
[id: string]: Partial<KeplerGlState>;
};
type CombineRegisterUpdateActions = RegisterEntryUpdaterAction['payload'] &
RenameEntryUpdaterAction['payload']; // Extend this type with additional actions to enforce strict typings
// INITIAL_STATE
const initialCoreState = {};
export function provideInitialState(initialState, extraReducers?) {
const coreReducer = coreReducerFactory(initialState, extraReducers);
const handleRegisterEntry = (
state: KeplerGlStateMap,
{
payload: {
id,
mint,
mapboxApiAccessToken,
mapboxApiUrl,
mapStylesReplaceDefault,
initialUiState
}
}: {
payload: RegisterEntryUpdaterAction['payload'];
}
): KeplerGlStateMap => {
// by default, always create a mint state even if the same id already exist
// if state.id exist and mint=false, keep the existing state
const previousState = state[id] && mint === false ? state[id] : undefined;
return {
// register entry to kepler.gl passing in mapbox config to mapStyle
...state,
[id]: coreReducer(
previousState,
keplerGlInit({mapboxApiAccessToken, mapboxApiUrl, mapStylesReplaceDefault, initialUiState})
)
};
};
const handleDeleteEntry = (
state: KeplerGlStateMap,
{payload: {id}}: {payload: {id: string}}
): KeplerGlStateMap =>
Object.keys(state).reduce(
(accu, curr) => ({
...accu,
...(curr === id ? {} : {[curr]: state[curr]})
}),
{}
);
const handleRenameEntry = (
state: KeplerGlStateMap,
{
payload: {oldId, newId}
}: {
payload: RenameEntryUpdaterAction['payload'];
}
): KeplerGlStateMap =>
Object.keys(state).reduce(
(accu, curr) => ({
...accu,
...{[curr === oldId ? newId : curr]: state[curr]}
}),
{}
);
return (state = initialCoreState, action) => {
// update child states
Object.keys(state).forEach(id => {
const updateItemState = coreReducer(state[id], _actionFor(id, action));
state = _updateProperty(state, id, updateItemState);
});
// perform additional state reducing (e.g. switch action.type etc...)
const handlers = {
[ActionTypes.REGISTER_ENTRY]: handleRegisterEntry,
[ActionTypes.DELETE_ENTRY]: handleDeleteEntry,
[ActionTypes.RENAME_ENTRY]: handleRenameEntry
};
return handleActions<KeplerGlStateMap, CombineRegisterUpdateActions>(
handlers,
initialCoreState
)(state, action);
};
}
const _keplerGlReducer = provideInitialState(initialCoreState);
function mergeInitialState(saved = {}, provided = {}, extraInitialStateKeys: string[] = []) {
const keys = ['mapState', 'mapStyle', 'visState', 'uiState', ...extraInitialStateKeys];
// shallow merge each reducer
const newState = keys.reduce(
(accu, key) => ({
...accu,
...(saved[key] && provided[key]
? {[key]: {...saved[key], ...provided[key]}}
: {[key]: saved[key] || provided[key] || {}})
}),
{}
);
return newState;
}
function decorate(target, savedInitialState = {}) {
const targetInitialState = savedInitialState;
/**
* Returns a kepler.gl reducer that will also pass each action through additional reducers spiecified.
* The parameter should be either a reducer map or a reducer function.
* The state passed into the additional action handler is the instance state.
* It will include all the subreducers `visState`, `uiState`, `mapState` and `mapStyle`.
* `.plugin` is only meant to be called once when mounting the keplerGlReducer to the store.
* **Note** This is an advanced option to give you more freedom to modify the internal state of the kepler.gl instance.
* You should only use this to adding additional actions instead of replacing default actions.
*
* @mixin keplerGlReducer.plugin
* @memberof keplerGlReducer
* @param {Object|Function} customReducer - A reducer map or a reducer
* @param {Object} options - options to be applied to custom reducer logic
* @param {Object} options.override - objects that describe which action to override, e.g. {[ActionTypes.LAYER_TYPE_CHANGE]: true}
* @public
* @example
* const myKeplerGlReducer = keplerGlReducer
* .plugin({
* // 1. as reducer map
* HIDE_AND_SHOW_SIDE_PANEL: (state, action) => ({
* ...state,
* uiState: {
* ...state.uiState,
* readOnly: !state.uiState.readOnly
* }
* })
* })
* .plugin(handleActions({
* // 2. as reducer
* 'HIDE_MAP_CONTROLS': (state, action) => ({
* ...state,
* uiState: {
* ...state.uiState,
* mapControls: hiddenMapControl
* }
* })
* }, {}));
*/
target.plugin = function plugin(customReducer, options) {
if (typeof customReducer === 'object') {
// if only provided a reducerMap, wrap it in a reducer
customReducer = handleActions(customReducer, {});
}
// use 'function' keyword to enable 'this'
// TODO: temporarily making action type to any, will fix that by creating a shared action interface
return decorate((state = {}, action: any = {}) => {
let nextState = state;
if (action.type && !options?.override?.[action.type]) {
nextState = this(state, action);
}
// for each entry in the staten
Object.keys(nextState).forEach(id => {
// update child states
nextState = _updateProperty(
nextState,
id,
customReducer(nextState[id], _actionFor(id, action))
);
});
return nextState;
});
};
/**
* Return a reducer that initiated with custom initial state.
* The parameter should be an object mapping from `subreducer` name to custom subreducer state,
* which will be shallow **merged** with default initial state.
*
* Default subreducer state:
* - [`visState`](./vis-state.md#INITIAL_VIS_STATE)
* - [`mapState`](./map-state.md#INITIAL_MAP_STATE)
* - [`mapStyle`](./map-style.md#INITIAL_MAP_STYLE)
* - [`uiState`](./ui-state.md#INITIAL_UI_STATE)
* @mixin keplerGlReducer.initialState
* @memberof keplerGlReducer
* @param {Object} iniSt - custom state to be merged with default initial state
* @param {Object} extraReducers - optional custom reducers in addition to the default `visState`, `mapState`, `mapStyle`, and `uiState`
* @public
* @example
* const myKeplerGlReducer = keplerGlReducer
* .initialState({
* uiState: {readOnly: true}
* });
*/
target.initialState = function initialState(iniSt, extraReducers = {}) {
// passing through extraInitialStateKeys and extraReducers allows external customization by adding additional subreducers and state beyond the default `visState`, `mapState`, `mapStyle`, and `uiState`
const extraInitialStateKeys = Object.keys(extraReducers);
const merged = mergeInitialState(targetInitialState, iniSt, extraInitialStateKeys);
const targetReducer = provideInitialState(merged, extraReducers);
return decorate(targetReducer, merged);
};
return target;
}
/**
* Kepler.gl reducer to be mounted to your store. You can mount `keplerGlReducer` at property `keplerGl`, if you choose
* to mount it at another address e.g. `foo` you will need to specify it when you mount `KeplerGl` component in your app with `getState: state => state.foo`
* @public
* @example
* import keplerGlReducer from 'kepler.gl/reducers';
* import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
* import {taskMiddleware} from 'react-palm/tasks';
*
* const initialState = {};
* const reducers = combineReducers({
* // <-- mount kepler.gl reducer in your app
* keplerGl: keplerGlReducer,
*
* // Your other reducers here
* app: appReducer
* });
*
* // using createStore
* export default createStore(reducer, initialState, applyMiddleware(taskMiddleware));
*/
const keplerGlReducer = decorate(_keplerGlReducer);
export default keplerGlReducer;