Skip to content

Commit

Permalink
Multiple Field Handling (#209)
Browse files Browse the repository at this point in the history
Currently only a single field can be saved to the field_provider. This
PR implements the ability to save, handle and delete multiple fields.
Also some improvements for the UI in managing the fields will be added.

Todo:

- [x] implement multiple field handling
   - [x] creating (including name of field)
   - [x] deleting
   - [x] change parameters 
- [x] write tests
- [x] test on a robot

---------

Co-authored-by: Pascal Schade <[email protected]>
  • Loading branch information
LukasBaecker and pascalzauberzeug authored Oct 25, 2024
1 parent 2d89396 commit 3724471
Show file tree
Hide file tree
Showing 11 changed files with 242 additions and 73 deletions.
45 changes: 38 additions & 7 deletions field_friend/automations/field_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,41 +19,55 @@ def __init__(self, gnss: Gnss) -> None:

self.FIELDS_CHANGED.register(self.refresh_fields)

self.selected_field: Field | None = None
self.FIELD_SELECTED = rosys.event.Event()
"""A field has been selected."""

def backup(self) -> dict:
return {
'fields': {f.id: f.to_dict() for f in self.fields},
'selected_field': self.selected_field.id if self.selected_field else None,
}

def restore(self, data: dict[str, dict]) -> None:
fields_data: dict[str, dict] = data.get('fields', {})
for field in list(fields_data.values()):
new_field = Field.from_dict(field)
self.fields.append(new_field)
selected_field_id = data.get('selected_field')
if selected_field_id:
self.selected_field = self.get_field(selected_field_id)
self.FIELD_SELECTED.emit()
self.FIELDS_CHANGED.emit()

def invalidate(self) -> None:
self.request_backup()
self.FIELDS_CHANGED.emit()
if self.selected_field and self.selected_field not in self.fields:
self.selected_field = None
self.FIELD_SELECTED.emit()

def get_field(self, id_: str | None) -> Field | None:
return next((f for f in self.fields if f.id == id_), None)

def create_field(self, new_field: Field) -> Field:
# TODO: delete the clear when we want to save multiple fields again
self.fields.clear()
self.fields.append(new_field)
self.select_field(new_field.id)
self.invalidate()
return new_field

def clear_fields(self) -> None:
self.fields.clear()
self.invalidate()

def delete_field(self, id_: str) -> None:
field = self.get_field(id_)
if field:
self.fields.remove(field)
self.invalidate()
def delete_selected_field(self) -> None:
if not self.selected_field:
self.log.warning('No field selected. Nothing was deleted.')
return
name = self.selected_field.name
self.fields.remove(self.selected_field)
self.log.info('Field %s has been deleted.', name)
self.invalidate()

def is_polygon(self, field: Field) -> bool:
try:
Expand All @@ -76,3 +90,20 @@ def add_row_support_point(self, field_id: str, row_support_point: RowSupportPoin
def refresh_fields(self) -> None:
for field in self.fields:
field.refresh()

def select_field(self, id_: str | None) -> None:
self.selected_field = self.get_field(id_)
self.FIELD_SELECTED.emit()

def update_field_parameters(self, field_id: str, name: str, row_number: int, row_spacing: float, outline_buffer_width: float) -> None:
field = self.get_field(field_id)
if not field:
self.log.warning('Field with id %s not found. Cannot update parameters.', field_id)
return
field.name = name
field.row_number = row_number
field.row_spacing = row_spacing
field.outline_buffer_width = outline_buffer_width
self.log.info('Updated parameters for field %s: row number = %d, row spacing = %f',
field.name, row_number, row_spacing)
self.invalidate()
10 changes: 5 additions & 5 deletions field_friend/automations/navigation/field_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def __init__(self, system: 'System', implement: Implement) -> None:
self.end_point: Point | None = None

self.field: Field | None = None
self.field_id: str | None = self.field_provider.selected_field.id if self.field_provider.selected_field else None
self.field_provider.FIELD_SELECTED.register(self._set_field_id)
self._loop: bool = False
self._drive_step = self.DRIVE_STEP
self._turn_step = self.TURN_STEP
Expand All @@ -55,6 +57,7 @@ def current_row(self) -> Row:

async def prepare(self) -> bool:
await super().prepare()
self.field = self.field_provider.get_field(self.field_id)
if self.field is None:
rosys.notify('No field selected', 'negative')
return False
Expand Down Expand Up @@ -268,11 +271,8 @@ def developer_ui(self) -> None:
.bind_value(self, '_max_gnss_waiting_time') \
.tooltip(f'MAX_GNSS_WAITING_TIME (default: {self.MAX_GNSS_WAITING_TIME:.2f}s)')

def _set_field(self, field_id: str) -> None:
field = self.field_provider.get_field(field_id)
if field is not None:
self.field = field
self.field_provider.FIELDS_CHANGED.emit()
def _set_field_id(self) -> None:
self.field_id = self.field_provider.selected_field.id if self.field_provider.selected_field else None

def create_simulation(self, crop_distance: float = 0.5) -> None:
self.detector.simulated_objects.clear()
Expand Down
24 changes: 19 additions & 5 deletions field_friend/interface/components/field_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ def __init__(self, system: 'System'):
self.field_provider = system.field_provider
self.first_row_start: GeoPoint | None = None
self.first_row_end: GeoPoint | None = None
self.row_spacing = 0.5
self.row_number = 10
self.field_name: str = 'Field'
self.row_spacing: float = 0.5
self.row_number: int = 10
self.outline_buffer_width: float = 2.0
self.next: Callable = self.find_first_row

with ui.dialog() as self.dialog, ui.card().style('width: 900px; max-width: none'):
Expand Down Expand Up @@ -64,6 +66,10 @@ def get_infos(self) -> None:
self.row_sight.content = ''
self.content.clear()
with self.content:
ui.input('Field Name') \
.props('dense outlined').classes('w-40') \
.tooltip('Enter a name for the field') \
.bind_value(self, 'field_name')
ui.number('Number of rows',
value=10, step=1, min=1) \
.props('dense outlined').classes('w-40') \
Expand All @@ -74,6 +80,11 @@ def get_infos(self) -> None:
.props('dense outlined').classes('w-40') \
.tooltip('Set the distance between the rows') \
.bind_value(self, 'row_spacing', forward=lambda v: v / 100.0, backward=lambda v: v * 100.0)
ui.number('Outline Buffer Width', suffix='m',
value=2, step=0.1, min=1) \
.props('dense outlined').classes('w-40') \
.tooltip('Set the width of the buffer around the field outline') \
.bind_value(self, 'outline_buffer_width')
self.next = self.find_row_ending

def find_row_ending(self) -> None:
Expand All @@ -98,10 +109,12 @@ def confirm_geometry(self) -> None:
self.content.clear()
with self.content:
with ui.row().classes('items-center'):
ui.label(f'Field Name: {self.field_name}').classes('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'Row Spacing: {self.row_spacing*100} cm').classes('text-lg')
ui.label(f'Number of Rows: {self.row_number}').classes('text-lg')
ui.label(f'Outline Buffer Width: {self.outline_buffer_width} m').classes('text-lg')
with ui.row().classes('items-center'):
ui.button('Cancel', on_click=self.dialog.close).props('color=red')
self.next = self._apply
Expand All @@ -112,11 +125,12 @@ def _apply(self) -> None:
ui.notify('No valid field parameters.')
return
self.field_provider.create_field(Field(id=str(uuid4()),
name='Field 1',
name=self.field_name,
first_row_start=self.first_row_start,
first_row_end=self.first_row_end,
row_spacing=self.row_spacing,
row_number=self.row_number))
row_number=self.row_number,
outline_buffer_width=self.outline_buffer_width))
self.first_row_start = None
self.first_row_end = None

Expand Down
4 changes: 2 additions & 2 deletions field_friend/interface/components/field_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def __init__(self, system: 'System') -> None:
self.field_provider: FieldProvider = system.field_provider
self._update()
self.field_provider.FIELDS_CHANGED.register_ui(self._update)
self.field_provider.FIELD_SELECTED.register_ui(self._update)

def create_fence(self, start, end):
height = 0.12
Expand Down Expand Up @@ -46,8 +47,7 @@ def create_fence(self, start, end):
'field_').material('#8b4513').rotate(np.pi/2, 0, 0)

def _update(self) -> None:
if isinstance(self.system.current_navigation, FieldNavigation):
self.update(self.system.current_navigation.field)
self.update(self.system.field_provider.selected_field)

def update(self, active_field: Field | None) -> None:
[obj.delete() for obj in list(self.scene.objects.values()) if obj.name and obj.name.startswith('field_')]
Expand Down
40 changes: 13 additions & 27 deletions field_friend/interface/components/leaflet_map.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@

import logging
from typing import TYPE_CHECKING, Literal, TypedDict
from typing import TYPE_CHECKING

from nicegui import app, ui
from nicegui.elements.leaflet_layers import GenericLayer, Marker, TileLayer

from ...automations import Field, Row
from ...localization.geo_point import GeoPoint
from .key_controls import KeyControls


class Active_object(TypedDict):
object_type: Literal["Rows", "Outline"]
object: Row


if TYPE_CHECKING:
from field_friend.system import System

Expand Down Expand Up @@ -43,24 +36,20 @@ def __init__(self, system: 'System', draw_tools: bool) -> None:
center_point = self.system.gnss.current.location
self.m: ui.leaflet
if draw_tools:
self.m = ui.leaflet(center=center_point.tuple,
zoom=13, draw_control=self.draw_control)
self.m = ui.leaflet(center=center_point.tuple, zoom=13, draw_control=self.draw_control)
else:
self.m = ui.leaflet(center=center_point.tuple,
zoom=13)
self.m = ui.leaflet(center=center_point.tuple, zoom=13)
self.m.clear_layers()
self.current_basemap: TileLayer | None = None
self.toggle_basemap()
self.field_layers: list[GenericLayer] = []
self.robot_marker: Marker | None = None
self.drawn_marker = None
self.row_layers: list = []
self.active_field: str | None = None
self.set_active_field()
self.update_layers()
self.zoom_to_robot()
self.field_provider.FIELDS_CHANGED.register(self.set_active_field)
self.field_provider.FIELDS_CHANGED.register(self.update_layers)
self.field_provider.FIELDS_CHANGED.register_ui(self.update_layers)
self.field_provider.FIELD_SELECTED.register_ui(self.update_layers)

self.gnss.ROBOT_GNSS_POSITION_CHANGED.register_ui(self.update_robot_position)

Expand All @@ -77,8 +66,9 @@ def buttons(self) -> None:
ui.button(on_click=self.zoom_to_field) \
.props('icon=polyline dense flat') \
.tooltip('center map on field boundaries').classes('ml-0')
ui.button("Update reference", on_click=self.gnss.update_reference).props("outline color=warning") \
.tooltip("Set current position as geo reference and restart the system").classes("ml-auto").style("display: block; margin-top:auto; margin-bottom: auto;")
ui.button('Update reference', on_click=self.gnss.update_reference).props('outline color=warning') \
.tooltip('Set current position as geo reference and restart the system').classes('ml-auto') \
.style('display: block; margin-top:auto; margin-bottom: auto;')

def abort_point_drawing(self, dialog) -> None:
self.on_dialog_close()
Expand All @@ -90,16 +80,15 @@ def update_layers(self) -> None:
self.m.remove_layer(layer)
self.field_layers = []
for field in self.field_provider.fields:
color = '#6E93D6' if field.id == self.active_field else '#999'
self.field_layers.append(self.m.generic_layer(name="polygon",
color = '#6E93D6' if self.field_provider.selected_field is not None and field.id == self.field_provider.selected_field.id else '#999'
self.field_layers.append(self.m.generic_layer(name='polygon',
args=[field.outline_as_tuples, {'color': color}]))
current_field: Field | None = self.field_provider.get_field(self.active_field)
for layer in self.row_layers:
self.m.remove_layer(layer)
self.row_layers = []
if current_field is not None:
for row in current_field.rows:
self.row_layers.append(self.m.generic_layer(name="polyline",
if self.field_provider.selected_field is not None:
for row in self.field_provider.selected_field.rows:
self.row_layers.append(self.m.generic_layer(name='polyline',
args=[row.points_as_tuples, {'color': '#F2C037'}]))

def update_robot_position(self, position: GeoPoint, dialog=None) -> None:
Expand Down Expand Up @@ -157,6 +146,3 @@ def on_dialog_close(self) -> None:
if self.drawn_marker is not None:
self.m.remove_layer(self.drawn_marker)
self.drawn_marker = None

def set_active_field(self) -> None:
self.active_field = self.field_provider.fields[0].id if len(self.field_provider.fields) > 0 else None
Loading

0 comments on commit 3724471

Please sign in to comment.