Added osm_nominatim for better cross-platform support, as geocoding is limited to Android/iOS only.
parent
e057d428a5
commit
35c79de771
|
@ -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,46 +75,90 @@ class LocationSelectorState extends State<LocationSelector> {
|
|||
}
|
||||
|
||||
try {
|
||||
List<Location> locations = await locationFromAddress(locationQuery);
|
||||
if (locations.isNotEmpty) {
|
||||
// Take first match
|
||||
Location firstLocation = locations.first;
|
||||
if (isMobile) {
|
||||
List<Location> locations = await locationFromAddress(locationQuery);
|
||||
if (locations.isNotEmpty) {
|
||||
// Take first match
|
||||
Location firstLocation = locations.first;
|
||||
|
||||
// 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();
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
} 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<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);
|
||||
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<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 ?? '';
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -377,10 +371,11 @@ class _UserDataPageState extends State<UserDataPage> {
|
|||
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<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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue