import React from 'react'
import PropTypes from 'prop-types'
import { ButtonContainer } from './styled'
import Line from './Line'
import Box from './Box'
import Circle from './Circle'
import Point from './Point'
import Polygon from './Polygon'
import Freehand from './Freehand'
import { DrawToolbar } from 'DrawToolbar'
import olFeature from 'ol/Feature'
import olDrawInteraction from 'ol/interaction/Draw'
import olSnapInteraction from 'ol/interaction/Snap'
import olGeomTypes from 'ol/geom/GeometryType'
import { VectorLayer } from '../classes'
import olLayerVector from 'ol/layer/Vector'
import olSourceVector from 'ol/source/Vector'
import olGeomCircle from 'ol/geom/Circle'
import { fromCircle } from 'ol/geom/Polygon'
import olCollection from 'ol/Collection'
import { connectToContext } from 'Provider'
import { getStyledFeatures } from './utils'
const OL_DRAW_TYPES = [...Object.values(olGeomTypes)]
/**
* A component for rendering basic draw tools
* @component
* @category Draw
* @since 0.18.0
*/
class Draw extends React.Component {
constructor (props) {
super(props)
this.state = {
type: '',
freehand: false,
feature: null,
interactions: []
}
this.escFunction = this.escFunction.bind(this)
}
componentDidMount () {
const { selectInteraction } = this.props
if (selectInteraction) {
selectInteraction.on('select', this.selectListener)
this.setState({ feature: selectInteraction.getFeatures().getArray()[0] })
}
document.addEventListener('keydown', this.escFunction, false)
}
componentWillUnmount () {
const { selectInteraction } = this.props
const { interactions } = this.state
if (selectInteraction) selectInteraction.un('select', this.selectListener)
if (interactions && Array.isArray(interactions) && interactions.length) this.handleDrawCancel()
document.removeEventListener('keydown', this.escFunction, false)
}
escFunction (event) {
if (event.keyCode === 27) { // esc key
this.handleDrawCancel()
}
}
selectListener = ({ selected }) => {
this.setState({ feature: selected[0] })
this.props?.selectedFeature?.(selected[0]) // eslint-disable-line no-unused-expressions
}
addInteraction = (opts) => {
const { type, freehand, geometryFunction } = opts
const { map, source, drawOpts, onInteractionAdded, preferences, getStyledFeatures } = this.props
const { interactions } = this.state
// if there's an existing interaction, cancel before we start a new one
if (Array.isArray(interactions) && interactions.length) this.handleDrawCancel()
if (!type) throw new Error('Needs a valid draw type')
if (!OL_DRAW_TYPES.includes(type)) throw new Error(`${type} is not a valid draw type`)
// construct the interaction parameters from the source and the optional draw options provideed from props and the arguments
const drawInteractionOpts = { source, stopClick: true, ...drawOpts, ...opts }
const drawInteraction = new olDrawInteraction(drawInteractionOpts)
const newInteractions = [drawInteraction]
const snap = preferences ? preferences.get?.('_SNAPPING_ENABLED') : this.props.snap
// if snap prop is true (default is true) create and push a snap interaction to the interactions array
if (snap) {
const snapOpts = preferences ? { pixelTolerance: preferences.get?.('_SNAPPING_TOLERANCE') } : this.props.snapOpts
const mapLayers = map.getLayers().getArray()
const res = map.getView().getResolution()
const vectorLayers = mapLayers.filter(layer => layer instanceof olLayerVector || layer instanceof VectorLayer)
const snapFeatures = new olCollection(getStyledFeatures(vectorLayers, res).map(([feature]) => feature))
const snapInteractionOpts = { features: snapFeatures, ...snapOpts }
const snapInteraction = new olSnapInteraction(snapInteractionOpts)
newInteractions.push(snapInteraction)
}
drawInteraction.on('drawstart', this.handleInteractionEvent)
drawInteraction.on('drawend', this.handleInteractionEvent)
// store some draw state and then add the interactions to the map
this.setState(
{ interactions: newInteractions, type, freehand, geometryFunction },
() => {
this.state.interactions.forEach(interaction => map.addInteraction(interaction))
// callback function for implementors
onInteractionAdded(drawInteraction)
}
)
}
handleInteractionEvent = (event) => {
const { onDrawBegin } = this.props
const { type, feature } = event
switch (type) {
case 'drawstart':
this.setState({ feature })
return onDrawBegin(feature, event)
case 'drawend':
return this.handleDrawFinish(feature, event)
default:
return false
}
}
getDrawInteraction = () => {
const { interactions } = this.state
return interactions.find(interaction => interaction instanceof olDrawInteraction)
}
getSnapInteraction = () => {
const { interactions } = this.state
return interactions.find(interaction => interaction instanceof olSnapInteraction)
}
handleDrawFinish = (feature) => {
const { onDrawFinish, map } = this.props
const { interactions } = this.state
// we only have a feature when the draw is 'finished' via OL
// and therefore there is a drawend event with a feature
const haveFeature = feature instanceof olFeature
const drawInteraction = this.getDrawInteraction()
// if we don't have a featue (user hit the DOM finish button)
// finish the drawing (this will trigger the drawend event)
if (drawInteraction && !haveFeature) drawInteraction.finishDrawing()
interactions.forEach(interaction => map.removeInteraction(interaction))
this.setState({ interactions: [], type: null, measureFeature: null })
const geom = feature.getGeometry()
const geomIsCircle = geom instanceof olGeomCircle
if (geomIsCircle) {
const circleGeom = fromCircle(geom, 180)
feature.setGeometry(circleGeom)
}
onDrawFinish(feature)
}
handleDrawCancel = () => {
const { source, map, onDrawCancel } = this.props
const { interactions } = this.state
const drawInteraction = this.getDrawInteraction()
try {
// unbind drawend (otherwise this gets fired by finishDrawing on the next line)
drawInteraction.un('drawend', this.handleInteractionEvent)
// now we can safely 'finish' the draw interaction (with no events fired)
drawInteraction.finishDrawing()
// now we remove the feature which was just added b/c we 'finished it'
source.removeFeature(source.getFeatures().pop())
} catch (e) {
console.warn(`Openlayers was unable to finish the drawing due to`, e) // eslint-disable-line
}
interactions.forEach(interaction => map.removeInteraction(interaction))
// callback function for implementors
onDrawCancel(drawInteraction)
this.setState({ interactions: [], type: null, feature: null })
}
render () {
const { type, freehand, geometryFunction, interactions } = this.state
const { translations } = this.props
return (
<div data-testid='Draw.container'>
{this.props.children
? React.Children.map(this.props.children, child => {
const moddedChild = React.cloneElement(child, {
...{ addInteraction: this.addInteraction, type, freehand, geometryFunction }, ...child.props
})
return moddedChild
})
: <ButtonContainer>
<Point addInteraction={this.addInteraction} type={type}
tooltipTitle={translations['_ol_kit.draw.pointTooltip']} />
<Line addInteraction={this.addInteraction} type={type}
freehand={freehand} tooltipTitle={translations['_ol_kit.draw.lineTooltip']} />
<Polygon addInteraction={this.addInteraction} type={type}
tooltipTitle={translations['_ol_kit.draw.polygonTooltip']} />
<Circle addInteraction={this.addInteraction} type={type}
geometryFunction={geometryFunction} tooltipTitle={translations['_ol_kit.draw.circleTooltip']} />
<Box addInteraction={this.addInteraction} type={type}
geometryFunction={geometryFunction} tooltipTitle={translations['_ol_kit.draw.boxTooltip']} />
<Freehand addInteraction={this.addInteraction} type={type} freehand={freehand}
tooltipTitle={translations['_ol_kit.draw.freehandTooltip']} />
</ButtonContainer>}
{
(Array.isArray(interactions) && interactions.length) ? (<DrawToolbar onFinish={this.handleDrawFinish} onCancel={this.handleDrawCancel} />) : null // eslint-disable-line
}
</div>
)
}
}
Draw.propTypes = {
/** openlayers map */
map: PropTypes.object.isRequired,
/** openlayers layer source */
source: PropTypes.object,
/** callback that's called when the feature's draw is completed and returns an openlayers feature */
onDrawFinish: PropTypes.func,
/** callback that's called when the feature's draw is started and returns an openlayers feature */
onDrawBegin: PropTypes.func,
/** callback that's called when the draw button icon is clicked and adds a openlayers draw interaction */
onInteractionAdded: PropTypes.func,
/** callback that's called when the feature's draw is canceled */
onDrawCancel: PropTypes.func,
/** openlayers draw interaction constructor props */
drawOpts: PropTypes.object,
/** translations object */
translations: PropTypes.object,
/** reference to openlayers select interaction which can optionally be managed by IA */
selectInteraction: PropTypes.object,
/** callback that returns the selected openlayers feature from the map */
selectedFeature: PropTypes.func,
/** openlayers snap opts object */
snapOpts: PropTypes.object,
/** boolean for enabling snap interaction */
snap: PropTypes.bool,
/** function to retrieve openlayers features and their styles */
getStyledFeatures: PropTypes.func,
/** The buttons passed to the Draw container */
children: PropTypes.node,
/* A preferences object */
preferences: PropTypes.object
}
Draw.defaultProps = {
getStyledFeatures,
drawOpts: {},
source: new olSourceVector(),
snapOpts: {},
snap: true,
onDrawFinish: () => {},
onDrawBegin: () => {},
onInteractionAdded: () => {},
onDrawCancel: () => {}
}
export default connectToContext(Draw)