Repository: martinfleis/momepy
Pull request #17 (2019-06-26 09:57:19)

Various bugfixes and enhancements
BUG: tessellation: _densify(), dissolve
BUG: extend_line fix for micro segments
ENH: get_network_id - allow user defined distance, remove tessellation
BUG: get_node_id refactor to use only nodes on the closest edge
ENH: nx_to_gdf save nodeIDs to edges
ENH: mutli2single works for linestrings
ENH: preprocess preparation for network false node detection



Patch:
diff --git a/momepy/elements.py b/momepy/elements.py
index 5a6d40f..7cb4df8 100644
--- a/momepy/elements.py
+++ b/momepy/elements.py
@@ -79,13 +79,17 @@ def tessellation(buildings, unique_id='uID', cut_buffer=50, queen_corners=False,
 
     # densify geometry before Voronoi tesselation
     def _densify(geom):
+        poly = geom
         wkt = geom.wkt  # shapely Polygon to wkt
         geom = ogr.CreateGeometryFromWkt(wkt)  # create ogr geometry
         geom.Segmentize(2)  # densify geometry by 2 metres
-        # geom.CloseRings() # fix for GDAL 2.4.1 bug
+        geom.CloseRings()  # fix for GDAL 2.4.1 bug
         wkt2 = geom.ExportToWkt()  # ogr geometry to wkt
-        new = loads(wkt2)  # wkt to shapely Polygon
-        return new
+        try:
+            new = loads(wkt2)  # wkt to shapely Polygon
+            return new
+        except:
+            return poly
 
     print('Densifying geometry...')
     objects['geometry'] = objects['geometry'].progress_map(_densify)
@@ -154,8 +158,7 @@ def tessellation(buildings, unique_id='uID', cut_buffer=50, queen_corners=False,
     regions_gdf.crs = buildings.crs
 
     print('Dissolving Voronoi polygons...')
-    morphological_tessellation = regions_gdf[[unique_id, 'geometry']].dissolve(by=unique_id)
-    morphological_tessellation[unique_id] = morphological_tessellation.index.astype('float')  # save unique id to column from index
+    morphological_tessellation = regions_gdf[[unique_id, 'geometry']].dissolve(by=unique_id, as_index=False)
 
     # cut infinity of voronoi by set buffer (thanks for script to Geoff Boeing)
     print('Preparing buffer zone for edge resolving (quadrat cut)...')
@@ -369,13 +372,16 @@ def snap_street_network_edge(network, buildings, tessellation, tolerance_street,
         Extends a line geometry withing GeoDataFrame to snap on itself withing tolerance.
         """
         if Point(l_coords[-2]).distance(Point(l_coords[-1])) <= 0.001:
-            extra = l_coords[-3:-1]
+            if len(l_coords) > 2:
+                extra = l_coords[-3:-1]
+            else:
+                return False
         else:
             extra = l_coords[-2:]
-        extrapolation = getExtrapoledLine(*extra, tolerance)  # we use the last two points
+        extrapolation = getExtrapoledLine(*extra, tolerance=tolerance)  # we use the last two points
 
         possible_intersections_index = list(sindex.intersection(extrapolation.bounds))
-        possible_intersections_lines = network.loc[possible_intersections_index]
+        possible_intersections_lines = network.iloc[possible_intersections_index]
         possible_intersections_clean = possible_intersections_lines.drop(idx, axis=0)
         possible_intersections = possible_intersections_clean.intersection(extrapolation)
 
@@ -389,30 +395,31 @@ def snap_street_network_edge(network, buildings, tessellation, tolerance_street,
                     true_int.append(possible_intersections[one][0])
                     true_int.append(possible_intersections[one][1])
 
-            if len(true_int) > 1:
-                distances = {}
-                ix = 0
-                for p in true_int:
-                    distance = p.distance(Point(l_coords[-1]))
-                    distances[ix] = distance
-                    ix = ix + 1
-                minimal = min(distances.items(), key=operator.itemgetter(1))[0]
-                new_point_coords = true_int[minimal].coords[0]
-            else:
-                new_point_coords = true_int[0].coords[0]
+            if len(true_int) >= 1:
+                if len(true_int) > 1:
+                    distances = {}
+                    ix = 0
+                    for p in true_int:
+                        distance = p.distance(Point(l_coords[-1]))
+                        distances[ix] = distance
+                        ix = ix + 1
+                    minimal = min(distances.items(), key=operator.itemgetter(1))[0]
+                    new_point_coords = true_int[minimal].coords[0]
+                else:
+                    new_point_coords = true_int[0].coords[0]
 
-            l_coords.append(new_point_coords)
-            new_extended_line = LineString(l_coords)
+                l_coords.append(new_point_coords)
+                new_extended_line = LineString(l_coords)
 
-            # check whether the line goes through buildings. if so, ignore it
-            possible_buildings_index = list(bindex.intersection(new_extended_line.bounds))
-            possible_buildings = buildings.iloc[possible_buildings_index]
-            possible_intersections = possible_buildings.intersection(new_extended_line)
+                # check whether the line goes through buildings. if so, ignore it
+                possible_buildings_index = list(bindex.intersection(new_extended_line.bounds))
+                possible_buildings = buildings.iloc[possible_buildings_index]
+                possible_intersections = possible_buildings.intersection(new_extended_line)
 
-            if possible_intersections.any():
-                pass
-            else:
-                network.loc[idx, 'geometry'] = new_extended_line
+                if possible_intersections.any():
+                    pass
+                else:
+                    network.loc[idx, 'geometry'] = new_extended_line
         else:
             return False
 
@@ -422,7 +429,10 @@ def snap_street_network_edge(network, buildings, tessellation, tolerance_street,
         Extends a line geometry withing GeoDataFrame to snap on the boundary of tessellation withing tolerance.
         """
         if Point(l_coords[-2]).distance(Point(l_coords[-1])) <= 0.001:
-            extra = l_coords[-3:-1]
+            if len(l_coords) > 2:
+                extra = l_coords[-3:-1]
+            else:
+                return False
         else:
             extra = l_coords[-2:]
         extrapolation = getExtrapoledLine(*extra, tolerance)  # we use the last two points
@@ -441,30 +451,31 @@ def snap_street_network_edge(network, buildings, tessellation, tolerance_street,
                 true_int.append(possible_intersections[0])
                 true_int.append(possible_intersections[1])
 
-            if len(true_int) > 1:
-                distances = {}
-                ix = 0
-                for p in true_int:
-                    distance = p.distance(Point(l_coords[-1]))
-                    distances[ix] = distance
-                    ix = ix + 1
-                minimal = min(distances.items(), key=operator.itemgetter(1))[0]
-                new_point_coords = true_int[minimal].coords[0]
-            else:
-                new_point_coords = true_int[0].coords[0]
+            if len(true_int) >= 1:
+                if len(true_int) > 1:
+                    distances = {}
+                    ix = 0
+                    for p in true_int:
+                        distance = p.distance(Point(l_coords[-1]))
+                        distances[ix] = distance
+                        ix = ix + 1
+                    minimal = min(distances.items(), key=operator.itemgetter(1))[0]
+                    new_point_coords = true_int[minimal].coords[0]
+                else:
+                    new_point_coords = true_int[0].coords[0]
 
-            l_coords.append(new_point_coords)
-            new_extended_line = LineString(l_coords)
+                l_coords.append(new_point_coords)
+                new_extended_line = LineString(l_coords)
 
-            # check whether the line goes through buildings. if so, ignore it
-            possible_buildings_index = list(bindex.intersection(new_extended_line.bounds))
-            possible_buildings = buildings.iloc[possible_buildings_index]
-            possible_intersections = possible_buildings.intersection(new_extended_line)
+                # check whether the line goes through buildings. if so, ignore it
+                possible_buildings_index = list(bindex.intersection(new_extended_line.bounds))
+                possible_buildings = buildings.iloc[possible_buildings_index]
+                possible_intersections = possible_buildings.intersection(new_extended_line)
 
-            if possible_intersections.any():
-                pass
-            else:
-                network.loc[idx, 'geometry'] = new_extended_line
+                if possible_intersections.any():
+                    pass
+                else:
+                    network.loc[idx, 'geometry'] = new_extended_line
 
     network = network.copy()
     # generating spatial index (rtree)
@@ -680,37 +691,33 @@ def blocks(cells, streets, buildings, id_name, unique_id):
     return (buildings, cells, blocks_save)
 
 
-def get_network_id(buildings, streets, unique_id_buildings, network_id, tessellation=None, unique_id_tessellation=None):
+def get_network_id(elements, streets, unique_id, network_id, tessellation=None, unique_id_tessellation=None,
+                   min_size=100):
     """
-    Snap each building to closest street network segment, saves its id.
+    Snap each element (preferably building) to the closest street network segment, saves its id.
 
-    Adds network ID to buildings and optionally tessellation.
+    Adds network ID to elements.
 
     Parameters
     ----------
-    buildings : GeoDataFrame
+    elements : GeoDataFrame
         GeoDataFrame containing buildings
     streets : GeoDataFrame
         GeoDataFrame containing street network with unique network ID.
         If there is none, it could be generated by :py:func:`momepy.elements.unique_id`.
-    unique_id_buildings : str, list, np.array, pd.Series (default None)
-        the name of the buildings dataframe column, np.array, or pd.Series with unique id
+    unique_id : str, list, np.array, pd.Series (default None)
+        the name of the elements dataframe column, np.array, or pd.Series with unique id
     network_id : str, list, np.array, pd.Series (default None)
         the name of the streets dataframe column, np.array, or pd.Series with network unique id.
-    tessellation : GeoDataFrame (default=None)
-        GeoDataFrame containing morphological tessellation
-    unique_id_tessellation : str, list, np.array, pd.Series (default None)
-        the name of the tessellation dataframe column, np.array, or pd.Series with unique id.
-        This should be the same for tessellation and buildings, IDs should match.
+    min_size : int (default 100)
+        min_size should be a vaule such that if you build a box centered in each
+        building centroid with edges of size `2*min_size`, you know a priori that at least one
+        segment is intersected with the box.
 
     Returns
     -------
-    buildings_nID, (tessellation_nID) : tuple
-
-    buildings_nID : Series
-        Series containing network ID for buildings
-    tessellation_nID : Series, optional
-        Series containing network ID for morphological tessellation
+    elements_nID : Series
+        Series containing network ID for elements
 
     Examples
     --------
@@ -727,13 +734,13 @@ def get_network_id(buildings, streets, unique_id_buildings, network_id, tessella
     Forming DataFrame...
     Joining DataFrames...
     Cleaning DataFrames...
-    Merging with buildings...
+    Merging with elements...
     Done.
     >>> buildings_df['nID'][0]
     1
     """
     INFTY = 1000000000000
-    MIN_SIZE = 100
+    MIN_SIZE = min_size
     # MIN_SIZE should be a vaule such that if you build a box centered in each
     # point with edges of size 2*MIN_SIZE, you know a priori that at least one
     # segment is intersected with the box. Otherwise, you could get an inexact
@@ -794,15 +801,15 @@ def get_network_id(buildings, streets, unique_id_buildings, network_id, tessella
 
         return result
 
-    if not isinstance(unique_id_buildings, str):
-        buildings['mm_uid'] = unique_id_buildings
-        unique_id_buildings = 'mm_uid'
+    if not isinstance(unique_id, str):
+        elements['mm_uid'] = unique_id
+        unique_id = 'mm_uid'
     if not isinstance(network_id, str):
         streets['mm_nid'] = network_id
         network_id = 'mm_nid'
 
     print('Generating centroids...')
-    buildings_c = buildings.copy()
+    buildings_c = elements.copy()
     buildings_c['geometry'] = buildings_c.centroid  # make centroids
 
     print('Generating list of points...')
@@ -832,42 +839,30 @@ def get_network_id(buildings, streets, unique_id_buildings, network_id, tessella
     print('Joining DataFrames...')
     joined = buildings_c.merge(df, on='idx')
     print('Cleaning DataFrames...')
-    cleaned = joined[[unique_id_buildings, network_id]]
-
-    print('Merging with buildings...')
-    buildings_m = buildings.merge(cleaned, on=unique_id_buildings)
-
-    if tessellation is not None:
-        if not isinstance(unique_id_tessellation, str):
-            tessellation['mm_uid'] = unique_id_tessellation
-            unique_id_tessellation = 'mm_uid'
-        print('Merging with tessellation...')
-        # attribute join cell -> building
-        tess_nid_eid = buildings_m[[unique_id_buildings, network_id]]
-        tessellation_m = tessellation.merge(tess_nid_eid, left_on=unique_id_tessellation, right_on=unique_id_buildings)
-
-        if 'mm_uid' in tessellation.columns:
-            tessellation.drop(columns=['mm_uid'], inplace=True)
-        if 'mm_uid' in buildings.columns:
-            buildings.drop(columns=['mm_uid'], inplace=True)
-        if 'mm_nid' in streets.columns:
-            streets.drop(columns=['mm_nid'], inplace=True)
-        print('Done.')
-        return buildings_m[network_id], tessellation_m[network_id]
-
-    if 'mm_uid' in buildings.columns:
-        buildings.drop(columns=['mm_uid'], inplace=True)
+    cleaned = joined[[unique_id, network_id]]
+
+    print('Merging with elements...')
+    elements_m = elements.merge(cleaned, on=unique_id)
+
+    if elements_m[network_id].isnull().any():
+        import warnings
+        warnings.warn('Some elements were not attached to the network. '
+                      'Set larger min_size. {} affected elements'.format(sum(elements_m[network_id].isnull())))
+
+    if 'mm_uid' in elements.columns:
+        elements.drop(columns=['mm_uid'], inplace=True)
     if 'mm_nid' in streets.columns:
         streets.drop(columns=['mm_nid'], inplace=True)
     print('Done.')
-    return buildings_m[network_id]
+    return elements_m[network_id]
 
 
-def get_node_id(objects, nodes, node_id, max_distance=200):
+def get_node_id(objects, nodes, edges, node_id, edge_id):
     """
-    Snap each building to closest street network node, saves its id.
+    Snap each building to closest street network node on the closest network edge.
 
-    Adds node ID to objects (preferably buildings).
+    Adds node ID to objects (preferably buildings). Gets ID of edge, and determines
+    which of its end points is closer.
 
     Parameters
     ----------
@@ -876,15 +871,16 @@ def get_node_id(objects, nodes, node_id, max_distance=200):
     nodes : GeoDataFrame
         GeoDataFrame containing street nodes with unique node ID.
         If there is none, it could be generated by :py:func:`momepy.unique_id`.
+    edges : GeoDataFrame
+        GeoDataFrame containing street edges with unique edge ID and IDs of start
+        and end points of each segment. Start and endpoints are default outcome of :py:func:`momepy.nx_to_gdf`.
     node_id : str, list, np.array, pd.Series (default None)
         the name of the nodes dataframe column, np.array, or pd.Series with unique id
-    max_distance : int (default=200)
-        maximal allowed distance between object centroid and assigned node
 
     Returns
     -------
-    buildings_nID : Series
-        Series containing nide ID for objects
+    node_ids : Series
+        Series containing node ID for objects
 
     Examples
     --------
@@ -894,28 +890,22 @@ def get_node_id(objects, nodes, node_id, max_distance=200):
         nodes['mm_noid'] = node_id
         node_id = 'mm_noid'
 
-    sindex = nodes.sindex
-
-    def _get_node(geometry, node_id, max_distance=max_distance):
-        centroid = geometry.centroid
-        x = centroid.x
-        y = centroid.y
-        bounds = (x - max_distance, y - max_distance, x + max_distance, y + max_distance)
-
-        possible_nodes_index = list(sindex.intersection(bounds))
-        possible_nodes = nodes.iloc[possible_nodes_index]
-
-        distances = []
-        for node in possible_nodes.geometry.iteritems():
-            distances.append(node[1].distance(centroid))
-
-        if distances:
-            true_node_index = distances.index(min(distances))
-            return possible_nodes.iloc[true_node_index][node_id]
-        return -1
+    results_list = []
+    for index, row in objects.iterrows():
+        centroid = row.geometry.centroid
+        edge = edges.loc[edges[edge_id] == row[edge_id]].iloc[0]
+        startID = edge.node_start
+        start = nodes.loc[nodes[node_id] == startID].iloc[0].geometry
+        sd = centroid.distance(start)
+        endID = edge.node_end
+        end = nodes.loc[nodes[node_id] == endID].iloc[0].geometry
+        ed = centroid.distance(end)
+        if sd > ed:
+            results_list.append(endID)
+        else:
+            results_list.append(startID)
 
-    tqdm.pandas()
-    series = objects.progress_apply(lambda row: _get_node(row['geometry'], node_id, max_distance), axis=1)
+    series = pd.Series(results_list)
     return series
 
 
diff --git a/momepy/intensity.py b/momepy/intensity.py
index e286322..e329a2a 100644
--- a/momepy/intensity.py
+++ b/momepy/intensity.py
@@ -455,3 +455,65 @@ def reached(streets, elements, unique_id, spatial_weights=None, mode='count'):
 
     print('Reached {} calculated.'.format(mode))
     return series
+
+
+def node_density(objects, nodes, spatial_weights=None, order=9, node_id='nodeID'):
+    """
+    Calculate the density of nodes within topological steps of morphological tessellation.
+    Node is marked as reached if reached cell con
+
+    .. math::
+
+
+    Parameters
+    ----------
+    objects : GeoDataFrame
+        GeoDataFrame containing tessellation objects to analyse
+    nodes : GeoDataFrame
+        GeoDataFrame containing nodes
+    spatial_weights : libpysal.weights, optional
+        spatial weights matrix - If None, Queen contiguity matrix of selected order will be calculated
+        based on objects.
+    order : int
+        order of Queen contiguity
+    node_id : str
+        name of the column of objects gdf with node id.
+
+    Returns
+    -------
+    Series
+        Series containing resulting values.
+
+    References
+    ---------
+    Jacob
+
+    Notes
+    -----
+
+    """
+    # define empty list for results
+    results_list = []
+
+    print('Calculating gross density...')
+
+    if not all(objects.index == range(len(objects))):
+        raise ValueError('Index is not consecutive range 0:x, spatial weights will not match objects.')
+
+    if spatial_weights is None:
+        print('Generating weights matrix (Queen) of {} topological steps...'.format(order))
+        from momepy import Queen_higher
+        # matrix to define area of analysis (more steps)
+        spatial_weights = Queen_higher(k=order, geodataframe=objects)
+
+    # iterating over rows one by one
+    for index, row in tqdm(objects.iterrows(), total=objects.shape[0]):
+        neighbours = spatial_weights.neighbors[index]
+        neighbours.append(index)
+        sub = objects.iloc[neighbours]
+        subnodes = set(sub['nodeID'])
+        results_list.append((len(subnodes) / sum(sub.geometry.area)) * 10000)
+
+    series = pd.Series(results_list)
+    print('Gross density calculated.')
+    return series
diff --git a/momepy/utils.py b/momepy/utils.py
index e691894..59f1321 100644
--- a/momepy/utils.py
+++ b/momepy/utils.py
@@ -114,7 +114,7 @@ def gdf_to_nx(gdf_network, length='mm_len'):
     return net
 
 
-def nx_to_gdf(net, nodes=True, edges=True, spatial_weights=False):
+def nx_to_gdf(net, nodes=True, edges=True, spatial_weights=False, nodeID='nodeID'):
     """
     Convert networkx.Graph to LineString GeoDataFrame and Point GeoDataFrame
 
@@ -128,6 +128,8 @@ def nx_to_gdf(net, nodes=True, edges=True, spatial_weights=False):
         export edges gdf
     spatial_weights : bool
         export libpysal spatial weights for nodes
+    nodeID : str
+        name of node ID column to be generated
 
     Returns
     -------
@@ -137,6 +139,10 @@ def nx_to_gdf(net, nodes=True, edges=True, spatial_weights=False):
     """
     # generate nodes and edges geodataframes from graph
     if nodes is True:
+        nid = 1
+        for n in net:
+            net.nodes[n][nodeID] = nid
+            nid += 1
         node_xy, node_data = zip(*net.nodes(data=True))
         gdf_nodes = gpd.GeoDataFrame(list(node_data), geometry=[Point(i, j) for i, j in node_xy])
         gdf_nodes.crs = net.graph['crs']
@@ -145,7 +151,17 @@ def nx_to_gdf(net, nodes=True, edges=True, spatial_weights=False):
 
     if edges is True:
         starts, ends, edge_data = zip(*net.edges(data=True))
+        if nodes is True:
+            node_start = []
+            node_end = []
+            for s in starts:
+                node_start.append(net.node[s][nodeID])
+            for e in ends:
+                node_end.append(net.node[e][nodeID])
         gdf_edges = gpd.GeoDataFrame(list(edge_data))
+        if nodes is True:
+            gdf_edges['node_start'] = node_start
+            gdf_edges['node_end'] = node_end
         gdf_edges.crs = net.graph['crs']
 
     if nodes is True and edges is True:
@@ -161,7 +177,7 @@ def nx_to_gdf(net, nodes=True, edges=True, spatial_weights=False):
 
 def multi2single(gdf):
     """
-    Convert MultiPolygon geometry of GeoDataFrame to Polygon geometries.
+    Convert multi-part geometry of GeoDataFrame to single-part geometries.
 
     Parameters
     ----------
@@ -171,11 +187,20 @@ def multi2single(gdf):
     Returns
     -------
     GeoDataFrame
-        GeoDataFrame containing only Polygons
+        GeoDataFrame containing only single-part geometry
 
     """
-    gdf_singlepoly = gdf[gdf.geometry.type == 'Polygon']
-    gdf_multipoly = gdf[gdf.geometry.type == 'MultiPolygon']
+    if gdf.iloc[0].geometry.type in ['Polygon', 'MultiPolygon']:
+        gdf_singlepoly = gdf[gdf.geometry.type == 'Polygon']
+        gdf_multipoly = gdf[gdf.geometry.type == 'MultiPolygon']
+    elif gdf.iloc[0].geometry.type in ['LineString', 'MultiLineString']:
+        gdf_singlepoly = gdf[gdf.geometry.type == 'LineString']
+        gdf_multipoly = gdf[gdf.geometry.type == 'MultiLineString']
+    elif gdf.iloc[0].geometry.type in ['Point', 'MultiPoint']:
+        gdf_singlepoly = gdf[gdf.geometry.type == 'Point']
+        gdf_multipoly = gdf[gdf.geometry.type == 'MultiPoint']
+    else:
+        raise ValueError('Unsupported geometry type:', gdf.iloc[0].geometry.type)
 
     for i, row in gdf_multipoly.iterrows():
         Series_geometries = pd.Series(row.geometry)
@@ -206,8 +231,8 @@ def limit_range(vals, rng):
     if len(vals) > 2:
         limited = []
         rng = sorted(rng)
-        lower = np.percentile(vals, rng[0])
-        higher = np.percentile(vals, rng[1])
+        lower = np.nanpercentile(vals, rng[0])
+        higher = np.nanpercentile(vals, rng[1])
         for x in vals:
             if x >= lower and x <= higher:
                 limited.append(x)
@@ -347,3 +372,62 @@ def preprocess(buildings, size=30, compactness=True, islands=True):
         blg.drop(delete, inplace=True)
     blg.drop(['neighbors', 'n_count', 'circu', 'uID'], axis=1, inplace=True)
     return blg
+
+
+def network_false_nodes(streets):
+    sindex = streets.sindex
+
+    false_points = []
+    print('Identifying false points...')
+    for idx, row in tqdm(streets.iterrows(), total=streets.shape[0]):
+        line = row['geometry']
+        l_coords = list(line.coords)
+        # network_w = network.drop(idx, axis=0)['geometry']  # ensure that it wont intersect itself
+        start = Point(l_coords[0])
+        end = Point(l_coords[-1])
+
+        # find out whether ends of the line are connected or not
+        possible_first_index = list(sindex.intersection(start.bounds))
+        possible_first_matches = streets.iloc[possible_first_index]
+        possible_first_matches_clean = possible_first_matches.drop(idx, axis=0)
+        real_first_matches = possible_first_matches_clean[possible_first_matches_clean.intersects(start)]
+
+        possible_second_index = list(sindex.intersection(end.bounds))
+        possible_second_matches = streets.iloc[possible_second_index]
+        possible_second_matches_clean = possible_second_matches.drop(idx, axis=0)
+        real_second_matches = possible_second_matches_clean[possible_second_matches_clean.intersects(end)]
+
+        if len(real_first_matches) == 1:
+            false_points.append(start)
+        if len(real_second_matches) == 1:
+            false_points.append(end)
+
+    false_xy = []
+    for p in false_points:
+        false_xy.append([p.x, p.y])
+
+    false_xy_unique = [list(x) for x in set(tuple(x) for x in false_xy)]
+
+    false_unique = []
+    for p in false_xy_unique:
+        false_unique.append(Point(p[0], p[1]))
+
+    geoms = streets.geometry
+
+    print('Merging segments...')
+    for point in tqdm(false_unique):
+        matches = list(geoms[geoms.intersects(point)].index)
+        idx = max(geoms.index) + 1
+        try:
+            multiline = geoms[matches[0]].union(geoms[matches[1]])
+            linestring = shapely.ops.linemerge(multiline)
+            geoms = geoms.append(gpd.GeoSeries(linestring, index=[idx]))
+            geoms = geoms.drop(matches)
+        except IndexError:
+            import warnings
+            warnings.warn('An exception during merging occured. Lines at point [{x}, {y}] were not merged.'.format(x=point.x, y=point.y))
+
+    geoms_gdf = gpd.GeoDataFrame(geometry=geoms)
+    geoms_gdf.crs = streets.crs
+    streets = geoms_gdf
+    return streets


Test patch:
diff --git a/tests/test_distribution.py b/tests/test_distribution.py
index 3acecf1..d9617f8 100644
--- a/tests/test_distribution.py
+++ b/tests/test_distribution.py
@@ -15,8 +15,7 @@ class TestDistribution:
         self.df_buildings['height'] = np.linspace(10., 30., 144)
         self.df_buildings['volume'] = mm.volume(self.df_buildings, 'height')
         self.df_streets['nID'] = mm.unique_id(self.df_streets)
-        self.df_buildings['nID'], self.df_tessellation['nID'] = mm.get_network_id(self.df_buildings, self.df_streets,
-                                                                                  'uID', 'nID', self.df_tessellation, 'uID')
+        self.df_buildings['nID'] = mm.get_network_id(self.df_buildings, self.df_streets, 'uID', 'nID')
 
     def test_orientation(self):
         self.df_buildings['orient'] = mm.orientation(self.df_buildings)
diff --git a/tests/test_elements.py b/tests/test_elements.py
index 6ef8a8e..b908262 100644
--- a/tests/test_elements.py
+++ b/tests/test_elements.py
@@ -11,6 +11,7 @@ class TestElements:
         self.df_buildings = gpd.read_file(test_file_path, layer='buildings')
         self.df_tessellation = gpd.read_file(test_file_path, layer='tessellation')
         self.df_streets = gpd.read_file(test_file_path, layer='streets')
+        self.df_streets['nID'] = range(len(self.df_streets))
 
     def test_tessellation(self):
         tessellation = mm.tessellation(self.df_buildings)
@@ -30,14 +31,12 @@ class TestElements:
         assert len(blocks) == 8
 
     def test_get_network_id(self):
-        self.df_streets['nID'] = range(len(self.df_streets))
-        buildings_id, tessellation_id = mm.get_network_id(self.df_buildings, self.df_streets, 'uID', 'nID', self.df_tessellation, 'uID')
+        buildings_id = mm.get_network_id(self.df_buildings, self.df_streets, 'uID', 'nID')
         assert not buildings_id.isna().any()
-        assert not tessellation_id.isna().any()
 
     def test_get_node_id(self):
         nx = mm.gdf_to_nx(self.df_streets)
-        nodes = mm.nx_to_gdf(nx, edges=False)
-        nodes['nodeid'] = range(len(nodes))
-        ids = mm.get_node_id(self.df_buildings, nodes, 'nodeid')
+        nodes, edges = mm.nx_to_gdf(nx)
+        self.df_buildings['nID'] = mm.get_network_id(self.df_buildings, self.df_streets, 'uID', 'nID')
+        ids = mm.get_node_id(self.df_buildings, nodes, edges, 'nodeID', 'nID')
         assert not ids.isna().any()