voxcity 0.6.18__py3-none-any.whl → 0.6.20__py3-none-any.whl
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/downloader/citygml.py +32 -18
- voxcity/exporter/obj.py +1405 -1191
- voxcity/generator.py +15 -1
- voxcity/simulator/solar.py +12 -2
- voxcity/utils/weather.py +276 -55
- {voxcity-0.6.18.dist-info → voxcity-0.6.20.dist-info}/METADATA +4 -2
- {voxcity-0.6.18.dist-info → voxcity-0.6.20.dist-info}/RECORD +10 -10
- {voxcity-0.6.18.dist-info → voxcity-0.6.20.dist-info}/WHEEL +1 -1
- {voxcity-0.6.18.dist-info → voxcity-0.6.20.dist-info/licenses}/AUTHORS.rst +0 -0
- {voxcity-0.6.18.dist-info → voxcity-0.6.20.dist-info/licenses}/LICENSE +0 -0
voxcity/generator.py
CHANGED
|
@@ -921,8 +921,22 @@ def get_voxcity_CityGML(rectangle_vertices, land_cover_source, canopy_height_sou
|
|
|
921
921
|
# Remove 'output_dir' from kwargs to prevent duplication
|
|
922
922
|
kwargs.pop('output_dir', None)
|
|
923
923
|
|
|
924
|
+
# SSL/HTTP options for CityGML download (optional)
|
|
925
|
+
# Backward compatible: accept 'verify' but prefer 'ssl_verify'
|
|
926
|
+
ssl_verify = kwargs.pop('ssl_verify', kwargs.pop('verify', True))
|
|
927
|
+
ca_bundle = kwargs.pop('ca_bundle', None)
|
|
928
|
+
timeout = kwargs.pop('timeout', 60)
|
|
929
|
+
|
|
924
930
|
# get all required gdfs
|
|
925
|
-
building_gdf, terrain_gdf, vegetation_gdf = load_buid_dem_veg_from_citygml(
|
|
931
|
+
building_gdf, terrain_gdf, vegetation_gdf = load_buid_dem_veg_from_citygml(
|
|
932
|
+
url=url_citygml,
|
|
933
|
+
citygml_path=citygml_path,
|
|
934
|
+
base_dir=output_dir,
|
|
935
|
+
rectangle_vertices=rectangle_vertices,
|
|
936
|
+
ssl_verify=ssl_verify,
|
|
937
|
+
ca_bundle=ca_bundle,
|
|
938
|
+
timeout=timeout
|
|
939
|
+
)
|
|
926
940
|
|
|
927
941
|
# Normalize CRS to WGS84 (EPSG:4326) to ensure consistent operations downstream
|
|
928
942
|
try:
|
voxcity/simulator/solar.py
CHANGED
|
@@ -1007,10 +1007,15 @@ def get_global_solar_irradiance_using_epw(
|
|
|
1007
1007
|
output_dir=output_dir,
|
|
1008
1008
|
max_distance=max_distance,
|
|
1009
1009
|
extract_zip=True,
|
|
1010
|
-
load_data=True
|
|
1010
|
+
load_data=True,
|
|
1011
|
+
allow_insecure_ssl=kwargs.get("allow_insecure_ssl", False),
|
|
1012
|
+
allow_http_fallback=kwargs.get("allow_http_fallback", False),
|
|
1013
|
+
ssl_verify=kwargs.get("ssl_verify", True)
|
|
1011
1014
|
)
|
|
1012
1015
|
|
|
1013
1016
|
# Read EPW data
|
|
1017
|
+
if epw_file_path is None:
|
|
1018
|
+
raise RuntimeError("EPW file path is None. Set 'epw_file_path' or enable 'download_nearest_epw' and ensure network succeeds.")
|
|
1014
1019
|
df, lon, lat, tz, elevation_m = read_epw_for_solar_simulation(epw_file_path)
|
|
1015
1020
|
if df.empty:
|
|
1016
1021
|
raise ValueError("No data in EPW file.")
|
|
@@ -2030,10 +2035,15 @@ def get_building_global_solar_irradiance_using_epw(
|
|
|
2030
2035
|
output_dir=output_dir,
|
|
2031
2036
|
max_distance=max_distance,
|
|
2032
2037
|
extract_zip=True,
|
|
2033
|
-
load_data=True
|
|
2038
|
+
load_data=True,
|
|
2039
|
+
allow_insecure_ssl=kwargs.get("allow_insecure_ssl", False),
|
|
2040
|
+
allow_http_fallback=kwargs.get("allow_http_fallback", False),
|
|
2041
|
+
ssl_verify=kwargs.get("ssl_verify", True)
|
|
2034
2042
|
)
|
|
2035
2043
|
|
|
2036
2044
|
# Read EPW data
|
|
2045
|
+
if epw_file_path is None:
|
|
2046
|
+
raise RuntimeError("EPW file path is None. Set 'epw_file_path' or enable 'download_nearest_epw' and ensure network succeeds.")
|
|
2037
2047
|
df, lon, lat, tz, elevation_m = read_epw_for_solar_simulation(epw_file_path)
|
|
2038
2048
|
if df.empty:
|
|
2039
2049
|
raise ValueError("No data in EPW file.")
|
voxcity/utils/weather.py
CHANGED
|
@@ -209,7 +209,9 @@ def process_epw(epw_path: Union[str, Path]) -> Tuple[pd.DataFrame, Dict]:
|
|
|
209
209
|
# =============================================================================
|
|
210
210
|
|
|
211
211
|
def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float, output_dir: str = "./", max_distance: Optional[float] = None,
|
|
212
|
-
extract_zip: bool = True, load_data: bool = True, region: Optional[Union[str, List[str]]] = None
|
|
212
|
+
extract_zip: bool = True, load_data: bool = True, region: Optional[Union[str, List[str]]] = None,
|
|
213
|
+
allow_insecure_ssl: bool = False, allow_http_fallback: bool = False,
|
|
214
|
+
ssl_verify: Union[bool, str] = True) -> Tuple[Optional[str], Optional[pd.DataFrame], Optional[Dict]]:
|
|
213
215
|
"""
|
|
214
216
|
Download and process EPW weather file from Climate.OneBuilding.Org based on coordinates.
|
|
215
217
|
|
|
@@ -233,11 +235,19 @@ def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float,
|
|
|
233
235
|
If no stations within this distance, uses closest available.
|
|
234
236
|
extract_zip (bool): Whether to extract the ZIP file (default True)
|
|
235
237
|
load_data (bool): Whether to load the EPW data into a DataFrame (default True)
|
|
236
|
-
region (str or List[str], optional): Specific region(s) to scan for stations.
|
|
237
|
-
|
|
238
|
-
"
|
|
239
|
-
"Europe", "Antarctica"
|
|
238
|
+
region (str or List[str], optional): Specific region(s) or dataset(s) to scan for stations.
|
|
239
|
+
Regions: "Africa", "Asia", "South_America",
|
|
240
|
+
"North_and_Central_America", "Southwest_Pacific",
|
|
241
|
+
"Europe", "Antarctica".
|
|
242
|
+
Sub-datasets (can be used alone or auto-included by region):
|
|
243
|
+
"Japan", "India", "CSWD", "CityUHK", "PHIKO",
|
|
244
|
+
"Argentina", "INMET_TRY", "AMTUes", "BrazFuture",
|
|
245
|
+
plus legacy "Canada", "USA", "Caribbean" (Region 4).
|
|
246
|
+
Use "all" to scan every dataset.
|
|
240
247
|
If None, will auto-detect region based on coordinates.
|
|
248
|
+
allow_insecure_ssl (bool): If True, on SSL errors retry with certificate verification disabled.
|
|
249
|
+
allow_http_fallback (bool): If True, on SSL/network errors, also try HTTP (insecure) fallback.
|
|
250
|
+
ssl_verify (bool|str): Passed to requests as 'verify' parameter for HTTPS; can be False or CA bundle path.
|
|
241
251
|
|
|
242
252
|
Returns:
|
|
243
253
|
Tuple containing:
|
|
@@ -250,35 +260,83 @@ def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float,
|
|
|
250
260
|
requests.exceptions.RequestException: If network requests fail
|
|
251
261
|
"""
|
|
252
262
|
|
|
253
|
-
# Regional KML sources from Climate.OneBuilding.Org
|
|
254
|
-
# Each region maintains
|
|
263
|
+
# Regional KML sources from Climate.OneBuilding.Org (2024+ TMYx structure)
|
|
264
|
+
# Each WMO region maintains a primary KML in /sources with the naming pattern:
|
|
265
|
+
# Region{N}_{Name}_TMYx_EPW_Processing_locations.kml
|
|
266
|
+
# Keep sub-region keys for backward compatibility (mapping to the Region KML where applicable)
|
|
255
267
|
KML_SOURCES = {
|
|
256
|
-
|
|
257
|
-
"
|
|
268
|
+
# WMO Region 1
|
|
269
|
+
"Africa": "https://climate.onebuilding.org/sources/Region1_Africa_TMYx_EPW_Processing_locations.kml",
|
|
270
|
+
# WMO Region 2
|
|
271
|
+
"Asia": "https://climate.onebuilding.org/sources/Region2_Asia_TMYx_EPW_Processing_locations.kml",
|
|
272
|
+
# Subsets/datasets within Asia that still publish dedicated KMLs
|
|
258
273
|
"Japan": "https://climate.onebuilding.org/sources/JGMY_EPW_Processing_locations.kml",
|
|
259
274
|
"India": "https://climate.onebuilding.org/sources/ITMY_EPW_Processing_locations.kml",
|
|
275
|
+
"CSWD": "https://climate.onebuilding.org/sources/CSWD_EPW_Processing_locations.kml",
|
|
276
|
+
"CityUHK": "https://climate.onebuilding.org/sources/CityUHK_EPW_Processing_locations.kml",
|
|
277
|
+
"PHIKO": "https://climate.onebuilding.org/sources/PHIKO_EPW_Processing_locations.kml",
|
|
278
|
+
# WMO Region 3
|
|
279
|
+
"South_America": "https://climate.onebuilding.org/sources/Region3_South_America_TMYx_EPW_Processing_locations.kml",
|
|
280
|
+
# Historical/legacy dataset for Argentina maintained separately
|
|
260
281
|
"Argentina": "https://climate.onebuilding.org/sources/ArgTMY_EPW_Processing_locations.kml",
|
|
282
|
+
"INMET_TRY": "https://climate.onebuilding.org/sources/INMET_TRY_EPW_Processing_locations.kml",
|
|
283
|
+
"AMTUes": "https://climate.onebuilding.org/sources/AMTUes_EPW_Processing_locations.kml",
|
|
284
|
+
"BrazFuture": "https://climate.onebuilding.org/sources/BrazFuture_EPW_Processing_locations.kml",
|
|
285
|
+
# WMO Region 4 (use subregion KMLs; umbrella selection expands to these)
|
|
286
|
+
# Note: There is no single unified Region 4 KML in /sources as of 2024.
|
|
287
|
+
# Use these three subregion KMLs instead.
|
|
261
288
|
"Canada": "https://climate.onebuilding.org/sources/Region4_Canada_TMYx_EPW_Processing_locations.kml",
|
|
262
289
|
"USA": "https://climate.onebuilding.org/sources/Region4_USA_TMYx_EPW_Processing_locations.kml",
|
|
263
290
|
"Caribbean": "https://climate.onebuilding.org/sources/Region4_NA_CA_Caribbean_TMYx_EPW_Processing_locations.kml",
|
|
291
|
+
# WMO Region 5
|
|
264
292
|
"Southwest_Pacific": "https://climate.onebuilding.org/sources/Region5_Southwest_Pacific_TMYx_EPW_Processing_locations.kml",
|
|
293
|
+
# WMO Region 6
|
|
265
294
|
"Europe": "https://climate.onebuilding.org/sources/Region6_Europe_TMYx_EPW_Processing_locations.kml",
|
|
266
|
-
|
|
295
|
+
# WMO Region 7
|
|
296
|
+
"Antarctica": "https://climate.onebuilding.org/sources/Region7_Antarctica_TMYx_EPW_Processing_locations.kml",
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# Group region selections to include relevant sub-datasets automatically
|
|
300
|
+
REGION_DATASET_GROUPS = {
|
|
301
|
+
"Africa": ["Africa"],
|
|
302
|
+
"Asia": ["Asia", "Japan", "India", "CSWD", "CityUHK", "PHIKO"],
|
|
303
|
+
"South_America": ["South_America", "Argentina", "INMET_TRY", "AMTUes", "BrazFuture"],
|
|
304
|
+
"North_and_Central_America": ["North_and_Central_America", "Canada", "USA", "Caribbean"],
|
|
305
|
+
"Southwest_Pacific": ["Southwest_Pacific"],
|
|
306
|
+
"Europe": ["Europe"],
|
|
307
|
+
"Antarctica": ["Antarctica"],
|
|
267
308
|
}
|
|
268
309
|
|
|
269
310
|
# Define approximate geographical boundaries for automatic region detection
|
|
270
311
|
# These bounds help determine which regional KML files to scan based on coordinates
|
|
271
312
|
REGION_BOUNDS = {
|
|
272
|
-
|
|
273
|
-
"
|
|
313
|
+
# WMO Region 1 - Africa (includes islands in Indian Ocean and Spanish territories off N. Africa)
|
|
314
|
+
"Africa": {"lon_min": -25, "lon_max": 80, "lat_min": -55, "lat_max": 45},
|
|
315
|
+
# WMO Region 2 - Asia (includes SE Asia, West Asia, Asian Russia, and BIOT)
|
|
316
|
+
"Asia": {"lon_min": 20, "lon_max": 180, "lat_min": -10, "lat_max": 80},
|
|
317
|
+
# Subsets
|
|
274
318
|
"Japan": {"lon_min": 127, "lon_max": 146, "lat_min": 24, "lat_max": 46},
|
|
275
319
|
"India": {"lon_min": 68, "lon_max": 97, "lat_min": 6, "lat_max": 36},
|
|
320
|
+
# WMO Region 3 - South America (includes Falklands, South Georgia/Sandwich, Galapagos)
|
|
321
|
+
"South_America": {"lon_min": -92, "lon_max": -20, "lat_min": -60, "lat_max": 15},
|
|
322
|
+
# Legacy/compatibility subset
|
|
276
323
|
"Argentina": {"lon_min": -75, "lon_max": -53, "lat_min": -55, "lat_max": -22},
|
|
324
|
+
# WMO Region 4 - North and Central America (includes Greenland and Caribbean)
|
|
325
|
+
"North_and_Central_America": {"lon_min": -180, "lon_max": 20, "lat_min": -10, "lat_max": 85},
|
|
326
|
+
# Backward-compatible subsets mapped to Region 4 KML
|
|
277
327
|
"Canada": {"lon_min": -141, "lon_max": -52, "lat_min": 42, "lat_max": 83},
|
|
278
328
|
"USA": {"lon_min": -170, "lon_max": -65, "lat_min": 20, "lat_max": 72},
|
|
279
329
|
"Caribbean": {"lon_min": -90, "lon_max": -59, "lat_min": 10, "lat_max": 27},
|
|
280
|
-
|
|
281
|
-
"
|
|
330
|
+
# WMO Region 5 - Southwest Pacific (covers SE Asia + Pacific islands + Hawaii via antimeridian)
|
|
331
|
+
"Southwest_Pacific": {
|
|
332
|
+
"boxes": [
|
|
333
|
+
{"lon_min": 90, "lon_max": 180, "lat_min": -50, "lat_max": 25},
|
|
334
|
+
{"lon_min": -180, "lon_max": -140, "lat_min": -50, "lat_max": 25},
|
|
335
|
+
]
|
|
336
|
+
},
|
|
337
|
+
# WMO Region 6 - Europe (includes Middle East countries listed and Greenland)
|
|
338
|
+
"Europe": {"lon_min": -75, "lon_max": 60, "lat_min": 25, "lat_max": 85},
|
|
339
|
+
# WMO Region 7 - Antarctica
|
|
282
340
|
"Antarctica": {"lon_min": -180, "lon_max": 180, "lat_min": -90, "lat_max": -60}
|
|
283
341
|
}
|
|
284
342
|
|
|
@@ -306,34 +364,45 @@ def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float,
|
|
|
306
364
|
elif lon > 180:
|
|
307
365
|
lon_adjusted = lon - 360
|
|
308
366
|
|
|
309
|
-
#
|
|
367
|
+
# Helper to test point within a single box
|
|
368
|
+
def _in_box(bx: Dict[str, float], lon_v: float, lat_v: float) -> bool:
|
|
369
|
+
return (bx["lon_min"] <= lon_v <= bx["lon_max"] and
|
|
370
|
+
bx["lat_min"] <= lat_v <= bx["lat_max"])
|
|
371
|
+
|
|
372
|
+
# Check if coordinates fall within any region bounds (support multi-box)
|
|
310
373
|
for region_name, bounds in REGION_BOUNDS.items():
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
374
|
+
if "boxes" in bounds:
|
|
375
|
+
for bx in bounds["boxes"]:
|
|
376
|
+
if _in_box(bx, lon_adjusted, lat):
|
|
377
|
+
matching_regions.append(region_name)
|
|
378
|
+
break
|
|
379
|
+
else:
|
|
380
|
+
if _in_box(bounds, lon_adjusted, lat):
|
|
381
|
+
matching_regions.append(region_name)
|
|
315
382
|
|
|
316
383
|
# If no regions matched, find the closest regions by boundary distance
|
|
317
384
|
if not matching_regions:
|
|
318
385
|
# Calculate "distance" to each region's boundary (simplified metric)
|
|
319
386
|
region_distances = []
|
|
320
387
|
for region_name, bounds in REGION_BOUNDS.items():
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
388
|
+
def _box_distance(bx: Dict[str, float]) -> float:
|
|
389
|
+
lon_dist = 0
|
|
390
|
+
if lon_adjusted < bx["lon_min"]:
|
|
391
|
+
lon_dist = bx["lon_min"] - lon_adjusted
|
|
392
|
+
elif lon_adjusted > bx["lon_max"]:
|
|
393
|
+
lon_dist = lon_adjusted - bx["lon_max"]
|
|
394
|
+
lat_dist = 0
|
|
395
|
+
if lat < bx["lat_min"]:
|
|
396
|
+
lat_dist = bx["lat_min"] - lat
|
|
397
|
+
elif lat > bx["lat_max"]:
|
|
398
|
+
lat_dist = lat - bx["lat_max"]
|
|
399
|
+
return (lon_dist**2 + lat_dist**2)**0.5
|
|
400
|
+
|
|
401
|
+
if "boxes" in bounds:
|
|
402
|
+
d = min(_box_distance(bx) for bx in bounds["boxes"])
|
|
403
|
+
else:
|
|
404
|
+
d = _box_distance(bounds)
|
|
405
|
+
region_distances.append((region_name, d))
|
|
337
406
|
|
|
338
407
|
# Get 3 closest regions to ensure we find stations
|
|
339
408
|
closest_regions = sorted(region_distances, key=lambda x: x[1])[:3]
|
|
@@ -511,6 +580,101 @@ def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float,
|
|
|
511
580
|
|
|
512
581
|
return metadata
|
|
513
582
|
|
|
583
|
+
def try_download_station_zip(original_url: str, timeout_s: int = 30) -> Optional[bytes]:
|
|
584
|
+
"""
|
|
585
|
+
Try downloading station archive; on 404s, attempt smart fallbacks.
|
|
586
|
+
|
|
587
|
+
Fallback strategies:
|
|
588
|
+
- Country rename: /TUR_Turkey/ -> /TUR_Turkiye/ (per Oct 2024 site update)
|
|
589
|
+
- TMYx period variants: .2009-2023.zip, .2007-2021.zip, .zip, .2004-2018.zip
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
original_url: URL extracted from KML
|
|
593
|
+
timeout_s: request timeout seconds
|
|
594
|
+
Returns:
|
|
595
|
+
Bytes of the downloaded zip on success, otherwise None
|
|
596
|
+
"""
|
|
597
|
+
def candidate_urls(url: str) -> List[str]:
|
|
598
|
+
urls = []
|
|
599
|
+
urls.append(url)
|
|
600
|
+
# Country rename variants
|
|
601
|
+
if "/TUR_Turkey/" in url:
|
|
602
|
+
urls.append(url.replace("/TUR_Turkey/", "/TUR_Turkiye/"))
|
|
603
|
+
if "/TUR_Turkiye/" in url:
|
|
604
|
+
urls.append(url.replace("/TUR_Turkiye/", "/TUR_Turkey/"))
|
|
605
|
+
# TMYx period variants
|
|
606
|
+
m = re.search(r"(.*_TMYx)(?:\.(\d{4}-\d{4}))?\.zip$", url)
|
|
607
|
+
if m:
|
|
608
|
+
base = m.group(1)
|
|
609
|
+
suffix = m.group(2)
|
|
610
|
+
variants = [
|
|
611
|
+
f"{base}.2009-2023.zip",
|
|
612
|
+
f"{base}.2007-2021.zip",
|
|
613
|
+
f"{base}.zip",
|
|
614
|
+
f"{base}.2004-2018.zip",
|
|
615
|
+
]
|
|
616
|
+
for v in variants:
|
|
617
|
+
if v not in urls:
|
|
618
|
+
urls.append(v)
|
|
619
|
+
# Also apply country rename to each variant
|
|
620
|
+
extra = []
|
|
621
|
+
for v in variants:
|
|
622
|
+
if "/TUR_Turkey/" in url:
|
|
623
|
+
extra.append(v.replace("/TUR_Turkey/", "/TUR_Turkiye/"))
|
|
624
|
+
if "/TUR_Turkiye/" in url:
|
|
625
|
+
extra.append(v.replace("/TUR_Turkiye/", "/TUR_Turkey/"))
|
|
626
|
+
for v in extra:
|
|
627
|
+
if v not in urls:
|
|
628
|
+
urls.append(v)
|
|
629
|
+
return urls
|
|
630
|
+
|
|
631
|
+
tried = set()
|
|
632
|
+
for u in candidate_urls(original_url):
|
|
633
|
+
if u in tried:
|
|
634
|
+
continue
|
|
635
|
+
tried.add(u)
|
|
636
|
+
try:
|
|
637
|
+
resp = requests.get(u, timeout=timeout_s, verify=ssl_verify)
|
|
638
|
+
resp.raise_for_status()
|
|
639
|
+
return resp.content
|
|
640
|
+
except requests.exceptions.SSLError:
|
|
641
|
+
# Retry with user-controlled insecure SSL
|
|
642
|
+
if allow_insecure_ssl:
|
|
643
|
+
try:
|
|
644
|
+
resp = requests.get(u, timeout=timeout_s, verify=False)
|
|
645
|
+
resp.raise_for_status()
|
|
646
|
+
return resp.content
|
|
647
|
+
except requests.exceptions.RequestException:
|
|
648
|
+
if allow_http_fallback and u.lower().startswith("https://"):
|
|
649
|
+
insecure_url = "http://" + u.split("://", 1)[1]
|
|
650
|
+
try:
|
|
651
|
+
resp = requests.get(insecure_url, timeout=timeout_s)
|
|
652
|
+
resp.raise_for_status()
|
|
653
|
+
return resp.content
|
|
654
|
+
except requests.exceptions.RequestException:
|
|
655
|
+
pass
|
|
656
|
+
continue
|
|
657
|
+
else:
|
|
658
|
+
if allow_http_fallback and u.lower().startswith("https://"):
|
|
659
|
+
insecure_url = "http://" + u.split("://", 1)[1]
|
|
660
|
+
try:
|
|
661
|
+
resp = requests.get(insecure_url, timeout=timeout_s)
|
|
662
|
+
resp.raise_for_status()
|
|
663
|
+
return resp.content
|
|
664
|
+
except requests.exceptions.RequestException:
|
|
665
|
+
pass
|
|
666
|
+
continue
|
|
667
|
+
except requests.exceptions.HTTPError as he:
|
|
668
|
+
# Only continue on 404; raise on other HTTP errors
|
|
669
|
+
if getattr(he.response, "status_code", None) == 404:
|
|
670
|
+
continue
|
|
671
|
+
else:
|
|
672
|
+
raise
|
|
673
|
+
except requests.exceptions.RequestException:
|
|
674
|
+
# On network errors, try next candidate
|
|
675
|
+
continue
|
|
676
|
+
return None
|
|
677
|
+
|
|
514
678
|
def get_stations_from_kml(kml_url: str) -> List[Dict]:
|
|
515
679
|
"""
|
|
516
680
|
Get weather stations from a KML file.
|
|
@@ -526,9 +690,32 @@ def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float,
|
|
|
526
690
|
List of dictionaries containing station metadata
|
|
527
691
|
"""
|
|
528
692
|
try:
|
|
529
|
-
# Download KML file with timeout
|
|
530
|
-
|
|
531
|
-
|
|
693
|
+
# Download KML file with timeout (secure first)
|
|
694
|
+
try:
|
|
695
|
+
response = requests.get(kml_url, timeout=30, verify=ssl_verify)
|
|
696
|
+
response.raise_for_status()
|
|
697
|
+
except requests.exceptions.SSLError:
|
|
698
|
+
if allow_insecure_ssl:
|
|
699
|
+
# Retry with certificate verification disabled (last resort)
|
|
700
|
+
try:
|
|
701
|
+
response = requests.get(kml_url, timeout=30, verify=False)
|
|
702
|
+
response.raise_for_status()
|
|
703
|
+
except requests.exceptions.RequestException:
|
|
704
|
+
# Try HTTP fallback if original was HTTPS and allowed
|
|
705
|
+
if allow_http_fallback and kml_url.lower().startswith("https://"):
|
|
706
|
+
insecure_url = "http://" + kml_url.split("://", 1)[1]
|
|
707
|
+
response = requests.get(insecure_url, timeout=30)
|
|
708
|
+
response.raise_for_status()
|
|
709
|
+
else:
|
|
710
|
+
raise
|
|
711
|
+
else:
|
|
712
|
+
# Try HTTP fallback only if allowed and original was HTTPS
|
|
713
|
+
if allow_http_fallback and kml_url.lower().startswith("https://"):
|
|
714
|
+
insecure_url = "http://" + kml_url.split("://", 1)[1]
|
|
715
|
+
response = requests.get(insecure_url, timeout=30)
|
|
716
|
+
response.raise_for_status()
|
|
717
|
+
else:
|
|
718
|
+
raise
|
|
532
719
|
|
|
533
720
|
# Try to decode content with multiple encodings
|
|
534
721
|
content = try_decode(response.content)
|
|
@@ -584,6 +771,18 @@ def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float,
|
|
|
584
771
|
|
|
585
772
|
# Determine which regions to scan based on user input or auto-detection
|
|
586
773
|
regions_to_scan = {}
|
|
774
|
+
def _add_selection(selection_name: str, mapping: Dict[str, str], out: Dict[str, str]):
|
|
775
|
+
"""Expand a region or dataset selection into concrete KML URLs."""
|
|
776
|
+
if selection_name in REGION_DATASET_GROUPS:
|
|
777
|
+
for key in REGION_DATASET_GROUPS[selection_name]:
|
|
778
|
+
if key in KML_SOURCES:
|
|
779
|
+
out[key] = KML_SOURCES[key]
|
|
780
|
+
elif selection_name in KML_SOURCES:
|
|
781
|
+
out[selection_name] = KML_SOURCES[selection_name]
|
|
782
|
+
else:
|
|
783
|
+
valid = sorted(list(REGION_DATASET_GROUPS.keys()) + list(KML_SOURCES.keys()))
|
|
784
|
+
raise ValueError(f"Invalid region/dataset: '{selection_name}'. Valid options include: {', '.join(valid)}")
|
|
785
|
+
|
|
587
786
|
if region is None:
|
|
588
787
|
# Auto-detect regions based on coordinates
|
|
589
788
|
detected_regions = detect_regions(longitude, latitude)
|
|
@@ -591,34 +790,32 @@ def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float,
|
|
|
591
790
|
if detected_regions:
|
|
592
791
|
print(f"Auto-detected regions: {', '.join(detected_regions)}")
|
|
593
792
|
for r in detected_regions:
|
|
594
|
-
|
|
793
|
+
_add_selection(r, KML_SOURCES, regions_to_scan)
|
|
595
794
|
else:
|
|
596
795
|
# Fallback to all regions if detection fails
|
|
597
796
|
print("Could not determine region from coordinates. Scanning all regions.")
|
|
598
|
-
regions_to_scan = KML_SOURCES
|
|
797
|
+
regions_to_scan = dict(KML_SOURCES)
|
|
599
798
|
elif isinstance(region, str):
|
|
600
799
|
# Handle string input for region selection
|
|
601
800
|
if region.lower() == "all":
|
|
602
|
-
regions_to_scan = KML_SOURCES
|
|
603
|
-
elif region in KML_SOURCES:
|
|
604
|
-
regions_to_scan[region] = KML_SOURCES[region]
|
|
801
|
+
regions_to_scan = dict(KML_SOURCES)
|
|
605
802
|
else:
|
|
606
|
-
|
|
607
|
-
raise ValueError(f"Invalid region: '{region}'. Valid regions are: {valid_regions}")
|
|
803
|
+
_add_selection(region, KML_SOURCES, regions_to_scan)
|
|
608
804
|
else:
|
|
609
805
|
# Handle list input for multiple regions
|
|
610
806
|
for r in region:
|
|
611
|
-
|
|
612
|
-
valid_regions = ", ".join(KML_SOURCES.keys())
|
|
613
|
-
raise ValueError(f"Invalid region: '{r}'. Valid regions are: {valid_regions}")
|
|
614
|
-
regions_to_scan[r] = KML_SOURCES[r]
|
|
807
|
+
_add_selection(r, KML_SOURCES, regions_to_scan)
|
|
615
808
|
|
|
616
809
|
# Get stations from selected KML sources
|
|
617
810
|
print("Fetching weather station data from Climate.OneBuilding.Org...")
|
|
618
811
|
all_stations = []
|
|
619
812
|
|
|
620
813
|
# Process each selected region
|
|
814
|
+
scanned_urls = set()
|
|
621
815
|
for region_name, url in regions_to_scan.items():
|
|
816
|
+
if url in scanned_urls:
|
|
817
|
+
continue
|
|
818
|
+
scanned_urls.add(url)
|
|
622
819
|
print(f"Scanning {region_name}...")
|
|
623
820
|
stations = get_stations_from_kml(url)
|
|
624
821
|
all_stations.extend(stations)
|
|
@@ -627,7 +824,23 @@ def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float,
|
|
|
627
824
|
print(f"\nTotal stations found: {len(all_stations)}")
|
|
628
825
|
|
|
629
826
|
if not all_stations:
|
|
630
|
-
|
|
827
|
+
# Fallback: if no stations found, try scanning all available datasets
|
|
828
|
+
if not (isinstance(region, str) and region.lower() == "all"):
|
|
829
|
+
print("No stations found from detected/selected regions. Falling back to global scan...")
|
|
830
|
+
regions_to_scan = dict(KML_SOURCES)
|
|
831
|
+
all_stations = []
|
|
832
|
+
scanned_urls = set()
|
|
833
|
+
for region_name, url in regions_to_scan.items():
|
|
834
|
+
if url in scanned_urls:
|
|
835
|
+
continue
|
|
836
|
+
scanned_urls.add(url)
|
|
837
|
+
print(f"Scanning {region_name}...")
|
|
838
|
+
stations = get_stations_from_kml(url)
|
|
839
|
+
all_stations.extend(stations)
|
|
840
|
+
print(f"Found {len(stations)} stations in {region_name}")
|
|
841
|
+
print(f"\nTotal stations found after global scan: {len(all_stations)}")
|
|
842
|
+
if not all_stations:
|
|
843
|
+
raise ValueError("No weather stations found")
|
|
631
844
|
|
|
632
845
|
# Calculate distances from target coordinates to all stations
|
|
633
846
|
stations_with_distances = [
|
|
@@ -654,10 +867,11 @@ def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float,
|
|
|
654
867
|
# Find the nearest weather station
|
|
655
868
|
nearest_station, distance = min(stations_with_distances, key=lambda x: x[1])
|
|
656
869
|
|
|
657
|
-
# Download the EPW
|
|
870
|
+
# Download the EPW archive from the nearest station with fallbacks
|
|
658
871
|
print(f"\nDownloading EPW file for {nearest_station['name']}...")
|
|
659
|
-
|
|
660
|
-
|
|
872
|
+
archive_bytes = try_download_station_zip(nearest_station['url'], timeout_s=30)
|
|
873
|
+
if archive_bytes is None:
|
|
874
|
+
raise ValueError(f"Failed to download EPW archive from station URL and fallbacks: {nearest_station['url']}")
|
|
661
875
|
|
|
662
876
|
# Create a temporary directory for zip extraction
|
|
663
877
|
temp_dir = Path(output_dir) / "temp"
|
|
@@ -666,7 +880,7 @@ def get_nearest_epw_from_climate_onebuilding(longitude: float, latitude: float,
|
|
|
666
880
|
# Save the downloaded zip file temporarily
|
|
667
881
|
zip_file = temp_dir / "weather_data.zip"
|
|
668
882
|
with open(zip_file, 'wb') as f:
|
|
669
|
-
f.write(
|
|
883
|
+
f.write(archive_bytes)
|
|
670
884
|
|
|
671
885
|
final_epw = None
|
|
672
886
|
try:
|
|
@@ -758,8 +972,15 @@ def read_epw_for_solar_simulation(epw_file_path):
|
|
|
758
972
|
Raises:
|
|
759
973
|
ValueError: If LOCATION line not found or data parsing fails
|
|
760
974
|
"""
|
|
975
|
+
# Validate input path
|
|
976
|
+
if epw_file_path is None:
|
|
977
|
+
raise TypeError("EPW file path is None. Provide a valid path or ensure download succeeded.")
|
|
978
|
+
epw_path_obj = Path(epw_file_path)
|
|
979
|
+
if not epw_path_obj.exists() or not epw_path_obj.is_file():
|
|
980
|
+
raise FileNotFoundError(f"EPW file not found: {epw_file_path}")
|
|
981
|
+
|
|
761
982
|
# Read the entire EPW file
|
|
762
|
-
with open(
|
|
983
|
+
with open(epw_path_obj, 'r', encoding='utf-8') as f:
|
|
763
984
|
lines = f.readlines()
|
|
764
985
|
|
|
765
986
|
# Find the LOCATION line (first line in EPW format)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: voxcity
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.20
|
|
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
|
+
License-File: AUTHORS.rst
|
|
7
|
+
License-File: LICENSE
|
|
6
8
|
Author: Kunihiko Fujiwara
|
|
7
9
|
Author-email: fujiwara.kunihiko@takenaka.co.jp
|
|
8
10
|
Maintainer: Kunihiko Fujiwara
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
voxcity/__init__.py,sha256=el9v3gfybHOF_GUYPeSOqN0-vCrTW0eU1mcvi0sEfeU,252
|
|
2
2
|
voxcity/downloader/__init__.py,sha256=o_T_EU7hZLGyXxX9wVWn1x-OAa3ThGYdnpgB1_2v3AE,151
|
|
3
|
-
voxcity/downloader/citygml.py,sha256=
|
|
3
|
+
voxcity/downloader/citygml.py,sha256=I8-wWijqVOA1VeH3nFP9ZlC3l6XvXfli6lB17ZIXHb0,42232
|
|
4
4
|
voxcity/downloader/eubucco.py,sha256=ln1YNaaOgJfxNfCtVbYaMm775-bUvpAA_LDv60_i22w,17875
|
|
5
5
|
voxcity/downloader/gee.py,sha256=nvJvYqcSZyyontRtG2cFeb__ZJfeY4rRN1NBPORxLwQ,23557
|
|
6
6
|
voxcity/downloader/mbfp.py,sha256=UXDVjsO0fnb0fSal9yqrSFEIBThnRmnutnp08kZTmCA,6595
|
|
@@ -13,8 +13,8 @@ voxcity/exporter/cityles.py,sha256=Kfl2PAn4WquqCdjOlyPrHysxPLaudh8QfsoC6WAXlvs,1
|
|
|
13
13
|
voxcity/exporter/envimet.py,sha256=Sh7s1JdQ6SgT_L2Xd_c4gtEGWK2hTS87bccaoIqik-s,31105
|
|
14
14
|
voxcity/exporter/magicavoxel.py,sha256=SfGEgTZRlossKx3Xrv9d3iKSX-HmfQJEL9lZHgWMDX4,12782
|
|
15
15
|
voxcity/exporter/netcdf.py,sha256=48rJ3wDsFhi9ANbElhMjXLxWMJoJzBt1gFbN0ekPp-A,7404
|
|
16
|
-
voxcity/exporter/obj.py,sha256=
|
|
17
|
-
voxcity/generator.py,sha256=
|
|
16
|
+
voxcity/exporter/obj.py,sha256=7xmVSQ_y-X8QLjNdASDPsaltGvmyW9-yAacocByYKl4,58160
|
|
17
|
+
voxcity/generator.py,sha256=OHSZU4z7Vj6Iqbp1J0xXlFUSPyMsvOgPv3Tren8mEBc,67302
|
|
18
18
|
voxcity/geoprocessor/__init__.py,sha256=WYxcAQrjGucIvGHF0JTC6rONZzL3kCms1S2vpzS6KaU,127
|
|
19
19
|
voxcity/geoprocessor/draw.py,sha256=AZMWq23wxxDJygNloCbVzWAAr1JO7nC94umf9LSxJ5o,49248
|
|
20
20
|
voxcity/geoprocessor/grid.py,sha256=NmlQwl1nJpS7MduVtJeJCG-xBfVIwKTOip7pMm3RhsY,76722
|
|
@@ -23,16 +23,16 @@ voxcity/geoprocessor/network.py,sha256=YynqR0nq_NUra_cQ3Z_56KxfRia1b6-hIzGCj3QT-
|
|
|
23
23
|
voxcity/geoprocessor/polygon.py,sha256=DfzXf6R-qoWXEZv1z1aHCVfr-DCuCFw6lieQT5cNHPA,61188
|
|
24
24
|
voxcity/geoprocessor/utils.py,sha256=s17XpgkLBelmNCk2wcUwTK1tEiFpguWR2BF_n7K17jg,31378
|
|
25
25
|
voxcity/simulator/__init__.py,sha256=APdkcdaovj0v_RPOaA4SBvFUKT2RM7Hxuuz3Sux4gCo,65
|
|
26
|
-
voxcity/simulator/solar.py,sha256=
|
|
26
|
+
voxcity/simulator/solar.py,sha256=4D5t2I79vBW1qXd90BZR0tMiA9WOEXWLG5b-d6E2XbQ,107127
|
|
27
27
|
voxcity/simulator/utils.py,sha256=sEYBB2-hLJxTiXQps1_-Fi7t1HN3-1OPOvBCWtgIisA,130
|
|
28
28
|
voxcity/simulator/view.py,sha256=k3FoS6gsibR-eDrTHJivJSQfvN3Tg8R8eSTeMqd9ans,93942
|
|
29
29
|
voxcity/utils/__init__.py,sha256=Q-NYCqYnAAaF80KuNwpqIjbE7Ec3Gr4y_khMLIMhJrg,68
|
|
30
30
|
voxcity/utils/lc.py,sha256=722Gz3lPbgAp0mmTZ-g-QKBbAnbxrcgaYwb1sa7q8Sk,16189
|
|
31
31
|
voxcity/utils/material.py,sha256=H8K8Lq4wBL6dQtgj7esUW2U6wLCOTeOtelkTDJoRgMo,10007
|
|
32
32
|
voxcity/utils/visualization.py,sha256=_c5WnhA0fawkseUviKEIpUUsF-M-OOLOT9FrpgQIh1A,118556
|
|
33
|
-
voxcity/utils/weather.py,sha256=
|
|
34
|
-
voxcity-0.6.
|
|
35
|
-
voxcity-0.6.
|
|
36
|
-
voxcity-0.6.
|
|
37
|
-
voxcity-0.6.
|
|
38
|
-
voxcity-0.6.
|
|
33
|
+
voxcity/utils/weather.py,sha256=cb6ZooL42Hc4214OtFiJ78cCgWYM6VE-DU8S3e-urRg,48449
|
|
34
|
+
voxcity-0.6.20.dist-info/licenses/AUTHORS.rst,sha256=m82vkI5QokEGdcHof2OxK39lf81w1P58kG9ZNNAKS9U,175
|
|
35
|
+
voxcity-0.6.20.dist-info/licenses/LICENSE,sha256=s_jE1Df1nTPL4A_5GCGic5Zwex0CVaPKcAmSilxJPPE,1089
|
|
36
|
+
voxcity-0.6.20.dist-info/METADATA,sha256=dRyklxMxGHwtTvE6lT3TfMFIlWBDZ354zt1Dr_Vv7qQ,26212
|
|
37
|
+
voxcity-0.6.20.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
|
|
38
|
+
voxcity-0.6.20.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|