forked from MapComplete/MapComplete
		
	Refactoring: move the units into the layers instead of the themes
This commit is contained in:
		
							parent
							
								
									3492b5d403
								
							
						
					
					
						commit
						206aff2c9a
					
				
					 16 changed files with 259 additions and 300 deletions
				
			
		|  | @ -1,5 +1,5 @@ | |||
| import {Translation} from "../UI/i18n/Translation"; | ||||
| import UnitConfigJson from "./ThemeConfig/Json/UnitConfigJson"; | ||||
| import {ApplicableUnitJson} from "./ThemeConfig/Json/UnitConfigJson"; | ||||
| import Translations from "../UI/i18n/Translations"; | ||||
| 
 | ||||
| export class Denomination { | ||||
|  | @ -9,7 +9,7 @@ export class Denomination { | |||
|     public readonly alternativeDenominations: string []; | ||||
|     private readonly _human: Translation; | ||||
| 
 | ||||
|     constructor(json: UnitConfigJson, context: string) { | ||||
|     constructor(json: ApplicableUnitJson, context: string) { | ||||
|         context = `${context}.unit(${json.canonicalDenomination})` | ||||
|         this.canonical = json.canonicalDenomination.trim() | ||||
|         if (this.canonical === undefined) { | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import {AndOrTagConfigJson} from "./TagConfigJson"; | |||
| import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; | ||||
| import FilterConfigJson from "./FilterConfigJson"; | ||||
| import {DeleteConfigJson} from "./DeleteConfigJson"; | ||||
| import UnitConfigJson from "./UnitConfigJson"; | ||||
| 
 | ||||
| /** | ||||
|  * Configuration for a single layer | ||||
|  | @ -317,4 +318,64 @@ export interface LayerConfigJson { | |||
|      */ | ||||
|     allowSplit?: boolean | ||||
| 
 | ||||
|     /** | ||||
|      * 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. | ||||
|      * | ||||
|      * # Rendering | ||||
|      * | ||||
|      * To render a value with long (human) denomination, use {canonical(key)} | ||||
|      * | ||||
|      * # 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?: UnitConfigJson[] | ||||
| 
 | ||||
| } | ||||
|  | @ -1,6 +1,6 @@ | |||
| import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; | ||||
| import UnitConfigJson from "./UnitConfigJson"; | ||||
| import {LayerConfigJson} from "./LayerConfigJson"; | ||||
| import UnitConfigJson from "./UnitConfigJson"; | ||||
| 
 | ||||
| /** | ||||
|  * Defines the entire theme. | ||||
|  | @ -217,83 +217,6 @@ export interface LayoutConfigJson { | |||
|      */ | ||||
|     layers: (LayerConfigJson | string | { builtin: string | 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. | ||||
|      * | ||||
|      * # Rendering | ||||
|      * | ||||
|      * To render a value with long (human) denomination, use {canonical(key)} | ||||
|      * | ||||
|      * # 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[], | ||||
| 
 | ||||
|         /** | ||||
|          * The possible denominations | ||||
|          */ | ||||
|         applicableUnits: UnitConfigJson[] | ||||
|         /** | ||||
|          * If set, invalid values will be erased in the MC application (but not in OSM of course!) | ||||
|          * Be careful with setting this | ||||
|          */ | ||||
|         eraseInvalidValues?: boolean; | ||||
|     }[] | ||||
| 
 | ||||
|     /** | ||||
|      * If defined, data will be clustered. | ||||
|      * Defaults to {maxZoom: 16, minNeeded: 500} | ||||
|  |  | |||
|  | @ -1,5 +1,23 @@ | |||
| export default interface UnitConfigJson { | ||||
| 
 | ||||
|     /** | ||||
|      * Every key from this list will be normalized | ||||
|      */ | ||||
|     appliesToKey: string[], | ||||
|     /** | ||||
|      * If set, invalid values will be erased in the MC application (but not in OSM of course!) | ||||
|      * Be careful with setting this | ||||
|      */ | ||||
|     eraseInvalidValues?: boolean; | ||||
|     /** | ||||
|      * The possible denominations | ||||
|      */ | ||||
|     applicableUnits:ApplicableUnitJson[] | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export interface ApplicableUnitJson | ||||
| { | ||||
|     /** | ||||
|      * The canonical value which will be added to the text. | ||||
|      * e.g. "m" for meters | ||||
|  | @ -32,5 +50,4 @@ export default interface UnitConfigJson { | |||
|      * If none is set, the first unit will be considered the default interpretation of a value without a unit | ||||
|      */ | ||||
|     default?: boolean | ||||
| 
 | ||||
| } | ||||
|  | @ -57,16 +57,16 @@ export default class LayerConfig { | |||
| 
 | ||||
|     constructor( | ||||
|         json: LayerConfigJson, | ||||
|         units?: Unit[], | ||||
|         context?: string, | ||||
|         official: boolean = true | ||||
|     ) { | ||||
|         this.units = units ?? []; | ||||
| 
 | ||||
|         context = context + "." + json.id; | ||||
|         const self = this; | ||||
|         this.id = json.id; | ||||
|         this.allowSplit = json.allowSplit ?? false; | ||||
|         this.name = Translations.T(json.name, context + ".name"); | ||||
|         this.units =   (json.units ?? []).map(((unitJson, i) => Unit.fromJson(unitJson, `${context}.unit[${i}]`))) | ||||
| 
 | ||||
|         if (json.description !== undefined) { | ||||
|             if (Object.keys(json.description).length === 0) { | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ import AllKnownLayers from "../../Customizations/AllKnownLayers"; | |||
| import {Utils} from "../../Utils"; | ||||
| import LayerConfig from "./LayerConfig"; | ||||
| import {Unit} from "../Unit"; | ||||
| import {Denomination} from "../Denomination"; | ||||
| import {LayerConfigJson} from "./Json/LayerConfigJson"; | ||||
| 
 | ||||
| export default class LayoutConfig { | ||||
|  | @ -52,7 +51,6 @@ export default class LayoutConfig { | |||
|     How long is the cache valid, in seconds? | ||||
|      */ | ||||
|     public readonly cacheTimeout?: number; | ||||
|     public readonly units: Unit[] = [] | ||||
|     public readonly overpassUrl: string; | ||||
|     public readonly overpassTimeout: number; | ||||
|     private readonly _official: boolean; | ||||
|  | @ -80,7 +78,6 @@ 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"); | ||||
|  | @ -101,7 +98,7 @@ export default class LayoutConfig { | |||
|             } | ||||
|         ); | ||||
|         this.defaultBackgroundId = json.defaultBackgroundId; | ||||
|         this.layers = LayoutConfig.ExtractLayers(json, this.units, official, context); | ||||
|         this.layers = LayoutConfig.ExtractLayers(json, official, context); | ||||
| 
 | ||||
|         // ALl the layers are constructed, let them share tagRenderings now!
 | ||||
|         const roaming: { r, source: LayerConfig }[] = [] | ||||
|  | @ -168,7 +165,7 @@ export default class LayoutConfig { | |||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private static ExtractLayers(json: LayoutConfigJson, units: Unit[], official: boolean, context: string): LayerConfig[] { | ||||
|     private static ExtractLayers(json: LayoutConfigJson, official: boolean, context: string): LayerConfig[] { | ||||
|         const result: LayerConfig[] = [] | ||||
| 
 | ||||
|         json.layers.forEach((layer, i) => { | ||||
|  | @ -176,7 +173,7 @@ export default class LayoutConfig { | |||
|                 if (AllKnownLayers.sharedLayersJson.get(layer) !== undefined) { | ||||
|                     if (json.overrideAll !== undefined) { | ||||
|                         let lyr = JSON.parse(JSON.stringify(AllKnownLayers.sharedLayersJson[layer])); | ||||
|                         const newLayer = new LayerConfig(Utils.Merge(json.overrideAll, lyr), units, `${json.id}+overrideAll.layers[${i}]`, official) | ||||
|                         const newLayer = new LayerConfig(Utils.Merge(json.overrideAll, lyr), `${json.id}+overrideAll.layers[${i}]`, official) | ||||
|                         result.push(newLayer) | ||||
|                         return | ||||
|                     } else { | ||||
|  | @ -194,7 +191,7 @@ export default class LayoutConfig { | |||
|                     layer = Utils.Merge(json.overrideAll, layer); | ||||
|                 } | ||||
|                 // @ts-ignore
 | ||||
|                 const newLayer = new LayerConfig(layer, units, `${json.id}.layers[${i}]`, official) | ||||
|                 const newLayer = new LayerConfig(layer, `${json.id}.layers[${i}]`, official) | ||||
|                 result.push(newLayer) | ||||
|                 return | ||||
|             } | ||||
|  | @ -213,7 +210,7 @@ export default class LayoutConfig { | |||
|                     newLayer = Utils.Merge(json.overrideAll, newLayer); | ||||
|                 } | ||||
|                 // @ts-ignore
 | ||||
|                 const layerConfig = new LayerConfig(newLayer, units, `${json.id}.layers[${i}]`, official) | ||||
|                 const layerConfig = new LayerConfig(newLayer, `${json.id}.layers[${i}]`, official) | ||||
|                 result.push(layerConfig) | ||||
|                 return | ||||
|             }) | ||||
|  | @ -223,54 +220,6 @@ export default class LayoutConfig { | |||
|         return result | ||||
|     } | ||||
| 
 | ||||
|     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]; | ||||
|                 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 Denomination(u, `${context}.units[${i}]`)) | ||||
|                 result.push(new Unit(appliesTo, applicable, unit.eraseInvalidValues ?? false)); | ||||
|             } | ||||
| 
 | ||||
|             const seenKeys = new Set<string>() | ||||
|             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[] { | ||||
|         if (this._official) { | ||||
|             return []; | ||||
|  |  | |||
|  | @ -67,7 +67,7 @@ export default class TagRenderingConfig { | |||
|         } | ||||
|         if (json.freeform) { | ||||
| 
 | ||||
|             if(json.freeform.addExtraTags !== undefined && json.freeform.addExtraTags.length === undefined){ | ||||
|             if(json.freeform.addExtraTags !== undefined && json.freeform.addExtraTags.map === undefined){ | ||||
|                 throw `Freeform.addExtraTags should be a list of strings - not a single string (at ${context})` | ||||
|             } | ||||
|             this.freeform = { | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import BaseUIElement from "../UI/BaseUIElement"; | |||
| import {FixedUiElement} from "../UI/Base/FixedUiElement"; | ||||
| import Combine from "../UI/Base/Combine"; | ||||
| import {Denomination} from "./Denomination"; | ||||
| import UnitConfigJson from "./ThemeConfig/Json/UnitConfigJson"; | ||||
| 
 | ||||
| export class Unit { | ||||
|     public readonly appliesToKeys: Set<string>; | ||||
|  | @ -52,6 +53,35 @@ export class Unit { | |||
|         this.possiblePostFixes.sort((a, b) => b.length - a.length) | ||||
|     } | ||||
| 
 | ||||
|      | ||||
|     static fromJson(json: UnitConfigJson, ctx: string){ | ||||
|         const appliesTo = json.appliesToKey | ||||
|         for (let i = 0; i < appliesTo.length; i++) { | ||||
|             let key = appliesTo[i]; | ||||
|             if (key.trim() !== key) { | ||||
|                 throw `${ctx}.appliesToKey[${i}] is invalid: it starts or ends with whitespace` | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if ((json.applicableUnits ?? []).length === 0) { | ||||
|             throw  `${ctx}: define at least one applicable unit` | ||||
|         } | ||||
|         // Some keys do have unit handling
 | ||||
| 
 | ||||
|         const defaultSet = json.applicableUnits.filter(u => u.default === true) | ||||
|         // No default is defined - we pick the first as default
 | ||||
|         if (defaultSet.length === 0) { | ||||
|             json.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 = json.applicableUnits.map((u, i) => new Denomination(u, `${ctx}.units[${i}]`)) | ||||
|         return new Unit(appliesTo, applicable, json.eraseInvalidValues ?? false) | ||||
|     } | ||||
|      | ||||
|     isApplicableToKey(key: string | undefined): boolean { | ||||
|         if (key === undefined) { | ||||
|             return false; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue