Skip to content

Commit

Permalink
2024.46
Browse files Browse the repository at this point in the history
- Coordinate now implements IParsable
- GpsTrack is now an indexer for IGpsPoint
- IGpsInput.CombineTracks now GpsTrack.CombineTracks
- CombineTracks now accepts params instead of List
- IGpsInput.CombineTrackNames and IsValidTrackIndex now part of TrackInfo
- Tsv (tab-separated values) output format support added
- SaveableBase initializes the document once, rather than each access
- Many SaveableAndTransformableBase properties given default values
- Fixed RandomPointBase.GenerateRandomCoordinates randomizer algorithm
  • Loading branch information
simon-techkid committed May 22, 2024
1 parent 7d5539c commit c726835
Show file tree
Hide file tree
Showing 14 changed files with 425 additions and 206 deletions.
130 changes: 111 additions & 19 deletions SpotifyGPX/Coordinate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ namespace SpotifyGPX;
IEquatable<Coordinate>,
IComparable,
IComparable<Coordinate>,
IFormattable
IFormattable,
IParsable<Coordinate>
{
/// <summary>
/// Creates a coordinate object with a latitude and longitude.
Expand Down Expand Up @@ -96,12 +97,12 @@ public bool Equals(Coordinate other)
}

/// <summary>
/// Calculates the distance between two coordinates.
/// Calculates the distance between two coordinates, assuming the Earth is flat.
/// </summary>
/// <param name="c1">The first coordinate.</param>
/// <param name="c2">The second coordinate.</param>
/// <returns>A double representing the distance between the two coordinates.</returns>
public static double CalculateDistance(Coordinate c1, Coordinate c2)
/// <returns>A <see langword="double"/> representing the flat distance between the two coordinates.</returns>
public static double operator %(Coordinate c1, Coordinate c2)
{
double latDiff = c2.Latitude - c1.Latitude;
double lonDiff = c2.Longitude - c1.Longitude;
Expand All @@ -111,6 +112,51 @@ public static double CalculateDistance(Coordinate c1, Coordinate c2)
return distance;
}

/// <summary>
/// Calculates the distance between two coordinates using the Haversine formula.
/// </summary>
/// <param name="c1">The first coordinate.</param>
/// <param name="c2">The second coordinate.</param>
/// <returns>A <see langword="double"/> representing the distance (in kilometers) between the two coordinates.</returns>
public static double operator ^(Coordinate c1, Coordinate c2)
{
const double R = 6371; // Radius of the Earth in km
double lat1Rad = ToRadians(c1.Latitude);
double lat2Rad = ToRadians(c2.Latitude);
double deltaLat = ToRadians(c2.Latitude - c1.Latitude);
double deltaLon = ToRadians(c2.Longitude - c1.Longitude);

double a = Math.Sin(deltaLat / 2) * Math.Sin(deltaLat / 2) +
Math.Cos(lat1Rad) * Math.Cos(lat2Rad) *
Math.Sin(deltaLon / 2) * Math.Sin(deltaLon / 2);
double c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));

return R * c; // Distance in km
}

/// <summary>
/// Calculates the bearing between two coordinates.
/// </summary>
/// <param name="c1">The first coordinate.</param>
/// <param name="c2">The second coordinate.</param>
/// <returns>A <see langword="double"/> representing the bearing (in degrees) between the first and the second coordinates.</returns>
public static double operator /(Coordinate c1, Coordinate c2)
{
double lat1Rad = ToRadians(c1.Latitude);
double lat2Rad = ToRadians(c2.Latitude);
double lon1Rad = ToRadians(c1.Longitude);
double lon2Rad = ToRadians(c2.Longitude);

double y = Math.Sin(lon2Rad - lon1Rad) * Math.Cos(lat2Rad);
double x = Math.Cos(lat1Rad) * Math.Sin(lat2Rad) -
Math.Sin(lat1Rad) * Math.Cos(lat2Rad) * Math.Cos(lon2Rad - lon1Rad);

double bearingRad = Math.Atan2(y, x);
double bearingDeg = (bearingRad * 180.0 / Math.PI + 360.0) % 360.0; // Convert radians to degrees and normalize

return bearingDeg;
}

