activity_files 0.1.2 copy "activity_files: ^0.1.2" to clipboard
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 RawActivity model 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.

3
likes
0
points
901
downloads

Publisher

verified publishereikedreier.xyz

Weekly Downloads

A pure Dart toolkit for parsing, editing, and encoding workout activity files such as GPX, TCX, and FIT.

Repository (GitHub)
View/report issues

Topics

#fitness #gpx #tcx #fit #sports-data

License

unknown (license)

Dependencies

args, collection, xml

More

Packages that depend on activity_files