Add waypoint list panel and bottom stats bar
- BottomStatsBar: persistent "Details: N WP · dist m · duration" pill
(HTML prototype's #bottomBar/#statsRow), tap opens the full waypoint
list. Mission/drone name and the Send button are intentionally not part
of this - mission library and upload aren't wired up yet.
- A warn banner ("Route exceeds drone capabilities at highlighted point")
appears above it whenever buildRouteGeometry finds a bad vertex or leg -
a direct, low-effort payoff from the fillet geometry work.
- WaypointListPanel: full-screen overlay listing every waypoint
(altitude/speed/execute/catch radius), matching the prototype's List
tab. Only one row editable at a time; editing swaps plain text for
draggable WaypointChip values and an execute dropdown. Row tap (outside
edit mode) centers the map on that waypoint and closes the panel; delete
removes it and adjusts the reticle's target index the same way the halo
menu's remove already does.
- WaypointChip: vertical-drag value chip (HTML prototype's .value-chip),
fixed step of 10 per 14px matching chipFieldConfig - simpler than the
original's dual vertical/horizontal axis-lock gesture, which felt like
more fidelity than the interaction warrants here.
- Provider gained setCatchRadius and setAction (direct assignment, as
opposed to toggleAction's toggle-to-none semantics used by the halo
menu) to support the list's editable fields.
- Altitude/Speed full-chart tabs from the prototype (drag-to-adjust
altitude/speed profile charts) are a separate, larger feature and not
part of this step.
Centering on a waypoint from the list calls MapController.move(), which -
like the reticle's auto-snap - doesn't emit MoveEnd, so snap detection is
invoked manually afterward to match the prototype's resulting behavior.
Added 4 widget tests (open empty list, row-tap centers and closes, delete
removes and updates the empty state, chip drag updates altitude) - one
required a pump(duration) after the drag to flush the chip's 120ms flash
Timer, which was otherwise still pending at test teardown. All 15 tests
and flutter analyze pass. Manually verified on the Pixel_10a emulator:
opened the list with two waypoints, edited a row's altitude via chip drag
(confirmed value change, accounting for device-pixel-ratio scaling vs the
widget-test logical-pixel case), and confirmed row-tap re-centers/closes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b93bbab23b
commit
d3c5a68636
@@ -9,10 +9,12 @@ import 'package:latlong2/latlong.dart';
|
||||
import '../../../domain/mission/route_geometry.dart';
|
||||
import '../../../domain/waypoint/flat_waypoint_list.dart';
|
||||
import '../../providers/current_mission_provider.dart';
|
||||
import '../../widgets/bottom_stats_bar.dart';
|
||||
import '../../widgets/halo_menu.dart';
|
||||
import '../../widgets/mission_map.dart';
|
||||
import '../../widgets/reticle_button.dart';
|
||||
import '../../widgets/value_wheel.dart';
|
||||
import '../../widgets/waypoint_list_panel.dart';
|
||||
|
||||
/// Reticle-Zustand (HTML-Demonstrator: Variable `mode`).
|
||||
/// idle: kein Wegpunkt in der Naehe, Tap legt einen neuen an.
|
||||
@@ -38,6 +40,7 @@ class _PlanScreenState extends ConsumerState<PlanScreen> {
|
||||
_ReticleMode _mode = _ReticleMode.idle;
|
||||
int _targetIndex = -1;
|
||||
bool _mapReady = false;
|
||||
bool _listOpen = false;
|
||||
|
||||
// TODO: aus dem aktiven Drohnen-Profil (Doku 3.5) uebernehmen, sobald die
|
||||
// Profilverwaltung existiert. Werte entsprechen dem Default-Profil
|
||||
@@ -214,6 +217,71 @@ class _PlanScreenState extends ConsumerState<PlanScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// Wegpunktliste (Doku 3.6): Aenderungen wirken auf einen explizit
|
||||
// angegebenen Index, nicht zwingend den aktuellen Reticle-Zielpunkt.
|
||||
|
||||
void _onRowAltitudeChanged(int index, double value) {
|
||||
ref.read(currentMissionProvider.notifier).setAltitude(index, value);
|
||||
if (_mode == _ReticleMode.editing && index == _targetIndex) {
|
||||
setState(() => _curAlt = value);
|
||||
}
|
||||
}
|
||||
|
||||
void _onRowSpeedChanged(int index, double value) {
|
||||
ref.read(currentMissionProvider.notifier).setSpeed(index, value);
|
||||
if (_mode == _ReticleMode.editing && index == _targetIndex) {
|
||||
setState(() => _curSpeed = value);
|
||||
}
|
||||
}
|
||||
|
||||
void _onRowCatchRadiusChanged(int index, double value) {
|
||||
ref.read(currentMissionProvider.notifier).setCatchRadius(index, value);
|
||||
}
|
||||
|
||||
void _onRowActionChanged(int index, WaypointAction action) {
|
||||
ref.read(currentMissionProvider.notifier).setAction(index, action);
|
||||
}
|
||||
|
||||
void _onRowDelete(int index) {
|
||||
ref.read(currentMissionProvider.notifier).removeAt(index);
|
||||
setState(() {
|
||||
if (_targetIndex == index) {
|
||||
_mode = _ReticleMode.idle;
|
||||
_targetIndex = -1;
|
||||
} else if (_targetIndex > index) {
|
||||
_targetIndex -= 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Zeile antippen zentriert die Karte auf diesen Wegpunkt und schliesst
|
||||
/// die Liste (Doku 3.6). Danach wird die Snap-Erkennung einmal manuell
|
||||
/// angestossen, da flutter_maps MapController.move() - anders als
|
||||
/// Leaflets panTo() - kein MoveEnd-Event feuert.
|
||||
void _onCenterOnWaypoint(int index) {
|
||||
final waypoints = ref.read(currentMissionProvider);
|
||||
if (index < 0 || index >= waypoints.length) return;
|
||||
final wp = waypoints[index];
|
||||
setState(() => _listOpen = false);
|
||||
_mapController.move(LatLng(wp.lat, wp.lon), _mapController.camera.zoom);
|
||||
_handleMoveEnd();
|
||||
}
|
||||
|
||||
({double distanceM, double durationSec}) _missionStats(
|
||||
List<Waypoint> waypoints,
|
||||
) {
|
||||
var totalDist = 0.0;
|
||||
var totalDur = 0.0;
|
||||
for (var i = 0; i < waypoints.length - 1; i++) {
|
||||
final a = LatLng(waypoints[i].lat, waypoints[i].lon);
|
||||
final b = LatLng(waypoints[i + 1].lat, waypoints[i + 1].lon);
|
||||
final d = _distance(a, b);
|
||||
totalDist += d;
|
||||
totalDur += d / waypoints[i + 1].speedMs;
|
||||
}
|
||||
return (distanceM: totalDist, durationSec: totalDur);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final waypoints = ref.watch(currentMissionProvider);
|
||||
@@ -224,6 +292,7 @@ class _PlanScreenState extends ConsumerState<PlanScreen> {
|
||||
maxClimbRate: _maxClimbRate,
|
||||
maxDescentRate: _maxDescentRate,
|
||||
);
|
||||
final missionStats = _missionStats(waypoints);
|
||||
|
||||
List<LatLng>? rubberBand;
|
||||
if (_mapReady && _mode == _ReticleMode.idle && waypoints.isNotEmpty) {
|
||||
@@ -314,6 +383,52 @@ class _PlanScreenState extends ConsumerState<PlanScreen> {
|
||||
right: 70,
|
||||
child: Center(child: _readout('Speed', _curSpeed, 'm/s')),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (routeGeometry.vertexBad.any((bad) => bad) ||
|
||||
routeGeometry.segments.any((s) => s.bad))
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: const Color(0xFF7C2D12),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
|
||||
child: const Text(
|
||||
'Route exceeds drone capabilities at highlighted point',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Color(0xFFFDE3D0), fontSize: 12),
|
||||
),
|
||||
),
|
||||
BottomStatsBar(
|
||||
waypointCount: waypoints.length,
|
||||
totalDistanceM: missionStats.distanceM,
|
||||
totalDurationSec: missionStats.durationSec,
|
||||
onTap: () => setState(() => _listOpen = true),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_listOpen)
|
||||
Positioned.fill(
|
||||
child: WaypointListPanel(
|
||||
activeIndex: _mode == _ReticleMode.idle ? -1 : _targetIndex,
|
||||
vertexBad: routeGeometry.vertexBad,
|
||||
altMin: _altMin,
|
||||
altMax: _altMax,
|
||||
speedMin: _speedMin,
|
||||
speedMax: _speedMax,
|
||||
onClose: () => setState(() => _listOpen = false),
|
||||
onCenterOnWaypoint: _onCenterOnWaypoint,
|
||||
onDelete: _onRowDelete,
|
||||
onAltitudeChanged: _onRowAltitudeChanged,
|
||||
onSpeedChanged: _onRowSpeedChanged,
|
||||
onCatchRadiusChanged: _onRowCatchRadiusChanged,
|
||||
onActionChanged: _onRowActionChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user