import * as React from 'react' ; import { Tree } from 'antd' ; import { AntTreeNodeDropEvent } from 'antd/lib/tree/Tree' ; import { ReactElement } from 'react' ; import { TreeProps , AntTreeNodeCheckedEvent } from 'antd/lib/tree' ; import { EventDataNode } from 'rc-tree/lib/interface' ; import OlMap from 'ol/Map' ; import OlLayerBase from 'ol/layer/Base' ; import OlLayerGroup from 'ol/layer/Group' ; import OlCollection from 'ol/Collection' ; import OlMapEvent from 'ol/MapEvent' ; import { unByKey } from 'ol/Observable' ; import { getUid } from 'ol' ; import _isBoolean from 'lodash/isBoolean' ; import _isFunction from 'lodash/isFunction' ; import _isEqual from 'lodash/isEqual' ; import Logger from '@terrestris/base-util/dist/Logger' ; import MapUtil from '@terrestris/ol-util/dist/MapUtil/MapUtil' ; import LayerTreeNode , { LayerTreeNodeProps } from './LayerTreeNode/LayerTreeNode' ; import { CSS_PREFIX } from '../constants' ; interface DefaultProps extends TreeProps { /**
     * An optional array-filter function that is applied to every layer and
     * subLayer. Return false to exclude this layer from the layerTree or true
     * to include it.
     *
     * Compare MDN Docs for Array.prototype.filter: https://mdn.io/array/filter
     */ filterFunction : ( value : any , index : number , array : any [ ] ) => boolean ; } export interface BaseProps { /**
     * An optional CSS class which should be added.
     */ className ? : string ; /**
     * A LayerGroup the Tree should handle.
     */ layerGroup ? : OlLayerGroup ; /**
     * The OpenLayers map the tree interacts with.
     */ map : OlMap ; /**
     * A function that can be used to pass a custom node title. It can return
     * any renderable element (String, Number, Element etc.) and receives
     * the layer instance of the current tree node.
     */ nodeTitleRenderer ? : ( layer : OlLayerBase ) => React . ReactNode ; } interface LayerTreeState { layerGroup : OlLayerGroup ; layerGroupRevision ? : number ; treeNodes : ReactElement < LayerTreeNodeProps > [ ] ; checkedKeys : React . ReactText [ ] ; mapResolution : number ; } export type LayerTreeProps = BaseProps & Partial < DefaultProps > & TreeProps ; class extends React . Component class LayerTree extends React . Component < LayerTreeProps , LayerTreeState > { /**
     * The default properties.
     */ static defaultProps : DefaultProps = { draggable : true , checkable : true , filterFunction : ( ) => true } ; private className = `${ CSS_PREFIX }layertree` ; private olListenerKeys = [ ] ; constructs constructor ( props ) { super ( props ) ; this . state = { layerGroup : null , layerGroupRevision : null , treeNodes : [ ] , checkedKeys : [ ] , mapResolution : - 1 } ; } param nextProps param prevState static getDerivedStateFromProps ( nextProps , prevState ) { if ( prevState . layerGroup && nextProps . layerGroup ) { if ( ! _isEqual ( getUid ( prevState . layerGroup ) , getUid ( nextProps . layerGroup ) ) || ! _isEqual ( prevState . layerGroupRevision , nextProps . layerGroup . getRevision ( ) ) ) { return { layerGroup : nextProps . layerGroup , layerGroupRevision : nextProps . layerGroup . getRevision ( ) } ; } } return null ; } /**
     * Determines what to do on the initial mount.
     */ componentDidMount ( ) { const layerGroup = this . props . layerGroup ? this . props . layerGroup : this . props . map . getLayerGroup ( ) ; const revision = this . props . layerGroup ? this . props . layerGroup . getRevision ( ) : 0 ; this . setState ( { layerGroup : layerGroup , layerGroupRevision : revision } , ( ) => { this . registerAddRemoveListeners ( this . state . layerGroup ) ; this . registerResolutionChangeHandler ( ) ; this . rebuildTreeNodes ( ) ; } ) ; } param prevProps param prevState componentDidUpdate ( prevProps , prevState ) { const { layerGroup , nodeTitleRenderer } = this . props ; if ( layerGroup && prevState . layerGroup ) { if ( ! _isEqual ( getUid ( prevState . layerGroup ) , getUid ( layerGroup ) ) ) { unByKey ( this . olListenerKeys ) ; this . olListenerKeys = [ ] ; this . registerAddRemoveListeners ( layerGroup ) ; this . rebuildTreeNodes ( ) ; } } if ( nodeTitleRenderer !== prevProps . nodeTitleRenderer ) { this . rebuildTreeNodes ( ) ; } } /**
     * Determines what to do when the component is unmounted.
     */ componentWillUnmount ( ) { unByKey ( this . olListenerKeys ) ; } param groupLayer treeNodesFromLayerGroup ( groupLayer ) { const layerArray = groupLayer . getLayers ( ) . getArray ( ) . filter ( this . props . filterFunction ) ; const treeNodes = layerArray . map ( ( layer ) => { return this . treeNodeFromLayer ( layer ) ; } ) ; treeNodes . reverse ( ) ; this . setState ( { treeNodes } ) ; } param groupLayer registerAddRemoveListeners ( groupLayer ) { const collection = groupLayer . getLayers ( ) ; const addEvtKey = collection . on ( 'add' , this . onCollectionAdd ) ; const removeEvtKey = collection . on ( 'remove' , this . onCollectionRemove ) ; const changeEvtKey = groupLayer . on ( 'change:layers' , this . onChangeLayers ) ; this . olListenerKeys . push ( addEvtKey , removeEvtKey , changeEvtKey ) ; collection . forEach ( ( layer ) => { if ( layer instanceof OlLayerGroup ) { this . registerAddRemoveListeners ( layer ) ; } } ) ; } /**
     * Registers an eventhandler on the `ol.View`, which will rebuild the tree
     * nodes whenever the view's resolution changes.
     */ registerResolutionChangeHandler ( ) { const { map } = this . props ; const evtKey = map . on ( 'moveend' , this . rebuildTreeNodes . bind ( this ) ) ; this . olListenerKeys . push ( evtKey ) ; } param evt onCollectionAdd = ( evt ) => { if ( evt . element instanceof OlLayerGroup ) { this . registerAddRemoveListeners ( evt . element ) ; } this . rebuildTreeNodes ( ) ; } ; param evt onCollectionRemove = ( evt ) => { this . unregisterEventsByLayer ( evt . element ) ; if ( evt . element instanceof OlLayerGroup ) { evt . element . getLayers ( ) . forEach ( ( layer ) => { this . unregisterEventsByLayer ( layer ) ; } ) ; } this . rebuildTreeNodes ( ) ; } ; param evt onChangeLayers = ( evt ) => { this . unregisterEventsByLayer ( evt . oldValue ) ; if ( evt . oldValue instanceof OlCollection ) { evt . oldValue . forEach ( ( layer ) => this . unregisterEventsByLayer ( layer ) ) ; } if ( evt . target instanceof OlLayerGroup ) { this . registerAddRemoveListeners ( evt . target ) ; } this . rebuildTreeNodes ( ) ; } ; param layer unregisterEventsByLayer = ( layer ) => { this . olListenerKeys = this . olListenerKeys . filter ( ( key ) => { if ( layer instanceof OlLayerGroup ) { const layers = layer . getLayers ( ) ; if ( key . target === layers ) { if ( ( key . type === 'add' && key . listener === this . onCollectionAdd ) || ( key . type === 'remove' && key . listener === this . onCollectionRemove ) || ( key . type === 'change:layers' && key . listener === this . onChangeLayers ) ) { unByKey ( key ) ; return false ; } } } else if ( key . target === layer ) { if ( key . type === 'change:visible' && key . listener === this . onLayerChangeVisible ) { unByKey ( key ) ; return false ; } } return true ; } ) ; } ; param evt rebuildTreeNodes = ( evt ? ) => { const { mapResolution } = this . state ; let newMapResolution = - 1 ; if ( evt ?. target instanceof OlMap ) { newMapResolution = evt . target . getView ( ) . getResolution ( ) ; if ( mapResolution === newMapResolution ) { return ; } } this . treeNodesFromLayerGroup ( this . state . layerGroup ) ; const checkedKeys = this . getVisibleOlUids ( ) ; this . setState ( { checkedKeys , mapResolution : newMapResolution } ) ; } ; param layer return getTreeNodeTitle ( layer ) { if ( _isFunction ( this . props . nodeTitleRenderer ) ) { return this . props . nodeTitleRenderer . call ( this , layer ) ; } else { return layer . get ( 'name' ) ; } } param layer return treeNodeFromLayer ( layer ) { let childNodes ; if ( layer instanceof OlLayerGroup ) { const childLayers = layer . getLayers ( ) . getArray ( ) . filter ( this . props . filterFunction ) ; childNodes = childLayers . map ( ( childLayer ) => { return this . treeNodeFromLayer ( childLayer ) ; } ) ; childNodes . reverse ( ) ; } else { if ( ! this . hasListener ( layer , 'change:visible' , this . onLayerChangeVisible ) ) { const eventKey = layer . on ( 'change:visible' , this . onLayerChangeVisible ) ; this . olListenerKeys . push ( eventKey ) ; } } return ( < LayerTreeNode title = { this . getTreeNodeTitle ( layer ) } key = { getUid ( layer ) } inResolutionRange = { MapUtil . layerInResolutionRange ( layer , this . props . map ) } > { childNodes } < /LayerTreeNode>); } param target param type param listener return hasListener = ( target , type , listener ) => { return this . olListenerKeys . some ( ( listenerKey ) => { return listenerKey . target === target && listenerKey . type === type && listenerKey . listener === listener ; } ) ; } ; param evt onLayerChangeVisible = ( ) => { const checkedKeys = this . getVisibleOlUids ( ) ; this . setState ( { checkedKeys } , ( ) => { this . rebuildTreeNodes ( ) ; } ) ; } ; return getVisibleOlUids = ( ) => { const layers = MapUtil . getAllLayers ( this . state . layerGroup , ( layer ) => { return ! ( layer instanceof OlLayerGroup ) && layer . getVisible ( ) ; } ) . filter ( this . props . filterFunction ) ; return layers . map ( getUid ) ; } ; param checkedKeys param checked onCheck ( checkedKeys , e ) { const { checked } = e ; const eventKey = e . node . props . eventKey ; const layer = MapUtil . getLayerByOlUid ( this . props . map , eventKey ) ; this . setLayerVisibility ( layer , checked ) ; } param layer param visibility setLayerVisibility ( layer , visibility ) { if ( ! ( layer instanceof OlLayerBase ) || ! _isBoolean ( visibility ) ) { Logger . error ( 'setLayerVisibility called without layer or visiblity.' ) ; return ; } if ( layer instanceof OlLayerGroup ) { layer . setVisible ( visibility ) ; layer . getLayers ( ) . forEach ( ( subLayer ) => { this . setLayerVisibility ( subLayer , visibility ) ; } ) ; } else { layer . setVisible ( visibility ) ; if ( visibility ) { const group = this . props . layerGroup ? this . props . layerGroup : this . props . map . getLayerGroup ( ) ; this . setParentFoldersVisible ( group , getUid ( layer ) , group ) ; } } } param currentGroup param olUid param masterGroup setParentFoldersVisible ( currentGroup , olUid , masterGroup ) { const items = currentGroup . getLayers ( ) . getArray ( ) ; const groups = items . filter ( l => l instanceof OlLayerGroup ) as OlLayerGroup [ ] ; const match = items . find ( i => getUid ( i ) === olUid ) ; if ( match ) { currentGroup . setVisible ( true ) ; this . setParentFoldersVisible ( masterGroup , getUid ( currentGroup ) , masterGroup ) ; return ; } groups . forEach ( g => { this . setParentFoldersVisible ( g , olUid , masterGroup ) ; } ) ; } param e onDrop ( e ) { const dragLayer = MapUtil . getLayerByOlUid ( this . props . map , e . dragNode . props . eventKey ) ; const dragInfo = MapUtil . getLayerPositionInfo ( dragLayer , this . props . map ) ; const dragCollection = dragInfo . groupLayer . getLayers ( ) ; const dropLayer = MapUtil . getLayerByOlUid ( this . props . map , e . node . props . eventKey ) ; const dropPos = e . node . props . pos . split ( '-' ) ; const location = e . dropPosition - Number ( dropPos [ dropPos . length - 1 ] ) ; dragCollection . remove ( dragLayer ) ; const dropInfo = MapUtil . getLayerPositionInfo ( dropLayer , this . props . map ) ; const dropPosition = dropInfo . position ; const dropCollection = dropInfo . groupLayer . getLayers ( ) ; if ( location === - 1 ) { if ( dropPosition === dropCollection . getLength ( ) - 1 ) { dropCollection . push ( dragLayer ) ; } else { dropCollection . insertAt ( dropPosition + 1 , dragLayer ) ; } } else if ( location === 0 ) { if ( dropLayer instanceof OlLayerGroup ) { dropLayer . getLayers ( ) . push ( dragLayer ) ; } else { dropCollection . insertAt ( dropPosition + 1 , dragLayer ) ; } } else if ( location === 1 ) { dropCollection . insertAt ( dropPosition , dragLayer ) ; } this . rebuildTreeNodes ( ) ; } /**
     * Call rebuildTreeNodes onExpand to avoid sync issues.
     *
     */ onExpand = ( expandedKeys , info ) => { const { onExpand } = this . props ; this . rebuildTreeNodes ( ) ; if ( onExpand ) { onExpand ( expandedKeys , info ) ; } } ; /**
     * The render function.
     */ render ( ) { const { className , layerGroup , map , nodeTitleRenderer , ... passThroughProps } = this . props ; let ddListeners ; if ( passThroughProps . draggable ) { ddListeners = { onDrop : this . onDrop . bind ( this ) } ; } const finalClassName = className ? `${ className } ${ this . className }` : this . className ; return ( < Tree className = { finalClassName } checkedKeys = { this . state . checkedKeys } onCheck = { this . onCheck . bind ( this ) } onExpand = { this . onExpand } { ... ddListeners } { ... passThroughProps } > { this . state . treeNodes } < /Tree>); } } export default LayerTree ;