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
16 changes: 8 additions & 8 deletions docs/source/publishing/ogcapi-maps.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,14 @@ required. An optional style name can be defined via `options.style`.

.. note::
According to the `Standard <https://docs.ogc.org/is/20-058/20-058.html#_5df53b56-5468-4c9d-acac-6abfddd83ccf>`_, OGC API - Maps
supports a `crs` parameter, expressed as an uri. Currently, this provider supports CRS84, WGS84 and Web Mercator; for a matter of convenience, they can be expressed in
a number of different ways, other than the uri format.

- `EPSG:4326`
- `EPSG:3857`
- `4326`
- `3857`,
- `CRS84`
supports a `crs` and `bbox-crs` parameters, expressed as an uri or a CURIE. Currently, this provider supports CRS84 and various CRS from the EPSG namespace; for a matter of convenience, they can be expressed
also as unsafe CURIEs.

- `http://www.opengis.net/def/crs/EPSG/0/4326`
- `[EPSG:4326]`
- `EPSG:4326` (unsafe)
- `CRS:84` (unsafe, for compatibility with WMS)
- `OGC:CRS84` (unsafe)

If `crs` is not provided, the server will default to the `storage_crs`; in case it does not exist, the default is `CRS84`.
If `crs-bbox` is not provided, it will default to `CRS84`. If the `bbox` is not provided, it will default to `-180, -90, 180, 90`.
Expand Down
27 changes: 7 additions & 20 deletions pygeoapi/api/maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
import logging
from typing import Tuple

from pygeoapi.crs import transform_bbox, DEFAULT_CRS
from pygeoapi.crs import transform_bbox, DEFAULT_CRS, get_uri
from pygeoapi.formats import F_JSON, FORMAT_TYPES
from pygeoapi.openapi import get_oas_30_parameters
from pygeoapi.plugin import load_plugin
Expand All @@ -64,18 +64,6 @@

DEFAULT_BBOX = [-180, -90, 180, 90] # CRS84

CRS_CODES = {
'4326': 'http://www.opengis.net/def/crs/EPSG/0/4326',
'3857': 'http://www.opengis.net/def/crs/EPSG/0/3857',
'CRS84': 'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
'http://www.opengis.net/def/crs/EPSG/0/4326': 'http://www.opengis.net/def/crs/EPSG/0/4326', # noqa
'http://www.opengis.net/def/crs/EPSG/0/3857': 'http://www.opengis.net/def/crs/EPSG/0/3857', # noqa
'http://www.opengis.net/def/crs/OGC/1.3/CRS84': 'http://www.opengis.net/def/crs/OGC/1.3/CRS84', # noqa
'EPSG:4326': 'http://www.opengis.net/def/crs/EPSG/0/4326',
'EPSG:3857': 'http://www.opengis.net/def/crs/EPSG/0/3857',
'CRS:84': 'http://www.opengis.net/def/crs/OGC/1.3/CRS84',
}


def get_collection_map(api: API, request: APIRequest,
dataset: str, style: str | None = None
Expand Down Expand Up @@ -123,11 +111,11 @@ def get_collection_map(api: API, request: APIRequest,
# if it does not exist or is not supported, use CRS84.
try:
if 'crs' not in request.params:
query_args['crs'] = CRS_CODES.get(collection_def.get('storage_crs',
DEFAULT_CRS), DEFAULT_CRS)
query_args['crs'] = collection_def.get('storage_crs',
DEFAULT_CRS)
else:
query_args['crs'] = CRS_CODES.get(request.params['crs'],
DEFAULT_CRS)
query_args['crs'] = get_uri(request.params['crs'])

except KeyError:
query_args['crs'] = DEFAULT_CRS

Expand All @@ -138,8 +126,7 @@ def get_collection_map(api: API, request: APIRequest,
if 'bbox-crs' not in request.params:
query_args['bbox-crs'] = DEFAULT_CRS
else:
query_args['bbox-crs'] = CRS_CODES.get(request.params['bbox-crs'],
DEFAULT_CRS)
query_args['bbox-crs'] = get_uri(request.params['bbox-crs'])
except KeyError:
query_args['bbox-crs'] = DEFAULT_CRS

Expand Down Expand Up @@ -190,9 +177,9 @@ def get_collection_map(api: API, request: APIRequest,

# the transformer function expects the crs to be in a uri format
if query_args['bbox-crs'] != query_args['crs']:
LOGGER.debug(f'Reprojecting bbox CRS: {query_args["crs"]}')
bbox = transform_bbox(bbox, query_args['bbox-crs'],
query_args['crs'], always_xy=True)
LOGGER.debug(f'Transformed bbox: {bbox}')
query_args['bbox'] = bbox

LOGGER.debug('Processing datetime parameter')
Expand Down
92 changes: 92 additions & 0 deletions pygeoapi/crs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Authors: Tom Kralidis <tomkralidis@gmail.com>
# Just van den Broecke <justb4@gmail.com>
#
# Copyright (c) 2026 Joana Simoes
# Copyright (c) 2025 Tom Kralidis
# Copyright (c) 2025 Just van den Broecke
#
Expand Down Expand Up @@ -47,6 +48,7 @@
shape as geojson_to_geom,
mapping as geom_to_geojson
)
from urllib.parse import urlparse

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -120,6 +122,96 @@ def get_supported_crs_list(
return supported_crs_list


def get_uri(crs: str) -> str:
"""
Parse a uri from a uri or a curie

:param crs: Uniform resource identifier of the coordinate
reference system. In accordance with
https://docs.ogc.org/pol/09-048r5.html#_naming_rule
Or a safe, or unsafe curie
https://docs.ogc.org/DRAFTS/20-024.html#conventions-curies

:raises `CRSError`: Error raised if no CRS could be identified from the
URI.

:returns: `crs uri` matching the input CRS.
"""

try:
crs = crs.lower()
result = urlparse(crs)

# If it is a uri, check if it is valid
LOGGER.debug(f'Attempt to parse a uri: {crs}')

if result.scheme not in ['http', 'https']:
raise CRSError('Invalid uri scheme')
if result.netloc is None or result.netloc != 'www.opengis.net':
raise CRSError('Invalid uri prefix')

path_el = [p for p in result.path.split('/') if p]

# Check if the path uri contains the relevant fragments
if len(path_el
) != 5 or path_el[0] != 'def' or path_el[1] != 'crs' or path_el[2] not in ['epsg', 'ogc']: # noqa
raise CRSError('Invalid uri fragments')

return crs

except CRSError:
try:
# Parse safe CURIE
curie = crs.strip('[]')
LOGGER.debug(f'Attempt to parse a curie: {curie}')

# We support all EPSG codes and CRS84
if curie not in ['crs:84', 'ogc:crs84']:
[curie_auth, curie_code] = curie.split(':')
if len(curie.split(':')) != 2 or (curie_auth != 'epsg'):
raise CRSError('Unsupported CRS')

return f'http://www.opengis.net/def/crs/EPSG/0/{curie_code}'
else:
return 'http://www.opengis.net/def/crs/OGC/1.3/CRS84'

except CRSError as e:
return e


def get_curie(crs: str) -> str:
"""
Get a WMS compatible CRS CURIE from a uri

:param crs: Uniform resource identifier of the coordinate
reference system. In accordance with
https://docs.ogc.org/pol/09-048r5.html#_naming_rule

:raises `CRSError`: Error raised if no CRS could be identified from the
URI.

:returns: `WMS CURIE` matching the input uri.
"""

try:
if not crs.startswith(("http://", "https://")):
raise CRSError('Not an uri')

crs = crs.lower()
path_el = [p for p in crs.split('/') if p]

# We support all EPSG codes and CRS84
if path_el[4] == 'epsg':
return f'EPSG:{path_el[6]}'
elif path_el[6] != 'crs84':
raise CRSError('Unsupported CRS')

return 'CRS:84'

except CRSError as e:
return e


def get_crs(crs: Union[str, pyproj.CRS]) -> pyproj.CRS:
"""
Get a `pyproj.CRS` instance from a CRS.
Expand Down
29 changes: 17 additions & 12 deletions pygeoapi/provider/wms_facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
import logging
from urllib.parse import urlencode

from pyproj.exceptions import CRSError

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import logging
from urllib.parse import urlencode

from pyproj.exceptions import CRSError

from pygeoapi.crs import get_crs_curie

(ordering by standard packages, 3rd party, local).


from pygeoapi.crs import get_curie

import pyproj
import requests

Expand All @@ -43,13 +47,7 @@
'png': 'image/png'
}

CRS_CODES = {
'http://www.opengis.net/def/crs/EPSG/0/4326': 'EPSG:4326',
'http://www.opengis.net/def/crs/EPSG/0/3857': 'EPSG:3857',
'http://www.opengis.net/def/crs/OGC/1.3/CRS84': 'CRS:84' # noqa
}

DEFAULT_CRS = 'CRS:84'
DEFAULT_WMS_CRS = 'CRS:84'


class WMSFacadeProvider(BaseProvider):
Expand All @@ -68,9 +66,9 @@ def __init__(self, provider_def):

LOGGER.debug(f'pyproj version: {pyproj.__version__}')

def query(self, style=None, bbox=[-180, -90, 180, 90], width=500,
height=300, crs=DEFAULT_CRS, datetime_=None, transparent=True,
format_='png', **kwargs):
def query(self, style=None, bbox=[-180, -90, 180, 90],
width=500, height=300, crs=DEFAULT_WMS_CRS, datetime_=None,
transparent=True, format_='png', **kwargs):
"""
Generate map

Expand All @@ -87,11 +85,18 @@ def query(self, style=None, bbox=[-180, -90, 180, 90], width=500,
:returns: `bytes` of map image
"""

LOGGER.debug(f'bbox: {bbox}')

self._transparent = 'TRUE'

version = self.options.get('version', '1.3.0')

if version == '1.3.0' and CRS_CODES.get(crs) == 'EPSG:4326':
try:
wms_crs = get_curie(crs)
except CRSError:
wms_crs = DEFAULT_WMS_CRS

if version == '1.3.0' and wms_crs == 'EPSG:4326':
bbox = [bbox[1], bbox[0], bbox[3], bbox[2]]
bbox2 = ','.join(map(str, bbox))

Expand All @@ -104,7 +109,7 @@ def query(self, style=None, bbox=[-180, -90, 180, 90], width=500,
'service': 'WMS',
'request': 'GetMap',
'bbox': bbox2,
crs_param: CRS_CODES.get(crs) or DEFAULT_CRS,
crs_param: wms_crs,
'layers': self.options['layer'],
'styles': self.options.get('style', 'default'),
'width': width,
Expand Down
10 changes: 4 additions & 6 deletions tests/provider/test_wms_facade_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import pytest

from pygeoapi.provider.wms_facade import WMSFacadeProvider
from pygeoapi.provider.base import ProviderQueryError


@pytest.fixture()
Expand Down Expand Up @@ -61,12 +62,9 @@ def test_crs_query(config):
results1 = p.query(crs='http://www.opengis.net/def/crs/EPSG/0/4326')
results2 = p.query(crs='http://www.opengis.net/def/crs/EPSG/0/3857')

print(results1)

check_is_PNG(results1)
check_is_PNG(results2)

# An invalid crs should default to default crs
results3 = p.query(crs='http://0000')

check_is_PNG(results3)
# An invalid uri triggers an error
with pytest.raises(ProviderQueryError):
p.query(crs='http://www.opengis.net/def/crs/FOO/0/9999')
Loading