django-gisserver 1.4.0__py3-none-any.whl → 1.4.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.
- {django_gisserver-1.4.0.dist-info → django_gisserver-1.4.1.dist-info}/METADATA +5 -3
- {django_gisserver-1.4.0.dist-info → django_gisserver-1.4.1.dist-info}/RECORD +7 -8
- gisserver/__init__.py +1 -1
- gisserver/features.py +3 -2
- gisserver/output/gml32_lxml.py +0 -612
- {django_gisserver-1.4.0.dist-info → django_gisserver-1.4.1.dist-info}/LICENSE +0 -0
- {django_gisserver-1.4.0.dist-info → django_gisserver-1.4.1.dist-info}/WHEEL +0 -0
- {django_gisserver-1.4.0.dist-info → django_gisserver-1.4.1.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: django-gisserver
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.1
|
|
4
4
|
Summary: Django speaking WFS 2.0 (exposing GeoDjango model fields)
|
|
5
5
|
Home-page: https://github.com/amsterdam/django-gisserver
|
|
6
6
|
Author: Diederik van der Boor
|
|
@@ -12,20 +12,22 @@ Classifier: Intended Audience :: Developers
|
|
|
12
12
|
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Framework :: Django
|
|
20
20
|
Classifier: Framework :: Django :: 3.2
|
|
21
21
|
Classifier: Framework :: Django :: 4.0
|
|
22
22
|
Classifier: Framework :: Django :: 4.2
|
|
23
|
+
Classifier: Framework :: Django :: 5.0
|
|
24
|
+
Classifier: Framework :: Django :: 5.1
|
|
23
25
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
24
26
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
25
27
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
26
28
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
29
|
Requires: Django (>=3.2)
|
|
28
|
-
Requires-Python: >=3.
|
|
30
|
+
Requires-Python: >=3.8
|
|
29
31
|
Description-Content-Type: text/markdown
|
|
30
32
|
License-File: LICENSE
|
|
31
33
|
Requires-Dist: Django >=3.2
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
gisserver/__init__.py,sha256=
|
|
1
|
+
gisserver/__init__.py,sha256=ouhGKnkyYnTmywjuiv3RGvKgca9hYJc5FP0cJxrQDgo,40
|
|
2
2
|
gisserver/conf.py,sha256=3LSfeDRTdJCLuopAOD14Y5m2iPhovreIaNI9zbaukpo,2343
|
|
3
3
|
gisserver/db.py,sha256=iuj43NonqNdKkLrerdzTucy-Lu5NmqymuE0bkwR6XWs,5318
|
|
4
4
|
gisserver/exceptions.py,sha256=TZVdmwKI90Ga-9l8A1H0u65V5yNvgg2TDgyN_LCQ2zI,4597
|
|
5
|
-
gisserver/features.py,sha256=
|
|
5
|
+
gisserver/features.py,sha256=GFbi-ap4NjlJh2Ka6Ssh-OjgZClLzVZyBqYqmolAtfI,32171
|
|
6
6
|
gisserver/geometries.py,sha256=VQVEFk0oRiqdxG28llb51aayf5m9cbyLljR3PrZfMTg,12400
|
|
7
7
|
gisserver/types.py,sha256=vyHPkUXEADikTJfRq7Nodqt0BCo7YV91dNyZw6GIMO4,35087
|
|
8
8
|
gisserver/views.py,sha256=A7TwUsG8NKdnjTbOXPbio1K-XRg6IIpy_xH__zwW8xI,13193
|
|
@@ -14,7 +14,6 @@ gisserver/output/base.py,sha256=NYyV78JgNDV7uUmnk6HNbbLTkt_N60YKPh2z1o-AuzM,7791
|
|
|
14
14
|
gisserver/output/csv.py,sha256=7UKnyhFyAH1yeOh719Ov3SkzoLF1Zzlp_0-wxRDU9bI,6222
|
|
15
15
|
gisserver/output/geojson.py,sha256=KB4w6DU_Dn7ZNDM5INDEtz7zhOAN75v03lUayPDzGPE,11296
|
|
16
16
|
gisserver/output/gml32.py,sha256=kaTljrRKIlxIshiJTtzmsWWJExzQSL4hEX_Q1i17k_4,30496
|
|
17
|
-
gisserver/output/gml32_lxml.py,sha256=u1aJPdbyVHha9vw7XN2rvD_lvyJZ9zVV3afdcN5Ryf8,25166
|
|
18
17
|
gisserver/output/results.py,sha256=HOmeSKLb0Tzsd9AamuXEPZyRmoXxHj7fKBqIbk1J5Co,13301
|
|
19
18
|
gisserver/output/utils.py,sha256=VR8nOAv1obhfumQy77RnpJpE3FopzuIJrkfosBq16w4,6569
|
|
20
19
|
gisserver/output/xmlschema.py,sha256=ltCglrQ4YXJ_dpMrkXb06dFfHc5a_nGog7fb-K25aeQ,4579
|
|
@@ -47,8 +46,8 @@ gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml,sha256=KKrI0YYQqZWt
|
|
|
47
46
|
gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml,sha256=CIBVQrKXmGMQ6BKbX4_0ru4lXtWnW5EIgHUbIi1V0Vc,636
|
|
48
47
|
gisserver/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
48
|
gisserver/templatetags/gisserver_tags.py,sha256=RFGqEj-EQi9FrJiFJ7HHAQqlU4fDKVKTtZrPNwURXgA,204
|
|
50
|
-
django_gisserver-1.4.
|
|
51
|
-
django_gisserver-1.4.
|
|
52
|
-
django_gisserver-1.4.
|
|
53
|
-
django_gisserver-1.4.
|
|
54
|
-
django_gisserver-1.4.
|
|
49
|
+
django_gisserver-1.4.1.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
|
|
50
|
+
django_gisserver-1.4.1.dist-info/METADATA,sha256=SpfcYIXvQiqTubhTKOAlarsHoGcMj9XeZQQRx1CJrW0,5874
|
|
51
|
+
django_gisserver-1.4.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
52
|
+
django_gisserver-1.4.1.dist-info/top_level.txt,sha256=8zFCEMmkpixE4TPiOAlxSq3PD2EXvAeFKwG4yZoIIOQ,10
|
|
53
|
+
django_gisserver-1.4.1.dist-info/RECORD,,
|
gisserver/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.4.
|
|
1
|
+
__version__ = "1.4.1" # follows PEP440
|
gisserver/features.py
CHANGED
|
@@ -189,7 +189,6 @@ class FeatureField:
|
|
|
189
189
|
self.model_field = None
|
|
190
190
|
self.parent = parent
|
|
191
191
|
self.abstract = abstract
|
|
192
|
-
|
|
193
192
|
# Allow to override the class attribute on 'self',
|
|
194
193
|
# which avoids having to subclass this field class as well.
|
|
195
194
|
if xsd_class is not None:
|
|
@@ -593,7 +592,9 @@ class FeatureType:
|
|
|
593
592
|
return [
|
|
594
593
|
(ff.model_field.name if ff.model_field else ff.model_attribute)
|
|
595
594
|
for ff in self.fields
|
|
596
|
-
if not ff.model_field.many_to_many
|
|
595
|
+
if not ff.model_field.many_to_many
|
|
596
|
+
and not ff.model_field.one_to_many
|
|
597
|
+
and not (ff.model_attribute and "." in ff.model_attribute)
|
|
597
598
|
]
|
|
598
599
|
|
|
599
600
|
@cached_property
|
gisserver/output/gml32_lxml.py
DELETED
|
@@ -1,612 +0,0 @@
|
|
|
1
|
-
"""Output rendering logic.
|
|
2
|
-
|
|
3
|
-
Note that the Django format_html() / mark_safe() logic is not used here,
|
|
4
|
-
as it's quite a performance improvement to just use html.escape().
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import re
|
|
10
|
-
from datetime import date, datetime, time, timezone
|
|
11
|
-
from io import BytesIO
|
|
12
|
-
from typing import cast
|
|
13
|
-
|
|
14
|
-
from django.contrib.gis import geos
|
|
15
|
-
from django.db import models
|
|
16
|
-
from django.http import HttpResponse, StreamingHttpResponse
|
|
17
|
-
from lxml import etree
|
|
18
|
-
|
|
19
|
-
from gisserver.db import (
|
|
20
|
-
AsGML,
|
|
21
|
-
build_db_annotations,
|
|
22
|
-
conditional_transform,
|
|
23
|
-
get_db_annotation,
|
|
24
|
-
get_db_geometry_selects,
|
|
25
|
-
get_db_geometry_target,
|
|
26
|
-
get_geometries_union,
|
|
27
|
-
)
|
|
28
|
-
from gisserver.exceptions import NotFound
|
|
29
|
-
from gisserver.features import FeatureRelation, FeatureType
|
|
30
|
-
from gisserver.geometries import CRS
|
|
31
|
-
from gisserver.parsers.fes20 import ValueReference
|
|
32
|
-
from gisserver.types import XsdComplexType, XsdElement
|
|
33
|
-
|
|
34
|
-
from .base import OutputRenderer
|
|
35
|
-
from .results import SimpleFeatureCollection
|
|
36
|
-
|
|
37
|
-
GML_RENDER_FUNCTIONS = {}
|
|
38
|
-
RE_GML_ID = re.compile(r'gml:id="[^"]+"')
|
|
39
|
-
|
|
40
|
-
WFS_NS = "http://www.opengis.net/wfs/2.0"
|
|
41
|
-
GML32_NS = "http://www.opengis.net/gml/3.2"
|
|
42
|
-
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
|
43
|
-
|
|
44
|
-
FIX_GML_NS = f' xmlns:gml="{GML32_NS}">'
|
|
45
|
-
|
|
46
|
-
# Avoid string concatenations in code:
|
|
47
|
-
XSI_NIL_ATTRIB = {f"{{{XSI_NS}}}nil": "true"}
|
|
48
|
-
GML_ID_ATTR = f"{{{GML32_NS}}}id" # gml:id="..."
|
|
49
|
-
WFS_MEMBER = f"{{{WFS_NS}}}member" # <wfs:member>
|
|
50
|
-
GML_BOUNDED_BY = f"{{{GML32_NS}}}boundedBy" # <gml:boundedBy>
|
|
51
|
-
GML_ENVELOPE = f"{{{GML32_NS}}}Envelope" # <gml:Envelope>
|
|
52
|
-
GML_LOWER_CORNER = f"{{{GML32_NS}}}lowerCorner" # <gml:lowerCorner>
|
|
53
|
-
GML_UPPER_CORNER = f"{{{GML32_NS}}}upperCorner" # <gml:upperCorner>
|
|
54
|
-
|
|
55
|
-
# register common namespaces globally, that's fine here I guess... *fingers crossed*
|
|
56
|
-
# etree.register_namespace("wfs", WFS_NS)
|
|
57
|
-
# etree.register_namespace("gml", GML32_NS)
|
|
58
|
-
NS_MAP = {
|
|
59
|
-
"wfs": WFS_NS,
|
|
60
|
-
"gml": GML32_NS,
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def default_if_none(value, default):
|
|
65
|
-
if value is None:
|
|
66
|
-
return default
|
|
67
|
-
else:
|
|
68
|
-
return value
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def register_geos_type(geos_type):
|
|
72
|
-
def _inc(func):
|
|
73
|
-
GML_RENDER_FUNCTIONS[geos_type] = func
|
|
74
|
-
return func
|
|
75
|
-
|
|
76
|
-
return _inc
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class GML32Renderer(OutputRenderer):
|
|
80
|
-
"""Render the GetFeature XML output in GML 3.2 format"""
|
|
81
|
-
|
|
82
|
-
content_type = "text/xml; charset=utf-8"
|
|
83
|
-
xml_collection_tag = "FeatureCollection"
|
|
84
|
-
chunk_size = 40_000
|
|
85
|
-
|
|
86
|
-
def __init__(self, *args, **kwargs):
|
|
87
|
-
super().__init__(*args, **kwargs)
|
|
88
|
-
self.nsmap = {**NS_MAP, "app": self.app_xml_namespace}
|
|
89
|
-
|
|
90
|
-
def _make_element(self, node: XsdElement, attrib=None) -> etree.Element:
|
|
91
|
-
tag = f"{{{self.nsmap[node.prefix]}}}{node.name}" # node.xml_name is prefix:name format.
|
|
92
|
-
return etree.Element(tag, attrib=attrib, nsmap=self.nsmap)
|
|
93
|
-
|
|
94
|
-
def _make_feature_element(self, feature_type: FeatureType, attrib=None) -> etree.Element:
|
|
95
|
-
tag = f"{{{self.nsmap[feature_type.xml_prefix]}}}{feature_type.name}" # node.xml_name is prefix:name format.
|
|
96
|
-
return etree.Element(tag, attrib=attrib, nsmap=self.nsmap)
|
|
97
|
-
|
|
98
|
-
def get_response(self):
|
|
99
|
-
"""Render the output as streaming response."""
|
|
100
|
-
from gisserver.queries import GetFeatureById
|
|
101
|
-
|
|
102
|
-
if isinstance(self.source_query, GetFeatureById):
|
|
103
|
-
# WFS spec requires that GetFeatureById output only returns the contents.
|
|
104
|
-
# The streaming response is avoided here, to allow returning a 404.
|
|
105
|
-
return self.get_standalone_response()
|
|
106
|
-
else:
|
|
107
|
-
# Use default streaming response, with render_stream()
|
|
108
|
-
response = super().get_response()
|
|
109
|
-
if isinstance(response, StreamingHttpResponse):
|
|
110
|
-
response["Content-Encoding"] = "gzip" # lxml.xmlfile compression
|
|
111
|
-
return response
|
|
112
|
-
|
|
113
|
-
def get_standalone_response(self):
|
|
114
|
-
"""Render a standalone item, for GetFeatureById"""
|
|
115
|
-
sub_collection = self.collection.results[0]
|
|
116
|
-
self.start_collection(sub_collection)
|
|
117
|
-
instance = sub_collection.first()
|
|
118
|
-
if instance is None:
|
|
119
|
-
raise NotFound("Feature not found.")
|
|
120
|
-
|
|
121
|
-
body = self.render_feature(
|
|
122
|
-
feature_type=sub_collection.feature_type,
|
|
123
|
-
instance=instance,
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
if isinstance(body, str):
|
|
127
|
-
# Best guess for GetFeatureById combined with
|
|
128
|
-
# GetPropertyValue&VALUEREFERENCE=@gml:id
|
|
129
|
-
return HttpResponse(body, content_type="text/plain")
|
|
130
|
-
else:
|
|
131
|
-
return HttpResponse(
|
|
132
|
-
etree.tostring(body, xml_declaration=True),
|
|
133
|
-
content_type=self.content_type,
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
def get_xsi_schema_location(self):
|
|
137
|
-
xsd_typenames = ",".join(
|
|
138
|
-
sub_collection.feature_type.name for sub_collection in self.collection.results
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
# These are pairs of "namespace location"
|
|
142
|
-
return (
|
|
143
|
-
f"{self.app_xml_namespace}"
|
|
144
|
-
f" {self.server_url}?SERVICE=WFS&VERSION=2.0.0&REQUEST=DescribeFeatureType&TYPENAMES={xsd_typenames}"
|
|
145
|
-
" http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd"
|
|
146
|
-
" http://www.opengis.net/gml/3.2 http://schemas.opengis.net/gml/3.2.1/gml.xsd"
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
def render_stream(self):
|
|
150
|
-
"""Render the XML as streaming content.
|
|
151
|
-
This renders the standard <wfs:FeatureCollection> / <wfs:ValueCollection>
|
|
152
|
-
"""
|
|
153
|
-
collection = self.collection
|
|
154
|
-
output = BytesIO()
|
|
155
|
-
number_matched = collection.number_matched
|
|
156
|
-
number_returned = collection.number_returned
|
|
157
|
-
|
|
158
|
-
root_attrib = {
|
|
159
|
-
f"{{{XSI_NS}}}schemaLocation": self.get_xsi_schema_location(),
|
|
160
|
-
"timestamp": collection.timestamp,
|
|
161
|
-
"numberMatched": (
|
|
162
|
-
str(int(number_matched)) if number_matched is not None else "unknown"
|
|
163
|
-
),
|
|
164
|
-
"numberReturned": str(number_returned),
|
|
165
|
-
}
|
|
166
|
-
if collection.next:
|
|
167
|
-
root_attrib["next"] = collection.next
|
|
168
|
-
if collection.previous:
|
|
169
|
-
root_attrib["previous"] = collection.previous
|
|
170
|
-
|
|
171
|
-
with etree.xmlfile(output) as xml_file:
|
|
172
|
-
xml_file.write_declaration()
|
|
173
|
-
|
|
174
|
-
# Either FeatureCollection / ValueCollection
|
|
175
|
-
with xml_file.element(
|
|
176
|
-
f"{{{WFS_NS}}}{self.xml_collection_tag}",
|
|
177
|
-
attrib=root_attrib,
|
|
178
|
-
nsmap=self.nsmap,
|
|
179
|
-
):
|
|
180
|
-
# no results in all sub collections, means not writing any <wfs:member> tags at all.
|
|
181
|
-
if number_returned:
|
|
182
|
-
has_multiple_collections = len(collection.results) > 1
|
|
183
|
-
|
|
184
|
-
for sub_collection in collection.results:
|
|
185
|
-
self.start_collection(sub_collection) # hook for subclasses
|
|
186
|
-
|
|
187
|
-
if has_multiple_collections:
|
|
188
|
-
xml_file.write(
|
|
189
|
-
(
|
|
190
|
-
f"<wfs:member>\n"
|
|
191
|
-
f"<wfs:{self.xml_collection_tag}"
|
|
192
|
-
f' timeStamp="{collection.timestamp}"'
|
|
193
|
-
f' numberMatched="{int(sub_collection.number_matched)}"'
|
|
194
|
-
f' numberReturned="{int(sub_collection.number_returned)}">\n'
|
|
195
|
-
).encode()
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
for i, instance in enumerate(sub_collection):
|
|
199
|
-
xml_file.write(
|
|
200
|
-
self.render_wfs_member(sub_collection.feature_type, instance)
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
# Only perform a 'yield' every once in a while,
|
|
204
|
-
# as it goes back-and-forth for writing it to the client.
|
|
205
|
-
if output.tell() > self.chunk_size:
|
|
206
|
-
xml_file.flush()
|
|
207
|
-
xml_chunk = output.getvalue()
|
|
208
|
-
output.seek(0)
|
|
209
|
-
output.truncate(0)
|
|
210
|
-
yield xml_chunk
|
|
211
|
-
|
|
212
|
-
if has_multiple_collections:
|
|
213
|
-
xml_file.write(
|
|
214
|
-
f"</wfs:{self.xml_collection_tag}>\n</wfs:member>\n".encode()
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
# xml_file.flush()
|
|
218
|
-
yield output.getvalue()
|
|
219
|
-
|
|
220
|
-
def start_collection(self, sub_collection: SimpleFeatureCollection):
|
|
221
|
-
"""Hook to allow initialization per feature type"""
|
|
222
|
-
pass
|
|
223
|
-
|
|
224
|
-
def render_wfs_member(
|
|
225
|
-
self, feature_type: FeatureType, instance: models.Model
|
|
226
|
-
) -> etree.Element:
|
|
227
|
-
"""Write the full <wfs:member> block."""
|
|
228
|
-
wfs_member_tag = etree.Element(WFS_MEMBER, nsmap=self.nsmap)
|
|
229
|
-
wfs_member_tag.append(self.render_feature(feature_type, instance))
|
|
230
|
-
return wfs_member_tag
|
|
231
|
-
|
|
232
|
-
def render_feature(
|
|
233
|
-
self, feature_type: FeatureType, instance: models.Model, nsmap=None
|
|
234
|
-
) -> etree.Element:
|
|
235
|
-
"""Write the contents of the object value.
|
|
236
|
-
|
|
237
|
-
This output is typically wrapped in <wfs:member> tags
|
|
238
|
-
unless it's used for a GetPropertyById response.
|
|
239
|
-
"""
|
|
240
|
-
self.gml_seq = 0 # need to increment this between write_xml_field calls
|
|
241
|
-
|
|
242
|
-
# Write <app:FeatureTypeName> start node
|
|
243
|
-
feature_tag = self._make_feature_element(
|
|
244
|
-
feature_type.xml_name,
|
|
245
|
-
attrib={GML_ID_ATTR: f"{feature_type.name}.{instance.pk}"},
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
# Add all base class members, in their correct ordering
|
|
249
|
-
# By having these as XsdElement objects instead of hard-coded writes,
|
|
250
|
-
# the query/filter logic also works for these elements.
|
|
251
|
-
if feature_type.xsd_type.base.is_complex_type:
|
|
252
|
-
for xsd_element in feature_type.xsd_type.base.elements:
|
|
253
|
-
if xsd_element.xml_name == "gml:boundedBy":
|
|
254
|
-
# Special case for <gml:boundedBy>, so it will render with
|
|
255
|
-
# the output CRS and can be overwritten with DB-rendered GML.
|
|
256
|
-
gml = self.render_bounds(feature_type, instance)
|
|
257
|
-
if gml is not None:
|
|
258
|
-
feature_tag.append(gml)
|
|
259
|
-
elif xsd_element.is_many:
|
|
260
|
-
# some <app:...> node that has multiple values
|
|
261
|
-
feature_tag.extend(self.render_many(feature_type, xsd_element, instance))
|
|
262
|
-
else:
|
|
263
|
-
# e.g. <gml:name>, or all other <app:...> nodes.
|
|
264
|
-
feature_tag.append(self.render_xml_field(feature_type, xsd_element, instance))
|
|
265
|
-
|
|
266
|
-
# Add all members
|
|
267
|
-
for xsd_element in feature_type.xsd_type.elements:
|
|
268
|
-
if xsd_element.is_many:
|
|
269
|
-
# some <app:...> node that has multiple values
|
|
270
|
-
feature_tag.extend(self.render_many(feature_type, xsd_element, instance))
|
|
271
|
-
else:
|
|
272
|
-
# e.g. <gml:name>, or all other <app:...> nodes.
|
|
273
|
-
feature_tag.append(self.render_xml_field(feature_type, xsd_element, instance))
|
|
274
|
-
|
|
275
|
-
return feature_tag
|
|
276
|
-
|
|
277
|
-
def render_bounds(self, feature_type, instance) -> etree.Element | None:
|
|
278
|
-
"""Render the GML bounds for the complete instance"""
|
|
279
|
-
envelope = feature_type.get_envelope(instance, self.output_crs)
|
|
280
|
-
if envelope is None:
|
|
281
|
-
return None
|
|
282
|
-
|
|
283
|
-
bounded_by = etree.Element(GML_BOUNDED_BY, nsmap=self.nsmap)
|
|
284
|
-
envelope = etree.SubElement(
|
|
285
|
-
bounded_by,
|
|
286
|
-
GML_ENVELOPE,
|
|
287
|
-
attrib={"srsDimension": "2", "srsName": self.xml_srs_name},
|
|
288
|
-
nsmap=self.nsmap,
|
|
289
|
-
)
|
|
290
|
-
lower = " ".join(map(str, envelope.lower_corner))
|
|
291
|
-
upper = " ".join(map(str, envelope.upper_corner))
|
|
292
|
-
etree.SubElement(envelope, GML_LOWER_CORNER, nsmap=self.nsmap).text = lower
|
|
293
|
-
etree.SubElement(envelope, GML_UPPER_CORNER, nsmap=self.nsmap).text = upper
|
|
294
|
-
return bounded_by
|
|
295
|
-
|
|
296
|
-
def render_many(
|
|
297
|
-
self, feature_type, xsd_element: XsdElement, instance: models.Model
|
|
298
|
-
) -> list[etree.Element]:
|
|
299
|
-
"""Render a single field, multiple times."""
|
|
300
|
-
value = xsd_element.get_value(instance)
|
|
301
|
-
if value is None:
|
|
302
|
-
# No tag for optional element (see PropertyIsNull), otherwise xsi:nil node.
|
|
303
|
-
if xsd_element.min_occurs == 0:
|
|
304
|
-
return []
|
|
305
|
-
else:
|
|
306
|
-
# <app:field xsi:nil="true"/>
|
|
307
|
-
return [self._make_element(xsd_element, attrib=XSI_NIL_ATTRIB)]
|
|
308
|
-
else:
|
|
309
|
-
# Render the tag multiple times
|
|
310
|
-
if xsd_element.type.is_complex_type:
|
|
311
|
-
# If the retrieved QuerySet was not filtered yet, do so now. This can't
|
|
312
|
-
# be done in get_value() because the FeatureType is not known there.
|
|
313
|
-
value = feature_type.filter_related_queryset(value)
|
|
314
|
-
|
|
315
|
-
return [
|
|
316
|
-
self.render_xml_field(feature_type, xsd_element, instance=item) for item in value
|
|
317
|
-
]
|
|
318
|
-
|
|
319
|
-
def render_xml_field(
|
|
320
|
-
self, feature_type: FeatureType, xsd_element: XsdElement, instance: models.Model
|
|
321
|
-
) -> etree.Element:
|
|
322
|
-
"""Write the value of a single field."""
|
|
323
|
-
if xsd_element.is_geometry:
|
|
324
|
-
# Short-cirquit, allow overriding:
|
|
325
|
-
return self.render_gml_field(feature_type, xsd_element, instance)
|
|
326
|
-
|
|
327
|
-
value = xsd_element.get_value(instance)
|
|
328
|
-
if value is None:
|
|
329
|
-
return self._make_element(xsd_element, attrib=XSI_NIL_ATTRIB)
|
|
330
|
-
elif xsd_element.type.is_complex_type:
|
|
331
|
-
# Expanded foreign relation / dictionary
|
|
332
|
-
return self.render_xml_complex_type(feature_type, xsd_element, value)
|
|
333
|
-
else:
|
|
334
|
-
xml_field = self._make_element(xsd_element)
|
|
335
|
-
xml_field.text = self._value_to_string(value)
|
|
336
|
-
return xml_field # <app:field>{value}</app:field>
|
|
337
|
-
|
|
338
|
-
def _value_to_string(self, value):
|
|
339
|
-
# TODO: can this be a lookup on the XsdElement?
|
|
340
|
-
if isinstance(value, str):
|
|
341
|
-
return value
|
|
342
|
-
elif isinstance(value, datetime):
|
|
343
|
-
return value.astimezone(timezone.utc).isoformat()
|
|
344
|
-
elif isinstance(value, (date, time)):
|
|
345
|
-
return value.isoformat()
|
|
346
|
-
elif isinstance(value, bool):
|
|
347
|
-
return "true" if value else "false"
|
|
348
|
-
else:
|
|
349
|
-
return str(value)
|
|
350
|
-
|
|
351
|
-
def render_xml_complex_type(self, feature_type, xsd_element, value) -> etree.Element:
|
|
352
|
-
"""Write a single field, that consists of sub elements"""
|
|
353
|
-
xsd_type = cast(XsdComplexType, xsd_element.type)
|
|
354
|
-
field = self._make_element(xsd_element)
|
|
355
|
-
for sub_element in xsd_type.elements:
|
|
356
|
-
if sub_element.is_many:
|
|
357
|
-
field.extend(self.render_many(feature_type, sub_element, instance=value))
|
|
358
|
-
else:
|
|
359
|
-
field.append(self.render_xml_field(feature_type, sub_element, instance=value))
|
|
360
|
-
|
|
361
|
-
return field
|
|
362
|
-
|
|
363
|
-
def render_gml_field(
|
|
364
|
-
self,
|
|
365
|
-
feature_type: FeatureType,
|
|
366
|
-
xsd_element: XsdElement,
|
|
367
|
-
instance,
|
|
368
|
-
) -> str:
|
|
369
|
-
"""Write the field that holds an GML tag.
|
|
370
|
-
This is a separate function on purpose, so it can be optimized for database-rendering.
|
|
371
|
-
"""
|
|
372
|
-
value: geos.GEOSGeometry = xsd_element.get_value(instance) # take from DB field
|
|
373
|
-
if value is None:
|
|
374
|
-
return self._make_element(xsd_element.xml_name, attrib=XSI_NIL_ATTRIB)
|
|
375
|
-
|
|
376
|
-
self.gml_seq += 1
|
|
377
|
-
gml_id = self.get_gml_id(feature_type, instance.pk, seq=self.gml_seq)
|
|
378
|
-
|
|
379
|
-
# Reparsing XML is faster than rendering it ourselves,
|
|
380
|
-
# because it avoids a C-API call for every coordinate
|
|
381
|
-
self.output_crs.apply_to(value)
|
|
382
|
-
gml_parsed = etree.XML(value.ogr.gml)
|
|
383
|
-
gml_parsed.attrib[GML_ID_ATTR] = gml_id
|
|
384
|
-
gml_parsed.attrib["srsName"] = self.xml_srs_name # TODO: needed?
|
|
385
|
-
|
|
386
|
-
node = self._make_element(xsd_element)
|
|
387
|
-
node.append(gml_parsed)
|
|
388
|
-
return node
|
|
389
|
-
|
|
390
|
-
def get_gml_id(self, feature_type: FeatureType, object_id, seq) -> str:
|
|
391
|
-
"""Generate the gml:id value, which is required for GML 3.2 objects."""
|
|
392
|
-
return f"{feature_type.name}.{object_id}.{seq}"
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
class DBGML32Renderer(GML32Renderer):
|
|
396
|
-
"""Faster GetFeature renderer that uses the database to render GML 3.2"""
|
|
397
|
-
|
|
398
|
-
@classmethod
|
|
399
|
-
def decorate_queryset(
|
|
400
|
-
cls,
|
|
401
|
-
feature_type: FeatureType,
|
|
402
|
-
queryset: models.QuerySet,
|
|
403
|
-
output_crs: CRS,
|
|
404
|
-
**params,
|
|
405
|
-
):
|
|
406
|
-
"""Update the queryset to let the database render the GML output.
|
|
407
|
-
This is far more efficient then GeoDjango's logic, which performs a
|
|
408
|
-
C-API call for every single coordinate of a geometry.
|
|
409
|
-
"""
|
|
410
|
-
queryset = super().decorate_queryset(feature_type, queryset, output_crs, **params)
|
|
411
|
-
|
|
412
|
-
# Retrieve geometries as pre-rendered instead.
|
|
413
|
-
gml_elements = feature_type.xsd_type.geometry_elements
|
|
414
|
-
geo_selects = get_db_geometry_selects(gml_elements, output_crs)
|
|
415
|
-
if geo_selects:
|
|
416
|
-
queryset = queryset.defer(*geo_selects.keys()).annotate(
|
|
417
|
-
_as_envelope_gml=cls.get_db_envelope_as_gml(feature_type, queryset, output_crs),
|
|
418
|
-
**build_db_annotations(geo_selects, "_as_gml_{name}", AsGML),
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
return queryset
|
|
422
|
-
|
|
423
|
-
@classmethod
|
|
424
|
-
def get_prefetch_queryset(
|
|
425
|
-
cls,
|
|
426
|
-
feature_type: FeatureType,
|
|
427
|
-
feature_relation: FeatureRelation,
|
|
428
|
-
output_crs: CRS,
|
|
429
|
-
) -> models.QuerySet | None:
|
|
430
|
-
"""Perform DB annotations for prefetched relations too."""
|
|
431
|
-
base = super().get_prefetch_queryset(feature_type, feature_relation, output_crs)
|
|
432
|
-
if base is None:
|
|
433
|
-
return None
|
|
434
|
-
|
|
435
|
-
# Find which fields are GML elements
|
|
436
|
-
gml_elements = []
|
|
437
|
-
for e in feature_relation.xsd_elements:
|
|
438
|
-
if e.is_geometry:
|
|
439
|
-
# Prefetching a flattened relation
|
|
440
|
-
gml_elements.append(e)
|
|
441
|
-
elif e.type.is_complex_type:
|
|
442
|
-
# Prefetching a complex type
|
|
443
|
-
xsd_type: XsdComplexType = cast(XsdComplexType, e.type)
|
|
444
|
-
gml_elements.extend(xsd_type.geometry_elements)
|
|
445
|
-
|
|
446
|
-
geometries = get_db_geometry_selects(gml_elements, output_crs)
|
|
447
|
-
if geometries:
|
|
448
|
-
# Exclude geometries from the fields, fetch them as pre-rendered annotations instead.
|
|
449
|
-
return base.defer(geometries.keys()).annotate(
|
|
450
|
-
**build_db_annotations(geometries, "_as_gml_{name}", AsGML),
|
|
451
|
-
)
|
|
452
|
-
else:
|
|
453
|
-
return base
|
|
454
|
-
|
|
455
|
-
@classmethod
|
|
456
|
-
def get_db_envelope_as_gml(cls, feature_type, queryset, output_crs) -> AsGML:
|
|
457
|
-
"""Offload the GML rendering of the envelope to the database.
|
|
458
|
-
|
|
459
|
-
This also avoids offloads the geometry union calculation to the DB.
|
|
460
|
-
"""
|
|
461
|
-
geo_fields_union = cls._get_geometries_union(feature_type, queryset, output_crs)
|
|
462
|
-
return AsGML(geo_fields_union, envelope=True)
|
|
463
|
-
|
|
464
|
-
@classmethod
|
|
465
|
-
def _get_geometries_union(cls, feature_type: FeatureType, queryset, output_crs):
|
|
466
|
-
"""Combine all geometries of the model in a single SQL function."""
|
|
467
|
-
# Apply transforms where needed, in case some geometries use a different SRID.
|
|
468
|
-
return get_geometries_union(
|
|
469
|
-
[
|
|
470
|
-
conditional_transform(
|
|
471
|
-
model_field.name,
|
|
472
|
-
model_field.srid,
|
|
473
|
-
output_srid=output_crs.srid,
|
|
474
|
-
)
|
|
475
|
-
for model_field in feature_type.geometry_fields
|
|
476
|
-
],
|
|
477
|
-
using=queryset.db,
|
|
478
|
-
)
|
|
479
|
-
|
|
480
|
-
def render_gml_field(
|
|
481
|
-
self,
|
|
482
|
-
feature_type: FeatureType,
|
|
483
|
-
xsd_element: XsdElement,
|
|
484
|
-
instance,
|
|
485
|
-
gml_id,
|
|
486
|
-
) -> str:
|
|
487
|
-
# Optimized path, pre-rendered GML
|
|
488
|
-
# Take from DB annotation instead of DB field.
|
|
489
|
-
gml_text = get_db_annotation(instance, xsd_element.name, "_as_gml_{name}")
|
|
490
|
-
if gml_text is None:
|
|
491
|
-
return self._make_element(xsd_element, attrib=XSI_NIL_ATTRIB)
|
|
492
|
-
else:
|
|
493
|
-
# Extract DB annotation, it needs an gml:id, and have namespaces following ours.
|
|
494
|
-
gml_text = gml_text.replace(">", FIX_GML_NS, 1) # fix parsing issues
|
|
495
|
-
self.gml_seq += 1
|
|
496
|
-
gml_id = self.get_gml_id(feature_type, instance.pk, seq=self.gml_seq)
|
|
497
|
-
gml_parsed = etree.XML(gml_text) # TODO: this means re-parsing the string..
|
|
498
|
-
gml_parsed.attrib[GML_ID_ATTR] = gml_id
|
|
499
|
-
|
|
500
|
-
node = self._make_element(xsd_element)
|
|
501
|
-
node.append(gml_parsed)
|
|
502
|
-
return node
|
|
503
|
-
|
|
504
|
-
def render_bounds(self, feature_type, instance):
|
|
505
|
-
"""Generate the <gml:boundedBy> from DB prerendering."""
|
|
506
|
-
gml_text = instance._as_envelope_gml
|
|
507
|
-
if gml_text is not None:
|
|
508
|
-
# TODO: need to reparse here, but can that be avoided??
|
|
509
|
-
gml_text = gml_text.replace(">", FIX_GML_NS, 1) # fix parsing issues
|
|
510
|
-
bounded_by = etree.Element(GML_BOUNDED_BY, nsmap=self.nsmap)
|
|
511
|
-
bounded_by.append(etree.XML(gml_text))
|
|
512
|
-
return bounded_by
|
|
513
|
-
# return f"<gml:boundedBy>{gml}</gml:boundedBy>\n"
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
class GML32ValueRenderer(GML32Renderer):
|
|
517
|
-
"""Render the GetPropertyValue XML output in GML 3.2 format"""
|
|
518
|
-
|
|
519
|
-
content_type = "text/xml; charset=utf-8"
|
|
520
|
-
xml_collection_tag = "ValueCollection"
|
|
521
|
-
|
|
522
|
-
def __init__(self, *args, value_reference: ValueReference, **kwargs):
|
|
523
|
-
self.value_reference = value_reference
|
|
524
|
-
super().__init__(*args, **kwargs)
|
|
525
|
-
self.xsd_node = None
|
|
526
|
-
|
|
527
|
-
@classmethod
|
|
528
|
-
def decorate_queryset(
|
|
529
|
-
cls,
|
|
530
|
-
feature_type: FeatureType,
|
|
531
|
-
queryset: models.QuerySet,
|
|
532
|
-
output_crs: CRS,
|
|
533
|
-
**params,
|
|
534
|
-
):
|
|
535
|
-
# Don't optimize queryset, it only retrieves one value
|
|
536
|
-
return queryset
|
|
537
|
-
|
|
538
|
-
def start_collection(self, sub_collection: SimpleFeatureCollection):
|
|
539
|
-
# Resolve which XsdNode is being rendered
|
|
540
|
-
match = sub_collection.feature_type.resolve_element(self.value_reference.xpath)
|
|
541
|
-
self.xsd_node = match.child
|
|
542
|
-
|
|
543
|
-
def render_wfs_member(
|
|
544
|
-
self, feature_type: FeatureType, instance: dict, extra_xmlns=""
|
|
545
|
-
) -> etree.Element:
|
|
546
|
-
"""Overwritten to handle attribute support."""
|
|
547
|
-
wsf_member = etree.Element(WFS_MEMBER, nsmap=self.nsmap)
|
|
548
|
-
if self.xsd_node.is_attribute:
|
|
549
|
-
# <wfs:member>{value}</wfs:member}
|
|
550
|
-
# When GetPropertyValue selects an attribute, it's value is rendered
|
|
551
|
-
# as plain-text (without spaces!) inside a <wfs:member> element.
|
|
552
|
-
# The format_value() is needed for @gml:id
|
|
553
|
-
body = self.xsd_node.format_raw_value(instance["member"])
|
|
554
|
-
wsf_member.text = body
|
|
555
|
-
else:
|
|
556
|
-
# <wfs:member><app:field>...</app:field></wfs:member>
|
|
557
|
-
# The call to GetPropertyValue selected an element.
|
|
558
|
-
# Render this single element tag inside the <wfs:member> parent.
|
|
559
|
-
wsf_member.append(self.render_feature(feature_type, instance))
|
|
560
|
-
return wsf_member
|
|
561
|
-
|
|
562
|
-
def render_feature(
|
|
563
|
-
self, feature_type: FeatureType, instance: dict, extra_xmlns=""
|
|
564
|
-
) -> etree.ElementTree | str:
|
|
565
|
-
"""Write the XML for a single object.
|
|
566
|
-
In this case, it's only a single XML tag.
|
|
567
|
-
"""
|
|
568
|
-
value = instance["member"]
|
|
569
|
-
if self.xsd_node.is_geometry:
|
|
570
|
-
gml_id = self.get_gml_id(feature_type, instance["pk"], seq=1)
|
|
571
|
-
return self.render_gml_field(
|
|
572
|
-
feature_type, self.xsd_node, instance=instance, gml_id=gml_id
|
|
573
|
-
)
|
|
574
|
-
else:
|
|
575
|
-
# The xsd_element is needed so write_xml_field() can render complex types.
|
|
576
|
-
if self.xsd_node.is_attribute:
|
|
577
|
-
# For GetFeatureById, allow returning raw values
|
|
578
|
-
value = self.xsd_node.format_raw_value(value) # needed for @gml:id
|
|
579
|
-
return str(value)
|
|
580
|
-
else:
|
|
581
|
-
return self.render_xml_field(feature_type, cast(XsdElement, self.xsd_node), value)
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
class DBGML32ValueRenderer(DBGML32Renderer, GML32ValueRenderer):
|
|
585
|
-
"""Faster GetPropertyValue renderer that uses the database to render GML 3.2"""
|
|
586
|
-
|
|
587
|
-
@classmethod
|
|
588
|
-
def decorate_queryset(cls, feature_type: FeatureType, queryset, output_crs, **params):
|
|
589
|
-
"""Update the queryset to let the database render the GML output."""
|
|
590
|
-
value_reference = params["valueReference"]
|
|
591
|
-
match = feature_type.resolve_element(value_reference.xpath)
|
|
592
|
-
if match.child.is_geometry:
|
|
593
|
-
# Add 'gml_member' to point to the pre-rendered GML version.
|
|
594
|
-
return queryset.values(
|
|
595
|
-
"pk", gml_member=AsGML(get_db_geometry_target(match, output_crs))
|
|
596
|
-
)
|
|
597
|
-
else:
|
|
598
|
-
return queryset
|
|
599
|
-
|
|
600
|
-
def render_wfs_member(self, feature_type: FeatureType, instance: dict, extra_xmlns="") -> str:
|
|
601
|
-
"""Write the XML for a single object."""
|
|
602
|
-
if "gml_member" in instance:
|
|
603
|
-
gml_id = self.get_gml_id(feature_type, instance["pk"], seq=1)
|
|
604
|
-
body = self.render_db_gml_field(
|
|
605
|
-
feature_type,
|
|
606
|
-
self.xsd_node,
|
|
607
|
-
instance["gml_member"],
|
|
608
|
-
gml_id=gml_id,
|
|
609
|
-
)
|
|
610
|
-
return f"<wfs:member>\n{body}</wfs:member>\n"
|
|
611
|
-
else:
|
|
612
|
-
return super().render_wfs_member(feature_type, instance, extra_xmlns=extra_xmlns)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|