Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2ad5d7f

Browse files
Gigaszimatthiasschaub
authored andcommittedJan 20, 2025··
feat(attribute-completeness): use sql query
Optionally use SQL based query instead of ohsome API query given a feature flag.
1 parent 387c167 commit 2ad5d7f

File tree

6 files changed

+107
-13
lines changed

6 files changed

+107
-13
lines changed
 

‎ohsome_quality_api/indicators/attribute_completeness/indicator.py

+56-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import json
12
import logging
3+
import os
24
from string import Template
35

4-
import dateutil.parser
6+
import dateutil
57
import plotly.graph_objects as go
68
from geojson import Feature
79

@@ -12,6 +14,10 @@
1214
from ohsome_quality_api.indicators.base import BaseIndicator
1315
from ohsome_quality_api.ohsome import client as ohsome_client
1416
from ohsome_quality_api.topics.models import BaseTopic as Topic
17+
from ohsome_quality_api.trino import client as trino_client
18+
from ohsome_quality_api.utils.helper_geo import get_bounding_box
19+
20+
WORKING_DIR = os.path.dirname(os.path.abspath(__file__))
1521

1622

1723
class AttributeCompleteness(BaseIndicator):
@@ -46,8 +52,10 @@ def __init__(
4652
attribute_keys: list[str] | None = None,
4753
attribute_filter: str | None = None,
4854
attribute_title: str | None = None,
55+
feature_flag_sql: bool = False, # Feature flag to use SQL instead of ohsome API queries
4956
) -> None:
5057
super().__init__(topic=topic, feature=feature)
58+
self.feature_flag_sql = feature_flag_sql
5159
self.threshold_yellow = 0.75
5260
self.threshold_red = 0.25
5361
self.attribute_keys = attribute_keys
@@ -56,7 +64,9 @@ def __init__(
5664
self.absolute_value_1 = None
5765
self.absolute_value_2 = None
5866
self.description = None
59-
if self.attribute_keys:
67+
if self.feature_flag_sql:
68+
self.attribute_filter = attribute_filter
69+
elif self.attribute_keys:
6070
self.attribute_filter = build_attribute_filter(
6171
self.attribute_keys,
6272
self.topic.key,
@@ -74,17 +84,50 @@ def __init__(
7484
)
7585

7686
async def preprocess(self) -> None:
77-
# Get attribute filter
78-
response = await ohsome_client.query(
79-
self.topic,
80-
self.feature,
81-
attribute_filter=self.attribute_filter,
82-
)
83-
timestamp = response["ratioResult"][0]["timestamp"]
84-
self.result.timestamp_osm = dateutil.parser.isoparse(timestamp)
85-
self.result.value = response["ratioResult"][0]["ratio"]
86-
self.absolute_value_1 = response["ratioResult"][0]["value"]
87-
self.absolute_value_2 = response["ratioResult"][0]["value2"]
87+
if self.feature_flag_sql:
88+
filter = self.topic.sql_filter
89+
file_path = os.path.join(WORKING_DIR, "query.sql")
90+
with open(file_path, "r") as file:
91+
template = file.read()
92+
93+
bounding_box = get_bounding_box(self.feature)
94+
geometry = json.dumps(self.feature["geometry"])
95+
96+
sql = template.format(
97+
bounding_box=bounding_box,
98+
geometry=geometry,
99+
filter=filter,
100+
)
101+
query = await trino_client.query(sql)
102+
results = await trino_client.fetch(query)
103+
# TODO: Check for None
104+
self.absolute_value_1 = results[0][0]
105+
106+
filter = self.topic.sql_filter + " AND " + self.attribute_filter
107+
sql = template.format(
108+
bounding_box=bounding_box,
109+
geometry=geometry,
110+
filter=filter,
111+
)
112+
query = await trino_client.query(sql)
113+
results = await trino_client.fetch(query)
114+
self.absolute_value_2 = results[0][0]
115+
116+
# timestamp = response["ratioResult"][0]["timestamp"]
117+
# self.result.timestamp_osm = dateutil.parser.isoparse(timestamp)
118+
self.result.value = self.absolute_value_2 / self.absolute_value_1
119+
else:
120+
# Get attribute filter
121+
response = await ohsome_client.query(
122+
self.topic,
123+
self.feature,
124+
attribute_filter=self.attribute_filter,
125+
)
126+
timestamp = response["ratioResult"][0]["timestamp"]
127+
self.result.timestamp_osm = dateutil.parser.isoparse(timestamp)
128+
self.result.value = response["ratioResult"][0]["ratio"]
129+
self.absolute_value_1 = response["ratioResult"][0]["value"]
130+
self.absolute_value_2 = response["ratioResult"][0]["value2"]
88131

89132
def calculate(self) -> None:
90133
# result (ratio) can be NaN if no features matching filter1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
WITH bpoly AS (
2+
SELECT
3+
to_geometry (from_geojson_geometry ('{geometry}')) AS geometry
4+
)
5+
SELECT
6+
Sum(
7+
CASE WHEN ST_Within (ST_GeometryFromText (contributions.geometry), bpoly.geometry) THEN
8+
length
9+
ELSE
10+
Cast(ST_Length (ST_Intersection (ST_GeometryFromText (contributions.geometry), bpoly.geometry)) AS integer)
11+
END) AS length
12+
FROM
13+
bpoly,
14+
sotm2024_iceberg.geo_sort.contributions AS contributions
15+
WHERE
16+
status = 'latest'
17+
AND ST_Intersects (bpoly.geometry, ST_GeometryFromText (contributions.geometry))
18+
AND {filter}
19+
AND (bbox.xmax <= {bounding_box.lon_max} AND bbox.xmin >= {bounding_box.lon_min})
20+
AND (bbox.ymax <= {bounding_box.lat_max} AND bbox.ymin >= {bounding_box.lat_min})

‎ohsome_quality_api/topics/models.py

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class TopicDefinition(BaseTopic):
3636
projects: list[ProjectEnum]
3737
source: str | None = None
3838
ratio_filter: str | None = None
39+
sql_filter: str | None = None
3940

4041

4142
class TopicData(BaseTopic):

‎ohsome_quality_api/topics/presets.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ car-roads:
6363
aggregation_type: length
6464
filter: >-
6565
highway in (motorway, trunk, primary, secondary, tertiary, residential, service, living_street, trunk_link, motorway_link, primary_link, secondary_link, tertiary_link) and type:way
66+
sql_filter: >-
67+
osm_type = 'way'
68+
AND element_at (contributions.tags, 'highway') IS NOT NULL
69+
AND contributions.tags['highway'] IN ('motorway', 'trunk',
70+
'motorway_link', 'trunk_link', 'primary', 'primary_link', 'secondary',
71+
'secondary_link', 'tertiary', 'tertiary_link', 'unclassified',
72+
'residential')
6673
indicators:
6774
- mapping-saturation
6875
- attribute-completeness

‎tests/conftest.py

+5
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ def topic_building_count(topic_key_building_count) -> TopicDefinition:
5858
return get_topic_preset(topic_key_building_count)
5959

6060

61+
@pytest.fixture(scope="class")
62+
def topic_car_roads() -> TopicDefinition:
63+
return get_topic_preset("car-roads")
64+
65+
6166
@pytest.fixture(scope="class")
6267
def topic_building_area() -> TopicDefinition:
6368
return get_topic_preset("building-area")

‎tests/integrationtests/indicators/test_attribute_completeness.py

+18
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,24 @@ def test_create_figure(self, indicator):
177177
pgo.Figure(indicator.result.figure) # test for valid Plotly figure
178178

179179

180+
class TestFeatureFlagSql:
181+
# TODO: use VCR cassette
182+
def test_preprocess_attribute_sql_filter(
183+
self,
184+
topic_car_roads,
185+
feature_germany_heidelberg,
186+
):
187+
attribute_filter = "element_at (contributions.tags, 'name') IS NOT NULL"
188+
indicator = AttributeCompleteness(
189+
topic_car_roads,
190+
feature_germany_heidelberg,
191+
attribute_filter=attribute_filter,
192+
feature_flag_sql=True,
193+
)
194+
asyncio.run(indicator.preprocess())
195+
assert indicator.result.value is not None
196+
197+
180198
def test_create_description_attribute_keys_single():
181199
indicator = AttributeCompleteness(
182200
get_topic_fixture("building-count"),

0 commit comments

Comments
 (0)
Please sign in to comment.