2024-05-09 13:31:00 +02:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:geocoding/geocoding.dart';
|
|
|
|
import 'package:geolocator/geolocator.dart';
|
2024-06-13 00:47:53 +02:00
|
|
|
import 'package:osm_nominatim/osm_nominatim.dart';
|
2024-05-10 23:17:35 +02:00
|
|
|
import '../models/location.dart';
|
2024-06-13 00:47:53 +02:00
|
|
|
import '../utils/helper.dart';
|
2024-06-22 21:56:45 +02:00
|
|
|
import 'text_with_bold.dart';
|
2024-05-09 13:31:00 +02:00
|
|
|
|
|
|
|
class LocationSelector extends StatefulWidget {
|
2024-05-09 21:55:17 +02:00
|
|
|
final Function(MyLocation) onLocationChanged; // Callback function
|
|
|
|
|
|
|
|
const LocationSelector({super.key, required this.onLocationChanged});
|
2024-05-09 13:31:00 +02:00
|
|
|
|
|
|
|
@override
|
|
|
|
LocationSelectorState createState() => LocationSelectorState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class LocationSelectorState extends State<LocationSelector> {
|
|
|
|
final TextEditingController _locationController = TextEditingController();
|
|
|
|
String _street = '';
|
|
|
|
String _country = '';
|
|
|
|
String? _administrativeArea; // DE: Bundesland
|
|
|
|
String _city = '';
|
|
|
|
String? _subLocality; // DE: Stadtteil
|
|
|
|
String? _postalCode;
|
|
|
|
double? _latitude;
|
|
|
|
double? _longitude;
|
|
|
|
String? errorText;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2024-06-12 00:39:29 +02:00
|
|
|
return SingleChildScrollView(
|
|
|
|
child: Column(
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
2024-06-15 01:25:09 +02:00
|
|
|
ElevatedButton.icon(
|
|
|
|
icon: const Icon(Icons.my_location),
|
|
|
|
onPressed: _getCurrentLocation,
|
|
|
|
label: const Text('Current Position'),
|
|
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
2024-06-12 00:39:29 +02:00
|
|
|
TextField(
|
|
|
|
controller: _locationController,
|
|
|
|
decoration: InputDecoration(
|
2024-06-15 01:25:09 +02:00
|
|
|
labelText: 'Search location',
|
2024-06-12 00:39:29 +02:00
|
|
|
suffixIcon: IconButton(
|
|
|
|
icon: const Icon(Icons.search),
|
|
|
|
onPressed: _searchLocation,
|
|
|
|
),
|
2024-05-09 13:31:00 +02:00
|
|
|
),
|
2024-05-09 21:55:17 +02:00
|
|
|
),
|
2024-06-15 01:25:09 +02:00
|
|
|
const SizedBox(height: 6),
|
2024-06-12 00:39:29 +02:00
|
|
|
Text(
|
|
|
|
errorText != null ? '$errorText' : '',
|
|
|
|
style: const TextStyle(color: Colors.red),
|
|
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
2024-06-22 21:56:45 +02:00
|
|
|
TextWithBold(leadingText: 'Country:', boldText: ' $_country'),
|
|
|
|
TextWithBold(leadingText: 'City:', boldText: ' $_city'),
|
2024-06-13 00:47:53 +02:00
|
|
|
Text('Postal Code: ${_postalCode ?? ''}'),
|
2024-06-12 00:39:29 +02:00
|
|
|
Text('Street: $_street'),
|
2024-06-13 00:47:53 +02:00
|
|
|
Text('State/Area: ${_administrativeArea ?? ''}'),
|
|
|
|
Text('Latitude: ${_latitude ?? '--'}'),
|
|
|
|
Text('Longitude: ${_longitude ?? '--'}'),
|
2024-06-12 00:39:29 +02:00
|
|
|
],
|
|
|
|
),
|
2024-05-09 21:55:17 +02:00
|
|
|
);
|
2024-05-09 13:31:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void _searchLocation() async {
|
2024-05-10 00:21:23 +02:00
|
|
|
String locationQuery = _locationController.text.trim();
|
2024-05-09 13:31:00 +02:00
|
|
|
|
2024-05-10 00:21:23 +02:00
|
|
|
if (locationQuery.isEmpty) {
|
|
|
|
_resetLocationData('Specify an address for the search');
|
2024-05-09 13:31:00 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2024-06-13 00:47:53 +02:00
|
|
|
if (isMobile) {
|
|
|
|
List<Location> locations = await locationFromAddress(locationQuery);
|
|
|
|
if (locations.isNotEmpty) {
|
|
|
|
// Take first match
|
|
|
|
Location firstLocation = locations.first;
|
2024-05-09 13:31:00 +02:00
|
|
|
|
2024-06-13 00:47:53 +02:00
|
|
|
// Extract country and city information
|
|
|
|
List<Placemark> placeMarks = await placemarkFromCoordinates(
|
|
|
|
firstLocation.latitude, firstLocation.longitude);
|
|
|
|
if (placeMarks.isNotEmpty) {
|
|
|
|
Placemark placeMark = placeMarks.first;
|
|
|
|
setState(() {
|
|
|
|
_latitude = firstLocation.latitude;
|
|
|
|
_longitude = firstLocation.longitude;
|
|
|
|
_street = placeMark.street ?? '';
|
|
|
|
_country = placeMark.country ?? '';
|
|
|
|
_city = placeMark.locality ?? '';
|
|
|
|
_subLocality = placeMark.subLocality;
|
|
|
|
_postalCode = placeMark.postalCode;
|
|
|
|
_administrativeArea = placeMark.administrativeArea;
|
|
|
|
errorText = null;
|
|
|
|
});
|
|
|
|
// location is found, trigger callback
|
|
|
|
triggerCallback();
|
|
|
|
} else {
|
|
|
|
setState(() {
|
|
|
|
// should placeMarks be empty return latitude and longitude anyway
|
|
|
|
_latitude = firstLocation.latitude;
|
|
|
|
_longitude = firstLocation.longitude;
|
|
|
|
errorText = null;
|
|
|
|
});
|
|
|
|
}
|
2024-05-09 13:31:00 +02:00
|
|
|
} else {
|
2024-06-13 00:47:53 +02:00
|
|
|
_resetLocationData('Location $locationQuery not found');
|
2024-05-09 13:31:00 +02:00
|
|
|
}
|
|
|
|
} else {
|
2024-06-13 00:47:53 +02:00
|
|
|
// other platforms than Android or iOS
|
|
|
|
|
|
|
|
try {
|
|
|
|
List<Place> searchResult = await Nominatim.searchByName(
|
|
|
|
query: locationQuery,
|
|
|
|
limit: 1,
|
|
|
|
addressDetails: true,
|
|
|
|
nameDetails: true,
|
|
|
|
);
|
|
|
|
|
|
|
|
Map<String, dynamic>? addressData = searchResult.single.address;
|
|
|
|
if (addressData != null) {
|
|
|
|
String street = _getStreetInfo(addressData);
|
|
|
|
String city = _getCityInfo(addressData);
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
_latitude = searchResult.single.lat;
|
|
|
|
_longitude = searchResult.single.lon;
|
|
|
|
_street = street;
|
|
|
|
_country = addressData.containsKey('country')
|
|
|
|
? addressData['country']
|
|
|
|
: '';
|
|
|
|
_administrativeArea = addressData.containsKey('state')
|
|
|
|
? addressData['state']
|
|
|
|
: null;
|
|
|
|
_city = city;
|
|
|
|
_subLocality = addressData.containsKey('suburb')
|
|
|
|
? addressData['suburb']
|
|
|
|
: null;
|
|
|
|
_postalCode = addressData.containsKey('postcode')
|
|
|
|
? addressData['postcode']
|
|
|
|
: null;
|
|
|
|
errorText = null;
|
|
|
|
});
|
|
|
|
triggerCallback();
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
_resetLocationData('Error looking up location $locationQuery: $e');
|
|
|
|
}
|
2024-05-09 13:31:00 +02:00
|
|
|
}
|
|
|
|
} catch (e) {
|
2024-05-10 00:21:23 +02:00
|
|
|
_resetLocationData('Error searching location $locationQuery: $e');
|
2024-05-09 13:31:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-13 00:47:53 +02:00
|
|
|
void triggerCallback() {
|
2024-05-10 23:17:35 +02:00
|
|
|
widget.onLocationChanged(MyLocation(
|
|
|
|
street: _street,
|
|
|
|
country: _country,
|
|
|
|
administrativeArea: _administrativeArea,
|
|
|
|
locality: _city,
|
|
|
|
subLocality: _subLocality,
|
|
|
|
postalCode: _postalCode,
|
|
|
|
latitude: _latitude,
|
|
|
|
longitude: _longitude,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2024-05-10 00:21:23 +02:00
|
|
|
/// Resets the location data and calls setState()
|
|
|
|
void _resetLocationData(String errorMessage) {
|
|
|
|
setState(() {
|
|
|
|
errorText = errorMessage;
|
|
|
|
_street = '';
|
|
|
|
_country = '';
|
|
|
|
_city = '';
|
|
|
|
_subLocality = null;
|
|
|
|
_postalCode = null;
|
|
|
|
_administrativeArea = null;
|
|
|
|
_latitude = null;
|
|
|
|
_longitude = null;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-06-13 00:47:53 +02:00
|
|
|
/// Determine users current position
|
2024-05-09 13:31:00 +02:00
|
|
|
void _getCurrentLocation() async {
|
|
|
|
bool serviceEnabled;
|
|
|
|
LocationPermission permission;
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Test if location services are enabled.
|
|
|
|
serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
|
|
|
if (!serviceEnabled) {
|
2024-06-15 01:25:09 +02:00
|
|
|
// Location services are not enabled, don't continue
|
2024-05-09 13:31:00 +02:00
|
|
|
// accessing the position and request users of the
|
|
|
|
// App to enable the location services.
|
2024-06-15 01:25:09 +02:00
|
|
|
setState(() {
|
|
|
|
errorText = 'Location services are disabled. '
|
|
|
|
'Enable location services and try again.';
|
|
|
|
});
|
|
|
|
|
|
|
|
return;
|
2024-05-09 13:31:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
permission = await Geolocator.checkPermission();
|
|
|
|
if (permission == LocationPermission.denied) {
|
|
|
|
permission = await Geolocator.requestPermission();
|
|
|
|
if (permission == LocationPermission.denied) {
|
|
|
|
// Permissions are denied, next time you could try
|
|
|
|
// requesting permissions again (this is also where
|
|
|
|
// Android's shouldShowRequestPermissionRationale
|
|
|
|
// returned true. According to Android guidelines
|
|
|
|
// your App should show an explanatory UI now.
|
2024-06-15 01:25:09 +02:00
|
|
|
setState(() {
|
|
|
|
errorText = 'Location permissions are denied';
|
|
|
|
});
|
|
|
|
return;
|
2024-05-09 13:31:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (permission == LocationPermission.deniedForever) {
|
|
|
|
// Permissions are denied forever, handle appropriately.
|
2024-06-15 01:25:09 +02:00
|
|
|
setState(() {
|
|
|
|
errorText =
|
|
|
|
'Location permissions are permanently denied, cannot request permissions.';
|
|
|
|
});
|
|
|
|
return;
|
2024-05-09 13:31:00 +02:00
|
|
|
}
|
|
|
|
} catch (e) {
|
2024-06-15 01:25:09 +02:00
|
|
|
errorText = 'Error getting location permission: $e';
|
2024-05-09 13:31:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
// When we reach here, permissions are granted and we can
|
|
|
|
// continue accessing the position of the device.
|
|
|
|
Position position = await Geolocator.getCurrentPosition(
|
|
|
|
desiredAccuracy: LocationAccuracy.high);
|
|
|
|
|
2024-06-13 00:47:53 +02:00
|
|
|
if (isMobile) {
|
|
|
|
// using package geocoding which works for Android and iOS only
|
|
|
|
List<Placemark> placeMarks = await placemarkFromCoordinates(
|
|
|
|
position.latitude, position.longitude);
|
|
|
|
|
|
|
|
if (placeMarks.isNotEmpty) {
|
|
|
|
Placemark placeMark = placeMarks.first;
|
|
|
|
setState(() {
|
|
|
|
_street = placeMark.street ?? '';
|
|
|
|
_country = placeMark.country ?? '';
|
|
|
|
_city = placeMark.locality ?? '';
|
2024-06-15 01:25:09 +02:00
|
|
|
_administrativeArea = placeMark.administrativeArea;
|
2024-06-13 00:47:53 +02:00
|
|
|
_latitude = position.latitude;
|
|
|
|
_longitude = position.longitude;
|
2024-06-15 01:25:09 +02:00
|
|
|
errorText = null;
|
2024-06-13 00:47:53 +02:00
|
|
|
});
|
|
|
|
triggerCallback();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// using osm_nominatim for other platforms than Android and iOS
|
|
|
|
try {
|
|
|
|
Place reverseSearchResult = await Nominatim.reverseSearch(
|
|
|
|
lat: position.latitude,
|
|
|
|
lon: position.longitude,
|
|
|
|
addressDetails: true,
|
|
|
|
nameDetails: true,
|
|
|
|
);
|
|
|
|
|
|
|
|
Map<String, dynamic>? addressData = reverseSearchResult.address;
|
|
|
|
|
|
|
|
if (addressData != null) {
|
|
|
|
String street = _getStreetInfo(addressData);
|
|
|
|
String city = _getCityInfo(addressData);
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
_latitude = position.latitude;
|
|
|
|
_longitude = position.longitude;
|
|
|
|
_street = street;
|
|
|
|
_country = addressData.containsKey('country')
|
|
|
|
? addressData['country']
|
|
|
|
: '';
|
|
|
|
_administrativeArea = addressData.containsKey('state')
|
|
|
|
? addressData['state']
|
|
|
|
: null;
|
|
|
|
_city = city;
|
|
|
|
_subLocality = addressData.containsKey('suburb')
|
|
|
|
? addressData['suburb']
|
|
|
|
: null;
|
|
|
|
_postalCode = addressData.containsKey('postcode')
|
|
|
|
? addressData['postcode']
|
|
|
|
: null;
|
|
|
|
errorText = null;
|
|
|
|
});
|
|
|
|
triggerCallback();
|
|
|
|
}
|
|
|
|
} catch (e) {
|
2024-06-15 01:25:09 +02:00
|
|
|
errorText = 'Error looking up current location: $e';
|
2024-06-13 00:47:53 +02:00
|
|
|
}
|
2024-05-09 13:31:00 +02:00
|
|
|
}
|
|
|
|
} catch (e) {
|
2024-06-15 01:25:09 +02:00
|
|
|
errorText = 'Error getting current location: $e';
|
2024-05-09 13:31:00 +02:00
|
|
|
}
|
|
|
|
}
|
2024-06-13 00:47:53 +02:00
|
|
|
|
|
|
|
// Categories according to OSM tagging are not always consistent.
|
|
|
|
// https://nominatim.org/release-docs/latest/api/Output/#addressdetails
|
|
|
|
|
|
|
|
/// Try to get nominatim street information.
|
|
|
|
String _getStreetInfo(Map<String, dynamic> addressData) {
|
|
|
|
String street = '';
|
|
|
|
if (addressData.containsKey('road')) {
|
|
|
|
street = addressData['road'];
|
|
|
|
} else if (addressData.containsKey('locality')) {
|
|
|
|
street = addressData['locality'];
|
|
|
|
} else if (addressData.containsKey('city_block')) {
|
|
|
|
street = addressData['city_block'];
|
|
|
|
}
|
|
|
|
return street;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Try to get nominatim city information.
|
|
|
|
String _getCityInfo(Map<String, dynamic> addressData) {
|
|
|
|
String city = '';
|
|
|
|
if (addressData.containsKey('city')) {
|
|
|
|
city = addressData['city'];
|
|
|
|
} else if (addressData.containsKey('village')) {
|
|
|
|
city = addressData['village'];
|
|
|
|
} else if (addressData.containsKey('town')) {
|
|
|
|
city = addressData['town'];
|
|
|
|
}
|
|
|
|
return city;
|
|
|
|
}
|
2024-05-09 13:31:00 +02:00
|
|
|
}
|