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.
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/METADATA +27 -10
- django_gisserver-2.1.1.dist-info/RECORD +68 -0
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/conf.py +23 -1
- gisserver/crs.py +452 -0
- gisserver/db.py +78 -6
- gisserver/exceptions.py +106 -2
- gisserver/extensions/functions.py +122 -28
- gisserver/extensions/queries.py +15 -10
- gisserver/features.py +46 -33
- gisserver/geometries.py +64 -306
- gisserver/management/commands/loadgeojson.py +41 -21
- gisserver/operations/base.py +11 -7
- gisserver/operations/wfs20.py +31 -93
- gisserver/output/__init__.py +6 -2
- gisserver/output/base.py +28 -13
- gisserver/output/csv.py +18 -6
- gisserver/output/geojson.py +7 -6
- gisserver/output/gml32.py +86 -27
- gisserver/output/results.py +25 -39
- gisserver/output/utils.py +9 -2
- gisserver/parsers/ast.py +177 -68
- gisserver/parsers/fes20/__init__.py +76 -4
- gisserver/parsers/fes20/expressions.py +97 -27
- gisserver/parsers/fes20/filters.py +9 -6
- gisserver/parsers/fes20/identifiers.py +27 -7
- gisserver/parsers/fes20/lookups.py +8 -6
- gisserver/parsers/fes20/operators.py +101 -49
- gisserver/parsers/fes20/sorting.py +14 -6
- gisserver/parsers/gml/__init__.py +10 -19
- gisserver/parsers/gml/base.py +32 -14
- gisserver/parsers/gml/geometries.py +54 -21
- gisserver/parsers/ows/kvp.py +10 -2
- gisserver/parsers/ows/requests.py +6 -4
- gisserver/parsers/query.py +6 -2
- gisserver/parsers/values.py +61 -4
- gisserver/parsers/wfs20/__init__.py +2 -0
- gisserver/parsers/wfs20/adhoc.py +28 -18
- gisserver/parsers/wfs20/base.py +12 -7
- gisserver/parsers/wfs20/projection.py +3 -3
- gisserver/parsers/wfs20/requests.py +1 -0
- gisserver/parsers/wfs20/stored.py +3 -2
- gisserver/parsers/xml.py +12 -0
- gisserver/projection.py +17 -7
- gisserver/static/gisserver/index.css +27 -6
- gisserver/templates/gisserver/base.html +15 -0
- gisserver/templates/gisserver/index.html +10 -16
- gisserver/templates/gisserver/service_description.html +12 -6
- gisserver/templates/gisserver/wfs/feature_field.html +1 -1
- gisserver/templates/gisserver/wfs/feature_type.html +44 -13
- gisserver/types.py +152 -82
- gisserver/views.py +47 -24
- django_gisserver-2.0.dist-info/RECORD +0 -66
- {django_gisserver-2.0.dist-info → django_gisserver-2.1.1.dist-info/licenses}/LICENSE +0 -0
- {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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: django-gisserver
|
|
3
|
-
Version: 2.
|
|
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.
|
|
37
|
-
Requires-Dist:
|
|
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
|
[](https://django-gisserver.readthedocs.io/en/latest/?badge=latest)
|
|
48
66
|
[](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
|
-
|
|
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,,
|
gisserver/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.
|
|
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()
|
|
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()
|