コード例 #1
0
ファイル: network.py プロジェクト: benjaminpillot/gis-tools
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
コード例 #2
0
ファイル: network.py プロジェクト: benjaminpillot/gis-tools
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
コード例 #3
0
ファイル: network.py プロジェクト: benjaminpillot/gis-tools
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]
コード例 #4
0
ファイル: raster.py プロジェクト: benjaminpillot/gis-tools
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
コード例 #5
0
ファイル: geocoding.py プロジェクト: benjaminpillot/gis-tools
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
コード例 #6
0
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)