11from typing import TYPE_CHECKING , Never , Self
2+ import os
3+ import xml .etree .ElementTree as ET
4+
25import matplotlib
36matplotlib .use ('Agg' )
47if TYPE_CHECKING :
58 import matplotlib .figure
69 import pyproj .crs .crs
710 import shapely .geometry .polygon
8- import os
9- import xml .etree .ElementTree as ET
10-
1111import contextily as ctx
1212import geopandas as gpd
1313import matplotlib .pyplot as plt
1414from matplotlib .backends .backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
1515import pyproj
1616from PyQt5 import QtWidgets , QtCore
1717from PyQt5 .QtWidgets import QMessageBox , QListWidgetItem , QApplication
18+ from PyQt5 .QtGui import QMovie
1819from psycopg2 import IntegrityError , InternalError
1920from qgis .core import QgsTask
2021from shapely import wkt
2122from shapely .ops import transform
22- from shapely .geometry import Polygon , Point
23+ from shapely .geometry import Point , Polygon
2324
2425from ..support_scripts .pyagriculture .agriculture import PyAgriculture
2526from ..widgets .find_iso_fields import FindIsoFieldWidget
2627
28+
29+ def get_auto_zoom_level (minx , miny , maxx , maxy , max_zoom = 17 , min_zoom = 10 ):
30+ """Estimate a suitable zoom level for the given bounds in meters (EPSG:3857)."""
31+ width = abs (maxx - minx )
32+ # These thresholds are tuned for Web Mercator (meters)
33+ if width < 100 : # < 100 m
34+ return max_zoom
35+ elif width < 1000 : # < 1 km
36+ return max_zoom - 2
37+ elif width < 10000 : # < 10 km
38+ return max_zoom - 4
39+ elif width < 100000 : # < 100 km
40+ return max_zoom - 6
41+ else :
42+ return min_zoom
43+
44+
2745def remove_invalid_points (gdf ):
2846 """Removes rows with invalid latitude and longitude values from the GeoDataFrame."""
2947 # Define valid ranges for latitude and longitude
3048 valid_lat_range = (- 90 , 90 )
3149 valid_lon_range = (- 180 , 180 )
3250
3351 # Filter out rows with invalid latitude and longitude values
34- valid_gdf = gdf [(gdf ['latitude' ].between (* valid_lat_range )) & (gdf ['longitude' ].between (* valid_lon_range ))]
52+ valid_gdf = gdf [
53+ (gdf ['latitude' ].between (* valid_lat_range )) &
54+ (gdf ['longitude' ].between (* valid_lon_range )) &
55+ ~ ((gdf ['latitude' ] == 0 ) & (gdf ['longitude' ] == 0 ))
56+ ]
3557
3658 return valid_gdf
3759
@@ -115,6 +137,8 @@ def _extract_coordinates(self: Self, root: ET.Element) -> list[list[str]|Never]:
115137 def find_from_tasks (self : Self ) -> None :
116138 """Finds additional data from Pyagriculture tasks"""
117139 self .py_agri = PyAgriculture (os .path .dirname (self .path ))
140+ self .show_loading_animation () # <-- Show spinner before starting task
141+ #self.parent.tsk_mngr.addTask(self.loading_tsk)
118142 if self .parent .test_mode is False :
119143 task = QgsTask .fromFunction ('Decode binary data' , self .py_agri .gather_data ,
120144 most_importants = [],
@@ -124,42 +148,86 @@ def find_from_tasks(self: Self) -> None:
124148 self .py_agri .gather_data (qtask = 'debug' , most_importants = [])
125149 self .populate_field_list2 ()
126150
127- def populate_field_list2 (self : Self , res : None = None ,
128- values : None = None ) -> None :
151+ def clear_layout (self ):
152+ """Remove all widgets from a given layout."""
153+ layout = self .fifw .WShowField .layout ()
154+ # Stop and remove the loading animation if present
155+ if hasattr (self , "movie" ):
156+ self .movie .stop ()
157+ if hasattr (self , "loading_label" ) and self .loading_label in [layout .itemAt (i ).widget () for i in range (layout .count ())]:
158+ layout .removeWidget (self .loading_label )
159+ self .loading_label .deleteLater ()
160+ del self .loading_label
161+ if layout is not None :
162+ for i in reversed (range (layout .count ())):
163+ widget = layout .itemAt (i ).widget ()
164+ if widget is not None :
165+ widget .setParent (None )
166+
167+ def show_loading_animation (self : Self ) -> None :
168+ """Show a spinning/loading animation in the canvas area."""
169+ layout = self .fifw .WShowField .layout ()
170+ if layout is None :
171+ layout = QtWidgets .QVBoxLayout ()
172+ self .fifw .WShowField .setLayout (layout )
173+ else :
174+ self .clear_layout ()
175+ # Create and add the loading animation
176+ print ('run' )
177+ self .loading_label = QtWidgets .QLabel ()
178+ gif_path = os .path .join (os .path .dirname (__file__ ),".." , "img" , "loading.gif" )
179+ if not os .path .exists (gif_path ):
180+ self .loading_label .setText ("Loading..." )
181+ else :
182+ self .movie = QMovie (gif_path )
183+ self .loading_label .setMovie (self .movie )
184+ self .movie .start ()
185+ layout .addWidget (self .loading_label )
186+
187+ def populate_field_list2 (self : Self , res : None = None , values : None = None ) -> None :
129188 """Populates the field list based on the pyagri tasks."""
189+ self .clear_layout ()
130190 self .fifw .LWFields .clear ()
131191 for i , task in enumerate (self .py_agri .tasks ):
132192 if 'longitude' not in task .columns :
133193 try :
134- # Assuming `gdf` is your GeoDataFrame with geometries
135- extent = task .total_bounds # Get [min_x, min_y, max_x, max_y]
136-
137- # Create a bounding box polygon
194+ extent = task .total_bounds
138195 convex_hull = Polygon ([
139- (extent [0 ], extent [1 ]), # Bottom-left corner (min_x, min_y)
140- (extent [0 ], extent [3 ]), # Top-left corner (min_x, max_y)
141- (extent [2 ], extent [3 ]), # Top-right corner (max_x, max_y)
142- (extent [2 ], extent [1 ]), # Bottom-right corner (max_x, min_y)
143- (extent [0 ], extent [1 ]) # Close the polygon (back to bottom-left)
196+ (extent [0 ], extent [1 ]),
197+ (extent [0 ], extent [3 ]),
198+ (extent [2 ], extent [3 ]),
199+ (extent [2 ], extent [1 ]),
200+ (extent [0 ], extent [1 ])
144201 ])
145202 except :
146- pass
203+ continue
147204 else :
148205 task ['geometry' ] = task .apply (lambda row : Point (row ['longitude' ], row ['latitude' ]), axis = 1 )
149206 gdf = gpd .GeoDataFrame (task , geometry = 'geometry' )
150207 gdf .set_crs (epsg = 4326 , inplace = True )
151208 gdf = remove_invalid_points (gdf )
152- convex_hull = gdf .unary_union .convex_hull
153- self .fields [f'Task { i } ' ] = convex_hull .wkt
154- self .fifw .LWFields .addItem (f'Task { i } ' )
209+ convex_hull = gdf .union_all ().convex_hull
210+
211+ # Only create a new polygon if there are at least 3 points left
212+ if len (convex_hull .exterior .coords ) < 3 :
213+ continue # Not enough points for a valid polygon
214+
215+ name = task .attrs .get ('task_name' , f'Task { i } ' )
216+ self .fields [name ] = convex_hull .wkt
217+ self .fifw .LWFields .addItem (name )
155218
156219 def on_item_clicked (self : Self , item : QListWidgetItem ) -> None :
157220 """Handles the event when an item in the field list is clicked."""
158221 item_name = item .text ()
222+ self .show_loading_animation ()
159223 if self .current_polygon != '' :
160224 self .save_updated_polygon ()
161225 if item_name != '' :
162- self .load_wkt (self .fields [item_name ])
226+ self .load_wkt (polygon_wkt = self .fields [item_name ])
227+ #tsk = QgsTask.fromFunction('Load polygon', self.load_wkt,
228+ # polygon_wkt=self.fields[item_name],
229+ # on_finished=None)
230+ #self.parent.tsk_mngr.addTask(tsk)
163231 self .current_polygon = self .fields [item_name ]
164232
165233 def _set_new_crs (self : Self ,
@@ -173,22 +241,28 @@ def _set_new_crs(self: Self,
173241 return transformed_polygon
174242
175243 def _plot_polygon_on_map (self : Self , polygon : "shapely.geometry.polygon.Polygon" ) -> "matplotlib.figure.Figure" :
176- """Plots the polygon on a map with interactivity for zoom and node editing."""
244+ """Plots the polygon or point on a map with interactivity for zoom and node editing."""
177245 if polygon .is_empty :
178246 fig , ax = plt .subplots (figsize = (12 , 9 ))
179247 ax .text (0.5 , 0.5 , 'No data points were found' , horizontalalignment = 'center' , verticalalignment = 'center' , transform = ax .transAxes , fontsize = 15 )
180248 ax .set_axis_off ()
181249 return fig
250+
251+ # Handle Polygon geometry as before
182252 polygon = self ._set_new_crs (polygon )
183253 minx , miny , maxx , maxy = polygon .bounds
254+
255+ # Calculate zoom level automatically
256+ zoom = get_auto_zoom_level (minx , miny , maxx , maxy , max_zoom = 17 , min_zoom = 10 )
257+
184258 fig , ax = plt .subplots (figsize = (12 , 9 ))
185259 patch_collection = ax .fill (* polygon .exterior .xy , edgecolor = 'm' , facecolor = 'none' )
186260 if patch_collection :
187261 self .polygon_patch = patch_collection [0 ]
188262
189- # Add the basemap
263+ # Add the basemap with the calculated zoom
190264 if not self .parent .test_mode :
191- ctx .add_basemap (ax , source = ctx .providers .Esri .WorldImagery , zoom = self . zoom_level )
265+ ctx .add_basemap (ax , source = ctx .providers .Esri .WorldImagery , zoom = zoom )
192266
193267 # Set the axis limits with padding
194268 padding = 0.15
@@ -254,13 +328,15 @@ def on_release(self, event):
254328 """Handles the event when a dragged point is released."""
255329 if not hasattr (self , 'dragging_point' ):
256330 return
257- self .dragging_point .set_animated (False )
331+ if self .dragging_point is not None :
332+ self .dragging_point .set_animated (False )
258333 self .dragging_point = None
259334 self .canvas .draw ()
260335
261336 def load_wkt (self : Self , polygon_wkt : str ) -> None :
262337 """Loads a polygon from WKT and plots it on the map."""
263338 polygon = wkt .loads (polygon_wkt )
339+ self .clear_layout ()
264340 fig = self ._plot_polygon_on_map (polygon )
265341 self .canvas = FigureCanvas (fig ) # Link the figure to the FigureCanvas
266342 self .canvas .setSizePolicy (QtWidgets .QSizePolicy .Expanding , QtWidgets .QSizePolicy .Expanding )
@@ -284,8 +360,7 @@ def save_updated_polygon(self: Self) -> None:
284360 new_polygon = Polygon (new_coords )
285361 for key , value in self .fields .items ():
286362 if value == self .current_polygon :
287- new_wkt = self ._set_new_crs (new_polygon , source_proj = pyproj .CRS ('EPSG:3857' ),
288- target_proj = pyproj .CRS ('EPSG:4326' ))
363+ new_wkt = self ._set_new_crs (new_polygon , source_proj = pyproj .CRS ('EPSG:3857' ), target_proj = pyproj .CRS ('EPSG:4326' ))
289364 self .fields [key ] = new_wkt .wkt
290365 break
291366 self .current_polygon = new_wkt .wkt
0 commit comments