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: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<LocationSelector> {
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,6 +75,7 @@ class LocationSelectorState extends State<LocationSelector> {
}
try {
if (isMobile) {
List<Location> locations = await locationFromAddress(locationQuery);
if (locations.isNotEmpty) {
// Take first match
@ -85,9 +89,9 @@ class LocationSelectorState extends State<LocationSelector> {
setState(() {
_latitude = firstLocation.latitude;
_longitude = firstLocation.longitude;
_street = '${placeMark.street}';
_country = placeMark.country!;
_city = placeMark.locality!;
_street = placeMark.street ?? '';
_country = placeMark.country ?? '';
_city = placeMark.locality ?? '';
_subLocality = placeMark.subLocality;
_postalCode = placeMark.postalCode;
_administrativeArea = placeMark.administrativeArea;
@ -106,12 +110,55 @@ class LocationSelectorState extends State<LocationSelector> {
} else {
_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) {
_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<LocationSelector> {
});
}
/// 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<LocationSelector> {
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
List<Placemark> placeMarks =
await placemarkFromCoordinates(position.latitude, position.longitude);
debugPrint(position.toString()); // TODO remove
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!;
_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<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) {
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 '../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),

View File

@ -34,7 +34,6 @@ class _UserDataPageState extends State<UserDataPage> {
List<LanguageSetting> languagesList = [];
final List<Language> _selectedLanguages = [];
// get instance of firestore and auth
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final AuthService _authService = AuthService();
@ -57,7 +56,6 @@ class _UserDataPageState extends State<UserDataPage> {
Future<void> _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<UserDataPage> {
}
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to save user data.'),
),
);
_showSnackBar('Failed to save user data.');
}
}
}
@ -380,7 +374,8 @@ class _UserDataPageState extends State<UserDataPage> {
onPressed: () {
AuthService().signOut();
},
icon: const Icon(Icons.logout))
icon: const Icon(Icons.logout),
)
],
),
body: Padding(
@ -450,6 +445,7 @@ class _UserDataPageState extends State<UserDataPage> {
],
// 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<UserDataPage> {
ButtonSegment<Gender>(
value: Gender.male,
label: Text('male'),
//icon: Icon(Icons.male_sharp),
),
ButtonSegment<Gender>(
value: Gender.female,
label: Text('female'),
//icon: Icon(Icons.female_sharp),
),
ButtonSegment<Gender>(
value: Gender.divers,

View File

@ -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<dynamic> list1, List<dynamic> 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<String> 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(

View File

@ -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:

View File

@ -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: