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