diff --git a/Logic/BBox.ts b/Logic/BBox.ts index 7289b793ba..fb0625886a 100644 --- a/Logic/BBox.ts +++ b/Logic/BBox.ts @@ -145,6 +145,15 @@ export class BBox { this.maxLat + latDiff]]) } + padAbsolute(degrees: number): BBox { + + return new BBox([[ + this.minLon - degrees, + this.minLat - degrees + ], [this.maxLon + degrees, + this.maxLat + degrees]]) + } + toLeaflet() { return [[this.minLat, this.minLon], [this.maxLat, this.maxLon]] } diff --git a/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts b/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts index 5b884ac87b..ad19b6f1ce 100644 --- a/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts +++ b/Logic/FeatureSource/Actors/SaveTileToLocalStorageActor.ts @@ -1,8 +1,3 @@ -/*** - * 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 ay - */ import FeatureSource, {Tiled} from "../FeatureSource"; import {Tiles} from "../../../Models/TileRange"; import {IdbLocalStorage} from "../../Web/IdbLocalStorage"; @@ -13,6 +8,11 @@ import SimpleFeatureSource from "../Sources/SimpleFeatureSource"; import FilteredLayer from "../../../Models/FilteredLayer"; import Loc from "../../../Models/Loc"; +/*** + * 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 ay + */ export default class SaveTileToLocalStorageActor { private readonly visitedTiles: UIEventSource> private readonly _layer: LayerConfig; diff --git a/Logic/ImageProviders/Imgur.ts b/Logic/ImageProviders/Imgur.ts index b06b2a6c0e..d7de993c00 100644 --- a/Logic/ImageProviders/Imgur.ts +++ b/Logic/ImageProviders/Imgur.ts @@ -44,7 +44,7 @@ export class Imgur extends ImageProvider { } - static uploadImage(title: string, description: string, blob, + static uploadImage(title: string, description: string, blob: File, handleSuccessfullUpload: ((imageURL: string) => void), onFail: (reason: string) => void) { diff --git a/Logic/Osm/Actions/DeleteAction.ts b/Logic/Osm/Actions/DeleteAction.ts index 4673fecada..a9d404ca34 100644 --- a/Logic/Osm/Actions/DeleteAction.ts +++ b/Logic/Osm/Actions/DeleteAction.ts @@ -1,4 +1,3 @@ -import State from "../../../State"; import {OsmObject} from "../OsmObject"; import OsmChangeAction from "./OsmChangeAction"; import {Changes} from "../Changes"; @@ -52,7 +51,7 @@ export default class DeleteAction extends OsmChangeAction { return await new ChangeTagAction( this._id, this._softDeletionTags, osmObject.tags, { - theme: State.state?.layoutToUse?.id ?? "unkown", + ... this.meta, changeType: "soft-delete" } ).CreateChangeDescriptions(changes) diff --git a/Logic/Osm/Overpass.ts b/Logic/Osm/Overpass.ts index df7fe5619c..f941fe2ba9 100644 --- a/Logic/Osm/Overpass.ts +++ b/Logic/Osm/Overpass.ts @@ -9,7 +9,6 @@ import * as osmtogeojson from "osmtogeojson"; * Interfaces overpass to get all the latest data */ export class Overpass { - public static testUrl: string = null private _filter: TagsFilter private readonly _interpreterUrl: string; private readonly _timeout: UIEventSource; @@ -36,10 +35,6 @@ export class Overpass { let query = this.buildQuery("[bbox:" + bounds.getSouth() + "," + bounds.getWest() + "," + bounds.getNorth() + "," + bounds.getEast() + "]") - if (Overpass.testUrl !== null) { - console.log("Using testing URL") - query = Overpass.testUrl; - } const self = this; const json = await Utils.downloadJson(query) diff --git a/Logic/State/FeaturePipelineState.ts b/Logic/State/FeaturePipelineState.ts index 90f0539e1f..02c2cd1bfa 100644 --- a/Logic/State/FeaturePipelineState.ts +++ b/Logic/State/FeaturePipelineState.ts @@ -9,6 +9,7 @@ import MapState from "./MapState"; import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"; import Hash from "../Web/Hash"; import {BBox} from "../BBox"; +import FeatureInfoBox from "../../UI/Popup/FeatureInfoBox"; export default class FeaturePipelineState extends MapState { @@ -93,8 +94,9 @@ export default class FeaturePipelineState extends MapState { leafletMap: self.leafletMap, layerToShow: source.layer.layerDef, doShowLayer: doShowFeatures, - allElements: self.allElements, - selectedElement: self.selectedElement + selectedElement: self.selectedElement, + state: self, + popup: (tags, layer) => new FeatureInfoBox(tags, layer, self) } ); }, this @@ -112,11 +114,13 @@ export default class FeaturePipelineState extends MapState { */ public AddClusteringToMap(leafletMap: UIEventSource) { const clustering = this.layoutToUse.clustering + const self = this; new ShowDataLayer({ features: this.featureAggregator.getCountsForZoom(clustering, this.locationControl, clustering.minNeededElements), leafletMap: leafletMap, layerToShow: ShowTileInfo.styling, - enablePopups: this.featureSwitchIsDebugging.data, + popup: this.featureSwitchIsDebugging.data ? (tags, layer) => new FeatureInfoBox(tags, layer, self) : undefined, + state: this }) } diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index 28f2152575..e5b92efefd 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -41,6 +41,12 @@ export class UIEventSource { source.addCallback((latestData) => { sink.setData(latestData?.data); + latestData.addCallback(data => { + if(source.data !== latestData){ + return true; + } + sink.setData(data) + }) }); for (const possibleSource of possibleSources ?? []) { diff --git a/Logic/Web/IdbLocalStorage.ts b/Logic/Web/IdbLocalStorage.ts index 6850c3b6fd..28300a3057 100644 --- a/Logic/Web/IdbLocalStorage.ts +++ b/Logic/Web/IdbLocalStorage.ts @@ -8,12 +8,17 @@ import {Utils} from "../../Utils"; export class IdbLocalStorage { - public static Get(key: string, options: { defaultValue?: T }): UIEventSource{ - const src = new UIEventSource(options.defaultValue, "idb-local-storage:"+key) + public static Get(key: string, options?: { defaultValue?: T , whenLoaded?: (t: T) => void}): UIEventSource{ + const src = new UIEventSource(options?.defaultValue, "idb-local-storage:"+key) if(Utils.runningFromConsole){ return src; } - idb.get(key).then(v => src.setData(v ?? options.defaultValue)) + idb.get(key).then(v => { + src.setData(v ?? options?.defaultValue); + if(options?.whenLoaded !== undefined){ + options?.whenLoaded(v) + } + }) src.addCallback(v => idb.set(key, v)) return src; diff --git a/Models/Constants.ts b/Models/Constants.ts index ff2f23fe43..b49c1df841 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -22,7 +22,7 @@ export default class Constants { /** * Layer IDs of layers which have special properties through built-in hooks */ - public static readonly priviliged_layers: string[] = [...Constants.added_by_default, "type_node", "note", ...Constants.no_include] + public static readonly priviliged_layers: string[] = [...Constants.added_by_default, "type_node", "note","import_candidate", ...Constants.no_include] // The user journey states thresholds when a new feature gets unlocked diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index 1b9d076418..7466f65ce4 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.ts @@ -19,10 +19,13 @@ export interface MinimapOptions { export interface MinimapObj { readonly leafletMap: UIEventSource, + readonly location: UIEventSource; + readonly bounds: UIEventSource; installBounds(factor: number | BBox, showRange?: boolean): void TakeScreenshot(): Promise; + } export default class Minimap { diff --git a/UI/Base/MinimapImplementation.ts b/UI/Base/MinimapImplementation.ts index 2f70f85510..e261650f37 100644 --- a/UI/Base/MinimapImplementation.ts +++ b/UI/Base/MinimapImplementation.ts @@ -11,19 +11,20 @@ import {BBox} from "../../Logic/BBox"; import 'leaflet-polylineoffset' import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter"; import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"; +import AvailableBaseLayersImplementation from "../../Logic/Actors/AvailableBaseLayersImplementation"; export default class MinimapImplementation extends BaseUIElement implements MinimapObj { private static _nextId = 0; public readonly leafletMap: UIEventSource private readonly _id: string; private readonly _background: UIEventSource; - private readonly _location: UIEventSource; + public readonly location: UIEventSource; private _isInited = false; private _allowMoving: boolean; private readonly _leafletoptions: any; private readonly _onFullyLoaded: (leaflet: L.Map) => void private readonly _attribution: BaseUIElement | boolean; - private readonly _bounds: UIEventSource | undefined; + public readonly bounds: UIEventSource | undefined; private readonly _addLayerControl: boolean; private readonly _options: MinimapOptions; @@ -32,8 +33,8 @@ export default class MinimapImplementation extends BaseUIElement implements Mini options = options ?? {} this.leafletMap = options.leafletMap ?? new UIEventSource(undefined) this._background = options?.background ?? new UIEventSource(AvailableBaseLayers.osmCarto) - this._location = options?.location ?? new UIEventSource({lat: 0, lon: 0, zoom: 1}) - this._bounds = options?.bounds; + this.location = options?.location ?? new UIEventSource({lat: 0, lon: 0, zoom: 1}) + this.bounds = options?.bounds; this._id = "minimap" + MinimapImplementation._nextId; this._allowMoving = options.allowMoving ?? true; this._leafletoptions = options.leafletOptions ?? {} @@ -46,6 +47,7 @@ export default class MinimapImplementation extends BaseUIElement implements Mini } public static initialize() { + AvailableBaseLayers.implement(new AvailableBaseLayersImplementation()) Minimap.createMiniMap = options => new MinimapImplementation(options) } @@ -153,7 +155,7 @@ export default class MinimapImplementation extends BaseUIElement implements Mini if (this._addLayerControl) { const switcher = new BackgroundMapSwitch({ - locationControl: this._location, + locationControl: this.location, backgroundLayer: this._background }, this._background @@ -180,7 +182,7 @@ export default class MinimapImplementation extends BaseUIElement implements Mini return; } this._isInited = true; - const location = this._location; + const location = this.location; const self = this; let currentLayer = this._background.data.layer() let latLon = <[number, number]>[location.data?.lat ?? 0, location.data?.lon ?? 0] @@ -268,8 +270,8 @@ export default class MinimapImplementation extends BaseUIElement implements Mini isRecursing = true; location.ping(); - if (self._bounds !== undefined) { - self._bounds.setData(BBox.fromLeafletBounds(map.getBounds())) + if (self.bounds !== undefined) { + self.bounds.setData(BBox.fromLeafletBounds(map.getBounds())) } @@ -295,8 +297,8 @@ export default class MinimapImplementation extends BaseUIElement implements Mini } }) - if (self._bounds !== undefined) { - self._bounds.setData(BBox.fromLeafletBounds(map.getBounds())) + if (self.bounds !== undefined) { + self.bounds.setData(BBox.fromLeafletBounds(map.getBounds())) } diff --git a/UI/Base/ScrollableFullScreen.ts b/UI/Base/ScrollableFullScreen.ts index 96fdb2eb00..3985ed16d2 100644 --- a/UI/Base/ScrollableFullScreen.ts +++ b/UI/Base/ScrollableFullScreen.ts @@ -50,10 +50,14 @@ export default class ScrollableFullScreen extends UIElement { self.Activate(); } else { // Some cleanup... - ScrollableFullScreen.empty.AttachTo("fullscreen") + const fs = document.getElementById("fullscreen"); + if(fs !== null){ + ScrollableFullScreen.empty.AttachTo("fullscreen") + fs.classList.add("hidden") + } + ScrollableFullScreen._currentlyOpen?.isShown?.setData(false); - fs.classList.add("hidden") } }) diff --git a/UI/BigComponents/Histogram.ts b/UI/BigComponents/Histogram.ts index 015fdbe302..2051257ae3 100644 --- a/UI/BigComponents/Histogram.ts +++ b/UI/BigComponents/Histogram.ts @@ -127,7 +127,7 @@ export default class Histogram extends VariableUiElement { ]), keys.map(_ => ["width: 20%"]) - ).SetClass("w-full"); + ).SetClass("w-full zebra-table"); }, [sortMode])); } } \ No newline at end of file diff --git a/UI/BigComponents/LeftControls.ts b/UI/BigComponents/LeftControls.ts index a996416f59..e3dc0b151a 100644 --- a/UI/BigComponents/LeftControls.ts +++ b/UI/BigComponents/LeftControls.ts @@ -7,40 +7,16 @@ import Svg from "../../Svg"; import AllDownloads from "./AllDownloads"; import FilterView from "./FilterView"; import {UIEventSource} from "../../Logic/UIEventSource"; -import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; -import Loc from "../../Models/Loc"; -import {BBox} from "../../Logic/BBox"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import FilteredLayer from "../../Models/FilteredLayer"; -import BaseLayer from "../../Models/BaseLayer"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; import BackgroundMapSwitch from "./BackgroundMapSwitch"; -import {FeatureSourceForLayer} from "../../Logic/FeatureSource/FeatureSource"; import Lazy from "../Base/Lazy"; import {VariableUiElement} from "../Base/VariableUIElement"; import FeatureInfoBox from "../Popup/FeatureInfoBox"; -import {ElementStorage} from "../../Logic/ElementStorage"; -import FeatureSwitchState from "../../Logic/State/FeatureSwitchState"; import CopyrightPanel from "./CopyrightPanel"; +import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; export default class LeftControls extends Combine { - constructor(state: FeatureSwitchState & { - allElements: ElementStorage; - currentView: FeatureSourceForLayer; - featureSwitchBackgroundSelection: UIEventSource; - layoutToUse: LayoutConfig, - featurePipeline: FeaturePipeline, - currentBounds: UIEventSource, - locationControl: UIEventSource, - overlayToggles: any, - featureSwitchEnableExport: UIEventSource, - featureSwitchExportAsPdf: UIEventSource, - filteredLayers: UIEventSource, - featureSwitchFilter: UIEventSource, - backgroundLayer: UIEventSource, - osmConnection: OsmConnection - }, + constructor(state: FeaturePipelineState, guiState: { currentViewControlIsOpened: UIEventSource; downloadControlIsOpened: UIEventSource, @@ -74,7 +50,7 @@ export default class LeftControls extends Combine { } return new Lazy(() => { const tagsSource = state.allElements.getEventSourceById(feature.properties.id) - return new FeatureInfoBox(tagsSource, currentViewFL.layerDef, "currentview", guiState.currentViewControlIsOpened) + return new FeatureInfoBox(tagsSource, currentViewFL.layerDef,state, "currentview", guiState.currentViewControlIsOpened) .SetClass("md:floating-element-width") }) })) diff --git a/UI/DefaultGUI.ts b/UI/DefaultGUI.ts index d31c701613..b7cf6a7388 100644 --- a/UI/DefaultGUI.ts +++ b/UI/DefaultGUI.ts @@ -61,7 +61,6 @@ export default class DefaultGUI { const hasPresets = state.layoutToUse.layers.some(layer => layer.presets.length > 0); const noteLayer: FilteredLayer = state.filteredLayers.data.filter(l => l.layerDef.id === "note")[0] let addNewNoteDialog: (isShown: UIEventSource) => BaseUIElement = undefined; - const t = Translations.t.notes if (noteLayer !== undefined) { addNewNoteDialog = (isShown) => new NewNoteUi(noteLayer, isShown, state) } @@ -144,7 +143,8 @@ export default class DefaultGUI { leafletMap: state.leafletMap, layerToShow: new LayerConfig(home_location_json, "all_known_layers", true), features: state.homeLocation, - enablePopups: false, + popup: undefined, + state }) state.leafletMap.addCallbackAndRunD(_ => { diff --git a/UI/ExportPDF.ts b/UI/ExportPDF.ts index 6f27794f85..c229e8c032 100644 --- a/UI/ExportPDF.ts +++ b/UI/ExportPDF.ts @@ -91,8 +91,9 @@ export default class ExportPDF { features: tile, leafletMap: minimap.leafletMap, layerToShow: tile.layer.layerDef, - enablePopups: false, - doShowLayer: tile.layer.isDisplayed + popup: undefined, + doShowLayer: tile.layer.isDisplayed, + state: undefined } ) }) diff --git a/UI/ImportFlow/ConflationChecker.ts b/UI/ImportFlow/ConflationChecker.ts new file mode 100644 index 0000000000..0de95de1e5 --- /dev/null +++ b/UI/ImportFlow/ConflationChecker.ts @@ -0,0 +1,242 @@ +import {BBox} from "../../Logic/BBox"; +import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; +import Combine from "../Base/Combine"; +import Title from "../Base/Title"; +import {Overpass} from "../../Logic/Osm/Overpass"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Constants from "../../Models/Constants"; +import RelationsTracker from "../../Logic/Osm/RelationsTracker"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import {FlowStep} from "./FlowStep"; +import Loading from "../Base/Loading"; +import {SubtleButton} from "../Base/SubtleButton"; +import Svg from "../../Svg"; +import {Utils} from "../../Utils"; +import {IdbLocalStorage} from "../../Logic/Web/IdbLocalStorage"; +import Minimap from "../Base/Minimap"; +import BaseLayer from "../../Models/BaseLayer"; +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; +import Loc from "../../Models/Loc"; +import Attribution from "../BigComponents/Attribution"; +import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; +import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; +import ValidatedTextField from "../Input/ValidatedTextField"; +import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource"; +import * as currentview from "../../assets/layers/current_view/current_view.json" +import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json" +import {GeoOperations} from "../../Logic/GeoOperations"; +import FeatureInfoBox from "../Popup/FeatureInfoBox"; +/** + * Given the data to import, the bbox and the layer, will query overpass for similar items + */ +export default class ConflationChecker extends Combine implements FlowStep { + + public readonly IsValid + public readonly Value + + constructor( + state, + params: { bbox: BBox, layer: LayerConfig, geojson: any }) { + + + const bbox = params.bbox.padAbsolute(0.0001) + const layer = params.layer; + const toImport = params.geojson; + let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached" >("idle") + + const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, { + whenLoaded: (v) => { + if (v !== undefined) { + console.log("Loaded from local storage:", v) + const [geojson, date] = v; + const timeDiff = (new Date().getTime() - date.getTime()) / 1000; + console.log("The cache is ", timeDiff, "seconds old") + if (timeDiff < 24 * 60 * 60) { + // Recently cached! + overpassStatus.setData("cached") + return; + } + } + // Load the data! + const url = Constants.defaultOverpassUrls[1] + const relationTracker = new RelationsTracker() + const overpass = new Overpass(params.layer.source.osmTags, [], url, new UIEventSource(180), relationTracker, true) + console.log("Loading from overpass!") + overpassStatus.setData("running") + overpass.queryGeoJson(bbox).then( + ([data, date] ) => { + console.log("Received overpass-data: ", data.features.length,"features are loaded at ", date); + overpassStatus.setData("success") + fromLocalStorage.setData([data, date]) + }, + (error) => {overpassStatus.setData({error})}) + + } + }); + + + const geojson : UIEventSource = fromLocalStorage.map(d => { + if (d === undefined) { + return undefined + } + return d[0] + }) + + const background = new UIEventSource(AvailableBaseLayers.osmCarto) + const location = new UIEventSource({lat: 0, lon: 0, zoom: 1}) + const currentBounds = new UIEventSource(undefined) + const zoomLevel = ValidatedTextField.InputForType("pnat") + zoomLevel.SetClass("ml-1 border border-black") + zoomLevel.GetValue().syncWith(LocalStorageSource.Get("importer-zoom-level", "14"), true) + const osmLiveData = Minimap.createMiniMap({ + allowMoving: true, + location, + background, + bounds: currentBounds, + attribution: new Attribution(location, state.osmConnection.userDetails, undefined, currentBounds) + }) + osmLiveData.SetClass("w-full").SetStyle("height: 500px") + const preview = new StaticFeatureSource(geojson.map(geojson => { + if(geojson?.features === undefined){ + return [] + } + const zoomedEnough: boolean = osmLiveData.location.data.zoom >= Number(zoomLevel.GetValue().data) + if(!zoomedEnough){ + return [] + } + const bounds = osmLiveData.bounds.data + return geojson.features.filter(f => BBox.get(f).overlapsWith(bounds)) + }, [osmLiveData.bounds, zoomLevel.GetValue()]), false); + + + + new ShowDataLayer({ + layerToShow:new LayerConfig(currentview), + state, + leafletMap: osmLiveData.leafletMap, + enablePopups: undefined, + zoomToFeatures: true, + features: new StaticFeatureSource([ + bbox.asGeoJson({}) + ], false) + }) + + + new ShowDataLayer({ + layerToShow:layer, + state, + leafletMap: osmLiveData.leafletMap, + popup: (tags, layer) => new FeatureInfoBox(tags, layer, state), + zoomToFeatures: false, + features: preview + }) + + new ShowDataLayer({ + layerToShow:new LayerConfig(import_candidate), + state, + leafletMap: osmLiveData.leafletMap, + popup: (tags, layer) => new FeatureInfoBox(tags, layer, state), + zoomToFeatures: false, + features: new StaticFeatureSource(toImport.features, false) + }) + + const nearbyCutoff = ValidatedTextField.InputForType("pnat") + nearbyCutoff.SetClass("ml-1 border border-black") + nearbyCutoff.GetValue().syncWith(LocalStorageSource.Get("importer-cutoff", "25"), true) + + const matchedFeaturesMap = Minimap.createMiniMap({ + allowMoving: true, + background + }) + matchedFeaturesMap.SetClass("w-full").SetStyle("height: 500px") + + // Featuresource showing OSM-features which are nearby a toImport-feature + const nearbyFeatures = new StaticFeatureSource(geojson.map(osmData => { + if(osmData?.features === undefined){ + return [] + } + const maxDist = Number(nearbyCutoff.GetValue().data) + return osmData.features.filter(f => + toImport.features.some(imp => + maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) ) + }, [nearbyCutoff.GetValue()]), false); + + // Featuresource showing OSM-features which are nearby a toImport-feature + const toImportWithNearby = new StaticFeatureSource(geojson.map(osmData => { + if(osmData?.features === undefined){ + return [] + } + const maxDist = Number(nearbyCutoff.GetValue().data) + return toImport.features.filter(imp => + osmData.features.some(f => + maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) ) + }, [nearbyCutoff.GetValue()]), false); + + new ShowDataLayer({ + layerToShow:layer, + state, + leafletMap: matchedFeaturesMap.leafletMap, + popup: (tags, layer) => new FeatureInfoBox(tags, layer, state), + zoomToFeatures: true, + features: nearbyFeatures + }) + + new ShowDataLayer({ + layerToShow:new LayerConfig(import_candidate), + state, + leafletMap: matchedFeaturesMap.leafletMap, + popup: (tags, layer) => new FeatureInfoBox(tags, layer, state), + zoomToFeatures: false, + features: toImportWithNearby + }) + + + super([ + new Title("Comparison with existing data"), + new VariableUiElement(overpassStatus.map(d => { + if (d === "idle") { + return new Loading("Checking local storage...") + } + if (d["error"] !== undefined) { + return new FixedUiElement("Could not load latest data from overpass: " + d["error"]).SetClass("alert") + } + if(d === "running"){ + return new Loading("Querying overpass...") + } + if(d === "cached"){ + return new FixedUiElement("Fetched data from local storage") + } + if(d === "success"){ + return new FixedUiElement("Data loaded") + } + return new FixedUiElement("Unexpected state "+d).SetClass("alert") + })), + new VariableUiElement( + geojson.map(geojson => { + if (geojson === undefined) { + return undefined; + } + return new SubtleButton(Svg.download_svg(), "Download the loaded geojson from overpass").onClick(() => { + Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "), "mapcomplete-" + layer.id + ".geojson", { + mimetype: "application/json+geo" + }) + }); + })), + + new Title("Live data on OSM"), + osmLiveData, + new Combine(["The live data is shown if the zoomlevel is at least ", zoomLevel, ". The current zoom level is ", new VariableUiElement(osmLiveData.location.map(l => ""+l.zoom))]).SetClass("flex"), + + new Title("Nearby features"), + new Combine([ "The following map shows features to import which have an OSM-feature within ", nearbyCutoff, "meter"]).SetClass("flex"), + new FixedUiElement("The red elements on the following map will not be imported!").SetClass("alert"), + "Set the range to 0 or 1 if you want to import them all", + matchedFeaturesMap + ]) + + this.IsValid = new UIEventSource(false) + this.Value = new UIEventSource(undefined) + } + +} \ No newline at end of file diff --git a/UI/ImportFlow/DataPanel.ts b/UI/ImportFlow/DataPanel.ts new file mode 100644 index 0000000000..85b3a95150 --- /dev/null +++ b/UI/ImportFlow/DataPanel.ts @@ -0,0 +1,156 @@ +import Combine from "../Base/Combine"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {BBox} from "../../Logic/BBox"; +import UserRelatedState from "../../Logic/State/UserRelatedState"; +import Translations from "../i18n/Translations"; +import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts"; +import Constants from "../../Models/Constants"; +import {DropDown} from "../Input/DropDown"; +import {Utils} from "../../Utils"; +import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; +import BaseLayer from "../../Models/BaseLayer"; +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; +import Loc from "../../Models/Loc"; +import Minimap from "../Base/Minimap"; +import Attribution from "../BigComponents/Attribution"; +import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; +import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; +import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; +import Toggle from "../Input/Toggle"; +import Table from "../Base/Table"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import {FlowStep} from "./FlowStep"; +import {Layer} from "leaflet"; + +/** + * Shows the data to import on a map, asks for the correct layer to be selected + */ +export class DataPanel extends Combine implements FlowStep<{ bbox: BBox, layer: LayerConfig, geojson: any }>{ + public readonly IsValid: UIEventSource; + public readonly Value: UIEventSource<{ bbox: BBox, layer: LayerConfig, geojson: any }> + + constructor( + state: UserRelatedState, + geojson: { features: { properties: any, geometry: { coordinates: [number, number] } }[] }) { + const t = Translations.t.importHelper; + + const propertyKeys = new Set() + console.log("Datapanel input got ", geojson) + for (const f of geojson.features) { + Object.keys(f.properties).forEach(key => propertyKeys.add(key)) + } + + + const availableLayers = AllKnownLayouts.AllPublicLayers().filter(l => l.name !== undefined && Constants.priviliged_layers.indexOf(l.id) < 0) + const layerPicker = new DropDown("Which layer does this import match with?", + [{shown: t.selectLayer, value: undefined}].concat(availableLayers.map(l => ({ + shown: l.name, + value: l + }))) + ) + + let autodetected = new UIEventSource(false) + for (const layer of availableLayers) { + const mismatched = geojson.features.some(f => + !layer.source.osmTags.matchesProperties(f.properties) + ) + if (!mismatched) { + layerPicker.GetValue().setData(layer); + layerPicker.GetValue().addCallback(_ => autodetected.setData(false)) + autodetected.setData(true) + break; + } + } + + const withId = geojson.features.map((f, i) => { + const copy = Utils.Clone(f) + copy.properties.id = "to-import/" + i + return copy + }) + + const matching: UIEventSource<{ properties: any, geometry: { coordinates: [number, number] } }[]> = layerPicker.GetValue().map((layer: LayerConfig) => { + if (layer === undefined) { + return undefined; + } + const matching: { properties: any, geometry: { coordinates: [number, number] } }[] = [] + + for (const feature of withId) { + if (layer.source.osmTags.matchesProperties(feature.properties)) { + matching.push(feature) + } + } + + return matching + }) + const background = new UIEventSource(AvailableBaseLayers.osmCarto) + const location = new UIEventSource({lat: 0, lon: 0, zoom: 1}) + const currentBounds = new UIEventSource(undefined) + const map = Minimap.createMiniMap({ + allowMoving: true, + location, + background, + bounds: currentBounds, + attribution: new Attribution(location, state.osmConnection.userDetails, undefined, currentBounds) + }) + map.SetClass("w-full").SetStyle("height: 500px") + + new ShowDataMultiLayer({ + layers: new UIEventSource(AllKnownLayouts.AllPublicLayers().map(l => ({ + layerDef: l, + isDisplayed: new UIEventSource(true), + appliedFilters: new UIEventSource>(undefined) + }))), + zoomToFeatures: true, + features: new StaticFeatureSource(matching, false), + state: { + ...state, + filteredLayers: new UIEventSource(undefined), + backgroundLayer: background + }, + leafletMap: map.leafletMap, + + }) + var bbox = matching.map(feats => BBox.bboxAroundAll(feats.map(f => new BBox([f.geometry.coordinates])))) + + super([ + "Has " + geojson.features.length + " features", + layerPicker, + new Toggle("Automatically detected layer", undefined, autodetected), + new Table(["", "Key", "Values", "Unique values seen"], + Array.from(propertyKeys).map(key => { + const uniqueValues = Utils.Dedup(Utils.NoNull(geojson.features.map(f => f.properties[key]))) + uniqueValues.sort() + return [geojson.features.filter(f => f.properties[key] !== undefined).length + "", key, uniqueValues.join(", "), "" + uniqueValues.length] + }) + ).SetClass("zebra-table table-auto"), + new VariableUiElement(matching.map(matching => { + if (matching === undefined) { + return undefined + } + const diff = geojson.features.length - matching.length; + if (diff === 0) { + return undefined + } + const obligatory = layerPicker.GetValue().data?.source?.osmTags?.asHumanString(false, false, {}); + return new FixedUiElement(`${diff} features will _not_ match this layer. Make sure that all obligatory objects are present: ${obligatory}`).SetClass("alert"); + })), + map + ]); + + this.Value = bbox.map(bbox => + ({ + bbox, + geojson, + layer: layerPicker.GetValue().data + }), [layerPicker.GetValue()]) + this.IsValid = matching.map(matching => { + if (matching === undefined) { + return false + } + const diff = geojson.features.length - matching.length; + return diff === 0; + }) + + } +} \ No newline at end of file diff --git a/UI/ImportFlow/FlowStep.ts b/UI/ImportFlow/FlowStep.ts new file mode 100644 index 0000000000..3b70e76eff --- /dev/null +++ b/UI/ImportFlow/FlowStep.ts @@ -0,0 +1,105 @@ +import {UIEventSource} from "../../Logic/UIEventSource"; +import Combine from "../Base/Combine"; +import BaseUIElement from "../BaseUIElement"; +import {SubtleButton} from "../Base/SubtleButton"; +import Svg from "../../Svg"; +import Translations from "../i18n/Translations"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import Toggle from "../Input/Toggle"; +import {UIElement} from "../UIElement"; + +export interface FlowStep extends BaseUIElement{ + readonly IsValid: UIEventSource + readonly Value: UIEventSource +} + +export class FlowPanelFactory { + private _initial: FlowStep; + private _steps: ((x: any) => FlowStep)[]; + private _stepNames: string[]; + + private constructor(initial: FlowStep, steps: ((x:any) => FlowStep)[], stepNames: string[]) { + this._initial = initial; + this._steps = steps; + this._stepNames = stepNames; + } + + public static start (step: FlowStep): FlowPanelFactory{ + return new FlowPanelFactory(step, [], []) + } + + public then(name: string, construct: ((t:T) => FlowStep)): FlowPanelFactory{ + return new FlowPanelFactory( + this._initial, + this._steps.concat([construct]), + this._stepNames.concat([name]) + ) + } + + public finish(construct: ((t: T, backButton?: BaseUIElement) => BaseUIElement)) : BaseUIElement { + // Construct all the flowpanels step by step (in reverse order) + const nextConstr : ((t:any, back?: UIElement) => BaseUIElement)[] = this._steps.map(_ => undefined) + nextConstr.push(construct) + + for (let i = this._steps.length - 1; i >= 0; i--){ + const createFlowStep : (value) => FlowStep = this._steps[i]; + nextConstr[i] = (value, backButton) => { + console.log("Creating flowSTep ", this._stepNames[i]) + const flowStep = createFlowStep(value) + return new FlowPanel(flowStep, nextConstr[i + 1], backButton); + } + } + + return new FlowPanel(this._initial, nextConstr[0],undefined) + } + +} + +export class FlowPanel extends Toggle { + + constructor( + initial: (FlowStep), + constructNextstep: ((input: T, backButton: BaseUIElement) => BaseUIElement), + backbutton?: BaseUIElement + ) { + const t = Translations.t.general; + + const currentStepActive = new UIEventSource(true); + + let nextStep: UIEventSource= new UIEventSource(undefined) + const backButtonForNextStep = new SubtleButton(Svg.back_svg(), t.back).onClick(() => { + currentStepActive.setData(true) + }) + + let elements : (BaseUIElement | string)[] = [] + if(initial !== undefined){ + // Startup the flow + elements = [ + initial, + new Combine([ + backbutton, + new Toggle( + new SubtleButton(Svg.back_svg().SetStyle("transform: rotate(180deg);"), t.next).onClick(() => { + const v = initial.Value.data; + nextStep.setData(constructNextstep(v, backButtonForNextStep)) + currentStepActive.setData(false) + }), + "Select a valid value to continue", + initial.IsValid + ) + ]).SetClass("flex w-full justify-end space-x-2") + + ] + } + + + super( + new Combine(elements).SetClass("h-full flex flex-col justify-between"), + new VariableUiElement(nextStep), + currentStepActive + ); + } + + + +} \ No newline at end of file diff --git a/UI/ImportFlow/ImportHelperGui.ts b/UI/ImportFlow/ImportHelperGui.ts new file mode 100644 index 0000000000..09f139bdf3 --- /dev/null +++ b/UI/ImportFlow/ImportHelperGui.ts @@ -0,0 +1,67 @@ +import Combine from "../Base/Combine"; +import {LoginToggle} from "../Popup/LoginButton"; +import Toggle from "../Input/Toggle"; +import LanguagePicker from "../LanguagePicker"; +import BackToIndex from "../BigComponents/BackToIndex"; +import UserRelatedState from "../../Logic/State/UserRelatedState"; +import BaseUIElement from "../BaseUIElement"; +import MoreScreen from "../BigComponents/MoreScreen"; +import MinimapImplementation from "../Base/MinimapImplementation"; +import Translations from "../i18n/Translations"; +import Constants from "../../Models/Constants"; +import {FlowPanel, FlowPanelFactory} from "./FlowStep"; +import {RequestFile} from "./RequestFile"; +import {DataPanel} from "./DataPanel"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import ConflationChecker from "./ConflationChecker"; + +export default class ImportHelperGui extends LoginToggle { + constructor() { + const t = Translations.t.importHelper; + + const state = new UserRelatedState(undefined) + + // We disable the userbadge, as various 'showData'-layers will give a read-only view in this case + state.featureSwitchUserbadge.setData(false) + + const leftContents: BaseUIElement[] = [ + new BackToIndex().SetClass("block pl-4"), + LanguagePicker.CreateLanguagePicker(Translations.t.importHelper.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"), + ].map(el => el?.SetClass("pl-4")) + + const leftBar = new Combine([ + new Combine(leftContents).SetClass("sticky top-4 m-4") + ]).SetClass("block w-full md:w-2/6 lg:w-1/6") + + + const mainPanel = + FlowPanelFactory + .start(new RequestFile()) + .then("datapanel", geojson => new DataPanel(state, geojson)) + .then("conflation", v => new ConflationChecker(state, v)) + .finish(_ => new FixedUiElement("All done!")) + + super( + new Toggle( + new Combine([ + leftBar, + mainPanel.SetClass("m-8 w-full mb-24") + ]).SetClass("h-full block md:flex") + + , + new Combine([ + t.lockNotice.Subs(Constants.userJourney), + MoreScreen.CreateProffessionalSerivesButton() + ]) + + , + state.osmConnection.userDetails.map(ud => ud.csCount >= Constants.userJourney.importHelperUnlock)), + + "Login needed...", + state) + } + +} + +MinimapImplementation.initialize() +new ImportHelperGui().AttachTo("main") \ No newline at end of file diff --git a/UI/ImportFlow/RequestFile.ts b/UI/ImportFlow/RequestFile.ts new file mode 100644 index 0000000000..2f8ea2861a --- /dev/null +++ b/UI/ImportFlow/RequestFile.ts @@ -0,0 +1,145 @@ +import Combine from "../Base/Combine"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Translations from "../i18n/Translations"; +import {SubtleButton} from "../Base/SubtleButton"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import Title from "../Base/Title"; +import InputElementMap from "../Input/InputElementMap"; +import BaseUIElement from "../BaseUIElement"; +import FileSelectorButton from "../Input/FileSelectorButton"; +import {FlowStep} from "./FlowStep"; + +class FileSelector extends InputElementMap }> { + constructor(label: BaseUIElement) { + super( + new FileSelectorButton(label, {allowMultiple: false, acceptType: "*"}), + (x0, x1) => { + // Total hack: x1 is undefined is the backvalue - we effectively make this a one-way-story + return x1 === undefined || x0 === x1; + }, + filelist => { + if (filelist === undefined) { + return undefined + } + const file = filelist.item(0) + return {name: file.name, contents: file.text()} + }, + _ => undefined + ) + } +} + +/** + * The first step in the import flow: load a file and validate that it is a correct geojson or CSV file + */ +export class RequestFile extends Combine implements FlowStep { + + public readonly IsValid: UIEventSource + /** + * The loaded GeoJSON + */ + public readonly Value: UIEventSource + + constructor() { + const t = Translations.t.importHelper; + const csvSelector = new FileSelector(new SubtleButton(undefined, t.selectFile)) + const loadedFiles = new VariableUiElement(csvSelector.GetValue().map(file => { + if (file === undefined) { + return t.noFilesLoaded.SetClass("alert") + } + return t.loadedFilesAre.Subs({file: file.name}).SetClass("thanks") + })) + + const text = UIEventSource.flatten( + csvSelector.GetValue().map(v => { + if (v === undefined) { + return undefined + } + return UIEventSource.FromPromise(v.contents) + })) + + const asGeoJson: UIEventSource = text.map(src => { + if (src === undefined) { + return undefined + } + try { + const parsed = JSON.parse(src) + if (parsed["type"] !== "FeatureCollection") { + return {error: "The loaded JSON-file is not a geojson-featurecollection"} + } + if (parsed.features.some(f => f.geometry.type != "Point")) { + return {error: "The loaded JSON-file should only contain points"} + } + return parsed; + + } catch (e) { + // Loading as CSV + const lines = src.split("\n") + const header = lines[0].split(",") + lines.splice(0, 1) + if (header.indexOf("lat") < 0 || header.indexOf("lon") < 0) { + return {error: "The header does not contain `lat` or `lon`"} + } + + const features = [] + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.trim() === "") { + continue + } + const attrs = line.split(",") + const properties = {} + for (let i = 0; i < header.length; i++) { + properties[header[i]] = attrs[i]; + } + const coordinates = [Number(properties["lon"]), Number(properties["lat"])] + delete properties["lat"] + delete properties["lon"] + if (coordinates.some(isNaN)) { + return {error: "A coordinate could not be parsed for line " + (i + 2)} + } + const f = { + type: "Feature", + properties, + geometry: { + type: "Point", + coordinates + } + }; + features.push(f) + } + + return { + type: "FeatureCollection", + features + } + } + }) + + + const errorIndicator = new VariableUiElement(asGeoJson.map(v => { + if (v === undefined) { + return undefined; + } + if (v?.error === undefined) { + return undefined; + } + return new FixedUiElement(v?.error).SetClass("alert"); + })) + + super([ + + new Title(t.title, 1), + t.description, + csvSelector, + loadedFiles, + errorIndicator + + ]); + this.IsValid = asGeoJson.map(geojson => geojson !== undefined && geojson["error"] === undefined) + this.Value = asGeoJson + } + + +} \ No newline at end of file diff --git a/UI/ImportHelperGui.ts b/UI/ImportHelperGui.ts deleted file mode 100644 index d3d768ec24..0000000000 --- a/UI/ImportHelperGui.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {FixedUiElement} from "./Base/FixedUiElement"; -import {LoginToggle} from "./Popup/LoginButton"; -import {OsmConnection} from "../Logic/Osm/OsmConnection"; -import UserRelatedState from "../Logic/State/UserRelatedState"; -import Combine from "./Base/Combine"; -import BackToIndex from "./BigComponents/BackToIndex"; -import BaseUIElement from "./BaseUIElement"; -import TableOfContents from "./Base/TableOfContents"; -import LanguagePicker from "./LanguagePicker"; -import Translations from "./i18n/Translations"; -import Constants from "../Models/Constants"; -import Toggle from "./Input/Toggle"; -import MoreScreen from "./BigComponents/MoreScreen"; -import Title from "./Base/Title"; - -export default class ImportHelperGui extends LoginToggle{ - - constructor() { - const t = Translations.t.importHelper; - - const state = new UserRelatedState(undefined) - - const leftContents: BaseUIElement[] = [ - new BackToIndex().SetClass("block pl-4"), - LanguagePicker.CreateLanguagePicker(Translations.t.importHelper.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"), - ].map(el => el?.SetClass("pl-4")) - - const leftBar = new Combine([ - new Combine(leftContents).SetClass("sticky top-4 m-4") - ]).SetClass("block w-full md:w-2/6 lg:w-1/6") - - - super( - - new Toggle( - new Combine([ - leftBar, - new Combine([ - new Title(t.title,1), - t.description - ]).SetClass("flex flex-col m-8") - ]).SetClass("block md:flex") - - , - new Combine([ - t.lockNotice.Subs(Constants.userJourney), - MoreScreen.CreateProffessionalSerivesButton() - ]) - - , - state.osmConnection.userDetails.map(ud => ud.csCount >= Constants.userJourney.importHelperUnlock)), - - "Login needed...", - state) - } - -} - - -new ImportHelperGui().AttachTo("main") \ No newline at end of file diff --git a/UI/Input/CombinedInputElement.ts b/UI/Input/CombinedInputElement.ts index b1fa39a8e4..2409898cd4 100644 --- a/UI/Input/CombinedInputElement.ts +++ b/UI/Input/CombinedInputElement.ts @@ -5,7 +5,6 @@ import BaseUIElement from "../BaseUIElement"; export default class CombinedInputElement extends InputElement { - public readonly IsSelected: UIEventSource; private readonly _a: InputElement; private readonly _b: InputElement; private readonly _combined: BaseUIElement; @@ -19,9 +18,6 @@ export default class CombinedInputElement extends InputElement { this._a = a; this._b = b; this._split = split; - this.IsSelected = this._a.IsSelected.map((isSelected) => { - return isSelected || b.IsSelected.data - }, [b.IsSelected]) this._combined = new Combine([this._a, this._b]); this._value = this._a.GetValue().map( t => combine(t, this._b?.GetValue()?.data), diff --git a/UI/Input/DropDown.ts b/UI/Input/DropDown.ts index 1930a2fe41..d7137a04bb 100644 --- a/UI/Input/DropDown.ts +++ b/UI/Input/DropDown.ts @@ -37,7 +37,7 @@ export class DropDown extends InputElement { el.id = "dropdown" + id; { - const labelEl = Translations.W(label).ConstructElement() + const labelEl = Translations.W(label)?.ConstructElement() if (labelEl !== undefined) { const labelHtml = document.createElement("label") labelHtml.appendChild(labelEl) diff --git a/UI/Input/FileSelectorButton.ts b/UI/Input/FileSelectorButton.ts index c6e1cf1f8b..8392ca0577 100644 --- a/UI/Input/FileSelectorButton.ts +++ b/UI/Input/FileSelectorButton.ts @@ -6,16 +6,20 @@ export default class FileSelectorButton extends InputElement { private static _nextid; IsSelected: UIEventSource; - private readonly _value = new UIEventSource(undefined); + private readonly _value = new UIEventSource(undefined); private readonly _label: BaseUIElement; private readonly _acceptType: string; + private readonly allowMultiple: boolean; - constructor(label: BaseUIElement, acceptType: string = "image/*") { + constructor(label: BaseUIElement, options?: + { acceptType: "image/*" | string, + allowMultiple: true | boolean}) { super(); this._label = label; - this._acceptType = acceptType; + this._acceptType = options?.acceptType ?? "image/*"; this.SetClass("block cursor-pointer") label.SetClass("cursor-pointer") + this.allowMultiple = options?.allowMultiple ?? true } GetValue(): UIEventSource { @@ -38,7 +42,7 @@ export default class FileSelectorButton extends InputElement { actualInputElement.type = "file"; actualInputElement.accept = this._acceptType; actualInputElement.name = "picField"; - actualInputElement.multiple = true; + actualInputElement.multiple = this.allowMultiple; actualInputElement.id = "fileselector" + FileSelectorButton._nextid; FileSelectorButton._nextid++; @@ -59,6 +63,20 @@ export default class FileSelectorButton extends InputElement { el.appendChild(actualInputElement) + el.addEventListener('dragover', (event) => { + event.stopPropagation(); + event.preventDefault(); + // Style the drag-and-drop as a "copy file" operation. + event.dataTransfer.dropEffect = 'copy'; + }); + + el.addEventListener('drop', (event) => { + event.stopPropagation(); + event.preventDefault(); + const fileList = event.dataTransfer.files; + this._value.setData(fileList) + }); + return el; } diff --git a/UI/Input/FixedInputElement.ts b/UI/Input/FixedInputElement.ts index 37e025b799..6dd39fc0d9 100644 --- a/UI/Input/FixedInputElement.ts +++ b/UI/Input/FixedInputElement.ts @@ -4,7 +4,6 @@ import Translations from "../i18n/Translations"; import BaseUIElement from "../BaseUIElement"; export class FixedInputElement extends InputElement { - public readonly IsSelected: UIEventSource = new UIEventSource(false); private readonly value: UIEventSource; private readonly _comparator: (t0: T, t1: T) => boolean; @@ -21,17 +20,12 @@ export class FixedInputElement extends InputElement { this.value = new UIEventSource(value); } - const selected = this.IsSelected; this._el = document.createElement("span") - this._el.addEventListener("mouseout", () => selected.setData(false)) const e = Translations.W(rendering)?.ConstructElement() if (e) { this._el.appendChild(e) } - this.onClick(() => { - selected.setData(true) - }) } GetValue(): UIEventSource { diff --git a/UI/Input/InputElement.ts b/UI/Input/InputElement.ts index f9920b1d6a..f8a27eddde 100644 --- a/UI/Input/InputElement.ts +++ b/UI/Input/InputElement.ts @@ -3,7 +3,6 @@ import BaseUIElement from "../BaseUIElement"; export abstract class InputElement extends BaseUIElement { - abstract IsSelected: UIEventSource; abstract GetValue(): UIEventSource; diff --git a/UI/Input/InputElementMap.ts b/UI/Input/InputElementMap.ts index a2a50f9d38..16b907f469 100644 --- a/UI/Input/InputElementMap.ts +++ b/UI/Input/InputElementMap.ts @@ -3,7 +3,6 @@ import {UIEventSource} from "../../Logic/UIEventSource"; export default class InputElementMap extends InputElement { - public readonly IsSelected: UIEventSource; private readonly _inputElement: InputElement; private isSame: (x0: X, x1: X) => boolean; private readonly fromX: (x: X) => T; @@ -21,7 +20,6 @@ export default class InputElementMap extends InputElement { this.fromX = fromX; this.toX = toX; this._inputElement = inputElement; - this.IsSelected = inputElement.IsSelected; const self = this; this._value = inputElement.GetValue().map( (t => { diff --git a/UI/Input/InputElementWrapper.ts b/UI/Input/InputElementWrapper.ts index 5a6633ade3..aeeb6fa6cd 100644 --- a/UI/Input/InputElementWrapper.ts +++ b/UI/Input/InputElementWrapper.ts @@ -6,14 +6,12 @@ import {SubstitutedTranslation} from "../SubstitutedTranslation"; import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; export default class InputElementWrapper extends InputElement { - public readonly IsSelected: UIEventSource; private readonly _inputElement: InputElement; private readonly _renderElement: BaseUIElement constructor(inputElement: InputElement, translation: Translation, key: string, tags: UIEventSource, state: FeaturePipelineState) { super() this._inputElement = inputElement; - this.IsSelected = inputElement.IsSelected const mapping = new Map() mapping.set(key, inputElement) diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts index 133244d89f..3513e9797e 100644 --- a/UI/Input/LocationInput.ts +++ b/UI/Input/LocationInput.ts @@ -38,6 +38,8 @@ export default class LocationInput extends InputElement implements MinimapO private readonly _snappedPointTags: any; private readonly _bounds: UIEventSource; private readonly map: BaseUIElement & MinimapObj; + public readonly bounds; + public readonly location; private readonly clickLocation: UIEventSource; private readonly _minZoom: number; @@ -146,6 +148,8 @@ export default class LocationInput extends InputElement implements MinimapO } ) this.leafletMap = this.map.leafletMap + this.bounds = this.map.bounds; + this.location = this.map.location; } GetValue(): UIEventSource { @@ -186,11 +190,10 @@ export default class LocationInput extends InputElement implements MinimapO console.log("Constructing the snap-to layer", this._snapTo) new ShowDataMultiLayer({ features: new StaticFeatureSource(this._snapTo, true), - enablePopups: false, + popup: undefined, zoomToFeatures: false, leafletMap: this.map.leafletMap, - layers: State.state.filteredLayers, - allElements: State.state.allElements + layers: State.state.filteredLayers } ) // Show the central point @@ -202,11 +205,11 @@ export default class LocationInput extends InputElement implements MinimapO }) new ShowDataLayer({ features: new StaticFeatureSource(matchPoint, true), - enablePopups: false, + popup: undefined, zoomToFeatures: false, leafletMap: this.map.leafletMap, layerToShow: this._matching_layer, - allElements: State.state.allElements, + state: State.state, selectedElement: State.state.selectedElement }) diff --git a/UI/Input/RadioButton.ts b/UI/Input/RadioButton.ts index 73310559b3..ae7cc2d721 100644 --- a/UI/Input/RadioButton.ts +++ b/UI/Input/RadioButton.ts @@ -4,7 +4,6 @@ import {Utils} from "../../Utils"; export class RadioButton extends InputElement { private static _nextId = 0; - IsSelected: UIEventSource = new UIEventSource(false); private readonly value: UIEventSource; private _elements: InputElement[]; private _selectFirstAsDefault: boolean; @@ -74,11 +73,7 @@ export class RadioButton extends InputElement { elements[i]?.onClick(() => { selectedElementIndex.setData(i); }); - elements[i].IsSelected.addCallback((isSelected) => { - if (isSelected) { - selectedElementIndex.setData(i); - } - }); + elements[i].GetValue().addCallback(() => { selectedElementIndex.setData(i); }); diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index a347613f39..d4106c321a 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -5,7 +5,6 @@ import BaseUIElement from "../BaseUIElement"; export class TextField extends InputElement { public readonly enterPressed = new UIEventSource(undefined); - public readonly IsSelected: UIEventSource = new UIEventSource(false); private readonly value: UIEventSource; private _element: HTMLElement; private readonly _isValid: (s: string, country?: () => string) => boolean; @@ -26,11 +25,6 @@ export class TextField extends InputElement { this.value = options?.value ?? new UIEventSource(undefined); this._isValid = options.isValid ?? (_ => true); - this.onClick(() => { - self.IsSelected.setData(true) - }); - - const placeholder = Translations.W(options.placeholder ?? "").ConstructElement().innerText.replace("'", "'"); this.SetClass("form-text-field") @@ -107,10 +101,6 @@ export class TextField extends InputElement { }; - field.addEventListener("focusin", () => self.IsSelected.setData(true)); - field.addEventListener("focusout", () => self.IsSelected.setData(false)); - - field.addEventListener("keyup", function (event) { if (event.key === "Enter") { // @ts-ignore diff --git a/UI/Input/VariableInputElement.ts b/UI/Input/VariableInputElement.ts index f7bb2d8f8f..e9a6aa3d97 100644 --- a/UI/Input/VariableInputElement.ts +++ b/UI/Input/VariableInputElement.ts @@ -5,7 +5,6 @@ import {VariableUiElement} from "../Base/VariableUIElement"; export default class VariableInputElement extends InputElement { - public readonly IsSelected: UIEventSource; private readonly value: UIEventSource; private readonly element: BaseUIElement private readonly upstream: UIEventSource>; @@ -16,7 +15,6 @@ export default class VariableInputElement extends InputElement { this.upstream = upstream; this.value = upstream.bind(v => v.GetValue()) this.element = new VariableUiElement(upstream) - this.IsSelected = upstream.bind(v => v.IsSelected) } GetValue(): UIEventSource { diff --git a/UI/OpeningHours/OpeningHoursPickerTable.ts b/UI/OpeningHours/OpeningHoursPickerTable.ts index 837bc10b0d..90fbd93c73 100644 --- a/UI/OpeningHours/OpeningHoursPickerTable.ts +++ b/UI/OpeningHours/OpeningHoursPickerTable.ts @@ -24,7 +24,6 @@ export default class OpeningHoursPickerTable extends InputElement Translations.t.general.weekdays.abbreviations.saturday, Translations.t.general.weekdays.abbreviations.sunday ] - public readonly IsSelected: UIEventSource; /* These html-elements are an overlay over the table columns and act as a host for the ranges in the weekdays */ @@ -34,7 +33,6 @@ export default class OpeningHoursPickerTable extends InputElement constructor(source?: UIEventSource) { super(); this.source = source ?? new UIEventSource([]); - this.IsSelected = new UIEventSource(false); this.SetStyle("width:100%;height:100%;display:block;"); } diff --git a/UI/Popup/AutoApplyButton.ts b/UI/Popup/AutoApplyButton.ts index bf545e79b1..807e5239f8 100644 --- a/UI/Popup/AutoApplyButton.ts +++ b/UI/Popup/AutoApplyButton.ts @@ -137,10 +137,10 @@ export default class AutoApplyButton implements SpecialVisualization { new ShowDataLayer({ leafletMap: previewMap.leafletMap, - enablePopups: false, + popup: undefined, zoomToFeatures: true, features: new StaticFeatureSource(features, false), - allElements: state.allElements, + state, layerToShow: layer.layerDef, }) diff --git a/UI/Popup/DeleteWizard.ts b/UI/Popup/DeleteWizard.ts index 5b66956f2a..7aa8a90a3f 100644 --- a/UI/Popup/DeleteWizard.ts +++ b/UI/Popup/DeleteWizard.ts @@ -1,5 +1,4 @@ import {VariableUiElement} from "../Base/VariableUIElement"; -import State from "../../State"; import Toggle from "../Input/Toggle"; import Translations from "../i18n/Translations"; import Svg from "../../Svg"; @@ -17,6 +16,10 @@ import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; import {AndOrTagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson"; import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig"; import {OsmObject} from "../../Logic/Osm/OsmObject"; +import {ElementStorage} from "../../Logic/ElementStorage"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import {Changes} from "../../Logic/Osm/Changes"; +import {OsmConnection} from "../../Logic/Osm/OsmConnection"; export default class DeleteWizard extends Toggle { /** @@ -35,13 +38,21 @@ export default class DeleteWizard extends Toggle { * (Note that _delete_reason is used as trigger to do actual deletion - setting such a tag WILL delete from the database with that as changeset comment) * * @param id: The id of the element to remove + * @param state: the state of the application * @param options softDeletionTags: the tags to apply if the user doesn't have permission to delete, e.g. 'disused:amenity=public_bookcase', 'amenity='. After applying, the element should not be picked up on the map anymore. If undefined, the wizard will only show up if the point can be (hard) deleted */ constructor(id: string, + state: { + osmConnection: OsmConnection; + allElements: ElementStorage, + layoutToUse?: LayoutConfig, + changes?: Changes + }, options: DeleteConfig) { - const deleteAbility = new DeleteabilityChecker(id, options.neededChangesets) - const tagsSource = State.state.allElements.getEventSourceById(id) + + const deleteAbility = new DeleteabilityChecker(id, state, options.neededChangesets) + const tagsSource = state.allElements.getEventSourceById(id) const isDeleted = new UIEventSource(false) const allowSoftDeletion = !!options.softDeletionTags @@ -59,12 +70,12 @@ export default class DeleteWizard extends Toggle { const deleteAction = new DeleteAction(id, options.softDeletionTags, { - theme: State.state?.layoutToUse?.id ?? "unkown", + theme: state?.layoutToUse?.id ?? "unkown", specialMotivation: deleteReasonMatch[0]?.v }, deleteAbility.canBeDeleted.data.canBeDeleted ) - State.state.changes.applyAction(deleteAction) + state.changes?.applyAction(deleteAction) isDeleted.setData(true) } @@ -77,6 +88,7 @@ export default class DeleteWizard extends Toggle { return new TagRenderingQuestion( tagsSource, config, + state, { cancelButton: cancelButton, /*Using a custom save button constructor erases all logic to actually save, so we have to listen for the click!*/ @@ -112,7 +124,7 @@ export default class DeleteWizard extends Toggle { new Toggle( question, new SubtleButton(Svg.envelope_ui(), t.readMessages.Clone()), - State.state.osmConnection.userDetails.map(ud => ud.csCount > Constants.userJourney.addNewPointWithUnreadMessagesUnlock || ud.unreadMessages == 0) + state.osmConnection.userDetails.map(ud => ud.csCount > Constants.userJourney.addNewPointWithUnreadMessagesUnlock || ud.unreadMessages == 0) ), deleteButton, @@ -131,8 +143,8 @@ export default class DeleteWizard extends Toggle { , deleteAbility.canBeDeleted.map(cbd => allowSoftDeletion || cbd.canBeDeleted !== false)), - t.loginToDelete.Clone().onClick(State.state.osmConnection.AttemptLogin), - State.state.osmConnection.isLoggedIn + t.loginToDelete.Clone().onClick(state.osmConnection.AttemptLogin), + state.osmConnection.isLoggedIn ), isDeleted), undefined, @@ -275,11 +287,16 @@ class DeleteabilityChecker { public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean, reason: Translation }>; private readonly _id: string; private readonly _allowDeletionAtChangesetCount: number; + private readonly _state: { + osmConnection: OsmConnection + }; constructor(id: string, + state: {osmConnection: OsmConnection}, allowDeletionAtChangesetCount?: number) { this._id = id; + this._state = state; this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE; this.canBeDeleted = new UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>({ @@ -299,6 +316,7 @@ class DeleteabilityChecker { const t = Translations.t.delete; const id = this._id; const state = this.canBeDeleted + const self = this; if (!id.startsWith("node")) { this.canBeDeleted.setData({ canBeDeleted: false, @@ -308,8 +326,7 @@ class DeleteabilityChecker { } // Does the currently logged in user have enough experience to delete this point? - - const deletingPointsOfOtherAllowed = State.state.osmConnection.userDetails.map(ud => { + const deletingPointsOfOtherAllowed = this._state.osmConnection.userDetails.map(ud => { if (ud === undefined) { return undefined; } @@ -320,15 +337,14 @@ class DeleteabilityChecker { }) const previousEditors = new UIEventSource(undefined) - const allByMyself = previousEditors.map(previous => { if (previous === null || previous === undefined) { // Not yet downloaded return null; } - const userId = State.state.osmConnection.userDetails.data.uid; + const userId = self._state.osmConnection.userDetails.data.uid; return !previous.some(editor => editor !== userId) - }, [State.state.osmConnection.userDetails]) + }, [self._state.osmConnection.userDetails]) // User allowed OR only edited by self? diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts index bd70be9b8a..2df461bda7 100644 --- a/UI/Popup/EditableTagRendering.ts +++ b/UI/Popup/EditableTagRendering.ts @@ -3,19 +3,20 @@ import TagRenderingQuestion from "./TagRenderingQuestion"; import Translations from "../i18n/Translations"; import Combine from "../Base/Combine"; import TagRenderingAnswer from "./TagRenderingAnswer"; -import State from "../../State"; import Svg from "../../Svg"; import Toggle from "../Input/Toggle"; import BaseUIElement from "../BaseUIElement"; import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; import {Unit} from "../../Models/Unit"; import Lazy from "../Base/Lazy"; +import {OsmConnection} from "../../Logic/Osm/OsmConnection"; export default class EditableTagRendering extends Toggle { constructor(tags: UIEventSource, configuration: TagRenderingConfig, units: Unit [], + state, options: { editMode?: UIEventSource, innerElementClasses?: string @@ -32,7 +33,7 @@ export default class EditableTagRendering extends Toggle { super( new Lazy(() => { const editMode = options.editMode ?? new UIEventSource(false) - const rendering = EditableTagRendering.CreateRendering(tags, configuration, units, editMode); + const rendering = EditableTagRendering.CreateRendering(state, tags, configuration, units, editMode); rendering.SetClass(options.innerElementClasses) return rendering }), @@ -41,12 +42,12 @@ export default class EditableTagRendering extends Toggle { ) } - private static CreateRendering(tags: UIEventSource, configuration: TagRenderingConfig, units: Unit[], editMode: UIEventSource): BaseUIElement { - const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration, State.state) + private static CreateRendering(state: {featureSwitchUserbadge?: UIEventSource, osmConnection: OsmConnection}, tags: UIEventSource, configuration: TagRenderingConfig, units: Unit[], editMode: UIEventSource): BaseUIElement { + const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration, state) answer.SetClass("w-full") let rendering = answer; - if (configuration.question !== undefined && State.state?.featureSwitchUserbadge?.data) { + if (configuration.question !== undefined && state?.featureSwitchUserbadge?.data) { // We have a question and editing is enabled const answerWithEditButton = new Combine([answer, new Toggle(new Combine([Svg.pencil_ui()]).SetClass("block relative h-10 w-10 p-2 float-right").SetStyle("border: 1px solid black; border-radius: 0.7em") @@ -54,12 +55,12 @@ export default class EditableTagRendering extends Toggle { editMode.setData(true); }), undefined, - State.state.osmConnection.isLoggedIn) + state.osmConnection.isLoggedIn) ]).SetClass("flex justify-between w-full") const question = new Lazy(() => - new TagRenderingQuestion(tags, configuration, + new TagRenderingQuestion(tags, configuration,state, { units: units, cancelButton: Translations.t.general.cancel.Clone() diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 0bcfd6197c..e3a10a3a3c 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -3,7 +3,6 @@ import EditableTagRendering from "./EditableTagRendering"; import QuestionBox from "./QuestionBox"; import Combine from "../Base/Combine"; import TagRenderingAnswer from "./TagRenderingAnswer"; -import State from "../../State"; import ScrollableFullScreen from "../Base/ScrollableFullScreen"; import Constants from "../../Models/Constants"; import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; @@ -16,18 +15,41 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; import {Utils} from "../../Utils"; import MoveWizard from "./MoveWizard"; import Toggle from "../Input/Toggle"; +import {OsmConnection} from "../../Logic/Osm/OsmConnection"; +import {Changes} from "../../Logic/Osm/Changes"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import {ElementStorage} from "../../Logic/ElementStorage"; +import FilteredLayer from "../../Models/FilteredLayer"; +import BaseLayer from "../../Models/BaseLayer"; +import Lazy from "../Base/Lazy"; export default class FeatureInfoBox extends ScrollableFullScreen { + public constructor( tags: UIEventSource, layerConfig: LayerConfig, + state: { + filteredLayers: UIEventSource; + backgroundLayer: UIEventSource; + featureSwitchIsTesting: UIEventSource; + featureSwitchIsDebugging: UIEventSource; + featureSwitchShowAllQuestions: UIEventSource; + osmConnection: OsmConnection, + featureSwitchUserbadge: UIEventSource, + changes: Changes, + layoutToUse: LayoutConfig, + allElements: ElementStorage + }, hashToShow?: string, - isShown?: UIEventSource + isShown?: UIEventSource, ) { - super(() => FeatureInfoBox.GenerateTitleBar(tags, layerConfig), - () => FeatureInfoBox.GenerateContent(tags, layerConfig), - hashToShow ?? tags.data.id, + if (state === undefined) { + throw "State is undefined!" + } + super(() => FeatureInfoBox.GenerateTitleBar(tags, layerConfig, state), + () => FeatureInfoBox.GenerateContent(tags, layerConfig, state), + hashToShow ?? tags.data.id ?? "item", isShown); if (layerConfig === undefined) { @@ -37,11 +59,12 @@ export default class FeatureInfoBox extends ScrollableFullScreen { } private static GenerateTitleBar(tags: UIEventSource, - layerConfig: LayerConfig): BaseUIElement { - const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI"), State.state) + layerConfig: LayerConfig, + state: {}): BaseUIElement { + 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"); const titleIcons = new Combine( - layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon, State.state, + layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon, state, "block w-8 h-8 max-h-8 align-baseline box-content sm:p-0.5 w-10",) )) .SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2") @@ -52,20 +75,32 @@ export default class FeatureInfoBox extends ScrollableFullScreen { } private static GenerateContent(tags: UIEventSource, - layerConfig: LayerConfig): BaseUIElement { + layerConfig: LayerConfig, + state: { + filteredLayers: UIEventSource; + backgroundLayer: UIEventSource; + featureSwitchIsTesting: UIEventSource; + featureSwitchIsDebugging: UIEventSource; + featureSwitchShowAllQuestions: UIEventSource; + osmConnection: OsmConnection, + featureSwitchUserbadge: UIEventSource, + changes: Changes, + layoutToUse: LayoutConfig, + allElements: ElementStorage + }): BaseUIElement { let questionBoxes: Map = new Map(); const allGroupNames = Utils.Dedup(layerConfig.tagRenderings.map(tr => tr.group)) - if (State.state.featureSwitchUserbadge.data) { + if (state?.featureSwitchUserbadge?.data ?? true) { const questionSpecs = layerConfig.tagRenderings.filter(tr => tr.id === "questions") for (const groupName of allGroupNames) { const questions = layerConfig.tagRenderings.filter(tr => tr.group === groupName) const questionSpec = questionSpecs.filter(tr => tr.group === groupName)[0] - const questionBox = new QuestionBox({ + const questionBox = new QuestionBox(state, { tagsSource: tags, tagRenderings: questions, units: layerConfig.units, - showAllQuestionsAtOnce: questionSpec?.freeform?.helperArgs["showAllQuestions"] ?? State.state.featureSwitchShowAllQuestions + showAllQuestionsAtOnce: questionSpec?.freeform?.helperArgs["showAllQuestions"] ?? state.featureSwitchShowAllQuestions }); questionBoxes.set(groupName, questionBox) } @@ -86,10 +121,10 @@ export default class FeatureInfoBox extends ScrollableFullScreen { if (tr.render !== undefined) { questionBox.SetClass("text-sm") - const renderedQuestion = new TagRenderingAnswer(tags, tr,State.state, + const renderedQuestion = new TagRenderingAnswer(tags, tr, state, tr.group + " questions", "", { - specialViz: new Map([["questions", questionBox]]) - }) + specialViz: new Map([["questions", questionBox]]) + }) const possiblyHidden = new Toggle( renderedQuestion, undefined, @@ -109,7 +144,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { classes = "" } - const etr = new EditableTagRendering(tags, tr, layerConfig.units, { + const etr = new EditableTagRendering(tags, tr, layerConfig.units, state, { innerElementClasses: innerClasses }) if (isHeader) { @@ -121,8 +156,29 @@ export default class FeatureInfoBox extends ScrollableFullScreen { allRenderings.push(...renderingsForGroup) } + allRenderings.push( + new Toggle( + new Lazy(() => FeatureInfoBox.createEditElements(questionBoxes, layerConfig, tags, state)), + undefined, + state.featureSwitchUserbadge + )) + return new Combine(allRenderings).SetClass("block") + } + /** + * All the edit elements, together (note that the question boxes are passed though) + * @param questionBoxes + * @param layerConfig + * @param tags + * @param state + * @private + */ + private static createEditElements(questionBoxes: Map, + layerConfig: LayerConfig, + tags: UIEventSource, + state: { filteredLayers: UIEventSource; backgroundLayer: UIEventSource; featureSwitchIsTesting: UIEventSource; featureSwitchIsDebugging: UIEventSource; featureSwitchShowAllQuestions: UIEventSource; osmConnection: OsmConnection; featureSwitchUserbadge: UIEventSource; changes: Changes; layoutToUse: LayoutConfig; allElements: ElementStorage }) + : BaseUIElement { let editElements: BaseUIElement[] = [] questionBoxes.forEach(questionBox => { editElements.push(questionBox); @@ -131,10 +187,13 @@ export default class FeatureInfoBox extends ScrollableFullScreen { if (layerConfig.allowMove) { editElements.push( new VariableUiElement(tags.map(tags => tags.id).map(id => { - const feature = State.state.allElements.ContainingFeatures.get(id) + const feature = state.allElements.ContainingFeatures.get(id) + if (feature === undefined) { + return "This feature is not register in the state.allElements and cannot be moved" + } return new MoveWizard( feature, - State.state, + state, layerConfig.allowMove ); }) @@ -147,6 +206,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { new VariableUiElement(tags.map(tags => tags.id).map(id => new DeleteWizard( id, + state, layerConfig.deletion )) ).SetClass("text-base")) @@ -155,59 +215,45 @@ export default class FeatureInfoBox extends ScrollableFullScreen { if (layerConfig.allowSplit) { editElements.push( new VariableUiElement(tags.map(tags => tags.id).map(id => - new SplitRoadWizard(id)) + new SplitRoadWizard(id, state)) ).SetClass("text-base")) } editElements.push( new VariableUiElement( - State.state.osmConnection.userDetails + state.osmConnection.userDetails .map(ud => ud.csCount) .map(csCount => { if (csCount <= Constants.userJourney.historyLinkVisible - && State.state.featureSwitchIsDebugging.data == false - && State.state.featureSwitchIsTesting.data === false) { + && state.featureSwitchIsDebugging.data == false + && state.featureSwitchIsTesting.data === false) { return undefined } - return new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("last_edit"), State.state); + return new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("last_edit"), state); - }, [State.state.featureSwitchIsDebugging, State.state.featureSwitchIsTesting]) + }, [state.featureSwitchIsDebugging, state.featureSwitchIsTesting]) ) ) editElements.push( new VariableUiElement( - State.state.featureSwitchIsDebugging.map(isDebugging => { + state.featureSwitchIsDebugging.map(isDebugging => { if (isDebugging) { const config_all_tags: TagRenderingConfig = new TagRenderingConfig({render: "{all_tags()}"}, ""); const config_download: TagRenderingConfig = new TagRenderingConfig({render: "{export_as_geojson()}"}, ""); const config_id: TagRenderingConfig = new TagRenderingConfig({render: "{open_in_iD()}"}, ""); - return new Combine([new TagRenderingAnswer(tags, config_all_tags, State.state), - new TagRenderingAnswer(tags, config_download, State.state), - new TagRenderingAnswer(tags, config_id, State.state)]) + return new Combine([new TagRenderingAnswer(tags, config_all_tags, state), + new TagRenderingAnswer(tags, config_download, state), + new TagRenderingAnswer(tags, config_id, state)]) } }) ) ) - const editors = new VariableUiElement(State.state.featureSwitchUserbadge.map( - userbadge => { - if (!userbadge) { - return undefined - } - return new Combine(editElements).SetClass("flex flex-col") - } - )) - allRenderings.push(editors) - - return new Combine(allRenderings).SetClass("block") + return new Combine(editElements).SetClass("flex flex-col") } - - - - } diff --git a/UI/Popup/ImportButton.ts b/UI/Popup/ImportButton.ts index 1d65a24321..2d5bc2f37d 100644 --- a/UI/Popup/ImportButton.ts +++ b/UI/Popup/ImportButton.ts @@ -246,10 +246,10 @@ ${Utils.special_visualizations_importRequirementDocs} // SHow all relevant data - including (eventually) the way of which the geometry will be replaced new ShowDataMultiLayer({ leafletMap: confirmationMap.leafletMap, - enablePopups: false, + popup: undefined, zoomToFeatures: true, features: new StaticFeatureSource([feature], false), - allElements: state.allElements, + state: state, layers: state.filteredLayers }) @@ -257,10 +257,10 @@ ${Utils.special_visualizations_importRequirementDocs} action.getPreview().then(changePreview => { new ShowDataLayer({ leafletMap: confirmationMap.leafletMap, - enablePopups: false, + popup: undefined, zoomToFeatures: false, features: changePreview, - allElements: state.allElements, + state, layerToShow: new LayerConfig(conflation_json, "all_known_layers", true) }) }) diff --git a/UI/Popup/QuestionBox.ts b/UI/Popup/QuestionBox.ts index 6ca5a59694..918c15d1e9 100644 --- a/UI/Popup/QuestionBox.ts +++ b/UI/Popup/QuestionBox.ts @@ -16,7 +16,7 @@ export default class QuestionBox extends VariableUiElement { public readonly skippedQuestions: UIEventSource; public readonly restingQuestions: UIEventSource; - constructor(options: { + constructor(state, options: { tagsSource: UIEventSource, tagRenderings: TagRenderingConfig[], units: Unit[], showAllQuestionsAtOnce?: boolean | UIEventSource @@ -34,7 +34,7 @@ export default class QuestionBox extends VariableUiElement { const tagRenderingQuestions = tagRenderings .map((tagRendering, i) => - new Lazy(() => new TagRenderingQuestion(tagsSource, tagRendering, + new Lazy(() => new TagRenderingQuestion(tagsSource, tagRendering, state, { units: units, afterSave: () => { diff --git a/UI/Popup/SplitRoadWizard.ts b/UI/Popup/SplitRoadWizard.ts index daeb7bbb12..4859763c20 100644 --- a/UI/Popup/SplitRoadWizard.ts +++ b/UI/Popup/SplitRoadWizard.ts @@ -3,7 +3,6 @@ import Svg from "../../Svg"; import {UIEventSource} from "../../Logic/UIEventSource"; import {SubtleButton} from "../Base/SubtleButton"; import Minimap from "../Base/Minimap"; -import State from "../../State"; import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; import {GeoOperations} from "../../Logic/GeoOperations"; import {LeafletMouseEvent} from "leaflet"; @@ -17,6 +16,12 @@ import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; import {BBox} from "../../Logic/BBox"; import * as split_point from "../../assets/layers/split_point/split_point.json" +import {OsmConnection} from "../../Logic/Osm/OsmConnection"; +import {Changes} from "../../Logic/Osm/Changes"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import {ElementStorage} from "../../Logic/ElementStorage"; +import BaseLayer from "../../Models/BaseLayer"; +import FilteredLayer from "../../Models/FilteredLayer"; export default class SplitRoadWizard extends Toggle { // @ts-ignore @@ -28,8 +33,19 @@ export default class SplitRoadWizard extends Toggle { * A UI Element used for splitting roads * * @param id: The id of the road to remove + * @param state: the state of the application */ - constructor(id: string) { + constructor(id: string, state: { + filteredLayers: UIEventSource; + backgroundLayer: UIEventSource; + featureSwitchIsTesting: UIEventSource; + featureSwitchIsDebugging: UIEventSource; + featureSwitchShowAllQuestions: UIEventSource; + osmConnection: OsmConnection, + featureSwitchUserbadge: UIEventSource, + changes: Changes, + layoutToUse: LayoutConfig, + allElements: ElementStorage}) { const t = Translations.t.split; @@ -41,12 +57,12 @@ export default class SplitRoadWizard extends Toggle { // Toggle variable between show split button and map const splitClicked = new UIEventSource(false); // Load the road with given id on the minimap - const roadElement = State.state.allElements.ContainingFeatures.get(id) + const roadElement = state.allElements.ContainingFeatures.get(id) // Minimap on which you can select the points to be splitted const miniMap = Minimap.createMiniMap( { - background: State.state.backgroundLayer, + background: state.backgroundLayer, allowMoving: true, leafletOptions: { minZoom: 14 @@ -62,19 +78,20 @@ export default class SplitRoadWizard extends Toggle { // Datalayer displaying the road and the cut points (if any) new ShowDataMultiLayer({ features: new StaticFeatureSource([roadElement], false), - layers: State.state.filteredLayers, + layers: state.filteredLayers, leafletMap: miniMap.leafletMap, - enablePopups: false, + popup: undefined, zoomToFeatures: true, - allElements: State.state.allElements, + state }) new ShowDataLayer({ features: new StaticFeatureSource(splitPoints, true), leafletMap: miniMap.leafletMap, zoomToFeatures: false, - enablePopups: false, + popup: undefined, layerToShow: SplitRoadWizard.splitLayerStyling, + state }) @@ -127,15 +144,15 @@ export default class SplitRoadWizard extends Toggle { // Only show the splitButton if logged in, else show login prompt const loginBtn = t.loginToSplit.Clone() - .onClick(() => State.state.osmConnection.AttemptLogin()) + .onClick(() => state.osmConnection.AttemptLogin()) .SetClass("login-button-friendly"); - const splitToggle = new Toggle(splitButton, loginBtn, State.state.osmConnection.isLoggedIn) + const splitToggle = new Toggle(splitButton, loginBtn, state.osmConnection.isLoggedIn) // Save button const saveButton = new Button(t.split.Clone(), () => { hasBeenSplit.setData(true) - State.state.changes.applyAction(new SplitAction(id, splitPoints.data.map(ff => ff.feature.geometry.coordinates), { - theme: State.state?.layoutToUse?.id + state.changes.applyAction(new SplitAction(id, splitPoints.data.map(ff => ff.feature.geometry.coordinates), { + theme: state?.layoutToUse?.id })) }) diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 76313e2fd3..ed76f5bf26 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -8,7 +8,6 @@ import {Utils} from "../../Utils"; import CheckBoxes from "../Input/Checkboxes"; import InputElementMap from "../Input/InputElementMap"; import {SaveButton} from "./SaveButton"; -import State from "../../State"; import {VariableUiElement} from "../Base/VariableUIElement"; import Translations from "../i18n/Translations"; import {FixedUiElement} from "../Base/FixedUiElement"; @@ -36,6 +35,7 @@ export default class TagRenderingQuestion extends Combine { constructor(tags: UIEventSource, configuration: TagRenderingConfig, + state, options?: { units?: Unit[], afterSave?: () => void, @@ -71,23 +71,23 @@ export default class TagRenderingQuestion extends Combine { } options = options ?? {} const applicableUnit = (options.units ?? []).filter(unit => unit.isApplicableToKey(configuration.freeform?.key))[0]; - const question = new SubstitutedTranslation(configuration.question, tags, State.state) + const question = new SubstitutedTranslation(configuration.question, tags, state) .SetClass("question-text"); const inputElement: InputElement = new VariableInputElement(applicableMappingsSrc.map(applicableMappings => - TagRenderingQuestion.GenerateInputElement(configuration, applicableMappings, applicableUnit, tags) + TagRenderingQuestion.GenerateInputElement(state, configuration, applicableMappings, applicableUnit, tags) )) const save = () => { const selection = inputElement.GetValue().data; if (selection) { - (State.state?.changes) + (state?.changes) .applyAction(new ChangeTagAction( tags.data.id, selection, tags.data, { - theme: State.state?.layoutToUse?.id ?? "unkown", + theme: state?.layoutToUse?.id ?? "unkown", changeType: "answer", } )).then(_ => { @@ -101,13 +101,13 @@ export default class TagRenderingQuestion extends Combine { if (options.saveButtonConstr === undefined) { options.saveButtonConstr = v => new SaveButton(v, - State.state?.osmConnection) + state?.osmConnection) .onClick(save) } const saveButton = new Combine([ options.saveButtonConstr(inputElement.GetValue()), - new Toggle(Translations.t.general.testing.SetClass("alert"), undefined, State.state.featureSwitchIsTesting) + new Toggle(Translations.t.general.testing.SetClass("alert"), undefined, state.featureSwitchIsTesting) ]) let bottomTags: BaseUIElement; @@ -117,7 +117,7 @@ export default class TagRenderingQuestion extends Combine { bottomTags = new VariableUiElement( inputElement.GetValue().map( (tagsFilter: TagsFilter) => { - const csCount = State.state?.osmConnection?.userDetails?.data?.csCount ?? 1000; + const csCount = state?.osmConnection?.userDetails?.data?.csCount ?? 1000; if (csCount < Constants.userJourney.tagsVisibleAt) { return ""; } @@ -145,14 +145,16 @@ export default class TagRenderingQuestion extends Combine { } - private static GenerateInputElement(configuration: TagRenderingConfig, - applicableMappings: { if: TagsFilter, then: any, ifnot?: TagsFilter, addExtraTags: Tag[] }[], - applicableUnit: Unit, - tagsSource: UIEventSource) + private static GenerateInputElement( + state, + configuration: TagRenderingConfig, + applicableMappings: { if: TagsFilter, then: any, ifnot?: TagsFilter, addExtraTags: Tag[] }[], + applicableUnit: Unit, + tagsSource: UIEventSource) : InputElement { // FreeForm input will be undefined if not present; will already contain a special input element if applicable - const ff = TagRenderingQuestion.GenerateFreeform(configuration, applicableUnit, tagsSource); + const ff = TagRenderingQuestion.GenerateFreeform(state, configuration, applicableUnit, tagsSource); const hasImages = applicableMappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0 @@ -188,7 +190,7 @@ export default class TagRenderingQuestion extends Combine { if (applicableMappings.length < 8 || configuration.multiAnswer || hasImages || ifNotsPresent) { - inputEls = (applicableMappings ?? []).map((mapping, i) => TagRenderingQuestion.GenerateMappingElement(tagsSource, mapping, allIfNotsExcept(i))); + inputEls = (applicableMappings ?? []).map((mapping, i) => TagRenderingQuestion.GenerateMappingElement(state, tagsSource, mapping, allIfNotsExcept(i))); inputEls = Utils.NoNull(inputEls); } else { const dropdown: InputElement = new DropDown("", @@ -336,6 +338,7 @@ export default class TagRenderingQuestion extends Combine { * Returns: [the element itself, the value to select if not selected. The contents of this UIEventSource might swap to undefined if the conditions to show the answer are unmet] */ private static GenerateMappingElement( + state, tagsSource: UIEventSource, mapping: { if: TagsFilter, @@ -352,12 +355,12 @@ export default class TagRenderingQuestion extends Combine { } return new FixedInputElement( - new SubstitutedTranslation(mapping.then, tagsSource, State.state), + new SubstitutedTranslation(mapping.then, tagsSource, state), tagging, (t0, t1) => t1.isEquivalent(t0)); } - private static GenerateFreeform(configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource): InputElement { + private static GenerateFreeform(state, configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource): InputElement { const freeform = configuration.freeform; if (freeform === undefined) { return undefined; @@ -397,12 +400,12 @@ export default class TagRenderingQuestion extends Combine { } const tagsData = tags.data; - const feature = State.state.allElements.ContainingFeatures.get(tagsData.id) + const feature = state.allElements.ContainingFeatures.get(tagsData.id) const input: InputElement = ValidatedTextField.InputForType(configuration.freeform.type, { isValid: (str) => (str.length <= 255), country: () => tagsData._country, location: [tagsData._lat, tagsData._lon], - mapBackgroundLayer: State.state.backgroundLayer, + mapBackgroundLayer: state.backgroundLayer, unit: applicableUnit, args: configuration.freeform.helperArgs, feature: feature @@ -418,7 +421,7 @@ export default class TagRenderingQuestion extends Combine { if (freeform.inline) { inputTagsFilter.SetClass("w-16-imp") - inputTagsFilter = new InputElementWrapper(inputTagsFilter, configuration.render, freeform.key, tags, State.state) + inputTagsFilter = new InputElementWrapper(inputTagsFilter, configuration.render, freeform.key, tags, state) inputTagsFilter.SetClass("block") } diff --git a/UI/ShowDataLayer/ShowDataLayer.ts b/UI/ShowDataLayer/ShowDataLayer.ts index 27ed05f150..92db8c6dbd 100644 --- a/UI/ShowDataLayer/ShowDataLayer.ts +++ b/UI/ShowDataLayer/ShowDataLayer.ts @@ -1,9 +1,9 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import FeatureInfoBox from "../Popup/FeatureInfoBox"; import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; import {ElementStorage} from "../../Logic/ElementStorage"; import RenderingMultiPlexerFeatureSource from "../../Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource"; +import ScrollableFullScreen from "../Base/ScrollableFullScreen"; /* // import 'leaflet-polylineoffset'; We don't actually import it here. It is imported in the 'MinimapImplementation'-class, which'll result in a patched 'L' object. @@ -44,12 +44,12 @@ export default class ShowDataLayer { */ private readonly leafletLayersPerId = new Map() private readonly showDataLayerid: number; - + private readonly createPopup : (tags: any, layer: LayerConfig) => ScrollableFullScreen + constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) { this._leafletMap = options.leafletMap; this.showDataLayerid = ShowDataLayer.dataLayerIds; ShowDataLayer.dataLayerIds++ - this._enablePopups = options.enablePopups ?? true; if (options.features === undefined) { console.error("Invalid ShowDataLayer invocation: options.features is undefed") throw "Invalid ShowDataLayer invocation: options.features is undefed" @@ -57,7 +57,12 @@ export default class ShowDataLayer { this._features = new RenderingMultiPlexerFeatureSource(options.features, options.layerToShow); this._layerToShow = options.layerToShow; this._selectedElement = options.selectedElement - this.allElements = options.allElements; + this.allElements = options.state?.allElements; + this.createPopup = undefined; + this._enablePopups = options.popup !== undefined; + if(options.popup !== undefined){ + this.createPopup = options.popup + } const self = this; options.leafletMap.addCallback(_ => { @@ -300,14 +305,14 @@ export default class ShowDataLayer { leafletLayer.bindPopup(popup); - let infobox: FeatureInfoBox = undefined; - + let infobox: ScrollableFullScreen = undefined; const id = `popup-${feature.properties.id}-${feature.geometry.type}-${this.showDataLayerid}-${this._cleanCount}-${feature.pointRenderingIndex ?? feature.lineRenderingIndex}-${feature.multiLineStringIndex ?? ""}` popup.setContent(`
Popup for ${feature.properties.id} ${feature.geometry.type} ${id} is loading
`) + const createpopup = this.createPopup; leafletLayer.on("popupopen", () => { if (infobox === undefined) { const tags = this.allElements?.getEventSourceById(feature.properties.id) ?? new UIEventSource(feature.properties); - infobox = new FeatureInfoBox(tags, layer); + infobox = createpopup(tags, layer ); infobox.isShown.addCallback(isShown => { if (!isShown) { diff --git a/UI/ShowDataLayer/ShowDataLayerOptions.ts b/UI/ShowDataLayer/ShowDataLayerOptions.ts index 4b811be68a..cfeb31e438 100644 --- a/UI/ShowDataLayer/ShowDataLayerOptions.ts +++ b/UI/ShowDataLayer/ShowDataLayerOptions.ts @@ -1,13 +1,20 @@ import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; import {UIEventSource} from "../../Logic/UIEventSource"; import {ElementStorage} from "../../Logic/ElementStorage"; +import {OsmConnection} from "../../Logic/Osm/OsmConnection"; +import {Changes} from "../../Logic/Osm/Changes"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import FilteredLayer from "../../Models/FilteredLayer"; +import BaseLayer from "../../Models/BaseLayer"; +import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; +import ScrollableFullScreen from "../Base/ScrollableFullScreen"; export interface ShowDataLayerOptions { features: FeatureSource, selectedElement?: UIEventSource, - allElements?: ElementStorage, leafletMap: UIEventSource, - enablePopups?: true | boolean, + popup?: undefined | ((tags: any, layer: LayerConfig) => ScrollableFullScreen), zoomToFeatures?: false | boolean, - doShowLayer?: UIEventSource + doShowLayer?: UIEventSource, + state?: {allElements?: ElementStorage} } \ No newline at end of file diff --git a/UI/ShowDataLayer/ShowTileInfo.ts b/UI/ShowDataLayer/ShowTileInfo.ts index 702e9d9c83..6fd296dbe1 100644 --- a/UI/ShowDataLayer/ShowTileInfo.ts +++ b/UI/ShowDataLayer/ShowTileInfo.ts @@ -6,6 +6,8 @@ import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeature import {GeoOperations} from "../../Logic/GeoOperations"; import {Tiles} from "../../Models/TileRange"; import * as clusterstyle from "../../assets/layers/cluster_style/cluster_style.json" +import State from "../../State"; +import FeatureInfoBox from "../Popup/FeatureInfoBox"; export default class ShowTileInfo { public static readonly styling = new LayerConfig(clusterstyle, "ShowTileInfo", true) @@ -53,7 +55,9 @@ export default class ShowTileInfo { layerToShow: ShowTileInfo.styling, features: new StaticFeatureSource(metaFeature, false), leafletMap: options.leafletMap, - doShowLayer: options.doShowLayer + doShowLayer: options.doShowLayer, + state: State.state, + popup: undefined }) } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index b15d58b8e2..8d89a61f36 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -12,7 +12,6 @@ import MangroveReviews from "../Logic/Web/MangroveReviews"; import Translations from "./i18n/Translations"; import ReviewForm from "./Reviews/ReviewForm"; import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"; -import State from "../State"; import BaseUIElement from "./BaseUIElement"; import Title from "./Base/Title"; import Table from "./Base/Table"; @@ -100,10 +99,10 @@ export default class SpecialVisualizations { funcName: "all_tags", docs: "Prints all key-value pairs of the object - used for debugging", args: [], - constr: ((state: State, tags: UIEventSource) => { + constr: ((state, tags: UIEventSource) => { const calculatedTags = [].concat( SimpleMetaTagger.lazyTags, - ...state.layoutToUse.layers.map(l => l.calculatedTags?.map(c => c[0]) ?? [])) + ...(state?.layoutToUse?.layers?.map(l => l.calculatedTags?.map(c => c[0]) ?? []) ?? [])) return new VariableUiElement(tags.map(tags => { const parts = []; for (const key in tags) { @@ -129,7 +128,7 @@ export default class SpecialVisualizations { ["key", "value"], parts ) - })).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;") + })).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;").SetClass("zebra-table") }) }, { @@ -140,7 +139,7 @@ export default class SpecialVisualizations { defaultValue: AllImageProviders.defaultKeys.join(","), doc: "The keys given to the images, e.g. if image is given, the first picture URL will be added as image, the second as image:0, the third as image:1, etc... " }], - constr: (state: State, tags, args) => { + constr: (state, tags, args) => { let imagePrefixes: string[] = undefined; if (args.length > 0) { imagePrefixes = [].concat(...args.map(a => a.split(","))); @@ -160,7 +159,7 @@ export default class SpecialVisualizations { doc: "The text to show on the button", defaultValue: "Add image" }], - constr: (state: State, tags, args) => { + constr: (state, tags, args) => { return new ImageUploadFlow(tags, state, args[0], args[1]) } }, @@ -259,11 +258,10 @@ export default class SpecialVisualizations { new ShowDataMultiLayer( { leafletMap: minimap["leafletMap"], - enablePopups: false, + popup: undefined, zoomToFeatures: true, - layers: State.state.filteredLayers, - features: new StaticFeatureSource(featuresToShow, true), - allElements: State.state.allElements + layers: state.filteredLayers, + features: new StaticFeatureSource(featuresToShow, true) } ) @@ -306,11 +304,11 @@ export default class SpecialVisualizations { new ShowDataLayer( { leafletMap: minimap["leafletMap"], - enablePopups: false, + popup: undefined, zoomToFeatures: true, layerToShow: new LayerConfig(left_right_style_json, "all_known_layers", true), features: new StaticFeatureSource([copy], false), - allElements: State.state.allElements + state } ) @@ -331,7 +329,7 @@ export default class SpecialVisualizations { name: "fallback", doc: "The identifier to use, if tags[subjectKey] as specified above is not available. This is effectively a fallback value" }], - constr: (state: State, tags, args) => { + constr: (state, tags, args) => { const tgs = tags.data; const key = args[0] ?? "name" let subject = tgs[key] ?? args[1]; @@ -364,7 +362,7 @@ export default class SpecialVisualizations { doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__" }], example: "A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`", - constr: (state: State, tagSource: UIEventSource, args) => { + constr: (state, tagSource: UIEventSource, args) => { return new OpeningHoursVisualization(tagSource, state, args[0], args[1], args[2]) } }, @@ -380,7 +378,7 @@ export default class SpecialVisualizations { }, { name: "path", doc: "The path (or shorthand) that should be returned" }], - constr: (state: State, tagSource: UIEventSource, args) => { + constr: (state, tagSource: UIEventSource, args) => { const url = args[0]; const shorthands = args[1]; const neededValue = args[2]; @@ -413,7 +411,7 @@ export default class SpecialVisualizations { } ], - constr: (state: State, tagSource: UIEventSource, args: string[]) => { + constr: (state, tagSource: UIEventSource, args: string[]) => { let assignColors = undefined; if (args.length >= 3) { @@ -461,7 +459,7 @@ export default class SpecialVisualizations { doc: "The url to share (default: current URL)", } ], - constr: (state: State, tagSource: UIEventSource, args) => { + constr: (state, tagSource: UIEventSource, args) => { if (window.navigator.share) { const generateShareData = () => { @@ -580,7 +578,7 @@ export default class SpecialVisualizations { funcName: "export_as_gpx", docs: "Exports the selected feature as GPX-file", args: [], - constr: (state, tagSource, args) => { + constr: (state, tagSource) => { const t = Translations.t.general.download; return new SubtleButton(Svg.download_ui(), @@ -605,7 +603,7 @@ export default class SpecialVisualizations { funcName: "export_as_geojson", docs: "Exports the selected feature as GeoJson-file", args: [], - constr: (state, tagSource, args) => { + constr: (state, tagSource) => { const t = Translations.t.general.download; return new SubtleButton(Svg.download_ui(), @@ -672,7 +670,7 @@ export default class SpecialVisualizations { doc: "Text to add onto the note when closing", } ], - constr: (state, tags, args, guiState) => { + constr: (state, tags, args) => { const t = Translations.t.notes; let icon = Svg.checkmark_svg() @@ -711,7 +709,7 @@ export default class SpecialVisualizations { defaultValue: "id" } ], - constr: (state, tags, args, guiState) => { + constr: (state, tags, args) => { const t = Translations.t.notes; const textField = ValidatedTextField.InputForType("text", {placeholder: t.addCommentPlaceholder}) diff --git a/assets/layers/import_candidate/import_candidate.json b/assets/layers/import_candidate/import_candidate.json new file mode 100644 index 0000000000..70720181e7 --- /dev/null +++ b/assets/layers/import_candidate/import_candidate.json @@ -0,0 +1,26 @@ +{ + "id": "import_candidate", + "description": "Layer used in the importHelper", + "source": { + "osmTags": { + "and": [] + } + }, + "mapRendering": [ + { + "location": [ + "point", + "centroid" + ], + "icon": "square:red;", + "iconSize": "15,15,center" + } + ], + "title": "Import candidate", + "tagRenderings": [ + { + "id": "all_tags", + "render": "{all_tags()}" + } + ] +} \ No newline at end of file diff --git a/assets/layers/public_bookcase/public_bookcase.json b/assets/layers/public_bookcase/public_bookcase.json index 82fa6f1299..5fc32a4e08 100644 --- a/assets/layers/public_bookcase/public_bookcase.json +++ b/assets/layers/public_bookcase/public_bookcase.json @@ -68,10 +68,6 @@ ], "tagRenderings": [ "images", - { - "id": "minimap", - "render": "{minimap():height: 9rem; border-radius: 2.5rem; overflow:hidden;border:1px solid gray}" - }, { "render": { "en": "The name of this bookcase is {name}", diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index 1191b9bcba..9111ea1b5f 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -912,6 +912,10 @@ video { margin-left: 0.25rem; } +.mb-24 { + margin-bottom: 6rem; +} + .mr-0 { margin-right: 0px; } @@ -1178,6 +1182,10 @@ video { flex-grow: 1; } +.table-auto { + table-layout: auto; +} + .border-collapse { border-collapse: collapse; } @@ -1272,6 +1280,12 @@ video { gap: 1rem; } +.space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + .self-end { align-self: flex-end; } @@ -2335,6 +2349,10 @@ li::marker { overflow-y: hidden; } +.zebra-table tr:nth-child(even) { + background-color: #f2f2f2; +} + .hover\:bg-blue-200:hover { --tw-bg-opacity: 1; background-color: rgba(191, 219, 254, var(--tw-bg-opacity)); @@ -2625,4 +2643,4 @@ li::marker { .xl\:inline { display: inline; } -} +} \ No newline at end of file diff --git a/index.css b/index.css index 7f7262740f..4acf261439 100644 --- a/index.css +++ b/index.css @@ -482,3 +482,5 @@ li::marker { transition: max-height .5s ease-in-out; overflow-y: hidden; } + +.zebra-table tr:nth-child(even) {background-color: #f2f2f2;} \ No newline at end of file diff --git a/index.ts b/index.ts index 024b40f795..64cf22e978 100644 --- a/index.ts +++ b/index.ts @@ -14,7 +14,6 @@ import {DefaultGuiState} from "./UI/DefaultGuiState"; // Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts running from console MinimapImplementation.initialize() -AvailableBaseLayers.implement(new AvailableBaseLayersImplementation()) ShowOverlayLayerImplementation.Implement(); // Miscelleanous Utils.DisableLongPresses() diff --git a/langs/en.json b/langs/en.json index 1d197fa5ae..03bf34682d 100644 --- a/langs/en.json +++ b/langs/en.json @@ -70,6 +70,9 @@ "readMessages": "You have unread messages. Read these before deleting a point - someone might have feedback" }, "general": { + "next": "Next", + "confirm": "Confirm", + "back": "Back", "backToMapcomplete": "Back to the theme overview", "loading": "Loading...", "pdf": { @@ -467,6 +470,12 @@ "importHelper": { "title": "Import helper", "description": "The import helper converts an external dataset to notes", - "lockNotice": "This page is locked. You need {importHelperUnlock} changesets before you can access here." + "lockNotice": "This page is locked. You need {importHelperUnlock} changesets before you can access here.", + "selectFile": "Select a .csv or .geojson file to get started", + "loadedFilesAre": "Currently loaded file is {file}", + "noFilesLoaded": "No file is currently loaded", + "selectLayer": "Select a layer...", + "selectFileTitle": "Select file", + "validateDataTitle": "Validate data" } } diff --git a/package.json b/package.json index 8593cc1fe8..0aa0b4c39d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "generate:polygon-features": "ts-node scripts/downloadFile.ts https://raw.githubusercontent.com/tyrasd/osm-polygon-features/master/polygon-features.json assets/polygon-features.json", "generate:images": "ts-node scripts/generateIncludedImages.ts", "generate:translations": "ts-node scripts/generateTranslations.ts", - "watch:translations": "cd langs && ls | entr npm run generate:translations", + "watch:translations": "cd langs && ls | entr -c npm run generate:translations", "reset:translations": "ts-node scripts/generateTranslations.ts --ignore-weblate", "generate:layouts": "ts-node scripts/generateLayouts.ts", "generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts", diff --git a/scripts/slice.ts b/scripts/slice.ts index 8fab5c52e8..ecb66ed785 100644 --- a/scripts/slice.ts +++ b/scripts/slice.ts @@ -126,10 +126,6 @@ async function main(args: string[]) { } delete f.bbox } - - //const knownKeys = Utils.Dedup([].concat(...allFeatures.map(f => Object.keys(f.properties)))) - //console.log("Kept keys: ", knownKeys) - TiledFeatureSource.createHierarchy( new StaticFeatureSource(allFeatures, false), {