forked from MapComplete/MapComplete
		
	refactoring(maplibre): add pointRendering
This commit is contained in:
		
							parent
							
								
									4f2bbf4b54
								
							
						
					
					
						commit
						1b3609b13f
					
				
					 10 changed files with 316 additions and 122 deletions
				
			
		|  | @ -186,6 +186,14 @@ export class BBox { | ||||||
|         ] |         ] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     toLngLat(): [[number, number], [number, number]] { | ||||||
|  |         return [ | ||||||
|  |             [this.minLon, this.minLat], | ||||||
|  |             [this.maxLon, this.maxLat], | ||||||
|  |         ] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     public asGeoJson<T>(properties: T): Feature<Polygon, T> { |     public asGeoJson<T>(properties: T): Feature<Polygon, T> { | ||||||
|         return { |         return { | ||||||
|             type: "Feature", |             type: "Feature", | ||||||
|  |  | ||||||
|  | @ -1,8 +1,6 @@ | ||||||
| import UserRelatedState from "./UserRelatedState" | import UserRelatedState from "./UserRelatedState" | ||||||
| import { Store, Stores, UIEventSource } from "../UIEventSource" | import { Store, Stores, UIEventSource } from "../UIEventSource" | ||||||
| import BaseLayer from "../../Models/BaseLayer" |  | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import AvailableBaseLayers from "../Actors/AvailableBaseLayers" |  | ||||||
| import Attribution from "../../UI/BigComponents/Attribution" | import Attribution from "../../UI/BigComponents/Attribution" | ||||||
| import Minimap, { MinimapObj } from "../../UI/Base/Minimap" | import Minimap, { MinimapObj } from "../../UI/Base/Minimap" | ||||||
| import { Tiles } from "../../Models/TileRange" | import { Tiles } from "../../Models/TileRange" | ||||||
|  | @ -43,10 +41,6 @@ export default class MapState extends UserRelatedState { | ||||||
|      The leaflet instance of the big basemap |      The leaflet instance of the big basemap | ||||||
|      */ |      */ | ||||||
|     public leafletMap = new UIEventSource<any /*L.Map*/>(undefined, "leafletmap") |     public leafletMap = new UIEventSource<any /*L.Map*/>(undefined, "leafletmap") | ||||||
|     /** |  | ||||||
|      * A list of currently available background layers |  | ||||||
|      */ |  | ||||||
|     public availableBackgroundLayers: Store<BaseLayer[]> |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * The current background layer |      * The current background layer | ||||||
|  |  | ||||||
|  | @ -2,6 +2,8 @@ import { Feature, Polygon } from "geojson" | ||||||
| import * as editorlayerindex from "../assets/editor-layer-index.json" | import * as editorlayerindex from "../assets/editor-layer-index.json" | ||||||
| import * as globallayers from "../assets/global-raster-layers.json" | import * as globallayers from "../assets/global-raster-layers.json" | ||||||
| import { BBox } from "../Logic/BBox" | import { BBox } from "../Logic/BBox" | ||||||
|  | import { Store, Stores } from "../Logic/UIEventSource" | ||||||
|  | import { GeoOperations } from "../Logic/GeoOperations" | ||||||
| 
 | 
 | ||||||
| export class AvailableRasterLayers { | export class AvailableRasterLayers { | ||||||
|     public static EditorLayerIndex: (Feature<Polygon, EditorLayerIndexProperties> & |     public static EditorLayerIndex: (Feature<Polygon, EditorLayerIndexProperties> & | ||||||
|  | @ -33,6 +35,35 @@ export class AvailableRasterLayers { | ||||||
|         properties: AvailableRasterLayers.osmCartoProperties, |         properties: AvailableRasterLayers.osmCartoProperties, | ||||||
|         geometry: BBox.global.asGeometry(), |         geometry: BBox.global.asGeometry(), | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public static layersAvailableAt( | ||||||
|  |         location: Store<{ lon: number; lat: number }> | ||||||
|  |     ): Store<RasterLayerPolygon[]> { | ||||||
|  |         const availableLayersBboxes = Stores.ListStabilized( | ||||||
|  |             location.mapD((loc) => { | ||||||
|  |                 const lonlat: [number, number] = [loc.lon, loc.lat] | ||||||
|  |                 return AvailableRasterLayers.EditorLayerIndex.filter((eliPolygon) => | ||||||
|  |                     BBox.get(eliPolygon).contains(lonlat) | ||||||
|  |                 ) | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
|  |         const available = Stores.ListStabilized( | ||||||
|  |             availableLayersBboxes.map((eliPolygons) => { | ||||||
|  |                 const loc = location.data | ||||||
|  |                 const lonlat: [number, number] = [loc.lon, loc.lat] | ||||||
|  |                 const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => { | ||||||
|  |                     if (eliPolygon.geometry === null) { | ||||||
|  |                         return true // global ELI-layer
 | ||||||
|  |                     } | ||||||
|  |                     return GeoOperations.inside(lonlat, eliPolygon) | ||||||
|  |                 }) | ||||||
|  |                 matching.unshift(AvailableRasterLayers.osmCarto) | ||||||
|  |                 matching.push(...AvailableRasterLayers.globalLayers) | ||||||
|  |                 return matching | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
|  |         return available | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class RasterLayerUtils { | export class RasterLayerUtils { | ||||||
|  |  | ||||||
|  | @ -5,12 +5,13 @@ import { TagUtils } from "../../Logic/Tags/TagUtils" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import Svg from "../../Svg" | import Svg from "../../Svg" | ||||||
| import WithContextLoader from "./WithContextLoader" | import WithContextLoader from "./WithContextLoader" | ||||||
| import { UIEventSource } from "../../Logic/UIEventSource" | import { Store } from "../../Logic/UIEventSource" | ||||||
| import BaseUIElement from "../../UI/BaseUIElement" | import BaseUIElement from "../../UI/BaseUIElement" | ||||||
| import { FixedUiElement } from "../../UI/Base/FixedUiElement" | import { FixedUiElement } from "../../UI/Base/FixedUiElement" | ||||||
| import Img from "../../UI/Base/Img" | import Img from "../../UI/Base/Img" | ||||||
| import Combine from "../../UI/Base/Combine" | import Combine from "../../UI/Base/Combine" | ||||||
| import { VariableUiElement } from "../../UI/Base/VariableUIElement" | import { VariableUiElement } from "../../UI/Base/VariableUIElement" | ||||||
|  | import { OsmTags } from "../OsmFeature" | ||||||
| 
 | 
 | ||||||
| export default class PointRenderingConfig extends WithContextLoader { | export default class PointRenderingConfig extends WithContextLoader { | ||||||
|     private static readonly allowed_location_codes = new Set<string>([ |     private static readonly allowed_location_codes = new Set<string>([ | ||||||
|  | @ -164,7 +165,7 @@ export default class PointRenderingConfig extends WithContextLoader { | ||||||
|         return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin) |         return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public GetSimpleIcon(tags: UIEventSource<any>): BaseUIElement { |     public GetSimpleIcon(tags: Store<OsmTags>): BaseUIElement { | ||||||
|         const self = this |         const self = this | ||||||
|         if (this.icon === undefined) { |         if (this.icon === undefined) { | ||||||
|             return undefined |             return undefined | ||||||
|  | @ -175,7 +176,7 @@ export default class PointRenderingConfig extends WithContextLoader { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public GenerateLeafletStyle( |     public GenerateLeafletStyle( | ||||||
|         tags: UIEventSource<any>, |         tags: Store<OsmTags>, | ||||||
|         clickable: boolean, |         clickable: boolean, | ||||||
|         options?: { |         options?: { | ||||||
|             noSize?: false | boolean |             noSize?: false | boolean | ||||||
|  | @ -183,11 +184,7 @@ export default class PointRenderingConfig extends WithContextLoader { | ||||||
|         } |         } | ||||||
|     ): { |     ): { | ||||||
|         html: BaseUIElement |         html: BaseUIElement | ||||||
|         iconSize: [number, number] |  | ||||||
|         iconAnchor: [number, number] |         iconAnchor: [number, number] | ||||||
|         popupAnchor: [number, number] |  | ||||||
|         iconUrl: string |  | ||||||
|         className: string |  | ||||||
|     } { |     } { | ||||||
|         function num(str, deflt = 40) { |         function num(str, deflt = 40) { | ||||||
|             const n = Number(str) |             const n = Number(str) | ||||||
|  | @ -211,20 +208,21 @@ export default class PointRenderingConfig extends WithContextLoader { | ||||||
|         let iconH = num(iconSize[1]) |         let iconH = num(iconSize[1]) | ||||||
|         const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center" |         const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center" | ||||||
| 
 | 
 | ||||||
|         let anchorW = iconW / 2 |         // in MapLibre, the offset is relative to the _center_ of the object, with left = [-x, 0] and up = [0,-y]
 | ||||||
|  |         let anchorW = 0 | ||||||
|         let anchorH = iconH / 2 |         let anchorH = iconH / 2 | ||||||
|         if (mode === "left") { |         if (mode === "left") { | ||||||
|             anchorW = 0 |             anchorW = -iconW / 2 | ||||||
|         } |         } | ||||||
|         if (mode === "right") { |         if (mode === "right") { | ||||||
|             anchorW = iconW |             anchorW = iconW / 2 | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (mode === "top") { |         if (mode === "top") { | ||||||
|             anchorH = 0 |             anchorH = -iconH / 2 | ||||||
|         } |         } | ||||||
|         if (mode === "bottom") { |         if (mode === "bottom") { | ||||||
|             anchorH = iconH |             anchorH = iconH / 2 | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const icon = this.GetSimpleIcon(tags) |         const icon = this.GetSimpleIcon(tags) | ||||||
|  | @ -264,15 +262,11 @@ export default class PointRenderingConfig extends WithContextLoader { | ||||||
|         } |         } | ||||||
|         return { |         return { | ||||||
|             html: htmlEl, |             html: htmlEl, | ||||||
|             iconSize: [iconW, iconH], |  | ||||||
|             iconAnchor: [anchorW, anchorH], |             iconAnchor: [anchorW, anchorH], | ||||||
|             popupAnchor: [0, 3 - anchorH], |  | ||||||
|             iconUrl: undefined, |  | ||||||
|             className: clickable ? "leaflet-div-icon" : "leaflet-div-icon unclickable", |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private GetBadges(tags: UIEventSource<any>): BaseUIElement { |     private GetBadges(tags: Store<OsmTags>): BaseUIElement { | ||||||
|         if (this.iconBadges.length === 0) { |         if (this.iconBadges.length === 0) { | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|  | @ -304,7 +298,7 @@ export default class PointRenderingConfig extends WithContextLoader { | ||||||
|         ).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0") |         ).SetClass("absolute bottom-0 right-1/3 h-1/2 w-0") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private GetLabel(tags: UIEventSource<any>): BaseUIElement { |     private GetLabel(tags: Store<OsmTags>): BaseUIElement { | ||||||
|         if (this.label === undefined) { |         if (this.label === undefined) { | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -3,8 +3,6 @@ import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import Loc from "../../Models/Loc" | import Loc from "../../Models/Loc" | ||||||
| import Svg from "../../Svg" | import Svg from "../../Svg" | ||||||
| import Toggle from "../Input/Toggle" | import Toggle from "../Input/Toggle" | ||||||
| import BaseLayer from "../../Models/BaseLayer" |  | ||||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" |  | ||||||
| import BaseUIElement from "../BaseUIElement" | import BaseUIElement from "../BaseUIElement" | ||||||
| import { GeoOperations } from "../../Logic/GeoOperations" | import { GeoOperations } from "../../Logic/GeoOperations" | ||||||
| import Hotkeys from "../Base/Hotkeys" | import Hotkeys from "../Base/Hotkeys" | ||||||
|  |  | ||||||
|  | @ -18,14 +18,12 @@ import { Unit } from "../../Models/Unit" | ||||||
| import { FixedInputElement } from "./FixedInputElement" | import { FixedInputElement } from "./FixedInputElement" | ||||||
| import WikidataSearchBox from "../Wikipedia/WikidataSearchBox" | import WikidataSearchBox from "../Wikipedia/WikidataSearchBox" | ||||||
| import Wikidata from "../../Logic/Web/Wikidata" | import Wikidata from "../../Logic/Web/Wikidata" | ||||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" |  | ||||||
| import Table from "../Base/Table" | import Table from "../Base/Table" | ||||||
| import Combine from "../Base/Combine" | import Combine from "../Base/Combine" | ||||||
| import Title from "../Base/Title" | import Title from "../Base/Title" | ||||||
| import InputElementMap from "./InputElementMap" | import InputElementMap from "./InputElementMap" | ||||||
| import Translations from "../i18n/Translations" | import Translations from "../i18n/Translations" | ||||||
| import { Translation } from "../i18n/Translation" | import { Translation } from "../i18n/Translation" | ||||||
| import BaseLayer from "../../Models/BaseLayer" |  | ||||||
| import Locale from "../i18n/Locale" | import Locale from "../i18n/Locale" | ||||||
| 
 | 
 | ||||||
| export class TextFieldDef { | export class TextFieldDef { | ||||||
|  |  | ||||||
|  | @ -1,65 +1,81 @@ | ||||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import type { Map as MLMap } from "maplibre-gl" | import type { Map as MLMap } from "maplibre-gl" | ||||||
| import { | import { RasterLayerPolygon, RasterLayerProperties } from "../../Models/RasterLayers" | ||||||
|     EditorLayerIndexProperties, |  | ||||||
|     RasterLayerPolygon, |  | ||||||
|     RasterLayerProperties, |  | ||||||
| } from "../../Models/RasterLayers" |  | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import Loc from "../../Models/Loc" | import { BBox } from "../../Logic/BBox" | ||||||
| 
 | 
 | ||||||
| export class MapLibreAdaptor { | export interface MapState { | ||||||
|  |     readonly location: UIEventSource<{ lon: number; lat: number }> | ||||||
|  |     readonly zoom: UIEventSource<number> | ||||||
|  |     readonly bounds: Store<BBox> | ||||||
|  |     readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined> | ||||||
|  | } | ||||||
|  | export class MapLibreAdaptor implements MapState { | ||||||
|     private readonly _maplibreMap: Store<MLMap> |     private readonly _maplibreMap: Store<MLMap> | ||||||
|     private readonly _backgroundLayer?: Store<RasterLayerPolygon> |  | ||||||
| 
 | 
 | ||||||
|     private _currentRasterLayer: string = undefined |     readonly location: UIEventSource<{ lon: number; lat: number }> | ||||||
|  |     readonly zoom: UIEventSource<number> | ||||||
|  |     readonly bounds: Store<BBox> | ||||||
|  |     readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined> | ||||||
|  |     private readonly _bounds: UIEventSource<BBox> | ||||||
| 
 | 
 | ||||||
|     constructor( |  | ||||||
|         maplibreMap: Store<MLMap>, |  | ||||||
|         state?: { |  | ||||||
|             // availableBackgroundLayers: Store<BaseLayer[]>
 |  | ||||||
|     /** |     /** | ||||||
|              * The current background layer |      * Used for internal bookkeeping (to remove a rasterLayer when done loading) | ||||||
|  |      * @private | ||||||
|      */ |      */ | ||||||
|             readonly backgroundLayer?: Store<RasterLayerPolygon> |     private _currentRasterLayer: string | ||||||
|             readonly locationControl?: UIEventSource<Loc> |     constructor(maplibreMap: Store<MLMap>, state?: Partial<Omit<MapState, "bounds">>) { | ||||||
|         } |  | ||||||
|     ) { |  | ||||||
|         this._maplibreMap = maplibreMap |         this._maplibreMap = maplibreMap | ||||||
|         this._backgroundLayer = state.backgroundLayer | 
 | ||||||
|  |         this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 }) | ||||||
|  |         this.zoom = state?.zoom ?? new UIEventSource(1) | ||||||
|  |         this._bounds = new UIEventSource(BBox.global) | ||||||
|  |         this.bounds = this._bounds | ||||||
|  |         this.rasterLayer = | ||||||
|  |             state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined) | ||||||
| 
 | 
 | ||||||
|         const self = this |         const self = this | ||||||
|         this._backgroundLayer?.addCallback((_) => self.setBackground()) |  | ||||||
| 
 |  | ||||||
|         maplibreMap.addCallbackAndRunD((map) => { |         maplibreMap.addCallbackAndRunD((map) => { | ||||||
|             map.on("load", () => { |             map.on("load", () => { | ||||||
|                 self.setBackground() |                 self.setBackground() | ||||||
|             }) |             }) | ||||||
|             if (state.locationControl) { |             self.MoveMapToCurrentLoc(this.location.data) | ||||||
|                 self.MoveMapToCurrentLoc(state.locationControl.data) |             self.SetZoom(this.zoom.data) | ||||||
|             map.on("moveend", () => { |             map.on("moveend", () => { | ||||||
|                     const dt = state.locationControl.data |                 const dt = this.location.data | ||||||
|                 dt.lon = map.getCenter().lng |                 dt.lon = map.getCenter().lng | ||||||
|                 dt.lat = map.getCenter().lat |                 dt.lat = map.getCenter().lat | ||||||
|                     dt.zoom = map.getZoom() |                 this.location.ping() | ||||||
|                     state.locationControl.ping() |                 this.zoom.setData(map.getZoom()) | ||||||
|             }) |             }) | ||||||
|             } |  | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         state.locationControl.addCallbackAndRunD((loc) => { |         this.rasterLayer.addCallback((_) => | ||||||
|  |             self.setBackground().catch((e) => { | ||||||
|  |                 console.error("Could not set background") | ||||||
|  |             }) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         this.location.addCallbackAndRunD((loc) => { | ||||||
|             self.MoveMapToCurrentLoc(loc) |             self.MoveMapToCurrentLoc(loc) | ||||||
|         }) |         }) | ||||||
|  |         this.zoom.addCallbackAndRunD((z) => self.SetZoom(z)) | ||||||
|     } |     } | ||||||
| 
 |     private SetZoom(z: number) { | ||||||
|     private MoveMapToCurrentLoc(loc: Loc) { |         const map = this._maplibreMap.data | ||||||
|  |         if (map === undefined || z === undefined) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         if (map.getZoom() !== z) { | ||||||
|  |             map.setZoom(z) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     private MoveMapToCurrentLoc(loc: { lat: number; lon: number }) { | ||||||
|         const map = this._maplibreMap.data |         const map = this._maplibreMap.data | ||||||
|         if (map === undefined || loc === undefined) { |         if (map === undefined || loc === undefined) { | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         if (map.getZoom() !== loc.zoom) { | 
 | ||||||
|             map.setZoom(loc.zoom) |  | ||||||
|         } |  | ||||||
|         const center = map.getCenter() |         const center = map.getCenter() | ||||||
|         if (center.lng !== loc.lon || center.lat !== loc.lat) { |         if (center.lng !== loc.lon || center.lat !== loc.lat) { | ||||||
|             map.setCenter({ lng: loc.lon, lat: loc.lat }) |             map.setCenter({ lng: loc.lon, lat: loc.lat }) | ||||||
|  | @ -120,14 +136,14 @@ export class MapLibreAdaptor { | ||||||
|         if (map === undefined) { |         if (map === undefined) { | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         const background: RasterLayerProperties = this._backgroundLayer?.data?.properties |         const background: RasterLayerProperties = this.rasterLayer?.data?.properties | ||||||
|         if (background !== undefined && this._currentRasterLayer === background.id) { |         if (background !== undefined && this._currentRasterLayer === background.id) { | ||||||
|             // already the correct background layer, nothing to do
 |             // already the correct background layer, nothing to do
 | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|         await this.awaitStyleIsLoaded() |         await this.awaitStyleIsLoaded() | ||||||
| 
 | 
 | ||||||
|         if (background !== this._backgroundLayer?.data?.properties) { |         if (background !== this.rasterLayer?.data?.properties) { | ||||||
|             // User selected another background in the meantime... abort
 |             // User selected another background in the meantime... abort
 | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|  |  | ||||||
							
								
								
									
										108
									
								
								UI/Map/ShowDataLayer.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								UI/Map/ShowDataLayer.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,108 @@ | ||||||
|  | import { ImmutableStore, Store } from "../../Logic/UIEventSource" | ||||||
|  | import type { Map as MlMap } from "maplibre-gl" | ||||||
|  | import { Marker } from "maplibre-gl" | ||||||
|  | import { ShowDataLayerOptions } from "../ShowDataLayer/ShowDataLayerOptions" | ||||||
|  | import { GeoOperations } from "../../Logic/GeoOperations" | ||||||
|  | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
|  | import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig" | ||||||
|  | import { OsmFeature, OsmTags } from "../../Models/OsmFeature" | ||||||
|  | import FeatureSource from "../../Logic/FeatureSource/FeatureSource" | ||||||
|  | import { BBox } from "../../Logic/BBox" | ||||||
|  | 
 | ||||||
|  | class PointRenderingLayer { | ||||||
|  |     private readonly _config: PointRenderingConfig | ||||||
|  |     private readonly _fetchStore?: (id: string) => Store<OsmTags> | ||||||
|  |     private readonly _map: MlMap | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         map: MlMap, | ||||||
|  |         features: FeatureSource, | ||||||
|  |         config: PointRenderingConfig, | ||||||
|  |         fetchStore?: (id: string) => Store<OsmTags> | ||||||
|  |     ) { | ||||||
|  |         this._config = config | ||||||
|  |         this._map = map | ||||||
|  |         this._fetchStore = fetchStore | ||||||
|  |         const cache: Map<string, Marker> = new Map<string, Marker>() | ||||||
|  |         const self = this | ||||||
|  |         features.features.addCallbackAndRunD((features) => { | ||||||
|  |             const unseenKeys = new Set(cache.keys()) | ||||||
|  |             for (const { feature } of features) { | ||||||
|  |                 const id = feature.properties.id | ||||||
|  |                 unseenKeys.delete(id) | ||||||
|  |                 const loc = GeoOperations.centerpointCoordinates(feature) | ||||||
|  |                 if (cache.has(id)) { | ||||||
|  |                     console.log("Not creating a marker for ", id) | ||||||
|  |                     const cached = cache.get(id) | ||||||
|  |                     const oldLoc = cached.getLngLat() | ||||||
|  |                     console.log("OldLoc vs newLoc", oldLoc, loc) | ||||||
|  |                     if (loc[0] !== oldLoc.lng && loc[1] !== oldLoc.lat) { | ||||||
|  |                         cached.setLngLat(loc) | ||||||
|  |                         console.log("MOVED") | ||||||
|  |                     } | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 console.log("Creating a marker for ", id) | ||||||
|  |                 const marker = self.addPoint(feature) | ||||||
|  |                 cache.set(id, marker) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             for (const unseenKey of unseenKeys) { | ||||||
|  |                 cache.get(unseenKey).remove() | ||||||
|  |                 cache.delete(unseenKey) | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private addPoint(feature: OsmFeature): Marker { | ||||||
|  |         let store: Store<OsmTags> | ||||||
|  |         if (this._fetchStore) { | ||||||
|  |             store = this._fetchStore(feature.properties.id) | ||||||
|  |         } else { | ||||||
|  |             store = new ImmutableStore(feature.properties) | ||||||
|  |         } | ||||||
|  |         const { html, iconAnchor } = this._config.GenerateLeafletStyle(store, true) | ||||||
|  |         html.SetClass("marker") | ||||||
|  |         const el = html.ConstructElement() | ||||||
|  | 
 | ||||||
|  |         el.addEventListener("click", function () { | ||||||
|  |             window.alert("Hello world!") | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         return new Marker(el) | ||||||
|  |             .setLngLat(GeoOperations.centerpointCoordinates(feature)) | ||||||
|  |             .setOffset(iconAnchor) | ||||||
|  |             .addTo(this._map) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class ShowDataLayer { | ||||||
|  |     private readonly _map: Store<MlMap> | ||||||
|  |     private _options: ShowDataLayerOptions & { layer: LayerConfig } | ||||||
|  | 
 | ||||||
|  |     constructor(map: Store<MlMap>, options: ShowDataLayerOptions & { layer: LayerConfig }) { | ||||||
|  |         this._map = map | ||||||
|  |         this._options = options | ||||||
|  |         const self = this | ||||||
|  |         map.addCallbackAndRunD((map) => self.initDrawFeatures(map)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private initDrawFeatures(map: MlMap) { | ||||||
|  |         for (const pointRenderingConfig of this._options.layer.mapRendering) { | ||||||
|  |             new PointRenderingLayer( | ||||||
|  |                 map, | ||||||
|  |                 this._options.features, | ||||||
|  |                 pointRenderingConfig, | ||||||
|  |                 this._options.fetchStore | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         if (this._options.zoomToFeatures) { | ||||||
|  |             const features = this._options.features.features.data | ||||||
|  |             const bbox = BBox.bboxAroundAll(features.map((f) => BBox.get(f.feature))) | ||||||
|  |             map.fitBounds(bbox.toLngLat(), { | ||||||
|  |                 padding: { top: 10, bottom: 10, left: 10, right: 10 }, | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -3,13 +3,35 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import { ElementStorage } from "../../Logic/ElementStorage" | import { ElementStorage } from "../../Logic/ElementStorage" | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen" | import ScrollableFullScreen from "../Base/ScrollableFullScreen" | ||||||
|  | import { OsmTags } from "../../Models/OsmFeature" | ||||||
| 
 | 
 | ||||||
| export interface ShowDataLayerOptions { | export interface ShowDataLayerOptions { | ||||||
|  |     /** | ||||||
|  |      * Features to show | ||||||
|  |      */ | ||||||
|     features: FeatureSource |     features: FeatureSource | ||||||
|  |     /** | ||||||
|  |      * Indication of the current selected element; overrides some filters | ||||||
|  |      */ | ||||||
|     selectedElement?: UIEventSource<any> |     selectedElement?: UIEventSource<any> | ||||||
|     leafletMap: Store<L.Map> |     /** | ||||||
|     popup?: undefined | ((tags: UIEventSource<any>, layer: LayerConfig) => ScrollableFullScreen) |      * What popup to build when a feature is selected | ||||||
|  |      */ | ||||||
|  |     buildPopup?: | ||||||
|  |         | undefined | ||||||
|  |         | ((tags: UIEventSource<any>, layer: LayerConfig) => ScrollableFullScreen) | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * If set, zoom to the features when initially loaded and when they are changed | ||||||
|  |      */ | ||||||
|     zoomToFeatures?: false | boolean |     zoomToFeatures?: false | boolean | ||||||
|     doShowLayer?: Store<boolean> |     /** | ||||||
|     state?: { allElements?: ElementStorage } |      * Toggles the layer on/off | ||||||
|  |      */ | ||||||
|  |     doShowLayer?: Store<true | boolean> | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Function which fetches the relevant store | ||||||
|  |      */ | ||||||
|  |     fetchStore?: (id: string) => Store<OsmTags> | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										121
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										121
									
								
								test.ts
									
										
									
									
									
								
							|  | @ -1,24 +1,23 @@ | ||||||
| import SvelteUIElement from "./UI/Base/SvelteUIElement" | import SvelteUIElement from "./UI/Base/SvelteUIElement" | ||||||
| import MaplibreMap from "./UI/Map/MaplibreMap.svelte" | import MaplibreMap from "./UI/Map/MaplibreMap.svelte" | ||||||
| import { Store, Stores, UIEventSource } from "./Logic/UIEventSource" | import { ImmutableStore, UIEventSource } from "./Logic/UIEventSource" | ||||||
| import { MapLibreAdaptor } from "./UI/Map/MapLibreAdaptor" | import { MapLibreAdaptor } from "./UI/Map/MapLibreAdaptor" | ||||||
| import { | import { AvailableRasterLayers, RasterLayerPolygon } from "./Models/RasterLayers" | ||||||
|     EditorLayerIndexProperties, |  | ||||||
|     RasterLayerPolygon, |  | ||||||
|     RasterLayerProperties, |  | ||||||
| } from "./Models/RasterLayers" |  | ||||||
| import type { Map as MlMap } from "maplibre-gl" | import type { Map as MlMap } from "maplibre-gl" | ||||||
| import { AvailableRasterLayers } from "./Models/RasterLayers" |  | ||||||
| import Loc from "./Models/Loc" |  | ||||||
| import { BBox } from "./Logic/BBox" |  | ||||||
| import { GeoOperations } from "./Logic/GeoOperations" |  | ||||||
| import RasterLayerPicker from "./UI/Map/RasterLayerPicker.svelte" | import RasterLayerPicker from "./UI/Map/RasterLayerPicker.svelte" | ||||||
| import BackgroundLayerResetter from "./Logic/Actors/BackgroundLayerResetter" | import BackgroundLayerResetter from "./Logic/Actors/BackgroundLayerResetter" | ||||||
| 
 | import { ShowDataLayer } from "./UI/Map/ShowDataLayer" | ||||||
|  | import StaticFeatureSource from "./Logic/FeatureSource/Sources/StaticFeatureSource" | ||||||
|  | import { Layer } from "leaflet" | ||||||
|  | import LayerConfig from "./Models/ThemeConfig/LayerConfig" | ||||||
|  | import * as bench from "./assets/generated/layers/bench.json" | ||||||
|  | import { Utils } from "./Utils" | ||||||
|  | import SimpleFeatureSource from "./Logic/FeatureSource/Sources/SimpleFeatureSource" | ||||||
|  | import { FilterState } from "./Models/FilteredLayer" | ||||||
|  | import { FixedUiElement } from "./UI/Base/FixedUiElement" | ||||||
| async function main() { | async function main() { | ||||||
|     const mlmap = new UIEventSource<MlMap>(undefined) |     const mlmap = new UIEventSource<MlMap>(undefined) | ||||||
|     const locationControl = new UIEventSource<Loc>({ |     const location = new UIEventSource<{ lon: number; lat: number }>({ | ||||||
|         zoom: 14, |  | ||||||
|         lat: 51.1, |         lat: 51.1, | ||||||
|         lon: 3.1, |         lon: 3.1, | ||||||
|     }) |     }) | ||||||
|  | @ -29,44 +28,70 @@ async function main() { | ||||||
|         .SetStyle("height: 50vh; width: 90%; margin: 1%") |         .SetStyle("height: 50vh; width: 90%; margin: 1%") | ||||||
|         .AttachTo("maindiv") |         .AttachTo("maindiv") | ||||||
|     const bg = new UIEventSource<RasterLayerPolygon>(undefined) |     const bg = new UIEventSource<RasterLayerPolygon>(undefined) | ||||||
|     new MapLibreAdaptor(mlmap, { |     const mla = new MapLibreAdaptor(mlmap, { | ||||||
|         backgroundLayer: bg, |         rasterLayer: bg, | ||||||
|         locationControl, |         location, | ||||||
|     }) |     }) | ||||||
| 
 | 
 | ||||||
|     const availableLayersBboxes = Stores.ListStabilized( |     const features = new UIEventSource([ | ||||||
|         locationControl.mapD((loc) => { |         { | ||||||
|             const lonlat: [number, number] = [loc.lon, loc.lat] |             feature: { | ||||||
|             return AvailableRasterLayers.EditorLayerIndex.filter((eliPolygon) => |                 type: "Feature", | ||||||
|                 BBox.get(eliPolygon).contains(lonlat) |                 properties: { | ||||||
|             ) |                     hello: "world", | ||||||
|         }) |                     id: "" + 1, | ||||||
|     ) |                 }, | ||||||
|     const availableLayers: Store<RasterLayerPolygon[]> = Stores.ListStabilized( |                 geometry: { | ||||||
|         availableLayersBboxes.map((eliPolygons) => { |                     type: "Point", | ||||||
|             const loc = locationControl.data |                     coordinates: [3.1, 51.2], | ||||||
|             const lonlat: [number, number] = [loc.lon, loc.lat] |                 }, | ||||||
|             const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => { |             }, | ||||||
|                 if (eliPolygon.geometry === null) { |             freshness: new Date(), | ||||||
|                     return true // global ELI-layer
 |         }, | ||||||
|  |     ]) | ||||||
|  |     const layer = new LayerConfig(bench) | ||||||
|  |     const options = { | ||||||
|  |         zoomToFeatures: false, | ||||||
|  |         features: new SimpleFeatureSource( | ||||||
|  |             { | ||||||
|  |                 layerDef: layer, | ||||||
|  |                 isDisplayed: new UIEventSource<boolean>(true), | ||||||
|  |                 appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined), | ||||||
|  |             }, | ||||||
|  |             0, | ||||||
|  |             features | ||||||
|  |         ), | ||||||
|  |         layer, | ||||||
|     } |     } | ||||||
|                 return GeoOperations.inside(lonlat, eliPolygon) |     new ShowDataLayer(mlmap, options) | ||||||
|             }) |     mla.zoom.set(9) | ||||||
|             matching.unshift(AvailableRasterLayers.osmCarto) |     mla.location.set({ lon: 3.1, lat: 51.1 }) | ||||||
|             matching.push(...AvailableRasterLayers.globalLayers) |     const availableLayers = AvailableRasterLayers.layersAvailableAt(location) | ||||||
|             return matching |     // new BackgroundLayerResetter(bg, availableLayers)
 | ||||||
|         }) |     // new SvelteUIElement(RasterLayerPicker, { availableLayers, value: bg }).AttachTo("extradiv")
 | ||||||
|     ) |     for (let i = 0; i <= 10; i++) { | ||||||
| 
 |         await Utils.waitFor(1000) | ||||||
|     availableLayers.map((a) => |         features.ping() | ||||||
|         console.log( |         new FixedUiElement("> " + (5 - i)).AttachTo("extradiv") | ||||||
|             "Availabe layers at current location:", |     } | ||||||
|             a.map((al) => al.properties.id) |     options.zoomToFeatures = false | ||||||
|         ) |     features.setData([ | ||||||
|     ) |         { | ||||||
| 
 |             feature: { | ||||||
|     new BackgroundLayerResetter(bg, availableLayers) |                 type: "Feature", | ||||||
|     new SvelteUIElement(RasterLayerPicker, { availableLayers, value: bg }).AttachTo("extradiv") |                 properties: { | ||||||
|  |                     hello: "world", | ||||||
|  |                     id: "" + 1, | ||||||
|  |                 }, | ||||||
|  |                 geometry: { | ||||||
|  |                     type: "Point", | ||||||
|  |                     coordinates: [3.103, 51.10003], | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             freshness: new Date(), | ||||||
|  |         }, | ||||||
|  |     ]) | ||||||
|  |     new FixedUiElement("> OK").AttachTo("extradiv") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| main().then((_) => {}) | main().then((_) => {}) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue