mirror of
https://github.com/ClemensFischer/XAML-Map-Control.git
synced 2026-04-05 06:26:41 +00:00
Version 4: Upgrade to VS 2017
This commit is contained in:
parent
2aafe32e00
commit
ec47f225b3
142 changed files with 1828 additions and 18384 deletions
58
MapControl/Shared/AzimuthalEquidistantProjection.cs
Normal file
58
MapControl/Shared/AzimuthalEquidistantProjection.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
137
MapControl/Shared/AzimuthalProjection.cs
Normal file
137
MapControl/Shared/AzimuthalProjection.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
138
MapControl/Shared/BingMapsTileLayer.cs
Normal file
138
MapControl/Shared/BingMapsTileLayer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
MapControl/Shared/BingMapsTileSource.cs
Normal file
39
MapControl/Shared/BingMapsTileSource.cs
Normal 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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
92
MapControl/Shared/BoundingBox.cs
Normal file
92
MapControl/Shared/BoundingBox.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
40
MapControl/Shared/CenteredBoundingBox.cs
Normal file
40
MapControl/Shared/CenteredBoundingBox.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
MapControl/Shared/EquirectangularProjection.cs
Normal file
54
MapControl/Shared/EquirectangularProjection.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
MapControl/Shared/GnomonicProjection.cs
Normal file
61
MapControl/Shared/GnomonicProjection.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
99
MapControl/Shared/HyperlinkText.cs
Normal file
99
MapControl/Shared/HyperlinkText.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
MapControl/Shared/Location.cs
Normal file
109
MapControl/Shared/Location.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
MapControl/Shared/LocationCollection.cs
Normal file
38
MapControl/Shared/LocationCollection.cs
Normal 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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
763
MapControl/Shared/MapBase.cs
Normal file
763
MapControl/Shared/MapBase.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
83
MapControl/Shared/MapGraticule.cs
Normal file
83
MapControl/Shared/MapGraticule.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
340
MapControl/Shared/MapImageLayer.cs
Normal file
340
MapControl/Shared/MapImageLayer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
110
MapControl/Shared/MapOverlay.cs
Normal file
110
MapControl/Shared/MapOverlay.cs
Normal 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); }
|
||||
}
|
||||
}
|
||||
}
|
||||
394
MapControl/Shared/MapPanel.cs
Normal file
394
MapControl/Shared/MapPanel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
112
MapControl/Shared/MapPath.cs
Normal file
112
MapControl/Shared/MapPath.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
MapControl/Shared/MapPolyline.cs
Normal file
71
MapControl/Shared/MapPolyline.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
186
MapControl/Shared/MapProjection.cs
Normal file
186
MapControl/Shared/MapProjection.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
406
MapControl/Shared/MapTileLayer.cs
Normal file
406
MapControl/Shared/MapTileLayer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
74
MapControl/Shared/OrthographicProjection.cs
Normal file
74
MapControl/Shared/OrthographicProjection.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
MapControl/Shared/StereographicProjection.cs
Normal file
59
MapControl/Shared/StereographicProjection.cs
Normal 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
41
MapControl/Shared/Tile.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
MapControl/Shared/TileGrid.cs
Normal file
46
MapControl/Shared/TileGrid.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
172
MapControl/Shared/TileImageLoader.cs
Normal file
172
MapControl/Shared/TileImageLoader.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
216
MapControl/Shared/TileSource.cs
Normal file
216
MapControl/Shared/TileSource.cs
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
29
MapControl/Shared/ViewportChangedEventArgs.cs
Normal file
29
MapControl/Shared/ViewportChangedEventArgs.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
85
MapControl/Shared/WebMercatorProjection.cs
Normal file
85
MapControl/Shared/WebMercatorProjection.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
162
MapControl/Shared/WmsImageLayer.cs
Normal file
162
MapControl/Shared/WmsImageLayer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue