Ticket #4797: gis_measure_0_r5634.diff
File gis_measure_0_r5634.diff, 15.9 KB (added by robert.coup@…, 9 years ago) 


django/contrib/gis/measure.py
1 """ 2 Distance and Area objects to allow for sensible and convienient calculation 3 and conversions. 4 5 Inspired by GeoPy (http://exogen.case.edu/projects/geopy/) 6 and Geoff Biggs' PhD work on dimensioned units for robotics. 7 8 Copyright (c) 2007, Robert Coup <robert.coup@onetrackmind.co.nz> 9 """ 10 11 from math import * 12 from decimal import Decimal 13 14 class Distance(object): 15 UNITS = { 16 'm': 1.0, 17 'km': 1000.0, 18 'mi': 1609.344, 19 'ft': 0.3048, 20 'yd': 0.9144, 21 'nm': 1852.0, 22 } 23 24 def __init__(self, default_unit=None, **kwargs): 25 self.m = 0.0 26 self._default_unit = 'm' 27 28 for unit,value in kwargs.items(): 29 if unit in self.UNITS: 30 self.m += self.UNITS[unit] * value 31 self._default_unit = unit 32 else: 33 raise AttributeError("Unknown unit type: " + unit) 34 35 if default_unit: 36 self._default_unit = default_unit 37 38 def __getattr__(self, name): 39 if name in self.UNITS: 40 return self.m / self.UNITS[name] 41 else: 42 raise AttributeError("Unknown unit type: " + name) 43 44 def __repr__(self): 45 return "Distance(%s=%s)" % (self._default_unit, getattr(self, self._default_unit)) 46 47 def __str__(self): 48 return "%s %s" % (getattr(self, self._default_unit), self._default_unit) 49 50 def __cmp__(self, other): 51 if isinstance(other, Distance): 52 return cmp(self.m, other.m) 53 else: 54 return NotImplemented 55 56 def __add__(self, other): 57 if isinstance(other, Distance): 58 return Distance(default_unit=self._default_unit, m=(self.m + other.m)) 59 else: 60 raise TypeError("Distance must be added with Distance") 61 62 def __iadd__(self, other): 63 if isinstance(other, Distance): 64 self.m += other.m 65 return self 66 else: 67 raise TypeError("Distance must be added with Distance") 68 69 def __sub__(self, other): 70 if isinstance(other, Distance): 71 return Distance(default_unit=self._default_unit, m=(self.m  other.m)) 72 else: 73 raise TypeError("Distance must be subtracted from Distance") 74 75 def __isub__(self, other): 76 if isinstance(other, Distance): 77 self.m = other.m 78 return self 79 else: 80 raise TypeError("Distance must be subtracted from Distance") 81 82 def __mul__(self, other): 83 if isinstance(other, (int, float, long, Decimal)): 84 return Distance(default_unit=self._default_unit, m=(self.m * float(other))) 85 elif isinstance(other, Distance): 86 return Area(default_unit='sq_' + self._default_unit, sq_m=(self.m * other.m)) 87 else: 88 raise TypeError("Distance must be multiplied with number or Distance") 89 90 def __imul__(self, other): 91 if isinstance(other, (int, float, long, Decimal)): 92 self.m *= float(other) 93 return self 94 else: 95 raise TypeError("Distance must be multiplied with number") 96 97 def __div__(self, other): 98 if isinstance(other, (int, float, long, Decimal)): 99 return Distance(default_unit=self._default_unit, m=(self.m / float(other))) 100 else: 101 raise TypeError("Distance must be divided with number") 102 103 def __idiv__(self, other): 104 if isinstance(other, (int, float, long, Decimal)): 105 self.m /= float(other) 106 return self 107 else: 108 raise TypeError("Distance must be divided with number") 109 110 def __nonzero__(self): 111 return bool(self.m) 112 113 class Area(object): 114 UNITS = { 115 'sq_m': 1.0, 116 'sq_km': 1000000.0, 117 'sq_mi': 2589988.110336, 118 'sq_ft': 0.09290304, 119 'sq_yd': 0.83612736, 120 'sq_nm': 3429904.0, 121 } 122 123 def __init__(self, default_unit=None, **kwargs): 124 self.sq_m = 0.0 125 self._default_unit = 'sq_m' 126 127 for unit,value in kwargs.items(): 128 if unit in self.UNITS: 129 self.sq_m += self.UNITS[unit] * value 130 self._default_unit = unit 131 else: 132 raise AttributeError("Unknown unit type: " + unit) 133 134 if default_unit: 135 self._default_unit = default_unit 136 137 def __getattr__(self, name): 138 if name in self.UNITS: 139 return self.sq_m / self.UNITS[name] 140 else: 141 raise AttributeError("Unknown unit type: " + name) 142 143 def __repr__(self): 144 return "Area(%s=%s)" % (self._default_unit, getattr(self, self._default_unit)) 145 146 def __str__(self): 147 return "%s %s" % (getattr(self, self._default_unit), self._default_unit) 148 149 def __cmp__(self, other): 150 if isinstance(other, Area): 151 return cmp(self.sq_m, other.sq_m) 152 else: 153 return NotImplemented 154 155 def __add__(self, other): 156 if isinstance(other, Area): 157 return Area(default_unit=self._default_unit, sq_m=(self.sq_m + other.sq_m)) 158 else: 159 raise TypeError("Area must be added with Area") 160 161 def __iadd__(self, other): 162 if isinstance(other, Area): 163 self.sq_m += other.sq_m 164 return self 165 else: 166 raise TypeError("Area must be added with Area") 167 168 def __sub__(self, other): 169 if isinstance(other, Area): 170 return Area(default_unit=self._default_unit, sq_m=(self.sq_m  other.sq_m)) 171 else: 172 raise TypeError("Area must be subtracted from Area") 173 174 def __isub__(self, other): 175 if isinstance(other, Area): 176 self.sq_m = other.sq_m 177 return self 178 else: 179 raise TypeError("Area must be subtracted from Area") 180 181 def __mul__(self, other): 182 if isinstance(other, (int, float, long, Decimal)): 183 return Area(default_unit=self._default_unit, sq_m=(self.sq_m * float(other))) 184 else: 185 raise TypeError("Area must be multiplied with number") 186 187 def __imul__(self, other): 188 if isinstance(other, (int, float, long, Decimal)): 189 self.sq_m *= float(other) 190 return self 191 else: 192 raise TypeError("Area must be multiplied with number") 193 194 def __div__(self, other): 195 if isinstance(other, (int, float, long, Decimal)): 196 return Area(default_unit=self._default_unit, sq_m=(self.sq_m / float(other))) 197 else: 198 raise TypeError("Area must be divided with number") 199 200 def __idiv__(self, other): 201 if isinstance(other, (int, float, long, Decimal)): 202 self.sq_m /= float(other) 203 return self 204 else: 205 raise TypeError("Area must be divided with number") 206 207 def __nonzero__(self): 208 return bool(self.sq_m) 209 210 211 # Shortcuts 212 D = Distance 213 A = Area 
django/contrib/gis/tests/test_measure.py
1 """ 2 Distance and Area objects to allow for sensible and convienient calculation 3 and conversions. Here are some tests. 4 """ 5 6 import unittest 7 from django.contrib.gis.measure import Distance, Area, D, A 8 9 class DistanceTest(unittest.TestCase): 10 "Testing the Distance object" 11 12 def testInit(self): 13 "Testing initialisation from valid units" 14 d = Distance(m=100) 15 self.assertEqual(d.m, 100) 16 17 d = D(m=100) 18 self.assertEqual(d.m, 100) 19 20 d = D(nm=100) 21 self.assertEqual(d.m, 185200) 22 23 def testInitInvalid(self): 24 "Testing initialisation from invalid units" 25 self.assertRaises(AttributeError, D, banana=100) 26 27 def testAccess(self): 28 "Testing access in different units" 29 d = D(m=100) 30 self.assertEqual(d.km, 0.1) 31 self.assertAlmostEqual(d.ft, 328.084, 3) 32 33 def testAccessInvalid(self): 34 "Testing access in invalid units" 35 d = D(m=100) 36 self.failIf(hasattr(d, 'banana')) 37 38 def testAddition(self): 39 "Test addition & subtraction" 40 d1 = D(m=100) 41 d2 = D(m=200) 42 43 d3 = d1 + d2 44 self.assertEqual(d3.m, 300) 45 d3 += d1 46 self.assertEqual(d3.m, 400) 47 48 d4 = d1  d2 49 self.assertEqual(d4.m, 100) 50 d4 = d1 51 self.assertEqual(d4.m, 200) 52 53 try: 54 d5 = d1 + 1 55 except TypeError, e: 56 pass 57 else: 58 self.fail('Distance + number should raise TypeError') 59 60 try: 61 d5 = d1  1 62 except TypeError, e: 63 pass 64 else: 65 self.fail('Distance  number should raise TypeError') 66 67 try: 68 d1 += 1 69 except TypeError, e: 70 pass 71 else: 72 self.fail('Distance += number should raise TypeError') 73 74 try: 75 d1 = 1 76 except TypeError, e: 77 pass 78 else: 79 self.fail('Distance = number should raise TypeError') 80 81 def testMultiplication(self): 82 "Test multiplication & division" 83 d1 = D(m=100) 84 85 d3 = d1 * 2 86 self.assertEqual(d3.m, 200) 87 d3 *= 5 88 self.assertEqual(d3.m, 1000) 89 90 d4 = d1 / 2 91 self.assertEqual(d4.m, 50) 92 d4 /= 5 93 self.assertEqual(d4.m, 10) 94 95 a5 = d1 * D(m=10) 96 self.assert_(isinstance(a5, Area)) 97 self.assertEqual(a5.sq_m, 100*10) 98 99 try: 100 d1 *= D(m=1) 101 except TypeError, e: 102 pass 103 else: 104 self.fail('Distance *= Distance should raise TypeError') 105 106 try: 107 d5 = d1 / D(m=1) 108 except TypeError, e: 109 pass 110 else: 111 self.fail('Distance / Distance should raise TypeError') 112 113 try: 114 d1 /= D(m=1) 115 except TypeError, e: 116 pass 117 else: 118 self.fail('Distance /= Distance should raise TypeError') 119 120 def testUnitConversions(self): 121 "Testing default units during maths" 122 d1 = D(m=100) 123 d2 = D(km=1) 124 125 d3 = d1 + d2 126 self.assertEqual(d3._default_unit, 'm') 127 d4 = d2 + d1 128 self.assertEqual(d4._default_unit, 'km') 129 d5 = d1 * 2 130 self.assertEqual(d5._default_unit, 'm') 131 d6 = d1 / 2 132 self.assertEqual(d6._default_unit, 'm') 133 134 def testComparisons(self): 135 "Testing comparisons" 136 d1 = D(m=100) 137 d2 = D(km=1) 138 d3 = D(km=0) 139 140 self.assert_(d2 > d1) 141 self.assert_(d1 == d1) 142 self.assert_(d1 < d2) 143 self.failIf(d3) 144 145 def testUnitsStr(self): 146 "Testing conversion to strings" 147 d1 = D(m=100) 148 d2 = D(km=3.5) 149 150 self.assertEqual(str(d1), '100.0 m') 151 self.assertEqual(str(d2), '3.5 km') 152 self.assertEqual(repr(d1), 'Distance(m=100.0)') 153 self.assertEqual(repr(d2), 'Distance(km=3.5)') 154 155 class AreaTest(unittest.TestCase): 156 "Testing the Area object" 157 158 def testInit(self): 159 "Testing initialisation from valid units" 160 a = Area(sq_m=100) 161 self.assertEqual(a.sq_m, 100) 162 163 a = A(sq_m=100) 164 self.assertEqual(a.sq_m, 100) 165 166 a = A(sq_mi=100) 167 self.assertEqual(a.sq_m, 258998811.0336) 168 169 def testInitInvaliA(self): 170 "Testing initialisation from invalid units" 171 self.assertRaises(AttributeError, A, banana=100) 172 173 def testAccess(self): 174 "Testing access in different units" 175 a = A(sq_m=100) 176 self.assertEqual(a.sq_km, 0.0001) 177 self.assertAlmostEqual(a.sq_ft, 1076.391, 3) 178 179 def testAccessInvaliA(self): 180 "Testing access in invalid units" 181 a = A(sq_m=100) 182 self.failIf(hasattr(a, 'banana')) 183 184 def testAddition(self): 185 "Test addition & subtraction" 186 a1 = A(sq_m=100) 187 a2 = A(sq_m=200) 188 189 a3 = a1 + a2 190 self.assertEqual(a3.sq_m, 300) 191 a3 += a1 192 self.assertEqual(a3.sq_m, 400) 193 194 a4 = a1  a2 195 self.assertEqual(a4.sq_m, 100) 196 a4 = a1 197 self.assertEqual(a4.sq_m, 200) 198 199 try: 200 a5 = a1 + 1 201 except TypeError, e: 202 pass 203 else: 204 self.fail('Area + number should raise TypeError') 205 206 try: 207 a5 = a1  1 208 except TypeError, e: 209 pass 210 else: 211 self.fail('Area  number should raise TypeError') 212 213 try: 214 a1 += 1 215 except TypeError, e: 216 pass 217 else: 218 self.fail('Area += number should raise TypeError') 219 220 try: 221 a1 = 1 222 except TypeError, e: 223 pass 224 else: 225 self.fail('Area = number should raise TypeError') 226 227 def testMultiplication(self): 228 "Test multiplication & division" 229 a1 = A(sq_m=100) 230 231 a3 = a1 * 2 232 self.assertEqual(a3.sq_m, 200) 233 a3 *= 5 234 self.assertEqual(a3.sq_m, 1000) 235 236 a4 = a1 / 2 237 self.assertEqual(a4.sq_m, 50) 238 a4 /= 5 239 self.assertEqual(a4.sq_m, 10) 240 241 try: 242 a5 = a1 * A(sq_m=1) 243 except TypeError, e: 244 pass 245 else: 246 self.fail('Area * Area should raise TypeError') 247 248 try: 249 a1 *= A(sq_m=1) 250 except TypeError, e: 251 pass 252 else: 253 self.fail('Area *= Area should raise TypeError') 254 255 try: 256 a5 = a1 / A(sq_m=1) 257 except TypeError, e: 258 pass 259 else: 260 self.fail('Area / Area should raise TypeError') 261 262 try: 263 a1 /= A(sq_m=1) 264 except TypeError, e: 265 pass 266 else: 267 self.fail('Area /= Area should raise TypeError') 268 269 def testUnitConversions(self): 270 "Testing default units during maths" 271 a1 = A(sq_m=100) 272 a2 = A(sq_km=1) 273 274 a3 = a1 + a2 275 self.assertEqual(a3._default_unit, 'sq_m') 276 a4 = a2 + a1 277 self.assertEqual(a4._default_unit, 'sq_km') 278 a5 = a1 * 2 279 self.assertEqual(a5._default_unit, 'sq_m') 280 a6 = a1 / 2 281 self.assertEqual(a6._default_unit, 'sq_m') 282 283 def testComparisons(self): 284 "Testing comparisons" 285 a1 = A(sq_m=100) 286 a2 = A(sq_km=1) 287 a3 = A(sq_km=0) 288 289 self.assert_(a2 > a1) 290 self.assert_(a1 == a1) 291 self.assert_(a1 < a2) 292 self.failIf(a3) 293 294 def testUnitsStr(self): 295 "Testing conversion to strings" 296 a1 = A(sq_m=100) 297 a2 = A(sq_km=3.5) 298 299 self.assertEqual(str(a1), '100.0 sq_m') 300 self.assertEqual(str(a2), '3.5 sq_km') 301 self.assertEqual(repr(a1), 'Area(sq_m=100.0)') 302 self.assertEqual(repr(a2), 'Area(sq_km=3.5)') 303 304 305 def suite(): 306 s = unittest.TestSuite() 307 s.addTest(unittest.makeSuite(DistanceTest)) 308 s.addTest(unittest.makeSuite(AreaTest)) 309 return s 310 311 def run(verbosity=2): 312 unittest.TextTestRunner(verbosity=verbosity).run(suite()) 313 314 if __name__=="__main__": 315 run() 316 No newline at end of file