main
dart 145 lines 4.97 KB
Raw
1 import 'dart:async';
2
3 import 'package:geocoding/geocoding.dart';
4 import 'package:geolocator/geolocator.dart';
5
6 /// Thin wrapper around [Geolocator] + [Geocoding] for the built-in
7 /// location tools.
8 ///
9 /// All methods return plain Dart types (strings / maps) so tool results can
10 /// be serialised without leaking platform model classes into the tool layer.
11 ///
12 /// Location is fetched on demand only (no background tracking, no caching,
13 /// no persistence). Reverse-geocoding is best-effort: when it fails the
14 /// raw coordinates are still returned with a `geocoding_error` note.
15 class LocationService {
16 LocationService._();
17 static final instance = LocationService._();
18
19 final Geocoding _geocoding = Geocoding();
20
21 // ---------------------------------------------------------------------------
22 // Permissions
23 // ---------------------------------------------------------------------------
24
25 /// Request when-in-use location access. Returns `true` when granted.
26 Future<bool> requestAccess() async {
27 final serviceEnabled = await Geolocator.isLocationServiceEnabled();
28 if (!serviceEnabled) return false;
29
30 var permission = await Geolocator.checkPermission();
31 if (permission == LocationPermission.denied) {
32 permission = await Geolocator.requestPermission();
33 }
34 return permission == LocationPermission.always ||
35 permission == LocationPermission.whileInUse;
36 }
37
38 /// Check current permission status without prompting.
39 Future<bool> hasAccess() async {
40 final permission = await Geolocator.checkPermission();
41 return permission == LocationPermission.always ||
42 permission == LocationPermission.whileInUse;
43 }
44
45 // ---------------------------------------------------------------------------
46 // Location
47 // ---------------------------------------------------------------------------
48
49 /// Returns a JSON-friendly map with coordinates and reverse-geocoded place.
50 ///
51 /// Throws a [StateError] with a human-readable message when location
52 /// services are disabled, permission is missing, or the fix times out.
53 Future<Map<String, dynamic>> getCurrentLocation() async {
54 final serviceEnabled = await Geolocator.isLocationServiceEnabled();
55 if (!serviceEnabled) {
56 throw StateError(
57 'Location services are disabled. '
58 'Please enable location services on your device.',
59 );
60 }
61
62 final permission = await Geolocator.checkPermission();
63 if (permission == LocationPermission.deniedForever) {
64 throw StateError(
65 'Location permission permanently denied. '
66 'Please grant location access in your device settings.',
67 );
68 }
69 if (permission != LocationPermission.always &&
70 permission != LocationPermission.whileInUse) {
71 throw StateError(
72 'Location permission not granted. '
73 'Please enable Location Access in Settings.',
74 );
75 }
76
77 late final Position position;
78 try {
79 position = await Geolocator.getCurrentPosition(
80 locationSettings: const LocationSettings(
81 accuracy: LocationAccuracy.high,
82 timeLimit: Duration(seconds: 15),
83 ),
84 );
85 } on TimeoutException {
86 throw StateError(
87 'Timed out while getting your location. Please try again.',
88 );
89 }
90
91 final result = <String, dynamic>{
92 'latitude': position.latitude,
93 'longitude': position.longitude,
94 'accuracy_m': position.accuracy,
95 'timestamp': position.timestamp.toIso8601String(),
96 };
97
98 try {
99 final placemarks = await _geocoding.placemarkFromCoordinates(
100 position.latitude,
101 position.longitude,
102 );
103 if (placemarks.isNotEmpty) {
104 final place = placemarks.first;
105 final placemark = <String, dynamic>{
106 'name': _nullIfEmpty(place.name),
107 'street': _nullIfEmpty(place.street),
108 'locality': _nullIfEmpty(place.locality),
109 'sub_admin_area': _nullIfEmpty(place.subAdministrativeArea),
110 'admin_area': _nullIfEmpty(place.administrativeArea),
111 'postal_code': _nullIfEmpty(place.postalCode),
112 'country': _nullIfEmpty(place.country),
113 'iso_country_code': _nullIfEmpty(place.isoCountryCode),
114 };
115 result['placemark'] = placemark;
116 result['formatted_address'] = _formatAddress(place);
117 }
118 } catch (e) {
119 result['geocoding_error'] =
120 'Reverse-geocoding unavailable ($e). Coordinates are still valid.';
121 }
122
123 return result;
124 }
125
126 // ---------------------------------------------------------------------------
127 // Helpers
128 // ---------------------------------------------------------------------------
129
130 String? _nullIfEmpty(String? value) {
131 if (value == null || value.trim().isEmpty) return null;
132 return value;
133 }
134
135 String _formatAddress(Placemark place) {
136 final parts = [
137 place.street,
138 place.locality,
139 place.administrativeArea,
140 place.postalCode,
141 place.country,
142 ].where((p) => p != null && p.trim().isNotEmpty).toList(growable: false);
143 return parts.join(', ');
144 }
145 }