django-gisserver 2.0__py3-none-any.whl → 2.1.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 (56) hide show
  1. {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/METADATA +27 -10
  2. django_gisserver-2.1.1.dist-info/RECORD +68 -0
  3. {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/conf.py +23 -1
  6. gisserver/crs.py +452 -0
  7. gisserver/db.py +78 -6
  8. gisserver/exceptions.py +106 -2
  9. gisserver/extensions/functions.py +122 -28
  10. gisserver/extensions/queries.py +15 -10
  11. gisserver/features.py +46 -33
  12. gisserver/geometries.py +64 -306
  13. gisserver/management/commands/loadgeojson.py +41 -21
  14. gisserver/operations/base.py +11 -7
  15. gisserver/operations/wfs20.py +31 -93
  16. gisserver/output/__init__.py +6 -2
  17. gisserver/output/base.py +28 -13
  18. gisserver/output/csv.py +18 -6
  19. gisserver/output/geojson.py +7 -6
  20. gisserver/output/gml32.py +86 -27
  21. gisserver/output/results.py +25 -39
  22. gisserver/output/utils.py +9 -2
  23. gisserver/parsers/ast.py +177 -68
  24. gisserver/parsers/fes20/__init__.py +76 -4
  25. gisserver/parsers/fes20/expressions.py +97 -27
  26. gisserver/parsers/fes20/filters.py +9 -6
  27. gisserver/parsers/fes20/identifiers.py +27 -7
  28. gisserver/parsers/fes20/lookups.py +8 -6
  29. gisserver/parsers/fes20/operators.py +101 -49
  30. gisserver/parsers/fes20/sorting.py +14 -6
  31. gisserver/parsers/gml/__init__.py +10 -19
  32. gisserver/parsers/gml/base.py +32 -14
  33. gisserver/parsers/gml/geometries.py +54 -21
  34. gisserver/parsers/ows/kvp.py +10 -2
  35. gisserver/parsers/ows/requests.py +6 -4
  36. gisserver/parsers/query.py +6 -2
  37. gisserver/parsers/values.py +61 -4
  38. gisserver/parsers/wfs20/__init__.py +2 -0
  39. gisserver/parsers/wfs20/adhoc.py +28 -18
  40. gisserver/parsers/wfs20/base.py +12 -7
  41. gisserver/parsers/wfs20/projection.py +3 -3
  42. gisserver/parsers/wfs20/requests.py +1 -0
  43. gisserver/parsers/wfs20/stored.py +3 -2
  44. gisserver/parsers/xml.py +12 -0
  45. gisserver/projection.py +17 -7
  46. gisserver/static/gisserver/index.css +27 -6
  47. gisserver/templates/gisserver/base.html +15 -0
  48. gisserver/templates/gisserver/index.html +10 -16
  49. gisserver/templates/gisserver/service_description.html +12 -6
  50. gisserver/templates/gisserver/wfs/feature_field.html +1 -1
  51. gisserver/templates/gisserver/wfs/feature_type.html +44 -13
  52. gisserver/types.py +152 -82
  53. gisserver/views.py +47 -24
  54. django_gisserver-2.0.dist-info/RECORD +0 -66
  55. {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info/licenses}/LICENSE +0 -0
  56. {django_gisserver-2.0.dist-info → django_gisserver-2.1.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.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,24 @@ 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: myst-parser>=3.0.1; extra == "docs"
50
+ Requires-Dist: psycopg2-binary>=2.8.4; extra == "docs"
51
+ Dynamic: author
52
+ Dynamic: author-email
53
+ Dynamic: classifier
54
+ Dynamic: description
55
+ Dynamic: description-content-type
56
+ Dynamic: home-page
57
+ Dynamic: license
58
+ Dynamic: license-file
59
+ Dynamic: provides-extra
60
+ Dynamic: requires
61
+ Dynamic: requires-dist
62
+ Dynamic: requires-python
63
+ Dynamic: summary
46
64
 
47
65
  [![Documentation](https://readthedocs.org/projects/django-gisserver/badge/?version=latest)](https://django-gisserver.readthedocs.io/en/latest/?badge=latest)
48
66
  [![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 +128,13 @@ class Restaurant(models.Model):
110
128
  Write a view that exposes this model as a WFS feature:
111
129
 
112
130
  ```python
131
+ from gisserver.crs import CRS, CRS84, WEB_MERCATOR
113
132
  from gisserver.features import FeatureType, ServiceDescription
114
- from gisserver.geometries import CRS, WGS84
115
133
  from gisserver.views import WFSView
116
134
  from .models import Restaurant
117
135
 
118
- RD_NEW = CRS.from_srid(28992)
136
+ # As example, the local coordinate system for The Netherlands
137
+ RD_NEW = CRS.from_string("urn:ogc:def:crs:EPSG::28992")
119
138
 
120
139
 
121
140
  class PlacesWFSView(WFSView):
@@ -138,7 +157,7 @@ class PlacesWFSView(WFSView):
138
157
  FeatureType(
139
158
  Restaurant.objects.all(),
140
159
  fields="__all__",
141
- other_crs=[RD_NEW]
160
+ other_crs=[RD_NEW, CRS84, WEB_MERCATOR]
142
161
  ),
143
162
  ]
144
163
  ```
@@ -176,5 +195,3 @@ software developed with public money is also publicly available.
176
195
 
177
196
  This package is initially developed by the City of Amsterdam, but the tools
178
197
  and concepts created in this project can be used in any city.
179
-
180
-
@@ -0,0 +1,68 @@
1
+ django_gisserver-2.1.1.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
2
+ gisserver/__init__.py,sha256=UUQ8Wbk0ewAYeTqfEUnsJx6oCvEq_2LWeacF6xnZ-tI,40
3
+ gisserver/compat.py,sha256=qmLeT7wGRIyA2ujUtVEuUX7qgr98snak9b5OTvnkhas,451
4
+ gisserver/conf.py,sha256=Bb2hbLx1SM4la6-e8PrBQ7dg2z3Z1sopOlaTrfa6gbg,3666
5
+ gisserver/crs.py,sha256=ChqLNZCHBCZmXphjJTT22jvhyMIwAAFtFSjwbH4Bigk,18330
6
+ gisserver/db.py,sha256=p2bb6KMjFzsEEedFx4xZ1bBOFDd19MembXUKCnkk_pM,8154
7
+ gisserver/exceptions.py,sha256=RhaRhY_DX_SzJuiXosyN9Mc0EnMkmz2frN0Cgxw9-Rk,10163
8
+ gisserver/features.py,sha256=njVzctaKBiGsuSz8ATO_4GlNsXiEM-kJcH0y2yjMFco,36445
9
+ gisserver/geometries.py,sha256=sXHr7LR4PgKImCVbENBxMBxAnz3AB8-TZWunIIGrAs0,3739
10
+ gisserver/projection.py,sha256=yja8baq8sWblhytrD0yPlTH-v5z6pIy6mzUu1sLc2eQ,15301
11
+ gisserver/types.py,sha256=XD63wq7iA3cHpdZ1r2MoxNK-sohGj5IoBCwoaA4xNzk,43939
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=RrQp7iiMYtYf-PAcYLqO53b53GYBD3_CoTtZQIqbhyQ,35174
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=zFb64uaIgAT9THV07l0pnuzWfXkzJ2CwT1z6lVGwrag,15688
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=IQ-ArgRtmk9FRaBP901DqzlC2NKfQhoTv_VcgxGDs4Q,33608
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=DbQTYGDLMw0JRwdNrSR7ia2vIAfS5AHz6lBOFvrfLc0,6120
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=O33rT5WEheEpJV-EOtsPf-U7PR0TmaR-MWxrTyFvQwY,9459
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=Luh9Ajc9C-9Q1QkwZcXI2NtRMRjiXxcRAXVn7PCGblo,994
57
+ gisserver/templates/gisserver/base.html,sha256=3wNNs8FwsG0FXJmEE0ad8W9nFfYiTIjipeD23X1QBY8,817
58
+ gisserver/templates/gisserver/index.html,sha256=fpijm56m7WzEynNHWMWmuDXXgqJLGWodgW-JNFlu-ts,664
59
+ gisserver/templates/gisserver/service_description.html,sha256=PbJk-V3u-NhjSDnwSdgzlud5U4WhRbCRWu-8EUACibg,1086
60
+ gisserver/templates/gisserver/wfs/feature_field.html,sha256=TmUNCme7E2vxofVVhiysD1A0ZTXhtDzd78ogGn9Y9lc,613
61
+ gisserver/templates/gisserver/wfs/feature_type.html,sha256=Kl_0c7nnlc7Z1bzJOq8v7fCbU7DLuWC3uJWrde1-_4Y,2395
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.1.dist-info/METADATA,sha256=SrC9CXZWeRJXbNIKEZzf2DlJHY_gtPnhmCD9NWn8eqo,6851
66
+ django_gisserver-2.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
67
+ django_gisserver-2.1.1.dist-info/top_level.txt,sha256=8zFCEMmkpixE4TPiOAlxSq3PD2EXvAeFKwG4yZoIIOQ,10
68
+ django_gisserver-2.1.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.1" # follows PEP440
gisserver/conf.py CHANGED
@@ -4,6 +4,8 @@ from django.conf import settings
4
4
  from django.core.signals import setting_changed
5
5
  from django.dispatch import receiver
6
6
 
7
+ _originals = {}
8
+
7
9
  # -- feature flags
8
10
 
9
11
  # Configure whether the <ows:WGS84BoundingBox> should be included in GetCapabilities.
@@ -30,6 +32,17 @@ GISSERVER_COUNT_NUMBER_MATCHED = getattr(settings, "GISSERVER_COUNT_NUMBER_MATCH
30
32
 
31
33
  # -- output rendering
32
34
 
35
+ # Following https://docs.geoserver.org/stable/en/user/services/wfs/axis_order.html here:
36
+ # Whether the older EPSG:4326 notation (instead of the OGC recommended styles)
37
+ # should render in legacy longitude/latitude (x/y) ordering.
38
+ # This increases interoperability with legacy web clients,
39
+ # as others use urn:ogc:def:crs:EPSG::4326 or http://www.opengis.net/def/crs/epsg/0/4326.
40
+ GISSERVER_FORCE_XY_EPSG_4326 = getattr(settings, "GISSERVER_FORCE_XY_EPSG_4326", True)
41
+
42
+ # Whether the legacy CRS notation http://www.opengis.net/gml/srs/epsg.xml# should render in X/Y
43
+ GISSERVER_FORCE_XY_OLD_CRS = getattr(settings, "GISSERVER_FORCE_XY_OLD_CRS", True)
44
+
45
+ # Extra output formats for GetFeature (see documentation for details)
33
46
  GISSERVER_EXTRA_OUTPUT_FORMATS = getattr(settings, "GISSERVER_EXTRA_OUTPUT_FORMATS", {})
34
47
  GISSERVER_GET_FEATURE_OUTPUT_FORMATS = getattr(
35
48
  settings, "GISSERVER_GET_FEATURE_OUTPUT_FORMATS", {}
@@ -59,4 +72,13 @@ def _on_settings_change(setting, value, enter, **kwargs):
59
72
  if not setting.startswith("GISSERVER_"):
60
73
  return
61
74
 
62
- globals()[setting] = value
75
+ conf_module = globals()
76
+ if value is None and not enter:
77
+ # override_settings().disable() returns what the django settings module had.
78
+ # Revert to our defaults here instead.
79
+ value = _originals.get(setting)
80
+ else:
81
+ # Track defaults of this file for reverting to them
82
+ _originals.setdefault(setting, conf_module[setting])
83
+
84
+ conf_module[setting] = value
gisserver/crs.py ADDED
@@ -0,0 +1,452 @@
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 import conf
19
+ from gisserver.exceptions import ExternalValueError
20
+
21
+ CRS_URN_REGEX = re.compile(
22
+ r"^urn:(?P<domain>[a-z]+)"
23
+ r":def:crs:(?P<authority>[a-z]+)"
24
+ r":(?P<version>[0-9]+(\.[0-9]+(\.[0-9]+)?)?)?"
25
+ r":(?P<id>[0-9]+|crs84)"
26
+ r"$",
27
+ re.IGNORECASE,
28
+ )
29
+
30
+ AnyGeometry = typing.TypeVar("AnyGeometry", GEOSGeometry, OGRGeometry)
31
+
32
+ __all__ = [
33
+ "CRS",
34
+ "CRS84",
35
+ "WEB_MERCATOR",
36
+ "WGS84",
37
+ ]
38
+
39
+ # Caches to avoid reinitializing WGS84 each time.
40
+ _COMMON_CRS_BY_STR = {}
41
+ _COMMON_CRS_BY_SRID = {}
42
+
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ @lru_cache(maxsize=200) # Using lru-cache to avoid repeated GDAL c-object construction
48
+ def _get_spatial_reference(srs_input: str | int, srs_type, axis_order):
49
+ """Construct an GDAL object reference"""
50
+ logger.debug(
51
+ "Constructed GDAL SpatialReference(%r, srs_type=%r, axis_order=%s)",
52
+ srs_input,
53
+ srs_type,
54
+ axis_order,
55
+ )
56
+ return SpatialReference(srs_input, srs_type=srs_type, axis_order=axis_order)
57
+
58
+
59
+ @lru_cache(maxsize=100)
60
+ def _get_coord_transform(source: SpatialReference, target: SpatialReference) -> CoordTransform:
61
+ """Get an efficient coordinate transformation object.
62
+
63
+ The CoordTransform should be used when performing the same
64
+ coordinate transformation repeatedly on different geometries.
65
+
66
+ Using a CoordinateTransform also allows setting the AxisOrder setting
67
+ on both ends. When calling ``GEOSGeometry.transform()``, Django will
68
+ create an internal CoordTransform object internally without setting AxisOrder,
69
+ implicitly setting its source SpatialReference to be 'AxisOrder.TRADITIONAL'.
70
+ """
71
+ return CoordTransform(source, target)
72
+
73
+
74
+ _get_proj_crs_from_string = lru_cache(maxsize=10)(pyproj.CRS.from_string)
75
+ _get_proj_crs_from_authority = lru_cache(maxsize=10)(pyproj.CRS.from_authority)
76
+
77
+
78
+ @dataclass(frozen=True, eq=False)
79
+ class CRS:
80
+ """
81
+ Represents a CRS (Coordinate Reference System), which preferably follows the URN format
82
+ as specified by `the OGC consortium <http://www.opengeospatial.org/ogcUrnPolicy>`_.
83
+ """
84
+
85
+ # CRS logic, based upon https://github.com/wglas85/django-wfs/blob/master/wfs/helpers.py
86
+ # Copyright (c) 2006 Wolfgang Glas - Apache 2.0 licensed
87
+ # Ported to Python 3.6 style.
88
+
89
+ #: Either "ogc" or "opengis", whereas "ogc" is highly recommended.
90
+ domain: str
91
+
92
+ #: Either "OGC" or "EPSG".
93
+ authority: str
94
+
95
+ #: The version of the authorities' SRS registry, which is empty or
96
+ #: contains two or three numeric components separated by dots like "6.9" or "6.11.9".
97
+ #: For WFS 2.0 this is typically empty.
98
+ version: str
99
+
100
+ #: A string representation of the coordinate system reference ID.
101
+ #: For OGC, only "CRS84" is supported as crsid. For EPSG, this is the formatted CRSID.
102
+ crsid: str
103
+
104
+ #: The integer representing the numeric spatial reference ID as
105
+ #: used by the EPSG and GIS database backends.
106
+ srid: int
107
+
108
+ #: GDAL SpatialReference with PROJ.4 / WKT content to describe the exact transformation.
109
+ backends: tuple[SpatialReference, SpatialReference] = (None, None)
110
+
111
+ #: Original input
112
+ origin: str = field(init=False, default=None)
113
+
114
+ #: Tell whether the input format used the legacy notation.
115
+ force_xy: bool = False
116
+
117
+ @classmethod
118
+ def from_string(cls, uri: str | int) -> CRS:
119
+ """
120
+ Parse an CRS (Coordinate Reference System) URI, which preferably follows the URN format
121
+ as specified by `the OGC consortium <http://www.opengeospatial.org/ogcUrnPolicy>`_
122
+ and construct a new CRS instance.
123
+
124
+ The value can be 3 things:
125
+
126
+ * A URI in OGC URN format.
127
+ * A legacy CRS URI ("epsg:<SRID>", or "http://www.opengis.net/...").
128
+ * A numeric SRID (which calls :meth:`from_srid()`)
129
+ """
130
+ if known_crs := _COMMON_CRS_BY_STR.get(uri):
131
+ return known_crs # Avoid object re-creation
132
+
133
+ if isinstance(uri, int) or uri.isdigit():
134
+ return cls.from_srid(int(uri))
135
+ elif uri.startswith("urn:"):
136
+ return cls._from_urn(uri)
137
+ else:
138
+ return cls._from_prefix(uri)
139
+
140
+ @classmethod
141
+ def from_srid(cls, srid: int):
142
+ """Instantiate this class using a numeric spatial reference ID
143
+
144
+ This is logically identical to calling::
145
+
146
+ CRS.from_string("urn:ogc:def:crs:EPSG::<SRID>")
147
+ """
148
+ if common_crs := _COMMON_CRS_BY_SRID.get(srid):
149
+ return common_crs # Avoid object re-creation
150
+
151
+ return cls(
152
+ domain="ogc",
153
+ authority="EPSG",
154
+ version="",
155
+ crsid=str(srid),
156
+ srid=int(srid),
157
+ )
158
+
159
+ @classmethod
160
+ def _from_urn(cls, urn): # noqa: C901
161
+ """Instantiate this class using a URN format.
162
+ This format is defined in https://portal.ogc.org/files/?artifact_id=30575.
163
+ """
164
+ urn_match = CRS_URN_REGEX.match(urn)
165
+ if not urn_match:
166
+ raise ExternalValueError(f"Unknown CRS URN [{urn}] specified: {CRS_URN_REGEX.pattern}")
167
+
168
+ domain = urn_match.group("domain")
169
+ authority = urn_match.group("authority").upper()
170
+
171
+ if domain not in ("ogc", "opengis"):
172
+ raise ExternalValueError(f"CRS URI [{urn}] contains unknown domain [{domain}]")
173
+
174
+ if authority == "EPSG":
175
+ crsid = urn_match.group("id")
176
+ try:
177
+ srid = int(crsid)
178
+ except ValueError:
179
+ raise ExternalValueError(
180
+ f"CRS URI [{urn}] should contain a numeric SRID value."
181
+ ) from None
182
+ elif authority == "OGC":
183
+ # urn:ogc:def:crs:OGC::CRS84 has x/y ordering (longitude/latitude)
184
+ crsid = urn_match.group("id").upper()
185
+ if crsid not in ("CRS84", "84"):
186
+ raise ExternalValueError(f"OGC CRS URI from [{urn}] contains unknown id [{id}]")
187
+ srid = 4326
188
+ else:
189
+ raise ExternalValueError(f"CRS URI [{urn}] contains unknown authority [{authority}]")
190
+
191
+ crs = cls(
192
+ domain=domain,
193
+ authority=authority,
194
+ version=urn_match.group(3) or "",
195
+ crsid=crsid,
196
+ srid=srid,
197
+ )
198
+ crs.__dict__["origin"] = urn
199
+ return crs
200
+
201
+ @classmethod
202
+ def _from_prefix(cls, uri):
203
+ """Instantiate this class from a non-URI notation.
204
+
205
+ The modern URL format (:samp:`http://www.opengis.net/def/crs/epsg/0/{xxxx}`)
206
+ is defined in https://portal.ogc.org/files/?artifact_id=46361.
207
+
208
+ Older notations like :samp:`EPSG:{xxxx}` or legacy XML URLs like
209
+ and :samp`:http://www.opengis.net/gml/srs/epsg.xml#{xxxx}` are also supported.
210
+ """
211
+ # Make sure origin uses the expected upper/lowercasing.
212
+ origin = uri.lower() if "://" in uri else uri.upper()
213
+ for prefix, force_xy in (
214
+ (
215
+ "EPSG:",
216
+ (conf.GISSERVER_FORCE_XY_EPSG_4326 and origin == "EPSG:4326"),
217
+ ),
218
+ (
219
+ "http://www.opengis.net/gml/srs/epsg.xml#",
220
+ conf.GISSERVER_FORCE_XY_OLD_CRS,
221
+ ),
222
+ ("http://www.opengis.net/def/crs/epsg/0/", False),
223
+ ):
224
+ if origin.startswith(prefix):
225
+ crsid = origin[len(prefix) :]
226
+ try:
227
+ srid = int(crsid)
228
+ except ValueError:
229
+ raise ExternalValueError(
230
+ f"CRS URI [{uri}] should contain a numeric SRID value."
231
+ ) from None
232
+
233
+ crs = cls(
234
+ domain="ogc",
235
+ authority="EPSG",
236
+ version="",
237
+ crsid=crsid,
238
+ srid=srid,
239
+ force_xy=force_xy,
240
+ )
241
+ crs.__dict__["origin"] = origin
242
+ return crs
243
+
244
+ raise ExternalValueError(f"Unknown CRS URI [{uri}] specified")
245
+
246
+ @property
247
+ def legacy(self):
248
+ """Return a legacy string in the format :samp:`http://www.opengis.net/gml/srs/epsg.xml#{srid}`."""
249
+ # This mirrors what GeoSever does, as this notation always has an axis ordering defined.
250
+ # Notations like EPSG:xxxx notation don't have such consistent usage.
251
+ return f"http://www.opengis.net/gml/srs/epsg.xml#{self.srid:d}"
252
+
253
+ @cached_property
254
+ def urn(self):
255
+ """Return The OGC URN corresponding to this CRS."""
256
+ return f"urn:{self.domain}:def:crs:{self.authority}:{self.version or ''}:{self.crsid}"
257
+
258
+ @property
259
+ def is_north_east_order(self) -> bool:
260
+ """Tell whether the axis is in north/east ordering."""
261
+ return self.axis_direction == ["north", "east"]
262
+
263
+ @cached_property
264
+ def axis_direction(self) -> list[str]:
265
+ """Tell what the axis ordering of this coordinate system is.
266
+
267
+ For example, WGS84 will return ``['north', 'east']``.
268
+
269
+ While computer systems typically use X,Y, other systems may use northing/easting.
270
+ Historically, latitude was easier to measure and given first.
271
+ In physics, using 'radial, polar, azimuthal' is again a different perspective.
272
+ See: https://wiki.osgeo.org/wiki/Axis_Order_Confusion for a good summary.
273
+ """
274
+ proj_crs = self._as_proj()
275
+ return [axis.direction for axis in proj_crs.axis_info]
276
+
277
+ def __str__(self):
278
+ return self.legacy if self.force_xy else self.urn
279
+
280
+ def __eq__(self, other):
281
+ if isinstance(other, CRS):
282
+ return self.matches(other, compare_legacy=True)
283
+ else:
284
+ return NotImplemented
285
+
286
+ def matches(self, other, compare_legacy=True) -> bool:
287
+ """Tell whether this CRS is identical to another one."""
288
+ return (
289
+ # "urn:ogc:def:crs:EPSG::4326" != "urn:ogc:def:crs:OGC::CRS84"
290
+ # even through they both share the same srid.
291
+ self.srid == other.srid
292
+ and self.authority == other.authority
293
+ and (
294
+ # Also, legacy notations like EPSG:4326 are treated differently,
295
+ # unless this is configured to not make a difference.
296
+ not compare_legacy
297
+ or self.force_xy == other.force_xy
298
+ )
299
+ )
300
+
301
+ def __hash__(self):
302
+ """Used to match objects in a set."""
303
+ return hash((self.authority, self.srid))
304
+
305
+ def _as_gdal(self, axis_order: AxisOrder) -> SpatialReference:
306
+ """Generate the GDAL Spatial Reference object."""
307
+ if self.backends[axis_order] is None:
308
+ backends = list(self.backends)
309
+ if self.origin and "://" not in self.origin: # avoid downloads and OGR errors
310
+ # Passing the origin helps to detect CRS84 strings
311
+ backends[axis_order] = _get_spatial_reference(self.origin, "user", axis_order)
312
+ else:
313
+ backends[axis_order] = _get_spatial_reference(self.srid, "epsg", axis_order)
314
+
315
+ # Write back in "readonly" format.
316
+ self.__dict__["backends"] = tuple(backends)
317
+
318
+ return self.backends[axis_order]
319
+
320
+ def _as_proj(self) -> pyproj.CRS:
321
+ """Generate the PROJ CRS object"""
322
+ if (
323
+ isinstance(self.origin, str)
324
+ and not self.origin.isdigit()
325
+ and "epsg.xml#" not in self.origin # not supported
326
+ ):
327
+ # Passing the origin helps to detect CRS84 strings
328
+ return _get_proj_crs_from_string(self.origin)
329
+ else:
330
+ return _get_proj_crs_from_authority(self.authority, self.srid)
331
+
332
+ def apply_to(
333
+ self,
334
+ geometry: AnyGeometry,
335
+ clone=False,
336
+ axis_order: AxisOrder | None = None,
337
+ ) -> AnyGeometry | None:
338
+ """Transform the geometry using this coordinate reference.
339
+
340
+ Every transformation within this package happens through this method,
341
+ giving full control over coordinate transformations.
342
+
343
+ A bit of background: geometries are provided as ``GEOSGeometry`` from the database.
344
+ This is basically a simple C-based storage implementing "OpenGIS Simple Features for SQL",
345
+ except it does *not* store axis orientation. These are assumed to be x/y.
346
+
347
+ To perform transformations, GeoDjango loads the GEOS-geometry into GDAL/OGR.
348
+ The transformed geometry is loaded back to GEOS. To avoid this conversion,
349
+ pass the OGR object directly and continue working on that.
350
+
351
+ Internally, this method caches the used GDAL ``CoordTransform`` object,
352
+ so repeated transformations of the same coordinate systems are faster.
353
+
354
+ The axis order can change during the transformation.
355
+ From a programming perspective, screen coordinates (x/y) were traditionally used.
356
+ However, various systems and industries have always worked with north/east (y/x).
357
+ This includes systems with critical safety requirements in aviation and maritime.
358
+ The CRS authority reflects this practice. What you need depends on the use case:
359
+
360
+ * The (GML) output of WFS 2.0 and WMS 1.3 respect the axis ordering of the CRS.
361
+ * GeoJSON always provides coordinates in x/y, to keep web-based clients simple.
362
+ * PostGIS stores the data in x/y.
363
+ * WFS 1.0 used x/y, WFS 1.3 used y/x except for ``EPSG:4326``.
364
+
365
+ When receiving legacy notations (e.g. ``EPSG:4326`` instead of ``urn:ogc:def:crs:EPSG::4326``),
366
+ the data is still projected in legacy ordering, unless ``GISSERVER_FORCE_XY_...`` is disabled.
367
+ This reflects the design of `GeoServer Axis Ordering
368
+ <https://docs.geoserver.org/stable/en/user/services/wfs/axis_order.html>`_
369
+ to have maximum interoperability with legacy/JavaScript clients.
370
+
371
+ After GDAL/OGR changed the axis orientation, that information is
372
+ lost when the return value is loaded back into GEOS.
373
+ To address this, :meth:`tag_geometry` is called on the result.
374
+
375
+ :param geometry: The GEOS Geometry, or GDAL/OGR loaded geometry.
376
+ :param clone: Whether the object is changed in-place, or a copy is returned.
377
+ For GEOS->GDAL->GEOS conversions, this makes no difference in efficiency.
378
+ :param axis_order: Which axis ordering to convert the geometry into (depends on the use-case).
379
+ """
380
+ if axis_order is None:
381
+ # This transforms by default to WFS 2 axis ordering (e.g. latitude/longitude),
382
+ # unless a legacy notation is used (e.g. EPSG:4326).
383
+ axis_order = AxisOrder.TRADITIONAL if self.force_xy else AxisOrder.AUTHORITY
384
+
385
+ if isinstance(geometry, OGRGeometry):
386
+ transform = _get_coord_transform(geometry.srs, self._as_gdal(axis_order=axis_order))
387
+ return geometry.transform(transform, clone=clone)
388
+ else:
389
+ # See if the geometry was tagged with a CRS.
390
+ # When the data comes from an unknown source, assume this is from database storage.
391
+ # PostGIS stores data in longitude/latitude (x/y) ordering, even for srid 4326.
392
+ # By passing the 'AxisOrder.TRADITIONAL', a conversion from 4326 to 4326
393
+ # with 'AxisOrder.AUTHORITY' will detect that coordinate ordering needs to be changed.
394
+ source_axis_order = getattr(geometry, "_axis_order", AxisOrder.TRADITIONAL)
395
+
396
+ if self.srid == geometry.srid and source_axis_order == axis_order:
397
+ # Avoid changes if spatial reference system is identical, and no axis need to change.
398
+ if clone:
399
+ return geometry.clone()
400
+ else:
401
+ return None
402
+
403
+ # Get GDAL spatial reference for converting coordinates (uses proj internally).
404
+ # Using a cached coordinate transform object (is faster for repeated transforms)
405
+ # The object is also tagged so another apply_to() call would recognize the state.
406
+ source = _get_spatial_reference(geometry.srid, "epsg", source_axis_order)
407
+ target = self._as_gdal(axis_order=axis_order)
408
+ transform = _get_coord_transform(source, target)
409
+
410
+ # Transform
411
+ geometry = geometry.transform(transform, clone=clone)
412
+ if clone:
413
+ self.tag_geometry(geometry, axis_order=axis_order)
414
+ return geometry
415
+
416
+ @classmethod
417
+ def tag_geometry(self, geometry: GEOSGeometry, axis_order: AxisOrder):
418
+ """Associate this object with the geometry.
419
+
420
+ This informs the :meth:`apply_to` method that this source geometry
421
+ already had the correct axis ordering (e.g. it was part of the ``<fes:BBOX>`` logic).
422
+ The srid integer doesn't communicate that information.
423
+ """
424
+ geometry._axis_order = axis_order
425
+
426
+ def cache_instance(self):
427
+ """Cache a common CRS, no need to re-instantiate the same object again.
428
+ This also makes sure that requests which use the same URN will get our CRS object
429
+ version, instead of a fresh new one.
430
+ """
431
+ if self.authority == "EPSG" and self.srid != 4326:
432
+ # Only register for EPSG to avoid conflicting axis ordering issues.
433
+ # (WGS84 and CRS84 both use srid 4326, and the 'EPSG:4326' notation is treated as legacy)
434
+ _COMMON_CRS_BY_SRID[self.srid] = self
435
+
436
+ _COMMON_CRS_BY_STR[str(self)] = self
437
+
438
+
439
+ #: Worldwide GPS, latitude/longitude (y/x). https://epsg.io/4326
440
+ WGS84 = CRS.from_string("urn:ogc:def:crs:EPSG::4326")
441
+
442
+ #: GeoJSON default. This is like WGS84 but with longitude/latitude (x/y).
443
+ CRS84 = CRS.from_string("urn:ogc:def:crs:OGC::CRS84")
444
+
445
+ #: Spherical Mercator (Google Maps, Bing Maps, OpenStreetMap, ...), see https://epsg.io/3857
446
+ WEB_MERCATOR = CRS.from_string("urn:ogc:def:crs:EPSG::3857")
447
+
448
+
449
+ # Register these common ones:
450
+ WGS84.cache_instance()
451
+ CRS84.cache_instance()
452
+ WEB_MERCATOR.cache_instance()