import React from 'react'
import PropTypes from 'prop-types'
import nanoid from 'nanoid'
import { VectorLayer, Preferences } from '../classes'
import olSourceVector from 'ol/source/Vector'
import { createBox } from 'ol/interaction/Draw'
import CircularProgress from '@material-ui/core/CircularProgress'
import Draw from './Draw'
import { Measure } from 'Measure'
import { SnapPreference, CoordinateLabelPreference } from 'Preferences'
import { connectToContext } from 'Provider'
import { styleMeasure } from './utils'
import { Container, ProgressWrapper } from './styled'
/**
* A prebuilt Draw Tools component
* @component
* @category Draw
* @since 0.18.0
*/
class DrawContainer extends React.Component {
constructor () {
super()
this.state = {}
this.onDrawStart = this.onDrawStart.bind(this)
this.onDrawCancel = this.onDrawCancel.bind(this)
this.onDrawEnd = this.onDrawEnd.bind(this)
this.renderMeasure = this.renderMeasure.bind(this)
this.handleToggle = this.handleToggle.bind(this)
}
safeGetPreference = (key) => this.props.preferences?.payload?.get?.(key)
getUoms = () => {
const uom = this.safeGetPreference('_UOM') || 'imperial'
const distanceUOM = this.safeGetPreference('_DISTANCE_LABEL_UOM')
const areaUOM = this.safeGetPreference('_AREA_LABEL_UOM')
return {
uomType: uom,
distanceUOM: distanceUOM || (uom === 'imperial' ? 'feet' : 'meters'),
areaUOM: areaUOM || (uom === 'imperial' ? 'acres' : 'hectares')
}
}
updateMeasureStyle = (newValues = this.getUoms()) => {
const { map } = this.props
const { feature } = this.state
const { distanceUOM, areaUOM } = newValues
const isLegacyMeasure = feature?.get('_vmf_type') === '_vmf_measurement'
const needsAreaLabels = feature?.get('_ol_kit_area_labels')
const needsDistanceLabels = feature?.get('_ol_kit_distance_labels')
const isMeasure = isLegacyMeasure || needsAreaLabels || needsDistanceLabels
this.setState(newValues)
if (isMeasure) {
const styleFunc = styleMeasure.bind(this,
map,
feature,
map.getView().getResolution(),
{ distanceUOM, areaUOM, map })
feature.setStyle(styleFunc)
}
}
getLayer () {
const { feature } = this.state
const title = feature.get('_vmf_type') === '_vmf_measurement' ? 'Measurements Layer' : 'Annotations Layer'
const layers = this.props.map.getLayers().getArray()
const exists = layers.find(l => l.get('_vmf_title') === title)
if (exists) {
return exists
} else {
const layer = new VectorLayer({
className: `_ol_kit_${title}`,
_vmf_id: nanoid(),
_vmf_title: title,
title: 'Annotations',
source: new olSourceVector()
})
this.props.map.addLayer(layer)
return layer
}
}
onDrawEnd (feature) {
console.debug('%cDrawEnd', 'color: cyan; font-style: italic;', feature) // eslint-disable-line
const layer = this.getLayer()
layer.getSource().addFeature(feature)
feature.set('_ol_kit_parent', layer)
feature.set('_ol_kit_annotation', true)
feature.set('_vmf_id', nanoid())
this.setState({ feature: null })
}
onDrawStart (feature, { target }) {
const { map } = this.props
const { distanceUOM, areaUOM } = this.getUoms()
const pointLabels = this.safeGetPreference('_POINT_LABELS_ENABLED')
const distanceLabelsEnabled = this.safeGetPreference('_DISTANCE_LABEL_ENABLED')
const areaLabelsEnabled = this.safeGetPreference('_AREA_LABEL_ENABLED')
const opts = { distanceUOM, areaUOM, map }
const isBoxDraw = target.geometryFunction_?.toString() === createBox().toString()
const drawMode = isBoxDraw ? 'Box' : target.mode_
const isFreehand = target.freehand_
if (distanceLabelsEnabled) feature.set('_ol_kit_distance_labels', true)
if (areaLabelsEnabled) feature.set('_ol_kit_area_labels', true)
if (pointLabels && !isFreehand) feature.set('_ol_kit_coordinate_labels', true)
if (drawMode === 'Circle') {
feature.set('_ol_kit_draw_mode', 'circle')
if (pointLabels) {
feature.set('_ol_kit_needs_centroid_label', true)
feature.set('_ol_kit_coordinate_labels', false) // To prevent all needVertexLabels for running in styles.js
}
}
const styleFunc = distanceLabelsEnabled || areaLabelsEnabled || pointLabels
? styleMeasure.bind(this, map, feature, map.getView().getResolution(), opts)
: undefined
feature.setStyle(styleFunc)
this.setState({ feature, drawMode })
console.debug('%cDrawStart', 'color: magenta; font-style: italic;') // eslint-disable-line
}
onInteractionAdded (interaction) {
console.debug('%cInteractionAdded', 'color: yellow; font-style: italic;', interaction) // eslint-disable-line
}
onDrawCancel (interaction) {
console.debug('%cDrawCancel', 'color: white; background-color: red; border-radius: 4px; font-style: italic;', interaction) // eslint-disable-line
this.setState({ feature: null })
}
handleUomChange = () => {
this.updateMeasureStyle()
}
selectedFeature = (feature) => {
this.setState({ feature })
this.props.selectedFeature(feature)
}
handleToggle = () => {
this.forceUpdate()
}
renderPreferences = () => {
const { translations, preferences } = this.props
const { status, payload } = preferences
switch (status) {
case 'loading':
return (
<ProgressWrapper key={nanoid()}>
<CircularProgress color={'primary'} />
</ProgressWrapper>
)
case 'success':
return (
<React.Fragment key={'Snap'}>
<SnapPreference
translations={translations}
preferences={payload}
onChange={this.handleToggle}
compact={false} />
</React.Fragment>
)
default:
return null
}
}
renderMeasure = () => {
const { uom, translations, showCoordinateLabels, preferences, showMeasurements } = this.props
if (!showMeasurements) return null
const { feature, geometryType, drawMode } = this.state
const { status, payload } = preferences
switch (status) {
case 'loading':
return (
<ProgressWrapper key={nanoid()}>
<CircularProgress color={'primary'} />
</ProgressWrapper>
)
case 'success':
return (
<React.Fragment key={'Measure'}>
{
showCoordinateLabels
? <CoordinateLabelPreference
compact={true}
translations={translations}
preferences={payload}
onChange={this.handleToggle} />
: null
}
<Measure
{...this.props}
feature={feature}
uom={uom || payload?.get?.('_UOM')}
onUomChange={this.handleUomChange}
geometryType={geometryType}
preferences={payload}
drawMode={drawMode} />
</React.Fragment>
)
default:
return null
}
}
render () {
const { preferences, children, style } = this.props
const drawChildren = children || [
this.renderMeasure(),
<Draw
{...this.props}
key={'Draw'}
preferences={preferences.payload}
onDrawFinish={this.onDrawEnd}
onDrawBegin={this.onDrawStart}
onInteractionAdded={this.onInteractionAdded}
onDrawCancel={this.onDrawCancel}
selectInteraction={this.props.selectInteraction} />,
this.renderPreferences()
]
return (
<Container style={style}>
{drawChildren}
</Container>
)
}
}
DrawContainer.propTypes = {
/** openlayers map */
map: PropTypes.object.isRequired,
/** reference to openlayers select interaction which can optionally be managed by IA */
selectInteraction: PropTypes.object,
/** an object with `get` and `put` methods to handle measure state */
preferences: PropTypes.object.isRequired,
/** translations object */
translations: PropTypes.object,
/** boolean for enabling snap interaction */
snap: PropTypes.bool,
/** openlayers snap options */
snapOpts: PropTypes.object,
/** boolean enabling the measurement component */
showMeasurements: PropTypes.bool,
/** boolean enabling the coordinate labels component */
showCoordinateLabels: PropTypes.bool,
/** velocity preference of either imperial or metric */
uom: PropTypes.string,
/** openlayers draw interaction constructor props */
drawOpts: PropTypes.object,
/** callback that returns the selected openlayers feature from the map */
selectedFeature: PropTypes.func,
/** pass custom style object to DrawContainer */
style: PropTypes.object,
/** pass child comps to opt out of the default controls */
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
])
}
DrawContainer.defaultProps = {
drawOpts: {},
snap: true,
showMeasurements: true,
showCoordinateLabels: true,
preferences: { status: 'success', payload: new Preferences() },
selectedFeature: () => {}
}
export default connectToContext(DrawContainer)