diff --git a/MapControl/Shared/MapBase.cs b/MapControl/Shared/MapBase.cs index 821888b1..fa744e42 100644 --- a/MapControl/Shared/MapBase.cs +++ b/MapControl/Shared/MapBase.cs @@ -76,18 +76,6 @@ namespace MapControl 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 - /// /// Raised when the current viewport has changed. /// @@ -504,7 +492,8 @@ namespace MapControl targetCenter.Latitude, Location.NearestLongitude(targetCenter.Longitude, Center.Longitude))), Duration = AnimationDuration, - EasingFunction = AnimationEasingFunction + EasingFunction = AnimationEasingFunction, + FillBehavior = AnimationFillBehavior }; centerAnimation.Completed += CenterAnimationCompleted; @@ -522,7 +511,6 @@ namespace MapControl InternalSetValue(CenterProperty, TargetCenter); InternalSetValue(CenterPointProperty, MapProjection.LocationToPoint(TargetCenter)); - RemoveAnimation(CenterPointProperty); // remove holding animation in WPF UpdateTransform(); } } @@ -607,7 +595,8 @@ namespace MapControl { To = targetZoomLevel, Duration = AnimationDuration, - EasingFunction = AnimationEasingFunction + EasingFunction = AnimationEasingFunction, + FillBehavior = AnimationFillBehavior }; zoomLevelAnimation.Completed += ZoomLevelAnimationCompleted; @@ -624,8 +613,6 @@ namespace MapControl zoomLevelAnimation = null; InternalSetValue(ZoomLevelProperty, TargetZoomLevel); - RemoveAnimation(ZoomLevelProperty); // remove holding animation in WPF - UpdateTransform(true); } } @@ -681,7 +668,8 @@ namespace MapControl { By = delta, Duration = AnimationDuration, - EasingFunction = AnimationEasingFunction + EasingFunction = AnimationEasingFunction, + FillBehavior = AnimationFillBehavior }; headingAnimation.Completed += HeadingAnimationCompleted; @@ -698,7 +686,6 @@ namespace MapControl headingAnimation = null; InternalSetValue(HeadingProperty, TargetHeading); - RemoveAnimation(HeadingProperty); // remove holding animation in WPF UpdateTransform(); } } diff --git a/MapControl/UWP/MapItemsControl.UWP.cs b/MapControl/Shared/MapItemsControl.cs similarity index 59% rename from MapControl/UWP/MapItemsControl.UWP.cs rename to MapControl/Shared/MapItemsControl.cs index a01a9d25..12f49643 100644 --- a/MapControl/UWP/MapItemsControl.UWP.cs +++ b/MapControl/Shared/MapItemsControl.cs @@ -2,20 +2,39 @@ // © 2017 Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) +#if WINDOWS_UWP using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; +#else +using System.Windows; +using System.Windows.Controls; +#endif namespace MapControl { /// - /// Manages a collection of selectable items on a Map. Uses MapItem as item container class. + /// Container class for an item in a MapItemsControl. + /// + public class MapItem : ListBoxItem + { + public MapItem() + { + DefaultStyleKey = typeof(MapItem); + + MapPanel.InitMapElement(this); + } + } + + /// + /// Manages a collection of selectable items on a Map. /// public class MapItemsControl : ListBox { public MapItemsControl() { DefaultStyleKey = typeof(MapItemsControl); - MapPanel.AddParentMapHandlers(this); + + MapPanel.InitMapElement(this); } protected override DependencyObject GetContainerForItemOverride() diff --git a/MapControl/Shared/MapPanel.cs b/MapControl/Shared/MapPanel.cs index 5a5889d5..0e608d8b 100644 --- a/MapControl/Shared/MapPanel.cs +++ b/MapControl/Shared/MapPanel.cs @@ -16,6 +16,10 @@ using System.Windows.Media; namespace MapControl { + /// + /// Optional interface to hold the value of the MapPanel.ParentMap attached property. + /// May be used to get notified when the property value changes. + /// public interface IMapElement { MapBase ParentMap { get; set; } @@ -35,6 +39,13 @@ namespace MapControl public static readonly DependencyProperty BoundingBoxProperty = DependencyProperty.RegisterAttached( "BoundingBox", typeof(BoundingBox), typeof(MapPanel), new PropertyMetadata(null, BoundingBoxPropertyChanged)); + private MapBase parentMap; + + public MapPanel() + { + InitMapElement(this); + } + public static Location GetLocation(UIElement element) { return (Location)element.GetValue(LocationProperty); @@ -55,12 +66,10 @@ namespace MapControl element.SetValue(BoundingBoxProperty, value); } - private MapBase parentMap; - public MapBase ParentMap { get { return parentMap; } - set { SetParentMapOverride(value); } + set { SetParentMap(value); } } protected override Size MeasureOverride(Size availableSize) @@ -101,7 +110,7 @@ namespace MapControl return finalSize; } - protected virtual void SetParentMapOverride(MapBase map) + protected virtual void SetParentMap(MapBase map) { if (parentMap != null && parentMap != this) { diff --git a/MapControl/Shared/MapPath.cs b/MapControl/Shared/MapPath.cs index 4dbf244a..d36a0bc3 100644 --- a/MapControl/Shared/MapPath.cs +++ b/MapControl/Shared/MapPath.cs @@ -24,6 +24,11 @@ namespace MapControl nameof(Location), typeof(Location), typeof(MapPath), new PropertyMetadata(null, (o, e) => ((MapPath)o).LocationPropertyChanged())); + public MapPath() + { + MapPanel.InitMapElement(this); + } + public Location Location { get { return (Location)GetValue(LocationProperty); } diff --git a/MapControl/Shared/MapTileLayer.cs b/MapControl/Shared/MapTileLayer.cs index ccf3e691..8343d4ea 100644 --- a/MapControl/Shared/MapTileLayer.cs +++ b/MapControl/Shared/MapTileLayer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; #if WINDOWS_UWP using Windows.Foundation; using Windows.UI.Xaml; @@ -21,13 +22,13 @@ namespace MapControl { public interface ITileImageLoader { - void LoadTiles(MapTileLayer tileLayer); + Task LoadTilesAsync(MapTileLayer tileLayer); } /// /// Fills the map viewport with map tiles from a TileSource. /// - public partial class MapTileLayer : Panel, IMapLayer + public class MapTileLayer : Panel, IMapLayer { /// /// A default MapTileLayer using OpenStreetMap data. @@ -66,9 +67,6 @@ namespace MapControl 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)); @@ -92,17 +90,16 @@ namespace MapControl public MapTileLayer(ITileImageLoader tileImageLoader) { - Initialize(); - + IsHitTestVisible = false; RenderTransform = new MatrixTransform(); TileImageLoader = tileImageLoader; Tiles = new List(); updateTimer = new DispatcherTimer { Interval = UpdateInterval }; updateTimer.Tick += (s, e) => UpdateTileGrid(); - } - partial void Initialize(); // Windows Runtime and Silverlight only + MapPanel.InitMapElement(this); + } public ITileImageLoader TileImageLoader { get; private set; } public ICollection Tiles { get; private set; } @@ -162,15 +159,6 @@ namespace MapControl set { SetValue(MaxZoomLevelProperty, value); } } - /// - /// Maximum number of parallel downloads that may be performed by the MapTileLayer's ITileImageLoader. - /// - public int MaxParallelDownloads - { - get { return (int)GetValue(MaxParallelDownloadsProperty); } - set { SetValue(MaxParallelDownloadsProperty, value); } - } - /// /// Minimum time interval between tile updates. /// @@ -400,7 +388,7 @@ namespace MapControl Children.Add(tile.Image); } - TileImageLoader.LoadTiles(this); + var task = TileImageLoader.LoadTilesAsync(this); } } } diff --git a/MapControl/UWP/Pushpin.UWP.cs b/MapControl/Shared/Pushpin.cs similarity index 53% rename from MapControl/UWP/Pushpin.UWP.cs rename to MapControl/Shared/Pushpin.cs index 2577f4be..97bb14e9 100644 --- a/MapControl/UWP/Pushpin.UWP.cs +++ b/MapControl/Shared/Pushpin.cs @@ -2,25 +2,24 @@ // © 2017 Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) +#if WINDOWS_UWP using Windows.UI.Xaml.Controls; +#else +using System.Windows.Controls; +#endif namespace MapControl { /// - /// Displays a pushpin at a geographic location provided by the MapPanel.Location attached property. + /// Pushpin at a geographic location specified by the MapPanel.Location attached property. /// public class Pushpin : ContentControl { public Pushpin() { DefaultStyleKey = typeof(Pushpin); - MapPanel.AddParentMapHandlers(this); - } - public Location Location - { - get { return (Location)GetValue(MapPanel.LocationProperty); } - set { SetValue(MapPanel.LocationProperty, value); } + MapPanel.InitMapElement(this); } } } diff --git a/MapControl/Shared/TileImageLoader.cs b/MapControl/Shared/TileImageLoader.cs index 51891c95..986ca740 100644 --- a/MapControl/Shared/TileImageLoader.cs +++ b/MapControl/Shared/TileImageLoader.cs @@ -7,6 +7,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.IO; using System.Linq; +using System.Net; using System.Threading; using System.Threading.Tasks; #if WINDOWS_UWP @@ -22,11 +23,6 @@ namespace MapControl /// public partial class TileImageLoader : ITileImageLoader { - /// - /// The HttpClient instance used when image data is downloaded from a web resource. - /// - public static HttpClient HttpClient { get; set; } = new HttpClient(); - /// /// Default expiration time for cached tile images. Used when no expiration time /// was transmitted on download. The default value is one day. @@ -50,13 +46,10 @@ namespace MapControl /// 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 pendingTiles = new ConcurrentStack(); private int taskCount; - public void LoadTiles(MapTileLayer tileLayer) + public async Task LoadTilesAsync(MapTileLayer tileLayer) { pendingTiles.Clear(); @@ -69,18 +62,18 @@ namespace MapControl if (Cache == null || string.IsNullOrEmpty(sourceName) || tileSource.UriFormat == null || !tileSource.UriFormat.StartsWith("http")) { - // no caching, load tile images in UI thread + // no caching, load tile images directly foreach (var tile in tiles) { - LoadTileImage(tileSource, tile); + await LoadTileImageAsync(tileSource, tile); } } else { pendingTiles.PushRange(tiles.Reverse().ToArray()); - while (taskCount < Math.Min(pendingTiles.Count, tileLayer.MaxParallelDownloads)) + while (taskCount < Math.Min(pendingTiles.Count, ServicePointManager.DefaultConnectionLimit)) { Interlocked.Increment(ref taskCount); @@ -95,13 +88,13 @@ namespace MapControl } } - private void LoadTileImage(TileSource tileSource, Tile tile) + private async Task LoadTileImageAsync(TileSource tileSource, Tile tile) { tile.Pending = false; try { - var imageSource = tileSource.LoadImage(tile.XIndex, tile.Y, tile.ZoomLevel); + var imageSource = await tileSource.LoadImageAsync(tile.XIndex, tile.Y, tile.ZoomLevel); if (imageSource != null) { diff --git a/MapControl/Shared/TileSource.cs b/MapControl/Shared/TileSource.cs index cf268663..aea491c4 100644 --- a/MapControl/Shared/TileSource.cs +++ b/MapControl/Shared/TileSource.cs @@ -4,13 +4,6 @@ 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 { @@ -92,17 +85,6 @@ namespace MapControl return getUri?.Invoke(x, y, zoomLevel); } - /// - /// Gets the map tile ImageSource without caching in TileImageLoader.Cache. - /// By overriding LoadImage an application can provide arbitrary tile images. - /// - 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 diff --git a/MapControl/UWP/MapBase.UWP.cs b/MapControl/UWP/MapBase.UWP.cs index fe16760c..9dacb391 100644 --- a/MapControl/UWP/MapBase.UWP.cs +++ b/MapControl/UWP/MapBase.UWP.cs @@ -6,11 +6,14 @@ using Windows.Foundation; using Windows.UI; using Windows.UI.Xaml; using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Media.Animation; namespace MapControl { public partial class MapBase { + private const FillBehavior AnimationFillBehavior = FillBehavior.HoldEnd; + public static readonly DependencyProperty ForegroundProperty = DependencyProperty.Register( nameof(Foreground), typeof(Brush), typeof(MapBase), new PropertyMetadata(new SolidColorBrush(Colors.Black))); @@ -39,25 +42,24 @@ namespace MapControl nameof(TargetHeading), typeof(double), typeof(MapBase), new PropertyMetadata(0d, (o, e) => ((MapBase)o).TargetHeadingPropertyChanged((double)e.NewValue))); - partial void Initialize() + public MapBase() { + MapProjection = new WebMercatorProjection(); + ScaleRotateTransform.Children.Add(ScaleTransform); + ScaleRotateTransform.Children.Add(RotateTransform); + // set Background by Style to enable resetting by ClearValue in MapLayerPropertyChanged var style = new Style(typeof(MapBase)); style.Setters.Add(new Setter(BackgroundProperty, new SolidColorBrush(Colors.Transparent))); Style = style; - var clip = new RectangleGeometry(); - Clip = clip; + Clip = new RectangleGeometry(); SizeChanged += (s, e) => { - if (clip.Rect.Width != e.NewSize.Width || clip.Rect.Height != e.NewSize.Height) - { - clip.Rect = new Rect(0d, 0d, e.NewSize.Width, e.NewSize.Height); - - ResetTransformCenter(); - UpdateTransform(); - } + Clip.Rect = new Rect(0d, 0d, e.NewSize.Width, e.NewSize.Height); + ResetTransformCenter(); + UpdateTransform(); }; } } diff --git a/MapControl/UWP/MapControl.UWP.csproj b/MapControl/UWP/MapControl.UWP.csproj index ad8d5913..a6a0b399 100644 --- a/MapControl/UWP/MapControl.UWP.csproj +++ b/MapControl/UWP/MapControl.UWP.csproj @@ -27,6 +27,7 @@ prompt 4 true + CS1998 AnyCPU @@ -88,6 +89,9 @@ MapImageLayer.cs + + MapItemsControl.cs + MapOverlay.cs @@ -112,6 +116,9 @@ OrthographicProjection.cs + + Pushpin.cs + StereographicProjection.cs @@ -143,18 +150,15 @@ - - - - + diff --git a/MapControl/UWP/MapItem.UWP.cs b/MapControl/UWP/MapItem.UWP.cs deleted file mode 100644 index 82189b56..00000000 --- a/MapControl/UWP/MapItem.UWP.cs +++ /dev/null @@ -1,26 +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.Controls; - -namespace MapControl -{ - /// - /// Container class for an item in a MapItemsControl. - /// - public class MapItem : ListBoxItem - { - public MapItem() - { - DefaultStyleKey = typeof(MapItem); - MapPanel.AddParentMapHandlers(this); - } - - public Location Location - { - get { return (Location)GetValue(MapPanel.LocationProperty); } - set { SetValue(MapPanel.LocationProperty, value); } - } - } -} diff --git a/MapControl/UWP/MapOverlay.UWP.cs b/MapControl/UWP/MapOverlay.UWP.cs index c6d18f93..04aa4b5f 100644 --- a/MapControl/UWP/MapOverlay.UWP.cs +++ b/MapControl/UWP/MapOverlay.UWP.cs @@ -56,7 +56,7 @@ namespace MapControl public static readonly DependencyProperty StrokeMiterLimitProperty = DependencyProperty.Register( nameof(StrokeMiterLimit), typeof(double), typeof(MapOverlay), new PropertyMetadata(1d)); - protected override void SetParentMapOverride(MapBase parentMap) + protected override void SetParentMap(MapBase parentMap) { if (GetBindingExpression(ForegroundProperty) != null) { @@ -89,7 +89,7 @@ namespace MapControl } } - base.SetParentMapOverride(parentMap); + base.SetParentMap(parentMap); } } } diff --git a/MapControl/UWP/MapPanel.UWP.cs b/MapControl/UWP/MapPanel.UWP.cs index 50b60b47..3571c282 100644 --- a/MapControl/UWP/MapPanel.UWP.cs +++ b/MapControl/UWP/MapPanel.UWP.cs @@ -12,27 +12,20 @@ namespace MapControl public static readonly DependencyProperty ParentMapProperty = DependencyProperty.RegisterAttached( "ParentMap", typeof(MapBase), typeof(MapPanel), new PropertyMetadata(null, ParentMapPropertyChanged)); - public MapPanel() + public static void InitMapElement(FrameworkElement element) { - if (this is MapBase) + if (element is MapBase) { - SetValue(ParentMapProperty, this); + element.SetValue(ParentMapProperty, element); } else { - AddParentMapHandlers(this); - } - } + // Workaround for missing property value inheritance in Windows Runtime. + // Loaded and Unloaded handlers set and clear the ParentMap property value. - /// - /// Helper method to work around missing property value inheritance in Silverlight and Windows Runtime. - /// Adds Loaded and Unloaded event handlers to the specified FrameworkElement, which set and clear the - /// value of the MapPanel.ParentMap attached property. - /// - public static void AddParentMapHandlers(FrameworkElement element) - { - element.Loaded += (s, e) => GetParentMap(element); - element.Unloaded += (s, e) => element.ClearValue(ParentMapProperty); + element.Loaded += (s, e) => GetParentMap(element); + element.Unloaded += (s, e) => element.ClearValue(ParentMapProperty); + } } public static MapBase GetParentMap(UIElement element) @@ -49,20 +42,12 @@ namespace MapControl private static MapBase FindParentMap(UIElement element) { - MapBase parentMap = null; - var parentElement = VisualTreeHelper.GetParent(element) as UIElement; + var parent = VisualTreeHelper.GetParent(element) as UIElement; - if (parentElement != null) - { - parentMap = parentElement as MapBase; - - if (parentMap == null) - { - parentMap = GetParentMap(parentElement); - } - } - - return parentMap; + return parent == null ? null + : ((parent as MapBase) + ?? (MapBase)element.GetValue(ParentMapProperty) + ?? FindParentMap(parent)); } } } diff --git a/MapControl/UWP/MapPath.UWP.cs b/MapControl/UWP/MapPath.UWP.cs index 43335bd6..1f6ee158 100644 --- a/MapControl/UWP/MapPath.UWP.cs +++ b/MapControl/UWP/MapPath.UWP.cs @@ -12,11 +12,6 @@ namespace MapControl { private Geometry data; - public MapPath() - { - MapPanel.AddParentMapHandlers(this); - } - protected override Size MeasureOverride(Size availableSize) { if (Stretch != Stretch.None) diff --git a/MapControl/UWP/TileImageLoader.UWP.cs b/MapControl/UWP/TileImageLoader.UWP.cs index 7d9e7b43..8d228bd8 100644 --- a/MapControl/UWP/TileImageLoader.UWP.cs +++ b/MapControl/UWP/TileImageLoader.UWP.cs @@ -9,7 +9,6 @@ using Windows.Storage; using Windows.Storage.Streams; using Windows.UI.Core; using Windows.UI.Xaml.Media.Imaging; -using Windows.Web.Http; namespace MapControl { @@ -31,11 +30,6 @@ namespace MapControl var buffer = cacheItem?.Buffer; var loaded = false; - //if (buffer != null) - //{ - // Debug.WriteLine("TileImageLoader: {0}: expire{1} {2}", cacheKey, cacheItem.Expiration < DateTime.UtcNow ? "d" : "s", cacheItem.Expiration); - //} - if (buffer == null || cacheItem.Expiration < DateTime.UtcNow) { loaded = await DownloadTileImageAsync(tile, uri, cacheKey); @@ -49,28 +43,26 @@ namespace MapControl private async Task DownloadTileImageAsync(Tile tile, Uri uri, string cacheKey) { + var success = false; + try { - using (var response = await HttpClient.GetAsync(uri)) + using (var response = await TileSource.HttpClient.GetAsync(uri)) { - if (response.IsSuccessStatusCode) + success = response.IsSuccessStatusCode; + + if (!success) { - string tileInfo; - - if (!response.Headers.TryGetValue(bingMapsTileInfo, out tileInfo) || - tileInfo != bingMapsNoTile) - { - var buffer = await response.Content.ReadAsBufferAsync(); - - await SetTileImageAsync(tile, buffer); // create BitmapImage in UI thread before caching - - await Cache.SetAsync(cacheKey, buffer, GetExpiration(response)); - } - - return true; + Debug.WriteLine("TileImageLoader: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); } + else if (TileSource.TileAvailable(response.Headers)) + { + var buffer = await response.Content.ReadAsBufferAsync(); - Debug.WriteLine("TileImageLoader: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); + await SetTileImageAsync(tile, buffer); // create BitmapImage before caching + + await Cache.SetAsync(cacheKey, buffer, GetExpiration(response)); + } } } catch (Exception ex) @@ -78,7 +70,7 @@ namespace MapControl Debug.WriteLine("TileImageLoader: {0}: {1}", uri, ex.Message); } - return false; + return success; } private async Task SetTileImageAsync(Tile tile, IBuffer buffer) diff --git a/MapControl/UWP/TileSource.UWP.cs b/MapControl/UWP/TileSource.UWP.cs new file mode 100644 index 00000000..46671b21 --- /dev/null +++ b/MapControl/UWP/TileSource.UWP.cs @@ -0,0 +1,84 @@ +// 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.Threading.Tasks; +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 partial class TileSource + { + /// + /// The HttpClient instance used when image data is downloaded from a web resource. + /// + public static HttpClient HttpClient { get; set; } = new HttpClient(); + + /// + /// Check HTTP response headers for tile unavailability, e.g. X-VE-Tile-Info=no-tile + /// + public static bool TileAvailable(HttpResponseHeaderCollection responseHeaders) + { + string tileInfo; + + return !responseHeaders.TryGetValue("X-VE-Tile-Info", out tileInfo) || tileInfo != "no-tile"; + } + + /// + /// Load a tile ImageSource asynchronously from GetUri(x, y, zoomLevel) + /// + public virtual async Task LoadImageAsync(int x, int y, int zoomLevel) + { + ImageSource imageSource = null; + + var uri = GetUri(x, y, zoomLevel); + + if (uri != null) + { + try + { + if (uri.Scheme == "http") + { + using (var response = await HttpClient.GetAsync(uri)) + { + if (!response.IsSuccessStatusCode) + { + Debug.WriteLine("TileSource: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); + } + else if (TileAvailable(response.Headers)) + { + var bitmapImage = new BitmapImage(); + + using (var stream = new InMemoryRandomAccessStream()) + { + await response.Content.WriteToStreamAsync(stream); + stream.Seek(0); + + await bitmapImage.SetSourceAsync(stream); + } + + imageSource = bitmapImage; + } + } + } + else + { + imageSource = new BitmapImage(uri); + } + } + catch (Exception ex) + { + Debug.WriteLine("TileSource: {0}: {1}", uri, ex.Message); + } + } + + return imageSource; + } + } +} diff --git a/MapControl/WPF/MapBase.WPF.cs b/MapControl/WPF/MapBase.WPF.cs index b21702a0..aa6808a6 100644 --- a/MapControl/WPF/MapBase.WPF.cs +++ b/MapControl/WPF/MapBase.WPF.cs @@ -5,11 +5,14 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Media; +using System.Windows.Media.Animation; namespace MapControl { public partial class MapBase { + private const FillBehavior AnimationFillBehavior = FillBehavior.Stop; + public static readonly DependencyProperty ForegroundProperty = Control.ForegroundProperty.AddOwner(typeof(MapBase)); @@ -49,9 +52,11 @@ namespace MapControl BackgroundProperty.OverrideMetadata(typeof(MapBase), new FrameworkPropertyMetadata(Brushes.Transparent)); } - partial void RemoveAnimation(DependencyProperty property) + public MapBase() { - BeginAnimation(property, null); + MapProjection = new WebMercatorProjection(); + ScaleRotateTransform.Children.Add(ScaleTransform); + ScaleRotateTransform.Children.Add(RotateTransform); } /// diff --git a/MapControl/WPF/MapControl.WPF.csproj b/MapControl/WPF/MapControl.WPF.csproj index f970cadf..4179e63f 100644 --- a/MapControl/WPF/MapControl.WPF.csproj +++ b/MapControl/WPF/MapControl.WPF.csproj @@ -23,6 +23,8 @@ DEBUG;TRACE prompt 4 + + none @@ -99,6 +101,9 @@ MapImageLayer.cs + + MapItemsControl.cs + MapOverlay.cs @@ -123,6 +128,9 @@ OrthographicProjection.cs + + Pushpin.cs + StereographicProjection.cs @@ -152,15 +160,12 @@ - - - - + diff --git a/MapControl/WPF/MapItem.WPF.cs b/MapControl/WPF/MapItem.WPF.cs deleted file mode 100644 index 6491ea8e..00000000 --- a/MapControl/WPF/MapItem.WPF.cs +++ /dev/null @@ -1,28 +0,0 @@ -// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control -// © 2017 Clemens Fischer -// Licensed under the Microsoft Public License (Ms-PL) - -using System.Windows; -using System.Windows.Controls; - -namespace MapControl -{ - /// - /// Container class for an item in a MapItemsControl. - /// - public class MapItem : ListBoxItem - { - static MapItem() - { - DefaultStyleKeyProperty.OverrideMetadata(typeof(MapItem), new FrameworkPropertyMetadata(typeof(MapItem))); - } - - public static readonly DependencyProperty LocationProperty = MapPanel.LocationProperty.AddOwner(typeof(MapItem)); - - public Location Location - { - get { return (Location)GetValue(LocationProperty); } - set { SetValue(LocationProperty, value); } - } - } -} diff --git a/MapControl/WPF/MapItemsControl.WPF.cs b/MapControl/WPF/MapItemsControl.WPF.cs deleted file mode 100644 index 2a7f3808..00000000 --- a/MapControl/WPF/MapItemsControl.WPF.cs +++ /dev/null @@ -1,30 +0,0 @@ -// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control -// © 2017 Clemens Fischer -// Licensed under the Microsoft Public License (Ms-PL) - -using System.Windows; -using System.Windows.Controls; - -namespace MapControl -{ - /// - /// Manages a collection of selectable items on a Map. Uses MapItem as item container class. - /// - public class MapItemsControl : ListBox - { - static MapItemsControl() - { - DefaultStyleKeyProperty.OverrideMetadata(typeof(MapItemsControl), new FrameworkPropertyMetadata(typeof(MapItemsControl))); - } - - protected override DependencyObject GetContainerForItemOverride() - { - return new MapItem(); - } - - protected override bool IsItemItsOwnContainerOverride(object item) - { - return item is MapItem; - } - } -} diff --git a/MapControl/WPF/MapOverlay.WPF.cs b/MapControl/WPF/MapOverlay.WPF.cs index a8ec2f39..a68d1388 100644 --- a/MapControl/WPF/MapOverlay.WPF.cs +++ b/MapControl/WPF/MapOverlay.WPF.cs @@ -51,7 +51,7 @@ namespace MapControl public static readonly DependencyProperty StrokeMiterLimitProperty = Shape.StrokeMiterLimitProperty.AddOwner( typeof(MapOverlay), new FrameworkPropertyMetadata { AffectsRender = true }); - protected override void SetParentMapOverride(MapBase parentMap) + protected override void SetParentMap(MapBase parentMap) { if (GetBindingExpression(StrokeProperty) != null) { @@ -67,7 +67,7 @@ namespace MapControl }); } - base.SetParentMapOverride(parentMap); + base.SetParentMap(parentMap); } } } diff --git a/MapControl/WPF/MapPanel.WPF.cs b/MapControl/WPF/MapPanel.WPF.cs index e7aa16bd..cf5a754f 100644 --- a/MapControl/WPF/MapPanel.WPF.cs +++ b/MapControl/WPF/MapPanel.WPF.cs @@ -14,17 +14,17 @@ namespace MapControl public static readonly DependencyProperty ParentMapProperty = ParentMapPropertyKey.DependencyProperty; - public MapPanel() - { - if (this is MapBase) - { - SetValue(ParentMapPropertyKey, this); - } - } - public static MapBase GetParentMap(UIElement element) { return (MapBase)element.GetValue(ParentMapProperty); } + + public static void InitMapElement(FrameworkElement element) + { + if (element is MapBase) + { + element.SetValue(ParentMapPropertyKey, element); + } + } } } diff --git a/MapControl/WPF/Pushpin.WPF.cs b/MapControl/WPF/Pushpin.WPF.cs deleted file mode 100644 index ef5ef99b..00000000 --- a/MapControl/WPF/Pushpin.WPF.cs +++ /dev/null @@ -1,29 +0,0 @@ -// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control -// © 2017 Clemens Fischer -// Licensed under the Microsoft Public License (Ms-PL) - -using System.Windows; -using System.Windows.Controls; - -namespace MapControl -{ - /// - /// Displays a pushpin at a geographic location provided by the MapPanel.Location attached property. - /// - public class Pushpin : ContentControl - { - static Pushpin() - { - DefaultStyleKeyProperty.OverrideMetadata(typeof(Pushpin), new FrameworkPropertyMetadata(typeof(Pushpin))); - } - - public static readonly DependencyProperty LocationProperty = - MapPanel.LocationProperty.AddOwner(typeof(Pushpin)); - - public Location Location - { - get { return (Location)GetValue(LocationProperty); } - set { SetValue(LocationProperty, value); } - } - } -} diff --git a/MapControl/WPF/TileImageLoader.WPF.cs b/MapControl/WPF/TileImageLoader.WPF.cs index 09b59438..5d261664 100644 --- a/MapControl/WPF/TileImageLoader.WPF.cs +++ b/MapControl/WPF/TileImageLoader.WPF.cs @@ -3,11 +3,8 @@ // 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.Runtime.Caching; using System.Text; using System.Threading.Tasks; @@ -34,11 +31,6 @@ namespace MapControl var buffer = GetCachedImage(cacheKey, out expiration); var loaded = false; - //if (buffer != null) - //{ - // Debug.WriteLine("TileImageLoader: {0}: expire{1} {2}", cacheKey, expiration < DateTime.UtcNow ? "d" : "s", expiration); - //} - if (buffer == null || expiration < DateTime.UtcNow) { loaded = await DownloadTileImageAsync(tile, uri, cacheKey); @@ -55,32 +47,31 @@ namespace MapControl private async Task DownloadTileImageAsync(Tile tile, Uri uri, string cacheKey) { + var success = false; + try { - using (var response = await HttpClient.GetAsync(uri)) + using (var response = await TileSource.HttpClient.GetAsync(uri)) { - if (response.IsSuccessStatusCode) + success = response.IsSuccessStatusCode; + + if (!success) { - IEnumerable tileInfo; - - if (!response.Headers.TryGetValues(bingMapsTileInfo, out tileInfo) || - !tileInfo.Contains(bingMapsNoTile)) + Debug.WriteLine("TileImageLoader: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); + } + else if (TileSource.TileAvailable(response.Headers)) + { + using (var stream = new MemoryStream()) { - using (var stream = new MemoryStream()) - { - await response.Content.CopyToAsync(stream); - stream.Seek(0, SeekOrigin.Begin); + await response.Content.CopyToAsync(stream); + stream.Seek(0, SeekOrigin.Begin); - await SetTileImageAsync(tile, stream); // create BitmapFrame in UI thread before caching + await SetTileImageAsync(tile, stream); // create BitmapFrame before caching - SetCachedImage(cacheKey, stream, GetExpiration(response)); - } + SetCachedImage(cacheKey, stream, GetExpiration(response)); } - - return true; } - Debug.WriteLine("TileImageLoader: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); } } catch (Exception ex) @@ -88,7 +79,7 @@ namespace MapControl Debug.WriteLine("TileImageLoader: {0}: {1}", uri, ex.Message); } - return false; + return success; } private async Task SetTileImageAsync(Tile tile, MemoryStream stream) diff --git a/MapControl/WPF/TileSource.WPF.cs b/MapControl/WPF/TileSource.WPF.cs new file mode 100644 index 00000000..96f1da7b --- /dev/null +++ b/MapControl/WPF/TileSource.WPF.cs @@ -0,0 +1,82 @@ +// 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 partial class TileSource + { + /// + /// The HttpClient instance used when image data is downloaded from a web resource. + /// + public static HttpClient HttpClient { get; set; } = new HttpClient(); + + /// + /// Check HTTP response headers for tile unavailability, e.g. X-VE-Tile-Info=no-tile + /// + public static bool TileAvailable(HttpResponseHeaders responseHeaders) + { + IEnumerable tileInfo; + + return !responseHeaders.TryGetValues("X-VE-Tile-Info", out tileInfo) || !tileInfo.Contains("no-tile"); + } + + /// + /// Load a tile ImageSource asynchronously from GetUri(x, y, zoomLevel) + /// + public virtual async Task LoadImageAsync(int x, int y, int zoomLevel) + { + ImageSource imageSource = null; + + var uri = GetUri(x, y, zoomLevel); + + if (uri != null) + { + try + { + if (uri.Scheme == "http") + { + using (var response = await HttpClient.GetAsync(uri)) + { + if (!response.IsSuccessStatusCode) + { + Debug.WriteLine("TileSource: {0}: {1} {2}", uri, (int)response.StatusCode, response.ReasonPhrase); + } + else if (TileAvailable(response.Headers)) + { + using (var stream = new MemoryStream()) + { + await response.Content.CopyToAsync(stream); + stream.Seek(0, SeekOrigin.Begin); + + imageSource = await Task.Run(() => BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad)); + } + } + } + } + else + { + imageSource = BitmapFrame.Create(uri, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); + } + } + catch (Exception ex) + { + Debug.WriteLine("TileSource: {0}: {1}", uri, ex.Message); + } + } + + return imageSource; + } + } +}