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.
- {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/METADATA +15 -13
- django_gisserver-1.5.0.dist-info/RECORD +54 -0
- {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/db.py +14 -20
- gisserver/exceptions.py +23 -9
- gisserver/features.py +64 -100
- gisserver/geometries.py +2 -2
- gisserver/operations/base.py +31 -21
- gisserver/operations/wfs20.py +44 -38
- gisserver/output/__init__.py +2 -1
- gisserver/output/base.py +43 -27
- gisserver/output/csv.py +38 -33
- gisserver/output/geojson.py +43 -51
- gisserver/output/gml32.py +88 -67
- gisserver/output/results.py +23 -8
- gisserver/output/utils.py +18 -2
- gisserver/output/xmlschema.py +1 -1
- gisserver/parsers/base.py +2 -2
- gisserver/parsers/fes20/__init__.py +18 -0
- gisserver/parsers/fes20/expressions.py +7 -12
- gisserver/parsers/fes20/functions.py +1 -1
- gisserver/parsers/fes20/operators.py +7 -3
- gisserver/parsers/fes20/query.py +11 -1
- gisserver/parsers/fes20/sorting.py +3 -1
- gisserver/parsers/gml/base.py +1 -1
- gisserver/queries/__init__.py +3 -0
- gisserver/queries/adhoc.py +16 -12
- gisserver/queries/base.py +76 -36
- gisserver/queries/projection.py +240 -0
- gisserver/queries/stored.py +7 -6
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +2 -2
- gisserver/types.py +78 -24
- gisserver/views.py +9 -20
- django_gisserver-1.4.0.dist-info/RECORD +0 -54
- gisserver/output/gml32_lxml.py +0 -612
- {django_gisserver-1.4.0.dist-info → django_gisserver-1.5.0.dist-info}/LICENSE +0 -0
- {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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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 = [
|
|
84
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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(
|
|
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
|
|
114
|
-
if
|
|
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
|
|
117
|
-
append(f"{
|
|
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(
|
|
122
|
+
append(xsd_element.name)
|
|
120
123
|
|
|
121
124
|
return names
|
|
122
125
|
|
|
123
|
-
def get_row(
|
|
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
|
|
128
|
-
if
|
|
129
|
-
append(self.render_geometry(instance,
|
|
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 =
|
|
133
|
-
if
|
|
134
|
-
for sub_field in
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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)
|
gisserver/output/geojson.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
47
|
+
projection: FeatureProjection,
|
|
50
48
|
queryset: models.QuerySet,
|
|
51
49
|
output_crs: CRS,
|
|
52
50
|
**params,
|
|
53
51
|
):
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
for
|
|
60
|
-
if
|
|
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(
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
133
|
+
json_geometry_name = b'"geometry_name":%b,' % orjson.dumps(name)
|
|
130
134
|
else:
|
|
131
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
queryset = queryset.defer(
|
|
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(
|
|
274
|
+
get_db_geometry_target(main_geometry_field, output_crs),
|
|
283
275
|
precision=conf.GISSERVER_DB_PRECISION,
|
|
284
276
|
)
|
|
285
277
|
)
|