Skip to content

Commit

Permalink
Fixed #17754 -- Refactored gis.measure
Browse files Browse the repository at this point in the history
This refactoring does allow much easier MeasureBase subclassing.
Many thanks to Ricardo di Virgilio for the initial patch.
  • Loading branch information
claudep committed Jun 14, 2012
1 parent fe873e2 commit 4d46106
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 178 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ answer newbie questions, and generally made Django that much better:
Rajesh Dhawan <rajesh.dhawan@gmail.com> Rajesh Dhawan <rajesh.dhawan@gmail.com>
Sander Dijkhuis <sander.dijkhuis@gmail.com> Sander Dijkhuis <sander.dijkhuis@gmail.com>
Jordan Dimov <s3x3y1@gmail.com> Jordan Dimov <s3x3y1@gmail.com>
Riccardo Di Virgilio
Nebojša Dorđević Nebojša Dorđević
dne@mayonnaise.net dne@mayonnaise.net
dready <wil@mojipage.com> dready <wil@mojipage.com>
Expand Down
313 changes: 139 additions & 174 deletions django/contrib/gis/measure.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
Distance and Area objects to allow for sensible and convienient calculation Distance and Area objects to allow for sensible and convienient calculation
and conversions. and conversions.
Authors: Robert Coup, Justin Bronn Authors: Robert Coup, Justin Bronn, Riccardo Di Virgilio
Inspired by GeoPy (http://exogen.case.edu/projects/geopy/) Inspired by GeoPy (http://exogen.case.edu/projects/geopy/)
and Geoff Biggs' PhD work on dimensioned units for robotics. and Geoff Biggs' PhD work on dimensioned units for robotics.
Expand All @@ -40,13 +40,134 @@


from django.utils.functional import total_ordering from django.utils.functional import total_ordering


NUMERIC_TYPES = (int, float, long, Decimal)
AREA_PREFIX = "sq_"

def pretty_name(obj):
return obj.__name__ if obj.__class__ == type else obj.__class__.__name__


@total_ordering
class MeasureBase(object): class MeasureBase(object):
STANDARD_UNIT = None
ALIAS = {}
UNITS = {}
LALIAS = {}

def __init__(self, default_unit=None, **kwargs):
value, self._default_unit = self.default_units(kwargs)
setattr(self, self.STANDARD_UNIT, value)
if default_unit and isinstance(default_unit, basestring):
self._default_unit = default_unit

def _get_standard(self):
return getattr(self, self.STANDARD_UNIT)

def _set_standard(self, value):
setattr(self, self.STANDARD_UNIT, value)

standard = property(_get_standard, _set_standard)

def __getattr__(self, name):
if name in self.UNITS:
return self.standard / self.UNITS[name]
else:
raise AttributeError('Unknown unit type: %s' % name)

def __repr__(self):
return '%s(%s=%s)' % (pretty_name(self), self._default_unit,
getattr(self, self._default_unit))

def __str__(self):
return '%s %s' % (getattr(self, self._default_unit), self._default_unit)

# **** Comparison methods ****

def __eq__(self, other):
if isinstance(other, self.__class__):
return self.standard == other.standard
else:
return NotImplemented

def __lt__(self, other):
if isinstance(other, self.__class__):
return self.standard < other.standard
else:
return NotImplemented

# **** Operators methods ****

def __add__(self, other):
if isinstance(other, self.__class__):
return self.__class__(default_unit=self._default_unit,
**{self.STANDARD_UNIT: (self.standard + other.standard)})
else:
raise TypeError('%(class)s must be added with %(class)s' % {"class":pretty_name(self)})

def __iadd__(self, other):
if isinstance(other, self.__class__):
self.standard += other.standard
return self
else:
raise TypeError('%(class)s must be added with %(class)s' % {"class":pretty_name(self)})

def __sub__(self, other):
if isinstance(other, self.__class__):
return self.__class__(default_unit=self._default_unit,
**{self.STANDARD_UNIT: (self.standard - other.standard)})
else:
raise TypeError('%(class)s must be subtracted from %(class)s' % {"class":pretty_name(self)})

def __isub__(self, other):
if isinstance(other, self.__class__):
self.standard -= other.standard
return self
else:
raise TypeError('%(class)s must be subtracted from %(class)s' % {"class":pretty_name(self)})

def __mul__(self, other):
if isinstance(other, NUMERIC_TYPES):
return self.__class__(default_unit=self._default_unit,
**{self.STANDARD_UNIT: (self.standard * other)})
else:
raise TypeError('%(class)s must be multiplied with number' % {"class":pretty_name(self)})

def __imul__(self, other):
if isinstance(other, NUMERIC_TYPES):
self.standard *= float(other)
return self
else:
raise TypeError('%(class)s must be multiplied with number' % {"class":pretty_name(self)})

def __rmul__(self, other):
return self * other

def __div__(self, other):
if isinstance(other, self.__class__):
return self.standard / other.standard
if isinstance(other, NUMERIC_TYPES):
return self.__class__(default_unit=self._default_unit,
**{self.STANDARD_UNIT: (self.standard / other)})
else:
raise TypeError('%(class)s must be divided with number or %(class)s' % {"class":pretty_name(self)})

def __idiv__(self, other):
if isinstance(other, NUMERIC_TYPES):
self.standard /= float(other)
return self
else:
raise TypeError('%(class)s must be divided with number' % {"class":pretty_name(self)})

def __nonzero__(self):
return bool(self.standard)

def default_units(self, kwargs): def default_units(self, kwargs):
""" """
Return the unit value and the default units specified Return the unit value and the default units specified
from the given keyword arguments dictionary. from the given keyword arguments dictionary.
""" """
val = 0.0 val = 0.0
default_unit = self.STANDARD_UNIT
for unit, value in kwargs.iteritems(): for unit, value in kwargs.iteritems():
if not isinstance(value, float): value = float(value) if not isinstance(value, float): value = float(value)
if unit in self.UNITS: if unit in self.UNITS:
Expand Down Expand Up @@ -86,8 +207,8 @@ def unit_attname(cls, unit_str):
else: else:
raise Exception('Could not find a unit keyword associated with "%s"' % unit_str) raise Exception('Could not find a unit keyword associated with "%s"' % unit_str)


@total_ordering
class Distance(MeasureBase): class Distance(MeasureBase):
STANDARD_UNIT = "m"
UNITS = { UNITS = {
'chain' : 20.1168, 'chain' : 20.1168,
'chain_benoit' : 20.116782, 'chain_benoit' : 20.116782,
Expand Down Expand Up @@ -163,189 +284,33 @@ class Distance(MeasureBase):
} }
LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()]) LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])


