kepler.gl
Version:
kepler.gl is a webgl based application to visualize large scale location data in the browser
379 lines (340 loc) • 10.3 kB
JavaScript
// Copyright (c) 2018 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 {console as Console} from 'global/window';
import {bindActionCreators} from 'redux';
import {json as requestJson} from 'd3-request';
import styled, {ThemeProvider} from 'styled-components';
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} from 'constants/default-settings';
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 {generateHashId} from 'utils/utils';
import {theme} from 'styles/base';
const GlobalStyle = styled.div`
font-family: ff-clan-web-pro, 'Helvetica Neue', Helvetica, sans-serif;
font-weight: 400;
font-size: 0.875em;
line-height: 1.71429;
*,
*: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};
}
`;
export const keplerGlChildDeps = [
...BottomWidgetFactory.deps,
...SidePanelFactory.deps,
...ModalContainerFactory.deps,
...MapContainerFactory.deps
];
KeplerGlFactory.deps = [
BottomWidgetFactory,
MapContainerFactory,
ModalContainerFactory,
SidePanelFactory,
PlotContainerFactory
];
function KeplerGlFactory(
BottomWidget,
MapContainer,
ModalWrapper,
SidePanel,
PlotContainer
) {
class KeplerGL extends Component {
static defaultProps = {
mapStyles: [],
width: 800,
height: 800,
appName: KEPLER_GL_NAME,
version: KEPLER_GL_VERSION
};
componentWillMount() {
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
) {
this._handleResize(nextProps);
}
}
_handleResize({width, height}) {
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 customeStyles = (this.props.mapStyles || []).map(ms => ({
...ms,
id: ms.id || generateHashId()
}));
[...customeStyles, ...defaultStyles].forEach(
style => {
if (style.style) {
this.props.mapStyleActions.loadMapStyles({
[style.id]: style
})
} else {
this._requestMapStyle(style);
}
}
);
};
_requestMapStyle = (mapStyle) => {
const {url, id} = mapStyle;
requestJson(url, (error, result) => {
if (error) {
Console.warn(`Error loading map style ${mapStyle.url}`);
} else {
this.props.mapStyleActions.loadMapStyles({
[id]: {...mapStyle, style: result}
});
}
});
};
render() {
const {
// props
id,
appName,
version,
onSaveMap,
width,
height,
mapboxApiAccessToken,
// redux state
mapStyle,
mapState,
uiState,
visState,
// actions,
visStateActions,
mapStateActions,
mapStyleActions,
uiStateActions
} = this.props;
const {
filters,
layers,
splitMaps, // this will store support for split map view is necessary
layerOrder,
layerBlending,
layerClasses,
interactionConfig,
datasets,
layerData,
hoverInfo,
clicked
} = visState;
const sideFields = {
appName,
version,
datasets,
filters,
layers,
layerOrder,
layerClasses,
interactionConfig,
mapStyle,
layerBlending,
onSaveMap,
uiState,
mapStyleActions,
visStateActions,
uiStateActions,
width: DIMENSIONS.sidePanel.width
};
const mapFields = {
datasets,
mapboxApiAccessToken,
mapState,
mapStyle,
mapControls: uiState.mapControls,
layers,
layerOrder,
layerData,
layerBlending,
interactionConfig,
hoverInfo,
clicked,
toggleMapControl: uiStateActions.toggleMapControl,
uiStateActions,
visStateActions,
mapStateActions
};
const isSplit = splitMaps && splitMaps.length > 1;
const containerW = mapState.width * (Number(isSplit) + 1);
const mapContainers = !isSplit
? [
<MapContainer
key={0}
index={0}
{...mapFields}
mapLayers={isSplit ? splitMaps[0].layers : null}
/>
]
: splitMaps.map((settings, index) => (
<MapContainer
key={index}
index={index}
{...mapFields}
mapLayers={splitMaps[index].layers}
/>
));
const isExporting = uiState.currentModal === EXPORT_IMAGE_ID;
return (
<ThemeProvider theme={theme}>
<GlobalStyle
style={{
position: 'relative',
width: `${width}px`,
height: `${height}px`
}}
className="kepler-gl"
id={`kepler-gl__${id}`}
innerRef={node => {
this.root = node;
}}
>
{!uiState.readOnly && <SidePanel {...sideFields} />}
<div className="maps" style={{display: 'flex'}}>
{mapContainers}
</div>
{isExporting &&
<PlotContainer
width={width}
height={height}
exportImageSetting={uiState.exportImage}
mapFields={mapFields}
startExportingImage={uiStateActions.startExportingImage}
setExportImageDataUri={uiStateActions.setExportImageDataUri}
/>
}
<BottomWidget
filters={filters}
datasets={datasets}
uiState={uiState}
visStateActions={visStateActions}
sidePanelWidth={
DIMENSIONS.sidePanel.width + DIMENSIONS.sidePanel.margin.left
}
containerW={containerW}
/>
<ModalWrapper
mapStyle={mapStyle}
visState={visState}
mapState={mapState}
uiState={uiState}
mapboxApiAccessToken={mapboxApiAccessToken}
visStateActions={visStateActions}
uiStateActions={uiStateActions}
mapStyleActions={mapStyleActions}
rootNode={this.root}
containerW={containerW}
containerH={mapState.height}
/>
</GlobalStyle>
</ThemeProvider>
);
}
}
return keplerGlConnect(mapStateToProps, mapDispatchToProps)(KeplerGL);
}
function mapStateToProps(state, props) {
return {
...props,
visState: state.visState,
mapStyle: state.mapStyle,
mapState: state.mapState,
uiState: state.uiState
};
}
function mapDispatchToProps(dispatch, ownProps) {
const userActions = ownProps.actions || {};
const [
visStateActions,
mapStateActions,
mapStyleActions,
uiStateActions
] = [
VisStateActions,
MapStateActions,
MapStyleActions,
UIStateActions
].map(actions =>
bindActionCreators(mergeActions(actions, userActions), dispatch)
);
return {
visStateActions,
mapStateActions,
mapStyleActions,
uiStateActions,
dispatch
};
}
/**
* Override default maps-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;