Skip to content

Commit

Permalink
field_creation_wizard and field update
Browse files Browse the repository at this point in the history
  • Loading branch information
LukasBaecker committed Sep 19, 2024
1 parent 24ce27e commit 3aa9b9f
Show file tree
Hide file tree
Showing 7 changed files with 431 additions and 97 deletions.
59 changes: 52 additions & 7 deletions field_friend/automations/field.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import math
from dataclasses import dataclass, field
from uuid import uuid4

import rosys
from shapely.geometry import Polygon
from shapely import offset_curve
from shapely.geometry import LineString, Polygon

from field_friend.localization import GeoPoint, GeoPointCollection

Expand All @@ -22,6 +25,8 @@ def reversed(self):
points=list(reversed(self.points)),
)

return self # Add this line to fix the missing return statement error

def line_segment(self) -> rosys.geometry.LineSegment:
return rosys.geometry.LineSegment(point1=self.points[0].cartesian(),
point2=self.points[-1].cartesian())
Expand All @@ -30,27 +35,67 @@ def line_segment(self) -> rosys.geometry.LineSegment:
@dataclass(slots=True, kw_only=True)
class Field(GeoPointCollection):
visualized: bool = False
first_row_start: GeoPoint | None = None
first_row_end: GeoPoint | None = None
row_spacing: float = 0.5
row_number: int = 10
obstacles: list[FieldObstacle] = field(default_factory=list)
rows: list[Row] = field(default_factory=list)
crop: str | None = None
outline_buffer_width: float = 2

@property
def outline(self) -> list[rosys.geometry.Point]:
return self.cartesian()
def outline(self) -> list[GeoPoint]:
assert self.first_row_start is not None
assert self.first_row_end is not None
ab_line = LineString([self.first_row_start.tuple, self.first_row_end.tuple])
last_row_linestring = offset_curve(ab_line, self.row_spacing * self.row_number / 100000)
end_row_points: list[GeoPoint] = []
for point in last_row_linestring.coords:
end_row_points.append(GeoPoint(lat=point[0], long=point[1]))
outline_unbuffered: list[GeoPoint] = []
for i, point in enumerate(end_row_points):
outline_unbuffered.append(point)
outline_unbuffered.append(self.first_row_end)
outline_unbuffered.append(self.first_row_start)
print(f'outline_unbuffered: {outline_unbuffered}')
outline_polygon = Polygon([(p.lat, p.long) for p in outline_unbuffered])
bufferd_polygon = outline_polygon.buffer(
self.outline_buffer_width/100000, join_style='mitre', mitre_limit=math.inf)
bufferd_polygon_coords = bufferd_polygon.exterior.coords
outline: list[GeoPoint] = []
for p in bufferd_polygon_coords:
outline.append(GeoPoint(lat=p[0], long=p[1]))
outline.append(outline[0])
return outline

@property
@ property
def outline_as_tuples(self) -> list[tuple[float, float]]:
return [p.tuple for p in self.outline]

def area(self) -> float:
outline = self.outline
if not outline:
return 0.0
polygon = Polygon([(p.x, p.y) for p in outline])
polygon = Polygon([(p.lat, p.long) for p in outline])
return polygon.area

def worked_area(self, worked_rows: int) -> float:
worked_area = 0.0
if self.area() > 0:
worked_area = worked_rows * self.area() / len(self.rows)
return worked_area

@ property
def rows(self) -> list[Row]:
assert self.first_row_start is not None
assert self.first_row_end is not None
ab_line = LineString([self.first_row_start.tuple, self.first_row_end.tuple])
rows = []
for i in range(self.row_number+1):
offset = i * self.row_spacing
offset_row_coordinated = offset_curve(ab_line, offset/100_000).coords
row_points: list[GeoPoint] = []
for point in offset_row_coordinated:
row_points.append(GeoPoint(lat=point[0], long=point[1]))
row = Row(id=str(uuid4()), name=f'{i + 1}', points=row_points)
rows.append(row)
return rows
2 changes: 2 additions & 0 deletions field_friend/automations/field_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def backup(self) -> dict:
}

def restore(self, data: dict[str, Any]) -> None:
print('tes')
fields_data = data.get('fields', [])
rosys.persistence.replace_list(self.fields, Field, fields_data)

Expand All @@ -49,6 +50,7 @@ def restore(self, data: dict[str, Any]) -> None:
for j, row in enumerate(rows):
for point in row.get('points_wgs84', []):
f.rows[j].points.append(GeoPoint(lat=point[0], long=point[1]))
print(f'fields 🛑: {self.fields[0].rows[10]}')

def invalidate(self) -> None:
self.request_backup()
Expand Down
103 changes: 23 additions & 80 deletions field_friend/interface/components/field_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import rosys
from nicegui import ui

from field_friend.automations import Field, Row
from field_friend.automations import Field
from field_friend.automations.navigation import StraightLineNavigation
from field_friend.interface.components.monitoring import CameraPosition
from field_friend.localization import GeoPoint
Expand All @@ -29,14 +29,10 @@ def __init__(self, system: 'System'):
self.plant_locator = system.plant_locator
self.gnss = system.gnss
self.field_provider = system.field_provider

self.field = Field(id=str(uuid4()), name=f'Field #{len(self.field_provider.fields) + 1}')
self.first_row_start: GeoPoint | None = None
self.first_row_end: GeoPoint | None = None
self.last_row_end: GeoPoint | None = None
self.row_spacing = 0.5
self.padding = 1
self.padding_bottom = 2
self.row_number = 10
self.next: Callable = self.find_first_row

with ui.dialog() as self.dialog, ui.card().style('width: 900px; max-width: none'):
Expand All @@ -63,26 +59,25 @@ def find_first_row(self) -> None:
'right before the first crop. '
'The blue line should be in the center of the row.') \
.classes('text-lg text-center')
ui.label('Place the back center of the robot over the start point of the row.') \
.classes('text-lg text-center')
self.next = self.get_infos

def get_infos(self) -> None:
self.headline.text = 'Field Parameters'
assert self.gnss.current is not None
if not ("R" in self.gnss.current.mode or self.gnss.current.mode == "SSSS"):
with self.content:
ui.label('No RTK fix available.').classes('text-red')
self.first_row_start = self.gnss.current.location

self.row_sight.content = ''
crops = self.plant_locator.crop_category_names[:]
crops.remove('coin_with_hole')
self.content.clear()
with self.content:
ui.select(label='Cultivated Crop', options=crops, clearable=True).classes('w-40') \
.bind_value(self.field, 'crop')
ui.number('Crop Spacing', suffix='cm',
value=20, step=1, min=1, max=60) \
ui.number('Number of rows',
value=10, step=1, min=1, max=500) \
.props('dense outlined').classes('w-40') \
.tooltip('Set the distance between the crops')
.tooltip('Set the number of rows.')\
.bind_value(self, 'row_number')
ui.number('Row Spacing', suffix='cm',
value=50, step=5, min=20, max=100) \
.props('dense outlined').classes('w-40') \
Expand All @@ -93,90 +88,38 @@ def get_infos(self) -> None:
def find_row_ending(self) -> None:
self.headline.text = 'Find Row Ending'
self.content.clear()
with self.content:
with ui.row().classes('items-center'):
rosys.automation.automation_controls(self.automator)
ui.label('Press "Play" to start driving forward. '
'At the end of the row, press the "Stop" button.') \
.classes('text-lg text-center')
self.next = self.drive_to_last_row

def drive_to_last_row(self) -> None:
assert self.gnss.current is not None
if not("R" in self.gnss.current.mode or self.gnss.current.mode == "SSSS"):
with self.content:
ui.label('No RTK fix available.').classes('text-red')
self.first_row_end = self.gnss.current.location

self.headline.text = 'Drive to Last Row'
self.content.clear()
with self.content:
rosys.driving.joystick(self.steerer, size=50, color='#6E93D6')
ui.label('Drive the robot to the last row on this side, '
'right before the first crop.') \
ui.label('Drive the robot to the end of the current row.') \
.classes('text-lg text-center')
ui.label('Place the back center of the robot over the end point of the row.') \
.classes('text-lg text-center')
self.next = self.confirm_geometry

def confirm_geometry(self) -> None:
assert self.gnss.current is not None
if not("R" in self.gnss.current.mode or self.gnss.current.mode == "SSSS"):
if not ("R" in self.gnss.current.mode or self.gnss.current.mode == "SSSS"):
with self.content:
ui.label('No RTK fix available.').classes('text-red')
self.last_row_end = self.gnss.current.location

self.first_row_end = self.gnss.current.location
assert self.first_row_end is not None
assert self.last_row_end is not None
if not self.build_geometry():
d = self.first_row_end.distance(self.last_row_end)
ui.label(f'The distance between first row and last row is {d:.2f} m. '
f'Which does not match well with the provided row spacing of {self.row_spacing} cm.') \
.classes('text-red')

# TODO we do not need a last row
# we now only work with first row, row_spacing and numbers of rows
self.headline.text = 'Confirm Geometry'
self.content.clear()
with self.content:
with ui.row().classes('items-center'):
rosys.automation.automation_controls(self.automator)
ui.label('Press "Play" to start driving forward. '
'At the end of the row, press the "Stop" button.') \
.classes('w-64 text-lg')
ui.label(f'First Row Start: {self.first_row_start}').classes('text-lg')
ui.label(f'First Row End: {self.first_row_end}').classes('text-lg')
ui.label(f'Row Spacing: {self.row_spacing} m').classes('text-lg')
ui.label(f'Number of Rows: {self.row_number}').classes('text-lg')
ui.button('Cancel', on_click=self.dialog.close).props('color=red')
self.next = self._apply

def build_geometry(self) -> bool:
"""Build the geometry of the field based on the given points.
Returns True if the row spacing matches the distance between the first and last row, False otherwise.
Will create rows in any case to make testing easier.
"""
assert self.first_row_start is not None
assert self.first_row_end is not None
assert self.last_row_end is not None
distance = self.first_row_end.distance(self.last_row_end)
number_of_rows = distance / (self.row_spacing) + 1
# get AB line
a = self.first_row_start.cartesian()
b = self.first_row_end.cartesian()
c = self.last_row_end.cartesian()
ab = a.direction(b)
bc = b.direction(c)
d = a.polar(distance, bc)
for i in range(int(number_of_rows)):
start = a.polar(i * self.row_spacing, bc)
end = b.polar(i * self.row_spacing, bc)
self.field.rows.append(Row(id=str(uuid4()), name=f'Row #{len(self.field.rows)}',
points=[self.first_row_start.shifted(start),
self.first_row_start.shifted(end)]
))
bottom_left = a.polar(-self.padding_bottom, ab).polar(-self.padding, bc)
top_left = b.polar(self.padding, ab).polar(-self.padding, bc)
top_right = c.polar(self.padding, ab).polar(self.padding, bc)
bottom_right = d.polar(-self.padding_bottom, ab).polar(self.padding, bc)
self.field.points = [self.first_row_start.shifted(p) for p in [bottom_left, top_left, top_right, bottom_right]]
return 1 - number_of_rows % 1 < 0.1

def _apply(self) -> None:
self.dialog.close()
self.field_provider.fields.append(self.field)
self.field_provider.fields.append(Field(id=str(uuid4()), name=f'field_{len(self.field_provider.fields) + 1}', first_row_start=self.first_row_start,
first_row_end=self.first_row_end, row_spacing=self.row_spacing, row_number=self.row_number))
self.field_provider.request_backup()
self.field_provider.FIELDS_CHANGED.emit()

Expand Down
18 changes: 9 additions & 9 deletions field_friend/interface/components/field_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from field_friend.system import System


TabType = Literal["Plants", "Obstacles", "Outline", "Rows"]
TabType = Literal["Obstacles", "Outline", "Rows"]


class ActiveObject(TypedDict):
Expand All @@ -27,9 +27,9 @@ def __init__(self, system: 'System', leaflet: leaflet_map) -> None:
self.field_provider = system.field_provider
self.odometer = system.odometer
self.gnss = system.gnss
self.cultivatable_crops = system.crop_category_names
# self.cultivatable_crops = system.crop_category_names
self.leaflet_map = leaflet
self.tab: TabType = "Plants"
self.tab: TabType = "Outline"
self.active_object: ActiveObject | None = None
self.active_field: Field | None = None
self.restore_actives()
Expand Down Expand Up @@ -134,16 +134,16 @@ def show_field_settings(self) -> None:
.classes("ml-auto").style("display: block; margin-top:auto; margin-bottom: auto;") \
.tooltip("Delete field")
with ui.tabs().style("width: 100%;") as self.tabs:
ui.tab("Plants", "Plants")
# ui.tab("Plants", "Plants")
ui.tab("Outline", "Outline")
ui.tab("Obstacles", "Obstacles")
ui.tab("Rows", "Rows")
with ui.tab_panels(self.tabs, value=f"{self.tab}", on_change=self.set_tab).style("width: 100%;") as self.panels:
with ui.tab_panel("Plants").style("width: 100%;"):
ui.select(self.cultivatable_crops, label="Cultivated Crop", on_change=self.field_provider.request_backup) \
.classes("w-40").props('clearable') \
.bind_value(self.active_field, "crop") \
.tooltip('Set the cultivated crop which should be kept safe')
# with ui.tab_panel("Plants").style("width: 100%;"):
# ui.select(self.cultivatable_crops, label="Cultivated Crop", on_change=self.field_provider.request_backup) \
# .classes("w-40").props('clearable') \
# .bind_value(self.active_field, "crop") \
# .tooltip('Set the cultivated crop which should be kept safe')
with ui.tab_panel("Outline").style("width: 100%;"):
for geo_point in self.active_field.points:
with ui.row().style("width: 100%;"):
Expand Down
Loading

0 comments on commit 3aa9b9f

Please sign in to comment.