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.
- {django-gisserver-2.0 → django_gisserver-2.1}/CHANGES.md +20 -1
- {django-gisserver-2.0/django_gisserver.egg-info → django_gisserver-2.1}/PKG-INFO +35 -9
- {django-gisserver-2.0 → django_gisserver-2.1}/README.md +4 -3
- {django-gisserver-2.0 → django_gisserver-2.1/django_gisserver.egg-info}/PKG-INFO +35 -9
- {django-gisserver-2.0 → django_gisserver-2.1}/django_gisserver.egg-info/SOURCES.txt +2 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/django_gisserver.egg-info/requires.txt +7 -1
- django_gisserver-2.1/gisserver/__init__.py +1 -0
- django_gisserver-2.1/gisserver/crs.py +401 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/db.py +71 -5
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/exceptions.py +106 -2
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/extensions/functions.py +122 -28
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/extensions/queries.py +15 -10
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/features.py +44 -36
- django_gisserver-2.1/gisserver/geometries.py +111 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/management/commands/loadgeojson.py +41 -21
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/operations/base.py +11 -7
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/operations/wfs20.py +31 -93
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/__init__.py +6 -2
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/base.py +28 -13
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/csv.py +18 -6
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/geojson.py +7 -6
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/gml32.py +43 -23
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/results.py +25 -39
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/utils.py +9 -2
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/ast.py +171 -65
- django_gisserver-2.1/gisserver/parsers/fes20/__init__.py +100 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/fes20/expressions.py +97 -27
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/fes20/filters.py +9 -6
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/fes20/identifiers.py +27 -7
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/fes20/lookups.py +8 -6
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/fes20/operators.py +101 -49
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/fes20/sorting.py +14 -6
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/gml/__init__.py +10 -19
- django_gisserver-2.1/gisserver/parsers/gml/base.py +58 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/gml/geometries.py +48 -21
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/ows/kvp.py +10 -2
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/ows/requests.py +6 -4
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/query.py +6 -2
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/values.py +61 -4
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/wfs20/__init__.py +2 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/wfs20/adhoc.py +25 -17
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/wfs20/base.py +12 -7
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/wfs20/projection.py +3 -3
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/wfs20/requests.py +1 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/wfs20/stored.py +3 -2
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/xml.py +12 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/projection.py +17 -7
- django_gisserver-2.1/gisserver/static/gisserver/index.css +20 -0
- django_gisserver-2.1/gisserver/templates/gisserver/base.html +12 -0
- django_gisserver-2.1/gisserver/templates/gisserver/index.html +20 -0
- django_gisserver-2.1/gisserver/templates/gisserver/service_description.html +22 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/templates/gisserver/wfs/feature_field.html +1 -1
- django_gisserver-2.1/gisserver/templates/gisserver/wfs/feature_type.html +45 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/types.py +150 -81
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/views.py +47 -24
- {django-gisserver-2.0 → django_gisserver-2.1}/setup.py +9 -2
- django-gisserver-2.0/gisserver/__init__.py +0 -1
- django-gisserver-2.0/gisserver/geometries.py +0 -353
- django-gisserver-2.0/gisserver/parsers/fes20/__init__.py +0 -28
- django-gisserver-2.0/gisserver/parsers/gml/base.py +0 -40
- django-gisserver-2.0/gisserver/static/gisserver/index.css +0 -15
- django-gisserver-2.0/gisserver/templates/gisserver/index.html +0 -26
- django-gisserver-2.0/gisserver/templates/gisserver/service_description.html +0 -16
- django-gisserver-2.0/gisserver/templates/gisserver/wfs/feature_type.html +0 -23
- {django-gisserver-2.0 → django_gisserver-2.1}/LICENSE +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/MANIFEST.in +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/django_gisserver.egg-info/dependency_links.txt +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/django_gisserver.egg-info/not-zip-safe +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/django_gisserver.egg-info/top_level.txt +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/compat.py +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/conf.py +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/extensions/__init__.py +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/management/__init__.py +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/management/commands/__init__.py +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/operations/__init__.py +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/iters.py +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/stored.py +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/output/xmlschema.py +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/__init__.py +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/parsers/ows/__init__.py +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/templatetags/__init__.py +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/gisserver/templatetags/gisserver_tags.py +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/pyproject.toml +0 -0
- {django-gisserver-2.0 → django_gisserver-2.1}/setup.cfg +0 -0
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
#
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: django-gisserver
|
|
3
|
-
Version: 2.
|
|
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
|
[](https://django-gisserver.readthedocs.io/en/latest/?badge=latest)
|
|
38
65
|
[](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
|
-
|
|
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
|
-
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: django-gisserver
|
|
3
|
-
Version: 2.
|
|
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
|
[](https://django-gisserver.readthedocs.io/en/latest/?badge=latest)
|
|
38
65
|
[](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
|
-
|
|
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
|
|
@@ -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()
|