class RoadNetwork(Network): """ Road network class Road network is basically a multi-directed graph with methods for computing fuel consumption and travel time of corresponding vehicles. """ roads = protected_property("edges") def __init__(self, roads, nodes, *args, **kwargs): """ :param roads: Road instance :param nodes: road nodes """ check_type(roads, Road, nodes, RoadNode) super().__init__(roads, nodes, *args, **kwargs) def build_graph(self, weight_one_way=None, weight_return=None): """ Build corresponding multi-directed graph :param weight_one_way: array of weight values for edge in one_way direction :param weight_return: array of weight values for edge in reverse direction :return: """ if weight_one_way is None: weight_one_way = self._edges.length if weight_return is None: weight_return = self._edges.length if len(weight_one_way) != len( self._edges) or len(weight_return) != len(self._edges): raise NetworkError( "Input argument(s) must have the same length as network edges") weight, from_node, to_node = [], [], [] for idx, coords in enumerate(self._edges.from_to): if self._edges.direction[idx] != "reverse": weight.append(weight_one_way[idx]) from_node.append(coords[0]) to_node.append(coords[1]) if self._edges.direction[idx] != "one-way": weight.append(weight_return[idx]) from_node.append(coords[1]) to_node.append(coords[0]) # Create multi-directed graph self._graph = nx.MultiDiGraph() self._graph.add_weighted_edges_from([ (from_n, to_n, w) for from_n, to_n, w in zip(from_node, to_node, weight) ]) return self def fuel_consumption(self, gross_hp, vehicle_weight, vehicle_frontal_area=7.92, engine_efficiency=0.4, fuel_energy_density=35, uphill_hp=0.8, downhill_hp=0.6, drag_resistance=0.35, mass_correction_factor=1.05, acceleration_rate=1.5 * 0.3048, deceleration_rate=-9.5 * 0.3048, rho_air=1.225): """ Compute fuel consumption on road segments :param vehicle_weight: :param gross_hp: :param vehicle_frontal_area: :param engine_efficiency: :param fuel_energy_density: fuel efficiency as L/MJ :param uphill_hp: :param downhill_hp: :param drag_resistance: :param mass_correction_factor: :param acceleration_rate: :param deceleration_rate: :param rho_air: air density :return: """ slope = [ self.roads.slope_of_geometry(i, slope_format="degree") for i in range(len(self.roads)) ] r_curvature = [ self.roads.radius_of_curvature_of_geometry(i) for i in range(len(self.roads)) ] road_length = [ self.roads.length_xyz_of_geometry(i) for i in range(len(self.roads)) ] # Maximum limited speed v_max = self._get_max_limited_speed(slope, r_curvature, vehicle_weight, gross_hp, uphill_hp, downhill_hp) # Maximum speed at intersection v_in_max, v_out_max = self._get_velocity_at_intersection() # Fuel demand fuel_demand = {'one-way': [], 'reverse': []} for n in range(len(self.roads)): # Travel time and distance of acceleration t_time_one_way, d_a_one_way, _ = get_travel_time_and_distance_of_acceleration( v_max["one-way"][n], road_length[n], v_in_max[n], v_out_max[n], acceleration_rate, deceleration_rate) t_time_reverse, d_a_reverse, _ = get_travel_time_and_distance_of_acceleration( v_max["reverse"][n], road_length[n], v_out_max[n], v_in_max[n], acceleration_rate, deceleration_rate) # Travel time (for mean velocity over road segment) v_mean_one_way = road_length[n] / t_time_one_way v_mean_reverse = road_length[n] / t_time_reverse # Energy demand u_r = self.roads.rolling_coefficient[n] * vehicle_weight * 9.81 * np.cos(slope[n] * np.pi / 180) * \ road_length[n] u_a_one_way = 0.5 * rho_air * vehicle_frontal_area * drag_resistance * v_mean_one_way**2 * road_length[ n] u_a_reverse = 0.5 * rho_air * vehicle_frontal_area * drag_resistance * v_mean_reverse**2 * road_length[ n] u_i_one_way = mass_correction_factor * vehicle_weight * acceleration_rate * d_a_one_way u_i_reverse = mass_correction_factor * vehicle_weight * acceleration_rate * d_a_reverse u_g_one_way = vehicle_weight * 9.81 * np.sin( slope[n] * np.pi / 180) * road_length[n] u_g_reverse = vehicle_weight * 9.81 * np.sin( -slope[n] * np.pi / 180) * road_length[n] fuel_demand["one-way"].append( np.maximum(0, (u_r + u_a_one_way + u_i_one_way + u_g_one_way) * 1e-6 / (fuel_energy_density * engine_efficiency))) fuel_demand["reverse"].append( np.maximum(0, (u_r + u_a_reverse + u_i_reverse + u_g_reverse) * 1e-6 / (fuel_energy_density * engine_efficiency))) return fuel_demand def travel_time(self, gross_hp, vehicle_weight, acceleration_rate=1.5 * 0.3048, deceleration_rate=-9.5 * 0.3048, uphill_hp=0.8, downhill_hp=0.6, time_format='h'): """ Compute travel time for each road segment Compute travel time for each road element according to given parameters :param gross_hp: gross horse power of the vehicle :param vehicle_weight: weight of the vehicle :param acceleration_rate: positive acceleration value :param deceleration_rate: negative acceleration value (deceleration) :param uphill_hp: available horsepower on uphill road (%) :param downhill_hp: available horsepower on downhill road (%) :param time_format: format of output time (seconds, minutes, hours) :return: """ travel_time = {'one-way': [], 'reverse': []} slope = [ self.roads.slope_of_geometry(i, slope_format="degree") for i in range(len(self.roads)) ] r_curvature = [ self.roads.radius_of_curvature_of_geometry(i) for i in range(len(self.roads)) ] road_length = [ self.roads.length_xyz_of_geometry(i) for i in range(len(self.roads)) ] # Maximum limited speed v_max = self._get_max_limited_speed(slope, r_curvature, vehicle_weight, gross_hp, uphill_hp, downhill_hp) # Maximum speed at intersection v_in_max, v_out_max = self._get_velocity_at_intersection() for v, d, v_in, v_out in zip(v_max["one-way"], road_length, v_in_max, v_out_max): time, _, _ = get_travel_time_and_distance_of_acceleration( v, d, v_in, v_out, acceleration_rate, deceleration_rate) travel_time["one-way"].append(TIME_FORMAT[time_format] * time) for v, d, v_in, v_out in zip(v_max["reverse"], road_length, v_in_max, v_out_max): time, _, _ = get_travel_time_and_distance_of_acceleration( v[::-1], d[::-1], v_out, v_in, acceleration_rate, deceleration_rate) travel_time["reverse"].append(TIME_FORMAT[time_format] * time) return travel_time def velocity(self, gross_hp, vehicle_weight, acceleration_rate=1.5 * 0.3048, deceleration_rate=-9.5 * 0.3048, uphill_hp=0.8, downhill_hp=0.6): """ Compute velocity for each road segment :param gross_hp: :param vehicle_weight: :param acceleration_rate: :param deceleration_rate: :param uphill_hp: :param downhill_hp: :return: """ velocity = {'one-way': [], 'reverse': []} slope = [ self.roads.slope_of_geometry(i, slope_format="degree") for i in range(len(self.roads)) ] r_curvature = [ self.roads.radius_of_curvature_of_geometry(i) for i in range(len(self.roads)) ] road_length = [ self.roads.length_xyz_of_geometry(i) for i in range(len(self.roads)) ] # Maximum limited speed v_max = self._get_max_limited_speed(slope, r_curvature, vehicle_weight, gross_hp, uphill_hp, downhill_hp) # Maximum speed at intersection v_in_max, v_out_max = self._get_velocity_at_intersection() for v, d, v_in, v_out in zip(v_max["one-way"], road_length, v_in_max, v_out_max): _, _, speed = get_travel_time_and_distance_of_acceleration( v, d, v_in, v_out, acceleration_rate, deceleration_rate) velocity["one-way"].append(speed) for v, d, v_in, v_out in zip(v_max["reverse"], road_length, v_in_max, v_out_max): _, _, speed = get_travel_time_and_distance_of_acceleration( v[::-1], d[::-1], v_out, v_in, acceleration_rate, deceleration_rate) velocity["reverse"].append(speed) velocity["v_max_one_way"] = v_max["one-way"] velocity["v_max_reverse"] = v_max["reverse"] velocity["v_slope_one_way"] = v_max["slope_one_way"] velocity["v_slope_reverse"] = v_max["slope_reverse"] velocity["v_curvature"] = v_max["curvature"] return velocity def _get_velocity_at_intersection(self): """ Velocity in crossing intersections Define maximum allowed entering and exiting velocities for each road segment :return: """ node_coords = [ (x, y) for x, y in zip(self.nodes.geometry.x, self.nodes.geometry.y) ] v_in = np.full(len(self.roads), 0) v_out = np.full(len(self.roads), 0) for from_to in self.roads.from_to: v_in[node_coords.index( from_to[0])] = self.nodes.max_speed[node_coords.index( from_to[0])] v_out[node_coords.index( from_to[1])] = self.nodes.max_speed[node_coords.index( from_to[1])] return v_in, v_out ################# # Private methods def _get_max_limited_speed(self, slope, r_curvature, vehicle_weight, gross_hp, uphill_hp, downhill_hp): """ Get maximum limited speed on road segments :param slope: :param r_curvature: :param vehicle_weight: :param gross_hp: :param uphill_hp: :param downhill_hp: :return: """ v_max = dict(slope_one_way=[], slope_reverse=[], curvature=[]) # Maximum speed due to slope (1 mechanical hp = 745.699872 W) ehp_uphill = gross_hp * uphill_hp * 745.699872 ehp_downhill = gross_hp * downhill_hp * 745.699872 for n in range(len(self.roads)): v_one_way = np.zeros(len(slope[n])) v_reverse = np.zeros(len(slope[n])) grade_resistance = 9.81 * vehicle_weight * np.sin( np.fabs(slope[n]) * np.pi / 180) rolling_resistance = 9.81 * self.roads.rolling_coefficient[ n] * vehicle_weight * np.cos(slope[n] * np.pi / 180) v_one_way[slope[n] < 0] = ehp_downhill / np.maximum( (grade_resistance[slope[n] < 0] - rolling_resistance[slope[n] < 0]), 0) v_one_way[slope[n] >= 0] = ehp_uphill / ( grade_resistance[slope[n] >= 0] + rolling_resistance[slope[n] >= 0]) v_reverse[slope[n] > 0] = ehp_downhill / np.maximum( (grade_resistance[slope[n] > 0] - rolling_resistance[slope[n] > 0]), 0) v_reverse[slope[n] <= 0] = ehp_uphill / ( grade_resistance[slope[n] <= 0] + rolling_resistance[slope[n] <= 0]) v_max["slope_one_way"].append(v_one_way) v_max["slope_reverse"].append(v_reverse) v_max["curvature"].append((self.roads.rollover_criterion[n] * r_curvature[n] * 9.81)**0.5) # Get maximum limiting speed, i.e. minimum among all previous values v_max["one-way"] = [ np.minimum(np.minimum(v_r, v_s), v_limit) for v_r, v_s, v_limit in zip( v_max["curvature"], v_max["slope_one_way"], self.roads.max_speed) ] v_max["reverse"] = [ np.minimum(np.minimum(v_r, v_s), v_limit) for v_r, v_s, v_limit in zip( v_max["curvature"], v_max["slope_reverse"], self.roads.max_speed) ] return v_max
class Network: """ Network base class Use this class to implement sub-class network geometry (e.g. from shapefile) and apply corresponding tools """ edges = protected_property("edges") nodes = protected_property("nodes") _graph = None def __init__(self, edges, nodes, match_edge_nodes=True, tolerance=1): """ Network class constructor :param edges: Edge instance :param nodes: Node instance :param match_edge_nodes: Boolean --> match edge nodes with respect to tolerance :param tolerance: distance tolerance for considering nodes and edge nodes the same (in m) """ check_type(edges, Edge, nodes, Node) self._edges = edges self._nodes = nodes # Retrieve edge nodes corresponding to nodes if match_edge_nodes: edge_nodes = self._edges.get_nodes() distance, nn = nodes.distance_and_nearest_neighbor(edge_nodes) self._nodes["geometry"] = [edge_nodes.geometry[n] for n in nn] self._nodes = self._nodes[distance <= tolerance] def _get_nearest_edge_node(self): """ Get nearest node from edge :return: """ nodes = self._edges.get_nodes() idx = r_tree_idx(nodes.geometry) edge_nodes = [] for geom in self.nodes.geometry: nn = list(idx.nearest(geom.bounds, 1)) edge_nodes.append(nodes.geometry[nn[0]]) return Node.from_gpd(geometry=edge_nodes, crs=self._edges.crs) @abstractmethod def build_graph(self, *args, **kwargs): pass def get_self_loops(self): """ Get self-loop edges in network :return: list of edge IDs """ return self_loops(self.graph) def get_remote_edges(self): """ Get remote edges in network :return: list of edge IDs """ return remote_edges(self.graph) def get_multi_edges(self): """ Get multi-edges in network :return: list of list of edge IDs """ return multi_edges(self.graph) def get_minimum_distance_to_network(self, layer): """ get minimum distance from given layer to network :param layer: :return: """ distance_to_edge = layer.distance(self.edges) distance_to_node = layer.distance(self.nodes) return np.minimum(distance_to_edge, distance_to_node) @type_assert(node_start=Point, node_end=Point) def get_shortest_path(self, node_start, node_end): """ Get shortest path between 2 nodes using Dijkstra algorithm :param node_start: shapely Point :param node_end: shapely Point :return: Edge instance of the path """ if node_start not in self.nodes.geometry or node_end not in self.nodes.geometry: raise EdgeError("Either source or destination node is invalid") node_start = (node_start.x, node_start.y) node_end = (node_end.x, node_end.y) if node_start == node_end: return [] # Empty path try: path = nx.dijkstra_path(self.graph, node_start, node_end) except nx.NetworkXNoPath: print("No available path between node {} and node {}".format( node_start, node_end)) return None else: return self._edges.get_path(path) @type_assert(node_start=Point, node_end=Point) def get_shortest_path_length(self, node_start, node_end, method: str = "networkx"): """ Get dijkstra shortest path length :param node_start: shapely Point :param node_end: shapely Point :param method: {'internal', 'networkx'} :return: length of path in m """ check_string(method, ("internal", "networkx")) if method == "internal": edge_path = self.get_shortest_path(node_start, node_end) length = 0 if edge_path is not None and edge_path != []: for edge in edge_path.geometry: length += edge.length elif edge_path is None: return None else: node_start = (node_start.x, node_start.y) node_end = (node_end.x, node_end.y) length = nx.dijkstra_path_length(self.graph, node_start, node_end) return length def get_all_shortest_paths(self): """ Get shortest paths between all graph nodes :return: """ return nx.all_pairs_dijkstra_path(self.graph) def get_all_shortest_path_lengths(self): """ Get shortest path lengths between all graph nodes :return: """ return nx.all_pairs_dijkstra_path_length(self.graph) @type_assert(source_node=Point) def get_all_shortest_paths_from_source(self, source_node): """ Get all paths from one source node using Dijkstra :param source_node: :return: """ if source_node not in self.nodes.geometry: raise NetworkError("Source node is invalid") source_node = (source_node.x, source_node.y) return nx.single_source_dijkstra_path(self.graph, source_node) @type_assert(source_node=Point) def get_all_shortest_path_lengths_from_source(self, source_node): """ :param source_node: :return: """ if source_node not in self.nodes.geometry: raise NetworkError("Source node is invalid") source_node = (source_node.x, source_node.y) return nx.single_source_dijkstra_path_length(self.graph, source_node) @type_assert(source_node=Point) def get_shortest_paths_from_source(self, source_node, target_nodes): """ Get multiple shortest paths from single source using Dijkstra :param source_node: :param target_nodes: :return: """ try: check_type_in_collection(target_nodes, Point) except TypeError: raise NetworkError("'%s' must be a collection of Point instances" % target_nodes) paths = [] all_paths = self.get_all_shortest_paths_from_source(source_node) for target in target_nodes: target = (target.x, target.y) if target in all_paths.keys(): paths.append(self._edges.get_path(all_paths[target])) return paths @type_assert(source_node=Point) def get_shortest_path_lengths_from_source(self, source_node, target_nodes): """ :param source_node: :param target_nodes: :return: """ try: check_type_in_collection(target_nodes, Point) except TypeError: raise NetworkError("'%s' must be a collection of Point instances" % target_nodes) path_lengths = [] all_path_lengths = self.get_all_shortest_path_lengths_from_source( source_node) for target in target_nodes: target = (target.x, target.y) if target in all_path_lengths.keys(): path_lengths.append(all_path_lengths[target]) return path_lengths def get_shortest_path_matrix(self): """ Get shortest path matrix Compute shortest path length between all starting and ending nodes :return: """ shortest_path = np.full((len(self.nodes), len(self.nodes)), np.nan) edge_nodes = self._get_nearest_edge_node() for i, geom_from in enumerate(edge_nodes.geometry): for n, geom_to in enumerate(edge_nodes.geometry): shortest_path[i, n] = self._edges.get_dijkstra_path_length( geom_from, geom_to) return shortest_path def plot(self, edge_color="blue", node_color="red"): """ :param edge_color: :param node_color: :return: """ self.edges.plot(layer_color=edge_color) self.nodes.plot(layer_color=node_color) @property def graph(self): if self._graph is None: raise NetworkError("Corresponding graph has not been built") else: return self._graph
class Edge(LineLayer): """ Edge class Class for implementing edges in a geo network """ from_to = protected_property('from_to') DEFAULT_DIRECTION = "two-ways" def __init__(self, edges, *args, **kwargs): """ Edge class constructor :param edges: geo-like file (e.g. shapefile) or geopandas data frame """ super().__init__(edges, *args, **kwargs) # Set Edge specific attributes if "direction" not in self.attributes(): self["direction"] = self.DEFAULT_DIRECTION # self._gpd_df["direction"] = [self.DEFAULT_DIRECTION] * len(self) # Set default direction (not directed) # Simplified edges self._from_to = [((geom.coords.xy[0][0], geom.coords.xy[1][0]), (geom.coords.xy[0][-1], geom.coords.xy[1][-1])) for geom in self.geometry] # Corresponding undirected multi-graph self._graph = nx.MultiGraph() self._graph.add_edges_from([(from_to[0], from_to[1], _id) for _id, from_to in enumerate(self.from_to) ]) # Override point layer class attribute (To Edge is associated Node) self._point_layer_class = Node def _check_attr_and_dic(self, attr_name, dic): if attr_name not in self.attributes(): raise EdgeError("Unknown attribute name '%s'" % attr_name) if any([val not in list(set(self[attr_name])) for val in dic.keys()]): raise EdgeError("Invalid key in '%s'" % dic) def find_disconnected_islands_and_fix(self, tolerance=None, method="delete"): """ Find disconnected components in network Find disconnected components/islands graphs in multi-graph and apply method (fix/reconnect, keep, delete) with respect to a given tolerance :param tolerance: :param method: :return: """ method = check_string( method, {'reconnect_and_delete', 'reconnect_and_keep', 'delete'}) sub_graphs = list((self._graph.subgraph(c) for c in nx.connected_components(self._graph))) main_component = max(sub_graphs, key=len) sub_graphs.remove(main_component) idx_edge = [] if method == "delete": for graph in sub_graphs: for edge in graph.edges: try: idx_edge.append(self.from_to.index((edge[0], edge[1]))) except ValueError: idx_edge.append(self.from_to.index((edge[1], edge[0]))) elif method == 'reconnect_and_delete': pass elif method == 'reconnect_and_keep': pass return self.drop(self.index[idx_edge]) # TODO: implement island reconnection and tolerance (see Edge.reconnect() method) @return_new_instance def get_path(self, path): edge_path = GeoDataFrame(columns=self.attributes(), crs=self.crs) for i in range(len(path) - 1): try: edge_path = edge_path.append( self._gpd_df.loc[self._from_to.index( (path[i], path[i + 1]))], ignore_index=True) except ValueError: edge_path = edge_path.append( self._gpd_df.loc[self._from_to.index( (path[i + 1], path[i]))], ignore_index=True) return edge_path def get_end_nodes(self): """ Get end nodes of edges Get end nodes, that is edge's end that does not connect to another edge :return: Node layer and list of edge IDs """ end = [(node, list(self._graph.edges(node, keys=True))[0]) for node in self._graph.nodes() if len(self._graph.edges(node)) == 1] return self._point_layer_class.from_gpd(geometry=[Point(n[0]) for n in end], crs=self.crs),\ [n[1][2] for n in end] def get_nodes(self): """ Get nodes from edges :return: """ return self._point_layer_class.from_gpd( geometry=[Point(node) for node in self._graph.nodes()], crs=self.crs) def get_multi_edges(self): """ Get multiple edges between 2 nodes :return: list of edge IDs that connect the same nodes """ return multi_edges(self._graph) def get_remote_edges(self): """ Get remote edges Remote edges are not connected to anything :return: list of edge IDs that are remote """ return remote_edges(self._graph) def get_self_loops(self): """ Get self-loop edges :return: list of edge IDs that are self-loops """ return self_loops(self._graph) @return_new_instance def get_simplified(self): """ Return simplified edge Return simplified Edge instance, that is only with starting and ending coordinates of each road segment :return: """ return GeoDataFrame( self._gpd_df.copy(), geometry=[LineString(from_to) for from_to in self._from_to], crs=self.crs) def get_single_edges(self): """ Get single edges Retrieve single edges, that is from intersection to intersection. Typically, when a node has only 2 neighbors, the corresponding edges can be merged into a new one. :return: list of list of edge IDs representing unique elements """ to_merge = [] all_connected_edges = [] for u, v, edge_id in self._graph.edges(keys=True): if edge_id not in all_connected_edges: connected_nodes = [u, v] connected_edges = [edge_id] for node in [u, v]: previous_node = node while "There are neighbors to connect": next_node = [ n for n in self._graph.neighbors(previous_node) if n not in connected_nodes ] if not next_node or len(next_node) > 1: break else: connected_edges.extend( list(self._graph[previous_node][next_node[0]])) connected_nodes.extend(next_node) previous_node = next_node[ 0] # previous node is now the next node in the chain all_connected_edges.extend(connected_edges) to_merge.append(connected_edges) return to_merge @return_new_instance def merge2(self): """ Merge edges from intersection to intersection Merge with respect to single entities, that is from intersecting node to intersecting node (node with more than 2 neighbors). :return: """ single_edges = self.get_single_edges() geometry, rows = [], [] for line in single_edges: geometry.append(linemerge(self.geometry[line].values)) rows.append(self._gpd_df.iloc[line[-1]]) return GeoDataFrame(rows, geometry=geometry, crs=self.crs) @return_new_instance def reconnect(self, tolerance): """ Reconnect disconnected edges with respect to tolerance :param tolerance: min distance (in m) for reconnection :return: """ # TODO: link with Edge.find_disconnected_islands_and_fix() method outdf = self._gpd_df.copy() nodes, edge_idx = self.get_end_nodes() nearest_nodes, node_idx = nodes.nearest_neighbors(nodes, tolerance) connected_nodes = [] for n, n_nodes in enumerate(nearest_nodes): n_idx = [ node for node in node_idx[n] if node not in connected_nodes ] if len(n_idx) > 1: e_idx = [edge_idx[i] for i in n_idx] connected_nodes.extend( [i for i in n_idx if i not in connected_nodes]) # Use outdf.geometry to ensure that changes are saved new_geometry = connect_lines_to_point( outdf.geometry[e_idx], centroid(n_nodes.geometry)) for edge, geom in zip(e_idx, new_geometry): outdf.loc[edge, "geometry"] = geom return outdf @type_assert(attribute_name=str, direction_dic=dict) def set_direction(self, attribute_name, direction_dic): """ Set edge direction :param attribute_name: layer attribute from which direction must be derived :param direction_dic: (valid direction values: "two-ways", "one-way", "reverse", None) :return: """ self._check_attr_and_dic(attribute_name, direction_dic) for key in direction_dic.keys(): if direction_dic[key] not in [ 'two-ways', 'one-way', 'reverse', None ]: raise EdgeError("'%s' is not a valid direction value" % direction_dic[key]) self._gpd_df.loc[self[attribute_name] == key, "direction"] = direction_dic[key] def split_at_ending_edges(self): """ Split edge on which ends another edge :return: """ nodes, edge_idx = self.get_end_nodes() splitting_nodes = nodes[[ True if intersects(geom, self.geometry, self.r_tree_idx).count(True) > 1 else False for geom in nodes.geometry ]] return self.split_at_points(splitting_nodes) def split_at_underlying_points(self, location, *args): """ Override parent class method Split corresponding attributes in addition to layer, i.e. :param location: :return: """ output = super().split_at_underlying_points(location) if len(args) == 0: return output outputs = [output] for attr in args: split_attr = [] for n, a in enumerate(attr): break_idx = [loc[1] for loc in location if loc[0] == n] if len(break_idx) == 0: split_attr.append(a) else: split_attr.extend( split_list_by_index(a, break_idx, include=False)) outputs.append(split_attr) return tuple(outputs) @property def direction(self): return self["direction"] @property def from_node(self): return [coords[0] for coords in self._from_to] @property def to_node(self): return [coords[1] for coords in self._from_to]
class RasterMap: """ RasterMap base class A RasterMap is a numpy array corresponding to a geo raster When the raster is stored using numpy array, and when it is possible, methods rely on numpy/rasterio packages, otherwise gdal is used """ # Mapping numpy types to gdal types (thanks to # https://gist.github.com/CMCDragonkai/ac6289fa84bcc8888035744d7e00e2e6) _numpy_to_gdal_type = {'uint8': 1, 'int8': 1, 'uint16': 2, 'int16': 3, 'uint32': 4, 'int32': 5, 'float32': 6, 'float64': 7, 'complex64': 10, 'compex128': 11} _numpy_to_ogr_type = {'uint8': 0, 'int8': 0, 'uint16': 0, 'int16': 0, 'uint32': 0, 'int32': 0, 'float32': 2, 'float64': 2} # GDAL/OGR underlying attributes _gdal_drv = gdal.GetDriverByName('GTiff') _ogr_shp_drv = ogr.GetDriverByName('ESRI Shapefile') _ogr_geojson_drv = ogr.GetDriverByName('GeoJSON') # Temp file _temp_raster_file = None # SetAccess = protected (property decorator) geo_grid = protected_property('geo_grid') raster_array = protected_property('raster_array') crs = protected_property('crs') x_origin = protected_property('x_origin') y_origin = protected_property('y_origin') res = protected_property('res') x_size = protected_property('x_size') y_size = protected_property('y_size') shape = protected_property('shape') no_data_value = protected_property('no_data_value') def __init__(self, raster, geo_grid: GeoGrid = None, no_data_value=None, crs=None): """ RasterMap constructor :param raster: raster file or numpy array :param geo_grid: GeoGrid instance :param no_data_value: data to be regarded as "no_data" :param crs: projection used for the raster :Example: >>> """ check_type(raster, (str, np.ndarray)) if type(raster) == np.ndarray: raster_file = None try: test_fit = geo_grid.data_fits(raster) crs = pyproj.CRS(crs) # crs = proj4_from(crs) if not test_fit: raise RasterMapError("Input geo grid does not fit raster") except AttributeError: raise RasterMapError("Geo grid argument has not been set") except (ValueError, TypeError): raise RasterMapError("Invalid projection: crs='{}'".format(crs)) else: raster_file = raster try: geo_grid = GeoGrid.from_raster_file(raster) crs = crs_from_raster(raster) raster = raster_to_array(raster) except RuntimeError: raise RasterMapError("Invalid/unknown file '%s'" % raster_file) # Set attributes self._raster_file = raster_file self._geo_grid = geo_grid self._raster_array = np.array(raster, dtype='float64') # Ensure compatibility with NaNs self._crs = crs self._res = self._geo_grid.res self._x_origin = self._geo_grid.geo_transform[0] self._y_origin = self._geo_grid.geo_transform[3] self._x_size = self._geo_grid.num_x self._y_size = self._geo_grid.num_y self._shape = self._raster_array.shape self._no_data_value = no_data_value if no_data_value is not None: self._raster_array[self._raster_array == no_data_value] = np.nan # Use attribute (raster_array) rather than # instance (self == ...) to avoid 'recursion' error with decorator above # Available filters self._filters = {"majority_filter": self._majority_filter, "sieve": self._gdal_sieve} def __del__(self): try: os.remove(self._temp_raster_file) except TypeError: pass @type_assert(filter_name=str) def apply_filter(self, filter_name, *args, **kwargs): """ Apply filter to raster :param filter_name: :param args: list of arguments related to filter function :param kwargs: list of keyword args related to filter function :return: """ return self._filters[filter_name](*args, **kwargs) @return_new_instance @type_assert(geo_layer=PolygonLayer) def clip(self, geo_layer: PolygonLayer, crop=False, all_touched=False): """ Clip raster according to GeoLayer polygon(s) Keep only points which are inside polygon boundaries and crop raster if necessary. :param geo_layer: GeoLayer instance :param crop: if True, crop raster :param all_touched: when True, all cells touched by polygons are considered within :return: """ check_proj(geo_layer.crs, self.crs) if crop: new_raster = self.get_raster_at(geo_layer) return new_raster.clip(geo_layer, crop=False, all_touched=all_touched) else: return self._burn_layer_values(geo_layer, False, all_touched) @return_new_instance def contour(self, interval, absolute_interval=True, percentile_min=2, percentile_max=98): """ Extract contour from raster :param interval: interval between contour lines :param absolute_interval: relative or absolute interval ? :param percentile_min: :param percentile_max: :return: """ values = self.raster_array[~np.isnan(self.raster_array)] v_min, v_max = np.percentile(values, [percentile_min, percentile_max]) if not absolute_interval: interval *= v_max contour_range = np.linspace(v_min, v_max, int((v_max - v_min)/interval) + 1) new_raster_array = np.zeros(self.raster_array.shape) new_raster_array[self.raster_array_without_nans < v_min] = np.mean(values[values < v_min]) new_raster_array[self.raster_array_without_nans >= v_max] = np.mean(values[values >= v_max]) for bins in zip(contour_range[0:-1], contour_range[1::]): new_raster_array[(self.raster_array_without_nans >= bins[0]) & (self.raster_array_without_nans < bins[ 1])] = np.mean(values[(values >= bins[0]) & (values < bins[1])]) new_raster_array[self.is_no_data()] = self.no_data_value return new_raster_array def copy(self): return copy.deepcopy(self) @return_new_instance @type_assert(factor=int, method=str, no_limit=bool) def disaggregate(self, factor: int = 1, method: str = 'nearest', no_limit=False): """ Disaggregate raster cells :param factor: scale factor for disaggregation (number of cells) :param method: 'linear' or 'nearest' (default = 'nearest') :param no_limit: no limit for disaggregation (default=False) :return: """ from scipy.interpolate import RegularGridInterpolator upper_limit = np.inf if no_limit else 10**8 / (self.geo_grid.num_x * self.geo_grid.num_y) if 1 < factor <= upper_limit: new_geo_grid = self.geo_grid.to_res(self.res/factor) try: interpolator = RegularGridInterpolator((self.geo_grid.lats, self.geo_grid.lons), self.raster_array[::-1, :], bounds_error=False, method=method) except ValueError: raise RasterMapError("Method should be 'linear' or 'nearest' but is {}".format(method)) return interpolator((new_geo_grid.latitude, new_geo_grid.longitude)), new_geo_grid else: warnings.warn("Invalid factor, factor = 1, or exceeded limit (set no_limit=True). Return copy of object") return self.copy() @return_new_instance @type_assert(geo_layer=PolygonLayer) def exclude(self, geo_layer: PolygonLayer, all_touched=False): """ Exclude raster cells within GeoLayer polygon(s) Keep only points outside from layer boundaries :param geo_layer: :param all_touched: when True, all cells touched by polygons are regarded as within :return: """ check_proj(geo_layer.crs, self.crs) return self._burn_layer_values(geo_layer, True, all_touched) def gdal_clip(self, extent): """ Static method for clipping large raster :param extent: array/list as [x_min, y_min, x_max, y_max] :return: """ pass def gdal_resample(self, factor): """ Resampling raster using GDAL Unlike the 'disaggregate' method, GDAL uses files for resampling (faster for large areas) :param factor: :return: """ return self._gdal_resample_raster(factor) @return_new_instance @collection_type_assert(ll_point=dict(collection=(list, tuple), length=2, type=(int, float)), ur_point=dict(collection=(list, tuple), type=(int, float), length=2)) def get_raster_at(self, layer=None, ll_point=None, ur_point=None): """ Extract sub-raster in current raster map Extract new raster from current raster map by giving either a geo lines_ or a new geo-square defined by lower-left point (ll_point) and upper right point (ur_point) such as for geo grids. :param layer: GeoLayer instance :param ll_point: tuple of 2 values (lat, lon) :param ur_point: tuple of 2 values (lat, lon) :return: RasterMap """ if layer is not None: check_proj(layer.crs, self.crs) ll_point = (layer.bounds[1], layer.bounds[0]) # Warning: (lat, lon) in that order ! ur_point = (layer.bounds[3], layer.bounds[2]) try: ll_point_r, ll_point_c = self._geo_grid.latlon_to_2d_index(ll_point[0], ll_point[1]) ur_point_r, ur_point_c = self._geo_grid.latlon_to_2d_index(ur_point[0], ur_point[1]) return self.raster_array[ur_point_r:ll_point_r + 1, ll_point_c:ur_point_c + 1], \ self.geo_grid[ur_point_r:ll_point_r + 1, ll_point_c:ur_point_c + 1] except GeoGridError: raise RasterMapError("Lower left or/and upper right points have not been rightly defined") def get_value_at(self, latitude, longitude): """ Get single raster value at latitude, longitude :param latitude: :param longitude: :return: """ r, c = self._geo_grid.latlon_to_2d_index(latitude, longitude) return self._raster_array[r, c] def is_latlong(self): return self.crs.is_geographic def is_no_data(self): return np.isnan(self.raster_array) def max(self): """ Return maximum value of raster :return: """ return np.nanmax(self.raster_array) def mean(self): """ Compute mean of raster map Mean of raster values :return: """ return np.nanmean(self.raster_array) def min(self): """ Return minimum value of raster :return: """ return np.nanmin(self.raster_array) def plot(self, ax=None, cmap=None, colorbar=False, colorbar_title=None, **kwargs): """ Plot Raster :param ax: :param cmap: :param colorbar: :param colorbar_title: :param kwargs: :return: """ extent = [self.x_origin, self.x_origin + self.res * self.x_size, self.y_origin - self.res * self.y_size, self.y_origin] if ax is None: _, ax = plt.subplots() # Use imshow to plot raster img = ax.imshow(self.raster_array, extent=extent, cmap=cmap, vmin=self.min(), vmax=self.max(), **kwargs) if colorbar: cbar = plt.colorbar(img) cbar.ax.set_ylabel(colorbar_title) return ax @type_assert(field_name=str) def polygonize(self, field_name, layer_name="layer", is_8_connected=False): """ Convert raster into vector polygon(s) :param field_name: name of the corresponding field in the final shape file :param layer_name: name of resulting layer :param is_8_connected: pixel connectivity used for polygon :return: """ check_type(is_8_connected, bool) with ShapeTempFile() as out_shp: self._gdal_polygonize(out_shp, layer_name, field_name, is_8_connected) return PolygonLayer(out_shp, name=layer_name) def sum(self): """ Compute sum of raster map :return: """ return np.nansum(self.raster_array) def surface(self): """ Return array of raster cell surface values :return: numpy ndarray """ surface = compute_surface(self.geo_grid.longitude - self.geo_grid.res / 2, self.geo_grid.longitude + self.geo_grid.res / 2, self.geo_grid.latitude + self.geo_grid.res / 2, self.geo_grid.latitude - self.geo_grid.res / 2, self.geo_type, ellipsoid_from(self.crs)) return surface def to_crs(self, crs): """ Reproject raster onto new CRS :param crs: :return: """ try: if self.crs != crs: srs = srs_from(crs) return self._gdal_warp(srs) else: return self.copy() except ValueError: raise RasterMapError("Invalid CRS '%s'" % crs) def to_file(self, raster_file, data_type=None): """ Save raster to file :param raster_file: :param data_type: data type :return: """ if data_type is None: dtype = self._numpy_to_gdal_type[self.data_type] else: try: dtype = self._numpy_to_gdal_type[data_type] except KeyError: raise RasterMapError("Invalid data type '%s'" % data_type) out = array_to_raster(raster_file, self.raster_array_without_nans, self.geo_grid, self.crs, datatype=dtype, no_data_value=self.no_data_value) return out @classmethod def is_equally_referenced(cls, *args): """ Check geo referencing equality between rasters :param args: :return: """ return False not in [raster_1.geo_grid == raster_2.geo_grid for raster_1, raster_2 in zip(args[:-1], args[1::])] @property def data_type(self): return str(self.raster_array.dtype) @property def geo_type(self): if self.is_latlong(): return 'latlon' else: return 'equal' @property def raster_array_without_nans(self): array = self.raster_array.copy() array[np.isnan(array)] = self.no_data_value return array @property def raster_file(self): """ Return underlying raster file If raster file does not exist, create a temporary one :return: """ if not isfile(self._raster_file): self._raster_file = os.path.join(tempfile.gettempdir(), str(uuid.uuid4())) self.to_file(self._raster_file) self._temp_raster_file = self._raster_file # with RasterTempFile() as file: # self._raster_file = file # self.to_file(self._raster_file) return self._raster_file @classmethod def merge(cls, list_of_raster, bounds=None): """ Merge several RasterMap instances :param list_of_raster: list of RasterMap class instance(s) :param bounds: bounds of the output RasterMap :return: """ # TODO: use gdal merge method list_of_datasets = [] for raster_map in list_of_raster: list_of_datasets.append(rasterio_open(raster_map.raster_file, 'r')) # Merge using rasterio array, transform = rasterio_merge(list_of_datasets, bounds=bounds) with RasterTempFile() as file: with rasterio_open(file, 'w', driver="GTiff", height=array.shape[1], width=array.shape[2], count=1, dtype=array.dtype, crs=list_of_raster[0].crs, transform=transform) as out_dst: out_dst.write(array.squeeze(), 1) return cls(file, no_data_value=list_of_raster[0].no_data_value) def __getitem__(self, key): if key.__class__ == type(self): key = key.raster_array if key.__class__ == slice: key = (key, key) if key.__class__ == tuple: if key[0].__class__ == int and key[1].__class__ == int: return self.raster_array.__getitem__(key) else: try: return self.__class__(self.raster_array.__getitem__(key), self.geo_grid.__getitem__(key), no_data_value=self.no_data_value, crs=self.crs) except IndexError: raise RasterMapError("Invalid indexing") except Exception as e: raise RuntimeError("Unknown error while getting data in raster map: {}".format(e)) elif key.__class__ == np.ndarray: return self.raster_array.__getitem__(key) else: raise RasterMapError("Invalid indexing") def __setitem__(self, key, value): if type(key) == type(self): self.raster_array.__setitem__(key.raster_array, value) else: try: self.raster_array.__setitem__(key, value) except IndexError: raise RasterMapError("Invalid indexing") except Exception as e: raise RuntimeError("Unknown error while setting data in raster map: {}".format(e)) return self def __rmul__(self, other): return self * other def __radd__(self, other): return self + other def __rsub__(self, other): return -self + other def __mul__(self, other): return self._apply_operator(other, '__mul__', '*') def __add__(self, other): return self._apply_operator(other, '__add__', '+') def __sub__(self, other): return self._apply_operator(other, '__sub__', '-') def __and__(self, other): return self._apply_comparison(other, '__and__', '&') def __or__(self, other): return self._apply_comparison(other, '__or__', '|') def __xor__(self, other): return self._apply_comparison(other, '__xor__', '^') def __eq__(self, other): return self._apply_comparison(other, '__eq__', '==') def __ne__(self, other): return self._apply_comparison(other, '__ne__', '!=') def __gt__(self, other): return self._apply_comparison(other, '__gt__', '>') def __lt__(self, other): return self._apply_comparison(other, '__lt__', '<') def __ge__(self, other): return self._apply_comparison(other, '__ge__', '>=') def __le__(self, other): return self._apply_comparison(other, '__le__', '<=') @return_new_instance def __neg__(self): return -self.raster_array # @return_new_instance def _apply_comparison(self, other, operator_function, operator_str): valid_values = np.full(self.raster_array.shape, False) if isinstance(other, RasterMap): other = other.raster_array try: valid_values[(~np.isnan(self.raster_array)) & (~np.isnan(other))] = True valid_values[valid_values] = self.raster_array[valid_values].__getattribute__(operator_function)( other[valid_values]) return valid_values except (TypeError, IndexError): valid_values[~np.isnan(self.raster_array)] = True valid_values[valid_values] = self.raster_array[valid_values].__getattribute__(operator_function)(other) return valid_values except Exception as e: raise RasterMapError("Comparison for '{}' has failed ({})".format(operator_str, e)) @return_new_instance def _apply_operator(self, other, operator_function, operator_str): if isinstance(other, RasterMap): if self.geo_grid == other.geo_grid: return self.raster_array.__getattribute__(operator_function)(other.raster_array) else: raise RasterMapError("Raster maps are not defined on the same geo grid") else: try: return self.raster_array.__getattribute__(operator_function)(other) except TypeError: raise RasterMapError("Unsupported operand type(s) for {}: '{}' and '{}'".format(operator_str, type(self).__name__, type(other).__name__)) except ValueError: raise RasterMapError("No match for operand {} between '{}' and '{}'".format(operator_str, type(self).__name__, type(other).__name__)) except Exception as e: raise RuntimeError("Unexpected error when applying {} between '{}' and '{}': {}" .format(operator_str, type(self).__name__, type(other).__name__, e)) def _burn_layer_values(self, geo_layer, mask, all_touched): """ Burn raster values inside or outside layer :param geo_layer: :param mask: :param all_touched: :return: """ layer = geo_layer.copy() layer["burn_value"] = 1 layer = layer.to_array(self.geo_grid, "burn_value", all_touched=all_touched) new_raster_array = self.raster_array.copy() if mask: new_raster_array[layer == 1] = np.nan else: new_raster_array[layer != 1] = np.nan return new_raster_array def _gdal_polygonize(self, out_shp, layer_name, field_name, is_8_connected): """ Polygonize raster using GDAL :param out_shp: shapefile :param layer_name: :param field_name: :param is_8_connected: :return: """ connectivity = "8CONNECTED=%d" % (8 if is_8_connected else 4) with GdalOpen(self.raster_file) as src_ds: src_band = src_ds.GetRasterBand(1) dst_ds = self._ogr_shp_drv.CreateDataSource(out_shp) dst_layer = dst_ds.CreateLayer(layer_name, srs_from(self.crs)) fd = ogr.FieldDefn(field_name, self._numpy_to_ogr_type[self.data_type]) dst_layer.CreateField(fd) gdal.Polygonize(src_band, src_band.GetMaskBand(), dst_layer, 0, [connectivity]) @gdal_decorator() def _gdal_resample_raster(self, out_raster, factor): """ Resample raster using GDAL utility :param factor: :return: """ with GdalOpen(self.raster_file) as source_ds: dst_ds = self._gdal_drv.Create(out_raster, self.x_size * factor, self.y_size * factor, 1, source_ds.GetRasterBand(1).DataType) resample_geo_transform = (self.x_origin, self.res / factor, 0, self.y_origin, 0, -self.res / factor) dst_ds.SetGeoTransform(resample_geo_transform) dst_ds.SetProjection(wkt_from(self.crs)) gdal.RegenerateOverview(source_ds.GetRasterBand(1), dst_ds.GetRasterBand(1), 'mode') @gdal_decorator() def _gdal_warp(self, output_raster, srs): with GdalOpen(self.raster_file) as src_ds: gdal.Warp(output_raster, src_ds, dstSRS=srs) @gdal_decorator() def _gdal_sieve(self, out_raster, *args, **kwargs): """ Sieve filter using gdal :param args: :param kwargs: :return: """ # Destination dataset dst_ds = self._clone_gdal_dataset(out_raster) # Apply sieve filter with GdalOpen(self.raster_file) as source_ds: gdal.SieveFilter(source_ds.GetRasterBand(1), None, dst_ds.GetRasterBand(1), *args, **kwargs) def _majority_filter(self): pass def _clone_gdal_dataset(self, out_raster): """ Create GDAL dataset based on raster map properties :param out_raster: out raster file :return: """ with GdalOpen(self.raster_file) as source_ds: dst_ds = self._gdal_drv.Create(out_raster, self.x_size, self.y_size, 1, source_ds.GetRasterBand(1).DataType) dst_ds.SetGeoTransform(self.geo_grid.geo_transform) dst_ds.SetProjection(wkt_from(self.crs)) if self.no_data_value is not None: dst_band = dst_ds.GetRasterBand(1) dst_band.SetNoDataValue(self.no_data_value) return dst_ds
class Address: """ Address base super class """ addresses = protected_property("addresses") place = protected_property("place") layers = protected_property("layers") def __init__(self, place): """ Build class instance :param place: """ self._place = place self._layers = None self._addresses = None def all_addresses(self, street_buffer=20): """ Retrieve all addresses corresponding to layers :param street_buffer: :return: """ list_of_layers = [layer.buffer(street_buffer) if layer.name == "highway" else layer for layer in self._layers] self._addresses = all_addresses(list_of_layers, by='name', to='address') return self def get_osm_layers(self, tags, crs=None, **kwargs): """ Retrieve OSM layers used for addressing (admin levels, streets, etc.) :param tags: list of tag/values tuples (e.g.: [("admin_level", ("10","11")), ("highway")] :param crs: set either crs or epsg code :param kwargs: keyword arguments of "from_osm" GeoLayer method :return: """ self._layers = [] for tag in tags: if len(tag) == 1: key, val = tag[0], None else: key, val = tag[0], tag[1] if key == "highway": layer = LineLayer.from_osm(self._place, key, val, **kwargs) else: layer = PolygonLayer.from_osm(self._place, key, val, **kwargs) self._layers.append(layer.to_crs(crs=crs)) return self def geocode(self, address_converter): """ Geocode addresses using converter model :param address_converter: AddressParser instance :return: """ pass
class GeoGrid(Geogrid): """GeoGrid class instance Define regular georeferenced grid based on cpc.geogrids module from NOAA """ latitude = protected_property('latitude') longitude = protected_property('longitude') geo_transform = protected_property('geo_transform') def __init__(self, ll_corner, ur_corner, res, geo_type="latlon"): """ GeoGrid class constructor See super class cpc.geogrids.Geogrid :param ll_corner: :param ur_corner: :param res: :param geo_type: grid type ('latlon' or 'equal') """ super().__init__(ll_corner=ll_corner, ur_corner=ur_corner, res=res, type=geo_type) if self.lats == [] or self.lons == []: raise GeoGridError( "GeoGrid has not been rightly defined (empty lats/lons field)") # Define geo transform used in raster computing (GDAL syntax) self._geo_transform = (self.ll_corner[1] - self.res / 2, self.res, 0, self.ur_corner[0] + self.res / 2, 0, -self.res) # Set lat and lon numpy arrays self._lat = np.asarray(self.lats)[::-1] self._lon = np.asarray(self.lons) # WARNING: sorry guys, but Geogrid implementation of num_x and num_y does not seem robust to me # Better to define num_x and num_y as length of lon and lat rather than doing some calculation that fails due # to float precision... (for really small resolutions though) self.num_x = len(self._lon) self.num_y = len(self._lat) # Compute lat/lon meshgrid of pixel centres self._longitude, self._latitude = np.meshgrid(self._lon, self._lat) @type_assert(lat=(int, float), lon=(int, float)) def latlon_to_2d_index(self, lat, lon): if lat > self._lat.max() + self.res/2 or lat < self._lat.min() - self.res/2 or lon > self._lon.max() + \ self.res/2 or lon < self._lon.min() - self.res/2: raise GeoGridError("Point is out of the geo grid") return np.argmin(np.abs(self._lat - lat)), np.argmin( np.abs(self._lon - lon)) def to_res(self, new_res): """ Define geo grid with new resolution Return new GeoGrid whose resolution is different :param new_res: :return: """ new_ll_corner = (self.ll_corner[0] - self.res / 2 + new_res / 2, self.geo_transform[0] + new_res / 2) new_ur_corner = (self.geo_transform[3] - new_res / 2, self.ur_corner[1] + self.res / 2 - new_res / 2) return GeoGrid(new_ll_corner, new_ur_corner, new_res, geo_type=self.type) def __eq__(self, other): if other.__class__ == self.__class__: return self.ll_corner == other.ll_corner and self.ur_corner == other.ur_corner and self.res == other.res else: return False def __getitem__(self, key): """ Get item in GeoGrid instance Get item in GeoGrid instance and return GeoGrid (array) or latitude and longitude (point) :param key: :return: """ # If key is only one slice, regard it as the same slice for rows and columns if key.__class__ == slice: key = (key, key) new_res = self.res if key.__class__ == tuple: if key[0].__class__ == slice or key[1].__class__ == slice: try: lat_south, lat_north = self._lat[key[0]][-1], self._lat[ key[0]][0] except TypeError: lat_south = lat_north = self._lat[key[0]] try: lon_west, lon_east = self._lon[key[1]][0], self._lon[ key[1]][-1] except TypeError: lon_west = lon_east = self._lon[key[1]] new_ll_corner, new_ur_corner = (lat_south, lon_west), (lat_north, lon_east) return GeoGrid(new_ll_corner, new_ur_corner, new_res, self.type) elif key[0].__class__ == int and key[1].__class__ == int: return self.latitude.__getitem__( key), self.longitude.__getitem__(key) else: raise GeoGridError("Invalid indexing") elif key.__class__ == int: return self.latitude.__getitem__(key), self.longitude.__getitem__( key) else: raise GeoGridError("Invalid indexing") @staticmethod def from_geo_file(geo_file: str, res, buffer_accuracy=1, to_crs: str = ''): """ Get geo grid from existing geo file (such as shapefile) :param geo_file: path to geo file (e.g. shapefile) :param res: resolution of the grid :param buffer_accuracy: accuracy of the buffer surrounded the region (in degrees) :param to_crs: final coordinate reference system of the geo grid (empty string for keeping the original CRS, under the form 'epsg:number' or pyproj name otherwise) :return: GeoGrid """ # Import locally (to avoid conflicts) from gistools.projections import proj4_from check_type(geo_file, str, res, (int, float), buffer_accuracy, (int, float), to_crs, str) if os.path.isfile(geo_file): geo_ds = gpd.read_file(geo_file) else: raise GeoGridError("{} is not a valid geo file".format(geo_file)) try: to_crs = pyproj.CRS(to_crs) except ValueError: warnings.warn( "Empty or invalid CRS/proj name. Geo grid matching geo file projection", GeoGridWarning) else: geo_ds = geo_ds.to_crs(to_crs) return GeoGrid.from_geopandas(geo_ds, res, buffer_accuracy) @staticmethod def from_geopandas(geopandas_series, res, buffer_accuracy=1): """ Get geo grid from specific geopandas database :param geopandas_series: :param res: :param buffer_accuracy: :return: """ check_type(geopandas_series, (gpd.GeoDataFrame, gpd.GeoSeries), res, (int, float), buffer_accuracy, (int, float)) # Default type for geo grid geo_type = 'latlon' try: # If resolution in km/m, type = 'equal' if not pyproj.Proj(geopandas_series.crs).crs.is_geographic: geo_type = 'equal' except AttributeError: raise ValueError("Input GeoSeries does not seem to be valid") try: bounds = geopandas_series.bounds except ValueError as e: raise GeoGridError("GeoSeries has invalid bounds:\n {}".format(e)) try: ll_corner = (buffer_accuracy * math.floor( (bounds.miny.min() + res / 2) / buffer_accuracy), buffer_accuracy * math.floor( (bounds.minx.min() + res / 2) / buffer_accuracy)) ur_corner = (buffer_accuracy * math.ceil( (bounds.maxy.max() - res / 2) / buffer_accuracy), buffer_accuracy * math.ceil( (bounds.maxx.max() - res / 2) / buffer_accuracy)) except (ValueError, ZeroDivisionError, FloatingPointError): raise ValueError( "Value of buffer accuracy ({}) is not valid".format( buffer_accuracy)) else: return GeoGrid(ll_corner, ur_corner, res, geo_type) @staticmethod def from_raster_file(raster_file: str): """ Retrieve geo grid from raster file :param raster_file: path to raster file :return: GeoGrid """ check_type(raster_file, str) try: source_ds = gdal.Open(raster_file) except RuntimeError as error: raise error geo_transform = source_ds.GetGeoTransform() # Warning: ll_corner and ur_corner correspond to grid corners # that is pixel centers, but gdal rasters are defined with respect # to pixel corners... That's why "+- res/2" ll_corner = (geo_transform[3] + source_ds.RasterYSize * geo_transform[5] - geo_transform[5] / 2, geo_transform[0] + geo_transform[1] / 2) ur_corner = (geo_transform[3] + geo_transform[5] / 2, geo_transform[0] + source_ds.RasterXSize * geo_transform[1] - geo_transform[1] / 2) # Geo type if crs_from_raster(raster_file).is_geographic: geo_type = "latlon" else: geo_type = "equal" if math.isclose(math.fabs(geo_transform[1]), math.fabs(geo_transform[5])): geo_grid = GeoGrid(ll_corner, ur_corner, math.fabs(geo_transform[1]), geo_type) else: geo_grid = None warnings.warn('No regular raster: no GeoGrid implemented', Warning) return geo_grid @staticmethod def from_hdr(hdr_file: str): """ Get geo grid from envi geospatial header file :param hdr_file: :return: """ check_file(hdr_file, '.hdr') hdr_info = read_hdr(hdr_file) if hdr_info['y_res'] != hdr_info['x_res']: raise AttributeError("X and Y image resolution must be the same") res = hdr_info['y_res'] ll_corner = (hdr_info['y_origin'] - hdr_info['y_size'] * res + res / 2, hdr_info['x_origin'] + res / 2) ur_corner = (hdr_info['y_origin'] - res / 2, hdr_info['x_origin'] + hdr_info['x_size'] * res - res / 2) if "Lat/Lon" in hdr_info["proj"]: geotype = "latlon" else: geotype = "equal" return GeoGrid(ll_corner, ur_corner, res, geotype)