def __init__(self, default_unit=None, **kwargs):
# The base unit is in meters.
self.m, self._default_unit = self.default_units(kwargs)
if default_unit and isinstance(default_unit, str):
self._default_unit = default_unit

def __getattr__(self, name):
if name in self.UNITS:
return self.m / self.UNITS[name]
else:
raise AttributeError('Unknown unit type: %s' % name)

def __repr__(self):
return 'Distance(%s=%s)' % (self._default_unit, getattr(self, self._default_unit))

def __str__(self):
return '%s %s' % (getattr(self, self._default_unit), self._default_unit)

def __eq__(self, other):
if isinstance(other, Distance):
return self.m == other.m
else:
return NotImplemented

def __lt__(self, other):
if isinstance(other, Distance):
return self.m < other.m
else:
return NotImplemented

def __add__(self, other):
if isinstance(other, Distance):
return Distance(default_unit=self._default_unit, m=(self.m + other.m))
else:
raise TypeError('Distance must be added with Distance')

def __iadd__(self, other):
if isinstance(other, Distance):
self.m += other.m
return self
else:
raise TypeError('Distance must be added with Distance')

def __sub__(self, other):
if isinstance(other, Distance):
return Distance(default_unit=self._default_unit, m=(self.m - other.m))
else:
raise TypeError('Distance must be subtracted from Distance')

def __isub__(self, other):
if isinstance(other, Distance):
self.m -= other.m
return self
else:
raise TypeError('Distance must be subtracted from Distance')

def __mul__(self, other): def __mul__(self, other):
if isinstance(other, (int, float, long, Decimal)): if isinstance(other, self.__class__):
return Distance(default_unit=self._default_unit, m=(self.m * float(other))) return Area(default_unit=AREA_PREFIX + self._default_unit,
elif isinstance(other, Distance): **{AREA_PREFIX + self.STANDARD_UNIT: (self.standard * other.standard)})
return Area(default_unit='sq_' + self._default_unit, sq_m=(self.m * other.m)) elif isinstance(other, NUMERIC_TYPES):
return self.__class__(default_unit=self._default_unit,
**{self.STANDARD_UNIT: (self.standard * other)})
else: else:
raise TypeError('Distance must be multiplied with number or Distance') raise TypeError('%(distance)s must be multiplied with number or %(distance)s' % {
"distance" : pretty_name(self.__class__),
})


def __imul__(self, other):
if isinstance(other, (int, float, long, Decimal)):
self.m *= float(other)
return self
else:
raise TypeError('Distance must be multiplied with number')


def __rmul__(self, other):
return self * other

def __div__(self, other):
if isinstance(other, (int, float, long, Decimal)):
return Distance(default_unit=self._default_unit, m=(self.m / float(other)))
else:
raise TypeError('Distance must be divided with number')

def __idiv__(self, other):
if isinstance(other, (int, float, long, Decimal)):
self.m /= float(other)
return self
else:
raise TypeError('Distance must be divided with number')

