Files
ignis_app/lib/features/homes/providers/location_providers.dart
2026-05-01 09:13:23 +07:00

238 lines
7.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
enum UserLocationIssue {
servicesDisabled,
permissionDenied,
permissionDeniedForever,
unavailable,
}
/// Состояние геолокации: позиция или причина отсутствия.
/// Запрашивается один раз, кешируется до перезапуска провайдера.
class UserLocation {
final Position? position;
final String? error; // null -- всё ок, иначе причина
final UserLocationIssue? issue;
final DateTime? updatedAt;
const UserLocation({this.position, this.error, this.issue, this.updatedAt});
bool get hasPosition => position != null;
bool get needsAppSettings =>
issue == UserLocationIssue.permissionDeniedForever;
bool get needsLocationSettings => issue == UserLocationIssue.servicesDisabled;
bool get canRequestPermission => issue == UserLocationIssue.permissionDenied;
/// Расстояние в км до точки. Возвращает 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, UserLocation>(
() => UserLocationNotifier(),
);
class UserLocationNotifier extends Notifier<UserLocation> {
StreamSubscription<Position>? _sub;
int _watchers = 0;
@override
UserLocation build() {
ref.onDispose(() {
_sub?.cancel();
_sub = null;
});
return const UserLocation();
}
/// Запросить текущую позицию. Первый вызов проверяет пермишены
/// и отдаёт lastKnown мгновенно (если есть).
Future<void> fetch() async {
if (state.hasPosition) return;
await refresh();
}
/// Начать непрерывное отслеживание. Вызывать из initState экрана.
/// Ref-counted: несколько экранов могут вызвать startWatching,
/// стрим остановится только когда все вызовут stopWatching.
Future<void> startWatching() async {
_watchers++;
if (_sub != null) return;
final permissionState = await _ensurePermission();
if (!permissionState.isGranted) {
state = permissionState.toLocation();
return;
}
if (!state.hasPosition) {
try {
final last = await Geolocator.getLastKnownPosition();
if (last != null) {
state = _fromPosition(last);
}
} catch (_) {}
}
const settings = LocationSettings(
accuracy: LocationAccuracy.low,
distanceFilter: 20,
);
_sub = Geolocator.getPositionStream(locationSettings: settings).listen(
(pos) => state = _fromPosition(pos),
onError: (e) {
debugPrint('Ошибка стрима геолокации: $e');
state = UserLocation(
error: 'Не удалось отслеживать позицию: $e',
issue: UserLocationIssue.unavailable,
updatedAt: state.updatedAt,
);
},
);
}
/// Остановить отслеживание. Вызывать из dispose экрана.
void stopWatching() {
_watchers = (_watchers - 1).clamp(0, 999);
if (_watchers == 0) {
_sub?.cancel();
_sub = null;
}
}
Future<void> refresh() async {
final permissionState = await _ensurePermission();
if (!permissionState.isGranted) {
state = permissionState.toLocation();
return;
}
try {
final last = await Geolocator.getLastKnownPosition();
if (last != null) {
state = _fromPosition(last);
return;
}
final pos = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.low,
timeLimit: Duration(seconds: 10),
),
);
state = _fromPosition(pos);
} catch (e) {
state = UserLocation(
error: 'Не удалось получить позицию: $e',
issue: UserLocationIssue.unavailable,
updatedAt: state.updatedAt,
);
}
}
Future<void> requestPermission() async {
await Geolocator.requestPermission();
if (_watchers > 0 && _sub == null) {
await startWatching();
return;
}
await refresh();
}
Future<void> openAppSettings() async {
await Geolocator.openAppSettings();
}
Future<void> openLocationSettings() async {
await Geolocator.openLocationSettings();
}
/// Проверить сервис и пермишены. Возвращает null если всё ок,
/// иначе строку с причиной ошибки.
Future<_LocationPermissionState> _ensurePermission() async {
if (!await Geolocator.isLocationServiceEnabled()) {
return const _LocationPermissionState(
issue: UserLocationIssue.servicesDisabled,
message: 'Геолокация выключена',
);
}
var perm = await Geolocator.checkPermission();
if (perm == LocationPermission.denied) {
perm = await Geolocator.requestPermission();
}
if (perm == LocationPermission.denied) {
return const _LocationPermissionState(
issue: UserLocationIssue.permissionDenied,
message: 'Нет разрешения на геолокацию',
);
}
if (perm == LocationPermission.deniedForever) {
return const _LocationPermissionState(
issue: UserLocationIssue.permissionDeniedForever,
message: 'Доступ к геолокации запрещён навсегда',
);
}
return const _LocationPermissionState();
}
UserLocation _fromPosition(Position position) {
return UserLocation(position: position, updatedAt: position.timestamp);
}
}
class _LocationPermissionState {
final UserLocationIssue? issue;
final String? message;
const _LocationPermissionState({this.issue, this.message});
bool get isGranted => issue == null;
UserLocation toLocation() {
return UserLocation(error: message, issue: issue);
}
}
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()} км';
}
}