3

I'm trying to create a standalone python script that uses PyQGIS to create a map with a raster layer read from WMS.

My idea was to create a QgsRasterLayer with provider set to wms with the time argument encoded into the URL.

To test, I've just tried adding them in the python console within QGIS:

# default time
url1 = "layers=MCD43C3_M_BSA&styles=rgb&bbox=-180,-90,180,90&height=512&width=512&crs=CRS:84&format=image/png&url=https://neo.gsfc.nasa.gov/wms/wms?version%3D1.3.0"

selected time 2017-01-01

url2 = "layers=MCD43C3_M_BSA&styles=rgb&bbox=-180,-90,180,90&height=512&width=512&crs=CRS:84&format=image/png&time=2017-01-01&url=https://neo.gsfc.nasa.gov/wms/wms?version%3D1.3.0"

iface.addRasterLayer(url1, "default", "wms") iface.addRasterLayer(url2, "selection", "wms")

However, I get the same raster both times, and the developer panel indicates the time parameter is not included in the query:

enter image description here

Is there a simple way I'm missing where I can select a date out of the available time steps from a WMS layer?

Edit:

til_b's excellent answer below has shown that one can use owslib to interact with the WMS server and with that easily select dates from the layer's time axis.

The issue is the intended use:

I want to use this WMS source to create a map using PyQGIS. I could indeed try to obtain the image some other way and then plug in into PyQGIS for mapping, but that seems overly complicated and has some gotchas:

  1. I need to define the image shape beforehand (I suppose that could be calculated from extent, resolution, dpi etc.)
  2. Some servers limit the max image shape to tiles (e.g. 512) so I'd need to get multiple tiles and stitch them together myself
  3. I need to encode the geoinfo myself if I want to map it together with other geospatial data

Doing all of this myself feels a lot like re-inventing the wheel - particularly it's already being handled by PyQGIS with the notable issue that I can't select a date!

