diff --git a/lib/components/location_selector.dart b/lib/components/location_selector.dart index a3d97c5..93e3dc6 100644 --- a/lib/components/location_selector.dart +++ b/lib/components/location_selector.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:geocoding/geocoding.dart'; import 'package:geolocator/geolocator.dart'; +import 'package:osm_nominatim/osm_nominatim.dart'; import '../models/location.dart'; +import '../utils/helper.dart'; class LocationSelector extends StatefulWidget { final Function(MyLocation) onLocationChanged; // Callback function @@ -46,18 +48,19 @@ class LocationSelectorState extends State { style: const TextStyle(color: Colors.red), ), const SizedBox(height: 20), - ElevatedButton( + ElevatedButton.icon( + icon: const Icon(Icons.my_location), onPressed: _getCurrentLocation, - child: const Text('Use Current Position'), + label: const Text('Current Position'), ), const SizedBox(height: 20), Text('Country: $_country'), Text('City: $_city'), - Text('Postal Code: $_postalCode'), + Text('Postal Code: ${_postalCode ?? ''}'), Text('Street: $_street'), - Text('Administrative Area: $_administrativeArea'), - Text('Latitude: $_latitude'), - Text('Longitude: $_longitude'), + Text('State/Area: ${_administrativeArea ?? ''}'), + Text('Latitude: ${_latitude ?? '--'}'), + Text('Longitude: ${_longitude ?? '--'}'), ], ), ); @@ -72,46 +75,90 @@ class LocationSelectorState extends State { } try { - List locations = await locationFromAddress(locationQuery); - if (locations.isNotEmpty) { - // Take first match - Location firstLocation = locations.first; + if (isMobile) { + List locations = await locationFromAddress(locationQuery); + if (locations.isNotEmpty) { + // Take first match + Location firstLocation = locations.first; - // Extract country and city information - List 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(); + // Extract country and city information + List 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; + }); + } } else { - setState(() { - // should placeMarks be empty return latitude and longitude anyway - _latitude = firstLocation.latitude; - _longitude = firstLocation.longitude; - errorText = null; - }); + _resetLocationData('Location $locationQuery not found'); } } else { - _resetLocationData('Location $locationQuery not found'); + // other platforms than Android or iOS + + try { + List searchResult = await Nominatim.searchByName( + query: locationQuery, + limit: 1, + addressDetails: true, + nameDetails: true, + ); + + debugPrint(searchResult.single.address.toString()); // TODO remove + + Map? 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'); + } } } catch (e) { _resetLocationData('Error searching location $locationQuery: $e'); } } - void triggerCallback(){ + void triggerCallback() { widget.onLocationChanged(MyLocation( street: _street, country: _country, @@ -139,7 +186,7 @@ class LocationSelectorState extends State { }); } - /// Determine users position using geolocator package + /// Determine users current position void _getCurrentLocation() async { bool serviceEnabled; LocationPermission permission; @@ -182,21 +229,96 @@ class LocationSelectorState extends State { Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high); - List placeMarks = - await placemarkFromCoordinates(position.latitude, position.longitude); - if (placeMarks.isNotEmpty) { - Placemark placeMark = placeMarks.first; - setState(() { - _street = placeMark.street!; - _country = placeMark.country!; - _city = placeMark.locality!; - _latitude = position.latitude; - _longitude = position.longitude; - }); - triggerCallback(); + debugPrint(position.toString()); // TODO remove + + if (isMobile) { + // using package geocoding which works for Android and iOS only + List placeMarks = await placemarkFromCoordinates( + position.latitude, position.longitude); + + if (placeMarks.isNotEmpty) { + Placemark placeMark = placeMarks.first; + setState(() { + _street = placeMark.street ?? ''; + _country = placeMark.country ?? ''; + _city = placeMark.locality ?? ''; + _latitude = position.latitude; + _longitude = position.longitude; + }); + 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? 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) { + debugPrint('Error looking up current location: $e'); + } } } catch (e) { debugPrint('Error getting current location: $e'); } } + + // 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 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 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; + } } diff --git a/lib/components/my_drawer.dart b/lib/components/my_drawer.dart index cb49d6b..6587f21 100644 --- a/lib/components/my_drawer.dart +++ b/lib/components/my_drawer.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '../pages/conversations_page.dart'; import '../pages/liked_users_page.dart'; -import '../pages/user_data_page.dart'; import '../pages/settings_page.dart'; import '../pages/user_matching_page.dart'; import '../pages/user_profile_page.dart'; @@ -42,15 +41,9 @@ class MyDrawer extends StatelessWidget { title: const Text('Home'), leading: const Icon(Icons.home), onTap: () { - // pop the drawer + // home screen is the only place this drawer is used on, + // so just pop the drawer, else add page navigation. Navigator.pop(context); - // Optional: Navigate to HomePage -/* Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => HomePage(), - ), - );*/ }, ), ), @@ -152,28 +145,6 @@ class MyDrawer extends StatelessWidget { ), ), - // TODO TESTING - user data tile - Padding( - padding: const EdgeInsets.only(left: 25), - child: ListTile( - title: const Text("TESTING - User Data"), - leading: const Icon(Icons.supervised_user_circle), - onTap: () { - // pop the drawer first, then navigate to destination - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const UserDataPage( - isRegProcess: false, - isEditMode: false, - ), - ), - ); - }, - ), - ), - // horizontal line Padding( padding: const EdgeInsets.symmetric(horizontal: 16), diff --git a/lib/pages/user_data_page.dart b/lib/pages/user_data_page.dart index 67e6e33..fd62440 100644 --- a/lib/pages/user_data_page.dart +++ b/lib/pages/user_data_page.dart @@ -34,7 +34,6 @@ class _UserDataPageState extends State { List languagesList = []; final List _selectedLanguages = []; - // get instance of firestore and auth final FirebaseFirestore _firestore = FirebaseFirestore.instance; final AuthService _authService = AuthService(); @@ -57,7 +56,6 @@ class _UserDataPageState extends State { Future _fetchSettings() async { try { - // Fetch user ID String currentUserId = _authService.getCurrentUser()!.uid; // Fetch user document fields (email, uid, gender, ...) from database @@ -355,11 +353,7 @@ class _UserDataPageState extends State { } } } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Failed to save user data.'), - ), - ); + _showSnackBar('Failed to save user data.'); } } } @@ -377,10 +371,11 @@ class _UserDataPageState extends State { actions: [ if (widget.isRegProcess) IconButton( - onPressed: () { - AuthService().signOut(); - }, - icon: const Icon(Icons.logout)) + onPressed: () { + AuthService().signOut(); + }, + icon: const Icon(Icons.logout), + ) ], ), body: Padding( @@ -450,6 +445,7 @@ class _UserDataPageState extends State { ], // Display selected secondary location or remove button if (_secondaryLocation != null) ...[ + const SizedBox(height: 12), Center( child: ElevatedButton( onPressed: _removeSecondaryLocation, @@ -505,12 +501,10 @@ class _UserDataPageState extends State { ButtonSegment( value: Gender.male, label: Text('male'), - //icon: Icon(Icons.male_sharp), ), ButtonSegment( value: Gender.female, label: Text('female'), - //icon: Icon(Icons.female_sharp), ), ButtonSegment( value: Gender.divers, diff --git a/lib/utils/helper.dart b/lib/utils/helper.dart index ec7e5d5..2604439 100644 --- a/lib/utils/helper.dart +++ b/lib/utils/helper.dart @@ -1,27 +1,33 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; - +import 'package:flutter/foundation.dart' show kIsWeb; +import 'dart:io' show Platform; import '../enumerations.dart'; -/// +/// Returns [true] if app is running on Mobile (Android or iOS), else [false]. +bool get isMobile { + if (kIsWeb) { + return false; + } else { + return Platform.isIOS || Platform.isAndroid; + } +} + /// Compare two lists by their content ignoring their elements order. -/// bool equalContent(List list1, List list2) { return const DeepCollectionEquality.unordered().equals(list1, list2); } -/// /// Creates a composite ID from the passed [ids]. /// In the format id(1)_id(n) -/// String getCompoundId(List ids) { ids.sort(); // sort to ensure the result is the same for any order of ids return ids.join('_'); } /// Returns a date format of '$weekday, $day. $month $year $hours:$minutes'. -/// For example: Sat. 3 Jun. 2024 15:03. +/// For example: [Sat. 3 Jun. 2024 15:03]. /// If any errors occur, an empty string will be returned. String formatTimestamp(Timestamp timestamp) { try { @@ -63,10 +69,8 @@ String formatTimestamp(Timestamp timestamp) { } } -/// /// Get pronoun for given [userGender]. /// Returns [He] if male, [She] if female, else [He/She]. -/// String getPronoun(Gender? userGender) { switch (userGender) { case Gender.male: @@ -78,9 +82,7 @@ String getPronoun(Gender? userGender) { } } -/// /// Get the [displayName] of our own Enumerations. -/// String getDisplayText(dynamic option) { // Check if the option is an enum and has a displayName property if (option is Enum) { @@ -93,9 +95,7 @@ String getDisplayText(dynamic option) { return option.toString().split('.').last; } -/// /// Show a simple message dialog -/// void showMsg(BuildContext context, String title, String content) { showDialog( context: context, @@ -114,6 +114,7 @@ void showMsg(BuildContext context, String title, String content) { ); } +/// Show a red colored SnackBar with a [Dismiss] Button void showErrorSnackBar(BuildContext context, String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -127,4 +128,4 @@ void showErrorSnackBar(BuildContext context, String message) { ), ), ); -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index 83ab765..f6719aa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -528,6 +528,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + osm_nominatim: + dependency: "direct main" + description: + name: osm_nominatim + sha256: "037f1af3abee92cf34e33b562cec1acbb0210234b1bdf3d8975bdef3849e6287" + url: "https://pub.dev" + source: hosted + version: "3.0.0" path: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c9c3de8..10ac70a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ dependencies: firebase_storage: ^11.7.7 image_cropper: ^6.0.0 percent_indicator: ^4.2.3 + osm_nominatim: ^3.0.0 dev_dependencies: flutter_test: