voxcity 0.3.8__tar.gz → 0.3.9__tar.gz

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.

Files changed (53) hide show
  1. {voxcity-0.3.8 → voxcity-0.3.9}/PKG-INFO +2 -1
  2. {voxcity-0.3.8 → voxcity-0.3.9}/pyproject.toml +2 -1
  3. voxcity-0.3.9/src/voxcity/geo/network.py +541 -0
  4. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity.egg-info/PKG-INFO +2 -1
  5. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity.egg-info/requires.txt +1 -0
  6. voxcity-0.3.8/src/voxcity/geo/network.py +0 -194
  7. {voxcity-0.3.8 → voxcity-0.3.9}/AUTHORS.rst +0 -0
  8. {voxcity-0.3.8 → voxcity-0.3.9}/CONTRIBUTING.rst +0 -0
  9. {voxcity-0.3.8 → voxcity-0.3.9}/HISTORY.rst +0 -0
  10. {voxcity-0.3.8 → voxcity-0.3.9}/LICENSE +0 -0
  11. {voxcity-0.3.8 → voxcity-0.3.9}/MANIFEST.in +0 -0
  12. {voxcity-0.3.8 → voxcity-0.3.9}/README.md +0 -0
  13. {voxcity-0.3.8 → voxcity-0.3.9}/docs/Makefile +0 -0
  14. {voxcity-0.3.8 → voxcity-0.3.9}/docs/archive/README.rst +0 -0
  15. {voxcity-0.3.8 → voxcity-0.3.9}/docs/authors.rst +0 -0
  16. {voxcity-0.3.8 → voxcity-0.3.9}/docs/conf.py +0 -0
  17. {voxcity-0.3.8 → voxcity-0.3.9}/docs/index.rst +0 -0
  18. {voxcity-0.3.8 → voxcity-0.3.9}/docs/make.bat +0 -0
  19. {voxcity-0.3.8 → voxcity-0.3.9}/setup.cfg +0 -0
  20. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/__init__.py +0 -0
  21. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/download/__init__.py +0 -0
  22. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/download/eubucco.py +0 -0
  23. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/download/gee.py +0 -0
  24. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/download/mbfp.py +0 -0
  25. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/download/oemj.py +0 -0
  26. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/download/omt.py +0 -0
  27. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/download/osm.py +0 -0
  28. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/download/overture.py +0 -0
  29. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/download/utils.py +0 -0
  30. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/file/__init_.py +0 -0
  31. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/file/envimet.py +0 -0
  32. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/file/geojson.py +0 -0
  33. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/file/magicavoxel.py +0 -0
  34. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/file/obj.py +0 -0
  35. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/geo/__init_.py +0 -0
  36. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/geo/draw.py +0 -0
  37. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/geo/grid.py +0 -0
  38. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/geo/utils.py +0 -0
  39. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/sim/__init_.py +0 -0
  40. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/sim/solar.py +0 -0
  41. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/sim/utils.py +0 -0
  42. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/sim/view.py +0 -0
  43. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/utils/__init_.py +0 -0
  44. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/utils/lc.py +0 -0
  45. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/utils/material.py +0 -0
  46. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/utils/visualization.py +0 -0
  47. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/utils/weather.py +0 -0
  48. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity/voxcity.py +0 -0
  49. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity.egg-info/SOURCES.txt +0 -0
  50. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity.egg-info/dependency_links.txt +0 -0
  51. {voxcity-0.3.8 → voxcity-0.3.9}/src/voxcity.egg-info/top_level.txt +0 -0
  52. {voxcity-0.3.8 → voxcity-0.3.9}/tests/__init__.py +0 -0
  53. {voxcity-0.3.8 → voxcity-0.3.9}/tests/voxelcity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: voxcity
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Summary: voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data
5
5
  Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
6
6
  Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
@@ -49,6 +49,7 @@ Requires-Dist: protobuf==3.20.3
49
49
  Requires-Dist: timezonefinder
50
50
  Requires-Dist: astral
