diff --git a/Directory.Build.props b/Directory.Build.props index 4b6986f..423df1b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,8 +9,8 @@ disable latest - 5.7.6 - 48 + 5.8.0 + 46 FitEdit EnduraByte LLC 2024 diff --git a/FitEdit.sln b/FitEdit.sln index bf52942..0fa561c 100644 --- a/FitEdit.sln +++ b/FitEdit.sln @@ -149,7 +149,6 @@ Global {9B06A0E5-9409-4D7C-B532-364C12264BE1}.Debug|Any CPU.Deploy.0 = Debug|Any CPU {9B06A0E5-9409-4D7C-B532-364C12264BE1}.Release|Any CPU.ActiveCfg = Release|Any CPU {9B06A0E5-9409-4D7C-B532-364C12264BE1}.Release|Any CPU.Build.0 = Release|Any CPU - {9B06A0E5-9409-4D7C-B532-364C12264BE1}.Release|Any CPU.Deploy.0 = Release|Any CPU {21ECAE7A-E6E1-4857-9631-F24951E3CA17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {21ECAE7A-E6E1-4857-9631-F24951E3CA17}.Debug|Any CPU.Build.0 = Debug|Any CPU {21ECAE7A-E6E1-4857-9631-F24951E3CA17}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Infrastructure/FitEdit.Adapters.Fit/Decode.cs b/Infrastructure/FitEdit.Adapters.Fit/Decode.cs index f4cb2d2..e9de169 100644 --- a/Infrastructure/FitEdit.Adapters.Fit/Decode.cs +++ b/Infrastructure/FitEdit.Adapters.Fit/Decode.cs @@ -283,17 +283,17 @@ public void DecodeNextMessage(Stream fitStream) long sourceIndex = fitStream.Position; byte nextByte = br.ReadByte(); - Log.Debug($"Message header: {nextByte:X2}"); + //Log.Debug($"Message header: {nextByte:X2}"); bool isCompressedHeader = (nextByte & Fit.CompressedHeaderMask) == Fit.CompressedHeaderMask; bool isMesgDefinition = (nextByte & Fit.MesgDefinitionMask) == Fit.MesgDefinitionMask; bool isDataMessage = (nextByte & Fit.MesgDefinitionMask) == Fit.MesgHeaderMask; bool isDevData = (nextByte & Fit.DevDataMask) == Fit.DevDataMask; - Log.Debug($" Compressed: {isCompressedHeader}"); - Log.Debug($" Definition: {isMesgDefinition}"); - Log.Debug($" Data: {isDataMessage}"); - Log.Debug($" DevData: {isDevData}"); + //Log.Debug($" Compressed: {isCompressedHeader}"); + //Log.Debug($" Definition: {isMesgDefinition}"); + //Log.Debug($" Data: {isDataMessage}"); + //Log.Debug($" DevData: {isDevData}"); // Is it a compressed timestamp mesg? if (isCompressedHeader) @@ -331,7 +331,7 @@ private void ReadDataMesg(Stream fitStream, BinaryReader br, long sourceIndex, b } MesgDefinition def = _localMesgDefs[localMesgNum]; - Log.Debug($" (global, local) message num: ({localMesgNum}, {def.GlobalMesgNum})"); + //Log.Debug($" (global, local) message num: ({localMesgNum}, {def.GlobalMesgNum})"); int fieldsSize = def.GetMesgSize() - 1; if (FitConfig.Discard.DataMessages.OfLargeSize diff --git a/Infrastructure/FitEdit.Data/Fit/Edits/RemoveGapsEdit.cs b/Infrastructure/FitEdit.Data/Fit/Edits/RemoveGapsEdit.cs new file mode 100644 index 0000000..81547aa --- /dev/null +++ b/Infrastructure/FitEdit.Data/Fit/Edits/RemoveGapsEdit.cs @@ -0,0 +1,58 @@ +namespace FitEdit.Data.Fit.Edits; + +public class RemoveGapsEdit(FitFile fit) : IEdit +{ + public FitFile Apply() + { + var minGap = TimeSpan.FromSeconds(60); + + var copy = new FitFile(fit); + var records = copy.Records; + + // Algorithm: + // For each pair of successive records i and i + 1 + // where time span between them differs by more than the min gap, + // make the time span between them 1s and update all later timestamps accordingly. + foreach (int i in Enumerable.Range(0, records.Count - 1)) + { + DateTime start = records[i].GetTimestamp().GetDateTime(); + DateTime end = records[i + 1].GetTimestamp().GetDateTime(); + TimeSpan gap = end - start; + + if (gap < minGap) + continue; + + SetRecordStartTimeCascading(records, i + 1, start + TimeSpan.FromSeconds(1)); + } + + copy.BackfillEvents(); + + return copy; + } + + /// + /// Set record i to the given start time, and shift all subsequent record timestamps by the time diff. + /// + private static void SetRecordStartTimeCascading(List records, int i, DateTime start) + { + // Set record i to the given start time. Keep track of the time diff. + DateTime oldEnd = records[i].GetTimestamp().GetDateTime(); + records[i].SetTimestamp(new Dynastream.Fit.DateTime(start)); + DateTime newEnd = records[i].GetTimestamp().GetDateTime(); + + TimeSpan diff = oldEnd - newEnd; + + // Shift all subsequent record timestamps + ShiftTimestamps(records, i + 1, diff); + } + + private static void ShiftTimestamps(List records, int i, TimeSpan diff) + { + for (int j = i; j < records.Count - 1; j++) + { + DateTime start = records[j].GetTimestamp().GetDateTime(); + + records[j].SetTimestamp(new Dynastream.Fit.DateTime(start - diff)); + } + } +} diff --git a/Infrastructure/FitEdit.Data/Fit/FitFileExtensions.cs b/Infrastructure/FitEdit.Data/Fit/FitFileExtensions.cs index c0958d0..8e08b08 100644 --- a/Infrastructure/FitEdit.Data/Fit/FitFileExtensions.cs +++ b/Infrastructure/FitEdit.Data/Fit/FitFileExtensions.cs @@ -472,7 +472,7 @@ private static List FindAll(this FitFile f, Type t) => f.Events.OfType fits, out Decode deco decoder.FitFileRead += () => { - Log.Debug($"Read FIT file with {tmp.Events.Count} messages"); + //Log.Debug($"Read FIT file with {tmp.Events.Count} messages"); tmp = new FitFile(); }; @@ -143,4 +143,4 @@ public bool TryGetDecoder(Stream stream, out List fits, out Decode deco public static class FitLog { -} \ No newline at end of file +} diff --git a/Infrastructure/FitEdit.Data/Fit/Writer.cs b/Infrastructure/FitEdit.Data/Fit/Writer.cs index 4dd7cff..6bc4412 100644 --- a/Infrastructure/FitEdit.Data/Fit/Writer.cs +++ b/Infrastructure/FitEdit.Data/Fit/Writer.cs @@ -56,4 +56,4 @@ public void Write(FitFile fitFile, Stream dest) Log.Info($"Wrote {fitFile.Messages.Count} messages and {fitFile.MessageDefinitions.Count} definitions"); encoder.Close(); } -} \ No newline at end of file +} diff --git a/Ui/FitEdit.Ui.Infra/appsettings.json b/Ui/FitEdit.Ui.Infra/appsettings.json index b31f65d..8f813c4 100644 --- a/Ui/FitEdit.Ui.Infra/appsettings.json +++ b/Ui/FitEdit.Ui.Infra/appsettings.json @@ -13,7 +13,7 @@ "Default": "Information", "Override": { "Microsoft": "Warning" - //"CompositionRoot": "Debug" + //, "CompositionRoot": "Debug" } }, "Using": [ "Serilog.Sinks.File", "Serilog.Sinks.Debug" ], diff --git a/Ui/FitEdit.Ui.iOS/Info.plist b/Ui/FitEdit.Ui.iOS/Info.plist index c4480b6..ef1cd36 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.5 + 5.8.0 LSRequiresIPhoneOS MinimumOSVersion @@ -57,5 +57,6 @@ CFBundleVersion + 46 - \ No newline at end of file + diff --git a/Ui/FitEdit.Ui/ViewModels/FileViewModel.cs b/Ui/FitEdit.Ui/ViewModels/FileViewModel.cs index 6b31fef..3e1adbe 100644 --- a/Ui/FitEdit.Ui/ViewModels/FileViewModel.cs +++ b/Ui/FitEdit.Ui/ViewModels/FileViewModel.cs @@ -5,6 +5,7 @@ using FitEdit.Adapters.Strava; using FitEdit.Data; using FitEdit.Data.Fit; +using FitEdit.Data.Fit.Edits; using FitEdit.Model; using FitEdit.Model.Extensions; using FitEdit.Model.GarminConnect; @@ -17,6 +18,7 @@ using FitEdit.Ui.Infra; using FitEdit.Ui.Model.Supabase; using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json.Bson; using ReactiveUI; using ReactiveUI.Fody.Helpers; @@ -735,6 +737,20 @@ public void HandleRepairBackfillClicked(UiFile uif) )); } + public void HandleRemoveGapsClicked(UiFile file) + { + FitFile? fit = new RemoveGapsEdit(file.FitFile).Apply(); + + Task.Run(async () => + { + await Persist(new FileReference + ( + $"(No Gaps) {file.Activity?.Name}", + fit.GetBytes() + )); + }); + } + public async Task HandleOpenGarminUploadPageClicked() => await browser_.OpenAsync("https://connect.garmin.com/modern/import-data"); public async Task HandleOpenStravaUploadPageClicked() => await browser_.OpenAsync("https://www.strava.com/upload/select"); diff --git a/Ui/FitEdit.Ui/ViewModels/PlotViewModel.cs b/Ui/FitEdit.Ui/ViewModels/PlotViewModel.cs index 7eac134..050d8a7 100644 --- a/Ui/FitEdit.Ui/ViewModels/PlotViewModel.cs +++ b/Ui/FitEdit.Ui/ViewModels/PlotViewModel.cs @@ -178,7 +178,7 @@ protected void Add(UiFile file) foreach (var record in fit.Records) { - var speed = (double?)record.GetEnhancedSpeed() ?? 0; + var speed = (double?)(record.GetSpeed() ?? record.GetEnhancedSpeed()) ?? 0; var hr = (double?)record.GetHeartRate() ?? 0; var cadence = (double?)record.GetCadence() ?? 0; var time = record.InstantOfTime(); diff --git a/Ui/FitEdit.Ui/Views/FileView.axaml b/Ui/FitEdit.Ui/Views/FileView.axaml index 760104d..2ab670a 100644 --- a/Ui/FitEdit.Ui/Views/FileView.axaml +++ b/Ui/FitEdit.Ui/Views/FileView.axaml @@ -75,6 +75,17 @@ + + + + Remove gaps > 60s from the file. This makes it look like e.g. you didn't take a break during your workout. This adjusts the timestamps of all records that follow a gap. + + + + - Creates a new activity with all of the changes applied. + Write all changes to the current FIT file. diff --git a/units b/units index ee9a805..a58fdea 160000 --- a/units +++ b/units @@ -1 +1 @@ -Subproject commit ee9a8059975424ef794f408e44bec052d3111476 +Subproject commit a58fdeab8447d1ed4a6a24592a0778d21806f5a5