diff --git a/assets/layers/elevator/elevator.json b/assets/layers/elevator/elevator.json
index 0d59e125db..a6ad920151 100644
--- a/assets/layers/elevator/elevator.json
+++ b/assets/layers/elevator/elevator.json
@@ -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"
+ ]
+ }
}
]
}
diff --git a/assets/layers/entrance/entrance.json b/assets/layers/entrance/entrance.json
index 1d17bdea59..91bf1d8bc0 100644
--- a/assets/layers/entrance/entrance.json
+++ b/assets/layers/entrance/entrance.json
@@ -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"
+ ]
+ }
}
]
}
diff --git a/assets/layers/hydrant/hydrant.json b/assets/layers/hydrant/hydrant.json
index 9049ee9e94..a3ee15762f 100644
--- a/assets/layers/hydrant/hydrant.json
+++ b/assets/layers/hydrant/hydrant.json
@@ -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"
+ ]
+ }
}
]
}
diff --git a/assets/layers/kerbs/kerbs.json b/assets/layers/kerbs/kerbs.json
index a2200d543b..59bae3a703 100644
--- a/assets/layers/kerbs/kerbs.json
+++ b/assets/layers/kerbs/kerbs.json
@@ -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"
+ ]
+ }
}
]
}
diff --git a/assets/layers/maxspeed/maxspeed.json b/assets/layers/maxspeed/maxspeed.json
index 6ce88e50bd..cc5e325f20 100644
--- a/assets/layers/maxspeed/maxspeed.json
+++ b/assets/layers/maxspeed/maxspeed.json
@@ -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"
+ ]
+ }
}
]
}
diff --git a/assets/layers/observation_tower/observation_tower.json b/assets/layers/observation_tower/observation_tower.json
index 0a71f60e87..c3a0f3ce92 100644
--- a/assets/layers/observation_tower/observation_tower.json
+++ b/assets/layers/observation_tower/observation_tower.json
@@ -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"
- }
- }
- ],
- "eraseInvalidValues": true
+ "height": {
+ "quantity": "distance",
+ "denominations": [
+ "m"
+ ]
+ }
}
]
}
diff --git a/assets/layers/reception_desk/reception_desk.json b/assets/layers/reception_desk/reception_desk.json
index b8e017a872..70c89f2304 100644
--- a/assets/layers/reception_desk/reception_desk.json
+++ b/assets/layers/reception_desk/reception_desk.json
@@ -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"
+ ]
+ }
}
]
}
diff --git a/assets/layers/speed_camera/speed_camera.json b/assets/layers/speed_camera/speed_camera.json
index 046cc7d16b..af401b7ad4 100644
--- a/assets/layers/speed_camera/speed_camera.json
+++ b/assets/layers/speed_camera/speed_camera.json
@@ -116,64 +116,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",
+ "denominations": [
+ "kmh",
+ "mph"
+ ]
+ }
}
]
}
diff --git a/assets/layers/speed_display/speed_display.json b/assets/layers/speed_display/speed_display.json
index a29fed5381..0688c33001 100644
--- a/assets/layers/speed_display/speed_display.json
+++ b/assets/layers/speed_display/speed_display.json
@@ -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"
+ ]
+ }
}
]
}
diff --git a/assets/layers/sport_pitch/sport_pitch.json b/assets/layers/sport_pitch/sport_pitch.json
index 2a7d07649a..ffc6a6babf 100644
--- a/assets/layers/sport_pitch/sport_pitch.json
+++ b/assets/layers/sport_pitch/sport_pitch.json
@@ -460,7 +460,8 @@
"if": "surface=fine_gravel",
"then": {
"en": "The surface is fine gravel",
- "nl": "De ondergrond bestaat uit grind"
+ "nl": "De ondergrond bestaat uit grind",
+ "de": "Die Oberfläche ist feiner Kies"
}
}
],
diff --git a/assets/layers/toilet/toilet.json b/assets/layers/toilet/toilet.json
index 91dc7dc9d4..4074e76152 100644
--- a/assets/layers/toilet/toilet.json
+++ b/assets/layers/toilet/toilet.json
@@ -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"
+ ]
+ }
}
]
}
diff --git a/assets/layers/toilet_at_amenity/toilet_at_amenity.json b/assets/layers/toilet_at_amenity/toilet_at_amenity.json
index 011c928817..d21309827a 100644
--- a/assets/layers/toilet_at_amenity/toilet_at_amenity.json
+++ b/assets/layers/toilet_at_amenity/toilet_at_amenity.json
@@ -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"
+ ]
+ }
}
]
}
diff --git a/assets/layers/unit/unit.json b/assets/layers/unit/unit.json
new file mode 100644
index 0000000000..72f33d4c34
--- /dev/null
+++ b/assets/layers/unit/unit.json
@@ -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
+}
diff --git a/assets/layers/walls_and_buildings/walls_and_buildings.json b/assets/layers/walls_and_buildings/walls_and_buildings.json
index 77251153fc..f8368a4646 100644
--- a/assets/layers/walls_and_buildings/walls_and_buildings.json
+++ b/assets/layers/walls_and_buildings/walls_and_buildings.json
@@ -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"
+ ]
+ }
}
]
}
diff --git a/assets/layers/windturbine/windturbine.json b/assets/layers/windturbine/windturbine.json
index 82e2787cc8..557956f1f0 100644
--- a/assets/layers/windturbine/windturbine.json
+++ b/assets/layers/windturbine/windturbine.json
@@ -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"
+ ]
+ }
}
]
}
diff --git a/assets/themes/climbing/climbing.json b/assets/themes/climbing/climbing.json
index 82a23b6be1..f359b932dd 100644
--- a/assets/themes/climbing/climbing.json
+++ b/assets/themes/climbing/climbing.json
@@ -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+": [
diff --git a/scripts/generateDocs.ts b/scripts/generateDocs.ts
index d95a9a1a8c..a31ffe55a3 100644
--- a/scripts/generateDocs.ts
+++ b/scripts/generateDocs.ts
@@ -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,242 +29,80 @@ 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(
- filename,
- html: string | BaseUIElement,
- autogenSource: string[],
- options?: {
- noTableOfContents: boolean
- }
-): void {
- if (!html) {
- return
- }
- for (const source of autogenSource) {
- if (source.indexOf("*") > 0) {
- continue
- }
- if (!existsSync(source)) {
- throw (
- "While creating a documentation file and checking that the generation sources are properly linked: source file " +
- source +
- " was not found. Typo?"
- )
- }
- }
-
- if (html instanceof Combine && !options?.noTableOfContents) {
- const toc = new TableOfContents(html)
- const els = html.getElements()
- html = new Combine([els.shift(), toc, ...els]).SetClass("flex flex-col")
- }
-
- let md = new Combine([
- Translations.W(html),
- "\n\nThis document is autogenerated from " +
- autogenSource
- .map(
- (file) =>
- `[${file}](https://github.com/pietervdvn/MapComplete/blob/develop/${file})`
- )
- .join(", "),
- ]).AsMarkdown()
-
- md.replace(/\n\n\n+/g, "\n\n")
-
- if (!md.endsWith("\n")) {
- md += "\n"
- }
-
- const warnAutomated =
- "[//]: # (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")
-}
+import Script from "./Script"
/**
- * Generates the documentation for the layers overview page
- * @constructor
+ * Converts a markdown-file into a .json file, which a walkthrough/slideshow element can use
+ *
+ * These are used in the studio
*/
-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
- }
+class ToSlideshowJson {
+ private readonly _source: string
+ private readonly _target: string
+
+ constructor(source: string, target: string) {
+ this._source = source
+ this._target = target
}
- const allLayers: LayerConfig[] = Array.from(AllSharedLayers.sharedLayers.values()).filter(
- (layer) => layer["source"] === null
- )
+ public convert() {
+ const lines = readFileSync(this._source, "utf8").split("\n")
- const builtinLayerIds: Set = new Set()
- allLayers.forEach((l) => builtinLayerIds.add(l.id))
-
- const themesPerLayer = new Map()
-
- for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) {
- for (const layer of layout.layers) {
- if (!builtinLayerIds.has(layer.id)) {
- continue
+ const sections: string[][] = []
+ let currentSection: string[] = []
+ for (let line of lines) {
+ if (line.trim().startsWith("# ")) {
+ sections.push(currentSection)
+ currentSection = []
}
- if (!themesPerLayer.has(layer.id)) {
- themesPerLayer.set(layer.id, [])
- }
- themesPerLayer.get(layer.id).push(layout.id)
+ line = line.replace('src="../../public/', 'src="./')
+ line = line.replace('src="../../', 'src="./')
+ currentSection.push(line)
}
- }
-
- // Determine the cross-dependencies
- const layerIsNeededBy: Map = new Map()
-
- 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(l.id) >= 0,
- Constants.no_include.indexOf(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")
- )
- ),
- ])
-}
-
-/**
- * Generates documentation for the layers.
- * Inline layers are included (if the theme is public)
- * @param callback
- * @constructor
- */
-function GenOverviewsForSingleLayer(
- callback: (layer: LayerConfig, element: BaseUIElement, inlineSource: string) => void
-): void {
- const allLayers: LayerConfig[] = Array.from(AllSharedLayers.sharedLayers.values()).filter(
- (layer) => layer["source"] !== null
- )
- const builtinLayerIds: Set = new Set()
- allLayers.forEach((l) => builtinLayerIds.add(l.id))
- const inlineLayers = new Map()
-
- for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) {
- if (layout.hideFromOverview) {
- continue
- }
-
- for (const layer of layout.layers) {
- if (layer.source === null) {
- continue
- }
- if (builtinLayerIds.has(layer.id)) {
- continue
- }
- if (layer.source.geojsonSource !== undefined) {
- // Not an OSM-source
- continue
- }
- allLayers.push(layer)
- builtinLayerIds.add(layer.id)
- inlineLayers.set(layer.id, layout.id)
- }
- }
-
- const themesPerLayer = new Map()
-
- for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) {
- if (layout.hideFromOverview) {
- continue
- }
- for (const layer of layout.layers) {
- if (!builtinLayerIds.has(layer.id)) {
- // This is an inline layer
- continue
- }
- if (!themesPerLayer.has(layer.id)) {
- themesPerLayer.set(layer.id, [])
- }
- themesPerLayer.get(layer.id).push(layout.id)
- }
- }
-
- // Determine the cross-dependencies
- const layerIsNeededBy: Map = new Map()
-
- 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)
- }
- }
-
- allLayers.forEach((layer) => {
- const element = layer.GenerateDocumentation(
- themesPerLayer.get(layer.id),
- layerIsNeededBy,
- DependencyCalculator.getLayerDependencies(layer)
+ sections.push(currentSection)
+ writeFileSync(
+ this._target,
+ JSON.stringify({
+ sections: sections.map((s) => s.join("\n")).filter((s) => s.length > 0),
+ })
)
- callback(layer, element, inlineLayers.get(layer.id))
- })
+ }
}
/**
- * The wikitable is updated as some tools show an overview of apps based on the wiki.
+ * 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.
*/
-function generateWikipage() {
- function generateWikiEntry(layout: {
+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 ""
}
@@ -287,174 +125,421 @@ function generateWikipage() {
|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)
+export class GenerateDocs extends Script {
+ constructor() {
+ super("Generates various documentation files")
}
- sections.push(currentSection)
- writeFileSync(
- target,
- JSON.stringify({
- sections: sections.map((s) => s.join("\n")).filter((s) => s.length > 0),
+
+ 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)
})
- )
-}
-function studioDocs() {
- studioDocsFor("./Docs/Studio/Introduction.md", "./src/assets/studio_introduction.json")
- studioDocsFor(
- "./Docs/Studio/TagRenderingIntro.md",
- "./src/assets/studio_tagrenderings_intro.json"
- )
-}
+ 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",
+ ])
-console.log("Starting documentation generation...")
-ScriptUtils.fixUtils()
-studioDocs()
-generateWikipage()
-GenOverviewsForSingleLayer((layer, element, inlineSource) => {
- ScriptUtils.erasableLog("Exporting layer documentation for", layer.id)
- if (!existsSync("./Docs/Layers")) {
- mkdirSync("./Docs/Layers")
+ new WikiPageGenerator().generate()
+
+ console.log("Generated docs")
}
- let source: string = `assets/layers/${layer.id}/${layer.id}.json`
- 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")
- }
- 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(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
- var layers = ScriptUtils.getLayerFiles().map((f) => f.parsed)
- var builtinsPerLayer = new Map()
- var layersUsingBuiltin = new Map()
- for (const layer of layers) {
- if (layer.tagRenderings === undefined) {
- continue
+ private WriteFile(
+ filename,
+ html: string | BaseUIElement,
+ autogenSource: string[],
+ options?: {
+ noTableOfContents: boolean
}
- const usedBuiltins: string[] = []
- for (const tagRendering of layer.tagRenderings) {
- if (typeof tagRendering === "string") {
- usedBuiltins.push(tagRendering)
+ ): void {
+ if (!html) {
+ return
+ }
+ for (const source of autogenSource) {
+ if (source.indexOf("*") > 0) {
continue
}
- if (tagRendering["builtin"] !== undefined) {
- const builtins = tagRendering["builtin"]
- if (typeof builtins === "string") {
- usedBuiltins.push(builtins)
- } else {
- usedBuiltins.push(...builtins)
+ if (!existsSync(source)) {
+ throw (
+ "While creating a documentation file and checking that the generation sources are properly linked: source file " +
+ source +
+ " was not found. Typo?"
+ )
+ }
+ }
+
+ if (html instanceof Combine && !options?.noTableOfContents) {
+ const toc = new TableOfContents(html)
+ const els = html.getElements()
+ html = new Combine([els.shift(), toc, ...els]).SetClass("flex flex-col")
+ }
+
+ let md = new Combine([
+ Translations.W(html),
+ "\n\nThis document is autogenerated from " +
+ autogenSource
+ .map(
+ (file) =>
+ `[${file}](https://github.com/pietervdvn/MapComplete/blob/develop/${file})`
+ )
+ .join(", "),
+ ]).AsMarkdown()
+
+ md.replace(/\n\n\n+/g, "\n\n")
+
+ if (!md.endsWith("\n")) {
+ md += "\n"
+ }
+
+ const warnAutomated =
+ "[//]: # (WARNING: this file is automatically generated. Please find the sources at the bottom and edit those sources)"
+
+ writeFileSync(filename, warnAutomated + md)
+ }
+
+ private generateHotkeyDocs() {
+ new ThemeViewState(new LayoutConfig(bookcases))
+ this.WriteFile("./Docs/Hotkeys.md", Hotkeys.generateDocumentation(), [])
+ }
+
+ private generateBuiltinUnits() {
+ const layer = new LayerConfig(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)
+ )
}
}
}
- for (const usedBuiltin of usedBuiltins) {
- const usingLayers = layersUsingBuiltin.get(usedBuiltin)
- if (usingLayers === undefined) {
- layersUsingBuiltin.set(usedBuiltin, [layer.id])
- } else {
- usingLayers.push(layer.id)
+
+ this.WriteFile("./Docs/builtin_units.md", new Combine([new Title("Units", 1), ...els]), [
+ `assets/layers/unit/unit.json`,
+ ])
+ }
+
+ /**
+ * Generates documentation for the all the individual layers.
+ * Inline layers are included (if the theme is public)
+ */
+ private generateOverviewsForAllSingleLayer(): void {
+ const allLayers: LayerConfig[] = Array.from(AllSharedLayers.sharedLayers.values()).filter(
+ (layer) => layer["source"] !== null
+ )
+ const builtinLayerIds: Set = new Set()
+ allLayers.forEach((l) => builtinLayerIds.add(l.id))
+ const inlineLayers = new Map()
+
+ for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) {
+ if (layout.hideFromOverview) {
+ continue
+ }
+
+ for (const layer of layout.layers) {
+ if (layer.source === null) {
+ continue
+ }
+ if (builtinLayerIds.has(layer.id)) {
+ continue
+ }
+ if (layer.source.geojsonSource !== undefined) {
+ // Not an OSM-source
+ continue
+ }
+ allLayers.push(layer)
+ builtinLayerIds.add(layer.id)
+ inlineLayers.set(layer.id, layout.id)
}
}
- builtinsPerLayer.set(layer.id, usedBuiltins)
+ const themesPerLayer = new Map()
+
+ for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) {
+ if (layout.hideFromOverview) {
+ continue
+ }
+ for (const layer of layout.layers) {
+ if (!builtinLayerIds.has(layer.id)) {
+ // This is an inline layer
+ continue
+ }
+ if (!themesPerLayer.has(layer.id)) {
+ themesPerLayer.set(layer.id, [])
+ }
+ themesPerLayer.get(layer.id).push(layout.id)
+ }
+ }
+
+ // Determine the cross-dependencies
+ const layerIsNeededBy: Map = new Map()
+
+ 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)
+ }
+ }
+
+ allLayers.forEach((layer) => {
+ const element = layer.GenerateDocumentation(
+ themesPerLayer.get(layer.id),
+ layerIsNeededBy,
+ DependencyCalculator.getLayerDependencies(layer)
+ )
+ const inlineSource = inlineLayers.get(layer.id)
+ ScriptUtils.erasableLog("Exporting layer documentation for", layer.id)
+ if (!existsSync("./Docs/Layers")) {
+ mkdirSync("./Docs/Layers")
+ }
+ let source: string = `assets/layers/${layer.id}/${layer.id}.json`
+ if (inlineSource !== undefined) {
+ source = `assets/themes/${inlineSource}/${inlineSource}.json`
+ }
+ this.WriteFile("./Docs/Layers/" + layer.id + ".md", element, [source], {
+ noTableOfContents: true,
+ })
+ })
}
- const docs = new Combine([
- new Title("Index of builtin TagRendering", 1),
- new Title("Existing builtin tagrenderings", 2),
- ...Array.from(layersUsingBuiltin.entries()).map(([builtin, usedByLayers]) =>
- new Combine([new Title(builtin), new List(usedByLayers)]).SetClass("flex flex-col")
- ),
- ]).SetClass("flex flex-col")
- WriteFile("./Docs/BuiltinIndex.md", docs, ["assets/layers/*.json"])
+ /**
+ * Generate the builtinIndex which shows interlayer dependencies
+ * @private
+ */
+
+ private generateBuiltinIndex() {
+ const layers = ScriptUtils.getLayerFiles().map((f) => f.parsed)
+ const builtinsPerLayer = new Map()
+ const layersUsingBuiltin = new Map()
+ for (const layer of layers) {
+ if (layer.tagRenderings === undefined) {
+ continue
+ }
+ const usedBuiltins: string[] = []
+ for (const tagRendering of layer.tagRenderings) {
+ if (typeof tagRendering === "string") {
+ usedBuiltins.push(tagRendering)
+ continue
+ }
+ if (tagRendering["builtin"] !== undefined) {
+ const builtins = tagRendering["builtin"]
+ if (typeof builtins === "string") {
+ usedBuiltins.push(builtins)
+ } else {
+ usedBuiltins.push(...builtins)
+ }
+ }
+ }
+ for (const usedBuiltin of usedBuiltins) {
+ const usingLayers = layersUsingBuiltin.get(usedBuiltin)
+ if (usingLayers === undefined) {
+ layersUsingBuiltin.set(usedBuiltin, [layer.id])
+ } else {
+ usingLayers.push(layer.id)
+ }
+ }
+
+ builtinsPerLayer.set(layer.id, usedBuiltins)
+ }
+
+ const docs = new Combine([
+ new Title("Index of builtin TagRendering", 1),
+ new Title("Existing builtin tagrenderings", 2),
+ ...Array.from(layersUsingBuiltin.entries()).map(([builtin, usedByLayers]) =>
+ new Combine([new Title(builtin), new List(usedByLayers)]).SetClass("flex flex-col")
+ ),
+ ]).SetClass("flex flex-col")
+ this.WriteFile("./Docs/BuiltinIndex.md", docs, ["assets/layers/*.json"])
+ }
+
+ private generateQueryParameterDocs() {
+ if (fakedom === undefined) {
+ throw "FakeDom not initialized"
+ }
+ QueryParameters.GetQueryParameter(
+ "mode",
+ "map",
+ "The mode the application starts in, e.g. 'map', 'dashboard' or 'statistics'"
+ )
+
+ this.WriteFile(
+ "./Docs/URL_Parameters.md",
+ QueryParameterDocumentation.GenerateQueryParameterDocs(),
+ ["src/Logic/Web/QueryParameters.ts", "src/UI/QueryParameterDocumentation.ts"]
+ )
+ }
+
+ private generateBuiltinQuestions() {
+ const qLayer = new LayerConfig(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 = new Set()
+ allLayers.forEach((l) => builtinLayerIds.add(l.id))
+
+ const themesPerLayer = new Map()
+
+ 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 = new Map()
+
+ 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(l.id) >= 0,
+ Constants.no_include.indexOf(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"])
+ }
}
-WriteFile("./Docs/URL_Parameters.md", QueryParameterDocumentation.GenerateQueryParameterDocs(), [
- "src/Logic/Web/QueryParameters.ts",
- "src/UI/QueryParameterDocumentation.ts",
-])
-if (fakedom === undefined) {
- throw "FakeDom not initialized"
-}
-QueryParameters.GetQueryParameter(
- "mode",
- "map",
- "The mode the application starts in, e.g. 'map', 'dashboard' or 'statistics'"
-)
-
-{
- new ThemeViewState(new LayoutConfig(bookcases))
- WriteFile("./Docs/Hotkeys.md", Hotkeys.generateDocumentation(), [])
-}
-
-console.log("Generated docs")
+new GenerateDocs().run()
diff --git a/src/Logic/DetermineLayout.ts b/src/Logic/DetermineLayout.ts
index 0b0e9b9a7d..a212e380cd 100644
--- a/src/Logic/DetermineLayout.ts
+++ b/src/Logic/DetermineLayout.ts
@@ -137,11 +137,12 @@ export default class DetermineLayout {
if (json.layers === undefined && json.tagRenderings !== undefined) {
// We got fed a layer instead of a theme
const layerConfig = json
- const iconTr: string | TagRenderingConfigJson = (
- layerConfig.pointRendering
- .map((mr) => mr?.marker?.find((icon) => icon.icon !== undefined)?.icon)
- .find((i) => i !== undefined)
- ) ?? "bug"
+ const iconTr: string | TagRenderingConfigJson =
+ (
+ layerConfig.pointRendering
+ .map((mr) => mr?.marker?.find((icon) => icon.icon !== undefined)?.icon)
+ .find((i) => i !== undefined)
+ ) ?? "bug"
const icon = new TagRenderingConfig(iconTr).render.txt
json = {
id: json.id,
@@ -156,8 +157,8 @@ export default class DetermineLayout {
}
const knownLayersDict = new Map()
- 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, layer)
}
const convertState: DesugaringContext = {
diff --git a/src/Models/Denomination.ts b/src/Models/Denomination.ts
index 04368f733e..4ec3ffb91b 100644
--- a/src/Models/Denomination.ts
+++ b/src/Models/Denomination.ts
@@ -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
- }
}
diff --git a/src/Models/ThemeConfig/Json/LayerConfigJson.ts b/src/Models/ThemeConfig/Json/LayerConfigJson.ts
index 7c81ddf4ca..bfcf551a3a 100644
--- a/src/Models/ThemeConfig/Json/LayerConfigJson.ts
+++ b/src/Models/ThemeConfig/Json/LayerConfigJson.ts
@@ -517,7 +517,10 @@ export interface LayerConfigJson {
*
* group: editing
*/
- units?: UnitConfigJson[]
+ units?: (
+ | UnitConfigJson
+ | Record
+ )[]
/**
* If set, synchronizes whether or not this layer is enabled.
diff --git a/src/Models/ThemeConfig/Json/UnitConfigJson.ts b/src/Models/ThemeConfig/Json/UnitConfigJson.ts
index fbfd0ef9b8..5c95728330 100644
--- a/src/Models/ThemeConfig/Json/UnitConfigJson.ts
+++ b/src/Models/ThemeConfig/Json/UnitConfigJson.ts
@@ -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()}`
*/
- 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
}
diff --git a/src/Models/ThemeConfig/LayerConfig.ts b/src/Models/ThemeConfig/LayerConfig.ts
index 4d8384c604..60813b15b1 100644
--- a/src/Models/ThemeConfig/LayerConfig.ts
+++ b/src/Models/ThemeConfig/LayerConfig.ts
@@ -105,8 +105,10 @@ 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) =>
- Unit.fromJson(unitJson, `${context}.unit[${i}]`)
+ this.units = [].concat(
+ ...(json.units ?? []).map((unitJson, i) =>
+ Unit.fromJson(unitJson, `${context}.unit[${i}]`)
+ )
)
if (json.description !== undefined) {
diff --git a/src/Models/Unit.ts b/src/Models/Unit.ts
index b28663e436..67613500d0 100644
--- a/src/Models/Unit.ts
+++ b/src/Models/Unit.ts
@@ -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
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,
+ ctx: string
+ ): Unit[] {
+ if (!json.appliesToKey && !json.quantity) {
+ return this.loadFromLibrary(json, ctx)
+ }
+ return [this.parse(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 {
+ const m = new Map()
+ const units = (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
- }
- 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
- }
+ 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
}
- 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]
}
}
diff --git a/src/UI/InputElement/ValidatedInput.svelte b/src/UI/InputElement/ValidatedInput.svelte
index 416db5347f..7abf14d35b 100644
--- a/src/UI/InputElement/ValidatedInput.svelte
+++ b/src/UI/InputElement/ValidatedInput.svelte
@@ -1,95 +1,102 @@
@@ -150,7 +157,7 @@
{/if}
{#if unit !== undefined}
-
+
{/if}
{/if}
diff --git a/src/UI/Popup/UnitInput.svelte b/src/UI/Popup/UnitInput.svelte
index 4cd3216e36..663cb8db0e 100644
--- a/src/UI/Popup/UnitInput.svelte
+++ b/src/UI/Popup/UnitInput.svelte
@@ -1,56 +1,67 @@