Files
anyway/frontend/lib/presentation/pages/trip_creation_flow.dart

288 lines
8.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart';
import 'package:anyway/core/constants.dart';
import 'package:anyway/presentation/pages/new_trip_preferences.dart';
class TripLocationSelectionPage extends StatefulWidget {
const TripLocationSelectionPage({
super.key,
this.autoNavigateToPreferences = true,
});
final bool autoNavigateToPreferences;
@override
State<TripLocationSelectionPage> createState() =>
_TripLocationSelectionPageState();
}
class _TripLocationSelectionPageState extends State<TripLocationSelectionPage> {
final CameraPosition _initialCameraPosition = const CameraPosition(
// TODO - maybe Paris is not the best default?
target: LatLng(48.8566, 2.3522),
zoom: 11.0,
);
GoogleMapController? _mapController;
LatLng? _selectedLocation;
bool _useLocation = true;
bool _loadingPreferences = true;
bool _isSearchingAddress = false;
final TextEditingController _searchController = TextEditingController();
static const Map<String, List<double>> _debugLocations = {
'paris': [48.8575, 2.3514],
'london': [51.5074, -0.1278],
'new york': [40.7128, -74.006],
'tokyo': [35.6895, 139.6917],
};
@override
void initState() {
super.initState();
_loadLocationPreference();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _loadLocationPreference() async {
final prefs = await SharedPreferences.getInstance();
final useLocation = prefs.getBool('useLocation') ?? true;
if (!mounted) return;
setState(() {
_useLocation = useLocation;
_loadingPreferences = false;
});
}
void _onLongPress(LatLng location) {
setState(() {
_selectedLocation = location;
});
_mapController?.animateCamera(CameraUpdate.newLatLng(location));
}
void _setSelectedLocationFromCoords(double lat, double lng) {
final latLng = LatLng(lat, lng);
setState(() {
_selectedLocation = latLng;
});
_mapController?.animateCamera(CameraUpdate.newLatLng(latLng));
}
Future<void> _useCurrentLocation() async {
try {
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.denied) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Location permission denied.')),
);
return;
}
if (permission == LocationPermission.deniedForever) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Location permission permanently denied.'),
),
);
return;
}
final position = await Geolocator.getCurrentPosition();
if (!mounted) return;
_setSelectedLocationFromCoords(position.latitude, position.longitude);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Unable to get current location: $e')),
);
}
}
Future<void> _searchForLocation(String rawQuery) async {
final query = rawQuery.trim();
if (query.isEmpty) {
return;
}
setState(() => _isSearchingAddress = true);
try {
List<Location> locations = [];
if (GeocodingPlatform.instance != null) {
locations = await locationFromAddress(query);
}
Location? selected;
if (locations.isNotEmpty) {
selected = locations.first;
} else {
final fallback = _debugLocations[query.toLowerCase()];
if (fallback != null) {
selected = Location(
latitude: fallback[0],
longitude: fallback[1],
timestamp: DateTime.now(),
);
}
}
if (selected == null) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('No results for "$query".')));
return;
}
if (!mounted) return;
_setSelectedLocationFromCoords(selected.latitude, selected.longitude);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Search failed: $e')));
} finally {
if (mounted) {
setState(() => _isSearchingAddress = false);
}
}
}
Set<Marker> get _markers => _selectedLocation == null
? <Marker>{}
: {
Marker(
markerId: const MarkerId('new-trip-start'),
position: _selectedLocation!,
infoWindow: const InfoWindow(title: 'Trip start'),
),
};
void _confirmLocation() {
if (_selectedLocation == null) return;
final startLocation = <double>[
_selectedLocation!.latitude,
_selectedLocation!.longitude,
];
if (!widget.autoNavigateToPreferences) {
Navigator.of(context).pop(startLocation);
return;
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => NewTripPreferencesPage(startLocation: startLocation),
),
);
}
@override
Widget build(BuildContext context) {
if (_loadingPreferences) {
return Scaffold(
appBar: AppBar(title: const Text('Choose Start Location')),
body: const Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
appBar: AppBar(title: const Text('Choose Start Location')),
body: Stack(
children: [
GoogleMap(
onMapCreated: (controller) => _mapController = controller,
initialCameraPosition: _initialCameraPosition,
onLongPress: _onLongPress,
markers: _markers,
cloudMapId: MAP_ID,
mapToolbarEnabled: false,
zoomControlsEnabled: false,
myLocationButtonEnabled: false,
myLocationEnabled: _useLocation,
),
Positioned(
top: 16,
left: 16,
right: 16,
child: Column(
children: [
SearchBar(
controller: _searchController,
hintText: 'Enter a city or long-press on the map',
leading: const Icon(Icons.search),
onSubmitted: _searchForLocation,
trailing: [
IconButton(
icon: _isSearchingAddress
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.send),
onPressed: _isSearchingAddress
? null
: () => _searchForLocation(_searchController.text),
),
],
),
const SizedBox(height: 8),
if (_useLocation)
Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
icon: const Icon(Icons.my_location),
label: const Text('Use current location'),
onPressed: _useCurrentLocation,
),
),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
_selectedLocation == null
? 'Long-press anywhere to drop the starting point.'
: 'Selected: ${_selectedLocation!.latitude.toStringAsFixed(4)}, ${_selectedLocation!.longitude.toStringAsFixed(4)}',
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
),
],
),
floatingActionButton: _selectedLocation == null
? null
: FloatingActionButton.extended(
onPressed: _confirmLocation,
icon: widget.autoNavigateToPreferences
? const Icon(Icons.tune)
: const Icon(Icons.check),
label: Text(
widget.autoNavigateToPreferences
? 'Select Preferences'
: 'Use this location',
),
),
);
}
}