diff --git a/workers/ohsome_quality_analyst/api/api.py b/workers/ohsome_quality_analyst/api/api.py index 1757e2fe6..7c69a4b84 100644 --- a/workers/ohsome_quality_analyst/api/api.py +++ b/workers/ohsome_quality_analyst/api/api.py @@ -32,7 +32,6 @@ from ohsome_quality_analyst.config import configure_logging from ohsome_quality_analyst.definitions import ( ATTRIBUTION_URL, - INDICATOR_LAYER, get_attribution, get_dataset_names, get_fid_fields, @@ -280,14 +279,6 @@ async def get_available_regions(asGeoJSON: bool = False): return response -@app.get("/indicator-layer-combinations") -async def get_indicator_layer_combinations(): - """Get names of available indicator-layer combinations.""" - response = empty_api_response() - response["result"] = INDICATOR_LAYER - return response - - @app.get("/indicators") async def indicator_names(): """Get names of available indicators.""" diff --git a/workers/ohsome_quality_analyst/api/request_models.py b/workers/ohsome_quality_analyst/api/request_models.py index 527af57d3..2f080f2f9 100644 --- a/workers/ohsome_quality_analyst/api/request_models.py +++ b/workers/ohsome_quality_analyst/api/request_models.py @@ -8,7 +8,7 @@ """ from enum import Enum -from typing import Optional, Union +from typing import Optional, Tuple, Union import pydantic from geojson import Feature, FeatureCollection @@ -16,12 +16,12 @@ from ohsome_quality_analyst.base.layer import LayerData from ohsome_quality_analyst.definitions import ( - INDICATOR_LAYER, get_dataset_names, get_fid_fields, get_indicator_names, get_layer_keys, get_report_names, + get_valid_layers, ) from ohsome_quality_analyst.utils.helper import loads_geojson, snake_to_lower_camel @@ -36,6 +36,14 @@ class BaseIndicator(BaseModel): name: IndicatorEnum = pydantic.Field( ..., title="Indicator Name", example="GhsPopComparisonBuildings" ) + thresholds: Optional[ + Tuple[ + Union[float, str], + Union[str, float], + Union[str, float], + Union[str, float], + ] + ] include_svg: bool = False include_html: bool = False include_data: bool = False @@ -48,6 +56,17 @@ class Config: allow_mutation = False extra = "forbid" + @pydantic.root_validator + @classmethod + def validate_thresholds(cls, values): + if values["thresholds"] is not None and values["name"] != "Currentness": + raise ValueError( + "Setting custom thresholds is only supported for the Currentness " + + "Indicator.", + ) + else: + return values + class BaseReport(BaseModel): name: ReportEnum = pydantic.Field( @@ -132,12 +151,15 @@ class IndicatorBpolys(BaseIndicator, BaseLayerName, BaseBpolys): @classmethod def validate_indicator_layer(cls, values): try: - indicator_layer = (values["name"].value, values["layer_key"].value) + indicator_key = values["name"].value + layer_key = values["layer_key"].value except KeyError: raise ValueError("An issue with the layer or indicator name occurred.") - if indicator_layer not in INDICATOR_LAYER: + if layer_key not in get_valid_layers(indicator_key): raise ValueError( - "Indicator layer combination is invalid: " + str(indicator_layer) + "Layer ({0}) is not available for indicator ({1})".format( + layer_key, indicator_key + ) ) else: return values @@ -148,12 +170,15 @@ class IndicatorDatabase(BaseIndicator, BaseLayerName, BaseDatabase): @classmethod def validate_indicator_layer(cls, values): try: - indicator_layer = (values["name"].value, values["layer_key"].value) + indicator_key = values["name"].value + layer_key = values["layer_key"].value except KeyError: raise ValueError("An issue with the layer or indicator name occurred.") - if indicator_layer not in INDICATOR_LAYER: + if layer_key not in get_valid_layers(indicator_key): raise ValueError( - "Indicator layer combination is invalid: " + str(indicator_layer) + "Layer ({0}) is not available for indicator ({1})".format( + layer_key, indicator_key + ) ) else: return values diff --git a/workers/ohsome_quality_analyst/base/indicator.py b/workers/ohsome_quality_analyst/base/indicator.py index 3bfdcf12c..ffa3b316d 100644 --- a/workers/ohsome_quality_analyst/base/indicator.py +++ b/workers/ohsome_quality_analyst/base/indicator.py @@ -5,7 +5,7 @@ from dataclasses import asdict, dataclass from datetime import datetime, timezone from io import StringIO -from typing import Dict, Literal, Optional +from typing import Dict, Literal, Optional, Tuple import matplotlib.pyplot as plt from dacite import from_dict @@ -42,8 +42,8 @@ class Result: value is determined by the result classes value (float): The result value class_ (int): The result class. An integer between 1 and 5. It maps to the - result labels. This value is used by the reports to determine an overall - result. + result labels (1 -> red; 5 -> green). This value is used by the reports to + determine an overall result. description (str): The result description. svg (str): Figure of the result as SVG """ @@ -63,17 +63,35 @@ def label(self) -> Literal["green", "yellow", "red", "undefined"]: class BaseIndicator(metaclass=ABCMeta): - """The base class of every indicator.""" + """The base class of every indicator. + + Attributes: + thresholds (tuple): A tuple with four float values representing the thresholds + between the result classes. The first element is the threshold between the + result class 1 and 2, the second element is the threshold between the result + class 2 and 3 and so on. + """ def __init__( self, layer: Layer, feature: Feature, + thresholds: Optional[Tuple[float, float, float, float]] = None, ) -> None: self.layer: Layer = layer self.feature: Feature = feature + # setattr(object, key, value) could be used instead of relying on from_dict. metadata = get_metadata("indicators", type(self).__name__) + + layer_thresholds = metadata.pop("layer-thresholds") + if thresholds is not None: + self.thresholds = thresholds + elif layer.key in layer_thresholds.keys(): + self.thresholds = layer_thresholds[layer.key] + if hasattr(self, "thresholds") is False or self.thresholds is None: + self.thresholds = layer_thresholds["default"] + self.metadata: Metadata = from_dict(data_class=Metadata, data=metadata) self.result: Result = Result( description=self.metadata.label_description["undefined"], diff --git a/workers/ohsome_quality_analyst/cli/cli.py b/workers/ohsome_quality_analyst/cli/cli.py index e5e05fc0f..13543a4ae 100644 --- a/workers/ohsome_quality_analyst/cli/cli.py +++ b/workers/ohsome_quality_analyst/cli/cli.py @@ -15,11 +15,7 @@ ) from ohsome_quality_analyst.cli import options from ohsome_quality_analyst.config import configure_logging, get_config_value -from ohsome_quality_analyst.definitions import ( - INDICATOR_LAYER, - load_layer_definitions, - load_metadata, -) +from ohsome_quality_analyst.definitions import load_layer_definitions, load_metadata from ohsome_quality_analyst.geodatabase import client as db_client from ohsome_quality_analyst.utils.helper import json_serialize, write_geojson @@ -93,13 +89,6 @@ def get_available_regions(): click.echo(format_row.format(region["ogc_fid"], region["name"])) -@cli.command("list-indicator-layer-combination") -def get_indicator_layer_combination(): - """List all possible indicator-layer-combinations.""" - for combination in INDICATOR_LAYER: - click.echo(combination) - - @cli.command("create-indicator") @cli_option(options.indicator_name) @cli_option(options.layer_key) diff --git a/workers/ohsome_quality_analyst/definitions.py b/workers/ohsome_quality_analyst/definitions.py index 023e93c46..54ee9d188 100644 --- a/workers/ohsome_quality_analyst/definitions.py +++ b/workers/ohsome_quality_analyst/definitions.py @@ -1,10 +1,12 @@ """Global Variables and Functions.""" +from __future__ import annotations + import glob import logging import os from dataclasses import dataclass from types import MappingProxyType -from typing import Dict, List, Optional +from typing import Dict, List, Literal, Optional import yaml @@ -57,130 +59,6 @@ class RasterDataset: ), ) -# Possible indicator layer combinations -INDICATOR_LAYER = ( - ("BuildingCompleteness", "building_area"), - ("GhsPopComparisonBuildings", "building_count"), - ("GhsPopComparisonRoads", "jrc_road_length"), - ("GhsPopComparisonRoads", "major_roads_length"), - ("MappingSaturation", "building_count"), - ("MappingSaturation", "major_roads_length"), - ("MappingSaturation", "amenities"), - ("MappingSaturation", "jrc_health_count"), - ("MappingSaturation", "jrc_mass_gathering_sites_count"), - ("MappingSaturation", "jrc_railway_length"), - ("MappingSaturation", "jrc_road_length"), - ("MappingSaturation", "jrc_education_count"), - ("MappingSaturation", "mapaction_settlements_count"), - ("MappingSaturation", "mapaction_major_roads_length"), - ("MappingSaturation", "mapaction_rail_length"), - ("MappingSaturation", "mapaction_lakes_area"), - ("MappingSaturation", "mapaction_rivers_length"), - ("MappingSaturation", "infrastructure_lines"), - ("MappingSaturation", "poi"), - ("MappingSaturation", "lulc"), - ("MappingSaturation", "schools"), - ("MappingSaturation", "kindergarten"), - ("MappingSaturation", "clinics"), - ("MappingSaturation", "doctors"), - ("MappingSaturation", "bus_stops"), - ("MappingSaturation", "tram_stops"), - ("MappingSaturation", "subway_stations"), - ("MappingSaturation", "supermarkets"), - ("MappingSaturation", "marketplaces"), - ("MappingSaturation", "parks"), - ("MappingSaturation", "forests"), - ("MappingSaturation", "fitness_centres"), - ("MappingSaturation", "fire_stations"), - ("MappingSaturation", "hospitals"), - ("MappingSaturation", "local_food_shops"), - ("MappingSaturation", "fast_food_restaurants"), - ("MappingSaturation", "restaurants"), - ("MappingSaturation", "supermarkets"), - ("MappingSaturation", "convenience_stores"), - ("MappingSaturation", "pubs_and_biergartens"), - ("MappingSaturation", "alcohol_and_beverages"), - ("MappingSaturation", "sweets_and_pasteries"), - ("MappingSaturation", "railway_length"), - ("MappingSaturation", "clc_arable_land_area"), - ("MappingSaturation", "clc_permanent_crops_area"), - ("MappingSaturation", "clc_pastures_area"), - ("MappingSaturation", "clc_forest_area"), - ("MappingSaturation", "clc_leaf_type"), - ("MappingSaturation", "clc_shrub_area"), - ("MappingSaturation", "clc_open_spaces_area"), - ("MappingSaturation", "clc_wetland_area"), - ("MappingSaturation", "clc_water_area"), - ("MappingSaturation", "clc_waterway_len"), - ("Currentness", "major_roads_count"), - ("Currentness", "building_count"), - ("Currentness", "amenities"), - ("Currentness", "jrc_health_count"), - ("Currentness", "jrc_education_count"), - ("Currentness", "jrc_road_count"), - ("Currentness", "jrc_railway_count"), - ("Currentness", "jrc_airport_count"), - ("Currentness", "jrc_water_treatment_plant_count"), - ("Currentness", "jrc_power_generation_plant_count"), - ("Currentness", "jrc_cultural_heritage_site_count"), - ("Currentness", "jrc_bridge_count"), - ("Currentness", "jrc_mass_gathering_sites_count"), - ("Currentness", "mapaction_settlements_count"), - ("Currentness", "mapaction_major_roads_length"), - ("Currentness", "mapaction_rail_length"), - ("Currentness", "mapaction_lakes_count"), - ("Currentness", "mapaction_rivers_length"), - ("Currentness", "infrastructure_lines"), - ("Currentness", "poi"), - ("Currentness", "lulc"), - ("Currentness", "schools"), - ("Currentness", "kindergarten"), - ("Currentness", "clinics"), - ("Currentness", "doctors"), - ("Currentness", "bus_stops"), - ("Currentness", "tram_stops"), - ("Currentness", "subway_stations"), - ("Currentness", "supermarkets"), - ("Currentness", "marketplaces"), - ("Currentness", "parks"), - ("Currentness", "forests"), - ("Currentness", "fitness_centres"), - ("Currentness", "fire_stations"), - ("Currentness", "hospitals"), - ("Currentness", "local_food_shops"), - ("Currentness", "fast_food_restaurants"), - ("Currentness", "restaurants"), - ("Currentness", "supermarkets"), - ("Currentness", "convenience_stores"), - ("Currentness", "pubs_and_biergartens"), - ("Currentness", "alcohol_and_beverages"), - ("Currentness", "sweets_and_pasteries"), - ("Currentness", "railway_length"), - ("Currentness", "clc_arable_land_area"), - ("Currentness", "clc_permanent_crops_area"), - ("Currentness", "clc_pastures_area"), - ("Currentness", "clc_forest_area"), - ("Currentness", "clc_leaf_type"), - ("Currentness", "clc_shrub_area"), - ("Currentness", "clc_open_spaces_area"), - ("Currentness", "clc_wetland_area"), - ("Currentness", "clc_water_area"), - ("Currentness", "clc_waterway_len"), - ("PoiDensity", "poi"), - ("TagsRatio", "building_count"), - ("TagsRatio", "major_roads_length"), - ("TagsRatio", "jrc_health_count"), - ("TagsRatio", "jrc_education_count"), - ("TagsRatio", "jrc_road_length"), - ("TagsRatio", "jrc_airport_count"), - ("TagsRatio", "jrc_power_generation_plant_count"), - ("TagsRatio", "jrc_cultural_heritage_site_count"), - ("TagsRatio", "jrc_bridge_count"), - ("TagsRatio", "jrc_mass_gathering_sites_count"), - ("TagsRatio", "clc_leaf_type"), - ("Minimal", "minimal"), -) - ATTRIBUTION_TEXTS = MappingProxyType( { "OSM": "© OpenStreetMap contributors", @@ -195,17 +73,16 @@ class RasterDataset: ) -def load_metadata(module_name: str) -> Dict: - """Read metadata of all indicators or reports from YAML files. +def load_metadata(module_name: Literal["indicators", "reports"]) -> Dict: + """Load metadata of all indicators or reports from YAML files. - Those text files are located in the directory of each indicator/report. + The YAML files are located in the directory of each individual indicator or report. - Args: - module_name: Either indicators or reports. Returns: - A Dict with the class names of the indicators/reports - as keys and metadata as values. + A dictionary with the indicator or report keys as directory keys and the content + of the YAML file (metadata) as values. """ + # TODO: Is this check needed if Literal is used in func declaration? if module_name != "indicators" and module_name != "reports": raise ValueError("module name value can only be 'indicators' or 'reports'.") @@ -342,11 +219,18 @@ def get_attribution(data_keys: list) -> str: return "; ".join([str(v) for v in filtered.values()]) -def get_valid_layers(indcator_name: str) -> tuple: - """Get valid Indicator/Layer combination of an Indicator.""" - return tuple([tup[1] for tup in INDICATOR_LAYER if tup[0] == indcator_name]) +def get_valid_layers(indicator_name: str) -> list: + """Get valid Indicator/Layer combination of an indicator.""" + metadata = load_metadata("indicators") + layers = metadata[indicator_name]["layer-thresholds"].keys() + return [l for l in layers if l != "default"] # noqa: E741 -def get_valid_indicators(layer_key: str) -> tuple: - """Get valid Indicator/Layer combination of a Layer.""" - return tuple([tup[0] for tup in INDICATOR_LAYER if tup[1] == layer_key]) +def get_valid_indicators(layer_key: str) -> list: + """Get valid Indicator/Layer combination of a layer.""" + metadata = load_metadata("indicators") + valid_indicators = [] + for indicator_key, metadata_ in metadata.items(): + if layer_key in metadata_["layer-thresholds"].keys(): + valid_indicators.append(indicator_key) + return valid_indicators diff --git a/workers/ohsome_quality_analyst/indicators/building_completeness/indicator.py b/workers/ohsome_quality_analyst/indicators/building_completeness/indicator.py index 9177e63e0..e661cbd36 100644 --- a/workers/ohsome_quality_analyst/indicators/building_completeness/indicator.py +++ b/workers/ohsome_quality_analyst/indicators/building_completeness/indicator.py @@ -2,6 +2,7 @@ import os from io import StringIO from string import Template +from typing import Optional, Tuple import dateutil.parser import geojson @@ -58,10 +59,14 @@ def __init__( self, layer: Layer, feature: Feature, + thresholds: Optional[Tuple[float, float, float, float]] = None, ) -> None: + if thresholds is None: + thresholds = (0.2, 0.5, 0.8, 0.9) super().__init__( layer=layer, feature=feature, + thresholds=thresholds, ) self.model_name: str = "Random Forest Regressor" # Lists of elements per hexagonal cell diff --git a/workers/ohsome_quality_analyst/indicators/building_completeness/metadata.yaml b/workers/ohsome_quality_analyst/indicators/building_completeness/metadata.yaml index 9710d8e1e..7e708bdae 100644 --- a/workers/ohsome_quality_analyst/indicators/building_completeness/metadata.yaml +++ b/workers/ohsome_quality_analyst/indicators/building_completeness/metadata.yaml @@ -23,3 +23,6 @@ BuildingCompleteness: average of the ratios per hex-cell between the building area mapped in OSM and the predicted building area is $completeness_ratio %. The weight is the predicted building area. + layer-thresholds: + default: [0.2, 0.5, 0.8, 0.9] + building_area: null diff --git a/workers/ohsome_quality_analyst/indicators/currentness/indicator.py b/workers/ohsome_quality_analyst/indicators/currentness/indicator.py index d40500239..cda66daa5 100644 --- a/workers/ohsome_quality_analyst/indicators/currentness/indicator.py +++ b/workers/ohsome_quality_analyst/indicators/currentness/indicator.py @@ -1,6 +1,7 @@ import logging from io import StringIO from string import Template +from typing import Optional, Tuple import dateutil.parser import geojson @@ -33,12 +34,13 @@ def __init__( self, layer: Layer, feature: geojson.Feature, + thresholds: Optional[Tuple[float, float, float, float]] = None, ) -> None: super().__init__(layer=layer, feature=feature) - self.threshold_4 = 2 - self.threshold_3 = 3 - self.threshold_2 = 4 - self.threshold_1 = 8 + self.threshold_4 = self.thresholds[0] + self.threshold_3 = self.thresholds[1] + self.threshold_2 = self.thresholds[2] + self.threshold_1 = self.thresholds[3] self.element_count = None self.contributions_sum = None self.contributions_rel = {} # yearly interval diff --git a/workers/ohsome_quality_analyst/indicators/currentness/metadata.yaml b/workers/ohsome_quality_analyst/indicators/currentness/metadata.yaml index 3b827991e..0d19f022d 100644 --- a/workers/ohsome_quality_analyst/indicators/currentness/metadata.yaml +++ b/workers/ohsome_quality_analyst/indicators/currentness/metadata.yaml @@ -19,7 +19,28 @@ Currentness: an extrinsic comparison to identify if this means that data quality is bad or if there is just nothing to map here. result_description: | - In the last $threshold_green years $green % of the elements were edited the last time. - In the period from $threshold_yellow_start to $threshold_yellow_end years ago $yellow % of the elements were edited the last time. - The remaining $red % were last edited more than $threshold_red years ago. - The median currentness of the $elements features ($layer_name) is $median_years year(s). + Over 50% of the $elements features ($layer_name) were edited $years. + layer-thresholds: + default: [ 2, 3, 4, 8] + amenities: null + building_count: null + infrastructure_lines: null + jrc_airport_count: null + jrc_bridge_count: null + jrc_cultural_heritage_site_count: null + jrc_education_count: null + jrc_health_count: null + jrc_mass_gathering_sites_count: null + jrc_power_generation_plant_count: null + jrc_railway_count: null + jrc_road_count: null + jrc_water_treatment_plant_count: null + lulc: null + major_roads_count: null + mapaction_lakes_count: null + mapaction_major_roads_length: null + mapaction_rail_length: null + mapaction_rivers_length: null + mapaction_settlements_count: null + poi: null + diff --git a/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_buildings/indicator.py b/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_buildings/indicator.py index 2693048dd..e19c8e65a 100644 --- a/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_buildings/indicator.py +++ b/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_buildings/indicator.py @@ -1,6 +1,7 @@ import logging from io import StringIO from string import Template +from typing import Optional, Tuple import dateutil.parser import matplotlib.pyplot as plt @@ -18,8 +19,13 @@ class GhsPopComparisonBuildings(BaseIndicator): """Set number of features and population into perspective.""" - def __init__(self, layer: Layer, feature: Feature) -> None: - super().__init__(layer=layer, feature=feature) + def __init__( + self, + layer: Layer, + feature: Feature, + thresholds: Optional[Tuple[dict, dict, dict, dict]] = None, + ) -> None: + super().__init__(layer=layer, feature=feature, thresholds=thresholds) # Those attributes will be set during lifecycle of the object. self.pop_count = None self.area = None @@ -34,13 +40,13 @@ def green_threshold_function(self, pop_per_sqkm) -> float: # TODO: Add docstring # TODO: adjust threshold functions # more precise values? maybe as fraction of the threshold functions? - return 5.0 * np.sqrt(pop_per_sqkm) + return self.thresholds[2]["a"] * np.sqrt(pop_per_sqkm) def yellow_threshold_function(self, pop_per_sqkm) -> float: # TODO: Add docstring # TODO: adjust threshold functions # more precise values? maybe as fraction of the threshold functions? - return 0.75 * np.sqrt(pop_per_sqkm) + return self.thresholds[0]["a"] * np.sqrt(pop_per_sqkm) async def preprocess(self) -> None: raster = get_raster_dataset("GHS_POP_R2019A") diff --git a/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_buildings/metadata.yaml b/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_buildings/metadata.yaml index 36d689f8c..774b9df3e 100644 --- a/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_buildings/metadata.yaml +++ b/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_buildings/metadata.yaml @@ -23,4 +23,7 @@ GhsPopComparisonBuildings: $pop_count people living in an area of $area sqkm, which results in a population density $pop_count_per_sqkm of people per sqkm. - $feature_count_per_sqkm buildings per sqkm mapped. + $feature_count_per_sqkm buildings per sqkm mapped. + layer-thresholds: + default: [{ a: 0.75 }, null, { a: 5.0 }, null] + building_count: null diff --git a/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_roads/indicator.py b/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_roads/indicator.py index 7a208a42b..1be3e6d44 100644 --- a/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_roads/indicator.py +++ b/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_roads/indicator.py @@ -1,6 +1,7 @@ import logging from io import StringIO from string import Template +from typing import Optional, Tuple import dateutil.parser import matplotlib.pyplot as plt @@ -18,8 +19,13 @@ class GhsPopComparisonRoads(BaseIndicator): """Set number of features and population into perspective.""" - def __init__(self, layer: Layer, feature: Feature) -> None: - super().__init__(layer=layer, feature=feature) + def __init__( + self, + layer: Layer, + feature: Feature, + thresholds: Optional[Tuple[dict, dict, dict, dict]] = None, + ) -> None: + super().__init__(layer=layer, feature=feature, thresholds=thresholds) # Those attributes will be set during lifecycle of the object. self.pop_count = None self.area = None @@ -33,14 +39,14 @@ def attribution(cls) -> str: def green_threshold_function(self, pop_per_sqkm) -> float: """Return road density threshold for green label.""" if pop_per_sqkm < 5000: - return pop_per_sqkm / 500 + return pop_per_sqkm / self.thresholds[2]["a"] else: return 10 def yellow_threshold_function(self, pop_per_sqkm) -> float: """Return road density threshold for yellow label.""" if pop_per_sqkm < 5000: - return pop_per_sqkm / 1000 + return pop_per_sqkm / self.thresholds[0]["a"] else: return 5 diff --git a/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_roads/metadata.yaml b/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_roads/metadata.yaml index d1e7a4c5c..20af991d8 100644 --- a/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_roads/metadata.yaml +++ b/workers/ohsome_quality_analyst/indicators/ghs_pop_comparison_roads/metadata.yaml @@ -24,3 +24,7 @@ GhsPopComparisonRoads: $area sqkm, which results in a population density $pop_count_per_sqkm of people per sqkm. $feature_length_per_sqkm km of roads per sqkm mapped. + layer-thresholds: + default: [{ a: 1000 }, null, { a: 500 }, null] + jrc_road_length: null + major_roads_length: null \ No newline at end of file diff --git a/workers/ohsome_quality_analyst/indicators/mapping_saturation/indicator.py b/workers/ohsome_quality_analyst/indicators/mapping_saturation/indicator.py index 033afb839..bd1386c8f 100644 --- a/workers/ohsome_quality_analyst/indicators/mapping_saturation/indicator.py +++ b/workers/ohsome_quality_analyst/indicators/mapping_saturation/indicator.py @@ -1,7 +1,7 @@ import logging from io import StringIO from string import Template -from typing import List, Optional, Union +from typing import List, Optional, Tuple, Union import matplotlib.pyplot as plt import numpy as np @@ -40,6 +40,7 @@ def __init__( self, layer: Layer, feature: Feature, + thresholds: Optional[Tuple[float, float, float, float]] = None, time_range: str = "2008-01-01//P1M", ) -> None: super().__init__(layer=layer, feature=feature) diff --git a/workers/ohsome_quality_analyst/indicators/mapping_saturation/metadata.yaml b/workers/ohsome_quality_analyst/indicators/mapping_saturation/metadata.yaml index 1e1dfe09d..c57e67fef 100644 --- a/workers/ohsome_quality_analyst/indicators/mapping_saturation/metadata.yaml +++ b/workers/ohsome_quality_analyst/indicators/mapping_saturation/metadata.yaml @@ -15,3 +15,21 @@ MappingSaturation: Saturation could not be calculated. result_description: | The saturation of the last 3 years is $saturation%. + layer-thresholds: + default: [0.3, null, 0.97, null] + amenities: null + building_count: null + infrastructure_lines: null + jrc_education_count: null + jrc_health_count: null + jrc_mass_gathering_sites_count: null + jrc_railway_length: null + jrc_road_length: null + lulc: null + major_roads_length: null + mapaction_lakes_area: null + mapaction_major_roads_length: null + mapaction_rail_length: null + mapaction_rivers_length: null + mapaction_settlements_count: null + poi: null diff --git a/workers/ohsome_quality_analyst/indicators/minimal/indicator.py b/workers/ohsome_quality_analyst/indicators/minimal/indicator.py index 118e7617f..76be13691 100644 --- a/workers/ohsome_quality_analyst/indicators/minimal/indicator.py +++ b/workers/ohsome_quality_analyst/indicators/minimal/indicator.py @@ -1,5 +1,6 @@ """An Indicator for testing purposes.""" from string import Template +from typing import Optional, Tuple import dateutil.parser from geojson import Feature @@ -10,8 +11,13 @@ class Minimal(BaseIndicator): - def __init__(self, layer: Layer, feature: Feature) -> None: - super().__init__(layer=layer, feature=feature) + def __init__( + self, + layer: Layer, + feature: Feature, + thresholds: Optional[Tuple[float, float, float, float]] = None, + ) -> None: + super().__init__(layer=layer, feature=feature, thresholds=thresholds) self.count = 0 async def preprocess(self) -> None: diff --git a/workers/ohsome_quality_analyst/indicators/minimal/metadata.yaml b/workers/ohsome_quality_analyst/indicators/minimal/metadata.yaml index 0dfc4bd8e..503af7c40 100644 --- a/workers/ohsome_quality_analyst/indicators/minimal/metadata.yaml +++ b/workers/ohsome_quality_analyst/indicators/minimal/metadata.yaml @@ -14,3 +14,10 @@ Minimal: The quality level could not be calculated for this indicator. result_description: | Some description of the result. + layer-thresholds: + # layer-key: [thresholds] + # A threshold is a list of four values (floats) or function parameters (associative list). + # The first element is the threshold between the result class 1 (maps to the label 'red'). + # The layer key "None" signifies the default thresholds. + default: [0.3, null, 0.97, null] + minimal: null diff --git a/workers/ohsome_quality_analyst/indicators/poi_density/indicator.py b/workers/ohsome_quality_analyst/indicators/poi_density/indicator.py index eb99d02b2..6eac95f19 100644 --- a/workers/ohsome_quality_analyst/indicators/poi_density/indicator.py +++ b/workers/ohsome_quality_analyst/indicators/poi_density/indicator.py @@ -1,6 +1,7 @@ import logging from io import StringIO from string import Template +from typing import Optional, Tuple import dateutil.parser import matplotlib.pyplot as plt @@ -17,8 +18,13 @@ class PoiDensity(BaseIndicator): - def __init__(self, layer: Layer, feature: Feature) -> None: - super().__init__(layer=layer, feature=feature) + def __init__( + self, + layer: Layer, + feature: Feature, + thresholds: Optional[Tuple[float, float, float, float]] = None, + ) -> None: + super().__init__(layer=layer, feature=feature, thresholds=thresholds) self.threshold_yellow = 30 self.threshold_red = 10 self.area_sqkm = None diff --git a/workers/ohsome_quality_analyst/indicators/poi_density/metadata.yaml b/workers/ohsome_quality_analyst/indicators/poi_density/metadata.yaml index 86863da0d..fd67874c9 100644 --- a/workers/ohsome_quality_analyst/indicators/poi_density/metadata.yaml +++ b/workers/ohsome_quality_analyst/indicators/poi_density/metadata.yaml @@ -22,3 +22,6 @@ PoiDensity: The density of landmarks (points of reference, e.g. waterbodies, supermarkets, churches, bus stops) is $result features per sqkm. + layer-thresholds: + default: [10, null, null, 30] + poi: null diff --git a/workers/ohsome_quality_analyst/indicators/tags_ratio/indicator.py b/workers/ohsome_quality_analyst/indicators/tags_ratio/indicator.py index 984267f64..a80025f55 100644 --- a/workers/ohsome_quality_analyst/indicators/tags_ratio/indicator.py +++ b/workers/ohsome_quality_analyst/indicators/tags_ratio/indicator.py @@ -1,6 +1,7 @@ import logging from io import StringIO from string import Template +from typing import Optional, Tuple import dateutil.parser import matplotlib.patches as mpatches @@ -13,8 +14,13 @@ class TagsRatio(BaseIndicator): - def __init__(self, layer: Layer, feature: Feature) -> None: - super().__init__(layer=layer, feature=feature) + def __init__( + self, + layer: Layer, + feature: Feature, + thresholds: Optional[Tuple[float, float, float, float]] = None, + ) -> None: + super().__init__(layer=layer, feature=feature, thresholds=thresholds) self.threshold_yellow = 0.75 self.threshold_red = 0.25 self.count_all = None diff --git a/workers/ohsome_quality_analyst/indicators/tags_ratio/metadata.yaml b/workers/ohsome_quality_analyst/indicators/tags_ratio/metadata.yaml index 6e7f27ac6..76e6ad5a2 100644 --- a/workers/ohsome_quality_analyst/indicators/tags_ratio/metadata.yaml +++ b/workers/ohsome_quality_analyst/indicators/tags_ratio/metadata.yaml @@ -17,3 +17,15 @@ TagsRatio: result_description: | The ratio of the features (all: $all) compared to features with expected tags (matched: $matched) is $result. + layer-thresholds: + default: [ 0.25, null, 0.75, null] + building_count: null + jrc_airport_count: null + jrc_bridge_count: null + jrc_cultural_heritage_site_count: null + jrc_education_count: null + jrc_health_count: null + jrc_mass_gathering_sites_count: null + jrc_power_generation_plant_count: null + jrc_road_length: null + major_roads_length: null diff --git a/workers/ohsome_quality_analyst/oqt.py b/workers/ohsome_quality_analyst/oqt.py index 8bdb5e21e..acfd426b7 100644 --- a/workers/ohsome_quality_analyst/oqt.py +++ b/workers/ohsome_quality_analyst/oqt.py @@ -23,7 +23,6 @@ from ohsome_quality_analyst.base.report import BaseReport as Report from ohsome_quality_analyst.config import get_config_value from ohsome_quality_analyst.definitions import ( - INDICATOR_LAYER, get_layer_definition, get_valid_indicators, get_valid_layers, @@ -168,7 +167,9 @@ async def _( feature_id = parameters.feature_id feature = await db_client.get_feature_from_db(dataset, feature_id) indicator_class = name_to_class(class_type="indicator", name=name) - indicator_raw = indicator_class(layer=layer, feature=feature) + indicator_raw = indicator_class( + layer=layer, feature=feature, thresholds=parameters.thresholds + ) failure = False try: indicator = await db_client.load_indicator_results( @@ -183,6 +184,7 @@ async def _( IndicatorBpolys( name=name, layerKey=parameters.layer_key.value, + thresholds=parameters.thresholds, bpolys=feature, ) ) @@ -200,6 +202,7 @@ async def _( name = parameters.name.value layer: Layer = get_layer_definition(parameters.layer_key.value) feature = parameters.bpolys + thresholds = parameters.thresholds logging.info("Calculating Indicator for custom AOI ...") logging.info("Feature id: {0:4}".format(feature.get("id", 1))) @@ -207,7 +210,7 @@ async def _( logging.info("Layer name: {0:4}".format(layer.name)) indicator_class = name_to_class(class_type="indicator", name=name) - indicator = indicator_class(layer, feature) + indicator = indicator_class(layer, feature, thresholds) logging.info("Run preprocessing") await indicator.preprocess() @@ -362,7 +365,8 @@ async def create_all_indicators( elif indicator_name is not None and layer_key is not None: indicator_layer = [(indicator_name, layer_key)] else: - indicator_layer = INDICATOR_LAYER + # TODO + indicator_layer = "INDICATOR_LAYER" tasks: List[asyncio.Task] = [] fids = await db_client.get_feature_ids(dataset) diff --git a/workers/ohsome_quality_analyst/reports/minimal/metadata.yaml b/workers/ohsome_quality_analyst/reports/minimal/metadata.yaml index 5c32447d7..5977fa20d 100644 --- a/workers/ohsome_quality_analyst/reports/minimal/metadata.yaml +++ b/workers/ohsome_quality_analyst/reports/minimal/metadata.yaml @@ -2,7 +2,7 @@ Minimal: name: Minimal description: | - This report shows the quality for two indicators: + This report shows the quality for two indicators":" Mapping Saturation and Currentness. It's main function is to test the interactions between database, api and website. diff --git a/workers/tests/integrationtests/test_api.py b/workers/tests/integrationtests/test_api.py index 9095fc3f7..6302981af 100644 --- a/workers/tests/integrationtests/test_api.py +++ b/workers/tests/integrationtests/test_api.py @@ -50,13 +50,6 @@ def test_get_available_regions(self): for region in response_content["result"]: self.assertIsInstance(region, dict) - def test_list_indicator_layer_combinations(self): - url = "/indicator-layer-combinations" - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - self.assertIsInstance(response.json(), dict) - def test_list_indicators(self): url = "/indicators" response = self.client.get(url) diff --git a/workers/tests/integrationtests/test_api_indicator_geojson_io.py b/workers/tests/integrationtests/test_api_indicator_geojson_io.py index e7c0e4fb1..f57ea5dc9 100644 --- a/workers/tests/integrationtests/test_api_indicator_geojson_io.py +++ b/workers/tests/integrationtests/test_api_indicator_geojson_io.py @@ -187,6 +187,7 @@ def test_indicator_layer_data(self): "bpolys": self.feature, "layer": { "name": "foo", + "key": "foo", "description": "", "data": { "result": [ @@ -205,6 +206,7 @@ def test_indicator_layer_data_invalid(self): "bpolys": self.feature, "layer": { "name": "foo", + "key": "foo", "description": "", "data": {"result": [{"value": 1.0}]}, # Missing timestamp item }, diff --git a/workers/tests/integrationtests/test_cli.py b/workers/tests/integrationtests/test_cli.py index d23baeb61..fa36a14ba 100644 --- a/workers/tests/integrationtests/test_cli.py +++ b/workers/tests/integrationtests/test_cli.py @@ -126,14 +126,6 @@ def test_get_available_regions(self): self.assertEqual(result.exit_code, 0) self.assertTrue(result.output, str) - def test_get_indicator_layer_combination(self): - result = self.runner.invoke( - cli, - ["-q", "list-indicator-layer-combination"], - ) - self.assertEqual(result.exit_code, 0) - self.assertIsInstance(result.output, str) - def test_list_indicators(self): result = self.runner.invoke( cli, diff --git a/workers/tests/integrationtests/test_oqt.py b/workers/tests/integrationtests/test_oqt.py index 223474bff..5456a4542 100644 --- a/workers/tests/integrationtests/test_oqt.py +++ b/workers/tests/integrationtests/test_oqt.py @@ -159,6 +159,34 @@ def test_create_all_indicators(self): ) ) + @oqt_vcr.use_cassette() + def test_create_all_indicators_valid_layer(self): + with mock.patch( + "ohsome_quality_analyst.geodatabase.client.get_feature_ids", + new_callable=AsyncMock, + ) as get_feature_ids_mock: + get_feature_ids_mock.return_value = ["3", "12", "3", "12", "3", "12"] + asyncio.run( + oqt.create_all_indicators( + dataset="regions", + indicator_name="Minimal", + ) + ) + + @oqt_vcr.use_cassette() + def test_create_all_indicators_valid_indicators(self): + with mock.patch( + "ohsome_quality_analyst.geodatabase.client.get_feature_ids", + new_callable=AsyncMock, + ) as get_feature_ids_mock: + get_feature_ids_mock.return_value = ["3", "12", "3", "12", "3", "12"] + asyncio.run( + oqt.create_all_indicators( + dataset="regions", + layer_key="minimal", + ) + ) + def test_check_area_size(self): path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "fixtures", "europe.geojson" diff --git a/workers/tests/unittests/test_definitions.py b/workers/tests/unittests/test_definitions.py index 6f4afdc1f..5e848d673 100644 --- a/workers/tests/unittests/test_definitions.py +++ b/workers/tests/unittests/test_definitions.py @@ -75,17 +75,17 @@ def test_get_valid_indicators(self): indicators = definitions.get_valid_indicators("building_count") self.assertEqual( indicators, - ( - "GhsPopComparisonBuildings", + [ "MappingSaturation", + "GhsPopComparisonBuildings", "Currentness", "TagsRatio", - ), + ], ) def test_get_valid_layers(self): layers = definitions.get_valid_layers("Minimal") self.assertEqual( layers, - ("minimal",), + ["minimal"], ) diff --git a/workers/tests/unittests/test_load_metadata.py b/workers/tests/unittests/test_load_metadata.py index d5b947c84..b2c8ca129 100644 --- a/workers/tests/unittests/test_load_metadata.py +++ b/workers/tests/unittests/test_load_metadata.py @@ -19,6 +19,7 @@ def test_validate_indicator_schema(self): "undefined": str, }, "result_description": str, + "layer-thresholds": dict, } } )