From 20c104007a50d5a01b1a6bcc8124dc30454654c3 Mon Sep 17 00:00:00 2001 From: Jakob Miksch Date: Wed, 28 Aug 2024 18:13:39 +0200 Subject: [PATCH] Enable publication via WFS --- docs/src/qsa-api/endpoints/projects.md | 19 ++++++ qsa-api/qsa_api/api/projects.py | 81 +++++++++++++++++++++++ qsa-api/qsa_api/project.py | 90 ++++++++++++++++++++++++++ 3 files changed, 190 insertions(+) diff --git a/docs/src/qsa-api/endpoints/projects.md b/docs/src/qsa-api/endpoints/projects.md index d7c8102..3b89cdf 100644 --- a/docs/src/qsa-api/endpoints/projects.md +++ b/docs/src/qsa-api/endpoints/projects.md @@ -53,6 +53,9 @@ 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/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} @@ -81,6 +84,22 @@ $ curl "http://localhost/api/projects/my_project/layers" \ }' ```` +#### 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 d0f456e..5928f6a 100644 --- a/qsa-api/qsa_api/api/projects.py +++ b/qsa-api/qsa_api/api/projects.py @@ -417,7 +417,88 @@ def project_info_layer(name, layer_name): 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), 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.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 + + 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.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 9179b85..e0b015f 100644 --- a/qsa-api/qsa_api/project.py +++ b/qsa-api/qsa_api/project.py @@ -113,6 +113,24 @@ def layers(self) -> list: self.debug(f"{len(layers)} layers found") return layers + @property + def layers_wfs(self) -> list: + 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 metadata(self) -> dict: m = {} @@ -228,6 +246,24 @@ def layer(self, name: str) -> dict: return infos return {} + + def layer_wfs(self, name: str) -> dict: + wfs_layers = self.layers_wfs + self.debug("wfs_layers " + str(wfs_layers)) + wfs_layer_names = [item['name'] for item in wfs_layers] + if name in wfs_layer_names: + project = QgsProject() + project.read(self._qgis_project_uri) + layers = project.mapLayersByName(name) + + if layers: + layer = layers[0] + infos = {} + infos["name"] = layer.name() + infos["precision"] = self._wfs_precision_from_layer_id(project, layer.id()) + return infos + + return {} def layer_update_style( self, layer_name: str, style_name: str, current: bool @@ -478,6 +514,47 @@ 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) + + # Attempt to get the layer ID + layers = project.mapLayersByName(name) + if not layers: + return False, f"Layer '{name}' does not exist" + layer_id = layers[0].id() + + wfs_layers = self.layers_wfs + wfs_layer_names = [item['name'] for item in wfs_layers] + 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 False, f"Layer '{name}' is already published" + + if not do_publish and not already_published: + return False, 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 add_style( self, @@ -757,6 +834,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: