UNPKG

kepler.gl

Version:

kepler.gl is a webgl based application to visualize large scale location data in the browser

767 lines (719 loc) 21 kB
// Copyright (c) 2021 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. // @ts-nocheck import { ADD_DATA_ID, DATA_TABLE_ID, DEFAULT_NOTIFICATION_TOPICS, DELETE_DATA_ID, EXPORT_DATA_TYPE, EXPORT_HTML_MAP_MODES, EXPORT_IMG_RATIOS, EXPORT_MAP_FORMATS, RESOLUTIONS, MAP_CONTROLS } from 'constants/default-settings'; import {LOCALE_CODES} from 'localization/locales'; import {createNotification, errorNotification} from 'utils/notifications-utils'; import {calculateExportImageSize} from 'utils/export-utils'; import {payload_, apply_, compose_} from './composer-helpers'; export const DEFAULT_ACTIVE_SIDE_PANEL = 'layer'; export const DEFAULT_MODAL = ADD_DATA_ID; /** * Updaters for `uiState` reducer. 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, {uiStateUpdaters} from 'kepler.gl/reducers'; * // Root Reducer * const reducers = combineReducers({ * keplerGl: keplerGlReducer, * app: appReducer * }); * * const composedReducer = (state, action) => { * switch (action.type) { * // click button to close side panel * case 'CLICK_BUTTON': * return { * ...state, * keplerGl: { * ...state.keplerGl, * foo: { * ...state.keplerGl.foo, * uiState: uiStateUpdaters.toggleSidePanelUpdater( * uiState, {payload: null} * ) * } * } * }; * } * return reducers(state, action); * }; * * export default composedReducer; */ /* eslint-disable no-unused-vars */ const uiStateUpdaters = null; /* eslint-enable no-unused-vars */ const DEFAULT_MAP_CONTROLS_FEATURES = { show: true, active: false, disableClose: false, // defines which map index users are interacting with (through map controls) activeMapIndex: 0 }; /** * A list of map control visibility and whether is it active. * @memberof uiStateUpdaters * @constant * @property visibleLayers Default: `{show: true, active: false}` * @property mapLegend Default: `{show: true, active: false}` * @property toggle3d Default: `{show: true}` * @property splitMap Default: `{show: true}` * @property mapDraw Default: `{show: true, active: false}` * @property mapLocale Default: `{show: false, active: false}` * @type {import('./ui-state-updaters').MapControls} * @public */ export const DEFAULT_MAP_CONTROLS = Object.keys(MAP_CONTROLS).reduce( (final, current) => ({ ...final, [current]: DEFAULT_MAP_CONTROLS_FEATURES }), {} ); /** * Default image export config * @memberof uiStateUpdaters * @constant * @property ratio Default: `'SCREEN'`, * @property resolution Default: `'ONE_X'`, * @property legend Default: `false`, * @property mapH Default: 0, * @property mapW Default: 0, * @property imageSize Default: {zoomOffset: 0, scale: 1, imageW: 0, imageH: 0}, * @property imageDataUri Default: `''`, * @property exporting Default: `false` * @property error Default: `false` * @type {import('./ui-state-updaters').ExportImage} * @public */ export const DEFAULT_EXPORT_IMAGE = { // user options ratio: EXPORT_IMG_RATIOS.SCREEN, resolution: RESOLUTIONS.ONE_X, legend: false, mapH: 0, mapW: 0, imageSize: { zoomOffset: 0, scale: 1, imageW: 0, imageH: 0 }, // when this is set to true, the mock map viewport will move to the center of data center: false, // exporting state imageDataUri: '', // exporting: used to attach plot-container to dom exporting: false, // processing: used as loading indicator when export image is being produced processing: false, error: false }; export const DEFAULT_LOAD_FILES = { fileLoading: false }; /** * Default initial `exportData` settings * @memberof uiStateUpdaters * @constant * @property selectedDataset Default: `''`, * @property dataType Default: `'csv'`, * @property filtered Default: `true`, * @type {import('./ui-state-updaters').ExportData} * @public */ export const DEFAULT_EXPORT_DATA = { selectedDataset: '', dataType: EXPORT_DATA_TYPE.CSV, filtered: true }; /** * @constant */ export const DEFAULT_NOTIFICATIONS = []; /** * @constant * @property exportMapboxAccessToken - Default: null, this is used when we provide a default mapbox token for users to take advantage of * @property userMapboxToken - Default: '', mapbox token provided by user through input field * @property mode - Default: 'READ', read only or editable * @type {import('./ui-state-updaters').ExportHtml} * @public */ export const DEFAULT_EXPORT_HTML = { exportMapboxAccessToken: null, userMapboxToken: '', mode: EXPORT_HTML_MAP_MODES.READ }; /** * @constant * @property hasData - Default: 'true', * @type {import('./ui-state-updaters').ExportJson} * @public */ export const DEFAULT_EXPORT_JSON = { hasData: true }; /** * Export Map Config * @constant * @property HTML - Default: 'DEFAULT_EXPORT_HTML', * @property JSON - Default: 'DEFAULT_EXPORT_JSON', * @property format - Default: 'HTML', * @type {import('./ui-state-updaters').ExportMap} * @public */ export const DEFAULT_EXPORT_MAP = { [EXPORT_MAP_FORMATS.HTML]: DEFAULT_EXPORT_HTML, [EXPORT_MAP_FORMATS.JSON]: DEFAULT_EXPORT_JSON, format: EXPORT_MAP_FORMATS.HTML }; /** * Default initial `uiState` * @memberof uiStateUpdaters * @constant * @property readOnly Default: `false` * @property activeSidePanel Default: `'layer'` * @property currentModal Default: `'addData'` * @property datasetKeyToRemove Default: `null` * @property visibleDropdown Default: `null` * @property exportImage Default: [`DEFAULT_EXPORT_IMAGE`](#default_export_image) * @property exportData Default: [`DEFAULT_EXPORT_DATA`](#default_export_data) * @property exportMap Default: [`DEFAULT_EXPORT_MAP`](#default_export_map) * @property mapControls Default: [`DEFAULT_MAP_CONTROLS`](#default_map_controls) * @property notifications Default: `[]` * @property notifications Default: `[]` * @property loadFiles * @type {import('./ui-state-updaters').UiState} * @public */ export const INITIAL_UI_STATE = { readOnly: false, activeSidePanel: DEFAULT_ACTIVE_SIDE_PANEL, currentModal: DEFAULT_MODAL, datasetKeyToRemove: null, visibleDropdown: null, // export image modal ui exportImage: DEFAULT_EXPORT_IMAGE, // export data modal ui exportData: DEFAULT_EXPORT_DATA, // html export exportMap: DEFAULT_EXPORT_MAP, // map control panels mapControls: DEFAULT_MAP_CONTROLS, // ui notifications notifications: DEFAULT_NOTIFICATIONS, // load files loadFiles: DEFAULT_LOAD_FILES, // Locale of the UI locale: LOCALE_CODES.en }; /* Updaters */ /** * @memberof uiStateUpdaters */ export const initUiStateUpdater = (state, action) => ({ ...state, ...(action.payload || {}).initialUiState }); /** * Toggle active side panel * @memberof uiStateUpdaters * @param state `uiState` * @param action * @param action.payload id of side panel to be shown, one of `layer`, `filter`, `interaction`, `map`. close side panel if `null` * @returns nextState * @type {typeof import('./ui-state-updaters').toggleSidePanelUpdater} * @public */ export const toggleSidePanelUpdater = (state, {payload: id}) => { return id === state.activeSidePanel ? state : { ...state, activeSidePanel: id }; }; /** * Show and hide modal dialog * @memberof uiStateUpdaters * @param state `uiState` * @param action * @paramaction.payload id of modal to be shown, null to hide modals. One of: * - [`DATA_TABLE_ID`](../constants/default-settings.md#data_table_id) * - [`DELETE_DATA_ID`](../constants/default-settings.md#delete_data_id) * - [`ADD_DATA_ID`](../constants/default-settings.md#add_data_id) * - [`EXPORT_IMAGE_ID`](../constants/default-settings.md#export_image_id) * - [`EXPORT_DATA_ID`](../constants/default-settings.md#export_data_id) * - [`ADD_MAP_STYLE_ID`](../constants/default-settings.md#add_map_style_id) * @returns nextState * @type {typeof import('./ui-state-updaters').toggleModalUpdater} * @public */ export const toggleModalUpdater = (state, {payload: id}) => ({ ...state, currentModal: id }); /** * Hide and show side panel header dropdown, activated by clicking the share link on top of the side panel * @memberof uiStateUpdaters * @type {typeof import('./ui-state-updaters').showExportDropdownUpdater} * @public */ export const showExportDropdownUpdater = (state, {payload: id}) => ({ ...state, visibleDropdown: id }); /** * Hide side panel header dropdown, activated by clicking the share link on top of the side panel * @memberof uiStateUpdaters * @type {typeof import('./ui-state-updaters').hideExportDropdownUpdater} * @public */ export const hideExportDropdownUpdater = state => ({ ...state, visibleDropdown: null }); /** * Toggle active map control panel * @memberof uiStateUpdaters * @param state `uiState` * @param action action * @param action.payload map control panel id, one of the keys of: [`DEFAULT_MAP_CONTROLS`](#default_map_controls) * @returns nextState * @type {typeof import('./ui-state-updaters').toggleMapControlUpdater} * @public */ export const toggleMapControlUpdater = (state, {payload: {panelId, index = 0}}) => ({ ...state, mapControls: { ...state.mapControls, [panelId]: { ...state.mapControls[panelId], // this handles split map interaction // Toggling from within the same map will simply toggle the active property // Toggling from within different maps we set the active property to true active: index === state.mapControls[panelId].activeMapIndex ? !state.mapControls[panelId].active : true, activeMapIndex: index } } }); /** * Toggle map control visibility * @memberof uiStateUpdaters * @param state `uiState` * @param action action * @param action.payload map control panel id, one of the keys of: [`DEFAULT_MAP_CONTROLS`](#default_map_controls) * @returns nextState * @type {typeof import('./ui-state-updaters').setMapControlVisibilityUpdater} * @public */ export const setMapControlVisibilityUpdater = (state, {payload: {panelId, show}}) => { if (!state.mapControls?.[panelId]) { return state; } return { ...state, mapControls: { ...state.mapControls, [panelId]: { ...state.mapControls[panelId], show: Boolean(show) } } }; }; /** * Toggle active map control panel * @memberof uiStateUpdaters * @param state `uiState` * @param action * @param action.payload dataset id * @returns nextState * @type {typeof import('./ui-state-updaters').openDeleteModalUpdater} * @public */ export const openDeleteModalUpdater = (state, {payload: datasetKeyToRemove}) => ({ ...state, currentModal: DELETE_DATA_ID, datasetKeyToRemove }); /** * Set `exportImage.legend` to `true` or `false` * @memberof uiStateUpdaters * @param state `uiState` * @returns nextState * @type {typeof import('./ui-state-updaters').setExportImageSettingUpdater} * @public */ export const setExportImageSettingUpdater = (state, {payload: newSetting}) => { const updated = {...state.exportImage, ...newSetting}; const imageSize = calculateExportImageSize(updated) || state.exportImage.imageSize; return { ...state, exportImage: { ...updated, imageSize } }; }; /** * Set `exportImage.setExportImageDataUri` to a image dataUri * @memberof uiStateUpdaters * @param state `uiState` * @param action * @param action.payload export image data uri * @returns nextState * @type {typeof import('./ui-state-updaters').setExportImageDataUriUpdater} * @public */ export const setExportImageDataUriUpdater = (state, {payload: dataUri}) => ({ ...state, exportImage: { ...state.exportImage, processing: false, imageDataUri: dataUri } }); /** * @memberof uiStateUpdaters * @type {typeof import('./ui-state-updaters').setExportImageErrorUpdater} * @public */ export const setExportImageErrorUpdater = (state, {payload: error}) => ({ ...state, exportImage: { ...state.exportImage, processing: false, error } }); /** * Delete cached export image * @memberof uiStateUpdaters * @type {typeof import('./ui-state-updaters').cleanupExportImageUpdater} * @public */ export const cleanupExportImageUpdater = state => ({ ...state, exportImage: { ...state.exportImage, exporting: false, imageDataUri: '', error: false, processing: false, center: false } }); /** * Start image exporting flow * @memberof uiStateUpdaters * @param state * @param options * @returns {UiState} * @type {typeof import('./ui-state-updaters').startExportingImage} * @public */ export const startExportingImageUpdater = (state, {payload: options = {}}) => { const imageSettings = { ...options, exporting: true }; return compose_([ cleanupExportImageUpdater, apply_(setExportImageSettingUpdater, payload_(imageSettings)) ])(state); }; /** * Set selected dataset for export * @memberof uiStateUpdaters * @param state `uiState` * @param action * @param action.payload dataset id * @returns nextState * @type {typeof import('./ui-state-updaters').setExportSelectedDatasetUpdater} * @public */ export const setExportSelectedDatasetUpdater = (state, {payload: dataset}) => ({ ...state, exportData: { ...state.exportData, selectedDataset: dataset } }); /** * Set data format for exporting data * @memberof uiStateUpdaters * @param state `uiState` * @param action * @param action.payload one of `'text/csv'` * @returns nextState * @type {typeof import('./ui-state-updaters').setExportDataTypeUpdater} * @public */ export const setExportDataTypeUpdater = (state, {payload: dataType}) => ({ ...state, exportData: { ...state.exportData, dataType } }); /** * Whether to export filtered data, `true` or `false` * @memberof uiStateUpdaters * @param state `uiState` * @param action * @param action.payload * @returns nextState * @type {typeof import('./ui-state-updaters').setExportFilteredUpdater} * @public */ export const setExportFilteredUpdater = (state, {payload: filtered}) => ({ ...state, exportData: { ...state.exportData, filtered } }); /** * Whether to including data in map config, toggle between `true` or `false` * @memberof uiStateUpdaters * @param state `uiState` * @returns nextState * @type {typeof import('./ui-state-updaters').setExportDataUpdater} * @public */ export const setExportDataUpdater = state => ({ ...state, exportMap: { ...state.exportMap, [EXPORT_MAP_FORMATS.JSON]: { ...state.exportMap[EXPORT_MAP_FORMATS.JSON], hasData: !state.exportMap[EXPORT_MAP_FORMATS.JSON].hasData } } }); /** * whether to export a mapbox access to HTML single page * @param state - `uiState` * @param action * @param action.payload * @returns nextState * @type {typeof import('./ui-state-updaters').setUserMapboxAccessTokenUpdater} * @public */ export const setUserMapboxAccessTokenUpdater = (state, {payload: userMapboxToken}) => ({ ...state, exportMap: { ...state.exportMap, [EXPORT_MAP_FORMATS.HTML]: { ...state.exportMap[EXPORT_MAP_FORMATS.HTML], userMapboxToken } } }); /** * Sets the export map format * @param state - `uiState` * @param action * @param action.payload format to use to export the map into * @return nextState * @type {typeof import('./ui-state-updaters').setExportMapFormatUpdater} */ export const setExportMapFormatUpdater = (state, {payload: format}) => ({ ...state, exportMap: { ...state.exportMap, format } }); /** * Set the export html map mode * @param state - `uiState` * @param action * @param action.payload to be set (available modes: EXPORT_HTML_MAP_MODES) * @return nextState * @type {typeof import('./ui-state-updaters').setExportMapHTMLModeUpdater} */ export const setExportMapHTMLModeUpdater = (state, {payload: mode}) => ({ ...state, exportMap: { ...state.exportMap, [EXPORT_MAP_FORMATS.HTML]: { ...state.exportMap[EXPORT_MAP_FORMATS.HTML], mode } } }); /** * Adds a new notification. * Updates a notification in case of matching ids. * @memberof uiStateUpdaters * @param state `uiState` * @param action * @param action.payload Params of a notification * @returns nextState * @type {typeof import('./ui-state-updaters').addNotificationUpdater} * @public */ export const addNotificationUpdater = (state, {payload}) => { let notifications; const payloadId = payload?.id; const notificationToUpdate = payloadId ? state.notifications.find(n => n.id === payloadId) : null; if (notificationToUpdate) { notifications = state.notifications.map(n => n.id === payloadId ? createNotification(payload) : n ); } else { notifications = [...(state.notifications || []), createNotification(payload)]; } return {...state, notifications}; }; /** * Remove a notification * @memberof uiStateUpdaters * @param state `uiState` * @param action * @param action.payload id of the notification to be removed * @returns nextState * @type {typeof import('./ui-state-updaters').removeNotificationUpdater} * @public */ export const removeNotificationUpdater = (state, {payload: id}) => ({ ...state, notifications: state.notifications.filter(n => n.id !== id) }); /** * Fired when file loading begin * @memberof uiStateUpdaters * @param state `uiState` * @returns nextState * @type {typeof import('./ui-state-updaters').loadFilesUpdater} * @public */ export const loadFilesUpdater = state => ({ ...state, loadFiles: { ...state.loadFiles, fileLoading: true } }); /** * Handles loading file success and set fileLoading property to false * @memberof uiStateUpdaters * @param state `uiState` * @returns nextState * @type {typeof import('./ui-state-updaters').loadFilesSuccessUpdater} */ export const loadFilesSuccessUpdater = state => ({ ...state, loadFiles: { ...state.loadFiles, fileLoading: false } }); /** * Handles load file error and set fileLoading property to false * @memberof uiStateUpdaters * @param state * @param action * @param action.error * @returns nextState * @type {typeof import('./ui-state-updaters').loadFilesErrUpdater} * @public */ export const loadFilesErrUpdater = (state, {error}) => addNotificationUpdater( { ...state, loadFiles: { ...state.loadFiles, fileLoading: false } }, { payload: errorNotification({ message: (error || {}).message || 'Failed to upload files', topic: DEFAULT_NOTIFICATION_TOPICS.global }) } ); /** * Handles toggle map split and reset all map control index to 0 * @memberof uiStateUpdaters * @param state * @returns nextState * @type {typeof import('./ui-state-updaters').toggleSplitMapUpdater} * @public */ export const toggleSplitMapUpdater = state => ({ ...state, mapControls: Object.entries(state.mapControls).reduce( (acc, entry) => ({ ...acc, [entry[0]]: { ...entry[1], activeMapIndex: 0 } }), {} ) }); /** * Toggle modal data * @memberof uiStateUpdaters * @param state * @returns nextState * @type {typeof import('./ui-state-updaters').showDatasetTableUpdater} * @public */ export const showDatasetTableUpdater = state => toggleModalUpdater(state, {payload: DATA_TABLE_ID}); /** * Set the locale of the UI * @memberof uiStateUpdaters * @param state `uiState` * @param action * @param action.payload * @param action.payload.locale locale * @returns nextState * @type {typeof import('./ui-state-updaters').setLocaleUpdater} * @public */ export const setLocaleUpdater = (state, {payload: {locale}}) => ({ ...state, locale });