1
0
mirror of https://github.com/django/django.git synced 2025-07-04 17:59:13 +00:00

gis: LayerMapping: Improved the internals (i.e., checking every feature in OGR Layer is no longer needed, removed unnecessary class constants); added real support ForeignKey model fields; added field_types property to Layer; fixed county shapefile because of Harris County, Georgia.

git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@6992 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Justin Bronn 2008-01-03 19:02:18 +00:00
parent 7e322b5908
commit b11172e4e7
7 changed files with 289 additions and 216 deletions

View File

@ -5,6 +5,7 @@ from ctypes import byref
from django.contrib.gis.gdal.envelope import Envelope, OGREnvelope from django.contrib.gis.gdal.envelope import Envelope, OGREnvelope
from django.contrib.gis.gdal.error import OGRException, OGRIndexError, SRSException from django.contrib.gis.gdal.error import OGRException, OGRIndexError, SRSException
from django.contrib.gis.gdal.feature import Feature from django.contrib.gis.gdal.feature import Feature
from django.contrib.gis.gdal.field import FIELD_CLASSES
from django.contrib.gis.gdal.geometries import OGRGeomType from django.contrib.gis.gdal.geometries import OGRGeomType
from django.contrib.gis.gdal.srs import SpatialReference from django.contrib.gis.gdal.srs import SpatialReference
@ -12,8 +13,8 @@ from django.contrib.gis.gdal.srs import SpatialReference
from django.contrib.gis.gdal.prototypes.ds import \ from django.contrib.gis.gdal.prototypes.ds import \
get_extent, get_fd_geom_type, get_fd_name, get_feature, get_feature_count, \ get_extent, get_fd_geom_type, get_fd_name, get_feature, get_feature_count, \
get_field_count, get_field_defn, get_field_name, get_field_precision, \ get_field_count, get_field_defn, get_field_name, get_field_precision, \
get_field_width, get_layer_defn, get_layer_srs, get_next_feature, \ get_field_width, get_field_type, get_layer_defn, get_layer_srs, \
reset_reading get_next_feature, reset_reading
from django.contrib.gis.gdal.prototypes.srs import clone_srs from django.contrib.gis.gdal.prototypes.srs import clone_srs
# For more information, see the OGR C API source code: # For more information, see the OGR C API source code:
@ -107,10 +108,24 @@ class Layer(object):
@property @property
def fields(self): def fields(self):
"Returns a list of the fields available in this Layer." """
Returns a list of string names corresponding to each of the Fields
available in this Layer.
"""
return [get_field_name(get_field_defn(self._ldefn, i)) return [get_field_name(get_field_defn(self._ldefn, i))
for i in xrange(self.num_fields) ] for i in xrange(self.num_fields) ]
@property
def field_types(self):
"""
Returns a list of the types of fields in this Layer. For example,
the list [OFTInteger, OFTReal, OFTString] would be returned for
an OGR layer that had an integer, a floating-point, and string
fields.
"""
return [FIELD_CLASSES[get_field_type(get_field_defn(self._ldefn, i))]
for i in xrange(self.num_fields)]
@property @property
def field_widths(self): def field_widths(self):
"Returns a list of the maximum field widths for the features." "Returns a list of the maximum field widths for the features."

View File

@ -1,7 +1,12 @@
from django.contrib.gis.db import models from django.contrib.gis.db import models
class State(models.Model):
name = models.CharField(max_length=20)
objects = models.GeoManager()
class County(models.Model): class County(models.Model):
name = models.CharField(max_length=25) name = models.CharField(max_length=25)
state = models.ForeignKey(State)
mpoly = models.MultiPolygonField(srid=4269) # Multipolygon in NAD83 mpoly = models.MultiPolygonField(srid=4269) # Multipolygon in NAD83
objects = models.GeoManager() objects = models.GeoManager()
@ -26,6 +31,7 @@ class Interstate(models.Model):
# Mapping dictionaries for the models above. # Mapping dictionaries for the models above.
co_mapping = {'name' : 'Name', co_mapping = {'name' : 'Name',
'state' : {'name' : 'State'}, # ForeignKey's use another mapping dictionary for the _related_ Model (State in this case).
'mpoly' : 'MULTIPOLYGON', # Will convert POLYGON features into MULTIPOLYGONS. 'mpoly' : 'MULTIPOLYGON', # Will convert POLYGON features into MULTIPOLYGONS.
} }

