Skip to content
Snippets Groups Projects
Commit 4ebb5bef authored by Laurens Oostwegel's avatar Laurens Oostwegel
Browse files

Add height from the OpenStreetMap height-related tags

parent c3875d3c
No related branches found
No related tags found
1 merge request!13Resolve "Add height from OSM `height` and `min_height` tags"
Pipeline #75341 passed
...@@ -40,16 +40,14 @@ class HeightAndFloorspaceRule: ...@@ -40,16 +40,14 @@ class HeightAndFloorspaceRule:
# If they are present, the floorspace of a building can be calculated. OpenStreetMap # If they are present, the floorspace of a building can be calculated. OpenStreetMap
# buildings can also contain a `height` tag but this tag is only parsed without the # buildings can also contain a `height` tag but this tag is only parsed without the
# floorspace being estimated. # floorspace being estimated.
try: stories, floorspace = self.get_stories_and_floorspace_from_osm(tags, area)
stories, floorspace = self.get_stories_and_floorspace_from_osm(tags, area)
# OSM can have unexpected data types and unexpected content in their tagging scheme,
# therefore we do not raise an exception in case of a ValueError, but instead ignore
# the values.
except ValueError:
stories, floorspace = None, None
if stories: if stories:
gem_taxonomy_height.append(stories) gem_taxonomy_height.append(stories)
height = self.get_height_from_osm(tags)
if height:
gem_taxonomy_height.append(height)
# If any of the number-of-stories and height tags are found in OpenStreetMap, they are # If any of the number-of-stories and height tags are found in OpenStreetMap, they are
# returned. # returned.
if len(gem_taxonomy_height) > 0: if len(gem_taxonomy_height) > 0:
...@@ -130,6 +128,36 @@ class HeightAndFloorspaceRule: ...@@ -130,6 +128,36 @@ class HeightAndFloorspaceRule:
return "+".join(story_tags), floorspace return "+".join(story_tags), floorspace
def get_height_from_osm(self, tags: dict):
"""
Get the height of a building, based on the `height` and `min_height` tags. The
information about parsing the tags comes from
https://wiki.openstreetmap.org/wiki/Key:building:levels.
Args:
tags (dict):
Building tags, such as the building levels or building type.
Returns:
A string formatted according to the `height` tag in the GEM Taxonomy.
"""
# Parse the height tag from OpenStreetMap.
height = self.tag_to_float(tags, "height")
if not height:
return None
# Check if there is a `min_height` tag.
min_height = self.tag_to_float(tags, "min_height")
if min_height:
height -= min_height
# Check if the height is higher than 0.
if height <= 0:
return None
return f"HHT:{height:.2f}"
def tag_to_float(self, tags: dict, tag_key: str): def tag_to_float(self, tags: dict, tag_key: str):
""" """
Try to parse a tag as a floating point value. If the value cannot be parsed, return Try to parse a tag as a floating point value. If the value cannot be parsed, return
...@@ -152,8 +180,9 @@ class HeightAndFloorspaceRule: ...@@ -152,8 +180,9 @@ class HeightAndFloorspaceRule:
if tag_value is None or tag_value == "": if tag_value is None or tag_value == "":
return None return None
# If the tag does exist, try to convert it to float. If that raises a `ValueError`, # OSM can have unexpected data types and unexpected content in their tagging scheme,
# return None. # therefore we do not raise an exception in case of a ValueError, but instead ignore
# the values.
try: try:
return float(tag_value) return float(tag_value)
except ValueError: except ValueError:
......
...@@ -29,26 +29,61 @@ def test_height_and_floorspace_rule(height_and_floorspace_rule): ...@@ -29,26 +29,61 @@ def test_height_and_floorspace_rule(height_and_floorspace_rule):
OSM and GHSL. OSM and GHSL.
""" """
building_information = { test_list = [
"tags": { # Test values related to building stories.
"building:levels": 5, [
"roof:levels": 2, {
"building:min_level": 1, "tags": {
"building:levels:underground": 0, "building:levels": "5",
}, "roof:levels": "2",
"area": 300.0, "building:min_level": "1",
} "building:levels:underground": "0",
result = height_and_floorspace_rule(**building_information) },
message = ( "area": 300.0,
f"The `height` attribute is not correct. " },
f"Expected `H:6` but estimated {result['height']}" "H:6", # Expected return height.
) 1350.0, # Expected return floorspace.
assert result["height"] == "H:6", message ],
message = ( # Test values related to building height.
f"The `floorspace` attribute is not correct. " [
f"Expected `1350` but estimated {result['floorspace']}" {
) "tags": {
assert result["floorspace"] == pytest.approx(1350.0), message "height": "5",
"min_height": "2",
},
"area": 300.0,
},
"HHT:3.00", # Expected return height.
None, # Expected return floorspace.
],
# Test values related to building levels and height.
[
{
"tags": {
"building:levels": "5",
"building:levels:underground": "1",
"height": "5",
"min_height": "2",
},
"area": 300.0,
},
"H:5+HBEX:1+HHT:3.00", # Expected return height.
1620.0, # Expected return floorspace.
],
]
for building_information, correct_height, correct_floorspace in test_list:
result = height_and_floorspace_rule(**building_information)
message = (
f"The `height` attribute is not correct. "
f"Expected `{correct_height}` but estimated `{result['height']}`."
)
assert result["height"] == correct_height, message
message = (
f"The `floorspace` attribute is not correct. "
f"Expected `{correct_floorspace}` but estimated `{result['floorspace']}`"
)
assert result["floorspace"] == pytest.approx(correct_floorspace), message
def test_get_stories_and_floorspace_from_osm(height_and_floorspace_rule): def test_get_stories_and_floorspace_from_osm(height_and_floorspace_rule):
...@@ -59,17 +94,21 @@ def test_get_stories_and_floorspace_from_osm(height_and_floorspace_rule): ...@@ -59,17 +94,21 @@ def test_get_stories_and_floorspace_from_osm(height_and_floorspace_rule):
rule_function = height_and_floorspace_rule.function.get_stories_and_floorspace_from_osm rule_function = height_and_floorspace_rule.function.get_stories_and_floorspace_from_osm
test_list = [ test_list = [
# Check with only building levels. # Check with only building levels.
[{"tags": {"building:levels": 5}, "area": 100.0}, "H:5", 450.0], [{"tags": {"building:levels": "5"}, "area": 100.0}, "H:5", 450.0],
# Check with only roof levels. # Check with only roof levels.
[{"tags": {"roof:levels": 5}, "area": 100.0}, "H:5", 225.0], [{"tags": {"roof:levels": "5"}, "area": 100.0}, "H:5", 225.0],
# Check with only levels underground. # Check with only levels underground.
[{"tags": {"building:levels:underground": 5}, "area": 100.0}, "HBEX:5", 450.0], [{"tags": {"building:levels:underground": "5"}, "area": 100.0}, "HBEX:5", 450.0],
# Check with a wrong type as input. # Check with a wrong type as input.
[{"tags": {"building:levels": "no"}, "area": 100.0}, None, None], [{"tags": {"building:levels": "no"}, "area": 100.0}, None, None],
# Check with a negative value. # Check with a negative value.
[{"tags": {"building:levels:underground": -1}, "area": 100.0}, None, None], [{"tags": {"building:levels:underground": "-1"}, "area": 100.0}, None, None],
# Check with a `building:min_level` value higher than the `building_levels` value. # Check with a `building:min_level` value higher than the `building_levels` value.
[{"tags": {"building:levels": 5, "building:min_level": 6}, "area": 100.0}, None, None], [
{"tags": {"building:levels": "5", "building:min_level": "6"}, "area": 100.0},
None,
None,
],
# Check without any tags. # Check without any tags.
[{"tags": {}, "area": 100.0}, None, None], [{"tags": {}, "area": 100.0}, None, None],
# Check with all tags. # Check with all tags.
...@@ -91,14 +130,14 @@ def test_get_stories_and_floorspace_from_osm(height_and_floorspace_rule): ...@@ -91,14 +130,14 @@ def test_get_stories_and_floorspace_from_osm(height_and_floorspace_rule):
for input_values, correct_height, correct_floorspace in test_list: for input_values, correct_height, correct_floorspace in test_list:
height, floorspace = rule_function(**input_values) height, floorspace = rule_function(**input_values)
# Check the height attribute # Check the height attribute.
message = ( message = (
f"The `height` attribute is not correct. Expected `{correct_height}` but estimated " f"The `height` attribute is not correct. Expected `{correct_height}` but estimated "
f"`{height}` using {input_values} as input." f"`{height}` using {input_values} as input."
) )
assert height == correct_height, message assert height == correct_height, message
# Check the floorspace attribute # Check the floorspace attribute.
message = ( message = (
f"The `floorspace` attribute is not correct. Expected `{correct_floorspace}` but " f"The `floorspace` attribute is not correct. Expected `{correct_floorspace}` but "
f"estimated `{floorspace}` using {input_values} as input." f"estimated `{floorspace}` using {input_values} as input."
...@@ -106,6 +145,43 @@ def test_get_stories_and_floorspace_from_osm(height_and_floorspace_rule): ...@@ -106,6 +145,43 @@ def test_get_stories_and_floorspace_from_osm(height_and_floorspace_rule):
assert floorspace == correct_floorspace, message assert floorspace == correct_floorspace, message
def test_get_height_from_osm(height_and_floorspace_rule):
"""
Test the function `tag_to_float` of the `HeightAndFloorspaceRule`.
"""
rule_function = height_and_floorspace_rule.function.get_height_from_osm
test_list = [
# Check a valid height.
[{"tags": {"height": "8"}}, "HHT:8.00"],
# Check a building with a floating point height value.
[{"tags": {"height": "8.2"}}, "HHT:8.20"],
# Check a building that is above ground.
[{"tags": {"height": "8", "min_height": "4"}}, "HHT:4.00"],
# Check a building with an invalid `min_height` attribute.
[{"tags": {"height": "8", "min_height": "no"}}, "HHT:8.00"],
# Check a building with a `min_height` higher than the `height` attribute.
[{"tags": {"height": "4", "min_height": "5"}}, None],
# Check a building with a negative height.
[{"tags": {"height": "-4"}}, None],
# Check a building with an invalid `height` attribute.
[{"tags": {"height": "no", "min_height": "5"}}, None],
# Check a building without tags.
[{"tags": {}}, None],
]
for input_values, correct_height in test_list:
height = rule_function(**input_values)
# Check the height attribute.
message = (
f"The `height` attribute is not correct. Expected `{correct_height}` but estimated "
f"`{height}` using {input_values} as input."
)
assert height == correct_height, message
def test_tag_to_float(height_and_floorspace_rule): def test_tag_to_float(height_and_floorspace_rule):
""" """
Test the function `tag_to_float` of the `HeightAndFloorspaceRule. Test the function `tag_to_float` of the `HeightAndFloorspaceRule.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment