Skip to content

Commit f23fafe

Browse files
Merge pull request #21 from geo-engine/wms_image
get wms result as image
2 parents b06e60c + b949ac4 commit f23fafe

File tree

6 files changed

+359
-157
lines changed

6 files changed

+359
-157
lines changed

examples/wms.ipynb

Lines changed: 200 additions & 141 deletions
Large diffs are not rendered by default.

geoengine/types.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,24 @@ def bbox_str(self) -> str:
6060
return ','.join(map(str, self.__spatial_bounds))
6161

6262
@property
63-
def bbox_ogc(self) -> str:
63+
def bbox_ogc_str(self) -> str:
6464
'''
65-
TODO: what is this method and why does it say that is returns a string?
65+
A comma-separated string representation of the spatial bounds with OGC axis ordering
66+
'''
67+
68+
return ','.join(map(str, self.bbox_ogc))
69+
70+
@property
71+
def bbox_ogc(self) -> Tuple[float, float, float, float]:
72+
'''
73+
Return the bbox with OGC axis ordering of the srs
6674
'''
6775

6876
# TODO: properly handle axis order
6977
bbox = self.__spatial_bounds
7078

7179
if self.__srs == "EPSG:4326":
72-
return [bbox[1], bbox[0], bbox[3], bbox[2]]
80+
return (bbox[1], bbox[0], bbox[3], bbox[2])
7381

7482
return bbox
7583

