import React, { Component } from 'react' import PropTypes from 'prop-types' import withStyles from '@mui/styles/withStyles' import intl from 'react-intl-universal' import ReactMapGL, { NavigationControl, FullscreenControl } from 'react-map-gl' import DeckGL, { ScatterplotLayer } from 'deck.gl' import Paper from '@mui/material/Paper' import TemporalMapTimeSlider from './TemporalMapTimeSlider' import './TemporalMapCommon.scss' import Typography from '@mui/material/Typography' import { has } from 'lodash' import Moment from 'moment' import { extendMoment } from 'moment-range' const moment = extendMoment(Moment) const styles = theme => ({ root: { height: 400, [theme.breakpoints.up('md')]: { height: 'calc(100% - 72px)' }, position: 'relative' }, tooltipContainer: { position: 'absolute', zIndex: 1, padding: theme.spacing(1), maxWidth: 500 }, navigationContainer: { position: 'absolute', top: 0, left: 0, padding: theme.spacing(1), zIndex: 1 }, fullscreenButton: { marginTop: theme.spacing(1) } }) /** * A component for displaying a WebGL map with an animated layer. * Based on https://github.com/AdriSolid/DECK.GL-Time-Slider */ class TemporalMap extends Component { constructor (props) { super(props) this.mapElementRef = React.createRef() this.state = { viewport: { longitude: 26.91, latitude: 62.326, zoom: 5.5, pitch: 0, bearing: 0 }, data: [], memory: [], dates: [], mounted: false } } componentDidMount () { this.props.fetchResults({ resultClass: this.props.resultClass, facetClass: this.props.facetClass }) this.setState({ mounted: true }) } componentDidUpdate = prevProps => { if (prevProps.results !== this.props.results) { const uniqueDates = this.props.results .map(d => d.startDate) .filter((value, index, self) => self.indexOf(value) === index) .sort() const startDate = uniqueDates[0] const endDate = uniqueDates[uniqueDates.length - 1] const range = moment.range(startDate, endDate) let days = Array.from(range.by('day')) days = days.map(m => m.format('YYYY-MM-DD')) const sliderValue = this.props.animationValue[0] const filteredData = this._filterData(sliderValue, this.props.results, days) this.setState({ data: filteredData, memory: this.props.results, dates: days }) } if (prevProps.animationValue !== this.props.animationValue) { const { memory, dates } = this.state const sliderValue = this.props.animationValue[0] const filteredData = this._filterData(sliderValue, memory, dates) this.setState({ data: filteredData }) } // check if filters have changed if (prevProps.facetUpdateID !== this.props.facetUpdateID) { this.props.animateMap([0]) // reset time slider this.props.fetchResults({ resultClass: this.props.resultClass, facetClass: this.props.facetClass, sortBy: null }) } }; _filterData = (sliderValue, data, dates) => { const animationCurrentDate = Date.parse(dates[sliderValue]) const newData = data.filter(value => { return Date.parse(value.startDate) <= animationCurrentDate }) newData.map(value => { const startDate = Date.parse(value.startDate) const range = moment.range(startDate, animationCurrentDate) if (range.diff('days') >= 2) { value.isNew = false } else { value.isNew = true } return value }) return newData } _renderTooltip () { const { hoveredObject, pointerX, pointerY } = this.state || {} return hoveredObject && ( <Paper className={this.props.classes.tooltipContainer} style={{ left: pointerX + 10, top: pointerY + 10 }}> <Typography variant='h6'> {hoveredObject.prefLabel} </Typography> <Typography> {intl.get('perspectives.battles.temporalMap.municipality')}: {hoveredObject.greaterPlace} </Typography> <Typography> {intl.get('perspectives.battles.temporalMap.startDate')}: {moment(hoveredObject.startDate).format('DD.MM.YYYY')} </Typography> <Typography> {intl.get('perspectives.battles.temporalMap.endDate')}: {moment(hoveredObject.endDate).format('DD.MM.YYYY')} </Typography> {has(hoveredObject, 'units') && <Typography> {intl.get('perspectives.battles.temporalMap.units')}: {hoveredObject.units} </Typography>} </Paper> ) } _renderLayers () { const { data } = this.state return [ new ScatterplotLayer({ id: 'time-layer', data, opacity: 0.3, stroked: true, filled: true, radiusScale: 15, radiusMinPixels: 8, radiusMaxPixels: 100, lineWidthMinPixels: 1, getPosition: d => [+d.long, +d.lat], getFillColor: d => d.isNew ? [255, 0, 0] : [0, 0, 0], pickable: true, autoHighlight: true, onHover: info => this.setState({ hoveredObject: info.object, pointerX: info.x, pointerY: info.y }) }) ] } handleOnViewportChange = viewport => this.state.mounted && this.setState({ viewport }); render () { const { viewport, memory, dates } = this.state const { classes, animateMap, portalConfig } = this.props const { mapboxAccessToken, mapboxStyle } = portalConfig.mapboxConfig return ( <div id='temporal-map-root' ref={this.mapElementRef} className={classes.root}> <ReactMapGL {...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('temporal-map-root')} /> </div> <DeckGL layers={this._renderLayers()} viewState={viewport} /> <TemporalMapTimeSlider mapElementRef={this.mapElementRef} memory={memory} dates={dates} animateMap={animateMap} initialValue={this.props.animationValue[0]} sliderDuration={portalConfig.temporalMapConfig.sliderDuration} /> {this._renderTooltip()} </ReactMapGL> </div> ) } } TemporalMap.propTypes = { /** * Material-UI styles. */ classes: PropTypes.object.isRequired, /** * Faceted search results. */ results: PropTypes.array, /** * Result class for fetching the results. */ resultClass: PropTypes.string.isRequired, /** * Facet class for fetching the results. */ facetClass: PropTypes.string.isRequired, /** * Redux action for fetching the results. */ fetchResults: PropTypes.func.isRequired, /** * State of the animation. */ animationValue: PropTypes.array.isRequired, /** * Redux action for animation. */ animateMap: PropTypes.func.isRequired, /** * ID for detecting updates in facets. */ facetUpdateID: PropTypes.number.isRequired } export const TemporalMapComponent = TemporalMap export default withStyles(styles)(TemporalMap)