From 156ebfe1775755c65403372a16dcc0dfe38b69af Mon Sep 17 00:00:00 2001 From: ClemensF Date: Fri, 27 Oct 2017 17:15:18 +0200 Subject: [PATCH] Added WorldMercatorProjection, fixed bounding box problems when changing projection. --- MapControl/Shared/MapImageLayer.cs | 226 +++++++++--------- MapControl/Shared/MapProjection.cs | 6 +- MapControl/Shared/TileImageLoader.cs | 12 +- MapControl/Shared/TileSource.cs | 15 +- MapControl/Shared/WebMercatorProjection.cs | 6 +- MapControl/Shared/WmsImageLayer.cs | 51 ++-- MapControl/Shared/WorldMercatorProjection.cs | 116 +++++++++ MapControl/UWP/ImageLoader.UWP.cs | 110 +++++++++ MapControl/UWP/MapControl.UWP.csproj | 6 +- MapControl/UWP/MapImageLayer.UWP.cs | 52 ---- MapControl/UWP/TileImageLoader.UWP.cs | 48 ++-- MapControl/UWP/TileSource.UWP.cs | 8 +- MapControl/WPF/ImageLoader.WPF.cs | 110 +++++++++ MapControl/WPF/MapControl.WPF.csproj | 6 +- MapControl/WPF/MapGraticule.WPF.cs | 96 ++++---- MapControl/WPF/MapImageLayer.WPF.cs | 52 ---- MapControl/WPF/TileImageLoader.WPF.cs | 51 +--- MapControl/WPF/TileSource.WPF.cs | 76 ------ SampleApps/UniversalApp/MainPage.xaml | 2 + SampleApps/WpfApplication/MainWindow.xaml | 2 + .../WpfApplication/WpfApplication.csproj | 4 - 21 files changed, 586 insertions(+), 469 deletions(-) create mode 100644 MapControl/Shared/WorldMercatorProjection.cs create mode 100644 MapControl/UWP/ImageLoader.UWP.cs delete mode 100644 MapControl/UWP/MapImageLayer.UWP.cs create mode 100644 MapControl/WPF/ImageLoader.WPF.cs delete mode 100644 MapControl/WPF/MapImageLayer.WPF.cs delete mode 100644 MapControl/WPF/TileSource.WPF.cs diff --git a/MapControl/Shared/MapImageLayer.cs b/MapControl/Shared/MapImageLayer.cs index 95e3f9dd..9316ccb0 100644 --- a/MapControl/Shared/MapImageLayer.cs +++ b/MapControl/Shared/MapImageLayer.cs @@ -4,19 +4,18 @@ using System; using System.Diagnostics; +using System.Threading.Tasks; #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 @@ -26,7 +25,7 @@ namespace MapControl /// 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. /// - public abstract partial class MapImageLayer : MapPanel, IMapLayer + public abstract class MapImageLayer : MapPanel, IMapLayer { public static readonly DependencyProperty MinLatitudeProperty = DependencyProperty.Register( nameof(MinLatitude), typeof(double), typeof(MapImageLayer), new PropertyMetadata(double.NaN)); @@ -64,7 +63,6 @@ namespace MapControl private readonly DispatcherTimer updateTimer; private BoundingBox boundingBox; - private int topImageIndex; private bool updateInProgress; public MapImageLayer() @@ -73,7 +71,7 @@ namespace MapControl Children.Add(new Image { Opacity = 0d, Stretch = Stretch.Fill }); updateTimer = new DispatcherTimer { Interval = UpdateInterval }; - updateTimer.Tick += (s, e) => UpdateImage(); + updateTimer.Tick += async (s, e) => await UpdateImage(); } /// @@ -180,34 +178,26 @@ namespace MapControl set { SetValue(MapBackgroundProperty, value); } } + /// + /// Returns an ImageSource for the specified bounding box. + /// + protected abstract Task GetImage(BoundingBox boundingBox); + protected override void OnViewportChanged(ViewportChangedEventArgs e) { - base.OnViewportChanged(e); - if (e.ProjectionChanged) { - UpdateImage((BitmapSource)null); - UpdateImage(); + ClearImages(); + + base.OnViewportChanged(e); + + var task = UpdateImage(); } else { - if (Math.Abs(e.LongitudeOffset) > 180d && boundingBox != null && boundingBox.HasValidBounds) - { - var offset = 360d * Math.Sign(e.LongitudeOffset); + AdjustBoundingBox(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)); - } - } - } + base.OnViewportChanged(e); if (updateTimer.IsEnabled && !UpdateWhileViewportChanging) { @@ -221,7 +211,7 @@ namespace MapControl } } - protected virtual void UpdateImage() + protected virtual async Task UpdateImage() { updateTimer.Stop(); @@ -233,104 +223,124 @@ namespace MapControl { 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; - } - } - ImageSource imageSource = null; - try + if (UpdateBoundingBox()) { - imageSource = GetImage(boundingBox); - } - catch (Exception ex) - { - Debug.WriteLine("MapImageLayer: " + ex.Message); + try + { + imageSource = await GetImage(boundingBox); + } + catch (Exception ex) + { + Debug.WriteLine("MapImageLayer: " + ex.Message); + } } - UpdateImage(imageSource); + SwapImages(imageSource); + + updateInProgress = false; } } - /// - /// Returns an ImageSource for the specified bounding box. - /// - protected abstract ImageSource GetImage(BoundingBox boundingBox); - - private void SetTopImage(ImageSource imageSource) + private bool UpdateBoundingBox() { - topImageIndex = (topImageIndex + 1) % 2; - var topImage = (Image)Children[topImageIndex]; + 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) + { + return false; + } + + 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; + } + + return true; + } + + private void AdjustBoundingBox(double longitudeOffset) + { + if (Math.Abs(longitudeOffset) > 180d && boundingBox != null && boundingBox.HasValidBounds) + { + var offset = 360d * Math.Sign(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)); + } + } + } + } + + private void ClearImages() + { + foreach (UIElement element in Children) + { + element.ClearValue(BoundingBoxProperty); + element.ClearValue(Image.SourceProperty); + } + } + + private void SwapImages(ImageSource imageSource) + { + var topImage = (Image)Children[0]; + var bottomImage = (Image)Children[1]; + + Children.RemoveAt(0); + Children.Insert(1, topImage); topImage.Source = imageSource; 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 { - topImage.BeginAnimation(OpacityProperty, new DoubleAnimation - { - To = 1d, - Duration = Tile.FadeDuration - }); + To = 1d, + Duration = Tile.FadeDuration + }); - bottomImage.BeginAnimation(OpacityProperty, new DoubleAnimation - { - To = 0d, - BeginTime = Tile.FadeDuration, - Duration = TimeSpan.Zero - }); - } - else + bottomImage.BeginAnimation(OpacityProperty, new DoubleAnimation { - topImage.Opacity = 0d; - bottomImage.Opacity = 0d; - bottomImage.Source = null; - } - - updateInProgress = false; + To = 0d, + BeginTime = Tile.FadeDuration, + Duration = TimeSpan.Zero + }); } } } diff --git a/MapControl/Shared/MapProjection.cs b/MapControl/Shared/MapProjection.cs index 76e53ced..6a322972 100644 --- a/MapControl/Shared/MapProjection.cs +++ b/MapControl/Shared/MapProjection.cs @@ -19,10 +19,14 @@ namespace MapControl /// public abstract class MapProjection { - public const int TileSize = 256; public const double Wgs84EquatorialRadius = 6378137d; + public const double Wgs84Flattening = 1d / 298.257223563; + public static readonly double Wgs84Eccentricity = Math.Sqrt((2d - Wgs84Flattening) * Wgs84Flattening); + public const double MetersPerDegree = Wgs84EquatorialRadius * Math.PI / 180d; + public const int TileSize = 256; + /// /// Gets the scaling factor from cartesian map coordinates in degrees to viewport coordinates for the specified zoom level. /// diff --git a/MapControl/Shared/TileImageLoader.cs b/MapControl/Shared/TileImageLoader.cs index cc39ed56..d889a586 100644 --- a/MapControl/Shared/TileImageLoader.cs +++ b/MapControl/Shared/TileImageLoader.cs @@ -9,11 +9,6 @@ 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 { @@ -139,14 +134,13 @@ namespace MapControl } } - private static DateTime GetExpiration(HttpResponseMessage response) + private static DateTime GetExpiration(TimeSpan? maxAge) { var expiration = DefaultCacheExpiration; - var headers = response.Headers; - if (headers.CacheControl != null && headers.CacheControl.MaxAge.HasValue) + if (maxAge.HasValue) { - expiration = headers.CacheControl.MaxAge.Value; + expiration = maxAge.Value; if (expiration < MinimumCacheExpiration) { diff --git a/MapControl/Shared/TileSource.cs b/MapControl/Shared/TileSource.cs index a388fccb..3522f2cf 100644 --- a/MapControl/Shared/TileSource.cs +++ b/MapControl/Shared/TileSource.cs @@ -8,10 +8,8 @@ using System.Globalization; using System.Threading.Tasks; #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 @@ -117,18 +115,7 @@ namespace MapControl { try { - if (!uri.IsAbsoluteUri || uri.Scheme == "file") - { - imageSource = await LoadLocalImageAsync(uri); - } - else if (uri.Scheme == "http") - { - imageSource = await LoadHttpImageAsync(uri); - } - else - { - imageSource = new BitmapImage(uri); - } + imageSource = await ImageLoader.LoadImageAsync(uri, true); } catch (Exception ex) { diff --git a/MapControl/Shared/WebMercatorProjection.cs b/MapControl/Shared/WebMercatorProjection.cs index d8e47c6a..68d0c89d 100644 --- a/MapControl/Shared/WebMercatorProjection.cs +++ b/MapControl/Shared/WebMercatorProjection.cs @@ -12,7 +12,7 @@ using System.Windows; namespace MapControl { /// - /// Transforms map coordinates according to the Web Mercator Projection. + /// Transforms map coordinates according to the Web (or Pseudo) Mercator Projection, EPSG:3857. /// 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. @@ -70,8 +70,6 @@ namespace MapControl 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; @@ -79,7 +77,7 @@ namespace MapControl public static double YToLatitude(double y) { - return Math.Atan(Math.Exp(y * Math.PI / 180d)) / Math.PI * 360d - 90d; + return 90d - Math.Atan(Math.Exp(-y * Math.PI / 180d)) / Math.PI * 360d; } } } diff --git a/MapControl/Shared/WmsImageLayer.cs b/MapControl/Shared/WmsImageLayer.cs index a5c59548..a173ebdd 100644 --- a/MapControl/Shared/WmsImageLayer.cs +++ b/MapControl/Shared/WmsImageLayer.cs @@ -11,12 +11,10 @@ using System.Threading.Tasks; using Windows.Data.Xml.Dom; using Windows.UI.Xaml; using Windows.UI.Xaml.Media; -using Windows.UI.Xaml.Media.Imaging; #else using System.Xml; using System.Windows; using System.Windows.Media; -using System.Windows.Media.Imaging; #endif namespace MapControl @@ -25,27 +23,27 @@ namespace MapControl { public static readonly DependencyProperty ServerUriProperty = DependencyProperty.Register( nameof(ServerUri), typeof(Uri), typeof(WmsImageLayer), - new PropertyMetadata(null, (o, e) => ((WmsImageLayer)o).UpdateImage())); + new PropertyMetadata(null, async (o, e) => await ((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())); + new PropertyMetadata("1.3.0", async (o, e) => await ((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())); + new PropertyMetadata(string.Empty, async (o, e) => await ((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())); + new PropertyMetadata(string.Empty, async (o, e) => await ((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())); + new PropertyMetadata("image/png", async (o, e) => await ((WmsImageLayer)o).UpdateImage())); public static readonly DependencyProperty TransparentProperty = DependencyProperty.Register( nameof(Transparent), typeof(bool), typeof(WmsImageLayer), - new PropertyMetadata(false, (o, e) => ((WmsImageLayer)o).UpdateImage())); + new PropertyMetadata(false, async (o, e) => await ((WmsImageLayer)o).UpdateImage())); private string layers = string.Empty; @@ -85,25 +83,32 @@ namespace MapControl set { SetValue(TransparentProperty, value); } } - protected override ImageSource GetImage(BoundingBox boundingBox) + protected override async Task GetImage(BoundingBox boundingBox) { - if (ServerUri == null) + ImageSource imageSource = null; + + if (ServerUri != null) { - return null; + var projectionParameters = ParentMap.MapProjection.WmsQueryParameters(boundingBox, Version); + + if (!string.IsNullOrEmpty(projectionParameters)) + { + var uri = GetRequestUri("GetMap" + + "&LAYERS=" + Layers + "&STYLES=" + Styles + "&FORMAT=" + Format + + "&TRANSPARENT=" + (Transparent ? "TRUE" : "FALSE") + "&" + projectionParameters); + + try + { + imageSource = await ImageLoader.LoadImageAsync(uri, false); + } + catch (Exception ex) + { + Debug.WriteLine("WmsImageLayer: {0}: {1}", uri, ex.Message); + } + } } - var projectionParameters = ParentMap.MapProjection.WmsQueryParameters(boundingBox, Version); - - if (string.IsNullOrEmpty(projectionParameters)) - { - return null; - } - - var uri = GetRequestUri("GetMap" - + "&LAYERS=" + Layers + "&STYLES=" + Styles + "&FORMAT=" + Format - + "&TRANSPARENT=" + (Transparent ? "TRUE" : "FALSE") + "&" + projectionParameters); - - return new BitmapImage(uri); + return imageSource; } public async Task> GetLayerNamesAsync() diff --git a/MapControl/Shared/WorldMercatorProjection.cs b/MapControl/Shared/WorldMercatorProjection.cs new file mode 100644 index 00000000..f7c4df51 --- /dev/null +++ b/MapControl/Shared/WorldMercatorProjection.cs @@ -0,0 +1,116 @@ +// 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 +{ + /// + /// Transforms map coordinates according to the "World Mercator" Projection, EPSG:3395. + /// Longitude values are transformed linearly to X values in meters, by multiplying with MetersPerDegree. + /// Latitude values are transformed according to the elliptical versions of the Mercator equations, + /// as shown in "Map Projections - A Working Manual" (https://pubs.usgs.gov/pp/1395/report.pdf), p.44. + /// + public class WorldMercatorProjection : MapProjection + { + public static double MinLatitudeDelta = 1d / Wgs84EquatorialRadius; // corresponds to 1 meter + public static int MaxIterations = 10; + + public WorldMercatorProjection() + : this("EPSG:3395") + { + } + + public WorldMercatorProjection(string crsId) + { + CrsId = crsId; + LongitudeScale = MetersPerDegree; + MaxLatitude = YToLatitude(180d); + } + + public override double GetViewportScale(double zoomLevel) + { + return DegreesToViewportScale(zoomLevel) / MetersPerDegree; + } + + public override Point GetMapScale(Location location) + { + var lat = location.Latitude * Math.PI / 180d; + var eSinLat = Wgs84Eccentricity * Math.Sin(lat); + var scale = ViewportScale * Math.Sqrt(1d - eSinLat * eSinLat) / Math.Cos(lat); + + 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) + { + if (latitude <= -90d) + { + return double.NegativeInfinity; + } + + if (latitude >= 90d) + { + return double.PositiveInfinity; + } + + var lat = latitude * Math.PI / 180d; + + return Math.Log(Math.Tan(lat / 2d + Math.PI / 4d) * ConformalFactor(lat)) / Math.PI * 180d; + } + + public static double YToLatitude(double y) + { + var t = Math.Exp(-y * Math.PI / 180d); + var lat = Math.PI / 2d - 2d * Math.Atan(t); + var latDelta = 1d; + + for (int i = 0; i < MaxIterations && latDelta > MinLatitudeDelta; i++) + { + var newLat = Math.PI / 2d - 2d * Math.Atan(t * ConformalFactor(lat)); + + latDelta = Math.Abs(newLat - lat); + lat = newLat; + } + + return lat / Math.PI * 180d; + } + + private static double ConformalFactor(double lat) + { + var eSinLat = Wgs84Eccentricity * Math.Sin(lat); + + return Math.Pow((1d - eSinLat) / (1d + eSinLat), Wgs84Eccentricity / 2d); + } + } +} diff --git a/MapControl/UWP/ImageLoader.UWP.cs b/MapControl/UWP/ImageLoader.UWP.cs new file mode 100644 index 00000000..053fcd98 --- /dev/null +++ b/MapControl/UWP/ImageLoader.UWP.cs @@ -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) + +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using Windows.Storage; +using Windows.Storage.Streams; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Media.Imaging; +using Windows.Web.Http; +using Windows.Web.Http.Headers; + +namespace MapControl +{ + public static class ImageLoader + { + /// + /// The HttpClient instance used when image data is downloaded from a web resource. + /// + public static HttpClient HttpClient { get; set; } = new HttpClient(); + + public static async Task LoadImageAsync(Uri uri, bool isTileImage) + { + if (!uri.IsAbsoluteUri || uri.Scheme == "file") + { + return await LoadLocalImageAsync(uri); + } + + if (uri.Scheme == "http") + { + return await LoadHttpImageAsync(uri, isTileImage); + } + + return new BitmapImage(uri); + } + + public static async Task LoadLocalImageAsync(Uri uri) + { + var path = uri.IsAbsoluteUri ? uri.LocalPath : uri.OriginalString; + + if (!await Task.Run(() => File.Exists(path))) + { + return null; + } + + var file = await StorageFile.GetFileFromPathAsync(path); + + using (var stream = await file.OpenReadAsync()) + { + var bitmapImage = new BitmapImage(); + await bitmapImage.SetSourceAsync(stream); + + return bitmapImage; + } + } + + public static async Task LoadHttpImageAsync(Uri uri, bool isTileImage) + { + using (var response = await HttpClient.GetAsync(uri)) + { + if (!response.IsSuccessStatusCode) + { + Debug.WriteLine("ImageLoader: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); + } + else if (!isTileImage || IsTileAvailable(response.Headers)) + { + using (var stream = new InMemoryRandomAccessStream()) + { + await response.Content.WriteToStreamAsync(stream); + stream.Seek(0); + + var bitmapImage = new BitmapImage(); + await bitmapImage.SetSourceAsync(stream); + + return bitmapImage; + } + } + + return null; + } + } + + public static async Task LoadHttpTileImageAsync(Uri uri, Func tileCallback) + { + using (var response = await HttpClient.GetAsync(uri)) + { + if (!response.IsSuccessStatusCode) + { + Debug.WriteLine("ImageLoader: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); + } + else if (IsTileAvailable(response.Headers)) + { + var buffer = await response.Content.ReadAsBufferAsync(); + + await tileCallback(buffer, response.Headers.CacheControl?.MaxAge); + } + + return response.IsSuccessStatusCode; + } + } + + private static bool IsTileAvailable(HttpResponseHeaderCollection responseHeaders) + { + return !responseHeaders.TryGetValue("X-VE-Tile-Info", out string tileInfo) || tileInfo != "no-tile"; + } + } +} diff --git a/MapControl/UWP/MapControl.UWP.csproj b/MapControl/UWP/MapControl.UWP.csproj index ae39c526..c0d70712 100644 --- a/MapControl/UWP/MapControl.UWP.csproj +++ b/MapControl/UWP/MapControl.UWP.csproj @@ -136,13 +136,15 @@ WmsImageLayer.cs + + WorldMercatorProjection.cs + - @@ -151,7 +153,7 @@ - + diff --git a/MapControl/UWP/MapImageLayer.UWP.cs b/MapControl/UWP/MapImageLayer.UWP.cs deleted file mode 100644 index 2952266d..00000000 --- a/MapControl/UWP/MapImageLayer.UWP.cs +++ /dev/null @@ -1,52 +0,0 @@ -// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control -// © 2017 Clemens Fischer -// Licensed under the Microsoft Public License (Ms-PL) - -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Media; -using Windows.UI.Xaml.Media.Imaging; - -namespace MapControl -{ - public partial class MapImageLayer - { - protected void UpdateImage(ImageSource imageSource) - { - SetTopImage(imageSource); - - var bitmapImage = imageSource as BitmapImage; - - if (bitmapImage != null) - { - bitmapImage.ImageOpened += BitmapImageOpened; - bitmapImage.ImageFailed += BitmapImageFailed; - } - else - { - SwapImages(); - } - } - - private void BitmapImageOpened(object sender, RoutedEventArgs e) - { - var bitmapImage = (BitmapImage)sender; - - bitmapImage.ImageOpened -= BitmapImageOpened; - bitmapImage.ImageFailed -= BitmapImageFailed; - - SwapImages(); - } - - private void BitmapImageFailed(object sender, ExceptionRoutedEventArgs e) - { - var bitmapImage = (BitmapImage)sender; - - bitmapImage.ImageOpened -= BitmapImageOpened; - bitmapImage.ImageFailed -= BitmapImageFailed; - - ((Image)Children[topImageIndex]).Source = null; - SwapImages(); - } - } -} diff --git a/MapControl/UWP/TileImageLoader.UWP.cs b/MapControl/UWP/TileImageLoader.UWP.cs index 62d44f0d..d9224209 100644 --- a/MapControl/UWP/TileImageLoader.UWP.cs +++ b/MapControl/UWP/TileImageLoader.UWP.cs @@ -36,50 +36,30 @@ namespace MapControl private async Task LoadTileImageAsync(Tile tile, Uri uri, string cacheKey) { var cacheItem = await Cache.GetAsync(cacheKey); - var buffer = cacheItem?.Buffer; + var cacheBuffer = cacheItem?.Buffer; var loaded = false; - if (buffer == null || cacheItem.Expiration < DateTime.UtcNow) + if (cacheBuffer == null || cacheItem.Expiration < DateTime.UtcNow) { - loaded = await DownloadTileImageAsync(tile, uri, cacheKey); - } - - if (!loaded && buffer != null) // keep expired image if download failed - { - await SetTileImageAsync(tile, buffer); - } - } - - private async Task DownloadTileImageAsync(Tile tile, Uri uri, string cacheKey) - { - var success = false; - - try - { - using (var response = await TileSource.HttpClient.GetAsync(uri)) + try { - success = response.IsSuccessStatusCode; - - if (!success) + loaded = await ImageLoader.LoadHttpTileImageAsync(uri, async (buffer, maxAge) => { - Debug.WriteLine("TileImageLoader: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); - } - else if (TileSource.TileAvailable(response.Headers)) - { - var buffer = await response.Content.ReadAsBufferAsync(); - await SetTileImageAsync(tile, buffer); // create BitmapImage before caching - await Cache.SetAsync(cacheKey, buffer, GetExpiration(response)); - } + await Cache.SetAsync(cacheKey, buffer, GetExpiration(maxAge)); + }); + } + catch (Exception ex) + { + Debug.WriteLine("TileImageLoader: {0}: {1}", uri, ex.Message); } } - catch (Exception ex) - { - Debug.WriteLine("TileImageLoader: {0}: {1}", uri, ex.Message); - } - return success; + if (!loaded && cacheBuffer != null) // keep expired image if download failed + { + await SetTileImageAsync(tile, cacheBuffer); + } } private async Task SetTileImageAsync(Tile tile, IBuffer buffer) diff --git a/MapControl/UWP/TileSource.UWP.cs b/MapControl/UWP/TileSource.UWP.cs index 5a5c47c3..e854a3e8 100644 --- a/MapControl/UWP/TileSource.UWP.cs +++ b/MapControl/UWP/TileSource.UWP.cs @@ -25,14 +25,14 @@ namespace MapControl /// /// Check HTTP response headers for tile availability, e.g. X-VE-Tile-Info=no-tile /// - public static bool TileAvailable(HttpResponseHeaderCollection responseHeaders) + public static bool IsTileAvailable(HttpResponseHeaderCollection responseHeaders) { string tileInfo; return !responseHeaders.TryGetValue("X-VE-Tile-Info", out tileInfo) || tileInfo != "no-tile"; } - protected async Task LoadLocalImageAsync(Uri uri) + protected static async Task LoadLocalImageAsync(Uri uri) { var path = uri.IsAbsoluteUri ? uri.LocalPath : uri.OriginalString; @@ -52,7 +52,7 @@ namespace MapControl } } - protected async Task LoadHttpImageAsync(Uri uri) + protected static async Task LoadHttpImageAsync(Uri uri) { using (var response = await HttpClient.GetAsync(uri)) { @@ -60,7 +60,7 @@ namespace MapControl { Debug.WriteLine("TileSource: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); } - else if (TileAvailable(response.Headers)) + else if (IsTileAvailable(response.Headers)) { using (var stream = new InMemoryRandomAccessStream()) { diff --git a/MapControl/WPF/ImageLoader.WPF.cs b/MapControl/WPF/ImageLoader.WPF.cs new file mode 100644 index 00000000..019a9bde --- /dev/null +++ b/MapControl/WPF/ImageLoader.WPF.cs @@ -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) + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace MapControl +{ + public static class ImageLoader + { + /// + /// The HttpClient instance used when image data is downloaded from a web resource. + /// + public static HttpClient HttpClient { get; set; } = new HttpClient(); + + public static async Task LoadImageAsync(Uri uri, bool isTileImage) + { + if (!uri.IsAbsoluteUri || uri.Scheme == "file") + { + return await LoadLocalImageAsync(uri); + } + + if (uri.Scheme == "http") + { + return await LoadHttpImageAsync(uri, isTileImage); + } + + return new BitmapImage(uri); + } + + public static Task LoadLocalImageAsync(Uri uri) + { + return Task.Run(() => + { + var path = uri.IsAbsoluteUri ? uri.LocalPath : uri.OriginalString; + + if (!File.Exists(path)) + { + return null; + } + + using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + return (ImageSource)BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); + } + }); + } + + public static async Task LoadHttpImageAsync(Uri uri, bool isTileImage) + { + using (var response = await HttpClient.GetAsync(uri)) + { + if (!response.IsSuccessStatusCode) + { + Debug.WriteLine("ImageLoader: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); + } + else if (!isTileImage || IsTileAvailable(response.Headers)) + { + using (var stream = new MemoryStream()) + { + await response.Content.CopyToAsync(stream); + stream.Seek(0, SeekOrigin.Begin); + + return BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); + } + } + + return null; + } + } + + public static async Task LoadHttpTileImageAsync(Uri uri, Func tileCallback) + { + using (var response = await HttpClient.GetAsync(uri)) + { + if (!response.IsSuccessStatusCode) + { + Debug.WriteLine("ImageLoader: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); + } + else if (IsTileAvailable(response.Headers)) + { + var stream = new MemoryStream(); + + await response.Content.CopyToAsync(stream); + stream.Seek(0, SeekOrigin.Begin); + + await tileCallback(stream, response.Headers.CacheControl?.MaxAge); + } + + return response.IsSuccessStatusCode; + } + } + + private static bool IsTileAvailable(HttpResponseHeaders responseHeaders) + { + IEnumerable tileInfo; + + return !responseHeaders.TryGetValues("X-VE-Tile-Info", out tileInfo) || !tileInfo.Contains("no-tile"); + } + } +} diff --git a/MapControl/WPF/MapControl.WPF.csproj b/MapControl/WPF/MapControl.WPF.csproj index a4464887..c24b1e31 100644 --- a/MapControl/WPF/MapControl.WPF.csproj +++ b/MapControl/WPF/MapControl.WPF.csproj @@ -155,17 +155,19 @@ WmsImageLayer.cs + + WorldMercatorProjection.cs + - - + diff --git a/MapControl/WPF/MapGraticule.WPF.cs b/MapControl/WPF/MapGraticule.WPF.cs index 23904949..6528027e 100644 --- a/MapControl/WPF/MapGraticule.WPF.cs +++ b/MapControl/WPF/MapGraticule.WPF.cs @@ -42,55 +42,59 @@ namespace MapControl if (projection != null && !double.IsNaN(projection.LongitudeScale)) { var bounds = projection.ViewportRectToBoundingBox(new Rect(ParentMap.RenderSize)); - var lineDistance = GetLineDistance(); - var labelFormat = GetLabelFormat(lineDistance); - var latLabelStart = Math.Ceiling(bounds.South / lineDistance) * lineDistance; - var lonLabelStart = Math.Ceiling(bounds.West / lineDistance) * lineDistance; - var latLabels = new List - - {ad1cb53e-7aa4-4ec0-b901-b4e0e2665133} - FileDbCache.WPF - {a204a102-c745-4d65-aec8-7b96faedef2d} MapControl.WPF