UNPKG

kepler.gl

Version:

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

397 lines (341 loc) 11 kB
// 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 {withTask} from 'react-palm/tasks'; import {default as Console} from 'global/console'; import {generateHashId, getError, isPlainObject} from 'utils/utils'; import { EXPORT_FILE_TO_CLOUD_TASK, ACTION_TASK, DELAY_TASK, LOAD_CLOUD_MAP_TASK, GET_SAVED_MAPS_TASK } from 'tasks/tasks'; import { exportFileSuccess, exportFileError, postSaveLoadSuccess, loadCloudMapSuccess, getSavedMapsSuccess, getSavedMapsError, loadCloudMapError, resetProviderStatus } from 'actions/provider-actions'; import {removeNotification, toggleModal, addNotification} from 'actions/ui-state-actions'; import {addDataToMap} from 'actions/actions'; import { DEFAULT_NOTIFICATION_TYPES, DEFAULT_NOTIFICATION_TOPICS, DATASET_FORMATS } from 'constants/default-settings'; import {toArray} from 'utils/utils'; import KeplerGlSchema from 'schemas'; export const INITIAL_PROVIDER_STATE = { isProviderLoading: false, isCloudMapLoading: false, providerError: null, currentProvider: null, successInfo: {}, mapSaved: null }; import {DATASET_HANDLERS} from 'processors/data-processor'; function createActionTask(action, payload) { if (typeof action === 'function') { return ACTION_TASK().map(_ => action(payload)); } return null; } function _validateProvider(provider, method) { if (!provider) { Console.error(`provider is not defined`); return false; } if (typeof provider[method] !== 'function') { Console.error(`${method} is not a function of Cloud provider: ${provider.name}`); return false; } return true; } function createGlobalNotificationTasks({type, message, delayClose = true}) { const id = generateHashId(); const successNote = { id, type: DEFAULT_NOTIFICATION_TYPES[type] || DEFAULT_NOTIFICATION_TYPES.success, topic: DEFAULT_NOTIFICATION_TOPICS.global, message }; const task = ACTION_TASK().map(_ => addNotification(successNote)); return delayClose ? [task, DELAY_TASK(3000).map(_ => removeNotification(id))] : [task]; } /** * This method will export the current kepler config file to the chosen cloud proder * add returns a share URL * * @param {*} state * @param {*} action */ export const exportFileToCloudUpdater = (state, action) => { const {mapData, provider, options = {}, onSuccess, onError, closeModal} = action.payload; if (!_validateProvider(provider, 'uploadMap')) { return state; } const newState = { ...state, isProviderLoading: true, currentProvider: provider.name }; // payload called by provider.uploadMap const payload = { mapData, options }; const uploadFileTask = EXPORT_FILE_TO_CLOUD_TASK({provider, payload}).bimap( // success response => exportFileSuccess({response, provider, options, onSuccess, closeModal}), // error error => exportFileError({error, provider, options, onError}) ); return withTask(newState, uploadFileTask); }; /** * * @param {*} state * @param {*} action */ export const exportFileSuccessUpdater = (state, action) => { const {response, provider, options, onSuccess, closeModal} = action.payload; const newState = { ...state, isProviderLoading: false, // TODO: do we always have to store this? successInfo: response, ...(!options.isPublic ? { mapSaved: provider.name } : {}) }; const tasks = [ createActionTask(onSuccess, {response, provider, options}), closeModal && ACTION_TASK().map(_ => postSaveLoadSuccess(`Map saved to ${state.currentProvider}!`)) ].filter(d => d); return tasks.length ? withTask(newState, tasks) : newState; }; /** * Close modal on success and display notification * @param {*} state * @param {*} action */ export const postSaveLoadSuccessUpdater = (state, action) => { const message = action.payload || `Saved / Load to ${state.currentProvider} Success`; const tasks = [ ACTION_TASK().map(_ => toggleModal(null)), ACTION_TASK().map(_ => resetProviderStatus()), ...createGlobalNotificationTasks({message}) ]; return withTask(state, tasks); }; /** * * @param {*} state * @param {*} action */ export const exportFileErrorUpdater = (state, action) => { const {error, provider, onError} = action.payload; const newState = { ...state, isProviderLoading: false, providerError: getError(error) }; const task = createActionTask(onError, {error, provider}); return task ? withTask(newState, task) : newState; }; export const loadCloudMapUpdater = (state, action) => { const {loadParams, provider, onSuccess, onError} = action.payload; if (!loadParams) { Console.warn('load map error: loadParams is undefined'); return state; } if (!_validateProvider(provider, 'downloadMap')) { return state; } const newState = { ...state, isProviderLoading: true, isCloudMapLoading: true }; // payload called by provider.downloadMap const uploadFileTask = LOAD_CLOUD_MAP_TASK({provider, payload: loadParams}).bimap( // success response => loadCloudMapSuccess({response, loadParams, provider, onSuccess, onError}), // error error => loadCloudMapError({error, provider, onError}) ); return withTask(newState, uploadFileTask); }; function checkLoadMapResponseError(response) { if (!response || !isPlainObject(response)) { return new Error('Load map response is empty'); } if (!isPlainObject(response.map)) { return new Error(`Load map response should be an object property "map"`); } if (!response.map.datasets || !response.map.config) { return new Error(`Load map response.map should be an object with property datasets or config`); } return null; } function getDatasetHandler(format) { const defaultHandler = DATASET_HANDLERS[DATASET_FORMATS.csv]; if (!format) { Console.warn('format is not provided in load map response, will use csv by default'); return defaultHandler; } if (!DATASET_HANDLERS[format]) { const supportedFormat = Object.keys(DATASET_FORMATS) .map(k => `'${k}'`) .join(', '); Console.warn( `unknown format ${format}. Please use one of ${supportedFormat}, will use csv by default` ); return defaultHandler; } return DATASET_HANDLERS[format]; } function parseLoadMapResponse(response, loadParams, provider) { const {map, format} = response; const processorMethod = getDatasetHandler(format); const parsedDatasets = toArray(map.datasets).map((ds, i) => { if (format === DATASET_FORMATS.keplergl) { // no need to obtain id, directly pass them in return processorMethod(ds); } const info = (ds && ds.info) || {id: generateHashId(6)}; const data = processorMethod(ds.data || ds); return {info, data}; }); const parsedConfig = map.config ? KeplerGlSchema.parseSavedConfig(map.config) : null; const info = { ...map.info, provider: provider.name, loadParams }; return {datasets: parsedDatasets, config: parsedConfig, info}; } export const loadCloudMapSuccessUpdater = (state, action) => { const {response, loadParams, provider, onSuccess, onError} = action.payload; const formatError = checkLoadMapResponseError(response); if (formatError) { // if response format is not correct return exportFileErrorUpdater(state, { payload: {error: formatError, provider, onError} }); } const newState = { ...state, mapSaved: provider.name, currentProvider: provider.name, isCloudMapLoading: false, isProviderLoading: false }; const payload = parseLoadMapResponse(response, loadParams, provider); const tasks = [ ACTION_TASK().map(_ => addDataToMap(payload)), createActionTask(onSuccess, {response, loadParams, provider}), ACTION_TASK().map(_ => postSaveLoadSuccess(`Map from ${provider.name} loaded`)) ].filter(d => d); return tasks.length ? withTask(newState, tasks) : newState; }; export const loadCloudMapErrorUpdater = (state, action) => { const message = getError(action.payload.error) || `Error loading saved map`; Console.warn(message); const newState = { ...state, isProviderLoading: false, isCloudMapLoading: false, providerError: null }; return withTask( newState, createGlobalNotificationTasks({type: 'error', message, delayClose: false}) ); }; /** * * @param {*} state * @param {*} action */ export const resetProviderStatusUpdater = (state, action) => ({ ...state, isProviderLoading: false, providerError: null, isCloudMapLoading: false, successInfo: {} }); /** * Set current cloudProvider * @param {*} state * @param {*} action */ export const setCloudProviderUpdater = (state, action) => ({ ...state, isProviderLoading: false, providerError: null, successInfo: {}, currentProvider: action.payload }); export const getSavedMapsUpdater = (state, action) => { const provider = action.payload; if (!_validateProvider(provider, 'listMaps')) { return state; } const getSavedMapsTask = GET_SAVED_MAPS_TASK(provider).bimap( // success visualizations => getSavedMapsSuccess({visualizations, provider}), // error error => getSavedMapsError({error, provider}) ); return withTask( { ...state, isProviderLoading: true }, getSavedMapsTask ); }; export const getSavedMapsSuccessUpdater = (state, action) => ({ ...state, isProviderLoading: false, visualizations: action.payload.visualizations }); export const getSavedMapsErrorUpdater = (state, action) => { const message = getError(action.payload.error) || `Error getting saved maps from ${state.currentProvider}`; Console.warn(action.payload.error); const newState = { ...state, currentProvider: null, isProviderLoading: false }; return withTask( newState, createGlobalNotificationTasks({type: 'error', message, delayClose: false}) ); };