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
|
@ -1,65 +1,81 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Map as MLMap } from "maplibre-gl"
|
||||
import {
|
||||
EditorLayerIndexProperties,
|
||||
RasterLayerPolygon,
|
||||
RasterLayerProperties,
|
||||
} from "../../Models/RasterLayers"
|
||||
import { RasterLayerPolygon, RasterLayerProperties } from "../../Models/RasterLayers"
|
||||
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 _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
|
||||
*/
|
||||
readonly backgroundLayer?: Store<RasterLayerPolygon>
|
||||
readonly locationControl?: UIEventSource<Loc>
|
||||
}
|
||||
) {
|
||||
/**
|
||||
* Used for internal bookkeeping (to remove a rasterLayer when done loading)
|
||||
* @private
|
||||
*/
|
||||
private _currentRasterLayer: string
|
||||
constructor(maplibreMap: Store<MLMap>, state?: Partial<Omit<MapState, "bounds">>) {
|
||||
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
|
||||
this._backgroundLayer?.addCallback((_) => self.setBackground())
|
||||
|
||||
maplibreMap.addCallbackAndRunD((map) => {
|
||||
map.on("load", () => {
|
||||
self.setBackground()
|
||||
})
|
||||
if (state.locationControl) {
|
||||
self.MoveMapToCurrentLoc(state.locationControl.data)
|
||||
map.on("moveend", () => {
|
||||
const dt = state.locationControl.data
|
||||
dt.lon = map.getCenter().lng
|
||||
dt.lat = map.getCenter().lat
|
||||
dt.zoom = map.getZoom()
|
||||
state.locationControl.ping()
|
||||
})
|
||||
}
|
||||
self.MoveMapToCurrentLoc(this.location.data)
|
||||
self.SetZoom(this.zoom.data)
|
||||
map.on("moveend", () => {
|
||||
const dt = this.location.data
|
||||
dt.lon = map.getCenter().lng
|
||||
dt.lat = map.getCenter().lat
|
||||
this.location.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)
|
||||
})
|
||||
this.zoom.addCallbackAndRunD((z) => self.SetZoom(z))
|
||||
}
|
||||
|
||||
private MoveMapToCurrentLoc(loc: Loc) {
|
||||
private SetZoom(z: number) {
|
||||
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
|
||||
if (map === undefined || loc === undefined) {
|
||||
return
|
||||
}
|
||||
if (map.getZoom() !== loc.zoom) {
|
||||
map.setZoom(loc.zoom)
|
||||
}
|
||||
|
||||
const center = map.getCenter()
|
||||
if (center.lng !== loc.lon || center.lat !== loc.lat) {
|
||||
map.setCenter({ lng: loc.lon, lat: loc.lat })
|
||||
|
@ -120,14 +136,14 @@ export class MapLibreAdaptor {
|
|||
if (map === undefined) {
|
||||
return
|
||||
}
|
||||
const background: RasterLayerProperties = this._backgroundLayer?.data?.properties
|
||||
const background: RasterLayerProperties = this.rasterLayer?.data?.properties
|
||||
if (background !== undefined && this._currentRasterLayer === background.id) {
|
||||
// already the correct background layer, nothing to do
|
||||
return
|
||||
}
|
||||
await this.awaitStyleIsLoaded()
|
||||
|
||||
if (background !== this._backgroundLayer?.data?.properties) {
|
||||
if (background !== this.rasterLayer?.data?.properties) {
|
||||
// User selected another background in the meantime... abort
|
||||
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 },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue