voxcity 0.3.3__py3-none-any.whl → 0.3.5__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/download/eubucco.py +9 -17
- voxcity/download/gee.py +4 -3
- voxcity/download/mbfp.py +7 -7
- voxcity/download/oemj.py +22 -22
- voxcity/download/omt.py +10 -10
- voxcity/download/osm.py +23 -21
- voxcity/download/overture.py +7 -15
- voxcity/file/envimet.py +4 -4
- voxcity/file/geojson.py +25 -39
- voxcity/geo/__init_.py +2 -1
- voxcity/geo/draw.py +41 -45
- voxcity/geo/grid.py +68 -145
- voxcity/geo/network.py +193 -0
- voxcity/geo/utils.py +79 -66
- voxcity/sim/solar.py +5 -5
- voxcity/sim/view.py +5 -5
- voxcity/utils/__init_.py +2 -1
- voxcity/utils/material.py +139 -0
- voxcity/utils/visualization.py +128 -354
- voxcity/utils/weather.py +7 -7
- voxcity/voxcity.py +56 -8
- {voxcity-0.3.3.dist-info → voxcity-0.3.5.dist-info}/METADATA +6 -5
- voxcity-0.3.5.dist-info/RECORD +36 -0
- {voxcity-0.3.3.dist-info → voxcity-0.3.5.dist-info}/WHEEL +1 -1
- voxcity-0.3.3.dist-info/RECORD +0 -34
- {voxcity-0.3.3.dist-info → voxcity-0.3.5.dist-info}/AUTHORS.rst +0 -0
- {voxcity-0.3.3.dist-info → voxcity-0.3.5.dist-info}/LICENSE +0 -0
- {voxcity-0.3.3.dist-info → voxcity-0.3.5.dist-info}/top_level.txt +0 -0
voxcity/geo/grid.py
CHANGED
|
@@ -4,7 +4,7 @@ This module provides functions for creating and manipulating grids of building h
|
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
import os
|
|
7
|
-
from shapely.geometry import Polygon
|
|
7
|
+
from shapely.geometry import Polygon, box
|
|
8
8
|
from scipy.ndimage import label, generate_binary_structure
|
|
9
9
|
from pyproj import Geod, Transformer, CRS
|
|
10
10
|
import rasterio
|
|
@@ -12,6 +12,7 @@ from affine import Affine
|
|
|
12
12
|
from shapely.geometry import box
|
|
13
13
|
from scipy.interpolate import griddata
|
|
14
14
|
from shapely.errors import GEOSException
|
|
15
|
+
import geopandas as gpd
|
|
15
16
|
|
|
16
17
|
from .utils import (
|
|
17
18
|
initialize_geod,
|
|
@@ -235,77 +236,6 @@ def tree_height_grid_from_land_cover(land_cover_grid_ori):
|
|
|
235
236
|
|
|
236
237
|
return tree_height_grid
|
|
237
238
|
|
|
238
|
-
def create_land_cover_grid_from_geotiff(tiff_path, mesh_size, land_cover_classes):
|
|
239
|
-
"""
|
|
240
|
-
Create a land cover grid from a GeoTIFF file.
|
|
241
|
-
|
|
242
|
-
Args:
|
|
243
|
-
tiff_path (str): Path to GeoTIFF file
|
|
244
|
-
mesh_size (float): Size of mesh cells
|
|
245
|
-
land_cover_classes (dict): Dictionary mapping land cover classes
|
|
246
|
-
|
|
247
|
-
Returns:
|
|
248
|
-
numpy.ndarray: Grid of land cover classes
|
|
249
|
-
"""
|
|
250
|
-
with rasterio.open(tiff_path) as src:
|
|
251
|
-
# Read RGB bands
|
|
252
|
-
img = src.read((1,2,3))
|
|
253
|
-
left, bottom, right, top = src.bounds
|
|
254
|
-
src_crs = src.crs
|
|
255
|
-
|
|
256
|
-
# Handle different coordinate reference systems
|
|
257
|
-
if src_crs.to_epsg() == 3857: # Web Mercator
|
|
258
|
-
# Convert bounds from Web Mercator to WGS84 for accurate distance calculations
|
|
259
|
-
wgs84 = CRS.from_epsg(4326)
|
|
260
|
-
transformer = Transformer.from_crs(src_crs, wgs84, always_xy=True)
|
|
261
|
-
left_wgs84, bottom_wgs84 = transformer.transform(left, bottom)
|
|
262
|
-
right_wgs84, top_wgs84 = transformer.transform(right, top)
|
|
263
|
-
|
|
264
|
-
# Calculate actual distances using geodesic calculations
|
|
265
|
-
geod = Geod(ellps="WGS84")
|
|
266
|
-
_, _, width = geod.inv(left_wgs84, bottom_wgs84, right_wgs84, bottom_wgs84)
|
|
267
|
-
_, _, height = geod.inv(left_wgs84, bottom_wgs84, left_wgs84, top_wgs84)
|
|
268
|
-
else:
|
|
269
|
-
# For projections already in meters, use simple subtraction
|
|
270
|
-
width = right - left
|
|
271
|
-
height = top - bottom
|
|
272
|
-
|
|
273
|
-
# Calculate grid dimensions based on mesh size
|
|
274
|
-
num_cells_x = int(width / mesh_size + 0.5)
|
|
275
|
-
num_cells_y = int(height / mesh_size + 0.5)
|
|
276
|
-
|
|
277
|
-
# Adjust mesh size to fit image exactly
|
|
278
|
-
adjusted_mesh_size_x = (right - left) / num_cells_x
|
|
279
|
-
adjusted_mesh_size_y = (top - bottom) / num_cells_y
|
|
280
|
-
|
|
281
|
-
# Create affine transform for new grid
|
|
282
|
-
new_affine = Affine(adjusted_mesh_size_x, 0, left, 0, -adjusted_mesh_size_y, top)
|
|
283
|
-
|
|
284
|
-
# Create coordinate grids
|
|
285
|
-
cols, rows = np.meshgrid(np.arange(num_cells_x), np.arange(num_cells_y))
|
|
286
|
-
xs, ys = new_affine * (cols, rows)
|
|
287
|
-
xs_flat, ys_flat = xs.flatten(), ys.flatten()
|
|
288
|
-
|
|
289
|
-
# Convert coordinates to image indices
|
|
290
|
-
row, col = src.index(xs_flat, ys_flat)
|
|
291
|
-
row, col = np.array(row), np.array(col)
|
|
292
|
-
|
|
293
|
-
# Filter out invalid indices
|
|
294
|
-
valid = (row >= 0) & (row < src.height) & (col >= 0) & (col < src.width)
|
|
295
|
-
row, col = row[valid], col[valid]
|
|
296
|
-
|
|
297
|
-
# Create output grid and fill with land cover classes
|
|
298
|
-
grid = np.full((num_cells_y, num_cells_x), 'No Data', dtype=object)
|
|
299
|
-
|
|
300
|
-
for i, (r, c) in enumerate(zip(row, col)):
|
|
301
|
-
cell_data = img[:, r, c]
|
|
302
|
-
dominant_class = get_dominant_class(cell_data, land_cover_classes)
|
|
303
|
-
grid_row, grid_col = np.unravel_index(i, (num_cells_y, num_cells_x))
|
|
304
|
-
grid[grid_row, grid_col] = dominant_class
|
|
305
|
-
|
|
306
|
-
# Flip grid vertically before returning
|
|
307
|
-
return np.flipud(grid)
|
|
308
|
-
|
|
309
239
|
def create_land_cover_grid_from_geotiff_polygon(tiff_path, mesh_size, land_cover_classes, polygon):
|
|
310
240
|
"""
|
|
311
241
|
Create a land cover grid from a GeoTIFF file within a polygon boundary.
|
|
@@ -329,7 +259,7 @@ def create_land_cover_grid_from_geotiff_polygon(tiff_path, mesh_size, land_cover
|
|
|
329
259
|
poly = Polygon(polygon)
|
|
330
260
|
|
|
331
261
|
# Get bounds of the polygon in WGS84 coordinates
|
|
332
|
-
bottom_wgs84,
|
|
262
|
+
left_wgs84, bottom_wgs84, right_wgs84, top_wgs84 = poly.bounds
|
|
333
263
|
# print(left, bottom, right, top)
|
|
334
264
|
|
|
335
265
|
# Calculate width and height using geodesic calculations for accuracy
|
|
@@ -381,7 +311,7 @@ def create_land_cover_grid_from_geojson_polygon(geojson_data, meshsize, source,
|
|
|
381
311
|
geojson_data (dict): GeoJSON data containing land cover polygons
|
|
382
312
|
meshsize (float): Size of each grid cell in meters
|
|
383
313
|
source (str): Source of the land cover data to determine class priorities
|
|
384
|
-
rectangle_vertices (list): List of 4 (lat
|
|
314
|
+
rectangle_vertices (list): List of 4 (lon,lat) coordinate pairs defining the rectangle bounds
|
|
385
315
|
|
|
386
316
|
Returns:
|
|
387
317
|
numpy.ndarray: 2D grid of land cover classes as strings
|
|
@@ -411,8 +341,8 @@ def create_land_cover_grid_from_geojson_polygon(geojson_data, meshsize, source,
|
|
|
411
341
|
vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
|
|
412
342
|
|
|
413
343
|
# Calculate actual distances between vertices using geodesic calculations
|
|
414
|
-
dist_side_1 = calculate_distance(geod, vertex_0[
|
|
415
|
-
dist_side_2 = calculate_distance(geod, vertex_0[
|
|
344
|
+
dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
|
|
345
|
+
dist_side_2 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_3[0], vertex_3[1])
|
|
416
346
|
|
|
417
347
|
# Create vectors representing the sides of the rectangle
|
|
418
348
|
side_1 = np.array(vertex_1) - np.array(vertex_0)
|
|
@@ -476,70 +406,6 @@ def create_land_cover_grid_from_geojson_polygon(geojson_data, meshsize, source,
|
|
|
476
406
|
continue
|
|
477
407
|
return grid
|
|
478
408
|
|
|
479
|
-
def create_canopy_height_grid_from_geotiff(tiff_path, mesh_size):
|
|
480
|
-
"""
|
|
481
|
-
Create a canopy height grid from a GeoTIFF file.
|
|
482
|
-
|
|
483
|
-
Args:
|
|
484
|
-
tiff_path (str): Path to GeoTIFF file
|
|
485
|
-
mesh_size (float): Size of mesh cells
|
|
486
|
-
|
|
487
|
-
Returns:
|
|
488
|
-
numpy.ndarray: Grid of canopy heights
|
|
489
|
-
"""
|
|
490
|
-
with rasterio.open(tiff_path) as src:
|
|
491
|
-
# Read single band height data
|
|
492
|
-
img = src.read(1)
|
|
493
|
-
left, bottom, right, top = src.bounds
|
|
494
|
-
src_crs = src.crs
|
|
495
|
-
|
|
496
|
-
# Handle coordinate system conversion and distance calculations
|
|
497
|
-
if src_crs.to_epsg() == 3857: # Web Mercator projection
|
|
498
|
-
# Convert bounds to WGS84 for accurate distance calculation
|
|
499
|
-
wgs84 = CRS.from_epsg(4326)
|
|
500
|
-
transformer = Transformer.from_crs(src_crs, wgs84, always_xy=True)
|
|
501
|
-
left_wgs84, bottom_wgs84 = transformer.transform(left, bottom)
|
|
502
|
-
right_wgs84, top_wgs84 = transformer.transform(right, top)
|
|
503
|
-
|
|
504
|
-
# Calculate actual distances using geodesic methods
|
|
505
|
-
geod = Geod(ellps="WGS84")
|
|
506
|
-
_, _, width = geod.inv(left_wgs84, bottom_wgs84, right_wgs84, bottom_wgs84)
|
|
507
|
-
_, _, height = geod.inv(left_wgs84, bottom_wgs84, left_wgs84, top_wgs84)
|
|
508
|
-
else:
|
|
509
|
-
# For projections already in meters, use simple subtraction
|
|
510
|
-
width = right - left
|
|
511
|
-
height = top - bottom
|
|
512
|
-
|
|
513
|
-
# Calculate grid dimensions and adjust mesh size
|
|
514
|
-
num_cells_x = int(width / mesh_size + 0.5)
|
|
515
|
-
num_cells_y = int(height / mesh_size + 0.5)
|
|
516
|
-
|
|
517
|
-
adjusted_mesh_size_x = (right - left) / num_cells_x
|
|
518
|
-
adjusted_mesh_size_y = (top - bottom) / num_cells_y
|
|
519
|
-
|
|
520
|
-
# Create affine transform for coordinate mapping
|
|
521
|
-
new_affine = Affine(adjusted_mesh_size_x, 0, left, 0, -adjusted_mesh_size_y, top)
|
|
522
|
-
|
|
523
|
-
# Generate coordinate grids
|
|
524
|
-
cols, rows = np.meshgrid(np.arange(num_cells_x), np.arange(num_cells_y))
|
|
525
|
-
xs, ys = new_affine * (cols, rows)
|
|
526
|
-
xs_flat, ys_flat = xs.flatten(), ys.flatten()
|
|
527
|
-
|
|
528
|
-
# Convert to image coordinates
|
|
529
|
-
row, col = src.index(xs_flat, ys_flat)
|
|
530
|
-
row, col = np.array(row), np.array(col)
|
|
531
|
-
|
|
532
|
-
# Filter valid indices
|
|
533
|
-
valid = (row >= 0) & (row < src.height) & (col >= 0) & (col < src.width)
|
|
534
|
-
row, col = row[valid], col[valid]
|
|
535
|
-
|
|
536
|
-
# Create output grid and fill with height values
|
|
537
|
-
grid = np.full((num_cells_y, num_cells_x), np.nan)
|
|
538
|
-
flat_indices = np.ravel_multi_index((row, col), img.shape)
|
|
539
|
-
np.put(grid, np.ravel_multi_index((rows.flatten()[valid], cols.flatten()[valid]), grid.shape), img.flat[flat_indices])
|
|
540
|
-
|
|
541
|
-
return np.flipud(grid)
|
|
542
|
-
|
|
543
409
|
def create_height_grid_from_geotiff_polygon(tiff_path, mesh_size, polygon):
|
|
544
410
|
"""
|
|
545
411
|
Create a height grid from a GeoTIFF file within a polygon boundary.
|
|
@@ -562,8 +428,9 @@ def create_height_grid_from_geotiff_polygon(tiff_path, mesh_size, polygon):
|
|
|
562
428
|
poly = Polygon(polygon)
|
|
563
429
|
|
|
564
430
|
# Get polygon bounds in WGS84
|
|
565
|
-
bottom_wgs84,
|
|
566
|
-
print(left, bottom, right, top)
|
|
431
|
+
left_wgs84, bottom_wgs84, right_wgs84, top_wgs84 = poly.bounds
|
|
432
|
+
# print(left, bottom, right, top)
|
|
433
|
+
# print(left_wgs84, bottom_wgs84, right_wgs84, top_wgs84)
|
|
567
434
|
|
|
568
435
|
# Calculate actual distances using geodesic methods
|
|
569
436
|
geod = Geod(ellps="WGS84")
|
|
@@ -624,8 +491,8 @@ def create_building_height_grid_from_geojson_polygon(geojson_data, meshsize, rec
|
|
|
624
491
|
vertex_0, vertex_1, vertex_3 = rectangle_vertices[0], rectangle_vertices[1], rectangle_vertices[3]
|
|
625
492
|
|
|
626
493
|
# Calculate distances between vertices
|
|
627
|
-
dist_side_1 = calculate_distance(geod, vertex_0[
|
|
628
|
-
dist_side_2 = calculate_distance(geod, vertex_0[
|
|
494
|
+
dist_side_1 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_1[0], vertex_1[1])
|
|
495
|
+
dist_side_2 = calculate_distance(geod, vertex_0[0], vertex_0[1], vertex_3[0], vertex_3[1])
|
|
629
496
|
|
|
630
497
|
# Calculate normalized vectors for grid orientation
|
|
631
498
|
side_1 = np.array(vertex_1) - np.array(vertex_0)
|
|
@@ -882,4 +749,60 @@ def create_dem_grid_from_geotiff_polygon(tiff_path, mesh_size, rectangle_vertice
|
|
|
882
749
|
# Use nearest neighbor interpolation for raw data
|
|
883
750
|
grid = griddata(points, values, (xx, yy), method='nearest')
|
|
884
751
|
|
|
885
|
-
return np.flipud(grid)
|
|
752
|
+
return np.flipud(grid)
|
|
753
|
+
|
|
754
|
+
def grid_to_geodataframe(grid_ori, rectangle_vertices, meshsize):
|
|
755
|
+
"""Converts a 2D grid to a GeoDataFrame with cell polygons and values.
|
|
756
|
+
|
|
757
|
+
Args:
|
|
758
|
+
grid: 2D numpy array containing grid values
|
|
759
|
+
rectangle_vertices: List of [lon, lat] coordinates defining area corners
|
|
760
|
+
meshsize: Size of each grid cell in meters
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
GeoDataFrame with columns:
|
|
764
|
+
- geometry: Polygon geometry of each grid cell
|
|
765
|
+
- value: Value from the grid
|
|
766
|
+
"""
|
|
767
|
+
grid = np.flipud(grid_ori.copy())
|
|
768
|
+
|
|
769
|
+
# Extract bounds from rectangle vertices
|
|
770
|
+
min_lon = min(v[0] for v in rectangle_vertices)
|
|
771
|
+
max_lon = max(v[0] for v in rectangle_vertices)
|
|
772
|
+
min_lat = min(v[1] for v in rectangle_vertices)
|
|
773
|
+
max_lat = max(v[1] for v in rectangle_vertices)
|
|
774
|
+
|
|
775
|
+
rows, cols = grid.shape
|
|
776
|
+
|
|
777
|
+
# Calculate cell sizes in degrees (approximate)
|
|
778
|
+
# 111,111 meters = 1 degree at equator
|
|
779
|
+
cell_size_lon = meshsize / (111111 * np.cos(np.mean([min_lat, max_lat]) * np.pi / 180))
|
|
780
|
+
cell_size_lat = meshsize / 111111
|
|
781
|
+
|
|
782
|
+
# Create lists to store data
|
|
783
|
+
polygons = []
|
|
784
|
+
values = []
|
|
785
|
+
|
|
786
|
+
# Create grid cells
|
|
787
|
+
for i in range(rows):
|
|
788
|
+
for j in range(cols):
|
|
789
|
+
# Calculate cell bounds
|
|
790
|
+
cell_min_lon = min_lon + j * cell_size_lon
|
|
791
|
+
cell_max_lon = min_lon + (j + 1) * cell_size_lon
|
|
792
|
+
# Flip vertical axis since grid is stored with origin at top-left
|
|
793
|
+
cell_min_lat = max_lat - (i + 1) * cell_size_lat
|
|
794
|
+
cell_max_lat = max_lat - i * cell_size_lat
|
|
795
|
+
|
|
796
|
+
# Create polygon for cell
|
|
797
|
+
cell_poly = box(cell_min_lon, cell_min_lat, cell_max_lon, cell_max_lat)
|
|
798
|
+
|
|
799
|
+
polygons.append(cell_poly)
|
|
800
|
+
values.append(grid[i, j])
|
|
801
|
+
|
|
802
|
+
# Create GeoDataFrame
|
|
803
|
+
gdf = gpd.GeoDataFrame({
|
|
804
|
+
'geometry': polygons,
|
|
805
|
+
'value': values
|
|
806
|
+
}, crs=CRS.from_epsg(4326))
|
|
807
|
+
|
|
808
|
+
return gdf
|
voxcity/geo/network.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import contextily as ctx
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import geopandas as gpd
|
|
6
|
+
from shapely.geometry import LineString
|
|
7
|
+
import networkx as nx
|
|
8
|
+
import osmnx as ox
|
|
9
|
+
|
|
10
|
+
from .grid import grid_to_geodataframe
|
|
11
|
+
|
|
12
|
+
def calculate_edge_values(G, gdf, value_col='value'):
|
|
13
|
+
"""
|
|
14
|
+
Calculate average values for graph edges based on intersection with polygons.
|
|
15
|
+
|
|
16
|
+
Parameters:
|
|
17
|
+
-----------
|
|
18
|
+
G : NetworkX Graph
|
|
19
|
+
Input graph with edges to analyze
|
|
20
|
+
gdf : GeoDataFrame
|
|
21
|
+
Grid containing polygons with values
|
|
22
|
+
value_col : str, default 'value'
|
|
23
|
+
Name of the column containing values in the grid
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
--------
|
|
27
|
+
dict
|
|
28
|
+
Dictionary with edge identifiers (u,v,k) as keys and average values as values
|
|
29
|
+
"""
|
|
30
|
+
edge_values = {}
|
|
31
|
+
for u, v, k, data in G.edges(data=True, keys=True):
|
|
32
|
+
if 'geometry' in data:
|
|
33
|
+
edge_line = data['geometry']
|
|
34
|
+
else:
|
|
35
|
+
start_node = G.nodes[u]
|
|
36
|
+
end_node = G.nodes[v]
|
|
37
|
+
edge_line = LineString([(start_node['x'], start_node['y']),
|
|
38
|
+
(end_node['x'], end_node['y'])])
|
|
39
|
+
|
|
40
|
+
intersecting_polys = gdf[gdf.geometry.intersects(edge_line)]
|
|
41
|
+
|
|
42
|
+
if len(intersecting_polys) > 0:
|
|
43
|
+
total_length = 0
|
|
44
|
+
weighted_sum = 0
|
|
45
|
+
|
|
46
|
+
for idx, poly in intersecting_polys.iterrows():
|
|
47
|
+
if pd.isna(poly[value_col]):
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
intersection = edge_line.intersection(poly.geometry)
|
|
51
|
+
if not intersection.is_empty:
|
|
52
|
+
length = intersection.length
|
|
53
|
+
total_length += length
|
|
54
|
+
weighted_sum += length * poly[value_col]
|
|
55
|
+
|
|
56
|
+
if total_length > 0:
|
|
57
|
+
avg_value = weighted_sum / total_length
|
|
58
|
+
edge_values[(u, v, k)] = avg_value
|
|
59
|
+
else:
|
|
60
|
+
edge_values[(u, v, k)] = np.nan
|
|
61
|
+
else:
|
|
62
|
+
edge_values[(u, v, k)] = np.nan
|
|
63
|
+
|
|
64
|
+
return edge_values
|
|
65
|
+
|
|
66
|
+
def get_network_values(grid, rectangle_vertices, meshsize, value_name='value', **kwargs):
|
|
67
|
+
"""
|
|
68
|
+
Analyze and visualize network values based on grid intersections.
|
|
69
|
+
|
|
70
|
+
Parameters:
|
|
71
|
+
-----------
|
|
72
|
+
grid : GeoDataFrame
|
|
73
|
+
Input grid with geometries and values
|
|
74
|
+
rectangle_vertices : list
|
|
75
|
+
List of coordinates defining the bounding box vertices
|
|
76
|
+
meshsize : float
|
|
77
|
+
Size of the mesh grid
|
|
78
|
+
value_name : str, default 'value'
|
|
79
|
+
Name of the column containing values in the grid
|
|
80
|
+
**kwargs : dict
|
|
81
|
+
Optional arguments including:
|
|
82
|
+
- network_type : str, default 'walk'
|
|
83
|
+
Type of network to download ('walk', 'drive', 'all', etc.)
|
|
84
|
+
- vis_graph : bool, default True
|
|
85
|
+
Whether to visualize the graph
|
|
86
|
+
- colormap : str, default 'viridis'
|
|
87
|
+
Matplotlib colormap name for visualization
|
|
88
|
+
- vmin : float, optional
|
|
89
|
+
Minimum value for color scaling
|
|
90
|
+
- vmax : float, optional
|
|
91
|
+
Maximum value for color scaling
|
|
92
|
+
- edge_width : float, default 1
|
|
93
|
+
Width of the edges in visualization
|
|
94
|
+
- fig_size : tuple, default (15,15)
|
|
95
|
+
Figure size for visualization
|
|
96
|
+
- zoom : int, default 16
|
|
97
|
+
Zoom level for the basemap
|
|
98
|
+
- basemap_style : ctx.providers, default CartoDB.Positron
|
|
99
|
+
Contextily basemap provider
|
|
100
|
+
- save_path : str, optional
|
|
101
|
+
Path to save the output GeoPackage
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
--------
|
|
105
|
+
tuple : (NetworkX Graph, GeoDataFrame)
|
|
106
|
+
Returns the processed graph and edge GeoDataFrame
|
|
107
|
+
"""
|
|
108
|
+
# Set default values for optional arguments
|
|
109
|
+
defaults = {
|
|
110
|
+
'network_type': 'walk',
|
|
111
|
+
'vis_graph': True,
|
|
112
|
+
'colormap': 'viridis',
|
|
113
|
+
'vmin': None,
|
|
114
|
+
'vmax': None,
|
|
115
|
+
'edge_width': 1,
|
|
116
|
+
'fig_size': (15,15),
|
|
117
|
+
'zoom': 16,
|
|
118
|
+
'basemap_style': ctx.providers.CartoDB.Positron,
|
|
119
|
+
'save_path': None
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Update defaults with provided kwargs
|
|
123
|
+
settings = defaults.copy()
|
|
124
|
+
settings.update(kwargs)
|
|
125
|
+
|
|
126
|
+
grid_gdf = grid_to_geodataframe(grid, rectangle_vertices, meshsize)
|
|
127
|
+
|
|
128
|
+
# Extract bounding box coordinates
|
|
129
|
+
north, south = rectangle_vertices[1][1], rectangle_vertices[0][1]
|
|
130
|
+
east, west = rectangle_vertices[2][0], rectangle_vertices[0][0]
|
|
131
|
+
bbox = (west, south, east, north)
|
|
132
|
+
|
|
133
|
+
# Download the road network
|
|
134
|
+
G = ox.graph.graph_from_bbox(bbox=bbox, network_type=settings['network_type'], simplify=True)
|
|
135
|
+
|
|
136
|
+
# Calculate edge values using the separate function
|
|
137
|
+
edge_values = calculate_edge_values(G, grid_gdf, "value")
|
|
138
|
+
|
|
139
|
+
# Add values to the graph
|
|
140
|
+
nx.set_edge_attributes(G, edge_values, value_name)
|
|
141
|
+
|
|
142
|
+
# Create GeoDataFrame from edges
|
|
143
|
+
edges_with_values = []
|
|
144
|
+
for u, v, k, data in G.edges(data=True, keys=True):
|
|
145
|
+
if 'geometry' in data:
|
|
146
|
+
edge_line = data['geometry']
|
|
147
|
+
else:
|
|
148
|
+
start_node = G.nodes[u]
|
|
149
|
+
end_node = G.nodes[v]
|
|
150
|
+
edge_line = LineString([(start_node['x'], start_node['y']),
|
|
151
|
+
(end_node['x'], end_node['y'])])
|
|
152
|
+
|
|
153
|
+
edges_with_values.append({
|
|
154
|
+
'geometry': edge_line,
|
|
155
|
+
value_name: data.get(value_name, np.nan),
|
|
156
|
+
'u': u,
|
|
157
|
+
'v': v,
|
|
158
|
+
'key': k
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
edge_gdf = gpd.GeoDataFrame(edges_with_values)
|
|
162
|
+
|
|
163
|
+
# Set CRS and save if requested
|
|
164
|
+
if edge_gdf.crs is None:
|
|
165
|
+
edge_gdf.set_crs(epsg=4326, inplace=True)
|
|
166
|
+
|
|
167
|
+
if settings['save_path']:
|
|
168
|
+
edge_gdf.to_file(settings['save_path'], driver="GPKG")
|
|
169
|
+
|
|
170
|
+
# Visualize if requested
|
|
171
|
+
if settings['vis_graph']:
|
|
172
|
+
edge_gdf_web = edge_gdf.to_crs(epsg=3857)
|
|
173
|
+
|
|
174
|
+
fig, ax = plt.subplots(figsize=settings['fig_size'])
|
|
175
|
+
|
|
176
|
+
plot = edge_gdf_web.plot(column=value_name,
|
|
177
|
+
ax=ax,
|
|
178
|
+
cmap=settings['colormap'],
|
|
179
|
+
legend=True,
|
|
180
|
+
vmin=settings['vmin'],
|
|
181
|
+
vmax=settings['vmax'],
|
|
182
|
+
linewidth=settings['edge_width'],
|
|
183
|
+
legend_kwds={'label': value_name})
|
|
184
|
+
|
|
185
|
+
ctx.add_basemap(ax,
|
|
186
|
+
source=settings['basemap_style'],
|
|
187
|
+
zoom=settings['zoom'])
|
|
188
|
+
|
|
189
|
+
ax.set_axis_off()
|
|
190
|
+
# plt.title(f'Network {value_name} Analysis', pad=20)
|
|
191
|
+
plt.show()
|
|
192
|
+
|
|
193
|
+
return G, edge_gdf
|