From 2147b8d3681bb7daa6139198e71394f5163f4406 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 22 Jun 2023 15:07:14 +0200 Subject: [PATCH] Studio: add tagInput element --- Logic/Web/TagInfo.ts | 49 ++++++++++ UI/InputElement/Validators.ts | 4 +- .../Validators/SimpleTagValidator.ts | 24 ++--- UI/InputElement/Validators/TagKeyValidator.ts | 30 ++++++ UI/Studio/EditLayerState.ts | 2 +- UI/Studio/RegisteredTagInput.svelte | 18 ++++ UI/Studio/SchemaBasedInput.svelte | 6 +- UI/Studio/TagExpression.svelte | 95 ++++++++++++++++++ UI/Studio/TagInfoStats.svelte | 60 ++++++++++++ UI/Studio/TagInput/BasicTagInput.svelte | 98 +++++++++++++++++++ UI/Studio/TagInput/TagInput.svelte | 16 +++ assets/layerconfigmeta.json | 2 +- assets/layoutconfigmeta.json | 4 +- public/css/index-tailwind-output.css | 8 +- scripts/generateDocs.ts | 29 +++--- test.ts | 10 +- 16 files changed, 413 insertions(+), 42 deletions(-) create mode 100644 Logic/Web/TagInfo.ts create mode 100644 UI/InputElement/Validators/TagKeyValidator.ts create mode 100644 UI/Studio/RegisteredTagInput.svelte create mode 100644 UI/Studio/TagExpression.svelte create mode 100644 UI/Studio/TagInfoStats.svelte create mode 100644 UI/Studio/TagInput/BasicTagInput.svelte create mode 100644 UI/Studio/TagInput/TagInput.svelte diff --git a/Logic/Web/TagInfo.ts b/Logic/Web/TagInfo.ts new file mode 100644 index 000000000..77250ae97 --- /dev/null +++ b/Logic/Web/TagInfo.ts @@ -0,0 +1,49 @@ +import exp from "constants" +import { Utils } from "../../Utils" + +export interface TagInfoStats { + /** + * The total number of entries in the data array, **not** the total number of objects known in OSM! + * + * Use `data.find(item => item.type==="all").count` for this + */ + total: number + data: { + type: "all" | "nodes" | "ways" | "relations" + count: number + count_fraction: number + }[] +} + +export default class TagInfo { + private readonly _backend: string + + public static readonly global = new TagInfo() + + constructor(backend = "https://taginfo.openstreetmap.org/") { + this._backend = backend + } + + public getStats(key: string, value?: string): Promise { + let url: string + if (value) { + url = `${this._backend}api/4/tag/stats?key=${key}&value=${value}` + } else { + url = `${this._backend}api/4/key/stats?key=${key}` + } + return Utils.downloadJsonCached(url, 1000 * 60 * 60) + } + + /** + * Creates the URL to the webpage containing more information + * @param k + * @param v + */ + webUrl(k: string, v: string) { + if (v) { + return `${this._backend}/tags/${k}=${v}#overview` + } else { + return `${this._backend}/keys/${k}#overview` + } + } +} diff --git a/UI/InputElement/Validators.ts b/UI/InputElement/Validators.ts index 16ce856f9..b82a17749 100644 --- a/UI/InputElement/Validators.ts +++ b/UI/InputElement/Validators.ts @@ -20,6 +20,7 @@ import Combine from "../Base/Combine" import Title from "../Base/Title" import SimpleTagValidator from "./Validators/SimpleTagValidator" import ImageUrlValidator from "./Validators/ImageUrlValidator" +import TagKeyValidator from "./Validators/TagKeyValidator"; export type ValidatorType = (typeof Validators.availableTypes)[number] @@ -60,8 +61,9 @@ export default class Validators { new PhoneValidator(), new OpeningHoursValidator(), new ColorValidator(), - new SimpleTagValidator(), new ImageUrlValidator(), + new SimpleTagValidator(), + new TagKeyValidator() ] private static _byType = Validators._byTypeConstructor() diff --git a/UI/InputElement/Validators/SimpleTagValidator.ts b/UI/InputElement/Validators/SimpleTagValidator.ts index 629fd8756..a982b1f75 100644 --- a/UI/InputElement/Validators/SimpleTagValidator.ts +++ b/UI/InputElement/Validators/SimpleTagValidator.ts @@ -1,11 +1,13 @@ import { Validator } from "../Validator" import { Translation } from "../../i18n/Translation" import Translations from "../../i18n/Translations" +import TagKeyValidator from "./TagKeyValidator" /** * Checks that the input conforms `key=value`, where `key` and `value` don't have too much weird characters */ export default class SimpleTagValidator extends Validator { + private static readonly KeyValidator = new TagKeyValidator() constructor() { super( "simple_tag", @@ -13,7 +15,7 @@ export default class SimpleTagValidator extends Validator { ) } - getFeedback(tag: string): Translation | undefined { + getFeedback(tag: string, _): Translation | undefined { const parts = tag.split("=") if (parts.length < 2) { return Translations.T("A tag should contain a = to separate the 'key' and 'value'") @@ -27,31 +29,23 @@ export default class SimpleTagValidator extends Validator { } const [key, value] = parts - if (key.length > 255) { - return Translations.T("A `key` should be at most 255 characters") + const keyFeedback = SimpleTagValidator.KeyValidator.getFeedback(key, _) + if (keyFeedback) { + return keyFeedback } + if (value.length > 255) { return Translations.T("A `value should be at most 255 characters") } - if (key.length == 0) { - return Translations.T("A `key` should not be empty") - } if (value.length == 0) { return Translations.T("A `value should not be empty") } - const keyRegex = /[a-zA-Z0-9:_]+/ - if (!key.match(keyRegex)) { - return Translations.T( - "A `key` should only have the characters `a-zA-Z0-9`, `:` or `_`" - ) - } - return undefined } - isValid(tag: string): boolean { - return this.getFeedback(tag) === undefined + isValid(tag: string, _): boolean { + return this.getFeedback(tag, _) === undefined } } diff --git a/UI/InputElement/Validators/TagKeyValidator.ts b/UI/InputElement/Validators/TagKeyValidator.ts new file mode 100644 index 000000000..5212feb20 --- /dev/null +++ b/UI/InputElement/Validators/TagKeyValidator.ts @@ -0,0 +1,30 @@ +import { Validator } from "../Validator" +import { Translation } from "../../i18n/Translation" +import Translations from "../../i18n/Translations" + +export default class TagKeyValidator extends Validator { + constructor() { + super("key", "Validates a key, mostly that no weird characters are used") + } + + getFeedback(key: string, _?: () => string): Translation | undefined { + if (key.length > 255) { + return Translations.T("A `key` should be at most 255 characters") + } + + if (key.length == 0) { + return Translations.T("A `key` should not be empty") + } + const keyRegex = /[a-zA-Z0-9:_]+/ + if (!key.match(keyRegex)) { + return Translations.T( + "A `key` should only have the characters `a-zA-Z0-9`, `:` or `_`" + ) + } + return undefined + } + + isValid(key: string, getCountry?: () => string): boolean { + return this.getFeedback(key, getCountry) === undefined + } +} diff --git a/UI/Studio/EditLayerState.ts b/UI/Studio/EditLayerState.ts index 78f60f05a..21af6a50a 100644 --- a/UI/Studio/EditLayerState.ts +++ b/UI/Studio/EditLayerState.ts @@ -22,7 +22,7 @@ export default class EditLayerState { this.configuration.addCallback((config) => console.log("Current config is", config)) } - public register(path: ReadonlyArray, value: Store) { + public register(path: ReadonlyArray, value: Store) { value.addCallbackAndRun((v) => { let entry = this.configuration.data for (let i = 0; i < path.length - 1; i++) { diff --git a/UI/Studio/RegisteredTagInput.svelte b/UI/Studio/RegisteredTagInput.svelte new file mode 100644 index 000000000..8b00d8903 --- /dev/null +++ b/UI/Studio/RegisteredTagInput.svelte @@ -0,0 +1,18 @@ + + + diff --git a/UI/Studio/SchemaBasedInput.svelte b/UI/Studio/SchemaBasedInput.svelte index aa176ad37..1ca1c2ad5 100644 --- a/UI/Studio/SchemaBasedInput.svelte +++ b/UI/Studio/SchemaBasedInput.svelte @@ -4,17 +4,21 @@ import EditLayerState from "./EditLayerState"; import SchemaBasedArray from "./SchemaBasedArray.svelte"; import SchemaBaseMultiType from "./SchemaBaseMultiType.svelte"; + import RegisteredTagInput from "./RegisteredTagInput.svelte"; export let schema: ConfigMeta export let state: EditLayerState export let path: (string | number)[] = [] + {#if schema.type === "array"} +{:else if schema.hints.typehint === "tag"} + {:else if schema.hints.types} - + {:else} {/if} diff --git a/UI/Studio/TagExpression.svelte b/UI/Studio/TagExpression.svelte new file mode 100644 index 000000000..3f8527a75 --- /dev/null +++ b/UI/Studio/TagExpression.svelte @@ -0,0 +1,95 @@ + + + +
+ + +
+ {#each $basicTags as basicTag} + + {/each} + {#each $expressions as expression} + + {/each} +
+ + +
+
+ +
diff --git a/UI/Studio/TagInfoStats.svelte b/UI/Studio/TagInfoStats.svelte new file mode 100644 index 000000000..06da9f4c1 --- /dev/null +++ b/UI/Studio/TagInfoStats.svelte @@ -0,0 +1,60 @@ + + +{#if $tagStabilized !== $tag} + +{:else if $tagInfoStats } + + {$total} features on OSM have this tag + +{/if} diff --git a/UI/Studio/TagInput/BasicTagInput.svelte b/UI/Studio/TagInput/BasicTagInput.svelte new file mode 100644 index 000000000..4a7f44a02 --- /dev/null +++ b/UI/Studio/TagInput/BasicTagInput.svelte @@ -0,0 +1,98 @@ + + + +
+ + + + + + {#if $feedbackKey} + + {:else if $feedbackValue} + + {:else if $feedbackGlobal} + + {/if} + + +
diff --git a/UI/Studio/TagInput/TagInput.svelte b/UI/Studio/TagInput/TagInput.svelte new file mode 100644 index 000000000..bd8832f1f --- /dev/null +++ b/UI/Studio/TagInput/TagInput.svelte @@ -0,0 +1,16 @@ + + +
+ +
diff --git a/assets/layerconfigmeta.json b/assets/layerconfigmeta.json index a46ed5646..ca6cc33ea 100644 --- a/assets/layerconfigmeta.json +++ b/assets/layerconfigmeta.json @@ -73,7 +73,7 @@ "properties": { "osmTags": { "$ref": "#/definitions/TagConfigJson", - "description": "question: Which tags must be present on the feature to show it in this layer?\nEvery source must set which tags have to be present in order to load the given layer." + "description": "question: Which tags must be present on the feature to show it in this layer?\n\n Every source must set which tags have to be present in order to load the given layer." }, "maxCacheAge": { "description": "question: How long (in seconds) is the data allowed to remain cached until it must be refreshed?\nThe maximum amount of seconds that a tile is allowed to linger in the cache\n\ntype: nat", diff --git a/assets/layoutconfigmeta.json b/assets/layoutconfigmeta.json index 03acc73b1..32bfaa026 100644 --- a/assets/layoutconfigmeta.json +++ b/assets/layoutconfigmeta.json @@ -320,7 +320,7 @@ "properties": { "osmTags": { "$ref": "#/definitions/TagConfigJson", - "description": "question: Which tags must be present on the feature to show it in this layer?\nEvery source must set which tags have to be present in order to load the given layer." + "description": "question: Which tags must be present on the feature to show it in this layer?\n\n Every source must set which tags have to be present in order to load the given layer." }, "maxCacheAge": { "description": "question: How long (in seconds) is the data allowed to remain cached until it must be refreshed?\nThe maximum amount of seconds that a tile is allowed to linger in the cache\n\ntype: nat", @@ -33700,7 +33700,7 @@ "properties": { "osmTags": { "$ref": "#/definitions/TagConfigJson", - "description": "question: Which tags must be present on the feature to show it in this layer?\nEvery source must set which tags have to be present in order to load the given layer." + "description": "question: Which tags must be present on the feature to show it in this layer?\n\n Every source must set which tags have to be present in order to load the given layer." }, "maxCacheAge": { "description": "question: How long (in seconds) is the data allowed to remain cached until it must be refreshed?\nThe maximum amount of seconds that a tile is allowed to linger in the cache\n\ntype: nat", diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 5d9d03809..73588f0cf 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -1589,10 +1589,6 @@ video { border-width: 2px; } -.border-8 { - border-width: 8px; -} - .border-x { border-left-width: 1px; border-right-width: 1px; @@ -1611,6 +1607,10 @@ video { border-bottom-width: 2px; } +.border-l-4 { + border-left-width: 4px; +} + .border-t { border-top-width: 1px; } diff --git a/scripts/generateDocs.ts b/scripts/generateDocs.ts index ed2e0c639..e3b48db80 100644 --- a/scripts/generateDocs.ts +++ b/scripts/generateDocs.ts @@ -25,10 +25,12 @@ import DependencyCalculator from "../Models/ThemeConfig/DependencyCalculator" import { AllSharedLayers } from "../Customizations/AllSharedLayers" import ThemeViewState from "../Models/ThemeViewState" import Validators from "../UI/InputElement/Validators" +import { TagUtils } from "../Logic/Tags/TagUtils" +import { Utils } from "../Utils" function WriteFile( filename, - html: BaseUIElement, + html: string | BaseUIElement, autogenSource: string[], options?: { noTableOfContents: boolean @@ -106,7 +108,7 @@ function GenerateDocumentationForTheme(theme: LayoutConfig): BaseUIElement { function GenLayerOverviewText(): BaseUIElement { for (const id of Constants.priviliged_layers) { if (!AllSharedLayers.sharedLayers.has(id)) { - throw "Priviliged layer definition not found: " + id + console.error("Priviliged layer definition not found: " + id) } } @@ -149,17 +151,17 @@ function GenLayerOverviewText(): BaseUIElement { "MapComplete has a few data layers available in the theme which have special properties through builtin-hooks. Furthermore, there are some normal layers (which are built from normal Theme-config files) but are so general that they get a mention here.", new Title("Priviliged layers", 1), new List(Constants.priviliged_layers.map((id) => "[" + id + "](#" + id + ")")), - ...Constants.priviliged_layers - .map((id) => AllSharedLayers.sharedLayers.get(id)) - .map((l) => - l.GenerateDocumentation( - themesPerLayer.get(l.id), - layerIsNeededBy, - DependencyCalculator.getLayerDependencies(l), - Constants.added_by_default.indexOf(l.id) >= 0, - Constants.no_include.indexOf(l.id) < 0 - ) - ), + ...Utils.NoNull( + Constants.priviliged_layers.map((id) => AllSharedLayers.sharedLayers.get(id)) + ).map((l) => + l.GenerateDocumentation( + themesPerLayer.get(l.id), + layerIsNeededBy, + DependencyCalculator.getLayerDependencies(l), + Constants.added_by_default.indexOf(l.id) >= 0, + Constants.no_include.indexOf(l.id) < 0 + ) + ), new Title("Normal layers", 1), "The following layers are included in MapComplete:", new List( @@ -350,6 +352,7 @@ WriteFile("./Docs/BuiltinQuestions.md", SharedTagRenderings.HelpText(), [ "Customizations/SharedTagRenderings.ts", "assets/tagRenderings/questions.json", ]) +WriteFile("./Docs/Tags_format.md", TagUtils.generateDocs(), ["Logic/Tags/TagUtils.ts"]) { // Generate the builtinIndex which shows interlayer dependencies diff --git a/test.ts b/test.ts index 36ce24a74..048de0835 100644 --- a/test.ts +++ b/test.ts @@ -3,13 +3,12 @@ import * as theme from "./assets/generated/themes/bookcases.json" import ThemeViewState from "./Models/ThemeViewState" import Combine from "./UI/Base/Combine" import SpecialVisualizations from "./UI/SpecialVisualizations" -import ValidatedInput from "./UI/InputElement/ValidatedInput.svelte" import SvelteUIElement from "./UI/Base/SvelteUIElement" +import TagInput from "./UI/Studio/TagInput/TagInput.svelte" import { UIEventSource } from "./Logic/UIEventSource" -import { Unit } from "./Models/Unit" -import { Denomination } from "./Models/Denomination" +import { TagsFilter } from "./Logic/Tags/TagsFilter" import { VariableUiElement } from "./UI/Base/VariableUIElement" -import { FixedUiElement } from "./UI/Base/FixedUiElement" +import { TagConfigJson } from "./Models/ThemeConfig/Json/TagConfigJson" function testspecial() { const layout = new LayoutConfig(theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data) @@ -21,6 +20,9 @@ function testspecial() { new Combine(all).AttachTo("maindiv") } +const tag = new UIEventSource(undefined) +new SvelteUIElement(TagInput, { tag }).AttachTo("maindiv") +new VariableUiElement(tag.map((t) => JSON.stringify(t))).AttachTo("extradiv") /*/ testspecial() //*/