385 lines
9.3 KiB
Dart
385 lines
9.3 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert';
|
||
import 'dart:math' as math;
|
||
import 'dart:ui';
|
||
|
||
import 'package:dio/dio.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:geolocator/geolocator.dart';
|
||
import 'package:libac_dart/nbt/Stream.dart';
|
||
import 'package:timetrack/consts.dart';
|
||
|
||
class SessionData {
|
||
static DateTime StartTime = DateTime(0);
|
||
|
||
static bool IsOnTheClock = false;
|
||
|
||
static List<Trip> Trips = [];
|
||
|
||
static Delivery? currentDelivery;
|
||
static Trip? currentTrip;
|
||
static List<Position> positions = [];
|
||
static late StreamSubscription<Position> _listener;
|
||
static Callbacks Calls = Callbacks();
|
||
static String LastSessionID = "";
|
||
static String DisplayError = "";
|
||
|
||
/// This flag is usually set when data is loaded from a saved state. Or when accessed using the Web version of the app.
|
||
static bool IsReadOnly = false;
|
||
|
||
static double GetTotalBasePay() {
|
||
double total = 0;
|
||
for (var trip in Trips) {
|
||
total += trip.BasePay;
|
||
}
|
||
|
||
return total;
|
||
}
|
||
|
||
static double GetTotalTips() {
|
||
double total = 0;
|
||
for (var trip in Trips) {
|
||
for (var drop in trip.deliveries) {
|
||
total += drop.TipAmount;
|
||
}
|
||
}
|
||
|
||
return total;
|
||
}
|
||
|
||
static double GetTotalPay() {
|
||
return GetTotalBasePay() + GetTotalTips();
|
||
}
|
||
|
||
static double GetTotalMiles() {
|
||
double total = 0;
|
||
total = _totalMilesTraveled(
|
||
positions,
|
||
minDistanceMeters: 5,
|
||
maxDistanceMeters: 512,
|
||
);
|
||
return total;
|
||
}
|
||
|
||
static String GetTotalMilesAsString() {
|
||
double miles = GetTotalMiles();
|
||
if (miles == 0) return "0.0";
|
||
List<String> split = miles.toString().split(".");
|
||
StringBuilder out = StringBuilder();
|
||
|
||
out.append(split[0]);
|
||
out.append(".");
|
||
out.append(split[1].substring(0, 4));
|
||
|
||
return out.toString();
|
||
}
|
||
|
||
//** Begin AI Generated code */
|
||
/// Converts meters → miles.
|
||
static const _metersPerMile = 1609.344;
|
||
|
||
/// Radius of the earth in meters (WGS‑84 mean radius).
|
||
static const _earthRadiusM = 6371000.0;
|
||
static double _haversineMeters(
|
||
double lat1,
|
||
double lon1,
|
||
double lat2,
|
||
double lon2,
|
||
) {
|
||
final dLat = _deg2rad(lat2 - lat1);
|
||
final dLon = _deg2rad(lon2 - lon1);
|
||
|
||
final a =
|
||
math.sin(dLat / 2) * math.sin(dLat / 2) +
|
||
math.cos(_deg2rad(lat1)) *
|
||
math.cos(_deg2rad(lat2)) *
|
||
math.sin(dLon / 2) *
|
||
math.sin(dLon / 2);
|
||
|
||
return _earthRadiusM * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
||
}
|
||
|
||
static double _deg2rad(double deg) => deg * math.pi / 180.0;
|
||
|
||
/// Returns total miles traveled after basic GPS‑noise cleanup.
|
||
///
|
||
/// * [minDistanceMeters] – drop segments shorter than this (jitter).
|
||
/// * [maxDistanceMeters] – drop segments longer than this (impossible jump).
|
||
static double _totalMilesTraveled(
|
||
List<Position> positions, {
|
||
double minDistanceMeters = 5,
|
||
double? maxDistanceMeters,
|
||
}) {
|
||
if (positions.length < 2) return 0;
|
||
|
||
var meters = 0.0;
|
||
|
||
for (var i = 1; i < positions.length; i++) {
|
||
final p1 = positions[i - 1];
|
||
final p2 = positions[i];
|
||
|
||
final d = _haversineMeters(
|
||
p1.latitude,
|
||
p1.longitude,
|
||
p2.latitude,
|
||
p2.longitude,
|
||
);
|
||
|
||
if (d < minDistanceMeters) continue; // too small → jitter
|
||
if (maxDistanceMeters != null && d > maxDistanceMeters)
|
||
continue; // glitch
|
||
|
||
meters += d;
|
||
}
|
||
return meters / _metersPerMile;
|
||
}
|
||
//** End AI Generated code */
|
||
|
||
static Future<void> Login() async {
|
||
StartTime = DateTime.now();
|
||
IsOnTheClock = true;
|
||
|
||
bool hasGPS;
|
||
|
||
LocationPermission perm;
|
||
hasGPS = await Geolocator.isLocationServiceEnabled();
|
||
if (!hasGPS) {
|
||
IsOnTheClock = false;
|
||
return Future.error("Location services are disabled");
|
||
}
|
||
|
||
perm = await Geolocator.checkPermission();
|
||
if (perm == LocationPermission.denied) {
|
||
perm = await Geolocator.requestPermission();
|
||
if (perm == LocationPermission.denied) {
|
||
IsOnTheClock = false;
|
||
return Future.error("Location permissions are denied");
|
||
}
|
||
}
|
||
|
||
if (perm == LocationPermission.deniedForever) {
|
||
IsOnTheClock = false;
|
||
return Future.error(
|
||
"Location permissions are denied permanently. Login cannot proceed.",
|
||
);
|
||
}
|
||
|
||
_listener = Geolocator.getPositionStream(
|
||
locationSettings: TTConsts.LOCATION_SETTINGS,
|
||
).listen((pos) {
|
||
if (!IsOnTheClock) {
|
||
_listener.cancel();
|
||
return;
|
||
}
|
||
positions.add(pos);
|
||
|
||
SessionData.Calls.dispatch();
|
||
});
|
||
}
|
||
|
||
static Future<void> Logout() async {
|
||
IsOnTheClock = false;
|
||
currentDelivery = null;
|
||
currentTrip = null;
|
||
_listener.cancel();
|
||
|
||
var saveData = SaveData();
|
||
print(saveData);
|
||
|
||
Trips = [];
|
||
positions = [];
|
||
|
||
Dio dio = Dio();
|
||
Map<String, dynamic> payload = {"cmd": "create", "data": saveData};
|
||
|
||
var reply = await dio.post(
|
||
TTConsts.SESSION_SERVER,
|
||
data: json.encode(payload),
|
||
);
|
||
Map<String, dynamic> replyJs = json.decode(reply.data as String);
|
||
if (replyJs["status"] == "ok") {
|
||
print("Successful upload");
|
||
LastSessionID = replyJs['session'] as String;
|
||
Calls.dispatch();
|
||
}
|
||
}
|
||
|
||
static Map<String, dynamic> SaveData() {
|
||
Map<String, dynamic> saveData = {};
|
||
|
||
List<Map<String, dynamic>> _trips = [];
|
||
for (var trip in Trips) {
|
||
_trips.add(trip.toJsonMap());
|
||
}
|
||
|
||
List<Map<String, dynamic>> _pos = [];
|
||
for (var pos in positions) {
|
||
_pos.add(pos.toJson());
|
||
}
|
||
|
||
saveData["trips"] = _trips;
|
||
saveData["positions"] = _pos;
|
||
|
||
return saveData;
|
||
}
|
||
|
||
static Future<void> DownloadData() async {
|
||
Dio dio = Dio();
|
||
Map<String, dynamic> payload = {"cmd": "get", "id": LastSessionID};
|
||
|
||
// Send the data, and get the response
|
||
var reply = await dio.post(
|
||
TTConsts.SESSION_SERVER,
|
||
data: json.encode(payload),
|
||
);
|
||
|
||
LoadData(reply.data as String);
|
||
}
|
||
|
||
static void LoadData(String js) {
|
||
Map<String, dynamic> _js = json.decode(js);
|
||
if (_js.containsKey("error")) {
|
||
LastSessionID = "";
|
||
return;
|
||
}
|
||
List<Map<String, dynamic>> _trips =
|
||
_js['trips'] as List<Map<String, dynamic>>;
|
||
List<Map<String, dynamic>> _pos =
|
||
_js['positions'] as List<Map<String, dynamic>>;
|
||
|
||
for (var trip in _trips) {
|
||
Trips.add(Trip.fromJsonMap(trip));
|
||
}
|
||
|
||
for (var position in _pos) {
|
||
positions.add(Position.fromMap(position));
|
||
}
|
||
|
||
IsReadOnly = true;
|
||
}
|
||
|
||
static Future<Position> GetNewLocation() async {
|
||
Position pos = await Geolocator.getCurrentPosition(
|
||
locationSettings: TTConsts.LOCATION_SETTINGS,
|
||
);
|
||
|
||
return pos;
|
||
}
|
||
|
||
static Trip GetNewTrip({required double basePay}) {
|
||
currentTrip = Trip(BasePay: basePay);
|
||
Trips.add(currentTrip!);
|
||
return currentTrip!;
|
||
}
|
||
|
||
static Delivery GetNewDelivery() {
|
||
if (currentTrip != null) {
|
||
var dropOff = currentTrip!.startNewDelivery();
|
||
;
|
||
currentDelivery = dropOff;
|
||
return dropOff;
|
||
} else {
|
||
throw Exception("A delivery cannot exist without a trip");
|
||
}
|
||
}
|
||
|
||
static void EndTrip() {
|
||
currentDelivery = null;
|
||
currentTrip = null;
|
||
}
|
||
}
|
||
|
||
class Delivery {
|
||
double TipAmount = 0;
|
||
Position? endLocation;
|
||
DateTime StartTime = DateTime.now();
|
||
|
||
Delivery() {
|
||
StartTime = DateTime.now();
|
||
}
|
||
|
||
Future<void> MarkEndLocation() async {
|
||
var pos = await SessionData.GetNewLocation();
|
||
endLocation = pos;
|
||
}
|
||
|
||
Map<String, dynamic> toJsonMap() {
|
||
return {
|
||
"tip": TipAmount,
|
||
"start": StartTime.toString(),
|
||
"endPos": endLocation?.toJson() ?? "incomplete",
|
||
};
|
||
}
|
||
|
||
static Delivery fromMap(Map<String, dynamic> jsx) {
|
||
Delivery delivery = Delivery();
|
||
delivery.StartTime = DateTime.parse(jsx['start'] as String);
|
||
delivery.TipAmount = jsx['tip'] as double;
|
||
if (jsx['endPos'] as String == "incomplete")
|
||
delivery.endLocation = null;
|
||
else
|
||
delivery.endLocation = Position.fromMap(jsx['endPos']);
|
||
|
||
return delivery;
|
||
}
|
||
}
|
||
|
||
class Trip {
|
||
List<Delivery> deliveries = [];
|
||
|
||
DateTime StartTime = DateTime(0);
|
||
|
||
double BasePay = 0.0;
|
||
|
||
Trip({required this.BasePay}) {
|
||
StartTime = DateTime.now();
|
||
}
|
||
|
||
Delivery startNewDelivery() {
|
||
var delivery = Delivery();
|
||
deliveries.add(delivery);
|
||
|
||
return delivery;
|
||
}
|
||
|
||
Map<String, dynamic> toJsonMap() {
|
||
Map<String, Object> _trip = {"start": StartTime.toString(), "pay": BasePay};
|
||
List<Map<String, dynamic>> _dropOffs = [];
|
||
for (var delivery in deliveries) {
|
||
_dropOffs.add(delivery.toJsonMap());
|
||
}
|
||
|
||
_trip["deliveries"] = _dropOffs;
|
||
|
||
return _trip;
|
||
}
|
||
|
||
static Trip fromJsonMap(Map<String, dynamic> jsx) {
|
||
Trip trip = Trip(BasePay: 0);
|
||
trip.BasePay = jsx['pay'] as double;
|
||
trip.StartTime = DateTime.parse(jsx['start'] as String);
|
||
trip.deliveries = [];
|
||
List<Map<String, dynamic>> _dropOffs =
|
||
jsx['deliveries'] as List<Map<String, dynamic>>;
|
||
|
||
for (var dropOff in _dropOffs) {
|
||
trip.deliveries.add(Delivery.fromMap(dropOff));
|
||
}
|
||
|
||
return trip;
|
||
}
|
||
}
|
||
|
||
class Callbacks {
|
||
VoidCallback? HomeCallback;
|
||
VoidCallback? MapCallback;
|
||
VoidCallback? UpdateSettingsCallback;
|
||
VoidCallback? WorkDataCallback;
|
||
|
||
void dispatch() {
|
||
if (HomeCallback != null) HomeCallback!();
|
||
if (MapCallback != null) MapCallback!();
|
||
if (UpdateSettingsCallback != null) UpdateSettingsCallback!();
|
||
if (WorkDataCallback != null) WorkDataCallback!();
|
||
}
|
||
}
|