summaryrefslogtreecommitdiff
path: root/lib/python2.7/site-packages/django/contrib/gis/utils/layermapping.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/python2.7/site-packages/django/contrib/gis/utils/layermapping.py')
-rw-r--r--lib/python2.7/site-packages/django/contrib/gis/utils/layermapping.py596
1 files changed, 596 insertions, 0 deletions
diff --git a/lib/python2.7/site-packages/django/contrib/gis/utils/layermapping.py b/lib/python2.7/site-packages/django/contrib/gis/utils/layermapping.py
new file mode 100644
index 0000000..a502aa2
--- /dev/null
+++ b/lib/python2.7/site-packages/django/contrib/gis/utils/layermapping.py
@@ -0,0 +1,596 @@
+# LayerMapping -- A Django Model/OGR Layer Mapping Utility
+"""
+ The LayerMapping class provides a way to map the contents of OGR
+ vector files (e.g. SHP files) to Geographic-enabled Django models.
+
+ For more information, please consult the GeoDjango documentation:
+ http://geodjango.org/docs/layermapping.html
+"""
+import sys
+from decimal import Decimal
+from django.core.exceptions import ObjectDoesNotExist
+from django.db import connections, router
+from django.contrib.gis.db.models import GeometryField
+from django.contrib.gis.gdal import (CoordTransform, DataSource,
+ OGRException, OGRGeometry, OGRGeomType, SpatialReference)
+from django.contrib.gis.gdal.field import (
+ OFTDate, OFTDateTime, OFTInteger, OFTReal, OFTString, OFTTime)
+from django.db import models, transaction
+from django.utils import six
+from django.utils.encoding import force_text
+
+
+# LayerMapping exceptions.
+class LayerMapError(Exception): pass
+class InvalidString(LayerMapError): pass
+class InvalidDecimal(LayerMapError): pass
+class InvalidInteger(LayerMapError): pass
+class MissingForeignKey(LayerMapError): pass
+
+class LayerMapping(object):
+ "A class that maps OGR Layers to GeoDjango Models."
+
+ # Acceptable 'base' types for a multi-geometry type.
+ MULTI_TYPES = {1 : OGRGeomType('MultiPoint'),
+ 2 : OGRGeomType('MultiLineString'),
+ 3 : OGRGeomType('MultiPolygon'),
+ OGRGeomType('Point25D').num : OGRGeomType('MultiPoint25D'),
+ OGRGeomType('LineString25D').num : OGRGeomType('MultiLineString25D'),
+ OGRGeomType('Polygon25D').num : OGRGeomType('MultiPolygon25D'),
+ }
+
+ # Acceptable Django field types and corresponding acceptable OGR
+ # counterparts.
+ FIELD_TYPES = {
+ models.AutoField : OFTInteger,
+ models.IntegerField : (OFTInteger, OFTReal, OFTString),
+ models.FloatField : (OFTInteger, OFTReal),
+ models.DateField : OFTDate,
+ models.DateTimeField : OFTDateTime,
+ models.EmailField : OFTString,
+ models.TimeField : OFTTime,
+ models.DecimalField : (OFTInteger, OFTReal),
+ models.CharField : OFTString,
+ models.SlugField : OFTString,
+ models.TextField : OFTString,
+ models.URLField : OFTString,
+ models.BigIntegerField : (OFTInteger, OFTReal, OFTString),
+ models.SmallIntegerField : (OFTInteger, OFTReal, OFTString),
+ models.PositiveSmallIntegerField : (OFTInteger, OFTReal, OFTString),
+ }
+
+ def __init__(self, model, data, mapping, layer=0,
+ source_srs=None, encoding='utf-8',
+ transaction_mode='commit_on_success',
+ transform=True, unique=None, using=None):
+ """
+ A LayerMapping object is initialized using the given Model (not an instance),
+ a DataSource (or string path to an OGR-supported data file), and a mapping
+ dictionary. See the module level docstring for more details and keyword
+ argument usage.
+ """
+ # Getting the DataSource and the associated Layer.
+ if isinstance(data, six.string_types):
+ self.ds = DataSource(data, encoding=encoding)
+ else:
+ self.ds = data
+ self.layer = self.ds[layer]
+
+ self.using = using if using is not None else router.db_for_write(model)
+ self.spatial_backend = connections[self.using].ops
+
+ # Setting the mapping & model attributes.
+ self.mapping = mapping
+ self.model = model
+
+ # Checking the layer -- intitialization of the object will fail if
+ # things don't check out before hand.
+ self.check_layer()
+
+ # Getting the geometry column associated with the model (an
+ # exception will be raised if there is no geometry column).
+ if self.spatial_backend.mysql:
+ transform = False
+ else:
+ self.geo_field = self.geometry_field()
+
+ # Checking the source spatial reference system, and getting
+ # the coordinate transformation object (unless the `transform`
+ # keyword is set to False)
+ if transform:
+ self.source_srs = self.check_srs(source_srs)
+ self.transform = self.coord_transform()
+ else:
+ self.transform = transform
+
+ # Setting the encoding for OFTString fields, if specified.
+ if encoding:
+ # Making sure the encoding exists, if not a LookupError
+ # exception will be thrown.
+ from codecs import lookup
+ lookup(encoding)
+ self.encoding = encoding
+ else:
+ self.encoding = None
+
+ if unique:
+ self.check_unique(unique)
+ transaction_mode = 'autocommit' # Has to be set to autocommit.
+ self.unique = unique
+ else:
+ self.unique = None
+
+ # Setting the transaction decorator with the function in the
+ # transaction modes dictionary.
+ self.transaction_mode = transaction_mode
+ if transaction_mode == 'autocommit':
+ self.transaction_decorator = None
+ elif transaction_mode == 'commit_on_success':
+ self.transaction_decorator = transaction.atomic
+ else:
+ raise LayerMapError('Unrecognized transaction mode: %s' % transaction_mode)
+
+ #### Checking routines used during initialization ####
+ def check_fid_range(self, fid_range):
+ "This checks the `fid_range` keyword."
+ if fid_range:
+ if isinstance(fid_range, (tuple, list)):
+ return slice(*fid_range)
+ elif isinstance(fid_range, slice):
+ return fid_range
+ else:
+ raise TypeError
+ else:
+ return None
+
+ def check_layer(self):
+ """
+ This checks the Layer metadata, and ensures that it is compatible
+ with the mapping information and model. Unlike previous revisions,
+ there is no need to increment through each feature in the Layer.
+ """
+ # The geometry field of the model is set here.
+ # TODO: Support more than one geometry field / model. However, this
+ # depends on the GDAL Driver in use.
+ self.geom_field = False
+ self.fields = {}
+
+ # Getting lists of the field names and the field types available in
+ # the OGR Layer.
+ ogr_fields = self.layer.fields
+ ogr_field_types = self.layer.field_types
+
+ # Function for determining if the OGR mapping field is in the Layer.
+ def check_ogr_fld(ogr_map_fld):
+ try:
+ idx = ogr_fields.index(ogr_map_fld)
+ except ValueError:
+ raise LayerMapError('Given mapping OGR field "%s" not found in OGR Layer.' % ogr_map_fld)
+ return idx
+
+ # No need to increment through each feature in the model, simply check
+ # the Layer metadata against what was given in the mapping dictionary.
+ for field_name, ogr_name in self.mapping.items():
+ # Ensuring that a corresponding field exists in the model
+ # for the given field name in the mapping.
+ try:
+ model_field = self.model._meta.get_field(field_name)
+ except models.fields.FieldDoesNotExist:
+ raise LayerMapError('Given mapping field "%s" not in given Model fields.' % field_name)
+
+ # Getting the string name for the Django field class (e.g., 'PointField').
+ fld_name = model_field.__class__.__name__
+
+ if isinstance(model_field, GeometryField):
+ if self.geom_field:
+ raise LayerMapError('LayerMapping does not support more than one GeometryField per model.')
+
+ # Getting the coordinate dimension of the geometry field.
+ coord_dim = model_field.dim
+
+ try:
+ if coord_dim == 3:
+ gtype = OGRGeomType(ogr_name + '25D')
+ else:
+ gtype = OGRGeomType(ogr_name)
+ except OGRException:
+ raise LayerMapError('Invalid mapping for GeometryField "%s".' % field_name)
+
+ # Making sure that the OGR Layer's Geometry is compatible.
+ ltype = self.layer.geom_type
+ if not (ltype.name.startswith(gtype.name) or self.make_multi(ltype, model_field)):
+ raise LayerMapError('Invalid mapping geometry; model has %s%s, '
+ 'layer geometry type is %s.' %
+ (fld_name, '(dim=3)' if coord_dim == 3 else '', ltype))
+
+ # Setting the `geom_field` attribute w/the name of the model field
+ # that is a Geometry. Also setting the coordinate dimension
+ # attribute.
+ self.geom_field = field_name
+ self.coord_dim = coord_dim
+ fields_val = model_field
+ elif isinstance(model_field, models.ForeignKey):
+ if isinstance(ogr_name, dict):
+ # Is every given related model mapping field in the Layer?
+ rel_model = model_field.rel.to
+ for rel_name, ogr_field in ogr_name.items():
+ idx = check_ogr_fld(ogr_field)
+ try:
+ rel_field = rel_model._meta.get_field(rel_name)
+ except models.fields.FieldDoesNotExist:
+ raise LayerMapError('ForeignKey mapping field "%s" not in %s fields.' %
+ (rel_name, rel_model.__class__.__name__))
+ fields_val = rel_model
+ else:
+ raise TypeError('ForeignKey mapping must be of dictionary type.')
+ else:
+ # Is the model field type supported by LayerMapping?
+ if not model_field.__class__ in self.FIELD_TYPES:
+ raise LayerMapError('Django field type "%s" has no OGR mapping (yet).' % fld_name)
+
+ # Is the OGR field in the Layer?
+ idx = check_ogr_fld(ogr_name)
+ ogr_field = ogr_field_types[idx]
+
+ # Can the OGR field type be mapped to the Django field type?
+ if not issubclass(ogr_field, self.FIELD_TYPES[model_field.__class__]):
+ raise LayerMapError('OGR field "%s" (of type %s) cannot be mapped to Django %s.' %
+ (ogr_field, ogr_field.__name__, fld_name))
+ fields_val = model_field
+
+ self.fields[field_name] = fields_val
+
+ def check_srs(self, source_srs):
+ "Checks the compatibility of the given spatial reference object."
+
+ if isinstance(source_srs, SpatialReference):
+ sr = source_srs
+ elif isinstance(source_srs, self.spatial_backend.spatial_ref_sys()):
+ sr = source_srs.srs
+ elif isinstance(source_srs, (int, six.string_types)):
+ sr = SpatialReference(source_srs)
+ else:
+ # Otherwise just pulling the SpatialReference from the layer
+ sr = self.layer.srs
+
+ if not sr:
+ raise LayerMapError('No source reference system defined.')
+ else:
+ return sr
+
+ def check_unique(self, unique):
+ "Checks the `unique` keyword parameter -- may be a sequence or string."
+ if isinstance(unique, (list, tuple)):
+ # List of fields to determine uniqueness with
+ for attr in unique:
+ if not attr in self.mapping: raise ValueError
+ elif isinstance(unique, six.string_types):
+ # Only a single field passed in.
+ if unique not in self.mapping: raise ValueError
+ else:
+ raise TypeError('Unique keyword argument must be set with a tuple, list, or string.')
+
+ #### Keyword argument retrieval routines ####
+ def feature_kwargs(self, feat):
+ """
+ Given an OGR Feature, this will return a dictionary of keyword arguments
+ for constructing the mapped model.
+ """
+ # The keyword arguments for model construction.
+ kwargs = {}
+
+ # Incrementing through each model field and OGR field in the
+ # dictionary mapping.
+ for field_name, ogr_name in self.mapping.items():
+ model_field = self.fields[field_name]
+
+ if isinstance(model_field, GeometryField):
+ # Verify OGR geometry.
+ try:
+ val = self.verify_geom(feat.geom, model_field)
+ except OGRException:
+ raise LayerMapError('Could not retrieve geometry from feature.')
+ elif isinstance(model_field, models.base.ModelBase):
+ # The related _model_, not a field was passed in -- indicating
+ # another mapping for the related Model.
+ val = self.verify_fk(feat, model_field, ogr_name)
+ else:
+ # Otherwise, verify OGR Field type.
+ val = self.verify_ogr_field(feat[ogr_name], model_field)
+
+ # Setting the keyword arguments for the field name with the
+ # value obtained above.
+ kwargs[field_name] = val
+
+ return kwargs
+
+ def unique_kwargs(self, kwargs):
+ """
+ Given the feature keyword arguments (from `feature_kwargs`) this routine
+ will construct and return the uniqueness keyword arguments -- a subset
+ of the feature kwargs.
+ """
+ if isinstance(self.unique, six.string_types):
+ return {self.unique : kwargs[self.unique]}
+ else:
+ return dict((fld, kwargs[fld]) for fld in self.unique)
+
+ #### Verification routines used in constructing model keyword arguments. ####
+ def verify_ogr_field(self, ogr_field, model_field):
+ """
+ Verifies if the OGR Field contents are acceptable to the Django
+ model field. If they are, the verified value is returned,
+ otherwise the proper exception is raised.
+ """
+ if (isinstance(ogr_field, OFTString) and
+ isinstance(model_field, (models.CharField, models.TextField))):
+ if self.encoding:
+ # The encoding for OGR data sources may be specified here
+ # (e.g., 'cp437' for Census Bureau boundary files).
+ val = force_text(ogr_field.value, self.encoding)
+ else:
+ val = ogr_field.value
+ if model_field.max_length and len(val) > model_field.max_length:
+ raise InvalidString('%s model field maximum string length is %s, given %s characters.' %
+ (model_field.name, model_field.max_length, len(val)))
+ elif isinstance(ogr_field, OFTReal) and isinstance(model_field, models.DecimalField):
+ try:
+ # Creating an instance of the Decimal value to use.
+ d = Decimal(str(ogr_field.value))
+ except:
+ raise InvalidDecimal('Could not construct decimal from: %s' % ogr_field.value)
+
+ # Getting the decimal value as a tuple.
+ dtup = d.as_tuple()
+ digits = dtup[1]
+ d_idx = dtup[2] # index where the decimal is
+
+ # Maximum amount of precision, or digits to the left of the decimal.
+ max_prec = model_field.max_digits - model_field.decimal_places
+
+ # Getting the digits to the left of the decimal place for the
+ # given decimal.
+ if d_idx < 0:
+ n_prec = len(digits[:d_idx])
+ else:
+ n_prec = len(digits) + d_idx
+
+ # If we have more than the maximum digits allowed, then throw an
+ # InvalidDecimal exception.
+ if n_prec > max_prec:
+ raise InvalidDecimal('A DecimalField with max_digits %d, decimal_places %d must round to an absolute value less than 10^%d.' %
+ (model_field.max_digits, model_field.decimal_places, max_prec))
+ val = d
+ elif isinstance(ogr_field, (OFTReal, OFTString)) and isinstance(model_field, models.IntegerField):
+ # Attempt to convert any OFTReal and OFTString value to an OFTInteger.
+ try:
+ val = int(ogr_field.value)
+ except:
+ raise InvalidInteger('Could not construct integer from: %s' % ogr_field.value)
+ else:
+ val = ogr_field.value
+ return val
+
+ def verify_fk(self, feat, rel_model, rel_mapping):
+ """
+ Given an OGR Feature, the related model and its dictionary mapping,
+ this routine will retrieve the related model for the ForeignKey
+ mapping.
+ """
+ # TODO: It is expensive to retrieve a model for every record --
+ # explore if an efficient mechanism exists for caching related
+ # ForeignKey models.
+
+ # Constructing and verifying the related model keyword arguments.
+ fk_kwargs = {}
+ for field_name, ogr_name in rel_mapping.items():
+ fk_kwargs[field_name] = self.verify_ogr_field(feat[ogr_name], rel_model._meta.get_field(field_name))
+
+ # Attempting to retrieve and return the related model.
+ try:
+ return rel_model.objects.using(self.using).get(**fk_kwargs)
+ except ObjectDoesNotExist:
+ raise MissingForeignKey('No ForeignKey %s model found with keyword arguments: %s' % (rel_model.__name__, fk_kwargs))
+
+ def verify_geom(self, geom, model_field):
+ """
+ Verifies the geometry -- will construct and return a GeometryCollection
+ if necessary (for example if the model field is MultiPolygonField while
+ the mapped shapefile only contains Polygons).
+ """
+ # Downgrade a 3D geom to a 2D one, if necessary.
+ if self.coord_dim != geom.coord_dim:
+ geom.coord_dim = self.coord_dim
+
+ if self.make_multi(geom.geom_type, model_field):
+ # Constructing a multi-geometry type to contain the single geometry
+ multi_type = self.MULTI_TYPES[geom.geom_type.num]
+ g = OGRGeometry(multi_type)
+ g.add(geom)
+ else:
+ g = geom
+
+ # Transforming the geometry with our Coordinate Transformation object,
+ # but only if the class variable `transform` is set w/a CoordTransform
+ # object.
+ if self.transform: g.transform(self.transform)
+
+ # Returning the WKT of the geometry.
+ return g.wkt
+
+ #### Other model methods ####
+ def coord_transform(self):
+ "Returns the coordinate transformation object."
+ SpatialRefSys = self.spatial_backend.spatial_ref_sys()
+ try:
+ # Getting the target spatial reference system
+ target_srs = SpatialRefSys.objects.using(self.using).get(srid=self.geo_field.srid).srs
+
+ # Creating the CoordTransform object
+ return CoordTransform(self.source_srs, target_srs)
+ except Exception as msg:
+ new_msg = 'Could not translate between the data source and model geometry: %s' % msg
+ six.reraise(LayerMapError, LayerMapError(new_msg), sys.exc_info()[2])
+
+ def geometry_field(self):
+ "Returns the GeometryField instance associated with the geographic column."
+ # Use the `get_field_by_name` on the model's options so that we
+ # get the correct field instance if there's model inheritance.
+ opts = self.model._meta
+ fld, model, direct, m2m = opts.get_field_by_name(self.geom_field)
+ return fld
+
+ def make_multi(self, geom_type, model_field):
+ """
+ Given the OGRGeomType for a geometry and its associated GeometryField,
+ determine whether the geometry should be turned into a GeometryCollection.
+ """
+ return (geom_type.num in self.MULTI_TYPES and
+ model_field.__class__.__name__ == 'Multi%s' % geom_type.django)
+
+ def save(self, verbose=False, fid_range=False, step=False,
+ progress=False, silent=False, stream=sys.stdout, strict=False):
+ """
+ Saves the contents from the OGR DataSource Layer into the database
+ according to the mapping dictionary given at initialization.
+
+ Keyword Parameters:
+ verbose:
+ If set, information will be printed subsequent to each model save
+ executed on the database.
+
+ fid_range:
+ May be set with a slice or tuple of (begin, end) feature ID's to map
+ from the data source. In other words, this keyword enables the user
+ to selectively import a subset range of features in the geographic
+ data source.
+
+ step:
+ If set with an integer, transactions will occur at every step
+ interval. For example, if step=1000, a commit would occur after
+ the 1,000th feature, the 2,000th feature etc.
+
+ progress:
+ When this keyword is set, status information will be printed giving
+ the number of features processed and sucessfully saved. By default,
+ progress information will pe printed every 1000 features processed,
+ however, this default may be overridden by setting this keyword with an
+ integer for the desired interval.
+
+ stream:
+ Status information will be written to this file handle. Defaults to
+ using `sys.stdout`, but any object with a `write` method is supported.
+
+ silent:
+ By default, non-fatal error notifications are printed to stdout, but
+ this keyword may be set to disable these notifications.
+
+ strict:
+ Execution of the model mapping will cease upon the first error
+ encountered. The default behavior is to attempt to continue.
+ """
+ # Getting the default Feature ID range.
+ default_range = self.check_fid_range(fid_range)
+
+ # Setting the progress interval, if requested.
+ if progress:
+ if progress is True or not isinstance(progress, int):
+ progress_interval = 1000
+ else:
+ progress_interval = progress
+
+ def _save(feat_range=default_range, num_feat=0, num_saved=0):
+ if feat_range:
+ layer_iter = self.layer[feat_range]
+ else:
+ layer_iter = self.layer
+
+ for feat in layer_iter:
+ num_feat += 1
+ # Getting the keyword arguments
+ try:
+ kwargs = self.feature_kwargs(feat)
+ except LayerMapError as msg:
+ # Something borked the validation
+ if strict: raise
+ elif not silent:
+ stream.write('Ignoring Feature ID %s because: %s\n' % (feat.fid, msg))
+ else:
+ # Constructing the model using the keyword args
+ is_update = False
+ if self.unique:
+ # If we want unique models on a particular field, handle the
+ # geometry appropriately.
+ try:
+ # Getting the keyword arguments and retrieving
+ # the unique model.
+ u_kwargs = self.unique_kwargs(kwargs)
+ m = self.model.objects.using(self.using).get(**u_kwargs)
+ is_update = True
+
+ # Getting the geometry (in OGR form), creating
+ # one from the kwargs WKT, adding in additional
+ # geometries, and update the attribute with the
+ # just-updated geometry WKT.
+ geom = getattr(m, self.geom_field).ogr
+ new = OGRGeometry(kwargs[self.geom_field])
+ for g in new: geom.add(g)
+ setattr(m, self.geom_field, geom.wkt)
+ except ObjectDoesNotExist:
+ # No unique model exists yet, create.
+ m = self.model(**kwargs)
+ else:
+ m = self.model(**kwargs)
+
+ try:
+ # Attempting to save.
+ m.save(using=self.using)
+ num_saved += 1
+ if verbose: stream.write('%s: %s\n' % ('Updated' if is_update else 'Saved', m))
+ except SystemExit:
+ raise
+ except Exception as msg:
+ if strict:
+ # Bailing out if the `strict` keyword is set.
+ if not silent:
+ stream.write('Failed to save the feature (id: %s) into the model with the keyword arguments:\n' % feat.fid)
+ stream.write('%s\n' % kwargs)
+ raise
+ elif not silent:
+ stream.write('Failed to save %s:\n %s\nContinuing\n' % (kwargs, msg))
+
+ # Printing progress information, if requested.
+ if progress and num_feat % progress_interval == 0:
+ stream.write('Processed %d features, saved %d ...\n' % (num_feat, num_saved))
+
+ # Only used for status output purposes -- incremental saving uses the
+ # values returned here.
+ return num_saved, num_feat
+
+ if self.transaction_decorator is not None:
+ _save = self.transaction_decorator(_save)
+
+ nfeat = self.layer.num_feat
+ if step and isinstance(step, int) and step < nfeat:
+ # Incremental saving is requested at the given interval (step)
+ if default_range:
+ raise LayerMapError('The `step` keyword may not be used in conjunction with the `fid_range` keyword.')
+ beg, num_feat, num_saved = (0, 0, 0)
+ indices = range(step, nfeat, step)
+ n_i = len(indices)
+
+ for i, end in enumerate(indices):
+ # Constructing the slice to use for this step; the last slice is
+ # special (e.g, [100:] instead of [90:100]).
+ if i+1 == n_i: step_slice = slice(beg, None)
+ else: step_slice = slice(beg, end)
+
+ try:
+ num_feat, num_saved = _save(step_slice, num_feat, num_saved)
+ beg = end
+ except:
+ stream.write('%s\nFailed to save slice: %s\n' % ('=-' * 20, step_slice))
+ raise
+ else:
+ # Otherwise, just calling the previously defined _save() function.
+ _save()