voxcity 0.3.7__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.
- {voxcity-0.3.7 → voxcity-0.3.9}/PKG-INFO +2 -1
- {voxcity-0.3.7 → voxcity-0.3.9}/pyproject.toml +2 -1
- voxcity-0.3.9/src/voxcity/geo/network.py +541 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/sim/solar.py +1 -1
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity.egg-info/PKG-INFO +2 -1
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity.egg-info/requires.txt +1 -0
- voxcity-0.3.7/src/voxcity/geo/network.py +0 -194
- {voxcity-0.3.7 → voxcity-0.3.9}/AUTHORS.rst +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/CONTRIBUTING.rst +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/HISTORY.rst +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/LICENSE +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/MANIFEST.in +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/README.md +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/docs/Makefile +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/docs/archive/README.rst +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/docs/authors.rst +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/docs/conf.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/docs/index.rst +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/docs/make.bat +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/setup.cfg +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/__init__.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/download/__init__.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/download/eubucco.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/download/gee.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/download/mbfp.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/download/oemj.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/download/omt.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/download/osm.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/download/overture.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/download/utils.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/file/__init_.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/file/envimet.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/file/geojson.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/file/magicavoxel.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/file/obj.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/geo/__init_.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/geo/draw.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/geo/grid.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/geo/utils.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/sim/__init_.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/sim/utils.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/sim/view.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/utils/__init_.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/utils/lc.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/utils/material.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/utils/visualization.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/utils/weather.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity/voxcity.py +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity.egg-info/SOURCES.txt +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity.egg-info/dependency_links.txt +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/src/voxcity.egg-info/top_level.txt +0 -0
- {voxcity-0.3.7 → voxcity-0.3.9}/tests/__init__.py +0 -0
- {voxcity-0.3.7 → 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.
|
|
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.
|
|
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
|
|
@@ -750,7 +750,7 @@ def get_global_solar_irradiance_using_epw(
|
|
|
750
750
|
end_hour = kwargs.get("end_hour", 23) # Default to 11 PM
|
|
751
751
|
|
|
752
752
|
# Filter dataframe for specified hours
|
|
753
|
-
df_filtered = df[df.index.hour
|
|
753
|
+
df_filtered = df[(df.index.hour >= start_hour) & (df.index.hour <= end_hour)]
|
|
754
754
|
|
|
755
755
|
solar_map = get_cumulative_global_solar_irradiance(
|
|
756
756
|
voxel_data,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: voxcity
|
|
3
|
-
Version: 0.3.
|
|
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,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
|
|
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
|
|
File without changes
|
|
File without changes
|