108 lines
3.5 KiB
Dart
108 lines
3.5 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:geolocator/geolocator.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
|
|
final locationServiceProvider = Provider<LocationService>((ref) {
|
|
return LocationService();
|
|
});
|
|
|
|
final locationStreamProvider = StreamProvider<Position>((ref) {
|
|
final service = ref.watch(locationServiceProvider);
|
|
return service.getFilteredPositionStream();
|
|
});
|
|
|
|
class LocationService {
|
|
Position? _lastValidPosition;
|
|
double? _emaSpeed;
|
|
|
|
// Filtering thresholds
|
|
static const double _maxRealisticSpeed = 100.0; // roughly 223 MPH in m/s
|
|
static const double _maxAcceleration = 20.0; // m/s^2 (unrealistic jump)
|
|
static const double _speedEmaAlpha = 0.3; // Smoothing factor for speed
|
|
|
|
Future<bool> requestPermissions() async {
|
|
final status = await Permission.locationWhenInUse.request();
|
|
if (status.isGranted) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Stream<Position> getFilteredPositionStream() {
|
|
final LocationSettings locationSettings = AndroidSettings(
|
|
accuracy: LocationAccuracy.bestForNavigation,
|
|
distanceFilter: 0,
|
|
intervalDuration: const Duration(milliseconds: 200), // 5Hz updates
|
|
);
|
|
|
|
return Geolocator.getPositionStream(locationSettings: locationSettings)
|
|
.where(_isValidPosition)
|
|
.map(_applyEmaFilter);
|
|
}
|
|
|
|
bool _isValidPosition(Position position) {
|
|
// Dynamic accuracy threshold: allow worse accuracy at higher speeds
|
|
final currentSpeed = _emaSpeed ?? position.speed;
|
|
final maxAcceptableAccuracy = currentSpeed > 20.0 ? 30.0 : 15.0;
|
|
|
|
if (position.accuracy > maxAcceptableAccuracy) {
|
|
return false; // Reject extremely poor accuracy
|
|
}
|
|
|
|
if (position.speed > _maxRealisticSpeed) {
|
|
return false; // Reject impossible speeds
|
|
}
|
|
|
|
if (_lastValidPosition != null) {
|
|
final timeDiff = (position.timestamp.difference(_lastValidPosition!.timestamp)).inMilliseconds / 1000.0;
|
|
if (timeDiff > 0) {
|
|
// Acceleration check
|
|
final accel = (position.speed - _lastValidPosition!.speed).abs() / timeDiff;
|
|
if (accel > _maxAcceleration) {
|
|
return false; // Reject impossible acceleration spikes
|
|
}
|
|
|
|
// Path consistency check (distance jump vs time)
|
|
final distanceJump = Geolocator.distanceBetween(
|
|
_lastValidPosition!.latitude, _lastValidPosition!.longitude,
|
|
position.latitude, position.longitude
|
|
);
|
|
|
|
// If distance jumped implies speed > max realistic speed + some buffer, reject
|
|
final impliedSpeed = distanceJump / timeDiff;
|
|
if (impliedSpeed > _maxRealisticSpeed * 1.5) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
_lastValidPosition = position;
|
|
return true;
|
|
}
|
|
|
|
Position _applyEmaFilter(Position position) {
|
|
if (_emaSpeed == null) {
|
|
_emaSpeed = position.speed;
|
|
} else {
|
|
_emaSpeed = _speedEmaAlpha * position.speed + (1 - _speedEmaAlpha) * _emaSpeed!;
|
|
}
|
|
|
|
// Return a new Position with the smoothed speed
|
|
return Position(
|
|
longitude: position.longitude,
|
|
latitude: position.latitude,
|
|
timestamp: position.timestamp,
|
|
accuracy: position.accuracy,
|
|
altitude: position.altitude,
|
|
heading: position.heading,
|
|
speed: _emaSpeed!,
|
|
speedAccuracy: position.speedAccuracy,
|
|
altitudeAccuracy: position.altitudeAccuracy,
|
|
headingAccuracy: position.headingAccuracy,
|
|
isMocked: position.isMocked,
|
|
floor: position.floor,
|
|
);
|
|
}
|
|
}
|