import 'gps_coordinate.dart'; import 'workout_segment.dart'; /// حالات المزامنة enum SyncStatus { pending, synced, failed } /// أنواع التمارين enum WorkoutType { running, walking } /// نموذج بيانات التمرين الكامل /// يحتوي على كل المعلومات المتعلقة بجلسة تمرين واحدة class Workout { final String id; WorkoutType workoutType; DateTime? startTime; DateTime? endTime; double distanceMeters; int durationSeconds; double elevationGain; double elevationLoss; double calories; double avgSpeedMps; double maxSpeedMps; double avgPaceMinKm; String? routePolyline; double? startLat; double? startLng; double? endLat; double? endLng; String? weather; double? temperature; String notes; SyncStatus syncStatus; DateTime? syncedAt; DateTime createdAt; // In-memory only (not persisted directly) List coordinates; List segments; // Tracking state (in-memory only) bool isPaused; int totalPausedDurationMs; DateTime? _pauseStartTime; Workout({ required this.id, this.workoutType = WorkoutType.running, this.startTime, this.endTime, this.distanceMeters = 0, this.durationSeconds = 0, this.elevationGain = 0, this.elevationLoss = 0, this.calories = 0, this.avgSpeedMps = 0, this.maxSpeedMps = 0, this.avgPaceMinKm = 0, this.routePolyline, this.startLat, this.startLng, this.endLat, this.endLng, this.weather, this.temperature, this.notes = '', this.syncStatus = SyncStatus.pending, this.syncedAt, DateTime? createdAt, List? coordinates, List? segments, this.isPaused = false, this.totalPausedDurationMs = 0, }) : createdAt = createdAt ?? DateTime.now(), coordinates = coordinates ?? [], segments = segments ?? []; // ─── Tracking Lifecycle ────────────────────────────────────────── void start() { startTime = DateTime.now(); isPaused = false; totalPausedDurationMs = 0; _pauseStartTime = null; } void pause() { if (!isPaused) { isPaused = true; _pauseStartTime = DateTime.now(); } } void resume() { if (isPaused && _pauseStartTime != null) { totalPausedDurationMs += DateTime.now().difference(_pauseStartTime!).inMilliseconds; isPaused = false; _pauseStartTime = null; } } void stop() { if (isPaused) resume(); // Account for final pause endTime = DateTime.now(); isPaused = false; _calculateFinalStats(); } /// Calculate active duration excluding pauses int get activeDurationSeconds { if (startTime == null) return 0; final end = endTime ?? DateTime.now(); int total = end.difference(startTime!).inSeconds; int paused = totalPausedDurationMs ~/ 1000; return (total - paused).clamp(0, total); } // ─── Stats Calculation ─────────────────────────────────────────── void _calculateFinalStats() { durationSeconds = activeDurationSeconds; if (durationSeconds > 0 && distanceMeters > 0) { avgSpeedMps = distanceMeters / durationSeconds; // Pace in min/km double kmPerHour = avgSpeedMps * 3.6; if (kmPerHour > 0) { avgPaceMinKm = 60.0 / kmPerHour; } } // Calories estimation (MET-based for 70kg person) calories = _estimateCalories(); // Elevation from coordinates _calculateElevation(); } double _estimateCalories() { // MET values: Running ~10, Walking ~3.8 double met = workoutType == WorkoutType.running ? 10.0 : 3.8; double weightKg = 70.0; // Default weight double hours = durationSeconds / 3600.0; return met * weightKg * hours; } void _calculateElevation() { if (coordinates.length < 2) return; double gain = 0; double loss = 0; for (int i = 1; i < coordinates.length; i++) { final prevAlt = coordinates[i - 1].altitude; final currAlt = coordinates[i].altitude; if (prevAlt != null && currAlt != null) { double diff = currAlt - prevAlt; // Filter noise: ignore changes < 1m if (diff > 1.0) { gain += diff; } else if (diff < -1.0) { loss += diff.abs(); } } } elevationGain = gain; elevationLoss = loss; } // ─── Serialization ────────────────────────────────────────────── Map toMap() { return { 'id': id, 'workout_type': workoutType.name, 'start_time': startTime?.toIso8601String(), 'end_time': endTime?.toIso8601String(), 'distance_meters': distanceMeters, 'duration_seconds': durationSeconds, 'elevation_gain': elevationGain, 'elevation_loss': elevationLoss, 'calories': calories, 'avg_speed_mps': avgSpeedMps, 'max_speed_mps': maxSpeedMps, 'avg_pace_min_km': avgPaceMinKm, 'route_polyline': routePolyline, 'start_lat': startLat, 'start_lng': startLng, 'end_lat': endLat, 'end_lng': endLng, 'weather': weather, 'temperature': temperature, 'notes': notes, 'sync_status': syncStatus.name, 'synced_at': syncedAt?.toIso8601String(), 'created_at': createdAt.toIso8601String(), }; } factory Workout.fromMap(Map map) { return Workout( id: map['id'] as String, workoutType: WorkoutType.values.firstWhere( (e) => e.name == map['workout_type'], orElse: () => WorkoutType.running, ), startTime: map['start_time'] != null ? DateTime.parse(map['start_time'] as String) : null, endTime: map['end_time'] != null ? DateTime.parse(map['end_time'] as String) : null, distanceMeters: (map['distance_meters'] as num?)?.toDouble() ?? 0, durationSeconds: (map['duration_seconds'] as num?)?.toInt() ?? 0, elevationGain: (map['elevation_gain'] as num?)?.toDouble() ?? 0, elevationLoss: (map['elevation_loss'] as num?)?.toDouble() ?? 0, calories: (map['calories'] as num?)?.toDouble() ?? 0, avgSpeedMps: (map['avg_speed_mps'] as num?)?.toDouble() ?? 0, maxSpeedMps: (map['max_speed_mps'] as num?)?.toDouble() ?? 0, avgPaceMinKm: (map['avg_pace_min_km'] as num?)?.toDouble() ?? 0, routePolyline: map['route_polyline'] as String?, startLat: (map['start_lat'] as num?)?.toDouble(), startLng: (map['start_lng'] as num?)?.toDouble(), endLat: (map['end_lat'] as num?)?.toDouble(), endLng: (map['end_lng'] as num?)?.toDouble(), weather: map['weather'] as String?, temperature: (map['temperature'] as num?)?.toDouble(), notes: map['notes'] as String? ?? '', syncStatus: SyncStatus.values.firstWhere( (e) => e.name == map['sync_status'], orElse: () => SyncStatus.pending, ), syncedAt: map['synced_at'] != null ? DateTime.parse(map['synced_at'] as String) : null, createdAt: map['created_at'] != null ? DateTime.parse(map['created_at'] as String) : DateTime.now(), ); } /// Convert to API payload for server submission Map toApiPayload() { return { 'client_workout_id': id, 'workout_type': workoutType.name, 'distance_meters': distanceMeters.toInt(), 'duration_seconds': durationSeconds, 'elevation_gain_meters': elevationGain.toInt(), 'elevation_loss_meters': elevationLoss.toInt(), 'calories_burned': calories, 'max_speed_mps': maxSpeedMps, 'route_polyline': routePolyline ?? '', 'start_time': startTime?.toUtc().toIso8601String() ?? '', 'end_time': endTime?.toUtc().toIso8601String() ?? '', 'weather_condition': weather ?? '', 'temperature_celsius': temperature ?? 0, 'notes': notes, 'is_public': false, 'segments': segments.map((s) => s.toApiMap()).toList(), }; } }