Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
341 changes: 200 additions & 141 deletions examples/wms.ipynb

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions geoengine/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,24 @@ def bbox_str(self) -> str:
return ','.join(map(str, self.__spatial_bounds))

@property
def bbox_ogc(self) -> str:
def bbox_ogc_str(self) -> str:
'''
TODO: what is this method and why does it say that is returns a string?
A comma-separated string representation of the spatial bounds with OGC axis ordering
'''

return ','.join(map(str, self.bbox_ogc))

@property
def bbox_ogc(self) -> Tuple[float, float, float, float]:
'''
Return the bbox with OGC axis ordering of the srs
'''

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

if self.__srs == "EPSG:4326":
return [bbox[1], bbox[0], bbox[3], bbox[2]]
return (bbox[1], bbox[0], bbox[3], bbox[2])

return bbox

Expand Down
45 changes: 38 additions & 7 deletions geoengine/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
'''

from __future__ import annotations
from typing import Any, Dict, List
from typing import Any, Dict, List, Tuple

from uuid import UUID
from logging import debug
from io import StringIO
from io import StringIO, BytesIO
import urllib.parse
import json

Expand All @@ -20,6 +20,7 @@
import rasterio
from vega import VegaLite
import numpy as np
from PIL import Image

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

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

if authority != 'EPSG:':
if authority != 'EPSG':
return fallback
try:
return ccrs.epsg(code)
Expand Down Expand Up @@ -236,7 +237,17 @@ def srs_to_projection(srs: str) -> ccrs.Projection:

return ax

def wms_get_map_curl(self, bbox: QueryRectangle) -> str:
def wms_get_map_as_image(self, bbox: QueryRectangle, colorizer_min_max: Tuple[float, float] = None) -> Image:
'''Return the result of a WMS request as a PIL Image'''

wms_request = self.__wms_get_map_request(bbox, colorizer_min_max)
response = req.Session().send(wms_request)

return Image.open(BytesIO(response.content))

def __wms_get_map_request(self,
bbox: QueryRectangle,
colorizer_min_max: Tuple[float, float] = None) -> req.PreparedRequest:
'''Return the WMS url for a workflow and a given `QueryRectangle`'''

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

colorizer = ''
if colorizer_min_max is not None:
colorizer = 'custom:' + json.dumps({
"type": "linearGradient",
"breakpoints": [{
"value": colorizer_min_max[0],
"color": [0, 0, 0, 255]
}, {
"value": colorizer_min_max[1],
"color": [255, 255, 255, 255]
}],
"noDataColor": [0, 0, 0, 0],
"defaultColor": [0, 0, 0, 0]
})

params = dict(
service='WMS',
version='1.3.0',
request="GetMap",
layers=str(self),
time=bbox.time_str,
crs=bbox.srs,
bbox=bbox.bbox_str,
bbox=bbox.bbox_ogc_str,
width=width,
height=height,
format='image/png',
styles='', # TODO: incorporate styling
styles=colorizer, # TODO: incorporate styling properly
)

wms_request = req.Request(
return req.Request(
'GET',
url=f'{session.server_url}/wms',
params=params,
headers=session.auth_header
).prepare()

def wms_get_map_curl(self, bbox: QueryRectangle, colorizer_min_max: Tuple[float, float] = None) -> str:
'''Return the WMS curl command for a workflow and a given `QueryRectangle`'''

wms_request = self.__wms_get_map_request(bbox, colorizer_min_max)

command = "curl -X {method} -H {headers} '{uri}'"
headers = ['"{0}: {1}"'.format(k, v) for k, v in wms_request.headers.items()]
headers = " -H ".join(headers)
Expand Down
5 changes: 3 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@ package_dir =
packages = find:
python_requires = >=3.7
install_requires =
numpy
cartopy
geopandas
matplotlib
numpy
owslib
pillow
pyepsg # for cartopy
rasterio
requests
scipy # for cartopy
vega
rasterio

[options.packages.find]
where = .
Expand Down
30 changes: 30 additions & 0 deletions tests/responses/4326.gml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<gml:metaDataProperty>
<epsg:CommonMetaData>
<epsg:type>geographic 2D</epsg:type>
<epsg:informationSource>EPSG. See 3D CRS for original information source.</epsg:informationSource>
<epsg:revisionDate>2007-08-27</epsg:revisionDate>
<epsg:changes>
<epsg:changeID xlink:href="urn:ogc:def:change-request:EPSG::2002.151" />
<epsg:changeID xlink:href="urn:ogc:def:change-request:EPSG::2003.370" />
<epsg:changeID xlink:href="urn:ogc:def:change-request:EPSG::2006.810" />
<epsg:changeID xlink:href="urn:ogc:def:change-request:EPSG::2007.079" />
</epsg:changes>
<epsg:show>true</epsg:show>
<epsg:isDeprecated>false</epsg:isDeprecated>
</epsg:CommonMetaData>
</gml:metaDataProperty>
<gml:metaDataProperty>
<epsg:CRSMetaData>
<epsg:projectionConversion xlink:href="urn:ogc:def:coordinateOperation:EPSG::15593" />
<epsg:sourceGeographicCRS xlink:href="urn:ogc:def:crs:EPSG::4979" />
</epsg:CRSMetaData>
</gml:metaDataProperty>
<gml:identifier codeSpace="OGP">urn:ogc:def:crs:EPSG::4326</gml:identifier>
<gml:name>WGS 84</gml:name>
<gml:domainOfValidity xlink:href="urn:ogc:def:area:EPSG::1262" />
<gml:scope>Horizontal component of 3D system. Used by the GPS satellite navigation system and for NATO military geodetic surveying.</gml:scope>
<gml:ellipsoidalCS xlink:href="urn:ogc:def:cs:EPSG::6422" />
<gml:geodeticDatum xlink:href="urn:ogc:def:datum:EPSG::6326" />
</gml:GeodeticCRS>
81 changes: 77 additions & 4 deletions tests/test_wms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import datetime
import unittest
import textwrap
from PIL import Image

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

m.get('http://epsg.io/4326.gml?download', body=epsg4326_gml)

# Unfortunately, we need a separate library to catch the request from the WMS call
with open("tests/responses/wms-ndvi.png", "rb") as wms_ndvi:
responses.add(
Expand Down Expand Up @@ -90,16 +93,84 @@ def test_ndvi(self):
self.assertEqual(type(ax), cartopy.mpl.geoaxes.GeoAxesSubplot)

# Check requests from the mocker
self.assertEqual(len(m.request_history), 3)
self.assertEqual(len(m.request_history), 4)

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

def test_ndvi_image(self):
with requests_mock.Mocker() as m,\
open("tests/responses/wms-ndvi.png", "rb") as ndvi_png,\
open("tests/responses/4326.gml", "rb") as epsg4326_gml:
m.post('http://mock-instance/anonymous', json={
"id": "c4983c3e-9b53-47ae-bda9-382223bd5081",
"project": None,
"view": None
})

m.post('http://mock-instance/workflow',
json={
"id": "5b9508a8-bd34-5a1c-acd6-75bb832d2d38"
},
request_headers={'Authorization': 'Bearer c4983c3e-9b53-47ae-bda9-382223bd5081'})

m.get('http://mock-instance/workflow/5b9508a8-bd34-5a1c-acd6-75bb832d2d38/metadata',
json={
"type": "raster",
"dataType": "U8",
"spatialReference": "EPSG:4326",
"measurement": {
"type": "unitless"
},
"noDataValue": 0.0
},
request_headers={'Authorization': 'Bearer c4983c3e-9b53-47ae-bda9-382223bd5081'})

m.get('http://epsg.io/4326.gml?download', body=epsg4326_gml)

# Unfortunately, we need a separate library to catch the request from the WMS call
m.get(
# pylint: disable=line-too-long
'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',
body=ndvi_png,
)

ge.initialize("http://mock-instance")

workflow_definition = {
"type": "Raster",
"operator": {
"type": "GdalSource",
"params": {
"dataset": {
"type": "internal",
"datasetId": "36574dc3-560a-4b09-9d22-d5945f2b8093"
}
}
}
}

time = datetime.strptime(
'2014-04-01T12:00:00.000Z', "%Y-%m-%dT%H:%M:%S.%f%z")

workflow = ge.register_workflow(workflow_definition)

img = workflow.wms_get_map_as_image(
QueryRectangle(
[-180.0, -90.0, 180.0, 90.0],
[time, time],
resolution=(1.8, 1.8)
),
colorizer_min_max=(0, 255)
)

self.assertEqual(img, Image.open("tests/responses/wms-ndvi.png"))

def test_wms_url(self):
with requests_mock.Mocker() as m:
with requests_mock.Mocker() as m, open("tests/responses/4326.gml", "rb") as epsg4326_gml:
m.post('http://mock-instance/anonymous', json={
"id": "c4983c3e-9b53-47ae-bda9-382223bd5081",
"project": None,
Expand All @@ -124,6 +195,8 @@ def test_wms_url(self):
},
request_headers={'Authorization': 'Bearer c4983c3e-9b53-47ae-bda9-382223bd5081'})

m.get('http://epsg.io/4326.gml?download', body=epsg4326_gml)

ge.initialize("http://mock-instance")

workflow_definition = {
Expand Down Expand Up @@ -153,7 +226,7 @@ def test_wms_url(self):
self.assertEqual(
# pylint: disable=line-too-long
wms_curl,
"""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='"""
"""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='"""
)

def test_result_descriptor(self):
Expand Down