django-gisserver 2.1__tar.gz → 2.1.2__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.
Files changed (80) hide show
  1. {django_gisserver-2.1 → django_gisserver-2.1.2}/CHANGES.md +19 -23
  2. {django_gisserver-2.1/django_gisserver.egg-info → django_gisserver-2.1.2}/PKG-INFO +2 -1
  3. {django_gisserver-2.1 → django_gisserver-2.1.2/django_gisserver.egg-info}/PKG-INFO +2 -1
  4. {django_gisserver-2.1 → django_gisserver-2.1.2}/django_gisserver.egg-info/SOURCES.txt +1 -0
  5. {django_gisserver-2.1 → django_gisserver-2.1.2}/django_gisserver.egg-info/requires.txt +1 -0
  6. django_gisserver-2.1.2/gisserver/__init__.py +1 -0
  7. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/conf.py +23 -1
  8. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/crs.py +84 -33
  9. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/db.py +7 -1
  10. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/features.py +18 -13
  11. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/geometries.py +1 -1
  12. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/output/gml32.py +47 -8
  13. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/ast.py +8 -5
  14. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/fes20/operators.py +1 -1
  15. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/gml/geometries.py +7 -1
  16. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/wfs20/adhoc.py +3 -1
  17. django_gisserver-2.1.2/gisserver/static/gisserver/bootstrap-5.3.6.min.css +6 -0
  18. django_gisserver-2.1.2/gisserver/static/gisserver/index.css +36 -0
  19. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/templates/gisserver/base.html +4 -1
  20. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/templates/gisserver/index.html +1 -1
  21. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/templates/gisserver/service_description.html +1 -1
  22. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/templates/gisserver/wfs/feature_type.html +11 -2
  23. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/types.py +2 -1
  24. {django_gisserver-2.1 → django_gisserver-2.1.2}/setup.py +1 -0
  25. django_gisserver-2.1/gisserver/__init__.py +0 -1
  26. django_gisserver-2.1/gisserver/static/gisserver/index.css +0 -20
  27. {django_gisserver-2.1 → django_gisserver-2.1.2}/LICENSE +0 -0
  28. {django_gisserver-2.1 → django_gisserver-2.1.2}/MANIFEST.in +0 -0
  29. {django_gisserver-2.1 → django_gisserver-2.1.2}/README.md +0 -0
  30. {django_gisserver-2.1 → django_gisserver-2.1.2}/django_gisserver.egg-info/dependency_links.txt +0 -0
  31. {django_gisserver-2.1 → django_gisserver-2.1.2}/django_gisserver.egg-info/not-zip-safe +0 -0
  32. {django_gisserver-2.1 → django_gisserver-2.1.2}/django_gisserver.egg-info/top_level.txt +0 -0
  33. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/compat.py +0 -0
  34. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/exceptions.py +0 -0
  35. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/extensions/__init__.py +0 -0
  36. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/extensions/functions.py +0 -0
  37. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/extensions/queries.py +0 -0
  38. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/management/__init__.py +0 -0
  39. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/management/commands/__init__.py +0 -0
  40. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/management/commands/loadgeojson.py +0 -0
  41. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/operations/__init__.py +0 -0
  42. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/operations/base.py +0 -0
  43. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/operations/wfs20.py +0 -0
  44. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/output/__init__.py +0 -0
  45. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/output/base.py +0 -0
  46. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/output/csv.py +0 -0
  47. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/output/geojson.py +0 -0
  48. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/output/iters.py +0 -0
  49. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/output/results.py +0 -0
  50. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/output/stored.py +0 -0
  51. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/output/utils.py +0 -0
  52. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/output/xmlschema.py +0 -0
  53. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/__init__.py +0 -0
  54. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/fes20/__init__.py +0 -0
  55. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/fes20/expressions.py +0 -0
  56. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/fes20/filters.py +0 -0
  57. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/fes20/identifiers.py +0 -0
  58. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/fes20/lookups.py +0 -0
  59. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/fes20/sorting.py +0 -0
  60. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/gml/__init__.py +0 -0
  61. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/gml/base.py +0 -0
  62. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/ows/__init__.py +0 -0
  63. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/ows/kvp.py +0 -0
  64. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/ows/requests.py +0 -0
  65. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/query.py +0 -0
  66. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/values.py +0 -0
  67. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/wfs20/__init__.py +0 -0
  68. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/wfs20/base.py +0 -0
  69. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/wfs20/projection.py +0 -0
  70. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/wfs20/requests.py +0 -0
  71. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/wfs20/stored.py +0 -0
  72. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/parsers/xml.py +0 -0
  73. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/projection.py +0 -0
  74. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +0 -0
  75. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/templates/gisserver/wfs/feature_field.html +0 -0
  76. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/templatetags/__init__.py +0 -0
  77. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/templatetags/gisserver_tags.py +0 -0
  78. {django_gisserver-2.1 → django_gisserver-2.1.2}/gisserver/views.py +0 -0
  79. {django_gisserver-2.1 → django_gisserver-2.1.2}/pyproject.toml +0 -0
  80. {django_gisserver-2.1 → django_gisserver-2.1.2}/setup.cfg +0 -0
@@ -1,3 +1,17 @@
1
+ # 2025-09-10 (2.1.2)
2
+
3
+ * Stop using CDN for bootstrap stylesheet, instead use a static local file.
4
+ This in order for the server to work in an airgapped environment (PR#72).
5
+
6
+ # 2025-06-06 (2.1.1)
7
+
8
+ * Fixed using legacy longitude/latitude rendering for `EPSG:4326` and `:http://www.opengis.net/gml/srs/epsg.xml#xxxx` SRS names.
9
+ * Improved styling for the default HTML page.
10
+
11
+ This increases interoperability with web-based clients and legacy libraries. GeoServer applies the same heuristic.
12
+ 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`
13
+ are not affected. If needed, this behavior can be disabled using `GISSERVER_FORCE_XY_EPSG_4326=False` and `GISSERVER_FORCE_XY_OLD_CRS=False`.
14
+
1
15
  # 2025-05-29 (2.1)
2
16
 
3
17
  * Added support for `ArrayField` field in CSV exports.
@@ -16,7 +30,6 @@
16
30
  * Fixed crash when receiving WFS 1.0 POST requests.
17
31
  * Fixed building readthedocs.
18
32
 
19
-
20
33
  # 2025-04-28 (2.0)
21
34
 
22
35
  * Added support for XML POST requests.
@@ -86,7 +99,6 @@ the notable API changes are:
86
99
  * `XsdElement.is_geometry` was unneeded, use `XsdElement.type.is_geometry` now.
87
100
  * `XsdElement.orm_path` points to the absolute path, and `XsdElement.local_orm_path` to the relative path.
88
101
 
89
-
90
102
  # 2024-11-25 (1.5.0)
91
103
 
92
104
  * Added `PROPERTYNAME` support
@@ -120,10 +132,11 @@ the notable API changes are:
120
132
  * Django 5 support added.
121
133
 
122
134
  ## Contributors
135
+
123
136
  We would like to thank the following contributors
124
137
  for their work on this release.
125
138
 
126
- - [tomdtp](https://github.com/tomdtp)
139
+ * [tomdtp](https://github.com/tomdtp)
127
140
 
128
141
  # 2023-06-08 (1.2.7)
129
142
 
@@ -143,25 +156,21 @@ for their work on this release.
143
156
 
144
157
  * Fixed type assertion when `django.contrib.postgres` was not installed.
145
158
 
146
-
147
159
  # 2022-09-07 (1.2.3)
148
160
 
149
161
  * Added "geojson" as output format alias in `GetCapabilities` for ESRI ArcGIS online.
150
162
 
151
-
152
163
  # 2022-07-28 (1.2.2)
153
164
 
154
165
  * Optimized response writing, buffering provement gave ~12-15% speedup.
155
166
  * Optimized GML response, reduced response size by ~9% by removing whitespace.
156
167
 
157
-
158
168
  # 2022-04-13 (1.2.1)
159
169
 
160
170
  * Fixed regression for auto-correcting xmlns for `<Filter>` tags that have leading whitespace.
161
171
  * Fixed weird crashes when geometry field is not provided.
162
172
  * Simplify `FeatureType.geometry_field` logic.
163
173
 
164
-
165
174
  # 2022-04-11 (1.2)
166
175
 
167
176
  * Added support for `maxOccurs=unbounded` elements (M2M, reverse foreign key, array fields).
@@ -182,7 +191,6 @@ for their work on this release.
182
191
  * Various code cleanups.
183
192
  * Dropped Python 3.6 support.
184
193
 
185
-
186
194
  # 2021-05-17 (1.1.3)
187
195
 
188
196
  * Included Django 3.2 in test matrix.
@@ -191,21 +199,18 @@ for their work on this release.
191
199
  * Updated pre-commit hooks.
192
200
  * Drop universal wheel declaration (Python 2 is no longer supported)
193
201
 
194
-
195
202
  # 2020-12-22 (1.1.2)
196
203
 
197
204
  * Fixed double ``>`` sign in ``<Filter xml..>>`` code when namespaces were auto-corrected.
198
205
  * Fixed basic ``<Beyond>`` support for distance queries.
199
206
  * Fixed parameter name for ``round()`` function.
200
207
 
201
-
202
208
  # 2020-08-19 (1.1.1)
203
209
 
204
210
  * Improve HTML page with "Using This WFS" section.
205
211
  * Fixed Django 3.1 compatibility.
206
212
  * Improved error message for `<fes:PropertyIsLike>` operator when comparing against a `<fes:ValueReference>` instead of `<fes:Literal>`.
207
213
 
208
-
209
214
  # 2020-08-13 (1.1)
210
215
 
211
216
  * Added browsable HTML views for WFS views, which can be extended/overwritten.
@@ -213,7 +218,6 @@ for their work on this release.
213
218
  * Added "abstract" property to field classes to provide a description.
214
219
  * Consider `?SERVICE=WFS` as default for `WFSView` views.
215
220
 
216
-
217
221
  # 2020-07-21 (1.0)
218
222
 
219
223
  * Added `FeatureType.show_name_field` to show/hide `<gml:name>` and GeoJSON `geometry_name`.
@@ -235,12 +239,10 @@ for their work on this release.
235
239
  * Fixed exception message for bad SRID's.
236
240
  * Internal code reorganizations.
237
241
 
238
-
239
242
  # 2020-07-09 (0.9.1)
240
243
 
241
244
  * Fixed `GetPropertyValue` calls for non-db optimized rendering.
242
245
 
243
-
244
246
  # 2020-07-09 (0.9)
245
247
 
246
248
  * Added support for nested, flattened fields.
@@ -260,7 +262,6 @@ for their work on this release.
260
262
  * Optimize feature retrieval, only fetch the actual fields being displayed.
261
263
  * Compactified XML output headers.
262
264
 
263
-
264
265
  # 2020-07-06 (0.8.4)
265
266
 
266
267
  * Added field renaming support with `field(..., model_attribute=...)`.
@@ -280,7 +281,6 @@ for their work on this release.
280
281
  * Fix exposed WFS capabilities to pass cite WFS Simple conformance
281
282
  * Fixed SORTBY parameter to handle renamed fields.
282
283
 
283
-
284
284
  # 2020-06-30 (0.8.3)
285
285
 
286
286
  * Added `GISSERVER_CAPABILITIES_BOUNDING_BOX` setting.
@@ -288,13 +288,11 @@ for their work on this release.
288
288
  * Fixed `DescribeFeatureType` to return all types when TYPENAMES is not provided.
289
289
  * Advertise `ImplementsMinimumXPath` in `GetCapabilities` for cite testing (other servers also do this while being incomplete).
290
290
 
291
-
292
291
  # 2020-06-30 (0.8.2)
293
292
 
294
293
  * Improve XPath matching support, allow "app:" prefix and root elements.
295
294
  * Refactored `FeatureType.resolve_element()` to return an `XPathMatch` object
296
295
 
297
-
298
296
  # 2020-06-29 (0.8.1)
299
297
 
300
298
  * Added unpaginated GeoJSON support (performance is good enough).
@@ -305,7 +303,6 @@ for their work on this release.
305
303
  * Optimized GeoJSON output by no longer selecting the other geometry fields.
306
304
  * Added shortcut properties to `XsdComplexType`: `gml_elements` and `complex_elements`.
307
305
 
308
-
309
306
  # 2020-06-25 (0.8)
310
307
 
311
308
  * Added preliminary support to render complex field types (e.g. relations).
@@ -328,7 +325,6 @@ for their work on this release.
328
325
  * Enforce using keyword-arguments on `FeatureType(...)`
329
326
  * Internal code reorganizations/cleanups.
330
327
 
331
-
332
328
  # 2020-06-08 (0.7)
333
329
 
334
330
  * Added database-based rendering for GML/GeoJSON (disable with `GISSERVER_USE_DB_RENDERING=False`).
@@ -400,6 +396,6 @@ for their work on this release.
400
396
  * First basic release that works in QGis.
401
397
  * Features:
402
398
 
403
- * WFS `GetCapabilities`
404
- * WFS `DescribeFeatureType`
405
- * WFS `GetFeature` with bbox and pagination support.
399
+ * WFS `GetCapabilities`
400
+ * WFS `DescribeFeatureType`
401
+ * WFS `GetFeature` with bbox and pagination support.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-gisserver
3
- Version: 2.1
3
+ Version: 2.1.2
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.2
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
@@ -64,6 +64,7 @@ gisserver/parsers/wfs20/base.py
64
64
  gisserver/parsers/wfs20/projection.py
65
65
  gisserver/parsers/wfs20/requests.py
66
66
  gisserver/parsers/wfs20/stored.py
67
+ gisserver/static/gisserver/bootstrap-5.3.6.min.css
67
68
  gisserver/static/gisserver/index.css
68
69
  gisserver/templates/gisserver/base.html
69
70
  gisserver/templates/gisserver/index.html
@@ -7,6 +7,7 @@ pyproj>=3.6.1
7
7
  [docs]
8
8
  Django~=5.0
9
9
  sphinxcontrib-django>=2.5
10
+ myst-parser>=3.0.1
10
11
  psycopg2-binary>=2.8.4
11
12
 
12
13
  [tests]
@@ -0,0 +1 @@
1
+ __version__ = "2.1.2" # 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()[setting] = value
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
- _COMMON_CRS_BY_URN = {}
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._from_legacy(uri)
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
- crs = cls(
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
- if known_crs := _COMMON_CRS_BY_URN.get(urn):
158
- return known_crs # Avoid object re-creation
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 _from_legacy(cls, uri):
199
- """Instantiate this class from a legacy URL"""
200
- luri = uri.lower()
201
- for head in (
202
- "epsg:",
203
- "http://www.opengis.net/def/crs/epsg/0/",
204
- "http://www.opengis.net/gml/srs/epsg.xml#",
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 luri.startswith(head):
207
- crsid = luri[len(head) :]
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"] = uri
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:`EPSG:{srid}`."""
230
- return f"EPSG:{self.srid:d}"
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
- # CRS84 is NOT equivalent to EPSG:4326.
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 isinstance(self.origin, str) and not self.origin.isdigit():
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 = AxisOrder.AUTHORITY,
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
- _COMMON_CRS_BY_URN[self.urn] = self
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 = 33 if self.envelope else 1 # 32 = bbox, 1 = long CRS urn
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
- try:
850
- pos = self.supported_crs.index(crs)
851
-
852
- # Replace the parsed CRS with the declared one, which may have a 'backend' configured.
853
- return self.supported_crs[pos]
854
- except ValueError:
855
- if conf.GISSERVER_SUPPORTED_CRS_ONLY:
856
- raise InvalidParameterValue(
857
- f"Feature '{self.name}' does not support SRID {crs.srid}.",
858
- locator=locator,
859
- ) from None
860
- else:
861
- return crs
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.