Refactoring: allow to reuse units, move all units into central file

This commit is contained in:
Pieter Vander Vennet 2023-12-12 03:46:51 +01:00
parent 067fb549c1
commit 94e07d5b13
30 changed files with 1495 additions and 1307 deletions

View file

@ -349,48 +349,27 @@
],
"units": [
{
"appliesToKey": [
"door:width",
"elevator:width",
"elevator:depth"
],
"defaultInput": "cm",
"applicableUnits": [
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter"
],
"useIfNoUnitGiven": true,
"human": {
"en": "meter",
"fr": "mètre",
"de": "Meter",
"nl": "meter",
"pa_PK": "میٹر",
"pl": "metr",
"ca": "metre",
"cs": "metr"
}
},
{
"canonicalDenomination": "cm",
"alternativeDenomination": [
"centimeter",
"cms"
],
"human": {
"en": "centimeter",
"fr": "centimètre",
"de": "Zentimeter",
"nl": "centimeter",
"pa_PK": "سینٹیمیٹر",
"pl": "centymetr",
"ca": "centímetre",
"cs": "centimetr"
}
}
"door:width": {
"quantity": "distance",
"canonical": "m",
"denominations": [
"cm"
]
},
"elevator:width": {
"quantity": "distance",
"canonical": "m",
"denominations": [
"cm"
]
},
"elevator:depth": {
"quantity": "distance",
"canonical": "m",
"denominations": [
"cm"
]
}
}
]
}

View file

@ -567,47 +567,20 @@
],
"units": [
{
"appliesToKey": [
"kerb:height",
"width"
],
"defaultInput": "cm",
"applicableUnits": [
{
"useIfNoUnitGiven": true,
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter"
],
"human": {
"en": "meter",
"fr": "mètre",
"de": "Meter",
"nl": "meter",
"pa_PK": "میٹر",
"pl": "metr",
"ca": "metre",
"cs": "metr"
}
},
{
"canonicalDenomination": "cm",
"alternativeDenomination": [
"centimeter",
"cms"
],
"human": {
"en": "centimeter",
"fr": "centimètre",
"de": "Zentimeter",
"nl": "centimeter",
"pa_PK": "سینٹیمیٹر",
"pl": "centrymetr",
"ca": "centimetre",
"cs": "centimetr"
}
}
"kerb:height": {
"quantity": "distance",
"canonical": "m",
"denominations": [
"cm"
]
},
"width": {
"quantity": "distance",
"canonical": "m",
"denominations": [
"cm"
]
}
}
]
}

View file

@ -549,37 +549,12 @@
],
"units": [
{
"applicableUnits": [
{
"canonicalDenomination": "",
"alternativeDenomination": [
"mm",
"millimeter",
"millimeters"
],
"human": {
"en": "millimeters",
"nl": "millimeter",
"de": "Millimeter",
"pa_PK": "ملیمیٹر",
"ru": "миллиметры",
"ca": "mil·límetres",
"cs": "milimetry"
},
"humanSingular": {
"en": "millimeter",
"nl": "millimeter",
"de": "Millimeter",
"pa_PK": "ملیمیٹر",
"ru": "миллиметр",
"ca": "mil·límetre",
"cs": "milimetr"
}
}
],
"appliesToKey": [
"fire_hydrant:diameter"
"fire_hydrant:diameter": {
"quantity": "distance",
"denominations": [
"mm"
]
}
}
]
}

View file

@ -394,69 +394,13 @@
],
"units": [
{
"applicableUnits": [
{
"canonicalDenomination": "cm",
"alternativeDenomination": [
"centimeter",
"centimeters"
],
"human": {
"en": "centimeters",
"nl": "centimeter",
"de": "Zentimeter",
"fr": "centimètres",
"pa_PK": "سینٹیمیٹر",
"ru": "сантиметры",
"ca": "centímetres",
"pl": "centymetry",
"cs": "centimetry"
},
"humanSingular": {
"en": "centimeter",
"nl": "centimeter",
"de": "Zentimeter",
"fr": "centimètre",
"pa_PK": "سینٹیمیٹر",
"ru": "сантиметр",
"ca": "centímetre",
"pl": "centymetr",
"cs": "centimetr"
}
},
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter",
"meters"
],
"human": {
"en": "meters",
"nl": "meter",
"de": "Meter",
"fr": "mètres",
"pa_PK": "میٹر",
"ru": "метры",
"ca": "metres",
"pl": "metry",
"cs": "metry"
},
"humanSingular": {
"en": "meter",
"nl": "meter",
"de": "Meter",
"fr": "mètre",
"pa_PK": "میٹر",
"ru": "метр",
"ca": "metre",
"pl": "metr",
"cs": "metr"
}
}
],
"appliesToKey": [
"kerb:height"
"kerb:height": {
"quantity": "distance",
"denominations": [
"cm",
"m"
]
}
}
]
}

View file

@ -155,73 +155,13 @@
"allowSplit": true,
"units": [
{
"applicableUnits": [
{
"#": "km/h is the default for a maxspeed; should be empty string",
"canonicalDenomination": "",
"alternativeDenomination": [
"km/u",
"kmh",
"kph"
],
"human": {
"en": "kilometers/hour",
"ca": "quilòmetres/hora",
"es": "kilómetros/hora",
"nl": "kilometers/uur",
"de": "Kilometer/Stunde",
"pa_PK": "ہر گھنٹہ وچ کیلومیٹر",
"fr": "kilomètres/heure",
"cs": "km/hod"
},
"humanShort": {
"en": "km/h",
"ca": "km/h",
"es": "km/h",
"nl": "km/u",
"de": "km/h",
"pa_PK": "ہر گھنٹے وچ کیلومیٹر",
"ru": "км/ч",
"fr": "km/h",
"cs": "km/h"
}
},
{
"canonicalDenomination": "mph",
"useIfNoUnitGiven": [
"gb",
"us"
],
"alternativeDenomination": [
"m/u",
"mh",
"m/ph"
],
"human": {
"en": "miles/hour",
"ca": "milles/hora",
"es": "millas/hora",
"nl": "miles/uur",
"de": "Meilen/Stunde",
"pa_PK": "ہر گھنٹہ وچ میل",
"fr": "miles/heure",
"cs": "míle/hod"
},
"humanShort": {
"en": "mph",
"ca": "mph",
"es": "mph",
"nl": "mph",
"de": "mph",
"pa_PK": "ہر گھنٹہ وچ میل",
"fr": "mph",
"cs": "mph"
}
}
],
"appliesToKey": [
"maxspeed"
"maxspeed": {
"quantity": "speed",
"canonical": "kmh",
"denominations": [
"mph"
]
}
}
]
}

View file

@ -362,29 +362,12 @@
},
"units": [
{
"appliesToKey": [
"height"
],
"applicableUnits": [
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter",
"mtr"
],
"human": {
"nl": " meter",
"en": " meter",
"ru": " метр",
"de": " Meter",
"ca": " metre",
"es": " metros",
"pl": " metr",
"cs": " metr"
"height": {
"quantity": "distance",
"denominations": [
"m"
]
}
}
],
"eraseInvalidValues": true
}
]
}

View file

@ -98,44 +98,13 @@
],
"units": [
{
"appliesToKey": [
"desk:height"
],
"applicableUnits": [
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter"
],
"human": {
"en": "meter",
"fr": "mètre",
"de": "Meter",
"nl": "meter",
"pa_PK": "میٹر",
"ca": "metre",
"pl": "metr",
"cs": "metr"
}
},
{
"canonicalDenomination": "cm",
"alternativeDenomination": [
"centimeter",
"cms"
],
"human": {
"en": "centimeter",
"fr": "centimètre",
"de": "Zentimeter",
"nl": "centimeter",
"pa_PK": "سینٹیمیٹر",
"ca": "centímetre",
"pl": "centymetr",
"cs": "centimetr"
}
}
"desk:height": {
"quantity": "distance",
"denominations": [
"m",
"cm"
]
}
}
]
}

View file

@ -116,64 +116,13 @@
],
"units": [
{
"appliesToKey": [
"maxspeed"
],
"applicableUnits": [
{
"#": "km/h is the default for a maxspeed; should be empty string",
"canonicalDenomination": "",
"alternativeDenomination": [
"km/u",
"maxspeed": {
"quantity": "speed",
"denominations": [
"kmh",
"kph"
],
"human": {
"en": "kilometers/hour",
"ca": "quilòmetres/hora",
"es": "kilómetros/hora",
"nl": "kilometers/uur",
"de": "Kilometer/Stunde",
"cs": "kilometry/hodinu"
},
"humanShort": {
"en": "km/h",
"ca": "km/h",
"es": "km/h",
"nl": "km/u",
"de": "km/h",
"cs": "km/h"
}
},
{
"canonicalDenomination": "mph",
"useIfNoUnitGiven": [
"gb",
"us"
],
"alternativeDenomination": [
"m/u",
"mh",
"m/ph"
],
"human": {
"en": "miles/hour",
"ca": "milles/hora",
"es": "millas/hora",
"nl": "miles/uur",
"de": "Meilen/Stunde",
"cs": "míle/hodinu"
},
"humanShort": {
"en": "mph",
"ca": "mph",
"es": "mph",
"nl": "mph",
"de": "mph",
"cs": "mph"
}
}
"mph"
]
}
}
]
}

View file

@ -115,64 +115,13 @@
],
"units": [
{
"appliesToKey": [
"maxspeed"
],
"applicableUnits": [
{
"#": "km/h is the default for a maxspeed; should be empty string",
"canonicalDenomination": "",
"alternativeDenomination": [
"km/u",
"kmh",
"kph"
],
"human": {
"en": "kilometers/hour",
"ca": "quilòmetres/hora",
"es": "kilómetros/hora",
"nl": "kilometers/uur",
"de": "Kilometer/Stunde",
"cs": "kilometry/hodinu"
},
"humanShort": {
"en": "km/h",
"ca": "km/h",
"es": "km/h",
"nl": "km/u",
"de": "km/h",
"cs": "km/h"
}
},
{
"canonicalDenomination": "mph",
"useIfNoUnitGiven": [
"gb",
"us"
],
"alternativeDenomination": [
"m/u",
"mh",
"m/ph"
],
"human": {
"en": "miles/hour",
"ca": "milles/hora",
"es": "millas/hora",
"nl": "miles/uur",
"de": "Meilen/Stunde",
"cs": "míle/hodinu"
},
"humanShort": {
"en": "mph",
"ca": "mph",
"es": "mph",
"nl": "mph",
"de": "mph",
"cs": "mph"
}
}
"maxspeed": {
"quantity": "speed",
"canonical": "kmh",
"denominations": [
"mph"
]
}
}
]
}

View file

@ -460,7 +460,8 @@
"if": "surface=fine_gravel",
"then": {
"en": "The surface is <b>fine gravel</b>",
"nl": "De ondergrond bestaat uit <b>grind</b>"
"nl": "De ondergrond bestaat uit <b>grind</b>",
"de": "Die Oberfläche ist <b>feiner Kies</b>"
}
}
],

View file

@ -841,44 +841,13 @@
},
"units": [
{
"appliesToKey": [
"door:width"
],
"applicableUnits": [
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter"
],
"human": {
"en": "meter",
"nl": "meter",
"fr": "mètre",
"de": "Meter",
"da": "meter",
"pa_PK": "میٹر",
"ca": "metre",
"cs": "metr"
}
},
{
"canonicalDenomination": "cm",
"alternativeDenomination": [
"centimeter",
"cms"
],
"human": {
"en": "centimeter",
"nl": "centimeter",
"fr": "centimètre",
"de": "Zentimeter",
"da": "centimeter",
"pa_PK": "سینٹیمیٹر",
"ca": "centimetre",
"cs": "centimetr"
}
}
"door:width": {
"quantity": "distance",
"denominations": [
"m",
"cm"
]
}
}
]
}

View file

@ -480,42 +480,13 @@
},
"units": [
{
"appliesToKey": [
"toilets:door:width"
],
"applicableUnits": [
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter"
],
"human": {
"en": "meter",
"nl": "meter",
"fr": "mètre",
"de": "Meter",
"da": "meter",
"ca": "metre",
"cs": "metr"
}
},
{
"canonicalDenomination": "cm",
"alternativeDenomination": [
"centimeter",
"cms"
],
"human": {
"en": "centimeter",
"nl": "centimeter",
"fr": "centimètre",
"de": "Zentimeter",
"da": "centimeter",
"ca": "centimetre",
"cs": "centimetr"
}
}
"toilets:door:width": {
"quantity": "distance",
"denominations": [
"m",
"cm"
]
}
}
]
}

View file

@ -0,0 +1,384 @@
{
"id": "unit",
"description": {
"en": "Library layer with all (common) units. Units can _only_ be imported from this file"
},
"source": "special:library",
"units": [
{
"quantity": "power",
"applicableUnits": [
{
"canonicalDenomination": "MW",
"alternativeDenomination": [
"megawatts",
"megawatt"
],
"human": {
"en": "{quantity} megawatts",
"nl": "{quantity} megawatt",
"fr": "{quantity} megawatts",
"de": "{quantity} Megawatt",
"eo": "{quantity} megavatoj",
"it": "{quantity} megawatt",
"ru": "{quantity} мегаватт",
"zh_Hant": "{quantity} 百萬瓦",
"id": "{quantity} megawat",
"hu": "{quantity} megawatt",
"ca": "{quantity} megavats",
"da": "{quantity} Megawatt",
"cs": "{quantity} megawatty"
}
},
{
"canonicalDenomination": "kW",
"alternativeDenomination": [
"kilowatts",
"kilowatt"
],
"human": {
"en": "{quantity} kilowatt",
"nl": "{quantity} kilowatt",
"fr": "{quantity} kilowatts",
"de": "{quantity} Kilowatt",
"eo": "{quantity} kilovatoj",
"it": "{quantity} kilowatt",
"nb_NO": "{quantity} kilowatt",
"ru": "{quantity} киловатт",
"zh_Hant": "{quantity} 千瓦",
"id": "{quantity} kilowat",
"hu": "{quantity} kilowatt",
"ca": "{quantity} quilovats",
"da": "{quantity} Kilowatt",
"cs": "{quantity} kilowatty"
}
},
{
"canonicalDenomination": "W",
"alternativeDenomination": [
"watts",
"watt"
],
"human": {
"en": "{quantity} watts",
"nl": "{quantity} watt",
"fr": "{quantity} watts",
"de": "{quantity} Watt",
"eo": "{quantity} vatoj",
"it": "{quantity} watt",
"ru": "{quantity} ватт",
"id": "{quantity} watt",
"hu": "{quantity} watt",
"ca": "{quantity} vats",
"da": "{quantity} Watt",
"cs": "{quantity} watty",
"zh_Hant": "{quantity} 瓦"
}
},
{
"canonicalDenomination": "GW",
"alternativeDenomination": [
"gigawatts",
"gigawatt"
],
"human": {
"en": "{quantity} gigawatts",
"nl": "{quantity} gigawatt",
"fr": "{quantity} gigawatts",
"de": "{quantity} Gigawatt",
"eo": "{quantity} gigavatoj",
"it": "{quantity} gigawatt",
"ru": "{quantity} гигаватт",
"id": "{quantity} gigawatt",
"hu": "{quantity} gigawatt",
"ca": "{quantity} gigavats",
"da": "{quantity} Gigawatt",
"cs": "{quantity} gigawatty",
"zh_Hant": "{quantity} 千兆瓦"
}
}
],
"eraseInvalidValues": true
},
{
"quantity": "voltage",
"applicableUnits": [
{
"canonicalDenomination": "V",
"alternativeDenomination": [
"v",
"volt",
"voltage",
"Volt"
],
"human": {
"en": "{quantity} Volt",
"nl": "{quantity} volt"
}
}
],
"eraseInvalidValues": true
},
{
"quantity": "current",
"applicableUnits": [
{
"canonicalDenomination": "A",
"alternativeDenomination": [
"a",
"amp",
"amperage",
"A"
],
"human": {
"en": "{quantity} A",
"nl": "{quantity} A"
}
}
],
"eraseInvalidValues": true
},
{
"quantity": "distance",
"eraseInvalidValue": true,
"applicableUnits": [
{
"canonicalDenomination": "m",
"useIfNoUnitGiven": true,
"alternativeDenomination": [
"meter",
"meters"
],
"human": {
"en": "{quantity} meter",
"nl": "{quantity} meter",
"fr": "{quantity} mètres",
"de": "{quantity} Meter",
"eo": "{quantity} metro",
"it": "{quantity} metri",
"ru": "{quantity} метр",
"id": "{quantity} meter",
"hu": "{quantity} méter",
"ca": "{quantity} metre",
"da": "{quantity} meter",
"cs": "{quantity} metr",
"es": "{quantity} metros",
"pl": "{quantity} metr",
"pa_PK": "{quantity}میٹر",
"zh_Hant": "{quantity} 公尺",
"nb_NO": "{quantity} meter",
"eu": "{quantity} ·metro"
},
"humanSingular": {
"en": "one meter",
"fr": "un mètre",
"nl": "één meter",
"de": "ein Meter"
}
},
{
"canonicalDenomination": "cm",
"alternativeDenomination": [
"centimeter",
"centimeters",
"cms"
],
"human": {
"en": "{quantity} centimeter",
"fr": "{quantity} centimètres",
"de": "{quantity} Zentimeter",
"da": "{quantity} centimeter",
"nl": "{quantity} centimeter",
"ca": "{quantity} centimetre",
"cs": "{quantity} centimetr",
"pl": "{quantity} centymetr",
"ru": "{quantity} сантиметры",
"pa_PK": " {quantity}سینٹیمیٹر"
},
"humanSingular": {
"en": "one centimeter",
"nl": "één centimeter"
}
},
{
"canonicalDenomination": "mm",
"alternativeDenomination": [
"millimeter",
"millimeters"
],
"human": {
"en": "{quantity} millimeters",
"nl": "{quantity} millimeter",
"de": "{quantity} Millimeter",
"ru": "{quantity} миллиметры",
"ca": "{quantity} mil·límetres",
"cs": "{quantity} milimetry",
"pa_PK": "{quantity} ملیمیٹر"
},
"humanSingular": {
"en": "one millimeter",
"nl": "één millimeter",
"de": "ein Millimeter"
}
},
{
"canonicalDenomination": "ft",
"alternativeDenomination": [
"feet",
"voet"
],
"human": {
"en": "{quantity} feet",
"nl": "{quantity} voet",
"fr": "{quantity} pieds",
"de": "{quantity} Fuß",
"eo": "{quantity} futo",
"it": "{quantity} piedi",
"ca": "{quantity} peus",
"es": "{quantity} pies",
"da": "{quantity} fod",
"cs": "{quantity} stopa",
"eu": "{quantity} ·hanka",
"pl": "{quantity} stopy",
"nb_NO": "{quantity} fot",
"pa_PK": "{quantity} ؜ فوٹ"
}
}
]
},
{
"quantity": "speed",
"applicableUnits": [
{
"#": "km/h is the default for a maxspeed; should be empty string",
"canonicalDenomination": "kmh",
"alternativeDenomination": [
"km/u",
"km/h",
"kph"
],
"human": {
"en": "{quantity} kilometers/hour",
"ca": "{quantity} quilòmetres/hora",
"es": "{quantity} kilómetros/hora",
"nl": "{quantity} kilometers/uur",
"de": "{quantity} Kilometer/Stunde",
"cs": "{quantity} kilometry/hodinu",
"pa_PK": "{quantity}ہر گھنٹہ وچ کیلومیٹر",
"fr": "{quantity} kilomètres/heure"
},
"humanShort": {
"en": "{quantity} km/h",
"ca": "{quantity} km/h",
"es": "{quantity} km/h",
"nl": "{quantity} km/u",
"de": "{quantity} km/h",
"cs": "{quantity} km/h",
"pa_PK": "{quantity}ہر گھنٹے وچ کیلومیٹر",
"ru": "{quantity} км/ч",
"fr": "{quantity} km/h"
}
},
{
"canonicalDenomination": "mph",
"addSpace": true,
"useIfNoUnitGiven": [
"gb",
"us"
],
"alternativeDenomination": [
"m/u",
"mh",
"m/ph"
],
"human": {
"en": "{quantity} miles/hour",
"ca": "{quantity} milles/hora",
"es": "{quantity} millas/hora",
"nl": "{quantity} miles/uur",
"de": "{quantity} Meilen/Stunde",
"cs": "{quantity} míle/hodinu",
"fr": "{quantity} miles/heure",
"pa_PK": "{quantity} ہر گھنٹہ وچ میل"
},
"humanShort": {
"en": "{quantity} mph",
"ca": "{quantity} mph",
"es": "{quantity} mph",
"nl": "{quantity} mph",
"de": "{quantity} mph",
"cs": "{quantity} mph",
"pa_PK": "{quantity}ہر گھنٹہ وچ میل",
"fr": "{quantity} mph"
}
}
]
},
{
"quantity": "duration",
"applicableUnits": [
{
"canonicalDenomination": "minutes",
"addSpace": true,
"canonicalDenominationSingular": "minute",
"alternativeDenomination": [
"m",
"min",
"mins",
"minuten",
"mns"
],
"human": {
"en": "{quantity} minutes",
"nl": "{quantity} minuten"
},
"humanSingular": {
"en": "one minute",
"nl": "één minuut"
}
},
{
"canonicalDenomination": "hours",
"addSpace": true,
"canonicalDenominationSingular": "hour",
"alternativeDenomination": [
"h",
"hrs",
"hours",
"u",
"uur",
"uren"
],
"human": {
"en": "{quantity} hours",
"nl": "{quantity} uren"
},
"humanSingular": {
"en": "one hour",
"nl": "één uur"
}
},
{
"canonicalDenomination": "days",
"addSpace": true,
"canonicalDenominationSingular": "day",
"alternativeDenomination": [
"dys",
"dagen",
"dag"
],
"human": {
"en": "{quantity} days",
"nl": "{quantity} day"
},
"humanSingular": {
"en": "one day",
"nl": "één dag"
}
}
]
}
],
"pointRendering": null,
"lineRendering": null
}

View file

@ -123,45 +123,20 @@
],
"units": [
{
"appliesToKey": [
"width",
"_biggest_width"
],
"defaultUnit": "cm",
"applicableUnits": [
{
"useIfNoUnitGiven": true,
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter"
],
"human": {
"en": "meter",
"fr": "mètre",
"de": "Meter",
"da": "meter",
"nl": "meter",
"ca": "metre",
"cs": "metr"
}
},
{
"canonicalDenomination": "cm",
"alternativeDenomination": [
"centimeter",
"cms"
],
"human": {
"en": "centimeter",
"fr": "centimètre",
"de": "Zentimeter",
"da": "centimeter",
"nl": "centimeter",
"ca": "centimetre",
"cs": "centimetr"
}
}
"width": {
"quantity": "distance",
"denominations": [
"m",
"cm"
]
},
"_biggest_width": {
"quantity": "distance",
"denominations": [
"m",
"cm"
]
}
}
]
}

View file

@ -300,130 +300,19 @@
],
"units": [
{
"appliesToKey": [
"generator:output:electricity"
],
"applicableUnits": [
{
"canonicalDenomination": "MW",
"alternativeDenomination": [
"megawatts",
"megawatt"
],
"human": {
"en": " megawatts",
"nl": " megawatt",
"fr": " megawatts",
"de": " Megawatt",
"eo": " megavatoj",
"it": " megawatt",
"ru": " мегаватт",
"zh_Hant": " 百萬瓦",
"id": " megawat",
"hu": " megawatt",
"ca": " megavats",
"da": " Megawatt",
"cs": " megawatty"
}
},
{
"canonicalDenomination": "kW",
"alternativeDenomination": [
"kilowatts",
"kilowatt"
],
"human": {
"en": " kilowatts",
"nl": " kilowatt",
"fr": " kilowatts",
"de": " Kilowatt",
"eo": " kilovatoj",
"it": " kilowatt",
"nb_NO": " kilowatt",
"ru": " киловатт",
"zh_Hant": " 千瓦",
"id": " kilowat",
"hu": " kilowatt",
"ca": " quilovats",
"da": " Kilowatt",
"cs": " kilowatty"
}
},
{
"canonicalDenomination": "W",
"alternativeDenomination": [
"watts",
"watt"
],
"human": {
"en": " watts",
"nl": " watt",
"fr": " watts",
"de": " Watt",
"eo": " vatoj",
"it": " watt",
"ru": " ватт",
"zh_Hant": " 瓦",
"id": " watt",
"hu": " watt",
"ca": " vats",
"da": " Watt",
"cs": " watty"
}
},
{
"canonicalDenomination": "GW",
"alternativeDenomination": [
"gigawatts",
"gigawatt"
],
"human": {
"en": " gigawatts",
"nl": " gigawatt",
"fr": " gigawatts",
"de": " Gigawatt",
"eo": " gigavatoj",
"it": " gigawatt",
"ru": " гигаватт",
"zh_Hant": " 千兆瓦",
"id": " gigawatt",
"hu": " gigawatt",
"ca": " gigavats",
"da": " Gigawatt",
"cs": " gigawatty"
}
}
],
"eraseInvalidValues": true
},
{
"appliesToKey": [
"height",
"rotor:diameter"
],
"applicableUnits": [
{
"canonicalDenomination": "m",
"alternativeDenomination": [
"meter"
],
"human": {
"en": " meter",
"nl": " meter",
"fr": " mètres",
"de": " Meter",
"eo": " metro",
"it": " metri",
"ru": " метр",
"zh_Hant": " 公尺",
"id": " meter",
"hu": " méter",
"ca": " metre",
"da": " meter",
"cs": " metr"
}
}
"generator:output:electricity": "power",
"height": {
"quantity": "distance",
"denominations": [
"m"
]
},
"rotor:diamter": {
"quantity": "distance",
"denominations": [
"m"
]
}
}
]
}

View file

