Commit a670dbf7 authored by Rick Gruber-Riemer's avatar Rick Gruber-Riemer

* Including aerodrome data from apt.dat and OSM in order to have a basis for...

* Including aerodrome data from apt.dat and OSM in order to have a basis for different handling of airport buildings.
* Primitive facade finding for airport buildings
* Intro of new parameter OWBB_BUILT_UP_MIN_LIT_AREA to better control lit areas
parent f9a20b71
......@@ -135,7 +135,7 @@ def process_scenery_tile(scenery_tile: SceneryTile, params_file_name: str,
# run programs
lit_areas, osm_buildings = ol.process(the_coords_transform)
lit_areas, osm_buildings = ol.process(the_coords_transform, my_airports)
process_built_stuff = True # only relevant for and E.g. can still run
if lit_areas is None and osm_buildings is None:
process_built_stuff = False
......@@ -707,11 +707,19 @@ class Building(object):
import owbb.models
if isinstance(, owbb.models.CityBlock) and > 0:
elif isinstance(, owbb.models.BuildingZone) \
and is owbb.models.BuildingZoneType.aerodrome:
building_class = get_building_class(self.tags)
return calc_levels_for_settlement_type(, building_class)
def _calculate_level_height(self) -> float:
import owbb.models
if isinstance(, owbb.models.BuildingZone) \
and is owbb.models.BuildingZoneType.aerodrome:
building_class = get_building_class(self.tags)
if building_class in [BuildingClass.industrial, BuildingClass.warehouse]:
......@@ -1533,6 +1541,40 @@ def overlap_check_convex_hull(orig_buildings: List[Building], stg_entries: List[
return buildings_after_remove_with_parent_children(orig_buildings, buildings_to_remove)
def update_building_tags_in_aerodromes(my_buildings: List[Building]) -> None:
"""Make sure that buildings in aerodromes are tagged such that they look kind of modern."""
import owbb.models
# first run the parents to make sure that all buildings below a building parent get same aeroway tag
my_parents = set()
for building in my_buildings:
if building.parent is not None and isinstance(, owbb.models.BuildingZone) \
and is owbb.models.BuildingZoneType.aerodrome:
for building_parent in my_parents:
aeroway_values = list()
for child in building_parent.children:
if s.K_AEROWAY in child.tags:
settled_value = s.V_AERO_OTHER
if len(aeroway_values) == 1:
settled_value = aeroway_values[0] # in all other situations (0 or > 1) we do not know what to apply
for child in building_parent.children:
if s.K_AEROWAY not in child.tags:
child.tags[s.K_AEROWAY] = settled_value
# now do all buildings including roof to make stuff easy in processing
for building in my_buildings:
if isinstance(, owbb.models.BuildingZone) \
and is owbb.models.BuildingZoneType.aerodrome:
if s.K_ROOF_SHAPE not in building.tags:
building.tags[s.K_ROOF_SHAPE] = s.V_FLAT
if s.K_AEROWAY not in building.tags:
building.tags[s.K_AEROWAY] = s.V_AERO_OTHER
def _analyse_worship_building(building: Building, building_parent: BuildingParent,
stg_manager: utils.stg_io2.STGManager, fg_elev: utilities.FGElev,
coords_transform: co.Transformation) -> bool:
......@@ -620,6 +620,8 @@ def process_buildings(coords_transform: coordinates.Transformation, fg_elev: uti
# final check on building parent hierarchy
if not the_buildings:"No buildings after overlap check etc. Stopping further processing.")
......@@ -128,3 +128,14 @@ Node numbering for ``gambrel`` roof type:
.. _Simple 3D Buildings:
.. _Place:
.. _Key Place:
.. _chapter-aerodromes:
Data for runways and helipads are read from the ``apt.dat file`` in ``$FG_ROOT/Airports/apt.dat.gz``. This information is used to avoid having crossing OSM roads to be visible and potentially creating a considerable bump when a plane rolls over.
Data for airport boundaries are also read from the ``apt.dat`` file (not all airports have information about boundaries). This data is then merged with OSM data for ``aeroway=aerodrome`` (again not all airports might be modelled with a zone). The resulting land-use is making sure that buildings within these zones will look more like airport buildings: using flat roofs (unless the roof type is explicitely modelled in OSM) and using a modern facade texture. Also: no buildings are generated inside zones for aerodromes.
......@@ -438,9 +438,11 @@ Generating Buildings Where OSM is Missing Buildings
It is possible to let ``osm2city`` generate buildings, where it is plausible that there in reality would be buildings, but buildings were not mapped in OSM. The following set of parameters make some customisation to specific areas possible. However parts of the processing is rather hard-coded (e.g. the available buildings are defined in code in module ``owbb/`` in function ``_read_building_models_library()``. Still the results are much better than an empty scene.
No additional buildings are generated inside zones for aerodromes.
A lot of processing is dependent on land-use information (see e.g. :ref:`Land-use Handling <chapter-howto-land-use-label>` and :ref:`Land-use Parameters <chapter-parameters-landuse-label>`). For a short explanation of the process used see :ref:`Generate Would-Be Buildings <chapter-howto-generate-would-be-buildings-label>`.
In rural and periphery settlement areas an attempt is made to have the same terrace houses or apartment buildings along both sides of a way.
In settlement areas an attempt is made to have the same terrace houses or apartment buildings along both sides of a way.
The first set of parameters determines the overall placement heuristics:
......@@ -649,13 +651,16 @@ The resulting built-up areas are also used for finding city and town areas — a
============================================= ======== ======= ==============================================================================
Parameter Type Default Description / Example
============================================= ======== ======= ==============================================================================
BUILT_UP_AREA_LIT_BUFFER Number 100 The buffer distance around built-up land-use areas to be used for lighting of
OWBB_BUILT_UP_BUFFER Number 50 The buffer distance around built-up land-use areas to be used for lighting of
streets. The number is chosen pretty large such that as many building zone
clusters as possible are connected. Also it is not unusual that the lighting
of streets starts a bit outside of a built-up area.
BUILT_UP_AREA_LIT_HOLES_MIN_AREA Number 100000 The minimum area in square metres a hole in a LIT_BUFFER needs to have to be
OWBB_BUILT_UP_AREA_HOLES_MIN_AREA Number 100000 The minimum area in square metres a hole in a LIT_BUFFER needs to have to be
not lit. In general this can be quite a large value and larger than e.g.
OWBB_BUILT_UP_MIN_LIT_AREA Number 100000 The minimum area of a lit area, such that it is actually used for lighting of
streets. Given buffering with ``OWBB_BUILT_UP_BUFFER`` alone is already a
large area, this value must not be chosen too small.
============================================= ======== ======= ==============================================================================
......@@ -19,14 +19,54 @@ import parameters
import owbb.models as m
import owbb.plotting as plotting
import owbb.would_be_buildings as wbb
import utils.osmparser as op
import utils.aptdat_io as aptdat_io
import utils.btg_io as btg
import utils.calc_tile as ct
import utils.osmparser as op
from utils.coordinates import disjoint_bounds, Transformation
from utils.stg_io2 import scenery_directory_name, SceneryType
from utils.utilities import time_logging, merge_buffers
def _process_aerodromes(building_zones: List[m.BuildingZone], aerodrome_zones: List[m.BuildingZone],
airports: List[aptdat_io.Airport], transformer: Transformation) -> None:
"""Merges aerodromes from OSM and apt.dat and then cuts the areas from buildings zones.
Aerodromes might be missing in apt.dat or OSM (or both - which we cannot correct)"""
apt_dat_polygons = list()
# get polygons from apt.dat in local coordinates
for airport in airports:
if airport.within_boundary(parameters.BOUNDARY_WEST, parameters.BOUNDARY_SOUTH,
parameters.BOUNDARY_EAST, parameters.BOUNDARY_NORTH):
my_polys = airport.create_boundary_polygons(transformer)
if my_polys is not None:
# see whether some polygons can be merged to reduce the list
apt_dat_polygons = merge_buffers(apt_dat_polygons)
# merge these polygons with existing aerodrome_zones
for aerodrome_zone in aerodrome_zones:
for poly in reversed(apt_dat_polygons):
if poly.disjoint(aerodrome_zone.geometry) is False:
aerodrome_zone.geometry = aerodrome_zone.geometry.union(poly)
# for the remaining polygons create new aerodrome_zones
for poly in apt_dat_polygons:
new_aerodrome_zone = m.BuildingZone(op.get_next_pseudo_osm_id(op.OSMFeatureType.landuse),
poly, m.BuildingZoneType.aerodrome)
# make sure that if a building zone is overlapping with a aerodrome that it is clipped
for building_zone in building_zones:
for aerodrome_zone in aerodrome_zones:
if building_zone.geometry.disjoint(aerodrome_zone.geometry) is False:
building_zone.geometry = building_zone.geometry.difference(aerodrome_zone.geometry)
# finally add all aerodrome_zones to the building_zones as regular zone
def _generate_building_zones_from_external(building_zones: List[m.BuildingZone],
external_landuses: List[m.BTGBuildingZone]) -> None:
"""Adds "missing" building_zones based on land-use info outside of OSM land-use"""
......@@ -533,6 +573,8 @@ def _link_building_zones_with_settlements(settlement_clusters: List[m.Settlement
z = 0
z_number = len(building_zones)
for zone in building_zones:
if zone.type_ is m.BuildingZoneType.aerodrome:
z += 1
if z % 20 == 0:
logging.debug('Processing %i out of %i building_zones', z, z_number)
......@@ -569,7 +611,7 @@ def _link_building_zones_with_settlements(settlement_clusters: List[m.Settlement
# now make sure that also zones outside of settlements get city blocks
for zone in building_zones:
if zone not in zones_processed_in_settlement:
if zone not in zones_processed_in_settlement and zone.type_ is not m.BuildingZoneType.aerodrome:
_assign_city_blocks(zone, highways_dict)
......@@ -577,6 +619,8 @@ def _sanity_check_settlement_types(building_zones: List[m.BuildingZone], highway
upgraded = 0
downgraded = 0
for zone in building_zones:
if zone.type_ is m.BuildingZoneType.aerodrome:
my_density = zone.density
if my_density < parameters.OWBB_PLACE_SANITY_DENSITY:
if zone.settlement_type in [bl.SettlementType.dense, bl.SettlementType.block]:
......@@ -626,7 +670,8 @@ def _count_zones_related_buildings(buildings: List[bl.Building], text: str) -> N'%i out of %i buildings are related to zone %s', total_related, len(buildings), text)
def process(transformer: Transformation) -> Tuple[Optional[List[Polygon]], Optional[List[bl.Building]]]:
def process(transformer: Transformation, airports: List[aptdat_io.Airport]) -> Tuple[Optional[List[Polygon]],
last_time = time.time()
bounds = m.Bounds.create_from_parameters(transformer)
......@@ -649,6 +694,7 @@ def process(transformer: Transformation) -> Tuple[Optional[List[Polygon]], Optio"Loading of cache %s or %s failed (%s)", cache_file_la, cache_file_bz, reason)
# =========== READ OSM DATA =============
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)
......@@ -659,6 +705,10 @@ def process(transformer: Transformation) -> Tuple[Optional[List[Polygon]], Optio
last_time = time_logging("Time used in seconds for parsing OSM data", last_time)
# =========== PROCESS AERODROME INFORMATION ============================
_process_aerodromes(building_zones, aerodrome_zones, airports, transformer)
last_time = time_logging("Time used in seconds for processing aerodromes", last_time)
btg_building_zones = list()
if parameters.OWBB_USE_BTG_LANDUSE:
......@@ -703,6 +753,7 @@ def process(transformer: Transformation) -> Tuple[Optional[List[Polygon]], Optio
_count_zones_related_buildings(osm_buildings, 'after lighting')
# =========== REDUCE THE BUILDING_ZONES TO BE WITHIN BOUNDS ==========================
# This is needed such that no additional buildings would be generated outside of the tile boundary.
building_zones = _check_clipping_border(building_zones, bounds)
last_time = time_logging("Time used in seconds for clipping to boundary", last_time)
......@@ -730,6 +781,13 @@ def process(transformer: Transformation) -> Tuple[Optional[List[Polygon]], Optio
_count_zones_related_buildings(osm_buildings, 'after settlement linking')
# now that settlement areas etc. are done, we can reduce the lit areas to those having a minimum area
before_lit = len(lit_areas)
for lit_area in reversed(lit_areas):
if lit_area.area < parameters.OWBB_BUILT_UP_MIN_LIT_AREA:
lit_areas.remove(lit_area)'Reduced the number of lit areas from %i to %i.', before_lit, len(lit_areas))
# ============ Finally guess the land-use type ========================================
for my_zone in building_zones:
if isinstance(my_zone, m.GeneratedBuildingZone):
......@@ -286,6 +286,8 @@ class BuildingZoneType(IntEnum): # element names must match OSM values apart fr
# interpreted as one. See GeneratedBuildingZone.guess_building_zone_type
port = 60
aerodrome = 90 # key = aeroway, not landuse
non_osm = 100 # used for land-uses constructed with heuristics and not in original data from OSM
# FlightGear in BTG files
......@@ -374,6 +376,8 @@ class BuildingZone(OSMFeatureArea):
def parse_tags(tags_dict: KeyValueDict) -> Union[None, BuildingZoneType]:
if s.K_AEROWAY in tags_dict and tags_dict[s.K_AEROWAY] == s.V_AERODROME:
return BuildingZoneType.aerodrome
value = tags_dict[s.K_LANDUSE]
for member in BuildingZoneType:
if value ==
......@@ -481,6 +485,10 @@ class BuildingZone(OSMFeatureArea):
for key in to_be_removed:
del open_spaces_dict[key]
def is_aerodrome(self) -> bool:
return self.type_ is BuildingZoneType.aerodrome
class BTGBuildingZone(object):
"""A land-use from materials in FlightGear read from BTG-files"""
......@@ -1184,6 +1192,17 @@ class TempGenBuildings(object):
return False
def process_aerodrome_refs(transformer: co.Transformation) -> List[BuildingZone]:
osm_result = op.fetch_osm_db_data_ways_key_values([op.create_key_value_pair(s.K_AEROWAY, s.V_AERODROME)])
my_ways = list()
for way in list(osm_result.ways_dict.values()):
my_way = BuildingZone.create_from_way(way, osm_result.nodes_dict, transformer)
if my_way.is_valid():
my_ways.append(my_way)"Aerodrome land-uses found: %s", len(my_ways))
return my_ways
def process_osm_building_zone_refs(transformer: co.Transformation) -> List[BuildingZone]:
osm_result = op.fetch_osm_db_data_ways_keys([s.K_LANDUSE])
my_ways = list()
......@@ -139,6 +139,8 @@ def _draw_osm_zones(building_zones: List[m.BuildingZone], ax: maxs.Axes) -> None
face_color = "magenta"
elif m.BuildingZoneType.farmyard is building_zone.type_:
face_color = "chocolate"
elif m.BuildingZoneType.aerodrome is building_zone.type_:
face_color = 'purple'
_add_patch_for_building_zone(building_zone, face_color, face_color, ax)
......@@ -282,7 +284,7 @@ def draw_zones(buildings: List[bl.Building], building_zones: List[m.BuildingZone
# OSM building zones original
my_figure = _create_a3_landscape_figure()
my_figure.suptitle("Original OpenStreetMap building zones \n[blue=commercial, green=industrial, dark orange=retail\
, magenta=residential, brown=farmyard, red=error]")
, magenta=residential, brown=farmyard, purple=aerodrome, red=error]")
ax = my_figure.add_subplot(111)
_draw_osm_zones(building_zones, ax)
_set_ax_limits_from_bounds(ax, bounds)
......@@ -303,7 +305,8 @@ gold=town and suburban, yellow=construction and industrial and port]")
# All land-use
my_figure = _create_a3_landscape_figure()
my_figure.suptitle("Original OpenStreetMap and generated building zones \n[blue=commercial, green=industrial\
, dark orange=retail, magenta=residential, brown=farmyard, red=error;\n lighter variants=generated from buildings;\
, dark orange=retail, magenta=residential, brown=farmyard, purple=aerodrome, red=error;\n \
lighter variants=generated from buildings;\
\ncyan=commercial and industrial, gold=continuous urban, yellow=discontinuous urban]")
ax = my_figure.add_subplot(111)
_draw_osm_zones(building_zones, ax)
......@@ -459,8 +459,10 @@ def process(transformer: co.Transformation, building_zones: List[m.BuildingZone]
elif b_zone.type_ is m.BuildingZoneType.aerodrome:
use_me = False
# check whether density is already all right
# check whether density is already all right if we still thing we are going to use the zone
if use_me:
total_building_area = 0
for building in b_zone.osm_buildings:
......@@ -171,6 +171,9 @@ BUILDING_USE_SHARED_WORSHIP = False # try to use shared models for worship buil
BUILDING_FACADE_DEFAULT_COLOUR = '#D3D3D3' # e.g. #d3d3d3 - light grey
BUILDING_ROOF_DEFAULT_COLOUR = '#B22222' # e.g. #b22222 - firebrick
# -- The more buildings end up in LOD rough, the more work for your GPU.
# Increasing any of the following parameters will decrease GPU load.
LOD_ALWAYS_DETAIL_BELOW_AREA = 150 # -- below this area, buildings will always be LOD detail
......@@ -311,6 +314,7 @@ OWBB_SPLIT_MADE_UP_LANDUSE_BY_MAJOR_LINES = True # for external and generated
# also used for buffering around water areas in cities
......@@ -54,7 +54,7 @@ def map_osm_roof_shape(osm_roof_shape: str) -> RoofShape:
_shape = osm_roof_shape.strip()
if len(_shape) == 0:
return RoofShape.flat
if _shape == 'flat':
if _shape == s.V_FLAT:
return RoofShape.flat
if _shape in ['skillion', 'lean_to', 'pitched', 'shed']:
return RoofShape.skillion
......@@ -9,6 +9,7 @@ import numpy as np
import parameters
from textures.materials import screen_texture_tags_for_colour_spelling, map_hex_colour
import utils.osmstrings as s
from utils.utilities import replace_with_os_separator, Stats
......@@ -252,6 +253,12 @@ class RoofManager(object):
def find_by_file_name(self, file_name: str) -> Optional[Texture]:
for candidate in self.__l:
if file_name in candidate.filename:
return candidate
return None # nothing found
def _screen_exclude_texture_by_name(self, texture: Texture) -> bool:
if isinstance(self, FacadeManager):
......@@ -423,6 +430,9 @@ class RoofManager(object):
class FacadeManager(RoofManager):
def find_matching_facade(self, requires: List[str], tags: Dict[str, str], height: float, width: float,
stats: Stats=None) -> Optional[Texture]:
if s.K_AEROWAY in tags:
return self._find_aeroway_facade(tags)
exclusions = []
# if 'roof:colour' in tags: FIXME why would we need this at all?
# exclusions.append("%s:%s" % ('roof:colour', tags['roof:colour']))
......@@ -464,6 +474,23 @@ class FacadeManager(RoofManager):
return new_candidates
TERMINAL_TEX = 'facade_modern_commercial_35x20m.jpg'
HANGAR_TEX = 'facade_industrial_red_white_24x18m.jpg'
OTHER_TEX = 'facade_modern_commercial_red_gray_20x14m.jpg'
def _find_aeroway_facade(self, tags: Dict[str, str]) -> Optional[Texture]:
"""A little hack to get facades that match a bit an airport."""
if s.V_HANGAR in tags:
chosen_tex = self.HANGAR_TEX
elif s.V_TERMINAL in tags:
chosen_tex = self.TERMINAL_TEX
if random.randint(0, 1) == 0:
chosen_tex = self.TERMINAL_TEX
chosen_tex = self.OTHER_TEX
return RoofManager.find_by_file_name(self, chosen_tex)
class SpecialManager(RoofManager):
"""Manager for all those textures, which are specific for a given ac-file or so."""
"""Handles reading from apt.dat airport files and read/write to pickle file for minimized representation.
See for the specification.
Currently only reading runway data in order to avoid roads/railways to cross a runway.
There is also data available in apt.dat for taxiways an apron, but is not used at current point in time.
Flightgear 2016.4 can read multiple files - see e.g.
However this module does only support reading from one apt.dat.gz by the user's choice (normally in
However this module does only support reading from the apt.dat.gz in $FG_ROOT/Airports/apt.dat.gz).
......@@ -679,6 +679,11 @@ def split_way_at_boundary(nodes_dict: Dict[int, Node], complete_way: Way, clippi
return split_ways
def create_key_value_pair(key: str, value: str) -> str:
"""A string combining a key and a value for query in PostGIS"""
return '{}=>{}'.format(key, value)
# ================ UNITTESTS =======================
class TestOSMParser(unittest.TestCase):
......@@ -66,9 +66,13 @@ K_WIRES = 'wires'
# ======================= VALUES ==================================
V_ACROSS = 'across'
V_AERODROME = 'aerodrome'
V_AERO_OTHER = 'aero_other' # does not exist in OSM - used when it is unsure whether terminal, hangar or different
V_ALONG = 'along'
V_BRIDGE = 'bridge'
V_BUILDING = 'building'
V_FLAT = 'flat'
V_HANGAR = 'hangar'
V_INNER = 'inner'
V_MULTIPOLYGON = 'multipolygon'
V_MULTISTOREY = 'multi-storey'
......@@ -77,6 +81,7 @@ V_OUTER = 'outer'
V_OUTLINE = 'outline'
V_PIER = 'pier'
V_PLATFORM = 'platform'
V_TERMINAL = 'terminal'
V_WAY = 'way'
V_YES = 'yes'
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