def _generate_buildings_along_highway(building_zone: m.BuildingZone, settlement_type: enu.SettlementType, highway: m.Highway, shared_models_list: List[m.SharedModel], is_reverse: bool, temp_buildings: m.TempGenBuildings): """ The central assumption is that existing blocked areas incl. buildings du not need a buffer. The to be populated buildings all bring their own constraints with regards to distance to road, distance to other buildings etc. Returns a TempGenBuildings object with all potential new generated buildings """ travelled_along = 0 highway_length = highway.geometry.length my_gen_building = m.GenBuilding(op.get_next_pseudo_osm_id(op.OSMFeatureType.building_owbb), random.choice(shared_models_list), highway.get_width(), settlement_type) if not is_reverse: point_on_line = highway.geometry.interpolate(0) else: point_on_line = highway.geometry.interpolate(highway_length) while travelled_along < highway_length: travelled_along += parameters.OWBB_STEP_DISTANCE prev_point_on_line = point_on_line if not is_reverse: point_on_line = highway.geometry.interpolate(travelled_along) else: point_on_line = highway.geometry.interpolate(highway_length - travelled_along) angle = co.calc_angle_of_line_local(prev_point_on_line.x, prev_point_on_line.y, point_on_line.x, point_on_line.y) buffer_polygon = my_gen_building.get_a_polygon(True, point_on_line, angle) if buffer_polygon.within(building_zone.geometry): valid_new_gen_building = True for blocked_area in building_zone.linked_blocked_areas: if buffer_polygon.intersects(blocked_area.polygon): valid_new_gen_building = False break if valid_new_gen_building: for blocked_area in temp_buildings.generated_blocked_areas: if buffer_polygon.intersects(blocked_area.polygon): valid_new_gen_building = False break if valid_new_gen_building: area_polygon = my_gen_building.get_a_polygon(False, point_on_line, angle) my_gen_building.set_location(point_on_line, angle, area_polygon, buffer_polygon) temp_buildings.add_generated(my_gen_building, m.BlockedArea(m.BlockedAreaType.gen_building, area_polygon)) # prepare a new building, which might get added in the next loop my_gen_building = m.GenBuilding(op.get_next_pseudo_osm_id(op.OSMFeatureType.building_owbb), random.choice(shared_models_list), highway.get_width(), settlement_type)
def _reduce_building_zones_with_btg_water(building_zones: List[m.BuildingZone], btg_water_areas: List[Polygon]) -> None: """Adds "missing" building_zones based on land-use info outside of OSM land-use""" counter = 0 for water_area in btg_water_areas: prep_geom = prep(water_area) parts = list() for building_zone in reversed(building_zones): if prep_geom.contains_properly(building_zone.geometry): counter += 1 building_zones.remove(building_zone) elif prep_geom.intersects(building_zone.geometry): counter += 1 diff = building_zone.geometry.difference(water_area) if isinstance(diff, Polygon): if diff.area >= parameters.OWBB_GENERATE_LANDUSE_LANDUSE_MIN_AREA: building_zone.geometry = diff else: building_zones.remove(building_zone) elif isinstance(diff, MultiPolygon): building_zones.remove(building_zone) is_first = True for poly in diff: if poly.area >= parameters.OWBB_GENERATE_LANDUSE_LANDUSE_MIN_AREA: if is_first: building_zone.geometry = poly parts.append(building_zone) is_first = False else: new_zone = m.BuildingZone(op.get_next_pseudo_osm_id(op.OSMFeatureType.landuse), poly, building_zone.type_) parts.append(new_zone) building_zones.extend(parts) logging.info("Corrected %i building zones with BTG water areas", counter)
def _split_multipolygon_generated_building_zone(zone: m.GeneratedBuildingZone) -> List[m.GeneratedBuildingZone]: """Checks whether a generated building zone's geometry is Multipolygon. If yes, then split into polygons. Algorithm distributes buildings and checks that minimal size and buildings are respected.""" split_zones = list() if isinstance(zone.geometry, MultiPolygon): # just to be sure if methods would be called by mistake on polygon new_generated = list() logging.debug("Handling a generated land-use Multipolygon with %d polygons", len(zone.geometry.geoms)) for split_polygon in zone.geometry.geoms: my_split_generated = m.GeneratedBuildingZone(op.get_next_pseudo_osm_id(op.OSMFeatureType.landuse), split_polygon, zone.type_) new_generated.append(my_split_generated) while len(zone.osm_buildings) > 0: my_building = zone.osm_buildings.pop() my_building.zone = None for my_split_generated in new_generated: if my_building.geometry.intersects(my_split_generated.geometry): my_split_generated.relate_building(my_building) break if my_building.zone is None: # maybe no intersection -> fall back as each building needs a zone new_generated[0].relate_building(my_building) for my_split_generated in new_generated: if my_split_generated.from_buildings and len(my_split_generated.osm_buildings) == 0: 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)) else: split_zones.append(zone) return split_zones
def _extend_osm_building_zones_with_btg_zones(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""" counter = 0 for external_landuse in external_landuses: my_geoms = list() my_geoms.append(external_landuse.geometry) for building_zone in building_zones: parts = list() for geom in my_geoms: if geom.within(building_zone.geometry) \ or geom.touches(building_zone.geometry): continue elif geom.intersects(building_zone.geometry): diff = geom.difference(building_zone.geometry) if isinstance(diff, Polygon): if diff.area >= parameters.OWBB_GENERATE_LANDUSE_LANDUSE_MIN_AREA: parts.append(diff) elif isinstance(diff, MultiPolygon): for poly in diff: if poly.area >= parameters.OWBB_GENERATE_LANDUSE_LANDUSE_MIN_AREA: parts.append(poly) else: if geom.area >= parameters.OWBB_GENERATE_LANDUSE_LANDUSE_MIN_AREA: parts.append(geom) my_geoms = parts for geom in my_geoms: generated = m.GeneratedBuildingZone(op.get_next_pseudo_osm_id(op.OSMFeatureType.landuse), geom, external_landuse.type_) building_zones.append(generated) counter += 1 logging.debug("Generated building zones from external land-use: %i", counter)
def _prepare_building_zone_with_highways(building_zone, highways_dict): """Link highways to BuildingZones to prepare for building generation. Either as whole if entirely within or as a set of intersections if intersecting """ to_be_removed = list() for my_highway in highways_dict.values(): if not my_highway.populate_buildings_along(): continue if my_highway.geometry.length < parameters.OWBB_MIN_STREET_LENGTH: continue if my_highway.geometry.within(building_zone.geometry): building_zone.linked_genways.append(my_highway) to_be_removed.append(my_highway.osm_id) continue # process intersections if my_highway.geometry.intersects(building_zone.geometry): intersections = list() intersections.append(my_highway.geometry.intersection(building_zone.geometry)) if len(intersections) > 0: for intersection in intersections: if isinstance(intersection, MultiLineString): for my_line in intersection: if isinstance(my_line, LineString): intersections.append(my_line) elif isinstance(intersection, LineString): if intersection.length >= parameters.OWBB_MIN_STREET_LENGTH: new_highway = m.Highway.create_from_scratch(op.get_next_pseudo_osm_id( op.OSMFeatureType.road), my_highway, intersection) building_zone.linked_genways.append(new_highway) for key in to_be_removed: del highways_dict[key]
def _process_osm_tree_row(nodes_dict, ways_dict, trees: List[Tree], coords_transform: co.Transformation, fg_elev: utilities.FGElev) -> None: """Trees in a row as mapped in OSM (natural=tree_row). NB: extends the existing list of trees from the input parameter. """ potential_trees = list() for way in list(ways_dict.values()): my_geometry = way.line_string_from_osm_way(nodes_dict, coords_transform) if my_geometry.length / len( way.refs) > parameters.C2P_TREES_MAX_AVG_DIST_TREES_ROW: for i in range( 0, int(my_geometry.length // parameters.C2P_TREES_DIST_TREES_ROW_CALCULATED)): my_point = my_geometry.interpolate( i * parameters.C2P_TREES_DIST_TREES_ROW_CALCULATED) my_id = op.get_next_pseudo_osm_id( op.OSMFeatureType.generic_node) elev = fg_elev.probe_elev((my_point.x, my_point.y), False) tree = Tree(my_id, my_point.x, my_point.y, elev) potential_trees.append(tree) # and then always the last node node = nodes_dict[way.refs[-1]] potential_trees.append( Tree.tree_from_node(node, coords_transform, fg_elev)) else: for ref in way.refs: node = nodes_dict[ref] potential_trees.append( Tree.tree_from_node(node, coords_transform, fg_elev)) _extend_trees_if_dist_ok(potential_trees, trees)
def match_local_coords_with_global_nodes( local_list: List[Tuple[float, float]], ref_list: List[int], all_nodes: Dict[int, op.Node], coords_transform: co.Transformation, osm_id: int, create_node: bool = False) -> List[int]: """Given a set of coordinates in local space find matching Node objects in global space. Matching is using a bit of tolerance (cf. parameter), which should be enough to account for conversion precision resp. float precision. If a node cannot be matched: if parameter create_node is False, then a ValueError is thrown - else a new Node is created and added to the all_nodes dict. """ matched_nodes = list() nodes_local = dict( ) # key is osm_id from Node, value is Tuple[float, float] for ref in ref_list: node = all_nodes[ref] nodes_local[node.osm_id] = coords_transform.to_local( (node.lon, node.lat)) for local in local_list: closest_distance = 999999 found_key = -1 for key, node_local in nodes_local.items(): distance = co.calc_distance_local(local[0], local[1], node_local[0], node_local[1]) if distance < closest_distance: closest_distance = distance if distance < parameters.TOLERANCE_MATCH_NODE: found_key = key break if found_key < 0: if create_node: lon, lat = coords_transform.to_global(local) new_node = op.Node( op.get_next_pseudo_osm_id( op.OSMFeatureType.building_relation), lat, lon) all_nodes[new_node.osm_id] = new_node matched_nodes.append(new_node.osm_id) else: raise ValueError( 'No match for parent with osm_id = %d. Closest: %f' % (osm_id, closest_distance)) else: matched_nodes.append(found_key) return matched_nodes
def _process_osm_trees_gardens( city_blocks: Set[CityBlock], parks: List[shg.Polygon], fg_elev: utilities.FGElev) -> Dict[e.TreeType, List[Tree]]: suburban_trees = list() town_trees = list() urban_trees = list() for city_block in city_blocks: tree_type = e.map_tree_type_from_settlement_type_garden( city_block.settlement_type) if city_block.type_ is e.BuildingZoneType.special_processing: continue prep_geom = prep(city_block.geometry) my_random_points = _generate_random_tree_points_in_polygon( city_block.geometry, prep_geom, parameters.C2P_TREES_DIST_BETWEEN_TREES_GARDEN, parameters.C2P_TREES_SKIP_RATE_TREES_GARDEN) for point in my_random_points: exclude = False for park in parks: # need to check for parks if point.within(park): exclude = True break if exclude: continue if not _test_point_in_building(point, {city_block}, 2.0): elev = fg_elev.probe_elev((point.x, point.y), False) my_tree = Tree( op.get_next_pseudo_osm_id(op.OSMFeatureType.generic_node), point.x, point.y, elev) if tree_type is e.TreeType.suburban: suburban_trees.append(my_tree) elif tree_type is e.TreeType.town: town_trees.append(my_tree) else: urban_trees.append(my_tree) garden_trees = dict() if suburban_trees: garden_trees[e.TreeType.suburban] = suburban_trees if town_trees: garden_trees[e.TreeType.town] = town_trees if urban_trees: garden_trees[e.TreeType.urban] = urban_trees logging.info('Number of trees added in gardens: %i', len(suburban_trees) + len(town_trees) + len(urban_trees)) return garden_trees
def _process_osm_trees_lined(nodes_dict, ways_dict, trees: List[Tree], coords_transform: co.Transformation, fg_elev: utilities.FGElev) -> None: """Trees in a line as mapped in OSM (tree_lined=*). NB: extends the existing list of trees from the input parameter. """ potential_trees = list() for way in list(ways_dict.values()): if s.K_NATURAL in way.tags and way.tags[s.K_NATURAL] == s.V_TREE_ROW: continue # was already processed in _process_osm_tree_row() tag_value = way.tags[s.K_TREE_LINED] if tag_value == s.V_NO: continue orig_line = way.line_string_from_osm_way(nodes_dict, coords_transform) tree_lines = list() if tag_value != s.V_RIGHT: line_geoms = orig_line.parallel_offset(4.0, 'left') if isinstance(line_geoms, shg.LineString): tree_lines.append(line_geoms) elif isinstance(line_geoms, shg.MultiLineString): for geom in line_geoms.geoms: tree_lines.append(geom) if tag_value != s.V_LEFT: line_geoms = orig_line.parallel_offset(4.0, 'right') if isinstance(line_geoms, shg.LineString): tree_lines.append(line_geoms) elif isinstance(line_geoms, shg.MultiLineString): for geom in line_geoms.geoms: tree_lines.append(geom) for my_line in tree_lines: for i in range( 0, int(my_line.length // parameters.C2P_TREES_DIST_TREES_ROW_CALCULATED) - 1): # i+0.5 such that start and end no direct tree -> often connect to other tree_lines or itself my_point = my_line.interpolate( (i + 0.5) * parameters.C2P_TREES_DIST_TREES_ROW_CALCULATED) my_id = op.get_next_pseudo_osm_id( op.OSMFeatureType.generic_node) elev = fg_elev.probe_elev((my_point.x, my_point.y), False) tree = Tree(my_id, my_point.x, my_point.y, elev) potential_trees.append(tree) _extend_trees_if_dist_ok(potential_trees, trees)
def _process_osm_trees_parks(parks: List[shg.Polygon], trees: List[Tree], city_blocks: Set[CityBlock], fg_elev: utilities.FGElev) -> None: """Additional trees based on specific land-use (not woods) in urban areas. NB: extends the existing list of trees from the input parameter. """ additional_trees = list( ) # not directly adding to trees due to spatial comparison mapped_factor = math.pow( parameters.C2P_TREES_DIST_BETWEEN_TREES_PARK_MAPPED, 2) logging.info('Number of area polygons for process for trees: %i', len(parks)) tree_points_to_check = list() for tree in trees: tree_points_to_check.append(shg.Point(tree.x, tree.y)) for my_geometry in parks: trees_contained = 0 prep_geom = prep(my_geometry) # check whether any of the existing manually mapped trees is within the area. # if yes, then most probably all trees where manually mapped for tree_point in tree_points_to_check: if prep_geom.contains(tree_point): trees_contained += 1 # not removing from to be checked as probably takes more time to remove than check again if trees_contained == 0 or (my_geometry.area / trees_contained) > mapped_factor: # we are good to try to add more trees points = _random_trees_in_area( my_geometry, prep_geom, city_blocks, parameters.C2P_TREES_DIST_BETWEEN_TREES_PARK, parameters.C2P_TREES_SKIP_RATE_TREES_PARK) for point in points: elev = fg_elev.probe_elev((point.x, point.y), False) additional_trees.append( Tree( op.get_next_pseudo_osm_id( op.OSMFeatureType.generic_node), point.x, point.y, elev)) logging.info('Number of trees added in areas: %i', len(additional_trees)) trees.extend(additional_trees)
def _create_btg_buildings_zones(btg_polys: Dict[str, List[Polygon]]) -> List[m.BTGBuildingZone]: btg_zones = list() for key, polys in btg_polys.items(): # find the corresponding BuildingZoneType type_ = None for member in enu.BuildingZoneType: btg_key = 'btg_' + key if btg_key == member.name: type_ = member break if type_ is None: raise Exception('Unknown BTG material: {}. Most probably a programming mismatch.'.format(key)) for poly in polys: my_zone = m.BTGBuildingZone(op.get_next_pseudo_osm_id(op.OSMFeatureType.landuse), type_, poly) btg_zones.append(my_zone) logging.debug('Created a total of %i zones from BTG', len(btg_zones)) return btg_zones
def _assign_city_blocks(building_zone: m.BuildingZone, highways_dict: Dict[int, m.Highway]) -> None: """Splits the land-use into (city) blocks, i.e. areas surrounded by streets. Brute force by buffering all highways, then take the geometry difference, which splits the zone into multiple polygons. Some of the polygons will be real city blocks, others will be border areas. Could also be done by using e.g. networkx.algorithms.cycles.cycle_basis.html. However is a bit more complicated logic and programming wise, but might be faster. """ building_zone.reset_city_blocks() highways_dict_copy1 = highways_dict.copy() # otherwise when using highways_dict in plotting it will be "used" polygons = list() intersecting_highways = _test_highway_intersecting_area(building_zone.geometry, highways_dict_copy1) 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, 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 \ geometry_difference.area >= parameters.OWBB_MIN_CITY_BLOCK_AREA: polygons.append(geometry_difference) elif isinstance(geometry_difference, MultiPolygon): my_polygons = geometry_difference.geoms for my_poly in my_polygons: if isinstance(my_poly, Polygon) and my_poly.is_valid and \ my_poly.area >= parameters.OWBB_MIN_CITY_BLOCK_AREA: polygons.append(my_poly) logging.debug('Found %i city blocks in building zone osm_ID=%i', len(polygons), building_zone.osm_id) for polygon in polygons: my_city_block = m.CityBlock(op.get_next_pseudo_osm_id(op.OSMFeatureType.landuse), polygon, building_zone.type_) building_zone.add_city_block(my_city_block) # now assign the osm_buildings to the city blocks building_zone.reassign_osm_buildings_to_city_blocks()
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: apt_dat_polygons.extend(my_polys) # 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) apt_dat_polygons.remove(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, enu.BuildingZoneType.aerodrome) aerodrome_zones.append(new_aerodrome_zone) # 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 building_zones.extend(aerodrome_zones)
def _generate_building_zones_from_buildings(building_zones: List[m.BuildingZone], buildings_outside: List[bl.Building]) -> None: """Adds "missing" building_zones based on building clusters outside of OSM land-use. The calculated values are implicitly updated in the referenced parameter building_zones""" zones_candidates = dict() for my_building in buildings_outside: buffer_distance = parameters.OWBB_GENERATE_LANDUSE_BUILDING_BUFFER_DISTANCE if my_building.area > parameters.OWBB_GENERATE_LANDUSE_BUILDING_BUFFER_DISTANCE**2: factor = math.sqrt(my_building.area / parameters.OWBB_GENERATE_LANDUSE_BUILDING_BUFFER_DISTANCE ** 2) buffer_distance = min(factor * parameters.OWBB_GENERATE_LANDUSE_BUILDING_BUFFER_DISTANCE, parameters.OWBB_GENERATE_LANDUSE_BUILDING_BUFFER_DISTANCE_MAX) buffer_polygon = my_building.geometry.buffer(buffer_distance) within_existing_building_zone = False for candidate in zones_candidates.values(): if buffer_polygon.intersects(candidate.geometry): candidate.geometry = candidate.geometry.union(buffer_polygon) candidate.relate_building(my_building) within_existing_building_zone = True break if not within_existing_building_zone: my_candidate = m.GeneratedBuildingZone(op.get_next_pseudo_osm_id(op.OSMFeatureType.landuse), buffer_polygon, enu.BuildingZoneType.non_osm) my_candidate.relate_building(my_building) zones_candidates[my_candidate.osm_id] = my_candidate logging.debug("Candidate land-uses found: %s", len(zones_candidates)) # Search once again for intersections in order to account for randomness in checks merged_candidate_ids = list() keys = list(zones_candidates.keys()) for i in range(0, len(zones_candidates)-2): for j in range(i+1, len(zones_candidates)-1): if zones_candidates[keys[i]].geometry.intersects(zones_candidates[keys[j]].geometry): merged_candidate_ids.append(keys[i]) zones_candidates[keys[j]].geometry = zones_candidates[keys[j]].geometry.union( zones_candidates[keys[i]].geometry) for building in zones_candidates[keys[i]].osm_buildings: zones_candidates[keys[j]].relate_building(building) break logging.debug("Candidate land-uses merged into others: %d", len(merged_candidate_ids)) # check for minimum size and then simplify geometry kept_candidates = list() for candidate in zones_candidates.values(): if candidate.osm_id in merged_candidate_ids: 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 for generated in kept_candidates: for building_zone in building_zones: # can be from OSM or external if generated.geometry.intersects(building_zone.geometry): generated.geometry = generated.geometry.difference(building_zone.geometry) # now make sure that there are no MultiPolygons logging.debug("Candidate land-uses before multi-polygon split: %d", len(kept_candidates)) polygon_candidates = list() for zone in kept_candidates: if isinstance(zone.geometry, MultiPolygon): polygon_candidates.extend(_split_multipolygon_generated_building_zone(zone)) else: polygon_candidates.append(zone) logging.debug("Candidate land-uses after multi-polygon split: %d", len(polygon_candidates)) building_zones.extend(polygon_candidates)