From 781dce946bde86f2c5d2ab6cc895d0cd56b97688 Mon Sep 17 00:00:00 2001 From: doublebyte1 Date: Wed, 13 May 2026 18:46:24 +0100 Subject: [PATCH 01/15] - added functions for parsing wms codes from uris and curies, and for transforming wms codes in uris --- pygeoapi/api/maps.py | 11 ++++--- pygeoapi/crs.py | 78 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/pygeoapi/api/maps.py b/pygeoapi/api/maps.py index 360673f18..c394b3cc5 100644 --- a/pygeoapi/api/maps.py +++ b/pygeoapi/api/maps.py @@ -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_crs_curie from pygeoapi.formats import F_JSON, FORMAT_TYPES from pygeoapi.openapi import get_oas_30_parameters from pygeoapi.plugin import load_plugin @@ -65,18 +65,18 @@ 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', + '4326': 'http://www.opengis.net/def/crs/EPSG/0/4326',# stop supporting this + '3857': 'http://www.opengis.net/def/crs/EPSG/0/3857',# stop supporting this + 'CRS84': 'http://www.opengis.net/def/crs/OGC/1.3/CRS84',# stop supporting this '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', + 'OGC:CRS84': 'http://www.opengis.net/def/crs/OGC/1.3/CRS84', } - def get_collection_map(api: API, request: APIRequest, dataset: str, style: str | None = None ) -> Tuple[dict, int, str]: @@ -128,6 +128,7 @@ def get_collection_map(api: API, request: APIRequest, else: query_args['crs'] = CRS_CODES.get(request.params['crs'], DEFAULT_CRS) + LOGGER.debug(get_crs_curie(request.params['crs'])) except KeyError: query_args['crs'] = DEFAULT_CRS diff --git a/pygeoapi/crs.py b/pygeoapi/crs.py index c66287b70..a8fe737e9 100644 --- a/pygeoapi/crs.py +++ b/pygeoapi/crs.py @@ -3,6 +3,7 @@ # Authors: Tom Kralidis # Just van den Broecke # +# Copyright (c) 2026 Joana Simoes # Copyright (c) 2025 Tom Kralidis # Copyright (c) 2025 Just van den Broecke # @@ -47,6 +48,7 @@ shape as geojson_to_geom, mapping as geom_to_geojson ) +from urllib.parse import urlparse LOGGER = logging.getLogger(__name__) @@ -119,6 +121,82 @@ def get_supported_crs_list( return supported_crs_list +def get_crs_uri(str) -> str: + + try: + if str.contains('epsg'): + curie_el = str.split(':') + return f'http://www.opengis.net/def/crs/EPSG/0/{curie_el[1]}' + elif str != 'CRS:84': + raise CRSError + + return 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' + except CRSError as e: + return e + +def get_crs_curie(str) -> str: + """ + Get a wms compatible crs curie 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 curie` matching the input crs. + """ + + try: + str = str.lower() + LOGGER.debug(str) + result = urlparse(str) + + 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']: + raise CRSError('Invalid uri fragments') + + # We support all EPSG crs and CRS84 + if path_el[2] == 'epsg': + return f'EPSG:{path_el[4]}' + elif path_el[4] != 'crs84': + raise CRSError('Unsupported crs') + + return 'CRS:84' + + except CRSError as e: + try: + # parse safe curies + curie = str.strip('[]') + LOGGER.debug(curie) + + curie_el = curie.split(':') + LOGGER.debug(len(curie_el)) + # We support all EPSG and CRS84 + if len(curie_el + ) > 2 or (curie_el[0] != 'epsg' and curie not in ['crs:84','ogc:crs84']): + raise CRSError('Unsupported crs') + + if curie in ['crs:84','ogc:crs84']: + return 'CRS:84' + + return curie.upper() + + except CRSError as e: + return e + + def get_crs(crs: Union[str, pyproj.CRS]) -> pyproj.CRS: """ From bdd4a8517b522c47b8078e3145f4113dfd09192c Mon Sep 17 00:00:00 2001 From: doublebyte Date: Sat, 20 Jun 2026 12:46:15 +0100 Subject: [PATCH 02/15] - fixed flake8 errors --- pygeoapi/api/maps.py | 7 +-- pygeoapi/crs.py | 115 ++++++++++++++++++++++--------------------- 2 files changed, 62 insertions(+), 60 deletions(-) diff --git a/pygeoapi/api/maps.py b/pygeoapi/api/maps.py index c394b3cc5..acd5a1ce4 100644 --- a/pygeoapi/api/maps.py +++ b/pygeoapi/api/maps.py @@ -65,9 +65,9 @@ DEFAULT_BBOX = [-180, -90, 180, 90] # CRS84 CRS_CODES = { - '4326': 'http://www.opengis.net/def/crs/EPSG/0/4326',# stop supporting this - '3857': 'http://www.opengis.net/def/crs/EPSG/0/3857',# stop supporting this - 'CRS84': 'http://www.opengis.net/def/crs/OGC/1.3/CRS84',# stop supporting this + '4326': 'http://www.opengis.net/def/crs/EPSG/0/4326', # stop supporting this # noqa + '3857': 'http://www.opengis.net/def/crs/EPSG/0/3857', # stop supporting this # noqa + 'CRS84': 'http://www.opengis.net/def/crs/OGC/1.3/CRS84', # stop supporting this # noqa '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 @@ -77,6 +77,7 @@ 'OGC:CRS84': 'http://www.opengis.net/def/crs/OGC/1.3/CRS84', } + def get_collection_map(api: API, request: APIRequest, dataset: str, style: str | None = None ) -> Tuple[dict, int, str]: diff --git a/pygeoapi/crs.py b/pygeoapi/crs.py index a8fe737e9..488a80e5e 100644 --- a/pygeoapi/crs.py +++ b/pygeoapi/crs.py @@ -121,6 +121,7 @@ def get_supported_crs_list( return supported_crs_list + def get_crs_uri(str) -> str: try: @@ -129,74 +130,74 @@ def get_crs_uri(str) -> str: return f'http://www.opengis.net/def/crs/EPSG/0/{curie_el[1]}' elif str != 'CRS:84': raise CRSError - + return 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' except CRSError as e: - return e + return e + def get_crs_curie(str) -> str: - """ - Get a wms compatible crs curie from a uri or a curie + """ + Get a wms compatible crs curie 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. - :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 + :returns: `crs curie` matching the input crs. + """ + + try: + str = str.lower() + LOGGER.debug(str) + result = urlparse(str) + + 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') - :raises `CRSError`: Error raised if no CRS could be identified from the - URI. + # We support all EPSG crs and CRS84 + if path_el[2] == 'epsg': + return f'EPSG:{path_el[4]}' + elif path_el[4] != 'crs84': + raise CRSError('Unsupported crs') - :returns: `crs curie` matching the input crs. - """ - + return 'CRS:84' + + except CRSError: try: - str = str.lower() - LOGGER.debug(str) - result = urlparse(str) - - 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']: - raise CRSError('Invalid uri fragments') - - # We support all EPSG crs and CRS84 - if path_el[2] == 'epsg': - return f'EPSG:{path_el[4]}' - elif path_el[4] != 'crs84': + # parse safe curies + curie = str.strip('[]') + LOGGER.debug(curie) + + curie_el = curie.split(':') + LOGGER.debug(len(curie_el)) + # We support all EPSG and CRS84 + if len(curie_el + ) > 2 or (curie_el[0] != 'epsg' and curie not in ['crs:84', 'ogc:crs84']): # noqa raise CRSError('Unsupported crs') - - return 'CRS:84' + + if curie in ['crs:84', 'ogc:crs84']: + return 'CRS:84' + + return curie.upper() except CRSError as e: - try: - # parse safe curies - curie = str.strip('[]') - LOGGER.debug(curie) - - curie_el = curie.split(':') - LOGGER.debug(len(curie_el)) - # We support all EPSG and CRS84 - if len(curie_el - ) > 2 or (curie_el[0] != 'epsg' and curie not in ['crs:84','ogc:crs84']): - raise CRSError('Unsupported crs') - - if curie in ['crs:84','ogc:crs84']: - return 'CRS:84' - - return curie.upper() - - except CRSError as e: - return e - - + return e + def get_crs(crs: Union[str, pyproj.CRS]) -> pyproj.CRS: """ From c9fbbd96b2389a2628c7e5aef60068859d4535b3 Mon Sep 17 00:00:00 2001 From: doublebyte Date: Sat, 20 Jun 2026 19:22:38 +0100 Subject: [PATCH 03/15] - Remove list of pre-defined crs and enable parsing uris from safe and unsafe curies --- pygeoapi/api/maps.py | 32 ++++--------- pygeoapi/crs.py | 81 ++++++++++++++++++++------------- pygeoapi/provider/wms_facade.py | 25 +++++----- 3 files changed, 71 insertions(+), 67 deletions(-) diff --git a/pygeoapi/api/maps.py b/pygeoapi/api/maps.py index acd5a1ce4..4256645cf 100644 --- a/pygeoapi/api/maps.py +++ b/pygeoapi/api/maps.py @@ -44,7 +44,7 @@ import logging from typing import Tuple -from pygeoapi.crs import transform_bbox, DEFAULT_CRS, get_crs_curie +from pygeoapi.crs import transform_bbox, DEFAULT_CRS, get_crs_curie, get_crs_uri from pygeoapi.formats import F_JSON, FORMAT_TYPES from pygeoapi.openapi import get_oas_30_parameters from pygeoapi.plugin import load_plugin @@ -64,20 +64,6 @@ DEFAULT_BBOX = [-180, -90, 180, 90] # CRS84 -CRS_CODES = { - '4326': 'http://www.opengis.net/def/crs/EPSG/0/4326', # stop supporting this # noqa - '3857': 'http://www.opengis.net/def/crs/EPSG/0/3857', # stop supporting this # noqa - 'CRS84': 'http://www.opengis.net/def/crs/OGC/1.3/CRS84', # stop supporting this # noqa - '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', - 'OGC:CRS84': 'http://www.opengis.net/def/crs/OGC/1.3/CRS84', -} - - def get_collection_map(api: API, request: APIRequest, dataset: str, style: str | None = None ) -> Tuple[dict, int, str]: @@ -124,12 +110,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) - else: - query_args['crs'] = CRS_CODES.get(request.params['crs'], + query_args['crs'] = collection_def.get('storage_crs', DEFAULT_CRS) - LOGGER.debug(get_crs_curie(request.params['crs'])) + else: + query_args['crs'] = get_crs_uri(request.params['crs']) + except KeyError: query_args['crs'] = DEFAULT_CRS @@ -140,8 +125,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_crs_uri(request.params['bbox-crs']) except KeyError: query_args['bbox-crs'] = DEFAULT_CRS @@ -189,12 +173,12 @@ def get_collection_map(api: API, request: APIRequest, LOGGER.error(exception) return headers, HTTPStatus.BAD_REQUEST, to_json( exception, api.pretty_print) - + # 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') diff --git a/pygeoapi/crs.py b/pygeoapi/crs.py index 488a80e5e..ac16ebe0d 100644 --- a/pygeoapi/crs.py +++ b/pygeoapi/crs.py @@ -121,24 +121,9 @@ def get_supported_crs_list( return supported_crs_list - def get_crs_uri(str) -> str: - - try: - if str.contains('epsg'): - curie_el = str.split(':') - return f'http://www.opengis.net/def/crs/EPSG/0/{curie_el[1]}' - elif str != 'CRS:84': - raise CRSError - - return 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' - except CRSError as e: - return e - - -def get_crs_curie(str) -> str: """ - Get a wms compatible crs curie from a uri or a curie + Parse a uri from a uri, a curie or a safe curie :param crs: Uniform resource identifier of the coordinate reference system. In accordance with @@ -149,14 +134,16 @@ def get_crs_curie(str) -> str: :raises `CRSError`: Error raised if no CRS could be identified from the URI. - :returns: `crs curie` matching the input crs. + :returns: `crs uri` matching the input crs. """ try: str = str.lower() - LOGGER.debug(str) result = urlparse(str) + # If it is a uri, check if it is valid + LOGGER.debug(f'Attempt to parse a uri: {str}') + if result.scheme not in ['http', 'https']: raise CRSError('Invalid uri scheme') if result.netloc is None or result.netloc != 'www.opengis.net': @@ -169,35 +156,65 @@ def get_crs_curie(str) -> str: ) != 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') - # We support all EPSG crs and CRS84 - if path_el[2] == 'epsg': - return f'EPSG:{path_el[4]}' - elif path_el[4] != 'crs84': - raise CRSError('Unsupported crs') - - return 'CRS:84' + return str except CRSError: try: - # parse safe curies + # Parse safe curie curie = str.strip('[]') - LOGGER.debug(curie) + LOGGER.debug(f'Attempt to parse a curie: {curie}') curie_el = curie.split(':') - LOGGER.debug(len(curie_el)) # We support all EPSG and CRS84 if len(curie_el - ) > 2 or (curie_el[0] != 'epsg' and curie not in ['crs:84', 'ogc:crs84']): # noqa + ) > 2 or (curie_el[0] != 'epsg' and curie not in ['crs84', 'ogc:crs84']): # noqa raise CRSError('Unsupported crs') - if curie in ['crs:84', 'ogc:crs84']: - return 'CRS:84' + if curie in ['crs84', 'ogc:crs84']: + LOGGER.debug(f'Returning CRS84') + return 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' - return curie.upper() + LOGGER.debug(f'Returning: http://www.opengis.net/def/crs/EPSG/0/{curie_el[1]}') + return f'http://www.opengis.net/def/crs/EPSG/0/{curie_el[1]}' except CRSError as e: return e + +def get_crs_curie(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 + 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: `wms curie` matching the input uri. + """ + + try: + if not str.startswith(("http://", "https://")): + raise CRSError('Not an uri') + + str = str.lower() + path_el = [p for p in str.split('/') if p] + + # We support all EPSG crs 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: """ diff --git a/pygeoapi/provider/wms_facade.py b/pygeoapi/provider/wms_facade.py index 4a69525eb..ef0b4cd47 100644 --- a/pygeoapi/provider/wms_facade.py +++ b/pygeoapi/provider/wms_facade.py @@ -31,6 +31,9 @@ import logging from urllib.parse import urlencode +from pygeoapi.crs import get_crs_curie +from pyproj.exceptions import CRSError + import pyproj import requests @@ -43,14 +46,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): """WMS 1.3.0 provider""" @@ -69,7 +65,7 @@ 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, + height=300, crs=DEFAULT_WMS_CRS, datetime_=None, transparent=True, format_='png', **kwargs): """ Generate map @@ -87,11 +83,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_crs_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)) @@ -104,7 +107,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, From 7f467adf2bf0bcbd7eeff6a182eb04fbc1e92167 Mon Sep 17 00:00:00 2001 From: doublebyte Date: Sat, 20 Jun 2026 19:31:10 +0100 Subject: [PATCH 04/15] - fixed flake8 errors --- pygeoapi/api/maps.py | 9 +++++---- pygeoapi/crs.py | 7 +++---- pygeoapi/provider/wms_facade.py | 7 ++++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pygeoapi/api/maps.py b/pygeoapi/api/maps.py index 4256645cf..3cf43f70e 100644 --- a/pygeoapi/api/maps.py +++ b/pygeoapi/api/maps.py @@ -44,7 +44,7 @@ import logging from typing import Tuple -from pygeoapi.crs import transform_bbox, DEFAULT_CRS, get_crs_curie, get_crs_uri +from pygeoapi.crs import transform_bbox, DEFAULT_CRS, get_crs_uri from pygeoapi.formats import F_JSON, FORMAT_TYPES from pygeoapi.openapi import get_oas_30_parameters from pygeoapi.plugin import load_plugin @@ -64,6 +64,7 @@ DEFAULT_BBOX = [-180, -90, 180, 90] # CRS84 + def get_collection_map(api: API, request: APIRequest, dataset: str, style: str | None = None ) -> Tuple[dict, int, str]: @@ -111,9 +112,9 @@ def get_collection_map(api: API, request: APIRequest, try: if 'crs' not in request.params: query_args['crs'] = collection_def.get('storage_crs', - DEFAULT_CRS) + DEFAULT_CRS) else: - query_args['crs'] = get_crs_uri(request.params['crs']) + query_args['crs'] = get_crs_uri(request.params['crs']) except KeyError: query_args['crs'] = DEFAULT_CRS @@ -173,7 +174,7 @@ def get_collection_map(api: API, request: APIRequest, LOGGER.error(exception) return headers, HTTPStatus.BAD_REQUEST, to_json( exception, api.pretty_print) - + # the transformer function expects the crs to be in a uri format if query_args['bbox-crs'] != query_args['crs']: bbox = transform_bbox(bbox, query_args['bbox-crs'], diff --git a/pygeoapi/crs.py b/pygeoapi/crs.py index ac16ebe0d..102ff78b1 100644 --- a/pygeoapi/crs.py +++ b/pygeoapi/crs.py @@ -121,6 +121,7 @@ def get_supported_crs_list( return supported_crs_list + def get_crs_uri(str) -> str: """ Parse a uri from a uri, a curie or a safe curie @@ -171,16 +172,14 @@ def get_crs_uri(str) -> str: raise CRSError('Unsupported crs') if curie in ['crs84', 'ogc:crs84']: - LOGGER.debug(f'Returning CRS84') return 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' - LOGGER.debug(f'Returning: http://www.opengis.net/def/crs/EPSG/0/{curie_el[1]}') return f'http://www.opengis.net/def/crs/EPSG/0/{curie_el[1]}' except CRSError as e: return e - + def get_crs_curie(str) -> str: """ Get a wms compatible crs curie from a uri @@ -202,7 +201,7 @@ def get_crs_curie(str) -> str: raise CRSError('Not an uri') str = str.lower() - path_el = [p for p in str.split('/') if p] + path_el = [p for p in str.split('/') if p] # We support all EPSG crs and CRS84 if path_el[4] == 'epsg': diff --git a/pygeoapi/provider/wms_facade.py b/pygeoapi/provider/wms_facade.py index ef0b4cd47..5ddbe2f7c 100644 --- a/pygeoapi/provider/wms_facade.py +++ b/pygeoapi/provider/wms_facade.py @@ -48,6 +48,7 @@ DEFAULT_WMS_CRS = 'CRS:84' + class WMSFacadeProvider(BaseProvider): """WMS 1.3.0 provider""" @@ -64,9 +65,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_WMS_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 From f287f2e757b58d1f0cca3e0cd211bfc026740d5f Mon Sep 17 00:00:00 2001 From: doublebyte Date: Sat, 20 Jun 2026 20:18:46 +0100 Subject: [PATCH 05/15] - updated unit test --- tests/provider/test_wms_facade_provider.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/provider/test_wms_facade_provider.py b/tests/provider/test_wms_facade_provider.py index dd829900e..de47c6576 100644 --- a/tests/provider/test_wms_facade_provider.py +++ b/tests/provider/test_wms_facade_provider.py @@ -31,7 +31,7 @@ import pytest from pygeoapi.provider.wms_facade import WMSFacadeProvider - +from pygeoapi.provider.base import ProviderQueryError @pytest.fixture() def config(): @@ -61,12 +61,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') From 202471689fbf7d56c002cd6536bdc065bcdc89b5 Mon Sep 17 00:00:00 2001 From: doublebyte Date: Sat, 20 Jun 2026 20:25:16 +0100 Subject: [PATCH 06/15] - fixed flake8 error --- tests/provider/test_wms_facade_provider.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/provider/test_wms_facade_provider.py b/tests/provider/test_wms_facade_provider.py index de47c6576..6c99b755e 100644 --- a/tests/provider/test_wms_facade_provider.py +++ b/tests/provider/test_wms_facade_provider.py @@ -33,6 +33,7 @@ from pygeoapi.provider.wms_facade import WMSFacadeProvider from pygeoapi.provider.base import ProviderQueryError + @pytest.fixture() def config(): return { From 707a3f4aafc50d91b2e597265d5e793c6081182e Mon Sep 17 00:00:00 2001 From: doublebyte Date: Sat, 20 Jun 2026 20:49:51 +0100 Subject: [PATCH 07/15] - updated docs --- docs/source/publishing/ogcapi-maps.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/source/publishing/ogcapi-maps.rst b/docs/source/publishing/ogcapi-maps.rst index e68b4eec1..686aeac95 100644 --- a/docs/source/publishing/ogcapi-maps.rst +++ b/docs/source/publishing/ogcapi-maps.rst @@ -118,14 +118,14 @@ required. An optional style name can be defined via `options.style`. .. note:: According to the `Standard `_, 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) + - `CRS84` (unsafe) + - `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`. From 8842e5f286bc062302f1d4676dbe534eb99b5873 Mon Sep 17 00:00:00 2001 From: doublebyte Date: Sun, 21 Jun 2026 16:46:05 +0100 Subject: [PATCH 08/15] - switched to accept CRS:84 (for compatibility with WMS) and not CRS84 --- docs/source/publishing/ogcapi-maps.rst | 2 +- pygeoapi/crs.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/publishing/ogcapi-maps.rst b/docs/source/publishing/ogcapi-maps.rst index 686aeac95..305d14da3 100644 --- a/docs/source/publishing/ogcapi-maps.rst +++ b/docs/source/publishing/ogcapi-maps.rst @@ -124,7 +124,7 @@ required. An optional style name can be defined via `options.style`. - `http://www.opengis.net/def/crs/EPSG/0/4326` - `[EPSG:4326]` - `EPSG:4326` (unsafe) - - `CRS84` (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`. diff --git a/pygeoapi/crs.py b/pygeoapi/crs.py index 102ff78b1..98bffe600 100644 --- a/pygeoapi/crs.py +++ b/pygeoapi/crs.py @@ -168,10 +168,10 @@ def get_crs_uri(str) -> str: curie_el = curie.split(':') # We support all EPSG and CRS84 if len(curie_el - ) > 2 or (curie_el[0] != 'epsg' and curie not in ['crs84', 'ogc:crs84']): # noqa + ) > 2 or (curie_el[0] != 'epsg' and curie not in ['crs:84', 'ogc:crs84']): # noqa raise CRSError('Unsupported crs') - if curie in ['crs84', 'ogc:crs84']: + if curie in ['crs:84', 'ogc:crs84']: return 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' return f'http://www.opengis.net/def/crs/EPSG/0/{curie_el[1]}' From a99c03b9cff4e376e9da2b8abc95e78bcf2b82e3 Mon Sep 17 00:00:00 2001 From: doublebyte Date: Sun, 21 Jun 2026 16:49:54 +0100 Subject: [PATCH 09/15] - capitalised curie and crs --- docs/source/publishing/ogcapi-maps.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/publishing/ogcapi-maps.rst b/docs/source/publishing/ogcapi-maps.rst index 305d14da3..ad326afb6 100644 --- a/docs/source/publishing/ogcapi-maps.rst +++ b/docs/source/publishing/ogcapi-maps.rst @@ -118,8 +118,8 @@ required. An optional style name can be defined via `options.style`. .. note:: According to the `Standard `_, OGC API - Maps - 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. + 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]` From 65e29ad186f83d9cf2c91f6bf6a791f6e3a6b5df Mon Sep 17 00:00:00 2001 From: doublebyte Date: Sun, 21 Jun 2026 17:17:34 +0100 Subject: [PATCH 10/15] - capitalised crs, curie - replaced "str" variable name by "crs" --- pygeoapi/crs.py | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/pygeoapi/crs.py b/pygeoapi/crs.py index 98bffe600..e45b6ff8c 100644 --- a/pygeoapi/crs.py +++ b/pygeoapi/crs.py @@ -122,9 +122,9 @@ def get_supported_crs_list( return supported_crs_list -def get_crs_uri(str) -> str: +def get_crs_uri(crs) -> str: """ - Parse a uri from a uri, a curie or a safe curie + Parse a uri from a uri or a curie :param crs: Uniform resource identifier of the coordinate reference system. In accordance with @@ -135,15 +135,15 @@ def get_crs_uri(str) -> str: :raises `CRSError`: Error raised if no CRS could be identified from the URI. - :returns: `crs uri` matching the input crs. + :returns: `crs uri` matching the input CRS. """ try: - str = str.lower() - result = urlparse(str) + crs = crs.lower() + result = urlparse(crs) # If it is a uri, check if it is valid - LOGGER.debug(f'Attempt to parse a uri: {str}') + LOGGER.debug(f'Attempt to parse a uri: {crs}') if result.scheme not in ['http', 'https']: raise CRSError('Invalid uri scheme') @@ -157,19 +157,19 @@ def get_crs_uri(str) -> str: ) != 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 str + return crs except CRSError: try: - # Parse safe curie - curie = str.strip('[]') + # Parse safe CURIE + curie = crs.strip('[]') LOGGER.debug(f'Attempt to parse a curie: {curie}') curie_el = curie.split(':') - # We support all EPSG and CRS84 + # We support all EPSG codes and CRS84 if len(curie_el ) > 2 or (curie_el[0] != 'epsg' and curie not in ['crs:84', 'ogc:crs84']): # noqa - raise CRSError('Unsupported crs') + raise CRSError('Unsupported CRS') if curie in ['crs:84', 'ogc:crs84']: return 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' @@ -180,34 +180,32 @@ def get_crs_uri(str) -> str: return e -def get_crs_curie(str) -> str: +def get_crs_curie(crs) -> str: """ - Get a wms compatible crs curie from a uri + 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 - 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: `wms curie` matching the input uri. + :returns: `WMS CURIE` matching the input uri. """ try: - if not str.startswith(("http://", "https://")): + if not crs.startswith(("http://", "https://")): raise CRSError('Not an uri') - str = str.lower() - path_el = [p for p in str.split('/') if p] + crs = crs.lower() + path_el = [p for p in crs.split('/') if p] - # We support all EPSG crs and CRS84 + # 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') + raise CRSError('Unsupported CRS') return 'CRS:84' From c18f630477d5a735b45c588d54ee48f84453bb8f Mon Sep 17 00:00:00 2001 From: doublebyte Date: Sun, 21 Jun 2026 17:20:43 +0100 Subject: [PATCH 11/15] - reordered library import --- pygeoapi/provider/wms_facade.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pygeoapi/provider/wms_facade.py b/pygeoapi/provider/wms_facade.py index 5ddbe2f7c..d9113ac81 100644 --- a/pygeoapi/provider/wms_facade.py +++ b/pygeoapi/provider/wms_facade.py @@ -31,9 +31,10 @@ import logging from urllib.parse import urlencode -from pygeoapi.crs import get_crs_curie from pyproj.exceptions import CRSError +from pygeoapi.crs import get_crs_curie + import pyproj import requests From 03caae3fa53e7b20b3eca4b6727532fc1da7971c Mon Sep 17 00:00:00 2001 From: doublebyte Date: Mon, 22 Jun 2026 12:32:16 +0100 Subject: [PATCH 12/15] - refactored condition logic for performance and clarity - store strip results directly on variables, since we do not need the full qualified array. --- pygeoapi/crs.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pygeoapi/crs.py b/pygeoapi/crs.py index e45b6ff8c..c9d978a73 100644 --- a/pygeoapi/crs.py +++ b/pygeoapi/crs.py @@ -165,16 +165,17 @@ def get_crs_uri(crs) -> str: curie = crs.strip('[]') LOGGER.debug(f'Attempt to parse a curie: {curie}') - curie_el = curie.split(':') # We support all EPSG codes and CRS84 - if len(curie_el - ) > 2 or (curie_el[0] != 'epsg' and curie not in ['crs:84', 'ogc:crs84']): # noqa - raise CRSError('Unsupported CRS') + 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') - if curie in ['crs:84', 'ogc:crs84']: + return f'http://www.opengis.net/def/crs/EPSG/0/{curie_code}' + else: return 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' - return f'http://www.opengis.net/def/crs/EPSG/0/{curie_el[1]}' + except CRSError as e: return e From df25afcb1bb0c178c4c389e72f8315e5424cd9c8 Mon Sep 17 00:00:00 2001 From: doublebyte Date: Mon, 22 Jun 2026 12:40:19 +0100 Subject: [PATCH 13/15] - renamed get_crs_uri and get_crs_curie for consistency with other function names --- pygeoapi/api/maps.py | 6 +++--- pygeoapi/crs.py | 4 ++-- pygeoapi/provider/wms_facade.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pygeoapi/api/maps.py b/pygeoapi/api/maps.py index 3cf43f70e..23ce03ca1 100644 --- a/pygeoapi/api/maps.py +++ b/pygeoapi/api/maps.py @@ -44,7 +44,7 @@ import logging from typing import Tuple -from pygeoapi.crs import transform_bbox, DEFAULT_CRS, get_crs_uri +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 @@ -114,7 +114,7 @@ def get_collection_map(api: API, request: APIRequest, query_args['crs'] = collection_def.get('storage_crs', DEFAULT_CRS) else: - query_args['crs'] = get_crs_uri(request.params['crs']) + query_args['crs'] = get_uri(request.params['crs']) except KeyError: query_args['crs'] = DEFAULT_CRS @@ -126,7 +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'] = get_crs_uri(request.params['bbox-crs']) + query_args['bbox-crs'] = get_uri(request.params['bbox-crs']) except KeyError: query_args['bbox-crs'] = DEFAULT_CRS diff --git a/pygeoapi/crs.py b/pygeoapi/crs.py index c9d978a73..65fd6fd0d 100644 --- a/pygeoapi/crs.py +++ b/pygeoapi/crs.py @@ -122,7 +122,7 @@ def get_supported_crs_list( return supported_crs_list -def get_crs_uri(crs) -> str: +def get_uri(crs) -> str: """ Parse a uri from a uri or a curie @@ -181,7 +181,7 @@ def get_crs_uri(crs) -> str: return e -def get_crs_curie(crs) -> str: +def get_curie(crs) -> str: """ Get a WMS compatible CRS CURIE from a uri diff --git a/pygeoapi/provider/wms_facade.py b/pygeoapi/provider/wms_facade.py index d9113ac81..429950b07 100644 --- a/pygeoapi/provider/wms_facade.py +++ b/pygeoapi/provider/wms_facade.py @@ -33,7 +33,7 @@ from pyproj.exceptions import CRSError -from pygeoapi.crs import get_crs_curie +from pygeoapi.crs import get_curie import pyproj import requests @@ -92,7 +92,7 @@ def query(self, style=None, bbox=[-180, -90, 180, 90], version = self.options.get('version', '1.3.0') try: - wms_crs = get_crs_curie(crs) + wms_crs = get_curie(crs) except CRSError: wms_crs = DEFAULT_WMS_CRS From a538d4380213c369527de6582ede39e10d453ade Mon Sep 17 00:00:00 2001 From: doublebyte Date: Mon, 22 Jun 2026 12:48:20 +0100 Subject: [PATCH 14/15] - added type hints on function arguments --- pygeoapi/crs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygeoapi/crs.py b/pygeoapi/crs.py index 65fd6fd0d..a385812f8 100644 --- a/pygeoapi/crs.py +++ b/pygeoapi/crs.py @@ -122,7 +122,7 @@ def get_supported_crs_list( return supported_crs_list -def get_uri(crs) -> str: +def get_uri(crs: str) -> str: """ Parse a uri from a uri or a curie @@ -181,7 +181,7 @@ def get_uri(crs) -> str: return e -def get_curie(crs) -> str: +def get_curie(crs: str) -> str: """ Get a WMS compatible CRS CURIE from a uri From 2ffcebacb49a407a124a47ce540f6c08c934712f Mon Sep 17 00:00:00 2001 From: doublebyte Date: Mon, 22 Jun 2026 12:50:18 +0100 Subject: [PATCH 15/15] - fixed flake8 errors --- pygeoapi/crs.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pygeoapi/crs.py b/pygeoapi/crs.py index a385812f8..ca0a28a4d 100644 --- a/pygeoapi/crs.py +++ b/pygeoapi/crs.py @@ -175,8 +175,6 @@ def get_uri(crs: str) -> str: else: return 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' - - except CRSError as e: return e