1212import numpy as np
1313import os
1414from rapida .ntl import utils
15- from rapida .admin .util import bbox_to_geojson_polygon
15+ from rapida .util .bbox_param_type import get_best_semantic_label
16+ from osgeo import gdal , gdal_array , ogr
17+ from rapida .util .geo import polygonize_raster_mask
18+ from rapida .cli .assess import assess
19+ import click
20+ from rapida .project .project import Project
21+ from tempfile import TemporaryDirectory
22+ import geopandas as gpd
23+ import datetime
1624logger = logging .getLogger ('rapida' )
17-
25+ gdal . UseExceptions ()
1826
1927
2028async def detect_outage (
2129 bbox : tuple [numbers .Number ] = None , nominal_date : datetime = None , deliverable : str = None ,
22- dst_dir : str = None , mask_clouds :bool = True , percentage_drop :int = None ,
23- display : bool = False , progress : Progress = None ):
30+ dst_dir : str = None , mask_clouds :bool = True , percentage_drop :int = None , pop_vars : str | tuple [ str ] = None ,
31+ display : bool = False , progress : Progress = None , year = datetime . datetime . now (). year ):
2432
2533 logger .info (f'Fetching best imagery for { deliverable } { bbox } -{ nominal_date } ' )
2634 # with open(os.path.join('/tmp', 'bbox.geojson'), "w") as ff:
@@ -44,7 +52,7 @@ async def detect_outage(
4452 monthly_timestamp , monthly_files = next (iter (monthly_results .items ()))
4553 monthly_product , monthly_image_files = next (iter (monthly_files .items ()))
4654
47- monthly_data , gt = extract_bb (image_files = monthly_image_files , sds_name = nasa_const .SUB_DATASETS ['A3' ],
55+ monthly_data , gt , rsrs = extract_bb (image_files = monthly_image_files , sds_name = nasa_const .SUB_DATASETS ['A3' ],
4856 bbox = bbox , progress = progress , return_gt = True )
4957
5058 monthly_data_label = f'{ monthly_product } _{ monthly_timestamp } '
@@ -165,11 +173,92 @@ async def detect_outage(
165173 arrays [f'{ daily_data_label } _ZSCORE' ] = zscore
166174 arrays [f'{ daily_data_label } _OUTAGE' ] = outage
167175
168-
169- file_name = utils .get_custom_bbox_label (bbox )
176+ # --- 5. UNIFIED DISPLAY & EXPORT ---
177+ #file_name = utils.get_custom_bbox_label(bbox)
178+ file_name = get_best_semantic_label (bbox = bbox )
170179 outage_tif_path = os .path .join (dst_dir , f'{ deliverable } _{ file_name } .tif' )
180+ outage_gpkg_path = os .path .join (dst_dir , f'{ deliverable } _{ file_name } .gpkg' )
171181 write_outage_tif (src_arrays = arrays , gt = gt , dst_path = outage_tif_path )
172- # --- 5. UNIFIED DISPLAY & EXPORT ---
182+
183+ if pop_vars :
184+ # vectorize outage
185+ gtiff_driver = gdal .GetDriverByName ("GTiff" )
186+
187+ # Ensure parent directory exists
188+ os .makedirs (os .path .dirname (os .path .abspath (outage_gpkg_path )), exist_ok = True )
189+
190+ if not os .path .exists (outage_gpkg_path ):
191+ # 1. Ask GDAL to sniff out the right driver name based on the path structure/extension
192+ v_driver = ogr .GetDriverByName ('GPKG' )
193+
194+ # 2. Use the driver to initialize the empty file header
195+ with v_driver .CreateDataSource (outage_gpkg_path ) as init_ds :
196+ pass # Successfully generated empty stub container
197+ for k , arr in arrays .items ():
198+ if 'outage' in k .lower ():
199+ # Ensure data type is compatible (e.g., forcing int64/bool to int32/uint8 if needed)
200+ if arr .dtype == bool :
201+ arr = arr .astype ("uint8" )
202+ elif arr .dtype == "int64" :
203+ arr = arr .astype ("int32" ) # GDAL standard doesn't always love 64-bit ints
204+ gdal_dt = gdal_array .NumericTypeCodeToGDALTypeCode (arr .dtype )
205+ rows , cols = arr .shape
206+ # Generate a unique virtual memory path
207+ vsimem_path = f"/vsimem/{ k } .tif"
208+
209+
210+ try :
211+ # Create, write, and safely close the dataset within the context manager
212+ with gtiff_driver .Create (vsimem_path , cols , rows , 1 , gdal_dt ) as ds :
213+ ds .SetGeoTransform (gt )
214+ ds .SetSpatialRef (rsrs )
215+ band = ds .GetRasterBand (1 )
216+ band .WriteArray (arr )
217+ band .FlushCache ()
218+
219+ # The dataset is now closed, but the file exists in /vsimem/
220+ polygonize_raster_mask (
221+ raster_ds = vsimem_path , # Passes the string path
222+ dst_dataset = outage_gpkg_path ,
223+ dst_layer = k ,
224+ geom_type = ogr .wkbPolygon
225+ )
226+
227+ with TemporaryDirectory (dir = dst_dir , delete = True ) as project_folder :
228+ project = Project (path = project_folder , polygons = outage_gpkg_path ,
229+ comment = 'temp project for outage zonal stats' )
230+ with click .Context (assess ) as ctx :
231+ ctx .ensure_object (dict )
232+ ctx .obj ['progress' ] = progress
233+ # 2. Use invoke. Do NOT pass 'ctx' manually here.
234+ # Click intercepts this and injects it as the first argument automatically.
235+ ctx .invoke (
236+ assess ,
237+ components = ('population' ,),
238+ variables = pop_vars ,
239+ year = year ,
240+ project = project .path ,
241+ force = False
242+ )
243+ stat_gpkg_path = os .path .join (project_folder , 'data' , f'{ project .name } .gpkg' )
244+ pop_stat_gdf = gpd .read_file (stat_gpkg_path , layer = 'stats.population' )
245+
246+ pop_stat_gdf = pop_stat_gdf .to_crs ('EPSG:4326' )
247+
248+ pop_stat_gdf .to_file (
249+ filename = outage_gpkg_path ,
250+ driver = "gpkg" ,
251+ engine = "pyogrio" ,
252+ mode = "w" ,
253+ layer = k ,
254+ promote_to_multi = True ,
255+ index = False
256+ )
257+
258+
259+ finally :
260+ # Unlink (delete) the virtual file to free up system memory
261+ gdal .Unlink (vsimem_path )
173262 if display :
174263 from rapida .ntl import vis
175264 vis .display2 (data = arrays ,
0 commit comments