Refactoring: Load availableMVTVectorLayers dynamically

This commit is contained in:
Pieter Vander Vennet 2025-01-23 12:30:42 +01:00
parent 2b3e3257fd
commit a50c6b531e
14 changed files with 115 additions and 68 deletions

View file

@ -1,6 +1,6 @@
import GeoJsonSource from "./GeoJsonSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { FeatureSource, UpdatableFeatureSource } from "../FeatureSource"
import { FeatureSource, IndexedFeatureSource, UpdatableFeatureSource } from "../FeatureSource"
import { Or } from "../../Tags/Or"
import FeatureSwitchState from "../../State/FeatureSwitchState"
import OverpassFeatureSource from "./OverpassFeatureSource"
@ -12,13 +12,15 @@ import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeature
import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource"
import DynamicMvtileSource from "../TiledFeatureSource/DynamicMvtTileSource"
import FeatureSourceMerger from "./FeatureSourceMerger"
import { Feature } from "geojson"
import { OsmFeature } from "../../../Models/OsmFeature"
/**
* This source will fetch the needed data from various sources for the given layout.
*
* Note that special layers (with `source=null` will be ignored)
*/
export default class ThemeSource extends FeatureSourceMerger {
export default class ThemeSource implements IndexedFeatureSource {
/**
* Indicates if a data source is loading something
*/
@ -26,6 +28,61 @@ export default class ThemeSource extends FeatureSourceMerger {
public static readonly fromCacheZoomLevel = 15
public features: UIEventSource<Feature[]> = new UIEventSource([])
public readonly featuresById: Store<Map<string, Feature>>
private readonly core: Store<ThemeSourceCore>
private readonly addedSources: FeatureSource[] = []
private readonly addedItems: OsmFeature[] = []
constructor(
layers: LayerConfig[],
featureSwitches: FeatureSwitchState,
mapProperties: { bounds: Store<BBox>; zoom: Store<number> },
backend: string,
isDisplayed: (id: string) => Store<boolean>,
mvtAvailableLayers: Store<Set<string>>,
fullNodeDatabaseSource?: FullNodeDatabaseSource
) {
const isLoading = new UIEventSource(true)
this.isLoading = isLoading
const features = this.features = new UIEventSource<Feature[]>([])
const featuresById = this.featuresById = new UIEventSource(new Map())
this.core = mvtAvailableLayers.mapD(mvtAvailableLayers => {
const core = new ThemeSourceCore(layers, featureSwitches, mapProperties, backend, isDisplayed, mvtAvailableLayers, isLoading, fullNodeDatabaseSource)
this.addedSources.forEach(src => core.addSource(src))
this.addedItems.forEach(item => core.addItem(item))
core.features.addCallbackAndRun(data => features.set(data))
core.featuresById.addCallbackAndRun(data => featuresById.set(data))
return core
})
}
public async downloadAll() {
return this.core.data.downloadAll()
}
public addSource(source: FeatureSource) {
this.core.data?.addSource(source)
this.addedSources.push(source)
}
public addItem(obj: OsmFeature) {
this.core.data?.addItem(obj)
this.addedItems.push(obj)
}
}
/**
* This source will fetch the needed data from various sources for the given layout.
*
* Note that special layers (with `source=null` will be ignored)
*/
class ThemeSourceCore extends FeatureSourceMerger {
/**
* This source is _only_ triggered when the data is downloaded for CSV export
* @private
@ -40,6 +97,7 @@ export default class ThemeSource extends FeatureSourceMerger {
backend: string,
isDisplayed: (id: string) => Store<boolean>,
mvtAvailableLayers: Set<string>,
isLoading: UIEventSource<boolean>,
fullNodeDatabaseSource?: FullNodeDatabaseSource
) {
const { bounds, zoom } = mapProperties
@ -58,7 +116,7 @@ export default class ThemeSource extends FeatureSourceMerger {
mapProperties,
{
isActive: isDisplayed(layer.id),
maxAge: layer.maxAgeOfCache,
maxAge: layer.maxAgeOfCache
}
)
fromCache.set(layer.id, src)
@ -66,13 +124,11 @@ export default class ThemeSource extends FeatureSourceMerger {
}
const mvtSources: UpdatableFeatureSource[] = osmLayers
.filter((f) => mvtAvailableLayers.has(f.id))
.map((l) => ThemeSource.setupMvtSource(l, mapProperties, isDisplayed(l.id)))
.map((l) => ThemeSourceCore.setupMvtSource(l, mapProperties, isDisplayed(l.id)))
const nonMvtSources: FeatureSource[] = []
const nonMvtLayers: LayerConfig[] = osmLayers.filter((l) => !mvtAvailableLayers.has(l.id))
const isLoading = new UIEventSource(false)
const osmApiSource = ThemeSource.setupOsmApiSource(
const osmApiSource = ThemeSourceCore.setupOsmApiSource(
osmLayers,
bounds,
zoom,
@ -89,7 +145,7 @@ export default class ThemeSource extends FeatureSourceMerger {
nonMvtLayers.map((l) => l.id),
" cannot be fetched from the cache server, defaulting to overpass/OSM-api"
)
overpassSource = ThemeSource.setupOverpass(osmLayers, bounds, zoom, featureSwitches)
overpassSource = ThemeSourceCore.setupOverpass(osmLayers, bounds, zoom, featureSwitches)
nonMvtSources.push(overpassSource)
}
@ -102,7 +158,7 @@ export default class ThemeSource extends FeatureSourceMerger {
osmApiSource?.isRunning?.addCallbackAndRun(() => setIsLoading())
const geojsonSources: UpdatableFeatureSource[] = geojsonlayers.map((l) =>
ThemeSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id))
ThemeSourceCore.setupGeojsonSource(l, mapProperties, isDisplayed(l.id))
)
const downloadAll = new OverpassFeatureSource(
@ -113,11 +169,11 @@ export default class ThemeSource extends FeatureSourceMerger {
overpassUrl: featureSwitches.overpassUrl,
overpassTimeout: featureSwitches.overpassTimeout,
overpassMaxZoom: new ImmutableStore(99),
widenFactor: 0,
widenFactor: 0
},
{
ignoreZoom: true,
isActive: new ImmutableStore(false),
isActive: new ImmutableStore(false)
}
)
@ -129,7 +185,6 @@ export default class ThemeSource extends FeatureSourceMerger {
downloadAll
)
this.isLoading = isLoading
this._downloadAll = downloadAll
this._mapBounds = mapProperties.bounds
}
@ -192,7 +247,7 @@ export default class ThemeSource extends FeatureSourceMerger {
backend,
isActive,
patchRelations: true,
fullNodeDatabase,
fullNodeDatabase
})
}
@ -224,11 +279,11 @@ export default class ThemeSource extends FeatureSourceMerger {
widenFactor: 1.5,
overpassUrl: featureSwitches.overpassUrl,
overpassTimeout: featureSwitches.overpassTimeout,
overpassMaxZoom: featureSwitches.overpassMaxZoom,
overpassMaxZoom: featureSwitches.overpassMaxZoom
},
{
padToTiles: zoom.map((zoom) => Math.min(15, zoom + 1)),
isActive,
isActive
}
)
}

View file

@ -8,8 +8,7 @@ export default class ThemeViewStateHashActor {
private readonly _state: {
indexedFeatures: IndexedFeatureSource,
selectedElement: UIEventSource<Feature>,
guistate: MenuState,
previewedImage: UIEventSource<object>
guistate: MenuState
}
private isUpdatingHash = false
@ -39,7 +38,6 @@ export default class ThemeViewStateHashActor {
indexedFeatures: IndexedFeatureSource,
selectedElement: UIEventSource<Feature>,
guistate: MenuState,
previewedImage: UIEventSource<object>
}) {
this._state = state
@ -150,14 +148,8 @@ export default class ThemeViewStateHashActor {
}
private back() {
console.log("Going back via hash actor")
const state = this._state
if (state.previewedImage.data) {
state.previewedImage.setData(undefined)
return
}
if (state.guistate.closeAll()) {
return
}
state.selectedElement.setData(undefined)
state.guistate.closeAll()
}
}

View file

@ -1,5 +1,6 @@
import ThemeConfig from "./ThemeConfig/ThemeConfig"
import { WithImageState } from "./ThemeViewState/WithImageState"
import { Store } from "../Logic/UIEventSource"
/**
*
@ -10,7 +11,7 @@ import { WithImageState } from "./ThemeViewState/WithImageState"
* It ties up all the needed elements and starts some actors.
*/
export default class ThemeViewState extends WithImageState {
constructor(layout: ThemeConfig, mvtAvailableLayers: Set<string>) {
constructor(layout: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
super(layout, mvtAvailableLayers)
}
}

View file

@ -35,7 +35,7 @@ export class WithChangesState extends WithLayoutSourceState {
*/
readonly hasDataInView: Store<FeatureViewState>
constructor(theme: ThemeConfig, mvtAvailableLayers: Set<string>) {
constructor(theme: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
super(theme, mvtAvailableLayers)
this.changes = new Changes(
{
@ -82,7 +82,7 @@ export class WithChangesState extends WithLayoutSourceState {
this.perLayerFiltered = this.showNormalDataOn(this.map)
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
this.toCacheSavers = theme.enableCache ? this.initSaveToLocalStorage() : undefined

View file

@ -3,6 +3,7 @@ import { MenuState } from "../MenuState"
import Hotkeys from "../../UI/Base/Hotkeys"
import Translations from "../../UI/i18n/Translations"
import { WithSpecialLayers } from "./WithSpecialLayers"
import { Store } from "../../Logic/UIEventSource"
/**
* Does all things related to:
@ -11,9 +12,10 @@ import { WithSpecialLayers } from "./WithSpecialLayers"
export class WithGuiState extends WithSpecialLayers {
readonly guistate: MenuState
constructor(theme: ThemeConfig, mvtAvailableLayers: Set<string>) {
constructor(theme: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
super(theme, mvtAvailableLayers)
this.guistate = new MenuState(
this.guistate = new MenuState(this.selectedElement)
this.guistate.openMenuIfNeeded(
this.featureSwitches.featureSwitchWelcomeMessage.data,
theme.id
)

View file

@ -22,7 +22,7 @@ export class WithLayoutSourceState extends WithSelectedElementState {
readonly floors: Store<string[]>
constructor(theme: ThemeConfig, mvtAvailableLayers: Set<string>) {
constructor(theme: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
super(theme)
/* Set up the layout source
* A bit tricky, as this is heavily intertwined with the 'changes'-element, which generates a stream of new and changed features too

View file

@ -7,11 +7,12 @@ import { WithVisualFeedbackState } from "./WithVisualFeedbackState"
import { ShowDataLayerOptions } from "../../UI/Map/ShowDataLayerOptions"
import LayerConfig from "../ThemeConfig/LayerConfig"
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
import { Store } from "../../Logic/UIEventSource"
export class WithSearchState extends WithVisualFeedbackState {
public readonly searchState: SearchState
constructor(theme: ThemeConfig, mvtAvailableLayers: Set<string>) {
constructor(theme: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
super(theme, mvtAvailableLayers)
this.searchState = new SearchState(this)
this.initHotkeysSearch()
@ -48,20 +49,12 @@ export class WithSearchState extends WithVisualFeedbackState {
)
Hotkeys.RegisterHotkey({ nomod: "Escape", onUp: true }, docs.closeSidebar, () => {
if (this.previewedImage.data !== undefined) {
this.previewedImage.setData(undefined)
return
}
if (this.selectedElement.data) {
this.selectedElement.setData(undefined)
if (this.guistate.closeAll()) {
return
}
if (this.searchState.showSearchDrawer.data) {
this.searchState.showSearchDrawer.set(false)
return
}
if (this.guistate.closeAll()) {
return
}
Zoomcontrol.resetzoom()
this.focusOnMap()

View file

@ -28,11 +28,10 @@ export class WithSelectedElementState extends UserMapFeatureswitchState {
})
this.mapProperties.lastClickLocation.addCallbackD((lastClick) => {
if (lastClick.mode !== "left" || !lastClick.nearestFeature) {
if (lastClick.mode !== "left") {
return
}
const f = lastClick.nearestFeature
this.setSelectedElement(f)
this.setSelectedElement(lastClick.nearestFeature)
})
@ -67,7 +66,7 @@ export class WithSelectedElementState extends UserMapFeatureswitchState {
const current = this.selectedElement.data
if (
current?.properties?.id !== undefined &&
current.properties.id === feature.properties.id
current.properties.id === feature?.properties?.id
) {
console.log("Not setting selected, same id", current, feature)
return // already set

View file

@ -14,7 +14,7 @@ import { LayerConfigJson } from "../ThemeConfig/Json/LayerConfigJson"
import last_click_layerconfig from "../../assets/generated/layers/last_click.json"
import { GeoOperations } from "../../Logic/GeoOperations"
import summaryLayer from "../../assets/generated/layers/summary.json"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import NearbyFeatureSource from "../../Logic/FeatureSource/Sources/NearbyFeatureSource"
import {
SummaryTileSource,
@ -43,7 +43,7 @@ export class WithSpecialLayers extends WithChangesState {
readonly visualFeedbackViewportBounds: UIEventSource<BBox> = new UIEventSource<BBox>(undefined)
constructor(theme: ThemeConfig, mvtAvailableLayers: Set<string>) {
constructor(theme: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
super(theme, mvtAvailableLayers)
this.favourites = new FavouritesFeatureSource(this)
@ -64,12 +64,12 @@ export class WithSpecialLayers extends WithChangesState {
this.featureSummary = this.setupSummaryLayer()
this.initActorsSpecialLayers()
this.drawSelectedElement()
this.drawSpecialLayers()
this.drawLastClick()
// Note: the lock-range is handled by UserMapFeatureSwitchState
{
// Activate metatagging for the 'current_view' layer
console.log(">>>", this.layerState.filteredLayers)
const currentViewLayer = this.layerState.filteredLayers.get("current_view")?.layerDef
if (currentViewLayer?.tagRenderings?.length > 0) {
const params = MetaTagging.createExtraFuncParams(this)
@ -163,8 +163,10 @@ export class WithSpecialLayers extends WithChangesState {
})
)
// show last click = new point/note marker
const features = new StaticFeatureSource(lastClickFiltered)
this.featureProperties.trackFeatureSource(features)
new ShowDataLayer(this.map, {
features: new StaticFeatureSource(lastClickFiltered),
features,
layer: lastClickLayerConfig,
onClick: (feature) => {
if (this.mapProperties.zoom.data >= Constants.minZoomLevelToAddNewPoint) {
@ -179,6 +181,13 @@ export class WithSpecialLayers extends WithChangesState {
})
}
private drawSelectedElement() {
const src = new StaticFeatureSource(
this.selectedElement.map((f) => (f === undefined ? [] : [f]))
)
ShowDataLayer.showMultipleLayers(this.map, src, this.theme.layers)
}
private drawSpecialLayers() {
type AddedByDefaultTypes = (typeof Constants.added_by_default)[number]
@ -187,6 +196,7 @@ export class WithSpecialLayers extends WithChangesState {
| "last_click" // handled by this.drawLastClick()
| "summary" // handled by setupSummaryLayer
| "range" // handled by UserMapFeatureSwitchState
| "selected_element" // handled by this.drawSelectedElement
>
const empty = []
/**
@ -199,10 +209,7 @@ export class WithSpecialLayers extends WithChangesState {
gps_track: this.geolocation.historicalUserLocationsTrack,
current_view: this.currentView,
favourite: this.favourites,
geocoded_image: new StaticFeatureSource(this.geocodedImages),
selected_element: new StaticFeatureSource(
this.selectedElement.map((f) => (f === undefined ? empty : [f]))
)
geocoded_image: new StaticFeatureSource(this.geocodedImages)
}

View file

@ -1,5 +1,5 @@
import ThemeConfig from "../ThemeConfig/ThemeConfig"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Hotkeys from "../../UI/Base/Hotkeys"
import Translations from "../../UI/i18n/Translations"
import ThemeViewState from "../ThemeViewState"
@ -11,7 +11,7 @@ export class WithVisualFeedbackState extends ThemeViewState {
*/
public readonly visualFeedback: UIEventSource<boolean> = new UIEventSource<boolean>(false)
constructor(theme: ThemeConfig, mvtAvailableLayers: Set<string>) {
constructor(theme: ThemeConfig, mvtAvailableLayers: Store<Set<string>>) {
super(theme, mvtAvailableLayers)
this.initHotkeysVisualFeedback()
@ -72,7 +72,7 @@ export class WithVisualFeedbackState extends ThemeViewState {
if (this.selectedElement.data !== undefined) {
return false
}
if (this.guistate.isSomethingOpen() || this.previewedImage.data !== undefined) {
if (this.guistate.isSomethingOpen()) {
return
}
if (

View file

@ -9,7 +9,7 @@
export let fullscreen: boolean = false
export let bodyPadding = "p-4 md:p-5 "
export let shown: UIEventSource<boolean>
export let dismissable = true
export let dismissable = false
/**
* Default: 50
*/

View file

@ -16,7 +16,6 @@ import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFea
import FilteredLayer from "../../Models/FilteredLayer"
import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import { featureEach } from "@turf/turf"
class PointRenderingLayer {
private readonly _config: PointRenderingConfig
@ -542,7 +541,7 @@ export default class ShowDataLayer {
mlmap: UIEventSource<MlMap>,
features: FeatureSource,
layers: LayerConfig[],
options?: Partial<ShowDataLayerOptions>
options?: Partial<Omit<ShowDataLayerOptions, "features" | "layer">>
) {
const perLayer: PerLayerFeatureSourceSplitter<FeatureSourceForLayer> =
new PerLayerFeatureSourceSplitter(

View file

@ -14,7 +14,6 @@ import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
import ThemeSource from "../Logic/FeatureSource/Sources/ThemeSource"
import { Map as MlMap } from "maplibre-gl"
@ -71,7 +70,6 @@ export interface SpecialVisualizationState {
readonly imageUploadManager: ImageUploadManager
readonly previewedImage: UIEventSource<ProvidedImage>
readonly nearbyImageSearcher: CombinedFetcher
readonly geolocation: GeoLocationHandler
readonly geocodedImages: UIEventSource<Feature[]>

View file

@ -8,6 +8,7 @@ import { Utils } from "./Utils"
import Constants from "./Models/Constants"
import ArrowDownTray from "@babeard/svelte-heroicons/mini/ArrowDownTray"
import { WithSearchState } from "./Models/ThemeViewState/WithSearchState"
import { UIEventSource } from "./Logic/UIEventSource"
function webgl_support() {
try {
@ -48,11 +49,11 @@ async function main() {
if (!webgl_support()) {
throw "WebGL is not supported or not enabled. This is essential for MapComplete to function, please enable this."
}
const [theme, availableLayers] = await Promise.all([
DetermineTheme.getTheme(),
await getAvailableLayers(),
])
console.log("The available layers on server are", Array.from(availableLayers))
const availableLayers = UIEventSource.FromPromise(getAvailableLayers())
const theme = await DetermineTheme.getTheme()
availableLayers.addCallbackAndRunD(availableLayers => {
console.log("The available layers on server are", Array.from(availableLayers))
})
const state = new WithSearchState(theme, availableLayers)
const target = document.getElementById("maindiv")
const childs = Array.from(target.children)