django-gisserver 1.5.0__py3-none-any.whl → 2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +34 -8
- django_gisserver-2.1.dist-info/RECORD +68 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/compat.py +23 -0
- gisserver/conf.py +7 -0
- gisserver/crs.py +401 -0
- gisserver/db.py +126 -51
- gisserver/exceptions.py +132 -4
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +131 -31
- gisserver/extensions/queries.py +266 -0
- gisserver/features.py +253 -181
- gisserver/geometries.py +64 -311
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +311 -0
- gisserver/operations/base.py +130 -312
- gisserver/operations/wfs20.py +399 -375
- gisserver/output/__init__.py +14 -49
- gisserver/output/base.py +198 -144
- gisserver/output/csv.py +78 -75
- gisserver/output/geojson.py +37 -37
- gisserver/output/gml32.py +287 -259
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +73 -61
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +81 -169
- gisserver/output/xmlschema.py +85 -46
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +426 -0
- gisserver/parsers/fes20/__init__.py +89 -31
- gisserver/parsers/fes20/expressions.py +172 -58
- gisserver/parsers/fes20/filters.py +116 -45
- gisserver/parsers/fes20/identifiers.py +66 -28
- gisserver/parsers/fes20/lookups.py +146 -0
- gisserver/parsers/fes20/operators.py +417 -161
- gisserver/parsers/fes20/sorting.py +113 -34
- gisserver/parsers/gml/__init__.py +17 -25
- gisserver/parsers/gml/base.py +36 -15
- gisserver/parsers/gml/geometries.py +105 -44
- gisserver/parsers/ows/__init__.py +25 -0
- gisserver/parsers/ows/kvp.py +198 -0
- gisserver/parsers/ows/requests.py +160 -0
- gisserver/parsers/query.py +179 -0
- gisserver/parsers/values.py +87 -4
- gisserver/parsers/wfs20/__init__.py +39 -0
- gisserver/parsers/wfs20/adhoc.py +253 -0
- gisserver/parsers/wfs20/base.py +148 -0
- gisserver/parsers/wfs20/projection.py +103 -0
- gisserver/parsers/wfs20/requests.py +483 -0
- gisserver/parsers/wfs20/stored.py +193 -0
- gisserver/parsers/xml.py +261 -0
- gisserver/projection.py +367 -0
- gisserver/static/gisserver/index.css +20 -4
- gisserver/templates/gisserver/base.html +12 -0
- gisserver/templates/gisserver/index.html +9 -15
- gisserver/templates/gisserver/service_description.html +12 -6
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
- gisserver/templates/gisserver/wfs/feature_field.html +3 -3
- gisserver/templates/gisserver/wfs/feature_type.html +35 -13
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +445 -313
- gisserver/views.py +227 -62
- django_gisserver-1.5.0.dist-info/RECORD +0 -54
- gisserver/parsers/base.py +0 -149
- gisserver/parsers/fes20/query.py +0 -285
- gisserver/parsers/tags.py +0 -102
- gisserver/queries/__init__.py +0 -37
- gisserver/queries/adhoc.py +0 -185
- gisserver/queries/base.py +0 -186
- gisserver/queries/projection.py +0 -240
- gisserver/queries/stored.py +0 -206
- gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
- gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
gisserver/output/gml32.py
CHANGED
|
@@ -11,6 +11,7 @@ much extra method calls per field. Some bits are non-DRY inlined for this reason
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import itertools
|
|
14
|
+
from collections import defaultdict
|
|
14
15
|
from datetime import date, datetime, time, timezone
|
|
15
16
|
from decimal import Decimal as D
|
|
16
17
|
from io import StringIO
|
|
@@ -18,30 +19,33 @@ from operator import itemgetter
|
|
|
18
19
|
from typing import cast
|
|
19
20
|
|
|
20
21
|
from django.contrib.gis import geos
|
|
22
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
21
23
|
from django.db import models
|
|
22
24
|
from django.http import HttpResponse
|
|
23
25
|
|
|
24
26
|
from gisserver.db import (
|
|
25
27
|
AsGML,
|
|
26
|
-
build_db_annotations,
|
|
27
|
-
conditional_transform,
|
|
28
|
-
get_db_annotation,
|
|
29
|
-
get_db_geometry_selects,
|
|
30
28
|
get_db_geometry_target,
|
|
29
|
+
get_db_rendered_geometry,
|
|
31
30
|
get_geometries_union,
|
|
31
|
+
replace_queryset_geometries,
|
|
32
32
|
)
|
|
33
33
|
from gisserver.exceptions import NotFound, WFSException
|
|
34
|
-
from gisserver.
|
|
35
|
-
from gisserver.
|
|
36
|
-
from gisserver.
|
|
37
|
-
from gisserver.
|
|
38
|
-
from gisserver.types import XsdElement, XsdNode
|
|
34
|
+
from gisserver.geometries import BoundingBox
|
|
35
|
+
from gisserver.parsers.xml import xmlns
|
|
36
|
+
from gisserver.projection import FeatureProjection, FeatureRelation
|
|
37
|
+
from gisserver.types import GeometryXsdElement, GmlBoundedByElement, XsdElement, XsdNode, XsdTypes
|
|
39
38
|
|
|
40
|
-
from .base import
|
|
39
|
+
from .base import CollectionOutputRenderer, XmlOutputRenderer
|
|
41
40
|
from .results import SimpleFeatureCollection
|
|
41
|
+
from .utils import (
|
|
42
|
+
attr_escape,
|
|
43
|
+
tag_escape,
|
|
44
|
+
value_to_text,
|
|
45
|
+
value_to_xml_string,
|
|
46
|
+
)
|
|
42
47
|
|
|
43
48
|
GML_RENDER_FUNCTIONS = {}
|
|
44
|
-
AUTO_STR = (int, float, D, date, time)
|
|
45
49
|
|
|
46
50
|
|
|
47
51
|
def register_geos_type(geos_type):
|
|
@@ -52,56 +56,62 @@ def register_geos_type(geos_type):
|
|
|
52
56
|
return _inc
|
|
53
57
|
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _attr_escape(s: str):
|
|
60
|
-
# Slightly faster then html.escape() as it doesn't replace single quotes.
|
|
61
|
-
# Having tried all possible variants, this code still outperforms other forms of escaping.
|
|
62
|
-
return s.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _value_to_xml_string(value):
|
|
66
|
-
# Simple scalar value
|
|
67
|
-
if isinstance(value, str): # most cases
|
|
68
|
-
return _tag_escape(value)
|
|
69
|
-
elif isinstance(value, datetime):
|
|
70
|
-
return value.astimezone(timezone.utc).isoformat()
|
|
71
|
-
elif isinstance(value, bool):
|
|
72
|
-
return "true" if value else "false"
|
|
73
|
-
elif isinstance(value, AUTO_STR):
|
|
74
|
-
return value # no need for _tag_escape(), and f"{value}" works faster.
|
|
75
|
-
else:
|
|
76
|
-
return _tag_escape(str(value))
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _value_to_text(value):
|
|
80
|
-
# Simple scalar value, no XML escapes
|
|
81
|
-
if isinstance(value, str): # most cases
|
|
82
|
-
return value
|
|
83
|
-
elif isinstance(value, datetime):
|
|
84
|
-
return value.astimezone(timezone.utc).isoformat()
|
|
85
|
-
elif isinstance(value, bool):
|
|
86
|
-
return "true" if value else "false"
|
|
87
|
-
else:
|
|
88
|
-
return value # f"{value} works faster and produces the right format.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class GML32Renderer(OutputRenderer):
|
|
59
|
+
class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
|
|
92
60
|
"""Render the GetFeature XML output in GML 3.2 format"""
|
|
93
61
|
|
|
94
62
|
content_type = "text/xml; charset=utf-8"
|
|
63
|
+
content_disposition = 'inline; filename="{typenames} {page} {date}.xml"'
|
|
95
64
|
xml_collection_tag = "FeatureCollection"
|
|
96
65
|
xml_sub_collection_tag = "FeatureCollection" # Mapserver does not use SimpleFeatureCollection
|
|
97
66
|
chunk_size = 40_000
|
|
98
67
|
gml_seq = 0
|
|
99
68
|
|
|
69
|
+
# Aliases to use for XML namespaces
|
|
70
|
+
xml_namespaces = {
|
|
71
|
+
"http://www.opengis.net/wfs/2.0": "wfs",
|
|
72
|
+
"http://www.opengis.net/gml/3.2": "gml",
|
|
73
|
+
"http://www.w3.org/2001/XMLSchema-instance": "xsi", # for xsi:nil="true" and xsi:schemaLocation.
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def __init__(self, *args, **kwargs):
|
|
77
|
+
super().__init__(*args, **kwargs)
|
|
78
|
+
self.feature_types = [
|
|
79
|
+
feature_type
|
|
80
|
+
for sub_collection in self.collection.results
|
|
81
|
+
for feature_type in sub_collection.feature_types
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
self.build_namespace_map()
|
|
85
|
+
|
|
86
|
+
def build_namespace_map(
|
|
87
|
+
self,
|
|
88
|
+
):
|
|
89
|
+
"""Collect all namespaces which namespaces are used by features."""
|
|
90
|
+
try:
|
|
91
|
+
# Make aliases for the feature type elements.
|
|
92
|
+
self.feature_qnames = {
|
|
93
|
+
feature_type: self.feature_to_qname(feature_type)
|
|
94
|
+
for feature_type in self.feature_types
|
|
95
|
+
}
|
|
96
|
+
self.xml_qnames = self._build_xml_qnames()
|
|
97
|
+
except KeyError as e:
|
|
98
|
+
raise ImproperlyConfigured(f"No XML namespace alias defined in WFSView for {e}") from e
|
|
99
|
+
|
|
100
|
+
def _build_xml_qnames(self) -> dict[XsdNode, str]:
|
|
101
|
+
"""Collect aliases for all rendered elements.
|
|
102
|
+
This uses a defaultdict so optional attributes (e.g. by GetPropertyValue) can also be resolved.
|
|
103
|
+
The dict lookup also speeds up, no need to call ``node_to_qname()`` for each tag.
|
|
104
|
+
"""
|
|
105
|
+
known_tags = {
|
|
106
|
+
xsd_element: self.to_qname(xsd_element)
|
|
107
|
+
for sub_collection in self.collection.results
|
|
108
|
+
for xsd_element in sub_collection.projection.all_elements
|
|
109
|
+
}
|
|
110
|
+
return defaultdict(self.to_qname, known_tags)
|
|
111
|
+
|
|
100
112
|
def get_response(self):
|
|
101
113
|
"""Render the output as streaming response."""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if isinstance(self.source_query, GetFeatureById):
|
|
114
|
+
if self.collection.results and self.collection.results[0].projection.output_standalone:
|
|
105
115
|
# WFS spec requires that GetFeatureById output only returns the contents.
|
|
106
116
|
# The streaming response is avoided here, to allow returning a 404.
|
|
107
117
|
return self.get_by_id_response()
|
|
@@ -112,15 +122,18 @@ class GML32Renderer(OutputRenderer):
|
|
|
112
122
|
def get_by_id_response(self):
|
|
113
123
|
"""Render a standalone item, for GetFeatureById"""
|
|
114
124
|
sub_collection = self.collection.results[0]
|
|
125
|
+
sub_collection.source_query.finalize_results(sub_collection) # Allow 404
|
|
115
126
|
self.start_collection(sub_collection)
|
|
127
|
+
|
|
116
128
|
instance = sub_collection.first()
|
|
117
129
|
if instance is None:
|
|
118
130
|
raise NotFound("Feature not found.")
|
|
119
131
|
|
|
132
|
+
self.app_namespaces.pop(xmlns.wfs20.value) # not rendering wfs tags.
|
|
120
133
|
self.output = StringIO()
|
|
121
134
|
self._write = self.output.write
|
|
122
135
|
self.write_by_id_response(
|
|
123
|
-
sub_collection, instance, extra_xmlns=self.
|
|
136
|
+
sub_collection, instance, extra_xmlns=f" {self.render_xmlns_attributes()}"
|
|
124
137
|
)
|
|
125
138
|
content = self.output.getvalue()
|
|
126
139
|
return HttpResponse(content, content_type=self.content_type)
|
|
@@ -134,41 +147,30 @@ class GML32Renderer(OutputRenderer):
|
|
|
134
147
|
extra_xmlns=extra_xmlns,
|
|
135
148
|
)
|
|
136
149
|
|
|
137
|
-
def
|
|
138
|
-
"""
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
def render_xsi_schema_location(self):
|
|
151
|
+
"""Render the value for the xsi:schemaLocation="..." block."""
|
|
152
|
+
# Find which namespaces are exposed
|
|
153
|
+
types_by_namespace = defaultdict(list)
|
|
154
|
+
for feature_type in self.feature_types:
|
|
155
|
+
types_by_namespace[feature_type.xml_namespace].append(feature_type)
|
|
156
|
+
|
|
157
|
+
# Schema location are pairs of "{namespace-uri} {schema-url} {namespace-uri2} {schema-url2}"
|
|
158
|
+
# WFS server types refer to the endpoint that generates the XML Schema for the given feature.
|
|
159
|
+
schema_locations = []
|
|
160
|
+
for xml_namespace, feature_types in types_by_namespace.items():
|
|
161
|
+
schema_locations.append(xml_namespace)
|
|
162
|
+
schema_locations.append(self.operation.view.get_xml_schema_url(feature_types))
|
|
163
|
+
|
|
164
|
+
schema_locations += [
|
|
144
165
|
"http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd",
|
|
145
166
|
"http://www.opengis.net/gml/3.2 http://schemas.opengis.net/gml/3.2.1/gml.xsd",
|
|
146
167
|
]
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
'xmlns:wfs="http://www.opengis.net/wfs/2.0" '
|
|
150
|
-
'xmlns:gml="http://www.opengis.net/gml/3.2" '
|
|
151
|
-
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
|
|
152
|
-
'xmlns:app="{app_xml_namespace}" '
|
|
153
|
-
'xsi:schemaLocation="{schema_location}"'
|
|
154
|
-
).format(
|
|
155
|
-
app_xml_namespace=_attr_escape(self.app_xml_namespace),
|
|
156
|
-
schema_location=_attr_escape(" ".join(schema_location)),
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
def render_xmlns_standalone(self):
|
|
160
|
-
"""Generate the xmlns block that the document needs"""
|
|
161
|
-
# xsi is needed for "xsi:nil="true"' attributes.
|
|
162
|
-
return (
|
|
163
|
-
f' xmlns:app="{_attr_escape(self.app_xml_namespace)}"'
|
|
164
|
-
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
|
165
|
-
' xmlns:gml="http://www.opengis.net/gml/3.2"'
|
|
166
|
-
)
|
|
168
|
+
return " ".join(schema_locations)
|
|
167
169
|
|
|
168
170
|
def render_exception(self, exception: Exception):
|
|
169
171
|
"""Render the exception in a format that fits with the output.
|
|
170
172
|
|
|
171
|
-
The WSF XSD spec has a hidden gem: an
|
|
173
|
+
The WSF XSD spec has a hidden gem: an ``<wfs:truncatedResponse>`` element
|
|
172
174
|
can be rendered at the end of a feature collection
|
|
173
175
|
to inform the client an error happened during rendering.
|
|
174
176
|
"""
|
|
@@ -180,15 +182,14 @@ class GML32Renderer(OutputRenderer):
|
|
|
180
182
|
exception = WFSException(message, code=exception.__class__.__name__, status_code=500)
|
|
181
183
|
exception.debug_hint = False
|
|
182
184
|
|
|
183
|
-
# Only at the top-level, an exception report can be rendered
|
|
184
|
-
|
|
185
|
-
f"</wfs:{self.xml_sub_collection_tag}
|
|
186
|
-
if
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
# Only at the top-level, an exception report can be rendered, close any remaining tags.
|
|
186
|
+
if len(self.collection.results) > 1:
|
|
187
|
+
sub_closing = f"</wfs:{self.xml_sub_collection_tag}>\n</wfs:member>\n"
|
|
188
|
+
if not buffer.endswith(sub_closing):
|
|
189
|
+
buffer += sub_closing
|
|
190
|
+
|
|
189
191
|
return (
|
|
190
192
|
f"{buffer}"
|
|
191
|
-
f"{closing_child}"
|
|
192
193
|
" <wfs:truncatedResponse>"
|
|
193
194
|
f"{exception.as_xml()}"
|
|
194
195
|
" </wfs:truncatedResponse>\n"
|
|
@@ -202,19 +203,24 @@ class GML32Renderer(OutputRenderer):
|
|
|
202
203
|
collection = self.collection
|
|
203
204
|
self.output = output = StringIO()
|
|
204
205
|
self._write = self.output.write
|
|
205
|
-
|
|
206
|
+
|
|
207
|
+
# The base class peaks the generator and handles early exceptions.
|
|
208
|
+
# Any database exceptions during calculating the number of results
|
|
209
|
+
# are all handled by the main WFS view.
|
|
206
210
|
number_matched = collection.number_matched
|
|
207
211
|
number_matched = int(number_matched) if number_matched is not None else "unknown"
|
|
208
212
|
number_returned = collection.number_returned
|
|
213
|
+
|
|
209
214
|
next = previous = ""
|
|
210
215
|
if collection.next:
|
|
211
|
-
next = f' next="{
|
|
216
|
+
next = f' next="{attr_escape(collection.next)}"'
|
|
212
217
|
if collection.previous:
|
|
213
|
-
previous = f' previous="{
|
|
218
|
+
previous = f' previous="{attr_escape(collection.previous)}"'
|
|
214
219
|
|
|
215
220
|
self._write(
|
|
216
221
|
f"""<?xml version='1.0' encoding="UTF-8" ?>\n"""
|
|
217
|
-
f"<wfs:{self.xml_collection_tag} {
|
|
222
|
+
f"<wfs:{self.xml_collection_tag} {self.render_xmlns_attributes()}"
|
|
223
|
+
f' xsi:schemaLocation="{attr_escape(self.render_xsi_schema_location())}"'
|
|
218
224
|
f' timeStamp="{collection.timestamp}"'
|
|
219
225
|
f' numberMatched="{number_matched}"'
|
|
220
226
|
f' numberReturned="{int(number_returned)}"'
|
|
@@ -236,7 +242,7 @@ class GML32Renderer(OutputRenderer):
|
|
|
236
242
|
f' numberReturned="{int(sub_collection.number_returned)}">\n'
|
|
237
243
|
)
|
|
238
244
|
|
|
239
|
-
for instance in sub_collection:
|
|
245
|
+
for instance in self.read_features(sub_collection):
|
|
240
246
|
self.gml_seq = 0 # need to increment this between write_xml_field calls
|
|
241
247
|
self._write("<wfs:member>\n")
|
|
242
248
|
self.write_feature(projection, instance)
|
|
@@ -268,24 +274,20 @@ class GML32Renderer(OutputRenderer):
|
|
|
268
274
|
unless it's used for a GetPropertyById response.
|
|
269
275
|
"""
|
|
270
276
|
feature_type = projection.feature_type
|
|
277
|
+
feature_xml_qname = self.feature_qnames[feature_type]
|
|
271
278
|
|
|
272
279
|
# Write <app:FeatureTypeName> start node
|
|
273
|
-
pk =
|
|
274
|
-
self._write(f'<{
|
|
280
|
+
pk = tag_escape(str(instance.pk))
|
|
281
|
+
self._write(f'<{feature_xml_qname} gml:id="{feature_type.name}.{pk}"{extra_xmlns}>\n')
|
|
275
282
|
# Write all fields, both base class and local elements.
|
|
276
283
|
for xsd_element in projection.xsd_root_elements:
|
|
277
284
|
# Note that writing 5000 features with 30 tags means this code make 150.000 method calls.
|
|
278
285
|
# Hence, branching to different rendering styles is branched here instead of making such
|
|
279
286
|
# call into a generic "write_field()" function and stepping out from there.
|
|
280
287
|
|
|
281
|
-
if xsd_element.is_geometry:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
# the output CRS and can be overwritten with DB-rendered GML.
|
|
285
|
-
self.write_bounds(projection, instance)
|
|
286
|
-
else:
|
|
287
|
-
# Separate call which can be optimized (no need to overload write_xml_field() for all calls).
|
|
288
|
-
self.write_gml_field(feature_type, xsd_element, instance)
|
|
288
|
+
if xsd_element.type.is_geometry:
|
|
289
|
+
# Separate call which can be optimized (no need to overload write_xml_field() for all calls).
|
|
290
|
+
self.write_gml_field(projection, xsd_element, instance)
|
|
289
291
|
else:
|
|
290
292
|
value = xsd_element.get_value(instance)
|
|
291
293
|
if xsd_element.is_many:
|
|
@@ -294,20 +296,7 @@ class GML32Renderer(OutputRenderer):
|
|
|
294
296
|
# e.g. <gml:name>, or all other <app:...> nodes.
|
|
295
297
|
self.write_xml_field(projection, xsd_element, value)
|
|
296
298
|
|
|
297
|
-
self._write(f"</{
|
|
298
|
-
|
|
299
|
-
def write_bounds(self, projection, instance) -> None:
|
|
300
|
-
"""Render the GML bounds for the complete instance"""
|
|
301
|
-
envelope = projection.feature_type.get_envelope(instance, self.output_crs)
|
|
302
|
-
if envelope is not None:
|
|
303
|
-
lower = " ".join(map(str, envelope.lower_corner))
|
|
304
|
-
upper = " ".join(map(str, envelope.upper_corner))
|
|
305
|
-
self._write(
|
|
306
|
-
f"""<gml:boundedBy><gml:Envelope srsDimension="2" srsName="{self.xml_srs_name}">
|
|
307
|
-
<gml:lowerCorner>{lower}</gml:lowerCorner>
|
|
308
|
-
<gml:upperCorner>{upper}</gml:upperCorner>
|
|
309
|
-
</gml:Envelope></gml:boundedBy>\n"""
|
|
310
|
-
)
|
|
299
|
+
self._write(f"</{feature_xml_qname}>\n")
|
|
311
300
|
|
|
312
301
|
def write_many(self, projection: FeatureProjection, xsd_element: XsdElement, value) -> None:
|
|
313
302
|
"""Write a node that has multiple values (e.g. array or queryset)."""
|
|
@@ -315,7 +304,8 @@ class GML32Renderer(OutputRenderer):
|
|
|
315
304
|
if value is None:
|
|
316
305
|
# No tag for optional element (see PropertyIsNull), otherwise xsi:nil node.
|
|
317
306
|
if xsd_element.min_occurs:
|
|
318
|
-
self.
|
|
307
|
+
xml_qname = self.xml_qnames[xsd_element]
|
|
308
|
+
self._write(f'<{xml_qname} xsi:nil="true"/>\n')
|
|
319
309
|
else:
|
|
320
310
|
for item in value:
|
|
321
311
|
self.write_xml_field(projection, xsd_element, value=item)
|
|
@@ -324,18 +314,18 @@ class GML32Renderer(OutputRenderer):
|
|
|
324
314
|
self, projection: FeatureProjection, xsd_element: XsdElement, value, extra_xmlns=""
|
|
325
315
|
):
|
|
326
316
|
"""Write the value of a single field."""
|
|
327
|
-
|
|
317
|
+
xml_qname = self.xml_qnames[xsd_element]
|
|
328
318
|
if value is None:
|
|
329
|
-
self._write(f'<{
|
|
319
|
+
self._write(f'<{xml_qname} xsi:nil="true"{extra_xmlns}/>\n')
|
|
330
320
|
elif xsd_element.type.is_complex_type:
|
|
331
321
|
# Expanded foreign relation / dictionary
|
|
332
322
|
self.write_xml_complex_type(projection, xsd_element, value, extra_xmlns=extra_xmlns)
|
|
333
323
|
else:
|
|
334
324
|
# As this is likely called 150.000 times during a request, this is optimized.
|
|
335
|
-
# Avoided a separate call to
|
|
325
|
+
# Avoided a separate call to value_to_xml_string() and avoided isinstance() here.
|
|
336
326
|
value_cls = value.__class__
|
|
337
327
|
if value_cls is str: # most cases
|
|
338
|
-
value =
|
|
328
|
+
value = tag_escape(value)
|
|
339
329
|
elif value_cls is datetime:
|
|
340
330
|
value = value.astimezone(timezone.utc).isoformat()
|
|
341
331
|
elif value_cls is bool:
|
|
@@ -349,50 +339,88 @@ class GML32Renderer(OutputRenderer):
|
|
|
349
339
|
):
|
|
350
340
|
# Non-string or custom field that extended a scalar.
|
|
351
341
|
# Any of the other types have a faster f"{value}" translation that produces the correct text.
|
|
352
|
-
value =
|
|
342
|
+
value = value_to_xml_string(value)
|
|
353
343
|
|
|
354
|
-
self._write(f"<{
|
|
344
|
+
self._write(f"<{xml_qname}{extra_xmlns}>{value}</{xml_qname}>\n")
|
|
355
345
|
|
|
356
346
|
def write_xml_complex_type(
|
|
357
347
|
self, projection: FeatureProjection, xsd_element: XsdElement, value, extra_xmlns=""
|
|
358
348
|
) -> None:
|
|
359
349
|
"""Write a single field, that consists of sub elements"""
|
|
360
|
-
self.
|
|
350
|
+
xml_qname = self.xml_qnames[xsd_element]
|
|
351
|
+
self._write(f"<{xml_qname}{extra_xmlns}>\n")
|
|
361
352
|
for sub_element in projection.xsd_child_nodes[xsd_element]:
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
self.
|
|
353
|
+
if sub_element.type.is_geometry:
|
|
354
|
+
# Separate call which can be optimized (no need to overload write_xml_field() for all calls).
|
|
355
|
+
self.write_gml_field(projection, sub_element, value)
|
|
365
356
|
else:
|
|
366
|
-
|
|
367
|
-
|
|
357
|
+
sub_value = sub_element.get_value(value)
|
|
358
|
+
if sub_element.is_many:
|
|
359
|
+
self.write_many(projection, sub_element, sub_value)
|
|
360
|
+
else:
|
|
361
|
+
self.write_xml_field(projection, sub_element, sub_value)
|
|
362
|
+
self._write(f"</{xml_qname}>\n")
|
|
368
363
|
|
|
369
364
|
def write_gml_field(
|
|
370
|
-
self,
|
|
365
|
+
self,
|
|
366
|
+
projection: FeatureProjection,
|
|
367
|
+
geo_element: GeometryXsdElement,
|
|
368
|
+
instance: models.Model,
|
|
369
|
+
extra_xmlns="",
|
|
371
370
|
) -> None:
|
|
372
371
|
"""Separate method to allow overriding this for db-performance optimizations."""
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
372
|
+
if geo_element.type is XsdTypes.gmlBoundingShapeType:
|
|
373
|
+
# Special case for <gml:boundedBy>, which doesn't need xsd:nil values, nor a gml:id.
|
|
374
|
+
# The value is not a GEOSGeometry either, as that is not exposed by django.contrib.gis.
|
|
375
|
+
envelope = cast(GmlBoundedByElement, geo_element).get_value(
|
|
376
|
+
instance, crs=projection.output_crs
|
|
377
|
+
)
|
|
378
|
+
if envelope is not None:
|
|
379
|
+
self._write(self.render_gml_bounds(envelope))
|
|
380
|
+
else:
|
|
381
|
+
# Regular geometry elements.
|
|
382
|
+
xml_qname = self.xml_qnames[geo_element]
|
|
383
|
+
value = geo_element.get_value(instance)
|
|
384
|
+
if value is None:
|
|
385
|
+
# Avoid incrementing gml_seq
|
|
386
|
+
self._write(f'<{xml_qname} xsi:nil="true"{extra_xmlns}/>\n')
|
|
387
|
+
else:
|
|
388
|
+
gml_id = self.get_gml_id(instance._meta.object_name, instance.pk)
|
|
389
|
+
|
|
390
|
+
# the following is somewhat faster, but will render GML 2, not GML 3.2:
|
|
391
|
+
# gml = value.ogr.gml
|
|
392
|
+
# pos = gml.find(">") # Will inject the gml:id="..." tag.
|
|
393
|
+
# gml = f"{gml[:pos]} gml:id="{attr_escape(gml_id)}"{gml[pos:]}"
|
|
380
394
|
|
|
381
|
-
|
|
395
|
+
gml = self.render_gml_value(projection, gml_id, value)
|
|
396
|
+
self._write(f"<{xml_qname}{extra_xmlns}>{gml}</{xml_qname}>\n")
|
|
382
397
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
398
|
+
def get_gml_id(self, prefix: str, object_id) -> str:
|
|
399
|
+
"""Generate the gml:id value, which is required for GML 3.2 objects."""
|
|
400
|
+
self.gml_seq += 1
|
|
401
|
+
return f"{prefix}.{object_id}.{self.gml_seq}"
|
|
387
402
|
|
|
388
|
-
|
|
389
|
-
|
|
403
|
+
def render_gml_bounds(self, envelope: BoundingBox) -> str:
|
|
404
|
+
"""Render the gml:boundedBy element that contains an Envelope.
|
|
405
|
+
This uses an internal object type,
|
|
406
|
+
as :mod:`django.contrib.gis.geos` only provides a 4-tuple envelope.
|
|
407
|
+
"""
|
|
408
|
+
return f"""<gml:boundedBy><gml:Envelope srsDimension="2" srsName="{attr_escape(envelope.crs.urn)}">
|
|
409
|
+
<gml:lowerCorner>{envelope.min_x} {envelope.min_y}</gml:lowerCorner>
|
|
410
|
+
<gml:upperCorner>{envelope.max_x} {envelope.max_y}</gml:upperCorner>
|
|
411
|
+
</gml:Envelope></gml:boundedBy>\n"""
|
|
390
412
|
|
|
391
|
-
def render_gml_value(
|
|
413
|
+
def render_gml_value(
|
|
414
|
+
self,
|
|
415
|
+
projection: FeatureProjection,
|
|
416
|
+
gml_id,
|
|
417
|
+
value: geos.GEOSGeometry | None,
|
|
418
|
+
extra_xmlns="",
|
|
419
|
+
) -> str:
|
|
392
420
|
"""Normal case: 'value' is raw geometry data.."""
|
|
393
421
|
# In case this is a standalone response, this will be the top-level element, hence includes the xmlns.
|
|
394
|
-
base_attrs = f' gml:id="{
|
|
395
|
-
|
|
422
|
+
base_attrs = f' gml:id="{attr_escape(gml_id)}" srsName="{attr_escape(projection.output_crs.urn)}"{extra_xmlns}'
|
|
423
|
+
projection.output_crs.apply_to(value)
|
|
396
424
|
return self._render_gml_type(value, base_attrs=base_attrs)
|
|
397
425
|
|
|
398
426
|
def _render_gml_type(self, value: geos.GEOSGeometry, base_attrs=""):
|
|
@@ -427,11 +455,16 @@ class GML32Renderer(OutputRenderer):
|
|
|
427
455
|
buf.write("</gml:Polygon>")
|
|
428
456
|
return buf.getvalue()
|
|
429
457
|
|
|
430
|
-
@register_geos_type(geos.
|
|
458
|
+
@register_geos_type(geos.GeometryCollection)
|
|
431
459
|
def render_gml_multi_geometry(self, value: geos.GeometryCollection, base_attrs):
|
|
432
460
|
children = "".join(self._render_gml_type(child) for child in value)
|
|
433
461
|
return f"<gml:MultiGeometry{base_attrs}>{children}</gml:MultiGeometry>"
|
|
434
462
|
|
|
463
|
+
@register_geos_type(geos.MultiPolygon)
|
|
464
|
+
def render_gml_multi_polygon(self, value: geos.GeometryCollection, base_attrs):
|
|
465
|
+
children = "".join(self._render_gml_type(child) for child in value)
|
|
466
|
+
return f"<gml:MultiPolygon{base_attrs}>{children}</gml:MultiPolygon>"
|
|
467
|
+
|
|
435
468
|
@register_geos_type(geos.MultiLineString)
|
|
436
469
|
def render_gml_multi_line_string(self, value: geos.MultiPoint, base_attrs):
|
|
437
470
|
children = "</gml:lineStringMember><gml:lineStringMember>".join(
|
|
@@ -477,15 +510,10 @@ class GML32Renderer(OutputRenderer):
|
|
|
477
510
|
"</gml:LineString>"
|
|
478
511
|
)
|
|
479
512
|
|
|
480
|
-
def get_gml_id(self, feature_type: FeatureType, object_id) -> str:
|
|
481
|
-
"""Generate the gml:id value, which is required for GML 3.2 objects."""
|
|
482
|
-
self.gml_seq += 1
|
|
483
|
-
return f"{feature_type.name}.{object_id}.{self.gml_seq}"
|
|
484
|
-
|
|
485
513
|
|
|
486
514
|
class DBGMLRenderingMixin:
|
|
487
515
|
|
|
488
|
-
def render_gml_value(self, gml_id, value: str, extra_xmlns=""):
|
|
516
|
+
def render_gml_value(self, projection, gml_id, value: str, extra_xmlns=""):
|
|
489
517
|
"""DB optimized: 'value' is pre-rendered GML XML string."""
|
|
490
518
|
# Write the gml:id inside the first tag
|
|
491
519
|
end_pos = value.find(">")
|
|
@@ -493,13 +521,13 @@ class DBGMLRenderingMixin:
|
|
|
493
521
|
id_pos = gml_tag.find("gml:id=")
|
|
494
522
|
if id_pos == -1:
|
|
495
523
|
# Inject
|
|
496
|
-
return f'{gml_tag} gml:id="{
|
|
524
|
+
return f'{gml_tag} gml:id="{attr_escape(gml_id)}"{extra_xmlns}{value[end_pos:]}'
|
|
497
525
|
else:
|
|
498
526
|
# Replace
|
|
499
527
|
end_pos1 = gml_tag.find('"', id_pos + 8)
|
|
500
528
|
return (
|
|
501
529
|
f"{gml_tag[:id_pos]}"
|
|
502
|
-
f'gml:id="{
|
|
530
|
+
f'gml:id="{attr_escape(gml_id)}'
|
|
503
531
|
f"{value[end_pos1:end_pos]}" # from " right until >
|
|
504
532
|
f"{extra_xmlns}" # extra namespaces?
|
|
505
533
|
f"{value[end_pos:]}" # from > and beyond
|
|
@@ -509,147 +537,134 @@ class DBGMLRenderingMixin:
|
|
|
509
537
|
class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
510
538
|
"""Faster GetFeature renderer that uses the database to render GML 3.2"""
|
|
511
539
|
|
|
512
|
-
|
|
513
|
-
def decorate_queryset(
|
|
514
|
-
cls,
|
|
515
|
-
projection: FeatureProjection,
|
|
516
|
-
queryset: models.QuerySet,
|
|
517
|
-
output_crs: CRS,
|
|
518
|
-
**params,
|
|
519
|
-
):
|
|
540
|
+
def decorate_queryset(self, projection: FeatureProjection, queryset: models.QuerySet):
|
|
520
541
|
"""Update the queryset to let the database render the GML output.
|
|
521
542
|
This is far more efficient than GeoDjango's logic, which performs a
|
|
522
543
|
C-API call for every single coordinate of a geometry.
|
|
523
544
|
"""
|
|
524
|
-
queryset = super().decorate_queryset(projection, queryset
|
|
545
|
+
queryset = super().decorate_queryset(projection, queryset)
|
|
525
546
|
|
|
526
|
-
# Retrieve
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
_as_envelope_gml=cls.get_db_envelope_as_gml(projection, queryset, output_crs),
|
|
531
|
-
**build_db_annotations(geo_selects, "_as_gml_{name}", AsGML),
|
|
547
|
+
# Retrieve gml:boundedBy in pre-rendered format.
|
|
548
|
+
if projection.has_bounded_by:
|
|
549
|
+
queryset = queryset.annotate(
|
|
550
|
+
_as_envelope_gml=self.get_db_envelope_as_gml(projection, queryset),
|
|
532
551
|
)
|
|
533
552
|
|
|
534
|
-
|
|
553
|
+
# Retrieve geometries as pre-rendered instead.
|
|
554
|
+
# Only take the geometries of the current level.
|
|
555
|
+
# The annotations for relations will be handled by prefetches and get_prefetch_queryset()
|
|
556
|
+
return replace_queryset_geometries(
|
|
557
|
+
queryset,
|
|
558
|
+
projection.geometry_elements,
|
|
559
|
+
projection.output_crs,
|
|
560
|
+
AsGML,
|
|
561
|
+
is_latlon=projection.output_crs.is_north_east_order,
|
|
562
|
+
)
|
|
535
563
|
|
|
536
|
-
@classmethod
|
|
537
564
|
def get_prefetch_queryset(
|
|
538
|
-
|
|
565
|
+
self,
|
|
539
566
|
projection: FeatureProjection,
|
|
540
567
|
feature_relation: FeatureRelation,
|
|
541
|
-
output_crs: CRS,
|
|
542
568
|
) -> models.QuerySet | None:
|
|
543
569
|
"""Perform DB annotations for prefetched relations too."""
|
|
544
|
-
|
|
545
|
-
if
|
|
570
|
+
queryset = super().get_prefetch_queryset(projection, feature_relation)
|
|
571
|
+
if queryset is None:
|
|
546
572
|
return None
|
|
547
573
|
|
|
548
574
|
# Find which fields are GML elements
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
return base
|
|
575
|
+
return replace_queryset_geometries(
|
|
576
|
+
queryset,
|
|
577
|
+
feature_relation.geometry_elements,
|
|
578
|
+
projection.output_crs,
|
|
579
|
+
AsGML,
|
|
580
|
+
is_latlon=projection.output_crs.is_north_east_order,
|
|
581
|
+
)
|
|
557
582
|
|
|
558
|
-
|
|
559
|
-
def get_db_envelope_as_gml(cls, projection: FeatureProjection, queryset, output_crs) -> AsGML:
|
|
583
|
+
def get_db_envelope_as_gml(self, projection: FeatureProjection, queryset) -> AsGML:
|
|
560
584
|
"""Offload the GML rendering of the envelope to the database.
|
|
561
585
|
|
|
562
586
|
This also avoids offloads the geometry union calculation to the DB.
|
|
563
587
|
"""
|
|
564
|
-
geo_fields_union =
|
|
565
|
-
return AsGML(
|
|
588
|
+
geo_fields_union = self._get_geometries_union(projection, queryset)
|
|
589
|
+
return AsGML(
|
|
590
|
+
geo_fields_union,
|
|
591
|
+
envelope=True,
|
|
592
|
+
is_latlon=projection.output_crs.is_north_east_order,
|
|
593
|
+
)
|
|
566
594
|
|
|
567
|
-
|
|
568
|
-
def _get_geometries_union(cls, projection: FeatureProjection, queryset, output_crs):
|
|
595
|
+
def _get_geometries_union(self, projection: FeatureProjection, queryset):
|
|
569
596
|
"""Combine all geometries of the model in a single SQL function."""
|
|
570
597
|
# Apply transforms where needed, in case some geometries use a different SRID.
|
|
571
598
|
return get_geometries_union(
|
|
572
599
|
[
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
output_srid=output_crs.srid,
|
|
577
|
-
)
|
|
578
|
-
for gml_element in projection.geometry_elements
|
|
600
|
+
get_db_geometry_target(geo_element, output_crs=projection.output_crs)
|
|
601
|
+
for geo_element in projection.all_geometry_elements
|
|
602
|
+
if geo_element.source is not None # excludes GmlBoundedByElement
|
|
579
603
|
],
|
|
580
604
|
using=queryset.db,
|
|
581
605
|
)
|
|
582
606
|
|
|
583
607
|
def write_gml_field(
|
|
584
|
-
self,
|
|
608
|
+
self,
|
|
609
|
+
projection: FeatureProjection,
|
|
610
|
+
geo_element: GeometryXsdElement,
|
|
611
|
+
instance: models.Model,
|
|
612
|
+
extra_xmlns="",
|
|
585
613
|
) -> None:
|
|
586
614
|
"""Write the value of an GML tag.
|
|
587
615
|
|
|
588
616
|
This optimized version takes a pre-rendered XML from the database query.
|
|
589
617
|
"""
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
618
|
+
xml_qname = self.xml_qnames[geo_element]
|
|
619
|
+
if geo_element.type is XsdTypes.gmlBoundingShapeType:
|
|
620
|
+
gml = instance._as_envelope_gml
|
|
621
|
+
if gml is None:
|
|
622
|
+
return
|
|
623
|
+
else:
|
|
624
|
+
value = get_db_rendered_geometry(instance, geo_element, AsGML)
|
|
625
|
+
if value is None:
|
|
626
|
+
# Avoid incrementing gml_seq, make nil tag.
|
|
627
|
+
self._write(f'<{xml_qname} xsi:nil="true"{extra_xmlns}/>\n')
|
|
628
|
+
return
|
|
596
629
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
630
|
+
# Get gml tag to write as value.
|
|
631
|
+
gml_id = self.get_gml_id(instance._meta.object_name, instance.pk)
|
|
632
|
+
gml = self.render_gml_value(projection, gml_id, value, extra_xmlns=extra_xmlns)
|
|
600
633
|
|
|
601
|
-
|
|
602
|
-
"""Generate the <gml:boundedBy> from DB pre-rendering."""
|
|
603
|
-
gml = instance._as_envelope_gml
|
|
604
|
-
if gml is not None:
|
|
605
|
-
self._write(f"<gml:boundedBy>{gml}</gml:boundedBy>\n")
|
|
634
|
+
self._write(f"<{xml_qname}{extra_xmlns}>{gml}</{xml_qname}>\n")
|
|
606
635
|
|
|
607
636
|
|
|
608
637
|
class GML32ValueRenderer(GML32Renderer):
|
|
609
638
|
"""Render the GetPropertyValue XML output in GML 3.2 format.
|
|
610
639
|
|
|
611
|
-
Geoserver seems to generate the element tag inside each
|
|
612
|
-
The GML standard demonstrates to render only their content inside a
|
|
613
|
-
(either plain text or an
|
|
640
|
+
Geoserver seems to generate the element tag inside each ``<wfs:member>`` element. We've applied this one.
|
|
641
|
+
The GML standard demonstrates to render only their content inside a ``<wfs:member>`` element
|
|
642
|
+
(either plain text or an ``<gml:...>`` tag). Not sure what is right here.
|
|
614
643
|
"""
|
|
615
644
|
|
|
616
645
|
content_type = "text/xml; charset=utf-8"
|
|
617
646
|
content_type_plain = "text/plain; charset=utf-8"
|
|
647
|
+
content_disposition = 'inline; filename="{typenames} {page} {date}-value.xml"'
|
|
648
|
+
content_disposition_plain = 'inline; filename="{typenames} {page} {date}-value.txt"'
|
|
618
649
|
xml_collection_tag = "ValueCollection"
|
|
619
650
|
xml_sub_collection_tag = "ValueCollection"
|
|
620
|
-
_escape_value = staticmethod(
|
|
651
|
+
_escape_value = staticmethod(value_to_xml_string)
|
|
621
652
|
gml_value_getter = itemgetter("member")
|
|
622
653
|
|
|
623
|
-
def
|
|
624
|
-
self.value_reference = value_reference
|
|
625
|
-
super().__init__(*args, **kwargs)
|
|
626
|
-
self.xsd_node: XsdNode | None = None
|
|
627
|
-
|
|
628
|
-
@classmethod
|
|
629
|
-
def decorate_queryset(
|
|
630
|
-
cls,
|
|
631
|
-
projection: FeatureProjection,
|
|
632
|
-
queryset: models.QuerySet,
|
|
633
|
-
output_crs: CRS,
|
|
634
|
-
**params,
|
|
635
|
-
):
|
|
654
|
+
def decorate_queryset(self, projection: FeatureProjection, queryset: models.QuerySet):
|
|
636
655
|
# Don't optimize queryset, it only retrieves one value
|
|
637
656
|
# The data is already limited to a ``queryset.values()`` in ``QueryExpression.get_queryset()``.
|
|
638
657
|
return queryset
|
|
639
658
|
|
|
640
|
-
def start_collection(self, sub_collection: SimpleFeatureCollection):
|
|
641
|
-
# Resolve which XsdNode is being rendered
|
|
642
|
-
match = sub_collection.feature_type.resolve_element(self.value_reference.xpath)
|
|
643
|
-
self.xsd_node = match.child
|
|
644
|
-
|
|
645
659
|
def write_by_id_response(
|
|
646
660
|
self, sub_collection: SimpleFeatureCollection, instance: dict, extra_xmlns
|
|
647
661
|
):
|
|
648
662
|
"""The value rendering only renders the value. not a complete feature"""
|
|
649
|
-
if
|
|
663
|
+
if sub_collection.projection.property_value_node.is_attribute:
|
|
650
664
|
# Output as plain text
|
|
651
665
|
self.content_type = self.content_type_plain # change for this instance!
|
|
652
|
-
self.
|
|
666
|
+
self.content_disposition = self.content_disposition_plain
|
|
667
|
+
self._escape_value = value_to_text # avoid XML escaping
|
|
653
668
|
else:
|
|
654
669
|
self._write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
|
655
670
|
|
|
@@ -660,23 +675,24 @@ class GML32ValueRenderer(GML32Renderer):
|
|
|
660
675
|
"""Write the XML for a single object.
|
|
661
676
|
In this case, it's only a single XML tag.
|
|
662
677
|
"""
|
|
663
|
-
|
|
678
|
+
xsd_node = projection.property_value_node
|
|
679
|
+
if xsd_node.type.is_geometry:
|
|
664
680
|
self.write_gml_field(
|
|
665
|
-
projection
|
|
666
|
-
cast(
|
|
681
|
+
projection,
|
|
682
|
+
cast(GeometryXsdElement, xsd_node),
|
|
667
683
|
instance,
|
|
668
684
|
extra_xmlns=extra_xmlns,
|
|
669
685
|
)
|
|
670
|
-
elif
|
|
686
|
+
elif xsd_node.is_attribute:
|
|
671
687
|
value = instance["member"]
|
|
672
688
|
if value is not None:
|
|
673
|
-
value =
|
|
689
|
+
value = xsd_node.format_raw_value(instance["member"]) # for gml:id
|
|
674
690
|
value = self._escape_value(value)
|
|
675
691
|
self._write(value)
|
|
676
|
-
elif
|
|
692
|
+
elif xsd_node.is_array:
|
|
677
693
|
if (value := instance["member"]) is not None:
|
|
678
694
|
# <wfs:member> doesn't allow multiple items as children, for new render as separate members.
|
|
679
|
-
|
|
695
|
+
xml_qname = self.xml_qnames[xsd_node]
|
|
680
696
|
first = True
|
|
681
697
|
for item in value:
|
|
682
698
|
if item is not None:
|
|
@@ -684,33 +700,42 @@ class GML32ValueRenderer(GML32Renderer):
|
|
|
684
700
|
self._write("</wfs:member>\n<wfs:member>")
|
|
685
701
|
|
|
686
702
|
item = self._escape_value(item)
|
|
687
|
-
self._write(f"<{
|
|
703
|
+
self._write(f"<{xml_qname}>{item}</{xml_qname}>\n")
|
|
688
704
|
first = False
|
|
689
|
-
elif
|
|
705
|
+
elif xsd_node.type.is_complex_type:
|
|
690
706
|
raise NotImplementedError("GetPropertyValue with complex types is not implemented")
|
|
691
707
|
# self.write_xml_complex_type(projection, self.xsd_node, instance['member'])
|
|
692
708
|
else:
|
|
693
709
|
self.write_xml_field(
|
|
694
710
|
projection,
|
|
695
|
-
cast(XsdElement,
|
|
711
|
+
cast(XsdElement, xsd_node),
|
|
696
712
|
value=instance["member"],
|
|
697
713
|
extra_xmlns=extra_xmlns,
|
|
698
714
|
)
|
|
699
715
|
|
|
700
716
|
def write_gml_field(
|
|
701
|
-
self,
|
|
717
|
+
self,
|
|
718
|
+
projection: FeatureProjection,
|
|
719
|
+
geo_element: GeometryXsdElement,
|
|
720
|
+
instance: dict,
|
|
721
|
+
extra_xmlns="",
|
|
702
722
|
) -> None:
|
|
703
723
|
"""Overwritten to allow dict access instead of model access."""
|
|
724
|
+
if geo_element.type is XsdTypes.gmlBoundingShapeType:
|
|
725
|
+
raise NotImplementedError(
|
|
726
|
+
"rendering <gml:boundedBy> in GetPropertyValue is not implemented."
|
|
727
|
+
)
|
|
728
|
+
|
|
704
729
|
value = self.gml_value_getter(instance) # "member" or "gml_member"
|
|
705
|
-
|
|
730
|
+
xml_qname = self.xml_qnames[geo_element]
|
|
706
731
|
if value is None:
|
|
707
732
|
# Avoid incrementing gml_seq
|
|
708
|
-
self._write(f'<{
|
|
733
|
+
self._write(f'<{xml_qname} xsi:nil="true"{extra_xmlns}/>\n')
|
|
709
734
|
return
|
|
710
735
|
|
|
711
|
-
gml_id = self.get_gml_id(
|
|
712
|
-
gml = self.render_gml_value(gml_id, value, extra_xmlns=extra_xmlns)
|
|
713
|
-
self._write(f"<{
|
|
736
|
+
gml_id = self.get_gml_id(geo_element.source.model._meta.object_name, instance["pk"])
|
|
737
|
+
gml = self.render_gml_value(projection, gml_id, value, extra_xmlns=extra_xmlns)
|
|
738
|
+
self._write(f"<{xml_qname}{extra_xmlns}>{gml}</{xml_qname}>\n")
|
|
714
739
|
|
|
715
740
|
|
|
716
741
|
class DBGML32ValueRenderer(DBGMLRenderingMixin, GML32ValueRenderer):
|
|
@@ -718,16 +743,19 @@ class DBGML32ValueRenderer(DBGMLRenderingMixin, GML32ValueRenderer):
|
|
|
718
743
|
|
|
719
744
|
gml_value_getter = itemgetter("gml_member")
|
|
720
745
|
|
|
721
|
-
|
|
722
|
-
def decorate_queryset(cls, projection: FeatureProjection, queryset, output_crs, **params):
|
|
746
|
+
def decorate_queryset(self, projection: FeatureProjection, queryset):
|
|
723
747
|
"""Update the queryset to let the database render the GML output."""
|
|
724
|
-
value_reference
|
|
725
|
-
|
|
726
|
-
if
|
|
748
|
+
# As this is a classmethod, self.value_reference is not available yet.
|
|
749
|
+
element = projection.property_value_node
|
|
750
|
+
if element.type.is_geometry:
|
|
727
751
|
# Add 'gml_member' to point to the pre-rendered GML version.
|
|
728
|
-
|
|
752
|
+
geo_element = cast(GeometryXsdElement, element)
|
|
729
753
|
return queryset.values(
|
|
730
|
-
"pk",
|
|
754
|
+
"pk",
|
|
755
|
+
gml_member=AsGML(
|
|
756
|
+
get_db_geometry_target(geo_element, projection.output_crs),
|
|
757
|
+
is_latlon=projection.output_crs.is_north_east_order,
|
|
758
|
+
),
|
|
731
759
|
)
|
|
732
760
|
else:
|
|
733
761
|
return queryset
|