From 8a5e03594b06339addc4ee0bd57b1dbf8a53e0aa Mon Sep 17 00:00:00 2001 From: rldhont Date: Fri, 26 Apr 2024 15:46:32 +0200 Subject: [PATCH] [Feature] Extending replaceExpressionText Request with ALL features and GeoJSON format For `replaceExpressionText` request of `Expression` service, it is possible to perform `replaceExpressionText` on all features of the layer and to get result as GeoJSON. To do so, you have to specify `FEATURES=ALL` and `Format=GeoJSON` in the request URL. --- lizmap_server/expression_service.py | 100 ++++++++----- ...xpression_service_replaceexpressiontext.py | 139 ++++++++++++++++++ 2 files changed, 205 insertions(+), 34 deletions(-) diff --git a/lizmap_server/expression_service.py b/lizmap_server/expression_service.py index 2fbfa68a..d6b197a5 100755 --- a/lizmap_server/expression_service.py +++ b/lizmap_server/expression_service.py @@ -14,12 +14,13 @@ QgsExpressionContextUtils, QgsFeature, QgsFeatureRequest, + QgsField, QgsFields, QgsJsonExporter, QgsJsonUtils, QgsProject, ) -from qgis.PyQt.QtCore import QTextCodec +from qgis.PyQt.QtCore import QTextCodec, QVariant from qgis.server import ( QgsRequestHandler, QgsServerRequest, @@ -343,7 +344,10 @@ def replace_expression_text( or FEATURES=[{"type": "Feature", "geometry": {}, "properties": {}}, {"type": "Feature", "geometry": {}, "properties": {}}] + or + FEATURES=ALL to get Replace expression texts for all features of the layer FORM_SCOPE=boolean to add formScope based on provided features + FORMAT=GeoJSON to get response as GeoJSON """ logger = Logger() layer_name = params.get('LAYER', '') @@ -431,39 +435,43 @@ def replace_expression_text( return # Check features - try: - geojson = json.loads(features) - except Exception: - logger.critical( - "JSON loads features '{}' exception:\n{}".format(features, traceback.format_exc())) - raise ExpressionServiceError( - "Bad request error", - "Invalid 'Evaluate' REQUEST: FEATURES '{}' are not well formed".format(features), - 400) + if features.upper() == 'ALL': + feature_fields = layer.fields() + feature_list = layer.getFeatures() + else: + try: + geojson = json.loads(features) + except Exception: + logger.critical( + "JSON loads features '{}' exception:\n{}".format(features, traceback.format_exc())) + raise ExpressionServiceError( + "Bad request error", + "Invalid 'Evaluate' REQUEST: FEATURES '{}' are not well formed".format(features), + 400) - if not geojson or not isinstance(geojson, list) or len(geojson) == 0: - raise ExpressionServiceError( - "Bad request error", - "Invalid 'Evaluate' REQUEST: FEATURES '{}' are not well formed".format(features), - 400) + if not geojson or not isinstance(geojson, list) or len(geojson) == 0: + raise ExpressionServiceError( + "Bad request error", + "Invalid 'Evaluate' REQUEST: FEATURES '{}' are not well formed".format(features), + 400) - if ('type' not in geojson[0]) or geojson[0]['type'] != 'Feature': - raise ExpressionServiceError( - "Bad request error", - ("Invalid 'Evaluate' REQUEST: FEATURES '{}' are not well formed: type not defined or not " - "Feature.").format(features), - 400) + if ('type' not in geojson[0]) or geojson[0]['type'] != 'Feature': + raise ExpressionServiceError( + "Bad request error", + ("Invalid 'Evaluate' REQUEST: FEATURES '{}' are not well formed: type not defined or not " + "Feature.").format(features), + 400) - # try to load features - # read fields - feature_fields = QgsJsonUtils.stringToFields( - '{ "type": "FeatureCollection","features":' + features + '}', - QTextCodec.codecForName("UTF-8")) - # read features - feature_list = QgsJsonUtils.stringToFeatureList( - '{ "type": "FeatureCollection","features":' + features + '}', - feature_fields, - QTextCodec.codecForName("UTF-8")) + # try to load features + # read fields + feature_fields = QgsJsonUtils.stringToFields( + '{ "type": "FeatureCollection","features":' + features + '}', + QTextCodec.codecForName("UTF-8")) + # read features + feature_list = QgsJsonUtils.stringToFeatureList( + '{ "type": "FeatureCollection","features":' + features + '}', + feature_fields, + QTextCodec.codecForName("UTF-8")) # features not well formed if not feature_list: @@ -480,6 +488,14 @@ def replace_expression_text( # form scope add_form_scope = to_bool(params.get('FORM_SCOPE')) + geojson_output = params.get('FORMAT', '').upper() == 'GEOJSON' + if geojson_output: + exporter = QgsJsonExporter() + exporter.setSourceCrs(layer.crs()) + geojson_fields = QgsFields() + for k in str_map.keys(): + geojson_fields.append(QgsField(k, QVariant.String)) + # loop through provided features to replace expression strings for f in feature_list: # clone the features with all attributes @@ -503,9 +519,25 @@ def replace_expression_text( for k, s in str_map.items(): value = QgsExpression.replaceExpressionText(s, exp_context, da) result[k] = json.loads(QgsJsonUtils.encodeValue(value)) - body['results'].append(result) - - write_json_response(body, response) + if geojson_output: + feature = QgsFeature(geojson_fields, f.id()) + feature.setGeometry(f.geometry()) + feature.setAttributes(list(result.values())) + body['results'].append(exporter.exportFeature(feature)) + else: + body['results'].append(result) + + if geojson_output: + response.setStatusCode(200) + response.setHeader("Content-Type", "application/vnd.geo+json; charset=utf-8") + response.write( + ',\n'.join([ + '{"type": "FeatureCollection"', + '"features": ['+',\n'.join(body['results'])+']}' + ]) + ) + else: + write_json_response(body, response) return @staticmethod diff --git a/test/test_expression_service_replaceexpressiontext.py b/test/test_expression_service_replaceexpressiontext.py index f87afc5b..f02bf547 100644 --- a/test/test_expression_service_replaceexpressiontext.py +++ b/test/test_expression_service_replaceexpressiontext.py @@ -195,6 +195,36 @@ def test_request_with_features(client): assert b['results'][1]['d'] == '105' +def test_request_with_features_all(client): + """ Test Expression replaceExpressionText request with Feature or Features parameter + """ + # Make a request + qs = f"?SERVICE=EXPRESSION&REQUEST=replaceExpressionText&MAP={PROJECT_FILE}&LAYER=france_parts" + qs += "&STRINGS={\"a\":\"%s\", \"b\":\"%s\", \"c\":\"%s\", \"d\":\"%s\"}" % ( + quote('[% 1 %]', safe=''), quote('[% 1 + 1 %]', safe=''), quote('[% NAME_1 %]', safe=''), + quote('[% round($area) %]', safe='')) + qs += "&FEATURES=ALL" + rv = client.get(qs, PROJECT_FILE) + assert rv.status_code == 200 + assert rv.headers.get('Content-Type', '').find('application/json') == 0 + + b = json.loads(rv.content.decode('utf-8')) + + assert 'status' in b + assert b['status'] == 'success' + + assert 'results' in b + assert len(b['results']) == 4 + + assert 'a' in b['results'][0] + assert b['results'][0]['a'] == '1' + assert 'b' in b['results'][0] + assert b['results'][0]['b'] == '2' + assert b['results'][0]['c'] == 'Basse-Normandie' + assert 'd' in b['results'][0] + assert b['results'][0]['d'] == '27186051602' + + def test_request_with_form_scope(client): """ Test Expression replaceExpressionText request without Feature or Features and Form_Scope parameters """ @@ -249,3 +279,112 @@ def test_request_with_form_scope(client): assert b['results'][0]['c'] == '' assert 'd' in b['results'][0] assert b['results'][0]['d'] == '102' + + +def test_request_with_features_geojson(client): + """ Test Expression replaceExpressionText request with Feature or Features parameter + """ + # Make a request + qs = f"?SERVICE=EXPRESSION&REQUEST=replaceExpressionText&MAP={PROJECT_FILE}&LAYER=france_parts" + qs += "&STRINGS={\"a\":\"%s\", \"b\":\"%s\", \"c\":\"%s\", \"d\":\"%s\"}" % ( + quote('[% 1 %]', safe=''), quote('[% 1 + 1 %]', safe=''), quote('[% prop0 %]', safe=''), + quote('[% $x %]', safe='')) + qs += "&FEATURE={\"type\":\"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [102.0, 0.5]}, \"properties\": {\"prop0\": \"value0\"}}" + qs += "&FORMAT=GeoJSON" + rv = client.get(qs, PROJECT_FILE) + assert rv.status_code == 200 + assert rv.headers.get('Content-Type', '').find('application/vnd.geo+json') == 0 + + b = json.loads(rv.content.decode('utf-8')) + + assert 'type' in b + assert b['type'] == 'FeatureCollection' + + assert 'features' in b + assert len(b['features']) == 1 + + assert 'id' in b['features'][0] + assert b['features'][0]['id'] == 0 + assert 'properties' in b['features'][0] + assert 'a' in b['features'][0]['properties'] + assert b['features'][0]['properties']['a'] == '1' + assert 'b' in b['features'][0]['properties'] + assert b['features'][0]['properties']['b'] == '2' + assert b['features'][0]['properties']['c'] == 'value0' + assert 'd' in b['features'][0]['properties'] + assert b['features'][0]['properties']['d'] == '102' + + # Make a request + qs = f"?SERVICE=EXPRESSION&REQUEST=replaceExpressionText&MAP={PROJECT_FILE}&LAYER=france_parts" + qs += "&STRINGS={\"a\":\"%s\", \"b\":\"%s\", \"c\":\"%s\", \"d\":\"%s\"}" % ( + quote('[% 1 %]', safe=''), quote('[% 1 + 1 %]', safe=''), quote('[% prop0 %]', safe=''), + quote('[% $x %]', safe='')) + qs += "&FEATURES=[" + qs += "{\"type\":\"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [102.0, 0.5]}, \"properties\": {\"prop0\": \"value0\"}}" + qs += ", " + qs += "{\"type\":\"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [105.0, 0.5]}, \"properties\": {\"prop0\": \"value1\"}}" + qs += "]" + qs += "&FORMAT=GeoJSON" + rv = client.get(qs, PROJECT_FILE) + assert rv.status_code == 200 + assert rv.headers.get('Content-Type', '').find('application/vnd.geo+json') == 0 + + b = json.loads(rv.content.decode('utf-8')) + + assert 'type' in b + assert b['type'] == 'FeatureCollection' + + assert 'features' in b + assert len(b['features']) == 2 + + assert 'id' in b['features'][0] + assert b['features'][0]['id'] == 0 + assert 'properties' in b['features'][0] + assert 'a' in b['features'][0]['properties'] + assert b['features'][0]['properties']['a'] == '1' + assert 'b' in b['features'][0]['properties'] + assert b['features'][0]['properties']['b'] == '2' + assert b['features'][0]['properties']['c'] == 'value0' + assert 'd' in b['features'][0]['properties'] + assert b['features'][0]['properties']['d'] == '102' + + assert 'id' in b['features'][1] + assert b['features'][1]['id'] == 1 + assert 'c' in b['features'][1]['properties'] + assert b['features'][1]['properties']['c'] == 'value1' + assert 'd' in b['features'][1]['properties'] + assert b['features'][1]['properties']['d'] == '105' + + +def test_request_with_features_all_geojson(client): + """ Test Expression replaceExpressionText request with Feature or Features parameter + """ + # Make a request + qs = f"?SERVICE=EXPRESSION&REQUEST=replaceExpressionText&MAP={PROJECT_FILE}&LAYER=france_parts" + qs += "&STRINGS={\"a\":\"%s\", \"b\":\"%s\", \"c\":\"%s\", \"d\":\"%s\"}" % ( + quote('[% 1 %]', safe=''), quote('[% 1 + 1 %]', safe=''), quote('[% NAME_1 %]', safe=''), + quote('[% round($area) %]', safe='')) + qs += "&FEATURES=ALL" + qs += "&FORMAT=GeoJSON" + rv = client.get(qs, PROJECT_FILE) + assert rv.status_code == 200 + assert rv.headers.get('Content-Type', '').find('application/vnd.geo+json') == 0 + + b = json.loads(rv.content.decode('utf-8')) + + assert 'type' in b + assert b['type'] == 'FeatureCollection' + + assert 'features' in b + assert len(b['features']) == 4 + + assert 'id' in b['features'][0] + assert b['features'][0]['id'] == 0 + assert 'properties' in b['features'][0] + assert 'a' in b['features'][0]['properties'] + assert b['features'][0]['properties']['a'] == '1' + assert 'b' in b['features'][0]['properties'] + assert b['features'][0]['properties']['b'] == '2' + assert b['features'][0]['properties']['c'] == 'Basse-Normandie' + assert 'd' in b['features'][0]['properties'] + assert b['features'][0]['properties']['d'] == '27186051602'