voxcity 0.6.16__tar.gz → 0.6.17__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 (37) hide show
  1. {voxcity-0.6.16 → voxcity-0.6.17}/PKG-INFO +1 -1
  2. {voxcity-0.6.16 → voxcity-0.6.17}/pyproject.toml +1 -1
  3. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/downloader/osm.py +23 -7
  4. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/downloader/overture.py +26 -1
  5. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/generator.py +102 -7
  6. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/geoprocessor/grid.py +1738 -1732
  7. {voxcity-0.6.16 → voxcity-0.6.17}/AUTHORS.rst +0 -0
  8. {voxcity-0.6.16 → voxcity-0.6.17}/LICENSE +0 -0
  9. {voxcity-0.6.16 → voxcity-0.6.17}/README.md +0 -0
  10. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/__init__.py +0 -0
  11. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/downloader/__init__.py +0 -0
  12. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/downloader/citygml.py +0 -0
  13. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/downloader/eubucco.py +0 -0
  14. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/downloader/gee.py +0 -0
  15. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/downloader/mbfp.py +0 -0
  16. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/downloader/oemj.py +0 -0
  17. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/downloader/utils.py +0 -0
  18. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/exporter/__init__.py +0 -0
  19. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/exporter/cityles.py +0 -0
  20. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/exporter/envimet.py +0 -0
  21. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/exporter/magicavoxel.py +0 -0
  22. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/exporter/obj.py +0 -0
  23. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/geoprocessor/__init__.py +0 -0
  24. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/geoprocessor/draw.py +0 -0
  25. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/geoprocessor/mesh.py +0 -0
  26. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/geoprocessor/network.py +0 -0
  27. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/geoprocessor/polygon.py +0 -0
  28. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/geoprocessor/utils.py +0 -0
  29. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/simulator/__init__.py +0 -0
  30. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/simulator/solar.py +0 -0
  31. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/simulator/utils.py +0 -0
  32. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/simulator/view.py +0 -0
  33. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/utils/__init__.py +0 -0
  34. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/utils/lc.py +0 -0
  35. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/utils/material.py +0 -0
  36. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/utils/visualization.py +0 -0
  37. {voxcity-0.6.16 → voxcity-0.6.17}/src/voxcity/utils/weather.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: voxcity
3
- Version: 0.6.16
3
+ Version: 0.6.17
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
  License: MIT
6
6
  Author: Kunihiko Fujiwara
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "voxcity"
3
- version = "0.6.16"
3
+ version = "0.6.17"
4
4
  description = "voxcity is an easy and one-stop tool to output 3d city models for microclimate simulation by integrating multiple geospatial open-data"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -370,7 +370,7 @@ def create_rings_from_ways(way_ids, ways, nodes):
370
370
 
371
371
  return rings
372
372
 
373
- def load_gdf_from_openstreetmap(rectangle_vertices):
373
+ def load_gdf_from_openstreetmap(rectangle_vertices, floor_height=3.0):
374
374
  """Download and process building footprint data from OpenStreetMap.
375
375
 
376
376
  This function:
@@ -471,7 +471,7 @@ def load_gdf_from_openstreetmap(rectangle_vertices):
471
471
  """
472
472
  return [coord for coord in geometry] # Keep original order since already (lon, lat)
473
473
 
474
- def get_height_from_properties(properties):
474
+ def get_height_from_properties(properties, floor_height=3.0):
475
475
  """Helper function to extract height from properties.
476
476
 
477
477
  Args:
@@ -487,9 +487,25 @@ def load_gdf_from_openstreetmap(rectangle_vertices):
487
487
  except ValueError:
488
488
  pass
489
489
 
490
+ # Infer from floors when available
491
+ floors_candidates = [
492
+ properties.get('building:levels'),
493
+ properties.get('levels'),
494
+ properties.get('num_floors')
495
+ ]
496
+ for floors in floors_candidates:
497
+ if floors is None:
498
+ continue
499
+ try:
500
+ floors_val = float(floors)
501
+ if floors_val > 0:
502
+ return float(floor_height) * floors_val
503
+ except ValueError:
504
+ continue
505
+
490
506
  return 0 # Default height if no valid height found
491
507
 
492
- def extract_properties(element):
508
+ def extract_properties(element, floor_height=3.0):
493
509
  """Helper function to extract and process properties from an element.
