Version 4: Upgrade to VS 2017

This commit is contained in:
ClemensF 2017-08-04 21:38:58 +02:00
parent 2aafe32e00
commit ec47f225b3
142 changed files with 1828 additions and 18384 deletions

View file

@ -0,0 +1,58 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 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
{
/// <summary>
/// Transforms map coordinates according to the Azimuthal Equidistant Projection.
/// </summary>
public class AzimuthalEquidistantProjection : AzimuthalProjection
{
public AzimuthalEquidistantProjection()
{
// No known standard or de-facto standard CRS ID
}
public AzimuthalEquidistantProjection(string crsId)
{
CrsId = crsId;
}
public override Point LocationToPoint(Location location)
{
if (location.Equals(projectionCenter))
{
return new Point();
}
double azimuth, distance;
GetAzimuthDistance(projectionCenter, location, out azimuth, out distance);
distance *= Wgs84EquatorialRadius;
return new Point(distance * Math.Sin(azimuth), distance * Math.Cos(azimuth));
}
public override Location PointToLocation(Point point)
{
if (point.X == 0d && point.Y == 0d)
{
return projectionCenter;
}
var azimuth = Math.Atan2(point.X, point.Y);
var distance = Math.Sqrt(point.X * point.X + point.Y * point.Y) / Wgs84EquatorialRadius;
return GetLocation(projectionCenter, azimuth, distance);
}
}
}

View file

@ -0,0 +1,137 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Globalization;
#if WINDOWS_UWP
using Windows.Foundation;
#else
using System.Windows;
#endif
namespace MapControl
{
/// <summary>
/// Base class for azimuthal map projections.
/// </summary>
public abstract class AzimuthalProjection : MapProjection
{
protected Location projectionCenter = new Location();
public AzimuthalProjection()
{
IsAzimuthal = true;
LongitudeScale = double.NaN;
}
public override double GetViewportScale(double zoomLevel)
{
return DegreesToViewportScale(zoomLevel) / MetersPerDegree;
}
public override Point GetMapScale(Location location)
{
return new Point(ViewportScale, ViewportScale);
}
public override Location TranslateLocation(Location location, Point translation)
{
var scaleY = ViewportScale * MetersPerDegree;
var scaleX = scaleY * Math.Cos(location.Latitude * Math.PI / 180d);
return new Location(
location.Latitude - translation.Y / scaleY,
location.Longitude + translation.X / scaleX);
}
public override Rect BoundingBoxToRect(BoundingBox boundingBox)
{
var cbbox = boundingBox as CenteredBoundingBox;
if (cbbox != null)
{
var center = LocationToPoint(cbbox.Center);
return new Rect(
center.X - cbbox.Width / 2d, center.Y - cbbox.Height / 2d,
cbbox.Width, cbbox.Height);
}
return base.BoundingBoxToRect(boundingBox);
}
public override BoundingBox RectToBoundingBox(Rect rect)
{
var center = PointToLocation(new Point(rect.X + rect.Width / 2d, rect.Y + rect.Height / 2d));
return new CenteredBoundingBox(center, rect.Width, rect.Height); // width and height in meters
}
public override void SetViewportTransform(Location projectionCenter, Location mapCenter, Point viewportCenter, double zoomLevel, double heading)
{
this.projectionCenter = projectionCenter;
base.SetViewportTransform(projectionCenter, mapCenter, viewportCenter, zoomLevel, heading);
}
public override string WmsQueryParameters(BoundingBox boundingBox, string version)
{
if (string.IsNullOrEmpty(CrsId))
{
return null;
}
var rect = BoundingBoxToRect(boundingBox);
var width = (int)Math.Round(ViewportScale * rect.Width);
var height = (int)Math.Round(ViewportScale * rect.Height);
var crs = version.StartsWith("1.1.") ? "SRS" : "CRS";
return string.Format(CultureInfo.InvariantCulture,
"{0}={1},1,{2},{3}&BBOX={4},{5},{6},{7}&WIDTH={8}&HEIGHT={9}",
crs, CrsId, projectionCenter.Longitude, projectionCenter.Latitude,
rect.X, rect.Y, (rect.X + rect.Width), (rect.Y + rect.Height), width, height);
}
/// <summary>
/// Calculates azimuth and distance in radians from location1 to location2.
/// The returned distance has to be multiplied with an appropriate earth radius.
/// </summary>
public static void GetAzimuthDistance(Location location1, Location location2, out double azimuth, out double distance)
{
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 cosDistance = sinLat1 * sinLat2 + cosLat1 * cosLat2 * cosLon12;
azimuth = Math.Atan2(sinLon12, cosLat1 * sinLat2 / cosLat2 - sinLat1 * cosLon12);
distance = Math.Acos(Math.Max(Math.Min(cosDistance, 1d), -1d));
}
/// <summary>
/// Calculates the Location of the point given by azimuth and distance in radians from location.
/// </summary>
public static Location GetLocation(Location location, double azimuth, double distance)
{
var lat1 = location.Latitude * Math.PI / 180d;
var sinDistance = Math.Sin(distance);
var cosDistance = Math.Cos(distance);
var cosAzimuth = Math.Cos(azimuth);
var sinAzimuth = Math.Sin(azimuth);
var cosLat1 = Math.Cos(lat1);
var sinLat1 = Math.Sin(lat1);
var sinLat2 = sinLat1 * cosDistance + cosLat1 * sinDistance * cosAzimuth;
var lat2 = Math.Asin(Math.Max(Math.Min(sinLat2, 1d), -1d));
var dLon = Math.Atan2(sinDistance * sinAzimuth, cosLat1 * cosDistance - sinLat1 * sinDistance * cosAzimuth);
return new Location(180d / Math.PI * lat2, location.Longitude + 180d / Math.PI * dLon);
}
}
}

View file

@ -0,0 +1,138 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
#if WINDOWS_UWP
using Windows.Data.Xml.Dom;
using Windows.UI.Xaml;
#else
using System.Windows;
using System.Xml;
#endif
namespace MapControl
{
/// <summary>
/// Displays Bing Maps tiles. The static ApiKey property must be set to a Bing Maps API Key.
/// Tile image URLs and min/max zoom levels are retrieved from the Imagery Metadata Service
/// (see http://msdn.microsoft.com/en-us/library/ff701716.aspx).
/// </summary>
public class BingMapsTileLayer : MapTileLayer
{
public enum MapMode
{
Road, Aerial, AerialWithLabels
}
public BingMapsTileLayer()
: this(new TileImageLoader())
{
}
public BingMapsTileLayer(ITileImageLoader tileImageLoader)
: base(tileImageLoader)
{
MinZoomLevel = 1;
MaxZoomLevel = 21;
Loaded += OnLoaded;
}
public static string ApiKey { get; set; }
public MapMode Mode { get; set; }
public string Culture { get; set; }
public Uri LogoImageUri { get; set; }
private async void OnLoaded(object sender, RoutedEventArgs e)
{
Loaded -= OnLoaded;
if (string.IsNullOrEmpty(ApiKey))
{
Debug.WriteLine("BingMapsTileLayer requires a Bing Maps API Key");
return;
}
var imageryMetadataUrl = "http://dev.virtualearth.net/REST/V1/Imagery/Metadata/" + Mode;
try
{
var document = await XmlDocument.LoadFromUriAsync(new Uri(imageryMetadataUrl + "?output=xml&key=" + ApiKey));
var imageryMetadata = document.DocumentElement.GetElementsByTagName("ImageryMetadata").OfType<XmlElement>().FirstOrDefault();
if (imageryMetadata != null)
{
ReadImageryMetadata(imageryMetadata);
}
var brandLogoUri = document.DocumentElement.GetElementsByTagName("BrandLogoUri").OfType<XmlElement>().FirstOrDefault();
if (brandLogoUri != null)
{
LogoImageUri = new Uri(brandLogoUri.InnerText);
}
}
catch (Exception ex)
{
Debug.WriteLine("BingMapsTileLayer: {0}: {1}", imageryMetadataUrl, ex.Message);
}
}
private void ReadImageryMetadata(XmlElement imageryMetadata)
{
string imageUrl = null;
string[] imageUrlSubdomains = null;
int? zoomMin = null;
int? zoomMax = null;
foreach (var element in imageryMetadata.ChildNodes.OfType<XmlElement>())
{
switch ((string)element.LocalName)
{
case "ImageUrl":
imageUrl = element.InnerText;
break;
case "ImageUrlSubdomains":
imageUrlSubdomains = element.ChildNodes
.OfType<XmlElement>()
.Where(e => (string)e.LocalName == "string")
.Select(e => e.InnerText)
.ToArray();
break;
case "ZoomMin":
zoomMin = int.Parse(element.InnerText);
break;
case "ZoomMax":
zoomMax = int.Parse(element.InnerText);
break;
default:
break;
}
}
if (!string.IsNullOrEmpty(imageUrl) && imageUrlSubdomains != null && imageUrlSubdomains.Length > 0)
{
if (zoomMin.HasValue && zoomMin.Value > MinZoomLevel)
{
MinZoomLevel = zoomMin.Value;
}
if (zoomMax.HasValue && zoomMax.Value < MaxZoomLevel)
{
MaxZoomLevel = zoomMax.Value;
}
if (string.IsNullOrEmpty(Culture))
{
Culture = CultureInfo.CurrentUICulture.Name;
}
TileSource = new BingMapsTileSource(imageUrl.Replace("{culture}", Culture), imageUrlSubdomains);
}
}
}
}

View file

@ -0,0 +1,39 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
namespace MapControl
{
public class BingMapsTileSource : TileSource
{
private readonly string[] subdomains;
public BingMapsTileSource(string uriFormat, string[] subdomains)
: base(uriFormat)
{
this.subdomains = subdomains;
}
public override Uri GetUri(int x, int y, int zoomLevel)
{
if (zoomLevel < 1)
{
return null;
}
var subdomain = subdomains[(x + y) % subdomains.Length];
var quadkey = new char[zoomLevel];
for (var z = zoomLevel - 1; z >= 0; z--, x /= 2, y /= 2)
{
quadkey[z] = (char)('0' + 2 * (y % 2) + (x % 2));
}
return new Uri(UriFormat
.Replace("{subdomain}", subdomain)
.Replace("{quadkey}", new string(quadkey)));
}
}
}

View file

@ -0,0 +1,92 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Globalization;
namespace MapControl
{
/// <summary>
/// A geographic bounding box with south and north latitude and west and east longitude values in degrees.
/// </summary>
public partial class BoundingBox
{
private double south;
private double west;
private double north;
private double east;
public BoundingBox()
{
}
public BoundingBox(double south, double west, double north, double east)
{
South = south;
West = west;
North = north;
East = east;
}
public double South
{
get { return south; }
set { south = Math.Min(Math.Max(value, -90d), 90d); }
}
public double West
{
get { return west; }
set { west = value; }
}
public double North
{
get { return north; }
set { north = Math.Min(Math.Max(value, -90d), 90d); }
}
public double East
{
get { return east; }
set { east = value; }
}
public virtual double Width
{
get { return east - west; }
}
public virtual double Height
{
get { return north - south; }
}
public bool HasValidBounds
{
get { return south < north && west < east; }
}
public virtual BoundingBox Clone()
{
return new BoundingBox(south, west, north, east);
}
public static BoundingBox Parse(string s)
{
var values = s.Split(new char[] { ',' });
if (values.Length != 4)
{
throw new FormatException("BoundingBox string must be a comma-separated list of four double values");
}
return new BoundingBox(
double.Parse(values[0], NumberStyles.Float, CultureInfo.InvariantCulture),
double.Parse(values[1], NumberStyles.Float, CultureInfo.InvariantCulture),
double.Parse(values[2], NumberStyles.Float, CultureInfo.InvariantCulture),
double.Parse(values[3], NumberStyles.Float, CultureInfo.InvariantCulture));
}
}
}

View file

@ -0,0 +1,40 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
namespace MapControl
{
public class CenteredBoundingBox : BoundingBox
{
private readonly Location center;
private readonly double width;
private readonly double height;
public CenteredBoundingBox(Location center, double width, double height)
{
this.center = center;
this.width = width;
this.height = height;
}
public Location Center
{
get { return center; }
}
public override double Width
{
get { return width; }
}
public override double Height
{
get { return height; }
}
public override BoundingBox Clone()
{
return new CenteredBoundingBox(center, width, height);
}
}
}

View file

@ -0,0 +1,54 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 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
{
/// <summary>
/// Transforms map coordinates according to the Equirectangular Projection.
/// Longitude and Latitude values are transformed identically to X and Y.
/// </summary>
public class EquirectangularProjection : MapProjection
{
public EquirectangularProjection()
: this("EPSG:4326")
{
}
public EquirectangularProjection(string crsId)
{
CrsId = crsId;
}
public override Point GetMapScale(Location location)
{
return new Point(
ViewportScale / (MetersPerDegree * Math.Cos(location.Latitude * Math.PI / 180d)),
ViewportScale / MetersPerDegree);
}
public override Point LocationToPoint(Location location)
{
return new Point(location.Longitude, location.Latitude);
}
public override Location PointToLocation(Point point)
{
return new Location(point.Y, point.X);
}
public override Location TranslateLocation(Location location, Point translation)
{
return new Location(
location.Latitude - translation.Y / ViewportScale,
location.Longitude + translation.X / ViewportScale);
}
}
}

View file

@ -0,0 +1,61 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 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
{
/// <summary>
/// Transforms map coordinates according to the Gnomonic Projection.
/// </summary>
public class GnomonicProjection : AzimuthalProjection
{
public GnomonicProjection()
: this("AUTO2:97001") // GeoServer non-standard CRS ID
{
}
public GnomonicProjection(string crsId)
{
CrsId = crsId;
}
public override Point LocationToPoint(Location location)
{
if (location.Equals(projectionCenter))
{
return new Point();
}
double azimuth, distance;
GetAzimuthDistance(projectionCenter, location, out azimuth, out distance);
var mapDistance = distance < Math.PI / 2d
? Wgs84EquatorialRadius * Math.Tan(distance)
: double.PositiveInfinity;
return new Point(mapDistance * Math.Sin(azimuth), mapDistance * Math.Cos(azimuth));
}
public override Location PointToLocation(Point point)
{
if (point.X == 0d && point.Y == 0d)
{
return projectionCenter;
}
var azimuth = Math.Atan2(point.X, point.Y);
var mapDistance = Math.Sqrt(point.X * point.X + point.Y * point.Y);
var distance = Math.Atan(mapDistance / Wgs84EquatorialRadius);
return GetLocation(projectionCenter, azimuth, distance);
}
}
}

