forked from MapComplete/MapComplete
		
	First version of unit handling: canonicalizing on input
This commit is contained in:
		
							parent
							
								
									fca3f45908
								
							
						
					
					
						commit
						0012a2f683
					
				
					 11 changed files with 379 additions and 48 deletions
				
			
		|  | @ -5,6 +5,7 @@ import {LayoutConfigJson} from "./LayoutConfigJson"; | ||||||
| import AllKnownLayers from "../AllKnownLayers"; | import AllKnownLayers from "../AllKnownLayers"; | ||||||
| import SharedTagRenderings from "../SharedTagRenderings"; | import SharedTagRenderings from "../SharedTagRenderings"; | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
|  | import {Unit} from "./Unit"; | ||||||
| 
 | 
 | ||||||
| export default class LayoutConfig { | export default class LayoutConfig { | ||||||
|     public readonly id: string; |     public readonly id: string; | ||||||
|  | @ -30,7 +31,6 @@ export default class LayoutConfig { | ||||||
|         maxZoom: number, |         maxZoom: number, | ||||||
|         minNeededElements: number |         minNeededElements: number | ||||||
|     }; |     }; | ||||||
| 
 |  | ||||||
|     public readonly hideFromOverview: boolean; |     public readonly hideFromOverview: boolean; | ||||||
|     public lockLocation: boolean | [[number, number], [number, number]]; |     public lockLocation: boolean | [[number, number], [number, number]]; | ||||||
|     public readonly enableUserBadge: boolean; |     public readonly enableUserBadge: boolean; | ||||||
|  | @ -42,12 +42,12 @@ export default class LayoutConfig { | ||||||
|     public readonly enableGeolocation: boolean; |     public readonly enableGeolocation: boolean; | ||||||
|     public readonly enableBackgroundLayerSelection: boolean; |     public readonly enableBackgroundLayerSelection: boolean; | ||||||
|     public readonly enableShowAllQuestions: boolean; |     public readonly enableShowAllQuestions: boolean; | ||||||
| 
 |  | ||||||
|     public readonly customCss?: string; |     public readonly customCss?: string; | ||||||
|     /* |     /* | ||||||
|     How long is the cache valid, in seconds? |     How long is the cache valid, in seconds? | ||||||
|      */ |      */ | ||||||
|     public readonly cacheTimeout?: number; |     public readonly cacheTimeout?: number; | ||||||
|  |     public readonly units: { appliesToKeys: Set<string>, applicableUnits: Unit[] }[] = [] | ||||||
|     private readonly _official: boolean; |     private readonly _official: boolean; | ||||||
| 
 | 
 | ||||||
|     constructor(json: LayoutConfigJson, official = true, context?: string) { |     constructor(json: LayoutConfigJson, official = true, context?: string) { | ||||||
|  | @ -185,6 +185,51 @@ export default class LayoutConfig { | ||||||
|         this.enableShowAllQuestions = json.enableShowAllQuestions ?? false; |         this.enableShowAllQuestions = json.enableShowAllQuestions ?? false; | ||||||
|         this.customCss = json.customCss; |         this.customCss = json.customCss; | ||||||
|         this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60) |         this.cacheTimeout = json.cacheTimout ?? (60 * 24 * 60 * 60) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         if ((json.units ?? []).length !== 0) { | ||||||
|  |             for (let i1 = 0; i1 < json.units.length; i1++) { | ||||||
|  |                 let unit = json.units[i1]; | ||||||
|  |                 const appliesTo = unit.appliesToKey | ||||||
|  | 
 | ||||||
|  |                 for (let i = 0; i < appliesTo.length; i++) { | ||||||
|  |                     let key = appliesTo[i]; | ||||||
|  |                     if (key.trim() !== key) { | ||||||
|  |                         throw `${context}.unit[${i1}].appliesToKey[${i}] is invalid: it starts or ends with whitespace` | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if ((unit.applicableUnits ?? []).length === 0) { | ||||||
|  |                     throw  `${context}: define at least one applicable unit` | ||||||
|  |                 } | ||||||
|  |                 // Some keys do have unit handling
 | ||||||
|  | 
 | ||||||
|  |                 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  | ||||||
|  |                 } | ||||||
|  |                | ||||||
|  |                 // 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 seenKeys = new Set<string>() | ||||||
|  |             for (const unit of this.units) { | ||||||
|  |                 const alreadySeen = Array.from(unit.appliesToKeys).filter(key => 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)) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public CustomCodeSnippets(): string[] { |     public CustomCodeSnippets(): string[] { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import {LayerConfigJson} from "./LayerConfigJson"; | import {LayerConfigJson} from "./LayerConfigJson"; | ||||||
| import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; | import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; | ||||||
|  | import UnitConfigJson from "./UnitConfigJson"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Defines the entire theme. |  * Defines the entire theme. | ||||||
|  | @ -186,6 +187,71 @@ export interface LayoutConfigJson { | ||||||
|      */ |      */ | ||||||
|     layers: (LayerConfigJson | string | { builtin: string, override: any })[], |     layers: (LayerConfigJson | string | { builtin: string, override: any })[], | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * In some cases, a value is represented in a certain unit (such as meters for heigt/distance/..., km/h for speed, ...) | ||||||
|  |      * | ||||||
|  |      * Sometimes, multiple denominations are possible (e.g. km/h vs mile/h; megawatt vs kilowatt vs gigawatt for power generators, ...) | ||||||
|  |      * | ||||||
|  |      * This brings in some troubles, as there are multiple ways to write it (no denomitation, 'm' vs 'meter' 'metre', ...) | ||||||
|  |      * | ||||||
|  |      * Not only do we want to write consistent data to OSM, we also want to present this consistently to the user. | ||||||
|  |      * This is handled by defining units. | ||||||
|  |      * | ||||||
|  |      * # Usage | ||||||
|  |      * | ||||||
|  |      * First of all, you define which keys have units applied, for example: | ||||||
|  |      * | ||||||
|  |      * ``` | ||||||
|  |      * units: [ | ||||||
|  |      *  appliesTo: ["maxspeed", "maxspeed:hgv", "maxspeed:bus"] | ||||||
|  |      *  applicableUnits: [ | ||||||
|  |      *      ... | ||||||
|  |      *  ] | ||||||
|  |      * ] | ||||||
|  |      * ``` | ||||||
|  |      * | ||||||
|  |      * ApplicableUnits defines which is the canonical extension, how it is presented to the user, ...: | ||||||
|  |      * | ||||||
|  |      * ``` | ||||||
|  |      * applicableUnits: [ | ||||||
|  |      * { | ||||||
|  |      *     canonicalDenomination: "km/h", | ||||||
|  |      *     alternativeDenomination: ["km/u", "kmh", "kph"] | ||||||
|  |      *     default: true, | ||||||
|  |      *     human: { | ||||||
|  |      *         en: "kilometer/hour", | ||||||
|  |      *         nl: "kilometer/uur" | ||||||
|  |      *     }, | ||||||
|  |      *     humanShort: { | ||||||
|  |      *         en: "km/h", | ||||||
|  |      *         nl: "km/u" | ||||||
|  |      *     } | ||||||
|  |      * }, | ||||||
|  |      * { | ||||||
|  |      *     canoncialDenomination: "mph", | ||||||
|  |      *     ... similar for miles an hour ... | ||||||
|  |      * } | ||||||
|  |      * ] | ||||||
|  |      * ``` | ||||||
|  |      * | ||||||
|  |      * | ||||||
|  |      * If this is defined, then every key which the denominations apply to (`maxspeed`, `maxspeed:hgv` and `maxspeed:bus`) will be rewritten at the metatagging stage: | ||||||
|  |      * every value will be parsed and the canonical extension will be added add presented to the other parts of the code. | ||||||
|  |      * | ||||||
|  |      * Also, if a freeform text field is used, an extra dropdown with applicable denominations will be given | ||||||
|  |      * | ||||||
|  |      */ | ||||||
|  |      | ||||||
|  |     units?: { | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Every key from this list will be normalized | ||||||
|  |          */ | ||||||
|  |         appliesToKey: string[], | ||||||
|  | 
 | ||||||
|  |         applicableUnits: UnitConfigJson[] | ||||||
|  |     }[] | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * If defined, data will be clustered. |      * If defined, data will be clustered. | ||||||
|      * Defaults to {maxZoom: 16, minNeeded: 500} |      * Defaults to {maxZoom: 16, minNeeded: 500} | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import {TagUtils} from "../../Logic/Tags/TagUtils"; | ||||||
| import {And} from "../../Logic/Tags/And"; | import {And} from "../../Logic/Tags/And"; | ||||||
| import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| /*** | /*** | ||||||
|  * The parsed version of TagRenderingConfigJSON |  * The parsed version of TagRenderingConfigJSON | ||||||
|  * Identical data, but with some methods and validation |  * Identical data, but with some methods and validation | ||||||
|  | @ -64,11 +65,16 @@ export default class TagRenderingConfig { | ||||||
|             this.condition = condition; |             this.condition = condition; | ||||||
|         } |         } | ||||||
|         if (json.freeform) { |         if (json.freeform) { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|             this.freeform = { |             this.freeform = { | ||||||
|                 key: json.freeform.key, |                 key: json.freeform.key, | ||||||
|                 type: json.freeform.type ?? "string", |                 type: json.freeform.type ?? "string", | ||||||
|                 addExtraTags: json.freeform.addExtraTags?.map((tg, i) => |                 addExtraTags: json.freeform.addExtraTags?.map((tg, i) => | ||||||
|                     FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [] |                     FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [], | ||||||
|  |                | ||||||
|  |                | ||||||
|             } |             } | ||||||
|             if (json.freeform["extraTags"] !== undefined) { |             if (json.freeform["extraTags"] !== undefined) { | ||||||
|                 throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})` |                 throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})` | ||||||
|  | @ -76,6 +82,9 @@ export default class TagRenderingConfig { | ||||||
|             if (this.freeform.key === undefined || this.freeform.key === "") { |             if (this.freeform.key === undefined || this.freeform.key === "") { | ||||||
|                 throw `Freeform.key is undefined or the empty string - this is not allowed; either fill out something or remove the freeform block alltogether. Error in ${context}` |                 throw `Freeform.key is undefined or the empty string - this is not allowed; either fill out something or remove the freeform block alltogether. Error in ${context}` | ||||||
|             } |             } | ||||||
|  |              | ||||||
|  |              | ||||||
|  |              | ||||||
|             if (ValidatedTextField.AllTypes[this.freeform.type] === undefined) { |             if (ValidatedTextField.AllTypes[this.freeform.type] === undefined) { | ||||||
|                 const knownKeys = ValidatedTextField.tpList.map(tp => tp.name).join(", "); |                 const knownKeys = ValidatedTextField.tpList.map(tp => tp.name).join(", "); | ||||||
|                 throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}` |                 throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}` | ||||||
|  | @ -272,7 +281,6 @@ export default class TagRenderingConfig { | ||||||
|         })) |         })) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         if (!freeformKeyUsed |         if (!freeformKeyUsed | ||||||
|             && tags[this.freeform.key] !== undefined) { |             && tags[this.freeform.key] !== undefined) { | ||||||
|             applicableMappings.push(this.render) |             applicableMappings.push(this.render) | ||||||
|  | @ -282,6 +290,7 @@ export default class TagRenderingConfig { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Gets the correct rendering value (or undefined if not known) |      * Gets the correct rendering value (or undefined if not known) | ||||||
|  |      * Not compatible with multiAnswer - use GetRenderValueS instead in that case | ||||||
|      * @constructor |      * @constructor | ||||||
|      */ |      */ | ||||||
|     public GetRenderValue(tags: any): Translation { |     public GetRenderValue(tags: any): Translation { | ||||||
|  |  | ||||||
|  | @ -45,6 +45,8 @@ export interface TagRenderingConfigJson { | ||||||
|          * Useful to add a 'fixme=freeform textfield used - to be checked' |          * Useful to add a 'fixme=freeform textfield used - to be checked' | ||||||
|          **/ |          **/ | ||||||
|         addExtraTags?: string[]; |         addExtraTags?: string[]; | ||||||
|  | 
 | ||||||
|  |          | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
							
								
								
									
										86
									
								
								Customizations/JSON/Unit.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								Customizations/JSON/Unit.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | ||||||
|  | import {Translation} from "../../UI/i18n/Translation"; | ||||||
|  | import UnitConfigJson from "./UnitConfigJson"; | ||||||
|  | import Translations from "../../UI/i18n/Translations"; | ||||||
|  | 
 | ||||||
|  | export class Unit { | ||||||
|  |     public readonly human: Translation; | ||||||
|  |     private readonly alternativeDenominations: string []; | ||||||
|  |     private readonly canonical: string; | ||||||
|  |     private readonly default: boolean; | ||||||
|  |     private readonly prefix: boolean; | ||||||
|  | 
 | ||||||
|  |     constructor(json: UnitConfigJson, context: string) { | ||||||
|  |         context = `${context}.unit(${json.canonicalDenomination})` | ||||||
|  |         this.canonical = json.canonicalDenomination.trim() | ||||||
|  |         if ((this.canonical ?? "") === "") { | ||||||
|  |             throw `${context}: this unit has no decent canonical value defined` | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         json.alternativeDenomination.forEach((v, i) => { | ||||||
|  |             if (((v?.trim() ?? "") === "")) { | ||||||
|  |                 throw `${context}.alternativeDenomination.${i}: invalid alternative denomination: undefined, null or only whitespace` | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         this.alternativeDenominations = json.alternativeDenomination?.map(v => v.trim()) ?? [] | ||||||
|  | 
 | ||||||
|  |         this.default = json.default ?? false; | ||||||
|  | 
 | ||||||
|  |         this.human = Translations.T(json.human, context + "human") | ||||||
|  | 
 | ||||||
|  |         this.prefix = json.prefix ?? false; | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public canonicalValue(value: string) { | ||||||
|  |         const stripped = this.StrippedValue(value) | ||||||
|  |         if(stripped === null){ | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         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) | ||||||
|  |      * - 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 { | ||||||
|  | 
 | ||||||
|  |         if (this.prefix) { | ||||||
|  |             if (value.startsWith(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)) { | ||||||
|  |                 return value.substring(0, value.length - this.canonical.length).trim(); | ||||||
|  |             } | ||||||
|  |             for (const alternativeValue of this.alternativeDenominations) { | ||||||
|  |                 if (value.endsWith(alternativeValue)) { | ||||||
|  |                     return value.substring(0, value.length - alternativeValue.length).trim(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         if (this.default) { | ||||||
|  |             const parsed = Number(value.trim()) | ||||||
|  |             if (!isNaN(parsed)) { | ||||||
|  |                 return value.trim(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								Customizations/JSON/UnitConfigJson.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								Customizations/JSON/UnitConfigJson.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | ||||||
|  | export default interface UnitConfigJson{ | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The canonical value which will be added to the text. | ||||||
|  |      * e.g. "m" for meters | ||||||
|  |      * If the user inputs '42', the canonical value will be added and it'll become '42m' | ||||||
|  |      */ | ||||||
|  |     canonicalDenomination: string, | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * A list of alternative values which can occur in the OSM database - used for parsing. | ||||||
|  |      */ | ||||||
|  |     alternativeDenomination?: string[], | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The value for humans in the dropdown. This should not use abbreviations and should be translated, e.g. | ||||||
|  |      * { | ||||||
|  |      *     "en": "meter", | ||||||
|  |      *     "fr": "metre" | ||||||
|  |      * } | ||||||
|  |      */ | ||||||
|  |     human?: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 | ||||||
|  |      */ | ||||||
|  |     prefix?: boolean | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The default interpretation - only one can be set. | ||||||
|  |      * If none is set, the first unit will be considered the default interpretation of a value without a unit | ||||||
|  |      */ | ||||||
|  |     default?: boolean | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -75,6 +75,39 @@ export default class SimpleMetaTagger { | ||||||
|             feature.area = sqMeters; |             feature.area = sqMeters; | ||||||
|         }) |         }) | ||||||
|     ); |     ); | ||||||
|  | 
 | ||||||
|  |     private static canonicalize = new SimpleMetaTagger( | ||||||
|  |         { | ||||||
|  |             doc: "If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical form (e.g. `1meter` will be rewritten to `1m`)", | ||||||
|  |             keys: [] | ||||||
|  | 
 | ||||||
|  |         }, | ||||||
|  |         (feature => { | ||||||
|  |             const units = State.state.layoutToUse.data.units ?? []; | ||||||
|  |             for (const key in feature.properties) { | ||||||
|  |                 if(!feature.properties.hasOwnProperty(key)){ | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  |                 for (const unit of units) { | ||||||
|  |                     if (!unit.appliesToKeys.has(key)) { | ||||||
|  |                         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; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|     private static lngth = new SimpleMetaTagger( |     private static lngth = new SimpleMetaTagger( | ||||||
|         { |         { | ||||||
|             keys: ["_length", "_length:km"], |             keys: ["_length", "_length:km"], | ||||||
|  | @ -215,7 +248,7 @@ export default class SimpleMetaTagger { | ||||||
|             keys: ["_width:needed", "_width:needed:no_pedestrians", "_width:difference"], |             keys: ["_width:needed", "_width:needed:no_pedestrians", "_width:difference"], | ||||||
|             doc: "Legacy for a specific project calculating the needed width for safe traffic on a road. Only activated if 'width:carriageway' is present" |             doc: "Legacy for a specific project calculating the needed width for safe traffic on a road. Only activated if 'width:carriageway' is present" | ||||||
|         }, |         }, | ||||||
|         (feature: any, index: number) => { |         feature => { | ||||||
| 
 | 
 | ||||||
|             const properties = feature.properties; |             const properties = feature.properties; | ||||||
|             if (properties["width:carriageway"] === undefined) { |             if (properties["width:carriageway"] === undefined) { | ||||||
|  | @ -352,6 +385,7 @@ export default class SimpleMetaTagger { | ||||||
|         SimpleMetaTagger.latlon, |         SimpleMetaTagger.latlon, | ||||||
|         SimpleMetaTagger.surfaceArea, |         SimpleMetaTagger.surfaceArea, | ||||||
|         SimpleMetaTagger.lngth, |         SimpleMetaTagger.lngth, | ||||||
|  |         SimpleMetaTagger.canonicalize, | ||||||
|         SimpleMetaTagger.country, |         SimpleMetaTagger.country, | ||||||
|         SimpleMetaTagger.isOpen, |         SimpleMetaTagger.isOpen, | ||||||
|         SimpleMetaTagger.carriageWayWidth, |         SimpleMetaTagger.carriageWayWidth, | ||||||
|  |  | ||||||
|  | @ -816,6 +816,20 @@ | ||||||
|       "wayHandling": 0 |       "wayHandling": 0 | ||||||
|     } |     } | ||||||
|   ], |   ], | ||||||
|  |   "units": [ | ||||||
|  |     { | ||||||
|  |       "appliesToKey": ["climbing:length"], | ||||||
|  |       "applicableUnits": [{ | ||||||
|  |         "canonicalDenomination": "m", | ||||||
|  |         "alternativeDenomination": ["meter","meters"], | ||||||
|  |         "human": { | ||||||
|  |           "en": "meter", | ||||||
|  |           "nl": "meter" | ||||||
|  |         }, | ||||||
|  |         "default": true | ||||||
|  |       }] | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|   "roamingRenderings": [ |   "roamingRenderings": [ | ||||||
|     { |     { | ||||||
|       "#": "Website", |       "#": "Website", | ||||||
|  |  | ||||||
|  | @ -214,7 +214,11 @@ class LayerOverviewUtils { | ||||||
|             const errors = layerErrorCount.concat(themeErrorCount).join("\n") |             const errors = layerErrorCount.concat(themeErrorCount).join("\n") | ||||||
|             console.log(errors) |             console.log(errors) | ||||||
|             const msg = (`Found ${layerErrorCount.length} errors in the layers; ${themeErrorCount.length} errors in the themes`) |             const msg = (`Found ${layerErrorCount.length} errors in the layers; ${themeErrorCount.length} errors in the themes`) | ||||||
|  |             console.log ("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") | ||||||
|  | 
 | ||||||
|             console.log(msg) |             console.log(msg) | ||||||
|  |             console.log ("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") | ||||||
|  |     | ||||||
|             if (process.argv.indexOf("--report") >= 0) { |             if (process.argv.indexOf("--report") >= 0) { | ||||||
|                 console.log("Writing report!") |                 console.log("Writing report!") | ||||||
|                 writeFileSync("layer_report.txt", errors) |                 writeFileSync("layer_report.txt", errors) | ||||||
|  |  | ||||||
|  | @ -33,7 +33,9 @@ const allTests = [ | ||||||
|     new GeoOperationsSpec(), |     new GeoOperationsSpec(), | ||||||
|     new ImageSearcherSpec(), |     new ImageSearcherSpec(), | ||||||
|     new ThemeSpec(), |     new ThemeSpec(), | ||||||
|     new UtilsSpec()] |     new UtilsSpec(), | ||||||
|  |     new UtilsSpec() | ||||||
|  | ] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| for (const test of allTests) { | for (const test of allTests) { | ||||||
|  |  | ||||||
							
								
								
									
										33
									
								
								test/Units.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								test/Units.spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | ||||||
|  | import T from "./TestHelper"; | ||||||
|  | import {Unit} from "../Customizations/JSON/Unit"; | ||||||
|  | import {equal} from "assert"; | ||||||
|  | 
 | ||||||
|  | export default class UnitsSpec extends T { | ||||||
|  | 
 | ||||||
|  |     constructor() { | ||||||
|  |         super("Units", [ | ||||||
|  |             ["Simple canonicalize", () => { | ||||||
|  | 
 | ||||||
|  |                 const unit = new Unit({ | ||||||
|  |                     canonicalDenomination: "m", | ||||||
|  |                     alternativeDenomination: ["meter"], | ||||||
|  |                     'default': true, | ||||||
|  |                     human: { | ||||||
|  |                         en: "meter" | ||||||
|  |                     } | ||||||
|  |                 }, "test") | ||||||
|  | 
 | ||||||
|  |                 equal(unit.canonicalValue("42m"), "42m") | ||||||
|  |                 equal(unit.canonicalValue("42"), "42m") | ||||||
|  |                 equal(unit.canonicalValue("42 m"), "42m") | ||||||
|  |                 equal(unit.canonicalValue("42 meter"), "42m") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             }] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         ]); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue