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.
Files changed (74) hide show
  1. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/METADATA +14 -4
  2. django_gisserver-2.0.dist-info/RECORD +66 -0
  3. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.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/db.py +56 -47
  8. gisserver/exceptions.py +26 -2
  9. gisserver/extensions/__init__.py +4 -0
  10. gisserver/{parsers/fes20 → extensions}/functions.py +10 -4
  11. gisserver/extensions/queries.py +261 -0
  12. gisserver/features.py +220 -156
  13. gisserver/geometries.py +32 -37
  14. gisserver/management/__init__.py +0 -0
  15. gisserver/management/commands/__init__.py +0 -0
  16. gisserver/management/commands/loadgeojson.py +291 -0
  17. gisserver/operations/base.py +122 -308
  18. gisserver/operations/wfs20.py +423 -337
  19. gisserver/output/__init__.py +9 -48
  20. gisserver/output/base.py +178 -139
  21. gisserver/output/csv.py +65 -74
  22. gisserver/output/geojson.py +34 -35
  23. gisserver/output/gml32.py +254 -246
  24. gisserver/output/iters.py +207 -0
  25. gisserver/output/results.py +52 -26
  26. gisserver/output/stored.py +143 -0
  27. gisserver/output/utils.py +75 -170
  28. gisserver/output/xmlschema.py +85 -46
  29. gisserver/parsers/__init__.py +10 -10
  30. gisserver/parsers/ast.py +320 -0
  31. gisserver/parsers/fes20/__init__.py +13 -27
  32. gisserver/parsers/fes20/expressions.py +82 -38
  33. gisserver/parsers/fes20/filters.py +111 -43
  34. gisserver/parsers/fes20/identifiers.py +44 -26
  35. gisserver/parsers/fes20/lookups.py +144 -0
  36. gisserver/parsers/fes20/operators.py +331 -127
  37. gisserver/parsers/fes20/sorting.py +104 -33
  38. gisserver/parsers/gml/__init__.py +12 -11
  39. gisserver/parsers/gml/base.py +5 -2
  40. gisserver/parsers/gml/geometries.py +69 -35
  41. gisserver/parsers/ows/__init__.py +25 -0
  42. gisserver/parsers/ows/kvp.py +190 -0
  43. gisserver/parsers/ows/requests.py +158 -0
  44. gisserver/parsers/query.py +175 -0
  45. gisserver/parsers/values.py +26 -0
  46. gisserver/parsers/wfs20/__init__.py +37 -0
  47. gisserver/parsers/wfs20/adhoc.py +245 -0
  48. gisserver/parsers/wfs20/base.py +143 -0
  49. gisserver/parsers/wfs20/projection.py +103 -0
  50. gisserver/parsers/wfs20/requests.py +482 -0
  51. gisserver/parsers/wfs20/stored.py +192 -0
  52. gisserver/parsers/xml.py +249 -0
  53. gisserver/projection.py +357 -0
  54. gisserver/static/gisserver/index.css +12 -1
  55. gisserver/templates/gisserver/index.html +1 -1
  56. gisserver/templates/gisserver/service_description.html +2 -2
  57. gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
  58. gisserver/templates/gisserver/wfs/feature_field.html +2 -2
  59. gisserver/templatetags/gisserver_tags.py +20 -0
  60. gisserver/types.py +322 -259
  61. gisserver/views.py +198 -56
  62. django_gisserver-1.5.0.dist-info/RECORD +0 -54
  63. gisserver/parsers/base.py +0 -149
  64. gisserver/parsers/fes20/query.py +0 -285
  65. gisserver/parsers/tags.py +0 -102
  66. gisserver/queries/__init__.py +0 -37
  67. gisserver/queries/adhoc.py +0 -185
  68. gisserver/queries/base.py +0 -186
  69. gisserver/queries/projection.py +0 -240
  70. gisserver/queries/stored.py +0 -206
  71. gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
  72. gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
  73. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
  74. {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.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,36 +147,25 @@ 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.
@@ -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="{_attr_escape(collection.next)}"'
212
+ next = f' next="{attr_escape(collection.next)}"'
212
213
  if collection.previous:
213
- previous = f' previous="{_attr_escape(collection.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} {xmlns}"
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 = _tag_escape(str(instance.pk))
274
- self._write(f'<{feature_type.xml_name} gml:id="{feature_type.name}.{pk}"{extra_xmlns}>\n')
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
- 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)
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"</{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
- )
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._write(f'<{xsd_element.xml_name} xsi:nil="true"/>\n')
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
- xml_name = xsd_element.xml_name
313
+ xml_qname = self.xml_qnames[xsd_element]
328
314
  if value is None:
329
- self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
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 _value_to_xml_string() and avoided isinstance() here.
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 = _tag_escape(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 = _value_to_xml_string(value)
338
+ value = value_to_xml_string(value)
353
339
 
354
- self._write(f"<{xml_name}{extra_xmlns}>{value}</{xml_name}>\n")
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._write(f"<{xsd_element.xml_name}{extra_xmlns}>\n")
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
- sub_value = sub_element.get_value(value)
363
- if sub_element.is_many:
364
- self.write_many(projection, sub_element, sub_value)
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
- self.write_xml_field(projection, sub_element, sub_value)
367
- self._write(f"</{xsd_element.xml_name}>\n")
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, feature_type, xsd_element: XsdElement, instance: models.Model, extra_xmlns=""
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
- # 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
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
- gml_id = self.get_gml_id(feature_type, instance.pk)
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
- # 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:]}"
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
- gml = self.render_gml_value(gml_id, value)
389
- self._write(f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n")
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(self, gml_id, value: geos.GEOSGeometry | None, extra_xmlns="") -> str:
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="{_attr_escape(gml_id)}" srsName="{self.xml_srs_name}"{extra_xmlns}'
395
- self.output_crs.apply_to(value)
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.MultiPolygon)
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="{_attr_escape(gml_id)}"{extra_xmlns}{value[end_pos:]}'
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="{_attr_escape(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
- @classmethod
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, output_crs, **params)
541
+ queryset = super().decorate_queryset(projection, queryset)
525
542
 
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),
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
- return queryset
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
- cls,
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
- base = super().get_prefetch_queryset(projection, feature_relation, output_crs)
545
- if base is None:
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
- 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
567
+ return replace_queryset_geometries(
568
+ queryset, feature_relation.geometry_elements, projection.output_crs, AsGML
569
+ )
557
570
 
558
- @classmethod
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 = cls._get_geometries_union(projection, queryset, output_crs)
576
+ geo_fields_union = self._get_geometries_union(projection, queryset)
565
577
  return AsGML(geo_fields_union, envelope=True)
566
578
 
567
- @classmethod
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
- 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
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, feature_type, xsd_element: XsdElement, instance: models.Model, extra_xmlns=""
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
- 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
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
- 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")
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
- 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")
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(_value_to_xml_string)
635
+ _escape_value = staticmethod(value_to_xml_string)
621
636
  gml_value_getter = itemgetter("member")
622
637
 
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
- ):
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 self.xsd_node.is_attribute:
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._escape_value = _value_to_text # avoid XML escaping
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
- if self.xsd_node.is_geometry:
662
+ xsd_node = projection.property_value_node
663
+ if xsd_node.type.is_geometry:
664
664
  self.write_gml_field(
665
- projection.feature_type,
666
- cast(XsdElement, self.xsd_node),
665
+ projection,
666
+ cast(GeometryXsdElement, xsd_node),
667
667
  instance,
668
668
  extra_xmlns=extra_xmlns,
669
669
  )
670
- elif self.xsd_node.is_attribute:
670
+ elif xsd_node.is_attribute:
671
671
  value = instance["member"]
672
672
  if value is not None:
673
- value = self.xsd_node.format_raw_value(instance["member"]) # for gml:id
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 self.xsd_node.is_array:
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
- xml_name = self.xsd_node.xml_name
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"<{xml_name}>{item}</{xml_name}>\n")
687
+ self._write(f"<{xml_qname}>{item}</{xml_qname}>\n")
688
688
  first = False
689
- elif self.xsd_node.type.is_complex_type:
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, self.xsd_node),
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, feature_type, xsd_element: XsdElement, instance: dict, extra_xmlns=""
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
- xml_name = xsd_element.xml_name
714
+ xml_qname = self.xml_qnames[geo_element]
706
715
  if value is None:
707
716
  # Avoid incrementing gml_seq
708
- self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
717
+ self._write(f'<{xml_qname} xsi:nil="true"{extra_xmlns}/>\n')
709
718
  return
710
719
 
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")
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
- @classmethod
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 = params["valueReference"]
725
- match = projection.feature_type.resolve_element(value_reference.xpath)
726
- if match.child.is_geometry:
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
- gml_element = cast(XsdElement, match.child)
736
+ geo_element = cast(GeometryXsdElement, element)
729
737
  return queryset.values(
730
- "pk", gml_member=AsGML(get_db_geometry_target(gml_element, output_crs))
738
+ "pk", gml_member=AsGML(get_db_geometry_target(geo_element, projection.output_crs))
731
739
  )
732
740
  else:
733
741
  return queryset