View file

@ -0,0 +1,99 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
#if WINDOWS_UWP
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Documents;
#else
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
#endif
namespace MapControl
{
public static class HyperlinkText
{
private static Regex regex = new Regex(@"\[([^\]]+)\]\(([^\)]+)\)");
/// <summary>
/// Converts text containing hyperlinks in markdown syntax [text](url)
/// to a collection of Run and Hyperlink inlines.
/// </summary>
public static IEnumerable<Inline> TextToInlines(this string text)
{
var inlines = new List<Inline>();
while (!string.IsNullOrEmpty(text))
{
var match = regex.Match(text);
Uri uri;
if (match.Success &&
match.Groups.Count == 3 &&
Uri.TryCreate(match.Groups[2].Value, UriKind.Absolute, out uri))
{
inlines.Add(new Run { Text = text.Substring(0, match.Index) });
text = text.Substring(match.Index + match.Length);
var link = new Hyperlink { NavigateUri = uri };
link.Inlines.Add(new Run { Text = match.Groups[1].Value });
#if !WINDOWS_UWP
link.ToolTip = uri.ToString();
link.RequestNavigate += (s, e) => System.Diagnostics.Process.Start(e.Uri.ToString());
#endif
inlines.Add(link);
}
else
{
inlines.Add(new Run { Text = text });
text = null;
}
}
return inlines;
}
public static readonly DependencyProperty InlinesSourceProperty = DependencyProperty.RegisterAttached(
"InlinesSource", typeof(string), typeof(HyperlinkText), new PropertyMetadata(null, InlinesSourcePropertyChanged));
public static string GetInlinesSource(UIElement element)
{
return (string)element.GetValue(InlinesSourceProperty);
}
public static void SetInlinesSource(UIElement element, string value)
{
element.SetValue(InlinesSourceProperty, value);
}
private static void InlinesSourcePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
InlineCollection inlines = null;
if (obj is TextBlock)
{
inlines = ((TextBlock)obj).Inlines;
}
else if (obj is Paragraph)
{
inlines = ((Paragraph)obj).Inlines;
}
if (inlines != null)
{
inlines.Clear();
foreach (var inline in TextToInlines((string)e.NewValue))
{
inlines.Add(inline);
}
}
}
}
}

View file

@ -0,0 +1,109 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Globalization;
namespace MapControl
{
/// <summary>
/// A geographic location with latitude and longitude values in degrees.
/// </summary>
public partial class Location : IEquatable<Location>
{
private double latitude;
private double longitude;
public Location()
{
}
public Location(double latitude, double longitude)
{
Latitude = latitude;
Longitude = longitude;
}
public double Latitude
{
get { return latitude; }
set { latitude = Math.Min(Math.Max(value, -90d), 90d); }
}
public double Longitude
{
get { return longitude; }
set { longitude = value; }
}
public bool Equals(Location location)
{
return location != null
&& Math.Abs(location.latitude - latitude) < 1e-9
&& Math.Abs(location.longitude - longitude) < 1e-9;
}
public override bool Equals(object obj)
{
return Equals(obj as Location);
}
public override int GetHashCode()
{
return latitude.GetHashCode() ^ longitude.GetHashCode();
}
public override string ToString()
{
return string.Format(CultureInfo.InvariantCulture, "{0:F5},{1:F5}", latitude, longitude);
}
public static Location Parse(string s)
{
var values = s.Split(new char[] { ',' });
if (values.Length != 2)
{
throw new FormatException("Location string must be a comma-separated pair of double values");
}
return new Location(
double.Parse(values[0], NumberStyles.Float, CultureInfo.InvariantCulture),
double.Parse(values[1], NumberStyles.Float, CultureInfo.InvariantCulture));
}
/// <summary>
/// Normalizes a longitude to a value in the interval [-180..180].
/// </summary>
public static double NormalizeLongitude(double longitude)
{
if (longitude < -180d)
{
longitude = ((longitude + 180d) % 360d) + 180d;
}
else if (longitude > 180d)
{
longitude = ((longitude - 180d) % 360d) - 180d;
}
return longitude;
}
internal static double NearestLongitude(double longitude, double referenceLongitude)
{
longitude = NormalizeLongitude(longitude);
if (longitude > referenceLongitude + 180d)
{
longitude -= 360d;
}
else if (longitude < referenceLongitude - 180d)
{
longitude += 360d;
}
return longitude;
}
}
}

View file

@ -0,0 +1,38 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace MapControl
{
/// <summary>
/// An ObservableCollection of Location with support for parsing.
/// </summary>
public partial class LocationCollection : ObservableCollection<Location>
{
public LocationCollection()
{
}
public LocationCollection(IEnumerable<Location> locations)
: base(locations)
{
}
public LocationCollection(List<Location> locations)
: base(locations)
{
}
public static LocationCollection Parse(string s)
{
var strings = s.Split(new char[] { ' ', ';' }, StringSplitOptions.RemoveEmptyEntries);
return new LocationCollection(strings.Select(l => Location.Parse(l)));
}
}
}

View file

