Viewing file: geo.py (20.32 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
import math
import numpy as np import numpy.ma as ma
import matplotlib rcParams = matplotlib.rcParams from matplotlib.artist import kwdocd from matplotlib.axes import Axes from matplotlib import cbook from matplotlib.patches import Circle from matplotlib.path import Path import matplotlib.spines as mspines import matplotlib.axis as maxis from matplotlib.ticker import Formatter, Locator, NullLocator, FixedLocator, NullFormatter from matplotlib.transforms import Affine2D, Affine2DBase, Bbox, \ BboxTransformTo, IdentityTransform, Transform, TransformWrapper
class GeoAxes(Axes): """ An abstract base class for geographic projections """ class ThetaFormatter(Formatter): """ Used to format the theta tick labels. Converts the native unit of radians into degrees and adds a degree symbol. """ def __init__(self, round_to=1.0): self._round_to = round_to
def __call__(self, x, pos=None): degrees = (x / np.pi) * 180.0 degrees = round(degrees / self._round_to) * self._round_to if rcParams['text.usetex'] and not rcParams['text.latex.unicode']: return r"$%0.0f^\circ$" % degrees else: return u"%0.0f\u00b0" % degrees
RESOLUTION = 75
def _init_axis(self): self.xaxis = maxis.XAxis(self) self.yaxis = maxis.YAxis(self) # Do not register xaxis or yaxis with spines -- as done in # Axes._init_axis() -- until GeoAxes.xaxis.cla() works. # self.spines['geo'].register_axis(self.yaxis) self._update_transScale()
def cla(self): Axes.cla(self)
self.set_longitude_grid(30) self.set_latitude_grid(15) self.set_longitude_grid_ends(75) self.xaxis.set_minor_locator(NullLocator()) self.yaxis.set_minor_locator(NullLocator()) self.xaxis.set_ticks_position('none') self.yaxis.set_ticks_position('none')
self.grid(rcParams['axes.grid'])
Axes.set_xlim(self, -np.pi, np.pi) Axes.set_ylim(self, -np.pi / 2.0, np.pi / 2.0)
def _set_lim_and_transforms(self): # A (possibly non-linear) projection on the (already scaled) data self.transProjection = self._get_core_transform(self.RESOLUTION)
self.transAffine = self._get_affine_transform()
self.transAxes = BboxTransformTo(self.bbox)
# The complete data transformation stack -- from data all the # way to display coordinates self.transData = \ self.transProjection + \ self.transAffine + \ self.transAxes
# This is the transform for longitude ticks. self._xaxis_pretransform = \ Affine2D() \ .scale(1.0, self._longitude_cap * 2.0) \ .translate(0.0, -self._longitude_cap) self._xaxis_transform = \ self._xaxis_pretransform + \ self.transData self._xaxis_text1_transform = \ Affine2D().scale(1.0, 0.0) + \ self.transData + \ Affine2D().translate(0.0, 4.0) self._xaxis_text2_transform = \ Affine2D().scale(1.0, 0.0) + \ self.transData + \ Affine2D().translate(0.0, -4.0)
# This is the transform for latitude ticks. yaxis_stretch = Affine2D().scale(np.pi * 2.0, 1.0).translate(-np.pi, 0.0) yaxis_space = Affine2D().scale(1.0, 1.1) self._yaxis_transform = \ yaxis_stretch + \ self.transData yaxis_text_base = \ yaxis_stretch + \ self.transProjection + \ (yaxis_space + \ self.transAffine + \ self.transAxes) self._yaxis_text1_transform = \ yaxis_text_base + \ Affine2D().translate(-8.0, 0.0) self._yaxis_text2_transform = \ yaxis_text_base + \ Affine2D().translate(8.0, 0.0)
def _get_affine_transform(self): transform = self._get_core_transform(1) xscale, _ = transform.transform_point((np.pi, 0)) _, yscale = transform.transform_point((0, np.pi / 2.0)) return Affine2D() \ .scale(0.5 / xscale, 0.5 / yscale) \ .translate(0.5, 0.5)
def get_xaxis_transform(self,which='grid'): assert which in ['tick1','tick2','grid'] return self._xaxis_transform
def get_xaxis_text1_transform(self, pad): return self._xaxis_text1_transform, 'bottom', 'center'
def get_xaxis_text2_transform(self, pad): return self._xaxis_text2_transform, 'top', 'center'
def get_yaxis_transform(self,which='grid'): assert which in ['tick1','tick2','grid'] return self._yaxis_transform
def get_yaxis_text1_transform(self, pad): return self._yaxis_text1_transform, 'center', 'right'
def get_yaxis_text2_transform(self, pad): return self._yaxis_text2_transform, 'center', 'left'
def _gen_axes_patch(self): return Circle((0.5, 0.5), 0.5)
def _gen_axes_spines(self): return {'geo':mspines.Spine.circular_spine(self, (0.5, 0.5), 0.5)}
def set_yscale(self, *args, **kwargs): if args[0] != 'linear': raise NotImplementedError
set_xscale = set_yscale
def set_xlim(self, *args, **kwargs): Axes.set_xlim(self, -np.pi, np.pi) Axes.set_ylim(self, -np.pi / 2.0, np.pi / 2.0)
set_ylim = set_xlim
def format_coord(self, long, lat): 'return a format string formatting the coordinate' long = long * (180.0 / np.pi) lat = lat * (180.0 / np.pi) if lat >= 0.0: ns = 'N' else: ns = 'S' if long >= 0.0: ew = 'E' else: ew = 'W' return u'%f\u00b0%s, %f\u00b0%s' % (abs(lat), ns, abs(long), ew)
def set_longitude_grid(self, degrees): """ Set the number of degrees between each longitude grid. """ number = (360.0 / degrees) + 1 self.xaxis.set_major_locator( FixedLocator( np.linspace(-np.pi, np.pi, number, True)[1:-1])) self._logitude_degrees = degrees self.xaxis.set_major_formatter(self.ThetaFormatter(degrees))
def set_latitude_grid(self, degrees): """ Set the number of degrees between each longitude grid. """ number = (180.0 / degrees) + 1 self.yaxis.set_major_locator( FixedLocator( np.linspace(-np.pi / 2.0, np.pi / 2.0, number, True)[1:-1])) self._latitude_degrees = degrees self.yaxis.set_major_formatter(self.ThetaFormatter(degrees))
def set_longitude_grid_ends(self, degrees): """ Set the latitude(s) at which to stop drawing the longitude grids. """ self._longitude_cap = degrees * (np.pi / 180.0) self._xaxis_pretransform \ .clear() \ .scale(1.0, self._longitude_cap * 2.0) \ .translate(0.0, -self._longitude_cap)
def get_data_ratio(self): ''' Return the aspect ratio of the data itself. ''' return 1.0
### Interactive panning
def can_zoom(self): """ Return True if this axes support the zoom box """ return False
def start_pan(self, x, y, button): pass
def end_pan(self): pass
def drag_pan(self, button, key, x, y): pass
class AitoffAxes(GeoAxes): name = 'aitoff'
class AitoffTransform(Transform): """ The base Aitoff transform. """ input_dims = 2 output_dims = 2 is_separable = False
def __init__(self, resolution): """ Create a new Aitoff transform. Resolution is the number of steps to interpolate between each input line segment to approximate its path in curved Aitoff space. """ Transform.__init__(self) self._resolution = resolution
def transform(self, ll): longitude = ll[:, 0:1] latitude = ll[:, 1:2]
# Pre-compute some values half_long = longitude / 2.0 cos_latitude = np.cos(latitude)
alpha = np.arccos(cos_latitude * np.cos(half_long)) # Mask this array, or we'll get divide-by-zero errors alpha = ma.masked_where(alpha == 0.0, alpha) # We want unnormalized sinc. numpy.sinc gives us normalized sinc_alpha = ma.sin(alpha) / alpha
x = (cos_latitude * np.sin(half_long)) / sinc_alpha y = (np.sin(latitude) / sinc_alpha) x.set_fill_value(0.0) y.set_fill_value(0.0) return np.concatenate((x.filled(), y.filled()), 1) transform.__doc__ = Transform.transform.__doc__
transform_non_affine = transform transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
def transform_path(self, path): vertices = path.vertices ipath = path.interpolated(self._resolution) return Path(self.transform(ipath.vertices), ipath.codes) transform_path.__doc__ = Transform.transform_path.__doc__
transform_path_non_affine = transform_path transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__
def inverted(self): return AitoffAxes.InvertedAitoffTransform(self._resolution) inverted.__doc__ = Transform.inverted.__doc__
class InvertedAitoffTransform(Transform): input_dims = 2 output_dims = 2 is_separable = False
def __init__(self, resolution): Transform.__init__(self) self._resolution = resolution
def transform(self, xy): # MGDTODO: Math is hard ;( return xy transform.__doc__ = Transform.transform.__doc__
def inverted(self): return AitoffAxes.AitoffTransform(self._resolution) inverted.__doc__ = Transform.inverted.__doc__
def __init__(self, *args, **kwargs): self._longitude_cap = np.pi / 2.0 GeoAxes.__init__(self, *args, **kwargs) self.set_aspect(0.5, adjustable='box', anchor='C') self.cla()
def _get_core_transform(self, resolution): return self.AitoffTransform(resolution)
class HammerAxes(GeoAxes): name = 'hammer'
class HammerTransform(Transform): """ The base Hammer transform. """ input_dims = 2 output_dims = 2 is_separable = False
def __init__(self, resolution): """ Create a new Hammer transform. Resolution is the number of steps to interpolate between each input line segment to approximate its path in curved Hammer space. """ Transform.__init__(self) self._resolution = resolution
def transform(self, ll): longitude = ll[:, 0:1] latitude = ll[:, 1:2]
# Pre-compute some values half_long = longitude / 2.0 cos_latitude = np.cos(latitude) sqrt2 = np.sqrt(2.0)
alpha = 1.0 + cos_latitude * np.cos(half_long) x = (2.0 * sqrt2) * (cos_latitude * np.sin(half_long)) / alpha y = (sqrt2 * np.sin(latitude)) / alpha return np.concatenate((x, y), 1) transform.__doc__ = Transform.transform.__doc__
transform_non_affine = transform transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
def transform_path(self, path): vertices = path.vertices ipath = path.interpolated(self._resolution) return Path(self.transform(ipath.vertices), ipath.codes) transform_path.__doc__ = Transform.transform_path.__doc__
transform_path_non_affine = transform_path transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__
def inverted(self): return HammerAxes.InvertedHammerTransform(self._resolution) inverted.__doc__ = Transform.inverted.__doc__
class InvertedHammerTransform(Transform): input_dims = 2 output_dims = 2 is_separable = False
def __init__(self, resolution): Transform.__init__(self) self._resolution = resolution
def transform(self, xy): x = xy[:, 0:1] y = xy[:, 1:2]
quarter_x = 0.25 * x half_y = 0.5 * y z = np.sqrt(1.0 - quarter_x*quarter_x - half_y*half_y) longitude = 2 * np.arctan((z*x) / (2.0 * (2.0*z*z - 1.0))) latitude = np.arcsin(y*z) return np.concatenate((longitude, latitude), 1) transform.__doc__ = Transform.transform.__doc__
def inverted(self): return HammerAxes.HammerTransform(self._resolution) inverted.__doc__ = Transform.inverted.__doc__
def __init__(self, *args, **kwargs): self._longitude_cap = np.pi / 2.0 GeoAxes.__init__(self, *args, **kwargs) self.set_aspect(0.5, adjustable='box', anchor='C') self.cla()
def _get_core_transform(self, resolution): return self.HammerTransform(resolution)
class MollweideAxes(GeoAxes): name = 'mollweide'
class MollweideTransform(Transform): """ The base Mollweide transform. """ input_dims = 2 output_dims = 2 is_separable = False
def __init__(self, resolution): """ Create a new Mollweide transform. Resolution is the number of steps to interpolate between each input line segment to approximate its path in curved Mollweide space. """ Transform.__init__(self) self._resolution = resolution
def transform(self, ll): def d(theta): delta = -(theta + np.sin(theta) - pi_sin_l) / (1 + np.cos(theta)) return delta, abs(delta) > 0.001
longitude = ll[:, 0:1] latitude = ll[:, 1:2]
pi_sin_l = np.pi * np.sin(latitude) theta = 2.0 * latitude delta, large_delta = d(theta) while np.any(large_delta): theta += np.where(large_delta, delta, 0) delta, large_delta = d(theta) aux = theta / 2
x = (2.0 * np.sqrt(2.0) * longitude * np.cos(aux)) / np.pi y = (np.sqrt(2.0) * np.sin(aux))
return np.concatenate((x, y), 1) transform.__doc__ = Transform.transform.__doc__
transform_non_affine = transform transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
def transform_path(self, path): vertices = path.vertices ipath = path.interpolated(self._resolution) return Path(self.transform(ipath.vertices), ipath.codes) transform_path.__doc__ = Transform.transform_path.__doc__
transform_path_non_affine = transform_path transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__
def inverted(self): return MollweideAxes.InvertedMollweideTransform(self._resolution) inverted.__doc__ = Transform.inverted.__doc__
class InvertedMollweideTransform(Transform): input_dims = 2 output_dims = 2 is_separable = False
def __init__(self, resolution): Transform.__init__(self) self._resolution = resolution
def transform(self, xy): # MGDTODO: Math is hard ;( return xy transform.__doc__ = Transform.transform.__doc__
def inverted(self): return MollweideAxes.MollweideTransform(self._resolution) inverted.__doc__ = Transform.inverted.__doc__
def __init__(self, *args, **kwargs): self._longitude_cap = np.pi / 2.0 GeoAxes.__init__(self, *args, **kwargs) self.set_aspect(0.5, adjustable='box', anchor='C') self.cla()
def _get_core_transform(self, resolution): return self.MollweideTransform(resolution)
class LambertAxes(GeoAxes): name = 'lambert'
class LambertTransform(Transform): """ The base Lambert transform. """ input_dims = 2 output_dims = 2 is_separable = False
def __init__(self, center_longitude, center_latitude, resolution): """ Create a new Lambert transform. Resolution is the number of steps to interpolate between each input line segment to approximate its path in curved Lambert space. """ Transform.__init__(self) self._resolution = resolution self._center_longitude = center_longitude self._center_latitude = center_latitude
def transform(self, ll): longitude = ll[:, 0:1] latitude = ll[:, 1:2] clong = self._center_longitude clat = self._center_latitude cos_lat = np.cos(latitude) sin_lat = np.sin(latitude) diff_long = longitude - clong cos_diff_long = np.cos(diff_long)
inner_k = (1.0 + np.sin(clat)*sin_lat + np.cos(clat)*cos_lat*cos_diff_long) # Prevent divide-by-zero problems inner_k = np.where(inner_k == 0.0, 1e-15, inner_k) k = np.sqrt(2.0 / inner_k) x = k*cos_lat*np.sin(diff_long) y = k*(np.cos(clat)*sin_lat - np.sin(clat)*cos_lat*cos_diff_long)
return np.concatenate((x, y), 1) transform.__doc__ = Transform.transform.__doc__
transform_non_affine = transform transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
def transform_path(self, path): vertices = path.vertices ipath = path.interpolated(self._resolution) return Path(self.transform(ipath.vertices), ipath.codes) transform_path.__doc__ = Transform.transform_path.__doc__
transform_path_non_affine = transform_path transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__
def inverted(self): return LambertAxes.InvertedLambertTransform( self._center_longitude, self._center_latitude, self._resolution) inverted.__doc__ = Transform.inverted.__doc__
class InvertedLambertTransform(Transform): input_dims = 2 output_dims = 2 is_separable = False
def __init__(self, center_longitude, center_latitude, resolution): Transform.__init__(self) self._resolution = resolution self._center_longitude = center_longitude self._center_latitude = center_latitude
def transform(self, xy): x = xy[:, 0:1] y = xy[:, 1:2] clong = self._center_longitude clat = self._center_latitude p = np.sqrt(x*x + y*y) p = np.where(p == 0.0, 1e-9, p) c = 2.0 * np.arcsin(0.5 * p) sin_c = np.sin(c) cos_c = np.cos(c)
lat = np.arcsin(cos_c*np.sin(clat) + ((y*sin_c*np.cos(clat)) / p)) long = clong + np.arctan( (x*sin_c) / (p*np.cos(clat)*cos_c - y*np.sin(clat)*sin_c))
return np.concatenate((long, lat), 1) transform.__doc__ = Transform.transform.__doc__
def inverted(self): return LambertAxes.LambertTransform( self._center_longitude, self._center_latitude, self._resolution) inverted.__doc__ = Transform.inverted.__doc__
def __init__(self, *args, **kwargs): self._longitude_cap = np.pi / 2.0 self._center_longitude = kwargs.pop("center_longitude", 0.0) self._center_latitude = kwargs.pop("center_latitude", 0.0) GeoAxes.__init__(self, *args, **kwargs) self.set_aspect('equal', adjustable='box', anchor='C') self.cla()
def cla(self): GeoAxes.cla(self) self.yaxis.set_major_formatter(NullFormatter())
def _get_core_transform(self, resolution): return self.LambertTransform( self._center_longitude, self._center_latitude, resolution)
def _get_affine_transform(self): return Affine2D() \ .scale(0.25) \ .translate(0.5, 0.5)
|