angle_helper.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. from __future__ import (absolute_import, division, print_function,
  2. unicode_literals)
  3. import six
  4. import numpy as np
  5. import math
  6. from mpl_toolkits.axisartist.grid_finder import ExtremeFinderSimple
  7. def select_step_degree(dv):
  8. degree_limits_ = [1.5, 3, 7, 13, 20, 40, 70, 120, 270, 520]
  9. degree_steps_ = [ 1, 2, 5, 10, 15, 30, 45, 90, 180, 360]
  10. degree_factors = [1.] * len(degree_steps_)
  11. minsec_limits_ = [1.5, 2.5, 3.5, 8, 11, 18, 25, 45]
  12. minsec_steps_ = [1, 2, 3, 5, 10, 15, 20, 30]
  13. minute_limits_ = np.array(minsec_limits_) / 60
  14. minute_factors = [60.] * len(minute_limits_)
  15. second_limits_ = np.array(minsec_limits_) / 3600
  16. second_factors = [3600.] * len(second_limits_)
  17. degree_limits = np.concatenate([second_limits_,
  18. minute_limits_,
  19. degree_limits_])
  20. degree_steps = np.concatenate([minsec_steps_,
  21. minsec_steps_,
  22. degree_steps_])
  23. degree_factors = np.concatenate([second_factors,
  24. minute_factors,
  25. degree_factors])
  26. n = degree_limits.searchsorted(dv)
  27. step = degree_steps[n]
  28. factor = degree_factors[n]
  29. return step, factor
  30. def select_step_hour(dv):
  31. hour_limits_ = [1.5, 2.5, 3.5, 5, 7, 10, 15, 21, 36]
  32. hour_steps_ = [1, 2 , 3, 4, 6, 8, 12, 18, 24]
  33. hour_factors = [1.] * len(hour_steps_)
  34. minsec_limits_ = [1.5, 2.5, 3.5, 4.5, 5.5, 8, 11, 14, 18, 25, 45]
  35. minsec_steps_ = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30]
  36. minute_limits_ = np.array(minsec_limits_) / 60
  37. minute_factors = [60.] * len(minute_limits_)
  38. second_limits_ = np.array(minsec_limits_) / 3600
  39. second_factors = [3600.] * len(second_limits_)
  40. hour_limits = np.concatenate([second_limits_,
  41. minute_limits_,
  42. hour_limits_])
  43. hour_steps = np.concatenate([minsec_steps_,
  44. minsec_steps_,
  45. hour_steps_])
  46. hour_factors = np.concatenate([second_factors,
  47. minute_factors,
  48. hour_factors])
  49. n = hour_limits.searchsorted(dv)
  50. step = hour_steps[n]
  51. factor = hour_factors[n]
  52. return step, factor
  53. def select_step_sub(dv):
  54. # subarcsec or degree
  55. tmp = 10.**(int(math.log10(dv))-1.)
  56. factor = 1./tmp
  57. if 1.5*tmp >= dv:
  58. step = 1
  59. elif 3.*tmp >= dv:
  60. step = 2
  61. elif 7.*tmp >= dv:
  62. step = 5
  63. else:
  64. step = 1
  65. factor = 0.1*factor
  66. return step, factor
  67. def select_step(v1, v2, nv, hour=False, include_last=True,
  68. threshold_factor=3600.):
  69. if v1 > v2:
  70. v1, v2 = v2, v1
  71. dv = (v2 - v1) / nv
  72. if hour:
  73. _select_step = select_step_hour
  74. cycle = 24.
  75. else:
  76. _select_step = select_step_degree
  77. cycle = 360.
  78. # for degree
  79. if dv > 1./threshold_factor:
  80. step, factor = _select_step(dv)
  81. else:
  82. step, factor = select_step_sub(dv*threshold_factor)
  83. factor = factor * threshold_factor
  84. f1, f2, fstep = v1*factor, v2*factor, step/factor
  85. levs = np.arange(np.floor(f1/step), np.ceil(f2/step)+0.5, dtype=int) * step
  86. # n : number of valid levels. If there is a cycle, e.g., [0, 90, 180,
  87. # 270, 360], the grid line needs to be extended from 0 to 360, so
  88. # we need to return the whole array. However, the last level (360)
  89. # needs to be ignored often. In this case, so we return n=4.
  90. n = len(levs)
  91. # we need to check the range of values
  92. # for example, -90 to 90, 0 to 360,
  93. if factor == 1. and (levs[-1] >= levs[0]+cycle): # check for cycle
  94. nv = int(cycle / step)
  95. if include_last:
  96. levs = levs[0] + np.arange(0, nv+1, 1) * step
  97. else:
  98. levs = levs[0] + np.arange(0, nv, 1) * step
  99. n = len(levs)
  100. return np.array(levs), n, factor
  101. def select_step24(v1, v2, nv, include_last=True, threshold_factor=3600):
  102. v1, v2 = v1/15., v2/15.
  103. levs, n, factor = select_step(v1, v2, nv, hour=True,
  104. include_last=include_last,
  105. threshold_factor=threshold_factor)
  106. return levs*15., n, factor
  107. def select_step360(v1, v2, nv, include_last=True, threshold_factor=3600):
  108. return select_step(v1, v2, nv, hour=False,
  109. include_last=include_last,
  110. threshold_factor=threshold_factor)
  111. class LocatorBase(object):
  112. def __init__(self, den, include_last=True):
  113. self.den = den
  114. self._include_last = include_last
  115. @property
  116. def nbins(self):
  117. return self.den
  118. @nbins.setter
  119. def nbins(self, v):
  120. self.den = v
  121. def set_params(self, nbins=None):
  122. if nbins is not None:
  123. self.den = int(nbins)
  124. class LocatorHMS(LocatorBase):
  125. def __call__(self, v1, v2):
  126. return select_step24(v1, v2, self.den, self._include_last)
  127. class LocatorHM(LocatorBase):
  128. def __call__(self, v1, v2):
  129. return select_step24(v1, v2, self.den, self._include_last,
  130. threshold_factor=60)
  131. class LocatorH(LocatorBase):
  132. def __call__(self, v1, v2):
  133. return select_step24(v1, v2, self.den, self._include_last,
  134. threshold_factor=1)
  135. class LocatorDMS(LocatorBase):
  136. def __call__(self, v1, v2):
  137. return select_step360(v1, v2, self.den, self._include_last)
  138. class LocatorDM(LocatorBase):
  139. def __call__(self, v1, v2):
  140. return select_step360(v1, v2, self.den, self._include_last,
  141. threshold_factor=60)
  142. class LocatorD(LocatorBase):
  143. def __call__(self, v1, v2):
  144. return select_step360(v1, v2, self.den, self._include_last,
  145. threshold_factor=1)
  146. class FormatterDMS(object):
  147. deg_mark = r"^{\circ}"
  148. min_mark = r"^{\prime}"
  149. sec_mark = r"^{\prime\prime}"
  150. fmt_d = "$%d" + deg_mark + "$"
  151. fmt_ds = r"$%d.%s" + deg_mark + "$"
  152. # %s for sign
  153. fmt_d_m = r"$%s%d" + deg_mark + r"\,%02d" + min_mark + "$"
  154. fmt_d_ms = r"$%s%d" + deg_mark + r"\,%02d.%s" + min_mark + "$"
  155. fmt_d_m_partial = "$%s%d" + deg_mark + r"\,%02d" + min_mark + r"\,"
  156. fmt_s_partial = "%02d" + sec_mark + "$"
  157. fmt_ss_partial = "%02d.%s" + sec_mark + "$"
  158. def _get_number_fraction(self, factor):
  159. ## check for fractional numbers
  160. number_fraction = None
  161. # check for 60
  162. for threshold in [1, 60, 3600]:
  163. if factor <= threshold:
  164. break
  165. d = factor // threshold
  166. int_log_d = int(np.floor(np.log10(d)))
  167. if 10**int_log_d == d and d != 1:
  168. number_fraction = int_log_d
  169. factor = factor // 10**int_log_d
  170. return factor, number_fraction
  171. return factor, number_fraction
  172. def __call__(self, direction, factor, values):
  173. if len(values) == 0:
  174. return []
  175. #ss = [[-1, 1][v>0] for v in values] #not py24 compliant
  176. values = np.asarray(values)
  177. ss = np.where(values>0, 1, -1)
  178. sign_map = {(-1, True):"-"}
  179. signs = [sign_map.get((s, v!=0), "") for s, v in zip(ss, values)]
  180. factor, number_fraction = self._get_number_fraction(factor)
  181. values = np.abs(values)
  182. if number_fraction is not None:
  183. values, frac_part = divmod(values, 10**number_fraction)
  184. frac_fmt = "%%0%dd" % (number_fraction,)
  185. frac_str = [frac_fmt % (f1,) for f1 in frac_part]
  186. if factor == 1:
  187. if number_fraction is None:
  188. return [self.fmt_d % (s*int(v),) for (s, v) in zip(ss, values)]
  189. else:
  190. return [self.fmt_ds % (s*int(v), f1)
  191. for (s, v, f1) in zip(ss, values, frac_str)]
  192. elif factor == 60:
  193. deg_part, min_part = divmod(values, 60)
  194. if number_fraction is None:
  195. return [self.fmt_d_m % (s1, d1, m1)
  196. for s1, d1, m1 in zip(signs, deg_part, min_part)]
  197. else:
  198. return [self.fmt_d_ms % (s, d1, m1, f1)
  199. for s, d1, m1, f1 in zip(signs, deg_part, min_part, frac_str)]
  200. elif factor == 3600:
  201. if ss[-1] == -1:
  202. inverse_order = True
  203. values = values[::-1]
  204. signs = signs[::-1]
  205. else:
  206. inverse_order = False
  207. l_hm_old = ""
  208. r = []
  209. deg_part, min_part_ = divmod(values, 3600)
  210. min_part, sec_part = divmod(min_part_, 60)
  211. if number_fraction is None:
  212. sec_str = [self.fmt_s_partial % (s1,) for s1 in sec_part]
  213. else:
  214. sec_str = [self.fmt_ss_partial % (s1, f1) for s1, f1 in zip(sec_part, frac_str)]
  215. for s, d1, m1, s1 in zip(signs, deg_part, min_part, sec_str):
  216. l_hm = self.fmt_d_m_partial % (s, d1, m1)
  217. if l_hm != l_hm_old:
  218. l_hm_old = l_hm
  219. l = l_hm + s1 #l_s
  220. else:
  221. l = "$" + s + s1
  222. r.append(l)
  223. if inverse_order:
  224. return r[::-1]
  225. else:
  226. return r
  227. else: # factor > 3600.
  228. return [r"$%s^{\circ}$" % (str(v),) for v in ss*values]
  229. class FormatterHMS(FormatterDMS):
  230. deg_mark = r"^\mathrm{h}"
  231. min_mark = r"^\mathrm{m}"
  232. sec_mark = r"^\mathrm{s}"
  233. fmt_d = "$%d" + deg_mark + "$"
  234. fmt_ds = r"$%d.%s" + deg_mark + "$"
  235. # %s for sign
  236. fmt_d_m = r"$%s%d" + deg_mark + r"\,%02d" + min_mark+"$"
  237. fmt_d_ms = r"$%s%d" + deg_mark + r"\,%02d.%s" + min_mark+"$"
  238. fmt_d_m_partial = "$%s%d" + deg_mark + r"\,%02d" + min_mark + r"\,"
  239. fmt_s_partial = "%02d" + sec_mark + "$"
  240. fmt_ss_partial = "%02d.%s" + sec_mark + "$"
  241. def __call__(self, direction, factor, values): # hour
  242. return FormatterDMS.__call__(self, direction, factor, np.asarray(values)/15.)
  243. class ExtremeFinderCycle(ExtremeFinderSimple):
  244. """
  245. When there is a cycle, e.g., longitude goes from 0-360.
  246. """
  247. def __init__(self,
  248. nx, ny,
  249. lon_cycle = 360.,
  250. lat_cycle = None,
  251. lon_minmax = None,
  252. lat_minmax = (-90, 90)
  253. ):
  254. #self.transfrom_xy = transform_xy
  255. #self.inv_transfrom_xy = inv_transform_xy
  256. self.nx, self.ny = nx, ny
  257. self.lon_cycle, self.lat_cycle = lon_cycle, lat_cycle
  258. self.lon_minmax = lon_minmax
  259. self.lat_minmax = lat_minmax
  260. def __call__(self, transform_xy, x1, y1, x2, y2):
  261. """
  262. get extreme values.
  263. x1, y1, x2, y2 in image coordinates (0-based)
  264. nx, ny : number of divisions in each axis
  265. """
  266. x_, y_ = np.linspace(x1, x2, self.nx), np.linspace(y1, y2, self.ny)
  267. x, y = np.meshgrid(x_, y_)
  268. lon, lat = transform_xy(np.ravel(x), np.ravel(y))
  269. # iron out jumps, but algorithm should be improved.
  270. # This is just naive way of doing and my fail for some cases.
  271. # Consider replacing this with numpy.unwrap
  272. # We are ignoring invalid warnings. They are triggered when
  273. # comparing arrays with NaNs using > We are already handling
  274. # that correctly using np.nanmin and np.nanmax
  275. with np.errstate(invalid='ignore'):
  276. if self.lon_cycle is not None:
  277. lon0 = np.nanmin(lon)
  278. lon -= 360. * ((lon - lon0) > 180.)
  279. if self.lat_cycle is not None:
  280. lat0 = np.nanmin(lat)
  281. lat -= 360. * ((lat - lat0) > 180.)
  282. lon_min, lon_max = np.nanmin(lon), np.nanmax(lon)
  283. lat_min, lat_max = np.nanmin(lat), np.nanmax(lat)
  284. lon_min, lon_max, lat_min, lat_max = \
  285. self._adjust_extremes(lon_min, lon_max, lat_min, lat_max)
  286. return lon_min, lon_max, lat_min, lat_max
  287. def _adjust_extremes(self, lon_min, lon_max, lat_min, lat_max):
  288. lon_min, lon_max, lat_min, lat_max = \
  289. self._add_pad(lon_min, lon_max, lat_min, lat_max)
  290. # check cycle
  291. if self.lon_cycle:
  292. lon_max = min(lon_max, lon_min + self.lon_cycle)
  293. if self.lat_cycle:
  294. lat_max = min(lat_max, lat_min + self.lat_cycle)
  295. if self.lon_minmax is not None:
  296. min0 = self.lon_minmax[0]
  297. lon_min = max(min0, lon_min)
  298. max0 = self.lon_minmax[1]
  299. lon_max = min(max0, lon_max)
  300. if self.lat_minmax is not None:
  301. min0 = self.lat_minmax[0]
  302. lat_min = max(min0, lat_min)
  303. max0 = self.lat_minmax[1]
  304. lat_max = min(max0, lat_max)
  305. return lon_min, lon_max, lat_min, lat_max