text: #!/usr/bin/env python
  # -*- coding: utf-8 -*-
  
  import math
  
  import geopandas as gpd
  import libpysal
  import networkx as nx
  import numpy as np
  from shapely.geometry import Point
  
  __all__ = [
      "unique_id",
      "gdf_to_nx",
      "nx_to_gdf",
      "limit_range",
  ]
  
  
  def unique_id(objects):
      """
      Add an attribute with unique ID to each row of GeoDataFrame.
  
      Parameters
      ----------
      objects : GeoDataFrame
          GeoDataFrame containing objects to analyse
  
      Returns
      -------
      Series
          Series containing resulting values.
  
      """
      series = range(len(objects))
      return series
  
  
  def _angle(a, b, c):
      """
      Measure angle between a-b, b-c. In degrees.
      Helper for gdf_to_nx.
      Adapted from cityseer's implementation.
      """
      a1 = math.degrees(math.atan2(b[1] - a[1], b[0] - a[0]))
      a2 = math.degrees(math.atan2(c[1] - b[1], c[0] - b[0]))
      return abs((a2 - a1 + 180) % 360 - 180)
  
  
  def _generate_primal(G, gdf_network, fields, multigraph, oneway_column=None):
      """
      Generate primal graph.
      Helper for gdf_to_nx.
      """
      G.graph["approach"] = "primal"
      key = 0
      for row in gdf_network.itertuples():
          first = row.geometry.coords[0]
          last = row.geometry.coords[-1]
  
          data = [r for r in row][1:]
          attributes = dict(zip(fields, data))
          if multigraph:
              G.add_edge(first, last, key=key, **attributes)
              key += 1
  
              if oneway_column:
                  oneway = bool(getattr(row, oneway_column))
                  if not oneway:
                      G.add_edge(last, first, key=key, **attributes)
                      key += 1
          else:
              G.add_edge(first, last, **attributes)
  
  
  def _generate_dual(G, gdf_network, fields, angles, multigraph, angle):
      """
      Generate dual graph
      Helper for gdf_to_nx.
      """
      G.graph["approach"] = "dual"
      key = 0
  
      sw = libpysal.weights.Queen.from_dataframe(gdf_network, silence_warnings=True)
      cent = gdf_network.geometry.centroid
      gdf_network["temp_x_coords"] = cent.x
      gdf_network["temp_y_coords"] = cent.y
  
      for i, row in enumerate(gdf_network.itertuples()):
          centroid = (row.temp_x_coords, row.temp_y_coords)
          data = [f for f in row][1:-2]
          attributes = dict(zip(fields, data))
          G.add_node(centroid, **attributes)
  
          if sw.cardinalities[i] > 0:
              for n in sw.neighbors[i]:
                  start = centroid
                  end = (
                      gdf_network["temp_x_coords"].iloc[n],
                      gdf_network["temp_y_coords"].iloc[n],
                  )
                  p0 = row.geometry.coords[0]
                  p1 = row.geometry.coords[-1]
                  geom = gdf_network.geometry.iloc[n]
                  p2 = geom.coords[0]
                  p3 = geom.coords[-1]
                  points = [p0, p1, p2, p3]
                  shared = [x for x in points if points.count(x) > 1]
                  if shared:  # fix for non-planar graph
                      remaining = [e for e in points if e not in [shared[0]]]
                      if len(remaining) == 2:
                          if angles:
                              angle_value = _angle(remaining[0], shared[0], remaining[1])
                              if multigraph:
                                  G.add_edge(start, end, key=0, **{angle: angle_value})
                                  key += 1
                              else:
                                  G.add_edge(start, end, **{angle: angle_value})
                          else:
                              if multigraph:
                                  G.add_edge(start, end, key=0)
                                  key += 1
                              else:
                                  G.add_edge(start, end)
  
  
  def gdf_to_nx(
      gdf_network,
      approach="primal",
      length="mm_len",
      multigraph=True,
      directed=False,
      angles=True,
      angle="angle",
      oneway_column=None,
  ):
      """
      Convert LineString GeoDataFrame to networkx.MultiGraph or other Graph as per
      specification.
  
      Preserves columns as edge or node attributes (depending on the ``approach``).
      Index is not preserved.
  
      See the User Guide page :doc:`../../user_guide/graph/convert` for details.
  
      Parameters
      ----------
      gdf_network : GeoDataFrame
          GeoDataFrame containing objects to convert
      approach : str, default 'primal'
          Allowed options are ``'primal'`` or ``'dual'``. Primal graph represents
          endpoints as nodes and LineStrings as edges, dual graph represents
          LineStrings as nodes and their topological relation as edges. In such a
          case, it can encode an angle between LineStrings as an edge attribute.
      length : str, default 'mm_len'
          name of attribute of segment length (geographical) which will be saved to graph
      multigraph : bool, default True
          create ``MultiGraph`` of ``Graph`` (potentially directed). ``MutliGraph``
          allows multiple
          edges between any pair of nodes, which is a common case in street networks.
      directed : bool, default False
          create directed graph (``DiGraph`` or ``MultiDiGraph``). Directionality follows
          the order of LineString coordinates.
      angles : bool, default True
          capture angles between LineStrings as an attribute of a dual graph. Ignored if
          ``approach="primal"``.
      angle : str, default 'angle'
          name of attribute of angle between LineStrings which will be saved to graph.
          Ignored if ``approach="primal"``.
      oneway_column : str, default None
          create an additional edge for each LineString which allows bidirectional path traversal by
          specifying the boolean column in the GeoDataFrame. Note, that the reverse conversion
          ``nx_to_gdf(gdf_to_nx(gdf, directed=True, oneway_column="oneway"))`` will contain
          additional duplicated geometries.
  
      Returns
      -------
      networkx.Graph,
      networkx.MultiGraph,
      networkx.DiGraph,
      networkx.MultiDiGraph
          Graph as per specification
  
      See also
      --------
      nx_to_gdf
  
      Examples
      --------
      >>> import geopandas as gpd
      >>> df = gpd.read_file(momepy.datasets.get_path('bubenec'), layer='streets')
      >>> df.head(5)
                                                  geometry
      0  LINESTRING (1603585.640 6464428.774, 1603413.2...
      1  LINESTRING (1603268.502 6464060.781, 1603296.8...
      2  LINESTRING (1603607.303 6464181.853, 1603592.8...
      3  LINESTRING (1603678.970 6464477.215, 1603675.6...
      4  LINESTRING (1603537.194 6464558.112, 1603557.6...
  
      Primal graph:
  
      >>> G = momepy.gdf_to_nx(df)
      >>> G
      <networkx.classes.multigraph.MultiGraph object at 0x7f8cf90fad50>
  
      >>> G_directed = momepy.gdf_to_nx(df, directed=True)
      >>> G_directed
      <networkx.classes.multidigraph.MultiDiGraph object at 0x7f8cf90f56d0>
  
      >>> G_digraph = momepy.gdf_to_nx(df, multigraph=False, directed=True)
      >>> G_digraph
      <networkx.classes.digraph.DiGraph object at 0x7f8cf9150c10>
  
      >>> G_graph = momepy.gdf_to_nx(df, multigraph=False, directed=False)
      >>> G_graph
      <networkx.classes.graph.Graph object at 0x7f8cf90facd0>
  
      Dual graph:
  
      >>> G_dual = momepy.gdf_to_nx(df, approach="dual")
      >>> G_dual
      <networkx.classes.multigraph.MultiGraph object at 0x7f8cf9150fd0>
  
  
      """
      gdf_network = gdf_network.copy()
      if "key" in gdf_network.columns:
          gdf_network.rename(columns={"key": "__key"}, inplace=True)
  
      if multigraph and directed:
          net = nx.MultiDiGraph()
      elif multigraph and not directed:
          net = nx.MultiGraph()
      elif not multigraph and directed:
          net = nx.DiGraph()
      else:
          net = nx.Graph()
  
      net.graph["crs"] = gdf_network.crs
      gdf_network[length] = gdf_network.geometry.length
      fields = list(gdf_network.columns)
  
      if approach == "primal":
          if oneway_column and not directed:
              raise ValueError(
                  "Bidirectional lines are only supported for directed graphs."
              )
  
          _generate_primal(net, gdf_network, fields, multigraph, oneway_column)
  
      elif approach == "dual":
          if directed:
              raise ValueError("Directed graphs are not supported in dual approach.")
  
          _generate_dual(
              net, gdf_network, fields, angles=angles, multigraph=multigraph, angle=angle
          )
  
      else:
          raise ValueError(
              f"Approach {approach} is not supported. Use 'primal' or 'dual'."
          )
  
      return net
  
  
  def _points_to_gdf(net):
      """
      Generate point gdf from nodes.
      Helper for nx_to_gdf.
      """
      node_xy, node_data = zip(*net.nodes(data=True))
      if isinstance(node_xy[0], int) and "x" in node_data[0].keys():
          geometry = [Point(data["x"], data["y"]) for data in node_data]  # osmnx graph
      else:
          geometry = [Point(*p) for p in node_xy]
      gdf_nodes = gpd.GeoDataFrame(list(node_data), geometry=geometry)
      if "crs" in net.graph.keys():
          gdf_nodes.crs = net.graph["crs"]
      return gdf_nodes
  
  
  def _lines_to_gdf(net, points, nodeID):
      """
      Generate linestring gdf from edges.
      Helper for nx_to_gdf.
      """
      starts, ends, edge_data = zip(*net.edges(data=True))
      gdf_edges = gpd.GeoDataFrame(list(edge_data))
  
      if points is True:
          node_start = []
          node_end = []
          for s in starts:
              node_start.append(net.nodes[s][nodeID])
          for e in ends:
              node_end.append(net.nodes[e][nodeID])
          gdf_edges["node_start"] = node_start
          gdf_edges["node_end"] = node_end
  
      if "crs" in net.graph.keys():
          gdf_edges.crs = net.graph["crs"]
  
      return gdf_edges
  
  
  def _primal_to_gdf(net, points, lines, spatial_weights, nodeID):
      """
      Generate gdf(s) from primal network.
      Helper for nx_to_gdf.
      """
      if points is True:
          gdf_nodes = _points_to_gdf(net)
  
          if spatial_weights is True:
              W = libpysal.weights.W.from_networkx(net)
              W.transform = "b"
  
      if lines is True:
          gdf_edges = _lines_to_gdf(net, points, nodeID)
  
      if points is True and lines is True:
          if spatial_weights is True:
              return gdf_nodes, gdf_edges, W
          return gdf_nodes, gdf_edges
      if points is True and lines is False:
          if spatial_weights is True:
              return gdf_nodes, W
          return gdf_nodes
      return gdf_edges
  
  
  def _dual_to_gdf(net):
      """
      Generate linestring gdf from dual network.
      Helper for nx_to_gdf.
      """
      starts, edge_data = zip(*net.nodes(data=True))
      gdf_edges = gpd.GeoDataFrame(list(edge_data))
      gdf_edges.crs = net.graph["crs"]
      return gdf_edges
  
  
  def nx_to_gdf(net, points=True, lines=True, spatial_weights=False, nodeID="nodeID"):
      """
      Convert ``networkx.Graph`` to LineString GeoDataFrame and Point GeoDataFrame.
  
      Automatically detects an ``approach`` of the graph and assignes edges and nodes to
      relevant geometry type.
  
      See the User Guide page :doc:`../../user_guide/graph/convert` for details.
  
      Parameters
      ----------
      net : networkx.Graph
          ``networkx.Graph``
      points : bool
          export point-based gdf representing intersections
      lines : bool
          export line-based gdf representing streets
      spatial_weights : bool
          export libpysal spatial weights for nodes (only for primal graphs)
      nodeID : str
          name of node ID column to be generated
  
      Returns
      -------
      GeoDataFrame
          Selected gdf or tuple of both gdfs or tuple of gdfs and weights
  
      See also
      --------
      gdf_to_nx
  
      Examples
      --------
      >>> import geopandas as gpd
      >>> df = gpd.read_file(momepy.datasets.get_path('bubenec'), layer='streets')
      >>> df.head(2)
                                                  geometry
      0  LINESTRING (1603585.640 6464428.774, 1603413.2...
      1  LINESTRING (1603268.502 6464060.781, 1603296.8...
      >>> G = momepy.gdf_to_nx(df)
  
      Converting primal Graph to points as intersections and lines as street segments:
  
      >>> points, lines = momepy.nx_to_gdf(G)
      >>> points.head(2)
         nodeID                         geometry
      0       1  POINT (1603585.640 6464428.774)
      1       2  POINT (1603413.206 6464228.730)
      >>> lines.head(2)
                           geometry      mm_len  node_start  node_end
      0  LINESTRING (1603585.640...  264.103950           1         2
      1  LINESTRING (1603561.740...   70.020202           1         9
  
      Storing relationship between points/nodes as libpysal W object:
  
      >>> points, lines, W = momepy.nx_to_gdf(G, spatial_weights=True)
      >>> W
      <libpysal.weights.weights.W object at 0x7f8d01837210>
  
      Converting dual Graph to lines. Dual Graph does not export edges to GDF:
  
      >>> G = momepy.gdf_to_nx(df, approach="dual")
      >>> lines = momepy.nx_to_gdf(G)
      >>> lines.head(2)
                                                  geometry      mm_len
      0  LINESTRING (1603585.640 6464428.774, 1603413.2...  264.103950
      1  LINESTRING (1603607.303 6464181.853, 1603592.8...  199.746503
      """
      # generate nodes and edges geodataframes from graph
      primal = None
      if "approach" in net.graph.keys():
          if net.graph["approach"] == "primal":
              primal = True
          elif net.graph["approach"] == "dual":
              return _dual_to_gdf(net)
          else:
              raise ValueError(
                  f"Approach {net.graph['approach']} is not supported. "
                  "Use 'primal' or 'dual'."
              )
  
      if not primal:
          import warnings
  
          warnings.warn("Approach is not set. Defaulting to 'primal'.")
  
      for nid, n in enumerate(net):
          net.nodes[n][nodeID] = nid
  
      return _primal_to_gdf(
          net, points=points, lines=lines, spatial_weights=spatial_weights, nodeID=nodeID
      )
  
  
  def limit_range(vals, rng):
      """
      Extract values within selected range
  
      Parameters
      ----------
      vals : array
  
      rng : Two-element sequence containing floats in range of [0,100], optional
          Percentiles over which to compute the range. Each must be
          between 0 and 100, inclusive. The order of the elements is not important.
      Returns
      -------
      array
          limited array
      """
      vals = np.asarray(vals)
      if len(vals) > 2:
          rng = sorted(rng)
          if np.isnan(vals).any():
              lower = np.nanpercentile(vals, rng[0], interpolation="nearest")
              higher = np.nanpercentile(vals, rng[1], interpolation="nearest")
          else:
              lower = np.percentile(vals, rng[0], interpolation="nearest")
              higher = np.percentile(vals, rng[1], interpolation="nearest")
          return vals[(lower <= vals) & (vals <= higher)]
      return vals
  
  
  def _azimuth(point1, point2):
      """azimuth between 2 shapely points (interval 0 - 180)"""
      angle = np.arctan2(point2[0] - point1[0], point2[1] - point1[1])
      return np.degrees(angle) if angle > 0 else np.degrees(angle) + 180
  