django-gisserver 2.0__py3-none-any.whl → 2.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/METADATA +27 -10
- django_gisserver-2.1.1.dist-info/RECORD +68 -0
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/conf.py +23 -1
- gisserver/crs.py +452 -0
- gisserver/db.py +78 -6
- gisserver/exceptions.py +106 -2
- gisserver/extensions/functions.py +122 -28
- gisserver/extensions/queries.py +15 -10
- gisserver/features.py +46 -33
- gisserver/geometries.py +64 -306
- gisserver/management/commands/loadgeojson.py +41 -21
- gisserver/operations/base.py +11 -7
- gisserver/operations/wfs20.py +31 -93
- gisserver/output/__init__.py +6 -2
- gisserver/output/base.py +28 -13
- gisserver/output/csv.py +18 -6
- gisserver/output/geojson.py +7 -6
- gisserver/output/gml32.py +86 -27
- gisserver/output/results.py +25 -39
- gisserver/output/utils.py +9 -2
- gisserver/parsers/ast.py +177 -68
- gisserver/parsers/fes20/__init__.py +76 -4
- gisserver/parsers/fes20/expressions.py +97 -27
- gisserver/parsers/fes20/filters.py +9 -6
- gisserver/parsers/fes20/identifiers.py +27 -7
- gisserver/parsers/fes20/lookups.py +8 -6
- gisserver/parsers/fes20/operators.py +101 -49
- gisserver/parsers/fes20/sorting.py +14 -6
- gisserver/parsers/gml/__init__.py +10 -19
- gisserver/parsers/gml/base.py +32 -14
- gisserver/parsers/gml/geometries.py +54 -21
- gisserver/parsers/ows/kvp.py +10 -2
- gisserver/parsers/ows/requests.py +6 -4
- gisserver/parsers/query.py +6 -2
- gisserver/parsers/values.py +61 -4
- gisserver/parsers/wfs20/__init__.py +2 -0
- gisserver/parsers/wfs20/adhoc.py +28 -18
- gisserver/parsers/wfs20/base.py +12 -7
- gisserver/parsers/wfs20/projection.py +3 -3
- gisserver/parsers/wfs20/requests.py +1 -0
- gisserver/parsers/wfs20/stored.py +3 -2
- gisserver/parsers/xml.py +12 -0
- gisserver/projection.py +17 -7
- gisserver/static/gisserver/index.css +27 -6
- gisserver/templates/gisserver/base.html +15 -0
- gisserver/templates/gisserver/index.html +10 -16
- gisserver/templates/gisserver/service_description.html +12 -6
- gisserver/templates/gisserver/wfs/feature_field.html +1 -1
- gisserver/templates/gisserver/wfs/feature_type.html +44 -13
- gisserver/types.py +152 -82
- gisserver/views.py +47 -24
- django_gisserver-2.0.dist-info/RECORD +0 -66
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info/licenses}/LICENSE +0 -0
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/top_level.txt +0 -0
gisserver/db.py
CHANGED
|
@@ -5,11 +5,13 @@ from __future__ import annotations
|
|
|
5
5
|
import logging
|
|
6
6
|
from functools import lru_cache, reduce
|
|
7
7
|
|
|
8
|
-
from django.contrib.gis.db.models import functions
|
|
8
|
+
from django.contrib.gis.db.models import Extent, PolygonField, functions
|
|
9
|
+
from django.contrib.gis.db.models.fields import ExtentField
|
|
9
10
|
from django.db import connection, connections, models
|
|
10
11
|
|
|
11
12
|
from gisserver import conf
|
|
12
|
-
from gisserver.
|
|
13
|
+
from gisserver.crs import CRS, WGS84
|
|
14
|
+
from gisserver.geometries import WGS84BoundingBox
|
|
13
15
|
from gisserver.types import GeometryXsdElement
|
|
14
16
|
|
|
15
17
|
logger = logging.getLogger(__name__)
|
|
@@ -29,6 +31,8 @@ class AsEWKT(functions.GeoFunc):
|
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
class AsGML(functions.AsGML):
|
|
34
|
+
"""An overwritten ST_AsGML() function to handle PostGIS extensions."""
|
|
35
|
+
|
|
32
36
|
name = "AsGML"
|
|
33
37
|
|
|
34
38
|
def __init__(
|
|
@@ -37,20 +41,61 @@ class AsGML(functions.AsGML):
|
|
|
37
41
|
version=3,
|
|
38
42
|
precision=conf.GISSERVER_DB_PRECISION,
|
|
39
43
|
envelope=False,
|
|
44
|
+
is_latlon=False,
|
|
45
|
+
long_urn=False,
|
|
40
46
|
**extra,
|
|
41
47
|
):
|
|
42
|
-
# Note that Django's AsGml
|
|
43
|
-
# the options is postgres-only.
|
|
48
|
+
# Note that Django's AsGml the defaults are: version=2, precision=8
|
|
44
49
|
super().__init__(expression, version, precision, **extra)
|
|
45
50
|
self.envelope = envelope
|
|
51
|
+
self.is_latlon = is_latlon
|
|
52
|
+
self.long_urn = long_urn
|
|
46
53
|
|
|
47
54
|
def as_postgresql(self, compiler, connection, **extra_context):
|
|
48
55
|
# Fill options parameter (https://postgis.net/docs/ST_AsGML.html)
|
|
49
|
-
options =
|
|
56
|
+
options = 0
|
|
57
|
+
if self.long_urn:
|
|
58
|
+
options |= 1 # long CRS urn
|
|
59
|
+
if self.envelope:
|
|
60
|
+
options |= 32 # bbox
|
|
61
|
+
if self.is_latlon:
|
|
62
|
+
# PostGIS provides the data in longitude/latitude format (east/north to look like x/y).
|
|
63
|
+
# However, WFS 2.0 fixed their axis by following the authority. The ST_AsGML() doesn't
|
|
64
|
+
# detect this on their own. It needs to be told to flip the coordinates.
|
|
65
|
+
# Passing option 16 flips the coordinates unconditionally, like using ST_FlipCoordinates()
|
|
66
|
+
# but more efficiently done during rendering. That happens in:
|
|
67
|
+
# https://github.com/postgis/postgis/blob/81e2bc783b77cc740291445e992658e1db7179e0/liblwgeom/lwout_gml.c#L121
|
|
68
|
+
options |= 16
|
|
69
|
+
|
|
50
70
|
template = f"%(function)s(%(expressions)s, {options})"
|
|
51
71
|
return self.as_sql(compiler, connection, template=template, **extra_context)
|
|
52
72
|
|
|
53
73
|
|
|
74
|
+
class ST_SetSRID(functions.Transform):
|
|
75
|
+
"""PostGIS function to assign an SRID to geometry.
|
|
76
|
+
When this is applied to the result from an ``Extent`` aggegrate,
|
|
77
|
+
it will convert that ``BBOX(...)`` value into a ``POLYGON(...)``.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
name = "SetSRID"
|
|
81
|
+
geom_param_pos = ()
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def geo_field(self):
|
|
85
|
+
return PolygonField(srid=self.source_expressions[1].value)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Box2D(functions.GeomOutputGeoFunc):
|
|
89
|
+
"""PostGIS function (without ST_) that converts a ``POLYGON(...)`` back to a ``BOX(...)``."""
|
|
90
|
+
|
|
91
|
+
name = "Box2D"
|
|
92
|
+
function = "Box2D" # no ST_ prefix.
|
|
93
|
+
output_field = ExtentField()
|
|
94
|
+
|
|
95
|
+
def convert_value(self, value, expression, connection):
|
|
96
|
+
return connection.ops.convert_extent(value)
|
|
97
|
+
|
|
98
|
+
|
|
54
99
|
class ST_Union(functions.Union):
|
|
55
100
|
name = "Union"
|
|
56
101
|
arity = None
|
|
@@ -62,6 +107,31 @@ class ST_Union(functions.Union):
|
|
|
62
107
|
return self.as_sql(compiler, connection, **extra_context)
|
|
63
108
|
|
|
64
109
|
|
|
110
|
+
def get_wgs84_bounding_box(
|
|
111
|
+
queryset: models.QuerySet, geo_element: GeometryXsdElement
|
|
112
|
+
) -> WGS84BoundingBox:
|
|
113
|
+
"""Calculate the WGS84 bounding box for a feature.
|
|
114
|
+
|
|
115
|
+
Note that the ``<ows:WGS84BoundingBox>`` element
|
|
116
|
+
always uses longitude/latitude, and doesn't describe a CRS.
|
|
117
|
+
"""
|
|
118
|
+
if connections[queryset.db].vendor == "postgresql":
|
|
119
|
+
# Allow a more efficient way to combine geometry first, transform once later
|
|
120
|
+
box = queryset.aggregate(
|
|
121
|
+
box=Box2D(
|
|
122
|
+
functions.Transform(
|
|
123
|
+
ST_SetSRID(Extent(geo_element.orm_path), srid=geo_element.source_srid),
|
|
124
|
+
srid=WGS84.srid,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
)["box"]
|
|
128
|
+
else:
|
|
129
|
+
# Need to transform each element to srid 4326 before combining it in an extent.
|
|
130
|
+
box = queryset.aggregate(box=Extent(get_db_geometry_target(geo_element, WGS84)))["box"]
|
|
131
|
+
|
|
132
|
+
return WGS84BoundingBox(*box) if box else None
|
|
133
|
+
|
|
134
|
+
|
|
65
135
|
def get_geometries_union(
|
|
66
136
|
expressions: list[str | functions.GeoFunc], using="default"
|
|
67
137
|
) -> str | functions.Union:
|
|
@@ -86,6 +156,7 @@ def replace_queryset_geometries(
|
|
|
86
156
|
geo_elements: list[GeometryXsdElement],
|
|
87
157
|
output_crs: CRS,
|
|
88
158
|
wrapper_func: type[functions.GeoFunc],
|
|
159
|
+
**wrapper_kwargs,
|
|
89
160
|
) -> models.QuerySet:
|
|
90
161
|
"""Replace the queryset geometry retrieval with a database-rendered version.
|
|
91
162
|
|
|
@@ -98,7 +169,8 @@ def replace_queryset_geometries(
|
|
|
98
169
|
defer_names.append(geo_element.local_orm_path)
|
|
99
170
|
annotation_name = _as_annotation_name(geo_element.local_orm_path, wrapper_func)
|
|
100
171
|
as_geo_map[annotation_name] = wrapper_func(
|
|
101
|
-
get_db_geometry_target(geo_element, output_crs, use_relative_path=True)
|
|
172
|
+
get_db_geometry_target(geo_element, output_crs, use_relative_path=True),
|
|
173
|
+
**wrapper_kwargs,
|
|
102
174
|
)
|
|
103
175
|
|
|
104
176
|
if not defer_names:
|
gisserver/exceptions.py
CHANGED
|
@@ -8,13 +8,23 @@ https://docs.opengeospatial.org/is/09-025r2/09-025r2.html#35
|
|
|
8
8
|
https://docs.opengeospatial.org/is/09-025r2/09-025r2.html#411
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
11
13
|
import logging
|
|
14
|
+
import typing
|
|
12
15
|
from contextlib import contextmanager
|
|
13
16
|
|
|
14
17
|
from django.conf import settings
|
|
18
|
+
from django.core.exceptions import FieldError, ValidationError
|
|
19
|
+
from django.db import InternalError, ProgrammingError
|
|
15
20
|
from django.http import HttpResponse
|
|
16
21
|
from django.utils.html import format_html
|
|
17
22
|
|
|
23
|
+
from gisserver import conf
|
|
24
|
+
|
|
25
|
+
if typing.TYPE_CHECKING:
|
|
26
|
+
from gisserver.parsers import wfs20
|
|
27
|
+
|
|
18
28
|
logger = logging.getLogger(__name__)
|
|
19
29
|
|
|
20
30
|
|
|
@@ -35,6 +45,90 @@ def wrap_parser_errors(name: str, locator: str):
|
|
|
35
45
|
raise InvalidParameterValue(f"Invalid {name} argument: {e}", locator=locator) from None
|
|
36
46
|
|
|
37
47
|
|
|
48
|
+
@contextmanager
|
|
49
|
+
def wrap_filter_errors(query: wfs20.QueryExpression): # noqa:C901
|
|
50
|
+
"""Perform a QuerySet/filter creation operation.
|
|
51
|
+
and trap many parser errors in the making of it."""
|
|
52
|
+
try:
|
|
53
|
+
yield
|
|
54
|
+
except ExternalParsingError as e:
|
|
55
|
+
# Bad input data
|
|
56
|
+
_log_filter_error(query, logging.ERROR, e)
|
|
57
|
+
raise OperationParsingFailed(str(e), locator=query.query_locator) from e
|
|
58
|
+
except ExternalValueError as e:
|
|
59
|
+
# Bad input data
|
|
60
|
+
_log_filter_error(query, logging.ERROR, e)
|
|
61
|
+
raise InvalidParameterValue(str(e), locator=query.query_locator) from e
|
|
62
|
+
except ValidationError as e:
|
|
63
|
+
# Bad input data
|
|
64
|
+
_log_filter_error(query, logging.ERROR, e)
|
|
65
|
+
raise OperationParsingFailed(
|
|
66
|
+
"\n".join(map(str, e.messages)),
|
|
67
|
+
locator=query.query_locator,
|
|
68
|
+
) from e
|
|
69
|
+
except FieldError as e:
|
|
70
|
+
# e.g. doing a LIKE on a foreign key, or requesting an unknown field.
|
|
71
|
+
if not conf.GISSERVER_WRAP_FILTER_DB_ERRORS:
|
|
72
|
+
raise
|
|
73
|
+
_log_filter_error(query, logging.ERROR, e)
|
|
74
|
+
raise InvalidParameterValue(
|
|
75
|
+
"Internal error when processing filter",
|
|
76
|
+
locator=query.query_locator,
|
|
77
|
+
) from e
|
|
78
|
+
except (InternalError, ProgrammingError) as e:
|
|
79
|
+
# e.g. comparing datetime against integer
|
|
80
|
+
if not conf.GISSERVER_WRAP_FILTER_DB_ERRORS:
|
|
81
|
+
raise
|
|
82
|
+
logger.exception("WFS request failed: %s\nQuery: %r", str(e), query)
|
|
83
|
+
msg = str(e)
|
|
84
|
+
locator = "srsName" if "Cannot find SRID" in msg else query.query_locator
|
|
85
|
+
raise InvalidParameterValue(f"Invalid request: {msg}", locator=locator) from e
|
|
86
|
+
except (TypeError, ValueError) as e:
|
|
87
|
+
# TypeError/ValueError could reference a datatype mismatch in an
|
|
88
|
+
# ORM query, but it could also be an internal bug. In most cases,
|
|
89
|
+
# this is already caught by XsdElement.validate_comparison().
|
|
90
|
+
raise
|
|
91
|
+
if _is_orm_error(e):
|
|
92
|
+
if query.query_locator == "STOREDQUERY_ID":
|
|
93
|
+
# This is a fallback, ideally the stored query performs its own validation.
|
|
94
|
+
raise InvalidParameterValue(
|
|
95
|
+
f"Invalid stored query parameter: {e}", locator=query.query_locator
|
|
96
|
+
) from e
|
|
97
|
+
else:
|
|
98
|
+
raise InvalidParameterValue(
|
|
99
|
+
f"Invalid filter query: {e}", locator=query.query_locator
|
|
100
|
+
) from e
|
|
101
|
+
raise
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_orm_error(exception: Exception) -> bool:
|
|
105
|
+
"""Tell whether an exception is caused by the ORM."""
|
|
106
|
+
traceback = exception.__traceback__
|
|
107
|
+
while traceback.tb_next is not None:
|
|
108
|
+
traceback = traceback.tb_next
|
|
109
|
+
if "/django/db/models/query" in traceback.tb_frame.f_code.co_filename:
|
|
110
|
+
return True
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _log_filter_error(query, level, exc):
|
|
115
|
+
"""Report a filtering parsing error in the logging"""
|
|
116
|
+
filter = getattr(query, "filter", None) # AdhocQuery only
|
|
117
|
+
fes_xml = filter.source if filter is not None else "(not provided)"
|
|
118
|
+
try:
|
|
119
|
+
sql = exc.__cause__.cursor.query.decode()
|
|
120
|
+
except AttributeError:
|
|
121
|
+
logger.log(level, "WFS query failed: %s\nFilter:\n%s", exc, fes_xml)
|
|
122
|
+
else:
|
|
123
|
+
logger.log(
|
|
124
|
+
level,
|
|
125
|
+
"WFS query failed: %s\nSQL Query: %s\n\nFilter:\n%s",
|
|
126
|
+
exc,
|
|
127
|
+
sql,
|
|
128
|
+
fes_xml,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
38
132
|
class ExternalValueError(ValueError):
|
|
39
133
|
"""Raise a ValueError for external input.
|
|
40
134
|
This helps to distinguish between internal bugs
|
|
@@ -46,6 +140,14 @@ class ExternalParsingError(ValueError):
|
|
|
46
140
|
"""Raise a ValueError for a parsing problem."""
|
|
47
141
|
|
|
48
142
|
|
|
143
|
+
class XmlElementNotSupported(ExternalParsingError):
|
|
144
|
+
"""Raise a ValueError when an XML tag is not known by the parser at all."""
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class InvalidXmlElement(ExternalParsingError):
|
|
148
|
+
"""Raise a ValueError when a particular XML tag wasn't expected."""
|
|
149
|
+
|
|
150
|
+
|
|
49
151
|
class OWSException(Exception):
|
|
50
152
|
"""Base class for XML based exceptions in this module."""
|
|
51
153
|
|
|
@@ -68,7 +170,8 @@ class OWSException(Exception):
|
|
|
68
170
|
self.code = code or self.code or self.__class__.__name__
|
|
69
171
|
self.status_code = status_code or self.status_code
|
|
70
172
|
|
|
71
|
-
def as_response(self):
|
|
173
|
+
def as_response(self) -> HttpResponse:
|
|
174
|
+
"""Return the excetion as HTTP response."""
|
|
72
175
|
logger.debug("Returning HTTP %d for %s: %s", self.status_code, self.code, self.text)
|
|
73
176
|
xml_body = self.as_xml()
|
|
74
177
|
return HttpResponse(
|
|
@@ -78,7 +181,8 @@ class OWSException(Exception):
|
|
|
78
181
|
reason=self.reason,
|
|
79
182
|
)
|
|
80
183
|
|
|
81
|
-
def as_xml(self):
|
|
184
|
+
def as_xml(self) -> str:
|
|
185
|
+
"""Serialize the exception to an XML string."""
|
|
82
186
|
return format_html(
|
|
83
187
|
"<ows:ExceptionReport"
|
|
84
188
|
' xmlns:ows="http://www.opengis.net/ows/1.1"'
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
"""Functions to be callable from
|
|
1
|
+
"""Functions to be callable from query filters.
|
|
2
|
+
|
|
3
|
+
By using the :attr:`function_registry`, custom stored functions can be registered in this server.
|
|
4
|
+
These are called by the filter queries using the :class:`~gisserver.parsers.fes20.expressions.Function` element.
|
|
5
|
+
Out of the box, various built-in functions are present.
|
|
6
|
+
|
|
7
|
+
Built-in options are documented in :ref:`functions`.
|
|
8
|
+
|
|
9
|
+
Most of the out-of-the box options are inspired
|
|
10
|
+
by `GeoServer <https://docs.geoserver.org/latest/en/user/filter/function_reference.html>`_.
|
|
11
|
+
Functions which already have a fes-syntax equivalent have been are omitted.
|
|
12
|
+
"""
|
|
2
13
|
|
|
3
14
|
from __future__ import annotations
|
|
4
15
|
|
|
@@ -6,15 +17,16 @@ from collections.abc import Callable
|
|
|
6
17
|
from dataclasses import dataclass
|
|
7
18
|
from typing import Union
|
|
8
19
|
|
|
20
|
+
import django
|
|
9
21
|
from django.contrib.gis.db.models import functions as gis
|
|
10
22
|
from django.db import models
|
|
11
23
|
from django.db.models import functions
|
|
12
|
-
from django.db.models.expressions import Combinable
|
|
24
|
+
from django.db.models.expressions import Combinable, Value
|
|
13
25
|
|
|
14
26
|
from gisserver.exceptions import InvalidParameterValue
|
|
15
27
|
from gisserver.types import XsdTypes
|
|
16
28
|
|
|
17
|
-
__all__ = ["function_registry"]
|
|
29
|
+
__all__ = ["function_registry", "FesFunctionRegistry", "FesFunction"]
|
|
18
30
|
|
|
19
31
|
FesArg = Union[Combinable, models.Q]
|
|
20
32
|
FesFunctionBody = Union[models.Func, Callable[..., models.Func]]
|
|
@@ -22,7 +34,7 @@ FesFunctionBody = Union[models.Func, Callable[..., models.Func]]
|
|
|
22
34
|
|
|
23
35
|
@dataclass(order=True)
|
|
24
36
|
class FesFunction:
|
|
25
|
-
"""A registered database function that can be used by ``<fes:Function name="..."
|
|
37
|
+
"""A registered database function that can be used by ``<fes:Function name="...">``.
|
|
26
38
|
|
|
27
39
|
The :class:`~gisserver.parsers.fes20.expressions.Function` class will resolve
|
|
28
40
|
these registered functions by name, and call :meth:`build_query` to include them
|
|
@@ -65,9 +77,11 @@ class FesFunctionRegistry:
|
|
|
65
77
|
self.functions = {}
|
|
66
78
|
|
|
67
79
|
def __bool__(self):
|
|
80
|
+
"""Tell whether there are functions"""
|
|
68
81
|
return bool(self.functions)
|
|
69
82
|
|
|
70
83
|
def __iter__(self):
|
|
84
|
+
"""Iterate over the functions"""
|
|
71
85
|
return iter(sorted(self.functions.values())) # for template rendering
|
|
72
86
|
|
|
73
87
|
def register(
|
|
@@ -112,15 +126,11 @@ class FesFunctionRegistry:
|
|
|
112
126
|
) from None
|
|
113
127
|
|
|
114
128
|
|
|
129
|
+
#: The function registry
|
|
115
130
|
function_registry = FesFunctionRegistry()
|
|
116
131
|
|
|
117
132
|
|
|
118
133
|
# Register a set of default SQL functions.
|
|
119
|
-
# These are based on GeoServer:
|
|
120
|
-
# https://docs.geoserver.org/latest/en/user/filter/function_reference.html
|
|
121
|
-
# Not implemented:
|
|
122
|
-
# - Aggregates (like Collection_*)
|
|
123
|
-
# - Comparisons (which already has fes variants)
|
|
124
134
|
|
|
125
135
|
# -- strings
|
|
126
136
|
|
|
@@ -131,6 +141,27 @@ function_registry.register(
|
|
|
131
141
|
returns=XsdTypes.string,
|
|
132
142
|
)
|
|
133
143
|
|
|
144
|
+
function_registry.register(
|
|
145
|
+
"strIndexOf",
|
|
146
|
+
lambda string, substring: (functions.StrIndex(string, Value(substring)) - 1),
|
|
147
|
+
arguments={"string": XsdTypes.string, "substring": XsdTypes.string},
|
|
148
|
+
returns=XsdTypes.string,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
function_registry.register(
|
|
152
|
+
"strSubstring",
|
|
153
|
+
lambda string, begin, end: functions.Substr(string, begin + 1, end - begin),
|
|
154
|
+
arguments={"string": XsdTypes.string, "begin": XsdTypes.integer, "end": XsdTypes.integer},
|
|
155
|
+
returns=XsdTypes.string,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
function_registry.register(
|
|
159
|
+
"strSubstringStart",
|
|
160
|
+
lambda string, begin, end: functions.Substr(string, begin + 1),
|
|
161
|
+
arguments={"string": XsdTypes.string, "begin": XsdTypes.integer},
|
|
162
|
+
returns=XsdTypes.string,
|
|
163
|
+
)
|
|
164
|
+
|
|
134
165
|
function_registry.register(
|
|
135
166
|
"strToLowerCase",
|
|
136
167
|
functions.Lower,
|
|
@@ -312,25 +343,25 @@ function_registry.register(
|
|
|
312
343
|
# -- geometric
|
|
313
344
|
|
|
314
345
|
function_registry.register(
|
|
315
|
-
"
|
|
346
|
+
"area",
|
|
316
347
|
gis.Area,
|
|
317
|
-
arguments={"
|
|
348
|
+
arguments={"geom": XsdTypes.gmlAbstractGeometryType},
|
|
318
349
|
returns=XsdTypes.double,
|
|
319
350
|
)
|
|
320
351
|
|
|
321
352
|
function_registry.register(
|
|
322
|
-
"
|
|
353
|
+
"centroid",
|
|
323
354
|
gis.Centroid,
|
|
324
|
-
arguments={"
|
|
355
|
+
arguments={"geom": XsdTypes.gmlAbstractGeometryType},
|
|
325
356
|
returns=XsdTypes.string,
|
|
326
357
|
)
|
|
327
358
|
|
|
328
359
|
function_registry.register(
|
|
329
|
-
"
|
|
360
|
+
"difference",
|
|
330
361
|
gis.Difference,
|
|
331
362
|
arguments={
|
|
332
|
-
"
|
|
333
|
-
"
|
|
363
|
+
"a": XsdTypes.gmlAbstractGeometryType,
|
|
364
|
+
"b": XsdTypes.gmlAbstractGeometryType,
|
|
334
365
|
},
|
|
335
366
|
returns=XsdTypes.string,
|
|
336
367
|
)
|
|
@@ -339,35 +370,98 @@ function_registry.register(
|
|
|
339
370
|
"distance",
|
|
340
371
|
gis.Distance,
|
|
341
372
|
arguments={
|
|
342
|
-
"
|
|
343
|
-
"
|
|
373
|
+
"a": XsdTypes.gmlAbstractGeometryType,
|
|
374
|
+
"b": XsdTypes.gmlAbstractGeometryType,
|
|
344
375
|
},
|
|
345
376
|
returns=XsdTypes.double,
|
|
346
377
|
)
|
|
347
378
|
|
|
348
379
|
function_registry.register(
|
|
349
|
-
"
|
|
380
|
+
"envelope",
|
|
350
381
|
gis.Envelope,
|
|
382
|
+
arguments={"geom": XsdTypes.gmlAbstractGeometryType},
|
|
383
|
+
returns=XsdTypes.gmlAbstractGeometryType, # returns point or polygon
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
function_registry.register(
|
|
387
|
+
"geomLength",
|
|
388
|
+
gis.Length,
|
|
351
389
|
arguments={"geometry": XsdTypes.gmlAbstractGeometryType},
|
|
352
|
-
returns=XsdTypes.
|
|
390
|
+
returns=XsdTypes.float,
|
|
353
391
|
)
|
|
354
392
|
|
|
355
393
|
function_registry.register(
|
|
356
|
-
"
|
|
394
|
+
"intersection",
|
|
357
395
|
gis.Intersection,
|
|
358
396
|
arguments={
|
|
359
|
-
"
|
|
360
|
-
"
|
|
397
|
+
"a": XsdTypes.gmlAbstractGeometryType,
|
|
398
|
+
"b": XsdTypes.gmlAbstractGeometryType,
|
|
361
399
|
},
|
|
362
|
-
returns=XsdTypes.
|
|
400
|
+
returns=XsdTypes.gmlAbstractGeometryType,
|
|
363
401
|
)
|
|
364
402
|
|
|
403
|
+
if django.VERSION >= (4, 2):
|
|
404
|
+
function_registry.register(
|
|
405
|
+
"isEmpty",
|
|
406
|
+
gis.IsEmpty,
|
|
407
|
+
arguments={
|
|
408
|
+
"geom": XsdTypes.gmlAbstractGeometryType,
|
|
409
|
+
},
|
|
410
|
+
returns=XsdTypes.boolean,
|
|
411
|
+
)
|
|
412
|
+
|
|
365
413
|
function_registry.register(
|
|
366
|
-
"
|
|
414
|
+
"isValid",
|
|
415
|
+
gis.IsValid,
|
|
416
|
+
arguments={
|
|
417
|
+
"geom": XsdTypes.gmlAbstractGeometryType,
|
|
418
|
+
},
|
|
419
|
+
returns=XsdTypes.boolean,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
function_registry.register(
|
|
423
|
+
"numGeometries",
|
|
424
|
+
gis.NumGeometries,
|
|
425
|
+
arguments={
|
|
426
|
+
"collection": XsdTypes.gmlAbstractGeometryType,
|
|
427
|
+
},
|
|
428
|
+
returns=XsdTypes.integer,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
function_registry.register(
|
|
432
|
+
"numPoints",
|
|
433
|
+
gis.NumPoints,
|
|
434
|
+
arguments={
|
|
435
|
+
"collection": XsdTypes.gmlAbstractGeometryType,
|
|
436
|
+
},
|
|
437
|
+
returns=XsdTypes.integer,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
function_registry.register(
|
|
441
|
+
"perimeter",
|
|
442
|
+
gis.Perimeter,
|
|
443
|
+
arguments={
|
|
444
|
+
"geom": XsdTypes.gmlAbstractGeometryType,
|
|
445
|
+
},
|
|
446
|
+
returns=XsdTypes.integer,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
function_registry.register(
|
|
450
|
+
"symDifference",
|
|
451
|
+
gis.SymDifference,
|
|
452
|
+
arguments={
|
|
453
|
+
"a": XsdTypes.gmlAbstractGeometryType,
|
|
454
|
+
"b": XsdTypes.gmlAbstractGeometryType,
|
|
455
|
+
},
|
|
456
|
+
returns=XsdTypes.gmlAbstractGeometryType,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
function_registry.register(
|
|
460
|
+
"union",
|
|
367
461
|
gis.Union,
|
|
368
462
|
arguments={
|
|
369
|
-
"
|
|
370
|
-
"
|
|
463
|
+
"a": XsdTypes.gmlAbstractGeometryType,
|
|
464
|
+
"b": XsdTypes.gmlAbstractGeometryType,
|
|
371
465
|
},
|
|
372
|
-
returns=XsdTypes.
|
|
466
|
+
returns=XsdTypes.gmlAbstractGeometryType,
|
|
373
467
|
)
|
gisserver/extensions/queries.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Storage and registry for stored queries.
|
|
2
2
|
These definitions follow the WFS spec.
|
|
3
3
|
|
|
4
|
-
By using the
|
|
5
|
-
Out of the box, only the mandatory built-in
|
|
4
|
+
By using the :attr:`stored_query_registry`, custom stored queries can be registered in this server.
|
|
5
|
+
Out of the box, only the mandatory built-in :class:`GetFeatureById` query is present.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -30,10 +30,14 @@ __all__ = (
|
|
|
30
30
|
"StoredQueryDescription",
|
|
31
31
|
"StoredQueryRegistry",
|
|
32
32
|
"stored_query_registry",
|
|
33
|
+
"WFS_LANGUAGE",
|
|
34
|
+
"FES_LANGUAGE",
|
|
33
35
|
)
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
#: The query body is a ``<wfs:Query>``.
|
|
38
|
+
WFS_LANGUAGE = "urn:ogc:def:queryLanguage:OGC-WFS::WFS_QueryExpression"
|
|
39
|
+
#: The query body is a ``<fes:Filter>``.
|
|
40
|
+
FES_LANGUAGE = "urn:ogc:def:queryLanguage:OGC-FES:Filter"
|
|
37
41
|
|
|
38
42
|
|
|
39
43
|
@dataclass
|
|
@@ -41,7 +45,7 @@ class QueryExpressionText:
|
|
|
41
45
|
"""Define the body of a stored query.
|
|
42
46
|
|
|
43
47
|
This object type is defined in the WFS spec.
|
|
44
|
-
It may contain a wfs:Query or
|
|
48
|
+
It may contain a ``<wfs:Query>`` or ``<fes:Filter>`` element.
|
|
45
49
|
"""
|
|
46
50
|
|
|
47
51
|
#: Which types the query will return.
|
|
@@ -61,7 +65,7 @@ class StoredQueryDescription:
|
|
|
61
65
|
This is based on the ``<wfs:StoredQueryDescription>`` element,
|
|
62
66
|
and returned in ``DescribeStoredQueries``.
|
|
63
67
|
|
|
64
|
-
While it's possible to define multiple QueryExpressionText nodes
|
|
68
|
+
While it's possible to define multiple :class:`QueryExpressionText` nodes
|
|
65
69
|
as metadata to describe a query, there is still only one implementation.
|
|
66
70
|
Note there is no 'typeNames=...' parameter for stored queries.
|
|
67
71
|
Only direct parameters act as input.
|
|
@@ -127,7 +131,7 @@ class StoredQueryImplementation:
|
|
|
127
131
|
|
|
128
132
|
|
|
129
133
|
class StoredQueryRegistry:
|
|
130
|
-
"""Registry of functions to be callable by
|
|
134
|
+
"""Registry of functions to be callable by ``<wfs:StoredQuery>`` and ``STOREDQUERY_ID=...``."""
|
|
131
135
|
|
|
132
136
|
def __init__(self):
|
|
133
137
|
self.stored_queries: dict[str, type(StoredQueryImplementation)] = {}
|
|
@@ -157,8 +161,7 @@ class StoredQueryRegistry:
|
|
|
157
161
|
raise TypeError("Either provide the 'meta' object or 'meta_kwargs'")
|
|
158
162
|
|
|
159
163
|
if query_expression is not None:
|
|
160
|
-
self._register(meta, query_expression)
|
|
161
|
-
return meta
|
|
164
|
+
return self._register(meta, query_expression)
|
|
162
165
|
else:
|
|
163
166
|
return partial(self._register, meta) # decorator effect.
|
|
164
167
|
|
|
@@ -174,6 +177,7 @@ class StoredQueryRegistry:
|
|
|
174
177
|
# for now link both. There is always a single implementation for the metadata.
|
|
175
178
|
meta.implementation_class = implementation_class
|
|
176
179
|
self.stored_queries[meta.id] = meta
|
|
180
|
+
return implementation_class # for decorator usage
|
|
177
181
|
|
|
178
182
|
def resolve_query(self, query_id) -> type[StoredQueryDescription]:
|
|
179
183
|
"""Find the stored procedure using the ID."""
|
|
@@ -186,6 +190,7 @@ class StoredQueryRegistry:
|
|
|
186
190
|
) from None
|
|
187
191
|
|
|
188
192
|
|
|
193
|
+
#: The stored query registry
|
|
189
194
|
stored_query_registry = StoredQueryRegistry()
|
|
190
195
|
|
|
191
196
|
|
|
@@ -211,7 +216,7 @@ class GetFeatureById(StoredQueryImplementation):
|
|
|
211
216
|
|
|
212
217
|
The execution of the ``GetFeatureById`` query is essentially the same as::
|
|
213
218
|
|
|
214
|
-
<wfs:Query xmlns:wfs=
|
|
219
|
+
<wfs:Query xmlns:wfs="..." xmlns:fes="...'>
|
|
215
220
|
<fes:Filter><fes:ResourceId rid='{ID}'/></fes:Filter>
|
|
216
221
|
</wfs:Query>
|
|
217
222
|
|