View File

@ -2,8 +2,8 @@ import os, unittest
from copy import copy from copy import copy
from datetime import date from datetime import date
from decimal import Decimal from decimal import Decimal
from models import City, County, CountyFeat, Interstate, city_mapping, co_mapping, cofeat_mapping, inter_mapping from models import City, County, CountyFeat, Interstate, State, city_mapping, co_mapping, cofeat_mapping, inter_mapping
from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError, InvalidDecimal from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError, InvalidDecimal, MissingForeignKey
from django.contrib.gis.gdal import DataSource from django.contrib.gis.gdal import DataSource
shp_path = os.path.dirname(__file__) shp_path = os.path.dirname(__file__)
@ -111,8 +111,8 @@ class LayerMapTest(unittest.TestCase):
self.assertAlmostEqual(p1[0], p2[0], 6) self.assertAlmostEqual(p1[0], p2[0], 6)
self.assertAlmostEqual(p1[1], p2[1], 6) self.assertAlmostEqual(p1[1], p2[1], 6)
def test04_layermap_unique_multigeometry(self): def test04_layermap_unique_multigeometry_fk(self):
"Testing the `unique`, and `transform` keywords and geometry collection conversion." "Testing the `unique`, and `transform`, geometry collection conversion, and ForeignKey mappings."
# All the following should work. # All the following should work.
try: try:
# Telling LayerMapping that we want no transformations performed on the data. # Telling LayerMapping that we want no transformations performed on the data.
@ -135,6 +135,23 @@ class LayerMapTest(unittest.TestCase):
# No source reference system defined in the shapefile, should raise an error. # No source reference system defined in the shapefile, should raise an error.
self.assertRaises(LayerMapError, LayerMapping, County, co_shp, co_mapping) self.assertRaises(LayerMapError, LayerMapping, County, co_shp, co_mapping)
# Passing in invalid ForeignKey mapping parameters -- must be a dictionary
# mapping for the model the ForeignKey points to.
bad_fk_map1 = copy(co_mapping); bad_fk_map1['state'] = 'name'
bad_fk_map2 = copy(co_mapping); bad_fk_map2['state'] = {'nombre' : 'State'}
self.assertRaises(TypeError, LayerMapping, County, co_shp, bad_fk_map1, transform=False)
self.assertRaises(LayerMapError, LayerMapping, County, co_shp, bad_fk_map2, transform=False)
# There exist no State models for the ForeignKey mapping to work -- should raise
# a MissingForeignKey exception (this error would be ignored if the `strict`
# keyword is not set).
lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique='name', silent=True, strict=True)
self.assertRaises(MissingForeignKey, lm.save)
# Now creating the state models so the ForeignKey mapping may work.
co, hi, tx = State(name='Colorado'), State(name='Hawaii'), State(name='Texas')
co.save(), hi.save(), tx.save()
# If a mapping is specified as a collection, all OGR fields that # If a mapping is specified as a collection, all OGR fields that
# are not collections will be converted into them. For example, # are not collections will be converted into them. For example,
# a Point column would be converted to MultiPoint. Other things being done # a Point column would be converted to MultiPoint. Other things being done
@ -148,21 +165,26 @@ class LayerMapTest(unittest.TestCase):
# appended to the geometry collection of the unique model. Thus, # appended to the geometry collection of the unique model. Thus,
# all of the various islands in Honolulu county will be in in one # all of the various islands in Honolulu county will be in in one
# database record with a MULTIPOLYGON type. # database record with a MULTIPOLYGON type.
lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique='name', silent=True) lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique='name', silent=True, strict=True)
lm.save() lm.save()
# A reference that doesn't use the unique keyword; a new database record will # A reference that doesn't use the unique keyword; a new database record will
# created for each polygon. # created for each polygon.
lm = LayerMapping(CountyFeat, co_shp, cofeat_mapping, transform=False, silent=True) lm = LayerMapping(CountyFeat, co_shp, cofeat_mapping, transform=False, silent=True, strict=True)
lm.save() lm.save()
# Dictionary to hold what's expected in the shapefile. # Dictionary to hold what's expected in the shapefile.
exp = {'names' : ('Bexar', 'Galveston', 'Harris', 'Honolulu', 'Pueblo'), names = ('Bexar', 'Galveston', 'Harris', 'Honolulu', 'Pueblo')
'num' : (1, 2, 2, 19, 1), # Number of polygons for each. nums = (1, 2, 1, 19, 1) # Number of polygons for each.
} states = ('Texas', 'Texas', 'Texas', 'Hawaii', 'Colorado')
for name, n in zip(exp['names'], exp['num']):
c = County.objects.get(name=name) # Should only be one record. for name, n, st in zip(names, nums, states):
# Should only be one record b/c of `unique` keyword.
c = County.objects.get(name=name)
self.assertEqual(n, len(c.mpoly)) self.assertEqual(n, len(c.mpoly))
self.assertEqual(st, c.state.name) # Checking ForeignKey mapping.
# Multiple records because `unique` was not set.
qs = CountyFeat.objects.filter(name=name) qs = CountyFeat.objects.filter(name=name)
self.assertEqual(n, qs.count()) self.assertEqual(n, qs.count())

