TimeTracker/lib/data.dart
zontreck 8293ddeb68 QOL: Chores.
Final beta before release 1.0.
2025-05-17 12:36:31 -07:00

431 lines
11 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: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 DateTime EndTime = DateTime(0);
static bool IsOnTheClock = false;
static List<Trip> Trips = [];
static Delivery? currentDelivery;
static Trip? currentTrip;
static List<SmallPosition> positions = [];
static late StreamSubscription<Position> _listener;
static Callbacks Calls = Callbacks();
static String LastSessionID = "";
static String DisplayError = "";
static double? TotalPay;
/// Is true if the try-catch is tripped or if not running on Android
static bool isWeb = false;
/// 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 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 (WGS84 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 GPSnoise cleanup.
///
/// * [minDistanceMeters] drop segments shorter than this (jitter).
/// * [maxDistanceMeters] drop segments longer than this (impossible jump).
static double _totalMilesTraveled(
List<SmallPosition> 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(SmallPosition.fromPosition(pos));
SessionData.Calls.dispatch();
});
}
static Future<void> Logout() async {
IsOnTheClock = false;
currentDelivery = null;
currentTrip = null;
_listener.cancel();
EndTime = DateTime.now();
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>> posx = [];
for (var pos in positions) {
posx.add(pos.toMap());
}
saveData["trips"] = trips;
saveData["positions"] = posx;
saveData["start"] = StartTime.toIso8601String();
saveData["end"] = EndTime.toIso8601String();
if (TotalPay != null) saveData["totalPay"] = TotalPay;
return saveData;
}
static Future<bool> DownloadData() async {
Dio dio = Dio();
Map<String, dynamic> payload = {"cmd": "get", "id": LastSessionID};
// Send the data, and get the response
try {
var reply = await dio.post(
TTConsts.SESSION_SERVER,
data: json.encode(payload),
);
return LoadData(reply.data as Map<String, dynamic>);
} catch (E) {
return false;
}
}
static bool LoadData(Map<String, dynamic> jsMap) {
if (jsMap.containsKey("error")) {
LastSessionID = "";
return false;
}
List<dynamic> trips = jsMap['trips'] as List<dynamic>;
List<dynamic> pos = jsMap['positions'] as List<dynamic>;
for (var trip in trips) {
Trips.add(Trip.fromJsonMap(trip as Map<String, dynamic>));
}
for (var position in pos) {
positions.add(SmallPosition.fromMap(position as Map<String, dynamic>));
}
if (jsMap.containsKey("start")) {
StartTime = DateTime.parse(jsMap['start'] as String);
}
if (jsMap.containsKey("end")) {
EndTime = DateTime.parse(jsMap["end"] as String);
}
if (jsMap.containsKey("totalPay")) {
TotalPay = jsMap["totalPay"] as double;
} else {
TotalPay = null;
}
IsReadOnly = true;
return true;
}
static Future<Position> GetNewLocation() async {
Position pos = await Geolocator.getCurrentPosition(
locationSettings: TTConsts.LOCATION_SETTINGS,
);
return pos;
}
static Trip GetNewTrip() {
currentTrip = Trip();
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;
}
/// [a] should be the Start Time,
///
/// [b] is the end time
static String GetTotalTimeWorked(DateTime a, DateTime b) {
Duration diff = b.difference(a);
int days = diff.inDays;
int hours = diff.inHours.remainder(24);
int minutes = diff.inMinutes.remainder(60);
int seconds = diff.inSeconds.remainder(60);
List<String> parts = [];
if (days > 0) parts.add('${days}d');
if (hours > 0) parts.add('${hours}h');
if (minutes > 0) parts.add('${minutes}m');
if (seconds > 0) parts.add('${seconds}s');
return parts.join(' ');
}
}
class Delivery {
SmallPosition? endLocation;
DateTime StartTime = DateTime.now();
Delivery() {
StartTime = DateTime.now();
}
Future<void> MarkEndLocation() async {
var pos = await SessionData.GetNewLocation();
endLocation = SmallPosition.fromPosition(pos);
}
Map<String, dynamic> toJsonMap() {
return {
"start": StartTime.toString(),
"endPos": endLocation?.toMap() ?? "incomplete",
};
}
static Delivery fromMap(Map<String, dynamic> jsx) {
Delivery delivery = Delivery();
delivery.StartTime = DateTime.parse(jsx['start'] as String);
if (jsx['endPos'] is String) {
delivery.endLocation = null;
} else {
delivery.endLocation = SmallPosition.fromMap(
jsx['endPos'] as Map<String, dynamic>,
);
}
return delivery;
}
}
class Trip {
List<Delivery> deliveries = [];
DateTime StartTime = DateTime(0);
Trip() {
StartTime = DateTime.now();
}
Delivery startNewDelivery() {
var delivery = Delivery();
deliveries.add(delivery);
return delivery;
}
Map<String, dynamic> toJsonMap() {
Map<String, Object> trip = {"start": StartTime.toString()};
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();
trip.StartTime = DateTime.parse(jsx['start'] as String);
trip.deliveries = [];
List<dynamic> dropOffs = jsx['deliveries'] as List<dynamic>;
for (var dropOff in dropOffs) {
trip.deliveries.add(Delivery.fromMap(dropOff as Map<String, dynamic>));
}
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!();
}
}
/// A simple wrapper for a position that strips away unnecessary information to create a more compact piece of save data
class SmallPosition {
double latitude;
double longitude;
SmallPosition({required this.latitude, required this.longitude});
static SmallPosition fromPosition(Position position) {
return SmallPosition(
latitude: position.latitude,
longitude: position.longitude,
);
}
Map<String, dynamic> toMap() {
return {"latitude": latitude, "longitude": longitude};
}
static SmallPosition fromMap(Map<String, dynamic> map) {
return SmallPosition(
latitude: map['latitude'] as double,
longitude: map['longitude'] as double,
);
}
}