From 0e27e95c6fde16d4c7135664a9c0a1660f5cbe6b Mon Sep 17 00:00:00 2001 From: ClemensFischer Date: Tue, 22 Nov 2022 19:15:34 +0100 Subject: [PATCH] WMTS tile handling --- MapControl/Shared/BingMapsTileSource.cs | 8 ++--- MapControl/Shared/BoundingBoxTileSource.cs | 10 +++---- MapControl/Shared/MapTileLayer.cs | 34 +++++++-------------- MapControl/Shared/Tile.cs | 14 +++------ MapControl/Shared/TileCollection.cs | 35 ++++++++++++++++++++++ MapControl/Shared/TileImageLoader.cs | 8 ++--- MapControl/Shared/TileSource.cs | 20 ++++++------- MapControl/Shared/WmtsTileMatrix.cs | 5 +++- MapControl/Shared/WmtsTileMatrixLayer.cs | 26 ++++++++++------ MapControl/Shared/WmtsTileSource.cs | 8 ++--- MapControl/UWP/MapControl.UWP.csproj | 3 ++ 11 files changed, 100 insertions(+), 71 deletions(-) create mode 100644 MapControl/Shared/TileCollection.cs diff --git a/MapControl/Shared/BingMapsTileSource.cs b/MapControl/Shared/BingMapsTileSource.cs index 02e90490..367f5a49 100644 --- a/MapControl/Shared/BingMapsTileSource.cs +++ b/MapControl/Shared/BingMapsTileSource.cs @@ -8,18 +8,18 @@ namespace MapControl { public class BingMapsTileSource : TileSource { - public override Uri GetUri(int x, int y, int zoomLevel) + public override Uri GetUri(int column, int row, int zoomLevel) { Uri uri = null; if (UriTemplate != null && Subdomains != null && Subdomains.Length > 0 && zoomLevel > 0) { - var subdomain = Subdomains[(x + y) % Subdomains.Length]; + var subdomain = Subdomains[(column + row) % Subdomains.Length]; var quadkey = new char[zoomLevel]; - for (var z = zoomLevel - 1; z >= 0; z--, x /= 2, y /= 2) + for (var z = zoomLevel - 1; z >= 0; z--, column /= 2, row /= 2) { - quadkey[z] = (char)('0' + 2 * (y % 2) + (x % 2)); + quadkey[z] = (char)('0' + 2 * (row % 2) + (column % 2)); } uri = new Uri(UriTemplate diff --git a/MapControl/Shared/BoundingBoxTileSource.cs b/MapControl/Shared/BoundingBoxTileSource.cs index f1287af5..cacf7c78 100644 --- a/MapControl/Shared/BoundingBoxTileSource.cs +++ b/MapControl/Shared/BoundingBoxTileSource.cs @@ -9,17 +9,17 @@ namespace MapControl { public class BoundingBoxTileSource : TileSource { - public override Uri GetUri(int x, int y, int zoomLevel) + public override Uri GetUri(int column, int row, int zoomLevel) { Uri uri = null; if (UriTemplate != null) { var tileSize = 360d / (1 << zoomLevel); // tile width in degrees - var west = MapProjection.Wgs84MeterPerDegree * (x * tileSize - 180d); - var east = MapProjection.Wgs84MeterPerDegree * ((x + 1) * tileSize - 180d); - var south = MapProjection.Wgs84MeterPerDegree * (180d - (y + 1) * tileSize); - var north = MapProjection.Wgs84MeterPerDegree * (180d - y * tileSize); + var west = MapProjection.Wgs84MeterPerDegree * (column * tileSize - 180d); + var east = MapProjection.Wgs84MeterPerDegree * ((column + 1) * tileSize - 180d); + var south = MapProjection.Wgs84MeterPerDegree * (180d - (row + 1) * tileSize); + var north = MapProjection.Wgs84MeterPerDegree * (180d - row * tileSize); if (UriTemplate.Contains("{bbox}")) { diff --git a/MapControl/Shared/MapTileLayer.cs b/MapControl/Shared/MapTileLayer.cs index b90ab83d..3377e175 100644 --- a/MapControl/Shared/MapTileLayer.cs +++ b/MapControl/Shared/MapTileLayer.cs @@ -3,8 +3,6 @@ // Licensed under the Microsoft Public License (Ms-PL) using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; #if WINUI using Windows.Foundation; @@ -62,7 +60,7 @@ namespace MapControl public TileMatrix TileMatrix { get; private set; } - public IReadOnlyCollection Tiles { get; private set; } = new List(); + public TileCollection Tiles { get; private set; } = new TileCollection(); /// /// Minimum zoom level supported by the MapTileLayer. Default value is 0. @@ -110,6 +108,8 @@ namespace MapControl { 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; @@ -138,7 +138,7 @@ namespace MapControl { if (Tiles.Count > 0) { - Tiles = new List(); // clear all + Tiles = new TileCollection(); // clear all } update = true; } @@ -182,7 +182,7 @@ namespace MapControl // var bounds = ParentMap.ViewTransform.GetTileMatrixBounds(tileMatrixScale, MapTopLeft, ParentMap.RenderSize); - // tile column and row index bounds + // tile X and Y bounds // var xMin = (int)Math.Floor(bounds.X / TileSize); var yMin = (int)Math.Floor(bounds.Y / TileSize); @@ -204,7 +204,7 @@ namespace MapControl private void UpdateTiles() { - var tiles = new List(); + var tiles = new TileCollection(); if (TileSource != null && TileMatrix != null) { @@ -218,32 +218,18 @@ namespace MapControl 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; + 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, (1 << z) - 1); + var y2 = Math.Min(TileMatrix.YMax / tileSize, numTiles - 1); for (var y = y1; y <= y2; y++) { for (var x = x1; x <= x2; x++) { - var tile = Tiles.FirstOrDefault(t => t.ZoomLevel == z && t.Y == y && t.X == x); - - if (tile == null) - { - tile = new Tile(z, x, y); - - var equivalentTile = Tiles.FirstOrDefault( - t => t.IsLoaded && t.ZoomLevel == z && t.Y == y && t.XIndex == tile.XIndex); - - if (equivalentTile != null) - { - tile.SetImageSource(equivalentTile.Image.Source, false); - } - } - - tiles.Add(tile); + tiles.Add(Tiles.GetTile(z, x, y, numTiles)); } } } diff --git a/MapControl/Shared/Tile.cs b/MapControl/Shared/Tile.cs index 8c96e845..4241485f 100644 --- a/MapControl/Shared/Tile.cs +++ b/MapControl/Shared/Tile.cs @@ -24,25 +24,19 @@ namespace MapControl { public partial class Tile { - public Tile(int zoomLevel, int x, int y) + public Tile(int zoomLevel, int x, int y, int columnCount) { ZoomLevel = zoomLevel; X = x; Y = y; + Column = ((x % columnCount) + columnCount) % columnCount; } public int ZoomLevel { get; } public int X { get; } public int Y { get; } - - public int XIndex - { - get - { - var numTiles = 1 << ZoomLevel; - return ((X % numTiles) + numTiles) % numTiles; - } - } + public int Column { get; } + public int Row => Y; public Image Image { get; } = new Image { diff --git a/MapControl/Shared/TileCollection.cs b/MapControl/Shared/TileCollection.cs new file mode 100644 index 00000000..c9436b7d --- /dev/null +++ b/MapControl/Shared/TileCollection.cs @@ -0,0 +1,35 @@ +// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control +// © 2022 Clemens Fischer +// Licensed under the Microsoft Public License (Ms-PL) + +using System.Collections.Generic; +using System.Linq; + +namespace MapControl +{ + public class TileCollection : List + { + /// + /// Get a matching Tile from a TileCollection or create a new one. + /// + public Tile GetTile(int zoomLevel, int x, int y, int columnCount) + { + var tile = this.FirstOrDefault(t => t.ZoomLevel == zoomLevel && t.X == x && t.Y == y); + + if (tile == null) + { + tile = new Tile(zoomLevel, x, y, columnCount); + + var equivalentTile = this.FirstOrDefault( + t => t.IsLoaded && t.ZoomLevel == tile.ZoomLevel && t.Column == tile.Column && t.Row == tile.Row); + + if (equivalentTile != null) + { + tile.SetImageSource(equivalentTile.Image.Source, false); // no opacity animation + } + } + + return tile; + } + } +} diff --git a/MapControl/Shared/TileImageLoader.cs b/MapControl/Shared/TileImageLoader.cs index 615ca40d..623bdf10 100644 --- a/MapControl/Shared/TileImageLoader.cs +++ b/MapControl/Shared/TileImageLoader.cs @@ -133,7 +133,7 @@ namespace MapControl } catch (Exception ex) { - Debug.WriteLine($"TileImageLoader: {tile.ZoomLevel}/{tile.XIndex}/{tile.Y}: {ex.Message}"); + Debug.WriteLine($"TileImageLoader: {tile.ZoomLevel}/{tile.Column}/{tile.Row}: {ex.Message}"); } if (Progress != null && !tileQueue.IsCanceled) @@ -149,10 +149,10 @@ namespace MapControl { if (string.IsNullOrEmpty(cacheName)) { - return LoadTile(tile, () => tileSource.LoadImageAsync(tile.XIndex, tile.Y, tile.ZoomLevel)); + return LoadTile(tile, () => tileSource.LoadImageAsync(tile.Column, tile.Row, tile.ZoomLevel)); } - var uri = tileSource.GetUri(tile.XIndex, tile.Y, tile.ZoomLevel); + var uri = tileSource.GetUri(tile.Column, tile.Row, tile.ZoomLevel); if (uri != null) { @@ -164,7 +164,7 @@ namespace MapControl } var cacheKey = string.Format(CultureInfo.InvariantCulture, - "{0}/{1}/{2}/{3}{4}", cacheName, tile.ZoomLevel, tile.XIndex, tile.Y, extension); + "{0}/{1}/{2}/{3}{4}", cacheName, tile.ZoomLevel, tile.Column, tile.Row, extension); return LoadCachedTile(tile, uri, cacheKey); } diff --git a/MapControl/Shared/TileSource.cs b/MapControl/Shared/TileSource.cs index 8608dc56..1ec502e5 100644 --- a/MapControl/Shared/TileSource.cs +++ b/MapControl/Shared/TileSource.cs @@ -49,20 +49,20 @@ namespace MapControl /// /// Gets the image Uri for the specified tile indices and zoom level. /// - public virtual Uri GetUri(int x, int y, int zoomLevel) + public virtual Uri GetUri(int column, int row, int zoomLevel) { Uri uri = null; - if (UriTemplate != null && x >= 0 && y >= 0 && zoomLevel >= 0) + if (UriTemplate != null && column >= 0 && row >= 0 && zoomLevel >= 0) { var uriString = UriTemplate - .Replace("{x}", x.ToString()) - .Replace("{y}", y.ToString()) + .Replace("{x}", column.ToString()) + .Replace("{y}", row.ToString()) .Replace("{z}", zoomLevel.ToString()); if (Subdomains != null && Subdomains.Length > 0) { - uriString = uriString.Replace("{s}", Subdomains[(x + y) % Subdomains.Length]); + uriString = uriString.Replace("{s}", Subdomains[(column + row) % Subdomains.Length]); } uri = new Uri(uriString, UriKind.RelativeOrAbsolute); @@ -72,11 +72,11 @@ namespace MapControl } /// - /// Loads a tile ImageSource asynchronously from GetUri(x, y, zoomLevel). + /// Loads a tile ImageSource asynchronously from GetUri(column, row, zoomLevel). /// - public virtual Task LoadImageAsync(int x, int y, int zoomLevel) + public virtual Task LoadImageAsync(int column, int row, int zoomLevel) { - var uri = GetUri(x, y, zoomLevel); + var uri = GetUri(column, row, zoomLevel); return uri != null ? ImageLoader.LoadImageAsync(uri) : Task.FromResult((ImageSource)null); } @@ -84,9 +84,9 @@ namespace MapControl public class TmsTileSource : TileSource { - public override Uri GetUri(int x, int y, int zoomLevel) + public override Uri GetUri(int column, int row, int zoomLevel) { - return base.GetUri(x, (1 << zoomLevel) - 1 - y, zoomLevel); + return base.GetUri(column, (1 << zoomLevel) - 1 - row, zoomLevel); } } } diff --git a/MapControl/Shared/WmtsTileMatrix.cs b/MapControl/Shared/WmtsTileMatrix.cs index 3e50d100..d545477a 100644 --- a/MapControl/Shared/WmtsTileMatrix.cs +++ b/MapControl/Shared/WmtsTileMatrix.cs @@ -10,11 +10,14 @@ namespace MapControl { public class WmtsTileMatrix { + // See 07-057r7_Web_Map_Tile_Service_Standard.pdf, section 6.1.a, page 8: + // "standardized rendering pixel size" is 0.28 mm + public WmtsTileMatrix(string identifier, double scaleDenominator, Point topLeft, int tileWidth, int tileHeight, int matrixWidth, int matrixHeight) { Identifier = identifier; - Scale = 1 / (scaleDenominator * 0.00028); + Scale = 1 / (scaleDenominator * 0.00028); // 0.28 mm TopLeft = topLeft; TileWidth = tileWidth; TileHeight = tileHeight; diff --git a/MapControl/Shared/WmtsTileMatrixLayer.cs b/MapControl/Shared/WmtsTileMatrixLayer.cs index b7a23c2c..6e19599a 100644 --- a/MapControl/Shared/WmtsTileMatrixLayer.cs +++ b/MapControl/Shared/WmtsTileMatrixLayer.cs @@ -3,8 +3,6 @@ // Licensed under the Microsoft Public License (Ms-PL) using System; -using System.Collections.Generic; -using System.Linq; #if WINUI using Windows.Foundation; using Microsoft.UI.Xaml.Controls; @@ -39,7 +37,7 @@ namespace MapControl public int XMax { get; private set; } public int YMax { get; private set; } - public IReadOnlyCollection Tiles { get; private set; } = new List(); + public TileCollection Tiles { get; private set; } = new TileCollection(); public void SetRenderTransform(ViewTransform viewTransform) { @@ -57,16 +55,26 @@ namespace MapControl // var bounds = viewTransform.GetTileMatrixBounds(TileMatrix.Scale, TileMatrix.TopLeft, viewSize); - // tile column and row index bounds + // tile X and Y bounds // var xMin = (int)Math.Floor(bounds.X / TileMatrix.TileWidth); var yMin = (int)Math.Floor(bounds.Y / TileMatrix.TileHeight); var xMax = (int)Math.Floor((bounds.X + bounds.Width) / TileMatrix.TileWidth); var yMax = (int)Math.Floor((bounds.Y + bounds.Height) / TileMatrix.TileHeight); - xMin = Math.Max(xMin, 0); + // total tile matrix width in meters + // + var totalWidth = TileMatrix.MatrixWidth * TileMatrix.TileWidth / TileMatrix.Scale; + + if (Math.Abs(totalWidth - 360d * MapProjection.Wgs84MeterPerDegree) > 1d) + { + // no full longitudinal coverage, restrict x index + // + xMin = Math.Max(xMin, 0); + xMax = Math.Min(Math.Max(xMax, 0), TileMatrix.MatrixWidth - 1); + } + yMin = Math.Max(yMin, 0); - xMax = Math.Min(Math.Max(xMax, 0), TileMatrix.MatrixWidth - 1); yMax = Math.Min(Math.Max(yMax, 0), TileMatrix.MatrixHeight - 1); if (XMin == xMin && YMin == yMin && XMax == xMax && YMax == yMax) @@ -84,17 +92,17 @@ namespace MapControl public void UpdateTiles() { - var newTiles = new List(); + var tiles = new TileCollection(); for (var y = YMin; y <= YMax; y++) { for (var x = XMin; x <= XMax; x++) { - newTiles.Add(Tiles.FirstOrDefault(t => t.X == x && t.Y == y) ?? new Tile(ZoomLevel, x, y)); + tiles.Add(Tiles.GetTile(ZoomLevel, x, y, TileMatrix.MatrixWidth)); } } - Tiles = newTiles; + Tiles = tiles; Children.Clear(); diff --git a/MapControl/Shared/WmtsTileSource.cs b/MapControl/Shared/WmtsTileSource.cs index a5a3328e..4232d993 100644 --- a/MapControl/Shared/WmtsTileSource.cs +++ b/MapControl/Shared/WmtsTileSource.cs @@ -10,19 +10,19 @@ namespace MapControl { public WmtsTileMatrixSet TileMatrixSet { get; set; } - public override Uri GetUri(int x, int y, int zoomLevel) + public override Uri GetUri(int column, int row, int zoomLevel) { Uri uri = null; if (UriTemplate != null && TileMatrixSet != null && TileMatrixSet.TileMatrixes.Count > zoomLevel && - x >= 0 && y >= 0 && zoomLevel >= 0) + column >= 0 && row >= 0 && zoomLevel >= 0) { uri = new Uri(UriTemplate .Replace("{TileMatrixSet}", TileMatrixSet.Identifier) .Replace("{TileMatrix}", TileMatrixSet.TileMatrixes[zoomLevel].Identifier) - .Replace("{TileCol}", x.ToString()) - .Replace("{TileRow}", y.ToString())); + .Replace("{TileCol}", column.ToString()) + .Replace("{TileRow}", row.ToString())); } return uri; diff --git a/MapControl/UWP/MapControl.UWP.csproj b/MapControl/UWP/MapControl.UWP.csproj index 3f4adb7f..43a9783f 100644 --- a/MapControl/UWP/MapControl.UWP.csproj +++ b/MapControl/UWP/MapControl.UWP.csproj @@ -149,6 +149,9 @@ Tile.cs + + TileCollection.cs + TileImageLoader.cs