// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control // Copyright © Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) using System; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; #if WPF using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Threading; #elif UWP using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media; #elif WINUI using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using DispatcherTimer = Microsoft.UI.Dispatching.DispatcherQueueTimer; #endif namespace MapControl { /// /// Displays a single map image, e.g. from a Web Map Service (WMS). /// The image must be provided by the abstract GetImageAsync() method. /// public abstract partial class MapImageLayer : MapPanel, IMapLayer { public static readonly DependencyProperty DescriptionProperty = DependencyPropertyHelper.Register(nameof(Description)); public static readonly DependencyProperty RelativeImageSizeProperty = DependencyPropertyHelper.Register(nameof(RelativeImageSize), 1d); public static readonly DependencyProperty UpdateIntervalProperty = DependencyPropertyHelper.Register(nameof(UpdateInterval), TimeSpan.FromSeconds(0.2), (layer, oldValue, newValue) => layer.updateTimer.Interval = newValue); public static readonly DependencyProperty UpdateWhileViewportChangingProperty = DependencyPropertyHelper.Register(nameof(UpdateWhileViewportChanging)); public static readonly DependencyProperty MapBackgroundProperty = DependencyPropertyHelper.Register(nameof(MapBackground)); public static readonly DependencyProperty MapForegroundProperty = DependencyPropertyHelper.Register(nameof(MapForeground)); public static readonly DependencyProperty LoadingProgressProperty = DependencyPropertyHelper.Register(nameof(LoadingProgress), 1d); private readonly Progress loadingProgress; private readonly DispatcherTimer updateTimer; private bool updateInProgress; public MapImageLayer() { IsHitTestVisible = false; loadingProgress = new Progress(p => SetValue(LoadingProgressProperty, p)); updateTimer = this.CreateTimer(UpdateInterval); updateTimer.Tick += async (s, e) => await UpdateImageAsync(); } /// /// Description of the layer. Used to display copyright information on top of the map. /// public string Description { get => (string)GetValue(DescriptionProperty); set => SetValue(DescriptionProperty, value); } /// /// Relative size of the map image in relation to the current view size. /// Setting a value greater than one will let MapImageLayer request images that /// are larger than the view, in order to support smooth panning. /// public double RelativeImageSize { get => (double)GetValue(RelativeImageSizeProperty); set => SetValue(RelativeImageSizeProperty, value); } /// /// Minimum time interval between images updates. /// public TimeSpan UpdateInterval { get => (TimeSpan)GetValue(UpdateIntervalProperty); set => SetValue(UpdateIntervalProperty, value); } /// /// Controls if images are updated while the viewport is still changing. /// public bool UpdateWhileViewportChanging { get => (bool)GetValue(UpdateWhileViewportChangingProperty); set => SetValue(UpdateWhileViewportChangingProperty, value); } /// /// Optional background brush. Sets MapBase.Background if not null and this layer is the base map layer. /// public Brush MapBackground { get => (Brush)GetValue(MapBackgroundProperty); set => SetValue(MapBackgroundProperty, value); } /// /// Optional foreground brush. Sets MapBase.Foreground if not null and this layer is the base map layer. /// public Brush MapForeground { get => (Brush)GetValue(MapForegroundProperty); set => SetValue(MapForegroundProperty, value); } /// /// Gets the progress of the ImageLoader as a double value between 0 and 1. /// public double LoadingProgress => (double)GetValue(LoadingProgressProperty); protected override void SetParentMap(MapBase map) { if (map != null) { while (Children.Count < 2) { Children.Add(new Image { Opacity = 0d, Stretch = Stretch.Fill }); } } else { updateTimer.Stop(); ClearImages(); Children.Clear(); } base.SetParentMap(map); } protected override async void OnViewportChanged(ViewportChangedEventArgs e) { base.OnViewportChanged(e); if (e.ProjectionChanged) { ClearImages(); await UpdateImageAsync(); // update immediately } else { updateTimer.Run(!UpdateWhileViewportChanging); } } protected abstract Task GetImageAsync(BoundingBox boundingBox, IProgress progress); protected async Task UpdateImageAsync() { if (updateInProgress) { // Update image on next tick, start timer if not running. // updateTimer.Run(); } else { updateInProgress = true; updateTimer.Stop(); ImageSource image = null; var boundingBox = GetImageBoundingBox(); if (boundingBox != null) { try { image = await GetImageAsync(boundingBox, loadingProgress); } catch (Exception ex) { Debug.WriteLine($"{nameof(MapImageLayer)}: {ex.Message}"); } } SwapImages(image, boundingBox); updateInProgress = false; } } private BoundingBox GetImageBoundingBox() { BoundingBox boundingBox = null; if (ParentMap != null && ParentMap.ActualWidth > 0d && ParentMap.ActualHeight > 0d) { var width = ParentMap.ActualWidth * RelativeImageSize; var height = ParentMap.ActualHeight * RelativeImageSize; var x = (ParentMap.ActualWidth - width) / 2d; var y = (ParentMap.ActualHeight - height) / 2d; boundingBox = ParentMap.ViewRectToBoundingBox(new Rect(x, y, width, height)); } return boundingBox; } private void ClearImages() { foreach (var image in Children.OfType()) { image.ClearValue(BoundingBoxProperty); image.ClearValue(Image.SourceProperty); } } private void SwapImages(ImageSource image, BoundingBox boundingBox) { if (Children.Count >= 2) { var topImage = (Image)Children[0]; Children.RemoveAt(0); Children.Insert(1, topImage); topImage.Source = image; SetBoundingBox(topImage, boundingBox); if (MapBase.ImageFadeDuration > TimeSpan.Zero) { FadeOver(); } else { topImage.Opacity = 1d; Children[0].Opacity = 0d; } } } } }