import 'dart:async'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; /// Состояние геолокации: позиция или причина отсутствия. /// Запрашивается один раз, кешируется до перезапуска провайдера. class UserLocation { final Position? position; final String? error; // null -- всё ок, иначе причина const UserLocation({this.position, this.error}); bool get hasPosition => position != null; /// Расстояние в км до точки. Возвращает null если нет позиции /// или у цели нет координат. double? distanceToKm(double? lat, double? lon) { if (position == null || lat == null || lon == null) return null; return calculateDistanceKm( position!.latitude, position!.longitude, lat, lon, ); } } final userLocationProvider = NotifierProvider( () => UserLocationNotifier(), ); class UserLocationNotifier extends Notifier { StreamSubscription? _sub; int _watchers = 0; @override UserLocation build() { ref.onDispose(() { _sub?.cancel(); _sub = null; }); return const UserLocation(); } /// Запросить текущую позицию. Первый вызов проверяет пермишены /// и отдаёт lastKnown мгновенно (если есть). Future fetch() async { if (state.hasPosition) return; final err = await _ensurePermission(); if (err != null) { state = UserLocation(error: err); return; } try { final last = await Geolocator.getLastKnownPosition(); if (last != null) { state = UserLocation(position: last); return; } final pos = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.low, timeLimit: Duration(seconds: 10), ), ); state = UserLocation(position: pos); } catch (e) { state = UserLocation(error: 'Ошибка: $e'); } } /// Начать непрерывное отслеживание. Вызывать из initState экрана. /// Ref-counted: несколько экранов могут вызвать startWatching, /// стрим остановится только когда все вызовут stopWatching. Future startWatching() async { _watchers++; if (_sub != null) return; final err = await _ensurePermission(); if (err != null) { state = UserLocation(error: err); return; } if (!state.hasPosition) { try { final last = await Geolocator.getLastKnownPosition(); if (last != null) { state = UserLocation(position: last); } } catch (_) {} } const settings = LocationSettings( accuracy: LocationAccuracy.low, distanceFilter: 20, ); _sub = Geolocator.getPositionStream(locationSettings: settings).listen( (pos) => state = UserLocation(position: pos), onError: (e) { debugPrint('Ошибка стрима геолокации: $e'); }, ); } /// Остановить отслеживание. Вызывать из dispose экрана. void stopWatching() { _watchers = (_watchers - 1).clamp(0, 999); if (_watchers == 0) { _sub?.cancel(); _sub = null; } } /// Проверить сервис и пермишены. Возвращает null если всё ок, /// иначе строку с причиной ошибки. Future _ensurePermission() async { if (!await Geolocator.isLocationServiceEnabled()) { return 'Геолокация выключена'; } var perm = await Geolocator.checkPermission(); if (perm == LocationPermission.denied) { perm = await Geolocator.requestPermission(); } if (perm == LocationPermission.denied) { return 'Нет разрешения'; } if (perm == LocationPermission.deniedForever) { return 'Разрешение запрещено навсегда'; } return null; } } double calculateDistanceKm(double lat1, double lon1, double lat2, double lon2) { const earthRadiusKm = 6371.0; final dLat = _degToRad(lat2 - lat1); final dLon = _degToRad(lon2 - lon1); final a = math.sin(dLat / 2) * math.sin(dLat / 2) + math.cos(_degToRad(lat1)) * math.cos(_degToRad(lat2)) * math.sin(dLon / 2) * math.sin(dLon / 2); final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); return earthRadiusKm * c; } double _degToRad(double deg) => deg * (math.pi / 180); /// Форматирование расстояния в человекочитаемый вид String formatDistance(double km) { if (km < 1.0) { return '${(km * 1000).round()} м'; } else if (km < 10.0) { return '${km.toStringAsFixed(1)} км'; } else { return '${km.round()} км'; } }