voxcity 0.3.9__py3-none-any.whl → 0.3.10__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/file/geojson.py +306 -99
- voxcity/geo/network.py +108 -144
- voxcity/sim/solar.py +12 -7
- voxcity/sim/view.py +5 -3
- voxcity/voxcity.py +23 -20
- {voxcity-0.3.9.dist-info → voxcity-0.3.10.dist-info}/METADATA +4 -4
- {voxcity-0.3.9.dist-info → voxcity-0.3.10.dist-info}/RECORD +11 -11
- {voxcity-0.3.9.dist-info → voxcity-0.3.10.dist-info}/WHEEL +1 -1
- {voxcity-0.3.9.dist-info → voxcity-0.3.10.dist-info}/AUTHORS.rst +0 -0
- {voxcity-0.3.9.dist-info → voxcity-0.3.10.dist-info}/LICENSE +0 -0
- {voxcity-0.3.9.dist-info → voxcity-0.3.10.dist-info}/top_level.txt +0 -0
voxcity/file/geojson.py
CHANGED
|
@@ -187,118 +187,325 @@ def extract_building_heights_from_geojson(geojson_data_0: List[Dict], geojson_da
|
|
|
187
187
|
|
|
188
188
|
return updated_geojson_data_0
|
|
189
189
|
|
|
190
|
-
from typing import List, Dict
|
|
191
|
-
from shapely.geometry import shape
|
|
192
|
-
from shapely.errors import GEOSException
|
|
193
|
-
import numpy as np
|
|
194
|
-
|
|
195
|
-
def complement_building_heights_from_geojson(geojson_data_0: List[Dict], geojson_data_1: List[Dict]) -> List[Dict]:
|
|
196
|
-
|
|
197
|
-
|
|
190
|
+
# from typing import List, Dict
|
|
191
|
+
# from shapely.geometry import shape
|
|
192
|
+
# from shapely.errors import GEOSException
|
|
193
|
+
# import numpy as np
|
|
194
|
+
|
|
195
|
+
# def complement_building_heights_from_geojson(geojson_data_0: List[Dict], geojson_data_1: List[Dict]) -> List[Dict]:
|
|
196
|
+
# """
|
|
197
|
+
# Complement building heights in one GeoJSON dataset with data from another and add non-intersecting buildings.
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
# Args:
|
|
200
|
+
# geojson_data_0 (List[Dict]): Primary GeoJSON features to update with heights
|
|
201
|
+
# geojson_data_1 (List[Dict]): Reference GeoJSON features containing height data
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
203
|
+
# Returns:
|
|
204
|
+
# List[Dict]: Updated GeoJSON features with complemented heights and additional buildings
|
|
205
|
+
# """
|
|
206
|
+
# # Convert primary dataset to Shapely polygons for intersection checking
|
|
207
|
+
# existing_buildings = []
|
|
208
|
+
# for feature in geojson_data_0:
|
|
209
|
+
# geom = shape(feature['geometry'])
|
|
210
|
+
# existing_buildings.append(geom)
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
# # Convert reference dataset to Shapely polygons with height info
|
|
213
|
+
# reference_buildings = []
|
|
214
|
+
# for feature in geojson_data_1:
|
|
215
|
+
# geom = shape(feature['geometry'])
|
|
216
|
+
# height = feature['properties']['height']
|
|
217
|
+
# reference_buildings.append((geom, height, feature))
|
|
218
218
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
# # Initialize counters for statistics
|
|
220
|
+
# count_0 = 0 # Buildings without height
|
|
221
|
+
# count_1 = 0 # Buildings updated with height
|
|
222
|
+
# count_2 = 0 # Buildings with no height data found
|
|
223
|
+
# count_3 = 0 # New non-intersecting buildings added
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
225
|
+
# # Process primary dataset and update heights where needed
|
|
226
|
+
# updated_geojson_data_0 = []
|
|
227
|
+
# for feature in geojson_data_0:
|
|
228
|
+
# geom = shape(feature['geometry'])
|
|
229
|
+
# height = feature['properties']['height']
|
|
230
|
+
# if height == 0:
|
|
231
|
+
# count_0 += 1
|
|
232
|
+
# # Calculate weighted average height based on overlapping areas
|
|
233
|
+
# overlapping_height_area = 0
|
|
234
|
+
# overlapping_area = 0
|
|
235
|
+
# for ref_geom, ref_height, _ in reference_buildings:
|
|
236
|
+
# try:
|
|
237
|
+
# if geom.intersects(ref_geom):
|
|
238
|
+
# overlap_area = geom.intersection(ref_geom).area
|
|
239
|
+
# overlapping_height_area += ref_height * overlap_area
|
|
240
|
+
# overlapping_area += overlap_area
|
|
241
|
+
# except GEOSException as e:
|
|
242
|
+
# # Try to fix invalid geometries
|
|
243
|
+
# try:
|
|
244
|
+
# fixed_ref_geom = ref_geom.buffer(0)
|
|
245
|
+
# if geom.intersects(fixed_ref_geom):
|
|
246
|
+
# overlap_area = geom.intersection(ref_geom).area
|
|
247
|
+
# overlapping_height_area += ref_height * overlap_area
|
|
248
|
+
# overlapping_area += overlap_area
|
|
249
|
+
# except Exception as fix_error:
|
|
250
|
+
# print(f"Failed to fix polygon")
|
|
251
|
+
# continue
|
|
252
252
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
253
|
+
# # Update height if overlapping buildings found
|
|
254
|
+
# if overlapping_height_area > 0:
|
|
255
|
+
# count_1 += 1
|
|
256
|
+
# new_height = overlapping_height_area / overlapping_area
|
|
257
|
+
# feature['properties']['height'] = new_height
|
|
258
|
+
# else:
|
|
259
|
+
# count_2 += 1
|
|
260
|
+
# feature['properties']['height'] = np.nan
|
|
261
261
|
|
|
262
|
-
|
|
262
|
+
# updated_geojson_data_0.append(feature)
|
|
263
263
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
264
|
+
# # Add non-intersecting buildings from reference dataset
|
|
265
|
+
# for ref_geom, ref_height, ref_feature in reference_buildings:
|
|
266
|
+
# has_intersection = False
|
|
267
|
+
# try:
|
|
268
|
+
# # Check if reference building intersects with any existing building
|
|
269
|
+
# for existing_geom in existing_buildings:
|
|
270
|
+
# if ref_geom.intersects(existing_geom):
|
|
271
|
+
# has_intersection = True
|
|
272
|
+
# break
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
# # Add building if it doesn't intersect with any existing ones
|
|
275
|
+
# if not has_intersection:
|
|
276
|
+
# updated_geojson_data_0.append(ref_feature)
|
|
277
|
+
# count_3 += 1
|
|
278
278
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
279
|
+
# except GEOSException as e:
|
|
280
|
+
# # Try to fix invalid geometries
|
|
281
|
+
# try:
|
|
282
|
+
# fixed_ref_geom = ref_geom.buffer(0)
|
|
283
|
+
# for existing_geom in existing_buildings:
|
|
284
|
+
# if fixed_ref_geom.intersects(existing_geom):
|
|
285
|
+
# has_intersection = True
|
|
286
|
+
# break
|
|
287
287
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
# if not has_intersection:
|
|
289
|
+
# updated_geojson_data_0.append(ref_feature)
|
|
290
|
+
# count_3 += 1
|
|
291
|
+
# except Exception as fix_error:
|
|
292
|
+
# print(f"Failed to process non-intersecting building")
|
|
293
|
+
# continue
|
|
294
294
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
295
|
+
# # Print statistics about updates
|
|
296
|
+
# if count_0 > 0:
|
|
297
|
+
# print(f"{count_0} of the total {len(geojson_data_0)} building footprint from base source did not have height data.")
|
|
298
|
+
# print(f"For {count_1} of these building footprints without height, values from complement source were assigned.")
|
|
299
|
+
# print(f"{count_3} non-intersecting buildings from Microsoft Building Footprints were added to the output.")
|
|
300
300
|
|
|
301
|
-
|
|
301
|
+
# return updated_geojson_data_0
|
|
302
|
+
|
|
303
|
+
import numpy as np
|
|
304
|
+
import geopandas as gpd
|
|
305
|
+
import pandas as pd
|
|
306
|
+
from shapely.geometry import shape
|
|
307
|
+
from shapely.errors import GEOSException
|
|
308
|
+
|
|
309
|
+
def geojson_to_gdf(geojson_data, id_col='id'):
|
|
310
|
+
"""
|
|
311
|
+
Convert a list of GeoJSON-like dict features into a GeoDataFrame.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
geojson_data (List[Dict]): A list of feature dicts (Fiona-like).
|
|
315
|
+
id_col (str): Name of property to use as an identifier. If not found,
|
|
316
|
+
we'll try to create a unique ID.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
gpd.GeoDataFrame: GeoDataFrame with geometry and property columns.
|
|
320
|
+
"""
|
|
321
|
+
# Build lists for geometry and properties
|
|
322
|
+
geometries = []
|
|
323
|
+
all_props = []
|
|
324
|
+
|
|
325
|
+
for i, feature in enumerate(geojson_data):
|
|
326
|
+
# Extract geometry
|
|
327
|
+
geom = feature.get('geometry')
|
|
328
|
+
shapely_geom = shape(geom) if geom else None
|
|
329
|
+
|
|
330
|
+
# Extract properties
|
|
331
|
+
props = feature.get('properties', {})
|
|
332
|
+
|
|
333
|
+
# If an ID column is missing, create one
|
|
334
|
+
if id_col not in props:
|
|
335
|
+
props[id_col] = i # fallback ID
|
|
336
|
+
|
|
337
|
+
# Capture geometry and all props
|
|
338
|
+
geometries.append(shapely_geom)
|
|
339
|
+
all_props.append(props)
|
|
340
|
+
|
|
341
|
+
gdf = gpd.GeoDataFrame(all_props, geometry=geometries, crs="EPSG:4326")
|
|
342
|
+
return gdf
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def complement_building_heights_gdf(geojson_data_0, geojson_data_1,
|
|
346
|
+
primary_id='id', ref_id='id'):
|
|
347
|
+
"""
|
|
348
|
+
Use a vectorized approach with GeoPandas to:
|
|
349
|
+
1) Convert both datasets to GeoDataFrames
|
|
350
|
+
2) Find intersections and compute weighted average heights
|
|
351
|
+
3) Update heights in the primary dataset
|
|
352
|
+
4) Add non-intersecting buildings from the reference dataset
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
geojson_data_0 (List[Dict]): Primary GeoJSON-like features
|
|
356
|
+
geojson_data_1 (List[Dict]): Reference GeoJSON-like features
|
|
357
|
+
primary_id (str): Name of the unique identifier in primary dataset's properties
|
|
358
|
+
ref_id (str): Name of the unique identifier in reference dataset's properties
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
gpd.GeoDataFrame: Updated GeoDataFrame (including new buildings).
|
|
362
|
+
You can convert it back to a list of dict features if needed.
|
|
363
|
+
"""
|
|
364
|
+
# ----------------------------------------------------------------
|
|
365
|
+
# 1) Convert primary and reference data to GeoDataFrames
|
|
366
|
+
# ----------------------------------------------------------------
|
|
367
|
+
gdf_primary = geojson_to_gdf(geojson_data_0, id_col=primary_id)
|
|
368
|
+
gdf_ref = geojson_to_gdf(geojson_data_1, id_col=ref_id)
|
|
369
|
+
|
|
370
|
+
# Ensure both are in the same CRS, e.g. EPSG:4326 or some projected CRS
|
|
371
|
+
# If needed, do something like:
|
|
372
|
+
# gdf_primary = gdf_primary.to_crs("EPSG:xxxx")
|
|
373
|
+
# gdf_ref = gdf_ref.to_crs("EPSG:xxxx")
|
|
374
|
+
|
|
375
|
+
# Make sure height columns exist
|
|
376
|
+
if 'height' not in gdf_primary.columns:
|
|
377
|
+
gdf_primary['height'] = 0.0
|
|
378
|
+
if 'height' not in gdf_ref.columns:
|
|
379
|
+
gdf_ref['height'] = 0.0
|
|
380
|
+
|
|
381
|
+
# ----------------------------------------------------------------
|
|
382
|
+
# 2) Intersection to compute areas for overlapping buildings
|
|
383
|
+
# ----------------------------------------------------------------
|
|
384
|
+
# We'll rename columns to avoid collision after overlay
|
|
385
|
+
gdf_primary = gdf_primary.rename(columns={'height': 'height_primary'})
|
|
386
|
+
gdf_ref = gdf_ref.rename(columns={'height': 'height_ref'})
|
|
387
|
+
|
|
388
|
+
# We perform an 'intersection' overlay to get the overlapping polygons
|
|
389
|
+
intersect_gdf = gpd.overlay(gdf_primary, gdf_ref, how='intersection')
|
|
390
|
+
|
|
391
|
+
# Compute intersection area
|
|
392
|
+
intersect_gdf['intersect_area'] = intersect_gdf.area
|
|
393
|
+
# Weighted area (height_ref * intersect_area)
|
|
394
|
+
intersect_gdf['height_area'] = intersect_gdf['height_ref'] * intersect_gdf['intersect_area']
|
|
395
|
+
|
|
396
|
+
# ----------------------------------------------------------------
|
|
397
|
+
# 3) Aggregate to get weighted average height for each primary building
|
|
398
|
+
# ----------------------------------------------------------------
|
|
399
|
+
# We group by the primary building ID, summing up the area and the 'height_area'
|
|
400
|
+
group_cols = {
|
|
401
|
+
'height_area': 'sum',
|
|
402
|
+
'intersect_area': 'sum'
|
|
403
|
+
}
|
|
404
|
+
grouped = intersect_gdf.groupby(gdf_primary[primary_id].name).agg(group_cols)
|
|
405
|
+
|
|
406
|
+
# Weighted average
|
|
407
|
+
grouped['weighted_height'] = grouped['height_area'] / grouped['intersect_area']
|
|
408
|
+
|
|
409
|
+
# ----------------------------------------------------------------
|
|
410
|
+
# 4) Merge aggregated results back to the primary GDF
|
|
411
|
+
# ----------------------------------------------------------------
|
|
412
|
+
# After merging, the primary GDF will have a column 'weighted_height'
|
|
413
|
+
gdf_primary = gdf_primary.merge(grouped['weighted_height'],
|
|
414
|
+
left_on=primary_id,
|
|
415
|
+
right_index=True,
|
|
416
|
+
how='left')
|
|
417
|
+
|
|
418
|
+
# Where primary had zero or missing height, we assign the new weighted height
|
|
419
|
+
zero_or_nan_mask = (gdf_primary['height_primary'] == 0) | (gdf_primary['height_primary'].isna())
|
|
420
|
+
gdf_primary.loc[zero_or_nan_mask, 'height_primary'] = gdf_primary.loc[zero_or_nan_mask, 'weighted_height']
|
|
421
|
+
|
|
422
|
+
# For any building that had no overlap, 'weighted_height' might be NaN.
|
|
423
|
+
# Keep it as NaN or set to 0 if you prefer:
|
|
424
|
+
gdf_primary['height_primary'] = gdf_primary['height_primary'].fillna(np.nan)
|
|
425
|
+
|
|
426
|
+
# ----------------------------------------------------------------
|
|
427
|
+
# 5) Identify reference buildings that do not intersect any primary building
|
|
428
|
+
# ----------------------------------------------------------------
|
|
429
|
+
# Another overlay or spatial join can do this:
|
|
430
|
+
# Option A: use 'difference' on reference to get non-overlapping parts, but that can chop polygons.
|
|
431
|
+
# Option B: check building-level intersection. We'll do a bounding test with sjoin.
|
|
432
|
+
|
|
433
|
+
# For building-level intersection, do a left join of ref onto primary.
|
|
434
|
+
# Then we'll identify which reference IDs are missing from the intersection result.
|
|
435
|
+
sjoin_gdf = gpd.sjoin(gdf_ref, gdf_primary, how='left', op='intersects')
|
|
436
|
+
|
|
437
|
+
# All reference buildings that did not intersect any primary building
|
|
438
|
+
non_intersect_ids = sjoin_gdf.loc[sjoin_gdf[primary_id].isna(), ref_id].unique()
|
|
439
|
+
|
|
440
|
+
# Extract them from the original reference GDF
|
|
441
|
+
gdf_ref_non_intersect = gdf_ref[gdf_ref[ref_id].isin(non_intersect_ids)]
|
|
442
|
+
|
|
443
|
+
# We'll rename columns back to 'height' to be consistent
|
|
444
|
+
gdf_ref_non_intersect = gdf_ref_non_intersect.rename(columns={'height_ref': 'height'})
|
|
445
|
+
|
|
446
|
+
# Also rename any other properties you prefer. For clarity, keep an ID so you know they came from reference.
|
|
447
|
+
|
|
448
|
+
# ----------------------------------------------------------------
|
|
449
|
+
# 6) Combine the updated primary GDF with the new reference buildings
|
|
450
|
+
# ----------------------------------------------------------------
|
|
451
|
+
# First, rename columns in updated primary GDF
|
|
452
|
+
gdf_primary = gdf_primary.rename(columns={'height_primary': 'height'})
|
|
453
|
+
# Drop the 'weighted_height' column to clean up
|
|
454
|
+
if 'weighted_height' in gdf_primary.columns:
|
|
455
|
+
gdf_primary.drop(columns='weighted_height', inplace=True)
|
|
456
|
+
|
|
457
|
+
# Concatenate
|
|
458
|
+
final_gdf = pd.concat([gdf_primary, gdf_ref_non_intersect], ignore_index=True)
|
|
459
|
+
|
|
460
|
+
# ----------------------------------------------------------------
|
|
461
|
+
# Return the combined GeoDataFrame
|
|
462
|
+
# (You can convert it back to a list of GeoJSON-like dictionaries)
|
|
463
|
+
# ----------------------------------------------------------------
|
|
464
|
+
return final_gdf
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def gdf_to_geojson_dicts(gdf, id_col='id'):
|
|
468
|
+
"""
|
|
469
|
+
Convert a GeoDataFrame to a list of dicts similar to GeoJSON features.
|
|
470
|
+
"""
|
|
471
|
+
records = gdf.to_dict(orient='records')
|
|
472
|
+
features = []
|
|
473
|
+
for rec in records:
|
|
474
|
+
# geometry is separate
|
|
475
|
+
geom = rec.pop('geometry', None)
|
|
476
|
+
if geom is not None:
|
|
477
|
+
geom = geom.__geo_interface__
|
|
478
|
+
# use or set ID
|
|
479
|
+
feature_id = rec.get(id_col, None)
|
|
480
|
+
props = {k: v for k, v in rec.items() if k != id_col}
|
|
481
|
+
# build GeoJSON-like feature dict
|
|
482
|
+
feature = {
|
|
483
|
+
'type': 'Feature',
|
|
484
|
+
'properties': props,
|
|
485
|
+
'geometry': geom
|
|
486
|
+
}
|
|
487
|
+
features.append(feature)
|
|
488
|
+
|
|
489
|
+
return features
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def complement_building_heights_from_geojson(geojson_data_0, geojson_data_1,
|
|
493
|
+
primary_id='id', ref_id='id'):
|
|
494
|
+
"""
|
|
495
|
+
High-level function that wraps the GeoPandas approach end-to-end.
|
|
496
|
+
Returns a list of GeoJSON-like feature dicts.
|
|
497
|
+
"""
|
|
498
|
+
# 1) Complement building heights using the GeoDataFrame approach
|
|
499
|
+
final_gdf = complement_building_heights_gdf(
|
|
500
|
+
geojson_data_0,
|
|
501
|
+
geojson_data_1,
|
|
502
|
+
primary_id=primary_id,
|
|
503
|
+
ref_id=ref_id
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# 2) Convert back to geojson-like dict format
|
|
507
|
+
updated_features = gdf_to_geojson_dicts(final_gdf, id_col=primary_id)
|
|
508
|
+
return updated_features
|
|
302
509
|
|
|
303
510
|
def load_geojsons_from_multiple_gz(file_paths):
|
|
304
511
|
"""
|
voxcity/geo/network.py
CHANGED
|
@@ -15,103 +15,75 @@ from joblib import Parallel, delayed
|
|
|
15
15
|
|
|
16
16
|
from .grid import grid_to_geodataframe
|
|
17
17
|
|
|
18
|
-
def
|
|
18
|
+
def vectorized_edge_values(G, polygons_gdf, value_col='value'):
|
|
19
19
|
"""
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
20
|
+
Compute average polygon values along each edge by:
|
|
21
|
+
1) Building an Edge GeoDataFrame in linestring form
|
|
22
|
+
2) Using gpd.overlay or sjoin to get their intersection with polygons
|
|
23
|
+
3) Computing length-weighted average
|
|
35
24
|
"""
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
# 1) Build edge GeoDataFrame (EPSG:4326)
|
|
26
|
+
records = []
|
|
27
|
+
for i, (u, v, k, data) in enumerate(G.edges(keys=True, data=True)):
|
|
38
28
|
if 'geometry' in data:
|
|
39
|
-
|
|
29
|
+
edge_geom = data['geometry']
|
|
40
30
|
else:
|
|
41
31
|
start_node = G.nodes[u]
|
|
42
32
|
end_node = G.nodes[v]
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
33
|
+
edge_geom = LineString([(start_node['x'], start_node['y']),
|
|
34
|
+
(end_node['x'], end_node['y'])])
|
|
35
|
+
records.append({
|
|
36
|
+
'edge_id': i, # unique ID for grouping
|
|
37
|
+
'u': u,
|
|
38
|
+
'v': v,
|
|
39
|
+
'k': k,
|
|
40
|
+
'geometry': edge_geom
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
edges_gdf = gpd.GeoDataFrame(records, crs="EPSG:4326")
|
|
44
|
+
if polygons_gdf.crs != edges_gdf.crs:
|
|
45
|
+
polygons_gdf = polygons_gdf.to_crs(edges_gdf.crs)
|
|
46
|
+
|
|
47
|
+
# 2) Use a projected CRS for length calculations
|
|
48
|
+
edges_3857 = edges_gdf.to_crs(epsg=3857)
|
|
49
|
+
polys_3857 = polygons_gdf.to_crs(epsg=3857)
|
|
50
|
+
|
|
51
|
+
# 3) Intersection: lines vs polygons -> lines clipped to polygons
|
|
52
|
+
# gpd.overlay with how='intersection' can yield partial lines
|
|
53
|
+
intersected = gpd.overlay(edges_3857, polys_3857, how='intersection')
|
|
54
|
+
|
|
55
|
+
# Now each row is a geometry representing the intersection segment,
|
|
56
|
+
# with columns from edges + polygons.
|
|
57
|
+
# For lines, 'intersection' yields the line portion inside each polygon.
|
|
58
|
+
# We'll compute the length, then do a length-weighted average of value_col.
|
|
59
|
+
|
|
60
|
+
intersected['seg_length'] = intersected.geometry.length
|
|
61
|
+
# Weighted contribution = seg_length * polygon_value
|
|
62
|
+
intersected['weighted_val'] = intersected['seg_length'] * intersected[value_col]
|
|
63
|
+
|
|
64
|
+
# 4) Group by edge_id
|
|
65
|
+
grouped = intersected.groupby('edge_id')
|
|
66
|
+
results = grouped.apply(
|
|
67
|
+
lambda df: df['weighted_val'].sum() / df['seg_length'].sum()
|
|
68
|
+
if df['seg_length'].sum() > 0 else np.nan
|
|
69
|
+
)
|
|
70
|
+
# results is a Series with index=edge_id
|
|
71
|
+
|
|
72
|
+
# 5) Map results back to edges
|
|
73
|
+
edge_values = {}
|
|
74
|
+
for edge_id, val in results.items():
|
|
75
|
+
rec = edges_gdf.iloc[edge_id]
|
|
76
|
+
edge_values[(rec['u'], rec['v'], rec['k'])] = val
|
|
77
|
+
|
|
70
78
|
return edge_values
|
|
71
79
|
|
|
72
|
-
def get_network_values(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
80
|
+
def get_network_values(
|
|
81
|
+
grid,
|
|
82
|
+
rectangle_vertices,
|
|
83
|
+
meshsize,
|
|
84
|
+
value_name='value',
|
|
85
|
+
**kwargs
|
|
86
|
+
):
|
|
115
87
|
defaults = {
|
|
116
88
|
'network_type': 'walk',
|
|
117
89
|
'vis_graph': True,
|
|
@@ -124,79 +96,71 @@ def get_network_values(grid, rectangle_vertices, meshsize, value_name='value', *
|
|
|
124
96
|
'basemap_style': ctx.providers.CartoDB.Positron,
|
|
125
97
|
'save_path': None
|
|
126
98
|
}
|
|
127
|
-
|
|
128
|
-
# Update defaults with provided kwargs
|
|
129
|
-
settings = defaults.copy()
|
|
130
|
-
settings.update(kwargs)
|
|
99
|
+
settings = {**defaults, **kwargs}
|
|
131
100
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
101
|
+
# Build polygons GDF if needed
|
|
102
|
+
polygons_gdf = (grid if isinstance(grid, gpd.GeoDataFrame)
|
|
103
|
+
else grid_to_geodataframe(grid, rectangle_vertices, meshsize))
|
|
104
|
+
if polygons_gdf.crs is None:
|
|
105
|
+
polygons_gdf.set_crs(epsg=4326, inplace=True)
|
|
106
|
+
|
|
107
|
+
# BBox
|
|
135
108
|
north, south = rectangle_vertices[1][1], rectangle_vertices[0][1]
|
|
136
|
-
east,
|
|
109
|
+
east, west = rectangle_vertices[2][0], rectangle_vertices[0][0]
|
|
137
110
|
bbox = (west, south, east, north)
|
|
138
|
-
|
|
139
|
-
# Download
|
|
140
|
-
G = ox.graph.graph_from_bbox(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
111
|
+
|
|
112
|
+
# Download OSMnx network
|
|
113
|
+
G = ox.graph.graph_from_bbox(
|
|
114
|
+
bbox=bbox,
|
|
115
|
+
network_type=settings['network_type'],
|
|
116
|
+
simplify=True
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Compute edge values with the vectorized function
|
|
120
|
+
edge_values = vectorized_edge_values(G, polygons_gdf, value_col="value")
|
|
121
|
+
nx.set_edge_attributes(G, edge_values, name=value_name)
|
|
122
|
+
|
|
123
|
+
# Build edge GDF
|
|
149
124
|
edges_with_values = []
|
|
150
125
|
for u, v, k, data in G.edges(data=True, keys=True):
|
|
151
126
|
if 'geometry' in data:
|
|
152
|
-
|
|
127
|
+
geom = data['geometry']
|
|
153
128
|
else:
|
|
154
129
|
start_node = G.nodes[u]
|
|
155
130
|
end_node = G.nodes[v]
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
131
|
+
geom = LineString([(start_node['x'], start_node['y']),
|
|
132
|
+
(end_node['x'], end_node['y'])])
|
|
133
|
+
|
|
134
|
+
val = data.get(value_name, np.nan)
|
|
159
135
|
edges_with_values.append({
|
|
160
|
-
'
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
'v': v,
|
|
164
|
-
'key': k
|
|
136
|
+
'u': u, 'v': v, 'key': k,
|
|
137
|
+
'geometry': geom,
|
|
138
|
+
value_name: val
|
|
165
139
|
})
|
|
166
|
-
|
|
167
|
-
edge_gdf = gpd.GeoDataFrame(edges_with_values)
|
|
168
|
-
|
|
169
|
-
#
|
|
170
|
-
if edge_gdf.crs is None:
|
|
171
|
-
edge_gdf.set_crs(epsg=4326, inplace=True)
|
|
172
|
-
|
|
140
|
+
|
|
141
|
+
edge_gdf = gpd.GeoDataFrame(edges_with_values, crs="EPSG:4326")
|
|
142
|
+
|
|
143
|
+
# Save
|
|
173
144
|
if settings['save_path']:
|
|
174
145
|
edge_gdf.to_file(settings['save_path'], driver="GPKG")
|
|
175
|
-
|
|
176
|
-
# Visualize if requested
|
|
146
|
+
|
|
177
147
|
if settings['vis_graph']:
|
|
178
148
|
edge_gdf_web = edge_gdf.to_crs(epsg=3857)
|
|
179
|
-
|
|
180
149
|
fig, ax = plt.subplots(figsize=settings['fig_size'])
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
ctx.add_basemap(ax,
|
|
193
|
-
source=settings['basemap_style'],
|
|
194
|
-
zoom=settings['zoom'])
|
|
195
|
-
|
|
150
|
+
edge_gdf_web.plot(
|
|
151
|
+
column=value_name,
|
|
152
|
+
ax=ax,
|
|
153
|
+
cmap=settings['colormap'],
|
|
154
|
+
legend=True,
|
|
155
|
+
vmin=settings['vmin'],
|
|
156
|
+
vmax=settings['vmax'],
|
|
157
|
+
linewidth=settings['edge_width'],
|
|
158
|
+
legend_kwds={'label': value_name, 'shrink': 0.5}
|
|
159
|
+
)
|
|
160
|
+
ctx.add_basemap(ax, source=settings['basemap_style'], zoom=settings['zoom'])
|
|
196
161
|
ax.set_axis_off()
|
|
197
|
-
# plt.title(f'Network {value_name} Analysis', pad=20)
|
|
198
162
|
plt.show()
|
|
199
|
-
|
|
163
|
+
|
|
200
164
|
return G, edge_gdf
|
|
201
165
|
|
|
202
166
|
# -------------------------------------------------------------------
|
voxcity/sim/solar.py
CHANGED
|
@@ -57,7 +57,7 @@ def compute_direct_solar_irradiance_map_binary(voxel_data, sun_direction, view_p
|
|
|
57
57
|
# Check if current voxel is empty/tree and voxel below is solid
|
|
58
58
|
if voxel_data[x, y, z] in (0, -2) and voxel_data[x, y, z - 1] not in (0, -2):
|
|
59
59
|
# Skip if standing on building/vegetation/water
|
|
60
|
-
if voxel_data[x, y, z - 1] in (
|
|
60
|
+
if (voxel_data[x, y, z - 1] in (7, 8, 9)) or (voxel_data[x, y, z - 1] < 0):
|
|
61
61
|
irradiance_map[x, y] = np.nan
|
|
62
62
|
found_observer = True
|
|
63
63
|
break
|
|
@@ -148,9 +148,10 @@ def get_direct_solar_irradiance_map(voxel_data, meshsize, azimuth_degrees_ori, e
|
|
|
148
148
|
cmap = plt.cm.get_cmap(colormap).copy()
|
|
149
149
|
cmap.set_bad(color='lightgray')
|
|
150
150
|
plt.figure(figsize=(10, 8))
|
|
151
|
-
plt.title("Horizontal Direct Solar Irradiance Map (0° = North)")
|
|
151
|
+
# plt.title("Horizontal Direct Solar Irradiance Map (0° = North)")
|
|
152
152
|
plt.imshow(direct_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
153
153
|
plt.colorbar(label='Direct Solar Irradiance (W/m²)')
|
|
154
|
+
plt.axis('off')
|
|
154
155
|
plt.show()
|
|
155
156
|
|
|
156
157
|
# Optional OBJ export
|
|
@@ -231,9 +232,10 @@ def get_diffuse_solar_irradiance_map(voxel_data, meshsize, diffuse_irradiance=1.
|
|
|
231
232
|
cmap = plt.cm.get_cmap(colormap).copy()
|
|
232
233
|
cmap.set_bad(color='lightgray')
|
|
233
234
|
plt.figure(figsize=(10, 8))
|
|
234
|
-
plt.title("Diffuse Solar Irradiance Map")
|
|
235
|
+
# plt.title("Diffuse Solar Irradiance Map")
|
|
235
236
|
plt.imshow(diffuse_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
236
237
|
plt.colorbar(label='Diffuse Solar Irradiance (W/m²)')
|
|
238
|
+
plt.axis('off')
|
|
237
239
|
plt.show()
|
|
238
240
|
|
|
239
241
|
# Optional OBJ export
|
|
@@ -310,7 +312,7 @@ def get_global_solar_irradiance_map(
|
|
|
310
312
|
# Create kwargs for diffuse calculation
|
|
311
313
|
direct_diffuse_kwargs = kwargs.copy()
|
|
312
314
|
direct_diffuse_kwargs.update({
|
|
313
|
-
'show_plot':
|
|
315
|
+
'show_plot': True,
|
|
314
316
|
'obj_export': False
|
|
315
317
|
})
|
|
316
318
|
|
|
@@ -343,9 +345,10 @@ def get_global_solar_irradiance_map(
|
|
|
343
345
|
cmap = plt.cm.get_cmap(colormap).copy()
|
|
344
346
|
cmap.set_bad(color='lightgray')
|
|
345
347
|
plt.figure(figsize=(10, 8))
|
|
346
|
-
plt.title("Global Solar Irradiance Map")
|
|
348
|
+
# plt.title("Global Solar Irradiance Map")
|
|
347
349
|
plt.imshow(global_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
348
350
|
plt.colorbar(label='Global Solar Irradiance (W/m²)')
|
|
351
|
+
plt.axis('off')
|
|
349
352
|
plt.show()
|
|
350
353
|
|
|
351
354
|
# Optional OBJ export
|
|
@@ -571,8 +574,9 @@ def get_cumulative_global_solar_irradiance(
|
|
|
571
574
|
cmap = plt.cm.get_cmap(colormap).copy()
|
|
572
575
|
cmap.set_bad(color='lightgray')
|
|
573
576
|
plt.figure(figsize=(8, 6))
|
|
574
|
-
plt.title(f"Global Solar Irradiance at {time_local.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
577
|
+
# plt.title(f"Global Solar Irradiance at {time_local.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
575
578
|
plt.imshow(global_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
579
|
+
plt.axis('off')
|
|
576
580
|
plt.colorbar(label='Global Solar Irradiance (W/m²)')
|
|
577
581
|
plt.show()
|
|
578
582
|
|
|
@@ -588,9 +592,10 @@ def get_cumulative_global_solar_irradiance(
|
|
|
588
592
|
cmap = plt.cm.get_cmap(colormap).copy()
|
|
589
593
|
cmap.set_bad(color='lightgray')
|
|
590
594
|
plt.figure(figsize=(8, 6))
|
|
591
|
-
plt.title("Cumulative Global Solar Irradiance Map")
|
|
595
|
+
# plt.title("Cumulative Global Solar Irradiance Map")
|
|
592
596
|
plt.imshow(cumulative_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
593
597
|
plt.colorbar(label='Cumulative Global Solar Irradiance (W/m²·hour)')
|
|
598
|
+
plt.axis('off')
|
|
594
599
|
plt.show()
|
|
595
600
|
|
|
596
601
|
# Optional OBJ export
|
voxcity/sim/view.py
CHANGED
|
@@ -413,6 +413,7 @@ def get_view_index(voxel_data, meshsize, mode=None, hit_values=None, inclusion_m
|
|
|
413
413
|
plt.figure(figsize=(10, 8))
|
|
414
414
|
plt.imshow(vi_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
415
415
|
plt.colorbar(label='View Index')
|
|
416
|
+
plt.axis('off')
|
|
416
417
|
plt.show()
|
|
417
418
|
|
|
418
419
|
# Optional OBJ export
|
|
@@ -626,7 +627,7 @@ def compute_visibility_map(voxel_data, landmark_positions, opaque_values, view_h
|
|
|
626
627
|
for z in range(1, nz):
|
|
627
628
|
if voxel_data[x, y, z] == 0 and voxel_data[x, y, z - 1] != 0:
|
|
628
629
|
# Skip if standing on building or vegetation
|
|
629
|
-
if voxel_data[x, y, z - 1] in (
|
|
630
|
+
if (voxel_data[x, y, z - 1] in (7, 8, 9)) or (voxel_data[x, y, z - 1] < 0):
|
|
630
631
|
visibility_map[x, y] = np.nan
|
|
631
632
|
found_observer = True
|
|
632
633
|
break
|
|
@@ -696,7 +697,7 @@ def compute_landmark_visibility(voxel_data, target_value=-30, view_height_voxel=
|
|
|
696
697
|
plt.legend(handles=[visible_patch, not_visible_patch],
|
|
697
698
|
loc='center left',
|
|
698
699
|
bbox_to_anchor=(1.0, 0.5))
|
|
699
|
-
|
|
700
|
+
plt.axis('off')
|
|
700
701
|
plt.show()
|
|
701
702
|
|
|
702
703
|
return np.flipud(visibility_map)
|
|
@@ -852,9 +853,10 @@ def get_sky_view_factor_map(voxel_data, meshsize, show_plot=False, **kwargs):
|
|
|
852
853
|
cmap = plt.cm.get_cmap(colormap).copy()
|
|
853
854
|
cmap.set_bad(color='lightgray')
|
|
854
855
|
plt.figure(figsize=(10, 8))
|
|
855
|
-
plt.title("Sky View Factor Map")
|
|
856
|
+
# plt.title("Sky View Factor Map")
|
|
856
857
|
plt.imshow(vi_map, origin='lower', cmap=cmap, vmin=vmin, vmax=vmax)
|
|
857
858
|
plt.colorbar(label='Sky View Factor')
|
|
859
|
+
plt.axis('off')
|
|
858
860
|
plt.show()
|
|
859
861
|
|
|
860
862
|
# Optional OBJ export
|
voxcity/voxcity.py
CHANGED
|
@@ -294,28 +294,31 @@ def get_dem_grid(rectangle_vertices, meshsize, source, output_dir, **kwargs):
|
|
|
294
294
|
print("Creating Digital Elevation Model (DEM) grid\n ")
|
|
295
295
|
print(f"Data source: {source}")
|
|
296
296
|
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
if source == "Local file":
|
|
298
|
+
geotiff_path = kwargs["dem_path"]
|
|
299
|
+
else:
|
|
300
|
+
# Initialize Earth Engine for accessing elevation data
|
|
301
|
+
initialize_earth_engine()
|
|
299
302
|
|
|
300
|
-
|
|
303
|
+
geotiff_path = os.path.join(output_dir, "dem.tif")
|
|
301
304
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
305
|
+
# Add buffer around ROI to ensure smooth interpolation at edges
|
|
306
|
+
buffer_distance = 100
|
|
307
|
+
roi = get_roi(rectangle_vertices)
|
|
308
|
+
roi_buffered = roi.buffer(buffer_distance)
|
|
309
|
+
|
|
310
|
+
# Get DEM data
|
|
311
|
+
image = get_dem_image(roi_buffered, source)
|
|
312
|
+
|
|
313
|
+
# Save DEM data with appropriate resolution based on source
|
|
314
|
+
if source in ["England 1m DTM", 'DEM France 1m', 'DEM France 5m', 'AUSTRALIA 5M DEM']:
|
|
315
|
+
save_geotiff(image, geotiff_path, scale=meshsize, region=roi_buffered, crs='EPSG:4326')
|
|
316
|
+
elif source == 'USGS 3DEP 1m':
|
|
317
|
+
scale = max(meshsize, 1.25)
|
|
318
|
+
save_geotiff(image, geotiff_path, scale=scale, region=roi_buffered, crs='EPSG:4326')
|
|
319
|
+
else:
|
|
320
|
+
# Default to 30m resolution for other sources
|
|
321
|
+
save_geotiff(image, geotiff_path, scale=30, region=roi_buffered)
|
|
319
322
|
|
|
320
323
|
# Create DEM grid with optional interpolation method
|
|
321
324
|
dem_interpolation = kwargs.get("dem_interpolation")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: voxcity
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
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>
|
|
@@ -66,7 +66,7 @@ Requires-Dist: ruff; extra == "dev"
|
|
|
66
66
|
|
|
67
67
|
# VoxCity
|
|
68
68
|
|
|
69
|
-
**VoxCity** is a Python package that
|
|
69
|
+
**VoxCity** is a Python package that provides a one-stop solution for grid-based 3D city model generation and urban simulation for cities worldwide. It integrates various geospatial datasets—such as building footprints, land cover, canopy height, and digital elevation models (DEMs)—to generate 2D and 3D representations of urban areas. It can export data in formats compatible with popular simulation tools like ENVI-MET, as well as visualization tools like MagicaVoxel, and supports simulations such as sky view index and green view index calculations.
|
|
70
70
|
|
|
71
71
|
<!-- <p align="center">
|
|
72
72
|
<picture>
|
|
@@ -284,7 +284,7 @@ The generated OBJ files can be opened and rendered in the following 3D visualiza
|
|
|
284
284
|
<img src="https://raw.githubusercontent.com/kunifujiwara/VoxCity/main/images/obj.png" alt="OBJ 3D City Model Rendered in Rhino" width="600">
|
|
285
285
|
</p>
|
|
286
286
|
<p align="center">
|
|
287
|
-
<em>Example Output Exported in OBJ and Rendered in
|
|
287
|
+
<em>Example Output Exported in OBJ and Rendered in Rhino</em>
|
|
288
288
|
</p>
|
|
289
289
|
|
|
290
290
|
#### MagicaVoxel VOX Files:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
voxcity/__init__.py,sha256=HJM0D2Mv9qpk4JdVzt2SRAAk-hA1D_pCO0ezZH9F7KA,248
|
|
2
|
-
voxcity/voxcity.py,sha256=
|
|
2
|
+
voxcity/voxcity.py,sha256=tZOgRnyvw4ZHuuJr_QP3qeen5DxhYOkDglNDtpB0hzY,33664
|
|
3
3
|
voxcity/download/__init__.py,sha256=OgGcGxOXF4tjcEL6DhOnt13DYPTvOigUelp5xIpTqM0,171
|
|
4
4
|
voxcity/download/eubucco.py,sha256=e1JXBuUfBptSDvNznSGckRs5Xgrj_SAFxk445J_o4KY,14854
|
|
5
5
|
voxcity/download/gee.py,sha256=j7jmzp44T3M6j_4DwhU9Y8Y6gqbZo1zFIlduQPc0jvk,14339
|
|
@@ -11,26 +11,26 @@ voxcity/download/overture.py,sha256=daOvsySC2KIcTcMJUSA7XdbMELJuyLAIM2vr1DRLGp0,
|
|
|
11
11
|
voxcity/download/utils.py,sha256=z6MdPxM96FWQVqvZW2Eg5pMewVHVysUP7F6ueeCwMfI,1375
|
|
12
12
|
voxcity/file/__init_.py,sha256=cVyNyE6axEpSd3CT5hGuMOAlOyU1p8lVP4jkF1-0Ad8,94
|
|
13
13
|
voxcity/file/envimet.py,sha256=SPVoSyYTMNyDRDFWsI0YAsIsb6yt_SXZeDUlhyqlEqY,24282
|
|
14
|
-
voxcity/file/geojson.py,sha256=
|
|
14
|
+
voxcity/file/geojson.py,sha256=h6WYr1bdvzC46w6CbjKzO4wMtVrKTSF7SGaOUuUMkCM,33412
|
|
15
15
|
voxcity/file/magicavoxel.py,sha256=Fsv7yGRXeKmp82xcG3rOb0t_HtoqltNq2tHl08xVlqY,7500
|
|
16
16
|
voxcity/file/obj.py,sha256=oW-kPoZj53nfmO9tXP3Wvizq6Kkjh-QQR8UBexRuMiI,21609
|
|
17
17
|
voxcity/geo/__init_.py,sha256=AZYQxK1zY1M_mDT1HmgcdVI86OAtwK7CNo3AOScLHco,88
|
|
18
18
|
voxcity/geo/draw.py,sha256=roljWXyqYdsWYkmb-5_WNxrJrfV5lnAt8uZblCCo_3Q,13555
|
|
19
19
|
voxcity/geo/grid.py,sha256=_MzO-Cu2GhlP9nuCql6f1pfbU2_OAL27aQ_zCj1u_zk,36288
|
|
20
|
-
voxcity/geo/network.py,sha256=
|
|
20
|
+
voxcity/geo/network.py,sha256=P1oaosieFPuWj9QbU9VmZHpoXV3HR61VssZMcUZLYIw,17403
|
|
21
21
|
voxcity/geo/utils.py,sha256=1BRHp-DDeOA8HG8jplY7Eo75G3oXkVGL6DGONL4BA8A,19815
|
|
22
22
|
voxcity/sim/__init_.py,sha256=APdkcdaovj0v_RPOaA4SBvFUKT2RM7Hxuuz3Sux4gCo,65
|
|
23
|
-
voxcity/sim/solar.py,sha256=
|
|
23
|
+
voxcity/sim/solar.py,sha256=wpnDC3BAm4edx5Z-dz2g_5rqnFNi_YKXneHY2RVBk1w,31968
|
|
24
24
|
voxcity/sim/utils.py,sha256=sEYBB2-hLJxTiXQps1_-Fi7t1HN3-1OPOvBCWtgIisA,130
|
|
25
|
-
voxcity/sim/view.py,sha256=
|
|
25
|
+
voxcity/sim/view.py,sha256=FoXovh406hmvL1obaIXi2MyiRnPdXHY9SWxqfqmcJnc,36758
|
|
26
26
|
voxcity/utils/__init_.py,sha256=nLYrj2huBbDBNMqfchCwexGP8Tlt9O_XluVDG7MoFkw,98
|
|
27
27
|
voxcity/utils/lc.py,sha256=RwPd-VY3POV3gTrBhM7TubgGb9MCd3nVah_G8iUEF7k,11562
|
|
28
28
|
voxcity/utils/material.py,sha256=Vt3IID5Ft54HNJcEC4zi31BCPqi_687X3CSp7rXaRVY,5907
|
|
29
29
|
voxcity/utils/visualization.py,sha256=FNBMN0V5IPuAdqvLHnqSGYqNS7jWesg0ZADEtsUtl0A,31925
|
|
30
30
|
voxcity/utils/weather.py,sha256=P6s1y_EstBL1OGP_MR_6u3vr-t6Uawg8uDckJnoI7FI,21482
|
|
31
|
-
voxcity-0.3.
|
|
32
|
-
voxcity-0.3.
|
|
33
|
-
voxcity-0.3.
|
|
34
|
-
voxcity-0.3.
|
|
35
|
-
voxcity-0.3.
|
|
36
|
-
voxcity-0.3.
|
|
31
|
+
voxcity-0.3.10.dist-info/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
|
|
32
|
+
voxcity-0.3.10.dist-info/LICENSE,sha256=-hGliOFiwUrUSoZiB5WF90xXGqinKyqiDI2t6hrnam8,1087
|
|
33
|
+
voxcity-0.3.10.dist-info/METADATA,sha256=Lf4DIH1_hPKyQkxAset4cMlL5dJWwHosXpBgj6Ybta8,25122
|
|
34
|
+
voxcity-0.3.10.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
35
|
+
voxcity-0.3.10.dist-info/top_level.txt,sha256=00b2U-LKfDllt6RL1R33MXie5MvxzUFye0NGD96t_8I,8
|
|
36
|
+
voxcity-0.3.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|