voxcity 0.6.27__tar.gz → 0.6.29__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 (39) hide show
  1. {voxcity-0.6.27 → voxcity-0.6.29}/PKG-INFO +1 -1
  2. {voxcity-0.6.27 → voxcity-0.6.29}/pyproject.toml +1 -1
  3. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/downloader/oemj.py +80 -8
  4. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/exporter/obj.py +26 -22
  5. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/generator.py +20 -1
  6. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/utils/visualization.py +1 -1
  7. {voxcity-0.6.27 → voxcity-0.6.29}/AUTHORS.rst +0 -0
  8. {voxcity-0.6.27 → voxcity-0.6.29}/LICENSE +0 -0
  9. {voxcity-0.6.27 → voxcity-0.6.29}/README.md +0 -0
  10. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/__init__.py +0 -0
  11. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/downloader/__init__.py +0 -0
  12. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/downloader/citygml.py +0 -0
  13. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/downloader/eubucco.py +0 -0
  14. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/downloader/gba.py +0 -0
  15. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/downloader/gee.py +0 -0
  16. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/downloader/mbfp.py +0 -0
  17. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/downloader/osm.py +0 -0
  18. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/downloader/overture.py +0 -0
  19. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/downloader/utils.py +0 -0
  20. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/exporter/__init__.py +0 -0
  21. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/exporter/cityles.py +0 -0
  22. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/exporter/envimet.py +0 -0
  23. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/exporter/magicavoxel.py +0 -0
  24. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/exporter/netcdf.py +0 -0
  25. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/geoprocessor/__init__.py +0 -0
  26. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/geoprocessor/draw.py +0 -0
  27. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/geoprocessor/grid.py +0 -0
  28. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/geoprocessor/mesh.py +0 -0
  29. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/geoprocessor/network.py +0 -0
  30. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/geoprocessor/polygon.py +0 -0
  31. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/geoprocessor/utils.py +0 -0
  32. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/simulator/__init__.py +0 -0
  33. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/simulator/solar.py +0 -0
  34. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/simulator/utils.py +0 -0
  35. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/simulator/view.py +0 -0
  36. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/utils/__init__.py +0 -0
  37. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/utils/lc.py +0 -0
  38. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/utils/material.py +0 -0
  39. {voxcity-0.6.27 → voxcity-0.6.29}/src/voxcity/utils/weather.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voxcity
3
- Version: 0.6.27
3
+ Version: 0.6.29
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
  License-File: AUTHORS.rst
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "voxcity"
3
- version = "0.6.27"
3
+ version = "0.6.29"
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"
@@ -19,6 +19,7 @@ Example Usage:
19
19
  """
20
20
 
21
21
  import requests
22
+ import os
22
23
  from PIL import Image, ImageDraw
23
24
  from io import BytesIO
24
25
  import math
@@ -73,7 +74,7 @@ def num2deg(xtile, ytile, zoom):
73
74
  lat_deg = math.degrees(lat_rad)
74
75
  return (lon_deg, lat_deg)
75
76
 
76
- def download_tiles(polygon, zoom):
77
+ def download_tiles(polygon, zoom, *, ssl_verify=True, allow_insecure_ssl=False, allow_http_fallback=False, timeout_s=30):
77
78
  """Download satellite imagery tiles covering a polygon region.
78
79
 
79
80
  Downloads all tiles that intersect with the given polygon at the specified zoom level
@@ -112,11 +113,70 @@ def download_tiles(polygon, zoom):
112
113
  for x in range(min(min_x, max_x), max(min_x, max_x) + 1):
113
114
  for y in range(min(min_y, max_y), max(min_y, max_y) + 1):
114
115
  url = f"https://www.open-earth-map.org/demo/Japan/{zoom}/{x}/{y}.png"
115
- response = requests.get(url)
116
- if response.status_code == 200:
117
- tiles[(x, y)] = Image.open(BytesIO(response.content))
118
- else:
119
- print(f"Failed to download tile: {url}")
116
+ # Try secure HTTPS first with user-provided verification option
117
+ content = None
118
+ try:
119
+ resp = requests.get(url, timeout=timeout_s, verify=ssl_verify)
120
+ if resp.status_code == 200:
121
+ content = resp.content
122
+ else:
123
+ print(f"Failed to download tile (status {resp.status_code}): {url}")
124
+ except requests.exceptions.SSLError:
125
+ # Optionally retry with certificate verification disabled
126
+ if allow_insecure_ssl:
127
+ try:
128
+ resp = requests.get(url, timeout=timeout_s, verify=False)
129
+ if resp.status_code == 200:
130
+ content = resp.content
131
+ else:
132
+ print(f"Failed to download tile (status {resp.status_code}) with insecure SSL: {url}")
133
+ except requests.exceptions.RequestException as e:
134
+ # Optionally try HTTP fallback
135
+ if allow_http_fallback and url.lower().startswith("https://"):
136
+ http_url = "http://" + url.split("://", 1)[1]
137
+ try:
138
+ resp = requests.get(http_url, timeout=timeout_s)
139
+ if resp.status_code == 200:
140
+ content = resp.content
141
+ else:
142
+ print(f"Failed to download tile over HTTP (status {resp.status_code}): {http_url}")
143
+ except requests.exceptions.RequestException as e2:
144
+ print(f"HTTP fallback failed for tile: {http_url} ({e2})")
145
+ else:
146
+ print(f"SSL error downloading tile: {url} ({e})")
147
+ else:
148
+ if allow_http_fallback and url.lower().startswith("https://"):
149
+ http_url = "http://" + url.split("://", 1)[1]
150
+ try:
151
+ resp = requests.get(http_url, timeout=timeout_s)
152
+ if resp.status_code == 200:
153
+ content = resp.content
154
+ else:
155
+ print(f"Failed to download tile over HTTP (status {resp.status_code}): {http_url}")
156
+ except requests.exceptions.RequestException as e:
157
+ print(f"HTTP fallback failed for tile: {http_url} ({e})")
158
+ else:
159
+ print(f"SSL error downloading tile: {url}")
160
+ except requests.exceptions.RequestException as e:
161
+ # Network error (timeout/connection). Try HTTP if allowed.
162
+ if allow_http_fallback and url.lower().startswith("https://"):
163
+ http_url = "http://" + url.split("://", 1)[1]
164
+ try:
165
+ resp = requests.get(http_url, timeout=timeout_s)
166
+ if resp.status_code == 200:
167
+ content = resp.content
168
+ else:
169
+ print(f"Failed to download tile over HTTP (status {resp.status_code}): {http_url}")
170
+ except requests.exceptions.RequestException as e2:
171
+ print(f"HTTP fallback failed for tile: {http_url} ({e2})")
172
+ else:
173
+ print(f"Error downloading tile: {url} ({e})")
174
+
175
+ if content is not None:
176
+ try:
177
+ tiles[(x, y)] = Image.open(BytesIO(content))
178
+ except Exception as e:
179
+ print(f"Error decoding tile image for {url}: {e}")
120
180
 
121
181
  return tiles, (min(min_x, max_x), min(min_y, max_y), max(min_x, max_x), max(min_y, max_y))
122
182
 
@@ -231,6 +291,11 @@ def save_as_geotiff(image, polygon, zoom, bbox, bounds, output_path):
231
291
  pixel_size_x = (lower_right_x - upper_left_x) / image.width
232
292
  pixel_size_y = (upper_left_y - lower_right_y) / image.height
233
293
 
294
+ # Ensure output directory exists
295
+ out_dir = os.path.dirname(output_path)
296
+ if out_dir:
297
+ os.makedirs(out_dir, exist_ok=True)
298
+
234
299
  # Create GeoTIFF
235
300
  driver = gdal.GetDriverByName('GTiff')
236
301
  dataset = driver.Create(output_path, image.width, image.height, 3, gdal.GDT_Byte)
@@ -249,7 +314,7 @@ def save_as_geotiff(image, polygon, zoom, bbox, bounds, output_path):
249
314
 
