import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
import olInteractionDraw from 'ol/interaction/Draw'
import { connectToContext } from 'Provider'
import PopupBase from './PopupBase'
import PopupDefaultInsert from './PopupInsert/PopupDefaultInsert'
import { addMovementListener, getLayersAndFeaturesForEvent, getPopupPositionFromFeatures, removeMovementListener } from './utils'
/**
* @component
* @category Popup
* @since 0.2.0
*/
class Popup extends Component {
constructor (props) {
super(props)
this.state = {
clickCoordinate: [0, 0],
clickPixel: [0, 0],
features: [],
loading: false,
popupPosition: {
arrow: 'none',
fits: false,
pixel: [0, 0]
},
show: false
}
this.timer = 0
this.isDoubleClick = false
this.defaultState = this.state
}
componentDidMount () {
const { map } = this.props
// bind to map click events
map.on('click', this.mapClickHandler)
map.on('dblclick', this.mapDoubleClickHandler)
}
componentWillUnmount () {
const { map } = this.props
map.un('click', this.mapClickHandler)
map.un('dblclick', this.mapDoubleClickHandler)
}
mapDoubleClickHandler = () => {
clearTimeout(this.timer)
this.isDoubleClick = true
this.hidePopup()
}
mapClickHandler = e => {
this.timer = setTimeout(() => {
if (!this.isDoubleClick) {
// Get the interactions from the map as an array.
const interactions = e.map.getInteractions().getArray()
// This checks to see if there is an active Draw interaction on the map and prevents the popup showing if it returns true.
if (interactions.find((i) => i instanceof olInteractionDraw && i.get('active'))) return this.hidePopup()
this.checkForFeaturesAtClick(e)
}
this.isDoubleClick = false
}, 300)
}
checkForFeaturesAtClick = e => {
const { map } = this.props
/**
this util returns an array of promises for each layer at the click location that resolve once wms features
are converted to wfs and adds them to the map (immediately resolves for features already on the map)
*/
const promises = getLayersAndFeaturesForEvent(e)
// cancel if no layers at click location
// do not add map movement listener or get features from layers
if (!promises.length) return this.hidePopup()
// when the map is panned, we need to re-calculate the position of the popup
const popupMoveHandler = () => {
const { show, features, popupPosition: lastPosition } = this.state
const opts = { lastPosition } // use current position as lastPosition for animation when moving map
// only compute new positions if the popup is showing
if (show) {
// this util returns a position object that is used by the popup component in render()
const popupPosition = getPopupPositionFromFeatures({ map }, features, opts)
this.setState({ popupPosition })
}
}
// this adds three listeners (map move, resize, zoom), all of which should re-position the popup
this.movementListener = addMovementListener(map, popupMoveHandler)
// parse promises for layers and features
this.getNewFeatures(e, promises)
}
getNewFeatures = async (e, promises) => {
const { onMapClick } = this.props
const popupPositionWhileLoading = getPopupPositionFromFeatures(e)
// show popup in loading state while before resolving
this.setState({
clickCoordinate: e.coordinate,
clickPixel: e.pixel,
loading: true,
popupPosition: popupPositionWhileLoading,
show: true
}, () => onMapClick(this.state))
const layers = await Promise.all(promises)
const parsedFeatures = layers.reduce((acc, { features }) => {
return [...acc, ...features]
}, [])
if (!parsedFeatures.length) return this.hidePopup()
// ol returns these in reverse z-index order
const features = parsedFeatures.reverse()
const popupPosition = getPopupPositionFromFeatures(e, features)
this.setState({ features, loading: false, popupPosition }, () => onMapClick(this.state))
}
hidePopup = () => {
const { onMapClick } = this.props
// stop tracking movement when popup show is set to false
this.movementListener && removeMovementListener(this.movementListener)
this.setState({ ...this.defaultState }, () => onMapClick(this.state))
}
onDragEnd = e => {
// if drag occurs in PopupBase, update pixel in state here
this.setState({
popupPosition: {
...this.state.popupPosition,
pixel: e.pinnedPixel
}
})
}
render () {
const { actions, children, map, show: propShow } = this.props
const { features, loading, popupPosition: { arrow, pixel }, show: stateShow } = this.state
const show = typeof propShow === 'boolean' ? propShow : stateShow // keep show prop as source of truth over state
return (
!show
? null
: (
ReactDOM.createPortal(
<PopupBase arrow={arrow} onPopupDragEnd={this.onDragEnd} pixel={pixel} {...this.props} show={show}>
{children || ( // default ui if no children are passed
<PopupDefaultInsert
actions={actions}
features={features}
loading={loading}
onClose={this.hidePopup}
/>
)}
</PopupBase>,
map.getTargetElement()
)
)
)
}
}
Popup.defaultProps = {
onMapClick: () => {},
show: undefined
}
Popup.propTypes = {
/** components passed to PopupDefaultInsert to render as actions */
actions: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]),
/** Pass components as children of Popup component */
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]),
/** a reference to openlayers map object */
map: PropTypes.object.isRequired,
/** callback fired on map clicks with state object:
{
clickCoordinate: [0, 0],
clickPixel: [0, 0],
features: [],
loading: false, // true after click before layers/features load
popupPosition: {
arrow: 'none',
fits: true, // does the popup have room to render around the feature bbox
pixel: [0, 0]
},
show: false // suggestion to display or hide popup -- <Popup show={sourceOfTruth}> (show prop takes priority)
}
*/
onMapClick: PropTypes.func,
/** boolean that is respected over internal state */
show: PropTypes.bool
}
export default connectToContext(Popup)