voxcity 0.6.10__tar.gz → 0.6.12__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.10 → voxcity-0.6.12}/PKG-INFO +1 -1
  2. {voxcity-0.6.10 → voxcity-0.6.12}/pyproject.toml +1 -1
  3. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/downloader/citygml.py +124 -8
  4. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/downloader/osm.py +75 -10
  5. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/generator.py +1136 -1136
  6. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/geoprocessor/draw.py +1085 -832
  7. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/utils/visualization.py +14 -6
  8. {voxcity-0.6.10 → voxcity-0.6.12}/AUTHORS.rst +0 -0
  9. {voxcity-0.6.10 → voxcity-0.6.12}/LICENSE +0 -0
  10. {voxcity-0.6.10 → voxcity-0.6.12}/README.md +0 -0
  11. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/__init__.py +0 -0
  12. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/downloader/__init__.py +0 -0
  13. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/downloader/eubucco.py +0 -0
  14. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/downloader/gee.py +0 -0
  15. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/downloader/mbfp.py +0 -0
  16. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/downloader/oemj.py +0 -0
  17. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/downloader/overture.py +0 -0
  18. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/downloader/utils.py +0 -0
  19. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/exporter/__init__.py +0 -0
  20. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/exporter/cityles.py +0 -0
  21. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/exporter/envimet.py +0 -0
  22. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/exporter/magicavoxel.py +0 -0
  23. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/exporter/obj.py +0 -0
  24. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/geoprocessor/__init__.py +0 -0
  25. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/geoprocessor/grid.py +0 -0
  26. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/geoprocessor/mesh.py +0 -0
  27. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/geoprocessor/network.py +0 -0
  28. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/geoprocessor/polygon.py +0 -0
  29. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/geoprocessor/utils.py +0 -0
  30. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/simulator/__init__.py +0 -0
  31. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/simulator/solar.py +0 -0
  32. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/simulator/utils.py +0 -0
  33. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/simulator/view.py +0 -0
  34. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/utils/__init__.py +0 -0
  35. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/utils/lc.py +0 -0
  36. {voxcity-0.6.10 → voxcity-0.6.12}/src/voxcity/utils/material.py +0 -0
  37. {voxcity-0.6.10 → voxcity-0.6.12}/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.10
3
+ Version: 0.6.12
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.10"
3
+ version = "0.6.12"
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')
@@ -398,7 +398,12 @@ def load_gdf_from_openstreetmap(rectangle_vertices):
398
398
  max_lat = max(v[1] for v in rectangle_vertices)
399
399
 
400
400
  # Enhanced Overpass API query with recursive member extraction
401
- overpass_url = "http://overpass-api.de/api/interpreter"
401
+ # Try multiple Overpass endpoints to improve resiliency against rate limits or outages
402
+ overpass_endpoints = [
403
+ "https://overpass-api.de/api/interpreter",
404
+ "https://overpass.kumi.systems/api/interpreter",
405
+ "https://overpass.openstreetmap.ru/api/interpreter",
406
+ ]
402
407
  overpass_query = f"""
403
408
  [out:json];
404
409
  (
@@ -412,9 +417,40 @@ def load_gdf_from_openstreetmap(rectangle_vertices):
412
417
  out geom;
413
418
  """
414
419
 
415
- # Send the request to the Overpass API
416
- response = requests.get(overpass_url, params={'data': overpass_query})
417
- data = response.json()
420
+ # Send the request to the Overpass API with fallbacks and robust JSON handling
421
+ headers = {"User-Agent": "voxcity/ci (https://github.com/voxcity)"}
422
+ data = None
423
+ last_error = None
424
+ for overpass_url in overpass_endpoints:
425
+ try:
426
+ response = requests.get(overpass_url, params={"data": overpass_query}, headers=headers, timeout=30)
427
+ # Ensure HTTP OK
428
+ if response.status_code != 200:
429
+ last_error = Exception(f"HTTP {response.status_code} from {overpass_url}")
430
+ continue
431
+ # Some servers return HTML/plain text on rate-limit; guard JSON parsing
432
+ content_type = response.headers.get("Content-Type", "")
433
+ if "json" not in content_type.lower():
434
+ # Attempt JSON anyway; if fails, try next endpoint
435
+ try:
436
+ data = response.json()
437
+ except Exception as e:
438
+ last_error = e
439
+ continue
440
+ else:
441
+ data = response.json()
442
+ # Validate structure
443
+ if not isinstance(data, dict) or "elements" not in data:
444
+ last_error = Exception(f"Malformed Overpass response from {overpass_url}")
445
+ data = None
446
+ continue
447
+ # Success
448
+ break
449
+ except Exception as e:
450
+ last_error = e
451
+ continue
452
+ if data is None:
453
+ raise RuntimeError(f"Failed to fetch OSM data from Overpass endpoints. Last error: {last_error}")
418
454
 
419
455
  # Build a mapping from (type, id) to element
420
456
  id_map = {}
@@ -932,14 +968,43 @@ def load_land_cover_gdf_from_osm(rectangle_vertices_ori):
932
968
  "out skel qt;"
933
969
  )
934
970
 
935
- # Overpass API endpoint
936
- overpass_url = "http://overpass-api.de/api/interpreter"
971
+ # Overpass API endpoints (fallbacks)
972
+ overpass_endpoints = [
973
+ "https://overpass-api.de/api/interpreter",
974
+ "https://overpass.kumi.systems/api/interpreter",
975
+ "https://overpass.openstreetmap.ru/api/interpreter",
976
+ ]
937
977
 
938
- # Fetch data from Overpass API
978
+ # Fetch data from Overpass API with fallbacks and robust JSON handling
939
979
  print("Fetching data from Overpass API...")
940
- response = requests.get(overpass_url, params={'data': query})
941
- response.raise_for_status()
942
- data = response.json()
980
+ headers = {"User-Agent": "voxcity/ci (https://github.com/voxcity)"}
981
+ data = None
982
+ last_error = None
983
+ for overpass_url in overpass_endpoints:
984
+ try:
985
+ response = requests.get(overpass_url, params={'data': query}, headers=headers, timeout=30)
986
+ if response.status_code != 200:
987
+ last_error = Exception(f"HTTP {response.status_code} from {overpass_url}")
988
+ continue
989
+ content_type = response.headers.get("Content-Type", "")
990
+ if "json" not in content_type.lower():
991
+ try:
992
+ data = response.json()
993
+ except Exception as e:
994
+ last_error = e
995
+ continue
996
+ else:
997
+ data = response.json()
998
+ if not isinstance(data, dict) or 'elements' not in data:
999
+ last_error = Exception(f"Malformed Overpass response from {overpass_url}")
1000
+ data = None
1001
+ continue
1002
+ break
1003
+ except Exception as e:
1004
+ last_error = e
1005
+ continue
1006
+ if data is None:
1007
+ raise RuntimeError(f"Failed to fetch OSM data from Overpass endpoints. Last error: {last_error}")
943
1008
 
944
1009
  # Convert OSM data to GeoJSON format using our custom converter instead of json2geojson
945
1010
  print("Converting data to GeoJSON format...")