Location search starts a new mission; shrink and center header bar

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>
This commit is contained in:
Constantin Leue
2026-07-28 15:22:36 +02:00
co-authored by Claude Sonnet 5
parent aaec7f0f72
commit 926d805156
7 changed files with 234 additions and 21 deletions
@@ -0,0 +1,68 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
/// Ergebnis einer Ortssuche: Koordinaten plus ein kompakter Anzeigename,
/// der auch als Missionsname dient (siehe MapSearchControls).
class GeocodingResult {
const GeocodingResult({
required this.label,
required this.lat,
required this.lon,
});
final String label;
final double lat;
final double lon;
}
/// Geocoding ueber Nominatim (OpenStreetMap) - analog doSearch() im HTML-
/// Demonstrator. Injectable http.Client analog WindService, damit dies in
/// Tests ohne echten Netzwerkzugriff ueberschrieben werden kann.
class NominatimService {
NominatimService({http.Client? client}) : _client = client ?? http.Client();
final http.Client _client;
Future<GeocodingResult?> search(String query) async {
final uri = Uri.https('nominatim.openstreetmap.org', '/search', {
'format': 'json',
'addressdetails': '1',
'accept-language': 'en',
'limit': '1',
'q': query,
});
final response = await _client
.get(uri, headers: {'User-Agent': 'dmc_app (Drone Mission Control)'});
if (response.statusCode != 200) return null;
final results = jsonDecode(response.body) as List;
if (results.isEmpty) return null;
final first = results.first as Map<String, dynamic>;
return GeocodingResult(
label: _extractPlaceLabel(first),
lat: double.parse(first['lat'] as String),
lon: double.parse(first['lon'] as String),
);
}
/// Analog extractPlaceLabel() im HTML-Demonstrator: bevorzugt einen
/// kompakten Ortsnamen aus den Nominatim-Adressbestandteilen, faellt sonst
/// auf den ersten Abschnitt von display_name zurueck.
String _extractPlaceLabel(Map<String, dynamic> result) {
final address = result['address'] as Map<String, dynamic>?;
final label = address?['city'] ??
address?['town'] ??
address?['village'] ??
address?['municipality'] ??
address?['suburb'] ??
address?['county'];
if (label is String && label.isNotEmpty) return label;
final displayName = result['display_name'] as String?;
if (displayName != null && displayName.isNotEmpty) {
return displayName.split(',').first;
}
return 'Unknown location';
}
}