Refactoring: move the units into the layers instead of the themes

This commit is contained in:
pietervdvn 2021-09-13 01:21:47 +02:00
parent 3492b5d403
commit 206aff2c9a
16 changed files with 259 additions and 300 deletions

View file

@ -9,13 +9,11 @@ export default class AllKnownLayers {
public static sharedLayers: Map<string, LayerConfig> = AllKnownLayers.getSharedLayers();
public static sharedLayersJson: Map<string, any> = AllKnownLayers.getSharedLayersJson();
public static sharedUnits: any[] = []
private static getSharedLayers(): Map<string, LayerConfig> {
const sharedLayers = new Map<string, LayerConfig>();
for (const layer of known_layers.layers) {
try {
const parsed = new LayerConfig(layer, AllKnownLayers.sharedUnits, "shared_layers")
const parsed = new LayerConfig(layer, "shared_layers")
sharedLayers.set(layer.id, parsed);
sharedLayers[layer.id] = parsed;
} catch (e) {
@ -35,7 +33,7 @@ export default class AllKnownLayers {
continue;
}
try {
const parsed = new LayerConfig(layer, AllKnownLayers.sharedUnits, "shared_layer_in_theme")
const parsed = new LayerConfig(layer, "shared_layer_in_theme")
sharedLayers.set(layer.id, parsed);
sharedLayers[layer.id] = parsed;
} catch (e) {

View file

@ -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) {

View file

@ -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[]
}

View file

@ -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}

View file

@ -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
}

View file

@ -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) {

View file

@ -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 [];

View file

@ -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 = {

View file

@ -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;

View file

@ -154,5 +154,26 @@
"tower:type=observation"
]
}
}
},
"units": [
{
"appliesToKey": [
"height"
],
"applicableUnits": [
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter",
"mtr"
],
"human": {
"nl": " meter",
"en": " meter"
}
}
],
"eraseInvalidValues": true
}
]
}

View file

@ -913,43 +913,6 @@
"wayHandling": 0
}
],
"units": [
{
"appliesToKey": [
"climbing:length",
"climbing:length:min",
"climbing:length:max"
],
"applicableUnits": [
{
"canonicalDenomination": "",
"alternativeDenomination": [
"m",
"meter",
"meters"
],
"human": {
"en": " meter",
"nl": " meter",
"fr": " mètres"
},
"default": true
},
{
"canonicalDenomination": "ft",
"alternativeDenomination": [
"feet",
"voet"
],
"human": {
"en": " feet",
"nl": " voet",
"fr": " pieds"
}
}
]
}
],
"roamingRenderings": [
{
"#": "Website",
@ -1466,6 +1429,7 @@
]
}
],
"overrideAll": {
"titleIcons": [
{
@ -1497,6 +1461,43 @@
"_embedding_feature:access=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').access",
"_embedding_feature:access:description=JSON.parse(feat.properties._embedding_features_with_access ?? '{}')['access:description']",
"_embedding_feature:id=JSON.parse(feat.properties._embedding_features_with_access ?? '{}').id"
],
"units+": [
{
"appliesToKey": [
"climbing:length",
"climbing:length:min",
"climbing:length:max"
],
"applicableUnits": [
{
"canonicalDenomination": "",
"alternativeDenomination": [
"m",
"meter",
"meters"
],
"human": {
"en": " meter",
"nl": " meter",
"fr": " mètres"
},
"default": true
},
{
"canonicalDenomination": "ft",
"alternativeDenomination": [
"feet",
"voet"
],
"human": {
"en": " feet",
"nl": " voet",
"fr": " pieds"
}
}
]
}
]
}
}

View file

@ -26,26 +26,5 @@
"socialImage": "",
"layers": [
"observation_tower"
],
"units": [
{
"appliesToKey": [
"height"
],
"applicableUnits": [
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter",
"mtr"
],
"human": {
"nl": " meter",
"en": " meter"
}
}
],
"eraseInvalidValues": true
}
]
}

View file