@ -0,0 +1,763 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
#if WINDOWS_UWP
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;
#else
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
#endif
namespace MapControl
{
public interface IMapLayer : IMapElement
{
Brush MapBackground { get; }
Brush MapForeground { get; }
}
/// <summary>
/// The map control. Displays map content provided by one or more MapTileLayers or MapImageLayers.
/// The visible map area is defined by the Center and ZoomLevel properties.
/// The map can be rotated by an angle that is given by the Heading property.
/// MapBase can contain map overlay child elements like other MapPanels or MapItemsControls.
/// </summary>
public partial class MapBase : MapPanel
{
private const double MaximumZoomLevel = 22d;
public static readonly DependencyProperty MapLayerProperty = DependencyProperty.Register(
nameof(MapLayer), typeof(UIElement), typeof(MapBase),
new PropertyMetadata(null, (o, e) => ((MapBase)o).MapLayerPropertyChanged((UIElement)e.OldValue, (UIElement)e.NewValue)));
public static readonly DependencyProperty MapProjectionProperty = DependencyProperty.Register(
nameof(MapProjection), typeof(MapProjection), typeof(MapBase),
new PropertyMetadata(null, (o, e) => ((MapBase)o).MapProjectionPropertyChanged()));
public static readonly DependencyProperty ProjectionCenterProperty = DependencyProperty.Register(
nameof(ProjectionCenter), typeof(Location), typeof(MapBase),
new PropertyMetadata(null, (o, e) => ((MapBase)o).ProjectionCenterPropertyChanged()));
public static readonly DependencyProperty MinZoomLevelProperty = DependencyProperty.Register(
nameof(MinZoomLevel), typeof(double), typeof(MapBase),
new PropertyMetadata(1d, (o, e) => ((MapBase)o).MinZoomLevelPropertyChanged((double)e.NewValue)));
public static readonly DependencyProperty MaxZoomLevelProperty = DependencyProperty.Register(
nameof(MaxZoomLevel), typeof(double), typeof(MapBase),
new PropertyMetadata(19d, (o, e) => ((MapBase)o).MaxZoomLevelPropertyChanged((double)e.NewValue)));
public static readonly DependencyProperty AnimationDurationProperty = DependencyProperty.Register(
nameof(AnimationDuration), typeof(TimeSpan), typeof(MapBase),
new PropertyMetadata(TimeSpan.FromSeconds(0.3)));
public static readonly DependencyProperty AnimationEasingFunctionProperty = DependencyProperty.Register(
nameof(AnimationEasingFunction), typeof(EasingFunctionBase), typeof(MapBase),
new PropertyMetadata(new QuadraticEase { EasingMode = EasingMode.EaseOut }));
public static readonly DependencyProperty TileFadeDurationProperty = DependencyProperty.Register(
nameof(TileFadeDuration), typeof(TimeSpan), typeof(MapBase),
new PropertyMetadata(Tile.FadeDuration, (o, e) => Tile.FadeDuration = (TimeSpan)e.NewValue));
internal static readonly DependencyProperty CenterPointProperty = DependencyProperty.Register(
"CenterPoint", typeof(Point), typeof(MapBase),
new PropertyMetadata(new Point(), (o, e) => ((MapBase)o).CenterPointPropertyChanged((Point)e.NewValue)));
private PointAnimation centerAnimation;
private DoubleAnimation zoomLevelAnimation;
private DoubleAnimation headingAnimation;
private Location transformCenter;
private Point viewportCenter;
private double centerLongitude;
private bool internalPropertyChange;
public MapBase()
{
Initialize();
MapProjection = new WebMercatorProjection();
ScaleRotateTransform.Children.Add(ScaleTransform);
ScaleRotateTransform.Children.Add(RotateTransform);
}
partial void Initialize(); // Windows Runtime and Silverlight only
partial void RemoveAnimation(DependencyProperty property); // WPF only
/// <summary>
/// Raised when the current viewport has changed.
/// </summary>
public event EventHandler<ViewportChangedEventArgs> ViewportChanged;
/// <summary>
/// Gets or sets the map foreground Brush.
/// </summary>
public Brush Foreground
{
get { return (Brush)GetValue(ForegroundProperty); }
set { SetValue(ForegroundProperty, value); }
}
/// <summary>
/// Gets or sets the base map layer, which is added as first element to the Children collection.
/// If the layer implements IMapLayer (like MapTileLayer or MapImageLayer), its (non-null) MapBackground
/// and MapForeground property values are used for the MapBase Background and Foreground properties.
/// </summary>
public UIElement MapLayer
{
get { return (UIElement)GetValue(MapLayerProperty); }
set { SetValue(MapLayerProperty, value); }
}
/// <summary>
/// Gets or sets the MapProjection used by the map control.
/// </summary>
public MapProjection MapProjection
{
get { return (MapProjection)GetValue(MapProjectionProperty); }
set { SetValue(MapProjectionProperty, value); }
}
/// <summary>
/// Gets or sets an optional center (reference point) for azimuthal projections.
/// If ProjectionCenter is null, the Center property value will be used instead.
/// </summary>
public Location ProjectionCenter
{
get { return (Location)GetValue(ProjectionCenterProperty); }
set { SetValue(ProjectionCenterProperty, value); }
}
/// <summary>
/// Gets or sets the location of the center point of the map.
/// </summary>
public Location Center
{
get { return (Location)GetValue(CenterProperty); }
set { SetValue(CenterProperty, value); }
}
/// <summary>
/// Gets or sets the target value of a Center animation.
/// </summary>
public Location TargetCenter
{
get { return (Location)GetValue(TargetCenterProperty); }
set { SetValue(TargetCenterProperty, value); }
}
/// <summary>
/// Gets or sets the minimum value of the ZoomLevel and TargetZommLevel properties.
/// Must be greater than or equal to zero and less than or equal to MaxZoomLevel.
/// The default value is 1.
/// </summary>
public double MinZoomLevel
{
get { return (double)GetValue(MinZoomLevelProperty); }
set { SetValue(MinZoomLevelProperty, value); }
}
/// <summary>
/// Gets or sets the maximum value of the ZoomLevel and TargetZommLevel properties.
/// Must be greater than or equal to MinZoomLevel and less than or equal to 20.
/// The default value is 19.
/// </summary>
public double MaxZoomLevel
{
get { return (double)GetValue(MaxZoomLevelProperty); }
set { SetValue(MaxZoomLevelProperty, value); }
}
/// <summary>
/// Gets or sets the map zoom level.
/// </summary>
public double ZoomLevel
{
get { return (double)GetValue(ZoomLevelProperty); }
set { SetValue(ZoomLevelProperty, value); }
}
/// <summary>
/// Gets or sets the target value of a ZoomLevel animation.
/// </summary>
public double TargetZoomLevel
{
get { return (double)GetValue(TargetZoomLevelProperty); }
set { SetValue(TargetZoomLevelProperty, value); }
}
/// <summary>
/// Gets or sets the map heading, i.e. a clockwise rotation angle in degrees.
/// </summary>
public double Heading
{
get { return (double)GetValue(HeadingProperty); }
set { SetValue(HeadingProperty, value); }
}
/// <summary>
/// Gets or sets the target value of a Heading animation.
/// </summary>
public double TargetHeading
{
get { return (double)GetValue(TargetHeadingProperty); }
set { SetValue(TargetHeadingProperty, value); }
}
/// <summary>
/// Gets or sets the Duration of the Center, ZoomLevel and Heading animations.
/// The default value is 0.3 seconds.
/// </summary>
public TimeSpan AnimationDuration
{
get { return (TimeSpan)GetValue(AnimationDurationProperty); }
set { SetValue(AnimationDurationProperty, value); }
}
/// <summary>
/// Gets or sets the EasingFunction of the Center, ZoomLevel and Heading animations.
/// The default value is a QuadraticEase with EasingMode.EaseOut.
/// </summary>
public EasingFunctionBase AnimationEasingFunction
{
get { return (EasingFunctionBase)GetValue(AnimationEasingFunctionProperty); }
set { SetValue(AnimationEasingFunctionProperty, value); }
}
/// <summary>
/// Gets or sets the Duration of the Tile Opacity animation.
/// The default value is 0.2 seconds.
/// </summary>
public TimeSpan TileFadeDuration
{
get { return (TimeSpan)GetValue(TileFadeDurationProperty); }
set { SetValue(TileFadeDurationProperty, value); }
}
/// <summary>
/// Gets the scaling transformation from meters to viewport coordinate units at the Center location.
/// </summary>
public ScaleTransform ScaleTransform { get; } = new ScaleTransform();
/// <summary>
/// Gets the transformation that rotates by the value of the Heading property.
/// </summary>
public RotateTransform RotateTransform { get; } = new RotateTransform();
/// <summary>
/// Gets the combination of ScaleTransform and RotateTransform
/// </summary>
public TransformGroup ScaleRotateTransform { get; } = new TransformGroup();
/// <summary>
/// Transforms a Location in geographic coordinates to a Point in viewport coordinates.
/// </summary>
public Point LocationToViewportPoint(Location location)
{
return MapProjection.LocationToViewportPoint(location);
}
/// <summary>
/// Transforms a Point in viewport coordinates to a Location in geographic coordinates.
/// </summary>
public Location ViewportPointToLocation(Point point)
{
return MapProjection.ViewportPointToLocation(point);
}
/// <summary>
/// Sets a temporary center point in viewport coordinates for scaling and rotation transformations.
/// This center point is automatically reset when the Center property is set by application code.
/// </summary>
public void SetTransformCenter(Point center)
{
transformCenter = MapProjection.ViewportPointToLocation(center);
viewportCenter = center;
}
/// <summary>
/// Resets the temporary transform center point set by SetTransformCenter.
/// </summary>
public void ResetTransformCenter()
{
transformCenter = null;
viewportCenter = new Point(RenderSize.Width / 2d, RenderSize.Height / 2d);
}
/// <summary>
/// Changes the Center property according to the specified map translation in viewport coordinates.
/// </summary>
public void TranslateMap(Point translation)
{
if (transformCenter != null)
{
ResetTransformCenter();
UpdateTransform();
}
if (translation.X != 0d || translation.Y != 0d)
{
if (Heading != 0d)
{
var cos = Math.Cos(Heading / 180d * Math.PI);
var sin = Math.Sin(Heading / 180d * Math.PI);
translation = new Point(
translation.X * cos + translation.Y * sin,
translation.Y * cos - translation.X * sin);
}
translation.X = -translation.X;
translation.Y = -translation.Y;
Center = MapProjection.TranslateLocation(Center, translation);
}
}
/// <summary>
/// Changes the Center, Heading and ZoomLevel properties according to the specified
/// viewport coordinate translation, rotation and scale delta values. Rotation and scaling
/// is performed relative to the specified center point in viewport coordinates.
/// </summary>
public void TransformMap(Point center, Point translation, double rotation, double scale)
{
if (rotation != 0d || scale != 1d)
{
transformCenter = MapProjection.ViewportPointToLocation(center);
viewportCenter = new Point(center.X + translation.X, center.Y + translation.Y);
if (rotation != 0d)
{
var heading = (((Heading + rotation) % 360d) + 360d) % 360d;
InternalSetValue(HeadingProperty, heading);
InternalSetValue(TargetHeadingProperty, heading);
}
if (scale != 1d)
{
var zoomLevel = Math.Min(Math.Max(ZoomLevel + Math.Log(scale, 2d), MinZoomLevel), MaxZoomLevel);
InternalSetValue(ZoomLevelProperty, zoomLevel);
InternalSetValue(TargetZoomLevelProperty, zoomLevel);
}
UpdateTransform(true);
}
else
{
TranslateMap(translation); // more precise
}
}
/// <summary>
/// Sets the value of the TargetZoomLevel property while retaining the specified center point
/// in viewport coordinates.
/// </summary>
public void ZoomMap(Point center, double zoomLevel)
{
zoomLevel = Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel);
if (TargetZoomLevel != zoomLevel)
{
SetTransformCenter(center);
if (double.IsNaN(MapProjection.LongitudeScale))
{
ZoomLevel = zoomLevel;
}
else
{
TargetZoomLevel = zoomLevel;
}
}
}
/// <summary>
/// Sets the TargetZoomLevel and TargetCenter properties so that the specified bounding box
/// fits into the current viewport. The TargetHeading property is set to zero.
/// </summary>
public void ZoomToBounds(BoundingBox boundingBox)
{
if (boundingBox != null && boundingBox.HasValidBounds)
{
var rect = MapProjection.BoundingBoxToRect(boundingBox);
var center = new Point(rect.X + rect.Width / 2d, rect.Y + rect.Height / 2d);
var scale0 = 1d / MapProjection.GetViewportScale(0d);
var lonScale = scale0 * RenderSize.Width / rect.Width;
var latScale = scale0 * RenderSize.Height / rect.Height;
var lonZoom = Math.Log(lonScale, 2d);
var latZoom = Math.Log(latScale, 2d);
TargetZoomLevel = Math.Min(lonZoom, latZoom);
TargetCenter = MapProjection.PointToLocation(center);
TargetHeading = 0d;
}
}
private void MapLayerPropertyChanged(UIElement oldLayer, UIElement newLayer)
{
if (oldLayer != null)
{
Children.Remove(oldLayer);
var mapLayer = oldLayer as IMapLayer;
if (mapLayer != null)
{
if (mapLayer.MapBackground != null)
{
ClearValue(BackgroundProperty);
}
if (mapLayer.MapForeground != null)
{
ClearValue(ForegroundProperty);
}
}
}
if (newLayer != null)
{
Children.Insert(0, newLayer);
var mapLayer = newLayer as IMapLayer;
if (mapLayer != null)
{
if (mapLayer.MapBackground != null)
{
Background = mapLayer.MapBackground;
}
if (mapLayer.MapForeground != null)
{
Foreground = mapLayer.MapForeground;
}
}
}
}
private void MapProjectionPropertyChanged()
{
ResetTransformCenter();
UpdateTransform(false, true);
}
private void ProjectionCenterPropertyChanged()
{
if (MapProjection.IsAzimuthal)
{
ResetTransformCenter();
UpdateTransform();
}
}
private void AdjustCenterProperty(DependencyProperty property, ref Location center)
{
if (center == null)
{
center = new Location();
InternalSetValue(property, center);
}
else if (center.Longitude < -180d || center.Longitude > 180d ||
center.Latitude < -MapProjection.MaxLatitude || center.Latitude > MapProjection.MaxLatitude)
{
center = new Location(
Math.Min(Math.Max(center.Latitude, -MapProjection.MaxLatitude), MapProjection.MaxLatitude),
Location.NormalizeLongitude(center.Longitude));
InternalSetValue(property, center);
}
}
private void CenterPropertyChanged(Location center)
{
if (!internalPropertyChange)
{
AdjustCenterProperty(CenterProperty, ref center);
UpdateTransform();
if (centerAnimation == null)
{
InternalSetValue(TargetCenterProperty, center);
InternalSetValue(CenterPointProperty, MapProjection.LocationToPoint(center));
}
}
}
private void TargetCenterPropertyChanged(Location targetCenter)
{
if (!internalPropertyChange)
{
AdjustCenterProperty(TargetCenterProperty, ref targetCenter);
if (!targetCenter.Equals(Center))
{
if (centerAnimation != null)
{
centerAnimation.Completed -= CenterAnimationCompleted;
}
// animate private CenterPoint property by PointAnimation
centerAnimation = new PointAnimation
{
From = MapProjection.LocationToPoint(Center),
To = MapProjection.LocationToPoint(new Location(
targetCenter.Latitude,
Location.NearestLongitude(targetCenter.Longitude, Center.Longitude))),
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction
};
centerAnimation.Completed += CenterAnimationCompleted;
this.BeginAnimation(CenterPointProperty, centerAnimation);
}
}
}
private void CenterAnimationCompleted(object sender, object e)
{
if (centerAnimation != null)
{
centerAnimation.Completed -= CenterAnimationCompleted;
centerAnimation = null;
InternalSetValue(CenterProperty, TargetCenter);
InternalSetValue(CenterPointProperty, MapProjection.LocationToPoint(TargetCenter));
RemoveAnimation(CenterPointProperty); // remove holding animation in WPF
UpdateTransform();
}
}
private void CenterPointPropertyChanged(Point centerPoint)
{
if (!internalPropertyChange)
{
var center = MapProjection.PointToLocation(centerPoint);
center.Longitude = Location.NormalizeLongitude(center.Longitude);
InternalSetValue(CenterProperty, center);
UpdateTransform();
}
}
private void MinZoomLevelPropertyChanged(double minZoomLevel)
{
if (minZoomLevel < 0d || minZoomLevel > MaxZoomLevel)
{
minZoomLevel = Math.Min(Math.Max(minZoomLevel, 0d), MaxZoomLevel);
InternalSetValue(MinZoomLevelProperty, minZoomLevel);
}
if (ZoomLevel < minZoomLevel)
{
ZoomLevel = minZoomLevel;
}
}
private void MaxZoomLevelPropertyChanged(double maxZoomLevel)
{
if (maxZoomLevel < MinZoomLevel || maxZoomLevel > MaximumZoomLevel)
{
maxZoomLevel = Math.Min(Math.Max(maxZoomLevel, MinZoomLevel), MaximumZoomLevel);
InternalSetValue(MaxZoomLevelProperty, maxZoomLevel);
}
if (ZoomLevel > maxZoomLevel)
{
ZoomLevel = maxZoomLevel;
}
}
private void AdjustZoomLevelProperty(DependencyProperty property, ref double zoomLevel)
{
if (zoomLevel < MinZoomLevel || zoomLevel > MaxZoomLevel)
{
zoomLevel = Math.Min(Math.Max(zoomLevel, MinZoomLevel), MaxZoomLevel);
InternalSetValue(property, zoomLevel);
}
}
private void ZoomLevelPropertyChanged(double zoomLevel)
{
if (!internalPropertyChange)
{
AdjustZoomLevelProperty(ZoomLevelProperty, ref zoomLevel);
UpdateTransform();
if (zoomLevelAnimation == null)
{
InternalSetValue(TargetZoomLevelProperty, zoomLevel);
}
}
}
private void TargetZoomLevelPropertyChanged(double targetZoomLevel)
{
if (!internalPropertyChange)
{
AdjustZoomLevelProperty(TargetZoomLevelProperty, ref targetZoomLevel);
if (targetZoomLevel != ZoomLevel)
{
if (zoomLevelAnimation != null)
{
zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted;
}
zoomLevelAnimation = new DoubleAnimation
{
To = targetZoomLevel,
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction
};
zoomLevelAnimation.Completed += ZoomLevelAnimationCompleted;
this.BeginAnimation(ZoomLevelProperty, zoomLevelAnimation);
}
}
}
private void ZoomLevelAnimationCompleted(object sender, object e)
{
if (zoomLevelAnimation != null)
{
zoomLevelAnimation.Completed -= ZoomLevelAnimationCompleted;
zoomLevelAnimation = null;
InternalSetValue(ZoomLevelProperty, TargetZoomLevel);
RemoveAnimation(ZoomLevelProperty); // remove holding animation in WPF
UpdateTransform(true);
}
}
private void AdjustHeadingProperty(DependencyProperty property, ref double heading)
{
if (heading < 0d || heading > 360d)
{
heading = ((heading % 360d) + 360d) % 360d;
InternalSetValue(property, heading);
}
}
private void HeadingPropertyChanged(double heading)
{
if (!internalPropertyChange)
{
AdjustHeadingProperty(HeadingProperty, ref heading);
UpdateTransform();
if (headingAnimation == null)
{
InternalSetValue(TargetHeadingProperty, heading);
}
}
}
private void TargetHeadingPropertyChanged(double targetHeading)
{
if (!internalPropertyChange)
{
AdjustHeadingProperty(TargetHeadingProperty, ref targetHeading);
if (targetHeading != Heading)
{
var delta = targetHeading - Heading;
if (delta > 180d)
{
delta -= 360d;
}
else if (delta < -180d)
{
delta += 360d;
}
if (headingAnimation != null)
{
headingAnimation.Completed -= HeadingAnimationCompleted;
}
headingAnimation = new DoubleAnimation
{
By = delta,
Duration = AnimationDuration,
EasingFunction = AnimationEasingFunction
};
headingAnimation.Completed += HeadingAnimationCompleted;
this.BeginAnimation(HeadingProperty, headingAnimation);
}
}
}
private void HeadingAnimationCompleted(object sender, object e)
{
if (headingAnimation != null)
{
headingAnimation.Completed -= HeadingAnimationCompleted;
headingAnimation = null;
InternalSetValue(HeadingProperty, TargetHeading);
RemoveAnimation(HeadingProperty); // remove holding animation in WPF
UpdateTransform();
}
}
private void InternalSetValue(DependencyProperty property, object value)
{
internalPropertyChange = true;
SetValue(property, value);
internalPropertyChange = false;
}
private void UpdateTransform(bool resetTransformCenter = false, bool projectionChanged = false)
{
var projection = MapProjection;
var center = transformCenter ?? Center;
projection.SetViewportTransform(ProjectionCenter ?? Center, center, viewportCenter, ZoomLevel, Heading);
if (transformCenter != null)
{
center = projection.ViewportPointToLocation(new Point(RenderSize.Width / 2d, RenderSize.Height / 2d));
center.Longitude = Location.NormalizeLongitude(center.Longitude);
if (center.Latitude < -projection.MaxLatitude || center.Latitude > projection.MaxLatitude)
{
center.Latitude = Math.Min(Math.Max(center.Latitude, -projection.MaxLatitude), projection.MaxLatitude);
resetTransformCenter = true;
}
InternalSetValue(CenterProperty, center);
if (centerAnimation == null)
{
InternalSetValue(TargetCenterProperty, center);
InternalSetValue(CenterPointProperty, projection.LocationToPoint(center));
}
if (resetTransformCenter)
{
ResetTransformCenter();
projection.SetViewportTransform(ProjectionCenter ?? center, center, viewportCenter, ZoomLevel, Heading);
}
}
var scale = projection.GetMapScale(center);
ScaleTransform.ScaleX = scale.X;
ScaleTransform.ScaleY = scale.Y;
RotateTransform.Angle = Heading;
OnViewportChanged(new ViewportChangedEventArgs(projectionChanged, Center.Longitude - centerLongitude));
centerLongitude = Center.Longitude;
}
protected override void OnViewportChanged(ViewportChangedEventArgs e)
{
base.OnViewportChanged(e);
ViewportChanged?.Invoke(this, e);
}
}
}

View file

@ -0,0 +1,83 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
#if WINDOWS_UWP
using Windows.UI.Xaml;
#else
using System.Windows;
#endif
namespace MapControl
{
/// <summary>
/// Draws a graticule overlay.
/// </summary>
public partial class MapGraticule : MapOverlay
{
public static readonly DependencyProperty MinLineDistanceProperty = DependencyProperty.Register(
nameof(MinLineDistance), typeof(double), typeof(MapGraticule), new PropertyMetadata(150d));
/// <summary>
/// Minimum graticule line distance in pixels. The default value is 150.
/// </summary>
public double MinLineDistance
{
get { return (double)GetValue(MinLineDistanceProperty); }
set { SetValue(MinLineDistanceProperty, value); }
}
private double GetLineDistance()
{
var minDistance = MinLineDistance / MapProjection.DegreesToViewportScale(ParentMap.ZoomLevel);
var scale = 1d;
if (minDistance < 1d)
{
scale = minDistance < 1d / 60d ? 3600d : 60d;
minDistance *= scale;
}
var lineDistances = new double[] { 1d, 2d, 5d, 10d, 15d, 30d, 60d };
var i = 0;
while (i < lineDistances.Length - 1 && lineDistances[i] < minDistance)
{
i++;
}
return lineDistances[i] / scale;
}
private static string GetLabelFormat(double lineDistance)
{
if (lineDistance < 1d / 60d)
{
return "{0} {1}°{2:00}'{3:00}\"";
}
if (lineDistance < 1d)
{
return "{0} {1}°{2:00}'";
}
return "{0} {1}°";
}
private static string GetLabelText(double value, string format, string hemispheres)
{
var hemisphere = hemispheres[0];
if (value < -1e-8) // ~1mm
{
value = -value;
hemisphere = hemispheres[1];
}
var seconds = (int)Math.Round(value * 3600d);
return string.Format(format, hemisphere, seconds / 3600, (seconds / 60) % 60, seconds % 60);
}
}
}

View file

@ -0,0 +1,340 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Diagnostics;
#if WINDOWS_UWP
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;
using Windows.UI.Xaml.Media.Imaging;
#else
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
#endif
namespace MapControl
{
/// <summary>
/// Map image layer. Fills the entire viewport with a map image, e.g. provided by a Web Map Service (WMS).
/// The image must be provided by the abstract UpdateImage(BoundingBox) method.
/// </summary>
public abstract partial class MapImageLayer : MapPanel, IMapLayer
{
public static readonly DependencyProperty MinLatitudeProperty = DependencyProperty.Register(
nameof(MinLatitude), typeof(double), typeof(MapImageLayer), new PropertyMetadata(double.NaN));
public static readonly DependencyProperty MaxLatitudeProperty = DependencyProperty.Register(
nameof(MaxLatitude), typeof(double), typeof(MapImageLayer), new PropertyMetadata(double.NaN));
public static readonly DependencyProperty MinLongitudeProperty = DependencyProperty.Register(
nameof(MinLongitude), typeof(double), typeof(MapImageLayer), new PropertyMetadata(double.NaN));
public static readonly DependencyProperty MaxLongitudeProperty = DependencyProperty.Register(
nameof(MaxLongitude), typeof(double), typeof(MapImageLayer), new PropertyMetadata(double.NaN));
public static readonly DependencyProperty MaxBoundingBoxWidthProperty = DependencyProperty.Register(
nameof(MaxBoundingBoxWidth), typeof(double), typeof(MapImageLayer), new PropertyMetadata(double.NaN));
public static readonly DependencyProperty RelativeImageSizeProperty = DependencyProperty.Register(
nameof(RelativeImageSize), typeof(double), typeof(MapImageLayer), new PropertyMetadata(1d));
public static readonly DependencyProperty UpdateIntervalProperty = DependencyProperty.Register(
nameof(UpdateInterval), typeof(TimeSpan), typeof(MapImageLayer),
new PropertyMetadata(TimeSpan.FromSeconds(0.2), (o, e) => ((MapImageLayer)o).updateTimer.Interval = (TimeSpan)e.NewValue));
public static readonly DependencyProperty UpdateWhileViewportChangingProperty = DependencyProperty.Register(
nameof(UpdateWhileViewportChanging), typeof(bool), typeof(MapImageLayer), new PropertyMetadata(false));
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(
nameof(Description), typeof(string), typeof(MapImageLayer), new PropertyMetadata(null));
public static readonly DependencyProperty MapBackgroundProperty = DependencyProperty.Register(
nameof(MapBackground), typeof(Brush), typeof(MapImageLayer), new PropertyMetadata(null));
public static readonly DependencyProperty MapForegroundProperty = DependencyProperty.Register(
nameof(MapForeground), typeof(Brush), typeof(MapImageLayer), new PropertyMetadata(null));
private readonly DispatcherTimer updateTimer;
private BoundingBox boundingBox;
private int topImageIndex;
private bool updateInProgress;
public MapImageLayer()
{
Children.Add(new Image { Opacity = 0d, Stretch = Stretch.Fill });
Children.Add(new Image { Opacity = 0d, Stretch = Stretch.Fill });
updateTimer = new DispatcherTimer { Interval = UpdateInterval };
updateTimer.Tick += (s, e) => UpdateImage();
}
/// <summary>
/// Optional minimum latitude value. Default is NaN.
/// </summary>
public double MinLatitude
{
get { return (double)GetValue(MinLatitudeProperty); }
set { SetValue(MinLatitudeProperty, value); }
}
/// <summary>
/// Optional maximum latitude value. Default is NaN.
/// </summary>
public double MaxLatitude
{
get { return (double)GetValue(MaxLatitudeProperty); }
set { SetValue(MaxLatitudeProperty, value); }
}
/// <summary>
/// Optional minimum longitude value. Default is NaN.
/// </summary>
public double MinLongitude
{
get { return (double)GetValue(MinLongitudeProperty); }
set { SetValue(MinLongitudeProperty, value); }
}
/// <summary>
/// Optional maximum longitude value. Default is NaN.
/// </summary>
public double MaxLongitude
{
get { return (double)GetValue(MaxLongitudeProperty); }
set { SetValue(MaxLongitudeProperty, value); }
}
/// <summary>
/// Optional maximum width of the map image's bounding box. Default is NaN.
/// </summary>
public double MaxBoundingBoxWidth
{
get { return (double)GetValue(MaxBoundingBoxWidthProperty); }
set { SetValue(MaxBoundingBoxWidthProperty, value); }
}
/// <summary>
/// Relative size of the map image in relation to the current viewport size.
/// Setting a value greater than one will let MapImageLayer request images that
/// are larger than the viewport, in order to support smooth panning.
/// </summary>
public double RelativeImageSize
{
get { return (double)GetValue(RelativeImageSizeProperty); }
set { SetValue(RelativeImageSizeProperty, value); }
}
/// <summary>
/// Minimum time interval between images updates.
/// </summary>
public TimeSpan UpdateInterval
{
get { return (TimeSpan)GetValue(UpdateIntervalProperty); }
set { SetValue(UpdateIntervalProperty, value); }
}
/// <summary>
/// Controls if images are updated while the viewport is still changing.
/// </summary>
public bool UpdateWhileViewportChanging
{
get { return (bool)GetValue(UpdateWhileViewportChangingProperty); }
set { SetValue(UpdateWhileViewportChangingProperty, value); }
}
/// <summary>
/// Description of the MapImageLayer.
/// Used to display copyright information on top of the map.
/// </summary>
public string Description
{
get { return (string)GetValue(DescriptionProperty); }
set { SetValue(DescriptionProperty, value); }
}
/// <summary>
/// Optional foreground brush.
/// Sets MapBase.Foreground if not null and the MapImageLayer is the base map layer.
/// </summary>
public Brush MapForeground
{
get { return (Brush)GetValue(MapForegroundProperty); }
set { SetValue(MapForegroundProperty, value); }
}
/// <summary>
/// Optional background brush.
/// Sets MapBase.Background if not null and the MapImageLayer is the base map layer.
/// </summary>
public Brush MapBackground
{
get { return (Brush)GetValue(MapBackgroundProperty); }
set { SetValue(MapBackgroundProperty, value); }
}
protected override void OnViewportChanged(ViewportChangedEventArgs e)
{
base.OnViewportChanged(e);
if (e.ProjectionChanged)
{
UpdateImage((BitmapSource)null);
UpdateImage();
}
else
{
if (Math.Abs(e.LongitudeOffset) > 180d && boundingBox != null && boundingBox.HasValidBounds)
{
var offset = 360d * Math.Sign(e.LongitudeOffset);
boundingBox.West += offset;
boundingBox.East += offset;
foreach (UIElement element in Children)
{
var bbox = GetBoundingBox(element);
if (bbox != null && bbox.HasValidBounds)
{
SetBoundingBox(element, new BoundingBox(bbox.South, bbox.West + offset, bbox.North, bbox.East + offset));
}
}
}
if (updateTimer.IsEnabled && !UpdateWhileViewportChanging)
{
updateTimer.Stop(); // restart
}
if (!updateTimer.IsEnabled)
{
updateTimer.Start();
}
}
}
protected virtual void UpdateImage()
{
updateTimer.Stop();
if (updateInProgress)
{
updateTimer.Start(); // update image on next timer tick
}
else if (ParentMap != null && ParentMap.RenderSize.Width > 0 && ParentMap.RenderSize.Height > 0)
{
updateInProgress = true;
var width = ParentMap.RenderSize.Width * RelativeImageSize;
var height = ParentMap.RenderSize.Height * RelativeImageSize;
var x = (ParentMap.RenderSize.Width - width) / 2d;
var y = (ParentMap.RenderSize.Height - height) / 2d;
var rect = new Rect(x, y, width, height);
boundingBox = ParentMap.MapProjection.ViewportRectToBoundingBox(rect);
if (boundingBox != null && boundingBox.HasValidBounds)
{
if (!double.IsNaN(MinLatitude) && boundingBox.South < MinLatitude)
{
boundingBox.South = MinLatitude;
}
if (!double.IsNaN(MinLongitude) && boundingBox.West < MinLongitude)
{
boundingBox.West = MinLongitude;
}
if (!double.IsNaN(MaxLatitude) && boundingBox.North > MaxLatitude)
{
boundingBox.North = MaxLatitude;
}
if (!double.IsNaN(MaxLongitude) && boundingBox.East > MaxLongitude)
{
boundingBox.East = MaxLongitude;
}
if (!double.IsNaN(MaxBoundingBoxWidth) && boundingBox.Width > MaxBoundingBoxWidth)
{
var d = (boundingBox.Width - MaxBoundingBoxWidth) / 2d;
boundingBox.West += d;
boundingBox.East -= d;
}
}
var imageUpdated = false;
try
{
imageUpdated = UpdateImage(boundingBox);
}
catch (Exception ex)
{
Debug.WriteLine("MapImageLayer: " + ex.Message);
}
if (!imageUpdated)
{
UpdateImage((BitmapSource)null);
}
}
}
/// <summary>
/// Creates an image request Uri or a BitmapSource for the specified image bounding box.
/// Must either call UpdateImage(Uri) or UpdateImage(BitmapSource) or return false on failure.
/// </summary>
protected abstract bool UpdateImage(BoundingBox boundingBox);
private void SetTopImage(BitmapSource bitmapSource)
{
topImageIndex = (topImageIndex + 1) % 2;
var topImage = (Image)Children[topImageIndex];
topImage.Source = bitmapSource;
SetBoundingBox(topImage, boundingBox?.Clone());
}
private void SwapImages()
{
var topImage = (Image)Children[topImageIndex];
var bottomImage = (Image)Children[(topImageIndex + 1) % 2];
Canvas.SetZIndex(topImage, 1);
Canvas.SetZIndex(bottomImage, 0);
if (topImage.Source != null)
{
topImage.BeginAnimation(OpacityProperty, new DoubleAnimation
{
To = 1d,
Duration = Tile.FadeDuration
});
bottomImage.BeginAnimation(OpacityProperty, new DoubleAnimation
{
To = 0d,
BeginTime = Tile.FadeDuration,
Duration = TimeSpan.Zero
});
}
else
{
topImage.Opacity = 0d;
bottomImage.Opacity = 0d;
bottomImage.Source = null;
}
updateInProgress = false;
}
}
}

View file

@ -0,0 +1,110 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
#if WINDOWS_UWP
using Windows.UI.Text;
using Windows.UI.Xaml.Media;
#else
using System.Windows;
using System.Windows.Media;
#endif
namespace MapControl
{
/// <summary>
/// Base class for map overlays with background, foreground, stroke and font properties.
/// </summary>
public partial class MapOverlay : MapPanel
{
public double FontSize
{
get { return (double)GetValue(FontSizeProperty); }
set { SetValue(FontSizeProperty, value); }
}
public FontFamily FontFamily
{
get { return (FontFamily)GetValue(FontFamilyProperty); }
set { SetValue(FontFamilyProperty, value); }
}
public FontStyle FontStyle
{
get { return (FontStyle)GetValue(FontStyleProperty); }
set { SetValue(FontStyleProperty, value); }
}
public FontStretch FontStretch
{
get { return (FontStretch)GetValue(FontStretchProperty); }
set { SetValue(FontStretchProperty, value); }
}
public FontWeight FontWeight
{
get { return (FontWeight)GetValue(FontWeightProperty); }
set { SetValue(FontWeightProperty, value); }
}
public Brush Foreground
{
get { return (Brush)GetValue(ForegroundProperty); }
set { SetValue(ForegroundProperty, value); }
}
public Brush Stroke
{
get { return (Brush)GetValue(StrokeProperty); }
set { SetValue(StrokeProperty, value); }
}
public double StrokeThickness
{
get { return (double)GetValue(StrokeThicknessProperty); }
set { SetValue(StrokeThicknessProperty, value); }
}
public DoubleCollection StrokeDashArray
{
get { return (DoubleCollection)GetValue(StrokeDashArrayProperty); }
set { SetValue(StrokeDashArrayProperty, value); }
}
public double StrokeDashOffset
{
get { return (double)GetValue(StrokeDashOffsetProperty); }
set { SetValue(StrokeDashOffsetProperty, value); }
}
public PenLineCap StrokeDashCap
{
get { return (PenLineCap)GetValue(StrokeDashCapProperty); }
set { SetValue(StrokeDashCapProperty, value); }
}
public PenLineCap StrokeStartLineCap
{
get { return (PenLineCap)GetValue(StrokeStartLineCapProperty); }
set { SetValue(StrokeStartLineCapProperty, value); }
}
public PenLineCap StrokeEndLineCap
{
get { return (PenLineCap)GetValue(StrokeEndLineCapProperty); }
set { SetValue(StrokeEndLineCapProperty, value); }
}
public PenLineJoin StrokeLineJoin
{
get { return (PenLineJoin)GetValue(StrokeLineJoinProperty); }
set { SetValue(StrokeLineJoinProperty, value); }
}
public double StrokeMiterLimit
{
get { return (double)GetValue(StrokeMiterLimitProperty); }
set { SetValue(StrokeMiterLimitProperty, value); }
}
}
}

View file

@ -0,0 +1,394 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
#if WINDOWS_UWP
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
#else
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
#endif
namespace MapControl
{
public interface IMapElement
{
MapBase ParentMap { get; set; }
}
/// <summary>
/// Arranges child elements on a Map at positions specified by the attached property Location,
/// or in rectangles specified by the attached property BoundingBox.
/// An element's viewport position is assigned as TranslateTransform to its RenderTransform property,
/// either directly or as last child of a TransformGroup.
/// </summary>
public partial class MapPanel : Panel, IMapElement
{
public static readonly DependencyProperty LocationProperty = DependencyProperty.RegisterAttached(
"Location", typeof(Location), typeof(MapPanel), new PropertyMetadata(null, LocationPropertyChanged));
public static readonly DependencyProperty BoundingBoxProperty = DependencyProperty.RegisterAttached(
"BoundingBox", typeof(BoundingBox), typeof(MapPanel), new PropertyMetadata(null, BoundingBoxPropertyChanged));
public static Location GetLocation(UIElement element)
{
return (Location)element.GetValue(LocationProperty);
}
public static void SetLocation(UIElement element, Location value)
{
element.SetValue(LocationProperty, value);
}
public static BoundingBox GetBoundingBox(UIElement element)
{
return (BoundingBox)element.GetValue(BoundingBoxProperty);
}
public static void SetBoundingBox(UIElement element, BoundingBox value)
{
element.SetValue(BoundingBoxProperty, value);
}
private MapBase parentMap;
public MapBase ParentMap
{
get { return parentMap; }
set { SetParentMapOverride(value); }
}
protected override Size MeasureOverride(Size availableSize)
{
availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
foreach (UIElement element in Children)
{
element.Measure(availableSize);
}
return new Size();
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement element in Children)
{
BoundingBox boundingBox;
Location location;
if ((boundingBox = GetBoundingBox(element)) != null)
{
ArrangeElementWithBoundingBox(element);
SetBoundingBoxRect(element, parentMap, boundingBox);
}
else if ((location = GetLocation(element)) != null)
{
ArrangeElementWithLocation(element);
SetViewportPosition(element, parentMap, location);
}
else
{
ArrangeElement(element, finalSize);
}
}
return finalSize;
}
protected virtual void SetParentMapOverride(MapBase map)
{
if (parentMap != null && parentMap != this)
{
parentMap.ViewportChanged -= OnViewportChanged;
}
parentMap = map;
if (parentMap != null && parentMap != this)
{
parentMap.ViewportChanged += OnViewportChanged;
OnViewportChanged(new ViewportChangedEventArgs());
}
}
protected virtual void OnViewportChanged(ViewportChangedEventArgs e)
{
foreach (UIElement element in Children)
{
BoundingBox boundingBox;
Location location;
if ((boundingBox = GetBoundingBox(element)) != null)
{
SetBoundingBoxRect(element, parentMap, boundingBox);
}
else if ((location = GetLocation(element)) != null)
{
SetViewportPosition(element, parentMap, location);
}
}
}
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
{
OnViewportChanged(e);
}
private static void ParentMapPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var mapElement = obj as IMapElement;
if (mapElement != null)
{
mapElement.ParentMap = e.NewValue as MapBase;
}
}
private static void LocationPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var element = (UIElement)obj;
var map = GetParentMap(element);
var location = (Location)e.NewValue;
if (location == null)
{
ArrangeElement(element, map?.RenderSize ?? new Size());
}
else if (e.OldValue == null)
{
ArrangeElementWithLocation(element); // arrange once when Location was null before
}
SetViewportPosition(element, map, location);
}
private static void BoundingBoxPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var element = (FrameworkElement)obj;
var map = GetParentMap(element);
var boundingBox = (BoundingBox)e.NewValue;
if (boundingBox == null)
{
ArrangeElement(element, map?.RenderSize ?? new Size());
}
else if (e.OldValue == null)
{
ArrangeElementWithBoundingBox(element); // arrange once when BoundingBox was null before
}
SetBoundingBoxRect(element, map, boundingBox);
}
private static void SetViewportPosition(UIElement element, MapBase parentMap, Location location)
{
var viewportPosition = new Point();
if (parentMap != null && location != null)
{
viewportPosition = parentMap.MapProjection.LocationToViewportPoint(location);
if (viewportPosition.X < 0d || viewportPosition.X > parentMap.RenderSize.Width ||
viewportPosition.Y < 0d || viewportPosition.Y > parentMap.RenderSize.Height)
{
viewportPosition = parentMap.MapProjection.LocationToViewportPoint(new Location(
location.Latitude,
Location.NearestLongitude(location.Longitude, parentMap.Center.Longitude)));
}
if ((bool)element.GetValue(UseLayoutRoundingProperty))
{
viewportPosition.X = Math.Round(viewportPosition.X);
viewportPosition.Y = Math.Round(viewportPosition.Y);
}
}
var translateTransform = element.RenderTransform as TranslateTransform;
if (translateTransform == null)
{
var transformGroup = element.RenderTransform as TransformGroup;
if (transformGroup == null)
{
translateTransform = new TranslateTransform();
element.RenderTransform = translateTransform;
}
else
{
if (transformGroup.Children.Count > 0)
{
translateTransform = transformGroup.Children[transformGroup.Children.Count - 1] as TranslateTransform;
}
if (translateTransform == null)
{
translateTransform = new TranslateTransform();
transformGroup.Children.Add(translateTransform);
}
}
}
translateTransform.X = viewportPosition.X;
translateTransform.Y = viewportPosition.Y;
}
private static void SetBoundingBoxRect(UIElement element, MapBase parentMap, BoundingBox boundingBox)
{
var rotation = 0d;
var viewportPosition = new Point();
if (parentMap != null && boundingBox != null)
{
var projection = parentMap.MapProjection;
var rect = projection.BoundingBoxToRect(boundingBox);
var center = new Point(rect.X + rect.Width / 2d, rect.Y + rect.Height / 2d);
rotation = parentMap.Heading;
viewportPosition = projection.ViewportTransform.Transform(center);
if (viewportPosition.X < 0d || viewportPosition.X > parentMap.RenderSize.Width ||
viewportPosition.Y < 0d || viewportPosition.Y > parentMap.RenderSize.Height)
{
var location = projection.PointToLocation(center);
location.Longitude = Location.NearestLongitude(location.Longitude, parentMap.Center.Longitude);
viewportPosition = projection.LocationToViewportPoint(location);
}
var width = rect.Width * projection.ViewportScale;
var height = rect.Height * projection.ViewportScale;
if (element is FrameworkElement)
{
((FrameworkElement)element).Width = width;
((FrameworkElement)element).Height = height;
}
else
{
element.Arrange(new Rect(-width / 2d, -height / 2d, width, height)); // ???
}
}
var transformGroup = element.RenderTransform as TransformGroup;
RotateTransform rotateTransform;
TranslateTransform translateTransform;
if (transformGroup == null ||
transformGroup.Children.Count != 2 ||
(rotateTransform = transformGroup.Children[0] as RotateTransform) == null ||
(translateTransform = transformGroup.Children[1] as TranslateTransform) == null)
{
transformGroup = new TransformGroup();
rotateTransform = new RotateTransform();
translateTransform = new TranslateTransform();
transformGroup.Children.Add(rotateTransform);
transformGroup.Children.Add(translateTransform);
element.RenderTransform = transformGroup;
element.RenderTransformOrigin = new Point(0.5, 0.5);
}
rotateTransform.Angle = rotation;
translateTransform.X = viewportPosition.X;
translateTransform.Y = viewportPosition.Y;
}
private static void ArrangeElementWithBoundingBox(UIElement element)
{
var size = element.DesiredSize;
element.Arrange(new Rect(-size.Width / 2d, -size.Height / 2d, size.Width, size.Height));
}
private static void ArrangeElementWithLocation(UIElement element)
{
var rect = new Rect(new Point(), element.DesiredSize);
if (element is FrameworkElement)
{
switch (((FrameworkElement)element).HorizontalAlignment)
{
case HorizontalAlignment.Center:
rect.X = -rect.Width / 2d;
break;
case HorizontalAlignment.Right:
rect.X = -rect.Width;
break;
default:
break;
}
switch (((FrameworkElement)element).VerticalAlignment)
{
case VerticalAlignment.Center:
rect.Y = -rect.Height / 2d;
break;
case VerticalAlignment.Bottom:
rect.Y = -rect.Height;
break;
default:
break;
}
}
element.Arrange(rect);
}
private static void ArrangeElement(UIElement element, Size parentSize)
{
var rect = new Rect(new Point(), element.DesiredSize);
if (element is FrameworkElement)
{
switch (((FrameworkElement)element).HorizontalAlignment)
{
case HorizontalAlignment.Center:
rect.X = (parentSize.Width - rect.Width) / 2d;
break;
case HorizontalAlignment.Right:
rect.X = parentSize.Width - rect.Width;
break;
case HorizontalAlignment.Stretch:
rect.Width = parentSize.Width;
break;
default:
break;
}
switch (((FrameworkElement)element).VerticalAlignment)
{
case VerticalAlignment.Center:
rect.Y = (parentSize.Height - rect.Height) / 2d;
break;
case VerticalAlignment.Bottom:
rect.Y = parentSize.Height - rect.Height;
break;
case VerticalAlignment.Stretch:
rect.Height = parentSize.Height;
break;
default:
break;
}
}
element.Arrange(rect);
}
}
}

View file

@ -0,0 +1,112 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
#if WINDOWS_UWP
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
#else
using System.Windows;
using System.Windows.Media;
#endif
namespace MapControl
{
/// <summary>
/// Base class for map shapes. The shape geometry is given by the Data property,
/// which must contain a Geometry defined in cartesian (projected) map coordinates.
/// Optionally, the Location property can by set to adjust the viewport position to the
/// visible map viewport, as done for elements where the MapPanel.Location property is set.
/// </summary>
public partial class MapPath : IMapElement
{
public static readonly DependencyProperty LocationProperty = DependencyProperty.Register(
nameof(Location), typeof(Location), typeof(MapPath),
new PropertyMetadata(null, (o, e) => ((MapPath)o).LocationPropertyChanged()));
public Location Location
{
get { return (Location)GetValue(LocationProperty); }
set { SetValue(LocationProperty, value); }
}
private readonly TransformGroup viewportTransform = new TransformGroup();
private MapBase parentMap;
public MapBase ParentMap
{
get { return parentMap; }
set
{
if (parentMap != null)
{
parentMap.ViewportChanged -= OnViewportChanged;
}
viewportTransform.Children.Clear();
parentMap = value;
if (parentMap != null)
{
viewportTransform.Children.Add(new TranslateTransform());
viewportTransform.Children.Add(parentMap.MapProjection.ViewportTransform);
parentMap.ViewportChanged += OnViewportChanged;
}
UpdateData();
}
}
protected virtual void UpdateData()
{
}
protected virtual void OnViewportChanged(ViewportChangedEventArgs e)
{
double longitudeScale = parentMap.MapProjection.LongitudeScale;
if (e.ProjectionChanged)
{
viewportTransform.Children[1] = parentMap.MapProjection.ViewportTransform;
}
if (e.ProjectionChanged || double.IsNaN(longitudeScale))
{
UpdateData();
}
if (!double.IsNaN(longitudeScale)) // a normal cylindrical projection
{
var longitudeOffset = 0d;
if (Location != null)
{
var viewportPosition = parentMap.MapProjection.LocationToViewportPoint(Location);
if (viewportPosition.X < 0d || viewportPosition.X > parentMap.RenderSize.Width ||
viewportPosition.Y < 0d || viewportPosition.Y > parentMap.RenderSize.Height)
{
var nearestLongitude = Location.NearestLongitude(Location.Longitude, parentMap.Center.Longitude);
longitudeOffset = longitudeScale * (nearestLongitude - Location.Longitude);
}
}
((TranslateTransform)viewportTransform.Children[0]).X = longitudeOffset;
}
}
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
{
OnViewportChanged(e);
}
private void LocationPropertyChanged()
{
if (parentMap != null)
{
OnViewportChanged(new ViewportChangedEventArgs());
}
}
}
}

View file

@ -0,0 +1,71 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System.Collections.Generic;
using System.Collections.Specialized;
#if WINDOWS_UWP
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
#else
using System.Windows;
using System.Windows.Media;
#endif
namespace MapControl
{
/// <summary>
/// A polyline or polygon created from a collection of Locations.
/// </summary>
public partial class MapPolyline : MapPath
{
public static readonly DependencyProperty LocationsProperty = DependencyProperty.Register(
nameof(Locations), typeof(IEnumerable<Location>), typeof(MapPolyline),
new PropertyMetadata(null, (o, e) => ((MapPolyline)o).LocationsPropertyChanged(e)));
public static readonly DependencyProperty IsClosedProperty = DependencyProperty.Register(
nameof(IsClosed), typeof(bool), typeof(MapPolyline),
new PropertyMetadata(false, (o, e) => ((MapPolyline)o).UpdateData()));
/// <summary>
/// Gets or sets a value that indicates if the polyline is closed, i.e. is a polygon.
/// </summary>
public bool IsClosed
{
get { return (bool)GetValue(IsClosedProperty); }
set { SetValue(IsClosedProperty, value); }
}
/// <summary>
/// Gets or sets the FillRule of the PathGeometry that represents the polyline.
/// </summary>
public FillRule FillRule
{
get { return (FillRule)GetValue(FillRuleProperty); }
set { SetValue(FillRuleProperty, value); }
}
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();
}
}
}

View file

