Index: django/contrib/gis/measure.py
===================================================================
--- django/contrib/gis/measure.py	(revision 0)
+++ django/contrib/gis/measure.py	(revision 0)
@@ -0,0 +1,213 @@
+"""
+Distance and Area objects to allow for sensible and convienient calculation 
+and conversions.
+
+Inspired by GeoPy (http://exogen.case.edu/projects/geopy/)
+and Geoff Biggs' PhD work on dimensioned units for robotics.
+
+Copyright (c) 2007, Robert Coup <robert.coup@onetrackmind.co.nz>
+"""
+
+from math import *
+from decimal import Decimal
+
+class Distance(object):
+    UNITS = {
+        'm': 1.0,
+        'km': 1000.0,
+        'mi': 1609.344,
+        'ft': 0.3048,
+        'yd': 0.9144,
+        'nm': 1852.0,
+    }
+
+    def __init__(self, default_unit=None, **kwargs):
+        self.m = 0.0
+        self._default_unit = 'm'
+        
+        for unit,value in kwargs.items():
+            if unit in self.UNITS:
+                self.m += self.UNITS[unit] * value
+                self._default_unit = unit
+            else:
+                raise AttributeError("Unknown unit type: " + unit)
+
+        if default_unit:
+            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: " + 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 __cmp__(self, other):
+        if isinstance(other, Distance):
+            return cmp(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):
+        if isinstance(other, (int, float, long, Decimal)):
+            return Distance(default_unit=self._default_unit, m=(self.m * float(other)))
+        elif isinstance(other, Distance):
+            return Area(default_unit='sq_' + self._default_unit, sq_m=(self.m * other.m))
+        else:
+            raise TypeError("Distance must be multiplied with number or Distance")
+    
+    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 __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)
+
+class Area(object):
+    UNITS = {
+        'sq_m': 1.0,
+        'sq_km': 1000000.0,
+        'sq_mi': 2589988.110336,
+        'sq_ft': 0.09290304,
+        'sq_yd': 0.83612736,
+        'sq_nm': 3429904.0,
+    }
+
+    def __init__(self, default_unit=None, **kwargs):
+        self.sq_m = 0.0
+        self._default_unit = 'sq_m'
+        
+        for unit,value in kwargs.items():
+            if unit in self.UNITS:
+                self.sq_m += self.UNITS[unit] * value
+                self._default_unit = unit
+            else:
+                raise AttributeError("Unknown unit type: " + unit)
+
+        if default_unit:
+            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 __cmp__(self, other):
+        if isinstance(other, Area):
+            return cmp(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 __div__(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 divided with number")
+
+    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
+D = Distance
+A = Area
Index: django/contrib/gis/tests/test_measure.py
===================================================================
--- django/contrib/gis/tests/test_measure.py	(revision 0)
+++ django/contrib/gis/tests/test_measure.py	(revision 0)
@@ -0,0 +1,315 @@
+"""
+Distance and Area objects to allow for sensible and convienient calculation 
+and conversions. Here are some tests.
+"""
+
+import unittest
+from django.contrib.gis.measure import Distance, Area, D, A
+
+class DistanceTest(unittest.TestCase):
+    "Testing the Distance object"
+
+    def testInit(self):
+        "Testing initialisation from valid units"
+        d = Distance(m=100)
+        self.assertEqual(d.m, 100)
+
+        d = D(m=100)
+        self.assertEqual(d.m, 100)
+        
+        d = D(nm=100)
+        self.assertEqual(d.m, 185200)
+    
+    def testInitInvalid(self):
+        "Testing initialisation from invalid units"
+        self.assertRaises(AttributeError, D, banana=100)
+
+    def testAccess(self):
+        "Testing access in different units"
+        d = D(m=100)
+        self.assertEqual(d.km, 0.1)
+        self.assertAlmostEqual(d.ft, 328.084, 3)
+    
+    def testAccessInvalid(self):
+        "Testing access in invalid units"
+        d = D(m=100)
+        self.failIf(hasattr(d, 'banana'))
+
+    def testAddition(self):
+        "Test addition & subtraction"
+        d1 = D(m=100)
+        d2 = D(m=200)
+
+        d3 = d1 + d2
+        self.assertEqual(d3.m, 300)
+        d3 += d1
+        self.assertEqual(d3.m, 400)
+        
+        d4 = d1 - d2
+        self.assertEqual(d4.m, -100)
+        d4 -= d1
+        self.assertEqual(d4.m, -200)
+        
+        try:
+            d5 = d1 + 1
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Distance + number should raise TypeError')
+
+        try:
+            d5 = d1 - 1
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Distance - number should raise TypeError')
+
+        try:
+            d1 += 1
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Distance += number should raise TypeError')
+
+        try:
+            d1 -= 1
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Distance -= number should raise TypeError')
+            
+    def testMultiplication(self):
+        "Test multiplication & division"
+        d1 = D(m=100)
+
+        d3 = d1 * 2
+        self.assertEqual(d3.m, 200)
+        d3 *= 5
+        self.assertEqual(d3.m, 1000)
+        
+        d4 = d1 / 2
+        self.assertEqual(d4.m, 50)
+        d4 /= 5
+        self.assertEqual(d4.m, 10)
+        
+        a5 = d1 * D(m=10)
+        self.assert_(isinstance(a5, Area))
+        self.assertEqual(a5.sq_m, 100*10)
+
+        try:
+            d1 *= D(m=1)
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Distance *= Distance should raise TypeError')
+            
+        try:
+            d5 = d1 / D(m=1)
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Distance / Distance should raise TypeError')
+
+        try:
+            d1 /= D(m=1)
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Distance /= Distance should raise TypeError')
+
+    def testUnitConversions(self):
+        "Testing default units during maths"
+        d1 = D(m=100)
+        d2 = D(km=1)
+
+        d3 = d1 + d2
+        self.assertEqual(d3._default_unit, 'm')
+        d4 = d2 + d1
+        self.assertEqual(d4._default_unit, 'km')
+        d5 = d1 * 2
+        self.assertEqual(d5._default_unit, 'm')
+        d6 = d1 / 2
+        self.assertEqual(d6._default_unit, 'm')
+    
+    def testComparisons(self):
+        "Testing comparisons"
+        d1 = D(m=100)
+        d2 = D(km=1)
+        d3 = D(km=0)
+        
+        self.assert_(d2 > d1)
+        self.assert_(d1 == d1)
+        self.assert_(d1 < d2)
+        self.failIf(d3)
+        
+    def testUnitsStr(self):
+        "Testing conversion to strings"
+        d1 = D(m=100)
+        d2 = D(km=3.5)
+        
+        self.assertEqual(str(d1), '100.0 m')
+        self.assertEqual(str(d2), '3.5 km')
+        self.assertEqual(repr(d1), 'Distance(m=100.0)')
+        self.assertEqual(repr(d2), 'Distance(km=3.5)')
+
+class AreaTest(unittest.TestCase):
+    "Testing the Area object"
+
+    def testInit(self):
+        "Testing initialisation from valid units"
+        a = Area(sq_m=100)
+        self.assertEqual(a.sq_m, 100)
+
+        a = A(sq_m=100)
+        self.assertEqual(a.sq_m, 100)
+
+        a = A(sq_mi=100)
+        self.assertEqual(a.sq_m, 258998811.0336)
+    
+    def testInitInvaliA(self):
+        "Testing initialisation from invalid units"
+        self.assertRaises(AttributeError, A, banana=100)
+
+    def testAccess(self):
+        "Testing access in different units"
+        a = A(sq_m=100)
+        self.assertEqual(a.sq_km, 0.0001)
+        self.assertAlmostEqual(a.sq_ft, 1076.391, 3)
+    
+    def testAccessInvaliA(self):
+        "Testing access in invalid units"
+        a = A(sq_m=100)
+        self.failIf(hasattr(a, 'banana'))
+
+    def testAddition(self):
+        "Test addition & subtraction"
+        a1 = A(sq_m=100)
+        a2 = A(sq_m=200)
+
+        a3 = a1 + a2
+        self.assertEqual(a3.sq_m, 300)
+        a3 += a1
+        self.assertEqual(a3.sq_m, 400)
+        
+        a4 = a1 - a2
+        self.assertEqual(a4.sq_m, -100)
+        a4 -= a1
+        self.assertEqual(a4.sq_m, -200)
+        
+        try:
+            a5 = a1 + 1
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Area + number should raise TypeError')
+
+        try:
+            a5 = a1 - 1
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Area - number should raise TypeError')
+
+        try:
+            a1 += 1
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Area += number should raise TypeError')
+
+        try:
+            a1 -= 1
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Area -= number should raise TypeError')
+            
+    def testMultiplication(self):
+        "Test multiplication & division"
+        a1 = A(sq_m=100)
+
+        a3 = a1 * 2
+        self.assertEqual(a3.sq_m, 200)
+        a3 *= 5
+        self.assertEqual(a3.sq_m, 1000)
+        
+        a4 = a1 / 2
+        self.assertEqual(a4.sq_m, 50)
+        a4 /= 5
+        self.assertEqual(a4.sq_m, 10)
+        
+        try:
+            a5 = a1 * A(sq_m=1)
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Area * Area should raise TypeError')
+
+        try:
+            a1 *= A(sq_m=1)
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Area *= Area should raise TypeError')
+            
+        try:
+            a5 = a1 / A(sq_m=1)
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Area / Area should raise TypeError')
+
+        try:
+            a1 /= A(sq_m=1)
+        except TypeError, e:
+            pass
+        else:
+            self.fail('Area /= Area should raise TypeError')
+
+    def testUnitConversions(self):
+        "Testing default units during maths"
+        a1 = A(sq_m=100)
+        a2 = A(sq_km=1)
+
+        a3 = a1 + a2
+        self.assertEqual(a3._default_unit, 'sq_m')
+        a4 = a2 + a1
+        self.assertEqual(a4._default_unit, 'sq_km')
+        a5 = a1 * 2
+        self.assertEqual(a5._default_unit, 'sq_m')
+        a6 = a1 / 2
+        self.assertEqual(a6._default_unit, 'sq_m')
+    
+    def testComparisons(self):
+        "Testing comparisons"
+        a1 = A(sq_m=100)
+        a2 = A(sq_km=1)
+        a3 = A(sq_km=0)
+        
+        self.assert_(a2 > a1)
+        self.assert_(a1 == a1)
+        self.assert_(a1 < a2)
+        self.failIf(a3)
+        
+    def testUnitsStr(self):
+        "Testing conversion to strings"
+        a1 = A(sq_m=100)
+        a2 = A(sq_km=3.5)
+        
+        self.assertEqual(str(a1), '100.0 sq_m')
+        self.assertEqual(str(a2), '3.5 sq_km')
+        self.assertEqual(repr(a1), 'Area(sq_m=100.0)')
+        self.assertEqual(repr(a2), 'Area(sq_km=3.5)')
+
+        
+def suite():
+    s = unittest.TestSuite()
+    s.addTest(unittest.makeSuite(DistanceTest))
+    s.addTest(unittest.makeSuite(AreaTest))
+    return s
+
+def run(verbosity=2):
+    unittest.TextTestRunner(verbosity=verbosity).run(suite())
+
+if __name__=="__main__":
+    run()
\ No newline at end of file
