diff --git a/Models/Constants.ts b/Models/Constants.ts index 6082fed8d1..76075a740e 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.9.10"; + public static vNumber = "0.9.11"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/Models/Denomination.ts b/Models/Denomination.ts index 3ea8900d42..04e4077d7a 100644 --- a/Models/Denomination.ts +++ b/Models/Denomination.ts @@ -1,13 +1,19 @@ import {Translation} from "../UI/i18n/Translation"; import {ApplicableUnitJson} from "./ThemeConfig/Json/UnitConfigJson"; import Translations from "../UI/i18n/Translations"; +import {UIEventSource} from "../Logic/UIEventSource"; +import BaseUIElement from "../UI/BaseUIElement"; +import Toggle from "../UI/Input/Toggle"; export class Denomination { public readonly canonical: string; - readonly default: boolean; - readonly prefix: boolean; + public readonly _canonicalSingular: string; + public readonly default: boolean; + public readonly prefix: boolean; public readonly alternativeDenominations: string []; private readonly _human: Translation; + private readonly _humanSingular?: Translation; + constructor(json: ApplicableUnitJson, context: string) { context = `${context}.unit(${json.canonicalDenomination})` @@ -15,6 +21,8 @@ export class Denomination { if (this.canonical === undefined) { throw `${context}: this unit has no decent canonical value defined` } + this._canonicalSingular = json.canonicalDenominationSingular?.trim() + json.alternativeDenomination.forEach((v, i) => { if (((v?.trim() ?? "") === "")) { @@ -27,6 +35,7 @@ export class Denomination { this.default = json.default ?? false; this._human = Translations.T(json.human, context + "human") + this._humanSingular = Translations.T(json.humanSingular, context + "humanSingular") this.prefix = json.prefix ?? false; @@ -35,7 +44,22 @@ export class Denomination { get human(): Translation { return this._human.Clone() } - + + get humanSingular(): Translation { + return (this._humanSingular ?? this._human).Clone() + } + + getToggledHuman(isSingular: UIEventSource): BaseUIElement{ + if(this._humanSingular === undefined){ + return this.human + } + return new Toggle( + this.humanSingular, + this.human, + isSingular + ) + } + public canonicalValue(value: string, actAsDefault?: boolean) { if (value === undefined) { return undefined; @@ -44,9 +68,12 @@ export class Denomination { if (stripped === null) { return null; } - return (stripped + " " + this.canonical.trim()).trim(); + if(stripped === "1" && this._canonicalSingular !== undefined){ + return "1 "+this._canonicalSingular + } + return stripped + " " + this.canonical; } - + /** * Returns the core value (without unit) if: * - the value ends with the canonical or an alternative value (or begins with if prefix is set) @@ -61,26 +88,36 @@ export class Denomination { } value = value.toLowerCase() - if (this.prefix) { - if (value.startsWith(this.canonical) && this.canonical !== "") { - return value.substring(this.canonical.length).trim(); - } - for (const alternativeValue of this.alternativeDenominations) { - if (value.startsWith(alternativeValue)) { - return value.substring(alternativeValue.length).trim(); - } - } - } else { - if (value.endsWith(this.canonical.toLowerCase()) && this.canonical !== "") { - return value.substring(0, value.length - this.canonical.length).trim(); - } - for (const alternativeValue of this.alternativeDenominations) { - if (value.endsWith(alternativeValue.toLowerCase())) { - return value.substring(0, value.length - alternativeValue.length).trim(); - } + const self = this; + function startsWith(key){ + if(self.prefix){ + return value.startsWith(key) + }else{ + return value.endsWith(key) + } + } + + function substr(key){ + if(self.prefix){ + return value.substr(key.length).trim() + }else{ + return value.substring(0, value.length - key.length).trim() + } + } + + if(this.canonical !== "" && startsWith(this.canonical.toLowerCase())){ + return substr(this.canonical) + } + + if(this._canonicalSingular !== undefined && this._canonicalSingular !== "" && startsWith(this._canonicalSingular)){ + return substr(this._canonicalSingular) + } + + for (const alternativeValue of this.alternativeDenominations) { + if (startsWith(alternativeValue)) { + return substr(alternativeValue); } } - if (this.default || actAsDefault) { const parsed = Number(value.trim()) diff --git a/Models/ThemeConfig/Json/UnitConfigJson.ts b/Models/ThemeConfig/Json/UnitConfigJson.ts index dafb1e5c14..c8c22afc0a 100644 --- a/Models/ThemeConfig/Json/UnitConfigJson.ts +++ b/Models/ThemeConfig/Json/UnitConfigJson.ts @@ -24,7 +24,12 @@ export interface ApplicableUnitJson * If the user inputs '42', the canonical value will be added and it'll become '42m' */ canonicalDenomination: string, - + /** + * The canonical denomination in the case that the unit is precisely '1' + */ + canonicalDenominationSingular?: string, + + /** * A list of alternative values which can occur in the OSM database - used for parsing. */ @@ -39,6 +44,15 @@ export interface ApplicableUnitJson */ human?: string | any + /** + * The value for humans in the dropdown. This should not use abbreviations and should be translated, e.g. + * { + * "en": "minute", + * "nl": "minuut"x² + * } + */ + humanSingular?: string | any + /** * If set, then the canonical value will be prefixed instead, e.g. for '€' * Note that if all values use 'prefix', the dropdown might move to before the text field diff --git a/Models/Unit.ts b/Models/Unit.ts index 1c59a3366d..2512580bc2 100644 --- a/Models/Unit.ts +++ b/Models/Unit.ts @@ -34,10 +34,10 @@ export class Unit { this.denominationsSorted = [...this.denominations] this.denominationsSorted.sort((a, b) => b.canonical.length - a.canonical.length) - const possiblePostFixes = new Set() function addPostfixesOf(str) { + if(str === undefined){return} str = str.toLowerCase() for (let i = 0; i < str.length + 1; i++) { const substr = str.substring(0, i) @@ -47,6 +47,7 @@ export class Unit { for (const denomination of this.denominations) { addPostfixesOf(denomination.canonical) + addPostfixesOf(denomination._canonicalSingular) denomination.alternativeDenominations.forEach(addPostfixesOf) } this.possiblePostFixes = Array.from(possiblePostFixes) @@ -111,7 +112,7 @@ export class Unit { return undefined; } const [stripped, denom] = this.findDenomination(value) - const human = denom?.human + const human = stripped === "1" ? denom?.humanSingular : denom?.human if (human === undefined) { return new FixedUiElement(stripped ?? value); } diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 425ae18d67..17a67de14f 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -361,14 +361,17 @@ export default class ValidatedTextField { // This implies: // We have to create a dropdown with applicable denominations, and fuse those values const unit = options.unit + + + const isSingular = input.GetValue().map(str => str?.trim() === "1") const unitDropDown = unit.denominations.length === 1 ? - new FixedInputElement(unit.denominations[0].human, unit.denominations[0]) + new FixedInputElement( unit.denominations[0].getToggledHuman(isSingular), unit.denominations[0]) : new DropDown("", unit.denominations.map(denom => { return { - shown: denom.human, + shown: denom.getToggledHuman(isSingular), value: denom } }) diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index e4950be2a4..ef6ae16987 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -370,7 +370,8 @@ export default class SpecialVisualizations { if (value === undefined) { return undefined } - const unit = state.layoutToUse.data.units.filter(unit => unit.isApplicableToKey(key))[0] + const allUnits = [].concat(...state.layoutToUse.data.layers.map(lyr => lyr.units)) + const unit = allUnits.filter(unit => unit.isApplicableToKey(key))[0] if (unit === undefined) { return value; } diff --git a/assets/layers/charging_station/charging_station.json b/assets/layers/charging_station/charging_station.json index e69f0259db..3935c515f9 100644 --- a/assets/layers/charging_station/charging_station.json +++ b/assets/layers/charging_station/charging_station.json @@ -344,8 +344,8 @@ "nl": "Hoeveel stekkers van type Schuko stekker zonder aardingspin (CEE7/4 type F) heeft dit oplaadpunt?" }, "render": { - "en": "There are Schuko wall plug without ground pin (CEE7/4 type F) plugs of type [object Map] available here", - "nl": "Hier zijn Schuko stekker zonder aardingspin (CEE7/4 type F) stekkers van het type [object Map]" + "en": "There are Schuko wall plug without ground pin (CEE7/4 type F) plugs of type Schuko wall plug without ground pin (CEE7/4 type F) available here", + "nl": "Hier zijn Schuko stekker zonder aardingspin (CEE7/4 type F) stekkers van het type Schuko stekker zonder aardingspin (CEE7/4 type F)" }, "freeform": { "key": "socket:schuko", @@ -451,8 +451,8 @@ "nl": "Hoeveel stekkers van type Europese stekker met aardingspin (CEE7/4 type E) heeft dit oplaadpunt?" }, "render": { - "en": "There are European wall plug with ground pin (CEE7/4 type E) plugs of type [object Map] available here", - "nl": "Hier zijn Europese stekker met aardingspin (CEE7/4 type E) stekkers van het type [object Map]" + "en": "There are European wall plug with ground pin (CEE7/4 type E) plugs of type European wall plug with ground pin (CEE7/4 type E) available here", + "nl": "Hier zijn Europese stekker met aardingspin (CEE7/4 type E) stekkers van het type Europese stekker met aardingspin (CEE7/4 type E)" }, "freeform": { "key": "socket:typee", @@ -565,8 +565,8 @@ "nl": "Hoeveel stekkers van type heeft dit oplaadpunt?" }, "render": { - "en": "There are Chademo plugs of type [object Map] available here", - "nl": "Hier zijn stekkers van het type [object Map]" + "en": "There are Chademo plugs of type Chademo available here", + "nl": "Hier zijn stekkers van het type " }, "freeform": { "key": "socket:chademo", @@ -672,8 +672,8 @@ "nl": "Hoeveel stekkers van type Type 1 met kabel (J1772) heeft dit oplaadpunt?" }, "render": { - "en": "There are Type 1 with cable (J1772) plugs of type [object Map] available here", - "nl": "Hier zijn Type 1 met kabel (J1772) stekkers van het type [object Map]" + "en": "There are Type 1 with cable (J1772) plugs of type Type 1 with cable (J1772) available here", + "nl": "Hier zijn Type 1 met kabel (J1772) stekkers van het type Type 1 met kabel (J1772)" }, "freeform": { "key": "socket:type1_cable", @@ -793,8 +793,8 @@ "nl": "Hoeveel stekkers van type Type 1 zonder kabel (J1772) heeft dit oplaadpunt?" }, "render": { - "en": "There are Type 1 without cable (J1772) plugs of type [object Map] available here", - "nl": "Hier zijn Type 1 zonder kabel (J1772) stekkers van het type [object Map]" + "en": "There are Type 1 without cable (J1772) plugs of type Type 1 without cable (J1772) available here", + "nl": "Hier zijn Type 1 zonder kabel (J1772) stekkers van het type Type 1 zonder kabel (J1772)" }, "freeform": { "key": "socket:type1", @@ -928,8 +928,8 @@ "nl": "Hoeveel stekkers van type heeft dit oplaadpunt?" }, "render": { - "en": "There are Type 1 CCS (aka Type 1 Combo) plugs of type [object Map] available here", - "nl": "Hier zijn stekkers van het type [object Map]" + "en": "There are Type 1 CCS (aka Type 1 Combo) plugs of type Type 1 CCS (aka Type 1 Combo) available here", + "nl": "Hier zijn stekkers van het type " }, "freeform": { "key": "socket:type1_combo", @@ -1070,8 +1070,8 @@ "nl": "Hoeveel stekkers van type heeft dit oplaadpunt?" }, "render": { - "en": "There are Tesla Supercharger plugs of type [object Map] available here", - "nl": "Hier zijn stekkers van het type [object Map]" + "en": "There are Tesla Supercharger plugs of type Tesla Supercharger available here", + "nl": "Hier zijn stekkers van het type " }, "freeform": { "key": "socket:tesla_supercharger", @@ -1198,8 +1198,8 @@ "nl": "Hoeveel stekkers van type heeft dit oplaadpunt?" }, "render": { - "en": "There are Type 2 (mennekes) plugs of type [object Map] available here", - "nl": "Hier zijn stekkers van het type [object Map]" + "en": "There are Type 2 (mennekes) plugs of type Type 2 (mennekes) available here", + "nl": "Hier zijn stekkers van het type " }, "freeform": { "key": "socket:type2", @@ -1326,8 +1326,8 @@ "nl": "Hoeveel stekkers van type heeft dit oplaadpunt?" }, "render": { - "en": "There are Type 2 CCS (mennekes) plugs of type [object Map] available here", - "nl": "Hier zijn stekkers van het type [object Map]" + "en": "There are Type 2 CCS (mennekes) plugs of type Type 2 CCS (mennekes) available here", + "nl": "Hier zijn stekkers van het type " }, "freeform": { "key": "socket:type2_combo", @@ -1441,52 +1441,6 @@ ] } }, - { - "#": "fee/charge", - "question": { - "en": "How much does one have to pay to use this charging station?", - "nl": "Hoeveel kost het gebruik van dit oplaadpunt?" - }, - "freeform": { - "key": "charge", - "addExtraTags": [ - "fee=yes" - ] - }, - "render": { - "en": "Using this charging station costs {charge}", - "nl": "Dit oplaadpunt gebruiken kost {charge}" - }, - "mappings": [ - { - "if": { - "and": [ - "fee=no", - "charge=" - ] - }, - "then": { - "nl": "Gratis te gebruiken", - "en": "Free to use" - } - } - ] - }, - { - "builtin": "payment-options", - "override": { - "mappings+": [ - { - "if": "payment:app=yes", - "ifnot": "payment:app=no", - "then": { - "en": "Payment is done using a dedicated app", - "nl": "Betalen via een app van het netwerk" - } - } - ] - } - }, { "#": "Authentication", "question": { @@ -1624,6 +1578,81 @@ } ] }, + { + "#": "fee/charge", + "question": { + "en": "How much does one have to pay to use this charging station?", + "nl": "Hoeveel kost het gebruik van dit oplaadpunt?" + }, + "freeform": { + "key": "charge", + "addExtraTags": [ + "fee=yes" + ] + }, + "render": { + "en": "Using this charging station costs {charge}", + "nl": "Dit oplaadpunt gebruiken kost {charge}" + }, + "mappings": [ + { + "if": { + "and": [ + "fee=no", + "charge=" + ] + }, + "then": { + "nl": "Gratis te gebruiken", + "en": "Free to use" + } + } + ] + }, + { + "builtin": "payment-options", + "override": { + "condition": { + "or": [ + "fee=yes", + "charge~*" + ] + }, + "mappings+": [ + { + "if": "payment:app=yes", + "ifnot": "payment:app=no", + "then": { + "en": "Payment is done using a dedicated app", + "nl": "Betalen via een app van het netwerk" + } + } + ] + } + }, + { + "#": "maxstay", + "question": { + "en": "What is the maximum amount of time one is allowed to stay here?", + "nl": "Hoelang mag een voertuig hier blijven staan?" + }, + "freeform": { + "key": "maxstay" + }, + "render": { + "en": "One can stay at most {canonical(maxstay)}", + "nl": "De maximale parkeertijd hier is {canonical(maxstay)}" + }, + "mappings": [ + { + "if": "maxstay=unlimited", + "then": { + "en": "No timelimit on leaving your vehicle here", + "nl": "Geen maximum parkeertijd" + } + } + ] + }, { "#": "Network", "render": { @@ -1992,6 +2021,69 @@ } ], "units": [ + { + "appliesToKey": [ + "maxstay" + ], + "applicableUnits": [ + { + "canonicalDenomination": "minutes", + "canonicalDenominationSingular": "minute", + "alternativeDenomination": [ + "m", + "min", + "mins", + "minuten", + "mns" + ], + "human": { + "en": " minutes", + "nl": " minuten" + }, + "humanSingular": { + "en": " minute", + "nl": " minuut" + } + }, + { + "canonicalDenomination": "hours", + "canonicalDenominationSingular": "hour", + "alternativeDenomination": [ + "h", + "hrs", + "hours", + "u", + "uur", + "uren" + ], + "human": { + "en": " hours", + "nl": " uren" + }, + "humanSingular": { + "en": " hour", + "nl": " uur" + } + }, + { + "canonicalDenomination": "days", + "canonicalDenominationSingular": "day", + "alternativeDenomination": [ + "dys", + "dagen", + "dag" + ], + "human": { + "en": " days", + "nl": " day" + }, + "humanSingular": { + "en": " day", + "nl": " dag" + } + } + ] + }, { "appliesToKey": [ "socket:schuko:voltage", diff --git a/assets/layers/charging_station/charging_station.protojson b/assets/layers/charging_station/charging_station.protojson index d6fd2c06e8..ddc91b36a4 100644 --- a/assets/layers/charging_station/charging_station.protojson +++ b/assets/layers/charging_station/charging_station.protojson @@ -331,6 +331,29 @@ ] } }, + { + "#": "maxstay", + "question": { + "en": "What is the maximum amount of time one is allowed to stay here?", + "nl": "Hoelang mag een voertuig hier blijven staan?" + }, + "freeform": { + "key": "maxstay" + }, + "render": { + "en": "One can stay at most {canonical(maxstay)}", + "nl": "De maximale parkeertijd hier is {canonical(maxstay)}" + }, + "mappings": [ + { + "if": "maxstay=unlimited", + "then": { + "en": "No timelimit on leaving your vehicle here", + "nl": "Geen maximum parkeertijd" + } + } + ] + }, { "#": "Network", "render": { @@ -624,5 +647,70 @@ } ] } + ], + "units": [ + { + "appliesToKey": [ + "maxstay" + ], + "applicableUnits": [ + { + "canonicalDenomination": "minutes", + "canonicalDenominationSingular": "minute", + "alternativeDenomination": [ + "m", + "min", + "mins", + "minuten", + "mns" + ], + "human": { + "en": " minutes", + "nl": " minuten" + }, + "humanSingular": { + "en": " minute", + "nl": " minuut" + } + }, + { + "canonicalDenomination": "hours", + "canonicalDenominationSingular": "hour", + "alternativeDenomination": [ + "h", + "hrs", + "hours", + "u", + "uur", + "uren" + ], + "human": { + "en": " hours", + "nl": " uren" + }, + "humanSingular": { + "en": " hour", + "nl": " uur" + } + }, + { + "canonicalDenomination": "days", + "canonicalDenominationSingular": "day", + "alternativeDenomination": [ + "dys", + "dagen", + "dag" + ], + "human": { + "en": " days", + "nl": " day" + }, + "humanSingular":{ + "en": " day", + "nl": " dag" + } + } + ] + } ] } diff --git a/assets/layers/charging_station/csvToJson.ts b/assets/layers/charging_station/csvToJson.ts index eca25e1ba1..7ea31a10fa 100644 --- a/assets/layers/charging_station/csvToJson.ts +++ b/assets/layers/charging_station/csvToJson.ts @@ -220,7 +220,9 @@ function run(file, protojson) { options: filterOptions }) - proto["units"] = [ + + + const extraUnits = [ { appliesToKey: entries.map(e => e.key + ":voltage"), applicableUnits: [{ @@ -267,6 +269,11 @@ function run(file, protojson) { }, ]; + if(proto["units"] == undefined){ + proto["units"] = [] + } + proto["units"].push(...extraUnits) + writeFileSync("charging_station.json", JSON.stringify(proto, undefined, " ")) }