// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control // © 2022 Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) using System; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; #if WINUI using Windows.Foundation; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Animation; using DispatcherTimer = Microsoft.UI.Dispatching.DispatcherQueueTimer; #elif UWP using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Animation; #else using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Threading; #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 class MapImageLayer : MapPanel, IMapLayer { public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register( nameof(Description), typeof(string), typeof(MapImageLayer), new PropertyMetadata(null)); 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 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)); public static readonly DependencyProperty LoadingProgressProperty = DependencyProperty.Register( nameof(LoadingProgress), typeof(double), typeof(MapImageLayer), new PropertyMetadata(1d)); private readonly Progress imageProgress; private readonly DispatcherTimer updateTimer; private bool updateInProgress; public MapImageLayer() { imageProgress = new Progress(p => LoadingProgress = 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 { return (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 { return (double)GetValue(RelativeImageSizeProperty); } set { SetValue(RelativeImageSizeProperty, value); } } /// /// Minimum time interval between images updates. /// public TimeSpan UpdateInterval { get { return (TimeSpan)GetValue(UpdateIntervalProperty); } set { SetValue(UpdateIntervalProperty, value); } } /// /// Controls if images are updated while the viewport is still changing. /// public bool UpdateWhileViewportChanging { get { return (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 { return (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 { return (Brush)GetValue(MapForegroundProperty); } set { SetValue(MapForegroundProperty, value); } } /// /// Gets the progress of the ImageLoader as a double value between 0 and 1. /// public double LoadingProgress { get { return (double)GetValue(LoadingProgressProperty); } private set { SetValue(LoadingProgressProperty, value); } } /// /// The current BoundingBox /// public BoundingBox BoundingBox { get; private set; } protected override void SetParentMap(MapBase map) { if (map == null) { updateTimer.Stop(); ClearImages(); Children.Clear(); } else if (Children.Count == 0) { Children.Add(new Image { Opacity = 0d, Stretch = Stretch.Fill }); Children.Add(new Image { Opacity = 0d, Stretch = Stretch.Fill }); } base.SetParentMap(map); } protected override async void OnViewportChanged(ViewportChangedEventArgs e) { if (e.ProjectionChanged) { ClearImages(); base.OnViewportChanged(e); await UpdateImageAsync(); } else { AdjustBoundingBox(e.LongitudeOffset); base.OnViewportChanged(e); updateTimer.Run(!UpdateWhileViewportChanging); } } protected async Task UpdateImageAsync() { if (updateInProgress) // update image on next tick { updateTimer.Run(); // start timer if not running } else { updateTimer.Stop(); if (ParentMap != null && ParentMap.RenderSize.Width > 0 && ParentMap.RenderSize.Height > 0) { updateInProgress = true; UpdateBoundingBox(); ImageSource image = null; if (BoundingBox != null) { try { image = await GetImageAsync(imageProgress); } catch (Exception ex) { Debug.WriteLine($"MapImageLayer: {ex.Message}"); } } SwapImages(image); updateInProgress = false; } } } protected abstract Task GetImageAsync(IProgress progress); private void UpdateBoundingBox() { 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.ViewRectToBoundingBox(rect); } private void AdjustBoundingBox(double longitudeOffset) { if (Math.Abs(longitudeOffset) > 180d && BoundingBox != null) { var offset = 360d * Math.Sign(longitudeOffset); BoundingBox = new BoundingBox(BoundingBox, offset); foreach (var image in Children.OfType()) { var imageBoundingBox = GetBoundingBox(image); if (imageBoundingBox != null) { SetBoundingBox(image, new BoundingBox(imageBoundingBox, offset)); } } } } private void ClearImages() { foreach (var image in Children.OfType()) { image.ClearValue(BoundingBoxProperty); image.ClearValue(Image.SourceProperty); } } private void SwapImages(ImageSource image) { if (Children.Count >= 2) { var topImage = (Image)Children[0]; var bottomImage = (Image)Children[1]; Children.RemoveAt(0); Children.Insert(1, topImage); topImage.Source = image; SetBoundingBox(topImage, BoundingBox); topImage.BeginAnimation(OpacityProperty, new DoubleAnimation { To = 1d, Duration = MapBase.ImageFadeDuration }); bottomImage.BeginAnimation(OpacityProperty, new DoubleAnimation { To = 0d, BeginTime = MapBase.ImageFadeDuration, Duration = TimeSpan.Zero }); } } } }