250
315
  dataset = None
251
316
 
252
- def save_oemj_as_geotiff(polygon, filepath, zoom=16):
317
+ def save_oemj_as_geotiff(polygon, filepath, zoom=16, *, ssl_verify=True, allow_insecure_ssl=False, allow_http_fallback=False, timeout_s=30):
253
318
  """Download and save OpenEarthMap Japan imagery as a georeferenced GeoTIFF file.
254
319
 
255
320
  This is the main function that orchestrates the entire process of downloading,
@@ -281,7 +346,14 @@ def save_oemj_as_geotiff(polygon, filepath, zoom=16):
281
346
  - The output GeoTIFF will be in Web Mercator projection (EPSG:3857)
282
347
  """
283
348
  try:
284
- tiles, bounds = download_tiles(polygon, zoom)
349
+ tiles, bounds = download_tiles(
350
+ polygon,
351
+ zoom,
352
+ ssl_verify=ssl_verify,
353
+ allow_insecure_ssl=allow_insecure_ssl,
354
+ allow_http_fallback=allow_http_fallback,
355
+ timeout_s=timeout_s,
356
+ )
285
357
  if not tiles:
286
358
  raise ValueError("No tiles were downloaded. Please check the polygon coordinates and zoom level.")
287
359
 
@@ -681,6 +681,7 @@ def export_netcdf_to_obj(
681
681
  classes_to_show=None,
682
682
  voxel_color_scheme="default",
683
683
  max_faces_warn=1_000_000,
684
+ export_vox_base=True,
684
685
  ):
