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/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, left_wgs84, top_wgs84, right_wgs84 = poly.bounds
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,lon) coordinate pairs defining the rectangle bounds
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[1], vertex_0[0], vertex_1[1], vertex_1[0])
415
- dist_side_2 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_3[1], vertex_3[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, left_wgs84, top_wgs84, right_wgs84 = poly.bounds
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[1], vertex_0[0], vertex_1[1], vertex_1[0])
628
- dist_side_2 = calculate_distance(geod, vertex_0[1], vertex_0[0], vertex_3[1], vertex_3[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