Beispiel #1
0
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)
Beispiel #2
0
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)
Beispiel #3
0
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
Beispiel #4
0
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)
Beispiel #5
0
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]
Beispiel #6
0
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)
Beispiel #7
0
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
Beispiel #8
0
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
Beispiel #9
0
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)
Beispiel #10
0
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)
Beispiel #11
0
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
Beispiel #12
0
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()
Beispiel #13
0
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)
Beispiel #14
0
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)