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 Trips = []; static Delivery? currentDelivery; static Trip? currentTrip; static List positions = []; static late StreamSubscription _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 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 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 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 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 Logout() async { IsOnTheClock = false; currentDelivery = null; currentTrip = null; _listener.cancel(); var saveData = SaveData(); print(saveData); Trips = []; positions = []; Dio dio = Dio(); Map payload = {"cmd": "create", "data": saveData}; var reply = await dio.post( TTConsts.SESSION_SERVER, data: json.encode(payload), ); Map replyJs = json.decode(reply.data as String); if (replyJs["status"] == "ok") { print("Successful upload"); LastSessionID = replyJs['session'] as String; Calls.dispatch(); } } static Map SaveData() { Map saveData = {}; List> _trips = []; for (var trip in Trips) { _trips.add(trip.toJsonMap()); } List> _pos = []; for (var pos in positions) { _pos.add(pos.toMap()); } saveData["trips"] = _trips; saveData["positions"] = _pos; return saveData; } static Future DownloadData() async { Dio dio = Dio(); Map 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 _js = json.decode(js); if (_js.containsKey("error")) { LastSessionID = ""; return; } List> _trips = _js['trips'] as List>; List> _pos = _js['positions'] as List>; for (var trip in _trips) { Trips.add(Trip.fromJsonMap(trip)); } for (var position in _pos) { positions.add(SmallPosition.fromMap(position)); } IsReadOnly = true; } static Future 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; } } class Delivery { Position? endLocation; DateTime StartTime = DateTime.now(); Delivery() { StartTime = DateTime.now(); } Future MarkEndLocation() async { var pos = await SessionData.GetNewLocation(); endLocation = pos; } Map toJsonMap() { return { "start": StartTime.toString(), "endPos": endLocation?.toJson() ?? "incomplete", }; } static Delivery fromMap(Map jsx) { Delivery delivery = Delivery(); delivery.StartTime = DateTime.parse(jsx['start'] as String); if (jsx['endPos'] as String == "incomplete") delivery.endLocation = null; else delivery.endLocation = Position.fromMap(jsx['endPos']); return delivery; } } class Trip { List deliveries = []; DateTime StartTime = DateTime(0); Trip() { StartTime = DateTime.now(); } Delivery startNewDelivery() { var delivery = Delivery(); deliveries.add(delivery); return delivery; } Map toJsonMap() { Map _trip = {"start": StartTime.toString()}; List> _dropOffs = []; for (var delivery in deliveries) { _dropOffs.add(delivery.toJsonMap()); } _trip["deliveries"] = _dropOffs; return _trip; } static Trip fromJsonMap(Map jsx) { Trip trip = Trip(); trip.StartTime = DateTime.parse(jsx['start'] as String); trip.deliveries = []; List> _dropOffs = jsx['deliveries'] as List>; 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!(); } } /// 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 toMap() { return {"latitude": latitude, "longitude": longitude}; } static SmallPosition fromMap(Map map) { return SmallPosition( latitude: map['latitude'] as double, longitude: map['longitude'] as double, ); } }