Skip to content

Updates to the OGC API - Maps Support#2308

Open
doublebyte1 wants to merge 16 commits intogeopython:masterfrom
doublebyte1:map-updates
Open

Updates to the OGC API - Maps Support#2308
doublebyte1 wants to merge 16 commits intogeopython:masterfrom
doublebyte1:map-updates

Conversation

@doublebyte1
Copy link
Copy Markdown
Contributor

@doublebyte1 doublebyte1 commented Mar 30, 2026

Overview

  • Introduced some guardrails for WebMercator, to prevent issues with out of bounds.
  • Since the WMS Facade does not support a bbox-crs different than the crs, there is no need to pass a bbox-crs to the provider; however we must ensure that we always pass the bbox in crs coordinates (transform, if needed).
  • Support default values for crs, bbox-crs and bbox.
  • Add support for storage_crs, as a configuration option.
  • Add Content-Crs and Content-Bbox headers to the response
  • Catchup with tests and documentation

This PR also addresses a current issue with the map preview on the collection page, which affects the demo server.

Additional information

In the collection page, we only show the preview map when the view is within the limits of the crs:

Screenshot from 2026-04-08 19-37-07 Screenshot from 2026-03-31 09-14-21 Screenshot from 2026-03-31 09-14-34

TODOS:

  • update unit tests
  • update docs

Dependency policy (RFC2)

  • I have ensured that this PR meets RFC2 requirements

Updates to public demo

I created an additional PR here, to add the storage_crs configuration option.

Contributions and licensing

(as per https://github.com/geopython/pygeoapi/blob/master/CONTRIBUTING.md#contributions-and-licensing)

  • I'd like to contribute [feature X|bugfix Y|docs|something else] to pygeoapi. I confirm that my contributions to pygeoapi will be compatible with the pygeoapi license guidelines at the time of contribution
  • I have already previously agreed to the pygeoapi Contributions and Licensing Guidelines

@doublebyte1 doublebyte1 marked this pull request as draft March 30, 2026 17:56
@webb-ben
Copy link
Copy Markdown
Member

Should the bbox-crs not be used to convert the bbox into the requested crs?

@doublebyte1
Copy link
Copy Markdown
Contributor Author

doublebyte1 commented Mar 31, 2026

Should the bbox-crs not be used to convert the bbox into the requested crs?

Whenever it exists, yes; but this is an optional parameter, so we should have a fallback if we want to support a minimal map query (e.g.: no crs, no bbox-crs, no bbox).

https://github.com/doublebyte1/pygeoapi/blob/b581b8cd93bf8691a5adee34c7ce8fd425784ae0/pygeoapi/api/maps.py#L120-129

@webb-ben
Copy link
Copy Markdown
Member

Makes some sense to me. Is there a reason to derive the default CRS from bbox or should we align this with crs list and storage_crs as implemented for OAFeat?

@doublebyte1
Copy link
Copy Markdown
Contributor Author

Makes some sense to me. Is there a reason to derive the default CRS from bbox or should we align this with crs list and storage_crs as implemented for OAFeat?

If we don' t have bbox-crs and bbox as parameters, we still need some defaults to provide a map to the user. The defaults are taken from what is defined on the spatial extents of the collections (bbox and bbox-crs), but I am open to more ideas (:

Examples:
https://demo.pygeoapi.io/master/collections/mapserver_world_map/map?f=png&crs=4326
https://demo.pygeoapi.io/master/collections/mapserver_world_map/map?f=png

If the spatial extents of the collection are not defined in the config, I also defined some defaults. But I am actually not sure, if that is even possible?

In the case of our WMS providers, the storage_crs make less sense, because they are basically facades.

@webb-ben
Copy link
Copy Markdown
Member

webb-ben commented Apr 1, 2026

Ahh. Using bbox and bbox-crs in that context makes sense. I was more thinking along the lines of conformance with https://docs.pygeoapi.io/en/latest/crs.html

@webb-ben
Copy link
Copy Markdown
Member

webb-ben commented Apr 1, 2026

Does the map emit its bbox and CRS? It is my understand that to know how to draw a map the client would need to know the extents of the map which I presume is done by the client dictating to the server these values.

Is /map without query args a special case where OAMap allows for 200 response without overloading the query args or is there some other way the projected CRS and extent of the map are returned to a client?

@doublebyte1
Copy link
Copy Markdown
Contributor Author

Does the map emit its bbox and CRS? It is my understand that to know how to draw a map the client would need to know the extents of the map which I presume is done by the client dictating to the server these values.

Is /map without query args a special case where OAMap allows for 200 response without overloading the query args or is there some other way the projected CRS and extent of the map are returned to a client?

I see your point now. No, we do not give that information back to the client. This is what is written in the Standard:

NOTE 1: When no crs parameters are specified, please refer to the Maps API core conformance class to know about the default CRS

NOTE 2: The default CRS of the BBOX is https://www.opengis.net/def/crs/OGC/1.3/CRS84 but the default CRS of the map is the native (storage) CRS.

Also, the bounding box is not optional. I need to update the code to reflect that, and the fact that the default crs of the bounding box is CRS84, rather than 4326. Regarding the native storage crs, I guess we need the user to declare it in the config? there is a layer crs in the GetCapabilities of the WMS service.

@doublebyte1
Copy link
Copy Markdown
Contributor Author

Does the map emit its bbox and CRS? It is my understand that to know how to draw a map the client would need to know the extents of the map which I presume is done by the client dictating to the server these values.

Is /map without query args a special case where OAMap allows for 200 response without overloading the query args or is there some other way the projected CRS and extent of the map are returned to a client?

You were right in your observations (: There is a map collection storage_crs in the Standard, and that should be the default crs; in case it does not exist, the server should default to CRS84 .

oam-crs

In the case of bbox-crs, it should just default to CRS84.

oam-bbox-crs

Ultimately, the crs and the bounding box are returned to the user in the response headers.

Content-Crs: http://www.opengis.net/def/crs/EPSG/0/4326
Content-Bbox: -180.0,-90.0,180.0,90.0

I reflected this logic in my latest commits.

@doublebyte1
Copy link
Copy Markdown
Contributor Author

CI failures seem unrelated

@doublebyte1 doublebyte1 marked this pull request as ready for review May 4, 2026 17:09
Comment thread pygeoapi/api/maps.py Outdated
]

DEFAULT_CRS = 'http://www.opengis.net/def/crs/EPSG/0/4326'
DEFAULT_CRS = 'http://www.opengis.net/def/crs/OGC/1.3/CRS84'
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.

Can these be a part of pygeoapi.crs? We have the concept of DEFAULT_STORAGE_CRS and DEFAULT_CRS already.

Comment thread pygeoapi/api/maps.py
'crs', collection_def.get('crs', DEFAULT_CRS))]
query_args['bbox_crs'] = CRS_CODES[request.params.get(
'bbox-crs', collection_def.get('crs', DEFAULT_CRS))]
query_args['transparent'] = request.params.get('transparent', True)
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.

Keep transparency arg parse?

Comment thread pygeoapi/api/maps.py Outdated
bbox = transform_bbox(bbox, query_args['bbox_crs'], query_args['crs'])

bbox = transform_bbox(bbox, query_args['bbox-crs'],
query_args['crs'], True)
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.

Suggested change
query_args['crs'], True)
query_args['crs'], always_xy=True)

Might be worth keeping this as a keyword argument to maintain clarity about what is happening here

Comment thread pygeoapi/provider/wms_facade.py Outdated
'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/EPSG/0/3857': 'EPSG:3857',
'http://www.opengis.net/def/crs/OGC/1.3/CRS84': 'urn:ogc:def:crs:OGC:1.3:CRS84' # noqa
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.

I get an error when I try to use a URN for WMS. The error says the CRS should be in the format AUTHORITY:CODE

@doublebyte1
Copy link
Copy Markdown
Contributor Author

@webb-ben Thanks a lot for the quick review! 👍🏽 Those are all great comments, addressed in my latest commits.

@doublebyte1 doublebyte1 requested a review from webb-ben May 4, 2026 19:10
Copy link
Copy Markdown
Member

@webb-ben webb-ben left a comment

Choose a reason for hiding this comment

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

+1. Wonder if we could just draw the image with default bbox on the map once for those lower zooms. Starts to become a whole hack though.

diff --git a/pygeoapi/templates/collections/collection.html b/pygeoapi/templates/collections/collection.html
index 1d522c3..887eebe 100644
--- a/pygeoapi/templates/collections/collection.html
+++ b/pygeoapi/templates/collections/collection.html
@@ -169,6 +169,7 @@
     var clampedBounds = L.latLngBounds(clampedSw, clampedNe);
 
     var ogcapi_layer = null;
+    var image_layer = null;
 
     {# if this collection has a map representation, add it to the map #}
     {% for link in data['links'] %}
@@ -179,12 +180,12 @@
             "transparent": true, 
             "bounds": clampedBounds 
         });
+        image_layer = L.imageOverlay("{{ link['href'] }}", clampedBounds, {opacity: 0.7, transparent: true});
         bbox_layer.setStyle({
             fillOpacity: 0
         });
       {% endif %}
     {% endfor %}
-
     // Check bounds and toggle the visibility of the imageOverlay, accordingly
     function toggleOverlayVisibility() {
 
@@ -199,9 +200,9 @@
         var isWithinBounds = (viewWidth <= 360) && (centerLng >= -180 && centerLng <= 180);
 
             if (isWithinBounds) {
-                if (!map.hasLayer(ogcapi_layer)) map.addLayer(ogcapi_layer);
+                if (!map.hasLayer(ogcapi_layer)) map.addLayer(ogcapi_layer); map.removeLayer(image_layer);
             } else {
-                if (map.hasLayer(ogcapi_layer)) map.removeLayer(ogcapi_layer);
+                if (map.hasLayer(ogcapi_layer)) map.removeLayer(ogcapi_layer); map.addLayer(image_layer);
             }
         }
     }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants