refactoring

This commit is contained in:
Pieter Vander Vennet 2023-03-28 05:13:48 +02:00
parent b94a8f5745
commit 5d0fe31c41
114 changed files with 2412 additions and 2958 deletions

View file

@ -2,11 +2,12 @@ import { Changes } from "../Osm/Changes"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import { UIEventSource } from "../UIEventSource" import { UIEventSource } from "../UIEventSource"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { Feature } from "geojson"
export default class PendingChangesUploader { export default class PendingChangesUploader {
private lastChange: Date private lastChange: Date
constructor(changes: Changes, selectedFeature: UIEventSource<any>) { constructor(changes: Changes, selectedFeature: UIEventSource<Feature>) {
const self = this const self = this
this.lastChange = new Date() this.lastChange = new Date()
changes.pendingChanges.addCallback(() => { changes.pendingChanges.addCallback(() => {

View file

@ -2,12 +2,19 @@ import { Store, UIEventSource } from "../UIEventSource"
import Locale from "../../UI/i18n/Locale" import Locale from "../../UI/i18n/Locale"
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer" import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer"
import Combine from "../../UI/Base/Combine" import Combine from "../../UI/Base/Combine"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { ElementStorage } from "../ElementStorage"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Feature } from "geojson"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
export default class TitleHandler { export default class TitleHandler {
constructor(selectedElement: Store<any>, layout: LayoutConfig, allElements: ElementStorage) { constructor(
selectedElement: Store<Feature>,
selectedLayer: Store<LayerConfig>,
allElements: FeaturePropertiesStore,
layout: LayoutConfig
) {
const currentTitle: Store<string> = selectedElement.map( const currentTitle: Store<string> = selectedElement.map(
(selected) => { (selected) => {
const defaultTitle = layout?.title?.txt ?? "MapComplete" const defaultTitle = layout?.title?.txt ?? "MapComplete"
@ -17,13 +24,14 @@ export default class TitleHandler {
} }
const tags = selected.properties const tags = selected.properties
for (const layer of layout.layers) { for (const layer of layout?.layers ?? []) {
if (layer.title === undefined) { if (layer.title === undefined) {
continue continue
} }
if (layer.source.osmTags.matchesProperties(tags)) { if (layer.source.osmTags.matchesProperties(tags)) {
const tagsSource = const tagsSource =
allElements.getEventSourceById(tags.id) ?? new UIEventSource<any>(tags) allElements.getStore(tags.id) ??
new UIEventSource<Record<string, string>>(tags)
const title = new TagRenderingAnswer(tagsSource, layer.title, {}) const title = new TagRenderingAnswer(tagsSource, layer.title, {})
return ( return (
new Combine([defaultTitle, " | ", title]).ConstructElement() new Combine([defaultTitle, " | ", title]).ConstructElement()
@ -33,7 +41,7 @@ export default class TitleHandler {
} }
return defaultTitle return defaultTitle
}, },
[Locale.language] [Locale.language, selectedLayer]
) )
currentTitle.addCallbackAndRunD((title) => { currentTitle.addCallbackAndRunD((title) => {

View file

@ -1,39 +1,31 @@
/// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions /// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions
import { Store, UIEventSource } from "./UIEventSource" import { Store, UIEventSource } from "./UIEventSource"
import FeaturePipeline from "./FeatureSource/FeaturePipeline"
import Loc from "../Models/Loc"
import { BBox } from "./BBox" import { BBox } from "./BBox"
import GeoIndexedStore from "./FeatureSource/Actors/GeoIndexedStore"
export default class ContributorCount { export default class ContributorCount {
public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource< public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource<
Map<string, number> Map<string, number>
>(new Map<string, number>()) >(new Map<string, number>())
private readonly state: { private readonly perLayer: ReadonlyMap<string, GeoIndexedStore>
featurePipeline: FeaturePipeline
currentBounds: Store<BBox>
locationControl: Store<Loc>
}
private lastUpdate: Date = undefined private lastUpdate: Date = undefined
constructor(state: { constructor(state: {
featurePipeline: FeaturePipeline bounds: Store<BBox>
currentBounds: Store<BBox> dataIsLoading: Store<boolean>
locationControl: Store<Loc> perLayer: ReadonlyMap<string, GeoIndexedStore>
}) { }) {
this.state = state this.perLayer = state.perLayer
const self = this const self = this
state.currentBounds.map((bbox) => { state.bounds.mapD(
(bbox) => {
self.update(bbox) self.update(bbox)
}) },
state.featurePipeline.runningQuery.addCallbackAndRun((_) => [state.dataIsLoading]
self.update(state.currentBounds.data)
) )
} }
private update(bbox: BBox) { private update(bbox: BBox) {
if (bbox === undefined) {
return
}
const now = new Date() const now = new Date()
if ( if (
this.lastUpdate !== undefined && this.lastUpdate !== undefined &&
@ -42,7 +34,9 @@ export default class ContributorCount {
return return
} }
this.lastUpdate = now this.lastUpdate = now
const featuresList = this.state.featurePipeline.GetAllFeaturesWithin(bbox) const featuresList = [].concat(
Array.from(this.perLayer.values()).map((fs) => fs.GetFeaturesWithin(bbox))
)
const hist = new Map<string, number>() const hist = new Map<string, number>()
for (const list of featuresList) { for (const list of featuresList) {
for (const feature of list) { for (const feature of list) {

View file

@ -1,6 +1,5 @@
import { GeoOperations } from "./GeoOperations" import { GeoOperations } from "./GeoOperations"
import Combine from "../UI/Base/Combine" import Combine from "../UI/Base/Combine"
import RelationsTracker from "./Osm/RelationsTracker"
import BaseUIElement from "../UI/BaseUIElement" import BaseUIElement from "../UI/BaseUIElement"
import List from "../UI/Base/List" import List from "../UI/Base/List"
import Title from "../UI/Base/Title" import Title from "../UI/Base/Title"

View file

@ -0,0 +1,41 @@
import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource"
import { Feature } from "geojson"
import { BBox } from "../../BBox"
import { GeoOperations } from "../../GeoOperations"
import { Store } from "../../UIEventSource"
import FilteredLayer from "../../../Models/FilteredLayer"
/**
* Allows the retrieval of all features in the requested BBox; useful for one-shot queries;
*
* Use a ClippedFeatureSource for a continuously updating featuresource
*/
export default class GeoIndexedStore implements FeatureSource {
public features: Store<Feature[]>
constructor(features: FeatureSource | Store<Feature[]>) {
this.features = features["features"] ?? features
}
/**
* Gets the current features within the given bbox.
*
* @param bbox
* @constructor
*/
public GetFeaturesWithin(bbox: BBox): Feature[] {
// TODO optimize
const bboxFeature = bbox.asGeoJson({})
return this.features.data.filter(
(f) => GeoOperations.intersect(f, bboxFeature) !== undefined
)
}
}
export class GeoIndexedStoreForLayer extends GeoIndexedStore implements FeatureSourceForLayer {
readonly layer: FilteredLayer
constructor(features: FeatureSource | Store<Feature[]>, layer: FilteredLayer) {
super(features)
this.layer = layer
}
}

View file

@ -1,11 +1,10 @@
import { FeatureSourceForLayer, Tiled } from "../FeatureSource" import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
import MetaTagging from "../../MetaTagging" import MetaTagging from "../../MetaTagging"
import { ExtraFuncParams } from "../../ExtraFunctions" import { ExtraFuncParams } from "../../ExtraFunctions"
import FeaturePipeline from "../FeaturePipeline"
import { BBox } from "../../BBox" import { BBox } from "../../BBox"
import { UIEventSource } from "../../UIEventSource" import { UIEventSource } from "../../UIEventSource"
/**** /**
* Concerned with the logic of updating the right layer at the right time * Concerned with the logic of updating the right layer at the right time
*/ */
class MetatagUpdater { class MetatagUpdater {

View file

@ -0,0 +1,36 @@
import FeatureSource, { Tiled } from "../FeatureSource"
import { Tiles } from "../../../Models/TileRange"
import { IdbLocalStorage } from "../../Web/IdbLocalStorage"
import { UIEventSource } from "../../UIEventSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { BBox } from "../../BBox"
import SimpleFeatureSource from "../Sources/SimpleFeatureSource"
import FilteredLayer from "../../../Models/FilteredLayer"
import Loc from "../../../Models/Loc"
import { Feature } from "geojson"
import TileLocalStorage from "./TileLocalStorage"
import { GeoOperations } from "../../GeoOperations"
import { Utils } from "../../../Utils"
/***
* Saves all the features that are passed in to localstorage, so they can be retrieved on the next run
*
* The data is saved in a tiled way on a fixed zoomlevel and is retrievable per layer.
*
* Also see the sibling class
*/
export default class SaveFeatureSourceToLocalStorage {
constructor(layername: string, zoomlevel: number, features: FeatureSource) {
const storage = TileLocalStorage.construct<Feature[]>(layername)
features.features.addCallbackAndRunD((features) => {
const sliced = GeoOperations.slice(zoomlevel, features)
sliced.forEach((features, tileIndex) => {
const src = storage.getTileSource(tileIndex)
if (Utils.sameList(src.data, features)) {
return
}
src.setData(features)
})
})
}
}

View file

@ -1,149 +0,0 @@
import FeatureSource, { Tiled } from "../FeatureSource"
import { Tiles } from "../../../Models/TileRange"
import { IdbLocalStorage } from "../../Web/IdbLocalStorage"
import { UIEventSource } from "../../UIEventSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { BBox } from "../../BBox"
import SimpleFeatureSource from "../Sources/SimpleFeatureSource"
import FilteredLayer from "../../../Models/FilteredLayer"
import Loc from "../../../Models/Loc"
import { Feature } from "geojson"
/***
* Saves all the features that are passed in to localstorage, so they can be retrieved on the next run
*
* Technically, more an Actor then a featuresource, but it fits more neatly this way
*/
export default class SaveTileToLocalStorageActor {
private readonly visitedTiles: UIEventSource<Map<number, Date>>
private readonly _layer: LayerConfig
private readonly _flayer: FilteredLayer
private readonly initializeTime = new Date()
constructor(layer: FilteredLayer) {
this._flayer = layer
this._layer = layer.layerDef
this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id, {
defaultValue: new Map<number, Date>(),
})
this.visitedTiles.stabilized(100).addCallbackAndRunD((tiles) => {
for (const key of Array.from(tiles.keys())) {
const tileFreshness = tiles.get(key)
const toOld =
this.initializeTime.getTime() - tileFreshness.getTime() >
1000 * this._layer.maxAgeOfCache
if (toOld) {
// Purge this tile
this.SetIdb(key, undefined)
console.debug("Purging tile", this._layer.id, key)
tiles.delete(key)
}
}
this.visitedTiles.ping()
return true
})
}
public LoadTilesFromDisk(
currentBounds: UIEventSource<BBox>,
location: UIEventSource<Loc>,
registerFreshness: (tileId: number, freshness: Date) => void,
registerTile: (src: FeatureSource & Tiled) => void
) {
const self = this
const loadedTiles = new Set<number>()
this.visitedTiles.addCallbackD((tiles) => {
if (tiles.size === 0) {
// We don't do anything yet as probably not yet loaded from disk
// We'll unregister later on
return
}
currentBounds.addCallbackAndRunD((bbox) => {
if (self._layer.minzoomVisible > location.data.zoom) {
// Not enough zoom
return
}
// Iterate over all available keys in the local storage, check which are needed and fresh enough
for (const key of Array.from(tiles.keys())) {
const tileFreshness = tiles.get(key)
if (tileFreshness > self.initializeTime) {
// This tile is loaded by another source
continue
}
registerFreshness(key, tileFreshness)
const tileBbox = BBox.fromTileIndex(key)
if (!bbox.overlapsWith(tileBbox)) {
continue
}
if (loadedTiles.has(key)) {
// Already loaded earlier
continue
}
loadedTiles.add(key)
this.GetIdb(key).then((features: Feature[]) => {
if (features === undefined) {
return
}
console.debug("Loaded tile " + self._layer.id + "_" + key + " from disk")
const src = new SimpleFeatureSource(
self._flayer,
key,
new UIEventSource<Feature[]>(features)
)
registerTile(src)
})
}
})
return true // Remove the callback
})
}
public addTile(tile: FeatureSource & Tiled) {
const self = this
tile.features.addCallbackAndRunD((features) => {
const now = new Date()
if (features.length > 0) {
self.SetIdb(tile.tileIndex, features)
}
// We _still_ write the time to know that this tile is empty!
this.MarkVisited(tile.tileIndex, now)
})
}
public poison(lon: number, lat: number) {
for (let z = 0; z < 25; z++) {
const { x, y } = Tiles.embedded_tile(lat, lon, z)
const tileId = Tiles.tile_index(z, x, y)
this.visitedTiles.data.delete(tileId)
}
}
public MarkVisited(tileId: number, freshness: Date) {
this.visitedTiles.data.set(tileId, freshness)
this.visitedTiles.ping()
}
private SetIdb(tileIndex, data) {
try {
IdbLocalStorage.SetDirectly(this._layer.id + "_" + tileIndex, data)
} catch (e) {
console.error(
"Could not save tile to indexed-db: ",
e,
"tileIndex is:",
tileIndex,
"for layer",
this._layer.id
)
}
}
private GetIdb(tileIndex) {
return IdbLocalStorage.GetDirectly(this._layer.id + "_" + tileIndex)
}
}

View file

@ -0,0 +1,63 @@
import { IdbLocalStorage } from "../../Web/IdbLocalStorage"
import { UIEventSource } from "../../UIEventSource"
/**
* A class which allows to read/write a tile to local storage.
*
* Does the heavy lifting for LocalStorageFeatureSource and SaveFeatureToLocalStorage
*/
export default class TileLocalStorage<T> {
private static perLayer: Record<string, TileLocalStorage<any>> = {}
private readonly _layername: string
private readonly cachedSources: Record<number, UIEventSource<T>> = {}
private constructor(layername: string) {
this._layername = layername
}
public static construct<T>(layername: string): TileLocalStorage<T> {
const cached = TileLocalStorage.perLayer[layername]
if (cached) {
return cached
}
const tls = new TileLocalStorage<T>(layername)
TileLocalStorage.perLayer[layername] = tls
return tls
}
/**
* Constructs a UIEventSource element which is synced with localStorage
* @param layername
* @param tileIndex
*/
public getTileSource(tileIndex: number): UIEventSource<T> {
const cached = this.cachedSources[tileIndex]
if (cached) {
return cached
}
const src = UIEventSource.FromPromise(this.GetIdb(tileIndex))
src.addCallbackD((data) => this.SetIdb(tileIndex, data))
this.cachedSources[tileIndex] = src
return src
}
private SetIdb(tileIndex: number, data): void {
try {
IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data)
} catch (e) {
console.error(
"Could not save tile to indexed-db: ",
e,
"tileIndex is:",
tileIndex,
"for layer",
this._layername
)
}
}
private GetIdb(tileIndex: number): Promise<any> {
return IdbLocalStorage.GetDirectly(this._layername + "_" + tileIndex)
}
}

View file

@ -1,581 +0,0 @@
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import FilteringFeatureSource from "./Sources/FilteringFeatureSource"
import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter"
import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "./FeatureSource"
import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource"
import { Store, UIEventSource } from "../UIEventSource"
import { TileHierarchyTools } from "./TiledFeatureSource/TileHierarchy"
import RememberingSource from "./Sources/RememberingSource"
import OverpassFeatureSource from "../Actors/OverpassFeatureSource"
import GeoJsonSource from "./Sources/GeoJsonSource"
import Loc from "../../Models/Loc"
import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor"
import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor"
import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource"
import { TileHierarchyMerger } from "./TiledFeatureSource/TileHierarchyMerger"
import { NewGeometryFromChangesFeatureSource } from "./Sources/NewGeometryFromChangesFeatureSource"
import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator"
/**
* Keeps track of the age of the loaded data.
* Has one freshness-Calculator for every layer
* @private
*/
import { BBox } from "../BBox"
import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource"
import { Tiles } from "../../Models/TileRange"
import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource"
import MapState from "../State/MapState"
import { OsmFeature } from "../../Models/OsmFeature"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { FilterState } from "../../Models/FilteredLayer"
import { GeoOperations } from "../GeoOperations"
import { Utils } from "../../Utils"
/**
* The features pipeline ties together a myriad of various datasources:
*
* - The Overpass-API
* - The OSM-API
* - Third-party geojson files, either sliced or directly.
*
* In order to truly understand this class, please have a look at the following diagram: https://cdn-images-1.medium.com/fit/c/800/618/1*qTK1iCtyJUr4zOyw4IFD7A.jpeg
*
*
*/
export default class FeaturePipeline {
public readonly sufficientlyZoomed: Store<boolean>
public readonly runningQuery: Store<boolean>
public readonly timeout: UIEventSource<number>
public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false)
public readonly newDataLoadedSignal: UIEventSource<FeatureSource> =
new UIEventSource<FeatureSource>(undefined)
/**
* Keeps track of all raw OSM-nodes.
* Only initialized if `ReplaceGeometryAction` is needed somewhere
*/
public readonly fullNodeDatabase?: FullNodeDatabaseSource
private readonly overpassUpdater: OverpassFeatureSource
private state: MapState
private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>
private readonly oldestAllowedDate: Date
private readonly osmSourceZoomLevel
private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>()
private readonly newGeometryHandler: NewGeometryFromChangesFeatureSource
constructor(
handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void,
state: MapState,
options?: {
/*Used for metatagging - will receive all the sources with changeGeometry applied but without filtering*/
handleRawFeatureSource: (source: FeatureSourceForLayer) => void
}
) {
this.state = state
const self = this
const expiryInSeconds = Math.min(
...(state.layoutToUse?.layers?.map((l) => l.maxAgeOfCache) ?? [])
)
this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds)
this.osmSourceZoomLevel = state.osmApiTileSize.data
const useOsmApi = state.locationControl.map(
(l) => l.zoom > (state.overpassMaxZoom.data ?? 12)
)
state.changes.allChanges.addCallbackAndRun((allChanges) => {
allChanges
.filter((ch) => ch.id < 0 && ch.changes !== undefined)
.map((ch) => ch.changes)
.filter((coor) => coor["lat"] !== undefined && coor["lon"] !== undefined)
.forEach((coor) => {
state.layoutToUse.layers.forEach((l) =>
self.localStorageSavers.get(l.id)?.poison(coor["lon"], coor["lat"])
)
})
})
this.sufficientlyZoomed = state.locationControl.map((location) => {
if (location?.zoom === undefined) {
return false
}
let minzoom = Math.min(
...state.filteredLayers.data.map((layer) => layer.layerDef.minzoom ?? 18)
)
return location.zoom >= minzoom
})
const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed)
const perLayerHierarchy = new Map<string, TileHierarchyMerger>()
this.perLayerHierarchy = perLayerHierarchy
// Given a tile, wraps it and passes it on to render (handled by 'handleFeatureSource'
function patchedHandleFeatureSource(
src: FeatureSourceForLayer & IndexedFeatureSource & Tiled
) {
// This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
const withChanges = new ChangeGeometryApplicator(src, state.changes)
const srcFiltered = new FilteringFeatureSource(state, src.tileIndex, withChanges)
handleFeatureSource(srcFiltered)
if (options?.handleRawFeatureSource) {
options.handleRawFeatureSource(withChanges)
}
self.somethingLoaded.setData(true)
// We do not mark as visited here, this is the responsability of the code near the actual loader (e.g. overpassLoader and OSMApiFeatureLoader)
}
for (const filteredLayer of state.filteredLayers.data) {
const id = filteredLayer.layerDef.id
const source = filteredLayer.layerDef.source
const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) =>
patchedHandleFeatureSource(tile)
)
perLayerHierarchy.set(id, hierarchy)
if (id === "type_node") {
this.fullNodeDatabase = new FullNodeDatabaseSource(filteredLayer, (tile) => {
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
})
continue
}
const localTileSaver = new SaveTileToLocalStorageActor(filteredLayer)
this.localStorageSavers.set(filteredLayer.layerDef.id, localTileSaver)
if (source.geojsonSource === undefined) {
// This is an OSM layer
// We load the cached values and register them
// Getting data from upstream happens a bit lower
localTileSaver.LoadTilesFromDisk(
state.currentBounds,
state.locationControl,
(tileIndex, freshness) =>
self.freshnesses.get(id).addTileLoad(tileIndex, freshness),
(tile) => {
console.debug("Loaded tile ", id, tile.tileIndex, "from local cache")
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
hierarchy.registerTile(tile)
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
}
)
continue
}
if (source.geojsonZoomLevel === undefined) {
// This is a 'load everything at once' geojson layer
const src = new GeoJsonSource(filteredLayer)
if (source.isOsmCacheLayer) {
// We split them up into tiles anyway as it is an OSM source
TiledFeatureSource.createHierarchy(src, {
layer: src.layer,
minZoomLevel: this.osmSourceZoomLevel,
noDuplicates: true,
registerTile: (tile) => {
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
perLayerHierarchy.get(id).registerTile(tile)
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
},
})
} else {
new RegisteringAllFromFeatureSourceActor(src, state.allElements)
perLayerHierarchy.get(id).registerTile(src)
src.features.addCallbackAndRunD((_) => self.onNewDataLoaded(src))
}
} else {
new DynamicGeoJsonTileSource(
filteredLayer,
(tile) => {
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
perLayerHierarchy.get(id).registerTile(tile)
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
},
state
)
}
}
const osmFeatureSource = new OsmFeatureSource({
isActive: useOsmApi,
neededTiles: neededTilesFromOsm,
handleTile: (tile) => {
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
if (tile.layer.layerDef.maxAgeOfCache > 0) {
const saver = self.localStorageSavers.get(tile.layer.layerDef.id)
if (saver === undefined) {
console.error(
"No localStorageSaver found for layer ",
tile.layer.layerDef.id
)
}
saver?.addTile(tile)
}
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
},
state: state,
markTileVisited: (tileId) =>
state.filteredLayers.data.forEach((flayer) => {
const layer = flayer.layerDef
if (layer.maxAgeOfCache > 0) {
const saver = self.localStorageSavers.get(layer.id)
if (saver === undefined) {
console.error("No local storage saver found for ", layer.id)
} else {
saver.MarkVisited(tileId, new Date())
}
}
self.freshnesses.get(layer.id).addTileLoad(tileId, new Date())
}),
})
if (this.fullNodeDatabase !== undefined) {
osmFeatureSource.rawDataHandlers.push((osmJson, tileId) =>
this.fullNodeDatabase.handleOsmJson(osmJson, tileId)
)
}
const updater = this.initOverpassUpdater(state, useOsmApi)
this.overpassUpdater = updater
this.timeout = updater.timeout
// Actually load data from the overpass source
new PerLayerFeatureSourceSplitter(
state.filteredLayers,
(source) =>
TiledFeatureSource.createHierarchy(source, {
layer: source.layer,
minZoomLevel: source.layer.layerDef.minzoom,
noDuplicates: true,
maxFeatureCount: state.layoutToUse.clustering.minNeededElements,
maxZoomLevel: state.layoutToUse.clustering.maxZoom,
registerTile: (tile) => {
// We save the tile data for the given layer to local storage - data sourced from overpass
self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile)
perLayerHierarchy
.get(source.layer.layerDef.id)
.registerTile(new RememberingSource(tile))
tile.features.addCallbackAndRunD((f) => {
if (f.length === 0) {
return
}
self.onNewDataLoaded(tile)
})
},
}),
updater,
{
handleLeftovers: (leftOvers) => {
console.warn("Overpass returned a few non-matched features:", leftOvers)
},
}
)
// Also load points/lines that are newly added.
const newGeometry = new NewGeometryFromChangesFeatureSource(
state.changes,
state.allElements,
state.osmConnection._oauth_config.url
)
this.newGeometryHandler = newGeometry
newGeometry.features.addCallbackAndRun((geometries) => {
console.debug("New geometries are:", geometries)
})
new RegisteringAllFromFeatureSourceActor(newGeometry, state.allElements)
// A NewGeometryFromChangesFeatureSource does not split per layer, so we do this next
new PerLayerFeatureSourceSplitter(
state.filteredLayers,
(perLayer) => {
// We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this
perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer)
// AT last, we always apply the metatags whenever possible
perLayer.features.addCallbackAndRunD((_) => {
self.onNewDataLoaded(perLayer)
})
},
newGeometry,
{
handleLeftovers: (leftOvers) => {
console.warn("Got some leftovers from the filteredLayers: ", leftOvers)
},
}
)
this.runningQuery = updater.runningQuery.map(
(overpass) => {
console.log(
"FeaturePipeline: runningQuery state changed: Overpass",
overpass ? "is querying," : "is idle,",
"osmFeatureSource is",
osmFeatureSource.isRunning
? "is running and needs " +
neededTilesFromOsm.data?.length +
" tiles (already got " +
osmFeatureSource.downloadedTiles.size +
" tiles )"
: "is idle"
)
return overpass || osmFeatureSource.isRunning.data
},
[osmFeatureSource.isRunning]
)
}
public GetAllFeaturesWithin(bbox: BBox): OsmFeature[][] {
const self = this
const tiles: OsmFeature[][] = []
Array.from(this.perLayerHierarchy.keys()).forEach((key) => {
const fetched: OsmFeature[][] = self.GetFeaturesWithin(key, bbox)
tiles.push(...fetched)
})
return tiles
}
public GetAllFeaturesAndMetaWithin(
bbox: BBox,
layerIdWhitelist?: Set<string>
): { features: OsmFeature[]; layer: string }[] {
const self = this
const tiles: { features: any[]; layer: string }[] = []
Array.from(this.perLayerHierarchy.keys()).forEach((key) => {
if (layerIdWhitelist !== undefined && !layerIdWhitelist.has(key)) {
return
}
return tiles.push({
layer: key,
features: [].concat(...self.GetFeaturesWithin(key, bbox)),
})
})
return tiles
}
/**
* Gets all the tiles which overlap with the given BBOX.
* This might imply that extra features might be shown
*/
public GetFeaturesWithin(layerId: string, bbox: BBox): OsmFeature[][] {
if (layerId === "*") {
return this.GetAllFeaturesWithin(bbox)
}
const requestedHierarchy = this.perLayerHierarchy.get(layerId)
if (requestedHierarchy === undefined) {
console.warn(
"Layer ",
layerId,
"is not defined. Try one of ",
Array.from(this.perLayerHierarchy.keys())
)
return undefined
}
return TileHierarchyTools.getTiles(requestedHierarchy, bbox)
.filter((featureSource) => featureSource.features?.data !== undefined)
.map((featureSource) => <OsmFeature[]>featureSource.features.data)
}
public GetTilesPerLayerWithin(
bbox: BBox,
handleTile: (tile: FeatureSourceForLayer & Tiled) => void
) {
Array.from(this.perLayerHierarchy.values()).forEach((hierarchy) => {
TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile)
})
}
private onNewDataLoaded(src: FeatureSource) {
this.newDataLoadedSignal.setData(src)
}
private freshnessForVisibleLayers(z: number, x: number, y: number): Date {
let oldestDate = undefined
for (const flayer of this.state.filteredLayers.data) {
if (!flayer.isDisplayed.data && !flayer.layerDef.forceLoad) {
continue
}
if (this.state.locationControl.data.zoom < flayer.layerDef.minzoom) {
continue
}
if (flayer.layerDef.maxAgeOfCache === 0) {
return undefined
}
const freshnessCalc = this.freshnesses.get(flayer.layerDef.id)
if (freshnessCalc === undefined) {
console.warn("No freshness tracker found for ", flayer.layerDef.id)
return undefined
}
const freshness = freshnessCalc.freshnessFor(z, x, y)
if (freshness === undefined) {
// SOmething is undefined --> we return undefined as we have to download
return undefined
}
if (oldestDate === undefined || oldestDate > freshness) {
oldestDate = freshness
}
}
return oldestDate
}
/*
* Gives an UIEventSource containing the tileIndexes of the tiles that should be loaded from OSM
* */
private getNeededTilesFromOsm(isSufficientlyZoomed: Store<boolean>): Store<number[]> {
const self = this
return this.state.currentBounds.map(
(bbox) => {
if (bbox === undefined) {
return []
}
if (!isSufficientlyZoomed.data) {
return []
}
const osmSourceZoomLevel = self.osmSourceZoomLevel
const range = bbox.containingTileRange(osmSourceZoomLevel)
const tileIndexes = []
if (range.total >= 100) {
// Too much tiles!
return undefined
}
Tiles.MapRange(range, (x, y) => {
const i = Tiles.tile_index(osmSourceZoomLevel, x, y)
const oldestDate = self.freshnessForVisibleLayers(osmSourceZoomLevel, x, y)
if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) {
console.debug(
"Skipping tile",
osmSourceZoomLevel,
x,
y,
"as a decently fresh one is available"
)
// The cached tiles contain decently fresh data
return undefined
}
tileIndexes.push(i)
})
return tileIndexes
},
[isSufficientlyZoomed]
)
}
private initOverpassUpdater(
state: {
layoutToUse: LayoutConfig
currentBounds: Store<BBox>
locationControl: Store<Loc>
readonly overpassUrl: Store<string[]>
readonly overpassTimeout: Store<number>
readonly overpassMaxZoom: Store<number>
},
useOsmApi: Store<boolean>
): OverpassFeatureSource {
const minzoom = Math.min(...state.layoutToUse.layers.map((layer) => layer.minzoom))
const overpassIsActive = state.currentBounds.map(
(bbox) => {
if (bbox === undefined) {
console.debug("Disabling overpass source: no bbox")
return false
}
let zoom = state.locationControl.data.zoom
if (zoom < minzoom) {
// We are zoomed out over the zoomlevel of any layer
console.debug("Disabling overpass source: zoom < minzoom")
return false
}
const range = bbox.containingTileRange(zoom)
if (range.total >= 5000) {
// Let's assume we don't have so much data cached
return true
}
const self = this
const allFreshnesses = Tiles.MapRange(range, (x, y) =>
self.freshnessForVisibleLayers(zoom, x, y)
)
return allFreshnesses.some(
(freshness) => freshness === undefined || freshness < this.oldestAllowedDate
)
},
[state.locationControl]
)
return new OverpassFeatureSource(state, {
padToTiles: state.locationControl.map((l) => Math.min(15, l.zoom + 1)),
isActive: useOsmApi.map((b) => !b && overpassIsActive.data, [overpassIsActive]),
})
}
/**
* Builds upon 'GetAllFeaturesAndMetaWithin', but does stricter BBOX-checking and applies the filters
*/
public getAllVisibleElementsWithmeta(
bbox: BBox
): { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] {
if (bbox === undefined) {
console.warn("No bbox")
return []
}
const layers = Utils.toIdRecord(this.state.layoutToUse.layers)
const elementsWithMeta: { features: OsmFeature[]; layer: string }[] =
this.GetAllFeaturesAndMetaWithin(bbox)
let elements: { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] = []
let seenElements = new Set<string>()
for (const elementsWithMetaElement of elementsWithMeta) {
const layer = layers[elementsWithMetaElement.layer]
if (layer.title === undefined) {
continue
}
const filtered = this.state.filteredLayers.data.find((fl) => fl.layerDef == layer)
for (let i = 0; i < elementsWithMetaElement.features.length; i++) {
const element = elementsWithMetaElement.features[i]
if (!filtered.isDisplayed.data) {
continue
}
if (seenElements.has(element.properties.id)) {
continue
}
seenElements.add(element.properties.id)
if (!bbox.overlapsWith(BBox.get(element))) {
continue
}
if (layer?.isShown !== undefined && !layer.isShown.matchesProperties(element)) {
continue
}
const activeFilters: FilterState[] = Array.from(
filtered.appliedFilters.data.values()
)
if (
!activeFilters.every(
(filter) =>
filter?.currentFilter === undefined ||
filter?.currentFilter?.matchesProperties(element.properties)
)
) {
continue
}
const center = GeoOperations.centerpointCoordinates(element)
elements.push({
element,
center,
layer: layers[elementsWithMetaElement.layer],
})
}
}
return elements
}
/**
* Inject a new point
*/
InjectNewPoint(geojson) {
this.newGeometryHandler.features.data.push(geojson)
this.newGeometryHandler.features.ping()
}
}

View file

@ -1,4 +1,4 @@
import { Store } from "../UIEventSource" import { Store, UIEventSource } from "../UIEventSource"
import FilteredLayer from "../../Models/FilteredLayer" import FilteredLayer from "../../Models/FilteredLayer"
import { BBox } from "../BBox" import { BBox } from "../BBox"
import { Feature } from "geojson" import { Feature } from "geojson"
@ -6,6 +6,9 @@ import { Feature } from "geojson"
export default interface FeatureSource { export default interface FeatureSource {
features: Store<Feature[]> features: Store<Feature[]>
} }
export interface WritableFeatureSource extends FeatureSource {
features: UIEventSource<Feature[]>
}
export interface Tiled { export interface Tiled {
tileIndex: number tileIndex: number

View file

@ -1,48 +1,59 @@
import FeatureSource from "./FeatureSource" import FeatureSource, { FeatureSourceForLayer } from "./FeatureSource"
import { Store } from "../UIEventSource"
import FilteredLayer from "../../Models/FilteredLayer" import FilteredLayer from "../../Models/FilteredLayer"
import SimpleFeatureSource from "./Sources/SimpleFeatureSource" import SimpleFeatureSource from "./Sources/SimpleFeatureSource"
import { Feature } from "geojson" import { Feature } from "geojson"
import { Utils } from "../../Utils"
import { UIEventSource } from "../UIEventSource"
/** /**
* In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled) * In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled)
* If this is the case, multiple objects with a different _matching_layer_id are generated. * If this is the case, multiple objects with a different _matching_layer_id are generated.
* In any case, this featureSource marks the objects with _matching_layer_id * In any case, this featureSource marks the objects with _matching_layer_id
*/ */
export default class PerLayerFeatureSourceSplitter { export default class PerLayerFeatureSourceSplitter<
T extends FeatureSourceForLayer = SimpleFeatureSource
> {
public readonly perLayer: ReadonlyMap<string, T>
constructor( constructor(
layers: Store<FilteredLayer[]>, layers: FilteredLayer[],
handleLayerData: (source: FeatureSource, layer: FilteredLayer) => void,
upstream: FeatureSource, upstream: FeatureSource,
options?: { options?: {
tileIndex?: number constructStore?: (features: UIEventSource<Feature[]>, layer: FilteredLayer) => T
handleLeftovers?: (featuresWithoutLayer: any[]) => void handleLeftovers?: (featuresWithoutLayer: any[]) => void
} }
) { ) {
const knownLayers = new Map<string, SimpleFeatureSource>() const knownLayers = new Map<string, T>()
this.perLayer = knownLayers
const layerSources = new Map<string, UIEventSource<Feature[]>>()
function update() { const constructStore =
const features = upstream.features?.data options?.constructStore ?? ((store, layer) => new SimpleFeatureSource(layer, store))
for (const layer of layers) {
const src = new UIEventSource<Feature[]>([])
layerSources.set(layer.layerDef.id, src)
knownLayers.set(layer.layerDef.id, <T>constructStore(src, layer))
}
upstream.features.addCallbackAndRunD((features) => {
if (features === undefined) { if (features === undefined) {
return return
} }
if (layers.data === undefined || layers.data.length === 0) { if (layers === undefined) {
return return
} }
// We try to figure out (for each feature) in which feature store it should be saved. // We try to figure out (for each feature) in which feature store it should be saved.
// Note that this splitter is only run when it is invoked by the overpass feature source, so we can't be sure in which layer it should go
const featuresPerLayer = new Map<string, Feature[]>() const featuresPerLayer = new Map<string, Feature[]>()
const noLayerFound = [] const noLayerFound: Feature[] = []
for (const layer of layers.data) { for (const layer of layers) {
featuresPerLayer.set(layer.layerDef.id, []) featuresPerLayer.set(layer.layerDef.id, [])
} }
for (const f of features) { for (const f of features) {
let foundALayer = false let foundALayer = false
for (const layer of layers.data) { for (const layer of layers) {
if (layer.layerDef.source.osmTags.matchesProperties(f.properties)) { if (layer.layerDef.source.osmTags.matchesProperties(f.properties)) {
// We have found our matching layer! // We have found our matching layer!
featuresPerLayer.get(layer.layerDef.id).push(f) featuresPerLayer.get(layer.layerDef.id).push(f)
@ -60,7 +71,7 @@ export default class PerLayerFeatureSourceSplitter {
// At this point, we have our features per layer as a list // At this point, we have our features per layer as a list
// We assign them to the correct featureSources // We assign them to the correct featureSources
for (const layer of layers.data) { for (const layer of layers) {
const id = layer.layerDef.id const id = layer.layerDef.id
const features = featuresPerLayer.get(id) const features = featuresPerLayer.get(id)
if (features === undefined) { if (features === undefined) {
@ -68,25 +79,24 @@ export default class PerLayerFeatureSourceSplitter {
continue continue
} }
let featureSource = knownLayers.get(id) const src = layerSources.get(id)
if (featureSource === undefined) {
// Not yet initialized - now is a good time if (Utils.sameList(src.data, features)) {
featureSource = new SimpleFeatureSource(layer) return
featureSource.features.setData(features)
knownLayers.set(id, featureSource)
handleLayerData(featureSource, layer)
} else {
featureSource.features.setData(features)
} }
src.setData(features)
} }
// AT last, the leftovers are handled // AT last, the leftovers are handled
if (options?.handleLeftovers !== undefined && noLayerFound.length > 0) { if (options?.handleLeftovers !== undefined && noLayerFound.length > 0) {
options.handleLeftovers(noLayerFound) options.handleLeftovers(noLayerFound)
} }
})
} }
layers.addCallback((_) => update()) public forEach(f: (featureSource: FeatureSourceForLayer) => void) {
upstream.features.addCallbackAndRunD((_) => update()) for (const fs of this.perLayer.values()) {
f(fs)
}
} }
} }

View file

@ -0,0 +1,17 @@
import FeatureSource from "../FeatureSource"
import { Feature, Polygon } from "geojson"
import StaticFeatureSource from "./StaticFeatureSource"
import { GeoOperations } from "../../GeoOperations"
/**
* Returns a clipped version of the original geojson. Ways which partially intersect the given feature will be split up
*/
export default class ClippedFeatureSource extends StaticFeatureSource {
constructor(features: FeatureSource, clipTo: Feature<Polygon>) {
super(
features.features.mapD((features) => {
return [].concat(features.map((feature) => GeoOperations.clipWith(feature, clipTo)))
})
)
}
}

View file

@ -1,15 +1,15 @@
import { Store, UIEventSource } from "../../UIEventSource" import { Store, UIEventSource } from "../../UIEventSource"
import FilteredLayer, { FilterState } from "../../../Models/FilteredLayer" import FilteredLayer from "../../../Models/FilteredLayer"
import FeatureSource from "../FeatureSource" import FeatureSource from "../FeatureSource"
import { TagsFilter } from "../../Tags/TagsFilter" import { TagsFilter } from "../../Tags/TagsFilter"
import { Feature } from "geojson" import { Feature } from "geojson"
import { OsmTags } from "../../../Models/OsmFeature" import { GlobalFilter } from "../../../Models/GlobalFilter"
export default class FilteringFeatureSource implements FeatureSource { export default class FilteringFeatureSource implements FeatureSource {
public features: UIEventSource<Feature[]> = new UIEventSource([]) public features: UIEventSource<Feature[]> = new UIEventSource([])
private readonly upstream: FeatureSource private readonly upstream: FeatureSource
private readonly _fetchStore?: (id: String) => Store<OsmTags> private readonly _fetchStore?: (id: string) => Store<Record<string, string>>
private readonly _globalFilters?: Store<{ filter: FilterState }[]> private readonly _globalFilters?: Store<GlobalFilter[]>
private readonly _alreadyRegistered = new Set<Store<any>>() private readonly _alreadyRegistered = new Set<Store<any>>()
private readonly _is_dirty = new UIEventSource(false) private readonly _is_dirty = new UIEventSource(false)
private readonly _layer: FilteredLayer private readonly _layer: FilteredLayer
@ -18,8 +18,8 @@ export default class FilteringFeatureSource implements FeatureSource {
constructor( constructor(
layer: FilteredLayer, layer: FilteredLayer,
upstream: FeatureSource, upstream: FeatureSource,
fetchStore?: (id: String) => Store<OsmTags>, fetchStore?: (id: string) => Store<Record<string, string>>,
globalFilters?: Store<{ filter: FilterState }[]>, globalFilters?: Store<GlobalFilter[]>,
metataggingUpdated?: Store<any> metataggingUpdated?: Store<any>
) { ) {
this.upstream = upstream this.upstream = upstream
@ -32,9 +32,11 @@ export default class FilteringFeatureSource implements FeatureSource {
self.update() self.update()
}) })
layer.appliedFilters.addCallback((_) => { layer.appliedFilters.forEach((value) =>
value.addCallback((_) => {
self.update() self.update()
}) })
)
this._is_dirty.stabilized(1000).addCallbackAndRunD((dirty) => { this._is_dirty.stabilized(1000).addCallbackAndRunD((dirty) => {
if (dirty) { if (dirty) {
@ -58,7 +60,7 @@ export default class FilteringFeatureSource implements FeatureSource {
const layer = this._layer const layer = this._layer
const features: Feature[] = this.upstream.features.data ?? [] const features: Feature[] = this.upstream.features.data ?? []
const includedFeatureIds = new Set<string>() const includedFeatureIds = new Set<string>()
const globalFilters = self._globalFilters?.data?.map((f) => f.filter) const globalFilters = self._globalFilters?.data?.map((f) => f)
const newFeatures = (features ?? []).filter((f) => { const newFeatures = (features ?? []).filter((f) => {
self.registerCallback(f) self.registerCallback(f)
@ -71,19 +73,26 @@ export default class FilteringFeatureSource implements FeatureSource {
return false return false
} }
const tagsFilter = Array.from(layer.appliedFilters?.data?.values() ?? []) for (const filter of layer.layerDef.filters) {
for (const filter of tagsFilter) { const state = layer.appliedFilters.get(filter.id).data
const neededTags: TagsFilter = filter?.currentFilter if (state === undefined) {
continue
}
let neededTags: TagsFilter
if (typeof state === "string") {
// This filter uses fields
} else {
neededTags = filter.options[state].osmTags
}
if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) { if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) {
// Hidden by the filter on the layer itself - we want to hide it no matter what // Hidden by the filter on the layer itself - we want to hide it no matter what
return false return false
} }
} }
for (const filter of globalFilters ?? []) { for (const globalFilter of globalFilters ?? []) {
const neededTags: TagsFilter = filter?.currentFilter const neededTags = globalFilter.osmTags
if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) { if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) {
// Hidden by the filter on the layer itself - we want to hide it no matter what
return false return false
} }
} }

View file

@ -58,7 +58,7 @@ export default class GeoJsonSource implements FeatureSource {
.replace("{x_max}", "" + bounds.maxLon) .replace("{x_max}", "" + bounds.maxLon)
} }
const eventsource = new UIEventSource<Feature[]>(undefined) const eventsource = new UIEventSource<Feature[]>([])
if (options?.isActive !== undefined) { if (options?.isActive !== undefined) {
options.isActive.addCallbackAndRunD(async (active) => { options.isActive.addCallbackAndRunD(async (active) => {
if (!active) { if (!active) {

View file

@ -1,14 +1,15 @@
import FeatureSource from "./FeatureSource" import GeoJsonSource from "./GeoJsonSource"
import { Store } from "../UIEventSource" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import FeatureSwitchState from "../State/FeatureSwitchState" import FeatureSource from "../FeatureSource"
import OverpassFeatureSource from "../Actors/OverpassFeatureSource" import { Or } from "../../Tags/Or"
import { BBox } from "../BBox" import FeatureSwitchState from "../../State/FeatureSwitchState"
import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource" import OverpassFeatureSource from "./OverpassFeatureSource"
import { Or } from "../Tags/Or" import { Store } from "../../UIEventSource"
import FeatureSourceMerger from "./Sources/FeatureSourceMerger" import OsmFeatureSource from "./OsmFeatureSource"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import FeatureSourceMerger from "./FeatureSourceMerger"
import GeoJsonSource from "./Sources/GeoJsonSource" import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource"
import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource" import { BBox } from "../../BBox"
import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource"
/** /**
* This source will fetch the needed data from various sources for the given layout. * This source will fetch the needed data from various sources for the given layout.
@ -17,22 +18,24 @@ import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSou
*/ */
export default class LayoutSource extends FeatureSourceMerger { export default class LayoutSource extends FeatureSourceMerger {
constructor( constructor(
filteredLayers: LayerConfig[], layers: LayerConfig[],
featureSwitches: FeatureSwitchState, featureSwitches: FeatureSwitchState,
newAndChangedElements: FeatureSource, newAndChangedElements: FeatureSource,
mapProperties: { bounds: Store<BBox>; zoom: Store<number> }, mapProperties: { bounds: Store<BBox>; zoom: Store<number> },
backend: string, backend: string,
isLayerActive: (id: string) => Store<boolean> isDisplayed: (id: string) => Store<boolean>
) { ) {
const { bounds, zoom } = mapProperties const { bounds, zoom } = mapProperties
// remove all 'special' layers // remove all 'special' layers
filteredLayers = filteredLayers.filter((flayer) => flayer.source !== null) layers = layers.filter((flayer) => flayer.source !== null)
const geojsonlayers = filteredLayers.filter( const geojsonlayers = layers.filter((layer) => layer.source.geojsonSource !== undefined)
(flayer) => flayer.source.geojsonSource !== undefined const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined)
) const fromCache = osmLayers.map(
const osmLayers = filteredLayers.filter( (l) =>
(flayer) => flayer.source.geojsonSource === undefined new LocalStorageFeatureSource(l.id, 15, mapProperties, {
isActive: isDisplayed(l.id),
})
) )
const overpassSource = LayoutSource.setupOverpass(osmLayers, bounds, zoom, featureSwitches) const overpassSource = LayoutSource.setupOverpass(osmLayers, bounds, zoom, featureSwitches)
const osmApiSource = LayoutSource.setupOsmApiSource( const osmApiSource = LayoutSource.setupOsmApiSource(
@ -43,11 +46,11 @@ export default class LayoutSource extends FeatureSourceMerger {
featureSwitches featureSwitches
) )
const geojsonSources: FeatureSource[] = geojsonlayers.map((l) => const geojsonSources: FeatureSource[] = geojsonlayers.map((l) =>
LayoutSource.setupGeojsonSource(l, mapProperties) LayoutSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id))
) )
const expiryInSeconds = Math.min(...(filteredLayers?.map((l) => l.maxAgeOfCache) ?? [])) const expiryInSeconds = Math.min(...(layers?.map((l) => l.maxAgeOfCache) ?? []))
super(overpassSource, osmApiSource, newAndChangedElements, ...geojsonSources) super(overpassSource, osmApiSource, newAndChangedElements, ...geojsonSources, ...fromCache)
} }
private static setupGeojsonSource( private static setupGeojsonSource(
@ -56,6 +59,10 @@ export default class LayoutSource extends FeatureSourceMerger {
isActive?: Store<boolean> isActive?: Store<boolean>
): FeatureSource { ): FeatureSource {
const source = layer.source const source = layer.source
isActive = mapProperties.zoom.map(
(z) => (isActive?.data ?? true) && z >= layer.maxzoom,
[isActive]
)
if (source.geojsonZoomLevel === undefined) { if (source.geojsonZoomLevel === undefined) {
// This is a 'load everything at once' geojson layer // This is a 'load everything at once' geojson layer
return new GeoJsonSource(layer, { isActive }) return new GeoJsonSource(layer, { isActive })

View file

@ -108,6 +108,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
} }
private async LoadTile(z, x, y): Promise<void> { private async LoadTile(z, x, y): Promise<void> {
console.log("OsmFeatureSource: loading ", z, x, y)
if (z >= 22) { if (z >= 22) {
throw "This is an absurd high zoom level" throw "This is an absurd high zoom level"
} }
@ -126,7 +127,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
let error = undefined let error = undefined
try { try {
const osmJson = await Utils.downloadJson(url) const osmJson = await Utils.downloadJsonCached(url, 2000)
try { try {
this.rawDataHandlers.forEach((handler) => this.rawDataHandlers.forEach((handler) =>
handler(osmJson, Tiles.tile_index(z, x, y)) handler(osmJson, Tiles.tile_index(z, x, y))

View file

@ -1,13 +1,13 @@
import { ImmutableStore, Store, UIEventSource } from "../UIEventSource"
import { Or } from "../Tags/Or"
import { Overpass } from "../Osm/Overpass"
import FeatureSource from "../FeatureSource/FeatureSource"
import { Utils } from "../../Utils"
import { TagsFilter } from "../Tags/TagsFilter"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { BBox } from "../BBox"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Feature } from "geojson" import { Feature } from "geojson"
import FeatureSource from "../FeatureSource"
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { Or } from "../../Tags/Or"
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
import { Overpass } from "../../Osm/Overpass"
import { Utils } from "../../../Utils"
import { TagsFilter } from "../../Tags/TagsFilter"
import { BBox } from "../../BBox"
/** /**
* A wrapper around the 'Overpass'-object. * A wrapper around the 'Overpass'-object.
@ -99,7 +99,11 @@ export default class OverpassFeatureSource implements FeatureSource {
) { ) {
return undefined return undefined
} }
const [bounds, date, updatedLayers] = await this.updateAsync() const result = await this.updateAsync()
if (!result) {
return
}
const [bounds, date, updatedLayers] = result
this._lastQueryBBox = bounds this._lastQueryBBox = bounds
} }
@ -188,6 +192,9 @@ export default class OverpassFeatureSource implements FeatureSource {
if (data === undefined) { if (data === undefined) {
return undefined return undefined
} }
// Some metatags are delivered by overpass _without_ underscore-prefix; we fix them below
// TODO FIXME re-enable this data.features.forEach((f) => SimpleMetaTaggers.objectMetaInfo.applyMetaTagsOnFeature(f))
self.features.setData(data.features) self.features.setData(data.features)
return [bounds, date, layersToDownload] return [bounds, date, layersToDownload]
} catch (e) { } catch (e) {

View file

@ -1,34 +0,0 @@
/**
* Every previously added point is remembered, but new points are added.
* Data coming from upstream will always overwrite a previous value
*/
import FeatureSource, { Tiled } from "../FeatureSource"
import { Store, UIEventSource } from "../../UIEventSource"
import { BBox } from "../../BBox"
import { Feature } from "geojson"
export default class RememberingSource implements FeatureSource, Tiled {
public readonly features: Store<Feature[]>
public readonly tileIndex: number
public readonly bbox: BBox
constructor(source: FeatureSource & Tiled) {
const self = this
this.tileIndex = source.tileIndex
this.bbox = source.bbox
const empty = []
const featureSource = new UIEventSource<Feature[]>(empty)
this.features = featureSource
source.features.addCallbackAndRunD((features) => {
const oldFeatures = self.features?.data ?? empty
// Then new ids
const ids = new Set<string>(features.map((f) => f.properties.id + f.geometry.type))
// the old data
const oldData = oldFeatures.filter(
(old) => !ids.has(old.feature.properties.id + old.feature.geometry.type)
)
featureSource.setData([...features, ...oldData])
})
}
}

View file

@ -0,0 +1,29 @@
import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource"
import StaticFeatureSource from "./StaticFeatureSource"
import { GeoOperations } from "../../GeoOperations"
import { BBox } from "../../BBox"
import exp from "constants"
import FilteredLayer from "../../../Models/FilteredLayer"
/**
* Results in a feature source which has all the elements that touch the given features
*/
export default class BBoxFeatureSource extends StaticFeatureSource {
constructor(features: FeatureSource, mustTouch: BBox) {
const bbox = mustTouch.asGeoJson({})
super(
features.features.mapD((features) =>
features.filter((feature) => GeoOperations.intersect(feature, bbox) !== undefined)
)
)
}
}
export class BBoxFeatureSourceForLayer extends BBoxFeatureSource implements FeatureSourceForLayer {
constructor(features: FeatureSourceForLayer, mustTouch: BBox) {
super(features, mustTouch)
this.layer = features.layer
}
readonly layer: FilteredLayer
}

View file

@ -84,7 +84,9 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
}) })
}, },
mapProperties, mapProperties,
{ isActive: options.isActive } {
isActive: options?.isActive,
}
) )
} }
} }

View file

@ -5,7 +5,8 @@ import FeatureSource from "../FeatureSource"
import FeatureSourceMerger from "../Sources/FeatureSourceMerger" import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
/*** /***
* A tiled source which dynamically loads the required tiles at a fixed zoom level * A tiled source which dynamically loads the required tiles at a fixed zoom level.
* A single featureSource will be initiliased for every tile in view; which will alter be merged into this featureSource
*/ */
export default class DynamicTileSource extends FeatureSourceMerger { export default class DynamicTileSource extends FeatureSourceMerger {
constructor( constructor(

View file

@ -1,11 +1,13 @@
import TileHierarchy from "./TileHierarchy"
import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource" import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource"
import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject" import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject"
import SimpleFeatureSource from "../Sources/SimpleFeatureSource" import SimpleFeatureSource from "../Sources/SimpleFeatureSource"
import FilteredLayer from "../../../Models/FilteredLayer" import FilteredLayer from "../../../Models/FilteredLayer"
import { UIEventSource } from "../../UIEventSource" import { UIEventSource } from "../../UIEventSource"
import { OsmTags } from "../../../Models/OsmFeature";
import { BBox } from "../../BBox";
import { Feature, Point } from "geojson";
export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSource & Tiled> { export default class FullNodeDatabaseSource {
public readonly loadedTiles = new Map<number, FeatureSource & Tiled>() public readonly loadedTiles = new Map<number, FeatureSource & Tiled>()
private readonly onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void private readonly onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void
private readonly layer: FilteredLayer private readonly layer: FilteredLayer
@ -81,4 +83,9 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
public GetParentWays(nodeId: number): UIEventSource<OsmWay[]> { public GetParentWays(nodeId: number): UIEventSource<OsmWay[]> {
return this.parentWays.get(nodeId) return this.parentWays.get(nodeId)
} }
getNodesWithin(bBox: BBox) : Feature<Point, OsmTags>[]{
// TODO
throw "TODO"
}
} }

View file

@ -0,0 +1,28 @@
import DynamicTileSource from "./DynamicTileSource"
import { Store } from "../../UIEventSource"
import { BBox } from "../../BBox"
import TileLocalStorage from "../Actors/TileLocalStorage"
import { Feature } from "geojson"
import StaticFeatureSource from "../Sources/StaticFeatureSource"
export default class LocalStorageFeatureSource extends DynamicTileSource {
constructor(
layername: string,
zoomlevel: number,
mapProperties: {
bounds: Store<BBox>
zoom: Store<number>
},
options?: {
isActive?: Store<boolean>
}
) {
const storage = TileLocalStorage.construct<Feature[]>(layername)
super(
zoomlevel,
(tileIndex) => new StaticFeatureSource(storage.getTileSource(tileIndex)),
mapProperties,
options
)
}
}

View file

@ -1,24 +0,0 @@
Data in MapComplete can come from multiple sources.
Currently, they are:
- The Overpass-API
- The OSM-API
- One or more GeoJSON files. This can be a single file or a set of tiled geojson files
- LocalStorage, containing features from a previous visit
- Changes made by the user introducing new features
When the data enters from Overpass or from the OSM-API, they are first distributed per layer:
OVERPASS | ---PerLayerFeatureSource---> FeatureSourceForLayer[]
OSM |
The GeoJSon files (not tiled) are then added to this list
A single FeatureSourcePerLayer is then further handled by splitting it into a tile hierarchy.
In order to keep thins snappy, they are distributed over a tiled database per layer.
## Notes
`cached-featuresbookcases` is the old key used `cahced-features{themeid}` and should be cleaned up

View file

@ -1,24 +0,0 @@
import FeatureSource, { Tiled } from "../FeatureSource"
import { BBox } from "../../BBox"
export default interface TileHierarchy<T extends FeatureSource> {
/**
* A mapping from 'tile_index' to the actual tile featrues
*/
loadedTiles: Map<number, T>
}
export class TileHierarchyTools {
public static getTiles<T extends FeatureSource & Tiled>(
hierarchy: TileHierarchy<T>,
bbox: BBox
): T[] {
const result: T[] = []
hierarchy.loadedTiles.forEach((tile) => {
if (tile.bbox.overlapsWith(bbox)) {
result.push(tile)
}
})
return result
}
}

View file

@ -1,58 +0,0 @@
import TileHierarchy from "./TileHierarchy"
import { UIEventSource } from "../../UIEventSource"
import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource"
import FilteredLayer from "../../../Models/FilteredLayer"
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
import { Tiles } from "../../../Models/TileRange"
import { BBox } from "../../BBox"
export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> {
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<
number,
FeatureSourceForLayer & Tiled
>()
public readonly layer: FilteredLayer
private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map<
number,
UIEventSource<FeatureSource[]>
>()
private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void
constructor(
layer: FilteredLayer,
handleTile: (
src: FeatureSourceForLayer & IndexedFeatureSource & Tiled,
index: number
) => void
) {
this.layer = layer
this._handleTile = handleTile
}
/**
* Add another feature source for the given tile.
* Entries for this tile will be merged
* @param src
*/
public registerTile(src: FeatureSource & Tiled) {
const index = src.tileIndex
if (this.sources.has(index)) {
const sources = this.sources.get(index)
sources.data.push(src)
sources.ping()
return
}
// We have to setup
const sources = new UIEventSource<FeatureSource[]>([src])
this.sources.set(index, sources)
const merger = new FeatureSourceMerger(
this.layer,
index,
BBox.fromTile(...Tiles.tile_from_index(index)),
sources
)
this.loadedTiles.set(index, merger)
this._handleTile(merger, index)
}
}

View file

@ -1,249 +0,0 @@
import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource"
import { Store, UIEventSource } from "../../UIEventSource"
import FilteredLayer from "../../../Models/FilteredLayer"
import TileHierarchy from "./TileHierarchy"
import { Tiles } from "../../../Models/TileRange"
import { BBox } from "../../BBox"
import { Feature } from "geojson";
/**
* Contains all features in a tiled fashion.
* The data will be automatically broken down into subtiles when there are too much features in a single tile or if the zoomlevel is too high
*/
export default class TiledFeatureSource
implements
Tiled,
IndexedFeatureSource,
FeatureSourceForLayer,
TileHierarchy<IndexedFeatureSource & FeatureSourceForLayer & Tiled>
{
public readonly z: number
public readonly x: number
public readonly y: number
public readonly parent: TiledFeatureSource
public readonly root: TiledFeatureSource
public readonly layer: FilteredLayer
/* An index of all known tiles. allTiles[z][x][y].get('layerid') will yield the corresponding tile.
* Only defined on the root element!
*/
public readonly loadedTiles: Map<number, TiledFeatureSource & FeatureSourceForLayer> = undefined
public readonly maxFeatureCount: number
public readonly name
public readonly features: UIEventSource<Feature[]>
public readonly containedIds: Store<Set<string>>
public readonly bbox: BBox
public readonly tileIndex: number
private upper_left: TiledFeatureSource
private upper_right: TiledFeatureSource
private lower_left: TiledFeatureSource
private lower_right: TiledFeatureSource
private readonly maxzoom: number
private readonly options: TiledFeatureSourceOptions
private constructor(
z: number,
x: number,
y: number,
parent: TiledFeatureSource,
options?: TiledFeatureSourceOptions
) {
this.z = z
this.x = x
this.y = y
this.bbox = BBox.fromTile(z, x, y)
this.tileIndex = Tiles.tile_index(z, x, y)
this.name = `TiledFeatureSource(${z},${x},${y})`
this.parent = parent
this.layer = options.layer
options = options ?? {}
this.maxFeatureCount = options?.maxFeatureCount ?? 250
this.maxzoom = options.maxZoomLevel ?? 18
this.options = options
if (parent === undefined) {
throw "Parent is not allowed to be undefined. Use null instead"
}
if (parent === null && z !== 0 && x !== 0 && y !== 0) {
throw "Invalid root tile: z, x and y should all be null"
}
if (parent === null) {
this.root = this
this.loadedTiles = new Map()
} else {
this.root = this.parent.root
this.loadedTiles = this.root.loadedTiles
const i = Tiles.tile_index(z, x, y)
this.root.loadedTiles.set(i, this)
}
this.features = new UIEventSource<any[]>([])
this.containedIds = this.features.map((features) => {
if (features === undefined) {
return undefined
}
return new Set(features.map((f) => f.properties.id))
})
// We register this tile, but only when there is some data in it
if (this.options.registerTile !== undefined) {
this.features.addCallbackAndRunD((features) => {
if (features.length === 0) {
return
}
this.options.registerTile(this)
return true
})
}
}
public static createHierarchy(
features: FeatureSource,
options?: TiledFeatureSourceOptions
): TiledFeatureSource {
options = {
...options,
layer: features["layer"] ?? options.layer,
}
const root = new TiledFeatureSource(0, 0, 0, null, options)
features.features?.addCallbackAndRunD((feats) => root.addFeatures(feats))
return root
}
private isSplitNeeded(featureCount: number) {
if (this.upper_left !== undefined) {
// This tile has been split previously, so we keep on splitting
return true
}
if (this.z >= this.maxzoom) {
// We are not allowed to split any further
return false
}
if (this.options.minZoomLevel !== undefined && this.z < this.options.minZoomLevel) {
// We must have at least this zoom level before we are allowed to start splitting
return true
}
// To much features - we split
return featureCount > this.maxFeatureCount
}
/***
* Adds the list of features to this hierarchy.
* If there are too much features, the list will be broken down and distributed over the subtiles (only retaining features that don't fit a subtile on this level)
* @param features
* @private
*/
private addFeatures(features: Feature[]) {
if (features === undefined || features.length === 0) {
return
}
if (!this.isSplitNeeded(features.length)) {
this.features.setData(features)
return
}
if (this.upper_left === undefined) {
this.upper_left = new TiledFeatureSource(
this.z + 1,
this.x * 2,
this.y * 2,
this,
this.options
)
this.upper_right = new TiledFeatureSource(
this.z + 1,
this.x * 2 + 1,
this.y * 2,
this,
this.options
)
this.lower_left = new TiledFeatureSource(
this.z + 1,
this.x * 2,
this.y * 2 + 1,
this,
this.options
)
this.lower_right = new TiledFeatureSource(
this.z + 1,
this.x * 2 + 1,
this.y * 2 + 1,
this,
this.options
)
}
const ulf = []
const urf = []
const llf = []
const lrf = []
const overlapsboundary = []
for (const feature of features) {
const bbox = BBox.get(feature)
// There are a few strategies to deal with features that cross tile boundaries
if (this.options.noDuplicates) {
// Strategy 1: We put the feature into a somewhat matching tile
if (bbox.overlapsWith(this.upper_left.bbox)) {
ulf.push(feature)
} else if (bbox.overlapsWith(this.upper_right.bbox)) {
urf.push(feature)
} else if (bbox.overlapsWith(this.lower_left.bbox)) {
llf.push(feature)
} else if (bbox.overlapsWith(this.lower_right.bbox)) {
lrf.push(feature)
} else {
overlapsboundary.push(feature)
}
} else if (this.options.minZoomLevel === undefined) {
// Strategy 2: put it into a strictly matching tile (or in this tile, which is slightly too big)
if (bbox.isContainedIn(this.upper_left.bbox)) {
ulf.push(feature)
} else if (bbox.isContainedIn(this.upper_right.bbox)) {
urf.push(feature)
} else if (bbox.isContainedIn(this.lower_left.bbox)) {
llf.push(feature)
} else if (bbox.isContainedIn(this.lower_right.bbox)) {
lrf.push(feature)
} else {
overlapsboundary.push(feature)
}
} else {
// Strategy 3: We duplicate a feature on a boundary into every tile as we need to get to the minZoomLevel
if (bbox.overlapsWith(this.upper_left.bbox)) {
ulf.push(feature)
}
if (bbox.overlapsWith(this.upper_right.bbox)) {
urf.push(feature)
}
if (bbox.overlapsWith(this.lower_left.bbox)) {
llf.push(feature)
}
if (bbox.overlapsWith(this.lower_right.bbox)) {
lrf.push(feature)
}
}
}
this.upper_left.addFeatures(ulf)
this.upper_right.addFeatures(urf)
this.lower_left.addFeatures(llf)
this.lower_right.addFeatures(lrf)
this.features.setData(overlapsboundary)
}
}
export interface TiledFeatureSourceOptions {
readonly maxFeatureCount?: number
readonly maxZoomLevel?: number
readonly minZoomLevel?: number
/**
* IF minZoomLevel is set, and if a feature runs through a tile boundary, it would normally be duplicated.
* Setting 'dontEnforceMinZoomLevel' will assign to feature to some matching subtile.
*/
readonly noDuplicates?: boolean
readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void
readonly layer?: FilteredLayer
}

View file

@ -2,19 +2,34 @@ import { BBox } from "./BBox"
import LayerConfig from "../Models/ThemeConfig/LayerConfig" import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import * as turf from "@turf/turf" import * as turf from "@turf/turf"
import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf" import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf"
import { Feature, Geometry, MultiPolygon, Polygon } from "geojson" import {
import { GeoJSON, LineString, Point, Position } from "geojson" Feature,
GeoJSON,
Geometry,
LineString,
MultiPolygon,
Point,
Polygon,
Position,
} from "geojson"
import togpx from "togpx" import togpx from "togpx"
import Constants from "../Models/Constants" import Constants from "../Models/Constants"
import { Tiles } from "../Models/TileRange"
export class GeoOperations { export class GeoOperations {
private static readonly _earthRadius = 6378137
private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2
/** /**
* Create a union between two features * Create a union between two features
*/ */
static union = turf.union public static union(f0: Feature, f1: Feature): Feature<Polygon | MultiPolygon> | null {
static intersect = turf.intersect return turf.union(<any>f0, <any>f1)
private static readonly _earthRadius = 6378137 }
private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2
public static intersect(f0: Feature, f1: Feature): Feature<Polygon | MultiPolygon> | null {
return turf.intersect(<any>f0, <any>f1)
}
static surfaceAreaInSqMeters(feature: any) { static surfaceAreaInSqMeters(feature: any) {
return turf.area(feature) return turf.area(feature)
@ -637,14 +652,14 @@ export class GeoOperations {
*/ */
static completelyWithin( static completelyWithin(
feature: Feature<Geometry, any>, feature: Feature<Geometry, any>,
possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any> possiblyEnclosingFeature: Feature<Polygon | MultiPolygon, any>
): boolean { ): boolean {
return booleanWithin(feature, possiblyEncloingFeature) return booleanWithin(feature, possiblyEnclosingFeature)
} }
/** /**
* Create an intersection between two features. * Create an intersection between two features.
* A new feature is returned based on 'toSplit', which'll have a geometry that is completely withing boundary * One or multiple new feature is returned based on 'toSplit', which'll have a geometry that is completely withing boundary
*/ */
public static clipWith(toSplit: Feature, boundary: Feature<Polygon>): Feature[] { public static clipWith(toSplit: Feature, boundary: Feature<Polygon>): Feature[] {
if (toSplit.geometry.type === "Point") { if (toSplit.geometry.type === "Point") {
@ -677,35 +692,6 @@ export class GeoOperations {
throw "Invalid geometry type with GeoOperations.clipWith: " + toSplit.geometry.type throw "Invalid geometry type with GeoOperations.clipWith: " + toSplit.geometry.type
} }
/**
* Helper function which does the heavy lifting for 'inside'
*/
private static pointInPolygonCoordinates(
x: number,
y: number,
coordinates: [number, number][][]
): boolean {
const inside = GeoOperations.pointWithinRing(
x,
y,
/*This is the outer ring of the polygon */ coordinates[0]
)
if (!inside) {
return false
}
for (let i = 1; i < coordinates.length; i++) {
const inHole = GeoOperations.pointWithinRing(
x,
y,
coordinates[i] /* These are inner rings, aka holes*/
)
if (inHole) {
return false
}
}
return true
}
/** /**
* *
* *
@ -763,6 +749,62 @@ export class GeoOperations {
throw "Unkown location type: " + location throw "Unkown location type: " + location
} }
} }
/**
* Constructs all tiles where features overlap with and puts those features in them.
* Long features (e.g. lines or polygons) which overlap with multiple tiles are referenced in each tile they overlap with
* @param zoomlevel
* @param features
*/
public static slice(zoomlevel: number, features: Feature[]): Map<number, Feature[]> {
const tiles = new Map<number, Feature[]>()
for (const feature of features) {
const bbox = BBox.get(feature)
Tiles.MapRange(Tiles.tileRangeFrom(bbox, zoomlevel), (x, y) => {
const i = Tiles.tile_index(zoomlevel, x, y)
let tiledata = tiles.get(i)
if (tiledata === undefined) {
tiledata = []
tiles.set(i, tiledata)
}
tiledata.push(feature)
})
}
return tiles
}
/**
* Helper function which does the heavy lifting for 'inside'
*/
private static pointInPolygonCoordinates(
x: number,
y: number,
coordinates: [number, number][][]
): boolean {
const inside = GeoOperations.pointWithinRing(
x,
y,
/*This is the outer ring of the polygon */ coordinates[0]
)
if (!inside) {
return false
}
for (let i = 1; i < coordinates.length; i++) {
const inHole = GeoOperations.pointWithinRing(
x,
y,
coordinates[i] /* These are inner rings, aka holes*/
)
if (inHole) {
return false
}
}
return true
}
private static pointWithinRing(x: number, y: number, ring: [number, number][]) { private static pointWithinRing(x: number, y: number, ring: [number, number][]) {
let inside = false let inside = false
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {

View file

@ -2,12 +2,12 @@ import { OsmCreateAction } from "./OsmChangeAction"
import { Tag } from "../../Tags/Tag" import { Tag } from "../../Tags/Tag"
import { Changes } from "../Changes" import { Changes } from "../Changes"
import { ChangeDescription } from "./ChangeDescription" import { ChangeDescription } from "./ChangeDescription"
import FeaturePipelineState from "../../State/FeaturePipelineState"
import FeatureSource from "../../FeatureSource/FeatureSource"
import CreateNewWayAction from "./CreateNewWayAction" import CreateNewWayAction from "./CreateNewWayAction"
import CreateWayWithPointReuseAction, { MergePointConfig } from "./CreateWayWithPointReuseAction" import CreateWayWithPointReuseAction, { MergePointConfig } from "./CreateWayWithPointReuseAction"
import { And } from "../../Tags/And" import { And } from "../../Tags/And"
import { TagUtils } from "../../Tags/TagUtils" import { TagUtils } from "../../Tags/TagUtils"
import { SpecialVisualizationState } from "../../../UI/SpecialVisualization"
import FeatureSource from "../../FeatureSource/FeatureSource"
/** /**
* More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points * More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points
@ -26,14 +26,14 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
tags: Tag[], tags: Tag[],
outerRingCoordinates: [number, number][], outerRingCoordinates: [number, number][],
innerRingsCoordinates: [number, number][][], innerRingsCoordinates: [number, number][][],
state: FeaturePipelineState, state: SpecialVisualizationState,
config: MergePointConfig[], config: MergePointConfig[],
changeType: "import" | "create" | string changeType: "import" | "create" | string
) { ) {
super(null, true) super(null, true)
this._tags = [...tags, new Tag("type", "multipolygon")] this._tags = [...tags, new Tag("type", "multipolygon")]
this.changeType = changeType this.changeType = changeType
this.theme = state?.layoutToUse?.id ?? "" this.theme = state?.layout?.id ?? ""
this.createOuterWay = new CreateWayWithPointReuseAction( this.createOuterWay = new CreateWayWithPointReuseAction(
[], [],
outerRingCoordinates, outerRingCoordinates,
@ -45,7 +45,7 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
new CreateNewWayAction( new CreateNewWayAction(
[], [],
ringCoordinates.map(([lon, lat]) => ({ lat, lon })), ringCoordinates.map(([lon, lat]) => ({ lat, lon })),
{ theme: state?.layoutToUse?.id } { theme: state?.layout?.id }
) )
) )
@ -59,6 +59,10 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
} }
} }
public async getPreview(): Promise<FeatureSource> {
return undefined
}
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
console.log("Running CMPWPRA") console.log("Running CMPWPRA")
const descriptions: ChangeDescription[] = [] const descriptions: ChangeDescription[] = []

View file

@ -2,7 +2,6 @@ import { OsmCreateAction } from "./OsmChangeAction"
import { Tag } from "../../Tags/Tag" import { Tag } from "../../Tags/Tag"
import { Changes } from "../Changes" import { Changes } from "../Changes"
import { ChangeDescription } from "./ChangeDescription" import { ChangeDescription } from "./ChangeDescription"
import FeaturePipelineState from "../../State/FeaturePipelineState"
import { BBox } from "../../BBox" import { BBox } from "../../BBox"
import { TagsFilter } from "../../Tags/TagsFilter" import { TagsFilter } from "../../Tags/TagsFilter"
import { GeoOperations } from "../../GeoOperations" import { GeoOperations } from "../../GeoOperations"
@ -10,6 +9,7 @@ import FeatureSource from "../../FeatureSource/FeatureSource"
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"
import CreateNewNodeAction from "./CreateNewNodeAction" import CreateNewNodeAction from "./CreateNewNodeAction"
import CreateNewWayAction from "./CreateNewWayAction" import CreateNewWayAction from "./CreateNewWayAction"
import { SpecialVisualizationState } from "../../../UI/SpecialVisualization"
export interface MergePointConfig { export interface MergePointConfig {
withinRangeOfM: number withinRangeOfM: number
@ -62,14 +62,14 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
* lngLat-coordinates * lngLat-coordinates
* @private * @private
*/ */
private _coordinateInfo: CoordinateInfo[] private readonly _coordinateInfo: CoordinateInfo[]
private _state: FeaturePipelineState private readonly _state: SpecialVisualizationState
private _config: MergePointConfig[] private readonly _config: MergePointConfig[]
constructor( constructor(
tags: Tag[], tags: Tag[],
coordinates: [number, number][], coordinates: [number, number][],
state: FeaturePipelineState, state: SpecialVisualizationState,
config: MergePointConfig[] config: MergePointConfig[]
) { ) {
super(null, true) super(null, true)
@ -188,7 +188,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
} }
public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
const theme = this._state?.layoutToUse?.id const theme = this._state?.layout?.id
const allChanges: ChangeDescription[] = [] const allChanges: ChangeDescription[] = []
const nodeIdsToUse: { lat: number; lon: number; nodeId?: number }[] = [] const nodeIdsToUse: { lat: number; lon: number; nodeId?: number }[] = []
for (let i = 0; i < this._coordinateInfo.length; i++) { for (let i = 0; i < this._coordinateInfo.length; i++) {
@ -252,9 +252,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] { private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] {
const bbox = new BBox(coordinates) const bbox = new BBox(coordinates)
const state = this._state const state = this._state
const allNodes = [].concat( const allNodes =state.fullNodeDatabase?.getNodesWithin(bbox.pad(1.2))
...(state?.featurePipeline?.GetFeaturesWithin("type_node", bbox.pad(1.2)) ?? [])
)
const maxDistance = Math.max(...this._config.map((c) => c.withinRangeOfM)) const maxDistance = Math.max(...this._config.map((c) => c.withinRangeOfM))
// Init coordianteinfo with undefined but the same length as coordinates // Init coordianteinfo with undefined but the same length as coordinates

View file

@ -12,8 +12,8 @@ import { And } from "../../Tags/And"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import { OsmConnection } from "../OsmConnection" import { OsmConnection } from "../OsmConnection"
import { Feature } from "@turf/turf" import { Feature } from "@turf/turf"
import FeaturePipeline from "../../FeatureSource/FeaturePipeline" import { Geometry, LineString, Point } from "geojson"
import { Geometry, LineString, Point, Polygon } from "geojson" import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
export default class ReplaceGeometryAction extends OsmChangeAction { export default class ReplaceGeometryAction extends OsmChangeAction {
/** /**
@ -22,7 +22,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
private readonly feature: any private readonly feature: any
private readonly state: { private readonly state: {
osmConnection: OsmConnection osmConnection: OsmConnection
featurePipeline: FeaturePipeline fullNodeDatabase?: FullNodeDatabaseSource
} }
private readonly wayToReplaceId: string private readonly wayToReplaceId: string
private readonly theme: string private readonly theme: string
@ -41,7 +41,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
constructor( constructor(
state: { state: {
osmConnection: OsmConnection osmConnection: OsmConnection
featurePipeline: FeaturePipeline fullNodeDatabase?: FullNodeDatabaseSource
}, },
feature: any, feature: any,
wayToReplaceId: string, wayToReplaceId: string,
@ -195,7 +195,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
}> { }> {
// TODO FIXME: if a new point has to be created, snap to already existing ways // TODO FIXME: if a new point has to be created, snap to already existing ways
const nodeDb = this.state.featurePipeline.fullNodeDatabase const nodeDb = this.state.fullNodeDatabase
if (nodeDb === undefined) { if (nodeDb === undefined) {
throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)"
} }
@ -415,7 +415,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
} }
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
const nodeDb = this.state.featurePipeline.fullNodeDatabase const nodeDb = this.state.fullNodeDatabase
if (nodeDb === undefined) { if (nodeDb === undefined) {
throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)" throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)"
} }

View file

@ -5,6 +5,10 @@ export interface GeoCodeResult {
display_name: string display_name: string
lat: number lat: number
lon: number lon: number
/**
* Format:
* [lat, lat, lon, lon]
*/
boundingbox: number[] boundingbox: number[]
osm_type: "node" | "way" | "relation" osm_type: "node" | "way" | "relation"
osm_id: string osm_id: string

View file

@ -15,6 +15,13 @@ import { OsmTags } from "../Models/OsmFeature"
import { UIEventSource } from "./UIEventSource" import { UIEventSource } from "./UIEventSource"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
/**
* All elements that are needed to perform metatagging
*/
export interface MetataggingState {
layout: LayoutConfig
}
export abstract class SimpleMetaTagger { export abstract class SimpleMetaTagger {
public readonly keys: string[] public readonly keys: string[]
public readonly doc: string public readonly doc: string
@ -60,7 +67,7 @@ export abstract class SimpleMetaTagger {
feature: any, feature: any,
layer: LayerConfig, layer: LayerConfig,
tagsStore: UIEventSource<Record<string, string>>, tagsStore: UIEventSource<Record<string, string>>,
state: { layout: LayoutConfig } state: MetataggingState
): boolean ): boolean
} }
@ -119,7 +126,7 @@ export class CountryTagger extends SimpleMetaTagger {
}) })
} }
applyMetaTagsOnFeature(feature, _, state) { applyMetaTagsOnFeature(feature, _, tagsSource) {
let centerPoint: any = GeoOperations.centerpoint(feature) let centerPoint: any = GeoOperations.centerpoint(feature)
const runningTasks = this.runningTasks const runningTasks = this.runningTasks
const lat = centerPoint.geometry.coordinates[1] const lat = centerPoint.geometry.coordinates[1]
@ -128,28 +135,29 @@ export class CountryTagger extends SimpleMetaTagger {
CountryTagger.coder CountryTagger.coder
.GetCountryCodeAsync(lon, lat) .GetCountryCodeAsync(lon, lat)
.then((countries) => { .then((countries) => {
runningTasks.delete(feature)
try {
const oldCountry = feature.properties["_country"] const oldCountry = feature.properties["_country"]
feature.properties["_country"] = countries[0].trim().toLowerCase() const newCountry = countries[0].trim().toLowerCase()
if (oldCountry !== feature.properties["_country"]) { if (oldCountry !== newCountry) {
const tagsSource = state?.allElements?.getEventSourceById( tagsSource.data["_country"] = newCountry
feature.properties.id
)
tagsSource?.ping() tagsSource?.ping()
} }
} catch (e) { })
.catch((e) => {
console.warn(e) console.warn(e)
}
})
.catch((_) => {
runningTasks.delete(feature)
}) })
.finally(() => runningTasks.delete(feature))
return false return false
} }
} }
class InlineMetaTagger extends SimpleMetaTagger { class InlineMetaTagger extends SimpleMetaTagger {
public readonly applyMetaTagsOnFeature: (
feature: any,
layer: LayerConfig,
tagsStore: UIEventSource<OsmTags>,
state: MetataggingState
) => boolean
constructor( constructor(
docs: { docs: {
keys: string[] keys: string[]
@ -166,23 +174,17 @@ class InlineMetaTagger extends SimpleMetaTagger {
feature: any, feature: any,
layer: LayerConfig, layer: LayerConfig,
tagsStore: UIEventSource<OsmTags>, tagsStore: UIEventSource<OsmTags>,
state: { layout: LayoutConfig } state: MetataggingState
) => boolean ) => boolean
) { ) {
super(docs) super(docs)
this.applyMetaTagsOnFeature = f this.applyMetaTagsOnFeature = f
} }
public readonly applyMetaTagsOnFeature: (
feature: any,
layer: LayerConfig,
tagsStore: UIEventSource<OsmTags>,
state: { layout: LayoutConfig }
) => boolean
} }
export default class SimpleMetaTaggers {
public static readonly objectMetaInfo = new InlineMetaTagger( export class RewriteMetaInfoTags extends SimpleMetaTagger {
{ constructor() {
super({
keys: [ keys: [
"_last_edit:contributor", "_last_edit:contributor",
"_last_edit:contributor:uid", "_last_edit:contributor:uid",
@ -192,8 +194,10 @@ export default class SimpleMetaTaggers {
"_backend", "_backend",
], ],
doc: "Information about the last edit of this object.", doc: "Information about the last edit of this object.",
}, })
(feature) => { }
applyMetaTagsOnFeature(feature: Feature): boolean {
/*Note: also called by 'UpdateTagsFromOsmAPI'*/ /*Note: also called by 'UpdateTagsFromOsmAPI'*/
const tgs = feature.properties const tgs = feature.properties
@ -215,7 +219,12 @@ export default class SimpleMetaTaggers {
move("version", "_version_number") move("version", "_version_number")
return movedSomething return movedSomething
} }
) }
export default class SimpleMetaTaggers {
/**
* A simple metatagger which rewrites various metatags as needed
*/
public static readonly objectMetaInfo = new RewriteMetaInfoTags()
public static country = new CountryTagger() public static country = new CountryTagger()
public static geometryType = new InlineMetaTagger( public static geometryType = new InlineMetaTagger(
{ {

View file

@ -1,10 +1,5 @@
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import FeaturePipeline from "../FeatureSource/FeaturePipeline"
import { Tiles } from "../../Models/TileRange"
import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler" import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"
import Hash from "../Web/Hash" import Hash from "../Web/Hash"
import { BBox } from "../BBox"
import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource"
import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator" import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator"
export default class FeaturePipelineState { export default class FeaturePipelineState {
@ -14,101 +9,9 @@ export default class FeaturePipelineState {
public readonly featurePipeline: FeaturePipeline public readonly featurePipeline: FeaturePipeline
private readonly metatagRecalculator: MetaTagRecalculator private readonly metatagRecalculator: MetaTagRecalculator
constructor(layoutToUse: LayoutConfig) { constructor() {
const clustering = layoutToUse?.clustering
const clusterCounter = this.featureAggregator
const self = this
/**
* We are a bit in a bind:
* There is the featurePipeline, which creates some sources during construction
* THere is the metatagger, which needs to have these sources registered AND which takes a FeaturePipeline as argument
*
* This is a bit of a catch-22 (except that it isn't)
* The sources that are registered in the constructor are saved into 'registeredSources' temporary
*
*/
const sourcesToRegister = []
function registerRaw(source: FeatureSourceForLayer & Tiled) {
if (self.metatagRecalculator === undefined) {
sourcesToRegister.push(source)
} else {
self.metatagRecalculator.registerSource(source)
}
}
function registerSource(source: FeatureSourceForLayer & Tiled) {
clusterCounter.addTile(source)
const sourceBBox = source.features.map((allFeatures) =>
BBox.bboxAroundAll(allFeatures.map(BBox.get))
)
// Do show features indicates if the respective 'showDataLayer' should be shown. It can be hidden by e.g. clustering
source.features.map(
(f) => {
const z = self.locationControl.data.zoom
if (!source.layer.isDisplayed.data) {
return false
}
const bounds = self.currentBounds.data
if (bounds === undefined) {
// Map is not yet displayed
return false
}
if (!sourceBBox.data.overlapsWith(bounds)) {
// Not within range -> features are hidden
return false
}
if (z < source.layer.layerDef.minzoom) {
// Layer is always hidden for this zoom level
return false
}
if (z > clustering.maxZoom) {
return true
}
if (f.length > clustering.minNeededElements) {
// This tile alone already has too much features
return false
}
let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex)
if (tileZ >= z) {
while (tileZ > z) {
tileZ--
tileX = Math.floor(tileX / 2)
tileY = Math.floor(tileY / 2)
}
if (
clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY))
?.totalValue > clustering.minNeededElements
) {
// To much elements
return false
}
}
return true
},
[self.currentBounds, source.layer.isDisplayed, sourceBBox]
)
}
this.featurePipeline = new FeaturePipeline(registerSource, this, {
handleRawFeatureSource: registerRaw,
})
this.metatagRecalculator = new MetaTagRecalculator(this, this.featurePipeline) this.metatagRecalculator = new MetaTagRecalculator(this, this.featurePipeline)
this.metatagRecalculator.registerSource(this.currentView) this.metatagRecalculator.registerSource(this.currentView)
sourcesToRegister.forEach((source) => self.metatagRecalculator.registerSource(source))
new SelectedFeatureHandler(Hash.hash, this) new SelectedFeatureHandler(Hash.hash, this)
} }
} }

View file

@ -1,10 +1,8 @@
import { UIEventSource } from "../UIEventSource" import { UIEventSource } from "../UIEventSource"
import { GlobalFilter } from "../../Models/GlobalFilter" import { GlobalFilter } from "../../Models/GlobalFilter"
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" import FilteredLayer from "../../Models/FilteredLayer"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { OsmConnection } from "../Osm/OsmConnection" import { OsmConnection } from "../Osm/OsmConnection"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { QueryParameters } from "../Web/QueryParameters"
/** /**
* The layer state keeps track of: * The layer state keeps track of:
@ -36,83 +34,14 @@ export default class LayerState {
this.osmConnection = osmConnection this.osmConnection = osmConnection
this.filteredLayers = new Map() this.filteredLayers = new Map()
for (const layer of layers) { for (const layer of layers) {
this.filteredLayers.set(layer.id, this.initFilteredLayer(layer, context)) this.filteredLayers.set(
layer.id,
FilteredLayer.initLinkedState(layer, context, this.osmConnection)
)
} }
layers.forEach((l) => this.linkFilterStates(l)) layers.forEach((l) => this.linkFilterStates(l))
} }
private static getPref(
osmConnection: OsmConnection,
key: string,
layer: LayerConfig
): UIEventSource<boolean> {
return osmConnection.GetPreference(key, layer.shownByDefault + "").sync(
(v) => {
if (v === undefined) {
return undefined
}
return v === "true"
},
[],
(b) => {
if (b === undefined) {
return undefined
}
return "" + b
}
)
}
/**
* INitializes a filtered layer for the given layer.
* @param layer
* @param context: probably the theme-name. This is used to disambiguate the user settings; e.g. when using the same layer in different contexts
* @private
*/
private initFilteredLayer(layer: LayerConfig, context: string): FilteredLayer | undefined {
let isDisplayed: UIEventSource<boolean>
const osmConnection = this.osmConnection
if (layer.syncSelection === "local") {
isDisplayed = LocalStorageSource.GetParsed(
context + "-layer-" + layer.id + "-enabled",
layer.shownByDefault
)
} else if (layer.syncSelection === "theme-only") {
isDisplayed = LayerState.getPref(
osmConnection,
context + "-layer-" + layer.id + "-enabled",
layer
)
} else if (layer.syncSelection === "global") {
isDisplayed = LayerState.getPref(osmConnection, "layer-" + layer.id + "-enabled", layer)
} else {
isDisplayed = QueryParameters.GetBooleanQueryParameter(
"layer-" + layer.id,
layer.shownByDefault,
"Wether or not layer " + layer.id + " is shown"
)
}
const flayer: FilteredLayer = {
isDisplayed,
layerDef: layer,
appliedFilters: new UIEventSource<Map<string, FilterState>>(
new Map<string, FilterState>()
),
}
layer.filters?.forEach((filterConfig) => {
const stateSrc = filterConfig.initState()
stateSrc.addCallbackAndRun((state) =>
flayer.appliedFilters.data.set(filterConfig.id, state)
)
flayer.appliedFilters
.map((dict) => dict.get(filterConfig.id))
.addCallback((state) => stateSrc.setData(state))
})
return flayer
}
/** /**
* Some layers copy the filter state of another layer - this is quite often the case for 'sibling'-layers, * Some layers copy the filter state of another layer - this is quite often the case for 'sibling'-layers,
* (where two variations of the same layer are used, e.g. a specific type of shop on all zoom levels and all shops on high zoom). * (where two variations of the same layer are used, e.g. a specific type of shop on all zoom levels and all shops on high zoom).
@ -136,10 +65,6 @@ export default class LayerState {
console.warn( console.warn(
"Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs "Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs
) )
this.filteredLayers.set(layer.id, { this.filteredLayers.set(layer.id, toReuse)
isDisplayed: toReuse.isDisplayed,
layerDef: layer,
appliedFilters: toReuse.appliedFilters,
})
} }
} }

View file

@ -17,14 +17,10 @@ export default class UserRelatedState {
The user credentials The user credentials
*/ */
public osmConnection: OsmConnection public osmConnection: OsmConnection
/**
THe change handler
*/
public changes: Changes
/** /**
* The key for mangrove * The key for mangrove
*/ */
public mangroveIdentity: MangroveIdentity public readonly mangroveIdentity: MangroveIdentity
public readonly installedUserThemes: Store<string[]> public readonly installedUserThemes: Store<string[]>

View file

@ -63,27 +63,10 @@ export class Stores {
stable.setData(undefined) stable.setData(undefined)
return return
} }
const oldList = stable.data if (Utils.sameList(stable.data, list)) {
if (oldList === list) {
return return
} }
if (oldList == list) {
return
}
if (oldList === undefined || oldList.length !== list.length) {
stable.setData(list) stable.setData(list)
return
}
for (let i = 0; i < list.length; i++) {
if (oldList[i] !== list[i]) {
stable.setData(list)
return
}
}
// No actual changes, so we don't do anything
return
}) })
return stable return stable
} }
@ -93,7 +76,7 @@ export abstract class Store<T> implements Readable<T> {
abstract readonly data: T abstract readonly data: T
/** /**
* OPtional value giving a title to the UIEventSource, mainly used for debugging * Optional value giving a title to the UIEventSource, mainly used for debugging
*/ */
public readonly tag: string | undefined public readonly tag: string | undefined
@ -794,4 +777,14 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
update(f: Updater<T> & ((value: T) => T)): void { update(f: Updater<T> & ((value: T) => T)): void {
this.setData(f(this.data)) this.setData(f(this.data))
} }
/**
* Create a new UIEVentSource. Whenever 'source' changes, the returned UIEventSource will get this value as well.
* However, this value can be overriden without affecting source
*/
static feedFrom<T>(store: Store<T>): UIEventSource<T> {
const src = new UIEventSource(store.data)
store.addCallback((t) => src.setData(t))
return src
}
} }

View file

@ -1,10 +1,8 @@
import { ImmutableStore, Store, UIEventSource } from "../UIEventSource" import { ImmutableStore, Store, UIEventSource } from "../UIEventSource"
import { MangroveReviews, Review } from "mangrove-reviews-typescript" import { MangroveReviews, Review } from "mangrove-reviews-typescript"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { Feature, Geometry, Position } from "geojson" import { Feature, Position } from "geojson"
import { GeoOperations } from "../GeoOperations" import { GeoOperations } from "../GeoOperations"
import { OsmTags } from "../../Models/OsmFeature"
import { ElementStorage } from "../ElementStorage"
export class MangroveIdentity { export class MangroveIdentity {
public readonly keypair: Store<CryptoKeyPair> public readonly keypair: Store<CryptoKeyPair>
@ -67,11 +65,9 @@ export default class FeatureReviews {
private readonly _identity: MangroveIdentity private readonly _identity: MangroveIdentity
private constructor( private constructor(
feature: Feature<Geometry, OsmTags>, feature: Feature,
state: { tagsSource: UIEventSource<Record<string, string>>,
allElements: ElementStorage mangroveIdentity?: MangroveIdentity,
mangroveIdentity?: MangroveIdentity
},
options?: { options?: {
nameKey?: "name" | string nameKey?: "name" | string
fallbackName?: string fallbackName?: string
@ -80,8 +76,7 @@ export default class FeatureReviews {
) { ) {
const centerLonLat = GeoOperations.centerpointCoordinates(feature) const centerLonLat = GeoOperations.centerpointCoordinates(feature)
;[this._lon, this._lat] = centerLonLat ;[this._lon, this._lat] = centerLonLat
this._identity = this._identity = mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined))
state?.mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined))
const nameKey = options?.nameKey ?? "name" const nameKey = options?.nameKey ?? "name"
if (feature.geometry.type === "Point") { if (feature.geometry.type === "Point") {
@ -108,9 +103,7 @@ export default class FeatureReviews {
this._uncertainty = options?.uncertaintyRadius ?? maxDistance this._uncertainty = options?.uncertaintyRadius ?? maxDistance
} }
this._name = state.allElements this._name = tagsSource .map((tags) => tags[nameKey] ?? options?.fallbackName)
.getEventSourceById(feature.properties.id)
.map((tags) => tags[nameKey] ?? options?.fallbackName)
this.subjectUri = this.ConstructSubjectUri() this.subjectUri = this.ConstructSubjectUri()
@ -136,11 +129,9 @@ export default class FeatureReviews {
* Construct a featureReviewsFor or fetches it from the cache * Construct a featureReviewsFor or fetches it from the cache
*/ */
public static construct( public static construct(
feature: Feature<Geometry, OsmTags>, feature: Feature,
state: { tagsSource: UIEventSource<Record<string, string>>,
allElements: ElementStorage mangroveIdentity?: MangroveIdentity,
mangroveIdentity?: MangroveIdentity
},
options?: { options?: {
nameKey?: "name" | string nameKey?: "name" | string
fallbackName?: string fallbackName?: string
@ -152,7 +143,7 @@ export default class FeatureReviews {
if (cached !== undefined) { if (cached !== undefined) {
return cached return cached
} }
const featureReviews = new FeatureReviews(feature, state, options) const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options)
FeatureReviews._featureReviewsCache[key] = featureReviews FeatureReviews._featureReviewsCache[key] = featureReviews
return featureReviews return featureReviews
} }

View file

@ -1,14 +1,90 @@
import { UIEventSource } from "../Logic/UIEventSource" import { UIEventSource } from "../Logic/UIEventSource"
import LayerConfig from "./ThemeConfig/LayerConfig" import LayerConfig from "./ThemeConfig/LayerConfig"
import { TagsFilter } from "../Logic/Tags/TagsFilter" import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
import { QueryParameters } from "../Logic/Web/QueryParameters"
export interface FilterState { export default class FilteredLayer {
currentFilter: TagsFilter /**
state: string | number * Wether or not the specified layer is shown
} */
export default interface FilteredLayer {
readonly isDisplayed: UIEventSource<boolean> readonly isDisplayed: UIEventSource<boolean>
readonly appliedFilters: UIEventSource<Map<string, FilterState>> /**
* Maps the filter.option.id onto the actual used state
*/
readonly appliedFilters: Map<string, UIEventSource<undefined | number | string>>
readonly layerDef: LayerConfig readonly layerDef: LayerConfig
constructor(
layer: LayerConfig,
appliedFilters?: Map<string, UIEventSource<undefined | number | string>>,
isDisplayed?: UIEventSource<boolean>
) {
this.layerDef = layer
this.isDisplayed = isDisplayed ?? new UIEventSource(true)
this.appliedFilters =
appliedFilters ?? new Map<string, UIEventSource<number | string | undefined>>()
}
/**
* Creates a FilteredLayer which is tied into the QueryParameters and/or user preferences
*/
public static initLinkedState(
layer: LayerConfig,
context: string,
osmConnection: OsmConnection
) {
let isDisplayed: UIEventSource<boolean>
if (layer.syncSelection === "local") {
isDisplayed = LocalStorageSource.GetParsed(
context + "-layer-" + layer.id + "-enabled",
layer.shownByDefault
)
} else if (layer.syncSelection === "theme-only") {
isDisplayed = FilteredLayer.getPref(
osmConnection,
context + "-layer-" + layer.id + "-enabled",
layer
)
} else if (layer.syncSelection === "global") {
isDisplayed = FilteredLayer.getPref(
osmConnection,
"layer-" + layer.id + "-enabled",
layer
)
} else {
isDisplayed = QueryParameters.GetBooleanQueryParameter(
"layer-" + layer.id,
layer.shownByDefault,
"Whether or not layer " + layer.id + " is shown"
)
}
const appliedFilters = new Map<string, UIEventSource<undefined | number | string>>()
for (const subfilter of layer.filters) {
appliedFilters.set(subfilter.id, subfilter.initState())
}
return new FilteredLayer(layer, appliedFilters, isDisplayed)
}
private static getPref(
osmConnection: OsmConnection,
key: string,
layer: LayerConfig
): UIEventSource<boolean> {
return osmConnection.GetPreference(key, layer.shownByDefault + "").sync(
(v) => {
if (v === undefined) {
return undefined
}
return v === "true"
},
[],
(b) => {
if (b === undefined) {
return undefined
}
return "" + b
}
)
}
} }

View file

@ -1,9 +1,10 @@
import { Translation, TypedTranslation } from "../UI/i18n/Translation" import { Translation, TypedTranslation } from "../UI/i18n/Translation"
import { FilterState } from "./FilteredLayer"
import { Tag } from "../Logic/Tags/Tag" import { Tag } from "../Logic/Tags/Tag"
import { TagsFilter } from "../Logic/Tags/TagsFilter"
export interface GlobalFilter { export interface GlobalFilter {
filter: FilterState osmTags: TagsFilter
state: number | string | undefined
id: string id: string
onNewPoint: { onNewPoint: {
safetyCheck: Translation safetyCheck: Translation

View file

@ -5,8 +5,10 @@ import { RasterLayerPolygon } from "./RasterLayers"
export interface MapProperties { export interface MapProperties {
readonly location: UIEventSource<{ lon: number; lat: number }> readonly location: UIEventSource<{ lon: number; lat: number }>
readonly zoom: UIEventSource<number> readonly zoom: UIEventSource<number>
readonly bounds: Store<BBox> readonly bounds: UIEventSource<BBox>
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined> readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
readonly maxbounds: UIEventSource<undefined | BBox> readonly maxbounds: UIEventSource<undefined | BBox>
readonly allowMoving: UIEventSource<true | boolean> readonly allowMoving: UIEventSource<true | boolean>
readonly allowZooming: UIEventSource<true | boolean>
} }

View file

@ -36,6 +36,14 @@ export class AvailableRasterLayers {
geometry: BBox.global.asGeometry(), geometry: BBox.global.asGeometry(),
} }
public static readonly maplibre: RasterLayerPolygon = {
type: "Feature",
properties: <any>{
name: "MapLibre",
url: null,
},
geometry: BBox.global.asGeometry(),
}
public static layersAvailableAt( public static layersAvailableAt(
location: Store<{ lon: number; lat: number }> location: Store<{ lon: number; lat: number }>
): Store<RasterLayerPolygon[]> { ): Store<RasterLayerPolygon[]> {
@ -58,6 +66,7 @@ export class AvailableRasterLayers {
return GeoOperations.inside(lonlat, eliPolygon) return GeoOperations.inside(lonlat, eliPolygon)
}) })
matching.unshift(AvailableRasterLayers.osmCarto) matching.unshift(AvailableRasterLayers.osmCarto)
matching.unshift(AvailableRasterLayers.maplibre)
matching.push(...AvailableRasterLayers.globalLayers) matching.push(...AvailableRasterLayers.globalLayers)
return matching return matching
}) })

View file

@ -5,7 +5,6 @@ import Translations from "../../UI/i18n/Translations"
import { TagUtils } from "../../Logic/Tags/TagUtils" import { TagUtils } from "../../Logic/Tags/TagUtils"
import { TagConfigJson } from "./Json/TagConfigJson" import { TagConfigJson } from "./Json/TagConfigJson"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import { FilterState } from "../FilteredLayer"
import { QueryParameters } from "../../Logic/Web/QueryParameters" import { QueryParameters } from "../../Logic/Web/QueryParameters"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { RegexTag } from "../../Logic/Tags/RegexTag" import { RegexTag } from "../../Logic/Tags/RegexTag"
@ -144,14 +143,7 @@ export default class FilterConfig {
}) })
} }
public initState(): UIEventSource<FilterState> { public initState(): UIEventSource<undefined | number | string> {
function reset(state: FilterState): string {
if (state === undefined) {
return ""
}
return "" + state.state
}
let defaultValue = "" let defaultValue = ""
if (this.options.length > 1) { if (this.options.length > 1) {
defaultValue = "" + (this.defaultSelection ?? 0) defaultValue = "" + (this.defaultSelection ?? 0)
@ -159,6 +151,8 @@ export default class FilterConfig {
// Only a single option // Only a single option
if (this.defaultSelection === 0) { if (this.defaultSelection === 0) {
defaultValue = "true" defaultValue = "true"
} else {
defaultValue = "false"
} }
} }
const qp = QueryParameters.GetQueryParameter( const qp = QueryParameters.GetQueryParameter(
@ -168,12 +162,6 @@ export default class FilterConfig {
) )
if (this.options.length > 1) { if (this.options.length > 1) {
// This is a multi-option filter; state should be a number which selects the correct entry
const possibleStates: FilterState[] = this.options.map((opt, i) => ({
currentFilter: opt.osmTags,
state: i,
}))
// We map the query parameter for this case // We map the query parameter for this case
return qp.sync( return qp.sync(
(str) => { (str) => {
@ -182,62 +170,29 @@ export default class FilterConfig {
// Nope, not a correct number! // Nope, not a correct number!
return undefined return undefined
} }
return possibleStates[parsed] return parsed
}, },
[], [],
reset (n) => "" + n
) )
} }
const option = this.options[0] const option = this.options[0]
if (option.fields.length > 0) { if (option.fields.length > 0) {
return qp.sync( return qp
(str) => {
// There are variables in play!
// str should encode a json-hash
try {
const props = JSON.parse(str)
const origTags = option.originalTagsSpec
const rewrittenTags = Utils.WalkJson(origTags, (v) => {
if (typeof v !== "string") {
return v
}
for (const key in props) {
v = (<string>v).replace("{" + key + "}", props[key])
}
return v
})
const parsed = TagUtils.Tag(rewrittenTags)
return <FilterState>{
currentFilter: parsed,
state: str,
}
} catch (e) {
return undefined
}
},
[],
reset
)
} }
// The last case is pretty boring: it is checked or it isn't
const filterState: FilterState = {
currentFilter: option.osmTags,
state: "true",
}
return qp.sync( return qp.sync(
(str) => { (str) => {
// Only a single option exists here // Only a single option exists here
if (str === "true") { if (str === "true") {
return filterState return 0
} }
return undefined return undefined
}, },
[], [],
reset (n) => (n === undefined ? "false" : "true")
) )
} }

View file

@ -205,25 +205,6 @@ export interface LayoutConfigJson {
} }
)[] )[]
/**
* If defined, data will be clustered.
* Defaults to {maxZoom: 16, minNeeded: 500}
*/
clustering?:
| {
/**
* All zoom levels above 'maxzoom' are not clustered anymore.
* Defaults to 18
*/
maxZoom?: number
/**
* The number of elements per tile needed to start clustering
* If clustering is defined, defaults to 250
*/
minNeededElements?: number
}
| false
/** /**
* The URL of a custom CSS stylesheet to modify the layout * The URL of a custom CSS stylesheet to modify the layout
*/ */

View file

@ -40,10 +40,6 @@ export default class LayoutConfig implements LayoutInformation {
public defaultBackgroundId?: string public defaultBackgroundId?: string
public layers: LayerConfig[] public layers: LayerConfig[]
public tileLayerSources: TilesourceConfig[] public tileLayerSources: TilesourceConfig[]
public readonly clustering?: {
maxZoom: number
minNeededElements: number
}
public readonly hideFromOverview: boolean public readonly hideFromOverview: boolean
public lockLocation: boolean | [[number, number], [number, number]] public lockLocation: boolean | [[number, number], [number, number]]
public readonly enableUserBadge: boolean public readonly enableUserBadge: boolean
@ -188,22 +184,6 @@ export default class LayoutConfig implements LayoutInformation {
context + ".extraLink" context + ".extraLink"
) )
this.clustering = {
maxZoom: 16,
minNeededElements: 250,
}
if (json.clustering === false) {
this.clustering = {
maxZoom: 0,
minNeededElements: 100000,
}
} else if (json.clustering) {
this.clustering = {
maxZoom: json.clustering.maxZoom ?? 18,
minNeededElements: json.clustering.minNeededElements ?? 250,
}
}
this.hideFromOverview = json.hideFromOverview ?? false this.hideFromOverview = json.hideFromOverview ?? false
this.lockLocation = <[[number, number], [number, number]]>json.lockLocation ?? undefined this.lockLocation = <[[number, number], [number, number]]>json.lockLocation ?? undefined
this.enableUserBadge = json.enableUserBadge ?? true this.enableUserBadge = json.enableUserBadge ?? true

View file

@ -11,8 +11,6 @@ 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"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
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>([
@ -176,7 +174,7 @@ export default class PointRenderingConfig extends WithContextLoader {
return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin) return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin)
} }
public GetSimpleIcon(tags: Store<OsmTags>): BaseUIElement { public GetSimpleIcon(tags: Store<Record<string, string>>): BaseUIElement {
const self = this const self = this
if (this.icon === undefined) { if (this.icon === undefined) {
return undefined return undefined
@ -187,7 +185,7 @@ export default class PointRenderingConfig extends WithContextLoader {
} }
public RenderIcon( public RenderIcon(
tags: Store<OsmTags>, tags: Store<Record<string, string>>,
clickable: boolean, clickable: boolean,
options?: { options?: {
noSize?: false | boolean noSize?: false | boolean
@ -277,7 +275,7 @@ export default class PointRenderingConfig extends WithContextLoader {
} }
} }
private GetBadges(tags: Store<OsmTags>): BaseUIElement { private GetBadges(tags: Store<Record<string, string>>): BaseUIElement {
if (this.iconBadges.length === 0) { if (this.iconBadges.length === 0) {
return undefined return undefined
} }
@ -309,7 +307,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: Store<OsmTags>): BaseUIElement { private GetLabel(tags: Store<Record<string, string>>): BaseUIElement {
if (this.label === undefined) { if (this.label === undefined) {
return undefined return undefined
} }

278
Models/ThemeViewState.ts Normal file
View file

@ -0,0 +1,278 @@
import LayoutConfig from "./ThemeConfig/LayoutConfig"
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
import { Changes } from "../Logic/Osm/Changes"
import { Store, UIEventSource } from "../Logic/UIEventSource"
import FeatureSource, {
IndexedFeatureSource,
WritableFeatureSource,
} from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { DefaultGuiState } from "../UI/DefaultGuiState"
import { MapProperties } from "./MapProperties"
import LayerState from "../Logic/State/LayerState"
import { Feature } from "geojson"
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import { Map as MlMap } from "maplibre-gl"
import InitialMapPositioning from "../Logic/Actors/InitialMapPositioning"
import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor"
import { GeoLocationState } from "../Logic/State/GeoLocationState"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import { QueryParameters } from "../Logic/Web/QueryParameters"
import UserRelatedState from "../Logic/State/UserRelatedState"
import LayerConfig from "./ThemeConfig/LayerConfig"
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
import { AvailableRasterLayers, RasterLayerPolygon } from "./RasterLayers"
import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"
import ShowDataLayer from "../UI/Map/ShowDataLayer"
import TitleHandler from "../Logic/Actors/TitleHandler"
import ChangeToElementsActor from "../Logic/Actors/ChangeToElementsActor"
import PendingChangesUploader from "../Logic/Actors/PendingChangesUploader"
import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater"
import { BBox } from "../Logic/BBox"
import Constants from "./Constants"
import Hotkeys from "../UI/Base/Hotkeys"
import Translations from "../UI/i18n/Translations"
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
/**
*
* The themeviewState contains all the state needed for the themeViewGUI.
*
* This is pretty much the 'brain' or the HQ of MapComplete
*
* It ties up all the needed elements and starts some actors.
*/
export default class ThemeViewState implements SpecialVisualizationState {
readonly layout: LayoutConfig
readonly map: UIEventSource<MlMap>
readonly changes: Changes
readonly featureSwitches: FeatureSwitchState
readonly featureSwitchIsTesting: Store<boolean>
readonly featureSwitchUserbadge: Store<boolean>
readonly featureProperties: FeaturePropertiesStore
readonly osmConnection: OsmConnection
readonly selectedElement: UIEventSource<Feature>
readonly mapProperties: MapProperties
readonly dataIsLoading: Store<boolean> // TODO
readonly guistate: DefaultGuiState
readonly fullNodeDatabase?: FullNodeDatabaseSource // TODO
readonly historicalUserLocations: WritableFeatureSource
readonly indexedFeatures: IndexedFeatureSource
readonly layerState: LayerState
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
readonly availableLayers: Store<RasterLayerPolygon[]>
readonly selectedLayer: UIEventSource<LayerConfig>
readonly userRelatedState: UserRelatedState
readonly geolocation: GeoLocationHandler
constructor(layout: LayoutConfig) {
this.layout = layout
this.guistate = new DefaultGuiState()
this.map = new UIEventSource<MlMap>(undefined)
const initial = new InitialMapPositioning(layout)
this.mapProperties = new MapLibreAdaptor(this.map, initial)
const geolocationState = new GeoLocationState()
this.featureSwitches = new FeatureSwitchState(layout)
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchUserbadge
this.osmConnection = new OsmConnection({
dryRun: this.featureSwitches.featureSwitchIsTesting,
fakeUser: this.featureSwitches.featureSwitchFakeUser.data,
oauth_token: QueryParameters.GetQueryParameter(
"oauth_token",
undefined,
"Used to complete the login"
),
osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data,
})
this.userRelatedState = new UserRelatedState(this.osmConnection, layout?.language)
this.selectedElement = new UIEventSource<Feature | undefined>(undefined, "Selected element")
this.selectedLayer = new UIEventSource<LayerConfig>(undefined, "Selected layer")
this.geolocation = new GeoLocationHandler(
geolocationState,
this.selectedElement,
this.mapProperties,
this.userRelatedState.gpsLocationHistoryRetentionTime
)
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location)
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
const indexedElements = new LayoutSource(
layout.layers,
this.featureSwitches,
new StaticFeatureSource([]),
this.mapProperties,
this.osmConnection.Backend(),
(id) => this.layerState.filteredLayers.get(id).isDisplayed
)
this.featureProperties = new FeaturePropertiesStore(indexedElements)
const perLayer = new PerLayerFeatureSourceSplitter(
Array.from(this.layerState.filteredLayers.values()),
indexedElements,
{
constructStore: (features, layer) => new GeoIndexedStoreForLayer(features, layer),
}
)
this.perLayer = perLayer.perLayer
this.perLayer.forEach((fs) => {
new SaveFeatureSourceToLocalStorage(fs.layer.layerDef.id, 15, fs)
const filtered = new FilteringFeatureSource(
fs.layer,
fs,
(id) => this.featureProperties.getStore(id),
this.layerState.globalFilters
)
const doShowLayer = this.mapProperties.zoom.map(
(z) =>
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
[fs.layer.isDisplayed]
)
doShowLayer.addCallbackAndRunD((doShow) =>
console.log(
"Layer",
fs.layer.layerDef.id,
"is",
doShow,
this.mapProperties.zoom.data,
fs.layer.layerDef.minzoom
)
)
new ShowDataLayer(this.map, {
layer: fs.layer.layerDef,
features: filtered,
doShowLayer,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
fetchStore: (id) => this.featureProperties.getStore(id),
})
})
this.changes = new Changes(
{
dryRun: this.featureSwitches.featureSwitchIsTesting,
allElements: indexedElements,
featurePropertiesStore: this.featureProperties,
osmConnection: this.osmConnection,
historicalUserLocations: this.geolocation.historicalUserLocations,
},
layout?.isLeftRightSensitive() ?? false
)
this.initActors()
this.drawSpecialLayers()
this.initHotkeys()
this.miscSetup()
}
/**
* Various small methods that need to be called
*/
private miscSetup() {
this.userRelatedState.markLayoutAsVisited(this.layout)
}
private initHotkeys() {
Hotkeys.RegisterHotkey(
{ nomod: "Escape", onUp: true },
Translations.t.hotkeyDocumentation.closeSidebar,
() => {
this.selectedElement.setData(undefined)
this.guistate.closeAll()
}
)
}
/**
* Add the special layers to the map
* @private
*/
private drawSpecialLayers() {
type AddedByDefaultTypes = typeof Constants.added_by_default[number]
/**
* A listing which maps the layerId onto the featureSource
*/
const empty = []
const specialLayers: Record<AddedByDefaultTypes | "current_view", FeatureSource> = {
home_location: this.userRelatedState.homeLocation,
gps_location: this.geolocation.currentUserLocation,
gps_location_history: this.geolocation.historicalUserLocations,
gps_track: this.geolocation.historicalUserLocationsTrack,
selected_element: new StaticFeatureSource(
this.selectedElement.map((f) => (f === undefined ? empty : [f]))
),
range: new StaticFeatureSource(
this.mapProperties.maxbounds.map((bbox) =>
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })]
)
),
current_view: new StaticFeatureSource(
this.mapProperties.bounds.map((bbox) =>
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "current_view" })]
)
),
}
if (this.layout?.lockLocation) {
const bbox = new BBox(this.layout.lockLocation)
this.mapProperties.maxbounds.setData(bbox)
ShowDataLayer.showRange(
this.map,
new StaticFeatureSource([bbox.asGeoJson({})]),
this.featureSwitches.featureSwitchIsTesting
)
}
this.layerState.filteredLayers
.get("range")
?.isDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true)
this.layerState.filteredLayers.forEach((flayer) => {
const features = specialLayers[flayer.layerDef.id]
if (features === undefined) {
return
}
new ShowDataLayer(this.map, {
features,
doShowLayer: flayer.isDisplayed,
layer: flayer.layerDef,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,
})
})
}
/**
* Setup various services for which no reference are needed
* @private
*/
private initActors() {
// Various actors that we don't need to reference
new TitleHandler(
this.selectedElement,
this.selectedLayer,
this.featureProperties,
this.layout
)
new ChangeToElementsActor(this.changes, this.featureProperties)
new PendingChangesUploader(this.changes, this.selectedElement)
new SelectedElementTagsUpdater({
allElements: this.featureProperties,
changes: this.changes,
selectedElement: this.selectedElement,
layoutToUse: this.layout,
osmConnection: this.osmConnection,
})
}
}

View file

@ -1,16 +0,0 @@
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"
import FeaturePipelineState from "./Logic/State/FeaturePipelineState"
/**
* Contains the global state: a bunch of UI-event sources
*/
export default class State extends FeaturePipelineState {
/* The singleton of the global state
*/
public static state: FeaturePipelineState
constructor(layoutToUse: LayoutConfig) {
super(layoutToUse)
}
}

13
UI/Base/Checkbox.svelte Normal file
View file

@ -0,0 +1,13 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource.js";
/**
* For some stupid reason, it is very hard to bind inputs
*/
export let selected: UIEventSource<boolean>;
let _c: boolean = selected.data ?? true;
$: selected.setData(_c)
</script>
<input type="checkbox" bind:checked={_c} />

15
UI/Base/Dropdown.svelte Normal file
View file

@ -0,0 +1,15 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource.js";
/**
* For some stupid reason, it is very hard to bind inputs
*/
export let value: UIEventSource<number>;
let i: number = value.data;
$: value.setData(i)
</script>
<select bind:value={i} >
<slot></slot>
</select>

View file

@ -1,14 +1,23 @@
<script lang="ts"> <script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"; import { UIEventSource } from "../../Logic/UIEventSource";
import { onDestroy } from "svelte";
/** /**
* For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here * For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here
*/ */
export let condition: UIEventSource<boolean>; export let condition: UIEventSource<boolean>;
let _c = condition.data; let _c = condition.data;
condition.addCallback(c => _c = c) onDestroy(condition.addCallback(c => {
/* Do _not_ abbreviate this as `.addCallback(c => _c = c)`. This is the same as writing `.addCallback(c => {return _c = c})`,
which will _unregister_ the callback if `c = true`! */
_c = c;
return false
}))
</script> </script>
{#if _c} {#if _c}
<slot></slot> <slot></slot>
{:else}
<slot name="else"></slot>
{/if} {/if}

18
UI/Base/IfNot.svelte Normal file
View file

@ -0,0 +1,18 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource";
import { onDestroy } from "svelte";
/**
* For some stupid reason, it is very hard to let {#if} work together with UIEventSources, so we wrap then here
*/
export let condition: UIEventSource<boolean>;
let _c = !condition.data;
onDestroy(condition.addCallback(c => {
_c = !c;
return false
}))
</script>
{#if _c}
<slot></slot>
{/if}

13
UI/Base/Loading.svelte Normal file
View file

@ -0,0 +1,13 @@
<script>
import ToSvelte from "./ToSvelte.svelte";
import Svg from "../../Svg";
</script>
<div class="pl-2 p-1 flex">
<div class="animate-spin self-center w-6 h-6 min-w-6">
<ToSvelte construct={Svg.loading_ui}></ToSvelte>
</div>
<div class="ml-2">
<slot></slot>
</div>
</div>

View file

@ -8,6 +8,6 @@
</script> </script>
<div on:click={e => dispatch("click", e)} class="subtle-background block rounded-full min-w-10 h-10 pointer-events-auto m-0.5 md:m-1 p-1"> <div on:click={e => dispatch("click", e)} class="subtle-background rounded-full min-w-10 w-fit h-10 m-0.5 md:m-1 p-1">
<slot class="m-4"></slot> <slot class="m-4"></slot>
</div> </div>

View file

@ -1,18 +1,23 @@
<script lang="ts"> <script lang="ts">
import BaseUIElement from "../BaseUIElement.js" import BaseUIElement from "../BaseUIElement.js";
import { onMount } from "svelte" import { onDestroy, onMount } from "svelte";
export let construct: BaseUIElement | (() => BaseUIElement)
let elem: HTMLElement
export let construct: BaseUIElement | (() => BaseUIElement);
let elem: HTMLElement;
let html: HTMLElement;
onMount(() => { onMount(() => {
let html = const uiElem = typeof construct === "function"
typeof construct === "function" ? construct() : construct;
? construct().ConstructElement() html =uiElem?.ConstructElement();
: construct.ConstructElement() if (html !== undefined) {
elem.replaceWith(html);
}
});
onDestroy(() => {
html?.remove();
});
elem.replaceWith(html)
})
</script> </script>
<span bind:this={elem} /> <span bind:this={elem} />

View file

@ -13,6 +13,7 @@ import { OpenIdEditor, OpenJosm } from "./CopyrightPanel"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import ScrollableFullScreen from "../Base/ScrollableFullScreen" import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import { DefaultGuiState } from "../DefaultGuiState" import { DefaultGuiState } from "../DefaultGuiState"
import DefaultGUI from "../DefaultGUI"
export class BackToThemeOverview extends Toggle { export class BackToThemeOverview extends Toggle {
constructor( constructor(
@ -42,6 +43,7 @@ export class ActionButtons extends Combine {
readonly locationControl: Store<Loc> readonly locationControl: Store<Loc>
readonly osmConnection: OsmConnection readonly osmConnection: OsmConnection
readonly featureSwitchMoreQuests: Store<boolean> readonly featureSwitchMoreQuests: Store<boolean>
readonly defaultGuiState: DefaultGuiState
}) { }) {
const imgSize = "h-6 w-6" const imgSize = "h-6 w-6"
const iconStyle = "height: 1.5rem; width: 1.5rem" const iconStyle = "height: 1.5rem; width: 1.5rem"
@ -82,8 +84,8 @@ export class ActionButtons extends Combine {
Translations.t.translations.activateButton Translations.t.translations.activateButton
).onClick(() => { ).onClick(() => {
ScrollableFullScreen.collapse() ScrollableFullScreen.collapse()
DefaultGuiState.state.userInfoIsOpened.setData(true) state.defaultGuiState.userInfoIsOpened.setData(true)
DefaultGuiState.state.userInfoFocusedQuestion.setData("translation-mode") state.defaultGuiState.userInfoFocusedQuestion.setData("translation-mode")
}), }),
]) ])
this.SetClass("block w-full link-no-underline") this.SetClass("block w-full link-no-underline")

View file

@ -14,25 +14,25 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import Title from "../Base/Title" import Title from "../Base/Title"
import { SubtleButton } from "../Base/SubtleButton" import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg" import Svg from "../../Svg"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import { BBox } from "../../Logic/BBox" import { BBox } from "../../Logic/BBox"
import Loc from "../../Models/Loc"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import ContributorCount from "../../Logic/ContributorCount" import ContributorCount from "../../Logic/ContributorCount"
import Img from "../Base/Img" import Img from "../Base/Img"
import { TypedTranslation } from "../i18n/Translation" import { TypedTranslation } from "../i18n/Translation"
import GeoIndexedStore from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
export class OpenIdEditor extends VariableUiElement { export class OpenIdEditor extends VariableUiElement {
constructor( constructor(
state: { readonly locationControl: Store<Loc> }, mapProperties: { location: Store<{ lon: number; lat: number }>; zoom: Store<number> },
iconStyle?: string, iconStyle?: string,
objectId?: string objectId?: string
) { ) {
const t = Translations.t.general.attribution const t = Translations.t.general.attribution
super( super(
state.locationControl.map((location) => { mapProperties.location.map(
(location) => {
let elementSelect = "" let elementSelect = ""
if (objectId !== undefined) { if (objectId !== undefined) {
const parts = objectId.split("/") const parts = objectId.split("/")
@ -46,22 +46,21 @@ export class OpenIdEditor extends VariableUiElement {
} }
} }
const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${ const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${
location?.zoom ?? 0 mapProperties.zoom?.data ?? 0
}/${location?.lat ?? 0}/${location?.lon ?? 0}` }/${location?.lat ?? 0}/${location?.lon ?? 0}`
return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, { return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, {
url: idLink, url: idLink,
newTab: true, newTab: true,
}) })
}) },
[mapProperties.zoom]
)
) )
} }
} }
export class OpenJosm extends Combine { export class OpenJosm extends Combine {
constructor( constructor(osmConnection: OsmConnection, bounds: Store<BBox>, iconStyle?: string) {
state: { osmConnection: OsmConnection; currentBounds: Store<BBox> },
iconStyle?: string
) {
const t = Translations.t.general.attribution const t = Translations.t.general.attribution
const josmState = new UIEventSource<string>(undefined) const josmState = new UIEventSource<string>(undefined)
@ -83,21 +82,21 @@ export class OpenJosm extends Combine {
const toggle = new Toggle( const toggle = new Toggle(
new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => { new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => {
const bounds: any = state.currentBounds.data const bbox = bounds.data
if (bounds === undefined) { if (bbox === undefined) {
return undefined return
} }
const top = bounds.getNorth() const top = bbox.getNorth()
const bottom = bounds.getSouth() const bottom = bbox.getSouth()
const right = bounds.getEast() const right = bbox.getEast()
const left = bounds.getWest() const left = bbox.getWest()
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}` const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
Utils.download(josmLink) Utils.download(josmLink)
.then((answer) => josmState.setData(answer.replace(/\n/g, "").trim())) .then((answer) => josmState.setData(answer.replace(/\n/g, "").trim()))
.catch((_) => josmState.setData("ERROR")) .catch((_) => josmState.setData("ERROR"))
}), }),
undefined, undefined,
state.osmConnection.userDetails.map( osmConnection.userDetails.map(
(ud) => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible (ud) => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible
) )
) )
@ -113,14 +112,14 @@ export default class CopyrightPanel extends Combine {
private static LicenseObject = CopyrightPanel.GenerateLicenses() private static LicenseObject = CopyrightPanel.GenerateLicenses()
constructor(state: { constructor(state: {
layoutToUse: LayoutConfig layout: LayoutConfig
featurePipeline: FeaturePipeline bounds: Store<BBox>
currentBounds: Store<BBox>
locationControl: UIEventSource<Loc>
osmConnection: OsmConnection osmConnection: OsmConnection
dataIsLoading: Store<boolean>
perLayer: ReadonlyMap<string, GeoIndexedStore>
}) { }) {
const t = Translations.t.general.attribution const t = Translations.t.general.attribution
const layoutToUse = state.layoutToUse const layoutToUse = state.layout
const iconAttributions: BaseUIElement[] = layoutToUse.usedImages.map( const iconAttributions: BaseUIElement[] = layoutToUse.usedImages.map(
CopyrightPanel.IconAttribution CopyrightPanel.IconAttribution

View file

@ -1,7 +1,6 @@
import { SubtleButton } from "../Base/SubtleButton" import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg" import Svg from "../../Svg"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import State from "../../State"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import CheckBoxes from "../Input/Checkboxes" import CheckBoxes from "../Input/Checkboxes"

View file

@ -1,16 +1,13 @@
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { FixedInputElement } from "../Input/FixedInputElement"
import { RadioButton } from "../Input/RadioButton"
import { VariableUiElement } from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement"
import Toggle, { ClickableToggle } from "../Input/Toggle" import Toggle from "../Input/Toggle"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import { Translation } from "../i18n/Translation" import { Translation } from "../i18n/Translation"
import Svg from "../../Svg" import Svg from "../../Svg"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" import FilteredLayer from "../../Models/FilteredLayer"
import BackgroundSelector from "./BackgroundSelector"
import FilterConfig from "../../Models/ThemeConfig/FilterConfig" import FilterConfig from "../../Models/ThemeConfig/FilterConfig"
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig" import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"
import { SubstitutedTranslation } from "../SubstitutedTranslation" import { SubstitutedTranslation } from "../SubstitutedTranslation"
@ -18,9 +15,7 @@ import ValidatedTextField from "../Input/ValidatedTextField"
import { QueryParameters } from "../../Logic/Web/QueryParameters" import { QueryParameters } from "../../Logic/Web/QueryParameters"
import { TagUtils } from "../../Logic/Tags/TagUtils" import { TagUtils } from "../../Logic/Tags/TagUtils"
import { InputElement } from "../Input/InputElement" import { InputElement } from "../Input/InputElement"
import { DropDown } from "../Input/DropDown"
import { FixedUiElement } from "../Base/FixedUiElement" import { FixedUiElement } from "../Base/FixedUiElement"
import BaseLayer from "../../Models/BaseLayer"
import Loc from "../../Models/Loc" import Loc from "../../Models/Loc"
import { BackToThemeOverview } from "./ActionButtons" import { BackToThemeOverview } from "./ActionButtons"
@ -272,102 +267,6 @@ export class LayerFilterPanel extends Combine {
return [tr, settableFilter] return [tr, settableFilter]
} }
private static createCheckboxFilter(
filterConfig: FilterConfig
): [BaseUIElement, UIEventSource<FilterState>] {
let option = filterConfig.options[0]
const icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6")
const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2 w-6")
const qp = QueryParameters.GetBooleanQueryParameter(
"filter-" + filterConfig.id,
false,
"Is filter '" + filterConfig.options[0].question.textFor("en") + " enabled?"
)
const toggle = new ClickableToggle(
new Combine([icon, option.question.Clone().SetClass("block")]).SetClass("flex"),
new Combine([iconUnselected, option.question.Clone().SetClass("block")]).SetClass(
"flex"
),
qp
)
.ToggleOnClick()
.SetClass("block m-1")
return [
toggle,
toggle.isEnabled.sync(
(enabled) =>
enabled
? {
currentFilter: option.osmTags,
state: "true",
}
: undefined,
[],
(f) => f !== undefined
),
]
}
private static createMultiFilter(
filterConfig: FilterConfig
): [BaseUIElement, UIEventSource<FilterState>] {
let options = filterConfig.options
const values: FilterState[] = options.map((f, i) => ({
currentFilter: f.osmTags,
state: i,
}))
let filterPicker: InputElement<number>
const value = QueryParameters.GetQueryParameter(
"filter-" + filterConfig.id,
"0",
"Value for filter " + filterConfig.id
).sync(
(str) => Number(str),
[],
(n) => "" + n
)
if (options.length <= 6) {
filterPicker = new RadioButton(
options.map(
(option, i) =>
new FixedInputElement(option.question.Clone().SetClass("block"), i)
),
{
value,
dontStyle: true,
}
)
} else {
filterPicker = new DropDown(
"",
options.map((option, i) => ({
value: i,
shown: option.question.Clone(),
})),
value
)
}
return [
filterPicker,
filterPicker.GetValue().sync(
(i) => values[i],
[],
(selected) => {
const v = selected?.state
if (v === undefined || typeof v === "string") {
return undefined
}
return v
}
),
]
}
private static createFilter( private static createFilter(
state: {}, state: {},
filterConfig: FilterConfig filterConfig: FilterConfig
@ -376,12 +275,6 @@ export class LayerFilterPanel extends Combine {
return LayerFilterPanel.createFilterWithFields(state, filterConfig) return LayerFilterPanel.createFilterWithFields(state, filterConfig)
} }
if (filterConfig.options.length === 1) { return undefined
return LayerFilterPanel.createCheckboxFilter(filterConfig)
}
const filter = LayerFilterPanel.createMultiFilter(filterConfig)
filter[0].SetClass("pl-2")
return filter
} }
} }

View file

@ -0,0 +1,79 @@
<script lang="ts">/**
* The FilterView shows the various options to enable/disable a single layer.
*/
import type FilteredLayer from "../../Models/FilteredLayer";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import ToSvelte from "../Base/ToSvelte.svelte";
import Checkbox from "../Base/Checkbox.svelte";
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
import type { Writable } from "svelte/store";
import If from "../Base/If.svelte";
import Dropdown from "../Base/Dropdown.svelte";
import { onDestroy } from "svelte";
export let filteredLayer: FilteredLayer;
export let zoomlevel: number;
let layer: LayerConfig = filteredLayer.layerDef;
let isDisplayed: boolean = filteredLayer.isDisplayed.data;
onDestroy(filteredLayer.isDisplayed.addCallbackAndRunD(d => {
isDisplayed = d;
return false
}));
/**
* Gets a UIEventSource as boolean for the given option, to be used with a checkbox
*/
function getBooleanStateFor(option: FilterConfig): Writable<boolean> {
const state = filteredLayer.appliedFilters.get(option.id);
return state.sync(f => f === 0, [], (b) => b ? 0 : undefined);
}
/**
* Gets a UIEventSource as number for the given option, to be used with a dropdown or radiobutton
*/
function getStateFor(option: FilterConfig): Writable<number> {
return filteredLayer.appliedFilters.get(option.id);
}
</script>
{#if filteredLayer.layerDef.name}
<div>
<label class="flex gap-1">
<Checkbox selected={filteredLayer.isDisplayed} />
<If condition={filteredLayer.isDisplayed}>
<ToSvelte construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6")}></ToSvelte>
<ToSvelte slot="else" construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 opacity-50")}></ToSvelte>
</If>
{filteredLayer.layerDef.name}
</label>
<If condition={filteredLayer.isDisplayed}>
<div id="subfilters" class="flex flex-col gap-y-1 mb-4 ml-4">
{#each filteredLayer.layerDef.filters as filter}
<div>
<!-- There are three (and a half) modes of filters: a single checkbox, a radio button/dropdown or with fields -->
{#if filter.options.length === 1 && filter.options[0].fields.length === 0}
<label>
<Checkbox selected={getBooleanStateFor(filter)} />
{filter.options[0].question}
</label>
{/if}
{#if filter.options.length > 1}
<Dropdown value={getStateFor(filter)}>
{#each filter.options as option, i}
<option value={i}>
{ option.question}
</option>
{/each}
</Dropdown>
{/if}
</div>
{/each}
</div>
</If>
</div>
{/if}

View file

@ -1,8 +1,7 @@
import { VariableUiElement } from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement"
import Svg from "../../Svg" import Svg from "../../Svg"
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler" import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"
import { BBox } from "../../Logic/BBox"
import Hotkeys from "../Base/Hotkeys" import Hotkeys from "../Base/Hotkeys"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
@ -94,14 +93,13 @@ export class GeolocationControl extends VariableUiElement {
return return
} }
if (geolocationState.currentGPSLocation.data === undefined) { // A location _is_ known! Let's move to this location
const currentLocation = geolocationState.currentGPSLocation.data
if (currentLocation === undefined) {
// No location is known yet, not much we can do // No location is known yet, not much we can do
lastClick.setData(new Date()) lastClick.setData(new Date())
return return
} }
// A location _is_ known! Let's move to this location
const currentLocation = geolocationState.currentGPSLocation.data
const inBounds = state.bounds.data.contains([ const inBounds = state.bounds.data.contains([
currentLocation.longitude, currentLocation.longitude,
currentLocation.latitude, currentLocation.latitude,

View file

@ -0,0 +1,94 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import type { Feature } from "geojson";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import ToSvelte from "../Base/ToSvelte.svelte";
import Svg from "../../Svg.js";
import Translations from "../i18n/Translations";
import Loading from "../Base/Loading.svelte";
import Hotkeys from "../Base/Hotkeys";
import { Geocoding } from "../../Logic/Osm/Geocoding";
import { BBox } from "../../Logic/BBox";
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore";
Translations.t;
export let bounds: UIEventSource<BBox>
export let layout: LayoutConfig;
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
export let selectedElement: UIEventSource<Feature>;
export let selectedLayer: UIEventSource<LayerConfig>;
let searchContents: string = undefined;
let isRunning: boolean = false;
let inputElement: HTMLInputElement;
let feedback: string = undefined
Hotkeys.RegisterHotkey(
{ ctrl: "F" },
Translations.t.hotkeyDocumentation.selectSearch,
() => {
inputElement?.focus()
inputElement?.select()
}
)
async function performSearch() {
try {
isRunning = true;
searchContents = searchContents?.trim() ?? ""
if (searchContents === "") {
return
}
const result = await Geocoding.Search(searchContents, bounds.data)
if (result.length == 0) {
feedback = Translations.t.search.nothing.txt
return
}
const poi = result[0]
const [lat0, lat1, lon0, lon1] = poi.boundingbox
bounds.set(new BBox([[lon0, lat0], [lon1, lat1]]).pad(0.01))
const id = poi.osm_type + "/" + poi.osm_id
const layers = Array.from(perLayer.values())
for (const layer of layers) {
const found = layer.features.data.find(f => f.properties.id === id)
selectedElement.setData(found)
selectedLayer.setData(layer.layer.layerDef)
}
}catch (e) {
console.error(e)
feedback = Translations.t.search.error.txt
} finally {
isRunning = false;
}
}
</script>
<div class="flex normal-background rounded-full pl-2">
<form>
{#if isRunning}
<Loading>{Translations.t.general.search.searching}</Loading>
{:else if feedback !== undefined}
<div class="alert" on:click={() => feedback = undefined}>
{feedback}
</div>
{:else }
<input
bind:this={inputElement}
on:keypress={keypr => keypr.key === "Enter" ? performSearch() : undefined}
bind:value={searchContents}
placeholder={Translations.t.general.search.search}>
{/if}
</form>
<div class="w-6 h-6" on:click={performSearch}>
<ToSvelte construct={Svg.search_ui}></ToSvelte>
</div>
</div>

View file

@ -1,12 +1,6 @@
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import Toggle from "../Input/Toggle"
import MapControlButton from "../MapControlButton"
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"
import Svg from "../../Svg"
import MapState from "../../Logic/State/MapState" import MapState from "../../Logic/State/MapState"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import LevelSelector from "./LevelSelector" import LevelSelector from "./LevelSelector"
import { GeolocationControl } from "./GeolocationControl"
export default class RightControls extends Combine { export default class RightControls extends Combine {
constructor(state: MapState & { featurePipeline: FeaturePipeline }) { constructor(state: MapState & { featurePipeline: FeaturePipeline }) {

View file

@ -0,0 +1,75 @@
<script lang="ts">
import type { Feature } from "geojson";
import { Store, UIEventSource } from "../../Logic/UIEventSource";
import TagRenderingAnswer from "../Popup/TagRenderingAnswer";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import ToSvelte from "../Base/ToSvelte.svelte";
import { VariableUiElement } from "../Base/VariableUIElement.js";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import { onDestroy } from "svelte";
export let selectedElement: UIEventSource<Feature>;
export let layer: UIEventSource<LayerConfig>;
export let tags: Store<UIEventSource<Record<string, string>>>;
let _tags: UIEventSource<Record<string, string>>;
onDestroy(tags.subscribe(tags => {
_tags = tags;
return false
}));
export let specialVisState: SpecialVisualizationState;
/**
* const title = new TagRenderingAnswer(
* tags,
* layerConfig.title ?? new TagRenderingConfig("POI"),
* state
* ).SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2 text-2xl")
* const titleIcons = new Combine(
* layerConfig.titleIcons.map((icon) => {
* return new TagRenderingAnswer(
* tags,
* icon,
* state,
* "block h-8 max-h-8 align-baseline box-content sm:p-0.5 titleicon"
* )
* })
* ).SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")
*
* return new Combine([
* new Combine([title, titleIcons]).SetClass(
* "flex flex-col sm:flex-row flex-grow justify-between"
* ),
* ])
*/
</script>
<div>
<div on:click={() =>selectedElement.setData(undefined)}>close</div>
<div class="flex flex-col sm:flex-row flex-grow justify-between">
<!-- Title element-->
<ToSvelte
construct={() => new VariableUiElement(tags.mapD(tags => new TagRenderingAnswer(tags, layer.data.title, specialVisState), [layer]))}></ToSvelte>
<div class="flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2">
{#each $layer.titleIcons as titleIconConfig (titleIconConfig.id)}
<div class="w-8 h-8">
<ToSvelte
construct={() => new VariableUiElement(tags.mapD(tags => new TagRenderingAnswer(tags, titleIconConfig, specialVisState)))}></ToSvelte>
</div>
{/each}
</div>
</div>
<ul>
{#each Object.keys($_tags) as key}
<li><b>{key}</b>=<b>{$_tags[key]}</b></li>
{/each}
</ul>
</div>

View file

@ -4,20 +4,14 @@ import Title from "../Base/Title"
import TagRenderingChart from "./TagRenderingChart" import TagRenderingChart from "./TagRenderingChart"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import Locale from "../i18n/Locale" import Locale from "../i18n/Locale"
import { UIEventSource } from "../../Logic/UIEventSource" import { FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource"
import { OsmFeature } from "../../Models/OsmFeature" import BaseUIElement from "../BaseUIElement"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
export default class StatisticsPanel extends VariableUiElement { export default class StatisticsForLayerPanel extends VariableUiElement {
constructor( constructor(elementsInview: FeatureSourceForLayer) {
elementsInview: UIEventSource<{ element: OsmFeature; layer: LayerConfig }[]>, const layer = elementsInview.layer.layerDef
state: {
layoutToUse: LayoutConfig
}
) {
super( super(
elementsInview.stabilized(1000).map( elementsInview.features.stabilized(1000).map(
(features) => { (features) => {
if (features === undefined) { if (features === undefined) {
return new Loading("Loading data") return new Loading("Loading data")
@ -25,16 +19,10 @@ export default class StatisticsPanel extends VariableUiElement {
if (features.length === 0) { if (features.length === 0) {
return "No elements in view" return "No elements in view"
} }
const els = [] const els: BaseUIElement[] = []
for (const layer of state.layoutToUse.layers) {
if (layer.name === undefined) {
continue
}
const featuresForLayer = features const featuresForLayer = features
.filter((f) => f.layer === layer)
.map((f) => f.element)
if (featuresForLayer.length === 0) { if (featuresForLayer.length === 0) {
continue return
} }
els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8")) els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8"))
@ -58,7 +46,6 @@ export default class StatisticsPanel extends VariableUiElement {
} }
} }
els.push(new Combine(layerStats).SetClass("flex flex-wrap")) els.push(new Combine(layerStats).SetClass("flex flex-wrap"))
}
return new Combine(els) return new Combine(els)
}, },
[Locale.language] [Locale.language]

View file

@ -11,6 +11,7 @@ import LoggedInUserIndicator from "../LoggedInUserIndicator"
import { ActionButtons } from "./ActionButtons" import { ActionButtons } from "./ActionButtons"
import { BBox } from "../../Logic/BBox" import { BBox } from "../../Logic/BBox"
import Loc from "../../Models/Loc" import Loc from "../../Models/Loc"
import { DefaultGuiState } from "../DefaultGuiState"
export default class ThemeIntroductionPanel extends Combine { export default class ThemeIntroductionPanel extends Combine {
constructor( constructor(
@ -24,6 +25,7 @@ export default class ThemeIntroductionPanel extends Combine {
osmConnection: OsmConnection osmConnection: OsmConnection
currentBounds: Store<BBox> currentBounds: Store<BBox>
locationControl: UIEventSource<Loc> locationControl: UIEventSource<Loc>
defaultGuiState: DefaultGuiState
}, },
guistate?: { userInfoIsOpened: UIEventSource<boolean> } guistate?: { userInfoIsOpened: UIEventSource<boolean> }
) { ) {

View file

@ -17,7 +17,7 @@ export default class UploadTraceToOsmUI extends LoginToggle {
constructor( constructor(
trace: (title: string) => string, trace: (title: string) => string,
state: { state: {
layoutToUse: LayoutConfig layout: LayoutConfig
osmConnection: OsmConnection osmConnection: OsmConnection
readonly featureSwitchUserbadge: Store<boolean> readonly featureSwitchUserbadge: Store<boolean>
}, },

View file

@ -1,5 +1,4 @@
import FeaturePipelineState from "../Logic/State/FeaturePipelineState" import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
import State from "../State"
import { Utils } from "../Utils" import { Utils } from "../Utils"
import { UIEventSource } from "../Logic/UIEventSource" import { UIEventSource } from "../Logic/UIEventSource"
import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs" import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs"
@ -11,14 +10,11 @@ import BaseUIElement from "./BaseUIElement"
import LeftControls from "./BigComponents/LeftControls" import LeftControls from "./BigComponents/LeftControls"
import RightControls from "./BigComponents/RightControls" import RightControls from "./BigComponents/RightControls"
import CenterMessageBox from "./CenterMessageBox" import CenterMessageBox from "./CenterMessageBox"
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"
import ScrollableFullScreen from "./Base/ScrollableFullScreen" import ScrollableFullScreen from "./Base/ScrollableFullScreen"
import Translations from "./i18n/Translations" import Translations from "./i18n/Translations"
import SimpleAddUI from "./BigComponents/SimpleAddUI" import SimpleAddUI from "./BigComponents/SimpleAddUI"
import StrayClickHandler from "../Logic/Actors/StrayClickHandler" import StrayClickHandler from "../Logic/Actors/StrayClickHandler"
import { DefaultGuiState } from "./DefaultGuiState" import { DefaultGuiState } from "./DefaultGuiState"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import home_location_json from "../assets/layers/home_location/home_location.json"
import NewNoteUi from "./Popup/NewNoteUi" import NewNoteUi from "./Popup/NewNoteUi"
import Combine from "./Base/Combine" import Combine from "./Base/Combine"
import AddNewMarker from "./BigComponents/AddNewMarker" import AddNewMarker from "./BigComponents/AddNewMarker"
@ -32,7 +28,6 @@ import { FixedUiElement } from "./Base/FixedUiElement"
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
import { GeoLocationState } from "../Logic/State/GeoLocationState" import { GeoLocationState } from "../Logic/State/GeoLocationState"
import Hotkeys from "./Base/Hotkeys" import Hotkeys from "./Base/Hotkeys"
import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers"
import CopyrightPanel from "./BigComponents/CopyrightPanel" import CopyrightPanel from "./BigComponents/CopyrightPanel"
import SvelteUIElement from "./Base/SvelteUIElement" import SvelteUIElement from "./Base/SvelteUIElement"
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte" import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
@ -50,9 +45,6 @@ export default class DefaultGUI {
constructor(state: FeaturePipelineState, guiState: DefaultGuiState) { constructor(state: FeaturePipelineState, guiState: DefaultGuiState) {
this.state = state this.state = state
this.guiState = guiState this.guiState = guiState
if (this.state.featureSwitchGeolocation.data) {
this.geolocationHandler = new GeoLocationHandler(new GeoLocationState(), state)
}
} }
public setup() { public setup() {
@ -74,10 +66,6 @@ export default class DefaultGUI {
this.state.backgroundLayer.setData(AvailableBaseLayers.osmCarto) this.state.backgroundLayer.setData(AvailableBaseLayers.osmCarto)
} }
) )
Utils.downloadJson("./service-worker-version")
.then((data) => console.log("Service worker", data))
.catch((_) => console.log("Service worker not active"))
} }
public setupClickDialogOnMap( public setupClickDialogOnMap(
@ -173,13 +161,6 @@ export default class DefaultGUI {
this.setupClickDialogOnMap(guiState.filterViewIsOpened, state) this.setupClickDialogOnMap(guiState.filterViewIsOpened, state)
new ShowDataLayer({
leafletMap: state.leafletMap,
layerToShow: new LayerConfig(home_location_json, "home_location", true),
features: state.homeLocation,
state,
})
const selectedElement: FilteredLayer = state.filteredLayers.data.filter( const selectedElement: FilteredLayer = state.filteredLayers.data.filter(
(l) => l.layerDef.id === "selected_element" (l) => l.layerDef.id === "selected_element"
)[0] )[0]
@ -285,23 +266,6 @@ export default class DefaultGUI {
.SetClass("flex items-center justify-center normal-background h-full") .SetClass("flex items-center justify-center normal-background h-full")
.AttachTo("on-small-screen") .AttachTo("on-small-screen")
new Combine([
Toggle.If(state.featureSwitchSearch, () => {
const search = new SearchAndGo(state).SetClass(
"shadow rounded-full h-min w-full overflow-hidden sm:max-w-sm pointer-events-auto"
)
Hotkeys.RegisterHotkey(
{ ctrl: "F" },
Translations.t.hotkeyDocumentation.selectSearch,
() => {
search.focus()
}
)
return search
}),
]).AttachTo("top-right")
new LeftControls(state, guiState).AttachTo("bottom-left") new LeftControls(state, guiState).AttachTo("bottom-left")
new RightControls(state, this.geolocationHandler).AttachTo("bottom-right") new RightControls(state, this.geolocationHandler).AttachTo("bottom-right")

View file

@ -1,13 +1,13 @@
import { UIEventSource } from "../Logic/UIEventSource" import { UIEventSource } from "../Logic/UIEventSource"
import { QueryParameters } from "../Logic/Web/QueryParameters"
import Hash from "../Logic/Web/Hash" import Hash from "../Logic/Web/Hash"
export class DefaultGuiState { export class DefaultGuiState {
static state: DefaultGuiState
public readonly welcomeMessageIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>( public readonly welcomeMessageIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(
false false
) )
public readonly menuIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(false)
public readonly downloadControlIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>( public readonly downloadControlIsOpened: UIEventSource<boolean> = new UIEventSource<boolean>(
false false
) )
@ -22,17 +22,8 @@ export class DefaultGuiState {
public readonly userInfoFocusedQuestion: UIEventSource<string> = new UIEventSource<string>( public readonly userInfoFocusedQuestion: UIEventSource<string> = new UIEventSource<string>(
undefined undefined
) )
public readonly welcomeMessageOpenedTab: UIEventSource<number>
constructor() { private readonly sources: Record<string, UIEventSource<boolean>> = {
this.welcomeMessageOpenedTab = UIEventSource.asFloat(
QueryParameters.GetQueryParameter(
"tab",
"0",
`The tab that is shown in the welcome-message.`
)
)
const sources = {
welcome: this.welcomeMessageIsOpened, welcome: this.welcomeMessageIsOpened,
download: this.downloadControlIsOpened, download: this.downloadControlIsOpened,
filters: this.filterViewIsOpened, filters: this.filterViewIsOpened,
@ -41,6 +32,7 @@ export class DefaultGuiState {
userinfo: this.userInfoIsOpened, userinfo: this.userInfoIsOpened,
} }
constructor() {
const self = this const self = this
this.userInfoIsOpened.addCallback((isOpen) => { this.userInfoIsOpened.addCallback((isOpen) => {
if (!isOpen) { if (!isOpen) {
@ -49,10 +41,16 @@ export class DefaultGuiState {
} }
}) })
sources[Hash.hash.data?.toLowerCase()]?.setData(true) this.sources[Hash.hash.data?.toLowerCase()]?.setData(true)
if (Hash.hash.data === "" || Hash.hash.data === undefined) { if (Hash.hash.data === "" || Hash.hash.data === undefined) {
this.welcomeMessageIsOpened.setData(true) this.welcomeMessageIsOpened.setData(true)
} }
} }
public closeAll() {
for (const sourceKey in this.sources) {
this.sources[sourceKey].setData(false)
}
}
} }

View file

@ -13,7 +13,7 @@ export default class DeleteImage extends Toggle {
constructor( constructor(
key: string, key: string,
tags: Store<any>, tags: Store<any>,
state: { layoutToUse: LayoutConfig; changes?: Changes; osmConnection?: OsmConnection } state: { layout: LayoutConfig; changes?: Changes; osmConnection?: OsmConnection }
) { ) {
const oldValue = tags.data[key] const oldValue = tags.data[key]
const isDeletedBadge = Translations.t.image.isDeleted const isDeletedBadge = Translations.t.image.isDeleted
@ -24,7 +24,7 @@ export default class DeleteImage extends Toggle {
await state?.changes?.applyAction( await state?.changes?.applyAction(
new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, { new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, {
changeType: "delete-image", changeType: "delete-image",
theme: state.layoutToUse.id, theme: state.layout.id,
}) })
) )
}) })
@ -39,7 +39,7 @@ export default class DeleteImage extends Toggle {
await state?.changes?.applyAction( await state?.changes?.applyAction(
new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data, { new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data, {
changeType: "answer", changeType: "answer",
theme: state.layoutToUse.id, theme: state.layout.id,
}) })
) )
}) })

View file

@ -14,7 +14,7 @@ export class ImageCarousel extends Toggle {
constructor( constructor(
images: Store<{ key: string; url: string; provider: ImageProvider }[]>, images: Store<{ key: string; url: string; provider: ImageProvider }[]>,
tags: Store<any>, tags: Store<any>,
state: { osmConnection?: OsmConnection; changes?: Changes; layoutToUse: LayoutConfig } state: { osmConnection?: OsmConnection; changes?: Changes; layout: LayoutConfig }
) { ) {
const uiElements = images.map( const uiElements = images.map(
(imageURLS: { key: string; url: string; provider: ImageProvider }[]) => { (imageURLS: { key: string; url: string; provider: ImageProvider }[]) => {

View file

@ -11,26 +11,19 @@ import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { FixedUiElement } from "../Base/FixedUiElement" import { FixedUiElement } from "../Base/FixedUiElement"
import { VariableUiElement } from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Changes } from "../../Logic/Osm/Changes"
import Loading from "../Base/Loading" import Loading from "../Base/Loading"
import { LoginToggle } from "../Popup/LoginButton" import { LoginToggle } from "../Popup/LoginButton"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import { DefaultGuiState } from "../DefaultGuiState" import { DefaultGuiState } from "../DefaultGuiState"
import ScrollableFullScreen from "../Base/ScrollableFullScreen" import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import { SpecialVisualizationState } from "../SpecialVisualization"
export class ImageUploadFlow extends Toggle { export class ImageUploadFlow extends Toggle {
private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>() private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>()
constructor( constructor(
tagsSource: Store<any>, tagsSource: Store<any>,
state: { state: SpecialVisualizationState,
osmConnection: OsmConnection
layoutToUse: LayoutConfig
changes: Changes
featureSwitchUserbadge: Store<boolean>
},
imagePrefix: string = "image", imagePrefix: string = "image",
text: string = undefined text: string = undefined
) { ) {
@ -56,7 +49,7 @@ export class ImageUploadFlow extends Toggle {
await state.changes.applyAction( await state.changes.applyAction(
new ChangeTagAction(tags.id, new Tag(key, url), tagsSource.data, { new ChangeTagAction(tags.id, new Tag(key, url), tagsSource.data, {
changeType: "add-image", changeType: "add-image",
theme: state.layoutToUse.id, theme: state.layout.id,
}) })
) )
console.log("Adding image:" + key, url) console.log("Adding image:" + key, url)
@ -111,7 +104,7 @@ export class ImageUploadFlow extends Toggle {
const tags = tagsSource.data const tags = tagsSource.data
const layout = state?.layoutToUse const layout = state?.layout
let matchingLayer: LayerConfig = undefined let matchingLayer: LayerConfig = undefined
for (const layer of layout?.layers ?? []) { for (const layer of layout?.layers ?? []) {
if (layer.source.osmTags.matchesProperties(tags)) { if (layer.source.osmTags.matchesProperties(tags)) {

View file

@ -1,31 +1,30 @@
import { InputElement } from "./InputElement" import { InputElement } from "./InputElement"
import { UIEventSource } from "../../Logic/UIEventSource" import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import Svg from "../../Svg" import Svg from "../../Svg"
import { Utils } from "../../Utils"
import Loc from "../../Models/Loc" import Loc from "../../Models/Loc"
import { GeoOperations } from "../../Logic/GeoOperations" import { GeoOperations } from "../../Logic/GeoOperations"
import Minimap, { MinimapObj } from "../Base/Minimap"
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
/** /**
* Selects a length after clicking on the minimap, in meters * Selects a length after clicking on the minimap, in meters
*/ */
export default class LengthInput extends InputElement<string> { export default class LengthInput extends InputElement<string> {
private readonly _location: UIEventSource<Loc> private readonly _location: Store<Loc>
private readonly value: UIEventSource<string> private readonly value: UIEventSource<string>
private readonly background: UIEventSource<any> private readonly background: Store<RasterLayerPolygon>
constructor( constructor(
mapBackground: UIEventSource<any>,
location: UIEventSource<Loc>, location: UIEventSource<Loc>,
mapBackground?: UIEventSource<RasterLayerPolygon>,
value?: UIEventSource<string> value?: UIEventSource<string>
) { ) {
super() super()
this._location = location this._location = location
this.value = value ?? new UIEventSource<string>(undefined) this.value = value ?? new UIEventSource<string>(undefined)
this.background = mapBackground this.background = mapBackground ?? new ImmutableStore(AvailableRasterLayers.osmCarto)
this.SetClass("block") this.SetClass("block")
} }
@ -41,7 +40,6 @@ export default class LengthInput extends InputElement<string> {
protected InnerConstructElement(): HTMLElement { protected InnerConstructElement(): HTMLElement {
let map: BaseUIElement & MinimapObj = undefined let map: BaseUIElement & MinimapObj = undefined
let layerControl: BaseUIElement = undefined let layerControl: BaseUIElement = undefined
if (!Utils.runningFromConsole) {
map = Minimap.createMiniMap({ map = Minimap.createMiniMap({
background: this.background, background: this.background,
allowMoving: false, allowMoving: false,
@ -62,7 +60,6 @@ export default class LengthInput extends InputElement<string> {
allowedCategories: ["map", "photo"], allowedCategories: ["map", "photo"],
} }
) )
}
const crosshair = new Combine([ const crosshair = new Combine([
Svg.length_crosshair_svg().SetStyle( Svg.length_crosshair_svg().SetStyle(
`position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);` `position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`

View file

@ -3,7 +3,7 @@ import * as EmailValidator from "email-validator"
import { parsePhoneNumberFromString } from "libphonenumber-js" import { parsePhoneNumberFromString } from "libphonenumber-js"
import { InputElement } from "./InputElement" import { InputElement } from "./InputElement"
import { TextField } from "./TextField" import { TextField } from "./TextField"
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import CombinedInputElement from "./CombinedInputElement" import CombinedInputElement from "./CombinedInputElement"
import SimpleDatePicker from "./SimpleDatePicker" import SimpleDatePicker from "./SimpleDatePicker"
import OpeningHoursInput from "../OpeningHours/OpeningHoursInput" import OpeningHoursInput from "../OpeningHours/OpeningHoursInput"
@ -25,6 +25,7 @@ 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 Locale from "../i18n/Locale" import Locale from "../i18n/Locale"
import { RasterLayerPolygon } from "../../Models/RasterLayers"
export class TextFieldDef { export class TextFieldDef {
public readonly name: string public readonly name: string
@ -638,7 +639,7 @@ class LengthTextField extends TextFieldDef {
location?: [number, number] location?: [number, number]
args?: string[] args?: string[]
feature?: any feature?: any
mapBackgroundLayer?: Store<BaseLayer> mapBackgroundLayer?: Store<RasterLayerPolygon>
} }
) => { ) => {
options = options ?? {} options = options ?? {}
@ -674,14 +675,18 @@ class LengthTextField extends TextFieldDef {
zoom: zoom, zoom: zoom,
}) })
if (args[1]) { if (args[1]) {
// We have a prefered map! // The arguments indicate the preferred background type
options.mapBackgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo( options.mapBackgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo(
location, location,
new UIEventSource<string[]>(args[1].split(",")) new ImmutableStore<string[]>(args[1].split(","))
) )
} }
const background = options?.mapBackgroundLayer const background = options?.mapBackgroundLayer
const li = new LengthInput(new UIEventSource<BaseLayer>(background.data), location, value) const li = new LengthInput(
new UIEventSource<RasterLayerPolygon>(background.data),
location,
value
)
li.SetStyle("height: 20rem;") li.SetStyle("height: 20rem;")
return li return li
} }

View file

@ -21,12 +21,20 @@ export class MapLibreAdaptor implements MapProperties {
"keyboard", "keyboard",
"touchZoomRotate", "touchZoomRotate",
] ]
private static maplibre_zoom_handlers = [
"scrollZoom",
"boxZoom",
"doubleClickZoom",
"touchZoomRotate",
]
readonly location: UIEventSource<{ lon: number; lat: number }> readonly location: UIEventSource<{ lon: number; lat: number }>
readonly zoom: UIEventSource<number> readonly zoom: UIEventSource<number>
readonly bounds: Store<BBox> readonly bounds: UIEventSource<BBox>
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined> readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
readonly maxbounds: UIEventSource<BBox | undefined> readonly maxbounds: UIEventSource<BBox | undefined>
readonly allowMoving: UIEventSource<true | boolean | undefined> readonly allowMoving: UIEventSource<true | boolean | undefined>
readonly allowZooming: UIEventSource<true | boolean | undefined>
readonly lastClickLocation: Store<undefined | { lon: number; lat: number }>
private readonly _maplibreMap: Store<MLMap> private readonly _maplibreMap: Store<MLMap>
private readonly _bounds: UIEventSource<BBox> private readonly _bounds: UIEventSource<BBox>
/** /**
@ -50,11 +58,14 @@ export class MapLibreAdaptor implements MapProperties {
}) })
this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined) this.maxbounds = state?.maxbounds ?? new UIEventSource(undefined)
this.allowMoving = state?.allowMoving ?? new UIEventSource(true) this.allowMoving = state?.allowMoving ?? new UIEventSource(true)
this.allowZooming = state?.allowZooming ?? new UIEventSource(true)
this._bounds = new UIEventSource(undefined) this._bounds = new UIEventSource(undefined)
this.bounds = this._bounds this.bounds = this._bounds
this.rasterLayer = this.rasterLayer =
state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined) state?.rasterLayer ?? new UIEventSource<RasterLayerPolygon | undefined>(undefined)
const lastClickLocation = new UIEventSource<{ lon: number; lat: number }>(undefined)
this.lastClickLocation = lastClickLocation
const self = this const self = this
maplibreMap.addCallbackAndRunD((map) => { maplibreMap.addCallbackAndRunD((map) => {
map.on("load", () => { map.on("load", () => {
@ -63,11 +74,13 @@ export class MapLibreAdaptor implements MapProperties {
self.SetZoom(self.zoom.data) self.SetZoom(self.zoom.data)
self.setMaxBounds(self.maxbounds.data) self.setMaxBounds(self.maxbounds.data)
self.setAllowMoving(self.allowMoving.data) self.setAllowMoving(self.allowMoving.data)
self.setAllowZooming(self.allowZooming.data)
}) })
self.MoveMapToCurrentLoc(self.location.data) self.MoveMapToCurrentLoc(self.location.data)
self.SetZoom(self.zoom.data) self.SetZoom(self.zoom.data)
self.setMaxBounds(self.maxbounds.data) self.setMaxBounds(self.maxbounds.data)
self.setAllowMoving(self.allowMoving.data) self.setAllowMoving(self.allowMoving.data)
self.setAllowZooming(self.allowZooming.data)
map.on("moveend", () => { map.on("moveend", () => {
const dt = this.location.data const dt = this.location.data
dt.lon = map.getCenter().lng dt.lon = map.getCenter().lng
@ -81,6 +94,11 @@ export class MapLibreAdaptor implements MapProperties {
]) ])
self._bounds.setData(bbox) self._bounds.setData(bbox)
}) })
map.on("click", (e) => {
const lon = e.lngLat.lng
const lat = e.lngLat.lat
lastClickLocation.setData({ lon, lat })
})
}) })
this.rasterLayer.addCallback((_) => this.rasterLayer.addCallback((_) =>
@ -95,6 +113,8 @@ export class MapLibreAdaptor implements MapProperties {
this.zoom.addCallbackAndRunD((z) => self.SetZoom(z)) this.zoom.addCallbackAndRunD((z) => self.SetZoom(z))
this.maxbounds.addCallbackAndRun((bbox) => self.setMaxBounds(bbox)) this.maxbounds.addCallbackAndRun((bbox) => self.setMaxBounds(bbox))
this.allowMoving.addCallbackAndRun((allowMoving) => self.setAllowMoving(allowMoving)) this.allowMoving.addCallbackAndRun((allowMoving) => self.setAllowMoving(allowMoving))
this.allowZooming.addCallbackAndRun((allowZooming) => self.setAllowZooming(allowZooming))
this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds))
} }
/** /**
@ -205,7 +225,7 @@ export class MapLibreAdaptor implements MapProperties {
// already the correct background layer, nothing to do // already the correct background layer, nothing to do
return return
} }
if (background === undefined) { if (!background?.url) {
// no background to set // no background to set
this.removeCurrentLayer(map) this.removeCurrentLayer(map)
this._currentRasterLayer = undefined this._currentRasterLayer = undefined
@ -266,4 +286,38 @@ export class MapLibreAdaptor implements MapProperties {
} }
} }
} }
private setAllowZooming(allow: true | boolean | undefined) {
const map = this._maplibreMap.data
if (map === undefined) {
return
}
if (allow === false) {
for (const id of MapLibreAdaptor.maplibre_zoom_handlers) {
map[id].disable()
}
} else {
for (const id of MapLibreAdaptor.maplibre_zoom_handlers) {
map[id].enable()
}
}
}
private setBounds(bounds: BBox) {
const map = this._maplibreMap.data
if (map === undefined) {
return
}
const oldBounds = map.getBounds()
const e = 0.0000001
const hasDiff =
Math.abs(oldBounds.getWest() - bounds.getWest()) > e &&
Math.abs(oldBounds.getEast() - bounds.getEast()) > e &&
Math.abs(oldBounds.getNorth() - bounds.getNorth()) > e &&
Math.abs(oldBounds.getSouth() - bounds.getSouth()) > e
if (!hasDiff) {
return
}
map.fitBounds(bounds.toLngLat())
}
} }

View file

@ -1,4 +1,4 @@
import { ImmutableStore, Store } from "../../Logic/UIEventSource" import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import type { Map as MlMap } from "maplibre-gl" import type { Map as MlMap } from "maplibre-gl"
import { GeoJSONSource, Marker } from "maplibre-gl" import { GeoJSONSource, Marker } from "maplibre-gl"
import { ShowDataLayerOptions } from "./ShowDataLayerOptions" import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
@ -14,10 +14,13 @@ import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import * as range_layer from "../../assets/layers/range/range.json" import * as range_layer from "../../assets/layers/range/range.json"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import FilteredLayer from "../../Models/FilteredLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
class PointRenderingLayer { class PointRenderingLayer {
private readonly _config: PointRenderingConfig private readonly _config: PointRenderingConfig
private readonly _fetchStore?: (id: string) => Store<OsmTags> private readonly _fetchStore?: (id: string) => Store<Record<string, string>>
private readonly _map: MlMap private readonly _map: MlMap
private readonly _onClick: (feature: Feature) => void private readonly _onClick: (feature: Feature) => void
private readonly _allMarkers: Map<string, Marker> = new Map<string, Marker>() private readonly _allMarkers: Map<string, Marker> = new Map<string, Marker>()
@ -27,7 +30,7 @@ class PointRenderingLayer {
features: FeatureSource, features: FeatureSource,
config: PointRenderingConfig, config: PointRenderingConfig,
visibility?: Store<boolean>, visibility?: Store<boolean>,
fetchStore?: (id: string) => Store<OsmTags>, fetchStore?: (id: string) => Store<Record<string, string>>,
onClick?: (feature: Feature) => void onClick?: (feature: Feature) => void
) { ) {
this._config = config this._config = config
@ -96,7 +99,7 @@ class PointRenderingLayer {
} }
private addPoint(feature: Feature, loc: [number, number]): Marker { private addPoint(feature: Feature, loc: [number, number]): Marker {
let store: Store<OsmTags> let store: Store<Record<string, string>>
if (this._fetchStore) { if (this._fetchStore) {
store = this._fetchStore(feature.properties.id) store = this._fetchStore(feature.properties.id)
} else { } else {
@ -143,7 +146,7 @@ class LineRenderingLayer {
private readonly _map: MlMap private readonly _map: MlMap
private readonly _config: LineRenderingConfig private readonly _config: LineRenderingConfig
private readonly _visibility?: Store<boolean> private readonly _visibility?: Store<boolean>
private readonly _fetchStore?: (id: string) => Store<OsmTags> private readonly _fetchStore?: (id: string) => Store<Record<string, string>>
private readonly _onClick?: (feature: Feature) => void private readonly _onClick?: (feature: Feature) => void
private readonly _layername: string private readonly _layername: string
private readonly _listenerInstalledOn: Set<string> = new Set<string>() private readonly _listenerInstalledOn: Set<string> = new Set<string>()
@ -154,7 +157,7 @@ class LineRenderingLayer {
layername: string, layername: string,
config: LineRenderingConfig, config: LineRenderingConfig,
visibility?: Store<boolean>, visibility?: Store<boolean>,
fetchStore?: (id: string) => Store<OsmTags>, fetchStore?: (id: string) => Store<Record<string, string>>,
onClick?: (feature: Feature) => void onClick?: (feature: Feature) => void
) { ) {
this._layername = layername this._layername = layername
@ -212,9 +215,10 @@ class LineRenderingLayer {
promoteId: "id", promoteId: "id",
}) })
// @ts-ignore // @ts-ignore
const linelayer = this._layername + "_line"
map.addLayer({ map.addLayer({
source: this._layername, source: this._layername,
id: this._layername + "_line", id: linelayer,
type: "line", type: "line",
paint: { paint: {
"line-color": ["feature-state", "color"], "line-color": ["feature-state", "color"],
@ -227,9 +231,10 @@ class LineRenderingLayer {
}, },
}) })
const polylayer = this._layername + "_polygon"
map.addLayer({ map.addLayer({
source: this._layername, source: this._layername,
id: this._layername + "_polygon", id: polylayer,
type: "fill", type: "fill",
filter: ["in", ["geometry-type"], ["literal", ["Polygon", "MultiPolygon"]]], filter: ["in", ["geometry-type"], ["literal", ["Polygon", "MultiPolygon"]]],
layout: {}, layout: {},
@ -238,6 +243,11 @@ class LineRenderingLayer {
"fill-opacity": 0.1, "fill-opacity": 0.1,
}, },
}) })
this._visibility.addCallbackAndRunD((visible) => {
map.setLayoutProperty(linelayer, "visibility", visible ? "visible" : "none")
map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none")
})
} else { } else {
src.setData({ src.setData({
type: "FeatureCollection", type: "FeatureCollection",
@ -295,6 +305,24 @@ export default class ShowDataLayer {
map.addCallbackAndRunD((map) => self.initDrawFeatures(map)) map.addCallbackAndRunD((map) => self.initDrawFeatures(map))
} }
public static showMultipleLayers(
mlmap: UIEventSource<MlMap>,
features: FeatureSource,
layers: LayerConfig[],
options?: Partial<ShowDataLayerOptions>
) {
const perLayer = new PerLayerFeatureSourceSplitter(
layers.map((l) => new FilteredLayer(l)),
new StaticFeatureSource(features)
)
perLayer.forEach((fs) => {
new ShowDataLayer(mlmap, {
layer: fs.layer.layerDef,
features: fs,
...(options ?? {}),
})
})
}
public static showRange( public static showRange(
map: Store<MlMap>, map: Store<MlMap>,
features: FeatureSource, features: FeatureSource,
@ -318,8 +346,11 @@ export default class ShowDataLayer {
} }
private initDrawFeatures(map: MlMap) { private initDrawFeatures(map: MlMap) {
const { features, doShowLayer, fetchStore, selectedElement } = this._options let { features, doShowLayer, fetchStore, selectedElement, selectedLayer } = this._options
const onClick = (feature: Feature) => selectedElement?.setData(feature) const onClick = (feature: Feature) => {
selectedElement?.setData(feature)
selectedLayer?.setData(this._options.layer)
}
for (let i = 0; i < this._options.layer.lineRendering.length; i++) { for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
const lineRenderingConfig = this._options.layer.lineRendering[i] const lineRenderingConfig = this._options.layer.lineRendering[i]
new LineRenderingLayer( new LineRenderingLayer(

View file

@ -1,6 +1,8 @@
import FeatureSource from "../../Logic/FeatureSource/FeatureSource" import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { OsmTags } from "../../Models/OsmFeature" import { OsmTags } from "../../Models/OsmFeature"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Feature } from "geojson"
export interface ShowDataLayerOptions { export interface ShowDataLayerOptions {
/** /**
@ -11,7 +13,12 @@ export interface ShowDataLayerOptions {
* Indication of the current selected element; overrides some filters. * Indication of the current selected element; overrides some filters.
* When a feature is tapped, the feature will be put in there * When a feature is tapped, the feature will be put in there
*/ */
selectedElement?: UIEventSource<any> selectedElement?: UIEventSource<Feature>
/**
* When a feature of this layer is tapped, the layer will be marked
*/
selectedLayer?: UIEventSource<LayerConfig>
/** /**
* If set, zoom to the features when initially loaded and when they are changed * If set, zoom to the features when initially loaded and when they are changed
@ -26,5 +33,5 @@ export interface ShowDataLayerOptions {
* Function which fetches the relevant store. * Function which fetches the relevant store.
* If given, the map will update when a property is changed * If given, the map will update when a property is changed
*/ */
fetchStore?: (id: string) => UIEventSource<OsmTags> fetchStore?: (id: string) => Store<Record<string, string>>
} }

View file

@ -1,39 +0,0 @@
/**
* SHows geojson on the given leaflet map, but attempts to figure out the correct layer first
*/
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import ShowDataLayer from "./ShowDataLayer"
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import FilteredLayer from "../../Models/FilteredLayer"
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
import { Map as MlMap } from "maplibre-gl"
import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource"
import { GlobalFilter } from "../../Models/GlobalFilter"
export default class ShowDataMultiLayer {
constructor(
map: Store<MlMap>,
options: ShowDataLayerOptions & {
layers: FilteredLayer[]
globalFilters?: Store<GlobalFilter[]>
}
) {
new PerLayerFeatureSourceSplitter(
new ImmutableStore(options.layers),
(features, layer) => {
const newOptions = {
...options,
layer: layer.layerDef,
features: new FilteringFeatureSource(
layer,
features,
options.fetchStore,
options.globalFilters
),
}
new ShowDataLayer(map, newOptions)
},
options.features
)
}
}

View file

@ -1,6 +1,4 @@
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import LocationInput from "../Input/LocationInput" import LocationInput from "../Input/LocationInput"
import { BBox } from "../../Logic/BBox" import { BBox } from "../../Logic/BBox"
@ -18,18 +16,13 @@ import { Tag } from "../../Logic/Tags/Tag"
import { WayId } from "../../Models/OsmFeature" import { WayId } from "../../Models/OsmFeature"
import { Translation } from "../i18n/Translation" import { Translation } from "../i18n/Translation"
import { Feature } from "geojson" import { Feature } from "geojson"
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers" import { AvailableRasterLayers } from "../../Models/RasterLayers"
import { GlobalFilter } from "../../Logic/State/GlobalFilter" import { SpecialVisualizationState } from "../SpecialVisualization"
import ClippedFeatureSource from "../../Logic/FeatureSource/Sources/ClippedFeatureSource"
export default class ConfirmLocationOfPoint extends Combine { export default class ConfirmLocationOfPoint extends Combine {
constructor( constructor(
state: { state: SpecialVisualizationState,
globalFilters: UIEventSource<GlobalFilter[]>
featureSwitchIsTesting: UIEventSource<boolean>
osmConnection: OsmConnection
featurePipeline: FeaturePipeline
backgroundLayer?: UIEventSource<RasterLayerPolygon | undefined>
},
filterViewIsOpened: UIEventSource<boolean>, filterViewIsOpened: UIEventSource<boolean>,
preset: PresetInfo, preset: PresetInfo,
confirmText: BaseUIElement, confirmText: BaseUIElement,
@ -55,7 +48,7 @@ export default class ConfirmLocationOfPoint extends Combine {
const locationSrc = new UIEventSource(zloc) const locationSrc = new UIEventSource(zloc)
let backgroundLayer = new UIEventSource( let backgroundLayer = new UIEventSource(
state?.backgroundLayer?.data ?? AvailableRasterLayers.osmCarto state?.mapProperties.rasterLayer?.data ?? AvailableRasterLayers.osmCarto
) )
if (preset.preciseInput.preferredBackground) { if (preset.preciseInput.preferredBackground) {
const defaultBackground = AvailableRasterLayers.SelectBestLayerAccordingTo( const defaultBackground = AvailableRasterLayers.SelectBestLayerAccordingTo(
@ -105,15 +98,13 @@ export default class ConfirmLocationOfPoint extends Combine {
Math.max(preset.boundsFactor ?? 0.25, 2) Math.max(preset.boundsFactor ?? 0.25, 2)
) )
loadedBbox = bbox loadedBbox = bbox
const allFeatures: Feature[] = [] const sources = preset.preciseInput.snapToLayers.map(
preset.preciseInput.snapToLayers.forEach((layerId) => { (layerId) =>
console.log("Snapping to", layerId) new ClippedFeatureSource(
state.featurePipeline state.perLayer.get(layerId),
.GetFeaturesWithin(layerId, bbox) bbox.asGeoJson({})
?.forEach((feats) => allFeatures.push(...(<any[]>feats))) )
}) )
console.log("Snapping to", allFeatures)
snapToFeatures.setData(allFeatures)
}) })
} }
} }

View file

@ -488,7 +488,7 @@ export class OH {
} }
public static CreateOhObject( public static CreateOhObject(
tags: object & { _lat: number; _lon: number; _country?: string }, tags: Record<string, string> & { _lat: number; _lon: number; _country?: string },
textToParse: string textToParse: string
) { ) {
// noinspection JSPotentiallyInvalidConstructorUsage // noinspection JSPotentiallyInvalidConstructorUsage

View file

@ -23,7 +23,7 @@ export default class OpeningHoursVisualization extends Toggle {
] ]
constructor( constructor(
tags: UIEventSource<any>, tags: UIEventSource<Record<string, string>>,
state: { osmConnection?: OsmConnection }, state: { osmConnection?: OsmConnection },
key: string, key: string,
prefix = "", prefix = "",
@ -49,7 +49,7 @@ export default class OpeningHoursVisualization extends Toggle {
} }
try { try {
return OpeningHoursVisualization.CreateFullVisualisation( return OpeningHoursVisualization.CreateFullVisualisation(
OH.CreateOhObject(tags.data, ohtext) OH.CreateOhObject(<any>tags.data, ohtext)
) )
} catch (e) { } catch (e) {
console.warn(e, e.stack) console.warn(e, e.stack)

View file

@ -8,7 +8,8 @@ import Toggle from "../Input/Toggle"
import { LoginToggle } from "./LoginButton" import { LoginToggle } from "./LoginButton"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import Title from "../Base/Title" import Title from "../Base/Title"
import { SpecialVisualization } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
export class AddNoteCommentViz implements SpecialVisualization { export class AddNoteCommentViz implements SpecialVisualization {
funcName = "add_note_comment" funcName = "add_note_comment"
@ -21,7 +22,11 @@ export class AddNoteCommentViz implements SpecialVisualization {
}, },
] ]
public constr(state, tags, args) { public constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[]
) {
const t = Translations.t.notes const t = Translations.t.notes
const textField = new TextField({ const textField = new TextField({
placeholder: t.addCommentPlaceholder, placeholder: t.addCommentPlaceholder,
@ -62,13 +67,12 @@ export class AddNoteCommentViz implements SpecialVisualization {
return t.addCommentAndClose return t.addCommentAndClose
}) })
) )
).onClick(() => { ).onClick(async () => {
const id = tags.data[args[1] ?? "id"] const id = tags.data[args[1] ?? "id"]
state.osmConnection.closeNote(id, txt.data).then((_) => { await state.osmConnection.closeNote(id, txt.data)
tags.data["closed_at"] = new Date().toISOString() tags.data["closed_at"] = new Date().toISOString()
tags.ping() tags.ping()
}) })
})
const reopen = new SubtleButton( const reopen = new SubtleButton(
Svg.note_svg().SetClass("max-h-7"), Svg.note_svg().SetClass("max-h-7"),
@ -80,13 +84,12 @@ export class AddNoteCommentViz implements SpecialVisualization {
return t.reopenNoteAndComment return t.reopenNoteAndComment
}) })
) )
).onClick(() => { ).onClick(async () => {
const id = tags.data[args[1] ?? "id"] const id = tags.data[args[1] ?? "id"]
state.osmConnection.reopenNote(id, txt.data).then((_) => { await state.osmConnection.reopenNote(id, txt.data)
tags.data["closed_at"] = undefined tags.data["closed_at"] = undefined
tags.ping() tags.ping()
}) })
})
const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "") const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "")
const stateButtons = new Toggle( const stateButtons = new Toggle(

View file

@ -1,7 +1,5 @@
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import { Stores, UIEventSource } from "../../Logic/UIEventSource" import { Stores, UIEventSource } from "../../Logic/UIEventSource"
import { DefaultGuiState } from "../DefaultGuiState"
import { SubtleButton } from "../Base/SubtleButton" import { SubtleButton } from "../Base/SubtleButton"
import Img from "../Base/Img" import Img from "../Base/Img"
import { FixedUiElement } from "../Base/FixedUiElement" import { FixedUiElement } from "../Base/FixedUiElement"
@ -9,8 +7,6 @@ import Combine from "../Base/Combine"
import Link from "../Base/Link" import Link from "../Base/Link"
import { SubstitutedTranslation } from "../SubstitutedTranslation" import { SubstitutedTranslation } from "../SubstitutedTranslation"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import Minimap from "../Base/Minimap"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import { VariableUiElement } from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement"
import Loading from "../Base/Loading" import Loading from "../Base/Loading"
@ -23,15 +19,21 @@ import FilteredLayer from "../../Models/FilteredLayer"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import Lazy from "../Base/Lazy" import Lazy from "../Base/Lazy"
import List from "../Base/List" import List from "../Base/List"
import { SpecialVisualization } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
import ShowDataLayer from "../Map/ShowDataLayer"
import SvelteUIElement from "../Base/SvelteUIElement"
import MaplibreMap from "../Map/MaplibreMap.svelte"
export interface AutoAction extends SpecialVisualization { export interface AutoAction extends SpecialVisualization {
supportsAutoAction: boolean supportsAutoAction: boolean
applyActionOn( applyActionOn(
state: { state: {
layoutToUse: LayoutConfig layout: LayoutConfig
changes: Changes changes: Changes
indexedFeatures: IndexedFeatureSource
}, },
tagSource: UIEventSource<any>, tagSource: UIEventSource<any>,
argument: string[] argument: string[]
@ -43,7 +45,7 @@ class ApplyButton extends UIElement {
private readonly text: string private readonly text: string
private readonly targetTagRendering: string private readonly targetTagRendering: string
private readonly target_layer_id: string private readonly target_layer_id: string
private readonly state: FeaturePipelineState private readonly state: SpecialVisualizationState
private readonly target_feature_ids: string[] private readonly target_feature_ids: string[]
private readonly buttonState = new UIEventSource< private readonly buttonState = new UIEventSource<
"idle" | "running" | "done" | { error: string } "idle" | "running" | "done" | { error: string }
@ -52,7 +54,7 @@ class ApplyButton extends UIElement {
private readonly tagRenderingConfig: TagRenderingConfig private readonly tagRenderingConfig: TagRenderingConfig
constructor( constructor(
state: FeaturePipelineState, state: SpecialVisualizationState,
target_feature_ids: string[], target_feature_ids: string[],
options: { options: {
target_layer_id: string target_layer_id: string
@ -68,9 +70,7 @@ class ApplyButton extends UIElement {
this.targetTagRendering = options.targetTagRendering this.targetTagRendering = options.targetTagRendering
this.text = options.text this.text = options.text
this.icon = options.icon this.icon = options.icon
this.layer = this.state.filteredLayers.data.find( this.layer = this.state.layerState.filteredLayers.get(this.target_layer_id)
(l) => l.layerDef.id === this.target_layer_id
)
this.tagRenderingConfig = this.layer.layerDef.tagRenderings.find( this.tagRenderingConfig = this.layer.layerDef.tagRenderings.find(
(tr) => tr.id === this.targetTagRendering (tr) => tr.id === this.targetTagRendering
) )
@ -101,22 +101,23 @@ class ApplyButton extends UIElement {
), ),
]).SetClass("subtle") ]).SetClass("subtle")
const previewMap = Minimap.createMiniMap({ const mlmap = new UIEventSource(undefined)
allowMoving: false, const mla = new MapLibreAdaptor(mlmap, {
background: this.state.backgroundLayer, rasterLayer: this.state.mapProperties.rasterLayer,
addLayerControl: true, })
}).SetClass("h-48") mla.allowZooming.setData(false)
mla.allowMoving.setData(false)
const previewMap = new SvelteUIElement(MaplibreMap, { map: mlmap }).SetClass("h-48")
const features = this.target_feature_ids.map((id) => const features = this.target_feature_ids.map((id) =>
this.state.allElements.ContainingFeatures.get(id) this.state.indexedFeatures.featuresById.data.get(id)
) )
new ShowDataLayer({ new ShowDataLayer(mlmap, {
leafletMap: previewMap.leafletMap,
zoomToFeatures: true,
features: StaticFeatureSource.fromGeojson(features), features: StaticFeatureSource.fromGeojson(features),
state: this.state, zoomToFeatures: true,
layerToShow: this.layer.layerDef, layer: this.layer.layerDef,
}) })
return new VariableUiElement( return new VariableUiElement(
@ -144,7 +145,7 @@ class ApplyButton extends UIElement {
console.log("Applying auto-action on " + this.target_feature_ids.length + " features") console.log("Applying auto-action on " + this.target_feature_ids.length + " features")
for (const targetFeatureId of this.target_feature_ids) { for (const targetFeatureId of this.target_feature_ids) {
const featureTags = this.state.allElements.getEventSourceById(targetFeatureId) const featureTags = this.state.featureProperties.getStore(targetFeatureId)
const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt
const specialRenderings = Utils.NoNull( const specialRenderings = Utils.NoNull(
SubstitutedTranslation.ExtractSpecialComponents(rendering).map((x) => x.special) SubstitutedTranslation.ExtractSpecialComponents(rendering).map((x) => x.special)
@ -234,14 +235,13 @@ export default class AutoApplyButton implements SpecialVisualization {
} }
constr( constr(
state: FeaturePipelineState, state: SpecialVisualizationState,
tagSource: UIEventSource<any>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[]
guistate: DefaultGuiState
): BaseUIElement { ): BaseUIElement {
try { try {
if ( if (
!state.layoutToUse.official && !state.layout.official &&
!( !(
state.featureSwitchIsTesting.data || state.featureSwitchIsTesting.data ||
state.osmConnection._oauth_config.url === state.osmConnection._oauth_config.url ===

View file

@ -1,4 +1,3 @@
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
@ -7,7 +6,8 @@ import Img from "../Base/Img"
import { SubtleButton } from "../Base/SubtleButton" import { SubtleButton } from "../Base/SubtleButton"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import { LoginToggle } from "./LoginButton" import { LoginToggle } from "./LoginButton"
import { SpecialVisualization } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
export class CloseNoteButton implements SpecialVisualization { export class CloseNoteButton implements SpecialVisualization {
public readonly funcName = "close_note" public readonly funcName = "close_note"
@ -43,7 +43,11 @@ export class CloseNoteButton implements SpecialVisualization {
}, },
] ]
public constr(state: FeaturePipelineState, tags, args): BaseUIElement { public constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[]
): BaseUIElement {
const t = Translations.t.notes const t = Translations.t.notes
const params: { const params: {
@ -78,7 +82,7 @@ export class CloseNoteButton implements SpecialVisualization {
closeButton = new Toggle( closeButton = new Toggle(
closeButton, closeButton,
params.zoomButton ?? "", params.zoomButton ?? "",
state.locationControl.map((l) => l.zoom >= Number(params.minZoom)) state.mapProperties.zoom.map((zoom) => zoom >= Number(params.minZoom))
) )
} }

View file

@ -4,14 +4,15 @@ import Svg from "../../Svg"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import { GeoOperations } from "../../Logic/GeoOperations" import { GeoOperations } from "../../Logic/GeoOperations"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { SpecialVisualization } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
export class ExportAsGpxViz implements SpecialVisualization { export class ExportAsGpxViz implements SpecialVisualization {
funcName = "export_as_gpx" funcName = "export_as_gpx"
docs = "Exports the selected feature as GPX-file" docs = "Exports the selected feature as GPX-file"
args = [] args = []
constr(state, tagSource) { constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>) {
const t = Translations.t.general.download const t = Translations.t.general.download
return new SubtleButton( return new SubtleButton(
@ -23,10 +24,10 @@ export class ExportAsGpxViz implements SpecialVisualization {
).onClick(() => { ).onClick(() => {
console.log("Exporting as GPX!") console.log("Exporting as GPX!")
const tags = tagSource.data const tags = tagSource.data
const feature = state.allElements.ContainingFeatures.get(tags.id) const feature = state.indexedFeatures.featuresById.data.get(tags.id)
const matchingLayer = state?.layoutToUse?.getMatchingLayer(tags) const layer = state?.layout?.getMatchingLayer(tags)
const gpx = GeoOperations.AsGpx(feature, matchingLayer) const gpx = GeoOperations.AsGpx(feature, { layer })
const title = matchingLayer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track" const title = layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track"
Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", { Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", {
mimetype: "{gpx=application/gpx+xml}", mimetype: "{gpx=application/gpx+xml}",
}) })

View file

@ -1,9 +1,8 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { FixedUiElement } from "../Base/FixedUiElement" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
// import Histogram from "../BigComponents/Histogram"; import Histogram from "../BigComponents/Histogram"
// import {SpecialVisualization} from "../SpecialVisualization";
export class HistogramViz { export class HistogramViz implements SpecialVisualization {
funcName = "histogram" funcName = "histogram"
docs = "Create a histogram for a list of given values, read from the properties." docs = "Create a histogram for a list of given values, read from the properties."
example = example =
@ -30,7 +29,11 @@ export class HistogramViz {
}, },
] ]
constr(state, tagSource: UIEventSource<any>, args: string[]) { constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[]
) {
let assignColors = undefined let assignColors = undefined
if (args.length >= 3) { if (args.length >= 3) {
const colors = [...args] const colors = [...args]
@ -63,10 +66,8 @@ export class HistogramViz {
return undefined return undefined
} }
}) })
return new FixedUiElement("HISTORGRAM")
/*
return new Histogram(listSource, args[1], args[2], { return new Histogram(listSource, args[1], args[2], {
assignColor: assignColors, assignColor: assignColors,
})*/ })
} }
} }

View file

@ -1,51 +1,47 @@
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement";
import { SubtleButton } from "../Base/SubtleButton" import { SubtleButton } from "../Base/SubtleButton";
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource";
import Combine from "../Base/Combine" import Combine from "../Base/Combine";
import { VariableUiElement } from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement";
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations";
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle";
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction" import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
import Loading from "../Base/Loading" import Loading from "../Base/Loading";
import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { OsmConnection } from "../../Logic/Osm/OsmConnection";
import Lazy from "../Base/Lazy" import Lazy from "../Base/Lazy";
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint" import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint";
import Img from "../Base/Img" import Img from "../Base/Img";
import FilteredLayer from "../../Models/FilteredLayer" import FilteredLayer from "../../Models/FilteredLayer";
import { FixedUiElement } from "../Base/FixedUiElement" import { FixedUiElement } from "../Base/FixedUiElement";
import Svg from "../../Svg" import Svg from "../../Svg";
import { Utils } from "../../Utils" import { Utils } from "../../Utils";
import Minimap from "../Base/Minimap" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" import CreateWayWithPointReuseAction, { MergePointConfig } from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import OsmChangeAction, { OsmCreateAction } from "../../Logic/Osm/Actions/OsmChangeAction";
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
import CreateWayWithPointReuseAction, { import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject";
MergePointConfig, import { PresetInfo } from "../BigComponents/SimpleAddUI";
} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction" import { TagUtils } from "../../Logic/Tags/TagUtils";
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction" import { And } from "../../Logic/Tags/And";
import FeatureSource from "../../Logic/FeatureSource/FeatureSource" import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject" import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction";
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" import { Tag } from "../../Logic/Tags/Tag";
import { DefaultGuiState } from "../DefaultGuiState" import TagApplyButton from "./TagApplyButton";
import { PresetInfo } from "../BigComponents/SimpleAddUI" import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import { TagUtils } from "../../Logic/Tags/TagUtils" import conflation_json from "../../assets/layers/conflation/conflation.json";
import { And } from "../../Logic/Tags/And" import { GeoOperations } from "../../Logic/GeoOperations";
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction" import { LoginToggle } from "./LoginButton";
import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction" import { AutoAction } from "./AutoApplyButton";
import { Tag } from "../../Logic/Tags/Tag" import Hash from "../../Logic/Web/Hash";
import TagApplyButton from "./TagApplyButton" import { PreciseInput } from "../../Models/ThemeConfig/PresetConfig";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization";
import conflation_json from "../../assets/layers/conflation/conflation.json"
import { GeoOperations } from "../../Logic/GeoOperations"
import { LoginToggle } from "./LoginButton"
import { AutoAction } from "./AutoApplyButton"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Changes } from "../../Logic/Osm/Changes"
import { ElementStorage } from "../../Logic/ElementStorage"
import Hash from "../../Logic/Web/Hash"
import { PreciseInput } from "../../Models/ThemeConfig/PresetConfig"
import { SpecialVisualization } from "../SpecialVisualization"
import Maproulette from "../../Logic/Maproulette"; import Maproulette from "../../Logic/Maproulette";
import { Feature, Point } from "geojson";
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson";
import ShowDataLayer from "../Map/ShowDataLayer";
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor";
import SvelteUIElement from "../Base/SvelteUIElement";
import MaplibreMap from "../Map/MaplibreMap.svelte";
/** /**
* A helper class for the various import-flows. * A helper class for the various import-flows.
@ -106,7 +102,7 @@ ${Utils.special_visualizations_importRequirementDocs}
} }
abstract constructElement( abstract constructElement(
state: FeaturePipelineState, state: SpecialVisualizationState,
args: { args: {
max_snap_distance: string max_snap_distance: string
snap_onto_layers: string snap_onto_layers: string
@ -116,13 +112,16 @@ ${Utils.special_visualizations_importRequirementDocs}
newTags: UIEventSource<any> newTags: UIEventSource<any>
targetLayer: string targetLayer: string
}, },
tagSource: UIEventSource<any>, tagSource: UIEventSource<Record<string, string>>,
guiState: DefaultGuiState, feature: Feature,
feature: any,
onCancelClicked: () => void onCancelClicked: () => void
): BaseUIElement ): BaseUIElement
constr(state, tagSource: UIEventSource<any>, argsRaw, guiState) { constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argsRaw: string[]
) {
/** /**
* Some generic import button pre-validation is implemented here: * Some generic import button pre-validation is implemented here:
* - Are we logged in? * - Are we logged in?
@ -139,7 +138,7 @@ ${Utils.special_visualizations_importRequirementDocs}
{ {
// Some initial validation // Some initial validation
if ( if (
!state.layoutToUse.official && !state.layout.official &&
!( !(
state.featureSwitchIsTesting.data || state.featureSwitchIsTesting.data ||
state.osmConnection._oauth_config.url === state.osmConnection._oauth_config.url ===
@ -148,11 +147,9 @@ ${Utils.special_visualizations_importRequirementDocs}
) { ) {
return new Combine([t.officialThemesOnly.SetClass("alert"), t.howToTest]) return new Combine([t.officialThemesOnly.SetClass("alert"), t.howToTest])
} }
const targetLayer: FilteredLayer = state.filteredLayers.data.filter( const targetLayer: FilteredLayer = state.layerState.filteredLayers.get(args.targetLayer)
(fl) => fl.layerDef.id === args.targetLayer
)[0]
if (targetLayer === undefined) { if (targetLayer === undefined) {
const e = `Target layer not defined: error in import button for theme: ${state.layoutToUse.id}: layer ${args.targetLayer} not found` const e = `Target layer not defined: error in import button for theme: ${state.layout.id}: layer ${args.targetLayer} not found`
console.error(e) console.error(e)
return new FixedUiElement(e).SetClass("alert") return new FixedUiElement(e).SetClass("alert")
} }
@ -167,7 +164,7 @@ ${Utils.special_visualizations_importRequirementDocs}
const inviteToImportButton = new SubtleButton(img, args.text) const inviteToImportButton = new SubtleButton(img, args.text)
const id = tagSource.data.id const id = tagSource.data.id
const feature = state.allElements.ContainingFeatures.get(id) const feature = state.indexedFeatures.featuresById.data.get(id)
// Explanation of the tags that will be applied onto the imported/conflated object // Explanation of the tags that will be applied onto the imported/conflated object
@ -205,22 +202,13 @@ ${Utils.special_visualizations_importRequirementDocs}
return tags._imported === "yes" return tags._imported === "yes"
}) })
/**** THe actual panel showing the import guiding map ****/ /**** The actual panel showing the import guiding map ****/
const importGuidingPanel = this.constructElement( const importGuidingPanel = this.constructElement(state, args, tagSource, feature, () =>
state, importClicked.setData(false)
args,
tagSource,
guiState,
feature,
() => importClicked.setData(false)
) )
const importFlow = new Toggle( const importFlow = new Toggle(
new Toggle( new Toggle(new Loading(t0.stillLoading), importGuidingPanel, state.dataIsLoading),
new Loading(t0.stillLoading),
importGuidingPanel,
state.featurePipeline.runningQuery
),
inviteToImportButton, inviteToImportButton,
importClicked importClicked
) )
@ -230,7 +218,7 @@ ${Utils.special_visualizations_importRequirementDocs}
new Toggle( new Toggle(
new Toggle(t.hasBeenImported, importFlow, isImported), new Toggle(t.hasBeenImported, importFlow, isImported),
t.zoomInMore.SetClass("alert block"), t.zoomInMore.SetClass("alert block"),
state.locationControl.map((l) => l.zoom >= 18) state.mapProperties.zoom.map((zoom) => zoom >= 18)
), ),
pleaseLoginButton, pleaseLoginButton,
state state
@ -258,8 +246,13 @@ ${Utils.special_visualizations_importRequirementDocs}
protected abstract canBeImported(feature: any) protected abstract canBeImported(feature: any)
private static readonly conflationLayer = new LayerConfig(
<LayerConfigJson>conflation_json,
"all_known_layers",
true
)
protected createConfirmPanelForWay( protected createConfirmPanelForWay(
state: FeaturePipelineState, state: SpecialVisualizationState,
args: { args: {
max_snap_distance: string max_snap_distance: string
snap_onto_layers: string snap_onto_layers: string
@ -270,32 +263,32 @@ ${Utils.special_visualizations_importRequirementDocs}
}, },
feature: any, feature: any,
originalFeatureTags: UIEventSource<any>, originalFeatureTags: UIEventSource<any>,
action: OsmChangeAction & { getPreview(): Promise<FeatureSource>; newElementId?: string }, action: OsmChangeAction & { getPreview?(): Promise<FeatureSource>; newElementId?: string },
onCancel: () => void onCancel: () => void
): BaseUIElement { ): BaseUIElement {
const self = this const self = this
const confirmationMap = Minimap.createMiniMap({ const map = new UIEventSource(undefined)
allowMoving: state.featureSwitchIsDebugging.data ?? false, new MapLibreAdaptor(map, {
background: state.backgroundLayer, allowMoving: UIEventSource.feedFrom(state.featureSwitchIsTesting),
allowZooming: UIEventSource.feedFrom(state.featureSwitchIsTesting),
rasterLayer: state.mapProperties.rasterLayer,
}) })
const confirmationMap = new SvelteUIElement(MaplibreMap, { map })
confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl") confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl")
// SHow all relevant data - including (eventually) the way of which the geometry will be replaced ShowDataLayer.showMultipleLayers(
new ShowDataMultiLayer({ map,
leafletMap: confirmationMap.leafletMap, new StaticFeatureSource([feature]),
zoomToFeatures: true, state.layout.layers,
features: StaticFeatureSource.fromGeojson([feature]), { zoomToFeatures: true }
state: state, )
layers: state.filteredLayers, // Show all relevant data - including (eventually) the way of which the geometry will be replaced
})
action.getPreview().then((changePreview) => { action.getPreview().then((changePreview) => {
new ShowDataLayer({ new ShowDataLayer(map, {
leafletMap: confirmationMap.leafletMap,
zoomToFeatures: false, zoomToFeatures: false,
features: changePreview, features: changePreview,
state, layer: AbstractImportButton.conflationLayer,
layerToShow: new LayerConfig(conflation_json, "all_known_layers", true),
}) })
}) })
@ -317,9 +310,9 @@ ${Utils.special_visualizations_importRequirementDocs}
{ {
originalFeatureTags.data["_imported"] = "yes" originalFeatureTags.data["_imported"] = "yes"
originalFeatureTags.ping() // will set isImported as per its definition originalFeatureTags.ping() // will set isImported as per its definition
state.changes.applyAction(action) await state.changes.applyAction(action)
const newId = action.newElementId ?? action.mainObjectId const newId = action.newElementId ?? action.mainObjectId
state.selectedElement.setData(state.allElements.ContainingFeatures.get(newId)) state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(newId))
} }
}) })
@ -392,7 +385,7 @@ export class ConflateButton extends AbstractImportButton {
} }
constructElement( constructElement(
state: FeaturePipelineState, state: SpecialVisualizationState,
args: { args: {
max_snap_distance: string max_snap_distance: string
snap_onto_layers: string snap_onto_layers: string
@ -403,8 +396,7 @@ export class ConflateButton extends AbstractImportButton {
targetLayer: string targetLayer: string
}, },
tagSource: UIEventSource<any>, tagSource: UIEventSource<any>,
guiState: DefaultGuiState, feature: Feature,
feature: any,
onCancelClicked: () => void onCancelClicked: () => void
): BaseUIElement { ): BaseUIElement {
const nodesMustMatch = args.snap_onto_layers const nodesMustMatch = args.snap_onto_layers
@ -424,10 +416,15 @@ export class ConflateButton extends AbstractImportButton {
const key = args["way_to_conflate"] const key = args["way_to_conflate"]
const wayToConflate = tagSource.data[key] const wayToConflate = tagSource.data[key]
feature = GeoOperations.removeOvernoding(feature) feature = GeoOperations.removeOvernoding(feature)
const action = new ReplaceGeometryAction(state, feature, wayToConflate, { const action: OsmChangeAction & { getPreview(): Promise<any> } = new ReplaceGeometryAction(
theme: state.layoutToUse.id, state,
feature,
wayToConflate,
{
theme: state.layout.id,
newTags: args.newTags.data, newTags: args.newTags.data,
}) }
)
return this.createConfirmPanelForWay( return this.createConfirmPanelForWay(
state, state,
@ -498,9 +495,9 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
newTags: UIEventSource<any> newTags: UIEventSource<any>
targetLayer: string targetLayer: string
}, },
state: FeaturePipelineState, state: SpecialVisualizationState,
mergeConfigs: any[] mergeConfigs: any[]
) { ): OsmCreateAction & { getPreview(): Promise<FeatureSource>; newElementId?: string } {
const coors = feature.geometry.coordinates const coors = feature.geometry.coordinates
if (feature.geometry.type === "Polygon" && coors.length > 1) { if (feature.geometry.type === "Polygon" && coors.length > 1) {
const outer = coors[0] const outer = coors[0]
@ -525,8 +522,8 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
} }
async applyActionOn( async applyActionOn(
state: { layoutToUse: LayoutConfig; changes: Changes; allElements: ElementStorage }, state: SpecialVisualizationState,
originalFeatureTags: UIEventSource<any>, originalFeatureTags: UIEventSource<Record<string, string>>,
argument: string[] argument: string[]
): Promise<void> { ): Promise<void> {
const id = originalFeatureTags.data.id const id = originalFeatureTags.data.id
@ -535,14 +532,9 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
} }
AbstractImportButton.importedIds.add(originalFeatureTags.data.id) AbstractImportButton.importedIds.add(originalFeatureTags.data.id)
const args = this.parseArgs(argument, originalFeatureTags) const args = this.parseArgs(argument, originalFeatureTags)
const feature = state.allElements.ContainingFeatures.get(id) const feature = state.indexedFeatures.featuresById.data.get(id)
const mergeConfigs = this.GetMergeConfig(args) const mergeConfigs = this.GetMergeConfig(args)
const action = ImportWayButton.CreateAction( const action = ImportWayButton.CreateAction(feature, args, state, mergeConfigs)
feature,
args,
<FeaturePipelineState>state,
mergeConfigs
)
await state.changes.applyAction(action) await state.changes.applyAction(action)
} }
@ -557,7 +549,13 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
return deps return deps
} }
constructElement(state, args, originalFeatureTags, guiState, feature, onCancel): BaseUIElement { constructElement(
state: SpecialVisualizationState,
args,
originalFeatureTags: UIEventSource<Record<string, string>>,
feature,
onCancel
): BaseUIElement {
const geometry = feature.geometry const geometry = feature.geometry
if (!(geometry.type == "LineString" || geometry.type === "Polygon")) { if (!(geometry.type == "LineString" || geometry.type === "Polygon")) {
@ -567,7 +565,12 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
// Upload the way to OSM // Upload the way to OSM
const mergeConfigs = this.GetMergeConfig(args) const mergeConfigs = this.GetMergeConfig(args)
let action = ImportWayButton.CreateAction(feature, args, state, mergeConfigs) let action: OsmCreateAction & {getPreview?: any} = ImportWayButton.CreateAction(
feature,
args,
state,
mergeConfigs
)
return this.createConfirmPanelForWay( return this.createConfirmPanelForWay(
state, state,
args, args,
@ -663,10 +666,9 @@ export class ImportPointButton extends AbstractImportButton {
note_id: string note_id: string
maproulette_id: string maproulette_id: string
}, },
state: FeaturePipelineState, state: SpecialVisualizationState,
guiState: DefaultGuiState,
originalFeatureTags: UIEventSource<any>, originalFeatureTags: UIEventSource<any>,
feature: any, feature: Feature<Point>,
onCancel: () => void, onCancel: () => void,
close: () => void close: () => void
): BaseUIElement { ): BaseUIElement {
@ -690,7 +692,7 @@ export class ImportPointButton extends AbstractImportButton {
} }
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, { const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
theme: state.layoutToUse.id, theme: state.layout.id,
changeType: "import", changeType: "import",
snapOnto: <OsmWay>snapOnto, snapOnto: <OsmWay>snapOnto,
specialMotivation: specialMotivation, specialMotivation: specialMotivation,
@ -698,7 +700,7 @@ export class ImportPointButton extends AbstractImportButton {
await state.changes.applyAction(newElementAction) await state.changes.applyAction(newElementAction)
state.selectedElement.setData( state.selectedElement.setData(
state.allElements.ContainingFeatures.get(newElementAction.newElementId) state.indexedFeatures.featuresById.data.get(newElementAction.newElementId)
) )
Hash.hash.setData(newElementAction.newElementId) Hash.hash.setData(newElementAction.newElementId)
@ -742,19 +744,17 @@ export class ImportPointButton extends AbstractImportButton {
const presetInfo = <PresetInfo>{ const presetInfo = <PresetInfo>{
tags: args.newTags.data, tags: args.newTags.data,
icon: () => new Img(args.icon), icon: () => new Img(args.icon),
layerToAddTo: state.filteredLayers.data.filter( layerToAddTo: state.layerState.filteredLayers.get(args.targetLayer),
(l) => l.layerDef.id === args.targetLayer
)[0],
name: args.text, name: args.text,
title: Translations.T(args.text), title: Translations.T(args.text),
preciseInput: preciseInputSpec, // must be explicitely assigned, if 'undefined' won't work otherwise preciseInput: preciseInputSpec, // must be explicitely assigned, if 'undefined' won't work otherwise
boundsFactor: 3, boundsFactor: 3,
} }
const [lon, lat] = feature.geometry.coordinates const [lon, lat] = <[number,number]> feature.geometry.coordinates
return new ConfirmLocationOfPoint( return new ConfirmLocationOfPoint(
state, state,
guiState.filterViewIsOpened, state.guistate.filterViewIsOpened,
presetInfo, presetInfo,
Translations.W(args.text), Translations.W(args.text),
{ {
@ -783,10 +783,9 @@ export class ImportPointButton extends AbstractImportButton {
} }
constructElement( constructElement(
state, state: SpecialVisualizationState,
args, args,
originalFeatureTags, originalFeatureTags,
guiState,
feature, feature,
onCancel: () => void onCancel: () => void
): BaseUIElement { ): BaseUIElement {
@ -797,7 +796,6 @@ export class ImportPointButton extends AbstractImportButton {
ImportPointButton.createConfirmPanelForPoint( ImportPointButton.createConfirmPanelForPoint(
args, args,
state, state,
guiState,
originalFeatureTags, originalFeatureTags,
feature, feature,
onCancel, onCancel,

View file

@ -1,9 +1,7 @@
import { SpecialVisualization } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import { VariableUiElement } from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement"
import { OsmTags } from "../../Models/OsmFeature"
import all_languages from "../../assets/language_translations.json" import all_languages from "../../assets/language_translations.json"
import { Translation } from "../i18n/Translation" import { Translation } from "../i18n/Translation"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
@ -16,10 +14,9 @@ import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../../Logic/Tags/And" import { And } from "../../Logic/Tags/And"
import { Tag } from "../../Logic/Tags/Tag" import { Tag } from "../../Logic/Tags/Tag"
import { EditButton, SaveButton } from "./SaveButton" import { EditButton, SaveButton } from "./SaveButton"
import { FixedUiElement } from "../Base/FixedUiElement"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import { On } from "../../Models/ThemeConfig/Conversion/Conversion" import { Feature } from "geojson"
export class LanguageElement implements SpecialVisualization { export class LanguageElement implements SpecialVisualization {
funcName: string = "language_chooser" funcName: string = "language_chooser"
@ -79,9 +76,10 @@ export class LanguageElement implements SpecialVisualization {
` `
constr( constr(
state: FeaturePipelineState, state: SpecialVisualizationState,
tagSource: UIEventSource<OsmTags>, tagSource: UIEventSource<Record<string, string>>,
argument: string[] argument: string[],
feature: Feature
): BaseUIElement { ): BaseUIElement {
let [key, question, item_render, single_render, all_render, on_no_known_languages, mode] = let [key, question, item_render, single_render, all_render, on_no_known_languages, mode] =
argument argument
@ -172,7 +170,7 @@ export class LanguageElement implements SpecialVisualization {
new And(selection), new And(selection),
tagSource.data, tagSource.data,
{ {
theme: state?.layoutToUse?.id ?? "unkown", theme: state?.layout?.id ?? "unkown",
changeType: "answer", changeType: "answer",
} }
) )

View file

@ -2,7 +2,9 @@ import { GeoOperations } from "../../Logic/GeoOperations"
import { MapillaryLink } from "../BigComponents/MapillaryLink" import { MapillaryLink } from "../BigComponents/MapillaryLink"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import Loc from "../../Models/Loc" import Loc from "../../Models/Loc"
import { SpecialVisualization } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { Feature } from "geojson"
import BaseUIElement from "../BaseUIElement"
export class MapillaryLinkVis implements SpecialVisualization { export class MapillaryLinkVis implements SpecialVisualization {
funcName = "mapillary_link" funcName = "mapillary_link"
@ -15,9 +17,13 @@ export class MapillaryLinkVis implements SpecialVisualization {
}, },
] ]
public constr(state, tagsSource, args) { public constr(
const feat = state.allElements.ContainingFeatures.get(tagsSource.data.id) state: SpecialVisualizationState,
const [lon, lat] = GeoOperations.centerpointCoordinates(feat) tagsSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature
): BaseUIElement {
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
let zoom = Number(args[0]) let zoom = Number(args[0])
if (isNaN(zoom)) { if (isNaN(zoom)) {
zoom = 18 zoom = 18

View file

@ -1,9 +1,14 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Loc from "../../Models/Loc"
import Minimap from "../Base/Minimap"
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import { SpecialVisualization } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { Feature } from "geojson"
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
import SvelteUIElement from "../Base/SvelteUIElement"
import MaplibreMap from "../Map/MaplibreMap.svelte"
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import FilteredLayer from "../../Models/FilteredLayer"
import ShowDataLayer from "../Map/ShowDataLayer"
import { stat } from "fs"
export class MinimapViz implements SpecialVisualization { export class MinimapViz implements SpecialVisualization {
funcName = "minimap" funcName = "minimap"
@ -22,16 +27,20 @@ export class MinimapViz implements SpecialVisualization {
] ]
example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`" example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`"
constr(state, tagSource, args, _) { constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[]
) {
if (state === undefined) { if (state === undefined) {
return undefined return undefined
} }
const keys = [...args] const keys = [...args]
keys.splice(0, 1) keys.splice(0, 1)
const featureStore = state.allElements.ContainingFeatures const featuresToShow: Store<Feature[]> = state.indexedFeatures.featuresById.map(
const featuresToShow: Store<{ freshness: Date; feature: any }[]> = tagSource.map( (featuresById) => {
(properties) => { const properties = tagSource.data
const features: { freshness: Date; feature: any }[] = [] const features: Feature[] = []
for (const key of keys) { for (const key of keys) {
const value = properties[key] const value = properties[key]
if (value === undefined || value === null) { if (value === undefined || value === null) {
@ -45,21 +54,22 @@ export class MinimapViz implements SpecialVisualization {
} }
for (const id of idList) { for (const id of idList) {
const feature = featureStore.get(id) const feature = featuresById.get(id)
if (feature === undefined) { if (feature === undefined) {
console.warn("No feature found for id ", id) console.warn("No feature found for id ", id)
continue continue
} }
features.push({ features.push(feature)
freshness: new Date(),
feature,
})
} }
} }
return features return features
} },
[tagSource]
) )
const properties = tagSource.data
const mlmap = new UIEventSource(undefined)
const mla = new MapLibreAdaptor(mlmap)
let zoom = 18 let zoom = 18
if (args[0]) { if (args[0]) {
const parsed = Number(args[0]) const parsed = Number(args[0])
@ -67,33 +77,18 @@ export class MinimapViz implements SpecialVisualization {
zoom = parsed zoom = parsed
} }
} }
const locationSource = new UIEventSource<Loc>({ mla.zoom.setData(zoom)
lat: Number(properties._lat), mla.allowMoving.setData(false)
lon: Number(properties._lon), mla.allowZooming.setData(false)
zoom: zoom,
})
const minimap = Minimap.createMiniMap({
background: state.backgroundLayer,
location: locationSource,
allowMoving: false,
})
locationSource.addCallback((loc) => { ShowDataLayer.showMultipleLayers(
if (loc.zoom > zoom) { mlmap,
// We zoom back new StaticFeatureSource(featuresToShow),
locationSource.data.zoom = zoom state.layout.layers
locationSource.ping() )
}
})
new ShowDataMultiLayer({ return new SvelteUIElement(MaplibreMap, { map: mlmap }).SetStyle(
leafletMap: minimap["leafletMap"], "overflow: hidden; pointer-events: none;"
zoomToFeatures: true, )
layers: state.filteredLayers,
features: new StaticFeatureSource(featuresToShow),
})
minimap.SetStyle("overflow: hidden; pointer-events: none;")
return minimap
} }
} }

View file

@ -2,17 +2,14 @@ import { Store } from "../../Logic/UIEventSource"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import { SubtleButton } from "../Base/SubtleButton" import { SubtleButton } from "../Base/SubtleButton"
import { Changes } from "../../Logic/Osm/Changes"
import { FixedUiElement } from "../Base/FixedUiElement" import { FixedUiElement } from "../Base/FixedUiElement"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import { VariableUiElement } from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { Tag } from "../../Logic/Tags/Tag" import { Tag } from "../../Logic/Tags/Tag"
import { ElementStorage } from "../../Logic/ElementStorage"
import { And } from "../../Logic/Tags/And" import { And } from "../../Logic/Tags/And"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { SpecialVisualizationState } from "../SpecialVisualization"
export interface MultiApplyParams { export interface MultiApplyParams {
featureIds: Store<string[]> featureIds: Store<string[]>
@ -21,12 +18,7 @@ export interface MultiApplyParams {
autoapply: boolean autoapply: boolean
overwrite: boolean overwrite: boolean
tagsSource: Store<any> tagsSource: Store<any>
state: { state: SpecialVisualizationState
changes: Changes
allElements: ElementStorage
layoutToUse: LayoutConfig
osmConnection: OsmConnection
}
} }
class MultiApplyExecutor { class MultiApplyExecutor {
@ -68,14 +60,14 @@ class MultiApplyExecutor {
console.log("Multi-applying changes...") console.log("Multi-applying changes...")
const featuresToChange = this.params.featureIds.data const featuresToChange = this.params.featureIds.data
const changes = this.params.state.changes const changes = this.params.state.changes
const allElements = this.params.state.allElements const allElements = this.params.state.featureProperties
const keysToChange = this.params.keysToApply const keysToChange = this.params.keysToApply
const overwrite = this.params.overwrite const overwrite = this.params.overwrite
const selfTags = this.params.tagsSource.data const selfTags = this.params.tagsSource.data
const theme = this.params.state.layoutToUse.id const theme = this.params.state.layout.id
for (const id of featuresToChange) { for (const id of featuresToChange) {
const tagsToApply: Tag[] = [] const tagsToApply: Tag[] = []
const otherFeatureTags = allElements.getEventSourceById(id).data const otherFeatureTags = allElements.getStore(id).data
for (const key of keysToChange) { for (const key of keysToChange) {
const newValue = selfTags[key] const newValue = selfTags[key]
if (newValue === undefined) { if (newValue === undefined) {

View file

@ -1,6 +1,6 @@
import { Store } from "../../Logic/UIEventSource" import { Store, UIEventSource } from "../../Logic/UIEventSource"
import MultiApply from "./MultiApply" import MultiApply from "./MultiApply"
import { SpecialVisualization } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
export class MultiApplyViz implements SpecialVisualization { export class MultiApplyViz implements SpecialVisualization {
funcName = "multi_apply" funcName = "multi_apply"
@ -31,7 +31,11 @@ export class MultiApplyViz implements SpecialVisualization {
example = example =
"{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}" "{multi_apply(_features_with_the_same_name_within_100m, name:etymology:wikidata;name:etymology, Apply etymology information on all nearby objects with the same name)}"
constr(state, tagsSource, args) { constr(
state: SpecialVisualizationState,
tagsSource: UIEventSource<Record<string, string>>,
args: string[]
) {
const featureIdsKey = args[0] const featureIdsKey = args[0]
const keysToApply = args[1].split(";") const keysToApply = args[1].split(";")
const text = args[2] const text = args[2]

View file

@ -1,6 +1,4 @@
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import { DefaultGuiState } from "../DefaultGuiState"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import { GeoOperations } from "../../Logic/GeoOperations" import { GeoOperations } from "../../Logic/GeoOperations"
@ -19,7 +17,7 @@ import { VariableUiElement } from "../Base/VariableUIElement"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import Title from "../Base/Title" import Title from "../Base/Title"
import { MapillaryLinkVis } from "./MapillaryLinkVis" import { MapillaryLinkVis } from "./MapillaryLinkVis"
import { SpecialVisualization } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
export class NearbyImageVis implements SpecialVisualization { export class NearbyImageVis implements SpecialVisualization {
args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [ args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [
@ -39,14 +37,13 @@ export class NearbyImageVis implements SpecialVisualization {
funcName = "nearby_images" funcName = "nearby_images"
constr( constr(
state: FeaturePipelineState, state: SpecialVisualizationState,
tagSource: UIEventSource<any>, tagSource: UIEventSource<Record<string, string>>,
args: string[], args: string[]
guistate: DefaultGuiState
): BaseUIElement { ): BaseUIElement {
const t = Translations.t.image.nearbyPictures const t = Translations.t.image.nearbyPictures
const mode: "open" | "expandable" | "collapsable" = <any>args[0] const mode: "open" | "expandable" | "collapsable" = <any>args[0]
const feature = state.allElements.ContainingFeatures.get(tagSource.data.id) const feature = state.indexedFeatures.featuresById.data.get(tagSource.data.id)
const [lon, lat] = GeoOperations.centerpointCoordinates(feature) const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
const id: string = tagSource.data["id"] const id: string = tagSource.data["id"]
const canBeEdited: boolean = !!id?.match("(node|way|relation)/-?[0-9]+") const canBeEdited: boolean = !!id?.match("(node|way|relation)/-?[0-9]+")
@ -69,7 +66,7 @@ export class NearbyImageVis implements SpecialVisualization {
} }
await state?.changes?.applyAction( await state?.changes?.applyAction(
new ChangeTagAction(id, new And(tags), tagSource.data, { new ChangeTagAction(id, new And(tags), tagSource.data, {
theme: state?.layoutToUse.id, theme: state?.layout.id,
changeType: "link-image", changeType: "link-image",
}) })
) )
@ -116,8 +113,8 @@ export class NearbyImageVis implements SpecialVisualization {
maxDaysOld: 365 * 3, maxDaysOld: 365 * 3,
} }
const slideshow = canBeEdited const slideshow = canBeEdited
? new SelectOneNearbyImage(options, state) ? new SelectOneNearbyImage(options, state.indexedFeatures)
: new NearbyImages(options, state) : new NearbyImages(options, state.indexedFeatures)
const controls = new Combine([ const controls = new Combine([
towardsCenter, towardsCenter,
new Combine([ new Combine([

View file

@ -13,9 +13,10 @@ import Translations from "../i18n/Translations"
import { Mapillary } from "../../Logic/ImageProviders/Mapillary" import { Mapillary } from "../../Logic/ImageProviders/Mapillary"
import { SubtleButton } from "../Base/SubtleButton" import { SubtleButton } from "../Base/SubtleButton"
import { GeoOperations } from "../../Logic/GeoOperations" import { GeoOperations } from "../../Logic/GeoOperations"
import { ElementStorage } from "../../Logic/ElementStorage"
import Lazy from "../Base/Lazy" import Lazy from "../Base/Lazy"
import P4C from "pic4carto" import P4C from "pic4carto"
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
export interface P4CPicture { export interface P4CPicture {
pictureUrl: string pictureUrl: string
date?: number date?: number
@ -47,15 +48,15 @@ export interface NearbyImageOptions {
} }
class ImagesInLoadedDataFetcher { class ImagesInLoadedDataFetcher {
private allElements: ElementStorage private indexedFeatures: IndexedFeatureSource
constructor(state: { allElements: ElementStorage }) { constructor(indexedFeatures: IndexedFeatureSource) {
this.allElements = state.allElements this.indexedFeatures = indexedFeatures
} }
public fetchAround(loc: { lon: number; lat: number; searchRadius?: number }): P4CPicture[] { public fetchAround(loc: { lon: number; lat: number; searchRadius?: number }): P4CPicture[] {
const foundImages: P4CPicture[] = [] const foundImages: P4CPicture[] = []
this.allElements.ContainingFeatures.forEach((feature) => { this.indexedFeatures.features.data.forEach((feature) => {
const props = feature.properties const props = feature.properties
const images = [] const images = []
if (props.image) { if (props.image) {
@ -100,7 +101,7 @@ class ImagesInLoadedDataFetcher {
} }
export default class NearbyImages extends Lazy { export default class NearbyImages extends Lazy {
constructor(options: NearbyImageOptions, state?: { allElements: ElementStorage }) { constructor(options: NearbyImageOptions, state?: IndexedFeatureSource) {
super(() => { super(() => {
const t = Translations.t.image.nearbyPictures const t = Translations.t.image.nearbyPictures
const shownImages = options.shownImagesCount ?? new UIEventSource(25) const shownImages = options.shownImagesCount ?? new UIEventSource(25)
@ -171,10 +172,7 @@ export default class NearbyImages extends Lazy {
) )
} }
private static buildPictureFetcher( private static buildPictureFetcher(options: NearbyImageOptions, state?: IndexedFeatureSource) {
options: NearbyImageOptions,
state?: { allElements: ElementStorage }
) {
const picManager = new P4C.PicturesManager({}) const picManager = new P4C.PicturesManager({})
const searchRadius = options.searchRadius ?? 500 const searchRadius = options.searchRadius ?? 500
@ -283,7 +281,7 @@ export class SelectOneNearbyImage extends NearbyImages implements InputElement<P
constructor( constructor(
options: NearbyImageOptions & { value?: UIEventSource<P4CPicture> }, options: NearbyImageOptions & { value?: UIEventSource<P4CPicture> },
state?: { allElements: ElementStorage } state?: IndexedFeatureSource
) { ) {
super(options, state) super(options, state)
this.value = options.value ?? new UIEventSource<P4CPicture>(undefined) this.value = options.value ?? new UIEventSource<P4CPicture>(undefined)

View file

@ -12,7 +12,7 @@ import Combine from "../Base/Combine"
import Svg from "../../Svg" import Svg from "../../Svg"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders" import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
import { SpecialVisualization } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization";
export class PlantNetDetectionViz implements SpecialVisualization { export class PlantNetDetectionViz implements SpecialVisualization {
funcName = "plantnet_detection" funcName = "plantnet_detection"
@ -27,7 +27,7 @@ export class PlantNetDetectionViz implements SpecialVisualization {
}, },
] ]
public constr(state, tags, args) { public constr(state: SpecialVisualizationState, tags: UIEventSource<Record<string, string>>, args: string[]) {
let imagePrefixes: string[] = undefined let imagePrefixes: string[] = undefined
if (args.length > 0) { if (args.length > 0) {
imagePrefixes = [].concat(...args.map((a) => a.split(","))) imagePrefixes = [].concat(...args.map((a) => a.split(",")))
@ -53,7 +53,7 @@ export class PlantNetDetectionViz implements SpecialVisualization {
]), ]),
tags.data, tags.data,
{ {
theme: state.layoutToUse.id, theme: state.layout.id,
changeType: "plantnet-ai-detection", changeType: "plantnet-ai-detection",
} }
) )

View file

@ -3,7 +3,7 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import ShareButton from "../BigComponents/ShareButton" import ShareButton from "../BigComponents/ShareButton"
import Svg from "../../Svg" import Svg from "../../Svg"
import { FixedUiElement } from "../Base/FixedUiElement" import { FixedUiElement } from "../Base/FixedUiElement"
import { SpecialVisualization } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization";
export class ShareLinkViz implements SpecialVisualization { export class ShareLinkViz implements SpecialVisualization {
funcName = "share_link" funcName = "share_link"
@ -17,12 +17,12 @@ export class ShareLinkViz implements SpecialVisualization {
}, },
] ]
public constr(state, tagSource: UIEventSource<any>, args) { public constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, args: string[]) {
if (window.navigator.share) { if (window.navigator.share) {
const generateShareData = () => { const generateShareData = () => {
const title = state?.layoutToUse?.title?.txt ?? "MapComplete" const title = state?.layout?.title?.txt ?? "MapComplete"
let matchingLayer: LayerConfig = state?.layoutToUse?.getMatchingLayer( let matchingLayer: LayerConfig = state?.layout?.getMatchingLayer(
tagSource?.data tagSource?.data
) )
let name = let name =
@ -41,7 +41,7 @@ export class ShareLinkViz implements SpecialVisualization {
return { return {
title: name, title: name,
url: url, url: url,
text: state?.layoutToUse?.shortDescription?.txt ?? "MapComplete", text: state?.layout?.shortDescription?.txt ?? "MapComplete",
} }
} }

View file

@ -4,7 +4,7 @@ import { VariableUiElement } from "../Base/VariableUIElement"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import EditableTagRendering from "./EditableTagRendering" import EditableTagRendering from "./EditableTagRendering"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import { SpecialVisualization } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
export class StealViz implements SpecialVisualization { export class StealViz implements SpecialVisualization {
funcName = "steal" funcName = "steal"
@ -21,12 +21,12 @@ export class StealViz implements SpecialVisualization {
required: true, required: true,
}, },
] ]
constr(state, featureTags, args) { constr(state: SpecialVisualizationState, featureTags, args) {
const [featureIdKey, layerAndtagRenderingIds] = args const [featureIdKey, layerAndtagRenderingIds] = args
const tagRenderings: [LayerConfig, TagRenderingConfig][] = [] const tagRenderings: [LayerConfig, TagRenderingConfig][] = []
for (const layerAndTagRenderingId of layerAndtagRenderingIds.split(";")) { for (const layerAndTagRenderingId of layerAndtagRenderingIds.split(";")) {
const [layerId, tagRenderingId] = layerAndTagRenderingId.trim().split(".") const [layerId, tagRenderingId] = layerAndTagRenderingId.trim().split(".")
const layer = state.layoutToUse.layers.find((l) => l.id === layerId) const layer = state.layout.layers.find((l) => l.id === layerId)
const tagRendering = layer.tagRenderings.find((tr) => tr.id === tagRenderingId) const tagRendering = layer.tagRenderings.find((tr) => tr.id === tagRenderingId)
tagRenderings.push([layer, tagRendering]) tagRenderings.push([layer, tagRendering])
} }
@ -39,7 +39,7 @@ export class StealViz implements SpecialVisualization {
if (featureId === undefined) { if (featureId === undefined) {
return undefined return undefined
} }
const otherTags = state.allElements.getEventSourceById(featureId) const otherTags = state.featureProperties.getStore(featureId)
const elements: BaseUIElement[] = [] const elements: BaseUIElement[] = []
for (const [layer, tagRendering] of tagRenderings) { for (const [layer, tagRendering] of tagRenderings) {
const el = new EditableTagRendering( const el = new EditableTagRendering(

View file

@ -11,10 +11,9 @@ import { And } from "../../Logic/Tags/And"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag" import { Tag } from "../../Logic/Tags/Tag"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Changes } from "../../Logic/Osm/Changes" import { Changes } from "../../Logic/Osm/Changes"
import { SpecialVisualization } from "../SpecialVisualization" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
export default class TagApplyButton implements AutoAction, SpecialVisualization { export default class TagApplyButton implements AutoAction, SpecialVisualization {
public readonly funcName = "tag_apply" public readonly funcName = "tag_apply"
@ -76,7 +75,10 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
return tgsSpec return tgsSpec
} }
public static generateTagsToApply(spec: string, tagSource: Store<any>): Store<Tag[]> { public static generateTagsToApply(
spec: string,
tagSource: Store<Record<string, string>>
): Store<Tag[]> {
// Check whether we need to look up a single value // Check whether we need to look up a single value
if (!spec.includes(";") && !spec.includes("=") && spec.includes("$")) { if (!spec.includes(";") && !spec.includes("=") && spec.includes("$")) {
@ -110,7 +112,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
async applyActionOn( async applyActionOn(
state: { state: {
layoutToUse: LayoutConfig layout: LayoutConfig
changes: Changes changes: Changes
}, },
tags: UIEventSource<any>, tags: UIEventSource<any>,
@ -125,7 +127,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
new And(tagsToApply.data), new And(tagsToApply.data),
tags.data, // We pass in the tags of the selected element, not the tags of the target element! tags.data, // We pass in the tags of the selected element, not the tags of the target element!
{ {
theme: state.layoutToUse.id, theme: state.layout.id,
changeType: "answer", changeType: "answer",
} }
) )
@ -133,8 +135,8 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
} }
public constr( public constr(
state: FeaturePipelineState, state: SpecialVisualizationState,
tags: UIEventSource<any>, tags: UIEventSource<Record<string, string>>,
args: string[] args: string[]
): BaseUIElement { ): BaseUIElement {
const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags) const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags)
@ -162,9 +164,9 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
const applyButton = new SubtleButton( const applyButton = new SubtleButton(
image, image,
new Combine([msg, tagsExplanation]).SetClass("flex flex-col") new Combine([msg, tagsExplanation]).SetClass("flex flex-col")
).onClick(() => { ).onClick(async () => {
self.applyActionOn(state, tags, args)
applied.setData(true) applied.setData(true)
await self.applyActionOn(state, tags, args)
}) })
return new Toggle( return new Toggle(

View file

Some files were not shown because too many files have changed in this diff Show more