refactoring
This commit is contained in:
parent
b94a8f5745
commit
5d0fe31c41
114 changed files with 2412 additions and 2958 deletions
|
@ -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(() => {
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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(
|
||||||
self.update(bbox)
|
(bbox) => {
|
||||||
})
|
self.update(bbox)
|
||||||
state.featurePipeline.runningQuery.addCallbackAndRun((_) =>
|
},
|
||||||
self.update(state.currentBounds.data)
|
[state.dataIsLoading]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
41
Logic/FeatureSource/Actors/GeoIndexedStore.ts
Normal file
41
Logic/FeatureSource/Actors/GeoIndexedStore.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
63
Logic/FeatureSource/Actors/TileLocalStorage.ts
Normal file
63
Logic/FeatureSource/Actors/TileLocalStorage.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
17
Logic/FeatureSource/Sources/ClippedFeatureSource.ts
Normal file
17
Logic/FeatureSource/Sources/ClippedFeatureSource.ts
Normal 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)))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) =>
|
||||||
self.update()
|
value.addCallback((_) => {
|
||||||
})
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 })
|
|
@ -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))
|
|
@ -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) {
|
|
@ -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])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
29
Logic/FeatureSource/Sources/TouchesBboxFeatureSource.ts
Normal file
29
Logic/FeatureSource/Sources/TouchesBboxFeatureSource.ts
Normal 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
|
||||||
|
}
|
|
@ -84,7 +84,9 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
mapProperties,
|
mapProperties,
|
||||||
{ isActive: options.isActive }
|
{
|
||||||
|
isActive: options?.isActive,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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++) {
|
||||||
|
|
|
@ -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[] = []
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
const oldCountry = feature.properties["_country"]
|
||||||
try {
|
const newCountry = countries[0].trim().toLowerCase()
|
||||||
const oldCountry = feature.properties["_country"]
|
if (oldCountry !== newCountry) {
|
||||||
feature.properties["_country"] = countries[0].trim().toLowerCase()
|
tagsSource.data["_country"] = newCountry
|
||||||
if (oldCountry !== feature.properties["_country"]) {
|
tagsSource?.ping()
|
||||||
const tagsSource = state?.allElements?.getEventSourceById(
|
|
||||||
feature.properties.id
|
|
||||||
)
|
|
||||||
tagsSource?.ping()
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(e)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((_) => {
|
.catch((e) => {
|
||||||
runningTasks.delete(feature)
|
console.warn(e)
|
||||||
})
|
})
|
||||||
|
.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,30 +194,37 @@ 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) => {
|
}
|
||||||
/*Note: also called by 'UpdateTagsFromOsmAPI'*/
|
|
||||||
|
|
||||||
const tgs = feature.properties
|
applyMetaTagsOnFeature(feature: Feature): boolean {
|
||||||
let movedSomething = false
|
/*Note: also called by 'UpdateTagsFromOsmAPI'*/
|
||||||
|
|
||||||
function move(src: string, target: string) {
|
const tgs = feature.properties
|
||||||
if (tgs[src] === undefined) {
|
let movedSomething = false
|
||||||
return
|
|
||||||
}
|
function move(src: string, target: string) {
|
||||||
tgs[target] = tgs[src]
|
if (tgs[src] === undefined) {
|
||||||
delete tgs[src]
|
return
|
||||||
movedSomething = true
|
|
||||||
}
|
}
|
||||||
|
tgs[target] = tgs[src]
|
||||||
move("user", "_last_edit:contributor")
|
delete tgs[src]
|
||||||
move("uid", "_last_edit:contributor:uid")
|
movedSomething = true
|
||||||
move("changeset", "_last_edit:changeset")
|
|
||||||
move("timestamp", "_last_edit:timestamp")
|
|
||||||
move("version", "_version_number")
|
|
||||||
return movedSomething
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
move("user", "_last_edit:contributor")
|
||||||
|
move("uid", "_last_edit:contributor:uid")
|
||||||
|
move("changeset", "_last_edit:changeset")
|
||||||
|
move("timestamp", "_last_edit:timestamp")
|
||||||
|
move("version", "_version_number")
|
||||||
|
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(
|
||||||
{
|
{
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[]>
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
stable.setData(list)
|
||||||
return
|
|
||||||
}
|
|
||||||
if (oldList === undefined || oldList.length !== list.length) {
|
|
||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
278
Models/ThemeViewState.ts
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
16
State.ts
16
State.ts
|
@ -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
13
UI/Base/Checkbox.svelte
Normal 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
15
UI/Base/Dropdown.svelte
Normal 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>
|
|
@ -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
18
UI/Base/IfNot.svelte
Normal 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
13
UI/Base/Loading.svelte
Normal 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>
|
|
@ -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>
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -14,54 +14,53 @@ 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(
|
||||||
let elementSelect = ""
|
(location) => {
|
||||||
if (objectId !== undefined) {
|
let elementSelect = ""
|
||||||
const parts = objectId.split("/")
|
if (objectId !== undefined) {
|
||||||
const tp = parts[0]
|
const parts = objectId.split("/")
|
||||||
if (
|
const tp = parts[0]
|
||||||
parts.length === 2 &&
|
if (
|
||||||
!isNaN(Number(parts[1])) &&
|
parts.length === 2 &&
|
||||||
(tp === "node" || tp === "way" || tp === "relation")
|
!isNaN(Number(parts[1])) &&
|
||||||
) {
|
(tp === "node" || tp === "way" || tp === "relation")
|
||||||
elementSelect = "&" + tp + "=" + parts[1]
|
) {
|
||||||
|
elementSelect = "&" + tp + "=" + parts[1]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${
|
||||||
const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${
|
mapProperties.zoom?.data ?? 0
|
||||||
location?.zoom ?? 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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
79
UI/BigComponents/Filterview.svelte
Normal file
79
UI/BigComponents/Filterview.svelte
Normal 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}
|
|
@ -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,
|
||||||
|
|
94
UI/BigComponents/Geosearch.svelte
Normal file
94
UI/BigComponents/Geosearch.svelte
Normal 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>
|
|
@ -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 }) {
|
||||||
|
|
75
UI/BigComponents/SelectedElementView.svelte
Normal file
75
UI/BigComponents/SelectedElementView.svelte
Normal 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>
|
|
@ -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,40 +19,33 @@ 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) {
|
const featuresForLayer = features
|
||||||
if (layer.name === undefined) {
|
if (featuresForLayer.length === 0) {
|
||||||
continue
|
return
|
||||||
}
|
|
||||||
const featuresForLayer = features
|
|
||||||
.filter((f) => f.layer === layer)
|
|
||||||
.map((f) => f.element)
|
|
||||||
if (featuresForLayer.length === 0) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8"))
|
|
||||||
|
|
||||||
const layerStats = []
|
|
||||||
for (const tagRendering of layer?.tagRenderings ?? []) {
|
|
||||||
const chart = new TagRenderingChart(featuresForLayer, tagRendering, {
|
|
||||||
chartclasses: "w-full",
|
|
||||||
chartstyle: "height: 60rem",
|
|
||||||
includeTitle: false,
|
|
||||||
})
|
|
||||||
const title = new Title(
|
|
||||||
tagRendering.question?.Clone() ?? tagRendering.id,
|
|
||||||
4
|
|
||||||
).SetClass("mt-8")
|
|
||||||
if (!chart.HasClass("hidden")) {
|
|
||||||
layerStats.push(
|
|
||||||
new Combine([title, chart]).SetClass(
|
|
||||||
"flex flex-col w-full lg:w-1/3"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
els.push(new Combine(layerStats).SetClass("flex flex-wrap"))
|
|
||||||
}
|
}
|
||||||
|
els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8"))
|
||||||
|
|
||||||
|
const layerStats = []
|
||||||
|
for (const tagRendering of layer?.tagRenderings ?? []) {
|
||||||
|
const chart = new TagRenderingChart(featuresForLayer, tagRendering, {
|
||||||
|
chartclasses: "w-full",
|
||||||
|
chartstyle: "height: 60rem",
|
||||||
|
includeTitle: false,
|
||||||
|
})
|
||||||
|
const title = new Title(
|
||||||
|
tagRendering.question?.Clone() ?? tagRendering.id,
|
||||||
|
4
|
||||||
|
).SetClass("mt-8")
|
||||||
|
if (!chart.HasClass("hidden")) {
|
||||||
|
layerStats.push(
|
||||||
|
new Combine([title, chart]).SetClass(
|
||||||
|
"flex flex-col w-full lg:w-1/3"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
els.push(new Combine(layerStats).SetClass("flex flex-wrap"))
|
||||||
return new Combine(els)
|
return new Combine(els)
|
||||||
},
|
},
|
||||||
[Locale.language]
|
[Locale.language]
|
||||||
|
|
|
@ -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> }
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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>
|
||||||
},
|
},
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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,25 +22,17 @@ 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>
|
|
||||||
|
private readonly sources: Record<string, UIEventSource<boolean>> = {
|
||||||
|
welcome: this.welcomeMessageIsOpened,
|
||||||
|
download: this.downloadControlIsOpened,
|
||||||
|
filters: this.filterViewIsOpened,
|
||||||
|
copyright: this.copyrightViewIsOpened,
|
||||||
|
currentview: this.currentViewControlIsOpened,
|
||||||
|
userinfo: this.userInfoIsOpened,
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.welcomeMessageOpenedTab = UIEventSource.asFloat(
|
|
||||||
QueryParameters.GetQueryParameter(
|
|
||||||
"tab",
|
|
||||||
"0",
|
|
||||||
`The tab that is shown in the welcome-message.`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
const sources = {
|
|
||||||
welcome: this.welcomeMessageIsOpened,
|
|
||||||
download: this.downloadControlIsOpened,
|
|
||||||
filters: this.filterViewIsOpened,
|
|
||||||
copyright: this.copyrightViewIsOpened,
|
|
||||||
currentview: this.currentViewControlIsOpened,
|
|
||||||
userinfo: this.userInfoIsOpened,
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 }[]) => {
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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,28 +40,26 @@ 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,
|
location: this._location,
|
||||||
location: this._location,
|
attribution: true,
|
||||||
attribution: true,
|
leafletOptions: {
|
||||||
leafletOptions: {
|
tap: true,
|
||||||
tap: true,
|
},
|
||||||
},
|
})
|
||||||
})
|
|
||||||
|
|
||||||
layerControl = new BackgroundMapSwitch(
|
layerControl = new BackgroundMapSwitch(
|
||||||
{
|
{
|
||||||
locationControl: this._location,
|
locationControl: this._location,
|
||||||
backgroundLayer: this.background,
|
backgroundLayer: this.background,
|
||||||
},
|
},
|
||||||
this.background,
|
this.background,
|
||||||
{
|
{
|
||||||
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);`
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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>>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,12 +67,11 @@ 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(
|
||||||
|
@ -80,12 +84,11 @@ 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"] ?? "") !== "")
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -153,8 +154,8 @@ class ApplyButton extends UIElement {
|
||||||
if (specialRenderings.length == 0) {
|
if (specialRenderings.length == 0) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"AutoApply: feature " +
|
"AutoApply: feature " +
|
||||||
targetFeatureId +
|
targetFeatureId +
|
||||||
" got a rendering without supported auto actions:",
|
" got a rendering without supported auto actions:",
|
||||||
rendering
|
rendering
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -224,7 +225,7 @@ export default class AutoApplyButton implements SpecialVisualization {
|
||||||
"To effectively use this button, you'll need some ingredients:",
|
"To effectively use this button, you'll need some ingredients:",
|
||||||
new List([
|
new List([
|
||||||
"A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " +
|
"A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " +
|
||||||
supportedActions.join(", "),
|
supportedActions.join(", "),
|
||||||
"A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the layer ",
|
"A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the layer ",
|
||||||
new Link("current_view", "./BuiltinLayers.md#current_view"),
|
new Link("current_view", "./BuiltinLayers.md#current_view"),
|
||||||
"Then, use a calculated tag on the host feature to determine the overlapping object ids",
|
"Then, use a calculated tag on the host feature to determine the overlapping object ids",
|
||||||
|
@ -234,18 +235,17 @@ 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 ===
|
||||||
OsmConnection.oauth_configs["osm-test"].url
|
OsmConnection.oauth_configs["osm-test"].url
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
const t = Translations.t.general.add.import
|
const t = Translations.t.general.add.import
|
||||||
|
|
|
@ -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))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}",
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
})*/
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
newTags: args.newTags.data,
|
feature,
|
||||||
})
|
wayToConflate,
|
||||||
|
{
|
||||||
|
theme: state.layout.id,
|
||||||
|
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,
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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([
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
0
UI/Popup/TagRenderingAnswer.svelte
Normal file
0
UI/Popup/TagRenderingAnswer.svelte
Normal file
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue