From 94e07d5b13f86837d02cb6d0906a501a1bdd35fc Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 12 Dec 2023 03:46:51 +0100 Subject: [PATCH] Refactoring: allow to reuse units, move all units into central file --- assets/layers/elevator/elevator.json | 63 +- assets/layers/entrance/entrance.json | 55 +- assets/layers/hydrant/hydrant.json | 37 +- assets/layers/kerbs/kerbs.json | 70 +- assets/layers/maxspeed/maxspeed.json | 74 +- .../observation_tower/observation_tower.json | 29 +- .../layers/reception_desk/reception_desk.json | 45 +- assets/layers/speed_camera/speed_camera.json | 65 +- .../layers/speed_display/speed_display.json | 65 +- assets/layers/sport_pitch/sport_pitch.json | 3 +- assets/layers/toilet/toilet.json | 45 +- .../toilet_at_amenity/toilet_at_amenity.json | 43 +- assets/layers/unit/unit.json | 384 ++++++++ .../walls_and_buildings.json | 53 +- assets/layers/windturbine/windturbine.json | 137 +-- assets/themes/climbing/climbing.json | 76 +- scripts/generateDocs.ts | 829 ++++++++++-------- src/Logic/DetermineLayout.ts | 15 +- src/Models/Denomination.ts | 110 ++- .../ThemeConfig/Json/LayerConfigJson.ts | 5 +- src/Models/ThemeConfig/Json/UnitConfigJson.ts | 13 +- src/Models/ThemeConfig/LayerConfig.ts | 6 +- src/Models/Unit.ts | 162 +++- src/UI/InputElement/ValidatedInput.svelte | 125 +-- src/UI/Popup/UnitInput.svelte | 73 +- src/UI/SpecialVisualizations.ts | 7 +- src/UI/i18n/Translations.ts | 2 +- src/assets/schemas/layerconfigmeta.json | 55 +- src/assets/schemas/layoutconfigmeta.json | 147 +++- test/Models/Units.spec.ts | 9 +- 30 files changed, 1495 insertions(+), 1307 deletions(-) create mode 100644 assets/layers/unit/unit.json diff --git a/assets/layers/elevator/elevator.json b/assets/layers/elevator/elevator.json index 0d59e125d..a6ad92015 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 1d17bdea5..91bf1d8bc 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 9049ee9e9..a3ee15762 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 a2200d543..59bae3a70 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 6ce88e50b..cc5e325f2 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 0a71f60e8..c3a0f3ce9 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 b8e017a87..70c89f230 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 046cc7d16..af401b7ad 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 a29fed538..0688c3300 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 2a7d07649..ffc6a6bab 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 91dc7dc9d..4074e7615 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 011c92881..d21309827 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 000000000..72f33d4c3 --- /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 77251153f..f8368a464 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 82e2787cc..557956f1f 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 82a23b6be..f359b932d 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 d95a9a1a8..a31ffe55a 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 0b0e9b9a7..a212e380c 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 04368f733..4ec3ffb91 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 7c81ddf4c..bfcf551a3 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 fbfd0ef9b..5c9572833 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 4d8384c60..60813b15b 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 b28663e43..67613500d 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 416db5347..7abf14d35 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 4cd3216e3..663cb8db0 100644 --- a/src/UI/Popup/UnitInput.svelte +++ b/src/UI/Popup/UnitInput.svelte @@ -1,56 +1,67 @@