494
510
 
495
511
  Args:
@@ -501,7 +517,7 @@ def load_gdf_from_openstreetmap(rectangle_vertices):
501
517
  properties = element.get('tags', {})
502
518
 
503
519
  # Get height (now using the helper function)
504
- height = get_height_from_properties(properties)
520
+ height = get_height_from_properties(properties, floor_height=floor_height)
505
521
 
506
522
  # Get min_height and min_level
507
523
  min_height = properties.get('min_height', '0')
@@ -526,7 +542,7 @@ def load_gdf_from_openstreetmap(rectangle_vertices):
526
542
  "is_inner": False,
527
543
  "levels": levels,
528
544
  "height_source": "explicit" if properties.get('height') or properties.get('building:height')
529
- else "levels" if levels is not None
545
+ else "levels" if (levels is not None) or (properties.get('num_floors') is not None)
530
546
  else "default",
531
547
  "min_level": min_level if min_level != '0' else None,
532
548
  "building": properties.get('building', 'no'),
@@ -584,13 +600,13 @@ def load_gdf_from_openstreetmap(rectangle_vertices):
584
600
  if element['type'] == 'way':
585
601
  if 'geometry' in element:
586
602
  coords = [(node['lon'], node['lat']) for node in element['geometry']]
587
- properties = extract_properties(element)
603
+ properties = extract_properties(element, floor_height=floor_height)
588
604
  feature = create_polygon_feature(coords, properties)
589
605
  if feature:
590
606
  features.append(feature)
591
607
 
592
608
  elif element['type'] == 'relation':
593
- properties = extract_properties(element)
609
+ properties = extract_properties(element, floor_height=floor_height)
594
610
 
595
611
  # Process each member of the relation
596
612
  for member in element['members']:
@@ -254,7 +254,7 @@ def join_gdfs_vertically(gdf1, gdf2):
254
254
 
255
255
  return combined_gdf
256
256
 
