From 3f9e53a05a63553eaba9974bf0a1c4079e2e43ee Mon Sep 17 00:00:00 2001 From: Laurens Oostwegel <laurens@gfz-potsdam.de> Date: Fri, 22 Sep 2023 11:31:27 +0200 Subject: [PATCH] Add the occupancy rule --- building/02_process/occupancy/occupancy.py | 592 +++++++++++++++++++++ building/02_process/occupancy/rule.xml | 12 + building/README.md | 4 + 3 files changed, 608 insertions(+) create mode 100644 building/02_process/occupancy/occupancy.py create mode 100644 building/02_process/occupancy/rule.xml diff --git a/building/02_process/occupancy/occupancy.py b/building/02_process/occupancy/occupancy.py new file mode 100644 index 0000000..e863b63 --- /dev/null +++ b/building/02_process/occupancy/occupancy.py @@ -0,0 +1,592 @@ +class OccupancyRule: + def __call__(self, tags, osm_spots, osm_lands, *args, **kwargs): + """ + Define the occupancy type of the building. There are 10 sequential rules that can + define an occupancy type: + 1. No occupancies: return UNK + 2. One occupancy: return that occupancy + 3. Overriding occupancies: return the overriding occupancy + 4. All tags have the same level 1: Return the `detail_1` of that occupancy + 5. Residential with garage: Return residential occupancy + 6. Shopping mall: With a combination of certain occupancy types, return COM1 + 7. All tags have the same level 0: Return the `detail_0` of that occupancy + 8. Residential/commercial: With a mix of only these two, return MIX1 + 9. Commercial/industrial: With a mix of only these two, return MIX5 + 10. Residential/industrial: With a mix of only these two, return MIX4 + For a more elaborate description, look at the comments before each rule. + + Args: + tags: + Building tags, such as the building levels or building type + osm_spots: + A list of the tags of all POIs inside the building. + osm_lands: + A list of the tags of all land-use features intersecting the building. + + Returns: + A dictionary with the occupancy of the building. + """ + + extended_tags = [tags] + osm_spots + osm_lands + occupancies = self.occupancies_from_tags(extended_tags) + + unique_occupancies_level_0 = set([occupancy.level_0 for occupancy in occupancies]) + unique_occupancies_level_1 = set( + [occupancy.detail_1 for occupancy in occupancies if occupancy.detail_1 != ""] + ) + + # Rule no occupancies + # If there are no occupancies, return `unknown` + if len(occupancies) == 0: + return {"occupancy": "UNK"} + + # Rule one occupancy + # If there is only one occupancy, return that occupancy + if len(occupancies) == 1: + return {"occupancy": occupancies[0].occupancy_string} + + # Rule overriding occupancies + # If any of the `overriding_occupancies` is in the list, return that one. + for occ in occupancies: + if occ in self.overriding_occupancies: + return {"occupancy": occ} + + # Rule two tags; same level 1 + # If there is one unique level 0 and one unique level 1 (which implies that the + # unique level 1 has the same level 0 as the unique level 0), returns level 1. + if len(unique_occupancies_level_0) == 1 and len(unique_occupancies_level_1) <= 1: + for occ in occupancies: + if occ.detail_1 != "" or len(unique_occupancies_level_1) == 0: + return {"occupancy": occ.level_1} + + # Rule residential with garage + # If there is a combination of residential occupancies and COM7 (covered parking + # garage), it returns "RES". + if all(occ.level_0 == "RES" or occ.level_1 == "COM7" for occ in occupancies): + return {"occupancy": "RES"} + + # Rule shopping mall + # A concatenation of "many" retail trade (COM1) and restaurants/bars/cafes (COM5), which + # could also have a cinema (ASS3), could likely be a shopping mall (COM1). + # + # The rule concludes that a combination of tags is a shopping mall when: + # - The only individual tags are "COM1" AND "COM5", irrespective of the number of + # times they appear, + # or + # - There are at least two cases of "COM1" and all other tags are "COM" and/or + # "COM2" and/or "COM3" and/or "COM5" and/or "COM7" and/or "COM11" and/or "ASS3". + if all(occ.level_1 in ["COM1", "COM5", "COM"] for occ in occupancies): + return {"occupancy": "COM1"} + com_1_occurance = sum(map(lambda occ: 1 if occ.level_1 == "COM1" else 0, occupancies)) + + if com_1_occurance >= 2 and all( + occ.level_0 == "COM" or occ.level_1 == "ASS3" for occ in occupancies + ): + return {"occupancy": "COM1"} + + # Rule many tags same level 0 + # Returns the unique value of level 0 occupancy, if only one value of level 0 + # occupancy but more than one different values of level 1 occupancy are associated + # with the building. + if len(unique_occupancies_level_0) == 1: + return {"occupancy": occupancies[0].level_0} + + # Rule residential and commercial + # When the tags are a mix of residential and commercial occupancies of the kind "COM1", + # "COM3" and/or "COM5" and/or "ASS3", the result is "MIX1" ("mostly residential and + # commercial"). + if ( + "RES" in unique_occupancies_level_0 + and ("COM" in unique_occupancies_level_0 or "ASS" in unique_occupancies_level_0) + and all( + occ.level_0 == "RES" or occ.level_1 in ["COM", "COM1", "COM3", "COM5", "ASS3"] + for occ in occupancies + ) + ): + return {"occupancy": "MIX1"} + + # Rule commercial and industrial + # When the tags are a mix of industrial ("IND" or "IND1" or "IND2") and commercial + # occupancies of the kind "COM", "COM1", "COM2", "COM3", "COM5", "COM7" and/or "COM11", + # the result is "MIX5" ("mostly industrial and commercial"). + if ( + "IND" in unique_occupancies_level_0 + and "COM" in unique_occupancies_level_0 + and all( + occ.level_0 == "IND" + or occ.level_1 in ["COM", "COM1", "COM2", "COM3", "COM5", "COM7", "COM11"] + for occ in occupancies + ) + ): + return {"occupancy": "MIX5"} + + # Rule residential and industrial + # When the tags are a mix of residential and industrial occupancies, the result is + # "MIX4" ("mostly residential and industrial"). + if all(occ.level_0 in ["RES", "IND"] for occ in occupancies): + return {"occupancy": "MIX4"} + + return {"occupancy": "UNK"} + + def occupancies_from_tags(self, tag_list): + """ + Given a list of tags, get all the possible occupancies according to the OSM tags to + occupancy mapping (attribute `occupancy_mapper`). + + Args: + tag_list: + List of tags that could be mapped to occupancy types + + Return: + A list of possible occupancy types + """ + + occupancies = [] + for osm_tags in tag_list: + for key, value in osm_tags.items(): + if (key, value) not in self.occupancy_mapper: + continue + occupancy = self.occupancy_mapper[key, value] + mapped_occupancies = occupancy.split("|") + occupancies.extend(mapped_occupancies) + + return [ + self.Occupancy(occupancy) for occupancy in occupancies if occupancy != "UNDECIDABLE" + ] + + class Occupancy: + def __init__(self, occupancy_as_string): + """ + Dissect a string formatted occupancy into three parts. For example, the RES2A + occupancy type has three parts: RES (residential), 2 (multi-dwelling) and A (2-unit + residential building / duplex). The first part is mapped to `detail_0`, the second + to `detail_1` and the third to `detail_2`. + + Args: + occupancy_as_string (str): + String formatted occupancy + """ + + self.detail_0 = "" + self.detail_1 = "" + self.detail_2 = "" + for idx, char in enumerate(occupancy_as_string): + # The first three + if idx < 3: + self.detail_0 = self.detail_0 + char + # If the character is a digit, it is part of detail_1. Detail_1 can be max 2 + # characters long, therefore the index should be below 5. + elif idx < 5 and char.isdigit(): + self.detail_1 = self.detail_1 + char + # Everything else is part of detail_2 + else: + self.detail_2 = occupancy_as_string[idx:] + break + + # Check if detail_0 exists + assert ( + self.detail_0 + ), f"could not extract occupancy level_0 from {occupancy_as_string}" + # Check if all characters in detail_0 are alphabetic + assert self.detail_0.isalpha(), ( + f"{occupancy_as_string} is not formatted correctly: " + + f"detail_0 {self.detail_0} should only consist of letters, not digits" + ) + # Check if all characters in detail_0 are upper-case + assert self.detail_0.isupper(), ( + f"{occupancy_as_string} is not formatted correctly: " + + f"detail_0 {self.detail_0} should be upper-case" + ) + + # Check the formatting of detail_1 + if self.detail_1: + # Check if all characters in detail_1 are digits + assert self.detail_1.isdigit(), ( + f"{occupancy_as_string} is not formatted correctly: " + + f"detail_1 {self.detail_0} should only consist of digits, not letters" + ) + + # Check the formatting of detail_2 + if self.detail_2: + # Check if all characters in detail_2 are alphabetic + assert self.detail_2.isalpha(), ( + f"{occupancy_as_string} is not formatted correctly: " + + f"detail_2 {self.detail_2} should only consist of letters, not digits" + ) + # Check if all characters in detail_2 are upper-case + assert self.detail_2.isupper(), ( + f"{occupancy_as_string} is not formatted correctly: " + + f"detail_2 {self.detail_2} should be upper-case" + ) + + self.occupancy_string = occupancy_as_string + + @property + def level_0(self): + """ + Return the detail 0 part of the Occupancy + """ + + return self.detail_0 + + @property + def level_1(self): + """ + Return the detail 0 and detail 1 parts of the Occupancy + """ + + return self.detail_0 + self.detail_1 + + @property + def level_2(self): + """ + Return the detail 0, detail 1 and detail 2 parts of the Occupancy + """ + + return self.detail_0 + self.detail_1 + self.detail_2 + + def __eq__(self, other): + """ + Check if two occupancies are the same. + + Args: + other (Occupancy): + Occupancy to compare with + """ + + return self.occupancy_string == other + + def __hash__(self): + """ + Use the occupancy string to hash the Occupancy class + """ + + return hash(self.occupancy_string) + + @property + def occupancy_mapper(self): + """ + Dictionary of OSM tags (key/value) as keys and occupancy type as values + """ + + # OSM occupancies to occupancies + return { + ("amenity", "university"): "EDU3", + ("amenity", "school"): "EDU2", + ("amenity", "college"): "EDU3", + ("amenity", "library"): "COM6", + ("amenity", "fuel"): "COM1", + ("amenity", "cinema"): "ASS3", + ("amenity", "theatre"): "ASS3", + ("amenity", "place_of_worship"): "ASS1", + ("amenity", "hospital"): "COM4", # Add MED1 extension in next version + ("landuse", "allotments"): "RES3", + ("landuse", "animal_keeping"): "AGR2", + # ("landuse", "cemetery"): "NOC", # Add extension in next version + ("landuse", "commercial"): "COM", + ("landuse", "education"): "EDU", + ("landuse", "farm"): "AGR", + ("landuse", "farmland"): "AGR", + ("landuse", "farmyard"): "AGR", + ("landuse", "government"): "GOV", + ("landuse", "greenhouse"): "AGR3", + ("landuse", "greenhouse_horticulture"): "AGR3", + ("landuse", "industrial"): "IND", + ("landuse", "landfill"): "IND", + ("landuse", "leisure"): "COM11", + ("landuse", "meadow"): "AGR", + ("landuse", "mine"): "IND1", + ("landuse", "military"): "GOV", # Add MIL in next version + ("landuse", "orchard"): "AGR", + ("landuse", "plant_nursery"): "AGR3", + ("landuse", "quarry"): "IND1", + # ("landuse", "railway"): "TRA", # Add extension in next version + ("landuse", "recreation_ground"): "COM11", + ("landuse", "religious"): "ASS1", + ("landuse", "residential"): "RES", + ("landuse", "retail"): "COM1", + ("landuse", "school"): "EDU2", + ("landuse", "vineyard"): "AGR", + ("landuse", "winter_sports"): "COM11", + ("leisure", "common"): "ASS4", + ("leisure", "dog_park"): "COM11", + ("leisure", "escape_game"): "COM11", + ("leisure", "golf_course"): "COM11", + ("leisure", "ice_rink"): "COM11", + ("leisure", "hackerspace"): "COM11", + ("leisure", "marina"): "COM11", + ("leisure", "miniature_golf"): "COM11", + ("leisure", "pitch"): "COM11", + ("leisure", "sports_centre"): "COM11", + ("leisure", "stadium"): "ASS2", + ("leisure", "swimming_area"): "COM11", + ("leisure", "swimming_pool"): "COM11", + ("leisure", "water_park"): "COM11", + ("tourism", "theme_park"): "COM11", + ("tourism", "zoo"): "AGR2|COM5|COM11", + ("aerialway", "station"): "COM9", # Add TRA7A extension in next version + ("aeroway", "hangar"): "COM10", # Add TRA7B extension in next version + ("aeroway", "terminal"): "COM10", # Add TRA7A extension in next version + ("amenity", "arts_centre"): "COM6", + ("amenity", "bank"): "COM3", + ("amenity", "bar"): "COM5", + ("amenity", "bbq"): "COM11", + ("amenity", "bicycle_rental"): "COM1", + ("amenity", "biergarten"): "COM5", + ("amenity", "bureau_de_change"): "COM1", + ("amenity", "bus_station"): "COM8", # Add TRA2A extension in next version + ("amenity", "cafe"): "COM5", + ("amenity", "car_rental"): "COM1", + ("amenity", "childcare"): "EDU1", + ("amenity", "clinic"): "COM4", + ("amenity", "community_centre"): "ASS4", + ("amenity", "courthouse"): "GOV1", + ("amenity", "dentist"): "COM4", # Add MED2 extension in next version + ("amenity", "doctors"): "COM4", # Add MED2 extension in next version + ("amenity", "driving_school"): "COM1", + ("amenity", "embassy"): "GOV1", + ("amenity", "events_venue"): "ASS4", + ("amenity", "fast_food"): "COM5", + ("amenity", "ferry_terminal"): "COM9", # Add TRA extension in next version + ("amenity", "fire_station"): "GOV2", + ("amenity", "food_court"): "COM5", + ("amenity", "grave_yard"): "ASS1", + ("amenity", "ice_cream"): "COM5", + ("amenity", "internet_cafe"): "COM1", + ("amenity", "kindergarten"): "EDU1", + ("amenity", "marketplace"): "COM1", + ("amenity", "mobile_money_agent"): "COM1", + ("amenity", "nightclub"): "COM11", + ("amenity", "nursing_home"): "RES4", + ("amenity", "pharmacy"): "COM4", # Add MED3 extension in next version + ("amenity", "police"): "GOV2", + ("amenity", "post_office"): "COM3", + ("amenity", "prison"): "RES4", + ("amenity", "pub"): "COM5", + ("amenity", "public_building"): "GOV", + ("amenity", "recycling"): "IND", + ("amenity", "restaurant"): "COM5", + ("amenity", "shelter"): "RES3", + ("amenity", "social_facility"): "ASS4", + ("amenity", "studio"): "COM3", + ("amenity", "swimming_pool"): "COM11", + ("amenity", "townhall"): "GOV1", + ("amenity", "veterinary"): "COM4", # Add MED4 extension in next version + ("building", "allotment_house"): "RES1", + ("building", "apartments"): "RES2", + ("building", "barn"): "AGR1", + ("building", "bungalow"): "RES3", + # ("building", "bunker"): "NOC", # Add extension in next version + ("building", "cabin"): "RES3", + ("building", "chapel"): "ASS1", + ("building", "church"): "ASS1", + ("building", "civic"): "GOV1", + ("building", "college"): "EDU3", + ("building", "commercial"): "COM", + ("building", "commercial;residential"): "MIX2", + # ("building", "construction"): "NOC", # Add extension in next version + ("building", "cowshed"): "AGR2", + ("building", "detached"): "RES1", + ("building", "dormitory"): "RES4", + ("building", "farm"): "AGR", + ("building", "farm_auxiliary"): "AGR", + ("building", "garage"): "COM7", # Add extension RES7|COM7 in next version + ("building", "garages"): "COM7", # Add extension RES7|COM7 in next version + ("building", "government"): "GOV1", + ("building", "grandstand"): "ASS2", + ("building", "greenhouse"): "AGR3", + ("building", "hangar"): "COM10", + ("building", "hospital"): "COM4", + ("building", "hotel"): "RES3", + ("building", "house"): "RES1", + ("building", "hut"): "RES1", # Add extension RES6 in next version + ("building", "industrial"): "IND", + ("building", "kindergarten"): "EDU1", + ("building", "kiosk"): "COM1", + ("building", "manufacture"): "IND2", + ("building", "mosque"): "ASS1", + ("building", "office"): "COM3", + ("building", "parking"): "COM7", + ("building", "public"): "GOV", + ("building", "residential"): "RES", + ("building", "retail"): "COM1", + ("building", "school"): "EDU2", + ("building", "semi"): "RES2A", + ("building", "semidetached_house"): "RES2A", + ("building", "service"): "IND", + # ("building", "shed"): "NOC", # Add extension in next version + ("building", "shop"): "COM", + ("building", "silo"): "AGR1", + ("building", "slurry_tank"): "AGR3", + ("building", "stable"): "AGR2", + ("building", "storage_tank"): "IND", + ("building", "sty"): "AGR2", + ("building", "supermarket"): "COM1", + ("building", "temple"): "ASS1", + ("building", "terrace"): "RES2", + # ("building", "toilets"): "NOC", # Add extension in next version + ("building", "train_station"): "COM9", # Add TRA5 extension in next version + ("building", "transformer_tower"): "IND", # Add LIF2BA in next version + ("building", "transportation"): "COM9", # Add TRA extension in next version + ("building", "trullo"): "RES1", + ("building", "university"): "EDU3", + ("building", "warehouse"): "COM2", + ("building:type", "apartment_building"): "RES2", + ("building:type", "apartments"): "RES2", + ("building:type", "barn"): "AGR1", + ("building:type", "dwelling_house"): "RES1", + ("building:type", "garage"): "COM7", # Add extension RES7|COM7 in next version + ("building:type", "greenhouse"): "AGR3", + ("building:type", "house"): "RES1", + ("building:type", "residential"): "RES", + ("building:type", "semidetached_house"): "RES2A", + ("building:use", "commercial"): "COM", + ("building:use", "residential"): "RES", + ("building:use", "residential;industrial"): "MIX4", + ("building:use", "terrace"): "RES2", + ("historic", "monument"): "COM6", + ("historic", "castle"): "COM6", + ("historic", "ruins"): "COM6", + ("man_made", "storage_tank"): "IND", + ("office", "government"): "GOV1", + ("office", "company"): "COM3", + ("office", "yes"): "COM3", + ("office", "estate_agent"): "COM3", + ("office", "insurance"): "COM3", + ("office", "lawyer"): "COM3", + ("office", "educational_institution"): "EDU", + ("office", "telecommunication"): "COM3", + ("office", "association"): "COM3", + ("office", "ngo"): "COM3", + ("office", "diplomatic"): "GOV1", + ("office", "it"): "COM3", + ("office", "administrative"): "COM3", + ("office", "employment_agency"): "COM3", + ("office", "accountant"): "COM3", + ("office", "research"): "EDU4", + ("office", "religion"): "COM3", + ("office", "architect"): "COM3", + ("office", "financial"): "COM3", + ("office", "tax_advisor"): "COM3", + ("office", "newspaper"): "COM3", + ("office", "advertising_agency"): "COM3", + ("office", "notary"): "COM3", + ("office", "political_party"): "COM3", + ("office", "logistics"): "COM3", + ("office", "travel_agent"): "COM3", + ("office", "energy_supplier"): "COM3", + ("office", "therapist"): "COM3", + ("office", "foundation"): "COM3", + ("office", "physician"): "COM3", + ("office", "financial_advisor"): "COM3", + ("office", "consulting"): "COM3", + ("office", "water_utility"): "COM3", + ("office", "coworking"): "COM3", + ("office", "forestry"): "COM3", + ("office", "property_management"): "COM3", + ("office", "charity"): "COM3", + ("shop", "alcohol"): "COM1", + ("shop", "bakery"): "COM1", + ("shop", "beauty"): "COM1", + ("shop", "butcher"): "COM1", + ("shop", "car"): "COM1", + ("shop", "car_parts"): "COM1", + ("shop", "car_repair"): "COM1", + ("shop", "clothes"): "COM1", + ("shop", "convenience"): "COM1", + ("shop", "doityourself"): "COM1", + ("shop", "electronics"): "COM1", + ("shop", "florist"): "COM1", + ("shop", "furniture"): "COM1", + ("shop", "hairdresser"): "COM1", + ("shop", "hardware"): "COM1", + ("shop", "kiosk"): "COM1", + ("shop", "mall"): "COM1", + ("shop", "mobile_phone"): "COM1", + ("shop", "shoes"): "COM1", + ("shop", "supermarket"): "COM1", + ("shop", "yes"): "COM1", + ("shop", "variety_store"): "COM1", + ("shop", "optician"): "COM1", + ("shop", "jewelry"): "COM1", + ("shop", "gift"): "COM1", + ("shop", "greengrocer"): "COM1", + ("shop", "department_store"): "COM1", + ("shop", "books"): "COM1", + ("shop", "bicycle"): "COM1", + ("shop", "travel_agency"): "COM1", + ("shop", "chemist"): "COM1", + ("shop", "sports"): "COM1", + ("shop", "laundry"): "COM1", + ("shop", "confectionery"): "COM1", + ("shop", "stationery"): "COM1", + ("shop", "pet"): "COM1", + ("shop", "computer"): "COM1", + ("shop", "vacant"): "COM1", + ("shop", "tyres"): "COM1", + ("shop", "beverages"): "COM1", + ("shop", "newsagent"): "COM1", + ("shop", "dry_cleaning"): "COM1", + ("shop", "cosmetics"): "COM1", + ("shop", "motorcycle"): "COM1", + ("shop", "garden_centre"): "COM1", + ("shop", "funeral_directors"): "COM1", + ("shop", "copyshop"): "COM1", + ("shop", "tailor"): "COM1", + ("shop", "tobacco"): "COM1", + ("shop", "toys"): "COM1", + ("shop", "farm"): "COM1", + ("shop", "deli"): "COM1", + ("shop", "interior_decoration"): "COM1", + ("shop", "seafood"): "COM1", + ("shop", "massage"): "COM1", + ("shop", "ticket"): "COM1", + ("shop", "storage_rental"): "COM2", + ("shop", "trade"): "COM1", + ("shop", "houseware"): "COM1", + ("shop", "photo"): "COM1", + ("shop", "pastry"): "COM1", + ("shop", "wine"): "COM1", + ("shop", "outdoor"): "COM1", + ("shop", "paint"): "COM1", + ("shop", "general"): "COM1", + ("shop", "art"): "COM1", + ("shop", "bookmaker"): "COM1", + ("shop", "boutique"): "COM1", + ("shop", "charity"): "COM1", + ("shop", "pawnbroker"): "COM1", + ("shop", "second_hand"): "COM1", + ("shop", "fabric"): "COM1", + ("shop", "kitchen"): "COM1", + ("shop", "medical_supply"): "COM1", + ("shop", "tattoo"): "COM1", + ("tourism", "hotel"): "RES3", + ("tourism", "museum"): "COM6", + } + + @property + def overriding_occupancies(self): + """ + List of overriding occupancies + """ + + # If one of these occupancies exist in the total occupancy list, we should override the + # occupancy with this one. + return { + "COM10", + "COM9", + "COM8", + "COM4", + "GOV2", + "GOV1", + "COM6", + "ASS2", + "EDU2", + "EDU3", + "EDU4", + "RES3", + "ASS1", + "AGR1", + "AGR2", + "AGR3", + } diff --git a/building/02_process/occupancy/rule.xml b/building/02_process/occupancy/rule.xml new file mode 100644 index 0000000..795f5f8 --- /dev/null +++ b/building/02_process/occupancy/rule.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<rule name="OccupancyRule"> + <input> + <param type="dict">tags</param> + <param type="list">osm_spots</param> + <param type="list">osm_lands</param> + </input> + <function filepath="occupancy.py"/> + <output> + <param type="float">occupancy</param> + </output> +</rule> diff --git a/building/README.md b/building/README.md index f37ee15..73c012a 100644 --- a/building/README.md +++ b/building/README.md @@ -28,6 +28,10 @@ Finds the `building:levels` tag in the attributes of the building or one of the 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. +* **Occupancy** + +Define the occupancy type of the building. + ### Upsert rules * **ObmBuildingsUpsert** -- GitLab