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

New ratio_dict parameter to determine levels in buildings

parent c084e3d2
This diff is collapsed.
......@@ -15,7 +15,7 @@ import logging
import os
import parameters
import utils.logging as ulog
import utils.log_helper as ulog
import utils.utilities
from utils.vec2d import Vec2d
from utils.stg_io2 import STGVerbType
......
......@@ -157,15 +157,31 @@ In OSM the height of a building can be described using the following keys:
* ``roof:levels`` (not used in osm2city)
* ``levels``
Most often none of these features are tagged and then the number of levels are determined based on the settlement type and the corresponding ``BUILDING_NUMBER_LEVELS_* `` parameter. The height is always calculated as the product of the number of levels times parameter ``BUILDING_LEVEL_HEIGHT_*``. If only the height is given, then the levels are calculated by simple rounding — and this level value is then used for calculating the height. The reason for this is that some uniformity in building heights/values is normally observed in the real world — and because textures have a defined height per level.
Most often none of these features are tagged and then the number of levels are determined based on the settlement type and the corresponding ``BUILDING_NUMBER_LEVELS_* `` parameter. The height is always calculated as the product of the number of levels times parameter ``BUILDING_LEVEL_HEIGHT_*``. If only the height is given, then the levels are calculated by simple rounding — and this level value is then used for calculating the height. The reason for this is that some uniformity in building heights/values is normally observed in the real world — and because the generic textures used have a defined height per level.
An exception to this is made for building parts in a relationship (`Simple 3D buildings`_), as the heights in this case might be necessary to be correct (e.g. a dome on a church).
There is some randomness about the number of levels within the same settlement type, which is determinded by using a dictionary of level=ratio pairs, like:
The distribution is using Python random triangular_, where low <= N <= high and the specified mode between those bounds.
::
BUILDING_NUMBER_LEVELS_CENTRE = {4: 0.2, 5: 0.7, 6: 0.1}
meaning that there is a ratio 0f 0.2 for 4 levels, a ratio of 0.7 for 5 levels and a ratio of 0.1 for 6 levels. I.e. the keys are integers for the number of levels and the values are the ratio, where the sum of ratios must be 1.
============================================= ======== ======= ==============================================================================
Parameter Type Default Description / Example
============================================= ======== ======= ==============================================================================
BUILDING_NUMBER_LEVELS_* Dict . A dictonary of level/ratio pairs per settlement type, which is used when a
building does not contain information about the number of levels.
BUILDING_LEVEL_HEIGHT_URBAN Number 3.5 The height per level. This value should not be changed unless special textures
are used. For settlement types ``centre``, ``block`` and ``dense``.
BUILDING_LEVEL_HEIGHT_RURAL Number 2.5 Ditto for settlement types ``periphery`` and ``rural``.
============================================= ======== ======= ==============================================================================
.. _triangular: https://docs.python.org/3.5/library/random.html#random.triangular
.. _Simple 3D buildings: http://wiki.openstreetmap.org/wiki/Simple_3D_buildings
......@@ -176,7 +192,7 @@ European Style Inner Cities (Experimental)
------------------------------------------
Given the available textures in ``osm2city-data`` and the limited tagging of buildings in OSM as of fall 2017, European cities look wrong, because there are too many modern facades used and too many flat roofs.
Given the available textures in ``osm2city-data`` and the in general limited tagging of buildings in OSM as of 201x, European cities look wrong, because there are too many modern facades used and too many flat roofs.
The following parameters try to "fix" this by adding OSM-tags ``roof:colour=red`` and ``roof:shape=gabled`` to all those buildings, which do not have parents or pseudo-parents (i.e. nor relationships or parts in OSM), but which share node references with other buildings. So typically what is happening in blocks in inner cities in Europe.
......@@ -188,10 +204,6 @@ Parameter Type Default Description
BUILDING_FORCE_EUROPEAN_INNER_CITY_STYLE Boolean False If True then some OSM tags are enforced to better simulate European style
buildings - especially in inner cities.
BUILDING_FORCE_EUROPEAN_MAX_LEVEL Integer 5 If the buildings is tagged with more levels than this parameter or the
corresponding height of levels * BUILDING_CITY_LEVEL_HEIGHT_HIGH, then the
OSM tags are not enforced.
============================================= ======== ======= ==============================================================================
Example of using the flag set to True in a part of Prague:
......@@ -454,14 +466,10 @@ This operations complements land-use information from OSM based on some simple h
============================================= ======== ======= ==============================================================================
Parameter Type Default Description / Example
============================================= ======== ======= ==============================================================================
OWBB_GENERATE_LANDUSE Boolean False Create land-use based on building clusters outside of existing land-use
information (OSM).
OWBB_GENERATE..._BUILDING_BUFFER_DISTANCE Number 30 The minimum buffering distance around a building.
OWBB_GENERATE..._BUILDING_BUFFER_DISTANCE_MAX Number 50 The maximum buffering distance around a building. The actual value is a
function of the previous parameter and the building's size (the larger the
building the larger the buffering distance - up to this max value.
OWBB_GENERATE_LANDUSE_LANDUSE_MIN_AREA Number 5000 The minimum area in square metres of a generated land-use. Otherwise it is
discarded.
OWBB_GENERATE_LANDUSE_LANDUSE_HOLES_MIN_AREA Number 20000 The minimum area for a hole within a generated land-use that is kept as a
hole (square metres).
OWBB_GENERATE..._SIMPLIFICATION_TOLERANCE Number 20 The tolerance in metres used for simplifying the geometry of the generated
......
......@@ -13,7 +13,7 @@ import numpy as np
import parameters
import pySkeleton.polygon as polygon
import utils.logging as ulog
import utils.log_helper as ulog
from utils import utilities
from utils.vec2d import Vec2d
......
......@@ -65,22 +65,21 @@ def _generate_building_zones_from_buildings(building_zones: List[m.BuildingZone]
kept_candidates = list()
for candidate in zones_candidates.values():
if candidate.osm_id in merged_candidate_ids:
continue
if candidate.geometry.area >= parameters.OWBB_GENERATE_LANDUSE_LANDUSE_MIN_AREA:
candidate.geometry = candidate.geometry.simplify(parameters.OWBB_GENERATE_LANDUSE_SIMPLIFICATION_TOLERANCE)
# remove interior holes, which are too small
if len(candidate.geometry.interiors) > 0:
new_interiors = list()
for interior in candidate.geometry.interiors:
interior_polygon = Polygon(interior)
logging.debug("Hole area: %f", interior_polygon.area)
if interior_polygon.area >= parameters.OWBB_GENERATE_LANDUSE_LANDUSE_HOLES_MIN_AREA:
new_interiors.append(interior)
logging.debug("Number of holes reduced: from %d to %d",
len(candidate.geometry.interiors), len(new_interiors))
replacement_polygon = Polygon(shell=candidate.geometry.exterior, holes=new_interiors)
candidate.geometry = replacement_polygon
kept_candidates.append(candidate)
continue # do not keep merged candidates
candidate.geometry = candidate.geometry.simplify(parameters.OWBB_GENERATE_LANDUSE_SIMPLIFICATION_TOLERANCE)
# remove interior holes, which are too small
if len(candidate.geometry.interiors) > 0:
new_interiors = list()
for interior in candidate.geometry.interiors:
interior_polygon = Polygon(interior)
logging.debug("Hole area: %f", interior_polygon.area)
if interior_polygon.area >= parameters.OWBB_GENERATE_LANDUSE_LANDUSE_HOLES_MIN_AREA:
new_interiors.append(interior)
logging.debug("Number of holes reduced: from %d to %d",
len(candidate.geometry.interiors), len(new_interiors))
replacement_polygon = Polygon(shell=candidate.geometry.exterior, holes=new_interiors)
candidate.geometry = replacement_polygon
kept_candidates.append(candidate)
logging.debug("Candidate land-uses with sufficient area found: %d", len(kept_candidates))
# make sure that new generated buildings zones do not intersect with other building zones
......@@ -123,8 +122,6 @@ def _split_multipolygon_generated_building_zone(zone: m.GeneratedBuildingZone) -
for my_split_generated in new_generated:
if my_split_generated.from_buildings and len(my_split_generated.osm_buildings) == 0:
continue
if my_split_generated.geometry.area < parameters.OWBB_GENERATE_LANDUSE_LANDUSE_MIN_AREA/2:
continue
split_zones.append(my_split_generated)
logging.debug("Added sub-polygon with area %d and %d buildings", my_split_generated.geometry.area,
len(my_split_generated.osm_buildings))
......@@ -173,7 +170,8 @@ def _assign_city_blocks(building_zone: m.BuildingZone, highways_dict: Dict[int,
if intersecting_highways:
buffers = list()
for highway in intersecting_highways:
buffers.append(highway.geometry.buffer(parameters.OWBB_CITY_BLOCK_HIGHWAY_BUFFER, cap_style=CAP_STYLE.square,
buffers.append(highway.geometry.buffer(parameters.OWBB_CITY_BLOCK_HIGHWAY_BUFFER,
cap_style=CAP_STYLE.square,
join_style=JOIN_STYLE.bevel))
geometry_difference = building_zone.geometry.difference(unary_union(buffers))
if isinstance(geometry_difference, Polygon) and geometry_difference.is_valid and \
......@@ -340,6 +338,7 @@ def _link_building_zones_with_settlements(settlement_clusters: List[m.Settlement
logging.debug('%i out of %i building_zones', z, z_number)
# within because lit-areas are always
if zone.geometry.within(settlement.geometry):
zone.settlement_type = bl.SettlementType.periphery
# create city blocks
_assign_city_blocks(zone, highways_dict)
# Test for being within a settlement type circle beginning with the highest ranking circles.
......@@ -349,21 +348,31 @@ def _link_building_zones_with_settlements(settlement_clusters: List[m.Settlement
for circle in centre_circles:
if not city_block.geometry.disjoint(circle):
intersecting = True
city_block.settlement_type = m.SettlementType.centre
city_block.settlement_type = bl.SettlementType.centre
break
if not intersecting:
for circle in block_circles:
if not city_block.geometry.disjoint(circle):
intersecting = True
city_block.settlement_type = m.SettlementType.block
city_block.settlement_type = bl.SettlementType.block
break
if not intersecting:
for circle in dense_circles:
if not city_block.geometry.disjoint(circle):
city_block.settlement_type = m.SettlementType.dense
city_block.settlement_type = bl.SettlementType.dense
break
def count_zones_related_buildings(buildings: List[bl.Building], text: str) -> None:
total_related = 0
for building in buildings:
if building.zone:
total_related += 1
logging.info('%i out of %i buildings are related to zone for %s', total_related, len(buildings), text)
def process(transformer: Transformation) -> Tuple[List[Polygon], List[bl.Building]]:
last_time = time.time()
......@@ -408,16 +417,19 @@ def process(transformer: Transformation) -> Tuple[List[Polygon], List[bl.Buildin
buildings_outside.append(candidate)
last_time = time_logging("Time used in seconds for assigning buildings to OSM zones", last_time)
if parameters.OWBB_GENERATE_LANDUSE:
_generate_building_zones_from_buildings(building_zones, buildings_outside)
_generate_building_zones_from_buildings(building_zones, buildings_outside)
del buildings_outside
last_time = time_logging("Time used in seconds for generating building zones", last_time)
count_zones_related_buildings(osm_buildings, 'after building generation')
# =========== CREATE POLYGONS FOR LIGHTING OF STREETS ================================
# Needs to be before finding city blocks as we need the boundary
lit_areas = _process_landuse_for_lighting(building_zones)
last_time = time_logging("Time used in seconds for finding lit areas", last_time)
count_zones_related_buildings(osm_buildings, 'after lighting')
# =========== MAKE SURE GENERATED LAND-USE DOES NOT CROSS MAJOR LINEAR OBJECTS =======
if parameters.OWBB_SPLIT_MADE_UP_LANDUSE_BY_MAJOR_LINES:
# finally split generated zones by major transport lines
......@@ -425,13 +437,18 @@ def process(transformer: Transformation) -> Tuple[List[Polygon], List[bl.Buildin
railways_dict, waterways_dict)
last_time = time_logging("Time used in seconds for splitting building zones by major lines", last_time)
count_zones_related_buildings(osm_buildings, 'after split major lines')
# =========== Link urban places with lit_area buffers ==================================
if parameters.FLAG_2018_3:
settlement_clusters = _create_settlement_clusters(lit_areas, urban_places)
last_time = time_logging('Time used in seconds for creating settlement_clusters', last_time)
settlement_clusters = _create_settlement_clusters(lit_areas, urban_places)
last_time = time_logging('Time used in seconds for creating settlement_clusters', last_time)
_link_building_zones_with_settlements(settlement_clusters, building_zones, highways_dict)
last_time = time_logging('Time used in seconds for linking building zones with settlement_clusters', last_time)
count_zones_related_buildings(osm_buildings, 'after settlement clusters')
_link_building_zones_with_settlements(settlement_clusters, building_zones, highways_dict)
last_time = time_logging('Time used in seconds for linking building zones with settlement_clusters', last_time)
count_zones_related_buildings(osm_buildings, 'after settlement linking')
# ============ Finally guess the land-use type ========================================
for my_zone in building_zones:
......@@ -452,6 +469,8 @@ def process(transformer: Transformation) -> Tuple[List[Polygon], List[bl.Buildin
waterways_dict)
osm_buildings.extend(generated_buildings)
count_zones_related_buildings(osm_buildings, 'after generating buildings')
# =========== WRITE TO CACHE AND RETURN
if parameters.OWBB_LANDUSE_CACHE:
try:
......
......@@ -182,16 +182,6 @@ class Place(OSMFeature):
return centre_circle, block_circle, dense_circle
@unique
class SettlementType(IntEnum):
"""Only assigned to city blocks, not building zones."""
centre = 1
block = 2
dense = 3
periphery = 4 # default within lit area
rural = 5 # only implicitly used for building zones without city blocks.
class SettlementCluster:
"""A polygon based on lit_area representing a settlement cluster.
Built-up areas can sprawl and a coherent area can contain several cities and towns."""
......@@ -335,13 +325,28 @@ class CityBlock:
self.geometry = geometry
self.building_zone_type = feature_type
self.osm_buildings = list() # List of already existing osm buildings
self.settlement_type = SettlementType.periphery
self.__settlement_type = None
self.settlement_type = building_lib.SettlementType.periphery
self.__building_levels = 0
def relate_building(self, building: building_lib.Building) -> None:
"""Link the building to this zone and link this zone to the building."""
self.osm_buildings.append(building)
building.zone = self
@property
def building_levels(self) -> int:
return self.__building_levels
@property
def settlement_type(self) -> building_lib.SettlementType:
return self.__settlement_type
@settlement_type.setter
def settlement_type(self, value):
self.__settlement_type = value
self.__building_levels = building_lib.calc_levels_for_settlement_type(value)
class BuildingZone(OSMFeatureArea):
""" A 'Landuse' OSM map feature
......@@ -362,6 +367,7 @@ class BuildingZone(OSMFeatureArea):
self.generated_buildings = list() # List of GenBuilding objects for generated non-osm buildings
self.linked_genways = list() # List of Highways that are available for generating buildings
self.linked_city_blocks = list()
self.settlement_type = building_lib.SettlementType.rural
@classmethod
def create_from_way(cls, way: op.Way, nodes_dict: Dict[int, op.Node],
......@@ -392,14 +398,17 @@ class BuildingZone(OSMFeatureArea):
def commit_temp_gen_buildings(self, temp_buildings, highway, is_reverse) -> None:
"""Commits a set of generated buildings to be definitively be part of a BuildingZone"""
self.linked_blocked_areas.extend(temp_buildings.generated_blocked_areas)
for building in temp_buildings.generated_buildings:
self.generated_buildings.append(building)
building.zone = self
if is_reverse and highway.reversed_city_block:
highway.reversed_city_block.relate_building(building)
elif not is_reverse:
for gen_building in temp_buildings.generated_buildings:
# relate to zone
self.generated_buildings.append(gen_building)
gen_building.zone = self
# relate to city block if available (we do not need to relate GenBuilding to CityBlock
if is_reverse:
if highway.reversed_city_block:
gen_building.zone = highway.reversed_city_block
else:
if highway.along_city_block:
highway.along_city_block.relate_building(building)
gen_building.zone = highway.along_city_block
def relate_building(self, building: building_lib.Building) -> None:
"""Link the building to this zone and link this zone to the building."""
......@@ -444,7 +453,7 @@ class BuildingZone(OSMFeatureArea):
break
logging.debug('Linked around &i out of %i high ways to city blocks in building zone %i', linked,
int(linked/len(self.linked_genways)), self.osm_id)
len(self.linked_genways), self.osm_id)
class GeneratedBuildingZone(BuildingZone):
......@@ -1074,6 +1083,7 @@ class GenBuilding(object):
self.y = 0 # the y coordinate of the mid-point in relation to the local coordinate system
self.angle = 0 # the angle in degrees from North (y-axis) in the local coordinate system for the building's
# static object local x-axis
self.zone = None # either a BuildingZone or a CityBlock
def _create_area_polygons(self, highway_width: float) -> None:
"""Creates polygons at (0,0) and no angle"""
......@@ -1121,6 +1131,7 @@ class GenBuilding(object):
moved = saf.translate(rotated, self.x, self.y)
my_building = building_lib.Building(self.gen_id, self.shared_model.building_model.tags,
moved.exterior, '')
my_building.zone = self.zone
return my_building
......
......@@ -24,7 +24,8 @@ import unittest
import textures.road
import utils.vec2d as v
import utils.calc_tile as ct
import utils.logging as ulog
import utils.log_helper as ulog
import utils.utilities as uu
# default_args_start # DO NOT MODIFY THIS LINE
# -*- coding: utf-8 -*-
......@@ -139,28 +140,19 @@ RECTIFY_90_TOLERANCE = 0.1
# Force European style inner cities with gables and red tiles
BUILDING_FORCE_EUROPEAN_INNER_CITY_STYLE = False
BUILDING_FORCE_EUROPEAN_MAX_LEVEL = 5 # If a building has more levels (or level*BUILDING_CITY_LEVEL_HEIGHT_HIGH height), then this is not applied
BUILDING_FAKE_AMBIENT_OCCLUSION = True # -- fake AO by darkening facade textures towards the ground, using
BUILDING_FAKE_AMBIENT_OCCLUSION_HEIGHT = 6. # 1 - VALUE * exp(- AGL / HEIGHT )
BUILDING_FAKE_AMBIENT_OCCLUSION_VALUE = 0.6
# -- Parameters which influence the height of buildings if no info from OSM is available.
# It uses a triangular distribution (see http://en.wikipedia.org/wiki/Triangular_distribution)
BUILDING_CITY_LEVELS_LOW = 2.0
BUILDING_CITY_LEVELS_MODE = 3.5
BUILDING_CITY_LEVELS_HIGH = 5.0
BUILDING_CITY_LEVEL_HEIGHT_LOW = 3.1
BUILDING_CITY_LEVEL_HEIGHT_MODE = 3.3
BUILDING_CITY_LEVEL_HEIGHT_HIGH = 3.6
# FIXME: above should be removed after FLAG_2018_3
BUILDING_NUMBER_LEVELS_CENTRE = {4: 0.2, 5: 0.7, 6: 0.1}
BUILDING_NUMBER_LEVELS_BLOCK = {4: 0.4, 5: 0.6}
BUILDING_NUMBER_LEVELS_DENSE = {3: 0.2, 4: 0.6, 5: 0.15, 6: 0.05}
BUILDING_NUMBER_LEVELS_PERIPHERY = {1: 0.3, 2: 0.65, 3: 0.05}
BUILDING_NUMBER_LEVELS_RURAL = {1: 0.3, 2: 0.7}
BUILDING_LEVEL_HEIGHT_URBAN = 3.5 # this value should not be changed unless special textures are used
BUILDING_LEVEL_HEIGHT_RURAL = 2.5 # ditto
BUILDING_LEVEL_HEIGHT_RURAL = 2.5 # ditto including periphery
BUILDING_USE_SHARED_WORSHIP = False # try to use shared models for worship buildings
......@@ -294,7 +286,6 @@ TEXTURES_EMPTY_LM_RGB_VALUE = 35
# ==================== BUILD-ZONES GENERATION ============
OWBB_LANDUSE_CACHE = False
OWBB_GENERATE_LANDUSE = False # from buildings outside of existing land-use zones
OWBB_GENERATE_LANDUSE_BUILDING_BUFFER_DISTANCE = 30
OWBB_GENERATE_LANDUSE_BUILDING_BUFFER_DISTANCE_MAX = 50
OWBB_GENERATE_LANDUSE_LANDUSE_MIN_AREA = 5000
......@@ -410,12 +401,16 @@ def _check_ratio_dict_parameter(ratio_dict: typing.Optional[typing.Dict], name:
if len(ratio_dict) == 0:
raise ValueError('Parameter %s must not be an empty dict'.format(name))
total = 0.
prev_key = -9999
for key, ratio in ratio_dict.items():
if not isinstance(key, int):
raise ValueError('key {} in parameter {} must be an int'.format(str(key), name))
if prev_key > key:
raise ValueError('key {} in parameter {} must be larger than previous key'.format(str(key), name))
if not isinstance(ratio, float):
raise ValueError('ratio {} for key {} in param {} must be a float'.format(str(ratio), str(key), name))
total += ratio
prev_key = key
if abs(total - 1) > 0.001:
raise ValueError('The total of all ratios in param {} must be 1'.format(name))
......@@ -510,8 +505,8 @@ def set_boundary(boundary_west: float, boundary_south: float,
In most situations should be called after method read_from_file().
"""
try:
utils.check_boundary(boundary_west, boundary_south, boundary_east, boundary_north)
except utils.BoundaryError as be:
uu.check_boundary(boundary_west, boundary_south, boundary_east, boundary_north)
except uu.BoundaryError as be:
logging.error(be.message)
sys.exit(1)
......@@ -561,6 +556,9 @@ class TestParameters(unittest.TestCase):
with self.assertRaises(ValueError):
_check_ratio_dict_parameter(my_ratio_dict, 'my_ratio_dict')
my_ratio_dict = {1: 0.01, 2: 1.}
with self.assertRaises(ValueError):
_check_ratio_dict_parameter(my_ratio_dict, 'my_ratio_dict')
my_ratio_dict = {2: 0.01, 1: .99}
with self.assertRaises(ValueError):
_check_ratio_dict_parameter(my_ratio_dict, 'my_ratio_dict')
my_ratio_dict = {1: 0.01, 2: 0.99}
......
......@@ -10,6 +10,7 @@ import math
import os
import os.path as osp
import pickle
import random
import subprocess
import sys
import textwrap
......@@ -23,7 +24,7 @@ import shapely.geometry as shg
import parameters
import utils.coordinates as co
import utils.logging as ulog
import utils.log_helper as ulog
import utils.osmparser as op
import utils.vec2d as ve
......@@ -541,6 +542,23 @@ def bounds_from_list(bounds_list: List[Tuple[float, float, float, float]]) -> Tu
return min_x, min_y, max_x, max_y
def random_value_from_ratio_dict_parameter(ratio_parameter: Dict[int, float]) -> int:
target_ratio = random.random()
return value_from_ratio_dict_parameter(target_ratio, ratio_parameter)
def value_from_ratio_dict_parameter(target_ratio: float, ratio_parameter: Dict[int, float]) -> int:
"""Finds the key value closet to and below the target ratio."""
total_ratio = 0.
return_value = 0
for key, ratio in ratio_parameter.items():
if target_ratio <= total_ratio:
return return_value
return_value = key
total_ratio += ratio
return return_value
def time_logging(message: str, last_time: float) -> float:
current_time = time.time()
logging.info(message + ": %f", current_time - last_time)
......@@ -718,3 +736,11 @@ class TestUtilities(unittest.TestCase):
def check_boundary_pass(self):
self.assertEqual(None, check_boundary(-2, -3, 1, -2))
def test_value_from_ratio_dict_parameter(self):
ratio_parameter = {1: 0.2, 2: 0.3, 3: 0.5}
self.assertEqual(1, value_from_ratio_dict_parameter(0.1, ratio_parameter))
self.assertEqual(1, value_from_ratio_dict_parameter(0.2, ratio_parameter))
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))
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