288 lines
8.7 KiB
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',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|