aves_mio0.31/lib/utils/fit_activity_parser.dart.old
2026-07-18 13:39:22 +02:00

274 lines
6.4 KiB
Dart

import 'dart:typed_data';
import 'package:aves/model/activity/activity_track.dart';
import 'package:fit_sdk/fit_sdk.dart' as fit;
import 'package:latlong2/latlong.dart';
class FitActivityParser {
static final Distance _distance = Distance();
static ActivityTrack parse(
Uint8List bytes, {
required String fileName,
}) {
final records = <_FitPoint>[];
double? sessionDistanceMeters;
Duration? sessionDuration;
String? sport;
final decoder = fit.Decode();
decoder.onMesg = (dynamic message) {
final messageName = '${message.name}'.toLowerCase();
final fields = <String, dynamic>{};
for (final dynamic field in message.fields) {
final name = '${field.name}'.toLowerCase();
fields[name] = field.value;
}
if (messageName == 'record') {
final latRaw = _readNumber(fields, const [
'position_lat',
'positionlat',
'lat',
'latitude',
]);
final lonRaw = _readNumber(fields, const [
'position_long',
'positionlon',
'lon',
'long',
'longitude',
]);
if (latRaw == null || lonRaw == null) return;
final lat = _semicirclesToDegrees(latRaw);
final lon = _semicirclesToDegrees(lonRaw);
if (!lat.isFinite || !lon.isFinite) return;
if (lat.abs() > 90 || lon.abs() > 180) return;
final elevation = _readNumber(fields, const [
'altitude',
'enhanced_altitude',
]);
final time = _readDateTime(fields, const [
'timestamp',
'time',
]);
records.add(
_FitPoint(
latLng: LatLng(lat, lon),
elevationMeters: elevation,
time: time,
),
);
}
if (messageName == 'session') {
sessionDistanceMeters ??= _readNumber(fields, const [
'total_distance',
'distance',
]);
final elapsedSeconds = _readNumber(fields, const [
'total_elapsed_time',
'total_timer_time',
]);
if (elapsedSeconds != null) {
sessionDuration = Duration(seconds: elapsedSeconds.round());
}
final sessionSport = fields['sport'];
if (sessionSport != null) {
sport = _normalizeSport('$sessionSport');
}
}
if (messageName == 'sport') {
final sportValue = fields['sport'];
if (sportValue != null) {
sport = _normalizeSport('$sportValue');
}
}
};
decoder.read(bytes);
final points = records
.where(
(point) =>
point.latLng.latitude.isFinite &&
point.latLng.longitude.isFinite,
)
.toList();
final tracks = <List<LatLng>>{};
if (points.length >= 2) {
tracks.add(points.map((point) => point.latLng).toList());
}
return ActivityTrack(
name: fileName,
format: 'FIT',
tracks: tracks,
pointCount: points.length,
distanceMeters: sessionDistanceMeters ?? _computeDistanceMeters(points),
elevationGainMeters: _computeElevationGainMeters(points),
duration: sessionDuration ?? _computeDuration(points),
startTime: points.map((point) => point.time).nonNulls.firstOrNull,
sport: sport,
);
}
static double? _readNumber(
Map<String, dynamic> fields,
List<String> names,
) {
for (final name in names) {
final value = fields[name];
if (value == null) continue;
if (value is num) {
return value.toDouble();
}
final parsed = double.tryParse('$value');
if (parsed != null && parsed.isFinite) {
return parsed;
}
}
return null;
}
static DateTime? _readDateTime(
Map<String, dynamic> fields,
List<String> names,
) {
for (final name in names) {
final value = fields[name];
if (value == null) continue;
if (value is DateTime) return value;
if (value is int) {
// FIT timestamp = seconds since 1989-12-31 UTC.
return DateTime.fromMillisecondsSinceEpoch(
(value + 631065600) * 1000,
isUtc: true,
);
}
final parsed = DateTime.tryParse('$value');
if (parsed != null) return parsed;
}
return null;
}
static double _semicirclesToDegrees(num value) {
return value.toDouble() * 180.0 / 2147483648.0;
}
static double? _computeDistanceMeters(List<_FitPoint> points) {
if (points.length < 2) return null;
var distanceMeters = 0.0;
for (var i = 1; i < points.length; i++) {
distanceMeters += _distance.as(
LengthUnit.Meter,
points[i - 1].latLng,
points[i].latLng,
);
}
return distanceMeters > 0 ? distanceMeters : null;
}
static double? _computeElevationGainMeters(List<_FitPoint> points) {
var gain = 0.0;
double? previousElevation;
for (final point in points) {
final elevation = point.elevationMeters;
if (elevation == null) continue;
final previous = previousElevation;
if (previous != null) {
final delta = elevation - previous;
if (delta > 0) {
gain += delta;
}
}
previousElevation = elevation;
}
return gain > 0 ? gain : null;
}
static Duration? _computeDuration(List<_FitPoint> points) {
final times = points.map((point) => point.time).nonNulls.toList();
if (times.length < 2) return null;
times.sort();
final duration = times.last.difference(times.first);
return duration.isNegative ? null : duration;
}
static String _normalizeSport(String raw) {
final value = raw.trim().toLowerCase();
switch (value) {
case 'running':
case 'run':
return 'Corsa';
case 'cycling':
case 'biking':
case 'bike':
return 'Bici';
case 'walking':
case 'walk':
return 'Camminata';
case 'hiking':
case 'trekking':
return 'Trekking';
case 'swimming':
return 'Nuoto';
default:
return raw.trim();
}
}
}
class _FitPoint {
final LatLng latLng;
final double? elevationMeters;
final DateTime? time;
const _FitPoint({
required this.latLng,
this.elevationMeters,
this.time,
});
}
extension _FirstOrNull<E> on Iterable<E> {
E? get firstOrNull {
final iterator = this.iterator;
if (!iterator.moveNext()) return null;
return iterator.current;
}
}