Statistics
| Revision:

gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / geopy / point.py @ 545

History | View | Annotate | Download (11.8 KB)

1
# encoding: utf-8
2
"""
3
:class:`.Point` data structure.
4
"""
5

    
6
import re
7
from itertools import islice
8
from geopy import util, units
9
from geopy.format import (
10
    DEGREE,
11
    PRIME,
12
    DOUBLE_PRIME,
13
    format_degrees,
14
    format_distance,
15
)
16
from geopy.compat import string_compare
17

    
18

    
19
POINT_PATTERN = re.compile(r"""
20
    .*?
21
    (?P<latitude>
22
      (?P<latitude_direction_front>[NS])?[ ]*
23
        (?P<latitude_degrees>-?%(FLOAT)s)(?:[%(DEGREE)sD\*\u00B0\s][ ]*
24
        (?:(?P<latitude_arcminutes>%(FLOAT)s)[%(PRIME)s'm][ ]*)?
25
        (?:(?P<latitude_arcseconds>%(FLOAT)s)[%(DOUBLE_PRIME)s"s][ ]*)?
26
        )?(?P<latitude_direction_back>[NS])?)
27
    %(SEP)s
28
    (?P<longitude>
29
      (?P<longitude_direction_front>[EW])?[ ]*
30
      (?P<longitude_degrees>-?%(FLOAT)s)(?:[%(DEGREE)sD\*\u00B0\s][ ]*
31
      (?:(?P<longitude_arcminutes>%(FLOAT)s)[%(PRIME)s'm][ ]*)?
32
      (?:(?P<longitude_arcseconds>%(FLOAT)s)[%(DOUBLE_PRIME)s"s][ ]*)?
33
      )?(?P<longitude_direction_back>[EW])?)(?:
34
    %(SEP)s
35
      (?P<altitude>
36
        (?P<altitude_distance>-?%(FLOAT)s)[ ]*
37
        (?P<altitude_units>km|m|mi|ft|nm|nmi)))?
38
    .*?$
39
""" % {
40
    "FLOAT": r'\d+(?:\.\d+)?',
41
    "DEGREE": DEGREE,
42
    "PRIME": PRIME,
43
    "DOUBLE_PRIME": DOUBLE_PRIME,
44
    "SEP": r'\s*[,;/\s]\s*',
45
}, re.X)
46

    
47

    
48
class Point(object):
49
    """
50
    A geodetic point with latitude, longitude, and altitude.
51

52
    Latitude and longitude are floating point values in degrees.
53
    Altitude is a floating point value in kilometers. The reference level
54
    is never considered and is thus application dependent, so be consistent!
55
    The default for all values is 0.
56

57
    Points can be created in a number of ways...
58

59
    With longitude, latitude, and altitude::
60

61
        >>> p1 = Point(41.5, -81, 0)
62
        >>> p2 = Point(latitude=41.5, longitude=-81)
63

64
    With a sequence of 0 to 3 values (longitude, latitude, altitude)::
65

66
        >>> p1 = Point([41.5, -81, 0])
67
        >>> p2 = Point((41.5, -81))
68

69
    Copy another `Point` instance::
70

71
        >>> p2 = Point(p1)
72
        >>> p2 == p1
73
        True
74
        >>> p2 is p1
75
        False
76

77
    Give a string containing at least latitude and longitude::
78

79
        >>> p1 = Point('41.5,-81.0')
80
        >>> p2 = Point('41.5 N -81.0 W')
81
        >>> p3 = Point('-41.5 S, 81.0 E, 2.5km')
82
        >>> p4 = Point('23 26m 22s N 23 27m 30s E 21.0mi')
83
        >>> p5 = Point('''3 26' 22" N 23 27' 30" E''')
84

85
    Point values can be accessed by name or by index::
86

87
        >>> p = Point(41.5, -81.0, 0)
88
        >>> p.latitude == p[0]
89
        True
90
        >>> p.longitude == p[1]
91
        True
92
        >>> p.altitude == p[2]
93
        True
94

95
    When unpacking (or iterating), a (latitude, longitude, altitude) tuple is
96
    returned::
97

98
        >>> latitude, longitude, altitude = p
99

100
    """
101

    
102
    __slots__ = ("latitude", "longitude", "altitude", "_items")
103

    
104
    POINT_PATTERN = POINT_PATTERN
105

    
106
    def __new__(cls, latitude=None, longitude=None, altitude=None):
107
        """
108
        :param float latitude: Latitude of point.
109
        :param float longitude: Longitude of point.
110
        :param float altitude: Altitude of point.
111
        """
