ThinkGeo.com    |     Documentation    |     Premium Support

WMS and zoom levels

Hi,

Would it be possible to use WMS online maps so that after certain zoom level e.g. 17 no new images would be fetched from server and instead images from previous level would be used ?

Kind of how WMTS works when server does not provide any new tiles based on TileMatrix when zooming in the map (requests are not sent and previous images are used on high levels)

Asking because it seems that most of WMS servers does not provide any good quality images on high zoom levels and this would help to decrease the amount of requests and costs in case of paid service.

Br, Simo

Hi Simon,

WMS requests do not contain a zoom level. A WMS GetMap request is based on BBOX, WIDTH, and HEIGHT, so WMS does not have the same “maximum zoom level” or “fallback to the previous zoom level” concept that WMTS/XYZ tile services have. When you zoom in, the client sends a new BBOX + WIDTH + HEIGHT request. There is no defined “zoom 17” request, and the WMS server cannot automatically return “zoom 16” imagery instead.

I’m also considering whether a MaximumRequestScale would make sense, but it may be confusing because the horizontal and vertical request resolutions can be different (BBOX.Width / WIDTH vs. BBOX.Height / HEIGHT), especially when the requested image is stretched, so there is no single unambiguous “request scale” to compare against. We could add it, but it would introduce more confusion.

As a result, I think it would be hard to add this property to WMS. You can either add some restrictions on the client side (MapView.MinScale, or customized MapView.Zooms), or use an XYZ/WMTS service with zoom levels available on the server side.

I hope this makes sense. Let me know what you think.

Thanks,
Ben

Hi,

Yes, I know that WMS works differently compared to WMTS that’s why I was asking “Kind of how WMTS works” :slight_smile:

We have had to add 3 extra zoom scales to map to give better resolution to see things on the map which are close to each other. We also have implemented possibility to download map to cache and downloading is the place now where this issue popped up. Normal case when user zooms to these extra levels it is not that big problem (amount of request/data is reasonable) although it feels kind of waste since images are just part of previous ones.

But with cache download e.g if download is done for 3km*3km area it could be that level 20 uses 1764 tiles / 17,64MB but level 23 uses 112896 tiles / 1,3GB. So amount of requests/data is so big that it is not possible to do the cache download to highest levels => map would not be available on our extra zoom levels without network connection.

Do you have any ideas how we could solve this kind of problem ?

Br, Simo

Hi Simo,

I see, so it’s not specific about WMS, but more about caching.

Please check this HowDoI sample:

  1. It load all the tiles from local cache
  2. it only has tiles for zoom 0-5, with “Render Beyond Max Zoom” enabled, it can render beyond zoom5.

Let me know if it doesn’t work for you.

Thanks,
Ben

Hi,

Well, that’s basically the exact functionality what is needed (set zoom level from which onwards tiles from that level are up-sampled) but how to use it with WMS since WmsAsyncLayer does not support MaxZoomOfTheData/RenderBeyondMaxZoom ? (Ideally it would be available for all other web based layers e.g. WFS/OGC Api but at this point WMS is the problematic one)

Also when testing it with WmtsAsyncLayer I noticed that even if those values are set and zoom levels after MaxZoomOfTheData are up-sampling tiles from previous level layer still sends unnecessary request to server on the background causing exceptions. So I would except that no requests are sent beyond zoom level set with MaxZoomOfTheData ?

Br, Simo

Hi Simo,

WmsAsyncLayer does not have MaxZoomOfTheData/RenderBeyondMaxZoom because WMS itself does not define a tile matrix or zoom level hierarchy. A WMS GetMap request simply asks for an image. Even if the client renders the map as multiple tiled WMS requests, each tile is still just an independent WMS GetMap image, and there is no standard “parent tile” or “previous zoom level” that the WMS layer can fall back to.

If your application is using tiled WMS rendering, the practical approach would be to handle this on the client side: cache those WMS-rendered tile images in memory or a tile cache, then reuse/up-sample the cached images with a custom layer or the demo pattern I shared. But adding MaxZoomOfTheData/RenderBeyondMaxZoom directly to WmsAsyncLayer would be confusing because those properties rely on tile matrix zoom semantics that WMS does not have.

For the WMTS issue you mentioned, we confirmed that when RenderBeyondMaxZoom was enabled, the layer could render the over-zoomed image from the max zoom level, but it could still bypass the tile cache and send an unnecessary request for the max-zoom parent tile in the background. This could cause server exceptions even though the visual result was already being up-sampled. We have fixed this in the latest version(v15.0.0-beta066) so the layer checks the cache first and avoids unnecessary source requests when the parent tile is already available.

Thanks,
Ben

Hi,

I misunderstood your point for sharing that demo. Thanks for clearing it out. It was actually quite easy to use existing tile cache from WMS layer and use custom layer on high zoom levels to reuse those tiles.

Also thanks for the fix for WMTS.

Br, Simo

Hi,

While testing WMTS with cache I noticed one issue. It seems that if cache is used and network is disconnected while application is running everything works as expected. But if the application is re-started without network connection WmtsLayer is not occasionally able to use existing cache.

Seems that atleast WmtsAsyncLayer OpenAsyncCore() or GetBoundingBoxCore() can throw an exception “The given key was not present in the dictionary.”. When this happens TileMatrixSetName is set but TileMatrixSets are not causing this exception when accessing TileMatrixSets with name.

Could you please check that issue ?

Br, Simo

hi @Simo_Karjalainen,

Thanks for reporting this problem.

I’ve fixed it already. Could you verify it when the new package(ThinkGeo.Core 15.0.0-beta067) is available?

Regards,
Leo

Hi,

Sure, I’ll check it when new beta is available.

Is there anyway how to apply that fix top of 14.4.5 (or 14.5.2) as a workaround to avoid that issue ?

Br, Simo

hi @Simo_Karjalainen,

I’m afraid not. Because the behavior I changed is inside private members of WmtsAsyncLayer rather than protected ones.

I’ll let you know once the new package is available.

Regards,
Leo

hi @Simo_Karjalainen,

The new package(ThinkGeo.Core 15.0.0-beta067) is available now, feel free to give it a try.

Regards,
Leo

Hi,

Tested 15.0.0-beta067 and with that cache is now used correctly when application is started without network connection.

Br, Simo

That’s great. Let me know if you have other questions.

Regards,
Leo

Hi,

I did make following setup based on that “Render Beyond Max Zoom”:

LayerOverlay with two layers:

  1. WmsAsyncLayer
  2. Custom RasterXyzTileAsyncLayer layer
    • Uses cache created by 1. layer with overridden GetTileAsyncCore()
    • MaxZoomOfTheData = 20 and RenderBeyondMaxZoom = true

By default 1. is visible and 2. is not visible. When entering to zoom level above 20 visibilities are changed so that 1. is not visible and 2. is visible. This way everything works fine (2. shows images from cache on high levels without need to getting those from server) but problem happens when map is zoomed quickly e.g from level 19 -> 21. Then 1. does not generate cache since drawing on level 20 is not done (cancelled) causing that 2. cannot find images from level 20 cache.

Do you have any ideas how to overcome this issue ? Best solution would be that 2. would be used on levels 20 above if cache is available but if not 1. could try to get image from server.

Br, Simo

Hi Simo,

I’m thinking you only need one custom RasterXyzTileAsyncLayer. In this layer

  1. Set MaxZoomOfTheData = 20; RenderBeyondMaxZoom=true;
  2. Set the TileCache.
  3. Send WMS request in GetTileAsyncCore
public class WmsCappedTileLayer : RasterXyzTileAsyncLayer 
  {
    private readonly WmsAsyncLayer _wms;

     public WmsCappedTileLayer(WmsAsyncLayer wms)
        : base(256, GeographyUnit.Meter, MaxExtents.SphericalMercator)
    {
        _wms = wms;                       // its Crs must match this layers tile matrix (see note below)
        MaxZoomOfTheData    = 20;         // never request the server above zoom 20
        RenderBeyondMaxZoom = true;       // up-sample the level-20 tile for 21+
        TileCache = new FileRasterTileCache(/* your existing cache folder */);
    }

    // Called only on a cache miss. For zoom > 20, z is always 20 (the up-sample base tile).
    protected override async Task<RasterTile> GetTileAsyncCore(
        int z, long x, long y, float resolutionFactor, CancellationToken ct)
    {
        // tile (z/x/y) -> world BBOX (row = y, column = x)
        var ext = TileMatrixSet.TileMatrices[z].GetCell(y, x).BoundingBox;

        if (!_wms.IsOpen)
            await _wms.OpenAsync(ct);

        // BBOX + WIDTH + HEIGHT -> a standard WMS GetMap
        var bytes = await _wms.DownloadImageAsync(
            TileMatrixSet.TileWidth, TileMatrixSet.TileHeight, 1, ext, MapUnit, ct);

        if (bytes == null || ct.IsCancellationRequested)
            return null;

        return new RasterTile(bytes, z, x, y);
    }  
}  

This class kind of converting the WMS server to a XYZ service, while only sending zoom <=20 requests. One note — CRS: DownloadImageAsync uses your extent as the BBOX directly (no reprojection), so the tile matrix and _wms.Crs must match.

Let me know if it works.

Thanks,
Ben

Hi,

Based on this latest example I ended up to solution where I kept my original solution with two layers on one overlay (it suites better to our application/easier to adapt) but I modified GetTileAsyncCore() (which was made based on “Render Beyond Max Zoom”) so that if tile does not exist it uses actual wsm layer to download tile first.

It looks like this:

>  protected override async Task<RasterTile> GetTileAsyncCore(int zoomLevel, long x, long y, float resolutionFactor, CancellationToken cancellationToken)
>  {
>      string path = $"{_cacheRoot}\\{zoomLevel}\\{x}\\{y}.png";
> 
>      // If tile exist in cache -> use it
>      if (File.Exists(path))
>      {
>          byte[] bytes = File.ReadAllBytes(path);
>          return new RasterTile(bytes, zoomLevel, x, y);
>      }
> 
>      // Tile does not exist -> use wms layer to download it first
>      RectangleShape ext = TileMatrixSet.TileMatrices[zoomLevel].GetCell(y, x).BoundingBox;
> 
>      if (!_wms.IsOpen)
>          await _wms.OpenAsync(cancellationToken);
> 
>      // BBOX + WIDTH + HEIGHT -> a standard WMS GetMap
>      byte[] tileBytes = await _wms.DownloadImageAsync(
>          TileMatrixSet.TileMatrices[zoomLevel].TileWidth,
>          TileMatrixSet.TileMatrices[zoomLevel].TileHeight,
>          1,
>          ext,
>          TileMatrixSet.TileMatrices[zoomLevel].GeographyUnit,
>          cancellationToken);
> 
>      if (tileBytes == null || cancellationToken.IsCancellationRequested)
>          return null;
> 
>      return new RasterTile(tileBytes, zoomLevel, x, y);
>  }

It seems to be working now also on those cases where map is zoomed quickly and levels above 20 are entered without cache for level 20. In that case wms layer is used for downloading the tile first. I guess that this is the same idea but used in a different way (use cache on high levels but if it does not exist download tile first) ?

Could you please clarify what does this “the tile matrix and _wms.Crs must match” exactly mean and how to verify that ? I’m not that familiar with all aspects related to maps so you need to spell it out for me. :slight_smile:

Br, Simo

Hi Simo,

Yes, that is the same general idea, just adapted to your two-layer setup. If the tile is already in cache, your custom layer uses it; if it is missing, it uses the WMS layer to download that tile first. That should also handle the quick zoom case where level 20 was skipped/cancelled and the cache was not created yet.

For the CRS note: TileMatrixSet.TileMatrices[zoomLevel].GetCell(y, x).BoundingBox returns the BBOX in the coordinate system of that tile matrix. DownloadImageAsync sends that BBOX directly to the WMS server, so _wms.Crs must use the same coordinate system. For example, if the tile matrix is Web Mercator/EPSG:3857, the WMS request should also use EPSG:3857. You can verify it by checking the WMS capabilities for supported CRS/SRS values and making sure it matches the tile matrix you are using.

Thanks,
Ben

Hi,

Yes, in my implementation _wms.Crs already contains the used coordinate system got from capabilities.xml. So my custom RasterXyzTileAsyncLayer knows the correct Crs.

Question is that on my custom RasterXyzTileAsyncLayer how do I set it’s TileMatrix to match that CRS ? Does RasterXyzTileAsyncLayer use some specific TileMatrix by default and can it be changed to match CRS e.g on my custom layers constructor ? Or how the TileMatrix is created for RasterXyzTileAsyncLayer initially ?

Br, Simo

Hi Simo,

WMS itself does not have a tile matrix, so there is no server-side matrix that your custom RasterXyzTileAsyncLayer needs to match.

In this case, the TileMatrixSet is only the client-side tiling grid. It controls how the custom layer converts z/x/y into a BBOX, and also how the local tile cache is organized. So the TileMatrixSet does not need to match any matrix on the WMS server, but the BBOX generated by the TileMatrixSet must be in the same coordinate system as _wms.Crs. The WMS server will interpret the BBOX values using the CRS in the GetMap request.

By default, RasterXyzTileAsyncLayer uses 256 x 256 tiles, GeographyUnit.Meter, and the standard Web Mercator/Spherical Mercator max extent. So if your WMS CRS is EPSG:3857, the default tile matrix is correct.

If your WMS CRS is another projected CRS using meters, the default tile matrix may still work in many cases because it is still a meter-based client-side grid. However, it may not be the best or cleanest grid for that CRS, and the local cache z/x/y values may look less natural. If the CRS extent is outside the default Web Mercator extent, or if you want the cache/grid to align better with that CRS, you should create a custom TileMatrixSet with a bounding box in that CRS.

To set a custom TileMatrixSet, the key is to use a bounding box and GeographyUnit that match the coordinates used by _wms.Crs. For example, EPSG:4326 would normally use GeographyUnit.DecimalDegree and a decimal-degree extent such as MaxExtents.DecimalDegree. A projected CRS in meters would normally use GeographyUnit.Meter with an extent in that projected coordinate system.

Thanks,
Ben