public object Clone()
{
return new Coordinate(Latitude, Longitude);
Expand All @@ -128,30 +174,77 @@ public int CompareTo(object? obj)

public int CompareTo(Coordinate other)
{
var latComparison = Latitude.CompareTo(other.Latitude);
if (latComparison != 0) return latComparison;
return Longitude.CompareTo(other.Longitude);
return (Latitude + Longitude).CompareTo(other.Latitude + other.Longitude);
}

public string ToString(string? format, IFormatProvider? formatProvider)
{
return $"Latitude: {Latitude.ToString(format, formatProvider)}, Longitude: {Longitude.ToString(format, formatProvider)}";
}

public double CalculateDistance(Coordinate other)
/// <summary>
/// Parses a string representation of a coordinate in the format "latitude,longitude".
/// </summary>
/// <param name="input">The string representation of the coordinate.</param>
/// <returns>A <see cref="Coordinate"/> object parsed from the input string.</returns>
public static Coordinate Parse(string input, IFormatProvider? provider)
{
const double R = 6371; // Radius of the Earth in km
double lat1Rad = ToRadians(Latitude);
double lat2Rad = ToRadians(other.Latitude);
double deltaLat = ToRadians(other.Latitude - Latitude);
double deltaLon = ToRadians(other.Longitude - Longitude);
if (string.IsNullOrWhiteSpace(input))
{
throw new ArgumentException("Input string cannot be null or empty.");
}

double a = Math.Sin(deltaLat / 2) * Math.Sin(deltaLat / 2) +
Math.Cos(lat1Rad) * Math.Cos(lat2Rad) *
Math.Sin(deltaLon / 2) * Math.Sin(deltaLon / 2);
double c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
string[] parts = input.Split(',');

return R * c; // Distance in km
if (parts.Length != 2)
{
throw new FormatException("Input string must be in the format 'latitude,longitude'.");
}

if (!double.TryParse(parts[0], out double latitude) || !double.TryParse(parts[1], provider, out double longitude))
{
throw new FormatException("Latitude and longitude must be valid double values.");
}

return new Coordinate(latitude, longitude);
}

/// <summary>
/// Tries to parse a string representation of a coordinate in the format "latitude,longitude".
/// </summary>
/// <param name="input">The string representation of the coordinate.</param>
/// <param name="result">When this method returns, contains the parsed Coordinate object, if the parsing succeeds.</param>
/// <returns><see langword="true"/> if the parsing was successful; otherwise, <see langword="false"/>.</returns>
public static bool TryParse(string? input, IFormatProvider? provider, out Coordinate result)
{
result = default;

if (string.IsNullOrWhiteSpace(input))
{
return false;
}

string[] parts = input.Split(',');

if (parts.Length != 2)
{
return false;
}

if (!double.TryParse(parts[0], out double latitude) || !double.TryParse(parts[1], provider, out double longitude))
{
return false;
}

result = new Coordinate(latitude, longitude);
return true;
}

public static string CalculateCompassDirection(double bearingDeg)
{
string[] compassDirections = { "N", "NE", "E", "SE", "S", "SW", "W", "NW", "N" };
int index = (int)((bearingDeg + 22.5) / 45.0);
return compassDirections[index];
}

public static double ToRadians(double angle) => Math.PI * angle / 180.0;
Expand All @@ -162,5 +255,4 @@ public static bool IsWithinBounds(double latitude, double longitude) =>
latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180;

public bool IsWithinBounds() => IsWithinBounds(Latitude, Longitude);

}
56 changes: 46 additions & 10 deletions SpotifyGPX/GpsTrack.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SpotifyGPX by Simon Field

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

Expand Down Expand Up @@ -52,16 +53,6 @@ public GpsTrack(int? index, string? name, TrackType type, List<IGpsPoint> points
/// </summary>
public readonly DateTimeOffset End { get; } // What time was the latest point logged?

public IEnumerator<IGpsPoint> GetEnumerator()
{
return Points.GetEnumerator();
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}

/// <summary>
/// Converts this <see cref="GpsTrack"/> object to a string.
/// </summary>
Expand All @@ -78,4 +69,49 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()

return builder.ToString();
}

/// <summary>
/// Combine multiple tracks into a single track.
/// </summary>
/// <param name="allTracks">A variable number of GPXTrack objects.</param>
/// <returns>A single GPXTrack with data from each provided track.</returns>
/// <exception cref="Exception">No tracks provided to combine.</exception>
public static GpsTrack CombineTracks(params GpsTrack[] allTracks)
{
if (allTracks == null || allTracks.Length == 0)
{
throw new Exception("No tracks provided to combine!");
}

// Combine all points from all tracks
var combinedPoints = allTracks.SelectMany(track => track.Points);

// Create a new GpsTrack with combined points
GpsTrack combinedTrack = new(
allTracks.Length,
TrackInfo.CombineTrackNames(allTracks.First().Track, allTracks.Last().Track),
TrackType.Combined,
combinedPoints.ToList()
);

return combinedTrack;
}

