import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Translate from 'classes/Translate'
import { immediateEditStyle } from './styles'
import { getVectorContext } from 'ol/render'
import { Toolbar } from 'Toolbar'
import { Knob } from 'react-rotary-knob'
import { connectToContext } from 'Provider'
import { Card, Grid, CardActions, Button, FormControlLabel } from '@material-ui/core'
import { withStyles } from '@material-ui/core/styles'
import centroid from '@turf/centroid'
import { olKitTurf } from './utils'
import olInteractionModify from 'ol/interaction/Modify'
import olCollection from 'ol/Collection'
import olStyleStyle from 'ol/style/Style'
const ButtonCardActions = withStyles(() => ({
root: {
padding: '4px 4px 3px 4px'
}
}))(CardActions)
const LeftCard = withStyles(() => ({
root: {
borderTopLeftRadius: '4px',
borderBottomLeftRadius: '4px',
borderBottomRightRadius: '0px',
borderTopRightRadius: '0px',
height: '38px'
}
}))(Card)
const CenterCard = withStyles(() => ({
root: {
borderRadius: '0px',
paddingLeft: '20px',
marginLeft: '0px',
height: '38px'
}
}))(Card)
const RightCard = withStyles(() => ({
root: {
borderTopRightRadius: '4px',
borderBottomRightRadius: '4px',
borderTopLeftRadius: '0px',
borderBottomLeftRadius: '0px',
marginLeft: '0px !important',
height: '38px'
}
}))(Card)
/**
* A component to edit geometries
* @component
* @category FeatureEditor
* @since 1.16.0
*/
class FeatureEditor extends Component {
constructor (props) {
super(props)
this.state = {
interactions: [],
editingFeature: null,
showMeasurements: false,
rotation: 0,
style: null
}
}
/** In the past we've used temporary layers added to the map to avoid modifying the original features directly.
However, this leads to a lot of cleanup since those layers need to be added/removed from both the map and state.
That is fine as long as everything goes smoothly but if there is additional logic listening to the map for changes to it's features
or layers than we can get left in a broken state that requires a reload. Using vectorContext.drawFeature instead of
a layer added to the map alleviates at least some of this risk.*/
_renderFeature = (vectorContext, feature, editStyle = this.props.editStyle) => { // vectorContext.drawFeature only respects a style object and since it is common to have style functions and arrays in Openlayers we need to break the other formats down into objects
const { map, areaUOM, distanceUOM, translations } = this.props
const { showMeasurements } = this.state
const measurementStyles = showMeasurements ? editStyle(feature, map, showMeasurements, { areaUOM, distanceUOM }, translations) : editStyle // eslint-disable-line
const styleType = Array.isArray(measurementStyles) ? 'array' : typeof editStyle
try {
switch (styleType) {
case 'array':
measurementStyles.map(style => {
const geom = style.getGeometry() ? style.getGeometry() : feature.getGeometry()
vectorContext.setStyle(style)
vectorContext.drawGeometry(geom)
}) // Arrays of style objects require a feature to be drawn for each style object in the array. This could also be recursively called but that would add extra complexity.
break
case 'function':
this._renderFeature(
vectorContext,
feature,
editStyle(feature, map, showMeasurements, { areaUOM, distanceUOM }, translations)
) // Openlayers style functions return style objects or arrays of style objects so we can call functions recursively.
break
default: // style object
vectorContext.drawFeature(feature, editStyle)
}
} catch (e) {
console.warn(`Geokit was unable to draw the features to the map, this is most likely due to an invalid style object: ${e.message}`, e) // eslint-disable-line
}
}
_renderEditOverlay = (e) => {
const { map, editStyle } = this.props
const { editingFeature } = this.state
const vectorContext = getVectorContext(e)
if (!editingFeature) return // to avoid using a setStateCallback we just check for editFeatures first
this._renderFeature(vectorContext, editingFeature, editStyle)
return map.render() // render the results asynchronously
}
_addPostComposeListener = () => {
const { editingFeature } = this.state
editingFeature?.get('_ol_kit_parent')?.on('postrender', this._renderEditOverlay) // eslint-disable-line
}
_removePostComposeListener = () => {
const { editingFeature } = this.state
editingFeature?.get('_ol_kit_parent')?.un('postrender', this._renderEditOverlay) // eslint-disable-line
}
_end = () => { // this function cleans up our state and map. If this does not execute correctly we could get stuck in a corrupted map state.
try {
const { map } = this.props
const { interactions } = this.state
this._removePostComposeListener()
interactions.forEach(i => map.removeInteraction(i))
this.setState({ editingFeature: null, style: null, interactions: [] })
} catch (err) {
console.warn(`Geokit encountered a problem while editing a feature: ${err.message}. \n`, err) // eslint-disable-line no-console
}
}
showMeasurements = () => {
this.setState({ showMeasurements: !this.state.showMeasurements })
}
cancelEdit = () => {
const { onEditCancel, editFeature, addEditFeatureToContext } = this.props
const { style } = this.state
this.setState(
{ canceled: true, editingFeature: null },
() => onEditCancel(editFeature, addEditFeatureToContext, style)
)
}
finishEdit = () => {
const { onEditFinish, addEditFeatureToContext, editFeature } = this.props
const { editingFeature, style } = this.state
this.setState(
{ editingFeature: null },
() => onEditFinish(editFeature, editingFeature, addEditFeatureToContext, style)
)
}
init () {
const { editOpts, map, onEditBegin, editFeature } = this.props
const clonedFeature = editFeature.clone() // create a collection of clones of the features in props, this avoids modifying the existing features
const opts = Object.assign({}, editOpts, {
pixelTolerance: 10,
features: new olCollection([clonedFeature]),
deleteCondition: ({ originalEvent, type }) => {
const { altKey, ctrlKey, shiftKey, metaKey } = originalEvent
const modifierKeyActive = altKey || ctrlKey || shiftKey || metaKey
const altClick = type === 'click' && modifierKeyActive
const rightClick = (type === 'pointerdown' || type === 'click') && originalEvent.button === 2
return rightClick || altClick
}
})
const translateInteraction = new Translate({ features: new olCollection([clonedFeature]) }) // ol/interaction/translate only checks for features on the map and since we are not adding these to the map (see additional comments) we use our own that knows to look for the features we pass to it whether or not they're on the map.
const modifyInteraction = new olInteractionModify(opts) // ol/interaction/modify doesn't care about the features being on the map or not so it's good to go
const style = clonedFeature.getStyle()
this.setState({
anchor: olKitTurf(centroid, [clonedFeature.getGeometry()]).getGeometry().getCoordinates(),
interactions: [modifyInteraction, translateInteraction],
editingFeature: clonedFeature,
style
}, () => {
this._addPostComposeListener()
})
map.addInteraction(translateInteraction)
map.addInteraction(modifyInteraction)
onEditBegin(clonedFeature) // callback function for IAs. FeatureEditor doesn't do anything to the original features so we tell the IA which features they passed in as props and what features we are editing. This should help if they want to add custom logic around these features.
}
componentDidMount () {
if (!this.props.editFeature) return
this.init()
}
componentDidUpdate (prevProps) {
const { editFeature } = this.props
const { interactions } = this.state
if (prevProps.editFeature && !editFeature) return this._end()
if (interactions.length === 2 || !editFeature) return
this.init()
}
componentWillUnmount = () => {
const { editingFeature } = this.state
if (editingFeature) console.warn(`Geokit FeatureEditor has been unmounted unexpectedly. This may lead undesirable behaviour in your application.`) // eslint-disable-line no-console
return this._end()
}
rotate = (val) => {
const { editingFeature, rotation, anchor } = this.state
const geometry = editingFeature.getGeometry()
const rotationDiff = val - rotation
this.setState({ rotation: val }, () => geometry.rotate(-rotationDiff * (Math.PI / 180), anchor))
}
render () {
const { translations } = this.props
const { editingFeature } = this.state
const knobStyle = {
width: '35px',
height: '35px',
padding: '2px'
}
if (!editingFeature) return null
return (
<Toolbar>
<Grid item>
<ButtonCardActions>
<LeftCard>
<Button color='secondary' onClick={this.cancelEdit}>
{translations['_ol_kit.edit.cancel']}
</Button>
</LeftCard>
<CenterCard style={{ paddingLeft: '20px', marginLeft: '0px' }}>
<FormControlLabel
style={{ marginBottom: '0px' }}
control={
<Knob style={knobStyle} unlockDistance={0} defaultValue={0} max={360} onChange={this.rotate} />
}
label={translations['_ol_kit.edit.rotate']}
/>
</CenterCard>
<RightCard>
<Button color='primary' onClick={this.finishEdit}>
{translations['_ol_kit.edit.finish']}
</Button>
</RightCard>
</ButtonCardActions>
</Grid>
</Toolbar>
)
}
}
FeatureEditor.propTypes = {
editOpts: PropTypes.exact({
condition: PropTypes.string,
deleteCondition: PropTypes.string,
insertVertexCondition: PropTypes.string,
pixelTolerance: PropTypes.number,
style: PropTypes.object,
source: PropTypes.object,
wrapX: PropTypes.bool
}).isRequired,
editFeature: PropTypes.object,
addEditFeatureToContext: PropTypes.func,
map: PropTypes.object,
onEditBegin: PropTypes.func,
onEditFinish: PropTypes.func,
onEditCancel: PropTypes.func,
editStyle: PropTypes.oneOfType([
PropTypes.func,
PropTypes.object,
PropTypes.array
]),
areaUOM: PropTypes.string,
distanceUOM: PropTypes.string,
translations: PropTypes.object
}
FeatureEditor.defaultProps = {
editOpts: {},
onEditFinish: (feature, updatedFeature, addEditFeatureToContext, style) => {
const geom = updatedFeature.getGeometry()
if (!feature) return
feature.setGeometry(geom)
feature.setStyle(style || null) // restore the original feature's style
addEditFeatureToContext(null)
},
onEditBegin: (feature) => {
feature.setStyle(new olStyleStyle({}))
},
onEditCancel: (feature, addEditFeatureToContext, style) => {
feature.setStyle(style || null) // restore the original feature's style
addEditFeatureToContext(null)
},
editStyle: (feature, map, showMeasurements = false, { areaUOM, distanceUOM }, translations) => { // eslint-disable-line
return immediateEditStyle(
{ areaUOM, distanceUOM, showMeasurements, map, translations, language: navigator.language },
feature,
map.getView().getResolution()
)
}
}
export default connectToContext(FeatureEditor)