2020 QgsFileDownloader ,
2121 QgsCoordinateTransform ,
2222 QgsCoordinateReferenceSystem ,
23+ QgsVectorLayer ,
2324)
2425from qgis .PyQt import QtCore
2526
3839 compress_raster ,
3940 get_layer_type ,
4041 convert_size ,
42+ transform_extent ,
43+ )
44+ from .request import (
45+ CplusApiPooling ,
46+ CplusApiRequest ,
47+ CplusApiRequestError ,
48+ CplusApiUrl ,
49+ JOB_COMPLETED_STATUS ,
4150)
42- from .request import CplusApiPooling , CplusApiRequest , CplusApiUrl , JOB_COMPLETED_STATUS
4351from ..definitions .defaults import DEFAULT_CRS_ID
4452
4553
@@ -744,7 +752,6 @@ class CalculateNatureBaseZonalStatsTask(QgsTask):
744752 """
745753
746754 status_message_changed = QtCore .pyqtSignal (str )
747- progress_changed = QtCore .pyqtSignal (float )
748755 results_ready = QtCore .pyqtSignal (object )
749756 task_finished = QtCore .pyqtSignal (bool )
750757
@@ -770,23 +777,76 @@ def _get_bbox_for_request(self) -> str:
770777
771778 return str (self .bbox )
772779
773- # Otherwise get saved scenario extent
774- extent = settings_manager .get_value (Settings .SCENARIO_EXTENT , default = None )
780+ # Otherwise get saved extents from settings
781+ clip_to_studyarea = settings_manager .get_value (
782+ Settings .CLIP_TO_STUDYAREA , default = False , setting_type = bool
783+ )
784+ if clip_to_studyarea :
785+ # From vector layer
786+ study_area_path = settings_manager .get_value (
787+ Settings .STUDYAREA_PATH , default = "" , setting_type = str
788+ )
789+ if not study_area_path or not os .path .exists (study_area_path ):
790+ log ("Path for determining layer extent is invalid." , info = False )
791+ return ""
792+
793+ aoi_layer = QgsVectorLayer (study_area_path , "AOI Layer" )
794+ if not aoi_layer .isValid ():
795+ log ("AOI layer is invalid." , info = False )
796+ return ""
797+
798+ source_crs = aoi_layer .crs ()
799+ if not source_crs :
800+ log ("CRS of AOI layer is undefined." , info = False )
801+ return ""
802+
803+ aoi_extent = aoi_layer .extent ()
804+ if not aoi_extent :
805+ log ("Extent of AOI layer is undefined." , info = False )
806+ return ""
807+
808+ # Reproject extent if required
809+ destination_crs = QgsCoordinateReferenceSystem ("EPSG:4326" )
810+ if source_crs != destination_crs :
811+ aoi_extent = transform_extent (aoi_extent , source_crs , destination_crs )
812+
813+ extent = [
814+ aoi_extent .xMinimum (),
815+ aoi_extent .yMinimum (),
816+ aoi_extent .xMaximum (),
817+ aoi_extent .yMaximum (),
818+ ]
819+ else :
820+ # From explicit extent definition
821+ settings_extent = settings_manager .get_value (
822+ Settings .SCENARIO_EXTENT , default = None
823+ )
824+ # Ensure in minX, minY, maxX, maxY format
825+ extent = [
826+ float (settings_extent [0 ]),
827+ float (settings_extent [2 ]),
828+ float (settings_extent [1 ]),
829+ float (settings_extent [3 ]),
830+ ]
831+
775832 if not extent or len (extent ) < 4 :
776833 raise ValueError ("Scenario extent is not defined or invalid." )
777834
778- return f"{ float ( extent [0 ]) } ,{ float ( extent [2 ]) } ,{ float ( extent [1 ]) } ,{ float ( extent [3 ]) } "
835+ return f"{ extent [0 ]} ,{ extent [1 ] } ,{ extent [2 ] } ,{ extent [3 ]} "
779836
780837 def run (self ) -> bool :
781838 """Initiate the zonal statistics calculation and poll until
782839 completed.
783840
841+ Use `poll_once` method for non-blocking iterations.
842+
784843 :returns: True if the calculation process succeeded or
785844 False it if failed.
786845 :rtype: bool
787846 """
788847 try :
789848 bbox_str = self ._get_bbox_for_request ()
849+ log (f"BBOX for Zonal stats request: { bbox_str } " )
790850 except Exception as e :
791851 log (f"Invalid bbox: { e } " )
792852 return False
@@ -827,12 +887,32 @@ def run(self) -> bool:
827887 f"Zonal statistics calculation started, task id: { task_uuid } "
828888 )
829889
830- pooling = self .request .fetch_zonal_statistics_progress (task_uuid )
890+ polling = self .request .fetch_zonal_statistics_progress (task_uuid )
831891
832892 # Repeatedly poll until final status
893+ status = True
833894 try :
834- while not self .isCanceled ():
835- response = pooling .results () or {}
895+ while True :
896+ if self .isCanceled ():
897+ polling .cancelled = True
898+ status = False
899+ break
900+
901+ try :
902+ # Use poll once which is non-blocking
903+ response = polling .poll_once () or {}
904+ except CplusApiRequestError as ex :
905+ log (f"Polling error: { ex } " , info = False )
906+ status = False
907+ break
908+ except Exception as ex :
909+ log (
910+ f"Error while polling zonal statistics progress: { ex } " ,
911+ info = False ,
912+ )
913+ time .sleep (self .polling_interval )
914+ continue
915+
836916 status_str = response .get ("status" )
837917 progress = response .get ("progress" , 0.0 )
838918 try :
@@ -845,18 +925,20 @@ def run(self) -> bool:
845925 progress_value = 0.0
846926
847927 self .setProgress (int (progress_value ))
848- self .progress_changed .emit (progress_value )
849928 self .set_status_message (
850929 f"Zonal statistics: { status_str } ({ progress_value :.1f} %)"
851930 )
931+
852932 if status_str in CplusApiPooling .FINAL_STATUS_LIST :
933+ if status_str != JOB_COMPLETED_STATUS :
934+ status = False
853935 self .result = response
854936 break
855937
856- # Pooling.results already sleeps between attempts, but keep guard
938+ # Sleep between poll iterations so that we can control the frequency
857939 time .sleep (self .polling_interval )
858940
859- return True
941+ return status
860942 except Exception as ex :
861943 log (
862944 f"Error while polling zonal statistics progress: { ex } " ,
@@ -865,7 +947,7 @@ def run(self) -> bool:
865947 return False
866948
867949 def finished (self , result : bool ):
868- """Emit signals and optionally persist results in settings."""
950+ """Emit signals and persist results in settings."""
869951 if result and self .result :
870952 results = self .result .get ("results" , [])
871953 self .results_ready .emit (results )
@@ -875,7 +957,10 @@ def finished(self, result: bool):
875957 "Empty result set for zonal statistics calculation of Naturebase layers." ,
876958 info = False ,
877959 )
878- result_info = ResultInfo (results , self .result .get ("finished_at" , "" ))
960+ sorted_results = []
961+ if results :
962+ sorted_results = sorted (results , key = lambda d : d ["layer_name" ])
963+ result_info = ResultInfo (sorted_results , self .result .get ("finished_at" , "" ))
879964 settings_manager .save_nature_base_zonal_stats (result_info )
880965 else :
881966 self .set_status_message ("Zonal statistics task failed or was cancelled" )
0 commit comments