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

267 lines
6.6 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// lib/utils/fit_activity_parser.dart
import 'dart:typed_data';
import 'package:aves/model/activity/activity_track.dart';
import 'package:fit_sdk/fit_sdk.dart' as fit;
import 'package:flutter/foundation.dart';
import 'package:latlong2/latlong.dart';
import 'gps_smoothing.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;
bool isZepp = false;
final decoder = fit.Decode();
decoder.onMesg = (dynamic message) {
final messageName = '${message.name}'.toLowerCase();
final fields = <String, dynamic>{};
for (final dynamic field in message.fields) {
fields[field.name.toLowerCase()] = field.value;
}
// Detect Zepp device
if (fields['manufacturer'] == 25 ||
fields['product'] == 25 ||
fields['serial_number']?.toString().contains('zepp') == true) {
isZepp = true;
}
if (messageName == 'record') {
final latRaw = _readNumber(fields, const [
'position_lat',
'positionlat',
'lat',
'latitude',
]);
final lonRaw = _readNumber(fields, const [
'position_long',
'positionlong',
'lon',
'longitude',
]);
if (latRaw == null || lonRaw == null) return;
final lat = _toDegrees(latRaw);
final lon = _toDegrees(lonRaw);
if (!lat.isFinite || !lon.isFinite) return;
if (lat.abs() > 90 || lon.abs() > 180) return;
final elevation = _readNumber(fields, const [
'enhanced_altitude',
'altitude',
]);
final time = _readDateTime(fields, const [
'timestamp',
'time',
]);
records.add(
_FitPoint(
latLng: LatLng(lat, lon),
elevationMeters: elevation,
time: time,
rawFields: fields,
),
);
}
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((p) => p.latLng.latitude.isFinite && p.latLng.longitude.isFinite)
.toList();
// Build track
List<LatLng> track = points.map((p) => p.latLng).toList();
// Apply smoothing ONLY for Zepp
if (isZepp) {
track = GpsSmoothing.douglasPeucker(track, 5.0);
}
final activity = ActivityTrack(
name: fileName,
format: 'FIT',
tracks: {track},
pointCount: track.length,
distanceMeters: sessionDistanceMeters ?? _computeDistance(track),
elevationGainMeters: _computeElevation(points),
duration: sessionDuration ?? _computeDuration(points),
startTime: points.map((p) => p.time).nonNulls.firstOrNull,
sport: sport,
);
return activity;
}
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();
if (value is String) return double.tryParse(value);
}
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 && value > 0) {
return DateTime.fromMillisecondsSinceEpoch(
(value + 631065600) * 1000,
isUtc: true,
);
}
if (value is String) {
final parsed = DateTime.tryParse(value);
if (parsed != null) return parsed.toUtc();
}
}
return null;
}
static double _toDegrees(num raw) {
final v = raw.toDouble();
if (v.abs() <= 180) return v;
return v * 180.0 / 2147483648.0;
}
static double? _computeDistance(List<LatLng> track) {
if (track.length < 2) return null;
double dist = 0;
for (int i = 1; i < track.length; i++) {
final seg = _distance.as(LengthUnit.Meter, track[i - 1], track[i]);
if (seg < 0 || seg > 50) continue; // antirumore Zepp
dist += seg;
}
return dist;
}
static double? _computeElevation(List<_FitPoint> points) {
double gain = 0;
double? prev;
for (final p in points) {
final e = p.elevationMeters;
if (e == null) continue;
if (prev != null) {
final delta = e - prev;
if (delta > 2) gain += delta; // soglia Zepp
}
prev = e;
}
return gain;
}
static Duration? _computeDuration(List<_FitPoint> points) {
final times = points.map((p) => p.time).nonNulls.toList();
if (times.length < 2) return null;
times.sort();
return times.last.difference(times.first);
}
static String _normalizeSport(String raw) {
final value = raw.trim().toLowerCase();
final numeric = int.tryParse(value);
if (numeric != null) {
switch (numeric) {
case 1: return 'Corsa';
case 2: return 'Bici';
case 5: return 'Nuoto';
case 6: return 'Camminata';
case 9: return 'Trekking';
case 20: return 'Mountain Bike';
default: return 'Sport $numeric';
}
}
switch (value) {
case 'running': return 'Corsa';
case 'cycling': return 'Bici';
case 'walking': return 'Camminata';
case 'hiking': return 'Trekking';
case 'swimming': return 'Nuoto';
default: return raw.trim();
}
}
}
class _FitPoint {
final LatLng latLng;
final double? elevationMeters;
final DateTime? time;
final Map<String, dynamic> rawFields;
const _FitPoint({
required this.latLng,
this.elevationMeters,
this.time,
required this.rawFields,
});
}
extension _FirstOrNull<E> on Iterable<E> {
E? get firstOrNull {
final it = iterator;
if (!it.moveNext()) return null;
return it.current;
}
}