import React from 'react' import PropTypes from 'prop-types' import withStyles from '@mui/styles/withStyles' import DeckGL from '@deck.gl/react' import { ArcLayer, PolygonLayer } from '@deck.gl/layers' import { HeatmapLayer, HexagonLayer } from '@deck.gl/aggregation-layers' import ReactMapGL, { NavigationControl, FullscreenControl, HTMLOverlay } from 'react-map-gl' import DeckArcLayerLegend from './DeckArcLayerLegend' import DeckArcLayerDialog from './DeckArcLayerDialog' import DeckArcLayerTooltip from './DeckArcLayerTooltip' import CircularProgress from '@mui/material/CircularProgress' /* Documentation links: https://deck.gl/#/documentation/getting-started/using-with-react?section=adding-a-base-map https://github.com/uber/deck.gl/blob/6.2-release/examples/website/arc/app.js http://deck.gl/#/documentation/deckgl-api-reference/layers/arc-layer https://blog.mapbox.com/mapbox-gl-js-react-764da6cc074a https://www.mapbox.com/mapbox-gl-js/api#map */ const styles = theme => ({ root: props => ({ height: 400, [theme.breakpoints.up(props.layoutConfig.hundredPercentHeightBreakPoint)]: { height: `calc(100% - ${props.layoutConfig.tabHeight}px)` } }), spinner: { height: 40, width: 40, position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%,-50%)' }, navigationContainer: { position: 'absolute', top: 0, left: 0, padding: theme.spacing(1), zIndex: 1 }, fullscreenButton: { position: 'absolute', top: 105 } }) /** * A component for WebGL maps using deck.gl and ReactMapGL. */ class Deck extends React.Component { state = { viewport: { longitude: this.props.center[1], latitude: this.props.center[0], zoom: this.props.zoom, pitch: 0, bearing: 0, width: 100, height: 100 }, dialog: { open: false, data: null, from: null, to: null }, hoverInfo: null } componentDidMount = () => { this.props.fetchResults({ resultClass: this.props.resultClass, facetClass: this.props.facetClass, sortBy: null }) this.setState({ mounted: true }) } componentDidUpdate = prevProps => { // check if filters have changed if (prevProps.facetUpdateID !== this.props.facetUpdateID) { this.props.fetchResults({ resultClass: this.props.resultClass, facetClass: this.props.facetClass, sortBy: null }) } // if (prevProps.instanceAnalysisDataUpdateID !== this.props.instanceAnalysisDataUpdateID) { // this.setState({ // dialog: { data: this.props.instanceAnalysisData } // }) // } } componentStateEqualsReduxState = () => { const { viewport } = this.state const { longitude, latitude, zoom } = viewport return ( zoom === this.props.zoom && longitude === this.props.center[1] && latitude === this.props.center[0] ) } setDialog = info => { this.setState({ dialog: { open: true, from: info.object.from, to: info.object.to } }) this.props.fetchInstanceAnalysis({ resultClass: `${this.props.resultClass}Dialog`, facetClass: this.props.facetClass, fromID: info.object.from.id, toID: info.object.to.id }) } closeDialog = () => this.setState({ dialog: { open: false, data: null } }) handleOnViewportChange = viewport => { if (this.state.mounted) { this.setState({ viewport }) } } renderSpinner () { if (this.props.fetching || this.props.fetchingInstanceAnalysisData) { return ( <div className={this.props.classes.spinner}> <CircularProgress /> </div> ) } return null } parseCoordinates = data => [+data.long, +data.lat] createArcLayer = data => new ArcLayer({ id: 'arc-layer', data, pickable: true, getWidth: this.props.getArcWidth ? this.props.getArcWidth : 3, getSourceColor: [0, 0, 255, 255], getTargetColor: [255, 0, 0, 255], getSourcePosition: d => this.parseCoordinates(d.from), getTargetPosition: d => this.parseCoordinates(d.to), onClick: info => this.setDialog(info), onHover: info => this.setState({ hoverInfo: info }), autoHighlight: true }) createHeatmapLayer = data => new HeatmapLayer({ id: 'heatmapLayer', data, ...(this.props.perspectiveConfig.resultClasses[this.props.resultClass].heatmapRadiusPixels && { radiusPixels: this.props.perspectiveConfig.resultClasses[this.props.resultClass].heatmapRadiusPixels }), ...(this.props.perspectiveConfig.resultClasses[this.props.resultClass].heatmapThreshold && { threshold: this.props.perspectiveConfig.resultClasses[this.props.resultClass].heatmapThreshold }), ...(this.props.perspectiveConfig.resultClasses[this.props.resultClass].heatmapIntensity && { intensity: this.props.perspectiveConfig.resultClasses[this.props.resultClass].heatmapIntensity }), getPosition: d => [+d.long, +d.lat], getWeight: d => +d.instanceCount }) createHexagonLayer = data => new HexagonLayer({ id: 'hexagon-layer', data, extruded: true, radius: 2000, elevationScale: 100, getPosition: d => [+d.long, +d.lat] /* onHover: ({ object, x, y }) => { const tooltip = `${object.centroid.join(', ')}\nCount: ${object.points.length}` Update tooltip http://deck.gl/#/documentation/developer-guide/adding-interactivity?section=example-display-a-tooltip-for-hovered-object } */ }) createPolygonLayer = data => new PolygonLayer({ id: 'polygon-layer', data, extruded: false, pickable: true, stroked: true, filled: true, lineWidthMinPixels: 1, getPolygon: d => d.polygon, getFillColor: d => d.choroplethColor, getLineColor: [80, 80, 80], getLineWidth: 1 }) render () { const { classes, layerType, fetching, results, showTooltips, portalConfig } = this.props const { mapboxAccessToken, mapboxStyle } = portalConfig.mapboxConfig const { hoverInfo } = this.state const showTooltip = showTooltips && hoverInfo && hoverInfo.object const hasData = !fetching && results && results.length > 0 && ( (results[0].lat && results[0].long) || (results[0].from && results[0].to) || results[0].polygon ) // console.log(hasData) /* It's OK to create a new Layer instance on every render https://github.com/uber/deck.gl/blob/master/docs/developer-guide/using-layers.md#should-i-be-creating-new-layers-on-every-render */ let layer = null if (hasData) { switch (layerType) { case 'arcLayer': layer = this.createArcLayer(results) break case 'heatmapLayer': layer = this.createHeatmapLayer(results) break case 'hexagonLayer': layer = this.createHexagonLayer(results) break case 'polygonLayer': layer = this.createPolygonLayer(results) break default: layer = this.createHeatmapLayer(results) break } } return ( <div className={classes.root}> <ReactMapGL {...this.state.viewport} width='100%' height='100%' reuseMaps mapStyle={`mapbox://styles/mapbox/${mapboxStyle}`} preventStyleDiffing mapboxApiAccessToken={mapboxAccessToken} onViewportChange={this.handleOnViewportChange} > <div className={classes.navigationContainer}> <NavigationControl /> <FullscreenControl className={classes.fullscreenButton} container={document.querySelector('mapboxgl-map')} /> </div> {layerType === 'arcLayer' && <HTMLOverlay redraw={() => <DeckArcLayerLegend title={this.props.legendTitle} fromText={this.props.legendFromText} toText={this.props.legendToText} />} />} <DeckGL viewState={this.state.viewport} layers={[layer]} getCursor={() => 'initial'} {...(layerType === 'polygonLayer' ? { getTooltip: ({ object }) => object && { html: ` <h2>${object.prefLabel}</h2> <div>${object.instanceCount}</div> ` // style: { // backgroundColor: '#f00', // fontSize: '0.8em' // } } } : {}) } /> {this.renderSpinner()} {layerType === 'arcLayer' && this.props.instanceAnalysisData && this.state.dialog.open && <DeckArcLayerDialog onClose={this.closeDialog.bind(this)} data={this.props.instanceAnalysisData} from={this.state.dialog.from} to={this.state.dialog.to} fromText={this.props.fromText} toText={this.props.toText} countText={this.props.countText} listHeadingSingleInstance={this.props.listHeadingSingleInstance} listHeadingMultipleInstances={this.props.listHeadingMultipleInstances} instanceVariable={[this.props.instanceVariable]} resultClass={this.props.resultClass} facetClass={this.props.facetClass} />} {layerType === 'arcLayer' && showTooltip && <DeckArcLayerTooltip data={hoverInfo} fromText={this.props.fromText} toText={this.props.toText} countText={this.props.countText} showMoreText={this.props.showMoreText} />} </ReactMapGL> </div> ) } } Deck.propTypes = { classes: PropTypes.object.isRequired, results: PropTypes.array, layerType: PropTypes.oneOf(['arcLayer', 'heatmapLayer', 'hexagonLayer', 'polygonLayer']), tooltips: PropTypes.bool, facetUpdateID: PropTypes.number, fetchResults: PropTypes.func, resultClass: PropTypes.string, facetClass: PropTypes.string, fetching: PropTypes.bool.isRequired, legendComponent: PropTypes.element, fromText: PropTypes.string, toText: PropTypes.string, legendFromText: PropTypes.string, legendToText: PropTypes.string, legendTitle: PropTypes.string, showMoreText: PropTypes.string, listHeadingSingleInstance: PropTypes.string, listHeadingMultipleInstances: PropTypes.string, layoutConfig: PropTypes.object.isRequired } export const DeckComponent = Deck export default withStyles(styles)(Deck)