51
51
  Requires-Dist: osmnx
52
+ Requires-Dist: joblib
52
53
  Provides-Extra: dev
53
54
  Requires-Dist: coverage; extra == "dev"
54
55
  Requires-Dist: mypy; extra == "dev"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "voxcity"
3
- version = "0.3.8"
3
+ version = "0.3.9"
4
4
  requires-python = ">=3.10,<3.13"
5
5
  classifiers = [
6
6
  "Programming Language :: Python :: 3.10",
@@ -51,6 +51,7 @@ dependencies = [
51
51
  "timezonefinder",
52
52
  "astral",
53
53
  "osmnx",
54
+ "joblib",
54
55
  ]
55
56
 
56
57
  [project.optional-dependencies]
@@ -0,0 +1,541 @@
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
+ import os
10
+ import shapely
11
+ from shapely.geometry import Point
12
+ from shapely.ops import transform
13
+ from pyproj import Transformer
14
+ from joblib import Parallel, delayed
15
+
16
+ from .grid import grid_to_geodataframe
17
+
18
+ def calculate_edge_values(G, gdf, value_col='value'):
19
+ """
20
+ Calculate average values for graph edges based on intersection with polygons.
21
+
22
+ Parameters:
23
+ -----------
24
+ G : NetworkX Graph
25
+ Input graph with edges to analyze
26
+ gdf : GeoDataFrame
27
+ Grid containing polygons with values
28
+ value_col : str, default 'value'
29
+ Name of the column containing values in the grid
30
+
31
+ Returns:
32
+ --------
33
+ dict
34
+ Dictionary with edge identifiers (u,v,k) as keys and average values as values
35
+ """
36
+ edge_values = {}
37
+ for u, v, k, data in G.edges(data=True, keys=True):
38
+ if 'geometry' in data:
39
+ edge_line = data['geometry']
40
+ else:
41
+ start_node = G.nodes[u]
42
+ end_node = G.nodes[v]
43
+ edge_line = LineString([(start_node['x'], start_node['y']),
44
+ (end_node['x'], end_node['y'])])
45
+
46
+ intersecting_polys = gdf[gdf.geometry.intersects(edge_line)]
47
+
48
+ if len(intersecting_polys) > 0:
49
+ total_length = 0
50
+ weighted_sum = 0
51
+
52
+ for idx, poly in intersecting_polys.iterrows():
53
+ if pd.isna(poly[value_col]):
54
+ continue
55
+
56
+ intersection = edge_line.intersection(poly.geometry)
57
+ if not intersection.is_empty:
58
+ length = intersection.length
59
+ total_length += length
60
+ weighted_sum += length * poly[value_col]
61
+
62
+ if total_length > 0:
63
+ avg_value = weighted_sum / total_length
64
+ edge_values[(u, v, k)] = avg_value
65
+ else:
66
+ edge_values[(u, v, k)] = np.nan
67
+ else:
68
+ edge_values[(u, v, k)] = np.nan
69
+
70
+ return edge_values
71
+
72
+ def get_network_values(grid, rectangle_vertices, meshsize, value_name='value', **kwargs):
73
+ """
74
+ Analyze and visualize network values based on grid intersections.
75
+
76
+ Parameters:
77
+ -----------
78
+ grid : GeoDataFrame
79
+ Input grid with geometries and values
80
+ rectangle_vertices : list
81
+ List of coordinates defining the bounding box vertices
82
+ meshsize : float
83
+ Size of the mesh grid
84
+ value_name : str, default 'value'
85
+ Name of the column containing values in the grid
86
+ **kwargs : dict
87
+ Optional arguments including:
88
+ - network_type : str, default 'walk'
89
+ Type of network to download ('walk', 'drive', 'all', etc.)
90
+ - vis_graph : bool, default True
91
+ Whether to visualize the graph
92
+ - colormap : str, default 'viridis'
93
+ Matplotlib colormap name for visualization
94
+ - vmin : float, optional
95
+ Minimum value for color scaling
96
+ - vmax : float, optional
97
+ Maximum value for color scaling
98
+ - edge_width : float, default 1
99
+ Width of the edges in visualization
100
+ - fig_size : tuple, default (15,15)
101
+ Figure size for visualization
102
+ - zoom : int, default 16
103
+ Zoom level for the basemap
104
+ - basemap_style : ctx.providers, default CartoDB.Positron
105
+ Contextily basemap provider
106
+ - save_path : str, optional
107
+ Path to save the output GeoPackage
108
+
109
+ Returns:
110
+ --------
111
+ tuple : (NetworkX Graph, GeoDataFrame)
112
+ Returns the processed graph and edge GeoDataFrame
113
+ """
114
+ # Set default values for optional arguments
115
+ defaults = {
116
+ 'network_type': 'walk',
117
+ 'vis_graph': True,
118
+ 'colormap': 'viridis',
119
+ 'vmin': None,
120
+ 'vmax': None,
121
+ 'edge_width': 1,
122
+ 'fig_size': (15,15),
123
+ 'zoom': 16,
124
+ 'basemap_style': ctx.providers.CartoDB.Positron,
125
+ 'save_path': None
126
+ }
127
+
128
+ # Update defaults with provided kwargs
129
+ settings = defaults.copy()
130
+ settings.update(kwargs)
131
+
132
+ grid_gdf = grid_to_geodataframe(grid, rectangle_vertices, meshsize)
133
+
134
+ # Extract bounding box coordinates
135
+ north, south = rectangle_vertices[1][1], rectangle_vertices[0][1]
136
+ east, west = rectangle_vertices[2][0], rectangle_vertices[0][0]
137
+ bbox = (west, south, east, north)
138
+
139
+ # Download the road network
140
+ G = ox.graph.graph_from_bbox(bbox=bbox, network_type=settings['network_type'], simplify=True)
141
+
142
+ # Calculate edge values using the separate function
143
+ edge_values = calculate_edge_values(G, grid_gdf, "value")
144
+
145
+ # Add values to the graph
146
+ nx.set_edge_attributes(G, edge_values, value_name)
147
+
148
+ # Create GeoDataFrame from edges
149
+ edges_with_values = []
150
+ for u, v, k, data in G.edges(data=True, keys=True):
151
+ if 'geometry' in data:
152
+ edge_line = data['geometry']
153
+ else:
154
+ start_node = G.nodes[u]
155
+ end_node = G.nodes[v]
156
+ edge_line = LineString([(start_node['x'], start_node['y']),
157
+ (end_node['x'], end_node['y'])])
158
+
159
+ edges_with_values.append({
160
+ 'geometry': edge_line,
161
+ value_name: data.get(value_name, np.nan),
162
+ 'u': u,
163
+ 'v': v,
164
+ 'key': k
165
+ })
166
+
167
+ edge_gdf = gpd.GeoDataFrame(edges_with_values)
168
+
169
+ # Set CRS and save if requested
170
+ if edge_gdf.crs is None:
171
+ edge_gdf.set_crs(epsg=4326, inplace=True)
172
+
173
+ if settings['save_path']:
174
+ edge_gdf.to_file(settings['save_path'], driver="GPKG")
175
+
176
+ # Visualize if requested
177
+ if settings['vis_graph']:
178
+ edge_gdf_web = edge_gdf.to_crs(epsg=3857)
179
+
180
+ fig, ax = plt.subplots(figsize=settings['fig_size'])
181
+
182
+ plot = edge_gdf_web.plot(column=value_name,
183
+ ax=ax,
184
+ cmap=settings['colormap'],
185
+ legend=True,
186
+ vmin=settings['vmin'],
187
+ vmax=settings['vmax'],
188
+ linewidth=settings['edge_width'],
189
+ legend_kwds={'label': value_name,
190
+ 'shrink': 0.5}) # Make colorbar 50% smaller
191
+
192
+ ctx.add_basemap(ax,
193
+ source=settings['basemap_style'],
194
+ zoom=settings['zoom'])
195
+
196
+ ax.set_axis_off()
197
+ # plt.title(f'Network {value_name} Analysis', pad=20)
198
+ plt.show()
199
+
200
+ return G, edge_gdf
201
+
202
+ # -------------------------------------------------------------------
203
+ # Optionally import your DEM helper
204
+ # -------------------------------------------------------------------
205
+ from voxcity.geo.grid import grid_to_geodataframe
206
+
207
+ # -------------------------------------------------------------------
208
+ # 1) Functions for interpolation, parallelization, and slope
209
+ # -------------------------------------------------------------------
210
+
211
+ def interpolate_points_along_line(line, interval):
212
+ """
213
+ Interpolate points along a single LineString at a given interval (in meters).
214
+ If the line is shorter than `interval`, only start/end points are returned.
215
+
216
+ Parameters
217
+ ----------
218
+ line : shapely.geometry.LineString
219
+ Edge geometry in EPSG:4326 (lon/lat).
220
+ interval : float
221
+ Distance in meters between interpolated points.
222
+
223
+ Returns
224
+ -------
225
+ list of shapely.geometry.Point
226
+ Points in EPSG:4326 along the line.
227
+ """
228
+ if line.is_empty:
229
+ return []
230
+
231
+ # Transformers for metric distance calculations
232
+ project = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True).transform
233
+ project_rev = Transformer.from_crs("EPSG:3857", "EPSG:4326", always_xy=True).transform
234
+
235
+ # Project line to Web Mercator
236
+ line_merc = shapely.ops.transform(project, line)
237
+ length_m = line_merc.length
238
+ if length_m == 0:
239
+ return [Point(line.coords[0])]
240
+
241
+ # If line is shorter than interval, just start & end
242
+ if length_m < interval:
243
+ return [Point(line.coords[0]), Point(line.coords[-1])]
244
+
245
+ # Otherwise, create distances
246
+ num_points = int(length_m // interval)
247
+ dists = [i * interval for i in range(num_points + 1)]
248
+ # Ensure end
249
+ if dists[-1] < length_m:
250
+ dists.append(length_m)
251
+
252
+ # Interpolate
253
+ points_merc = [line_merc.interpolate(d) for d in dists]
254
+ # Reproject back
255
+ return [shapely.ops.transform(project_rev, pt) for pt in points_merc]
256
+
257
+
258
+ def gather_interpolation_points(G, interval=10.0, n_jobs=1):
259
+ """
260
+ Gather all interpolation points for each edge in the graph into a single GeoDataFrame.
261
+ Can be parallelized with `n_jobs`.
262
+
263
+ Parameters
264
+ ----------
265
+ G : networkx.MultiDiGraph
266
+ OSMnx graph with 'geometry' attributes or x,y coordinates in the nodes.
267
+ interval : float, default=10.0
268
+ Interpolation distance interval in meters.
269
+ n_jobs : int, default=1
270
+ Number of parallel jobs (1 => no parallelization).
271
+
272
+ Returns
273
+ -------
274
+ gpd.GeoDataFrame
275
+ Columns: edge_id, index_in_edge, geometry (EPSG:4326).
276
+ """
277
+ edges = list(G.edges(keys=True, data=True))
278
+
279
+ def process_edge(u, v, k, data, idx):
280
+ if 'geometry' in data:
281
+ line = data['geometry']
282
+ else:
283
+ # If no geometry, build from node coords
284
+ start_node = G.nodes[u]
285
+ end_node = G.nodes[v]
286
+ line = LineString([(start_node['x'], start_node['y']),
287
+ (end_node['x'], end_node['y'])])
288
+
289
+ pts = interpolate_points_along_line(line, interval)
290
+ df = pd.DataFrame({
291
+ 'edge_id': [idx]*len(pts),
292
+ 'index_in_edge': np.arange(len(pts)),
293
+ 'geometry': pts
294
+ })
295
+ return df
296
+
297
+ # Parallel interpolation
298
+ results = Parallel(n_jobs=n_jobs, backend='threading')(
299
+ delayed(process_edge)(u, v, k, data, i)
300
+ for i, (u, v, k, data) in enumerate(edges)
301
+ )
302
+
303
+ all_points_df = pd.concat(results, ignore_index=True)
304
+ points_gdf = gpd.GeoDataFrame(all_points_df, geometry='geometry', crs="EPSG:4326")
305
+ return points_gdf
306
+
307
+
308
+ def fetch_elevations_for_points(points_gdf_3857, dem_gdf_3857, elevation_col='value'):
309
+ """
310
+ Do a spatial join (nearest) in a projected CRS (EPSG:3857) to fetch DEM elevations.
311
+
312
+ Parameters
313
+ ----------
314
+ points_gdf_3857 : gpd.GeoDataFrame
315
+ Interpolation points in EPSG:3857.
316
+ dem_gdf_3857 : gpd.GeoDataFrame
317
+ DEM polygons in EPSG:3857, must have `elevation_col`.
318
+ elevation_col : str, default='value'
319
+ Column with elevation values in dem_gdf_3857.
320
+
321
+ Returns
322
+ -------
323
+ gpd.GeoDataFrame
324
+ A copy of points_gdf_3857 with new column 'elevation'.
325
+ """
326
+ joined = gpd.sjoin_nearest(
327
+ points_gdf_3857,
328
+ dem_gdf_3857[[elevation_col, 'geometry']].copy(),
329
+ how='left',
330
+ distance_col='dist_to_poly'
331
+ )
332
+ joined.rename(columns={elevation_col: 'elevation'}, inplace=True)
333
+ return joined
334
+
335
+
336
+ def compute_slope_for_group(df):
337
+ """
338
+ Given a subset of points for a single edge, compute average slope between
339
+ consecutive points, using columns: geometry, elevation, index_in_edge.
340
+
341
+ Note: We assume df is already in EPSG:3857 for direct distance calculations.
342
+ """
343
+ # Sort by position along the edge
344
+ df = df.sort_values("index_in_edge")
345
+
346
+ # Coordinates
347
+ xs = df.geometry.x.to_numpy()
348
+ ys = df.geometry.y.to_numpy()
349
+ elevs = df["elevation"].to_numpy()
350
+
351
+ # Differences
352
+ dx = np.diff(xs)
353
+ dy = np.diff(ys)
354
+ horizontal_dist = np.sqrt(dx**2 + dy**2)
355
+ elev_diff = np.diff(elevs)
356
+
357
+ # Slope in %
358
+ valid_mask = horizontal_dist > 0
359
+ slopes = (np.abs(elev_diff[valid_mask]) / horizontal_dist[valid_mask]) * 100
360
+
361
+ if len(slopes) == 0:
362
+ return np.nan
363
+ return slopes.mean()
364
+
365
+
366
+ def calculate_edge_slopes_from_join(joined_points_gdf, n_edges):
367
+ """
368
+ Calculate average slopes for each edge by grouping joined points.
369
+
370
+ Parameters
371
+ ----------
372
+ joined_points_gdf : gpd.GeoDataFrame
373
+ Must have columns: edge_id, index_in_edge, elevation, geometry (EPSG:3857).
374
+ n_edges : int
375
+ Number of edges from the graph.
376
+
377
+ Returns
378
+ -------
379
+ dict
380
+ edge_id -> average slope (in %).
381
+ """
382
+ # We'll group by edge_id, ignoring the group columns in apply (pandas >= 2.1).
383
+ # If your pandas version < 2.1, just do a column subset after groupby.
384
+ # E.g. .groupby("edge_id", group_keys=False)[["geometry","elevation","index_in_edge"]]...
385
+ grouped = joined_points_gdf.groupby("edge_id", group_keys=False)
386
+ results = grouped[["geometry", "elevation", "index_in_edge"]].apply(compute_slope_for_group)
387
+
388
+ # Convert series -> dict
389
+ slope_dict = results.to_dict()
390
+
391
+ # Fill any missing edge IDs with NaN
392
+ for i in range(n_edges):
393
+ if i not in slope_dict:
394
+ slope_dict[i] = np.nan
395
+
396
+ return slope_dict
397
+
398
+ # -------------------------------------------------------------------
399
+ # 2) Main function to analyze network slopes
400
+ # -------------------------------------------------------------------
401
+
402
+ def analyze_network_slopes(
403
+ dem_grid,
404
+ meshsize,
405
+ value_name='slope',
406
+ interval=10.0,
407
+ n_jobs=1,
408
+ **kwargs
409
+ ):
410
+ """
411
+ Analyze and visualize network slopes based on DEM data, using vectorized + parallel methods.
412
+
413
+ Parameters
414
+ ----------
415
+ dem_grid : array-like
416
+ DEM grid data.
417
+ meshsize : float
418
+ Mesh grid size.
419
+ value_name : str, default='slope'
420
+ Column name for slopes assigned to each edge.
421
+ interval : float, default=10.0
422
+ Interpolation distance in meters.
423
+ n_jobs : int, default=1
424
+ Parallelization for edge interpolation (1 => sequential).
425
+ **kwargs : dict
426
+ Additional parameters:
427
+ - rectangle_vertices : list of (x, y) in EPSG:4326
428
+ - network_type : str, default='walk'
429
+ - vis_graph : bool, default=True
430
+ - colormap, vmin, vmax, edge_width, fig_size, zoom, basemap_style, alpha
431
+ - output_directory, output_file_name
432
+ """
433
+ defaults = {
434
+ 'rectangle_vertices': None,
435
+ 'network_type': 'walk',
436
+ 'vis_graph': True,
437
+ 'colormap': 'viridis',
438
+ 'vmin': None,
439
+ 'vmax': None,
440
+ 'edge_width': 1,
441
+ 'fig_size': (15, 15),
442
+ 'zoom': 16,
443
+ 'basemap_style': ctx.providers.CartoDB.Positron,
444
+ 'output_directory': None,
445
+ 'output_file_name': 'network_slopes',
446
+ 'alpha': 1.0
447
+ }
448
+ settings = {**defaults, **kwargs}
449
+
450
+ # Validate bounding box
451
+ if settings['rectangle_vertices'] is None:
452
+ raise ValueError("Must supply 'rectangle_vertices' in kwargs.")
453
+
454
+ # 1) Build DEM GeoDataFrame in EPSG:4326
455
+ dem_gdf = grid_to_geodataframe(dem_grid, settings['rectangle_vertices'], meshsize)
456
+ if dem_gdf.crs is None:
457
+ dem_gdf.set_crs(epsg=4326, inplace=True)
458
+
459
+ # 2) Download bounding box from rectangle_vertices
460
+ north, south = settings['rectangle_vertices'][1][1], settings['rectangle_vertices'][0][1]
461
+ east, west = settings['rectangle_vertices'][2][0], settings['rectangle_vertices'][0][0]
462
+ bbox = (west, south, east, north)
463
+
464
+ G = ox.graph.graph_from_bbox(
465
+ bbox=bbox,
466
+ network_type=settings['network_type'],
467
+ simplify=True
468
+ )
469
+
470
+ # 3) Interpolate points along edges (EPSG:4326)
471
+ points_gdf_4326 = gather_interpolation_points(G, interval=interval, n_jobs=n_jobs)
472
+
473
+ # 4) Reproject DEM + Points to EPSG:3857 for correct distance operations
474
+ dem_gdf_3857 = dem_gdf.to_crs(epsg=3857)
475
+ points_gdf_3857 = points_gdf_4326.to_crs(epsg=3857)
476
+
477
+ # 5) Perform spatial join to get elevations
478
+ joined_points_3857 = fetch_elevations_for_points(points_gdf_3857, dem_gdf_3857, elevation_col='value')
479
+
480
+ # 6) Compute slopes for each edge
481
+ n_edges = len(list(G.edges(keys=True)))
482
+ slope_dict = calculate_edge_slopes_from_join(joined_points_3857, n_edges)
483
+
484
+ # 7) Assign slopes back to G
485
+ edges = list(G.edges(keys=True, data=True))
486
+ edge_slopes = {}
487
+ for i, (u, v, k, data) in enumerate(edges):
488
+ edge_slopes[(u, v, k)] = slope_dict.get(i, np.nan)
489
+ nx.set_edge_attributes(G, edge_slopes, name=value_name)
490
+
491
+ # 8) Build an edge GeoDataFrame in EPSG:4326
492
+ edges_with_values = []
493
+ for (u, v, k, data), edge_id in zip(edges, range(len(edges))):
494
+ if 'geometry' in data:
495
+ geom = data['geometry']
496
+ else:
497
+ start_node = G.nodes[u]
498
+ end_node = G.nodes[v]
499
+ geom = LineString([(start_node['x'], start_node['y']),
500
+ (end_node['x'], end_node['y'])])
501
+
502
+ edges_with_values.append({
503
+ 'u': u,
504
+ 'v': v,
505
+ 'key': k,
506
+ 'geometry': geom,
507
+ value_name: slope_dict.get(edge_id, np.nan)
508
+ })
509
+
510
+ edge_gdf = gpd.GeoDataFrame(edges_with_values, crs="EPSG:4326")
511
+
512
+ # 9) Save output if requested
513
+ if settings['output_directory']:
514
+ os.makedirs(settings['output_directory'], exist_ok=True)
515
+ out_path = os.path.join(
516
+ settings['output_directory'],
517
+ f"{settings['output_file_name']}.gpkg"
518
+ )
519
+ edge_gdf.to_file(out_path, driver="GPKG")
520
+
521
+ # 10) Visualization
522
+ if settings['vis_graph']:
523
+ edge_gdf_web = edge_gdf.to_crs(epsg=3857)
524
+ fig, ax = plt.subplots(figsize=settings['fig_size'])
525
+ edge_gdf_web.plot(
526
+ column=value_name,
527
+ ax=ax,
528
+ cmap=settings['colormap'],
529
+ legend=True,
530
+ vmin=settings['vmin'],
531
+ vmax=settings['vmax'],
532
+ linewidth=settings['edge_width'],
533
+ alpha=settings['alpha'],
534
+ legend_kwds={'label': f"{value_name} (%)"}
535
+ )
536
+ ctx.add_basemap(ax, source=settings['basemap_style'], zoom=settings['zoom'])
537
+ ax.set_axis_off()
538
+ plt.title(f'Network {value_name} Analysis', pad=20)
539
+ plt.show()
540
+
541
+ return G, edge_gdf
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: voxcity
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Summary: voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data
5
5
  Author-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
6
6
  Maintainer-email: Kunihiko Fujiwara <kunihiko@nus.edu.sg>
@@ -49,6 +49,7 @@ Requires-Dist: protobuf==3.20.3
49
49
  Requires-Dist: timezonefinder
50
50
  Requires-Dist: astral
51
51
  Requires-Dist: osmnx
52
+ Requires-Dist: joblib
52
53
  Provides-Extra: dev
53
54
  Requires-Dist: coverage; extra == "dev"
54
55
  Requires-Dist: mypy; extra == "dev"
@@ -32,6 +32,7 @@ protobuf==3.20.3
32
32
  timezonefinder
33
33
  astral
34
34
  osmnx
35
+ joblib
35
36
 
36
37
  [dev]
37
38
  coverage
@@ -1,194 +0,0 @@
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
- 'shrink': 0.5}) # Make colorbar 50% smaller
185
-
186
- ctx.add_basemap(ax,
187
- source=settings['basemap_style'],
188
- zoom=settings['zoom'])
189
-
190
- ax.set_axis_off()
191
- # plt.title(f'Network {value_name} Analysis', pad=20)
192
- plt.show()
193
-
194
- return G, edge_gdf
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes