UNPKG

kepler.gl

Version:

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

508 lines (475 loc) 17 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 React, {Component} from 'react'; import PropTypes from 'prop-types'; import {css} from 'styled-components'; import {findDOMNode} from 'react-dom'; import {createSelector} from 'reselect'; import get from 'lodash.get'; import ModalDialogFactory from './modals/modal-dialog'; import KeplerGlSchema from 'schemas'; import {exportJson, exportHtml, exportData, exportImage, exportMap} from 'utils/export-utils'; import {isValidMapInfo} from 'utils/map-info-utils'; // modals import DeleteDatasetModalFactory from './modals/delete-data-modal'; import OverWriteMapModalFactory from './modals/overwrite-map-modal'; import DataTableModalFactory from './modals/data-table-modal'; import LoadDataModalFactory from './modals/load-data-modal'; import ExportImageModalFactory from './modals/export-image-modal'; import ExportDataModalFactory from './modals/export-data-modal'; import ExportMapModalFactory from './modals/export-map-modal/export-map-modal'; import AddMapStyleModalFactory from './modals/add-map-style-modal'; import SaveMapModalFactory from './modals/save-map-modal'; import ShareMapModalFactory from './modals/share-map-modal'; // Breakpoints import {media} from 'styles/media-breakpoints'; // Template import { ADD_DATA_ID, DATA_TABLE_ID, DELETE_DATA_ID, EXPORT_DATA_ID, EXPORT_IMAGE_ID, EXPORT_MAP_ID, ADD_MAP_STYLE_ID, SAVE_MAP_ID, SHARE_MAP_ID, OVERWRITE_MAP_ID } from 'constants/default-settings'; import {EXPORT_MAP_FORMATS} from '../constants/default-settings'; const DataTableModalStyle = css` top: 80px; padding: 32px 0 0 0; width: 90vw; max-width: 90vw; ${media.portable` padding: 0; `} ${media.palm` padding: 0; margin: 0 auto; `} `; const smallModalCss = css` width: 40%; padding: 40px 40px 32px 40px; `; const LoadDataModalStyle = css` top: 60px; `; const DefaultStyle = css` max-width: 960px; `; ModalContainerFactory.deps = [ DeleteDatasetModalFactory, OverWriteMapModalFactory, DataTableModalFactory, LoadDataModalFactory, ExportImageModalFactory, ExportDataModalFactory, ExportMapModalFactory, AddMapStyleModalFactory, ModalDialogFactory, SaveMapModalFactory, ShareMapModalFactory ]; export default function ModalContainerFactory( DeleteDatasetModal, OverWriteMapModal, DataTableModal, LoadDataModal, ExportImageModal, ExportDataModal, ExportMapModal, AddMapStyleModal, ModalDialog, SaveMapModal, ShareMapModal ) { class ModalWrapper extends Component { static propTypes = { rootNode: PropTypes.object, containerW: PropTypes.number, containerH: PropTypes.number, mapboxApiAccessToken: PropTypes.string.isRequired, mapboxApiUrl: PropTypes.string, mapState: PropTypes.object.isRequired, mapStyle: PropTypes.object.isRequired, uiState: PropTypes.object.isRequired, visState: PropTypes.object.isRequired, visStateActions: PropTypes.object.isRequired, uiStateActions: PropTypes.object.isRequired, mapStyleActions: PropTypes.object.isRequired, onSaveToStorage: PropTypes.func, cloudProviders: PropTypes.arrayOf(PropTypes.object) }; cloudProviders = props => props.cloudProviders; providerWithStorage = createSelector(this.cloudProviders, cloudProviders => cloudProviders.filter(p => p.hasPrivateStorage()) ); providerWithShare = createSelector(this.cloudProviders, cloudProviders => cloudProviders.filter(p => p.hasSharingUrl()) ); _closeModal = () => { this.props.uiStateActions.toggleModal(null); }; _deleteDataset = key => { this.props.visStateActions.removeDataset(key); this._closeModal(); }; _onAddCustomMapStyle = () => { this.props.mapStyleActions.addCustomMapStyle(); this._closeModal(); }; _onFileUpload = blob => { this.props.visStateActions.loadFiles(blob); }; _onExportImage = () => { if (!this.props.uiState.exportImage.exporting) { exportImage(this.props, this.props.uiState.exportImage); this.props.uiStateActions.cleanupExportImage(); this._closeModal(); } }; _onExportData = () => { exportData(this.props, this.props.uiState.exportData); this._closeModal(); }; _onExportMap = () => { const {uiState} = this.props; const {format} = uiState.exportMap; (format === EXPORT_MAP_FORMATS.HTML ? exportHtml : exportJson)( this.props, this.props.uiState.exportMap[format] || {} ); this._closeModal(); }; _exportFileToCloud = ({provider, isPublic, overwrite, closeModal}) => { const toSave = exportMap(this.props); this.props.providerActions.exportFileToCloud({ mapData: toSave, provider, options: { isPublic, overwrite }, closeModal, onSuccess: this.props.onExportToCloudSuccess, onError: this.props.onExportToCloudError }); }; _onSaveMap = (overwrite = false) => { const {currentProvider} = this.props.providerState; const provider = this.props.cloudProviders.find(p => p.name === currentProvider); this._exportFileToCloud({ provider, isPublic: false, overwrite, closeModal: true }); }; _onOverwriteMap = () => { this._onSaveMap(true); }; _onShareMapUrl = provider => { this._exportFileToCloud({provider, isPublic: true, overwrite: false, closeModal: false}); }; _onCloseSaveMap = () => { this.props.providerActions.resetProviderStatus(); this._closeModal(); }; _onLoadCloudMap = payload => { this.props.providerActions.loadCloudMap({ ...payload, onSuccess: this.props.onLoadCloudMapSuccess, onError: this.props.onLoadCloudMapError }); }; /* eslint-disable complexity */ render() { const { containerW, containerH, mapStyle, mapState, uiState, visState, rootNode, visStateActions, uiStateActions, providerState } = this.props; const {currentModal, datasetKeyToRemove} = uiState; const {datasets, layers, editingDataset} = visState; let template = null; let modalProps = {}; if (currentModal && currentModal.id && currentModal.template) { // if currentMdoal template is already provided // TODO: need to check whether template is valid template = <currentModal.template />; modalProps = currentModal.modalProps; } else { switch (currentModal) { case DATA_TABLE_ID: const width = containerW * 0.9; template = ( <DataTableModal width={containerW * 0.9} height={containerH * 0.85} datasets={datasets} dataId={editingDataset} showDatasetTable={visStateActions.showDatasetTable} sortTableColumn={visStateActions.sortTableColumn} pinTableColumn={visStateActions.pinTableColumn} copyTableColumn={visStateActions.copyTableColumn} /> ); // TODO: we need to make this width consistent with the css rule defined modal.js:32 max-width: 70vw modalProps.cssStyle = css` ${DataTableModalStyle}; ${media.palm` width: ${width}px; `} `; break; case DELETE_DATA_ID: // validate options if (datasetKeyToRemove && datasets && datasets[datasetKeyToRemove]) { template = ( <DeleteDatasetModal dataset={datasets[datasetKeyToRemove]} layers={layers} /> ); modalProps = { title: 'modal.title.deleteDataset', cssStyle: smallModalCss, footer: true, onConfirm: () => this._deleteDataset(datasetKeyToRemove), onCancel: this._closeModal, confirmButton: { negative: true, large: true, children: 'modal.button.delete' } }; } break; // in case we add a new case after this one case ADD_DATA_ID: template = ( <LoadDataModal {...providerState} onClose={this._closeModal} onFileUpload={this._onFileUpload} onLoadCloudMap={this._onLoadCloudMap} cloudProviders={this.providerWithStorage(this.props)} onSetCloudProvider={this.props.providerActions.setCloudProvider} getSavedMaps={this.props.providerActions.getSavedMaps} loadFiles={uiState.loadFiles} {...uiState.loadFiles} /> ); modalProps = { title: 'modal.title.addDataToMap', cssStyle: LoadDataModalStyle, footer: false, onConfirm: this._closeModal }; break; case EXPORT_IMAGE_ID: template = ( <ExportImageModal exportImage={uiState.exportImage} mapW={containerW} mapH={containerH} onUpdateSetting={uiStateActions.setExportImageSetting} /> ); modalProps = { title: 'modal.title.exportImage', footer: true, onCancel: this._closeModal, onConfirm: this._onExportImage, confirmButton: { large: true, disabled: uiState.exportImage.exporting, children: 'modal.button.download' } }; break; case EXPORT_DATA_ID: template = ( <ExportDataModal {...uiState.exportData} datasets={datasets} applyCPUFilter={this.props.visStateActions.applyCPUFilter} onClose={this._closeModal} onChangeExportDataType={uiStateActions.setExportDataType} onChangeExportSelectedDataset={uiStateActions.setExportSelectedDataset} onChangeExportFiltered={uiStateActions.setExportFiltered} /> ); modalProps = { title: 'modal.title.exportData', footer: true, onCancel: this._closeModal, onConfirm: this._onExportData, confirmButton: { large: true, children: 'modal.button.export' } }; break; case EXPORT_MAP_ID: const keplerGlConfig = KeplerGlSchema.getConfigToSave({ mapStyle, visState, mapState, uiState }); template = ( <ExportMapModal config={keplerGlConfig} options={uiState.exportMap} onChangeExportMapFormat={uiStateActions.setExportMapFormat} onEditUserMapboxAccessToken={uiStateActions.setUserMapboxAccessToken} onChangeExportMapHTMLMode={uiStateActions.setExportHTMLMapMode} /> ); modalProps = { title: 'modal.title.exportMap', footer: true, onCancel: this._closeModal, onConfirm: this._onExportMap, confirmButton: { large: true, children: 'modal.button.export' } }; break; case ADD_MAP_STYLE_ID: template = ( <AddMapStyleModal mapboxApiAccessToken={this.props.mapboxApiAccessToken} mapboxApiUrl={this.props.mapboxApiUrl} mapState={this.props.mapState} inputStyle={mapStyle.inputStyle} inputMapStyle={this.props.mapStyleActions.inputMapStyle} loadCustomMapStyle={this.props.mapStyleActions.loadCustomMapStyle} /> ); modalProps = { title: 'modal.title.addCustomMapboxStyle', footer: true, onCancel: this._closeModal, onConfirm: this._onAddCustomMapStyle, confirmButton: { large: true, disabled: !mapStyle.inputStyle.style, children: 'modal.button.addStyle' } }; break; case SAVE_MAP_ID: template = ( <SaveMapModal {...providerState} exportImage={uiState.exportImage} mapInfo={visState.mapInfo} onSetMapInfo={visStateActions.setMapInfo} onUpdateImageSetting={uiStateActions.setExportImageSetting} cloudProviders={this.providerWithStorage(this.props)} onSetCloudProvider={this.props.providerActions.setCloudProvider} /> ); modalProps = { title: 'modal.title.saveMap', footer: true, onCancel: this._closeModal, onConfirm: () => this._onSaveMap(false), confirmButton: { large: true, disabled: uiState.exportImage.exporting || !isValidMapInfo(visState.mapInfo) || !providerState.currentProvider, children: 'modal.button.save' } }; break; case OVERWRITE_MAP_ID: template = ( <OverWriteMapModal {...providerState} cloudProviders={this.props.cloudProviders} title={get(visState, ['mapInfo', 'title'])} onSetCloudProvider={this.props.providerActions.setCloudProvider} onUpdateImageSetting={uiStateActions.setExportImageSetting} /> ); modalProps = { title: 'Overwrite Existing File?', cssStyle: smallModalCss, footer: true, onConfirm: this._onOverwriteMap, onCancel: this._closeModal, confirmButton: { large: true, children: 'Yes', disabled: uiState.exportImage.exporting || !isValidMapInfo(visState.mapInfo) || !providerState.currentProvider } }; break; case SHARE_MAP_ID: template = ( <ShareMapModal {...providerState} isReady={!uiState.exportImage.exporting} cloudProviders={this.providerWithShare(this.props)} onExport={this._onShareMapUrl} onSetCloudProvider={this.props.providerActions.setCloudProvider} onUpdateImageSetting={uiStateActions.setExportImageSetting} /> ); modalProps = { title: 'modal.title.shareURL', onCancel: this._onCloseSaveMap }; break; default: break; } } return this.props.rootNode ? ( <ModalDialog parentSelector={() => findDOMNode(rootNode)} isOpen={Boolean(currentModal)} onCancel={this._closeModal} {...modalProps} cssStyle={DefaultStyle.concat(modalProps.cssStyle || '')} > {template} </ModalDialog> ) : null; } /* eslint-enable complexity */ } return ModalWrapper; }