Commit 08dc791f authored by Rick Gruber-Riemer's avatar Rick Gruber-Riemer

* Updates to landuse to simulate missing land-use by buffering building clusters.

* Reduction of flickering by processing building:part without relationship tagging.
parent fa0da2bc
......@@ -31,7 +31,7 @@ from utils.stg_io2 import STGVerbType
class BuildingParent(object):
"""The parent of buildings that are part of a Simple3D building.
Mostly used to coordinate textures for facades and roofs.
The parts determine the common textures by a simple: the first to set the values wins the race.
The parts determine the common textures by a simple rule: the first to set the values wins the race.
"""
def __init__(self, osm_id: int) -> None:
self.osm_id = osm_id
......@@ -114,14 +114,12 @@ class Building(object):
Read-only access to node coordinates via self.X[node][0|1]
"""
def __init__(self, osm_id: int, tags: Dict[str, str], outer_ring: BaseGeometry, name: str,
def __init__(self, osm_id: int, tags: Dict[str, str], outer_ring: shg.LinearRing, name: str,
height: float, levels: int,
stg_typ: STGVerbType=None, stg_hdg=None, inner_rings_list=list(), building_type='unknown',
roof_type: str='flat', roof_height: int=0, refs: List[int]=list()) -> None:
self.osm_id = osm_id
self.tags = tags
self.refs = refs
self.inner_rings_list = inner_rings_list
self.name = name
self.stg_typ = stg_typ # STGVerbType
self.stg_hdg = stg_hdg
......@@ -131,7 +129,6 @@ class Building(object):
self.longest_edge_len = 0.
self.levels = levels
self.first_node = 0 # index of first node in final OBJECT node list
self.anchor = Vec2d(list(outer_ring.coords[0]))
self.facade_texture = None
self.roof_texture = None
self.roof_complex = False # if False then compat:roof-flat; else compat:roof-pitched
......@@ -139,15 +136,17 @@ class Building(object):
self.roof_type = roof_type # str: flat, skillion, pyramidal, dome, gabled, half-hipped, hipped
self.ceiling = 0.
self.LOD = None # see utils.utilities.LOD for values
self.outer_nodes_closest = []
if len(outer_ring.coords) > 2:
self._set_polygon(outer_ring, self.inner_rings_list)
else:
self.polygon = None
if self.inner_rings_list:
self.roll_inner_nodes()
self.refs = None
self.inner_rings_list = None
self.outer_nodes_closest = None
self.anchor = None
self.polygon = None
self.update_geometry(outer_ring, inner_rings_list, refs)
self.building_type = building_type
self.parent = None # BuildingParent if available
self.pseudo_parents = list() # list of Building: only set for building:part without a real parent
self.ground_elev = None
self.ground_elev_min = None
self.ground_elev_max = None
......@@ -158,6 +157,18 @@ class Building(object):
self.model3d = tags['model3d']
self.angle3d = tags['angle3d']
def update_geometry(self, outer_ring: shg.LinearRing, inner_rings_list=list(), refs: List[int]=list()) -> None:
self.refs = refs
self.inner_rings_list = inner_rings_list
self.outer_nodes_closest = []
self.anchor = Vec2d(list(outer_ring.coords[0]))
if len(outer_ring.coords) > 2:
self._set_polygon(outer_ring, self.inner_rings_list)
else:
self.polygon = None
if self.inner_rings_list:
self.roll_inner_nodes()
def roll_inner_nodes(self) -> None:
"""Roll inner rings such that the node closest to an outer node goes first.
......@@ -777,9 +788,18 @@ def analyse(buildings: List[Building], fg_elev: FGElev,
b.parent = building_parent
building_parents[building_parent.osm_id] = building_parent
# align textures etc. for buildings with parents or pseudo-parents
for key, parent in building_parents.items():
parent.align_textures_children()
for building_part in new_buildings:
if building_part.pseudo_parents:
# FIXME there should be some checks whether the part is really the highest / most levels
# to determine whether the pseudo_parent really should pick the part's texture
for pseudo_parent in building_part.pseudo_parents:
pseudo_parent.facade_texture = building_part.facade_texture
pseudo_parent.roof_texture = building_part.roof_texture
return new_buildings
......
This diff is collapsed.
......@@ -153,7 +153,9 @@ LOD_PERCENTAGE_DETAIL = 0.5 # -- of the remaining buildings, this percen
LIGHTMAP_ENABLE = True # -- include lightmap in xml
OBSTRUCTION_LIGHT_MIN_LEVELS = 15 # -- put obstruction lights on buildings with >= given levels
CLUSTER_MIN_OBJECTS = 5 # -- discard cluster if too little objects
CLUSTER_MIN_OBJECTS = 5 # -- discard cluster if too few objects
BUILDING_TOLERANCE_MATCH_NODE = 0.5 # when searching for a OSM node based on distance: what is the allowed tolerance
# =============================================================================
......
......@@ -1683,50 +1683,6 @@ def _process_osm_highway(nodes_dict, ways_dict, my_coord_transformator):
return my_highways
def _generate_landuse_from_buildings(osm_landuses, building_refs: List[shg.Polygon]):
"""Adds "missing" landuses based on building clusters"""
my_landuse_candidates = dict()
index = 10000000000
for my_building in building_refs:
# check whether the building already is in a land use
within_existing_landuse = False
for osm_landuse in list(osm_landuses.values()):
if my_building.intersects(osm_landuse.polygon):
within_existing_landuse = True
break
if not within_existing_landuse:
# create new clusters of land uses
buffer_distance = parameters.LU_LANDUSE_BUILDING_BUFFER_DISTANCE
if my_building.area > parameters.LU_LANDUSE_BUILDING_BUFFER_DISTANCE**2:
factor = math.sqrt(my_building.area / parameters.LU_LANDUSE_BUILDING_BUFFER_DISTANCE**2)
buffer_distance = min(factor*parameters.LU_LANDUSE_BUILDING_BUFFER_DISTANCE,
parameters.LU_LANDUSE_BUILDING_BUFFER_DISTANCE_MAX)
buffer_polygon = my_building.buffer(buffer_distance)
buffer_polygon = buffer_polygon
within_existing_landuse = False
for candidate in list(my_landuse_candidates.values()):
if buffer_polygon.intersects(candidate.polygon):
candidate.polygon = candidate.polygon.union(buffer_polygon)
candidate.number_of_buildings += 1
within_existing_landuse = True
break
if not within_existing_landuse:
index += 1
my_candidate = landuse.Landuse(index)
my_candidate.polygon = buffer_polygon
my_candidate.number_of_buildings = 1
my_candidate.type_ = landuse.Landuse.TYPE_NON_OSM
my_landuse_candidates[my_candidate.osm_id] = my_candidate
# add landuse candidates to landuses
logging.debug("Candidate land-uses found: %s", len(my_landuse_candidates))
for candidate in list(my_landuse_candidates.values()):
if candidate.polygon.area < parameters.LU_LANDUSE_MIN_AREA:
del my_landuse_candidates[candidate.osm_id]
logging.debug("Candidate land-uses with sufficient area found: %s", len(my_landuse_candidates))
return my_landuse_candidates
def _fetch_osm_file_data() -> Tuple[Dict[int, osmparser.Node], Dict[int, osmparser.Way]]:
start_time = time.time()
# the lists below are in sequence: buildings references, power/aerialway, railway overhead, landuse and highway
......@@ -1824,9 +1780,7 @@ def process(coords_transform: coordinates.Transformation, fg_elev: utilities.FGE
landuse_refs = landuse.process_osm_landuse_refs(osm_nodes_dict, osm_ways_dict, coords_transform)
if parameters.LU_LANDUSE_GENERATE_LANDUSE:
generated_landuses = _generate_landuse_from_buildings(landuse_refs, building_refs)
for generated in list(generated_landuses.values()):
landuse_refs[generated.osm_id] = generated
landuse.generate_landuse_from_buildings(landuse_refs, building_refs)
logging.info('Number of landuse references: %s', len(landuse_refs))
streetlamp_buffers = _merge_streetlamp_buffers(landuse_refs)
logging.info('Number of streetlamp buffers: %s', len(streetlamp_buffers))
......
import logging
import math
from typing import Dict, List
import shapely.geometry as shg
......@@ -51,14 +52,91 @@ def process_osm_landuse_refs(nodes_dict: Dict[int, osm.Node], ways_dict: Dict[in
if my_landuse.polygon.is_valid and not my_landuse.polygon.is_empty:
my_landuses[my_landuse.osm_id] = my_landuse
logging.debug("OSM land-uses found: %s", len(my_landuses))
logging.info("OSM land-uses found: %s", len(my_landuses))
return my_landuses
def process_osm_landuse_as_areas(nodes_dict, ways_dict, my_coord_transformator) -> List[shg.Polygon]:
def process_osm_landuse_as_areas(nodes_dict, ways_dict, my_coord_transformator: Transformation) -> List[shg.Polygon]:
"""Just a wrapper around process_osm_landuse_refs(...) to get the list of polygons."""
landuse_refs = process_osm_landuse_refs(nodes_dict, ways_dict, my_coord_transformator)
if parameters.LU_LANDUSE_GENERATE_LANDUSE:
building_refs = _process_osm_building_refs(my_coord_transformator)
# generate_landuse_from_buildings(landuse_refs, building_refs)
landuse_areas = list()
for key, value in landuse_refs.items():
landuse_areas.append(value.polygon.buffer(parameters.BUILT_UP_AREA_LIT_BUFFER))
return landuse_areas
def generate_landuse_from_buildings(osm_landuses: Dict[int, Landuse], building_refs: List[shg.Polygon]):
"""Adds 'missing' land-uses based on building clusters."""
my_landuse_candidates = dict()
index = 0
for my_building in building_refs:
# check whether the building already is in a land use
within_existing_landuse = False
for osm_landuse in list(osm_landuses.values()):
if my_building.intersects(osm_landuse.polygon):
within_existing_landuse = True
break
if not within_existing_landuse:
# create new clusters of land uses
buffer_distance = parameters.LU_LANDUSE_BUILDING_BUFFER_DISTANCE
if my_building.area > parameters.LU_LANDUSE_BUILDING_BUFFER_DISTANCE**2:
factor = math.sqrt(my_building.area / parameters.LU_LANDUSE_BUILDING_BUFFER_DISTANCE**2)
buffer_distance = min(factor*parameters.LU_LANDUSE_BUILDING_BUFFER_DISTANCE,
parameters.LU_LANDUSE_BUILDING_BUFFER_DISTANCE_MAX)
buffer_polygon = my_building.buffer(buffer_distance)
buffer_polygon = buffer_polygon
within_existing_landuse = False
for key, candidate in my_landuse_candidates.items():
if buffer_polygon.intersects(candidate.polygon):
candidate.polygon = candidate.polygon.union(buffer_polygon)
candidate.number_of_buildings += 1
within_existing_landuse = True
break
if not within_existing_landuse:
index -= 1
my_candidate = Landuse(index)
my_candidate.polygon = buffer_polygon
my_candidate.number_of_buildings = 1
my_candidate.type_ = Landuse.TYPE_NON_OSM
my_landuse_candidates[my_candidate.osm_id] = my_candidate
# add landuse candidates to landuses
logging.debug("Candidate land-uses found: %d", len(my_landuse_candidates))
added = 0
for key, candidate in my_landuse_candidates.items():
if candidate.polygon.area >= parameters.LU_LANDUSE_MIN_AREA:
osm_landuses[key] = candidate
added += 1
logging.info("Candidate land-uses with sufficient area added: %d", added)
return my_landuse_candidates
def _process_osm_building_refs(my_coord_transformator: Transformation) -> List[shg.Polygon]:
"""Takes all buildings' convex hull (but not building:part) to be used for landuse processing.
Only valid if database is used.
"""
my_buildings = list()
if parameters.USE_DATABASE:
osm_way_result = osm.fetch_osm_db_data_ways_keys(['building'])
osm_nodes_dict = osm_way_result.nodes_dict
osm_ways_dict = osm_way_result.ways_dict
for way in list(osm_ways_dict.values()):
for key in way.tags:
if "building" == key:
my_coordinates = list()
for ref in way.refs:
if ref in osm_nodes_dict:
my_node = osm_nodes_dict[ref]
my_coordinates.append(my_coord_transformator.toLocal((my_node.lon, my_node.lat)))
if 2 < len(my_coordinates):
my_polygon = shg.Polygon(my_coordinates)
if my_polygon.is_valid and not my_polygon.is_empty:
my_buildings.append(my_polygon.convex_hull)
return my_buildings
......@@ -17,9 +17,9 @@ import xml.sax
import psycopg2
import shapely.geometry as shg
from utils.coordinates import Transformation
import parameters
from utils.coordinates import Transformation
PSEUDO_OSM_ID = -1 # For those nodes and ways, which get added as part of processing. Not written back to OSM.
......
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