django-gisserver 1.4.0__py3-none-any.whl → 1.5.0__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 (38) hide show
  1. {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/METADATA +15 -13
  2. django_gisserver-1.5.0.dist-info/RECORD +54 -0
  3. {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/WHEEL +1 -1
  4. gisserver/__init__.py +1 -1
  5. gisserver/db.py +14 -20
  6. gisserver/exceptions.py +23 -9
  7. gisserver/features.py +64 -100
  8. gisserver/geometries.py +2 -2
  9. gisserver/operations/base.py +31 -21
  10. gisserver/operations/wfs20.py +44 -38
  11. gisserver/output/__init__.py +2 -1
  12. gisserver/output/base.py +43 -27
  13. gisserver/output/csv.py +38 -33
  14. gisserver/output/geojson.py +43 -51
  15. gisserver/output/gml32.py +88 -67
  16. gisserver/output/results.py +23 -8
  17. gisserver/output/utils.py +18 -2
  18. gisserver/output/xmlschema.py +1 -1
  19. gisserver/parsers/base.py +2 -2
  20. gisserver/parsers/fes20/__init__.py +18 -0
  21. gisserver/parsers/fes20/expressions.py +7 -12
  22. gisserver/parsers/fes20/functions.py +1 -1
  23. gisserver/parsers/fes20/operators.py +7 -3
  24. gisserver/parsers/fes20/query.py +11 -1
  25. gisserver/parsers/fes20/sorting.py +3 -1
  26. gisserver/parsers/gml/base.py +1 -1
  27. gisserver/queries/__init__.py +3 -0
  28. gisserver/queries/adhoc.py +16 -12
  29. gisserver/queries/base.py +76 -36
  30. gisserver/queries/projection.py +240 -0
  31. gisserver/queries/stored.py +7 -6
  32. gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +2 -2
  33. gisserver/types.py +78 -24
  34. gisserver/views.py +9 -20
  35. django_gisserver-1.4.0.dist-info/RECORD +0 -54
  36. gisserver/output/gml32_lxml.py +0 -612
  37. {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/LICENSE +0 -0
  38. {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/top_level.txt +0 -0
gisserver/output/csv.py CHANGED
@@ -6,7 +6,6 @@ import csv
6
6
  from datetime import datetime, timezone
7
7
  from io import StringIO
8
8
 
9
- from django.conf import settings
10
9
  from django.db import models
11
10
 
12
11
  from gisserver import conf
@@ -16,9 +15,9 @@ from gisserver.db import (
16
15
  get_db_annotation,
17
16
  get_db_geometry_selects,
18
17
  )
19
- from gisserver.features import FeatureType
20
18
  from gisserver.geometries import CRS
21
- from gisserver.types import XsdComplexType, XsdElement
19
+ from gisserver.queries import FeatureProjection
20
+ from gisserver.types import XsdElement
22
21
 
23
22
  from .base import OutputRenderer
24
23
 
@@ -41,7 +40,7 @@ class CSVRenderer(OutputRenderer):
41
40
  @classmethod
42
41
  def decorate_queryset(
43
42
  cls,
44
- feature_type: FeatureType,
43
+ projection: FeatureProjection,
45
44
  queryset: models.QuerySet,
46
45
  output_crs: CRS,
47
46
  **params,
@@ -49,17 +48,15 @@ class CSVRenderer(OutputRenderer):
49
48
  """Make sure relations are included with select-related to avoid N-queries.
50
49
  Using prefetch_related() isn't possible with .iterator().
51
50
  """
52
- xsd_type: XsdComplexType = feature_type.xsd_type
53
-
54
51
  # Take all relations that are expanded to complex elements,
55
52
  # and all relations that are fetched for flattened elements.
56
53
  related = {
57
54
  xsd_element.orm_path
58
- for xsd_element in xsd_type.complex_elements
55
+ for xsd_element in projection.complex_elements
59
56
  if not xsd_element.is_many
60
57
  } | {
61
58
  xsd_element.orm_relation[0]
62
- for xsd_element in xsd_type.flattened_elements
59
+ for xsd_element in projection.flattened_elements
63
60
  if not xsd_element.is_many
64
61
  }
65
62
  if related:
@@ -68,11 +65,12 @@ class CSVRenderer(OutputRenderer):
68
65
  return queryset
69
66
 
70
67
  def render_stream(self):
71
- output = StringIO()
68
+ self.output = output = StringIO()
72
69
  writer = csv.writer(output, dialect=self.dialect)
73
70
 
74
71
  is_first_collection = True
75
72
  for sub_collection in self.collection.results:
73
+ projection = sub_collection.projection
76
74
  if is_first_collection:
77
75
  is_first_collection = False
78
76
  else:
@@ -80,14 +78,18 @@ class CSVRenderer(OutputRenderer):
80
78
  output.write("\n\n")
81
79
 
82
80
  # Write the header
83
- fields = [f for f in sub_collection.feature_type.xsd_type.elements if not f.is_many]
84
- writer.writerow(self.get_header(fields))
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))
85
87
 
86
88
  # By using .iterator(), the results are streamed with as little memory as
87
89
  # possible. Doing prefetch_related() is not possible now. That could only
88
90
  # be implemented with cursor pagination for large sets for 1000+ results.
89
91
  for instance in sub_collection.iterator():
90
- writer.writerow(self.get_row(instance, fields))
92
+ writer.writerow(self.get_row(instance, projection, fields))
91
93
 
92
94
  # Only perform a 'yield' every once in a while,
93
95
  # as it goes back-and-forth for writing it to the client.
@@ -101,37 +103,40 @@ class CSVRenderer(OutputRenderer):
101
103
 
102
104
  def render_exception(self, exception: Exception):
103
105
  """Render the exception in a format that fits with the output."""
104
- if settings.DEBUG:
105
- return f"\n\n{exception.__class__.__name__}: {exception}\n"
106
- else:
107
- return f"\n\n{exception.__class__.__name__} during rendering!\n"
106
+ message = super().render_exception(exception)
107
+ buffer = self.output.getvalue()
108
+ return f"{buffer}\n\n{message}\n"
108
109
 
109
- def get_header(self, fields: list[XsdElement]) -> list[str]:
110
+ def get_header(
111
+ self, projection: FeatureProjection, xsd_elements: list[XsdElement]
112
+ ) -> list[str]:
110
113
  """Return all field names."""
111
114
  names = []
112
115
  append = names.append
113
- for field in fields:
114
- if field.type.is_complex_type:
116
+ for xsd_element in xsd_elements:
117
+ if xsd_element.type.is_complex_type:
115
118
  # Expand complex types
116
- for sub_field in field.type.elements:
117
- append(f"{field.name}.{sub_field.name}")
119
+ for sub_field in projection.xsd_child_nodes[xsd_element]:
120
+ append(f"{xsd_element.name}.{sub_field.name}")
118
121
  else:
119
- append(field.name)
122
+ append(xsd_element.name)
120
123
 
121
124
  return names
122
125
 
123
- def get_row(self, instance: models.Model, fields: list[XsdElement]):
126
+ def get_row(
127
+ self, instance: models.Model, projection: FeatureProjection, xsd_elements: list[XsdElement]
128
+ ):
124
129
  """Return all field values for a single row."""
125
130
  values = []
126
131
  append = values.append
127
- for field in fields:
128
- if field.is_geometry:
129
- append(self.render_geometry(instance, field))
132
+ for xsd_element in xsd_elements:
133
+ if xsd_element.is_geometry:
134
+ append(self.render_geometry(instance, xsd_element))
130
135
  continue
131
136
 
132
- value = field.get_value(instance)
133
- if field.type.is_complex_type:
134
- for sub_field in field.type.elements:
137
+ value = xsd_element.get_value(instance)
138
+ if xsd_element.type.is_complex_type:
139
+ for sub_field in projection.xsd_child_nodes[xsd_element]:
135
140
  append(sub_field.get_value(value))
136
141
  elif isinstance(value, list):
137
142
  # Array field
@@ -149,22 +154,22 @@ class CSVRenderer(OutputRenderer):
149
154
 
150
155
  class DBCSVRenderer(CSVRenderer):
151
156
  """Further optimized CSV renderer that uses the database to render EWKT.
152
- This is about 40% faster then calling the GEOS C-API from python.
157
+ This is about 40% faster than calling the GEOS C-API from python.
153
158
  """
154
159
 
155
160
  @classmethod
156
161
  def decorate_queryset(
157
162
  cls,
158
- feature_type: FeatureType,
163
+ projection: FeatureProjection,
159
164
  queryset: models.QuerySet,
160
165
  output_crs: CRS,
161
166
  **params,
162
167
  ) -> models.QuerySet:
163
- queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
168
+ queryset = super().decorate_queryset(projection, queryset, output_crs, **params)
164
169
 
165
170
  # Instead of reading the binary geometry data,
166
171
  # ask the database to generate EWKT data directly.
167
- geo_selects = get_db_geometry_selects(feature_type.xsd_type.geometry_elements, output_crs)
172
+ geo_selects = get_db_geometry_selects(projection.geometry_elements, output_crs)
168
173
  if geo_selects:
169
174
  queryset = queryset.defer(*geo_selects.keys()).annotate(
170
175
  **build_db_annotations(geo_selects, "_as_ewkt_{name}", AsEWKT)
@@ -3,19 +3,17 @@
3
3
  from datetime import datetime, timezone
4
4
  from decimal import Decimal
5
5
  from io import BytesIO
6
- from typing import cast
7
6
 
8
7
  import orjson
9
- from django.conf import settings
10
8
  from django.contrib.gis.db.models.functions import AsGeoJSON
11
9
  from django.db import models
12
10
  from django.utils.functional import Promise
13
11
 
14
12
  from gisserver import conf
15
13
  from gisserver.db import get_db_geometry_target
16
- from gisserver.features import FeatureType
17
14
  from gisserver.geometries import CRS
18
- from gisserver.types import XsdComplexType
15
+ from gisserver.queries import FeatureProjection
16
+ from gisserver.types import XsdElement
19
17
 
20
18
  from .base import OutputRenderer
21
19
 
@@ -46,18 +44,21 @@ class GeoJsonRenderer(OutputRenderer):
46
44
  @classmethod
47
45
  def decorate_queryset(
48
46
  cls,
49
- feature_type: FeatureType,
47
+ projection: FeatureProjection,
50
48
  queryset: models.QuerySet,
51
49
  output_crs: CRS,
52
50
  **params,
53
51
  ):
54
- queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
52
+ """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)
55
56
 
56
57
  # Other geometries can be excluded as these are not rendered by 'properties'
57
58
  other_geometries = [
58
- model_field.name
59
- for model_field in feature_type.geometry_fields
60
- if model_field is not feature_type.geometry_field
59
+ gml_element.orm_path
60
+ for gml_element in projection.geometry_elements
61
+ if gml_element is not main_geo_element
61
62
  ]
62
63
  if other_geometries:
63
64
  queryset = queryset.defer(*other_geometries)
@@ -65,7 +66,7 @@ class GeoJsonRenderer(OutputRenderer):
65
66
  return queryset
66
67
 
67
68
  def render_stream(self):
68
- output = BytesIO()
69
+ self.output = output = BytesIO()
69
70
 
70
71
  # Generate the header from a Python dict,
71
72
  # but replace the last "}" into a comma, to allow writing more
@@ -78,6 +79,8 @@ class GeoJsonRenderer(OutputRenderer):
78
79
  # Flatten the results, they are not grouped in a second FeatureCollection
79
80
  is_first_collection = True
80
81
  for sub_collection in self.collection.results:
82
+ projection = sub_collection.projection
83
+
81
84
  if is_first_collection:
82
85
  is_first_collection = False
83
86
  else:
@@ -92,7 +95,7 @@ class GeoJsonRenderer(OutputRenderer):
92
95
 
93
96
  # The "properties" object is generated by orjson.dumps(),
94
97
  # while the "geometry" object uses the built-in 'GEOSGeometry.json' result.
95
- output.write(self.render_feature(sub_collection.feature_type, instance))
98
+ output.write(self.render_feature(projection, instance))
96
99
 
97
100
  # Only perform a 'yield' every once in a while,
98
101
  # as it goes back-and-forth for writing it to the client.
@@ -113,34 +116,27 @@ class GeoJsonRenderer(OutputRenderer):
113
116
 
114
117
  def render_exception(self, exception: Exception):
115
118
  """Render the exception in a format that fits with the output."""
116
- if settings.DEBUG:
117
- return f"/* {exception.__class__.__name__}: {exception} */\n"
118
- else:
119
- return f"/* {exception.__class__.__name__} during rendering! */\n"
119
+ message = super().render_exception(exception)
120
+ buffer = self.output.getvalue()
121
+ return f"{buffer}/* {message} */\n"
120
122
 
121
- def render_feature(self, feature_type: FeatureType, instance: models.Model) -> bytes:
123
+ def render_feature(self, projection: FeatureProjection, instance: models.Model) -> bytes:
122
124
  """Render the output of a single feature"""
123
125
 
124
126
  # Get all instance attributes:
125
- properties = self.get_properties(feature_type, feature_type.xsd_type, instance)
127
+ properties = self.get_properties(projection, projection.xsd_root_elements, instance)
128
+ feature_type = projection.feature_type
126
129
 
130
+ # Add the name field
127
131
  if feature_type.show_name_field:
128
132
  name = feature_type.get_display_value(instance)
129
- geometry_name = b'"geometry_name":%b,' % orjson.dumps(name)
133
+ json_geometry_name = b'"geometry_name":%b,' % orjson.dumps(name)
130
134
  else:
131
- geometry_name = b""
132
-
133
- return (
134
- b" {"
135
- b'"type":"Feature",'
136
- b'"id":%b,'
137
- b"%b"
138
- b'"geometry":%b,'
139
- b'"properties":%b'
140
- b"}"
141
- ) % (
135
+ json_geometry_name = b""
136
+
137
+ return b' {"type":"Feature","id":%b,%b"geometry":%b,"properties":%b}' % (
142
138
  orjson.dumps(f"{feature_type.name}.{instance.pk}"),
143
- geometry_name,
139
+ json_geometry_name,
144
140
  self.render_geometry(feature_type, instance),
145
141
  orjson.dumps(properties, default=_json_default),
146
142
  )
@@ -218,18 +214,19 @@ class GeoJsonRenderer(OutputRenderer):
218
214
  return links
219
215
 
220
216
  def get_properties(
221
- self,
222
- feature_type: FeatureType,
223
- xsd_type: XsdComplexType,
224
- instance: models.Model,
217
+ self, projection: FeatureProjection, xsd_elements: list[XsdElement], instance: models.Model
225
218
  ) -> dict:
226
219
  """Collect the data for the 'properties' field.
227
220
 
228
221
  This is based on the original XSD definition,
229
222
  so the rendering is consistent with other output formats.
223
+
224
+ :param projection: Overview of all fields to render.
225
+ :param xsd_elements: The XSD elements to render.
226
+ :param instance: The object instance.
230
227
  """
231
228
  props = {}
232
- for xsd_element in xsd_type.elements:
229
+ for xsd_element in xsd_elements:
233
230
  if not xsd_element.is_geometry:
234
231
  value = xsd_element.get_value(instance)
235
232
  if xsd_element.type.is_complex_type:
@@ -237,21 +234,16 @@ class GeoJsonRenderer(OutputRenderer):
237
234
  if value is None:
238
235
  props[xsd_element.name] = None
239
236
  else:
240
- value_xsd_type = cast(XsdComplexType, xsd_element.type)
237
+ sub_elements = projection.xsd_child_nodes[xsd_element]
241
238
  if xsd_element.is_many:
242
- # If the retrieved QuerySet was not filtered yet, do so now.
243
- # This can't be done in get_value() because the FeatureType
244
- # is not known there.
245
- value = feature_type.filter_related_queryset(value)
246
-
247
239
  # "..._to_many relation; reverse FK, M2M or array field.
248
240
  props[xsd_element.name] = [
249
- self.get_properties(feature_type, value_xsd_type, item)
241
+ self.get_properties(projection, sub_elements, item)
250
242
  for item in value
251
243
  ]
252
244
  else:
253
245
  props[xsd_element.name] = self.get_properties(
254
- feature_type, value_xsd_type, value
246
+ projection, sub_elements, value
255
247
  )
256
248
  else:
257
249
  # Scalar value, or list (for ArrayField).
@@ -263,23 +255,23 @@ class GeoJsonRenderer(OutputRenderer):
263
255
  class DBGeoJsonRenderer(GeoJsonRenderer):
264
256
  """GeoJSON renderer that relays the geometry rendering to the database.
265
257
 
266
- This is even more efficient then calling the C-API for each feature.
258
+ This is even more efficient than calling the C-API for each feature.
267
259
  """
268
260
 
269
261
  @classmethod
270
- def decorate_queryset(self, feature_type: FeatureType, queryset, output_crs, **params):
262
+ def decorate_queryset(self, projection: FeatureProjection, queryset, output_crs, **params):
271
263
  """Update the queryset to let the database render the GML output.
272
- This is far more efficient then GeoDjango's logic, which performs a
264
+ This is far more efficient than GeoDjango's logic, which performs a
273
265
  C-API call for every single coordinate of a geometry.
274
266
  """
275
- queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
267
+ queryset = super().decorate_queryset(projection, queryset, output_crs, **params)
276
268
  # If desired, the entire FeatureCollection could be rendered
277
269
  # in PostgreSQL as well: https://postgis.net/docs/ST_AsGeoJSON.html
278
- if feature_type.geometry_fields:
279
- match = feature_type.resolve_element(feature_type.geometry_field.name)
280
- queryset = queryset.defer(feature_type.geometry_field.name).annotate(
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(
281
273
  _as_db_geojson=AsGeoJSON(
282
- get_db_geometry_target(match, output_crs),
274
+ get_db_geometry_target(main_geometry_field, output_crs),
283
275
  precision=conf.GISSERVER_DB_PRECISION,
284
276
  )
285
277
  )