Przeglądaj źródła

Processing GPX tracks to extract stats

Lukas Angerer 3 lat temu
rodzic
commit
8158f07f96

+ 3 - 0
src/RunnersMeet.Server/AppServer.cs

@@ -1,5 +1,6 @@
 using Microsoft.AspNetCore.Authentication.JwtBearer;
 using Microsoft.IdentityModel.Tokens;
+using RunnersMeet.Server.GpxFormat;
 using RunnersMeet.Server.Persistence;
 
 namespace RunnersMeet.Server;
@@ -74,6 +75,8 @@ public class AppServer
 		services.AddSingleton<IFileStorage, FileStorage>();
 		services.AddScoped<QueryFactory, QueryFactory>();
 
+		services.AddSingleton<GpxParser>();
+
 		services.Configure<PersistenceOptions>(config.GetSection(PersistenceOptions.Persistence));
 		services.AddTransient<IPersistenceOptions, PersistenceOptionsAccessor>();
 	}

+ 6 - 2
src/RunnersMeet.Server/Controllers/TracksController.cs

@@ -2,6 +2,7 @@ using System.Security.Claims;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using RunnersMeet.Server.Domain;
+using RunnersMeet.Server.GpxFormat;
 using RunnersMeet.Server.Persistence;
 
 namespace RunnersMeet.Server.Controllers;
@@ -12,11 +13,13 @@ namespace RunnersMeet.Server.Controllers;
 public class TracksController : ControllerBase
 {
 	private readonly IFileStorage _fileStorage;
+	private readonly GpxParser _gpxParser;
 	private readonly QueryFactory _queryFactory;
 
-	public TracksController(IFileStorage fileStorage, QueryFactory queryFactory)
+	public TracksController(IFileStorage fileStorage, GpxParser gpxParser, QueryFactory queryFactory)
 	{
 		_fileStorage = fileStorage;
+		_gpxParser = gpxParser;
 		_queryFactory = queryFactory;
 	}
 
@@ -31,8 +34,9 @@ public class TracksController : ControllerBase
 	{
 		var fileName = await _fileStorage.UploadFileAsync(file, name, cancellationToken);
 
+		var gpxSummary = _gpxParser.ExtractSummary(_fileStorage.OpenFileRead(fileName));
 		var userId = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "<unknown>";
-		var track = _queryFactory.CreateTrackCommand().Create(userId, fileName);
+		var track = _queryFactory.CreateTrackCommand().Create(userId, fileName, gpxSummary);
 
 		return Ok(track);
 	}

+ 38 - 0
src/RunnersMeet.Server/GpxFormat/GpsPoint.cs

@@ -0,0 +1,38 @@
+namespace RunnersMeet.Server.GpxFormat;
+
+public struct GpsPoint
+{
+	/// <summary>
+	/// Earth radius in meters
+	/// </summary>
+	public const int Radius = 6371000;
+
+	public double Lat { get; } = 0;
+	public double Lon { get; } = 0;
+	public double Elevation { get; }
+
+	public GpsPoint(double latDeg, double lonDeg, double elevation)
+	{
+		Lat = latDeg * Math.PI / 180;
+		Lon = lonDeg * Math.PI / 180;
+		Elevation = elevation;
+	}
+
+	public double Distance(GpsPoint other)
+	{
+		var deltaLat = other.Lat - Lat;
+		var deltaLon = other.Lon - Lon;
+
+		var a = Math.Pow(Math.Sin(deltaLat / 2.0), 2)
+		        + Math.Cos(Lat) * Math.Cos(other.Lat) * Math.Pow(Math.Sin(deltaLon / 2), 2);
+
+		var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
+
+		return Radius * c;
+	}
+
+	public double ElevationGain(GpsPoint other)
+	{
+		return other.Elevation - Elevation;
+	}
+}

+ 14 - 0
src/RunnersMeet.Server/GpxFormat/GpxParser.cs

@@ -0,0 +1,14 @@
+using System.Xml;
+
+namespace RunnersMeet.Server.GpxFormat;
+
+public class GpxParser
+{
+	public GpxSummary ExtractSummary(Stream gpxStream)
+	{
+		var doc = new XmlDocument();
+		doc.Load(gpxStream);
+
+		return new GpxSummary(doc);
+	}
+}

+ 58 - 0
src/RunnersMeet.Server/GpxFormat/GpxSummary.cs

@@ -0,0 +1,58 @@
+using System.Xml;
+
+namespace RunnersMeet.Server.GpxFormat;
+
+public class GpxSummary
+{
+	public string TrackName { get; }
+	public string Creator { get; }
+	public double Distance { get; }
+	public double ElevationUp { get; }
+	public double ElevationDown { get; }
+
+	public GpxSummary(XmlDocument gpxDoc)
+	{
+		if (gpxDoc.DocumentElement == null)
+		{
+			throw new ArgumentException("GPX document must have a root 'gpx' element");
+		}
+
+		var nsManager = new XmlNamespaceManager(gpxDoc.NameTable);
+		nsManager.AddNamespace("ns", gpxDoc.DocumentElement.NamespaceURI);
+
+		Creator = gpxDoc.DocumentElement.GetAttribute("creator");
+		TrackName = gpxDoc.SelectSingleNode("/ns:gpx/ns:trk/ns:name", nsManager)?.InnerText ?? String.Empty;
+		Distance = 0;
+		ElevationUp = 0;
+		ElevationDown = 0;
+
+		var track = gpxDoc.SelectSingleNode("/ns:gpx/ns:trk", nsManager);
+		if (track != null)
+		{
+			GpsPoint? previous = null;
+			foreach (XmlElement point in track.SelectNodes("//ns:trkpt", nsManager)?.Cast<XmlElement>() ?? Enumerable.Empty<XmlElement>())
+			{
+				var current = new GpsPoint(
+					Double.Parse(point.GetAttribute("lat")),
+					Double.Parse(point.GetAttribute("lon")),
+					Double.Parse(point.SelectSingleNode("ns:ele", nsManager)?.InnerText ?? "0"));
+
+				if (previous != null)
+				{
+					Distance += previous.Value.Distance(current);
+					var elevationGain = previous.Value.ElevationGain(current);
+					if (elevationGain > 0)
+					{
+						ElevationUp += elevationGain;
+					}
+					else if (elevationGain < 0)
+					{
+						ElevationDown -= elevationGain;
+					}
+				}
+
+				previous = current;
+			}
+		}
+	}
+}

+ 6 - 5
src/RunnersMeet.Server/Persistence/CreateTrackCommand.cs

@@ -1,4 +1,5 @@
 using RunnersMeet.Server.Domain;
+using RunnersMeet.Server.GpxFormat;
 
 namespace RunnersMeet.Server.Persistence;
 
@@ -11,16 +12,16 @@ public class CreateTrackCommand
 		_database = database;
 	}
 
