forked from MapComplete/MapComplete
		
	Themes(toilets): add an allowed range to some freeform inputs, allow to specify 'units' in the freeform, add the possibility to convert units
This commit is contained in:
		
							parent
							
								
									32cb8f489f
								
							
						
					
					
						commit
						fb8ead2a2c
					
				
					 16 changed files with 270 additions and 103 deletions
				
			
		|  | @ -79,7 +79,20 @@ | |||
|       ], | ||||
|       "freeform": { | ||||
|         "key": "height", | ||||
|         "type": "pfloat" | ||||
|         "type": "pfloat", | ||||
|         "unit": { | ||||
|           "quantity": "distance", | ||||
|           "denominations": [ | ||||
|             "m", | ||||
|             "cm" | ||||
|           ] | ||||
|         }, | ||||
|         "range": { | ||||
|           "warnBelow": 0.8, | ||||
|           "warnAbove": 1.7, | ||||
|           "min": 0.4, | ||||
|           "max": 2 | ||||
|         } | ||||
|       }, | ||||
|       "render": { | ||||
|         "en": "The changing table is {canonical(height)} high", | ||||
|  | @ -104,7 +117,20 @@ | |||
|       }, | ||||
|       "freeform": { | ||||
|         "key": "min_height", | ||||
|         "type": "pfloat" | ||||
|         "type": "pfloat", | ||||
|         "unit": { | ||||
|           "quantity": "distance", | ||||
|           "denominations": [ | ||||
|             "m", | ||||
|             "cm" | ||||
|           ] | ||||
|         }, | ||||
|         "range": { | ||||
|           "warnBelow": 0.8, | ||||
|           "warnAbove": 1.7, | ||||
|           "min": 0.4, | ||||
|           "max": 2 | ||||
|         } | ||||
|       }, | ||||
|       "render": { | ||||
|         "en": "The lowest height of the adult changing table is {canonical(min_height)}", | ||||
|  | @ -134,7 +160,20 @@ | |||
|       }, | ||||
|       "freeform": { | ||||
|         "key": "max_height", | ||||
|         "type": "pfloat" | ||||
|         "type": "pfloat", | ||||
|         "unit": { | ||||
|           "quantity": "distance", | ||||
|           "denominations": [ | ||||
|             "m", | ||||
|             "cm" | ||||
|           ] | ||||
|         }, | ||||
|         "range": { | ||||
|           "warnBelow": 0.8, | ||||
|           "warnAbove": 1.7, | ||||
|           "min": 0.4, | ||||
|           "max": 2 | ||||
|         } | ||||
|       }, | ||||
|       "render": { | ||||
|         "en": "The highest height of the adult changing table is {canonical(max_height)}", | ||||
|  | @ -226,34 +265,5 @@ | |||
|     "en": "Adult changing table", | ||||
|     "nl": "Verzorgingstafel voor volwassenen", | ||||
|     "it": "Fasciatoio per adulti" | ||||
|   }, | ||||
|   "units": [ | ||||
|     { | ||||
|       "adult:height": { | ||||
|         "quantity": "distance", | ||||
|         "denominations": [ | ||||
|           "m", | ||||
|           "cm" | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "adult:min_height": { | ||||
|         "quantity": "distance", | ||||
|         "denominations": [ | ||||
|           "m", | ||||
|           "cm" | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "adult:max_height": { | ||||
|         "quantity": "distance", | ||||
|         "denominations": [ | ||||
|           "m", | ||||
|           "cm" | ||||
|         ] | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -588,7 +588,20 @@ | |||
|       }, | ||||
|       "freeform": { | ||||
|         "key": "width", | ||||
|         "type": "pfloat" | ||||
|         "type": "pfloat", | ||||
|         "unit": { | ||||
|           "quantity": "distance", | ||||
|           "canonical": "m", | ||||
|           "denominations": [ | ||||
|             "cm" | ||||
|           ] | ||||
|         }, | ||||
|         "range": { | ||||
|           "warnBelow": 0.8, | ||||
|           "warnAbove": 1.7, | ||||
|           "min": 0.4, | ||||
|           "max": 2 | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|  | @ -629,7 +642,18 @@ | |||
|           "es": "Altura del bordillo de la puerta", | ||||
|           "it": "Altezza del gradino della porta" | ||||
|         }, | ||||
|         "type": "pfloat" | ||||
|         "type": "pfloat", | ||||
|         "unit": { | ||||
|           "quantity": "distance", | ||||
|           "canonical": "m", | ||||
|           "denominations": [ | ||||
|             "cm" | ||||
|           ] | ||||
|         }, | ||||
|         "range": { | ||||
|           "warnAbove": 0.25, | ||||
|           "max": 0.5 | ||||
|         } | ||||
|       }, | ||||
|       "mappings": [ | ||||
|         { | ||||
|  | @ -720,23 +744,5 @@ | |||
|   "allowMove": { | ||||
|     "enableImproveAccuracy": true, | ||||
|     "enableRelocation": false | ||||
|   }, | ||||
|   "units": [ | ||||
|     { | ||||
|       "kerb:height": { | ||||
|         "quantity": "distance", | ||||
|         "canonical": "m", | ||||
|         "denominations": [ | ||||
|           "cm" | ||||
|         ] | ||||
|       }, | ||||
|       "width": { | ||||
|         "quantity": "distance", | ||||
|         "canonical": "m", | ||||
|         "denominations": [ | ||||
|           "cm" | ||||
|         ] | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1534,7 +1534,7 @@ | |||
|       }, | ||||
|       "render": { | ||||
|         "en": "The door to the wheelchair-accessible toilet is {canonical(door:width)} wide", | ||||
|         "nl": "De deur naar de rolstoeltoegankelijke toilet is {canonical(door:width)} wide", | ||||
|         "nl": "De deur naar de rolstoeltoegankelijke toilet is {canonical(door:width)}", | ||||
|         "fr": "La porte des toilettes accessibles aux fauteuils roulants a une large de {canonical(door:width)}", | ||||
|         "de": "Die Tür zur rollstuhlgerechten Toilette ist {canonical(door:width)} breit", | ||||
|         "da": "Døren til det kørestolsvenlige toilet er {canonical(door:width)} bred", | ||||
|  | @ -1545,7 +1545,20 @@ | |||
|       }, | ||||
|       "freeform": { | ||||
|         "key": "door:width", | ||||
|         "type": "pfloat" | ||||
|         "type": "pfloat", | ||||
|         "unit": { | ||||
|           "quantity": "distance", | ||||
|           "denominations": [ | ||||
|             "m", | ||||
|             "cm" | ||||
|           ] | ||||
|         }, | ||||
|         "range": { | ||||
|           "warnBelow": 0.6, | ||||
|           "min": 0.4, | ||||
|           "warnAbove": 2, | ||||
|           "max": 4 | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|  | @ -1734,16 +1747,5 @@ | |||
|   "allowMove": { | ||||
|     "enableRelocation": false, | ||||
|     "enableImproveAccuracy": true | ||||
|   }, | ||||
|   "units": [ | ||||
|     { | ||||
|       "door:width": { | ||||
|         "quantity": "distance", | ||||
|         "denominations": [ | ||||
|           "m", | ||||
|           "cm" | ||||
|         ] | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -36,7 +36,8 @@ | |||
|             "da": "{quantity} Megawatt", | ||||
|             "cs": "{quantity} megawatty", | ||||
|             "es": "{quantity} megavatios" | ||||
|           } | ||||
|           }, | ||||
|           "factorToCanonical": 1000000 | ||||
|         }, | ||||
|         { | ||||
|           "canonicalDenomination": "kW", | ||||
|  | @ -60,7 +61,8 @@ | |||
|             "da": "{quantity} Kilowatt", | ||||
|             "cs": "{quantity} kilowatty", | ||||
|             "es": "{quantity} kilovatios" | ||||
|           } | ||||
|           }, | ||||
|           "factorToCanonical": 1000 | ||||
|         }, | ||||
|         { | ||||
|           "canonicalDenomination": "W", | ||||
|  | @ -106,7 +108,8 @@ | |||
|             "cs": "{quantity} gigawatty", | ||||
|             "zh_Hant": "{quantity} 千兆瓦", | ||||
|             "es": "{quantity} gigavatios" | ||||
|           } | ||||
|           }, | ||||
|           "factorToCanonical": 1000000000 | ||||
|         } | ||||
|       ], | ||||
|       "eraseInvalidValues": true | ||||
|  | @ -231,7 +234,8 @@ | |||
|             "hu": "egy centiméter", | ||||
|             "es": "un centímetro", | ||||
|             "it": "one centimeter" | ||||
|           } | ||||
|           }, | ||||
|           "factorToCanonical": 0.01 | ||||
|         }, | ||||
|         { | ||||
|           "canonicalDenomination": "mm", | ||||
|  | @ -259,7 +263,8 @@ | |||
|             "hu": "egy milliméter", | ||||
|             "es": "un milímetro", | ||||
|             "it": "one millimeter" | ||||
|           } | ||||
|           }, | ||||
|           "factorToCanonical": 0.001 | ||||
|         }, | ||||
|         { | ||||
|           "canonicalDenomination": "ft", | ||||
|  | @ -283,7 +288,8 @@ | |||
|             "nb_NO": "{quantity} fot", | ||||
|             "pa_PK": "{quantity}  فوٹ", | ||||
|             "hu": "{quantity} láb" | ||||
|           } | ||||
|           }, | ||||
|           "factorToCanonical": 0.3048 | ||||
|         } | ||||
|       ] | ||||
|     }, | ||||
|  |  | |||
|  | @ -858,6 +858,12 @@ | |||
|             "description": "a number", | ||||
|             "feedback": "This is not a number" | ||||
|         }, | ||||
|         "generic": { | ||||
|             "suspiciouslyHigh": "This value is suspiciously high. Are you sure it is correct?", | ||||
|             "suspiciouslyLow": "This value is suspiciously low. Are you sure it is correct?", | ||||
|             "tooHigh": "This value is too high - the highest allowed value is {max}", | ||||
|             "tooLow": "This value is too low - the lowest allowed value is {min}" | ||||
|         }, | ||||
|         "id": { | ||||
|             "description": "an identifier", | ||||
|             "invalidCharacter": "An id can only contain letters, digits and underscores", | ||||
|  |  | |||
|  | @ -280,11 +280,19 @@ export class GenerateDocs extends Script { | |||
|             "Units ", | ||||
|             "## " + layer.id, | ||||
|         ] | ||||
| 
 | ||||
|         for (const unit of layer.units) { | ||||
|             els.push("### " + unit.quantity) | ||||
|             const defaultUnit = unit.getDefaultDenomination(() => undefined) | ||||
|             for (const denomination of unit.denominations) { | ||||
|                 els.push("#### " + denomination.canonical) | ||||
|                 if (denomination.validator) { | ||||
|                     els.push(`Validator is *${denomination.validator.name}*`) | ||||
|                 } | ||||
| 
 | ||||
|                 if (denomination.factorToCanonical) { | ||||
|                     els.push(`1${denomination.canonical} = ${denomination.factorToCanonical}${defaultUnit.canonical}`) | ||||
|                 } | ||||
| 
 | ||||
|                 if (denomination.useIfNoUnitGiven === true) { | ||||
|                     els.push("*Default denomination*") | ||||
|                 } else if ( | ||||
|  |  | |||
|  | @ -16,7 +16,12 @@ export class Denomination { | |||
|     public readonly alternativeDenominations: string[] | ||||
|     public readonly human: TypedTranslation<{ quantity: string }> | ||||
|     public readonly humanSingular?: Translation | ||||
|     private readonly _validator: Validator | ||||
|     public readonly validator: Validator | ||||
|     /** | ||||
|      * IF a conversion to the canonical value is possible, this is the factor. | ||||
|      * E.g. for "cm", the factor is 0.01, as "1cm = 0.01m" | ||||
|      */ | ||||
|     public readonly factorToCanonical?: number | ||||
| 
 | ||||
|     private constructor( | ||||
|         canonical: string, | ||||
|  | @ -27,7 +32,8 @@ export class Denomination { | |||
|         alternativeDenominations: string[], | ||||
|         _human: TypedTranslation<{ quantity: string }>, | ||||
|         _humanSingular: Translation, | ||||
|         validator: Validator | ||||
|         validator: Validator, | ||||
|         factorToCanonical: number | ||||
|     ) { | ||||
|         this.canonical = canonical | ||||
|         this._canonicalSingular = _canonicalSingular | ||||
|  | @ -37,7 +43,8 @@ export class Denomination { | |||
|         this.alternativeDenominations = alternativeDenominations | ||||
|         this.human = _human | ||||
|         this.humanSingular = _humanSingular | ||||
|         this._validator = validator | ||||
|         this.validator = validator | ||||
|         this.factorToCanonical = factorToCanonical | ||||
|     } | ||||
| 
 | ||||
|     public static fromJson(json: DenominationConfigJson, validator: Validator, context: string) { | ||||
|  | @ -73,7 +80,8 @@ export class Denomination { | |||
|             json.alternativeDenomination?.map((v) => v.trim()) ?? [], | ||||
|             humanTexts, | ||||
|             Translations.T(json.humanSingular, context + "humanSingular"), | ||||
|             validator | ||||
|             validator, | ||||
|             json.factorToCanonical | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|  | @ -87,7 +95,8 @@ export class Denomination { | |||
|             this.alternativeDenominations, | ||||
|             this.human, | ||||
|             this.humanSingular, | ||||
|             this._validator | ||||
|             this.validator, | ||||
|             this.factorToCanonical | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|  | @ -101,7 +110,8 @@ export class Denomination { | |||
|             [this.canonical, ...this.alternativeDenominations], | ||||
|             this.human, | ||||
|             this.humanSingular, | ||||
|             this._validator | ||||
|             this.validator, | ||||
|             this.factorToCanonical | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|  | @ -217,10 +227,10 @@ export class Denomination { | |||
|             return null | ||||
|         } | ||||
| 
 | ||||
|         if (!this._validator.isValid(value.trim())) { | ||||
|         if (!this.validator.isValid(value.trim())) { | ||||
|             return null | ||||
|         } | ||||
|         return this._validator.reformat(value.trim()) | ||||
|         return this.validator.reformat(value.trim()) | ||||
|     } | ||||
| 
 | ||||
|     withValidator(validator: Validator) { | ||||
|  | @ -233,7 +243,8 @@ export class Denomination { | |||
|             this.alternativeDenominations, | ||||
|             this.human, | ||||
|             this.humanSingular, | ||||
|             validator | ||||
|             validator, | ||||
|             this.factorToCanonical | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -962,6 +962,7 @@ class MoveUnitConfigs extends DesugaringStep<LayerConfigJson> { | |||
|             json.units.push({ | ||||
|                 [qtr.freeform.key]: unitConfig | ||||
|             }) | ||||
|             // Note: we do not delete the config - this way, if the tagRendering is imported in another layer, the unit comes along
 | ||||
|         } | ||||
|         return json | ||||
|     } | ||||
|  | @ -1052,6 +1053,7 @@ export class PrepareLayer extends Fuse<LayerConfigJson> { | |||
|             ), | ||||
|             new AddFiltersFromTagRenderings(), | ||||
|             new ExpandFilter(state), | ||||
|             new MoveUnitConfigs(), | ||||
|             new PruneFilters() | ||||
|         ) | ||||
|     } | ||||
|  |  | |||
|  | @ -312,6 +312,16 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs | |||
| 
 | ||||
|             canonical?: string | ||||
|             inverted?: boolean | ||||
|         }, | ||||
|         /** | ||||
|          * question: In what range should the value be? | ||||
|          * For example, a door width under 65cm is suspicious, under 40cm it is a mistake. | ||||
|          */ | ||||
|         range?: { | ||||
|             min?: number, | ||||
|             warnBelow?: number, | ||||
|             warnAbove?: number | ||||
|             max?: number | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -85,7 +85,8 @@ export default interface UnitConfigJson { | |||
|      * When a default input method should be used, this can be specified by setting the canonical denomination here, e.g. | ||||
|      * `defaultInput: "cm"`. This must be a denomination which appears in the applicableUnits | ||||
|      */ | ||||
|     defaultInput?: string | ||||
|     defaultInput?: string, | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export interface DenominationConfigJson { | ||||
|  | @ -154,4 +155,9 @@ export interface DenominationConfigJson { | |||
|      * E.g.: `50 mph` instad of `50mph` | ||||
|      */ | ||||
|     addSpace?: boolean | ||||
| 
 | ||||
|     /** | ||||
|      * If the canonical unit (e.g. 1m) is multiplied with the factorToCanonical (0.01) you will get the current unit (1cm) | ||||
|      */ | ||||
|     factorToCanonical?: number | ||||
| } | ||||
|  |  | |||
|  | @ -5,10 +5,7 @@ import { TagUtils } from "../../Logic/Tags/TagUtils" | |||
| import { And } from "../../Logic/Tags/And" | ||||
| import { Utils } from "../../Utils" | ||||
| import { Tag } from "../../Logic/Tags/Tag" | ||||
| import { | ||||
|     MappingConfigJson, | ||||
|     QuestionableTagRenderingConfigJson, | ||||
| } from "./Json/QuestionableTagRenderingConfigJson" | ||||
| import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson" | ||||
| import Validators, { ValidatorType } from "../../UI/InputElement/Validators" | ||||
| import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" | ||||
| import { RegexTag } from "../../Logic/Tags/RegexTag" | ||||
|  | @ -20,6 +17,7 @@ import MarkdownUtils from "../../Utils/MarkdownUtils" | |||
| import { UploadableTag } from "../../Logic/Tags/TagTypes" | ||||
| import LayerConfig from "./LayerConfig" | ||||
| import ComparingTag from "../../Logic/Tags/ComparingTag" | ||||
| import { Unit } from "../Unit" | ||||
| 
 | ||||
| export interface Mapping { | ||||
|     readonly if: UploadableTag | ||||
|  | @ -41,6 +39,12 @@ export interface Mapping { | |||
|     readonly priorityIf?: TagsFilter | ||||
| } | ||||
| 
 | ||||
| export interface ValueRange { | ||||
|     min?: number, | ||||
|     max?: number, | ||||
|     warnBelow?: number, | ||||
|     warnAbove?: number | ||||
| } | ||||
| /*** | ||||
|  * The parsed version of TagRenderingConfigJSON | ||||
|  * Identical data, but with some methods and validation | ||||
|  | @ -79,6 +83,7 @@ export default class TagRenderingConfig { | |||
|         readonly default?: string | ||||
|         readonly postfixDistinguished?: string | ||||
|         readonly args?: any | ||||
|         readonly range: ValueRange | ||||
|     } | ||||
| 
 | ||||
|     public readonly multiAnswer: boolean | ||||
|  | @ -233,6 +238,7 @@ export default class TagRenderingConfig { | |||
|                 default: json.freeform.default, | ||||
|                 postfixDistinguished: json.freeform.postfixDistinguished?.trim(), | ||||
|                 args: json.freeform.helperArgs, | ||||
|                 range: json.freeform.range | ||||
|             } | ||||
|             if (json.freeform["extraTags"] !== undefined) { | ||||
|                 throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})` | ||||
|  | @ -787,7 +793,8 @@ export default class TagRenderingConfig { | |||
|         freeformValue: string | undefined, | ||||
|         singleSelectedMapping: number, | ||||
|         multiSelectedMapping: boolean[] | undefined, | ||||
|         currentProperties: Record<string, string> | ||||
|         currentProperties: Record<string, string>, | ||||
|         unit?: Unit | ||||
|     ): UploadableTag[] { | ||||
|         if (typeof freeformValue === "string") { | ||||
|             freeformValue = freeformValue?.trim() | ||||
|  | @ -795,7 +802,14 @@ export default class TagRenderingConfig { | |||
| 
 | ||||
|         const validator = Validators.get(<ValidatorType>this.freeform?.type) | ||||
|         if (validator && freeformValue) { | ||||
|             freeformValue = validator.reformat(freeformValue, () => currentProperties["_country"]) | ||||
|             // We try to reformat; but a unit might annoy us here
 | ||||
|             if (unit) { | ||||
|                 const [valueNoUnit, denom] = unit.findDenomination(freeformValue, () => currentProperties["_country"]) | ||||
|                 const formatted = validator.reformat(valueNoUnit, () => currentProperties["_country"]) | ||||
|                 freeformValue = formatted + denom.canonical | ||||
|             } else { | ||||
|                 freeformValue = validator.reformat(freeformValue, () => currentProperties["_country"]) | ||||
|             } | ||||
|         } | ||||
|         if (freeformValue === "") { | ||||
|             freeformValue = undefined | ||||
|  | @ -918,9 +932,23 @@ export default class TagRenderingConfig { | |||
|     GenerateDocumentation(lang: string = "en"): string { | ||||
|         let freeform: string = undefined | ||||
|         if (this.render) { | ||||
|             freeform = "*" + this.render.textFor(lang) + "*" | ||||
|             freeform = "\n*" + this.render.textFor(lang) + "*" | ||||
|             if (this.freeform?.key) { | ||||
|                 freeform += " is shown if `" + this.freeform.key + "` is set" | ||||
|                 freeform += " is shown if `" + this.freeform.key + "` is set." | ||||
|             } | ||||
|             if (this.question && this.freeform.range) { | ||||
|                 freeform += "\n\nThe allowed input is of type " + (this.freeform.type ?? "string") | ||||
|                 if (this.freeform.range) { | ||||
|                     const r = this.freeform.range | ||||
|                     freeform += ` and is in range ${r.min ?? "-infinty"} until ${r.max ?? "infinity"} (both inclusive).` | ||||
|                     if (r.warnAbove && r.warnBelow) { | ||||
|                         freeform += ` A warning will appear if the value is outside of ${r.warnBelow} and ${r.warnAbove}.` | ||||
|                     } else if (r.warnBelow) { | ||||
|                         freeform += ` A warning will appear below ${r.warnBelow}.` | ||||
|                     } else if (r.warnAbove) { | ||||
|                         freeform += ` A warning will appear above ${r.warnAbove}.` | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import unit from "../../assets/layers/unit/unit.json" | |||
| import TagRenderingConfig from "./ThemeConfig/TagRenderingConfig" | ||||
| import Validators, { ValidatorType } from "../UI/InputElement/Validators" | ||||
| import { Validator } from "../UI/InputElement/Validator" | ||||
| import FloatValidator from "../UI/InputElement/Validators/FloatValidator" | ||||
| 
 | ||||
| export class Unit { | ||||
|     private static allUnits = this.initUnits() | ||||
|  | @ -334,10 +335,11 @@ export class Unit { | |||
|         return [undefined, undefined] | ||||
|     } | ||||
| 
 | ||||
|     asHumanLongValue(value: string, country: () => string): BaseUIElement | string { | ||||
|     asHumanLongValue(value: string | number, country: () => string): BaseUIElement | string { | ||||
|         if (value === undefined) { | ||||
|             return undefined | ||||
|         } | ||||
|         value = "" + value | ||||
|         const [stripped, denom] = this.findDenomination(value, country) | ||||
|         const human = denom?.human | ||||
|         if (this.inverted) { | ||||
|  | @ -393,4 +395,19 @@ export class Unit { | |||
|         } | ||||
|         return this.denominations[0] | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the value in the canonical denomination; | ||||
|      * e.g. "1cm -> 0.01" as it is 0.01meter | ||||
|      * @param v | ||||
|      */ | ||||
|     public valueInCanonical(value: string, country: () => string): number { | ||||
|         const denom = this.findDenomination(value, country) | ||||
|         if (!denom) { | ||||
|             return undefined | ||||
|         } | ||||
|         const [v, d] = denom | ||||
|         const vf = new FloatValidator().reformat(v) | ||||
|         return Number(vf) * (d.factorToCanonical ?? 1) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -4,12 +4,17 @@ | |||
|   import Validators from "./Validators" | ||||
|   import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|   import { Translation } from "../i18n/Translation" | ||||
| 
 | ||||
|   import { createEventDispatcher, onDestroy } from "svelte" | ||||
|   import { Validator } from "./Validator" | ||||
|   import { Unit } from "../../Models/Unit" | ||||
|   import UnitInput from "../Popup/UnitInput.svelte" | ||||
|   import { Utils } from "../../Utils" | ||||
|   import { twMerge } from "tailwind-merge" | ||||
|   import type { ValueRange } from "../../Models/ThemeConfig/TagRenderingConfig" | ||||
|   import Translations from "../i18n/Translations" | ||||
|   import FloatValidator from "./Validators/FloatValidator" | ||||
|   import BaseUIElement from "../BaseUIElement" | ||||
| 
 | ||||
|   export let type: ValidatorType | ||||
|   export let feedback: UIEventSource<Translation> | undefined = undefined | ||||
|  | @ -18,6 +23,7 @@ | |||
|   export let placeholder: string | Translation | undefined = undefined | ||||
|   export let autofocus: boolean = false | ||||
|   export let unit: Unit = undefined | ||||
|   export let range: ValueRange = undefined | ||||
|   /** | ||||
|    * Valid state, exported to the calling component | ||||
|    */ | ||||
|  | @ -42,7 +48,7 @@ | |||
| 
 | ||||
|   function initValueAndDenom() { | ||||
|     if (unit && value.data) { | ||||
|       const [v, denom] = unit?.findDenomination(value.data, getCountry) | ||||
|       const [v, denom] = unit.findDenomination(value.data, getCountry) | ||||
|       if (denom) { | ||||
|         unvalidatedText.setData(v) | ||||
|         selectedUnit.setData(denom.canonical) | ||||
|  | @ -62,7 +68,6 @@ | |||
|     } | ||||
|   } | ||||
|   initValueAndDenom() | ||||
| 
 | ||||
|   $: { | ||||
|     // The type changed -> reset some values | ||||
|     validator = Validators.get(type ?? "string") | ||||
|  | @ -77,6 +82,41 @@ | |||
|     initValueAndDenom() | ||||
|   } | ||||
| 
 | ||||
|   const t = Translations.t.validation.generic | ||||
| 
 | ||||
|   /** | ||||
|    * Side effect: sets the feedback, returns true/false if valid | ||||
|    * @param canonicalValue | ||||
|    */ | ||||
|   function validateRange(canonicalValue: number): boolean { | ||||
|     if (!range) { | ||||
|       return true | ||||
|     } | ||||
|     if (canonicalValue < range.warnBelow) { | ||||
|       feedback.set(t.suspiciouslyLow) | ||||
|     } | ||||
|     if (canonicalValue > range.warnAbove) { | ||||
|       feedback.set(t.suspiciouslyHigh) | ||||
|     } | ||||
|     if (canonicalValue > range.max) { | ||||
|       let max: number | string | BaseUIElement = range.max | ||||
|       if (unit) { | ||||
|         max = unit.asHumanLongValue(max) | ||||
|       } | ||||
|       feedback.set(t.tooHigh.Subs({ max })) | ||||
|       return false | ||||
|     } | ||||
|     if (canonicalValue < range.min) { | ||||
|       let min: number | string | BaseUIElement = range.min | ||||
|       if (unit) { | ||||
|         min = unit.asHumanLongValue(min) | ||||
|       } | ||||
|       feedback.set(t.tooLow.Subs({ min })) | ||||
|       return false | ||||
|     } | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   function setValues() { | ||||
|     // Update the value stores | ||||
|     const v = unvalidatedText.data | ||||
|  | @ -92,13 +132,22 @@ | |||
|     } | ||||
| 
 | ||||
|     if (selectedUnit.data) { | ||||
|       value.setData(unit.toOsm(v, selectedUnit.data)) | ||||
|       const canonicalValue = unit.valueInCanonical(v + selectedUnit.data) | ||||
|       if (validateRange(canonicalValue)) { | ||||
|         value.setData(unit.toOsm(v, selectedUnit.data)) | ||||
|       } else { | ||||
|         value.set(undefined) | ||||
|       } | ||||
|     } else { | ||||
|       value.setData(v) | ||||
|       if (validateRange(v)) { | ||||
|         value.setData(v) | ||||
|       } else { | ||||
|         value.set(undefined) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   onDestroy(unvalidatedText.addCallbackAndRun((_) => setValues())) | ||||
|   onDestroy(unvalidatedText.addCallbackAndRun(() => setValues())) | ||||
|   if (unit === undefined) { | ||||
|     onDestroy( | ||||
|       value.addCallbackAndRunD((fromUpstream) => { | ||||
|  | @ -110,7 +159,7 @@ | |||
|   } else { | ||||
|     // Handled by the UnitInput | ||||
|   } | ||||
|   onDestroy(selectedUnit.addCallback((_) => setValues())) | ||||
|   onDestroy(selectedUnit.addCallback(() => setValues())) | ||||
|   if (validator === undefined) { | ||||
|     throw ( | ||||
|       "Not a valid type (no validator found) for type '" + | ||||
|  |  | |||
|  | @ -17,7 +17,6 @@ export default class FloatValidator extends Validator { | |||
|      * new FloatValidator().isValid("0,2") // => true
 | ||||
|      */ | ||||
|     isValid(str: string) { | ||||
|         console.log("Is valid?", str, FloatValidator.formattingHasComma) | ||||
|         if (!FloatValidator.formattingHasComma) { | ||||
|             str = str.replace(",", ".") | ||||
|         } | ||||
|  | @ -28,7 +27,11 @@ export default class FloatValidator extends Validator { | |||
|         if (!FloatValidator.formattingHasComma) { | ||||
|             str = str.replace(",", ".") | ||||
|         } | ||||
|         return "" + Number(str) | ||||
|         let formatted = "" + Number(str) | ||||
|         if (str.startsWith("0") && str.length > 1 && str.indexOf(".") < 0) { | ||||
|             formatted = "0" + formatted | ||||
|         } | ||||
|         return formatted | ||||
|     } | ||||
| 
 | ||||
|     getFeedback(s: string): Translation { | ||||
|  |  | |||
|  | @ -53,6 +53,7 @@ | |||
|         type={config.freeform.type} | ||||
|         {placeholder} | ||||
|         {value} | ||||
|         range={config.freeform.range} | ||||
|       /> | ||||
|     </Inline> | ||||
|   {:else if InputHelpers.hideInputField.indexOf(config.freeform.type) < 0} | ||||
|  | @ -66,6 +67,7 @@ | |||
|       {placeholder} | ||||
|       {value} | ||||
|       {unvalidatedText} | ||||
|       range={config.freeform.range} | ||||
|     /> | ||||
|   {/if} | ||||
| 
 | ||||
|  |  | |||
|  | @ -221,7 +221,8 @@ | |||
|           $freeformInput, | ||||
|           selectedMapping, | ||||
|           checkedMappings, | ||||
|           tags.data | ||||
|           tags.data, | ||||
|           unit | ||||
|         ) | ||||
|         if (featureSwitchIsDebugging?.data) { | ||||
|           console.log( | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue