using System; using System.Collections.Generic; using System.Linq; #if WPF using System.Windows; using System.Windows.Media; #elif UWP using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Media; #elif WINUI using Windows.Foundation; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; #elif AVALONIA using Avalonia; using Avalonia.Media; #endif namespace MapControl { /// /// Displays a Web Mercator tile pyramid. /// public class MapTileLayer : TilePyramidLayer { private const int tileSize = 256; private static readonly Point mapTopLeft = new(-180d * MapProjection.Wgs84MeterPerDegree, 180d * MapProjection.Wgs84MeterPerDegree); public static readonly DependencyProperty TileSourceProperty = DependencyPropertyHelper.Register(nameof(TileSource), null, (layer, oldValue, newValue) => layer.UpdateTileCollection(true)); public static readonly DependencyProperty MinZoomLevelProperty = DependencyPropertyHelper.Register(nameof(MinZoomLevel), 0); public static readonly DependencyProperty MaxZoomLevelProperty = DependencyPropertyHelper.Register(nameof(MaxZoomLevel), 19); public static readonly DependencyProperty ZoomLevelOffsetProperty = DependencyPropertyHelper.Register(nameof(ZoomLevelOffset), 0d); /// /// A default MapTileLayer using OpenStreetMap data. /// public static MapTileLayer OpenStreetMapTileLayer => new() { TileSource = TileSource.Parse("https://tile.openstreetmap.org/{z}/{x}/{y}.png"), SourceName = "OpenStreetMap", Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)" }; public MapTileLayer() { this.SetRenderTransform(new MatrixTransform()); } public override IReadOnlyCollection SupportedCrsIds { get; } = [WebMercatorProjection.DefaultCrsId]; public TileMatrix TileMatrix { get; private set; } public ICollection Tiles { get; private set; } = []; /// /// Provides the ImagesSource or image request Uri for map tiles. /// public TileSource TileSource { get => (TileSource)GetValue(TileSourceProperty); set => SetValue(TileSourceProperty, value); } /// /// Minimum zoom level supported by the MapTileLayer. Default value is 0. /// public int MinZoomLevel { get => (int)GetValue(MinZoomLevelProperty); set => SetValue(MinZoomLevelProperty, value); } /// /// Maximum zoom level supported by the MapTileLayer. Default value is 19. /// public int MaxZoomLevel { get => (int)GetValue(MaxZoomLevelProperty); set => SetValue(MaxZoomLevelProperty, value); } /// /// Optional offset between the map zoom level and the topmost tile zoom level. /// Default value is 0. /// public double ZoomLevelOffset { get => (double)GetValue(ZoomLevelOffsetProperty); set => SetValue(ZoomLevelOffsetProperty, value); } protected override Size MeasureOverride(Size availableSize) { foreach (var tile in Tiles) { tile.Image.Measure(availableSize); } return new Size(); } protected override Size ArrangeOverride(Size finalSize) { foreach (var tile in Tiles) { // Arrange tiles relative to TileMatrix.XMin/YMin. // var tileSize = MapTileLayer.tileSize << (TileMatrix.ZoomLevel - tile.ZoomLevel); var x = tileSize * tile.X - MapTileLayer.tileSize * TileMatrix.XMin; var y = tileSize * tile.Y - MapTileLayer.tileSize * TileMatrix.YMin; tile.Image.Width = tileSize; tile.Image.Height = tileSize; tile.Image.Arrange(new Rect(x, y, tileSize, tileSize)); } return finalSize; } protected override void UpdateRenderTransform() { if (TileMatrix != null) { // Tile matrix origin in pixels. // var tileMatrixOrigin = new Point(tileSize * TileMatrix.XMin, tileSize * TileMatrix.YMin); var tileMatrixScale = MapBase.ZoomLevelToScale(TileMatrix.ZoomLevel); ((MatrixTransform)RenderTransform).Matrix = ParentMap.ViewTransform.GetTileLayerTransform(tileMatrixScale, mapTopLeft, tileMatrixOrigin); } } protected override void UpdateTileCollection() { UpdateTileCollection(false); } private void UpdateTileCollection(bool tileSourceChanged) { if (TileSource == null || ParentMap == null || !SupportedCrsIds.Contains(ParentMap.MapProjection.CrsId)) { CancelLoadTiles(); Children.Clear(); Tiles.Clear(); TileMatrix = null; } else if (SetTileMatrix() || tileSourceChanged) { if (tileSourceChanged) { Tiles.Clear(); } UpdateRenderTransform(); UpdateTiles(); BeginLoadTiles(Tiles, TileSource, SourceName); } } private bool SetTileMatrix() { // Add 0.001 to avoid floating point precision. // var tileMatrixZoomLevel = (int)Math.Floor(ParentMap.ZoomLevel - ZoomLevelOffset + 0.001); var tileMatrixScale = MapBase.ZoomLevelToScale(tileMatrixZoomLevel); // Tile matrix bounds in pixels. // var bounds = ParentMap.ViewTransform.GetTileMatrixBounds(tileMatrixScale, mapTopLeft, ParentMap.ActualWidth, ParentMap.ActualHeight); // Tile X and Y bounds. // var xMin = (int)Math.Floor(bounds.X / tileSize); var yMin = (int)Math.Floor(bounds.Y / tileSize); var xMax = (int)Math.Floor((bounds.X + bounds.Width) / tileSize); var yMax = (int)Math.Floor((bounds.Y + bounds.Height) / tileSize); if (TileMatrix != null && TileMatrix.ZoomLevel == tileMatrixZoomLevel && TileMatrix.XMin == xMin && TileMatrix.YMin == yMin && TileMatrix.XMax == xMax && TileMatrix.YMax == yMax) { return false; } TileMatrix = new TileMatrix(tileMatrixZoomLevel, xMin, yMin, xMax, yMax); return true; } private void UpdateTiles() { var tiles = new ImageTileList(); var maxZoomLevel = Math.Min(TileMatrix.ZoomLevel, MaxZoomLevel); if (maxZoomLevel >= MinZoomLevel) { var minZoomLevel = maxZoomLevel; if (IsBaseMapLayer) { var bgLevels = Math.Max(MaxBackgroundLevels, 0); minZoomLevel = Math.Max(TileMatrix.ZoomLevel - bgLevels, MinZoomLevel); } for (var zoomLevel = minZoomLevel; zoomLevel <= maxZoomLevel; zoomLevel++) { var tileCount = 1 << zoomLevel; // per row and column // Right-shift divides with rounding down also negative values, https://stackoverflow.com/q/55196178 // var shift = TileMatrix.ZoomLevel - zoomLevel; var xMin = TileMatrix.XMin >> shift; // may be < 0 var xMax = TileMatrix.XMax >> shift; // may be >= tileCount var yMin = Math.Max(TileMatrix.YMin >> shift, 0); var yMax = Math.Min(TileMatrix.YMax >> shift, tileCount - 1); tiles.FillMatrix(Tiles, zoomLevel, xMin, yMin, xMax, yMax, tileCount); } } Tiles = tiles; Children.Clear(); foreach (var tile in tiles) { Children.Add(tile.Image); } } } }