685
686
  """
686
687
  Export two OBJ/MTL files using the same local meter frame:
@@ -718,6 +719,8 @@ def export_netcdf_to_obj(
718
719
  classes_to_show (set[int]|None): Optional subset of voxel classes to export; None -> all present (except 0).
719
720
  voxel_color_scheme (str): Color scheme name passed to get_voxel_color_map.
720
721
  max_faces_warn (int): Warn if a single class exceeds this many faces.
722
+ export_vox_base (bool): If False, skip exporting VoxCity OBJ/MTL; VoxCity input
723
+ is still used to define the shared coordinate system for scalar OBJ.
721
724
 
722
725
  Returns:
723
726
  dict: Paths of written files: keys 'vox_obj','vox_mtl','tm_obj','tm_mtl' (values may be None).
@@ -1345,27 +1348,28 @@ def export_netcdf_to_obj(
1345
1348
  vox_meshes = {}
1346
1349
  tm_meshes = {}
1347
1350
 
1348
- present = set(np.unique(Av_kji))
1349
- present.discard(0)
1350
- if classes_to_show is not None:
1351
- present &= set(classes_to_show)
1352
- present = sorted(present)
1353
-
1354
- faces_total = 0
1355
- voxel_color_map = get_voxel_color_map(color_scheme=voxel_color_scheme)
1356
- for cls in present:
1357
- mask = Av_kji == cls
1358
- if not np.any(mask):
1359
- continue
1360
- rgb = voxel_color_map.get(int(cls), [200, 200, 200])
1361
- if greedy_vox:
1362
- m_cls, faces = make_voxel_mesh_uniform_color_greedy(mask, Xv_s, Yv_s, Zv_s, rgb=rgb, name=f"class_{int(cls)}")
1363
- else:
1364
- m_cls, faces = make_voxel_mesh_uniform_color(mask, Xv_s, Yv_s, Zv_s, rgb=rgb, name=f"class_{int(cls)}")
1365
- if m_cls is not None:
1366
- vox_meshes[f"voxclass_{int(cls)}"] = m_cls
1367
- faces_total += faces
1368
- print(f"[VoxCity] total voxel faces: {faces_total:,}")
1351
+ if export_vox_base:
1352
+ present = set(np.unique(Av_kji))
1353
+ present.discard(0)
1354
+ if classes_to_show is not None:
1355
+ present &= set(classes_to_show)
1356
+ present = sorted(present)
1357
+
1358
+ faces_total = 0
1359
+ voxel_color_map = get_voxel_color_map(color_scheme=voxel_color_scheme)
1360
+ for cls in present:
1361
+ mask = Av_kji == cls
1362
+ if not np.any(mask):
1363
+ continue
1364
+ rgb = voxel_color_map.get(int(cls), [200, 200, 200])
1365
+ if greedy_vox:
1366
+ m_cls, faces = make_voxel_mesh_uniform_color_greedy(mask, Xv_s, Yv_s, Zv_s, rgb=rgb, name=f"class_{int(cls)}")
1367
+ else:
1368
+ m_cls, faces = make_voxel_mesh_uniform_color(mask, Xv_s, Yv_s, Zv_s, rgb=rgb, name=f"class_{int(cls)}")
1369
+ if m_cls is not None:
1370
+ vox_meshes[f"voxclass_{int(cls)}"] = m_cls
1371
+ faces_total += faces
1372
+ print(f"[VoxCity] total voxel faces: {faces_total:,}")
1369
1373
 
1370
1374
  iso_meshes = build_tm_isosurfaces_regular_grid(
1371
1375
  A_scalar=A_s,
@@ -1390,7 +1394,7 @@ def export_netcdf_to_obj(
1390
1394
 
1391
1395
  os.makedirs(output_dir, exist_ok=True)
1392
1396
  obj_vox = mtl_vox = obj_tm = mtl_tm = None
1393
- if vox_meshes:
1397
+ if export_vox_base and vox_meshes:
1394
1398
  obj_vox, mtl_vox = save_obj_with_mtl_and_normals(vox_meshes, output_dir, vox_base_filename)
1395
1399
  if tm_meshes:
1396
1400
  obj_tm, mtl_tm = save_obj_with_mtl_and_normals(tm_meshes, output_dir, tm_base_filename)
@@ -147,7 +147,26 @@ def get_land_cover_grid(rectangle_vertices, meshsize, source, output_dir, **kwar
147
147
  save_geotiff_dynamic_world_v1(roi, geotiff_path, dynamic_world_date)
148
148
  elif source == 'OpenEarthMapJapan':
149
149
  # Japan-specific land cover dataset
150
- save_oemj_as_geotiff(rectangle_vertices, geotiff_path)
150
+ # Allow SSL/HTTP options to be passed through kwargs
151
+ ssl_verify = kwargs.get('ssl_verify', kwargs.get('verify', True))
152
+ allow_insecure_ssl = kwargs.get('allow_insecure_ssl', False)
153
+ allow_http_fallback = kwargs.get('allow_http_fallback', False)
154
+ timeout_s = kwargs.get('timeout', 30)
155
+
156
+ save_oemj_as_geotiff(
157
+ rectangle_vertices,
158
+ geotiff_path,
159
+ ssl_verify=ssl_verify,
160
+ allow_insecure_ssl=allow_insecure_ssl,
161
+ allow_http_fallback=allow_http_fallback,
162
+ timeout_s=timeout_s,
163
+ )
164
+ # Ensure the file was actually created before proceeding
165
+ if not os.path.exists(geotiff_path):
166
+ raise FileNotFoundError(
167
+ f"OEMJ download failed; expected GeoTIFF not found: {geotiff_path}. "
168
+ "You can try setting ssl_verify=False or allow_http_fallback=True in kwargs."
169
+ )
151
170
  elif source == 'OpenStreetMap':
152
171
  # Vector-based land cover from OpenStreetMap
153
172
  # This bypasses the GeoTIFF workflow and gets data directly as GeoJSON
@@ -2830,7 +2830,7 @@ def visualize_voxcity_plotly(
2830
2830
  )
2831
2831
 
2832
2832
  fig.update_layout(
2833
- title=title or "VoxCity 3D",
2833
+ # title=title or "VoxCity 3D",
2834
2834
  width=width,
2835
2835
  height=height,
2836
2836
  scene=dict(
File without changes
File without changes
File without changes