first commit
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
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<GpsCoordinate> coordinates;
|
||||
List<WorkoutSegment> 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<GpsCoordinate>? coordinates,
|
||||
List<WorkoutSegment>? 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> toApiPayload() {
|
||||
return {
|
||||
'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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user