Custom filtering logic
This commit is contained in:
parent
00599d7b1e
commit
5226e4dd90
6 changed files with 136 additions and 12 deletions
|
|
@ -92,7 +92,7 @@ PODS:
|
|||
- nanopb (~> 3.30910.0)
|
||||
- PromisesSwift (~> 2.1)
|
||||
- Flutter (1.0.0)
|
||||
- flutter_background_geolocation (4.16.10):
|
||||
- flutter_background_geolocation (4.16.11):
|
||||
- CocoaLumberjack (~> 3.8.5)
|
||||
- Flutter
|
||||
- GoogleAppMeasurement (11.13.0):
|
||||
|
|
@ -234,7 +234,7 @@ SPEC CHECKSUMS:
|
|||
FirebaseRemoteConfigInterop: 7b74ceaa54e28863ed17fa39da8951692725eced
|
||||
FirebaseSessions: eaa8ec037e7793769defe4201c20bd4d976f9677
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_background_geolocation: a67777b118f342ae7e59d42f5afafff42a131aaf
|
||||
flutter_background_geolocation: defe705fe7c50b1be772e0d298ed3765fa17c022
|
||||
GoogleAppMeasurement: 0dfca1a4b534d123de3945e28f77869d10d0d600
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg;
|
||||
import 'package:traccar_client/location_cache.dart';
|
||||
import 'package:traccar_client/preferences.dart';
|
||||
import 'package:wakelock_partial_android/wakelock_partial_android.dart';
|
||||
|
||||
|
|
@ -13,6 +17,7 @@ class GeolocationService {
|
|||
}
|
||||
bg.BackgroundGeolocation.onEnabledChange(onEnabledChange);
|
||||
bg.BackgroundGeolocation.onMotionChange(onMotionChange);
|
||||
bg.BackgroundGeolocation.onLocation(onLocation);
|
||||
bg.BackgroundGeolocation.onHeartbeat(onHeartbeat);
|
||||
}
|
||||
|
||||
|
|
@ -34,9 +39,67 @@ class GeolocationService {
|
|||
}
|
||||
}
|
||||
|
||||
static Future<void> onLocation(bg.Location location) async {
|
||||
if (_shouldDelete(location)) {
|
||||
await bg.BackgroundGeolocation.destroyLocation(location.uuid);
|
||||
} else {
|
||||
LocationCache.set(location);
|
||||
try {
|
||||
await bg.BackgroundGeolocation.sync();
|
||||
} catch (error) {
|
||||
developer.log('Failed to send location', error: error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> onHeartbeat(bg.HeartbeatEvent event) async {
|
||||
await bg.BackgroundGeolocation.getCurrentPosition(samples: 1, persist: true, extras: {'heartbeat': true});
|
||||
}
|
||||
|
||||
static bool _shouldDelete(bg.Location location) {
|
||||
if (!location.isMoving) return false;
|
||||
if (location.extras?.isNotEmpty == true) return false;
|
||||
|
||||
final lastLocation = LocationCache.get();
|
||||
if (lastLocation == null) return false;
|
||||
|
||||
final duration = DateTime.parse(location.timestamp).difference(DateTime.parse(lastLocation.timestamp)).inSeconds;
|
||||
|
||||
final fastestInterval = Preferences.instance.getInt(Preferences.fastestInterval);
|
||||
if (fastestInterval != null && duration < fastestInterval) return true;
|
||||
|
||||
final distance = _distance(lastLocation, location);
|
||||
|
||||
final distanceFilter = Preferences.instance.getInt(Preferences.distance) ?? 0;
|
||||
if (distanceFilter > 0 && distance >= distanceFilter) return false;
|
||||
|
||||
final isHighestAccuracy = Preferences.instance.getString(Preferences.accuracy) == 'highest';
|
||||
|
||||
if (distanceFilter == 0 || isHighestAccuracy) {
|
||||
final intervalFilter = Preferences.instance.getInt(Preferences.interval) ?? 0;
|
||||
if (intervalFilter > 0 && duration >= intervalFilter) return false;
|
||||
}
|
||||
|
||||
if (isHighestAccuracy && lastLocation.heading >= 0 && location.coords.heading > 0) {
|
||||
final angle = (location.coords.heading - lastLocation.heading).abs();
|
||||
final angleFilter = Preferences.instance.getInt(Preferences.angle) ?? 0;
|
||||
if (angleFilter > 0 && angle >= angleFilter) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static double _distance(Location from, bg.Location to) {
|
||||
const earthRadius = 6371000; // meters
|
||||
final dLat = _degToRad(to.coords.latitude - from.latitude);
|
||||
final dLon = _degToRad(to.coords.longitude - from.longitude);
|
||||
final a = sin(dLat / 2) * sin(dLat / 2) +
|
||||
cos(_degToRad(from.latitude)) * cos(_degToRad(to.coords.latitude)) * sin(dLon / 2) * sin(dLon / 2);
|
||||
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
||||
return earthRadius * c;
|
||||
}
|
||||
|
||||
static double _degToRad(double degree) => degree * pi / 180.0;
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
|
|
@ -49,6 +112,9 @@ void headlessTask(bg.HeadlessEvent headlessEvent) async {
|
|||
case bg.Event.MOTIONCHANGE:
|
||||
await GeolocationService.onMotionChange(headlessEvent.event);
|
||||
break;
|
||||
case bg.Event.LOCATION:
|
||||
await GeolocationService.onLocation(headlessEvent.event);
|
||||
break;
|
||||
case bg.Event.HEARTBEAT:
|
||||
await GeolocationService.onHeartbeat(headlessEvent.event);
|
||||
break;
|
||||
|
|
|
|||
51
lib/location_cache.dart
Normal file
51
lib/location_cache.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg;
|
||||
import 'package:traccar_client/preferences.dart';
|
||||
|
||||
class Location {
|
||||
final String timestamp;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final double heading;
|
||||
const Location({
|
||||
required this.timestamp,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
required this.heading,
|
||||
});
|
||||
}
|
||||
|
||||
class LocationCache {
|
||||
static Location? _last;
|
||||
|
||||
static Location? get() {
|
||||
if (_last == null) {
|
||||
final timestamp = Preferences.instance.getString(Preferences.lastTimestamp);
|
||||
final latitude = Preferences.instance.getDouble(Preferences.lastLatitude);
|
||||
final longitude = Preferences.instance.getDouble(Preferences.lastLongitude);
|
||||
final heading = Preferences.instance.getDouble(Preferences.lastHeading);
|
||||
if (timestamp != null && latitude != null && longitude != null && heading != null) {
|
||||
_last = Location(
|
||||
timestamp: timestamp,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
heading: heading,
|
||||
);
|
||||
}
|
||||
}
|
||||
return _last;
|
||||
}
|
||||
|
||||
static Future<void> set(bg.Location location) async {
|
||||
final last = Location(
|
||||
timestamp: location.timestamp,
|
||||
latitude: location.coords.latitude,
|
||||
longitude: location.coords.longitude,
|
||||
heading: location.coords.heading,
|
||||
);
|
||||
Preferences.instance.setString(Preferences.lastTimestamp, last.timestamp);
|
||||
Preferences.instance.setDouble(Preferences.lastLatitude, last.latitude);
|
||||
Preferences.instance.setDouble(Preferences.lastLongitude, last.longitude);
|
||||
Preferences.instance.setDouble(Preferences.lastHeading, last.heading);
|
||||
_last = last;
|
||||
}
|
||||
}
|
||||
|
|
@ -119,8 +119,7 @@ class _MainScreenState extends State<MainScreen> {
|
|||
FilledButton.tonal(
|
||||
onPressed: () async {
|
||||
try {
|
||||
await bg.BackgroundGeolocation.getCurrentPosition(samples: 1, persist: true);
|
||||
await bg.BackgroundGeolocation.sync();
|
||||
await bg.BackgroundGeolocation.getCurrentPosition(samples: 1, persist: true, extras: {'manual': true});
|
||||
} catch (error) {
|
||||
developer.log('Failed to fetch location', error: error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:shared_preferences_android/shared_preferences_android.dart';
|
|||
|
||||
class Preferences {
|
||||
static late SharedPreferencesWithCache instance;
|
||||
|
||||
static const String id = 'id';
|
||||
static const String url = 'url';
|
||||
static const String accuracy = 'accuracy';
|
||||
|
|
@ -20,6 +21,11 @@ class Preferences {
|
|||
static const String wakelock = 'wakelock';
|
||||
static const String stopDetection = 'stop_detection';
|
||||
|
||||
static const String lastTimestamp = 'lastTimestamp';
|
||||
static const String lastLatitude = 'lastLatitude';
|
||||
static const String lastLongitude = 'lastLongitude';
|
||||
static const String lastHeading = 'lastHeading';
|
||||
|
||||
static Future<void> init() async {
|
||||
instance = await SharedPreferencesWithCache.create(
|
||||
sharedPreferencesOptions: Platform.isAndroid
|
||||
|
|
@ -29,6 +35,7 @@ class Preferences {
|
|||
allowList: {
|
||||
id, url, accuracy, distance, interval, angle, heartbeat,
|
||||
fastestInterval, buffer, wakelock, stopDetection,
|
||||
lastTimestamp, lastLatitude, lastLongitude, lastHeading,
|
||||
'device_id_preference', 'server_url_preference', 'accuracy_preference',
|
||||
'frequency_preference', 'distance_preference', 'buffer_preference',
|
||||
},
|
||||
|
|
@ -66,6 +73,7 @@ class Preferences {
|
|||
}
|
||||
|
||||
static bg.Config geolocationConfig() {
|
||||
final isHighestAccuracy = instance.getString(accuracy) == 'highest';
|
||||
final locationUpdateInterval = (instance.getInt(interval) ?? 0) * 1000;
|
||||
final fastestLocationUpdateInterval = (instance.getInt(fastestInterval) ?? 30) * 1000;
|
||||
final heartbeatInterval = instance.getInt(heartbeat) ?? 0;
|
||||
|
|
@ -79,12 +87,13 @@ class Preferences {
|
|||
'low' => bg.Config.DESIRED_ACCURACY_LOW,
|
||||
_ => bg.Config.DESIRED_ACCURACY_MEDIUM,
|
||||
},
|
||||
autoSync: false,
|
||||
url: _formatUrl(instance.getString(url)),
|
||||
params: {
|
||||
"device_id": instance.getString(id),
|
||||
'device_id': instance.getString(id),
|
||||
},
|
||||
distanceFilter: instance.getInt(distance)?.toDouble(),
|
||||
locationUpdateInterval: locationUpdateInterval > 0 ? locationUpdateInterval : null,
|
||||
distanceFilter: isHighestAccuracy ? 0 : instance.getInt(distance)?.toDouble(),
|
||||
locationUpdateInterval: isHighestAccuracy ? 0 : (locationUpdateInterval > 0 ? locationUpdateInterval : null),
|
||||
heartbeatInterval: heartbeatInterval > 0 ? heartbeatInterval : null,
|
||||
maxRecordsToPersist: instance.getBool(buffer) != false ? -1 : 1,
|
||||
logLevel: bg.Config.LOG_LEVEL_VERBOSE,
|
||||
|
|
@ -97,10 +106,10 @@ class Preferences {
|
|||
pausesLocationUpdatesAutomatically: instance.getBool(stopDetection) == false,
|
||||
fastestLocationUpdateInterval: fastestLocationUpdateInterval > 0 ? fastestLocationUpdateInterval : null,
|
||||
backgroundPermissionRationale: bg.PermissionRationale(
|
||||
title: "Allow {applicationName} to access this device's location in the background",
|
||||
message: "For reliable tracking, please enable {backgroundPermissionOptionLabel} location access.",
|
||||
positiveAction: "Change to {backgroundPermissionOptionLabel}",
|
||||
negativeAction: "Cancel"
|
||||
title: 'Allow {applicationName} to access this device\'s location in the background',
|
||||
message: 'For reliable tracking, please enable {backgroundPermissionOptionLabel} location access.',
|
||||
positiveAction: 'Change to {backgroundPermissionOptionLabel}',
|
||||
negativeAction: 'Cancel'
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ class _QuickActionsInitializerState extends State<QuickActionsInitializer> {
|
|||
case 'sos':
|
||||
try {
|
||||
await bg.BackgroundGeolocation.getCurrentPosition(samples: 1, persist: true, extras: {'alarm': 'sos'});
|
||||
await bg.BackgroundGeolocation.sync();
|
||||
} catch (error) {
|
||||
developer.log('Failed to send alert', error: error);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue