def _parse_stg_entries_for_convex_hull(stg_entries: List[STGEntry], my_coord_transformation: Transformation, tile_box: shg.Polygon) -> None: """ Parses the ac-file content for a set of STGEntry objects and sets their boundary attribute to be the convex hull of all points in the ac-file in the specified local coordinate system. If there is a problem creating the convex hull, then the stg_entry will be removed. """ ac_filename = "" for entry in reversed(stg_entries): if entry.verb_type in [ STGVerbType.object_static, STGVerbType.object_shared ]: try: ac_filename = entry.obj_filename if ac_filename.endswith(".xml"): entry.overwrite_filename( _extract_ac_from_xml( entry.get_obj_path_and_name(), entry.get_obj_path_and_name( parameters.PATH_TO_SCENERY))) boundary_polygon = _extract_boundary( entry.get_obj_path_and_name(), entry.get_obj_path_and_name(parameters.PATH_TO_SCENERY)) rotated_polygon = affinity.rotate(boundary_polygon, entry.hdg - 90, (0, 0)) x_y_point = my_coord_transformation.to_local( (entry.lon, entry.lat)) translated_polygon = affinity.translate( rotated_polygon, x_y_point[0], x_y_point[1]) if entry.verb_type is STGVerbType.object_static and parameters.OVERLAP_CHECK_CH_BUFFER_STATIC > 0.01: entry.convex_hull = translated_polygon.buffer( parameters.OVERLAP_CHECK_CH_BUFFER_STATIC, shg.CAP_STYLE.square) elif entry.verb_type is STGVerbType.object_shared and parameters.OVERLAP_CHECK_CH_BUFFER_SHARED > 0.01: entry.convex_hull = translated_polygon.buffer( parameters.OVERLAP_CHECK_CH_BUFFER_SHARED, shg.CAP_STYLE.square) else: entry.convex_hull = translated_polygon except IOError as reason: logging.warning( "Ignoring unreadable stg_entry of type %s and file name %s: %s", entry.verb_type, entry.obj_filename, reason) except ValueError: # Happens e.g. for waterfalls, where the xml-file only references a <particlesystem> logging.debug( "AC-filename could be wrong in xml-file %s - or just no ac-file referenced", ac_filename) # Now check, whether we are interested if entry.convex_hull is None or entry.convex_hull.is_valid is False or entry.convex_hull.is_empty: logging.warning( "Ignoring stg_entry of type %s and file name %s: convex hull invalid", entry.verb_type, entry.obj_filename) stg_entries.remove(entry) elif entry.convex_hull.within( tile_box) is False and entry.convex_hull.disjoint( tile_box): stg_entries.remove(entry)
def tree_from_node(cls, node: op.Node, coords_transform: co.Transformation, fg_elev: utilities.FGElev) -> 'Tree': elev = fg_elev.probe_elev((node.lon, node.lat), True) x, y = coords_transform.to_local((node.lon, node.lat)) tree = Tree(node.osm_id, x, y, elev) tree.parse_tags(node.tags) return tree
def get_extent_local( transformer: co.Transformation) -> typing.Tuple[co.Vec2d, co.Vec2d]: cmin = co.Vec2d(BOUNDARY_WEST, BOUNDARY_SOUTH) cmax = co.Vec2d(BOUNDARY_EAST, BOUNDARY_NORTH) logging.info("min/max " + str(cmin) + " " + str(cmax)) lmin = co.Vec2d(transformer.to_local((cmin.x, cmin.y))) lmax = co.Vec2d(transformer.to_local((cmax.x, cmax.y))) return lmin, lmax
def _get_tile_bounds_local( transform: co.Transformation) -> Tuple[float, float, float, float]: """Based on parameters use the tile border as bounds. To be used as axis limits. """ min_point = transform.to_local((p.BOUNDARY_WEST, p.BOUNDARY_SOUTH)) max_point = transform.to_local((p.BOUNDARY_EAST, p.BOUNDARY_NORTH)) return min_point[0], min_point[1], max_point[0], max_point[1]
def refs_to_ring(coords_transform: Transformation, refs: List[int], nodes_dict: Dict[int, Node]) -> shg.LinearRing: """Accept a list of OSM refs, return a linear ring.""" coords = [] for ref in refs: c = nodes_dict[ref] coords.append(coords_transform.to_local((c.lon, c.lat))) ring = shg.polygon.LinearRing(coords) return ring
def create_polygons(self, transformer: co.Transformation) -> Optional[List[Polygon]]: if self.not_empty: boundaries = list() for my_list in self.nodes_lists: if len(my_list) < 3: continue my_boundary = Polygon([transformer.to_local(n) for n in my_list]) if my_boundary.is_valid: boundaries.append(my_boundary) return boundaries return None
def read_btg_file(transformer: Transformation, airport_code: Optional[str] = None) -> Optional[BTGReader]: """There is a need to do a local coordinate transformation, as BTG also has a local coordinate transformation, but there the center will be in the middle of the tile, whereas here it can be another place if the boundary is not a whole tile.""" lon_lat = parameters.get_center_global() path_to_btg = ct.construct_path_to_files(parameters.PATH_TO_SCENERY, scenery_directory_name(SceneryType.terrain), (lon_lat.lon, lon_lat.lat)) tile_index = parameters.get_tile_index() # from cartesian ellipsoid to geodetic flat trans_proj = pyproj.Transformer.from_crs({"proj": 'geocent', "ellps": 'WGS84', "datum": 'WGS84'}, "EPSG:4326") file_name = ct.construct_btg_file_name_from_tile_index(tile_index) if airport_code: file_name = ct.construct_btg_file_name_from_airport_code(airport_code) btg_file_name = os.path.join(path_to_btg, file_name) if not os.path.isfile(btg_file_name): logging.warning('File %s does not exist. Ocean or missing in Terrasync?', btg_file_name) return None logging.debug('Reading btg file: %s', btg_file_name) btg_reader = BTGReader(btg_file_name, True if airport_code is not None else False) gbs_center = btg_reader.gbs_center v_max_x = 0 v_max_y = 0 v_max_z = 0 v_min_x = 0 v_min_y = 0 v_min_z = 0 for vertex in btg_reader.vertices: if vertex.x >= 0: v_max_x = max(v_max_x, vertex.x) else: v_min_x = min(v_min_x, vertex.x) if vertex.y >= 0: v_max_y = max(v_max_y, vertex.y) else: v_min_y = min(v_min_y, vertex.y) if vertex.z >= 0: v_max_z = max(v_max_z, vertex.z) else: v_min_z = min(v_min_z, vertex.z) # translate to lon_lat and then to local coordinates lat, lon, _alt = trans_proj.transform(vertex.x + gbs_center.x, vertex.y + gbs_center.y, vertex.z + gbs_center.z, radians=False) vertex.x, vertex.y = transformer.to_local((lon, lat)) vertex.z = _alt return btg_reader
def line_string_from_osm_way(self, nodes_dict: Dict[int, Node], transformer: Transformation) \ -> Optional[shg.LineString]: my_coordinates = list() for ref in self.refs: if ref in nodes_dict: my_node = nodes_dict[ref] x, y = transformer.to_local((my_node.lon, my_node.lat)) my_coordinates.append((x, y)) if len(my_coordinates) >= 2: my_geometry = shg.LineString(my_coordinates) if my_geometry.is_valid and not my_geometry.is_empty: return my_geometry return None
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 polygon_from_osm_way(self, nodes_dict: Dict[int, Node], my_coord_transformator: Transformation) \ -> Optional[shg.Polygon]: """Creates a shapely polygon in local coordinates. Or None is something is not valid.""" my_coordinates = list() for ref in self.refs: if ref in nodes_dict: my_node = nodes_dict[ref] x, y = my_coord_transformator.to_local((my_node.lon, my_node.lat)) my_coordinates.append((x, y)) if len(my_coordinates) >= 3: my_polygon = shg.Polygon(my_coordinates) if not my_polygon.is_valid: # it might be self-touching or self-crossing polygons clean = my_polygon.buffer(0) # cf. http://toblerity.org/shapely/manual.html#constructive-methods if clean.is_valid: my_polygon = clean # it is now a Polygon or a MultiPolygon if my_polygon.is_valid and not my_polygon.is_empty: return my_polygon return None
def __init__(self, transform: co.Transformation, way: op.Way, nodes_dict: Dict[int, op.Node], lit_areas: List[shg.Polygon], width: float, tex_coords: Tuple[float, float] = road.EMBANKMENT_1): self.width = width self.way = way self.nodes_dict = nodes_dict self.written_to_ac = False self.vectors = None # numpy array defined in compute_angle_etc() self.normals = None # numpy array defined in compute_angle_etc() self.angle = None # numpy array defined in compute_angle_etc() self.segment_len = None # numpy array defined in compute_angle_etc() self.dist = None # numpy array defined in compute_angle_etc() osm_nodes = [nodes_dict[r] for r in way.refs] nodes = np.array( [transform.to_local((n.lon, n.lat)) for n in osm_nodes]) self.center = shg.LineString(nodes) self.lighting = list() # same number of elements as self.center self._prepare_lighting(nodes, lit_areas) try: self._compute_angle_etc() self.left, self.right = self._compute_sides(self.width / 2.) # LineStrings except Warning as reason: logging.warning("Warning in OSM_ID %i: %s", self.way.osm_id, reason) self.tex = tex_coords # determines which part of texture we use # set in roads.py self.cluster_ref = None # Cluster self.junction0 = None # utils.graph.Junction self.junction1 = None # utils.graph.Junction
def _create_pseudo_stg_entries_for_exclude_areas( areas: Optional[List[List[Tuple[float, float]]]], transform: Transformation) -> List[STGEntry]: """Create a list of faked STGEntries for exclude areas. We cannot control the data quality of the user provided input, so no recovery on error -> fail fast. """ if areas is None or len(areas) == 0: return list() faked_entries = list() for i, list_of_tuples in enumerate(areas): my_coordinates = list() for lon_lat in list_of_tuples: x, y = transform.to_local((lon_lat[0], lon_lat[1])) my_coordinates.append((x, y)) if len(my_coordinates) >= 3: my_polygon = shg.Polygon(my_coordinates) if my_polygon.is_valid and not my_polygon.is_empty: lon, lat = transform.to_global( (my_polygon.centroid.x, my_polygon.centroid.y)) my_entry = STGEntry(STGVerbType.object_static.name, 'exclude area', 'fake', lon, lat, 0, 0) my_entry.convex_hull = my_polygon faked_entries.append(my_entry) else: raise ValueError( 'Resulting exclude area polygon is not valid or empty: Entry: %i', i + 1) else: raise ValueError( 'There must be at least 3 coordinate tuples per exclude area polygon. Entry: %i', i + 1) logging.info( 'Added %i fake static STGEntries for OVERLAP_CHECK_EXCLUDE_AREAS', len(faked_entries)) return faked_entries
def process_polygons_from_btg_faces(btg_reader: BTGReader, materials: List[str], exclusion_materials: bool, transformer: Transformation, merge_polys: bool = True) -> Dict[str, List[Polygon]]: """For a given set of BTG materials merge the faces read from BTG into as few polygons as possible. Parameter exclusion_materials means whether the list of materials is for excluding or including in outcome.""" btg_lon, btg_lat = btg_reader.gbs_lon_lat btg_x, btg_y = transformer.to_local((btg_lon, btg_lat)) logging.debug('Difference between BTG and transformer: x = %f, y = %f', btg_x, btg_y) if exclusion_materials: # airport has same origin as tile btg_x = 0 btg_y = 0 btg_polys = dict() min_x, min_y = transformer.to_local((parameters.BOUNDARY_WEST, parameters.BOUNDARY_SOUTH)) max_x, max_y = transformer.to_local((parameters.BOUNDARY_EAST, parameters.BOUNDARY_NORTH)) bounds = (min_x, min_y, max_x, max_y) disjoint = 0 accepted = 0 counter = 0 merged_counter = 0 for key, faces_list in btg_reader.faces.items(): if (exclusion_materials and key not in materials) or (exclusion_materials is False and key in materials): temp_polys = list() for face in faces_list: counter += 1 v0 = btg_reader.vertices[face.vertices[0]] v1 = btg_reader.vertices[face.vertices[1]] v2 = btg_reader.vertices[face.vertices[2]] # create the triangle polygon my_geometry = Polygon([(v0.x - btg_x, v0.y - btg_y), (v1.x - btg_x, v1.y - btg_y), (v2.x - btg_x, v2.y - btg_y), (v0.x - btg_x, v0.y - btg_y)]) if not my_geometry.is_valid: # it might be self-touching or self-crossing polygons clean = my_geometry.buffer(0) # cf. http://toblerity.org/shapely/manual.html#constructive-methods if clean.is_valid: my_geometry = clean # it is now a Polygon or a MultiPolygon else: # lets try with a different sequence of points my_geometry = Polygon([(v0.x - btg_x, v0.y - btg_y), (v2.x - btg_x, v2.y - btg_y), (v1.x - btg_x, v1.y - btg_y), (v0.x - btg_x, v0.y - btg_y)]) if not my_geometry.is_valid: clean = my_geometry.buffer(0) if clean.is_valid: my_geometry = clean if isinstance(my_geometry, Polygon) and my_geometry.is_valid and not my_geometry.is_empty: if not disjoint_bounds(bounds, my_geometry.bounds): temp_polys.append(my_geometry) accepted += 1 else: disjoint += 1 else: pass # just discard the triangle # merge polygons as much as possible in order to reduce processing and not having polygons # smaller than parameters.OWBB_GENERATE_LANDUSE_LANDUSE_MIN_AREA if merge_polys: merged_list = merge_buffers(temp_polys) merged_counter += len(merged_list) btg_polys[key] = merged_list else: btg_polys[key] = temp_polys logging.debug('Out of %i faces %i were disjoint and %i were accepted with the bounds.', counter, disjoint, accepted) logging.info('Number of polygons found: %i. Used materials: %s (excluding: %s)', counter, str(materials), str(exclusion_materials)) if merge_polys: logging.info('These were reduced to %i polygons', merged_counter) return btg_polys
def read_stg_entries_in_boundary(transform: Transformation, for_roads: bool) -> List[STGEntry]: """Returns a list of all STGEntries within the boundary according to parameters. Adds all tiles bordering the chosen tile in order to make sure that objects crossing tile borders but maybe located outside also are taken into account. It uses the PATH_TO_SCENERY and PATH_TO_SCENERY_OPT (which are static), not PATH_TO_OUTPUT. If my_cord_transform is set, then for each entry the convex hull is calculated in local coordinates. """ bucket_span = calc_tile.bucket_span( parameters.BOUNDARY_NORTH - (parameters.BOUNDARY_NORTH - parameters.BOUNDARY_SOUTH) / 2) boundary_west = parameters.BOUNDARY_WEST - bucket_span boundary_east = parameters.BOUNDARY_EAST + bucket_span boundary_north = parameters.BOUNDARY_NORTH + 1. / 8. boundary_south = parameters.BOUNDARY_SOUTH - 1. / 8. stg_entries = list() stg_files = calc_tile.get_stg_files_in_boundary( boundary_west, boundary_south, boundary_east, boundary_north, parameters.PATH_TO_SCENERY, "Objects") if parameters.PATH_TO_SCENERY_OPT: for my_path in parameters.PATH_TO_SCENERY_OPT: stg_files_opt = calc_tile.get_stg_files_in_boundary( boundary_west, boundary_south, boundary_east, boundary_north, my_path, "Objects") stg_files.extend(stg_files_opt) for filename in stg_files: stg_entries.extend(read_stg_entries(filename)) # exclude entries in skip list for entry in reversed(stg_entries): if entry.obj_filename in parameters.SKIP_LIST_OVERLAP: stg_entries.remove(entry) # the border of the original tile in local coordinates south_west = transform.to_local( (parameters.BOUNDARY_WEST, parameters.BOUNDARY_SOUTH)) north_east = transform.to_local( (parameters.BOUNDARY_EAST, parameters.BOUNDARY_NORTH)) tile_box = shg.box(south_west[0], south_west[1], north_east[0], north_east[1]) _parse_stg_entries_for_convex_hull(stg_entries, transform, tile_box) # after having all original stg-entries, lets check for exclude areas if for_roads: areas = parameters.OVERLAP_CHECK_EXCLUDE_AREAS_ROADS else: areas = parameters.OVERLAP_CHECK_EXCLUDE_AREAS_BUILDINGS exclude_area_entries = _create_pseudo_stg_entries_for_exclude_areas( areas, transform) # remove all static entries within the exclude areas - as they might have problems in their geometry count_removed = 0 for entry in reversed(stg_entries): if entry.verb_type is not STGVerbType.object_static: continue x, y = transform.to_local((entry.lon, entry.lat)) for fake_entry in exclude_area_entries: if fake_entry.convex_hull.contains(shg.Point(x, y)): stg_entries.remove(entry) count_removed += 1 break # finally add the fake exclude areas to the list of entries logging.info( 'Removed %i static object entries due to OVERLAP_CHECK_EXCLUDE_AREAS', count_removed) stg_entries.extend(exclude_area_entries) return stg_entries