Fills in MspFlightControllerLink.uploadMission(), which previously just
threw UnimplementedError, plus a new verifyMission() (both now on the
generic FlightControllerLink interface, protocol-neutral by signature -
MAVLink/ArduPilot get their own implementation later without touching
callers).
All iNAV-specific encoding lives in the new msp_waypoint_codec.dart:
- encodeMspSetWaypoint(): the 21-byte MSP_SET_WP payload. Action/P1/P2/P3
byte layout was checked against the actual iNAV 9.1.0 source
(navigation.c/navigation.h), not guessed - notably our generic `loiter`
action has no configurable duration, so it maps to
NAV_WP_ACTION_HOLD_TIME with the max representable p1 (int16 max, not
0xFFFF - that would read as -1 and end the hold immediately instead of
never).
- parseMspWpGetInfo(): decodes MSP_WP_GETINFO's validity/count fields,
used by verifyMission() to confirm the FC actually accepted the full
mission (Doku 2.2/4.5 Ready-to-Fly-Gate: "upload + verified").
uploadMission() sends one MSP_SET_WP per waypoint in order (iNAV has no
batch command - WP#1 resets the FC's mission list, every next number must
follow immediately, only the last carries NAV_WP_FLAG_LAST) and rejects
missions above NAV_MAX_WAYPOINTS upfront instead of silently truncating.
MissionSyncService now calls the real verifyMission() instead of always
confirming, throwing MissionVerificationException when the FC doesn't
confirm the mission.
FlyScreen's footer swaps the warnings button for a send button (Doku:
"ersetze den warnings button mit einem wp send button", pink horizontal
PaperPlaneIcon, matching the existing paper-plane drone iconography) -
warnings/event log stay reachable via the drone status pill's Warnings
tab. BottomStatsBar/MissionFooterBar gained onSendTap/sending in place of
the old forceShowWarningsButton.
Verified end to end on the Pixel_10a emulator: tapping send with WLAN as
the active connection type triggers the real WifiNetworkSpecifier flow
through MspFlightControllerLink (correctly reports "no devices found" -
expected, no real mLRS bridge on the emulator); the actual MSP_SET_WP/
MSP_WP_GETINFO wire behavior is covered by tests against a fake FC
responder over LoopbackTransport instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Mini-Altitude-Profile terrain data only ever lived in TerrainNotifier's
in-memory state (one slot, keyed by route). Every app restart, or every
switch away from and back to a mission, forced a full re-fetch of all
AWS Terrarium elevation tiles for the route - the noticeably slow load
the user reported for some missions was this happening on every visit,
not just once.
Adds a nullable terrainProfileJson column to the missions table
(schema v6->v7) and a saveTerrainProfile() repository method, kept
separate from the regular waypoint upsert() so an ordinary autosave never
clobbers an already-cached profile. TerrainNotifier persists a profile
right after a successful fetch (fire-and-forget) and gains restore(),
called from CurrentMissionMetaNotifier whenever a mission is loaded/
started so a previously fetched profile is available immediately -
ensureFor() still validates its routeKey before use, so a stale restored
profile is never shown for a route that has since changed.
Also included in mission export/import (MissionExportData/
ParsedMissionImport) so sharing a mission carries its terrain cache along
instead of forcing the recipient to refetch it.
Sample-count/resolution stays as-is for now (still fixed 30m spacing,
10-2000 samples) - adapting the sampling density to terrain variance
(e.g. coarser sampling over flat ground) is a separate follow-up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fly mode previously showed just a black placeholder. It now renders the
same MissionMap (route + waypoint markers) and footer (warnings banner,
altitude profile, BottomStatsBar) as the Plan screen, minus the
reticle/wheels since flying observes the route rather than replanning it.
Waypoints can still be edited via the details list for spontaneous
in-flight changes.
Live drone position comes from a new telemetryProvider/
flightControllerLinkProvider pair (autoDispose), the first place
FlightControllerLink/TelemetryFrame get wired into the UI - currently
backed by MockFlightControllerLink until a real MSP transport exists
(Doku 4.19). A drone marker renders on the map once a telemetry frame
arrives.
The Fly-mode header keeps the fit-to-area button but replaces "zoom to
home" with "zoom to drone" (FlyMapControls) - the first waypoint isn't a
meaningful reference point anymore once airborne.
Extracted computeMissionStats and MissionFooterBar out of PlanScreen so
Plan/Fly share the exact same stats/footer logic instead of duplicating
it, and extracted NavIconButton out of MapSearchControls so both Plan's
and Fly's map controls use the same button widget.
Widget tests that now mount FlyScreen switch from the ProviderScope-based
_wrap() helper to a manual ProviderContainer with an explicit
FlightControllerLink.disconnect() call before test end - the mock's
Timer.periodic doesn't get cancelled by container disposal alone, and
flutter_test's pending-timer check runs before addTearDown callbacks.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ready-to-Fly-Gate (Doku 2.2/4.5): AppModeCubit.toFly() erzwang bisher
Upload + verifiziert + disarmed und warf sonst einen StateError - da
MissionSyncService/Upload-Flow noch nicht angebunden sind, war das Gate
dauerhaft geschlossen und der Fly-Modus damit von der UI aus gar nicht
erreichbar. toFly() wechselt jetzt unbedingt in den Fly-Modus; die
readyToFly-Logik bleibt als Getter erhalten fuer den spaeteren Upload-
Flow. Der eigentliche Schutz vor einem Missions-Upload im armed-Zustand
lebt unveraendert auf Transport-Ebene (MockFlightControllerLink.
uploadMission() wirft dort weiterhin bei armed).
Fly-Modus-Kopfzeile (Doku 3.11/7.2, HTML-Demonstrator: #flightModePill/
#flightModeDropdown): top_mode_bar.dart zeigte im Fly-Modus bisher nur
eine leere Spacer-Flaeche. Zeigt jetzt die Wind-Pille (weiterhin
relevant, Doku 7.2) und eine neue Flugmodus-Auswahl:
- ui/providers/flight_mode_provider.dart: haelt den lokal gewaehlten
FlightMode (Doku 3.1: missionRun/guidedPoint/returnHome/hold), noch
ohne echte FlightControllerLink-Anbindung (Doku 4.19). reset() setzt
auf missionRun (Waypoint) zurueck.
- ui/widgets/flight_mode_pill.dart: PopupMenuButton-Pille "Mode: X" mit
den vier Optionen Waypoint/Point and Fly/Return to Home/Manual (1:1
aus dem HTML-Demonstrator uebernommene Labels).
- top_mode_bar.dart: Fly-Button setzt beim Eintritt in den Fly-Modus
den Flugmodus zurueck auf Waypoint (Doku 3.11: "Standard-
Rueckstellung ... beim Eintritt in Fly-Modus").
Layout-Bug beim Implementieren gefunden und behoben: Expanded(child:
Center(child: FlightModePill())) liess die Kopfzeile ueber den
gesamten Bildschirm expandieren (derselbe "Center/Align ohne Faktor
expandiert auf verfuegbare Constraints"-Fehler wie zuvor schon bei der
Bottom-Stats-Bar in dieser Session) - behoben durch Entfernen des
ueberfluessigen Center-Wrappers, da FlightModePill sein Zentrieren
bereits selbst per Container-alignment uebernimmt. Zusaetzlich einen
RenderFlex-Overflow bei langen Modusnamen ("Return to Home") behoben,
indem der Label-Text in Flexible mit TextOverflow.ellipsis gewrappt
wurde.
Getestet: 2 neue Widget-Tests (Fly-Button wechselt jetzt direkt in den
Fly-Modus statt eine Gate-Snackbar zu zeigen; Flugmodus-Dropdown
wechselt den Modus und setzt ihn beim erneuten Eintritt zurueck),
bestehender Gate-Test ersetzt, ein bestehender Test vereinfacht (die
Gate-Erfuellung vor toFly() ist nicht mehr noetig). Alle 118 Tests
sowie flutter analyze bestehen. Manuell auf dem Pixel_10a-Emulator
verifiziert: Fly-Button wechselt direkt um, Dropdown zeigt alle vier
Optionen, Auswahl uebernimmt das Label korrekt (auch bei langen Namen
ohne Overflow), Rueckstellung auf Waypoint bei erneutem Eintritt
funktioniert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
WindArrowIcon zeigte bisher windabwaerts (Fliessrichtung der Luft),
gedreht um (dirFrom + 180) Grad - 1:1 uebernommen aus windArrowSvg() im
HTML-Demonstrator. Die Standardkonvention bei Wetterfahnen und
Windbarben auf Wetterkarten ist jedoch, dass der Pfeil windaufwaerts
zeigt, also dorthin, woher der Wind kommt. Dreht jetzt direkt um
dirFromDeg statt (dirFromDeg + 180) - betrifft zentral alle Verwendungen
(Kopfleisten-Windpille, Wegpunktlisten-Zeile, Windmarker auf der Karte),
da diese alle nur dirFromDeg durchreichen.
Weicht damit bewusst vom HTML-Demonstrator ab (dort weiterhin
Fliessrichtung) - im Code-Kommentar dokumentiert.
flutter analyze und alle 117 Tests bestehen unveraendert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ersetzt die einfachen Zahlen-TextFormFields im Drohnenprofil-Editor durch
tastaturlose Swipe-Chips (Doku 3.5, HTML-Demonstrator: SwipeValueInput) -
vertikales Ziehen aendert den Wert in grossen Schritten (stepBig),
horizontales Ziehen in kleinen Schritten (stepSmall). Die Zugrichtung wird
beim ersten Ueberschreiten einer 8px-Schwelle einmalig festgelegt und
bleibt fuer den Rest der Geste gesperrt, damit eine leicht schraege Geste
nicht zwischen den beiden Schrittweiten hin- und herspringt. Min/Max/
Schrittweiten je Feld 1:1 aus DRONE_EDITOR_GROUPS im HTML-Demonstrator
uebernommen. Zusaetzlich ein "modified"-Punkt neben dem Label, sobald ein
Wert vom Ausgangswert beim Oeffnen des Editors abweicht (HTML: .modified-
dot/.chip-modified).
lib/ui/widgets/swipe_value_input.dart: neuer, wiederverwendbarer Chip.
Gesten-Konflikt mit der scrollbaren Formular-Liste (wichtigster Teil
dieser Aenderung): Die Chips sitzen in einer vertikal scrollbaren
ListView (elf Felder in Gruppen passen nicht auf einen Bildschirm). Ein
GestureDetector.onPan* auf dem Chip verlor dabei auf einem echten Geraet
durchgehend gegen die eigene Scroll-Geste der ListView - onPanStart/
onPanUpdate feuerten nie, weder fuer vertikale noch horizontale Zuege
(mit Debug-Prints auf dem Pixel_10a-Emulator verifiziert). Behoben durch:
- SwipeValueInput nutzt jetzt rohe Pointer-Events (Listener statt
GestureDetector.onPan*) - diese werden unabhaengig vom Gesture-Arena-
Ausgang immer zugestellt.
- Ein neuer onDragActiveChanged-Callback informiert das Elternwidget,
solange eine Geste aktiv ist; DroneProfileEditor sperrt darueber die
ListView (NeverScrollableScrollPhysics) fuer die Dauer des Ziehens, so
dass die Liste waehrenddessen nicht mitscrollt.
Getestet: 7 Widget-Tests fuer SwipeValueInput (grosse/kleine Schritte,
Richtungswechsel, Klemmen auf min/max, Rundung auf Nachkommastellen,
"modified"-Punkt, sowie ein Regressionstest fuer exakt dieses Szenario -
Chip innerhalb einer scrollbaren ListView). Alle 117 Tests sowie flutter
analyze bestehen. Manuell auf dem Pixel_10a-Emulator verifiziert:
vertikaler Zug auf "Min (stall)" aendert den Wert in 10er-Schritten ohne
die Liste zu scrollen, horizontaler Zug auf "Cruise" in 1er-Schritten -
beide mit korrekt aufleuchtendem "modified"-Indikator.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Kartendrehung (Doku 4.6): flutter_map's Zwei-Finger-Twist-Geste bewusst
deaktiviert (InteractionOptions mit InteractiveFlag.all & ~rotate in
mission_map.dart) - die Drohne fliegt nordorientiert, eine gedrehte Karte
wuerde die Peilung von Wegpunkten/Reticle nur verwirren, zumal der HTML-
Demonstrator gar keine Drehung kennt.
Warnungsliste (Doku 3.10), analog dem #warningsBtn/#warningsPanel-Flow im
HTML-Demonstrator:
- domain/mission/mission_warnings.dart: computeMissionWarnings() sammelt
alle aktuell zutreffenden Probleme (Route-Geometrie ausserhalb der
Drohnenfaehigkeiten, zu starker Wind, zu geringer Bodenabstand,
Reichweite/Ausdauer ueberschritten) zu einer Liste aus {text, action}-
Eintraegen - reine Funktion aus bereits vorhandenen Zwischenergebnissen,
unabhaengig von Riverpod/Widgets testbar.
- domain/wind/wind_math.dart: computeLegWindBad() ergaenzt (Bodenge-
schwindigkeit <= 1 m/s oder Windgeschwindigkeit > Drohnen-Maximum) -
bislang gab es nur die Windkomponente/Bodengeschwindigkeit selbst, aber
keine "zu stark"-Markierung.
- ui/widgets/warnings_panel.dart: Vollbild-Liste, jede Zeile verweist per
Aktionslabel auf den Loesungs-Screen ("Open altitude view"/"Open speed
view"/"Show on map").
- ui/widgets/bottom_stats_bar.dart: roter Warnungs-Button neben den
Mission-/Drohnen-Chips, nur sichtbar solange Warnungen aktiv sind.
- ui/widgets/waypoint_list_panel.dart: `_PanelTab` zu `PanelTab` public
gemacht plus neuer `initialTab`-Parameter, damit das Panel direkt auf
dem Altitude- oder Speed-Tab geoeffnet werden kann statt immer auf der
Liste zu starten.
- ui/providers/map_controller_provider.dart: fitMapToWaypoints()-Hilfs-
funktion extrahiert und in MapSearchControls' Fit-Button (vorher eigene
Kopie der Logik) sowie fuer die "Show on map"-Warnungsaktion
wiederverwendet.
- ui/screens/plan/plan_screen.dart: berechnet die Warnungsliste, ersetzt
den bisherigen Einzel-Banner (nur "Route exceeds...") durch alle
aktuellen Warnungstexte (durch " · " getrennt, wie im HTML-Demonstrator)
und verdrahtet Warnungs-Button/-Liste mit der passenden Navigation.
Getestet: 4 neue Unit-Tests fuer computeLegWindBad, 7 fuer
computeMissionWarnings (inkl. Grenzfaelle: veraltetes Gelaendeprofil,
kein Limit bei maxRangeM/maxEnduranceMin <= 0), 2 neue Widget-Tests
(Warnungs-Button erscheint bei Reichweiten-Ueberschreitung und oeffnet
die Liste; Antippen einer Wind-Warnung oeffnet den Speed-Tab direkt).
Alle 110 Tests sowie flutter analyze bestehen. Manuell auf dem
Pixel_10a-Emulator verifiziert: Steigraten-Ueberschreitung (Altitude auf
220m bei kurzer Distanz) macht Route/Banner/Warnungs-Button rot,
Warnungsliste zeigt den Eintrag mit "Show on map", Antippen schliesst
die Liste und zoomt die Karte korrekt auf die Bounding-Box beider
Wegpunkte.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sharing/Import (Doku 3.5/3.6), analog shareMission()/shareDrone() und den
importMissionInput/importDroneInput-Handlern im HTML-Demonstrator:
- services/sharing/mission_share_codec.dart, drone_share_codec.dart: reine
Funktionen zum Bauen/Parsen des Export-JSON (appVersion/exportedAt-Umschlag),
inkl. Erkennung einer abweichenden Hauptversion beim Import - vollstaendig
unit-getestet ohne Plugin-Abhaengigkeit.
- services/database/waypoint_json_codec.dart: Waypoint-JSON-(De-)Serialisierung
aus mission_repository.dart herausgezogen, damit Persistenz und Sharing
exakt dasselbe Dateiformat verwenden statt es zu duplizieren.
- services/sharing/sharing_service.dart: duenne I/O-Schicht - schreibt eine
temporaere Datei und oeffnet das native Share-Sheet (share_plus), bzw.
liest eine vom Nutzer per Systemdialog ausgewaehlte JSON-Datei
(file_picker). Als Klasse mit Instanzmethoden gehalten, damit sie sich in
Tests durch einen Fake ersetzen laesst.
- ui/widgets/missions_drones_panel.dart: Share-Icon je Zeile, Import-Button
im Toolbar beider Tabs.
Paket-Versionen bewusst gewaehlt: file_picker 11.0.2/share_plus 11.x wurden
zunaechst wegen einer win32-Konflikt-Aufloesung genutzt, kompilierten auf
diesem Projekt (AGP 9.0.1) aber nicht - file_picker < 12.0.0-beta.1 prueft
nur AGP-Version >= 9 und ueberspringt dann das Anwenden des Kotlin-Android-
Plugins, in der Annahme, AGPs eingebauter Kotlin-Support wuerde das
uebernehmen, was hier zu einem fehlenden compileReleaseKotlin-Task und
"Symbol nicht gefunden" fuehrte. file_picker >=12.0.0-beta.1 respektiert
zusaetzlich die bereits vom Flutter-Template gesetzte Gradle-Property
android.builtInKotlin=false und wendet das Plugin dann korrekt an -
file_picker auf ">=12.0.0-beta.1 <13.0.0" (share_plus zurueck auf ^13.3.0,
beide dann konsistent auf win32 ^6.x) gesetzt, um dies zu nutzen.
Zusaetzlich beim manuellen Durchtesten auf dem Pixel_10a-Emulator einen
zweiten, davon unabhaengigen Bug gefunden und behoben: CurrentMissionMeta-
Notifier.loadMission()/restoreLastSession() aktualisierten zwar Wegpunkte
und Missionsname, bewegten aber nie die Kartenkamera - eine geladene
Mission wurde dadurch mit falscher Kartenausschnitt angezeigt (Name/
Wegpunkte einer Stadt, Karte noch an der zuletzt betrachteten Stelle).
Fix: _fitMapToWaypoints() zentriert nach dem Laden auf die Bounding-Box
der Mission, analog der bereits vorhandenen _onFitPressed()-Logik in
MapSearchControls.
Getestet: 13 neue Unit-Tests fuer die Sharing-Codecs, 4 neue Widget-Tests
mit einem Fake-SharingService (kein echter Platform-Channel-Zugriff in
Tests). Alle 97 Tests sowie flutter analyze bestehen. Manuell auf dem
Pixel_10a-Emulator verifiziert: natives Share-Sheet oeffnet sich mit der
korrekten JSON-Datei, Datei-Import ueber den Systemdialog legt eine neue
Mission bzw. ein neues Drohnenprofil an, Kartensprung beim Laden einer
Mission funktioniert jetzt korrekt fuer sowohl manuelles Laden als auch
den Autosave-Restore beim App-Start.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rundet das Altitude-Tab um ein reales Gelaendeprofil ab, damit sichtbar
wird, ob die geplante Flughoehe ausreichend Bodenabstand haelt:
- domain/mission/terrain_math.dart: reine Sampling-/Geometrie-Funktionen
(routeKeyForTerrain fuer Caching, distanzbasiertes Sampling alle 30m,
lineare Sollhoehen-Interpolation, terrainDangerRanges fuer Abschnitte
mit < 10m Bodenabstand) - unabhaengig testbar ohne Netzwerk/UI.
- services/terrain/terrain_service.dart: laedt AWS-Terrarium-PNG-Kacheln
(elevation-tiles-prod, zoom 13) und dekodiert sie ueber dart:ui
(instantiateImageCodec/toByteData), ohne zusaetzliches Bildpaket.
Elevation = R*256 + G + B/256 - 32768 pro Pixel; Kacheln werden pro
Route nur einmal geladen (nach Kachel gruppierte Sample-Punkte).
- ui/providers/terrain_provider.dart: cached das geladene Profil ueber
routeKeyForTerrain - reine Werteaenderungen (Hoehe/Speed) loesen keinen
erneuten Kachel-Download aus, nur eine tatsaechliche Ortsverschiebung.
- ui/widgets/full_value_chart.dart: _paintDangerBands/_paintTerrain
zeichnen rote Gefahrenbaender bzw. die braune Gelaende-Flaeche, in der
gleichen Reihenfolge wie im HTML-Demonstrator (dangerHtml, grid,
terrainHtml, dann Soll-Kurve obenauf).
- ui/widgets/waypoint_list_panel.dart: triggert ensureFor() beim
Wechsel auf den Altitude-Tab sowie bei Routenaenderungen waehrend
dieser aktiv ist (ref.listenManual auf currentMissionProvider).
Getestet: 13 neue Unit-Tests fuer die reine Geometrie/Sampling-Logik,
3 Service-Tests mit einer zur Testzeit synthetisch erzeugten PNG
(dart:ui-Encoding, keine Testasset-Datei noetig). Alle 80 Tests sowie
flutter analyze bestehen. Manuell auf dem Pixel_10a-Emulator verifiziert:
Altitude-Tab zeigt das reale Amsterdam-Gelaendeprofil (nahe Meereshoehe)
korrekt als Flaeche unter der Sollhoehen-Linie.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Zwei vom Nutzer per Screenshot gemeldete Probleme:
1. Der Wind-Marker auf der Karte liess bei kurzen Werten (z.B. "1 m/s")
weiterhin sichtbaren Leerraum rechts neben dem Text - die letzte
Anpassung hatte die Marker-Box auf den unguenstigsten Fall
(zweistellig, "25 m/s") fixiert bemessen, wodurch einstellige Werte
in derselben Box zu viel Platz hatten. flutter_map's Marker verlangt
eine feste Breite (kein intrinsisches Sizing), also wird sie jetzt
pro Wegpunkt aus der Ziffernzahl der Geschwindigkeit berechnet
(+11px/Ziffer, per Widget-Messung ermittelt) statt eine einzelne
Konstante fuer alle Faelle zu verwenden.
2. Die Kopfleiste sass nicht mittig, sondern wirkte nach rechts
verschoben. Ursache: SafeArea wendet links/rechts die jeweils
tatsaechlichen (auf diesem Emulator unterschiedlich grossen)
Insets an - dadurch war der fuer FractionallySizedBox verfuegbare
Bereich selbst schon asymmetrisch zur Bildschirmmitte verschoben.
Ersetzt durch denselben symmetrischen Randabstand (max(links,
rechts)), der in PlanScreen fuer die Drehraeder bereits etabliert ist.
Verifiziert: flutter analyze (0 issues), flutter test (64/64), manuell
auf Pixel_10a-Emulator - Kopfleiste jetzt sichtbar mittig, mehrere
Wind-Marker mit "1 m/s" zeigen keinen Leerraum mehr.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ortssuche verschob bisher nur die Karte - der HTML-Demonstrator beginnt
bei einer Suche immer eine neue, nach dem gefundenen Ort benannte
Mission (setMissionFromPlace(), aufgerufen aus doSearch()). Vorherige
Aenderungen gehen dabei nicht verloren, da flushPendingAutosave()
(hier: CurrentMissionMetaNotifier._flushNow()) zuerst noch ausstehende
Autosaves schreibt.
Die Nominatim-Suche wurde dafuer aus MapSearchControls in einen
eigenstaendigen NominatimService extrahiert (analog WindService):
injectable http.Client, damit sich die Suche in Tests ohne echten
Netzwerkzugriff ueberschreiben laesst. CurrentMissionMetaNotifier bekam
dafuer startNewFromPlace() (startNew() intern darauf umgebaut, um
Duplikation zu vermeiden).
Kopfleiste von 86%/64% (Plan-/Fly-Modus) auf einheitlich 70%
Bildschirmbreite verkleinert - zentriert war sie durch Align(topCenter)
+ FractionallySizedBox bereits strukturell korrekt, wirkte bei der
vollen Breite aber unausgewogen.
Verifiziert: flutter analyze (0 issues), flutter test (64/64, davon 4
neue NominatimService-Tests und 1 neuer Widget-Test fuer den
Missions-Reset bei Ortssuche), manuell auf Pixel_10a-Emulator - Suche
nach "Rotterdam" setzt Fusszeile auf "Mission: Rotterdam" mit 0
Wegpunkten trotz zuvor bestehender Mission mit Wegpunkten.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Vervollstaendigt Doku 3.5/3.6/7.1/7.3/7.5: ein Vollbild-Verwaltungsmenue
fuer Missionen und Drohnenprofile (per Tabs umschaltbar, wie im HTML-
Demonstrator #missionsPanel gemeinsam mit der Profilliste), Autosave der
aktuell bearbeiteten Mission, Drift-Persistenz statt der bisherigen
In-Memory-only currentMissionProvider, sowie Anzeige/Zugang ueber neue
Chips in der Fusszeile (HTML-Demonstrator: #missionNameBtn/#droneNameBtn).
Datenbank (services/database/): Missions-Tabelle um updatedAt ergaenzt,
neue DroneProfiles- und AppSettingsTable-Tabellen (schemaVersion 2 mit
onUpgrade-Migration). Drei Repositories kapseln Drift-Zugriff +
JSON-(De-)Serialisierung der Wegpunktliste: MissionRepository,
DroneProfileRepository, AppSettingsRepository - je mit Unit-Tests gegen
eine In-Memory-Datenbank (NativeDatabase.memory()).
Provider: CurrentMissionMetaNotifier haelt Name/id der aktuellen Mission
und autosaved sie 800ms-debounced (identisch zum HTML-Demonstrator:
scheduleAutosave()/flushPendingAutosave()) - inklusive Wiederherstellung
des zuletzt bearbeiteten Autosave-Drafts beim App-Start (Doku 7.5).
ActiveDroneProfileNotifier haelt das aktive Profil, seedet beim ersten
Start automatisch T1 Ranger und merkt sich die Auswahl ueber
AppSettingsTable neustart-fest.
UI: MissionsDronesPanel (Missions-Tab: Liste mit Umbenennen/Loeschen/
Laden; Drones-Tab: Liste mit Bearbeiten/Loeschen/Auswaehlen, faellt beim
Loeschen des aktiven Profils auf das naechste zurueck). DroneProfileEditor
als Formular (bewusste Vereinfachung gegenueber den Swipe-Gesten-Chips
des HTML-Demonstrators - bei elf Feldern ist ein normales Formular auf
einem Mobilgeraet zugaenglicher). Rename per AlertDialog+TextField statt
Browser-prompt().
Aktives Drohnenprofil ist jetzt tatsaechlich wirksam statt nur eine feste
Anzeige: Speed-/Alt-Drehrad-Grenzen, Fangradius neuer Wegpunkte und die
Flugpfad-Machbarkeitspruefung (Kurvenradius, Steig-/Sinkrate) in
PlanScreen und WaypointListPanel lesen jetzt activeDroneProfileProvider
statt der bisherigen statischen DefaultDroneProfile-Konstanten (die als
T1-Ranger-Seed-Werte weiterleben).
Beim Verifizieren zwei echte Bugs in BottomStatsBar gefunden und
gefixt: ein Stack+Align-ohne-Factor blaehte sich auf unbegrenzte Groesse
auf und verschob die Details-Pille aus ihrer sichtbaren Position (durch
zwei gleich grosse Expanded-Bereiche ersetzt); zwei ConstrainedBox-Chips
nebeneinander verursachten auf schmaleren Breiten einen RenderFlex-
Overflow (durch Flexible ersetzt).
Widget-Tests: AppShell/PlanScreen initialisieren beim Start jetzt die
echte Datenbank - alle Tests, die AppShell pumpen, ueberschreiben
appDatabaseProvider testweise mit einer In-Memory-Instanz. Ausserdem
mussten mehrere Tests den neuen 800ms-Autosave-Timer abwarten (wie
zuvor schon beim WaypointChip-Flash-Timer etabliert), damit nach
Testende kein Timer mehr aussteht.
Verifiziert: flutter analyze (0 issues), flutter test (59/59, davon 14
neue Repository-Tests), manuell auf Pixel_10a-Emulator - Wegpunkte
werden automatisch als "New mission" gespeichert und erscheinen in der
Liste, Umbenennen/Laden/Loeschen funktionieren, neues Drohnenprofil mit
abweichender Max-Speed wird angelegt+aktiviert+in der Fusszeile
angezeigt, Loeschen des aktiven Profils faellt auf das verbleibende
zurueck.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Der Wind-Marker auf der Karte sass in einer flutter_map Marker-Box mit
fester width/height (100x26), die dem Container tighte Constraints
vorgibt - dadurch fuellte die Pille (Hintergrund/Rand) diese Box exakt
aus, auch wenn Icon+Text (bei font11) natuerlich nur ca. 90-101x21
brauchen. Der ueberschuessige Platz erschien als sichtbar zu grosse/zu
lange Pille.
Feste Groesse jetzt an der tatsaechlich benoetigten Groesse ausgerichtet
(per Widget-Messung ermittelt: 90x21 bei einstelliger, 101x21 bei
zweistelliger m/s-Anzeige) statt geschaetzt grosszuegig bemessen -
Schriftgroesse/Icon-Groesse selbst unveraendert (11/15, wie urspruenglich
implementiert).
Verifiziert: flutter analyze (0 issues), flutter test (45/45), manuell
auf Pixel_10a-Emulator - Pille umschliesst Pfeil+Text jetzt ohne
sichtbaren Leerraum.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Vervollstaendigt den in der letzten Session zurueckgestellten Teil des
Wind-Features (Doku 3.8): die Kopfleisten-Pille toggelt jetzt tatsaechlich
sichtbare Wind-Marker an jedem Wegpunkt, statt nur ihren eigenen Zustand
zu halten (HTML-Demonstrator: showPerWaypointWind steuerte im Original
direkt die divIcon-Marker in redrawMapLayers()).
WindMarkerPill (neuer Widget) zeigt Richtungspfeil + Geschwindigkeit in
einer kleinen dunklen Pille. MissionMap bekommt dafuer einen eigenen
windMarkers-Parameter (flutter_map MarkerLayer, gesondert von den
bestehenden CircleMarker-Wegpunkten, da MarkerLayer echte Widgets statt
nur CustomPaint-Kreise rendert). PlanScreen baut die Liste aus
ref.watch(headerWindProvider).showPerWaypoint plus den bereits per
"Fetch wind" geladenen Wegpunkt-Winddaten.
Zwei Dinge beim Verifizieren gefunden und gefixt:
- Marker-Groesse (width/height) war zu knapp bemessen und liess die
Pille per RenderFlex-Overflow abschneiden - vergroessert und die
Verankerung entsprechend angepasst.
- Der neue Widget-Test placierte den Test-Wegpunkt zunaechst ausserhalb
des sichtbaren Kartenausschnitts: anders als CircleLayer rendert
flutter_map's MarkerLayer nur Marker innerhalb der aktuellen Viewport-
Bounds. Wegpunkt jetzt am Kartenzentrum (MissionMap._initialCenter)
platziert.
Verifiziert: flutter analyze (0 issues), flutter test (45/45, ein neuer
Test fuer Marker-Sichtbarkeit vor/nach Antippen der Pille), manuell auf
Pixel_10a-Emulator - drei Wegpunkte mit geladenem Wind zeigen nach
Antippen der Pille je eine Wind-Anzeige, verschwinden beim erneuten
Antippen wieder.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Portiert das Wind-System des HTML-Demonstrators (Doku 3.8) vollstaendig:
Open-Meteo-Winddaten, interpoliert zwischen Druckflaechen- und festen
Nabenhoehen-Stuetzpunkten auf die tatsaechliche Zielhoehe.
Domain (lib/domain/wind/wind_math.dart, reines Dart): Hoehen-
Interpolation (linear + zirkulaer fuer Windrichtung), Peilung zwischen
zwei Punkten, Kopf-/Rueckenwind-Komponente pro Flugleg - 1:1 uebernommen
aus interpolateWindAtHeight()/bearingBetween()/headTailwindComponent()
des Demonstrators, per Unit-Tests abgesichert.
Service (lib/services/wind/wind_service.dart): zwei getrennte,
fokussierte Open-Meteo-Requests statt eines kombinierten (Doku 4.10 -
ein 35-Variablen-Request verursachte im Demonstrator Modellauswahl-
Fehler). Drei Verbraucher: Kopfleisten-Pille (120 m AGL ueber
Referenzort), gebuendelter Wegpunkt-Fetch (ein Request fuer alle
Koordinaten via Open-Meteos Mehrfach-Location-Syntax) und das
Validierungspanel (rohe Messwerte je Nabenhoehe/Druckflaeche). Per
MockClient getestet (kein echter Netzwerkzugriff in Tests).
UI:
- HeaderWindPill (TopModeBar, nur ausserhalb Fly-Modus): zeigt
Windrichtung/-geschwindigkeit, Antippen schaltet perspektivisch die
Wind-Marker der Wegpunkte um (Zustand vorbereitet, HTML-Demonstrator:
showPerWaypointWind).
- WaypointListPanel: "Fetch wind"/"Validate"-Buttons im Panel-Header,
neue WIND-Spalte in der Liste.
- WindValidatePanel: neue Vollbild-Route (gleiches Muster wie
WaypointListPanel) mit Rohdaten je Hoehenstufe inkl. Boeen (Doku 4.9:
Boeen nur hier, nicht in der Wegpunktanzeige) und Interpolationsergebnis.
- FullValueChart (Speed-Tab): Kopf-/Rueckenwind-Diamanten pro Leg samt
gestrichelter Nulllinie sowie gestrichelte Bodengeschwindigkeits-
Stufenlinie.
Bug gefunden und gefixt waehrend der Emulator-Verifikation: die
Y-Achsen-Autoskalierung klemmte weiterhin auf den gueltigen Drehrad-
Wertebereich (13-25 m/s), wodurch nahe Null liegende Windkomponenten
(bei schwachem Wind der Normalfall) ausserhalb des sichtbaren Bereichs
lagen und weder Diamanten noch Nulllinie zu sehen waren. Der HTML-
Demonstrator klemmt computeSpeedChartRange() bewusst nicht auf SPD_MIN/
SPD_MAX; das Klemmen in FullValueChart._range() jetzt entsprechend
entfernt.
Waypoint-Modell um windSpeedMs/windDirFromDeg/windElevationM erweitert
(nullable, reine Planungshilfe - der Encoder liest diese Felder nie,
Doku 3.2). currentMissionProvider bekommt replaceAll() fuer den
gebuendelten Wind-Fetch, der alle Wegpunkte gleichzeitig aktualisiert.
Verifiziert: flutter analyze (0 issues), flutter test (44/44, davon 14
neue Wind-Mathematik- und 6 neue WindService-Tests), manuell auf
Pixel_10a-Emulator mit echtem Open-Meteo-Netzwerkzugriff - Kopfleisten-
Pille zeigt Live-Wind, Fetch/Validate fuellen Liste bzw. oeffnen das
Validierungspanel mit echten Messwerten, Speed-Plot zeigt nach dem Fix
Diamanten/Nulllinie/Bodengeschwindigkeit korrekt skaliert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wegpunktliste: der aktive Wegpunkt (per Halo-Menue "Edit" ausgewaehlt)
war bei laengeren Missionen ausserhalb des sichtbaren Bereichs, da die
Liste beim Oeffnen immer oben startete. WaypointListPanel bekommt jetzt
einen ScrollController mit fester Zeilenhoehe (itemExtent, damit die
Zielposition ohne Layout-Messung berechnet werden kann) und scrollt per
post-frame-Callback zur aktiven Zeile - sowohl beim ersten Oeffnen als
auch beim Zurueckwechseln vom Altitude-/Speed-Tab auf List.
WaypointChip (der Swipe-Wertechip in den Listenzeilen) hatte eine fest
einprogrammierte Schrittgroesse von 10 fuer alle Felder (Alt/Speed/
Catch) - fuer Speed bei einer Spanne von nur 13-25 m/s viel zu grob,
ein einzelner Drag-Schritt sprang schon fast durch den gesamten
gueltigen Bereich. Schrittgroesse ist jetzt ein expliziter Parameter;
Speed nutzt DefaultDroneProfile.speedStep (1), Altitude weiterhin
DefaultDroneProfile.altStep (10), Catch-Radius weiterhin 10 (keine
Beschwerde hierzu, Wert unveraendert). Der Alt-/Speed-Drehrad und die
Vollbild-Charts nutzten diese Profile bereits korrekt - der Row-Chip
war die einzige Stelle mit hartcodiertem Wert.
Verifiziert: flutter analyze (0 issues), flutter test (24/24, zwei neue
Tests fuer Auto-Scroll und Speed-Schrittgroesse), manuell auf
Pixel_10a-Emulator mit 7 Wegpunkten - Liste oeffnet direkt bei
Wegpunkt 7, Speed-Chip-Drag aendert 15 m/s in kleinen Schritten (18)
statt in Zehnerspruengen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Suche, Home- und Fit-Button sassen bisher in einer eigenen Leiste unter
der Plan/Fly-Kopfleiste - ein reiner UI-Kompromiss, weil die Karte
(inkl. MapController) in PlanScreen lebte und von der aeusseren
Kopfleiste aus nicht erreichbar war. Der HTML-Demonstrator zeigt
Suche/Home/Fit dagegen als Mittelteil derselben Leiste wie die
Modus-Buttons (#searchWrap/#homeBtn/#fitBtn in #topBarMiddle) - das war
also keine Stilfrage, sondern eine Datenfluss-Frage.
Loesung: MapController aus einem PlanScreen-privaten Feld in einen
app-lebenslangen mapControllerProvider (Riverpod) gehoben. Damit kann
die neue MapSearchControls (ersetzt TopNavBar) direkt in TopModeBar
eingebettet werden und lebt architektonisch dort, wo sie hingehoert:
bei der Kartensteuerung, nicht als Kind von PlanScreen.
Als Nebeneffekt - tatsaechlich der wichtigere Punkt - bleibt die
Kamera-Position/Zoom jetzt auch beim Wechsel Plan -> Fly -> Plan
erhalten. Verifiziert per flutter_map-Quellcode (FlutterMap haengt
einen extern uebergebenen Controller beim Neu-Mounten nicht an und
disposed ihn nicht) sowie per neuem Widget-Test, der den Cubit direkt
durch das Ready-to-Fly-Gate schickt (der Fly-Modus ist ueber die UI
aktuell nicht erreichbar, da Upload/Verify noch nicht implementiert
ist) und die Kamera vor/nach dem Wechsel vergleicht.
Zusaetzlich: _onMapEvent unterscheidet jetzt per MapEventMove.source,
ob eine Kartenbewegung programmatisch (mapController, z.B. durch Suche/
Home/Fit) oder per Geste ausgeloest wurde, und triggert den
Snap-to-Edit-Handler entsprechend nur bei Gesten bzw. sofort bei
programmatischen Moves - das ersetzt den bisherigen Ansatz, an jeder
Aufrufstelle manuell _handleMoveEnd() zu rufen, der nicht mehr skaliert
sobald diese Aufrufstellen (wie jetzt) in einem anderen Widget liegen.
Verifiziert: flutter analyze (0 issues), flutter test (22/22, inkl.
neuem Kamera-Persistenz-Test), manuell auf Pixel_10a-Emulator (Release-
Build) - Suche/Home/Fit erscheinen fusioniert in der gruenen Pille,
Kartenverschiebung bleibt nach Wechsel in den (durch das Gate weiterhin
blockierten) Fly-Modus sichtbar unveraendert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- TopNavBar: Nominatim geocoding search field plus Home (center on first
waypoint) and Fit (zoom to mission bounds) buttons (HTML prototype:
#searchWrap/#homeBtn/#fitBtn). Both show a SnackBar instead of doing
nothing when there are no waypoints yet.
- Renders as its own bar just below TopModeBar rather than fused into the
same pill: TopModeBar lives in AppShell, a layer above PlanScreen, which
owns the MapController these actions actually need. Fusing them would
need lifting the MapController to shared state - reasonable follow-up
if pixel-fidelity with the prototype's single bar matters later, but
not necessary for the functionality itself.
- Replaced SimpleAttributionWidget with a compact custom attribution: OSM's
tile usage policy requires visible attribution to stay, so instead of
removing it (as first asked) I shrank it and dropped the "flutter_map |"
prefix per the user's follow-up choice, so it reads cleanly against the
now-opaque bottom bar instead of looking like a second banner.
Added 5 tests (nav bar renders, home/fit SnackBars with no waypoints,
empty search is a no-op). All 26 tests and flutter analyze pass. Verified
on the Pixel_10a emulator: searched "Berlin" and confirmed the map flew
there, dropped a waypoint, panned far away, and confirmed Home re-centers
and snaps onto it exactly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Wheels/readouts now add max(MediaQuery.padding.left/right) on both sides
symmetrically (HTML prototype: max(env(safe-area-inset-left),
env(safe-area-inset-right))), so a front-camera cutout in landscape
doesn't sit behind the altitude wheel, and both sides stay in sync.
- WaypointListPanel is now pushed via Navigator (PageRouteBuilder + fade)
instead of being an internal Stack overlay in PlanScreen. It was
rendering *under* AppShell's TopModeBar before, since TopModeBar is
always the last (topmost) child of AppShell's own Stack regardless of
what PlanScreen draws internally - a plain bool flag inside PlanScreen
had no way to paint above a sibling higher up the tree. A pushed route
paints above the entire invoking route's content by construction, so
this was the actual fix rather than reshuffling Z-order inside PlanScreen.
- Extracted DefaultDroneProfile (T1 Ranger constants) out of PlanScreen's
private statics into lib/domain/mission/, since WaypointListPanel now
needs the same values to compute its own route geometry reactively
(previously passed down as a vertexBad/altMin/altMax/... snapshot, which
would have gone stale while the panel was open across a route boundary).
- Bottom bar now has the prototype's rgba(15,15,15,0.72) background
(previously fully transparent outside the stats pill), and gained
MiniAltitudeProfile - the non-interactive height-profile sparkline
that sits above the stats row.
- Added the Altitude/Speed full-screen chart tabs (FullValueChart):
drag any waypoint's point to adjust its altitude/speed, X position
proportional to cumulative route distance, matching the prototype's
buildFullChart()/attachChartDrag(). Terrain overlay and wind diamonds
from the prototype aren't ported - terrain (doc 3.9) and wind (doc 3.8)
systems don't exist yet in the Flutter app.
- RouteGeometry now also exposes legClimbBad (was computed internally but
not surfaced), needed by both the mini profile and the chart tabs.
Discovered while updating the widget tests: a single tester.pump() after
tapping something that triggers Navigator.push/pop isn't enough - the
push/pop itself needs a frame to register before a duration-based pump
can animate it, so both are needed in sequence. Added 2 new tests for the
chart tabs; all 17 tests and flutter analyze pass. Verified all four
changes on the Pixel_10a emulator: wheels, opaque bottom bar with the
mini profile, the list panel now fully covering the top bar (confirmed by
its complete absence while the panel is open), and dragging points on
both chart tabs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 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>
- RouteGeometry (lib/domain/mission/route_geometry.dart): physically
grounded flight path - straight segments + tangential arcs at each
course change, sized from the drone's minimum turn radius (doc 3.7/4.6),
replacing a naive spline that would suggest unrealistic turn radii.
Marks a vertex "bad" (turn angle >= 160°, tangent length exceeding 90%
of either adjacent leg, or exceeding the waypoint's catch radius) and a
leg "bad" when the required climb/descent rate exceeds the drone's
max climb/descent rate. Pure Dart, no Flutter dependency, so it stays
usable if the mission domain is ever split into its own package (4.22).
Wind-based turn-radius correction from the prototype isn't ported yet -
the wind system (doc 3.8) doesn't exist in the Flutter app.
- MissionMap now draws each RouteSegment as its own Polyline, colored red
when bad instead of a single plain white line; waypoint markers turn red
too when their vertex is bad.
- ValueWheel: vertical drag-to-adjust tape control (HTML prototype's
#altWheel/#spdWheel), custom-painted tick marks, blue center indicator,
gradient fade at top/bottom. Wired into PlanScreen on both screen edges
with Alt/Speed readouts.
- curAlt/curSpeed now live in PlanScreen state instead of fixed constants:
dropping a new waypoint uses whatever the wheels are currently set to;
entering editing mode on an existing waypoint loads its values into the
wheels; adjusting a wheel while editing writes live into that waypoint
(provider gained setAltitude/setSpeed to match the existing
moveWaypoint/toggleAction pattern).
Added dedicated unit tests for the geometry (empty list, straight line,
feasible 90° turn producing a line-arc-line segment sequence, an
infeasible near-180° turn, and an infeasible descent rate) rather than
relying on eyeballing it on the emulator - this is exactly the kind of
ported-math correctness that's hard to verify visually but easy to get
subtly wrong. All 11 tests (previous 6 + these 5) and flutter analyze
pass. Also manually verified the wheels on the Pixel_10a emulator: drag
changes the value and the on-screen readout in real time.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Full reticle interaction state machine (idle/snapped/editing), matching
the HTML prototype's mode variable:
- idle: reticle shows "+", tap drops a new waypoint at the map center.
- snapped: after any pan ends, if the map center lands within the
reticle's on-screen radius (converted to meters at the current
zoom/latitude) of an existing waypoint, the view auto-recenters onto it
and the reticle switches to a pencil icon. Tap to enter editing.
- editing: reticle shows a checkmark; panning the map now live-updates the
target waypoint's position (drag-to-reposition) instead of moving the
camera "away" from it, and the HaloMenu radial menu appears.
HaloMenu: three ring-wedge hit regions (custom ClipPath/CustomClipper,
angles/radii taken from the prototype's buildRingWedgePath) for
Loiter/Landing/Remove, with Material icons standing in for the
demonstrator's hand-drawn SVGs. Loiter/Landing toggle the waypoint's
action and drop back to snapped; Remove deletes it and returns to idle.
MissionMap now draws a straight white polyline connecting all waypoints
in order (the physically-grounded straight-line + fillet-arc geometry
from doc 3.7/4.6 is still open - this is a straight-line placeholder) plus
a dashed rubber-band preview from the last waypoint to the live map center
while idle.
Note on porting from Leaflet: flutter_map's MapController.move() emits
MapEventMove, not MapEventMoveEnd, so (unlike the prototype's panTo()) the
auto-recenter-on-snap doesn't re-trigger snap detection - no "autoPanning"
guard flag was needed.
Verified with flutter analyze, flutter test (three new tests: halo menu
open/close, remove, loiter toggle - the wedge tap tests had to target the
icon's exact center via tapAt(), since a wedge's bounding-box center falls
outside its actual donut-segment hit area), and a full manual pass on the
Pixel_10a emulator: idle -> add -> snap (auto-recenter confirmed) -> edit
-> drag-reposition (confirmed via re-snap after further panning showing
the moved position, not the original one) -> halo remove -> back to idle,
plus the connecting line rendering correctly between two waypoints set
far apart.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- ReticleButton: fixed-at-screen-center "+" circle (HTML prototype:
#centerCluster/#reticleBtn), tapping it drops a waypoint at the current
map center.
- CurrentMissionNotifier (Riverpod): holds the waypoint list for the
mission being edited. Reactive single-value state per doc 4.16, so
Riverpod rather than a new Bloc/Cubit.
- MissionMap now renders each waypoint as a CircleLayer marker (white
border, dark fill, matching the prototype's L.circleMarker styling).
- PlanScreen owns the MapController needed to read the camera center on
tap, and wires the reticle to the provider.
Default altitude/speed/catch-radius (60m / 15 m/s / 60m) are hardcoded to
the T1 Ranger default profile values from the prototype for now - TODO
once drone profile settings (doc 3.5) exist.
Verified with flutter analyze, flutter test (incl. a new test covering the
add-waypoint path), and a real run on the Pixel_10a emulator: panned the
map and dropped two waypoints, both markers stayed correctly georeferenced.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- TopModeBar widget: persistent pill-shaped header (HTML prototype:
#topBarWrap/#editModeBtn/#flyModeBtn) tinted in the active mode's color,
with the inactive mode offered as a solid button in the corner to switch
into it.
- AppShell now provides a single shared Scaffold + Stack, overlaying
TopModeBar on top of whichever screen (Plan/Fly) is active, instead of
each screen owning its own Scaffold.
- Wired the Fly button to AppModeCubit.toFly(), which enforces the
Ready-to-Fly gate (doc 4.5). Since upload/verify isn't wired up yet, the
gate is never satisfied yet, so tapping Fly now shows a SnackBar instead
of throwing an uncaught StateError.
- Extended the widget test to cover both the bar's presence and the
gate-rejection path (was the source of a real bug: an initial
negative-margin Container hack for the "bleed into the corner" look
violated Container's margin.isNonNegative assertion and crashed the
whole tree - fixed with padding instead).
Verified with flutter analyze, flutter test, and a real run on the
Pixel_10a emulator (visually matches the prototype screenshots in design/,
Fly-tap correctly shows the gate message without crashing or switching mode).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Lock orientation to landscape (sensorLandscape in the manifest,
SystemChrome.setPreferredOrientations in main.dart) and enable immersive
fullscreen, matching the prototype's full-bleed map UI (doc 7.7).
- Add MissionMap widget: flutter_map + standard OSM raster tiles, same
default center/zoom as the HTML prototype (Amsterdam, zoom 16), OSM
attribution bottom-left. PlanScreen now renders it full-screen instead of
the placeholder text.
- Add the release-manifest INTERNET permission (previously only present in
the debug manifest, which would have silently broken tile loading and any
future network calls in release builds).
- Update the smoke test to check for the FlutterMap widget instead of the
removed placeholder text.
Verified with flutter analyze, flutter test, and a real run on the
Pixel_10a emulator (fullscreen landscape confirmed, tiles loading).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Disable Kotlin incremental compilation (kotlin.incremental=false): the
project lives on D: while Gradle's Kotlin daemon caches default to C:,
which crashes with "this and base files have different roots" when
closing incremental caches.
- Drop flutter_map_tile_caching for now: it pins objectbox_flutter_libs
^4.1.0, which fails checkReleaseAarMetadata against newer AndroidX
transitive deps (needs compileSdk 34+, itself built against android-31).
Not used in code yet, so deferred rather than forcing an unverified
dependency_override.
- Add web/ platform for fast local UI iteration without an emulator.
- Ignore .claude/ (Claude Code session state, not project content).
Verified: flutter analyze, flutter test, and a real release build running
on a newly created Pixel_10a-profile Android emulator (API 36).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>