257
- def load_gdf_from_overture(rectangle_vertices):
257
+ def load_gdf_from_overture(rectangle_vertices, floor_height=3.0):
258
258
  """
259
259
  Download and process building footprint data from Overture Maps.
260
260
 
@@ -287,6 +287,31 @@ def load_gdf_from_overture(rectangle_vertices):
287
287
  # Combine both datasets into a single comprehensive building dataset
288
288
  joined_building_gdf = join_gdfs_vertically(building_gdf, building_part_gdf)
289
289
 
290
+ # Ensure numeric height and infer from floors when missing
291
+ try:
292
+ joined_building_gdf['height'] = pd.to_numeric(joined_building_gdf.get('height', None), errors='coerce')
293
+ except Exception:
294
+ # Create height column if missing
295
+ joined_building_gdf['height'] = None
296
+ joined_building_gdf['height'] = pd.to_numeric(joined_building_gdf['height'], errors='coerce')
297
+
298
+ # Combine possible floors columns (first non-null among candidates)
299
+ floors_candidates = []
300
+ for col in ['building:levels', 'levels', 'num_floors', 'floors']:
301
+ if col in joined_building_gdf.columns:
302
+ floors_candidates.append(pd.to_numeric(joined_building_gdf[col], errors='coerce'))
303
+ if floors_candidates:
304
+ floors_series = floors_candidates[0]
305
+ for s in floors_candidates[1:]:
306
+ floors_series = floors_series.combine_first(s)
307
+ # Infer height where height is NaN/<=0 and floors > 0
308
+ mask_missing_height = (~joined_building_gdf['height'].notna()) | (joined_building_gdf['height'] <= 0)
309
+ if isinstance(floor_height, (int, float)):
310
+ inferred = floors_series * float(floor_height)
311
+ else:
312
+ inferred = floors_series * 3.0
313
+ joined_building_gdf.loc[mask_missing_height & (floors_series > 0), 'height'] = inferred
314
+
290
315
  # Assign sequential IDs based on the final dataset index
291
316
  joined_building_gdf['id'] = joined_building_gdf.index
292
317
 
@@ -226,12 +226,14 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, b
226
226
  else:
227
227
  # Fetch building data from primary source
228
228
  # Each source has different data formats and processing requirements
229
+ # Floor height (m) for inferring heights from floors/levels
230
+ floor_height = kwargs.get("floor_height", 3.0)
229
231
  if source == 'Microsoft Building Footprints':
230
232
  # Machine learning-derived building footprints from satellite imagery
231
233
  gdf = get_mbfp_gdf(output_dir, rectangle_vertices)
232
234
  elif source == 'OpenStreetMap':
233
235
  # Crowd-sourced building data with varying completeness
234
- gdf = load_gdf_from_openstreetmap(rectangle_vertices)
236
+ gdf = load_gdf_from_openstreetmap(rectangle_vertices, floor_height=floor_height)
235
237
  elif source == "Open Building 2.5D Temporal":
236
238
  # Special case: this source provides both footprints and heights
237
239
  # Skip GeoDataFrame processing and create grids directly
@@ -244,7 +246,7 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, b
244
246
  # gdf = load_gdf_from_openmaptiles(rectangle_vertices, kwargs["maptiler_API_key"])
245
247
  elif source == "Overture":
246
248
  # Open building dataset from Overture Maps Foundation
247
- gdf = load_gdf_from_overture(rectangle_vertices)
249
+ gdf = load_gdf_from_overture(rectangle_vertices, floor_height=floor_height)
248
250
  elif source == "Local file":
249
251
  # Handle user-provided local building data files
250
252
  _, extension = os.path.splitext(kwargs["building_path"])
@@ -285,13 +287,13 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, b
285
287
  if building_complementary_source == 'Microsoft Building Footprints':
286
288
  gdf_comp = get_mbfp_gdf(output_dir, rectangle_vertices)
287
289
  elif building_complementary_source == 'OpenStreetMap':
288
- gdf_comp = load_gdf_from_openstreetmap(rectangle_vertices)
290
+ gdf_comp = load_gdf_from_openstreetmap(rectangle_vertices, floor_height=floor_height)
289
291
  elif building_complementary_source == 'EUBUCCO v0.1':
290
292
  gdf_comp = load_gdf_from_eubucco(rectangle_vertices, output_dir)
291
293
  # elif building_complementary_source == "OpenMapTiles":
292
294
  # gdf_comp = load_gdf_from_openmaptiles(rectangle_vertices, kwargs["maptiler_API_key"])
293
295
  elif building_complementary_source == "Overture":
294
- gdf_comp = load_gdf_from_overture(rectangle_vertices)
296
+ gdf_comp = load_gdf_from_overture(rectangle_vertices, floor_height=floor_height)
295
297
  elif building_complementary_source == "Local file":
296
298
  _, extension = os.path.splitext(kwargs["building_complementary_path"])
297
299
  if extension == ".gpkg":
@@ -715,7 +717,12 @@ def get_voxcity(rectangle_vertices, building_source, land_cover_source, canopy_h
715
717
  # Create uniform canopy height for all tree-covered areas (top grid)
716
718
  canopy_height_grid = np.zeros_like(land_cover_grid, dtype=float)
717
719
  static_tree_height = kwargs.get("static_tree_height", 10.0)
718
- tree_mask = (land_cover_grid == 4)
720
+ # Determine tree class indices based on source-specific class names
721
+ _classes = get_land_cover_classes(land_cover_source)
722
+ _class_to_int = {name: i for i, name in enumerate(_classes.values())}
723
+ _tree_labels = ["Tree", "Trees", "Tree Canopy"]
724
+ _tree_indices = [_class_to_int[label] for label in _tree_labels if label in _class_to_int]
725
+ tree_mask = np.isin(land_cover_grid, _tree_indices) if _tree_indices else np.zeros_like(land_cover_grid, dtype=bool)
719
726
  canopy_height_grid[tree_mask] = static_tree_height
720
727
 
721
728
  # Derive bottom from trunk_height_ratio
@@ -917,11 +924,79 @@ def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_sou
917
924
  # get all required gdfs
918
925
  building_gdf, terrain_gdf, vegetation_gdf = load_buid_dem_veg_from_citygml(url=url_citygml, citygml_path=citygml_path, base_dir=output_dir, rectangle_vertices=rectangle_vertices)
919
926
 
927
+ # Normalize CRS to WGS84 (EPSG:4326) to ensure consistent operations downstream
928
+ try:
929
+ import geopandas as gpd # noqa: F401
930
+ if building_gdf is not None:
931
+ if building_gdf.crs is None:
932
+ building_gdf = building_gdf.set_crs(epsg=4326)
933
+ elif getattr(building_gdf.crs, 'to_epsg', lambda: None)() != 4326 and building_gdf.crs != "EPSG:4326":
934
+ building_gdf = building_gdf.to_crs(epsg=4326)
935
+ if terrain_gdf is not None:
936
+ if terrain_gdf.crs is None:
937
+ terrain_gdf = terrain_gdf.set_crs(epsg=4326)
938
+ elif getattr(terrain_gdf.crs, 'to_epsg', lambda: None)() != 4326 and terrain_gdf.crs != "EPSG:4326":
939
+ terrain_gdf = terrain_gdf.to_crs(epsg=4326)
940
+ if vegetation_gdf is not None:
941
+ if vegetation_gdf.crs is None:
942
+ vegetation_gdf = vegetation_gdf.set_crs(epsg=4326)
943
+ elif getattr(vegetation_gdf.crs, 'to_epsg', lambda: None)() != 4326 and vegetation_gdf.crs != "EPSG:4326":
944
+ vegetation_gdf = vegetation_gdf.to_crs(epsg=4326)
945
+ except Exception:
946
+ pass
947
+
920
948
  land_cover_grid = get_land_cover_grid(rectangle_vertices, meshsize, land_cover_source, output_dir, **kwargs)
921
949
 
922
950
  # building_height_grid, building_min_height_grid, building_id_grid, building_gdf = get_building_height_grid(rectangle_vertices, meshsize, building_source, output_dir, **kwargs)
923
951
  print("Creating building height grid")
924
- # Filter kwargs to only those accepted by create_building_height_grid_from_gdf_polygon
952
+ # Prepare complementary building source if provided
953
+ building_complementary_source = kwargs.get("building_complementary_source")
954
+ gdf_comp = None
955
+ geotiff_path_comp = None
956
+ complement_building_footprints = kwargs.get("complement_building_footprints")
957
+ # Default to complement footprints when a complementary source is specified
958
+ if complement_building_footprints is None and (building_complementary_source not in (None, "None")):
959
+ complement_building_footprints = True
960
+
961
+ if (building_complementary_source is not None) and (building_complementary_source != "None"):
962
+ # Vector complementary sources
963
+ floor_height = kwargs.get("floor_height", 3.0)
964
+ if building_complementary_source == 'Microsoft Building Footprints':
965
+ gdf_comp = get_mbfp_gdf(kwargs.get("output_dir", "output"), rectangle_vertices)
966
+ elif building_complementary_source == 'OpenStreetMap':
967
+ gdf_comp = load_gdf_from_openstreetmap(rectangle_vertices, floor_height=floor_height)
968
+ elif building_complementary_source == 'EUBUCCO v0.1':
969
+ gdf_comp = load_gdf_from_eubucco(rectangle_vertices, kwargs.get("output_dir", "output"))
970
+ elif building_complementary_source == 'Overture':
971
+ gdf_comp = load_gdf_from_overture(rectangle_vertices, floor_height=floor_height)
972
+ elif building_complementary_source == 'Local file':
973
+ comp_path = kwargs.get("building_complementary_path")
974
+ if comp_path is not None:
975
+ _, extension = os.path.splitext(comp_path)
976
+ if extension == ".gpkg":
977
+ gdf_comp = get_gdf_from_gpkg(comp_path, rectangle_vertices)
978
+ # Ensure complementary GDF uses WGS84
979
+ if gdf_comp is not None:
980
+ try:
981
+ if gdf_comp.crs is None:
982
+ gdf_comp = gdf_comp.set_crs(epsg=4326)
983
+ elif getattr(gdf_comp.crs, 'to_epsg', lambda: None)() != 4326 and gdf_comp.crs != "EPSG:4326":
984
+ gdf_comp = gdf_comp.to_crs(epsg=4326)
985
+ except Exception:
986
+ pass
987
+ # Raster complementary sources (height only)
988
+ elif building_complementary_source == "Open Building 2.5D Temporal":
989
+ roi = get_roi(rectangle_vertices)
990
+ os.makedirs(kwargs.get("output_dir", "output"), exist_ok=True)
991
+ geotiff_path_comp = os.path.join(kwargs.get("output_dir", "output"), "building_height.tif")
992
+ save_geotiff_open_buildings_temporal(roi, geotiff_path_comp)
993
+ elif building_complementary_source in ["England 1m DSM - DTM", "Netherlands 0.5m DSM - DTM"]:
994
+ roi = get_roi(rectangle_vertices)
995
+ os.makedirs(kwargs.get("output_dir", "output"), exist_ok=True)
996
+ geotiff_path_comp = os.path.join(kwargs.get("output_dir", "output"), "building_height.tif")
997
+ save_geotiff_dsm_minus_dtm(roi, geotiff_path_comp, meshsize, building_complementary_source)
998
+
999
+ # Filter and assemble kwargs accepted by the grid function
925
1000
  _allowed_building_kwargs = {
926
1001
  "overlapping_footprint",
927
1002
  "gdf_comp",
@@ -930,6 +1005,21 @@ def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_sou
930
1005
  "complement_height",
931
1006
  }
932
1007
  _building_kwargs = {k: v for k, v in kwargs.items() if k in _allowed_building_kwargs}
1008
+ if gdf_comp is not None:
1009
+ _building_kwargs["gdf_comp"] = gdf_comp
1010
+ if geotiff_path_comp is not None:
1011
+ _building_kwargs["geotiff_path_comp"] = geotiff_path_comp
1012
+ if complement_building_footprints is not None:
1013
+ _building_kwargs["complement_building_footprints"] = complement_building_footprints
1014
+
1015
+ # Map user-provided building_complement_height -> complement_height for grid builder
1016
+ comp_height_user = kwargs.get("building_complement_height")
1017
+ if comp_height_user is not None:
1018
+ _building_kwargs["complement_height"] = comp_height_user
1019
+ # If footprints are being complemented and no height provided, default to 10
1020
+ if _building_kwargs.get("complement_building_footprints") and ("complement_height" not in _building_kwargs):
1021
+ _building_kwargs["complement_height"] = 10.0
1022
+
933
1023
  building_height_grid, building_min_height_grid, building_id_grid, filtered_buildings = create_building_height_grid_from_gdf_polygon(
934
1024
  building_gdf, meshsize, rectangle_vertices, **_building_kwargs
935
1025
  )
@@ -953,7 +1043,12 @@ def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_sou
953
1043
 
954
1044
  # Set default static height for trees (20 meters is a typical average tree height)
955
1045
  static_tree_height = kwargs.get("static_tree_height", 10.0)
956
- tree_mask = (land_cover_grid == 4)
1046
+ # Determine tree class indices based on source-specific class names
1047
+ _classes = get_land_cover_classes(land_cover_source)
1048
+ _class_to_int = {name: i for i, name in enumerate(_classes.values())}
1049
+ _tree_labels = ["Tree", "Trees", "Tree Canopy"]
1050
+ _tree_indices = [_class_to_int[label] for label in _tree_labels if label in _class_to_int]
1051
+ tree_mask = np.isin(land_cover_grid, _tree_indices) if _tree_indices else np.zeros_like(land_cover_grid, dtype=bool)
957
1052
 
958
1053
  # Set static height for tree cells
959
1054
  canopy_height_grid_comp[tree_mask] = static_tree_height