-	public Track Create(string owner, FileName fileName)
+	public Track Create(string owner, FileName fileName, GpxSummary gpxSummary)
 	{
 		var track = new Track
 		{
 			Owner = owner,
 			FileHash = fileName.Hash,
-			DisplayName = fileName.DisplayName,
-			Distance = 0.0,
-			ElevationUp = 0.0,
-			ElevationDown = 0.0,
+			DisplayName = gpxSummary.TrackName,
+			Distance = gpxSummary.Distance,
+			ElevationUp = gpxSummary.ElevationUp,
+			ElevationDown = gpxSummary.ElevationDown,
 		};
 
 		_database.Tracks.Insert(track);

+ 6 - 0
src/RunnersMeet.Server/Persistence/FileStorage.cs

@@ -19,6 +19,12 @@ public class FileStorage : IFileStorage
 		return fileName;
 	}
 
+	public Stream OpenFileRead(FileName name)
+	{
+		var path = Path.Combine(_persistenceOptions.FileStorageRootPath, name.GetPath());
+		return File.OpenRead(path);
+	}
+
 	private FileStream CreateFile(FileName name)
 	{
 		var path = Path.Combine(_persistenceOptions.FileStorageRootPath, name.GetPath());

+ 1 - 0
src/RunnersMeet.Server/Persistence/IFileStorage.cs

@@ -3,4 +3,5 @@ namespace RunnersMeet.Server.Persistence;
 public interface IFileStorage
 {
 	public Task<FileName> UploadFileAsync(IFormFile file, string? displayName, CancellationToken cancellationToken = default);
+	public Stream OpenFileRead(FileName name);
 }

+ 4 - 0
src/RunnersMeet.Server/RunnersMeet.Server.csproj

@@ -20,4 +20,8 @@
       </Content>
     </ItemGroup>
 
+    <ItemGroup>
+      <Folder Include="data\files" />
+    </ItemGroup>
+
 </Project>