UNPKG

kepler.gl.geoiq

Version:

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

478 lines (429 loc) 13.4 kB
// Copyright (c) 2023 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, createRef} from 'react'; import {console as Console} from 'global/window'; import {bindActionCreators} from 'redux'; import styled, {ThemeProvider, withTheme} from 'styled-components'; import {createSelector} from 'reselect'; import {connect as keplerGlConnect} from 'connect/keplergl-connect'; import * as VisStateActions from 'actions/vis-state-actions'; import * as MapStateActions from 'actions/map-state-actions'; import * as MapStyleActions from 'actions/map-style-actions'; import * as UIStateActions from 'actions/ui-state-actions'; import { EXPORT_IMAGE_ID, DIMENSIONS, KEPLER_GL_NAME, KEPLER_GL_VERSION, THEME, DEFAULT_MAPBOX_API_URL } from 'constants/default-settings'; import {MISSING_MAPBOX_TOKEN} from 'constants/user-feedbacks'; import SidePanelFactory from './side-panel'; import MapContainerFactory from './map-container'; import BottomWidgetFactory from './bottom-widget'; import ModalContainerFactory from './modal-container'; import PlotContainerFactory from './plot-container'; import NotificationPanelFactory from './notification-panel'; import {generateHashId} from 'utils/utils'; import {validateToken} from 'utils/mapbox-utils'; import {theme as basicTheme, themeLT} from 'styles/base'; import continuousColorLegend from 'react-vis/dist/legends/continuous-color-legend'; // Maybe we should think about exporting this or creating a variable // as part of the base.js theme const GlobalStyle = styled.div` font-family: ${props => props.theme.fontFamily}; font-weight: ${props => props.theme.fontWeight}; font-size: ${props => props.theme.fontSize}; line-height: ${props => props.theme.lineHeight}; margin-left: ${props => (props.sidePanel === null ? '0px' : '381px')}; margin-right: 0px; *, *:before, *:after { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } ul { margin: 0; padding: 0; } li { margin: 0; } a { text-decoration: none; color: ${props => props.theme.labelColor}; } `; KeplerGlFactory.deps = [ BottomWidgetFactory, MapContainerFactory, ModalContainerFactory, SidePanelFactory, PlotContainerFactory, NotificationPanelFactory ]; function KeplerGlFactory( BottomWidget, MapContainer, ModalWrapper, SidePanel, PlotContainer, NotificationPanel ) { class KeplerGL extends Component { static defaultProps = { mapStyles: [], mapStylesReplaceDefault: false, mapboxApiUrl: DEFAULT_MAPBOX_API_URL, width: 800, height: 800, appName: KEPLER_GL_NAME, version: KEPLER_GL_VERSION, sidePanelWidth: DIMENSIONS.sidePanel.width, theme: {} }; componentWillMount() { this._validateMapboxToken(); this._loadMapStyle(this.props.mapStyles); this._handleResize(this.props); } componentWillReceiveProps(nextProps) { if ( // if dimension props has changed this.props.height !== nextProps.height || this.props.width !== nextProps.width || // react-map-gl will dispatch updateViewport after this._handleResize is called // here we check if this.props.mapState.height is sync with props.height nextProps.height !== this.props.mapState.height || nextProps.uiState.activeSidePanel !== this.props.uiState.activeSidePanel ) { this._handleResize(nextProps); } } root = createRef(); /* selector */ themeSelector = props => props.theme; availableThemeSelector = createSelector(this.themeSelector, theme => typeof theme === 'object' ? { ...basicTheme, ...theme } : theme === THEME.light ? themeLT : theme ); _validateMapboxToken() { const {mapboxApiAccessToken} = this.props; if (!validateToken(mapboxApiAccessToken)) { Console.warn(MISSING_MAPBOX_TOKEN); } } _handleResize({width, height, uiState}) { width = uiState.activeSidePanel === null ? width : width - 381; if (!Number.isFinite(width) || !Number.isFinite(height)) { Console.warn('width and height is required'); return; } this.props.mapStateActions.updateMap({ width: width / (1 + Number(this.props.mapState.isSplit)), height }); } _loadMapStyle = () => { const defaultStyles = Object.values(this.props.mapStyle.mapStyles); // add id to custom map styles if not given const customStyles = (this.props.mapStyles || []).map(ms => ({ ...ms, id: ms.id || generateHashId() })); const allStyles = [...customStyles, ...defaultStyles].reduce( (accu, style) => { const hasStyleObject = style.style && typeof style.style === 'object'; accu[hasStyleObject ? 'toLoad' : 'toRequest'][style.id] = style; return accu; }, {toLoad: {}, toRequest: {}} ); this.props.mapStyleActions.loadMapStyles(allStyles.toLoad); this.props.mapStyleActions.requestMapStyles(allStyles.toRequest); }; render() { var { // auth auth, // project, // props id, appName, version, onSaveMap, onViewStateChange, width, height, mapboxApiAccessToken, mapboxApiUrl, getMapboxRef, // redux state mapStyle, mapState, uiState, visState, // actions, visStateActions, mapStateActions, mapStyleActions, uiStateActions } = this.props; const { filters, layers, widgets, splitMaps, // this will store support for split map view is necessary layerOrder, layerBlending, layerClasses, widgetOrder, widgetClasses, interactionConfig, datasets, layerData, hoverInfo, clicked, mousePos, animationConfig } = visState; const notificationPanelFields = { removeNotification: uiStateActions.removeNotification, notifications: uiState.notifications }; const sideFields = { appName, version, auth, project, datasets, filters, layers, layerOrder, layerClasses, widgets, widgetOrder, widgetClasses, interactionConfig, mapStyle, layerBlending, onSaveMap, uiState, mapState, mapStyleActions, visStateActions, uiStateActions, width: this.props.sidePanelWidth }; const mapFields = { auth, project, datasets, getMapboxRef, mapboxApiAccessToken, mapboxApiUrl, mapState, uiState, editor: visState.editor, mapStyle, layers, layerOrder, layerData, layerBlending, filters, widgets, interactionConfig, hoverInfo, clicked, mousePos, readOnly: uiState.readOnly, onViewStateChange, uiStateActions, visStateActions, mapStateActions, animationConfig }; const isSplit = splitMaps && splitMaps.length > 1; const containerW = mapState.width * (Number(isSplit) + 1); const mapContainers = !isSplit ? [ <MapContainer id="save" key={0} index={0} {...mapFields} mapLayers={isSplit ? splitMaps[0].layers : null} /> ] : splitMaps.map((settings, index) => ( <MapContainer id="save" key={index} index={index} {...mapFields} mapLayers={splitMaps[index].layers} /> )); const isExporting = uiState.exportImage.exporting; const theme = this.availableThemeSelector(this.props); // width = uiState.activeSidePanel === null ? width : width - 381; return ( <React.Fragment> <SidePanel {...sideFields} /> <ThemeProvider theme={theme}> <GlobalStyle width={width} height={height} sidePanel={uiState.activeSidePanel} className="kepler-gl" id={`kepler-gl__${id}`} ref={this.root} > {/* <NotificationPanel {...notificationPanelFields} /> */} <div className="maps" style={{display: 'flex'}}> {mapContainers} </div> {isExporting && ( <PlotContainer width={width} height={height} exportImageSetting={uiState.exportImage} mapFields={mapFields} addNotification={uiStateActions.addNotification} startExportingImage={uiStateActions.startExportingImage} setExportImageDataUri={uiStateActions.setExportImageDataUri} setExportImageError={uiStateActions.setExportImageError} /> )} <BottomWidget filters={filters} datasets={datasets} uiState={uiState} layers={layers} animationConfig={animationConfig} visStateActions={visStateActions} sidePanelWidth={ uiState.readOnly ? 0 : this.props.sidePanelWidth + DIMENSIONS.sidePanel.margin.left } containerW={containerW} /> <ModalWrapper mapStyle={mapStyle} visState={visState} mapState={mapState} uiState={uiState} mapboxApiAccessToken={mapboxApiAccessToken} mapboxApiUrl={mapboxApiUrl} visStateActions={visStateActions} uiStateActions={uiStateActions} mapStyleActions={mapStyleActions} rootNode={this.root.current} containerW={containerW} containerH={mapState.height} /> </GlobalStyle> </ThemeProvider> </React.Fragment> ); } } return keplerGlConnect( mapStateToProps, makeMapDispatchToProps )(withTheme(KeplerGL)); } function mapStateToProps(state, props) { return { ...props, visState: state.visState, mapStyle: state.mapStyle, mapState: state.mapState, uiState: state.uiState, auth: props.state.auth, project: props.state.project }; } const defaultUserActions = {}; const getDispatch = dispatch => dispatch; const getUserActions = (dispatch, props) => props.actions || defaultUserActions; function makeGetActionCreators() { return createSelector( [getDispatch, getUserActions], (dispatch, userActions) => { const [ visStateActions, mapStateActions, mapStyleActions, uiStateActions ] = [ VisStateActions, MapStateActions, MapStyleActions, UIStateActions ].map(actions => bindActionCreators(mergeActions(actions, userActions), dispatch) ); return { visStateActions, mapStateActions, mapStyleActions, uiStateActions, dispatch }; } ); } function makeMapDispatchToProps() { const getActionCreators = makeGetActionCreators(); const mapDispatchToProps = (dispatch, ownProps) => { const groupedActionCreators = getActionCreators(dispatch, ownProps); return { ...groupedActionCreators, dispatch }; }; return mapDispatchToProps; } /** * Override default kepler.gl actions with user defined actions using the same key */ function mergeActions(actions, userActions) { const overrides = {}; for (const key in userActions) { if (userActions.hasOwnProperty(key) && actions.hasOwnProperty(key)) { overrides[key] = userActions[key]; } } return {...actions, ...overrides}; } export default KeplerGlFactory;