// lib/model/activity/activity_track.dart import 'package:latlong2/latlong.dart'; class ActivityTrack { final String name; final String format; final Set> tracks; /// Numero punti validi letti dal file prima di eventuale semplificazione grafica. final int pointCount; final double? distanceMeters; final double? elevationGainMeters; final Duration? duration; final DateTime? startTime; final String? sport; final String? diagnostics; const ActivityTrack({ required this.name, required this.format, required this.tracks, required this.pointCount, this.distanceMeters, this.elevationGainMeters, this.duration, this.startTime, this.sport, this.diagnostics, }); bool get isEmpty => tracks.isEmpty || tracks.every((segment) => segment.isEmpty); Iterable get points sync* { for (final segment in tracks) { yield* segment; } } List get validPoints { return points .where( (point) => point.latitude.isFinite && point.longitude.isFinite && point.latitude.abs() <= 90 && point.longitude.abs() <= 180, ) .toList(growable: false); } int get displayPointCount => validPoints.length; LatLng? get center { final allPoints = validPoints; if (allPoints.isEmpty) return null; final latitude = allPoints.fold( 0, (sum, point) => sum + point.latitude, ) / allPoints.length; final longitude = allPoints.fold( 0, (sum, point) => sum + point.longitude, ) / allPoints.length; return LatLng(latitude, longitude); } double? get minLatitude { final allPoints = validPoints; if (allPoints.isEmpty) return null; return allPoints .map((point) => point.latitude) .reduce((a, b) => a < b ? a : b); } double? get maxLatitude { final allPoints = validPoints; if (allPoints.isEmpty) return null; return allPoints .map((point) => point.latitude) .reduce((a, b) => a > b ? a : b); } double? get minLongitude { final allPoints = validPoints; if (allPoints.isEmpty) return null; return allPoints .map((point) => point.longitude) .reduce((a, b) => a < b ? a : b); } double? get maxLongitude { final allPoints = validPoints; if (allPoints.isEmpty) return null; return allPoints .map((point) => point.longitude) .reduce((a, b) => a > b ? a : b); } bool get hasBounds => minLatitude != null && maxLatitude != null && minLongitude != null && maxLongitude != null; /// Angolo sud-ovest del rettangolo che contiene tutta la traccia. LatLng? get southWest { final south = minLatitude; final west = minLongitude; if (south == null || west == null) return null; return LatLng(south, west); } /// Angolo nord-est del rettangolo che contiene tutta la traccia. LatLng? get northEast { final north = maxLatitude; final east = maxLongitude; if (north == null || east == null) return null; return LatLng(north, east); } /// Centro geometrico del bounding box. /// /// A differenza di [center], non dipende dalla densita dei punti. Per la mappa /// spesso e' piu stabile per tracce con pause o punti molto concentrati. LatLng? get boundsCenter { final south = minLatitude; final north = maxLatitude; final west = minLongitude; final east = maxLongitude; if (south == null || north == null || west == null || east == null) { return null; } return LatLng( (south + north) / 2, (west + east) / 2, ); } String get distanceLabel { final value = distanceMeters; if (value == null) return '- km'; return '${(value / 1000).toStringAsFixed(2)} km'; } String get elevationGainLabel { final value = elevationGainMeters; if (value == null) return 'D+ -'; return 'D+ ${value.round()} m'; } String get durationLabel { final value = duration; if (value == null) return '-'; final hours = value.inHours; final minutes = value.inMinutes.remainder(60); final seconds = value.inSeconds.remainder(60); if (hours > 0) { return '${hours}h ${minutes}m'; } if (minutes > 0) { return '${minutes}m ${seconds}s'; } return '${seconds}s'; } String get sportLabel { final value = sport; if (value == null || value.trim().isEmpty) return 'Attività'; return value; } }