public IGpsPoint this[int index]
{
get
{
return Points[index];
}
}

public IEnumerator<IGpsPoint> GetEnumerator()
{
return Points.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
44 changes: 3 additions & 41 deletions SpotifyGPX/Input/IGpsInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public List<GpsTrack> GetSelectedTracks()
{
// If the GPX contains more than one track, provide user parsing options:

GpsTrack combinedTrack = CombineTracks(AllTracks); // Generate a combined track (cohesive of all included tracks)
GpsTrack combinedTrack = GpsTrack.CombineTracks(AllTracks.ToArray()); // Generate a combined track (cohesive of all included tracks)
AllTracks = CalculateGaps(AllTracks); // Add gaps between tracks as track options
AllTracks.Add(combinedTrack); // Add the combined track to the end of the list

Expand Down Expand Up @@ -95,7 +95,7 @@ private static List<GpsTrack> HandleMultipleTracks(List<GpsTrack> allTracks)
while (true)
{
string input = Console.ReadLine() ?? string.Empty;
if (int.TryParse(input, out selectedTrackIndex) && IsValidTrackIndex(selectedTrackIndex, allTracks.Count))
if (int.TryParse(input, out selectedTrackIndex) && TrackInfo.IsValidTrackIndex(selectedTrackIndex, allTracks.Count))
{
break; // Return this selection below
}
Expand All @@ -116,28 +116,6 @@ private static List<GpsTrack> HandleMultipleTracks(List<GpsTrack> allTracks)
return selectedTracks;
}

/// <summary>
/// Combine a list of tracks into a single track.
/// </summary>
/// <param name="allTracks">A list of GPXTrack objects.</param>
/// <returns>A single GPXTrack with data from each in the list.</returns>
/// <exception cref="Exception">The list provided was null or contained no tracks.</exception>
private static GpsTrack CombineTracks(List<GpsTrack> allTracks)
{
if (allTracks == null || allTracks.Count == 0)
{
throw new Exception("No tracks provided to combine!");
}

// Combine all points from all tracks
var combinedPoints = allTracks.SelectMany(track => track.Points);

// Create a new GPXTrack with combined points
GpsTrack combinedTrack = new(allTracks.Count, CombinedOrGapTrackName(allTracks.First().Track, allTracks.Last().Track), TrackType.Combined, combinedPoints.ToList());

return combinedTrack;
}

/// <summary>
/// Calculate all the gaps between tracks.
/// </summary>
Expand All @@ -153,7 +131,7 @@ private static List<GpsTrack> CalculateGaps(List<GpsTrack> allTracks)
GpsTrack followingTrack = allTracks[index + 1]; // Get the track after the current track (next one)
IGpsPoint end = gpxTrack.Points.Last(); // Get the last point of the current track
IGpsPoint next = followingTrack.Points.First(); // Get the first point of the next track
string gapName = CombinedOrGapTrackName(gpxTrack.Track, followingTrack.Track); // Create a name for the gap track based on the name of the current track and next track
string gapName = TrackInfo.CombineTrackNames(gpxTrack.Track, followingTrack.Track); // Create a name for the gap track based on the name of the current track and next track
if (end.Time != next.Time)
{
Expand All @@ -171,22 +149,6 @@ private static List<GpsTrack> CalculateGaps(List<GpsTrack> allTracks)
.ToList();
}

/// <summary>
/// Creates a friendly name for a bridge track (combination or gap track) between GPXTrack objects.
/// </summary>
/// <param name="track1">The track before the break.</param>
/// <param name="track2">The track after the break.</param>
/// <returns>A name combining the names of the two given tracks.</returns>
private static string CombinedOrGapTrackName(TrackInfo track1, TrackInfo track2) => $"{track1.ToString()}-{track2.ToString()}";

/// <summary>
/// Determines whether a user-input track selection is valid.
/// </summary>
/// <param name="index">The user-provided index of a GPXTrack.</param>
/// <param name="totalTracks">The total number of tracks available for selection.</param>
/// <returns>True, if the user-provided index is an existing GPXTrack.</returns>
private static bool IsValidTrackIndex(int index, int totalTracks) => index >= 0 && index < totalTracks;

/// <summary>
/// The total number of GPS tracks in the source file.
/// </summary>
Expand Down
Loading

0 comments on commit c726835

Please sign in to comment.