django-gisserver 1.5.0__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 (74) hide show
  1. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/METADATA +14 -4
  2. django_gisserver-2.0.dist-info/RECORD +66 -0
  3. {django_gisserver-1.5.0.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 +56 -47
  8. gisserver/exceptions.py +26 -2
  9. gisserver/extensions/__init__.py +4 -0
  10. gisserver/{parsers/fes20 → extensions}/functions.py +10 -4
  11. gisserver/extensions/queries.py +261 -0
  12. gisserver/features.py +220 -156
  13. gisserver/geometries.py +32 -37
  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 +122 -308
  18. gisserver/operations/wfs20.py +423 -337
  19. gisserver/output/__init__.py +9 -48
  20. gisserver/output/base.py +178 -139
  21. gisserver/output/csv.py +65 -74
  22. gisserver/output/geojson.py +34 -35
  23. gisserver/output/gml32.py +254 -246
  24. gisserver/output/iters.py +207 -0
  25. gisserver/output/results.py +52 -26
  26. gisserver/output/stored.py +143 -0
  27. gisserver/output/utils.py +75 -170
  28. gisserver/output/xmlschema.py +85 -46
  29. gisserver/parsers/__init__.py +10 -10
  30. gisserver/parsers/ast.py +320 -0
  31. gisserver/parsers/fes20/__init__.py +13 -27
  32. gisserver/parsers/fes20/expressions.py +82 -38
  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 +331 -127
  37. gisserver/parsers/fes20/sorting.py +104 -33
  38. gisserver/parsers/gml/__init__.py +12 -11
  39. gisserver/parsers/gml/base.py +5 -2
  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 +9 -9
  58. gisserver/templates/gisserver/wfs/feature_field.html +2 -2
  59. gisserver/templatetags/gisserver_tags.py +20 -0
  60. gisserver/types.py +322 -259
  61. gisserver/views.py +198 -56
  62. django_gisserver-1.5.0.dist-info/RECORD +0 -54
  63. gisserver/parsers/base.py +0 -149
  64. gisserver/parsers/fes20/query.py +0 -285
  65. gisserver/parsers/tags.py +0 -102
  66. gisserver/queries/__init__.py +0 -37
  67. gisserver/queries/adhoc.py +0 -185
  68. gisserver/queries/base.py +0 -186
  69. gisserver/queries/projection.py +0 -240
  70. gisserver/queries/stored.py +0 -206
  71. gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
  72. gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
  73. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/LICENSE +0 -0
  74. {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.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
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 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:
115
+ if xsd_element.type.is_geometry:
134
116
  append(self.render_geometry(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,9 @@ 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(self, instance: models.Model, geo_element: GeometryXsdElement):
151
138
  """Render the contents of a geometry value."""
152
- return field.get_value(instance)
139
+ return geo_element.get_value(instance)
153
140
 
154
141
 
155
142
  class DBCSVRenderer(CSVRenderer):
@@ -157,25 +144,29 @@ class DBCSVRenderer(CSVRenderer):
157
144
  This is about 40% faster than calling the GEOS C-API from python.
158
145
  """
159
146
 
160
- @classmethod
161
147
  def decorate_queryset(
162
- cls,
163
- projection: FeatureProjection,
164
- queryset: models.QuerySet,
165
- output_crs: CRS,
166
- **params,
148
+ self, projection: FeatureProjection, queryset: models.QuerySet
167
149
  ) -> 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}")
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)
@@ -11,11 +11,11 @@ from django.utils.functional import Promise
11
11
 
12
12
  from gisserver import conf
13
13
  from gisserver.db import get_db_geometry_target
14
- from gisserver.geometries import CRS
15
- from gisserver.queries import FeatureProjection
14
+ from gisserver.geometries import CRS84, WGS84
15
+ from gisserver.projection import FeatureProjection
16
16
  from gisserver.types import XsdElement
17
17
 
18
- from .base import OutputRenderer
18
+ from .base import CollectionOutputRenderer
19
19
 
20
20
 
21
21
  def _json_default(obj):
@@ -25,7 +25,7 @@ def _json_default(obj):
25
25
  raise TypeError(f"Unable to serialize {obj.__class__.__name__} to JSON")
26
26
 
27
27
 
28
- class GeoJsonRenderer(OutputRenderer):
28
+ class GeoJsonRenderer(CollectionOutputRenderer):
29
29
  """Fast GeoJSON renderer, using a stream response.
30
30
 
31
31
  The complex encoding bits are handled by the C-library "orjson"
@@ -41,29 +41,26 @@ class GeoJsonRenderer(OutputRenderer):
41
41
  max_page_size = conf.GISSERVER_GEOJSON_MAX_PAGE_SIZE
42
42
  chunk_size = 40_000
43
43
 
44
- @classmethod
45
44
  def decorate_queryset(
46
- cls,
45
+ self,
47
46
  projection: FeatureProjection,
48
47
  queryset: models.QuerySet,
49
- output_crs: CRS,
50
- **params,
51
48
  ):
52
49
  """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)
50
+ # make sure output CRS matches the coordinate ordering that GEOSGeometry.json returns
51
+ if projection.output_crs == WGS84:
52
+ projection.output_crs = CRS84
56
53
 
54
+ # Make sure geometry is always queried.
57
55
  # 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)
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
+ )
65
61
 
66
- return queryset
62
+ # Apply the normal optimizations with this altered projection
63
+ return super().decorate_queryset(projection, queryset)
67
64
 
68
65
  def render_stream(self):
69
66
  self.output = output = BytesIO()
@@ -117,7 +114,7 @@ class GeoJsonRenderer(OutputRenderer):
117
114
  def render_exception(self, exception: Exception):
118
115
  """Render the exception in a format that fits with the output."""
119
116
  message = super().render_exception(exception)
120
- buffer = self.output.getvalue()
117
+ buffer = self.output.getvalue().decode()
121
118
  return f"{buffer}/* {message} */\n"
122
119
 
123
120
  def render_feature(self, projection: FeatureProjection, instance: models.Model) -> bytes:
@@ -137,7 +134,7 @@ class GeoJsonRenderer(OutputRenderer):
137
134
  return b' {"type":"Feature","id":%b,%b"geometry":%b,"properties":%b}' % (
138
135
  orjson.dumps(f"{feature_type.name}.{instance.pk}"),
139
136
  json_geometry_name,
140
- self.render_geometry(feature_type, instance),
137
+ self.render_geometry(projection, instance),
141
138
  orjson.dumps(properties, default=_json_default),
142
139
  )
143
140
 
@@ -150,15 +147,17 @@ class GeoJsonRenderer(OutputRenderer):
150
147
  else:
151
148
  return value
152
149
 
153
- def render_geometry(self, feature_type, instance: models.Model) -> bytes:
150
+ def render_geometry(self, projection: FeatureProjection, instance: models.Model) -> bytes:
154
151
  """Generate the proper GeoJSON notation for a geometry.
155
152
  This calls the GDAL C-API rendering found in 'GEOSGeometry.json'
156
153
  """
157
- geometry = getattr(instance, feature_type.geometry_field.name)
154
+ geometry = projection.get_main_geometry_value(instance)
158
155
  if geometry is None:
159
156
  return b"null"
160
157
 
161
- 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)
162
161
  return geometry.json.encode()
163
162
 
164
163
  def get_header(self) -> dict:
@@ -167,11 +166,12 @@ class GeoJsonRenderer(OutputRenderer):
167
166
  The format is based on the WFS 3.0 DRAFT. The count fields are moved
168
167
  to the footer allowing them to be calculated without performing queries.
169
168
  """
169
+ output_crs = self.collection.results[0].projection.output_crs
170
170
  return {
171
171
  "type": "FeatureCollection",
172
172
  "timeStamp": self._format_geojson_value(self.collection.timestamp),
173
173
  # "numberReturned": is written at the end for better query performance.
174
- "crs": {"type": "name", "properties": {"name": str(self.output_crs)}},
174
+ "crs": {"type": "name", "properties": {"name": str(output_crs)}},
175
175
  }
176
176
 
177
177
  def get_footer(self) -> dict:
@@ -227,7 +227,7 @@ class GeoJsonRenderer(OutputRenderer):
227
227
  """
228
228
  props = {}
229
229
  for xsd_element in xsd_elements:
230
- if not xsd_element.is_geometry:
230
+ if not xsd_element.type.is_geometry:
231
231
  value = xsd_element.get_value(instance)
232
232
  if xsd_element.type.is_complex_type:
233
233
  # Nested object data
@@ -258,30 +258,29 @@ class DBGeoJsonRenderer(GeoJsonRenderer):
258
258
  This is even more efficient than calling the C-API for each feature.
259
259
  """
260
260
 
261
- @classmethod
262
- def decorate_queryset(self, projection: FeatureProjection, queryset, output_crs, **params):
261
+ def decorate_queryset(self, projection: FeatureProjection, queryset):
263
262
  """Update the queryset to let the database render the GML output.
264
263
  This is far more efficient than GeoDjango's logic, which performs a
265
264
  C-API call for every single coordinate of a geometry.
266
265
  """
267
- queryset = super().decorate_queryset(projection, queryset, output_crs, **params)
266
+ queryset = super().decorate_queryset(projection, queryset)
268
267
  # If desired, the entire FeatureCollection could be rendered
269
268
  # 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(
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(
273
272
  _as_db_geojson=AsGeoJSON(
274
- get_db_geometry_target(main_geometry_field, output_crs),
273
+ get_db_geometry_target(main_geo_element, projection.output_crs),
275
274
  precision=conf.GISSERVER_DB_PRECISION,
276
275
  )
277
276
  )
278
277
 
279
278
  return queryset
280
279
 
281
- def render_geometry(self, feature_type, instance: models.Model) -> bytes:
280
+ def render_geometry(self, projection: FeatureProjection, instance: models.Model) -> bytes:
282
281
  """Generate the proper GeoJSON notation for a geometry"""
283
282
  # Database server rendering
284
- if not feature_type.geometry_fields:
283
+ if projection.main_geometry_element is None:
285
284
  return b"null"
286
285
 
287
286
  geojson = instance._as_db_geojson