Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Indicator data input #245

Merged
merged 12 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
- Update `poi` layer based on ([`openpoiservice`]) [#246])
- Remove `ideal_vgi_poi` layer in favor of new `poi` layer ([#246])


### New Features

- Add new parameter `layer` containing `name`, `description` and `data` fields to the API endpoint `indicator`. Only available for POST requests. This enables to compute indicators for given data. ([#245])


### Other Changes

- Use ([`rasterstats`]) to provide access to third-party raster datasets stored on disk ([#227])
Expand All @@ -23,6 +29,7 @@
[#227]: https://github.com/GIScience/ohsome-quality-analyst/pull/227
[#239]: https://github.com/GIScience/ohsome-quality-analyst/pull/239
[#242]: https://github.com/GIScience/ohsome-quality-analyst/pull/242
[#245]: https://github.com/GIScience/ohsome-quality-analyst/pull/245
[#246]: https://github.com/GIScience/ohsome-quality-analyst/pull/246
[#254]: https://github.com/GIScience/ohsome-quality-analyst/pull/254
[#266]: https://github.com/GIScience/ohsome-quality-analyst/pull/266
Expand Down
3 changes: 3 additions & 0 deletions sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ sonar.plsql.file.suffixes=""
# exclude non-API worker files from coverage report
sonar.coverage.exclusions=database/**,website/**,workers/scripts/**,workers/tests/**

# the fixtures file only contains values, no code that can be duplicated
sonar.cpd.exclusions=workers/tests/unittests/mapping_saturation/fixtures.py

sonar.issue.ignore.multicriteria=e1
# S117: local variable and function parameter names should comply with a naming convention
# Ignore for math formula parameter
Expand Down
6 changes: 5 additions & 1 deletion workers/ohsome_quality_analyst/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
INDICATOR_EXAMPLES,
REPORT_EXAMPLES,
IndicatorBpolys,
IndicatorData,
IndicatorDatabase,
ReportBpolys,
ReportDatabase,
Expand All @@ -41,6 +42,7 @@
get_report_names,
)
from ohsome_quality_analyst.utils.exceptions import (
LayerDataSchemaError,
OhsomeApiError,
RasterDatasetNotFoundError,
RasterDatasetUndefinedError,
Expand Down Expand Up @@ -108,13 +110,15 @@ async def validation_exception_handler(

@app.exception_handler(OhsomeApiError)
@app.exception_handler(SizeRestrictionError)
@app.exception_handler(LayerDataSchemaError)
async def oqt_exception_handler(
request: Request,
exception: Union[
OhsomeApiError,
SizeRestrictionError,
RasterDatasetNotFoundError,
RasterDatasetUndefinedError,
LayerDataSchemaError,
],
):
"""Exception handler for custom OQT exceptions."""
Expand Down Expand Up @@ -149,7 +153,7 @@ async def get_indicator(parameters=Depends(IndicatorDatabase)):

@app.post("/indicator")
async def post_indicator(
parameters: Union[IndicatorBpolys, IndicatorDatabase] = Body(
parameters: Union[IndicatorBpolys, IndicatorDatabase, IndicatorData] = Body(
...,
examples=INDICATOR_EXAMPLES,
)
Expand Down
132 changes: 111 additions & 21 deletions workers/ohsome_quality_analyst/api/request_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from geojson import Feature, FeatureCollection
from pydantic import BaseModel

from ohsome_quality_analyst.base.layer import LayerData
from ohsome_quality_analyst.utils.definitions import (
INDICATOR_LAYER,
get_dataset_names_api,
Expand All @@ -35,26 +36,9 @@ class BaseIndicator(BaseModel):
name: IndicatorEnum = pydantic.Field(
..., title="Indicator Name", example="GhsPopComparisonBuildings"
)
layer_name: LayerEnum = pydantic.Field(
..., title="Layer Name", example="building_count"
)
include_svg: bool = False
include_html: bool = False

@pydantic.root_validator
@classmethod
def validate_indicator_layer(cls, values):
try:
indicator_layer = (values["name"].value, values["layer_name"].value)
except KeyError:
raise ValueError("An issue with the layer or indicator name occurred.")
if indicator_layer not in INDICATOR_LAYER:
raise ValueError(
"Indicator layer combination is invalid: " + str(indicator_layer)
)
else:
return values

class Config:
"""Pydantic config class."""

Expand All @@ -76,6 +60,23 @@ class Config:
extra = "forbid"


class BaseLayerName(BaseModel):
"""Model for the `layer_name` parameter."""

layer_name: LayerEnum = pydantic.Field(
..., title="Layer Name", example="building_count"
)


class BaseLayerData(BaseModel):
"""Model for the parameter `layer`.

The Layer consists of name, description and data.
"""

layer: LayerData


class BaseBpolys(BaseModel):
"""Model for the `bpolys` parameter."""

Expand Down Expand Up @@ -118,12 +119,49 @@ class BaseDatabase(BaseModel):
fid_field: Optional[FidFieldEnum] = None


class IndicatorBpolys(BaseIndicator, BaseBpolys):
pass
class IndicatorBpolys(BaseIndicator, BaseLayerName, BaseBpolys):
@pydantic.root_validator
@classmethod
def validate_indicator_layer(cls, values):
try:
indicator_layer = (values["name"].value, values["layer_name"].value)
except KeyError:
raise ValueError("An issue with the layer or indicator name occurred.")
if indicator_layer not in INDICATOR_LAYER:
raise ValueError(
"Indicator layer combination is invalid: " + str(indicator_layer)
)
else:
return values


class IndicatorDatabase(BaseIndicator, BaseDatabase):
pass
class IndicatorDatabase(BaseIndicator, BaseLayerName, BaseDatabase):
@pydantic.root_validator
@classmethod
def validate_indicator_layer(cls, values):
try:
indicator_layer = (values["name"].value, values["layer_name"].value)
except KeyError:
raise ValueError("An issue with the layer or indicator name occurred.")
if indicator_layer not in INDICATOR_LAYER:
raise ValueError(
"Indicator layer combination is invalid: " + str(indicator_layer)
)
else:
return values


class IndicatorData(BaseIndicator, BaseLayerData, BaseBpolys):
@pydantic.validator("name")
@classmethod
def validate_indicator_name(cls, name):
if name.value != "MappingSaturation":
raise ValueError(
"Computing an Indicator for a Layer with data attached is only "
+ "supported for the Mapping Saturation Indicator."
)
else:
return name


class ReportBpolys(BaseReport, BaseBpolys):
Expand Down Expand Up @@ -178,6 +216,58 @@ class ReportDatabase(BaseReport, BaseDatabase):
"featureId": 3,
},
},
"Custom AOI and custom Layer": {
"summary": (
"Request an Indicator for a custom AOI (`bpolys`) and a custom Layer "
"(`layer`)."
),
"description": (
"The Layer must have a name and description. The data associated with this "
"Layer must be provided as well. "
),
"value": {
"name": "MappingSaturation",
"bpolys": {
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[8.674092292785645, 49.40427147224242],
[8.695850372314453, 49.40427147224242],
[8.695850372314453, 49.415552187316095],
[8.674092292785645, 49.415552187316095],
[8.674092292785645, 49.40427147224242],
]
],
},
},
"layer": {
"name": "My layer name",
"description": "My layer description",
"data": {
"result": [
{"value": v, "timestamp": "2020-03-20T01:30:08.180856"}
# fmt: off
for v in [
1.0, 1.0, 4.0, 44.0, 114.0, 226.0, 241.0, 252.0, 272.0,
275.0, 279.0, 298.0, 306.0, 307.0, 426.0, 472.0, 482.0,
498.0, 502.0, 555.0, 557.0, 607.0, 610.0, 631.0, 637.0,
655.0, 695.0, 1011.0, 5669.0, 7217.0, 8579.0, 8755.0,
8990.0, 9043.0, 9288.0, 9412.0, 9670.0, 10416.0, 10840.0,
12925.0, 13698.0, 14369.0, 15360.0, 15743.0, 16052.0,
16459.0, 21903.0, 22655.0, 22860.0, 23022.0, 24809.0,
24960.0, 26690.0, 26760.0, 26931.0, 26920.0, 28372.0,
28837.0, 28900.0, 28945.0, 29003.0, 29047.0, 29091.0,
29270.0, 29267.0, 29287.0, 29348.0, 29378.0, 29406.0,
29624.0, 29634.0, 29631.0, 29806.0,
]
# fmt: on
]
},
},
},
},
}

REPORT_EXAMPLES = {
Expand Down
40 changes: 9 additions & 31 deletions workers/ohsome_quality_analyst/base/indicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,39 +16,21 @@
from geojson import Feature
from jinja2 import Environment, FileSystemLoader

from ohsome_quality_analyst.utils.definitions import (
get_attribution,
get_layer_definition,
get_metadata,
)
from ohsome_quality_analyst.base.layer import BaseLayer as Layer
from ohsome_quality_analyst.utils.definitions import get_attribution, get_metadata
from ohsome_quality_analyst.utils.helper import flatten_dict, json_serialize


@dataclass
class Metadata:
"""Metadata of an indicator as defined in the metadata.yaml file"""
"""Metadata of an indicator as defined in the metadata.yaml file."""

name: str
description: str
label_description: Dict
result_description: str


@dataclass
class LayerDefinition:
"""Definitions of a layer as defined in the layer_definition.yaml file.

The definition consist of the ohsome API Parameter needed to create the layer.
"""

name: str
description: str
endpoint: str
filter: str
ratio_filter: Optional[str] = None
source: Optional[str] = None


@dataclass
class Result:
"""The result of the Indicator.
Expand Down Expand Up @@ -77,18 +59,14 @@ class BaseIndicator(metaclass=ABCMeta):

def __init__(
self,
layer_name: str,
layer: Layer,
feature: Feature,
) -> None:
self.feature = feature

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__)
self.metadata: Metadata = from_dict(data_class=Metadata, data=metadata)

self.layer: LayerDefinition = from_dict(
data_class=LayerDefinition, data=get_layer_definition(layer_name)
)
self.result: Result = Result(
# UTC datetime object representing the current time.
timestamp_oqt=datetime.now(timezone.utc),
Expand All @@ -101,7 +79,7 @@ def __init__(
)

def as_feature(self, flatten: bool = False) -> Feature:
"""Returns a GeoJSON Feature object.
"""Return a GeoJSON Feature object.

The properties of the Feature contains the attributes of the indicator.
The geometry (and properties) of the input GeoJSON object is preserved.
Expand Down Expand Up @@ -155,7 +133,7 @@ def data(self) -> dict:

@classmethod
def attribution(cls) -> str:
"""Data attribution as text.
"""Return data attribution as text.

Defaults to OpenStreetMap attribution.

Expand Down Expand Up @@ -190,7 +168,7 @@ def create_figure(self) -> None:
pass

def _get_default_figure(self) -> str:
"""Return a SVG as default figure for indicators"""
"""Return a SVG as default figure for indicators."""
px = 1 / plt.rcParams["figure.dpi"] # Pixel in inches
figsize = (400 * px, 400 * px)
plt.figure(figsize=figsize)
Expand Down
30 changes: 30 additions & 0 deletions workers/ohsome_quality_analyst/base/layer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from dataclasses import dataclass
from typing import Optional


@dataclass
class BaseLayer:
name: str
description: str


@dataclass
class LayerDefinition(BaseLayer):
"""Layer class including the ohsome API parameters needed to retrieve the data.

Note:
The layer name, description and ohsome API parameters are defined in the
`layer_definitions.yaml` file.
"""

endpoint: str
filter_: str
source: Optional[str] = None
ratio_filter: Optional[str] = None


@dataclass
class LayerData(BaseLayer):
"""Layer class including the data associated with the layer."""

data: dict
Loading