Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom TileLayer from Stadia maps #2836

Open
shtirlitsDva opened this issue Jan 3, 2025 · 3 comments
Open

Custom TileLayer from Stadia maps #2836

shtirlitsDva opened this issue Jan 3, 2025 · 3 comments

Comments

@shtirlitsDva
Copy link

shtirlitsDva commented Jan 3, 2025

Mapsui Version
Mapsui 4.1.8 5.0.0-beta.6

Mapsui Platform
Windows WPF .NET 8

Device
Windows

I try to add a TileLayer using tiles from Stadia maps:
https://docs.stadiamaps.com/map-styles/alidade-smooth-dark/

The url is one of the next two:
Raster XYZ PNG URL format (up to zoom 20)
https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png

Raster XYZ PNG URL format (up to zoom 20; for OpenLayers, QGIS, MapLibre, and other renderers without retina placeholder support)
https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}@2x.png

I don't know which one works with Mapsui, so I try both. I don't get any map, just white background and no errors.
I've tried replicating how Mapsui is creating OSM map by reading the source code for Mapsui.Tiling.OpenStreetMap.CreateTileLayer(), as I couldn't find a sample that showed this:

private static IPersistentCache<byte[]>? _defaultCache;
private static BruTile.Attribution _stadiaAttribution = new("© Stadia Maps", "https://stadiamaps.com/");
private void CreateMapFirstTime()
{
    if (Mymap == null) return;

    Mymap.Layers.Clear();

    //OSM map <- this works, so disregard next line
    Mymap.Layers.Add(Mapsui.Tiling.OpenStreetMap.CreateTileLayer());

    #region Attempt to add Stadia map tiles -> failed
    // Stadia maps tiles
    string userAgent = 
        $"user-agent-of-{Path.GetFileNameWithoutExtension(
            System.AppDomain.CurrentDomain.FriendlyName)}";

    var httpTileSource = new HttpTileSource(
        new GlobalSphericalMercator(),
        "https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}@2x.png", //or {r} instead of '@2x', I've also tried plain '/{z}/{x}/{y}.png',
        ["a", "b", "c"] <- I've tried with and without, same result
        name: "Stadia Maps",
        attribution: _stadiaAttribution,
        apiKey: "xxx", //<- Unsure if this is necessary. The retina URL works in python without an api key, but I've created an api key to be sure.
        //configureHttpRequestMessage: (r) => r.Headers.TryAddWithoutValidation("User-Agent", userAgent), <- this is from Mapsui source code, but in my version of BruTile the HttpTileSource does not have this argument in the constructor. My version (5.0.6) is just the one I get when installing current Mapsui release.
        persistentCache: _defaultCache
        );

    httpTileSource.AddHeader("User-Agent", userAgent); <- instead of the one in the constructor

    var stadiaLayer = new TileLayer(httpTileSource) { Name = "Stadia" };

    // Add the custom tile layer to the map
    Mymap.Layers.Add(stadiaLayer); 
    #endregion

The code above doesn't work, what am I doing wrong?

I tried creating a BruTile test project using an example on their page:

var tileSource = new HttpTileSource( 
    new GlobalSphericalMercator(0, 18),
    "https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}.png",
    ["a", "b", "c"],
    apiKey: "xxx",
    name: "Stadia");

var extent = new Extent(-20037508, -20037508, 20037508, 20037508);
var screenWidthInPixels = 400; // The width of the map on screen in pixels
var resolution = extent.Width / screenWidthInPixels;
var tileInfos = tileSource.Schema.GetTileInfos(extent, resolution);

Console.WriteLine("Show tile info");
foreach (var tileInfo in tileInfos)
{
    var tile = await tileSource.GetTileAsync(tileInfo);

    Console.WriteLine(
        $"tile col: {tileInfo.Index.Col}, " +
        $"tile row: {tileInfo.Index.Row}, " +
        $"tile level: {tileInfo.Index.Level} , " +
        $"tile size {tile.Length}");
}

but here I get 401 'Unauthorized' error in the 'var tile = await tileSource.GetTileAsync(tileInfo);' line.

So maybe this is what happens behind the scene in Mapsui, but Mapsui eats the exception so I don't get any errors.
And I don't understand why I would be 'Unauthorized' while using an api key. Also I use these tiles in a blazor with leaflet project and it works without any api keys.

What am I doing wrong?

@shtirlitsDva
Copy link
Author

Okay, why do I always find a solution after I've posted a cry for help and after I've banged my head against a wall for several hours?
The immediate solution is to add a header with API key, it doesn't work as an argument in the constructor.
So I've added: tileSource.AddHeader("Stadia-Auth", "api key here");

And the tiles load now...

But why does it work in js leaflet and python without api keys???

@shtirlitsDva
Copy link
Author

Also, is there a way to load retina tiles? the ones where the url has {r}: '/{z}/{x}/{y}{r}.png'?

@pauldendulk
Copy link
Member

But why does it work in js leaflet and python without api keys???

Perhaps because of the user-agent or referer. To investigate this you can inspect those in the webpages that use leaflet and set the same user-agent and/or referer in the BruTile tile source.

Also, is there a way to load retina tiles? the ones where the url has {r}: '/{z}/{x}/{y}{r}.png'?

What is the r here? The resolution of the retina display? If yes, I guess this is the same for all tiles requested from that device. In that case you could detect the r of the device in code somehow and set that value in the url before you assign it to the tile source.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants