Added osm_nominatim for better cross-platform support, as geocoding is limited to Android/iOS only.

master
Rafael 2024-06-13 00:47:53 +02:00
parent e057d428a5
commit 35c79de771
6 changed files with 203 additions and 106 deletions

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart'; import 'package:geocoding/geocoding.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:osm_nominatim/osm_nominatim.dart';
import '../models/location.dart'; import '../models/location.dart';
import '../utils/helper.dart';
class LocationSelector extends StatefulWidget { class LocationSelector extends StatefulWidget {
final Function(MyLocation) onLocationChanged; // Callback function final Function(MyLocation) onLocationChanged; // Callback function
@ -46,18 +48,19 @@ class LocationSelectorState extends State<LocationSelector> {
style: const TextStyle(color: Colors.red), style: const TextStyle(color: Colors.red),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
ElevatedButton( ElevatedButton.icon(
icon: const Icon(Icons.my_location),
onPressed: _getCurrentLocation, onPressed: _getCurrentLocation,
child: const Text('Use Current Position'), label: const Text('Current Position'),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Text('Country: $_country'), Text('Country: $_country'),
Text('City: $_city'), Text('City: $_city'),
Text('Postal Code: $_postalCode'), Text('Postal Code: ${_postalCode ?? ''}'),
Text('Street: $_street'), Text('Street: $_street'),
Text('Administrative Area: $_administrativeArea'), Text('State/Area: ${_administrativeArea ?? ''}'),
Text('Latitude: $_latitude'), Text('Latitude: ${_latitude ?? '--'}'),
Text('Longitude: $_longitude'), Text('Longitude: ${_longitude ?? '--'}'),
], ],
), ),
); );
@ -72,6 +75,7 @@ class LocationSelectorState extends State<LocationSelector> {
} }
try { try {
if (isMobile) {
List<Location> locations = await locationFromAddress(locationQuery); List<Location> locations = await locationFromAddress(locationQuery);
if (locations.isNotEmpty) { if (locations.isNotEmpty) {
// Take first match // Take first match
@ -85,9 +89,9 @@ class LocationSelectorState extends State<LocationSelector> {
setState(() { setState(() {
_latitude = firstLocation.latitude; _latitude = firstLocation.latitude;
_longitude = firstLocation.longitude; _longitude = firstLocation.longitude;
_street = '${placeMark.street}'; _street = placeMark.street ?? '';
_country = placeMark.country!; _country = placeMark.country ?? '';
_city = placeMark.locality!; _city = placeMark.locality ?? '';
_subLocality = placeMark.subLocality; _subLocality = placeMark.subLocality;
_postalCode = placeMark.postalCode; _postalCode = placeMark.postalCode;
_administrativeArea = placeMark.administrativeArea; _administrativeArea = placeMark.administrativeArea;
@ -106,6 +110,49 @@ class LocationSelectorState extends State<LocationSelector> {
} else { } else {
_resetLocationData('Location $locationQuery not found'); _resetLocationData('Location $locationQuery not found');
} }
} else {
// other platforms than Android or iOS
try {
List<Place> searchResult = await Nominatim.searchByName(
query: locationQuery,
limit: 1,
addressDetails: true,
nameDetails: true,
);
debugPrint(searchResult.single.address.toString()); // TODO remove
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');
}
}
} catch (e) { } catch (e) {
_resetLocationData('Error searching location $locationQuery: $e'); _resetLocationData('Error searching location $locationQuery: $e');
} }
@ -139,7 +186,7 @@ class LocationSelectorState extends State<LocationSelector> {
}); });
} }
/// Determine users position using geolocator package /// Determine users current position
void _getCurrentLocation() async { void _getCurrentLocation() async {
bool serviceEnabled; bool serviceEnabled;
LocationPermission permission; LocationPermission permission;
@ -182,21 +229,96 @@ class LocationSelectorState extends State<LocationSelector> {
Position position = await Geolocator.getCurrentPosition( Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high); desiredAccuracy: LocationAccuracy.high);
List<Placemark> placeMarks = debugPrint(position.toString()); // TODO remove
await placemarkFromCoordinates(position.latitude, position.longitude);
if (isMobile) {
// using package geocoding which works for Android and iOS only
List<Placemark> placeMarks = await placemarkFromCoordinates(
position.latitude, position.longitude);
if (placeMarks.isNotEmpty) { if (placeMarks.isNotEmpty) {
Placemark placeMark = placeMarks.first; Placemark placeMark = placeMarks.first;
setState(() { setState(() {
_street = placeMark.street!; _street = placeMark.street ?? '';
_country = placeMark.country!; _country = placeMark.country ?? '';
_city = placeMark.locality!; _city = placeMark.locality ?? '';
_latitude = position.latitude; _latitude = position.latitude;
_longitude = position.longitude; _longitude = position.longitude;
}); });
triggerCallback(); 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) {
debugPrint('Error looking up current location: $e');
}
}
} catch (e) { } catch (e) {
debugPrint('Error getting current location: $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<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;
}
} }

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../pages/conversations_page.dart'; import '../pages/conversations_page.dart';
import '../pages/liked_users_page.dart'; import '../pages/liked_users_page.dart';
import '../pages/user_data_page.dart';
import '../pages/settings_page.dart'; import '../pages/settings_page.dart';
import '../pages/user_matching_page.dart'; import '../pages/user_matching_page.dart';
import '../pages/user_profile_page.dart'; import '../pages/user_profile_page.dart';
@ -42,15 +41,9 @@ class MyDrawer extends StatelessWidget {
title: const Text('Home'), title: const Text('Home'),
leading: const Icon(Icons.home), leading: const Icon(Icons.home),
onTap: () { 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); 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 // horizontal line
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),

View File

@ -34,7 +34,6 @@ class _UserDataPageState extends State<UserDataPage> {
List<LanguageSetting> languagesList = []; List<LanguageSetting> languagesList = [];
final List<Language> _selectedLanguages = []; final List<Language> _selectedLanguages = [];
// get instance of firestore and auth
final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final AuthService _authService = AuthService(); final AuthService _authService = AuthService();
@ -57,7 +56,6 @@ class _UserDataPageState extends State<UserDataPage> {
Future<void> _fetchSettings() async { Future<void> _fetchSettings() async {
try { try {
// Fetch user ID
String currentUserId = _authService.getCurrentUser()!.uid; String currentUserId = _authService.getCurrentUser()!.uid;
// Fetch user document fields (email, uid, gender, ...) from database // Fetch user document fields (email, uid, gender, ...) from database
@ -355,11 +353,7 @@ class _UserDataPageState extends State<UserDataPage> {
} }
} }
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( _showSnackBar('Failed to save user data.');
const SnackBar(
content: Text('Failed to save user data.'),
),
);
} }
} }
} }
@ -380,7 +374,8 @@ class _UserDataPageState extends State<UserDataPage> {
onPressed: () { onPressed: () {
AuthService().signOut(); AuthService().signOut();
}, },
icon: const Icon(Icons.logout)) icon: const Icon(Icons.logout),
)
], ],
), ),
body: Padding( body: Padding(
@ -450,6 +445,7 @@ class _UserDataPageState extends State<UserDataPage> {
], ],
// Display selected secondary location or remove button // Display selected secondary location or remove button
if (_secondaryLocation != null) ...[ if (_secondaryLocation != null) ...[
const SizedBox(height: 12),
Center( Center(
child: ElevatedButton( child: ElevatedButton(
onPressed: _removeSecondaryLocation, onPressed: _removeSecondaryLocation,
@ -505,12 +501,10 @@ class _UserDataPageState extends State<UserDataPage> {
ButtonSegment<Gender>( ButtonSegment<Gender>(
value: Gender.male, value: Gender.male,
label: Text('male'), label: Text('male'),
//icon: Icon(Icons.male_sharp),
), ),
ButtonSegment<Gender>( ButtonSegment<Gender>(
value: Gender.female, value: Gender.female,
label: Text('female'), label: Text('female'),
//icon: Icon(Icons.female_sharp),
), ),
ButtonSegment<Gender>( ButtonSegment<Gender>(
value: Gender.divers, value: Gender.divers,

View File

@ -1,27 +1,33 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:io' show Platform;
import '../enumerations.dart'; 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. /// Compare two lists by their content ignoring their elements order.
///
bool equalContent(List<dynamic> list1, List<dynamic> list2) { bool equalContent(List<dynamic> list1, List<dynamic> list2) {
return const DeepCollectionEquality.unordered().equals(list1, list2); return const DeepCollectionEquality.unordered().equals(list1, list2);
} }
///
/// Creates a composite ID from the passed [ids]. /// Creates a composite ID from the passed [ids].
/// In the format id(1)_id(n) /// In the format id(1)_id(n)
///
String getCompoundId(List<String> ids) { String getCompoundId(List<String> ids) {
ids.sort(); // sort to ensure the result is the same for any order of ids ids.sort(); // sort to ensure the result is the same for any order of ids
return ids.join('_'); return ids.join('_');
} }
/// Returns a date format of '$weekday, $day. $month $year $hours:$minutes'. /// 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. /// If any errors occur, an empty string will be returned.
String formatTimestamp(Timestamp timestamp) { String formatTimestamp(Timestamp timestamp) {
try { try {
@ -63,10 +69,8 @@ String formatTimestamp(Timestamp timestamp) {
} }
} }
///
/// Get pronoun for given [userGender]. /// Get pronoun for given [userGender].
/// Returns [He] if male, [She] if female, else [He/She]. /// Returns [He] if male, [She] if female, else [He/She].
///
String getPronoun(Gender? userGender) { String getPronoun(Gender? userGender) {
switch (userGender) { switch (userGender) {
case Gender.male: case Gender.male:
@ -78,9 +82,7 @@ String getPronoun(Gender? userGender) {
} }
} }
///
/// Get the [displayName] of our own Enumerations. /// Get the [displayName] of our own Enumerations.
///
String getDisplayText(dynamic option) { String getDisplayText(dynamic option) {
// Check if the option is an enum and has a displayName property // Check if the option is an enum and has a displayName property
if (option is Enum) { if (option is Enum) {
@ -93,9 +95,7 @@ String getDisplayText(dynamic option) {
return option.toString().split('.').last; return option.toString().split('.').last;
} }
///
/// Show a simple message dialog /// Show a simple message dialog
///
void showMsg(BuildContext context, String title, String content) { void showMsg(BuildContext context, String title, String content) {
showDialog( showDialog(
context: context, 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) { void showErrorSnackBar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(

View File

@ -528,6 +528,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" 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: path:
dependency: transitive dependency: transitive
description: description:

View File

@ -48,6 +48,7 @@ dependencies:
firebase_storage: ^11.7.7 firebase_storage: ^11.7.7
image_cropper: ^6.0.0 image_cropper: ^6.0.0
percent_indicator: ^4.2.3 percent_indicator: ^4.2.3
osm_nominatim: ^3.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: