import * as React from 'react' ; import _isEmpty from 'lodash/isEmpty' ; import OlMap from 'ol/Map' ; import OlLayerVector from 'ol/layer/Vector' ; import OlSourceVector from 'ol/source/Vector' ; import OlCollection from 'ol/Collection' ; import OlMultiPolygon from 'ol/geom/MultiPolygon' ; import OlMultiLineString from 'ol/geom/MultiLineString' ; import OlStyleStyle from 'ol/style/Style' ; import OlStyleStroke from 'ol/style/Stroke' ; import OlStyleFill from 'ol/style/Fill' ; import OlStyleCircle from 'ol/style/Circle' ; import OlInteractionDraw , { DrawEvent } from 'ol/interaction/Draw' ; import { unByKey } from 'ol/Observable' ; import OlOverlay from 'ol/Overlay' ; import OlMapBrowserEvent from 'ol/MapBrowserEvent' ; import OlGeometryType from 'ol/geom/GeometryType' ; import OlOverlayPositioning from 'ol/OverlayPositioning' ; import MeasureUtil from '@terrestris/ol-util/dist/MeasureUtil/MeasureUtil' ; import MapUtil from '@terrestris/ol-util/dist/MapUtil/MapUtil' ; import ToggleButton , { ToggleButtonProps } from '../ToggleButton/ToggleButton' ; import { CSS_PREFIX } from '../../constants' ; import './MeasureButton.less' ; interface DefaultProps { /**
     * Name of system vector layer which will be used to draw measurement
     * results.
     */ measureLayerName : string ; /**
     * Fill color of the measurement feature.
     */ fillColor : string ; /**
     * Stroke color of the measurement feature.
     */ strokeColor : string ; /**
     * Determines if a marker with current measurement should be shown every
     * time the user clicks while measuring a distance. Default is false.
     */ showMeasureInfoOnClickedPoints : boolean ; /**
     * Determines if a tooltip with helpful information is shown next to the mouse
     * position. Default is true.
     */ showHelpTooltip : boolean ; /**
     * How many decimal places will be allowed for the measure tooltips.
     * Default is 2.
     */ decimalPlacesInTooltips : number ; /**
     * Used to allow / disallow multiple drawings at a time on the map.
     * Default is false.
     * TODO known issue: only label of the last drawn feature will be shown!
     */ multipleDrawing : boolean ; /**
     * Tooltip which will be shown on map mouserover after measurement button
     * was activated.
     */ clickToDrawText : string ; /**
     * Tooltip which will be shown after polygon measurement button was toggled
     * and at least one click in the map is occured.
     */ continuePolygonMsg : string ; /**
     * Tooltip which will be shown after line measurement button was toggled
     * and at least one click in the map is occured.
     */ continueLineMsg : string ; /**
     * Tooltip which will be shown after angle measurement button was toggled
     * and at least one click in the map is occured.
     */ continueAngleMsg : string ; /**
     * CSS classes we'll assign to the popups and tooltips from measuring.
     * Overwrite this object to style the text of the popups / overlays, if you
     * don't want to use default classes.
     */ measureTooltipCssClasses : { tooltip : string ; tooltipDynamic : string ; tooltipStatic : string ; } ; /**
     * Whether the measure button is pressed.
     */ pressed : boolean ; /**
     * A custom onToogle function that will be called if button is toggled
     */ onToggle : ( pressed : boolean ) => void ; } interface BaseProps { /**
     * The className which should be added.
     */ className ? : string ; /**
     * Instance of OL map this component is bound to.
     */ map : OlMap ; /**
     * Whether line, area or angle will be measured.
     */ measureType : 'line' | 'polygon' | 'angle' ; /**
     * Whether the measure is using geodesic or cartesian mode. Geodesic is used by default.
     */ geodesic : true ; } export type MeasureButtonProps = BaseProps & Partial < DefaultProps > & ToggleButtonProps ; class extends React . Component class MeasureButton extends React . Component < MeasureButtonProps > { /**
     * The default properties.
     */ static defaultProps : DefaultProps = { measureLayerName : 'react-geo_measure' , fillColor : 'rgba(255, 0, 0, 0.5)' , strokeColor : 'rgba(255, 0, 0, 0.8)' , showMeasureInfoOnClickedPoints : false , showHelpTooltip : true , decimalPlacesInTooltips : 2 , multipleDrawing : false , continuePolygonMsg : 'Click to draw area' , continueLineMsg : 'Click to draw line' , continueAngleMsg : 'Click to draw angle' , clickToDrawText : 'Click to measure' , measureTooltipCssClasses : { tooltip : `${ CSS_PREFIX }measure-tooltip` , tooltipDynamic : `${ CSS_PREFIX }measure-tooltip-dynamic` , tooltipStatic : `${ CSS_PREFIX }measure-tooltip-static` } , pressed : false , onToggle : ( ) => undefined } ; private className = `${ CSS_PREFIX }measurebutton` ; private _feature = null ; private _measureTooltip = null ; private _helpTooltip = null ; private _helpTooltipElement = null ; private _measureTooltipElement = null ; private _createdTooltipOverlays = [ ] ; private _createdTooltipDivs = [ ] ; private _eventKeys = { drawstart : null , drawend : null , pointermove : null , click : null , change : null } ; private _measureLayer = null ; private _drawInteraction = null ; constructs constructor ( props ) { super ( props ) ; this . onDrawInteractionActiveChange = this . onDrawInteractionActiveChange . bind ( this ) ; this . onToggle = this . onToggle . bind ( this ) ; this . onDrawStart = this . onDrawStart . bind ( this ) ; this . onDrawEnd = this . onDrawEnd . bind ( this ) ; this . onDrawInteractionGeometryChange = this . onDrawInteractionGeometryChange . bind ( this ) ; this . onMapPointerMove = this . onMapPointerMove . bind ( this ) ; this . onMapClick = this . onMapClick . bind ( this ) ; } method componentDidMount ( ) { this . createMeasureLayer ( ) ; this . createDrawInteraction ( ) ; } /**
     * Ensures that component is properly cleaned up on unmount.
     */ componentWillUnmount ( ) { if ( this . props . pressed ) { this . onToggle ( false ) ; } } method onToggle ( pressed ) { const { map , onToggle } = this . props ; this . cleanup ( ) ; onToggle ( pressed ) ; if ( pressed && this . _drawInteraction ) { this . _drawInteraction . setActive ( pressed ) ; this . _eventKeys . drawstart = this . _drawInteraction . on ( 'drawstart' , e => this . onDrawStart ( e ) ) ; this . _eventKeys . drawend = this . _drawInteraction . on ( 'drawend' , e => this . onDrawEnd ( e ) ) ; this . _eventKeys . pointermove = map . on ( 'pointermove' , e => this . onMapPointerMove ( e ) ) ; } } method createMeasureLayer ( ) { const { measureLayerName , fillColor , strokeColor , map } = this . props ; let measureLayer = MapUtil . getLayerByName ( map , measureLayerName ) ; if ( ! measureLayer ) { measureLayer = new OlLayerVector ( { properties : { name : measureLayerName , } , source : new OlSourceVector ( { features : new OlCollection ( ) } ) , style : new OlStyleStyle ( { fill : new OlStyleFill ( { color : fillColor } ) , stroke : new OlStyleStroke ( { color : strokeColor , width : 2 } ) , image : new OlStyleCircle ( { radius : 7 , fill : new OlStyleFill ( { color : fillColor } ) } ) } ) } ) ; map . addLayer ( measureLayer ) ; } this . _measureLayer = measureLayer ; } return { OlInteractionDraw } method createDrawInteraction ( ) { const { fillColor , strokeColor , measureType , pressed , map } = this . props ; const maxPoints = measureType === 'angle' ? 2 : undefined ; const drawType = measureType === 'polygon' ? OlGeometryType . MULTI_POLYGON : OlGeometryType . MULTI_LINE_STRING ; const drawInteraction = new OlInteractionDraw ( { source : this . _measureLayer . getSource ( ) , type : drawType , maxPoints : maxPoints , style : new OlStyleStyle ( { fill : new OlStyleFill ( { color : fillColor } ) , stroke : new OlStyleStroke ( { color : strokeColor , lineDash : [ 10 , 10 ] , width : 2 } ) , image : new OlStyleCircle ( { radius : 5 , stroke : new OlStyleStroke ( { color : strokeColor } ) , fill : new OlStyleFill ( { color : fillColor } ) } ) } ) , freehandCondition : function ( ) { return false ; } } ) ; map . addInteraction ( drawInteraction ) ; drawInteraction . on ( 'change:active' , ( ) => this . onDrawInteractionActiveChange ( ) ) ; this . _drawInteraction = drawInteraction ; if ( pressed ) { this . onDrawInteractionActiveChange ( ) ; } drawInteraction . setActive ( pressed ) ; } /**
     * Adjusts visibility of measurement related tooltips depending on active
     * status of draw interaction.
     */ onDrawInteractionActiveChange ( ) { const { showHelpTooltip } = this . props ; if ( this . _drawInteraction . getActive ( ) ) { if ( showHelpTooltip ) { this . createHelpTooltip ( ) ; } this . createMeasureTooltip ( ) ; } else { if ( showHelpTooltip ) { this . removeHelpTooltip ( ) ; } this . removeMeasureTooltip ( ) ; } } param evt onDrawInteractionGeometryChange ( ) { this . updateMeasureTooltip ( ) ; } param evt onMapClick ( evt ) { const { measureType , showMeasureInfoOnClickedPoints } = this . props ; if ( showMeasureInfoOnClickedPoints && measureType === 'line' ) { this . addMeasureStopTooltip ( evt . coordinate ) ; } } param evt method onDrawStart ( evt ) { const { showHelpTooltip , multipleDrawing , map } = this . props ; const source = this . _measureLayer . getSource ( ) ; this . _feature = evt . feature ; this . _eventKeys . change = this . _feature . getGeometry ( ) . on ( 'change' , this . onDrawInteractionGeometryChange ) ; this . _eventKeys . click = map . on ( 'click' , ( e ) => this . onMapClick ( e ) ) ; if ( ! multipleDrawing && source . getFeatures ( ) . length > 0 ) { this . cleanupTooltips ( ) ; this . createMeasureTooltip ( ) ; if ( showHelpTooltip ) { this . createHelpTooltip ( ) ; } source . clear ( ) ; } } method onDrawEnd ( evt ) { const { measureType , multipleDrawing , showMeasureInfoOnClickedPoints , measureTooltipCssClasses } = this . props ; if ( this . _eventKeys . click ) { unByKey ( this . _eventKeys . click ) ; } if ( this . _eventKeys . change ) { unByKey ( this . _eventKeys . change ) ; } if ( multipleDrawing ) { this . addMeasureStopTooltip ( ( evt . feature . getGeometry ( ) as OlMultiPolygon | OlMultiLineString ) . getLastCoordinate ( ) ) ; } if ( ( multipleDrawing || showMeasureInfoOnClickedPoints ) && measureType === 'line' ) { this . removeMeasureTooltip ( ) ; } else { this . _measureTooltipElement . className = `${ measureTooltipCssClasses . tooltip } ${ measureTooltipCssClasses . tooltipStatic }` ; this . _measureTooltip . setOffset ( [ 0 , - 7 ] ) ; } this . updateMeasureTooltip ( ) ; this . _feature = null ; if ( ( multipleDrawing || showMeasureInfoOnClickedPoints ) && measureType === 'line' ) { this . _measureTooltipElement = null ; this . createMeasureTooltip ( ) ; } } param coordinate addMeasureStopTooltip ( coordinate ) { const { measureType , decimalPlacesInTooltips , map , measureTooltipCssClasses , geodesic } = this . props ; if ( ! _isEmpty ( this . _feature ) ) { let geom = this . _feature . getGeometry ( ) ; if ( geom instanceof OlMultiPolygon ) { geom = geom . getPolygons ( ) [ 0 ] ; } if ( geom instanceof OlMultiLineString ) { geom = geom . getLineStrings ( ) [ 0 ] ; } const value = measureType === 'line' ? MeasureUtil . formatLength ( geom , map , decimalPlacesInTooltips , geodesic ) : MeasureUtil . formatArea ( geom , map , decimalPlacesInTooltips , geodesic ) ; if ( parseInt ( value , 10 ) > 0 ) { const div = document . createElement ( 'div' ) ; div . className = `${ measureTooltipCssClasses . tooltip } ${ measureTooltipCssClasses . tooltipStatic }` ; div . innerHTML = value ; const tooltip = new OlOverlay ( { element : div , offset : [ 0 , - 15 ] , positioning : OlOverlayPositioning . BOTTOM_CENTER } ) ; map . addOverlay ( tooltip ) ; tooltip . setPosition ( coordinate ) ; this . _createdTooltipDivs . push ( div ) ; this . _createdTooltipOverlays . push ( tooltip ) ; } } } /**
     * Creates a new measure tooltip as `OlOverlay`.
     */ createMeasureTooltip ( ) { const { map , measureTooltipCssClasses } = this . props ; if ( this . _measureTooltipElement ) { return ; } this . _measureTooltipElement = document . createElement ( 'div' ) ; this . _measureTooltipElement . className = `${ measureTooltipCssClasses . tooltip } ${ measureTooltipCssClasses . tooltipDynamic }` ; this . _measureTooltip = new OlOverlay ( { element : this . _measureTooltipElement , offset : [ 0 , - 15 ] , positioning : OlOverlayPositioning . BOTTOM_CENTER } ) ; map . addOverlay ( this . _measureTooltip ) ; } /**
     * Creates a new help tooltip as `OlOverlay`.
     */ createHelpTooltip ( ) { const { map , measureTooltipCssClasses } = this . props ; if ( this . _helpTooltipElement ) { return ; } this . _helpTooltipElement = document . createElement ( 'div' ) ; this . _helpTooltipElement . className = measureTooltipCssClasses . tooltip ; this . _helpTooltip = new OlOverlay ( { element : this . _helpTooltipElement , offset : [ 15 , 0 ] , positioning : OlOverlayPositioning . CENTER_LEFT } ) ; map . addOverlay ( this . _helpTooltip ) ; } /**
     * Removes help tooltip from the map if measure button was untoggled.
     */ removeHelpTooltip ( ) { if ( this . _helpTooltip ) { this . props . map . removeOverlay ( this . _helpTooltip ) ; } this . _helpTooltipElement = null ; this . _helpTooltip = null ; } method removeMeasureTooltip ( ) { if ( this . _measureTooltip ) { this . props . map . removeOverlay ( this . _measureTooltip ) ; } this . _measureTooltipElement = null ; this . _measureTooltip = null ; } method cleanupTooltips ( ) { const { map } = this . props ; this . _createdTooltipOverlays . forEach ( ( tooltipOverlay ) => { map . removeOverlay ( tooltipOverlay ) ; } ) ; this . _createdTooltipOverlays = [ ] ; this . _createdTooltipDivs . forEach ( ( tooltipDiv ) => { const parent = tooltipDiv && tooltipDiv . parentNode ; if ( parent ) { parent . removeChild ( tooltipDiv ) ; } } ) ; this . _createdTooltipDivs = [ ] ; this . removeMeasureTooltip ( ) ; } method cleanup ( ) { if ( this . _drawInteraction ) { this . _drawInteraction . setActive ( false ) ; } Object . keys ( this . _eventKeys ) . forEach ( key => { if ( this . _eventKeys [ key ] ) { unByKey ( this . _eventKeys [ key ] ) ; } } ) ; this . cleanupTooltips ( ) ; if ( this . _measureLayer ) { this . _measureLayer . getSource ( ) . clear ( ) ; } } param evt onMapPointerMove ( evt ) { if ( ! evt . dragging ) { this . updateHelpTooltip ( evt . coordinate ) ; } } param coordinate updateHelpTooltip ( coordinate ) { const { measureType , clickToDrawText , continuePolygonMsg , continueLineMsg , continueAngleMsg } = this . props ; if ( ! this . _helpTooltipElement ) { return ; } let msg = clickToDrawText ; if ( this . _helpTooltipElement ) { if ( measureType === 'polygon' ) { msg = continuePolygonMsg ; } else if ( measureType === 'line' ) { msg = continueLineMsg ; } else if ( measureType === 'angle' ) { msg = continueAngleMsg ; } this . _helpTooltipElement . innerHTML = msg ; this . _helpTooltip . setPosition ( coordinate ) ; } } /**
     * Updates the text and position of the measture tooltip.
     */ updateMeasureTooltip ( ) { const { measureType , decimalPlacesInTooltips , map , geodesic } = this . props ; if ( ! this . _measureTooltipElement ) { return ; } if ( this . _feature ) { let output ; let geom = this . _feature . getGeometry ( ) ; if ( geom instanceof OlMultiPolygon ) { geom = geom . getPolygons ( ) [ 0 ] ; } if ( geom instanceof OlMultiLineString ) { geom = geom . getLineStrings ( ) [ 0 ] ; } let measureTooltipCoord = geom . getLastCoordinate ( ) ; if ( measureType === 'polygon' ) { output = MeasureUtil . formatArea ( geom , map , decimalPlacesInTooltips , geodesic ) ; measureTooltipCoord = geom . getInteriorPoint ( ) . getCoordinates ( ) ; } else if ( measureType === 'line' ) { output = MeasureUtil . formatLength ( geom , map , decimalPlacesInTooltips , geodesic ) ; } else if ( measureType === 'angle' ) { output = MeasureUtil . formatAngle ( geom , map , decimalPlacesInTooltips ) ; } this . _measureTooltipElement . innerHTML = output ; this . _measureTooltip . setPosition ( measureTooltipCoord ) ; } } /**
     * The render function.
     */ render ( ) { const { className , map , measureType , measureLayerName , fillColor , strokeColor , showMeasureInfoOnClickedPoints , showHelpTooltip , multipleDrawing , clickToDrawText , continuePolygonMsg , continueLineMsg , continueAngleMsg , decimalPlacesInTooltips , measureTooltipCssClasses , onToggle , ... passThroughProps } = this . props ; const finalClassName = className ? `${ className } ${ this . className }` : this . className ; return ( < ToggleButton onToggle = { this . onToggle } className = { finalClassName } { ... passThroughProps } />); } } export default MeasureButton ;