geoengine/workflow.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
'''
44

55
from __future__ import annotations
6-
from typing import Any, Dict, List
6+
from typing import Any, Dict, List, Tuple
77

88
from uuid import UUID
99
from logging import debug
10-
from io import StringIO
10+
from io import StringIO, BytesIO
1111
import urllib.parse
1212
import json
1313

@@ -20,6 +20,7 @@
2020
import rasterio
2121
from vega import VegaLite
2222
import numpy as np
23+
from PIL import Image
2324

2425
from geoengine.types import ProvenanceOutput, QueryRectangle, ResultDescriptor
2526
from geoengine.auth import get_session
@@ -205,7 +206,7 @@ def srs_to_projection(srs: str) -> ccrs.Projection:
205206

206207
[authority, code] = srs.split(':')
207208

208-
if authority != 'EPSG:':
209+
if authority != 'EPSG':
209210
return fallback
210211
try:
211212
return ccrs.epsg(code)
@@ -236,7 +237,17 @@ def srs_to_projection(srs: str) -> ccrs.Projection:
236237

237238
return ax
238239

239-
def wms_get_map_curl(self, bbox: QueryRectangle) -> str:
240+
def wms_get_map_as_image(self, bbox: QueryRectangle, colorizer_min_max: Tuple[float, float] = None) -> Image:
241+
'''Return the result of a WMS request as a PIL Image'''
242+
243+
wms_request = self.__wms_get_map_request(bbox, colorizer_min_max)
244+
response = req.Session().send(wms_request)
245+
246+
return Image.open(BytesIO(response.content))
247+
248+
def __wms_get_map_request(self,
249+
bbox: QueryRectangle,
250+
colorizer_min_max: Tuple[float, float] = None) -> req.PreparedRequest:
240251
'''Return the WMS url for a workflow and a given `QueryRectangle`'''
241252

242253
if not self.__result_descriptor.is_raster_result():
@@ -247,27 +258,47 @@ def wms_get_map_curl(self, bbox: QueryRectangle) -> str:
247258
width = int((bbox.xmax - bbox.xmin) / bbox.resolution[0])
248259
height = int((bbox.ymax - bbox.ymin) / bbox.resolution[1])
249260

261+
colorizer = ''
262+
if colorizer_min_max is not None:
263+
colorizer = 'custom:' + json.dumps({
264+
"type": "linearGradient",
265+
"breakpoints": [{
266+
"value": colorizer_min_max[0],
267+
"color": [0, 0, 0, 255]
268+
}, {
269+
"value": colorizer_min_max[1],
270+
"color": [255, 255, 255, 255]
271+
}],
272+
"noDataColor": [0, 0, 0, 0],
273+
"defaultColor": [0, 0, 0, 0]
274+
})
275+
250276
params = dict(
251277
service='WMS',
252278
version='1.3.0',
253279
request="GetMap",
254280
layers=str(self),
255281
time=bbox.time_str,
256282
crs=bbox.srs,
257-
bbox=bbox.bbox_str,
283+
bbox=bbox.bbox_ogc_str,
258284
width=width,
259285
height=height,
260286
format='image/png',
261-
styles='', # TODO: incorporate styling
287+
styles=colorizer, # TODO: incorporate styling properly
262288
)
263289

264-
wms_request = req.Request(
290+
return req.Request(
265291
'GET',
266292
url=f'{session.server_url}/wms',
267293
params=params,
268294
headers=session.auth_header
269295
).prepare()
270296

297+
def wms_get_map_curl(self, bbox: QueryRectangle, colorizer_min_max: Tuple[float, float] = None) -> str:
298+
'''Return the WMS curl command for a workflow and a given `QueryRectangle`'''
299+
300+
wms_request = self.__wms_get_map_request(bbox, colorizer_min_max)
301+
271302
command = "curl -X {method} -H {headers} '{uri}'"
272303
headers = ['"{0}: {1}"'.format(k, v) for k, v in wms_request.headers.items()]
273304
headers = " -H ".join(headers)

setup.cfg

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,17 @@ package_dir =
1818
packages = find:
1919
python_requires = >=3.7
2020
install_requires =
21-
numpy
2221
cartopy
2322
geopandas
2423
matplotlib
24+
numpy
2525
owslib
26+
pillow
2627
pyepsg # for cartopy
28+
rasterio
2729
requests
2830
scipy # for cartopy
2931
vega
30-
rasterio
3132

3233
[options.packages.find]
3334
where = .

tests/responses/4326.gml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<gml:GeodeticCRS xmlns:epsg="urn:x-ogp:spec:schema-xsd:EPSG:1.0:dataset" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:xlink="http://www.w3.org/1999/xlink" gml:id="epsg-crs-4326">
3+
<gml:metaDataProperty>
4+
<epsg:CommonMetaData>
5+
<epsg:type>geographic 2D</epsg:type>
6+
<epsg:informationSource>EPSG. See 3D CRS for original information source.</epsg:informationSource>
7+
<epsg:revisionDate>2007-08-27</epsg:revisionDate>
8+
<epsg:changes>
9+
<epsg:changeID xlink:href="urn:ogc:def:change-request:EPSG::2002.151" />
10+
<epsg:changeID xlink:href="urn:ogc:def:change-request:EPSG::2003.370" />
11+
<epsg:changeID xlink:href="urn:ogc:def:change-request:EPSG::2006.810" />
12+
<epsg:changeID xlink:href="urn:ogc:def:change-request:EPSG::2007.079" />
13+
</epsg:changes>
14+
<epsg:show>true</epsg:show>
15+
<epsg:isDeprecated>false</epsg:isDeprecated>
16+
</epsg:CommonMetaData>
17+
</gml:metaDataProperty>
18+
<gml:metaDataProperty>
19+
<epsg:CRSMetaData>
20+
<epsg:projectionConversion xlink:href="urn:ogc:def:coordinateOperation:EPSG::15593" />
21+
<epsg:sourceGeographicCRS xlink:href="urn:ogc:def:crs:EPSG::4979" />
22+
</epsg:CRSMetaData>
23+
</gml:metaDataProperty>
24+
<gml:identifier codeSpace="OGP">urn:ogc:def:crs:EPSG::4326</gml:identifier>
25+
<gml:name>WGS 84</gml:name>
26+
<gml:domainOfValidity xlink:href="urn:ogc:def:area:EPSG::1262" />
27+
<gml:scope>Horizontal component of 3D system. Used by the GPS satellite navigation system and for NATO military geodetic surveying.</gml:scope>
28+
<gml:ellipsoidalCS xlink:href="urn:ogc:def:cs:EPSG::6422" />
29+
<gml:geodeticDatum xlink:href="urn:ogc:def:datum:EPSG::6326" />
30+
</gml:GeodeticCRS>

tests/test_wms.py

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from datetime import datetime
44
import unittest
55
import textwrap
6+
from PIL import Image
67

78
import requests_mock
89
import cartopy.mpl.geoaxes
@@ -22,7 +23,7 @@ def setUp(self) -> None:
2223
@responses.activate
2324
@ImageTesting(['wms'], tolerance=0)
2425
def test_ndvi(self):
25-
with requests_mock.Mocker() as m:
26+
with requests_mock.Mocker() as m, open("tests/responses/4326.gml", "rb") as epsg4326_gml:
2627
m.post('http://mock-instance/anonymous', json={
2728
"id": "c4983c3e-9b53-47ae-bda9-382223bd5081",
2829
"project": None,
@@ -47,6 +48,8 @@ def test_ndvi(self):
4748
},
4849
request_headers={'Authorization': 'Bearer c4983c3e-9b53-47ae-bda9-382223bd5081'})
4950

51+
m.get('http://epsg.io/4326.gml?download', body=epsg4326_gml)
52+
5053
# Unfortunately, we need a separate library to catch the request from the WMS call
5154
with open("tests/responses/wms-ndvi.png", "rb") as wms_ndvi:
5255
responses.add(
@@ -90,16 +93,84 @@ def test_ndvi(self):
9093
self.assertEqual(type(ax), cartopy.mpl.geoaxes.GeoAxesSubplot)
9194

9295
# Check requests from the mocker
93-
self.assertEqual(len(m.request_history), 3)
96+
self.assertEqual(len(m.request_history), 4)
9497

9598
workflow_request = m.request_history[1]
9699
self.assertEqual(workflow_request.method, "POST")
97100
self.assertEqual(workflow_request.url,
98101
"http://mock-instance/workflow")
99102
self.assertEqual(workflow_request.json(), workflow_definition)
100103

104+
def test_ndvi_image(self):
105+
with requests_mock.Mocker() as m,\
106+
open("tests/responses/wms-ndvi.png", "rb") as ndvi_png,\
107+
open("tests/responses/4326.gml", "rb") as epsg4326_gml:
108+
m.post('http://mock-instance/anonymous', json={
109+
"id": "c4983c3e-9b53-47ae-bda9-382223bd5081",
110+
"project": None,
111+
"view": None
112+
})
113+
114+
m.post('http://mock-instance/workflow',
115+
json={
116+
"id": "5b9508a8-bd34-5a1c-acd6-75bb832d2d38"
117+
},
118+
request_headers={'Authorization': 'Bearer c4983c3e-9b53-47ae-bda9-382223bd5081'})
119+
120+
m.get('http://mock-instance/workflow/5b9508a8-bd34-5a1c-acd6-75bb832d2d38/metadata',
121+
json={
122+
"type": "raster",
123+
"dataType": "U8",
124+
"spatialReference": "EPSG:4326",
125+
"measurement": {
126+
"type": "unitless"
127+
},
128+
"noDataValue": 0.0
129+
},
130+
request_headers={'Authorization': 'Bearer c4983c3e-9b53-47ae-bda9-382223bd5081'})
131+
132+
m.get('http://epsg.io/4326.gml?download', body=epsg4326_gml)
133+
134+
# Unfortunately, we need a separate library to catch the request from the WMS call
135+
m.get(
136+
# pylint: disable=line-too-long
137+
'http://mock-instance/wms?service=WMS&version=1.3.0&request=GetMap&layers=5b9508a8-bd34-5a1c-acd6-75bb832d2d38&time=2014-04-01T12%3A00%3A00.000%2B00%3A00&crs=EPSG%3A4326&bbox=-90.0%2C-180.0%2C90.0%2C180.0&width=200&height=100&format=image%2Fpng&styles=custom%3A%7B%22type%22%3A+%22linearGradient%22%2C+%22breakpoints%22%3A+%5B%7B%22value%22%3A+0%2C+%22color%22%3A+%5B0%2C+0%2C+0%2C+255%5D%7D%2C+%7B%22value%22%3A+255%2C+%22color%22%3A+%5B255%2C+255%2C+255%2C+255%5D%7D%5D%2C+%22noDataColor%22%3A+%5B0%2C+0%2C+0%2C+0%5D%2C+%22defaultColor%22%3A+%5B0%2C+0%2C+0%2C+0%5D%7D',
138+
body=ndvi_png,
139+
)
140+
141+
ge.initialize("http://mock-instance")
142+
143+
workflow_definition = {
144+
"type": "Raster",
145+
"operator": {
146+
"type": "GdalSource",
147+
"params": {
148+
"dataset": {
149+
"type": "internal",
150+
"datasetId": "36574dc3-560a-4b09-9d22-d5945f2b8093"
151+
}
152+
}
153+
}
154+
}
155+
156+
time = datetime.strptime(
157+
'2014-04-01T12:00:00.000Z', "%Y-%m-%dT%H:%M:%S.%f%z")
158+
159+
workflow = ge.register_workflow(workflow_definition)
160+
161+
img = workflow.wms_get_map_as_image(
162+
QueryRectangle(
163+
[-180.0, -90.0, 180.0, 90.0],
164+
[time, time],
165+
resolution=(1.8, 1.8)
166+
),
167+
colorizer_min_max=(0, 255)
168+
)
169+
170+
self.assertEqual(img, Image.open("tests/responses/wms-ndvi.png"))
171+
101172
def test_wms_url(self):
102-
with requests_mock.Mocker() as m:
173+
with requests_mock.Mocker() as m, open("tests/responses/4326.gml", "rb") as epsg4326_gml:
103174
m.post('http://mock-instance/anonymous', json={
104175
"id": "c4983c3e-9b53-47ae-bda9-382223bd5081",
105176
"project": None,
@@ -124,6 +195,8 @@ def test_wms_url(self):
124195
},
125196
request_headers={'Authorization': 'Bearer c4983c3e-9b53-47ae-bda9-382223bd5081'})
126197

198+
m.get('http://epsg.io/4326.gml?download', body=epsg4326_gml)
199+
127200
ge.initialize("http://mock-instance")
128201

129202
workflow_definition = {
@@ -153,7 +226,7 @@ def test_wms_url(self):
153226
self.assertEqual(
154227
# pylint: disable=line-too-long
155228
wms_curl,
156-
"""curl -X GET -H "Authorization: Bearer c4983c3e-9b53-47ae-bda9-382223bd5081" 'http://mock-instance/wms?service=WMS&version=1.3.0&request=GetMap&layers=5b9508a8-bd34-5a1c-acd6-75bb832d2d38&time=2014-04-01T12%3A00%3A00.000%2B00%3A00&crs=EPSG%3A4326&bbox=-180.0%2C-90.0%2C180.0%2C90.0&width=360&height=180&format=image%2Fpng&styles='"""
229+
"""curl -X GET -H "Authorization: Bearer c4983c3e-9b53-47ae-bda9-382223bd5081" 'http://mock-instance/wms?service=WMS&version=1.3.0&request=GetMap&layers=5b9508a8-bd34-5a1c-acd6-75bb832d2d38&time=2014-04-01T12%3A00%3A00.000%2B00%3A00&crs=EPSG%3A4326&bbox=-90.0%2C-180.0%2C90.0%2C180.0&width=360&height=180&format=image%2Fpng&styles='"""
157230
)
158231

159232
def test_result_descriptor(self):

0 commit comments

Comments
 (0)