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.
Files changed (77) hide show
  1. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +34 -8
  2. django_gisserver-2.1.dist-info/RECORD +68 -0
  3. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/compat.py +23 -0
  6. gisserver/conf.py +7 -0
  7. gisserver/crs.py +401 -0
  8. gisserver/db.py +126 -51
  9. gisserver/exceptions.py +132 -4
  10. gisserver/extensions/__init__.py +4 -0
  11. gisserver/{parsers/fes20 → extensions}/functions.py +131 -31
  12. gisserver/extensions/queries.py +266 -0
  13. gisserver/features.py +253 -181
  14. gisserver/geometries.py +64 -311
  15. gisserver/management/__init__.py +0 -0
  16. gisserver/management/commands/__init__.py +0 -0
  17. gisserver/management/commands/loadgeojson.py +311 -0
  18. gisserver/operations/base.py +130 -312
  19. gisserver/operations/wfs20.py +399 -375
  20. gisserver/output/__init__.py +14 -49
  21. gisserver/output/base.py +198 -144
  22. gisserver/output/csv.py +78 -75
  23. gisserver/output/geojson.py +37 -37
  24. gisserver/output/gml32.py +287 -259
  25. gisserver/output/iters.py +207 -0
  26. gisserver/output/results.py +73 -61
  27. gisserver/output/stored.py +143 -0
  28. gisserver/output/utils.py +81 -169
  29. gisserver/output/xmlschema.py +85 -46
  30. gisserver/parsers/__init__.py +10 -10
  31. gisserver/parsers/ast.py +426 -0
  32. gisserver/parsers/fes20/__init__.py +89 -31
  33. gisserver/parsers/fes20/expressions.py +172 -58
  34. gisserver/parsers/fes20/filters.py +116 -45
  35. gisserver/parsers/fes20/identifiers.py +66 -28
  36. gisserver/parsers/fes20/lookups.py +146 -0
  37. gisserver/parsers/fes20/operators.py +417 -161
  38. gisserver/parsers/fes20/sorting.py +113 -34
  39. gisserver/parsers/gml/__init__.py +17 -25
  40. gisserver/parsers/gml/base.py +36 -15
  41. gisserver/parsers/gml/geometries.py +105 -44
  42. gisserver/parsers/ows/__init__.py +25 -0
  43. gisserver/parsers/ows/kvp.py +198 -0
  44. gisserver/parsers/ows/requests.py +160 -0
  45. gisserver/parsers/query.py +179 -0
  46. gisserver/parsers/values.py +87 -4
  47. gisserver/parsers/wfs20/__init__.py +39 -0
  48. gisserver/parsers/wfs20/adhoc.py +253 -0
  49. gisserver/parsers/wfs20/base.py +148 -0
  50. gisserver/parsers/wfs20/projection.py +103 -0
  51. gisserver/parsers/wfs20/requests.py +483 -0
  52. gisserver/parsers/wfs20/stored.py +193 -0
  53. gisserver/parsers/xml.py +261 -0
  54. gisserver/projection.py +367 -0
  55. gisserver/static/gisserver/index.css +20 -4
  56. gisserver/templates/gisserver/base.html +12 -0
  57. gisserver/templates/gisserver/index.html +9 -15
  58. gisserver/templates/gisserver/service_description.html +12 -6
  59. gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
  60. gisserver/templates/gisserver/wfs/feature_field.html +3 -3
  61. gisserver/templates/gisserver/wfs/feature_type.html +35 -13
  62. gisserver/templatetags/gisserver_tags.py +20 -0
  63. gisserver/types.py +445 -313
  64. gisserver/views.py +227 -62
  65. django_gisserver-1.5.0.dist-info/RECORD +0 -54
  66. gisserver/parsers/base.py +0 -149
  67. gisserver/parsers/fes20/query.py +0 -285
  68. gisserver/parsers/tags.py +0 -102
  69. gisserver/queries/__init__.py +0 -37
  70. gisserver/queries/adhoc.py +0 -185
  71. gisserver/queries/base.py +0 -186
  72. gisserver/queries/projection.py +0 -240
  73. gisserver/queries/stored.py +0 -206
  74. gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
  75. gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
  76. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
  77. {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.features import FeatureType
35
- from gisserver.geometries import CRS
36
- from gisserver.parsers.fes20 import ValueReference
37
- from gisserver.queries import FeatureProjection, FeatureRelation
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 OutputRenderer
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
- def _tag_escape(s: str):
56
- return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
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
- from gisserver.queries import GetFeatureById
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.render_xmlns_standalone()
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 render_xmlns(self):
138
- """Generate the xmlns block that the document needs"""
139
- xsd_typenames = ",".join(
140
- sub_collection.feature_type.name for sub_collection in self.collection.results
141
- )
142
- schema_location = [
143
- f"{self.app_xml_namespace} {self.server_url}?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType&TYPENAMES={xsd_typenames}", # noqa: E501
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 <wfs:truncatedResponse> element
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
- closing_child = (
185
- f"</wfs:{self.xml_sub_collection_tag}></wfs:member>\n"
186
- if len(self.collection.results) > 1
187
- else ""
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
- xmlns = self.render_xmlns().strip()
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="{_attr_escape(collection.next)}"'
216
+ next = f' next="{attr_escape(collection.next)}"'
212
217
  if collection.previous:
213
- previous = f' previous="{_attr_escape(collection.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} {xmlns}"
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 = _tag_escape(str(instance.pk))
274
- self._write(f'<{feature_type.xml_name} gml:id="{feature_type.name}.{pk}"{extra_xmlns}>\n')
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
- if xsd_element.xml_name == "gml:boundedBy":
283
- # Special case for <gml:boundedBy>, so it will render with
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"</{feature_type.xml_name}>\n")
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._write(f'<{xsd_element.xml_name} xsi:nil="true"/>\n')
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
- xml_name = xsd_element.xml_name
317
+ xml_qname = self.xml_qnames[xsd_element]
328
318
  if value is None:
329
- self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
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 _value_to_xml_string() and avoided isinstance() here.
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 = _tag_escape(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 = _value_to_xml_string(value)
342
+ value = value_to_xml_string(value)
353
343
 
354
- self._write(f"<{xml_name}{extra_xmlns}>{value}</{xml_name}>\n")
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._write(f"<{xsd_element.xml_name}{extra_xmlns}>\n")
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
- sub_value = sub_element.get_value(value)
363
- if sub_element.is_many:
364
- self.write_many(projection, sub_element, sub_value)
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
- self.write_xml_field(projection, sub_element, sub_value)
367
- self._write(f"</{xsd_element.xml_name}>\n")
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, feature_type, xsd_element: XsdElement, instance: models.Model, extra_xmlns=""
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
- # Need to have instance.pk data here (and instance['pk'] for value rendering)
374
- value = xsd_element.get_value(instance)
375
- xml_name = xsd_element.xml_name
376
- if value is None:
377
- # Avoid incrementing gml_seq
378
- self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
379
- return
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
- gml_id = self.get_gml_id(feature_type, instance.pk)
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
- # the following is somewhat faster, but will render GML 2, not GML 3.2:
384
- # gml = value.ogr.gml
385
- # pos = gml.find(">") # Will inject the gml:id="..." tag.
386
- # gml = f"{gml[:pos]} gml:id="{_attr_escape(gml_id)}"{gml[pos:]}"
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
- gml = self.render_gml_value(gml_id, value)
389
- self._write(f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n")
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(self, gml_id, value: geos.GEOSGeometry | None, extra_xmlns="") -> str:
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="{_attr_escape(gml_id)}" srsName="{self.xml_srs_name}"{extra_xmlns}'
395
- self.output_crs.apply_to(value)
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.MultiPolygon)
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="{_attr_escape(gml_id)}"{extra_xmlns}{value[end_pos:]}'
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="{_attr_escape(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
- @classmethod
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, output_crs, **params)
545
+ queryset = super().decorate_queryset(projection, queryset)
525
546
 
526
- # Retrieve geometries as pre-rendered instead.
527
- geo_selects = get_db_geometry_selects(projection.geometry_elements, output_crs)
528
- if geo_selects:
529
- queryset = queryset.defer(*geo_selects.keys()).annotate(
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
- return queryset
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
- cls,
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
- base = super().get_prefetch_queryset(projection, feature_relation, output_crs)
545
- if base is None:
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
- geometries = get_db_geometry_selects(feature_relation.geometry_elements, output_crs)
550
- if geometries:
551
- # Exclude geometries from the fields, fetch them as pre-rendered annotations instead.
552
- return base.defer(*geometries.keys()).annotate(
553
- **build_db_annotations(geometries, "_as_gml_{name}", AsGML),
554
- )
555
- else:
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
- @classmethod
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 = cls._get_geometries_union(projection, queryset, output_crs)
565
- return AsGML(geo_fields_union, envelope=True)
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
- @classmethod
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
- conditional_transform(
574
- gml_element.orm_path,
575
- gml_element.source.srid,
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, feature_type, xsd_element: XsdElement, instance: models.Model, extra_xmlns=""
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
- value = get_db_annotation(instance, xsd_element.name, "_as_gml_{name}")
591
- xml_name = xsd_element.xml_name
592
- if value is None:
593
- # Avoid incrementing gml_seq
594
- self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
595
- return
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
- gml_id = self.get_gml_id(feature_type, instance.pk)
598
- gml = self.render_gml_value(gml_id, value, extra_xmlns=extra_xmlns)
599
- self._write(f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n")
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
- def write_bounds(self, projection, instance) -> None:
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 <wfs:member> element. We've applied this one.
612
- The GML standard demonstrates to render only their content inside a <wfs:member> element
613
- (either plain text or an <gml:...> tag). Not sure what is right here.
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(_value_to_xml_string)
651
+ _escape_value = staticmethod(value_to_xml_string)
621
652
  gml_value_getter = itemgetter("member")
622
653
 
623
- def __init__(self, *args, value_reference: ValueReference, **kwargs):
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 self.xsd_node.is_attribute:
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._escape_value = _value_to_text # avoid XML escaping
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
- if self.xsd_node.is_geometry:
678
+ xsd_node = projection.property_value_node
679
+ if xsd_node.type.is_geometry:
664
680
  self.write_gml_field(
665
- projection.feature_type,
666
- cast(XsdElement, self.xsd_node),
681
+ projection,
682
+ cast(GeometryXsdElement, xsd_node),
667
683
  instance,
668
684
  extra_xmlns=extra_xmlns,
669
685
  )
670
- elif self.xsd_node.is_attribute:
686
+ elif xsd_node.is_attribute:
671
687
  value = instance["member"]
672
688
  if value is not None:
673
- value = self.xsd_node.format_raw_value(instance["member"]) # for gml:id
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 self.xsd_node.is_array:
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
- xml_name = self.xsd_node.xml_name
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"<{xml_name}>{item}</{xml_name}>\n")
703
+ self._write(f"<{xml_qname}>{item}</{xml_qname}>\n")
688
704
  first = False
689
- elif self.xsd_node.type.is_complex_type:
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, self.xsd_node),
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, feature_type, xsd_element: XsdElement, instance: dict, extra_xmlns=""
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
- xml_name = xsd_element.xml_name
730
+ xml_qname = self.xml_qnames[geo_element]
706
731
  if value is None:
707
732
  # Avoid incrementing gml_seq
708
- self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
733
+ self._write(f'<{xml_qname} xsi:nil="true"{extra_xmlns}/>\n')
709
734
  return
710
735
 
711
- gml_id = self.get_gml_id(feature_type, instance["pk"])
712
- gml = self.render_gml_value(gml_id, value, extra_xmlns=extra_xmlns)
713
- self._write(f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n")
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
- @classmethod
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 = params["valueReference"]
725
- match = projection.feature_type.resolve_element(value_reference.xpath)
726
- if match.child.is_geometry:
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
- gml_element = cast(XsdElement, match.child)
752
+ geo_element = cast(GeometryXsdElement, element)
729
753
  return queryset.values(
730
- "pk", gml_member=AsGML(get_db_geometry_target(gml_element, output_crs))
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