diff --git a/MapControl/Shared/MapBase.MapLayer.cs b/MapControl/Shared/MapBase.MapLayer.cs index 9baf933d..8922f974 100644 --- a/MapControl/Shared/MapBase.MapLayer.cs +++ b/MapControl/Shared/MapBase.MapLayer.cs @@ -22,6 +22,7 @@ namespace MapControl { Brush MapBackground { get; } Brush MapForeground { get; } + IReadOnlyCollection SupportedMapProjections { get; } } public partial class MapBase diff --git a/MapControl/Shared/MapImageLayer.cs b/MapControl/Shared/MapImageLayer.cs index 5b7454a9..e46e3624 100644 --- a/MapControl/Shared/MapImageLayer.cs +++ b/MapControl/Shared/MapImageLayer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; #if WPF @@ -126,6 +127,8 @@ namespace MapControl /// public double LoadingProgress => (double)GetValue(LoadingProgressProperty); + public abstract IReadOnlyCollection SupportedMapProjections { get; } + protected override void SetParentMap(MapBase map) { if (map != null) @@ -183,7 +186,11 @@ namespace MapControl ImageSource image = null; BoundingBox boundingBox = null; - if (ParentMap != null && ParentMap.ActualWidth > 0d && ParentMap.ActualHeight > 0d) + if (ParentMap != null && + ParentMap.ActualWidth > 0d && + ParentMap.ActualHeight > 0d && + SupportedMapProjections != null && + SupportedMapProjections.Contains(ParentMap.MapProjection.CrsId)) { var width = ParentMap.ActualWidth * RelativeImageSize; var height = ParentMap.ActualHeight * RelativeImageSize; diff --git a/MapControl/Shared/MapTileLayer.cs b/MapControl/Shared/MapTileLayer.cs index d79bc0d2..4934b542 100644 --- a/MapControl/Shared/MapTileLayer.cs +++ b/MapControl/Shared/MapTileLayer.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Linq; + #if WPF using System.Windows; using System.Windows.Media; @@ -33,22 +36,24 @@ namespace MapControl private const int TileSize = 256; - private static readonly Point MapTopLeft = new Point( - -180d * MapProjection.Wgs84MeterPerDegree, 180d * MapProjection.Wgs84MeterPerDegree); + private static readonly Point MapTopLeft = new(-180d * MapProjection.Wgs84MeterPerDegree, + 180d * MapProjection.Wgs84MeterPerDegree); /// /// A default MapTileLayer using OpenStreetMap data. /// - public static MapTileLayer OpenStreetMapTileLayer => new MapTileLayer + public static MapTileLayer OpenStreetMapTileLayer => new() { TileSource = new TileSource { UriTemplate = "https://tile.openstreetmap.org/{z}/{x}/{y}.png" }, SourceName = "OpenStreetMap", Description = "© [OpenStreetMap Contributors](http://www.openstreetmap.org/copyright)" }; + public override IReadOnlyCollection SupportedMapProjections { get; } = ["EPSG:3857"]; + public TileMatrix TileMatrix { get; private set; } - public TileCollection Tiles { get; private set; } = new TileCollection(); + public TileCollection Tiles { get; private set; } = []; /// /// Minimum zoom level supported by the MapTileLayer. Default value is 0. @@ -113,7 +118,7 @@ namespace MapControl protected override void UpdateTileLayerAsync(bool resetTiles) { - if (ParentMap == null || ParentMap.MapProjection.Type != MapProjectionType.WebMercator) + if (ParentMap == null || !SupportedMapProjections.Contains(ParentMap.MapProjection.CrsId)) { TileMatrix = null; Children.Clear(); diff --git a/MapControl/Shared/MapTileLayerBase.cs b/MapControl/Shared/MapTileLayerBase.cs index 1f975336..12a47acb 100644 --- a/MapControl/Shared/MapTileLayerBase.cs +++ b/MapControl/Shared/MapTileLayerBase.cs @@ -189,6 +189,8 @@ namespace MapControl } } + public abstract IReadOnlyCollection SupportedMapProjections { get; } + protected bool IsBaseMapLayer => parentMap != null && parentMap.Children.Count > 0 && parentMap.Children[0] == this; protected void BeginLoadTiles(IEnumerable tiles, string cacheName) diff --git a/MapControl/Shared/WmsImageLayer.cs b/MapControl/Shared/WmsImageLayer.cs index 4e962b32..20836ec7 100644 --- a/MapControl/Shared/WmsImageLayer.cs +++ b/MapControl/Shared/WmsImageLayer.cs @@ -16,6 +16,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; #elif AVALONIA using Avalonia; +using Avalonia.Interactivity; #endif namespace MapControl @@ -26,27 +27,24 @@ namespace MapControl public partial class WmsImageLayer : MapImageLayer { private static ILogger logger; - private static ILogger Logger => logger ??= ImageLoader.LoggerFactory?.CreateLogger(); + private static ILogger Logger => logger ??= ImageLoader.LoggerFactory?.CreateLogger(typeof(WmsImageLayer)); public static readonly DependencyProperty ServiceUriProperty = DependencyPropertyHelper.Register(nameof(ServiceUri), null, async (layer, oldValue, newValue) => await layer.UpdateImageAsync()); - public static readonly DependencyProperty WmsStylesProperty = - DependencyPropertyHelper.Register(nameof(WmsStyles), "", + public static readonly DependencyProperty RequestStylesProperty = + DependencyPropertyHelper.Register(nameof(RequestStyles), "", async (layer, oldValue, newValue) => await layer.UpdateImageAsync()); - public static readonly DependencyProperty WmsLayersProperty = - DependencyPropertyHelper.Register(nameof(WmsLayers), null, - async (layer, oldValue, newValue) => - { - // Ignore property change from GetImageAsync when WmsLayers was null. - // - if (oldValue != null) - { - await layer.UpdateImageAsync(); - } - }); + public static readonly DependencyProperty RequestLayersProperty = + DependencyPropertyHelper.Register(nameof(RequestLayers), null, + async (layer, oldValue, newValue) => await layer.UpdateImageAsync()); + + public WmsImageLayer() + { + Loaded += OnLoaded; + } /// /// The base request URL. @@ -60,42 +58,30 @@ namespace MapControl /// /// Comma-separated sequence of requested WMS Styles. Default is an empty string. /// - public string WmsStyles + public string RequestStyles { - get => (string)GetValue(WmsStylesProperty); - set => SetValue(WmsStylesProperty, value); + get => (string)GetValue(RequestStylesProperty); + set => SetValue(RequestStylesProperty, value); } /// - /// Comma-separated sequence of WMS Layer names to be displayed. If not set, the first Layer is displayed. + /// Comma-separated sequence of WMS Layer names to be displayed. If not set, the default Layer is displayed. /// - public string WmsLayers + public string RequestLayers { - get => (string)GetValue(WmsLayersProperty); - set => SetValue(WmsLayersProperty, value); + get => (string)GetValue(RequestLayersProperty); + set => SetValue(RequestLayersProperty, value); } /// - /// Gets a list of all layer names returned by a GetCapabilities response. + /// Gets a collection of all Layer names available in a WMS. /// - public async Task> GetLayerNamesAsync() - { - IEnumerable layerNames = null; + public IReadOnlyCollection AvailableLayers { get; private set; } - var capabilities = await GetCapabilitiesAsync(); - - if (capabilities != null) - { - var ns = capabilities.Name.Namespace; - - layerNames = capabilities - .Descendants(ns + "Layer") - .Select(e => e.Element(ns + "Name")?.Value) - .Where(n => !string.IsNullOrEmpty(n)); - } - - return layerNames; - } + /// + /// Gets a collection of all CRSs supported by a WMS. + /// + public override IReadOnlyCollection SupportedMapProjections => mapProjections; /// /// Loads an XElement from the URL returned by GetCapabilitiesRequestUri(). @@ -167,48 +153,37 @@ namespace MapControl try { - if (ServiceUri != null && ParentMap?.MapProjection != null) + if (boundingBox.West >= -180d && boundingBox.East <= 180d || + ParentMap.MapProjection.Type > MapProjectionType.NormalCylindrical) { - if (WmsLayers == null && - ServiceUri.OriginalString.IndexOf("LAYERS=", StringComparison.OrdinalIgnoreCase) < 0) + var uri = GetMapRequestUri(boundingBox); + + if (uri != null) { - // Get first Layer from a GetCapabilities response. - // - WmsLayers = (await GetLayerNamesAsync())?.FirstOrDefault() ?? ""; + image = await ImageLoader.LoadImageAsync(new Uri(uri), progress); } + } + else + { + BoundingBox bbox1, bbox2; - if (boundingBox.West >= -180d && boundingBox.East <= 180d || - ParentMap.MapProjection.Type > MapProjectionType.NormalCylindrical) + if (boundingBox.West < -180d) { - var uri = GetMapRequestUri(boundingBox); - - if (uri != null) - { - image = await ImageLoader.LoadImageAsync(new Uri(uri), progress); - } + bbox1 = new BoundingBox(boundingBox.South, boundingBox.West + 360, boundingBox.North, 180d); + bbox2 = new BoundingBox(boundingBox.South, -180d, boundingBox.North, boundingBox.East); } else { - BoundingBox bbox1, bbox2; + bbox1 = new BoundingBox(boundingBox.South, boundingBox.West, boundingBox.North, 180d); + bbox2 = new BoundingBox(boundingBox.South, -180d, boundingBox.North, boundingBox.East - 360d); + } - if (boundingBox.West < -180d) - { - bbox1 = new BoundingBox(boundingBox.South, boundingBox.West + 360, boundingBox.North, 180d); - bbox2 = new BoundingBox(boundingBox.South, -180d, boundingBox.North, boundingBox.East); - } - else - { - bbox1 = new BoundingBox(boundingBox.South, boundingBox.West, boundingBox.North, 180d); - bbox2 = new BoundingBox(boundingBox.South, -180d, boundingBox.North, boundingBox.East - 360d); - } + var uri1 = GetMapRequestUri(bbox1); + var uri2 = GetMapRequestUri(bbox2); - var uri1 = GetMapRequestUri(bbox1); - var uri2 = GetMapRequestUri(bbox2); - - if (uri1 != null && uri2 != null) - { - image = await ImageLoader.LoadMergedImageAsync(new Uri(uri1), new Uri(uri2), progress); - } + if (uri1 != null && uri2 != null) + { + image = await ImageLoader.LoadMergedImageAsync(new Uri(uri1), new Uri(uri2), progress); } } } @@ -251,8 +226,8 @@ namespace MapControl { "SERVICE", "WMS" }, { "VERSION", "1.3.0" }, { "REQUEST", "GetMap" }, - { "LAYERS", WmsLayers ?? "" }, - { "STYLES", WmsStyles ?? "" }, + { "LAYERS", RequestLayers ?? "" }, + { "STYLES", RequestStyles ?? "" }, { "FORMAT", "image/png" }, { "CRS", GetCrsValue() }, { "BBOX", GetBboxValue(boundingBox, bbox.Value) }, @@ -289,8 +264,8 @@ namespace MapControl { "SERVICE", "WMS" }, { "VERSION", "1.3.0" }, { "REQUEST", "GetFeatureInfo" }, - { "LAYERS", WmsLayers ?? "" }, - { "STYLES", WmsStyles ?? "" }, + { "LAYERS", RequestLayers ?? "" }, + { "STYLES", RequestStyles ?? "" }, { "FORMAT", "image/png" }, { "INFO_FORMAT", format }, { "CRS", GetCrsValue() }, @@ -312,25 +287,25 @@ namespace MapControl protected virtual string GetCrsValue() { var projection = ParentMap.MapProjection; - var crsId = projection.CrsId; + var crs = projection.CrsId; - if (crsId.StartsWith("AUTO2:") || crsId.StartsWith("AUTO:")) + if (crs.StartsWith("AUTO2:") || crs.StartsWith("AUTO:")) { - crsId = string.Format(CultureInfo.InvariantCulture, "{0},1,{1},{2}", crsId, projection.Center.Longitude, projection.Center.Latitude); + crs = string.Format(CultureInfo.InvariantCulture, "{0},1,{1},{2}", crs, projection.Center.Longitude, projection.Center.Latitude); } - return crsId; + return crs; } protected virtual string GetBboxValue(BoundingBox boundingBox, Rect mapBoundingBox) { - var crsId = ParentMap.MapProjection.CrsId; + var crs = ParentMap.MapProjection.CrsId; string format; double x1, y1, x2, y2; - if (crsId == "CRS:84" || crsId == "EPSG:4326") + if (crs == "CRS:84" || crs == "EPSG:4326") { - format = crsId == "CRS:84" ? "{0:F8},{1:F8},{2:F8},{3:F8}" : "{1:F8},{0:F8},{3:F8},{2:F8}"; + format = crs == "CRS:84" ? "{0:F8},{1:F8},{2:F8},{3:F8}" : "{1:F8},{0:F8},{3:F8},{2:F8}"; x1 = boundingBox.West; y1 = boundingBox.South; x2 = boundingBox.East; @@ -366,5 +341,48 @@ namespace MapControl return uri.Replace(" ", "%20"); } + + private List mapProjections; + + private async void OnLoaded(object sender, RoutedEventArgs e) + { + Loaded -= OnLoaded; + + if (AvailableLayers == null && ServiceUri != null) + { + var capabilities = await GetCapabilitiesAsync(); + + if (capabilities != null) + { + var ns = capabilities.Name.Namespace; + var capability = capabilities.Element(ns + "Capability"); + + mapProjections = capability + .Element(ns + "Layer") + .Descendants(ns + "CRS") + .Select(e => e.Value) + .ToList(); + + var layerNames = capability + .Descendants(ns + "Layer") + .Select(e => e.Element(ns + "Name")?.Value) + .Where(n => !string.IsNullOrEmpty(n)) + .ToList(); + + AvailableLayers = layerNames; + + if (layerNames.Count > 0 && + RequestLayers == null && + ServiceUri.OriginalString.IndexOf("LAYERS=", StringComparison.OrdinalIgnoreCase) < 0) + { + RequestLayers = layerNames[0]; + } + else + { + await UpdateImageAsync(); + } + } + } + } } } diff --git a/MapControl/Shared/WmtsTileLayer.cs b/MapControl/Shared/WmtsTileLayer.cs index 8f57aa5b..e1bc2861 100644 --- a/MapControl/Shared/WmtsTileLayer.cs +++ b/MapControl/Shared/WmtsTileLayer.cs @@ -23,7 +23,7 @@ namespace MapControl public partial class WmtsTileLayer : MapTileLayerBase { private static ILogger logger; - private static ILogger Logger => logger ??= ImageLoader.LoggerFactory?.CreateLogger(); + private static ILogger Logger => logger ??= ImageLoader.LoggerFactory?.CreateLogger(typeof(WmtsTileLayer)); public static readonly DependencyProperty CapabilitiesUriProperty = DependencyPropertyHelper.Register(nameof(CapabilitiesUri), null, @@ -68,11 +68,19 @@ namespace MapControl set => SetValue(PreferredTileMatrixSetsProperty, value); } - public IEnumerable ChildLayers => Children.Cast(); - + /// + /// Gets a dictionary of all tile matrix sets supported by a WMTS, with their CRS as dictionary key. + /// public Dictionary TileMatrixSets { get; } = []; - protected virtual WmtsTileSource CreateTileSource(string uriTemplate) => new WmtsTileSource { UriTemplate = uriTemplate }; + /// + /// Gets a collection of all CRSs supported by a WMTS. + /// + public override IReadOnlyCollection SupportedMapProjections => TileMatrixSets.Keys; + + protected IEnumerable ChildLayers => Children.Cast(); + + protected virtual WmtsTileSource CreateTileSource(string uriTemplate) => new() { UriTemplate = uriTemplate }; protected override Size MeasureOverride(Size availableSize) { @@ -192,10 +200,10 @@ namespace MapControl var capabilities = await WmtsCapabilities.ReadCapabilitiesAsync(CapabilitiesUri, Layer); foreach (var tileMatrixSet in capabilities.TileMatrixSets - .Where(s => !TileMatrixSets.ContainsKey(s.SupportedCrs) || + .Where(s => !TileMatrixSets.ContainsKey(s.SupportedMapProjection) || PreferredTileMatrixSets != null && PreferredTileMatrixSets.Contains(s.Identifier))) { - TileMatrixSets[tileMatrixSet.SupportedCrs] = tileMatrixSet; + TileMatrixSets[tileMatrixSet.SupportedMapProjection] = tileMatrixSet; } Layer = capabilities.Layer; diff --git a/MapControl/Shared/WmtsTileMatrixSet.cs b/MapControl/Shared/WmtsTileMatrixSet.cs index 5dd16328..8eb26e2d 100644 --- a/MapControl/Shared/WmtsTileMatrixSet.cs +++ b/MapControl/Shared/WmtsTileMatrixSet.cs @@ -6,16 +6,16 @@ namespace MapControl { public class WmtsTileMatrixSet { - public WmtsTileMatrixSet(string identifier, string supportedCrs, IEnumerable tileMatrixes) + public WmtsTileMatrixSet(string identifier, string supportedMapProjection, IEnumerable tileMatrixes) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException($"The {nameof(identifier)} argument must not be null or empty.", nameof(identifier)); } - if (string.IsNullOrEmpty(supportedCrs)) + if (string.IsNullOrEmpty(supportedMapProjection)) { - throw new ArgumentException($"The {nameof(supportedCrs)} argument must not be null or empty.", nameof(supportedCrs)); + throw new ArgumentException($"The {nameof(supportedMapProjection)} argument must not be null or empty.", nameof(supportedMapProjection)); } if (tileMatrixes == null || !tileMatrixes.Any()) @@ -24,12 +24,12 @@ namespace MapControl } Identifier = identifier; - SupportedCrs = supportedCrs; + SupportedMapProjection = supportedMapProjection; TileMatrixes = tileMatrixes.OrderBy(m => m.Scale).ToList(); } public string Identifier { get; } - public string SupportedCrs { get; } + public string SupportedMapProjection { get; } public IList TileMatrixes { get; } } } diff --git a/SampleApps/ProjectionDemo/MainWindow.xaml.cs b/SampleApps/ProjectionDemo/MainWindow.xaml.cs index 0e7907e1..d8ab71b7 100644 --- a/SampleApps/ProjectionDemo/MainWindow.xaml.cs +++ b/SampleApps/ProjectionDemo/MainWindow.xaml.cs @@ -28,7 +28,7 @@ namespace ProjectionDemo new WmsImageLayer { ServiceUri = new Uri("http://ows.terrestris.de/osm/service"), - WmsLayers = "OSM-WMS" + RequestLayers = "OSM-WMS" }); viewModel.Layers.Add( @@ -36,7 +36,7 @@ namespace ProjectionDemo new WmsImageLayer { ServiceUri = new Uri("https://sgx.geodatenzentrum.de/wms_topplus_open"), - WmsLayers = "web" + RequestLayers = "web" }); viewModel.Layers.Add( @@ -44,7 +44,7 @@ namespace ProjectionDemo new WmsImageLayer { ServiceUri = new Uri("https://sgx.geodatenzentrum.de/wms_basemapde"), - WmsLayers = "de_basemapde_web_raster_farbe" + RequestLayers = "de_basemapde_web_raster_farbe" }); viewModel.Layers.Add( @@ -52,7 +52,7 @@ namespace ProjectionDemo new WmsImageLayer { ServiceUri = new Uri("https://geoportal.wiesbaden.de/cgi-bin/mapserv.fcgi?map=d:/openwimap/umn/map/orthophoto/orthophotos.map"), - WmsLayers = "orthophoto2023" + RequestLayers = "orthophoto2023" }); viewModel.CurrentProjection = viewModel.Projections[0];