django-gisserver 2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-gisserver
3
- Version: 2.1
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
@@ -46,6 +46,7 @@ Requires-Dist: pytest-cov>=2.11.1; extra == "tests"
46
46
  Provides-Extra: docs
47
47
  Requires-Dist: Django~=5.0; extra == "docs"
48
48
  Requires-Dist: sphinxcontrib-django>=2.5; extra == "docs"
49
+ Requires-Dist: myst-parser>=3.0.1; extra == "docs"
49
50
  Requires-Dist: psycopg2-binary>=2.8.4; extra == "docs"
50
51
  Dynamic: author
51
52
  Dynamic: author-email
@@ -1,14 +1,14 @@
1
- django_gisserver-2.1.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
2
- gisserver/__init__.py,sha256=EZRBJR6U2egJTZDzOVWUz5AOLuAcwyxzkLAMaShy2TA,38
1
+ django_gisserver-2.1.1.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
2
+ gisserver/__init__.py,sha256=UUQ8Wbk0ewAYeTqfEUnsJx6oCvEq_2LWeacF6xnZ-tI,40
3
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
4
+ gisserver/conf.py,sha256=Bb2hbLx1SM4la6-e8PrBQ7dg2z3Z1sopOlaTrfa6gbg,3666
5
+ gisserver/crs.py,sha256=ChqLNZCHBCZmXphjJTT22jvhyMIwAAFtFSjwbH4Bigk,18330
6
+ gisserver/db.py,sha256=p2bb6KMjFzsEEedFx4xZ1bBOFDd19MembXUKCnkk_pM,8154
7
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
8
+ gisserver/features.py,sha256=njVzctaKBiGsuSz8ATO_4GlNsXiEM-kJcH0y2yjMFco,36445
9
+ gisserver/geometries.py,sha256=sXHr7LR4PgKImCVbENBxMBxAnz3AB8-TZWunIIGrAs0,3739
10
10
  gisserver/projection.py,sha256=yja8baq8sWblhytrD0yPlTH-v5z6pIy6mzUu1sLc2eQ,15301
11
- gisserver/types.py,sha256=Dwxs7c8jclRK9HhXub9Syv8MM1OVmfez1XP464jGuv4,43911
11
+ gisserver/types.py,sha256=XD63wq7iA3cHpdZ1r2MoxNK-sohGj5IoBCwoaA4xNzk,43939
12
12
  gisserver/views.py,sha256=vTB0-BuNKoqPKjKIwNe8xtm_erM_yD8tmkw25vppoj4,20312
13
13
  gisserver/extensions/__init__.py,sha256=MsYUsNsoxxjeDHhx4DW9eGXvmOnI5PkoUXvKxRZaRRw,136
14
14
  gisserver/extensions/functions.py,sha256=enEarl4TUfGVA0QWlURU-QZjTaLbIEjOGjNzjF7z8jI,11629
@@ -23,14 +23,14 @@ gisserver/output/__init__.py,sha256=1-bzgNq1yGuWGse2zLXUwTnR7-FpfosGCzDm97diKRM,
23
23
  gisserver/output/base.py,sha256=igcOhjJkziPlpPJjwfyyC0S1LHfRI8Em-UMNnX5Lk-E,11247
24
24
  gisserver/output/csv.py,sha256=1Bz3o3-dvdEFWWLrDj-cxCp7My6IMMakmwAZIfPsjc4,6906
25
25
  gisserver/output/geojson.py,sha256=0Mm_fqi-v1MzxWJ3tDL1OrJpcteZ9nQBgilqQ_nG7V4,11633
26
- gisserver/output/gml32.py,sha256=GNqmEx-GPgT4EvykDEauMZL5AcQcq4rahIAY6Bm9lLo,33266
26
+ gisserver/output/gml32.py,sha256=RrQp7iiMYtYf-PAcYLqO53b53GYBD3_CoTtZQIqbhyQ,35174
27
27
  gisserver/output/iters.py,sha256=YbvtIIy977Snm5m7cYyQext0GCpo8S_u4azTCBNqqgw,8355
28
28
  gisserver/output/results.py,sha256=_RNm7IVBC5j5CQY2q65MAWCdVSfRSexas7f8KtPGN9g,14668
29
29
  gisserver/output/stored.py,sha256=m3JcR4MUilyuM_5J6XJDOIyzrhRJbcpg-6FMKy69csc,5763
30
30
  gisserver/output/utils.py,sha256=dA4Y-6ZUBjjCC8ueO5AcYaVebB5M6ynXUtrw7yyOcoQ,3001
31
31
  gisserver/output/xmlschema.py,sha256=fHj8h9-vG508uRmM-kD5r2omde2wa_myJWWwcWe2lus,6445
32
32
  gisserver/parsers/__init__.py,sha256=eGORBeT0F8fR3AadV1uq8dbCeZtIeK0-__CcXwSvXgs,402
33
- gisserver/parsers/ast.py,sha256=3ereccG1m_PWNJXP_IdUKiX0I_zLpnIghubb0-p6sas,15499
33
+ gisserver/parsers/ast.py,sha256=zFb64uaIgAT9THV07l0pnuzWfXkzJ2CwT1z6lVGwrag,15688
34
34
  gisserver/parsers/query.py,sha256=IbsWBSAlOKDgz8BS-JirxVCqzQxSpYqIxGEhv0-sOVc,6695
35
35
  gisserver/parsers/values.py,sha256=QCnHBlMAGDefoC51ujOrQc1YEb3RYYP7aq5UIDweJO0,4166
36
36
  gisserver/parsers/xml.py,sha256=uys1ddpx4BEDFg1WmknfATl4vJpgpKbb0yV2QT2kJqs,9753
@@ -39,30 +39,30 @@ gisserver/parsers/fes20/expressions.py,sha256=Yw1RvotstgTIlW49_Vfa4b1iODFl8Nd34R
39
39
  gisserver/parsers/fes20/filters.py,sha256=ssS-XhnUoWayPrAx1ZNQj7RlTAu-eS47pB_a0zeWjCQ,6677
40
40
  gisserver/parsers/fes20/identifiers.py,sha256=Y1diAHHVYWBWNy4wPakXdx9DPloX-jphdbmD8DTuhhU,4107
41
41
  gisserver/parsers/fes20/lookups.py,sha256=rrWGM5s3qx-yMbz7ogg8BTxzcmQdHfqqJ_9jF7BObrg,5430
42
- gisserver/parsers/fes20/operators.py,sha256=szkIXIdvKKwNqCTOjyO_KKvpgsxHHPjMppEqWdWmzz4,33609
42
+ gisserver/parsers/fes20/operators.py,sha256=IQ-ArgRtmk9FRaBP901DqzlC2NKfQhoTv_VcgxGDs4Q,33608
43
43
  gisserver/parsers/fes20/sorting.py,sha256=RRCepyXCxD9zt8a0fu5oneV8Ta8A0-bOU5aou4v8nQE,5034
44
44
  gisserver/parsers/gml/__init__.py,sha256=T7_kSRnPTGvwoOXpcPgGeRTjO5zUxbvifkkUHUK_pLI,1484
45
45
  gisserver/parsers/gml/base.py,sha256=uBg9BRBZYAmXrfhQuhs-cVZKJqs8Ksh61CPezPN2-cU,1636
46
- gisserver/parsers/gml/geometries.py,sha256=oC8RltuylrivZSdMBMlvB_tjgWHEB6Jm_WICHRlwV1o,5710
46
+ gisserver/parsers/gml/geometries.py,sha256=DbQTYGDLMw0JRwdNrSR7ia2vIAfS5AHz6lBOFvrfLc0,6120
47
47
  gisserver/parsers/ows/__init__.py,sha256=0SeM6vfwjWI-WeRD7eS_iN4Jsl8uzXr2yMFpZVHokb4,624
48
48
  gisserver/parsers/ows/kvp.py,sha256=1NDXcaO1Z3TRxZ4F11xoNnsutq12oEyYoJv7SMX1IPE,7235
49
49
  gisserver/parsers/ows/requests.py,sha256=ob66EWEJBEbjgGoPhk8qBE0BuDn2sYPsjr4Z_elvqss,5540
50
50
  gisserver/parsers/wfs20/__init__.py,sha256=t66iqcImqerH3Z1tMiWCms3WLmJXybUDqhcEXvITaWU,924
51
- gisserver/parsers/wfs20/adhoc.py,sha256=C6uBzxs90S2Z2Cr_sNP6u7x0QMIc_jz_Zcf76asqjFA,9428
51
+ gisserver/parsers/wfs20/adhoc.py,sha256=O33rT5WEheEpJV-EOtsPf-U7PR0TmaR-MWxrTyFvQwY,9459
52
52
  gisserver/parsers/wfs20/base.py,sha256=nPXwyQryjEdwoBq78a6KLHv5wzqdDbkeW4kfmOHXvIM,5867
53
53
  gisserver/parsers/wfs20/projection.py,sha256=irGQrVyOj5q9Or1rw99A3nn3on6PlGeB4261XUx6AkE,3932
54
54
  gisserver/parsers/wfs20/requests.py,sha256=Oi4HxiMs9_ij7CEOFmyu4GeN6VKyc9lJBkvIG5zrrSM,16853
55
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
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
60
  gisserver/templates/gisserver/wfs/feature_field.html,sha256=TmUNCme7E2vxofVVhiysD1A0ZTXhtDzd78ogGn9Y9lc,613
61
- gisserver/templates/gisserver/wfs/feature_type.html,sha256=RDWwlAhrVPa-_J9Ys_AthaEXNxZ0Sqrx0Z2evHBUP64,2010
61
+ gisserver/templates/gisserver/wfs/feature_type.html,sha256=Kl_0c7nnlc7Z1bzJOq8v7fCbU7DLuWC3uJWrde1-_4Y,2395
62
62
  gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml,sha256=SGZSn7cDU_Oai6d45xrkqhax5CPyI1oIc8BZLOEw1X4,8695
63
63
  gisserver/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
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,,
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" # 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 CHANGED
@@ -15,6 +15,7 @@ import pyproj
15
15
  from django.contrib.gis.gdal import AxisOrder, CoordTransform, OGRGeometry, SpatialReference
16
16
  from django.contrib.gis.geos import GEOSGeometry
17
17
 
18
+ from gisserver import conf
18
19
  from gisserver.exceptions import ExternalValueError
19
20
 
20
21
  CRS_URN_REGEX = re.compile(
@@ -36,7 +37,7 @@ __all__ = [
36
37
  ]
37
38
 
38
39
  # Caches to avoid reinitializing WGS84 each time.
39
- _COMMON_CRS_BY_URN = {}
40
+ _COMMON_CRS_BY_STR = {}
40
41
  _COMMON_CRS_BY_SRID = {}
41
42
 
42
43
 
@@ -74,7 +75,7 @@ _get_proj_crs_from_string = lru_cache(maxsize=10)(pyproj.CRS.from_string)
74
75
  _get_proj_crs_from_authority = lru_cache(maxsize=10)(pyproj.CRS.from_authority)
75
76
 
76
77
 
77
- @dataclass(frozen=True)
78
+ @dataclass(frozen=True, eq=False)
78
79
  class CRS:
79
80
  """
80
81
  Represents a CRS (Coordinate Reference System), which preferably follows the URN format
@@ -110,6 +111,9 @@ class CRS:
110
111
  #: Original input
111
112
  origin: str = field(init=False, default=None)
112
113
 
114
+ #: Tell whether the input format used the legacy notation.
115
+ force_xy: bool = False
116
+
113
117
  @classmethod
114
118
  def from_string(cls, uri: str | int) -> CRS:
115
119
  """
@@ -123,12 +127,15 @@ class CRS:
123
127
  * A legacy CRS URI ("epsg:<SRID>", or "http://www.opengis.net/...").
124
128
  * A numeric SRID (which calls :meth:`from_srid()`)
125
129
  """
130
+ if known_crs := _COMMON_CRS_BY_STR.get(uri):
131
+ return known_crs # Avoid object re-creation
132
+
126
133
  if isinstance(uri, int) or uri.isdigit():
127
134
  return cls.from_srid(int(uri))
128
135
  elif uri.startswith("urn:"):
129
136
  return cls._from_urn(uri)
130
137
  else:
131
- return cls._from_legacy(uri)
138
+ return cls._from_prefix(uri)
132
139
 
133
140
  @classmethod
134
141
  def from_srid(cls, srid: int):
@@ -141,22 +148,19 @@ class CRS:
141
148
  if common_crs := _COMMON_CRS_BY_SRID.get(srid):
142
149
  return common_crs # Avoid object re-creation
143
150
 
144
- crs = cls(
151
+ return cls(
145
152
  domain="ogc",
146
153
  authority="EPSG",
147
154
  version="",
148
155
  crsid=str(srid),
149
156
  srid=int(srid),
150
157
  )
151
- crs.__dict__["origin"] = srid
152
- return crs
153
158
 
154
159
  @classmethod
155
160
  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
-
161
+ """Instantiate this class using a URN format.
162
+ This format is defined in https://portal.ogc.org/files/?artifact_id=30575.
163
+ """
160
164
  urn_match = CRS_URN_REGEX.match(urn)
161
165
  if not urn_match:
162
166
  raise ExternalValueError(f"Unknown CRS URN [{urn}] specified: {CRS_URN_REGEX.pattern}")
@@ -187,7 +191,7 @@ class CRS:
187
191
  crs = cls(
188
192
  domain=domain,
189
193
  authority=authority,
190
- version=urn_match.group(3),
194
+ version=urn_match.group(3) or "",
191
195
  crsid=crsid,
192
196
  srid=srid,
193
197
  )
@@ -195,16 +199,30 @@ class CRS:
195
199
  return crs
196
200
 
197
201
  @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#",
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),
205
223
  ):
206
- if luri.startswith(head):
207
- crsid = luri[len(head) :]
224
+ if origin.startswith(prefix):
225
+ crsid = origin[len(prefix) :]
208
226
  try:
209
227
  srid = int(crsid)
210
228
  except ValueError:
@@ -218,16 +236,19 @@ class CRS:
218
236
  version="",
219
237
  crsid=crsid,
220
238
  srid=srid,
239
+ force_xy=force_xy,
221
240
  )
222
- crs.__dict__["origin"] = uri
241
+ crs.__dict__["origin"] = origin
223
242
  return crs
224
243
 
225
244
  raise ExternalValueError(f"Unknown CRS URI [{uri}] specified")
226
245
 
227
246
  @property
228
247
  def legacy(self):
229
- """Return a legacy string in the format :samp:`EPSG:{srid}`."""
230
- return f"EPSG:{self.srid:d}"
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}"
231
252
 
232
253
  @cached_property
233
254
  def urn(self):
@@ -254,16 +275,29 @@ class CRS:
254
275
  return [axis.direction for axis in proj_crs.axis_info]
255
276
 
256
277
  def __str__(self):
257
- return self.urn
278
+ return self.legacy if self.force_xy else self.urn
258
279
 
259
280
  def __eq__(self, other):
260
281
  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
282
+ return self.matches(other, compare_legacy=True)
264
283
  else:
265
284
  return NotImplemented
266
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
+
267
301
  def __hash__(self):
268
302
  """Used to match objects in a set."""
269
303
  return hash((self.authority, self.srid))
@@ -272,7 +306,8 @@ class CRS:
272
306
  """Generate the GDAL Spatial Reference object."""
273
307
  if self.backends[axis_order] is None:
274
308
  backends = list(self.backends)
275
- if self.origin:
309
+ if self.origin and "://" not in self.origin: # avoid downloads and OGR errors
310
+ # Passing the origin helps to detect CRS84 strings
276
311
  backends[axis_order] = _get_spatial_reference(self.origin, "user", axis_order)
277
312
  else:
278
313
  backends[axis_order] = _get_spatial_reference(self.srid, "epsg", axis_order)
@@ -284,7 +319,12 @@ class CRS:
284
319
 
285
320
  def _as_proj(self) -> pyproj.CRS:
286
321
  """Generate the PROJ CRS object"""
287
- if isinstance(self.origin, str) and not self.origin.isdigit():
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
288
328
  return _get_proj_crs_from_string(self.origin)
289
329
  else:
290
330
  return _get_proj_crs_from_authority(self.authority, self.srid)
@@ -293,7 +333,7 @@ class CRS:
293
333
  self,
294
334
  geometry: AnyGeometry,
295
335
  clone=False,
296
- axis_order: AxisOrder = AxisOrder.AUTHORITY,
336
+ axis_order: AxisOrder | None = None,
297
337
  ) -> AnyGeometry | None:
298
338
  """Transform the geometry using this coordinate reference.
299
339
 
@@ -322,6 +362,12 @@ class CRS:
322
362
  * PostGIS stores the data in x/y.
323
363
  * WFS 1.0 used x/y, WFS 1.3 used y/x except for ``EPSG:4326``.
324
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
+
325
371
  After GDAL/OGR changed the axis orientation, that information is
326
372
  lost when the return value is loaded back into GEOS.
327
373
  To address this, :meth:`tag_geometry` is called on the result.
@@ -331,6 +377,11 @@ class CRS:
331
377
  For GEOS->GDAL->GEOS conversions, this makes no difference in efficiency.
332
378
  :param axis_order: Which axis ordering to convert the geometry into (depends on the use-case).
333
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
+
334
385
  if isinstance(geometry, OGRGeometry):
335
386
  transform = _get_coord_transform(geometry.srs, self._as_gdal(axis_order=axis_order))
336
387
  return geometry.transform(transform, clone=clone)
@@ -377,12 +428,12 @@ class CRS:
377
428
  This also makes sure that requests which use the same URN will get our CRS object
378
429
  version, instead of a fresh new one.
379
430
  """
380
- if self.authority == "EPSG":
431
+ if self.authority == "EPSG" and self.srid != 4326:
381
432
  # Only register for EPSG to avoid conflicting axis ordering issues.
382
- # (WGS84 and CRS84 both use srid 4326)
433
+ # (WGS84 and CRS84 both use srid 4326, and the 'EPSG:4326' notation is treated as legacy)
383
434
  _COMMON_CRS_BY_SRID[self.srid] = self
384
435
 
385
- _COMMON_CRS_BY_URN[self.urn] = self
436
+ _COMMON_CRS_BY_STR[str(self)] = self
386
437
 
387
438
 
388
439
  #: Worldwide GPS, latitude/longitude (y/x). https://epsg.io/4326
gisserver/db.py CHANGED
@@ -42,16 +42,22 @@ class AsGML(functions.AsGML):
42
42
  precision=conf.GISSERVER_DB_PRECISION,
43
43
  envelope=False,
44
44
  is_latlon=False,
45
+ long_urn=False,
45
46
  **extra,
46
47
  ):
47
48
  # Note that Django's AsGml the defaults are: version=2, precision=8
48
49
  super().__init__(expression, version, precision, **extra)
49
50
  self.envelope = envelope
50
51
  self.is_latlon = is_latlon
52
+ self.long_urn = long_urn
51
53
 
52
54
  def as_postgresql(self, compiler, connection, **extra_context):
53
55
  # Fill options parameter (https://postgis.net/docs/ST_AsGML.html)
54
- options = 33 if self.envelope else 1 # 32 = bbox, 1 = long CRS urn
56
+ options = 0
57
+ if self.long_urn:
58
+ options |= 1 # long CRS urn
59
+ if self.envelope:
60
+ options |= 32 # bbox
55
61
  if self.is_latlon:
56
62
  # PostGIS provides the data in longitude/latitude format (east/north to look like x/y).
57
63
  # However, WFS 2.0 fixed their axis by following the authority. The ST_AsGML() doesn't
gisserver/features.py CHANGED
@@ -846,19 +846,24 @@ class FeatureType:
846
846
 
847
847
  def resolve_crs(self, crs: CRS, locator="") -> CRS:
848
848
  """Check a parsed CRS against the list of supported types."""
849
- try:
850
- pos = self.supported_crs.index(crs)
851
-
852
- # Replace the parsed CRS with the declared one, which may have a 'backend' configured.
853
- return self.supported_crs[pos]
854
- except ValueError:
855
- if conf.GISSERVER_SUPPORTED_CRS_ONLY:
856
- raise InvalidParameterValue(
857
- f"Feature '{self.name}' does not support SRID {crs.srid}.",
858
- locator=locator,
859
- ) from None
860
- else:
861
- return crs
849
+ for candidate in self.supported_crs:
850
+ # Not using self.supported_crs.index(crs), as that depends on CRS.__eq__():
851
+ if candidate.matches(crs, compare_legacy=False):
852
+ if candidate.force_xy != crs.force_xy:
853
+ # user provided legacy CRS, allow output in legacy CRS
854
+ return crs
855
+ else:
856
+ # Replace the parsed CRS with the declared one.
857
+ return candidate
858
+
859
+ # No match found
860
+ if conf.GISSERVER_SUPPORTED_CRS_ONLY:
861
+ raise InvalidParameterValue(
862
+ f"Feature '{self.name}' does not support CRS '{crs}'.",
863
+ locator=locator,
864
+ ) from None
865
+ else:
866
+ return crs
862
867
 
863
868
 
864
869
  class HDict(dict):
gisserver/geometries.py CHANGED
@@ -27,7 +27,7 @@ class BoundingBox:
27
27
  Due to the overlap between 2 types, this element is used for 2 cases:
28
28
 
29
29
  * The ``<ows:WGS84BoundingBox>`` element for ``GetCapabilities``.
30
- * The ``<gml:Envelope>`` inside an``<gml:boundedBy>`` single feature.
30
+ * The ``<gml:Envelope>`` inside an ``<gml:boundedBy>`` single feature.
31
31
 
32
32
  While both classes have no common base class (and exist in different schema's),
33
33
  their properties are identical.
gisserver/output/gml32.py CHANGED
@@ -11,6 +11,7 @@ much extra method calls per field. Some bits are non-DRY inlined for this reason
11
11
  from __future__ import annotations
12
12
 
13
13
  import itertools
14
+ import re
14
15
  from collections import defaultdict
15
16
  from datetime import date, datetime, time, timezone
16
17
  from decimal import Decimal as D
@@ -23,6 +24,7 @@ from django.core.exceptions import ImproperlyConfigured
23
24
  from django.db import models
24
25
  from django.http import HttpResponse
25
26
 
27
+ from gisserver.crs import CRS84
26
28
  from gisserver.db import (
27
29
  AsGML,
28
30
  get_db_geometry_target,
@@ -46,6 +48,7 @@ from .utils import (
46
48
  )
47
49
 
48
50
  GML_RENDER_FUNCTIONS = {}
51
+ RE_SRS_NAME = re.compile(r'srsName="([^"]+)"')
49
52
 
50
53
 
51
54
  def register_geos_type(geos_type):
@@ -232,6 +235,15 @@ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
232
235
 
233
236
  for sub_collection in collection.results:
234
237
  projection = sub_collection.projection
238
+ if projection.output_crs.force_xy and projection.output_crs.is_north_east_order:
239
+ self._write(
240
+ "<!--\n"
241
+ f" NOTE: you are requesting the legacy projection notation '{tag_escape(projection.output_crs.origin)}'."
242
+ f" Please use '{tag_escape(projection.output_crs.urn)}' instead.\n\n"
243
+ " This also means output coordinates are ordered in legacy the 'west, north' axis ordering.\n"
244
+ "\n-->\n"
245
+ )
246
+
235
247
  self.start_collection(sub_collection)
236
248
  if has_multiple_collections:
237
249
  self._write(
@@ -405,7 +417,7 @@ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
405
417
  This uses an internal object type,
406
418
  as :mod:`django.contrib.gis.geos` only provides a 4-tuple envelope.
407
419
  """
408
- return f"""<gml:boundedBy><gml:Envelope srsDimension="2" srsName="{attr_escape(envelope.crs.urn)}">
420
+ return f"""<gml:boundedBy><gml:Envelope srsDimension="2" srsName="{attr_escape(str(envelope.crs))}">
409
421
  <gml:lowerCorner>{envelope.min_x} {envelope.min_y}</gml:lowerCorner>
410
422
  <gml:upperCorner>{envelope.max_x} {envelope.max_y}</gml:upperCorner>
411
423
  </gml:Envelope></gml:boundedBy>\n"""
@@ -419,7 +431,7 @@ class GML32Renderer(CollectionOutputRenderer, XmlOutputRenderer):
419
431
  ) -> str:
420
432
  """Normal case: 'value' is raw geometry data.."""
421
433
  # In case this is a standalone response, this will be the top-level element, hence includes the xmlns.
422
- base_attrs = f' gml:id="{attr_escape(gml_id)}" srsName="{attr_escape(projection.output_crs.urn)}"{extra_xmlns}'
434
+ base_attrs = f' gml:id="{attr_escape(gml_id)}" srsName="{attr_escape(str(projection.output_crs))}"{extra_xmlns}'
423
435
  projection.output_crs.apply_to(value)
424
436
  return self._render_gml_type(value, base_attrs=base_attrs)
425
437
 
@@ -521,11 +533,11 @@ class DBGMLRenderingMixin:
521
533
  id_pos = gml_tag.find("gml:id=")
522
534
  if id_pos == -1:
523
535
  # Inject
524
- return f'{gml_tag} gml:id="{attr_escape(gml_id)}"{extra_xmlns}{value[end_pos:]}'
536
+ gml = f'{gml_tag} gml:id="{attr_escape(gml_id)}"{extra_xmlns}{value[end_pos:]}'
525
537
  else:
526
538
  # Replace
527
539
  end_pos1 = gml_tag.find('"', id_pos + 8)
528
- return (
540
+ gml = (
529
541
  f"{gml_tag[:id_pos]}"
530
542
  f'gml:id="{attr_escape(gml_id)}'
531
543
  f"{value[end_pos1:end_pos]}" # from " right until >
@@ -533,6 +545,23 @@ class DBGMLRenderingMixin:
533
545
  f"{value[end_pos:]}" # from > and beyond
534
546
  )
535
547
 
548
+ return self._fix_db_crs_name(projection, gml)
549
+
550
+ def _fix_db_crs_name(self, projection: FeatureProjection, gml: str) -> str:
551
+ if projection.output_crs.force_xy:
552
+ # When legacy output is used, make sure the srsName matches the input.
553
+ # PostgreSQL will still generate the EPSG:xxxx notation.
554
+ return gml.replace(
555
+ 'srsName="EPSG:', 'srsName="http://www.opengis.net/gml/srs/epsg.xml#', 1
556
+ )
557
+ elif projection.output_crs == CRS84:
558
+ # Fix PostgreSQL not knowing it's CRS84 or WGS84 (both srid 4326)
559
+ return gml.replace(
560
+ 'srsName="urn:ogc:def:crs:EPSG::4326"', 'srsName="urn:ogc:def:crs:OGC::CRS84"', 1
561
+ )
562
+ else:
563
+ return gml
564
+
536
565
 
537
566
  class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
538
567
  """Faster GetFeature renderer that uses the database to render GML 3.2"""
@@ -553,12 +582,14 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
553
582
  # Retrieve geometries as pre-rendered instead.
554
583
  # Only take the geometries of the current level.
555
584
  # The annotations for relations will be handled by prefetches and get_prefetch_queryset()
585
+ use_modern = not projection.output_crs.force_xy
556
586
  return replace_queryset_geometries(
557
587
  queryset,
558
588
  projection.geometry_elements,
559
589
  projection.output_crs,
560
590
  AsGML,
561
- is_latlon=projection.output_crs.is_north_east_order,
591
+ is_latlon=use_modern and projection.output_crs.is_north_east_order,
592
+ long_urn=use_modern,
562
593
  )
563
594
 
564
595
  def get_prefetch_queryset(
@@ -572,12 +603,14 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
572
603
  return None
573
604
 
574
605
  # Find which fields are GML elements
606
+ use_modern = not projection.output_crs.force_xy
575
607
  return replace_queryset_geometries(
576
608
  queryset,
577
609
  feature_relation.geometry_elements,
578
610
  projection.output_crs,
579
611
  AsGML,
580
- is_latlon=projection.output_crs.is_north_east_order,
612
+ is_latlon=use_modern and projection.output_crs.is_north_east_order,
613
+ long_urn=use_modern,
581
614
  )
582
615
 
583
616
  def get_db_envelope_as_gml(self, projection: FeatureProjection, queryset) -> AsGML:
@@ -586,10 +619,12 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
586
619
  This also avoids offloads the geometry union calculation to the DB.
587
620
  """
588
621
  geo_fields_union = self._get_geometries_union(projection, queryset)
622
+ use_modern = not projection.output_crs.force_xy
589
623
  return AsGML(
590
624
  geo_fields_union,
591
625
  envelope=True,
592
- is_latlon=projection.output_crs.is_north_east_order,
626
+ is_latlon=use_modern and projection.output_crs.is_north_east_order,
627
+ long_urn=use_modern,
593
628
  )
594
629
 
595
630
  def _get_geometries_union(self, projection: FeatureProjection, queryset):
@@ -620,6 +655,8 @@ class DBGML32Renderer(DBGMLRenderingMixin, GML32Renderer):
620
655
  gml = instance._as_envelope_gml
621
656
  if gml is None:
622
657
  return
658
+
659
+ gml = self._fix_db_crs_name(projection, gml)
623
660
  else:
624
661
  value = get_db_rendered_geometry(instance, geo_element, AsGML)
625
662
  if value is None:
@@ -750,11 +787,13 @@ class DBGML32ValueRenderer(DBGMLRenderingMixin, GML32ValueRenderer):
750
787
  if element.type.is_geometry:
751
788
  # Add 'gml_member' to point to the pre-rendered GML version.
752
789
  geo_element = cast(GeometryXsdElement, element)
790
+ use_modern = not projection.output_crs.force_xy
753
791
  return queryset.values(
754
792
  "pk",
755
793
  gml_member=AsGML(
756
794
  get_db_geometry_target(geo_element, projection.output_crs),
757
- is_latlon=projection.output_crs.is_north_east_order,
795
+ is_latlon=use_modern and projection.output_crs.is_north_east_order,
796
+ long_urn=use_modern,
758
797
  ),
759
798
  )
760
799
  else:
gisserver/parsers/ast.py CHANGED
@@ -6,7 +6,7 @@ For example, the FES filter syntax can be processed into objects that build an O
6
6
  Python classes can inherit :class:`AstNode` and register themselves as the parser/handler
7
7
  for a given tag. Both normal Python classes and dataclass work,
8
8
  as long as it has an :meth:`AstNode.from_xml` class method.
9
- The custom `from_xml()` method should copy the XML data into local attributes.
9
+ The custom ``from_xml()`` method should copy the XML data into local attributes.
10
10
 
11
11
  Next, when :meth:`TagRegistry.node_from_xml` is called,
12
12
  it will detect which class the XML Element refers to and initialize it using the ``from_xml()`` call.
@@ -58,7 +58,7 @@ class TagNameEnum(Enum):
58
58
  """Cast the element tag name into the enum member.
59
59
 
60
60
  This translates the element name
61
- such as``{http://www.opengis.net/fes/2.0}PropertyIsEqualTo``
61
+ such as ``{http://www.opengis.net/fes/2.0}PropertyIsEqualTo``
62
62
  into a ``PropertyIsEqualTo`` member.
63
63
  """
64
64
  tag_name = element.tag
@@ -90,6 +90,7 @@ class AstNode:
90
90
  an XML tag into a Python (data) class.
91
91
  """
92
92
 
93
+ #: Default namespace of the element and subclasses, if not given by ``@tag_registry.register()``.
93
94
  xml_ns: xmlns | str | None = None
94
95
 
95
96
  _xml_tags = []
@@ -99,6 +100,8 @@ class AstNode:
99
100
  """Tell the default tag by which this class is registered"""
100
101
  return cls._xml_tags[0]
101
102
 
103
+ xml_name.__doc__ = "Tell the default tag by which this class is registered"
104
+
102
105
  def __init_subclass__(cls):
103
106
  # Each class level has a fresh list of supported child tags.
104
107
  cls._xml_tags = []
@@ -124,7 +127,7 @@ class AstNode:
124
127
 
125
128
  @classmethod
126
129
  def get_tag_names(cls) -> list[str]:
127
- """Provide all known XMl tags that this code can parse."""
130
+ """Provide all known XML tags that this code can parse."""
128
131
  try:
129
132
  # Because a cached class property is hard to build
130
133
  return _KNOWN_TAG_NAMES[cls]
@@ -233,14 +236,14 @@ class TagRegistry:
233
236
  def node_from_xml(self, element: NSElement, allowed_types: tuple[type[A]] | None = None) -> A:
234
237
  """Find the ``AstNode`` subclass that corresponds to the given XML element,
235
238
  and initialize it with the element. This is a convenience shortcut.
236
- ``"""
239
+ """
237
240
  node_class = self.resolve_class(element, allowed_types)
238
241
  return node_class.from_xml(element)
239
242
 
240
243
  def resolve_class(
241
244
  self, element: NSElement, allowed_types: tuple[type[A]] | None = None
242
245
  ) -> type[A]:
243
- """Find the ``AstNode`` subclass that corresponds to the given XML element."""
246
+ """Find the :class:`AstNode` subclass that corresponds to the given XML element."""
244
247
  try:
245
248
  node_class = self.parsers[element.tag]
246
249
  except KeyError:
@@ -227,7 +227,7 @@ class Operator(AstNode):
227
227
 
228
228
  @dataclass
229
229
  class IdOperator(Operator):
230
- """List of :class:`~gisserver.parsers.fes20.identifers.ResourceId`` objects.
230
+ """List of :class:`~gisserver.parsers.fes20.identifers.ResourceId` objects.
231
231
 
232
232
  A ``<fes:Filter>`` only has a single predicate.
233
233
  Hence, this operator is used to wrap the ``<fes:ResourceId>`` elements in the syntax::
@@ -89,7 +89,13 @@ class GEOSGMLGeometry(AbstractGeometry):
89
89
  # This avoids having to support the whole GEOS logic.
90
90
  geos_data = GEOSGeometry.from_gml(tostring(element))
91
91
  geos_data.srid = srs.srid
92
- CRS.tag_geometry(geos_data, axis_order=AxisOrder.AUTHORITY)
92
+
93
+ # Using the WFS 2 format (urn:ogc:def:crs:EPSG::4326"), coordinates should be latitude/longitude.
94
+ # However, when providing legacy formats like srsName="EPSG:4326",
95
+ # input is assumed to be in legacy longitude/latitude axis ordering too.
96
+ # This reflects what GeoServer does: https://docs.geoserver.org/main/en/user/services/wfs/axis_order.html
97
+ if not srs.force_xy:
98
+ CRS.tag_geometry(geos_data, axis_order=AxisOrder.AUTHORITY)
93
99
  return cls(srs=srs, geos_data=geos_data)
94
100
 
95
101
  def __repr__(self):
@@ -48,7 +48,9 @@ class AdhocQuery(QueryExpression):
48
48
  <fes:SortBy>...</fes:SortBy>
49
49
  </wfs:Query>
50
50
 
51
- And supports the KVP syntax::
51
+ And supports the KVP syntax:
52
+
53
+ .. code-block:: urlencoded
52
54
 
53
55
  ?SERVICE=WFS&...&TYPENAMES=ns:myType&FILTER=...&SORTBY=...&SRSNAME=...&PROPERTYNAME=...
54
56
  ?SERVICE=WFS&...&TYPENAMES=ns:myType&BBOX=...&SORTBY=...&SRSNAME=...&PROPERTYNAME=...
@@ -1,17 +1,33 @@
1
- /* general CSS that can be disabled by removing the .meta-page in the overwritten templates */
1
+ /* Common page styling */
2
+
3
+ /*
4
+ This general CSS that can be disabled
5
+ by removing the .meta-page in the overwritten templates
6
+ */
2
7
 
3
8
  .meta-page {
4
- font-family: Arial, sans-serif;
5
- line-height: 1.5;
6
- margin: 0 2rem 4rem 2rem;
9
+ margin: 2rem 2rem 4rem 2rem;
7
10
  }
8
11
 
9
12
  .meta-page h2 { margin: 2rem 0 0.5rem 0; }
10
13
  .meta-page h3 { margin: 1rem 0 0.5rem 0; }
11
14
  .meta-page p { margin: 0 0 1rem 0; }
12
15
 
16
+ .meta-page code { color: #000; }
17
+
18
+ .meta-page dl {
19
+ display: grid;
20
+ grid-gap: 2px 16px;
21
+ grid-template-columns: max-content;
22
+ }
23
+ .meta-page dd { grid-column-start: 2; margin: 0; }
24
+
25
+ .meta-page .wfs-feature-type { margin-bottom: 2.5rem; }
26
+ .meta-page .field-name { min-width: 20%; }
27
+ .meta-page .field-type { min-width: 25%; }
28
+
13
29
  /* application-specific */
14
- .connect-url { margin-left: 4rem; }
30
+ p.connect-url { margin-left: 2.5rem; font-weight: bold; }
15
31
 
16
32
  tbody th { text-align: left; padding-right: 2rem; }
17
33
  tbody td { padding-right: 2rem; }
@@ -3,7 +3,10 @@
3
3
  <head>
4
4
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
5
5
  <title>{% block title %}{{ service_description.title }} {{ accept_operations|dictsort:0|join:"/" }}{% endblock %}</title>
6
- {% block link %}<link rel="stylesheet" type="text/css" href="{% static 'gisserver/index.css' %}">{% endblock %}
6
+ {% block link %}
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
8
+ <link rel="stylesheet" type="text/css" href="{% static 'gisserver/index.css' %}">
9
+ {% endblock %}
7
10
  {% block extrahead %}{% endblock %}
8
11
  </head>
9
12
  <body class="{% block body-class %}meta-page{% endblock %}">
@@ -12,7 +12,7 @@
12
12
  {% if wfs_features %}
13
13
  <h2>{% translate "WFS Feature Types" %}</h2>
14
14
  {% for feature_type in wfs_features %}
15
- <article>{% include "gisserver/wfs/feature_type.html" %}</article>
15
+ <article class="wfs-feature-type">{% include "gisserver/wfs/feature_type.html" %}</article>
16
16
  {% endfor %}
17
17
  {% endif %}
18
18
  {% endblock %}
@@ -18,5 +18,5 @@
18
18
  {% if connect_url %}
19
19
  <h2>{% translate "Using This WFS" %}</h2>
20
20
  <p>{% translate "Add the following URL to your GIS application:" %}</p>
21
- <p class="connect-url"><code>{{ connect_url }}</code></p>
21
+ <p class="connect-url"><samp>{{ connect_url }}</samp></p>
22
22
  {% endif %}
@@ -13,9 +13,13 @@
13
13
  <dt>{% translate "Supported CRS" %}:</dt>
14
14
  <dd>
15
15
  {% if GISSERVER_SUPPORTED_CRS_ONLY %}
16
- {% blocktranslate with default_crs=feature_type.crs %}Any CRS value is supported, source data uses {{ default_crs }}.{% endblocktranslate %}
16
+ {% blocktranslate trimmed with default_crs=feature_type.crs supported_crs=feature_type.supported_crs|join:", " %}
17
+ {{ supported_crs }}, and all others. Source data uses {{ default_crs }}.
18
+ {% endblocktranslate %}
17
19
  {% else %}
18
- {{ feature_type.supported_crs|join:", " }}
20
+ {% blocktranslate trimmed with default_crs=feature_type.crs supported_crs=feature_type.supported_crs|join:", " %}
21
+ {{ supported_crs }}. Source data uses {{ default_crs }}.
22
+ {% endblocktranslate %}
19
23
  {% endif %}
20
24
  </dd>
21
25
  {% if wfs_output_formats %}
@@ -35,6 +39,11 @@
35
39
  {% block fields %}
36
40
  <p>{% translate "The following fields are available:" %}</p>
37
41
  <table class="table table-striped">
42
+ <colgroup>
43
+ <col class="field-name" />
44
+ <col class="field-type" />
45
+ <col class="field-description" />
46
+ </colgroup>
38
47
  <thead><tr><th>{% translate "Field Name" %}</th><th>{% translate "Type" %}</th><th>{% translate "Description" %}</th></tr></thead>
39
48
  <tbody>
40
49
  {% for field in feature_type.fields %}
gisserver/types.py CHANGED
@@ -3,7 +3,7 @@
3
3
  These types are the internal schema definition, and the foundation for all output generation.
4
4
 
5
5
  The end-users of this library typically create a WFS feature type definition by using
6
- the :class:~gisserver.features.FeatureType` / :class:`~gisserver.features.FeatureField` classes.
6
+ the :class:`~gisserver.features.FeatureType` / :class:`~gisserver.features.FeatureField` classes.
7
7
 
8
8
  The feature type classes use the model metadata to construct the internal XMLSchema structure.
9
9
  Nearly all WFS requests are handled by walking this structure (like ``DescribeFeatureType``
@@ -57,6 +57,7 @@ __all__ = [
57
57
  "GeometryXsdElement",
58
58
  "GmlIdAttribute",
59
59
  "GmlNameElement",
60
+ "GmlBoundedByElement",
60
61
  "ORMPath",
61
62
  "XPathMatch",
62
63
  "XsdAnyType",