From 718a199e56d1ad2a6477812637e00095e7138773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20K=C3=B6nig?= Date: Sun, 8 Jan 2023 14:00:40 +0100 Subject: [PATCH] #2 Updated unittests and README.md --- README.md | 56 +++++++++++++++++-- .../FileLoad/GpxFileCustomExtensionsTests.cs | 48 ++++++++++++++++ .../FileLoad/GpxFileLoadAsyncTests.cs | 37 +++++++++--- .../FileLoad/GpxFileLoadTests.cs | 37 +++++++++--- src/RolandK.Formats.Gpx/GpxFile.cs | 35 ++---------- 5 files changed, 164 insertions(+), 49 deletions(-) create mode 100644 src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileCustomExtensionsTests.cs diff --git a/README.md b/README.md index 9c3d022..60e74c9 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,61 @@ # RolandK.Formats.Gpx -### Common Information +## Common Information A .NET Standard library for reading and writing GPX (GPS Exchange Format) files. This library was build for my [GpxViewer](https://github.com/RolandKoenig/GpxViewer) project. It is based on the System.Xml.Serialization.XmlSerializer class and therefore available for .NET Framework and .NET Core projects. -### Build +## Build [![Continuous integration](https://github.com/RolandKoenig/RolandK.Formats.Gpx/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/RolandKoenig/RolandK.Formats.Gpx/actions/workflows/continuous-integration.yml) -### Feature overview +## Feature overview - Full document model for gpx files - Load gpx files - Write gpx files - - Add custom metadata to gpx files - - Don't lose other custom metadate after loading and saving gpx files \ No newline at end of file + - Add custom xml extensions to gpx files + - Don't lose other custom xml extensions information after loading and saving gpx files + +## Samples +### Load GPX file +Load file file (here Kösseine.gpx) and get total count of tracks, routes and waypoints in the file +```csharp +var gpxFile = await GpxFile.LoadAsync("Kösseine.gpx"); + +var countTracks = gpxFile.Tracks.Count; +var countRoutes = gpxFile.Routes.Count; +var countWaypoints = gpxFile.Waypoints.Count; +``` + +### Save GPX file +Save a previously loaded / created / modified file +```csharp +await GpxFile.SaveAsync(gpxFile, "MyFile.gpx"); +``` + +### Add custom xml extension +Your have to define your own xml extension types and give them a namespace +```csharp +[XmlType("MyTrackExtension", Namespace = "http://testing.rolandk.net/")] +public class MyTrackExtension +{ + public bool AlreadyDone { get; set; } = false; +} +``` + +Then you have to register these xml extension types and their namespaces to GpxFile. +You should do this somewhere in your startup code of your application. +```csharp +// You have to register your own xml extension types +GpxFile.RegisterExtensionType(typeof(MyTrackExtension)); +GpxFile.RegisterNamespace("rktest", "http://testing.rolandk.net/"); +``` + +Now you can access these xml extensions from c# code +```csharp +var gpxFile = await GpxFile.LoadAsync(inStream); +var gpxTrack = gpxFile.Tracks[0]; + +gpxTrack.Extensions ??= new GpxExtensions(); +var myTrackExtension = gpxTrack.Extensions.GetOrCreateExtension(); + +myTrackExtension.AlreadyDone = true; // This property comes from our own xml extension +``` \ No newline at end of file diff --git a/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileCustomExtensionsTests.cs b/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileCustomExtensionsTests.cs new file mode 100644 index 0000000..1fa65d1 --- /dev/null +++ b/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileCustomExtensionsTests.cs @@ -0,0 +1,48 @@ +using System.Xml.Serialization; + +namespace RolandK.Formats.Gpx.Tests.FileLoad; + +public class GpxFileCustomExtensionsTests +{ + [Fact] + public async Task AddCustomMetadata() + { + // Arrange + await using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( + typeof(GpxFileLoadTests),"Test_Gpx1_1.gpx"); + + // Act + GpxFile.RegisterExtensionType(typeof(MyTrackExtension)); + GpxFile.RegisterNamespace("rktest", "http://testing.rolandk.net/"); + + var originalGpxFile = await GpxFile.LoadAsync(inStream); + var originalGpxTrack = originalGpxFile.Tracks[0]; + + originalGpxTrack.Extensions ??= new GpxExtensions(); + var myTrackExtension = originalGpxTrack.Extensions.GetOrCreateExtension(); + + myTrackExtension.AlreadyDone = true; + + using var writingMemoryStream = new MemoryStream(1024 * 100); + await GpxFile.SaveAsync(originalGpxFile, writingMemoryStream); + + using var readingMemoryStream = new MemoryStream(writingMemoryStream.GetBuffer()); + var reloadedFile = await GpxFile.LoadAsync(readingMemoryStream); + + // Assert + Assert.Single(reloadedFile.Tracks); + + var reloadedTrack = reloadedFile.Tracks[0]; + Assert.NotNull(reloadedTrack.Extensions); + + var reloadedExtension = reloadedTrack.Extensions.TryGetSingleExtension(); + Assert.NotNull(reloadedExtension); + Assert.True(reloadedExtension.AlreadyDone); + } + + [XmlType("MyTrackExtension", Namespace = "http://testing.rolandk.net/")] + public class MyTrackExtension + { + public bool AlreadyDone { get; set; } + } +} diff --git a/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadAsyncTests.cs b/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadAsyncTests.cs index f749139..13ae951 100644 --- a/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadAsyncTests.cs +++ b/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadAsyncTests.cs @@ -1,71 +1,95 @@ -using System.Text; +using System.Diagnostics.CodeAnalysis; +using System.Text; namespace RolandK.Formats.Gpx.Tests.FileLoad; +[SuppressMessage("ReSharper", "RedundantArgumentDefaultValue")] public class GpxFileLoadAsyncTests { [Fact] public async Task GpxVersion1_1_CompatibilityMode() { + // Arrange await using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_1.gpx"); + // Act var gpxFile = await GpxFile.LoadAsync(inStream, GpxFileDeserializationMethod.Compatibility); + // Assert Assert.NotNull(gpxFile); Assert.NotNull(gpxFile.Metadata); - Assert.Equal("Kösseine", gpxFile!.Metadata!.Name); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); } [Fact] public async Task GpxVersion1_1_Gpx1_1Mode() { + // Arrange await using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_1.gpx"); + // Act var gpxFile = await GpxFile.LoadAsync(inStream, GpxFileDeserializationMethod.OnlyGpx1_1); + // Assert Assert.NotNull(gpxFile); Assert.NotNull(gpxFile.Metadata); - Assert.Equal("Kösseine", gpxFile!.Metadata!.Name); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); } [Fact] public async Task GpxVersion1_1_on_xml_1_1() { + // Arrange await using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_1_on_xml_1_1.gpx"); + // Act var gpxFile = await GpxFile.LoadAsync(inStream, GpxFileDeserializationMethod.Compatibility); + // Assert Assert.NotNull(gpxFile); Assert.NotNull(gpxFile.Metadata); - Assert.Equal("Kösseine", gpxFile!.Metadata!.Name); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); } [Fact] public async Task GpxVersion1_0() { + // Arrange await using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_0.gpx"); + // Act var gpxFile = await GpxFile.LoadAsync(inStream, GpxFileDeserializationMethod.Compatibility); + // Assert Assert.NotNull(gpxFile); Assert.NotNull(gpxFile.Metadata); - Assert.Equal("Kösseine", gpxFile!.Metadata!.Name); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); } [Fact] public async Task GpxVersion1_0_SaveAs1_1() { + // Arrange await using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_0.gpx"); + // Act var gpxFile = await GpxFile.LoadAsync(inStream, GpxFileDeserializationMethod.Compatibility); var outStrBuilder = new StringBuilder(33000); using (var strWriter = new StringWriter(outStrBuilder)) @@ -74,11 +98,10 @@ public async Task GpxVersion1_0_SaveAs1_1() } var writtenFile = outStrBuilder.ToString(); - // Check output + // Assert Assert.True(writtenFile.Contains("version=\"1.1\""), "Version attribute"); Assert.True(writtenFile.Contains("xmlns=\"http://www.topografix.com/GPX/1/1\""), "Default namespace"); - // Check original data Assert.Equal("1.0", gpxFile.Version); } } \ No newline at end of file diff --git a/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadTests.cs b/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadTests.cs index 9ac0da7..2b698d9 100644 --- a/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadTests.cs +++ b/src/RolandK.Formats.Gpx.Tests/FileLoad/GpxFileLoadTests.cs @@ -1,71 +1,95 @@ -using System.Text; +using System.Diagnostics.CodeAnalysis; +using System.Text; namespace RolandK.Formats.Gpx.Tests.FileLoad; +[SuppressMessage("ReSharper", "RedundantArgumentDefaultValue")] public class GpxFileLoadTests { [Fact] public void GpxVersion1_1_CompatibilityMode() { + // Arrange using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_1.gpx"); + // Act var gpxFile = GpxFile.Load(inStream, GpxFileDeserializationMethod.Compatibility); + // Assert Assert.NotNull(gpxFile); Assert.NotNull(gpxFile.Metadata); - Assert.Equal("Kösseine", gpxFile!.Metadata!.Name); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); } [Fact] public void GpxVersion1_1_Gpx1_1Mode() { + // Arrange using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_1.gpx"); + // Act var gpxFile = GpxFile.Load(inStream, GpxFileDeserializationMethod.OnlyGpx1_1); + // Assert Assert.NotNull(gpxFile); Assert.NotNull(gpxFile.Metadata); - Assert.Equal("Kösseine", gpxFile!.Metadata!.Name); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); } [Fact] public void GpxVersion1_1_on_xml_1_1() { + // Arrange using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_1_on_xml_1_1.gpx"); + // Act var gpxFile = GpxFile.Load(inStream, GpxFileDeserializationMethod.Compatibility); + // Assert Assert.NotNull(gpxFile); Assert.NotNull(gpxFile.Metadata); - Assert.Equal("Kösseine", gpxFile!.Metadata!.Name); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); } [Fact] public void GpxVersion1_0() { + // Arrange using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_0.gpx"); + // Act var gpxFile = GpxFile.Load(inStream, GpxFileDeserializationMethod.Compatibility); + // Assert Assert.NotNull(gpxFile); Assert.NotNull(gpxFile.Metadata); - Assert.Equal("Kösseine", gpxFile!.Metadata!.Name); + Assert.Equal("Kösseine", gpxFile.Metadata.Name); Assert.Single(gpxFile.Tracks); + Assert.Single(gpxFile.Tracks[0].Segments); + Assert.Equal(228, gpxFile.Tracks[0].Segments[0].Points.Count); } [Fact] public void GpxVersion1_0_SaveAs1_1() { + // Arrange using var inStream = GpxTestUtilities.ReadFromEmbeddedResource( typeof(GpxFileLoadTests),"Test_Gpx1_0.gpx"); + // Act var gpxFile = GpxFile.Load(inStream, GpxFileDeserializationMethod.Compatibility); var outStrBuilder = new StringBuilder(33000); using (var strWriter = new StringWriter(outStrBuilder)) @@ -74,11 +98,10 @@ public void GpxVersion1_0_SaveAs1_1() } var writtenFile = outStrBuilder.ToString(); - // Check output + // Assert Assert.True(writtenFile.Contains("version=\"1.1\""), "Version attribute"); Assert.True(writtenFile.Contains("xmlns=\"http://www.topografix.com/GPX/1/1\""), "Default namespace"); - // Check original data Assert.Equal("1.0", gpxFile.Version); } } \ No newline at end of file diff --git a/src/RolandK.Formats.Gpx/GpxFile.cs b/src/RolandK.Formats.Gpx/GpxFile.cs index dca17bd..86253e1 100644 --- a/src/RolandK.Formats.Gpx/GpxFile.cs +++ b/src/RolandK.Formats.Gpx/GpxFile.cs @@ -45,31 +45,6 @@ static GpxFile() s_cachedSerializer = new ConcurrentDictionary(); } - public GpxTrack CreateAndAddDummyTrack(string name, params GpxWaypoint[] waypoints) - { - var gpxTrack = new GpxTrack(); - gpxTrack.Name = name; - this.Tracks.Add(gpxTrack); - - var gpxTrackSegment = new GpxTrackSegment(); - gpxTrack.Segments.Add(gpxTrackSegment); - - gpxTrackSegment.Points.AddRange(waypoints); - - return gpxTrack; - } - - public GpxRoute CreateAndAddDummyRoute(string name, params GpxWaypoint[] waypoints) - { - var gpxRoute = new GpxRoute(); - gpxRoute.Name = name; - this.Routes.Add(gpxRoute); - - gpxRoute.RoutePoints.AddRange(waypoints); - - return gpxRoute; - } - public void EnsureNamespaceDeclarations() { if(s_extensionNamespaces != null) @@ -226,7 +201,7 @@ private static GpxFile PrepareGpxFileForSaving(GpxFile originalFile) return fileToSave; } - public static GpxFile Load(TextReader textReader, GpxFileDeserializationMethod method) + public static GpxFile Load(TextReader textReader, GpxFileDeserializationMethod method = GpxFileDeserializationMethod.Compatibility) { switch (method) { @@ -320,28 +295,28 @@ public static async Task LoadAsync(TextReader textReader, GpxFileDeseri } } - public static GpxFile Load(Stream stream, GpxFileDeserializationMethod method) + public static GpxFile Load(Stream stream, GpxFileDeserializationMethod method = GpxFileDeserializationMethod.Compatibility) { using var streamReader = new StreamReader(stream); return Load(streamReader, method); } - public static async Task LoadAsync(Stream stream, GpxFileDeserializationMethod method) + public static async Task LoadAsync(Stream stream, GpxFileDeserializationMethod method = GpxFileDeserializationMethod.Compatibility) { using var streamReader = new StreamReader(stream); return await LoadAsync(streamReader, method); } - public static GpxFile Load(string sourceFile, GpxFileDeserializationMethod method) + public static GpxFile Load(string sourceFile, GpxFileDeserializationMethod method = GpxFileDeserializationMethod.Compatibility) { using var fileStream = File.OpenRead(sourceFile); return Load(fileStream, method); } - public static async Task LoadAsync(string sourceFile, GpxFileDeserializationMethod method) + public static async Task LoadAsync(string sourceFile, GpxFileDeserializationMethod method = GpxFileDeserializationMethod.Compatibility) { using var fileStream = File.OpenRead(sourceFile);