voxcity 0.5.14__py3-none-any.whl → 0.5.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of voxcity might be problematic. Click here for more details.
- voxcity/downloader/citygml.py +202 -28
- voxcity/downloader/eubucco.py +91 -14
- voxcity/downloader/gee.py +164 -22
- voxcity/downloader/mbfp.py +55 -9
- voxcity/downloader/oemj.py +110 -24
- voxcity/downloader/omt.py +74 -7
- voxcity/downloader/osm.py +109 -23
- voxcity/downloader/overture.py +108 -23
- voxcity/downloader/utils.py +37 -7
- voxcity/exporter/envimet.py +180 -61
- voxcity/exporter/magicavoxel.py +138 -28
- voxcity/exporter/obj.py +159 -36
- voxcity/generator.py +159 -76
- voxcity/geoprocessor/draw.py +180 -27
- voxcity/geoprocessor/grid.py +178 -38
- voxcity/geoprocessor/mesh.py +347 -43
- voxcity/geoprocessor/network.py +196 -63
- voxcity/geoprocessor/polygon.py +365 -88
- voxcity/geoprocessor/utils.py +283 -72
- voxcity/simulator/solar.py +596 -201
- voxcity/simulator/view.py +278 -723
- voxcity/utils/lc.py +183 -0
- voxcity/utils/material.py +99 -32
- voxcity/utils/visualization.py +2578 -1988
- voxcity/utils/weather.py +816 -615
- {voxcity-0.5.14.dist-info → voxcity-0.5.15.dist-info}/METADATA +10 -12
- voxcity-0.5.15.dist-info/RECORD +38 -0
- {voxcity-0.5.14.dist-info → voxcity-0.5.15.dist-info}/WHEEL +1 -1
- voxcity-0.5.14.dist-info/RECORD +0 -38
- {voxcity-0.5.14.dist-info → voxcity-0.5.15.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.5.14.dist-info → voxcity-0.5.15.dist-info}/licenses/LICENSE +0 -0
- {voxcity-0.5.14.dist-info → voxcity-0.5.15.dist-info}/top_level.txt +0 -0
voxcity/geoprocessor/network.py
CHANGED
|
@@ -19,17 +19,42 @@ from .grid import grid_to_geodataframe
|
|
|
19
19
|
|
|
20
20
|
def vectorized_edge_values(G, polygons_gdf, value_col='value'):
|
|
21
21
|
"""
|
|
22
|
-
Compute average polygon values along each edge
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
Compute average polygon values along each edge in a network graph using vectorized operations.
|
|
23
|
+
|
|
24
|
+
This function performs efficient computation of average values from polygons that intersect
|
|
25
|
+
with network edges. It uses GeoDataFrames for vectorized spatial operations instead of
|
|
26
|
+
iterating over individual edges.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
G : networkx.MultiDiGraph
|
|
31
|
+
OSMnx graph with edges containing either geometry attributes or node coordinates.
|
|
32
|
+
polygons_gdf : geopandas.GeoDataFrame
|
|
33
|
+
GeoDataFrame containing polygons with values to be averaged along edges.
|
|
34
|
+
value_col : str, default='value'
|
|
35
|
+
Name of the column in polygons_gdf containing the values to average.
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
dict
|
|
40
|
+
Dictionary mapping edge tuples (u, v, k) to their computed average values.
|
|
41
|
+
Values are length-weighted averages of intersecting polygon values.
|
|
42
|
+
|
|
43
|
+
Notes
|
|
44
|
+
-----
|
|
45
|
+
The process involves:
|
|
46
|
+
1. Converting edges to a GeoDataFrame with LineString geometries
|
|
47
|
+
2. Projecting geometries to a metric CRS (EPSG:3857) for accurate length calculations
|
|
48
|
+
3. Computing intersections between edges and polygons
|
|
49
|
+
4. Calculating length-weighted averages of polygon values for each edge
|
|
26
50
|
"""
|
|
27
|
-
#
|
|
51
|
+
# Build edge GeoDataFrame in WGS84 (EPSG:4326)
|
|
28
52
|
records = []
|
|
29
53
|
for i, (u, v, k, data) in enumerate(G.edges(keys=True, data=True)):
|
|
30
54
|
if 'geometry' in data:
|
|
31
55
|
edge_geom = data['geometry']
|
|
32
56
|
else:
|
|
57
|
+
# Create LineString from node coordinates if no geometry exists
|
|
33
58
|
start_node = G.nodes[u]
|
|
34
59
|
end_node = G.nodes[v]
|
|
35
60
|
edge_geom = LineString([(start_node['x'], start_node['y']),
|
|
@@ -46,32 +71,25 @@ def vectorized_edge_values(G, polygons_gdf, value_col='value'):
|
|
|
46
71
|
if polygons_gdf.crs != edges_gdf.crs:
|
|
47
72
|
polygons_gdf = polygons_gdf.to_crs(edges_gdf.crs)
|
|
48
73
|
|
|
49
|
-
#
|
|
74
|
+
# Project to Web Mercator for accurate length calculations
|
|
50
75
|
edges_3857 = edges_gdf.to_crs(epsg=3857)
|
|
51
76
|
polys_3857 = polygons_gdf.to_crs(epsg=3857)
|
|
52
77
|
|
|
53
|
-
#
|
|
54
|
-
# gpd.overlay with how='intersection' can yield partial lines
|
|
78
|
+
# Compute intersections between edges and polygons
|
|
55
79
|
intersected = gpd.overlay(edges_3857, polys_3857, how='intersection')
|
|
56
80
|
|
|
57
|
-
#
|
|
58
|
-
# with columns from edges + polygons.
|
|
59
|
-
# For lines, 'intersection' yields the line portion inside each polygon.
|
|
60
|
-
# We'll compute the length, then do a length-weighted average of value_col.
|
|
61
|
-
|
|
81
|
+
# Calculate length-weighted averages
|
|
62
82
|
intersected['seg_length'] = intersected.geometry.length
|
|
63
|
-
# Weighted contribution = seg_length * polygon_value
|
|
64
83
|
intersected['weighted_val'] = intersected['seg_length'] * intersected[value_col]
|
|
65
84
|
|
|
66
|
-
#
|
|
85
|
+
# Group by edge and compute weighted averages
|
|
67
86
|
grouped = intersected.groupby('edge_id')
|
|
68
87
|
results = grouped.apply(
|
|
69
88
|
lambda df: df['weighted_val'].sum() / df['seg_length'].sum()
|
|
70
89
|
if df['seg_length'].sum() > 0 else np.nan
|
|
71
90
|
)
|
|
72
|
-
# results is a Series with index=edge_id
|
|
73
91
|
|
|
74
|
-
#
|
|
92
|
+
# Map results back to edge tuples
|
|
75
93
|
edge_values = {}
|
|
76
94
|
for edge_id, val in results.items():
|
|
77
95
|
rec = edges_gdf.iloc[edge_id]
|
|
@@ -86,6 +104,50 @@ def get_network_values(
|
|
|
86
104
|
value_name='value',
|
|
87
105
|
**kwargs
|
|
88
106
|
):
|
|
107
|
+
"""
|
|
108
|
+
Extract and visualize values from a grid along a street network.
|
|
109
|
+
|
|
110
|
+
This function downloads a street network from OpenStreetMap for a given area,
|
|
111
|
+
computes average grid values along network edges, and optionally visualizes
|
|
112
|
+
the results on an interactive map.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
grid : array-like or geopandas.GeoDataFrame
|
|
117
|
+
Either a grid array of values or a pre-built GeoDataFrame with polygons and values.
|
|
118
|
+
rectangle_vertices : list of tuples
|
|
119
|
+
List of (lon, lat) coordinates defining the bounding rectangle in EPSG:4326.
|
|
120
|
+
meshsize : float
|
|
121
|
+
Size of each grid cell (used only if grid is array-like).
|
|
122
|
+
value_name : str, default='value'
|
|
123
|
+
Name to use for the edge attribute storing computed values.
|
|
124
|
+
**kwargs : dict
|
|
125
|
+
Additional visualization and processing parameters:
|
|
126
|
+
- network_type : str, default='walk'
|
|
127
|
+
Type of street network to download ('walk', 'drive', etc.)
|
|
128
|
+
- vis_graph : bool, default=True
|
|
129
|
+
Whether to display the visualization
|
|
130
|
+
- colormap : str, default='viridis'
|
|
131
|
+
Matplotlib colormap for edge colors
|
|
132
|
+
- vmin, vmax : float, optional
|
|
133
|
+
Value range for color mapping
|
|
134
|
+
- edge_width : float, default=1
|
|
135
|
+
Width of edge lines in visualization
|
|
136
|
+
- fig_size : tuple, default=(15,15)
|
|
137
|
+
Figure size in inches
|
|
138
|
+
- zoom : int, default=16
|
|
139
|
+
Zoom level for basemap
|
|
140
|
+
- basemap_style : ctx.providers, default=CartoDB.Positron
|
|
141
|
+
Contextily basemap provider
|
|
142
|
+
- save_path : str, optional
|
|
143
|
+
Path to save the edge GeoDataFrame as a GeoPackage
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
tuple
|
|
148
|
+
(networkx.MultiDiGraph, geopandas.GeoDataFrame)
|
|
149
|
+
The network graph with computed edge values and edge geometries as a GeoDataFrame.
|
|
150
|
+
"""
|
|
89
151
|
defaults = {
|
|
90
152
|
'network_type': 'walk',
|
|
91
153
|
'vis_graph': True,
|
|
@@ -174,6 +236,10 @@ def interpolate_points_along_line(line, interval):
|
|
|
174
236
|
Interpolate points along a single LineString at a given interval (in meters).
|
|
175
237
|
If the line is shorter than `interval`, only start/end points are returned.
|
|
176
238
|
|
|
239
|
+
This function handles coordinate system transformations to ensure accurate
|
|
240
|
+
distance measurements, working in Web Mercator (EPSG:3857) for distance
|
|
241
|
+
calculations while maintaining WGS84 (EPSG:4326) for input/output.
|
|
242
|
+
|
|
177
243
|
Parameters
|
|
178
244
|
----------
|
|
179
245
|
line : shapely.geometry.LineString
|
|
@@ -184,7 +250,9 @@ def interpolate_points_along_line(line, interval):
|
|
|
184
250
|
Returns
|
|
185
251
|
-------
|
|
186
252
|
list of shapely.geometry.Point
|
|
187
|
-
Points in EPSG:4326 along the line.
|
|
253
|
+
Points in EPSG:4326 along the line, spaced approximately `interval` meters apart.
|
|
254
|
+
For lines shorter than interval, only start and end points are returned.
|
|
255
|
+
For empty lines, an empty list is returned.
|
|
188
256
|
"""
|
|
189
257
|
if line.is_empty:
|
|
190
258
|
return []
|
|
@@ -219,7 +287,11 @@ def interpolate_points_along_line(line, interval):
|
|
|
219
287
|
def gather_interpolation_points(G, interval=10.0, n_jobs=1):
|
|
220
288
|
"""
|
|
221
289
|
Gather all interpolation points for each edge in the graph into a single GeoDataFrame.
|
|
222
|
-
|
|
290
|
+
Supports parallel processing for improved performance on large networks.
|
|
291
|
+
|
|
292
|
+
This function processes each edge in the graph, either using its geometry attribute
|
|
293
|
+
or creating a LineString from node coordinates, then interpolates points along it
|
|
294
|
+
at the specified interval.
|
|
223
295
|
|
|
224
296
|
Parameters
|
|
225
297
|
----------
|
|
@@ -228,12 +300,16 @@ def gather_interpolation_points(G, interval=10.0, n_jobs=1):
|
|
|
228
300
|
interval : float, default=10.0
|
|
229
301
|
Interpolation distance interval in meters.
|
|
230
302
|
n_jobs : int, default=1
|
|
231
|
-
Number of parallel jobs
|
|
303
|
+
Number of parallel jobs for processing edges. Set to 1 for sequential processing,
|
|
304
|
+
or -1 to use all available CPU cores.
|
|
232
305
|
|
|
233
306
|
Returns
|
|
234
307
|
-------
|
|
235
308
|
gpd.GeoDataFrame
|
|
236
|
-
|
|
309
|
+
GeoDataFrame in EPSG:4326 with columns:
|
|
310
|
+
- edge_id: Index of the edge in the graph
|
|
311
|
+
- index_in_edge: Position of the point along its edge
|
|
312
|
+
- geometry: Point geometry
|
|
237
313
|
"""
|
|
238
314
|
edges = list(G.edges(keys=True, data=True))
|
|
239
315
|
|
|
@@ -268,21 +344,26 @@ def gather_interpolation_points(G, interval=10.0, n_jobs=1):
|
|
|
268
344
|
|
|
269
345
|
def fetch_elevations_for_points(points_gdf_3857, dem_gdf_3857, elevation_col='value'):
|
|
270
346
|
"""
|
|
271
|
-
|
|
347
|
+
Perform a spatial join to fetch DEM elevations for interpolated points.
|
|
348
|
+
|
|
349
|
+
Uses nearest neighbor matching in projected coordinates (EPSG:3857) to ensure
|
|
350
|
+
accurate distance calculations when finding the closest DEM cell for each point.
|
|
272
351
|
|
|
273
352
|
Parameters
|
|
274
353
|
----------
|
|
275
354
|
points_gdf_3857 : gpd.GeoDataFrame
|
|
276
|
-
Interpolation points in EPSG:3857.
|
|
355
|
+
Interpolation points in EPSG:3857 projection.
|
|
277
356
|
dem_gdf_3857 : gpd.GeoDataFrame
|
|
278
|
-
DEM polygons in EPSG:3857,
|
|
357
|
+
DEM polygons in EPSG:3857 projection, containing elevation values.
|
|
279
358
|
elevation_col : str, default='value'
|
|
280
|
-
|
|
359
|
+
Name of the column containing elevation values in dem_gdf_3857.
|
|
281
360
|
|
|
282
361
|
Returns
|
|
283
362
|
-------
|
|
284
363
|
gpd.GeoDataFrame
|
|
285
|
-
|
|
364
|
+
Copy of points_gdf_3857 with additional columns:
|
|
365
|
+
- elevation: Elevation value from nearest DEM cell
|
|
366
|
+
- dist_to_poly: Distance to nearest DEM cell
|
|
286
367
|
"""
|
|
287
368
|
joined = gpd.sjoin_nearest(
|
|
288
369
|
points_gdf_3857,
|
|
@@ -296,10 +377,25 @@ def fetch_elevations_for_points(points_gdf_3857, dem_gdf_3857, elevation_col='va
|
|
|
296
377
|
|
|
297
378
|
def compute_slope_for_group(df):
|
|
298
379
|
"""
|
|
299
|
-
|
|
300
|
-
|
|
380
|
+
Compute average slope between consecutive points along a single edge.
|
|
381
|
+
|
|
382
|
+
Slopes are calculated as absolute percentage grade (rise/run * 100) between
|
|
383
|
+
consecutive points, then averaged for the entire edge. Points must be in
|
|
384
|
+
EPSG:3857 projection for accurate horizontal distance calculations.
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
df : pd.DataFrame
|
|
389
|
+
DataFrame containing points for a single edge with columns:
|
|
390
|
+
- geometry: Point geometries in EPSG:3857
|
|
391
|
+
- elevation: Elevation values in meters
|
|
392
|
+
- index_in_edge: Position along the edge for sorting
|
|
301
393
|
|
|
302
|
-
|
|
394
|
+
Returns
|
|
395
|
+
-------
|
|
396
|
+
float
|
|
397
|
+
Average slope as a percentage, or np.nan if no valid slopes can be computed
|
|
398
|
+
(e.g., when points are coincident or no elevation change).
|
|
303
399
|
"""
|
|
304
400
|
# Sort by position along the edge
|
|
305
401
|
df = df.sort_values("index_in_edge")
|
|
@@ -326,19 +422,28 @@ def compute_slope_for_group(df):
|
|
|
326
422
|
|
|
327
423
|
def calculate_edge_slopes_from_join(joined_points_gdf, n_edges):
|
|
328
424
|
"""
|
|
329
|
-
Calculate average slopes for
|
|
425
|
+
Calculate average slopes for all edges in the network from interpolated points.
|
|
426
|
+
|
|
427
|
+
This function groups points by edge_id and computes the average slope for each edge
|
|
428
|
+
using the compute_slope_for_group function. It ensures all edges in the original
|
|
429
|
+
graph have a slope value, even if no valid slope could be computed.
|
|
330
430
|
|
|
331
431
|
Parameters
|
|
332
432
|
----------
|
|
333
433
|
joined_points_gdf : gpd.GeoDataFrame
|
|
334
|
-
|
|
434
|
+
Points with elevations in EPSG:3857, must have columns:
|
|
435
|
+
- edge_id: Index of the edge in the graph
|
|
436
|
+
- index_in_edge: Position along the edge
|
|
437
|
+
- elevation: Elevation value
|
|
438
|
+
- geometry: Point geometry
|
|
335
439
|
n_edges : int
|
|
336
|
-
|
|
440
|
+
Total number of edges in the original graph.
|
|
337
441
|
|
|
338
442
|
Returns
|
|
339
443
|
-------
|
|
340
444
|
dict
|
|
341
|
-
edge_id
|
|
445
|
+
Dictionary mapping edge_id to average slope (in %). Edges with no valid
|
|
446
|
+
slope calculation are assigned np.nan.
|
|
342
447
|
"""
|
|
343
448
|
# We'll group by edge_id, ignoring the group columns in apply (pandas >= 2.1).
|
|
344
449
|
# If your pandas version < 2.1, just do a column subset after groupby.
|
|
@@ -369,27 +474,72 @@ def analyze_network_slopes(
|
|
|
369
474
|
**kwargs
|
|
370
475
|
):
|
|
371
476
|
"""
|
|
372
|
-
Analyze and visualize network slopes
|
|
477
|
+
Analyze and visualize street network slopes using Digital Elevation Model (DEM) data.
|
|
478
|
+
|
|
479
|
+
This function performs a comprehensive analysis of street network slopes by:
|
|
480
|
+
1. Converting DEM data to a GeoDataFrame of elevation polygons
|
|
481
|
+
2. Downloading the street network from OpenStreetMap
|
|
482
|
+
3. Interpolating points along network edges
|
|
483
|
+
4. Matching points to DEM elevations
|
|
484
|
+
5. Computing slopes between consecutive points
|
|
485
|
+
6. Aggregating slopes per edge
|
|
486
|
+
7. Optionally visualizing results on an interactive map
|
|
487
|
+
|
|
488
|
+
The analysis uses appropriate coordinate transformations between WGS84 (EPSG:4326)
|
|
489
|
+
for geographic operations and Web Mercator (EPSG:3857) for distance calculations.
|
|
373
490
|
|
|
374
491
|
Parameters
|
|
375
492
|
----------
|
|
376
493
|
dem_grid : array-like
|
|
377
|
-
|
|
494
|
+
Digital Elevation Model grid data containing elevation values.
|
|
378
495
|
meshsize : float
|
|
379
|
-
|
|
496
|
+
Size of each DEM grid cell.
|
|
380
497
|
value_name : str, default='slope'
|
|
381
|
-
|
|
498
|
+
Name to use for the slope attribute in output data.
|
|
382
499
|
interval : float, default=10.0
|
|
383
|
-
|
|
500
|
+
Distance in meters between interpolated points along edges.
|
|
384
501
|
n_jobs : int, default=1
|
|
385
|
-
|
|
502
|
+
Number of parallel jobs for edge processing.
|
|
386
503
|
**kwargs : dict
|
|
387
|
-
Additional parameters:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
504
|
+
Additional configuration parameters:
|
|
505
|
+
- rectangle_vertices : list of (lon, lat), required
|
|
506
|
+
Coordinates defining the analysis area in EPSG:4326
|
|
507
|
+
- network_type : str, default='walk'
|
|
508
|
+
Type of street network to download
|
|
509
|
+
- vis_graph : bool, default=True
|
|
510
|
+
Whether to create visualization
|
|
511
|
+
- colormap : str, default='viridis'
|
|
512
|
+
Matplotlib colormap for slope visualization
|
|
513
|
+
- vmin, vmax : float, optional
|
|
514
|
+
Value range for slope coloring
|
|
515
|
+
- edge_width : float, default=1
|
|
516
|
+
Width of edge lines in plot
|
|
517
|
+
- fig_size : tuple, default=(15,15)
|
|
518
|
+
Figure size in inches
|
|
519
|
+
- zoom : int, default=16
|
|
520
|
+
Zoom level for basemap
|
|
521
|
+
- basemap_style : ctx.providers, default=CartoDB.Positron
|
|
522
|
+
Contextily basemap provider
|
|
523
|
+
- output_directory : str, optional
|
|
524
|
+
Directory to save results
|
|
525
|
+
- output_file_name : str, default='network_slopes'
|
|
526
|
+
Base name for output files
|
|
527
|
+
- alpha : float, default=1.0
|
|
528
|
+
Transparency of edge lines in visualization
|
|
529
|
+
|
|
530
|
+
Returns
|
|
531
|
+
-------
|
|
532
|
+
tuple
|
|
533
|
+
(networkx.MultiDiGraph, geopandas.GeoDataFrame)
|
|
534
|
+
- Graph with slope values as edge attributes
|
|
535
|
+
- GeoDataFrame of edges with geometries and slope values
|
|
536
|
+
|
|
537
|
+
Notes
|
|
538
|
+
-----
|
|
539
|
+
- Slopes are calculated as absolute percentage grades (rise/run * 100)
|
|
540
|
+
- Edge slopes are length-weighted averages of point-to-point slopes
|
|
541
|
+
- The visualization includes a basemap and legend showing slope percentages
|
|
542
|
+
- If output_directory is specified, results are saved as a GeoPackage
|
|
393
543
|
"""
|
|
394
544
|
defaults = {
|
|
395
545
|
'rectangle_vertices': None,
|
|
@@ -482,7 +632,7 @@ def analyze_network_slopes(
|
|
|
482
632
|
# 10) Visualization
|
|
483
633
|
if settings['vis_graph']:
|
|
484
634
|
# Create a Polygon from the rectangle vertices
|
|
485
|
-
rectangle_polygon = Polygon(rectangle_vertices)
|
|
635
|
+
rectangle_polygon = Polygon(settings['rectangle_vertices'])
|
|
486
636
|
|
|
487
637
|
# Convert the rectangle polygon to the same CRS as edge_gdf_web
|
|
488
638
|
rectangle_gdf = gpd.GeoDataFrame(index=[0], crs='epsg:4326', geometry=[rectangle_polygon])
|
|
@@ -526,22 +676,5 @@ def analyze_network_slopes(
|
|
|
526
676
|
|
|
527
677
|
# Show the plot
|
|
528
678
|
plt.show()
|
|
529
|
-
# edge_gdf_web = edge_gdf.to_crs(epsg=3857)
|
|
530
|
-
# fig, ax = plt.subplots(figsize=settings['fig_size'])
|
|
531
|
-
# edge_gdf_web.plot(
|
|
532
|
-
# column=value_name,
|
|
533
|
-
# ax=ax,
|
|
534
|
-
# cmap=settings['colormap'],
|
|
535
|
-
# legend=True,
|
|
536
|
-
# vmin=settings['vmin'],
|
|
537
|
-
# vmax=settings['vmax'],
|
|
538
|
-
# linewidth=settings['edge_width'],
|
|
539
|
-
# alpha=settings['alpha'],
|
|
540
|
-
# legend_kwds={'label': f"{value_name} (%)"}
|
|
541
|
-
# )
|
|
542
|
-
# ctx.add_basemap(ax, source=settings['basemap_style'], zoom=settings['zoom'])
|
|
543
|
-
# ax.set_axis_off()
|
|
544
|
-
# plt.title(f'Network {value_name} Analysis', pad=20)
|
|
545
|
-
# plt.show()
|
|
546
679
|
|
|
547
680
|
return G, edge_gdf
|