Commit 5c9a421f authored by Rick Gruber-Riemer's avatar Rick Gruber-Riemer

* Simplification of buildings with "balconies" and related two new parameters

* Changing roof_height if not in OSM tags: if angle given, then use that, otherwise use the level_height of the building
parent 8d7e068c
......@@ -346,12 +346,15 @@ class Building(object):
self.outer_nodes_closest = [y for (y, x) in yx]
self._set_polygon(self.polygon.exterior, self.inner_rings_list)
def simplify(self, tolerance): # TODO: not used
original_nodes = self.pts_outer_count + len(self.pts_inner)
self.polygon = self.polygon.simplify(tolerance)
nnodes_simplified = original_nodes - (self.pts_outer_count + len(self.pts_inner))
# FIXME: simplify interiors
return nnodes_simplified
def simplify(self) -> int:
"""Simplifies the geometry, but only if no inners and if 2 neighbouring points."""
if self.has_inner:
return 0
original_number = len(self.polygon.exterior.coords)
self.polygon = utilities.simplify_balconies(self.polygon, parameters.BUILDING_SIMPLIFY_TOLERANCE_LINE,
parameters.BUILDING_SIMPLIFY_TOLERANCE_AWAY)
simplified_number = len(self.polygon.exterior.coords)
return original_number - simplified_number
def _set_polygon(self, outer: shg.LinearRing, inner: List[shg.LinearRing]=list()) -> None:
self.polygon = shg.Polygon(outer, inner)
......@@ -382,6 +385,10 @@ class Building(object):
def pts_outer(self) -> List[Tuple[float, float]]:
return list(self.polygon.exterior.coords)[:-1]
@property
def has_inner(self) -> bool:
return len(self.polygon.interiors) > 0
@property
def pts_inner(self) -> List[Tuple[float, float]]:
return [coord for interior in self.polygon.interiors for coord in list(interior.coords)[:-1]]
......@@ -724,7 +731,10 @@ class Building(object):
allow_complex_roofs = True
# no complex roof on buildings with inner rings
if self.polygon.interiors:
allow_complex_roofs = False
if len(self.polygon.interiors) == 1:
self.roof_shape = roofs.RoofShape.skillion
else:
allow_complex_roofs = False
# no complex roof on large buildings
elif self.area > parameters.BUILDING_COMPLEX_ROOFS_MAX_AREA:
allow_complex_roofs = False
......@@ -801,15 +811,13 @@ class Building(object):
else:
if s.K_ROOF_ANGLE in self.tags:
angle = float(self.tags[s.K_ROOF_ANGLE])
while angle > 0:
temp_roof_height = tan(np.deg2rad(angle)) * (self.edge_length_pts[1] / 2)
if temp_roof_height < parameters.BUILDING_SKILLION_ROOF_MAX_HEIGHT:
break
angle -= 1
else:
angle = random.uniform(parameters.BUILDING_SKEL_ROOFS_MIN_ANGLE,
parameters.BUILDING_SKEL_ROOFS_MAX_ANGLE)
while angle > 0:
temp_roof_height = tan(np.deg2rad(angle)) * (self.edge_length_pts[1] / 2)
if temp_roof_height < parameters.BUILDING_SKILLION_ROOF_MAX_HEIGHT:
break
angle -= 1
temp_roof_height = calc_level_height_for_settlement_type(self.zone.settlement_type)
if s.K_ROOF_SLOPE_DIRECTION in self.tags:
# Input angle
......@@ -885,23 +893,21 @@ class Building(object):
# get roof:height given by osm
self.roof_height = utils.osmparser.parse_length(self.tags[s.K_ROOF_HEIGHT])
else:
# random roof:height
else: # roof:height based on heuristics
if self.roof_shape is roofs.RoofShape.flat:
self.roof_height = 0.
else:
if s.K_ROOF_ANGLE in self.tags:
angle = float(self.tags[s.K_ROOF_ANGLE])
else:
angle = random.uniform(parameters.BUILDING_SKEL_ROOFS_MIN_ANGLE,
parameters.BUILDING_SKEL_ROOFS_MAX_ANGLE)
while angle > 0:
temp_roof_height = tan(np.deg2rad(angle)) * (self.edge_length_pts[1] / 2)
if temp_roof_height < parameters.BUILDING_SKEL_ROOF_MAX_HEIGHT:
break
angle -= 5
if temp_roof_height > parameters.BUILDING_SKEL_ROOF_MAX_HEIGHT:
temp_roof_height = parameters.BUILDING_SKEL_ROOF_MAX_HEIGHT
while angle > 0:
temp_roof_height = tan(np.deg2rad(angle)) * (self.edge_length_pts[1] / 2)
if temp_roof_height < parameters.BUILDING_SKEL_ROOF_MAX_HEIGHT:
break
angle -= 5
if temp_roof_height > parameters.BUILDING_SKEL_ROOF_MAX_HEIGHT:
temp_roof_height = parameters.BUILDING_SKEL_ROOF_MAX_HEIGHT
else: # use the same as level height
temp_roof_height = calc_level_height_for_settlement_type(self.zone.settlement_type)
self.roof_height = temp_roof_height
def write_to_ac(self, ac_object: ac3d.Object, cluster_elev: float, cluster_offset: Vec2d,
......@@ -1278,11 +1284,12 @@ def analyse(buildings: List[Building], fg_elev: utilities.FGElev, stg_manager: u
b.enforce_european_style(building_parent)
if not b.is_external_model:
if building_parent is None: # do not simplify if in parent/child relationship
stats.nodes_simplified += b.simplify()
try:
# FIXME RICK stats.nodes_simplified += b.simplify(parameters.BUILDING_SIMPLIFY_TOLERANCE)
b.roll_inner_nodes()
except Exception as reason:
logging.warning("simplify or roll_inner_nodes failed (OSM ID %i, %s)", b.osm_id, reason)
logging.warning("Roll_inner_nodes failed (OSM ID %i, %s)", b.osm_id, reason)
continue
if not b.analyse_elev_and_water(fg_elev):
......
......@@ -120,6 +120,61 @@ NO_ELEV Boolean False The only re
Buildings
=========
.. _chapter-parameters-buildings-diverse
------------------
Diverse Parameters
------------------
Parameters which influence the number of buildings from OSM taken to output.
============================================= ======== ======= ==============================================================================
Parameter Type Default Description / Example
============================================= ======== ======= ==============================================================================
LOD_ALWAYS_DETAIL_BELOW_AREA Integer 150 Below this area, buildings will always be LOD detailed
BUILDING_MIN_HEIGHT Number 0.0 Minimum height from bottom to top without roof height of a building to be
included in output (does not include roof). Different from OSM tag
"min_height", which states that the bottom of the building hovers min_height
over the ground. If set to 0.0, then not taken into consideration (default).
BUILDING_MIN_AREA Number 50.0 Minimum area for a building to be included in output (not used for buildings
with parent).
BUILDING_PART_MIN_AREA Number 10.0 Minimum area for building:parts.
BUILDING_REDUCE_THRESHOLD Number 200.0 Threshold area of a building below which a rate of buildings gets reduced
from output.
BUILDING_REDUCE_RATE Number 0.5 Rate (between 0 and 1) of buildings below a threshold which get reduced
randomly in output.
BUILDING_REDUCE_CHECK_TOUCH Boolean False Before removing a building due to area, check whether it is touching another
building and therefore should be kept.
BUILDING_NEVER_SKIP_LEVELS Integer 6 Buildings that tall will never be skipped.
============================================= ======== ======= ==============================================================================
In order to reduce the total number of nodes of the buildings mesh and thereby reducing both disk space volume and rendering demands as well as to simplify the rendering of roofs, the geometry of buildings is simplified as follows:
* Only if not part of a building parent
* Only if no inner circles
* Only if a multiple of 4 nodes gets reduced and always 4 neighbouring points are removed at the same time (e.g. something that looks like a balcony from above, but can also point inwards into the building)
* If points get removed, which are also part of a neighbour building, then the simplification is not accepted.
* The tolerance of the below parameters is respected.
============================================= ======== ======= ==============================================================================
Parameter Type Default Description / Example
============================================= ======== ======= ==============================================================================
BUILDING_SIMPLIFY_TOLERANCE_LINE Number 1.0 The point on the base line may at most be this value away from the straight
line between the node before the balcony and the node after the balcony.
This in order to prevent that e.g. a stair-case feature is removed.
BUILDING_SIMPLIFY_TOLERANCE_AWAY Number 2.5 The 2 points sticking out (or in) may not be more than this value away from
the straight line between the node before the balcony and the node after the
balcony. This in order to prevent clearly visible "balconies" to be removed.
============================================= ======== ======= ==============================================================================
.. _chapter-parameters-lod-label:
-----------------------------
......
......@@ -117,7 +117,8 @@ BUILDING_REDUCE_THRESHOLD = 200.0 # -- threshold area of a building below whic
BUILDING_REDUCE_RATE = 0.5 # -- rate (between 0 and 1) of buildings below a threshold which get reduced randomly in output
BUILDING_REDUCE_CHECK_TOUCH = False # -- before removing a building due to area, check whether it is touching another building and therefore should be kept
BUILDING_NEVER_SKIP_LEVELS = 6 # -- buildings that tall will never be skipped
BUILDING_SIMPLIFY_TOLERANCE = 1.0 # -- all points in the simplified building will be within the tolerance distance of the original geometry.
BUILDING_SIMPLIFY_TOLERANCE_LINE = 1.0
BUILDING_SIMPLIFY_TOLERANCE_AWAY = 2.5
BUILDING_COMPLEX_ROOFS = True # -- generate complex roofs on buildings? I.e. other shapes than horizontal and flat
BUILDING_COMPLEX_ROOFS_MIN_LEVELS = 1 # don't put complex roof on buildings smaller than the specified value unless there is an explicit roof:shape flag
......
......@@ -238,7 +238,8 @@ class Stats(object):
textures_used_percent = 99.9
logging.info(textwrap.dedent("""
used tex %i out of %i (%2.0f %%)""" % (len(textures_used), len(self.textures_total), textures_used_percent)))
used tex %i out of %i (%2.0f %%)""" % (len(textures_used), len(self.textures_total),
textures_used_percent)))
logging.debug(textwrap.dedent("""
Used Textures : """))
for item in sorted(list(textures_used.items()), key=lambda item: item[1], reverse=True):
......@@ -668,6 +669,62 @@ def fit_offsets_for_rectangle_with_hull(angle: float, hull: shg.Polygon, model_l
return new_x, new_y
def _safe_index(index: int, number_of_elements: int) -> int:
"""Makes sure that if index flows over it continues at start"""
if index > number_of_elements - 1:
return index - number_of_elements
else:
return index
def simplify_balconies(original: shg.Polygon, distance_tolerance_line: float,
distance_tolerance_away: float) -> shg.Polygon:
"""Removes edges from polygons, which look like balconies on a building.
Removes always 4 points at a time.
Let us assume a building front with nodes 0, 1, .., 5 - where nodes [1, 2, 3, 4] would form a balcony. Then
after removing these four points the new front would be [0, 5]. To make sure that it was a balcony and we do
not remove e.g. something that looks like a staircase, we make sure that neither point 1 or 4 is very distant
from the new front -> parameter distance_tolerance_line.
Also to make sure it is not a too distinguishing feature by not letting the outer points 2 and 3 be too far
away from the new front.
"""
if len(original.exterior.coords) < 8: # at least a triangle with a balcony (plus an extra point to close)
return original
to_remove_points = set() # index positions
counter = 0
my_coords = list(original.exterior.coords)
del my_coords[-1] # we do not need the repeated first point closing the polygon
num_coords = len(my_coords)
while counter < len(my_coords):
valid_removal = False
base_line = shg.LineString([my_coords[counter], my_coords[_safe_index(counter + 5, num_coords)]])
my_point = shg.Point(my_coords[_safe_index(counter + 1, num_coords)])
if base_line.distance(my_point) < distance_tolerance_line:
my_point = shg.Point(my_coords[_safe_index(counter + 4, num_coords)])
if base_line.distance(my_point) < distance_tolerance_line:
my_point = shg.Point(my_coords[_safe_index(counter + 2, num_coords)])
if base_line.distance(my_point) < distance_tolerance_away:
my_point = shg.Point(my_coords[_safe_index(counter + 3, num_coords)])
if base_line.distance(my_point) < distance_tolerance_away:
valid_removal = True
if valid_removal:
for i in range(1, 5):
to_remove_points.add(_safe_index(counter + i, num_coords))
counter += 4
counter += 1
if len(to_remove_points) == 0:
return original
else:
ny_coords = list()
for i in range(len(my_coords)):
if i not in to_remove_points:
ny_coords.append(my_coords[i])
reduced_poly = shg.Polygon(shg.LinearRing(ny_coords))
return reduced_poly
# ================ PLOTTING FOR VISUAL TESTING =====
import utils.plot_utilities as pu
......@@ -744,3 +801,26 @@ class TestUtilities(unittest.TestCase):
self.assertEqual(2, value_from_ratio_dict_parameter(0.3, ratio_parameter))
self.assertEqual(2, value_from_ratio_dict_parameter(0.5, ratio_parameter))
self.assertEqual(3, value_from_ratio_dict_parameter(1., ratio_parameter))
def test_simplify_balconies(self):
# too few nodes
six_node_polygon = shg.Polygon([(0, 0), (10, 0), (10, 5), (9, 5), (9, 6), (0, 6)])
simplified_poly = simplify_balconies(six_node_polygon, 0.5, 2.)
self.assertEqual(6 + 1, len(simplified_poly.exterior.coords))
# balcony to remove
eight_node_polygon = shg.Polygon([(0, 0), (10, 0), (10, 5), (9, 5), (9, 6), (4, 6), (4, 5), (0, 5)])
simplified_poly = simplify_balconies(eight_node_polygon, 0.5, 2.)
self.assertEqual(4 + 1, len(simplified_poly.exterior.coords))
# balcony too far away (also checking inward)
eight_node_polygon = shg.Polygon([(0, 0), (1, 0), (1, 3), (3, 3), (3, 0), (10, 0), (10, 5), (0, 5)])
simplified_poly = simplify_balconies(eight_node_polygon, 0.5, 2.)
self.assertEqual(8 + 1, len(simplified_poly.exterior.coords))
# balcony base not close to line (and testing index=0 in the balcony)
eight_node_polygon = shg.Polygon([(4, 6), (0, 5), (0, 0), (10, 0), (10, 5), (9, 6), (9, 7), (4, 7)])
simplified_poly = simplify_balconies(eight_node_polygon, 0.5, 2.)
self.assertEqual(8 + 1, len(simplified_poly.exterior.coords))
# two balconies to remove
twelve_node_polygon = shg.Polygon([(0, 0), (1, 0), (1, 1), (3, 1), (3, 0), (10, 0), (10, 5), (9, 5), (9, 6),
(4, 6), (4, 5), (0, 5)])
simplified_poly = simplify_balconies(twelve_node_polygon, 0.5, 2.)
self.assertEqual(4 + 1, len(simplified_poly.exterior.coords))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment