forked from MapComplete/MapComplete
Feature: first version of clustering at low zoom levels, filters don't update yet (WIP)
This commit is contained in:
parent
4e033a93a5
commit
8360ab9a8b
11 changed files with 562 additions and 262 deletions
|
|
@ -1,14 +1,11 @@
|
|||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { AddLayerObject, Alignment, Map as MlMap } from "maplibre-gl"
|
||||
import { GeoJSONSource, Marker } from "maplibre-gl"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { AddLayerObject, Map as MlMap } from "maplibre-gl"
|
||||
import { GeoJSONSource } from "maplibre-gl"
|
||||
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import { FeatureSource, FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { Feature, Point } from "geojson"
|
||||
import { Feature } from "geojson"
|
||||
import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig"
|
||||
import { Utils } from "../../Utils"
|
||||
import * as range_layer from "../../../assets/layers/range/range.json"
|
||||
|
|
@ -17,209 +14,9 @@ import FilteredLayer from "../../Models/FilteredLayer"
|
|||
import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource"
|
||||
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
|
||||
class PointRenderingLayer {
|
||||
private readonly _config: PointRenderingConfig
|
||||
private readonly _visibility?: Store<boolean>
|
||||
private readonly _fetchStore?: (id: string) => Store<Record<string, string>>
|
||||
private readonly _map: MlMap
|
||||
private readonly _onClick: (feature: Feature) => void
|
||||
private readonly _allMarkers: Map<string, Marker> = new Map<string, Marker>()
|
||||
private readonly _selectedElement: Store<{ properties: { id?: string } }>
|
||||
private readonly _markedAsSelected: HTMLElement[] = []
|
||||
private readonly _metatags: Store<Record<string, string>>
|
||||
private _dirty = false
|
||||
|
||||
constructor(
|
||||
map: MlMap,
|
||||
layer: LayerConfig,
|
||||
features: FeatureSource,
|
||||
config: PointRenderingConfig,
|
||||
metatags?: Store<Record<string, string>>,
|
||||
visibility?: Store<boolean>,
|
||||
fetchStore?: (id: string) => Store<Record<string, string>>,
|
||||
onClick?: (feature: Feature) => void,
|
||||
selectedElement?: Store<{ properties: { id?: string } }>
|
||||
) {
|
||||
this._visibility = visibility
|
||||
this._config = config
|
||||
this._map = map
|
||||
this._metatags = metatags
|
||||
this._fetchStore = fetchStore
|
||||
this._onClick = onClick
|
||||
this._selectedElement = selectedElement
|
||||
if (!features?.features) {
|
||||
throw (
|
||||
"Could not setup a PointRenderingLayer; features?.features is undefined/null. The layer is " +
|
||||
layer.id
|
||||
)
|
||||
}
|
||||
features.features?.addCallbackAndRunD((features) => this.updateFeatures(features))
|
||||
visibility?.addCallbackAndRunD((visible) => {
|
||||
if (visible === true && this._dirty) {
|
||||
this.updateFeatures(features.features.data)
|
||||
}
|
||||
this.setVisibility(visible)
|
||||
})
|
||||
selectedElement?.addCallbackAndRun((selected) => {
|
||||
this._markedAsSelected.forEach((el) => el.classList.remove("selected"))
|
||||
this._markedAsSelected.splice(0, this._markedAsSelected.length)
|
||||
if (selected === undefined) {
|
||||
return
|
||||
}
|
||||
PointRenderingConfig.allowed_location_codes.forEach((code) => {
|
||||
const marker = this._allMarkers
|
||||
.get(selected.properties?.id + "-" + code)
|
||||
?.getElement()
|
||||
if (marker === undefined) {
|
||||
return
|
||||
}
|
||||
marker?.classList?.add("selected")
|
||||
this._markedAsSelected.push(marker)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private updateFeatures(features: Feature[]) {
|
||||
if (this._visibility?.data === false) {
|
||||
this._dirty = true
|
||||
return
|
||||
}
|
||||
this._dirty = false
|
||||
const cache = this._allMarkers
|
||||
const unseenKeys = new Set(cache.keys())
|
||||
for (const location of this._config.location) {
|
||||
for (const feature of features) {
|
||||
if (feature?.geometry === undefined) {
|
||||
console.warn(
|
||||
"Got an invalid feature:",
|
||||
features,
|
||||
" while rendering",
|
||||
location,
|
||||
"of",
|
||||
this._config
|
||||
)
|
||||
}
|
||||
const id = feature.properties.id + "-" + location
|
||||
unseenKeys.delete(id)
|
||||
|
||||
if (location === "waypoints") {
|
||||
if (feature.geometry.type === "LineString") {
|
||||
for (const loc of feature.geometry.coordinates) {
|
||||
this.addPoint(feature, <[number, number]>loc)
|
||||
}
|
||||
}
|
||||
if (
|
||||
feature.geometry.type === "MultiLineString" ||
|
||||
feature.geometry.type === "Polygon"
|
||||
) {
|
||||
for (const coors of feature.geometry.coordinates) {
|
||||
for (const loc of coors) {
|
||||
this.addPoint(feature, <[number, number]>loc)
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const loc = GeoOperations.featureToCoordinateWithRenderingType(feature, location)
|
||||
if (loc === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (cache.has(id)) {
|
||||
const cached = cache.get(id)
|
||||
const oldLoc = cached.getLngLat()
|
||||
if (loc[0] !== oldLoc.lng && loc[1] !== oldLoc.lat) {
|
||||
cached.setLngLat(loc)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const marker = this.addPoint(feature, loc)
|
||||
if (this._selectedElement?.data === feature.properties.id) {
|
||||
marker.getElement().classList.add("selected")
|
||||
this._markedAsSelected.push(marker.getElement())
|
||||
}
|
||||
cache.set(id, marker)
|
||||
}
|
||||
}
|
||||
|
||||
for (const unseenKey of unseenKeys) {
|
||||
cache.get(unseenKey).remove()
|
||||
cache.delete(unseenKey)
|
||||
}
|
||||
}
|
||||
|
||||
private setVisibility(visible: boolean) {
|
||||
for (const marker of this._allMarkers.values()) {
|
||||
if (visible) {
|
||||
marker.getElement().classList.remove("hidden")
|
||||
} else {
|
||||
marker.getElement().classList.add("hidden")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addPoint(feature: Feature, loc: [number, number]): Marker {
|
||||
let store: Store<Record<string, string>>
|
||||
if (this._fetchStore) {
|
||||
store = this._fetchStore(feature.properties.id)
|
||||
} else {
|
||||
store = new ImmutableStore(<OsmTags>feature.properties)
|
||||
}
|
||||
const { html, iconAnchor } = this._config.RenderIcon(store, { metatags: this._metatags })
|
||||
html.SetClass("marker")
|
||||
if (this._onClick !== undefined) {
|
||||
html.SetClass("cursor-pointer")
|
||||
}
|
||||
const el = html.ConstructElement()
|
||||
|
||||
store.addCallbackAndRunD((tags) => {
|
||||
if (tags._deleted === "yes") {
|
||||
html.SetClass("grayscale")
|
||||
}
|
||||
})
|
||||
|
||||
if (this._onClick) {
|
||||
el.addEventListener("click", (ev) => {
|
||||
ev.preventDefault()
|
||||
this._onClick(feature)
|
||||
// Workaround to signal the MapLibreAdaptor to ignore this click
|
||||
ev["consumed"] = true
|
||||
})
|
||||
}
|
||||
|
||||
const marker = new Marker({ element: el })
|
||||
.setLngLat(loc)
|
||||
.setOffset(iconAnchor)
|
||||
.addTo(this._map)
|
||||
store
|
||||
.map((tags) => this._config.pitchAlignment.GetRenderValue(tags).Subs(tags).txt)
|
||||
.addCallbackAndRun((pitchAligment) =>
|
||||
marker.setPitchAlignment(<Alignment>pitchAligment)
|
||||
)
|
||||
store
|
||||
.map((tags) => this._config.rotationAlignment.GetRenderValue(tags).Subs(tags).txt)
|
||||
.addCallbackAndRun((pitchAligment) =>
|
||||
marker.setRotationAlignment(<Alignment>pitchAligment)
|
||||
)
|
||||
|
||||
if (feature.geometry.type === "Point") {
|
||||
// When the tags get 'pinged', check that the location didn't change
|
||||
store.addCallbackAndRunD(() => {
|
||||
// Check if the location is still the same
|
||||
const oldLoc = marker.getLngLat()
|
||||
const newloc = (<Point>feature.geometry).coordinates
|
||||
if (newloc[0] === oldLoc.lng && newloc[1] === oldLoc.lat) {
|
||||
return
|
||||
}
|
||||
marker.setLngLat({ lon: newloc[0], lat: newloc[1] })
|
||||
})
|
||||
}
|
||||
return marker
|
||||
}
|
||||
}
|
||||
import { PointRenderingLayer } from "./PointRenderingLayer"
|
||||
import { ClusteringFeatureSource } from "../../Logic/FeatureSource/TiledFeatureSource/ClusteringFeatureSource"
|
||||
import summaryLayer from "../../../public/assets/generated/layers/summary.json"
|
||||
|
||||
class LineRenderingLayer {
|
||||
/**
|
||||
|
|
@ -554,6 +351,7 @@ export default class ShowDataLayer {
|
|||
drawLines?: true | boolean
|
||||
}
|
||||
) {
|
||||
console.trace("Creating a data layer for", options.layer.id)
|
||||
this._options = options
|
||||
this.onDestroy.push(map.addCallbackAndRunD((map) => this.initDrawFeatures(map)))
|
||||
}
|
||||
|
|
@ -591,6 +389,26 @@ export default class ShowDataLayer {
|
|||
})
|
||||
}
|
||||
|
||||
public static showLayerClustered(mlmap: Store<MlMap>,
|
||||
state: { mapProperties: { zoom: UIEventSource<number> } },
|
||||
options: ShowDataLayerOptions & { layer: LayerConfig }
|
||||
) {
|
||||
options.preprocessPoints = feats => {
|
||||
const clustering = new ClusteringFeatureSource(feats, state.mapProperties.zoom.map(z => z + 2),
|
||||
options.layer.id,
|
||||
{
|
||||
cutoff: 5
|
||||
})
|
||||
new ShowDataLayer(mlmap, {
|
||||
features: clustering.summaryPoints,
|
||||
layer: new LayerConfig(<LayerConfigJson>(<unknown>summaryLayer), "summaryLayer")
|
||||
// doShowLayer: this.mapProperties.zoom.map((z) => z < maxzoom),
|
||||
})
|
||||
return clustering
|
||||
}
|
||||
new ShowDataLayer(mlmap, options)
|
||||
}
|
||||
|
||||
public static showRange(
|
||||
map: Store<MlMap>,
|
||||
features: FeatureSource,
|
||||
|
|
@ -635,6 +453,7 @@ export default class ShowDataLayer {
|
|||
layer,
|
||||
drawLines,
|
||||
drawMarkers,
|
||||
preprocessPoints
|
||||
} = this._options
|
||||
let onClick = this._options.onClick
|
||||
if (!onClick && selectedElement && layer.title !== undefined) {
|
||||
|
|
@ -672,7 +491,8 @@ export default class ShowDataLayer {
|
|||
doShowLayer,
|
||||
fetchStore,
|
||||
onClick,
|
||||
selectedElement
|
||||
selectedElement,
|
||||
preprocessPoints
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue