From 9bc4dba57118314d3e7d70eec0eba419b7d1dd35 Mon Sep 17 00:00:00 2001
From: Laurens <laurens@gfz-potsdam.de>
Date: Thu, 21 Sep 2023 17:05:23 +0200
Subject: [PATCH] Add processing rules

---
 .gitlab-ci.yml                                |  1 -
 building/02_process/geometry/geometry.py      | 15 ++++++
 building/02_process/geometry/rule.xml         | 10 ++++
 building/02_process/quadkey/quadkey.py        | 23 ++++++++
 building/02_process/quadkey/rule.xml          | 11 ++++
 .../02_process/relation_id/relation_id.py     | 18 +++++++
 building/02_process/relation_id/rule.xml      | 10 ++++
 .../stories_and_floorspace/rule.xml           | 13 +++++
 .../stories_and_floorspace.py                 | 52 +++++++++++++++++++
 .../obm_buildings_upsert.py                   |  6 +--
 .../03_upsert/obm_buildings_upsert/rule.xml   |  2 +-
 building/README.md                            | 18 ++++++-
 12 files changed, 173 insertions(+), 6 deletions(-)
 create mode 100644 building/02_process/geometry/geometry.py
 create mode 100644 building/02_process/geometry/rule.xml
 create mode 100644 building/02_process/quadkey/quadkey.py
 create mode 100644 building/02_process/quadkey/rule.xml
 create mode 100644 building/02_process/relation_id/relation_id.py
 create mode 100644 building/02_process/relation_id/rule.xml
 create mode 100644 building/02_process/stories_and_floorspace/rule.xml
 create mode 100644 building/02_process/stories_and_floorspace/stories_and_floorspace.py

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ea42571..51cc9e0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -25,7 +25,6 @@ linters:
   script:
     - flake8 --max-line-length=$LINE_LENGTH $SOURCES
     - black --check --line-length $LINE_LENGTH $SOURCES
-    - pylint -E $SOURCES
 
 release_job:
   stage: release
diff --git a/building/02_process/geometry/geometry.py b/building/02_process/geometry/geometry.py
new file mode 100644
index 0000000..bbbc8f0
--- /dev/null
+++ b/building/02_process/geometry/geometry.py
@@ -0,0 +1,15 @@
+class GeometryRule:
+    def __call__(self, geometry, *args, **kwargs):
+        """
+        Wrap the WKT formatted geometry of a building in the `ST_GeomFromText()` function
+
+        Args:
+            geometry (str):
+                WKT formatted geometry of the building
+
+        Returns:
+            A dictionary with the WKT formatted geometry of the building wrapped in the
+            `ST_GeomFromText()` function.
+        """
+
+        return {"geometry": f"ST_GeomFromText('{geometry}', 4326)"}
diff --git a/building/02_process/geometry/rule.xml b/building/02_process/geometry/rule.xml
new file mode 100644
index 0000000..d357c5a
--- /dev/null
+++ b/building/02_process/geometry/rule.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<rule name="GeometryRule">
+    <input>
+        <param type="str">geometry</param>
+    </input>
+    <function filepath="geometry.py"/>
+    <output>
+        <param type="str">geometry</param>
+    </output>
+</rule>
diff --git a/building/02_process/quadkey/quadkey.py b/building/02_process/quadkey/quadkey.py
new file mode 100644
index 0000000..a9d3110
--- /dev/null
+++ b/building/02_process/quadkey/quadkey.py
@@ -0,0 +1,23 @@
+class QuadkeyRule:
+    def __call__(self, longitude, latitude, *args, **kwargs):
+        """
+        Determine the Quadkey of the tile in which the building centroid is located given its
+        longitude and latitude.
+
+        Args:
+            longitude:
+                Longitude of the building centroid
+            latitude:
+                Latitude of the building centroid
+
+        Returns:
+            A dictionary with the zoom-level 18 Quadkey.
+        """
+
+        import mercantile
+
+        if longitude is None or latitude is None:
+            quadkey = None
+        else:
+            quadkey = mercantile.quadkey(mercantile.tile(longitude, latitude, 18))
+        return {"quadkey": quadkey}
diff --git a/building/02_process/quadkey/rule.xml b/building/02_process/quadkey/rule.xml
new file mode 100644
index 0000000..07d368e
--- /dev/null
+++ b/building/02_process/quadkey/rule.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<rule name="QuadkeyRule">
+    <input>
+        <param type="float">longitude</param>
+        <param type="float">latitude</param>
+    </input>
+    <function filepath="quadkey.py"/>
+    <output>
+        <param type="str">quadkey</param>
+    </output>
+</rule>
diff --git a/building/02_process/relation_id/relation_id.py b/building/02_process/relation_id/relation_id.py
new file mode 100644
index 0000000..2ef2a6e
--- /dev/null
+++ b/building/02_process/relation_id/relation_id.py
@@ -0,0 +1,18 @@
+class RelationIDRule:
+    def __call__(self, relations, *args, **kwargs):
+        """
+        Determine the building relation OSM ID.
+
+        Args:
+            relations:
+                List of the attributes of all relations that the building is member of.
+
+        Returns:
+            OSM ID of the first relation that the building is member of.
+        """
+
+        for relation in relations:
+            osm_id = relation.get("osm_id", None)
+            if osm_id is not None:
+                return {"relation_id": osm_id}
+        return {"relation_id": None}
diff --git a/building/02_process/relation_id/rule.xml b/building/02_process/relation_id/rule.xml
new file mode 100644
index 0000000..701f24c
--- /dev/null
+++ b/building/02_process/relation_id/rule.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<rule name="RelationIDRule">
+    <input>
+        <param type="list">relations</param>
+    </input>
+    <function filepath="relation_id.py"/>
+    <output>
+        <param type="int">relation_id</param>
+    </output>
+</rule>
diff --git a/building/02_process/stories_and_floorspace/rule.xml b/building/02_process/stories_and_floorspace/rule.xml
new file mode 100644
index 0000000..24ce38c
--- /dev/null
+++ b/building/02_process/stories_and_floorspace/rule.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<rule name="StoriesAndFloorspaceRule" category="building">
+    <input>
+        <param type="int">tags</param>
+        <param type="float">relations</param>
+        <param type="float">area</param>
+    </input>
+    <function filepath="stories_and_floorspace.py"/>
+    <output>
+        <param type="float">storeys</param>
+        <param type="float">floorspace</param>
+    </output>
+</rule>
diff --git a/building/02_process/stories_and_floorspace/stories_and_floorspace.py b/building/02_process/stories_and_floorspace/stories_and_floorspace.py
new file mode 100644
index 0000000..dbf5bd0
--- /dev/null
+++ b/building/02_process/stories_and_floorspace/stories_and_floorspace.py
@@ -0,0 +1,52 @@
+class StoriesAndFloorspaceRule:
+    def __call__(self, tags, relations, area, *args, **kwargs):
+        """
+        Find the `building:levels` tag in the attributes of the building or one of the building
+        relations and save this as the number of stories. Calculate the floorspace of the
+        building, based on the footprint size of the building and the number of stories.
+
+        Args:
+            tags (dict):
+                Building tags, such as the building levels or building type.
+            relations (list):
+                List of the attributes of all relations that the building is member of.
+            area (float):
+                Footprint size of the building.
+
+        Returns:
+            A dictionary with the number of stories and the floorspace of the building.
+        """
+
+        from math import ceil
+
+        story_options = [
+            self.get_story_tag(building["tags"]) for building in [tags] + relations
+        ]
+
+        for story_string in story_options:
+            if story_string is None or story_string == "":
+                continue
+            try:
+                stories = ceil(float(story_string))
+                if stories < 1:
+                    raise ValueError("Number of stories cannot be below 1")
+                floorspace = stories * area
+                return {"stories": stories, "floorspace": floorspace}
+            except ValueError:
+                continue
+        return {"stories": None, "floorspace": None}
+
+    @staticmethod
+    def get_story_tag(tags):
+        """
+        Get the number of stories, if the attribute `building:levels` exist
+
+        Args:
+            tags:
+                Building tags, such as the building levels or building type.
+
+        Returns:
+            Number of stories, if the attribute `building:levels` exist, otherwise `None`.
+        """
+
+        return tags.get("building:levels", None)
diff --git a/building/03_upsert/obm_buildings_upsert/obm_buildings_upsert.py b/building/03_upsert/obm_buildings_upsert/obm_buildings_upsert.py
index ada6f97..862c8b2 100644
--- a/building/03_upsert/obm_buildings_upsert/obm_buildings_upsert.py
+++ b/building/03_upsert/obm_buildings_upsert/obm_buildings_upsert.py
@@ -6,7 +6,7 @@ class ObmBuildingsUpsert:
         osm_id,
         floorspace=None,
         occupancy=None,
