django-gisserver 2.0__py3-none-any.whl → 2.1.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.
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/METADATA +27 -10
- django_gisserver-2.1.1.dist-info/RECORD +68 -0
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/conf.py +23 -1
- gisserver/crs.py +452 -0
- gisserver/db.py +78 -6
- gisserver/exceptions.py +106 -2
- gisserver/extensions/functions.py +122 -28
- gisserver/extensions/queries.py +15 -10
- gisserver/features.py +46 -33
- gisserver/geometries.py +64 -306
- gisserver/management/commands/loadgeojson.py +41 -21
- gisserver/operations/base.py +11 -7
- gisserver/operations/wfs20.py +31 -93
- gisserver/output/__init__.py +6 -2
- gisserver/output/base.py +28 -13
- gisserver/output/csv.py +18 -6
- gisserver/output/geojson.py +7 -6
- gisserver/output/gml32.py +86 -27
- gisserver/output/results.py +25 -39
- gisserver/output/utils.py +9 -2
- gisserver/parsers/ast.py +177 -68
- gisserver/parsers/fes20/__init__.py +76 -4
- gisserver/parsers/fes20/expressions.py +97 -27
- gisserver/parsers/fes20/filters.py +9 -6
- gisserver/parsers/fes20/identifiers.py +27 -7
- gisserver/parsers/fes20/lookups.py +8 -6
- gisserver/parsers/fes20/operators.py +101 -49
- gisserver/parsers/fes20/sorting.py +14 -6
- gisserver/parsers/gml/__init__.py +10 -19
- gisserver/parsers/gml/base.py +32 -14
- gisserver/parsers/gml/geometries.py +54 -21
- gisserver/parsers/ows/kvp.py +10 -2
- gisserver/parsers/ows/requests.py +6 -4
- gisserver/parsers/query.py +6 -2
- gisserver/parsers/values.py +61 -4
- gisserver/parsers/wfs20/__init__.py +2 -0
- gisserver/parsers/wfs20/adhoc.py +28 -18
- gisserver/parsers/wfs20/base.py +12 -7
- gisserver/parsers/wfs20/projection.py +3 -3
- gisserver/parsers/wfs20/requests.py +1 -0
- gisserver/parsers/wfs20/stored.py +3 -2
- gisserver/parsers/xml.py +12 -0
- gisserver/projection.py +17 -7
- gisserver/static/gisserver/index.css +27 -6
- gisserver/templates/gisserver/base.html +15 -0
- gisserver/templates/gisserver/index.html +10 -16
- gisserver/templates/gisserver/service_description.html +12 -6
- gisserver/templates/gisserver/wfs/feature_field.html +1 -1
- gisserver/templates/gisserver/wfs/feature_type.html +44 -13
- gisserver/types.py +152 -82
- gisserver/views.py +47 -24
- django_gisserver-2.0.dist-info/RECORD +0 -66
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info/licenses}/LICENSE +0 -0
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/top_level.txt +0 -0
gisserver/output/gml32.py
CHANGED
|
@@ -11,6 +11,7 @@ much extra method calls per field. Some bits are non-DRY inlined for this reason
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import itertools
|
|
14
|
+
import re
|
|
14
15
|
from collections import defaultdict
|
|
15
16
|
from datetime import date, datetime, time, timezone
|
|
16
17
|
from decimal import Decimal as D
|
|
@@ -23,6 +24,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|
|
23
24
|
from django.db import models
|
|
24
25
|
from django.http import HttpResponse
|
|
25
26
|
|
|
27
|
+
from gisserver.crs import CRS84
|
|
26
28
|
from gisserver.db import (
|
|
27
29
|
AsGML,
|
|
28
30
|
get_db_geometry_target,
|
|
@@ -46,6 +48,7 @@ from .utils import (
|
|
|
46
48
|
)
|
|
47
49
|
|
|
48
50
|
GML_RENDER_FUNCTIONS = {}
|
|
51
|
+
RE_SRS_NAME = re.compile(r'srsName="([^"]+)"')
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
def register_geos_type(geos_type):
|
|
@@ -170,7 +173,7 @@ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
|
|
|
170
173
|
def render_exception(self, exception: Exception):
|
|
171
174
|
"""Render the exception in a format that fits with the output.
|
|
172
175
|
|
|
173
|
-
The WSF XSD spec has a hidden gem: an
|
|
176
|
+
The WSF XSD spec has a hidden gem: an ``<wfs:truncatedResponse>`` element
|
|
174
177
|
can be rendered at the end of a feature collection
|
|
175
178
|
to inform the client an error happened during rendering.
|
|
176
179
|
"""
|
|
@@ -182,15 +185,14 @@ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
|
|
|
182
185
|
exception = WFSException(message, code=exception.__class__.__name__, status_code=500)
|
|
183
186
|
exception.debug_hint = False
|
|
184
187
|
|
|
185
|
-
# Only at the top-level, an exception report can be rendered
|
|
186
|
-
|
|
187
|
-
f"</wfs:{self.xml_sub_collection_tag}
|
|
188
|
-
if
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
# Only at the top-level, an exception report can be rendered, close any remaining tags.
|
|
189
|
+
if len(self.collection.results) > 1:
|
|
190
|
+
sub_closing = f"</wfs:{self.xml_sub_collection_tag}>\n</wfs:member>\n"
|
|
191
|
+
if not buffer.endswith(sub_closing):
|
|
192
|
+
buffer += sub_closing
|
|
193
|
+
|
|
191
194
|
return (
|
|
192
195
|
f"{buffer}"
|
|
193
|
-
f"{closing_child}"
|
|
194
196
|
" <wfs:truncatedResponse>"
|
|
195
197
|
f"{exception.as_xml()}"
|
|
196
198
|
" </wfs:truncatedResponse>\n"
|
|
@@ -204,9 +206,14 @@ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
|
|
|
204
206
|
collection = self.collection
|
|
205
207
|
self.output = output = StringIO()
|
|
206
208
|
self._write = self.output.write
|
|
209
|
+
|
|
210
|
+
# The base class peaks the generator and handles early exceptions.
|
|
211
|
+
# Any database exceptions during calculating the number of results
|
|
212
|
+
# are all handled by the main WFS view.
|
|
207
213
|
number_matched = collection.number_matched
|
|
208
214
|
number_matched = int(number_matched) if number_matched is not None else "unknown"
|
|
209
215
|
number_returned = collection.number_returned
|
|
216
|
+
|
|
210
217
|
next = previous = ""
|
|
211
218
|
if collection.next:
|
|
212
219
|
next = f' next="{attr_escape(collection.next)}"'
|
|
@@ -228,6 +235,15 @@ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
|
|
|
228
235
|
|
|
229
236
|
for sub_collection in collection.results:
|
|
230
237
|
projection = sub_collection.projection
|
|
238
|
+
if projection.output_crs.force_xy and projection.output_crs.is_north_east_order:
|
|
239
|
+
self._write(
|
|
240
|
+
"<!--\n"
|
|
241
|
+
f" NOTE: you are requesting the legacy projection notation '{tag_escape(projection.output_crs.origin)}'."
|
|
242
|
+
f" Please use '{tag_escape(projection.output_crs.urn)}' instead.\n\n"
|
|
243
|
+
" This also means output coordinates are ordered in legacy the 'west, north' axis ordering.\n"
|
|
244
|
+
"\n-->\n"
|
|
245
|
+
)
|
|
246
|
+
|
|
231
247
|
self.start_collection(sub_collection)
|
|
232
248
|
if has_multiple_collections:
|
|
233
249
|
self._write(
|
|
@@ -238,7 +254,7 @@ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
|
|
|
238
254
|
f' numberReturned="{int(sub_collection.number_returned)}">\n'
|
|
239
255
|
)
|
|
240
256
|
|
|
241
|
-
for instance in sub_collection:
|
|
257
|
+
for instance in self.read_features(sub_collection):
|
|
242
258
|
self.gml_seq = 0 # need to increment this between write_xml_field calls
|
|
243
259
|
self._write("<wfs:member>\n")
|
|
244
260
|
self.write_feature(projection, instance)
|
|
@@ -368,11 +384,11 @@ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
|
|
|
368
384
|
if geo_element.type is XsdTypes.gmlBoundingShapeType:
|
|
369
385
|
# Special case for <gml:boundedBy>, which doesn't need xsd:nil values, nor a gml:id.
|
|
370
386
|
# The value is not a GEOSGeometry either, as that is not exposed by django.contrib.gis.
|
|
371
|
-
|
|
387
|
+
envelope = cast(GmlBoundedByElement, geo_element).get_value(
|
|
372
388
|
instance, crs=projection.output_crs
|
|
373
389
|
)
|
|
374
|
-
if
|
|
375
|
-
self._write(self.render_gml_bounds(
|
|
390
|
+
if envelope is not None:
|
|
391
|
+
self._write(self.render_gml_bounds(envelope))
|
|
376
392
|
else:
|
|
377
393
|
# Regular geometry elements.
|
|
378
394
|
xml_qname = self.xml_qnames[geo_element]
|
|
@@ -398,12 +414,12 @@ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
|
|
|
398
414
|
|
|
399
415
|
def render_gml_bounds(self, envelope: BoundingBox) -> str:
|
|
400
416
|
"""Render the gml:boundedBy element that contains an Envelope.
|
|
401
|
-
|
|
402
|
-
as :mod:`django.contrib.gis.geos` only provides a
|
|
417
|
+
This uses an internal object type,
|
|
418
|
+
as :mod:`django.contrib.gis.geos` only provides a 4-tuple envelope.
|
|
403
419
|
"""
|
|
404
|
-
return f"""<gml:boundedBy><gml:Envelope srsDimension="2" srsName="{attr_escape(envelope.crs
|
|
405
|
-
<gml:lowerCorner>{envelope.
|
|
406
|
-
<gml:upperCorner>{envelope.
|
|
420
|
+
return f"""<gml:boundedBy><gml:Envelope srsDimension="2" srsName="{attr_escape(str(envelope.crs))}">
|
|
421
|
+
<gml:lowerCorner>{envelope.min_x} {envelope.min_y}</gml:lowerCorner>
|
|
422
|
+
<gml:upperCorner>{envelope.max_x} {envelope.max_y}</gml:upperCorner>
|
|
407
423
|
</gml:Envelope></gml:boundedBy>\n"""
|
|
408
424
|
|
|
409
425
|
def render_gml_value(
|
|
@@ -415,7 +431,7 @@ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
|
|
|
415
431
|
) -> str:
|
|
416
432
|
"""Normal case: 'value' is raw geometry data.."""
|
|
417
433
|
# In case this is a standalone response, this will be the top-level element, hence includes the xmlns.
|
|
418
|
-
base_attrs = f' gml:id="{attr_escape(gml_id)}" srsName="{attr_escape(projection.output_crs
|
|
434
|
+
base_attrs = f' gml:id="{attr_escape(gml_id)}" srsName="{attr_escape(str(projection.output_crs))}"{extra_xmlns}'
|
|
419
435
|
projection.output_crs.apply_to(value)
|
|
420
436
|
return self._render_gml_type(value, base_attrs=base_attrs)
|
|
421
437
|
|
|
@@ -517,11 +533,11 @@ class DBGMLRenderingMixin:
|
|
|
517
533
|
id_pos = gml_tag.find("gml:id=")
|
|
518
534
|
if id_pos == -1:
|
|
519
535
|
# Inject
|
|
520
|
-
|
|
536
|
+
gml = f'{gml_tag} gml:id="{attr_escape(gml_id)}"{extra_xmlns}{value[end_pos:]}'
|
|
521
537
|
else:
|
|
522
538
|
# Replace
|
|
523
539
|
end_pos1 = gml_tag.find('"', id_pos + 8)
|
|
524
|
-
|
|
540
|
+
gml = (
|
|
525
541
|
f"{gml_tag[:id_pos]}"
|
|
526
542
|
f'gml:id="{attr_escape(gml_id)}'
|
|
527
543
|
f"{value[end_pos1:end_pos]}" # from " right until >
|
|
@@ -529,6 +545,23 @@ class DBGMLRenderingMixin:
|
|
|
529
545
|
f"{value[end_pos:]}" # from > and beyond
|
|
530
546
|
)
|
|
531
547
|
|
|
548
|
+
return self._fix_db_crs_name(projection, gml)
|
|
549
|
+
|
|
550
|
+
def _fix_db_crs_name(self, projection: FeatureProjection, gml: str) -> str:
|
|
551
|
+
if projection.output_crs.force_xy:
|
|
552
|
+
# When legacy output is used, make sure the srsName matches the input.
|
|
553
|
+
# PostgreSQL will still generate the EPSG:xxxx notation.
|
|
554
|
+
return gml.replace(
|
|
555
|
+
'srsName="EPSG:', 'srsName="http://www.opengis.net/gml/srs/epsg.xml#', 1
|
|
556
|
+
)
|
|
557
|
+
elif projection.output_crs == CRS84:
|
|
558
|
+
# Fix PostgreSQL not knowing it's CRS84 or WGS84 (both srid 4326)
|
|
559
|
+
return gml.replace(
|
|
560
|
+
'srsName="urn:ogc:def:crs:EPSG::4326"', 'srsName="urn:ogc:def:crs:OGC::CRS84"', 1
|
|
561
|
+
)
|
|
562
|
+
else:
|
|
563
|
+
return gml
|
|
564
|
+
|
|
532
565
|
|
|
533
566
|
class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
534
567
|
"""Faster GetFeature renderer that uses the database to render GML 3.2"""
|
|
@@ -549,8 +582,14 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
|
549
582
|
# Retrieve geometries as pre-rendered instead.
|
|
550
583
|
# Only take the geometries of the current level.
|
|
551
584
|
# The annotations for relations will be handled by prefetches and get_prefetch_queryset()
|
|
585
|
+
use_modern = not projection.output_crs.force_xy
|
|
552
586
|
return replace_queryset_geometries(
|
|
553
|
-
queryset,
|
|
587
|
+
queryset,
|
|
588
|
+
projection.geometry_elements,
|
|
589
|
+
projection.output_crs,
|
|
590
|
+
AsGML,
|
|
591
|
+
is_latlon=use_modern and projection.output_crs.is_north_east_order,
|
|
592
|
+
long_urn=use_modern,
|
|
554
593
|
)
|
|
555
594
|
|
|
556
595
|
def get_prefetch_queryset(
|
|
@@ -564,8 +603,14 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
|
564
603
|
return None
|
|
565
604
|
|
|
566
605
|
# Find which fields are GML elements
|
|
606
|
+
use_modern = not projection.output_crs.force_xy
|
|
567
607
|
return replace_queryset_geometries(
|
|
568
|
-
queryset,
|
|
608
|
+
queryset,
|
|
609
|
+
feature_relation.geometry_elements,
|
|
610
|
+
projection.output_crs,
|
|
611
|
+
AsGML,
|
|
612
|
+
is_latlon=use_modern and projection.output_crs.is_north_east_order,
|
|
613
|
+
long_urn=use_modern,
|
|
569
614
|
)
|
|
570
615
|
|
|
571
616
|
def get_db_envelope_as_gml(self, projection: FeatureProjection, queryset) -> AsGML:
|
|
@@ -574,7 +619,13 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
|
574
619
|
This also avoids offloads the geometry union calculation to the DB.
|
|
575
620
|
"""
|
|
576
621
|
geo_fields_union = self._get_geometries_union(projection, queryset)
|
|
577
|
-
|
|
622
|
+
use_modern = not projection.output_crs.force_xy
|
|
623
|
+
return AsGML(
|
|
624
|
+
geo_fields_union,
|
|
625
|
+
envelope=True,
|
|
626
|
+
is_latlon=use_modern and projection.output_crs.is_north_east_order,
|
|
627
|
+
long_urn=use_modern,
|
|
628
|
+
)
|
|
578
629
|
|
|
579
630
|
def _get_geometries_union(self, projection: FeatureProjection, queryset):
|
|
580
631
|
"""Combine all geometries of the model in a single SQL function."""
|
|
@@ -604,6 +655,8 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
|
604
655
|
gml = instance._as_envelope_gml
|
|
605
656
|
if gml is None:
|
|
606
657
|
return
|
|
658
|
+
|
|
659
|
+
gml = self._fix_db_crs_name(projection, gml)
|
|
607
660
|
else:
|
|
608
661
|
value = get_db_rendered_geometry(instance, geo_element, AsGML)
|
|
609
662
|
if value is None:
|
|
@@ -621,9 +674,9 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
|
621
674
|
class GML32ValueRenderer(GML32Renderer):
|
|
622
675
|
"""Render the GetPropertyValue XML output in GML 3.2 format.
|
|
623
676
|
|
|
624
|
-
Geoserver seems to generate the element tag inside each
|
|
625
|
-
The GML standard demonstrates to render only their content inside a
|
|
626
|
-
(either plain text or an
|
|
677
|
+
Geoserver seems to generate the element tag inside each ``<wfs:member>`` element. We've applied this one.
|
|
678
|
+
The GML standard demonstrates to render only their content inside a ``<wfs:member>`` element
|
|
679
|
+
(either plain text or an ``<gml:...>`` tag). Not sure what is right here.
|
|
627
680
|
"""
|
|
628
681
|
|
|
629
682
|
content_type = "text/xml; charset=utf-8"
|
|
@@ -734,8 +787,14 @@ class DBGML32ValueRenderer(DBGMLRenderingMixin, GML32ValueRenderer):
|
|
|
734
787
|
if element.type.is_geometry:
|
|
735
788
|
# Add 'gml_member' to point to the pre-rendered GML version.
|
|
736
789
|
geo_element = cast(GeometryXsdElement, element)
|
|
790
|
+
use_modern = not projection.output_crs.force_xy
|
|
737
791
|
return queryset.values(
|
|
738
|
-
"pk",
|
|
792
|
+
"pk",
|
|
793
|
+
gml_member=AsGML(
|
|
794
|
+
get_db_geometry_target(geo_element, projection.output_crs),
|
|
795
|
+
is_latlon=use_modern and projection.output_crs.is_north_east_order,
|
|
796
|
+
long_urn=use_modern,
|
|
797
|
+
),
|
|
739
798
|
)
|
|
740
799
|
else:
|
|
741
800
|
return queryset
|
gisserver/output/results.py
CHANGED
|
@@ -7,18 +7,17 @@ properties match the WFS 2.0 spec closely.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import math
|
|
10
|
-
import operator
|
|
11
10
|
import typing
|
|
12
11
|
from collections.abc import Iterable
|
|
13
12
|
from datetime import timezone
|
|
14
|
-
from functools import cached_property
|
|
13
|
+
from functools import cached_property
|
|
15
14
|
|
|
16
15
|
from django.db import models
|
|
17
16
|
from django.utils.timezone import now
|
|
18
17
|
|
|
19
18
|
from gisserver import conf
|
|
19
|
+
from gisserver.exceptions import wrap_filter_errors
|
|
20
20
|
from gisserver.features import FeatureType
|
|
21
|
-
from gisserver.geometries import BoundingBox
|
|
22
21
|
|
|
23
22
|
from .iters import ChunkedQuerySetIterator, CountingIterator
|
|
24
23
|
|
|
@@ -33,7 +32,7 @@ class SimpleFeatureCollection:
|
|
|
33
32
|
"""Wrapper to read a result set.
|
|
34
33
|
|
|
35
34
|
This object type is defined in the WFS spec.
|
|
36
|
-
It holds a collection of
|
|
35
|
+
It holds a collection of ``<wfs:member>`` objects.
|
|
37
36
|
"""
|
|
38
37
|
|
|
39
38
|
def __init__(
|
|
@@ -86,7 +85,8 @@ class SimpleFeatureCollection:
|
|
|
86
85
|
"""Explicitly request the results to be streamed.
|
|
87
86
|
|
|
88
87
|
This can be used by output formats that stream results, and don't
|
|
89
|
-
access
|
|
88
|
+
access :attr:`number_returned`.
|
|
89
|
+
Note this is not compatible with ``prefetch_related()``.
|
|
90
90
|
"""
|
|
91
91
|
if self._result_iterator is not None:
|
|
92
92
|
raise RuntimeError("Results for feature collection are read twice.")
|
|
@@ -126,12 +126,13 @@ class SimpleFeatureCollection:
|
|
|
126
126
|
return self.queryset[self.start : self.stop + (1 if add_sentinel else 0)]
|
|
127
127
|
|
|
128
128
|
def first(self):
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
with wrap_filter_errors(self.source_query):
|
|
130
|
+
try:
|
|
131
|
+
# Don't query a full page, return only one instance (for GetFeatureById)
|
|
132
|
+
# This also preserves the extra added annotations (like _as_gml_FIELD)
|
|
133
|
+
return self.queryset[self.start]
|
|
134
|
+
except IndexError:
|
|
135
|
+
return None
|
|
135
136
|
|
|
136
137
|
def fetch_results(self):
|
|
137
138
|
"""Forcefully read the results early."""
|
|
@@ -148,11 +149,15 @@ class SimpleFeatureCollection:
|
|
|
148
149
|
if self.stop == math.inf:
|
|
149
150
|
# Infinite page requested, see if start is still requested
|
|
150
151
|
qs = self.queryset[self.start :] if self.start else self.queryset.all()
|
|
151
|
-
|
|
152
|
+
|
|
153
|
+
with wrap_filter_errors(self.source_query):
|
|
154
|
+
self._result_cache = list(qs)
|
|
152
155
|
elif self._use_sentinel_record:
|
|
153
156
|
# No counting, but instead fetch an extra item as sentinel to see if there are more results.
|
|
154
157
|
qs = self.queryset[self.start : self.stop + 1]
|
|
155
|
-
|
|
158
|
+
|
|
159
|
+
with wrap_filter_errors(self.source_query):
|
|
160
|
+
page_results = list(qs)
|
|
156
161
|
|
|
157
162
|
# The stop + 1 sentinel allows checking if there is a next page.
|
|
158
163
|
# This means no COUNT() is needed to detect that.
|
|
@@ -167,7 +172,9 @@ class SimpleFeatureCollection:
|
|
|
167
172
|
# Fetch exactly the page size, no more is needed.
|
|
168
173
|
# Will use a COUNT on the total table, so it can be used to see if there are more pages.
|
|
169
174
|
qs = self.queryset[self.start : self.stop]
|
|
170
|
-
|
|
175
|
+
|
|
176
|
+
with wrap_filter_errors(self.source_query):
|
|
177
|
+
self._result_cache = list(qs)
|
|
171
178
|
|
|
172
179
|
@cached_property
|
|
173
180
|
def _use_sentinel_record(self) -> bool:
|
|
@@ -224,7 +231,8 @@ class SimpleFeatureCollection:
|
|
|
224
231
|
qs.query.annotations = clean_annotations
|
|
225
232
|
|
|
226
233
|
# Calculate, cache and return
|
|
227
|
-
self.
|
|
234
|
+
with wrap_filter_errors(self.source_query):
|
|
235
|
+
self._number_matched = qs.count()
|
|
228
236
|
return self._number_matched
|
|
229
237
|
|
|
230
238
|
@property
|
|
@@ -278,28 +286,11 @@ class SimpleFeatureCollection:
|
|
|
278
286
|
# but since the projection needs to be calculated once, it's stored here for convenience.
|
|
279
287
|
return self.source_query.get_projection()
|
|
280
288
|
|
|
281
|
-
def get_bounding_box(self) -> BoundingBox:
|
|
282
|
-
"""Determine bounding box of all items."""
|
|
283
|
-
self.fetch_results() # Avoid querying results twice
|
|
284
|
-
|
|
285
|
-
# Start with an obviously invalid bbox,
|
|
286
|
-
# which corrects at the first extend_to_geometry call.
|
|
287
|
-
bbox = BoundingBox(math.inf, math.inf, -math.inf, -math.inf)
|
|
288
|
-
|
|
289
|
-
# Allow the geometry to exist in a dotted relationship.
|
|
290
|
-
for instance in self:
|
|
291
|
-
geometry_value = self.projection.get_main_geometry_value(instance)
|
|
292
|
-
if geometry_value is None:
|
|
293
|
-
continue
|
|
294
|
-
|
|
295
|
-
bbox.extend_to_geometry(geometry_value)
|
|
296
|
-
|
|
297
|
-
return bbox
|
|
298
|
-
|
|
299
289
|
|
|
300
290
|
class FeatureCollection:
|
|
301
|
-
"""WFS object that holds the result type for GetFeature
|
|
291
|
+
"""WFS object that holds the result type for ``GetFeature``.
|
|
302
292
|
This object type is defined in the WFS spec.
|
|
293
|
+
It holds a collection of :class:`SimpleFeatureCollection` results.
|
|
303
294
|
"""
|
|
304
295
|
|
|
305
296
|
def __init__(
|
|
@@ -323,11 +314,6 @@ class FeatureCollection:
|
|
|
323
314
|
self.date = now()
|
|
324
315
|
self.timestamp = self.date.astimezone(timezone.utc).isoformat()
|
|
325
316
|
|
|
326
|
-
def get_bounding_box(self) -> BoundingBox:
|
|
327
|
-
"""Determine bounding box of all items."""
|
|
328
|
-
# Combine the bounding box of all collections
|
|
329
|
-
return reduce(operator.add, [c.get_bounding_box() for c in self.results])
|
|
330
|
-
|
|
331
317
|
@cached_property
|
|
332
318
|
def number_returned(self) -> int:
|
|
333
319
|
"""Return the total number of returned features"""
|
gisserver/output/utils.py
CHANGED
|
@@ -23,16 +23,20 @@ __all__ = (
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def tag_escape(s: str):
|
|
26
|
+
"""Escape a value for usage in XML text."""
|
|
26
27
|
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def attr_escape(s: str):
|
|
30
|
-
|
|
31
|
+
"""Escape a value for usage in an XML attribute.
|
|
32
|
+
This is slightly faster than ``html.escape()`` as it doesn't replace single quotes.
|
|
33
|
+
"""
|
|
31
34
|
# Having tried all possible variants, this code still outperforms other forms of escaping.
|
|
32
35
|
return s.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
def value_to_xml_string(value):
|
|
39
|
+
"""Format a Python value for usage in XML text."""
|
|
36
40
|
# Simple scalar value
|
|
37
41
|
if isinstance(value, str): # most cases
|
|
38
42
|
return tag_escape(value)
|
|
@@ -47,6 +51,9 @@ def value_to_xml_string(value):
|
|
|
47
51
|
|
|
48
52
|
|
|
49
53
|
def value_to_text(value):
|
|
54
|
+
"""Format a Python value for usage in plain text output.
|
|
55
|
+
This doesn't do any XML escaping.
|
|
56
|
+
"""
|
|
50
57
|
# Simple scalar value, no XML escapes
|
|
51
58
|
if isinstance(value, str): # most cases
|
|
52
59
|
return value
|
|
@@ -59,7 +66,7 @@ def value_to_text(value):
|
|
|
59
66
|
|
|
60
67
|
|
|
61
68
|
def render_xmlns_attributes(xml_namespaces: dict[str, str]):
|
|
62
|
-
"""Render XML Namespace declaration attributes"""
|
|
69
|
+
"""Render XML Namespace declaration attributes, i.e. ``xmlns:prefix="uri"`` for each dict item."""
|
|
63
70
|
return " ".join(
|
|
64
71
|
f'xmlns:{prefix}="{xml_namespace}"' if prefix else f'xmlns="{xml_namespace}"'
|
|
65
72
|
for xml_namespace, prefix in xml_namespaces.items()
|