From f7eaec2243a1de03ff28521996ce891424787fe1 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 5 May 2023 02:03:41 +0200 Subject: [PATCH] Refactoring: remove import flow, fix various issues, get PDF-export working (but not quite) --- Logic/Osm/Actions/ReplaceGeometryAction.ts | 2 +- .../Conversion/CreateNoteImportLayer.ts | 8 +- UI/AllThemesGui.ts | 2 - UI/BigComponents/ExtraLinkButton.ts | 46 +- UI/BigComponents/LeftControls.ts | 62 -- UI/BigComponents/ShareScreen.ts | 27 +- UI/BigComponents/ThemeIntroductionPanel.ts | 88 -- UI/BigComponents/UserInformation.ts | 68 -- UI/DefaultGUI.ts | 48 -- UI/DefaultGuiState.ts | 56 -- UI/ImportFlow/AskMetadata.ts | 128 --- .../CompareToAlreadyExistingNotes.ts | 196 ----- UI/ImportFlow/ConfirmProcess.ts | 35 - UI/ImportFlow/ConflationChecker.ts | 359 -------- UI/ImportFlow/CreateNotes.ts | 136 --- UI/ImportFlow/FlowStep.ts | 151 ---- UI/ImportFlow/ImportHelperGui.ts | 83 -- UI/ImportFlow/ImportUtils.ts | 44 - UI/ImportFlow/ImportViewerGui.ts | 800 ------------------ UI/ImportFlow/Introdution.ts | 43 - UI/ImportFlow/LoginToImport.ts | 74 -- UI/ImportFlow/MapPreview.ts | 162 ---- UI/ImportFlow/PreviewPanel.ts | 94 -- UI/ImportFlow/RequestFile.ts | 188 ---- UI/ImportFlow/SelectTheme.ts | 192 ----- UI/Map/MapLibreAdaptor.ts | 18 +- .../TagRendering/TagRenderingQuestion.svelte | 6 +- UI/QueryParameterDocumentation.ts | 4 +- UI/ThemeViewGUI.svelte | 8 +- Utils/pngMapCreator.ts | 145 +--- Utils/svgToPdf.ts | 715 ++++++++-------- assets/themes/natuurpunt/natuurpunt.css | 1 - css/index-tailwind-output.css | 25 +- index.css | 520 ++++++------ package-lock.json | 120 --- test.ts | 15 +- 36 files changed, 739 insertions(+), 3930 deletions(-) delete mode 100644 UI/BigComponents/LeftControls.ts delete mode 100644 UI/BigComponents/ThemeIntroductionPanel.ts delete mode 100644 UI/BigComponents/UserInformation.ts delete mode 100644 UI/DefaultGUI.ts delete mode 100644 UI/DefaultGuiState.ts delete mode 100644 UI/ImportFlow/AskMetadata.ts delete mode 100644 UI/ImportFlow/CompareToAlreadyExistingNotes.ts delete mode 100644 UI/ImportFlow/ConfirmProcess.ts delete mode 100644 UI/ImportFlow/ConflationChecker.ts delete mode 100644 UI/ImportFlow/CreateNotes.ts delete mode 100644 UI/ImportFlow/FlowStep.ts delete mode 100644 UI/ImportFlow/ImportHelperGui.ts delete mode 100644 UI/ImportFlow/ImportUtils.ts delete mode 100644 UI/ImportFlow/ImportViewerGui.ts delete mode 100644 UI/ImportFlow/Introdution.ts delete mode 100644 UI/ImportFlow/LoginToImport.ts delete mode 100644 UI/ImportFlow/MapPreview.ts delete mode 100644 UI/ImportFlow/PreviewPanel.ts delete mode 100644 UI/ImportFlow/RequestFile.ts delete mode 100644 UI/ImportFlow/SelectTheme.ts diff --git a/Logic/Osm/Actions/ReplaceGeometryAction.ts b/Logic/Osm/Actions/ReplaceGeometryAction.ts index e131a1c58..9efe482b3 100644 --- a/Logic/Osm/Actions/ReplaceGeometryAction.ts +++ b/Logic/Osm/Actions/ReplaceGeometryAction.ts @@ -465,7 +465,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { changeType: "conflation", } ) - allChanges.push(...(await addExtraTags.CreateChangeDescriptions(changes))) + allChanges.push(...(await addExtraTags.CreateChangeDescriptions())) } const newCoordinates = [...this.targetCoordinates] diff --git a/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts b/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts index 609e3fafc..d74f1f8f5 100644 --- a/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts +++ b/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts @@ -1,9 +1,9 @@ -import { Conversion } from "./Conversion" +import {Conversion} from "./Conversion" import LayerConfig from "../LayerConfig" -import { LayerConfigJson } from "../Json/LayerConfigJson" +import {LayerConfigJson} from "../Json/LayerConfigJson" import Translations from "../../../UI/i18n/Translations" import PointRenderingConfigJson from "../Json/PointRenderingConfigJson" -import { Translation, TypedTranslation } from "../../../UI/i18n/Translation" +import {Translation, TypedTranslation} from "../../../UI/i18n/Translation" export default class CreateNoteImportLayer extends Conversion { /** @@ -82,7 +82,7 @@ export default class CreateNoteImportLayer extends Conversion(translation: TypedTranslation, subs: T): object { + function trs(translation: TypedTranslation, subs: T): Record { return { ...translation.Subs(subs).translations, _context: translation.context } } diff --git a/UI/AllThemesGui.ts b/UI/AllThemesGui.ts index d9e93ad36..f71c34ccf 100644 --- a/UI/AllThemesGui.ts +++ b/UI/AllThemesGui.ts @@ -6,7 +6,6 @@ import Translations from "./i18n/Translations" import Constants from "../Models/Constants" import LanguagePicker from "./LanguagePicker" import IndexText from "./BigComponents/IndexText" -import { ImportViewerLinks } from "./BigComponents/UserInformation" import { LoginToggle } from "./Popup/LoginButton" import { ImmutableStore } from "../Logic/UIEventSource" import { OsmConnection } from "../Logic/Osm/OsmConnection" @@ -29,7 +28,6 @@ export default class AllThemesGui { osmConnection, featureSwitchUserbadge: new ImmutableStore(true), }), - new ImportViewerLinks(state.osmConnection), Translations.t.general.aboutMapComplete.intro.SetClass("link-underline"), new FixedUiElement("v" + Constants.vNumber).SetClass("block"), ]) diff --git a/UI/BigComponents/ExtraLinkButton.ts b/UI/BigComponents/ExtraLinkButton.ts index fcc9fd77d..80287c73f 100644 --- a/UI/BigComponents/ExtraLinkButton.ts +++ b/UI/BigComponents/ExtraLinkButton.ts @@ -1,31 +1,30 @@ -import { UIElement } from "../UIElement" +import {UIElement} from "../UIElement" import BaseUIElement from "../BaseUIElement" -import { UIEventSource } from "../../Logic/UIEventSource" +import {Store} from "../../Logic/UIEventSource" import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig" import Img from "../Base/Img" -import { SubtleButton } from "../Base/SubtleButton" +import {SubtleButton} from "../Base/SubtleButton" import Toggle from "../Input/Toggle" -import Loc from "../../Models/Loc" import Locale from "../i18n/Locale" -import { Utils } from "../../Utils" +import {Utils} from "../../Utils" import Svg from "../../Svg" import Translations from "../i18n/Translations" -import { Translation } from "../i18n/Translation" +import {Translation} from "../i18n/Translation" +interface ExtraLinkButtonState { + layout: { id: string; title: Translation } + featureSwitches: { featureSwitchWelcomeMessage: Store }, + mapProperties: { + location: Store<{ lon: number, lat: number }>; + zoom: Store + } +} export default class ExtraLinkButton extends UIElement { private readonly _config: ExtraLinkConfig - private readonly state: { - layoutToUse: { id: string; title: Translation } - featureSwitchWelcomeMessage: UIEventSource - locationControl: UIEventSource - } + private readonly state: ExtraLinkButtonState constructor( - state: { - featureSwitchWelcomeMessage: UIEventSource - locationControl: UIEventSource - layoutToUse: { id: string; title: Translation } - }, + state: ExtraLinkButtonState, config: ExtraLinkConfig ) { super() @@ -41,19 +40,18 @@ export default class ExtraLinkButton extends UIElement { const c = this._config const isIframe = window !== window.top - if (c.requirements?.has("iframe") && !isIframe) { return undefined } if (c.requirements?.has("no-iframe") && isIframe) { - return undefined + return undefined } let link: BaseUIElement - const theme = this.state.layoutToUse?.id ?? "" + const theme = this.state.layout?.id ?? "" const basepath = window.location.host - const href = this.state.locationControl.map((loc) => { + const href = this.state.mapProperties.location.map((loc) => { const subs = { ...loc, theme: theme, @@ -61,7 +59,7 @@ export default class ExtraLinkButton extends UIElement { language: Locale.language.data, } return Utils.SubstituteKeys(c.href, subs) - }) + }, [this.state.mapProperties.zoom]) let img: BaseUIElement = Svg.pop_out_ui() if (c.icon !== undefined) { @@ -71,7 +69,7 @@ export default class ExtraLinkButton extends UIElement { let text: Translation if (c.text === undefined) { text = Translations.t.general.screenToSmall.Subs({ - theme: this.state.layoutToUse.title, + theme: this.state.layout.title, }) } else { text = c.text.Clone() @@ -83,11 +81,11 @@ export default class ExtraLinkButton extends UIElement { }) if (c.requirements?.has("no-welcome-message")) { - link = new Toggle(undefined, link, this.state.featureSwitchWelcomeMessage) + link = new Toggle(undefined, link, this.state.featureSwitches.featureSwitchWelcomeMessage) } if (c.requirements?.has("welcome-message")) { - link = new Toggle(link, undefined, this.state.featureSwitchWelcomeMessage) + link = new Toggle(link, undefined, this.state.featureSwitches.featureSwitchWelcomeMessage) } return link diff --git a/UI/BigComponents/LeftControls.ts b/UI/BigComponents/LeftControls.ts deleted file mode 100644 index a0e71a5c4..000000000 --- a/UI/BigComponents/LeftControls.ts +++ /dev/null @@ -1,62 +0,0 @@ -import Combine from "../Base/Combine" -import Toggle from "../Input/Toggle" -import MapControlButton from "../MapControlButton" -import Svg from "../../Svg" -import AllDownloads from "./AllDownloads" -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import Lazy from "../Base/Lazy" -import { VariableUiElement } from "../Base/VariableUIElement" -import { DefaultGuiState } from "../DefaultGuiState" - -export default class LeftControls extends Combine { - constructor(state: FeaturePipelineState, guiState: DefaultGuiState) { - const currentViewFL = state.currentView?.layer - const currentViewAction = new Toggle( - new Lazy(() => { - const feature: Store = state.currentView.features.map((ffs) => ffs[0]) - const icon = new VariableUiElement( - feature.map((feature) => { - const defaultIcon = Svg.checkbox_empty_svg() - if (feature === undefined) { - return defaultIcon - } - const tags = { ...feature.properties, button: "yes" } - const elem = currentViewFL.layerDef.mapRendering[0]?.GetSimpleIcon( - new UIEventSource(tags) - ) - if (elem === undefined) { - return defaultIcon - } - return elem - }) - ).SetClass("inline-block w-full h-full") - - feature.map((feature) => { - if (feature === undefined) { - return undefined - } - const tagsSource = state.allElements.getEventSourceById(feature.properties.id) - return new FeatureInfoBox(tagsSource, currentViewFL.layerDef, state, { - hashToShow: "currentview", - isShown: guiState.currentViewControlIsOpened, - }) - }) - - return new MapControlButton(icon) - }).onClick(() => { - guiState.currentViewControlIsOpened.setData(true) - }), - - undefined, - new UIEventSource( - currentViewFL !== undefined && currentViewFL?.layerDef?.tagRenderings !== null - ) - ) - - new AllDownloads(guiState.downloadControlIsOpened, state) - - super([currentViewAction]) - - this.SetClass("flex flex-col") - } -} diff --git a/UI/BigComponents/ShareScreen.ts b/UI/BigComponents/ShareScreen.ts index a0f859205..3f9019c06 100644 --- a/UI/BigComponents/ShareScreen.ts +++ b/UI/BigComponents/ShareScreen.ts @@ -1,22 +1,19 @@ -import { VariableUiElement } from "../Base/VariableUIElement" -import { Translation } from "../i18n/Translation" +import {VariableUiElement} from "../Base/VariableUIElement" +import {Translation} from "../i18n/Translation" import Svg from "../../Svg" import Combine from "../Base/Combine" -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import { Utils } from "../../Utils" +import {Store, UIEventSource} from "../../Logic/UIEventSource" +import {Utils} from "../../Utils" import Translations from "../i18n/Translations" import BaseUIElement from "../BaseUIElement" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import Loc from "../../Models/Loc" -import FilteredLayer from "../../Models/FilteredLayer" -import { InputElement } from "../Input/InputElement" -import { CheckBox } from "../Input/Checkboxes" -import { SubtleButton } from "../Base/SubtleButton" +import {InputElement} from "../Input/InputElement" +import {CheckBox} from "../Input/Checkboxes" +import {SubtleButton} from "../Base/SubtleButton" import LZString from "lz-string" -import { SpecialVisualizationState } from "../SpecialVisualization" +import {SpecialVisualizationState} from "../SpecialVisualization" -export default class ShareScreen extends Combine { +class ShareScreen extends Combine{ constructor(state: SpecialVisualizationState) { const layout = state?.layout const tr = Translations.t.general.sharescreen @@ -63,7 +60,7 @@ export default class ShareScreen extends Combine { return "layer-" + flayer.layerDef.id + "=" + flayer.isDisplayed.data } - const currentLayer: Store<{ id: string; name: string } | undefined> = + const currentLayer: Store<{ id: string; name: string | Record } | undefined> = state.mapProperties.rasterLayer.map((l) => l?.properties) const currentBackground = new VariableUiElement( currentLayer.map((layer) => { @@ -93,13 +90,13 @@ export default class ShareScreen extends Combine { (includeLayerSelection) => { if (includeLayerSelection) { return Utils.NoNull( - state.layerState.filteredLayers.map(fLayerToParam) + Array.from( state.layerState.filteredLayers.values()).map(fLayerToParam) ).join("&") } else { return null } }, - state.filteredLayers.data.map((flayer) => flayer.isDisplayed) + Array.from(state.layerState.filteredLayers.values()).map((flayer) => flayer.isDisplayed) ) ) diff --git a/UI/BigComponents/ThemeIntroductionPanel.ts b/UI/BigComponents/ThemeIntroductionPanel.ts deleted file mode 100644 index 35caf06b4..000000000 --- a/UI/BigComponents/ThemeIntroductionPanel.ts +++ /dev/null @@ -1,88 +0,0 @@ -import Combine from "../Base/Combine" -import LanguagePicker from "../LanguagePicker" -import Translations from "../i18n/Translations" -import Toggle from "../Input/Toggle" -import { SubtleButton } from "../Base/SubtleButton" -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import { LoginToggle } from "../Popup/LoginButton" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import { OsmConnection } from "../../Logic/Osm/OsmConnection" -import LoggedInUserIndicator from "../LoggedInUserIndicator" -import { BBox } from "../../Logic/BBox" -import Loc from "../../Models/Loc" -import { DefaultGuiState } from "../DefaultGuiState" - -export default class ThemeIntroductionPanel extends Combine { - constructor( - isShown: UIEventSource, - currentTab: UIEventSource, - state: { - featureSwitchMoreQuests: UIEventSource - featureSwitchAddNew: UIEventSource - featureSwitchUserbadge: UIEventSource - layoutToUse: LayoutConfig - osmConnection: OsmConnection - currentBounds: Store - locationControl: UIEventSource - defaultGuiState: DefaultGuiState - }, - guistate?: { userInfoIsOpened: UIEventSource } - ) { - const t = Translations.t.general - const layout = state.layoutToUse - - const languagePicker = new LanguagePicker(layout.language, t.pickLanguage.Clone()) - - const toTheMap = new SubtleButton( - undefined, - t.openTheMap.Clone().SetClass("text-xl font-bold w-full text-center") - ) - .onClick(() => { - isShown.setData(false) - }) - .SetClass("only-on-mobile") - - const loggedInUserInfo = new LoggedInUserIndicator(state.osmConnection, { - firstLine: Translations.t.general.welcomeBack.Clone(), - }) - if (guistate?.userInfoIsOpened) { - loggedInUserInfo.onClick(() => { - guistate.userInfoIsOpened.setData(true) - }) - } - - const loginStatus = new Toggle( - new LoginToggle( - loggedInUserInfo, - new Combine([ - Translations.t.general.loginWithOpenStreetMap.SetClass("text-xl font-bold"), - Translations.t.general.loginOnlyNeededToEdit.Clone().SetClass("font-bold"), - ]).SetClass("flex flex-col"), - state - ), - undefined, - state.featureSwitchUserbadge - ) - - const hasPresets = layout.layers.some((l) => l.presets?.length > 0) - super([ - layout.description.Clone().SetClass("block mb-4"), - new Combine([ - t.welcomeExplanation.general, - hasPresets - ? Toggle.If(state.featureSwitchAddNew, () => t.welcomeExplanation.addNew) - : undefined, - ]).SetClass("flex flex-col mt-2"), - - toTheMap, - loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"), - layout.descriptionTail?.Clone().SetClass("block mt-4"), - - languagePicker?.SetClass("block mt-4 pb-8 border-b-2 border-dotted border-gray-400"), - - ...layout.CustomCodeSnippets(), - ]) - - this.SetClass("link-underline") - } -} diff --git a/UI/BigComponents/UserInformation.ts b/UI/BigComponents/UserInformation.ts deleted file mode 100644 index f5d9a9dc1..000000000 --- a/UI/BigComponents/UserInformation.ts +++ /dev/null @@ -1,68 +0,0 @@ -import Translations from "../i18n/Translations" -import { OsmConnection } from "../../Logic/Osm/OsmConnection" -import Combine from "../Base/Combine" -import { SubtleButton } from "../Base/SubtleButton" -import Svg from "../../Svg" -import { VariableUiElement } from "../Base/VariableUIElement" -import Img from "../Base/Img" -import { FixedUiElement } from "../Base/FixedUiElement" -import Link from "../Base/Link" -import { UIEventSource } from "../../Logic/UIEventSource" -import Loc from "../../Models/Loc" -import BaseUIElement from "../BaseUIElement" -import Showdown from "showdown" -import LanguagePicker from "../LanguagePicker" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import Constants from "../../Models/Constants" - -export class ImportViewerLinks extends VariableUiElement { - constructor(osmConnection: OsmConnection) { - super( - osmConnection.userDetails.map((ud) => { - if (ud.csCount < Constants.userJourney.importHelperUnlock) { - return undefined - } - return new Combine([ - new SubtleButton(undefined, Translations.t.importHelper.title, { - url: "import_helper.html", - }), - new SubtleButton(Svg.note_svg(), Translations.t.importInspector.title, { - url: "import_viewer.html", - }), - ]) - }) - ) - } -} - -class UserInformationMainPanel extends VariableUiElement { - private readonly settings: UIEventSource> - private readonly userInfoFocusedQuestion?: UIEventSource - - constructor( - osmConnection: OsmConnection, - locationControl: UIEventSource, - layout: LayoutConfig, - isOpened: UIEventSource, - userInfoFocusedQuestion?: UIEventSource - ) { - const settings = new UIEventSource>({}) - - super() - this.settings = settings - this.userInfoFocusedQuestion = userInfoFocusedQuestion - const self = this - userInfoFocusedQuestion.addCallbackD((_) => { - self.focusOnSelectedQuestion() - }) - } - - public focusOnSelectedQuestion() { - const focusedId = this.userInfoFocusedQuestion.data - console.log("Focusing on", focusedId, this.settings.data[focusedId]) - if (focusedId === undefined) { - return - } - this.settings.data[focusedId]?.ScrollIntoView() - } -} diff --git a/UI/DefaultGUI.ts b/UI/DefaultGUI.ts deleted file mode 100644 index 24d943680..000000000 --- a/UI/DefaultGUI.ts +++ /dev/null @@ -1,48 +0,0 @@ -import Toggle from "./Input/Toggle" -import LeftControls from "./BigComponents/LeftControls" -import CenterMessageBox from "./CenterMessageBox" -import { DefaultGuiState } from "./DefaultGuiState" -import Combine from "./Base/Combine" -import ExtraLinkButton from "./BigComponents/ExtraLinkButton" -import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" - -/** - * The default MapComplete GUI initializer - * - * Adds a welcome pane, control buttons, ... etc to index.html - */ -export default class DefaultGUI { - private readonly guiState: DefaultGuiState - private readonly geolocationHandler: GeoLocationHandler | undefined - - constructor(guiState: DefaultGuiState) { - this.guiState = guiState - } - - public setup() { - const extraLink = Toggle.If( - state.featureSwitchExtraLinkEnabled, - () => new ExtraLinkButton(state, state.layoutToUse.extraLink) - ) - - new Combine([extraLink]).SetClass("flex flex-col").AttachTo("top-left") - - new Combine([ - new ExtraLinkButton(state, { - ...state.layoutToUse.extraLink, - newTab: true, - requirements: new Set< - "iframe" | "no-iframe" | "welcome-message" | "no-welcome-message" - >(), - }), - ]) - .SetClass("flex items-center justify-center normal-background h-full") - .AttachTo("on-small-screen") - - const guiState = this.guiState - new LeftControls(state, guiState).AttachTo("bottom-left") - - new CenterMessageBox(state).AttachTo("centermessage") - document?.getElementById("centermessage")?.classList?.add("pointer-events-none") - } -} diff --git a/UI/DefaultGuiState.ts b/UI/DefaultGuiState.ts deleted file mode 100644 index c0f4094d0..000000000 --- a/UI/DefaultGuiState.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { UIEventSource } from "../Logic/UIEventSource" -import Hash from "../Logic/Web/Hash" - -export class DefaultGuiState { - public readonly welcomeMessageIsOpened: UIEventSource = new UIEventSource( - false - ) - - public readonly menuIsOpened: UIEventSource = new UIEventSource(false) - - public readonly downloadControlIsOpened: UIEventSource = new UIEventSource( - false - ) - public readonly filterViewIsOpened: UIEventSource = new UIEventSource(false) - public readonly copyrightViewIsOpened: UIEventSource = new UIEventSource( - false - ) - public readonly currentViewControlIsOpened: UIEventSource = new UIEventSource( - false - ) - public readonly userInfoIsOpened: UIEventSource = new UIEventSource(false) - public readonly userInfoFocusedQuestion: UIEventSource = new UIEventSource( - undefined - ) - - private readonly sources: Record> = { - welcome: this.welcomeMessageIsOpened, - download: this.downloadControlIsOpened, - filters: this.filterViewIsOpened, - copyright: this.copyrightViewIsOpened, - currentview: this.currentViewControlIsOpened, - userinfo: this.userInfoIsOpened, - } - - constructor() { - const self = this - this.userInfoIsOpened.addCallback((isOpen) => { - if (!isOpen) { - console.log("Resetting focused question") - self.userInfoFocusedQuestion.setData(undefined) - } - }) - - this.sources[Hash.hash.data?.toLowerCase()]?.setData(true) - - if (Hash.hash.data === "" || Hash.hash.data === undefined) { - this.welcomeMessageIsOpened.setData(true) - } - } - - public closeAll(except) { - for (const sourceKey in this.sources) { - this.sources[sourceKey].setData(false) - } - } -} diff --git a/UI/ImportFlow/AskMetadata.ts b/UI/ImportFlow/AskMetadata.ts deleted file mode 100644 index 0784ccb1b..000000000 --- a/UI/ImportFlow/AskMetadata.ts +++ /dev/null @@ -1,128 +0,0 @@ -import Combine from "../Base/Combine" -import { FlowStep } from "./FlowStep" -import { Store } from "../../Logic/UIEventSource" -import ValidatedTextField from "../Input/ValidatedTextField" -import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource" -import Title from "../Base/Title" -import { VariableUiElement } from "../Base/VariableUIElement" -import Translations from "../i18n/Translations" -import { SubtleButton } from "../Base/SubtleButton" -import Svg from "../../Svg" -import { Utils } from "../../Utils" - -export class AskMetadata - extends Combine - implements - FlowStep<{ - features: any[] - wikilink: string - intro: string - source: string - theme: string - }> -{ - public readonly Value: Store<{ - features: any[] - wikilink: string - intro: string - source: string - theme: string - }> - public readonly IsValid: Store - - constructor(params: { features: any[]; theme: string }) { - const t = Translations.t.importHelper.askMetadata - const introduction = ValidatedTextField.ForType("text").ConstructInputElement({ - value: LocalStorageSource.Get("import-helper-introduction-text"), - inputStyle: "width: 100%", - }) - - const wikilink = ValidatedTextField.ForType("url").ConstructInputElement({ - value: LocalStorageSource.Get("import-helper-wikilink-text"), - inputStyle: "width: 100%", - }) - - const source = ValidatedTextField.ForType("string").ConstructInputElement({ - value: LocalStorageSource.Get("import-helper-source-text"), - inputStyle: "width: 100%", - }) - - super([ - new Title(t.title), - t.intro.Subs({ count: params.features.length }), - t.giveDescription, - introduction.SetClass("w-full border border-black"), - t.giveSource, - source.SetClass("w-full border border-black"), - t.giveWikilink, - wikilink.SetClass("w-full border border-black"), - new VariableUiElement( - wikilink.GetValue().map((wikilink) => { - try { - const url = new URL(wikilink) - if (url.hostname.toLowerCase() !== "wiki.openstreetmap.org") { - return t.shouldBeOsmWikilink.SetClass("alert") - } - - if (url.pathname.toLowerCase() === "/wiki/main_page") { - return t.shouldNotBeHomepage.SetClass("alert") - } - } catch (e) { - return t.shouldBeUrl.SetClass("alert") - } - }) - ), - t.orDownload, - new SubtleButton(Svg.download_svg(), t.downloadGeojson).OnClickWithLoading( - "Preparing your download", - async () => { - const geojson = { - type: "FeatureCollection", - features: params.features, - } - Utils.offerContentsAsDownloadableFile( - JSON.stringify(geojson), - "prepared_import_" + params.theme + ".geojson", - { - mimetype: "application/vnd.geo+json", - } - ) - } - ), - ]) - this.SetClass("flex flex-col") - - this.Value = introduction.GetValue().map( - (intro) => { - return { - features: params.features, - wikilink: wikilink.GetValue().data, - intro, - source: source.GetValue().data, - theme: params.theme, - } - }, - [wikilink.GetValue(), source.GetValue()] - ) - - this.IsValid = this.Value.map((obj) => { - if (obj === undefined) { - return false - } - if ([obj.features, obj.intro, obj.wikilink, obj.source].some((v) => v === undefined)) { - return false - } - - try { - const url = new URL(obj.wikilink) - if (url.hostname.toLowerCase() !== "wiki.openstreetmap.org") { - return false - } - } catch (e) { - return false - } - - return true - }) - } -} diff --git a/UI/ImportFlow/CompareToAlreadyExistingNotes.ts b/UI/ImportFlow/CompareToAlreadyExistingNotes.ts deleted file mode 100644 index fc69ef53d..000000000 --- a/UI/ImportFlow/CompareToAlreadyExistingNotes.ts +++ /dev/null @@ -1,196 +0,0 @@ -import Combine from "../Base/Combine" -import { FlowStep } from "./FlowStep" -import { BBox } from "../../Logic/BBox" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import CreateNoteImportLayer from "../../Models/ThemeConfig/Conversion/CreateNoteImportLayer" -import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" -import GeoJsonSource from "../../Logic/FeatureSource/Sources/GeoJsonSource" -import MetaTagging from "../../Logic/MetaTagging" -import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource" -import Minimap from "../Base/Minimap" -import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" -import FeatureInfoBox from "../Popup/FeatureInfoBox" -import { ImportUtils } from "./ImportUtils" -import import_candidate from "../../assets/layers/import_candidate/import_candidate.json" -import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" -import Title from "../Base/Title" -import Loading from "../Base/Loading" -import { VariableUiElement } from "../Base/VariableUIElement" -import known_layers from "../../assets/generated/known_layers.json" -import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" -import Translations from "../i18n/Translations" -import { Feature } from "geojson" -import DivContainer from "../Base/DivContainer" - -/** - * Filters out points for which the import-note already exists, to prevent duplicates - */ -export class CompareToAlreadyExistingNotes - extends Combine - implements FlowStep<{ bbox: BBox; layer: LayerConfig; features: any[]; theme: string }> -{ - public IsValid: Store - public Value: Store<{ bbox: BBox; layer: LayerConfig; features: any[]; theme: string }> - - constructor(state, params: { bbox: BBox; layer: LayerConfig; features: any[]; theme: string }) { - const t = Translations.t.importHelper.compareToAlreadyExistingNotes - const layerConfig = known_layers.layers.filter((l) => l.id === params.layer.id)[0] - if (layerConfig === undefined) { - console.error("WEIRD: layer not found in the builtin layer overview") - } - const importLayerJson = new CreateNoteImportLayer(150).convertStrict( - layerConfig, - "CompareToAlreadyExistingNotes" - ) - const importLayer = new LayerConfig(importLayerJson, "import-layer-dynamic") - const flayer: FilteredLayer = { - appliedFilters: new UIEventSource>( - new Map() - ), - isDisplayed: new UIEventSource(true), - layerDef: importLayer, - } - const allNotesWithinBbox = new GeoJsonSource(flayer, params.bbox.padAbsolute(0.0001)) - - allNotesWithinBbox.features.map((f) => - MetaTagging.addMetatags( - f, - { - getFeaturesWithin: () => [], - getFeatureById: () => undefined, - }, - importLayer, - state, - { - includeDates: true, - // We assume that the non-dated metatags are already set by the cache generator - includeNonDates: true, - } - ) - ) - const alreadyOpenImportNotes = new FilteringFeatureSource( - state, - undefined, - allNotesWithinBbox - ) - const map = Minimap.createMiniMap() - map.SetClass("w-full").SetStyle("height: 500px") - - const comparisonMap = Minimap.createMiniMap({ - location: map.location, - }) - comparisonMap.SetClass("w-full").SetStyle("height: 500px") - - new ShowDataLayer({ - layerToShow: importLayer, - state, - zoomToFeatures: true, - leafletMap: map.leafletMap, - features: alreadyOpenImportNotes, - popup: (tags, layer) => new FeatureInfoBox(tags, layer, state), - }) - - const maxDistance = new UIEventSource(10) - - const partitionedImportPoints = ImportUtils.partitionFeaturesIfNearby( - params, - alreadyOpenImportNotes.features.map((ff) => ({ features: ff.map((ff) => ff.feature) })), - maxDistance - ) - - new ShowDataLayer({ - layerToShow: new LayerConfig(import_candidate), - state, - zoomToFeatures: true, - leafletMap: comparisonMap.leafletMap, - features: new StaticFeatureSource( - partitionedImportPoints.map((p) => p.hasNearby) - ), - popup: (tags, layer) => new FeatureInfoBox(tags, layer, state), - }) - - super([ - new Title(t.titleLong), - new VariableUiElement( - alreadyOpenImportNotes.features.map( - (notesWithImport) => { - if ( - allNotesWithinBbox.state.data !== undefined && - allNotesWithinBbox.state.data["error"] !== undefined - ) { - const error = allNotesWithinBbox.state.data["error"] - t.loadingFailed.Subs({ error }) - } - if ( - allNotesWithinBbox.features.data === undefined || - allNotesWithinBbox.features.data.length === 0 - ) { - return new Loading(t.loading) - } - if (notesWithImport.length === 0) { - return t.noPreviousNotesFound.SetClass("thanks") - } - return new Combine([ - t.mapExplanation.Subs(params.features), - map, - new DivContainer("fullscreen"), - new VariableUiElement( - partitionedImportPoints.map(({ noNearby, hasNearby }) => { - if (noNearby.length === 0) { - // Nothing can be imported - return t.completelyImported - .SetClass("alert w-full block") - .SetStyle("padding: 0.5rem") - } - - if (hasNearby.length === 0) { - // All points can be imported - return t.nothingNearby - .SetClass("thanks w-full block") - .SetStyle("padding: 0.5rem") - } - - return new Combine([ - t.someNearby - .Subs({ - hasNearby: hasNearby.length, - distance: maxDistance.data, - }) - .SetClass("alert"), - t.wontBeImported, - comparisonMap.SetClass("w-full"), - ]).SetClass("w-full") - }) - ), - ]).SetClass("flex flex-col") - }, - [allNotesWithinBbox.features, allNotesWithinBbox.state] - ) - ), - ]) - this.SetClass("flex flex-col") - this.Value = partitionedImportPoints.map(({ noNearby }) => ({ - features: noNearby, - bbox: params.bbox, - layer: params.layer, - theme: params.theme, - })) - - this.IsValid = alreadyOpenImportNotes.features.map( - (ff) => { - if (allNotesWithinBbox.features.data.length === 0) { - // Not yet loaded - return false - } - if (ff.length == 0) { - // No import notes at all - return true - } - - return partitionedImportPoints.data.noNearby.length > 0 // at least _something_ can be imported - }, - [partitionedImportPoints, allNotesWithinBbox.features] - ) - } -} diff --git a/UI/ImportFlow/ConfirmProcess.ts b/UI/ImportFlow/ConfirmProcess.ts deleted file mode 100644 index b91ab5368..000000000 --- a/UI/ImportFlow/ConfirmProcess.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Combine from "../Base/Combine" -import { FlowStep } from "./FlowStep" -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import Link from "../Base/Link" -import CheckBoxes from "../Input/Checkboxes" -import Title from "../Base/Title" -import Translations from "../i18n/Translations" - -export class ConfirmProcess - extends Combine - implements FlowStep<{ features: any[]; theme: string }> -{ - public IsValid: Store - public Value: Store<{ features: any[]; theme: string }> - - constructor(v: { features: any[]; theme: string }) { - const t = Translations.t.importHelper.confirmProcess - const elements = [ - new Link( - t.readImportGuidelines, - "https://wiki.openstreetmap.org/wiki/Import_guidelines", - true - ), - t.contactedCommunity, - t.licenseIsCompatible, - t.wikipageIsMade, - ] - const toConfirm = new CheckBoxes(elements) - - super([new Title(t.titleLong), toConfirm]) - this.SetClass("link-underline") - this.IsValid = toConfirm.GetValue().map((selected) => elements.length == selected.length) - this.Value = new UIEventSource<{ features: any[]; theme: string }>(v) - } -} diff --git a/UI/ImportFlow/ConflationChecker.ts b/UI/ImportFlow/ConflationChecker.ts deleted file mode 100644 index e8af9e9ab..000000000 --- a/UI/ImportFlow/ConflationChecker.ts +++ /dev/null @@ -1,359 +0,0 @@ -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 { Store, UIEventSource } from "../../Logic/UIEventSource"; -import Constants from "../../Models/Constants"; -import RelationsTracker from "../../Logic/Osm/RelationsTracker"; -import { VariableUiElement } from "../Base/VariableUIElement"; -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 ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; -import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; -import ValidatedTextField from "../Input/ValidatedTextField"; -import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"; -import import_candidate from "../../assets/layers/import_candidate/import_candidate.json"; -import { GeoOperations } from "../../Logic/GeoOperations"; -import FeatureInfoBox from "../Popup/FeatureInfoBox"; -import { ImportUtils } from "./ImportUtils"; -import Translations from "../i18n/Translations"; -import currentview from "../../assets/layers/current_view/current_view.json"; -import { CheckBox } from "../Input/Checkboxes"; -import { Feature, FeatureCollection, Point } from "geojson"; -import DivContainer from "../Base/DivContainer"; - -/** - * Given the data to import, the bbox and the layer, will query overpass for similar items - */ -export default class ConflationChecker - extends Combine - implements FlowStep<{ features: Feature[]; theme: string }> -{ - public readonly IsValid - public readonly Value: Store<{ features: Feature[]; theme: string }> - - constructor( - state, - params: { bbox: BBox; layer: LayerConfig; theme: string; features: Feature[] } - ) { - const t = Translations.t.importHelper.conflationChecker - - const bbox = params.bbox.padAbsolute(0.0001) - const layer = params.layer - - const toImport: { features: any[] } = params - let overpassStatus = new UIEventSource< - { error: string } | "running" | "success" | "idle" | "cached" - >("idle") - - function loadDataFromOverpass() { - // 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 fromLocalStorage = IdbLocalStorage.Get<[any, Date]>( - "importer-overpass-cache-" + layer.id, - { - whenLoaded: (v) => { - if (v !== undefined && v !== null) { - console.log("Loaded from local storage:", v) - overpassStatus.setData("cached") - } else { - loadDataFromOverpass() - } - }, - } - ) - - const cacheAge = fromLocalStorage.map((d) => { - if (d === undefined || d[1] === undefined) { - return undefined - } - const [_, loadedDate] = d - return (new Date().getTime() - loadedDate.getTime()) / 1000 - }) - cacheAge.addCallbackD((timeDiff) => { - if (timeDiff < 24 * 60 * 60) { - // Recently cached! - overpassStatus.setData("cached") - return - } else { - loadDataFromOverpass() - } - }) - - const geojson: Store = 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.ForType("pnat").ConstructInputElement({ - value: LocalStorageSource.GetParsed("importer-zoom-level", "0"), - }) - zoomLevel.SetClass("ml-1 border border-black") - const osmLiveData = Minimap.createMiniMap({ - allowMoving: true, - location, - background, - bounds: currentBounds, - }) - osmLiveData.SetClass("w-full").SetStyle("height: 500px") - - const geojsonFeatures: Store = geojson.map( - (geojson) => { - if (geojson?.features === undefined) { - return [] - } - const currentZoom = zoomLevel.GetValue().data - const zoomedEnough: boolean = osmLiveData.location.data.zoom >= Number(currentZoom) - if (currentZoom !== undefined && !zoomedEnough) { - return [] - } - const bounds = osmLiveData.bounds.data - if (bounds === undefined) { - return geojson.features - } - return geojson.features.filter((f) => BBox.get(f).overlapsWith(bounds)) - }, - [osmLiveData.bounds, zoomLevel.GetValue()] - ) - - const preview = new StaticFeatureSource(geojsonFeatures) - - new ShowDataLayer({ - layerToShow: new LayerConfig(currentview), - state, - leafletMap: osmLiveData.leafletMap, - popup: undefined, - zoomToFeatures: true, - features: StaticFeatureSource.fromGeojson([bbox.asGeoJson({})]), - }) - - new ShowDataLayer({ - layerToShow: layer, - state, - leafletMap: osmLiveData.leafletMap, - popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }), - zoomToFeatures: false, - features: preview, - }) - - new ShowDataLayer({ - layerToShow: new LayerConfig(import_candidate), - state, - leafletMap: osmLiveData.leafletMap, - popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }), - zoomToFeatures: false, - features: StaticFeatureSource.fromGeojson(toImport.features), - }) - - const nearbyCutoff = ValidatedTextField.ForType("pnat").ConstructInputElement() - 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 geojsonMapped: Store = 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().stabilized(500)] - ) - const nearbyFeatures = new StaticFeatureSource(geojsonMapped) - const paritionedImport = ImportUtils.partitionFeaturesIfNearby( - toImport, - geojson, - nearbyCutoff.GetValue().map(Number) - ) - - // Featuresource showing OSM-features which are nearby a toImport-feature - const toImportWithNearby = new StaticFeatureSource( - paritionedImport.map((els) => els?.hasNearby ?? []) - ) - toImportWithNearby.features.addCallback((nearby) => - console.log("The following features are near an already existing object:", nearby) - ) - - new ShowDataLayer({ - layerToShow: new LayerConfig(import_candidate), - state, - leafletMap: matchedFeaturesMap.leafletMap, - popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }), - zoomToFeatures: false, - features: toImportWithNearby, - }) - const showOsmLayer = new CheckBox(t.showOsmLayerInConflationMap, true) - new ShowDataLayer({ - layerToShow: layer, - state, - leafletMap: matchedFeaturesMap.leafletMap, - popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, { setHash: false }), - zoomToFeatures: true, - features: nearbyFeatures, - doShowLayer: showOsmLayer.GetValue(), - }) - - const conflationMaps = new Combine([ - new VariableUiElement( - geojson.map((geojson) => { - if (geojson === undefined) { - return undefined - } - return new SubtleButton(Svg.download_svg(), t.downloadOverpassData).onClick( - () => { - Utils.offerContentsAsDownloadableFile( - JSON.stringify(geojson, null, " "), - "mapcomplete-" + layer.id + ".geojson", - { - mimetype: "application/json+geo", - } - ) - } - ) - }) - ), - new VariableUiElement( - cacheAge.map((age) => { - if (age === undefined) { - return undefined - } - if (age < 0) { - return t.cacheExpired - } - return new Combine([ - t.loadedDataAge.Subs({ age: Utils.toHumanTime(age) }), - new SubtleButton(Svg.reload_svg().SetClass("h-8"), t.reloadTheCache) - .onClick(loadDataFromOverpass) - .SetClass("h-12"), - ]) - }) - ), - - new Title(t.titleLive), - t.importCandidatesCount.Subs({ count: toImport.features.length }), - new VariableUiElement( - geojson.map((geojson) => { - if ( - geojson?.features?.length === undefined || - geojson?.features?.length === 0 - ) { - return t.nothingLoaded.Subs(layer).SetClass("alert") - } - return new Combine([ - t.osmLoaded.Subs({ count: geojson.features.length, name: layer.name }), - ]) - }) - ), - osmLiveData, - new Combine([ - t.zoomLevelSelection, - zoomLevel, - new VariableUiElement( - osmLiveData.location.map((location) => { - return t.zoomIn.Subs({ current: location.zoom }) - }) - ), - ]).SetClass("flex"), - new DivContainer("fullscreen"), - new Title(t.titleNearby), - new Combine([t.mapShowingNearbyIntro, nearbyCutoff]).SetClass("flex"), - new VariableUiElement( - toImportWithNearby.features.map((feats) => - t.nearbyWarn.Subs({ count: feats.length }).SetClass("alert") - ) - ), - t.setRangeToZero, - matchedFeaturesMap, - showOsmLayer, - ]).SetClass("flex flex-col") - super([ - new Title(t.title), - new VariableUiElement( - overpassStatus.map((d) => { - if (d === "idle") { - return new Loading(t.states.idle) - } - if (d === "running") { - return new Loading(t.states.running) - } - if (d["error"] !== undefined) { - return t.states.error.Subs({ error: d["error"] }).SetClass("alert") - } - - if (d === "cached") { - return conflationMaps - } - if (d === "success") { - return conflationMaps - } - return t.states.unexpected.Subs({ state: d }).SetClass("alert") - }) - ), - ]) - - this.Value = paritionedImport.map((feats) => ({ - theme: params.theme, - features: feats?.noNearby, - layer: params.layer, - })) - this.IsValid = this.Value.map((v) => v?.features !== undefined && v.features.length > 0) - } -} diff --git a/UI/ImportFlow/CreateNotes.ts b/UI/ImportFlow/CreateNotes.ts deleted file mode 100644 index 81713c713..000000000 --- a/UI/ImportFlow/CreateNotes.ts +++ /dev/null @@ -1,136 +0,0 @@ -import Combine from "../Base/Combine" -import { OsmConnection } from "../../Logic/Osm/OsmConnection" -import { UIEventSource } from "../../Logic/UIEventSource" -import Title from "../Base/Title" -import Toggle from "../Input/Toggle" -import Loading from "../Base/Loading" -import { VariableUiElement } from "../Base/VariableUIElement" -import { FixedUiElement } from "../Base/FixedUiElement" -import { SubtleButton } from "../Base/SubtleButton" -import Svg from "../../Svg" -import Translations from "../i18n/Translations" -import { Translation } from "../i18n/Translation" - -export class CreateNotes extends Combine { - public static createNoteContentsUi( - feature: { properties: any; geometry: { coordinates: [number, number] } }, - options: { wikilink: string; intro: string; source: string; theme: string } - ): (Translation | string)[] { - const src = feature.properties["source"] ?? feature.properties["src"] ?? options.source - delete feature.properties["source"] - delete feature.properties["src"] - let extraNote = "" - if (feature.properties["note"]) { - extraNote = feature.properties["note"] + "\n" - delete feature.properties["note"] - } - - const tags: string[] = [] - for (const key in feature.properties) { - if (feature.properties[key] === null || feature.properties[key] === undefined) { - console.warn("Null or undefined key for ", feature.properties) - continue - } - if (feature.properties[key] === "") { - continue - } - tags.push( - key + - "=" + - (feature.properties[key] + "") - .replace(/=/, "\\=") - .replace(/;/g, "\\;") - .replace(/\n/g, "\\n") - ) - } - const lat = feature.geometry.coordinates[1] - const lon = feature.geometry.coordinates[0] - const note = Translations.t.importHelper.noteParts - return [ - options.intro, - extraNote, - note.datasource.Subs({ source: src }), - note.wikilink.Subs(options), - "", - note.importEasily, - `https://mapcomplete.osm.be/${options.theme}.html?z=18&lat=${lat}&lon=${lon}#import`, - ...tags, - ] - } - - public static createNoteContents( - feature: { properties: any; geometry: { coordinates: [number, number] } }, - options: { wikilink: string; intro: string; source: string; theme: string } - ): string[] { - return CreateNotes.createNoteContentsUi(feature, options).map((trOrStr) => { - if (typeof trOrStr === "string") { - return trOrStr - } - return trOrStr.txt - }) - } - - constructor( - state: { osmConnection: OsmConnection }, - v: { features: any[]; wikilink: string; intro: string; source: string; theme: string } - ) { - const t = Translations.t.importHelper.createNotes - const createdNotes: UIEventSource = new UIEventSource([]) - const failed = new UIEventSource([]) - const currentNote = createdNotes.map((n) => n.length) - - for (const f of v.features) { - const lat = f.geometry.coordinates[1] - const lon = f.geometry.coordinates[0] - const text = CreateNotes.createNoteContents(f, v).join("\n") - - state.osmConnection.openNote(lat, lon, text).then( - ({ id }) => { - createdNotes.data.push(id) - createdNotes.ping() - }, - (err) => { - failed.data.push(err) - failed.ping() - } - ) - } - - super([ - new Title(t.title), - t.loading, - new Toggle( - new Loading( - new VariableUiElement( - currentNote.map((count) => - t.creating.Subs({ - count, - total: v.features.length, - }) - ) - ) - ), - new Combine([ - Svg.party_svg().SetClass("w-24"), - t.done.Subs({ count: v.features.length }).SetClass("thanks"), - new SubtleButton(Svg.note_svg(), t.openImportViewer, { - url: "import_viewer.html", - }), - ]), - currentNote.map((count) => count < v.features.length) - ), - new VariableUiElement( - failed.map((failed) => { - if (failed.length === 0) { - return undefined - } - return new Combine([ - new FixedUiElement("Some entries failed").SetClass("alert"), - ...failed, - ]).SetClass("flex flex-col") - }) - ), - ]) - this.SetClass("flex flex-col") - } -} diff --git a/UI/ImportFlow/FlowStep.ts b/UI/ImportFlow/FlowStep.ts deleted file mode 100644 index 1da6fdce9..000000000 --- a/UI/ImportFlow/FlowStep.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Store, 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: Store - readonly Value: Store -} - -export class FlowPanelFactory { - private _initial: FlowStep - private _steps: ((x: any) => FlowStep)[] - private _stepNames: (string | BaseUIElement)[] - - private constructor( - initial: FlowStep, - steps: ((x: any) => FlowStep)[], - stepNames: (string | BaseUIElement)[] - ) { - this._initial = initial - this._steps = steps - this._stepNames = stepNames - } - - public static start( - name: { title: BaseUIElement }, - step: FlowStep - ): FlowPanelFactory { - return new FlowPanelFactory(step, [], [name.title]) - } - - public then( - name: string | { title: BaseUIElement }, - construct: (t: T) => FlowStep - ): FlowPanelFactory { - return new FlowPanelFactory( - this._initial, - this._steps.concat([construct]), - this._stepNames.concat([name["title"] ?? name]) - ) - } - - public finish( - name: string | BaseUIElement, - construct: (t: T, backButton?: BaseUIElement) => BaseUIElement - ): { - flow: BaseUIElement - furthestStep: UIEventSource - titles: (string | BaseUIElement)[] - } { - const furthestStep = new UIEventSource(0) - // 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] - const isConfirm = i == this._steps.length - 1 - nextConstr[i] = (value, backButton) => { - const flowStep = createFlowStep(value) - furthestStep.setData(i + 1) - const panel = new FlowPanel(flowStep, nextConstr[i + 1], backButton, isConfirm) - panel.isActive.addCallbackAndRun((active) => { - if (active) { - furthestStep.setData(i + 1) - } - }) - return panel - } - } - - const flow = new FlowPanel(this._initial, nextConstr[0]) - flow.isActive.addCallbackAndRun((active) => { - if (active) { - furthestStep.setData(0) - } - }) - return { - flow, - furthestStep, - titles: this._stepNames, - } - } -} - -export class FlowPanel extends Toggle { - public isActive: UIEventSource - - constructor( - initial: FlowStep, - constructNextstep: (input: T, backButton: BaseUIElement) => BaseUIElement, - backbutton?: BaseUIElement, - isConfirm = false - ) { - 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)[] = [] - const isError = new UIEventSource(false) - if (initial !== undefined) { - // Startup the flow - elements = [ - initial, - - new Combine([ - backbutton, - new Toggle( - new SubtleButton( - isConfirm - ? Svg.checkmark_svg() - : Svg.back_svg().SetStyle("transform: rotate(180deg);"), - isConfirm ? t.confirm : t.next - ).onClick(() => { - try { - const v = initial.Value.data - nextStep.setData(constructNextstep(v, backButtonForNextStep)) - currentStepActive.setData(false) - } catch (e) { - console.error(e) - isError.setData(true) - } - }), - new SubtleButton(Svg.invalid_svg(), t.notValid), - initial.IsValid - ), - new Toggle(t.error.SetClass("alert"), undefined, isError), - ]).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 - ) - this.isActive = currentStepActive - } -} diff --git a/UI/ImportFlow/ImportHelperGui.ts b/UI/ImportFlow/ImportHelperGui.ts deleted file mode 100644 index 2ae3503db..000000000 --- a/UI/ImportFlow/ImportHelperGui.ts +++ /dev/null @@ -1,83 +0,0 @@ -import Combine from "../Base/Combine" -import Toggle from "../Input/Toggle" -import LanguagePicker from "../LanguagePicker" -import UserRelatedState from "../../Logic/State/UserRelatedState" -import BaseUIElement from "../BaseUIElement" -import Translations from "../i18n/Translations" -import { FlowPanelFactory } from "./FlowStep" -import { RequestFile } from "./RequestFile" -import { PreviewAttributesPanel } from "./PreviewPanel" -import ConflationChecker from "./ConflationChecker" -import { AskMetadata } from "./AskMetadata" -import { ConfirmProcess } from "./ConfirmProcess" -import { CreateNotes } from "./CreateNotes" -import { VariableUiElement } from "../Base/VariableUIElement" -import List from "../Base/List" -import { CompareToAlreadyExistingNotes } from "./CompareToAlreadyExistingNotes" -import Introdution from "./Introdution" -import LoginToImport from "./LoginToImport" -import { MapPreview } from "./MapPreview" -import LeftIndex from "../Base/LeftIndex" -import { SubtleButton } from "../Base/SubtleButton" -import SelectTheme from "./SelectTheme" - -export default class ImportHelperGui extends LeftIndex { - constructor() { - const state = new UserRelatedState(undefined) - const t = Translations.t.importHelper - const { flow, furthestStep, titles } = FlowPanelFactory.start( - t.introduction, - new Introdution() - ) - .then(t.login, (_) => new LoginToImport(state)) - .then(t.selectFile, (_) => new RequestFile()) - .then(t.previewAttributes, (geojson) => new PreviewAttributesPanel(state, geojson)) - .then(t.mapPreview, (geojson) => new MapPreview(state, geojson)) - .then(t.selectTheme, (v) => new SelectTheme(v)) - .then( - t.compareToAlreadyExistingNotes, - (v) => new CompareToAlreadyExistingNotes(state, v) - ) - .then(t.conflationChecker, (v) => new ConflationChecker(state, v)) - .then(t.confirmProcess, (v) => new ConfirmProcess(v)) - .then(t.askMetadata, (v) => new AskMetadata(v)) - .finish(t.createNotes.title, (v) => new CreateNotes(state, v)) - - const toc = new List( - titles.map( - (title, i) => - new VariableUiElement( - furthestStep.map((currentStep) => { - if (i > currentStep) { - return new Combine([title]).SetClass("subtle") - } - if (i == currentStep) { - return new Combine([title]).SetClass("font-bold") - } - if (i < currentStep) { - return title - } - }) - ) - ), - true - ) - - const leftContents: BaseUIElement[] = [ - new SubtleButton(undefined, t.gotoImportViewer, { - url: "import_viewer.html", - }), - toc, - new Toggle(t.testMode.SetClass("block alert"), undefined, state.featureSwitchIsTesting), - new LanguagePicker( - Translations.t.importHelper.title.SupportedLanguages(), - "" - )?.SetClass("mt-4 self-end flex-col"), - ].map((el) => el?.SetClass("pl-4")) - - super(leftContents, flow) - } -} - -MinimapImplementation.initialize() -new ImportHelperGui().AttachTo("main") diff --git a/UI/ImportFlow/ImportUtils.ts b/UI/ImportFlow/ImportUtils.ts deleted file mode 100644 index 204a0b47d..000000000 --- a/UI/ImportFlow/ImportUtils.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Store } from "../../Logic/UIEventSource" -import { GeoOperations } from "../../Logic/GeoOperations" -import { Feature, Point } from "geojson" - -export class ImportUtils { - public static partitionFeaturesIfNearby( - toPartitionFeatureCollection: { features: Feature[] }, - compareWith: Store<{ features: Feature[] }>, - cutoffDistanceInMeters: Store - ): Store<{ hasNearby: Feature[]; noNearby: Feature[] }> { - return compareWith.map( - (osmData) => { - if (osmData?.features === undefined) { - return undefined - } - if (osmData.features.length === 0) { - return { noNearby: toPartitionFeatureCollection.features, hasNearby: [] } - } - const maxDist = cutoffDistanceInMeters.data - - const hasNearby = [] - const noNearby = [] - for (const toImportElement of toPartitionFeatureCollection.features) { - const hasNearbyFeature = osmData.features.some( - (f) => - maxDist >= - GeoOperations.distanceBetween( - (toImportElement.geometry).coordinates, - GeoOperations.centerpointCoordinates(f) - ) - ) - if (hasNearbyFeature) { - hasNearby.push(toImportElement) - } else { - noNearby.push(toImportElement) - } - } - - return { hasNearby, noNearby } - }, - [cutoffDistanceInMeters] - ) - } -} diff --git a/UI/ImportFlow/ImportViewerGui.ts b/UI/ImportFlow/ImportViewerGui.ts deleted file mode 100644 index 8ac9b7da5..000000000 --- a/UI/ImportFlow/ImportViewerGui.ts +++ /dev/null @@ -1,800 +0,0 @@ -import Combine from "../Base/Combine" -import UserRelatedState from "../../Logic/State/UserRelatedState" -import { VariableUiElement } from "../Base/VariableUIElement" -import { Utils } from "../../Utils" -import { UIEventSource } from "../../Logic/UIEventSource" -import Title from "../Base/Title" -import Translations from "../i18n/Translations" -import Loading from "../Base/Loading" -import { FixedUiElement } from "../Base/FixedUiElement" -import Link from "../Base/Link" -import { DropDown } from "../Input/DropDown" -import BaseUIElement from "../BaseUIElement" -import { SubtleButton } from "../Base/SubtleButton" -import Svg from "../../Svg" -import Toggle, { ClickableToggle } from "../Input/Toggle" -import Table from "../Base/Table" -import LeftIndex from "../Base/LeftIndex" -import Toggleable, { Accordeon } from "../Base/Toggleable" -import TableOfContents from "../Base/TableOfContents" -import { LoginToggle } from "../Popup/LoginButton" -import { QueryParameters } from "../../Logic/Web/QueryParameters" -import Lazy from "../Base/Lazy" -import { Button } from "../Base/Button" -import ChartJs from "../Base/ChartJs" - -interface NoteProperties { - id: number - url: string - date_created: string - closed_at?: string - status: "open" | "closed" - comments: { - date: string - uid: number - user: string - text: string - html: string - }[] -} - -interface NoteState { - props: NoteProperties - theme: string - intro: string - dateStr: string - status: - | "imported" - | "already_mapped" - | "invalid" - | "closed" - | "not_found" - | "open" - | "has_comments" -} - -class DownloadStatisticsButton extends SubtleButton { - constructor(states: NoteState[][]) { - super(Svg.statistics_svg(), "Download statistics") - this.onClick(() => { - const st: NoteState[] = [].concat(...states) - - const fields = [ - "id", - "status", - "theme", - "date_created", - "date_closed", - "days_open", - "intro", - "...comments", - ] - const values: string[][] = st.map((note) => { - return [ - note.props.id + "", - note.status, - note.theme, - note.props.date_created?.substr(0, note.props.date_created.length - 3), - note.props.closed_at?.substr(0, note.props.closed_at.length - 3) ?? "", - JSON.stringify(note.intro), - ...note.props.comments.map( - (c) => JSON.stringify(c.user) + ": " + JSON.stringify(c.text) - ), - ] - }) - - Utils.offerContentsAsDownloadableFile( - [fields, ...values].map((c) => c.join(", ")).join("\n"), - "mapcomplete_import_notes_overview.csv", - { - mimetype: "text/csv", - } - ) - }) - } -} - -class MassAction extends Combine { - constructor(state: UserRelatedState, props: NoteProperties[]) { - const textField = ValidatedTextField.ForType("text").ConstructInputElement() - - const actions = new DropDown<{ - predicate: (p: NoteProperties) => boolean - action: (p: NoteProperties) => Promise - }>("On which notes should an action be performed?", [ - { - value: undefined, - shown: "Pick an option...", - }, - { - value: { - predicate: (p) => p.status === "open", - action: async (p) => { - const txt = textField.GetValue().data - state.osmConnection.closeNote(p.id, txt) - }, - }, - shown: "Add comment to every open note and close all notes", - }, - { - value: { - predicate: (p) => p.status === "open", - action: async (p) => { - const txt = textField.GetValue().data - state.osmConnection.addCommentToNote(p.id, txt) - }, - }, - shown: "Add comment to every open note", - }, - /* - { - // This was a one-off for one of the first imports - value:{ - predicate: p => p.status === "open" && p.comments[0].text.split("\n").find(l => l.startsWith("note=")) !== undefined, - action: async p => { - const note = p.comments[0].text.split("\n").find(l => l.startsWith("note=")).substr("note=".length) - state.osmConnection.addCommentToNode(p.id, note) - } - }, - shown:"On every open note, read the 'note='-tag and and this note as comment. (This action ignores the textfield)" - },//*/ - ]) - - const handledNotesCounter = new UIEventSource(undefined) - const apply = new SubtleButton(Svg.checkmark_svg(), "Apply action").onClick(async () => { - const { predicate, action } = actions.GetValue().data - for (let i = 0; i < props.length; i++) { - handledNotesCounter.setData(i) - const prop = props[i] - if (!predicate(prop)) { - continue - } - await action(prop) - } - handledNotesCounter.setData(props.length) - }) - super([ - actions, - textField.SetClass("w-full border border-black"), - new Toggle( - new Toggle( - apply, - - new Toggle( - new Loading( - new VariableUiElement( - handledNotesCounter.map((state) => { - if (state === props.length) { - return "All done!" - } - return ( - "Handling note " + (state + 1) + " out of " + props.length - ) - }) - ) - ), - new Combine([Svg.checkmark_svg().SetClass("h-8"), "All done!"]).SetClass( - "thanks flex p-4" - ), - handledNotesCounter.map((s) => s < props.length) - ), - handledNotesCounter.map((s) => s === undefined) - ), - - new VariableUiElement( - textField - .GetValue() - .map( - (txt) => - "Type a text of at least 15 characters to apply the action. Currently, there are " + - (txt?.length ?? 0) + - " characters" - ) - ).SetClass("alert"), - actions - .GetValue() - .map( - (v) => v !== undefined && textField.GetValue()?.data?.length > 15, - [textField.GetValue()] - ) - ), - new Toggle( - new FixedUiElement("Testmode enable").SetClass("alert"), - undefined, - state.featureSwitchIsTesting - ), - ]) - } -} - -class Statistics extends Combine { - private static r() { - return Math.floor(Math.random() * 256) - } - - private static randomColour(): string { - return "rgba(" + Statistics.r() + "," + Statistics.r() + "," + Statistics.r() + ")" - } - private static CreatePieByAuthor(closed_by: Record): ChartJs { - const importers = Object.keys(closed_by) - importers.sort((a, b) => closed_by[b].at(-1) - closed_by[a].at(-1)) - return new ChartJs({ - type: "doughnut", - data: { - labels: importers, - datasets: [ - { - label: "Closed by", - data: importers.map((k) => closed_by[k].at(-1)), - backgroundColor: importers.map((_) => Statistics.randomColour()), - }, - ], - }, - }) - } - - private static CreateStatePie(noteStates: NoteState[]) { - const colors = { - imported: "#0aa323", - already_mapped: "#00bbff", - invalid: "#ff0000", - closed: "#000000", - not_found: "#ff6d00", - open: "#626262", - has_comments: "#a8a8a8", - } - const knownStates = Object.keys(colors) - const byState = knownStates.map( - (targetState) => noteStates.filter((ns) => ns.status === targetState).length - ) - - return new ChartJs({ - type: "doughnut", - data: { - labels: knownStates.map( - (state, i) => - state + " " + Math.floor((100 * byState[i]) / noteStates.length) + "%" - ), - datasets: [ - { - label: "Status by", - data: byState, - backgroundColor: knownStates.map((state) => colors[state]), - }, - ], - }, - }) - } - - constructor(noteStates: NoteState[]) { - if (noteStates.length === 0) { - super([]) - return - } - // We assume all notes are created at the same time - let dateOpened = new Date(noteStates[0].dateStr) - for (const noteState of noteStates) { - const openDate = new Date(noteState.dateStr) - if (openDate < dateOpened) { - dateOpened = openDate - } - } - const today = new Date() - const daysBetween = (today.getTime() - dateOpened.getTime()) / (1000 * 60 * 60 * 24) - const ranges = { - dates: [], - is_open: [], - } - const closed_by: Record = {} - - for (const noteState of noteStates) { - const closing_user = noteState.props.comments.at(-1).user - if (closed_by[closing_user] === undefined) { - closed_by[closing_user] = [] - } - } - - for (let i = -1; i < daysBetween; i++) { - const dt = new Date(dateOpened.getTime() + 24 * 60 * 60 * 1000 * i) - let open_count = 0 - - for (const closing_user in closed_by) { - closed_by[closing_user].push(0) - } - - for (const noteState of noteStates) { - const openDate = new Date(noteState.dateStr) - if (openDate > dt) { - // Not created at this point - continue - } - if (noteState.props.closed_at === undefined) { - open_count++ - } else if ( - new Date(noteState.props.closed_at.substring(0, 10)).getTime() > dt.getTime() - ) { - open_count++ - } else { - const closing_user = noteState.props.comments.at(-1).user - const user_count = closed_by[closing_user] - user_count[user_count.length - 1] += 1 - } - } - - ranges.dates.push( - new Date(dateOpened.getTime() + i * 1000 * 60 * 60 * 24) - .toISOString() - .substring(0, 10) - ) - ranges.is_open.push(open_count) - } - - const labels = ranges.dates.map((i) => "" + i) - const data = { - labels: labels, - datasets: [ - { - label: "Total open", - data: ranges.is_open, - fill: false, - borderColor: "rgb(75, 192, 192)", - tension: 0.1, - }, - ], - } - for (const closing_user in closed_by) { - if (closed_by[closing_user].at(-1) <= 10) { - continue - } - data.datasets.push({ - label: "Closed by " + closing_user, - data: closed_by[closing_user], - fill: false, - borderColor: Statistics.randomColour(), - tension: 0.1, - }) - } - - super([ - new ChartJs({ - type: "line", - data, - options: { - scales: { - yAxes: [ - { - ticks: { - beginAtZero: true, - }, - }, - ], - }, - }, - }), - new Combine([ - Statistics.CreatePieByAuthor(closed_by), - Statistics.CreateStatePie(noteStates), - ]) - .SetClass("flex w-full h-32") - .SetStyle("width: 40rem"), - ]) - this.SetClass("block w-full h-64 border border-red") - } -} - -class NoteTable extends Combine { - private static individualActions: [() => BaseUIElement, string][] = [ - [Svg.not_found_svg, "This feature does not exist"], - [Svg.addSmall_svg, "imported"], - [Svg.duplicate_svg, "Already mapped"], - ] - - constructor(noteStates: NoteState[], state?: UserRelatedState) { - const typicalComment = noteStates[0].props.comments[0].html - - const table = new Table( - ["id", "status", "last comment", "last modified by", "actions"], - noteStates.map((ns) => NoteTable.noteField(ns, state)), - { sortable: true } - ).SetClass("zebra-table link-underline") - - super([ - new Title("Mass apply an action on " + noteStates.length + " notes below"), - state !== undefined - ? new MassAction( - state, - noteStates.map((ns) => ns.props) - ).SetClass("block") - : undefined, - table, - new Title("Example note", 4), - new FixedUiElement(typicalComment).SetClass("literal-code link-underline"), - ]) - this.SetClass("flex flex-col") - } - - private static noteField(ns: NoteState, state: UserRelatedState) { - const link = new Link( - "" + ns.props.id, - "https://openstreetmap.org/note/" + ns.props.id, - true - ) - let last_comment = "" - const last_comment_props = ns.props.comments[ns.props.comments.length - 1] - const before_last_comment = ns.props.comments[ns.props.comments.length - 2] - if (ns.props.comments.length > 1) { - last_comment = last_comment_props.text - if (last_comment === undefined && before_last_comment?.uid === last_comment_props.uid) { - last_comment = before_last_comment.text - } - } - const statusIcon = BatchView.icons[ns.status]().SetClass("h-4 w-4 shrink-0") - const togglestate = new UIEventSource(false) - const changed = new UIEventSource(undefined) - - const lazyButtons = new Lazy(() => - new Combine( - this.individualActions.map(([img, text]) => - img() - .onClick(async () => { - if (ns.props.status === "closed") { - await state.osmConnection.reopenNote(ns.props.id) - } - await state.osmConnection.closeNote(ns.props.id, text) - changed.setData(text) - }) - .SetClass("h-8 w-8") - ) - ).SetClass("flex") - ) - - const appliedButtons = new VariableUiElement( - changed.map((currentState) => (currentState === undefined ? lazyButtons : currentState)) - ) - - const buttons = Toggle.If( - state?.osmConnection?.isLoggedIn, - () => - new ClickableToggle( - appliedButtons, - new Button("edit...", () => { - console.log("Enabling...") - togglestate.setData(true) - }), - togglestate - ) - ) - return [ - link, - new Combine([statusIcon, ns.status]).SetClass("flex"), - last_comment, - new Link( - last_comment_props.user, - "https://www.openstreetmap.org/user/" + last_comment_props.user, - true - ), - buttons, - ] - } -} - -class BatchView extends Toggleable { - public static icons = { - open: Svg.compass_svg, - has_comments: Svg.speech_bubble_svg, - imported: Svg.addSmall_svg, - already_mapped: Svg.checkmark_svg, - not_found: Svg.not_found_svg, - closed: Svg.close_svg, - invalid: Svg.invalid_svg, - } - - constructor(noteStates: NoteState[], state?: UserRelatedState) { - noteStates.sort((a, b) => a.props.id - b.props.id) - - const { theme, intro, dateStr } = noteStates[0] - - const statusHist = new Map() - for (const noteState of noteStates) { - const st = noteState.status - const c = statusHist.get(st) ?? 0 - statusHist.set(st, c + 1) - } - - const unresolvedTotal = - (statusHist.get("open") ?? 0) + (statusHist.get("has_comments") ?? 0) - const badges: BaseUIElement[] = [ - new FixedUiElement(dateStr).SetClass("literal-code rounded-full"), - new FixedUiElement(noteStates.length + " total") - .SetClass("literal-code rounded-full ml-1 border-4 border-gray") - .onClick(() => filterOn.setData(undefined)), - unresolvedTotal === 0 - ? new Combine([Svg.party_svg().SetClass("h-6 m-1"), "All done!"]).SetClass( - "flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black" - ) - : new FixedUiElement( - Math.round(100 - (100 * unresolvedTotal) / noteStates.length) + "%" - ).SetClass("literal-code rounded-full ml-1"), - ] - - const filterOn = new UIEventSource(undefined) - Object.keys(BatchView.icons).forEach((status) => { - const count = statusHist.get(status) - if (count === undefined) { - return undefined - } - - const normal = new Combine([ - BatchView.icons[status]().SetClass("h-6 m-1"), - count + " " + status, - ]).SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black") - const selected = new Combine([ - BatchView.icons[status]().SetClass("h-6 m-1"), - count + " " + status, - ]).SetClass( - "flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border-4 border-black animate-pulse" - ) - - const toggle = new ClickableToggle( - selected, - normal, - filterOn.sync( - (f) => f === status, - [], - (selected, previous) => { - if (selected) { - return status - } - if (previous === status) { - return undefined - } - return previous - } - ) - ).ToggleOnClick() - - badges.push(toggle) - }) - - const fullTable = new Combine([ - new NoteTable(noteStates, state), - new Statistics(noteStates), - ]) - - super( - new Combine([ - new Title(theme + ": " + intro, 2), - new Combine(badges).SetClass("flex flex-wrap"), - ]), - - new VariableUiElement( - filterOn.map((filter) => { - if (filter === undefined) { - return fullTable - } - const notes = noteStates.filter((ns) => ns.status === filter) - return new Combine([new NoteTable(notes, state), new Statistics(notes)]) - }) - ), - { - closeOnClick: false, - } - ) - } -} - -class ImportInspector extends VariableUiElement { - constructor( - userDetails: { uid: number } | { display_name: string; search?: string }, - state: UserRelatedState - ) { - let url - - if (userDetails["uid"] !== undefined) { - url = - "https://api.openstreetmap.org/api/0.6/notes/search.json?user=" + - userDetails["uid"] + - "&closed=730&limit=10000&sort=created_at&q=%23import" - } else { - url = - "https://api.openstreetmap.org/api/0.6/notes/search.json?display_name=" + - encodeURIComponent(userDetails["display_name"]) + - "&limit=10000&closed=730&sort=created_at&q=" - if (userDetails["search"] !== "") { - url += userDetails["search"] - } else { - url += "#import" - } - } - const notes: UIEventSource< - { error: string } | { success: { features: { properties: NoteProperties }[] } } - > = UIEventSource.FromPromiseWithErr(Utils.downloadJson(url)) - super( - notes.map((notes) => { - if (notes === undefined) { - return new Loading("Loading notes which mention '#import'") - } - if (notes["error"] !== undefined) { - return new FixedUiElement("Something went wrong: " + notes["error"]).SetClass( - "alert" - ) - } - // We only care about the properties here - let props: NoteProperties[] = notes["success"].features.map((f) => f.properties) - if (userDetails["uid"]) { - props = props.filter((n) => n.comments[0].uid === userDetails["uid"]) - } - if (userDetails["display_name"] !== undefined) { - const display_name = userDetails["display_name"] - props = props.filter((n) => n.comments[0].user === display_name) - } - - const perBatch: NoteState[][] = Array.from( - ImportInspector.SplitNotesIntoBatches(props).values() - ) - const els: Toggleable[] = perBatch.map( - (noteStates) => new BatchView(noteStates, state) - ) - - const accordeon = new Accordeon(els) - let contents = [] - if (state?.osmConnection?.isLoggedIn?.data) { - contents = [ - new Title(Translations.t.importInspector.title, 1), - new SubtleButton(undefined, "Create a new batch of imports", { - url: "import_helper.html", - }), - ] - } - contents.push(accordeon) - contents.push( - new Combine([ - new Title("Statistics for all notes"), - new Statistics([].concat(...perBatch)), - ]) - ) - const content = new Combine(contents) - return new LeftIndex( - [ - new TableOfContents(content, { noTopLevel: true, maxDepth: 1 }).SetClass( - "subtle" - ), - new DownloadStatisticsButton(perBatch), - ], - content - ) - }) - ) - } - - /** - * Creates distinct batches of note, where 'date', 'intro' and 'theme' are identical - */ - private static SplitNotesIntoBatches(props: NoteProperties[]): Map { - const perBatch = new Map() - const prefix = "https://mapcomplete.osm.be/" - for (const prop of props) { - const lines = prop.comments[0].text.split("\n") - const trigger = lines.findIndex((l) => l.startsWith(prefix) && l.endsWith("#import")) - if (trigger < 0) { - continue - } - let theme = lines[trigger].substr(prefix.length) - theme = theme.substr(0, theme.indexOf(".")) - const date = Utils.ParseDate(prop.date_created) - const dateStr = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() - const key = theme + lines[0] + dateStr - if (!perBatch.has(key)) { - perBatch.set(key, []) - } - let status: - | "open" - | "closed" - | "imported" - | "invalid" - | "already_mapped" - | "not_found" - | "has_comments" = "open" - - function has(keywords: string[], comment: string): boolean { - return keywords.some((keyword) => comment.toLowerCase().indexOf(keyword) >= 0) - } - - if (prop.closed_at !== undefined) { - const lastComment = prop.comments[prop.comments.length - 1].text.toLowerCase() - if (has(["does not exist", "bestaat niet", "geen"], lastComment)) { - status = "not_found" - } else if ( - has( - [ - "already mapped", - "reeds", - "dubbele note", - "stond er al", - "stonden er al", - "staat er al", - "staan er al", - "stond al", - "stonden al", - "staat al", - "staan al", - ], - lastComment - ) - ) { - status = "already_mapped" - } else if ( - lastComment.indexOf("invalid") >= 0 || - lastComment.indexOf("incorrect") >= 0 - ) { - status = "invalid" - } else if ( - has( - [ - "imported", - "erbij", - "toegevoegd", - "added", - "gemapped", - "gemapt", - "mapped", - "done", - "openstreetmap.org/changeset", - ], - lastComment - ) - ) { - status = "imported" - } else { - status = "closed" - } - } else if (prop.comments.length > 1) { - status = "has_comments" - } - - perBatch.get(key).push({ - props: prop, - intro: lines[0], - theme, - dateStr, - status, - }) - } - return perBatch - } -} - -class ImportViewerGui extends LoginToggle { - constructor() { - const state = new UserRelatedState(undefined) - const displayNameParam = QueryParameters.GetQueryParameter( - "user", - "", - "The username of the person whom you want to see the notes for" - ) - const searchParam = QueryParameters.GetQueryParameter( - "search", - "", - "A text that should be included in the first comment of the note to be shown" - ) - super( - new VariableUiElement( - state.osmConnection.userDetails.map( - (ud) => { - const display_name = displayNameParam.data - const search = searchParam.data - if (display_name !== "" || search !== "") { - return new ImportInspector({ display_name, search }, undefined) - } - return new ImportInspector(ud, state) - }, - [displayNameParam, searchParam] - ) - ), - "Login to inspect your import flows", - state - ) - } -} - -new ImportViewerGui().AttachTo("main") diff --git a/UI/ImportFlow/Introdution.ts b/UI/ImportFlow/Introdution.ts deleted file mode 100644 index e5f14f0af..000000000 --- a/UI/ImportFlow/Introdution.ts +++ /dev/null @@ -1,43 +0,0 @@ -import Combine from "../Base/Combine" -import { FlowStep } from "./FlowStep" -import { UIEventSource } from "../../Logic/UIEventSource" -import Translations from "../i18n/Translations" -import Title from "../Base/Title" -import { CreateNotes } from "./CreateNotes" -import { FixedUiElement } from "../Base/FixedUiElement" - -export default class Introdution extends Combine implements FlowStep { - readonly IsValid: UIEventSource - readonly Value: UIEventSource - - constructor() { - const example = CreateNotes.createNoteContentsUi( - { - properties: { - some_key: "some_value", - note: "a note in the original dataset", - }, - geometry: { - coordinates: [3.4, 51.2], - }, - }, - { - wikilink: - "https://wiki.openstreetmap.org/wiki/Imports/", - intro: "There might be an XYZ here", - theme: "theme", - source: "source of the data", - } - ).map((el) => (el === "" ? new FixedUiElement("").SetClass("block") : el)) - - super([ - new Title(Translations.t.importHelper.introduction.title), - Translations.t.importHelper.introduction.description, - Translations.t.importHelper.introduction.importFormat, - new Combine([new Combine(example).SetClass("flex flex-col")]).SetClass("literal-code"), - ]) - this.SetClass("flex flex-col") - this.IsValid = new UIEventSource(true) - this.Value = new UIEventSource(undefined) - } -} diff --git a/UI/ImportFlow/LoginToImport.ts b/UI/ImportFlow/LoginToImport.ts deleted file mode 100644 index 9e5a6696c..000000000 --- a/UI/ImportFlow/LoginToImport.ts +++ /dev/null @@ -1,74 +0,0 @@ -import Combine from "../Base/Combine" -import { FlowStep } from "./FlowStep" -import UserRelatedState from "../../Logic/State/UserRelatedState" -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import Translations from "../i18n/Translations" -import Title from "../Base/Title" -import { VariableUiElement } from "../Base/VariableUIElement" -import { LoginToggle } from "../Popup/LoginButton" -import Img from "../Base/Img" -import Constants from "../../Models/Constants" -import Toggle from "../Input/Toggle" -import { SubtleButton } from "../Base/SubtleButton" -import Svg from "../../Svg" -import MoreScreen from "../BigComponents/MoreScreen" -import CheckBoxes from "../Input/Checkboxes" - -export default class LoginToImport extends Combine implements FlowStep { - readonly IsValid: Store - readonly Value: Store - - private static readonly whitelist = [15015689] - - constructor(state: UserRelatedState) { - const t = Translations.t.importHelper.login - const check = new CheckBoxes([ - new VariableUiElement( - state.osmConnection.userDetails.map((ud) => t.loginIsCorrect.Subs(ud)) - ), - ]) - const isValid = state.osmConnection.userDetails.map( - (ud) => - LoginToImport.whitelist.indexOf(ud.uid) >= 0 || - ud.csCount >= Constants.userJourney.importHelperUnlock - ) - super([ - new Title(t.userAccountTitle), - new LoginToggle( - new VariableUiElement( - state.osmConnection.userDetails.map((ud) => { - if (ud === undefined) { - return undefined - } - return new Combine([ - new Img(ud.img ?? "./assets/svgs/help.svg").SetClass( - "w-16 h-16 rounded-full" - ), - t.loggedInWith.Subs(ud), - new SubtleButton( - Svg.logout_svg().SetClass("h-8"), - Translations.t.general.logout - ).onClick(() => state.osmConnection.LogOut()), - check, - ]) - }) - ), - t.loginRequired, - state - ), - new Toggle( - undefined, - new Combine([ - t.lockNotice.Subs(Constants.userJourney).SetClass("alert"), - MoreScreen.CreateProffessionalSerivesButton(), - ]), - isValid - ), - ]) - this.Value = new UIEventSource(state) - this.IsValid = isValid.map( - (isValid) => isValid && check.GetValue().data.length > 0, - [check.GetValue()] - ) - } -} diff --git a/UI/ImportFlow/MapPreview.ts b/UI/ImportFlow/MapPreview.ts deleted file mode 100644 index 854486044..000000000 --- a/UI/ImportFlow/MapPreview.ts +++ /dev/null @@ -1,162 +0,0 @@ -import Combine from "../Base/Combine" -import { Store, 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 { DropDown } from "../Input/DropDown" -import { Utils } from "../../Utils" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import Loc from "../../Models/Loc" -import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" -import Toggle from "../Input/Toggle" -import { VariableUiElement } from "../Base/VariableUIElement" -import { FlowStep } from "./FlowStep" -import Title from "../Base/Title" -import CheckBoxes from "../Input/Checkboxes" -import { Feature, Point } from "geojson" -import DivContainer from "../Base/DivContainer" -import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers" -import { MapLibreAdaptor } from "../Map/MapLibreAdaptor" -import ShowDataLayer from "../Map/ShowDataLayer" - -/** - * Shows the data to import on a map, asks for the correct layer to be selected - */ -export class MapPreview - extends Combine - implements FlowStep<{ bbox: BBox; layer: LayerConfig; features: Feature[] }> -{ - public readonly IsValid: Store - public readonly Value: Store<{ bbox: BBox; layer: LayerConfig; features: any[] }> - - constructor(state: UserRelatedState, geojson: { features: Feature[] }) { - const t = Translations.t.importHelper.mapPreview - - const propertyKeys = new Set() - for (const f of geojson.features) { - Object.keys(f.properties).forEach((key) => propertyKeys.add(key)) - } - - const availableLayers = AllKnownLayouts.AllPublicLayers().filter( - (l) => l.name !== undefined && l.source !== undefined - ) - const layerPicker = new DropDown( - t.selectLayer, - [{ 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) { - console.log("Autodected layer", layer.id) - 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 - }) - - // Create a store which has only features matching the selected layer - const matching: Store = layerPicker.GetValue().map((layer: LayerConfig) => { - if (layer === undefined) { - console.log("No matching layer found") - return [] - } - const matching: Feature[] = [] - - for (const feature of withId) { - if (layer.source.osmTags.matchesProperties(feature.properties)) { - matching.push(feature) - } - } - console.log("Matching features: ", matching) - - return matching - }) - const background = new UIEventSource(AvailableRasterLayers.osmCarto) - const location = new UIEventSource({ lat: 0, lon: 0, zoom: 1 }) - const currentBounds = new UIEventSource(undefined) - const { ui, mapproperties, map } = MapLibreAdaptor.construct() - - ui.SetClass("w-full").SetStyle("height: 500px") - - layerPicker.GetValue().addCallbackAndRunD((layerToShow) => { - new ShowDataLayer(map, { - layer: layerToShow, - zoomToFeatures: true, - features: new StaticFeatureSource(matching), - }) - }) - - const bbox = matching.map((feats) => - BBox.bboxAroundAll( - feats.map((f) => new BBox([(>f).geometry.coordinates])) - ) - ) - - const mismatchIndicator = 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 t.mismatch.Subs({ count: diff, tags: obligatory }).SetClass("alert") - }) - ) - - const confirm = new CheckBoxes([t.confirm]) - super([ - new Title(t.title, 1), - layerPicker, - new Toggle(t.autodetected.SetClass("thanks"), undefined, autodetected), - mismatchIndicator, - ui, - new DivContainer("fullscreen"), - confirm, - ]) - - this.Value = bbox.map( - (bbox) => ({ - bbox, - features: matching.data, - layer: layerPicker.GetValue().data, - }), - [layerPicker.GetValue(), matching] - ) - - this.IsValid = matching.map( - (matching) => { - if (matching === undefined) { - return false - } - if (confirm.GetValue().data.length !== 1) { - return false - } - const diff = geojson.features.length - matching.length - return diff === 0 - }, - [confirm.GetValue()] - ) - } -} diff --git a/UI/ImportFlow/PreviewPanel.ts b/UI/ImportFlow/PreviewPanel.ts deleted file mode 100644 index fd4a64e5b..000000000 --- a/UI/ImportFlow/PreviewPanel.ts +++ /dev/null @@ -1,94 +0,0 @@ -import Combine from "../Base/Combine" -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import UserRelatedState from "../../Logic/State/UserRelatedState" -import Translations from "../i18n/Translations" -import { Utils } from "../../Utils" -import { FlowStep } from "./FlowStep" -import Title from "../Base/Title" -import BaseUIElement from "../BaseUIElement" -import Histogram from "../BigComponents/Histogram" -import Toggleable from "../Base/Toggleable" -import List from "../Base/List" -import CheckBoxes from "../Input/Checkboxes" -import { Feature, Point } from "geojson" - -/** - * Shows the attributes by value, requests to check them of - */ -export class PreviewAttributesPanel - extends Combine - implements FlowStep<{ features: Feature[] }> -{ - public readonly IsValid: Store - public readonly Value: Store<{ features: Feature[] }> - - constructor(state: UserRelatedState, geojson: { features: Feature[] }) { - const t = Translations.t.importHelper.previewAttributes - - const propertyKeys = new Set() - for (const f of geojson.features) { - Object.keys(f.properties).forEach((key) => propertyKeys.add(key)) - } - - const attributeOverview: BaseUIElement[] = [] - - const n = geojson.features.length - for (const key of Array.from(propertyKeys)) { - const values = Utils.NoNull(geojson.features.map((f) => f.properties[key])) - const allSame = !values.some((v) => v !== values[0]) - let countSummary: BaseUIElement - if (values.length === n) { - countSummary = t.allAttributesSame - } else { - countSummary = t.someHaveSame.Subs({ - count: values.length, - percentage: Math.floor((100 * values.length) / n), - }) - } - if (allSame) { - attributeOverview.push(new Title(key + "=" + values[0])) - attributeOverview.push(countSummary) - continue - } - - const uniqueCount = new Set(values).size - if (uniqueCount !== values.length && uniqueCount < 15) { - attributeOverview.push() - // There are some overlapping values: histogram time! - let hist: BaseUIElement = new Combine([ - countSummary, - new Histogram(new UIEventSource(values), "Value", "Occurence", { - sortMode: "count-rev", - }), - ]).SetClass("flex flex-col") - - const title = new Title(key + "=*") - if (uniqueCount > 15) { - hist = new Toggleable(title, hist.SetClass("block")).Collapse() - } else { - attributeOverview.push(title) - } - - attributeOverview.push(hist) - continue - } - - // All values are different or too much unique values, we add a boring (but collapsable) list - attributeOverview.push( - new Toggleable(new Title(key + "=*"), new Combine([countSummary, new List(values)])) - ) - } - - const confirm = new CheckBoxes([t.inspectLooksCorrect]) - - super([ - new Title(t.inspectDataTitle.Subs({ count: geojson.features.length })), - "Extra remark: An attribute with 'source' or 'src' will be added as 'source' into the map pin; an attribute 'note' will be added into the map pin as well. These values won't be imported", - ...attributeOverview, - confirm, - ]) - - this.Value = new UIEventSource<{ features: Feature[] }>(geojson) - this.IsValid = confirm.GetValue().map((selected) => selected.length == 1) - } -} diff --git a/UI/ImportFlow/RequestFile.ts b/UI/ImportFlow/RequestFile.ts deleted file mode 100644 index 8d550d1bd..000000000 --- a/UI/ImportFlow/RequestFile.ts +++ /dev/null @@ -1,188 +0,0 @@ -import Combine from "../Base/Combine" -import { Store, Stores } from "../../Logic/UIEventSource" -import Translations from "../i18n/Translations" -import { SubtleButton } from "../Base/SubtleButton" -import { VariableUiElement } from "../Base/VariableUIElement" -import Title from "../Base/Title" -import InputElementMap from "../Input/InputElementMap" -import BaseUIElement from "../BaseUIElement" -import FileSelectorButton from "../Input/FileSelectorButton" -import { FlowStep } from "./FlowStep" -import { parse } from "papaparse" -import { FixedUiElement } from "../Base/FixedUiElement" -import { TagUtils } from "../../Logic/Tags/TagUtils" -import { Feature, Point } from "geojson" - -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<{ features: any[] }> { - public readonly IsValid: Store - /** - * The loaded GeoJSON - */ - public readonly Value: Store<{ features: Feature[] }> - - constructor() { - const t = Translations.t.importHelper.selectFile - const csvSelector = new FileSelector(new SubtleButton(undefined, t.description)) - 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 = Stores.flatten( - csvSelector.GetValue().map((v) => { - if (v === undefined) { - return undefined - } - return Stores.FromPromise(v.contents) - }) - ) - - const asGeoJson: Store = text.map( - (src: string) => { - if (src === undefined) { - return undefined - } - try { - const parsed = JSON.parse(src) - if (parsed["type"] !== "FeatureCollection") { - return { error: t.errNotFeatureCollection } - } - if (parsed.features.some((f) => f.geometry.type != "Point")) { - return { error: t.errPointsOnly } - } - parsed.features.forEach((f) => { - const props = f.properties - for (const key in props) { - if ( - props[key] === undefined || - props[key] === null || - props[key] === "" - ) { - delete props[key] - } - if (!TagUtils.isValidKey(key)) { - return { error: "Probably an invalid key: " + key } - } - } - }) - return parsed - } catch (e) { - // Loading as CSV - var lines: string[][] = parse(src).data - const header = lines[0] - lines.splice(0, 1) - if (header.indexOf("lat") < 0 || header.indexOf("lon") < 0) { - return { error: t.errNoLatOrLon } - } - - if (header.some((h) => h.trim() == "")) { - return { error: t.errNoName } - } - - if (new Set(header).size !== header.length) { - return { error: t.errDuplicate } - } - - const features = [] - for (let i = 0; i < lines.length; i++) { - const attrs = lines[i] - if (attrs.length == 0 || (attrs.length == 1 && attrs[0] == "")) { - // empty line - continue - } - const properties = {} - for (let i = 0; i < header.length; i++) { - const v = attrs[i] - if (v === undefined || v === "") { - continue - } - properties[header[i]] = v - } - 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 - } - let err: BaseUIElement - if (typeof v.error === "string") { - err = new FixedUiElement(v.error) - } else if (v.error.Clone !== undefined) { - err = v.error.Clone() - } else { - err = v.error - } - return err.SetClass("alert") - }) - ) - - super([ - new Title(t.title, 1), - t.fileFormatDescription, - t.fileFormatDescriptionCsv, - t.fileFormatDescriptionGeoJson, - csvSelector, - loadedFiles, - errorIndicator, - ]) - this.SetClass("flex flex-col wi") - this.IsValid = asGeoJson.map( - (geojson) => geojson !== undefined && geojson["error"] === undefined - ) - this.Value = asGeoJson - } -} diff --git a/UI/ImportFlow/SelectTheme.ts b/UI/ImportFlow/SelectTheme.ts deleted file mode 100644 index e750c0293..000000000 --- a/UI/ImportFlow/SelectTheme.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { FlowStep } from "./FlowStep" -import Combine from "../Base/Combine" -import { Store } from "../../Logic/UIEventSource" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import { InputElement } from "../Input/InputElement" -import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts" -import { FixedInputElement } from "../Input/FixedInputElement" -import Img from "../Base/Img" -import Title from "../Base/Title" -import { RadioButton } from "../Input/RadioButton" -import { And } from "../../Logic/Tags/And" -import { VariableUiElement } from "../Base/VariableUIElement" -import Toggleable from "../Base/Toggleable" -import { BBox } from "../../Logic/BBox" -import BaseUIElement from "../BaseUIElement" -import PresetConfig from "../../Models/ThemeConfig/PresetConfig" -import List from "../Base/List" -import Translations from "../i18n/Translations" - -export default class SelectTheme - extends Combine - implements - FlowStep<{ - features: any[] - theme: string - layer: LayerConfig - bbox: BBox - }> -{ - public readonly Value: Store<{ - features: any[] - theme: string - layer: LayerConfig - bbox: BBox - }> - public readonly IsValid: Store - - constructor(params: { features: any[]; layer: LayerConfig; bbox: BBox }) { - const t = Translations.t.importHelper.selectTheme - let options: InputElement[] = Array.from(AllKnownLayouts.allKnownLayouts.values()) - .filter((th) => th.layers.some((l) => l.id === params.layer.id)) - .filter((th) => th.id !== "personal") - .map( - (th) => - new FixedInputElement( - new Combine([ - new Img(th.icon).SetClass("block h-12 w-12 br-4"), - new Title(th.title), - ]).SetClass("flex items-center"), - th.id - ) - ) - - const themeRadios = new RadioButton(options, { - selectFirstAsDefault: false, - }) - - const applicablePresets = themeRadios.GetValue().map((theme) => { - if (theme === undefined) { - return [] - } - // we get the layer with the correct ID via the actual theme config, as the actual theme might have different presets due to overrides - const themeConfig = AllKnownLayouts.allKnownLayouts.get(theme) - const layer = themeConfig.layers.find((l) => l.id === params.layer.id) - return layer.presets - }) - - const nonMatchedElements = applicablePresets.map((presets) => { - if (presets === undefined || presets.length === 0) { - return undefined - } - return params.features.filter( - (feat) => - !presets.some((preset) => - new And(preset.tags).matchesProperties(feat.properties) - ) - ) - }) - - super([ - new Title(t.title), - t.intro, - themeRadios, - new VariableUiElement( - applicablePresets.map( - (applicablePresets) => { - if (themeRadios.GetValue().data === undefined) { - return undefined - } - if (applicablePresets === undefined || applicablePresets.length === 0) { - return t.noMatchingPresets.SetClass("alert") - } - }, - [themeRadios.GetValue()] - ) - ), - - new VariableUiElement( - nonMatchedElements.map( - (unmatched) => - SelectTheme.nonMatchedElementsPanel(unmatched, applicablePresets.data), - [applicablePresets] - ) - ), - ]) - this.SetClass("flex flex-col") - - this.Value = themeRadios.GetValue().map((theme) => ({ - features: params.features, - layer: params.layer, - bbox: params.bbox, - theme, - })) - - this.IsValid = this.Value.map( - (obj) => { - if (obj === undefined) { - return false - } - if ([obj.theme, obj.features].some((v) => v === undefined)) { - return false - } - if (applicablePresets.data === undefined || applicablePresets.data.length === 0) { - return false - } - if ((nonMatchedElements.data?.length ?? 0) > 0) { - return false - } - - return true - }, - [applicablePresets] - ) - } - - private static nonMatchedElementsPanel( - unmatched: any[], - applicablePresets: PresetConfig[] - ): BaseUIElement { - if (unmatched === undefined || unmatched.length === 0) { - return - } - const t = Translations.t.importHelper.selectTheme - - const applicablePresetsOverview = applicablePresets.map((preset) => - t.needsTags - .Subs({ - title: preset.title, - tags: preset.tags.map((t) => t.asHumanString()).join(" & "), - }) - .SetClass("thanks") - ) - - const unmatchedPanels: BaseUIElement[] = [] - for (const feat of unmatched) { - const parts: BaseUIElement[] = [] - parts.push( - new Combine( - Object.keys(feat.properties).map((k) => k + "=" + feat.properties[k]) - ).SetClass("flex flex-col") - ) - - for (const preset of applicablePresets) { - const tags = new And(preset.tags).asChange({}) - const missing = [] - for (const { k, v } of tags) { - if (preset[k] === undefined) { - missing.push(t.missing.Subs({ k, v })) - } else if (feat.properties[k] !== v) { - missing.push(t.misMatch.Subs({ k, v, properties: feat.properties })) - } - } - - if (missing.length > 0) { - parts.push( - new Combine([t.notApplicable.Subs(preset), new List(missing)]).SetClass( - "flex flex-col alert" - ) - ) - } - } - - unmatchedPanels.push(new Combine(parts).SetClass("flex flex-col")) - } - - return new Combine([ - t.displayNonMatchingCount.Subs(unmatched).SetClass("alert"), - ...applicablePresetsOverview, - new Toggleable(new Title(t.unmatchedTitle), new Combine(unmatchedPanels)), - ]).SetClass("flex flex-col") - } -} diff --git a/UI/Map/MapLibreAdaptor.ts b/UI/Map/MapLibreAdaptor.ts index f989f164a..5437beb7b 100644 --- a/UI/Map/MapLibreAdaptor.ts +++ b/UI/Map/MapLibreAdaptor.ts @@ -196,7 +196,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { async exportAsPng(): Promise { const map = this._maplibreMap.data - if (map === undefined) { + if (!map) { return undefined } @@ -317,7 +317,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { private async awaitStyleIsLoaded(): Promise { const map = this._maplibreMap.data - if (map === undefined) { + if (!map) { return } while (!map?.isStyleLoaded()) { @@ -335,7 +335,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { private async setBackground() { const map = this._maplibreMap.data - if (map === undefined) { + if (!map) { return } const background: RasterLayerProperties = this.rasterLayer?.data?.properties @@ -381,7 +381,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { private setMaxBounds(bbox: undefined | BBox) { const map = this._maplibreMap.data - if (map === undefined) { + if (!map) { return } if (bbox) { @@ -393,7 +393,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { private setAllowMoving(allow: true | boolean | undefined) { const map = this._maplibreMap.data - if (map === undefined) { + if (!map) { return } if (allow === false) { @@ -409,7 +409,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { private setMinzoom(minzoom: number) { const map = this._maplibreMap.data - if (map === undefined) { + if (!map) { return } map.setMinZoom(minzoom) @@ -417,7 +417,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { private setMaxzoom(maxzoom: number) { const map = this._maplibreMap.data - if (map === undefined) { + if (!map) { return } map.setMaxZoom(maxzoom) @@ -425,7 +425,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { private setAllowZooming(allow: true | boolean | undefined) { const map = this._maplibreMap.data - if (map === undefined) { + if (!map) { return } if (allow === false) { @@ -441,7 +441,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { private setBounds(bounds: BBox) { const map = this._maplibreMap.data - if (map === undefined || bounds === undefined) { + if (!map || bounds === undefined) { return } const oldBounds = map.getBounds() diff --git a/UI/Popup/TagRendering/TagRenderingQuestion.svelte b/UI/Popup/TagRendering/TagRenderingQuestion.svelte index f5c952baa..06874edb5 100644 --- a/UI/Popup/TagRendering/TagRenderingQuestion.svelte +++ b/UI/Popup/TagRendering/TagRenderingQuestion.svelte @@ -164,9 +164,9 @@ {#if config.freeform?.key} {/if} @@ -182,7 +182,7 @@ {/each} {#if config.freeform?.key}