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/output/csv.py CHANGED
@@ -9,20 +9,14 @@ from io import StringIO
9
9
  from django.db import models
10
10
 
11
11
  from gisserver import conf
12
- from gisserver.db import (
13
- AsEWKT,
14
- build_db_annotations,
15
- get_db_annotation,
16
- get_db_geometry_selects,
17
- )
18
- from gisserver.geometries import CRS
19
- from gisserver.queries import FeatureProjection
20
- from gisserver.types import XsdElement
12
+ from gisserver.db import AsEWKT, get_db_rendered_geometry, replace_queryset_geometries
13
+ from gisserver.projection import FeatureProjection, FeatureRelation
14
+ from gisserver.types import GeometryXsdElement, XsdElement, XsdTypes
21
15
 
22
- from .base import OutputRenderer
16
+ from .base import CollectionOutputRenderer
23
17
 
24
18
 
25
- class CSVRenderer(OutputRenderer):
19
+ class CSVRenderer(CollectionOutputRenderer):
26
20
  """Fast CSV renderer, using a stream response.
27
21
 
28
22
  The complex encoding bits are handled by the "csv" library.
@@ -37,32 +31,22 @@ class CSVRenderer(OutputRenderer):
37
31
  #: or one of the registered names like: "unix", "excel", "excel-tab"
38
32
  dialect = "unix"
39
33
 
40
- @classmethod
41
34
  def decorate_queryset(
42
- cls,
43
- projection: FeatureProjection,
44
- queryset: models.QuerySet,
45
- output_crs: CRS,
46
- **params,
35
+ self, projection: FeatureProjection, queryset: models.QuerySet
47
36
  ) -> models.QuerySet:
48
37
  """Make sure relations are included with select-related to avoid N-queries.
49
38
  Using prefetch_related() isn't possible with .iterator().
50
39
  """
51
- # Take all relations that are expanded to complex elements,
52
- # and all relations that are fetched for flattened elements.
53
- related = {
54
- xsd_element.orm_path
55
- for xsd_element in projection.complex_elements
56
- if not xsd_element.is_many
57
- } | {
58
- xsd_element.orm_relation[0]
59
- for xsd_element in projection.flattened_elements
60
- if not xsd_element.is_many
61
- }
62
- if related:
63
- queryset = queryset.select_related(*related)
64
-
65
- return queryset
40
+ # First, make sure no array or m2m elements exist,
41
+ # as these are not possible to render in CSV.
42
+ projection.remove_fields(
43
+ lambda e: (e.is_many and not e.is_array)
44
+ or e.type == XsdTypes.gmlCodeType # gml:name
45
+ or e.type == XsdTypes.gmlBoundingShapeType # gml:boundedBy
46
+ )
47
+
48
+ # All database optimizations
49
+ return super().decorate_queryset(projection, queryset)
66
50
 
67
51
  def render_stream(self):
68
52
  self.output = output = StringIO()
@@ -78,18 +62,12 @@ class CSVRenderer(OutputRenderer):
78
62
  output.write("\n\n")
79
63
 
80
64
  # Write the header
81
- fields = [
82
- f
83
- for f in projection.xsd_root_elements
84
- if not f.is_many and f.xml_name not in ("gml:name", "gml:boundedBy")
85
- ]
86
- writer.writerow(self.get_header(projection, fields))
87
-
88
- # By using .iterator(), the results are streamed with as little memory as
89
- # possible. Doing prefetch_related() is not possible now. That could only
90
- # be implemented with cursor pagination for large sets for 1000+ results.
91
- for instance in sub_collection.iterator():
92
- writer.writerow(self.get_row(instance, projection, fields))
65
+ xsd_elements = projection.xsd_root_elements
66
+ writer.writerow(self.get_header(projection, xsd_elements))
67
+
68
+ # Write all rows
69
+ for instance in self.read_features(sub_collection):
70
+ writer.writerow(self.get_row(instance, projection, xsd_elements))
93
71
 
94
72
  # Only perform a 'yield' every once in a while,
95
73
  # as it goes back-and-forth for writing it to the client.
@@ -108,18 +86,22 @@ class CSVRenderer(OutputRenderer):
108
86
  return f"{buffer}\n\n{message}\n"
109
87
 
110
88
  def get_header(
111
- self, projection: FeatureProjection, xsd_elements: list[XsdElement]
89
+ self, projection: FeatureProjection, xsd_elements: list[XsdElement], prefix=""
112
90
  ) -> list[str]:
113
91
  """Return all field names."""
114
92
  names = []
115
93
  append = names.append
116
94
  for xsd_element in xsd_elements:
117
95
  if xsd_element.type.is_complex_type:
118
- # Expand complex types
119
- for sub_field in projection.xsd_child_nodes[xsd_element]:
120
- append(f"{xsd_element.name}.{sub_field.name}")
96
+ names.extend(
97
+ self.get_header(
98
+ projection,
99
+ xsd_elements=projection.xsd_child_nodes[xsd_element],
100
+ prefix=f"{prefix}{xsd_element.name}.",
101
+ )
102
+ )
121
103
  else:
122
- append(xsd_element.name)
104
+ append(f"{prefix}{xsd_element.name}")
123
105
 
124
106
  return names
125
107
 
@@ -130,14 +112,19 @@ class CSVRenderer(OutputRenderer):
130
112
  values = []
131
113
  append = values.append
132
114
  for xsd_element in xsd_elements:
133
- if xsd_element.is_geometry:
134
- append(self.render_geometry(instance, xsd_element))
115
+ if xsd_element.type.is_geometry:
116
+ append(self.render_geometry(projection, instance, xsd_element))
135
117
  continue
136
118
 
137
119
  value = xsd_element.get_value(instance)
138
120
  if xsd_element.type.is_complex_type:
139
- for sub_field in projection.xsd_child_nodes[xsd_element]:
140
- append(sub_field.get_value(value))
121
+ values.extend(
122
+ self.get_row(
123
+ instance=value,
124
+ projection=projection,
125
+ xsd_elements=projection.xsd_child_nodes[xsd_element],
126
+ )
127
+ )
141
128
  elif isinstance(value, list):
142
129
  # Array field
143
130
  append(",".join(map(str, value)))
@@ -147,9 +134,16 @@ class CSVRenderer(OutputRenderer):
147
134
  append(value)
148
135
  return values
149
136
 
150
- def render_geometry(self, instance: models.Model, field: XsdElement):
137
+ def render_geometry(
138
+ self,
139
+ projection: FeatureProjection,
140
+ instance: models.Model,
141
+ geo_element: GeometryXsdElement,
142
+ ) -> str:
151
143
  """Render the contents of a geometry value."""
152
- return field.get_value(instance)
144
+ geometry = geo_element.get_value(instance)
145
+ projection.output_crs.apply_to(geometry)
146
+ return geometry.ewkt
153
147
 
154
148
 
155
149
  class DBCSVRenderer(CSVRenderer):
@@ -157,25 +151,34 @@ class DBCSVRenderer(CSVRenderer):
157
151
  This is about 40% faster than calling the GEOS C-API from python.
158
152
  """
