// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control // Copyright © 2023 Clemens Fischer // Licensed under the Microsoft Public License (Ms-PL) using System; using System.Threading.Tasks; #if WINUI using Windows.Foundation; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; #elif UWP using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Media; #else using System.Windows; using System.Windows.Media; #endif namespace MapControl { /// /// Displays web mercator map tiles. /// public class MapTileLayer : MapTileLayerBase { private const int TileSize = 256; private static readonly Point MapTopLeft = new Point( -180d * MapProjection.Wgs84MeterPerDegree, 180d * MapProjection.Wgs84MeterPerDegree); /// /// A default MapTileLayer using OpenStreetMap data. /// public static MapTileLayer OpenStreetMapTileLayer => new MapTileLayer { TileSource = new TileSource { UriTemplate = "https://tile.openstreetmap.org/{z}/{x}/{y}.png" }, SourceName = "OpenStreetMap", Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)" }; public static readonly DependencyProperty MinZoomLevelProperty = DependencyProperty.Register( nameof(MinZoomLevel), typeof(int), typeof(MapTileLayer), new PropertyMetadata(0)); public static readonly DependencyProperty MaxZoomLevelProperty = DependencyProperty.Register( nameof(MaxZoomLevel), typeof(int), typeof(MapTileLayer), new PropertyMetadata(19)); public static readonly DependencyProperty ZoomLevelOffsetProperty = DependencyProperty.Register( nameof(ZoomLevelOffset), typeof(double), typeof(MapTileLayer), new PropertyMetadata(0d)); public MapTileLayer() : this(new TileImageLoader()) { } public MapTileLayer(ITileImageLoader tileImageLoader) : base(tileImageLoader) { } public TileMatrix TileMatrix { get; private set; } public TileCollection Tiles { get; private set; } = new TileCollection(); /// /// 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) { availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity); foreach (var tile in Tiles) { tile.Image.Measure(availableSize); } return new Size(); } protected override Size ArrangeOverride(Size finalSize) { if (TileMatrix != null) { foreach (var tile in Tiles) { // Arrange tiles relative to XMin/YMin. // var tileSize = TileSize << (TileMatrix.ZoomLevel - tile.ZoomLevel); var x = tileSize * tile.X - TileSize * TileMatrix.XMin; var y = tileSize * tile.Y - TileSize * TileMatrix.YMin; tile.Image.Width = tileSize; tile.Image.Height = tileSize; tile.Image.Arrange(new Rect(x, y, tileSize, tileSize)); } } return finalSize; } protected override Task UpdateTileLayer() { var updateTiles = false; if (ParentMap == null || ParentMap.MapProjection.Type != MapProjectionType.WebMercator) { updateTiles = TileMatrix != null; TileMatrix = null; } else { if (TileSource != TileImageLoader.TileSource) { Tiles = new TileCollection(); // clear all updateTiles = true; } if (SetTileMatrix()) { updateTiles = true; } SetRenderTransform(); } if (updateTiles) { UpdateTiles(); return TileImageLoader.LoadTiles(Tiles, TileSource, SourceName); } return Task.CompletedTask; } protected override void SetRenderTransform() { if (TileMatrix != null) { // Tile matrix origin in pixels. // var tileMatrixOrigin = new Point(TileSize * TileMatrix.XMin, TileSize * TileMatrix.YMin); var tileMatrixScale = ViewTransform.ZoomLevelToScale(TileMatrix.ZoomLevel); ((MatrixTransform)RenderTransform).Matrix = ParentMap.ViewTransform.GetTileLayerTransform(tileMatrixScale, MapTopLeft, tileMatrixOrigin); } } private bool SetTileMatrix() { // Add 0.001 to avoid rounding issues. // var tileMatrixZoomLevel = (int)Math.Floor(ParentMap.ZoomLevel - ZoomLevelOffset + 0.001); var tileMatrixScale = ViewTransform.ZoomLevelToScale(tileMatrixZoomLevel); // Bounds in tile pixels from view size. // var bounds = ParentMap.ViewTransform.GetTileMatrixBounds(tileMatrixScale, MapTopLeft, ParentMap.RenderSize); // 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 TileCollection(); if (TileSource != null && TileMatrix != null) { var maxZoomLevel = Math.Min(TileMatrix.ZoomLevel, MaxZoomLevel); if (maxZoomLevel >= MinZoomLevel) { var minZoomLevel = IsBaseMapLayer ? Math.Max(TileMatrix.ZoomLevel - MaxBackgroundLevels, MinZoomLevel) : maxZoomLevel; for (var z = minZoomLevel; z <= maxZoomLevel; z++) { var numTiles = 1 << z; var tileSize = 1 << (TileMatrix.ZoomLevel - z); var x1 = (int)Math.Floor((double)TileMatrix.XMin / tileSize); // may be negative var x2 = TileMatrix.XMax / tileSize; // may be greater than numTiles-1 var y1 = Math.Max(TileMatrix.YMin / tileSize, 0); var y2 = Math.Min(TileMatrix.YMax / tileSize, numTiles - 1); for (var y = y1; y <= y2; y++) { for (var x = x1; x <= x2; x++) { tiles.Add(Tiles.GetTile(z, x, y, numTiles)); } } } } } Tiles = tiles; Children.Clear(); foreach (var tile in tiles) { Children.Add(tile.Image); } } } }