2022-07-08 03:14:55 +02:00
import { Store , UIEventSource } from "../../Logic/UIEventSource"
2022-04-30 00:41:33 +02:00
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
2022-07-08 03:14:55 +02:00
private readonly _leafletMap : Store < L.Map >
2022-04-30 00:41:33 +02:00
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
2022-09-08 21:40:48 +02:00
)
2022-04-30 00:41:33 +02:00
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-09-08 21:40:48 +02:00
)
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
2022-09-08 21:40:48 +02:00
)
2022-04-30 00:41:33 +02:00
} )
. 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
2022-09-08 21:40:48 +02:00
)
2022-04-30 00:41:33 +02:00
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
2022-09-08 21:40:48 +02:00
)
2022-04-30 00:41:33 +02:00
leafletLayer . bindPopup ( popup )
2022-09-08 21:40:48 +02:00
2022-04-30 00:41:33 +02:00
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 ,
} )
}
}