From 11d6efbd33af6a1b2db43a4e53b55b449dc851ea Mon Sep 17 00:00:00 2001 From: Matthias Schaub Date: Thu, 26 Sep 2024 04:15:56 +0200 Subject: [PATCH 1/3] refactor(indicators): separate templates from metadata separate indicators text templates for label and result description from metadata. --- .../attribute_completeness/indicator.py | 8 ++--- .../attribute_completeness/metadata.yaml | 12 ------- .../attribute_completeness/templates.yaml | 13 ++++++++ ohsome_quality_api/indicators/base.py | 33 +++++++++++++++---- .../building_comparison/indicator.py | 8 ++--- .../building_comparison/metadata.yaml | 11 ------- .../building_comparison/templates.yaml | 12 +++++++ .../indicators/currentness/indicator.py | 4 +-- .../indicators/currentness/metadata.yaml | 11 ------- .../indicators/currentness/templates.yaml | 12 +++++++ .../indicators/density/indicator.py | 8 ++--- .../indicators/density/metadata.yaml | 17 ---------- .../indicators/density/templates.yaml | 18 ++++++++++ .../mapping_saturation/indicator.py | 4 +-- .../mapping_saturation/metadata.yaml | 11 ------- .../mapping_saturation/templates.yaml | 12 +++++++ .../indicators/minimal/indicator.py | 6 ++-- .../indicators/minimal/metadata.yaml | 11 ------- .../indicators/minimal/templates.yaml | 12 +++++++ ohsome_quality_api/indicators/models.py | 9 +++-- .../indicators/road_comparison/indicator.py | 2 +- .../indicators/road_comparison/metadata.yaml | 14 -------- .../indicators/road_comparison/templates.yaml | 15 +++++++++ tests/integrationtests/test_base_indicator.py | 11 ++++++- 24 files changed, 158 insertions(+), 116 deletions(-) create mode 100644 ohsome_quality_api/indicators/attribute_completeness/templates.yaml create mode 100644 ohsome_quality_api/indicators/building_comparison/templates.yaml create mode 100644 ohsome_quality_api/indicators/currentness/templates.yaml create mode 100644 ohsome_quality_api/indicators/density/templates.yaml create mode 100644 ohsome_quality_api/indicators/mapping_saturation/templates.yaml create mode 100644 ohsome_quality_api/indicators/minimal/templates.yaml create mode 100644 ohsome_quality_api/indicators/road_comparison/templates.yaml diff --git a/ohsome_quality_api/indicators/attribute_completeness/indicator.py b/ohsome_quality_api/indicators/attribute_completeness/indicator.py index d782d6090..0b153d18c 100644 --- a/ohsome_quality_api/indicators/attribute_completeness/indicator.py +++ b/ohsome_quality_api/indicators/attribute_completeness/indicator.py @@ -67,7 +67,7 @@ def calculate(self) -> None: self.result.value = None if self.result.value is None: return - description = Template(self.metadata.result_description).substitute( + description = Template(self.templates.result_description).substitute( result=round(self.result.value, 2), all=round(self.absolute_value_1, 1), matched=round(self.absolute_value_2, 1), @@ -79,17 +79,17 @@ def calculate(self) -> None: if self.result.value >= self.threshold_yellow: self.result.class_ = 5 self.result.description = ( - description + self.metadata.label_description["green"] + description + self.templates.label_description["green"] ) elif self.threshold_yellow > self.result.value >= self.threshold_red: self.result.class_ = 3 self.result.description = ( - description + self.metadata.label_description["yellow"] + description + self.templates.label_description["yellow"] ) else: self.result.class_ = 1 self.result.description = ( - description + self.metadata.label_description["red"] + description + self.templates.label_description["red"] ) def create_figure(self) -> None: diff --git a/ohsome_quality_api/indicators/attribute_completeness/metadata.yaml b/ohsome_quality_api/indicators/attribute_completeness/metadata.yaml index 5602d5330..42aafd0f0 100644 --- a/ohsome_quality_api/indicators/attribute_completeness/metadata.yaml +++ b/ohsome_quality_api/indicators/attribute_completeness/metadata.yaml @@ -8,15 +8,3 @@ attribute-completeness: Derive the ratio of OSM features compared to features which match additional expected tags (e.g. amenity=hospital vs amenity=hospital and wheelchair=yes). - label_description: - red: >- - Less than 25% of the features match the expected tags. - yellow: >- - Around 25-75% of the features match the expected tags. - green: >- - More than 75% of the features match the expected tags. - undefined: >- - The quality level could not be calculated for this indicator. - result_description: >- - The ratio of the features (all: $all) compared to features with - expected tags (matched: $matched) is $result. diff --git a/ohsome_quality_api/indicators/attribute_completeness/templates.yaml b/ohsome_quality_api/indicators/attribute_completeness/templates.yaml new file mode 100644 index 000000000..111f58da4 --- /dev/null +++ b/ohsome_quality_api/indicators/attribute_completeness/templates.yaml @@ -0,0 +1,13 @@ +--- +label_description: + red: >- + Less than 25% of the features match the expected tags. + yellow: >- + Around 25-75% of the features match the expected tags. + green: >- + More than 75% of the features match the expected tags. + undefined: >- + The quality level could not be calculated for this indicator. +result_description: >- + The ratio of the features (all: $all) compared to features with + expected tags (matched: $matched) is $result. diff --git a/ohsome_quality_api/indicators/base.py b/ohsome_quality_api/indicators/base.py index ee0b02f82..12fbf3167 100644 --- a/ohsome_quality_api/indicators/base.py +++ b/ohsome_quality_api/indicators/base.py @@ -1,13 +1,23 @@ import json +import os from abc import ABCMeta, abstractmethod import plotly.graph_objects as go +import yaml from geojson import Feature, Polygon from ohsome_quality_api.definitions import get_attribution, get_metadata -from ohsome_quality_api.indicators.models import IndicatorMetadata, Result +from ohsome_quality_api.indicators.models import ( + IndicatorMetadata, + IndicatorTemplates, + Result, +) from ohsome_quality_api.topics.models import BaseTopic as Topic -from ohsome_quality_api.utils.helper import json_serialize +from ohsome_quality_api.utils.helper import ( + camel_to_snake, + get_module_dir, + json_serialize, +) class BaseIndicator(metaclass=ABCMeta): @@ -21,10 +31,11 @@ def __init__( self.metadata: IndicatorMetadata = get_metadata( "indicators", type(self).__name__ ) + self.templates: IndicatorTemplates = self.get_template() self.topic: Topic = topic self.feature: Feature = feature self.result: Result = Result( - description=self.metadata.label_description["undefined"], + description=self.templates.label_description["undefined"], ) self._get_default_figure() @@ -34,10 +45,7 @@ def as_dict(self, include_data: bool = False, exclude_label: bool = False) -> di else: result = self.result.model_dump(by_alias=True) raw_dict = { - "metadata": self.metadata.model_dump( - by_alias=True, - exclude={"result_description", "label_description"}, - ), + "metadata": self.metadata.model_dump(by_alias=True), "topic": self.topic.model_dump( by_alias=True, exclude={"ratio_filter"}, @@ -86,6 +94,7 @@ def data(self) -> dict: data = vars(self).copy() data.pop("result") data.pop("metadata") + data.pop("templates") data.pop("topic") data.pop("feature") return json.loads(json.dumps(data, default=json_serialize).encode()) @@ -167,3 +176,13 @@ def _get_default_figure(self) -> None: raw = fig.to_dict() raw["layout"].pop("template") # remove boilerplate self.result.figure = raw + + def get_template(self) -> IndicatorTemplates: + """Get template for indicator.""" + indicator_key = camel_to_snake(type(self).__name__) + dir = get_module_dir(f"ohsome_quality_api.indicators.{indicator_key}") + file = os.path.join(dir, "templates.yaml") + with open(file, "r") as file: + raw = yaml.safe_load(file) + templates = IndicatorTemplates(**raw) + return templates diff --git a/ohsome_quality_api/indicators/building_comparison/indicator.py b/ohsome_quality_api/indicators/building_comparison/indicator.py index 420226adf..6cf5be9b1 100644 --- a/ohsome_quality_api/indicators/building_comparison/indicator.py +++ b/ohsome_quality_api/indicators/building_comparison/indicator.py @@ -111,7 +111,7 @@ def calculate(self) -> None: edge_case = self.check_minor_edge_cases(key) # ZeroDivisionError can not occur because of `check_major_edge_cases()` self.ratio[key] = self.area_osm[key] / self.area_ref[key] - template = Template(self.metadata.result_description) + template = Template(self.templates.result_description) description = template.substitute( ratio=round(self.ratio[key] * 100, 2), coverage=round(self.area_cov[key] * 100, 2), @@ -130,13 +130,13 @@ def calculate(self) -> None: self.result.class_ = 3 elif self.th_low > self.result.value >= 0: self.result.class_ = 1 - label_description = self.metadata.label_description[self.result.label] + label_description = self.templates.label_description[self.result.label] self.result.description = " ".join((label_description, result_description)) elif major_edge_case: - label_description = self.metadata.label_description[self.result.label] + label_description = self.templates.label_description[self.result.label] self.result.description = " ".join((label_description, result_description)) else: - label_description = self.metadata.label_description[self.result.label] + label_description = self.templates.label_description[self.result.label] edge_case = ( "OSM has substantivly more buildings than the reference datasets. The " "reference dataset is likely to miss many buildings." diff --git a/ohsome_quality_api/indicators/building_comparison/metadata.yaml b/ohsome_quality_api/indicators/building_comparison/metadata.yaml index 0bc928028..11fff1343 100644 --- a/ohsome_quality_api/indicators/building_comparison/metadata.yaml +++ b/ohsome_quality_api/indicators/building_comparison/metadata.yaml @@ -7,14 +7,3 @@ building-comparison: quality_dimension: completeness description: >- Comparison of OSM buildings with the buildings of reference datasets. - label_description: - red: >- - The completeness of OSM buildings in your area-of-interest is low. - yellow: >- - The completeness of OSM buildings in your area-of-interest is medium. - green: >- - The completeness of OSM buildings in your area-of-interest is high. - undefined: >- - Comparison could not be made. - result_description: >- - The completeness in comparison to $dataset is $ratio%. diff --git a/ohsome_quality_api/indicators/building_comparison/templates.yaml b/ohsome_quality_api/indicators/building_comparison/templates.yaml new file mode 100644 index 000000000..4ad550100 --- /dev/null +++ b/ohsome_quality_api/indicators/building_comparison/templates.yaml @@ -0,0 +1,12 @@ +--- +label_description: + red: >- + The completeness of OSM buildings in your area-of-interest is low. + yellow: >- + The completeness of OSM buildings in your area-of-interest is medium. + green: >- + The completeness of OSM buildings in your area-of-interest is high. + undefined: >- + Comparison could not be made. +result_description: >- + The completeness in comparison to $dataset is $ratio%. diff --git a/ohsome_quality_api/indicators/currentness/indicator.py b/ohsome_quality_api/indicators/currentness/indicator.py index 832d0e82d..454d5360c 100644 --- a/ohsome_quality_api/indicators/currentness/indicator.py +++ b/ohsome_quality_api/indicators/currentness/indicator.py @@ -158,9 +158,9 @@ def calculate(self): else: self.result.class_ = 1 - label_description = self.metadata.label_description[self.result.label] + label_description = self.templates.label_description[self.result.label] self.result.description += Template( - self.metadata.result_description + self.templates.result_description ).substitute( up_to_date_contrib_rel=f"{sum(self.bin_up_to_date.contrib_rel) * 100:.0f}", num_of_elements=int(self.contrib_sum), diff --git a/ohsome_quality_api/indicators/currentness/metadata.yaml b/ohsome_quality_api/indicators/currentness/metadata.yaml index 166bc7dbd..902d39323 100644 --- a/ohsome_quality_api/indicators/currentness/metadata.yaml +++ b/ohsome_quality_api/indicators/currentness/metadata.yaml @@ -7,14 +7,3 @@ currentness: quality_dimension: currentness description: >- Estimate currentness of features by classifying contributions based on topic specific temporal thresholds into three groups: up-to-date, in-between and out-of-date. - label_description: - red: >- - Many features are out-of-date. - yellow: >- - Some features are up-to-date and some features are out-of-date. - green: >- - Most features are up-to-date. - undefined: >- - The quality level could not be calculated for this indicator. - result_description: >- - In the area of interest $up_to_date_contrib_rel% of the $num_of_elements features were edited (created or modified) for the last time in the period between $from_timestamp and $to_timestamp. diff --git a/ohsome_quality_api/indicators/currentness/templates.yaml b/ohsome_quality_api/indicators/currentness/templates.yaml new file mode 100644 index 000000000..971f02045 --- /dev/null +++ b/ohsome_quality_api/indicators/currentness/templates.yaml @@ -0,0 +1,12 @@ +--- +label_description: + red: >- + Many features are out-of-date. + yellow: >- + Some features are up-to-date and some features are out-of-date. + green: >- + Most features are up-to-date. + undefined: >- + The quality level could not be calculated for this indicator. +result_description: >- + In the area of interest $up_to_date_contrib_rel% of the $num_of_elements features were edited (created or modified) for the last time in the period between $from_timestamp and $to_timestamp. diff --git a/ohsome_quality_api/indicators/density/indicator.py b/ohsome_quality_api/indicators/density/indicator.py index 116d0ef69..82ade018c 100644 --- a/ohsome_quality_api/indicators/density/indicator.py +++ b/ohsome_quality_api/indicators/density/indicator.py @@ -40,24 +40,24 @@ def calculate(self) -> None: # TODO: we need to think about how we handle this # if there are different topics self.result.value = self.count / self.area_sqkm # density - description = Template(self.metadata.result_description).substitute( + description = Template(self.templates.result_description).substitute( result=f"{self.result.value:.2f}" ) if self.result.value >= self.threshold_yellow: self.result.class_ = 5 self.result.description = ( - description + self.metadata.label_description["green"] + description + self.templates.label_description["green"] ) else: if self.result.value > self.threshold_red: self.result.class_ = 3 self.result.description = ( - description + self.metadata.label_description["yellow"] + description + self.templates.label_description["yellow"] ) else: self.result.class_ = 1 self.result.description = ( - description + self.metadata.label_description["red"] + description + self.templates.label_description["red"] ) def create_figure(self) -> None: diff --git a/ohsome_quality_api/indicators/density/metadata.yaml b/ohsome_quality_api/indicators/density/metadata.yaml index 84d614b99..7d1c3407c 100644 --- a/ohsome_quality_api/indicators/density/metadata.yaml +++ b/ohsome_quality_api/indicators/density/metadata.yaml @@ -8,20 +8,3 @@ density: The density of features. It is calculated by the number of features divided by the area in square-kilometers. - label_description: - red: >- - It is probably hard to orientate on OSM-based sketchmaps of this region. - There are just few orientation providing features available, - you should explore, if participants can orientate properly. - yellow: >- - It might be difficult to orientate on OSM-based sketchmaps of - this region. There are not many orientation providing features available, - you should explore, if participants can orientate properly. - green: >- - It is probably easy to orientate on OSM-based sketchmaps of this region. - undefined: >- - The quality level could not be calculated for this indicator. - result_description: >- - The density of landmarks - (points of reference, e.g. waterbodies, supermarkets, churches, bus stops) - is $result features per sqkm. diff --git a/ohsome_quality_api/indicators/density/templates.yaml b/ohsome_quality_api/indicators/density/templates.yaml new file mode 100644 index 000000000..0b1fc78ec --- /dev/null +++ b/ohsome_quality_api/indicators/density/templates.yaml @@ -0,0 +1,18 @@ +--- +label_description: + red: >- + It is probably hard to orientate on OSM-based sketchmaps of this region. + There are just few orientation providing features available, + you should explore, if participants can orientate properly. + yellow: >- + It might be difficult to orientate on OSM-based sketchmaps of + this region. There are not many orientation providing features available, + you should explore, if participants can orientate properly. + green: >- + It is probably easy to orientate on OSM-based sketchmaps of this region. + undefined: >- + The quality level could not be calculated for this indicator. +result_description: >- + The density of landmarks + (points of reference, e.g. waterbodies, supermarkets, churches, bus stops) + is $result features per sqkm. diff --git a/ohsome_quality_api/indicators/mapping_saturation/indicator.py b/ohsome_quality_api/indicators/mapping_saturation/indicator.py index e6d206732..d8a6478c1 100644 --- a/ohsome_quality_api/indicators/mapping_saturation/indicator.py +++ b/ohsome_quality_api/indicators/mapping_saturation/indicator.py @@ -148,11 +148,11 @@ def calculate(self) -> None: # noqa: C901 self.result.value ) ) - description = Template(self.metadata.result_description).substitute( + description = Template(self.templates.result_description).substitute( saturation=round(self.result.value * 100, 2) ) self.result.description = ( - description + self.metadata.label_description[self.result.label] + description + self.templates.label_description[self.result.label] ) def create_figure(self) -> None: diff --git a/ohsome_quality_api/indicators/mapping_saturation/metadata.yaml b/ohsome_quality_api/indicators/mapping_saturation/metadata.yaml index 333428786..164c6c8c0 100644 --- a/ohsome_quality_api/indicators/mapping_saturation/metadata.yaml +++ b/ohsome_quality_api/indicators/mapping_saturation/metadata.yaml @@ -14,14 +14,3 @@ mapping-saturation: description: >- Calculate if mapping has saturated. High saturation has been reached if the growth of the fitted curve is minimal. - label_description: - red: >- - No saturation identified (Saturation ≤ 30%). - yellow: >- - Saturation is in progress (30% < Saturation ≤ 97%). - green: >- - High saturation has been reached (97% < Saturation ≤ 100%). - undefined: >- - Saturation could not be calculated. - result_description: >- - The saturation of the last 3 years is $saturation%. diff --git a/ohsome_quality_api/indicators/mapping_saturation/templates.yaml b/ohsome_quality_api/indicators/mapping_saturation/templates.yaml new file mode 100644 index 000000000..09fef3ef4 --- /dev/null +++ b/ohsome_quality_api/indicators/mapping_saturation/templates.yaml @@ -0,0 +1,12 @@ +--- +label_description: + red: >- + No saturation identified (Saturation ≤ 30%). + yellow: >- + Saturation is in progress (30% < Saturation ≤ 97%). + green: >- + High saturation has been reached (97% < Saturation ≤ 100%). + undefined: >- + Saturation could not be calculated. +result_description: >- + The saturation of the last 3 years is $saturation%. diff --git a/ohsome_quality_api/indicators/minimal/indicator.py b/ohsome_quality_api/indicators/minimal/indicator.py index f3476c00e..001487e65 100644 --- a/ohsome_quality_api/indicators/minimal/indicator.py +++ b/ohsome_quality_api/indicators/minimal/indicator.py @@ -23,9 +23,11 @@ async def preprocess(self) -> None: ) def calculate(self) -> None: - description = Template(self.metadata.result_description).substitute() + description = Template(self.templates.result_description).substitute() self.result.value = 1.0 - self.result.description = description + self.metadata.label_description["green"] + self.result.description = ( + description + self.templates.label_description["green"] + ) def create_figure(self) -> None: # Do nothing ... diff --git a/ohsome_quality_api/indicators/minimal/metadata.yaml b/ohsome_quality_api/indicators/minimal/metadata.yaml index 66fb6b630..49cae1cce 100644 --- a/ohsome_quality_api/indicators/minimal/metadata.yaml +++ b/ohsome_quality_api/indicators/minimal/metadata.yaml @@ -6,14 +6,3 @@ minimal: quality_dimension: minimal description: >- An minimal Indicator for testing purposes. - label_description: - red: >- - Bad data quality. - yellow: >- - Medium data quality. - green: >- - Good data quality. - undefined: >- - The quality level could not be calculated for this indicator. - result_description: >- - Some description of the result. diff --git a/ohsome_quality_api/indicators/minimal/templates.yaml b/ohsome_quality_api/indicators/minimal/templates.yaml new file mode 100644 index 000000000..b7f5e0a21 --- /dev/null +++ b/ohsome_quality_api/indicators/minimal/templates.yaml @@ -0,0 +1,12 @@ +--- +label_description: + red: >- + Bad data quality. + yellow: >- + Medium data quality. + green: >- + Good data quality. + undefined: >- + The quality level could not be calculated for this indicator. +result_description: >- + Some description of the result. diff --git a/ohsome_quality_api/indicators/models.py b/ohsome_quality_api/indicators/models.py index 272b7e90d..ab7418210 100644 --- a/ohsome_quality_api/indicators/models.py +++ b/ohsome_quality_api/indicators/models.py @@ -13,8 +13,6 @@ class IndicatorMetadata(BaseModel): name: str description: str - label_description: dict - result_description: str projects: list[ProjectEnum] quality_dimension: QualityDimensionEnum model_config = ConfigDict( @@ -26,6 +24,13 @@ class IndicatorMetadata(BaseModel): ) +class IndicatorTemplates(BaseModel): + """Result text templates of an indicator as defined in the templates.yaml file.""" + + label_description: dict[str, str] + result_description: str + + class Result(BaseModel): """The result of the Indicator. diff --git a/ohsome_quality_api/indicators/road_comparison/indicator.py b/ohsome_quality_api/indicators/road_comparison/indicator.py index 57946c0f9..08270700b 100644 --- a/ohsome_quality_api/indicators/road_comparison/indicator.py +++ b/ohsome_quality_api/indicators/road_comparison/indicator.py @@ -138,7 +138,7 @@ def calculate(self) -> None: elif self.th_low > self.result.value >= 0: self.result.class_ = 1 - label_description = self.metadata.label_description[self.result.label] + label_description = self.templates.label_description[self.result.label] self.result.description += label_description # remove double white spaces self.result.description = " ".join(self.result.description.split()) diff --git a/ohsome_quality_api/indicators/road_comparison/metadata.yaml b/ohsome_quality_api/indicators/road_comparison/metadata.yaml index f0b66f7ce..e950387f3 100644 --- a/ohsome_quality_api/indicators/road_comparison/metadata.yaml +++ b/ohsome_quality_api/indicators/road_comparison/metadata.yaml @@ -8,17 +8,3 @@ road-comparison: description: >- Compare the road length of OSM roads with the road length of reference data. - label_description: - red: >- - The completeness of OSM roads in your area-of-interest is low. - yellow: >- - The completeness of OSM roads in your area-of-interest is medium. - green: >- - The completeness of OSM roads in your area-of-interest is high. - - undefined: >- - Comparison could not be made. - result_description: >- - The completeness in comparison to $dataset is $ratio%. - $dataset have a total length of $length_total. - $length_matched of $dataset are covered by OSM roads. diff --git a/ohsome_quality_api/indicators/road_comparison/templates.yaml b/ohsome_quality_api/indicators/road_comparison/templates.yaml new file mode 100644 index 000000000..d2c5e6429 --- /dev/null +++ b/ohsome_quality_api/indicators/road_comparison/templates.yaml @@ -0,0 +1,15 @@ +--- +label_description: + red: >- + The completeness of OSM roads in your area-of-interest is low. + yellow: >- + The completeness of OSM roads in your area-of-interest is medium. + green: >- + The completeness of OSM roads in your area-of-interest is high. + + undefined: >- + Comparison could not be made. +result_description: >- + The completeness in comparison to $dataset is $ratio%. + $dataset have a total length of $length_total. + $length_matched of $dataset are covered by OSM roads. diff --git a/tests/integrationtests/test_base_indicator.py b/tests/integrationtests/test_base_indicator.py index 6f007dff1..b045b448a 100644 --- a/tests/integrationtests/test_base_indicator.py +++ b/tests/integrationtests/test_base_indicator.py @@ -5,7 +5,10 @@ from geojson import Feature from ohsome_quality_api.indicators.minimal.indicator import Minimal -from ohsome_quality_api.indicators.models import Result +from ohsome_quality_api.indicators.models import ( + IndicatorTemplates, + Result, +) from .utils import get_geojson_fixture, get_topic_fixture @@ -81,6 +84,12 @@ def test_figure(self, feature, topic): # comment out for manual test # pio.show(indicator.result.figure) + def test_get_template(self, feature, topic): + indicator = Minimal(feature=feature, topic=topic) + indicator.get_template() + assert isinstance(indicator.templates, IndicatorTemplates) + assert isinstance(indicator.result, Result) + class TestBaseResult: def test_label(self): From dc9873a6c5e687cfa11526cb652cac8eede1f58f Mon Sep 17 00:00:00 2001 From: Matthias Schaub Date: Thu, 26 Sep 2024 05:30:45 +0200 Subject: [PATCH 2/3] refactor(indicators): merge all metadata files merge all indicator metadata files into one similar to metadata files of topics and projects --- ohsome_quality_api/api/api.py | 9 +-- ohsome_quality_api/definitions.py | 30 +------- .../attribute_completeness/metadata.yaml | 10 --- ohsome_quality_api/indicators/base.py | 8 ++- .../building_comparison/metadata.yaml | 9 --- .../indicators/currentness/metadata.yaml | 9 --- ohsome_quality_api/indicators/definitions.py | 33 +++++++-- .../indicators/density/metadata.yaml | 10 --- ohsome_quality_api/indicators/indicators.yaml | 69 +++++++++++++++++++ .../mapping_saturation/metadata.yaml | 16 ----- .../indicators/minimal/metadata.yaml | 8 --- .../indicators/road_comparison/metadata.yaml | 10 --- tests/conftest.py | 10 +-- tests/unittests/test_definitions.py | 28 -------- tests/unittests/test_helper.py | 4 +- .../unittests/test_indicators_definitions.py | 11 ++- 16 files changed, 122 insertions(+), 152 deletions(-) delete mode 100644 ohsome_quality_api/indicators/attribute_completeness/metadata.yaml delete mode 100644 ohsome_quality_api/indicators/building_comparison/metadata.yaml delete mode 100644 ohsome_quality_api/indicators/currentness/metadata.yaml delete mode 100644 ohsome_quality_api/indicators/density/metadata.yaml create mode 100644 ohsome_quality_api/indicators/indicators.yaml delete mode 100644 ohsome_quality_api/indicators/mapping_saturation/metadata.yaml delete mode 100644 ohsome_quality_api/indicators/minimal/metadata.yaml delete mode 100644 ohsome_quality_api/indicators/road_comparison/metadata.yaml diff --git a/ohsome_quality_api/api/api.py b/ohsome_quality_api/api/api.py index a0a710d68..eb26ff927 100644 --- a/ohsome_quality_api/api/api.py +++ b/ohsome_quality_api/api/api.py @@ -42,14 +42,12 @@ ) from ohsome_quality_api.attributes.definitions import get_attributes, load_attributes from ohsome_quality_api.config import configure_logging -from ohsome_quality_api.definitions import ( - ATTRIBUTION_URL, - get_metadata, -) +from ohsome_quality_api.definitions import ATTRIBUTION_URL from ohsome_quality_api.indicators.definitions import ( IndicatorEnum, IndicatorEnumRequest, get_coverage, + get_indicator, get_indicator_metadata, ) from ohsome_quality_api.projects.definitions import ( @@ -75,7 +73,6 @@ ) from ohsome_quality_api.utils.helper import ( get_class_from_key, - hyphen_to_camel, json_serialize, ) from ohsome_quality_api.utils.validators import ( @@ -450,7 +447,7 @@ async def metadata_indicators(project: ProjectEnum = DEFAULT_PROJECT) -> Any: ) async def metadata_indicators_by_key(key: IndicatorEnum) -> Any: """Get metadata of an indicator by key.""" - metadata = get_metadata("indicators", hyphen_to_camel(key.value)) + metadata = get_indicator(key.value) return {"result": {key.value: metadata}} diff --git a/ohsome_quality_api/definitions.py b/ohsome_quality_api/definitions.py index a05510cd0..b7a1dedec 100644 --- a/ohsome_quality_api/definitions.py +++ b/ohsome_quality_api/definitions.py @@ -1,17 +1,14 @@ """Global Variables and Functions.""" import glob -import logging from enum import Enum from types import MappingProxyType -from typing import Iterable, Literal +from typing import Literal import yaml from ohsome_quality_api.indicators.models import IndicatorMetadata -from ohsome_quality_api.topics.definitions import load_topic_presets from ohsome_quality_api.utils.helper import ( - camel_to_hyphen, get_module_dir, ) @@ -77,31 +74,6 @@ def load_metadata( return metadata -def get_metadata( - module_name: Literal["indicators"], class_name: str -) -> IndicatorMetadata: - """Get metadata of an indicator based on its class name. - - This is implemented outside the metadata class to be able to access metadata of all - indicators without instantiating of those. - - Args: - module_name: indicators. - class_name: Class name of an indicator (camel case). - """ - metadata = load_metadata(module_name) - try: - return metadata[camel_to_hyphen(class_name)] - except KeyError: - logging.error("Invalid class name: " + class_name) - raise - - -# TODO: duplicate of func with the same name in projects/definition.py ? -def get_project_keys() -> Iterable[str]: - return set(t.project for t in load_topic_presets().values()) - - def get_attribution(data_keys: list) -> str: """Return attribution text. Individual attributions are separated by semicolons.""" assert set(data_keys) <= {"OSM", "GHSL", "VNL", "EUBUCCO", "Microsoft Buildings"} diff --git a/ohsome_quality_api/indicators/attribute_completeness/metadata.yaml b/ohsome_quality_api/indicators/attribute_completeness/metadata.yaml deleted file mode 100644 index 42aafd0f0..000000000 --- a/ohsome_quality_api/indicators/attribute_completeness/metadata.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -attribute-completeness: - name: Attribute Completeness - projects: - - bkg - quality_dimension: completeness - description: >- - Derive the ratio of OSM features compared to features which - match additional expected tags (e.g. amenity=hospital vs - amenity=hospital and wheelchair=yes). diff --git a/ohsome_quality_api/indicators/base.py b/ohsome_quality_api/indicators/base.py index 12fbf3167..fd81da468 100644 --- a/ohsome_quality_api/indicators/base.py +++ b/ohsome_quality_api/indicators/base.py @@ -6,7 +6,8 @@ import yaml from geojson import Feature, Polygon -from ohsome_quality_api.definitions import get_attribution, get_metadata +from ohsome_quality_api.definitions import get_attribution +from ohsome_quality_api.indicators.definitions import get_indicator from ohsome_quality_api.indicators.models import ( IndicatorMetadata, IndicatorTemplates, @@ -14,6 +15,7 @@ ) from ohsome_quality_api.topics.models import BaseTopic as Topic from ohsome_quality_api.utils.helper import ( + camel_to_hyphen, camel_to_snake, get_module_dir, json_serialize, @@ -28,8 +30,8 @@ def __init__( topic: Topic, feature: Feature, ) -> None: - self.metadata: IndicatorMetadata = get_metadata( - "indicators", type(self).__name__ + self.metadata: IndicatorMetadata = get_indicator( + camel_to_hyphen(type(self).__name__) ) self.templates: IndicatorTemplates = self.get_template() self.topic: Topic = topic diff --git a/ohsome_quality_api/indicators/building_comparison/metadata.yaml b/ohsome_quality_api/indicators/building_comparison/metadata.yaml deleted file mode 100644 index 11fff1343..000000000 --- a/ohsome_quality_api/indicators/building_comparison/metadata.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -building-comparison: - name: Building Comparison - projects: - - bkg - - core - quality_dimension: completeness - description: >- - Comparison of OSM buildings with the buildings of reference datasets. diff --git a/ohsome_quality_api/indicators/currentness/metadata.yaml b/ohsome_quality_api/indicators/currentness/metadata.yaml deleted file mode 100644 index 902d39323..000000000 --- a/ohsome_quality_api/indicators/currentness/metadata.yaml +++ /dev/null @@ -1,9 +0,0 @@ ---- -currentness: - name: Currentness - projects: - - core - - bkg - quality_dimension: currentness - description: >- - Estimate currentness of features by classifying contributions based on topic specific temporal thresholds into three groups: up-to-date, in-between and out-of-date. diff --git a/ohsome_quality_api/indicators/definitions.py b/ohsome_quality_api/indicators/definitions.py index e3802ae96..9b442ed4c 100644 --- a/ohsome_quality_api/indicators/definitions.py +++ b/ohsome_quality_api/indicators/definitions.py @@ -1,26 +1,51 @@ +import os from enum import Enum +import yaml from geojson import FeatureCollection -from ohsome_quality_api.definitions import load_metadata from ohsome_quality_api.indicators.models import IndicatorMetadata from ohsome_quality_api.projects.definitions import ProjectEnum from ohsome_quality_api.topics.definitions import load_topic_presets -from ohsome_quality_api.utils.helper import get_class_from_key +from ohsome_quality_api.utils.helper import ( + get_class_from_key, + get_module_dir, +) def get_indicator_keys() -> list[str]: - return list(load_metadata("indicators").keys()) + return [str(t) for t in load_indicators().keys()] + + +def load_indicators() -> dict[str, IndicatorMetadata]: + directory = get_module_dir("ohsome_quality_api.indicators") + file = os.path.join(directory, "indicators.yaml") + with open(file, "r") as f: + raw = yaml.safe_load(f) + indicators = {} + for k, v in raw.items(): + indicators[k] = IndicatorMetadata(**v) + return indicators def get_indicator_metadata(project: ProjectEnum = None) -> dict[str, IndicatorMetadata]: - indicators = load_metadata("indicators") + indicators = load_indicators() if project is not None: return {k: v for k, v in indicators.items() if project in v.projects} else: return indicators +def get_indicator(indicator_key: str) -> IndicatorMetadata: + indicators = get_indicator_metadata() + try: + return indicators[indicator_key] + except KeyError as error: + raise KeyError( + "Invalid project key. Valid project keys are: " + str(indicators.keys()) + ) from error + + def get_valid_indicators(topic_key: str) -> tuple: """Get valid Indicator/Topic combination of a Topic.""" td = load_topic_presets() diff --git a/ohsome_quality_api/indicators/density/metadata.yaml b/ohsome_quality_api/indicators/density/metadata.yaml deleted file mode 100644 index 7d1c3407c..000000000 --- a/ohsome_quality_api/indicators/density/metadata.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -density: - name: Density - projects: - - experimental - quality_dimension: none - description: >- - The density of features. - It is calculated by the number of features - divided by the area in square-kilometers. diff --git a/ohsome_quality_api/indicators/indicators.yaml b/ohsome_quality_api/indicators/indicators.yaml new file mode 100644 index 000000000..f54af45b2 --- /dev/null +++ b/ohsome_quality_api/indicators/indicators.yaml @@ -0,0 +1,69 @@ +--- +density: + name: Density + projects: + - experimental + quality_dimension: none + description: >- + The density of features. + It is calculated by the number of features + divided by the area in square-kilometers. +minimal: + name: Minimal + projects: + - misc + quality_dimension: minimal + description: >- + An minimal Indicator for testing purposes. +currentness: + name: Currentness + projects: + - core + - bkg + quality_dimension: currentness + description: >- + Estimate currentness of features by classifying contributions based on + topic specific temporal thresholds into three groups: up-to-date, + in-between and out-of-date. +road-comparison: + name: Road Comparison + projects: + - bkg + - core + quality_dimension: completeness + description: >- + Compare the road length of OSM roads with the road length of + reference data. +mapping-saturation: + name: Mapping Saturation + projects: + - core + - corine-land-cover + - expanse + - experimental + - idealvgi + - mapaction + - sketchmap + - bkg + quality_dimension: completeness + description: >- + Calculate if mapping has saturated. + High saturation has been reached if the growth of the fitted curve is + minimal. +building-comparison: + name: Building Comparison + projects: + - bkg + - core + quality_dimension: completeness + description: >- + Comparison of OSM buildings with the buildings of reference datasets. +attribute-completeness: + name: Attribute Completeness + projects: + - bkg + quality_dimension: completeness + description: >- + Derive the ratio of OSM features compared to features which + match additional expected tags (e.g. amenity=hospital vs + amenity=hospital and wheelchair=yes). diff --git a/ohsome_quality_api/indicators/mapping_saturation/metadata.yaml b/ohsome_quality_api/indicators/mapping_saturation/metadata.yaml deleted file mode 100644 index 164c6c8c0..000000000 --- a/ohsome_quality_api/indicators/mapping_saturation/metadata.yaml +++ /dev/null @@ -1,16 +0,0 @@ ---- -mapping-saturation: - name: Mapping Saturation - projects: - - core - - corine-land-cover - - expanse - - experimental - - idealvgi - - mapaction - - sketchmap - - bkg - quality_dimension: completeness - description: >- - Calculate if mapping has saturated. - High saturation has been reached if the growth of the fitted curve is minimal. diff --git a/ohsome_quality_api/indicators/minimal/metadata.yaml b/ohsome_quality_api/indicators/minimal/metadata.yaml deleted file mode 100644 index 49cae1cce..000000000 --- a/ohsome_quality_api/indicators/minimal/metadata.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -minimal: - name: Minimal - projects: - - misc - quality_dimension: minimal - description: >- - An minimal Indicator for testing purposes. diff --git a/ohsome_quality_api/indicators/road_comparison/metadata.yaml b/ohsome_quality_api/indicators/road_comparison/metadata.yaml deleted file mode 100644 index e950387f3..000000000 --- a/ohsome_quality_api/indicators/road_comparison/metadata.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -road-comparison: - name: Road Comparison - projects: - - bkg - - core - quality_dimension: completeness - description: >- - Compare the road length of OSM roads with the road length of - reference data. diff --git a/tests/conftest.py b/tests/conftest.py index 142a30786..b4b319d79 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,9 +6,9 @@ from geojson import Feature, FeatureCollection, Polygon from ohsome_quality_api.attributes.models import Attribute -from ohsome_quality_api.definitions import ( - get_metadata, - load_metadata, +from ohsome_quality_api.indicators.definitions import ( + get_indicator, + get_indicator_metadata, ) from ohsome_quality_api.indicators.models import IndicatorMetadata from ohsome_quality_api.projects.definitions import get_project, load_projects @@ -241,9 +241,9 @@ def feature_collection_unsupported_geometry_type(request) -> FeatureCollection: @pytest.fixture def metadata_indicator_minimal() -> dict[str, IndicatorMetadata]: - return {"minimal": get_metadata("indicators", "Minimal")} + return {"minimal": get_indicator("minimal")} @pytest.fixture def indicators_metadata() -> dict[str, IndicatorMetadata]: - return load_metadata("indicators") + return get_indicator_metadata() diff --git a/tests/unittests/test_definitions.py b/tests/unittests/test_definitions.py index 3cf1dd01c..a335563d1 100644 --- a/tests/unittests/test_definitions.py +++ b/tests/unittests/test_definitions.py @@ -1,7 +1,5 @@ import unittest -import pytest - from ohsome_quality_api import definitions from ohsome_quality_api.indicators.models import ( IndicatorMetadata as IndicatorMetadata, @@ -24,29 +22,3 @@ def test_get_attribution(self): ) self.assertRaises(AssertionError, definitions.get_attribution, ["MSO"]) - - -def test_load_metadata_indicator(): - metadata = definitions.load_metadata("indicators") - assert isinstance(metadata, dict) - for v in metadata.values(): - assert isinstance(v, IndicatorMetadata) - - -def test_load_metadata_wrong_module(): - with pytest.raises(AssertionError): - definitions.load_metadata("foo") - with pytest.raises(AssertionError): - definitions.load_metadata("") - - -def test_get_metadata_indicator(): - metadata = definitions.get_metadata("indicators", "Minimal") - assert isinstance(metadata, IndicatorMetadata) - - -def test_get_metadata_wrong_class(): - with pytest.raises(KeyError): - definitions.get_metadata("indicators", "foo") - with pytest.raises(KeyError): - definitions.get_metadata("indicators", "") diff --git a/tests/unittests/test_helper.py b/tests/unittests/test_helper.py index 6e7959c61..770b4090a 100644 --- a/tests/unittests/test_helper.py +++ b/tests/unittests/test_helper.py @@ -6,7 +6,7 @@ import geojson import numpy as np -from ohsome_quality_api.definitions import load_metadata +from ohsome_quality_api.indicators.definitions import get_indicator_metadata from ohsome_quality_api.indicators.mapping_saturation import models from ohsome_quality_api.indicators.minimal.indicator import ( Minimal as MinimalIndicator, @@ -40,7 +40,7 @@ def test_name_to_class(self): MinimalIndicator, ) - self.indicators = load_metadata("indicators") + self.indicators = get_indicator_metadata() for indicator_name in self.indicators.keys(): self.assertIsNotNone( get_class_from_key(class_type="indicator", key=indicator_name) diff --git a/tests/unittests/test_indicators_definitions.py b/tests/unittests/test_indicators_definitions.py index 8fd36fe3e..4cc92922a 100644 --- a/tests/unittests/test_indicators_definitions.py +++ b/tests/unittests/test_indicators_definitions.py @@ -31,7 +31,7 @@ def mock_get_reference_coverage(class_mocker): ) -def test_get_indicator_names(): +def test_get_indicator_keys(): names = definitions.get_indicator_keys() assert isinstance(names, list) @@ -41,14 +41,14 @@ def test_get_valid_indicators(): assert indicators == ("mapping-saturation", "currentness", "attribute-completeness") -def test_get_indicator_definitions(): +def test_get_indicator_metadata(): indicators = definitions.get_indicator_metadata() assert isinstance(indicators, dict) for indicator in indicators.values(): assert isinstance(indicator, models.IndicatorMetadata) -def test_get_indicator_definitions_with_project(): +def test_get_indicator_metadata_filtered_by_project(): indicators = definitions.get_indicator_metadata("core") assert isinstance(indicators, dict) for indicator in indicators.values(): @@ -56,6 +56,11 @@ def test_get_indicator_definitions_with_project(): assert indicator.projects == ["core"] +def test_get_indicator(metadata_indicator_minimal): + indicator = definitions.get_indicator("minimal") + assert indicator == metadata_indicator_minimal["minimal"] + + def test_get_coverage(mock_get_reference_coverage): coverage = asyncio.run( definitions.get_coverage("building-comparison", inverse=False) From bc7ddffc20a74d428d084fe8e06fe9af21d2f3b9 Mon Sep 17 00:00:00 2001 From: Matthias Schaub Date: Thu, 26 Sep 2024 05:34:43 +0200 Subject: [PATCH 3/3] chore: update CHANGELOG --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7809533a0..93fd1674b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,13 @@ - report endpoint: has been deleted ([#818]) - ### Other Changes - a new regression test suite has been added to support safer deployments of new versions ([#820]) +- refactor(indicators): separate templates from metadata and merge all metadata files ([#673]) - +[#673]: https://github.com/GIScience/ohsome-quality-api/issues/673 [#818]: https://github.com/GIScience/ohsome-quality-api/pull/818 [#820]: https://github.com/GIScience/ohsome-quality-api/issues/820