111 lines
4.7 KiB
C#
111 lines
4.7 KiB
C#
using System.Xml.Linq;
|
|
|
|
namespace Server.WatchBody.Import;
|
|
|
|
/// <summary>
|
|
/// Упрощённый разбор .tcx/.gpx (производные форматы — документ
|
|
/// форматов): тип/старт/длительность + пульс-записи; подходов в этих
|
|
/// форматах нет. fit_file_id — имя файла (дедупликация).
|
|
/// </summary>
|
|
public class TcxGpxParser
|
|
{
|
|
public ActivityFacts Parse(string path)
|
|
{
|
|
var name = Path.GetFileName(path);
|
|
XDocument doc;
|
|
try
|
|
{
|
|
doc = XDocument.Load(path);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
throw new FitParseException($"XML не читается: {name} — {e.Message}");
|
|
}
|
|
|
|
return doc.Root!.Name.LocalName == "TrainingCenterDatabase"
|
|
? ParseTcx(doc, name)
|
|
: ParseGpx(doc, name);
|
|
}
|
|
|
|
private static ActivityFacts ParseTcx(XDocument doc, string name)
|
|
{
|
|
var ns = doc.Root!.Name.Namespace;
|
|
var activity = doc.Descendants(ns + "Activity").FirstOrDefault()
|
|
?? throw new FitParseException($"в TCX нет Activity: {name}");
|
|
var id = (string?)activity.Element(ns + "Id");
|
|
var start = ParseStart(id) ?? throw new FitParseException($"в TCX нет Id-старта: {name}");
|
|
|
|
var records = activity.Descendants(ns + "Trackpoint")
|
|
.Select(tp => new
|
|
{
|
|
time = DateTime.TryParse((string?)tp.Element(ns + "Time"), out var t)
|
|
? t.ToUniversalTime() : (DateTime?)null,
|
|
hr = (int?)tp.Element(ns + "HeartRateBpm")?.Element(ns + "Value"),
|
|
})
|
|
.Where(r => r.time != null && r.hr != null && r.hr > 0)
|
|
.Select(r => (time: r.time!.Value, hr: r.hr!.Value))
|
|
.ToList();
|
|
|
|
var durationS = (int)((records.Count > 0 ? records.Max(r => r.time) : start) - start).TotalSeconds;
|
|
var type = (string?)activity.Attribute("Sport") ?? "unknown";
|
|
return Build(name, start, durationS, type.ToLowerInvariant(), records, device: string.Empty);
|
|
}
|
|
|
|
private static ActivityFacts ParseGpx(XDocument doc, string name)
|
|
{
|
|
var start = doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "time") is { } t
|
|
? DateTime.Parse(t.Value).ToUniversalTime()
|
|
: throw new FitParseException($"в GPX нет времени: {name}");
|
|
var records = doc.Descendants()
|
|
.Where(e => e.Name.LocalName is "trkpt" or "wpt")
|
|
.Select(pt => new
|
|
{
|
|
time = pt.Descendants().FirstOrDefault(x => x.Name.LocalName == "time") is { } ptTime &&
|
|
DateTime.TryParse(ptTime.Value, out var ptParsed)
|
|
? ptParsed.ToUniversalTime() : (DateTime?)null,
|
|
hr = pt.Descendants().FirstOrDefault(x => x.Name.LocalName is "hr" or "gpxtpx:hr"
|
|
|| x.Name.LocalName.EndsWith("hr")) is { } hrEl && int.TryParse(hrEl.Value, out var hr)
|
|
? hr : (int?)null,
|
|
})
|
|
.Where(r => r.time != null && r.hr != null && r.hr > 0)
|
|
.Select(r => (time: r.time!.Value, hr: r.hr!.Value))
|
|
.ToList();
|
|
|
|
var durationS = (int)((records.Count > 0 ? records.Max(r => r.time) : start) - start).TotalSeconds;
|
|
return Build(name, start, durationS, "unknown", records, device: string.Empty);
|
|
}
|
|
|
|
internal static ActivityFacts Build(string fileId, DateTime start, int durationS,
|
|
string type, List<(DateTime time, int hr)> records, string device)
|
|
{
|
|
var facts = new Dictionary<string, object?>
|
|
{
|
|
["schema"] = "workout-facts/1",
|
|
["session"] = new Dictionary<string, object?>
|
|
{
|
|
["type"] = type,
|
|
["start"] = start.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'"),
|
|
["duration_s"] = durationS,
|
|
},
|
|
["laps"] = Array.Empty<object>(),
|
|
["records"] = records.Select(r => new Dictionary<string, object?>
|
|
{
|
|
["t_offset_s"] = (int)(r.time - start).TotalSeconds,
|
|
["hr"] = r.hr,
|
|
}).ToList(),
|
|
["sets"] = Array.Empty<object>(),
|
|
["fit_file_id"] = fileId,
|
|
["device"] = device,
|
|
};
|
|
int? avgHr = records.Count == 0
|
|
? null
|
|
: (int)Math.Round(records.Average(r => r.hr), MidpointRounding.AwayFromZero);
|
|
return new ActivityFacts(
|
|
System.Text.Json.JsonSerializer.Serialize(facts),
|
|
start, avgHr, null, fileId, device);
|
|
}
|
|
|
|
private static DateTime? ParseStart(string? text) =>
|
|
text != null && DateTime.TryParse(text, out var value) ? value.ToUniversalTime() : null;
|
|
}
|