From d091df83903304683fad33c39b05ed6d31340fe4 Mon Sep 17 00:00:00 2001 From: Jakob Miksch Date: Thu, 29 Aug 2024 14:37:46 +0200 Subject: [PATCH] Enable WMS/WFS publication --- docs/src/qsa-api/endpoints/projects.md | 64 +++++++- qsa-api/qsa_api/api/projects.py | 217 +++++++++++++++++++++++++ qsa-api/qsa_api/project.py | 176 ++++++++++++++++++++ 3 files changed, 453 insertions(+), 4 deletions(-) diff --git a/docs/src/qsa-api/endpoints/projects.md b/docs/src/qsa-api/endpoints/projects.md index d7c81025..9e4755c3 100644 --- a/docs/src/qsa-api/endpoints/projects.md +++ b/docs/src/qsa-api/endpoints/projects.md @@ -53,17 +53,25 @@ empty. | POST | `/api/projects/{project}/layers` | Add layer to project. See [Layer definition](#layer-definition) for more information. | | POST | `/api/projects/{project}/layers/{layer}/style` | Add/Update layer's style with `name` (style name) and `current` (`true` or `false`) | | DELETE | `/api/projects/{project}/layers/{layer}` | Remove layer from project | +| GET | `/api/projects/{project}/layers/wms` | List all published WMS layers | +| GET | `/api/projects/{project}/layers/wms/feature-info`| List WMS Feature Info settings | +| POST | `/api/projects/{project}/layers/wms/feature-info`| Change WMS Feature Info settings | +| GET | `/api/projects/{project}/layers/{layer}/wms` | List a published WMS layer's metadata | +| POST | `/api/projects/{project}/layers/{layer}/wms` | Toggle WMS publication status of an existing layer | +| GET | `/api/projects/{project}/layers/wfs` | List all published WFS layers | +| GET | `/api/projects/{project}/layers/{layer}/wfs` | List a published WFS layer's metadata | +| POST | `/api/projects/{project}/layers/{layer}/wfs` | Toggle WFS publication status of an existing vector layer | -#### Layer definition {#layer-definition} +### Layer definition {#layer-definition} A layer can be added to a project thanks to the next parameters: - `type` : `raster` or `vector` - `name` : the layer's name - `datasource` : the link to the datasource according to the storage backend - - filesystem : `/tmp/raster.tif` - - AWS S3 : `/vsis3/bucket/raster.tif` - - PostGIS : `service=qsa table=\"public\".\"lines\" (geom)` + - filesystem : `/tmp/raster.tif` + - AWS S3 : `/vsis3/bucket/raster.tif` + - PostGIS : `service=qsa table=\"public\".\"lines\" (geom)` - `overview` (optional) : automatically build overviews for raster layers stored in S3 buckets - `crs` (optional) : CRS (automatically detected by default) @@ -81,6 +89,54 @@ $ curl "http://localhost/api/projects/my_project/layers" \ }' ```` +### WMS + +### Publication + +QGIS Server publishes WMS layers automatically. These parameters are needed to change the publication status of a layer: + +- `published` : If the layer shall be published as boolean. Allowed values: `true`, `false` + +```` shell +$ curl "http://localhost/api/projects/my_project/layers/my_layer/wms" \ + -X POST \ + -H 'Content-Type: application/json' \ + -d '{ + "published": false + }' +```` + +### Feature Info + +By default QGIS Server does not send the geometry on a WMS Feature Info request. This can be changed with this configuration: + +- `publish_geometry`: If WMS Feature Info request shall return the geometry. Allowed values: `true`, `false` + +```` shell +$ curl "http://localhost/api/projects/my_project/layers/wms/feature-info" \ + -X POST \ + -H 'Content-Type: application/json' \ + -d '{ + "publish_geometry": true + }' +```` + +### WFS Publication + +These parameters are needed to publish an existing vector layer as WFS: + +- `published` : If the layer shall be published as boolean. Allowed values: `true`, `false` +- `geometry_precision` (optional) : the geometric precision as integer, default is `8` + +```` shell +$ curl "http://localhost/api/projects/my_project/layers/my_layer/wfs" \ + -X POST \ + -H 'Content-Type: application/json' \ + -d '{ + "published": true + }' +```` + ## Style A QSA style may be used through the `STYLE` OGC web services parameter to diff --git a/qsa-api/qsa_api/api/projects.py b/qsa-api/qsa_api/api/projects.py index d0f456e2..66dbaa95 100644 --- a/qsa-api/qsa_api/api/projects.py +++ b/qsa-api/qsa_api/api/projects.py @@ -417,7 +417,224 @@ def project_info_layer(name, layer_name): except Exception as e: logger().exception(str(e)) return {"error": "internal server error"}, 415 + +@projects.get("//layers/wms") +def project_layers_wms(name): + """ + Get all published WMS layers + """ + log_request() + try: + psql_schema = request.args.get("schema", default="public") + project = QSAProject(name, psql_schema) + if project.exists(): + return jsonify(project.layers_wms), 201 + else: + return {"error": "Project does not exist"}, 415 + except Exception as e: + logger().exception(str(e)) + return {"error": "internal server error"}, 415 + +@projects.get("//layers/wms/feature-info") +def project_wms_feature_info(name): + """ + Information about the WMS Feature Info settings + """ + log_request() + try: + psql_schema = request.args.get("schema", default="public") + project = QSAProject(name, psql_schema) + if project.exists(): + return jsonify(project.project_wms_feature_info), 201 + else: + return {"error": "Project does not exist"}, 415 + except Exception as e: + logger().exception(str(e)) + return {"error": "internal server error"}, 415 + + +@projects.get("//layers/wfs") +def project_layers_wfs(name): + """ + Get all published WFS layers + """ + log_request() + try: + psql_schema = request.args.get("schema", default="public") + project = QSAProject(name, psql_schema) + if project.exists(): + return jsonify(project.layers_wfs_info), 201 + else: + return {"error": "Project does not exist"}, 415 + except Exception as e: + logger().exception(str(e)) + return {"error": "internal server error"}, 415 + +@projects.get("//layers//wfs") +def project_info_layer_wfs(name, layer_name): + """ + Get information about a single WFS layer + """ + log_request() + try: + psql_schema = request.args.get("schema", default="public") + project = QSAProject(name, psql_schema) + if project.exists(): + return jsonify(project.layer_wfs(layer_name)), 201 + else: + return {"error": "Project does not exist"}, 415 + except Exception as e: + logger().exception(str(e)) + return {"error": "internal server error"}, 415 + +@projects.get("//layers//wms") +def project_info_layer_wms(name, layer_name): + """ + Get information about a single WMS layer + """ + log_request() + try: + psql_schema = request.args.get("schema", default="public") + project = QSAProject(name, psql_schema) + if project.exists(): + return jsonify(project.layer_wms(layer_name)), 201 + else: + return {"error": "Project does not exist"}, 415 + except Exception as e: + logger().exception(str(e)) + return {"error": "internal server error"}, 415 + +@projects.post("//layers//wfs") +def project_publish_wfs_layer(name, layer_name): + """ + Change WFS publication status of an vector layer + """ + log_request() + try: + json_schema = { + "type": "object", + "required": ["published"], + "properties": { + "published": {"type": "boolean"}, + "geometry_precision": {"type": "integer"} + }, + } + + psql_schema = request.args.get("schema", default="public") + project = QSAProject(name, psql_schema) + + if project.exists(): + data = request.get_json() + try: + validate(data, json_schema) + except ValidationError as e: + return {"error": e.message}, 415 + + published = data["published"] + + geometry_precision = 8 # default value in QGIS Server + + if "geometry_precision" in data: + geometry_precision = data["geometry_precision"] + + project = QSAProject(name, psql_schema) + rc, err = project.publish_wfs_layer(layer_name, published, geometry_precision) + + if err: + return {"error": err}, 415 + return jsonify(rc), 201 + + else: + return {"error": "Project does not exist"}, 415 + + except Exception as e: + logger().exception(str(e)) + return {"error": "internal server error"}, 415 + +@projects.post("//layers//wms") +def project_publish_wms_layer(name, layer_name): + """ + Change WMS publication status of an vector layer + """ + log_request() + try: + json_schema = { + "type": "object", + "required": ["published"], + "properties": { + "published": {"type": "boolean"} + }, + } + + psql_schema = request.args.get("schema", default="public") + project = QSAProject(name, psql_schema) + + if project.exists(): + data = request.get_json() + try: + validate(data, json_schema) + except ValidationError as e: + return {"error": e.message}, 415 + + published = data["published"] + + project = QSAProject(name, psql_schema) + + rc, err = project.publish_wms_layer(layer_name, published) + + if err: + return {"error": err}, 415 + return jsonify(rc), 201 + + else: + return {"error": "Project does not exist"}, 415 + + except Exception as e: + logger().exception(str(e)) + return {"error": "internal server error"}, 415 + +@projects.post("//layers/wms/feature-info") +def project_wms_adjust_feature_info(name): + """ + Adjust the WMS Feature Info settings + """ + log_request() + try: + json_schema = { + "type": "object", + "required": ["publish_geometry"], + "properties": { + "publish_geometry": {"type": "boolean"} + }, + } + + psql_schema = request.args.get("schema", default="public") + project = QSAProject(name, psql_schema) + + if project.exists(): + data = request.get_json() + try: + validate(data, json_schema) + except ValidationError as e: + return {"error": e.message}, 415 + + publish_geometry = data["publish_geometry"] + + project = QSAProject(name, psql_schema) + + rc, err = project.wms_adjust_feature_info(publish_geometry) + + if err: + return {"error": err}, 415 + return jsonify(rc), 201 + + else: + return {"error": "Project does not exist"}, 415 + + except Exception as e: + logger().exception(str(e)) + return {"error": "internal server error"}, 415 @projects.delete("//layers/") def project_del_layer(name, layer_name): diff --git a/qsa-api/qsa_api/project.py b/qsa-api/qsa_api/project.py index 9179b852..8461082a 100644 --- a/qsa-api/qsa_api/project.py +++ b/qsa-api/qsa_api/project.py @@ -113,6 +113,56 @@ def layers(self) -> list: self.debug(f"{len(layers)} layers found") return layers + @property + def layers_wfs_info(self) -> list: + """ + List WFS layers with additional information + """ + wfs_layers = [] + + p = QgsProject() + p.read(self._qgis_project_uri, Qgis.ProjectReadFlag.DontResolveLayers) + + for layer_id in p.readListEntry('WFSLayers', '/')[0]: + layer_infos = p.mapLayer(layer_id) + if layer_infos: + wfs_layers.append({ + "name": layer_infos.name(), + "precision": self._wfs_precision_from_layer_id(p, layer_id) + }) + + self.debug(f"{len(wfs_layers)} layers found") + return wfs_layers + + @property + def layers_wfs(self) -> list: + return [wfs_layer["name"] for wfs_layer in self.layers_wfs_info] + + @property + def layers_wms(self) -> list: + p = QgsProject() + p.read(self._qgis_project_uri, Qgis.ProjectReadFlag.DontResolveLayers) + + hidden_wms_layers = p.readListEntry('WMSRestrictedLayers', '/')[0] + + wms_layers = [] + for layer in self.layers: + if layer not in hidden_wms_layers: + wms_layers.append(layer) + + return wms_layers + + @property + def project_wms_feature_info(self) -> list: + p = QgsProject() + p.read(self._qgis_project_uri, Qgis.ProjectReadFlag.DontResolveLayers) + + publish_geometry = p.readBoolEntry('WMSAddWktGeometry', '/')[0] + + return { + "publish_geometry": publish_geometry + } + @property def metadata(self) -> dict: m = {} @@ -228,6 +278,31 @@ def layer(self, name: str) -> dict: return infos return {} + + def layer_wfs(self, name: str) -> dict: + if name not in self.layers_wfs: + return {} + + project = QgsProject() + project.read(self._qgis_project_uri) + layers = project.mapLayersByName(name) + + if not layers: + return {} + + layer = layers[0] + infos = {} + infos["name"] = layer.name() + infos["precision"] = self._wfs_precision_from_layer_id(project, layer.id()) + return infos + + + def layer_wms(self, name: str) -> dict: + if name in self.layers_wms: + return { + "name": name + } + return {} def layer_update_style( self, layer_name: str, style_name: str, current: bool @@ -478,6 +553,94 @@ def add_layer( mp.write() return True, "" + + def publish_wfs_layer( + self, + name: str, + do_publish: bool, + geometry_precision: int + ) -> (bool, str): + project = QgsProject() + project.read(self._qgis_project_uri, Qgis.ProjectReadFlag.DontResolveLayers) + + # get layer ID + layers = project.mapLayersByName(name) + if not layers: + return False, f"Layer '{name}' does not exist" + layer_id = layers[0].id() + + wfs_layer_names = self.layers_wfs + wfs_layer_ids = self._layer_ids_from_names(project, wfs_layer_names) + + already_published = name in wfs_layer_names + + if do_publish and already_published: + return f"Layer '{name}' is already published", "" + + if not do_publish and not already_published: + return f"Layer '{name}' is not published", "" + + if do_publish: + wfs_layer_ids.append(layer_id) + project.writeEntry("WFSLayersPrecision", f"/{layer_id}", geometry_precision) + else: + wfs_layer_ids.remove(layer_id) + project.removeEntry('WFSLayersPrecision', f"/{layer_id}") + + project.writeEntry("WFSLayers", "/", wfs_layer_ids) + + self.debug("Write QGIS project") + project.write() + + return True, "" + + def publish_wms_layer( + self, + name: str, + do_publish: bool, + ) -> (bool, str): + project = QgsProject() + project.read(self._qgis_project_uri, Qgis.ProjectReadFlag.DontResolveLayers) + + # get layer ID + layers = project.mapLayersByName(name) + if not layers: + return False, f"Layer '{name}' does not exist" + + already_published = name in self.layers_wms + + if do_publish and already_published: + return f"Layer '{name}' is already published", "" + + if not do_publish and not already_published: + return f"Layer '{name}' is not published", "" + + hidden_wms_layers = project.readListEntry('WMSRestrictedLayers', '/')[0] + if do_publish: + hidden_wms_layers.remove(name) + else: + hidden_wms_layers.append(name) + + project.writeEntry( "WMSRestrictedLayers" , "/", hidden_wms_layers) + + self.debug("Write QGIS project") + project.write() + + return True, "" + + def wms_adjust_feature_info( + self, + publish_geometry: bool + ) -> (bool, str): + project = QgsProject() + project.read(self._qgis_project_uri, Qgis.ProjectReadFlag.DontResolveLayers) + + project.writeEntry('WMSAddWktGeometry','/', publish_geometry) + + self.debug("Write QGIS project") + project.write() + + return True, "" def add_style( self, @@ -757,6 +920,19 @@ def _layer_bbox(lyr) -> list: .split(" "), ) ) + + @staticmethod + def _wfs_precision_from_layer_id(qgis_project: QgsProject, wfs_layer_id: str) -> int: + return qgis_project.readEntry('WFSLayersPrecision', '/' + wfs_layer_id)[0] + + @staticmethod + def _layer_ids_from_names(qgis_project: QgsProject, layer_names: list[str]) -> list[str]: + layer_ids = [] + for layer_name in layer_names: + layers = qgis_project.mapLayersByName(layer_name) + if layers: + layer_ids.append(layers[0].id()) + return layer_ids @property def _qgis_project_uri(self) -> str: