ThinkGeo.com    |     Documentation    |     Premium Support

WMS/WFS Feedback / Wishes

Hi Ben, we recently moved WMS and WFS into our GPU workflow and noticed following friction points where we had to write workarounds or guards where we thought that ThinkGeo could natively handle this.
Take this as a wishlist which would make our adoption easier or maybe the implementation even better :smiley:

Bugs / lifetime contracts to fix

  1. Concurrent opening is unsafe despite ThreadSafetyLevel.Safe.
    LayerTileView calls OpenAsync per visible tile. AsyncLayer.OpenAsync checks
    IsOpen, awaits initialization, and then sets IsOpen, without serializing callers.
    AsyncFeatureSource.OpenAsync has the same gap. Both WMS and WFS advertise Safe.
    WMS capabilities parsing can therefore run concurrently against shared state.
    AGK observed IndexOutOfRangeException with the Bayern Festpunkte service after
    moving it into a MultiTile LayerOverlay; serializing initialization fixed the
    reported failure. WFS has the same unguarded entry path by code inspection;
    an equivalent WFS crash has not been reproduced on Windows.

    Request: serialize/coalesce opening in the SDK, including retry after failure and
    cancellation. Keep tile downloads parallel. Test simultaneous opening of one
    reader and document the open/close concurrency contract.

    Reproduction shape: put a WmsAsyncLayer configured for
    https://geoservices.bayern.de/od/wms/afis/v1/festpunkte? into an already-open
    MultiTile LayerOverlay so tiles open it lazily; alternatively issue overlapping
    OpenAsync calls on the same closed reader. Use DrawAndThrowException to see
    the failure rather than only an error tile. Timing affects reproduction.

  2. SDK-created HTTP clients lack deterministic layer cleanup.
    WebBasedAsyncLayer creates an HttpClient, but has no corresponding close/dispose
    cleanup. WFS’s feature source borrows that client, so closing the feature source
    does not release it either. WmsRasterTileSource also creates clients and exposes
    neither Close nor IDisposable/IAsyncDisposable.

    Request: define ownership for supplied clients/handlers versus SDK-created ones,
    provide deterministic disposal, and clean up failed opens. Keeping a client for
    reuse across Close/Open can be valid, provided final disposal exists. AGK currently
    releases owned clients on closure/failed opening through WebLayerLifecycle.

  3. Opening cancellation is swallowed.
    AsyncLayer.OpenAsync returns normally for an already-cancelled token and catches
    OperationCanceledException/TaskCanceledException without rethrowing, leaving a
    reader closed. This can look like successful initialization to callers. It can
    also hide HttpClient timeouts, whose exception derives from TaskCanceledException.

    Request: propagate caller cancellation as a cancelled task; preserve timeout
    failures when the caller’s token is not cancelled. AGK checks cancellation again
    after opening before querying metadata or constructing GPU adapters.

API improvements that would remove AGK workarounds

  1. A configurable, owned WMS GPU source.
    The v15 sample uses WmsRasterTileSource. It supports request headers through
    SendingHttpRequest, parameters, output format, proxy and timeout. However, it
    hides its WmsClient, fixes the CRS to EPSG:3857, and lacks metadata and lifetime
    APIs. AGK needs selected layer/style names, credentials, transparent GetMap,
    GetServerLayers scale advice and deterministic cleanup.

    Request: accept a configured WmsAsyncLayer or WmsClient, with explicit ownership,
    or expose equivalent configuration/metadata and disposal. This would let AGK
    delete its small WmsLayerTileSource adapter. Non-3857 GPU support is a separate
    enhancement; AGK currently falls back to classic for those configurations.

  2. Visible-only, cancellable LayerOverlay opening.
    LayerOverlay.OpenAsyncCore currently opens every layer, including hidden remote
    services, without a draw cancellation token. A hidden unavailable service can
    therefore delay or fail map initialization.

    Request: support deferred/visible-only opening with cancellation, while retaining
    wrapping setup. AGK overrides that initialization in StaticLayerOverlay.

  3. Metadata-only async feature translation.
    FeatureLayerTranslator obtains geometry information through a synchronous source
    adapter. If that adapter was closed, it opens it and closes it afterward. AGK
    pre-opens the adapter to retain the independent WFS reader across restyles.

    Request: allow supplying geometry metadata, or provide async translation with a
    clear borrowed-source contract, so style changes require no service opening or
    closure. This is an API improvement, not a claim that temporary-open cleanup is
    intrinsically wrong.

Thanks!
J

Hi Julian,

All the wishes come true in 15.0.0-beta174. :grin:

  1. Concurrent opening. AsyncLayer and AsyncFeatureSource now take a gate per instance and re-check IsOpen inside it. Tile downloads stay parallel. A failed open still leaves the reader closed, so the next caller retries.

  2. HttpClient cleanup. A layer that built its own client disposes it on close and drops the reference, so a later open builds a fresh one. A client you supply is never touched - it may be an IHttpClientFactory singleton shared with the rest of your app. WmsRasterTileSource, WmtsRasterTileSource and ElevationRasterTileSource are IDisposable now, but you dispose them, not us: a source survives a style swap by design and can sit in two styles at once, so nothing in here can tell when you are finished with one.

  3. Opening cancellation. This swallowed more than you said. HttpClient reports a timeout as TaskCanceledException, so a service that never answered was indistinguishable from a draw you cancelled. Only a cancel you asked for is quiet now; a timeout takes the ordinary failure path.

  4. Configurable WMS source. A constructor takes a configured WmsClient, and a Client
    property reaches it. Layers, styles, transparency, format, proxy and timeout were all typed
    there already, and you can ask it what the server publishes. Three things were simply broken:
    Credentials and CapabilitiesCacheTimeout were settable and read nowhere, and the zoom range was
    hard-coded 0 to 19 rather than taken from your layers’ scale range. The CRS stays 3857, now
    verified rather than assumed.

  5. LayerOverlay opening. Visible layers only, with the draw token reaching the layers. Two more came with it: one layer that fails to open no longer strands the ones behind it, and the wrapping settings still reach every layer including hidden ones, so a layer you show later is not left without them.

  6. Metadata-only translation. The synchronous view the cutter reads your async source through never reported the inner source’s open state, so the translation thought it had opened your source and closed it again in its finally - your live reader, on every restyle. It reports the inner state now, so a source you have open is left open. The geometry type was free all along; it reads a property, with no request.

Two things to know when you upgrade:

  • Adding a HIDDEN layer to a LayerOverlay no longer opens it. If you relied on that to query a layer you never draw, open it yourself.

  • Overlay.OpenAsyncCore() and CloseAsyncCore() now take a CancellationToken. If you override the ones without the CancellationToken, the compiler will say no

Thanks,
Ben

Thanks Ben, thats looking good! We were able to remove almost all our boilerplate wrapper code :smiley:
Two quick follow ups and a bug we found:

ThinkGeo WMS/WFS follow-ups

Three remaining issues identified by inspecting the 15.0.0-beta174 assemblies.
The cancellation issue below also matches an observed exception stack; the other
runtime reproductions remain pending.

1. Failed opening skips HTTP client cleanup

WebBasedAsyncLayer.OpenAsyncCore creates an SDK-owned HttpClient. If a WMS
capabilities or WFS schema request fails, AsyncLayer.OpenAsync leaves IsOpen
false without releasing that client. Public CloseAsync then skips
CloseAsyncCore because the layer is not open.

Reproduction:

  1. Open a WmsAsyncLayer or WfsV2AsyncLayer without supplying an HttpClient.
  2. Make GetCapabilities or DescribeFeatureType fail or time out.
  3. Call CloseAsync after the failed open.
  4. Check that the SDK-owned client remains allocated instead of being disposed
    and cleared.

Please release resources created during a failed or cancelled open, preserve
caller-supplied clients, and allow subsequent opening to retry. Cover both owned
and supplied clients in the regression.

2. WMS GPU source accepts XML service errors as image bytes

WmsRasterTileSource.GetTileImageAsync checks HTTP success, then returns the body
as image bytes. A WMS ServiceExceptionReport returned with HTTP 200 therefore
reaches image decoding; if a TileCache is configured, those bytes can also be cached.
WmsAsyncLayer.DownloadImageAsyncCore already distinguishes images from XML
service errors and raises the service message.

Reproduction: return HTTP 200 with an XML ServiceExceptionReport from GetMap,
then call WmsRasterTileSource.GetTileImageAsync. It returns the XML bytes instead
of reporting the service error.

Please apply the classic reader’s response validation and service-exception
reporting to the GPU source before returning or caching the response.

3. Cancelling WMS capabilities throws NullReferenceException

WebBasedAsyncLayer.SendWebRequestAsync catches OperationCanceledException when
the caller’s token is cancelled and returns null. The WMS capabilities loader
then calls EnsureSuccessStatusCode() on that null response without checking it.
A normal cancellation therefore becomes NullReferenceException and misses the
quiet cancellation handling in AsyncLayer.OpenAsync.

Observed with the Bayern Festpunkte service:
https://geoservices.bayern.de/od/wms/afis/v1/festpunkte?
The stack shows HttpClient cancellation, followed by WebBasedAsyncLayer.b,
WmsAsyncLayer.f, WmsAsyncLayer.H and WmsAsyncLayer.g.

Reproduction: disable the capabilities cache, start WmsAsyncLayer.OpenAsync(token)
and cancel the token while GetCapabilities is in flight. For a deterministic test,
use an HttpMessageHandler that waits for cancellation before returning a response.

Please propagate cancellation through the capabilities loader without dereferencing
a null response. Leave the layer closed and retryable, and continue reporting actual
HTTP timeouts as failures when the caller’s token was not cancelled. Also handle
requests cancelled through SendingHttpRequest.Cancel, which likewise return null.


Additionally, and I don’t know if that is feasable to implement for GPU render or if we should instead switch to use Web Mercator (but all our coordinates are EPSG 25832):

4. API request: native projected coordinates throughout GPU rendering

