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.
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/METADATA +14 -4
- django_gisserver-2.0.dist-info/RECORD +66 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/WHEEL +1 -1
- gisserver/__init__.py +1 -1
- gisserver/compat.py +23 -0
- gisserver/conf.py +7 -0
- gisserver/db.py +56 -47
- gisserver/exceptions.py +26 -2
- gisserver/extensions/__init__.py +4 -0
- gisserver/{parsers/fes20 → extensions}/functions.py +10 -4
- gisserver/extensions/queries.py +261 -0
- gisserver/features.py +220 -156
- gisserver/geometries.py +32 -37
- gisserver/management/__init__.py +0 -0
- gisserver/management/commands/__init__.py +0 -0
- gisserver/management/commands/loadgeojson.py +291 -0
- gisserver/operations/base.py +122 -308
- gisserver/operations/wfs20.py +423 -337
- gisserver/output/__init__.py +9 -48
- gisserver/output/base.py +178 -139
- gisserver/output/csv.py +65 -74
- gisserver/output/geojson.py +34 -35
- gisserver/output/gml32.py +254 -246
- gisserver/output/iters.py +207 -0
- gisserver/output/results.py +52 -26
- gisserver/output/stored.py +143 -0
- gisserver/output/utils.py +75 -170
- gisserver/output/xmlschema.py +85 -46
- gisserver/parsers/__init__.py +10 -10
- gisserver/parsers/ast.py +320 -0
- gisserver/parsers/fes20/__init__.py +13 -27
- gisserver/parsers/fes20/expressions.py +82 -38
- gisserver/parsers/fes20/filters.py +111 -43
- gisserver/parsers/fes20/identifiers.py +44 -26
- gisserver/parsers/fes20/lookups.py +144 -0
- gisserver/parsers/fes20/operators.py +331 -127
- gisserver/parsers/fes20/sorting.py +104 -33
- gisserver/parsers/gml/__init__.py +12 -11
- gisserver/parsers/gml/base.py +5 -2
- gisserver/parsers/gml/geometries.py +69 -35
- gisserver/parsers/ows/__init__.py +25 -0
- gisserver/parsers/ows/kvp.py +190 -0
- gisserver/parsers/ows/requests.py +158 -0
- gisserver/parsers/query.py +175 -0
- gisserver/parsers/values.py +26 -0
- gisserver/parsers/wfs20/__init__.py +37 -0
- gisserver/parsers/wfs20/adhoc.py +245 -0
- gisserver/parsers/wfs20/base.py +143 -0
- gisserver/parsers/wfs20/projection.py +103 -0
- gisserver/parsers/wfs20/requests.py +482 -0
- gisserver/parsers/wfs20/stored.py +192 -0
- gisserver/parsers/xml.py +249 -0
- gisserver/projection.py +357 -0
- gisserver/static/gisserver/index.css +12 -1
- gisserver/templates/gisserver/index.html +1 -1
- gisserver/templates/gisserver/service_description.html +2 -2
- gisserver/templates/gisserver/wfs/2.0.0/get_capabilities.xml +9 -9
- gisserver/templates/gisserver/wfs/feature_field.html +2 -2
- gisserver/templatetags/gisserver_tags.py +20 -0
- gisserver/types.py +322 -259
- gisserver/views.py +198 -56
- 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.0.dist-info}/LICENSE +0 -0
- {django_gisserver-1.5.0.dist-info → django_gisserver-2.0.dist-info}/top_level.txt +0 -0
gisserver/geometries.py
CHANGED
|
@@ -6,15 +6,16 @@ The bounding box can be calculated within Python, or read from a database result
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import logging
|
|
9
10
|
import re
|
|
10
11
|
from dataclasses import dataclass, field
|
|
11
12
|
from decimal import Decimal
|
|
12
|
-
from functools import lru_cache
|
|
13
|
+
from functools import cached_property, lru_cache
|
|
13
14
|
|
|
14
|
-
from django.contrib.gis.gdal import CoordTransform, SpatialReference
|
|
15
|
-
from django.contrib.gis.geos import GEOSGeometry
|
|
15
|
+
from django.contrib.gis.gdal import AxisOrder, CoordTransform, SpatialReference
|
|
16
|
+
from django.contrib.gis.geos import GEOSGeometry
|
|
16
17
|
|
|
17
|
-
from gisserver.exceptions import
|
|
18
|
+
from gisserver.exceptions import ExternalValueError
|
|
18
19
|
|
|
19
20
|
CRS_URN_REGEX = re.compile(
|
|
20
21
|
r"^urn:(?P<domain>[a-z]+)"
|
|
@@ -27,15 +28,27 @@ CRS_URN_REGEX = re.compile(
|
|
|
27
28
|
|
|
28
29
|
__all__ = [
|
|
29
30
|
"CRS",
|
|
31
|
+
"WEB_MERCATOR",
|
|
30
32
|
"WGS84",
|
|
31
33
|
"BoundingBox",
|
|
32
34
|
]
|
|
33
35
|
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
|
|
39
|
+
@lru_cache(maxsize=200) # Using lru-cache to avoid repeated GDAL c-object construction
|
|
40
|
+
def _get_spatial_reference(srs_input: str | int, srs_type="user", axis_order=None):
|
|
37
41
|
"""Construct an GDAL object reference"""
|
|
38
|
-
|
|
42
|
+
if axis_order is None:
|
|
43
|
+
# WFS 1.0 used x/y coordinates, WFS 2.0 uses the ordering from the CRS authority.
|
|
44
|
+
axis_order = AxisOrder.AUTHORITY
|
|
45
|
+
|
|
46
|
+
logger.debug(
|
|
47
|
+
"Constructed GDAL SpatialReference(%r, srs_type=%r, axis_order=%s)",
|
|
48
|
+
srs_input,
|
|
49
|
+
srs_type,
|
|
50
|
+
axis_order,
|
|
51
|
+
)
|
|
39
52
|
return SpatialReference(srs_input, srs_type=srs_type, axis_order=axis_order)
|
|
40
53
|
|
|
41
54
|
|
|
@@ -162,6 +175,7 @@ class CRS:
|
|
|
162
175
|
f"CRS URI [{urn}] should contain a numeric SRID value."
|
|
163
176
|
) from None
|
|
164
177
|
elif authority == "OGC":
|
|
178
|
+
# urn:ogc:def:crs:OGC::CRS84 has x/y ordering (longitude/latitude)
|
|
165
179
|
crsid = urn_match.group("id").upper()
|
|
166
180
|
if crsid != "CRS84":
|
|
167
181
|
raise ExternalValueError(f"OGC CRS URI from [{urn}] contains unknown id [{id}]")
|
|
@@ -216,7 +230,7 @@ class CRS:
|
|
|
216
230
|
"""Return a legacy string in the format "EPSG:<srid>"""
|
|
217
231
|
return f"EPSG:{self.srid:d}"
|
|
218
232
|
|
|
219
|
-
@
|
|
233
|
+
@cached_property
|
|
220
234
|
def urn(self):
|
|
221
235
|
"""Return The OGC URN corresponding to this CRS."""
|
|
222
236
|
return f"urn:{self.domain}:def:crs:{self.authority}:{self.version or ''}:{self.crsid}"
|
|
@@ -227,7 +241,7 @@ class CRS:
|
|
|
227
241
|
def __eq__(self, other):
|
|
228
242
|
if isinstance(other, CRS):
|
|
229
243
|
# CRS84 is NOT equivalent to EPSG:4326.
|
|
230
|
-
# EPSG:4326 specifies coordinates in lat/long order and
|
|
244
|
+
# EPSG:4326 specifies coordinates in lat/long order and CRS84 in long/lat order.
|
|
231
245
|
return self.authority == other.authority and self.srid == other.srid
|
|
232
246
|
else:
|
|
233
247
|
return NotImplemented
|
|
@@ -267,12 +281,19 @@ class CRS:
|
|
|
267
281
|
return geometry.transform(transform, clone=clone)
|
|
268
282
|
|
|
269
283
|
|
|
270
|
-
|
|
284
|
+
# Worldwide GPS, latitude/longitude (y/x). https://epsg.io/4326
|
|
285
|
+
WGS84 = CRS.from_string("urn:ogc:def:crs:EPSG::4326")
|
|
286
|
+
|
|
287
|
+
# GeoJSON default. This is like WGS84 but with longitude/latitude (x/y).
|
|
288
|
+
CRS84 = CRS.from_string("urn:ogc:def:crs:OGC::CRS84")
|
|
289
|
+
|
|
290
|
+
#: Spherical Mercator (Google Maps, Bing Maps, OpenStreetMap, ...), see https://epsg.io/3857
|
|
291
|
+
WEB_MERCATOR = CRS.from_string("urn:ogc:def:crs:EPSG::3857")
|
|
271
292
|
|
|
272
293
|
|
|
273
294
|
@dataclass
|
|
274
295
|
class BoundingBox:
|
|
275
|
-
"""A bounding box that describes the extent of a map layer"""
|
|
296
|
+
"""A bounding box (or "envelope") that describes the extent of a map layer"""
|
|
276
297
|
|
|
277
298
|
south: Decimal # longitude
|
|
278
299
|
west: Decimal # latitude
|
|
@@ -280,25 +301,6 @@ class BoundingBox:
|
|
|
280
301
|
east: Decimal # latitude
|
|
281
302
|
crs: CRS | None = None
|
|
282
303
|
|
|
283
|
-
@classmethod
|
|
284
|
-
def from_string(cls, bbox):
|
|
285
|
-
"""Parse the bounding box from an input string.
|
|
286
|
-
|
|
287
|
-
It can either be 4 coordinates, or 4 coordinates with a special reference system.
|
|
288
|
-
"""
|
|
289
|
-
bbox = bbox.split(",")
|
|
290
|
-
if not (4 <= len(bbox) <= 5):
|
|
291
|
-
raise ExternalParsingError(
|
|
292
|
-
f"Input does not contain bounding box, expected 4 or 5 values, not {bbox}."
|
|
293
|
-
)
|
|
294
|
-
return cls(
|
|
295
|
-
Decimal(bbox[0]),
|
|
296
|
-
Decimal(bbox[1]),
|
|
297
|
-
Decimal(bbox[2]),
|
|
298
|
-
Decimal(bbox[3]),
|
|
299
|
-
CRS.from_string(bbox[4]) if len(bbox) == 5 else None,
|
|
300
|
-
)
|
|
301
|
-
|
|
302
304
|
@classmethod
|
|
303
305
|
def from_geometry(cls, geometry: GEOSGeometry, crs: CRS | None = None):
|
|
304
306
|
"""Construct the bounding box for a geometry"""
|
|
@@ -349,10 +351,3 @@ class BoundingBox:
|
|
|
349
351
|
)
|
|
350
352
|
else:
|
|
351
353
|
return NotImplemented
|
|
352
|
-
|
|
353
|
-
def as_polygon(self) -> Polygon:
|
|
354
|
-
"""Convert the value into a GEOS polygon."""
|
|
355
|
-
polygon = Polygon.from_bbox((self.south, self.west, self.north, self.east))
|
|
356
|
-
if self.crs is not None:
|
|
357
|
-
polygon.srid = self.crs.srid
|
|
358
|
-
return polygon
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,291 @@
|
|
|
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 functools import reduce
|
|
8
|
+
from itertools import islice
|
|
9
|
+
from urllib.request import urlopen
|
|
10
|
+
|
|
11
|
+
from django.apps import apps
|
|
12
|
+
from django.contrib.gis.db.models import GeometryField
|
|
13
|
+
from django.contrib.gis.geos import GEOSGeometry
|
|
14
|
+
from django.core.exceptions import FieldDoesNotExist
|
|
15
|
+
from django.core.management import BaseCommand, CommandError, CommandParser
|
|
16
|
+
from django.db import DEFAULT_DB_ALIAS, connections, models, transaction
|
|
17
|
+
|
|
18
|
+
from gisserver.geometries import CRS, WGS84
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_model(value):
|
|
22
|
+
try:
|
|
23
|
+
return apps.get_model(value)
|
|
24
|
+
except LookupError as e:
|
|
25
|
+
raise ValueError(str(e)) from e
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_fields(value):
|
|
29
|
+
field_map = {}
|
|
30
|
+
for pair in value.split(","):
|
|
31
|
+
geojson_name, _, field_name = pair.strip().partition("=")
|
|
32
|
+
if not value:
|
|
33
|
+
raise ValueError("Expect property=field,property2=field2 format")
|
|
34
|
+
field_map[geojson_name] = field_name
|
|
35
|
+
return field_map
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Command(BaseCommand):
|
|
39
|
+
"""Quick command to import data in the WFS server."""
|
|
40
|
+
|
|
41
|
+
help = (
|
|
42
|
+
"Import GeoJSON data into a WFS feature collection. This can be done using:"
|
|
43
|
+
" manage.py loadgeojson --model=places.Province"
|
|
44
|
+
" -f name=name,prop2=field2 --geometry-field geometry provinces.json"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def add_arguments(self, parser: CommandParser):
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--database",
|
|
50
|
+
default=DEFAULT_DB_ALIAS,
|
|
51
|
+
choices=tuple(connections),
|
|
52
|
+
help=(
|
|
53
|
+
"Nominates a specific database to load fixtures into. Defaults to the "
|
|
54
|
+
'"default" database.'
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"-m",
|
|
59
|
+
"--model",
|
|
60
|
+
required=True,
|
|
61
|
+
metavar="MODEL",
|
|
62
|
+
type=_parse_model,
|
|
63
|
+
help="Django model, in the format: app_label.ModelName.",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"-g",
|
|
67
|
+
"--geometry-field",
|
|
68
|
+
required=False,
|
|
69
|
+
metavar="NAME",
|
|
70
|
+
help="Name of the model's geometry field, auto-detected if not specified.",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"-f",
|
|
74
|
+
"--field",
|
|
75
|
+
nargs="*",
|
|
76
|
+
dest="map_fields",
|
|
77
|
+
type=_parse_fields,
|
|
78
|
+
metavar="NAME=FIELD",
|
|
79
|
+
help=(
|
|
80
|
+
"Map GeoJSON properties to Django fields, in the format: property1=field1,property2=field2,... "
|
|
81
|
+
"By default, this autodetects common field names only. "
|
|
82
|
+
"Explicitly ignore fields by using -f property=,property2=field2."
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"geojson-file",
|
|
87
|
+
help="GeoJSON file or URL to import. For debugging, it's better to download the file first.",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def handle(self, *args, **options):
|
|
91
|
+
# Get arguments
|
|
92
|
+
self.using = options["database"]
|
|
93
|
+
self.connection = connections[self.using]
|
|
94
|
+
model: type[models.Model] = options["model"]
|
|
95
|
+
main_geometry_field = self._get_geometry_field(model, options["geometry_field"])
|
|
96
|
+
field_map = (
|
|
97
|
+
self._parse_field_map(model, options["map_fields"]) if options["map_fields"] else {}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Read the file
|
|
101
|
+
geojson = self._load_geojson(options["geojson-file"])
|
|
102
|
+
if not geojson["features"]:
|
|
103
|
+
self.stdout.write(self.style.NOTICE("Empty GeoJSON data"))
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
# See if properties match the Django field names, use those too.
|
|
107
|
+
# (unless these are mapped already via the command line args).
|
|
108
|
+
field_map.update(self._get_auto_field_map(model, geojson["features"][0], field_map))
|
|
109
|
+
|
|
110
|
+
# See if a CRS is declared
|
|
111
|
+
self.crs = self._read_crs(geojson)
|
|
112
|
+
|
|
113
|
+
# Import in chunks
|
|
114
|
+
num_imported = 0
|
|
115
|
+
id_field = model._meta.pk.name
|
|
116
|
+
with transaction.atomic(using=self.using):
|
|
117
|
+
features = iter(self._read_geojson(geojson, model, main_geometry_field, field_map))
|
|
118
|
+
while batch := list(islice(features, 100)):
|
|
119
|
+
if id_field in batch[0]:
|
|
120
|
+
# The ID field is provided, allow "on conflict update..."
|
|
121
|
+
unique_fields = [id_field]
|
|
122
|
+
update_fields = list(batch[0].keys())
|
|
123
|
+
update_fields.remove(id_field)
|
|
124
|
+
|
|
125
|
+
model.objects.using(self.using).bulk_create(
|
|
126
|
+
[model(**values) for values in batch],
|
|
127
|
+
update_conflicts=True,
|
|
128
|
+
unique_fields=unique_fields,
|
|
129
|
+
update_fields=update_fields,
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
model.objects.using(self.using).bulk_create(
|
|
133
|
+
[model(**values) for values in batch]
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
num_imported += len(batch)
|
|
137
|
+
|
|
138
|
+
self.stdout.write(f"Installed {num_imported} feature(s)")
|
|
139
|
+
|
|
140
|
+
def _get_geometry_field(self, model: type[models.Model], field_name: str | None):
|
|
141
|
+
"""Find the geometry field name, or validate it."""
|
|
142
|
+
if field_name:
|
|
143
|
+
# Field is given in CLI args, validate it.
|
|
144
|
+
try:
|
|
145
|
+
field = model._meta.get_field(field_name)
|
|
146
|
+
except FieldDoesNotExist as e:
|
|
147
|
+
raise CommandError(f"Invalid value for --geometry-field: {e}") from e
|
|
148
|
+
|
|
149
|
+
if not isinstance(field, GeometryField):
|
|
150
|
+
raise CommandError("Invalid value for --geometry-field: not a GeometryField")
|
|
151
|
+
else:
|
|
152
|
+
# Field is not given, auto-detect
|
|
153
|
+
try:
|
|
154
|
+
field_name = next(
|
|
155
|
+
f.name for f in model._meta.fields if isinstance(f, GeometryField)
|
|
156
|
+
)
|
|
157
|
+
except StopIteration:
|
|
158
|
+
raise CommandError(
|
|
159
|
+
f"Model {model._meta.label} does not have any GeometryField member."
|
|
160
|
+
) from None
|
|
161
|
+
|
|
162
|
+
return field_name
|
|
163
|
+
|
|
164
|
+
def _load_geojson(self, filename) -> dict:
|
|
165
|
+
"""Parse and validate the GeoJSON data into Python dict data."""
|
|
166
|
+
try:
|
|
167
|
+
if "://" in filename:
|
|
168
|
+
with urlopen(filename, timeout=60) as response: # noqa: S310
|
|
169
|
+
geojson = json.load(response)
|
|
170
|
+
else:
|
|
171
|
+
with open(filename) as fh:
|
|
172
|
+
geojson = json.load(fh)
|
|
173
|
+
except OSError as e: # FileNotFoundError or HTTP errors
|
|
174
|
+
raise CommandError(str(e)) from e
|
|
175
|
+
except (ValueError, TypeError) as e:
|
|
176
|
+
raise CommandError(f"Unable to parse GeoJSON: {e}") from e
|
|
177
|
+
|
|
178
|
+
if (
|
|
179
|
+
not isinstance(geojson, dict)
|
|
180
|
+
or geojson.get("type") != "FeatureCollection"
|
|
181
|
+
or "features" not in geojson
|
|
182
|
+
):
|
|
183
|
+
raise CommandError("Invalid GeoJSON data, expected FeatureCollection element.")
|
|
184
|
+
|
|
185
|
+
return geojson
|
|
186
|
+
|
|
187
|
+
def _read_crs(self, geojson: dict) -> CRS:
|
|
188
|
+
"""Find the CRS that should be used for all geometry data."""
|
|
189
|
+
crs = geojson.get("crs")
|
|
190
|
+
if not crs:
|
|
191
|
+
return WGS84
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
type = crs["type"]
|
|
195
|
+
if type == "name":
|
|
196
|
+
return CRS.from_string(crs["properties"]["name"])
|
|
197
|
+
else:
|
|
198
|
+
# type 'link' with subtype 'proj4/ogcwkt/esriwkt' are not handled here.
|
|
199
|
+
raise CommandError(f"CRS type {type} is not supported.")
|
|
200
|
+
except KeyError as e:
|
|
201
|
+
raise CommandError(f"CRS is invalid, missing '{e}' field") from e
|
|
202
|
+
|
|
203
|
+
def _parse_field_map(self, model, map_fields: list[dict]) -> dict:
|
|
204
|
+
"""Validate that the provided field mapping points to model fields."""
|
|
205
|
+
field_map: dict = reduce(operator.or_, map_fields)
|
|
206
|
+
for field_name in field_map.values():
|
|
207
|
+
try:
|
|
208
|
+
model._meta.get_field(field_name)
|
|
209
|
+
except FieldDoesNotExist as e:
|
|
210
|
+
raise CommandError(f"Field '{field_name}' does not exist in Django model.") from e
|
|
211
|
+
|
|
212
|
+
return field_map
|
|
213
|
+
|
|
214
|
+
def _get_auto_field_map(self, model, feature: dict, field_map: dict):
|
|
215
|
+
"""Autodetect which properties match model field names."""
|
|
216
|
+
auto_field_map = {}
|
|
217
|
+
for geojson_name in feature.get("properties", ()):
|
|
218
|
+
if geojson_name in field_map:
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
model._meta.get_field(geojson_name)
|
|
223
|
+
except FieldDoesNotExist:
|
|
224
|
+
msg = (
|
|
225
|
+
f"GeoJSON property '{geojson_name}' is not mapped nor exists in Django model, skipping."
|
|
226
|
+
if field_map
|
|
227
|
+
else f"GeoJSON property '{geojson_name}' does not exist in Django model, skipping."
|
|
228
|
+
)
|
|
229
|
+
self.stderr.write(self.style.WARNING(msg))
|
|
230
|
+
else:
|
|
231
|
+
self.stdout.write(
|
|
232
|
+
f"GeoJSON property '{geojson_name}' also exists as Django model field, using."
|
|
233
|
+
)
|
|
234
|
+
auto_field_map[geojson_name] = geojson_name
|
|
235
|
+
|
|
236
|
+
return auto_field_map
|
|
237
|
+
|
|
238
|
+
def _read_geojson(
|
|
239
|
+
self, geojson: dict, model: type[models.Model], main_geometry_field: str, field_map: dict
|
|
240
|
+
):
|
|
241
|
+
"""Convert the GeoJSON data to model field names."""
|
|
242
|
+
pk_field = model._meta.pk.name
|
|
243
|
+
|
|
244
|
+
for feature in geojson["features"]:
|
|
245
|
+
# Validate basic layout
|
|
246
|
+
try:
|
|
247
|
+
feature_type = feature["type"]
|
|
248
|
+
geometry_data = feature["geometry"]
|
|
249
|
+
except KeyError as e:
|
|
250
|
+
raise CommandError(f"Feature does not have required '{e}' element.") from e
|
|
251
|
+
if feature_type != "Feature":
|
|
252
|
+
raise CommandError(f"Expected 'Feature', not {feature_type}")
|
|
253
|
+
|
|
254
|
+
# Collect all fields
|
|
255
|
+
properties = feature.get("properties", {})
|
|
256
|
+
field_values = {
|
|
257
|
+
field_name: self._parse_value(
|
|
258
|
+
model._meta.get_field(field_name), properties.get(geojson_name)
|
|
259
|
+
)
|
|
260
|
+
for geojson_name, field_name in field_map.items()
|
|
261
|
+
if field_name
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# Store the geometry, note GEOS only parses stringified JSON.
|
|
265
|
+
geometry = GEOSGeometry(json.dumps(geometry_data), srid=self.crs.srid)
|
|
266
|
+
geometry.srid = self.crs.srid # override default
|
|
267
|
+
field_values[main_geometry_field] = geometry
|
|
268
|
+
|
|
269
|
+
# Try to decode the identifier if it's present
|
|
270
|
+
if pk_field not in field_values and (id_value := feature.get("id")):
|
|
271
|
+
try:
|
|
272
|
+
field_values[pk_field] = self._parse_id(model._meta.pk, id_value)
|
|
273
|
+
except (ValueError, TypeError):
|
|
274
|
+
self.stderr.write(
|
|
275
|
+
self.style.WARNING(
|
|
276
|
+
f"Feature id value '{id_value}' can't be stored in the primary key field. ignoring."
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Pass as Django model fields
|
|
281
|
+
yield field_values
|
|
282
|
+
|
|
283
|
+
def _parse_id(self, pk_field: models.Field, id_value):
|
|
284
|
+
# Allow TypeName.id format, see if it parses
|
|
285
|
+
return pk_field.get_db_prep_save(id_value.rpartition(".")[2], connection=self.connection)
|
|
286
|
+
|
|
287
|
+
def _parse_value(self, field: models.Field, value):
|
|
288
|
+
try:
|
|
289
|
+
return field.get_db_prep_save(value, connection=self.connection)
|
|
290
|
+
except (ValueError, TypeError) as e:
|
|
291
|
+
raise CommandError(f"Can't parse {value!r} in model field '{field.name}': {e}.") from e
|