Skip to content

Commit

Permalink
Add OGC API Features support
Browse files Browse the repository at this point in the history
  • Loading branch information
llienher committed Nov 15, 2024
1 parent 56b4872 commit 43f1615
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 22 deletions.
4 changes: 3 additions & 1 deletion admin/c2cgeoportal_admin/schemas/dimensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
from c2cgeoportal_commons.models.main import Dimension


def dimensions_schema_node(prop: InstrumentedAttribute[Any]) -> colander.SequenceSchema:
def dimensions_schema_node(
prop: InstrumentedAttribute[Any], # pylint: disable=unsubscriptable-object`
) -> colander.SequenceSchema: # pylint: disable=unsubscriptable-object`
"""Get the scheme of the dimensions."""
return colander.SequenceSchema(
GeoFormSchemaNode(Dimension, name="dimension", widget=MappingWidget(template="dimension")),
Expand Down
4 changes: 3 additions & 1 deletion admin/c2cgeoportal_admin/schemas/restriction_areas.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
from c2cgeoportal_commons.models.main import RestrictionArea


def restrictionareas_schema_node(prop: InstrumentedAttribute[Any]) -> colander.SequenceSchema:
def restrictionareas_schema_node(
prop: InstrumentedAttribute[Any], # pylint: disable=unsubscriptable-object`
) -> colander.SequenceSchema: # pylint: disable=unsubscriptable-object`
"""Get the schema of a restriction area."""
return colander.SequenceSchema(
GeoFormManyToManySchemaNode(RestrictionArea, None),
Expand Down
1 change: 1 addition & 0 deletions doc/integrator/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ This chapter describes advanced configuration settings.
objectstorage
headers
admin_interface
ogc_api
52 changes: 52 additions & 0 deletions doc/integrator/ogc_api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.. _integrator_ogc_api:

OGC API Features
----------------

The OGC API is a new standard that defines modular API following REST standard to access spatial data.
The current implementation is only on the server side and is based on the OGC API - Features standard and only works with the `items` request using a bbox filter.

It is currently supported by both MapServer and QGIS Server.

The following new environment variables should be added to the docker-compose-lib.yaml file for the 'MapServer' service:

.. prompt:: bash::

environment:
...
- MAPSERVER_BASE_PATH=/mapserv_proxy
- OGCAPI_HTML_TEMPLATE_DIRECTORY=/usr/local/share/mapserver/ogcapi/templates/html-bootstrap4/
...


In the MapServer configuration file, the following changes could be added to set an alias to a the MAPS block:
.. prompt:: bash::

#
# Map aliases
#
MAPS
ExampleOGCserver "/etc/mapserver/mapserver.map"
END

Then used this alias as the ogc-server value in the URL for MapServer.

It is also recommended for any server type, to remove all special characters in the OGC Server names because they appear in the path of the URL.

QGIS Server and MapServer are already configured to support the OGC API and detect automatically if the compatible path is requested.

Landing pages for both MapServer and QGIS Server are not supported yet.

OGC API features are accessible through mapserv_proxy, with the following URLs:
* ``/mapserv_proxy/<ogc-server>/ogcapi/*``: The MapServer path.
* ``/mapserv_proxy/<ogc-server>/wfs3/*``: The QGIS Server path.


OGC API Documentation
---------------------

MapServer documentation:
https://mapserver.org/ogc/ogc_api.html

QGIS Server documentation:
https://docs.qgis.org/3.34/en/docs/server_manual/services/ogcapif.html
17 changes: 16 additions & 1 deletion geoportal/c2cgeoportal_geoportal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ def handle(event: InvalidateCacheEvent) -> None:
pregenerator=C2CPregenerator(role=True),
request_method="POST",
)
# The tow next views are used to serve the application on the URL /mapserv_proxy/<ogc server name>
# The two next views are used to serve the application on the URL /mapserv_proxy/<ogc server name>
# instead of /mapserv_proxy?ogcserver=<ogc server name>, required for QGIS server landing page
config.add_route(
"mapserverproxy_get_path",
Expand All @@ -677,6 +677,21 @@ def handle(event: InvalidateCacheEvent) -> None:
pregenerator=C2CPregenerator(role=True),
request_method="POST",
)
# OGC Api routes
config.add_route(
"mapserverproxy_ogcapi_mapserver",
"/mapserv_proxy/{ogcserver}/ogcapi/*path",
mapserverproxy=True,
pregenerator=C2CPregenerator(role=True),
request_method="GET",
)
config.add_route(
"mapserverproxy_ogcapi_qgisserver",
"/mapserv_proxy/{ogcserver}/wfs3/*path",
mapserverproxy=True,
pregenerator=C2CPregenerator(role=True),
request_method="GET",
)
add_cors_route(config, "/mapserv_proxy", "mapserver")

# Add route to the tinyows proxy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ services:
environment:
- PGOPTIONS
- MAPSERVER_CONFIG_FILE=/etc/mapserver/mapserver.conf
- MAPSERVER_BASE_PATH=/mapserv_proxy
- OGCAPI_HTML_TEMPLATE_DIRECTORY=/usr/local/share/mapserver/ogcapi/templates/html-bootstrap4/
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
- AWS_DEFAULT_REGION
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,57 @@
("https://front/c2c/health_check", {"checker": "check_collector"}, 2),
("https://front/admin/layertree", {}, 10),
("https://front/admin/layertree/children", {}, 10),
("http://mapserver:8080/mapserv_proxy", {"SERVICE": "WMS", "REQUEST": "GetCapabilities"}, 60),
(
"https://mapserver:8080/mapserv_proxy",
{"SERVICE": "WMS", "REQUEST": "GetCapabilities"},
60,
),
(
"https://front/mapserv_proxy",
{"ogcserver": "source for image/png", "SERVICE": "WMS", "REQUEST": "GetCapabilities"},
60,
),
(
"https://front/mapserv_proxy",
{"ogcserver": "Main PNG", "SERVICE": "WMS", "REQUEST": "GetCapabilities"},
60,
),
(
"https://front/mapserv_proxy",
{"ogcserver": "QGIS server", "SERVICE": "WMS", "REQUEST": "GetCapabilities"},
60,
),
(
"https://mapserver:8080/mapserv_proxy/",
{"SERVICE": "WMS", "REQUEST": "GetCapabilities", "MAP": "MainPNG"},
60,
),
(
"https://qgisserver:8080/mapserv_proxy/",
{"SERVICE": "WMS", "REQUEST": "GetCapabilities", "MAP": "/etc/qgisserver/project.qgs"},
60,
),
# OGC API - Features
(
"https://mapserver:8080/mapserv_proxy/MainPNG/ogcapi/collections/osm_open/items",
{"ogcserver": "Main PNG", "bbox": "6.0,46.0,7.0,47.0", "limit": "100"},
60,
),
(
"https://qgisserver:8080/mapserv_proxy/wfs3/collections/points/items",
{"map": "/etc/qgisserver/project.qgs", "bbox": "6.0,46.0,7.0,47.0", "limit": "100"},
60,
),
(
"https://front/mapserv_proxy/MainPNG/ogcapi/collections/osm_open/items",
{"ogcserver": "Main PNG", "bbox": "6.0,46.0,7.0,47.0", "limit": "100"},
60,
),
(
"https://front/mapserv_proxy/QGIS%20server/ogcapi/collections/points/items",
{"ogcserver": "QGIS server", "bbox": "6.0,46.0,7.0,47.0", "limit": "100"},
60,
),
],
)
def test_url(url: str, params: dict[str, str], timeout: int) -> None:
Expand Down
83 changes: 65 additions & 18 deletions geoportal/c2cgeoportal_geoportal/views/mapserverproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,24 +74,7 @@ def proxy(self) -> Response:
# GetFeatureInfo requests. For GetLegendGraphic requests we do not send layer_name, but MapServer
# should not use the DATA string for GetLegendGraphic.

if self.ogc_server.auth == main.OGCSERVER_AUTH_STANDARD:
self.params["role_ids"] = ",".join([str(e) for e in get_roles_id(self.request)])

# In some application we want to display the features owned by a user than we need his id.
self.params["user_id"] = self.user.id if self.user is not None else "-1"

# Do not allows direct variable substitution
for k in list(self.params.keys()):
if len(k) > 1 and k[:2].capitalize() == "S_":
_LOG.warning("Direct substitution not allowed (%s=%s).", k, self.params[k])
del self.params[k]

if (
self.ogc_server.auth == main.OGCSERVER_AUTH_STANDARD
and self.ogc_server.type == main.OGCSERVER_TYPE_MAPSERVER
):
# Add functionalities params
self.params.update(get_mapserver_substitution_params(self.request))
self._setup_auth()

# Get method
method = self.request.method
Expand Down Expand Up @@ -165,6 +148,70 @@ def proxy(self) -> Response:

return response

def _setup_auth(self) -> None:
if self.ogc_server.auth == main.OGCSERVER_AUTH_STANDARD:
self.params["role_ids"] = ",".join([str(e) for e in get_roles_id(self.request)])

# In some application we want to display the features owned by a user than we need his id.
self.params["user_id"] = self.user.id if self.user is not None else "-1"

# Do not allows direct variable substitution
for k in list(self.params.keys()):
if len(k) > 1 and k[:2].capitalize() == "S_":
_LOG.warning("Direct substitution not allowed (%s=%s).", k, self.params[k])
del self.params[k]

if (
self.ogc_server.auth == main.OGCSERVER_AUTH_STANDARD
and self.ogc_server.type == main.OGCSERVER_TYPE_MAPSERVER
):
# Add functionalities params
self.params.update(get_mapserver_substitution_params(self.request))

@view_config(route_name="mapserverproxy_ogcapi_mapserver") # type: ignore
def proxy_ogcapi_mapserver(self) -> Response:
return self.proxy_ogcapi("ogcapi")

@view_config(route_name="mapserverproxy_ogcapi_qgisserver") # type: ignore
def proxy_ogcapi_qgisserver(self) -> Response:
return self.proxy_ogcapi("wfs3")

def proxy_ogcapi(self, subpath: str) -> Response:
self._setup_auth()

use_cache = False

errors: set[str] = set()

_url = self._get_wfs_url(errors)
if _url is not None:
_url.path = "/".join([_url.path.rstrip("/"), subpath, *self.request.matchdict["path"]])
_LOG.warning("URL: %s", _url)
_LOG.warning(self.request.matchdict)

if _url is None:
_LOG.error("Error getting the URL:\n%s", "\n".join(errors))
raise HTTPInternalServerError()

cache_control = Cache.PRIVATE_NO

headers = self.get_headers()
# Add headers for Geoserver
if self.ogc_server.auth == main.OGCSERVER_AUTH_GEOSERVER and self.user is not None:
headers["sec-username"] = self.user.username
headers["sec-roles"] = ";".join(get_roles_name(self.request))

response = self._proxy_callback(
cache_control,
url=_url,
params=self.params,
cache=use_cache,
headers=headers,
body=self.request.body,
)

return response

def _proxy_callback(
self, cache_control: Cache, url: Url, params: dict[str, str], **kwargs: Any
) -> Response:
Expand Down

0 comments on commit 43f1615

Please sign in to comment.