Skip to content

Commit

Permalink
Position enhancements (#112)
Browse files Browse the repository at this point in the history
- Show more orientation data
- Allow user to enter coordinates in degrees/minutes/seconds format
- Generalize the Autonomy page to be a Map page for placing markers
- Show an arrow on the map to indicate direction
  • Loading branch information
Levi-Lesches authored Jun 1, 2023
1 parent 2a87c03 commit bdb0f50
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 37 deletions.
1 change: 1 addition & 0 deletions lib/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export "src/models/view/science.dart";

// Builder models
export "src/models/view/builders/autonomy_command.dart";
export "src/models/view/builders/gps.dart";
export "src/models/view/builders/science_command.dart";
export "src/models/view/builders/mars_command.dart";
export "src/models/view/builders/builder.dart";
Expand Down
2 changes: 1 addition & 1 deletion lib/pages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Routes {
static const String science = "Science Analysis";

/// The name of the autonomy page.
static const String autonomy = "Autonomy";
static const String autonomy = "Map";

/// The name of the MARS page.
static const String mars = "MARS";
Expand Down
8 changes: 7 additions & 1 deletion lib/src/data/metrics/position.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ class PositionMetrics extends Metrics<RoverPosition> {
" Latitude: ${data.gps.latitude.toStringAsFixed(2)}°",
" Longitude: ${data.gps.longitude.toStringAsFixed(2)}°",
" Altitude: ${data.gps.altitude.toStringAsFixed(2)} m",
"Orientation: ${data.orientation.y.toStringAsFixed(2)}° of N",
"Orientation:",
" X: ${data.orientation.x.toStringAsFixed(2)}°",
" Y: ${data.orientation.y.toStringAsFixed(2)}°",
" Z: ${data.orientation.z.toStringAsFixed(2)}°",
"Distance: ${data.gps.distanceTo(baseStation).toStringAsFixed(2)} m",
];

Expand All @@ -38,4 +41,7 @@ class PositionMetrics extends Metrics<RoverPosition> {
super.update(value);
models.sockets.mars.sendMessage(MarsCommand(rover: value.gps));
}

/// The angle to orient the rover on the top-down map.
double get angle => data.orientation.z;
}
2 changes: 1 addition & 1 deletion lib/src/data/protobuf.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ extension AutonomyStateUtils on AutonomyState {
/// The human-readable name of the task.
String get humanName {
switch (this) {
case AutonomyState.AUTONOMY_STATE_UNDEFINED: return "";
case AutonomyState.AUTONOMY_STATE_UNDEFINED: return "Disabled";
case AutonomyState.PATHING: return "Calculating path...";
case AutonomyState.APPROACHING: return "Approaching destination";
case AutonomyState.AT_DESTINATION: return "Arrived at destination";
Expand Down
28 changes: 26 additions & 2 deletions lib/src/models/view/autonomy.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ enum AutonomyCell {
/// This cell is along the rover's path to its destination.
path,
/// This cell is traversable but otherwise not of interest.
empty
empty,
/// THis cell was manually marked as a point of interest.
marker,
}

/// Like an [Offset] from Flutter, but using integers instead of doubles.
Expand Down Expand Up @@ -75,6 +77,11 @@ class AutonomyModel with ChangeNotifier {
]
];

/// A list of markers maanually placed by the user. Useful for the Extreme Retrieval Mission.
List<GpsCoordinates> markers = [];
/// The view model to edit the coordinate of the marker.
GpsBuilder markerBuilder = GpsBuilder();

/// The rover's current position.
GpsCoordinates get roverPosition => models.rover.metrics.position.data.gps;

Expand All @@ -84,14 +91,17 @@ class AutonomyModel with ChangeNotifier {
/// The grid of size [gridSize] with the rover in the center, ready to draw on the UI.
List<List<AutonomyCell>> get grid {
final result = empty;
markCell(result, roverPosition, AutonomyCell.rover);
markCell(result, data.destination, AutonomyCell.destination);
markCell(result, roverPosition, AutonomyCell.rover);
for (final obstacle in data.obstacles) {
markCell(result, obstacle, AutonomyCell.obstacle);
}
for (final path in data.path) {
markCell(result, path, AutonomyCell.path);
}
for (final marker in markers) {
markCell(result, marker, AutonomyCell.marker);
}
return result;
}

Expand Down Expand Up @@ -143,4 +153,18 @@ class AutonomyModel with ChangeNotifier {
data.mergeFromMessage(value);
notifyListeners();
}

/// Places the marker in [markerBuilder].
void placeMarker() {
markers.add(markerBuilder.value);
markerBuilder.clear();
notifyListeners();
}

/// Deletes all the markers in [markers].
void clearMarkers() {
markers.clear();
markerBuilder.clear();
notifyListeners();
}
}
12 changes: 6 additions & 6 deletions lib/src/models/view/builders/autonomy_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import "package:rover_dashboard/models.dart";
class AutonomyCommandBuilder extends ValueBuilder<AutonomyCommand> {
/// The type of task the rover should complete.
AutonomyTask task = AutonomyTask.GPS_ONLY;
/// The latitude of the destination.
final latitude = NumberBuilder<double>(0);
/// The longitude of the destination.
final longitude = NumberBuilder<double>(0);

/// The view model to edit the [AutonomyCommand.destination].
final gps = GpsBuilder();

/// The handshake as received by the rover after [submit] is called.
AutonomyCommand? _handshake;

/// Whether the dashboard is awaiting a response from the rover.
bool isLoading = false;

Expand All @@ -31,11 +31,11 @@ class AutonomyCommandBuilder extends ValueBuilder<AutonomyCommand> {
}

@override
bool get isValid => latitude.isValid && longitude.isValid;
bool get isValid => gps.isValid;

@override
AutonomyCommand get value => AutonomyCommand(
destination: GpsCoordinates(latitude: latitude.value, longitude: longitude.value),
destination: gps.value,
task: task,
);

Expand Down
12 changes: 10 additions & 2 deletions lib/src/models/view/builders/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ class NumberBuilder<T extends num> extends TextBuilder<T> {
bool get isInteger => List<int> == List<T>;

/// The minimum allowed value.
num? min;
final num? min;

/// The maximum allowed value.
num? max;
final num? max;

/// Creates a number builder based on an initial value.
NumberBuilder(super.value, {this.min, this.max});
Expand All @@ -95,6 +95,14 @@ class NumberBuilder<T extends num> extends TextBuilder<T> {
}
notifyListeners();
}

/// Clears the value in this builder.
void clear() {
error = null;
value = (isInteger ? 0 : 0.0) as T;
controller.text = value.toString();
notifyListeners();
}
}

/// A specialized [TextBuilder] to handle IP addresses.
Expand Down
84 changes: 84 additions & 0 deletions lib/src/models/view/builders/gps.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import "package:rover_dashboard/data.dart";
import "package:rover_dashboard/models.dart";

/// The format to enter a GPS coordinate.
///
/// Internally, all the math is done in [decimal], but [GpsBuilder] supports [degrees] as well.
enum GpsType {
/// Degrees, minutes, seconds format.
degrees,
/// Decimal longitude and latitude.
decimal;

/// The human-readable name of the GPS type.
String get humanName => switch (this) {
GpsType.degrees => "Degrees",
GpsType.decimal => "Decimal",
};
}

/// A [ValueBuilder] to modify a [GpsCoordinates] object in either [GpsType].
class GpsBuilder extends ValueBuilder<GpsCoordinates> {
/// The format to enter a GPS coordinate.
GpsType type = GpsType.decimal;

/// Longitude in decimal degrees.
final longDecimal = NumberBuilder<double>(0);
/// Latitude in decimal degrees.
final latDecimal = NumberBuilder<double>(0);

/// Longitude in degrees.
final longDegrees = NumberBuilder<int>(0);
/// Longitude in minutes.
final longMinutes = NumberBuilder<int>(0);
/// Longitude in seconds.
final longSeconds = NumberBuilder<int>(0);

/// Latitude in degrees.
final latDegrees = NumberBuilder<int>(0);
/// Latitude in minutes.
final latMinutes = NumberBuilder<int>(0);
/// Latitude in seconds.
final latSeconds = NumberBuilder<int>(0);

@override
List<NumberBuilder<dynamic>> get otherBuilders => [
longDecimal, latDecimal,
longDegrees, longMinutes, longSeconds,
latDegrees, latMinutes, latSeconds,
];

@override
bool get isValid => type == GpsType.decimal
? (latDecimal.isValid && longDecimal.isValid)
: (latDegrees.isValid && longDegrees.isValid && latMinutes.isValid && longMinutes.isValid && latSeconds.isValid && longSeconds.isValid);

@override
GpsCoordinates get value => switch (type) {
GpsType.decimal => GpsCoordinates(longitude: longDecimal.value, latitude: latDecimal.value),
GpsType.degrees => GpsCoordinates(
longitude: longDegrees.value + (longMinutes.value / 60) + (longSeconds.value / 3600),
latitude: latDegrees.value + (latMinutes.value / 60) + (latSeconds.value / 3600),
),
};

/// Clears all the data in these text boxes.
void clear() {
longDecimal.clear();
longDegrees.clear();
longMinutes.clear();
longSeconds.clear();

latDecimal.clear();
latDegrees.clear();
latMinutes.clear();
latSeconds.clear();
notifyListeners();
}

/// Updates the [GpsType].
void updateType(GpsType input) {
type = input;
notifyListeners();
}
}
65 changes: 43 additions & 22 deletions lib/src/pages/autonomy.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "dart:math";
import "package:flutter/material.dart";

import "package:rover_dashboard/data.dart";
Expand All @@ -16,6 +17,7 @@ class AutonomyPage extends StatelessWidget {
AutonomyCell.obstacle => Colors.black,
AutonomyCell.path => Colors.blueGrey,
AutonomyCell.empty => Colors.white,
AutonomyCell.marker => Colors.red,
};

@override
Expand All @@ -27,11 +29,20 @@ class AutonomyPage extends StatelessWidget {
const SizedBox(height: 48),
for (final row in model.grid) Expanded(
child: Row(children: [
for (final cell in row) Expanded(child: Container(
height: double.infinity,
width: 24,
decoration: BoxDecoration(color: getColor(cell), border: Border.all()),
),)
for (final cell in row) Expanded(
child: Container(
height: double.infinity,
width: 24,
decoration: BoxDecoration(color: getColor(cell), border: Border.all()),
child: cell != AutonomyCell.rover ? null : ProviderConsumer<PositionMetrics>.value(
value: models.rover.metrics.position,
builder: (position) => Transform.rotate(
angle: position.angle * pi / 180,
child: const Icon(Icons.arrow_upward, size: 24),
),
),
),
),
],),
),
const SizedBox(height: 4),
Expand All @@ -54,6 +65,10 @@ class AutonomyPage extends StatelessWidget {
Container(width: 24, height: 24, color: Colors.blueGrey),
const SizedBox(width: 4),
Text("Path", style: context.textTheme.titleMedium),
const SizedBox(width: 24),
Container(width: 24, height: 24, color: Colors.red),
const SizedBox(width: 4),
Text("Marker", style: context.textTheme.titleMedium),
const Spacer(),
Text("Zoom: ", style: context.textTheme.titleLarge),
Expanded(flex: 2, child: Slider(
Expand All @@ -65,24 +80,24 @@ class AutonomyPage extends StatelessWidget {
onChanged: (value) => model.zoom(value.toInt()),
),),
],),
Row(children: [
const SizedBox(width: 4),
Text("Place marker: ", style: context.textTheme.titleLarge),
const SizedBox(width: 8),
ElevatedButton.icon(icon: const Icon(Icons.clear), label: const Text("Clear all"), onPressed: model.clearMarkers),
const Spacer(),
GpsEditor(model: model.markerBuilder),
const SizedBox(width: 8),
ElevatedButton(onPressed: model.placeMarker, child: const Text("Place")),
const SizedBox(width: 8),
],),
const Divider(),
ProviderConsumer<AutonomyCommandBuilder>(
create: () => AutonomyCommandBuilder(),
builder: (command) => Row(mainAxisSize: MainAxisSize.min, children: [
const SizedBox(width: 4),
Text("Next destination: ", style: context.textTheme.titleLarge),
Text("Autonomy: ", style: context.textTheme.titleLarge),
const Spacer(),
SizedBox(width: 250, child: NumberEditor(
name: "Longitude",
model: command.longitude,
width: 12,
titleFlex: 1,
),),
SizedBox(width: 250, child: NumberEditor(
name: "Latitude",
model: command.latitude,
width: 12,
titleFlex: 1,
),),
DropdownEditor<AutonomyTask>(
name: "Task type",
value: command.task,
Expand All @@ -93,6 +108,9 @@ class AutonomyPage extends StatelessWidget {
onChanged: command.updateTask,
humanName: (task) => task.humanName,
),
const SizedBox(width: 16),
GpsEditor(model: command.gps),
const SizedBox(width: 8),
ElevatedButton(
onPressed: command.isLoading ? null : command.submit,
child: const Text("Submit"),
Expand All @@ -105,14 +123,17 @@ class AutonomyPage extends StatelessWidget {
),
Container(
color: context.colorScheme.surface,
height: 48,
height: 50,
child: Row(children: [ // The header at the top
const SizedBox(width: 8),
Text("Autonomy", style: context.textTheme.headlineMedium),
Text("Map", style: context.textTheme.headlineMedium),
const Spacer(),
Text("State: ${model.data.state.humanName}", style: context.textTheme.headlineSmall),
Text("Autonomy status", style: context.textTheme.headlineMedium),
const SizedBox(width: 16),
Text("State: ${model.data.state.humanName}.", style: context.textTheme.titleLarge),
const SizedBox(width: 8),
Text("Task: ${model.data.task.humanName}", style: context.textTheme.titleLarge),
const VerticalDivider(),
Text("Task: ${model.data.task.humanName}", style: context.textTheme.headlineSmall),
const ViewsSelector(currentView: Routes.autonomy),
],),
),
Expand Down
Loading

0 comments on commit bdb0f50

Please sign in to comment.