159
153
 
160
- @classmethod
161
154
  def decorate_queryset(
162
- cls,
163
- projection: FeatureProjection,
164
- queryset: models.QuerySet,
165
- output_crs: CRS,
166
- **params,
155
+ self, projection: FeatureProjection, queryset: models.QuerySet
167
156
  ) -> models.QuerySet:
168
- queryset = super().decorate_queryset(projection, queryset, output_crs, **params)
169
-
170
- # Instead of reading the binary geometry data,
171
- # ask the database to generate EWKT data directly.
172
- geo_selects = get_db_geometry_selects(projection.geometry_elements, output_crs)
173
- if geo_selects:
174
- queryset = queryset.defer(*geo_selects.keys()).annotate(
175
- **build_db_annotations(geo_selects, "_as_ewkt_{name}", AsEWKT)
176
- )
177
-
178
- return queryset
179
-
180
- def render_geometry(self, instance: models.Model, field: XsdElement):
181
- return get_db_annotation(instance, field.name, "_as_ewkt_{name}")
157
+ # Instead of reading the binary geometry data, let the database generate EWKT data.
158
+ # As annotations can't be done for select_related() objects, prefetches are used instead.
159
+ queryset = super().decorate_queryset(projection, queryset)
160
+ return replace_queryset_geometries(
161
+ queryset, projection.geometry_elements, projection.output_crs, AsEWKT
162
+ )
163
+
164
+ def get_prefetch_queryset(
165
+ self, projection: FeatureProjection, feature_relation: FeatureRelation
166
+ ) -> models.QuerySet | None:
167
+ """Perform DB annotations for prefetched relations too."""
168
+ queryset = super().get_prefetch_queryset(projection, feature_relation)
169
+ if queryset is None:
170
+ return None
171
+
172
+ # Find which fields are GML elements, annotate these too.
173
+ return replace_queryset_geometries(
174
+ queryset, feature_relation.geometry_elements, projection.output_crs, AsEWKT
175
+ )
176
+
177
+ def render_geometry(
178
+ self,
179
+ projection: FeatureProjection,
180
+ instance: models.Model,
181
+ geo_element: GeometryXsdElement,
182
+ ):
183
+ """Render the geometry using a database-rendered version."""
184
+ return get_db_rendered_geometry(instance, geo_element, AsEWKT)
@@ -6,16 +6,17 @@ from io import BytesIO
6
6
 
7
7
  import orjson
8
8
  from django.contrib.gis.db.models.functions import AsGeoJSON
9
+ from django.contrib.gis.gdal import AxisOrder
9
10
  from django.db import models
10
11
  from django.utils.functional import Promise
11
12
 
12
13
  from gisserver import conf
14
+ from gisserver.crs import CRS84, WGS84
13
15
  from gisserver.db import get_db_geometry_target
14
- from gisserver.geometries import CRS
15
- from gisserver.queries import FeatureProjection
16
+ from gisserver.projection import FeatureProjection
16
17
  from gisserver.types import XsdElement
17
18
 
18
- from .base import OutputRenderer
19
+ from .base import CollectionOutputRenderer
19
20
 
20
21
 
21
22
  def _json_default(obj):
@@ -25,13 +26,13 @@ def _json_default(obj):
25
26
  raise TypeError(f"Unable to serialize {obj.__class__.__name__} to JSON")
26
27
 
27
28
 
28
- class GeoJsonRenderer(OutputRenderer):
29
+ class GeoJsonRenderer(CollectionOutputRenderer):
29
30
  """Fast GeoJSON renderer, using a stream response.
30
31
 
31
32
  The complex encoding bits are handled by the C-library "orjson"
32
33
  and the geojson property of GEOSGeometry.
33
34
 
34
- NOTE: While Django has a GeoJSON serializer
35
+ While Django has a GeoJSON serializer
35
36
  (see https://docs.djangoproject.com/en/3.0/ref/contrib/gis/serializers/),
36
37
  it does not offer streaming response handling.
37
38
  """
@@ -41,29 +42,26 @@ class GeoJsonRenderer(OutputRenderer):
41
42
  max_page_size = conf.GISSERVER_GEOJSON_MAX_PAGE_SIZE
42
43
  chunk_size = 40_000
43
44
 
44
- @classmethod
45
45
  def decorate_queryset(
46
- cls,
46
+ self,
47
47
  projection: FeatureProjection,
48
48
  queryset: models.QuerySet,
49
- output_crs: CRS,
50
- **params,
51
49
  ):
52
50
  """Redefine which fields to query, always include geometry, but remove all others"""
53
- main_geo_element = projection.feature_type.main_geometry_element
54
- projection.add_field(main_geo_element) # make sure geometry is always queried
55
- queryset = super().decorate_queryset(projection, queryset, output_crs, **params)
51
+ # make sure output CRS matches the coordinate ordering that GEOSGeometry.json returns
52
+ if projection.output_crs == WGS84:
53
+ projection.output_crs = CRS84
56
54
 
55
+ # Make sure geometry is always queried.
57
56
  # Other geometries can be excluded as these are not rendered by 'properties'
58
- other_geometries = [
59
- gml_element.orm_path
60
- for gml_element in projection.geometry_elements
61
- if gml_element is not main_geo_element
62
- ]
63
- if other_geometries:
64
- queryset = queryset.defer(*other_geometries)
57
+ main_geo_element = projection.feature_type.main_geometry_element
58
+ projection.add_field(main_geo_element)
59
+ projection.remove_fields(
60
+ lambda element: element.type.is_geometry and element is not main_geo_element
61
+ )
65
62
 
66
- return queryset
63
+ # Apply the normal optimizations with this altered projection
64
+ return super().decorate_queryset(projection, queryset)
67
65
 
68
66
  def render_stream(self):
69
67
  self.output = output = BytesIO()
@@ -87,7 +85,7 @@ class GeoJsonRenderer(OutputRenderer):
87
85
  output.write(b",\n")
88
86
 
89
87
  is_first = True
90
- for instance in sub_collection:
88
+ for instance in self.read_features(sub_collection):
91
89
  if is_first:
92
90
  is_first = False
93
91
  else:
@@ -117,7 +115,7 @@ class GeoJsonRenderer(OutputRenderer):
117
115
  def render_exception(self, exception: Exception):
118
116
  """Render the exception in a format that fits with the output."""
119
117
  message = super().render_exception(exception)
120
- buffer = self.output.getvalue()
118
+ buffer = self.output.getvalue().decode()
121
119
  return f"{buffer}/* {message} */\n"
122
120
 
123
121
  def render_feature(self, projection: FeatureProjection, instance: models.Model) -> bytes:
@@ -137,7 +135,7 @@ class GeoJsonRenderer(OutputRenderer):
137
135
  return b' {"type":"Feature","id":%b,%b"geometry":%b,"properties":%b}' % (
138
136
  orjson.dumps(f"{feature_type.name}.{instance.pk}"),
139
137
  json_geometry_name,
140
- self.render_geometry(feature_type, instance),
138
+ self.render_geometry(projection, instance),
141
139
  orjson.dumps(properties, default=_json_default),
142
140
  )
143
141
 
@@ -150,15 +148,17 @@ class GeoJsonRenderer(OutputRenderer):
150
148
  else:
151
149
  return value
152
150
 
153
- def render_geometry(self, feature_type, instance: models.Model) -> bytes:
151
+ def render_geometry(self, projection: FeatureProjection, instance: models.Model) -> bytes:
154
152
  """Generate the proper GeoJSON notation for a geometry.
155
153
  This calls the GDAL C-API rendering found in 'GEOSGeometry.json'
156
154
  """
157
- geometry = getattr(instance, feature_type.geometry_field.name)
155
+ geometry = projection.get_main_geometry_value(instance)
158
156
  if geometry is None:
159
157
  return b"null"
160
158
 
161
- self.output_crs.apply_to(geometry)
159
+ # The GeoJSON spec requires coordinates to be x,y (longitude,latitude),
160
+ # so web-based clients don't have to ship projection tables.
161
+ projection.output_crs.apply_to(geometry, axis_order=AxisOrder.TRADITIONAL)
162
162
  return geometry.json.encode()
163
163
 
164
164
  def get_header(self) -> dict:
@@ -167,11 +167,12 @@ class GeoJsonRenderer(OutputRenderer):
167
167
  The format is based on the WFS 3.0 DRAFT. The count fields are moved
168
168
  to the footer allowing them to be calculated without performing queries.
169
169
  """
170
+ output_crs = self.collection.results[0].projection.output_crs
170
171
  return {
171
172
  "type": "FeatureCollection",
172
173
  "timeStamp": self._format_geojson_value(self.collection.timestamp),
173
174
  # "numberReturned": is written at the end for better query performance.
174
- "crs": {"type": "name", "properties": {"name": str(self.output_crs)}},
175
+ "crs": {"type": "name", "properties": {"name": str(output_crs)}},
175
176
  }
176
177
 
177
178
  def get_footer(self) -> dict:
@@ -227,7 +228,7 @@ class GeoJsonRenderer(OutputRenderer):
227
228
  """
228
229
  props = {}
229
230
  for xsd_element in xsd_elements:
230
- if not xsd_element.is_geometry:
231
+ if not xsd_element.type.is_geometry:
231
232
  value = xsd_element.get_value(instance)
232
233
  if xsd_element.type.is_complex_type:
233
234
  # Nested object data
@@ -258,30 +259,29 @@ class DBGeoJsonRenderer(GeoJsonRenderer):
258
259
  This is even more efficient than calling the C-API for each feature.
259
260
  """
260
261
 
261
- @classmethod
262
- def decorate_queryset(self, projection: FeatureProjection, queryset, output_crs, **params):
262
+ def decorate_queryset(self, projection: FeatureProjection, queryset):
263
263
  """Update the queryset to let the database render the GML output.
264
264
  This is far more efficient than GeoDjango's logic, which performs a
265
265
  C-API call for every single coordinate of a geometry.
266
266
  """
267
- queryset = super().decorate_queryset(projection, queryset, output_crs, **params)
267
+ queryset = super().decorate_queryset(projection, queryset)
268
268
  # If desired, the entire FeatureCollection could be rendered
269
269
  # in PostgreSQL as well: https://postgis.net/docs/ST_AsGeoJSON.html
270
- main_geometry_field = projection.feature_type.main_geometry_element
271
- if main_geometry_field is not None:
272
- queryset = queryset.defer(main_geometry_field.orm_path).annotate(
270
+ main_geo_element = projection.feature_type.main_geometry_element
271
+ if main_geo_element is not None:
272
+ queryset = queryset.defer(main_geo_element.orm_path).annotate(
273
273
  _as_db_geojson=AsGeoJSON(
274
- get_db_geometry_target(main_geometry_field, output_crs),
274
+ get_db_geometry_target(main_geo_element, projection.output_crs),
275
275
  precision=conf.GISSERVER_DB_PRECISION,
276
276
  )
277
277
  )
278
278
 
279
279
  return queryset
280
280
 
281
- def render_geometry(self, feature_type, instance: models.Model) -> bytes:
281
+ def render_geometry(self, projection: FeatureProjection, instance: models.Model) -> bytes:
282
282
  """Generate the proper GeoJSON notation for a geometry"""
283
283
  # Database server rendering
284
- if not feature_type.geometry_fields:
284
+ if projection.main_geometry_element is None:
285
285
  return b"null"
286
286
 
287
287
  geojson = instance._as_db_geojson