112
        single_arg = longitude is None and altitude is None
113
        if single_arg and not isinstance(latitude, util.NUMBER_TYPES):
114
            arg = latitude
115
            if arg is None: # pragma: no cover
116
                pass
117
            elif isinstance(arg, Point):
118
                return cls.from_point(arg)
119
            elif isinstance(arg, string_compare):
120
                return cls.from_string(arg)
121
            else:
122
                try:
123
                    seq = iter(arg)
124
                except TypeError: # pragma: no cover
125
                    raise TypeError(
126
                        "Failed to create Point instance from %r." % (arg,)
127
                    )
128
                else:
129
                    return cls.from_sequence(seq)
130

    
131
        latitude = float(latitude or 0.0)
132
        if abs(latitude) > 90:
133
            latitude = ((latitude + 90) % 180) - 90
134

    
135
        longitude = float(longitude or 0.0)
136
        if abs(longitude) > 180:
137
            longitude = ((longitude + 180) % 360) - 180
138

    
139
        altitude = float(altitude or 0.0)
140

    
141
        self = super(Point, cls).__new__(cls)
142
        self.latitude = latitude
143
        self.longitude = longitude
144
        self.altitude = altitude
145
        self._items = [self.latitude, self.longitude, self.altitude]
146
        return self
147

    
148
    def __getitem__(self, index):
149
        return self._items[index]
150

    
151
    def __setitem__(self, index, value):
152
        self._items[index] = value
153

    
154
    def __iter__(self):
155
        return iter((self.latitude, self.longitude, self.altitude))
156

    
157
    def __repr__(self):
158
        return "Point(%r, %r, %r)" % tuple(self._items)
159

    
160
    def format(self, altitude=None, deg_char='', min_char='m', sec_char='s'):
161
        """
162
        Format decimal degrees (DD) to degrees minutes seconds (DMS)
163
        """
164
        latitude = "%s %s" % (
165
            format_degrees(abs(self.latitude), symbols={
166
                'deg': deg_char, 'arcmin': min_char, 'arcsec': sec_char
167
            }),
168
            self.latitude >= 0 and 'N' or 'S'
169
        )
170
        longitude = "%s %s" % (
171
            format_degrees(abs(self.longitude), symbols={
172
                'deg': deg_char, 'arcmin': min_char, 'arcsec': sec_char
173
            }),
174
            self.longitude >= 0 and 'E' or 'W'
175
        )
176
        coordinates = [latitude, longitude]
177

    
178
        if altitude is None:
179
            altitude = bool(self.altitude)
180
        if altitude:
181
            if not isinstance(altitude, string_compare):
182
                altitude = 'km'
183
            coordinates.append(self.format_altitude(altitude))
184

    
185
        return ", ".join(coordinates)
186

    
187
    def format_decimal(self, altitude=None):
188
        """
189
        Format decimal degrees with altitude
190
        """
191
        coordinates = [str(self.latitude), str(self.longitude)]
192

    
193
        if altitude is None:
194
            altitude = bool(self.altitude)
195
        if altitude is True:
196
            if not isinstance(altitude, string_compare):
197
                altitude = 'km'
198
            coordinates.append(self.format_altitude(altitude))
199

    
200
        return ", ".join(coordinates)
201

    
202
    def format_altitude(self, unit='km'):
203
        """
204
        Foamt altitude with unit
205
        """
206
        return format_distance(self.altitude, unit=unit)
207

    
208
    def __str__(self):
209
        return self.format()
210

    
211
    def __unicode__(self):
212
        return self.format(
213
            None, DEGREE, PRIME, DOUBLE_PRIME
214
        )
215

    
216
    def __eq__(self, other):
217
        return tuple(self) == tuple(other)
218

    
219
    def __ne__(self, other):
220
        return tuple(self) != tuple(other)
221

    
222
    @classmethod
223
    def parse_degrees(cls, degrees, arcminutes, arcseconds, direction=None):
224
        """
225
        Parse degrees minutes seconds including direction (N, S, E, W)
226
        """
227
        degrees = float(degrees)
228
        negative = degrees < 0
229
        arcminutes = float(arcminutes)
230
        arcseconds = float(arcseconds)
231

    
232
        if arcminutes or arcseconds:
233
            more = units.degrees(arcminutes=arcminutes, arcseconds=arcseconds)
234
            if negative:
235
                degrees -= more
236
            else:
237
                degrees += more
238

    
239
        if direction in [None, 'N', 'E']:
240
            return degrees
241
        elif direction in ['S', 'W']:
242
            return -degrees
243
        else:
244
            raise ValueError("Invalid direction! Should be one of [NSEW].")