@ -145,82 +145,82 @@
"fr": "Éolienne"
}
}
]
}
],
"units": [
{
"appliesToKey": [
"generator:output:electricity"
],
"applicableUnits": [
"units": [
{
"canonicalDenomination": "MW",
"alternativeDenomination": [
"megawatts",
"megawatt"
"appliesToKey": [
"generator:output:electricity"
],
"human": {
"en": " megawatts",
"nl": " megawatt",
"fr": " megawatts"
}
"applicableUnits": [
{
"canonicalDenomination": "MW",
"alternativeDenomination": [
"megawatts",
"megawatt"
],
"human": {
"en": " megawatts",
"nl": " megawatt",
"fr": " megawatts"
}
},
{
"canonicalDenomination": "kW",
"alternativeDenomination": [
"kilowatts",
"kilowatt"
],
"human": {
"en": " kilowatts",
"nl": " kilowatt",
"fr": " kilowatts"
}
},
{
"canonicalDenomination": "W",
"alternativeDenomination": [
"watts",
"watt"
],
"human": {
"en": " watts",
"nl": " watt",
"fr": " watts"
}
},
{
"canonicalDenomination": "GW",
"alternativeDenomination": [
"gigawatts",
"gigawatt"
],
"human": {
"en": " gigawatts",
"nl": " gigawatt",
"fr": " gigawatts"
}
}
],
"eraseInvalidValues": true
},
{
"canonicalDenomination": "kW",
"alternativeDenomination": [
"kilowatts",
"kilowatt"
"appliesToKey": [
"height",
"rotor:diameter"
],
"human": {
"en": " kilowatts",
"nl": " kilowatt",
"fr": " kilowatts"
}
},
{
"canonicalDenomination": "W",
"alternativeDenomination": [
"watts",
"watt"
],
"human": {
"en": " watts",
"nl": " watt",
"fr": " watts"
}
},
{
"canonicalDenomination": "GW",
"alternativeDenomination": [
"gigawatts",
"gigawatt"
],
"human": {
"en": " gigawatts",
"nl": " gigawatt",
"fr": " gigawatts"
}
}
],
"eraseInvalidValues": true
},
{
"appliesToKey": [
"height",
"rotor:diameter"
],
"applicableUnits": [
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter"
],
"human": {
"en": " meter",
"nl": " meter",
"fr": " mètres"
}
"applicableUnits": [
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter"
],
"human": {
"en": " meter",
"nl": " meter",
"fr": " mètres"
}
}
]
}
]
}

View file

@ -39,26 +39,5 @@
"binocular",
"observation_tower"
],
"hideFromOverview": true,
"units": [
{
"appliesToKey": [
"height"
],
"applicableUnits": [
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter",
"mtr"
],
"human": {
"nl": " meter",
"en": " meter"
}
}
],
"eraseInvalidValues": true
}
]
"hideFromOverview": true
}

View file

@ -41,7 +41,7 @@ for (const layerConfigJson of themeConfigJson.layers) {
layerConfigJson["source"] = {osmTags: tags}
}
// @ts-ignore
const layerConfig = new LayerConfig(layerConfigJson, AllKnownLayers.sharedUnits, "fix theme", true)
const layerConfig = new LayerConfig(layerConfigJson, "fix theme", true)
const images: string[] = Array.from(layerConfig.ExtractImages())
const remoteImages = images.filter(img => img.startsWith("http"))

View file

@ -1,7 +1,6 @@
import ScriptUtils from "./ScriptUtils";
import {writeFileSync} from "fs";
import * as licenses from "../assets/generated/license_info.json"
import AllKnownLayers from "../Customizations/AllKnownLayers";
import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson";
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
@ -49,7 +48,7 @@ class LayerOverviewUtils {
errorCount.push("Layer " + layerJson.id + "still uses the old 'overpassTags'-format. Please use \"source\": {\"osmTags\": <tags>}' instead of \"overpassTags\": <tags> (note: this isn't your fault, the custom theme generator still spits out the old format)")
}
try {
const layer = new LayerConfig(layerJson, AllKnownLayers.sharedUnits, "test", true)
const layer = new LayerConfig(layerJson, "test", true)
const images = Array.from(layer.ExtractImages())
const remoteImages = images.filter(img => img.indexOf("http") == 0)
for (const remoteImage of remoteImages) {
@ -104,7 +103,7 @@ class LayerOverviewUtils {
throw "Duplicate identifier: " + layerFile.parsed.id + " in file " + layerFile.path
}
layerErrorCount.push(...this.validateLayer(layerFile.parsed, layerFile.path, knownPaths))
knownLayerIds.set(layerFile.parsed.id, new LayerConfig(layerFile.parsed, AllKnownLayers.sharedUnits))
knownLayerIds.set(layerFile.parsed.id, new LayerConfig(layerFile.parsed))
}
let themeErrorCount = []
@ -114,6 +113,9 @@ class LayerOverviewUtils {
if (typeof themeFile.language === "string") {
themeErrorCount.push("The theme " + themeFile.id + " has a string as language. Please use a list of strings")
}
if (themeFile["units"] !== undefined) {
themeErrorCount.push("The theme " + themeFile.id + " has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) ")
}
for (const layer of themeFile.layers) {
if (typeof layer === "string") {
if (!knownLayerIds.has(layer)) {
@ -153,7 +155,6 @@ class LayerOverviewUtils {
themeErrorCount.push("Theme ids should be the same as the name.json, but we got id: " + theme.id + " and filename " + filename + " (" + themePath + ")")
}
} catch (e) {
themeErrorCount.push("Could not parse theme " + themeFile["id"] + "due to", e)
}