From 732189955bab169bc63c439762ba0bf25c5058f6 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 1 Aug 2024 19:34:13 +0200 Subject: [PATCH 01/53] Better error message if invalid theme --- src/Logic/DetermineLayout.ts | 26 +++++++++++++------------- src/all_themes_index.ts | 1 - src/index.ts | 1 - 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Logic/DetermineLayout.ts b/src/Logic/DetermineLayout.ts index 0debbb5c65..ae2ffc734d 100644 --- a/src/Logic/DetermineLayout.ts +++ b/src/Logic/DetermineLayout.ts @@ -14,11 +14,7 @@ import licenses from "../assets/generated/license_info.json" import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" import { FixImages } from "../Models/ThemeConfig/Conversion/FixImages" import questions from "../assets/generated/layers/questions.json" -import { - DoesImageExist, - PrevalidateTheme, - ValidateThemeAndLayers, -} from "../Models/ThemeConfig/Conversion/Validation" +import { DoesImageExist, PrevalidateTheme, ValidateThemeAndLayers } from "../Models/ThemeConfig/Conversion/Validation" import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion" import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson" import Hash from "./Web/Hash" @@ -109,11 +105,14 @@ export default class DetermineLayout { layoutId, "The layout to load into MapComplete" ).data - const layout = AllKnownLayouts.allKnownLayouts.get(layoutId?.toLowerCase()) - if (layout === undefined) { - throw "No builtin map theme with name " + layoutId + " exists" + const id = layoutId?.toLowerCase() + const layouts = AllKnownLayouts.allKnownLayouts + if (layouts.getConfig(id) === undefined) { + const alternatives = Utils.sortedByLevenshteinDistance(id, Array.from(layouts.keys()), i => i).slice(0, 3) + const msg = (`No builtin map theme with name ${layoutId} exists. Perhaps you meant one of ${alternatives.join(", ")}`) + throw msg } - return layout + return layouts.get(id) } public static async LoadLayoutFromHash( @@ -162,6 +161,7 @@ export default class DetermineLayout { return dict } + private static getSharedTagRenderingOrder(): string[] { return questions.tagRenderings.map((tr) => tr.id) } @@ -200,11 +200,11 @@ export default class DetermineLayout { id: json.id, description: json.description, descriptionTail: { - en: "
Layer only mode.
The loaded custom theme actually isn't a custom theme, but only contains a layer.", + en: "
Layer only mode.
The loaded custom theme actually isn't a custom theme, but only contains a layer." }, icon, title: json.name, - layers: [json], + layers: [json] } } @@ -217,7 +217,7 @@ export default class DetermineLayout { tagRenderings: DetermineLayout.getSharedTagRenderings(), tagRenderingOrder: DetermineLayout.getSharedTagRenderingOrder(), sharedLayers: knownLayersDict, - publicLayers: new Set(), + publicLayers: new Set() } json = new FixLegacyTheme().convertStrict(json) const raw = json @@ -241,7 +241,7 @@ export default class DetermineLayout { } return new LayoutConfig(json, false, { definitionRaw: JSON.stringify(raw, null, " "), - definedAtUrl: sourceUrl, + definedAtUrl: sourceUrl }) } diff --git a/src/all_themes_index.ts b/src/all_themes_index.ts index 5fac705c0c..fb4f4fe0c4 100644 --- a/src/all_themes_index.ts +++ b/src/all_themes_index.ts @@ -1,5 +1,4 @@ import { QueryParameters } from "./Logic/Web/QueryParameters" -import SvelteUIElement from "./UI/Base/SvelteUIElement" import AllThemesGui from "./UI/AllThemesGui.svelte" const layout = QueryParameters.GetQueryParameter("layout", undefined).data ?? "" diff --git a/src/index.ts b/src/index.ts index 975335b879..263584cb06 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,7 +41,6 @@ async function getAvailableLayers(): Promise> { } async function main() { - // @ts-ignore try { if (!webgl_support()) { throw "WebGL is not supported or not enabled. This is essential for MapComplete to function, please enable this." From c9fa625c98afc4a0fe012a5aad929ef16e505d2a Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 1 Aug 2024 19:35:08 +0200 Subject: [PATCH 02/53] Refactoring: port CloseNoteButton to svelte --- package.json | 2 +- src/UI/Popup/LoginButton.ts | 96 -------------------- src/UI/Popup/Notes/CloseNoteButton.svelte | 54 +++++++++++ src/UI/Popup/Notes/CloseNoteButton.ts | 106 ---------------------- src/UI/SpecialVisualizations.ts | 65 ++++++++++++- src/Utils.ts | 6 +- 6 files changed, 120 insertions(+), 209 deletions(-) delete mode 100644 src/UI/Popup/LoginButton.ts create mode 100644 src/UI/Popup/Notes/CloseNoteButton.svelte delete mode 100644 src/UI/Popup/Notes/CloseNoteButton.ts diff --git a/package.json b/package.json index 5ff3e109b0..d0383e0ba3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapcomplete", - "version": "0.44.13", + "version": "0.44.14", "repository": "https://github.com/pietervdvn/MapComplete", "description": "A small website to edit OSM easily", "bugs": "https://github.com/pietervdvn/MapComplete/issues", diff --git a/src/UI/Popup/LoginButton.ts b/src/UI/Popup/LoginButton.ts deleted file mode 100644 index 846a5f4f11..0000000000 --- a/src/UI/Popup/LoginButton.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { SubtleButton } from "../Base/SubtleButton" -import BaseUIElement from "../BaseUIElement" -import { OsmConnection, OsmServiceState } from "../../Logic/Osm/OsmConnection" -import { VariableUiElement } from "../Base/VariableUIElement" -import Loading from "../Base/Loading" -import Translations from "../i18n/Translations" -import { ImmutableStore, Store } from "../../Logic/UIEventSource" -import Combine from "../Base/Combine" -import { Translation } from "../i18n/Translation" -import SvelteUIElement from "../Base/SvelteUIElement" -import Login from "../../assets/svg/Login.svelte" -import Invalid from "../../assets/svg/Invalid.svelte" - -class LoginButton extends SubtleButton { - constructor( - text: BaseUIElement | string, - state: { - osmConnection?: OsmConnection - }, - icon?: BaseUIElement | string - ) { - super(icon ?? new SvelteUIElement(Login), text) - this.onClick(() => { - state.osmConnection?.AttemptLogin() - }) - } -} - -export class LoginToggle extends VariableUiElement { - /** - * Constructs an element which shows 'el' if the user is logged in - * If not logged in, 'text' is shown on the button which invites to login. - * - * If logging in is not possible for some reason, an appropriate error message is shown - * - * State contains the 'osmConnection' to work with - * @param el: Element to show when logged in - * @param text: To show on the login button. Default: nothing - * @param state: if no osmConnection is given, assumes test situation and will show 'el' as if logged in - */ - constructor( - el: BaseUIElement, - text: BaseUIElement | string, - state: { - readonly osmConnection?: OsmConnection - readonly featureSwitchUserbadge?: Store - } - ) { - const loading = new Loading("Trying to log in...") - const login = text === undefined ? undefined : new LoginButton(text, state) - const t = Translations.t.general - const offlineModes: Partial> = { - offline: t.loginFailedOfflineMode, - unreachable: t.loginFailedUnreachableMode, - readonly: t.loginFailedReadonlyMode, - } - - super( - state.osmConnection?.loadingStatus?.map( - (osmConnectionState) => { - if (state.featureSwitchUserbadge?.data == false) { - // All features to login with are disabled - return undefined - } - - const apiState = state.osmConnection?.apiIsOnline?.data ?? "online" - const apiTranslation = offlineModes[apiState] - if (apiTranslation !== undefined) { - return new Combine([ - new SvelteUIElement(Invalid).SetClass("w-8 h-8 m-2 shrink-0"), - apiTranslation, - ]).SetClass("flex items-center alert max-w-64") - } - - if (osmConnectionState === "loading") { - return loading - } - if (osmConnectionState === "not-attempted") { - return login - } - if (osmConnectionState === "logged-in") { - return el - } - - // Fallback - return new LoginButton( - Translations.t.general.loginFailed, - state, - new SvelteUIElement(Invalid) - ) - }, - [state.featureSwitchUserbadge, state.osmConnection?.apiIsOnline] - ) ?? new ImmutableStore(el) - ) - } -} diff --git a/src/UI/Popup/Notes/CloseNoteButton.svelte b/src/UI/Popup/Notes/CloseNoteButton.svelte new file mode 100644 index 0000000000..31fe64ab59 --- /dev/null +++ b/src/UI/Popup/Notes/CloseNoteButton.svelte @@ -0,0 +1,54 @@ + + + +
+ +
+ + + {#if $isClosed} + + {:else if minzoom <= $curZoom} + + {:else if zoomMoreMessage} + {zoomMoreMessage} + {/if} + +
diff --git a/src/UI/Popup/Notes/CloseNoteButton.ts b/src/UI/Popup/Notes/CloseNoteButton.ts deleted file mode 100644 index 657f0f97fe..0000000000 --- a/src/UI/Popup/Notes/CloseNoteButton.ts +++ /dev/null @@ -1,106 +0,0 @@ -import BaseUIElement from "../../BaseUIElement" -import Translations from "../../i18n/Translations" -import { Utils } from "../../../Utils" -import Img from "../../Base/Img" -import { SubtleButton } from "../../Base/SubtleButton" -import Toggle from "../../Input/Toggle" -import { LoginToggle } from ".././LoginButton" -import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization" -import { UIEventSource } from "../../../Logic/UIEventSource" -import Constants from "../../../Models/Constants" -import SvelteUIElement from "../../Base/SvelteUIElement" -import Checkmark from "../../../assets/svg/Checkmark.svelte" -import NoteCommentElement from "./NoteCommentElement" -import Icon from "../../Map/Icon.svelte" - -export class CloseNoteButton implements SpecialVisualization { - public readonly funcName = "close_note" - public readonly needsUrls = [Constants.osmAuthConfig.url] - public readonly docs = - "Button to close a note. A predefined text can be defined to close the note with. If the note is already closed, will show a small text." - public readonly args = [ - { - name: "text", - doc: "Text to show on this button", - required: true, - }, - { - name: "icon", - doc: "Icon to show", - defaultValue: "checkmark.svg", - }, - { - name: "idkey", - doc: "The property name where the ID of the note to close can be found", - defaultValue: "id", - }, - { - name: "comment", - doc: "Text to add onto the note when closing", - }, - { - name: "minZoom", - doc: "If set, only show the closenote button if zoomed in enough", - }, - { - name: "zoomButton", - doc: "Text to show if not zoomed in enough", - }, - ] - - public constr( - state: SpecialVisualizationState, - tags: UIEventSource>, - args: string[] - ): BaseUIElement { - const t = Translations.t.notes - - const params: { - text: string - icon: string - idkey: string - comment: string - minZoom: string - zoomButton: string - } = Utils.ParseVisArgs(this.args, args) - - let icon: BaseUIElement = new SvelteUIElement(Icon, { - icon: params.icon ?? "checkmark.svg", - }) - let textToShow = t.closeNote - if ((params.text ?? "") !== "") { - textToShow = Translations.T(args[0]) - } - - let closeButton: BaseUIElement = new SubtleButton(icon, textToShow) - const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "") - closeButton.onClick(() => { - const id = tags.data[args[2] ?? "id"] - const text = args[3] - state.osmConnection.closeNote(id, text)?.then((_) => { - NoteCommentElement.addCommentTo(text, tags, state) - tags.data["closed_at"] = new Date().toISOString() - tags.ping() - }) - }) - - if ((params.minZoom ?? "") !== "" && !isNaN(Number(params.minZoom))) { - closeButton = new Toggle( - closeButton, - params.zoomButton ?? "", - state.mapProperties.zoom.map((zoom) => zoom >= Number(params.minZoom)) - ) - } - - return new LoginToggle( - new Toggle( - t.isClosed.SetClass("thanks"), - closeButton, - - isClosed - ), - t.loginToClose, - state - ) - } -} diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index e2c38da117..47629cd1cc 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -15,7 +15,6 @@ import { MultiApplyViz } from "./Popup/MultiApplyViz" import { AddNoteCommentViz } from "./Popup/Notes/AddNoteCommentViz" import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz" import TagApplyButton from "./Popup/TagApplyButton" -import { CloseNoteButton } from "./Popup/Notes/CloseNoteButton" import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis" import { ImmutableStore, Store, Stores, UIEventSource } from "../Logic/UIEventSource" import AllTagsPanel from "./Popup/AllTagsPanel.svelte" @@ -99,6 +98,7 @@ import Trash from "@babeard/svelte-heroicons/mini/Trash" import NothingKnown from "./Popup/NothingKnown.svelte" import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" import { And } from "../Logic/Tags/And" +import CloseNoteButton from "./Popup/Notes/CloseNoteButton.svelte" class NearbyImageVis implements SpecialVisualization { // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests @@ -215,6 +215,66 @@ class StealViz implements SpecialVisualization { } } +class CloseNoteViz implements SpecialVisualization { + public readonly funcName = "close_note" + public readonly needsUrls = [Constants.osmAuthConfig.url] + public readonly docs = + "Button to close a note. A predefined text can be defined to close the note with. If the note is already closed, will show a small text." + public readonly args = [ + { + name: "text", + doc: "Text to show on this button", + required: true, + }, + { + name: "icon", + doc: "Icon to show", + defaultValue: "checkmark.svg", + }, + { + name: "idkey", + doc: "The property name where the ID of the note to close can be found", + defaultValue: "id", + }, + { + name: "comment", + doc: "Text to add onto the note when closing", + }, + { + name: "minZoom", + doc: "If set, only show the closenote button if zoomed in enough", + }, + { + name: "zoomButton", + doc: "Text to show if not zoomed in enough", + }, + ] + + public constr(state: SpecialVisualizationState, tags: UIEventSource>, args: string[], feature: Feature, layer: LayerConfig): SvelteUIElement { + + const { + text, + icon, + idkey, + comment, + minZoom, + zoomButton + } = Utils.ParseVisArgs(this.args, args) + + + return new SvelteUIElement(CloseNoteButton, { + state, + tags, + icon, + idkey, + message: comment, + text: Translations.T(text), + minzoom: minZoom, + zoomMoreMessage: zoomButton + }) + } +} + /** * Thin wrapper around QuestionBox.svelte to include it into the special Visualisations */ @@ -526,7 +586,7 @@ export default class SpecialVisualizations { }) }, }, - new CloseNoteButton(), + new CloseNoteViz(), new PlantNetDetectionViz(), new TagApplyButton(), @@ -534,7 +594,6 @@ export default class SpecialVisualizations { new PointImportButtonViz(), new WayImportButtonViz(), new ConflateImportButtonViz(), - new NearbyImageVis(), { diff --git a/src/Utils.ts b/src/Utils.ts index 0bdfcfcdf1..f9947d7f80 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -170,10 +170,10 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be /** * Parses the arguments for special visualisations */ - public static ParseVisArgs( + public static ParseVisArgs>( specs: { name: string; defaultValue?: string }[], args: string[] - ): Record { + ): T { const parsed: Record = {} if (args.length > specs.length) { throw ( @@ -193,7 +193,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be parsed[spec.name] = arg } - return parsed + return parsed } static EncodeXmlValue(str) { From 994bd5f091a1378ce1d928b1f1cf9076f833bb1d Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 1 Aug 2024 19:42:32 +0200 Subject: [PATCH 03/53] Refactoring: Remove some unused or deprecated classes --- src/UI/Base/Button.ts | 25 ------------------------- src/UI/Base/DivContainer.ts | 19 ------------------- src/UI/Base/Hotkeys.ts | 8 +------- src/UI/Base/TableOfContents.ts | 1 - src/UI/Input/Toggle.ts | 14 -------------- src/UI/i18n/Translations.ts | 6 ------ 6 files changed, 1 insertion(+), 72 deletions(-) delete mode 100644 src/UI/Base/Button.ts delete mode 100644 src/UI/Base/DivContainer.ts diff --git a/src/UI/Base/Button.ts b/src/UI/Base/Button.ts deleted file mode 100644 index ea514be65c..0000000000 --- a/src/UI/Base/Button.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Translations from "../i18n/Translations" -import BaseUIElement from "../BaseUIElement" - -export class Button extends BaseUIElement { - private _text: BaseUIElement - - constructor(text: string | BaseUIElement, onclick: () => void | Promise) { - super() - this._text = Translations.W(text) - this.onClick(onclick) - } - - protected InnerConstructElement(): HTMLElement { - const el = this._text.ConstructElement() - if (el === undefined) { - return undefined - } - const form = document.createElement("form") - const button = document.createElement("button") - button.type = "button" - button.appendChild(el) - form.appendChild(button) - return form - } -} diff --git a/src/UI/Base/DivContainer.ts b/src/UI/Base/DivContainer.ts deleted file mode 100644 index f36b041d62..0000000000 --- a/src/UI/Base/DivContainer.ts +++ /dev/null @@ -1,19 +0,0 @@ -import BaseUIElement from "../BaseUIElement" - -/** - * Introduces a new element which has an ID - * Mostly a workaround for the import viewer - */ -export default class DivContainer extends BaseUIElement { - private readonly _id: string - - constructor(id: string) { - super() - this._id = id - } - protected InnerConstructElement(): HTMLElement { - const e = document.createElement("div") - e.id = this._id - return e - } -} diff --git a/src/UI/Base/Hotkeys.ts b/src/UI/Base/Hotkeys.ts index 03dfb1b28a..a7fe3bf822 100644 --- a/src/UI/Base/Hotkeys.ts +++ b/src/UI/Base/Hotkeys.ts @@ -1,12 +1,6 @@ import { Utils } from "../../Utils" -import Combine from "./Combine" -import BaseUIElement from "../BaseUIElement" -import Title from "./Title" -import Table from "./Table" -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import { VariableUiElement } from "./VariableUIElement" +import { UIEventSource } from "../../Logic/UIEventSource" import { Translation } from "../i18n/Translation" -import { FixedUiElement } from "./FixedUiElement" import Translations from "../i18n/Translations" import MarkdownUtils from "../../Utils/MarkdownUtils" import Locale from "../i18n/Locale" diff --git a/src/UI/Base/TableOfContents.ts b/src/UI/Base/TableOfContents.ts index e75ee7b31a..5240d7b444 100644 --- a/src/UI/Base/TableOfContents.ts +++ b/src/UI/Base/TableOfContents.ts @@ -2,7 +2,6 @@ import BaseUIElement from "../BaseUIElement" import List from "./List" import { marked } from "marked" import { parse as parse_html } from "node-html-parser" -import { default as turndown } from "turndown" import { Utils } from "../../Utils" export default class TableOfContents { diff --git a/src/UI/Input/Toggle.ts b/src/UI/Input/Toggle.ts index 85412fb1ee..c37b1c27d8 100644 --- a/src/UI/Input/Toggle.ts +++ b/src/UI/Input/Toggle.ts @@ -1,7 +1,6 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource" import BaseUIElement from "../BaseUIElement" import { VariableUiElement } from "../Base/VariableUIElement" -import Lazy from "../Base/Lazy" /** * The 'Toggle' is a UIElement showing either one of two elements, depending on the state. @@ -19,12 +18,6 @@ export default class Toggle extends VariableUiElement { this.isEnabled = isEnabled } - public static If(condition: Store, constructor: () => BaseUIElement): BaseUIElement { - if (constructor === undefined) { - return undefined - } - return new Toggle(new Lazy(constructor), undefined, condition) - } } /** @@ -42,11 +35,4 @@ export class ClickableToggle extends Toggle { this.isEnabled = isEnabled } - public ToggleOnClick(): ClickableToggle { - const self = this - this.onClick(() => { - self.isEnabled.setData(!self.isEnabled.data) - }) - return this - } } diff --git a/src/UI/i18n/Translations.ts b/src/UI/i18n/Translations.ts index da6b2591da..2006a5e899 100644 --- a/src/UI/i18n/Translations.ts +++ b/src/UI/i18n/Translations.ts @@ -34,12 +34,6 @@ export default class Translations { return s } const v = JSON.stringify(s) - if (v.length > 100) { - const shortened = v.substring(0, 100) + "..." - return new ClickableToggle(v, shortened) - .ToggleOnClick() - .SetClass("literal-code button") - } return new FixedUiElement(v).SetClass("literal-code") } return s From 13d00608d5687dca387abea72dd0b6b0f8b7c01b Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 1 Aug 2024 20:44:37 +0200 Subject: [PATCH 04/53] UX: improve 'move wizard' layout --- src/UI/Map/Icon.svelte | 3 +++ src/UI/Popup/MoveWizard.svelte | 6 +++--- src/UI/Popup/MoveWizardState.ts | 6 +++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/UI/Map/Icon.svelte b/src/UI/Map/Icon.svelte index c199abe930..c775e1c312 100644 --- a/src/UI/Map/Icon.svelte +++ b/src/UI/Map/Icon.svelte @@ -38,6 +38,7 @@ import { Utils } from "../../Utils" import Gear from "../../assets/svg/Gear.svelte" import { DesktopComputerIcon } from "@rgossiaux/svelte-heroicons/solid" + import Relocation from "../../assets/svg/Relocation.svelte" /** * Renders a single icon. @@ -139,6 +140,8 @@ {:else if icon === "computer"} + {:else if icon === "relocation"} + {:else if Utils.isEmoji(icon)} {icon} diff --git a/src/UI/Popup/MoveWizard.svelte b/src/UI/Popup/MoveWizard.svelte index 006015c5fd..ad2a7a0737 100644 --- a/src/UI/Popup/MoveWizard.svelte +++ b/src/UI/Popup/MoveWizard.svelte @@ -23,6 +23,7 @@ import BackButton from "../Base/BackButton.svelte" import ChevronLeft from "@babeard/svelte-heroicons/solid/ChevronLeft" import ThemeViewState from "../../Models/ThemeViewState" + import Icon from "../Map/Icon.svelte" export let state: ThemeViewState @@ -47,7 +48,7 @@ location: new UIEventSource({ lon, lat }), minzoom: new UIEventSource($reason.minZoom), rasterLayer: state.mapProperties.rasterLayer, - zoom: new UIEventSource($reason?.startZoom ?? 16), + zoom: new UIEventSource($reason?.startZoom ?? 16) } } @@ -84,7 +85,6 @@ {#if currentStep === "reason" && moveWizardState.reasons.length > 1} - {#each moveWizardState.reasons as reasonSpec} {/each} diff --git a/src/UI/Popup/MoveWizardState.ts b/src/UI/Popup/MoveWizardState.ts index 565b89a116..73f0d5a036 100644 --- a/src/UI/Popup/MoveWizardState.ts +++ b/src/UI/Popup/MoveWizardState.ts @@ -16,7 +16,7 @@ import Location from "../../assets/svg/Location.svelte" export interface MoveReason { text: Translation | string invitingText: Translation | string - icon: BaseUIElement + icon: string changesetCommentValue: string lockBounds: true | boolean includeSearch: false | boolean @@ -48,7 +48,7 @@ export class MoveWizardState { reasons.push({ text: t.reasons.reasonRelocation, invitingText: t.inviteToMove.reasonRelocation, - icon: new SvelteUIElement(Relocation), + icon: "relocation", changesetCommentValue: "relocated", lockBounds: false, background: undefined, @@ -62,7 +62,7 @@ export class MoveWizardState { reasons.push({ text: t.reasons.reasonInaccurate, invitingText: t.inviteToMove.reasonInaccurate, - icon: new SvelteUIElement(Location), + icon: "location", changesetCommentValue: "improve_accuracy", lockBounds: true, includeSearch: false, From 2e7703c8eca465c12df2a3ff59ed29e6ab1b126b Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 2 Aug 2024 13:31:45 +0200 Subject: [PATCH 05/53] UX: add indicicator in settings of pending changes, add button to clear the selected changes --- assets/layers/usersettings/usersettings.json | 6 +++ langs/en.json | 1 + src/Logic/Tags/TagUtils.ts | 10 ++++- .../PendingChangesIndicator.svelte | 37 +++++++++++++++++-- src/UI/SpecialVisualizations.ts | 12 +++++- 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json index 1abbc1cae9..96cf0e5401 100644 --- a/assets/layers/usersettings/usersettings.json +++ b/assets/layers/usersettings/usersettings.json @@ -901,6 +901,12 @@ } ] }, + { + "id": "pending_changes", + "render": { + "*": "{pending_changes()}" + } + }, { "id": "show_debug", "question": { diff --git a/langs/en.json b/langs/en.json index 01f9971b9f..aa99604c72 100644 --- a/langs/en.json +++ b/langs/en.json @@ -213,6 +213,7 @@ "backgroundMap": "Select a background layer", "backgroundSwitch": "Switch background", "cancel": "Cancel", + "clearPendingChanges": "Clear pending changes", "confirm": "Confirm", "customThemeIntro": "These are previously visited user-generated themes.", "customThemeTitle": "Custom themes", diff --git a/src/Logic/Tags/TagUtils.ts b/src/Logic/Tags/TagUtils.ts index 639c0abd8e..bddb172952 100644 --- a/src/Logic/Tags/TagUtils.ts +++ b/src/Logic/Tags/TagUtils.ts @@ -218,7 +218,7 @@ export class TagUtils { * * TagUtils.KVtoProperties([new Tag("a","b"), new Tag("c","d")] // => {a: "b", c: "d"} */ - static KVtoProperties(tags: Tag[]): Record { + static KVtoProperties(tags: {key: string, value: string}[]): Record { const properties: Record = {} for (const tag of tags) { properties[tag.key] = tag.value @@ -226,6 +226,14 @@ export class TagUtils { return properties } + static KVObjtoProperties(tags: {k: string, v: string}[]): Record { + const properties: Record = {} + for (const tag of tags) { + properties[tag.k] = tag.v + } + return properties + } + static changeAsProperties(kvs: { k: string; v: string }[]): Record { const tags: Record = {} for (const kv of kvs) { diff --git a/src/UI/BigComponents/PendingChangesIndicator.svelte b/src/UI/BigComponents/PendingChangesIndicator.svelte index a9b37435f7..1c27491903 100644 --- a/src/UI/BigComponents/PendingChangesIndicator.svelte +++ b/src/UI/BigComponents/PendingChangesIndicator.svelte @@ -5,13 +5,15 @@ import Loading from "../Base/Loading.svelte" import Translations from "../i18n/Translations" import Tr from "../Base/Tr.svelte" + import { TagUtils } from "../../Logic/Tags/TagUtils" export let state: SpecialVisualizationState + export let compact: boolean = true const changes: Changes = state.changes const isUploading: Store = changes.isUploading - const pendingChangesCount: Store = changes.pendingChanges.map((ls) => ls.length) const errors = changes.errors + const pending = changes.pendingChanges
- {:else if $pendingChangesCount === 1} + {:else if $pending.length === 1} - {:else if $pendingChangesCount > 1} + {:else if $pending.length > 1} {/if} {#each $errors as error} {/each} + + {#if !compact && $pending.length > 0} + + +
    + + {#each $pending as pending} +
  • + + {#if pending.changes !== undefined} + Create {pending.type}/{pending.id} {JSON.stringify(TagUtils.KVObjtoProperties(pending.tags))} + {:else} + Modify {pending.type}/{pending.id} {JSON.stringify(pending.tags)} + {/if} + {#if pending.type === "way" && pending.changes?.nodes} + {pending.changes.nodes.join(" ")} + {/if} + +
  • + {/each} +
+ + {/if} + +
diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index 47629cd1cc..6573665ac8 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -99,6 +99,7 @@ import NothingKnown from "./Popup/NothingKnown.svelte" import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" import { And } from "../Logic/Tags/And" import CloseNoteButton from "./Popup/Notes/CloseNoteButton.svelte" +import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte" class NearbyImageVis implements SpecialVisualization { // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests @@ -118,7 +119,6 @@ class NearbyImageVis implements SpecialVisualization { "A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature" funcName = "nearby_images" needsUrls = CombinedFetcher.apiUrls - svelteBased = true constr( state: SpecialVisualizationState, @@ -2014,8 +2014,16 @@ export default class SpecialVisualizations { return mostShadowed?.description ?? matchingPresets[0]?.description }) return new VariableUiElement(translation) - }, + } }, + { + funcName:"pending_changes", + docs: "A module showing the pending changes, with the option to clear the pending changes", + args:[], + constr(state: SpecialVisualizationState, tagSource: UIEventSource>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement { + return new SvelteUIElement(PendingChangesIndicator, {state, compact: false}) + } + } ] specialVisualizations.push(new AutoApplyButton(specialVisualizations)) From 7b95303e766155247ec36d8290c090f9e229478c Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 2 Aug 2024 13:32:47 +0200 Subject: [PATCH 06/53] Add feature switch to disable the cache, partial fix to make GRB theme workable again --- assets/themes/grb/grb.json | 3 +- .../Actors/SelectedElementTagsUpdater.ts | 2 +- .../Sources/ChangeGeometryApplicator.ts | 9 +---- .../FeatureSource/Sources/LayoutSource.ts | 36 ++++++++++--------- src/Logic/State/FeatureSwitchState.ts | 9 +++++ .../ThemeConfig/Json/LayoutConfigJson.ts | 10 ++++++ src/Models/ThemeConfig/LayoutConfig.ts | 2 ++ src/Models/ThemeViewState.ts | 7 ++-- 8 files changed, 47 insertions(+), 31 deletions(-) diff --git a/assets/themes/grb/grb.json b/assets/themes/grb/grb.json index 52dad43ab8..7cbace46a4 100644 --- a/assets/themes/grb/grb.json +++ b/assets/themes/grb/grb.json @@ -787,5 +787,6 @@ }, "overpassMaxZoom": 15, "osmApiTileSize": 17, - "widenFactor": 2 + "widenFactor": 2, + "enableCache": false } diff --git a/src/Logic/Actors/SelectedElementTagsUpdater.ts b/src/Logic/Actors/SelectedElementTagsUpdater.ts index 9789ba6a77..9472d29b31 100644 --- a/src/Logic/Actors/SelectedElementTagsUpdater.ts +++ b/src/Logic/Actors/SelectedElementTagsUpdater.ts @@ -112,7 +112,7 @@ export default class SelectedElementTagsUpdater { private invalidateCache(s: Feature) { const state = this.state const wasPartOfLayer = state.layout.getMatchingLayer(s.properties) - state.toCacheSavers.get(wasPartOfLayer.id).invalidateCacheAround(BBox.get(s)) + state.toCacheSavers?.get(wasPartOfLayer.id)?.invalidateCacheAround(BBox.get(s)) } private installCallback() { const state = this.state diff --git a/src/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts b/src/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts index 9bed9e8421..871c865578 100644 --- a/src/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts +++ b/src/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts @@ -75,14 +75,7 @@ export default class ChangeGeometryApplicator implements FeatureSource { newFeatures.push(feature) continue } - console.log( - "Applying a geometry change onto:", - feature, - "The change is:", - change, - "which becomes:", - copy - ) + newFeatures.push(copy) } this.features.setData(newFeatures) diff --git a/src/Logic/FeatureSource/Sources/LayoutSource.ts b/src/Logic/FeatureSource/Sources/LayoutSource.ts index 601c752530..9cfa8ed856 100644 --- a/src/Logic/FeatureSource/Sources/LayoutSource.ts +++ b/src/Logic/FeatureSource/Sources/LayoutSource.ts @@ -27,6 +27,7 @@ export default class LayoutSource extends FeatureSourceMerger { private readonly supportsForceDownload: UpdatableFeatureSource[] public static readonly fromCacheZoomLevel = 15 + constructor( layers: LayerConfig[], featureSwitches: FeatureSwitchState, @@ -45,20 +46,22 @@ export default class LayoutSource extends FeatureSourceMerger { const geojsonlayers = layers.filter((layer) => layer.source.geojsonSource !== undefined) const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined) const fromCache = new Map() - for (const layer of osmLayers) { - const src = new LocalStorageFeatureSource( - backend, - layer, - LayoutSource.fromCacheZoomLevel, - mapProperties, - { - isActive: isDisplayed(layer.id), - maxAge: layer.maxAgeOfCache, - } - ) - fromCache.set(layer.id, src) - } + if (featureSwitches.featureSwitchCache.data) { + for (const layer of osmLayers) { + const src = new LocalStorageFeatureSource( + backend, + layer, + LayoutSource.fromCacheZoomLevel, + mapProperties, + { + isActive: isDisplayed(layer.id), + maxAge: layer.maxAgeOfCache + } + ) + fromCache.set(layer.id, src) + } + } const mvtSources: UpdatableFeatureSource[] = osmLayers .filter((f) => mvtAvailableLayers.has(f.id)) .map((l) => LayoutSource.setupMvtSource(l, mapProperties, isDisplayed(l.id))) @@ -104,7 +107,6 @@ export default class LayoutSource extends FeatureSourceMerger { super(...geojsonSources, ...Array.from(fromCache.values()), ...mvtSources, ...nonMvtSources) this.isLoading = isLoading - this.fromCache = fromCache supportsForceDownload.push(...geojsonSources) supportsForceDownload.push(...mvtSources) // Non-mvt sources are handled by overpass this.supportsForceDownload = supportsForceDownload @@ -168,7 +170,7 @@ export default class LayoutSource extends FeatureSourceMerger { backend, isActive, patchRelations: true, - fullNodeDatabase, + fullNodeDatabase }) } @@ -200,11 +202,11 @@ export default class LayoutSource extends FeatureSourceMerger { widenFactor: featureSwitches.layoutToUse.widenFactor, overpassUrl: featureSwitches.overpassUrl, overpassTimeout: featureSwitches.overpassTimeout, - overpassMaxZoom: featureSwitches.overpassMaxZoom, + overpassMaxZoom: featureSwitches.overpassMaxZoom }, { padToTiles: zoom.map((zoom) => Math.min(15, zoom + 1)), - isActive, + isActive } ) } diff --git a/src/Logic/State/FeatureSwitchState.ts b/src/Logic/State/FeatureSwitchState.ts index 0487ff54b9..3c2a936c2f 100644 --- a/src/Logic/State/FeatureSwitchState.ts +++ b/src/Logic/State/FeatureSwitchState.ts @@ -57,6 +57,8 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches { public readonly featureSwitchBackToThemeOverview: UIEventSource public readonly featureSwitchShareScreen: UIEventSource public readonly featureSwitchGeolocation: UIEventSource + public readonly featureSwitchCache: UIEventSource + public readonly featureSwitchIsTesting: UIEventSource public readonly featureSwitchIsDebugging: UIEventSource public readonly featureSwitchShowAllQuestions: UIEventSource @@ -176,6 +178,13 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches { "Enable the export as GeoJSON and CSV button" ) + this.featureSwitchCache = FeatureSwitchUtils.initSwitch( + "fs-cache", + layoutToUse?.enableCache ?? true, + "Enable/disable caching from localStorage" + ) + + let testingDefaultValue = false if ( !Utils.runningFromConsole && diff --git a/src/Models/ThemeConfig/Json/LayoutConfigJson.ts b/src/Models/ThemeConfig/Json/LayoutConfigJson.ts index 748573fb29..107103ad4b 100644 --- a/src/Models/ThemeConfig/Json/LayoutConfigJson.ts +++ b/src/Models/ThemeConfig/Json/LayoutConfigJson.ts @@ -450,4 +450,14 @@ export interface LayoutConfigJson { * iftrue: Do not write 'change_within_x_m' and do not indicate that this was done by survey */ enableMorePrivacy: boolean + /** + * question: Should this theme have the cache enabled? + * + * Should only be dissabled in highly specific cases, such as the GRB-theme + * + * ifunset: Cache is enabled + * iffalse: Do not cache data + * group: hidden + */ + enableCache?: true | boolean } diff --git a/src/Models/ThemeConfig/LayoutConfig.ts b/src/Models/ThemeConfig/LayoutConfig.ts index 936d50f17c..3863938fbb 100644 --- a/src/Models/ThemeConfig/LayoutConfig.ts +++ b/src/Models/ThemeConfig/LayoutConfig.ts @@ -81,6 +81,7 @@ export default class LayoutConfig implements LayoutInformation { private readonly layersDict: Map private readonly source: LayoutConfigJson + public readonly enableCache: boolean constructor( json: LayoutConfigJson, @@ -98,6 +99,7 @@ export default class LayoutConfig implements LayoutInformation { this.id = json.id this.definedAtUrl = options?.definedAtUrl this.definitionRaw = options?.definitionRaw + this.enableCache = json.enableCache ?? true if (official) { if (json.id.toLowerCase() !== json.id) { throw "The id of a theme should be lowercase: " + json.id diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index c0b6d10298..b0f63f503d 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -204,7 +204,6 @@ export default class ThemeViewState implements SpecialVisualizationState { this.osmConnection.isLoggedIn ) - const self = this this.layerState = new LayerState( this.osmConnection, layout.layers, @@ -241,7 +240,7 @@ export default class ThemeViewState implements SpecialVisualizationState { this.featureSwitches, this.mapProperties, this.osmConnection.Backend(), - (id) => self.layerState.filteredLayers.get(id).isDisplayed, + (id) => this.layerState.filteredLayers.get(id).isDisplayed, mvtAvailableLayers, this.fullNodeDatabase ) @@ -316,7 +315,7 @@ export default class ThemeViewState implements SpecialVisualizationState { } const floors = new Set() for (const feature of features) { - let level = feature.properties["_level"] + const level = feature.properties["_level"] if (level) { const levels = level.split(";") for (const l of levels) { @@ -379,7 +378,7 @@ export default class ThemeViewState implements SpecialVisualizationState { this.featureSummary = this.setupSummaryLayer( new LayerConfig(summaryLayer, "summaryLayer", true) ) - this.toCacheSavers = this.initSaveToLocalStorage() + this.toCacheSavers = layout.enableCache ? this.initSaveToLocalStorage() : undefined this.initActors() this.drawSpecialLayers() this.initHotkeys() From e47ec86874afebd4f6efc3d7b6ac835549e5ddad Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 2 Aug 2024 13:33:29 +0200 Subject: [PATCH 07/53] Small fixes to copyright-panel and restoring the state of the menus --- src/Logic/Web/ThemeViewStateHashActor.ts | 39 ++++++++++++---------- src/UI/BigComponents/CopyrightPanel.svelte | 5 ++- src/UI/ThemeViewGUI.svelte | 5 ++- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/Logic/Web/ThemeViewStateHashActor.ts b/src/Logic/Web/ThemeViewStateHashActor.ts index 817d7173a2..250eecd8d7 100644 --- a/src/Logic/Web/ThemeViewStateHashActor.ts +++ b/src/Logic/Web/ThemeViewStateHashActor.ts @@ -17,7 +17,7 @@ export default class ThemeViewStateHashActor { "The possible hashes are:", "", MenuState._menuviewTabs.map((tab) => "`menu:" + tab + "`").join(","), - MenuState._themeviewTabs.map((tab) => "`theme-menu:" + tab + "`").join(","), + MenuState._themeviewTabs.map((tab) => "`theme-menu:" + tab + "`").join(",") ] /** @@ -120,27 +120,30 @@ export default class ThemeViewStateHashActor { private loadStateFromHash(hash: string) { const state = this._state - const parts = hash.split(":") - outer: for (const { toggle, name, submenu } of state.guistate.allToggles) { - for (const part of parts) { - if (part === name) { + for (const superpart of hash.split(";")) { + const parts = superpart.at(-1)?.split(":") ?? [] + + outer: for (const { toggle, name, submenu } of state.guistate.allToggles) { + for (const part of parts) { + if (part.indexOf(":") < 0) { + if (part === name) { + toggle.setData(true) + continue outer + } + continue + } + const [main, submenuValue] = part.split(":") + if (part !== main) { + continue + } toggle.setData(true) + submenu?.setData(submenuValue) continue outer } - if (part.indexOf(":") < 0) { - continue - } - const [main, submenuValue] = part.split(":") - if (part !== main) { - continue - } - toggle.setData(true) - submenu?.setData(submenuValue) - continue outer - } - // If we arrive here, the loop above has not found any match - toggle.setData(false) + // If we arrive here, the loop above has not found any match + toggle.setData(false) + } } } diff --git a/src/UI/BigComponents/CopyrightPanel.svelte b/src/UI/BigComponents/CopyrightPanel.svelte index 6072a56f01..69b4cb2f8f 100644 --- a/src/UI/BigComponents/CopyrightPanel.svelte +++ b/src/UI/BigComponents/CopyrightPanel.svelte @@ -13,11 +13,10 @@ import ContributorCount from "../../Logic/ContributorCount" import BaseUIElement from "../BaseUIElement" import Github from "../../assets/svg/Github.svelte" - import { DatabaseIcon, TranslateIcon } from "@rgossiaux/svelte-heroicons/solid" + import { TranslateIcon } from "@rgossiaux/svelte-heroicons/solid" import Osm_logo from "../../assets/svg/Osm_logo.svelte" import Generic_map from "../../assets/svg/Generic_map.svelte" - import { PencilIcon, UserGroupIcon, UsersIcon } from "@babeard/svelte-heroicons/solid" - import Loading from "../Base/Loading.svelte" + import { UserGroupIcon} from "@babeard/svelte-heroicons/solid" import Marker from "../Map/Marker.svelte" export let state: SpecialVisualizationState diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 0b3528baee..d36450093e 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -46,7 +46,6 @@ import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte" import Cross from "../assets/svg/Cross.svelte" import LanguagePicker from "./InputElement/LanguagePicker.svelte" - import Bug from "../assets/svg/Bug.svelte" import Min from "../assets/svg/Min.svelte" import Plus from "../assets/svg/Plus.svelte" import Filter from "../assets/svg/Filter.svelte" @@ -645,7 +644,7 @@ - state.guistate.privacyPanelIsOpened.setData(false)}> + state.guistate.copyrightPanelIsOpened.setData(false)}>

@@ -655,7 +654,7 @@

- new CopyrightPanel(state)} /> +
From e68edeb29fa604daee0c7638999bcebab2fd7b11 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 2 Aug 2024 13:52:45 +0200 Subject: [PATCH 08/53] Themes: ghost signs: add wall-based artworks to be able to mark them as ghost sign and to avoid duplicates --- assets/themes/ghostsigns/ghostsigns.json | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/assets/themes/ghostsigns/ghostsigns.json b/assets/themes/ghostsigns/ghostsigns.json index 4ac461cc08..e58696d193 100644 --- a/assets/themes/ghostsigns/ghostsigns.json +++ b/assets/themes/ghostsigns/ghostsigns.json @@ -216,6 +216,56 @@ ], "isCounted": false } + }, + { + "builtin": "artwork", + "override": { + "minzoom": 16, + "presets=": null, + "id": "artwork_on_wall", + "+tagRenderings": [ + { + "id": "historic_or_not", + "question": { + "en": "Is this artwork a historic advertisement?" + }, + "mappings": [ + { + "if": "historic=advertising", + "addExtraTags": [ + "advertising=wall_painting" + ], + "then": { + "en": "This artwork is a historic advertisement" + } + }, + { + "if": "historic=", + "addExtraTags": [ + "advertising=" + ], + "then": { + "en": "This artwork is not a historic advertisement" + } + } + ] + } + ], + "source": { + "osmTags": { + "and+": { + "or": [ + "artwork_type=mural", + "artwork_type=graffiti", + "artwork_type=mosaic", + "artwork_type=relief", + "artwork_type=painting", + "artwork_type=mural_painting" + ] + } + } + } + } } ] } From d16418de918a490cdb9909887d22185e06822da2 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 2 Aug 2024 13:52:56 +0200 Subject: [PATCH 09/53] Fix move dialog --- src/UI/Popup/MoveWizard.svelte | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/UI/Popup/MoveWizard.svelte b/src/UI/Popup/MoveWizard.svelte index ad2a7a0737..b1b086506c 100644 --- a/src/UI/Popup/MoveWizard.svelte +++ b/src/UI/Popup/MoveWizard.svelte @@ -3,13 +3,10 @@ import type { MoveReason } from "./MoveWizardState" import { MoveWizardState } from "./MoveWizardState" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" - import ToSvelte from "../Base/ToSvelte.svelte" import Tr from "../Base/Tr.svelte" import Translations from "../i18n/Translations" import Move from "../../assets/svg/Move.svelte" import Move_not_allowed from "../../assets/svg/Move_not_allowed.svelte" - import type { SpecialVisualizationState } from "../SpecialVisualization" - import { XCircleIcon } from "@babeard/svelte-heroicons/solid" import type { MapProperties } from "../../Models/MapProperties" import type { Feature, Point } from "geojson" import { GeoOperations } from "../../Logic/GeoOperations" @@ -74,9 +71,7 @@ {#if moveWizardState.reasons.length === 1} - + {:else} From 452ba7d61a3dd8deaff0f85ac0ebeaeb080d39d3 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 2 Aug 2024 13:53:24 +0200 Subject: [PATCH 10/53] Chore: remove import and console log --- src/Logic/MetaTagging.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Logic/MetaTagging.ts b/src/Logic/MetaTagging.ts index b95c57b842..55f78d96cc 100644 --- a/src/Logic/MetaTagging.ts +++ b/src/Logic/MetaTagging.ts @@ -9,7 +9,6 @@ import { IndexedFeatureSource } from "./FeatureSource/FeatureSource" import OsmObjectDownloader from "./Osm/OsmObjectDownloader" import { Utils } from "../Utils" import { Store, UIEventSource } from "./UIEventSource" -import { selectDefault } from "../Utils/selectDefault" /** * Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ... @@ -130,7 +129,7 @@ export default class MetaTagging { state.featureProperties, { includeDates: !lightUpdate, - evaluateStrict: !lightUpdate, + evaluateStrict: !lightUpdate } ) } @@ -281,7 +280,9 @@ export default class MetaTagging { atLeastOneFeatureChanged = true } } - console.debug("Strictly evaluated ", strictlyEvaluated, " values") // Do not remove this + if (strictlyEvaluated > 0) { + console.debug("Strictly evaluated ", strictlyEvaluated, " values") // Do not remove this + } return atLeastOneFeatureChanged } @@ -304,7 +305,7 @@ export default class MetaTagging { return [] } return [state.perLayer.get(layerId).GetFeaturesWithin(bbox)] - }, + } } } @@ -349,8 +350,8 @@ export default class MetaTagging { if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) { console.warn( "Could not calculate a " + - (isStrict ? "strict " : "") + - "calculated tag for key", + (isStrict ? "strict " : "") + + "calculated tag for key", key, "for feature", feat.properties.id, @@ -358,9 +359,9 @@ export default class MetaTagging { code, "(in layer", layerId + - ") due to \n" + - e + - "\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", + ") due to \n" + + e + + "\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", e, e.stack, { feat } From 3dc0014f352ac26b26bf43c7ebf991c897428b77 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 2 Aug 2024 19:05:16 +0200 Subject: [PATCH 11/53] Chore: regenerate schemas --- Docs/Schemas/LayoutConfigJson.schema.json | 4 +- Docs/Schemas/LayoutConfigJsonJSC.ts | 4 +- src/assets/schemas/layoutconfigmeta.json | 222 +++++++++++++++++++++- 3 files changed, 224 insertions(+), 6 deletions(-) diff --git a/Docs/Schemas/LayoutConfigJson.schema.json b/Docs/Schemas/LayoutConfigJson.schema.json index 2611927722..f8fbe6486e 100644 --- a/Docs/Schemas/LayoutConfigJson.schema.json +++ b/Docs/Schemas/LayoutConfigJson.schema.json @@ -58,7 +58,7 @@ ] }, "icon": { - "description": "question: What icon should be used to represent this theme?\n\nUsed as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...\n\nEither a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)\n\nType: icon\ngroup: basic", + "description": "question: What icon should be used to represent this theme?\n\nUsed as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...\n\nEither a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)\n\nType: icon\nsuggestions: return Constants.defaultPinIcons.map(i => ({if: \"value=\"+i, then: i, icon: i}))\ngroup: basic", "type": "string" }, "socialImage": { @@ -104,7 +104,7 @@ "type": "boolean" }, "layers": { - "description": "question: What layers should this map show?\ntype: layer[]\ntypes: hidden | layer | hidden\ngroup: layers\nsuggestions: return Array.from(layers.keys()).map(key => ({if: \"value=\"+key, then: \"\"+key+\" (builtin) - \"+layers.get(key).description}))\n\nA theme must contain at least one layer.\n\nA layer contains all features of a single type, for example \"shops\", \"bicycle pumps\", \"benches\".\nNote that every layer contains a specification of attributes that it should match. MapComplete will fetch the relevant data from either overpass, the OSM-API or the cache server.\nIf a feature can match multiple layers, the first matching layer in the list will be used.\nThis implies that the _order_ of the layers is important.\n\n\n
\nNote that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: \"layername\", override: ...}\n\nThe 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer\n\nFor example: If you would like to use layer nature reserves, but only from a specific operator (eg. Natuurpunt) you would use the following in your theme:\n\n```\n\"layer\": {\n \"builtin\": \"nature_reserve\",\n \"override\": {\"source\":\n {\"osmTags\": {\n \"+and\":[\"operator=Natuurpunt\"]\n }\n }\n }\n}\n```\n\nIt's also possible to load multiple layers at once, for example, if you would like for both drinking water and benches to start at the zoomlevel at 12, you would use the following:\n\n```\n\"layer\": {\n \"builtin\": [\"benches\", \"drinking_water\"],\n \"override\": {\"minzoom\": 12}\n}\n```\n
", + "description": "question: What layers should this map show?\ntype: layer[]\ntypes: hidden | layer | hidden\ngroup: layers\ntitle: value[\"builtin\"] ?? value[\"id\"] ?? value\nsuggestions: return Array.from(layers.keys()).map(key => ({if: \"value=\"+key, then: \"\"+key+\" (builtin) - \"+layers.get(key).description}))\n\nA theme must contain at least one layer.\n\nA layer contains all features of a single type, for example \"shops\", \"bicycle pumps\", \"benches\".\nNote that every layer contains a specification of attributes that it should match. MapComplete will fetch the relevant data from either overpass, the OSM-API or the cache server.\nIf a feature can match multiple layers, the first matching layer in the list will be used.\nThis implies that the _order_ of the layers is important.\n\n\n
\nNote that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: \"layername\", override: ...}\n\nThe 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer\n\nFor example: If you would like to use layer nature reserves, but only from a specific operator (eg. Natuurpunt) you would use the following in your theme:\n\n```\n\"layer\": {\n \"builtin\": \"nature_reserve\",\n \"override\": {\"source\":\n {\"osmTags\": {\n \"+and\":[\"operator=Natuurpunt\"]\n }\n }\n }\n}\n```\n\nIt's also possible to load multiple layers at once, for example, if you would like for both drinking water and benches to start at the zoomlevel at 12, you would use the following:\n\n```\n\"layer\": {\n \"builtin\": [\"benches\", \"drinking_water\"],\n \"override\": {\"minzoom\": 12}\n}\n```\n
", "type": "array", "items": { "anyOf": [ diff --git a/Docs/Schemas/LayoutConfigJsonJSC.ts b/Docs/Schemas/LayoutConfigJsonJSC.ts index 22751c5539..968898b2d4 100644 --- a/Docs/Schemas/LayoutConfigJsonJSC.ts +++ b/Docs/Schemas/LayoutConfigJsonJSC.ts @@ -58,7 +58,7 @@ export default { ] }, "icon": { - "description": "question: What icon should be used to represent this theme?\n\nUsed as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...\n\nEither a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)\n\nType: icon\ngroup: basic", + "description": "question: What icon should be used to represent this theme?\n\nUsed as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...\n\nEither a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)\n\nType: icon\nsuggestions: return Constants.defaultPinIcons.map(i => ({if: \"value=\"+i, then: i, icon: i}))\ngroup: basic", "type": "string" }, "socialImage": { @@ -104,7 +104,7 @@ export default { "type": "boolean" }, "layers": { - "description": "question: What layers should this map show?\ntype: layer[]\ntypes: hidden | layer | hidden\ngroup: layers\nsuggestions: return Array.from(layers.keys()).map(key => ({if: \"value=\"+key, then: \"\"+key+\" (builtin) - \"+layers.get(key).description}))\n\nA theme must contain at least one layer.\n\nA layer contains all features of a single type, for example \"shops\", \"bicycle pumps\", \"benches\".\nNote that every layer contains a specification of attributes that it should match. MapComplete will fetch the relevant data from either overpass, the OSM-API or the cache server.\nIf a feature can match multiple layers, the first matching layer in the list will be used.\nThis implies that the _order_ of the layers is important.\n\n\n
\nNote that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: \"layername\", override: ...}\n\nThe 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer\n\nFor example: If you would like to use layer nature reserves, but only from a specific operator (eg. Natuurpunt) you would use the following in your theme:\n\n```\n\"layer\": {\n \"builtin\": \"nature_reserve\",\n \"override\": {\"source\":\n {\"osmTags\": {\n \"+and\":[\"operator=Natuurpunt\"]\n }\n }\n }\n}\n```\n\nIt's also possible to load multiple layers at once, for example, if you would like for both drinking water and benches to start at the zoomlevel at 12, you would use the following:\n\n```\n\"layer\": {\n \"builtin\": [\"benches\", \"drinking_water\"],\n \"override\": {\"minzoom\": 12}\n}\n```\n
", + "description": "question: What layers should this map show?\ntype: layer[]\ntypes: hidden | layer | hidden\ngroup: layers\ntitle: value[\"builtin\"] ?? value[\"id\"] ?? value\nsuggestions: return Array.from(layers.keys()).map(key => ({if: \"value=\"+key, then: \"\"+key+\" (builtin) - \"+layers.get(key).description}))\n\nA theme must contain at least one layer.\n\nA layer contains all features of a single type, for example \"shops\", \"bicycle pumps\", \"benches\".\nNote that every layer contains a specification of attributes that it should match. MapComplete will fetch the relevant data from either overpass, the OSM-API or the cache server.\nIf a feature can match multiple layers, the first matching layer in the list will be used.\nThis implies that the _order_ of the layers is important.\n\n\n
\nNote that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: \"layername\", override: ...}\n\nThe 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer\n\nFor example: If you would like to use layer nature reserves, but only from a specific operator (eg. Natuurpunt) you would use the following in your theme:\n\n```\n\"layer\": {\n \"builtin\": \"nature_reserve\",\n \"override\": {\"source\":\n {\"osmTags\": {\n \"+and\":[\"operator=Natuurpunt\"]\n }\n }\n }\n}\n```\n\nIt's also possible to load multiple layers at once, for example, if you would like for both drinking water and benches to start at the zoomlevel at 12, you would use the following:\n\n```\n\"layer\": {\n \"builtin\": [\"benches\", \"drinking_water\"],\n \"override\": {\"minzoom\": 12}\n}\n```\n
", "type": "array", "items": { "anyOf": [ diff --git a/src/assets/schemas/layoutconfigmeta.json b/src/assets/schemas/layoutconfigmeta.json index 1e17242d3b..04fbd31b8d 100644 --- a/src/assets/schemas/layoutconfigmeta.json +++ b/src/assets/schemas/layoutconfigmeta.json @@ -110,7 +110,224 @@ "hints": { "typehint": "icon", "group": "basic", - "question": "What icon should be used to represent this theme?" + "question": "What icon should be used to represent this theme?", + "suggestions": [ + { + "if": "value=addSmall", + "then": "addSmall", + "icon": "addSmall" + }, + { + "if": "value=brick_wall_round", + "then": "brick_wall_round", + "icon": "brick_wall_round" + }, + { + "if": "value=brick_wall_square", + "then": "brick_wall_square", + "icon": "brick_wall_square" + }, + { + "if": "value=bug", + "then": "bug", + "icon": "bug" + }, + { + "if": "value=checkmark", + "then": "checkmark", + "icon": "checkmark" + }, + { + "if": "value=checkmark", + "then": "checkmark", + "icon": "checkmark" + }, + { + "if": "value=circle", + "then": "circle", + "icon": "circle" + }, + { + "if": "value=clock", + "then": "clock", + "icon": "clock" + }, + { + "if": "value=close", + "then": "close", + "icon": "close" + }, + { + "if": "value=close", + "then": "close", + "icon": "close" + }, + { + "if": "value=confirm", + "then": "confirm", + "icon": "confirm" + }, + { + "if": "value=computer", + "then": "computer", + "icon": "computer" + }, + { + "if": "value=cross_bottom_right", + "then": "cross_bottom_right", + "icon": "cross_bottom_right" + }, + { + "if": "value=crosshair", + "then": "crosshair", + "icon": "crosshair" + }, + { + "if": "value=desktop", + "then": "desktop", + "icon": "desktop" + }, + { + "if": "value=direction", + "then": "direction", + "icon": "direction" + }, + { + "if": "value=gear", + "then": "gear", + "icon": "gear" + }, + { + "if": "value=gps_arrow", + "then": "gps_arrow", + "icon": "gps_arrow" + }, + { + "if": "value=heart", + "then": "heart", + "icon": "heart" + }, + { + "if": "value=heart_outline", + "then": "heart_outline", + "icon": "heart_outline" + }, + { + "if": "value=help", + "then": "help", + "icon": "help" + }, + { + "if": "value=help", + "then": "help", + "icon": "help" + }, + { + "if": "value=home", + "then": "home", + "icon": "home" + }, + { + "if": "value=invalid", + "then": "invalid", + "icon": "invalid" + }, + { + "if": "value=invalid", + "then": "invalid", + "icon": "invalid" + }, + { + "if": "value=link", + "then": "link", + "icon": "link" + }, + { + "if": "value=location", + "then": "location", + "icon": "location" + }, + { + "if": "value=location_empty", + "then": "location_empty", + "icon": "location_empty" + }, + { + "if": "value=location_locked", + "then": "location_locked", + "icon": "location_locked" + }, + { + "if": "value=mastodon", + "then": "mastodon", + "icon": "mastodon" + }, + { + "if": "value=not_found", + "then": "not_found", + "icon": "not_found" + }, + { + "if": "value=note", + "then": "note", + "icon": "note" + }, + { + "if": "value=party", + "then": "party", + "icon": "party" + }, + { + "if": "value=pin", + "then": "pin", + "icon": "pin" + }, + { + "if": "value=resolved", + "then": "resolved", + "icon": "resolved" + }, + { + "if": "value=ring", + "then": "ring", + "icon": "ring" + }, + { + "if": "value=scissors", + "then": "scissors", + "icon": "scissors" + }, + { + "if": "value=square", + "then": "square", + "icon": "square" + }, + { + "if": "value=square_rounded", + "then": "square_rounded", + "icon": "square_rounded" + }, + { + "if": "value=teardrop", + "then": "teardrop", + "icon": "teardrop" + }, + { + "if": "value=teardrop_with_hole_green", + "then": "teardrop_with_hole_green", + "icon": "teardrop_with_hole_green" + }, + { + "if": "value=triangle", + "then": "triangle", + "icon": "triangle" + }, + { + "if": "value=wifi", + "then": "wifi", + "icon": "wifi" + } + ] }, "type": "string", "description": "Used as logo in the more-screen and (for official themes) as favicon, webmanifest logo, ...\nEither a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)" @@ -964,7 +1181,8 @@ "if": "value=windturbine", "then": "windturbine (builtin) - Modern windmills generating electricity" } - ] + ], + "title": "value[\"builtin\"] ?? value[\"id\"] ?? value" }, "type": [ { From 5d1c93396d5a3935102182f060395e608bf17276 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 2 Aug 2024 19:06:14 +0200 Subject: [PATCH 12/53] Add various improvements and fixes to studio, should fix #2055 --- public/css/index-tailwind-output.css | 18 +- src/Logic/State/UserSettingsMetaTagging.ts | 48 +-- .../ThemeConfig/Conversion/Conversion.ts | 7 +- .../ThemeConfig/Conversion/PrepareTheme.ts | 2 +- .../ThemeConfig/Conversion/Validation.ts | 310 +++++++++--------- .../ThemeConfig/Json/LayoutConfigJson.ts | 2 + src/UI/Flowbite/AccordionSingle.svelte | 2 +- .../TagRendering/TagRenderingQuestion.svelte | 3 - .../CollapsedTagRenderingPreview.svelte | 207 ++++++++++++ src/UI/Studio/EditLayerState.ts | 49 +-- src/UI/Studio/QuestionPreview.svelte | 47 ++- src/UI/Studio/RawEditor.svelte | 46 ++- src/UI/Studio/Region.svelte | 7 +- src/UI/Studio/SchemaBasedArray.svelte | 171 +--------- src/UI/Studio/SchemaBasedField.svelte | 4 +- src/UI/Studio/SchemaBasedInput.svelte | 7 +- src/UI/Studio/SchemaBasedMultiType.svelte | 4 +- src/UI/StudioGUI.svelte | 4 +- src/index.css | 11 + 19 files changed, 531 insertions(+), 418 deletions(-) create mode 100644 src/UI/Studio/CollapsedTagRenderingPreview.svelte diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 5666d26c7a..4ff2caa99f 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -993,10 +993,6 @@ video { margin-right: 4rem; } -.mb-4 { - margin-bottom: 1rem; -} - .mt-4 { margin-top: 1rem; } @@ -1029,6 +1025,10 @@ video { margin-right: 0.25rem; } +.mb-4 { + margin-bottom: 1rem; +} + .ml-1 { margin-left: 0.25rem; } @@ -4686,6 +4686,16 @@ textarea { color: black; } +h2.group { + /* For flowbite accordions */ + margin: 0; +} + +.group button { + /* For flowbite accordions */ + border-radius: 0; +} + /************************* OTHER CATEGORIES ********************************/ /** diff --git a/src/Logic/State/UserSettingsMetaTagging.ts b/src/Logic/State/UserSettingsMetaTagging.ts index 6e568c5c32..33a5ae85b5 100644 --- a/src/Logic/State/UserSettingsMetaTagging.ts +++ b/src/Logic/State/UserSettingsMetaTagging.ts @@ -1,42 +1,14 @@ import { Utils } from "../../Utils" /** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */ export class ThemeMetaTagging { - public static readonly themeName = "usersettings" + public static readonly themeName = "usersettings" - public metaTaggging_for_usersettings(feat: { properties: Record }) { - Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => - feat.properties._description - .match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) - ?.at(1) - ) - Utils.AddLazyProperty( - feat.properties, - "_d", - () => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? "" - ) - Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () => - ((feat) => { - const e = document.createElement("div") - e.innerHTML = feat.properties._d - return Array.from(e.getElementsByTagName("a")).filter( - (a) => a.href.match(/mastodon|en.osm.town/) !== null - )[0]?.href - })(feat) - ) - Utils.AddLazyProperty(feat.properties, "_mastodon_link", () => - ((feat) => { - const e = document.createElement("div") - e.innerHTML = feat.properties._d - return Array.from(e.getElementsByTagName("a")).filter( - (a) => a.getAttribute("rel")?.indexOf("me") >= 0 - )[0]?.href - })(feat) - ) - Utils.AddLazyProperty( - feat.properties, - "_mastodon_candidate", - () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a - ) - feat.properties["__current_backgroun"] = "initial_value" - } -} + public metaTaggging_for_usersettings(feat: {properties: Record}) { + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) ) + Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' ) + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) ) + Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) ) + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a ) + feat.properties['__current_backgroun'] = 'initial_value' + } +} \ No newline at end of file diff --git a/src/Models/ThemeConfig/Conversion/Conversion.ts b/src/Models/ThemeConfig/Conversion/Conversion.ts index 1c8dc641c5..9285659cdd 100644 --- a/src/Models/ThemeConfig/Conversion/Conversion.ts +++ b/src/Models/ThemeConfig/Conversion/Conversion.ts @@ -73,15 +73,20 @@ export abstract class DesugaringStep extends Conversion {} export class Pipe extends Conversion { private readonly _step0: Conversion private readonly _step1: Conversion + private readonly _failfast: boolean - constructor(step0: Conversion, step1: Conversion) { + constructor(step0: Conversion, step1: Conversion, failfast = false) { super("Merges two steps with different types", [], `Pipe(${step0.name}, ${step1.name})`) this._step0 = step0 this._step1 = step1 + this._failfast = failfast } convert(json: TIn, context: ConversionContext): TOut { const r0 = this._step0.convert(json, context.inOperation(this._step0.name)) + if(context.hasErrors() && this._failfast){ + return undefined + } return this._step1.convert(r0, context.inOperation(this._step1.name)) } } diff --git a/src/Models/ThemeConfig/Conversion/PrepareTheme.ts b/src/Models/ThemeConfig/Conversion/PrepareTheme.ts index dec7a71090..12c5a60a0d 100644 --- a/src/Models/ThemeConfig/Conversion/PrepareTheme.ts +++ b/src/Models/ThemeConfig/Conversion/PrepareTheme.ts @@ -21,6 +21,7 @@ import DependencyCalculator from "../DependencyCalculator" import { AddContextToTranslations } from "./AddContextToTranslations" import ValidationUtils from "./ValidationUtils" import { ConversionContext } from "./ConversionContext" +import { PrevalidateTheme } from "./Validation" class SubstituteLayer extends Conversion { private readonly _state: DesugaringContext @@ -664,7 +665,6 @@ export class PrepareTheme extends Fuse { ) { super( "Fully prepares and expands a theme", - new AddContextToTranslationsInLayout(), new PreparePersonalTheme(state), new WarnForUnsubstitutedLayersInTheme(), diff --git a/src/Models/ThemeConfig/Conversion/Validation.ts b/src/Models/ThemeConfig/Conversion/Validation.ts index 6769b1bf1f..f0f860c519 100644 --- a/src/Models/ThemeConfig/Conversion/Validation.ts +++ b/src/Models/ThemeConfig/Conversion/Validation.ts @@ -36,7 +36,7 @@ class ValidateLanguageCompleteness extends DesugaringStep { super( "Checks that the given object is fully translated in the specified languages", [], - "ValidateLanguageCompleteness" + "ValidateLanguageCompleteness", ) this._languages = languages ?? ["en"] } @@ -50,18 +50,18 @@ class ValidateLanguageCompleteness extends DesugaringStep { .filter( (t) => t.tr.translations[neededLanguage] === undefined && - t.tr.translations["*"] === undefined + t.tr.translations["*"] === undefined, ) .forEach((missing) => { context .enter(missing.context.split(".")) .err( `The theme ${obj.id} should be translation-complete for ` + - neededLanguage + - ", but it lacks a translation for " + - missing.context + - ".\n\tThe known translation is " + - missing.tr.textFor("en") + neededLanguage + + ", but it lacks a translation for " + + missing.context + + ".\n\tThe known translation is " + + missing.tr.textFor("en"), ) }) } @@ -78,7 +78,7 @@ export class DoesImageExist extends DesugaringStep { constructor( knownImagePaths: Set, checkExistsSync: (path: string) => boolean = undefined, - ignore?: Set + ignore?: Set, ) { super("Checks if an image exists", [], "DoesImageExist") this._ignore = ignore @@ -114,15 +114,15 @@ export class DoesImageExist extends DesugaringStep { if (!this._knownImagePaths.has(image)) { if (this.doesPathExist === undefined) { context.err( - `Image with path ${image} not found or not attributed; it is used in ${context}` + `Image with path ${image} not found or not attributed; it is used in ${context}`, ) } else if (!this.doesPathExist(image)) { context.err( - `Image with path ${image} does not exist.\n Check for typo's and missing directories in the path.` + `Image with path ${image} does not exist.\n Check for typo's and missing directories in the path.`, ) } else { context.err( - `Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info` + `Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`, ) } } @@ -146,7 +146,7 @@ export class ValidateTheme extends DesugaringStep { doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, - sharedTagRenderings?: Set + sharedTagRenderings?: Set, ) { super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme") this._validateImage = doesImageExist @@ -165,15 +165,15 @@ export class ValidateTheme extends DesugaringStep { if (json["units"] !== undefined) { context.err( "The theme " + - json.id + - " has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) " + json.id + + " has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) ", ) } if (json["roamingRenderings"] !== undefined) { context.err( "Theme " + - json.id + - " contains an old 'roamingRenderings'. Use an 'overrideAll' instead" + json.id + + " contains an old 'roamingRenderings'. Use an 'overrideAll' instead", ) } } @@ -191,10 +191,10 @@ export class ValidateTheme extends DesugaringStep { for (const remoteImage of remoteImages) { context.err( "Found a remote image: " + - remoteImage.path + - " in theme " + - json.id + - ", please download it." + remoteImage.path + + " in theme " + + json.id + + ", please download it.", ) } for (const image of images) { @@ -210,17 +210,17 @@ export class ValidateTheme extends DesugaringStep { const filename = this._path.substring( this._path.lastIndexOf("/") + 1, - this._path.length - 5 + this._path.length - 5, ) if (theme.id !== filename) { context.err( "Theme ids should be the same as the name.json, but we got id: " + - theme.id + - " and filename " + - filename + - " (" + - this._path + - ")" + theme.id + + " and filename " + + filename + + " (" + + this._path + + ")", ) } this._validateImage.convert(theme.icon, context.enter("icon")) @@ -228,13 +228,13 @@ export class ValidateTheme extends DesugaringStep { const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"])) if (dups.length > 0) { context.err( - `The theme ${json.id} defines multiple layers with id ${dups.join(", ")}` + `The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`, ) } if (json["mustHaveLanguage"] !== undefined) { new ValidateLanguageCompleteness(...json["mustHaveLanguage"]).convert( theme, - context + context, ) } if (!json.hideFromOverview && theme.id !== "personal" && this._isBuiltin) { @@ -242,7 +242,7 @@ export class ValidateTheme extends DesugaringStep { const targetLanguage = theme.title.SupportedLanguages()[0] if (targetLanguage !== "en") { context.err( - `TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key` + `TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key`, ) } @@ -286,7 +286,7 @@ export class ValidateTheme extends DesugaringStep { .err( `This layer ID is not known: ${backgroundId}. Perhaps you meant one of ${nearby .slice(0, 5) - .join(", ")}` + .join(", ")}`, ) } } @@ -309,7 +309,7 @@ export class ValidateThemeAndLayers extends Fuse { doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, - sharedTagRenderings?: Set + sharedTagRenderings?: Set, ) { super( "Validates a theme and the contained layers", @@ -319,10 +319,10 @@ export class ValidateThemeAndLayers extends Fuse { new Each( new Bypass( (layer) => Constants.added_by_default.indexOf(layer.id) < 0, - new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true) - ) - ) - ) + new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true), + ), + ), + ), ) } } @@ -332,7 +332,7 @@ class OverrideShadowingCheck extends DesugaringStep { super( "Checks that an 'overrideAll' does not override a single override", [], - "OverrideShadowingCheck" + "OverrideShadowingCheck", ) } @@ -378,6 +378,9 @@ class MiscThemeChecks extends DesugaringStep { if (json.id !== "personal" && (json.layers === undefined || json.layers.length === 0)) { context.err("The theme " + json.id + " has no 'layers' defined") } + if (!Array.isArray(json.layers)) { + context.enter("layers").err("The 'layers'-field should be an array, but it is not. Did you pase a layer identifier and forget to add the '[' and ']'?") + } if (json.socialImage === "") { context.warn("Social image for theme " + json.id + " is the emtpy string") } @@ -406,7 +409,7 @@ class MiscThemeChecks extends DesugaringStep { context .enter("overideAll") .err( - "'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them." + "'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them.", ) } return json @@ -418,7 +421,7 @@ export class PrevalidateTheme extends Fuse { super( "Various consistency checks on the raw JSON", new MiscThemeChecks(), - new OverrideShadowingCheck() + new OverrideShadowingCheck(), ) } } @@ -428,7 +431,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep ["_abc"] */ private static extractCalculatedTagNames( - layerConfig?: LayerConfigJson | { calculatedTags: string[] } + layerConfig?: LayerConfigJson | { calculatedTags: string[] }, ) { return ( layerConfig?.calculatedTags?.map((ct) => { @@ -728,16 +731,16 @@ export class DetectShadowedMappings extends DesugaringStep\` instead. The images found are ${images.join( - ", " - )}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged` + ", ", + )}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged`, ) } else { ctx.info( `Ignored image ${images.join( - ", " - )} in 'then'-clause of a mapping as this check has been disabled` + ", ", + )} in 'then'-clause of a mapping as this check has been disabled`, ) for (const image of images) { @@ -832,7 +835,7 @@ class ValidatePossibleLinks extends DesugaringStep does have `rel='noopener'` set", [], - "ValidatePossibleLinks" + "ValidatePossibleLinks", ) } @@ -862,21 +865,21 @@ class ValidatePossibleLinks extends DesugaringStep, - context: ConversionContext + context: ConversionContext, ): string | Record { if (typeof json === "string") { if (this.isTabnabbingProne(json)) { context.err( "The string " + - json + - " has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping" + json + + " has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping", ) } } else { for (const k in json) { if (this.isTabnabbingProne(json[k])) { context.err( - `The translation for ${k} '${json[k]}' has a link targeting \`_blank\`, but it doesn't have \`rel='noopener'\` set. This gives rise to reverse tabnapping` + `The translation for ${k} '${json[k]}' has a link targeting \`_blank\`, but it doesn't have \`rel='noopener'\` set. This gives rise to reverse tabnapping`, ) } } @@ -894,7 +897,7 @@ class CheckTranslation extends DesugaringStep { super( "Checks that a translation is valid and internally consistent", ["*"], - "CheckTranslation" + "CheckTranslation", ) this._allowUndefined = allowUndefined } @@ -935,6 +938,7 @@ class CheckTranslation extends DesugaringStep { class MiscTagRenderingChecks extends DesugaringStep { private readonly _layerConfig: LayerConfigJson + constructor(layerConfig?: LayerConfigJson) { super("Miscellaneous checks on the tagrendering", ["special"], "MiscTagRenderingChecks") this._layerConfig = layerConfig @@ -942,17 +946,17 @@ class MiscTagRenderingChecks extends DesugaringStep { convert( json: TagRenderingConfigJson | QuestionableTagRenderingConfigJson, - context: ConversionContext + context: ConversionContext, ): TagRenderingConfigJson { if (json["special"] !== undefined) { context.err( - 'Detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`' + "Detected `special` on the top level. Did you mean `{\"render\":{ \"special\": ... }}`", ) } if (Object.keys(json).length === 1 && typeof json["render"] === "string") { context.warn( - `use the content directly instead of {render: ${JSON.stringify(json["render"])}}` + `use the content directly instead of {render: ${JSON.stringify(json["render"])}}`, ) } @@ -964,7 +968,7 @@ class MiscTagRenderingChecks extends DesugaringStep { const mapping: MappingConfigJson = json.mappings[i] CheckTranslation.noUndefined.convert( mapping.then, - context.enters("mappings", i, "then") + context.enters("mappings", i, "then"), ) if (!mapping.if) { console.log( @@ -973,7 +977,7 @@ class MiscTagRenderingChecks extends DesugaringStep { "if", mapping.if, context.path.join("."), - mapping.then + mapping.then, ) context.enters("mappings", i, "if").err("No `if` is defined") } @@ -983,7 +987,7 @@ class MiscTagRenderingChecks extends DesugaringStep { context .enters("mappings", i, "addExtraTags", j) .err( - "Detected a 'null' or 'undefined' value. Either specify a tag or delete this item" + "Detected a 'null' or 'undefined' value. Either specify a tag or delete this item", ) } } @@ -994,18 +998,18 @@ class MiscTagRenderingChecks extends DesugaringStep { context .enters("mappings", i, "then") .warn( - "A mapping should not start with 'yes' or 'no'. If the attribute is known, it will only show 'yes' or 'no' without the question, resulting in a weird phrasing in the information box" + "A mapping should not start with 'yes' or 'no'. If the attribute is known, it will only show 'yes' or 'no' without the question, resulting in a weird phrasing in the information box", ) } } } if (json["group"]) { - context.err('Groups are deprecated, use `"label": ["' + json["group"] + '"]` instead') + context.err("Groups are deprecated, use `\"label\": [\"" + json["group"] + "\"]` instead") } if (json["question"] && json.freeform?.key === undefined && json.mappings === undefined) { context.err( - "A question is defined, but no mappings nor freeform (key) are. Add at least one of them" + "A question is defined, but no mappings nor freeform (key) are. Add at least one of them", ) } if (json["question"] && !json.freeform && (json.mappings?.length ?? 0) == 1) { @@ -1015,7 +1019,7 @@ class MiscTagRenderingChecks extends DesugaringStep { context .enter("questionHint") .err( - "A questionHint is defined, but no question is given. As such, the questionHint will never be shown" + "A questionHint is defined, but no question is given. As such, the questionHint will never be shown", ) } @@ -1023,7 +1027,7 @@ class MiscTagRenderingChecks extends DesugaringStep { context .enters("icon", "size") .err( - "size is not a valid attribute. Did you mean 'class'? Class can be one of `small`, `medium` or `large`" + "size is not a valid attribute. Did you mean 'class'? Class can be one of `small`, `medium` or `large`", ) } @@ -1033,10 +1037,10 @@ class MiscTagRenderingChecks extends DesugaringStep { .enter("render") .err( "This tagRendering allows to set a value to key " + - json.freeform.key + - ", but does not define a `render`. Please, add a value here which contains `{" + - json.freeform.key + - "}`" + json.freeform.key + + ", but does not define a `render`. Please, add a value here which contains `{" + + json.freeform.key + + "}`", ) } else { const render = new Translation(json.render) @@ -1067,7 +1071,7 @@ class MiscTagRenderingChecks extends DesugaringStep { const keyFirstArg = ["canonical", "fediverse_link", "translated"] if ( keyFirstArg.some( - (funcName) => txt.indexOf(`{${funcName}(${json.freeform.key}`) >= 0 + (funcName) => txt.indexOf(`{${funcName}(${json.freeform.key}`) >= 0, ) ) { continue @@ -1091,7 +1095,7 @@ class MiscTagRenderingChecks extends DesugaringStep { context .enter("render") .err( - `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. Did you perhaps forget to set "freeform.type: 'wikidata'"?` + `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. Did you perhaps forget to set "freeform.type: 'wikidata'"?`, ) continue } @@ -1103,7 +1107,7 @@ class MiscTagRenderingChecks extends DesugaringStep { context .enter("render") .err( - `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. However, it does contain ${json.freeform.key} without braces. Did you forget the braces?\n\tThe current text is ${txt}` + `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. However, it does contain ${json.freeform.key} without braces. Did you forget the braces?\n\tThe current text is ${txt}`, ) continue } @@ -1111,7 +1115,7 @@ class MiscTagRenderingChecks extends DesugaringStep { context .enter("render") .err( - `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. This is a bug, as this rendering should show exactly this freeform key!\n\tThe current text is ${txt}` + `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. This is a bug, as this rendering should show exactly this freeform key!\n\tThe current text is ${txt}`, ) } } @@ -1126,22 +1130,22 @@ class MiscTagRenderingChecks extends DesugaringStep { .enters("freeform", "type") .err( "No entry found in the 'Name Suggestion Index'. None of the 'osmSource'-tags match an entry in the NSI.\n\tOsmSource-tags are " + - tags.map((t) => new Tag(t.key, t.value).asHumanString()).join(" ; ") + tags.map((t) => new Tag(t.key, t.value).asHumanString()).join(" ; "), ) } } else if (json.freeform.type === "nsi") { context .enters("freeform", "type") .warn( - "No need to explicitly set type to 'NSI', autodetected based on freeform type" + "No need to explicitly set type to 'NSI', autodetected based on freeform type", ) } } if (json.render && json["question"] && json.freeform === undefined) { context.err( `Detected a tagrendering which takes input without freeform key in ${context}; the question is ${new Translation( - json["question"] - ).textFor("en")}` + json["question"], + ).textFor("en")}`, ) } @@ -1152,9 +1156,9 @@ class MiscTagRenderingChecks extends DesugaringStep { .enters("freeform", "type") .err( "Unknown type: " + - freeformType + - "; try one of " + - Validators.availableTypes.join(", ") + freeformType + + "; try one of " + + Validators.availableTypes.join(", "), ) } } @@ -1192,7 +1196,7 @@ export class ValidateTagRenderings extends Fuse { new On("question", new ValidatePossibleLinks()), new On("questionHint", new ValidatePossibleLinks()), new On("mappings", new Each(new On("then", new ValidatePossibleLinks()))), - new MiscTagRenderingChecks(layerConfig) + new MiscTagRenderingChecks(layerConfig), ) } } @@ -1211,7 +1215,7 @@ export class PrevalidateLayer extends DesugaringStep { path: string, isBuiltin: boolean, doesImageExist: DoesImageExist, - studioValidations: boolean + studioValidations: boolean, ) { super("Runs various checks against common mistakes for a layer", [], "PrevalidateLayer") this._path = path @@ -1237,7 +1241,7 @@ export class PrevalidateLayer extends DesugaringStep { context .enter("source") .err( - "No source section is defined; please define one as data is not loaded otherwise" + "No source section is defined; please define one as data is not loaded otherwise", ) } else { if (json.source === "special" || json.source === "special:library") { @@ -1245,7 +1249,7 @@ export class PrevalidateLayer extends DesugaringStep { context .enters("source", "osmTags") .err( - "No osmTags defined in the source section - these should always be present, even for geojson layer" + "No osmTags defined in the source section - these should always be present, even for geojson layer", ) } else { const osmTags = TagUtils.Tag(json.source["osmTags"], context + "source.osmTags") @@ -1254,7 +1258,7 @@ export class PrevalidateLayer extends DesugaringStep { .enters("source", "osmTags") .err( "The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" + - osmTags.asHumanString(false, false, {}) + osmTags.asHumanString(false, false, {}), ) } } @@ -1280,10 +1284,10 @@ export class PrevalidateLayer extends DesugaringStep { .enter("syncSelection") .err( "Invalid sync-selection: must be one of " + - LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") + - " but got '" + - json.syncSelection + - "'" + LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") + + " but got '" + + json.syncSelection + + "'", ) } if (json["pointRenderings"]?.length > 0) { @@ -1302,7 +1306,7 @@ export class PrevalidateLayer extends DesugaringStep { } json.pointRendering?.forEach((pr, i) => - this._validatePointRendering.convert(pr, context.enters("pointeRendering", i)) + this._validatePointRendering.convert(pr, context.enters("pointeRendering", i)), ) if (json["mapRendering"]) { @@ -1319,8 +1323,8 @@ export class PrevalidateLayer extends DesugaringStep { if (!Constants.priviliged_layers.find((x) => x == json.id)) { context.err( "Layer " + - json.id + - " uses 'special' as source.osmTags. However, this layer is not a priviliged layer" + json.id + + " uses 'special' as source.osmTags. However, this layer is not a priviliged layer", ) } } @@ -1335,19 +1339,19 @@ export class PrevalidateLayer extends DesugaringStep { context .enter("title") .err( - "This layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error." + "This layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error.", ) } if (json.title === null) { context.info( - "Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set." + "Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set.", ) } { // Check for multiple, identical builtin questions - usability for studio users const duplicates = Utils.Duplicates( - json.tagRenderings.filter((tr) => typeof tr === "string") + json.tagRenderings.filter((tr) => typeof tr === "string"), ) for (let i = 0; i < json.tagRenderings.length; i++) { const tagRendering = json.tagRenderings[i] @@ -1377,7 +1381,7 @@ export class PrevalidateLayer extends DesugaringStep { { // duplicate ids in tagrenderings check const duplicates = Utils.NoNull( - Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"]))) + Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"]))), ) if (duplicates.length > 0) { // It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list @@ -1385,11 +1389,11 @@ export class PrevalidateLayer extends DesugaringStep { .enter("tagRenderings") .err( "Some tagrenderings have a duplicate id: " + - duplicates.join(", ") + - "\n" + - JSON.stringify( - json.tagRenderings.filter((tr) => duplicates.indexOf(tr["id"]) >= 0) - ) + duplicates.join(", ") + + "\n" + + JSON.stringify( + json.tagRenderings.filter((tr) => duplicates.indexOf(tr["id"]) >= 0), + ), ) } } @@ -1422,8 +1426,8 @@ export class PrevalidateLayer extends DesugaringStep { if (json["overpassTags"] !== undefined) { context.err( "Layer " + - json.id + - 'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": }\' instead of "overpassTags": (note: this isn\'t your fault, the custom theme generator still spits out the old format)' + json.id + + "still uses the old 'overpassTags'-format. Please use \"source\": {\"osmTags\": }' instead of \"overpassTags\": (note: this isn't your fault, the custom theme generator still spits out the old format)", ) } const forbiddenTopLevel = [ @@ -1443,7 +1447,7 @@ export class PrevalidateLayer extends DesugaringStep { } if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) { context.err( - "Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'" + "Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'", ) } @@ -1460,9 +1464,9 @@ export class PrevalidateLayer extends DesugaringStep { if (this._path != undefined && this._path.indexOf(expected) < 0) { context.err( "Layer is in an incorrect place. The path is " + - this._path + - ", but expected " + - expected + this._path + + ", but expected " + + expected, ) } } @@ -1480,13 +1484,13 @@ export class PrevalidateLayer extends DesugaringStep { .enter(["tagRenderings", ...emptyIndexes]) .err( `Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${emptyIndexes.join( - "," - )}])` + ",", + )}])`, ) } const duplicateIds = Utils.Duplicates( - (json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions") + (json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions"), ) if (duplicateIds.length > 0 && !Utils.runningFromConsole) { context @@ -1510,7 +1514,7 @@ export class PrevalidateLayer extends DesugaringStep { if (json.tagRenderings !== undefined) { new On( "tagRenderings", - new Each(new ValidateTagRenderings(json, this._doesImageExist)) + new Each(new ValidateTagRenderings(json, this._doesImageExist)), ).convert(json, context) } @@ -1537,7 +1541,7 @@ export class PrevalidateLayer extends DesugaringStep { context .enters("pointRendering", i, "marker", indexM, "icon", "condition") .err( - "Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead." + "Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead.", ) } } @@ -1575,9 +1579,9 @@ export class PrevalidateLayer extends DesugaringStep { .enters("presets", i, "tags") .err( "This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " + - tags.asHumanString(false, false, {}) + - "\n The required tags are: " + - baseTags.asHumanString(false, false, {}) + tags.asHumanString(false, false, {}) + + "\n The required tags are: " + + baseTags.asHumanString(false, false, {}), ) } } @@ -1594,7 +1598,7 @@ export class ValidateLayerConfig extends DesugaringStep { isBuiltin: boolean, doesImageExist: DoesImageExist, studioValidations: boolean = false, - skipDefaultLayers: boolean = false + skipDefaultLayers: boolean = false, ) { super("Thin wrapper around 'ValidateLayer", [], "ValidateLayerConfig") this.validator = new ValidateLayer( @@ -1602,7 +1606,7 @@ export class ValidateLayerConfig extends DesugaringStep { isBuiltin, doesImageExist, studioValidations, - skipDefaultLayers + skipDefaultLayers, ) } @@ -1630,7 +1634,7 @@ class ValidatePointRendering extends DesugaringStep { context .enter("markers") .err( - `Detected a field 'markerS' in pointRendering. It is written as a singular case` + `Detected a field 'markerS' in pointRendering. It is written as a singular case`, ) } if (json.marker && !Array.isArray(json.marker)) { @@ -1640,7 +1644,7 @@ class ValidatePointRendering extends DesugaringStep { context .enter("location") .err( - "A pointRendering should have at least one 'location' to defined where it should be rendered. " + "A pointRendering should have at least one 'location' to defined where it should be rendered. ", ) } return json @@ -1659,26 +1663,26 @@ export class ValidateLayer extends Conversion< isBuiltin: boolean, doesImageExist: DoesImageExist, studioValidations: boolean = false, - skipDefaultLayers: boolean = false + skipDefaultLayers: boolean = false, ) { super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer") this._prevalidation = new PrevalidateLayer( path, isBuiltin, doesImageExist, - studioValidations + studioValidations, ) this._skipDefaultLayers = skipDefaultLayers } convert( json: LayerConfigJson, - context: ConversionContext + context: ConversionContext, ): { parsed: LayerConfig; raw: LayerConfigJson } { context = context.inOperation(this.name) if (typeof json === "string") { context.err( - `Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed` + `Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`, ) return undefined } @@ -1709,7 +1713,7 @@ export class ValidateLayer extends Conversion< context .enters("calculatedTags", i) .err( - `Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}` + `Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`, ) } } @@ -1757,7 +1761,7 @@ export class ValidateLayer extends Conversion< context .enters("allowMove", "enableAccuracy") .err( - "`enableAccuracy` is written with two C in the first occurrence and only one in the last" + "`enableAccuracy` is written with two C in the first occurrence and only one in the last", ) } @@ -1788,8 +1792,8 @@ export class ValidateFilter extends DesugaringStep { .enters("fields", i) .err( `Invalid filter: ${type} is not a valid textfield type.\n\tTry one of ${Array.from( - Validators.availableTypes - ).join(",")}` + Validators.availableTypes, + ).join(",")}`, ) } } @@ -1806,13 +1810,13 @@ export class DetectDuplicateFilters extends DesugaringStep<{ super( "Tries to detect layers where a shared filter can be used (or where similar filters occur)", [], - "DetectDuplicateFilters" + "DetectDuplicateFilters", ) } convert( json: { layers: LayerConfigJson[]; themes: LayoutConfigJson[] }, - context: ConversionContext + context: ConversionContext, ): { layers: LayerConfigJson[]; themes: LayoutConfigJson[] } { const { layers, themes } = json const perOsmTag = new Map< @@ -1876,7 +1880,7 @@ export class DetectDuplicateFilters extends DesugaringStep<{ filter: FilterConfigJson }[] >, - layout?: LayoutConfigJson | undefined + layout?: LayoutConfigJson | undefined, ): void { if (layer.filter === undefined || layer.filter === null) { return @@ -1916,7 +1920,7 @@ export class DetectDuplicatePresets extends DesugaringStep { super( "Detects mappings which have identical (english) names or identical mappings.", ["presets"], - "DetectDuplicatePresets" + "DetectDuplicatePresets", ) } @@ -1927,13 +1931,13 @@ export class DetectDuplicatePresets extends DesugaringStep { if (new Set(enNames).size != enNames.length) { const dups = Utils.Duplicates(enNames) const layersWithDup = json.layers.filter((l) => - l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0) + l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0), ) const layerIds = layersWithDup.map((l) => l.id) context.err( `This theme has multiple presets which are named:${dups}, namely layers ${layerIds.join( - ", " - )} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets` + ", ", + )} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`, ) } @@ -1948,17 +1952,17 @@ export class DetectDuplicatePresets extends DesugaringStep { Utils.SameObject(presetATags, presetBTags) && Utils.sameList( presetA.preciseInput.snapToLayers, - presetB.preciseInput.snapToLayers + presetB.preciseInput.snapToLayers, ) ) { context.err( `This theme has multiple presets with the same tags: ${presetATags.asHumanString( false, false, - {} + {}, )}, namely the preset '${presets[i].title.textFor("en")}' and '${presets[ j - ].title.textFor("en")}'` + ].title.textFor("en")}'`, ) } } @@ -1983,13 +1987,13 @@ export class ValidateThemeEnsemble extends Conversion< super( "Validates that all themes together are logical, i.e. no duplicate ids exists within (overriden) themes", [], - "ValidateThemeEnsemble" + "ValidateThemeEnsemble", ) } convert( json: LayoutConfig[], - context: ConversionContext + context: ConversionContext, ): Map< string, { @@ -2040,11 +2044,11 @@ export class ValidateThemeEnsemble extends Conversion< context.err( [ "The layer with id '" + - id + - "' is found in multiple themes with different tag definitions:", + id + + "' is found in multiple themes with different tag definitions:", "\t In theme " + oldTheme + ":\t" + oldTags.asHumanString(false, false, {}), "\tIn theme " + theme.id + ":\t" + tags.asHumanString(false, false, {}), - ].join("\n") + ].join("\n"), ) } } diff --git a/src/Models/ThemeConfig/Json/LayoutConfigJson.ts b/src/Models/ThemeConfig/Json/LayoutConfigJson.ts index 107103ad4b..28c9596c1c 100644 --- a/src/Models/ThemeConfig/Json/LayoutConfigJson.ts +++ b/src/Models/ThemeConfig/Json/LayoutConfigJson.ts @@ -80,6 +80,7 @@ export interface LayoutConfigJson { * Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64) * * Type: icon + * suggestions: return Constants.defaultPinIcons.map(i => ({if: "value="+i, then: i, icon: i})) * group: basic * */ @@ -156,6 +157,7 @@ export interface LayoutConfigJson { * type: layer[] * types: hidden | layer | hidden * group: layers + * title: value["builtin"] ?? value["id"] ?? value * suggestions: return Array.from(layers.keys()).map(key => ({if: "value="+key, then: ""+key+" (builtin) - "+layers.get(key).description})) * * A theme must contain at least one layer. diff --git a/src/UI/Flowbite/AccordionSingle.svelte b/src/UI/Flowbite/AccordionSingle.svelte index afd6568ff5..da4d901435 100644 --- a/src/UI/Flowbite/AccordionSingle.svelte +++ b/src/UI/Flowbite/AccordionSingle.svelte @@ -6,7 +6,7 @@ - +
diff --git a/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte b/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte index e096759b83..86901cb84f 100644 --- a/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte +++ b/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte @@ -373,7 +373,6 @@ {feedback} {unit} {state} - {extraTags} feature={selectedElement} value={freeformInput} unvalidatedText={freeformInputUnvalidated} @@ -418,7 +417,6 @@ {feedback} {unit} {state} - {extraTags} feature={selectedElement} value={freeformInput} unvalidatedText={freeformInputUnvalidated} @@ -464,7 +462,6 @@ {feedback} {unit} {state} - {extraTags} feature={selectedElement} value={freeformInput} unvalidatedText={freeformInputUnvalidated} diff --git a/src/UI/Studio/CollapsedTagRenderingPreview.svelte b/src/UI/Studio/CollapsedTagRenderingPreview.svelte new file mode 100644 index 0000000000..920bcb8324 --- /dev/null +++ b/src/UI/Studio/CollapsedTagRenderingPreview.svelte @@ -0,0 +1,207 @@ + + + + +
+ {#if !isTagRenderingBlock} +
+
+ {#if schema.hints.icon} + + {/if} + {#if schema.hints.title} + +
+ {singular} + {i} +
+ {:else} + {singular} + {i} + {/if} +
+ +
+ {:else if typeof value === "string"} + Builtin: {value} + {:else if value["builtin"]} + reused tagrendering {JSON.stringify(value["builtin"])} + {:else} + + {/if} +
+
+ {#if isTagRenderingBlock} + + + + {#if i > 0} + + + + {/if} + {#if i + 1 < $currentValue.length} + + + {/if} + + {:else if schema.hints.types} + + {:else} + {#each subparts as subpart} + + {/each} + {/if} +
+
diff --git a/src/UI/Studio/EditLayerState.ts b/src/UI/Studio/EditLayerState.ts index 45ab9914e8..95e3374dbe 100644 --- a/src/UI/Studio/EditLayerState.ts +++ b/src/UI/Studio/EditLayerState.ts @@ -8,7 +8,7 @@ import { Pipe, } from "../../Models/ThemeConfig/Conversion/Conversion" import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer" -import { ValidateLayer, ValidateTheme } from "../../Models/ThemeConfig/Conversion/Validation" +import { PrevalidateTheme, ValidateLayer, ValidateTheme } from "../../Models/ThemeConfig/Conversion/Validation" import { AllSharedLayers } from "../../Customizations/AllSharedLayers" import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" import { TagUtils } from "../../Logic/Tags/TagUtils" @@ -33,6 +33,8 @@ export abstract class EditJsonState { public readonly schema: ConfigMeta[] public readonly category: "layers" | "themes" public readonly server: StudioServer + public readonly osmConnection: OsmConnection + public readonly showIntro: UIEventSource<"no" | "intro" | "tagrenderings"> = ( LocalStorageSource.Get("studio-show-intro", "intro") ) @@ -51,7 +53,7 @@ export abstract class EditJsonState { * The EditLayerUI shows a 'schemaBasedInput' for this path to pop advanced questions out */ public readonly highlightedItem: UIEventSource = new UIEventSource( - undefined + undefined, ) private sendingUpdates = false private readonly _stores = new Map>() @@ -60,10 +62,12 @@ export abstract class EditJsonState { schema: ConfigMeta[], server: StudioServer, category: "layers" | "themes", + osmConnection: OsmConnection, options?: { expertMode?: UIEventSource - } + }, ) { + this.osmConnection = osmConnection this.schema = schema this.server = server this.category = category @@ -88,6 +92,10 @@ export abstract class EditJsonState { await this.server.update(id, config, this.category) }) this.messages = this.createMessagesStore() + this.register(["credits"], this.osmConnection.userDetails.mapD(u => u.name), false) + this.register(["credits:uid"], this.osmConnection.userDetails.mapD(u => u.uid), false) + + } public startSavingUpdates(enabled = true) { @@ -132,7 +140,7 @@ export abstract class EditJsonState { public register( path: ReadonlyArray, value: Store, - noInitialSync: boolean = true + noInitialSync: boolean = true, ): () => void { const unsync = value.addCallback((v) => { this.setValueAt(path, v) @@ -146,7 +154,7 @@ export abstract class EditJsonState { public getSchemaStartingWith(path: string[]) { return this.schema.filter( (sch) => - !path.some((part, i) => !(sch.path.length > path.length && sch.path[i] === part)) + !path.some((part, i) => !(sch.path.length > path.length && sch.path[i] === part)), ) } @@ -167,7 +175,7 @@ export abstract class EditJsonState { const schemas = this.schema.filter( (sch) => sch !== undefined && - !path.some((part, i) => !(sch.path.length == path.length && sch.path[i] === part)) + !path.some((part, i) => !(sch.path.length == path.length && sch.path[i] === part)), ) if (schemas.length == 0) { console.warn("No schemas found for path", path.join(".")) @@ -257,12 +265,12 @@ class ContextRewritingStep extends Conversion { constructor( state: DesugaringContext, step: Conversion, - getTagRenderings: (t: T) => TagRenderingConfigJson[] + getTagRenderings: (t: T) => TagRenderingConfigJson[], ) { super( "When validating a layer, the tagRenderings are first expanded. Some builtin tagRendering-calls (e.g. `contact`) will introduce _multiple_ tagRenderings, causing the count to be off. This class rewrites the error messages to fix this", [], - "ContextRewritingStep" + "ContextRewritingStep", ) this._state = state this._step = step @@ -272,7 +280,7 @@ class ContextRewritingStep extends Conversion { convert(json: LayerConfigJson, context: ConversionContext): T { const converted = this._step.convert(json, context) const originalIds = json.tagRenderings?.map( - (tr) => (tr)["id"] + (tr) => (tr)["id"], ) if (!originalIds) { return converted @@ -307,7 +315,6 @@ class ContextRewritingStep extends Conversion { export default class EditLayerState extends EditJsonState { // Needed for the special visualisations - public readonly osmConnection: OsmConnection public readonly imageUploadManager = { getCountsFor() { return 0 @@ -335,10 +342,9 @@ export default class EditLayerState extends EditJsonState { schema: ConfigMeta[], server: StudioServer, osmConnection: OsmConnection, - options: { expertMode: UIEventSource } + options: { expertMode: UIEventSource }, ) { - super(schema, server, "layers", options) - this.osmConnection = osmConnection + super(schema, server, "layers", osmConnection, options) this.layout = { getMatchingLayer: () => { try { @@ -393,7 +399,7 @@ export default class EditLayerState extends EditJsonState { return new ContextRewritingStep( state, new Pipe(new PrepareLayer(state), new ValidateLayer("dynamic", false, undefined, true)), - (t) => t.raw.tagRenderings + (t) => t.raw.tagRenderings, ) } @@ -427,7 +433,7 @@ export default class EditLayerState extends EditJsonState { } protected async validate( - configuration: Partial + configuration: Partial, ): Promise { const layers = AllSharedLayers.getSharedLayersConfigs() @@ -456,16 +462,19 @@ export class EditThemeState extends EditJsonState { constructor( schema: ConfigMeta[], server: StudioServer, - options: { expertMode: UIEventSource } + osmConnection: OsmConnection, + options: { expertMode: UIEventSource }, ) { - super(schema, server, "themes", options) + super(schema, server, "themes", osmConnection, options) this.setupFixers() } protected buildValidation(state: DesugaringContext): Conversion { - return new Pipe( - new PrepareTheme(state), - new ValidateTheme(undefined, "", false, new Set(state.tagRenderings.keys())) + return new Pipe(new PrevalidateTheme(), + new Pipe( + new PrepareTheme(state), + new ValidateTheme(undefined, "", false, new Set(state.tagRenderings.keys())), + ), true, ) } diff --git a/src/UI/Studio/QuestionPreview.svelte b/src/UI/Studio/QuestionPreview.svelte index 5a68403ebb..13271bc513 100644 --- a/src/UI/Studio/QuestionPreview.svelte +++ b/src/UI/Studio/QuestionPreview.svelte @@ -1,11 +1,13 @@ -
+{#if useFallback} +