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

building-comparison: fix checks and result description #739

Merged
merged 8 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# 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])
matthiasschaub marked this conversation as resolved.
Show resolved Hide resolved


matthiasschaub marked this conversation as resolved.
Show resolved Hide resolved
## 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)
92 changes: 57 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,58 @@ 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()):
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:
self.result.description += (
"\n" + self.metadata.label_description[self.result.label]
)
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 +142,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