@ -0,0 +1,186 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Globalization;
#if WINDOWS_UWP
using Windows.Foundation;
using Windows.UI.Xaml.Media;
#else
using System.Windows;
using System.Windows.Media;
#endif
namespace MapControl
{
/// <summary>
/// Defines a map projection between geographic coordinates, cartesian map coordinates and viewport coordinates.
/// </summary>
public abstract class MapProjection
{
public const int TileSize = 256;
public const double Wgs84EquatorialRadius = 6378137d;
public const double MetersPerDegree = Wgs84EquatorialRadius * Math.PI / 180d;
/// <summary>
/// Gets the scaling factor from cartesian map coordinates in degrees to viewport coordinates for the specified zoom level.
/// </summary>
public static double DegreesToViewportScale(double zoomLevel)
{
return Math.Pow(2d, zoomLevel) * TileSize / 360d;
}
/// <summary>
/// Gets or sets the WMS 1.3.0 CRS Identifier.
/// </summary>
public string CrsId { get; set; }
/// <summary>
/// Indicates if this is a web mercator projection, i.e. compatible with map tile layers.
/// </summary>
public bool IsWebMercator { get; protected set; } = false;
/// <summary>
/// Indicates if this is an azimuthal projection.
/// </summary>
public bool IsAzimuthal { get; protected set; } = false;
/// <summary>
/// Gets the scale factor from longitude to x values of a normal cylindrical projection.
/// Returns NaN if this is not a normal cylindrical projection.
/// </summary>
public double LongitudeScale { get; protected set; } = 1d;
/// <summary>
/// Gets the absolute value of the minimum and maximum latitude that can be transformed.
/// </summary>
public double MaxLatitude { get; protected set; } = 90d;
/// <summary>
/// Gets the scaling factor from cartesian map coordinates to viewport coordinates.
/// </summary>
public double ViewportScale { get; protected set; }
/// <summary>
/// Gets the transformation from cartesian map coordinates to viewport coordinates (pixels).
/// </summary>
public MatrixTransform ViewportTransform { get; } = new MatrixTransform();
/// <summary>
/// Gets the scaling factor from cartesian map coordinates to viewport coordinates for the specified zoom level.
/// </summary>
public virtual double GetViewportScale(double zoomLevel)
{
return DegreesToViewportScale(zoomLevel);
}
/// <summary>
/// Gets the map scale at the specified Location as viewport coordinate units per meter (px/m).
/// </summary>
public abstract Point GetMapScale(Location location);
/// <summary>
/// Transforms a Location in geographic coordinates to a Point in cartesian map coordinates.
/// </summary>
public abstract Point LocationToPoint(Location location);
/// <summary>
/// Transforms a Point in cartesian map coordinates to a Location in geographic coordinates.
/// </summary>
public abstract Location PointToLocation(Point point);
/// <summary>
/// Translates a Location in geographic coordinates by the specified small amount in viewport coordinates.
/// </summary>
public abstract Location TranslateLocation(Location location, Point translation);
/// <summary>
/// Transforms a BoundingBox in geographic coordinates to a Rect in cartesian map coordinates.
/// </summary>
public virtual Rect BoundingBoxToRect(BoundingBox boundingBox)
{
return new Rect(
LocationToPoint(new Location(boundingBox.South, boundingBox.West)),
LocationToPoint(new Location(boundingBox.North, boundingBox.East)));
}
/// <summary>
/// Transforms a Rect in cartesian map coordinates to a BoundingBox in geographic coordinates.
/// </summary>
public virtual BoundingBox RectToBoundingBox(Rect rect)
{
var sw = PointToLocation(new Point(rect.X, rect.Y));
var ne = PointToLocation(new Point(rect.X + rect.Width, rect.Y + rect.Height));
return new BoundingBox(sw.Latitude, sw.Longitude, ne.Latitude, ne.Longitude);
}
/// <summary>
/// Transforms a Location in geographic coordinates to a Point in viewport coordinates.
/// </summary>
public Point LocationToViewportPoint(Location location)
{
return ViewportTransform.Transform(LocationToPoint(location));
}
/// <summary>
/// Transforms a Point in viewport coordinates to a Location in geographic coordinates.
/// </summary>
public Location ViewportPointToLocation(Point point)
{
return PointToLocation(ViewportTransform.Inverse.Transform(point));
}
/// <summary>
/// Transforms a Rect in viewport coordinates to a BoundingBox in geographic coordinates.
/// </summary>
public BoundingBox ViewportRectToBoundingBox(Rect rect)
{
return RectToBoundingBox(ViewportTransform.Inverse.TransformBounds(rect));
}
/// <summary>
/// Sets ViewportScale and ViewportTransform values.
/// </summary>
public virtual void SetViewportTransform(Location projectionCenter, Location mapCenter, Point viewportCenter, double zoomLevel, double heading)
{
ViewportScale = GetViewportScale(zoomLevel);
var center = LocationToPoint(mapCenter);
ViewportTransform.Matrix = MatrixEx.TranslateScaleRotateTranslate(
-center.X, -center.Y, ViewportScale, -ViewportScale, heading, viewportCenter.X, viewportCenter.Y);
}
/// <summary>
/// Gets a WMS 1.3.0 query parameter string from the specified bounding box,
/// e.g. "CRS=...&BBOX=...&WIDTH=...&HEIGHT=..."
/// </summary>
public virtual string WmsQueryParameters(BoundingBox boundingBox, string version = "1.3.0")
{
if (string.IsNullOrEmpty(CrsId))
{
return null;
}
var format = "CRS={0}&BBOX={1},{2},{3},{4}&WIDTH={5}&HEIGHT={6}";
if (version.StartsWith("1.1."))
{
format = "SRS={0}&BBOX={1},{2},{3},{4}&WIDTH={5}&HEIGHT={6}";
}
else if (CrsId == "EPSG:4326")
{
format = "CRS={0}&BBOX={2},{1},{4},{3}&WIDTH={5}&HEIGHT={6}";
}
var rect = BoundingBoxToRect(boundingBox);
var width = (int)Math.Round(ViewportScale * rect.Width);
var height = (int)Math.Round(ViewportScale * rect.Height);
return string.Format(CultureInfo.InvariantCulture, format, CrsId,
rect.X, rect.Y, (rect.X + rect.Width), (rect.Y + rect.Height), width, height);
}
}
}

View file

@ -0,0 +1,406 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Generic;
using System.Linq;
#if WINDOWS_UWP
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
#else
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
#endif
namespace MapControl
{
public interface ITileImageLoader
{
void LoadTiles(MapTileLayer tileLayer);
}
/// <summary>
/// Fills the map viewport with map tiles from a TileSource.
/// </summary>
public partial class MapTileLayer : Panel, IMapLayer
{
/// <summary>
/// A default MapTileLayer using OpenStreetMap data.
/// </summary>
public static MapTileLayer OpenStreetMapTileLayer
{
get
{
return new MapTileLayer
{
SourceName = "OpenStreetMap",
Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)",
TileSource = new TileSource { UriFormat = "http://{c}.tile.openstreetmap.org/{z}/{x}/{y}.png" },
MaxZoomLevel = 19
};
}
}
public static readonly DependencyProperty TileSourceProperty = DependencyProperty.Register(
nameof(TileSource), typeof(TileSource), typeof(MapTileLayer),
new PropertyMetadata(null, (o, e) => ((MapTileLayer)o).TileSourcePropertyChanged()));
public static readonly DependencyProperty SourceNameProperty = DependencyProperty.Register(
nameof(SourceName), typeof(string), typeof(MapTileLayer), new PropertyMetadata(null));
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(
nameof(Description), typeof(string), typeof(MapTileLayer), new PropertyMetadata(null));
public static readonly DependencyProperty ZoomLevelOffsetProperty = DependencyProperty.Register(
nameof(ZoomLevelOffset), typeof(double), typeof(MapTileLayer),
new PropertyMetadata(0d, (o, e) => ((MapTileLayer)o).UpdateTileGrid()));
public static readonly DependencyProperty MinZoomLevelProperty = DependencyProperty.Register(
nameof(MinZoomLevel), typeof(int), typeof(MapTileLayer), new PropertyMetadata(0));
public static readonly DependencyProperty MaxZoomLevelProperty = DependencyProperty.Register(
nameof(MaxZoomLevel), typeof(int), typeof(MapTileLayer), new PropertyMetadata(18));
public static readonly DependencyProperty MaxParallelDownloadsProperty = DependencyProperty.Register(
nameof(MaxParallelDownloads), typeof(int), typeof(MapTileLayer), new PropertyMetadata(4));
public static readonly DependencyProperty UpdateIntervalProperty = DependencyProperty.Register(
nameof(UpdateInterval), typeof(TimeSpan), typeof(MapTileLayer),
new PropertyMetadata(TimeSpan.FromSeconds(0.2), (o, e) => ((MapTileLayer)o).updateTimer.Interval = (TimeSpan)e.NewValue));
public static readonly DependencyProperty UpdateWhileViewportChangingProperty = DependencyProperty.Register(
nameof(UpdateWhileViewportChanging), typeof(bool), typeof(MapTileLayer), new PropertyMetadata(true));
public static readonly DependencyProperty MapBackgroundProperty = DependencyProperty.Register(
nameof(MapBackground), typeof(Brush), typeof(MapTileLayer), new PropertyMetadata(null));
public static readonly DependencyProperty MapForegroundProperty = DependencyProperty.Register(
nameof(MapForeground), typeof(Brush), typeof(MapTileLayer), new PropertyMetadata(null));
private readonly DispatcherTimer updateTimer;
private MapBase parentMap;
public MapTileLayer()
: this(new TileImageLoader())
{
}
public MapTileLayer(ITileImageLoader tileImageLoader)
{
Initialize();
RenderTransform = new MatrixTransform();
TileImageLoader = tileImageLoader;
Tiles = new List<Tile>();
updateTimer = new DispatcherTimer { Interval = UpdateInterval };
updateTimer.Tick += (s, e) => UpdateTileGrid();
}
partial void Initialize(); // Windows Runtime and Silverlight only
public ITileImageLoader TileImageLoader { get; private set; }
public ICollection<Tile> Tiles { get; private set; }
public TileGrid TileGrid { get; private set; }
/// <summary>
/// Provides map tile URIs or images.
/// </summary>
public TileSource TileSource
{
get { return (TileSource)GetValue(TileSourceProperty); }
set { SetValue(TileSourceProperty, value); }
}
/// <summary>
/// Name of the TileSource. Used as component of a tile cache key.
/// </summary>
public string SourceName
{
get { return (string)GetValue(SourceNameProperty); }
set { SetValue(SourceNameProperty, value); }
}
/// <summary>
/// Description of the MapTileLayer. Used to display copyright information on top of the map.
/// </summary>
public string Description
{
get { return (string)GetValue(DescriptionProperty); }
set { SetValue(DescriptionProperty, value); }
}
/// <summary>
/// Adds an offset to the Map's ZoomLevel for a relative scale between the Map and the MapTileLayer.
/// </summary>
public double ZoomLevelOffset
{
get { return (double)GetValue(ZoomLevelOffsetProperty); }
set { SetValue(ZoomLevelOffsetProperty, value); }
}
/// <summary>
/// Minimum zoom level supported by the MapTileLayer.
/// </summary>
public int MinZoomLevel
{
get { return (int)GetValue(MinZoomLevelProperty); }
set { SetValue(MinZoomLevelProperty, value); }
}
/// <summary>
/// Maximum zoom level supported by the MapTileLayer.
/// </summary>
public int MaxZoomLevel
{
get { return (int)GetValue(MaxZoomLevelProperty); }
set { SetValue(MaxZoomLevelProperty, value); }
}
/// <summary>
/// Maximum number of parallel downloads that may be performed by the MapTileLayer's ITileImageLoader.
/// </summary>
public int MaxParallelDownloads
{
get { return (int)GetValue(MaxParallelDownloadsProperty); }
set { SetValue(MaxParallelDownloadsProperty, value); }
}
/// <summary>
/// Minimum time interval between tile updates.
/// </summary>
public TimeSpan UpdateInterval
{
get { return (TimeSpan)GetValue(UpdateIntervalProperty); }
set { SetValue(UpdateIntervalProperty, value); }
}
/// <summary>
/// Controls if tiles are updated while the viewport is still changing.
/// </summary>
public bool UpdateWhileViewportChanging
{
get { return (bool)GetValue(UpdateWhileViewportChangingProperty); }
set { SetValue(UpdateWhileViewportChangingProperty, value); }
}
/// <summary>
/// Optional background brush. Sets MapBase.Background if not null and the MapTileLayer is the base map layer.
/// </summary>
public Brush MapBackground
{
get { return (Brush)GetValue(MapBackgroundProperty); }
set { SetValue(MapBackgroundProperty, value); }
}
/// <summary>
/// Optional foreground brush. Sets MapBase.Foreground if not null and the MapTileLayer is the base map layer.
/// </summary>
public Brush MapForeground
{
get { return (Brush)GetValue(MapForegroundProperty); }
set { SetValue(MapForegroundProperty, value); }
}
public MapBase ParentMap
{
get { return parentMap; }
set
{
if (parentMap != null)
{
parentMap.ViewportChanged -= OnViewportChanged;
}
parentMap = value;
if (parentMap != null)
{
parentMap.ViewportChanged += OnViewportChanged;
}
UpdateTileGrid();
}
}
protected override Size MeasureOverride(Size availableSize)
{
availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
foreach (UIElement element in Children)
{
element.Measure(availableSize);
}
return new Size();
}
protected override Size ArrangeOverride(Size finalSize)
{
if (TileGrid != null)
{
foreach (var tile in Tiles)
{
var tileSize = MapProjection.TileSize << (TileGrid.ZoomLevel - tile.ZoomLevel);
var x = tileSize * tile.X - MapProjection.TileSize * TileGrid.XMin;
var y = tileSize * tile.Y - MapProjection.TileSize * TileGrid.YMin;
tile.Image.Width = tileSize;
tile.Image.Height = tileSize;
tile.Image.Arrange(new Rect(x, y, tileSize, tileSize));
}
}
return finalSize;
}
protected virtual void UpdateTileGrid()
{
updateTimer.Stop();
if (parentMap != null && parentMap.MapProjection.IsWebMercator)
{
var tileGrid = GetTileGrid();
if (!tileGrid.Equals(TileGrid))
{
TileGrid = tileGrid;
SetRenderTransform();
UpdateTiles();
}
}
else
{
TileGrid = null;
UpdateTiles();
}
}
private void TileSourcePropertyChanged()
{
if (TileGrid != null)
{
Tiles.Clear();
UpdateTiles();
}
}
private void OnViewportChanged(object sender, ViewportChangedEventArgs e)
{
if (TileGrid == null || e.ProjectionChanged || Math.Abs(e.LongitudeOffset) > 180d)
{
UpdateTileGrid(); // update immediately when projection has changed or center has moved across 180° longitude
}
else
{
SetRenderTransform();
if (updateTimer.IsEnabled && !UpdateWhileViewportChanging)
{
updateTimer.Stop(); // restart
}
if (!updateTimer.IsEnabled)
{
updateTimer.Start();
}
}
}
private TileGrid GetTileGrid()
{
var tileZoomLevel = Math.Max(0, (int)Math.Round(parentMap.ZoomLevel + ZoomLevelOffset));
var tileScale = (double)(1 << tileZoomLevel);
var scale = tileScale / (Math.Pow(2d, parentMap.ZoomLevel) * MapProjection.TileSize);
var tileCenterX = tileScale * (0.5 + parentMap.Center.Longitude / 360d);
var tileCenterY = tileScale * (0.5 - WebMercatorProjection.LatitudeToY(parentMap.Center.Latitude) / 360d);
var viewCenterX = parentMap.RenderSize.Width / 2d;
var viewCenterY = parentMap.RenderSize.Height / 2d;
var transform = new MatrixTransform
{
Matrix = MatrixEx.TranslateScaleRotateTranslate(-viewCenterX, -viewCenterY, scale, scale, -parentMap.Heading, tileCenterX, tileCenterY)
};
var bounds = transform.TransformBounds(new Rect(0d, 0d, parentMap.RenderSize.Width, parentMap.RenderSize.Height));
return new TileGrid(tileZoomLevel,
(int)Math.Floor(bounds.X), (int)Math.Floor(bounds.Y),
(int)Math.Floor(bounds.X + bounds.Width), (int)Math.Floor(bounds.Y + bounds.Height));
}
private void SetRenderTransform()
{
var tileScale = (double)(1 << TileGrid.ZoomLevel);
var scale = Math.Pow(2d, parentMap.ZoomLevel) / tileScale;
var tileCenterX = tileScale * (0.5 + parentMap.Center.Longitude / 360d);
var tileCenterY = tileScale * (0.5 - WebMercatorProjection.LatitudeToY(parentMap.Center.Latitude) / 360d);
var tileOriginX = MapProjection.TileSize * (tileCenterX - TileGrid.XMin);
var tileOriginY = MapProjection.TileSize * (tileCenterY - TileGrid.YMin);
var viewCenterX = parentMap.RenderSize.Width / 2d;
var viewCenterY = parentMap.RenderSize.Height / 2d;
((MatrixTransform)RenderTransform).Matrix = MatrixEx.TranslateScaleRotateTranslate(
-tileOriginX, -tileOriginY, scale, scale, parentMap.Heading, viewCenterX, viewCenterY);
}
private void UpdateTiles()
{
var newTiles = new List<Tile>();
if (parentMap != null && TileGrid != null && TileSource != null)
{
var maxZoomLevel = Math.Min(TileGrid.ZoomLevel, MaxZoomLevel);
var minZoomLevel = parentMap.MapLayer == this ? MinZoomLevel : maxZoomLevel; // load background tiles only if this is the base layer
for (var z = minZoomLevel; z <= maxZoomLevel; z++)
{
var tileSize = 1 << (TileGrid.ZoomLevel - z);
var x1 = (int)Math.Floor((double)TileGrid.XMin / tileSize); // may be negative
var x2 = TileGrid.XMax / tileSize;
var y1 = Math.Max(TileGrid.YMin / tileSize, 0);
var y2 = Math.Min(TileGrid.YMax / tileSize, (1 << z) - 1);
for (var y = y1; y <= y2; y++)
{
for (var x = x1; x <= x2; x++)
{
var tile = Tiles.FirstOrDefault(t => t.ZoomLevel == z && t.X == x && t.Y == y);
if (tile == null)
{
tile = new Tile(z, x, y);
var equivalentTile = Tiles.FirstOrDefault(
t => t.ZoomLevel == z && t.XIndex == tile.XIndex && t.Y == y && t.Image.Source != null);
if (equivalentTile != null)
{
tile.SetImage(equivalentTile.Image.Source, false); // do not animate to avoid flicker when crossing 180° longitude
}
}
newTiles.Add(tile);
}
}
}
}
Tiles = newTiles;
Children.Clear();
foreach (var tile in Tiles)
{
Children.Add(tile.Image);
}
TileImageLoader.LoadTiles(this);
}
}
}

