From 4ebb5bef77d7ac14e98a04048a69322215801bf9 Mon Sep 17 00:00:00 2001 From: Laurens Oostwegel <laurens@gfz-potsdam.de> Date: Fri, 12 Jul 2024 14:13:18 +0200 Subject: [PATCH] Add height from the OpenStreetMap height-related tags --- .../height_and_floorspace.py | 47 +++++-- tests/test_height_and_floorspace_rule.py | 130 ++++++++++++++---- 2 files changed, 141 insertions(+), 36 deletions(-) diff --git a/building/02_process/height_and_floorspace/height_and_floorspace.py b/building/02_process/height_and_floorspace/height_and_floorspace.py index d1f24bb..c29239c 100644 --- a/building/02_process/height_and_floorspace/height_and_floorspace.py +++ b/building/02_process/height_and_floorspace/height_and_floorspace.py @@ -40,16 +40,14 @@ class HeightAndFloorspaceRule: # 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 # floorspace being estimated. - try: - 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 + stories, floorspace = self.get_stories_and_floorspace_from_osm(tags, area) if 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 # returned. if len(gem_taxonomy_height) > 0: @@ -130,6 +128,36 @@ class HeightAndFloorspaceRule: 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): """ Try to parse a tag as a floating point value. If the value cannot be parsed, return @@ -152,8 +180,9 @@ class HeightAndFloorspaceRule: if tag_value is None or tag_value == "": return None - # If the tag does exist, try to convert it to float. If that raises a `ValueError`, - # return None. + # 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. try: return float(tag_value) except ValueError: diff --git a/tests/test_height_and_floorspace_rule.py b/tests/test_height_and_floorspace_rule.py index 9aa34c7..d20303f 100644 --- a/tests/test_height_and_floorspace_rule.py +++ b/tests/test_height_and_floorspace_rule.py @@ -29,26 +29,61 @@ def test_height_and_floorspace_rule(height_and_floorspace_rule): OSM and GHSL. """ - building_information = { - "tags": { - "building:levels": 5, - "roof:levels": 2, - "building:min_level": 1, - "building:levels:underground": 0, - }, - "area": 300.0, - } - result = height_and_floorspace_rule(**building_information) - message = ( - f"The `height` attribute is not correct. " - f"Expected `H:6` but estimated {result['height']}" - ) - assert result["height"] == "H:6", message - message = ( - f"The `floorspace` attribute is not correct. " - f"Expected `1350` but estimated {result['floorspace']}" - ) - assert result["floorspace"] == pytest.approx(1350.0), message + test_list = [ + # Test values related to building stories. + [ + { + "tags": { + "building:levels": "5", + "roof:levels": "2", + "building:min_level": "1", + "building:levels:underground": "0", + }, + "area": 300.0, + }, + "H:6", # Expected return height. + 1350.0, # Expected return floorspace. + ], + # Test values related to building height. + [ + { + "tags": { + "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): @@ -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 test_list = [ # 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. - [{"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. - [{"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. [{"tags": {"building:levels": "no"}, "area": 100.0}, None, None], # 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. - [{"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. [{"tags": {}, "area": 100.0}, None, None], # Check with all tags. @@ -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: height, floorspace = rule_function(**input_values) - # Check the height attribute + # 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 - # Check the floorspace attribute + # Check the floorspace attribute. message = ( f"The `floorspace` attribute is not correct. Expected `{correct_floorspace}` but " f"estimated `{floorspace}` using {input_values} as input." @@ -106,6 +145,43 @@ def test_get_stories_and_floorspace_from_osm(height_and_floorspace_rule): 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): """ Test the function `tag_to_float` of the `HeightAndFloorspaceRule. -- GitLab