Can the GPU map use EPSG:25832 as its working coordinate system throughout,
including vector/raster sources, WMS requests, camera extents and overlays?
The requirement is to keep coordinates in the projected CRS, without converting
source data and map interaction coordinates to EPSG:3857.

The beta174 APIs inspected so far retain Web Mercator assumptions:

  • WmsRasterTileSource(WmsClient) rejects CRS values other than EPSG:3857 and
    aliases 900913, 102100 and 102113.
  • FeatureSourceVectorTileCutter constructs a fixed Web Mercator tile grid.
    Its extent validation explicitly requires sources to deliver EPSG:3857 metres;
    the cutter does not reproject them.
  • ClassicRasterTileSource also uses a fixed world square and XYZ tile arithmetic.
  • DisplayProjection.FromEpsg(25832) changes the display projection, but
    ToDisplay(PointShape webMercatorPoint) takes Web Mercator coordinates.
    The supplied AnyProjectionRaster sample retains the original map extent when
    changing display projection; it does not change the source/camera coordinate CRS.

Please confirm whether a supported native projected-coordinate path exists. If
not, please expose a map/source CRS and compatible tile-grid configuration across
these components. Changing only the WMS CRS check or only DisplayProjection does
not provide this contract.

Hi Julian,

The first three items are fixed in beta175:

  1. Failed opening skipped the cleanup. Fixed. Now it releases on failure too. A client you supplied is untouched, and opening again afterwards works.

  2. XML service errors accepted as image bytes. Fixed. It is reported now with the server’s own message.

  3. Cancelling capabilities threw NullReferenceException. Fixed.

Also: WmsClient used to default to EPSG:4326 while every tile grid here is web mercator. It defaults to 3857 now.

  1. Native projected coordinates. There’s no supported path today. The renderer works in web mercator, and DisplayProjection changes what you see rather than what it works in. We are still working on it and will keep you posted.

Thanks,
Ben

Hi Ben, just FYI:

WmsClient still defaults to EPSG:4326 in the latest beta

Thanks for the fixes!

Hi Julian,

The default value is fixed in beta176 — thanks for catching it. Both new WmsClient(uri) and new WmsRasterTileSource(uri) ask for EPSG:3857 now; an explicit CRS still does what you pass.

A server that cannot give you 3857. If it can, ask it in 3857 and nothing else is needed. If it cannot, it used to be a dead end — that is what “the CRS stays 3857” at beta174 meant. Now: leave WmsClient.Crs on the server’s CRS and set source.ProjectionConverter = new GdalProjectionConverter(thatSrid, 3857). The source asks for the ground each mercator tile covers, in the server’s CRS, and warps the answer into the tile. Use the GDAL converter — the managed one throws for an image.

Native projected coordinates. Last time I said there was no supported path and that we would keep you posted. There is one in this beta: the map itself works in your CRS — tiles cut on your grid, drawn and labelled there, no Mercator anywhere. A Mercator raster is then the foreign one, and it is warped onto your grid:

var streets = new ShapeFileFeatureSource(@"D:\data\streets_2056.shp");

var map = new FeatureSourceVectorTileSource(streets.GetBoundingBox(), GeographyUnit.Meter);
map.FeatureSources.Add("streets", streets);

var aerial = new ThinkGeoRasterTileSource(apiKey, ThinkGeoRasterMapType.Aerial);
aerial.ReprojectOnto(new GdalProjectionConverter(3857, 2056), map.TileMatrixSet);

A quadtree root has to be square, so a bounding box is grown into the smallest square that covers it — your data is inside either way. The one shape that cannot work is the conventional 2:1 EPSG:4326 ladder: a square degree grid draws, but it is your own grid, not the one an existing 4326 service is addressed on.

When you upgrade: IRasterTileSource is gone. Your WmsLayerTileSource adapter becomes : RasterTileSource with a base constructor call, and GetTileImageAsync becomes protected override GetTileImageCoreAsync; MinDataZoom / MaxDataZoom / Refreshed / Refresh come from the base now.

Thanks,
Ben

Thanks Ben! Reset coming in clutch!

We tried implementing the new native projected coordinates. However, that didn’t really work for us:

WMS grid configuration needs a public entry point

WmsRasterTileSource exposes only these constructors:

WmsRasterTileSource(Uri uri)
WmsRasterTileSource(WmsClient client)

Both construct the default Web Mercator grid. TileMatrixSet has no public setter.
Calling ReprojectOnto(converter, nativeGrid) throws before any network request:

WmsRasterTileSource publishes on the grid it was built with and fetches the ground each tile needs, so it has no tiles of its own to move onto a second grid. Build it on the grid you want and set ProjectionConverter to say what the server answers in.

The inherited NativeTileMatrixSet getter is protected and virtual, so a subclass
can supply a grid. Please confirm this is the intended customization point, and
consider a constructor accepting a configured WmsClient and TileMatrixSet so
WMS can share a vector source’s native grid without an adapter.

For example, with both the map and WMS in EPSG:25832, the source should request
GetMap bounds directly on that grid. If their CRSs differ, a GDAL converter should
warp between the server CRS and the map CRS.

Thanks! J