django-gisserver 1.2.7__py3-none-any.whl → 1.4.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 (41) hide show
  1. {django_gisserver-1.2.7.dist-info → django_gisserver-1.4.0.dist-info}/METADATA +12 -12
  2. django_gisserver-1.4.0.dist-info/RECORD +54 -0
  3. {django_gisserver-1.2.7.dist-info → django_gisserver-1.4.0.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/conf.py +9 -12
  6. gisserver/db.py +6 -10
  7. gisserver/exceptions.py +1 -0
  8. gisserver/features.py +18 -29
  9. gisserver/geometries.py +11 -25
  10. gisserver/operations/base.py +19 -40
  11. gisserver/operations/wfs20.py +8 -20
  12. gisserver/output/__init__.py +7 -2
  13. gisserver/output/base.py +4 -13
  14. gisserver/output/csv.py +15 -19
  15. gisserver/output/geojson.py +16 -20
  16. gisserver/output/gml32.py +310 -283
  17. gisserver/output/gml32_lxml.py +612 -0
  18. gisserver/output/results.py +107 -22
  19. gisserver/output/utils.py +15 -5
  20. gisserver/output/xmlschema.py +7 -8
  21. gisserver/parsers/base.py +2 -4
  22. gisserver/parsers/fes20/expressions.py +6 -13
  23. gisserver/parsers/fes20/filters.py +6 -5
  24. gisserver/parsers/fes20/functions.py +4 -4
  25. gisserver/parsers/fes20/identifiers.py +1 -0
  26. gisserver/parsers/fes20/operators.py +16 -43
  27. gisserver/parsers/fes20/query.py +1 -3
  28. gisserver/parsers/fes20/sorting.py +1 -3
  29. gisserver/parsers/gml/__init__.py +1 -0
  30. gisserver/parsers/gml/base.py +1 -0
  31. gisserver/parsers/values.py +1 -3
  32. gisserver/queries/__init__.py +1 -0
  33. gisserver/queries/adhoc.py +3 -6
  34. gisserver/queries/base.py +2 -6
  35. gisserver/queries/stored.py +3 -6
  36. gisserver/types.py +59 -46
  37. gisserver/views.py +7 -10
  38. django_gisserver-1.2.7.dist-info/RECORD +0 -54
  39. gisserver/output/buffer.py +0 -64
  40. {django_gisserver-1.2.7.dist-info → django_gisserver-1.4.0.dist-info}/LICENSE +0 -0
  41. {django_gisserver-1.2.7.dist-info → django_gisserver-1.4.0.dist-info}/top_level.txt +0 -0
gisserver/output/gml32.py CHANGED
@@ -2,19 +2,24 @@
2
2
 
3
3
  Note that the Django format_html() / mark_safe() logic is not used here,
4
4
  as it's quite a performance improvement to just use html.escape().
5
+
6
+ We've tried replacing this code with lxml and that turned out to be much slower.
7
+ As some functions will be called 5000x, this code is also designed to avoid making
8
+ much extra method calls per field. Some bits are non-DRY inlined for this reason.
5
9
  """
10
+
6
11
  from __future__ import annotations
7
12
 
8
13
  import itertools
9
- import re
10
- from datetime import date, datetime, time
11
- from html import escape
14
+ from datetime import date, datetime, time, timezone
15
+ from decimal import Decimal as D
16
+ from io import StringIO
17
+ from operator import itemgetter
12
18
  from typing import cast
13
19
 
14
20
  from django.contrib.gis import geos
15
21
  from django.db import models
16
22
  from django.http import HttpResponse
17
- from django.utils.timezone import utc
18
23
 
19
24
  from gisserver.db import (
20
25
  AsGML,
@@ -29,21 +34,13 @@ from gisserver.exceptions import NotFound
29
34
  from gisserver.features import FeatureRelation, FeatureType
30
35
  from gisserver.geometries import CRS
31
36
  from gisserver.parsers.fes20 import ValueReference
32
- from gisserver.types import XsdComplexType, XsdElement
37
+ from gisserver.types import XsdComplexType, XsdElement, XsdNode
33
38
 
34
39
  from .base import OutputRenderer
35
- from .buffer import StringBuffer
36
40
  from .results import SimpleFeatureCollection
37
41
 
38
42
  GML_RENDER_FUNCTIONS = {}
39
- RE_GML_ID = re.compile(r'gml:id="[^"]+"')
40
-
41
-
42
- def default_if_none(value, default):
43
- if value is None:
44
- return default
45
- else:
46
- return value
43
+ AUTO_STR = (int, float, D, date, time)
47
44
 
48
45
 
49
46
  def register_geos_type(geos_type):
@@ -54,11 +51,49 @@ def register_geos_type(geos_type):
54
51
  return _inc
55
52
 
56
53
 
54
+ def _tag_escape(s: str):
55
+ return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
56
+
57
+
58
+ def _attr_escape(s: str):
59
+ # Slightly faster then html.escape() as it doesn't replace single quotes.
60
+ # Having tried all possible variants, this code still outperforms other forms of escaping.
61
+ return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
62
+
63
+
64
+ def _value_to_xml_string(value):
65
+ # Simple scalar value
66
+ if isinstance(value, str): # most cases
67
+ return _tag_escape(value)
68
+ elif isinstance(value, datetime):
69
+ return value.astimezone(timezone.utc).isoformat()
70
+ elif isinstance(value, bool):
71
+ return "true" if value else "false"
72
+ elif isinstance(value, AUTO_STR):
73
+ return value # no need for _tag_escape(), and f"{value}" works faster.
74
+ else:
75
+ return _tag_escape(str(value))
76
+
77
+
78
+ def _value_to_text(value):
79
+ # Simple scalar value, no XML escapes
80
+ if isinstance(value, str): # most cases
81
+ return value
82
+ elif isinstance(value, datetime):
83
+ return value.astimezone(timezone.utc).isoformat()
84
+ elif isinstance(value, bool):
85
+ return "true" if value else "false"
86
+ else:
87
+ return value # f"{value} works faster and produces the right format.
88
+
89
+
57
90
  class GML32Renderer(OutputRenderer):
58
91
  """Render the GetFeature XML output in GML 3.2 format"""
59
92
 
60
93
  content_type = "text/xml; charset=utf-8"
61
94
  xml_collection_tag = "FeatureCollection"
95
+ chunk_size = 40_000
96
+ gml_seq = 0
62
97
 
63
98
  def get_response(self):
64
99
  """Render the output as streaming response."""
@@ -67,12 +102,12 @@ class GML32Renderer(OutputRenderer):
67
102
  if isinstance(self.source_query, GetFeatureById):
68
103
  # WFS spec requires that GetFeatureById output only returns the contents.
69
104
  # The streaming response is avoided here, to allow returning a 404.
70
- return self.get_standalone_response()
105
+ return self.get_by_id_response()
71
106
  else:
72
107
  # Use default streaming response, with render_stream()
73
108
  return super().get_response()
74
109
 
75
- def get_standalone_response(self):
110
+ def get_by_id_response(self):
76
111
  """Render a standalone item, for GetFeatureById"""
77
112
  sub_collection = self.collection.results[0]
78
113
  self.start_collection(sub_collection)
@@ -80,27 +115,27 @@ class GML32Renderer(OutputRenderer):
80
115
  if instance is None:
81
116
  raise NotFound("Feature not found.")
82
117
 
83
- body = self.render_wfs_member_contents(
118
+ self.output = StringIO()
119
+ self._write = self.output.write
120
+ self.write_by_id_response(
121
+ sub_collection, instance, extra_xmlns=self.render_xmlns_standalone()
122
+ )
123
+ content = self.output.getvalue()
124
+ return HttpResponse(content, content_type=self.content_type)
125
+
126
+ def write_by_id_response(self, sub_collection: SimpleFeatureCollection, instance, extra_xmlns):
127
+ """Default behavior for standalone response is writing a feature (can be changed by GetPropertyValue)"""
128
+ self._write('<?xml version="1.0" encoding="UTF-8"?>\n')
129
+ self.write_feature(
84
130
  feature_type=sub_collection.feature_type,
85
131
  instance=instance,
86
- extra_xmlns=self.render_xmlns_standalone(),
87
- ).lstrip(" ")
88
-
89
- if body.startswith("<"):
90
- return HttpResponse(
91
- content=f'<?xml version="1.0" encoding="UTF-8"?>\n{body}',
92
- content_type=self.content_type,
93
- )
94
- else:
95
- # Best guess for GetFeatureById combined with
96
- # GetPropertyValue&VALUEREFERENCE=@gml:id
97
- return HttpResponse(body, content_type="text/plain")
132
+ extra_xmlns=extra_xmlns,
133
+ )
98
134
 
99
135
  def render_xmlns(self):
100
136
  """Generate the xmlns block that the document needs"""
101
137
  xsd_typenames = ",".join(
102
- sub_collection.feature_type.name
103
- for sub_collection in self.collection.results
138
+ sub_collection.feature_type.name for sub_collection in self.collection.results
104
139
  )
105
140
  schema_location = [
106
141
  f"{self.app_xml_namespace} {self.server_url}?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType&TYPENAMES={xsd_typenames}", # noqa: E501
@@ -115,15 +150,15 @@ class GML32Renderer(OutputRenderer):
115
150
  'xmlns:app="{app_xml_namespace}" '
116
151
  'xsi:schemaLocation="{schema_location}"'
117
152
  ).format(
118
- app_xml_namespace=escape(self.app_xml_namespace),
119
- schema_location=escape(" ".join(schema_location)),
153
+ app_xml_namespace=_attr_escape(self.app_xml_namespace),
154
+ schema_location=_attr_escape(" ".join(schema_location)),
120
155
  )
121
156
 
122
157
  def render_xmlns_standalone(self):
123
158
  """Generate the xmlns block that the document needs"""
124
159
  # xsi is needed for "xsi:nil="true"' attributes.
125
160
  return (
126
- f' xmlns:app="{escape(self.app_xml_namespace)}"'
161
+ f' xmlns:app="{_attr_escape(self.app_xml_namespace)}"'
127
162
  ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
128
163
  ' xmlns:gml="http://www.opengis.net/gml/3.2"'
129
164
  )
@@ -133,20 +168,19 @@ class GML32Renderer(OutputRenderer):
133
168
  This renders the standard <wfs:FeatureCollection> / <wfs:ValueCollection>
134
169
  """
135
170
  collection = self.collection
136
- output = StringBuffer()
171
+ self.output = output = StringIO()
172
+ self._write = self.output.write
137
173
  xmlns = self.render_xmlns().strip()
138
174
  number_matched = collection.number_matched
139
- number_matched = (
140
- int(number_matched) if number_matched is not None else "unknown"
141
- )
175
+ number_matched = int(number_matched) if number_matched is not None else "unknown"
142
176
  number_returned = collection.number_returned
143
177
  next = previous = ""
144
178
  if collection.next:
145
- next = f' next="{escape(collection.next)}"'
179
+ next = f' next="{_attr_escape(collection.next)}"'
146
180
  if collection.previous:
147
- previous = f' previous="{escape(collection.previous)}"'
181
+ previous = f' previous="{_attr_escape(collection.previous)}"'
148
182
 
149
- output.write(
183
+ self._write(
150
184
  f"""<?xml version='1.0' encoding="UTF-8" ?>\n"""
151
185
  f"<wfs:{self.xml_collection_tag} {xmlns}"
152
186
  f' timeStamp="{collection.timestamp}"'
@@ -161,7 +195,7 @@ class GML32Renderer(OutputRenderer):
161
195
  for sub_collection in collection.results:
162
196
  self.start_collection(sub_collection)
163
197
  if has_multiple_collections:
164
- output.write(
198
+ self._write(
165
199
  f"<wfs:member>\n"
166
200
  f"<wfs:{self.xml_collection_tag}"
167
201
  f' timeStamp="{collection.timestamp}"'
@@ -170,193 +204,175 @@ class GML32Renderer(OutputRenderer):
170
204
  )
171
205
 
172
206
  for instance in sub_collection:
173
- output.write(
174
- self.render_wfs_member(sub_collection.feature_type, instance)
175
- )
207
+ self.gml_seq = 0 # need to increment this between write_xml_field calls
208
+ self._write("<wfs:member>\n")
209
+ self.write_feature(sub_collection.feature_type, instance)
210
+ self._write("</wfs:member>\n")
176
211
 
177
212
  # Only perform a 'yield' every once in a while,
178
213
  # as it goes back-and-forth for writing it to the client.
179
- if output.is_full():
180
- yield output.flush()
214
+ if output.tell() > self.chunk_size:
215
+ xml_chunk = output.getvalue()
216
+ output.seek(0)
217
+ output.truncate(0)
218
+ yield xml_chunk
181
219
 
182
220
  if has_multiple_collections:
183
- output.write(f"</wfs:{self.xml_collection_tag}>\n</wfs:member>\n")
221
+ self._write(f"</wfs:{self.xml_collection_tag}>\n</wfs:member>\n")
184
222
 
185
- output.write(f"</wfs:{self.xml_collection_tag}>\n")
186
- yield output.flush()
223
+ self._write(f"</wfs:{self.xml_collection_tag}>\n")
224
+ yield output.getvalue()
187
225
 
188
226
  def start_collection(self, sub_collection: SimpleFeatureCollection):
189
227
  """Hook to allow initialization per feature type"""
190
- pass
191
228
 
192
- def render_wfs_member(
229
+ def write_feature(
193
230
  self, feature_type: FeatureType, instance: models.Model, extra_xmlns=""
194
- ):
195
- """Write the full <wfs:member> block."""
196
- body = self.render_wfs_member_contents(
197
- feature_type, instance, extra_xmlns=extra_xmlns
198
- )
199
- return f"<wfs:member>\n{body}</wfs:member>\n"
200
-
201
- def render_wfs_member_contents(
202
- self, feature_type: FeatureType, instance: models.Model, extra_xmlns=""
203
- ) -> str:
231
+ ) -> None:
204
232
  """Write the contents of the object value.
205
233
 
206
234
  This output is typically wrapped in <wfs:member> tags
207
235
  unless it's used for a GetPropertyById response.
208
236
  """
209
- self.gml_seq = 0 # need to increment this between render_xml_field calls
210
-
211
237
  # Write <app:FeatureTypeName> start node
212
- pk = escape(str(instance.pk))
213
- output = StringBuffer()
214
- output.write(
215
- f'<{feature_type.xml_name} gml:id="{feature_type.name}.{pk}"{extra_xmlns}>\n'
216
- )
217
-
218
- # Add all base class members, in their correct ordering
219
- # By having these as XsdElement objects instead of hard-coded writes,
220
- # the query/filter logic also works for these elements.
221
- if feature_type.xsd_type.base.is_complex_type:
222
- for xsd_element in feature_type.xsd_type.base.elements:
238
+ pk = _tag_escape(str(instance.pk))
239
+ self._write(f'<{feature_type.xml_name} gml:id="{feature_type.name}.{pk}"{extra_xmlns}>\n')
240
+
241
+ # Write all fields, both base class and local elements.
242
+ for xsd_element in feature_type.xsd_type.all_elements:
243
+ # Note that writing 5000 features with 30 tags means this code make 150.000 method calls.
244
+ # Hence, branching to different rendering styles is branched here instead of making such
245
+ # call into a generic "write_field()" function and stepping out from there.
246
+ if xsd_element.is_geometry:
223
247
  if xsd_element.xml_name == "gml:boundedBy":
224
248
  # Special case for <gml:boundedBy>, so it will render with
225
249
  # the output CRS and can be overwritten with DB-rendered GML.
226
- gml = self.render_bounds(feature_type, instance)
227
- if gml is not None:
228
- output.write(gml)
250
+ self.write_bounds(feature_type, instance)
251
+ else:
252
+ # Separate call which can be optimized (no need to overload write_xml_field() for all calls).
253
+ self.write_gml_field(feature_type, xsd_element, instance)
254
+ else:
255
+ value = xsd_element.get_value(instance)
256
+ if xsd_element.is_many:
257
+ self.write_many(feature_type, xsd_element, value)
229
258
  else:
230
259
  # e.g. <gml:name>, or all other <app:...> nodes.
231
- output.write(
232
- self.render_element(feature_type, xsd_element, instance)
233
- )
260
+ self.write_xml_field(feature_type, xsd_element, value)
234
261
 
235
- # Add all members
236
- for xsd_element in feature_type.xsd_type.elements:
237
- output.write(self.render_element(feature_type, xsd_element, instance))
262
+ self._write(f"</{feature_type.xml_name}>\n")
238
263
 
239
- output.write(f"</{feature_type.xml_name}>\n")
240
- return output.getvalue()
241
-
242
- def render_bounds(self, feature_type, instance) -> str | None:
264
+ def write_bounds(self, feature_type, instance) -> None:
243
265
  """Render the GML bounds for the complete instance"""
244
266
  envelope = feature_type.get_envelope(instance, self.output_crs)
245
267
  if envelope is not None:
246
268
  lower = " ".join(map(str, envelope.lower_corner))
247
269
  upper = " ".join(map(str, envelope.upper_corner))
248
- return f"""<gml:boundedBy><gml:Envelope srsDimension="2" srsName="{self.xml_srs_name}">
270
+ self._write(
271
+ f"""<gml:boundedBy><gml:Envelope srsDimension="2" srsName="{self.xml_srs_name}">
249
272
  <gml:lowerCorner>{lower}</gml:lowerCorner>
250
273
  <gml:upperCorner>{upper}</gml:upperCorner>
251
274
  </gml:Envelope></gml:boundedBy>\n"""
252
-
253
- def render_element(
254
- self, feature_type, xsd_element: XsdElement, instance: models.Model
255
- ):
256
- """Rendering of a single field."""
257
- value = xsd_element.get_value(instance)
258
- if xsd_element.is_geometry and value is not None:
259
- # None check happens here to avoid incrementing for none values
260
- self.gml_seq += 1
261
- return self.render_gml_field(
262
- feature_type,
263
- xsd_element,
264
- value,
265
- gml_id=self.get_gml_id(feature_type, instance.pk, seq=self.gml_seq),
266
275
  )
267
- else:
268
- return self.render_xml_field(feature_type, xsd_element, value)
269
276
 
270
- def render_xml_field(
271
- self, feature_type: FeatureType, xsd_element: XsdElement, value, extra_xmlns=""
272
- ) -> str:
273
- """Write the value of a single field."""
274
- if xsd_element.is_many:
275
- if value is None:
276
- # No tag for optional element (see PropertyIsNull), otherwise xsi:nil node.
277
- if xsd_element.min_occurs == 0:
278
- return ""
279
- else:
280
- return f'<{xsd_element.xml_name} xsi:nil="true"{extra_xmlns}/>\n'
281
- else:
282
- # Render the tag multiple times
283
- if xsd_element.type.is_complex_type:
284
- # If the retrieved QuerySet was not filtered yet, do so now. This can't
285
- # be done in get_value() because the FeatureType is not known there.
286
- value = feature_type.filter_related_queryset(value)
287
-
288
- return "".join(
289
- self._render_xml_field(
290
- feature_type, xsd_element, value=item, extra_xmlns=extra_xmlns
291
- )
292
- for item in value
293
- )
277
+ def write_many(self, feature_type: FeatureType, xsd_element: XsdElement, value) -> None:
278
+ """Write a node that has multiple values (e.g. array or queryset)."""
279
+ # some <app:...> node that has multiple values
280
+ if value is None:
281
+ # No tag for optional element (see PropertyIsNull), otherwise xsi:nil node.
282
+ if xsd_element.min_occurs:
283
+ self._write(f'<{xsd_element.xml_name} xsi:nil="true"/>\n')
294
284
  else:
295
- return self._render_xml_field(
296
- feature_type, xsd_element, value, extra_xmlns=extra_xmlns
297
- )
285
+ # Render the tag multiple times
286
+ if xsd_element.type.is_complex_type:
287
+ # If the retrieved QuerySet was not filtered yet, do so now. This can't
288
+ # be done in get_value() because the FeatureType is not known there.
289
+ value = feature_type.filter_related_queryset(value)
290
+
291
+ for item in value:
292
+ self.write_xml_field(feature_type, xsd_element, value=item)
298
293
 
299
- def _render_xml_field(
294
+ def write_xml_field(
300
295
  self, feature_type: FeatureType, xsd_element: XsdElement, value, extra_xmlns=""
301
- ) -> str:
296
+ ):
297
+ """Write the value of a single field."""
302
298
  xml_name = xsd_element.xml_name
303
299
  if value is None:
304
- return f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n'
300
+ self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
305
301
  elif xsd_element.type.is_complex_type:
306
302
  # Expanded foreign relation / dictionary
307
- return self.render_xml_complex_type(feature_type, xsd_element, value)
308
- elif isinstance(value, datetime):
309
- value = value.astimezone(utc).isoformat()
310
- elif isinstance(value, (date, time)):
311
- value = value.isoformat()
312
- elif isinstance(value, bool):
313
- value = "true" if value else "false"
303
+ self.write_xml_complex_type(feature_type, xsd_element, value, extra_xmlns=extra_xmlns)
314
304
  else:
315
- value = escape(str(value))
316
-
317
- return f"<{xml_name}{extra_xmlns}>{value}</{xml_name}>\n"
318
-
319
- def render_xml_complex_type(self, feature_type, xsd_element, value) -> str:
305
+ # As this is likely called 150.000 times during a request, this is optimized.
306
+ # Avoided a separate call to _value_to_xml_string() and avoided isinstance() here.
307
+ value_cls = value.__class__
308
+ if value_cls is str: # most cases
309
+ value = _tag_escape(value)
310
+ elif value_cls is datetime:
311
+ value = value.astimezone(timezone.utc).isoformat()
312
+ elif value_cls is bool:
313
+ value = "true" if value else "false"
314
+ elif (
315
+ value_cls is not int
316
+ and value_cls is not float
317
+ and value_cls is not D
318
+ and value_cls is not date
319
+ and value_cls is not time
320
+ ):
321
+ # Non-string or custom field that extended a scalar.
322
+ # Any of the other types have a faster f"{value}" translation that produces the correct text.
323
+ value = _value_to_xml_string(value)
324
+
325
+ self._write(f"<{xml_name}{extra_xmlns}>{value}</{xml_name}>\n")
326
+
327
+ def write_xml_complex_type(self, feature_type, xsd_element, value, extra_xmlns="") -> None:
320
328
  """Write a single field, that consists of sub elements"""
321
329
  xsd_type = cast(XsdComplexType, xsd_element.type)
322
- output = StringBuffer()
323
- output.write(f"<{xsd_element.xml_name}>\n")
330
+ self._write(f"<{xsd_element.xml_name}{extra_xmlns}>\n")
324
331
  for sub_element in xsd_type.elements:
325
- output.write(self.render_element(feature_type, sub_element, instance=value))
326
- output.write(f"</{xsd_element.xml_name}>\n")
327
- return output.getvalue()
328
-
329
- def render_gml_field(
330
- self,
331
- feature_type: FeatureType,
332
- xsd_element: XsdElement,
333
- value,
334
- gml_id,
335
- extra_xmlns="",
336
- ) -> str:
337
- """Write the value of an GML tag"""
332
+ sub_value = sub_element.get_value(value)
333
+ if sub_element.is_many:
334
+ self.write_many(feature_type, sub_element, sub_value)
335
+ else:
336
+ self.write_xml_field(feature_type, sub_element, sub_value)
337
+ self._write(f"</{xsd_element.xml_name}>\n")
338
+
339
+ def write_gml_field(
340
+ self, feature_type, xsd_element: XsdElement, instance: models.Model, extra_xmlns=""
341
+ ) -> None:
342
+ """Separate method to allow overriding this for db-performance optimizations."""
343
+ # Need to have instance.pk data here (and instance['pk'] for value rendering)
344
+ value = xsd_element.get_value(instance)
338
345
  xml_name = xsd_element.xml_name
339
- gml = self.render_gml_value(value, gml_id=gml_id)
340
- return f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n"
346
+ if value is None:
347
+ # Avoid incrementing gml_seq
348
+ self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
349
+ return
350
+
351
+ gml_id = self.get_gml_id(feature_type, instance.pk)
341
352
 
342
- def render_gml_value(
343
- self, value: geos.GEOSGeometry, gml_id: str, extra_xmlns=""
344
- ) -> str:
345
- """Convert a Geometry into GML syntax."""
353
+ # the following is somewhat faster, but will render GML 2, not GML 3.2:
354
+ # gml = value.ogr.gml
355
+ # pos = gml.find(">") # Will inject the gml:id="..." tag.
356
+ # gml = f"{gml[:pos]} gml:id="{_attr_escape(gml_id)}"{gml[pos:]}"
357
+
358
+ gml = self.render_gml_value(gml_id, value)
359
+ self._write(f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n")
360
+
361
+ def render_gml_value(self, gml_id, value: geos.GEOSGeometry | None, extra_xmlns="") -> str:
362
+ """Normal case: 'value' is raw geometry data.."""
363
+ # In case this is a standalone response, this will be the top-level element, hence includes the xmlns.
364
+ base_attrs = f' gml:id="{_attr_escape(gml_id)}" srsName="{self.xml_srs_name}"{extra_xmlns}'
346
365
  self.output_crs.apply_to(value)
347
- base_attrs = (
348
- f' gml:id="{escape(gml_id)}" srsName="{self.xml_srs_name}"{extra_xmlns}'
349
- )
350
- return self._render_gml_type(value, base_attrs)
366
+ return self._render_gml_type(value, base_attrs=base_attrs)
351
367
 
352
368
  def _render_gml_type(self, value: geos.GEOSGeometry, base_attrs=""):
369
+ """Render an GML value (this is also called from MultiPolygon)."""
353
370
  try:
354
371
  # Avoid isinstance checks, do a direct lookup
355
372
  method = GML_RENDER_FUNCTIONS[value.__class__]
356
373
  except KeyError:
357
374
  return f"<!-- No rendering implemented for {value.geom_type} -->"
358
- else:
359
- return method(self, value, base_attrs=base_attrs)
375
+ return method(self, value, base_attrs=base_attrs)
360
376
 
361
377
  @register_geos_type(geos.Point)
362
378
  def render_gml_point(self, value: geos.Point, base_attrs=""):
@@ -372,7 +388,7 @@ class GML32Renderer(OutputRenderer):
372
388
  def render_gml_polygon(self, value: geos.Polygon, base_attrs=""):
373
389
  # lol: http://erouault.blogspot.com/2014/04/gml-madness.html
374
390
  ext_ring = self.render_gml_linear_ring(value.exterior_ring)
375
- buf = StringBuffer()
391
+ buf = StringIO()
376
392
  buf.write(f"<gml:Polygon{base_attrs}><gml:exterior>{ext_ring}</gml:exterior>")
377
393
  for i in range(value.num_interior_rings):
378
394
  buf.write("<gml:interior>")
@@ -410,6 +426,7 @@ class GML32Renderer(OutputRenderer):
410
426
 
411
427
  @register_geos_type(geos.LinearRing)
412
428
  def render_gml_linear_ring(self, value: geos.LinearRing, base_attrs=""):
429
+ # NOTE: this is super slow. value.tuple performs a C-API call for every point!
413
430
  coords = " ".join(map(str, itertools.chain.from_iterable(value.tuple)))
414
431
  dim = "3" if value.hasz else "2"
415
432
  # <gml:coordinates> is still valid in GML3, but deprecated (part of GML2).
@@ -421,6 +438,7 @@ class GML32Renderer(OutputRenderer):
421
438
 
422
439
  @register_geos_type(geos.LineString)
423
440
  def render_gml_line_string(self, value: geos.LineString, base_attrs=""):
441
+ # NOTE: this is super slow. value.tuple performs a C-API call for every point!
424
442
  coords = " ".join(map(str, itertools.chain.from_iterable(value.tuple)))
425
443
  dim = "3" if value.hasz else "2"
426
444
  return (
@@ -429,12 +447,36 @@ class GML32Renderer(OutputRenderer):
429
447
  "</gml:LineString>"
430
448
  )
431
449
 
432
- def get_gml_id(self, feature_type: FeatureType, object_id, seq) -> str:
450
+ def get_gml_id(self, feature_type: FeatureType, object_id) -> str:
433
451
  """Generate the gml:id value, which is required for GML 3.2 objects."""
434
- return f"{feature_type.name}.{object_id}.{seq}"
452
+ self.gml_seq += 1
453
+ return f"{feature_type.name}.{object_id}.{self.gml_seq}"
454
+
455
+
456
+ class DBGMLRenderingMixin:
457
+
458
+ def render_gml_value(self, gml_id, value: str, extra_xmlns=""):
459
+ """DB optimized: 'value' is pre-rendered GML XML string."""
460
+ # Write the gml:id inside the first tag
461
+ end_pos = value.find(">")
462
+ gml_tag = value[:end_pos]
463
+ id_pos = gml_tag.find("gml:id=")
464
+ if id_pos == -1:
465
+ # Inject
466
+ return f'{gml_tag} gml:id="{_attr_escape(gml_id)}"{extra_xmlns}{value[end_pos:]}'
467
+ else:
468
+ # Replace
469
+ end_pos1 = gml_tag.find('"', id_pos + 8)
470
+ return (
471
+ f"{gml_tag[:id_pos]}"
472
+ f'gml:id="{_attr_escape(gml_id)}'
473
+ f"{value[end_pos1:end_pos]}" # from " right until >
474
+ f"{extra_xmlns}" # extra namespaces?
475
+ f"{value[end_pos:]}" # from > and beyond
476
+ )
435
477
 
436
478
 
437
- class DBGML32Renderer(GML32Renderer):
479
+ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
438
480
  """Faster GetFeature renderer that uses the database to render GML 3.2"""
439
481
 
440
482
  @classmethod
@@ -449,18 +491,14 @@ class DBGML32Renderer(GML32Renderer):
449
491
  This is far more efficient then GeoDjango's logic, which performs a
450
492
  C-API call for every single coordinate of a geometry.
451
493
  """
452
- queryset = super().decorate_queryset(
453
- feature_type, queryset, output_crs, **params
454
- )
494
+ queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
455
495
 
456
496
  # Retrieve geometries as pre-rendered instead.
457
497
  gml_elements = feature_type.xsd_type.geometry_elements
458
498
  geo_selects = get_db_geometry_selects(gml_elements, output_crs)
459
499
  if geo_selects:
460
500
  queryset = queryset.defer(*geo_selects.keys()).annotate(
461
- _as_envelope_gml=cls.get_db_envelope_as_gml(
462
- feature_type, queryset, output_crs
463
- ),
501
+ _as_envelope_gml=cls.get_db_envelope_as_gml(feature_type, queryset, output_crs),
464
502
  **build_db_annotations(geo_selects, "_as_gml_{name}", AsGML),
465
503
  )
466
504
 
@@ -523,67 +561,49 @@ class DBGML32Renderer(GML32Renderer):
523
561
  using=queryset.db,
524
562
  )
525
563
 
526
- def render_element(
527
- self, feature_type, xsd_element: XsdElement, instance: models.Model
528
- ):
529
- if xsd_element.is_geometry:
530
- # Optimized path, pre-rendered GML
531
- value = get_db_annotation(instance, xsd_element.name, "_as_gml_{name}")
532
- if value is None:
533
- # Avoid incrementing gml_seq
534
- return f'<{xsd_element.xml_name} xsi:nil="true"/>\n'
535
-
536
- self.gml_seq += 1
537
- return self.render_db_gml_field(
538
- feature_type,
539
- xsd_element,
540
- value,
541
- gml_id=self.get_gml_id(feature_type, instance.pk, seq=self.gml_seq),
542
- )
543
- else:
544
- return super().render_element(feature_type, xsd_element, instance)
564
+ def write_gml_field(
565
+ self, feature_type, xsd_element: XsdElement, instance: models.Model, extra_xmlns=""
566
+ ) -> None:
567
+ """Write the value of an GML tag.
545
568
 
546
- def render_db_gml_field(
547
- self,
548
- feature_type: FeatureType,
549
- xsd_element: XsdElement,
550
- value,
551
- gml_id,
552
- extra_xmlns="",
553
- ) -> str:
554
- """Write the value of an GML tag"""
569
+ This optimized version takes a pre-rendered XML from the database query.
570
+ """
571
+ value = get_db_annotation(instance, xsd_element.name, "_as_gml_{name}")
555
572
  xml_name = xsd_element.xml_name
556
573
  if value is None:
557
- return f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n'
574
+ # Avoid incrementing gml_seq
575
+ self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
576
+ return
558
577
 
559
- # Write the gml:id inside the first tag
560
- pos = value.find(">")
561
- first_tag = value[:pos]
562
- if "gml:id" in first_tag:
563
- first_tag = RE_GML_ID.sub(f'gml:id="{escape(gml_id)}"', first_tag, 1)
564
- else:
565
- first_tag += f' gml:id="{escape(gml_id)}"'
578
+ gml_id = self.get_gml_id(feature_type, instance.pk)
579
+ gml = self.render_gml_value(gml_id, value, extra_xmlns=extra_xmlns)
580
+ self._write(f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n")
566
581
 
567
- gml = first_tag + value[pos:]
568
- return f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n"
569
-
570
- def render_bounds(self, feature_type, instance):
582
+ def write_bounds(self, feature_type, instance) -> None:
571
583
  """Generate the <gml:boundedBy> from DB prerendering."""
572
584
  gml = instance._as_envelope_gml
573
585
  if gml is not None:
574
- return f"<gml:boundedBy>{gml}</gml:boundedBy>\n"
586
+ self._write(f"<gml:boundedBy>{gml}</gml:boundedBy>\n")
575
587
 
576
588
 
577
589
  class GML32ValueRenderer(GML32Renderer):
578
- """Render the GetPropertyValue XML output in GML 3.2 format"""
590
+ """Render the GetPropertyValue XML output in GML 3.2 format.
591
+
592
+ Geoserver seems to generate the element tag inside each <wfs:member> element. We've applied this one.
593
+ The GML standard demonstrates to render only their content inside a <wfs:member> element
594
+ (either plain text or an <gml:...> tag). Not sure what is right here.
595
+ """
579
596
 
580
597
  content_type = "text/xml; charset=utf-8"
598
+ content_type_plain = "text/plain; charset=utf-8"
581
599
  xml_collection_tag = "ValueCollection"
600
+ _escape_value = staticmethod(_value_to_xml_string)
601
+ gml_value_getter = itemgetter("member")
582
602
 
583
603
  def __init__(self, *args, value_reference: ValueReference, **kwargs):
584
604
  self.value_reference = value_reference
585
605
  super().__init__(*args, **kwargs)
586
- self.xsd_node = None
606
+ self.xsd_node: XsdNode | None = None
587
607
 
588
608
  @classmethod
589
609
  def decorate_queryset(
@@ -594,6 +614,7 @@ class GML32ValueRenderer(GML32Renderer):
594
614
  **params,
595
615
  ):
596
616
  # Don't optimize queryset, it only retrieves one value
617
+ # The data is already limited to a ``queryset.values()`` in ``QueryExpression.get_queryset()``.
597
618
  return queryset
598
619
 
599
620
  def start_collection(self, sub_collection: SimpleFeatureCollection):
@@ -601,60 +622,84 @@ class GML32ValueRenderer(GML32Renderer):
601
622
  match = sub_collection.feature_type.resolve_element(self.value_reference.xpath)
602
623
  self.xsd_node = match.child
603
624
 
604
- def render_wfs_member(
605
- self, feature_type: FeatureType, instance: dict, extra_xmlns=""
625
+ def write_by_id_response(
626
+ self, sub_collection: SimpleFeatureCollection, instance: dict, extra_xmlns
606
627
  ):
607
- """Overwritten to handle attribute support."""
628
+ """The value rendering only renders the value. not a complete feature"""
608
629
  if self.xsd_node.is_attribute:
609
- # When GetPropertyValue selects an attribute, it's value is rendered
610
- # as plain-text (without spaces!) inside a <wfs:member> element.
611
- # The format_value() is needed for @gml:id
612
- body = self.xsd_node.format_value(instance["member"])
613
- return f"<wfs:member>{body}</wfs:member>\n"
630
+ # Output as plain text
631
+ self.content_type = self.content_type_plain # change for this instance!
632
+ self._escape_value = _value_to_text # avoid XML escaping
614
633
  else:
615
- # The call to GetPropertyValue selected an element.
616
- # Render this single element tag inside the <wfs:member> parent.
617
- body = self.render_wfs_member_contents(feature_type, instance)
618
- return f"<wfs:member>\n{body}</wfs:member>\n"
619
-
620
- def render_wfs_member_contents(
621
- self, feature_type: FeatureType, instance: dict, extra_xmlns=""
622
- ) -> str:
634
+ self._write('<?xml version="1.0" encoding="UTF-8"?>\n')
635
+
636
+ # Write the single tag, no <wfs:member> around it.
637
+ self.write_feature(sub_collection.feature_type, instance, extra_xmlns=extra_xmlns)
638
+
639
+ def write_feature(self, feature_type: FeatureType, instance: dict, extra_xmlns="") -> None:
623
640
  """Write the XML for a single object.
624
641
  In this case, it's only a single XML tag.
625
642
  """
626
- value = instance["member"]
627
643
  if self.xsd_node.is_geometry:
628
- gml_id = self.get_gml_id(feature_type, instance["pk"], seq=1)
629
- return self.render_gml_field(
644
+ self.write_gml_field(
630
645
  feature_type,
631
- self.xsd_node,
632
- value=value,
633
- gml_id=gml_id,
646
+ cast(XsdElement, self.xsd_node),
647
+ instance,
634
648
  extra_xmlns=extra_xmlns,
635
649
  )
650
+ elif self.xsd_node.is_attribute:
651
+ value = instance["member"]
652
+ if value is not None:
653
+ value = self.xsd_node.format_raw_value(instance["member"]) # for gml:id
654
+ value = self._escape_value(value)
655
+ self._write(value)
656
+ elif self.xsd_node.is_array:
657
+ if (value := instance["member"]) is not None:
658
+ # <wfs:member> doesn't allow multiple items as children, for new render as separate members.
659
+ xml_name = self.xsd_node.xml_name
660
+ first = True
661
+ for item in value:
662
+ if item is not None:
663
+ if not first:
664
+ self._write("</wfs:member>\n<wfs:member>")
665
+
666
+ item = self._escape_value(item)
667
+ self._write(f"<{xml_name}>{item}</{xml_name}>\n")
668
+ first = False
669
+ elif self.xsd_node.type.is_complex_type:
670
+ raise NotImplementedError("GetPropertyValue with complex types is not implemented")
671
+ # self.write_xml_complex_type(feature_type, self.xsd_node, instance['member'])
636
672
  else:
637
- # The xsd_element is needed so render_xml_field() can render complex types.
638
- value = self.xsd_node.format_value(value) # needed for @gml:id
639
- if self.xsd_node.is_attribute:
640
- # For GetFeatureById, allow returning raw values
641
- return str(value)
642
- else:
643
- return self.render_xml_field(
644
- feature_type,
645
- cast(XsdElement, self.xsd_node),
646
- value,
647
- extra_xmlns=extra_xmlns,
648
- )
673
+ self.write_xml_field(
674
+ feature_type,
675
+ cast(XsdElement, self.xsd_node),
676
+ value=instance["member"],
677
+ extra_xmlns=extra_xmlns,
678
+ )
649
679
 
680
+ def write_gml_field(
681
+ self, feature_type, xsd_element: XsdElement, instance: dict, extra_xmlns=""
682
+ ) -> None:
683
+ """Overwritten to allow dict access instead of model access."""
684
+ value = self.gml_value_getter(instance) # "member" or "gml_member"
685
+ xml_name = xsd_element.xml_name
686
+ if value is None:
687
+ # Avoid incrementing gml_seq
688
+ self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
689
+ return
690
+
691
+ gml_id = self.get_gml_id(feature_type, instance["pk"])
692
+ gml = self.render_gml_value(gml_id, value, extra_xmlns=extra_xmlns)
693
+ self._write(f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n")
650
694
 
651
- class DBGML32ValueRenderer(DBGML32Renderer, GML32ValueRenderer):
695
+
696
+ class DBGML32ValueRenderer(DBGMLRenderingMixin, GML32ValueRenderer):
652
697
  """Faster GetPropertyValue renderer that uses the database to render GML 3.2"""
653
698
 
699
+ gml_value_getter = itemgetter("gml_member")
700
+
654
701
  @classmethod
655
- def decorate_queryset(
656
- cls, feature_type: FeatureType, queryset, output_crs, **params
657
- ):
702
+ def decorate_queryset(cls, feature_type: FeatureType, queryset, output_crs, **params):
658
703
  """Update the queryset to let the database render the GML output."""
659
704
  value_reference = params["valueReference"]
660
705
  match = feature_type.resolve_element(value_reference.xpath)
@@ -665,21 +710,3 @@ class DBGML32ValueRenderer(DBGML32Renderer, GML32ValueRenderer):
665
710
  )
666
711
  else:
667
712
  return queryset
668
-
669
- def render_wfs_member(
670
- self, feature_type: FeatureType, instance: dict, extra_xmlns=""
671
- ) -> str:
672
- """Write the XML for a single object."""
673
- if "gml_member" in instance:
674
- gml_id = self.get_gml_id(feature_type, instance["pk"], seq=1)
675
- body = self.render_db_gml_field(
676
- feature_type,
677
- self.xsd_node,
678
- instance["gml_member"],
679
- gml_id=gml_id,
680
- )
681
- return f"<wfs:member>\n{body}</wfs:member>\n"
682
- else:
683
- return super().render_wfs_member(
684
- feature_type, instance, extra_xmlns=extra_xmlns
685
- )