View File

@ -45,10 +45,13 @@
encoding parameters. encoding parameters.
check: check:
By default, LayerMapping increments through each feature in the Due to optimizations, this keyword argument is deprecated and will
layer to ensure that it is compatible with the given model and be removed in future revisions.
mapping. Setting this keyword to False, disables this action,
which will speed up execution time for very large files. pipe:
Status information will be written to this file handle. Defaults
to using `sys.stdout`, but any object with a `write` method is
supported.
silent: silent:
By default, non-fatal error notifications are printed to stdout; this By default, non-fatal error notifications are printed to stdout; this
@ -56,7 +59,8 @@
strict: strict:
Setting this keyword to True will instruct the save() method to Setting this keyword to True will instruct the save() method to
cease execution on the first error encountered. cease execution on the first error encountered. The default behavior
is to attempt to continue even if errors are encountered.
transaction_mode: transaction_mode:
May be 'commit_on_success' (default) or 'autocommit'. May be 'commit_on_success' (default) or 'autocommit'.
@ -121,64 +125,48 @@ Example:
the layer, use the `source_srs` keyword with a SpatialReference object to the layer, use the `source_srs` keyword with a SpatialReference object to
specify one. specify one.
""" """
import sys
from datetime import date, datetime from datetime import date, datetime
from decimal import Decimal from decimal import Decimal
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import connection, transaction from django.contrib.gis.db.models.fields import GeometryField
from django.db.models.fields.related import ForeignKey
from django.contrib.gis.db.backend import SPATIAL_BACKEND from django.contrib.gis.db.backend import SPATIAL_BACKEND
from django.contrib.gis.gdal import CoordTransform, DataSource, \ from django.contrib.gis.gdal import CoordTransform, DataSource, \
OGRException, OGRGeometry, OGRGeomType, SpatialReference OGRException, OGRGeometry, OGRGeomType, SpatialReference
from django.contrib.gis.gdal.field import OFTDate, OFTDateTime, OFTInteger, OFTReal, OFTString, OFTTime from django.contrib.gis.gdal.field import \
OFTDate, OFTDateTime, OFTInteger, OFTReal, OFTString, OFTTime
from django.contrib.gis.models import GeometryColumns, SpatialRefSys from django.contrib.gis.models import GeometryColumns, SpatialRefSys
from django.db import models, transaction
# LayerMapping exceptions. # LayerMapping exceptions.
class LayerMapError(Exception): pass class LayerMapError(Exception): pass
class InvalidString(LayerMapError): pass class InvalidString(LayerMapError): pass
class InvalidDecimal(LayerMapError): pass class InvalidDecimal(LayerMapError): pass
class MissingForeignKey(LayerMapError): pass
class LayerMapping(object): class LayerMapping(object):
"A class that maps OGR Layers to GeoDjango Models." "A class that maps OGR Layers to GeoDjango Models."
# A mapping of given geometry types to their OGR integer type.
OGC_TYPES = {'POINT' : OGRGeomType('Point'),
'LINESTRING' : OGRGeomType('LineString'),
'POLYGON' : OGRGeomType('Polygon'),
'MULTIPOINT' : OGRGeomType('MultiPoint'),
'MULTILINESTRING' : OGRGeomType('MultiLineString'),
'MULTIPOLYGON' : OGRGeomType('MultiPolygon'),
'GEOMETRYCOLLECTION' : OGRGeomType('GeometryCollection'),
}
# The django.contrib.gis model types.
GIS_FIELDS = {'PointField' : 'POINT',
'LineStringField': 'LINESTRING',
'PolygonField': 'POLYGON',
'MultiPointField' : 'MULTIPOINT',
'MultiLineStringField' : 'MULTILINESTRING',
'MultiPolygonField' : 'MULTIPOLYGON',
'GeometryCollectionField' : 'GEOMETRYCOLLECTION',
}
# Acceptable 'base' types for a multi-geometry type. # Acceptable 'base' types for a multi-geometry type.
MULTI_TYPES = {'POINT' : OGRGeomType('MultiPoint'), MULTI_TYPES = {1 : OGRGeomType('MultiPoint'),
'LINESTRING' : OGRGeomType('MultiLineString'), 2 : OGRGeomType('MultiLineString'),
'POLYGON' : OGRGeomType('MultiPolygon'), 3 : OGRGeomType('MultiPolygon'),
} }
# The acceptable Django field types that map to OGR fields. # Acceptable Django field types and corresponding acceptable OGR
# counterparts.
FIELD_TYPES = { FIELD_TYPES = {
'AutoField' : OFTInteger, models.AutoField : OFTInteger,
'IntegerField' : OFTInteger, models.IntegerField : (OFTInteger, OFTReal),
'FloatField' : OFTReal, models.FloatField : (OFTInteger, OFTReal),
'DateField' : OFTDate, models.DateField : OFTDate,
'DateTimeField' : OFTDateTime, models.DateTimeField : OFTDateTime,
'TimeField' : OFTTime, models.TimeField : OFTTime,
'DecimalField' : OFTReal, models.DecimalField : (OFTInteger, OFTReal),
'CharField' : OFTString, models.CharField : OFTString,
'TextField' : OFTString, models.TextField : OFTString,
'SmallIntegerField' : OFTInteger, models.SmallIntegerField : (OFTInteger, OFTReal),
'PositiveSmallIntegerField' : OFTInteger, models.PositiveSmallIntegerField : (OFTInteger, OFTReal),
} }
# The acceptable transaction modes. # The acceptable transaction modes.
@ -187,17 +175,17 @@ class LayerMapping(object):
} }
def __init__(self, model, data, mapping, layer=0, def __init__(self, model, data, mapping, layer=0,
source_srs=None, encoding=None, check=True, source_srs=None, encoding=None, check=True, pipe=sys.stdout,
progress=False, interval=1000, strict=False, silent=False, progress=False, interval=1000, strict=False, silent=False,
transaction_mode='commit_on_success', transform=True, transaction_mode='commit_on_success', transform=True,
unique=False): unique=False):
"Takes the Django model, the data source, and the mapping (dictionary)" """
A LayerMapping object is initialized using the given Model (not an instance),
# Getting the field names and types from the model a DataSource (or string path to an OGR-supported data file), and a mapping
self.fields = dict((f.name, self.map_foreign_key(f)) for f in model._meta.fields) dictionary. See the module level docstring for more details and keyword
self.field_classes = dict((f.name, f) for f in model._meta.fields) argument usage.
"""
# Getting the DataSource and its Layer # Getting the DataSource and the associated Layer.
if isinstance(data, basestring): if isinstance(data, basestring):
self.ds = DataSource(data) self.ds = DataSource(data)
else: else:
@ -223,14 +211,16 @@ class LayerMapping(object):
self.transform = transform self.transform = transform
# Checking the layer -- intitialization of the object will fail if # Checking the layer -- intitialization of the object will fail if
# things don't check out before hand. This may be time-consuming, # things don't check out before hand.
# and disabled by setting the `check` keyword to False. self.check_layer()
if check: self.check_layer()
# The silent, strict, progress, and interval flags. # The strict flag -- if it is set, exceptions will be propagated up.
self.silent = silent
self.strict = strict self.strict = strict
# Setting the keyword arguments related to status printing.
self.silent = silent
self.progress = progress self.progress = progress
self.pipe = pipe
self.interval = interval self.interval = interval
# Setting the encoding for OFTString fields, if specified. # Setting the encoding for OFTString fields, if specified.
@ -258,62 +248,91 @@ class LayerMapping(object):
else: else:
raise LayerMapError('Unrecognized transaction mode: %s' % transaction_mode) raise LayerMapError('Unrecognized transaction mode: %s' % transaction_mode)
def check_feature(self, feat): #### Checking routines used during initialization ####
"Checks the OGR feature against the model fields and mapping."
HAS_GEO = False
# Incrementing through each model_field & ogr_field in the given mapping.
for model_field, ogr_field in self.mapping.items():
# Making sure the given mapping model field is in the given model fields.
if model_field in self.fields:
model_type = self.fields[model_field]
elif model_field[:-3] in self.fields: #foreign key
model_type = self.fields[model_field[:-3]]
else:
raise LayerMapError('Given mapping field "%s" not in given Model fields!' % model_field)
### Handling if we get a geometry in the Field ###
if ogr_field in self.OGC_TYPES:
# At this time, no more than one geographic field per model =(
if HAS_GEO:
raise LayerMapError('More than one geographic field in mapping not allowed (yet).')
else:
HAS_GEO = ogr_field
# Making sure this geometry field type is a valid Django GIS field.
if not model_type in self.GIS_FIELDS:
raise LayerMapError('Unknown Django GIS field type "%s"' % model_type)
# Getting the OGRGeometry, it's type (an integer) and it's name (a string)
geom = feat.geom
gtype = geom.geom_type
gname = geom.geom_name
if self.make_multi(gname, model_type):
# Do we have to 'upsample' into a Geometry Collection?
pass
elif gtype == self.OGC_TYPES[self.GIS_FIELDS[model_type]]:
# The geometry type otherwise was expected
pass
else:
raise LayerMapError('Invalid mapping geometry; model has %s, feature has %s' % (model_type, gtype))
### Handling other fields ###
else:
# Making sure the model field is supported.
if not model_type in self.FIELD_TYPES:
raise LayerMapError('Django field type "%s" has no OGR mapping (yet).' % model_type)
# Otherwise, we've got an OGR Field. Making sure that an
# index exists for the mapping OGR field.
try:
fi = feat.index(ogr_field)
except:
raise LayerMapError('Given mapping OGR field "%s" not in given OGR layer feature!' % ogr_field)
def check_layer(self): def check_layer(self):
"Checks every feature in this object's layer." """
for feat in self.layer: This checks the Layer metadata, and ensures that it is compatible
self.check_feature(feat) 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.
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.')
try:
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 (gtype == ltype or self.make_multi(ltype, model_field)):
raise LayerMapError('Invalid mapping geometry; model has %s, feature has %s.' % (fld_name, gtype))
# Setting the `geom_field` attribute w/the name of the model field
# that is a Geometry.
self.geom_field = field_name
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)
# Can the OGR field type be mapped to the Django field type?
if not issubclass(ogr_field_types[idx], self.FIELD_TYPES[model_field.__class__]):
raise LayerMapError('OGR field "%s" (of type %s) cannot be mapped to Django %s.' %
(ogr_field, ogr_field_types[idx].__name__, fld_name))
fields_val = model_field
self.fields[field_name] = fields_val
def check_srs(self, source_srs): def check_srs(self, source_srs):
"Checks the compatibility of the given spatial reference object." "Checks the compatibility of the given spatial reference object."
@ -321,7 +340,7 @@ class LayerMapping(object):
sr = source_srs sr = source_srs
elif isinstance(source_srs, SpatialRefSys): elif isinstance(source_srs, SpatialRefSys):
sr = source_srs.srs sr = source_srs.srs
elif isinstance(source_srs, (int, str)): elif isinstance(source_srs, (int, basestring)):
sr = SpatialReference(source_srs) sr = SpatialReference(source_srs)
else: else:
# Otherwise just pulling the SpatialReference from the layer # Otherwise just pulling the SpatialReference from the layer
@ -334,14 +353,6 @@ class LayerMapping(object):
def check_unique(self, unique): def check_unique(self, unique):
"Checks the `unique` keyword parameter -- may be a sequence or string." "Checks the `unique` keyword parameter -- may be a sequence or string."
# Getting the geometry field; only the first encountered GeometryField
# will be used.
self.geom_field = False
for model_field, ogr_fld in self.mapping.items():
if ogr_fld in self.OGC_TYPES:
self.geom_field = model_field
break
if isinstance(unique, (list, tuple)): if isinstance(unique, (list, tuple)):
# List of fields to determine uniqueness with # List of fields to determine uniqueness with
for attr in unique: for attr in unique:
@ -352,20 +363,14 @@ class LayerMapping(object):
else: else:
raise TypeError('Unique keyword argument must be set with a tuple, list, or string.') raise TypeError('Unique keyword argument must be set with a tuple, list, or string.')
def coord_transform(self): #### Keyword argument retrieval routines ####
"Returns the coordinate transformation object."
try:
# Getting the target spatial reference system
target_srs = SpatialRefSys.objects.get(srid=self.geo_col.srid).srs
# Creating the CoordTransform object
return CoordTransform(self.source_srs, target_srs)
except Exception, msg:
raise LayerMapError('Could not translate between the data source and model geometry: %s' % msg)
def feature_kwargs(self, feat): def feature_kwargs(self, feat):
"Returns the keyword arguments needed for saving a feature." """
Given an OGR Feature, this will return a dictionary of keyword arguments
for constructing the mapped model. Also returned is the `all_prepped`
flag, which is used to signal that a model corresponding to a ForeignKey
mapping does not exist.
"""
# The keyword arguments for model construction. # The keyword arguments for model construction.
kwargs = {} kwargs = {}
@ -375,37 +380,24 @@ class LayerMapping(object):
# Incrementing through each model field and OGR field in the # Incrementing through each model field and OGR field in the
# dictionary mapping. # dictionary mapping.
for model_field, ogr_field in self.mapping.items(): for field_name, ogr_name in self.mapping.items():
is_fk = False model_field = self.fields[field_name]
try:
model_type = self.fields[model_field]
except KeyError: #foreign key
# The -3 index is b/c foreign keys are appended w/'_id'.
model_type = self.fields[model_field[:-3]]
is_fk = True
if ogr_field in self.OGC_TYPES: if isinstance(model_field, GeometryField):
# Verify OGR geometry. # Verify OGR geometry.
val = self.verify_geom(feat.geom, model_type) val = self.verify_geom(feat.geom, model_field)
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)
if not val: all_prepped = False
else: else:
# Otherwise, verify OGR Field type. # Otherwise, verify OGR Field type.
val = self.verify_field(feat[ogr_field], model_field) val = self.verify_ogr_field(feat[ogr_name], model_field)
if is_fk: # Setting the keyword arguments for the field name with the
# Handling if foreign key. # value obtained above.
rel_obj = None kwargs[field_name] = val
field_name = model_field[:-3]
try:
# FIXME: refactor to efficiently fetch FKs.
# Requires significant re-work. :-/
rel = self.model._meta.get_field(field_name).rel
rel_obj = rel.to._default_manager.get(**{('%s__exact' % rel.field_name):val})
except ObjectDoesNotExist:
all_prepped = False
kwargs[model_field[:-3]] = rel_obj
else:
kwargs[model_field] = val
return kwargs, all_prepped return kwargs, all_prepped
@ -420,29 +412,29 @@ class LayerMapping(object):
else: else:
return dict((fld, kwargs[fld]) for fld in self.unique) return dict((fld, kwargs[fld]) for fld in self.unique)
def verify_field(self, fld, model_field): #### 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 Verifies if the OGR Field contents are acceptable to the Django
model field. If they are, the verified value is returned, model field. If they are, the verified value is returned,
otherwise the proper exception is raised. otherwise the proper exception is raised.
""" """
field_class = self.field_classes[model_field] if isinstance(ogr_field, OFTString):
if isinstance(fld, OFTString):
if self.encoding: if self.encoding:
# The encoding for OGR data sources may be specified here # The encoding for OGR data sources may be specified here
# (e.g., 'cp437' for Census Bureau boundary files). # (e.g., 'cp437' for Census Bureau boundary files).
val = unicode(fld.value, self.encoding) val = unicode(ogr_field.value, self.encoding)
else: else:
val = fld.value val = ogr_field.value
if len(val) > field_class.max_length: if len(val) > model_field.max_length:
raise InvalidString('%s model field maximum string length is %s, given %s characters.' % raise InvalidString('%s model field maximum string length is %s, given %s characters.' %
(model_field, field_class.max_length, len(val))) (model_field.name, model_field.max_length, len(val)))
elif isinstance(fld, OFTReal): elif isinstance(ogr_field, OFTReal):
try: try:
# Creating an instance of the Decimal value to use. # Creating an instance of the Decimal value to use.
d = Decimal(str(fld.value)) d = Decimal(str(ogr_field.value))
except: except:
raise InvalidDecimal('Could not construct decimal from: %s' % fld) raise InvalidDecimal('Could not construct decimal from: %s' % ogr_field)
# Getting the decimal value as a tuple. # Getting the decimal value as a tuple.
dtup = d.as_tuple() dtup = d.as_tuple()
@ -450,7 +442,7 @@ class LayerMapping(object):
d_idx = dtup[2] # index where the decimal is d_idx = dtup[2] # index where the decimal is
# Maximum amount of precision, or digits to the left of the decimal. # Maximum amount of precision, or digits to the left of the decimal.
max_prec = field_class.max_digits - field_class.decimal_places max_prec = model_field.max_digits - model_field.decimal_places
# Getting the digits to the left of the decimal place for the # Getting the digits to the left of the decimal place for the
# given decimal. # given decimal.
@ -463,17 +455,43 @@ class LayerMapping(object):
# InvalidDecimal exception. # InvalidDecimal exception.
if n_prec > max_prec: 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.' % raise InvalidDecimal('A DecimalField with max_digits %d, decimal_places %d must round to an absolute value less than 10^%d.' %
(field_class.max_digits, field_class.decimal_places, max_prec)) (model_field.max_digits, model_field.decimal_places, max_prec))
val = d val = d
else: else:
val = fld.value val = ogr_field.value
return val return val
def verify_geom(self, geom, model_type): def verify_fk(self, feat, rel_model, rel_mapping):
"Verifies the geometry." """
if self.make_multi(geom.geom_name, model_type): 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.get(**fk_kwargs)
except ObjectDoesNotExist:
if self.strict: raise MissingForeignKey('No %s model found with keyword arguments: %s' % (rel_model.__name__, fk_kwargs))
else: return None
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).
"""
if self.make_multi(geom.geom_type, model_field):
# Constructing a multi-geometry type to contain the single geometry # Constructing a multi-geometry type to contain the single geometry
multi_type = self.MULTI_TYPES[geom.geom_name] multi_type = self.MULTI_TYPES[geom.geom_type.num]
g = OGRGeometry(multi_type) g = OGRGeometry(multi_type)
g.add(geom) g.add(geom)
else: else:
@ -487,6 +505,18 @@ class LayerMapping(object):
# Returning the WKT of the geometry. # Returning the WKT of the geometry.
return g.wkt return g.wkt
#### Other model methods ####
def coord_transform(self):
"Returns the coordinate transformation object."
try:
# Getting the target spatial reference system
target_srs = SpatialRefSys.objects.get(srid=self.geo_col.srid).srs
# Creating the CoordTransform object
return CoordTransform(self.source_srs, target_srs)
except Exception, msg:
raise LayerMapError('Could not translate between the data source and model geometry: %s' % msg)
def geometry_column(self): def geometry_column(self):
"Returns the GeometryColumn model associated with the geographic column." "Returns the GeometryColumn model associated with the geographic column."
# Getting the GeometryColumn object. # Getting the GeometryColumn object.
@ -498,24 +528,23 @@ class LayerMapping(object):
except Exception, msg: except Exception, msg:
raise LayerMapError('Geometry column does not exist for model. (did you run syncdb?):\n %s' % msg) raise LayerMapError('Geometry column does not exist for model. (did you run syncdb?):\n %s' % msg)
def make_multi(self, geom_name, model_type): def make_multi(self, geom_type, model_field):
"Determines whether the geometry should be made into a GeometryCollection." """
return (geom_name in self.MULTI_TYPES) and (model_type.startswith('Multi')) Given the OGRGeomType for a geometry and its associated GeometryField,
determine whether the geometry should be turned into a GeometryCollection.
def map_foreign_key(self, django_field): """
"Handles fields within foreign keys for the given field." return (geom_type.num in self.MULTI_TYPES and
if not django_field.__class__ is ForeignKey: model_field.__class__.__name__ == 'Multi%s' % geom_type.django)
# Returning the field's class name.
return django_field.__class__.__name__
else:
# Otherwise, getting the type of the related field's
# from the Foreign key.
rf = django_field.rel.get_related_field()
return rf.get_internal_type()
def save(self, verbose=False): def save(self, verbose=False):
"Runs the layer mapping on the given SHP file, and saves to the database." """
Saves the contents from the OGR DataSource Layer into the database
according to the mapping dictionary given at initialization. If
the `verbose` keyword is set, information will be printed subsequent
to each model save executed on the database.
"""
# Defining the 'real' save method, utilizing the transaction
# decorator created during initialization.
@self.transaction_decorator @self.transaction_decorator
def _save(): def _save():
num_feat = 0 num_feat = 0
@ -530,7 +559,7 @@ class LayerMapping(object):
# Something borked the validation # Something borked the validation
if self.strict: raise if self.strict: raise
elif not self.silent: elif not self.silent:
print 'Ignoring Feature ID %s because: %s' % (feat.fid, msg) self.pipe.write('Ignoring Feature ID %s because: %s\n' % (feat.fid, msg))
else: else:
# Constructing the model using the keyword args # Constructing the model using the keyword args
if all_prepped: if all_prepped:
@ -561,7 +590,7 @@ class LayerMapping(object):
# Attempting to save. # Attempting to save.
m.save() m.save()
num_saved += 1 num_saved += 1
if verbose: print 'Saved: %s' % m if verbose: self.pipe.write('Saved: %s\n' % m)
except SystemExit: except SystemExit:
raise raise
except Exception, msg: except Exception, msg:
@ -572,17 +601,18 @@ class LayerMapping(object):
if self.strict: if self.strict:
# Bailing out if the `strict` keyword is set. # Bailing out if the `strict` keyword is set.
if not self.silent: if not self.silent:
print 'Failed to save the feature (id: %s) into the model with the keyword arguments:' % feat.fid self.pipe.write('Failed to save the feature (id: %s) into the model with the keyword arguments:\n' % feat.fid)
print kwargs self.pipe.write('%s\n' % kwargs)
raise raise
elif not self.silent: elif not self.silent:
print 'Failed to save %s:\n %s\nContinuing' % (kwargs, msg) self.pipe.write('Failed to save %s:\n %s\nContinuing\n' % (kwargs, msg))
else: else:
print 'Skipping %s due to missing relation.' % kwargs if not self.silent: self.pipe.write('Skipping due to missing relation:\n%s\n' % kwargs)
# Printing progress information, if requested. # Printing progress information, if requested.
if self.progress and num_feat % self.interval == 0: if self.progress and num_feat % self.interval == 0:
print 'Processed %d features, saved %d ...' % (num_feat, num_saved) self.pipe.write('Processed %d features, saved %d ...\n' % (num_feat, num_saved))
# Calling our defined function, which will use the specified # Calling our defined function, which will use the specified
# trasaction mode. # trasaction mode.