diff --git a/Logic/Web/TagInfo.ts b/Logic/Web/TagInfo.ts new file mode 100644 index 0000000000..77250ae97f --- /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 16ce856f95..b82a17749e 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 629fd87561..a982b1f757 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 0000000000..5212feb207 --- /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 78f60f05ae..21af6a50ac 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 0000000000..8b00d89031 --- /dev/null +++ b/UI/Studio/RegisteredTagInput.svelte @@ -0,0 +1,18 @@ + + + diff --git a/UI/Studio/SchemaBasedInput.svelte b/UI/Studio/SchemaBasedInput.svelte index aa176ad370..1ca1c2ad50 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 0000000000..3f8527a75e --- /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 0000000000..06da9f4c14 --- /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 0000000000..4a7f44a02e --- /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 0000000000..bd8832f1fa --- /dev/null +++ b/UI/Studio/TagInput/TagInput.svelte @@ -0,0 +1,16 @@ + + +
+ +
diff --git a/assets/layerconfigmeta.json b/assets/layerconfigmeta.json index a46ed5646c..ca6cc33ead 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 03acc73b15..32bfaa0267 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 5d9d038097..73588f0cfd 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 ed2e0c639e..e3b48db800 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 36ce24a744..048de0835e 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() //*/