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
|
@@ -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,18 @@ 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
|
+
|
|
93
|
+
# Using the WFS 2 format (urn:ogc:def:crs:EPSG::4326"), coordinates should be latitude/longitude.
|
|
94
|
+
# However, when providing legacy formats like srsName="EPSG:4326",
|
|
95
|
+
# input is assumed to be in legacy longitude/latitude axis ordering too.
|
|
96
|
+
# This reflects what GeoServer does: https://docs.geoserver.org/main/en/user/services/wfs/axis_order.html
|
|
97
|
+
if not srs.force_xy:
|
|
98
|
+
CRS.tag_geometry(geos_data, axis_order=AxisOrder.AUTHORITY)
|
|
90
99
|
return cls(srs=srs, geos_data=geos_data)
|
|
91
100
|
|
|
92
101
|
def __repr__(self):
|
|
93
102
|
# Better rendering for unit test debugging
|
|
94
|
-
return f"
|
|
103
|
+
return f"{self.__class__.__name__}(srs={self.srs!r}, geos_data=GEOSGeometry({self.geos_data.wkt!r}))"
|
|
95
104
|
|
|
96
105
|
@property
|
|
97
106
|
def wkt(self) -> str:
|
|
@@ -112,23 +121,47 @@ class GEOSGMLGeometry(AbstractGeometry):
|
|
|
112
121
|
elif compiler.feature_types: # for unit tests
|
|
113
122
|
self.srs = compiler.feature_types[0].resolve_crs(self.srs, locator="bbox")
|
|
114
123
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
@tag_registry.register("
|
|
124
|
-
@tag_registry.register("
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
124
|
+
# Make sure the data is suitable for processing by the ORM.
|
|
125
|
+
# The database needs the geometry in traditional (x/y) ordering.
|
|
126
|
+
if self.srs.is_north_east_order:
|
|
127
|
+
return self.srs.apply_to(self.geos_data, clone=True, axis_order=AxisOrder.TRADITIONAL)
|
|
128
|
+
else:
|
|
129
|
+
return self.geos_data
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@tag_registry.register("TimeInstant", hidden=True)
|
|
133
|
+
@tag_registry.register("TimePeriod", hidden=True)
|
|
134
|
+
class AbstractTimeGeometricPrimitive(AbstractTimePrimitive):
|
|
135
|
+
"""Not implemented: the whole GML temporal logic.
|
|
136
|
+
|
|
137
|
+
Examples for GML time elements include::
|
|
138
|
+
|
|
139
|
+
<gml:TimeInstant gml:id="TI1">
|
|
140
|
+
<gml:timePosition>2005-05-19T09:28:40Z</gml:timePosition>
|
|
141
|
+
</gml:TimeInstant>
|
|
142
|
+
|
|
143
|
+
and::
|
|
144
|
+
|
|
145
|
+
<gml:TimePeriod gml:id="TP1">
|
|
146
|
+
<gml:begin>
|
|
147
|
+
<gml:TimeInstant gml:id="TI1">
|
|
148
|
+
<gml:timePosition>2005-05-17T00:00:00Z</gml:timePosition>
|
|
149
|
+
</gml:TimeInstant>
|
|
150
|
+
</gml:begin>
|
|
151
|
+
<gml:end>
|
|
152
|
+
<gml:TimeInstant gml:id="TI2">
|
|
153
|
+
<gml:timePosition>2005-05-23T00:00:00Z</gml:timePosition>
|
|
154
|
+
</gml:TimeInstant>
|
|
155
|
+
</gml:end>
|
|
156
|
+
</gml:TimePeriod>
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
xml_ns = xmlns.gml32
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@tag_registry.register("TimeNode", hidden=True)
|
|
163
|
+
@tag_registry.register("TimeEdge", hidden=True)
|
|
164
|
+
class AbstractTimeTopologyPrimitiveType(AbstractTimePrimitive):
|
|
165
|
+
"""Not implemented: GML temporal logic for TimeNode/TimeEdge."""
|
|
133
166
|
|
|
134
167
|
xml_ns = xmlns.gml32
|
gisserver/parsers/ows/kvp.py
CHANGED
|
@@ -105,7 +105,9 @@ class KVPRequest:
|
|
|
105
105
|
def split_parameter_lists(self) -> list[KVPRequest]:
|
|
106
106
|
"""Split the parameter lists into individual requests.
|
|
107
107
|
|
|
108
|
-
This translates a request such as
|
|
108
|
+
This translates a request such as:
|
|
109
|
+
|
|
110
|
+
.. code-block:: urlencoded
|
|
109
111
|
|
|
110
112
|
TYPENAMES=(ns1:F1,ns2:F2)(ns1:F1,ns1:F1)
|
|
111
113
|
&ALIASES=(A,B)(C,D)
|
|
@@ -113,15 +115,21 @@ class KVPRequest:
|
|
|
113
115
|
|
|
114
116
|
into separate pairs:
|
|
115
117
|
|
|
118
|
+
.. code-block:: urlencoded
|
|
119
|
+
|
|
116
120
|
TYPENAMES=ns1:F1,ns2:F2&ALIASES=A,B&FILTER=<Filter>…for A,B…</Filter>
|
|
117
121
|
TYPENAMES=ns1:F1,ns1:F1&ALIASES=C,D&FILTER=<Filter>…for C,D…</Filter>
|
|
118
122
|
|
|
119
123
|
It's both possible have some query parameters split and some shared.
|
|
120
124
|
For example to have two different bounding boxes:
|
|
121
125
|
|
|
126
|
+
.. code-block:: urlencoded
|
|
127
|
+
|
|
122
128
|
TYPENAMES=(INWATER_1M)(BuiltUpA_1M)&BBOX=(40.9821,...)(40.5874,...)
|
|
123
129
|
|
|
124
|
-
or have a single bounding box for both queries
|
|
130
|
+
or have a single bounding box for both queries:
|
|
131
|
+
|
|
132
|
+
.. code-block:: urlencoded
|
|
125
133
|
|
|
126
134
|
TYPENAMES=(INWATER_1M)(BuiltUpA_1M)&BBOX=40.9821,23.4948,41.0257,23.5525
|
|
127
135
|
"""
|
|
@@ -13,7 +13,7 @@ from gisserver.exceptions import (
|
|
|
13
13
|
OperationNotSupported,
|
|
14
14
|
OperationParsingFailed,
|
|
15
15
|
)
|
|
16
|
-
from gisserver.parsers.ast import
|
|
16
|
+
from gisserver.parsers.ast import AstNode, tag_registry
|
|
17
17
|
from gisserver.parsers.xml import NSElement, parse_xml_from_string, split_ns, xmlns
|
|
18
18
|
|
|
19
19
|
from .kvp import KVPRequest
|
|
@@ -28,7 +28,7 @@ __all__ = (
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
@dataclass
|
|
31
|
-
class BaseOwsRequest(
|
|
31
|
+
class BaseOwsRequest(AstNode):
|
|
32
32
|
"""Base request data for all request types of the OWS standards.
|
|
33
33
|
This mirrors the ``<wfs:BaseRequestType>`` element from the WFS spec.
|
|
34
34
|
"""
|
|
@@ -80,7 +80,7 @@ class BaseOwsRequest(BaseNode):
|
|
|
80
80
|
return {
|
|
81
81
|
"SERVICE": self.service,
|
|
82
82
|
"VERSION": str(self.version),
|
|
83
|
-
"REQUEST": split_ns(self.
|
|
83
|
+
"REQUEST": split_ns(self.xml_name)[1],
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
|
|
@@ -118,7 +118,9 @@ def resolve_kvp_parser_class(kvp: KVPRequest) -> type[BaseOwsRequest]:
|
|
|
118
118
|
try:
|
|
119
119
|
request_cls = request_classes[request.upper()]
|
|
120
120
|
except KeyError:
|
|
121
|
-
allowed = ", ".join(
|
|
121
|
+
allowed = ", ".join(
|
|
122
|
+
xml_tag for node in request_classes.values() for xml_tag in node.get_tag_names()
|
|
123
|
+
)
|
|
122
124
|
raise OperationNotSupported(
|
|
123
125
|
f"'{request}' is not implemented, supported are: {allowed}.",
|
|
124
126
|
locator="request",
|
gisserver/parsers/query.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import logging
|
|
6
6
|
import operator
|
|
7
7
|
from datetime import date, datetime
|
|
8
|
+
from decimal import Decimal as D
|
|
8
9
|
from functools import reduce
|
|
9
10
|
from typing import Union
|
|
10
11
|
|
|
@@ -16,14 +17,15 @@ from django.db.models.expressions import Combinable, Func
|
|
|
16
17
|
from gisserver.features import FeatureType
|
|
17
18
|
|
|
18
19
|
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
+
ScalarTypes = (bool, int, str, D, date, datetime) # Not Union for Python 3.9
|
|
21
|
+
RhsTypes = Union[Combinable, Func, Q, GEOSGeometry, bool, int, D, str, date, datetime, tuple]
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class CompiledQuery:
|
|
23
25
|
"""Intermediate data for translating FES queries to Django.
|
|
24
26
|
|
|
25
27
|
This class effectively contains all data from the ``<fes:Filter>`` object,
|
|
26
|
-
but using a format that can be translated to a
|
|
28
|
+
but using a format that can be translated to a Django QuerySet.
|
|
27
29
|
|
|
28
30
|
As the Abstract Syntax Tree of a FES-filter creates the ORM query,
|
|
29
31
|
it fills this object with all intermediate bits. This allows building
|
|
@@ -64,6 +66,7 @@ class CompiledQuery:
|
|
|
64
66
|
return name
|
|
65
67
|
|
|
66
68
|
def add_distinct(self):
|
|
69
|
+
"""Enforce "SELECT DISTINCT" on the query, used when joining 1-N or N-M relationships."""
|
|
67
70
|
self.distinct = True
|
|
68
71
|
|
|
69
72
|
def add_lookups(self, q_object: Q, type_name: str | None = None):
|
|
@@ -87,6 +90,7 @@ class CompiledQuery:
|
|
|
87
90
|
"""
|
|
88
91
|
if not isinstance(q_object, Q):
|
|
89
92
|
raise TypeError()
|
|
93
|
+
# Note the ORM also provides a FilteredRelation() option, that is not explored yet here.
|
|
90
94
|
self.extra_lookups.append(q_object)
|
|
91
95
|
|
|
92
96
|
def add_ordering(self, ordering: list[str]):
|
gisserver/parsers/values.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
"""Parsing of scalar values in the request."""
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
import re
|
|
3
|
-
from datetime import datetime
|
|
5
|
+
from datetime import date, datetime, time
|
|
4
6
|
from decimal import Decimal as D
|
|
5
7
|
|
|
6
|
-
from django.utils.dateparse import parse_datetime
|
|
8
|
+
from django.utils.dateparse import parse_date, parse_datetime, parse_duration, parse_time
|
|
7
9
|
|
|
8
10
|
from gisserver.exceptions import ExternalParsingError
|
|
9
11
|
|
|
@@ -12,7 +14,15 @@ RE_FLOAT = re.compile(r"\A[0-9]+(\.[0-9]+)\Z")
|
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def auto_cast(value: str):
|
|
15
|
-
"""Automatically cast a value to a scalar.
|
|
17
|
+
"""Automatically cast a value to a scalar.
|
|
18
|
+
|
|
19
|
+
This recognizes integers, floats and ISO datetimes.
|
|
20
|
+
Booleans are not handled, as that leads to unpredictable behavior
|
|
21
|
+
(as seen in Yaml for example).
|
|
22
|
+
"""
|
|
23
|
+
if not isinstance(value, str):
|
|
24
|
+
return value
|
|
25
|
+
|
|
16
26
|
if value.isdigit():
|
|
17
27
|
return int(value)
|
|
18
28
|
elif RE_FLOAT.match(value):
|
|
@@ -26,14 +36,61 @@ def auto_cast(value: str):
|
|
|
26
36
|
return value
|
|
27
37
|
|
|
28
38
|
|
|
39
|
+
def parse_iso_date(raw_value: str) -> date:
|
|
40
|
+
"""Translate ISO date into a Python date value."""
|
|
41
|
+
try:
|
|
42
|
+
value = parse_date(raw_value)
|
|
43
|
+
except ValueError as e:
|
|
44
|
+
raise ExternalParsingError(str(e)) from e
|
|
45
|
+
|
|
46
|
+
if value is None:
|
|
47
|
+
raise ExternalParsingError("Date must be in YYYY-MM-DD format.")
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
|
|
29
51
|
def parse_iso_datetime(raw_value: str) -> datetime:
|
|
30
|
-
|
|
52
|
+
"""Translate ISO datetimes into a Python datetime value."""
|
|
53
|
+
try:
|
|
54
|
+
value = parse_datetime(raw_value)
|
|
55
|
+
except ValueError as e:
|
|
56
|
+
raise ExternalParsingError(str(e)) from e
|
|
57
|
+
|
|
31
58
|
if value is None:
|
|
32
59
|
raise ExternalParsingError("Date must be in YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format.")
|
|
33
60
|
return value
|
|
34
61
|
|
|
35
62
|
|
|
63
|
+
def parse_iso_time(raw_value: str) -> time:
|
|
64
|
+
"""Translate ISO times into a Python time value."""
|
|
65
|
+
try:
|
|
66
|
+
value = parse_time(raw_value)
|
|
67
|
+
except ValueError as e:
|
|
68
|
+
raise ExternalParsingError(str(e)) from e
|
|
69
|
+
|
|
70
|
+
if value is None:
|
|
71
|
+
raise ExternalParsingError("Time must be in HH:MM[:ss[.uuuuuu]][TZ] format.")
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_iso_duration(raw_value: str) -> time:
|
|
76
|
+
"""Translate ISO times into a Python time value."""
|
|
77
|
+
# The parse_duration() supports multiple formats, including ISO8601.
|
|
78
|
+
# Limit it to only one format.
|
|
79
|
+
if not raw_value.startswith(("P", "-P")):
|
|
80
|
+
raise ExternalParsingError("Duration must be in ISO8601 format (e.g. PT1H).")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
value = parse_duration(raw_value)
|
|
84
|
+
except ValueError as e:
|
|
85
|
+
raise ExternalParsingError(str(e)) from e
|
|
86
|
+
|
|
87
|
+
if value is None:
|
|
88
|
+
raise ExternalParsingError("Duration must be in ISO8601 format (e.g. PT1H).")
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
|
|
36
92
|
def parse_bool(raw_value: str):
|
|
93
|
+
"""Translate XML notations of true/1 and false/0 into a boolean."""
|
|
37
94
|
if raw_value in ("true", "1"):
|
|
38
95
|
return True
|
|
39
96
|
elif raw_value in ("false", "0"):
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""WFS 2.0 element parsing.
|
|
2
2
|
|
|
3
|
+
These classes parse the XML request body.
|
|
4
|
+
|
|
3
5
|
The full spec can be found at: https://www.ogc.org/publications/standard/wfs/.
|
|
4
6
|
Secondly, using https://www.mediamaps.ch/ogc/schemas-xsdoc/sld/1.2/wfs_xsd.html can be very
|
|
5
7
|
helpful to see which options each object type should support.
|
gisserver/parsers/wfs20/adhoc.py
CHANGED
|
@@ -14,12 +14,12 @@ from functools import cached_property
|
|
|
14
14
|
|
|
15
15
|
from django.db.models import Q
|
|
16
16
|
|
|
17
|
+
from gisserver.crs import CRS
|
|
17
18
|
from gisserver.exceptions import (
|
|
18
19
|
InvalidParameterValue,
|
|
19
20
|
MissingParameterValue,
|
|
20
21
|
OperationNotSupported,
|
|
21
22
|
)
|
|
22
|
-
from gisserver.geometries import CRS
|
|
23
23
|
from gisserver.parsers import fes20
|
|
24
24
|
from gisserver.parsers.ast import tag_registry
|
|
25
25
|
from gisserver.parsers.ows import KVPRequest
|
|
@@ -48,7 +48,9 @@ class AdhocQuery(QueryExpression):
|
|
|
48
48
|
<fes:SortBy>...</fes:SortBy>
|
|
49
49
|
</wfs:Query>
|
|
50
50
|
|
|
51
|
-
And supports the KVP syntax
|
|
51
|
+
And supports the KVP syntax:
|
|
52
|
+
|
|
53
|
+
.. code-block:: urlencoded
|
|
52
54
|
|
|
53
55
|
?SERVICE=WFS&...&TYPENAMES=ns:myType&FILTER=...&SORTBY=...&SRSNAME=...&PROPERTYNAME=...
|
|
54
56
|
?SERVICE=WFS&...&TYPENAMES=ns:myType&BBOX=...&SORTBY=...&SRSNAME=...&PROPERTYNAME=...
|
|
@@ -59,7 +61,7 @@ class AdhocQuery(QueryExpression):
|
|
|
59
61
|
|
|
60
62
|
The WFS Spec has 3 class levels for this:
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
- AdhocQueryExpression (types, projection, selection, sorting)
|
|
63
65
|
- Query (adds srsName, featureVersion)
|
|
64
66
|
- StoredQuery (adds storedQueryID)
|
|
65
67
|
|
|
@@ -71,25 +73,31 @@ class AdhocQuery(QueryExpression):
|
|
|
71
73
|
https://www.mediamaps.ch/ogc/schemas-xsdoc/sld/1.2/query_xsd.html#AbstractAdhocQueryExpressionType
|
|
72
74
|
"""
|
|
73
75
|
|
|
74
|
-
# Tag attributes (fes:AbstractAdhocQueryExpression)
|
|
76
|
+
# Tag attributes (implements ``fes:AbstractAdhocQueryExpression``)
|
|
75
77
|
# WFS allows multiple names to construct JOIN queries.
|
|
76
78
|
# See https://docs.ogc.org/is/09-025r2/09-025r2.html#107
|
|
77
79
|
# and https://docs.ogc.org/is/09-025r2/09-025r2.html#190
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
|
|
81
|
+
#: The 'typeNames' value if the request provided them. use :meth:`get_type_names` instead.
|
|
82
|
+
typeNames: list[str]
|
|
83
|
+
#: Aliases for typeNames are used for joining the same table twice. (JOIN statements are not supported yet).
|
|
84
|
+
aliases: list[str] | None = None
|
|
85
|
+
#: For XML POST requests, this handle value is returned in the ``<ows:Exception>``.
|
|
86
|
+
handle: str = ""
|
|
87
|
+
|
|
88
|
+
# part of the <wfs:Query> tag attributes:
|
|
89
|
+
#: The Coordinate Reference System to render the tag in
|
|
82
90
|
srsName: CRS | None = None
|
|
83
91
|
|
|
84
|
-
|
|
92
|
+
#: Projection clause (implements ``fes:AbstractProjectionClause``)
|
|
85
93
|
property_names: list[PropertyName] | None = None
|
|
86
94
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
#: Selection clause (implements ``fes:AbstractSelectionClause``).
|
|
96
|
+
#: - for XML POST this is encoded in a <fes:Filter> tag.
|
|
97
|
+
#: - for HTTP GET, this is encoded as FILTER, FILTER_LANGUAGE, RESOURCEID, BBOX.
|
|
90
98
|
filter: fes20.Filter | None = None
|
|
91
99
|
|
|
92
|
-
|
|
100
|
+
#: Sorting Clause (implements ``fes:AbstractSortingClause``)
|
|
93
101
|
sortBy: fes20.SortBy | None = None
|
|
94
102
|
|
|
95
103
|
def __post_init__(self):
|
|
@@ -188,7 +196,9 @@ class AdhocQuery(QueryExpression):
|
|
|
188
196
|
return "filter"
|
|
189
197
|
|
|
190
198
|
def get_type_names(self) -> list[str]:
|
|
191
|
-
"""Tell which type names this query uses.
|
|
199
|
+
"""Tell which type names this query uses.
|
|
200
|
+
Multiple values means a JOIN is made (not supported yet).
|
|
201
|
+
"""
|
|
192
202
|
if not self.typeNames and self.filter is not None:
|
|
193
203
|
# Also make the behavior consistent, always supply the type name.
|
|
194
204
|
return self.filter.get_resource_id_types() or []
|
|
@@ -196,7 +206,7 @@ class AdhocQuery(QueryExpression):
|
|
|
196
206
|
return self.typeNames
|
|
197
207
|
|
|
198
208
|
def get_projection(self) -> FeatureProjection:
|
|
199
|
-
"""Tell how the
|
|
209
|
+
"""Tell how the ``<wfs:Query>`` element should be displayed."""
|
|
200
210
|
return FeatureProjection(
|
|
201
211
|
self.feature_types,
|
|
202
212
|
self.property_names,
|
|
@@ -205,7 +215,7 @@ class AdhocQuery(QueryExpression):
|
|
|
205
215
|
)
|
|
206
216
|
|
|
207
217
|
def bind(self, *args, **kwargs):
|
|
208
|
-
"""
|
|
218
|
+
"""Override to make sure the 'locator' points to the actual object that defined the type."""
|
|
209
219
|
try:
|
|
210
220
|
super().bind(*args, **kwargs)
|
|
211
221
|
except InvalidParameterValue as e:
|
|
@@ -219,7 +229,7 @@ class AdhocQuery(QueryExpression):
|
|
|
219
229
|
|
|
220
230
|
def build_query(self, compiler: CompiledQuery) -> Q | None:
|
|
221
231
|
"""Apply our collected filter data to the compiler."""
|
|
222
|
-
# Add the
|
|
232
|
+
# Add the sorting
|
|
223
233
|
if self.sortBy is not None:
|
|
224
234
|
self.sortBy.build_ordering(compiler)
|
|
225
235
|
|
|
@@ -230,7 +240,7 @@ class AdhocQuery(QueryExpression):
|
|
|
230
240
|
else:
|
|
231
241
|
return None
|
|
232
242
|
|
|
233
|
-
def as_kvp(self):
|
|
243
|
+
def as_kvp(self) -> dict:
|
|
234
244
|
"""Translate the POST request into KVP GET parameters. This is needed for pagination."""
|
|
235
245
|
params = super().as_kvp()
|
|
236
246
|
params["TYPENAMES"] = ",".join(self.typeNames)
|
gisserver/parsers/wfs20/base.py
CHANGED
|
@@ -23,12 +23,12 @@ from gisserver.exceptions import (
|
|
|
23
23
|
)
|
|
24
24
|
from gisserver.features import FeatureType
|
|
25
25
|
from gisserver.parsers import fes20, wfs20
|
|
26
|
-
from gisserver.parsers.ast import
|
|
26
|
+
from gisserver.parsers.ast import AstNode
|
|
27
27
|
from gisserver.parsers.query import CompiledQuery
|
|
28
28
|
from gisserver.projection import FeatureProjection
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
class QueryExpression(
|
|
31
|
+
class QueryExpression(AstNode):
|
|
32
32
|
"""WFS base class for all queries.
|
|
33
33
|
This object type is defined in the WFS spec (as ``<fes:AbstractQueryExpression>``).
|
|
34
34
|
|
|
@@ -47,16 +47,20 @@ class QueryExpression(BaseNode):
|
|
|
47
47
|
* :meth:`get_queryset` defines the full results.
|
|
48
48
|
"""
|
|
49
49
|
|
|
50
|
+
#: Configuration for the 'locator' argument in exceptions
|
|
50
51
|
query_locator: ClassVar[str] = None
|
|
51
52
|
|
|
52
53
|
# QueryExpression
|
|
54
|
+
#: The 'handle' that will be returned in exceptions.
|
|
53
55
|
handle: str = ""
|
|
54
56
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
#: Projection parameters (overwritten by subclasses)
|
|
58
|
+
#: In the WFS spec, this is only part of the operation/presentation.
|
|
59
|
+
#: For Django, we'd like to make this part of the query too.
|
|
58
60
|
property_names: list[wfs20.PropertyName] | None = None # PropertyName
|
|
59
|
-
|
|
61
|
+
|
|
62
|
+
#: The valueReference for the GetPropertyValue call, provided here for extra ORM filtering.
|
|
63
|
+
value_reference: fes20.ValueReference | None = None
|
|
60
64
|
|
|
61
65
|
def bind(
|
|
62
66
|
self,
|
|
@@ -120,6 +124,7 @@ class QueryExpression(BaseNode):
|
|
|
120
124
|
|
|
121
125
|
def get_type_names(self) -> list[str]:
|
|
122
126
|
"""Tell which type names this query applies to.
|
|
127
|
+
Multiple values means a JOIN is made (not supported yet).
|
|
123
128
|
|
|
124
129
|
This method needs to be defined in subclasses.
|
|
125
130
|
"""
|
|
@@ -135,7 +140,7 @@ class QueryExpression(BaseNode):
|
|
|
135
140
|
"""Define the compiled query that filters the queryset."""
|
|
136
141
|
raise NotImplementedError()
|
|
137
142
|
|
|
138
|
-
def as_kvp(self):
|
|
143
|
+
def as_kvp(self) -> dict:
|
|
139
144
|
"""Translate the POST request into KVP GET parameters. This is needed for pagination."""
|
|
140
145
|
params = {}
|
|
141
146
|
if self.property_names:
|
|
@@ -10,14 +10,14 @@ from gisserver.exceptions import (
|
|
|
10
10
|
OperationParsingFailed,
|
|
11
11
|
OperationProcessingFailed,
|
|
12
12
|
)
|
|
13
|
-
from gisserver.parsers.ast import
|
|
13
|
+
from gisserver.parsers.ast import AstNode, tag_registry
|
|
14
14
|
from gisserver.parsers.query import CompiledQuery
|
|
15
15
|
from gisserver.parsers.xml import NSElement, xmlns
|
|
16
16
|
from gisserver.types import XPathMatch
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class ResolveValue(Enum):
|
|
20
|
-
"""The wfs:ResolveValueType enum, used by :class:`StandardResolveParameters`."""
|
|
20
|
+
"""The ``wfs:ResolveValueType`` enum, used by :class:`StandardResolveParameters`."""
|
|
21
21
|
|
|
22
22
|
local = "local"
|
|
23
23
|
remote = "remote"
|
|
@@ -31,7 +31,7 @@ class ResolveValue(Enum):
|
|
|
31
31
|
|
|
32
32
|
@dataclass
|
|
33
33
|
@tag_registry.register("PropertyName", xmlns.wfs)
|
|
34
|
-
class PropertyName(
|
|
34
|
+
class PropertyName(AstNode):
|
|
35
35
|
"""The ``<wfs:PropertyName>`` element in the projection clause.
|
|
36
36
|
|
|
37
37
|
This parses and handles the syntax::
|
|
@@ -45,6 +45,7 @@ WFS_STORED_QUERY = xmlns.wfs20.qname("StoredQuery")
|
|
|
45
45
|
|
|
46
46
|
@dataclass
|
|
47
47
|
@tag_registry.register("GetCapabilities", xmlns.wfs20)
|
|
48
|
+
@tag_registry.register("GetCapabilities", xmlns.wfs1, hidden=True) # to give negotiation error.
|
|
48
49
|
class GetCapabilities(BaseOwsRequest):
|
|
49
50
|
"""Request parsing for GetCapabilities.
|
|
50
51
|
|
|
@@ -60,7 +60,7 @@ class StoredQuery(QueryExpression):
|
|
|
60
60
|
Note that the base class logic (such as ``<wfs:PropertyName>`` elements) are still applicable.
|
|
61
61
|
|
|
62
62
|
This element resolves the stored query using the
|
|
63
|
-
:class:`~gisserver.
|
|
63
|
+
:class:`~gisserver.extensions.queries.StoredQueryRegistry`,
|
|
64
64
|
and passes the execution to this custom function.
|
|
65
65
|
"""
|
|
66
66
|
|
|
@@ -165,7 +165,8 @@ class StoredQuery(QueryExpression):
|
|
|
165
165
|
"""Forward queryset creation to the implementation class."""
|
|
166
166
|
return self.implementation.build_query(compiler)
|
|
167
167
|
|
|
168
|
-
def as_kvp(self):
|
|
168
|
+
def as_kvp(self) -> dict:
|
|
169
|
+
"""Translate the POST request into KVP GET parameters. This is needed for pagination."""
|
|
169
170
|
# As this is such edge case, only support the minimal for CITE tests.
|
|
170
171
|
params = super().as_kvp()
|
|
171
172
|
params["STOREDQUERY_ID"] = self.id
|
gisserver/parsers/xml.py
CHANGED
|
@@ -50,8 +50,10 @@ class xmlns(Enum):
|
|
|
50
50
|
ows20 = "http://www.opengis.net/ows/2.0"
|
|
51
51
|
wms = "http://www.opengis.net/wms" # Web Map Service (WMS)
|
|
52
52
|
wcs = "http://www.opengis.net/wcs" # Web Coverage Service (WCS)
|
|
53
|
+
wcs20 = "http://www.opengis.net/wcs/2.0"
|
|
53
54
|
wps = "http://www.opengis.net/wps/1.0.0" # Web Processing Service (WPS)
|
|
54
55
|
wmts = "http://www.opengis.net/wmts/1.0" # Web Map Tile Service (WMTS)
|
|
56
|
+
wfs1 = "http://www.opengis.net/wfs"
|
|
55
57
|
wfs20 = "http://www.opengis.net/wfs/2.0" # Web Feature Service (WFS)
|
|
56
58
|
fes20 = "http://www.opengis.net/fes/2.0" # Filter Encoding Standard (FES)
|
|
57
59
|
gml21 = "http://www.opengis.net/gml"
|
|
@@ -108,6 +110,16 @@ class NSElement(Element):
|
|
|
108
110
|
"""Resolve an aliased QName value to its fully qualified name."""
|
|
109
111
|
return parse_qname(qname, self.ns_aliases)
|
|
110
112
|
|
|
113
|
+
@property
|
|
114
|
+
def qname(self) -> str:
|
|
115
|
+
"""Provde the tag name in its original short format"""
|
|
116
|
+
ns, localname = split_ns(self.tag)
|
|
117
|
+
if ns:
|
|
118
|
+
for prefix, full_ns in self.ns_aliases.items():
|
|
119
|
+
if full_ns == ns:
|
|
120
|
+
return f"{prefix}:{localname}" if prefix else localname
|
|
121
|
+
return localname
|
|
122
|
+
|
|
111
123
|
def get_str_attribute(self, name: str) -> str:
|
|
112
124
|
"""Resolve an attribute, raise an error when it's missing."""
|
|
113
125
|
try:
|
gisserver/projection.py
CHANGED
|
@@ -29,8 +29,8 @@ if typing.TYPE_CHECKING:
|
|
|
29
29
|
from django.contrib.gis.geos import GEOSGeometry
|
|
30
30
|
from django.db import models
|
|
31
31
|
|
|
32
|
+
from gisserver.crs import CRS
|
|
32
33
|
from gisserver.features import FeatureType
|
|
33
|
-
from gisserver.geometries import CRS
|
|
34
34
|
from gisserver.parsers import fes20, wfs20
|
|
35
35
|
|
|
36
36
|
__all__ = (
|
|
@@ -47,14 +47,25 @@ class FeatureProjection:
|
|
|
47
47
|
|
|
48
48
|
Instead of walking over the full XSD object tree,
|
|
49
49
|
this object wraps that and makes sure only the actual requested fields are used.
|
|
50
|
-
When a PROPERTYNAME is used in the request,
|
|
51
|
-
which fields to retrieve, which to prefetch, and which to render.
|
|
50
|
+
When a ``PROPERTYNAME`` (or ``<wfs:PropertyName>``) is used in the request,
|
|
51
|
+
this will limit which fields to retrieve, which to prefetch, and which to render.
|
|
52
52
|
"""
|
|
53
53
|
|
|
54
|
+
#: Referencing the Feature that is rendered.
|
|
54
55
|
feature_type: FeatureType
|
|
56
|
+
|
|
57
|
+
#: The list of root element to render for this feature.
|
|
55
58
|
xsd_root_elements: list[XsdElement]
|
|
59
|
+
|
|
60
|
+
#: The subset of child nodes to render for a given element.
|
|
56
61
|
xsd_child_nodes: dict[XsdElement | None, list[XsdElement]]
|
|
57
62
|
|
|
63
|
+
#: The output Coordinate Reference System
|
|
64
|
+
output_crs: CRS
|
|
65
|
+
|
|
66
|
+
#: Whether the output should be rendered without wrapper tags (for GetFeatureById).
|
|
67
|
+
output_standalone: bool
|
|
68
|
+
|
|
58
69
|
def __init__(
|
|
59
70
|
self,
|
|
60
71
|
feature_types: list[FeatureType],
|
|
@@ -69,7 +80,8 @@ class FeatureProjection:
|
|
|
69
80
|
:param property_names: Limited list of fields to render only.
|
|
70
81
|
:param value_reference: Single element to display fo GetPropertyValue
|
|
71
82
|
:param output_crs: Which coordinate reference system to use for geometry data.
|
|
72
|
-
:param output_standalone:
|
|
83
|
+
:param output_standalone: Used for the ``GetFeatureById`` stored query.
|
|
84
|
+
This removes the ``wfs:FeatureCollection><wfs:member>`` wrapper elements from the output.
|
|
73
85
|
"""
|
|
74
86
|
self.feature_types = feature_types
|
|
75
87
|
self.feature_type = feature_types[0] # JOIN still not supported.
|
|
@@ -302,9 +314,7 @@ class FeatureProjection:
|
|
|
302
314
|
|
|
303
315
|
@dataclass
|
|
304
316
|
class FeatureRelation:
|
|
305
|
-
"""Tell which related fields are queried by the feature.
|
|
306
|
-
Each dict holds an ORM-path, with the relevant sub-elements.
|
|
307
|
-
"""
|
|
317
|
+
"""Tell which related fields are queried by the feature."""
|
|
308
318
|
|
|
309
319
|
#: The ORM path that is queried for this particular relation
|
|
310
320
|
orm_path: str
|