@ -120,61 +120,27 @@
],
"units+": [
{
"appliesToKey": [
"climbing:length",
"climbing:length:min",
"climbing:length:max"
],
"applicableUnits": [
{
"canonicalDenomination": "",
"alternativeDenomination": [
"m",
"meter",
"meters"
],
"human": {
"en": " meter",
"nl": " meter",
"fr": " mètres",
"de": " Meter",
"eo": " metro",
"it": " metri",
"ru": " метр",
"ca": " metre",
"nb_NO": " meter",
"es": " metro",
"da": " meter",
"pa_PK": " ؜ میٹر",
"cs": " metr",
"eu": " ·metro",
"pl": " metry"
}
},
{
"canonicalDenomination": "ft",
"alternativeDenomination": [
"feet",
"voet"
],
"human": {
"en": " feet",
"nl": " voet",
"fr": " pieds",
"de": " Fuß",
"eo": " futo",
"it": " piedi",
"ca": " peus",
"nb_NO": " fot",
"es": " pies",
"da": " fod",
"pa_PK": " ؜ فوٹ",
"cs": " stopa",
"eu": " ·hanka",
"pl": " stopy"
}
}
"climbing:length": {
"quantity": "distance",
"canonical": "m",
"denominations": [
"ft"
]
},
"climbing:length:min": {
"quantity": "distance",
"canonical": "m",
"denominations": [
"ft"
]
},
"climbing:length:max": {
"quantity": "distance",
"canonical": "m",
"denominations": [
"ft"
]
}
}
],
"tagRenderings+": [

View file

@ -1,6 +1,6 @@
import Combine from "../src/UI/Base/Combine"
import BaseUIElement from "../src/UI/BaseUIElement"
import { existsSync, mkdirSync, readFileSync, writeFile, writeFileSync } from "fs"
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
import { AllKnownLayouts } from "../src/Customizations/AllKnownLayouts"
import TableOfContents from "../src/UI/Base/TableOfContents"
import SimpleMetaTaggers from "../src/Logic/SimpleMetaTagger"
@ -15,7 +15,7 @@ import themeOverview from "../src/assets/generated/theme_overview.json"
import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig"
import bookcases from "../src/assets/generated/themes/bookcases.json"
import fakedom from "fake-dom"
import unit from "../src/assets/generated/layers/unit.json"
import Hotkeys from "../src/UI/Base/Hotkeys"
import { QueryParameters } from "../src/Logic/Web/QueryParameters"
import Link from "../src/UI/Base/Link"
@ -29,14 +29,170 @@ import questions from "../src/assets/generated/layers/questions.json"
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
import { Utils } from "../src/Utils"
import { TagUtils } from "../src/Logic/Tags/TagUtils"
function WriteFile(
import Script from "./Script"
/**
* Converts a markdown-file into a .json file, which a walkthrough/slideshow element can use
*
* These are used in the studio
*/
class ToSlideshowJson {
private readonly _source: string
private readonly _target: string
constructor(source: string, target: string) {
this._source = source
this._target = target
}
public convert() {
const lines = readFileSync(this._source, "utf8").split("\n")
const sections: string[][] = []
let currentSection: string[] = []
for (let line of lines) {
if (line.trim().startsWith("# ")) {
sections.push(currentSection)
currentSection = []
}
line = line.replace('src="../../public/', 'src="./')
line = line.replace('src="../../', 'src="./')
currentSection.push(line)
}
sections.push(currentSection)
writeFileSync(
this._target,
JSON.stringify({
sections: sections.map((s) => s.join("\n")).filter((s) => s.length > 0),
})
)
}
}
/**
* Generates a wiki page with the theme overview
* The wikitable should be updated regularly as some tools show an overview of apps based on the wiki.
*/
class WikiPageGenerator {
private readonly _target: string
constructor(target: string = "Docs/wikiIndex.txt") {
this._target = target
}
generate() {
let wikiPage =
'{|class="wikitable sortable"\n' +
"! Name, link !! Genre !! Covered region !! Language !! Description !! Free materials !! Image\n" +
"|-"
for (const layout of themeOverview) {
if (layout.hideFromOverview) {
continue
}
wikiPage += "\n" + this.generateWikiEntryFor(layout)
}
wikiPage += "\n|}"
writeFileSync(this._target, wikiPage)
}
private generateWikiEntryFor(layout: {
hideFromOverview: boolean
id: string
shortDescription: any
}): string {
if (layout.hideFromOverview) {
return ""
}
const languagesInDescr = Array.from(Object.keys(layout.shortDescription)).filter(
(k) => k !== "_context"
)
const languages = languagesInDescr.map((ln) => `{{#language:${ln}|en}}`).join(", ")
let auth = "Yes"
return `{{service_item
|name= [https://mapcomplete.org/${layout.id} ${layout.id}]
|region= Worldwide
|lang= ${languages}
|descr= A MapComplete theme: ${Translations.T(layout.shortDescription)
.textFor("en")
.replace("<a href='", "[[")
.replace(/'>.*<\/a>/, "]]")}
|material= {{yes|[https://mapcomplete.org/ ${auth}]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, ${layout.id}
}}`
}
}
export class GenerateDocs extends Script {
constructor() {
super("Generates various documentation files")
}
async main(args: string[]) {
console.log("Starting documentation generation...")
ScriptUtils.fixUtils()
if (!existsSync("./Docs/Themes")) {
mkdirSync("./Docs/Themes")
}
this.WriteFile("./Docs/Tags_format.md", TagUtils.generateDocs(), [
"src/Logic/Tags/TagUtils.ts",
])
new ToSlideshowJson(
"./Docs/Studio/Introduction.md",
"./src/assets/studio_introduction.json"
).convert()
new ToSlideshowJson(
"./Docs/Studio/TagRenderingIntro.md",
"./src/assets/studio_tagrenderings_intro.json"
).convert()
this.generateHotkeyDocs()
this.generateBuiltinIndex()
this.generateQueryParameterDocs()
this.generateBuiltinQuestions()
this.generateOverviewsForAllSingleLayer()
this.generateLayerOverviewText()
this.generateBuiltinUnits()
Array.from(AllKnownLayouts.allKnownLayouts.values()).map((theme) => {
this.generateForTheme(theme)
})
this.WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage(), [
"src/UI/SpecialVisualizations.ts",
])
this.WriteFile(
"./Docs/CalculatedTags.md",
new Combine([
new Title("Metatags", 1),
SimpleMetaTaggers.HelpText(),
ExtraFunctions.HelpText(),
]).SetClass("flex-col"),
["src/Logic/SimpleMetaTagger.ts", "src/Logic/ExtraFunctions.ts"]
)
this.WriteFile("./Docs/SpecialInputElements.md", Validators.HelpText(), [
"src/UI/InputElement/Validators.ts",
])
new WikiPageGenerator().generate()
console.log("Generated docs")
}
private WriteFile(
filename,
html: string | BaseUIElement,
autogenSource: string[],
options?: {
noTableOfContents: boolean
}
): void {
): void {
if (!html) {
return
}
@ -80,112 +236,52 @@ function WriteFile(
"[//]: # (WARNING: this file is automatically generated. Please find the sources at the bottom and edit those sources)"
writeFileSync(filename, warnAutomated + md)
}
function GenerateDocumentationForTheme(theme: LayoutConfig): BaseUIElement {
return new Combine([
new Title(
new Combine([
theme.title,
"(",
new Link(theme.id, "https://mapcomplete.org/" + theme.id),
")",
]),
2
),
theme.description,
"This theme contains the following layers:",
new List(
theme.layers
.filter((l) => !l.id.startsWith("note_import_"))
.map((l) => new Link(l.id, "../Layers/" + l.id + ".md"))
),
"Available languages:",
new List(theme.language.filter((ln) => ln !== "_context")),
]).SetClass("flex flex-col")
}
/**
* Generates the documentation for the layers overview page
* @constructor
*/
function GenLayerOverviewText(): BaseUIElement {
for (const id of Constants.priviliged_layers) {
if (!AllSharedLayers.sharedLayers.has(id)) {
console.error("Priviliged layer definition not found: " + id)
return undefined
}
}
const allLayers: LayerConfig[] = Array.from(AllSharedLayers.sharedLayers.values()).filter(
(layer) => layer["source"] === null
private generateHotkeyDocs() {
new ThemeViewState(new LayoutConfig(<any>bookcases))
this.WriteFile("./Docs/Hotkeys.md", Hotkeys.generateDocumentation(), [])
}
private generateBuiltinUnits() {
const layer = new LayerConfig(<LayerConfigJson>unit, "units", true)
const els: (BaseUIElement | string)[] = [new Title(layer.id, 2)]
for (const unit of layer.units) {
els.push(new Title(unit.quantity))
for (const denomination of unit.denominations) {
els.push(new Title(denomination.canonical, 4))
if (denomination.useIfNoUnitGiven === true) {
els.push("*Default denomination*")
} else if (
denomination.useIfNoUnitGiven &&
denomination.useIfNoUnitGiven.length > 0
) {
els.push("Default denomination in the following countries:")
els.push(new List(denomination.useIfNoUnitGiven))
}
if (denomination.prefix) {
els.push("Prefixed")
}
if (denomination.alternativeDenominations.length > 0) {
els.push(
"Alternative denominations:",
new List(denomination.alternativeDenominations)
)
const builtinLayerIds: Set<string> = new Set<string>()
allLayers.forEach((l) => builtinLayerIds.add(l.id))
const themesPerLayer = new Map<string, string[]>()
for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) {
for (const layer of layout.layers) {
if (!builtinLayerIds.has(layer.id)) {
continue
}
if (!themesPerLayer.has(layer.id)) {
themesPerLayer.set(layer.id, [])
}
themesPerLayer.get(layer.id).push(layout.id)
}
}
// Determine the cross-dependencies
const layerIsNeededBy: Map<string, string[]> = new Map<string, string[]>()
for (const layer of allLayers) {
for (const dep of DependencyCalculator.getLayerDependencies(layer)) {
const dependency = dep.neededLayer
if (!layerIsNeededBy.has(dependency)) {
layerIsNeededBy.set(dependency, [])
}
layerIsNeededBy.get(dependency).push(layer.id)
}
}
return new Combine([
new Title("Special and other useful layers", 1),
"MapComplete has a few data layers available in the theme which have special properties through builtin-hooks. Furthermore, there are some normal layers (which are built from normal Theme-config files) but are so general that they get a mention here.",
new Title("Priviliged layers", 1),
new List(Constants.priviliged_layers.map((id) => "[" + id + "](#" + id + ")")),
...Utils.NoNull(
Constants.priviliged_layers.map((id) => AllSharedLayers.sharedLayers.get(id))
).map((l) =>
l.GenerateDocumentation(
themesPerLayer.get(l.id),
layerIsNeededBy,
DependencyCalculator.getLayerDependencies(l),
Constants.added_by_default.indexOf(<any>l.id) >= 0,
Constants.no_include.indexOf(<any>l.id) < 0
)
),
new Title("Normal layers", 1),
"The following layers are included in MapComplete:",
new List(
Array.from(AllSharedLayers.sharedLayers.keys()).map(
(id) => new Link(id, "./Layers/" + id + ".md")
)
),
this.WriteFile("./Docs/builtin_units.md", new Combine([new Title("Units", 1), ...els]), [
`assets/layers/unit/unit.json`,
])
}
}
/**
* Generates documentation for the layers.
/**
* Generates documentation for the all the individual layers.
* Inline layers are included (if the theme is public)
* @param callback
* @constructor
*/
function GenOverviewsForSingleLayer(
callback: (layer: LayerConfig, element: BaseUIElement, inlineSource: string) => void
): void {
private generateOverviewsForAllSingleLayer(): void {
const allLayers: LayerConfig[] = Array.from(AllSharedLayers.sharedLayers.values()).filter(
(layer) => layer["source"] !== null
)
@ -252,99 +348,7 @@ function GenOverviewsForSingleLayer(
layerIsNeededBy,
DependencyCalculator.getLayerDependencies(layer)
)
callback(layer, element, inlineLayers.get(layer.id))
})
}
/**
* The wikitable is updated as some tools show an overview of apps based on the wiki.
*/
function generateWikipage() {
function generateWikiEntry(layout: {
hideFromOverview: boolean
id: string
shortDescription: any
}) {
if (layout.hideFromOverview) {
return ""
}
const languagesInDescr = Array.from(Object.keys(layout.shortDescription)).filter(
(k) => k !== "_context"
)
const languages = languagesInDescr.map((ln) => `{{#language:${ln}|en}}`).join(", ")
let auth = "Yes"
return `{{service_item
|name= [https://mapcomplete.org/${layout.id} ${layout.id}]
|region= Worldwide
|lang= ${languages}
|descr= A MapComplete theme: ${Translations.T(layout.shortDescription)
.textFor("en")
.replace("<a href='", "[[")
.replace(/'>.*<\/a>/, "]]")}
|material= {{yes|[https://mapcomplete.org/ ${auth}]}}
|image= MapComplete_Screenshot.png
|genre= POI, editor, ${layout.id}
}}`
}
let wikiPage =
'{|class="wikitable sortable"\n' +
"! Name, link !! Genre !! Covered region !! Language !! Description !! Free materials !! Image\n" +
"|-"
for (const layout of themeOverview) {
if (layout.hideFromOverview) {
continue
}
wikiPage += "\n" + generateWikiEntry(layout)
}
wikiPage += "\n|}"
writeFile("Docs/wikiIndex.txt", wikiPage, (err) => {
if (err !== null) {
console.log("Could not save wikiindex", err)
}
})
}
function studioDocsFor(source: string, target: string) {
const lines = readFileSync(source, "utf8").split("\n")
const sections: string[][] = []
let currentSection: string[] = []
for (let line of lines) {
if (line.trim().startsWith("# ")) {
sections.push(currentSection)
currentSection = []
}
line = line.replace('src="../../public/', 'src="./')
line = line.replace('src="../../', 'src="./')
currentSection.push(line)
}
sections.push(currentSection)
writeFileSync(
target,
JSON.stringify({
sections: sections.map((s) => s.join("\n")).filter((s) => s.length > 0),
})
)
}
function studioDocs() {
studioDocsFor("./Docs/Studio/Introduction.md", "./src/assets/studio_introduction.json")
studioDocsFor(
"./Docs/Studio/TagRenderingIntro.md",
"./src/assets/studio_tagrenderings_intro.json"
)
}
console.log("Starting documentation generation...")
ScriptUtils.fixUtils()
studioDocs()
generateWikipage()
GenOverviewsForSingleLayer((layer, element, inlineSource) => {
const inlineSource = inlineLayers.get(layer.id)
ScriptUtils.erasableLog("Exporting layer documentation for", layer.id)
if (!existsSync("./Docs/Layers")) {
mkdirSync("./Docs/Layers")
@ -353,51 +357,21 @@ GenOverviewsForSingleLayer((layer, element, inlineSource) => {
if (inlineSource !== undefined) {
source = `assets/themes/${inlineSource}/${inlineSource}.json`
}
WriteFile("./Docs/Layers/" + layer.id + ".md", element, [source], { noTableOfContents: true })
})
Array.from(AllKnownLayouts.allKnownLayouts.values()).map((theme) => {
if (!existsSync("./Docs/Themes")) {
mkdirSync("./Docs/Themes")
this.WriteFile("./Docs/Layers/" + layer.id + ".md", element, [source], {
noTableOfContents: true,
})
})
}
const docs = GenerateDocumentationForTheme(theme)
WriteFile(
"./Docs/Themes/" + theme.id + ".md",
docs,
[`assets/themes/${theme.id}/${theme.id}.json`],
{ noTableOfContents: true }
)
})
WriteFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage(), [
"src/UI/SpecialVisualizations.ts",
])
WriteFile(
"./Docs/CalculatedTags.md",
new Combine([
new Title("Metatags", 1),
SimpleMetaTaggers.HelpText(),
ExtraFunctions.HelpText(),
]).SetClass("flex-col"),
["src/Logic/SimpleMetaTagger.ts", "src/Logic/ExtraFunctions.ts"]
)
WriteFile("./Docs/SpecialInputElements.md", Validators.HelpText(), [
"src/UI/InputElement/Validators.ts",
])
WriteFile("./Docs/BuiltinLayers.md", GenLayerOverviewText(), [
"src/Customizations/AllKnownLayouts.ts",
])
const qLayer = new LayerConfig(<LayerConfigJson>questions, "questions.json", true)
WriteFile("./Docs/BuiltinQuestions.md", qLayer.GenerateDocumentation([], new Map(), []), [
"assets/layers/questions/questions.json",
])
WriteFile("./Docs/Tags_format.md", TagUtils.generateDocs(), ["src/Logic/Tags/TagUtils.ts"])
/**
* Generate the builtinIndex which shows interlayer dependencies
* @private
*/
{
// Generate the builtinIndex which shows interlayer dependencies
var layers = ScriptUtils.getLayerFiles().map((f) => f.parsed)
var builtinsPerLayer = new Map<string, string[]>()
var layersUsingBuiltin = new Map<string /* Builtin */, string[]>()
private generateBuiltinIndex() {
const layers = ScriptUtils.getLayerFiles().map((f) => f.parsed)
const builtinsPerLayer = new Map<string, string[]>()
const layersUsingBuiltin = new Map<string /* Builtin */, string[]>()
for (const layer of layers) {
if (layer.tagRenderings === undefined) {
continue
@ -436,25 +410,136 @@ WriteFile("./Docs/Tags_format.md", TagUtils.generateDocs(), ["src/Logic/Tags/Tag
new Combine([new Title(builtin), new List(usedByLayers)]).SetClass("flex flex-col")
),
]).SetClass("flex flex-col")
WriteFile("./Docs/BuiltinIndex.md", docs, ["assets/layers/*.json"])
}
this.WriteFile("./Docs/BuiltinIndex.md", docs, ["assets/layers/*.json"])
}
WriteFile("./Docs/URL_Parameters.md", QueryParameterDocumentation.GenerateQueryParameterDocs(), [
"src/Logic/Web/QueryParameters.ts",
"src/UI/QueryParameterDocumentation.ts",
])
if (fakedom === undefined) {
private generateQueryParameterDocs() {
if (fakedom === undefined) {
throw "FakeDom not initialized"
}
QueryParameters.GetQueryParameter(
}
QueryParameters.GetQueryParameter(
"mode",
"map",
"The mode the application starts in, e.g. 'map', 'dashboard' or 'statistics'"
)
)
{
new ThemeViewState(new LayoutConfig(<any>bookcases))
WriteFile("./Docs/Hotkeys.md", Hotkeys.generateDocumentation(), [])
this.WriteFile(
"./Docs/URL_Parameters.md",
QueryParameterDocumentation.GenerateQueryParameterDocs(),
["src/Logic/Web/QueryParameters.ts", "src/UI/QueryParameterDocumentation.ts"]
)
}
private generateBuiltinQuestions() {
const qLayer = new LayerConfig(<LayerConfigJson>questions, "questions.json", true)
this.WriteFile(
"./Docs/BuiltinQuestions.md",
qLayer.GenerateDocumentation([], new Map(), []),
["assets/layers/questions/questions.json"]
)
}
private generateForTheme(theme: LayoutConfig): void {
const el = new Combine([
new Title(
new Combine([
theme.title,
"(",
new Link(theme.id, "https://mapcomplete.org/" + theme.id),
")",
]),
2
),
theme.description,
"This theme contains the following layers:",
new List(
theme.layers
.filter((l) => !l.id.startsWith("note_import_"))
.map((l) => new Link(l.id, "../Layers/" + l.id + ".md"))
),
"Available languages:",
new List(theme.language.filter((ln) => ln !== "_context")),
]).SetClass("flex flex-col")
this.WriteFile(
"./Docs/Themes/" + theme.id + ".md",
el,
[`assets/themes/${theme.id}/${theme.id}.json`],
{ noTableOfContents: true }
)
}
/**
* Generates the documentation for the layers overview page
* @constructor
*/
private generateLayerOverviewText(): BaseUIElement {
for (const id of Constants.priviliged_layers) {
if (!AllSharedLayers.sharedLayers.has(id)) {
console.error("Priviliged layer definition not found: " + id)
return undefined
}
}
const allLayers: LayerConfig[] = Array.from(AllSharedLayers.sharedLayers.values()).filter(
(layer) => layer["source"] === null
)
const builtinLayerIds: Set<string> = new Set<string>()
allLayers.forEach((l) => builtinLayerIds.add(l.id))
const themesPerLayer = new Map<string, string[]>()
for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) {
for (const layer of layout.layers) {
if (!builtinLayerIds.has(layer.id)) {
continue
}
if (!themesPerLayer.has(layer.id)) {
themesPerLayer.set(layer.id, [])
}
themesPerLayer.get(layer.id).push(layout.id)
}
}
// Determine the cross-dependencies
const layerIsNeededBy: Map<string, string[]> = new Map<string, string[]>()
for (const layer of allLayers) {
for (const dep of DependencyCalculator.getLayerDependencies(layer)) {
const dependency = dep.neededLayer
if (!layerIsNeededBy.has(dependency)) {
layerIsNeededBy.set(dependency, [])
}
layerIsNeededBy.get(dependency).push(layer.id)
}
}
const el = new Combine([
new Title("Special and other useful layers", 1),
"MapComplete has a few data layers available in the theme which have special properties through builtin-hooks. Furthermore, there are some normal layers (which are built from normal Theme-config files) but are so general that they get a mention here.",
new Title("Priviliged layers", 1),
new List(Constants.priviliged_layers.map((id) => "[" + id + "](#" + id + ")")),
...Utils.NoNull(
Constants.priviliged_layers.map((id) => AllSharedLayers.sharedLayers.get(id))
).map((l) =>
l.GenerateDocumentation(
themesPerLayer.get(l.id),
layerIsNeededBy,
DependencyCalculator.getLayerDependencies(l),
Constants.added_by_default.indexOf(<any>l.id) >= 0,
Constants.no_include.indexOf(<any>l.id) < 0
)
),
new Title("Normal layers", 1),
"The following layers are included in MapComplete:",
new List(
Array.from(AllSharedLayers.sharedLayers.keys()).map(
(id) => new Link(id, "./Layers/" + id + ".md")
)
),
])
this.WriteFile("./Docs/BuiltinLayers.md", el, ["src/Customizations/AllKnownLayouts.ts"])
}
}
console.log("Generated docs")
new GenerateDocs().run()

View file

@ -137,7 +137,8 @@ export default class DetermineLayout {
if (json.layers === undefined && json.tagRenderings !== undefined) {
// We got fed a layer instead of a theme
const layerConfig = <LayerConfigJson>json
const iconTr: string | TagRenderingConfigJson = <any>(
const iconTr: string | TagRenderingConfigJson =
<any>(
layerConfig.pointRendering
.map((mr) => mr?.marker?.find((icon) => icon.icon !== undefined)?.icon)
.find((i) => i !== undefined)
@ -156,8 +157,8 @@ export default class DetermineLayout {
}
const knownLayersDict = new Map<string, LayerConfigJson>()
for (const key in known_layers.layers) {
const layer = known_layers.layers[key]
for (const key in known_layers["layers"]) {
const layer = known_layers["layers"][key]
knownLayersDict.set(layer.id, <LayerConfigJson>layer)
}
const convertState: DesugaringContext = {

View file

@ -1,4 +1,4 @@
import { Translation } from "../UI/i18n/Translation"
import { Translation, TypedTranslation } from "../UI/i18n/Translation"
import { DenominationConfigJson } from "./ThemeConfig/Json/UnitConfigJson"
import Translations from "../UI/i18n/Translations"
@ -9,20 +9,39 @@ import Translations from "../UI/i18n/Translations"
export class Denomination {
public readonly canonical: string
public readonly _canonicalSingular: string
public readonly useAsDefaultInput: boolean | string[]
public readonly useIfNoUnitGiven: boolean | string[]
public readonly prefix: boolean
public readonly addSpace: boolean
public readonly alternativeDenominations: string[]
private readonly _human: Translation
private readonly _humanSingular?: Translation
public readonly human: TypedTranslation<{ quantity: string }>
public readonly humanSingular?: Translation
constructor(json: DenominationConfigJson, useAsDefaultInput: boolean, context: string) {
private constructor(
canonical: string,
_canonicalSingular: string,
useIfNoUnitGiven: boolean | string[],
prefix: boolean,
addSpace: boolean,
alternativeDenominations: string[],
_human: TypedTranslation<{ quantity: string }>,
_humanSingular?: Translation
) {
this.canonical = canonical
this._canonicalSingular = _canonicalSingular
this.useIfNoUnitGiven = useIfNoUnitGiven
this.prefix = prefix
this.addSpace = addSpace
this.alternativeDenominations = alternativeDenominations
this.human = _human
this.humanSingular = _humanSingular
}
public static fromJson(json: DenominationConfigJson, context: string) {
context = `${context}.unit(${json.canonicalDenomination})`
this.canonical = json.canonicalDenomination.trim()
if (this.canonical === undefined) {
const canonical = json.canonicalDenomination.trim()
if (canonical === undefined) {
throw `${context}: this unit has no decent canonical value defined`
}
this._canonicalSingular = json.canonicalDenominationSingular?.trim()
json.alternativeDenomination?.forEach((v, i) => {
if ((v?.trim() ?? "") === "") {
@ -30,40 +49,67 @@ export class Denomination {
}
})
this.alternativeDenominations = json.alternativeDenomination?.map((v) => v.trim()) ?? []
if (json["default" /* @code-quality: ignore*/] !== undefined) {
throw `${context} uses the old 'default'-key. Use "useIfNoUnitGiven" or "useAsDefaultInput" instead`
}
this.useIfNoUnitGiven = json.useIfNoUnitGiven
this.useAsDefaultInput = useAsDefaultInput ?? json.useIfNoUnitGiven
this._human = Translations.T(json.human, context + "human")
this._humanSingular = Translations.T(json.humanSingular, context + "humanSingular")
this.prefix = json.prefix ?? false
const humanTexts = Translations.T(json.human, context + "human")
humanTexts.OnEveryLanguage((text, language) => {
if (text.indexOf("{quantity}") < 0) {
throw `In denomination: a human text should contain {quantity} (at ${context}.human.${language})`
}
return text
})
return new Denomination(
canonical,
json.canonicalDenominationSingular?.trim(),
json.useIfNoUnitGiven,
json.prefix ?? false,
json.addSpace ?? false,
json.alternativeDenomination?.map((v) => v.trim()) ?? [],
humanTexts,
Translations.T(json.humanSingular, context + "humanSingular")
)
}
get human(): Translation {
return this._human.Clone()
public clone() {
return new Denomination(
this.canonical,
this._canonicalSingular,
this.useIfNoUnitGiven,
this.prefix,
this.addSpace,
this.alternativeDenominations,
this.human,
this.humanSingular
)
}
get humanSingular(): Translation {
return (this._humanSingular ?? this._human).Clone()
public withBlankCanonical() {
return new Denomination(
"",
this._canonicalSingular,
this.useIfNoUnitGiven,
this.prefix,
this.addSpace,
[this.canonical, ...this.alternativeDenominations],
this.human,
this.humanSingular
)
}
/**
* Create a representation of the given value
* Create the canonical, human representation of the given value
* @param value the value from OSM
* @param actAsDefault if set and the value can be parsed as number, will be parsed and trimmed
*
* const unit = new Denomination({
* const unit = Denomination.fromJson({
* canonicalDenomination: "m",
* alternativeDenomination: ["meter"],
* human: {
* en: "meter"
* en: "{quantity} meter"
* }
* }, false, "test")
* }, "test")
* unit.canonicalValue("42m", true) // =>"42 m"
* unit.canonicalValue("42", true) // =>"42 m"
* unit.canonicalValue("42 m", true) // =>"42 m"
@ -72,13 +118,13 @@ export class Denomination {
* unit.canonicalValue("42", true) // =>"42 m"
*
* // Should be trimmed if canonical is empty
* const unit = new Denomination({
* const unit = Denomination.fromJson({
* canonicalDenomination: "",
* alternativeDenomination: ["meter","m"],
* human: {
* en: "meter"
* en: "{quantity} meter"
* }
* }, false, "test")
* }, "test")
* unit.canonicalValue("42m", true) // =>"42"
* unit.canonicalValue("42", true) // =>"42"
* unit.canonicalValue("42 m", true) // =>"42"
@ -160,14 +206,4 @@ export class Denomination {
return null
}
isDefaultDenomination(country: () => string) {
if (this.useIfNoUnitGiven === true) {
return true
}
if (this.useIfNoUnitGiven === false) {
return false
}
return this.useIfNoUnitGiven.indexOf(country()) >= 0
}
}

View file

@ -517,7 +517,10 @@ export interface LayerConfigJson {
*
* group: editing
*/
units?: UnitConfigJson[]
units?: (
| UnitConfigJson
| Record<string, string | { quantity: string; denominations: string[]; canonical?: string }>
)[]
/**
* If set, synchronizes whether or not this layer is enabled.

View file

@ -57,12 +57,16 @@
*
*/
export default interface UnitConfigJson {
/**
* What is quantified? E.g. 'speed', 'length' (including width, diameter, ...), 'electric tension', 'electric current', 'duration'
*/
quantity?: string
/**
* Every key from this list will be normalized.
*
* To render the value properly (with a human readable denomination), use `{canonical(<key>)}`
*/
appliesToKey: string[]
appliesToKey?: string[]
/**
* If set, invalid values will be erased in the MC application (but not in OSM of course!)
* Be careful with setting this
@ -143,4 +147,11 @@ export interface DenominationConfigJson {
* Note that if all values use 'prefix', the dropdown might move to before the text field
*/
prefix?: boolean
/**
* If set, add a space between the quantity and the denomination.
*
* E.g.: `50 mph` instad of `50mph`
*/
addSpace?: boolean
}

View file

@ -105,9 +105,11 @@ export default class LayerConfig extends WithContextLoader {
".units: the 'units'-section should be a list; you probably have an object there"
)
}
this.units = (json.units ?? []).map((unitJson, i) =>
this.units = [].concat(
...(json.units ?? []).map((unitJson, i) =>
Unit.fromJson(unitJson, `${context}.unit[${i}]`)
)
)
if (json.description !== undefined) {
if (Object.keys(json.description).length === 0) {

View file

@ -3,18 +3,23 @@ import { FixedUiElement } from "../UI/Base/FixedUiElement"
import Combine from "../UI/Base/Combine"
import { Denomination } from "./Denomination"
import UnitConfigJson from "./ThemeConfig/Json/UnitConfigJson"
import unit from "../../assets/layers/unit/unit.json"
export class Unit {
private static allUnits = this.initUnits()
public readonly appliesToKeys: Set<string>
public readonly denominations: Denomination[]
public readonly denominationsSorted: Denomination[]
public readonly eraseInvalid: boolean
public readonly quantity: string
constructor(
quantity: string,
appliesToKeys: string[],
applicableDenominations: Denomination[],
eraseInvalid: boolean
) {
this.quantity = quantity
this.appliesToKeys = new Set(appliesToKeys)
this.denominations = applicableDenominations
this.eraseInvalid = eraseInvalid
@ -60,12 +65,24 @@ export class Unit {
}
}
static fromJson(
json:
| UnitConfigJson
| Record<string, string | { quantity: string; denominations: string[] }>,
ctx: string
): Unit[] {
if (!json.appliesToKey && !json.quantity) {
return this.loadFromLibrary(<any>json, ctx)
}
return [this.parse(<UnitConfigJson>json, ctx)]
}
/**
*
* // Should detect invalid defaultInput
* let threwError = false
* try{
* Unit.fromJson({
* Unit.parse({
* appliesToKey: ["length"],
* defaultInput: "xcm",
* applicableUnits: [
@ -82,7 +99,7 @@ export class Unit {
* threwError // => true
*
* // Should work
* Unit.fromJson({
* Unit.parse({
* appliesToKey: ["length"],
* defaultInput: "xcm",
* applicableUnits: [
@ -98,9 +115,9 @@ export class Unit {
* ]
* }, "test")
*/
static fromJson(json: UnitConfigJson, ctx: string) {
private static parse(json: UnitConfigJson, ctx: string): Unit {
const appliesTo = json.appliesToKey
for (let i = 0; i < appliesTo.length; i++) {
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`
@ -112,15 +129,8 @@ export class Unit {
}
// Some keys do have unit handling
const applicable = json.applicableUnits.map(
(u, i) =>
new Denomination(
u,
u.canonicalDenomination === undefined
? undefined
: u.canonicalDenomination.trim() === json.defaultInput,
`${ctx}.units[${i}]`
)
const applicable = json.applicableUnits.map((u, i) =>
Denomination.fromJson(u, `${ctx}.units[${i}]`)
)
if (
@ -133,7 +143,85 @@ export class Unit {
.map((denom) => denom.canonical)
.join(", ")}`
}
return new Unit(appliesTo, applicable, json.eraseInvalidValues ?? false)
return new Unit(
json.quantity ?? "",
appliesTo,
applicable,
json.eraseInvalidValues ?? false
)
}
private static initUnits(): Map<string, Unit> {
const m = new Map<string, Unit>()
const units = (<UnitConfigJson[]>unit.units).map((json, i) =>
this.parse(json, "unit.json.units." + i)
)
for (const unit of units) {
m.set(unit.quantity, unit)
}
return m
}
private static getFromLibrary(name: string, ctx: string): Unit {
const loaded = this.allUnits.get(name)
if (loaded === undefined) {
throw (
"No unit with quantity name " +
name +
" found (at " +
ctx +
"). Try one of: " +
Array.from(this.allUnits.keys()).join(", ")
)
}
return loaded
}
private static loadFromLibrary(
spec: Record<
string,
string | { quantity: string; denominations: string[]; canonical?: string }
>,
ctx: string
): Unit[] {
const units: Unit[] = []
for (const key in spec) {
const toLoad = spec[key]
if (typeof toLoad === "string") {
const loaded = this.getFromLibrary(toLoad, ctx)
units.push(
new Unit(loaded.quantity, [key], loaded.denominations, loaded.eraseInvalid)
)
continue
}
const loaded = this.getFromLibrary(toLoad.quantity, ctx)
const quantity = toLoad.quantity
function fetchDenom(d: string): Denomination {
const found = loaded.denominations.find(
(denom) => denom.canonical.toLowerCase() === d
)
if (!found) {
throw (
`Could not find a denomination \`${d}\`for quantity ${quantity} at ${ctx}. Perhaps you meant to use on of ` +
loaded.denominations.map((d) => d.canonical).join(", ")
)
}
return found
}
const denoms = toLoad.denominations
.map((d) => d.toLowerCase())
.map((d) => fetchDenom(d))
if (toLoad.canonical) {
const canonical = fetchDenom(toLoad.canonical)
denoms.unshift(canonical.withBlankCanonical())
}
units.push(new Unit(loaded.quantity, [key], denoms, loaded.eraseInvalid))
}
return units
}
isApplicableToKey(key: string | undefined): boolean {
@ -161,47 +249,34 @@ export class Unit {
return [undefined, undefined]
}
asHumanLongValue(value: string, country: () => string): BaseUIElement {
asHumanLongValue(value: string, country: () => string): BaseUIElement | string {
if (value === undefined) {
return undefined
}
const [stripped, denom] = this.findDenomination(value, country)
const human = stripped === "1" ? denom?.humanSingular : denom?.human
if (stripped === "1") {
return denom?.humanSingular ?? stripped
}
const human = denom?.human
if (human === undefined) {
return new FixedUiElement(stripped ?? value)
return stripped ?? value
}
const elems = denom.prefix ? [human, stripped] : [stripped, human]
return new Combine(elems)
return human.Subs({ quantity: value })
}
public getDefaultInput(country: () => string | string[]) {
console.log("Searching the default denomination for input", country)
for (const denomination of this.denominations) {
if (denomination.useAsDefaultInput === true) {
return denomination
public toOsm(value: string, denomination: string) {
const denom = this.denominations.find((d) => d.canonical === denomination)
const space = denom.addSpace ? " " : ""
if (denom.prefix) {
return denom.canonical + space + value
}
if (
denomination.useAsDefaultInput === undefined ||
denomination.useAsDefaultInput === false
) {
continue
}
let countries: string | string[] = country()
if (typeof countries === "string") {
countries = countries.split(",")
}
const denominationCountries: string[] = denomination.useAsDefaultInput
if (countries.some((country) => denominationCountries.indexOf(country) >= 0)) {
return denomination
}
}
return this.denominations[0]
return value + space + denom.canonical
}
public getDefaultDenomination(country: () => string) {
for (const denomination of this.denominations) {
if (denomination.useIfNoUnitGiven === true || denomination.canonical === "") {
if (denomination.useIfNoUnitGiven === true) {
return denomination
}
if (
@ -219,6 +294,11 @@ export class Unit {
return denomination
}
}
for (const denomination of this.denominations) {
if (denomination.canonical === "") {
return denomination
}
}
return this.denominations[0]
}
}

View file

@ -1,95 +1,102 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import type { ValidatorType } from "./Validators"
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 { UIEventSource } from "../../Logic/UIEventSource";
import type { ValidatorType } from "./Validators";
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";
export let type: ValidatorType
export let feedback: UIEventSource<Translation> | undefined = undefined
export let cls: string = undefined
export let getCountry: () => string | undefined
export let placeholder: string | Translation | undefined
export let unit: Unit = undefined
export let value: UIEventSource<string>
export let type: ValidatorType;
export let feedback: UIEventSource<Translation> | undefined = undefined;
export let cls: string = undefined;
export let getCountry: () => string | undefined;
export let placeholder: string | Translation | undefined;
export let unit: Unit = undefined;
/**
* Valid state, exported to the calling component
*/
export let value: UIEventSource<string | undefined>;
/**
* Internal state bound to the input element.
*
* This is only copied to 'value' when appropriate so that no invalid values leak outside;
* Additionally, the unit is added when copying
*/
let _value = new UIEventSource(value.data ?? "")
let _value = new UIEventSource(value.data ?? "");
let validator: Validator = Validators.get(type ?? "string")
let validator: Validator = Validators.get(type ?? "string");
if (validator === undefined) {
console.warn("Didn't find a validator for type", type)
console.warn("Didn't find a validator for type", type);
}
let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined)
let _placeholder = placeholder ?? validator?.getPlaceholder() ?? type
let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined);
let _placeholder = placeholder ?? validator?.getPlaceholder() ?? type;
function initValueAndDenom() {
if (unit && value.data) {
const [v, denom] = unit?.findDenomination(value.data, getCountry)
const [v, denom] = unit?.findDenomination(value.data, getCountry);
if (denom) {
_value.setData(v)
selectedUnit.setData(denom.canonical)
_value.setData(v);
selectedUnit.setData(denom.canonical);
} else {
_value.setData(value.data ?? "")
_value.setData(value.data ?? "");
}
} else {
_value.setData(value.data ?? "")
_value.setData(value.data ?? "");
}
}
initValueAndDenom()
initValueAndDenom();
$: {
// The type changed -> reset some values
validator = Validators.get(type ?? "string")
validator = Validators.get(type ?? "string");
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
feedback?.setData(validator?.getFeedback(_value.data, getCountry))
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type;
feedback?.setData(validator?.getFeedback(_value.data, getCountry));
initValueAndDenom()
initValueAndDenom();
}
function setValues() {
// Update the value stores
const v = _value.data
const v = _value.data;
if (!validator?.isValid(v, getCountry) || v === "") {
feedback?.setData(validator?.getFeedback(v, getCountry))
value.setData("")
return
feedback?.setData(validator?.getFeedback(v, getCountry));
value.setData("");
return;
}
if (unit !== undefined && isNaN(Number(v))) {
value.setData(undefined)
return
value.setData(undefined);
return;
}
feedback?.setData(undefined)
feedback?.setData(undefined);
if (selectedUnit.data) {
value.setData(v + selectedUnit.data)
value.setData(unit.toOsm(v, selectedUnit.data))
} else {
value.setData(v)
value.setData(v);
}
}
onDestroy(_value.addCallbackAndRun((_) => setValues()))
onDestroy(_value.addCallbackAndRun((_) => setValues()));
if (unit === undefined) {
onDestroy(
value.addCallbackAndRunD((fromUpstream) => {
if (_value.data !== fromUpstream && fromUpstream !== "") {
_value.setData(fromUpstream)
_value.setData(fromUpstream);
}
})
)
onDestroy(selectedUnit.addCallback((_) => setValues()))
);
}else{
// Handled by the UnitInput
}
onDestroy(selectedUnit.addCallback((_) => setValues()));
if (validator === undefined) {
throw (
"Not a valid type (no validator found) for type '" +
@ -102,17 +109,17 @@
)
.slice(0, 5)
.join(", ")
)
);
}
const isValid = _value.map((v) => validator?.isValid(v, getCountry) ?? true)
const isValid = _value.map((v) => validator?.isValid(v, getCountry) ?? true);
let htmlElem: HTMLInputElement
let htmlElem: HTMLInputElement;
let dispatch = createEventDispatcher<{ selected; submit }>()
let dispatch = createEventDispatcher<{ selected; submit }>();
$: {
if (htmlElem !== undefined) {
htmlElem.onfocus = () => dispatch("selected")
htmlElem.onfocus = () => dispatch("selected");
}
}
@ -121,9 +128,9 @@
*/
function sendSubmit() {
if (feedback?.data) {
console.log("Not sending a submit as there is feedback")
console.log("Not sending a submit as there is feedback");
}
dispatch("submit")
dispatch("submit");
}
</script>
@ -150,7 +157,7 @@
{/if}
{#if unit !== undefined}
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value} />
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value} {getCountry} />
{/if}
</form>
{/if}

View file

@ -1,56 +1,67 @@
<script lang="ts">
import { Unit } from "../../Models/Unit"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Tr from "../Base/Tr.svelte"
import { onDestroy } from "svelte"
import { Unit } from "../../Models/Unit";
import { Store, UIEventSource } from "../../Logic/UIEventSource";
import Tr from "../Base/Tr.svelte";
import { onDestroy, onMount } from "svelte";
import { Denomination } from "../../Models/Denomination";
export let unit: Unit
export let unit: Unit;
/**
* The current value of the input field
* Not necessarily a correct number
* Not necessarily a correct number, should not contain the denomination
*/
export let textValue: UIEventSource<string>
export let textValue: UIEventSource<string>;
/**
* The actual _valid_ value that is upstreamed
* The actual _valid_ value that is upstreamed, including the denomination
*/
export let upstreamValue: Store<string>
export let upstreamValue: Store<string>;
let isSingle: Store<boolean> = textValue.map((v) => Number(v) === 1)
let isSingle: Store<boolean> = textValue.map((v) => Number(v) === 1);
export let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined);
export let getCountry = () => "?";
onMount(() => {
console.log("Setting selected unit based on country", getCountry(), upstreamValue.data)
if(upstreamValue.data === undefined || upstreamValue.data === ""){
// Init the selected unit
let denomination: Denomination = unit.getDefaultDenomination(getCountry);
console.log("Found denom", denomination.canonical)
selectedUnit.setData(denomination.canonical)
}
})
export let selectedUnit: UIEventSource<string> = new UIEventSource<string>(undefined)
export let getCountry = () => "be"
console.log("Unit", unit)
onDestroy(
upstreamValue.addCallbackAndRun((v) => {
if (v === undefined) {
if (!selectedUnit.data) {
selectedUnit.setData(unit.getDefaultDenomination(getCountry).canonical)
}
if(v === undefined || v === ""){
return
}
const selected = unit.findDenomination(v, getCountry)
if (selected === undefined) {
selectedUnit.setData(unit.getDefaultDenomination(getCountry).canonical)
return
let denomination: Denomination = unit.getDefaultDenomination(getCountry);
const selected = unit.findDenomination(v, getCountry);
if(selected){
denomination = selected[1];
}
const [value, denomination] = selected
selectedUnit.setData(denomination.canonical)
return
selectedUnit.setData(denomination.canonical);
})
)
);
onDestroy(
textValue.addCallbackAndRunD((v) => {
// Fallback in case that the user manually types a denomination
const [value, denomination] = unit.findDenomination(v, getCountry)
const [value, denomination] = unit.findDenomination(v, getCountry);
if (value === undefined || denomination === undefined) {
return
return;
}
textValue.setData(value)
selectedUnit.setData(denomination.canonical)
if(value === v){
// The input value actually didn't have a denomination typed out - so lets ignore this one
// If a denomination is given, it is the default value anyway
return;
}
textValue.setData(value);
selectedUnit.setData(denomination.canonical);
})
)
);
</script>
<select bind:value={$selectedUnit}>
@ -59,7 +70,7 @@
{#if $isSingle}
<Tr t={denom.humanSingular} />
{:else}
<Tr t={denom.human} />
<Tr t={denom.human.Subs({quantity: ""})} />
{/if}
</option>
{/each}

View file

@ -83,6 +83,7 @@ import NearbyImages from "./Image/NearbyImages.svelte"
import NearbyImagesCollapsed from "./Image/NearbyImagesCollapsed.svelte"
import { svelte } from "@sveltejs/vite-plugin-svelte"
import MoveWizard from "./Popup/MoveWizard.svelte"
import { Unit } from "../Models/Unit"
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -890,7 +891,7 @@ export default class SpecialVisualizations {
if (value === undefined) {
return undefined
}
const allUnits = [].concat(
const allUnits: Unit[] = [].concat(
...(state?.layout?.layers?.map((lyr) => lyr.units) ?? [])
)
const unit = allUnits.filter((unit) =>
@ -899,7 +900,9 @@ export default class SpecialVisualizations {
if (unit === undefined) {
return value
}
return unit.asHumanLongValue(value)
const getCountry = () => tagSource.data._country
const [v, denom] = unit.findDenomination(value, getCountry)
return unit.asHumanLongValue(v, getCountry)
})
)
},

View file

@ -131,7 +131,7 @@ export default class Translations {
}
static isProbablyATranslation(transl: any) {
if (typeof transl !== "object") {
if (!transl || typeof transl !== "object") {
return false
}
if (Object.keys(transl).length == 0) {

View file

@ -1080,9 +1080,21 @@
"type": "array",
"items": {
"anyOf": [
{
"allOf": [
{
"$ref": "#/definitions/TagRenderingConfigJson"
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}
]
},
{
"type": "string"
}
@ -1103,7 +1115,16 @@
"maxItems": 1
}
],
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)"
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)\nUse `auto:<tagrenderingId>` to automatically create an icon based on a tagRendering which has icons"
},
{
"path": [
"titleIcons"
],
"required": false,
"hints": {},
"type": "object",
"description": "A TagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet.\nFor an _editable_ tagRendering, use 'QuestionableTagRenderingConfigJson' instead, which extends this one"
},
{
"path": [
@ -10380,6 +10401,10 @@
"if": "value=tree_node",
"then": "tree_node - A layer showing trees"
},
{
"if": "value=unit",
"then": "unit - Library layer with all common units"
},
{
"if": "value=usersettings",
"then": "usersettings - A special layer which is not meant to be shown on a map, but which is used to set user settings"
@ -10447,7 +10472,7 @@
"hints": {
"typehint": "tagrendering[]",
"group": "tagrenderings",
"question": "Edit this attribute showing piece/question"
"question": "Edit this way this attributed is displayed or queried"
},
"type": [
{
@ -15969,21 +15994,20 @@
},
{
"path": [
"units"
"units",
"quantity"
],
"required": false,
"hints": {
"default": "ult: true,"
},
"type": "object",
"description": "In some cases, a value is represented in a certain unit (such as meters for heigt/distance/..., km/h for speed, ...)\nSometimes, multiple denominations are possible (e.g. km/h vs mile/h; megawatt vs kilowatt vs gigawatt for power generators, ...)\nThis brings in some troubles, as there are multiple ways to write it (no denomitation, 'm' vs 'meter' 'metre', ...)\nNot only do we want to write consistent data to OSM, we also want to present this consistently to the user.\nThis is handled by defining units.\n# Rendering\nTo render a value with long (human) denomination, use {canonical(key)}\n# Usage\nFirst of all, you define which keys have units applied, for example:\n```\nunits: [\n appliesTo: [\"maxspeed\", \"maxspeed:hgv\", \"maxspeed:bus\"]\n applicableUnits: [\n ...\n ]\n]\n```\nApplicableUnits defines which is the canonical extension, how it is presented to the user, ...:\n```\napplicableUnits: [\n{\n canonicalDenomination: \"km/h\",\n alternativeDenomination: [\"km/u\", \"kmh\", \"kph\"]\n human: {\n en: \"kilometer/hour\",\n nl: \"kilometer/uur\"\n },\n humanShort: {\n en: \"km/h\",\n nl: \"km/u\"\n }\n},\n{\n canoncialDenomination: \"mph\",\n ... similar for miles an hour ...\n}\n]\n```\nIf this is defined, then every key which the denominations apply to (`maxspeed`, `maxspeed:hgv` and `maxspeed:bus`) will be rewritten at the metatagging stage:\nevery value will be parsed and the canonical extension will be added add presented to the other parts of the code.\nAlso, if a freeform text field is used, an extra dropdown with applicable denominations will be given"
"hints": {},
"type": "string",
"description": "What is quantified? E.g. 'speed', 'length' (including width, diameter, ...), 'electric tension', 'electric current', 'duration'"
},
{
"path": [
"units",
"appliesToKey"
],
"required": true,
"required": false,
"hints": {},
"type": "array",
"description": "Every key from this list will be normalized.\nTo render the value properly (with a human readable denomination), use `{canonical(<key>)}`"
@ -16109,6 +16133,17 @@
"type": "boolean",
"description": "If set, then the canonical value will be prefixed instead, e.g. for '€'\nNote that if all values use 'prefix', the dropdown might move to before the text field"
},
{
"path": [
"units",
"applicableUnits",
"addSpace"
],
"required": false,
"hints": {},
"type": "boolean",
"description": "If set, add a space between the quantity and the denomination.\nE.g.: `50 mph` instad of `50mph`"
},
{
"path": [
"units",

View file

@ -832,6 +832,10 @@
"if": "value=tree_node",
"then": "tree_node - A layer showing trees"
},
{
"if": "value=unit",
"then": "unit - Library layer with all common units"
},
{
"if": "value=usersettings",
"then": "usersettings - A special layer which is not meant to be shown on a map, but which is used to set user settings"
@ -1183,15 +1187,27 @@
"type": "boolean"
},
"titleIcons": {
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)\n\nType: icon[]\ngroup: infobox",
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)\n\nUse `auto:<tagrenderingId>` to automatically create an icon based on a tagRendering which has icons\n\nType: icon[]\ngroup: infobox",
"anyOf": [
{
"type": "array",
"items": {
"anyOf": [
{
"allOf": [
{
"$ref": "#/definitions/TagRenderingConfigJson"
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}
]
},
{
"type": "string"
}
@ -1300,7 +1316,7 @@
}
},
"tagRenderings": {
"description": "question: Edit this attribute showing piece/question\n\nA tag rendering is a block that either shows the known value or asks a question.\n\nRefer to the class `TagRenderingConfigJson` to see the possibilities.\n\nNote that we can also use a string here - where the string refers to a tag rendering defined in `assets/questions/questions.json`,\nwhere a few very general questions are defined e.g. website, phone number, ...\nFurthermore, _all_ the questions of another layer can be reused with `otherlayer.*`\nIf you need only a single of the tagRenderings, use `otherlayer.tagrenderingId`\nIf one or more questions have a 'group' or 'label' set, select all the entries with the corresponding group or label with `otherlayer.*group`\nRemark: if a tagRendering is 'lent' from another layer, the 'source'-tags are copied and added as condition.\nIf they are not wanted, remove them with an override\n\nA special value is 'questions', which indicates the location of the questions box. If not specified, it'll be appended to the bottom of the featureInfobox.\n\nAt last, one can define a group of renderings where parts of all strings will be replaced by multiple other strings.\nThis is mainly create questions for a 'left' and a 'right' side of the road.\nThese will be grouped and questions will be asked together\n\ntype: tagrendering[]\ngroup: tagrenderings",
"description": "question: Edit this way this attributed is displayed or queried\n\nA tag rendering is a block that either shows the known value or asks a question.\n\nRefer to the class `TagRenderingConfigJson` to see the possibilities.\n\nNote that we can also use a string here - where the string refers to a tag rendering defined in `assets/questions/questions.json`,\nwhere a few very general questions are defined e.g. website, phone number, ...\nFurthermore, _all_ the questions of another layer can be reused with `otherlayer.*`\nIf you need only a single of the tagRenderings, use `otherlayer.tagrenderingId`\nIf one or more questions have a 'group' or 'label' set, select all the entries with the corresponding group or label with `otherlayer.*group`\nRemark: if a tagRendering is 'lent' from another layer, the 'source'-tags are copied and added as condition.\nIf they are not wanted, remove them with an override\n\nA special value is 'questions', which indicates the location of the questions box. If not specified, it'll be appended to the bottom of the featureInfobox.\n\nAt last, one can define a group of renderings where parts of all strings will be replaced by multiple other strings.\nThis is mainly create questions for a 'left' and a 'right' side of the road.\nThese will be grouped and questions will be asked together\n\ntype: tagrendering[]\ngroup: tagrenderings",
"type": "array",
"items": {
"anyOf": [
@ -1706,7 +1722,14 @@
"units": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/default_2"
},
{
"$ref": "#/definitions/Record<string,string|{quantity:string;denominations:string[];}>"
}
]
}
},
"syncSelection": {
@ -2888,9 +2911,21 @@
"type": "array",
"items": {
"anyOf": [
{
"allOf": [
{
"$ref": "#/definitions/TagRenderingConfigJson"
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}
]
},
{
"type": "string"
}
@ -2911,7 +2946,17 @@
"maxItems": 1
}
],
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)"
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)\nUse `auto:<tagrenderingId>` to automatically create an icon based on a tagRendering which has icons"
},
{
"path": [
"layers",
"titleIcons"
],
"required": false,
"hints": {},
"type": "object",
"description": "A TagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet.\nFor an _editable_ tagRendering, use 'QuestionableTagRenderingConfigJson' instead, which extends this one"
},
{
"path": [
@ -12476,6 +12521,10 @@
"if": "value=tree_node",
"then": "tree_node - A layer showing trees"
},
{
"if": "value=unit",
"then": "unit - Library layer with all common units"
},
{
"if": "value=usersettings",
"then": "usersettings - A special layer which is not meant to be shown on a map, but which is used to set user settings"
@ -12545,7 +12594,7 @@
"hints": {
"typehint": "tagrendering[]",
"group": "tagrenderings",
"question": "Edit this attribute showing piece/question"
"question": "Edit this way this attributed is displayed or queried"
},
"type": [
{
@ -18273,14 +18322,13 @@
{
"path": [
"layers",
"units"
"units",
"quantity"
],
"required": false,
"hints": {
"default": "ult: true,"
},
"type": "object",
"description": "In some cases, a value is represented in a certain unit (such as meters for heigt/distance/..., km/h for speed, ...)\nSometimes, multiple denominations are possible (e.g. km/h vs mile/h; megawatt vs kilowatt vs gigawatt for power generators, ...)\nThis brings in some troubles, as there are multiple ways to write it (no denomitation, 'm' vs 'meter' 'metre', ...)\nNot only do we want to write consistent data to OSM, we also want to present this consistently to the user.\nThis is handled by defining units.\n# Rendering\nTo render a value with long (human) denomination, use {canonical(key)}\n# Usage\nFirst of all, you define which keys have units applied, for example:\n```\nunits: [\n appliesTo: [\"maxspeed\", \"maxspeed:hgv\", \"maxspeed:bus\"]\n applicableUnits: [\n ...\n ]\n]\n```\nApplicableUnits defines which is the canonical extension, how it is presented to the user, ...:\n```\napplicableUnits: [\n{\n canonicalDenomination: \"km/h\",\n alternativeDenomination: [\"km/u\", \"kmh\", \"kph\"]\n human: {\n en: \"kilometer/hour\",\n nl: \"kilometer/uur\"\n },\n humanShort: {\n en: \"km/h\",\n nl: \"km/u\"\n }\n},\n{\n canoncialDenomination: \"mph\",\n ... similar for miles an hour ...\n}\n]\n```\nIf this is defined, then every key which the denominations apply to (`maxspeed`, `maxspeed:hgv` and `maxspeed:bus`) will be rewritten at the metatagging stage:\nevery value will be parsed and the canonical extension will be added add presented to the other parts of the code.\nAlso, if a freeform text field is used, an extra dropdown with applicable denominations will be given"
"hints": {},
"type": "string",
"description": "What is quantified? E.g. 'speed', 'length' (including width, diameter, ...), 'electric tension', 'electric current', 'duration'"
},
{
"path": [
@ -18288,7 +18336,7 @@
"units",
"appliesToKey"
],
"required": true,
"required": false,
"hints": {},
"type": "array",
"description": "Every key from this list will be normalized.\nTo render the value properly (with a human readable denomination), use `{canonical(<key>)}`"
@ -18423,6 +18471,18 @@
"type": "boolean",
"description": "If set, then the canonical value will be prefixed instead, e.g. for '€'\nNote that if all values use 'prefix', the dropdown might move to before the text field"
},
{
"path": [
"layers",
"units",
"applicableUnits",
"addSpace"
],
"required": false,
"hints": {},
"type": "boolean",
"description": "If set, add a space between the quantity and the denomination.\nE.g.: `50 mph` instad of `50mph`"
},
{
"path": [
"layers",
@ -19645,9 +19705,21 @@
"type": "array",
"items": {
"anyOf": [
{
"allOf": [
{
"$ref": "#/definitions/TagRenderingConfigJson"
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}
]
},
{
"type": "string"
}
@ -19668,7 +19740,18 @@
"maxItems": 1
}
],
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)"
"description": "Small icons shown next to the title.\nIf not specified, the OsmLink and wikipedia links will be used by default.\nUse an empty array to hide them.\nNote that \"defaults\" will insert all the default titleIcons (which are added automatically)\nUse `auto:<tagrenderingId>` to automatically create an icon based on a tagRendering which has icons"
},
{
"path": [
"layers",
"override",
"titleIcons"
],
"required": false,
"hints": {},
"type": "object",
"description": "A TagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet.\nFor an _editable_ tagRendering, use 'QuestionableTagRenderingConfigJson' instead, which extends this one"
},
{
"path": [
@ -29521,6 +29604,10 @@
"if": "value=tree_node",
"then": "tree_node - A layer showing trees"
},
{
"if": "value=unit",
"then": "unit - Library layer with all common units"
},
{
"if": "value=usersettings",
"then": "usersettings - A special layer which is not meant to be shown on a map, but which is used to set user settings"
@ -29592,7 +29679,7 @@
"hints": {
"typehint": "tagrendering[]",
"group": "tagrenderings",
"question": "Edit this attribute showing piece/question"
"question": "Edit this way this attributed is displayed or queried"
},
"type": [
{
@ -35526,14 +35613,13 @@
"path": [
"layers",
"override",
"units"
"units",
"quantity"
],
"required": false,
"hints": {
"default": "ult: true,"
},
"type": "object",
"description": "In some cases, a value is represented in a certain unit (such as meters for heigt/distance/..., km/h for speed, ...)\nSometimes, multiple denominations are possible (e.g. km/h vs mile/h; megawatt vs kilowatt vs gigawatt for power generators, ...)\nThis brings in some troubles, as there are multiple ways to write it (no denomitation, 'm' vs 'meter' 'metre', ...)\nNot only do we want to write consistent data to OSM, we also want to present this consistently to the user.\nThis is handled by defining units.\n# Rendering\nTo render a value with long (human) denomination, use {canonical(key)}\n# Usage\nFirst of all, you define which keys have units applied, for example:\n```\nunits: [\n appliesTo: [\"maxspeed\", \"maxspeed:hgv\", \"maxspeed:bus\"]\n applicableUnits: [\n ...\n ]\n]\n```\nApplicableUnits defines which is the canonical extension, how it is presented to the user, ...:\n```\napplicableUnits: [\n{\n canonicalDenomination: \"km/h\",\n alternativeDenomination: [\"km/u\", \"kmh\", \"kph\"]\n human: {\n en: \"kilometer/hour\",\n nl: \"kilometer/uur\"\n },\n humanShort: {\n en: \"km/h\",\n nl: \"km/u\"\n }\n},\n{\n canoncialDenomination: \"mph\",\n ... similar for miles an hour ...\n}\n]\n```\nIf this is defined, then every key which the denominations apply to (`maxspeed`, `maxspeed:hgv` and `maxspeed:bus`) will be rewritten at the metatagging stage:\nevery value will be parsed and the canonical extension will be added add presented to the other parts of the code.\nAlso, if a freeform text field is used, an extra dropdown with applicable denominations will be given"
"hints": {},
"type": "string",
"description": "What is quantified? E.g. 'speed', 'length' (including width, diameter, ...), 'electric tension', 'electric current', 'duration'"
},
{
"path": [
@ -35542,7 +35628,7 @@
"units",
"appliesToKey"
],
"required": true,
"required": false,
"hints": {},
"type": "array",
"description": "Every key from this list will be normalized.\nTo render the value properly (with a human readable denomination), use `{canonical(<key>)}`"
@ -35686,6 +35772,19 @@
"type": "boolean",
"description": "If set, then the canonical value will be prefixed instead, e.g. for '€'\nNote that if all values use 'prefix', the dropdown might move to before the text field"
},
{
"path": [
"layers",
"override",
"units",
"applicableUnits",
"addSpace"
],
"required": false,
"hints": {},
"type": "boolean",
"description": "If set, add a space between the quantity and the denomination.\nE.g.: `50 mph` instad of `50mph`"
},
{
"path": [
"layers",

View file

@ -4,22 +4,21 @@ import { describe, expect, it } from "vitest"
describe("Unit", () => {
it("should convert a value back and forth", () => {
const denomintion = new Denomination(
const denomintion = Denomination.fromJson(
{
canonicalDenomination: "MW",
alternativeDenomination: ["megawatts", "megawatt"],
human: {
en: " megawatts",
nl: " megawatt",
en: "{quantity} megawatts",
nl: "{quantity} megawatt",
},
},
false,
"test"
)
const canonical = denomintion.canonicalValue("5", true)
expect(canonical).toBe("5 MW")
const units = new Unit(["key"], [denomintion], false)
const units = new Unit("quantity", ["key"], [denomintion], false)
const [detected, detectedDenom] = units.findDenomination("5 MW", () => "be")
expect(detected).toBe("5")
expect(detectedDenom).toBe(denomintion)