django-gisserver 1.4.1__py3-none-any.whl → 2.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 (73) hide show
  1. {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/METADATA +23 -13
  2. django_gisserver-2.0.dist-info/RECORD +66 -0
  3. {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.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/db.py +63 -60
  8. gisserver/exceptions.py +47 -9
  9. gisserver/extensions/__init__.py +4 -0
  10. gisserver/{parsers/fes20 → extensions}/functions.py +11 -5
  11. gisserver/extensions/queries.py +261 -0
  12. gisserver/features.py +267 -240
  13. gisserver/geometries.py +34 -39
  14. gisserver/management/__init__.py +0 -0
  15. gisserver/management/commands/__init__.py +0 -0
  16. gisserver/management/commands/loadgeojson.py +291 -0
  17. gisserver/operations/base.py +129 -305
  18. gisserver/operations/wfs20.py +428 -336
  19. gisserver/output/__init__.py +10 -48
  20. gisserver/output/base.py +198 -143
  21. gisserver/output/csv.py +81 -85
  22. gisserver/output/geojson.py +63 -72
  23. gisserver/output/gml32.py +310 -281
  24. gisserver/output/iters.py +207 -0
  25. gisserver/output/results.py +71 -30
  26. gisserver/output/stored.py +143 -0
  27. gisserver/output/utils.py +75 -154
  28. gisserver/output/xmlschema.py +86 -47
  29. gisserver/parsers/__init__.py +10 -10
  30. gisserver/parsers/ast.py +320 -0
  31. gisserver/parsers/fes20/__init__.py +15 -11
  32. gisserver/parsers/fes20/expressions.py +89 -50
  33. gisserver/parsers/fes20/filters.py +111 -43
  34. gisserver/parsers/fes20/identifiers.py +44 -26
  35. gisserver/parsers/fes20/lookups.py +144 -0
  36. gisserver/parsers/fes20/operators.py +336 -128
  37. gisserver/parsers/fes20/sorting.py +107 -34
  38. gisserver/parsers/gml/__init__.py +12 -11
  39. gisserver/parsers/gml/base.py +6 -3
  40. gisserver/parsers/gml/geometries.py +69 -35
  41. gisserver/parsers/ows/__init__.py +25 -0
  42. gisserver/parsers/ows/kvp.py +190 -0
  43. gisserver/parsers/ows/requests.py +158 -0
  44. gisserver/parsers/query.py +175 -0
  45. gisserver/parsers/values.py +26 -0
  46. gisserver/parsers/wfs20/__init__.py +37 -0
  47. gisserver/parsers/wfs20/adhoc.py +245 -0
  48. gisserver/parsers/wfs20/base.py +143 -0
  49. gisserver/parsers/wfs20/projection.py +103 -0
  50. gisserver/parsers/wfs20/requests.py +482 -0
  51. gisserver/parsers/wfs20/stored.py +192 -0
  52. gisserver/parsers/xml.py +249 -0
  53. gisserver/projection.py +357 -0
  54. gisserver/static/gisserver/index.css +12 -1
  55. gisserver/templates/gisserver/index.html +1 -1
  56. gisserver/templates/gisserver/service_description.html +2 -2
  57. gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +11 -11
  58. gisserver/templates/gisserver/wfs/feature_field.html +2 -2
  59. gisserver/templatetags/gisserver_tags.py +20 -0
  60. gisserver/types.py +375 -258
  61. gisserver/views.py +206 -75
  62. django_gisserver-1.4.1.dist-info/RECORD +0 -53
  63. gisserver/parsers/base.py +0 -149
  64. gisserver/parsers/fes20/query.py +0 -275
  65. gisserver/parsers/tags.py +0 -102
  66. gisserver/queries/__init__.py +0 -34
  67. gisserver/queries/adhoc.py +0 -181
  68. gisserver/queries/base.py +0 -146
  69. gisserver/queries/stored.py +0 -205
  70. gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
  71. gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
  72. {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
  73. {django_gisserver-1.4.1.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
gisserver/output/csv.py CHANGED
@@ -6,24 +6,17 @@ 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
13
- from gisserver.db import (
14
- AsEWKT,
15
- build_db_annotations,
16
- get_db_annotation,
17
- get_db_geometry_selects,
18
- )
19
- from gisserver.features import FeatureType
20
- from gisserver.geometries import CRS
21
- from gisserver.types import XsdComplexType, 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
22
15
 
23
- from .base import OutputRenderer
16
+ from .base import CollectionOutputRenderer
24
17
 
25
18
 
26
- class CSVRenderer(OutputRenderer):
19
+ class CSVRenderer(CollectionOutputRenderer):
27
20
  """Fast CSV renderer, using a stream response.
28
21
 
29
22
  The complex encoding bits are handled by the "csv" library.
@@ -38,41 +31,30 @@ class CSVRenderer(OutputRenderer):
38
31
  #: or one of the registered names like: "unix", "excel", "excel-tab"
39
32
  dialect = "unix"
40
33
 
41
- @classmethod
42
34
  def decorate_queryset(
43
- cls,
44
- feature_type: FeatureType,
45
- queryset: models.QuerySet,
46
- output_crs: CRS,
47
- **params,
35
+ self, projection: FeatureProjection, queryset: models.QuerySet
48
36
  ) -> models.QuerySet:
49
37
  """Make sure relations are included with select-related to avoid N-queries.
50
38
  Using prefetch_related() isn't possible with .iterator().
51
39
  """
52
- xsd_type: XsdComplexType = feature_type.xsd_type
53
-
54
- # Take all relations that are expanded to complex elements,
55
- # and all relations that are fetched for flattened elements.
56
- related = {
57
- xsd_element.orm_path
58
- for xsd_element in xsd_type.complex_elements
59
- if not xsd_element.is_many
60
- } | {
61
- xsd_element.orm_relation[0]
62
- for xsd_element in xsd_type.flattened_elements
63
- if not xsd_element.is_many
64
- }
65
- if related:
66
- queryset = queryset.select_related(*related)
67
-
68
- 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
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)
69
50
 
70
51
  def render_stream(self):
71
- output = StringIO()
52
+ self.output = output = StringIO()
72
53
  writer = csv.writer(output, dialect=self.dialect)
73
54
 
74
55
  is_first_collection = True
75
56
  for sub_collection in self.collection.results:
57
+ projection = sub_collection.projection
76
58
  if is_first_collection:
77
59
  is_first_collection = False
78
60
  else:
@@ -80,14 +62,12 @@ class CSVRenderer(OutputRenderer):
80
62
  output.write("\n\n")
81
63
 
82
64
  # 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))
65
+ xsd_elements = projection.xsd_root_elements
66
+ writer.writerow(self.get_header(projection, xsd_elements))
85
67
 
86
- # By using .iterator(), the results are streamed with as little memory as
87
- # possible. Doing prefetch_related() is not possible now. That could only
88
- # be implemented with cursor pagination for large sets for 1000+ results.
89
- for instance in sub_collection.iterator():
90
- writer.writerow(self.get_row(instance, fields))
68
+ # Write all rows
69
+ for instance in sub_collection:
70
+ writer.writerow(self.get_row(instance, projection, xsd_elements))
91
71
 
92
72
  # Only perform a 'yield' every once in a while,
93
73
  # as it goes back-and-forth for writing it to the client.
@@ -101,38 +81,50 @@ class CSVRenderer(OutputRenderer):
101
81
 
102
82
  def render_exception(self, exception: Exception):
103
83
  """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"
84
+ message = super().render_exception(exception)
85
+ buffer = self.output.getvalue()
86
+ return f"{buffer}\n\n{message}\n"
108
87
 
109
- def get_header(self, fields: list[XsdElement]) -> list[str]:
88
+ def get_header(
89
+ self, projection: FeatureProjection, xsd_elements: list[XsdElement], prefix=""
90
+ ) -> list[str]:
110
91
  """Return all field names."""
111
92
  names = []
112
93
  append = names.append
113
- for field in fields:
114
- if field.type.is_complex_type:
115
- # Expand complex types
116
- for sub_field in field.type.elements:
117
- append(f"{field.name}.{sub_field.name}")
94
+ for xsd_element in xsd_elements:
95
+ if xsd_element.type.is_complex_type:
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
+ )
118
103
  else:
119
- append(field.name)
104
+ append(f"{prefix}{xsd_element.name}")
120
105
 
121
106
  return names
122
107
 
123
- def get_row(self, instance: models.Model, fields: list[XsdElement]):
108
+ def get_row(
109
+ self, instance: models.Model, projection: FeatureProjection, xsd_elements: list[XsdElement]
110
+ ):
124
111
  """Return all field values for a single row."""
125
112
  values = []
126
113
  append = values.append
127
- for field in fields:
128
- if field.is_geometry:
129
- append(self.render_geometry(instance, field))
114
+ for xsd_element in xsd_elements:
115
+ if xsd_element.type.is_geometry:
116
+ append(self.render_geometry(instance, xsd_element))
130
117
  continue
131
118
 
132
- value = field.get_value(instance)
133
- if field.type.is_complex_type:
134
- for sub_field in field.type.elements:
135
- append(sub_field.get_value(value))
119
+ value = xsd_element.get_value(instance)
120
+ if xsd_element.type.is_complex_type:
121
+ values.extend(
122
+ self.get_row(
123
+ instance=value,
124
+ projection=projection,
125
+ xsd_elements=projection.xsd_child_nodes[xsd_element],
126
+ )
127
+ )
136
128
  elif isinstance(value, list):
137
129
  # Array field
138
130
  append(",".join(map(str, value)))
@@ -142,35 +134,39 @@ class CSVRenderer(OutputRenderer):
142
134
  append(value)
143
135
  return values
144
136
 
145
- def render_geometry(self, instance: models.Model, field: XsdElement):
137
+ def render_geometry(self, instance: models.Model, geo_element: GeometryXsdElement):
146
138
  """Render the contents of a geometry value."""
147
- return field.get_value(instance)
139
+ return geo_element.get_value(instance)
148
140
 
149
141
 
150
142
  class DBCSVRenderer(CSVRenderer):
151
143
  """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.
144
+ This is about 40% faster than calling the GEOS C-API from python.
153
145
  """
154
146
 
155
- @classmethod
156
147
  def decorate_queryset(
157
- cls,
158
- feature_type: FeatureType,
159
- queryset: models.QuerySet,
160
- output_crs: CRS,
161
- **params,
148
+ self, projection: FeatureProjection, queryset: models.QuerySet
162
149
  ) -> models.QuerySet:
163
- queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
164
-
165
- # Instead of reading the binary geometry data,
166
- # ask the database to generate EWKT data directly.
167
- geo_selects = get_db_geometry_selects(feature_type.xsd_type.geometry_elements, output_crs)
168
- if geo_selects:
169
- queryset = queryset.defer(*geo_selects.keys()).annotate(
170
- **build_db_annotations(geo_selects, "_as_ewkt_{name}", AsEWKT)
171
- )
172
-
173
- return queryset
174
-
175
- def render_geometry(self, instance: models.Model, field: XsdElement):
176
- return get_db_annotation(instance, field.name, "_as_ewkt_{name}")
150
+ # Instead of reading the binary geometry data, let the database generate EWKT data.
151
+ # As annotations can't be done for select_related() objects, prefetches are used instead.
152
+ queryset = super().decorate_queryset(projection, queryset)
153
+ return replace_queryset_geometries(
154
+ queryset, projection.geometry_elements, projection.output_crs, AsEWKT
155
+ )
156
+
157
+ def get_prefetch_queryset(
158
+ self, projection: FeatureProjection, feature_relation: FeatureRelation
159
+ ) -> models.QuerySet | None:
160
+ """Perform DB annotations for prefetched relations too."""
161
+ queryset = super().get_prefetch_queryset(projection, feature_relation)
162
+ if queryset is None:
163
+ return None
164
+
165
+ # Find which fields are GML elements, annotate these too.
166
+ return replace_queryset_geometries(
167
+ queryset, feature_relation.geometry_elements, projection.output_crs, AsEWKT
168
+ )
169
+
170
+ def render_geometry(self, instance: models.Model, geo_element: GeometryXsdElement):
171
+ """Render the geometry using a database-rendered version."""
172
+ return get_db_rendered_geometry(instance, geo_element, AsEWKT)
@@ -3,21 +3,19 @@
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
- from gisserver.geometries import CRS
18
- from gisserver.types import XsdComplexType
14
+ from gisserver.geometries import CRS84, WGS84
15
+ from gisserver.projection import FeatureProjection
16
+ from gisserver.types import XsdElement
19
17
 
20
- from .base import OutputRenderer
18
+ from .base import CollectionOutputRenderer
21
19
 
22
20
 
23
21
  def _json_default(obj):
@@ -27,7 +25,7 @@ def _json_default(obj):
27
25
  raise TypeError(f"Unable to serialize {obj.__class__.__name__} to JSON")
28
26
 
29
27
 
30
- class GeoJsonRenderer(OutputRenderer):
28
+ class GeoJsonRenderer(CollectionOutputRenderer):
31
29
  """Fast GeoJSON renderer, using a stream response.
32
30
 
33
31
  The complex encoding bits are handled by the C-library "orjson"
@@ -43,29 +41,29 @@ class GeoJsonRenderer(OutputRenderer):
43
41
  max_page_size = conf.GISSERVER_GEOJSON_MAX_PAGE_SIZE
44
42
  chunk_size = 40_000
45
43
 
46
- @classmethod
47
44
  def decorate_queryset(
48
- cls,
49
- feature_type: FeatureType,
45
+ self,
46
+ projection: FeatureProjection,
50
47
  queryset: models.QuerySet,
51
- output_crs: CRS,
52
- **params,
53
48
  ):
54
- queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
49
+ """Redefine which fields to query, always include geometry, but remove all others"""
50
+ # make sure output CRS matches the coordinate ordering that GEOSGeometry.json returns
51
+ if projection.output_crs == WGS84:
52
+ projection.output_crs = CRS84
55
53
 
54
+ # Make sure geometry is always queried.
56
55
  # Other geometries can be excluded as these are not rendered by 'properties'
57
- 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
61
- ]
62
- if other_geometries:
63
- queryset = queryset.defer(*other_geometries)
56
+ main_geo_element = projection.feature_type.main_geometry_element
57
+ projection.add_field(main_geo_element)
58
+ projection.remove_fields(
59
+ lambda element: element.type.is_geometry and element is not main_geo_element
60
+ )
64
61
 
65
- return queryset
62
+ # Apply the normal optimizations with this altered projection
63
+ return super().decorate_queryset(projection, queryset)
66
64
 
67
65
  def render_stream(self):
68
- output = BytesIO()
66
+ self.output = output = BytesIO()
69
67
 
70
68
  # Generate the header from a Python dict,
71
69
  # but replace the last "}" into a comma, to allow writing more
@@ -78,6 +76,8 @@ class GeoJsonRenderer(OutputRenderer):
78
76
  # Flatten the results, they are not grouped in a second FeatureCollection
79
77
  is_first_collection = True
80
78
  for sub_collection in self.collection.results:
79
+ projection = sub_collection.projection
80
+
81
81
  if is_first_collection:
82
82
  is_first_collection = False
83
83
  else:
@@ -92,7 +92,7 @@ class GeoJsonRenderer(OutputRenderer):
92
92
 
93
93
  # The "properties" object is generated by orjson.dumps(),
94
94
  # while the "geometry" object uses the built-in 'GEOSGeometry.json' result.
95
- output.write(self.render_feature(sub_collection.feature_type, instance))
95
+ output.write(self.render_feature(projection, instance))
96
96
 
97
97
  # Only perform a 'yield' every once in a while,
98
98
  # as it goes back-and-forth for writing it to the client.
@@ -113,35 +113,28 @@ class GeoJsonRenderer(OutputRenderer):
113
113
 
114
114
  def render_exception(self, exception: Exception):
115
115
  """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"
116
+ message = super().render_exception(exception)
117
+ buffer = self.output.getvalue().decode()
118
+ return f"{buffer}/* {message} */\n"
120
119
 
121
- def render_feature(self, feature_type: FeatureType, instance: models.Model) -> bytes:
120
+ def render_feature(self, projection: FeatureProjection, instance: models.Model) -> bytes:
122
121
  """Render the output of a single feature"""
123
122
 
124
123
  # Get all instance attributes:
125
- properties = self.get_properties(feature_type, feature_type.xsd_type, instance)
124
+ properties = self.get_properties(projection, projection.xsd_root_elements, instance)
125
+ feature_type = projection.feature_type
126
126
 
127
+ # Add the name field
127
128
  if feature_type.show_name_field:
128
129
  name = feature_type.get_display_value(instance)
129
- geometry_name = b'"geometry_name":%b,' % orjson.dumps(name)
130
+ json_geometry_name = b'"geometry_name":%b,' % orjson.dumps(name)
130
131
  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
- ) % (
132
+ json_geometry_name = b""
133
+
134
+ return b' {"type":"Feature","id":%b,%b"geometry":%b,"properties":%b}' % (
142
135
  orjson.dumps(f"{feature_type.name}.{instance.pk}"),
143
- geometry_name,
144
- self.render_geometry(feature_type, instance),
136
+ json_geometry_name,
137
+ self.render_geometry(projection, instance),
145
138
  orjson.dumps(properties, default=_json_default),
146
139
  )
147
140
 
@@ -154,15 +147,17 @@ class GeoJsonRenderer(OutputRenderer):
154
147
  else:
155
148
  return value
156
149
 
157
- def render_geometry(self, feature_type, instance: models.Model) -> bytes:
150
+ def render_geometry(self, projection: FeatureProjection, instance: models.Model) -> bytes:
158
151
  """Generate the proper GeoJSON notation for a geometry.
159
152
  This calls the GDAL C-API rendering found in 'GEOSGeometry.json'
160
153
  """
161
- geometry = getattr(instance, feature_type.geometry_field.name)
154
+ geometry = projection.get_main_geometry_value(instance)
162
155
  if geometry is None:
163
156
  return b"null"
164
157
 
165
- self.output_crs.apply_to(geometry)
158
+ # The .json property always outputs coordinates as x,y (longitude,latitude),
159
+ # which the GeoJSON spec requires.
160
+ projection.output_crs.apply_to(geometry)
166
161
  return geometry.json.encode()
167
162
 
168
163
  def get_header(self) -> dict:
@@ -171,11 +166,12 @@ class GeoJsonRenderer(OutputRenderer):
171
166
  The format is based on the WFS 3.0 DRAFT. The count fields are moved
172
167
  to the footer allowing them to be calculated without performing queries.
173
168
  """
169
+ output_crs = self.collection.results[0].projection.output_crs
174
170
  return {
175
171
  "type": "FeatureCollection",
176
172
  "timeStamp": self._format_geojson_value(self.collection.timestamp),
177
173
  # "numberReturned": is written at the end for better query performance.
178
- "crs": {"type": "name", "properties": {"name": str(self.output_crs)}},
174
+ "crs": {"type": "name", "properties": {"name": str(output_crs)}},
179
175
  }
180
176
 
181
177
  def get_footer(self) -> dict:
@@ -218,40 +214,36 @@ 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:
233
- if not xsd_element.is_geometry:
229
+ for xsd_element in xsd_elements:
230
+ if not xsd_element.type.is_geometry:
234
231
  value = xsd_element.get_value(instance)
235
232
  if xsd_element.type.is_complex_type:
236
233
  # Nested object data
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,33 +255,32 @@ 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
- @classmethod
270
- def decorate_queryset(self, feature_type: FeatureType, queryset, output_crs, **params):
261
+ def decorate_queryset(self, projection: FeatureProjection, queryset):
271
262
  """Update the queryset to let the database render the GML output.
272
- This is far more efficient then GeoDjango's logic, which performs a
263
+ This is far more efficient than GeoDjango's logic, which performs a
273
264
  C-API call for every single coordinate of a geometry.
274
265
  """
275
- queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
266
+ queryset = super().decorate_queryset(projection, queryset)
276
267
  # If desired, the entire FeatureCollection could be rendered
277
268
  # 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(
269
+ main_geo_element = projection.feature_type.main_geometry_element
270
+ if main_geo_element is not None:
271
+ queryset = queryset.defer(main_geo_element.orm_path).annotate(
281
272
  _as_db_geojson=AsGeoJSON(
282
- get_db_geometry_target(match, output_crs),
273
+ get_db_geometry_target(main_geo_element, projection.output_crs),
283
274
  precision=conf.GISSERVER_DB_PRECISION,
284
275
  )
285
276
  )
286
277
 
287
278
  return queryset
288
279
 
289
- def render_geometry(self, feature_type, instance: models.Model) -> bytes:
280
+ def render_geometry(self, projection: FeatureProjection, instance: models.Model) -> bytes:
290
281
  """Generate the proper GeoJSON notation for a geometry"""
291
282
  # Database server rendering
292
- if not feature_type.geometry_fields:
283
+ if projection.main_geometry_element is None:
293
284
  return b"null"
294
285
 
295
286
  geojson = instance._as_db_geojson