voxcity 0.6.9__tar.gz → 0.6.11__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.9 → voxcity-0.6.11}/PKG-INFO +1 -1
  2. {voxcity-0.6.9 → voxcity-0.6.11}/pyproject.toml +1 -1
  3. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/downloader/citygml.py +124 -8
  4. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/generator.py +53 -1
  5. {voxcity-0.6.9 → voxcity-0.6.11}/AUTHORS.rst +0 -0
  6. {voxcity-0.6.9 → voxcity-0.6.11}/LICENSE +0 -0
  7. {voxcity-0.6.9 → voxcity-0.6.11}/README.md +0 -0
  8. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/__init__.py +0 -0
  9. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/downloader/__init__.py +0 -0
  10. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/downloader/eubucco.py +0 -0
  11. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/downloader/gee.py +0 -0
  12. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/downloader/mbfp.py +0 -0
  13. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/downloader/oemj.py +0 -0
  14. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/downloader/osm.py +0 -0
  15. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/downloader/overture.py +0 -0
  16. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/downloader/utils.py +0 -0
  17. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/exporter/__init__.py +0 -0
  18. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/exporter/cityles.py +0 -0
  19. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/exporter/envimet.py +0 -0
  20. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/exporter/magicavoxel.py +0 -0
  21. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/exporter/obj.py +0 -0
  22. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/geoprocessor/__init__.py +0 -0
  23. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/geoprocessor/draw.py +0 -0
  24. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/geoprocessor/grid.py +0 -0
  25. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/geoprocessor/mesh.py +0 -0
  26. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/geoprocessor/network.py +0 -0
  27. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/geoprocessor/polygon.py +0 -0
  28. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/geoprocessor/utils.py +0 -0
  29. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/simulator/__init__.py +0 -0
  30. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/simulator/solar.py +0 -0
  31. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/simulator/utils.py +0 -0
  32. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/simulator/view.py +0 -0
  33. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/utils/__init__.py +0 -0
  34. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/utils/lc.py +0 -0
  35. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/utils/material.py +0 -0
  36. {voxcity-0.6.9 → voxcity-0.6.11}/src/voxcity/utils/visualization.py +0 -0
  37. {voxcity-0.6.9 → voxcity-0.6.11}/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.9
3
+ Version: 0.6.11
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.9"
3
+ version = "0.6.11"
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"
@@ -398,6 +398,43 @@ def extract_vegetation_info(file_path, namespaces):
398
398
  try:
399
399
  tree = ET.parse(file_path)
400
400
  root = tree.getroot()
401
+
402
+ # Build namespaces dynamically from the file and merge with provided ones
403
+ nsmap = root.nsmap or {}
404
+ # Fallbacks in case discovery fails
405
+ fallback_ns = {
406
+ 'core': 'http://www.opengis.net/citygml/2.0',
407
+ 'bldg': 'http://www.opengis.net/citygml/building/2.0',
408
+ 'gml': 'http://www.opengis.net/gml',
409
+ 'uro': 'https://www.geospatial.jp/iur/uro/3.0',
410
+ 'dem': 'http://www.opengis.net/citygml/relief/2.0',
411
+ 'veg': 'http://www.opengis.net/citygml/vegetation/2.0'
412
+ }
413
+
414
+ def pick_ns(prefix, keyword=None, fallback_key=None):
415
+ # Prefer provided namespaces if valid
416
+ if isinstance(namespaces, dict) and prefix in namespaces and namespaces[prefix]:
417
+ return namespaces[prefix]
418
+ # Then try from document nsmap
419
+ uri = nsmap.get(prefix)
420
+ if uri:
421
+ return uri
422
+ # Then try keyword search
423
+ if keyword:
424
+ for v in nsmap.values():
425
+ if isinstance(v, str) and keyword in v:
426
+ return v
427
+ # Finally fallback
428
+ return fallback_ns[fallback_key or prefix]
429
+
430
+ ns = {
431
+ 'core': pick_ns('core', keyword='citygml', fallback_key='core'),
432
+ 'bldg': pick_ns('bldg', keyword='building', fallback_key='bldg'),
433
+ 'gml': pick_ns('gml', keyword='gml', fallback_key='gml'),
434
+ 'uro': pick_ns('uro', keyword='iur/uro', fallback_key='uro'),
435
+ 'dem': pick_ns('dem', keyword='relief', fallback_key='dem'),
436
+ 'veg': pick_ns('veg', keyword='vegetation', fallback_key='veg')
437
+ }
401
438
  except Exception as e:
402
439
  print(f"Error parsing CityGML file {Path(file_path).name}: {e}")
403
440
  return vegetation_elements
@@ -405,8 +442,8 @@ def extract_vegetation_info(file_path, namespaces):
405
442
  # Helper: parse polygons in <gml:MultiSurface> or <veg:lodXMultiSurface>
406
443
  def parse_lod_multisurface(lod_elem):
407
444
  polygons = []
408
- for poly_node in lod_elem.findall('.//gml:Polygon', namespaces):
409
- ring_node = poly_node.find('.//gml:exterior//gml:LinearRing//gml:posList', namespaces)
445
+ for poly_node in lod_elem.findall('.//gml:Polygon', ns):
446
+ ring_node = poly_node.find('.//gml:exterior//gml:LinearRing//gml:posList', ns)
410
447
  if ring_node is None or ring_node.text is None:
411
448
  continue
412
449
  coords_text = ring_node.text.strip().split()
@@ -442,25 +479,68 @@ def extract_vegetation_info(file_path, namespaces):
442
479
  "lod0MultiSurface", "lod1MultiSurface", "lod2MultiSurface", "lod3MultiSurface", "lod4MultiSurface"
443
480
  ]
444
481
  for lod_tag in geometry_lods:
445
- lod_elem = veg_elem.find(f'.//veg:{lod_tag}', namespaces)
482
+ lod_elem = veg_elem.find(f'.//veg:{lod_tag}', ns)
446
483
  if lod_elem is not None:
447
484
  geom = parse_lod_multisurface(lod_elem)
448
485
  if geom is not None:
449
486
  return geom
450
487
  return None
451
488
 
489
+ def compute_lod_height(veg_elem):
490
+ """
491
+ Fallback: compute vegetation height from Z values in any gml:posList
492
+ under the available LOD geometry elements. Returns (max_z - min_z)
493
+ if any Z values are found; otherwise None.
494
+ """
495
+ z_values = []
496
+ geometry_lods = [
497
+ "lod0Geometry", "lod1Geometry", "lod2Geometry", "lod3Geometry", "lod4Geometry",
498
+ "lod0MultiSurface", "lod1MultiSurface", "lod2MultiSurface", "lod3MultiSurface", "lod4MultiSurface"
499
+ ]
500
+ try:
501
+ for lod_tag in geometry_lods:
502
+ lod_elem = veg_elem.find(f'.//veg:{lod_tag}', ns)
503
+ if lod_elem is None:
504
+ continue
505
+ for pos_list in lod_elem.findall('.//gml:posList', ns):
506
+ if pos_list.text is None:
507
+ continue
508
+ coords_text = pos_list.text.strip().split()
509
+ # Expect triplets (x,y,z) or (lat,lon,z). Z should be each 3rd value
510
+ for i in range(2, len(coords_text), 3):
511
+ try:
512
+ z = float(coords_text[i])
513
+ if not np.isinf(z) and not np.isnan(z):
514
+ z_values.append(z)
515
+ except Exception:
516
+ continue
517
+ if z_values:
518
+ return float(max(z_values) - min(z_values))
519
+ except Exception:
520
+ pass
521
+ return None
522
+
452
523
  # 1) PlantCover
453
- for plant_cover in root.findall('.//veg:PlantCover', namespaces):
524
+ for plant_cover in root.findall('.//veg:PlantCover', ns):
454
525
  cover_id = plant_cover.get('{http://www.opengis.net/gml}id')
455
- avg_height_elem = plant_cover.find('.//veg:averageHeight', namespaces)
526
+ avg_height_elem = plant_cover.find('.//veg:averageHeight', ns)
456
527
  if avg_height_elem is not None and avg_height_elem.text:
457
528
  try:
458
529
  vegetation_height = float(avg_height_elem.text)
530
+ # Treat sentinel values like -9999 as missing
531
+ if vegetation_height <= -9998:
532
+ vegetation_height = None
459
533
  except:
460
534
  vegetation_height = None
461
535
  else:
462
536
  vegetation_height = None
463
537
 
538
+ # Fallback to geometry-derived height if needed
539
+ if vegetation_height is None:
540
+ derived_h = compute_lod_height(plant_cover)
541
+ if derived_h is not None:
542
+ vegetation_height = derived_h
543
+
464
544
  geometry = get_veg_geometry(plant_cover)
465
545
  if geometry is not None and not geometry.is_empty:
466
546
  vegetation_elements.append({
@@ -472,17 +552,26 @@ def extract_vegetation_info(file_path, namespaces):
472
552
  })
473
553
 
474
554
  # 2) SolitaryVegetationObject
475
- for solitary in root.findall('.//veg:SolitaryVegetationObject', namespaces):
555
+ for solitary in root.findall('.//veg:SolitaryVegetationObject', ns):
476
556
  veg_id = solitary.get('{http://www.opengis.net/gml}id')
477
- height_elem = solitary.find('.//veg:height', namespaces)
557
+ height_elem = solitary.find('.//veg:height', ns)
478
558
  if height_elem is not None and height_elem.text:
479
559
  try:
480
560
  veg_height = float(height_elem.text)
561
+ # Treat sentinel values like -9999 as missing
562
+ if veg_height <= -9998:
563
+ veg_height = None
481
564
  except:
482
565
  veg_height = None
483
566
  else:
484
567
  veg_height = None
485
568
 
569
+ # Fallback to geometry-derived height if attribute is missing/unparseable
570
+ if veg_height is None:
571
+ derived_h = compute_lod_height(solitary)
572
+ if derived_h is not None:
573
+ veg_height = derived_h
574
+
486
575
  geometry = get_veg_geometry(solitary)
487
576
  if geometry is not None and not geometry.is_empty:
