django-gisserver 1.5.0__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.5.0.dist-info → django_gisserver-2.0.dist-info}/METADATA +14 -4
- django_gisserver-2.0.dist-info/RECORD +66 -0
- {django_gisserver-1.5.0.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 +56 -47
- gisserver/exceptions.py +26 -2
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +10 -4
- gisserver/extensions/queries.py +261 -0
- gisserver/features.py +220 -156
- gisserver/geometries.py +32 -37
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +291 -0
- gisserver/operations/base.py +122 -308
- gisserver/operations/wfs20.py +423 -337
- gisserver/output/__init__.py +9 -48
- gisserver/output/base.py +178 -139
- gisserver/output/csv.py +65 -74
- gisserver/output/geojson.py +34 -35
- gisserver/output/gml32.py +254 -246
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +52 -26
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +75 -170
- gisserver/output/xmlschema.py +85 -46
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +320 -0
- gisserver/parsers/fes20/__init__.py +13 -27
- gisserver/parsers/fes20/expressions.py +82 -38
- 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 +331 -127
- gisserver/parsers/fes20/sorting.py +104 -33
- gisserver/parsers/gml/__init__.py +12 -11
- gisserver/parsers/gml/base.py +5 -2
- 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 +9 -9
- gisserver/templates/gisserver/wfs/feature_field.html +2 -2
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +322 -259
- gisserver/views.py +198 -56
- 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.0.dist-info}/LICENSE +0 -0
- {django_gisserver-1.5.0.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,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,36 +147,25 @@ 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.
|
|
@@ -202,19 +204,19 @@ class GML32Renderer(OutputRenderer):
|
|
|
202
204
|
collection = self.collection
|
|
203
205
|
self.output = output = StringIO()
|
|
204
206
|
self._write = self.output.write
|
|
205
|
-
xmlns = self.render_xmlns().strip()
|
|
206
207
|
number_matched = collection.number_matched
|
|
207
208
|
number_matched = int(number_matched) if number_matched is not None else "unknown"
|
|
208
209
|
number_returned = collection.number_returned
|
|
209
210
|
next = previous = ""
|
|
210
211
|
if collection.next:
|
|
211
|
-
next = f' next="{
|
|
212
|
+
next = f' next="{attr_escape(collection.next)}"'
|
|
212
213
|
if collection.previous:
|
|
213
|
-
previous = f' previous="{
|
|
214
|
+
previous = f' previous="{attr_escape(collection.previous)}"'
|
|
214
215
|
|
|
215
216
|
self._write(
|
|
216
217
|
f"""<?xml version='1.0' encoding="UTF-8" ?>\n"""
|
|
217
|
-
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())}"'
|
|
218
220
|
f' timeStamp="{collection.timestamp}"'
|
|
219
221
|
f' numberMatched="{number_matched}"'
|
|
220
222
|
f' numberReturned="{int(number_returned)}"'
|
|
@@ -268,24 +270,20 @@ class GML32Renderer(OutputRenderer):
|
|
|
268
270
|
unless it's used for a GetPropertyById response.
|
|
269
271
|
"""
|
|
270
272
|
feature_type = projection.feature_type
|
|
273
|
+
feature_xml_qname = self.feature_qnames[feature_type]
|
|
271
274
|
|
|
272
275
|
# Write <app:FeatureTypeName> start node
|
|
273
|
-
pk =
|
|
274
|
-
self._write(f'<{
|
|
276
|
+
pk = tag_escape(str(instance.pk))
|
|
277
|
+
self._write(f'<{feature_xml_qname} gml:id="{feature_type.name}.{pk}"{extra_xmlns}>\n')
|
|
275
278
|
# Write all fields, both base class and local elements.
|
|
276
279
|
for xsd_element in projection.xsd_root_elements:
|
|
277
280
|
# Note that writing 5000 features with 30 tags means this code make 150.000 method calls.
|
|
278
281
|
# Hence, branching to different rendering styles is branched here instead of making such
|
|
279
282
|
# call into a generic "write_field()" function and stepping out from there.
|
|
280
283
|
|
|
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)
|
|
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)
|
|
289
287
|
else:
|
|
290
288
|
value = xsd_element.get_value(instance)
|
|
291
289
|
if xsd_element.is_many:
|
|
@@ -294,20 +292,7 @@ class GML32Renderer(OutputRenderer):
|
|
|
294
292
|
# e.g. <gml:name>, or all other <app:...> nodes.
|
|
295
293
|
self.write_xml_field(projection, xsd_element, value)
|
|
296
294
|
|
|
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
|
-
)
|
|
295
|
+
self._write(f"</{feature_xml_qname}>\n")
|
|
311
296
|
|
|
312
297
|
def write_many(self, projection: FeatureProjection, xsd_element: XsdElement, value) -> None:
|
|
313
298
|
"""Write a node that has multiple values (e.g. array or queryset)."""
|
|
@@ -315,7 +300,8 @@ class GML32Renderer(OutputRenderer):
|
|
|
315
300
|
if value is None:
|
|
316
301
|
# No tag for optional element (see PropertyIsNull), otherwise xsi:nil node.
|
|
317
302
|
if xsd_element.min_occurs:
|
|
318
|
-
self.
|
|
303
|
+
xml_qname = self.xml_qnames[xsd_element]
|
|
304
|
+
self._write(f'<{xml_qname} xsi:nil="true"/>\n')
|
|
319
305
|
else:
|
|
320
306
|
for item in value:
|
|
321
307
|
self.write_xml_field(projection, xsd_element, value=item)
|
|
@@ -324,18 +310,18 @@ class GML32Renderer(OutputRenderer):
|
|
|
324
310
|
self, projection: FeatureProjection, xsd_element: XsdElement, value, extra_xmlns=""
|
|
325
311
|
):
|
|
326
312
|
"""Write the value of a single field."""
|
|
327
|
-
|
|
313
|
+
xml_qname = self.xml_qnames[xsd_element]
|
|
328
314
|
if value is None:
|
|
329
|
-
self._write(f'<{
|
|
315
|
+
self._write(f'<{xml_qname} xsi:nil="true"{extra_xmlns}/>\n')
|
|
330
316
|
elif xsd_element.type.is_complex_type:
|
|
331
317
|
# Expanded foreign relation / dictionary
|
|
332
318
|
self.write_xml_complex_type(projection, xsd_element, value, extra_xmlns=extra_xmlns)
|
|
333
319
|
else:
|
|
334
320
|
# As this is likely called 150.000 times during a request, this is optimized.
|
|
335
|
-
# Avoided a separate call to
|
|
321
|
+
# Avoided a separate call to value_to_xml_string() and avoided isinstance() here.
|
|
336
322
|
value_cls = value.__class__
|
|
337
323
|
if value_cls is str: # most cases
|
|
338
|
-
value =
|
|
324
|
+
value = tag_escape(value)
|
|
339
325
|
elif value_cls is datetime:
|
|
340
326
|
value = value.astimezone(timezone.utc).isoformat()
|
|
341
327
|
elif value_cls is bool:
|
|
@@ -349,50 +335,88 @@ class GML32Renderer(OutputRenderer):
|
|
|
349
335
|
):
|
|
350
336
|
# Non-string or custom field that extended a scalar.
|
|
351
337
|
# Any of the other types have a faster f"{value}" translation that produces the correct text.
|
|
352
|
-
value =
|
|
338
|
+
value = value_to_xml_string(value)
|
|
353
339
|
|
|
354
|
-
self._write(f"<{
|
|
340
|
+
self._write(f"<{xml_qname}{extra_xmlns}>{value}</{xml_qname}>\n")
|
|
355
341
|
|
|
356
342
|
def write_xml_complex_type(
|
|
357
343
|
self, projection: FeatureProjection, xsd_element: XsdElement, value, extra_xmlns=""
|
|
358
344
|
) -> None:
|
|
359
345
|
"""Write a single field, that consists of sub elements"""
|
|
360
|
-
self.
|
|
346
|
+
xml_qname = self.xml_qnames[xsd_element]
|
|
347
|
+
self._write(f"<{xml_qname}{extra_xmlns}>\n")
|
|
361
348
|
for sub_element in projection.xsd_child_nodes[xsd_element]:
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
self.
|
|
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)
|
|
365
352
|
else:
|
|
366
|
-
|
|
367
|
-
|
|
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")
|
|
368
359
|
|
|
369
360
|
def write_gml_field(
|
|
370
|
-
self,
|
|
361
|
+
self,
|
|
362
|
+
projection: FeatureProjection,
|
|
363
|
+
geo_element: GeometryXsdElement,
|
|
364
|
+
instance: models.Model,
|
|
365
|
+
extra_xmlns="",
|
|
371
366
|
) -> None:
|
|
372
367
|
"""Separate method to allow overriding this for db-performance optimizations."""
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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)
|
|
380
385
|
|
|
381
|
-
|
|
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:]}"
|
|
382
390
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
# pos = gml.find(">") # Will inject the gml:id="..." tag.
|
|
386
|
-
# 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")
|
|
387
393
|
|
|
388
|
-
|
|
389
|
-
|
|
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}"
|
|
398
|
+
|
|
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"""
|
|
390
408
|
|
|
391
|
-
def render_gml_value(
|
|
409
|
+
def render_gml_value(
|
|
410
|
+
self,
|
|
411
|
+
projection: FeatureProjection,
|
|
412
|
+
gml_id,
|
|
413
|
+
value: geos.GEOSGeometry | None,
|
|
414
|
+
extra_xmlns="",
|
|
415
|
+
) -> str:
|
|
392
416
|
"""Normal case: 'value' is raw geometry data.."""
|
|
393
417
|
# 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
|
-
|
|
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)
|
|
396
420
|
return self._render_gml_type(value, base_attrs=base_attrs)
|
|
397
421
|
|
|
398
422
|
def _render_gml_type(self, value: geos.GEOSGeometry, base_attrs=""):
|
|
@@ -427,11 +451,16 @@ class GML32Renderer(OutputRenderer):
|
|
|
427
451
|
buf.write("</gml:Polygon>")
|
|
428
452
|
return buf.getvalue()
|
|
429
453
|
|
|
430
|
-
@register_geos_type(geos.
|
|
454
|
+
@register_geos_type(geos.GeometryCollection)
|
|
431
455
|
def render_gml_multi_geometry(self, value: geos.GeometryCollection, base_attrs):
|
|
432
456
|
children = "".join(self._render_gml_type(child) for child in value)
|
|
433
457
|
return f"<gml:MultiGeometry{base_attrs}>{children}</gml:MultiGeometry>"
|
|
434
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
|
+
|
|
435
464
|
@register_geos_type(geos.MultiLineString)
|
|
436
465
|
def render_gml_multi_line_string(self, value: geos.MultiPoint, base_attrs):
|
|
437
466
|
children = "</gml:lineStringMember><gml:lineStringMember>".join(
|
|
@@ -477,15 +506,10 @@ class GML32Renderer(OutputRenderer):
|
|
|
477
506
|
"</gml:LineString>"
|
|
478
507
|
)
|
|
479
508
|
|
|
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
509
|
|
|
486
510
|
class DBGMLRenderingMixin:
|
|
487
511
|
|
|
488
|
-
def render_gml_value(self, gml_id, value: str, extra_xmlns=""):
|
|
512
|
+
def render_gml_value(self, projection, gml_id, value: str, extra_xmlns=""):
|
|
489
513
|
"""DB optimized: 'value' is pre-rendered GML XML string."""
|
|
490
514
|
# Write the gml:id inside the first tag
|
|
491
515
|
end_pos = value.find(">")
|
|
@@ -493,13 +517,13 @@ class DBGMLRenderingMixin:
|
|
|
493
517
|
id_pos = gml_tag.find("gml:id=")
|
|
494
518
|
if id_pos == -1:
|
|
495
519
|
# Inject
|
|
496
|
-
return f'{gml_tag} gml:id="{
|
|
520
|
+
return f'{gml_tag} gml:id="{attr_escape(gml_id)}"{extra_xmlns}{value[end_pos:]}'
|
|
497
521
|
else:
|
|
498
522
|
# Replace
|
|
499
523
|
end_pos1 = gml_tag.find('"', id_pos + 8)
|
|
500
524
|
return (
|
|
501
525
|
f"{gml_tag[:id_pos]}"
|
|
502
|
-
f'gml:id="{
|
|
526
|
+
f'gml:id="{attr_escape(gml_id)}'
|
|
503
527
|
f"{value[end_pos1:end_pos]}" # from " right until >
|
|
504
528
|
f"{extra_xmlns}" # extra namespaces?
|
|
505
529
|
f"{value[end_pos:]}" # from > and beyond
|
|
@@ -509,100 +533,89 @@ class DBGMLRenderingMixin:
|
|
|
509
533
|
class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
510
534
|
"""Faster GetFeature renderer that uses the database to render GML 3.2"""
|
|
511
535
|
|
|
512
|
-
|
|
513
|
-
def decorate_queryset(
|
|
514
|
-
cls,
|
|
515
|
-
projection: FeatureProjection,
|
|
516
|
-
queryset: models.QuerySet,
|
|
517
|
-
output_crs: CRS,
|
|
518
|
-
**params,
|
|
519
|
-
):
|
|
536
|
+
def decorate_queryset(self, projection: FeatureProjection, queryset: models.QuerySet):
|
|
520
537
|
"""Update the queryset to let the database render the GML output.
|
|
521
538
|
This is far more efficient than GeoDjango's logic, which performs a
|
|
522
539
|
C-API call for every single coordinate of a geometry.
|
|
523
540
|
"""
|
|
524
|
-
queryset = super().decorate_queryset(projection, queryset
|
|
541
|
+
queryset = super().decorate_queryset(projection, queryset)
|
|
525
542
|
|
|
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),
|
|
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),
|
|
532
547
|
)
|
|
533
548
|
|
|
534
|
-
|
|
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
|
+
)
|
|
535
555
|
|
|
536
|
-
@classmethod
|
|
537
556
|
def get_prefetch_queryset(
|
|
538
|
-
|
|
557
|
+
self,
|
|
539
558
|
projection: FeatureProjection,
|
|
540
559
|
feature_relation: FeatureRelation,
|
|
541
|
-
output_crs: CRS,
|
|
542
560
|
) -> models.QuerySet | None:
|
|
543
561
|
"""Perform DB annotations for prefetched relations too."""
|
|
544
|
-
|
|
545
|
-
if
|
|
562
|
+
queryset = super().get_prefetch_queryset(projection, feature_relation)
|
|
563
|
+
if queryset is None:
|
|
546
564
|
return None
|
|
547
565
|
|
|
548
566
|
# Find which fields are GML elements
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
return base.defer(*geometries.keys()).annotate(
|
|
553
|
-
**build_db_annotations(geometries, "_as_gml_{name}", AsGML),
|
|
554
|
-
)
|
|
555
|
-
else:
|
|
556
|
-
return base
|
|
567
|
+
return replace_queryset_geometries(
|
|
568
|
+
queryset, feature_relation.geometry_elements, projection.output_crs, AsGML
|
|
569
|
+
)
|
|
557
570
|
|
|
558
|
-
|
|
559
|
-
def get_db_envelope_as_gml(cls, projection: FeatureProjection, queryset, output_crs) -> AsGML:
|
|
571
|
+
def get_db_envelope_as_gml(self, projection: FeatureProjection, queryset) -> AsGML:
|
|
560
572
|
"""Offload the GML rendering of the envelope to the database.
|
|
561
573
|
|
|
562
574
|
This also avoids offloads the geometry union calculation to the DB.
|
|
563
575
|
"""
|
|
564
|
-
geo_fields_union =
|
|
576
|
+
geo_fields_union = self._get_geometries_union(projection, queryset)
|
|
565
577
|
return AsGML(geo_fields_union, envelope=True)
|
|
566
578
|
|
|
567
|
-
|
|
568
|
-
def _get_geometries_union(cls, projection: FeatureProjection, queryset, output_crs):
|
|
579
|
+
def _get_geometries_union(self, projection: FeatureProjection, queryset):
|
|
569
580
|
"""Combine all geometries of the model in a single SQL function."""
|
|
570
581
|
# Apply transforms where needed, in case some geometries use a different SRID.
|
|
571
582
|
return get_geometries_union(
|
|
572
583
|
[
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
output_srid=output_crs.srid,
|
|
577
|
-
)
|
|
578
|
-
for gml_element in projection.geometry_elements
|
|
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
|
|
579
587
|
],
|
|
580
588
|
using=queryset.db,
|
|
581
589
|
)
|
|
582
590
|
|
|
583
591
|
def write_gml_field(
|
|
584
|
-
self,
|
|
592
|
+
self,
|
|
593
|
+
projection: FeatureProjection,
|
|
594
|
+
geo_element: GeometryXsdElement,
|
|
595
|
+
instance: models.Model,
|
|
596
|
+
extra_xmlns="",
|
|
585
597
|
) -> None:
|
|
586
598
|
"""Write the value of an GML tag.
|
|
587
599
|
|
|
588
600
|
This optimized version takes a pre-rendered XML from the database query.
|
|
589
601
|
"""
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
|
596
613
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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)
|
|
600
617
|
|
|
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")
|
|
618
|
+
self._write(f"<{xml_qname}{extra_xmlns}>{gml}</{xml_qname}>\n")
|
|
606
619
|
|
|
607
620
|
|
|
608
621
|
class GML32ValueRenderer(GML32Renderer):
|
|
@@ -615,41 +628,27 @@ class GML32ValueRenderer(GML32Renderer):
|
|
|
615
628
|
|
|
616
629
|
content_type = "text/xml; charset=utf-8"
|
|
617
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"'
|
|
618
633
|
xml_collection_tag = "ValueCollection"
|
|
619
634
|
xml_sub_collection_tag = "ValueCollection"
|
|
620
|
-
_escape_value = staticmethod(
|
|
635
|
+
_escape_value = staticmethod(value_to_xml_string)
|
|
621
636
|
gml_value_getter = itemgetter("member")
|
|
622
637
|
|
|
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
|
-
):
|
|
638
|
+
def decorate_queryset(self, projection: FeatureProjection, queryset: models.QuerySet):
|
|
636
639
|
# Don't optimize queryset, it only retrieves one value
|
|
637
640
|
# The data is already limited to a ``queryset.values()`` in ``QueryExpression.get_queryset()``.
|
|
638
641
|
return queryset
|
|
639
642
|
|
|
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
643
|
def write_by_id_response(
|
|
646
644
|
self, sub_collection: SimpleFeatureCollection, instance: dict, extra_xmlns
|
|
647
645
|
):
|
|
648
646
|
"""The value rendering only renders the value. not a complete feature"""
|
|
649
|
-
if
|
|
647
|
+
if sub_collection.projection.property_value_node.is_attribute:
|
|
650
648
|
# Output as plain text
|
|
651
649
|
self.content_type = self.content_type_plain # change for this instance!
|
|
652
|
-
self.
|
|
650
|
+
self.content_disposition = self.content_disposition_plain
|
|
651
|
+
self._escape_value = value_to_text # avoid XML escaping
|
|
653
652
|
else:
|
|
654
653
|
self._write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
|
655
654
|
|
|
@@ -660,23 +659,24 @@ class GML32ValueRenderer(GML32Renderer):
|
|
|
660
659
|
"""Write the XML for a single object.
|
|
661
660
|
In this case, it's only a single XML tag.
|
|
662
661
|
"""
|
|
663
|
-
|
|
662
|
+
xsd_node = projection.property_value_node
|
|
663
|
+
if xsd_node.type.is_geometry:
|
|
664
664
|
self.write_gml_field(
|
|
665
|
-
projection
|
|
666
|
-
cast(
|
|
665
|
+
projection,
|
|
666
|
+
cast(GeometryXsdElement, xsd_node),
|
|
667
667
|
instance,
|
|
668
668
|
extra_xmlns=extra_xmlns,
|
|
669
669
|
)
|
|
670
|
-
elif
|
|
670
|
+
elif xsd_node.is_attribute:
|
|
671
671
|
value = instance["member"]
|
|
672
672
|
if value is not None:
|
|
673
|
-
value =
|
|
673
|
+
value = xsd_node.format_raw_value(instance["member"]) # for gml:id
|
|
674
674
|
value = self._escape_value(value)
|
|
675
675
|
self._write(value)
|
|
676
|
-
elif
|
|
676
|
+
elif xsd_node.is_array:
|
|
677
677
|
if (value := instance["member"]) is not None:
|
|
678
678
|
# <wfs:member> doesn't allow multiple items as children, for new render as separate members.
|
|
679
|
-
|
|
679
|
+
xml_qname = self.xml_qnames[xsd_node]
|
|
680
680
|
first = True
|
|
681
681
|
for item in value:
|
|
682
682
|
if item is not None:
|
|
@@ -684,33 +684,42 @@ class GML32ValueRenderer(GML32Renderer):
|
|
|
684
684
|
self._write("</wfs:member>\n<wfs:member>")
|
|
685
685
|
|
|
686
686
|
item = self._escape_value(item)
|
|
687
|
-
self._write(f"<{
|
|
687
|
+
self._write(f"<{xml_qname}>{item}</{xml_qname}>\n")
|
|
688
688
|
first = False
|
|
689
|
-
elif
|
|
689
|
+
elif xsd_node.type.is_complex_type:
|
|
690
690
|
raise NotImplementedError("GetPropertyValue with complex types is not implemented")
|
|
691
691
|
# self.write_xml_complex_type(projection, self.xsd_node, instance['member'])
|
|
692
692
|
else:
|
|
693
693
|
self.write_xml_field(
|
|
694
694
|
projection,
|
|
695
|
-
cast(XsdElement,
|
|
695
|
+
cast(XsdElement, xsd_node),
|
|
696
696
|
value=instance["member"],
|
|
697
697
|
extra_xmlns=extra_xmlns,
|
|
698
698
|
)
|
|
699
699
|
|
|
700
700
|
def write_gml_field(
|
|
701
|
-
self,
|
|
701
|
+
self,
|
|
702
|
+
projection: FeatureProjection,
|
|
703
|
+
geo_element: GeometryXsdElement,
|
|
704
|
+
instance: dict,
|
|
705
|
+
extra_xmlns="",
|
|
702
706
|
) -> None:
|
|
703
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
|
+
|
|
704
713
|
value = self.gml_value_getter(instance) # "member" or "gml_member"
|
|
705
|
-
|
|
714
|
+
xml_qname = self.xml_qnames[geo_element]
|
|
706
715
|
if value is None:
|
|
707
716
|
# Avoid incrementing gml_seq
|
|
708
|
-
self._write(f'<{
|
|
717
|
+
self._write(f'<{xml_qname} xsi:nil="true"{extra_xmlns}/>\n')
|
|
709
718
|
return
|
|
710
719
|
|
|
711
|
-
gml_id = self.get_gml_id(
|
|
712
|
-
gml = self.render_gml_value(gml_id, value, extra_xmlns=extra_xmlns)
|
|
713
|
-
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")
|
|
714
723
|
|
|
715
724
|
|
|
716
725
|
class DBGML32ValueRenderer(DBGMLRenderingMixin, GML32ValueRenderer):
|
|
@@ -718,16 +727,15 @@ class DBGML32ValueRenderer(DBGMLRenderingMixin, GML32ValueRenderer):
|
|
|
718
727
|
|
|
719
728
|
gml_value_getter = itemgetter("gml_member")
|
|
720
729
|
|
|
721
|
-
|
|
722
|
-
def decorate_queryset(cls, projection: FeatureProjection, queryset, output_crs, **params):
|
|
730
|
+
def decorate_queryset(self, projection: FeatureProjection, queryset):
|
|
723
731
|
"""Update the queryset to let the database render the GML output."""
|
|
724
|
-
value_reference
|
|
725
|
-
|
|
726
|
-
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:
|
|
727
735
|
# Add 'gml_member' to point to the pre-rendered GML version.
|
|
728
|
-
|
|
736
|
+
geo_element = cast(GeometryXsdElement, element)
|
|
729
737
|
return queryset.values(
|
|
730
|
-
"pk", gml_member=AsGML(get_db_geometry_target(
|
|
738
|
+
"pk", gml_member=AsGML(get_db_geometry_target(geo_element, projection.output_crs))
|
|
731
739
|
)
|
|
732
740
|
else:
|
|
733
741
|
return queryset
|