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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +34 -8
  2. django_gisserver-2.1.dist-info/RECORD +68 -0
  3. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/compat.py +23 -0
  6. gisserver/conf.py +7 -0
  7. gisserver/crs.py +401 -0
  8. gisserver/db.py +126 -51
  9. gisserver/exceptions.py +132 -4
  10. gisserver/extensions/__init__.py +4 -0
  11. gisserver/{parsers/fes20 → extensions}/functions.py +131 -31
  12. gisserver/extensions/queries.py +266 -0
  13. gisserver/features.py +253 -181
  14. gisserver/geometries.py +64 -311
  15. gisserver/management/__init__.py +0 -0
  16. gisserver/management/commands/__init__.py +0 -0
  17. gisserver/management/commands/loadgeojson.py +311 -0
  18. gisserver/operations/base.py +130 -312
  19. gisserver/operations/wfs20.py +399 -375
  20. gisserver/output/__init__.py +14 -49
  21. gisserver/output/base.py +198 -144
  22. gisserver/output/csv.py +78 -75
  23. gisserver/output/geojson.py +37 -37
  24. gisserver/output/gml32.py +287 -259
  25. gisserver/output/iters.py +207 -0
  26. gisserver/output/results.py +73 -61
  27. gisserver/output/stored.py +143 -0
  28. gisserver/output/utils.py +81 -169
  29. gisserver/output/xmlschema.py +85 -46
  30. gisserver/parsers/__init__.py +10 -10
  31. gisserver/parsers/ast.py +426 -0
  32. gisserver/parsers/fes20/__init__.py +89 -31
  33. gisserver/parsers/fes20/expressions.py +172 -58
  34. gisserver/parsers/fes20/filters.py +116 -45
  35. gisserver/parsers/fes20/identifiers.py +66 -28
  36. gisserver/parsers/fes20/lookups.py +146 -0
  37. gisserver/parsers/fes20/operators.py +417 -161
  38. gisserver/parsers/fes20/sorting.py +113 -34
  39. gisserver/parsers/gml/__init__.py +17 -25
  40. gisserver/parsers/gml/base.py +36 -15
  41. gisserver/parsers/gml/geometries.py +105 -44
  42. gisserver/parsers/ows/__init__.py +25 -0
  43. gisserver/parsers/ows/kvp.py +198 -0
  44. gisserver/parsers/ows/requests.py +160 -0
  45. gisserver/parsers/query.py +179 -0
  46. gisserver/parsers/values.py +87 -4
  47. gisserver/parsers/wfs20/__init__.py +39 -0
  48. gisserver/parsers/wfs20/adhoc.py +253 -0
  49. gisserver/parsers/wfs20/base.py +148 -0
  50. gisserver/parsers/wfs20/projection.py +103 -0
  51. gisserver/parsers/wfs20/requests.py +483 -0
  52. gisserver/parsers/wfs20/stored.py +193 -0
  53. gisserver/parsers/xml.py +261 -0
  54. gisserver/projection.py +367 -0
  55. gisserver/static/gisserver/index.css +20 -4
  56. gisserver/templates/gisserver/base.html +12 -0
  57. gisserver/templates/gisserver/index.html +9 -15
  58. gisserver/templates/gisserver/service_description.html +12 -6
  59. gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
  60. gisserver/templates/gisserver/wfs/feature_field.html +3 -3
  61. gisserver/templates/gisserver/wfs/feature_type.html +35 -13
  62. gisserver/templatetags/gisserver_tags.py +20 -0
  63. gisserver/types.py +445 -313
  64. gisserver/views.py +227 -62
  65. django_gisserver-1.5.0.dist-info/RECORD +0 -54
  66. gisserver/parsers/base.py +0 -149
  67. gisserver/parsers/fes20/query.py +0 -285
  68. gisserver/parsers/tags.py +0 -102
  69. gisserver/queries/__init__.py +0 -37
  70. gisserver/queries/adhoc.py +0 -185
  71. gisserver/queries/base.py +0 -186
  72. gisserver/queries/projection.py +0 -240
  73. gisserver/queries/stored.py +0 -206
  74. gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
  75. gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
  76. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
  77. {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
gisserver/geometries.py CHANGED
@@ -1,338 +1,86 @@
1
1
  """Helper classes to handle geometry data types.
2
2
 
3
- This includes the CRS parsing, coordinate transforms and bounding box object.
4
3
  The bounding box can be calculated within Python, or read from a database result.
5
4
  """
6
5
 
7
6
  from __future__ import annotations
8
7
 
9
- import re
10
- from dataclasses import dataclass, field
11
- from decimal import Decimal
12
- from functools import lru_cache
8
+ import math
9
+ from dataclasses import dataclass
13
10
 
14
- from django.contrib.gis.gdal import CoordTransform, SpatialReference
15
- from django.contrib.gis.geos import GEOSGeometry, Polygon
11
+ from django.contrib.gis.geos import GEOSGeometry
16
12
 
17
- from gisserver.exceptions import ExternalParsingError, ExternalValueError
13
+ from gisserver.crs import CRS, CRS84, WEB_MERCATOR, WGS84 # noqa: F401 (keep old exports)
18
14
 
19
- CRS_URN_REGEX = re.compile(
20
- r"^urn:(?P<domain>[a-z]+)"
21
- r":def:crs:(?P<authority>[a-z]+)"
22
- r":(?P<version>[0-9]+\.[0-9]+(\.[0-9]+)?)?"
23
- r":(?P<id>[0-9]+|crs84)"
24
- r"$",
25
- re.IGNORECASE,
26
- )
15
+ #: The CRS for the ``<ows:WGS84BoundingBox>`` element:
16
+ WGS84_BOUNDING_BOX_CRS = CRS.from_string("urn:ogc:def:crs:OGC:2:84")
27
17
 
28
18
  __all__ = [
29
- "CRS",
30
- "WGS84",
31
19
  "BoundingBox",
20
+ "WGS84BoundingBox",
32
21
  ]
33
22
 
34
23
 
35
- @lru_cache(maxsize=200)
36
- def _get_spatial_reference(srs_input, srs_type="user", axis_order=None):
37
- """Construct an GDAL object reference"""
38
- # Using lru-cache to avoid repeated GDAL c-object construction
39
- return SpatialReference(srs_input, srs_type=srs_type, axis_order=axis_order)
24
+ @dataclass
25
+ class BoundingBox:
26
+ """A bounding box.
27
+ Due to the overlap between 2 types, this element is used for 2 cases:
40
28
 
29
+ * The ``<ows:WGS84BoundingBox>`` element for ``GetCapabilities``.
30
+ * The ``<gml:Envelope>`` inside an``<gml:boundedBy>`` single feature.
41
31
 
42
- @lru_cache(maxsize=100)
43
- def _get_coord_transform(
44
- source: int | SpatialReference, target: int | SpatialReference
45
- ) -> CoordTransform:
46
- """Get an efficient coordinate transformation object.
32
+ While both classes have no common base class (and exist in different schema's),
33
+ their properties are identical.
47
34
 
48
- The CoordTransform should be used when performing the same
49
- coordinate transformation repeatedly on different geometries.
35
+ The X/Y coordinates can be either latitude or longitude, depending on the CRS.
50
36
 
51
- NOTE that the cache could be busted when CRS objects are
52
- repeatedly created with a custom 'backend' object.
37
+ Note this isn't using the GDAL/OGR "Envelope" object, as that doesn't expose the CRS,
38
+ and requires constant copies to merge geometries.
53
39
  """
54
- if isinstance(source, int):
55
- source = _get_spatial_reference(source, srs_type="epsg")
56
- if isinstance(target, int):
57
- target = _get_spatial_reference(target, srs_type="epsg")
58
-
59
- return CoordTransform(source, target)
60
-
61
-
62
- @dataclass(frozen=True)
63
- class CRS:
64
- """
65
- Represents a CRS (Coordinate Reference System), which preferably follows the URN format
66
- as specified by `the OGC consortium <http://www.opengeospatial.org/ogcUrnPolicy>`_.
67
- """
68
-
69
- # CRS logic, based upon https://github.com/wglas85/django-wfs/blob/master/wfs/helpers.py
70
- # Copyright (c) 2006 Wolfgang Glas - Apache 2.0 licensed
71
- # Ported to Python 3.6 style.
72
-
73
- #: Either "ogc" or "opengis", whereas "ogc" is highly recommended.
74
- domain: str
75
-
76
- #: Either "OGC" or "EPSG".
77
- authority: str
78
-
79
- #: The version of the authorities' SRS registry, which is empty or
80
- #: contains two or three numeric components separated by dots like "6.9" or "6.11.9".
81
- #: For WFS 2.0 this is typically empty.
82
- version: str
83
-
84
- #: A string representation of the coordinate system reference ID.
85
- #: For OGC, only "CRS84" is supported as crsid. For EPSG, this is the formatted CRSID.
86
- crsid: str
87
-
88
- #: The integer representing the numeric spatial reference ID as
89
- #: used by the EPSG and GIS database backends.
90
- srid: int
91
-
92
- #: GDAL SpatialReference with PROJ.4 / WKT content to describe the exact transformation.
93
- backend: SpatialReference | None = None
94
-
95
- #: Original input
96
- origin: str = field(init=False, default=None)
97
-
98
- has_custom_backend: bool = field(init=False)
99
-
100
- def __post_init__(self):
101
- # Using __dict__ because of frozen=True
102
- self.__dict__["has_custom_backend"] = self.backend is not None
103
-
104
- @classmethod
105
- def from_string(cls, uri: str | int, backend: SpatialReference | None = None) -> CRS:
106
- """
107
- Parse an CRS (Coordinate Reference System) URI, which preferably follows the URN format
108
- as specified by `the OGC consortium <http://www.opengeospatial.org/ogcUrnPolicy>`_
109
- and construct a new CRS instance.
110
-
111
- The value can be 3 things:
112
-
113
- * A URI in OGC URN format.
114
- * A legacy CRS URI ("epsg:<SRID>", or "http://www.opengis.net/...").
115
- * A numeric SRID (which calls `from_srid()`)
116
- """
117
- if isinstance(uri, int) or uri.isdigit():
118
- return cls.from_srid(int(uri), backend=backend)
119
- elif uri.startswith("urn:"):
120
- return cls._from_urn(uri, backend=backend)
121
- else:
122
- return cls._from_legacy(uri, backend=backend)
123
-
124
- @classmethod
125
- def from_srid(cls, srid: int, backend=None):
126
- """Instantiate this class using a numeric spatial reference ID
127
-
128
- This is logically identical to calling::
129
-
130
- CRS.from_string("urn:ogc:def:crs:EPSG:6.9:<SRID>")
131
- """
132
- crs = cls(
133
- domain="ogc",
134
- authority="EPSG",
135
- version="",
136
- crsid=str(srid),
137
- srid=int(srid),
138
- backend=backend,
139
- )
140
- crs.__dict__["origin"] = srid
141
- return crs
142
-
143
- @classmethod
144
- def _from_urn(cls, urn, backend=None): # noqa: C901
145
- """Instantiate this class using a URN format."""
146
- urn_match = CRS_URN_REGEX.match(urn)
147
- if not urn_match:
148
- raise ExternalValueError(f"Unknown CRS URN [{urn}] specified: {CRS_URN_REGEX.pattern}")
149
-
150
- domain = urn_match.group("domain")
151
- authority = urn_match.group("authority").upper()
152
-
153
- if domain not in ("ogc", "opengis"):
154
- raise ExternalValueError(f"CRS URI [{urn}] contains unknown domain [{domain}]")
155
-
156
- if authority == "EPSG":
157
- crsid = urn_match.group("id")
158
- try:
159
- srid = int(crsid)
160
- except ValueError:
161
- raise ExternalValueError(
162
- f"CRS URI [{urn}] should contain a numeric SRID value."
163
- ) from None
164
- elif authority == "OGC":
165
- crsid = urn_match.group("id").upper()
166
- if crsid != "CRS84":
167
- raise ExternalValueError(f"OGC CRS URI from [{urn}] contains unknown id [{id}]")
168
- srid = 4326
169
- else:
170
- raise ExternalValueError(f"CRS URI [{urn}] contains unknown authority [{authority}]")
171
-
172
- crs = cls(
173
- domain=domain,
174
- authority=authority,
175
- version=urn_match.group(3),
176
- crsid=crsid,
177
- srid=srid,
178
- backend=backend,
179
- )
180
- crs.__dict__["origin"] = urn
181
- return crs
182
-
183
- @classmethod
184
- def _from_legacy(cls, uri, backend=None):
185
- """Instantiate this class from a legacy URL"""
186
- luri = uri.lower()
187
- for head in (
188
- "epsg:",
189
- "http://www.opengis.net/def/crs/epsg/0/",
190
- "http://www.opengis.net/gml/srs/epsg.xml#",
191
- ):
192
- if luri.startswith(head):
193
- crsid = luri[len(head) :]
194
- try:
195
- srid = int(crsid)
196
- except ValueError:
197
- raise ExternalValueError(
198
- f"CRS URI [{uri}] should contain a numeric SRID value."
199
- ) from None
200
40
 
201
- crs = cls(
202
- domain="ogc",
203
- authority="EPSG",
204
- version="",
205
- crsid=crsid,
206
- srid=srid,
207
- backend=backend,
208
- )
209
- crs.__dict__["origin"] = uri
210
- return crs
211
-
212
- raise ExternalValueError(f"Unknown CRS URI [{uri}] specified")
213
-
214
- @property
215
- def legacy(self):
216
- """Return a legacy string in the format "EPSG:<srid>"""
217
- return f"EPSG:{self.srid:d}"
218
-
219
- @property
220
- def urn(self):
221
- """Return The OGC URN corresponding to this CRS."""
222
- return f"urn:{self.domain}:def:crs:{self.authority}:{self.version or ''}:{self.crsid}"
223
-
224
- def __str__(self):
225
- return self.urn
226
-
227
- def __eq__(self, other):
228
- if isinstance(other, CRS):
229
- # CRS84 is NOT equivalent to EPSG:4326.
230
- # EPSG:4326 specifies coordinates in lat/long order and CRS:84 in long/lat order.
231
- return self.authority == other.authority and self.srid == other.srid
232
- else:
233
- return NotImplemented
234
-
235
- def __hash__(self):
236
- """Used to match objects in a set."""
237
- return hash((self.authority, self.srid))
238
-
239
- def _as_gdal(self) -> SpatialReference:
240
- """Generate the GDAL Spatial Reference object"""
241
- if self.backend is None:
242
- # Avoid repeated construction, reuse the object from cache if possible.
243
- # Note that the original data is used, as it also defines axis orientation.
244
- if self.origin:
245
- self.__dict__["backend"] = _get_spatial_reference(self.origin)
246
- else:
247
- self.__dict__["backend"] = _get_spatial_reference(self.srid, srs_type="epsg")
248
- return self.backend
249
-
250
- def apply_to(self, geometry: GEOSGeometry, clone=False) -> GEOSGeometry | None:
251
- """Transform the geometry using this coordinate reference.
252
-
253
- This method caches the used CoordTransform object
254
-
255
- Every transformation within this package happens through this method,
256
- giving full control over coordinate transformations.
257
- """
258
- if self.srid == geometry.srid:
259
- # Avoid changes if spatial reference system is identical.
260
- if clone:
261
- return geometry.clone()
262
- else:
263
- return None
264
- else:
265
- # Convert using GDAL / proj
266
- transform = _get_coord_transform(geometry.srid, self._as_gdal())
267
- return geometry.transform(transform, clone=clone)
268
-
269
-
270
- WGS84 = CRS.from_srid(4326) # aka EPSG:4326
271
-
272
-
273
- @dataclass
274
- class BoundingBox:
275
- """A bounding box that describes the extent of a map layer"""
276
-
277
- south: Decimal # longitude
278
- west: Decimal # latitude
279
- north: Decimal # longitude
280
- east: Decimal # latitude
41
+ min_x: float
42
+ min_y: float
43
+ max_x: float
44
+ max_y: float
281
45
  crs: CRS | None = None
282
46
 
283
47
  @classmethod
284
- def from_string(cls, bbox):
285
- """Parse the bounding box from an input string.
48
+ def from_geometries(cls, geometries: list[GEOSGeometry], crs: CRS) -> BoundingBox | None:
49
+ """Calculate the extent of a collection of geometries."""
50
+ if not geometries:
51
+ return None
52
+ elif len(geometries) == 1:
53
+ # Common case: feature has a single geometry
54
+ ogr_geometry = geometries[0].ogr
55
+ crs.apply_to(ogr_geometry, clone=False)
56
+ return cls(*ogr_geometry.extent, crs=crs)
57
+ else:
58
+ # Feature has multiple geometries.
59
+ # Start with an obviously invalid bbox,
60
+ # which corrects at the first extend_to_geometry call.
61
+ result = cls(math.inf, math.inf, -math.inf, -math.inf, crs=crs)
286
62
 
287
- It can either be 4 coordinates, or 4 coordinates with a special reference system.
288
- """
289
- bbox = bbox.split(",")
290
- if not (4 <= len(bbox) <= 5):
291
- raise ExternalParsingError(
292
- f"Input does not contain bounding box, expected 4 or 5 values, not {bbox}."
293
- )
294
- return cls(
295
- Decimal(bbox[0]),
296
- Decimal(bbox[1]),
297
- Decimal(bbox[2]),
298
- Decimal(bbox[3]),
299
- CRS.from_string(bbox[4]) if len(bbox) == 5 else None,
300
- )
63
+ for geometry in geometries:
64
+ ogr_geometry = geometry.ogr
65
+ crs.apply_to(ogr_geometry, clone=False)
66
+ result.extend_to(*ogr_geometry.extent)
301
67
 
302
- @classmethod
303
- def from_geometry(cls, geometry: GEOSGeometry, crs: CRS | None = None):
304
- """Construct the bounding box for a geometry"""
305
- if crs is None:
306
- crs = CRS.from_srid(geometry.srid)
307
- elif geometry.srid != crs.srid:
308
- geometry = crs.apply_to(geometry, clone=True)
309
-
310
- return cls(*geometry.extent, crs=crs)
68
+ return result
311
69
 
312
70
  @property
313
71
  def lower_corner(self):
314
- return [self.south, self.west]
72
+ return [self.min_x, self.min_y]
315
73
 
316
74
  @property
317
75
  def upper_corner(self):
318
- return [self.north, self.east]
319
-
320
- def __repr__(self):
321
- return f"BoundingBox({self.south}, {self.west}, {self.north}, {self.east})"
76
+ return [self.max_x, self.max_y]
322
77
 
323
- def extend_to(self, lower_lon: float, lower_lat: float, upper_lon: float, upper_lat: float):
78
+ def extend_to(self, min_x: float, min_y: float, max_x: float, max_y: float):
324
79
  """Expand the bounding box in-place"""
325
- self.south = min(self.south, lower_lon)
326
- self.west = min(self.west, lower_lat)
327
- self.north = max(self.north, upper_lon)
328
- self.east = max(self.east, upper_lat)
329
-
330
- def extend_to_geometry(self, geometry: GEOSGeometry):
331
- """Extend this bounding box with the coordinates of a given geometry."""
332
- if self.crs is not None and geometry.srid != self.crs.srid:
333
- geometry = self.crs.apply_to(geometry, clone=True)
334
-
335
- self.extend_to(*geometry.extent)
80
+ self.min_x = min(self.min_x, min_x)
81
+ self.min_y = min(self.min_y, min_y)
82
+ self.max_x = max(self.max_x, max_x)
83
+ self.max_y = max(self.max_y, max_y)
336
84
 
337
85
  def __add__(self, other):
338
86
  """Combine both extents into a larger box."""
@@ -341,18 +89,23 @@ class BoundingBox:
341
89
  raise ValueError(
342
90
  "Can't combine instances with different spatial reference systems"
343
91
  )
344
- return BoundingBox(
345
- min(self.south, other.south),
346
- min(self.west, other.west),
347
- max(self.north, other.north),
348
- max(self.east, other.east),
92
+ return self.__class__(
93
+ min(self.min_x, other.min_x),
94
+ min(self.min_y, other.min_y),
95
+ max(self.max_x, other.max_x),
96
+ max(self.max_y, other.max_y),
97
+ crs=self.crs,
349
98
  )
350
99
  else:
351
100
  return NotImplemented
352
101
 
353
- def as_polygon(self) -> Polygon:
354
- """Convert the value into a GEOS polygon."""
355
- polygon = Polygon.from_bbox((self.south, self.west, self.north, self.east))
356
- if self.crs is not None:
357
- polygon.srid = self.crs.srid
358
- return polygon
102
+
103
+ class WGS84BoundingBox(BoundingBox):
104
+ """The ``<ows:WGS84BoundingBox>`` element for the ``GetCapabilities`` element.
105
+
106
+ This always has coordinates are always in longitude/latitude axis ordering,
107
+ the CRS is fixed to ``urn:ogc:def:crs:OGC:2:84``.
108
+ """
109
+
110
+ def __init__(self, min_x: float, min_y: float, max_x: float, max_y: float):
111
+ super().__init__(min_x, min_y, max_x, max_y, crs=WGS84_BOUNDING_BOX_CRS)
File without changes
File without changes