UNPKG

kepler.gl

Version:

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

449 lines (385 loc) 12.9 kB
// 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. // libraries import React, {Component} from 'react'; import PropTypes from 'prop-types'; import MapboxGLMap from 'react-map-gl'; import DeckGL from 'deck.gl'; import {GL} from 'luma.gl'; import {registerShaderModules, setParameters} from 'luma.gl'; import pickingModule from 'shaderlib/picking-module'; import brushingModule from 'shaderlib/brushing-module'; // components import MapPopoverFactory from 'components/map/map-popover'; import MapControlFactory from 'components/map/map-control'; import {StyledMapContainer} from 'components/common/styled-components'; // Overlay type import {generateMapboxLayers, updateMapboxLayers} from '../layers/mapbox-utils'; import {transformRequest} from 'utils/map-style-utils/mapbox-utils'; // default-settings import {LAYER_BLENDINGS} from 'constants/default-settings'; const MAP_STYLE = { container: { display: 'inline-block', position: 'relative' }, top: { position: 'absolute', top: '0px', pointerEvents: 'none' } }; const getGlConst = d => GL[d]; const MAPBOXGL_STYLE_UPDATE = 'style.load'; MapContainerFactory.deps = [ MapPopoverFactory, MapControlFactory]; export default function MapContainerFactory(MapPopover, MapControl) { class MapContainer extends Component { static propTypes = { // required datasets: PropTypes.object, interactionConfig: PropTypes.object.isRequired, layerBlending: PropTypes.string.isRequired, layerOrder: PropTypes.arrayOf(PropTypes.any).isRequired, layerData: PropTypes.arrayOf(PropTypes.any).isRequired, layers: PropTypes.arrayOf(PropTypes.any).isRequired, mapState: PropTypes.object.isRequired, mapStyle: PropTypes.object.isRequired, mapControls: PropTypes.object.isRequired, mapboxApiAccessToken: PropTypes.string.isRequired, toggleMapControl: PropTypes.func.isRequired, visStateActions: PropTypes.object.isRequired, mapStateActions: PropTypes.object.isRequired, // optional isExport: PropTypes.bool, clicked: PropTypes.object, hoverInfo: PropTypes.object, mapLayers: PropTypes.object, onMapToggleLayer: PropTypes.func, onMapStyleLoaded: PropTypes.func, onMapRender: PropTypes.func }; static defaultProps = { MapComponent: MapboxGLMap }; constructor(props) { super(props); this.state = { mousePosition: [0, 0] }; this.previousLayers = { // [layers.id]: mapboxLayerConfig }; } componentWillUnmount() { // unbind mapboxgl event listener if (this._map) { this._map.off(MAPBOXGL_STYLE_UPDATE); } } /* component private functions */ _onCloseMapPopover = () => { this.props.visStateActions.onLayerClick(null); }; _onLayerSetDomain = (idx, colorDomain) => { this.props.visStateActions.layerConfigChange(this.props.layers[idx], { colorDomain }); }; _onWebGLInitialized = gl => { registerShaderModules( [pickingModule, brushingModule], { ignoreMultipleRegistrations: true }); // allow Uint32 indices in building layer // gl.getExtension('OES_element_index_uint'); }; _onMouseMove = evt => { const {interactionConfig: {brush}} = this.props; if (evt.nativeEvent && brush.enabled) { this.setState({ mousePosition: [evt.nativeEvent.offsetX, evt.nativeEvent.offsetY] }); } }; _handleMapToggleLayer = layerId => { const {index: mapIndex = 0, visStateActions} = this.props; visStateActions.toggleLayerForMap(mapIndex, layerId); }; _setMapboxMap = (mapbox) => { if (!this._map && mapbox) { this._map = mapbox.getMap(); // bind mapboxgl event listener this._map.on(MAPBOXGL_STYLE_UPDATE, () => { // force refresh mapboxgl layers updateMapboxLayers( this._map, this._renderMapboxLayers(), this.previousLayers, this.props.mapLayers, {force: true} ); if (typeof this.props.onMapStyleLoaded === 'function') { this.props.onMapStyleLoaded(this._map); } }); this._map.on('render', () => { if (typeof this.props.onMapRender === 'function') { this.props.onMapRender(this._map); } }); } } _onBeforeRender = ({gl}) => { this._setlayerBlending(gl); }; _setlayerBlending = gl => { const blending = LAYER_BLENDINGS[this.props.layerBlending]; const {blendFunc, blendEquation} = blending; setParameters(gl, { [GL.BLEND]: true, ...(blendFunc ? { blendFunc: blendFunc.map(getGlConst), blendEquation: Array.isArray(blendEquation) ? blendEquation.map(getGlConst) : getGlConst(blendEquation) } : {}) }); }; /* component render functions */ /* eslint-disable complexity */ _renderObjectLayerPopover() { // TODO: move this into reducer so it can be tested const { mapState, hoverInfo, clicked, datasets, interactionConfig, layers, mapLayers } = this.props; // if clicked something, ignore hover behavior const objectInfo = clicked || hoverInfo; if ( !interactionConfig.tooltip.enabled || !objectInfo || !objectInfo.picked ) { // nothing hovered return null; } const {lngLat, object, layer: overlay} = objectInfo; // deckgl layer to kepler-gl layer const layer = layers[overlay.props.idx]; if ( !layer || !layer.config.isVisible || !object || !layer.getHoverData || (mapLayers && !mapLayers[layer.id].isVisible) ) { // layer is not visible return null; } const {config: {dataId}} = layer; const {allData, fields} = datasets[dataId]; const data = layer.getHoverData(object, allData); // project lnglat to screen so that tooltip follows the object on zoom const {viewport} = overlay.context; const {x, y} = this._getHoverXY(viewport, lngLat) || objectInfo; const popoverProps = { data, fields, fieldsToShow: interactionConfig.tooltip.config.fieldsToShow[dataId], layer, isVisible: true, x, y, freezed: Boolean(clicked), onClose: this._onCloseMapPopover, mapState }; return ( <div> <MapPopover {...popoverProps} /> </div> ); } /* eslint-enable complexity */ _getHoverXY(viewport, lngLat) { const screenCoord = !viewport || !lngLat ? null : viewport.project(lngLat); return screenCoord && {x: screenCoord[0], y: screenCoord[1]}; } _shouldRenderLayer(layer, data, mapLayers) { const isAvailableAndVisible = !(mapLayers && mapLayers[layer.id]) || mapLayers[layer.id].isVisible; return layer.shouldRenderLayer(data) && isAvailableAndVisible; } _renderLayer = (overlays, idx) => { const { layers, layerData, hoverInfo, clicked, mapLayers, mapState, interactionConfig } = this.props; const {mousePosition} = this.state; const layer = layers[idx]; const data = layerData[idx]; const layerInteraction = { mousePosition }; const objectHovered = clicked || hoverInfo; const layerCallbacks = { onSetLayerDomain: val => this._onLayerSetDomain(idx, val) }; if (!this._shouldRenderLayer(layer, data, mapLayers)) { return overlays; } let layerOverlay = []; // Layer is Layer class if (typeof layer.renderLayer === 'function') { layerOverlay = layer.renderLayer({ data, idx, layerInteraction, objectHovered, mapState, interactionConfig, layerCallbacks }); } if (layerOverlay.length) { overlays = overlays.concat(layerOverlay); } return overlays; }; _renderOverlay() { const { mapState, layerData, layerOrder, visStateActions } = this.props; let deckGlLayers = []; // wait until data is ready before render data layers if (layerData && layerData.length) { // last layer render first deckGlLayers = layerOrder .slice() .reverse() .reduce(this._renderLayer, []); } return ( <DeckGL viewState={mapState} id="default-deckgl-overlay" layers={deckGlLayers} onWebGLInitialized={this._onWebGLInitialized} onBeforeRender={this._onBeforeRender} onLayerHover={visStateActions.onLayerHover} onLayerClick={visStateActions.onLayerClick} /> ); } _renderMapboxLayers() { const { layers, layerData, layerOrder } = this.props; return generateMapboxLayers(layers, layerData, layerOrder); } _renderMapboxOverlays() { if (this._map && this._map.isStyleLoaded()) { const mapboxLayers = this._renderMapboxLayers(); updateMapboxLayers( this._map, mapboxLayers, this.previousLayers, this.props.mapLayers ); this.previousLayers = mapboxLayers.reduce((final, layer) => ({ ...final, [layer.id]: layer.config }), {}) } } render() { const {mapState, mapStyle, mapStateActions} = this.props; const {updateMap, onMapClick} = mapStateActions; if (!mapStyle.bottomMapStyle) { // style not yet loaded return <div/>; } const {mapLayers, layers, datasets, mapboxApiAccessToken, mapControls, toggleMapControl} = this.props; const mapProps = { ...mapState, preserveDrawingBuffer: true, mapboxApiAccessToken, onViewportChange: updateMap, transformRequest }; return ( <StyledMapContainer style={MAP_STYLE.container} onMouseMove={this._onMouseMove}> <MapControl datasets={datasets} dragRotate={mapState.dragRotate} isSplit={mapState.isSplit} isExport={this.props.isExport} layers={layers} mapIndex={this.props.index} mapLayers={mapLayers} mapControls={mapControls} scale={mapState.scale || 1} top={0} onTogglePerspective={mapStateActions.togglePerspective} onToggleSplitMap={mapStateActions.toggleSplitMap} onMapToggleLayer={this._handleMapToggleLayer} onToggleFullScreen={mapStateActions.toggleFullScreen} onToggleMapControl={toggleMapControl} /> <this.props.MapComponent {...mapProps} key="bottom" ref={this._setMapboxMap} mapStyle={mapStyle.bottomMapStyle} onClick={onMapClick} getCursor={this.props.hoverInfo ? () => 'pointer' : undefined} > {this._renderOverlay()} {this._renderMapboxOverlays()} </this.props.MapComponent> {mapStyle.topMapStyle && ( <div style={MAP_STYLE.top}> <this.props.MapComponent {...mapProps} key="top" mapStyle={mapStyle.topMapStyle} /> </div> )} {this._renderObjectLayerPopover()} </StyledMapContainer> ); } } return MapContainer; }