Skip to content

Commit f486f73

Browse files
author
Axel Hörteborn
committed
Updated to version 2.13.2
1 parent 7b9fb59 commit f486f73

11 files changed

Lines changed: 663 additions & 535 deletions

File tree

i18n/GeoDataFarm_sv.qm

109 Bytes
Binary file not shown.

i18n/GeoDataFarm_sv.ts

Lines changed: 216 additions & 208 deletions
Large diffs are not rendered by default.

i18n/geodatafarm_de.qm

125 Bytes
Binary file not shown.

i18n/geodatafarm_de.ts

Lines changed: 216 additions & 208 deletions
Large diffs are not rendered by default.

img/loading.gif

81.5 KB
Loading

import_data/handle_iso11783.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,20 @@ def get_task_data(self: Self) -> dict:
134134
continue
135135
except Exception as e:
136136
print(f'error: {e}')
137-
return
137+
task_names[task_nr] = []
138+
continue
139+
# Remove rows where both latitude and longitude are 0
140+
data_set = data_set[~((data_set['latitude'] == 0) & (data_set['longitude'] == 0))]
141+
if len(data_set) == 0:
142+
task_names[task_nr] = []
143+
continue
138144
fields = []
145+
if len(data_set) < 100:
146+
divider = len(data_set)
147+
else:
148+
divider = 10
139149
sql = "with start_sel as ("
140-
for index, row in data_set.iloc[::int(len(data_set)/10)].iterrows():
150+
for index, row in data_set.iloc[::int(len(data_set)/divider)].iterrows():
141151
sql +=f"""select field_name from fields where st_intersects(polygon, st_geomfromtext('Point({row["longitude"]} {row["latitude"]})', 4326))
142152
UNION """
143153
sql = sql[:-6] + ") select field_name from start_sel group by field_name"
@@ -217,7 +227,11 @@ def populate2(self: Self, res: str="", values: str="") -> None:
217227
"""The end of populate the second table when all data is decoded
218228
from the qtask"""
219229
task_names = self.get_task_data()
220-
self.IXB.TWISODataSelect.setRowCount(len(self.tasks_to_include))
230+
valids = 0
231+
for row in task_names.values():
232+
if len(row) > 0:
233+
valids += 1
234+
self.IXB.TWISODataSelect.setRowCount(valids)
221235
self.IXB.TWISODataSelect.setColumnCount(4)
222236
self.IXB.TWISODataSelect.setHorizontalHeaderLabels([self.tr('To include'), self.tr('Date'), self.tr('Field'),
223237
self.tr('Crops')])
@@ -235,7 +249,7 @@ def populate2(self: Self, res: str="", values: str="") -> None:
235249
item1 = QtWidgets.QTableWidgetItem('Include')
236250
item1.setFlags(QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled)
237251
item1.setCheckState(QtCore.Qt.Checked)
238-
self.checkboxes2.append([i, j, item1])
252+
self.checkboxes2.append([j, j, item1])
239253
item2 = QtWidgets.QTableWidgetItem(row[0][1])
240254
item2.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable)
241255
field_column = RadioComboBox()
@@ -245,10 +259,10 @@ def populate2(self: Self, res: str="", values: str="") -> None:
245259
field_column.setCurrentIndex(1)
246260
crops = QtWidgets.QComboBox()
247261
self.populate.reload_crops(crops)
248-
self.IXB.TWISODataSelect.setItem(i, 0, item1)
249-
self.IXB.TWISODataSelect.setItem(i, 1, item2)
250-
self.IXB.TWISODataSelect.setCellWidget(i, 2, field_column)
251-
self.IXB.TWISODataSelect.setCellWidget(i, 3, crops)
262+
self.IXB.TWISODataSelect.setItem(j, 0, item1)
263+
self.IXB.TWISODataSelect.setItem(j, 1, item2)
264+
self.IXB.TWISODataSelect.setCellWidget(j, 2, field_column)
265+
self.IXB.TWISODataSelect.setCellWidget(j, 3, crops)
252266
self.checkboxes3.append(field_column)
253267
self.checkboxes4.append(crops)
254268
self.tasks.append(self.rename_duplicate_columns(self.py_agri.tasks[i]))
@@ -648,7 +662,9 @@ def insert_data(qtask: None, db: DB, data: pd.DataFrame, schema: str, insert_sql
648662
if suc[2] == 0:
649663
return False, True, 'No data was found on that field.'
650664
if schema != 'harvest':
651-
create_polygons(db, schema, tbl_name, field)
665+
r = create_polygons(db, schema, tbl_name, field)
666+
if not suc[0]:
667+
return False, True, r[1]
652668
db.execute_sql(f"DROP TABLE {schema}.temp_table{tsk_nr}")
653669
if qtask is not None:
654670
qtask.setProgress(90)

import_data/handle_text_data.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ def create_table(db: DB, schema: str, heading_row: list[str],
635635

636636

637637
def create_polygons(db: DB, schema: str, tbl_name: str,
638-
field: str) -> None:
638+
field: str) -> list:
639639
sql = """drop table if exists {schema}.temp_tbl2;
640640
WITH voronoi_temp2 AS (
641641
SELECT ST_dump(ST_VoronoiPolygons(ST_Collect(pos))) as vor
@@ -647,9 +647,10 @@ def create_polygons(db: DB, schema: str, tbl_name: str,
647647
SET polygon = st_multi(ST_Intersection(geom, (select polygon
648648
from fields where field_name = '{field}')))
649649
FROM {schema}.temp_tbl2
650-
WHERE st_intersects(pos, geom)""".format(schema=schema, tbl=tbl_name, field=field)
651-
db.execute_sql(sql)
650+
WHERE st_intersects(pos, geom) AND ST_IsValid(geom)""".format(schema=schema, tbl=tbl_name, field=field)
651+
res = db.execute_sql(sql, return_failure=True)
652652
db.execute_sql("drop table if exists {schema}.temp_tbl2;".format(schema=schema))
653+
return res
653654

654655

655656
def insert_data_to_database(task: str, db: DB, params: dict) -> list[bool]:
@@ -793,12 +794,14 @@ def insert_data_to_database(task: str, db: DB, params: dict) -> list[bool]:
793794
if task != 'debug':
794795
task.setProgress(70)
795796
if schema != 'harvest':
796-
create_polygons(db, schema, tbl_name, field)
797+
r = create_polygons(db, schema, tbl_name, field)
798+
if not r[0]:
799+
return [False, r[1], '', 'Creating polygons failed']
797800
db.create_indexes(tbl_name, focus_col, schema, primary_key=False)
798801
if params['move']:
799802
suc = move_points(db, params['move_x'], params['move_y'], tbl_name, task)
800803
if not suc[0]:
801-
True, no_miss_heading, some_wrong_len, sql
804+
return [False, no_miss_heading, some_wrong_len, sql]
802805
else:
803806
task = suc[1]
804807
if task != 'debug':

metadata.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ name=GeoDataFarm
1111
qgisMinimumVersion=2.99
1212
qgisMaximumVersion=3.99
1313
description=For farmers, tracking efforts on the field, planing for next season etc.
14-
version=2.13.1
14+
version=2.13.2
1515
author=Axel Horteborn
1616
email=geodatafarm@gmail.com
1717

@@ -25,6 +25,9 @@ repository=https://github.com/axelande/geodatafarm3
2525

2626
# Uncomment the following line and add your changelog:
2727
changelog=
28+
Version 2.13.2 (2025-08-30)
29+
- [bug] Added support for missing unit in the isoXML import.
30+
- [bug] More isoXML fixes.
2831
Version 2.13.1 (2025-08-19)
2932
- [bug] fixed failed import statement
3033
Version 2.13.0 (2025-08-13)

support_scripts/find_iso_field.py

Lines changed: 102 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,59 @@
11
from typing import TYPE_CHECKING, Never, Self
2+
import os
3+
import xml.etree.ElementTree as ET
4+
25
import matplotlib
36
matplotlib.use('Agg')
47
if 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-
1111
import contextily as ctx
1212
import geopandas as gpd
1313
import matplotlib.pyplot as plt
1414
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
1515
import pyproj
1616
from PyQt5 import QtWidgets, QtCore
1717
from PyQt5.QtWidgets import QMessageBox, QListWidgetItem, QApplication
18+
from PyQt5.QtGui import QMovie
1819
from psycopg2 import IntegrityError, InternalError
1920
from qgis.core import QgsTask
2021
from shapely import wkt
2122
from shapely.ops import transform
22-
from shapely.geometry import Polygon, Point
23+
from shapely.geometry import Point, Polygon
2324

2425
from ..support_scripts.pyagriculture.agriculture import PyAgriculture
2526
from ..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+
2745
def 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

Comments
 (0)