71 lines
3.3 KiB
C#
71 lines
3.3 KiB
C#
using System.Text.Json;
|
||
|
||
namespace Server.WatchBody.Web.Services;
|
||
|
||
/// <summary>
|
||
/// Таймлайн тренировки из facts_json (spec 03 §2 m08): по Lap —
|
||
/// длительность/avg/peak пульс/отдых; пульсограмма records с
|
||
/// прореживанием до 600 точек.
|
||
/// </summary>
|
||
public static class FactsTimeline
|
||
{
|
||
public static (List<Dictionary<string, object?>> Timeline, List<Dictionary<string, int>> Curve) Parse(
|
||
string? factsJson, int sessionDurationS)
|
||
{
|
||
var timeline = new List<Dictionary<string, object?>>();
|
||
var curve = new List<Dictionary<string, int>>();
|
||
if (string.IsNullOrWhiteSpace(factsJson)) return (timeline, curve);
|
||
try
|
||
{
|
||
using var doc = JsonDocument.Parse(factsJson);
|
||
var root = doc.RootElement;
|
||
|
||
var records = new List<(int t, int hr)>();
|
||
if (root.TryGetProperty("records", out var recordNodes))
|
||
foreach (var r in recordNodes.EnumerateArray())
|
||
if (r.TryGetProperty("t_offset_s", out var t) && r.TryGetProperty("hr", out var hr))
|
||
records.Add((t.GetInt32(), hr.GetInt32()));
|
||
|
||
// пульсограмма: не более 600 точек
|
||
if (records.Count > 0)
|
||
{
|
||
var step = Math.Max(1, (int)Math.Ceiling(records.Count / 600.0));
|
||
for (var i = 0; i < records.Count; i += step)
|
||
curve.Add(new Dictionary<string, int> { ["t"] = records[i].t, ["hr"] = records[i].hr });
|
||
}
|
||
|
||
if (!root.TryGetProperty("laps", out var lapNodes)) return (timeline, curve);
|
||
var laps = new List<(DateTime start, int durationS)>();
|
||
foreach (var l in lapNodes.EnumerateArray())
|
||
if (l.TryGetProperty("start", out var s) && l.TryGetProperty("duration_s", out var d) &&
|
||
DateTime.TryParse(s.GetString(), null, System.Globalization.DateTimeStyles.AssumeUniversal |
|
||
System.Globalization.DateTimeStyles.AdjustToUniversal, out var start))
|
||
laps.Add((start, d.GetInt32()));
|
||
var sessionStart = laps.Count > 0 ? laps.Min(l => l.start)
|
||
: records.Count > 0 ? DateTime.MinValue : DateTime.MinValue;
|
||
|
||
for (var i = 0; i < laps.Count; i++)
|
||
{
|
||
var offset = (int)(laps[i].start - sessionStart).TotalSeconds;
|
||
var end = offset + laps[i].durationS;
|
||
var nextStart = i + 1 < laps.Count ? (int)(laps[i + 1].start - sessionStart).TotalSeconds : end;
|
||
var inLap = records.Where(r => r.t >= offset && r.t <= end).Select(r => r.hr).ToList();
|
||
timeline.Add(new Dictionary<string, object?>
|
||
{
|
||
["index"] = i,
|
||
["startOffsetS"] = offset,
|
||
["durationS"] = laps[i].durationS,
|
||
["avgHr"] = inLap.Count > 0 ? (int)Math.Round(inLap.Average(), MidpointRounding.AwayFromZero) : null,
|
||
["peakHr"] = inLap.Count > 0 ? inLap.Max() : null,
|
||
["restAfterS"] = Math.Max(0, nextStart - end),
|
||
});
|
||
}
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
// битые факты — пустой таймлайн
|
||
}
|
||
return (timeline, curve);
|
||
}
|
||
}
|