Files
ignis_app/lib/features/homes/providers/location_providers.dart
2026-04-27 23:21:47 +07:00

170 lines
5.1 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';
/// Состояние геолокации: позиция или причина отсутствия.
/// Запрашивается один раз, кешируется до перезапуска провайдера.
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, 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;
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<void> 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<String?> _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()} км';
}
}