Add location search, home/fit map buttons, and slim OSM attribution
- 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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3091476771
commit
dc8e6f9d6e
@@ -1,9 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../../../domain/mission/default_drone_profile.dart';
|
||||
@@ -15,6 +17,7 @@ import '../../widgets/halo_menu.dart';
|
||||
import '../../widgets/mini_altitude_profile.dart';
|
||||
import '../../widgets/mission_map.dart';
|
||||
import '../../widgets/reticle_button.dart';
|
||||
import '../../widgets/top_nav_bar.dart';
|
||||
import '../../widgets/value_wheel.dart';
|
||||
import '../../widgets/waypoint_list_panel.dart';
|
||||
|
||||
@@ -254,6 +257,68 @@ class _PlanScreenState extends ConsumerState<PlanScreen> {
|
||||
_handleMoveEnd();
|
||||
}
|
||||
|
||||
/// Ortssuche per Nominatim-Geocoding (HTML-Demonstrator: doSearch()).
|
||||
/// Zentriert die Karte auf den ersten Treffer.
|
||||
Future<void> _onSearch(String query) async {
|
||||
final q = query.trim();
|
||||
if (q.isEmpty) return;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
try {
|
||||
final uri = Uri.https('nominatim.openstreetmap.org', '/search', {
|
||||
'format': 'json',
|
||||
'addressdetails': '1',
|
||||
'accept-language': 'en',
|
||||
'limit': '1',
|
||||
'q': q,
|
||||
});
|
||||
final response = await http
|
||||
.get(uri, headers: {'User-Agent': 'dmc_app (Drone Mission Control)'});
|
||||
final results = jsonDecode(response.body) as List;
|
||||
if (results.isEmpty) {
|
||||
messenger.showSnackBar(const SnackBar(content: Text('Location not found')));
|
||||
return;
|
||||
}
|
||||
final first = results.first as Map<String, dynamic>;
|
||||
final lat = double.parse(first['lat'] as String);
|
||||
final lon = double.parse(first['lon'] as String);
|
||||
_mapController.move(LatLng(lat, lon), 15);
|
||||
_handleMoveEnd();
|
||||
} catch (_) {
|
||||
messenger.showSnackBar(const SnackBar(content: Text('Search failed')));
|
||||
}
|
||||
}
|
||||
|
||||
/// Zentriert auf den ersten Wegpunkt (HTML-Demonstrator: homeBtn).
|
||||
void _onHomePressed() {
|
||||
final waypoints = ref.read(currentMissionProvider);
|
||||
if (waypoints.isEmpty) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text('No waypoints yet')));
|
||||
return;
|
||||
}
|
||||
final home = waypoints.first;
|
||||
_mapController.move(LatLng(home.lat, home.lon), 16);
|
||||
_handleMoveEnd();
|
||||
}
|
||||
|
||||
/// Zoomt auf die gesamte Mission (HTML-Demonstrator: fitToWaypoints()).
|
||||
void _onFitPressed() {
|
||||
final waypoints = ref.read(currentMissionProvider);
|
||||
if (waypoints.isEmpty) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text('No waypoints to fit')));
|
||||
return;
|
||||
}
|
||||
final points = [for (final w in waypoints) LatLng(w.lat, w.lon)];
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(
|
||||
bounds: LatLngBounds.fromPoints(points),
|
||||
padding: const EdgeInsets.fromLTRB(50, 50, 50, 140),
|
||||
),
|
||||
);
|
||||
_handleMoveEnd();
|
||||
}
|
||||
|
||||
/// Oeffnet die Wegpunktliste als eigene Vollbild-Route statt als
|
||||
/// internen Stack-Overlay, damit sie wirklich alles ueberdeckt -
|
||||
/// inklusive der Kopfleiste im AppShell darueber.
|
||||
@@ -357,6 +422,21 @@ class _PlanScreenState extends ConsumerState<PlanScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
// Direkt unterhalb der Plan/Fly-Kopfleiste (AppShell/TopModeBar),
|
||||
// dieselbe Hoehe wie der Start der Draeder (top:56) - dort ist
|
||||
// die Kopfleiste garantiert bereits zu Ende.
|
||||
top: 62,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: TopNavBar(
|
||||
onSearch: _onSearch,
|
||||
onHome: _onHomePressed,
|
||||
onFit: _onFitPressed,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_mode == _ReticleMode.editing && targetWaypoint != null)
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
|
||||
Reference in New Issue
Block a user