import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'dart:async'; import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; /// Model Classes class GpsCoordinate { final double latitude; final double longitude; final double? altitude; final double? accuracy; final DateTime timestamp; GpsCoordinate({ required this.latitude, required this.longitude, this.altitude, this.accuracy, required this.timestamp, }); Map toMap() { return { 'latitude': latitude, 'longitude': longitude, 'altitude': altitude, 'accuracy': accuracy, 'timestamp': timestamp.toIso8601String(), }; } factory GpsCoordinate.fromMap(Map map) { return GpsCoordinate( latitude: map['latitude'] as double, longitude: map['longitude'] as double, altitude: map['altitude'] as double?, accuracy: map['accuracy'] as double?, timestamp: DateTime.parse(map['timestamp'] as String), ); } } class WorkoutSession { final String id; String workoutType; // 'running' or 'walking' DateTime? startTime; DateTime? endTime; List coordinates; bool isPaused; int totalPausedDuration; // in milliseconds WorkoutSession({ required this.id, required this.workoutType, this.startTime, this.endTime, List? coordinates, this.isPaused = false, this.totalPausedDuration = 0, }) : coordinates = coordinates ?? []; int get duration { if (startTime == null || endTime == null) return 0; int activeDuration = endTime!.difference(startTime!).inSeconds - (totalPausedDuration ~/ 1000); return activeDuration > 0 ? activeDuration : 0; } double getDistance() { double totalDistance = 0.0; for (int i = 0; i < coordinates.length - 1; i++) { totalDistance += _haversineDistance( coordinates[i].latitude, coordinates[i].longitude, coordinates[i + 1].latitude, coordinates[i + 1].longitude, ); } return totalDistance; } static double _haversineDistance( double lat1, double lng1, double lat2, double lng2) { const R = 6371000; // Earth's radius in meters final dLat = _toRad(lat2 - lat1); final dLng = _toRad(lng2 - lng1); final a = sin(dLat / 2) * sin(dLat / 2) + cos(_toRad(lat1)) * cos(_toRad(lat2)) * sin(dLng / 2) * sin(dLng / 2); final c = 2 * atan2(sqrt(a), sqrt(1 - a)); return R * c; } static double _toRad(double degree) => degree * pi / 180; Map toMap() { return { 'id': id, 'workoutType': workoutType, 'startTime': startTime?.toIso8601String(), 'endTime': endTime?.toIso8601String(), 'coordinates': jsonEncode(coordinates.map((c) => c.toMap()).toList()), 'isPaused': isPaused ? 1 : 0, 'totalPausedDuration': totalPausedDuration, }; } } /// Location Service - Handles background GPS tracking class LocationService extends GetxService { late Database _db; final _dbPath = 'fitness_tracking.db'; final tableName = 'gps_coordinates'; @override Future onInit() async { super.onInit(); await _initializeDatabase(); } Future _initializeDatabase() async { try { final dbPath = join(await getDatabasesPath(), _dbPath); _db = await openDatabase( dbPath, version: 1, onCreate: (db, version) async { await db.execute(''' CREATE TABLE $tableName ( id INTEGER PRIMARY KEY AUTOINCREMENT, workout_session_id TEXT NOT NULL, latitude REAL NOT NULL, longitude REAL NOT NULL, altitude REAL, accuracy REAL, timestamp TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) '''); // Create index for faster queries await db.execute(''' CREATE INDEX idx_workout_session_id ON $tableName(workout_session_id) '''); }, ); } catch (e) { debugPrint('Database initialization error: $e'); rethrow; } } Future saveCoordinate( String workoutSessionId, GpsCoordinate coordinate) async { try { await _db.insert( tableName, { 'workout_session_id': workoutSessionId, 'latitude': coordinate.latitude, 'longitude': coordinate.longitude, 'altitude': coordinate.altitude, 'accuracy': coordinate.accuracy, 'timestamp': coordinate.timestamp.toIso8601String(), }, conflictAlgorithm: ConflictAlgorithm.ignore, ); } catch (e) { debugPrint('Error saving coordinate: $e'); } } Future> getCoordinates(String workoutSessionId) async { try { final result = await _db.query( tableName, where: 'workout_session_id = ?', whereArgs: [workoutSessionId], orderBy: 'timestamp ASC', ); return result .map((map) => GpsCoordinate( latitude: map['latitude'] as double, longitude: map['longitude'] as double, altitude: map['altitude'] as double?, accuracy: map['accuracy'] as double?, timestamp: DateTime.parse(map['timestamp'] as String), )) .toList(); } catch (e) { debugPrint('Error retrieving coordinates: $e'); return []; } } Future clearCoordinates(String workoutSessionId) async { try { await _db.delete( tableName, where: 'workout_session_id = ?', whereArgs: [workoutSessionId], ); } catch (e) { debugPrint('Error clearing coordinates: $e'); } } Future getCoordinateCount(String workoutSessionId) async { try { final result = await _db.rawQuery( 'SELECT COUNT(*) as count FROM $tableName WHERE workout_session_id = ?', [workoutSessionId], ); return Sqflite.firstIntValue(result) ?? 0; } catch (e) { debugPrint('Error getting coordinate count: $e'); return 0; } } @override void onClose() { _db.close(); super.onClose(); } } /// Polyline Encoding/Decoding Utility class PolylineUtility { static String encodePolyline(List coordinates) { String encoded = ''; List previous = [0, 0]; for (var point in coordinates) { List current = [ (point.latitude * 1e5).toInt(), (point.longitude * 1e5).toInt(), ]; for (int j = 0; j < 2; j++) { int curr = current[j]; int prev = previous[j]; int value = curr - prev; value = value < 0 ? ~(value << 1) : (value << 1); while (value >= 0x20) { int chunk = (0x20 | (value & 0x1f)) + 63; encoded += String.fromCharCode(chunk); value >>= 5; } encoded += String.fromCharCode(value + 63); previous[j] = current[j]; } } return encoded; } } /// HMAC Signature Generation class HmacSignatureGenerator { static String generateSignature( String apiKey, String apiSecret, String payload, int timestamp, ) { final message = '$timestamp|$apiKey|$payload'; final hmac = Hmac(sha256, utf8.encode(apiSecret)); final digest = hmac.convert(utf8.encode(message)); return digest.toString(); } } /// Main Workout Controller class WorkoutController extends GetxController { final LocationService locationService = Get.put(LocationService()); late WorkoutSession _currentWorkout; final _isTracking = false.obs; final _isPaused = false.obs; final _elapsedTime = 0.obs; final _coordinates = [].obs; final _distance = 0.0.obs; final _currentSpeed = 0.0.obs; final _maxSpeed = 0.0.obs; final _isSubmitting = false.obs; final _apiKey = ''.obs; final _apiSecret = ''.obs; // Observables bool get isTracking => _isTracking.value; bool get isPaused => _isPaused.value; int get elapsedTime => _elapsedTime.value; List get coordinates => _coordinates; double get distance => _distance.value; double get currentSpeed => _currentSpeed.value; double get maxSpeed => _maxSpeed.value; bool get isSubmitting => _isSubmitting.value; WorkoutSession get currentWorkout => _currentWorkout; Timer? _timerTracking; Timer? _timerUpdate; @override void onInit() { super.onInit(); _initializeWorkout(); } void _initializeWorkout() { _currentWorkout = WorkoutSession( id: DateTime.now().millisecondsSinceEpoch.toString(), workoutType: 'running', ); } void setWorkoutType(String type) { if (type == 'running' || type == 'walking') { _currentWorkout.workoutType = type; } } void setApiCredentials(String apiKey, String apiSecret) { _apiKey.value = apiKey; _apiSecret.value = apiSecret; } Future startWorkout() async { if (_isTracking.value) return; _currentWorkout.startTime = DateTime.now(); _currentWorkout.isPaused = false; _currentWorkout.totalPausedDuration = 0; _isTracking.value = true; _isPaused.value = false; _coordinates.clear(); _distance.value = 0.0; _elapsedTime.value = 0; // Start elapsed time timer (updates every second) _timerTracking = Timer.periodic(Duration(seconds: 1), (_) { if (!_isPaused.value && _currentWorkout.startTime != null) { final now = DateTime.now(); int activeDuration = now.difference(_currentWorkout.startTime!).inSeconds - (_currentWorkout.totalPausedDuration ~/ 1000); _elapsedTime.value = activeDuration > 0 ? activeDuration : 0; } }); // Start position update timer (every 5 seconds or based on distance) _timerUpdate = Timer.periodic(Duration(seconds: 5), (_) async { if (!_isPaused.value) { await _updateLocation(); } }); } Future pauseWorkout() async { if (!_isTracking.value || _isPaused.value) return; _isPaused.value = true; _currentWorkout.isPaused = true; _currentWorkout.endTime = DateTime.now(); // temporary end for pause } Future resumeWorkout() async { if (!_isTracking.value || !_isPaused.value) return; _isPaused.value = false; _currentWorkout.isPaused = false; // Add pause duration to total if (_currentWorkout.endTime != null) { _currentWorkout.totalPausedDuration += DateTime.now().difference(_currentWorkout.endTime!).inMilliseconds; } } Future stopWorkout() async { if (!_isTracking.value) return; _isTracking.value = false; _isPaused.value = false; _currentWorkout.endTime = DateTime.now(); // Cancel timers _timerTracking?.cancel(); _timerUpdate?.cancel(); // Fetch all stored coordinates from SQLite _coordinates.value = await locationService.getCoordinates(_currentWorkout.id); // Update distance calculation _distance.value = _currentWorkout.getDistance(); // Prepare for submission } Future _updateLocation() async { // In production, get location from location_service package // This is a placeholder for the actual GPS tracking try { // Simulated coordinate update final coordinate = GpsCoordinate( latitude: 37.7749 + (DateTime.now().microsecond % 1000) / 100000, longitude: -122.4194 + (DateTime.now().microsecond % 1000) / 100000, altitude: 50.0, accuracy: 5.0, timestamp: DateTime.now(), ); await locationService.saveCoordinate(_currentWorkout.id, coordinate); _coordinates.add(coordinate); // Update distance if (_coordinates.length > 1) { _distance.value = _currentWorkout.getDistance(); } // Update current speed (simplified) if (_coordinates.length > 1) { final lastCoord = _coordinates[_coordinates.length - 2]; final distance = _haversineDistance( lastCoord.latitude, lastCoord.longitude, coordinate.latitude, coordinate.longitude, ); final timeDiff = coordinate.timestamp.difference(lastCoord.timestamp).inSeconds; if (timeDiff > 0) { _currentSpeed.value = distance / timeDiff; if (_currentSpeed.value > _maxSpeed.value) { _maxSpeed.value = _currentSpeed.value; } } } } catch (e) { debugPrint('Error updating location: $e'); } } static double _haversineDistance( double lat1, double lng1, double lat2, double lng2) { const R = 6371000; // Earth's radius in meters final dLat = _toRad(lat2 - lat1); final dLng = _toRad(lng2 - lng1); final a = sin(dLat / 2) * sin(dLat / 2) + cos(_toRad(lat1)) * cos(_toRad(lat2)) * sin(dLng / 2) * sin(dLng / 2); final c = 2 * atan2(sqrt(a), sqrt(1 - a)); return R * c; } static double _toRad(double degree) => degree * pi / 180; Future> prepareWorkoutPayload() async { final polyline = PolylineUtility.encodePolyline(_coordinates); final distance = _distance.value.toInt(); final duration = _elapsedTime.value; return { 'workout_type': _currentWorkout.workoutType, 'distance_meters': distance, 'duration_seconds': duration, 'elevation_gain_meters': 0, // TODO: Calculate from coordinates 'elevation_loss_meters': 0, // TODO: Calculate from coordinates 'calories_burned': _estimateCalories(distance, duration, _currentWorkout.workoutType), 'max_speed_mps': _maxSpeed.value, 'route_polyline': polyline, 'start_time': _currentWorkout.startTime?.toUtc().toIso8601String() ?? '', 'end_time': _currentWorkout.endTime?.toUtc().toIso8601String() ?? '', 'weather_condition': 'sunny', 'temperature_celsius': 22.0, 'notes': '', 'is_public': false, 'segments': [], }; } double _estimateCalories(int distance, int duration, String workoutType) { // Simplified calorie calculation // For a 70kg person, approximately: // Running: 100 calories per 1km // Walking: 50 calories per 1km double caloriesPerKm = workoutType == 'running' ? 100 : 50; return (distance / 1000) * caloriesPerKm; } Future submitWorkout(String apiEndpoint) async { if (_isSubmitting.value) return false; _isSubmitting.value = true; try { final payload = await prepareWorkoutPayload(); final payloadJson = jsonEncode(payload); // Save workout locally to SQLite debugPrint('Saving workout locally: $payloadJson'); // Store the completed workout in local database // In a real app, you might create a "completed_workouts" table await locationService.saveCoordinate( _currentWorkout.id, GpsCoordinate( latitude: 0, longitude: 0, timestamp: DateTime.now(), ), ); // Simulate successful storage await Future.delayed(Duration(seconds: 1)); // Initialize new workout session _initializeWorkout(); return true; } catch (e) { debugPrint('Error saving workout: $e'); return false; } finally { _isSubmitting.value = false; } } @override void onClose() { _timerTracking?.cancel(); _timerUpdate?.cancel(); super.onClose(); } } // Formatting Utilities String formatTime(int seconds) { int hours = seconds ~/ 3600; int minutes = (seconds % 3600) ~/ 60; int secs = seconds % 60; String twoDigits(int n) => n.toString().padLeft(2, '0'); if (hours > 0) { return '${twoDigits(hours)}:${twoDigits(minutes)}:${twoDigits(secs)}'; } return '${twoDigits(minutes)}:${twoDigits(secs)}'; } String formatDistance(double meters) { if (meters >= 1000) { return '${(meters / 1000).toStringAsFixed(2)} km'; } return '${meters.toStringAsFixed(0)} m'; } String formatPace(double metersPerSecond) { if (metersPerSecond == 0) return '--:-- min/km'; double kmPerHour = metersPerSecond * 3.6; double pacePerKm = 60 / kmPerHour; int minutes = pacePerKm.toInt(); int seconds = ((pacePerKm - minutes) * 60).toInt(); return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')} min/km'; }