~tim-clicks/sahana-eden/minor

« back to all changes in this revision

Viewing changes to modules/geopy/point.py

  • Committer: Fran Boon
  • Date: 2010-02-19 01:03:42 UTC
  • Revision ID: flavour@partyvibe.com-20100219010342-gkgpxt681ikz6xd8
Include geopy, amend to look for simplejson in gluon not django, add OSM geocoder from tcarobruce

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import re
 
2
from itertools import islice
 
3
from geopy import util, units, format
 
4
 
 
5
class Point(object):
 
6
    """
 
7
    A geodetic point with latitude, longitude, and altitude.
 
8
    
 
9
    Latitude and longitude are floating point values in degrees.
 
10
    Altitude is a floating point value in kilometers. The reference level
 
11
    is never considered and is thus application dependent, so be consistent!
 
12
    The default for all values is 0.
 
13
    
 
14
    Points can be created in a number of ways...
 
15
        
 
16
    With longitude, latitude, and altitude:
 
17
    >>> p1 = Point(41.5, -81, 0)
 
18
    >>> p2 = Point(latitude=41.5, longitude=-81)
 
19
    
 
20
    With a sequence of 0 to 3 values (longitude, latitude, altitude):
 
21
    >>> p1 = Point([41.5, -81, 0])
 
22
    >>> p2 = Point((41.5, -81))
 
23
    
 
24
    Copy another `Point` instance:
 
25
    >>> p2 = Point(p1)
 
26
    >>> p2 == p1
 
27
    True
 
28
    >>> p2 is p1
 
29
    False
 
30
    
 
31
    Give an object with a 'point' attribute, such as a `Location` instance:
 
32
    >>> p = Point(location)
 
33
    
 
34
    Give a string containing at least latitude and longitude:
 
35
    >>> p1 = Point('41.5,-81.0')
 
36
    >>> p2 = Point('41.5 N -81.0 W')
 
37
    >>> p3 = Point('-41.5 S, 81.0 E, 2.5km')
 
38
    >>> p4 = Point('23 26m 22s N 23 27m 30s E 21.0mi')
 
39
    >>> p5 = Point('''3 26' 22" N 23 27' 30" E''')
 
40
    
 
41
    Point values can be accessed by name or by index:
 
42
    >>> p = Point(41.5, -81.0, 0)
 
43
    >>> p.latitude == p[0]
 
44
    True
 
45
    >>> p.longitude == p[1]
 
46
    True
 
47
    >>> p.altitude == p[2]
 
48
    True
 
49
    
 
50
    When unpacking (or iterating), only latitude and longitude are included:
 
51
    >>> latitude, longitude = p
 
52
    
 
53
    """
 
54
    UTIL_PATTERNS = dict(
 
55
        FLOAT=r'\d+(?:\.\d+)?',
 
56
        DEGREE=format.DEGREE,
 
57
        PRIME=format.PRIME,
 
58
        DOUBLE_PRIME=format.DOUBLE_PRIME,
 
59
        SEP=r'\s*[,;\s]\s*'
 
60
    )
 
61
    POINT_PATTERN = re.compile(r"""
 
62
        \s*
 
63
        (?P<latitude>
 
64
          (?P<latitude_degrees>-?%(FLOAT)s)(?:[%(DEGREE)s ][ ]*
 
65
            (?:(?P<latitude_arcminutes>%(FLOAT)s)[%(PRIME)s'm][ ]*)?
 
66
            (?:(?P<latitude_arcseconds>%(FLOAT)s)[%(DOUBLE_PRIME)s"s][ ]*)?
 
67
            )?(?P<latitude_direction>[NS])?)
 
68
        %(SEP)s
 
69
        (?P<longitude>
 
70
          (?P<longitude_degrees>-?%(FLOAT)s)(?:[%(DEGREE)s\s][ ]*
 
71
          (?:(?P<longitude_arcminutes>%(FLOAT)s)[%(PRIME)s'm][ ]*)?
 
72
          (?:(?P<longitude_arcseconds>%(FLOAT)s)[%(DOUBLE_PRIME)s"s][ ]*)?
 
73
          )?(?P<longitude_direction>[EW])?)(?:
 
74
        %(SEP)s
 
75
          (?P<altitude>
 
76
            (?P<altitude_distance>-?%(FLOAT)s)[ ]*
 
77
            (?P<altitude_units>km|m|mi|ft|nm|nmi)))?
 
78
        \s*$
 
79
    """ % UTIL_PATTERNS, re.X)
 
80
    
 
81
    def __new__(cls, latitude=None, longitude=None, altitude=None):
 
82
        single_arg = longitude is None and altitude is None
 
83
        if single_arg and not isinstance(latitude, util.NUMBER_TYPES):
 
84
            arg = latitude
 
85
            if arg is None:
 
86
                pass
 
87
            elif isinstance(arg, Point):
 
88
                return cls.from_point(arg)
 
89
            elif isinstance(arg, basestring):
 
90
                return cls.from_string(arg)
 
91
            else:
 
92
                try:
 
93
                    seq = iter(arg)
 
94
                except TypeError:
 
95
                    raise TypeError(
 
96
                        "Failed to create Point instance from %r." % (arg,)
 
97
                    )
 
98
                else:
 
99
                    return cls.from_sequence(seq)
 
100
        
 
101
        latitude = float(latitude or 0)
 
102
        if abs(latitude) > 90:
 
103
            raise ValueError("Latitude out of range [-90, 90]: %r" % latitude)
 
104
        
 
105
        longitude = float(longitude or 0)
 
106
        if abs(longitude) > 180:
 
107
            raise ValueError("Longitude out of range [-180, 180]: %r" % longitude)
 
108
        
 
109
        altitude = float(altitude or 0)
 
110
        
 
111
        self = super(Point, cls).__new__(cls)
 
112
        self.latitude = latitude
 
113
        self.longitude = longitude
 
114
        self.altitude = altitude
 
115
        return self
 
116
    
 
117
    def __getitem__(self, index):
 
118
        return (self.latitude, self.longitude, self.altitude)[index]
 
119
    
 
120
    def __setitem__(self, index, value):
 
121
        point = [self.latitude, self.longitude, self.altitude]
 
122
        point[index] = value
 
123
        self.latitude, self.longitude, self.altitude = point
 
124
    
 
125
    def __iter__(self):
 
126
        return iter((self.latitude, self.longitude, self.altitude))
 
127
    
 
128
    def __repr__(self):
 
129
        return "Point(%r, %r, %r)" % (
 
130
            self.latitude, self.longitude, self.altitude
 
131
        )
 
132
    
 
133
    def format(self, altitude=None, deg_char='', min_char='m', sec_char='s'):
 
134
        latitude = "%s %s" % (
 
135
            format.angle(abs(self.latitude), deg_char, min_char, sec_char),
 
136
            self.latitude >= 0 and 'N' or 'S'
 
137
        )
 
138
        longitude = "%s %s" % (
 
139
            format.angle(abs(self.longitude), deg_char, min_char, sec_char),
 
140
            self.longitude >= 0 and 'E' or 'W'
 
141
        )
 
142
        coordinates = [latitude, longitude]
 
143
        
 
144
        if altitude is None:
 
145
            altitude = bool(self.altitude)
 
146
        if altitude:
 
147
            if not isinstance(altitude, basestring):
 
148
                altitude = 'km'
 
149
            coordinates.append(self.format_altitude(altitude))
 
150
        
 
151
        return ", ".join(coordinates)
 
152
    
 
153
    def format_decimal(self, altitude=None):
 
154
        latitude = "%s" % self.latitude
 
155
        longitude = "%s" % self.longitude
 
156
        coordinates = [latitude, longitude]
 
157
        
 
158
        if altitude is None:
 
159
            altitude = bool(self.altitude)
 
160
        if altitude:
 
161
            if not isinstance(altitude, basestring):
 
162
                altitude = 'km'
 
163
            coordinates.append(self.format_altitude(altitude))
 
164
        
 
165
        return ", ".join(coordinates)
 
166
    
 
167
    def format_altitude(self, unit='km'):
 
168
        return format.distance(self.altitude, unit)
 
169
    
 
170
    def __str__(self):
 
171
        return self.format()
 
172
    
 
173
    def __unicode__(self):
 
174
        return self.format(
 
175
            None, format.DEGREE, format.PRIME, format.DOUBLE_PRIME
 
176
        )
 
177
    
 
178
    def __eq__(self, other):
 
179
        return tuple(self) == tuple(other)
 
180
    
 
181
    def __ne__(self, other):
 
182
        return tuple(self) != tuple(other)
 
183
    
 
184
    @classmethod
 
185
    def parse_degrees(cls, degrees, arcminutes, arcseconds, direction=None):
 
186
        negative = degrees < 0 or degrees.startswith('-')
 
187
        degrees = float(degrees or 0)
 
188
        arcminutes = float(arcminutes or 0)
 
189
        arcseconds = float(arcseconds or 0)
 
190
        
 
191
        if arcminutes or arcseconds:
 
192
            more = units.degrees(arcminutes=arcminutes, arcseconds=arcseconds)
 
193
            if negative:
 
194
                degrees -= more
 
195
            else:
 
196
                degrees += more
 
197
        
 
198
        if direction in [None, 'N', 'E']:
 
199
            return degrees
 
200
        elif direction in ['S', 'W']:
 
201
            return -degrees
 
202
        else:
 
203
            raise ValueError("Invalid direction! Should be one of [NSEW].")
 
204
    
 
205
    @classmethod
 
206
    def parse_altitude(cls, distance, unit):
 
207
        if distance is not None:
 
208
            distance = float(distance)
 
209
            CONVERTERS = {
 
210
                'km': lambda d: d,
 
211
                'm': lambda d: units.kilometers(meters=d),
 
212
                'mi': lambda d: units.kilometers(miles=d),
 
213
                'ft': lambda d: units.kilometers(feet=d),
 
214
                'nm': lambda d: units.kilometers(nautical=d),
 
215
                'nmi': lambda d: units.kilometers(nautical=d)
 
216
            }
 
217
            return CONVERTERS[unit](distance)
 
218
        else:
 
219
            return distance
 
220
    
 
221
    @classmethod
 
222
    def from_string(cls, string):
 
223
        """
 
224
        Create and return a Point instance from a string containing latitude
 
225
        and longitude, and optionally, altitude.
 
226
        
 
227
        Latitude and longitude must be in degrees and may be in decimal form
 
228
        or indicate arcminutes and arcseconds (labeled with Unicode prime and
 
229
        double prime, ASCII quote and double quote or 'm' and 's'). The degree
 
230
        symbol is optional and may be included after the decimal places (in
 
231
        decimal form) and before the arcminutes and arcseconds otherwise.
 
232
        Coordinates given from south and west (indicated by S and W suffixes)
 
233
        will be converted to north and east by switching their signs. If no
 
234
        (or partial) cardinal directions are given, north and east are the
 
235
        assumed directions. Latitude and longitude must be separated by at
 
236
        least whitespace, a comma, or a semicolon (each with optional
 
237
        surrounding whitespace).
 
238
        
 
239
        Altitude, if supplied, must be a decimal number with given units.
 
240
        The following unit abbrevations (case-insensitive) are supported:
 
241
        
 
242
            km (kilometers)
 
243
            m (meters)
 
244
            mi (miles)
 
245
            ft (feet)
 
246
            nm, nmi (nautical miles)
 
247
        
 
248
        Some example strings the will work include:
 
249
        
 
250
            41.5;-81.0
 
251
            41.5,-81.0
 
252
            41.5 -81.0
 
253
            41.5 N -81.0 W
 
254
            -41.5 S;81.0 E
 
255
            23 26m 22s N 23 27m 30s E
 
256
            23 26' 22" N 23 27' 30" E
 
257
        
 
258
        """
 
259
        match = re.match(cls.POINT_PATTERN, string)
 
260
        if match:
 
261
            latitude = cls.parse_degrees(
 
262
                match.group('latitude_degrees'),
 
263
                match.group('latitude_arcminutes'),
 
264
                match.group('latitude_arcseconds'),
 
265
                match.group('latitude_direction')
 
266
            )
 
267
            longitude = cls.parse_degrees(
 
268
                match.group('longitude_degrees'),
 
269
                match.group('longitude_arcminutes'),
 
270
                match.group('longitude_arcseconds'),
 
271
                match.group('longitude_direction'),
 
272
            )
 
273
            altitude = cls.parse_altitude(
 
274
                match.group('altitude_distance'),
 
275
                match.group('altitude_units')
 
276
            )
 
277
            return cls(latitude, longitude, altitude)
 
278
        else:
 
279
            raise ValueError(
 
280
                "Failed to create Point instance from string: unknown format."
 
281
            )
 
282
    
 
283
    @classmethod
 
284
    def from_sequence(cls, seq):
 
285
        """
 
286
        Create and return a new Point instance from any iterable with 0 to
 
287
        3 elements.  The elements, if present, must be latitude, longitude,
 
288
        and altitude, respectively.
 
289
        
 
290
        """
 
291
        args = tuple(islice(seq, 4))
 
292
        return cls(*args)
 
293
    
 
294
    @classmethod
 
295
    def from_point(cls, point):
 
296
        """
 
297
        Create and return a new Point instance from another Point instance.
 
298
        
 
299
        """
 
300
        return cls(point.latitude, point.longitude, point.altitude)