import { ID , SearchResult } from "facilmap-types" ; import { DomEvent , Evented , Handler , LatLngBounds , LeafletEvent , Map , Point , Polyline , Util } from "leaflet" ; import { LinesLayer , MarkerLayer , MarkersLayer , OverpassElement , OverpassLayer , SearchResultsLayer } from "facilmap-leaflet" ; import BoxSelection from "./box-selection" ; export type SelectedItem = { type : "marker" | "line" ; id : ID ; } | { type : "searchResult" ; result : SearchResult ; layerId : number ; } | { type : "overpass" ; element : OverpassElement ; } ; function isAllowedSibling ( a , b ) { if ( [ "marker" , "line" ] . includes ( a . type ) && [ "marker" , "line" ] . includes ( b . type ) ) return true ; else if ( a . type == "searchResult" && b . type == "searchResult" ) return a . layerId == b . layerId ; else if ( a . type == "overpass" && b . type == "overpass" ) return true ; else return false ; } function byType < T extends SelectedItem [ "type" ] > ( items , type ) { return items . filter ( ( i ) => i . type === type ) as any ; } function isSame ( a , b ) { if ( ( a . type == "marker" && b . type == "marker" ) || ( a . type == "line" && b . type == "line" ) ) return a . id == b . id ; else if ( a . type == "searchResult" && b . type == "searchResult" ) return a . result === b . result ; else if ( a . type == "overpass" && b . type == "overpass" ) return a . element === b . element ; else return false ; } export default class SelectionHandler extends Handler { _selection : SelectedItem [ ] = [ ] ; _markersLayer : MarkersLayer ; _linesLayer : LinesLayer ; _searchResultLayers : SearchResultsLayer [ ] ; _overpassLayer : OverpassLayer ; _boxSelectionHandler : BoxSelection ; _selectionBeforeBox : SelectedItem [ ] = [ ] ; _isBoxInteraction = false ; _mapInteraction : number = 0 ; _isLongClick : boolean = false ; constructor ( map , markersLayer , linesLayer , searchResultsLayer , overpassLayer ) { super ( map ) ; this . _boxSelectionHandler = new BoxSelection ( map ) . on ( "selectstart" , this . handleBoxSelectStart ) . on ( "select" , this . handleBoxSelect ) . on ( "selectend" , this . handleBoxSelectEnd ) ; this . _markersLayer = markersLayer ; this . _linesLayer = linesLayer ; this . _searchResultLayers = [ searchResultsLayer ] ; this . _overpassLayer = overpassLayer ; } enable ( ) { super . enable ( ) ; this . _boxSelectionHandler . enable ( ) ; return this ; } disable ( ) { this . _boxSelectionHandler . disable ( ) ; super . disable ( ) ; return this ; } addHooks ( ) { this . _markersLayer . on ( "click" , this . handleClickMarker ) ; this . _linesLayer . on ( "click" , this . handleClickLine ) ; for ( const layer of this . _searchResultLayers ) layer . on ( "click" , this . handleClickSearchResult ) ; this . _overpassLayer . on ( "click" , this . handleClickOverpass ) ; this . _map . on ( "click" , this . handleClickMap ) ; this . _map . on ( "fmInteractionStart" , this . handleMapInteractionStart ) ; this . _map . on ( "fmInteractionEnd" , this . handleMapInteractionEnd ) ; this . _map . getContainer ( ) . addEventListener ( "mousedown" , this . handleMapMouseDown ) ; this . _map . getContainer ( ) . addEventListener ( "touchstart" , this . handleMapMouseDown ) ; } removeHooks ( ) { this . _markersLayer . off ( "click" , this . handleClickMarker ) ; this . _linesLayer . off ( "click" , this . handleClickLine ) ; for ( const layer of this . _searchResultLayers ) layer . off ( "click" , this . handleClickSearchResult ) ; this . _overpassLayer . off ( "click" , this . handleClickOverpass ) ; this . _map . off ( "click" , this . handleClickMap ) ; this . _map . off ( "fmInteractionStart" , this . handleMapInteractionStart ) ; this . _map . off ( "fmInteractionEnd" , this . handleMapInteractionEnd ) ; this . _map . getContainer ( ) . removeEventListener ( "mousedown" , this . handleMapMouseDown ) ; this . _map . getContainer ( ) . removeEventListener ( "touchstart" , this . handleMapMouseDown ) ; } addSearchResultLayer ( layer ) { if ( this . _searchResultLayers . includes ( layer ) ) return ; if ( this . _enabled ) layer . on ( "click" , this . handleClickSearchResult ) ; this . _searchResultLayers . push ( layer ) ; } removeSearchResultLayer ( layer ) { const idx = this . _searchResultLayers . indexOf ( layer ) ; if ( idx == - 1 ) return ; layer . off ( "click" , this . handleClickSearchResult ) ; this . _searchResultLayers . splice ( idx , 1 ) ; const layerId = Util . stamp ( layer ) ; const without = this . _selection . filter ( ( item ) => item . type != "searchResult" || item . layerId != layerId ) ; if ( without . length != this . _selection . length ) this . setSelectedItems ( without ) ; } getSelection ( ) { return this . _selection ; } setSelectedItems ( items , open = false ) { this . _selection = items ; this . _markersLayer . setHighlightedMarkers ( new Set ( byType ( items , "marker" ) . map ( ( i ) => i . id ) ) ) ; this . _linesLayer . setHighlightedLines ( new Set ( byType ( items , "line" ) . map ( ( i ) => i . id ) ) ) ; for ( const layer of this . _searchResultLayers ) { const layerId = Util . stamp ( layer ) ; layer . setHighlightedResults ( new Set ( byType ( items , "searchResult" ) . filter ( ( i ) => i . layerId == layerId ) . map ( ( i ) => i . result ) ) ) ; } this . _overpassLayer . setHighlightedElements ( new Set ( byType ( items , "overpass" ) . map ( ( i ) => i . element ) ) ) ; this . fire ( "fmChangeSelection" , { open } ) ; } isSelected ( item ) { return this . _selection . some ( ( i ) => isSame ( i , item ) ) ; } selectItem ( item , open = false ) { if ( ! this . isSelected ( item ) ) this . setSelectedItems ( [ ... this . _selection , item ] , open ) ; else if ( open ) this . fire ( "fmChangeSelection" , { open } ) ; } unselectItem ( item ) { this . setSelectedItems ( this . _selection . filter ( ( i ) => ! isSame ( i , item ) ) ) ; } toggleItem ( item , open = false ) { if ( this . isSelected ( item ) ) this . unselectItem ( item ) ; else if ( ! this . _selection . some ( ( i ) => ! isAllowedSibling ( item , i ) ) ) this . selectItem ( item , open ) ; } handleClickItem ( item , e ) { if ( this . _mapInteraction ) return ; DomEvent . stopPropagation ( e ) ; if ( ( e . originalEvent as any ) . ctrlKey ) this . toggleItem ( item , true ) ; else this . setSelectedItems ( [ item ] , true ) ; } handleClickMarker = ( e ) : void => { if ( e . propagatedFrom ?. marker ?. id ) this . handleClickItem ( { type : "marker" , id : e . propagatedFrom . marker . id } , e ) ; } ; handleClickLine = ( e ) : void => { if ( e . propagatedFrom ?. line ?. id ) this . handleClickItem ( { type : "line" , id : e . propagatedFrom . line . id } , e ) ; } ; handleClickSearchResult = ( e ) : void => { if ( e . propagatedFrom ?. _fmSearchResult ) this . handleClickItem ( { type : "searchResult" , result : e . propagatedFrom . _fmSearchResult , layerId : Util . stamp ( e . target ) } , e ) ; } ; handleClickOverpass = ( e ) : void => { if ( e . propagatedFrom ?. _fmOverpassElement ) this . handleClickItem ( { type : "overpass" , element : e . propagatedFrom . _fmOverpassElement } , e ) ; } ; handleClickMap = ( e ) : void => { if ( this . _mapInteraction || this . _isLongClick || this . _isBoxInteraction ) return ; if ( ! ( e . originalEvent as any ) . ctrlKey ) this . setSelectedItems ( [ ] ) ; } ; handleMapMouseDown = ( e ) : void => { if ( "button" in e && e . button != null && e . button != 0 ) return ; if ( "touches" in e && e . touches && e . touches . length != 1 ) return ; const pos = this . _map . mouseEventToContainerPoint ( ( "touches" in e ? e . touches [ 0 ] : e ) as any ) ; const timeout = setTimeout ( ( ) => { this . _isLongClick = true ; this . fire ( "fmLongClick" , { latlng : this . _map . mouseEventToLatLng ( ( "touches" in e ? e . touches [ 0 ] : e ) as any ) } ) ; } , 500 ) ; const handleMouseMove = ( e ) => { if ( pos . distanceTo ( this . _map . mouseEventToContainerPoint ( ( "touches" in e ? e . touches [ 0 ] : e ) as any ) ) > ( this . _map . dragging as any ) . _draggable . options . clickTolerance ) clear ( ) ; } ; const handleContextMenu = ( e ) => { DomEvent . preventDefault ( e ) ; } ; const clear = ( ) => { clearTimeout ( timeout ) ; this . _map . getContainer ( ) . removeEventListener ( "mousemove" , handleMouseMove ) ; this . _map . getContainer ( ) . removeEventListener ( "touchmove" , handleMouseMove ) ; this . _map . getContainer ( ) . removeEventListener ( "mouseup" , clear ) ; this . _map . getContainer ( ) . removeEventListener ( "touchend" , clear ) ; this . _map . getContainer ( ) . removeEventListener ( "contextmenu" , handleContextMenu ) ; setTimeout ( ( ) => { this . _isLongClick = false ; } , 0 ) ; } ; this . _map . getContainer ( ) . addEventListener ( "mousemove" , handleMouseMove ) ; this . _map . getContainer ( ) . addEventListener ( "touchmove" , handleMouseMove ) ; this . _map . getContainer ( ) . addEventListener ( "mouseup" , clear ) ; this . _map . getContainer ( ) . addEventListener ( "touchend" , clear ) ; this . _map . getContainer ( ) . addEventListener ( "contextmenu" , handleContextMenu ) ; } ; handleMapInteractionStart = ( ) : void => { this . _mapInteraction ++ ; } ; handleMapInteractionEnd = ( ) : void => { this . _mapInteraction -- ; } ; handleBoxSelectStart = ( e ) : void => { this . _isBoxInteraction = true ; this . _selectionBeforeBox = e . ctrlKey ? [ ... this . _selection ] : [ ] ; } ; handleBoxSelect = ( e ) : void => { const bounds = e . bounds ; const selection = [ ... this . _markersLayer . getLayers ( ) . filter ( ( layer ) => layer instanceof MarkerLayer && bounds . contains ( layer . getLatLng ( ) ) ) . map ( ( layer ) : SelectedItem => ( { type : "marker" , id : ( layer as any ) . marker . id } ) ) , ... this . _linesLayer . getLayers ( ) . filter ( ( layer ) => layer instanceof Polyline && bounds . contains ( layer . getBounds ( ) ) ) . map ( ( layer ) : SelectedItem => ( { type : "line" , id : ( layer as any ) . line . id } ) ) ] . filter ( ( item1 ) => ! this . _selectionBeforeBox . some ( ( item2 ) => isSame ( item1 , item2 ) ) ) ; if ( selection . length == 0 ) this . setSelectedItems ( this . _selectionBeforeBox , true ) ; else { this . setSelectedItems ( [ ... this . _selectionBeforeBox . filter ( ( item ) => isAllowedSibling ( selection [ 0 ] , item ) ) , ... selection ] , true ) ; } } ; handleBoxSelectEnd = ( ) : void => { this . _selectionBeforeBox = [ ] ; setTimeout ( ( ) => { this . _isBoxInteraction = false ; } , 0 ) ; } ; } export default interface SelectionHandler extends Evented { } Object . assign ( SelectionHandler . prototype , Evented . prototype ) ;