View file

@ -0,0 +1,74 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 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
{
/// <summary>
/// Transforms map coordinates according to the Orthographic Projection.
/// </summary>
public class OrthographicProjection : AzimuthalProjection
{
public OrthographicProjection()
: this("AUTO2:42003")
{
}
public OrthographicProjection(string crsId)
{
CrsId = crsId;
}
public override Point LocationToPoint(Location location)
{
if (location.Equals(projectionCenter))
{
return new Point();
}
var lat0 = projectionCenter.Latitude * Math.PI / 180d;
var lat = location.Latitude * Math.PI / 180d;
var dLon = (location.Longitude - projectionCenter.Longitude) * Math.PI / 180d;
return new Point(
Wgs84EquatorialRadius * Math.Cos(lat) * Math.Sin(dLon),
Wgs84EquatorialRadius * (Math.Cos(lat0) * Math.Sin(lat) - Math.Sin(lat0) * Math.Cos(lat) * Math.Cos(dLon)));
}
public override Location PointToLocation(Point point)
{
if (point.X == 0d && point.Y == 0d)
{
return projectionCenter;
}
var x = point.X / Wgs84EquatorialRadius;
var y = point.Y / Wgs84EquatorialRadius;
var r2 = x * x + y * y;
if (r2 > 1d)
{
return new Location(double.NaN, double.NaN);
}
var r = Math.Sqrt(r2);
var sinC = r;
var cosC = Math.Sqrt(1 - r2);
var lat0 = projectionCenter.Latitude * Math.PI / 180d;
var cosLat0 = Math.Cos(lat0);
var sinLat0 = Math.Sin(lat0);
return new Location(
180d / Math.PI * Math.Asin(cosC * sinLat0 + y * sinC * cosLat0 / r),
180d / Math.PI * Math.Atan2(x * sinC, r * cosC * cosLat0 - y * sinC * sinLat0) + projectionCenter.Longitude);
}
}
}

View file

@ -0,0 +1,59 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 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
{
/// <summary>
/// Transforms map coordinates according to the Gnomonic Projection.
/// </summary>
public class StereographicProjection : AzimuthalProjection
{
public StereographicProjection()
: this("AUTO2:97002") // GeoServer non-standard CRS ID
{
}
public StereographicProjection(string crsId)
{
CrsId = crsId;
}
public override Point LocationToPoint(Location location)
{
if (location.Equals(projectionCenter))
{
return new Point();
}
double azimuth, distance;
GetAzimuthDistance(projectionCenter, location, out azimuth, out distance);
var mapDistance = 2d * Wgs84EquatorialRadius * Math.Tan(distance / 2d);
return new Point(mapDistance * Math.Sin(azimuth), mapDistance * Math.Cos(azimuth));
}
public override Location PointToLocation(Point point)
{
if (point.X == 0d && point.Y == 0d)
{
return projectionCenter;
}
var azimuth = Math.Atan2(point.X, point.Y);
var mapDistance = Math.Sqrt(point.X * point.X + point.Y * point.Y);
var distance = 2d * Math.Atan(mapDistance / (2d * Wgs84EquatorialRadius));
return GetLocation(projectionCenter, azimuth, distance);
}
}
}

41
MapControl/Shared/Tile.cs Normal file
View file

@ -0,0 +1,41 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
#if WINDOWS_UWP
using Windows.UI.Xaml.Controls;
#else
using System.Windows.Controls;
#endif
namespace MapControl
{
public partial class Tile
{
public static TimeSpan FadeDuration { get; set; } = TimeSpan.FromSeconds(0.1);
public readonly int ZoomLevel;
public readonly int X;
public readonly int Y;
public readonly Image Image = new Image { Opacity = 0d };
public Tile(int zoomLevel, int x, int y)
{
ZoomLevel = zoomLevel;
X = x;
Y = y;
}
public bool Pending { get; set; } = true;
public int XIndex
{
get
{
var numTiles = 1 << ZoomLevel;
return ((X % numTiles) + numTiles) % numTiles;
}
}
}
}

View file

@ -0,0 +1,46 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
namespace MapControl
{
public class TileGrid : IEquatable<TileGrid>
{
public readonly int ZoomLevel;
public readonly int XMin;
public readonly int YMin;
public readonly int XMax;
public readonly int YMax;
public TileGrid(int zoomLevel, int xMin, int yMin, int xMax, int yMax)
{
ZoomLevel = zoomLevel;
XMin = xMin;
YMin = yMin;
XMax = xMax;
YMax = yMax;
}
public bool Equals(TileGrid tileGrid)
{
return tileGrid != null
&& tileGrid.ZoomLevel == ZoomLevel
&& tileGrid.XMin == XMin
&& tileGrid.YMin == YMin
&& tileGrid.XMax == XMax
&& tileGrid.YMax == YMax;
}
public override bool Equals(object obj)
{
return Equals(obj as TileGrid);
}
public override int GetHashCode()
{
return ZoomLevel ^ XMin ^ YMin ^ XMax ^ YMax;
}
}
}

View file

@ -0,0 +1,172 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
#if WINDOWS_UWP
using Windows.Web.Http;
#else
using System.Net.Http;
#endif
namespace MapControl
{
/// <summary>
/// Loads and optionally caches map tile images for a MapTileLayer.
/// </summary>
public partial class TileImageLoader : ITileImageLoader
{
/// <summary>
/// The HttpClient instance used when image data is downloaded from a web resource.
/// </summary>
public static HttpClient HttpClient { get; set; } = new HttpClient();
/// <summary>
/// Default expiration time for cached tile images. Used when no expiration time
/// was transmitted on download. The default value is one day.
/// </summary>
public static TimeSpan DefaultCacheExpiration { get; set; } = TimeSpan.FromDays(1);
/// <summary>
/// Minimum expiration time for cached tile images. The default value is one hour.
/// </summary>
public static TimeSpan MinimumCacheExpiration { get; set; } = TimeSpan.FromHours(1);
/// <summary>
/// Maximum expiration time for cached tile images. The default value is one week.
/// </summary>
public static TimeSpan MaximumCacheExpiration { get; set; } = TimeSpan.FromDays(7);
/// <summary>
/// Format string for creating cache keys from the SourceName property of a TileSource,
/// the ZoomLevel, XIndex, and Y properties of a Tile, and the image file extension.
/// The default value is "{0};{1};{2};{3}{4}".
/// </summary>
public static string CacheKeyFormat { get; set; } = "{0};{1};{2};{3}{4}";
private const string bingMapsTileInfo = "X-VE-Tile-Info";
private const string bingMapsNoTile = "no-tile";
private readonly ConcurrentStack<Tile> pendingTiles = new ConcurrentStack<Tile>();
private int taskCount;
public void LoadTiles(MapTileLayer tileLayer)
{
pendingTiles.Clear();
var tileSource = tileLayer.TileSource;
var sourceName = tileLayer.SourceName;
var tiles = tileLayer.Tiles.Where(t => t.Pending);
if (tileSource != null && tiles.Any())
{
if (Cache == null || string.IsNullOrEmpty(sourceName) ||
tileSource.UriFormat == null || !tileSource.UriFormat.StartsWith("http"))
{
// no caching, load tile images in UI thread
foreach (var tile in tiles)
{
LoadTileImage(tileSource, tile);
}
}
else
{
pendingTiles.PushRange(tiles.Reverse().ToArray());
while (taskCount < Math.Min(pendingTiles.Count, tileLayer.MaxParallelDownloads))
{
Interlocked.Increment(ref taskCount);
var task = Task.Run(async () => // do not await
{
await LoadPendingTilesAsync(tileSource, sourceName); // run multiple times in parallel
Interlocked.Decrement(ref taskCount);
});
}
}
}
}
private void LoadTileImage(TileSource tileSource, Tile tile)
{
tile.Pending = false;
try
{
var imageSource = tileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel);
if (imageSource != null)
{
tile.SetImage(imageSource);
}
}
catch (Exception ex)
{
Debug.WriteLine("TileImageLoader: {0}/{1}/{2}: {3}", tile.ZoomLevel, tile.XIndex, tile.Y, ex.Message);
}
}
private async Task LoadPendingTilesAsync(TileSource tileSource, string sourceName)
{
Tile tile;
while (pendingTiles.TryPop(out tile))
{
tile.Pending = false;
try
{
var uri = tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel);
if (uri != null)
{
var extension = Path.GetExtension(uri.LocalPath);
if (string.IsNullOrEmpty(extension) || extension == ".jpeg")
{
extension = ".jpg";
}
var cacheKey = string.Format(CacheKeyFormat, sourceName, tile.ZoomLevel, tile.XIndex, tile.Y, extension);
await LoadTileImageAsync(tile, uri, cacheKey);
}
}
catch (Exception ex)
{
Debug.WriteLine("TileImageLoader: {0}/{1}/{2}: {3}", tile.ZoomLevel, tile.XIndex, tile.Y, ex.Message);
}
}
}
private static DateTime GetExpiration(HttpResponseMessage response)
{
var expiration = DefaultCacheExpiration;
var headers = response.Headers;
if (headers.CacheControl != null && headers.CacheControl.MaxAge.HasValue)
{
expiration = headers.CacheControl.MaxAge.Value;
if (expiration < MinimumCacheExpiration)
{
expiration = MinimumCacheExpiration;
}
else if (expiration > MaximumCacheExpiration)
{
expiration = MaximumCacheExpiration;
}
}
return DateTime.UtcNow.Add(expiration);
}
}
}

View file

@ -0,0 +1,216 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Globalization;
#if WINDOWS_UWP
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging;
#else
using System.Windows.Media;
using System.Windows.Media.Imaging;
#endif
namespace MapControl
{
/// <summary>
/// Provides the download Uri or ImageSource of map tiles.
/// </summary>
public partial class TileSource
{
private Func<int, int, int, Uri> getUri;
private string uriFormat = string.Empty;
public TileSource()
{
}
protected TileSource(string uriFormat)
{
this.uriFormat = uriFormat;
}
/// <summary>
/// Gets or sets the format string to produce tile Uris.
/// </summary>
public string UriFormat
{
get { return uriFormat; }
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("The value of the UriFormat property must not be null or empty.");
}
uriFormat = value;
if (uriFormat.Contains("{x}") && uriFormat.Contains("{y}") && uriFormat.Contains("{z}"))
{
if (uriFormat.Contains("{c}"))
{
getUri = GetOpenStreetMapUri;
}
else if (uriFormat.Contains("{i}"))
{
getUri = GetGoogleMapsUri;
}
else if (uriFormat.Contains("{n}"))
{
getUri = GetMapQuestUri;
}
else
{
getUri = GetBasicUri;
}
}
else if (uriFormat.Contains("{x}") && uriFormat.Contains("{v}") && uriFormat.Contains("{z}"))
{
getUri = GetTmsUri;
}
else if (uriFormat.Contains("{q}")) // {i} is optional
{
getUri = GetQuadKeyUri;
}
else if (uriFormat.Contains("{W}") && uriFormat.Contains("{S}") && uriFormat.Contains("{E}") && uriFormat.Contains("{N}"))
{
getUri = GetBoundingBoxUri;
}
else if (uriFormat.Contains("{w}") && uriFormat.Contains("{s}") && uriFormat.Contains("{e}") && uriFormat.Contains("{n}"))
{
getUri = GetLatLonBoundingBoxUri;
}
}
}
/// <summary>
/// Gets the map tile Uri.
/// </summary>
public virtual Uri GetUri(int x, int y, int zoomLevel)
{
return getUri?.Invoke(x, y, zoomLevel);
}
/// <summary>
/// Gets the map tile ImageSource without caching in TileImageLoader.Cache.
/// By overriding LoadImage an application can provide arbitrary tile images.
/// </summary>
public virtual ImageSource LoadImage(int x, int y, int zoomLevel)
{
var uri = GetUri(x, y, zoomLevel);
return uri != null ? new BitmapImage(uri) : null;
}
private Uri GetBasicUri(int x, int y, int zoomLevel)
{
return new Uri(uriFormat
.Replace("{x}", x.ToString())
.Replace("{y}", y.ToString())
.Replace("{z}", zoomLevel.ToString()),
UriKind.RelativeOrAbsolute);
}
private Uri GetOpenStreetMapUri(int x, int y, int zoomLevel)
{
var hostIndex = (x + y) % 3;
return new Uri(uriFormat
.Replace("{c}", "abc".Substring(hostIndex, 1))
.Replace("{x}", x.ToString())
.Replace("{y}", y.ToString())
.Replace("{z}", zoomLevel.ToString()),
UriKind.RelativeOrAbsolute);
}
private Uri GetGoogleMapsUri(int x, int y, int zoomLevel)
{
var hostIndex = (x + y) % 4;
return new Uri(uriFormat
.Replace("{i}", hostIndex.ToString())
.Replace("{x}", x.ToString())
.Replace("{y}", y.ToString())
.Replace("{z}", zoomLevel.ToString()),
UriKind.RelativeOrAbsolute);
}
private Uri GetMapQuestUri(int x, int y, int zoomLevel)
{
var hostIndex = (x + y) % 4 + 1;
return new Uri(uriFormat
.Replace("{n}", hostIndex.ToString())
.Replace("{x}", x.ToString())
.Replace("{y}", y.ToString())
.Replace("{z}", zoomLevel.ToString()),
UriKind.RelativeOrAbsolute);
}
private Uri GetTmsUri(int x, int y, int zoomLevel)
{
y = (1 << zoomLevel) - 1 - y;
return new Uri(uriFormat
.Replace("{x}", x.ToString())
.Replace("{v}", y.ToString())
.Replace("{z}", zoomLevel.ToString()),
UriKind.RelativeOrAbsolute);
}
private Uri GetQuadKeyUri(int x, int y, int zoomLevel)
{
if (zoomLevel < 1)
{
return null;
}
var quadkey = new char[zoomLevel];
for (var z = zoomLevel - 1; z >= 0; z--, x /= 2, y /= 2)
{
quadkey[z] = (char)('0' + 2 * (y % 2) + (x % 2));
}
return new Uri(uriFormat
.Replace("{i}", new string(quadkey, zoomLevel - 1, 1))
.Replace("{q}", new string(quadkey)),
UriKind.RelativeOrAbsolute);
}
private Uri GetBoundingBoxUri(int x, int y, int zoomLevel)
{
var tileSize = 360d / (1 << zoomLevel); // tile width in degrees
var west = MapProjection.MetersPerDegree * (x * tileSize - 180d);
var east = MapProjection.MetersPerDegree * ((x + 1) * tileSize - 180d);
var south = MapProjection.MetersPerDegree * (180d - (y + 1) * tileSize);
var north = MapProjection.MetersPerDegree * (180d - y * tileSize);
return new Uri(uriFormat
.Replace("{W}", west.ToString(CultureInfo.InvariantCulture))
.Replace("{S}", south.ToString(CultureInfo.InvariantCulture))
.Replace("{E}", east.ToString(CultureInfo.InvariantCulture))
.Replace("{N}", north.ToString(CultureInfo.InvariantCulture))
.Replace("{X}", MapProjection.TileSize.ToString())
.Replace("{Y}", MapProjection.TileSize.ToString()));
}
private Uri GetLatLonBoundingBoxUri(int x, int y, int zoomLevel)
{
var tileSize = 360d / (1 << zoomLevel); // tile width in degrees
var west = x * tileSize - 180d;
var east = (x + 1) * tileSize - 180d;
var south = WebMercatorProjection.YToLatitude(180d - (y + 1) * tileSize);
var north = WebMercatorProjection.YToLatitude(180d - y * tileSize);
return new Uri(uriFormat
.Replace("{w}", west.ToString(CultureInfo.InvariantCulture))
.Replace("{s}", south.ToString(CultureInfo.InvariantCulture))
.Replace("{e}", east.ToString(CultureInfo.InvariantCulture))
.Replace("{n}", north.ToString(CultureInfo.InvariantCulture))
.Replace("{X}", MapProjection.TileSize.ToString())
.Replace("{Y}", MapProjection.TileSize.ToString()));
}
}
}

View file

@ -0,0 +1,29 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
namespace MapControl
{
public class ViewportChangedEventArgs : EventArgs
{
public ViewportChangedEventArgs(bool projectionChanged = false, double longitudeOffset = 0d)
{
ProjectionChanged = projectionChanged;
LongitudeOffset = longitudeOffset;
}
/// <summary>
/// Indicates if the map projection has changed, i.e. if a MapTileLayer or MapImageLayer should be
/// immediately updated, or MapPath Data in cartesian map coordinates should be recalculated.
/// </summary>
public bool ProjectionChanged { get; }
/// <summary>
/// Offset of the map center longitude value from the previous viewport.
/// Used to detect if the map center has moved across 180° longitude.
/// </summary>
public double LongitudeOffset { get; }
}
}

View file

@ -0,0 +1,85 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 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
{
/// <summary>
/// Transforms map coordinates according to the Web Mercator Projection.
/// Longitude values are transformed linearly to X values in meters, by multiplying with MetersPerDegree.
/// Latitude values in the interval [-MaxLatitude .. MaxLatitude] are transformed to Y values in meters
/// in the interval [-R*pi .. R*pi], R=Wgs84EquatorialRadius.
/// </summary>
public class WebMercatorProjection : MapProjection
{
public WebMercatorProjection()
: this("EPSG:3857")
{
}
public WebMercatorProjection(string crsId)
{
CrsId = crsId;
IsWebMercator = true;
LongitudeScale = MetersPerDegree;
MaxLatitude = YToLatitude(180d);
}
public override double GetViewportScale(double zoomLevel)
{
return DegreesToViewportScale(zoomLevel) / MetersPerDegree;
}
public override Point GetMapScale(Location location)
{
var scale = ViewportScale / Math.Cos(location.Latitude * Math.PI / 180d);
return new Point(scale, scale);
}
public override Point LocationToPoint(Location location)
{
return new Point(
MetersPerDegree * location.Longitude,
MetersPerDegree * LatitudeToY(location.Latitude));
}
public override Location PointToLocation(Point point)
{
return new Location(
YToLatitude(point.Y / MetersPerDegree),
point.X / MetersPerDegree);
}
public override Location TranslateLocation(Location location, Point translation)
{
var scaleX = MetersPerDegree * ViewportScale;
var scaleY = scaleX / Math.Cos(location.Latitude * Math.PI / 180d);
return new Location(
location.Latitude - translation.Y / scaleY,
location.Longitude + translation.X / scaleX);
}
public static double LatitudeToY(double latitude)
{
var lat = latitude * Math.PI / 180d;
return latitude <= -90d ? double.NegativeInfinity
: latitude >= 90d ? double.PositiveInfinity
: Math.Log(Math.Tan((latitude + 90d) * Math.PI / 360d)) / Math.PI * 180d;
}
public static double YToLatitude(double y)
{
return Math.Atan(Math.Exp(y * Math.PI / 180d)) / Math.PI * 360d - 90d;
}
}
}

View file

@ -0,0 +1,162 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2017 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
#if WINDOWS_UWP
using Windows.Data.Xml.Dom;
using Windows.UI.Xaml;
#else
using System.Windows;
using System.Xml;
#endif
namespace MapControl
{
public partial class WmsImageLayer : MapImageLayer
{
public static readonly DependencyProperty ServerUriProperty = DependencyProperty.Register(
nameof(ServerUri), typeof(Uri), typeof(WmsImageLayer),
new PropertyMetadata(null, (o, e) => ((WmsImageLayer)o).UpdateImage()));
public static readonly DependencyProperty VersionProperty = DependencyProperty.Register(
nameof(Version), typeof(string), typeof(WmsImageLayer),
new PropertyMetadata("1.3.0", (o, e) => ((WmsImageLayer)o).UpdateImage()));
public static readonly DependencyProperty LayersProperty = DependencyProperty.Register(
nameof(Layers), typeof(string), typeof(WmsImageLayer),
new PropertyMetadata(string.Empty, (o, e) => ((WmsImageLayer)o).UpdateImage()));
public static readonly DependencyProperty StylesProperty = DependencyProperty.Register(
nameof(Styles), typeof(string), typeof(WmsImageLayer),
new PropertyMetadata(string.Empty, (o, e) => ((WmsImageLayer)o).UpdateImage()));
public static readonly DependencyProperty FormatProperty = DependencyProperty.Register(
nameof(Format), typeof(string), typeof(WmsImageLayer),
new PropertyMetadata("image/png", (o, e) => ((WmsImageLayer)o).UpdateImage()));
public static readonly DependencyProperty TransparentProperty = DependencyProperty.Register(
nameof(Transparent), typeof(bool), typeof(WmsImageLayer),
new PropertyMetadata(false, (o, e) => ((WmsImageLayer)o).UpdateImage()));
private string layers = string.Empty;
public Uri ServerUri
{
get { return (Uri)GetValue(ServerUriProperty); }
set { SetValue(ServerUriProperty, value); }
}
public string Version
{
get { return (string)GetValue(VersionProperty); }
set { SetValue(VersionProperty, value); }
}
public string Layers
{
get { return (string)GetValue(LayersProperty); }
set { SetValue(LayersProperty, value); }
}
public string Styles
{
get { return (string)GetValue(StylesProperty); }
set { SetValue(StylesProperty, value); }
}
public string Format
{
get { return (string)GetValue(FormatProperty); }
set { SetValue(FormatProperty, value); }
}
public bool Transparent
{
get { return (bool)GetValue(TransparentProperty); }
set { SetValue(TransparentProperty, value); }
}
protected override bool UpdateImage(BoundingBox boundingBox)
{
if (ServerUri == null)
{
return false;
}
var projectionParameters = ParentMap.MapProjection.WmsQueryParameters(boundingBox, Version);
if (string.IsNullOrEmpty(projectionParameters))
{
return false;
}
UpdateImage(GetRequestUri("GetMap"
+ "&LAYERS=" + Layers + "&STYLES=" + Styles + "&FORMAT=" + Format
+ "&TRANSPARENT=" + (Transparent ? "TRUE" : "FALSE") + "&" + projectionParameters));
return true;
}
public async Task<IList<string>> GetLayerNamesAsync()
{
if (ServerUri == null)
{
return null;
}
var layerNames = new List<string>();
try
{
var document = await XmlDocument.LoadFromUriAsync(GetRequestUri("GetCapabilities"));
var capability = ChildElements(document.DocumentElement, "Capability").FirstOrDefault();
if (capability != null)
{
var rootLayer = ChildElements(capability, "Layer").FirstOrDefault();
if (rootLayer != null)
{
foreach (var layer in ChildElements(rootLayer, "Layer"))
{
var name = ChildElements(layer, "Name").FirstOrDefault();
if (name != null)
{
layerNames.Add(name.InnerText);
}
}
}
}
}
catch (Exception ex)
{
Debug.WriteLine("WmsImageLayer: {0}: {1}", ServerUri, ex.Message);
}
return layerNames;
}
private Uri GetRequestUri(string query)
{
var uri = ServerUri.ToString();
if (!uri.EndsWith("?") && !uri.EndsWith("&"))
{
uri += "?";
}
uri += "SERVICE=WMS&VERSION=" + Version + "&REQUEST=" + query;
return new Uri(uri.Replace(" ", "%20"));
}
private static IEnumerable<XmlElement> ChildElements(XmlElement element, string name)
{
return element.ChildNodes.OfType<XmlElement>().Where(e => (string)e.LocalName == name);
}
}
}