django-gisserver 1.5.0__py3-none-any.whl → 2.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.5.0.dist-info → django_gisserver-2.1.dist-info}/METADATA +34 -8
- django_gisserver-2.1.dist-info/RECORD +68 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/compat.py +23 -0
- gisserver/conf.py +7 -0
- gisserver/crs.py +401 -0
- gisserver/db.py +126 -51
- gisserver/exceptions.py +132 -4
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +131 -31
- gisserver/extensions/queries.py +266 -0
- gisserver/features.py +253 -181
- gisserver/geometries.py +64 -311
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +311 -0
- gisserver/operations/base.py +130 -312
- gisserver/operations/wfs20.py +399 -375
- gisserver/output/__init__.py +14 -49
- gisserver/output/base.py +198 -144
- gisserver/output/csv.py +78 -75
- gisserver/output/geojson.py +37 -37
- gisserver/output/gml32.py +287 -259
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +73 -61
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +81 -169
- gisserver/output/xmlschema.py +85 -46
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +426 -0
- gisserver/parsers/fes20/__init__.py +89 -31
- gisserver/parsers/fes20/expressions.py +172 -58
- gisserver/parsers/fes20/filters.py +116 -45
- gisserver/parsers/fes20/identifiers.py +66 -28
- gisserver/parsers/fes20/lookups.py +146 -0
- gisserver/parsers/fes20/operators.py +417 -161
- gisserver/parsers/fes20/sorting.py +113 -34
- gisserver/parsers/gml/__init__.py +17 -25
- gisserver/parsers/gml/base.py +36 -15
- gisserver/parsers/gml/geometries.py +105 -44
- gisserver/parsers/ows/__init__.py +25 -0
- gisserver/parsers/ows/kvp.py +198 -0
- gisserver/parsers/ows/requests.py +160 -0
- gisserver/parsers/query.py +179 -0
- gisserver/parsers/values.py +87 -4
- gisserver/parsers/wfs20/__init__.py +39 -0
- gisserver/parsers/wfs20/adhoc.py +253 -0
- gisserver/parsers/wfs20/base.py +148 -0
- gisserver/parsers/wfs20/projection.py +103 -0
- gisserver/parsers/wfs20/requests.py +483 -0
- gisserver/parsers/wfs20/stored.py +193 -0
- gisserver/parsers/xml.py +261 -0
- gisserver/projection.py +367 -0
- gisserver/static/gisserver/index.css +20 -4
- gisserver/templates/gisserver/base.html +12 -0
- gisserver/templates/gisserver/index.html +9 -15
- gisserver/templates/gisserver/service_description.html +12 -6
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
- gisserver/templates/gisserver/wfs/feature_field.html +3 -3
- gisserver/templates/gisserver/wfs/feature_type.html +35 -13
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +445 -313
- gisserver/views.py +227 -62
- django_gisserver-1.5.0.dist-info/RECORD +0 -54
- gisserver/parsers/base.py +0 -149
- gisserver/parsers/fes20/query.py +0 -285
- gisserver/parsers/tags.py +0 -102
- gisserver/queries/__init__.py +0 -37
- gisserver/queries/adhoc.py +0 -185
- gisserver/queries/base.py +0 -186
- gisserver/queries/projection.py +0 -240
- gisserver/queries/stored.py +0 -206
- gisserver/templates/gisserver/wfs/2.0.0/describe_stored_queries.xml +0 -20
- gisserver/templates/gisserver/wfs/2.0.0/list_stored_queries.xml +0 -14
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info/licenses}/LICENSE +0 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Quick utility to import GeoJSON data in the Django project."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import operator
|
|
7
|
+
from argparse import ArgumentTypeError
|
|
8
|
+
from functools import reduce
|
|
9
|
+
from itertools import islice
|
|
10
|
+
from urllib.request import urlopen
|
|
11
|
+
|
|
12
|
+
from django.apps import apps
|
|
13
|
+
from django.contrib.gis.db.models import GeometryField
|
|
14
|
+
from django.contrib.gis.geos import GEOSGeometry
|
|
15
|
+
from django.core.exceptions import FieldDoesNotExist
|
|
16
|
+
from django.core.management import BaseCommand, CommandError, CommandParser
|
|
17
|
+
from django.db import DEFAULT_DB_ALIAS, connections, models, transaction
|
|
18
|
+
|
|
19
|
+
from gisserver.crs import CRS, CRS84
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_model(value):
|
|
23
|
+
try:
|
|
24
|
+
return apps.get_model(value)
|
|
25
|
+
except LookupError as e:
|
|
26
|
+
raise ArgumentTypeError(str(e)) from e
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _parse_fields(value):
|
|
30
|
+
field_map = {}
|
|
31
|
+
for pair in value.split(","):
|
|
32
|
+
geojson_name, _, field_name = pair.strip().partition("=")
|
|
33
|
+
if not field_name:
|
|
34
|
+
raise ArgumentTypeError("Expect property=field,property2=field2 format")
|
|
35
|
+
field_map[geojson_name] = field_name
|
|
36
|
+
return field_map
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Command(BaseCommand):
|
|
40
|
+
"""Quick command to import data in the WFS server."""
|
|
41
|
+
|
|
42
|
+
help = (
|
|
43
|
+
"Import GeoJSON data into a WFS feature collection. This can be done using:"
|
|
44
|
+
" manage.py loadgeojson --model=places.Province"
|
|
45
|
+
" -f name=name,prop2=field2 --geometry-field geometry provinces.json"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def add_arguments(self, parser: CommandParser):
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--database",
|
|
51
|
+
default=DEFAULT_DB_ALIAS,
|
|
52
|
+
choices=tuple(connections),
|
|
53
|
+
help=(
|
|
54
|
+
"Nominates a specific database to load fixtures into. Defaults to the "
|
|
55
|
+
'"default" database.'
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"-m",
|
|
60
|
+
"--model",
|
|
61
|
+
required=True,
|
|
62
|
+
metavar="MODEL",
|
|
63
|
+
type=_parse_model,
|
|
64
|
+
help="Django model, in the format: app_label.ModelName.",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"-g",
|
|
68
|
+
"--geometry-field",
|
|
69
|
+
required=False,
|
|
70
|
+
metavar="NAME",
|
|
71
|
+
help="Name of the model's geometry field, auto-detected if not specified.",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"-f",
|
|
75
|
+
"--field",
|
|
76
|
+
action="append",
|
|
77
|
+
dest="map_fields",
|
|
78
|
+
type=_parse_fields,
|
|
79
|
+
metavar="NAME=FIELD",
|
|
80
|
+
help=(
|
|
81
|
+
"Map GeoJSON properties to Django fields, in the format: property1=field1,property2=field2,... "
|
|
82
|
+
"By default, this autodetects common field names only. "
|
|
83
|
+
"Explicitly ignore fields by using -f property=,property2=field2."
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"geojson-file",
|
|
88
|
+
help="GeoJSON file or URL to import. For debugging, it's better to download the file first.",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def handle(self, *args, **options):
|
|
92
|
+
# Get arguments
|
|
93
|
+
self.using = options["database"]
|
|
94
|
+
self.connection = connections[self.using]
|
|
95
|
+
model: type[models.Model] = options["model"]
|
|
96
|
+
main_geometry_field = self._get_geometry_field(model, options["geometry_field"])
|
|
97
|
+
field_map = self._parse_field_map(model, options["map_fields"])
|
|
98
|
+
|
|
99
|
+
# Read the file
|
|
100
|
+
geojson = self._load_geojson(options["geojson-file"])
|
|
101
|
+
if not geojson["features"]:
|
|
102
|
+
self.stdout.write(self.style.NOTICE("Empty GeoJSON data"))
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# See if properties match the Django field names, use those too.
|
|
106
|
+
# (unless these are mapped already via the command line args).
|
|
107
|
+
field_map.update(self._get_auto_field_map(model, geojson["features"][0], field_map))
|
|
108
|
+
|
|
109
|
+
# See if a CRS is declared
|
|
110
|
+
self.crs = self._read_crs(geojson)
|
|
111
|
+
|
|
112
|
+
# Import in chunks
|
|
113
|
+
num_imported = 0
|
|
114
|
+
id_field = model._meta.pk.name
|
|
115
|
+
with transaction.atomic(using=self.using):
|
|
116
|
+
features = iter(self._read_geojson(geojson, model, main_geometry_field, field_map))
|
|
117
|
+
while batch := list(islice(features, 100)):
|
|
118
|
+
if id_field in batch[0]:
|
|
119
|
+
# The ID field is provided, allow "on conflict update..."
|
|
120
|
+
unique_fields = [id_field]
|
|
121
|
+
update_fields = list(batch[0].keys())
|
|
122
|
+
update_fields.remove(id_field)
|
|
123
|
+
|
|
124
|
+
model.objects.using(self.using).bulk_create(
|
|
125
|
+
[model(**values) for values in batch],
|
|
126
|
+
update_conflicts=True,
|
|
127
|
+
unique_fields=unique_fields,
|
|
128
|
+
update_fields=update_fields,
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
model.objects.using(self.using).bulk_create(
|
|
132
|
+
[model(**values) for values in batch]
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
num_imported += len(batch)
|
|
136
|
+
|
|
137
|
+
self.stdout.write(f"Installed {num_imported} feature(s)")
|
|
138
|
+
|
|
139
|
+
def _get_geometry_field(self, model: type[models.Model], field_name: str | None):
|
|
140
|
+
"""Find the geometry field name, or validate it."""
|
|
141
|
+
if field_name:
|
|
142
|
+
# Field is given in CLI args, validate it.
|
|
143
|
+
try:
|
|
144
|
+
field = model._meta.get_field(field_name)
|
|
145
|
+
except FieldDoesNotExist as e:
|
|
146
|
+
raise CommandError(f"Invalid value for --geometry-field: {e}") from e
|
|
147
|
+
|
|
148
|
+
if not isinstance(field, GeometryField):
|
|
149
|
+
raise CommandError("Invalid value for --geometry-field: not a GeometryField")
|
|
150
|
+
else:
|
|
151
|
+
# Field is not given, auto-detect
|
|
152
|
+
try:
|
|
153
|
+
field_name = next(
|
|
154
|
+
f.name for f in model._meta.fields if isinstance(f, GeometryField)
|
|
155
|
+
)
|
|
156
|
+
except StopIteration:
|
|
157
|
+
raise CommandError(
|
|
158
|
+
f"Model {model._meta.label} does not have any GeometryField member."
|
|
159
|
+
) from None
|
|
160
|
+
|
|
161
|
+
return field_name
|
|
162
|
+
|
|
163
|
+
def _load_geojson(self, filename) -> dict:
|
|
164
|
+
"""Parse and validate the GeoJSON data into Python dict data."""
|
|
165
|
+
try:
|
|
166
|
+
if "://" in filename:
|
|
167
|
+
with urlopen(filename, timeout=60) as response: # noqa: S310
|
|
168
|
+
geojson = json.load(response)
|
|
169
|
+
else:
|
|
170
|
+
with open(filename) as fh:
|
|
171
|
+
geojson = json.load(fh)
|
|
172
|
+
except OSError as e: # FileNotFoundError or HTTP errors
|
|
173
|
+
raise CommandError(str(e)) from e
|
|
174
|
+
except (ValueError, TypeError) as e:
|
|
175
|
+
raise CommandError(f"Unable to parse GeoJSON: {e}") from e
|
|
176
|
+
|
|
177
|
+
if (
|
|
178
|
+
not isinstance(geojson, dict)
|
|
179
|
+
or geojson.get("type") != "FeatureCollection"
|
|
180
|
+
or "features" not in geojson
|
|
181
|
+
):
|
|
182
|
+
raise CommandError("Invalid GeoJSON data, expected FeatureCollection element.")
|
|
183
|
+
|
|
184
|
+
return geojson
|
|
185
|
+
|
|
186
|
+
def _read_crs(self, geojson: dict) -> CRS:
|
|
187
|
+
"""Find the CRS that should be used for all geometry data."""
|
|
188
|
+
crs = geojson.get("crs")
|
|
189
|
+
if not crs:
|
|
190
|
+
return CRS84 # default for GeoJSON
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
type = crs["type"]
|
|
194
|
+
if type == "name":
|
|
195
|
+
return CRS.from_string(crs["properties"]["name"])
|
|
196
|
+
else:
|
|
197
|
+
# type 'link' with subtype 'proj4/ogcwkt/esriwkt' are not handled here.
|
|
198
|
+
raise CommandError(f"CRS type {type} is not supported.")
|
|
199
|
+
except KeyError as e:
|
|
200
|
+
raise CommandError(f"CRS is invalid, missing '{e}' field") from e
|
|
201
|
+
|
|
202
|
+
def _parse_field_map(self, model, map_fields: list[dict]) -> dict:
|
|
203
|
+
"""Validate that the provided field mapping points to model fields."""
|
|
204
|
+
if not map_fields:
|
|
205
|
+
return {}
|
|
206
|
+
|
|
207
|
+
field_map: dict = reduce(operator.or_, map_fields)
|
|
208
|
+
for field_name in field_map.values():
|
|
209
|
+
try:
|
|
210
|
+
model._meta.get_field(field_name)
|
|
211
|
+
except FieldDoesNotExist as e:
|
|
212
|
+
raise CommandError(f"Field '{field_name}' does not exist in Django model.") from e
|
|
213
|
+
|
|
214
|
+
return field_map
|
|
215
|
+
|
|
216
|
+
def _get_auto_field_map(self, model, feature: dict, field_map: dict):
|
|
217
|
+
"""Autodetect which properties match model field names."""
|
|
218
|
+
auto_field_map = {}
|
|
219
|
+
for geojson_name in feature.get("properties", ()):
|
|
220
|
+
if geojson_name in field_map:
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
model._meta.get_field(geojson_name)
|
|
225
|
+
except FieldDoesNotExist:
|
|
226
|
+
msg = (
|
|
227
|
+
f"GeoJSON property '{geojson_name}' is not mapped nor exists in Django model, skipping."
|
|
228
|
+
if field_map
|
|
229
|
+
else f"GeoJSON property '{geojson_name}' does not exist in Django model, skipping."
|
|
230
|
+
)
|
|
231
|
+
self.stderr.write(self.style.WARNING(msg))
|
|
232
|
+
else:
|
|
233
|
+
self.stdout.write(
|
|
234
|
+
f"GeoJSON property '{geojson_name}' also exists as Django model field, using."
|
|
235
|
+
)
|
|
236
|
+
auto_field_map[geojson_name] = geojson_name
|
|
237
|
+
|
|
238
|
+
return auto_field_map
|
|
239
|
+
|
|
240
|
+
def _read_geojson(
|
|
241
|
+
self, geojson: dict, model: type[models.Model], main_geometry_field: str, field_map: dict
|
|
242
|
+
):
|
|
243
|
+
"""Convert the GeoJSON data to model field names."""
|
|
244
|
+
pk_field = model._meta.pk.name
|
|
245
|
+
|
|
246
|
+
for feature in geojson["features"]:
|
|
247
|
+
# Validate basic layout
|
|
248
|
+
try:
|
|
249
|
+
feature_type = feature["type"]
|
|
250
|
+
geometry_data = feature["geometry"]
|
|
251
|
+
except KeyError as e:
|
|
252
|
+
raise CommandError(f"Feature does not have required '{e}' element.") from e
|
|
253
|
+
if feature_type != "Feature":
|
|
254
|
+
raise CommandError(f"Expected 'Feature', not {feature_type}")
|
|
255
|
+
|
|
256
|
+
# Collect all fields
|
|
257
|
+
properties = feature.get("properties", {})
|
|
258
|
+
field_values = {
|
|
259
|
+
field_name: self._parse_value(
|
|
260
|
+
model._meta.get_field(field_name), properties.get(geojson_name)
|
|
261
|
+
)
|
|
262
|
+
for geojson_name, field_name in field_map.items()
|
|
263
|
+
if field_name
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# Store the geometry, note GEOS only parses stringified JSON.
|
|
267
|
+
if geometry_data is None:
|
|
268
|
+
geometry = None
|
|
269
|
+
else:
|
|
270
|
+
try:
|
|
271
|
+
# NOTE: this looses the axis ordering information,
|
|
272
|
+
# and assumes x/y as the standard prescribes.
|
|
273
|
+
geometry = GEOSGeometry(json.dumps(geometry_data), srid=self.crs.srid)
|
|
274
|
+
except ValueError as e:
|
|
275
|
+
raise CommandError(
|
|
276
|
+
f"Unable to parse geometry data: {geometry_data!r}: {e}"
|
|
277
|
+
) from e
|
|
278
|
+
geometry.srid = self.crs.srid # override default
|
|
279
|
+
|
|
280
|
+
field_values[main_geometry_field] = geometry
|
|
281
|
+
|
|
282
|
+
# Try to decode the identifier if it's present
|
|
283
|
+
if (
|
|
284
|
+
pk_field not in field_values
|
|
285
|
+
and (raw_value := feature.get("id")) is not None
|
|
286
|
+
and (id_value := self._parse_id(model._meta.pk, raw_value)) is not None
|
|
287
|
+
):
|
|
288
|
+
field_values[pk_field] = id_value
|
|
289
|
+
|
|
290
|
+
# Pass as Django model fields
|
|
291
|
+
yield field_values
|
|
292
|
+
|
|
293
|
+
def _parse_id(self, pk_field: models.Field, id_value):
|
|
294
|
+
# Allow TypeName.id format, see if it parses
|
|
295
|
+
try:
|
|
296
|
+
return pk_field.get_db_prep_save(
|
|
297
|
+
id_value.rpartition(".")[2], connection=self.connection
|
|
298
|
+
)
|
|
299
|
+
except (ValueError, TypeError):
|
|
300
|
+
self.stderr.write(
|
|
301
|
+
self.style.WARNING(
|
|
302
|
+
f"Feature id value '{id_value}' can't be stored in the primary key field. ignoring."
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
def _parse_value(self, field: models.Field, value):
|
|
308
|
+
try:
|
|
309
|
+
return field.get_db_prep_save(value, connection=self.connection)
|
|
310
|
+
except (ValueError, TypeError) as e:
|
|
311
|
+
raise CommandError(f"Can't parse {value!r} in model field '{field.name}': {e}.") from e
|