// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control // Copyright © 2024 Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; using System; using System.Collections.Generic; using System.Threading.Tasks; namespace MapControl { public abstract class MapTileLayerBase : Panel { public static readonly StyledProperty TileSourceProperty = AvaloniaProperty.Register(nameof(TileSource)); public static readonly StyledProperty SourceNameProperty = AvaloniaProperty.Register(nameof(SourceName)); public static readonly StyledProperty DescriptionProperty = AvaloniaProperty.Register(nameof(Description)); public static readonly StyledProperty MaxBackgroundLevelsProperty = AvaloniaProperty.Register(nameof(MaxBackgroundLevels), 5); public static readonly StyledProperty UpdateIntervalProperty = AvaloniaProperty.Register(nameof(AvaloniaProperty), TimeSpan.FromSeconds(0.2)); public static readonly StyledProperty UpdateWhileViewportChangingProperty = AvaloniaProperty.Register(nameof(UpdateWhileViewportChanging)); public static readonly StyledProperty MapBackgroundProperty = AvaloniaProperty.Register(nameof(MapBackground)); public static readonly StyledProperty MapForegroundProperty = AvaloniaProperty.Register(nameof(MapForeground)); public static readonly DirectProperty LoadingProgressProperty = AvaloniaProperty.RegisterDirect(nameof(LoadingProgress), layer => layer.loadingProgressValue); private readonly DispatcherTimer updateTimer; private readonly Progress loadingProgress; private double loadingProgressValue; private ITileImageLoader tileImageLoader; static MapTileLayerBase() { MapPanel.ParentMapProperty.Changed.AddClassHandler( (layer, args) => layer.OnParentMapPropertyChanged(args.NewValue.Value)); TileSourceProperty.Changed.AddClassHandler( async (layer, args) => await layer.Update(true)); UpdateIntervalProperty.Changed.AddClassHandler( (layer, args) => layer.updateTimer.Interval = args.NewValue.Value); } protected MapTileLayerBase() { RenderTransform = new MatrixTransform(); RenderTransformOrigin = new RelativePoint(); loadingProgress = new Progress(p => SetAndRaise(LoadingProgressProperty, ref loadingProgressValue, p)); updateTimer = this.CreateTimer(UpdateInterval); updateTimer.Tick += async (s, e) => await Update(false); } public MapBase ParentMap { get; private set; } public ITileImageLoader TileImageLoader { get => tileImageLoader ??= new TileImageLoader(); set => tileImageLoader = value; } /// /// Provides map tile URIs or images. /// public TileSource TileSource { get => GetValue(TileSourceProperty); set => SetValue(TileSourceProperty, value); } /// /// Name of the TileSource. Used as component of a tile cache key. /// public string SourceName { get => GetValue(SourceNameProperty); set => SetValue(SourceNameProperty, value); } /// /// Description of the layer. Used to display copyright information on top of the map. /// public string Description { get => GetValue(DescriptionProperty); set => SetValue(DescriptionProperty, value); } /// /// Maximum number of background tile levels. Default value is 5. /// Only effective in a MapTileLayer or WmtsTileLayer that is the MapLayer of its ParentMap. /// public int MaxBackgroundLevels { get => GetValue(MaxBackgroundLevelsProperty); set => SetValue(MaxBackgroundLevelsProperty, value); } /// /// Minimum time interval between tile updates. /// public TimeSpan UpdateInterval { get => GetValue(UpdateIntervalProperty); set => SetValue(UpdateIntervalProperty, value); } /// /// Controls if tiles are updated while the viewport is still changing. /// public bool UpdateWhileViewportChanging { get => GetValue(UpdateWhileViewportChangingProperty); set => SetValue(UpdateWhileViewportChangingProperty, value); } /// /// Optional background brush. Sets MapBase.Background if not null and this layer is the base map layer. /// public IBrush MapBackground { get => GetValue(MapBackgroundProperty); set => SetValue(MapBackgroundProperty, value); } /// /// Optional foreground brush. Sets MapBase.Foreground if not null and this layer is the base map layer. /// public IBrush MapForeground { get => GetValue(MapForegroundProperty); set => SetValue(MapForegroundProperty, value); } /// /// Gets the progress of the TileImageLoader as a double value between 0 and 1. /// public double LoadingProgress => loadingProgressValue; protected bool IsBaseMapLayer { get { var parentMap = MapPanel.GetParentMap(this); return parentMap != null && parentMap.Children.Count > 0 && parentMap.Children[0] == this; } } protected abstract void SetRenderTransform(); protected abstract Task UpdateTileLayer(bool tileSourceChanged); protected Task LoadTiles(IEnumerable tiles, string cacheName) { return TileImageLoader.LoadTilesAsync(tiles, TileSource, cacheName, loadingProgress); } private Task Update(bool tileSourceChanged) { updateTimer.Stop(); return UpdateTileLayer(tileSourceChanged); } private async void OnViewportChanged(object sender, ViewportChangedEventArgs e) { if (e.TransformCenterChanged || e.ProjectionChanged || Children.Count == 0) { await Update(false); // update immediately } else { SetRenderTransform(); updateTimer.Run(!UpdateWhileViewportChanging); } } private void OnParentMapPropertyChanged(MapBase parentMap) { if (ParentMap != null) { ParentMap.ViewportChanged -= OnViewportChanged; } ParentMap = parentMap; if (ParentMap != null) { ParentMap.ViewportChanged += OnViewportChanged; } updateTimer.Run(); } } }