-        storeys=None,
+        stories=None,
         relation_id=None,
         quadkey=None,
         *args,
@@ -27,7 +27,7 @@ class ObmBuildingsUpsert:
                 The calculated floorspace of the building.
             occupancy (str):
                 The occupancy type formatted to the GEM occupancy types.
-            storeys (int):
+            stories (int):
                 Number of stories in the building.
             relation_id (int):
                 ID of the OSM relation feature that this building is part of.
@@ -44,7 +44,7 @@ class ObmBuildingsUpsert:
                 {geometry},
                 {self.format_value(floorspace)},
                 {self.format_value(occupancy)},
-                {self.format_value(storeys)},
+                {self.format_value(stories)},
                 {self.format_value(relation_id)},
                 {self.format_value(quadkey)},
                 NOW()
diff --git a/building/03_upsert/obm_buildings_upsert/rule.xml b/building/03_upsert/obm_buildings_upsert/rule.xml
index 4ab035b..8ca9c06 100644
--- a/building/03_upsert/obm_buildings_upsert/rule.xml
+++ b/building/03_upsert/obm_buildings_upsert/rule.xml
@@ -6,7 +6,7 @@
         <param type="int">osm_id</param>
         <param type="float">floorspace</param>
         <param type="str">occupancy</param>
-        <param type="int">storeys</param>
+        <param type="int">stories</param>
         <param type="int">relation_id</param>
         <param type="str">quadkey</param>
     </input>
diff --git a/building/README.md b/building/README.md
index 41e1b25..f37ee15 100644
--- a/building/README.md
+++ b/building/README.md
@@ -10,7 +10,23 @@ all land-use objects that intersect the building.
                             
 ### Process rules
 
-*Not implemented*
+* **Geometry**
+
+Wraps the WKT formatted geometry of a building in the `ST_GeomFromText()` function
+
+* **Quadkey**
+
+Determines the Quadkey tile of a building, based on the latitude and longitude of the centroid.
+
+* **RelationID**
+
+Determines the building relation OSM ID.
+
+* **StoriesAndFloorspace**
+
+Finds the `building:levels` tag in the attributes of the building or one of the building
+relations and saves it as number of stories. Calculates the floorspace of the building, based on the
+footprint size of the building and the number of stories.
 
 ### Upsert rules
                                   
-- 
GitLab