I really think there must be a way to do this with PyQGIS since you can clearly do it with the GUI. Either it's a bit more elaborate (maybe using the Temporal Controller perhaps), or I'm just missing the obvious! (or neither and it's a bug ... )

Val
  • 273
  • 2
  • 13
  • 1
    Your example requests aren't complete, they're missing service=WMS& and request=GetMap&. The other odd thing about the developer panel response is use of different CRS, and swapping of bbox – nmtoken Nov 04 '22 at 11:31
  • 1
    @nmtoken maybe QGIS fills the service & request query args? In any case, using these links works just fine for me (besides the missing TIME arg). Regarding CRS: CRS:84 is equivalent to EPSG:4326, maybe something else going on behind the QGIS scenes. The WMS service publishes CRS:84 why I need to use it in the request – Val Nov 04 '22 at 11:37
  • 1
    CRS:84 and EPSG:4326 are not equivalent, CRS is lon/lat axis order and EPSG:4326 is lat/long. I agree that as the service only advertises CRS:84 as supported you should use it as the CRS in the request. The fact that the developer panel reports it used epsg:4326, and you get an image is interesting, because the service will reject such a request, (try https://neo.gsfc.nasa.gov/wms/wms?version=1.3.0&layers=MCD43C3_M_BSA&styles=rgb&bbox=-90,-180,900,180&height=512&width=512&crs=epsg:4326&format=image/png&request=GetMap&service=WMS&) so the panel is not giving correct information – nmtoken Nov 04 '22 at 13:06
  • 1
    thanks @nmtoken, your points RE CRS are appreciated but they don't solve the problem with selecting a specific time slice – Val Nov 04 '22 at 13:12
  • 1
    As you now know the developer panel is giving incorrect information, it would appear you need another way to check what actual requests are sent; whether of not they include the time parameter for example – nmtoken Nov 05 '22 at 12:24
  • The example in the question is actually just a reproducible example. “In reality” I’m using a WMS server where I can monitor the requests coming in, and I can see the time argument is dropped by PyQGIS – Val Nov 05 '22 at 19:06
  • Do you need QGIS, or would you be OK with using OWSlib? – til_b Nov 07 '22 at 12:00
  • @til_b OWSlib is fine if I can get an image for a defined area that then can be mapped with PyQGIS – Val Nov 07 '22 at 12:07

3 Answers3

3

Use owslib per their instructions at https://geopython.github.io/OWSLib/usage.html#wms .

OWSlib passes any additional HTTP parameters directly to the server (checked with a simple netstat -lp 9999 listening socket).

Order of commands to test the behaviour:

In a linux shell: netstat -lp 9999. Leave running.

In python:

from owslib.wms import WebMapService
wms = WebMapService('http://localhost:9999/?bar=baz', version='1.3.0')

The request that arrives on the netstat socket is

GET /?bar=baz&service=WMS&request=GetCapabilities&version=1.3.0 HTTP/1.1
Host: localhost:9999
User-Agent: python-requests/2.22.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

It contains the bar=baz parameters.

You should then be able to produce an output image using "plain" OWSlib with the extra parameter arrriving at the server.

It the server supports image/geotiff as output format you can plug that directly into QGIS, if not you'll have to write a world file for the jpeg or png image with the following code (ripped from a real project, redacted, so may need some tweaking to run, but you can see the ideas):

def wf_params(ul,lr,size):
    """ul: upper left corner.
lr: lower right corner.
Only for straight-edge rectangles!"""
    ### World file writing from 
    ### https://gis.stackexchange.com/questions/269581/creating-tfw-world-files-from-tiff-coordinates-in-excel
    width, height = size
    xul,yul = ul  # -> coordinates of the upper left corner
    xlr,ylr = lr  # -> coordinates of the lower rigth corner
#print("%.3f %.3f" % (width,height))
#print("%.3f %.3f" % (xul,yul))
#print("%.3f %.3f" % (xlr,ylr))

A = (xlr-xul)/width
D = 0
B = 0
E = (ylr-yul)/height 
#center of the upper left pixel 
C = xul + (A * .5)
F = yul + (E * .5)
return (A,D,B,E,C,F)

def writeWF(params,fullpath): """Write a world file from the parameters produced by wf_param""" print("Writing worldfile to %s" % fullpath) f = open(fullpath,'w') for p in params: f.write("%.3f\n" % p); f.close()

...

bb = <the bounding box used for the owslib WMS request, given as [lowerleftX, lowerleftY, upperrightX, upperrightY>

size of the requested WMS image

outsize = 512

upperleft = (bb[0],bb[3]) lowerright = (bb[2],bb[1])

parms = wf_params(upperleft,lowerright,outsize)

writeWF(parms ,full_outname_world)

til_b
  • 5,074
  • 1
  • 19
  • 36
  • Thanks! Indeed I can select a separate timestep. But I can't use it to generate an image for mapping as 1) I need to know the shape of the output and 2) I'm limited by the max shape enforced by the server. For instance, if the max shape is 512, I can't map any bigger AOI in reasonable resolution – Val Nov 07 '22 at 14:06
  • while this was not 100% the answer I was looking for, it's definitely the most deserving for the bounty! Thanks again for the help – Val Nov 15 '22 at 14:02
3

Looking at the source code, it turns out to select a specific date you need

  1. "time-aware" layer
  2. select the time using the start/end format

Regarding 1), I've linked in the comments to a thread in the dev mailinglist which explains the required components.

Essentially it boils down to adding the following query arguments to your url:

  • allowTemporalUpdates=true
  • temporalSource=provider
  • type=wmst
  • timeDimensionExtent

with the timeDimensionExtent to be defined by the capabilities from the server.

Regarding 2), if all is needed is one date, we can just use the same date as both start and end, which results in a query time=2017-01-01/2017-01-01 (for this example).

Using the shortcut of copying the time aware URL from the GUI (pointed out by @til_b), everything seems to work:

# original non time-aware URL
url1 = "layers=MCD43C3_M_BSA&styles=rgb&bbox=-180,-90,180,90&height=512&width=512&crs=CRS:84&format=image/png&url=https://neo.gsfc.nasa.gov/wms/wms?version%3D1.3.0"

new time aware URL with time query 2017-01-01

url2 = "allowTemporalUpdates=true&contextualWMSLegend=0&crs=CRS:84&dpiMode=7&featureCount=10&format=image/png&layers=MCD43C3_M_BSA&styles&temporalSource=provider&timeDimensionExtent=2000-02-01/2016-04-01/P1M,2016-06-01/2017-02-01/P1M&type=wmst&time=2017-01-01/2017-01-01&url=https://neo.gsfc.nasa.gov/wms/wms?version%3D1.3.0"

iface.addRasterLayer(url1, "default", "wms") iface.addRasterLayer(url2, "selection", "wms")

Not only are the two layers different now, the developer panel is also showing the TIME query as expected:

enter image description here

Val
  • 273
  • 2
  • 13
1

It's probably as simple as sticking the parameter urlencoded into the QGIS layer URI string.

url2 = "layers=MCD43C3_M_BSA&styles=rgb&bbox=-180,-90,180,90&height=512&width=512&crs=CRS:84&format=image/png&url=https://neo.gsfc.nasa.gov/wms/wms?version%3D1.3.0%3Dtime=2017-01-01"

Your original code had the parameter time as a part of the URL for QGIS addRasterLayer. You want it as part of the WMS URL inside the URL that gets to addRasterLayer.

From the "Add WMS Layer" dialog it works:

enter image description here

So what you can do is add the layer via the dialog and then get its URI from the python console. Select the layer, open the python console and run iface.activeLayer().dataProvider().uri(). I am not really sure how to get that URI in a format to stick back into pyqgis to create a URI programmatically. I have done that for MSSQL layers, but never for WMS layers...

til_b
  • 5,074
  • 1
  • 19
  • 36
  • Nice thinking, but I had the same idea before posting this and it does not work (not sure if you checked it?) – Val Nov 07 '22 at 14:51
  • the question is specific to not use the GUI (otherwise I'd just use the Temporal Controller which works file). If you run my example with the python console within QGIS, you can see it doesn't work - even with your changes. But I appreciate the effort!! – Val Nov 07 '22 at 15:57
  • 1
    Yes, but you can use the GUI one time, find out the URI, and put it in the script for later use – til_b Nov 07 '22 at 17:20
  • 1
    Right, if I add the URL by GUI and then look at the .uri(), it's "time-enabled" as was discussed in this mailing list convo from 2021. Ok, so the URLs in my example are NOT time aware. What's unclear now is how to select a specific point in time using a time aware layer. Looks like this is not encoded in the URL and seems also that time is dropped on purpose to enable the selection at the "proper" point. Just no idea what this way is – Val Nov 07 '22 at 17:41
  • I googled a bit and came across https://gis.stackexchange.com/questions/34667/does-qgis-have-wms-t-wms-with-time-support , search terms were "time aware wms layer pyqgis". Apparently the standard is called "WMS-T" (great, we have WMST and WMTS, which are totally different things). Maybe you can find anything about that? Also, they mention a temporal manager in QGIS. Maybe you can find info on that too. – til_b Nov 08 '22 at 13:33
  • There is a recent commit in the QGIS repo (June 18), which concerns WMS-T. Did not read it all, but may be related. https://github.com/qgis/QGIS/commit/dbdacac777d74b00b1957b9cac81d162b01ec61e – til_b Nov 08 '22 at 13:37
  • Thanks @til_b - really appreciate the effort, thank you! You are right, this is all somehow connected, and therefore quite the rabbit hole. I think the solution could be using the Temporal controller (manager?) which has an API doc here - but no idea how that could be used. – Val Nov 08 '22 at 14:39