django-gisserver 2.0__tar.gz → 2.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.
Files changed (85) hide show
  1. {django-gisserver-2.0 → django_gisserver-2.1}/CHANGES.md +20 -1
  2. {django-gisserver-2.0/django_gisserver.egg-info → django_gisserver-2.1}/PKG-INFO +35 -9
  3. {django-gisserver-2.0 → django_gisserver-2.1}/README.md +4 -3
  4. {django-gisserver-2.0 → django_gisserver-2.1/django_gisserver.egg-info}/PKG-INFO +35 -9
  5. {django-gisserver-2.0 → django_gisserver-2.1}/django_gisserver.egg-info/SOURCES.txt +2 -0
  6. {django-gisserver-2.0 → django_gisserver-2.1}/django_gisserver.egg-info/requires.txt +7 -1
  7. django_gisserver-2.1/gisserver/__init__.py +1 -0
  8. django_gisserver-2.1/gisserver/crs.py +401 -0
  9. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/db.py +71 -5
  10. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/exceptions.py +106 -2
  11. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/extensions/functions.py +122 -28
  12. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/extensions/queries.py +15 -10
  13. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/features.py +44 -36
  14. django_gisserver-2.1/gisserver/geometries.py +111 -0
  15. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/management/commands/loadgeojson.py +41 -21
  16. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/operations/base.py +11 -7
  17. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/operations/wfs20.py +31 -93
  18. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/__init__.py +6 -2
  19. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/base.py +28 -13
  20. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/csv.py +18 -6
  21. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/geojson.py +7 -6
  22. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/gml32.py +43 -23
  23. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/results.py +25 -39
  24. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/utils.py +9 -2
  25. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/ast.py +171 -65
  26. django_gisserver-2.1/gisserver/parsers/fes20/__init__.py +100 -0
  27. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/fes20/expressions.py +97 -27
  28. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/fes20/filters.py +9 -6
  29. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/fes20/identifiers.py +27 -7
  30. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/fes20/lookups.py +8 -6
  31. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/fes20/operators.py +101 -49
  32. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/fes20/sorting.py +14 -6
  33. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/gml/__init__.py +10 -19
  34. django_gisserver-2.1/gisserver/parsers/gml/base.py +58 -0
  35. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/gml/geometries.py +48 -21
  36. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/ows/kvp.py +10 -2
  37. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/ows/requests.py +6 -4
  38. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/query.py +6 -2
  39. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/values.py +61 -4
  40. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/wfs20/__init__.py +2 -0
  41. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/wfs20/adhoc.py +25 -17
  42. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/wfs20/base.py +12 -7
  43. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/wfs20/projection.py +3 -3
  44. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/wfs20/requests.py +1 -0
  45. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/wfs20/stored.py +3 -2
  46. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/xml.py +12 -0
  47. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/projection.py +17 -7
  48. django_gisserver-2.1/gisserver/static/gisserver/index.css +20 -0
  49. django_gisserver-2.1/gisserver/templates/gisserver/base.html +12 -0
  50. django_gisserver-2.1/gisserver/templates/gisserver/index.html +20 -0
  51. django_gisserver-2.1/gisserver/templates/gisserver/service_description.html +22 -0
  52. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/templates/gisserver/wfs/feature_field.html +1 -1
  53. django_gisserver-2.1/gisserver/templates/gisserver/wfs/feature_type.html +45 -0
  54. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/types.py +150 -81
  55. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/views.py +47 -24
  56. {django-gisserver-2.0 → django_gisserver-2.1}/setup.py +9 -2
  57. django-gisserver-2.0/gisserver/__init__.py +0 -1
  58. django-gisserver-2.0/gisserver/geometries.py +0 -353
  59. django-gisserver-2.0/gisserver/parsers/fes20/__init__.py +0 -28
  60. django-gisserver-2.0/gisserver/parsers/gml/base.py +0 -40
  61. django-gisserver-2.0/gisserver/static/gisserver/index.css +0 -15
  62. django-gisserver-2.0/gisserver/templates/gisserver/index.html +0 -26
  63. django-gisserver-2.0/gisserver/templates/gisserver/service_description.html +0 -16
  64. django-gisserver-2.0/gisserver/templates/gisserver/wfs/feature_type.html +0 -23
  65. {django-gisserver-2.0 → django_gisserver-2.1}/LICENSE +0 -0
  66. {django-gisserver-2.0 → django_gisserver-2.1}/MANIFEST.in +0 -0
  67. {django-gisserver-2.0 → django_gisserver-2.1}/django_gisserver.egg-info/dependency_links.txt +0 -0
  68. {django-gisserver-2.0 → django_gisserver-2.1}/django_gisserver.egg-info/not-zip-safe +0 -0
  69. {django-gisserver-2.0 → django_gisserver-2.1}/django_gisserver.egg-info/top_level.txt +0 -0
  70. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/compat.py +0 -0
  71. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/conf.py +0 -0
  72. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/extensions/__init__.py +0 -0
  73. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/management/__init__.py +0 -0
  74. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/management/commands/__init__.py +0 -0
  75. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/operations/__init__.py +0 -0
  76. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/iters.py +0 -0
  77. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/stored.py +0 -0
  78. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/xmlschema.py +0 -0
  79. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/__init__.py +0 -0
  80. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/ows/__init__.py +0 -0
  81. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +0 -0
  82. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/templatetags/__init__.py +0 -0
  83. {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/templatetags/gisserver_tags.py +0 -0
  84. {django-gisserver-2.0 → django_gisserver-2.1}/pyproject.toml +0 -0
  85. {django-gisserver-2.0 → django_gisserver-2.1}/setup.cfg +0 -0
@@ -1,4 +1,23 @@
1
- # 2024-04-28 (2.0)
1
+ # 2025-05-29 (2.1)
2
+
3
+ * Added support for `ArrayField` field in CSV exports.
4
+ * Added support for `models.DurationField` for features.
5
+ * Added proper axis ordering handling.
6
+ * Changed built-in fes functions to match current GeoServer signatures.
7
+ * Improved error handling for invalid filter input (also fixes 500 errors).
8
+ * Improved error handling for database query errors during rendering.
9
+ * Improved error messages for unexpected tags in the XML body.
10
+ * Improved error handling for fes 1.0 arithmetic operators (e.g. typing: `date > 2020-01-01` in QGis).
11
+ * Improved HTML index page, make it easier to override and restyle.
12
+ * Improved documentation, added API documentation.
13
+ * Fixed axis ordering handling for `EPSG:4326` and other longitude/latitude CRS definitions.
14
+ * Fixed comparing to `NULL` when date/time input is invalid.
15
+ * Fixed filter comparisons for `<`, `<=`, `>`, `>=` when using a reversed "value < property" ordering.
16
+ * Fixed crash when receiving WFS 1.0 POST requests.
17
+ * Fixed building readthedocs.
18
+
19
+
20
+ # 2025-04-28 (2.0)
2
21
 
3
22
  * Added support for XML POST requests.
4
23
  * Added support for geometry elements on child nodes.
@@ -1,12 +1,11 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: django-gisserver
3
- Version: 2.0
3
+ Version: 2.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
7
7
  Author-email: opensource@edoburu.nl
8
8
  License: Mozilla Public License 2.0
9
- Platform: UNKNOWN
10
9
  Classifier: Development Status :: 5 - Production/Stable
11
10
  Classifier: Environment :: Web Environment
12
11
  Classifier: Intended Audience :: Developers
@@ -31,8 +30,36 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
31
30
  Requires: Django (>=3.2)
32
31
  Requires-Python: >=3.9
33
32
  Description-Content-Type: text/markdown
34
- Provides-Extra: tests
35
33
  License-File: LICENSE
34
+ Requires-Dist: Django>=3.2
35
+ Requires-Dist: defusedxml>=0.7.1
36
+ Requires-Dist: lru_dict>=1.1.7
37
+ Requires-Dist: orjson>=3.9.15
38
+ Requires-Dist: pyproj>=3.6.1
39
+ Provides-Extra: tests
40
+ Requires-Dist: django-environ>=0.4.5; extra == "tests"
41
+ Requires-Dist: psycopg2-binary>=2.8.4; extra == "tests"
42
+ Requires-Dist: lxml>=4.9.1; extra == "tests"
43
+ Requires-Dist: pytest>=6.2.3; extra == "tests"
44
+ Requires-Dist: pytest-django>=4.1.0; extra == "tests"
45
+ Requires-Dist: pytest-cov>=2.11.1; extra == "tests"
46
+ Provides-Extra: docs
47
+ Requires-Dist: Django~=5.0; extra == "docs"
48
+ Requires-Dist: sphinxcontrib-django>=2.5; extra == "docs"
49
+ Requires-Dist: psycopg2-binary>=2.8.4; extra == "docs"
50
+ Dynamic: author
51
+ Dynamic: author-email
52
+ Dynamic: classifier
53
+ Dynamic: description
54
+ Dynamic: description-content-type
55
+ Dynamic: home-page
56
+ Dynamic: license
57
+ Dynamic: license-file
58
+ Dynamic: provides-extra
59
+ Dynamic: requires
60
+ Dynamic: requires-dist
61
+ Dynamic: requires-python
62
+ Dynamic: summary
36
63
 
37
64
  [![Documentation](https://readthedocs.org/projects/django-gisserver/badge/?version=latest)](https://django-gisserver.readthedocs.io/en/latest/?badge=latest)
38
65
  [![Actions](https://github.com/Amsterdam/django-gisserver/actions/workflows/tests.yaml/badge.svg?branch=main)](https://github.com/Amsterdam/django-gisserver/actions/workflows/tests.yaml)
@@ -100,12 +127,13 @@ class Restaurant(models.Model):
100
127
  Write a view that exposes this model as a WFS feature:
101
128
 
102
129
  ```python
130
+ from gisserver.crs import CRS, CRS84, WEB_MERCATOR
103
131
  from gisserver.features import FeatureType, ServiceDescription
104
- from gisserver.geometries import CRS, WGS84
105
132
  from gisserver.views import WFSView
106
133
  from .models import Restaurant
107
134
 
108
- RD_NEW = CRS.from_srid(28992)
135
+ # As example, the local coordinate system for The Netherlands
136
+ RD_NEW = CRS.from_string("urn:ogc:def:crs:EPSG::28992")
109
137
 
110
138
 
111
139
  class PlacesWFSView(WFSView):
@@ -128,7 +156,7 @@ class PlacesWFSView(WFSView):
128
156
  FeatureType(
129
157
  Restaurant.objects.all(),
130
158
  fields="__all__",
131
- other_crs=[RD_NEW]
159
+ other_crs=[RD_NEW, CRS84, WEB_MERCATOR]
132
160
  ),
133
161
  ]
134
162
  ```
@@ -166,5 +194,3 @@ software developed with public money is also publicly available.
166
194
 
167
195
  This package is initially developed by the City of Amsterdam, but the tools
168
196
  and concepts created in this project can be used in any city.
169
-
170
-
@@ -64,12 +64,13 @@ class Restaurant(models.Model):
64
64
  Write a view that exposes this model as a WFS feature:
65
65
 
66
66
  ```python
67
+ from gisserver.crs import CRS, CRS84, WEB_MERCATOR
67
68
  from gisserver.features import FeatureType, ServiceDescription
68
- from gisserver.geometries import CRS, WGS84
69
69
  from gisserver.views import WFSView
70
70
  from .models import Restaurant
71
71
 
72
- RD_NEW = CRS.from_srid(28992)
72
+ # As example, the local coordinate system for The Netherlands
73
+ RD_NEW = CRS.from_string("urn:ogc:def:crs:EPSG::28992")
73
74
 
74
75
 
75
76
  class PlacesWFSView(WFSView):
@@ -92,7 +93,7 @@ class PlacesWFSView(WFSView):
92
93
  FeatureType(
93
94
  Restaurant.objects.all(),
94
95
  fields="__all__",
95
- other_crs=[RD_NEW]
96
+ other_crs=[RD_NEW, CRS84, WEB_MERCATOR]
96
97
  ),
97
98
  ]
98
99
  ```
@@ -1,12 +1,11 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: django-gisserver
3
- Version: 2.0
3
+ Version: 2.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
7
7
  Author-email: opensource@edoburu.nl
8
8
  License: Mozilla Public License 2.0
9
- Platform: UNKNOWN
10
9
  Classifier: Development Status :: 5 - Production/Stable
11
10
  Classifier: Environment :: Web Environment
12
11
  Classifier: Intended Audience :: Developers
@@ -31,8 +30,36 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
31
30
  Requires: Django (>=3.2)
32
31
  Requires-Python: >=3.9
33
32
  Description-Content-Type: text/markdown
34
- Provides-Extra: tests
35
33
  License-File: LICENSE
34
+ Requires-Dist: Django>=3.2
35
+ Requires-Dist: defusedxml>=0.7.1
36
+ Requires-Dist: lru_dict>=1.1.7
37
+ Requires-Dist: orjson>=3.9.15
38
+ Requires-Dist: pyproj>=3.6.1
39
+ Provides-Extra: tests
40
+ Requires-Dist: django-environ>=0.4.5; extra == "tests"
41
+ Requires-Dist: psycopg2-binary>=2.8.4; extra == "tests"
42
+ Requires-Dist: lxml>=4.9.1; extra == "tests"
43
+ Requires-Dist: pytest>=6.2.3; extra == "tests"
44
+ Requires-Dist: pytest-django>=4.1.0; extra == "tests"
45
+ Requires-Dist: pytest-cov>=2.11.1; extra == "tests"
46
+ Provides-Extra: docs
47
+ Requires-Dist: Django~=5.0; extra == "docs"
48
+ Requires-Dist: sphinxcontrib-django>=2.5; extra == "docs"
49
+ Requires-Dist: psycopg2-binary>=2.8.4; extra == "docs"
50
+ Dynamic: author
51
+ Dynamic: author-email
52
+ Dynamic: classifier
53
+ Dynamic: description
54
+ Dynamic: description-content-type
55
+ Dynamic: home-page
56
+ Dynamic: license
57
+ Dynamic: license-file
58
+ Dynamic: provides-extra
59
+ Dynamic: requires
60
+ Dynamic: requires-dist
61
+ Dynamic: requires-python
62
+ Dynamic: summary
36
63
 
37
64
  [![Documentation](https://readthedocs.org/projects/django-gisserver/badge/?version=latest)](https://django-gisserver.readthedocs.io/en/latest/?badge=latest)
38
65
  [![Actions](https://github.com/Amsterdam/django-gisserver/actions/workflows/tests.yaml/badge.svg?branch=main)](https://github.com/Amsterdam/django-gisserver/actions/workflows/tests.yaml)
@@ -100,12 +127,13 @@ class Restaurant(models.Model):
100
127
  Write a view that exposes this model as a WFS feature:
101
128
 
102
129
  ```python
130
+ from gisserver.crs import CRS, CRS84, WEB_MERCATOR
103
131
  from gisserver.features import FeatureType, ServiceDescription
104
- from gisserver.geometries import CRS, WGS84
105
132
  from gisserver.views import WFSView
106
133
  from .models import Restaurant
107
134
 
108
- RD_NEW = CRS.from_srid(28992)
135
+ # As example, the local coordinate system for The Netherlands
136
+ RD_NEW = CRS.from_string("urn:ogc:def:crs:EPSG::28992")
109
137
 
110
138
 
111
139
  class PlacesWFSView(WFSView):
@@ -128,7 +156,7 @@ class PlacesWFSView(WFSView):
128
156
  FeatureType(
129
157
  Restaurant.objects.all(),
130
158
  fields="__all__",
131
- other_crs=[RD_NEW]
159
+ other_crs=[RD_NEW, CRS84, WEB_MERCATOR]
132
160
  ),
133
161
  ]
134
162
  ```
@@ -166,5 +194,3 @@ software developed with public money is also publicly available.
166
194
 
167
195
  This package is initially developed by the City of Amsterdam, but the tools
168
196
  and concepts created in this project can be used in any city.
169
-
170
-
@@ -13,6 +13,7 @@ django_gisserver.egg-info/top_level.txt
13
13
  gisserver/__init__.py
14
14
  gisserver/compat.py
15
15
  gisserver/conf.py
16
+ gisserver/crs.py
16
17
  gisserver/db.py
17
18
  gisserver/exceptions.py
18
19
  gisserver/features.py
@@ -64,6 +65,7 @@ gisserver/parsers/wfs20/projection.py
64
65
  gisserver/parsers/wfs20/requests.py
65
66
  gisserver/parsers/wfs20/stored.py
66
67
  gisserver/static/gisserver/index.css
68
+ gisserver/templates/gisserver/base.html
67
69
  gisserver/templates/gisserver/index.html
68
70
  gisserver/templates/gisserver/service_description.html
69
71
  gisserver/templates/gisserver/wfs/feature_field.html
@@ -1,7 +1,13 @@
1
1
  Django>=3.2
2
- defusedxml>=0.6.0
2
+ defusedxml>=0.7.1
3
3
  lru_dict>=1.1.7
4
4
  orjson>=3.9.15
5
+ pyproj>=3.6.1
6
+
7
+ [docs]
8
+ Django~=5.0
9
+ sphinxcontrib-django>=2.5
10
+ psycopg2-binary>=2.8.4
5
11
 
6
12
  [tests]
7
13
  django-environ>=0.4.5
@@ -0,0 +1 @@
1
+ __version__ = "2.1" # follows PEP440
@@ -0,0 +1,401 @@
1
+ """Helper classes for Coordinate Reference System translations.
2
+
3
+ This includes the CRS parsing, coordinate transforms and axis orientation.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import re
10
+ import typing
11
+ from dataclasses import dataclass, field
12
+ from functools import cached_property, lru_cache
13
+
14
+ import pyproj
15
+ from django.contrib.gis.gdal import AxisOrder, CoordTransform, OGRGeometry, SpatialReference
16
+ from django.contrib.gis.geos import GEOSGeometry
17
+
18
+ from gisserver.exceptions import ExternalValueError
19
+
20
+ CRS_URN_REGEX = re.compile(
21
+ r"^urn:(?P<domain>[a-z]+)"
22
+ r":def:crs:(?P<authority>[a-z]+)"
23
+ r":(?P<version>[0-9]+(\.[0-9]+(\.[0-9]+)?)?)?"
24
+ r":(?P<id>[0-9]+|crs84)"
25
+ r"$",
26
+ re.IGNORECASE,
27
+ )
28
+
29
+ AnyGeometry = typing.TypeVar("AnyGeometry", GEOSGeometry, OGRGeometry)
30
+
31
+ __all__ = [
32
+ "CRS",
33
+ "CRS84",
34
+ "WEB_MERCATOR",
35
+ "WGS84",
36
+ ]
37
+
38
+ # Caches to avoid reinitializing WGS84 each time.
39
+ _COMMON_CRS_BY_URN = {}
40
+ _COMMON_CRS_BY_SRID = {}
41
+
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ @lru_cache(maxsize=200) # Using lru-cache to avoid repeated GDAL c-object construction
47
+ def _get_spatial_reference(srs_input: str | int, srs_type, axis_order):
48
+ """Construct an GDAL object reference"""
49
+ logger.debug(
50
+ "Constructed GDAL SpatialReference(%r, srs_type=%r, axis_order=%s)",
51
+ srs_input,
52
+ srs_type,
53
+ axis_order,
54
+ )
55
+ return SpatialReference(srs_input, srs_type=srs_type, axis_order=axis_order)
56
+
57
+
58
+ @lru_cache(maxsize=100)
59
+ def _get_coord_transform(source: SpatialReference, target: SpatialReference) -> CoordTransform:
60
+ """Get an efficient coordinate transformation object.
61
+
62
+ The CoordTransform should be used when performing the same
63
+ coordinate transformation repeatedly on different geometries.
64
+
65
+ Using a CoordinateTransform also allows setting the AxisOrder setting
66
+ on both ends. When calling ``GEOSGeometry.transform()``, Django will
67
+ create an internal CoordTransform object internally without setting AxisOrder,
68
+ implicitly setting its source SpatialReference to be 'AxisOrder.TRADITIONAL'.
69
+ """
70
+ return CoordTransform(source, target)
71
+
72
+
73
+ _get_proj_crs_from_string = lru_cache(maxsize=10)(pyproj.CRS.from_string)
74
+ _get_proj_crs_from_authority = lru_cache(maxsize=10)(pyproj.CRS.from_authority)
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class CRS:
79
+ """
80
+ Represents a CRS (Coordinate Reference System), which preferably follows the URN format
81
+ as specified by `the OGC consortium <http://www.opengeospatial.org/ogcUrnPolicy>`_.
82
+ """
83
+
84
+ # CRS logic, based upon https://github.com/wglas85/django-wfs/blob/master/wfs/helpers.py
85
+ # Copyright (c) 2006 Wolfgang Glas - Apache 2.0 licensed
86
+ # Ported to Python 3.6 style.
87
+
88
+ #: Either "ogc" or "opengis", whereas "ogc" is highly recommended.
89
+ domain: str
90
+
91
+ #: Either "OGC" or "EPSG".
92
+ authority: str
93
+
94
+ #: The version of the authorities' SRS registry, which is empty or
95
+ #: contains two or three numeric components separated by dots like "6.9" or "6.11.9".
96
+ #: For WFS 2.0 this is typically empty.
97
+ version: str
98
+
99
+ #: A string representation of the coordinate system reference ID.
100
+ #: For OGC, only "CRS84" is supported as crsid. For EPSG, this is the formatted CRSID.
101
+ crsid: str
102
+
103
+ #: The integer representing the numeric spatial reference ID as
104
+ #: used by the EPSG and GIS database backends.
105
+ srid: int
106
+
107
+ #: GDAL SpatialReference with PROJ.4 / WKT content to describe the exact transformation.
108
+ backends: tuple[SpatialReference, SpatialReference] = (None, None)
109
+
110
+ #: Original input
111
+ origin: str = field(init=False, default=None)
112
+
113
+ @classmethod
114
+ def from_string(cls, uri: str | int) -> CRS:
115
+ """
116
+ Parse an CRS (Coordinate Reference System) URI, which preferably follows the URN format
117
+ as specified by `the OGC consortium <http://www.opengeospatial.org/ogcUrnPolicy>`_
118
+ and construct a new CRS instance.
119
+
120
+ The value can be 3 things:
121
+
122
+ * A URI in OGC URN format.
123
+ * A legacy CRS URI ("epsg:<SRID>", or "http://www.opengis.net/...").
124
+ * A numeric SRID (which calls :meth:`from_srid()`)
125
+ """
126
+ if isinstance(uri, int) or uri.isdigit():
127
+ return cls.from_srid(int(uri))
128
+ elif uri.startswith("urn:"):
129
+ return cls._from_urn(uri)
130
+ else:
131
+ return cls._from_legacy(uri)
132
+
133
+ @classmethod
134
+ def from_srid(cls, srid: int):
135
+ """Instantiate this class using a numeric spatial reference ID
136
+
137
+ This is logically identical to calling::
138
+
139
+ CRS.from_string("urn:ogc:def:crs:EPSG::<SRID>")
140
+ """
141
+ if common_crs := _COMMON_CRS_BY_SRID.get(srid):
142
+ return common_crs # Avoid object re-creation
143
+
144
+ crs = cls(
145
+ domain="ogc",
146
+ authority="EPSG",
147
+ version="",
148
+ crsid=str(srid),
149
+ srid=int(srid),
150
+ )
151
+ crs.__dict__["origin"] = srid
152
+ return crs
153
+
154
+ @classmethod
155
+ 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
+
160
+ urn_match = CRS_URN_REGEX.match(urn)
161
+ if not urn_match:
162
+ raise ExternalValueError(f"Unknown CRS URN [{urn}] specified: {CRS_URN_REGEX.pattern}")
163
+
164
+ domain = urn_match.group("domain")
165
+ authority = urn_match.group("authority").upper()
166
+
167
+ if domain not in ("ogc", "opengis"):
168
+ raise ExternalValueError(f"CRS URI [{urn}] contains unknown domain [{domain}]")
169
+
170
+ if authority == "EPSG":
171
+ crsid = urn_match.group("id")
172
+ try:
173
+ srid = int(crsid)
174
+ except ValueError:
175
+ raise ExternalValueError(
176
+ f"CRS URI [{urn}] should contain a numeric SRID value."
177
+ ) from None
178
+ elif authority == "OGC":
179
+ # urn:ogc:def:crs:OGC::CRS84 has x/y ordering (longitude/latitude)
180
+ crsid = urn_match.group("id").upper()
181
+ if crsid not in ("CRS84", "84"):
182
+ raise ExternalValueError(f"OGC CRS URI from [{urn}] contains unknown id [{id}]")
183
+ srid = 4326
184
+ else:
185
+ raise ExternalValueError(f"CRS URI [{urn}] contains unknown authority [{authority}]")
186
+
187
+ crs = cls(
188
+ domain=domain,
189
+ authority=authority,
190
+ version=urn_match.group(3),
191
+ crsid=crsid,
192
+ srid=srid,
193
+ )
194
+ crs.__dict__["origin"] = urn
195
+ return crs
196
+
197
+ @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#",
205
+ ):
206
+ if luri.startswith(head):
207
+ crsid = luri[len(head) :]
208
+ try:
209
+ srid = int(crsid)
210
+ except ValueError:
211
+ raise ExternalValueError(
212
+ f"CRS URI [{uri}] should contain a numeric SRID value."
213
+ ) from None
214
+
215
+ crs = cls(
216
+ domain="ogc",
217
+ authority="EPSG",
218
+ version="",
219
+ crsid=crsid,
220
+ srid=srid,
221
+ )
222
+ crs.__dict__["origin"] = uri
223
+ return crs
224
+
225
+ raise ExternalValueError(f"Unknown CRS URI [{uri}] specified")
226
+
227
+ @property
228
+ def legacy(self):
229
+ """Return a legacy string in the format :samp:`EPSG:{srid}`."""
230
+ return f"EPSG:{self.srid:d}"
231
+
232
+ @cached_property
233
+ def urn(self):
234
+ """Return The OGC URN corresponding to this CRS."""
235
+ return f"urn:{self.domain}:def:crs:{self.authority}:{self.version or ''}:{self.crsid}"
236
+
237
+ @property
238
+ def is_north_east_order(self) -> bool:
239
+ """Tell whether the axis is in north/east ordering."""
240
+ return self.axis_direction == ["north", "east"]
241
+
242
+ @cached_property
243
+ def axis_direction(self) -> list[str]:
244
+ """Tell what the axis ordering of this coordinate system is.
245
+
246
+ For example, WGS84 will return ``['north', 'east']``.
247
+
248
+ While computer systems typically use X,Y, other systems may use northing/easting.
249
+ Historically, latitude was easier to measure and given first.
250
+ In physics, using 'radial, polar, azimuthal' is again a different perspective.
251
+ See: https://wiki.osgeo.org/wiki/Axis_Order_Confusion for a good summary.
252
+ """
253
+ proj_crs = self._as_proj()
254
+ return [axis.direction for axis in proj_crs.axis_info]
255
+
256
+ def __str__(self):
257
+ return self.urn
258
+
259
+ def __eq__(self, other):
260
+ 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
264
+ else:
265
+ return NotImplemented
266
+
267
+ def __hash__(self):
268
+ """Used to match objects in a set."""
269
+ return hash((self.authority, self.srid))
270
+
271
+ def _as_gdal(self, axis_order: AxisOrder) -> SpatialReference:
272
+ """Generate the GDAL Spatial Reference object."""
273
+ if self.backends[axis_order] is None:
274
+ backends = list(self.backends)
275
+ if self.origin:
276
+ backends[axis_order] = _get_spatial_reference(self.origin, "user", axis_order)
277
+ else:
278
+ backends[axis_order] = _get_spatial_reference(self.srid, "epsg", axis_order)
279
+
280
+ # Write back in "readonly" format.
281
+ self.__dict__["backends"] = tuple(backends)
282
+
283
+ return self.backends[axis_order]
284
+
285
+ def _as_proj(self) -> pyproj.CRS:
286
+ """Generate the PROJ CRS object"""
287
+ if isinstance(self.origin, str) and not self.origin.isdigit():
288
+ return _get_proj_crs_from_string(self.origin)
289
+ else:
290
+ return _get_proj_crs_from_authority(self.authority, self.srid)
291
+
292
+ def apply_to(
293
+ self,
294
+ geometry: AnyGeometry,
295
+ clone=False,
296
+ axis_order: AxisOrder = AxisOrder.AUTHORITY,
297
+ ) -> AnyGeometry | None:
298
+ """Transform the geometry using this coordinate reference.
299
+
300
+ Every transformation within this package happens through this method,
301
+ giving full control over coordinate transformations.
302
+
303
+ A bit of background: geometries are provided as ``GEOSGeometry`` from the database.
304
+ This is basically a simple C-based storage implementing "OpenGIS Simple Features for SQL",
305
+ except it does *not* store axis orientation. These are assumed to be x/y.
306
+
307
+ To perform transformations, GeoDjango loads the GEOS-geometry into GDAL/OGR.
308
+ The transformed geometry is loaded back to GEOS. To avoid this conversion,
309
+ pass the OGR object directly and continue working on that.
310
+
311
+ Internally, this method caches the used GDAL ``CoordTransform`` object,
312
+ so repeated transformations of the same coordinate systems are faster.
313
+
314
+ The axis order can change during the transformation.
315
+ From a programming perspective, screen coordinates (x/y) were traditionally used.
316
+ However, various systems and industries have always worked with north/east (y/x).
317
+ This includes systems with critical safety requirements in aviation and maritime.
318
+ The CRS authority reflects this practice. What you need depends on the use case:
319
+
320
+ * The (GML) output of WFS 2.0 and WMS 1.3 respect the axis ordering of the CRS.
321
+ * GeoJSON always provides coordinates in x/y, to keep web-based clients simple.
322
+ * PostGIS stores the data in x/y.
323
+ * WFS 1.0 used x/y, WFS 1.3 used y/x except for ``EPSG:4326``.
324
+
325
+ After GDAL/OGR changed the axis orientation, that information is
326
+ lost when the return value is loaded back into GEOS.
327
+ To address this, :meth:`tag_geometry` is called on the result.
328
+
329
+ :param geometry: The GEOS Geometry, or GDAL/OGR loaded geometry.
330
+ :param clone: Whether the object is changed in-place, or a copy is returned.
331
+ For GEOS->GDAL->GEOS conversions, this makes no difference in efficiency.
332
+ :param axis_order: Which axis ordering to convert the geometry into (depends on the use-case).
333
+ """
334
+ if isinstance(geometry, OGRGeometry):
335
+ transform = _get_coord_transform(geometry.srs, self._as_gdal(axis_order=axis_order))
336
+ return geometry.transform(transform, clone=clone)
337
+ else:
338
+ # See if the geometry was tagged with a CRS.
339
+ # When the data comes from an unknown source, assume this is from database storage.
340
+ # PostGIS stores data in longitude/latitude (x/y) ordering, even for srid 4326.
341
+ # By passing the 'AxisOrder.TRADITIONAL', a conversion from 4326 to 4326
342
+ # with 'AxisOrder.AUTHORITY' will detect that coordinate ordering needs to be changed.
343
+ source_axis_order = getattr(geometry, "_axis_order", AxisOrder.TRADITIONAL)
344
+
345
+ if self.srid == geometry.srid and source_axis_order == axis_order:
346
+ # Avoid changes if spatial reference system is identical, and no axis need to change.
347
+ if clone:
348
+ return geometry.clone()
349
+ else:
350
+ return None
351
+
352
+ # Get GDAL spatial reference for converting coordinates (uses proj internally).
353
+ # Using a cached coordinate transform object (is faster for repeated transforms)
354
+ # The object is also tagged so another apply_to() call would recognize the state.
355
+ source = _get_spatial_reference(geometry.srid, "epsg", source_axis_order)
356
+ target = self._as_gdal(axis_order=axis_order)
357
+ transform = _get_coord_transform(source, target)
358
+
359
+ # Transform
360
+ geometry = geometry.transform(transform, clone=clone)
361
+ if clone:
362
+ self.tag_geometry(geometry, axis_order=axis_order)
363
+ return geometry
364
+
365
+ @classmethod
366
+ def tag_geometry(self, geometry: GEOSGeometry, axis_order: AxisOrder):
367
+ """Associate this object with the geometry.
368
+
369
+ This informs the :meth:`apply_to` method that this source geometry
370
+ already had the correct axis ordering (e.g. it was part of the ``<fes:BBOX>`` logic).
371
+ The srid integer doesn't communicate that information.
372
+ """
373
+ geometry._axis_order = axis_order
374
+
375
+ def cache_instance(self):
376
+ """Cache a common CRS, no need to re-instantiate the same object again.
377
+ This also makes sure that requests which use the same URN will get our CRS object
378
+ version, instead of a fresh new one.
379
+ """
380
+ if self.authority == "EPSG":
381
+ # Only register for EPSG to avoid conflicting axis ordering issues.
382
+ # (WGS84 and CRS84 both use srid 4326)
383
+ _COMMON_CRS_BY_SRID[self.srid] = self
384
+
385
+ _COMMON_CRS_BY_URN[self.urn] = self
386
+
387
+
388
+ #: Worldwide GPS, latitude/longitude (y/x). https://epsg.io/4326
389
+ WGS84 = CRS.from_string("urn:ogc:def:crs:EPSG::4326")
390
+
391
+ #: GeoJSON default. This is like WGS84 but with longitude/latitude (x/y).
392
+ CRS84 = CRS.from_string("urn:ogc:def:crs:OGC::CRS84")
393
+
394
+ #: Spherical Mercator (Google Maps, Bing Maps, OpenStreetMap, ...), see https://epsg.io/3857
395
+ WEB_MERCATOR = CRS.from_string("urn:ogc:def:crs:EPSG::3857")
396
+
397
+
398
+ # Register these common ones:
399
+ WGS84.cache_instance()
400
+ CRS84.cache_instance()
401
+ WEB_MERCATOR.cache_instance()