245

    
246
    @classmethod
247
    def parse_altitude(cls, distance, unit):
248
        """
249
        Parse altitude managing units conversion
250
        """
251
        if distance is not None:
252
            distance = float(distance)
253
            CONVERTERS = {
254
                'km': lambda d: d,
255
                'm': lambda d: units.kilometers(meters=d),
256
                'mi': lambda d: units.kilometers(miles=d),
257
                'ft': lambda d: units.kilometers(feet=d),
258
                'nm': lambda d: units.kilometers(nautical=d),
259
                'nmi': lambda d: units.kilometers(nautical=d)
260
            }
261
            try:
262
                return CONVERTERS[unit](distance)
263
            except KeyError: # pragma: no cover
264
                raise NotImplementedError(
265
                    'Bad distance unit specified, valid are: %r' %
266
                    CONVERTERS.keys()
267
                )
268
        else:
269
            return distance
270

    
271
    @classmethod
272
    def from_string(cls, string):
273
        """
274
        Create and return a ``Point`` instance from a string containing
275
        latitude and longitude, and optionally, altitude.
276

277
        Latitude and longitude must be in degrees and may be in decimal form
278
        or indicate arcminutes and arcseconds (labeled with Unicode prime and
279
        double prime, ASCII quote and double quote or 'm' and 's'). The degree
280
        symbol is optional and may be included after the decimal places (in
281
        decimal form) and before the arcminutes and arcseconds otherwise.
282
        Coordinates given from south and west (indicated by S and W suffixes)
283
        will be converted to north and east by switching their signs. If no
284
        (or partial) cardinal directions are given, north and east are the
285
        assumed directions. Latitude and longitude must be separated by at
286
        least whitespace, a comma, or a semicolon (each with optional
287
        surrounding whitespace).
288

289
        Altitude, if supplied, must be a decimal number with given units.
290
        The following unit abbrevations (case-insensitive) are supported:
291

292
            - ``km`` (kilometers)
293
            - ``m`` (meters)
294
            - ``mi`` (miles)
295
            - ``ft`` (feet)
296
            - ``nm``, ``nmi`` (nautical miles)
297

298
        Some example strings the will work include:
299

300
            - 41.5;-81.0
301
            - 41.5,-81.0
302
            - 41.5 -81.0
303
            - 41.5 N -81.0 W
304
            - -41.5 S;81.0 E
305
            - 23 26m 22s N 23 27m 30s E
306
            - 23 26' 22" N 23 27' 30" E
307
            - UT: N 39°20' 0'' / W 74°35' 0''
308

309
        """
310
        match = re.match(cls.POINT_PATTERN, re.sub(r"''", r'"', string))
311
        if match:
312
            latitude_direction = None
313
            if match.group("latitude_direction_front"):
314
                latitude_direction = match.group("latitude_direction_front")
315
            elif match.group("latitude_direction_back"):
316
                latitude_direction = match.group("latitude_direction_back")
317

    
318
            longitude_direction = None
319
            if match.group("longitude_direction_front"):
320
                longitude_direction = match.group("longitude_direction_front")
321
            elif match.group("longitude_direction_back"):
322
                longitude_direction = match.group("longitude_direction_back")
323
            latitude = cls.parse_degrees(
324
                match.group('latitude_degrees') or 0.0,
325
                match.group('latitude_arcminutes') or 0.0,
326
                match.group('latitude_arcseconds') or 0.0,
327
                latitude_direction
328
            )
329
            longitude = cls.parse_degrees(
330
                match.group('longitude_degrees') or 0.0,
331
                match.group('longitude_arcminutes') or 0.0,
332
                match.group('longitude_arcseconds') or 0.0,
333
                longitude_direction
334
            )
335
            altitude = cls.parse_altitude(
336
                match.group('altitude_distance'),
337
                match.group('altitude_units')
338
            )
339
            return cls(latitude, longitude, altitude)
340
        else:
341
            raise ValueError(
342
                "Failed to create Point instance from string: unknown format."
343
            )
344

    
345
    @classmethod
346
    def from_sequence(cls, seq):
347
        """
348
        Create and return a new ``Point`` instance from any iterable with 0 to
349
        3 elements.  The elements, if present, must be latitude, longitude,
350
        and altitude, respectively.
351
        """
352
        args = tuple(islice(seq, 4))
353
        return cls(*args)
354

    
355
    @classmethod
356
    def from_point(cls, point):
357
        """
358
        Create and return a new ``Point`` instance from another ``Point``
359
        instance.
360
        """
361
        return cls(point.latitude, point.longitude, point.altitude)