Skip to content

Commit

Permalink
[Feature] Extending replaceExpressionText Request with ALL features a…
Browse files Browse the repository at this point in the history
…nd 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.
  • Loading branch information
rldhont committed Apr 26, 2024
1 parent 7787d53 commit 8a5e035
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 34 deletions.
100 changes: 66 additions & 34 deletions lizmap_server/expression_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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', '')
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand Down
139 changes: 139 additions & 0 deletions test/test_expression_service_replaceexpressiontext.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand Down Expand Up @@ -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'

0 comments on commit 8a5e035

Please sign in to comment.