django-gisserver 1.4.1__py3-none-any.whl → 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_gisserver-1.4.1.dist-info → django_gisserver-1.5.0.dist-info}/METADATA +11 -11
- django_gisserver-1.5.0.dist-info/RECORD +54 -0
- {django_gisserver-1.4.1.dist-info → django_gisserver-1.5.0.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/db.py +14 -20
- gisserver/exceptions.py +23 -9
- gisserver/features.py +62 -99
- gisserver/geometries.py +2 -2
- gisserver/operations/base.py +31 -21
- gisserver/operations/wfs20.py +44 -38
- gisserver/output/__init__.py +2 -1
- gisserver/output/base.py +43 -27
- gisserver/output/csv.py +38 -33
- gisserver/output/geojson.py +43 -51
- gisserver/output/gml32.py +88 -67
- gisserver/output/results.py +23 -8
- gisserver/output/utils.py +18 -2
- gisserver/output/xmlschema.py +1 -1
- gisserver/parsers/base.py +2 -2
- gisserver/parsers/fes20/__init__.py +18 -0
- gisserver/parsers/fes20/expressions.py +7 -12
- gisserver/parsers/fes20/functions.py +1 -1
- gisserver/parsers/fes20/operators.py +7 -3
- gisserver/parsers/fes20/query.py +11 -1
- gisserver/parsers/fes20/sorting.py +3 -1
- gisserver/parsers/gml/base.py +1 -1
- gisserver/queries/__init__.py +3 -0
- gisserver/queries/adhoc.py +16 -12
- gisserver/queries/base.py +76 -36
- gisserver/queries/projection.py +240 -0
- gisserver/queries/stored.py +7 -6
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +2 -2
- gisserver/types.py +78 -24
- gisserver/views.py +9 -20
- django_gisserver-1.4.1.dist-info/RECORD +0 -53
- {django_gisserver-1.4.1.dist-info → django_gisserver-1.5.0.dist-info}/LICENSE +0 -0
- {django_gisserver-1.4.1.dist-info → django_gisserver-1.5.0.dist-info}/top_level.txt +0 -0
gisserver/output/gml32.py
CHANGED
|
@@ -30,11 +30,12 @@ from gisserver.db import (
|
|
|
30
30
|
get_db_geometry_target,
|
|
31
31
|
get_geometries_union,
|
|
32
32
|
)
|
|
33
|
-
from gisserver.exceptions import NotFound
|
|
34
|
-
from gisserver.features import
|
|
33
|
+
from gisserver.exceptions import NotFound, WFSException
|
|
34
|
+
from gisserver.features import FeatureType
|
|
35
35
|
from gisserver.geometries import CRS
|
|
36
36
|
from gisserver.parsers.fes20 import ValueReference
|
|
37
|
-
from gisserver.
|
|
37
|
+
from gisserver.queries import FeatureProjection, FeatureRelation
|
|
38
|
+
from gisserver.types import XsdElement, XsdNode
|
|
38
39
|
|
|
39
40
|
from .base import OutputRenderer
|
|
40
41
|
from .results import SimpleFeatureCollection
|
|
@@ -92,6 +93,7 @@ class GML32Renderer(OutputRenderer):
|
|
|
92
93
|
|
|
93
94
|
content_type = "text/xml; charset=utf-8"
|
|
94
95
|
xml_collection_tag = "FeatureCollection"
|
|
96
|
+
xml_sub_collection_tag = "FeatureCollection" # Mapserver does not use SimpleFeatureCollection
|
|
95
97
|
chunk_size = 40_000
|
|
96
98
|
gml_seq = 0
|
|
97
99
|
|
|
@@ -127,7 +129,7 @@ class GML32Renderer(OutputRenderer):
|
|
|
127
129
|
"""Default behavior for standalone response is writing a feature (can be changed by GetPropertyValue)"""
|
|
128
130
|
self._write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
|
129
131
|
self.write_feature(
|
|
130
|
-
|
|
132
|
+
projection=sub_collection.projection,
|
|
131
133
|
instance=instance,
|
|
132
134
|
extra_xmlns=extra_xmlns,
|
|
133
135
|
)
|
|
@@ -163,6 +165,36 @@ class GML32Renderer(OutputRenderer):
|
|
|
163
165
|
' xmlns:gml="http://www.opengis.net/gml/3.2"'
|
|
164
166
|
)
|
|
165
167
|
|
|
168
|
+
def render_exception(self, exception: Exception):
|
|
169
|
+
"""Render the exception in a format that fits with the output.
|
|
170
|
+
|
|
171
|
+
The WSF XSD spec has a hidden gem: an <wfs:truncatedResponse> element
|
|
172
|
+
can be rendered at the end of a feature collection
|
|
173
|
+
to inform the client an error happened during rendering.
|
|
174
|
+
"""
|
|
175
|
+
message = super().render_exception(exception)
|
|
176
|
+
buffer = self.output.getvalue()
|
|
177
|
+
|
|
178
|
+
# Wrap into <ows:ExceptionReport> tag.
|
|
179
|
+
if not isinstance(exception, WFSException):
|
|
180
|
+
exception = WFSException(message, code=exception.__class__.__name__, status_code=500)
|
|
181
|
+
exception.debug_hint = False
|
|
182
|
+
|
|
183
|
+
# Only at the top-level, an exception report can be rendered
|
|
184
|
+
closing_child = (
|
|
185
|
+
f"</wfs:{self.xml_sub_collection_tag}></wfs:member>\n"
|
|
186
|
+
if len(self.collection.results) > 1
|
|
187
|
+
else ""
|
|
188
|
+
)
|
|
189
|
+
return (
|
|
190
|
+
f"{buffer}"
|
|
191
|
+
f"{closing_child}"
|
|
192
|
+
" <wfs:truncatedResponse>"
|
|
193
|
+
f"{exception.as_xml()}"
|
|
194
|
+
" </wfs:truncatedResponse>\n"
|
|
195
|
+
f"</wfs:{self.xml_collection_tag}>\n"
|
|
196
|
+
)
|
|
197
|
+
|
|
166
198
|
def render_stream(self):
|
|
167
199
|
"""Render the XML as streaming content.
|
|
168
200
|
This renders the standard <wfs:FeatureCollection> / <wfs:ValueCollection>
|
|
@@ -193,11 +225,12 @@ class GML32Renderer(OutputRenderer):
|
|
|
193
225
|
has_multiple_collections = len(collection.results) > 1
|
|
194
226
|
|
|
195
227
|
for sub_collection in collection.results:
|
|
228
|
+
projection = sub_collection.projection
|
|
196
229
|
self.start_collection(sub_collection)
|
|
197
230
|
if has_multiple_collections:
|
|
198
231
|
self._write(
|
|
199
232
|
f"<wfs:member>\n"
|
|
200
|
-
f"<wfs:{self.
|
|
233
|
+
f"<wfs:{self.xml_sub_collection_tag}"
|
|
201
234
|
f' timeStamp="{collection.timestamp}"'
|
|
202
235
|
f' numberMatched="{int(sub_collection.number_matched)}"'
|
|
203
236
|
f' numberReturned="{int(sub_collection.number_returned)}">\n'
|
|
@@ -206,7 +239,7 @@ class GML32Renderer(OutputRenderer):
|
|
|
206
239
|
for instance in sub_collection:
|
|
207
240
|
self.gml_seq = 0 # need to increment this between write_xml_field calls
|
|
208
241
|
self._write("<wfs:member>\n")
|
|
209
|
-
self.write_feature(
|
|
242
|
+
self.write_feature(projection, instance)
|
|
210
243
|
self._write("</wfs:member>\n")
|
|
211
244
|
|
|
212
245
|
# Only perform a 'yield' every once in a while,
|
|
@@ -218,7 +251,7 @@ class GML32Renderer(OutputRenderer):
|
|
|
218
251
|
yield xml_chunk
|
|
219
252
|
|
|
220
253
|
if has_multiple_collections:
|
|
221
|
-
self._write(f"</wfs:{self.
|
|
254
|
+
self._write(f"</wfs:{self.xml_sub_collection_tag}>\n</wfs:member>\n")
|
|
222
255
|
|
|
223
256
|
self._write(f"</wfs:{self.xml_collection_tag}>\n")
|
|
224
257
|
yield output.getvalue()
|
|
@@ -227,43 +260,45 @@ class GML32Renderer(OutputRenderer):
|
|
|
227
260
|
"""Hook to allow initialization per feature type"""
|
|
228
261
|
|
|
229
262
|
def write_feature(
|
|
230
|
-
self,
|
|
263
|
+
self, projection: FeatureProjection, instance: models.Model, extra_xmlns=""
|
|
231
264
|
) -> None:
|
|
232
265
|
"""Write the contents of the object value.
|
|
233
266
|
|
|
234
267
|
This output is typically wrapped in <wfs:member> tags
|
|
235
268
|
unless it's used for a GetPropertyById response.
|
|
236
269
|
"""
|
|
270
|
+
feature_type = projection.feature_type
|
|
271
|
+
|
|
237
272
|
# Write <app:FeatureTypeName> start node
|
|
238
273
|
pk = _tag_escape(str(instance.pk))
|
|
239
274
|
self._write(f'<{feature_type.xml_name} gml:id="{feature_type.name}.{pk}"{extra_xmlns}>\n')
|
|
240
|
-
|
|
241
275
|
# Write all fields, both base class and local elements.
|
|
242
|
-
for xsd_element in
|
|
276
|
+
for xsd_element in projection.xsd_root_elements:
|
|
243
277
|
# Note that writing 5000 features with 30 tags means this code make 150.000 method calls.
|
|
244
278
|
# Hence, branching to different rendering styles is branched here instead of making such
|
|
245
279
|
# call into a generic "write_field()" function and stepping out from there.
|
|
280
|
+
|
|
246
281
|
if xsd_element.is_geometry:
|
|
247
282
|
if xsd_element.xml_name == "gml:boundedBy":
|
|
248
283
|
# Special case for <gml:boundedBy>, so it will render with
|
|
249
284
|
# the output CRS and can be overwritten with DB-rendered GML.
|
|
250
|
-
self.write_bounds(
|
|
285
|
+
self.write_bounds(projection, instance)
|
|
251
286
|
else:
|
|
252
287
|
# Separate call which can be optimized (no need to overload write_xml_field() for all calls).
|
|
253
288
|
self.write_gml_field(feature_type, xsd_element, instance)
|
|
254
289
|
else:
|
|
255
290
|
value = xsd_element.get_value(instance)
|
|
256
291
|
if xsd_element.is_many:
|
|
257
|
-
self.write_many(
|
|
292
|
+
self.write_many(projection, xsd_element, value)
|
|
258
293
|
else:
|
|
259
294
|
# e.g. <gml:name>, or all other <app:...> nodes.
|
|
260
|
-
self.write_xml_field(
|
|
295
|
+
self.write_xml_field(projection, xsd_element, value)
|
|
261
296
|
|
|
262
297
|
self._write(f"</{feature_type.xml_name}>\n")
|
|
263
298
|
|
|
264
|
-
def write_bounds(self,
|
|
299
|
+
def write_bounds(self, projection, instance) -> None:
|
|
265
300
|
"""Render the GML bounds for the complete instance"""
|
|
266
|
-
envelope = feature_type.get_envelope(instance, self.output_crs)
|
|
301
|
+
envelope = projection.feature_type.get_envelope(instance, self.output_crs)
|
|
267
302
|
if envelope is not None:
|
|
268
303
|
lower = " ".join(map(str, envelope.lower_corner))
|
|
269
304
|
upper = " ".join(map(str, envelope.upper_corner))
|
|
@@ -274,7 +309,7 @@ class GML32Renderer(OutputRenderer):
|
|
|
274
309
|
</gml:Envelope></gml:boundedBy>\n"""
|
|
275
310
|
)
|
|
276
311
|
|
|
277
|
-
def write_many(self,
|
|
312
|
+
def write_many(self, projection: FeatureProjection, xsd_element: XsdElement, value) -> None:
|
|
278
313
|
"""Write a node that has multiple values (e.g. array or queryset)."""
|
|
279
314
|
# some <app:...> node that has multiple values
|
|
280
315
|
if value is None:
|
|
@@ -282,17 +317,11 @@ class GML32Renderer(OutputRenderer):
|
|
|
282
317
|
if xsd_element.min_occurs:
|
|
283
318
|
self._write(f'<{xsd_element.xml_name} xsi:nil="true"/>\n')
|
|
284
319
|
else:
|
|
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
320
|
for item in value:
|
|
292
|
-
self.write_xml_field(
|
|
321
|
+
self.write_xml_field(projection, xsd_element, value=item)
|
|
293
322
|
|
|
294
323
|
def write_xml_field(
|
|
295
|
-
self,
|
|
324
|
+
self, projection: FeatureProjection, xsd_element: XsdElement, value, extra_xmlns=""
|
|
296
325
|
):
|
|
297
326
|
"""Write the value of a single field."""
|
|
298
327
|
xml_name = xsd_element.xml_name
|
|
@@ -300,7 +329,7 @@ class GML32Renderer(OutputRenderer):
|
|
|
300
329
|
self._write(f'<{xml_name} xsi:nil="true"{extra_xmlns}/>\n')
|
|
301
330
|
elif xsd_element.type.is_complex_type:
|
|
302
331
|
# Expanded foreign relation / dictionary
|
|
303
|
-
self.write_xml_complex_type(
|
|
332
|
+
self.write_xml_complex_type(projection, xsd_element, value, extra_xmlns=extra_xmlns)
|
|
304
333
|
else:
|
|
305
334
|
# As this is likely called 150.000 times during a request, this is optimized.
|
|
306
335
|
# Avoided a separate call to _value_to_xml_string() and avoided isinstance() here.
|
|
@@ -324,16 +353,17 @@ class GML32Renderer(OutputRenderer):
|
|
|
324
353
|
|
|
325
354
|
self._write(f"<{xml_name}{extra_xmlns}>{value}</{xml_name}>\n")
|
|
326
355
|
|
|
327
|
-
def write_xml_complex_type(
|
|
356
|
+
def write_xml_complex_type(
|
|
357
|
+
self, projection: FeatureProjection, xsd_element: XsdElement, value, extra_xmlns=""
|
|
358
|
+
) -> None:
|
|
328
359
|
"""Write a single field, that consists of sub elements"""
|
|
329
|
-
xsd_type = cast(XsdComplexType, xsd_element.type)
|
|
330
360
|
self._write(f"<{xsd_element.xml_name}{extra_xmlns}>\n")
|
|
331
|
-
for sub_element in
|
|
361
|
+
for sub_element in projection.xsd_child_nodes[xsd_element]:
|
|
332
362
|
sub_value = sub_element.get_value(value)
|
|
333
363
|
if sub_element.is_many:
|
|
334
|
-
self.write_many(
|
|
364
|
+
self.write_many(projection, sub_element, sub_value)
|
|
335
365
|
else:
|
|
336
|
-
self.write_xml_field(
|
|
366
|
+
self.write_xml_field(projection, sub_element, sub_value)
|
|
337
367
|
self._write(f"</{xsd_element.xml_name}>\n")
|
|
338
368
|
|
|
339
369
|
def write_gml_field(
|
|
@@ -482,23 +512,22 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
|
482
512
|
@classmethod
|
|
483
513
|
def decorate_queryset(
|
|
484
514
|
cls,
|
|
485
|
-
|
|
515
|
+
projection: FeatureProjection,
|
|
486
516
|
queryset: models.QuerySet,
|
|
487
517
|
output_crs: CRS,
|
|
488
518
|
**params,
|
|
489
519
|
):
|
|
490
520
|
"""Update the queryset to let the database render the GML output.
|
|
491
|
-
This is far more efficient
|
|
521
|
+
This is far more efficient than GeoDjango's logic, which performs a
|
|
492
522
|
C-API call for every single coordinate of a geometry.
|
|
493
523
|
"""
|
|
494
|
-
queryset = super().decorate_queryset(
|
|
524
|
+
queryset = super().decorate_queryset(projection, queryset, output_crs, **params)
|
|
495
525
|
|
|
496
526
|
# Retrieve geometries as pre-rendered instead.
|
|
497
|
-
|
|
498
|
-
geo_selects = get_db_geometry_selects(gml_elements, output_crs)
|
|
527
|
+
geo_selects = get_db_geometry_selects(projection.geometry_elements, output_crs)
|
|
499
528
|
if geo_selects:
|
|
500
529
|
queryset = queryset.defer(*geo_selects.keys()).annotate(
|
|
501
|
-
_as_envelope_gml=cls.get_db_envelope_as_gml(
|
|
530
|
+
_as_envelope_gml=cls.get_db_envelope_as_gml(projection, queryset, output_crs),
|
|
502
531
|
**build_db_annotations(geo_selects, "_as_gml_{name}", AsGML),
|
|
503
532
|
)
|
|
504
533
|
|
|
@@ -507,56 +536,46 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
|
507
536
|
@classmethod
|
|
508
537
|
def get_prefetch_queryset(
|
|
509
538
|
cls,
|
|
510
|
-
|
|
539
|
+
projection: FeatureProjection,
|
|
511
540
|
feature_relation: FeatureRelation,
|
|
512
541
|
output_crs: CRS,
|
|
513
542
|
) -> models.QuerySet | None:
|
|
514
543
|
"""Perform DB annotations for prefetched relations too."""
|
|
515
|
-
base = super().get_prefetch_queryset(
|
|
544
|
+
base = super().get_prefetch_queryset(projection, feature_relation, output_crs)
|
|
516
545
|
if base is None:
|
|
517
546
|
return None
|
|
518
547
|
|
|
519
548
|
# Find which fields are GML elements
|
|
520
|
-
|
|
521
|
-
for e in feature_relation.xsd_elements:
|
|
522
|
-
if e.is_geometry:
|
|
523
|
-
# Prefetching a flattened relation
|
|
524
|
-
gml_elements.append(e)
|
|
525
|
-
elif e.type.is_complex_type:
|
|
526
|
-
# Prefetching a complex type
|
|
527
|
-
xsd_type: XsdComplexType = cast(XsdComplexType, e.type)
|
|
528
|
-
gml_elements.extend(xsd_type.geometry_elements)
|
|
529
|
-
|
|
530
|
-
geometries = get_db_geometry_selects(gml_elements, output_crs)
|
|
549
|
+
geometries = get_db_geometry_selects(feature_relation.geometry_elements, output_crs)
|
|
531
550
|
if geometries:
|
|
532
551
|
# Exclude geometries from the fields, fetch them as pre-rendered annotations instead.
|
|
533
|
-
return base.defer(geometries.keys()).annotate(
|
|
552
|
+
return base.defer(*geometries.keys()).annotate(
|
|
534
553
|
**build_db_annotations(geometries, "_as_gml_{name}", AsGML),
|
|
535
554
|
)
|
|
536
555
|
else:
|
|
537
556
|
return base
|
|
538
557
|
|
|
539
558
|
@classmethod
|
|
540
|
-
def get_db_envelope_as_gml(cls,
|
|
559
|
+
def get_db_envelope_as_gml(cls, projection: FeatureProjection, queryset, output_crs) -> AsGML:
|
|
541
560
|
"""Offload the GML rendering of the envelope to the database.
|
|
542
561
|
|
|
543
562
|
This also avoids offloads the geometry union calculation to the DB.
|
|
544
563
|
"""
|
|
545
|
-
geo_fields_union = cls._get_geometries_union(
|
|
564
|
+
geo_fields_union = cls._get_geometries_union(projection, queryset, output_crs)
|
|
546
565
|
return AsGML(geo_fields_union, envelope=True)
|
|
547
566
|
|
|
548
567
|
@classmethod
|
|
549
|
-
def _get_geometries_union(cls,
|
|
568
|
+
def _get_geometries_union(cls, projection: FeatureProjection, queryset, output_crs):
|
|
550
569
|
"""Combine all geometries of the model in a single SQL function."""
|
|
551
570
|
# Apply transforms where needed, in case some geometries use a different SRID.
|
|
552
571
|
return get_geometries_union(
|
|
553
572
|
[
|
|
554
573
|
conditional_transform(
|
|
555
|
-
|
|
556
|
-
|
|
574
|
+
gml_element.orm_path,
|
|
575
|
+
gml_element.source.srid,
|
|
557
576
|
output_srid=output_crs.srid,
|
|
558
577
|
)
|
|
559
|
-
for
|
|
578
|
+
for gml_element in projection.geometry_elements
|
|
560
579
|
],
|
|
561
580
|
using=queryset.db,
|
|
562
581
|
)
|
|
@@ -579,8 +598,8 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
|
579
598
|
gml = self.render_gml_value(gml_id, value, extra_xmlns=extra_xmlns)
|
|
580
599
|
self._write(f"<{xml_name}{extra_xmlns}>{gml}</{xml_name}>\n")
|
|
581
600
|
|
|
582
|
-
def write_bounds(self,
|
|
583
|
-
"""Generate the <gml:boundedBy> from DB
|
|
601
|
+
def write_bounds(self, projection, instance) -> None:
|
|
602
|
+
"""Generate the <gml:boundedBy> from DB pre-rendering."""
|
|
584
603
|
gml = instance._as_envelope_gml
|
|
585
604
|
if gml is not None:
|
|
586
605
|
self._write(f"<gml:boundedBy>{gml}</gml:boundedBy>\n")
|
|
@@ -597,6 +616,7 @@ class GML32ValueRenderer(GML32Renderer):
|
|
|
597
616
|
content_type = "text/xml; charset=utf-8"
|
|
598
617
|
content_type_plain = "text/plain; charset=utf-8"
|
|
599
618
|
xml_collection_tag = "ValueCollection"
|
|
619
|
+
xml_sub_collection_tag = "ValueCollection"
|
|
600
620
|
_escape_value = staticmethod(_value_to_xml_string)
|
|
601
621
|
gml_value_getter = itemgetter("member")
|
|
602
622
|
|
|
@@ -608,7 +628,7 @@ class GML32ValueRenderer(GML32Renderer):
|
|
|
608
628
|
@classmethod
|
|
609
629
|
def decorate_queryset(
|
|
610
630
|
cls,
|
|
611
|
-
|
|
631
|
+
projection: FeatureProjection,
|
|
612
632
|
queryset: models.QuerySet,
|
|
613
633
|
output_crs: CRS,
|
|
614
634
|
**params,
|
|
@@ -634,15 +654,15 @@ class GML32ValueRenderer(GML32Renderer):
|
|
|
634
654
|
self._write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
|
635
655
|
|
|
636
656
|
# Write the single tag, no <wfs:member> around it.
|
|
637
|
-
self.write_feature(sub_collection.
|
|
657
|
+
self.write_feature(sub_collection.projection, instance, extra_xmlns=extra_xmlns)
|
|
638
658
|
|
|
639
|
-
def write_feature(self,
|
|
659
|
+
def write_feature(self, projection: FeatureProjection, instance: dict, extra_xmlns="") -> None:
|
|
640
660
|
"""Write the XML for a single object.
|
|
641
661
|
In this case, it's only a single XML tag.
|
|
642
662
|
"""
|
|
643
663
|
if self.xsd_node.is_geometry:
|
|
644
664
|
self.write_gml_field(
|
|
645
|
-
feature_type,
|
|
665
|
+
projection.feature_type,
|
|
646
666
|
cast(XsdElement, self.xsd_node),
|
|
647
667
|
instance,
|
|
648
668
|
extra_xmlns=extra_xmlns,
|
|
@@ -668,10 +688,10 @@ class GML32ValueRenderer(GML32Renderer):
|
|
|
668
688
|
first = False
|
|
669
689
|
elif self.xsd_node.type.is_complex_type:
|
|
670
690
|
raise NotImplementedError("GetPropertyValue with complex types is not implemented")
|
|
671
|
-
# self.write_xml_complex_type(
|
|
691
|
+
# self.write_xml_complex_type(projection, self.xsd_node, instance['member'])
|
|
672
692
|
else:
|
|
673
693
|
self.write_xml_field(
|
|
674
|
-
|
|
694
|
+
projection,
|
|
675
695
|
cast(XsdElement, self.xsd_node),
|
|
676
696
|
value=instance["member"],
|
|
677
697
|
extra_xmlns=extra_xmlns,
|
|
@@ -699,14 +719,15 @@ class DBGML32ValueRenderer(DBGMLRenderingMixin, GML32ValueRenderer):
|
|
|
699
719
|
gml_value_getter = itemgetter("gml_member")
|
|
700
720
|
|
|
701
721
|
@classmethod
|
|
702
|
-
def decorate_queryset(cls,
|
|
722
|
+
def decorate_queryset(cls, projection: FeatureProjection, queryset, output_crs, **params):
|
|
703
723
|
"""Update the queryset to let the database render the GML output."""
|
|
704
724
|
value_reference = params["valueReference"]
|
|
705
|
-
match = feature_type.resolve_element(value_reference.xpath)
|
|
725
|
+
match = projection.feature_type.resolve_element(value_reference.xpath)
|
|
706
726
|
if match.child.is_geometry:
|
|
707
727
|
# Add 'gml_member' to point to the pre-rendered GML version.
|
|
728
|
+
gml_element = cast(XsdElement, match.child)
|
|
708
729
|
return queryset.values(
|
|
709
|
-
"pk", gml_member=AsGML(get_db_geometry_target(
|
|
730
|
+
"pk", gml_member=AsGML(get_db_geometry_target(gml_element, output_crs))
|
|
710
731
|
)
|
|
711
732
|
else:
|
|
712
733
|
return queryset
|
gisserver/output/results.py
CHANGED
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import math
|
|
10
10
|
import operator
|
|
11
|
+
import typing
|
|
11
12
|
from collections.abc import Iterable
|
|
12
13
|
from datetime import timezone
|
|
13
14
|
from functools import cached_property, reduce
|
|
@@ -21,6 +22,9 @@ from gisserver.geometries import BoundingBox
|
|
|
21
22
|
|
|
22
23
|
from .utils import ChunkedQuerySetIterator, CountingIterator
|
|
23
24
|
|
|
25
|
+
if typing.TYPE_CHECKING:
|
|
26
|
+
from gisserver.queries import FeatureProjection, QueryExpression
|
|
27
|
+
|
|
24
28
|
|
|
25
29
|
class SimpleFeatureCollection:
|
|
26
30
|
"""Wrapper to read a result set.
|
|
@@ -31,15 +35,18 @@ class SimpleFeatureCollection:
|
|
|
31
35
|
|
|
32
36
|
def __init__(
|
|
33
37
|
self,
|
|
38
|
+
source_query: QueryExpression,
|
|
34
39
|
feature_type: FeatureType,
|
|
35
40
|
queryset: models.QuerySet,
|
|
36
41
|
start: int,
|
|
37
42
|
stop: int,
|
|
38
43
|
):
|
|
44
|
+
self.source_query = source_query
|
|
39
45
|
self.feature_type = feature_type
|
|
40
46
|
self.queryset = queryset
|
|
41
47
|
self.start = start
|
|
42
48
|
self.stop = stop
|
|
49
|
+
|
|
43
50
|
self._result_cache = None
|
|
44
51
|
self._result_iterator = None
|
|
45
52
|
self._has_more = None
|
|
@@ -67,7 +74,7 @@ class SimpleFeatureCollection:
|
|
|
67
74
|
def iterator(self):
|
|
68
75
|
"""Explicitly request the results to be streamed.
|
|
69
76
|
|
|
70
|
-
This can be used by output formats that stream
|
|
77
|
+
This can be used by output formats that stream results, and don't
|
|
71
78
|
access `number_returned`. Note this is not compatible with prefetch_related().
|
|
72
79
|
"""
|
|
73
80
|
if self._result_iterator is not None:
|
|
@@ -170,7 +177,7 @@ class SimpleFeatureCollection:
|
|
|
170
177
|
# When requesting the data after the fact, results are counted.
|
|
171
178
|
return self._result_iterator.number_returned
|
|
172
179
|
else:
|
|
173
|
-
# Count by fetching all data. Otherwise the results are queried twice.
|
|
180
|
+
# Count by fetching all data. Otherwise, the results are queried twice.
|
|
174
181
|
# For GML/XML, it's not possible the stream the queryset results
|
|
175
182
|
# as the first tag needs to describe the number of results.
|
|
176
183
|
self.fetch_results()
|
|
@@ -187,7 +194,7 @@ class SimpleFeatureCollection:
|
|
|
187
194
|
qs = self.queryset
|
|
188
195
|
clean_annotations = {
|
|
189
196
|
# HACK: remove database optimizations from output renderer.
|
|
190
|
-
# Otherwise it becomes SELECT COUNT(*) FROM (SELECT AsGML(..), ...)
|
|
197
|
+
# Otherwise, it becomes SELECT COUNT(*) FROM (SELECT AsGML(..), ...)
|
|
191
198
|
key: value
|
|
192
199
|
for key, value in qs.query.annotations.items()
|
|
193
200
|
if not key.startswith("_as_")
|
|
@@ -229,11 +236,18 @@ class SimpleFeatureCollection:
|
|
|
229
236
|
elif self._has_more is not None:
|
|
230
237
|
return self._has_more # did page+1 record check, answer is known.
|
|
231
238
|
elif self._is_surely_last_page:
|
|
232
|
-
return False #
|
|
239
|
+
return False # Fewer results than expected, answer is known.
|
|
233
240
|
|
|
234
241
|
# This will perform an slow COUNT() query...
|
|
235
242
|
return self.stop < self.number_matched
|
|
236
243
|
|
|
244
|
+
@cached_property
|
|
245
|
+
def projection(self) -> FeatureProjection:
|
|
246
|
+
"""Provide the projection to render these results with."""
|
|
247
|
+
# Note this attribute would technically be part of the 'query' object,
|
|
248
|
+
# but since the projection needs to be mapped per feature type, it's stored here for convenience.
|
|
249
|
+
return self.source_query.get_projection(self.feature_type)
|
|
250
|
+
|
|
237
251
|
def get_bounding_box(self) -> BoundingBox:
|
|
238
252
|
"""Determine bounding box of all items."""
|
|
239
253
|
self.fetch_results() # Avoid querying results twice
|
|
@@ -245,11 +259,11 @@ class SimpleFeatureCollection:
|
|
|
245
259
|
self.feature_type.geometry_field.name
|
|
246
260
|
).child
|
|
247
261
|
for instance in self:
|
|
248
|
-
|
|
249
|
-
if
|
|
262
|
+
geometry_value = geometry_field.get_value(instance)
|
|
263
|
+
if geometry_value is None:
|
|
250
264
|
continue
|
|
251
265
|
|
|
252
|
-
bbox.extend_to_geometry(
|
|
266
|
+
bbox.extend_to_geometry(geometry_value)
|
|
253
267
|
|
|
254
268
|
return bbox
|
|
255
269
|
|
|
@@ -270,6 +284,7 @@ class FeatureCollection:
|
|
|
270
284
|
previous: str | None = None,
|
|
271
285
|
):
|
|
272
286
|
"""
|
|
287
|
+
:param source_query: The query that generated this output.
|
|
273
288
|
:param results: All retrieved feature collections (one per FeatureType)
|
|
274
289
|
:param number_matched: Total number of features across all pages
|
|
275
290
|
:param next: URL of the next page
|
|
@@ -305,7 +320,7 @@ class FeatureCollection:
|
|
|
305
320
|
conf.GISSERVER_COUNT_NUMBER_MATCHED == 2 and self.results[0].start > 0
|
|
306
321
|
):
|
|
307
322
|
# Report "unknown" for either all pages, or the second page.
|
|
308
|
-
# Most clients don't need this metadata and thus we avoid a COUNT query.
|
|
323
|
+
# Most clients don't need this metadata, and thus we avoid a COUNT query.
|
|
309
324
|
return None
|
|
310
325
|
|
|
311
326
|
return sum(c.number_matched for c in self.results)
|
gisserver/output/utils.py
CHANGED
|
@@ -5,7 +5,7 @@ from collections.abc import Iterable
|
|
|
5
5
|
from itertools import islice
|
|
6
6
|
from typing import TypeVar
|
|
7
7
|
|
|
8
|
-
from django.db import models
|
|
8
|
+
from django.db import connections, models
|
|
9
9
|
from lru import LRU
|
|
10
10
|
|
|
11
11
|
M = TypeVar("M", bound=models.Model)
|
|
@@ -89,7 +89,7 @@ class ChunkedQuerySetIterator(Iterable[M]):
|
|
|
89
89
|
self._number_returned = 0
|
|
90
90
|
self._in_iterator = True
|
|
91
91
|
try:
|
|
92
|
-
qs_iter = iter(self.
|
|
92
|
+
qs_iter = iter(self._get_queryset_iterator())
|
|
93
93
|
|
|
94
94
|
# Keep fetching chunks
|
|
95
95
|
while True:
|
|
@@ -107,6 +107,22 @@ class ChunkedQuerySetIterator(Iterable[M]):
|
|
|
107
107
|
finally:
|
|
108
108
|
self._in_iterator = False
|
|
109
109
|
|
|
110
|
+
def _get_queryset_iterator(self) -> Iterable:
|
|
111
|
+
"""The body of queryset.iterator(), while circumventing prefetching."""
|
|
112
|
+
# The old code did return `self.queryset.iterator(chunk_size=self.sql_chunk_size)`
|
|
113
|
+
# However, Django 4 supports using prefetch_related() with iterator() in that scenario.
|
|
114
|
+
#
|
|
115
|
+
# This code is the core of Django's QuerySet.iterator() that only produces the
|
|
116
|
+
# old-style iteration, without any prefetches. Those are added by this class instead.
|
|
117
|
+
use_chunked_fetch = not connections[self.queryset.db].settings_dict.get(
|
|
118
|
+
"DISABLE_SERVER_SIDE_CURSORS"
|
|
119
|
+
)
|
|
120
|
+
iterable = self.queryset._iterable_class(
|
|
121
|
+
self.queryset, chunked_fetch=use_chunked_fetch, chunk_size=self.sql_chunk_size
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
yield from iterable
|
|
125
|
+
|
|
110
126
|
@property
|
|
111
127
|
def number_returned(self) -> int:
|
|
112
128
|
"""Tell how many objects the iterator processed"""
|
gisserver/output/xmlschema.py
CHANGED
|
@@ -73,7 +73,7 @@ class XMLSchemaRenderer(OutputRenderer):
|
|
|
73
73
|
|
|
74
74
|
# Next, the complexType is rendered that defines the element contents.
|
|
75
75
|
# Next, the complexType(s) are rendered that defines the element contents.
|
|
76
|
-
# In case any fields are expanded (hence become
|
|
76
|
+
# In case any fields are expanded (hence become subtypes), these are also included.
|
|
77
77
|
output.write(self.render_complex_type(xsd_type))
|
|
78
78
|
for complex_type in self._get_complex_types(xsd_type):
|
|
79
79
|
output.write(self.render_complex_type(complex_type))
|
gisserver/parsers/base.py
CHANGED
|
@@ -65,7 +65,7 @@ class TagRegistry:
|
|
|
65
65
|
self.parsers = {}
|
|
66
66
|
|
|
67
67
|
def register(self, name=None, namespace=None, hidden=False):
|
|
68
|
-
"""Decorator to register
|
|
68
|
+
"""Decorator to register a class as XML node parser.
|
|
69
69
|
|
|
70
70
|
This registers the decorated class as the designated parser
|
|
71
71
|
for a specific XML tag.
|
|
@@ -116,7 +116,7 @@ class TagRegistry:
|
|
|
116
116
|
"""Convert the element into a Python class.
|
|
117
117
|
|
|
118
118
|
This locates the parser from the registered tag.
|
|
119
|
-
It's assumed that the tag has
|
|
119
|
+
It's assumed that the tag has a "from_xml()" method too.
|
|
120
120
|
"""
|
|
121
121
|
try:
|
|
122
122
|
real_cls = self.resolve_class(element.tag)
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from gisserver.exceptions import OperationParsingFailed
|
|
4
|
+
|
|
1
5
|
from .expressions import ValueReference
|
|
2
6
|
from .filters import Filter
|
|
3
7
|
from .functions import function_registry
|
|
@@ -22,3 +26,17 @@ def parse_resource_id_kvp(value) -> IdOperator:
|
|
|
22
26
|
This returns an IdOperator, as it needs to support multiple pairs.
|
|
23
27
|
"""
|
|
24
28
|
return IdOperator([ResourceId(rid) for rid in value.split(",")])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_property_name(value) -> list[ValueReference] | None:
|
|
32
|
+
"""Parse the PROPERTYNAME parameter"""
|
|
33
|
+
if not value or value == "*":
|
|
34
|
+
return None # WFS 1 logic
|
|
35
|
+
|
|
36
|
+
if "(" in value:
|
|
37
|
+
raise OperationParsingFailed(
|
|
38
|
+
"Parameter lists to perform multiple queries are not supported yet.",
|
|
39
|
+
locator="propertyname",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return [ValueReference(x) for x in value.split(",")]
|
|
@@ -49,8 +49,8 @@ OUTPUT_FIELDS = {
|
|
|
49
49
|
class BinaryOperatorType(TagNameEnum):
|
|
50
50
|
"""FES 1.0 Arithmetic operators.
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
still assume the server supports these. Hence these need to be included.
|
|
52
|
+
These are no longer part of the FES 2.0 spec, but clients (like QGis)
|
|
53
|
+
still assume the server supports these. Hence, these need to be included.
|
|
54
54
|
"""
|
|
55
55
|
|
|
56
56
|
Add = operator.add
|
|
@@ -100,7 +100,7 @@ class Literal(Expression):
|
|
|
100
100
|
|
|
101
101
|
@cached_property
|
|
102
102
|
def value(self) -> ParsedValue: # officially <xsd:any>
|
|
103
|
-
"""Access the value of the element,
|
|
103
|
+
"""Access the value of the element, cast to the appropriate data type."""
|
|
104
104
|
if not isinstance(self.raw_value, str):
|
|
105
105
|
return self.raw_value # GML element or None
|
|
106
106
|
elif self.type:
|
|
@@ -197,7 +197,7 @@ class ValueReference(Expression):
|
|
|
197
197
|
return match.build_rhs(compiler)
|
|
198
198
|
|
|
199
199
|
def parse_xpath(self, feature_type=None) -> ORMPath:
|
|
200
|
-
"""Convert the XPath into
|
|
200
|
+
"""Convert the XPath into the required ORM query elements."""
|
|
201
201
|
if feature_type is not None:
|
|
202
202
|
# Can resolve against XSD paths, find the correct DB field name
|
|
203
203
|
return feature_type.resolve_element(self.xpath)
|
|
@@ -209,12 +209,7 @@ class ValueReference(Expression):
|
|
|
209
209
|
@cached_property
|
|
210
210
|
def element_name(self):
|
|
211
211
|
"""Tell which element this reference points to."""
|
|
212
|
-
|
|
213
|
-
pos = self.xpath.rindex("/")
|
|
214
|
-
except ValueError:
|
|
215
|
-
return self.xpath
|
|
216
|
-
else:
|
|
217
|
-
return self.xpath[pos + 1 :]
|
|
212
|
+
return self.xpath.rpartition("/")[2]
|
|
218
213
|
|
|
219
214
|
|
|
220
215
|
@dataclass
|
|
@@ -245,8 +240,8 @@ class Function(Expression):
|
|
|
245
240
|
class BinaryOperator(Expression):
|
|
246
241
|
"""Support for FES 1.0 arithmetic operators.
|
|
247
242
|
|
|
248
|
-
|
|
249
|
-
still assume the server supports these. Hence these need to be included.
|
|
243
|
+
These are no longer part of the FES 2.0 spec, but clients (like QGis)
|
|
244
|
+
still assume the server supports these. Hence, these need to be included.
|
|
250
245
|
"""
|
|
251
246
|
|
|
252
247
|
_operatorType: BinaryOperatorType
|
|
@@ -97,7 +97,7 @@ class FesFunctionRegistry:
|
|
|
97
97
|
return _wrapper
|
|
98
98
|
|
|
99
99
|
def resolve_function(self, function_name) -> FesFunction:
|
|
100
|
-
"""Resole the function using
|
|
100
|
+
"""Resole the function using its name."""
|
|
101
101
|
try:
|
|
102
102
|
return self.functions[function_name]
|
|
103
103
|
except KeyError:
|