web/lib/django/contrib/gis/utils/layermapping.py
changeset 0 0d40e90630ef
child 29 cc9b7e14412b
equal deleted inserted replaced
-1:000000000000 0:0d40e90630ef
       
     1 # LayerMapping -- A Django Model/OGR Layer Mapping Utility
       
     2 """
       
     3  The LayerMapping class provides a way to map the contents of OGR
       
     4  vector files (e.g. SHP files) to Geographic-enabled Django models.
       
     5 
       
     6  This grew out of my personal needs, specifically the code repetition
       
     7  that went into pulling geometries and fields out of an OGR layer,
       
     8  converting to another coordinate system (e.g. WGS84), and then inserting
       
     9  into a GeoDjango model.
       
    10 
       
    11  Please report any bugs encountered using this utility.
       
    12 
       
    13  Requirements:  OGR C Library (from GDAL) required.
       
    14 
       
    15  Usage: 
       
    16   lm = LayerMapping(model, source_file, mapping) where,
       
    17 
       
    18   model:
       
    19    GeoDjango model (not an instance)
       
    20 
       
    21   data:
       
    22    OGR-supported data source file (e.g. a shapefile) or
       
    23     gdal.DataSource instance
       
    24 
       
    25   mapping:
       
    26    A python dictionary, keys are strings corresponding
       
    27    to the GeoDjango model field, and values correspond to
       
    28    string field names for the OGR feature, or if the model field
       
    29    is a geographic then it should correspond to the OGR
       
    30    geometry type, e.g. 'POINT', 'LINESTRING', 'POLYGON'.
       
    31 
       
    32  Keyword Args:
       
    33   layer:
       
    34    The index of the layer to use from the Data Source (defaults to 0)
       
    35 
       
    36   source_srs:
       
    37    Use this to specify the source SRS manually (for example, 
       
    38    some shapefiles don't come with a '.prj' file).  An integer SRID,
       
    39    a string WKT, and SpatialReference objects are valid parameters.
       
    40 
       
    41   encoding:
       
    42    Specifies the encoding of the string in the OGR data source.
       
    43    For example, 'latin-1', 'utf-8', and 'cp437' are all valid
       
    44    encoding parameters.
       
    45 
       
    46   transaction_mode:
       
    47    May be 'commit_on_success' (default) or 'autocommit'.
       
    48 
       
    49   transform:
       
    50    Setting this to False will disable all coordinate transformations.  
       
    51 
       
    52   unique:
       
    53    Setting this to the name, or a tuple of names, from the given
       
    54    model will create models unique only to the given name(s).
       
    55    Geometries will from each feature will be added into the collection
       
    56    associated with the unique model.  Forces transaction mode to
       
    57    be 'autocommit'.
       
    58 
       
    59 Example:
       
    60 
       
    61  1. You need a GDAL-supported data source, like a shapefile.
       
    62 
       
    63   Assume we're using the test_poly SHP file:
       
    64   >>> from django.contrib.gis.gdal import DataSource
       
    65   >>> ds = DataSource('test_poly.shp')
       
    66   >>> layer = ds[0]
       
    67   >>> print layer.fields # Exploring the fields in the layer, we only want the 'str' field.
       
    68   ['float', 'int', 'str']
       
    69   >>> print len(layer) # getting the number of features in the layer (should be 3)
       
    70   3
       
    71   >>> print layer.geom_type # Should be 3 (a Polygon)
       
    72   3
       
    73   >>> print layer.srs # WGS84
       
    74   GEOGCS["GCS_WGS_1984",
       
    75       DATUM["WGS_1984",
       
    76           SPHEROID["WGS_1984",6378137,298.257223563]],
       
    77       PRIMEM["Greenwich",0],
       
    78       UNIT["Degree",0.017453292519943295]]
       
    79 
       
    80  2. Now we define our corresponding Django model (make sure to use syncdb):
       
    81 
       
    82   from django.contrib.gis.db import models
       
    83   class TestGeo(models.Model, models.GeoMixin):
       
    84       name = models.CharField(maxlength=25) # corresponds to the 'str' field
       
    85       poly = models.PolygonField(srid=4269) # we want our model in a different SRID
       
    86       objects = models.GeoManager()
       
    87       def __str__(self):
       
    88           return 'Name: %s' % self.name
       
    89 
       
    90  3. Use LayerMapping to extract all the features and place them in the database:
       
    91 
       
    92   >>> from django.contrib.gis.utils import LayerMapping
       
    93   >>> from geoapp.models import TestGeo
       
    94   >>> mapping = {'name' : 'str', # The 'name' model field maps to the 'str' layer field.
       
    95                  'poly' : 'POLYGON', # For geometry fields use OGC name.
       
    96                  } # The mapping is a dictionary
       
    97   >>> lm = LayerMapping(TestGeo, 'test_poly.shp', mapping) 
       
    98   >>> lm.save(verbose=True) # Save the layermap, imports the data. 
       
    99   Saved: Name: 1
       
   100   Saved: Name: 2
       
   101   Saved: Name: 3
       
   102 
       
   103  LayerMapping just transformed the three geometries from the SHP file from their
       
   104  source spatial reference system (WGS84) to the spatial reference system of
       
   105  the GeoDjango model (NAD83).  If no spatial reference system is defined for
       
   106  the layer, use the `source_srs` keyword with a SpatialReference object to
       
   107  specify one.
       
   108 """
       
   109 import sys
       
   110 from datetime import date, datetime
       
   111 from decimal import Decimal
       
   112 from django.core.exceptions import ObjectDoesNotExist
       
   113 from django.contrib.gis.db.models import GeometryField
       
   114 from django.contrib.gis.db.backend import SpatialBackend
       
   115 from django.contrib.gis.gdal import CoordTransform, DataSource, \
       
   116     OGRException, OGRGeometry, OGRGeomType, SpatialReference
       
   117 from django.contrib.gis.gdal.field import \
       
   118     OFTDate, OFTDateTime, OFTInteger, OFTReal, OFTString, OFTTime
       
   119 from django.db import models, transaction
       
   120 from django.contrib.localflavor.us.models import USStateField
       
   121 
       
   122 # LayerMapping exceptions.
       
   123 class LayerMapError(Exception): pass
       
   124 class InvalidString(LayerMapError): pass
       
   125 class InvalidDecimal(LayerMapError): pass
       
   126 class InvalidInteger(LayerMapError): pass
       
   127 class MissingForeignKey(LayerMapError): pass
       
   128 
       
   129 class LayerMapping(object):
       
   130     "A class that maps OGR Layers to GeoDjango Models."
       
   131     
       
   132     # Acceptable 'base' types for a multi-geometry type.
       
   133     MULTI_TYPES = {1 : OGRGeomType('MultiPoint'),
       
   134                    2 : OGRGeomType('MultiLineString'),
       
   135                    3 : OGRGeomType('MultiPolygon'),
       
   136                    }
       
   137 
       
   138     # Acceptable Django field types and corresponding acceptable OGR
       
   139     # counterparts.
       
   140     FIELD_TYPES = {
       
   141         models.AutoField : OFTInteger,
       
   142         models.IntegerField : (OFTInteger, OFTReal, OFTString),
       
   143         models.FloatField : (OFTInteger, OFTReal),
       
   144         models.DateField : OFTDate,
       
   145         models.DateTimeField : OFTDateTime,
       
   146         models.EmailField : OFTString,
       
   147         models.TimeField : OFTTime,
       
   148         models.DecimalField : (OFTInteger, OFTReal),
       
   149         models.CharField : OFTString,
       
   150         models.SlugField : OFTString,
       
   151         models.TextField : OFTString,
       
   152         models.URLField : OFTString,
       
   153         USStateField : OFTString,
       
   154         models.XMLField : OFTString,
       
   155         models.SmallIntegerField : (OFTInteger, OFTReal, OFTString),
       
   156         models.PositiveSmallIntegerField : (OFTInteger, OFTReal, OFTString),
       
   157         }
       
   158 
       
   159     # The acceptable transaction modes.
       
   160     TRANSACTION_MODES = {'autocommit' : transaction.autocommit,
       
   161                          'commit_on_success' : transaction.commit_on_success,
       
   162                          }
       
   163 
       
   164     def __init__(self, model, data, mapping, layer=0, 
       
   165                  source_srs=None, encoding=None,
       
   166                  transaction_mode='commit_on_success', 
       
   167                  transform=True, unique=None):
       
   168         """
       
   169         A LayerMapping object is initialized using the given Model (not an instance),
       
   170         a DataSource (or string path to an OGR-supported data file), and a mapping
       
   171         dictionary.  See the module level docstring for more details and keyword
       
   172         argument usage.
       
   173         """
       
   174         # Getting the DataSource and the associated Layer.
       
   175         if isinstance(data, basestring):
       
   176             self.ds = DataSource(data)
       
   177         else:
       
   178             self.ds = data
       
   179         self.layer = self.ds[layer]
       
   180 
       
   181         # Setting the mapping & model attributes.
       
   182         self.mapping = mapping
       
   183         self.model = model
       
   184  
       
   185         # Checking the layer -- intitialization of the object will fail if
       
   186         # things don't check out before hand.
       
   187         self.check_layer()
       
   188 
       
   189         # Getting the geometry column associated with the model (an 
       
   190         # exception will be raised if there is no geometry column).
       
   191         if SpatialBackend.mysql:
       
   192             transform = False
       
   193         else:
       
   194             self.geo_col = self.geometry_column()
       
   195 
       
   196         # Checking the source spatial reference system, and getting
       
   197         # the coordinate transformation object (unless the `transform`
       
   198         # keyword is set to False)
       
   199         if transform:
       
   200             self.source_srs = self.check_srs(source_srs)
       
   201             self.transform = self.coord_transform()
       
   202         else:
       
   203             self.transform = transform
       
   204 
       
   205         # Setting the encoding for OFTString fields, if specified.
       
   206         if encoding:
       
   207             # Making sure the encoding exists, if not a LookupError
       
   208             # exception will be thrown.
       
   209             from codecs import lookup
       
   210             lookup(encoding)
       
   211             self.encoding = encoding
       
   212         else:
       
   213             self.encoding = None
       
   214 
       
   215         if unique:
       
   216             self.check_unique(unique)
       
   217             transaction_mode = 'autocommit' # Has to be set to autocommit.
       
   218             self.unique = unique
       
   219         else:
       
   220             self.unique = None
       
   221 
       
   222         # Setting the transaction decorator with the function in the 
       
   223         # transaction modes dictionary.
       
   224         if transaction_mode in self.TRANSACTION_MODES:
       
   225             self.transaction_decorator = self.TRANSACTION_MODES[transaction_mode]
       
   226             self.transaction_mode = transaction_mode
       
   227         else:
       
   228             raise LayerMapError('Unrecognized transaction mode: %s' % transaction_mode)
       
   229     
       
   230     #### Checking routines used during initialization ####
       
   231     def check_fid_range(self, fid_range):
       
   232         "This checks the `fid_range` keyword."
       
   233         if fid_range:
       
   234             if isinstance(fid_range, (tuple, list)):
       
   235                 return slice(*fid_range)
       
   236             elif isinstance(fid_range, slice):
       
   237                 return fid_range
       
   238             else:
       
   239                 raise TypeError
       
   240         else:
       
   241             return None
       
   242 
       
   243     def check_layer(self):
       
   244         """
       
   245         This checks the Layer metadata, and ensures that it is compatible
       
   246         with the mapping information and model.  Unlike previous revisions,
       
   247         there is no need to increment through each feature in the Layer.
       
   248         """
       
   249         # The geometry field of the model is set here.
       
   250         # TODO: Support more than one geometry field / model.  However, this
       
   251         # depends on the GDAL Driver in use.
       
   252         self.geom_field = False
       
   253         self.fields = {}
       
   254 
       
   255         # Getting lists of the field names and the field types available in
       
   256         # the OGR Layer.
       
   257         ogr_fields = self.layer.fields
       
   258         ogr_field_types = self.layer.field_types
       
   259 
       
   260         # Function for determining if the OGR mapping field is in the Layer.
       
   261         def check_ogr_fld(ogr_map_fld):
       
   262             try:
       
   263                 idx = ogr_fields.index(ogr_map_fld)
       
   264             except ValueError:
       
   265                 raise LayerMapError('Given mapping OGR field "%s" not found in OGR Layer.' % ogr_map_fld)
       
   266             return idx
       
   267 
       
   268         # No need to increment through each feature in the model, simply check
       
   269         # the Layer metadata against what was given in the mapping dictionary.
       
   270         for field_name, ogr_name in self.mapping.items():
       
   271             # Ensuring that a corresponding field exists in the model
       
   272             # for the given field name in the mapping.
       
   273             try:
       
   274                 model_field = self.model._meta.get_field(field_name)
       
   275             except models.fields.FieldDoesNotExist:
       
   276                 raise LayerMapError('Given mapping field "%s" not in given Model fields.' % field_name)
       
   277 
       
   278             # Getting the string name for the Django field class (e.g., 'PointField').
       
   279             fld_name = model_field.__class__.__name__
       
   280 
       
   281             if isinstance(model_field, GeometryField):
       
   282                 if self.geom_field:
       
   283                     raise LayerMapError('LayerMapping does not support more than one GeometryField per model.')
       
   284 
       
   285                 try:
       
   286                     gtype = OGRGeomType(ogr_name)
       
   287                 except OGRException:
       
   288                     raise LayerMapError('Invalid mapping for GeometryField "%s".' % field_name)
       
   289 
       
   290                 # Making sure that the OGR Layer's Geometry is compatible.
       
   291                 ltype = self.layer.geom_type
       
   292                 if not (gtype == ltype or self.make_multi(ltype, model_field)):
       
   293                     raise LayerMapError('Invalid mapping geometry; model has %s, feature has %s.' % (fld_name, gtype))
       
   294 
       
   295                 # Setting the `geom_field` attribute w/the name of the model field
       
   296                 # that is a Geometry.
       
   297                 self.geom_field = field_name
       
   298                 fields_val = model_field
       
   299             elif isinstance(model_field, models.ForeignKey):
       
   300                 if isinstance(ogr_name, dict):
       
   301                     # Is every given related model mapping field in the Layer?
       
   302                     rel_model = model_field.rel.to
       
   303                     for rel_name, ogr_field in ogr_name.items(): 
       
   304                         idx = check_ogr_fld(ogr_field)
       
   305                         try:
       
   306                             rel_field = rel_model._meta.get_field(rel_name)
       
   307                         except models.fields.FieldDoesNotExist:
       
   308                             raise LayerMapError('ForeignKey mapping field "%s" not in %s fields.' % 
       
   309                                                 (rel_name, rel_model.__class__.__name__))
       
   310                     fields_val = rel_model
       
   311                 else:
       
   312                     raise TypeError('ForeignKey mapping must be of dictionary type.')
       
   313             else:
       
   314                 # Is the model field type supported by LayerMapping?
       
   315                 if not model_field.__class__ in self.FIELD_TYPES:
       
   316                     raise LayerMapError('Django field type "%s" has no OGR mapping (yet).' % fld_name)
       
   317 
       
   318                 # Is the OGR field in the Layer?
       
   319                 idx = check_ogr_fld(ogr_name)
       
   320                 ogr_field = ogr_field_types[idx]
       
   321 
       
   322                 # Can the OGR field type be mapped to the Django field type?
       
   323                 if not issubclass(ogr_field, self.FIELD_TYPES[model_field.__class__]):
       
   324                     raise LayerMapError('OGR field "%s" (of type %s) cannot be mapped to Django %s.' % 
       
   325                                         (ogr_field, ogr_field.__name__, fld_name))
       
   326                 fields_val = model_field
       
   327         
       
   328             self.fields[field_name] = fields_val
       
   329 
       
   330     def check_srs(self, source_srs):
       
   331         "Checks the compatibility of the given spatial reference object."
       
   332         from django.contrib.gis.models import SpatialRefSys
       
   333         if isinstance(source_srs, SpatialReference):
       
   334             sr = source_srs
       
   335         elif isinstance(source_srs, SpatialRefSys):
       
   336             sr = source_srs.srs
       
   337         elif isinstance(source_srs, (int, basestring)):
       
   338             sr = SpatialReference(source_srs)
       
   339         else:
       
   340             # Otherwise just pulling the SpatialReference from the layer
       
   341             sr = self.layer.srs
       
   342         
       
   343         if not sr:
       
   344             raise LayerMapError('No source reference system defined.')
       
   345         else:
       
   346             return sr
       
   347 
       
   348     def check_unique(self, unique):
       
   349         "Checks the `unique` keyword parameter -- may be a sequence or string."
       
   350         if isinstance(unique, (list, tuple)):
       
   351             # List of fields to determine uniqueness with
       
   352             for attr in unique: 
       
   353                 if not attr in self.mapping: raise ValueError
       
   354         elif isinstance(unique, basestring):
       
   355             # Only a single field passed in.
       
   356             if unique not in self.mapping: raise ValueError
       
   357         else:
       
   358             raise TypeError('Unique keyword argument must be set with a tuple, list, or string.')
       
   359 
       
   360     #### Keyword argument retrieval routines ####
       
   361     def feature_kwargs(self, feat):
       
   362         """
       
   363         Given an OGR Feature, this will return a dictionary of keyword arguments
       
   364         for constructing the mapped model.
       
   365         """
       
   366         # The keyword arguments for model construction.
       
   367         kwargs = {}
       
   368 
       
   369         # Incrementing through each model field and OGR field in the
       
   370         # dictionary mapping.
       
   371         for field_name, ogr_name in self.mapping.items():
       
   372             model_field = self.fields[field_name]
       
   373             
       
   374             if isinstance(model_field, GeometryField):
       
   375                 # Verify OGR geometry.
       
   376                 val = self.verify_geom(feat.geom, model_field)
       
   377             elif isinstance(model_field, models.base.ModelBase):
       
   378                 # The related _model_, not a field was passed in -- indicating
       
   379                 # another mapping for the related Model.
       
   380                 val = self.verify_fk(feat, model_field, ogr_name)
       
   381             else:
       
   382                 # Otherwise, verify OGR Field type.
       
   383                 val = self.verify_ogr_field(feat[ogr_name], model_field)
       
   384 
       
   385             # Setting the keyword arguments for the field name with the
       
   386             # value obtained above.
       
   387             kwargs[field_name] = val
       
   388             
       
   389         return kwargs
       
   390 
       
   391     def unique_kwargs(self, kwargs):
       
   392         """
       
   393         Given the feature keyword arguments (from `feature_kwargs`) this routine
       
   394         will construct and return the uniqueness keyword arguments -- a subset
       
   395         of the feature kwargs.
       
   396         """
       
   397         if isinstance(self.unique, basestring):
       
   398             return {self.unique : kwargs[self.unique]}
       
   399         else:
       
   400             return dict((fld, kwargs[fld]) for fld in self.unique)
       
   401 
       
   402     #### Verification routines used in constructing model keyword arguments. ####
       
   403     def verify_ogr_field(self, ogr_field, model_field):
       
   404         """
       
   405         Verifies if the OGR Field contents are acceptable to the Django
       
   406         model field.  If they are, the verified value is returned, 
       
   407         otherwise the proper exception is raised.
       
   408         """
       
   409         if (isinstance(ogr_field, OFTString) and 
       
   410             isinstance(model_field, (models.CharField, models.TextField))): 
       
   411             if self.encoding:
       
   412                 # The encoding for OGR data sources may be specified here
       
   413                 # (e.g., 'cp437' for Census Bureau boundary files).
       
   414                 val = unicode(ogr_field.value, self.encoding)
       
   415             else:
       
   416                 val = ogr_field.value
       
   417                 if len(val) > model_field.max_length:
       
   418                     raise InvalidString('%s model field maximum string length is %s, given %s characters.' %
       
   419                                         (model_field.name, model_field.max_length, len(val)))
       
   420         elif isinstance(ogr_field, OFTReal) and isinstance(model_field, models.DecimalField):
       
   421             try:
       
   422                 # Creating an instance of the Decimal value to use.
       
   423                 d = Decimal(str(ogr_field.value))
       
   424             except:
       
   425                 raise InvalidDecimal('Could not construct decimal from: %s' % ogr_field.value)
       
   426 
       
   427             # Getting the decimal value as a tuple.
       
   428             dtup = d.as_tuple()
       
   429             digits = dtup[1]
       
   430             d_idx = dtup[2] # index where the decimal is
       
   431 
       
   432             # Maximum amount of precision, or digits to the left of the decimal.
       
   433             max_prec = model_field.max_digits - model_field.decimal_places
       
   434 
       
   435             # Getting the digits to the left of the decimal place for the 
       
   436             # given decimal.
       
   437             if d_idx < 0:
       
   438                 n_prec = len(digits[:d_idx])
       
   439             else:
       
   440                 n_prec = len(digits) + d_idx
       
   441 
       
   442             # If we have more than the maximum digits allowed, then throw an 
       
   443             # InvalidDecimal exception.
       
   444             if n_prec > max_prec:
       
   445                 raise InvalidDecimal('A DecimalField with max_digits %d, decimal_places %d must round to an absolute value less than 10^%d.' %
       
   446                                      (model_field.max_digits, model_field.decimal_places, max_prec))
       
   447             val = d
       
   448         elif isinstance(ogr_field, (OFTReal, OFTString)) and isinstance(model_field, models.IntegerField):
       
   449             # Attempt to convert any OFTReal and OFTString value to an OFTInteger.
       
   450             try:
       
   451                 val = int(ogr_field.value)
       
   452             except:
       
   453                 raise InvalidInteger('Could not construct integer from: %s' % ogr_field.value)
       
   454         else:
       
   455             val = ogr_field.value
       
   456         return val
       
   457 
       
   458     def verify_fk(self, feat, rel_model, rel_mapping):
       
   459         """
       
   460         Given an OGR Feature, the related model and its dictionary mapping,
       
   461         this routine will retrieve the related model for the ForeignKey
       
   462         mapping.
       
   463         """
       
   464         # TODO: It is expensive to retrieve a model for every record --
       
   465         #  explore if an efficient mechanism exists for caching related 
       
   466         #  ForeignKey models.
       
   467 
       
   468         # Constructing and verifying the related model keyword arguments.
       
   469         fk_kwargs = {}
       
   470         for field_name, ogr_name in rel_mapping.items():
       
   471             fk_kwargs[field_name] = self.verify_ogr_field(feat[ogr_name], rel_model._meta.get_field(field_name))
       
   472 
       
   473         # Attempting to retrieve and return the related model.
       
   474         try:
       
   475             return rel_model.objects.get(**fk_kwargs)
       
   476         except ObjectDoesNotExist:
       
   477             raise MissingForeignKey('No ForeignKey %s model found with keyword arguments: %s' % (rel_model.__name__, fk_kwargs))
       
   478             
       
   479     def verify_geom(self, geom, model_field):
       
   480         """
       
   481         Verifies the geometry -- will construct and return a GeometryCollection
       
   482         if necessary (for example if the model field is MultiPolygonField while
       
   483         the mapped shapefile only contains Polygons).
       
   484         """
       
   485         if self.make_multi(geom.geom_type, model_field):
       
   486             # Constructing a multi-geometry type to contain the single geometry
       
   487             multi_type = self.MULTI_TYPES[geom.geom_type.num]
       
   488             g = OGRGeometry(multi_type)
       
   489             g.add(geom)
       
   490         else:
       
   491             g = geom
       
   492 
       
   493         # Transforming the geometry with our Coordinate Transformation object,
       
   494         # but only if the class variable `transform` is set w/a CoordTransform 
       
   495         # object.
       
   496         if self.transform: g.transform(self.transform)
       
   497         
       
   498         # Returning the WKT of the geometry.
       
   499         return g.wkt
       
   500 
       
   501     #### Other model methods ####
       
   502     def coord_transform(self):
       
   503         "Returns the coordinate transformation object."
       
   504         from django.contrib.gis.models import SpatialRefSys
       
   505         try:
       
   506             # Getting the target spatial reference system
       
   507             target_srs = SpatialRefSys.objects.get(srid=self.geo_col.srid).srs
       
   508 
       
   509             # Creating the CoordTransform object
       
   510             return CoordTransform(self.source_srs, target_srs)
       
   511         except Exception, msg:
       
   512             raise LayerMapError('Could not translate between the data source and model geometry: %s' % msg)
       
   513 
       
   514     def geometry_column(self):
       
   515         "Returns the GeometryColumn model associated with the geographic column."
       
   516         from django.contrib.gis.models import GeometryColumns
       
   517         # Getting the GeometryColumn object.
       
   518         try:
       
   519             db_table = self.model._meta.db_table
       
   520             geo_col = self.geom_field
       
   521             if SpatialBackend.oracle:
       
   522                 # Making upper case for Oracle.
       
   523                 db_table = db_table.upper()
       
   524                 geo_col = geo_col.upper()
       
   525             gc_kwargs = {GeometryColumns.table_name_col() : db_table,
       
   526                          GeometryColumns.geom_col_name() : geo_col,
       
   527                          }
       
   528             return GeometryColumns.objects.get(**gc_kwargs)
       
   529         except Exception, msg:
       
   530             raise LayerMapError('Geometry column does not exist for model. (did you run syncdb?):\n %s' % msg)
       
   531 
       
   532     def make_multi(self, geom_type, model_field):
       
   533         """
       
   534         Given the OGRGeomType for a geometry and its associated GeometryField, 
       
   535         determine whether the geometry should be turned into a GeometryCollection.
       
   536         """
       
   537         return (geom_type.num in self.MULTI_TYPES and 
       
   538                 model_field.__class__.__name__ == 'Multi%s' % geom_type.django)
       
   539 
       
   540     def save(self, verbose=False, fid_range=False, step=False, 
       
   541              progress=False, silent=False, stream=sys.stdout, strict=False):
       
   542         """
       
   543         Saves the contents from the OGR DataSource Layer into the database
       
   544         according to the mapping dictionary given at initialization. 
       
   545         
       
   546         Keyword Parameters:
       
   547          verbose:
       
   548            If set, information will be printed subsequent to each model save 
       
   549            executed on the database.
       
   550 
       
   551          fid_range:
       
   552            May be set with a slice or tuple of (begin, end) feature ID's to map
       
   553            from the data source.  In other words, this keyword enables the user
       
   554            to selectively import a subset range of features in the geographic
       
   555            data source.
       
   556 
       
   557          step:
       
   558            If set with an integer, transactions will occur at every step 
       
   559            interval. For example, if step=1000, a commit would occur after 
       
   560            the 1,000th feature, the 2,000th feature etc.
       
   561 
       
   562          progress:
       
   563            When this keyword is set, status information will be printed giving 
       
   564            the number of features processed and sucessfully saved.  By default, 
       
   565            progress information will pe printed every 1000 features processed, 
       
   566            however, this default may be overridden by setting this keyword with an 
       
   567            integer for the desired interval.
       
   568 
       
   569          stream:
       
   570            Status information will be written to this file handle.  Defaults to 
       
   571            using `sys.stdout`, but any object with a `write` method is supported.
       
   572 
       
   573          silent:
       
   574            By default, non-fatal error notifications are printed to stdout, but 
       
   575            this keyword may be set to disable these notifications.
       
   576 
       
   577          strict:
       
   578            Execution of the model mapping will cease upon the first error 
       
   579            encountered.  The default behavior is to attempt to continue.
       
   580         """
       
   581         # Getting the default Feature ID range.
       
   582         default_range = self.check_fid_range(fid_range)
       
   583     
       
   584         # Setting the progress interval, if requested.
       
   585         if progress:
       
   586             if progress is True or not isinstance(progress, int):
       
   587                 progress_interval = 1000
       
   588             else:
       
   589                 progress_interval = progress
       
   590 
       
   591         # Defining the 'real' save method, utilizing the transaction 
       
   592         # decorator created during initialization.
       
   593         @self.transaction_decorator
       
   594         def _save(feat_range=default_range, num_feat=0, num_saved=0):
       
   595             if feat_range:
       
   596                 layer_iter = self.layer[feat_range]
       
   597             else:
       
   598                 layer_iter = self.layer
       
   599 
       
   600             for feat in layer_iter:
       
   601                 num_feat += 1
       
   602                 # Getting the keyword arguments
       
   603                 try:
       
   604                     kwargs = self.feature_kwargs(feat)
       
   605                 except LayerMapError, msg:
       
   606                     # Something borked the validation
       
   607                     if strict: raise
       
   608                     elif not silent: 
       
   609                         stream.write('Ignoring Feature ID %s because: %s\n' % (feat.fid, msg))
       
   610                 else:
       
   611                     # Constructing the model using the keyword args
       
   612                     is_update = False
       
   613                     if self.unique:
       
   614                         # If we want unique models on a particular field, handle the
       
   615                         # geometry appropriately.
       
   616                         try:
       
   617                             # Getting the keyword arguments and retrieving
       
   618                             # the unique model.
       
   619                             u_kwargs = self.unique_kwargs(kwargs)
       
   620                             m = self.model.objects.get(**u_kwargs)
       
   621                             is_update = True
       
   622                                 
       
   623                             # Getting the geometry (in OGR form), creating 
       
   624                             # one from the kwargs WKT, adding in additional 
       
   625                             # geometries, and update the attribute with the 
       
   626                             # just-updated geometry WKT.
       
   627                             geom = getattr(m, self.geom_field).ogr
       
   628                             new = OGRGeometry(kwargs[self.geom_field])
       
   629                             for g in new: geom.add(g) 
       
   630                             setattr(m, self.geom_field, geom.wkt)
       
   631                         except ObjectDoesNotExist:
       
   632                             # No unique model exists yet, create.
       
   633                             m = self.model(**kwargs)
       
   634                     else:
       
   635                         m = self.model(**kwargs)
       
   636 
       
   637                     try:
       
   638                         # Attempting to save.
       
   639                         m.save()
       
   640                         num_saved += 1
       
   641                         if verbose: stream.write('%s: %s\n' % (is_update and 'Updated' or 'Saved', m))
       
   642                     except SystemExit:
       
   643                         raise
       
   644                     except Exception, msg:
       
   645                         if self.transaction_mode == 'autocommit':
       
   646                             # Rolling back the transaction so that other model saves
       
   647                             # will work.
       
   648                             transaction.rollback_unless_managed()
       
   649                         if strict: 
       
   650                             # Bailing out if the `strict` keyword is set.
       
   651                             if not silent:
       
   652                                 stream.write('Failed to save the feature (id: %s) into the model with the keyword arguments:\n' % feat.fid)
       
   653                                 stream.write('%s\n' % kwargs)
       
   654                             raise
       
   655                         elif not silent:
       
   656                             stream.write('Failed to save %s:\n %s\nContinuing\n' % (kwargs, msg))
       
   657 
       
   658                 # Printing progress information, if requested.
       
   659                 if progress and num_feat % progress_interval == 0:
       
   660                     stream.write('Processed %d features, saved %d ...\n' % (num_feat, num_saved))
       
   661         
       
   662             # Only used for status output purposes -- incremental saving uses the
       
   663             # values returned here.
       
   664             return num_saved, num_feat
       
   665 
       
   666         nfeat = self.layer.num_feat
       
   667         if step and isinstance(step, int) and step < nfeat:
       
   668             # Incremental saving is requested at the given interval (step) 
       
   669             if default_range: 
       
   670                 raise LayerMapError('The `step` keyword may not be used in conjunction with the `fid_range` keyword.')
       
   671             beg, num_feat, num_saved = (0, 0, 0)
       
   672             indices = range(step, nfeat, step)
       
   673             n_i = len(indices)
       
   674 
       
   675             for i, end in enumerate(indices):
       
   676                 # Constructing the slice to use for this step; the last slice is
       
   677                 # special (e.g, [100:] instead of [90:100]).
       
   678                 if i+1 == n_i: step_slice = slice(beg, None)
       
   679                 else: step_slice = slice(beg, end)
       
   680             
       
   681                 try:
       
   682                     num_feat, num_saved = _save(step_slice, num_feat, num_saved)
       
   683                     beg = end
       
   684                 except:
       
   685                     stream.write('%s\nFailed to save slice: %s\n' % ('=-' * 20, step_slice))
       
   686                     raise
       
   687         else:
       
   688             # Otherwise, just calling the previously defined _save() function.
       
   689             _save()