diff --git a/MapControl/Shared/LocationCollection.cs b/MapControl/Shared/LocationCollection.cs index eda679f4..52733042 100644 --- a/MapControl/Shared/LocationCollection.cs +++ b/MapControl/Shared/LocationCollection.cs @@ -9,7 +9,7 @@ using System.Linq; namespace MapControl { /// - /// A collection of Locations with support for parsing. + /// A collection of Locations with support for string parsing and calculation of great circle and rhumb line locations. /// #if !WINDOWS_UWP [System.ComponentModel.TypeConverter(typeof(LocationCollectionConverter))] @@ -30,11 +30,155 @@ namespace MapControl { } + public void Add(double latitude, double longitude) + { + if (Count > 0) + { + var deltaLon = longitude - this[Count - 1].Longitude; + + if (deltaLon < -180d) + { + longitude += 360d; + } + else if (deltaLon > 180) + { + longitude -= 360; + } + } + + Add(new Location(latitude, longitude)); + } + public static LocationCollection Parse(string s) { var strings = s.Split(new char[] { ' ', ';' }, StringSplitOptions.RemoveEmptyEntries); return new LocationCollection(strings.Select(l => Location.Parse(l))); } + + /// + /// see https://en.wikipedia.org/wiki/Great-circle_navigation + /// + public static LocationCollection CalculateGreatCircleLocations(Location location1, Location location2, double resolution = 1d) + { + if (resolution <= 0d) + { + throw new ArgumentOutOfRangeException( + nameof(resolution), "The parameter resolution must be greater than zero."); + } + + resolution *= Math.PI / 180d; + + var lat1 = location1.Latitude * Math.PI / 180d; + var lon1 = location1.Longitude * Math.PI / 180d; + var lat2 = location2.Latitude * Math.PI / 180d; + var lon2 = location2.Longitude * Math.PI / 180d; + var cosLat1 = Math.Cos(lat1); + var sinLat1 = Math.Sin(lat1); + var cosLat2 = Math.Cos(lat2); + var sinLat2 = Math.Sin(lat2); + var cosLon12 = Math.Cos(lon2 - lon1); + var sinLon12 = Math.Sin(lon2 - lon1); + + var a = cosLat1 * sinLat2 - sinLat1 * cosLat2 * cosLon12; + var b = cosLat2 * sinLon12; + var s12 = Math.Atan2(Math.Sqrt(a * a + b * b), sinLat1 * sinLat2 + cosLat1 * cosLat2 * cosLon12); + + var locations = new LocationCollection(new Location(location1.Latitude, location1.Longitude)); + + if (s12 > resolution) + { + var n = (int)Math.Round(s12 / resolution); + + var az1 = Math.Atan2(sinLon12, cosLat1 * sinLat2 / cosLat2 - sinLat1 * cosLon12); + var cosAz1 = Math.Cos(az1); + var sinAz1 = Math.Sin(az1); + + var az0 = Math.Atan2(sinAz1 * cosLat1, Math.Sqrt(cosAz1 * cosAz1 + sinAz1 * sinAz1 * sinLat1 * sinLat1)); + var sinAz0 = Math.Sin(az0); + var cosAz0 = Math.Cos(az0); + + var s01 = Math.Atan2(sinLat1, cosLat1 * cosAz1); + var lon0 = lon1 - Math.Atan2(sinAz0 * Math.Sin(s01), Math.Cos(s01)); + + for (int i = 1; i < n; i++) + { + double s = s01 + i * s12 / n; + double sinS = Math.Sin(s); + double cosS = Math.Cos(s); + double lat = Math.Atan2(cosAz0 * sinS, Math.Sqrt(cosS * cosS + sinAz0 * sinAz0 * sinS * sinS)); + double lon = Math.Atan2(sinAz0 * sinS, cosS) + lon0; + + locations.Add(lat * 180d / Math.PI, lon * 180d / Math.PI); + } + } + + locations.Add(location2.Latitude, location2.Longitude); + + return locations; + } + + /// + /// see https://en.wikipedia.org/wiki/Rhumb_line + /// + public static LocationCollection CalculateRhumbLineLocations(Location location1, Location location2, double resolution = 1d) + { + if (resolution <= 0d) + { + throw new ArgumentOutOfRangeException( + nameof(resolution), "The parameter resolution must be greater than zero."); + } + + resolution *= Math.PI / 180d; + + var lat1 = location1.Latitude; + var lat2 = location2.Latitude; + var y1 = WebMercatorProjection.LatitudeToY(lat1); + var y2 = WebMercatorProjection.LatitudeToY(lat2); + + if (double.IsInfinity(y1)) + { + throw new ArgumentOutOfRangeException( + nameof(location1), "The parameter location1 must have an absolute latitude value of less than 90 degrees."); + } + + if (double.IsInfinity(y2)) + { + throw new ArgumentOutOfRangeException( + nameof(location2), "The parameter location2 must have an absolute latitude value of less than 90 degrees."); + } + + var lon1 = location1.Longitude; + var lon2 = location2.Longitude; + var dlat = lat2 - lat1; + var dlon = lon2 - lon1; + var dy = y2 - y1; + + // sec(beta) = 1 / cos(atan(dx,dy)) = sqrt(1 + (dx/dy)^2) + var sec = Math.Sqrt(1d + dlon * dlon / (dy * dy)); + + var s12 = sec < 1000d + ? Math.Abs(dlat * Math.PI / 180d * sec) + : Math.Abs(dlon * Math.PI / 180d); + + var locations = new LocationCollection(new Location(lat1, lon1)); + + if (s12 > resolution) + { + var n = (int)Math.Round(s12 / resolution); + + for (int i = 1; i < n; i++) + { + double lon = lon1 + i * dlon / n; + double lat = WebMercatorProjection.YToLatitude(y1 + i * dy / n); + + locations.Add(lat, lon); + } + } + + locations.Add(lat2, lon2); + + return locations; + } } } diff --git a/MapControl/Shared/LocationEx.cs b/MapControl/Shared/LocationEx.cs index c40e4515..57e35352 100644 --- a/MapControl/Shared/LocationEx.cs +++ b/MapControl/Shared/LocationEx.cs @@ -3,7 +3,6 @@ // Licensed under the Microsoft Public License (Ms-PL) using System; -using System.Linq; namespace MapControl { @@ -15,7 +14,8 @@ namespace MapControl /// /// see https://en.wikipedia.org/wiki/Great-circle_navigation /// - public static double GreatCircleDistance(this Location location1, Location location2, double earthRadius = MapProjection.Wgs84EquatorialRadius) + public static double GreatCircleDistance( + this Location location1, Location location2, double earthRadius = MapProjection.Wgs84EquatorialRadius) { var lat1 = location1.Latitude * Math.PI / 180d; var lon1 = location1.Longitude * Math.PI / 180d; @@ -37,7 +37,8 @@ namespace MapControl /// /// see https://en.wikipedia.org/wiki/Great-circle_navigation /// - public static Location GreatCircleLocation(this Location location, double azimuth, double distance, double earthRadius = MapProjection.Wgs84EquatorialRadius) + public static Location GreatCircleLocation( + this Location location, double azimuth, double distance, double earthRadius = MapProjection.Wgs84EquatorialRadius) { var s12 = distance / earthRadius; var az1 = azimuth * Math.PI / 180d; @@ -54,151 +55,5 @@ namespace MapControl return new Location(lat2 * 180d / Math.PI, lon2 * 180d / Math.PI); } - - public static LocationCollection CalculateMeridianLocations(this Location location, double latitude2, double resolution = 1d) - { - if (resolution <= 0d) - { - throw new ArgumentOutOfRangeException("The parameter resolution must be greater than zero."); - } - - var locations = new LocationCollection(); - var s = latitude2 - location.Latitude; - var n = (int)Math.Ceiling(Math.Abs(s) / resolution); - - for (int i = 0; i <= n; i++) - { - locations.Add(new Location(location.Latitude + i * s / n, location.Longitude)); - } - - return locations; - } - - /// - /// see https://en.wikipedia.org/wiki/Great-circle_navigation - /// - public static LocationCollection CalculateGreatCircleLocations(this Location location1, Location location2, double resolution = 1d) - { - if (resolution <= 0d) - { - throw new ArgumentOutOfRangeException("The parameter resolution must be greater than zero."); - } - - if (location1.Longitude == location2.Longitude || - location1.Latitude <= -90d || location1.Latitude >= 90d || - location2.Latitude <= -90d || location2.Latitude >= 90d) - { - return CalculateMeridianLocations(location1, location2.Latitude); - } - - var locations = new LocationCollection(new Location(location1.Latitude, location1.Longitude)); - - var lat1 = location1.Latitude * Math.PI / 180d; - var lon1 = location1.Longitude * Math.PI / 180d; - var lat2 = location2.Latitude * Math.PI / 180d; - var lon2 = location2.Longitude * Math.PI / 180d; - var cosLat1 = Math.Cos(lat1); - var sinLat1 = Math.Sin(lat1); - var cosLat2 = Math.Cos(lat2); - var sinLat2 = Math.Sin(lat2); - var cosLon12 = Math.Cos(lon2 - lon1); - var sinLon12 = Math.Sin(lon2 - lon1); - - var cosS12 = sinLat1 * sinLat2 + cosLat1 * cosLat2 * cosLon12; - var s12 = Math.Acos(Math.Min(Math.Max(cosS12, -1d), 1d)); - var n = (int)Math.Ceiling(s12 / resolution * 180d / Math.PI); - - if (n > 1) - { - var az1 = Math.Atan2(sinLon12, cosLat1 * sinLat2 / cosLat2 - sinLat1 * cosLon12); - var cosAz1 = Math.Cos(az1); - var sinAz1 = Math.Sin(az1); - - var az0 = Math.Atan2(sinAz1 * cosLat1, Math.Sqrt(cosAz1 * cosAz1 + sinAz1 * sinAz1 * sinLat1 * sinLat1)); - var sinAz0 = Math.Sin(az0); - var cosAz0 = Math.Cos(az0); - - var s01 = Math.Atan2(sinLat1, cosLat1 * cosAz1); - var lon0 = lon1 - Math.Atan2(sinAz0 * Math.Sin(s01), Math.Cos(s01)); - - for (int i = 1; i < n; i++) - { - double s = s01 + i * s12 / n; - double sinS = Math.Sin(s); - double cosS = Math.Cos(s); - double lat = Math.Atan2(cosAz0 * sinS, Math.Sqrt(cosS * cosS + sinAz0 * sinAz0 * sinS * sinS)); - double lon = Math.Atan2(sinAz0 * sinS, cosS) + lon0; - - locations.Add(lat * 180d / Math.PI, lon * 180d / Math.PI); - } - } - - locations.Add(location2.Latitude, location2.Longitude); - return locations; - } - - /// - /// see https://en.wikipedia.org/wiki/Rhumb_line - /// - public static LocationCollection CalculateRhumbLineLocations(this Location location1, Location location2, double resolution = 1d) - { - if (resolution <= 0d) - { - throw new ArgumentOutOfRangeException("The parameter resolution must be greater than zero."); - } - - var y1 = WebMercatorProjection.LatitudeToY(location1.Latitude); - - if (double.IsInfinity(y1)) - { - throw new ArgumentOutOfRangeException("The parameter location1 must have an absolute latitude value of less than 90 degrees."); - } - - var y2 = WebMercatorProjection.LatitudeToY(location2.Latitude); - - if (double.IsInfinity(y2)) - { - throw new ArgumentOutOfRangeException("The parameter location2 must have an absolute latitude value of less than 90 degrees."); - } - - var x1 = location1.Longitude; - var x2 = location2.Longitude; - var dx = x2 - x1; - var dy = y2 - y1; - var s = Math.Sqrt(dx * dx + dy * dy); - var n = (int)Math.Ceiling(s / resolution); - - var locations = new LocationCollection(new Location(location1.Latitude, location1.Longitude)); - - for (int i = 1; i < n; i++) - { - double x = x1 + i * dx / n; - double y = y1 + i * dy / n; - - locations.Add(WebMercatorProjection.YToLatitude(y), x); - } - - locations.Add(location2.Latitude, location2.Longitude); - return locations; - } - - public static void Add(this LocationCollection locations, double latitude, double longitude) - { - if (locations.Count > 0) - { - var deltaLon = longitude - locations.Last().Longitude; - - if (deltaLon < -180d) - { - longitude += 360d; - } - else if (deltaLon > 180) - { - longitude -= 360; - } - } - - locations.Add(new Location(latitude, longitude)); - } } }