488
577
  vegetation_elements.append({
@@ -577,7 +666,8 @@ def process_citygml_file(file_path):
577
666
  terrain_elements = []
578
667
  vegetation_elements = []
579
668
 
580
- namespaces = {
669
+ # Default/fallback namespaces (used if not present in the file)
670
+ fallback_namespaces = {
581
671
  'core': 'http://www.opengis.net/citygml/2.0',
582
672
  'bldg': 'http://www.opengis.net/citygml/building/2.0',
583
673
  'gml': 'http://www.opengis.net/gml',
@@ -590,6 +680,32 @@ def process_citygml_file(file_path):
590
680
  tree = ET.parse(file_path)
591
681
  root = tree.getroot()
592
682
 
683
+ # Build namespaces dynamically from the file, falling back to defaults.
684
+ nsmap = root.nsmap or {}
685
+
686
+ def pick_ns(prefix, keyword=None, fallback_key=None):
687
+ # Try explicit prefix first
688
+ uri = nsmap.get(prefix)
689
+ if uri:
690
+ return uri
691
+ # Try to discover by keyword in URI (e.g., 'vegetation', 'building', 'relief')
692
+ if keyword:
693
+ for v in nsmap.values():
694
+ if isinstance(v, str) and keyword in v:
695
+ return v
696
+ # Fallback to defaults
697
+ return fallback_namespaces[fallback_key or prefix]
698
+
699
+ namespaces = {
700
+ 'core': pick_ns('core', keyword='citygml', fallback_key='core'),
701
+ 'bldg': pick_ns('bldg', keyword='building', fallback_key='bldg'),
702
+ 'gml': pick_ns('gml', keyword='gml', fallback_key='gml'),
703
+ 'uro': pick_ns('uro', keyword='iur/uro', fallback_key='uro'),
704
+ 'dem': pick_ns('dem', keyword='relief', fallback_key='dem'),
705
+ # Accept CityGML 2.0 or 3.0 vegetation namespaces
706
+ 'veg': pick_ns('veg', keyword='vegetation', fallback_key='veg')
707
+ }
708
+
593
709
  # Extract Buildings
594
710
  for building in root.findall('.//bldg:Building', namespaces):
595
711
  building_id = building.get('{http://www.opengis.net/gml}id')
@@ -710,6 +710,7 @@ def get_voxcity(rectangle_vertices, building_source, land_cover_source, canopy_h
710
710
  # This is useful when the area of interest is part of a larger urban area
711
711
  remove_perimeter_object = kwargs.get("remove_perimeter_object")
712
712
  if (remove_perimeter_object is not None) and (remove_perimeter_object > 0):
713
+ print("apply perimeter removal")
713
714
  # Calculate perimeter width based on grid dimensions
714
715
  w_peri = int(remove_perimeter_object * building_height_grid.shape[0] + 0.5)
715
716
  h_peri = int(remove_perimeter_object * building_height_grid.shape[1] + 0.5)
@@ -730,6 +731,31 @@ def get_voxcity(rectangle_vertices, building_source, land_cover_source, canopy_h
730
731
  building_height_grid[positions] = 0
731
732
  building_min_height_grid[positions] = [[] for _ in range(len(building_min_height_grid[positions]))]
732
733
 
734
+ # Visualize grids after optional perimeter removal
735
+ grid_vis = kwargs.get("gridvis", True)
736
+ if grid_vis:
737
+ # Building height grid visualization (zeros hidden)
738
+ building_height_grid_nan = building_height_grid.copy()
739
+ building_height_grid_nan[building_height_grid_nan == 0] = np.nan
740
+ visualize_numerical_grid(
741
+ np.flipud(building_height_grid_nan),
742
+ meshsize,
743
+ "building height (m)",
744
+ cmap='viridis',
745
+ label='Value'
746
+ )
747
+
748
+ # Canopy height grid visualization (zeros hidden)
749
+ canopy_height_grid_nan = canopy_height_grid.copy()
750
+ canopy_height_grid_nan[canopy_height_grid_nan == 0] = np.nan
751
+ visualize_numerical_grid(
752
+ np.flipud(canopy_height_grid_nan),
753
+ meshsize,
754
+ "Tree canopy height (m)",
755
+ cmap='Greens',
756
+ label='Tree canopy height (m)'
757
+ )
758
+
733
759
  # STEP 5: Generate optional 2D visualizations on interactive maps
734
760
  mapvis = kwargs.get("mapvis")
735
761
  if mapvis:
@@ -926,6 +952,7 @@ def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_sou
926
952
  # Remove objects near perimeter if specified
927
953
  remove_perimeter_object = kwargs.get("remove_perimeter_object")
928
954
  if (remove_perimeter_object is not None) and (remove_perimeter_object > 0):
955
+ print("apply perimeter removal")
929
956
  # Calculate perimeter width based on grid dimensions
930
957
  w_peri = int(remove_perimeter_object * building_height_grid.shape[0] + 0.5)
931
958
  h_peri = int(remove_perimeter_object * building_height_grid.shape[1] + 0.5)
@@ -944,7 +971,32 @@ def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_sou
944
971
  for remove_id in remove_ids:
945
972
  positions = np.where(building_id_grid == remove_id)
946
973
  building_height_grid[positions] = 0
947
- building_min_height_grid[positions] = [[] for _ in range(len(building_min_height_grid[positions]))]
974
+ building_min_height_grid[positions] = [[] for _ in range(len(building_min_height_grid[positions]))]
975
+
976
+ # Visualize grids after optional perimeter removal
977
+ grid_vis = kwargs.get("gridvis", True)
978
+ if grid_vis:
979
+ # Building height grid visualization (zeros hidden)
980
+ building_height_grid_nan = building_height_grid.copy()
981
+ building_height_grid_nan[building_height_grid_nan == 0] = np.nan
982
+ visualize_numerical_grid(
983
+ np.flipud(building_height_grid_nan),
984
+ meshsize,
985
+ "building height (m)",
986
+ cmap='viridis',
987
+ label='Value'
988
+ )
989
+
990
+ # Canopy height grid visualization (zeros hidden)
991
+ canopy_height_grid_nan = canopy_height_grid.copy()
992
+ canopy_height_grid_nan[canopy_height_grid_nan == 0] = np.nan
993
+ visualize_numerical_grid(
994
+ np.flipud(canopy_height_grid_nan),
995
+ meshsize,
996
+ "Tree canopy height (m)",
997
+ cmap='Greens',
998
+ label='Tree canopy height (m)'
999
+ )
948
1000
 
949
1001
  # Generate 3D voxel grid
950
1002
  voxcity_grid = create_3d_voxel(building_height_grid, building_min_height_grid, building_id_grid, land_cover_grid, dem_grid, canopy_height_grid, meshsize, land_cover_source)
File without changes
File without changes
File without changes