From 54c924f9e7c0eca8b71379a28fee9dcc858f84ab Mon Sep 17 00:00:00 2001 From: ClemensF Date: Sun, 11 Feb 2018 19:46:31 +0100 Subject: [PATCH] Version 4.4.1: MapPolygon for UWP in progress --- FileDbCache/UWP/Properties/AssemblyInfo.cs | 4 +- FileDbCache/WPF/Properties/AssemblyInfo.cs | 4 +- MBTiles/UWP/Properties/AssemblyInfo.cs | 4 +- MBTiles/WPF/Properties/AssemblyInfo.cs | 4 +- MapControl/Shared/Intersections.cs | 105 +++++++++++++ MapControl/Shared/LocationEx.cs | 172 +++++++++++++++++++++ MapControl/Shared/MapShape.cs | 16 +- MapControl/UWP/MapControl.UWP.csproj | 7 + MapControl/UWP/MapPolygon.UWP.cs | 55 +++++++ MapControl/UWP/MapPolyline.UWP.cs | 60 +------ MapControl/UWP/MapShape.UWP.cs | 62 ++++++++ MapControl/UWP/Properties/AssemblyInfo.cs | 4 +- MapControl/WPF/MapControl.WPF.csproj | 6 + MapControl/WPF/MapGraticule.WPF.cs | 115 ++++++++------ MapControl/WPF/Properties/AssemblyInfo.cs | 4 +- 15 files changed, 497 insertions(+), 125 deletions(-) create mode 100644 MapControl/Shared/Intersections.cs create mode 100644 MapControl/Shared/LocationEx.cs create mode 100644 MapControl/UWP/MapPolygon.UWP.cs diff --git a/FileDbCache/UWP/Properties/AssemblyInfo.cs b/FileDbCache/UWP/Properties/AssemblyInfo.cs index d1482c16..a3e202bc 100644 --- a/FileDbCache/UWP/Properties/AssemblyInfo.cs +++ b/FileDbCache/UWP/Properties/AssemblyInfo.cs @@ -7,8 +7,8 @@ using System.Runtime.InteropServices; [assembly: AssemblyCompany("Clemens Fischer")] [assembly: AssemblyCopyright("© 2018 Clemens Fischer")] [assembly: AssemblyTrademark("")] -[assembly: AssemblyVersion("4.4.0")] -[assembly: AssemblyFileVersion("4.4.0")] +[assembly: AssemblyVersion("4.4.1")] +[assembly: AssemblyFileVersion("4.4.1")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] diff --git a/FileDbCache/WPF/Properties/AssemblyInfo.cs b/FileDbCache/WPF/Properties/AssemblyInfo.cs index 11d32ffc..930a9109 100644 --- a/FileDbCache/WPF/Properties/AssemblyInfo.cs +++ b/FileDbCache/WPF/Properties/AssemblyInfo.cs @@ -7,8 +7,8 @@ using System.Runtime.InteropServices; [assembly: AssemblyCompany("Clemens Fischer")] [assembly: AssemblyCopyright("© 2018 Clemens Fischer")] [assembly: AssemblyTrademark("")] -[assembly: AssemblyVersion("4.4.0")] -[assembly: AssemblyFileVersion("4.4.0")] +[assembly: AssemblyVersion("4.4.1")] +[assembly: AssemblyFileVersion("4.4.1")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] diff --git a/MBTiles/UWP/Properties/AssemblyInfo.cs b/MBTiles/UWP/Properties/AssemblyInfo.cs index 9f0caf66..2d008093 100644 --- a/MBTiles/UWP/Properties/AssemblyInfo.cs +++ b/MBTiles/UWP/Properties/AssemblyInfo.cs @@ -7,8 +7,8 @@ using System.Runtime.InteropServices; [assembly: AssemblyCompany("Clemens Fischer")] [assembly: AssemblyCopyright("© 2018 Clemens Fischer")] [assembly: AssemblyTrademark("")] -[assembly: AssemblyVersion("4.4.0")] -[assembly: AssemblyFileVersion("4.4.0")] +[assembly: AssemblyVersion("4.4.1")] +[assembly: AssemblyFileVersion("4.4.1")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] diff --git a/MBTiles/WPF/Properties/AssemblyInfo.cs b/MBTiles/WPF/Properties/AssemblyInfo.cs index 35124928..99111b66 100644 --- a/MBTiles/WPF/Properties/AssemblyInfo.cs +++ b/MBTiles/WPF/Properties/AssemblyInfo.cs @@ -7,8 +7,8 @@ using System.Runtime.InteropServices; [assembly: AssemblyCompany("Clemens Fischer")] [assembly: AssemblyCopyright("© 2018 Clemens Fischer")] [assembly: AssemblyTrademark("")] -[assembly: AssemblyVersion("4.4.0")] -[assembly: AssemblyFileVersion("4.4.0")] +[assembly: AssemblyVersion("4.4.1")] +[assembly: AssemblyFileVersion("4.4.1")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] diff --git a/MapControl/Shared/Intersections.cs b/MapControl/Shared/Intersections.cs new file mode 100644 index 00000000..4272fac0 --- /dev/null +++ b/MapControl/Shared/Intersections.cs @@ -0,0 +1,105 @@ +// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control +// © 2018 Clemens Fischer +// Licensed under the Microsoft Public License (Ms-PL) + +using System; +#if WINDOWS_UWP +using Windows.Foundation; +#else +using System.Windows; +#endif + +namespace MapControl +{ + public static class Intersections + { + /// + /// Returns the intersection point of two line segments given by (p1,p2) and (p3,p4), + /// or null if no intersection exists. See https://stackoverflow.com/a/1968345. + /// + public static Point? GetIntersection(Point p1, Point p2, Point p3, Point p4) + { + var x12 = p2.X - p1.X; + var y12 = p2.Y - p1.Y; + var x34 = p4.X - p3.X; + var y34 = p4.Y - p3.Y; + var x13 = p3.X - p1.X; + var y13 = p3.Y - p1.Y; + + var d = x12 * y34 - x34 * y12; + var s = (x13 * y12 - y13 * x12) / d; + var t = (x13 * y34 - y13 * x34) / d; + + if (s >= 0d && s <= 1d && t >= 0d && t <= 1d) + { + return new Point(p1.X + t * x12, p1.Y + t * y12); + } + + return null; + } + + /// + /// Calculates the potential intersections of a line segment given by (p1,p2) with a rectangle. + /// Updates either p1, p2, or both with any found intersection and returns a value that indicates + /// whether the segment intersects or lies inside the rectangle. + /// + public static bool GetIntersections(ref Point p1, ref Point p2, Rect rect) + { + if (rect.Contains(p1) && rect.Contains(p2)) + { + return true; + } + + var topLeft = new Point(rect.Left, rect.Top); + var topRight = new Point(rect.Right, rect.Top); + var bottomLeft = new Point(rect.Left, rect.Bottom); + var bottomRight = new Point(rect.Right, rect.Bottom); + var numIntersections = 0; + + if (GetIntersection(ref p1, ref p2, topLeft, bottomLeft, p => p.X <= rect.Left)) // left edge + { + numIntersections++; + } + + if (GetIntersection(ref p1, ref p2, topLeft, topRight, p => p.Y <= rect.Top)) // top edge + { + numIntersections++; + } + + if (numIntersections < 2 && + GetIntersection(ref p1, ref p2, topRight, bottomRight, p => p.X >= rect.Right)) // right edge + { + numIntersections++; + } + + if (numIntersections < 2 && + GetIntersection(ref p1, ref p2, bottomLeft, bottomRight, p => p.Y >= rect.Bottom)) // bottom edge + { + numIntersections++; + } + + return numIntersections > 0; + } + + private static bool GetIntersection(ref Point p1, ref Point p2, Point p3, Point p4, Func condition) + { + var intersection = GetIntersection(p1, p2, p3, p4); + + if (!intersection.HasValue) + { + return false; + } + + if (condition(p1)) + { + p1 = intersection.Value; + } + else + { + p2 = intersection.Value; + } + + return true; + } + } +} diff --git a/MapControl/Shared/LocationEx.cs b/MapControl/Shared/LocationEx.cs new file mode 100644 index 00000000..f340728d --- /dev/null +++ b/MapControl/Shared/LocationEx.cs @@ -0,0 +1,172 @@ +// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control +// © 2018 Clemens Fischer +// Licensed under the Microsoft Public License (Ms-PL) + +using System; +using System.Linq; + +namespace MapControl +{ + public static class LocationEx + { + /// + /// see https://en.wikipedia.org/wiki/Great-circle_navigation + /// + 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; + var lat2 = location2.Latitude * Math.PI / 180d; + var lon2 = location2.Longitude * Math.PI / 180d; + var cosS12 = Math.Sin(lat1) * Math.Sin(lat2) + Math.Cos(lat1) * Math.Cos(lat2) * Math.Cos(lon2 - lon1); + + return earthRadius * Math.Acos(Math.Min(Math.Max(cosS12, -1d), 1d)); + } + + public static LocationCollection CalculateMeridianLocations(this Location location, double latitude2, double resolution = 1d) + { + if (resolution <= 0d) + { + throw new ArgumentOutOfRangeException("resolution"); + } + + 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("resolution"); + } + + 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("resolution"); + } + + var y1 = WebMercatorProjection.LatitudeToY(location1.Latitude); + + if (double.IsInfinity(y1)) + { + throw new ArgumentOutOfRangeException("location1"); + } + + var y2 = WebMercatorProjection.LatitudeToY(location2.Latitude); + + if (double.IsInfinity(y2)) + { + throw new ArgumentOutOfRangeException("location2"); + } + + 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)); + } + } +} diff --git a/MapControl/Shared/MapShape.cs b/MapControl/Shared/MapShape.cs index cd64abf1..e45d236d 100644 --- a/MapControl/Shared/MapShape.cs +++ b/MapControl/Shared/MapShape.cs @@ -32,6 +32,14 @@ namespace MapControl set { SetValue(LocationProperty, value); } } + private void LocationPropertyChanged() + { + if (parentMap != null) + { + OnViewportChanged(parentMap, new ViewportChangedEventArgs()); + } + } + private MapBase parentMap; public MapBase ParentMap @@ -99,13 +107,5 @@ namespace MapControl return longitudeOffset; } - - private void LocationPropertyChanged() - { - if (parentMap != null) - { - OnViewportChanged(parentMap, new ViewportChangedEventArgs()); - } - } } } diff --git a/MapControl/UWP/MapControl.UWP.csproj b/MapControl/UWP/MapControl.UWP.csproj index cf6b0b51..1b1208f2 100644 --- a/MapControl/UWP/MapControl.UWP.csproj +++ b/MapControl/UWP/MapControl.UWP.csproj @@ -67,12 +67,18 @@ HyperlinkText.cs + + Intersections.cs + Location.cs LocationCollection.cs + + LocationEx.cs + MapBase.cs @@ -144,6 +150,7 @@ + diff --git a/MapControl/UWP/MapPolygon.UWP.cs b/MapControl/UWP/MapPolygon.UWP.cs new file mode 100644 index 00000000..3c4575c3 --- /dev/null +++ b/MapControl/UWP/MapPolygon.UWP.cs @@ -0,0 +1,55 @@ +// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control +// © 2018 Clemens Fischer +// Licensed under the Microsoft Public License (Ms-PL) + +using System.Collections.Generic; +using System.Linq; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Media; + +namespace MapControl +{ + /// + /// A polygon defined by a collection of Locations. + /// + public class MapPolygon : MapShape + { + public static readonly DependencyProperty LocationsProperty = DependencyProperty.Register( + nameof(Locations), typeof(IEnumerable), typeof(MapPolygon), + new PropertyMetadata(null, (o, e) => ((MapPolygon)o).LocationsPropertyChanged(e))); + + /// + /// Gets or sets the Locations that define the polyline points. + /// + public IEnumerable Locations + { + get { return (IEnumerable)GetValue(LocationsProperty); } + set { SetValue(LocationsProperty, value); } + } + + protected override void UpdateData() + { + var figures = ((PathGeometry)Data).Figures; + figures.Clear(); + + if (ParentMap != null && Locations != null) + { + var locations = Locations; + var offset = GetLongitudeOffset(); + + if (offset != 0d) + { + locations = locations.Select(loc => new Location(loc.Latitude, loc.Longitude + offset)); + } + + var points = locations.Select(loc => ParentMap.MapProjection.LocationToViewportPoint(loc)).ToList(); + + if (points.Count >= 2) + { + points.Add(points[0]); + CreatePolylineFigures(points); + } + } + } + } +} diff --git a/MapControl/UWP/MapPolyline.UWP.cs b/MapControl/UWP/MapPolyline.UWP.cs index 83e4df6d..c94bab00 100644 --- a/MapControl/UWP/MapPolyline.UWP.cs +++ b/MapControl/UWP/MapPolyline.UWP.cs @@ -3,7 +3,6 @@ // Licensed under the Microsoft Public License (Ms-PL) using System.Collections.Generic; -using System.Collections.Specialized; using System.Linq; using Windows.UI.Xaml; using Windows.UI.Xaml.Media; @@ -30,72 +29,25 @@ namespace MapControl protected override void UpdateData() { - var geometry = (PathGeometry)Data; - geometry.Figures.Clear(); + ((PathGeometry)Data).Figures.Clear(); - if (ParentMap != null && Locations != null && Locations.Any()) + if (ParentMap != null && Locations != null) { - PathFigure figure = null; - PolyLineSegment segment = null; - var size = ParentMap.RenderSize; - var offset = GetLongitudeOffset(); var locations = Locations; + var offset = GetLongitudeOffset(); if (offset != 0d) { locations = locations.Select(loc => new Location(loc.Latitude, loc.Longitude + offset)); } - var points = locations.Select(loc => ParentMap.MapProjection.LocationToViewportPoint(loc)); - var p1 = points.First(); + var points = locations.Select(loc => ParentMap.MapProjection.LocationToViewportPoint(loc)).ToList(); - foreach (var p2 in points.Skip(1)) + if (points.Count >= 2) { - if ((p1.X <= 0 && p2.X <= 0) || (p1.X >= size.Width && p2.X >= size.Width) || - (p1.Y <= 0 && p2.Y <= 0) || (p1.Y >= size.Height && p2.Y >= size.Height)) - { - // line (p1,p2) is out of visible bounds, end figure - figure = null; - } - else - { - if (figure == null) - { - figure = new PathFigure { StartPoint = p1, IsClosed = false, IsFilled = false }; - segment = new PolyLineSegment(); - figure.Segments.Add(segment); - geometry.Figures.Add(figure); - } - - segment.Points.Add(p2); - } - - p1 = p2; + CreatePolylineFigures(points); } } } - - private void LocationsPropertyChanged(DependencyPropertyChangedEventArgs e) - { - var oldCollection = e.OldValue as INotifyCollectionChanged; - var newCollection = e.NewValue as INotifyCollectionChanged; - - if (oldCollection != null) - { - oldCollection.CollectionChanged -= LocationCollectionChanged; - } - - if (newCollection != null) - { - newCollection.CollectionChanged += LocationCollectionChanged; - } - - UpdateData(); - } - - private void LocationCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - UpdateData(); - } } } diff --git a/MapControl/UWP/MapShape.UWP.cs b/MapControl/UWP/MapShape.UWP.cs index f1f604ba..42939056 100644 --- a/MapControl/UWP/MapShape.UWP.cs +++ b/MapControl/UWP/MapShape.UWP.cs @@ -2,6 +2,11 @@ // © 2018 Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) +using System.Collections.Generic; +using System.Collections.Specialized; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Shapes; namespace MapControl @@ -17,5 +22,62 @@ namespace MapControl { UpdateData(); } + + protected void LocationsPropertyChanged(DependencyPropertyChangedEventArgs e) + { + var oldCollection = e.OldValue as INotifyCollectionChanged; + var newCollection = e.NewValue as INotifyCollectionChanged; + + if (oldCollection != null) + { + oldCollection.CollectionChanged -= LocationCollectionChanged; + } + + if (newCollection != null) + { + newCollection.CollectionChanged += LocationCollectionChanged; + } + + UpdateData(); + } + + protected void LocationCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + UpdateData(); + } + + protected void CreatePolylineFigures(IList points) + { + var viewport = new Rect(0, 0, ParentMap.RenderSize.Width, ParentMap.RenderSize.Height); + var figures = ((PathGeometry)Data).Figures; + + PathFigure figure = null; + PolyLineSegment segment = null; + + for (int i = 1; i < points.Count; i++) + { + var p1 = points[i - 1]; + var p2 = points[i]; + var inside = Intersections.GetIntersections(ref p1, ref p2, viewport); + + if (inside) + { + if (figure == null) + { + figure = new PathFigure { StartPoint = p1, IsClosed = false, IsFilled = false }; + segment = new PolyLineSegment(); + figure.Segments.Add(segment); + figures.Add(figure); + } + + segment.Points.Add(p2); + } + + if (!inside || p2 != points[i]) + { + figure = null; + } + } + } } } diff --git a/MapControl/UWP/Properties/AssemblyInfo.cs b/MapControl/UWP/Properties/AssemblyInfo.cs index 3c3e9e03..bb8e6f6f 100644 --- a/MapControl/UWP/Properties/AssemblyInfo.cs +++ b/MapControl/UWP/Properties/AssemblyInfo.cs @@ -7,8 +7,8 @@ using System.Runtime.InteropServices; [assembly: AssemblyCompany("Clemens Fischer")] [assembly: AssemblyCopyright("© 2018 Clemens Fischer")] [assembly: AssemblyTrademark("")] -[assembly: AssemblyVersion("4.4.0")] -[assembly: AssemblyFileVersion("4.4.0")] +[assembly: AssemblyVersion("4.4.1")] +[assembly: AssemblyFileVersion("4.4.1")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] diff --git a/MapControl/WPF/MapControl.WPF.csproj b/MapControl/WPF/MapControl.WPF.csproj index 9ba7d387..d1eb863f 100644 --- a/MapControl/WPF/MapControl.WPF.csproj +++ b/MapControl/WPF/MapControl.WPF.csproj @@ -86,12 +86,18 @@ HyperlinkText.cs + + Intersections.cs + Location.cs LocationCollection.cs + + LocationEx.cs + MapBase.cs diff --git a/MapControl/WPF/MapGraticule.WPF.cs b/MapControl/WPF/MapGraticule.WPF.cs index 20781e22..1359205f 100644 --- a/MapControl/WPF/MapGraticule.WPF.cs +++ b/MapControl/WPF/MapGraticule.WPF.cs @@ -39,62 +39,75 @@ namespace MapControl { var projection = ParentMap?.MapProjection; - if (projection != null && !projection.IsAzimuthal) + if (projection != null) { - var bounds = projection.ViewportRectToBoundingBox(new Rect(ParentMap.RenderSize)); + var lineDistance = GetLineDistance(); + var labelFormat = GetLabelFormat(lineDistance); - if (bounds.HasValidBounds) + if (projection.IsAzimuthal) { - var lineDistance = GetLineDistance(); - var labelFormat = GetLabelFormat(lineDistance); - var latLabelStart = Math.Ceiling(bounds.South / lineDistance) * lineDistance; - var lonLabelStart = Math.Ceiling(bounds.West / lineDistance) * lineDistance; - var latLabels = new List