From 966fcda8d1387998d248831dd8b539ca13f55424 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 22 Jun 2021 03:16:45 +0200 Subject: [PATCH] Add support for units to clean up tags when they enter mapcomplete; add example of this usage in the climbing theme, add climbing theme title icons with length and needed number of carabiners --- Customizations/AllKnownLayers.ts | 6 +- .../JSON/{Unit.ts => Denomination.ts} | 79 ++++++++++++++++--- Customizations/JSON/LayerConfig.ts | 4 + Customizations/JSON/LayerConfigJson.ts | 3 +- Customizations/JSON/LayoutConfig.ts | 34 ++++---- Logic/SimpleMetaTagger.ts | 15 ++-- Models/Constants.ts | 2 +- UI/Input/CombinedInputElement.ts | 42 +++++++--- UI/Input/ValidatedTextField.ts | 5 +- UI/Popup/EditableTagRendering.ts | 4 +- UI/Popup/FeatureInfoBox.ts | 8 +- UI/Popup/QuestionBox.ts | 12 +-- UI/Popup/TagRenderingQuestion.ts | 50 +++++++++--- UI/SpecialVisualizations.ts | 64 ++++++++++----- assets/themes/climbing/climbing.json | 52 ++++++++---- assets/themes/climbing/license_info.json | 18 +++++ scripts/fixTheme.ts | 3 +- scripts/generateLayerOverview.ts | 5 +- test.ts | 3 +- test/Units.spec.ts | 4 +- 20 files changed, 302 insertions(+), 111 deletions(-) rename Customizations/JSON/{Unit.ts => Denomination.ts} (51%) diff --git a/Customizations/AllKnownLayers.ts b/Customizations/AllKnownLayers.ts index ff92d2e5cb..2e5ba33915 100644 --- a/Customizations/AllKnownLayers.ts +++ b/Customizations/AllKnownLayers.ts @@ -8,12 +8,14 @@ export default class AllKnownLayers { // Must be below the list... public static sharedLayers: Map = AllKnownLayers.getSharedLayers(); public static sharedLayersJson: Map = AllKnownLayers.getSharedLayersJson(); + + public static sharedUnits: any[] = [] private static getSharedLayers(): Map { const sharedLayers = new Map(); for (const layer of known_layers.layers) { try { - const parsed = new LayerConfig(layer, "shared_layers") + const parsed = new LayerConfig(layer, AllKnownLayers.sharedUnits,"shared_layers") sharedLayers.set(layer.id, parsed); sharedLayers[layer.id] = parsed; } catch (e) { @@ -33,7 +35,7 @@ export default class AllKnownLayers { continue; } try { - const parsed = new LayerConfig(layer, "shared_layer_in_theme") + const parsed = new LayerConfig(layer, AllKnownLayers.sharedUnits ,"shared_layer_in_theme") sharedLayers.set(layer.id, parsed); sharedLayers[layer.id] = parsed; } catch (e) { diff --git a/Customizations/JSON/Unit.ts b/Customizations/JSON/Denomination.ts similarity index 51% rename from Customizations/JSON/Unit.ts rename to Customizations/JSON/Denomination.ts index 5a8c49a095..9ecdf4c534 100644 --- a/Customizations/JSON/Unit.ts +++ b/Customizations/JSON/Denomination.ts @@ -1,13 +1,59 @@ import {Translation} from "../../UI/i18n/Translation"; import UnitConfigJson from "./UnitConfigJson"; import Translations from "../../UI/i18n/Translations"; +import BaseUIElement from "../../UI/BaseUIElement"; +import Combine from "../../UI/Base/Combine"; export class Unit { - public readonly human: Translation; + public readonly appliesToKeys: Set; + public readonly denominations : Denomination[]; + public readonly defaultDenom: Denomination; + constructor(appliesToKeys: string[], applicableUnits: Denomination[]) { + this.appliesToKeys = new Set( appliesToKeys); + this.denominations = applicableUnits; +this.defaultDenom = applicableUnits.filter(denom => denom.default)[0] + } + + isApplicableToKey(key: string | undefined) : boolean { + if(key === undefined){ + return false; + } + + return this.appliesToKeys.has(key); + } + + /** + * Finds which denomination is applicable and gives the stripped value back + */ + findDenomination(valueWithDenom: string) : [string, Denomination] { + for (const denomination of this.denominations) { + const bare = denomination.StrippedValue(valueWithDenom) + if(bare !== null){ + return [bare, denomination] + } + } + return [undefined, undefined] + } + + asHumanLongValue(value: string): BaseUIElement { + if(value === undefined){ + return undefined; + } + const [stripped, denom] = this.findDenomination(value) + const human = denom.human + + const elems = denom.prefix ? [human, stripped] : [stripped , human]; + return new Combine(elems) + + } +} + +export class Denomination { + private readonly _human: Translation; private readonly alternativeDenominations: string []; - private readonly canonical: string; - private readonly default: boolean; - private readonly prefix: boolean; + public readonly canonical: string; + readonly default: boolean; + readonly prefix: boolean; constructor(json: UnitConfigJson, context: string) { context = `${context}.unit(${json.canonicalDenomination})` @@ -26,15 +72,22 @@ export class Unit { this.default = json.default ?? false; - this.human = Translations.T(json.human, context + "human") + this._human = Translations.T(json.human, context + "human") this.prefix = json.prefix ?? false; } + + get human() : Translation { + return this._human.Clone() + } - public canonicalValue(value: string) { - const stripped = this.StrippedValue(value) - if(stripped === null){ + public canonicalValue(value: string, actAsDefault?: boolean) { + if(value === undefined){ + return undefined; + } + const stripped = this.StrippedValue(value, actAsDefault) + if (stripped === null) { return null; } return stripped + this.canonical @@ -46,11 +99,13 @@ export class Unit { * - the value is a Number (without unit) and default is set * * Returns null if it doesn't match this unit - * @param value - * @constructor */ - private StrippedValue(value: string): string { + public StrippedValue(value: string, actAsDefault?: boolean): string { + if(value === undefined){ + return undefined; + } + if (this.prefix) { if (value.startsWith(this.canonical)) { return value.substring(this.canonical.length).trim(); @@ -72,7 +127,7 @@ export class Unit { } - if (this.default) { + if (this.default || actAsDefault) { const parsed = Number(value.trim()) if (!isNaN(parsed)) { return value.trim(); diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index 10d8ddfe1b..a95e0e319e 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -17,6 +17,7 @@ import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import {Tag} from "../../Logic/Tags/Tag"; import SubstitutingTag from "../../Logic/Tags/SubstitutingTag"; import BaseUIElement from "../../UI/BaseUIElement"; +import {Denomination, Unit} from "./Denomination"; export default class LayerConfig { @@ -46,6 +47,7 @@ export default class LayerConfig { width: TagRenderingConfig; dashArray: TagRenderingConfig; wayHandling: number; + public readonly units: Unit[]; presets: { title: Translation, @@ -56,8 +58,10 @@ export default class LayerConfig { tagRenderings: TagRenderingConfig []; constructor(json: LayerConfigJson, + units:Unit[], context?: string, official: boolean = true,) { + this.units = units; context = context + "." + json.id; const self = this; this.id = json.id; diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts index 924a218a6a..3564699b97 100644 --- a/Customizations/JSON/LayerConfigJson.ts +++ b/Customizations/JSON/LayerConfigJson.ts @@ -109,7 +109,8 @@ export interface LayerConfigJson { /** * Small icons shown next to the title. * If not specified, the OsmLink and wikipedia links will be used by default. - * Use an empty array to hide them + * Use an empty array to hide them. + * Note that "defaults" will insert all the default titleIcons */ titleIcons?: (string | TagRenderingConfigJson)[]; diff --git a/Customizations/JSON/LayoutConfig.ts b/Customizations/JSON/LayoutConfig.ts index df4e8a8afa..77487a6631 100644 --- a/Customizations/JSON/LayoutConfig.ts +++ b/Customizations/JSON/LayoutConfig.ts @@ -5,7 +5,7 @@ import {LayoutConfigJson} from "./LayoutConfigJson"; import AllKnownLayers from "../AllKnownLayers"; import SharedTagRenderings from "../SharedTagRenderings"; import {Utils} from "../../Utils"; -import {Unit} from "./Unit"; +import {Denomination, Unit} from "./Denomination"; export default class LayoutConfig { public readonly id: string; @@ -47,7 +47,7 @@ export default class LayoutConfig { How long is the cache valid, in seconds? */ public readonly cacheTimeout?: number; - public readonly units: { appliesToKeys: Set, applicableUnits: Unit[] }[] = [] + public readonly units: Unit[] = [] private readonly _official: boolean; constructor(json: LayoutConfigJson, official = true, context?: string) { @@ -73,6 +73,7 @@ export default class LayoutConfig { if (json.description === undefined) { throw "Description not defined in " + this.id; } + this.units = LayoutConfig.ExtractUnits(json, context); this.title = new Translation(json.title, context + ".title"); this.description = new Translation(json.description, context + ".description"); this.shortDescription = json.shortDescription === undefined ? this.description.FirstSentence() : new Translation(json.shortDescription, context + ".shortdescription"); @@ -98,7 +99,7 @@ export default class LayoutConfig { if (AllKnownLayers.sharedLayersJson[layer] !== undefined) { if (json.overrideAll !== undefined) { let lyr = JSON.parse(JSON.stringify(AllKnownLayers.sharedLayersJson[layer])); - return new LayerConfig(Utils.Merge(json.overrideAll, lyr), `${this.id}+overrideAll.layers[${i}]`, official); + return new LayerConfig(Utils.Merge(json.overrideAll, lyr), this.units,`${this.id}+overrideAll.layers[${i}]`, official); } else { return AllKnownLayers.sharedLayers[layer] } @@ -124,7 +125,7 @@ export default class LayoutConfig { } // @ts-ignore - return new LayerConfig(layer, `${this.id}.layers[${i}]`, official) + return new LayerConfig(layer, this.units, `${this.id}.layers[${i}]`, official) }); // ALl the layers are constructed, let them share tags in now! @@ -187,6 +188,10 @@ export default class LayoutConfig { this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60) + } + + private static ExtractUnits(json: LayoutConfigJson, context: string) : Unit[]{ + const result: Unit[] = [] if ((json.units ?? []).length !== 0) { for (let i1 = 0; i1 < json.units.length; i1++) { let unit = json.units[i1]; @@ -206,30 +211,31 @@ export default class LayoutConfig { const defaultSet = unit.applicableUnits.filter(u => u.default === true) // No default is defined - we pick the first as default - if(defaultSet.length === 0){ - unit.applicableUnits[0].default = true + if (defaultSet.length === 0) { + unit.applicableUnits[0].default = true } - + // Check that there are not multiple defaults if (defaultSet.length > 1) { throw `Multiple units are set as default: they have canonical values of ${defaultSet.map(u => u.canonicalDenomination).join(", ")}` } - const applicable = unit.applicableUnits.map((u, i) => new Unit(u, `${context}.units[${i}]`)) - this.units.push({ - appliesToKeys: new Set(appliesTo), - applicableUnits: applicable - }) + const applicable = unit.applicableUnits.map((u, i) => new Denomination(u, `${context}.units[${i}]`)) + result.push(new Unit( appliesTo, applicable)); } const seenKeys = new Set() - for (const unit of this.units) { - const alreadySeen = Array.from(unit.appliesToKeys).filter(key => seenKeys.has(key)); + for (const unit of result) { + const alreadySeen = Array.from(unit.appliesToKeys).filter((key: string) => seenKeys.has(key)); if (alreadySeen.length > 0) { throw `${context}.units: multiple units define the same keys. The key(s) ${alreadySeen.join(",")} occur multiple times` } unit.appliesToKeys.forEach(key => seenKeys.add(key)) } + return result; + } + + } public CustomCodeSnippets(): string[] { diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index 0698637ab9..6d9491aeda 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -85,7 +85,7 @@ export default class SimpleMetaTagger { (feature => { const units = State.state.layoutToUse.data.units ?? []; for (const key in feature.properties) { - if(!feature.properties.hasOwnProperty(key)){ + if (!feature.properties.hasOwnProperty(key)) { continue; } for (const unit of units) { @@ -93,15 +93,10 @@ export default class SimpleMetaTagger { continue; } const value = feature.properties[key] - - for (const applicableUnit of unit.applicableUnits) { - const canonical = applicableUnit.canonicalValue(value) - if (canonical == null) { - continue - } - console.log("Rewritten ", key, " from", value, "into", canonical) - feature.properties[key] = canonical; - } + const [, denomination] = unit.findDenomination(value) + const canonical = denomination.canonicalValue(value) + console.log("Rewritten ", key, " from", value, "into", canonical) + feature.properties[key] = canonical; } } diff --git a/Models/Constants.ts b/Models/Constants.ts index e572076d86..a8d6f58a4a 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.8.0a"; + public static vNumber = "0.8.1"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/UI/Input/CombinedInputElement.ts b/UI/Input/CombinedInputElement.ts index 83672e9a6b..38d3fc7bcd 100644 --- a/UI/Input/CombinedInputElement.ts +++ b/UI/Input/CombinedInputElement.ts @@ -3,30 +3,48 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import Combine from "../Base/Combine"; import BaseUIElement from "../BaseUIElement"; -export default class CombinedInputElement extends InputElement { - protected InnerConstructElement(): HTMLElement { - return this._combined.ConstructElement(); - } - private readonly _a: InputElement; - private readonly _b: BaseUIElement; - private readonly _combined: BaseUIElement; +export default class CombinedInputElement extends InputElement { + public readonly IsSelected: UIEventSource; - constructor(a: InputElement, b: InputElement) { + private readonly _a: InputElement; + private readonly _b: InputElement; + private readonly _combined: BaseUIElement; + private readonly _value: UIEventSource + private readonly _split: (x: X) => [T, J]; + + constructor(a: InputElement, b: InputElement, + combine: (t: T, j: J) => X, + split: (x: X) => [T, J]) { super(); this._a = a; this._b = b; + this._split = split; this.IsSelected = this._a.IsSelected.map((isSelected) => { return isSelected || b.IsSelected.data }, [b.IsSelected]) this._combined = new Combine([this._a, this._b]); + this._value = this._a.GetValue().map( + t => combine(t, this._b.GetValue().data), + [this._b.GetValue()], + ) + .addCallback(x => { + const [t, j] = split(x) + this._a.GetValue().setData(t) + this._b.GetValue().setData(j) + }) } - GetValue(): UIEventSource { - return this._a.GetValue(); + GetValue(): UIEventSource { + return this._value; } - IsValid(t: T): boolean { - return this._a.IsValid(t); + IsValid(x: X): boolean { + const [t, j] = this._split(x) + return this._a.IsValid(t) && this._b.IsValid(j); + } + + protected InnerConstructElement(): HTMLElement { + return this._combined.ConstructElement(); } } \ No newline at end of file diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 51b24b2e4b..0471e660eb 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -270,7 +270,10 @@ export default class ValidatedTextField { if (tp.inputHelper) { input = new CombinedInputElement(input, tp.inputHelper(input.GetValue(), { location: options.location - })); + }), + (a, b) => a, // We can ignore b, as they are linked earlier + a => [a, a] + ); } return input; } diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts index 85952d9a61..10ac7be1aa 100644 --- a/UI/Popup/EditableTagRendering.ts +++ b/UI/Popup/EditableTagRendering.ts @@ -8,11 +8,13 @@ import State from "../../State"; import Svg from "../../Svg"; import Toggle from "../Input/Toggle"; import BaseUIElement from "../BaseUIElement"; +import {Unit} from "../../Customizations/JSON/Denomination"; export default class EditableTagRendering extends Toggle { constructor(tags: UIEventSource, configuration: TagRenderingConfig, + units: Unit [], editMode = new UIEventSource(false) ) { const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration) @@ -41,7 +43,7 @@ export default class EditableTagRendering extends Toggle { editMode.setData(false) }); - const question = new TagRenderingQuestion(tags, configuration, + const question = new TagRenderingQuestion(tags, configuration,units, () => { editMode.setData(false) }, diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 7f375d298a..7055bfaad7 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -17,7 +17,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { public constructor( tags: UIEventSource, - layerConfig: LayerConfig + layerConfig: LayerConfig, ) { super(() => FeatureInfoBox.GenerateTitleBar(tags, layerConfig), () => FeatureInfoBox.GenerateContent(tags, layerConfig), @@ -35,7 +35,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { .SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2"); const titleIcons = new Combine( layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon, - "block w-8 h-8 align-baseline box-content sm:p-0.5", "width: 2rem !important;") + "block w-8 h-8 align-baseline box-content sm:p-0.5") )) .SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2") @@ -49,7 +49,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { let questionBox: UIElement = undefined; if (State.state.featureSwitchUserbadge.data) { - questionBox = new QuestionBox(tags, layerConfig.tagRenderings); + questionBox = new QuestionBox(tags, layerConfig.tagRenderings, layerConfig.units); } let questionBoxIsUsed = false; @@ -59,7 +59,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { questionBoxIsUsed = true; return questionBox; } - return new EditableTagRendering(tags, tr); + return new EditableTagRendering(tags, tr, layerConfig.units); }); if (!questionBoxIsUsed) { renderings.push(questionBox); diff --git a/UI/Popup/QuestionBox.ts b/UI/Popup/QuestionBox.ts index 76e8f8ed32..0dc845a1c7 100644 --- a/UI/Popup/QuestionBox.ts +++ b/UI/Popup/QuestionBox.ts @@ -5,6 +5,8 @@ import TagRenderingQuestion from "./TagRenderingQuestion"; import Translations from "../i18n/Translations"; import State from "../../State"; import Combine from "../Base/Combine"; +import BaseUIElement from "../BaseUIElement"; +import {Unit} from "../../Customizations/JSON/Denomination"; /** @@ -14,12 +16,12 @@ export default class QuestionBox extends UIElement { private readonly _tags: UIEventSource; private readonly _tagRenderings: TagRenderingConfig[]; - private _tagRenderingQuestions: UIElement[]; + private _tagRenderingQuestions: BaseUIElement[]; private _skippedQuestions: UIEventSource = new UIEventSource([]) - private _skippedQuestionsButton: UIElement; + private _skippedQuestionsButton: BaseUIElement; - constructor(tags: UIEventSource, tagRenderings: TagRenderingConfig[]) { + constructor(tags: UIEventSource, tagRenderings: TagRenderingConfig[], units: Unit[]) { super(tags); this.ListenTo(this._skippedQuestions); this._tags = tags; @@ -28,7 +30,7 @@ export default class QuestionBox extends UIElement { .filter(tr => tr.question !== undefined) .filter(tr => tr.question !== null); this._tagRenderingQuestions = this._tagRenderings - .map((tagRendering, i) => new TagRenderingQuestion(this._tags, tagRendering, + .map((tagRendering, i) => new TagRenderingQuestion(this._tags, tagRendering,units, () => { // We save self._skippedQuestions.ping(); @@ -49,7 +51,7 @@ export default class QuestionBox extends UIElement { } InnerRender() { - const allQuestions : UIElement[] = [] + const allQuestions : BaseUIElement[] = [] for (let i = 0; i < this._tagRenderingQuestions.length; i++) { let tagRendering = this._tagRenderings[i]; diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 2b6dd7bc46..91a74a1f9a 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -24,6 +24,8 @@ import {And} from "../../Logic/Tags/And"; import {TagUtils} from "../../Logic/Tags/TagUtils"; import BaseUIElement from "../BaseUIElement"; import {DropDown} from "../Input/DropDown"; +import {Unit} from "../../Customizations/JSON/Denomination"; +import CombinedInputElement from "../Input/CombinedInputElement"; /** * Shows the question element. @@ -38,14 +40,17 @@ export default class TagRenderingQuestion extends UIElement { private _inputElement: InputElement; private _cancelButton: BaseUIElement; private _appliedTags: BaseUIElement; + private readonly _applicableUnit: Unit; private _question: BaseUIElement; constructor(tags: UIEventSource, configuration: TagRenderingConfig, + units: Unit[], afterSave?: () => void, cancelButton?: BaseUIElement ) { super(tags); + this._applicableUnit = units.filter(unit => unit.isApplicableToKey(configuration.freeform?.key))[0]; this._tags = tags; this._configuration = configuration; this._cancelButton = cancelButton; @@ -114,9 +119,9 @@ export default class TagRenderingQuestion extends UIElement { const self = this; let inputEls: InputElement[]; - const mappings = (this._configuration.mappings??[]) - .filter( mapping => { - if(mapping.hideInAnswer === true){ + const mappings = (this._configuration.mappings ?? []) + .filter(mapping => { + if (mapping.hideInAnswer === true) { return false; } if (typeof (mapping.hideInAnswer) !== "boolean" && mapping.hideInAnswer.matchesProperties(this._tags.data)) { @@ -124,9 +129,9 @@ export default class TagRenderingQuestion extends UIElement { } return true; }) - - - let allIfNots: TagsFilter[] = Utils.NoNull(this._configuration.mappings?.map(m => m.ifnot) ?? [] ); + + + let allIfNots: TagsFilter[] = Utils.NoNull(this._configuration.mappings?.map(m => m.ifnot) ?? []); const ff = this.GenerateFreeform(); const hasImages = mappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0 @@ -272,7 +277,7 @@ export default class TagRenderingQuestion extends UIElement { then: Translation, hideInAnswer: boolean | TagsFilter }, ifNot?: TagsFilter[]): InputElement { - + let tagging = mapping.if; if (ifNot.length > 0) { tagging = new And([tagging, ...ifNot]) @@ -323,16 +328,41 @@ export default class TagRenderingQuestion extends UIElement { return undefined; } - const textField = ValidatedTextField.InputForType(this._configuration.freeform.type, { + let input: InputElement = ValidatedTextField.InputForType(this._configuration.freeform.type, { isValid: (str) => (str.length <= 255), country: () => this._tags.data._country, location: [this._tags.data._lat, this._tags.data._lon] }); - textField.GetValue().setData(this._tags.data[this._configuration.freeform.key]); + if (this._applicableUnit) { + // We need to apply a unit. + // This implies: + // We have to create a dropdown with applicable denominations, and fuse those values + const unit = this._applicableUnit + const unitDropDown = new DropDown("", + unit.denominations.map(denom => { + return { + shown: denom.human, + value: denom + } + }) + ) + unitDropDown.GetValue().setData(this._applicableUnit.defaultDenom) + unitDropDown.SetStyle("width: min-content") + + input = new CombinedInputElement( + input, + unitDropDown, + (text, denom) => denom?.canonicalValue(text, true) ?? text, + (valueWithDenom: string) => unit.findDenomination(valueWithDenom) + ).SetClass("flex") + } + + + input.GetValue().setData(this._tags.data[this._configuration.freeform.key]); return new InputElementMap( - textField, (a, b) => a === b || (a?.isEquivalent(b) ?? false), + input, (a, b) => a === b || (a?.isEquivalent(b) ?? false), pickString, toString ); diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 1ee996af19..b1f34f7576 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -33,23 +33,24 @@ export default class SpecialVisualizations { args: { name: string, defaultValue?: string, doc: string }[] }[] = - [{ - funcName: "all_tags", - docs: "Prints all key-value pairs of the object - used for debugging", - args: [], - constr: ((state: State, tags: UIEventSource) => { - return new VariableUiElement(tags.map(tags => { - const parts = []; - for (const key in tags) { - if (!tags.hasOwnProperty(key)) { - continue; + [ + { + funcName: "all_tags", + docs: "Prints all key-value pairs of the object - used for debugging", + args: [], + constr: ((state: State, tags: UIEventSource) => { + return new VariableUiElement(tags.map(tags => { + const parts = []; + for (const key in tags) { + if (!tags.hasOwnProperty(key)) { + continue; + } + parts.push(key + "=" + tags[key]); } - parts.push(key + "=" + tags[key]); - } - return parts.join("
") - })).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;") - }) - }, + return parts.join("
") + })).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;") + }) + }, { funcName: "image_carousel", @@ -252,13 +253,40 @@ export default class SpecialVisualizations { } } - return new ShareButton(Svg.share_ui(), generateShareData) + return new ShareButton(Svg.share_svg().SetClass("w-8 h-8"), generateShareData) } else { return new FixedUiElement("") } } - } + }, + {funcName: "canonical", + docs: "Converts a short, canonical value into the long, translated text", + example: "{canonical(length)} will give 42 metre (in french)", + args:[{ + name:"key", + doc: "The key of the tag to give the canonical text for" + }], + constr: (state, tagSource, args) => { + const key = args [0] + return new VariableUiElement( + tagSource.map(tags => tags[key]).map(value => { + if(value === undefined){ + return undefined + } + const unit = state.layoutToUse.data.units.filter(unit => unit.isApplicableToKey(key))[0] + if(unit === undefined){ + return value; + } + + return unit.asHumanLongValue(value); + + }, + [ state.layoutToUse]) + + + ) + }} ] static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage(); diff --git a/assets/themes/climbing/climbing.json b/assets/themes/climbing/climbing.json index 065b0eae5b..17b0925d83 100644 --- a/assets/themes/climbing/climbing.json +++ b/assets/themes/climbing/climbing.json @@ -323,6 +323,7 @@ } ] }, + "tagRenderings": [ "images", "questions", @@ -371,11 +372,11 @@ "nl": "Hoe lang is deze klimroute (in meters)?" }, "render": { - "de": "Diese Route ist {climbing:length} Meter lang", - "en": "This route is {climbing:length} meter long", - "nl": "Deze klimroute is {climbing:length} meter lang", - "ja": "このルート長は、 {climbing:length} メーターです", - "nb_NO": "Denne ruten er {climbing:length} meter lang" + "de": "Diese Route ist {canonical(climbing:length)} lang", + "en": "This route is {canonical(climbing:length)} long", + "nl": "Deze klimroute is {canonical(climbing:length)} lang", + "ja": "このルート長は、 {canonical(climbing:length)} メーターです", + "nb_NO": "Denne ruten er {canonical(climbing:length)} lang" }, "freeform": { "key": "climbing:length", @@ -827,10 +828,17 @@ "canonicalDenomination": "m", "alternativeDenomination": ["meter","meters"], "human": { - "en": "meter", - "nl": "meter" + "en": " meter", + "nl": " meter" }, "default": true + },{ + "canonicalDenomination": "ft", + "alternativeDenomination": ["feet","voet"], + "human": { + "en": " feet", + "nl": " voet" + } }] } ], @@ -955,10 +963,10 @@ { "#": "Avg length?", "render": { - "de": "Die Routen sind durchschnittlich {climbing:length}m lang", - "en": "The routes are {climbing:length}m long on average", - "nl": "De klimroutes zijn gemiddeld {climbing:length}m lang", - "ja": "ルートの長さは平均で{climbing:length} mです" + "de": "Die Routen sind durchschnittlich {canonical(climbing:length)} lang", + "en": "The routes are {canonical(climbing:length)} long on average", + "nl": "De klimroutes zijn gemiddeld {canonical(climbing:length)} lang", + "ja": "ルートの長さは平均で{canonical(climbing:length)}です" }, "condition": { "and": [ @@ -1321,12 +1329,28 @@ } ], "overrideAll": { + "titleIcons": [ + { + "render": "
{climbing:length}
", + "freeform": { + "key": "climbing:length" + } + }, + + { + "render": "
{climbing:bolted}
", + "freeform": { + "key": "climbing:bolted" + } + }, + + "defaults"], "+calculatedTags": [ "_embedding_feature_properties=feat.overlapWith('climbing').map(f => f.feat.properties).filter(p => p !== undefined).map(p => {return{access: p.access, id: p.id, name: p.name, climbing: p.climbing, 'access:description': p['access:description']}})", "_embedding_features_with_access=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.access !== undefined)[0]", - "_embedding_feature_with_rock=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.rock !== undefined)[0]", - "_embedding_features_with_rock:rock=JSON.parse(_embedding_feature_with_rock).rock", - "_embedding_features_with_rock:id=JSON.parse(_embedding_feature_with_rock).id", + "_embedding_feature_with_rock=JSON.parse(feat.properties._embedding_feature_properties ?? '[]').filter(p => p.rock !== undefined)[0] ?? '{}'", + "_embedding_features_with_rock:rock=JSON.parse(_embedding_feature_with_rock)?.rock", + "_embedding_features_with_rock:id=JSON.parse(_embedding_feature_with_rock)?.id", "_embedding_feature:access=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').access", "_embedding_feature:access:description=JSON.parse(feat.properties._embedding_features_with_access ?? '{}')['access:description']", "_embedding_feature:id=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').id" diff --git a/assets/themes/climbing/license_info.json b/assets/themes/climbing/license_info.json index e6542e346a..64c3387ff3 100644 --- a/assets/themes/climbing/license_info.json +++ b/assets/themes/climbing/license_info.json @@ -1,4 +1,22 @@ [ + { + "authors": [ + "Matthew Dera" + ], + "path": "carabiner.svg", + "license": "CC-BY-SA 4.0", + "sources": [ + "https://thenounproject.com/term/carabiner/30076/" + ] + }, + { + "authors": [ + "Pieter Vander Vennet" + ], + "path": "height.svg", + "license": "CC0", + "sources": [] + }, { "authors": [ "Polarbear w", diff --git a/scripts/fixTheme.ts b/scripts/fixTheme.ts index b3d0404691..991e56573a 100644 --- a/scripts/fixTheme.ts +++ b/scripts/fixTheme.ts @@ -9,6 +9,7 @@ import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson"; import {Layer} from "leaflet"; import LayerConfig from "../Customizations/JSON/LayerConfig"; import SmallLicense from "../Models/smallLicense"; +import AllKnownLayers from "../Customizations/AllKnownLayers"; if(process.argv.length == 2){ console.log("USAGE: ts-node scripts/fixTheme ") @@ -37,7 +38,7 @@ for (const layerConfigJson of themeConfigJson.layers) { layerConfigJson["source"] = { osmTags : tags} } // @ts-ignore - const layerConfig = new LayerConfig(layerConfigJson, true) + const layerConfig = new LayerConfig(layerConfigJson, AllKnownLayers.sharedUnits, "fix theme",true) const images : string[] = Array.from(layerConfig.ExtractImages()) const remoteImages = images.filter(img => img.startsWith("http")) for (const remoteImage of remoteImages) { diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 748c2919f9..db051e27e4 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -9,6 +9,7 @@ import LayoutConfig from "../Customizations/JSON/LayoutConfig"; import {LayerConfigJson} from "../Customizations/JSON/LayerConfigJson"; import {Translation} from "../UI/i18n/Translation"; import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson"; +import AllKnownLayers from "../Customizations/AllKnownLayers"; // This scripts scans 'assets/layers/*.json' for layer definition files and 'assets/themes/*.json' for theme definition files. // It spits out an overview of those to be used to load them @@ -48,7 +49,7 @@ class LayerOverviewUtils { errorCount.push("Layer " + layerJson.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)") } try { - const layer = new LayerConfig(layerJson, "test", true) + const layer = new LayerConfig(layerJson, AllKnownLayers.sharedUnits,"test", true) const images = Array.from(layer.ExtractImages()) const remoteImages = images.filter(img => img.indexOf("http") == 0) for (const remoteImage of remoteImages) { @@ -153,7 +154,7 @@ class LayerOverviewUtils { for (const layerFile of layerFiles) { layerErrorCount.push(...this.validateLayer(layerFile.parsed, layerFile.path, knownPaths)) - knownLayerIds.set(layerFile.parsed.id, new LayerConfig(layerFile.parsed)) + knownLayerIds.set(layerFile.parsed.id, new LayerConfig(layerFile.parsed,AllKnownLayers.sharedUnits)) } let themeErrorCount = [] diff --git a/test.ts b/test.ts index 1736e2aa98..373849c40c 100644 --- a/test.ts +++ b/test.ts @@ -50,7 +50,8 @@ function TestTagRendering(){ } ], - }, undefined, "test") + }, undefined, "test"), + [] ).AttachTo("maindiv") new VariableUiElement(tagsSource.map(tags => tags["valves"])).SetClass("alert").AttachTo("extradiv") } diff --git a/test/Units.spec.ts b/test/Units.spec.ts index dd05416436..6433881c2d 100644 --- a/test/Units.spec.ts +++ b/test/Units.spec.ts @@ -1,5 +1,5 @@ import T from "./TestHelper"; -import {Unit} from "../Customizations/JSON/Unit"; +import {Denomination} from "../Customizations/JSON/Denomination"; import {equal} from "assert"; export default class UnitsSpec extends T { @@ -8,7 +8,7 @@ export default class UnitsSpec extends T { super("Units", [ ["Simple canonicalize", () => { - const unit = new Unit({ + const unit = new Denomination({ canonicalDenomination: "m", alternativeDenomination: ["meter"], 'default': true,