voxcity 0.6.26__tar.gz → 0.6.27__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.26 → voxcity-0.6.27}/PKG-INFO +1 -1
  2. {voxcity-0.6.26 → voxcity-0.6.27}/pyproject.toml +1 -1
  3. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/downloader/__init__.py +2 -1
  4. voxcity-0.6.27/src/voxcity/downloader/gba.py +210 -0
  5. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/generator.py +17 -2
  6. {voxcity-0.6.26 → voxcity-0.6.27}/AUTHORS.rst +0 -0
  7. {voxcity-0.6.26 → voxcity-0.6.27}/LICENSE +0 -0
  8. {voxcity-0.6.26 → voxcity-0.6.27}/README.md +0 -0
  9. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/__init__.py +0 -0
  10. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/downloader/citygml.py +0 -0
  11. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/downloader/eubucco.py +0 -0
  12. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/downloader/gee.py +0 -0
  13. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/downloader/mbfp.py +0 -0
  14. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/downloader/oemj.py +0 -0
  15. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/downloader/osm.py +0 -0
  16. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/downloader/overture.py +0 -0
  17. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/downloader/utils.py +0 -0
  18. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/exporter/__init__.py +0 -0
  19. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/exporter/cityles.py +0 -0
  20. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/exporter/envimet.py +0 -0
  21. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/exporter/magicavoxel.py +0 -0
  22. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/exporter/netcdf.py +0 -0
  23. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/exporter/obj.py +0 -0
  24. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/geoprocessor/__init__.py +0 -0
  25. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/geoprocessor/draw.py +0 -0
  26. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/geoprocessor/grid.py +0 -0
  27. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/geoprocessor/mesh.py +0 -0
  28. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/geoprocessor/network.py +0 -0
  29. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/geoprocessor/polygon.py +0 -0
  30. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/geoprocessor/utils.py +0 -0
  31. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/simulator/__init__.py +0 -0
  32. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/simulator/solar.py +0 -0
  33. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/simulator/utils.py +0 -0
  34. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/simulator/view.py +0 -0
  35. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/utils/__init__.py +0 -0
  36. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/utils/lc.py +0 -0
  37. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/utils/material.py +0 -0
  38. {voxcity-0.6.26 → voxcity-0.6.27}/src/voxcity/utils/visualization.py +0 -0
  39. {voxcity-0.6.26 → voxcity-0.6.27}/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.26
3
+ Version: 0.6.27
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.26"
3
+ version = "0.6.27"
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"
@@ -4,4 +4,5 @@ from .gee import *
4
4
  from .osm import *
5
5
  from .oemj import *
6
6
  from .eubucco import *
7
- from .overture import *
7
+ from .overture import *
8
+ from .gba import *
@@ -0,0 +1,210 @@
1
+ """
2
+ Downloader for Global Building Atlas (GBA) LOD1 polygons.
3
+
4
+ This module downloads GeoParquet tiles from the Global Building Atlas (GBA)
5
+ hosted at data.source.coop, selects tiles intersecting a user-specified
6
+ rectangle, loads them into a GeoDataFrame, and filters features to the
7
+ rectangle extent.
8
+
9
+ Tile scheme:
10
+ - Global 5x5-degree tiles named like: e010_n50_e015_n45.parquet
11
+ - longitudes: e/w with 3-digit zero padding (e.g., e010, w060)
12
+ - latitudes: n/s with 2-digit zero padding (e.g., n50, s25)
13
+ - filename order: west_lon, north_lat, east_lon, south_lat
14
+
15
+ Usage:
16
+ gdf = load_gdf_from_gba(rectangle_vertices=[(lon1, lat1), (lon2, lat2), ...])
17
+
18
+ Notes:
19
+ - Output CRS is EPSG:4326.
20
+ - Requires pyarrow or fastparquet for parquet reading via GeoPandas.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import math
26
+ import os
27
+ import tempfile
28
+ from typing import Iterable, List, Optional, Sequence, Tuple
29
+
30
+ import geopandas as gpd
31
+ import pandas as pd
32
+ import requests
33
+ from shapely.geometry import Polygon
34
+
35
+
36
+ def _bbox_from_rectangle_vertices(vertices: Sequence[Tuple[float, float]]) -> Tuple[float, float, float, float]:
37
+ """
38
+ Convert rectangle vertices in (lon, lat) into bbox as (min_lon, min_lat, max_lon, max_lat).
39
+ """
40
+ if not vertices:
41
+ raise ValueError("rectangle_vertices must be a non-empty sequence of (lon, lat)")
42
+ lons = [v[0] for v in vertices]
43
+ lats = [v[1] for v in vertices]
44
+ return (min(lons), min(lats), max(lons), max(lats))
45
+
46
+
47
+ def _pad_lon(deg: int) -> str:
48
+ return f"{abs(deg):03d}"
49
+
50
+
51
+ def _pad_lat(deg: int) -> str:
52
+ return f"{abs(deg):02d}"
53
+
54
+
55
+ def _lon_tag(deg: int) -> str:
56
+ return ("e" if deg >= 0 else "w") + _pad_lon(deg)
57
+
58
+
59
+ def _lat_tag(deg: int) -> str:
60
+ return ("n" if deg >= 0 else "s") + _pad_lat(deg)
61
+
62
+
63
+ def _snap_down(value: float, step: int) -> int:
64
+ return int(math.floor(value / step) * step)
65
+
66
+
67
+ def _snap_up(value: float, step: int) -> int:
68
+ return int(math.ceil(value / step) * step)
69
+
70
+
71
+ def _generate_tile_bounds_for_bbox(
72
+ min_lon: float, min_lat: float, max_lon: float, max_lat: float, tile_size_deg: int = 5
73
+ ) -> Iterable[Tuple[int, int, int, int]]:
74
+ """
75
+ Generate 5-degree tile bounds (west, south, east, north) covering bbox.
76
+ All values are integer degrees aligned to tile_size_deg.
77
+ """
78
+ west = _snap_down(min_lon, tile_size_deg)
79
+ east = _snap_up(max_lon, tile_size_deg)
80
+ south = _snap_down(min_lat, tile_size_deg)
81
+ north = _snap_up(max_lat, tile_size_deg)
82
+
83
+ for lon in range(west, east, tile_size_deg):
84
+ for lat in range(south, north, tile_size_deg):
85
+ yield (lon, lat, lon + tile_size_deg, lat + tile_size_deg)
86
+
87
+
88
+ def _tile_filename(west: int, south: int, east: int, north: int) -> str:
89
+ """
90
+ Construct GBA tile filename for given integer-degree bounds.
91
+ Naming convention examples:
92
+ e010_n50_e015_n45.parquet
93
+ e140_s25_e145_s30.parquet
94
+ w060_s30_w055_s35.parquet
95
+ """
96
+ return f"{_lon_tag(west)}_{_lat_tag(north)}_{_lon_tag(east)}_{_lat_tag(south)}.parquet"
97
+
98
+
99
+ def _tile_url(base_url: str, west: int, south: int, east: int, north: int) -> str:
100
+ filename = _tile_filename(west, south, east, north)
101
+ return f"{base_url.rstrip('/')}/{filename}"
102
+
103
+
104
+ def _download_parquet(url: str, download_dir: str, timeout: int = 60) -> Optional[str]:
105
+ """
106
+ Download a parquet file to download_dir. Returns local filepath or None if not found.
107
+ """
108
+ try:
109
+ with requests.get(url, stream=True, timeout=timeout) as r:
110
+ if r.status_code != 200:
111
+ return None
112
+ filename = os.path.basename(url)
113
+ local_path = os.path.join(download_dir, filename)
114
+ with open(local_path, "wb") as f:
115
+ for chunk in r.iter_content(chunk_size=1024 * 1024):
116
+ if chunk:
117
+ f.write(chunk)
118
+ return local_path
119
+ except requests.RequestException:
120
+ return None
121
+
122
+
123
+ def _filter_to_rectangle(gdf: gpd.GeoDataFrame, rectangle: Polygon, clip: bool) -> gpd.GeoDataFrame:
124
+ gdf = gdf[gdf.geometry.notnull()].copy()
125
+ # Ensure CRS is WGS84
126
+ if gdf.crs is None:
127
+ gdf.set_crs(epsg=4326, inplace=True)
128
+ elif gdf.crs.to_epsg() != 4326:
129
+ gdf = gdf.to_crs(epsg=4326)
130
+
131
+ intersects = gdf.intersects(rectangle)
132
+ gdf = gdf[intersects].copy()
133
+ if clip and not gdf.empty:
134
+ # GeoPandas clip performs overlay to trim geometries to rectangle
135
+ gdf = gpd.clip(gdf, gpd.GeoSeries([rectangle], crs="EPSG:4326").to_frame("geometry"))
136
+ return gdf
137
+
138
+
139
+ def load_gdf_from_gba(
140
+ rectangle_vertices: Sequence[Tuple[float, float]],
141
+ base_url: str = "https://data.source.coop/tge-labs/globalbuildingatlas-lod1",
142
+ download_dir: Optional[str] = None,
143
+ clip_to_rectangle: bool = False,
144
+ ) -> Optional[gpd.GeoDataFrame]:
145
+ """
146
+ Download GBA tiles intersecting a rectangle and return combined GeoDataFrame.
147
+
148
+ Args:
149
+ rectangle_vertices: Sequence of (lon, lat) defining the area of interest.
150
+ base_url: Base URL hosting GBA parquet tiles.
151
+ download_dir: Optional directory to store downloaded tiles. If None, a
152
+ temporary directory is used and cleaned up by the OS later.
153
+ clip_to_rectangle: If True, geometries are clipped to rectangle extent.
154
+
155
+ Returns:
156
+ GeoDataFrame with EPSG:4326 geometry and an 'id' column, or None if no data.
157
+ """
158
+ min_lon, min_lat, max_lon, max_lat = _bbox_from_rectangle_vertices(rectangle_vertices)
159
+ rectangle = Polygon([
160
+ (min_lon, min_lat),
161
+ (max_lon, min_lat),
162
+ (max_lon, max_lat),
163
+ (min_lon, max_lat),
164
+ (min_lon, min_lat),
165
+ ])
166
+
167
+ tmp_dir_created = False
168
+ if download_dir is None:
169
+ download_dir = tempfile.mkdtemp(prefix="gba_tiles_")
170
+ tmp_dir_created = True
171
+ else:
172
+ os.makedirs(download_dir, exist_ok=True)
173
+
174
+ local_files: List[str] = []
175
+ for west, south, east, north in _generate_tile_bounds_for_bbox(min_lon, min_lat, max_lon, max_lat):
176
+ url = _tile_url(base_url, west, south, east, north)
177
+ local = _download_parquet(url, download_dir)
178
+ if local is not None:
179
+ local_files.append(local)
180
+
181
+ if not local_files:
182
+ return None
183
+
184
+ gdfs: List[gpd.GeoDataFrame] = []
185
+ for path in local_files:
186
+ try:
187
+ # GeoParquet read
188
+ gdf = gpd.read_parquet(path)
189
+ if gdf is not None and not gdf.empty:
190
+ gdfs.append(gdf)
191
+ except Exception:
192
+ # Skip unreadable tiles
193
+ continue
194
+
195
+ if not gdfs:
196
+ return None
197
+
198
+ combined = pd.concat(gdfs, ignore_index=True)
199
+ combined = gpd.GeoDataFrame(combined, geometry="geometry")
200
+ combined = _filter_to_rectangle(combined, rectangle, clip=clip_to_rectangle)
201
+
202
+ if combined.empty:
203
+ return None
204
+
205
+ # Ensure sequential ids
206
+ combined["id"] = combined.index.astype(int)
207
+ combined.set_crs(epsg=4326, inplace=True, allow_override=True)
208
+ return combined
209
+
210
+
@@ -48,6 +48,7 @@ from .downloader.oemj import save_oemj_as_geotiff
48
48
  from .downloader.eubucco import load_gdf_from_eubucco
49
49
  from .downloader.overture import load_gdf_from_overture
50
50
  from .downloader.citygml import load_buid_dem_veg_from_citygml
51
+ from .downloader.gba import load_gdf_from_gba
51
52
 
52
53
  # Google Earth Engine related imports - for satellite and elevation data
53
54
  from .downloader.gee import (
@@ -206,8 +207,9 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, b
206
207
  - list: Filtered building features
207
208
  """
208
209
 
209
- # Initialize Earth Engine for satellite-based building data sources
210
- if source not in ["OpenStreetMap", "Overture", "Local file", "GeoDataFrame"]:
210
+ # Initialize Earth Engine only for building sources that require it
211
+ ee_required_sources = {"Open Building 2.5D Temporal"}
212
+ if source in ee_required_sources:
211
213
  initialize_earth_engine()
212
214
 
213
215
  print("Creating Building Height grid\n ")
@@ -243,6 +245,11 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, b
243
245
  elif source == "Overture":
244
246
  # Open building dataset from Overture Maps Foundation
245
247
  gdf = load_gdf_from_overture(rectangle_vertices, floor_height=floor_height)
248
+ elif source in ("GBA", "Global Building Atlas"):
249
+ # Global Building Atlas LOD1 polygons (GeoParquet tiles)
250
+ clip_gba = kwargs.get("gba_clip", False)
251
+ gba_download_dir = kwargs.get("gba_download_dir")
252
+ gdf = load_gdf_from_gba(rectangle_vertices, download_dir=gba_download_dir, clip_to_rectangle=clip_gba)
246
253
  elif source == "Local file":
247
254
  # Handle user-provided local building data files
248
255
  _, extension = os.path.splitext(kwargs["building_path"])
@@ -290,6 +297,10 @@ def get_building_height_grid(rectangle_vertices, meshsize, source, output_dir, b
290
297
  # gdf_comp = load_gdf_from_openmaptiles(rectangle_vertices, kwargs["maptiler_API_key"])
291
298
  elif building_complementary_source == "Overture":
292
299
  gdf_comp = load_gdf_from_overture(rectangle_vertices, floor_height=floor_height)
300
+ elif building_complementary_source in ("GBA", "Global Building Atlas"):
301
+ clip_gba = kwargs.get("gba_clip", False)
302
+ gba_download_dir = kwargs.get("gba_download_dir")
303
+ gdf_comp = load_gdf_from_gba(rectangle_vertices, download_dir=gba_download_dir, clip_to_rectangle=clip_gba)
293
304
  elif building_complementary_source == "Local file":
294
305
  _, extension = os.path.splitext(kwargs["building_complementary_path"])
295
306
  if extension == ".gpkg":
@@ -979,6 +990,10 @@ def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_sou
979
990
  gdf_comp = load_gdf_from_eubucco(rectangle_vertices, kwargs.get("output_dir", "output"))
980
991
  elif building_complementary_source == 'Overture':
981
992
  gdf_comp = load_gdf_from_overture(rectangle_vertices, floor_height=floor_height)
993
+ elif building_complementary_source in ("GBA", "Global Building Atlas"):
994
+ clip_gba = kwargs.get("gba_clip", False)
995
+ gba_download_dir = kwargs.get("gba_download_dir")
996
+ gdf_comp = load_gdf_from_gba(rectangle_vertices, download_dir=gba_download_dir, clip_to_rectangle=clip_gba)
982
997
  elif building_complementary_source == 'Local file':
983
998
  comp_path = kwargs.get("building_complementary_path")
984
999
  if comp_path is not None:
File without changes
File without changes
File without changes