voxcity 0.6.26__tar.gz → 0.6.28__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.
- {voxcity-0.6.26 → voxcity-0.6.28}/PKG-INFO +1 -1
- {voxcity-0.6.26 → voxcity-0.6.28}/pyproject.toml +1 -1
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/downloader/__init__.py +2 -1
- voxcity-0.6.28/src/voxcity/downloader/gba.py +210 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/generator.py +17 -2
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/utils/visualization.py +1 -1
- {voxcity-0.6.26 → voxcity-0.6.28}/AUTHORS.rst +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/LICENSE +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/README.md +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/__init__.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/downloader/citygml.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/downloader/eubucco.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/downloader/gee.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/downloader/mbfp.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/downloader/oemj.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/downloader/osm.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/downloader/overture.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/downloader/utils.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/exporter/__init__.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/exporter/cityles.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/exporter/envimet.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/exporter/magicavoxel.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/exporter/netcdf.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/exporter/obj.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/geoprocessor/__init__.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/geoprocessor/draw.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/geoprocessor/grid.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/geoprocessor/mesh.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/geoprocessor/network.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/geoprocessor/polygon.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/geoprocessor/utils.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/simulator/__init__.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/simulator/solar.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/simulator/utils.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/simulator/view.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/utils/__init__.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/utils/lc.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/utils/material.py +0 -0
- {voxcity-0.6.26 → voxcity-0.6.28}/src/voxcity/utils/weather.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "voxcity"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.28"
|
|
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"
|
|
@@ -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
|
|
210
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|