def __nonzero__(self):
return bool(self.m)

@total_ordering
class Area(MeasureBase): class Area(MeasureBase):
STANDARD_UNIT = AREA_PREFIX + Distance.STANDARD_UNIT
# Getting the square units values and the alias dictionary. # Getting the square units values and the alias dictionary.
UNITS = dict([('sq_%s' % k, v ** 2) for k, v in Distance.UNITS.items()]) UNITS = dict([('%s%s' % (AREA_PREFIX, k), v ** 2) for k, v in Distance.UNITS.items()])
ALIAS = dict([(k, 'sq_%s' % v) for k, v in Distance.ALIAS.items()]) ALIAS = dict([(k, '%s%s' % (AREA_PREFIX, v)) for k, v in Distance.ALIAS.items()])
LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()]) LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])


def __init__(self, default_unit=None, **kwargs):
self.sq_m, self._default_unit = self.default_units(kwargs)
if default_unit and isinstance(default_unit, str):
self._default_unit = default_unit

def __getattr__(self, name):
if name in self.UNITS:
return self.sq_m / self.UNITS[name]
else:
raise AttributeError('Unknown unit type: ' + name)

def __repr__(self):
return 'Area(%s=%s)' % (self._default_unit, getattr(self, self._default_unit))

def __str__(self):
return '%s %s' % (getattr(self, self._default_unit), self._default_unit)

def __eq__(self, other):
if isinstance(other, Area):
return self.sq_m == other.sq_m
else:
return NotImplemented

def __lt__(self, other):
if isinstance(other, Area):
return self.sq_m < other.sq_m
else:
return NotImplemented

def __add__(self, other):
if isinstance(other, Area):
return Area(default_unit=self._default_unit, sq_m=(self.sq_m + other.sq_m))
else:
raise TypeError('Area must be added with Area')

def __iadd__(self, other):
if isinstance(other, Area):
self.sq_m += other.sq_m
return self
else:
raise TypeError('Area must be added with Area')

def __sub__(self, other):
if isinstance(other, Area):
return Area(default_unit=self._default_unit, sq_m=(self.sq_m - other.sq_m))
else:
raise TypeError('Area must be subtracted from Area')

def __isub__(self, other):
if isinstance(other, Area):
self.sq_m -= other.sq_m
return self
else:
raise TypeError('Area must be subtracted from Area')

def __mul__(self, other):
if isinstance(other, (int, float, long, Decimal)):
return Area(default_unit=self._default_unit, sq_m=(self.sq_m * float(other)))
else:
raise TypeError('Area must be multiplied with number')

def __imul__(self, other):
if isinstance(other, (int, float, long, Decimal)):
self.sq_m *= float(other)
return self
else:
raise TypeError('Area must be multiplied with number')

def __rmul__(self, other):
return self * other

def __div__(self, other): def __div__(self, other):
if isinstance(other, (int, float, long, Decimal)): if isinstance(other, NUMERIC_TYPES):
return Area(default_unit=self._default_unit, sq_m=(self.sq_m / float(other))) return self.__class__(default_unit=self._default_unit,
**{self.STANDARD_UNIT: (self.standard / other)})
else: else:
raise TypeError('Area must be divided with number') raise TypeError('%(class)s must be divided by a number' % {"class":pretty_name(self)})


def __idiv__(self, other):
if isinstance(other, (int, float, long, Decimal)):
self.sq_m /= float(other)
return self
else:
raise TypeError('Area must be divided with number')

def __nonzero__(self):
return bool(self.sq_m)


# Shortcuts # Shortcuts
D = Distance D = Distance
Expand Down
6 changes: 2 additions & 4 deletions django/contrib/gis/tests/test_measure.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ def testMultiplication(self):
self.assertEqual(d4.m, 50) self.assertEqual(d4.m, 50)
d4 /= 5 d4 /= 5
self.assertEqual(d4.m, 10) self.assertEqual(d4.m, 10)
d5 = d1 / D(m=2)
self.assertEqual(d5, 50)


a5 = d1 * D(m=10) a5 = d1 * D(m=10)
self.assertTrue(isinstance(a5, Area)) self.assertTrue(isinstance(a5, Area))
Expand All @@ -102,10 +104,6 @@ def testMultiplication(self):
d1 *= D(m=1) d1 *= D(m=1)
self.fail('Distance *= Distance should raise TypeError') self.fail('Distance *= Distance should raise TypeError')


with self.assertRaises(TypeError):
d5 = d1 / D(m=1)
self.fail('Distance / Distance should raise TypeError')

with self.assertRaises(TypeError): with self.assertRaises(TypeError):
d1 /= D(m=1) d1 /= D(m=1)
self.fail('Distance /= Distance should raise TypeError') self.fail('Distance /= Distance should raise TypeError')
Expand Down

0 comments on commit 4d46106

Please sign in to comment.