Commit 61464a36 authored by Rick Gruber-Riemer's avatar Rick Gruber-Riemer

Make relations between building parts based on shared referenced nodes

parent 30a43ac3
......@@ -152,7 +152,7 @@ class Building(object):
# set during method called by init(...) through self.update_geometry and related sub-calls
self.refs = None # contains only the refs of the outer_ring
self.refs_shared = dict() # refs shared with other buildings (dict of index position, value False or True)
self.refs_shared = dict() # refs shared with other buildings (dict of index position, value list of osm_id's)
self.inner_rings_list = None
self.outer_nodes_closest = None
self.polygon = None # can have inner and outer rings, i.e. the real polygon
......@@ -439,8 +439,8 @@ class Building(object):
def has_neighbours(self) -> bool:
"""To know whether this building shares references (nodes) with other buildings"""
return len(self.refs_shared) > 0
"""To know whether this building shares at least 2 references (nodes) with other buildings"""
return len(self.refs_shared) > 1
def has_parent(self) -> bool:
......@@ -1274,6 +1274,9 @@ class BuildingParent(object):
child.parent = self
def contains_child(self, child: Building) -> bool:
return child in self.children
def add_tags(self, tags: Dict[str, str]) -> None:
"""The added tags are either from the outline if simple3d or otherwise from the original building
used as a parent for building_parts, if not relation was given."""
......@@ -1336,6 +1339,13 @@ class BuildingParent(object):
return building_parents
def transfer_children(self, other_parent: 'BuildingParent') -> None:
"""Transfer all children from this parent to another.
Once all children have a new parent then there will be no pointers to this one -> no del of children necessary.
for child in self.children:
def clean_building_parents_dangling_children(my_buildings: List[Building]) -> None:
"""Make sure that buildings with a parent, which only has this child, gets no parent.
......@@ -1459,8 +1469,12 @@ def _relate_neighbours(buildings: List[Building]) -> None:
for pos_i in range(len(first_building.refs)):
for pos_j in range(len(second_building.refs)):
if first_building.refs[pos_i] == second_building.refs[pos_j]:
first_building.refs_shared[pos_i] = True
second_building.refs_shared[pos_j] = True
if pos_i not in first_building.refs_shared:
first_building.refs_shared[pos_i] = list()
if pos_j not in second_building.refs_shared:
second_building.refs_shared[pos_j] = list()
for b in buildings:
if b.has_neighbours:
......@@ -16,7 +16,7 @@ import os
import random
import textwrap
import time
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Tuple
import shapely.geometry as shg
import shapely.ops as sho
......@@ -492,6 +492,68 @@ def _process_building_parts(nodes_dict: Dict[int, op.Node],
len(building_parents), stats_original_removed)
def process_building_loose_parts(nodes_dict: Dict[int, op.Node], my_buildings: List[building_lib.Building]) -> None:
"""Checks whether some buildings actually should have the same parent based on shared references.
This is due to the fact that relations in OSM are not always done clearly - and building:parts are often
only related by geometry and not distinct modelling in OSM.
Therefore the following assumption is made:
* If a building shares 4 or more nodes with another building, then it is related and they get the same parent
* If a buildings shares 3 consecutive nodes and for each segment the length is more than 2 metres, then
they are related and get the same parent. In order not to depend on correct sequence etc. it is just assumed
that the distance between each of the 3 points must be more than 2 metres. It is very improbable that that
would not be the case for the distance between the 2 pints not directly connected. We take the risk.
new_relations = 0
for first_building in my_buildings:
potential_attached =
ref_set_first = set(first_building.refs)
for second_building in potential_attached:
if first_building.osm_id == second_building.osm_id: # do not compare with self
if first_building.parent is not None and first_building.parent.contains_child(second_building):
continue # existing relationship, nothing to add
ref_set_second = set(second_building.refs)
if ref_set_first.isdisjoint(ref_set_second):
continue # not related (actually could be related if within, but then _process_building_parts())
common_refs = list()
for pos_i in range(len(first_building.refs)):
for pos_j in range(len(second_building.refs)):
if first_building.refs[pos_i] == second_building.refs[pos_j]:
# now check our requirements
if len(common_refs) < 3:
new_relationship = False
if len(common_refs) > 3:
new_relationship = True
node_0 = nodes_dict[common_refs[0]]
node_1 = nodes_dict[common_refs[1]]
node_2 = nodes_dict[common_refs[2]]
len_side_1 = coordinates.calc_distance_global(node_0.lon,, node_1.lon,
len_side_2 = coordinates.calc_distance_global(node_1.lon,, node_2.lon,
len_side_3 = coordinates.calc_distance_global(node_2.lon,, node_0.lon,
if len_side_1 > 2 and len_side_2 > 2 and len_side_3 > 2:
new_relationship = True
if new_relationship:
new_relations += 1
if first_building.parent and second_building.parent:
# need to decide on one parent (we pick first) and transfer all children from other parent
elif first_building.parent:
elif second_building.parent:
my_parent = building_lib.BuildingParent(op.get_next_pseudo_osm_id(
op.OSMFeatureType.building_relation), False)
my_parent.add_child(second_building)'Created %i new building relations based on shared references', new_relations)
def _process_osm_building(nodes_dict: Dict[int, op.Node], ways_dict: Dict[int, op.Way],
coords_transform: coordinates.Transformation) -> Dict[int, building_lib.Building]:
my_buildings = dict()
......@@ -612,7 +674,8 @@ def _write_obstruction_lights(path: str, file_name: str,
return False
def construct_buildings_from_osm(coords_transform: coordinates.Transformation) -> List[building_lib.Building]:
def construct_buildings_from_osm(coords_transform: coordinates.Transformation) -> Tuple[List[building_lib.Building],
Dict[int, op.Node]]:
osm_read_results = op.fetch_osm_db_data_ways_keys([s.K_BUILDING, s.K_BUILDING_PART])
osm_read_results = op.fetch_osm_db_data_relations_buildings(osm_read_results)
osm_nodes_dict = osm_read_results.nodes_dict
......@@ -634,7 +697,7 @@ def construct_buildings_from_osm(coords_transform: coordinates.Transformation) -
_ = utilities.time_logging('Time used in seconds for processing building parts', last_time)
# for convenience change to list from dict
return list(the_buildings.values())
return list(the_buildings.values()), osm_nodes_dict
def _debug_building_list_lsme(coords_transform: coordinates.Transformation, file_writer, list_elev: float) -> None:
......@@ -735,7 +735,7 @@ def process(transformer: Transformation, airports: List[aptdat_io.Airport]) -> T
aerodrome_zones = m.process_aerodrome_refs(transformer)
building_zones = m.process_osm_building_zone_refs(transformer)
urban_places, farm_places = m.process_osm_place_refs(transformer)
osm_buildings = bu.construct_buildings_from_osm(transformer)
osm_buildings, building_nodes_dict = bu.construct_buildings_from_osm(transformer)
highways_dict = m.process_osm_highway_refs(transformer)
railways_dict = m.process_osm_railway_refs(transformer)
waterways_dict = m.process_osm_waterway_refs(transformer)
......@@ -855,6 +855,11 @@ def process(transformer: Transformation, airports: List[aptdat_io.Airport]) -> T
last_time = time_logging("Time used in seconds for guessing zone types", last_time)
# =========== See whether we can do more building relations ============================
# this is done as late as possible to reduce exec time by only looking in the building's same zone
bu.process_building_loose_parts(building_nodes_dict, osm_buildings)
last_time = time_logging('Time used in seconds for processing building loose parts', last_time)
# =========== FINALIZE Land-use PROCESSING =============================================
if parameters.DEBUG_PLOT_LANDUSE:'Start of plotting zones')
......@@ -85,7 +85,7 @@ def separate_hipped(ac_object: ac.Object, b, roof_mat_idx: int) -> None:
def separate_gable(ac_object, b, roof_mat_idx: int, facade_mat_idx: int, inward_meters=0.) -> None:
"""Gabled roof (or hipped if inward_meters > 0) with 4 nodes."""
"""Gabled or gambrel roof (or hipped if inward_meters > 0) with 4 nodes."""
t = b.roof_texture
if b.roof_height:
......@@ -736,7 +736,7 @@ def simplify_balconies(original: shg.Polygon, distance_tolerance_line: float,
to_remove_points.add(_safe_index(counter + i, num_coords))
for i in range(counter + 5, num_coords):
if i in refs_shared:
refs_shared[i - 4] = True
refs_shared[i - 4] = refs_shared[i]
del refs_shared[i]
counter += 4
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment