Skip to content

Commit

Permalink
Merge pull request #739 from GIScience/building-comparision-fix-up
Browse files Browse the repository at this point in the history
building-comparison: fix checks and result description
  • Loading branch information
ConstantinNicolai authored Nov 14, 2023
2 parents ab46474 + e5941bf commit c3be2a3
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 39 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## Current Main

### Bug Fixes

- building-comparison: is calculated for coverage area only ([#739])
- building-comparison: result description now shows correct coverage percentage ([#739])

### New Features

- building-comparison: AOI is now clipped to the coverage area ([#739])

### Other Changes

- building-comparison: no quality estimation for areas with strong difference to reference data ([#739])
- test(db): add missing mock for getting coverage from database in tests ([#739])

[#739]: https://github.com/GIScience/ohsome-quality-api/pull/739


## Release 1.0.2

### Bug Fixes
Expand Down
13 changes: 13 additions & 0 deletions ohsome_quality_api/geodatabase/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from contextlib import asynccontextmanager

import asyncpg
import geojson
from asyncpg import Record
from geojson import Feature, FeatureCollection

Expand Down Expand Up @@ -98,3 +99,15 @@ async def get_eubucco_coverage_intersection_area(bpoly: Feature) -> list[Record]
geom = str(bpoly.geometry)
async with get_connection() as conn:
return await conn.fetch(query, geom)


async def get_eubucco_coverage_intersection(bpoly: Feature) -> Feature:
"""Get intersection geometry of AoI and coverage geometry."""
file_path = os.path.join(WORKING_DIR, "get_coverage_intersection.sql")
with open(file_path, "r") as file:
query = file.read()
geom = str(bpoly.geometry)
async with get_connection() as conn:
result = await conn.fetch(query, geom)
bpoly["geometry"] = geojson.loads(result[0]["geom"])
return bpoly
11 changes: 11 additions & 0 deletions ohsome_quality_api/geodatabase/get_coverage_intersection.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
WITH bpoly AS (
SELECT
ST_Setsrid (ST_GeomFromGeoJSON ($1), 4326) AS geom
)
SELECT
ST_AsGeoJSON (ST_Intersection (bpoly.geom, coverage.geom)) AS geom
FROM
bpoly,
eubucco_v0_1_coverage_simple coverage
WHERE
ST_Intersects (bpoly.geom, coverage.geom)
90 changes: 55 additions & 35 deletions ohsome_quality_api/indicators/building_comparison/indicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def __init__(
# TODO: Evaluate thresholds
self.th_high = 0.85 # Above or equal to this value label should be green
self.th_low = 0.50 # Above or equal to this value label should be yellow
self.above_one_th = 1.30

@classmethod
async def coverage(cls) -> Polygon | MultiPolygon:
Expand All @@ -49,44 +50,56 @@ async def preprocess(self) -> None:
else:
self.coverage["EUBUCCO"] = None
return
if not self.check_major_edge_cases():
self.feature = await db_client.get_eubucco_coverage_intersection(
self.feature
)
db_query_result = await db_client.get_building_area(self.feature)
raw = db_query_result[0]["area"] or 0
self.area_references["EUBUCCO"] = raw / (1000 * 1000)

db_query_result = await db_client.get_building_area(self.feature)
raw = db_query_result[0]["area"] or 0
self.area_references["EUBUCCO"] = raw / (1000 * 1000)

osm_query_result = await ohsome_client.query(
self.topic,
self.feature,
)
raw = osm_query_result["result"][0]["value"] or 0 # if None
self.area_osm = raw / (1000 * 1000)
self.result.timestamp_osm = parser.isoparse(
osm_query_result["result"][0]["timestamp"]
)
osm_query_result = await ohsome_client.query(
self.topic,
self.feature,
)
raw = osm_query_result["result"][0]["value"] or 0 # if None
self.area_osm = raw / (1000 * 1000)
self.result.timestamp_osm = parser.isoparse(
osm_query_result["result"][0]["timestamp"]
)

def calculate(self) -> None:
# TODO: put checks into check_corner_cases. Let result be undefined.

major_edge_case_description = self.check_major_edge_cases()
if major_edge_case_description:
self.result.description = major_edge_case_description
if not self.result.description == "":
return
elif self.check_minor_edge_cases():
if self.check_minor_edge_cases():
self.result.description = self.check_minor_edge_cases()
return
else:
self.result.description = ""

self.result.value = float(
mean([self.area_osm / v for v in self.area_references.values()])
)
if all(v == 0 for v in self.area_references.values()):
self.result.description += "Warning: No reference data in this area. "
pass
else:
self.result.value = float(
mean(
[self.area_osm / v for v in self.area_references.values() if v != 0]
)
)

if self.result.value >= self.th_high:
if self.result.value is None:
return
elif self.above_one_th >= self.result.value >= self.th_high:
self.result.class_ = 5
elif self.result.value >= self.th_low:
elif self.th_high > self.result.value >= self.th_low:
self.result.class_ = 3
else:
elif self.th_low > self.result.value >= 0:
self.result.class_ = 1
elif self.result.value > self.above_one_th:
self.result.description += (
"Warning: No quality estimation made. "
"OSM and reference data differ. Reference data is likely outdated. "
)

template = Template(self.metadata.result_description)
self.result.description += template.substitute(
Expand Down Expand Up @@ -127,27 +140,34 @@ def create_figure(self) -> None:
raw["layout"].pop("template") # remove boilerplate
self.result.figure = raw

def check_major_edge_cases(self) -> str:
def check_major_edge_cases(self) -> bool:
coverage = self.coverage["EUBUCCO"]
# TODO: generalize function
if coverage is None or coverage == 0.00:
return "Reference dataset does not cover area-of-interest."
elif coverage < 0.50:
return (
"Only {:.2f}% of the area-of-interest is covered ".format(coverage)
self.result.description = (
"Reference dataset does not cover area-of-interest."
)
return True
elif coverage < 0.10:
self.result.description = (
"Only {:.2f}% of the area-of-interest is covered ".format(
coverage * 100
)
+ "by the reference dataset (EUBUCCO). "
+ "No quality estimation is possible."
)
return True
else:
return ""
self.result.description = ""
return False

def check_minor_edge_cases(self) -> str:
coverage = self.coverage["EUBUCCO"]
if coverage < 0.85:
if coverage < 0.95:
return (
"Only {:.2f}% of the area-of-interest is covered ".format(coverage)
+ "by the reference dataset (EUBUCCO). "
+ "No quality estimation is possible."
"Warning: Reference data does not cover the whole input geometry. "
+ f"Input geometry is clipped to the coverage. Results is calculated"
f" for {coverage}% of the input geometry."
)
else:
return ""
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,61 @@ interactions:
- accept-encoding
http_version: HTTP/1.1
status_code: 200
- request:
body: filter=building%3D%2A+and+building%21%3Dno+and+geometry%3Apolygon&bpolys=%7B%22type%22%3A+%22FeatureCollection%22%2C+%22features%22%3A+%5B%7B%22type%22%3A+%22Feature%22%2C+%22geometry%22%3A+%7B%22type%22%3A+%22Polygon%22%2C+%22coordinates%22%3A+%5B%5B%5B13.369514%2C+52.498879%5D%2C+%5B13.373609%2C+52.504164%5D%2C+%5B13.37498%2C+52.503376%5D%2C+%5B13.37765%2C+52.507966%5D%2C+%5B13.39923%2C+52.508077%5D%2C+%5B13.400228%2C+52.509382%5D%2C+%5B13.40803%2C+52.506183%5D%2C+%5B13.409969%2C+52.506928%5D%2C+%5B13.414072%2C+52.504037%5D%2C+%5B13.4272%2C+52.505667%5D%2C+%5B13.429402%2C+52.508563%5D%2C+%5B13.42278%2C+52.512234%5D%2C+%5B13.425927%2C+52.518393%5D%2C+%5B13.429188%2C+52.521203%5D%2C+%5B13.419753%2C+52.525546%5D%2C+%5B13.423642%2C+52.527915%5D%2C+%5B13.438748%2C+52.528778%5D%2C+%5B13.442277%2C+52.531026%5D%2C+%5B13.447168%2C+52.526408%5D%2C+%5B13.452183%2C+52.527796%5D%2C+%5B13.456157%2C+52.522458%5D%2C+%5B13.45529%2C+52.521272%5D%2C+%5B13.462695%2C+52.519928%5D%2C+%5B13.47239%2C+52.520571%5D%2C+%5B13.477729%2C+52.51472%5D%2C+%5B13.475888%2C+52.514863%5D%2C+%5B13.476266%2C+52.510439%5D%2C+%5B13.471164%2C+52.505138%5D%2C+%5B13.468573%2C+52.499657%5D%2C+%5B13.473076%2C+52.499003%5D%2C+%5B13.491443%2C+52.488283%5D%2C+%5B13.482954%2C+52.48605%5D%2C+%5B13.481678%2C+52.487638%5D%2C+%5B13.478628%2C+52.487033%5D%2C+%5B13.476147%2C+52.489944%5D%2C+%5B13.464279%2C+52.493719%5D%2C+%5B13.463355%2C+52.495486%5D%2C+%5B13.45293%2C+52.497707%5D%2C+%5B13.445549%2C+52.494856%5D%2C+%5B13.439262%2C+52.48961%5D%2C+%5B13.4204%2C+52.495864%5D%2C+%5B13.42535%2C+52.488182%5D%2C+%5B13.423705%2C+52.486384%5D%2C+%5B13.407887%2C+52.48886%5D%2C+%5B13.406346%2C+52.482792%5D%2C+%5B13.394613%2C+52.484022%5D%2C+%5B13.394261%2C+52.485775%5D%2C+%5B13.371607%2C+52.484979%5D%2C+%5B13.374019%2C+52.485167%5D%2C+%5B13.373518%2C+52.48797%5D%2C+%5B13.376562%2C+52.49167%5D%2C+%5B13.368229%2C+52.493336%5D%2C+%5B13.369514%2C+52.498879%5D%5D%5D%7D%2C+%22properties%22%3A+%7B%22osm_id%22%3A+-55764%2C+%22boundary%22%3A+%22administrative%22%2C+%22admin_level%22%3A+9%2C+%22parents%22%3A+%22-62422%2C-51477%22%2C+%22name%22%3A+%22Friedrichshain-Kreuzberg%22%2C+%22local_name%22%3A+%22Friedrichshain-Kreuzberg%22%2C+%22name_en%22%3A+null%7D%7D%5D%7D
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
connection:
- keep-alive
content-length:
- '2231'
content-type:
- application/x-www-form-urlencoded
host:
- api.ohsome.org
user-agent:
- ohsome-quality-api/1.0.2
method: POST
uri: https://api.ohsome.org/v1/elements/area
response:
content: "{\n \"attribution\" : {\n \"url\" : \"https://ohsome.org/copyrights\",\n
\ \"text\" : \"\xA9 OpenStreetMap contributors\"\n },\n \"apiVersion\" :
\"1.10.1\",\n \"result\" : [ {\n \"timestamp\" : \"2023-10-22T20:00:00Z\",\n
\ \"value\" : 5026307.68\n } ]\n}"
headers:
access-control-allow-credentials:
- 'true'
access-control-allow-headers:
- Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization
access-control-allow-methods:
- POST, GET
access-control-allow-origin:
- '*'
access-control-max-age:
- '3600'
cache-control:
- no-cache, no-store, must-revalidate
connection:
- Keep-Alive
content-encoding:
- gzip
content-type:
- application/json
date:
- Mon, 13 Nov 2023 11:14:11 GMT
keep-alive:
- timeout=5, max=100
server:
- Apache
strict-transport-security:
- max-age=63072000; includeSubdomains;
transfer-encoding:
- chunked
vary:
- accept-encoding
http_version: HTTP/1.1
status_code: 200
version: 1
Loading

0 comments on commit c3be2a3

Please sign in to comment.