django-gisserver 2.0__py3-none-any.whl → 2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +26 -10
  2. django_gisserver-2.1.dist-info/RECORD +68 -0
  3. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/crs.py +401 -0
  6. gisserver/db.py +71 -5
  7. gisserver/exceptions.py +106 -2
  8. gisserver/extensions/functions.py +122 -28
  9. gisserver/extensions/queries.py +15 -10
  10. gisserver/features.py +44 -36
  11. gisserver/geometries.py +64 -306
  12. gisserver/management/commands/loadgeojson.py +41 -21
  13. gisserver/operations/base.py +11 -7
  14. gisserver/operations/wfs20.py +31 -93
  15. gisserver/output/__init__.py +6 -2
  16. gisserver/output/base.py +28 -13
  17. gisserver/output/csv.py +18 -6
  18. gisserver/output/geojson.py +7 -6
  19. gisserver/output/gml32.py +43 -23
  20. gisserver/output/results.py +25 -39
  21. gisserver/output/utils.py +9 -2
  22. gisserver/parsers/ast.py +171 -65
  23. gisserver/parsers/fes20/__init__.py +76 -4
  24. gisserver/parsers/fes20/expressions.py +97 -27
  25. gisserver/parsers/fes20/filters.py +9 -6
  26. gisserver/parsers/fes20/identifiers.py +27 -7
  27. gisserver/parsers/fes20/lookups.py +8 -6
  28. gisserver/parsers/fes20/operators.py +101 -49
  29. gisserver/parsers/fes20/sorting.py +14 -6
  30. gisserver/parsers/gml/__init__.py +10 -19
  31. gisserver/parsers/gml/base.py +32 -14
  32. gisserver/parsers/gml/geometries.py +48 -21
  33. gisserver/parsers/ows/kvp.py +10 -2
  34. gisserver/parsers/ows/requests.py +6 -4
  35. gisserver/parsers/query.py +6 -2
  36. gisserver/parsers/values.py +61 -4
  37. gisserver/parsers/wfs20/__init__.py +2 -0
  38. gisserver/parsers/wfs20/adhoc.py +25 -17
  39. gisserver/parsers/wfs20/base.py +12 -7
  40. gisserver/parsers/wfs20/projection.py +3 -3
  41. gisserver/parsers/wfs20/requests.py +1 -0
  42. gisserver/parsers/wfs20/stored.py +3 -2
  43. gisserver/parsers/xml.py +12 -0
  44. gisserver/projection.py +17 -7
  45. gisserver/static/gisserver/index.css +8 -3
  46. gisserver/templates/gisserver/base.html +12 -0
  47. gisserver/templates/gisserver/index.html +9 -15
  48. gisserver/templates/gisserver/service_description.html +12 -6
  49. gisserver/templates/gisserver/wfs/feature_field.html +1 -1
  50. gisserver/templates/gisserver/wfs/feature_type.html +35 -13
  51. gisserver/types.py +150 -81
  52. gisserver/views.py +47 -24
  53. django_gisserver-2.0.dist-info/RECORD +0 -66
  54. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
  55. {django_gisserver-2.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
@@ -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
@@ -33,9 +32,10 @@ Requires-Python: >=3.9
33
32
  Description-Content-Type: text/markdown
34
33
  License-File: LICENSE
35
34
  Requires-Dist: Django>=3.2
36
- Requires-Dist: defusedxml>=0.6.0
37
- Requires-Dist: lru-dict>=1.1.7
35
+ Requires-Dist: defusedxml>=0.7.1
36
+ Requires-Dist: lru_dict>=1.1.7
38
37
  Requires-Dist: orjson>=3.9.15
38
+ Requires-Dist: pyproj>=3.6.1
39
39
  Provides-Extra: tests
40
40
  Requires-Dist: django-environ>=0.4.5; extra == "tests"
41
41
  Requires-Dist: psycopg2-binary>=2.8.4; extra == "tests"
@@ -43,6 +43,23 @@ Requires-Dist: lxml>=4.9.1; extra == "tests"
43
43
  Requires-Dist: pytest>=6.2.3; extra == "tests"
44
44
  Requires-Dist: pytest-django>=4.1.0; extra == "tests"
45
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
46
63
 
47
64
  [![Documentation](https://readthedocs.org/projects/django-gisserver/badge/?version=latest)](https://django-gisserver.readthedocs.io/en/latest/?badge=latest)
48
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)
@@ -110,12 +127,13 @@ class Restaurant(models.Model):
110
127
  Write a view that exposes this model as a WFS feature:
111
128
 
112
129
  ```python
130
+ from gisserver.crs import CRS, CRS84, WEB_MERCATOR
113
131
  from gisserver.features import FeatureType, ServiceDescription
114
- from gisserver.geometries import CRS, WGS84
115
132
  from gisserver.views import WFSView
116
133
  from .models import Restaurant
117
134
 
118
- 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")
119
137
 
120
138
 
121
139
  class PlacesWFSView(WFSView):
@@ -138,7 +156,7 @@ class PlacesWFSView(WFSView):
138
156
  FeatureType(
139
157
  Restaurant.objects.all(),
140
158
  fields="__all__",
141
- other_crs=[RD_NEW]
159
+ other_crs=[RD_NEW, CRS84, WEB_MERCATOR]
142
160
  ),
143
161
  ]
144
162
  ```
@@ -176,5 +194,3 @@ software developed with public money is also publicly available.
176
194
 
177
195
  This package is initially developed by the City of Amsterdam, but the tools
178
196
  and concepts created in this project can be used in any city.
179
-
180
-
@@ -0,0 +1,68 @@
1
+ django_gisserver-2.1.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
2
+ gisserver/__init__.py,sha256=EZRBJR6U2egJTZDzOVWUz5AOLuAcwyxzkLAMaShy2TA,38
3
+ gisserver/compat.py,sha256=qmLeT7wGRIyA2ujUtVEuUX7qgr98snak9b5OTvnkhas,451
4
+ gisserver/conf.py,sha256=ppoyVS95mMoQXVbPxPar-tkq1t7fE0J3F6IyKitMWuA,2563
5
+ gisserver/crs.py,sha256=Bll3N9FrEG0jqEFDq5bzxwKHk1PrX8DDUqHJBcsgnZw,15648
6
+ gisserver/db.py,sha256=NI__Kjp_W2Pfip0HL33so4QckTP8AFAlL0Ss1xehhf4,8026
7
+ gisserver/exceptions.py,sha256=RhaRhY_DX_SzJuiXosyN9Mc0EnMkmz2frN0Cgxw9-Rk,10163
8
+ gisserver/features.py,sha256=2EONhGuOteY1Fb_RLnFXo80aZ8-SOlA8YGrZepPh-DE,36200
9
+ gisserver/geometries.py,sha256=-Jl02yKo7KXJO5AgW93453P2qLfA3kYJKgvPSonsbTU,3738
10
+ gisserver/projection.py,sha256=yja8baq8sWblhytrD0yPlTH-v5z6pIy6mzUu1sLc2eQ,15301
11
+ gisserver/types.py,sha256=Dwxs7c8jclRK9HhXub9Syv8MM1OVmfez1XP464jGuv4,43911
12
+ gisserver/views.py,sha256=vTB0-BuNKoqPKjKIwNe8xtm_erM_yD8tmkw25vppoj4,20312
13
+ gisserver/extensions/__init__.py,sha256=MsYUsNsoxxjeDHhx4DW9eGXvmOnI5PkoUXvKxRZaRRw,136
14
+ gisserver/extensions/functions.py,sha256=enEarl4TUfGVA0QWlURU-QZjTaLbIEjOGjNzjF7z8jI,11629
15
+ gisserver/extensions/queries.py,sha256=uqiuUnHi2YiFhFA7cBsaBg9MyFOU-v9Gdp0tHJ1wE_k,9653
16
+ gisserver/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ gisserver/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ gisserver/management/commands/loadgeojson.py,sha256=lmvxXFEVGOWRELLB3u7M3hiJg2TQEICJOatdHLw8toI,12130
19
+ gisserver/operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ gisserver/operations/base.py,sha256=SP3PV78bIFCeCQxEFMqIhj0YBjn1g6UqhyoK_Ub4ZxU,10113
21
+ gisserver/operations/wfs20.py,sha256=r_2dcqzN5xw2LIOVE7Ofe93t9GvNx7ESFWKRnnW10Y4,20964
22
+ gisserver/output/__init__.py,sha256=1-bzgNq1yGuWGse2zLXUwTnR7-FpfosGCzDm97diKRM,1133
23
+ gisserver/output/base.py,sha256=igcOhjJkziPlpPJjwfyyC0S1LHfRI8Em-UMNnX5Lk-E,11247
24
+ gisserver/output/csv.py,sha256=1Bz3o3-dvdEFWWLrDj-cxCp7My6IMMakmwAZIfPsjc4,6906
25
+ gisserver/output/geojson.py,sha256=0Mm_fqi-v1MzxWJ3tDL1OrJpcteZ9nQBgilqQ_nG7V4,11633
26
+ gisserver/output/gml32.py,sha256=GNqmEx-GPgT4EvykDEauMZL5AcQcq4rahIAY6Bm9lLo,33266
27
+ gisserver/output/iters.py,sha256=YbvtIIy977Snm5m7cYyQext0GCpo8S_u4azTCBNqqgw,8355
28
+ gisserver/output/results.py,sha256=_RNm7IVBC5j5CQY2q65MAWCdVSfRSexas7f8KtPGN9g,14668
29
+ gisserver/output/stored.py,sha256=m3JcR4MUilyuM_5J6XJDOIyzrhRJbcpg-6FMKy69csc,5763
30
+ gisserver/output/utils.py,sha256=dA4Y-6ZUBjjCC8ueO5AcYaVebB5M6ynXUtrw7yyOcoQ,3001
31
+ gisserver/output/xmlschema.py,sha256=fHj8h9-vG508uRmM-kD5r2omde2wa_myJWWwcWe2lus,6445
32
+ gisserver/parsers/__init__.py,sha256=eGORBeT0F8fR3AadV1uq8dbCeZtIeK0-__CcXwSvXgs,402
33
+ gisserver/parsers/ast.py,sha256=3ereccG1m_PWNJXP_IdUKiX0I_zLpnIghubb0-p6sas,15499
34
+ gisserver/parsers/query.py,sha256=IbsWBSAlOKDgz8BS-JirxVCqzQxSpYqIxGEhv0-sOVc,6695
35
+ gisserver/parsers/values.py,sha256=QCnHBlMAGDefoC51ujOrQc1YEb3RYYP7aq5UIDweJO0,4166
36
+ gisserver/parsers/xml.py,sha256=uys1ddpx4BEDFg1WmknfATl4vJpgpKbb0yV2QT2kJqs,9753
37
+ gisserver/parsers/fes20/__init__.py,sha256=P24auC3LN_DEnF6bK0er52F8I2_oLS26gBCJOMcpNT4,2423
38
+ gisserver/parsers/fes20/expressions.py,sha256=Yw1RvotstgTIlW49_Vfa4b1iODFl8Nd34RKpPLuJjTw,14015
39
+ gisserver/parsers/fes20/filters.py,sha256=ssS-XhnUoWayPrAx1ZNQj7RlTAu-eS47pB_a0zeWjCQ,6677
40
+ gisserver/parsers/fes20/identifiers.py,sha256=Y1diAHHVYWBWNy4wPakXdx9DPloX-jphdbmD8DTuhhU,4107
41
+ gisserver/parsers/fes20/lookups.py,sha256=rrWGM5s3qx-yMbz7ogg8BTxzcmQdHfqqJ_9jF7BObrg,5430
42
+ gisserver/parsers/fes20/operators.py,sha256=szkIXIdvKKwNqCTOjyO_KKvpgsxHHPjMppEqWdWmzz4,33609
43
+ gisserver/parsers/fes20/sorting.py,sha256=RRCepyXCxD9zt8a0fu5oneV8Ta8A0-bOU5aou4v8nQE,5034
44
+ gisserver/parsers/gml/__init__.py,sha256=T7_kSRnPTGvwoOXpcPgGeRTjO5zUxbvifkkUHUK_pLI,1484
45
+ gisserver/parsers/gml/base.py,sha256=uBg9BRBZYAmXrfhQuhs-cVZKJqs8Ksh61CPezPN2-cU,1636
46
+ gisserver/parsers/gml/geometries.py,sha256=oC8RltuylrivZSdMBMlvB_tjgWHEB6Jm_WICHRlwV1o,5710
47
+ gisserver/parsers/ows/__init__.py,sha256=0SeM6vfwjWI-WeRD7eS_iN4Jsl8uzXr2yMFpZVHokb4,624
48
+ gisserver/parsers/ows/kvp.py,sha256=1NDXcaO1Z3TRxZ4F11xoNnsutq12oEyYoJv7SMX1IPE,7235
49
+ gisserver/parsers/ows/requests.py,sha256=ob66EWEJBEbjgGoPhk8qBE0BuDn2sYPsjr4Z_elvqss,5540
50
+ gisserver/parsers/wfs20/__init__.py,sha256=t66iqcImqerH3Z1tMiWCms3WLmJXybUDqhcEXvITaWU,924
51
+ gisserver/parsers/wfs20/adhoc.py,sha256=C6uBzxs90S2Z2Cr_sNP6u7x0QMIc_jz_Zcf76asqjFA,9428
52
+ gisserver/parsers/wfs20/base.py,sha256=nPXwyQryjEdwoBq78a6KLHv5wzqdDbkeW4kfmOHXvIM,5867
53
+ gisserver/parsers/wfs20/projection.py,sha256=irGQrVyOj5q9Or1rw99A3nn3on6PlGeB4261XUx6AkE,3932
54
+ gisserver/parsers/wfs20/requests.py,sha256=Oi4HxiMs9_ij7CEOFmyu4GeN6VKyc9lJBkvIG5zrrSM,16853
55
+ gisserver/parsers/wfs20/stored.py,sha256=ptTYGxfVkwlNtn6uF1my_2fSTpM9KWdtzEZPyKWpTyk,7195
56
+ gisserver/static/gisserver/index.css,sha256=pfIpi-aZalg_181S4ac34J2BhsIy7yz00-jTgDfWNog,663
57
+ gisserver/templates/gisserver/base.html,sha256=7HdLTW9-XwLrUL2TJsuHATMRNELYFBmLYFMQG80V9Xk,590
58
+ gisserver/templates/gisserver/index.html,sha256=_n7YSBLVZg9SGDnFmvTMTTn8psAFTiIUvCjFM7Cthuc,639
59
+ gisserver/templates/gisserver/service_description.html,sha256=E74PcikUA2vSSUJvsa8iZtGX7OpKU4i4IHINjHhibDw,1086
60
+ gisserver/templates/gisserver/wfs/feature_field.html,sha256=TmUNCme7E2vxofVVhiysD1A0ZTXhtDzd78ogGn9Y9lc,613
61
+ gisserver/templates/gisserver/wfs/feature_type.html,sha256=RDWwlAhrVPa-_J9Ys_AthaEXNxZ0Sqrx0Z2evHBUP64,2010
62
+ gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml,sha256=SGZSn7cDU_Oai6d45xrkqhax5CPyI1oIc8BZLOEw1X4,8695
63
+ gisserver/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
+ gisserver/templatetags/gisserver_tags.py,sha256=L2YG-PxiKR0QWBMRLLrL_VR94p6wCb5HzgnrPMg4hug,995
65
+ django_gisserver-2.1.dist-info/METADATA,sha256=V-cCFscSUoeVvL69Q1L-iNNOzYNfuCmNciOA7X8MdVQ,6798
66
+ django_gisserver-2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
67
+ django_gisserver-2.1.dist-info/top_level.txt,sha256=8zFCEMmkpixE4TPiOAlxSq3PD2EXvAeFKwG4yZoIIOQ,10
68
+ django_gisserver-2.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.45.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
gisserver/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2.0" # follows PEP440
1
+ __version__ = "2.1" # follows PEP440
gisserver/crs.py ADDED
@@ -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()