/django/contrib/gis/measure.py
Python | 336 lines | 300 code | 0 blank | 36 comment | 3 complexity | fc0142a37292eb9c41893aabb9a7b12f MD5 | raw file
1# Copyright (c) 2007, Robert Coup <robert.coup@onetrackmind.co.nz> 2# All rights reserved. 3# 4# Redistribution and use in source and binary forms, with or without modification, 5# are permitted provided that the following conditions are met: 6# 7# 1. Redistributions of source code must retain the above copyright notice, 8# this list of conditions and the following disclaimer. 9# 10# 2. Redistributions in binary form must reproduce the above copyright 11# notice, this list of conditions and the following disclaimer in the 12# documentation and/or other materials provided with the distribution. 13# 14# 3. Neither the name of Distance nor the names of its contributors may be used 15# to endorse or promote products derived from this software without 16# specific prior written permission. 17# 18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28# 29""" 30Distance and Area objects to allow for sensible and convienient calculation 31and conversions. 32 33Authors: Robert Coup, Justin Bronn 34 35Inspired by GeoPy (http://exogen.case.edu/projects/geopy/) 36and Geoff Biggs' PhD work on dimensioned units for robotics. 37""" 38__all__ = ['A', 'Area', 'D', 'Distance'] 39from decimal import Decimal 40 41class MeasureBase(object): 42 def default_units(self, kwargs): 43 """ 44 Return the unit value and the default units specified 45 from the given keyword arguments dictionary. 46 """ 47 val = 0.0 48 for unit, value in kwargs.iteritems(): 49 if not isinstance(value, float): value = float(value) 50 if unit in self.UNITS: 51 val += self.UNITS[unit] * value 52 default_unit = unit 53 elif unit in self.ALIAS: 54 u = self.ALIAS[unit] 55 val += self.UNITS[u] * value 56 default_unit = u 57 else: 58 lower = unit.lower() 59 if lower in self.UNITS: 60 val += self.UNITS[lower] * value 61 default_unit = lower 62 elif lower in self.LALIAS: 63 u = self.LALIAS[lower] 64 val += self.UNITS[u] * value 65 default_unit = u 66 else: 67 raise AttributeError('Unknown unit type: %s' % unit) 68 return val, default_unit 69 70 @classmethod 71 def unit_attname(cls, unit_str): 72 """ 73 Retrieves the unit attribute name for the given unit string. 74 For example, if the given unit string is 'metre', 'm' would be returned. 75 An exception is raised if an attribute cannot be found. 76 """ 77 lower = unit_str.lower() 78 if unit_str in cls.UNITS: 79 return unit_str 80 elif lower in cls.UNITS: 81 return lower 82 elif lower in cls.LALIAS: 83 return cls.LALIAS[lower] 84 else: 85 raise Exception('Could not find a unit keyword associated with "%s"' % unit_str) 86 87class Distance(MeasureBase): 88 UNITS = { 89 'chain' : 20.1168, 90 'chain_benoit' : 20.116782, 91 'chain_sears' : 20.1167645, 92 'british_chain_benoit' : 20.1167824944, 93 'british_chain_sears' : 20.1167651216, 94 'british_chain_sears_truncated' : 20.116756, 95 'cm' : 0.01, 96 'british_ft' : 0.304799471539, 97 'british_yd' : 0.914398414616, 98 'clarke_ft' : 0.3047972654, 99 'clarke_link' : 0.201166195164, 100 'fathom' : 1.8288, 101 'ft': 0.3048, 102 'german_m' : 1.0000135965, 103 'gold_coast_ft' : 0.304799710181508, 104 'indian_yd' : 0.914398530744, 105 'inch' : 0.0254, 106 'km': 1000.0, 107 'link' : 0.201168, 108 'link_benoit' : 0.20116782, 109 'link_sears' : 0.20116765, 110 'm': 1.0, 111 'mi': 1609.344, 112 'mm' : 0.001, 113 'nm': 1852.0, 114 'nm_uk' : 1853.184, 115 'rod' : 5.0292, 116 'sears_yd' : 0.91439841, 117 'survey_ft' : 0.304800609601, 118 'um' : 0.000001, 119 'yd': 0.9144, 120 } 121 122 # Unit aliases for `UNIT` terms encountered in Spatial Reference WKT. 123 ALIAS = { 124 'centimeter' : 'cm', 125 'foot' : 'ft', 126 'inches' : 'inch', 127 'kilometer' : 'km', 128 'kilometre' : 'km', 129 'meter' : 'm', 130 'metre' : 'm', 131 'micrometer' : 'um', 132 'micrometre' : 'um', 133 'millimeter' : 'mm', 134 'millimetre' : 'mm', 135 'mile' : 'mi', 136 'yard' : 'yd', 137 'British chain (Benoit 1895 B)' : 'british_chain_benoit', 138 'British chain (Sears 1922)' : 'british_chain_sears', 139 'British chain (Sears 1922 truncated)' : 'british_chain_sears_truncated', 140 'British foot (Sears 1922)' : 'british_ft', 141 'British foot' : 'british_ft', 142 'British yard (Sears 1922)' : 'british_yd', 143 'British yard' : 'british_yd', 144 "Clarke's Foot" : 'clarke_ft', 145 "Clarke's link" : 'clarke_link', 146 'Chain (Benoit)' : 'chain_benoit', 147 'Chain (Sears)' : 'chain_sears', 148 'Foot (International)' : 'ft', 149 'German legal metre' : 'german_m', 150 'Gold Coast foot' : 'gold_coast_ft', 151 'Indian yard' : 'indian_yd', 152 'Link (Benoit)': 'link_benoit', 153 'Link (Sears)': 'link_sears', 154 'Nautical Mile' : 'nm', 155 'Nautical Mile (UK)' : 'nm_uk', 156 'US survey foot' : 'survey_ft', 157 'U.S. Foot' : 'survey_ft', 158 'Yard (Indian)' : 'indian_yd', 159 'Yard (Sears)' : 'sears_yd' 160 } 161 LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()]) 162 163 def __init__(self, default_unit=None, **kwargs): 164 # The base unit is in meters. 165 self.m, self._default_unit = self.default_units(kwargs) 166 if default_unit and isinstance(default_unit, str): 167 self._default_unit = default_unit 168 169 def __getattr__(self, name): 170 if name in self.UNITS: 171 return self.m / self.UNITS[name] 172 else: 173 raise AttributeError('Unknown unit type: %s' % name) 174 175 def __repr__(self): 176 return 'Distance(%s=%s)' % (self._default_unit, getattr(self, self._default_unit)) 177 178 def __str__(self): 179 return '%s %s' % (getattr(self, self._default_unit), self._default_unit) 180 181 def __cmp__(self, other): 182 if isinstance(other, Distance): 183 return cmp(self.m, other.m) 184 else: 185 return NotImplemented 186 187 def __add__(self, other): 188 if isinstance(other, Distance): 189 return Distance(default_unit=self._default_unit, m=(self.m + other.m)) 190 else: 191 raise TypeError('Distance must be added with Distance') 192 193 def __iadd__(self, other): 194 if isinstance(other, Distance): 195 self.m += other.m 196 return self 197 else: 198 raise TypeError('Distance must be added with Distance') 199 200 def __sub__(self, other): 201 if isinstance(other, Distance): 202 return Distance(default_unit=self._default_unit, m=(self.m - other.m)) 203 else: 204 raise TypeError('Distance must be subtracted from Distance') 205 206 def __isub__(self, other): 207 if isinstance(other, Distance): 208 self.m -= other.m 209 return self 210 else: 211 raise TypeError('Distance must be subtracted from Distance') 212 213 def __mul__(self, other): 214 if isinstance(other, (int, float, long, Decimal)): 215 return Distance(default_unit=self._default_unit, m=(self.m * float(other))) 216 elif isinstance(other, Distance): 217 return Area(default_unit='sq_' + self._default_unit, sq_m=(self.m * other.m)) 218 else: 219 raise TypeError('Distance must be multiplied with number or Distance') 220 221 def __imul__(self, other): 222 if isinstance(other, (int, float, long, Decimal)): 223 self.m *= float(other) 224 return self 225 else: 226 raise TypeError('Distance must be multiplied with number') 227 228 def __rmul__(self, other): 229 return self * other 230 231 def __div__(self, other): 232 if isinstance(other, (int, float, long, Decimal)): 233 return Distance(default_unit=self._default_unit, m=(self.m / float(other))) 234 else: 235 raise TypeError('Distance must be divided with number') 236 237 def __idiv__(self, other): 238 if isinstance(other, (int, float, long, Decimal)): 239 self.m /= float(other) 240 return self 241 else: 242 raise TypeError('Distance must be divided with number') 243 244 def __nonzero__(self): 245 return bool(self.m) 246 247class Area(MeasureBase): 248 # Getting the square units values and the alias dictionary. 249 UNITS = dict([('sq_%s' % k, v ** 2) for k, v in Distance.UNITS.items()]) 250 ALIAS = dict([(k, 'sq_%s' % v) for k, v in Distance.ALIAS.items()]) 251 LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()]) 252 253 def __init__(self, default_unit=None, **kwargs): 254 self.sq_m, self._default_unit = self.default_units(kwargs) 255 if default_unit and isinstance(default_unit, str): 256 self._default_unit = default_unit 257 258 def __getattr__(self, name): 259 if name in self.UNITS: 260 return self.sq_m / self.UNITS[name] 261 else: 262 raise AttributeError('Unknown unit type: ' + name) 263 264 def __repr__(self): 265 return 'Area(%s=%s)' % (self._default_unit, getattr(self, self._default_unit)) 266 267 def __str__(self): 268 return '%s %s' % (getattr(self, self._default_unit), self._default_unit) 269 270 def __cmp__(self, other): 271 if isinstance(other, Area): 272 return cmp(self.sq_m, other.sq_m) 273 else: 274 return NotImplemented 275 276 def __add__(self, other): 277 if isinstance(other, Area): 278 return Area(default_unit=self._default_unit, sq_m=(self.sq_m + other.sq_m)) 279 else: 280 raise TypeError('Area must be added with Area') 281 282 def __iadd__(self, other): 283 if isinstance(other, Area): 284 self.sq_m += other.sq_m 285 return self 286 else: 287 raise TypeError('Area must be added with Area') 288 289 def __sub__(self, other): 290 if isinstance(other, Area): 291 return Area(default_unit=self._default_unit, sq_m=(self.sq_m - other.sq_m)) 292 else: 293 raise TypeError('Area must be subtracted from Area') 294 295 def __isub__(self, other): 296 if isinstance(other, Area): 297 self.sq_m -= other.sq_m 298 return self 299 else: 300 raise TypeError('Area must be subtracted from Area') 301 302 def __mul__(self, other): 303 if isinstance(other, (int, float, long, Decimal)): 304 return Area(default_unit=self._default_unit, sq_m=(self.sq_m * float(other))) 305 else: 306 raise TypeError('Area must be multiplied with number') 307 308 def __imul__(self, other): 309 if isinstance(other, (int, float, long, Decimal)): 310 self.sq_m *= float(other) 311 return self 312 else: 313 raise TypeError('Area must be multiplied with number') 314 315 def __rmul__(self, other): 316 return self * other 317 318 def __div__(self, other): 319 if isinstance(other, (int, float, long, Decimal)): 320 return Area(default_unit=self._default_unit, sq_m=(self.sq_m / float(other))) 321 else: 322 raise TypeError('Area must be divided with number') 323 324 def __idiv__(self, other): 325 if isinstance(other, (int, float, long, Decimal)): 326 self.sq_m /= float(other) 327 return self 328 else: 329 raise TypeError('Area must be divided with number') 330 331 def __nonzero__(self): 332 return bool(self.sq_m) 333 334# Shortcuts 335D = Distance 336A = Area