#!/usr/bin/env python
"""Functionality relating to parsing and comparing longitudes."""
import numpy as np
import xarray as xr
[docs]def lon_to_0360(lon):
"""Convert longitude(s) to be within [0, 360).
The Eastern hemisphere corresponds to 0 <= lon + (n*360) < 180, and the
Western Hemisphere corresponds to 180 <= lon + (n*360) < 360, where 'n' is
any integer (positive, negative, or zero).
Parameters
----------
lon : scalar or sequence of scalars
One or more longitude values to be converted to lie in the [0, 360)
range
Returns
-------
If ``lon`` is a scalar, then a scalar of the same type in the range [0,
360). If ``lon`` is array-like, then an array-like of the same type
with each element a scalar in the range [0, 360).
"""
quotient = lon // 360
return lon - quotient*360
def _lon_in_west_hem(lon):
if lon_to_0360(lon) >= 180:
return True
else:
return False
[docs]def lon_to_pm180(lon):
"""Convert longitude(s) to be within [-180, 180).
The Eastern hemisphere corresponds to 0 <= lon + (n*360) < 180, and the
Western Hemisphere corresponds to 180 <= lon + (n*360) < 360, where 'n' is
any integer (positive, negative, or zero).
Parameters
----------
lon : scalar or sequence of scalars
One or more longitude values to be converted to lie in the [-180, 180)
range
Returns
-------
If ``lon`` is a scalar, then a scalar of the same type in the range
[-180, 180). If ``lon`` is array-like, then an array-like of the same
type with each element a scalar in the range [-180, 180).
"""
lon0360 = lon_to_0360(lon)
if _lon_in_west_hem(lon0360):
return lon0360 - 360
else:
return lon0360
def _maybe_cast_to_lon(obj, strict=False):
if isinstance(obj, Longitude):
return obj
try:
return Longitude(obj)
except (ValueError, TypeError) as e:
if strict:
raise type(e)(str(e))
else:
return obj
def _other_to_lon(func):
"""Wrapper for casting Longitude operator arguments to Longitude"""
def func_other_to_lon(obj, other):
return func(obj, _maybe_cast_to_lon(other))
return func_other_to_lon
[docs]class Longitude(object):
"""Geographic longitude.
Enables unambiguous comparison of longitudes using the standard comparison
operators, regardless of they were initially represented with a 0 to 360
convention, -180 to 180 convention, or anything else, and even if the
original convention differs between them.
Specifically, the ``<`` operator assesses if the first object is to the
west of the second object, with the standard convention that longitudes in
the Western Hemisphere are always to the west of longitudes in the Eastern
Hemisphere. The ``>`` operator is defined analogously. ``==``, ``>=``,
and ``<=`` are also all defined.
In addition to other Longitude objects, the operators can be used to
compare a Longitude object to anything that can be casted to a Longitude
object, or to any sequence (e.g. a list or xarray.DataArray) whose elements
can be casted to Longitude objects.
"""
def __init__(self, value):
"""
Parameters
----------
value : {scalar, str}
Scalars get converted to longitudes using the convention that 0-180
corresponds to the Eastern Hemisphere, 180-360 corresponds to the
Western Hemisphere, 360-540 the Eastern Hemisphere, and so on,
including for negative numbers.
Strings must be castable to a float or be a positive number in the
range 0-180 followed by a single letter 'e' or 'w' (case
insensitive). For example, ``Longitude('10w')`` would yield a
``Longitude`` object corresponding to 10 degrees west longitude.
"""
try:
val_as_float = float(value)
except (ValueError, TypeError):
if not isinstance(value, str):
raise ValueError('value must be a scalar or a string')
if value[-1].lower() not in ('w', 'e'):
raise ValueError("string inputs must end in 'e' or 'w'")
try:
lon_value = float(value[:-1])
except ValueError:
raise ValueError('improperly formatted string')
if (lon_value < 0) or (lon_value > 180):
raise ValueError('Value given as strings with hemisphere '
'identifier must have numerical values '
'within 0 and +180. Value given: '
'{}'.format(lon_value))
self._longitude = lon_value
self._hemisphere = value[-1].upper()
else:
lon_pm180 = lon_to_pm180(val_as_float)
if _lon_in_west_hem(val_as_float):
self._longitude = abs(lon_pm180)
self._hemisphere = 'W'
else:
self._longitude = lon_pm180
self._hemisphere = 'E'
@property
def longitude(self):
"""(scalar) The unsigned numerical value of the longitude.
Always in the range 0 to 180. Must be combined with the ``hemisphere``
attribute to specify the exact latitude.
"""
return self._longitude
@longitude.setter
def longitude(self, value):
raise ValueError("'longitude' property cannot be modified after "
"Longitude object has been created.")
@property
def hemisphere(self):
"""{'W', 'E'} The longitude's hemisphere, either western or eastern."""
return self._hemisphere
@hemisphere.setter
def hemisphere(self, value):
raise ValueError("'hemisphere' property cannot be modified after "
"Longitude object has been created.")
def __repr__(self):
return "Longitude('{0}{1}')".format(self.longitude, self.hemisphere)
@_other_to_lon
def __eq__(self, other):
if isinstance(other, Longitude):
return (self.hemisphere == other.hemisphere and
self.longitude == other.longitude)
else:
return xr.apply_ufunc(np.equal, other, self)
@_other_to_lon
def __lt__(self, other):
if isinstance(other, Longitude):
if self.hemisphere == 'W':
if other.hemisphere == 'E':
return True
else:
return self.longitude > other.longitude
else:
if other.hemisphere == 'W':
return False
else:
return self.longitude < other.longitude
else:
return xr.apply_ufunc(np.greater, other, self)
@_other_to_lon
def __gt__(self, other):
if isinstance(other, Longitude):
if self.hemisphere == 'W':
if other.hemisphere == 'E':
return False
else:
return self.longitude < other.longitude
else:
if other.hemisphere == 'W':
return True
else:
return self.longitude > other.longitude
else:
return xr.apply_ufunc(np.less, other, self)
@_other_to_lon
def __le__(self, other):
if isinstance(other, Longitude):
return self < other or self == other
else:
return xr.apply_ufunc(np.greater_equal, other, self)
@_other_to_lon
def __ge__(self, other):
if isinstance(other, Longitude):
return self > other or self == other
else:
return xr.apply_ufunc(np.less_equal, other, self)
[docs] def to_0360(self):
"""Convert longitude to its numerical value within [0, 360)."""
if self.hemisphere == 'W':
return -1*self.longitude + 360
else:
return self.longitude
[docs] def to_pm180(self):
"""Convert longitude to its numerical value within [-180, 180)."""
if self.hemisphere == 'W':
return -1*self.longitude
else:
return self.longitude
@_other_to_lon
def __add__(self, other):
return Longitude(self.to_0360() + other.to_0360())
@_other_to_lon
def __sub__(self, other):
return Longitude(self.to_0360() - other.to_0360())
if __name__ == '__main__':
pass