django-gisserver 2.1__tar.gz → 2.1.1__tar.gz
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.1 → django_gisserver-2.1.1}/CHANGES.md +10 -0
- {django_gisserver-2.1/django_gisserver.egg-info → django_gisserver-2.1.1}/PKG-INFO +2 -1
- {django_gisserver-2.1 → django_gisserver-2.1.1/django_gisserver.egg-info}/PKG-INFO +2 -1
- {django_gisserver-2.1 → django_gisserver-2.1.1}/django_gisserver.egg-info/requires.txt +1 -0
- django_gisserver-2.1.1/gisserver/__init__.py +1 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/conf.py +23 -1
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/crs.py +84 -33
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/db.py +7 -1
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/features.py +18 -13
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/geometries.py +1 -1
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/output/gml32.py +47 -8
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/ast.py +8 -5
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/fes20/operators.py +1 -1
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/gml/geometries.py +7 -1
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/wfs20/adhoc.py +3 -1
- django_gisserver-2.1.1/gisserver/static/gisserver/index.css +36 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/templates/gisserver/base.html +4 -1
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/templates/gisserver/index.html +1 -1
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/templates/gisserver/service_description.html +1 -1
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/templates/gisserver/wfs/feature_type.html +11 -2
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/types.py +2 -1
- {django_gisserver-2.1 → django_gisserver-2.1.1}/setup.py +1 -0
- django_gisserver-2.1/gisserver/__init__.py +0 -1
- django_gisserver-2.1/gisserver/static/gisserver/index.css +0 -20
- {django_gisserver-2.1 → django_gisserver-2.1.1}/LICENSE +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/MANIFEST.in +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/README.md +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/django_gisserver.egg-info/SOURCES.txt +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/django_gisserver.egg-info/dependency_links.txt +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/django_gisserver.egg-info/not-zip-safe +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/django_gisserver.egg-info/top_level.txt +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/compat.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/exceptions.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/extensions/__init__.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/extensions/functions.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/extensions/queries.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/management/__init__.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/management/commands/__init__.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/management/commands/loadgeojson.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/operations/__init__.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/operations/base.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/operations/wfs20.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/output/__init__.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/output/base.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/output/csv.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/output/geojson.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/output/iters.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/output/results.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/output/stored.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/output/utils.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/output/xmlschema.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/__init__.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/fes20/__init__.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/fes20/expressions.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/fes20/filters.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/fes20/identifiers.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/fes20/lookups.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/fes20/sorting.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/gml/__init__.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/gml/base.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/ows/__init__.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/ows/kvp.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/ows/requests.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/query.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/values.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/wfs20/__init__.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/wfs20/base.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/wfs20/projection.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/wfs20/requests.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/wfs20/stored.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/parsers/xml.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/projection.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/templates/gisserver/wfs/feature_field.html +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/templatetags/__init__.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/templatetags/gisserver_tags.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/views.py +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/pyproject.toml +0 -0
- {django_gisserver-2.1 → django_gisserver-2.1.1}/setup.cfg +0 -0
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
# 2025-06-06 (2.1.1)
|
|
2
|
+
|
|
3
|
+
* Fixed using legacy longitude/latitude rendering for `EPSG:4326` and `:http://www.opengis.net/gml/srs/epsg.xml#xxxx` SRS names.
|
|
4
|
+
* Improved styling for the default HTML page.
|
|
5
|
+
|
|
6
|
+
This increases interoperability with web-based clients and legacy libraries. GeoServer applies the same heuristic.
|
|
7
|
+
Clients that use the official OGC notations such as `urn:ogc:def:crs:EPSG::4326` and `http://www.opengis.net/def/crs/epsg/0/4326`
|
|
8
|
+
are not affected. If needed, this behavior can be disabled using `GISSERVER_FORCE_XY_EPSG_4326=False` and `GISSERVER_FORCE_XY_OLD_CRS=False`.
|
|
9
|
+
|
|
10
|
+
|
|
1
11
|
# 2025-05-29 (2.1)
|
|
2
12
|
|
|
3
13
|
* Added support for `ArrayField` field in CSV exports.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-gisserver
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: Django speaking WFS 2.0 (exposing GeoDjango model fields)
|
|
5
5
|
Home-page: https://github.com/amsterdam/django-gisserver
|
|
6
6
|
Author: Diederik van der Boor
|
|
@@ -46,6 +46,7 @@ Requires-Dist: pytest-cov>=2.11.1; extra == "tests"
|
|
|
46
46
|
Provides-Extra: docs
|
|
47
47
|
Requires-Dist: Django~=5.0; extra == "docs"
|
|
48
48
|
Requires-Dist: sphinxcontrib-django>=2.5; extra == "docs"
|
|
49
|
+
Requires-Dist: myst-parser>=3.0.1; extra == "docs"
|
|
49
50
|
Requires-Dist: psycopg2-binary>=2.8.4; extra == "docs"
|
|
50
51
|
Dynamic: author
|
|
51
52
|
Dynamic: author-email
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-gisserver
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: Django speaking WFS 2.0 (exposing GeoDjango model fields)
|
|
5
5
|
Home-page: https://github.com/amsterdam/django-gisserver
|
|
6
6
|
Author: Diederik van der Boor
|
|
@@ -46,6 +46,7 @@ Requires-Dist: pytest-cov>=2.11.1; extra == "tests"
|
|
|
46
46
|
Provides-Extra: docs
|
|
47
47
|
Requires-Dist: Django~=5.0; extra == "docs"
|
|
48
48
|
Requires-Dist: sphinxcontrib-django>=2.5; extra == "docs"
|
|
49
|
+
Requires-Dist: myst-parser>=3.0.1; extra == "docs"
|
|
49
50
|
Requires-Dist: psycopg2-binary>=2.8.4; extra == "docs"
|
|
50
51
|
Dynamic: author
|
|
51
52
|
Dynamic: author-email
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.1.1" # follows PEP440
|
|
@@ -4,6 +4,8 @@ from django.conf import settings
|
|
|
4
4
|
from django.core.signals import setting_changed
|
|
5
5
|
from django.dispatch import receiver
|
|
6
6
|
|
|
7
|
+
_originals = {}
|
|
8
|
+
|
|
7
9
|
# -- feature flags
|
|
8
10
|
|
|
9
11
|
# Configure whether the <ows:WGS84BoundingBox> should be included in GetCapabilities.
|
|
@@ -30,6 +32,17 @@ GISSERVER_COUNT_NUMBER_MATCHED = getattr(settings, "GISSERVER_COUNT_NUMBER_MATCH
|
|
|
30
32
|
|
|
31
33
|
# -- output rendering
|
|
32
34
|
|
|
35
|
+
# Following https://docs.geoserver.org/stable/en/user/services/wfs/axis_order.html here:
|
|
36
|
+
# Whether the older EPSG:4326 notation (instead of the OGC recommended styles)
|
|
37
|
+
# should render in legacy longitude/latitude (x/y) ordering.
|
|
38
|
+
# This increases interoperability with legacy web clients,
|
|
39
|
+
# as others use urn:ogc:def:crs:EPSG::4326 or http://www.opengis.net/def/crs/epsg/0/4326.
|
|
40
|
+
GISSERVER_FORCE_XY_EPSG_4326 = getattr(settings, "GISSERVER_FORCE_XY_EPSG_4326", True)
|
|
41
|
+
|
|
42
|
+
# Whether the legacy CRS notation http://www.opengis.net/gml/srs/epsg.xml# should render in X/Y
|
|
43
|
+
GISSERVER_FORCE_XY_OLD_CRS = getattr(settings, "GISSERVER_FORCE_XY_OLD_CRS", True)
|
|
44
|
+
|
|
45
|
+
# Extra output formats for GetFeature (see documentation for details)
|
|
33
46
|
GISSERVER_EXTRA_OUTPUT_FORMATS = getattr(settings, "GISSERVER_EXTRA_OUTPUT_FORMATS", {})
|
|
34
47
|
GISSERVER_GET_FEATURE_OUTPUT_FORMATS = getattr(
|
|
35
48
|
settings, "GISSERVER_GET_FEATURE_OUTPUT_FORMATS", {}
|
|
@@ -59,4 +72,13 @@ def _on_settings_change(setting, value, enter, **kwargs):
|
|
|
59
72
|
if not setting.startswith("GISSERVER_"):
|
|
60
73
|
return
|
|
61
74
|
|
|
62
|
-
globals()
|
|
75
|
+
conf_module = globals()
|
|
76
|
+
if value is None and not enter:
|
|
77
|
+
# override_settings().disable() returns what the django settings module had.
|
|
78
|
+
# Revert to our defaults here instead.
|
|
79
|
+
value = _originals.get(setting)
|
|
80
|
+
else:
|
|
81
|
+
# Track defaults of this file for reverting to them
|
|
82
|
+
_originals.setdefault(setting, conf_module[setting])
|
|
83
|
+
|
|
84
|
+
conf_module[setting] = value
|
|
@@ -15,6 +15,7 @@ import pyproj
|
|
|
15
15
|
from django.contrib.gis.gdal import AxisOrder, CoordTransform, OGRGeometry, SpatialReference
|
|
16
16
|
from django.contrib.gis.geos import GEOSGeometry
|
|
17
17
|
|
|
18
|
+
from gisserver import conf
|
|
18
19
|
from gisserver.exceptions import ExternalValueError
|
|
19
20
|
|
|
20
21
|
CRS_URN_REGEX = re.compile(
|
|
@@ -36,7 +37,7 @@ __all__ = [
|
|
|
36
37
|
]
|
|
37
38
|
|
|
38
39
|
# Caches to avoid reinitializing WGS84 each time.
|
|
39
|
-
|
|
40
|
+
_COMMON_CRS_BY_STR = {}
|
|
40
41
|
_COMMON_CRS_BY_SRID = {}
|
|
41
42
|
|
|
42
43
|
|
|
@@ -74,7 +75,7 @@ _get_proj_crs_from_string = lru_cache(maxsize=10)(pyproj.CRS.from_string)
|
|
|
74
75
|
_get_proj_crs_from_authority = lru_cache(maxsize=10)(pyproj.CRS.from_authority)
|
|
75
76
|
|
|
76
77
|
|
|
77
|
-
@dataclass(frozen=True)
|
|
78
|
+
@dataclass(frozen=True, eq=False)
|
|
78
79
|
class CRS:
|
|
79
80
|
"""
|
|
80
81
|
Represents a CRS (Coordinate Reference System), which preferably follows the URN format
|
|
@@ -110,6 +111,9 @@ class CRS:
|
|
|
110
111
|
#: Original input
|
|
111
112
|
origin: str = field(init=False, default=None)
|
|
112
113
|
|
|
114
|
+
#: Tell whether the input format used the legacy notation.
|
|
115
|
+
force_xy: bool = False
|
|
116
|
+
|
|
113
117
|
@classmethod
|
|
114
118
|
def from_string(cls, uri: str | int) -> CRS:
|
|
115
119
|
"""
|
|
@@ -123,12 +127,15 @@ class CRS:
|
|
|
123
127
|
* A legacy CRS URI ("epsg:<SRID>", or "http://www.opengis.net/...").
|
|
124
128
|
* A numeric SRID (which calls :meth:`from_srid()`)
|
|
125
129
|
"""
|
|
130
|
+
if known_crs := _COMMON_CRS_BY_STR.get(uri):
|
|
131
|
+
return known_crs # Avoid object re-creation
|
|
132
|
+
|
|
126
133
|
if isinstance(uri, int) or uri.isdigit():
|
|
127
134
|
return cls.from_srid(int(uri))
|
|
128
135
|
elif uri.startswith("urn:"):
|
|
129
136
|
return cls._from_urn(uri)
|
|
130
137
|
else:
|
|
131
|
-
return cls.
|
|
138
|
+
return cls._from_prefix(uri)
|
|
132
139
|
|
|
133
140
|
@classmethod
|
|
134
141
|
def from_srid(cls, srid: int):
|
|
@@ -141,22 +148,19 @@ class CRS:
|
|
|
141
148
|
if common_crs := _COMMON_CRS_BY_SRID.get(srid):
|
|
142
149
|
return common_crs # Avoid object re-creation
|
|
143
150
|
|
|
144
|
-
|
|
151
|
+
return cls(
|
|
145
152
|
domain="ogc",
|
|
146
153
|
authority="EPSG",
|
|
147
154
|
version="",
|
|
148
155
|
crsid=str(srid),
|
|
149
156
|
srid=int(srid),
|
|
150
157
|
)
|
|
151
|
-
crs.__dict__["origin"] = srid
|
|
152
|
-
return crs
|
|
153
158
|
|
|
154
159
|
@classmethod
|
|
155
160
|
def _from_urn(cls, urn): # noqa: C901
|
|
156
|
-
"""Instantiate this class using a URN format.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
"""Instantiate this class using a URN format.
|
|
162
|
+
This format is defined in https://portal.ogc.org/files/?artifact_id=30575.
|
|
163
|
+
"""
|
|
160
164
|
urn_match = CRS_URN_REGEX.match(urn)
|
|
161
165
|
if not urn_match:
|
|
162
166
|
raise ExternalValueError(f"Unknown CRS URN [{urn}] specified: {CRS_URN_REGEX.pattern}")
|
|
@@ -187,7 +191,7 @@ class CRS:
|
|
|
187
191
|
crs = cls(
|
|
188
192
|
domain=domain,
|
|
189
193
|
authority=authority,
|
|
190
|
-
version=urn_match.group(3),
|
|
194
|
+
version=urn_match.group(3) or "",
|
|
191
195
|
crsid=crsid,
|
|
192
196
|
srid=srid,
|
|
193
197
|
)
|
|
@@ -195,16 +199,30 @@ class CRS:
|
|
|
195
199
|
return crs
|
|
196
200
|
|
|
197
201
|
@classmethod
|
|
198
|
-
def
|
|
199
|
-
"""Instantiate this class from a
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
def _from_prefix(cls, uri):
|
|
203
|
+
"""Instantiate this class from a non-URI notation.
|
|
204
|
+
|
|
205
|
+
The modern URL format (:samp:`http://www.opengis.net/def/crs/epsg/0/{xxxx}`)
|
|
206
|
+
is defined in https://portal.ogc.org/files/?artifact_id=46361.
|
|
207
|
+
|
|
208
|
+
Older notations like :samp:`EPSG:{xxxx}` or legacy XML URLs like
|
|
209
|
+
and :samp`:http://www.opengis.net/gml/srs/epsg.xml#{xxxx}` are also supported.
|
|
210
|
+
"""
|
|
211
|
+
# Make sure origin uses the expected upper/lowercasing.
|
|
212
|
+
origin = uri.lower() if "://" in uri else uri.upper()
|
|
213
|
+
for prefix, force_xy in (
|
|
214
|
+
(
|
|
215
|
+
"EPSG:",
|
|
216
|
+
(conf.GISSERVER_FORCE_XY_EPSG_4326 and origin == "EPSG:4326"),
|
|
217
|
+
),
|
|
218
|
+
(
|
|
219
|
+
"http://www.opengis.net/gml/srs/epsg.xml#",
|
|
220
|
+
conf.GISSERVER_FORCE_XY_OLD_CRS,
|
|
221
|
+
),
|
|
222
|
+
("http://www.opengis.net/def/crs/epsg/0/", False),
|
|
205
223
|
):
|
|
206
|
-
if
|
|
207
|
-
crsid =
|
|
224
|
+
if origin.startswith(prefix):
|
|
225
|
+
crsid = origin[len(prefix) :]
|
|
208
226
|
try:
|
|
209
227
|
srid = int(crsid)
|
|
210
228
|
except ValueError:
|
|
@@ -218,16 +236,19 @@ class CRS:
|
|
|
218
236
|
version="",
|
|
219
237
|
crsid=crsid,
|
|
220
238
|
srid=srid,
|
|
239
|
+
force_xy=force_xy,
|
|
221
240
|
)
|
|
222
|
-
crs.__dict__["origin"] =
|
|
241
|
+
crs.__dict__["origin"] = origin
|
|
223
242
|
return crs
|
|
224
243
|
|
|
225
244
|
raise ExternalValueError(f"Unknown CRS URI [{uri}] specified")
|
|
226
245
|
|
|
227
246
|
@property
|
|
228
247
|
def legacy(self):
|
|
229
|
-
"""Return a legacy string in the format :samp:`
|
|
230
|
-
|
|
248
|
+
"""Return a legacy string in the format :samp:`http://www.opengis.net/gml/srs/epsg.xml#{srid}`."""
|
|
249
|
+
# This mirrors what GeoSever does, as this notation always has an axis ordering defined.
|
|
250
|
+
# Notations like EPSG:xxxx notation don't have such consistent usage.
|
|
251
|
+
return f"http://www.opengis.net/gml/srs/epsg.xml#{self.srid:d}"
|
|
231
252
|
|
|
232
253
|
@cached_property
|
|
233
254
|
def urn(self):
|
|
@@ -254,16 +275,29 @@ class CRS:
|
|
|
254
275
|
return [axis.direction for axis in proj_crs.axis_info]
|
|
255
276
|
|
|
256
277
|
def __str__(self):
|
|
257
|
-
return self.urn
|
|
278
|
+
return self.legacy if self.force_xy else self.urn
|
|
258
279
|
|
|
259
280
|
def __eq__(self, other):
|
|
260
281
|
if isinstance(other, CRS):
|
|
261
|
-
|
|
262
|
-
# EPSG:4326 specifies coordinates in lat/long order and CRS84 in long/lat order.
|
|
263
|
-
return self.authority == other.authority and self.srid == other.srid
|
|
282
|
+
return self.matches(other, compare_legacy=True)
|
|
264
283
|
else:
|
|
265
284
|
return NotImplemented
|
|
266
285
|
|
|
286
|
+
def matches(self, other, compare_legacy=True) -> bool:
|
|
287
|
+
"""Tell whether this CRS is identical to another one."""
|
|
288
|
+
return (
|
|
289
|
+
# "urn:ogc:def:crs:EPSG::4326" != "urn:ogc:def:crs:OGC::CRS84"
|
|
290
|
+
# even through they both share the same srid.
|
|
291
|
+
self.srid == other.srid
|
|
292
|
+
and self.authority == other.authority
|
|
293
|
+
and (
|
|
294
|
+
# Also, legacy notations like EPSG:4326 are treated differently,
|
|
295
|
+
# unless this is configured to not make a difference.
|
|
296
|
+
not compare_legacy
|
|
297
|
+
or self.force_xy == other.force_xy
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
|
|
267
301
|
def __hash__(self):
|
|
268
302
|
"""Used to match objects in a set."""
|
|
269
303
|
return hash((self.authority, self.srid))
|
|
@@ -272,7 +306,8 @@ class CRS:
|
|
|
272
306
|
"""Generate the GDAL Spatial Reference object."""
|
|
273
307
|
if self.backends[axis_order] is None:
|
|
274
308
|
backends = list(self.backends)
|
|
275
|
-
if self.origin:
|
|
309
|
+
if self.origin and "://" not in self.origin: # avoid downloads and OGR errors
|
|
310
|
+
# Passing the origin helps to detect CRS84 strings
|
|
276
311
|
backends[axis_order] = _get_spatial_reference(self.origin, "user", axis_order)
|
|
277
312
|
else:
|
|
278
313
|
backends[axis_order] = _get_spatial_reference(self.srid, "epsg", axis_order)
|
|
@@ -284,7 +319,12 @@ class CRS:
|
|
|
284
319
|
|
|
285
320
|
def _as_proj(self) -> pyproj.CRS:
|
|
286
321
|
"""Generate the PROJ CRS object"""
|
|
287
|
-
if
|
|
322
|
+
if (
|
|
323
|
+
isinstance(self.origin, str)
|
|
324
|
+
and not self.origin.isdigit()
|
|
325
|
+
and "epsg.xml#" not in self.origin # not supported
|
|
326
|
+
):
|
|
327
|
+
# Passing the origin helps to detect CRS84 strings
|
|
288
328
|
return _get_proj_crs_from_string(self.origin)
|
|
289
329
|
else:
|
|
290
330
|
return _get_proj_crs_from_authority(self.authority, self.srid)
|
|
@@ -293,7 +333,7 @@ class CRS:
|
|
|
293
333
|
self,
|
|
294
334
|
geometry: AnyGeometry,
|
|
295
335
|
clone=False,
|
|
296
|
-
axis_order: AxisOrder =
|
|
336
|
+
axis_order: AxisOrder | None = None,
|
|
297
337
|
) -> AnyGeometry | None:
|
|
298
338
|
"""Transform the geometry using this coordinate reference.
|
|
299
339
|
|
|
@@ -322,6 +362,12 @@ class CRS:
|
|
|
322
362
|
* PostGIS stores the data in x/y.
|
|
323
363
|
* WFS 1.0 used x/y, WFS 1.3 used y/x except for ``EPSG:4326``.
|
|
324
364
|
|
|
365
|
+
When receiving legacy notations (e.g. ``EPSG:4326`` instead of ``urn:ogc:def:crs:EPSG::4326``),
|
|
366
|
+
the data is still projected in legacy ordering, unless ``GISSERVER_FORCE_XY_...`` is disabled.
|
|
367
|
+
This reflects the design of `GeoServer Axis Ordering
|
|
368
|
+
<https://docs.geoserver.org/stable/en/user/services/wfs/axis_order.html>`_
|
|
369
|
+
to have maximum interoperability with legacy/JavaScript clients.
|
|
370
|
+
|
|
325
371
|
After GDAL/OGR changed the axis orientation, that information is
|
|
326
372
|
lost when the return value is loaded back into GEOS.
|
|
327
373
|
To address this, :meth:`tag_geometry` is called on the result.
|
|
@@ -331,6 +377,11 @@ class CRS:
|
|
|
331
377
|
For GEOS->GDAL->GEOS conversions, this makes no difference in efficiency.
|
|
332
378
|
:param axis_order: Which axis ordering to convert the geometry into (depends on the use-case).
|
|
333
379
|
"""
|
|
380
|
+
if axis_order is None:
|
|
381
|
+
# This transforms by default to WFS 2 axis ordering (e.g. latitude/longitude),
|
|
382
|
+
# unless a legacy notation is used (e.g. EPSG:4326).
|
|
383
|
+
axis_order = AxisOrder.TRADITIONAL if self.force_xy else AxisOrder.AUTHORITY
|
|
384
|
+
|
|
334
385
|
if isinstance(geometry, OGRGeometry):
|
|
335
386
|
transform = _get_coord_transform(geometry.srs, self._as_gdal(axis_order=axis_order))
|
|
336
387
|
return geometry.transform(transform, clone=clone)
|
|
@@ -377,12 +428,12 @@ class CRS:
|
|
|
377
428
|
This also makes sure that requests which use the same URN will get our CRS object
|
|
378
429
|
version, instead of a fresh new one.
|
|
379
430
|
"""
|
|
380
|
-
if self.authority == "EPSG":
|
|
431
|
+
if self.authority == "EPSG" and self.srid != 4326:
|
|
381
432
|
# Only register for EPSG to avoid conflicting axis ordering issues.
|
|
382
|
-
# (WGS84 and CRS84 both use srid 4326)
|
|
433
|
+
# (WGS84 and CRS84 both use srid 4326, and the 'EPSG:4326' notation is treated as legacy)
|
|
383
434
|
_COMMON_CRS_BY_SRID[self.srid] = self
|
|
384
435
|
|
|
385
|
-
|
|
436
|
+
_COMMON_CRS_BY_STR[str(self)] = self
|
|
386
437
|
|
|
387
438
|
|
|
388
439
|
#: Worldwide GPS, latitude/longitude (y/x). https://epsg.io/4326
|
|
@@ -42,16 +42,22 @@ class AsGML(functions.AsGML):
|
|
|
42
42
|
precision=conf.GISSERVER_DB_PRECISION,
|
|
43
43
|
envelope=False,
|
|
44
44
|
is_latlon=False,
|
|
45
|
+
long_urn=False,
|
|
45
46
|
**extra,
|
|
46
47
|
):
|
|
47
48
|
# Note that Django's AsGml the defaults are: version=2, precision=8
|
|
48
49
|
super().__init__(expression, version, precision, **extra)
|
|
49
50
|
self.envelope = envelope
|
|
50
51
|
self.is_latlon = is_latlon
|
|
52
|
+
self.long_urn = long_urn
|
|
51
53
|
|
|
52
54
|
def as_postgresql(self, compiler, connection, **extra_context):
|
|
53
55
|
# Fill options parameter (https://postgis.net/docs/ST_AsGML.html)
|
|
54
|
-
options =
|
|
56
|
+
options = 0
|
|
57
|
+
if self.long_urn:
|
|
58
|
+
options |= 1 # long CRS urn
|
|
59
|
+
if self.envelope:
|
|
60
|
+
options |= 32 # bbox
|
|
55
61
|
if self.is_latlon:
|
|
56
62
|
# PostGIS provides the data in longitude/latitude format (east/north to look like x/y).
|
|
57
63
|
# However, WFS 2.0 fixed their axis by following the authority. The ST_AsGML() doesn't
|
|
@@ -846,19 +846,24 @@ class FeatureType:
|
|
|
846
846
|
|
|
847
847
|
def resolve_crs(self, crs: CRS, locator="") -> CRS:
|
|
848
848
|
"""Check a parsed CRS against the list of supported types."""
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
849
|
+
for candidate in self.supported_crs:
|
|
850
|
+
# Not using self.supported_crs.index(crs), as that depends on CRS.__eq__():
|
|
851
|
+
if candidate.matches(crs, compare_legacy=False):
|
|
852
|
+
if candidate.force_xy != crs.force_xy:
|
|
853
|
+
# user provided legacy CRS, allow output in legacy CRS
|
|
854
|
+
return crs
|
|
855
|
+
else:
|
|
856
|
+
# Replace the parsed CRS with the declared one.
|
|
857
|
+
return candidate
|
|
858
|
+
|
|
859
|
+
# No match found
|
|
860
|
+
if conf.GISSERVER_SUPPORTED_CRS_ONLY:
|
|
861
|
+
raise InvalidParameterValue(
|
|
862
|
+
f"Feature '{self.name}' does not support CRS '{crs}'.",
|
|
863
|
+
locator=locator,
|
|
864
|
+
) from None
|
|
865
|
+
else:
|
|
866
|
+
return crs
|
|
862
867
|
|
|
863
868
|
|
|
864
869
|
class HDict(dict):
|
|
@@ -27,7 +27,7 @@ class BoundingBox:
|
|
|
27
27
|
Due to the overlap between 2 types, this element is used for 2 cases:
|
|
28
28
|
|
|
29
29
|
* The ``<ows:WGS84BoundingBox>`` element for ``GetCapabilities``.
|
|
30
|
-
* The ``<gml:Envelope>`` inside an``<gml:boundedBy>`` single feature.
|
|
30
|
+
* The ``<gml:Envelope>`` inside an ``<gml:boundedBy>`` single feature.
|
|
31
31
|
|
|
32
32
|
While both classes have no common base class (and exist in different schema's),
|
|
33
33
|
their properties are identical.
|
|
@@ -11,6 +11,7 @@ much extra method calls per field. Some bits are non-DRY inlined for this reason
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import itertools
|
|
14
|
+
import re
|
|
14
15
|
from collections import defaultdict
|
|
15
16
|
from datetime import date, datetime, time, timezone
|
|
16
17
|
from decimal import Decimal as D
|
|
@@ -23,6 +24,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|
|
23
24
|
from django.db import models
|
|
24
25
|
from django.http import HttpResponse
|
|
25
26
|
|
|
27
|
+
from gisserver.crs import CRS84
|
|
26
28
|
from gisserver.db import (
|
|
27
29
|
AsGML,
|
|
28
30
|
get_db_geometry_target,
|
|
@@ -46,6 +48,7 @@ from .utils import (
|
|
|
46
48
|
)
|
|
47
49
|
|
|
48
50
|
GML_RENDER_FUNCTIONS = {}
|
|
51
|
+
RE_SRS_NAME = re.compile(r'srsName="([^"]+)"')
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
def register_geos_type(geos_type):
|
|
@@ -232,6 +235,15 @@ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
|
|
|
232
235
|
|
|
233
236
|
for sub_collection in collection.results:
|
|
234
237
|
projection = sub_collection.projection
|
|
238
|
+
if projection.output_crs.force_xy and projection.output_crs.is_north_east_order:
|
|
239
|
+
self._write(
|
|
240
|
+
"<!--\n"
|
|
241
|
+
f" NOTE: you are requesting the legacy projection notation '{tag_escape(projection.output_crs.origin)}'."
|
|
242
|
+
f" Please use '{tag_escape(projection.output_crs.urn)}' instead.\n\n"
|
|
243
|
+
" This also means output coordinates are ordered in legacy the 'west, north' axis ordering.\n"
|
|
244
|
+
"\n-->\n"
|
|
245
|
+
)
|
|
246
|
+
|
|
235
247
|
self.start_collection(sub_collection)
|
|
236
248
|
if has_multiple_collections:
|
|
237
249
|
self._write(
|
|
@@ -405,7 +417,7 @@ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
|
|
|
405
417
|
This uses an internal object type,
|
|
406
418
|
as :mod:`django.contrib.gis.geos` only provides a 4-tuple envelope.
|
|
407
419
|
"""
|
|
408
|
-
return f"""<gml:boundedBy><gml:Envelope srsDimension="2" srsName="{attr_escape(envelope.crs
|
|
420
|
+
return f"""<gml:boundedBy><gml:Envelope srsDimension="2" srsName="{attr_escape(str(envelope.crs))}">
|
|
409
421
|
<gml:lowerCorner>{envelope.min_x} {envelope.min_y}</gml:lowerCorner>
|
|
410
422
|
<gml:upperCorner>{envelope.max_x} {envelope.max_y}</gml:upperCorner>
|
|
411
423
|
</gml:Envelope></gml:boundedBy>\n"""
|
|
@@ -419,7 +431,7 @@ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
|
|
|
419
431
|
) -> str:
|
|
420
432
|
"""Normal case: 'value' is raw geometry data.."""
|
|
421
433
|
# In case this is a standalone response, this will be the top-level element, hence includes the xmlns.
|
|
422
|
-
base_attrs = f' gml:id="{attr_escape(gml_id)}" srsName="{attr_escape(projection.output_crs
|
|
434
|
+
base_attrs = f' gml:id="{attr_escape(gml_id)}" srsName="{attr_escape(str(projection.output_crs))}"{extra_xmlns}'
|
|
423
435
|
projection.output_crs.apply_to(value)
|
|
424
436
|
return self._render_gml_type(value, base_attrs=base_attrs)
|
|
425
437
|
|
|
@@ -521,11 +533,11 @@ class DBGMLRenderingMixin:
|
|
|
521
533
|
id_pos = gml_tag.find("gml:id=")
|
|
522
534
|
if id_pos == -1:
|
|
523
535
|
# Inject
|
|
524
|
-
|
|
536
|
+
gml = f'{gml_tag} gml:id="{attr_escape(gml_id)}"{extra_xmlns}{value[end_pos:]}'
|
|
525
537
|
else:
|
|
526
538
|
# Replace
|
|
527
539
|
end_pos1 = gml_tag.find('"', id_pos + 8)
|
|
528
|
-
|
|
540
|
+
gml = (
|
|
529
541
|
f"{gml_tag[:id_pos]}"
|
|
530
542
|
f'gml:id="{attr_escape(gml_id)}'
|
|
531
543
|
f"{value[end_pos1:end_pos]}" # from " right until >
|
|
@@ -533,6 +545,23 @@ class DBGMLRenderingMixin:
|
|
|
533
545
|
f"{value[end_pos:]}" # from > and beyond
|
|
534
546
|
)
|
|
535
547
|
|
|
548
|
+
return self._fix_db_crs_name(projection, gml)
|
|
549
|
+
|
|
550
|
+
def _fix_db_crs_name(self, projection: FeatureProjection, gml: str) -> str:
|
|
551
|
+
if projection.output_crs.force_xy:
|
|
552
|
+
# When legacy output is used, make sure the srsName matches the input.
|
|
553
|
+
# PostgreSQL will still generate the EPSG:xxxx notation.
|
|
554
|
+
return gml.replace(
|
|
555
|
+
'srsName="EPSG:', 'srsName="http://www.opengis.net/gml/srs/epsg.xml#', 1
|
|
556
|
+
)
|
|
557
|
+
elif projection.output_crs == CRS84:
|
|
558
|
+
# Fix PostgreSQL not knowing it's CRS84 or WGS84 (both srid 4326)
|
|
559
|
+
return gml.replace(
|
|
560
|
+
'srsName="urn:ogc:def:crs:EPSG::4326"', 'srsName="urn:ogc:def:crs:OGC::CRS84"', 1
|
|
561
|
+
)
|
|
562
|
+
else:
|
|
563
|
+
return gml
|
|
564
|
+
|
|
536
565
|
|
|
537
566
|
class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
538
567
|
"""Faster GetFeature renderer that uses the database to render GML 3.2"""
|
|
@@ -553,12 +582,14 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
|
553
582
|
# Retrieve geometries as pre-rendered instead.
|
|
554
583
|
# Only take the geometries of the current level.
|
|
555
584
|
# The annotations for relations will be handled by prefetches and get_prefetch_queryset()
|
|
585
|
+
use_modern = not projection.output_crs.force_xy
|
|
556
586
|
return replace_queryset_geometries(
|
|
557
587
|
queryset,
|
|
558
588
|
projection.geometry_elements,
|
|
559
589
|
projection.output_crs,
|
|
560
590
|
AsGML,
|
|
561
|
-
is_latlon=projection.output_crs.is_north_east_order,
|
|
591
|
+
is_latlon=use_modern and projection.output_crs.is_north_east_order,
|
|
592
|
+
long_urn=use_modern,
|
|
562
593
|
)
|
|
563
594
|
|
|
564
595
|
def get_prefetch_queryset(
|
|
@@ -572,12 +603,14 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
|
572
603
|
return None
|
|
573
604
|
|
|
574
605
|
# Find which fields are GML elements
|
|
606
|
+
use_modern = not projection.output_crs.force_xy
|
|
575
607
|
return replace_queryset_geometries(
|
|
576
608
|
queryset,
|
|
577
609
|
feature_relation.geometry_elements,
|
|
578
610
|
projection.output_crs,
|
|
579
611
|
AsGML,
|
|
580
|
-
is_latlon=projection.output_crs.is_north_east_order,
|
|
612
|
+
is_latlon=use_modern and projection.output_crs.is_north_east_order,
|
|
613
|
+
long_urn=use_modern,
|
|
581
614
|
)
|
|
582
615
|
|
|
583
616
|
def get_db_envelope_as_gml(self, projection: FeatureProjection, queryset) -> AsGML:
|
|
@@ -586,10 +619,12 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
|
586
619
|
This also avoids offloads the geometry union calculation to the DB.
|
|
587
620
|
"""
|
|
588
621
|
geo_fields_union = self._get_geometries_union(projection, queryset)
|
|
622
|
+
use_modern = not projection.output_crs.force_xy
|
|
589
623
|
return AsGML(
|
|
590
624
|
geo_fields_union,
|
|
591
625
|
envelope=True,
|
|
592
|
-
is_latlon=projection.output_crs.is_north_east_order,
|
|
626
|
+
is_latlon=use_modern and projection.output_crs.is_north_east_order,
|
|
627
|
+
long_urn=use_modern,
|
|
593
628
|
)
|
|
594
629
|
|
|
595
630
|
def _get_geometries_union(self, projection: FeatureProjection, queryset):
|
|
@@ -620,6 +655,8 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
|
|
|
620
655
|
gml = instance._as_envelope_gml
|
|
621
656
|
if gml is None:
|
|
622
657
|
return
|
|
658
|
+
|
|
659
|
+
gml = self._fix_db_crs_name(projection, gml)
|
|
623
660
|
else:
|
|
624
661
|
value = get_db_rendered_geometry(instance, geo_element, AsGML)
|
|
625
662
|
if value is None:
|
|
@@ -750,11 +787,13 @@ class DBGML32ValueRenderer(DBGMLRenderingMixin, GML32ValueRenderer):
|
|
|
750
787
|
if element.type.is_geometry:
|
|
751
788
|
# Add 'gml_member' to point to the pre-rendered GML version.
|
|
752
789
|
geo_element = cast(GeometryXsdElement, element)
|
|
790
|
+
use_modern = not projection.output_crs.force_xy
|
|
753
791
|
return queryset.values(
|
|
754
792
|
"pk",
|
|
755
793
|
gml_member=AsGML(
|
|
756
794
|
get_db_geometry_target(geo_element, projection.output_crs),
|
|
757
|
-
is_latlon=projection.output_crs.is_north_east_order,
|
|
795
|
+
is_latlon=use_modern and projection.output_crs.is_north_east_order,
|
|
796
|
+
long_urn=use_modern,
|
|
758
797
|
),
|
|
759
798
|
)
|
|
760
799
|
else:
|
|
@@ -6,7 +6,7 @@ For example, the FES filter syntax can be processed into objects that build an O
|
|
|
6
6
|
Python classes can inherit :class:`AstNode` and register themselves as the parser/handler
|
|
7
7
|
for a given tag. Both normal Python classes and dataclass work,
|
|
8
8
|
as long as it has an :meth:`AstNode.from_xml` class method.
|
|
9
|
-
The custom
|
|
9
|
+
The custom ``from_xml()`` method should copy the XML data into local attributes.
|
|
10
10
|
|
|
11
11
|
Next, when :meth:`TagRegistry.node_from_xml` is called,
|
|
12
12
|
it will detect which class the XML Element refers to and initialize it using the ``from_xml()`` call.
|
|
@@ -58,7 +58,7 @@ class TagNameEnum(Enum):
|
|
|
58
58
|
"""Cast the element tag name into the enum member.
|
|
59
59
|
|
|
60
60
|
This translates the element name
|
|
61
|
-
such as``{http://www.opengis.net/fes/2.0}PropertyIsEqualTo``
|
|
61
|
+
such as ``{http://www.opengis.net/fes/2.0}PropertyIsEqualTo``
|
|
62
62
|
into a ``PropertyIsEqualTo`` member.
|
|
63
63
|
"""
|
|
64
64
|
tag_name = element.tag
|
|
@@ -90,6 +90,7 @@ class AstNode:
|
|
|
90
90
|
an XML tag into a Python (data) class.
|
|
91
91
|
"""
|
|
92
92
|
|
|
93
|
+
#: Default namespace of the element and subclasses, if not given by ``@tag_registry.register()``.
|
|
93
94
|
xml_ns: xmlns | str | None = None
|
|
94
95
|
|
|
95
96
|
_xml_tags = []
|
|
@@ -99,6 +100,8 @@ class AstNode:
|
|
|
99
100
|
"""Tell the default tag by which this class is registered"""
|
|
100
101
|
return cls._xml_tags[0]
|
|
101
102
|
|
|
103
|
+
xml_name.__doc__ = "Tell the default tag by which this class is registered"
|
|
104
|
+
|
|
102
105
|
def __init_subclass__(cls):
|
|
103
106
|
# Each class level has a fresh list of supported child tags.
|
|
104
107
|
cls._xml_tags = []
|
|
@@ -124,7 +127,7 @@ class AstNode:
|
|
|
124
127
|
|
|
125
128
|
@classmethod
|
|
126
129
|
def get_tag_names(cls) -> list[str]:
|
|
127
|
-
"""Provide all known
|
|
130
|
+
"""Provide all known XML tags that this code can parse."""
|
|
128
131
|
try:
|
|
129
132
|
# Because a cached class property is hard to build
|
|
130
133
|
return _KNOWN_TAG_NAMES[cls]
|
|
@@ -233,14 +236,14 @@ class TagRegistry:
|
|
|
233
236
|
def node_from_xml(self, element: NSElement, allowed_types: tuple[type[A]] | None = None) -> A:
|
|
234
237
|
"""Find the ``AstNode`` subclass that corresponds to the given XML element,
|
|
235
238
|
and initialize it with the element. This is a convenience shortcut.
|
|
236
|
-
|
|
239
|
+
"""
|
|
237
240
|
node_class = self.resolve_class(element, allowed_types)
|
|
238
241
|
return node_class.from_xml(element)
|
|
239
242
|
|
|
240
243
|
def resolve_class(
|
|
241
244
|
self, element: NSElement, allowed_types: tuple[type[A]] | None = None
|
|
242
245
|
) -> type[A]:
|
|
243
|
-
"""Find the
|
|
246
|
+
"""Find the :class:`AstNode` subclass that corresponds to the given XML element."""
|
|
244
247
|
try:
|
|
245
248
|
node_class = self.parsers[element.tag]
|
|
246
249
|
except KeyError:
|
|
@@ -227,7 +227,7 @@ class Operator(AstNode):
|
|
|
227
227
|
|
|
228
228
|
@dataclass
|
|
229
229
|
class IdOperator(Operator):
|
|
230
|
-
"""List of :class:`~gisserver.parsers.fes20.identifers.ResourceId
|
|
230
|
+
"""List of :class:`~gisserver.parsers.fes20.identifers.ResourceId` objects.
|
|
231
231
|
|
|
232
232
|
A ``<fes:Filter>`` only has a single predicate.
|
|
233
233
|
Hence, this operator is used to wrap the ``<fes:ResourceId>`` elements in the syntax::
|
|
@@ -89,7 +89,13 @@ class GEOSGMLGeometry(AbstractGeometry):
|
|
|
89
89
|
# This avoids having to support the whole GEOS logic.
|
|
90
90
|
geos_data = GEOSGeometry.from_gml(tostring(element))
|
|
91
91
|
geos_data.srid = srs.srid
|
|
92
|
-
|
|
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)
|
|
93
99
|
return cls(srs=srs, geos_data=geos_data)
|
|
94
100
|
|
|
95
101
|
def __repr__(self):
|
|
@@ -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=...
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/* Common page styling */
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
This general CSS that can be disabled
|
|
5
|
+
by removing the .meta-page in the overwritten templates
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.meta-page {
|
|
9
|
+
margin: 2rem 2rem 4rem 2rem;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.meta-page h2 { margin: 2rem 0 0.5rem 0; }
|
|
13
|
+
.meta-page h3 { margin: 1rem 0 0.5rem 0; }
|
|
14
|
+
.meta-page p { margin: 0 0 1rem 0; }
|
|
15
|
+
|
|
16
|
+
.meta-page code { color: #000; }
|
|
17
|
+
|
|
18
|
+
.meta-page dl {
|
|
19
|
+
display: grid;
|
|
20
|
+
grid-gap: 2px 16px;
|
|
21
|
+
grid-template-columns: max-content;
|
|
22
|
+
}
|
|
23
|
+
.meta-page dd { grid-column-start: 2; margin: 0; }
|
|
24
|
+
|
|
25
|
+
.meta-page .wfs-feature-type { margin-bottom: 2.5rem; }
|
|
26
|
+
.meta-page .field-name { min-width: 20%; }
|
|
27
|
+
.meta-page .field-type { min-width: 25%; }
|
|
28
|
+
|
|
29
|
+
/* application-specific */
|
|
30
|
+
p.connect-url { margin-left: 2.5rem; font-weight: bold; }
|
|
31
|
+
|
|
32
|
+
tbody th { text-align: left; padding-right: 2rem; }
|
|
33
|
+
tbody td { padding-right: 2rem; }
|
|
34
|
+
.table > tbody > tr.complex-level-1 > th { padding-left: 20px; }
|
|
35
|
+
.table > tbody > tr.complex-level-2 > th { padding-left: 40px; }
|
|
36
|
+
.table > tbody > tr.complex-level-3 > th { padding-left: 60px; }
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
|
5
5
|
<title>{% block title %}{{ service_description.title }} {{ accept_operations|dictsort:0|join:"/" }}{% endblock %}</title>
|
|
6
|
-
{% block link %}
|
|
6
|
+
{% block link %}
|
|
7
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
|
|
8
|
+
<link rel="stylesheet" type="text/css" href="{% static 'gisserver/index.css' %}">
|
|
9
|
+
{% endblock %}
|
|
7
10
|
{% block extrahead %}{% endblock %}
|
|
8
11
|
</head>
|
|
9
12
|
<body class="{% block body-class %}meta-page{% endblock %}">
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{% if wfs_features %}
|
|
13
13
|
<h2>{% translate "WFS Feature Types" %}</h2>
|
|
14
14
|
{% for feature_type in wfs_features %}
|
|
15
|
-
<article>{% include "gisserver/wfs/feature_type.html" %}</article>
|
|
15
|
+
<article class="wfs-feature-type">{% include "gisserver/wfs/feature_type.html" %}</article>
|
|
16
16
|
{% endfor %}
|
|
17
17
|
{% endif %}
|
|
18
18
|
{% endblock %}
|
|
@@ -18,5 +18,5 @@
|
|
|
18
18
|
{% if connect_url %}
|
|
19
19
|
<h2>{% translate "Using This WFS" %}</h2>
|
|
20
20
|
<p>{% translate "Add the following URL to your GIS application:" %}</p>
|
|
21
|
-
<p class="connect-url"><
|
|
21
|
+
<p class="connect-url"><samp>{{ connect_url }}</samp></p>
|
|
22
22
|
{% endif %}
|
{django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/templates/gisserver/wfs/feature_type.html
RENAMED
|
@@ -13,9 +13,13 @@
|
|
|
13
13
|
<dt>{% translate "Supported CRS" %}:</dt>
|
|
14
14
|
<dd>
|
|
15
15
|
{% if GISSERVER_SUPPORTED_CRS_ONLY %}
|
|
16
|
-
{% blocktranslate with default_crs=feature_type.crs
|
|
16
|
+
{% blocktranslate trimmed with default_crs=feature_type.crs supported_crs=feature_type.supported_crs|join:", " %}
|
|
17
|
+
{{ supported_crs }}, and all others. Source data uses {{ default_crs }}.
|
|
18
|
+
{% endblocktranslate %}
|
|
17
19
|
{% else %}
|
|
18
|
-
{
|
|
20
|
+
{% blocktranslate trimmed with default_crs=feature_type.crs supported_crs=feature_type.supported_crs|join:", " %}
|
|
21
|
+
{{ supported_crs }}. Source data uses {{ default_crs }}.
|
|
22
|
+
{% endblocktranslate %}
|
|
19
23
|
{% endif %}
|
|
20
24
|
</dd>
|
|
21
25
|
{% if wfs_output_formats %}
|
|
@@ -35,6 +39,11 @@
|
|
|
35
39
|
{% block fields %}
|
|
36
40
|
<p>{% translate "The following fields are available:" %}</p>
|
|
37
41
|
<table class="table table-striped">
|
|
42
|
+
<colgroup>
|
|
43
|
+
<col class="field-name" />
|
|
44
|
+
<col class="field-type" />
|
|
45
|
+
<col class="field-description" />
|
|
46
|
+
</colgroup>
|
|
38
47
|
<thead><tr><th>{% translate "Field Name" %}</th><th>{% translate "Type" %}</th><th>{% translate "Description" %}</th></tr></thead>
|
|
39
48
|
<tbody>
|
|
40
49
|
{% for field in feature_type.fields %}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
These types are the internal schema definition, and the foundation for all output generation.
|
|
4
4
|
|
|
5
5
|
The end-users of this library typically create a WFS feature type definition by using
|
|
6
|
-
the :class
|
|
6
|
+
the :class:`~gisserver.features.FeatureType` / :class:`~gisserver.features.FeatureField` classes.
|
|
7
7
|
|
|
8
8
|
The feature type classes use the model metadata to construct the internal XMLSchema structure.
|
|
9
9
|
Nearly all WFS requests are handled by walking this structure (like ``DescribeFeatureType``
|
|
@@ -57,6 +57,7 @@ __all__ = [
|
|
|
57
57
|
"GeometryXsdElement",
|
|
58
58
|
"GmlIdAttribute",
|
|
59
59
|
"GmlNameElement",
|
|
60
|
+
"GmlBoundedByElement",
|
|
60
61
|
"ORMPath",
|
|
61
62
|
"XPathMatch",
|
|
62
63
|
"XsdAnyType",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.1" # follows PEP440
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/* general CSS that can be disabled by removing the .meta-page in the overwritten templates */
|
|
2
|
-
|
|
3
|
-
.meta-page {
|
|
4
|
-
font-family: Arial, sans-serif;
|
|
5
|
-
line-height: 1.5;
|
|
6
|
-
margin: 0 2rem 4rem 2rem;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
.meta-page h2 { margin: 2rem 0 0.5rem 0; }
|
|
10
|
-
.meta-page h3 { margin: 1rem 0 0.5rem 0; }
|
|
11
|
-
.meta-page p { margin: 0 0 1rem 0; }
|
|
12
|
-
|
|
13
|
-
/* application-specific */
|
|
14
|
-
.connect-url { margin-left: 4rem; }
|
|
15
|
-
|
|
16
|
-
tbody th { text-align: left; padding-right: 2rem; }
|
|
17
|
-
tbody td { padding-right: 2rem; }
|
|
18
|
-
.table > tbody > tr.complex-level-1 > th { padding-left: 20px; }
|
|
19
|
-
.table > tbody > tr.complex-level-2 > th { padding-left: 40px; }
|
|
20
|
-
.table > tbody > tr.complex-level-3 > th { padding-left: 60px; }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_gisserver-2.1 → django_gisserver-2.1.1}/django_gisserver.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/management/commands/loadgeojson.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_gisserver-2.1 → django_gisserver-2.1.1}/gisserver/templates/gisserver/wfs/feature_field.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|