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.
Files changed (55) hide show
  1. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +26 -10
  2. django_gisserver-2.1.dist-info/RECORD +68 -0
  3. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/crs.py +401 -0
  6. gisserver/db.py +71 -5
  7. gisserver/exceptions.py +106 -2
  8. gisserver/extensions/functions.py +122 -28
  9. gisserver/extensions/queries.py +15 -10
  10. gisserver/features.py +44 -36
  11. gisserver/geometries.py +64 -306
  12. gisserver/management/commands/loadgeojson.py +41 -21
  13. gisserver/operations/base.py +11 -7
  14. gisserver/operations/wfs20.py +31 -93
  15. gisserver/output/__init__.py +6 -2
  16. gisserver/output/base.py +28 -13
  17. gisserver/output/csv.py +18 -6
  18. gisserver/output/geojson.py +7 -6
  19. gisserver/output/gml32.py +43 -23
  20. gisserver/output/results.py +25 -39
  21. gisserver/output/utils.py +9 -2
  22. gisserver/parsers/ast.py +171 -65
  23. gisserver/parsers/fes20/__init__.py +76 -4
  24. gisserver/parsers/fes20/expressions.py +97 -27
  25. gisserver/parsers/fes20/filters.py +9 -6
  26. gisserver/parsers/fes20/identifiers.py +27 -7
  27. gisserver/parsers/fes20/lookups.py +8 -6
  28. gisserver/parsers/fes20/operators.py +101 -49
  29. gisserver/parsers/fes20/sorting.py +14 -6
  30. gisserver/parsers/gml/__init__.py +10 -19
  31. gisserver/parsers/gml/base.py +32 -14
  32. gisserver/parsers/gml/geometries.py +48 -21
  33. gisserver/parsers/ows/kvp.py +10 -2
  34. gisserver/parsers/ows/requests.py +6 -4
  35. gisserver/parsers/query.py +6 -2
  36. gisserver/parsers/values.py +61 -4
  37. gisserver/parsers/wfs20/__init__.py +2 -0
  38. gisserver/parsers/wfs20/adhoc.py +25 -17
  39. gisserver/parsers/wfs20/base.py +12 -7
  40. gisserver/parsers/wfs20/projection.py +3 -3
  41. gisserver/parsers/wfs20/requests.py +1 -0
  42. gisserver/parsers/wfs20/stored.py +3 -2
  43. gisserver/parsers/xml.py +12 -0
  44. gisserver/projection.py +17 -7
  45. gisserver/static/gisserver/index.css +8 -3
  46. gisserver/templates/gisserver/base.html +12 -0
  47. gisserver/templates/gisserver/index.html +9 -15
  48. gisserver/templates/gisserver/service_description.html +12 -6
  49. gisserver/templates/gisserver/wfs/feature_field.html +1 -1
  50. gisserver/templates/gisserver/wfs/feature_type.html +35 -13
  51. gisserver/types.py +150 -81
  52. gisserver/views.py +47 -24
  53. django_gisserver-2.0.dist-info/RECORD +0 -66
  54. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
  55. {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, Protocol, Union
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
- BaseNode,
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
- class Measure(BaseNode):
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(BaseNode):
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 <fes:ResourceId> elements as children.
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
- if compiler.feature_types:
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
- lhs = lhs.build_lhs(compiler)
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
- comparison = Q(**{f"{lhs}__{lookup}": rhs})
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" raise an error.
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
- if compiler.feature_types:
351
- self.validate_comparison(compiler, lhs, lookup, rhs[0])
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, 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, "LowerBoundary", "UpperBoundary")
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, 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.xml_tags[0]} operator."
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 <tns:p xsi:nil='true'>, this returns true.
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 <tns:p>, this returns true.
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, 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 BaseNode, expect_children, expect_tag, tag_registry
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(BaseNode):
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, "SortOrder")
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(BaseNode):
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
- """Generic support for various GML versions.
1
+ """Additional parsing logic for GML values.
2
2
 
3
- These functions locate GML objects, and redirect to the proper parser.
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) -> GmlRootNodes:
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) -> GmlRootNodes:
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=GmlRootNodes.__args__)
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
- result = []
50
- for child in element:
51
- # This selects all GML elements, including 2.1
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)]
@@ -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 BaseNode
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
- <gml:AbstractGeometry> implements the ISO 19107 GM_Object.
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
- # Allow the value to be used in a binary operator
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 Envelope(BaseNode):
25
- """Abstract base classes for all GML objects, regardless of their version.
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(BaseNode):
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
- GM_Object = AbstractGeometry
40
- GM_Envelope = Envelope
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, TM_Object
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"GMLGEOSGeometry(srs={self.srs!r}, geos_data=GEOSGeometry({self.geos_data.wkt!r}))"
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
- return self.geos_data
116
-
117
-
118
- @tag_registry.register("After")
119
- @tag_registry.register("Before")
120
- @tag_registry.register("Begins")
121
- @tag_registry.register("BegunBy")
122
- @tag_registry.register("TContains")
123
- @tag_registry.register("TEquals")
124
- @tag_registry.register("TOverlaps")
125
- @tag_registry.register("During")
126
- @tag_registry.register("Meets")
127
- @tag_registry.register("OverlappedBy")
128
- @tag_registry.register("MetBy")
129
- @tag_registry.register("EndedBy")
130
- @tag_registry.register("AnyInteracts")
131
- class TM_GeometricPrimitive(TM_Object):
132
- """Not implemented: the whole GML temporal logic"""
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