activity_files 0.1.2
activity_files: ^0.1.2 copied to clipboard
A pure Dart toolkit for parsing, editing, and encoding workout activity files such as GPX, TCX, and FIT.
activity_files #
Licensed under the BSD 3-Clause License. See LICENSE for details.
A pure Dart toolkit for reading, editing, validating, and writing workout
activity files. activity_files provides format-agnostic models, robust GPX and
TCX parsers/encoders, transformation utilities, and a CLI for quick conversions
or validation.
FIT payloads can be handled as raw bytes or base64 strings; the library now provides both string- and byte-oriented APIs.
Highlights #
- Unified
RawActivitymodel with geographic samples, sensor channels, laps, and sport metadata. - Namespace-tolerant GPX/TCX parsers that surface non-fatal schema issues as warnings instead of throwing.
- Channel-aware encoders with configurable matching tolerances and numeric
precision via
EncoderOptions. - Immutable editing pipeline (
RawEditor,RawTransforms) for cropping, resampling, smoothing, and derived metrics. - Structural validation helpers producing concise error/warning reports.
- Optional CLI (
bin/activity_files.dart) for converting files or running validations from the terminal.
Getting started #
Add the package to pubspec.yaml:
dependencies:
activity_files: ^0.1.2
Then install dependencies:
dart pub get
See example/main.dart for a minimal round-trip through the encoders.
RawActivity model #
final activity = RawActivity(
points: [
GeoPoint(latitude: 40.0, longitude: -105.0, elevation: 1601, time: DateTime.utc(2024, 3, 1, 10)),
GeoPoint(latitude: 40.0005, longitude: -105.0005, elevation: 1602, time: DateTime.utc(2024, 3, 1, 10, 0, 10)),
],
channels: {
Channel.heartRate: [
Sample(time: DateTime.utc(2024, 3, 1, 10), value: 140),
Sample(time: DateTime.utc(2024, 3, 1, 10, 0, 10), value: 142),
],
},
laps: [
Lap(
startTime: DateTime.utc(2024, 3, 1, 10),
endTime: DateTime.utc(2024, 3, 1, 10, 0, 10),
distanceMeters: 70,
),
],
sport: Sport.running,
creator: 'Example Watch',
);
Parsing and encoding #
// Parse GPX to RawActivity (plus non-fatal warnings).
final result = ActivityParser.parse(gpxString, ActivityFileFormat.gpx);
for (final warning in result.warnings) {
print('Warning: $warning');
}
final activity = result.activity;
// Encode back to TCX with custom tolerances & precision.
final options = EncoderOptions(
defaultMaxDelta: const Duration(seconds: 2),
precisionLatLon: 6,
precisionEle: 1,
maxDeltaPerChannel: {
Channel.heartRate: const Duration(seconds: 1),
Channel.cadence: const Duration(seconds: 1),
},
);
final tcxString = ActivityEncoder.encode(activity, ActivityFileFormat.tcx, options: options);
final fitBase64 = ActivityEncoder.encode(activity, ActivityFileFormat.fit, options: options);
final fitBytes = base64Decode(fitBase64);
final fitActivity =
ActivityParser.parseBytes(fitBytes, ActivityFileFormat.fit).activity;
> Tip: `ActivityParser.parseBytes` accepts binary FIT payloads directly, so you
> can feed `File('ride.fit').readAsBytesSync()` without wrapping it in base64.
Editing pipeline #
final editor = RawEditor(activity)
.sortAndDedup()
.trimInvalid()
.crop(activity.startTime!, activity.endTime!.subtract(const Duration(minutes: 1)))
.downsampleTime(const Duration(seconds: 5))
.smoothHR(5)
.recomputeDistanceAndSpeed();
final cleaned = editor.activity;
final resampled = RawTransforms.resample(cleaned, step: const Duration(seconds: 2));
final (activity: withDistance, totalDistance: total) = RawTransforms.computeCumulativeDistance(resampled);
print('Distance: ${total.toStringAsFixed(1)} m');
Validation #
final validation = validateRawActivity(withDistance);
if (validation.errors.isEmpty) {
print('Activity valid with ${validation.warnings.length} warning(s).');
} else {
print('Validation failed:');
validation.errors.forEach(print);
}
Converter facade #
final warnings = <String>[];
final converted = ActivityConverter.convert(
gpxString,
from: ActivityFileFormat.gpx,
to: ActivityFileFormat.tcx,
encoderOptions: options,
warnings: warnings,
);
if (warnings.isNotEmpty) {
warnings.forEach(print);
}
// FIT conversions yield base64 strings containing the binary payload.
final fitWarnings = <String>[];
final fitResult = ActivityConverter.convert(
gpxString,
from: ActivityFileFormat.gpx,
to: ActivityFileFormat.fit,
encoderOptions: options,
warnings: fitWarnings,
);
final fitRoundTrip = ActivityParser.parseBytes(
base64Decode(fitResult),
ActivityFileFormat.fit,
).activity;
// Tip: ActivityConverter.convert also accepts raw FIT bytes (List<int>) for the
// input when converting from binary FIT files.
CLI usage #
$ dart run bin/activity_files.dart convert --from gpx --to tcx -i ride.gpx -o ride.tcx \
--max-delta-seconds 2 --precision-latlon 7 --hr-max-delta 1
$ dart run bin/activity_files.dart validate --format gpx -i ride.gpx --gap-threshold 180
The CLI reports parser warnings, validation warnings, and exits with a non-zero
status when validation errors are detected.
Binary FIT inputs are read directly from .fit files, and FIT outputs are
written as binary files (base64 is only used when you opt into the string APIs).
Contributing #
Issues and pull requests are welcome, especially for additional format fixtures. The package is released under the BSD 3-Clause license.