django-gisserver 2.0__py3-none-any.whl → 2.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.dist-info}/METADATA +26 -10
- django_gisserver-2.1.dist-info/RECORD +68 -0
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/crs.py +401 -0
- gisserver/db.py +71 -5
- gisserver/exceptions.py +106 -2
- gisserver/extensions/functions.py +122 -28
- gisserver/extensions/queries.py +15 -10
- gisserver/features.py +44 -36
- 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 +43 -23
- gisserver/output/results.py +25 -39
- gisserver/output/utils.py +9 -2
- gisserver/parsers/ast.py +171 -65
- 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 +48 -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 +25 -17
- 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 +8 -3
- gisserver/templates/gisserver/base.html +12 -0
- gisserver/templates/gisserver/index.html +9 -15
- gisserver/templates/gisserver/service_description.html +12 -6
- gisserver/templates/gisserver/wfs/feature_field.html +1 -1
- gisserver/templates/gisserver/wfs/feature_type.html +35 -13
- gisserver/types.py +150 -81
- gisserver/views.py +47 -24
- django_gisserver-2.0.dist-info/RECORD +0 -66
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
"""These classes map to the FES 2.0 specification for operators.
|
|
2
2
|
The class names and attributes are identical to those in the FES spec.
|
|
3
|
+
|
|
4
|
+
Inheritance structure:
|
|
5
|
+
|
|
6
|
+
* :class:`Operator`
|
|
7
|
+
|
|
8
|
+
* :class:`IdOperator` chains ``<fes:ResourceId>``.
|
|
9
|
+
* :class:`NonIdOperator`
|
|
10
|
+
|
|
11
|
+
* :class:`ComparisonOperator`
|
|
12
|
+
|
|
13
|
+
* :class:`BinaryComparisonOperator` for :class:`BinaryComparisonName` tags like ``<fes:PropertyIsEqualTo>``.
|
|
14
|
+
* :class:`BetweenComparisonOperator` for ``<fes:PropertyIsBetween>``
|
|
15
|
+
* :class:`LikeOperator` for ``<fes:PropertyIsLike>``.
|
|
16
|
+
* :class:`NilOperator` for ``<fes:PropertyIsNil>``.
|
|
17
|
+
* :class:`NullOperator` for ``<fes:PropertyIsNill>``.
|
|
18
|
+
|
|
19
|
+
* :class:`SpatialOperator`
|
|
20
|
+
|
|
21
|
+
* :class:`DistanceOperator` for the :class:`DistanceOperator` tags: ``<fes:DWithin>`` and ``<fes:Beyond>``.
|
|
22
|
+
* :class:`BinarySpatialOperator` for :class:`SpatialOperatorName` tags like ``<fes:BBOX>``.
|
|
23
|
+
|
|
24
|
+
* :class:`TemporalOperator` for :class:`TemporalOperatorName` tags like ``<fes:After>``.
|
|
25
|
+
* :class:`LogicalOperator`
|
|
26
|
+
|
|
27
|
+
* :class:`BinaryLogicOperator` for the :class:`BinaryLogicType` tags: ``<fes:And>`` and ``<fes:Or>``.
|
|
28
|
+
* :class:`UnaryLogicOperator` for the :class:`UnaryLogicType` tag: ``<fes:Not>``.
|
|
29
|
+
|
|
30
|
+
* :class:`ExtensionOperator` for custom additions.
|
|
3
31
|
"""
|
|
4
32
|
|
|
5
33
|
from __future__ import annotations
|
|
@@ -11,7 +39,7 @@ from decimal import Decimal
|
|
|
11
39
|
from enum import Enum
|
|
12
40
|
from functools import cached_property, reduce
|
|
13
41
|
from itertools import groupby
|
|
14
|
-
from typing import ClassVar,
|
|
42
|
+
from typing import ClassVar, Union
|
|
15
43
|
|
|
16
44
|
from django.contrib.gis import measure
|
|
17
45
|
from django.db.models import Q
|
|
@@ -23,13 +51,13 @@ from gisserver.exceptions import (
|
|
|
23
51
|
)
|
|
24
52
|
from gisserver.parsers import gml
|
|
25
53
|
from gisserver.parsers.ast import (
|
|
26
|
-
|
|
54
|
+
AstNode,
|
|
27
55
|
TagNameEnum,
|
|
28
56
|
expect_children,
|
|
29
57
|
expect_tag,
|
|
30
58
|
tag_registry,
|
|
31
59
|
)
|
|
32
|
-
from gisserver.parsers.query import CompiledQuery, RhsTypes
|
|
60
|
+
from gisserver.parsers.query import CompiledQuery, RhsTypes, ScalarTypes
|
|
33
61
|
from gisserver.parsers.values import fix_type_name
|
|
34
62
|
from gisserver.parsers.xml import NSElement, xmlns
|
|
35
63
|
from gisserver.types import GeometryXsdElement, XPathMatch
|
|
@@ -40,7 +68,10 @@ from .lookups import ARRAY_LOOKUPS # also registers the lookups.
|
|
|
40
68
|
|
|
41
69
|
logger = logging.getLogger(__name__)
|
|
42
70
|
|
|
71
|
+
#: Define the types that a ``<gml:SpatialDescription>`` can be:
|
|
43
72
|
SpatialDescription = Union[gml.GM_Object, gml.GM_Envelope, ValueReference]
|
|
73
|
+
|
|
74
|
+
#: Define the types that a ``<gml:TemporalOperand>`` can be:
|
|
44
75
|
TemporalOperand = Union[gml.TM_Object, ValueReference]
|
|
45
76
|
|
|
46
77
|
# Fully qualified tag names
|
|
@@ -48,16 +79,11 @@ FES_VALUE_REFERENCE = xmlns.fes20.qname("ValueReference")
|
|
|
48
79
|
FES_DISTANCE = xmlns.fes20.qname("Distance")
|
|
49
80
|
FES_LOWER_BOUNDARY = xmlns.fes20.qname("LowerBoundary")
|
|
50
81
|
FES_UPPER_BOUNDARY = xmlns.fes20.qname("UpperBoundary")
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class HasBuildRhs(Protocol):
|
|
54
|
-
"""Define interface for any class that has ``build_rhs()``."""
|
|
55
|
-
|
|
56
|
-
def build_rhs(self, compiler) -> RhsTypes: ...
|
|
82
|
+
FES1_PROPERTY_NAME = xmlns.fes20.qname("PropertyName") # old tag sometimes used by clients
|
|
57
83
|
|
|
58
84
|
|
|
59
85
|
class MatchAction(Enum):
|
|
60
|
-
"""Values for the 'matchAction' attribute of the BinaryComparisonOperator
|
|
86
|
+
"""Values for the 'matchAction' attribute of the :class:`BinaryComparisonOperator`."""
|
|
61
87
|
|
|
62
88
|
All = "All"
|
|
63
89
|
Any = "Any"
|
|
@@ -81,6 +107,14 @@ class BinaryComparisonName(TagNameEnum):
|
|
|
81
107
|
PropertyIsGreaterThanOrEqualTo = "gte"
|
|
82
108
|
|
|
83
109
|
|
|
110
|
+
REVERSE_LOOKUPS = {
|
|
111
|
+
"gt": "lt",
|
|
112
|
+
"gte": "lte",
|
|
113
|
+
"lt": "gt",
|
|
114
|
+
"lte": "gte",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
84
118
|
class DistanceOperatorName(TagNameEnum):
|
|
85
119
|
"""XML tag names mapped to distance operators for the ORM."""
|
|
86
120
|
|
|
@@ -146,7 +180,8 @@ class UnaryLogicType(TagNameEnum):
|
|
|
146
180
|
|
|
147
181
|
|
|
148
182
|
@dataclass
|
|
149
|
-
|
|
183
|
+
@tag_registry.register("Distance")
|
|
184
|
+
class Measure(AstNode):
|
|
150
185
|
"""A measurement for a distance element.
|
|
151
186
|
|
|
152
187
|
This parses and handles the syntax::
|
|
@@ -175,7 +210,7 @@ class Measure(BaseNode):
|
|
|
175
210
|
return measure.Distance(default_unit=self.uom, **{self.uom: self.value})
|
|
176
211
|
|
|
177
212
|
|
|
178
|
-
class Operator(
|
|
213
|
+
class Operator(AstNode):
|
|
179
214
|
"""Abstract base class, as defined by FES spec.
|
|
180
215
|
|
|
181
216
|
This base class is also used in parsing; for example the ``<fes:Filter>``
|
|
@@ -192,7 +227,16 @@ class Operator(BaseNode):
|
|
|
192
227
|
|
|
193
228
|
@dataclass
|
|
194
229
|
class IdOperator(Operator):
|
|
195
|
-
"""List of ResourceId objects
|
|
230
|
+
"""List of :class:`~gisserver.parsers.fes20.identifers.ResourceId`` objects.
|
|
231
|
+
|
|
232
|
+
A ``<fes:Filter>`` only has a single predicate.
|
|
233
|
+
Hence, this operator is used to wrap the ``<fes:ResourceId>`` elements in the syntax::
|
|
234
|
+
|
|
235
|
+
<fes:Filter>
|
|
236
|
+
<fes:ResourceId rid="typename.123" />
|
|
237
|
+
<fes:ResourceId rid="typename.345" />
|
|
238
|
+
</fes:Filter>
|
|
239
|
+
"""
|
|
196
240
|
|
|
197
241
|
id: list[Id]
|
|
198
242
|
|
|
@@ -242,7 +286,7 @@ class NonIdOperator(Operator):
|
|
|
242
286
|
"""Abstract base class, as defined by FES spec.
|
|
243
287
|
|
|
244
288
|
This is used for nearly all operators,
|
|
245
|
-
except those that have
|
|
289
|
+
except those that have ``<fes:ResourceId>`` elements as children.
|
|
246
290
|
|
|
247
291
|
Some operators, such as the ``<fes:And>``, ``<fes:Or>`` and ``<fes:Not>`` operators
|
|
248
292
|
explicitly support only ``NonIdOperator`` elements as arguments.
|
|
@@ -266,18 +310,28 @@ class NonIdOperator(Operator):
|
|
|
266
310
|
# lhs and rhs are allowed to be reversed. However, the SQL compiler
|
|
267
311
|
# works much simpler when Django can predict the actual data type.
|
|
268
312
|
if isinstance(lhs, Literal) and isinstance(rhs, ValueReference):
|
|
313
|
+
logger.debug("Filter switches lhs/rhs for %s %s %s", lhs.raw_value, lookup, rhs.xpath)
|
|
269
314
|
lhs, rhs = rhs, lhs
|
|
315
|
+
lookup = REVERSE_LOOKUPS.get(lookup, lookup) # >= should become <=
|
|
270
316
|
|
|
271
|
-
|
|
272
|
-
lookup = self.validate_comparison(compiler, lhs, lookup, rhs)
|
|
317
|
+
lookup = self.validate_comparison(compiler, lhs, lookup, rhs)
|
|
273
318
|
|
|
274
319
|
# Build Django Q-object
|
|
275
|
-
|
|
320
|
+
lhs_orm_name = lhs.build_lhs(compiler)
|
|
276
321
|
|
|
277
322
|
if isinstance(rhs, (Expression, gml.GM_Object)):
|
|
278
323
|
rhs = rhs.build_rhs(compiler)
|
|
279
|
-
|
|
280
|
-
|
|
324
|
+
if (
|
|
325
|
+
isinstance(lhs, ValueReference)
|
|
326
|
+
and isinstance(rhs, ScalarTypes)
|
|
327
|
+
and lookup != "isnull"
|
|
328
|
+
):
|
|
329
|
+
# Building the expression resolved as a scalar after all (for BinaryOperator).
|
|
330
|
+
# It allows performing a better type check:
|
|
331
|
+
xsd_element = lhs.parse_xpath(compiler.feature_types).child
|
|
332
|
+
xsd_element.to_python(rhs)
|
|
333
|
+
|
|
334
|
+
comparison = Q(**{f"{lhs_orm_name}__{lookup}": rhs})
|
|
281
335
|
return compiler.apply_extra_lookups(comparison)
|
|
282
336
|
|
|
283
337
|
def validate_comparison(
|
|
@@ -286,20 +340,20 @@ class NonIdOperator(Operator):
|
|
|
286
340
|
lhs: Expression,
|
|
287
341
|
lookup: str,
|
|
288
342
|
rhs: Expression | gml.GM_Object | RhsTypes,
|
|
289
|
-
):
|
|
343
|
+
) -> str:
|
|
290
344
|
"""Validate whether a given comparison is even possible.
|
|
291
345
|
|
|
292
346
|
For example, comparisons like ``name == "test"`` are fine,
|
|
293
|
-
but ``geometry < 4`` or ``datefield == 35.2
|
|
294
|
-
|
|
295
|
-
The lhs/rhs are expected to be ordered in a logical sequence.
|
|
296
|
-
So ``<value> == <element>`` should be provided as ``<element> == <value>``.
|
|
347
|
+
but ``geometry < 4`` or ``datefield == 35.2`` raise an error.
|
|
297
348
|
|
|
298
349
|
:param compiler: The object that holds the intermediate state
|
|
299
350
|
:param lhs: The left-hand-side of the comparison (e.g. the element).
|
|
300
351
|
:param lookup: The ORM lookup expression being used (e.g. ``equals`` or ``fes_like``).
|
|
301
352
|
:param rhs: The right-hand-side of the comparison (e.g. the value).
|
|
302
353
|
"""
|
|
354
|
+
if isinstance(lhs, Literal) and isinstance(rhs, ValueReference):
|
|
355
|
+
lhs, rhs = rhs, lhs
|
|
356
|
+
|
|
303
357
|
if isinstance(lhs, ValueReference):
|
|
304
358
|
xsd_element = lhs.parse_xpath(compiler.feature_types).child
|
|
305
359
|
tag = self._source if self._source is not None else None
|
|
@@ -336,6 +390,9 @@ class NonIdOperator(Operator):
|
|
|
336
390
|
# When a common case of value comparison is done, the inputs
|
|
337
391
|
# can be validated before the ORM query is constructed.
|
|
338
392
|
xsd_element.validate_comparison(rhs.raw_value, lookup=lookup, tag=tag)
|
|
393
|
+
elif isinstance(rhs, ScalarTypes) and lookup != "isnull":
|
|
394
|
+
# Can still compare two elements, e.g. date >= ((2020 - 12) - 10) by QGis
|
|
395
|
+
xsd_element.validate_comparison(rhs, lookup=lookup, tag=tag)
|
|
339
396
|
|
|
340
397
|
return lookup
|
|
341
398
|
|
|
@@ -347,9 +404,8 @@ class NonIdOperator(Operator):
|
|
|
347
404
|
rhs: tuple[Expression | ValueReference | gml.GM_Object, Expression | Measure],
|
|
348
405
|
) -> Q:
|
|
349
406
|
"""Use the value in comparison with 2 other values (e.g. between query)"""
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
self.validate_comparison(compiler, lhs, lookup, rhs[1])
|
|
407
|
+
self.validate_comparison(compiler, lhs, lookup, rhs[0])
|
|
408
|
+
self.validate_comparison(compiler, lhs, lookup, rhs[1])
|
|
353
409
|
|
|
354
410
|
field_name = lhs.build_lhs(compiler)
|
|
355
411
|
comparison = Q(
|
|
@@ -390,7 +446,7 @@ class DistanceOperator(SpatialOperator):
|
|
|
390
446
|
operatorType: DistanceOperatorName
|
|
391
447
|
geometry: gml.GM_Object
|
|
392
448
|
distance: Measure
|
|
393
|
-
_source: str | None = field(compare=False, default=None)
|
|
449
|
+
_source: str | None = field(compare=False, default=None, repr=False)
|
|
394
450
|
|
|
395
451
|
@classmethod
|
|
396
452
|
@expect_children(3, ValueReference, gml.GM_Object, Measure)
|
|
@@ -442,7 +498,7 @@ class BinarySpatialOperator(SpatialOperator):
|
|
|
442
498
|
operatorType: SpatialOperatorName
|
|
443
499
|
operand1: ValueReference | None
|
|
444
500
|
operand2: SpatialDescription
|
|
445
|
-
_source: str | None = field(compare=False, default=None)
|
|
501
|
+
_source: str | None = field(compare=False, default=None, repr=False)
|
|
446
502
|
|
|
447
503
|
@classmethod
|
|
448
504
|
def from_xml(cls, element: NSElement):
|
|
@@ -498,7 +554,7 @@ class _ResolvedValueReference(ValueReference):
|
|
|
498
554
|
|
|
499
555
|
|
|
500
556
|
@dataclass
|
|
501
|
-
@tag_registry.register(TemporalOperatorName) # <After>, <Before>, ...
|
|
557
|
+
@tag_registry.register(TemporalOperatorName, hidden=True) # <After>, <Before>, ...
|
|
502
558
|
class TemporalOperator(NonIdOperator):
|
|
503
559
|
"""Comparisons with dates.
|
|
504
560
|
|
|
@@ -536,7 +592,7 @@ class TemporalOperator(NonIdOperator):
|
|
|
536
592
|
operatorType: TemporalOperatorName
|
|
537
593
|
operand1: ValueReference
|
|
538
594
|
operand2: TemporalOperand
|
|
539
|
-
_source: str | None = field(compare=False, default=None)
|
|
595
|
+
_source: str | None = field(compare=False, default=None, repr=False)
|
|
540
596
|
|
|
541
597
|
@classmethod
|
|
542
598
|
@expect_children(2, ValueReference, *TemporalOperand.__args__)
|
|
@@ -557,15 +613,11 @@ class ComparisonOperator(NonIdOperator):
|
|
|
557
613
|
and allows grouping various comparisons together.
|
|
558
614
|
"""
|
|
559
615
|
|
|
560
|
-
# Start counting fresh here, to collect the capabilities
|
|
561
|
-
# that are listed in the <fes20:ComparisonOperators> node:
|
|
562
|
-
xml_tags = []
|
|
563
|
-
|
|
564
616
|
|
|
565
617
|
@dataclass
|
|
566
618
|
@tag_registry.register(BinaryComparisonName) # <PropertyIs...>
|
|
567
619
|
class BinaryComparisonOperator(ComparisonOperator):
|
|
568
|
-
"""A comparison between 2 values, e.g. A == B
|
|
620
|
+
"""A comparison between 2 values, e.g. *A == B*.
|
|
569
621
|
|
|
570
622
|
This parses and handles the syntax::
|
|
571
623
|
|
|
@@ -588,7 +640,7 @@ class BinaryComparisonOperator(ComparisonOperator):
|
|
|
588
640
|
_source: str | None = field(compare=False, default=None)
|
|
589
641
|
|
|
590
642
|
@classmethod
|
|
591
|
-
@expect_children(2, Expression,
|
|
643
|
+
@expect_children(2, Expression, silent_allowed=(FES1_PROPERTY_NAME,))
|
|
592
644
|
def from_xml(cls, element: NSElement):
|
|
593
645
|
return cls(
|
|
594
646
|
operatorType=BinaryComparisonName.from_xml(element),
|
|
@@ -626,10 +678,10 @@ class BetweenComparisonOperator(ComparisonOperator):
|
|
|
626
678
|
expression: Expression
|
|
627
679
|
lowerBoundary: Expression
|
|
628
680
|
upperBoundary: Expression
|
|
629
|
-
_source: str | None = field(compare=False, default=None)
|
|
681
|
+
_source: str | None = field(compare=False, default=None, repr=False)
|
|
630
682
|
|
|
631
683
|
@classmethod
|
|
632
|
-
@expect_children(3, Expression,
|
|
684
|
+
@expect_children(3, Expression, FES_LOWER_BOUNDARY, FES_UPPER_BOUNDARY)
|
|
633
685
|
def from_xml(cls, element: NSElement):
|
|
634
686
|
if (element[1].tag != FES_LOWER_BOUNDARY) or (element[2].tag != FES_UPPER_BOUNDARY):
|
|
635
687
|
raise ExternalParsingError(
|
|
@@ -678,10 +730,10 @@ class LikeOperator(ComparisonOperator):
|
|
|
678
730
|
wildCard: str
|
|
679
731
|
singleChar: str
|
|
680
732
|
escapeChar: str
|
|
681
|
-
_source: str | None = field(compare=False, default=None)
|
|
733
|
+
_source: str | None = field(compare=False, default=None, repr=False)
|
|
682
734
|
|
|
683
735
|
@classmethod
|
|
684
|
-
@expect_children(2, Expression
|
|
736
|
+
@expect_children(2, Expression)
|
|
685
737
|
def from_xml(cls, element: NSElement):
|
|
686
738
|
return cls(
|
|
687
739
|
expression=(
|
|
@@ -711,7 +763,7 @@ class LikeOperator(ComparisonOperator):
|
|
|
711
763
|
rhs = Literal(raw_value=value)
|
|
712
764
|
else:
|
|
713
765
|
raise ExternalParsingError(
|
|
714
|
-
f"Expected a literal value for the {self.
|
|
766
|
+
f"Expected a literal value for the {self.xml_name} operator."
|
|
715
767
|
)
|
|
716
768
|
|
|
717
769
|
# Use the FesLike lookup
|
|
@@ -722,7 +774,7 @@ class LikeOperator(ComparisonOperator):
|
|
|
722
774
|
@tag_registry.register("PropertyIsNil")
|
|
723
775
|
class NilOperator(ComparisonOperator):
|
|
724
776
|
"""Check whether the value evaluates to null/None.
|
|
725
|
-
If the WFS returned a property element with
|
|
777
|
+
If the WFS returned a property element with ``<app:field xsi:nil='true'>``, this returns true.
|
|
726
778
|
|
|
727
779
|
It parses and handles syntax such as::
|
|
728
780
|
|
|
@@ -736,7 +788,7 @@ class NilOperator(ComparisonOperator):
|
|
|
736
788
|
|
|
737
789
|
expression: Expression | None
|
|
738
790
|
nilReason: str
|
|
739
|
-
_source: str | None = field(compare=False, default=None)
|
|
791
|
+
_source: str | None = field(compare=False, default=None, repr=False)
|
|
740
792
|
|
|
741
793
|
# Allow checking whether the geometry is null
|
|
742
794
|
allow_geometries: ClassVar[bool] = True
|
|
@@ -759,7 +811,7 @@ class NilOperator(ComparisonOperator):
|
|
|
759
811
|
@tag_registry.register("PropertyIsNull")
|
|
760
812
|
class NullOperator(ComparisonOperator):
|
|
761
813
|
"""Check whether the property exists.
|
|
762
|
-
If the WFS would not return the property element
|
|
814
|
+
If the WFS would not return the property element ``<app:field>``, this returns true.
|
|
763
815
|
|
|
764
816
|
It parses and handles syntax such as::
|
|
765
817
|
|
|
@@ -771,7 +823,7 @@ class NullOperator(ComparisonOperator):
|
|
|
771
823
|
"""
|
|
772
824
|
|
|
773
825
|
expression: Expression
|
|
774
|
-
_source: str | None = field(compare=False, default=None)
|
|
826
|
+
_source: str | None = field(compare=False, default=None, repr=False)
|
|
775
827
|
|
|
776
828
|
# Allow checking whether the geometry is null
|
|
777
829
|
allow_geometries: ClassVar[bool] = True
|
|
@@ -815,10 +867,10 @@ class BinaryLogicOperator(LogicalOperator):
|
|
|
815
867
|
|
|
816
868
|
operands: list[NonIdOperator]
|
|
817
869
|
operatorType: BinaryLogicType
|
|
818
|
-
_source: str | None = field(compare=False, default=None)
|
|
870
|
+
_source: str | None = field(compare=False, default=None, repr=False)
|
|
819
871
|
|
|
820
872
|
@classmethod
|
|
821
|
-
@expect_children(2, NonIdOperator
|
|
873
|
+
@expect_children(2, NonIdOperator)
|
|
822
874
|
def from_xml(cls, element: NSElement):
|
|
823
875
|
return cls(
|
|
824
876
|
operands=[NonIdOperator.child_from_xml(child) for child in element],
|
|
@@ -848,7 +900,7 @@ class UnaryLogicOperator(LogicalOperator):
|
|
|
848
900
|
|
|
849
901
|
operands: NonIdOperator
|
|
850
902
|
operatorType: UnaryLogicType
|
|
851
|
-
_source: str | None = field(compare=False, default=None)
|
|
903
|
+
_source: str | None = field(compare=False, default=None, repr=False)
|
|
852
904
|
|
|
853
905
|
@classmethod
|
|
854
906
|
@expect_children(1, NonIdOperator)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
"""The FES elements that handle sorting."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
5
|
from dataclasses import dataclass
|
|
4
6
|
from enum import Enum
|
|
5
7
|
|
|
6
8
|
from gisserver.exceptions import InvalidParameterValue
|
|
7
|
-
from gisserver.parsers.ast import
|
|
9
|
+
from gisserver.parsers.ast import AstNode, expect_children, expect_tag, tag_registry
|
|
8
10
|
from gisserver.parsers.fes20 import ValueReference
|
|
9
11
|
from gisserver.parsers.ows import KVPRequest
|
|
10
12
|
from gisserver.parsers.query import CompiledQuery
|
|
@@ -14,11 +16,16 @@ FES_SORT_ORDER = xmlns.fes20.qname("SortOrder")
|
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class SortOrder(Enum):
|
|
19
|
+
#: Ascending order
|
|
17
20
|
ASC = ""
|
|
21
|
+
#: Descrending order
|
|
18
22
|
DESC = "-"
|
|
19
23
|
|
|
20
|
-
# Support WFS 1 names for clients that still use this
|
|
24
|
+
# Support WFS 1 names for clients that still use this:
|
|
25
|
+
|
|
26
|
+
#: WFS 1 name that clients still use for ascending.
|
|
21
27
|
A = ASC
|
|
28
|
+
#: WFS 1 name that clients still use for descending.
|
|
22
29
|
D = DESC
|
|
23
30
|
|
|
24
31
|
@classmethod
|
|
@@ -38,7 +45,7 @@ class SortOrder(Enum):
|
|
|
38
45
|
|
|
39
46
|
@dataclass
|
|
40
47
|
@tag_registry.register("SortProperty", xmlns.fes20)
|
|
41
|
-
class SortProperty(
|
|
48
|
+
class SortProperty(AstNode):
|
|
42
49
|
"""This class name is based on the WFS spec.
|
|
43
50
|
|
|
44
51
|
This parses and handles the syntax::
|
|
@@ -60,7 +67,7 @@ class SortProperty(BaseNode):
|
|
|
60
67
|
|
|
61
68
|
@classmethod
|
|
62
69
|
@expect_tag(xmlns.fes20, "SortProperty")
|
|
63
|
-
@expect_children(1, ValueReference,
|
|
70
|
+
@expect_children(1, ValueReference, FES_SORT_ORDER)
|
|
64
71
|
def from_xml(cls, element: NSElement) -> SortProperty:
|
|
65
72
|
"""Parse the incoming XML"""
|
|
66
73
|
sort_order = element.find(FES_SORT_ORDER)
|
|
@@ -90,7 +97,7 @@ class SortProperty(BaseNode):
|
|
|
90
97
|
|
|
91
98
|
@dataclass
|
|
92
99
|
@tag_registry.register("SortBy", xmlns.fes20)
|
|
93
|
-
class SortBy(
|
|
100
|
+
class SortBy(AstNode):
|
|
94
101
|
"""The sortBy clause.
|
|
95
102
|
|
|
96
103
|
This parses and handles the syntax::
|
|
@@ -105,10 +112,11 @@ class SortBy(BaseNode):
|
|
|
105
112
|
It also supports the SORTBY parameter for GET requests.
|
|
106
113
|
"""
|
|
107
114
|
|
|
115
|
+
#: The ``<fes:SortProperty>`` elements.
|
|
108
116
|
sort_properties: list[SortProperty]
|
|
109
117
|
|
|
110
118
|
@classmethod
|
|
111
|
-
@expect_children(1)
|
|
119
|
+
@expect_children(1, SortProperty)
|
|
112
120
|
def from_xml(cls, element: NSElement) -> SortBy:
|
|
113
121
|
"""Parse the XML tag."""
|
|
114
122
|
return cls(
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Additional parsing logic for GML values.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Most GML elements are parsed through GeoDjango by using the GEOSGeometry element.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from typing import Union
|
|
9
|
-
|
|
10
8
|
from gisserver.exceptions import ExternalParsingError
|
|
11
9
|
from gisserver.parsers.ast import tag_registry
|
|
12
10
|
from gisserver.parsers.xml import NSElement, parse_xml_from_string
|
|
@@ -14,9 +12,6 @@ from gisserver.parsers.xml import NSElement, parse_xml_from_string
|
|
|
14
12
|
from .base import AbstractGeometry, GM_Envelope, GM_Object, TM_Object
|
|
15
13
|
from .geometries import GEOSGMLGeometry, is_gml_element # also do tag registration
|
|
16
14
|
|
|
17
|
-
# All known root nodes as GML object:
|
|
18
|
-
GmlRootNodes = Union[GM_Object, GM_Envelope, TM_Object]
|
|
19
|
-
|
|
20
15
|
__all__ = [
|
|
21
16
|
"GM_Object",
|
|
22
17
|
"GM_Envelope",
|
|
@@ -29,27 +24,23 @@ __all__ = [
|
|
|
29
24
|
]
|
|
30
25
|
|
|
31
26
|
|
|
32
|
-
def parse_gml(text: str | bytes) ->
|
|
27
|
+
def parse_gml(text: str | bytes) -> GM_Object | GM_Envelope | TM_Object:
|
|
33
28
|
"""Parse an XML <gml:...> string."""
|
|
34
29
|
root_element = parse_xml_from_string(text)
|
|
35
30
|
return parse_gml_node(root_element)
|
|
36
31
|
|
|
37
32
|
|
|
38
|
-
def parse_gml_node(element: NSElement) ->
|
|
39
|
-
"""Parse the element"""
|
|
33
|
+
def parse_gml_node(element: NSElement) -> GM_Object | GM_Envelope | TM_Object:
|
|
34
|
+
"""Parse the GML element."""
|
|
40
35
|
if not is_gml_element(element):
|
|
41
36
|
raise ExternalParsingError(f"Expected GML namespace for {element.tag}")
|
|
42
37
|
|
|
43
38
|
# All known root nodes as GML object:
|
|
44
|
-
return tag_registry.node_from_xml(element, allowed_types=
|
|
39
|
+
return tag_registry.node_from_xml(element, allowed_types=(GM_Object, GM_Envelope, TM_Object))
|
|
45
40
|
|
|
46
41
|
|
|
47
42
|
def find_gml_nodes(element: NSElement) -> list[NSElement]:
|
|
48
|
-
"""Find all gml elements in a node
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if is_gml_element(child):
|
|
53
|
-
result.append(child)
|
|
54
|
-
|
|
55
|
-
return result
|
|
43
|
+
"""Find all ``<gml:...>`` elements in a node.
|
|
44
|
+
This selects all GML elements, including GML 2.1 tags.
|
|
45
|
+
"""
|
|
46
|
+
return [child for child in element if is_gml_element(child)]
|
gisserver/parsers/gml/base.py
CHANGED
|
@@ -6,29 +6,35 @@ map to the GML implementations. These names are referenced by the FES spec.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from gisserver.parsers.ast import
|
|
9
|
+
from gisserver.parsers.ast import AstNode
|
|
10
10
|
from gisserver.parsers.query import CompiledQuery
|
|
11
11
|
|
|
12
|
+
__all__ = (
|
|
13
|
+
"GM_Object",
|
|
14
|
+
"GM_Envelope",
|
|
15
|
+
"TM_Object",
|
|
16
|
+
"AbstractTimeObject",
|
|
17
|
+
"AbstractTimePrimitive",
|
|
18
|
+
"Envelope",
|
|
19
|
+
"AbstractGeometry",
|
|
20
|
+
)
|
|
12
21
|
|
|
13
|
-
class AbstractGeometry(BaseNode):
|
|
14
|
-
"""Abstract base classes for all GML objects, regardless of their version.
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
"""
|
|
23
|
+
class GM_Object(AstNode):
|
|
24
|
+
"""Abstract base classes for all GML objects, regardless of their version."""
|
|
18
25
|
|
|
19
26
|
def build_rhs(self, compiler: CompiledQuery):
|
|
20
|
-
|
|
27
|
+
"""Required function to implement.
|
|
28
|
+
This allows using the value to be used in a binary operator.
|
|
29
|
+
"""
|
|
21
30
|
raise NotImplementedError()
|
|
22
31
|
|
|
23
32
|
|
|
24
|
-
class
|
|
25
|
-
"""Abstract base classes for
|
|
26
|
-
|
|
27
|
-
<gml:Envelope> implements ISO 19107 GM_Envelope (see D.2.3.4 and ISO 19107:2003, 6.4.3).
|
|
28
|
-
"""
|
|
33
|
+
class GM_Envelope(AstNode):
|
|
34
|
+
"""Abstract base classes for the GML envelope, regardless of their version."""
|
|
29
35
|
|
|
30
36
|
|
|
31
|
-
class TM_Object(
|
|
37
|
+
class TM_Object(AstNode):
|
|
32
38
|
"""Abstract base classes for temporal GML objects, regardless of their version.
|
|
33
39
|
|
|
34
40
|
See ISO 19108 TM_Object (see D.2.5.2 and ISO 19108:2002, 5.2.2)
|
|
@@ -36,5 +42,17 @@ class TM_Object(BaseNode):
|
|
|
36
42
|
|
|
37
43
|
|
|
38
44
|
# Instead of polluting the MRO with unneeded base classes, create aliases:
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
|
|
46
|
+
#: The ``<gml:AbstractGeometry>`` definition implements the ISO 19107 GM_Object.
|
|
47
|
+
AbstractGeometry = GM_Object
|
|
48
|
+
|
|
49
|
+
#: The ``<gml:Envelope>`` implements ISO 19107 GM_Envelope (see D.2.3.4 and ISO 19107:2003, 6.4.3).
|
|
50
|
+
Envelope = GM_Envelope
|
|
51
|
+
|
|
52
|
+
# See https://www.mediamaps.ch/ogc/schemas-xsdoc/sld/1.1.0/temporal_xsd.html
|
|
53
|
+
|
|
54
|
+
#: The base class for all time objects
|
|
55
|
+
AbstractTimeObject = TM_Object
|
|
56
|
+
|
|
57
|
+
#: The base classes for time primitives.
|
|
58
|
+
AbstractTimePrimitive = AbstractTimeObject
|
|
@@ -6,15 +6,16 @@ Overview of GML 3.2 changes: https://mapserver.org/el/development/rfc/ms-rfc-105
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from xml.etree.ElementTree import tostring
|
|
8
8
|
|
|
9
|
+
from django.contrib.gis.gdal import AxisOrder
|
|
9
10
|
from django.contrib.gis.geos import GEOSGeometry, Polygon
|
|
10
11
|
|
|
12
|
+
from gisserver.crs import CRS
|
|
11
13
|
from gisserver.exceptions import ExternalParsingError
|
|
12
|
-
from gisserver.geometries import CRS
|
|
13
14
|
from gisserver.parsers.ast import tag_registry
|
|
14
15
|
from gisserver.parsers.query import CompiledQuery
|
|
15
16
|
from gisserver.parsers.xml import NSElement, xmlns
|
|
16
17
|
|
|
17
|
-
from .base import AbstractGeometry,
|
|
18
|
+
from .base import AbstractGeometry, AbstractTimePrimitive
|
|
18
19
|
|
|
19
20
|
_ANY_GML_NS = "{http://www.opengis.net/gml/"
|
|
20
21
|
|
|
@@ -71,6 +72,7 @@ class GEOSGMLGeometry(AbstractGeometry):
|
|
|
71
72
|
crs = None # will be resolved
|
|
72
73
|
|
|
73
74
|
# Wrap in an element that the filter can use.
|
|
75
|
+
CRS.tag_geometry(polygon, axis_order=AxisOrder.AUTHORITY)
|
|
74
76
|
return cls(srs=crs, geos_data=polygon)
|
|
75
77
|
|
|
76
78
|
@classmethod
|
|
@@ -87,11 +89,12 @@ class GEOSGMLGeometry(AbstractGeometry):
|
|
|
87
89
|
# This avoids having to support the whole GEOS logic.
|
|
88
90
|
geos_data = GEOSGeometry.from_gml(tostring(element))
|
|
89
91
|
geos_data.srid = srs.srid
|
|
92
|
+
CRS.tag_geometry(geos_data, axis_order=AxisOrder.AUTHORITY)
|
|
90
93
|
return cls(srs=srs, geos_data=geos_data)
|
|
91
94
|
|
|
92
95
|
def __repr__(self):
|
|
93
96
|
# Better rendering for unit test debugging
|
|
94
|
-
return f"
|
|
97
|
+
return f"{self.__class__.__name__}(srs={self.srs!r}, geos_data=GEOSGeometry({self.geos_data.wkt!r}))"
|
|
95
98
|
|
|
96
99
|
@property
|
|
97
100
|
def wkt(self) -> str:
|
|
@@ -112,23 +115,47 @@ class GEOSGMLGeometry(AbstractGeometry):
|
|
|
112
115
|
elif compiler.feature_types: # for unit tests
|
|
113
116
|
self.srs = compiler.feature_types[0].resolve_crs(self.srs, locator="bbox")
|
|
114
117
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
@tag_registry.register("
|
|
124
|
-
@tag_registry.register("
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
118
|
+
# Make sure the data is suitable for processing by the ORM.
|
|
119
|
+
# The database needs the geometry in traditional (x/y) ordering.
|
|
120
|
+
if self.srs.is_north_east_order:
|
|
121
|
+
return self.srs.apply_to(self.geos_data, clone=True, axis_order=AxisOrder.TRADITIONAL)
|
|
122
|
+
else:
|
|
123
|
+
return self.geos_data
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@tag_registry.register("TimeInstant", hidden=True)
|
|
127
|
+
@tag_registry.register("TimePeriod", hidden=True)
|
|
128
|
+
class AbstractTimeGeometricPrimitive(AbstractTimePrimitive):
|
|
129
|
+
"""Not implemented: the whole GML temporal logic.
|
|
130
|
+
|
|
131
|
+
Examples for GML time elements include::
|
|
132
|
+
|
|
133
|
+
<gml:TimeInstant gml:id="TI1">
|
|
134
|
+
<gml:timePosition>2005-05-19T09:28:40Z</gml:timePosition>
|
|
135
|
+
</gml:TimeInstant>
|
|
136
|
+
|
|
137
|
+
and::
|
|
138
|
+
|
|
139
|
+
<gml:TimePeriod gml:id="TP1">
|
|
140
|
+
<gml:begin>
|
|
141
|
+
<gml:TimeInstant gml:id="TI1">
|
|
142
|
+
<gml:timePosition>2005-05-17T00:00:00Z</gml:timePosition>
|
|
143
|
+
</gml:TimeInstant>
|
|
144
|
+
</gml:begin>
|
|
145
|
+
<gml:end>
|
|
146
|
+
<gml:TimeInstant gml:id="TI2">
|
|
147
|
+
<gml:timePosition>2005-05-23T00:00:00Z</gml:timePosition>
|
|
148
|
+
</gml:TimeInstant>
|
|
149
|
+
</gml:end>
|
|
150
|
+
</gml:TimePeriod>
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
xml_ns = xmlns.gml32
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@tag_registry.register("TimeNode", hidden=True)
|
|
157
|
+
@tag_registry.register("TimeEdge", hidden=True)
|
|
158
|
+
class AbstractTimeTopologyPrimitiveType(AbstractTimePrimitive):
|
|
159
|
+
"""Not implemented: GML temporal logic for TimeNode/TimeEdge."""
|
|
133
160
|
|
|
134
161
|
xml_ns = xmlns.gml32
|