BruTile / BruTile

BruTile is a .NET library to access tile services like those of OpenStreetMap, MapBox or GeodanMaps.
Apache License 2.0
327 stars 119 forks source link

Error parsing WMTS from IGN #182

Closed pauldendulk closed 1 year ago

pauldendulk commented 1 year ago

Copied from Mapsui https://github.com/Mapsui/Mapsui/issues/1752


Hello,

I use Mapsui.Forms release 4.0.0-beta.5 in a Xamarin application.

The error comes when I try to parse capabilities from IGN Orthophotos layer, the WmtsParser.Parse method throw this exception :

System.ArgumentException: identifier at BruTile.Wmts.CrsUnitOfMeasureRegistry.get_Item (BruTile.Wmts.CrsIdentifier identifier)

This is the code I use :

var httpClient = new System.Net.Http.HttpClient();
var response = await httpClient.GetStreamAsync("https://wxs.ign.fr/ortho/geoportail/wmts?SERVICE=wmts&VERSION=1.0.0&REQUEST=GetCapabilities");
var tileSources = WmtsParser.Parse(response);
var ortho  = tileSources.First(t => t.Name == "ORTHOIMAGERY.ORTHOPHOTOS");
if (DefaultCache != null)
   ortho.PersistentCache = DefaultCache;
TileLayer Layer = new TileLayer(ortho) { Name = "Photos IGN" };

The same code with an other provider runs well :

var httpClient = new System.Net.Http.HttpClient();
var response = await httpClient.GetStreamAsync("https://geodata.nationaalgeoregister.nl/wmts/top10nl?VERSION=1.0.0&request=GetCapabilities");
var tileSources = WmtsParser.Parse(response);
var nature2000TileSource = tileSources.First(t => t.Name == "natura2000");
if (DefaultCache != null)
   nature2000TileSource.PersistentCache = DefaultCache;
TileLayer Layer = new TileLayer(nature2000TileSource) { Name = nature2000TileSource.Name }; 
pauldendulk commented 1 year ago

Looks like BruTile is expecting a CRS that is not there. What needs to be done is to reproduce this in a unit test. Store the capabilities, load the stream and try to parse it. This should be added in WmtsTests.cs.

gpsaliola commented 1 year ago

I found an error in the file "Client.cs" in the routine "private XmlDocument GetRemoteXml(string url)". You should call the method "r.Read();" after "var r = new XmlTextReader(url, stReader) { XmlResolver = null };" to avoid return "none" in "doc.Load(r);". This is the correct code

    private XmlDocument GetRemoteXml(string url)
    {
        try
        {
            var doc = new XmlDocument { XmlResolver = null };

            using (var task = _getStreamAsync(url))
            {
                using (var stReader = new StreamReader(task.Result))
                {
                    var r = new XmlTextReader(url, stReader) { XmlResolver = null };
                    r.Read();
                    doc.Load(r);
                    task.Result.Close();
                }
            }

            _nsmgr = new XmlNamespaceManager(doc.NameTable);
            return doc;
        }
        catch (Exception ex)
        {
            var message = "Could not download capabilities";
            Logger.Log(LogLevel.Warning, message, ex);
            throw new ApplicationException(message, ex);
        }
    }

However some WMS maps works fine but other no. For example "Spain Orthophoto" works fine (url = "https://www.ign.es/wms-inspire/pnoa-ma", layer = "OI.OrthoimageCoverage"). The WMS map "Italy PCN Cartografia di base IGM 25.000 - Lazio" doesn't work (url = "http://sgi2.isprambiente.it/arcgis/services/raster/igm25k_lazio_wgs/ImageServer/WMSServer", layer = "igm25k_lazio_wgs").

pauldendulk commented 1 year ago

@gpsaliola Are you posting on the correct issue?

pauldendulk commented 1 year ago

Started work on this in this PR: https://github.com/BruTile/BruTile/pull/183.

Avoiding the crash is easy, but there is still a problem. In BruTile UnitsPerPixel needs to be calculated. Unfortunately WMTS is designed in such a way that we need information about the specific projection to calculate the UnitsPerPixel. We have stored this information for a number of projections but the projection used in the xml is not supported in this code.

Not sure what would be the best solution. Some thoughts:

gpsaliola commented 1 year ago

I believe most of the maps are projected in Transverse Mercator.

edfr73 commented 1 year ago

It seems that IGN uses EPSG codes 3857 and 2154 (Lambert 93), maybe Brutile doesn't know 2154 which is typically french.

gpsaliola commented 1 year ago

Yes, EPSG 2154 uses "Lambert Conformal Conic (2SP)" projection. It is typically French, but 90% of the world's maps use Transverse Mercator.

edfr73 commented 1 year ago

So, what about using Lambert 93 in BruTiles ?

edfr73 commented 1 year ago

It is typically French, but 90% of the world's maps use Transverse Mercator.

So, what do you suggest to use french maps with Brutiles, as I don't need 90% of the world maps but the IGN ones ?

pauldendulk commented 1 year ago
  • It would be great if we would need no projection info at all.
  • Add an option for the user to add the missing information
  • Perhaps we need to treat WMTS different from all other tile sources in BruTile.

After looking at the code just now it seems to me that the only way to avoid the needs for the specific projection information would be to treat the parser for WMTS different than the other. In that case BruTile should not try to set the Resolution.UnitsPerPixel. A using library like Mapsui should do those calculations instead. This would be a big change with consequences for Mapsui.

The problem in this case was that the authority was unknown. We fix here is to return metre as default. This is not guaranteed to be correct for all cases but we already do this for unknown EPSG codes.

In this case the authority was 'IGNF' and the identifier 'LAMB93', which is equal to 'EPSG:2154' for which metre is correct, https://epsg.io/2154. So, this fix solves the problem in this specific case.

pauldendulk commented 1 year ago

Fixed in 5.0.3.

edfr73 commented 1 year ago

Thanks for all, I can now use French IGN maps with BruTiles 5.0.3 and Mapsui.Forms 4.0.0-beta.5.