diff --git a/Directory.Build.props b/Directory.Build.props
index f7cc80c8..1edfd13c 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -9,8 +9,8 @@
disable
latest
- 5.7.4
- 46
+ 5.7.5
+ 47
FitEdit
EnduraByte LLC 2024
diff --git a/Infrastructure/FitEdit.Adapters.Fit/Extensions/FieldTools.cs b/Infrastructure/FitEdit.Adapters.Fit/Extensions/FieldTools.cs
index 526663ca..d3bca0e7 100644
--- a/Infrastructure/FitEdit.Adapters.Fit/Extensions/FieldTools.cs
+++ b/Infrastructure/FitEdit.Adapters.Fit/Extensions/FieldTools.cs
@@ -9,7 +9,7 @@ public static class FieldTools
public static void ReadFieldValue(
FieldBase field,
byte size,
- EndianBinaryReader mesgReader)
+ BinaryReader mesgReader)
{
byte baseType = (byte)(field.Type & Fit.BaseTypeNumMask);
// strings may be an array and are of variable length
@@ -76,7 +76,7 @@ public static void ReadFieldValue(
private static bool TryReadValue(
out object value,
byte type,
- EndianBinaryReader mesgReader,
+ BinaryReader mesgReader,
byte size)
{
bool invalid = true;
diff --git a/Infrastructure/FitEdit.Adapters.Fit/Mesg.cs b/Infrastructure/FitEdit.Adapters.Fit/Mesg.cs
index 9e303772..b4b9009d 100644
--- a/Infrastructure/FitEdit.Adapters.Fit/Mesg.cs
+++ b/Infrastructure/FitEdit.Adapters.Fit/Mesg.cs
@@ -120,7 +120,7 @@ public Mesg(Stream fitStream, MesgDefinition defnMesg)
public void Read(Stream inStream, MesgDefinition defnMesg)
{
inStream.Position = 1;
- EndianBinaryReader mesgReader = new EndianBinaryReader(inStream, defnMesg.IsBigEndian);
+ BinaryReader mesgReader = new EndianBinaryReader(inStream, defnMesg.IsBigEndian);
LocalNum = defnMesg.LocalMesgNum;
diff --git a/Infrastructure/FitEdit.Adapters.Fit/MesgDefinition.cs b/Infrastructure/FitEdit.Adapters.Fit/MesgDefinition.cs
index 5bf03b27..2ad00bd7 100644
--- a/Infrastructure/FitEdit.Adapters.Fit/MesgDefinition.cs
+++ b/Infrastructure/FitEdit.Adapters.Fit/MesgDefinition.cs
@@ -90,12 +90,12 @@ internal IEnumerable DeveloperFieldDefinitions
#endregion
#region Constructors
- internal MesgDefinition() : base((MesgDefinition)null)
- {
- LocalMesgNum = 0;
- GlobalMesgNum = (ushort)MesgNum.Invalid;
- architecture = Fit.LittleEndian;
- }
+ // internal MesgDefinition() : base((MesgDefinition)null)
+ // {
+ // LocalMesgNum = 0;
+ // GlobalMesgNum = (ushort)MesgNum.Invalid;
+ // architecture = Fit.LittleEndian;
+ // }
internal MesgDefinition(
Stream source,
@@ -137,7 +137,7 @@ public MesgDefinition(MesgDefinition mesgDef) : base(mesgDef)
{
LocalMesgNum = mesgDef.LocalMesgNum;
GlobalMesgNum = mesgDef.GlobalMesgNum;
- architecture = mesgDef.IsBigEndian ? Fit.BigEndian : Fit.LittleEndian;
+ architecture = mesgDef.architecture;
NumFields = mesgDef.NumFields;
foreach (FieldDefinition fieldDef in mesgDef.fieldDefs)
diff --git a/Infrastructure/FitEdit.Data/Fit/Edits/EmptyEdit.cs b/Infrastructure/FitEdit.Data/Fit/Edits/EmptyEdit.cs
new file mode 100644
index 00000000..f57a6114
--- /dev/null
+++ b/Infrastructure/FitEdit.Data/Fit/Edits/EmptyEdit.cs
@@ -0,0 +1,6 @@
+namespace FitEdit.Data.Fit.Edits;
+
+public class EmptyEdit : IEdit
+{
+ public FitFile Apply() => new();
+}
\ No newline at end of file
diff --git a/Infrastructure/FitEdit.Data/Fit/Edits/IEdit.cs b/Infrastructure/FitEdit.Data/Fit/Edits/IEdit.cs
new file mode 100644
index 00000000..c75bad1a
--- /dev/null
+++ b/Infrastructure/FitEdit.Data/Fit/Edits/IEdit.cs
@@ -0,0 +1,6 @@
+namespace FitEdit.Data.Fit.Edits;
+
+public interface IEdit
+{
+ public FitFile Apply();
+}
\ No newline at end of file
diff --git a/Infrastructure/FitEdit.Data/Fit/Edits/SplitLapEdit.cs b/Infrastructure/FitEdit.Data/Fit/Edits/SplitLapEdit.cs
new file mode 100644
index 00000000..04a55e6d
--- /dev/null
+++ b/Infrastructure/FitEdit.Data/Fit/Edits/SplitLapEdit.cs
@@ -0,0 +1,33 @@
+using Dynastream.Fit;
+
+namespace FitEdit.Data.Fit.Edits;
+
+public class SplitLapEdit(FitFile fit, RecordMesg rec) : IEdit
+{
+ public FitFile Apply()
+ {
+ var lap = FindLapContaining(fit, rec);
+
+ var records1 = fit.GetRecords(lap.Start(), rec.InstantOfTime());
+ var records2 = fit.GetRecords(rec.InstantOfTime(), lap.End());
+
+ var start1 = records1.First().InstantOfTime();
+ var end1 = records1.Last().InstantOfTime();
+
+ var start2 = records2.First().InstantOfTime();
+ var end2 = records2.Last().InstantOfTime();
+
+ var lap1 = FitFileExtensions.ReconstructLap(records1, start1, end1);
+ var lap2 = FitFileExtensions.ReconstructLap(records2, start2, end2);
+
+ fit.Remove(lap);
+ fit.Add(lap1);
+ fit.Add(lap2);
+
+ fit.ForwardfillEvents();
+ return fit;
+ }
+
+ // Find the lap the RecordMesg belongs to
+ private static LapMesg FindLapContaining(FitFile fit, RecordMesg record) => fit.Get().FirstOrDefault(lap => record.IsBetween(lap.Start(), lap.End()));
+}
\ No newline at end of file
diff --git a/Infrastructure/FitEdit.Data/Fit/FitFileExtensions.cs b/Infrastructure/FitEdit.Data/Fit/FitFileExtensions.cs
index f423f9ad..c7c5a844 100644
--- a/Infrastructure/FitEdit.Data/Fit/FitFileExtensions.cs
+++ b/Infrastructure/FitEdit.Data/Fit/FitFileExtensions.cs
@@ -178,7 +178,7 @@ public static FitFile BackfillEvents(this FitFile f, int resolution = 100, Actio
public static string OneLine(this FitFile f) => f.Sessions.Count switch
{
1 => $"From {f.Sessions[0].Start()} to {f.Sessions[0].End()}: {f.Sessions[0].GetTotalDistance()} m in {f.Sessions[0].GetTotalElapsedTime()}s ({f.Sessions[0].GetEnhancedAvgSpeed():0.##} m/s)",
- _ when f.Sessions.Count > 1 => $"From {f.Sessions.First().Start()} to {f.Sessions.Last().End()}: {f.Sessions.TotalDistance()} m in {f.Sessions.TotalElapsedTime()}s",
+ > 1 => $"From {f.Sessions.First().Start()} to {f.Sessions.Last().End()}: {f.Sessions.TotalDistance()} m in {f.Sessions.TotalElapsedTime()}s",
_ => "No sessions",
};
@@ -223,8 +223,8 @@ private static FitFile Print(this FitFile f, Action print, bool showReco
foreach (var rec in lapRecords)
{
- var speed = new Speed { Unit = Unit.MetersPerSecond, Value = (double)rec.GetEnhancedSpeed() };
- var distance = new Distance { Unit = Unit.Meter, Value = (double)rec.GetDistance() };
+ var speed = new Speed { Unit = Unit.MetersPerSecond, Value = rec.GetEnhancedSpeed() ?? 0 };
+ var distance = new Distance { Unit = Unit.Meter, Value = rec.GetDistance() ?? 0 };
print($" At {rec.InstantOfTime():HH:mm:ss}: {distance.Miles():0.##} mi, {speed.Convert(Unit.MinutesPerMile)}, {rec.GetHeartRate()} bpm, {(rec.GetCadence() + rec.GetFractionalCadence()) * 2} cad");
//print($" At {rec.Start():HH:mm:ss}: {rec.GetDistance():0.##} m, {rec.GetEnhancedSpeed():0.##} m/s, {rec.GetHeartRate()} bpm, {(rec.GetCadence() + rec.GetFractionalCadence()) * 2} cad");
@@ -371,6 +371,7 @@ public static FitFile ApplySpeeds(this FitFile fitFile, Dictionary l
return merged;
}
+
public static (FitFile first, FitFile second) SplitAt(this FitFile source, DateTime at)
{
DateTime start = source.GetStartTime();
@@ -391,17 +392,21 @@ public static List SplitByLap(this FitFile? source)
.ToList();
}
- private static FitFile? ExtractRecords(this FitFile? source, DateTime start, DateTime end)
- {
- List records = source
+ private static FitFile? ExtractRecords(this FitFile? source, DateTime start, DateTime end) =>
+ RepairAdditively(source, GetRecords(source, start, end));
+
+ public static List GetLaps(this FitFile? source, DateTime start, DateTime end) => source
+ .Get()
+ .InstantBetween(start, end)
+ .ToList()
+ .Sorted(MessageExtensions.SortByTimestamp);
+
+ public static List GetRecords(this FitFile? source, DateTime start, DateTime end) => source
.Get()
.InstantBetween(start, end)
.ToList()
.Sorted(MessageExtensions.SortByTimestamp);
- return RepairAdditively(source, records);
- }
-
private static List FindAll(this FitFile f) where T : Mesg => f.FindAll(typeof(T)).Cast().ToList();
private static List FindAll(this FitFile f, Type t) => f.Events.OfType()
@@ -420,7 +425,7 @@ private static List FindAll(this FitFile f, Type t) => f.Events.OfType
- /// This method preserves more information than ,
+ /// This method preserves more information than ,
/// such as individual sports in multisport events like triathlons, sessions, and laps.
/// Use that one when this one does not work.
///
@@ -510,8 +515,8 @@ private static List FindAll(this FitFile f, Type t) => f.Events.OfType().FirstOrDefault();
- fileId!.SetTimeCreated(start);
+ var fileId = source.Get().FirstOrDefault() ?? new FileIdMesg();
+ fileId.SetTimeCreated(start);
var fileCreator = source.Get().FirstOrDefault();
var deviceInfo = source.Get().InstantBetween(start, end);
@@ -531,14 +536,14 @@ private static List FindAll(this FitFile f, Type t) => f.Events.OfType records2 = EnsureCumulativeDistance(records);
SessionMesg session = ReconstructSession(source, records2);
- LapMesg lap = ReconstructLap(source, records2);
+ LapMesg lap = source.ReconstructLap(records2);
ActivityMesg activity = ReconstructActivity(source);
List mesgs = new();
- if (fileId != null) { mesgs.Add(fileId); }
+ mesgs.Add(fileId);
if (fileCreator != null) { mesgs.Add(fileCreator); }
- if (startEvent != null) { mesgs.Add(startEvent); }
+ mesgs.Add(startEvent);
if (deviceInfo != null) { mesgs.AddRange(deviceInfo); }
if (deviceSettings != null) { mesgs.Add(deviceSettings); }
if (userProfile != null) { mesgs.Add(userProfile); }
@@ -673,7 +678,6 @@ private static void AddDistance(this RecordMesg record, double distance)
var sport = source.Get().FirstOrDefault();
var firstLap = source.Get().FirstOrDefault();
- var lastLap = source.Get().LastOrDefault();
var session = source.Get().FirstOrDefault();
var activity = source.Get().FirstOrDefault();
@@ -693,7 +697,7 @@ private static void AddDistance(this RecordMesg record, double distance)
if (firstLap is null)
{
- firstLap = ReconstructLap(source, records);
+ firstLap = source.ReconstructLap(records);
if (sport != null)
{
@@ -735,7 +739,7 @@ private static void AddDistance(this RecordMesg record, double distance)
Dynastream.Fit.DateTime? lapEnd = lap.GetStartTime();
float? elapsed = lap.GetTotalElapsedTime();
- if (lap != null && elapsed != null)
+ if (elapsed != null)
{
lapEnd.Add(new Dynastream.Fit.DateTime((uint)elapsed));
}
@@ -743,18 +747,22 @@ private static void AddDistance(this RecordMesg record, double distance)
return lapEnd?.GetDateTime();
}
- private static LapMesg ReconstructLap(FitFile source, List records)
+ public static LapMesg ReconstructLap(List records, DateTime start, DateTime end) =>
+ ReconstructLap(records, new Dynastream.Fit.DateTime(start), new Dynastream.Fit.DateTime(end));
+
+ private static LapMesg ReconstructLap(List records, Dynastream.Fit.DateTime start, Dynastream.Fit.DateTime end)
{
- if (!records.Any())
- {
- return GetFakeLap(source);
- }
-
- var start = new Dynastream.Fit.DateTime(source.GetStartTime());
- var end = new Dynastream.Fit.DateTime(source.GetEndTime());
-
- float totalDistance = SumDistance(records);
-
+ double avgSpeed = records.Average(r => r.GetEnhancedSpeed() ?? 0);
+ double maxSpeed = records.Max(r => r.GetEnhancedSpeed() ?? 0);
+ double avgHr = records.Average(r => r.GetHeartRate() ?? 0);
+ double maxHr = records.Max(r => r.GetHeartRate() ?? 0);
+ double avgCadence = records.Average(r => r.GetCadence() ?? 0);
+ double maxCadence = records.Max(r => r.GetCadence() ?? 0);
+ double avgPower = records.Average(r => r.GetPower() ?? 0);
+ double maxPower = records.Max(r => r.GetPower() ?? 0);
+ uint totalCalories = (records.Last().GetCalories() ?? 0U) - (records.First().GetCalories() ?? 0U);
+ double totalDistance = (records.Last().GetDistance() ?? 0) - (records.First().GetDistance() ?? 0);
+
var lap = new LapMesg();
lap.SetStartTime(start);
lap.SetTimestamp(end);
@@ -766,11 +774,26 @@ private static LapMesg ReconstructLap(FitFile source, List records)
lap.SetEndPositionLong(records.Last().GetPositionLong());
lap.SetTotalElapsedTime(end.GetTimeStamp() - start.GetTimeStamp());
lap.SetTotalTimerTime(end.GetTimeStamp() - start.GetTimeStamp());
- lap.SetTotalDistance(totalDistance);
+ lap.SetTotalDistance((float)totalDistance);
+ lap.SetTotalCalories((ushort)totalCalories);
+ lap.SetEnhancedAvgSpeed((float)avgSpeed);
+ lap.SetEnhancedMaxSpeed((float)maxSpeed);
+ lap.SetAvgHeartRate((byte)avgHr);
+ lap.SetMaxHeartRate((byte)maxHr);
+ lap.SetAvgCadence((byte)avgCadence);
+ lap.SetMaxCadence((byte)maxCadence);
+ lap.SetAvgPower((ushort)avgPower);
+ lap.SetMaxPower((ushort)maxPower);
+
lap.SetLapTrigger(LapTrigger.SessionEnd);
return lap;
}
+
+ private static LapMesg ReconstructLap(this FitFile source, List records) =>
+ records.Any()
+ ? ReconstructLap(records, source.GetStartTime(), source.GetEndTime())
+ : GetFakeLap(source);
private static SessionMesg ReconstructSession(FitFile source, List records)
{
@@ -782,8 +805,6 @@ private static SessionMesg ReconstructSession(FitFile source, List r
var start = new Dynastream.Fit.DateTime(source.GetStartTime());
var end = new Dynastream.Fit.DateTime(source.GetEndTime());
- float totalDistance = SumDistance(records);
-
var session = new SessionMesg();
session.SetStartTime(start);
session.SetTimestamp(end);
@@ -795,7 +816,7 @@ private static SessionMesg ReconstructSession(FitFile source, List r
session.SetEndPositionLong(records.Last().GetPositionLong());
session.SetTotalElapsedTime(end.GetTimeStamp() - start.GetTimeStamp());
session.SetTotalTimerTime(end.GetTimeStamp() - start.GetTimeStamp());
- session.SetTotalDistance(totalDistance);
+ session.SetTotalDistance(records.Last().GetDistance() - records.First().GetDistance());
session.SetTrigger(SessionTrigger.ActivityEnd);
return session;
}
@@ -844,20 +865,15 @@ private static SessionMesg GetFakeSession(FitFile source)
///
/// Sum the distance between all records
///
- private static float SumDistance(List records)
+ private static double SumDistance(List records)
{
- double? sum = 0;
-
var penultimate = Math.Max(0, records.Count - 1);
- foreach (int i in Enumerable.Range(1, penultimate))
- {
- sum += records[i].GetDistance() - (double?)records[i - 1].GetDistance();
- }
-
- return (float)(sum ?? 0);
+ return Enumerable.Range(1, penultimate)
+ .Aggregate(0, (current, i) =>
+ current + ((records[i].GetDistance() ?? 0) - (records[i - 1].GetDistance() ?? 0)));
}
-
+
///
/// Reconstruct missing Session messages from Lap messages.
///
@@ -988,7 +1004,7 @@ public static void RemoveAll(this FitFile fit) where T : Mesg
///
/// Remove all messages and message definitions for the given message type.
///
- public static void RemoveAll(this FitFile fit, Type t)
+ public static void RemoveAllOfType(this FitFile fit, Type t)
{
var matches = fit.FindAll(t);
foreach (var match in matches)
@@ -1026,21 +1042,4 @@ public static void RemoveAll(this FitFile fit, Type t, DateTime after = default,
private static bool HasMessageOfType(this EventArgs e, Type t) =>
e is MesgEventArgs mea && mea.mesg.Num == MessageFactory.MesgNums[t]
|| e is MesgDefinitionEventArgs mdea && mdea.mesgDef.GlobalMesgNum == MessageFactory.MesgNums[t];
-
- ///
- /// Return true if the given message occurs between the given DateTimes.
- /// If either DateTime is not specified, it is not considered.
- ///
- private static bool IsBetween(this Mesg mesg, DateTime after = default, DateTime before = default) => mesg switch
- {
- _ when mesg is IDurationOfTime dur
- && (after == default || dur.GetStartTime().GetDateTime() > after)
- && (before == default || dur.GetTimestamp().GetDateTime() <= before) => true,
-
- _ when mesg is IInstantOfTime inst
- && (after == default || inst.GetTimestamp().GetDateTime() > after)
- && (before == default || inst.GetTimestamp().GetDateTime() <= before) => true,
-
- _ => false,
- };
-}
+}
\ No newline at end of file
diff --git a/Infrastructure/FitEdit.Data/Fit/MesgExtensions.cs b/Infrastructure/FitEdit.Data/Fit/MesgExtensions.cs
index 96ae943a..6d6ddfe5 100644
--- a/Infrastructure/FitEdit.Data/Fit/MesgExtensions.cs
+++ b/Infrastructure/FitEdit.Data/Fit/MesgExtensions.cs
@@ -6,6 +6,7 @@
using FitEdit.Model.Extensions;
using Dynastream.Fit;
using AssemblyExtensions = FitEdit.Model.Extensions.AssemblyExtensions;
+using DateTime = System.DateTime;
namespace FitEdit.Data.Fit;
@@ -22,6 +23,21 @@ static MesgExtensions()
fit_ = assembly;
}
+ ///
+ /// Return true if the given message occurs between the given DateTimes.
+ /// If either DateTime is not specified, it is not considered.
+ ///
+ public static bool IsBetween(this Mesg mesg, DateTime after = default, DateTime before = default) => mesg switch
+ {
+ IDurationOfTime dur when (after == default || dur.GetStartTime().GetDateTime() > after)
+ && (before == default || dur.GetTimestamp().GetDateTime() <= before) => true,
+
+ IInstantOfTime inst when (after == default || inst.GetTimestamp().GetDateTime() > after)
+ && (before == default || inst.GetTimestamp().GetDateTime() <= before) => true,
+
+ _ => false,
+ };
+
public static void SetFieldValue(this Mesg mesg, string name, object? value, bool pretty)
{
try
diff --git a/Ui/FitEdit.Ui.iOS/Info.plist b/Ui/FitEdit.Ui.iOS/Info.plist
index 3906503c..c4480b6e 100644
--- a/Ui/FitEdit.Ui.iOS/Info.plist
+++ b/Ui/FitEdit.Ui.iOS/Info.plist
@@ -7,7 +7,7 @@
CFBundleIdentifier
com.endurabyte.fitedit
CFBundleShortVersionString
- 5.7.4
+ 5.7.5
LSRequiresIPhoneOS
MinimumOSVersion
@@ -58,4 +58,4 @@
CFBundleVersion
-
+
\ No newline at end of file
diff --git a/Ui/FitEdit.Ui/ViewModels/RecordViewModel.cs b/Ui/FitEdit.Ui/ViewModels/RecordViewModel.cs
index ce8b5699..44b73456 100644
--- a/Ui/FitEdit.Ui/ViewModels/RecordViewModel.cs
+++ b/Ui/FitEdit.Ui/ViewModels/RecordViewModel.cs
@@ -10,6 +10,7 @@
using Dynastream.Fit;
using FitEdit.Data;
using FitEdit.Data.Fit;
+using FitEdit.Data.Fit.Edits;
using FitEdit.Model.Extensions;
using FitEdit.Ui.Converters;
using FitEdit.Ui.Model;
@@ -576,6 +577,31 @@ private void AddContextMenus(string mesgName, DataGrid dg)
};
menu.Items.Add(delete);
+ if (mesgName == "Record")
+ {
+ var menuItem = new MenuItem
+ {
+ Header = "Split Activity Here",
+ Command = ReactiveCommand.Create(SplitActivity),
+ };
+ ToolTip.SetTip(menuItem,
+ "Split the activity at the selected record."
+ + "\nThe first activity will contain all messages up to and including the selected row."
+ + "\nThe second activity will contain all messages after the selected row.");
+ menu.Items.Add(menuItem);
+ }
+
+ if (mesgName == "Record")
+ {
+ var menuItem = new MenuItem
+ {
+ Header = "Split Lap Here",
+ Command = ReactiveCommand.Create(SplitLap),
+ };
+ ToolTip.SetTip(menuItem, "Split the lap at the selected record into two laps.");
+ menu.Items.Add(menuItem);
+ }
+
if (mesgName == "Lap")
{
var menuItem = new MenuItem
@@ -679,6 +705,15 @@ public async Task SplitActivity()
await fileService_.CreateAsync(first, "(Split 1)");
await fileService_.CreateAsync(second, "(Split 2)");
}
+
+ public void SplitLap()
+ {
+ if (fitFile_ is null) { return; }
+ IEdit edit = new SplitLapEdit(fitFile_, fitFile_.Records[SelectedIndex]);
+ edit.Apply();
+
+ HaveUnsavedChanges = true;
+ }
private void HandleCellEditEnding(object? sender, DataGridCellEditEndingEventArgs e)
{
@@ -733,6 +768,6 @@ private void SetFieldValues(List messages, string fieldName, obj
{
message.SetFieldValue(fieldName, newValue, PrettifyFields);
}
+ HaveUnsavedChanges = true;
}
-
}
diff --git a/Ui/FitEdit.Ui/Views/RecordView.axaml b/Ui/FitEdit.Ui/Views/RecordView.axaml
index c8aeb46e..88d3417a 100644
--- a/Ui/FitEdit.Ui/Views/RecordView.axaml
+++ b/Ui/FitEdit.Ui/Views/RecordView.axaml
@@ -57,18 +57,6 @@
-
-