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