2022-04-30 00:41:33 +02:00
import { UIEventSource } from "../../Logic/UIEventSource" ;
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" ;
import { ShowDataLayerOptions } from "./ShowDataLayerOptions" ;
import { ElementStorage } from "../../Logic/ElementStorage" ;
import RenderingMultiPlexerFeatureSource from "../../Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource" ;
import ScrollableFullScreen from "../Base/ScrollableFullScreen" ;
/ *
// import 'leaflet-polylineoffset';
We don 't actually import it here. It is imported in the ' MinimapImplementation '-class, which' ll result in a patched 'L' object .
Even though actually importing this here would seem cleaner , we don ' t do this as this breaks some scripts :
- Scripts are ran in ts - node
- ts - node doesn 't define the ' window ' - object
- Importing this will execute some code which needs the window object
* /
/ * *
* The data layer shows all the given geojson elements with the appropriate icon etc
* /
export default class ShowDataLayerImplementation {
private static dataLayerIds = 0
private readonly _leafletMap : UIEventSource < L.Map > ;
private readonly _enablePopups : boolean ;
private readonly _features : RenderingMultiPlexerFeatureSource
private readonly _layerToShow : LayerConfig ;
private readonly _selectedElement : UIEventSource < any >
private readonly allElements : ElementStorage
// Used to generate a fresh ID when needed
private _cleanCount = 0 ;
private geoLayer = undefined ;
/ * *
* A collection of functions to call when the current geolayer is unregistered
* /
private unregister : ( ( ) = > void ) [ ] = [ ] ;
private isDirty = false ;
/ * *
* If the selected element triggers , this is used to lookup the correct layer and to open the popup
* Used to avoid a lot of callbacks on the selected element
*
* Note : the key of this dictionary is 'feature.properties.id+features.geometry.type' as one feature might have multiple presentations
* @private
* /
private readonly leafletLayersPerId = new Map < string , { feature : any , leafletlayer : any } > ( )
private readonly showDataLayerid : number ;
private readonly createPopup : ( tags : UIEventSource < any > , layer : LayerConfig ) = > ScrollableFullScreen
/ * *
* Creates a datalayer .
*
* If 'createPopup' is set , this function is called every time that 'popupOpen' is called
* @param options
* /
constructor ( options : ShowDataLayerOptions & { layerToShow : LayerConfig } ) {
this . _leafletMap = options . leafletMap ;
this . showDataLayerid = ShowDataLayerImplementation . dataLayerIds ;
ShowDataLayerImplementation . dataLayerIds ++
if ( options . features === undefined ) {
console . error ( "Invalid ShowDataLayer invocation: options.features is undefed" )
throw "Invalid ShowDataLayer invocation: options.features is undefed"
}
this . _features = new RenderingMultiPlexerFeatureSource ( options . features , options . layerToShow ) ;
this . _layerToShow = options . layerToShow ;
this . _selectedElement = options . selectedElement
this . allElements = options . state ? . allElements ;
this . createPopup = undefined ;
this . _enablePopups = options . popup !== undefined ;
if ( options . popup !== undefined ) {
this . createPopup = options . popup
}
const self = this ;
options . leafletMap . addCallback ( _ = > {
return self . update ( options )
}
) ;
this . _features . features . addCallback ( _ = > self . update ( options ) ) ;
options . doShowLayer ? . addCallback ( doShow = > {
const mp = options . leafletMap . data ;
if ( mp === null ) {
self . Destroy ( )
return true ;
}
if ( mp == undefined ) {
return ;
}
if ( doShow ) {
if ( self . isDirty ) {
return self . update ( options )
} else {
mp . addLayer ( this . geoLayer )
}
} else {
if ( this . geoLayer !== undefined ) {
mp . removeLayer ( this . geoLayer )
this . unregister . forEach ( f = > f ( ) )
this . unregister = [ ]
}
}
} )
this . _selectedElement ? . addCallbackAndRunD ( selected = > {
self . openPopupOfSelectedElement ( selected )
} )
this . update ( options )
}
private Destroy() {
this . unregister . forEach ( f = > f ( ) )
}
private openPopupOfSelectedElement ( selected ) {
if ( selected === undefined ) {
return
}
if ( this . _leafletMap . data === undefined ) {
return ;
}
const v = this . leafletLayersPerId . get ( selected . properties . id + selected . geometry . type )
if ( v === undefined ) {
return ;
}
const leafletLayer = v . leafletlayer
const feature = v . feature
if ( leafletLayer . getPopup ( ) . isOpen ( ) ) {
return ;
}
if ( selected . properties . id !== feature . properties . id ) {
return ;
}
if ( feature . id !== feature . properties . id ) {
// Probably a feature which has renamed
// the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too
console . log ( "Not opening the popup for" , feature , "as probably renamed" )
return ;
}
if ( selected . geometry . type === feature . geometry . type // If a feature is rendered both as way and as point, opening one popup might trigger the other to open, which might trigger the one to open again
) {
leafletLayer . openPopup ( )
}
}
private update ( options : ShowDataLayerOptions ) : boolean {
if ( this . _features . features . data === undefined ) {
return ;
}
this . isDirty = true ;
if ( options ? . doShowLayer ? . data === false ) {
return ;
}
const mp = options . leafletMap . data ;
if ( mp === null ) {
return true ; // Unregister as the map has been destroyed
}
if ( mp === undefined ) {
return ;
}
this . _cleanCount ++
// clean all the old stuff away, if any
if ( this . geoLayer !== undefined ) {
mp . removeLayer ( this . geoLayer ) ;
}
const self = this ;
const data = {
type : "FeatureCollection" ,
features : [ ]
}
// @ts-ignore
this . geoLayer = L . geoJSON ( data , {
style : feature = > self . createStyleFor ( feature ) ,
pointToLayer : ( feature , latLng ) = > self . pointToLayer ( feature , latLng ) ,
onEachFeature : ( feature , leafletLayer ) = > self . postProcessFeature ( feature , leafletLayer )
} ) ;
const selfLayer = this . geoLayer ;
const allFeats = this . _features . features . data ;
for ( const feat of allFeats ) {
if ( feat === undefined ) {
continue
}
try {
if ( feat . geometry . type === "LineString" ) {
const coords = L . GeoJSON . coordsToLatLngs ( feat . geometry . coordinates )
const tagsSource = this . allElements ? . addOrGetElement ( feat ) ? ? new UIEventSource < any > ( feat . properties ) ;
let offsettedLine ;
tagsSource
2022-06-05 02:24:14 +02:00
. map ( tags = > this . _layerToShow . lineRendering [ feat . lineRenderingIndex ] . GenerateLeafletStyle ( tags ) )
2022-04-30 00:41:33 +02:00
. withEqualityStabilized ( ( a , b ) = > {
if ( a === b ) {
return true
}
if ( a === undefined || b === undefined ) {
return false
}
return a . offset === b . offset && a . color === b . color && a . weight === b . weight && a . dashArray === b . dashArray
} )
. addCallbackAndRunD ( lineStyle = > {
if ( offsettedLine !== undefined ) {
self . geoLayer . removeLayer ( offsettedLine )
}
// @ts-ignore
offsettedLine = L . polyline ( coords , lineStyle ) ;
this . postProcessFeature ( feat , offsettedLine )
offsettedLine . addTo ( this . geoLayer )
// If 'self.geoLayer' is not the same as the layer the feature is added to, we can safely remove this callback
return self . geoLayer !== selfLayer
} )
} else {
this . geoLayer . addData ( feat ) ;
}
} catch ( e ) {
console . error ( "Could not add " , feat , "to the geojson layer in leaflet due to" , e , e . stack )
}
}
if ( options . zoomToFeatures ? ? false ) {
if ( this . geoLayer . getLayers ( ) . length > 0 ) {
try {
const bounds = this . geoLayer . getBounds ( )
mp . fitBounds ( bounds , { animate : false } )
} catch ( e ) {
console . debug ( "Invalid bounds" , e )
}
}
}
if ( options . doShowLayer ? . data ? ? true ) {
mp . addLayer ( this . geoLayer )
}
this . isDirty = false ;
this . openPopupOfSelectedElement ( this . _selectedElement ? . data )
}
private createStyleFor ( feature ) {
const tagsSource = this . allElements ? . addOrGetElement ( feature ) ? ? new UIEventSource < any > ( feature . properties ) ;
// Every object is tied to exactly one layer
const layer = this . _layerToShow
const pointRenderingIndex = feature . pointRenderingIndex
const lineRenderingIndex = feature . lineRenderingIndex
if ( pointRenderingIndex !== undefined ) {
const style = layer . mapRendering [ pointRenderingIndex ] . GenerateLeafletStyle ( tagsSource , this . _enablePopups )
return {
icon : style
}
}
if ( lineRenderingIndex !== undefined ) {
return layer . lineRendering [ lineRenderingIndex ] . GenerateLeafletStyle ( tagsSource . data )
}
throw "Neither lineRendering nor mapRendering defined for " + feature
}
private pointToLayer ( feature , latLng ) : L . Layer {
// Leaflet cannot handle geojson points natively
// We have to convert them to the appropriate icon
// Click handling is done in the next step
const layer : LayerConfig = this . _layerToShow
if ( layer === undefined ) {
return ;
}
let tagSource = this . allElements ? . getEventSourceById ( feature . properties . id ) ? ? new UIEventSource < any > ( feature . properties )
const clickable = ! ( layer . title === undefined && ( layer . tagRenderings ? ? [ ] ) . length === 0 ) && this . _enablePopups
let style : any = layer . mapRendering [ feature . pointRenderingIndex ] . GenerateLeafletStyle ( tagSource , clickable ) ;
const baseElement = style . html ;
if ( ! this . _enablePopups ) {
baseElement . SetStyle ( "cursor: initial !important" )
}
style . html = style . html . ConstructElement ( )
return L . marker ( latLng , {
icon : L.divIcon ( style )
} ) ;
}
/ * *
* Post processing - basically adding the popup
* @param feature
* @param leafletLayer
* @private
* /
private postProcessFeature ( feature , leafletLayer : L.Layer ) {
const layer : LayerConfig = this . _layerToShow
if ( layer . title === undefined || ! this . _enablePopups ) {
// No popup action defined -> Don't do anything
// or probably a map in the popup - no popups needed!
return ;
}
const popup = L . popup ( {
autoPan : true ,
closeOnEscapeKey : true ,
closeButton : false ,
autoPanPaddingTopLeft : [ 15 , 15 ] ,
} , leafletLayer ) ;
leafletLayer . bindPopup ( popup ) ;
let infobox : ScrollableFullScreen = undefined ;
const id = ` popup- ${ feature . properties . id } - ${ feature . geometry . type } - ${ this . showDataLayerid } - ${ this . _cleanCount } - ${ feature . pointRenderingIndex ? ? feature . lineRenderingIndex } - ${ feature . multiLineStringIndex ? ? "" } `
popup . setContent ( ` <div style='height: 65vh' id=' ${ id } '>Popup for ${ feature . properties . id } ${ feature . geometry . type } ${ id } is loading</div> ` )
const createpopup = this . createPopup ;
leafletLayer . on ( "popupopen" , ( ) = > {
if ( infobox === undefined ) {
const tags = this . allElements ? . getEventSourceById ( feature . properties . id ) ? ? new UIEventSource < any > ( feature . properties ) ;
infobox = createpopup ( tags , layer ) ;
infobox . isShown . addCallback ( isShown = > {
if ( ! isShown ) {
leafletLayer . closePopup ( )
}
} ) ;
}
infobox . AttachTo ( id )
infobox . Activate ( ) ;
this . unregister . push ( ( ) = > {
console . log ( "Destroying infobox" )
infobox . Destroy ( ) ;
} )
if ( this . _selectedElement ? . data ? . properties ? . id !== feature . properties . id ) {
this . _selectedElement ? . setData ( feature )
}
} ) ;
// Add the feature to the index to open the popup when needed
this . leafletLayersPerId . set ( feature . properties . id + feature . geometry . type , {
feature : feature ,
leafletlayer : leafletLayer
} )
}
}