LSSTApplications  10.0+286,10.0+36,10.0+46,10.0-2-g4f67435,10.1+152,10.1+37,11.0,11.0+1,11.0-1-g47edd16,11.0-1-g60db491,11.0-1-g7418c06,11.0-2-g04d2804,11.0-2-g68503cd,11.0-2-g818369d,11.0-2-gb8b8ce7
LSSTDataManagementBasePackage
geometry.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008, 2009, 2010 LSST Corporation.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 
23 import math
24 import operator
25 
26 
27 # Unit conversion factors
28 ARCSEC_PER_DEG = 3600.0
29 DEG_PER_ARCSEC = 1.0 / 3600.0
30 
31 # Angle comparison slack
32 ANGLE_EPSILON = 0.001 * DEG_PER_ARCSEC # 1 milli-arcsec in degrees
33 
34 # Used in pole proximity tests
35 POLE_EPSILON = 1.0 * DEG_PER_ARCSEC # 1 arcsec in degrees
36 
37 # Dot product of 2 cartesian unit vectors must be < COS_MAX,
38 # or they are considered degenerate.
39 COS_MAX = 1.0 - 1.0e-15
40 
41 # Square norm of the cross product of 2 cartesian unit vectors must be
42 # >= CROSS_N2MIN, or the edge joining them is considered degenerate
43 CROSS_N2MIN = 2e-15
44 
45 # Dot product of a unit plane normal and a cartesian unit vector must be
46 # > SIN_MIN, or the vector is considered to be on the plane
47 SIN_MIN = math.sqrt(CROSS_N2MIN)
48 
49 # Constants related to floating point arithmetic
50 INF = float('inf')
51 NEG_INF = float('-inf')
52 try:
53  # python 2.6+
54  from sys import float_info
55  MAX_FLOAT = float_info.max
56  MIN_FLOAT = float_info.min
57  EPSILON = float_info.epsilon
58 except ImportError:
59  # assume python float is an IEEE754 double
60  MAX_FLOAT = 1.7976931348623157e+308
61  MIN_FLOAT = 2.2250738585072014e-308
62  EPSILON = 2.2204460492503131e-16
63 
64 if hasattr(math, 'isinf'):
65  # python 2.6+
66  isinf = math.isinf
67 else:
68  def isinf(x):
69  return x == INF or x == NEG_INF
70 
71 # -- Utility methods ----
72 
73 def dot(v1, v2):
74  """Returns the dot product of cartesian 3-vectors v1 and v2.
75  """
76  return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]
77 
78 def cross(v1, v2):
79  """Returns the cross product of cartesian 3-vectors v1 and v2.
80  """
81  return (v1[1] * v2[2] - v1[2] * v2[1],
82  v1[2] * v2[0] - v1[0] * v2[2],
83  v1[0] * v2[1] - v1[1] * v2[0])
84 
85 def invScale(v, s):
86  """Returns a copy of the cartesian 3-vector v scaled by 1 / s.
87  """
88  return (v[0] / s, v[1] / s, v[2] / s)
89 
90 def normalize(v):
91  """Returns a normalized copy of the cartesian 3-vector v.
92  """
93  n = math.sqrt(dot(v, v))
94  if n == 0.0:
95  raise RuntimeError('Cannot normalize a 3-vector with 0 magnitude')
96  return (v[0] / n, v[1] / n, v[2] / n)
97 
98 def sphericalCoords(*args):
99  """Returns spherical coordinates in degrees for the input coordinates,
100  which can be spherical or 3D cartesian. The 2 (spherical) or 3
101  (cartesian 3-vector) inputs can be passed either individually
102  or as a tuple/list, and can be of any type convertible to a float.
103  """
104  if len(args) == 1:
105  args = args[0]
106  if len(args) == 2:
107  t = (float(args[0]), float(args[1]))
108  if t[1] < -90.0 or t[1] > 90.0:
109  raise RuntimeError('Latitude angle is out of bounds')
110  return t
111  elif len(args) == 3:
112  x = float(args[0])
113  y = float(args[1])
114  z = float(args[2])
115  d2 = x * x + y * y
116  if d2 == 0.0:
117  theta = 0.0
118  else:
119  theta = math.degrees(math.atan2(y, x))
120  if theta < 0.0:
121  theta += 360.0
122  if z == 0.0:
123  phi = 0.0
124  else:
125  phi = math.degrees(math.atan2(z, math.sqrt(d2)))
126  return (theta, phi)
127  raise TypeError('Expecting 2 or 3 coordinates, or a tuple/list ' +
128  'containing 2 or 3 coordinates')
129 
131  """Returns a unit cartesian 3-vector corresponding to the input
132  coordinates, which can be spherical or 3D cartesian. The 2 (spherical)
133  or 3 (cartesian 3-vector) input coordinates can either be passed
134  individually or as a tuple/list, and can be of any type convertible
135  to a float.
136  """
137  if len(args) == 1:
138  args = args[0]
139  if len(args) == 2:
140  theta = math.radians(float(args[0]))
141  phi = math.radians(float(args[1]))
142  cosPhi = math.cos(phi)
143  return (math.cos(theta) * cosPhi,
144  math.sin(theta) * cosPhi,
145  math.sin(phi))
146  elif len(args) == 3:
147  x = float(args[0])
148  y = float(args[1])
149  z = float(args[2])
150  n = math.sqrt(x * x + y * y + z * z)
151  if n == 0.0:
152  raise RuntimeError('Cannot normalize a 3-vector with 0 magnitude')
153  return (x / n, y / n, z / n)
154  raise TypeError('Expecting 2 or 3 coordinates, or a tuple/list ' +
155  'containing 2 or 3 coordinates')
156 
158  """Returns the angular separation in degrees between points p1 and p2,
159  which must both be specified in spherical coordinates. The implementation
160  uses the halversine distance formula.
161  """
162  sdt = math.sin(math.radians(p1[0] - p2[0]) * 0.5)
163  sdp = math.sin(math.radians(p1[1] - p2[1]) * 0.5)
164  cc = math.cos(math.radians(p1[1])) * math.cos(math.radians(p2[1]))
165  s = math.sqrt(sdp * sdp + cc * sdt * sdt)
166  if s > 1.0:
167  return 180.0
168  else:
169  return 2.0 * math.degrees(math.asin(s))
170 
171 def clampPhi(phi):
172  """Clamps the input latitude angle phi to [-90.0, 90.0] deg.
173  """
174  if phi < -90.0:
175  return -90.0
176  elif phi >= 90.0:
177  return 90.0
178  return phi
179 
180 def reduceTheta(theta):
181  """Range reduces the given longitude angle to lie in the range
182  [0.0, 360.0).
183  """
184  theta = math.fmod(theta, 360.0)
185  if theta < 0.0:
186  return theta + 360.0
187  else:
188  return theta
189 
190 def northEast(v):
191  """Returns unit N,E basis vectors for a point v, which must be a
192  cartesian 3-vector.
193  """
194  north = (-v[0] * v[2],
195  -v[1] * v[2],
196  v[0] * v[0] + v[1] * v[1])
197  if north == (0.0, 0.0, 0.0):
198  # pick an arbitrary orthogonal basis with z = 0
199  north = (-1.0, 0.0, 0.0)
200  east = (0.0, 1.0, 0.0)
201  else:
202  north = normalize(north)
203  east = normalize(cross(north, v))
204  return north, east
205 
206 def alpha(r, centerPhi, phi):
207  """Returns the longitude angle alpha of the intersections (alpha, phi),
208  (-alpha, phi) of the circle centered on (0, centerPhi) with radius r and
209  the plane z = sin(phi). If there is no intersection, None is returned.
210  """
211  if phi < centerPhi - r or phi > centerPhi + r:
212  return None
213  a = abs(centerPhi - phi)
214  if a <= r - (90.0 - phi) or a <= r - (90.0 + phi):
215  return None
216  p = math.radians(phi)
217  cp = math.radians(centerPhi)
218  x = math.cos(math.radians(r)) - math.sin(cp) * math.sin(cp)
219  u = math.cos(cp) * math.cos(p)
220  y = math.sqrt(abs(u * u - x * x))
221  return math.degrees(abs(math.atan2(y, x)))
222 
223 def maxAlpha(r, centerPhi):
224  """Computes alpha, the extent in longitude angle [-alpha, alpha] of the
225  circle with radius r and center (0, centerPhi) on the unit sphere.
226  Both r and centerPhi are assumed to be in units of degrees.
227  centerPhi is clamped to lie in the range [-90,90] and r must
228  lie in the range [0, 90].
229  """
230  assert r >= 0.0 and r <= 90.0
231  if r == 0.0:
232  return 0.0
233  centerPhi = clampPhi(centerPhi)
234  if abs(centerPhi) + r > 90.0 - POLE_EPSILON:
235  return 180.0
236  r = math.radians(r)
237  c = math.radians(centerPhi)
238  y = math.sin(r)
239  x = math.sqrt(abs(math.cos(c - r) * math.cos(c + r)))
240  return math.degrees(abs(math.atan(y / x)))
241 
243  """Returns the angular separation in degrees between points v1 and v2,
244  which must both be specified as cartesian 3-vectors.
245  """
246  cs = dot(v1, v2)
247  n = cross(v1, v2)
248  ss = math.sqrt(dot(n, n))
249  if cs == 0.0 and ss == 0.0:
250  return 0.0
251  return math.degrees(math.atan2(ss, cs))
252 
253 def minEdgeSep(p, n, v1, v2):
254  """Returns the minimum angular separation in degrees between p
255  and points on the great circle edge with plane normal n and
256  vertices v1, v2. All inputs must be unit cartesian 3-vectors.
257  """
258  p1 = cross(n, v1)
259  p2 = cross(n, v2)
260  if dot(p1, p) >= 0.0 and dot(p2, p) <= 0.0:
261  return abs(90.0 - cartesianAngularSep(p, n))
262  else:
263  return min(cartesianAngularSep(p, v1), cartesianAngularSep(p, v2))
264 
265 def minPhiEdgeSep(p, phi, minTheta, maxTheta):
266  """Returns the minimum angular separation in degrees between p
267  and points on the small circle edge with constant latitude angle
268  phi and vertices (minTheta, phi), (maxTheta, phi). p must be in
269  spherical coordinates.
270  """
271  if minTheta > maxTheta:
272  if p[0] >= minTheta or p[0] <= maxTheta:
273  return abs(p[1] - phi)
274  else:
275  if p[0] >= minTheta and p[0] <= maxTheta:
276  return abs(p[1] - phi)
277  return min(sphericalAngularSep(p, (minTheta, phi)),
278  sphericalAngularSep(p, (maxTheta, phi)))
279 
280 def minThetaEdgeSep(p, theta, minPhi, maxPhi):
281  """Returns the minimum angular separation in degrees between p
282  and points on the great circle edge with constant longitude angle
283  theta and vertices (theta, minPhi), (theta, maxPhi). p must be a
284  unit cartesian 3-vector.
285  """
286  v1 = cartesianUnitVector(theta, minPhi)
287  v2 = cartesianUnitVector(theta, maxPhi)
288  n = cross(v1, v2)
289  d2 = dot(n, n)
290  if d2 == 0.0:
291  return min(cartesianAngularSep(p, v1), cartesianAngularSep(p, v2))
292  return minEdgeSep(p, invScale(n, math.sqrt(d2)), v1, v2)
293 
294 def centroid(vertices):
295  """Computes the centroid of a set of vertices (which must be passed in
296  as a list of cartesian unit vectors) on the unit sphere.
297  """
298  x, y, z = 0.0, 0.0, 0.0
299  # Note: could use more accurate summation routines
300  for v in vertices:
301  x += v[0]
302  y += v[1]
303  z += v[2]
304  return normalize((x, y, z))
305 
306 def between(p, n, v1, v2):
307  """Tests whether p lies on the shortest great circle arc from cartesian
308  unit vectors v1 to v2, assuming that p is a unit vector on the plane
309  defined by the origin, v1 and v2.
310  """
311  p1 = cross(n, v1)
312  p2 = cross(n, v2)
313  return dot(p1, p) >= 0.0 and dot(p2, p) <= 0.0
314 
315 def segments(phiMin, phiMax, width):
316  """Computes the number of segments to divide the given latitude angle
317  range [phiMin, phiMax] (degrees) into. Two points within the range
318  separated by at least one segment are guaranteed to have angular
319  separation of at least width degrees.
320  """
321  p = max(abs(phiMin), abs(phiMax))
322  if p > 90.0 - 1.0 * DEG_PER_ARCSEC:
323  return 1
324  if width >= 180.0:
325  return 1
326  elif width < 1.0 * DEG_PER_ARCSEC:
327  width = 1.0 * DEG_PER_ARCSEC
328  p = math.radians(p)
329  cw = math.cos(math.radians(width))
330  sp = math.sin(p)
331  cp = math.cos(p)
332  x = cw - sp * sp
333  u = cp * cp
334  y = math.sqrt(abs(u * u - x * x))
335  return int(math.floor((2.0 * math.pi) / abs(math.atan2(y, x))))
336 
337 
338 # -- Regions on the unit sphere ----
339 
340 # TODO: make this an ABC once python 2.6 becomes the default
341 class SphericalRegion(object):
342  """Base class for regions on the unit sphere.
343  """
344  pass
345 
346 
347 class SphericalBox(SphericalRegion):
348  """A spherical coordinate space bounding box.
349 
350  This is similar to a bounding box in cartesian space in that
351  it is specified by a pair of points; however, a spherical box may
352  correspond to the entire unit-sphere, a spherical cap, a lune or
353  the traditional rectangle. Additionally, spherical boxes can span
354  the 0/360 degree longitude angle discontinuity.
355 
356  Note that points falling exactly on spherical box edges are
357  considered to be inside (contained by) the box.
358  """
359  def __init__(self, *args):
360  """
361  Creates a new spherical box. If no arguments are supplied, then
362  an empty box is created. If the arguments consist of a single
363  SphericalRegion, then a copy of its bounding box is created.
364  Otherwise, the arguments must consist of a pair of 2 (spherical)
365  or 3 (cartesian 3-vector) element coordinate tuples/lists that
366  specify the minimum/maximum longitude/latitude angles for the box.
367  Latitude angles must be within [-90, 90] degrees, and the minimum
368  latitude angle must be less than or equal to the maximum. If both
369  minimum and maximum longitude angles lie in the range [0.0, 360.0],
370  then the maximum can be less than the minimum. For example, a box
371  with min/max longitude angles of 350/10 deg spans the longitude angle
372  ranges [350, 360) and [0, 10]. Otherwise, the minimum must be less
373  than or equal to the maximum, though values can be arbitrary. If
374  the two are are separated by 360 degrees or more, then the box
375  spans longitude angles [0, 360). Otherwise, both values are range
376  reduced. For example, a spherical box with min/max longitude angles
377  specified as 350/370 deg spans longitude angle ranges [350, 360) and
378  [0, 10].
379  """
380  if len(args) == 0:
381  self.setEmpty()
382  return
383  elif len(args) == 1:
384  if isinstance(args[0], SphericalRegion):
385  bbox = args[0].getBoundingBox()
386  self.min = tuple(bbox.getMin())
387  self.max = tuple(bbox.getMax())
388  return
389  args = args[0]
390  if len(args) == 2:
391  self.min = sphericalCoords(args[0])
392  self.max = sphericalCoords(args[1])
393  else:
394  raise TypeError('Expecting a spherical region, 2 points, '
395  'or a tuple/list containing 2 points')
396  if self.min[1] > self.max[1]:
397  raise RuntimeError(
398  'Latitude angle minimum is greater than maximum')
399  if (self.max[0] < self.min[0] and
400  (self.max[0] < 0.0 or self.min[0] > 360.0)):
401  raise RuntimeError(
402  'Longitude angle minimum is greater than maximum')
403  # Range-reduce longitude angles
404  if self.max[0] - self.min[0] >= 360.0:
405  self.min = (0.0, self.min[1])
406  self.max = (360.0, self.max[1])
407  else:
408  self.min = (reduceTheta(self.min[0]), self.min[1])
409  self.max = (reduceTheta(self.max[0]), self.max[1])
410 
411  def wraps(self):
412  """Returns True if this spherical box wraps across the 0/360
413  degree longitude angle discontinuity.
414  """
415  return self.min[0] > self.max[0]
416 
417  def getBoundingBox(self):
418  """Returns a bounding box for this spherical region.
419  """
420  return self
421 
422  def getMin(self):
423  """Returns the minimum longitude and latitude angles of this
424  spherical box as a 2-tuple of floats (in units of degrees).
425  """
426  return self.min
427 
428  def getMax(self):
429  """Returns the maximum longitude and latitude angles of this
430  spherical box as a 2-tuple of floats (in units of degrees).
431  """
432  return self.max
433 
434  def getThetaExtent(self):
435  """Returns the extent in longitude angle of this box.
436  """
437  if self.wraps():
438  return 360.0 - self.min[0] + self.max[0]
439  else:
440  return self.max[0] - self.min[0]
441 
442  def getCenter(self):
443  """Returns an 2-tuple of floats corresponding to the longitude/latitude
444  angles (in degrees) of the center of this spherical box.
445  """
446  centerTheta = 0.5 * (self.min[0] + self.max[0])
447  centerPhi = 0.5 * (self.min[1] + self.max[1])
448  if self.wraps():
449  if centerTheta >= 180.0:
450  centerTheta -= 180.0
451  else:
452  centerTheta += 180.0
453  return (centerTheta, centerPhi)
454 
455  def isEmpty(self):
456  """Returns True if this spherical box contains no points.
457  """
458  return self.min[1] > self.max[1]
459 
460  def isFull(self):
461  """Returns True if this spherical box contains every point
462  on the unit sphere.
463  """
464  return self.min == (0.0, -90.0) and self.max == (360.0, 90.0)
465 
466  def containsPoint(self, p):
467  """Returns True if this spherical box contains the given point,
468  which must be specified in spherical coordinates.
469  """
470  if p[1] < self.min[1] or p[1] > self.max[1]:
471  return False
472  theta = reduceTheta(p[0])
473  if self.wraps():
474  return theta >= self.min[0] or theta <= self.max[0]
475  else:
476  return theta >= self.min[0] and theta <= self.max[0]
477 
478  def contains(self, pointOrRegion):
479  """Returns True if this spherical box completely contains the given
480  point or spherical region. Note that the implementation is
481  conservative where ellipses are concerned: False may be returned
482  for an ellipse that is actually completely contained in this box.
483  """
484  if self.isEmpty():
485  return False
486  if isinstance(pointOrRegion, SphericalRegion):
487  b = pointOrRegion.getBoundingBox()
488  if b.isEmpty():
489  return False
490  if b.min[1] < self.min[1] or b.max[1] > self.max[1]:
491  return False
492  if self.wraps():
493  if b.wraps():
494  return b.min[0] >= self.min[0] and b.max[0] <= self.max[0]
495  else:
496  return b.min[0] >= self.min[0] or b.max[0] <= self.max[0]
497  else:
498  if b.wraps():
499  return self.min[0] == 0.0 and self.max[0] == 360.0
500  else:
501  return b.min[0] >= self.min[0] and b.max[0] <= self.max[0]
502  else:
503  return self.containsPoint(sphericalCoords(pointOrRegion))
504 
505  def intersects(self, pointOrRegion):
506  """Returns True if this spherical box intersects the given point
507  or spherical region. Note that the implementation is conservative:
508  True may be returned for a region that does not actually intersect
509  this box.
510  """
511  if self.isEmpty():
512  return False
513  if isinstance(pointOrRegion, SphericalBox):
514  b = pointOrRegion
515  if b.isEmpty():
516  return False
517  if b.min[1] > self.max[1] or b.max[1] < self.min[1]:
518  return False
519  if self.wraps():
520  if b.wraps():
521  return True
522  else:
523  return b.min[0] <= self.max[0] or b.max[0] >= self.min[0]
524  else:
525  if b.wraps():
526  return self.min[0] <= b.max[0] or self.max[0] >= b.min[0]
527  else:
528  return self.min[0] <= b.max[0] and self.max[0] >= b.min[0]
529  elif isinstance(pointOrRegion, SphericalRegion):
530  return pointOrRegion.intersects(self)
531  else:
532  return self.containsPoint(sphericalCoords(pointOrRegion))
533 
534  def extend(self, pointOrRegion):
535  """Extends this box to the smallest spherical box S containing
536  the union of this box with the specified point or spherical region.
537  """
538  if self == pointOrRegion:
539  return self
540  if isinstance(pointOrRegion, SphericalRegion):
541  b = pointOrRegion.getBoundingBox()
542  if b.isEmpty():
543  return self
544  elif self.isEmpty():
545  self.min = tuple(b.min)
546  self.max = tuple(b.max)
547  minPhi = min(self.min[1], b.min[1])
548  maxPhi = max(self.max[1], b.max[1])
549  minTheta = self.min[0]
550  maxTheta = self.max[0]
551  if self.wraps():
552  if b.wraps():
553  minMinRa = min(self.min[0], b.min[0])
554  maxMaxRa = max(self.max[0], b.max[0])
555  if maxMaxRa >= minMinRa:
556  minTheta = 0.0
557  maxTheta = 360.0
558  else:
559  minTheta = minMinRa
560  maxTheta = maxMaxRa
561  else:
562  if b.min[0] <= self.max[0] and b.max[0] >= self.min[0]:
563  minTheta = 0.0
564  maxTheta = 360.0
565  elif b.min[0] - self.max[0] > self.min[0] - b.max[0]:
566  minTheta = b.min[0]
567  else:
568  maxTheta = b.max[0]
569  else:
570  if b.wraps():
571  if self.min[0] <= b.max[0] and self.max[0] >= b.min[0]:
572  minTheta = 0.0
573  maxTheta = 360.0
574  elif self.min[0] - b.max[0] > b.min[0] - self.max[0]:
575  maxTheta = b.max[0]
576  else:
577  minTheta = b.min[0]
578  else:
579  if b.min[0] > self.max[0]:
580  if (360.0 - b.min[0] + self.max[0] <
581  b.max[0] - self.min[0]):
582  minTheta = b.min[0]
583  else:
584  maxTheta = b.max[0]
585  elif self.min[0] > b.max[0]:
586  if (360.0 - self.min[0] + b.max[0] <
587  self.max[0] - b.min[0]):
588  maxTheta = b.max[0]
589  else:
590  minTheta = b.min[0]
591  else:
592  minTheta = min(self.min[0], b.min[0])
593  maxTheta = max(self.max[0], b.max[0])
594  self.min = (minTheta, minPhi)
595  self.max = (maxTheta, maxPhi)
596  else:
597  p = sphericalCoords(pointOrRegion)
598  theta, phi = reduceTheta(p[0]), p[1]
599  if self.containsPoint(p):
600  return self
601  elif self.isEmpty():
602  self.min = (theta, phi)
603  self.max = (theta, phi)
604  else:
605  minPhi = min(self.min[1], phi)
606  maxPhi = max(self.max[1], phi)
607  minTheta = self.min[0]
608  maxTheta = self.max[0]
609  if self.wraps():
610  if self.min[0] - theta > theta - self.max[0]:
611  maxTheta = theta
612  else:
613  minTheta = theta
614  elif theta < self.min[0]:
615  if self.min[0] - theta <= 360.0 - self.max[0] + theta:
616  minTheta = theta
617  else:
618  maxTheta = theta
619  elif theta - self.max[0] <= 360.0 - theta + self.min[0]:
620  maxTheta = theta
621  else:
622  minTheta = theta
623  self.min = (minTheta, minPhi)
624  self.max = (maxTheta, maxPhi)
625  return self
626 
627  def shrink(self, box):
628  """Shrinks this box to the smallest spherical box containing
629  the intersection of this box and the specified one.
630  """
631  b = box
632  if not isinstance(b, SphericalBox):
633  raise TypeError('Expecting a SphericalBox object')
634  if self == b or self.isEmpty():
635  return self
636  elif b.isEmpty():
637  return self.setEmpty()
638  minPhi = max(self.min[1], b.min[1])
639  maxPhi = min(self.max[1], b.max[1])
640  minTheta = self.min[0]
641  maxTheta = self.max[0]
642  if self.wraps():
643  if b.wraps():
644  minTheta = max(minTheta, b.min[0])
645  maxTheta = min(maxTheta, b.max[0])
646  else:
647  if b.max[0] >= minTheta:
648  if b.min[0] <= maxTheta:
649  if b.max[0] - b.min[0] <= 360.0 - minTheta + maxTheta:
650  minTheta = b.min[0]
651  maxTheta = b.max[0]
652  else:
653  minTheta = max(minTheta, b.min[0])
654  maxTheta = b.max[0]
655  elif b.min[0] <= maxTheta:
656  minTheta = b.min[0]
657  maxTheta = min(maxTheta, b.max[0])
658  else:
659  minPhi = 90.0
660  maxPhi = -90.0
661  else:
662  if b.wraps():
663  if maxTheta >= b.min[0]:
664  if minTheta <= b.max[0]:
665  if maxTheta - minTheta > 360.0 - b.min[0] + b.max[0]:
666  minTheta = b.min[0]
667  maxTheta = b.max[0]
668  else:
669  minTheta = max(minTheta, b.min[0])
670  elif minTheta <= b.max[0]:
671  maxTheta = b.max[0]
672  else:
673  minPhi = 90.0
674  maxPhi = -90.0
675  elif minTheta > b.max[0] or maxTheta < b.min[0]:
676  minPhi = 90.0
677  maxPhi = -90.0
678  else:
679  minTheta = max(minTheta, b.min[0])
680  maxTheta = min(maxTheta, b.max[0])
681  self.min = (minTheta, minPhi)
682  self.max = (maxTheta, maxPhi)
683  return self
684 
685  def setEmpty(self):
686  """Empties this spherical box.
687  """
688  self.min = (0.0, 90.0)
689  self.max = (0.0, -90.0)
690  return self
691 
692  def setFull(self):
693  """Expands this spherical box to fill the unit sphere.
694  """
695  self.min = (0.0, -90.0)
696  self.max = (360.0, 90.0)
697  return self
698 
699  def __repr__(self):
700  """Returns a string representation of this spherical box.
701  """
702  if self.isEmpty():
703  return ''.join([self.__class__.__name__, '(', ')'])
704  return ''.join([self.__class__.__name__, '(',
705  repr(self.min), ', ', repr(self.max), ')'])
706 
707  def __eq__(self, other):
708  if isinstance(other, SphericalBox):
709  if self.isEmpty() and other.isEmpty():
710  return True
711  return self.min == other.min and self.max == other.max
712  return False
713 
714  @staticmethod
715  def edge(v1, v2, n):
716  """Returns a spherical bounding box for the great circle edge
717  connecting v1 to v2 with plane normal n. All arguments must be
718  cartesian unit vectors.
719  """
720  theta1, phi1 = sphericalCoords(v1)
721  theta2, phi2 = sphericalCoords(v2)
722  # Compute latitude angle range of the edge
723  minPhi = min(phi1, phi2)
724  maxPhi = max(phi1, phi2)
725  d = n[0] * n[0] + n[1] * n[1]
726  if abs(d) > MIN_FLOAT:
727  # compute the 2 (antipodal) latitude angle extrema of n
728  if abs(n[2]) <= SIN_MIN:
729  ex = (0.0, 0.0, -1.0)
730  else:
731  ex = (n[0] * n[2] / d, n[1] * n[2] / d, -d)
732  # check whether either extremum is inside the edge
733  if between(ex, n, v1, v2):
734  minPhi = min(minPhi, sphericalCoords(ex)[1])
735  ex = (-ex[0], -ex[1], -ex[2])
736  if between(ex, n, v1, v2):
737  maxPhi = max(maxPhi, sphericalCoords(ex)[1])
738  # Compute longitude angle range of the edge
739  if abs(n[2]) <= SIN_MIN:
740  # great circle is very close to a pole
741  d = min(abs(theta1 - theta2), abs(360.0 - theta1 + theta2))
742  if d >= 90.0 and d <= 270.0:
743  # d is closer to 180 than 0/360: edge crosses over a pole
744  minTheta = 0.0
745  maxTheta = 360.0
746  else:
747  # theta1 and theta2 are nearly identical
748  minTheta = min(theta1, theta2)
749  maxTheta = max(theta1, theta2)
750  if maxTheta - minTheta > 180.0:
751  # min/max on opposite sides of 0/360
752  # longitude angle discontinuity
753  tmp = maxTheta
754  maxTheta = minTheta
755  minTheta = tmp
756  elif n[2] > 0.0:
757  minTheta = theta1
758  maxTheta = theta2
759  else:
760  minTheta = theta2
761  maxTheta = theta1
762  # return results
763  return SphericalBox((minTheta, minPhi), (maxTheta, maxPhi))
764 
765 
767  """A circle on the unit sphere. Points falling exactly on the
768  circle are considered to be inside (contained by) the circle.
769  """
770  def __init__(self, center, radius):
771  """Creates a new spherical circle with the given center and radius.
772  """
773  self.center = sphericalCoords(center)
774  self.radius = float(radius)
775  self.boundingBox = None
776  if self.radius < 0.0 or self.radius > 180.0:
777  raise RuntimeError(
778  'Circle radius is negative or greater than 180 deg')
779  self.center = (reduceTheta(self.center[0]), self.center[1])
780 
781  def getBoundingBox(self):
782  """Returns a bounding box for this spherical circle.
783  """
784  if self.boundingBox == None:
785  if self.isEmpty():
786  self.boundingBox = SphericalBox()
787  elif self.isFull():
788  self.boundingBox = SphericalBox()
789  self.boundingBox.setFull()
790  else:
791  alpha = maxAlpha(self.radius, self.center[1])
792  minPhi = clampPhi(self.center[1] - self.radius)
793  maxPhi = clampPhi(self.center[1] + self.radius)
794  if alpha > 180.0 - ANGLE_EPSILON:
795  minTheta = 0.0
796  maxTheta = 360.0
797  else:
798  minTheta = self.center[0] - alpha
799  maxTheta = self.center[0] + alpha
800  self.boundingBox = SphericalBox((minTheta, minPhi),
801  (maxTheta, maxPhi))
802  return self.boundingBox
803 
804  def getBoundingCircle(self):
805  return self
806 
807  def getCenter(self):
808  """Returns an (ra, dec) 2-tuple of floats corresponding to the
809  center of this circle.
810  """
811  return self.center
812 
813  def getRadius(self):
814  """Returns the radius (degrees) of this circle.
815  """
816  return self.radius
817 
818  def isEmpty(self):
819  """Returns True if this circle contains no points.
820  """
821  return self.radius < 0.0
822 
823  def isFull(self):
824  """Returns True if this spherical box contains every point
825  on the unit sphere.
826  """
827  return self.radius >= 180.0
828 
829  def contains(self, pointOrRegion):
830  """Returns True if the specified point or spherical region is
831  completely contained in this circle. Note that the implementation
832  is conservative where ellipses are concerned: False may be returned
833  for an ellipse that is actually completely contained in this circle.
834  """
835  if self.isEmpty():
836  return False
837  pr = pointOrRegion
838  c = self.center
839  r = self.radius
840  if isinstance(pr, SphericalBox):
841  if pr.isEmpty():
842  return False
843  minp = pr.getMin()
844  maxp = pr.getMax()
845  if (sphericalAngularSep(c, minp) > r or
846  sphericalAngularSep(c, maxp) > r or
847  sphericalAngularSep(c, (minp[0], maxp[1])) > r or
848  sphericalAngularSep(c, (maxp[0], minp[1])) > r):
849  return False
850  a = alpha(r, c[1], minp[1])
851  if a != None:
852  if (pr.containsPoint((c[0] + a, minp[1])) or
853  pr.containsPoint((c[0] - a, minp[1]))):
854  return False
855  a = alpha(r, c[1], maxp[1])
856  if a != None:
857  if (pr.containsPoint((c[0] + a, maxp[1])) or
858  pr.containsPoint((c[0] - a, maxp[1]))):
859  return False
860  return True
861  elif isinstance(pr, SphericalCircle):
862  if pr.isEmpty():
863  return False
864  return sphericalAngularSep(c, pr.center) <= r - pr.radius
865  elif isinstance(pr, SphericalEllipse):
866  bc = pr.getBoundingCircle()
867  return sphericalAngularSep(c, bc.center) <= r - bc.radius
868  elif isinstance(pr, SphericalConvexPolygon):
869  p = cartesianUnitVector(c)
870  for v in pr.getVertices():
871  if cartesianAngularSep(p, v) > r:
872  return False
873  return True
874  else:
875  return sphericalAngularSep(c, sphericalCoords(pr)) <= r
876 
877  def intersects(self, pointOrRegion):
878  """Returns True if the given point or spherical region intersects
879  this circle. Note that the implementation is conservative where
880  ellipses are concerned: True may be returned for an ellipse that
881  is actually disjoint from this circle.
882  """
883  if self.isEmpty():
884  return False
885  pr = pointOrRegion
886  c = self.center
887  r = self.radius
888  if isinstance(pr, SphericalBox):
889  if pr.isEmpty():
890  return False
891  elif pr.containsPoint(c):
892  return True
893  minp = pr.getMin()
894  maxp = pr.getMax()
895  if (minPhiEdgeSep(c, minp[1], minp[0], maxp[0]) <= r or
896  minPhiEdgeSep(c, maxp[1], minp[0], maxp[0]) <= r):
897  return True
898  p = cartesianUnitVector(c)
899  return (minThetaEdgeSep(p, minp[0], minp[1], maxp[1]) <= r or
900  minThetaEdgeSep(p, maxp[0], minp[1], maxp[1]) <= r)
901  elif isinstance(pr, SphericalCircle):
902  if pr.isEmpty():
903  return False
904  return sphericalAngularSep(c, pr.center) <= r + pr.radius
905  elif isinstance(pr, SphericalEllipse):
906  bc = pr.getBoundingCircle()
907  return sphericalAngularSep(c, bc.center) <= r + bc.radius
908  elif isinstance(pr, SphericalConvexPolygon):
909  return pr.intersects(self)
910  else:
911  return sphericalAngularSep(c, sphericalCoords(pr)) <= r
912 
913  def __repr__(self):
914  """Returns a string representation of this circle.
915  """
916  return ''.join([self.__class__.__name__ , '(', repr(self.center),
917  ', ', repr(self.radius), ')'])
918 
919  def __eq__(self, other):
920  if isinstance(other, SphericalCircle):
921  if self.isEmpty() and other.isEmpty():
922  return True
923  if self.radius == other.radius:
924  if self.center[1] == other.center[1]:
925  if abs(self.center[1]) == 90.0:
926  return True
927  return self.center[0] == other.center[0]
928  return False
929 
930 
932  """An ellipse on the unit sphere. This is a standard 2D cartesian
933  ellipse defined on the plane tangent to the unit sphere at the ellipse
934  center and then orthographically projected onto the surface of the
935  unit sphere.
936  """
937  def __init__(self, center,
938  semiMajorAxisLength, semiMinorAxisLength, majorAxisAngle):
939  self.center = sphericalCoords(center)
940  self.semiMajorAxisLength = float(semiMajorAxisLength)
941  self.semiMinorAxisLength = float(semiMinorAxisLength)
942  a = math.fmod(float(majorAxisAngle), 180.0)
943  if a < 0.0:
944  a += 180.0
945  self.majorAxisAngle = a
946  self.boundingCircle = None
947  self.innerCircle = None
948  if self.semiMinorAxisLength < 0.0:
949  raise RuntimeError('Negative semi-minor axis length')
951  raise RuntimeError(
952  'Semi-major axis length is less than semi-minor axis length')
953  # large spherical ellipses don't make much sense
954  if self.semiMajorAxisLength > 10.0 * ARCSEC_PER_DEG:
955  raise RuntimeError(
956  'Semi-major axis length must be less than or equal to 10 deg')
957  self.center = (reduceTheta(self.center[0]), self.center[1])
958 
959  def getBoundingBox(self):
960  """Returns a bounding box for this spherical ellipse. Note that at
961  present this is conservative: a bounding box for the circle C with
962  radius equal to the semi-major axis length of this ellipse is returned.
963  """
964  return self.getBoundingCircle().getBoundingBox()
965 
966  def getBoundingCircle(self):
967  """Returns a bounding circle for this spherical ellipse. This is
968  a circle with the same center as this ellipse and with radius
969  equal to the arcsine of the semi-major axis length.
970  """
971  if self.boundingCircle == None:
972  r = math.degrees(math.asin(math.radians(
973  DEG_PER_ARCSEC * self.semiMajorAxisLength)))
974  self.boundingCircle = SphericalCircle(self.center, r)
975  return self.boundingCircle
976 
977  def getInnerCircle(self):
978  """Returns the circle of maximum radius having the same center as
979  this ellipse and which is completely contained in the ellipse.
980  """
981  if self.innerCircle == None:
982  r = math.degrees(math.asin(math.radians(
983  DEG_PER_ARCSEC * self.semiMinorAxisLength)))
984  self.innerCircle = SphericalCircle(self.center, r)
985  return self.innerCircle
986 
987  def getCenter(self):
988  """Returns an (ra, dec) 2-tuple of floats corresponding to the center
989  of this ellipse.
990  """
991  return self.center
992 
993  def getMajorAxisAngle(self):
994  """Return the major axis angle (east of north, in degrees) for this
995  ellipse.
996  """
997  return self.majorAxisAngle
998 
1000  """Returns the semi-major axis length of this ellipse. Units
1001  are in arcsec since ellipses are typically small.
1002  """
1003  return self.semiMajorAxisLength
1004 
1006  """Returns the semi-minor axis length of this ellipse. Units
1007  are in arcsec since ellipses are typically small.
1008  """
1009  return self.semiMinorAxisLength
1010 
1011  def _containsPoint(self, v):
1012  theta = math.radians(self.center[0])
1013  phi = math.radians(self.center[1])
1014  ang = math.radians(self.majorAxisAngle)
1015  sinTheta = math.sin(theta)
1016  cosTheta = math.cos(theta)
1017  sinPhi = math.sin(phi)
1018  cosPhi = math.cos(phi)
1019  sinAng = math.sin(ang)
1020  cosAng = math.cos(ang)
1021  # get coords of input point in (N,E) basis
1022  n = cosPhi * v[2] - sinPhi * (sinTheta * v[1] + cosTheta * v[0])
1023  e = cosTheta * v[1] - sinTheta * v[0]
1024  # rotate by negated major axis angle
1025  x = sinAng * e + cosAng * n
1026  y = cosAng * e - sinAng * n
1027  # scale by inverse of semi-axis-lengths
1028  x /= math.radians(self.semiMajorAxisLength * DEG_PER_ARCSEC)
1029  y /= math.radians(self.semiMinorAxisLength * DEG_PER_ARCSEC)
1030  # Apply point in circle test for the unit circle centered at the origin
1031  return x * x + y * y <= 1.0
1032 
1033  def contains(self, pointOrRegion):
1034  """Returns True if the specified point or spherical region is
1035  completely contained in this ellipse. The implementation is
1036  conservative in the sense that False may be returned for a region
1037  that really is contained by this ellipse.
1038  """
1039  if isinstance(pointOrRegion, (tuple, list)):
1040  v = cartesianUnitVector(pointOrRegion)
1041  return self._containsPoint(v)
1042  else:
1043  return self.getInnerCircle().contains(pointOrRegion)
1044 
1045  def intersects(self, pointOrRegion):
1046  """Returns True if the specified point or spherical region intersects
1047  this ellipse. The implementation is conservative in the sense that
1048  True may be returned for a region that does not intersect this
1049  ellipse.
1050  """
1051  if isinstance(pointOrRegion, (tuple, list)):
1052  v = cartesianUnitVector(pointOrRegion)
1053  return self._containsPoint(v)
1054  else:
1055  return self.getBoundingCircle().intersects(pointOrRegion)
1056 
1057  def __repr__(self):
1058  """Returns a string representation of this ellipse.
1059  """
1060  return ''.join([
1061  self.__class__.__name__ , '(', repr(self.center), ', ',
1062  repr(self.semiMajorAxisLength), ', ',
1063  repr(self.semiMinorAxisLength), ', ',
1064  repr(self.majorAxisAngle), ')'])
1065 
1066  def __eq__(self, other):
1067  if isinstance(other, SphericalEllipse):
1068  return (self.center == other.center and
1069  self.semiMajorAxisLength == other.semiMajorAxisLength and
1070  self.semiMinorAxisLength == other.semiMinorAxisLength and
1071  self.majorAxisAngle == other.majorAxisAngle)
1072  return False
1073 
1074 
1076  """A convex polygon on the unit sphere with great circle edges. Points
1077  falling exactly on polygon edges are considered to be inside (contained
1078  by) the polygon.
1079  """
1080  def __init__(self, *args):
1081  """Creates a new polygon. Arguments must be either:
1082 
1083  1. a SphericalConvexPolygon
1084  2. a list of vertices
1085  3. a list of vertices and a list of corresponding edges
1086 
1087  In the first case, a copy is constructed. In the second case,
1088  the argument must be a sequence of 3 element tuples/lists
1089  (unit cartesian 3-vectors) - a copy of the vertices is stored
1090  and polygon edges are computed. In the last case, copies of the
1091  input vertex and edge lists are stored.
1092 
1093  Vertices must be hemispherical and in counter-clockwise order when
1094  viewed from outside the unit sphere in a right handed coordinate
1095  system. They must also form a convex polygon.
1096 
1097  When edges are specified, the i-th edge must correspond to the plane
1098  equation of great circle connecting vertices (i - 1, i), that is,
1099  the edge should be a unit cartesian 3-vector parallel to v[i-1] ^ v[i]
1100  (where ^ denotes the vector cross product).
1101 
1102  Note that these conditions are NOT verified for performance reasons.
1103  Operations on SphericalConvexPolygon objects constructed with inputs
1104  not satisfying these conditions are undefined. Use the convex()
1105  function to check for convexity and ordering of the vertices. Or,
1106  use the convexHull() function to create a SphericalConvexPolygon
1107  from an arbitrary list of hemispherical input vertices.
1108  """
1109  self.boundingBox = None
1110  self.boundingCircle = None
1111  if len(args) == 0:
1112  raise RuntimeError('Expecting at least one argument')
1113  elif len(args) == 1:
1114  if isinstance(args[0], SphericalConvexPolygon):
1115  self.vertices = list(args[0].vertices)
1116  self.edges = list(args[0].edges)
1117  else:
1118  self.vertices = list(args[0])
1119  self._computeEdges()
1120  elif len(args) == 2:
1121  self.vertices = list(args[0])
1122  self.edges = list(args[1])
1123  if len(self.edges) != len(self.vertices):
1124  raise RuntimeError(
1125  'number of edges does not match number of vertices')
1126  else:
1127  self.vertices = list(args)
1128  self._computeEdges()
1129  if len(self.vertices) < 3:
1130  raise RuntimeError(
1131  'spherical polygon must contain at least 3 vertices')
1132 
1133  def _computeEdges(self):
1134  """Computes edge plane normals from vertices.
1135  """
1136  v = self.vertices
1137  n = len(v)
1138  edges = []
1139  for i in xrange(n):
1140  edges.append(normalize(cross(v[i - 1], v[i])))
1141  self.edges = edges
1142 
1143  def getVertices(self):
1144  """Returns the list of polygon vertices.
1145  """
1146  return self.vertices
1147 
1148  def getEdges(self):
1149  """Returns the list of polygon edges. The i-th edge is the plane
1150  equation for the great circle edge formed by vertices i-1 and i.
1151  """
1152  return self.edges
1153 
1155  """Returns a bounding circle (not necessarily minimal) for this
1156  spherical convex polygon.
1157  """
1158  if self.boundingCircle == None:
1159  center = centroid(self.vertices)
1160  radius = 0.0
1161  for v in self.vertices:
1162  radius = max(radius, cartesianAngularSep(center, v))
1163  self.boundingCircle = SphericalCircle(center, radius)
1164  return self.boundingCircle
1165 
1166  def getBoundingBox(self):
1167  """Returns a bounding box for this spherical convex polygon.
1168  """
1169  if self.boundingBox == None:
1170  self.boundingBox = SphericalBox()
1171  for i in xrange(0, len(self.vertices)):
1172  self.boundingBox.extend(SphericalBox.edge(
1173  self.vertices[i - 1], self.vertices[i], self.edges[i]))
1174  return self.boundingBox
1175 
1176  def getZRange(self):
1177  """Returns the z coordinate range spanned by this spherical
1178  convex polygon.
1179  """
1180  bbox = self.getBoundingBox()
1181  return (math.sin(math.radians(bbox.getMin()[1])),
1182  math.sin(math.radians(bbox.getMax()[1])))
1183 
1184  def clip(self, plane):
1185  """Returns the polygon corresponding to the intersection of this
1186  polygon with the positive half space defined by the given plane.
1187  The plane must be specified with a cartesian unit vector (its
1188  normal) and always passes through the origin.
1189 
1190  Clipping is performed using the Sutherland-Hodgman algorithm,
1191  adapted for spherical polygons.
1192  """
1193  vertices, edges, classification = [], [], []
1194  vin, vout = False, False
1195  for i in xrange(len(self.vertices)):
1196  d = dot(plane, self.vertices[i])
1197  if d > SIN_MIN: vin = True
1198  elif d < -SIN_MIN: vout = True
1199  else: d = 0.0
1200  classification.append(d)
1201  if not vin and not vout:
1202  # polygon and clipping plane are coplanar
1203  return None
1204  if not vout:
1205  return self
1206  elif not vin:
1207  return None
1208  v0 = self.vertices[-1]
1209  d0 = classification[-1]
1210  for i in xrange(len(self.vertices)):
1211  v1 = self.vertices[i]
1212  d1 = classification[i]
1213  if d0 > 0.0:
1214  if d1 >= 0.0:
1215  # positive to positive, positive to zero: no intersection,
1216  # add current input vertex to output polygon
1217  vertices.append(v1)
1218  edges.append(self.edges[i])
1219  else:
1220  # positive to negative: add intersection point to polygon
1221  f = d0 / (d0 - d1)
1222  v = normalize((v0[0] + (v1[0] - v0[0]) * f,
1223  v0[1] + (v1[1] - v0[1]) * f,
1224  v0[2] + (v1[2] - v0[2]) * f))
1225  vertices.append(v)
1226  edges.append(self.edges[i])
1227  elif d0 == 0.0:
1228  if d1 >= 0.0:
1229  # zero to positive: no intersection, add current input
1230  # vertex to output polygon.
1231  vertices.append(v1)
1232  edges.append(self.edges[i])
1233  # zero to zero: coplanar edge - since the polygon has vertices
1234  # on both sides of plane, this implies concavity or a
1235  # duplicate vertex. Under the convexity assumption, this
1236  # must be caused by a near duplicate vertex, so skip the
1237  # vertex.
1238  #
1239  # zero to negative: no intersection, skip the vertex
1240  else:
1241  if d1 > 0.0:
1242  # negative to positive: add intersection point to output
1243  # polygon followed by the current input vertex
1244  f = d0 / (d0 - d1)
1245  v = normalize((v0[0] + (v1[0] - v0[0]) * f,
1246  v0[1] + (v1[1] - v0[1]) * f,
1247  v0[2] + (v1[2] - v0[2]) * f))
1248  vertices.append(v)
1249  edges.append(tuple(plane))
1250  vertices.append(v1)
1251  edges.append(self.edges[i])
1252  elif d1 == 0.0:
1253  # negative to zero: add current input vertex to output
1254  # polygon
1255  vertices.append(v1)
1256  edges.append(tuple(plane))
1257  # negative to negative: no intersection, skip vertex
1258  v0 = v1
1259  d0 = d1
1260  return SphericalConvexPolygon(vertices, edges)
1261 
1262  def intersect(self, poly):
1263  """Returns the intersection of poly with this polygon, or
1264  None if the intersection does not exist.
1265  """
1266  if not isinstance(poly, SphericalConvexPolygon):
1267  raise TypeError('Expecting a SphericalConvexPolygon object')
1268  p = self
1269  for edge in poly.getEdges():
1270  p = p.clip(edge)
1271  if p == None:
1272  break
1273  return p
1274 
1275  def area(self):
1276  """Returns the area of this spherical convex polygon.
1277  """
1278  numVerts = len(self.vertices)
1279  angleSum = 0.0
1280  for i in xrange(numVerts):
1281  tmp = cross(self.edges[i - 1], self.edges[i])
1282  sina = math.sqrt(dot(tmp, tmp))
1283  cosa = -dot(self.edges[i - 1], self.edges[i])
1284  a = math.atan2(sina, cosa)
1285  angleSum += a
1286  return angleSum - (numVerts - 2) * math.pi
1287 
1288  def containsPoint(self, v):
1289  for e in self.edges:
1290  if dot(v, e) < 0.0:
1291  return False
1292  return True
1293 
1294  def contains(self, pointOrRegion):
1295  """Returns True if the specified point or spherical region is
1296  completely contained in this polygon. Note that the implementation
1297  is conservative where ellipses are concerned: False may be returned
1298  for an ellipse that is actually completely contained by this polygon.
1299  """
1300  pr = pointOrRegion
1301  if isinstance(pr, SphericalConvexPolygon):
1302  for v in pr.getVertices():
1303  if not self.containsPoint(v):
1304  return False
1305  return True
1306  elif isinstance(pr, SphericalEllipse):
1307  return self.contains(pr.getBoundingCircle())
1308  elif isinstance(pr, SphericalCircle):
1309  cv = cartesianUnitVector(pr.getCenter())
1310  if not self.containsPoint(cv):
1311  return False
1312  else:
1313  minSep = INF
1314  for i in xrange(len(self.vertices)):
1315  s = minEdgeSep(cv, self.edges[i],
1316  self.vertices[i - 1], self.vertices[i])
1317  minSep = min(minSep, s)
1318  return minSep >= pr.getRadius()
1319  elif isinstance(pr, SphericalBox):
1320  # check that all box vertices are inside the polygon
1321  bMin, bMax = pr.getMin(), pr.getMax()
1322  bzMin = math.sin(math.radians(bMin[1]))
1323  bzMax = math.sin(math.radians(bMax[1]))
1324  verts = map(cartesianUnitVector,
1325  (bMin, bMax, (bMin[0], bMax[1]), (bMax[0], bMin[1])))
1326  for v in verts:
1327  if not self.containsPoint(v):
1328  return False
1329  # check that intersections of box small circles with polygon
1330  # edges either don't exist or fall outside the box.
1331  for i in xrange(len(self.vertices)):
1332  v0 = self.vertices[i - 1]
1333  v1 = self.vertices[i]
1334  e = self.edges[i]
1335  d = e[0] * e[0] + e[1] * e[1]
1336  if abs(e[2]) >= COS_MAX or d < MIN_FLOAT:
1337  # polygon edge is approximately described by z = +/-1.
1338  # box vertices are inside the polygon, so they
1339  # cannot intersect the edge.
1340  continue
1341  D = d - bzMin * bzMin
1342  if D >= 0.0:
1343  # polygon edge intersects z = bzMin
1344  D = math.sqrt(D)
1345  xr = -e[0] * e[2] * bzMin
1346  yr = -e[1] * e[2] * bzMin
1347  i1 = ((xr + e[1] * D) / d, (yr - e[0] * D) / d, bzMin)
1348  i2 = ((xr - e[1] * D) / d, (yr + e[0] * D) / d, bzMin)
1349  if (pr.containsPoint(sphericalCoords(i1)) or
1350  pr.containsPoint(sphericalCoords(i2))):
1351  return False
1352  D = d - bzMax * bzMax
1353  if D >= 0.0:
1354  # polygon edge intersects z = bzMax
1355  D = math.sqrt(D)
1356  xr = -e[0] * e[2] * bzMax
1357  yr = -e[1] * e[2] * bzMax
1358  i1 = ((xr + e[1] * D) / d, (yr - e[0] * D) / d, bzMax)
1359  i2 = ((xr - e[1] * D) / d, (yr + e[0] * D) / d, bzMax)
1360  if (pr.containsPoint(sphericalCoords(i1)) or
1361  pr.containsPoint(sphericalCoords(i2))):
1362  return False
1363  return True
1364  else:
1365  return self.containsPoint(cartesianUnitVector(pr))
1366 
1367  def intersects(self, pointOrRegion):
1368  """Returns True if the given point or spherical region intersects
1369  this polygon. Note that the implementation is conservative where
1370  ellipses are concerned: True may be returned for an ellipse that
1371  is actually disjoint from this polygon.
1372  """
1373  pr = pointOrRegion
1374  if isinstance(pr, SphericalConvexPolygon):
1375  return self.intersect(pr) != None
1376  elif isinstance(pr, SphericalEllipse):
1377  return self.intersects(pr.getBoundingCircle())
1378  elif isinstance(pr, SphericalCircle):
1379  cv = cartesianUnitVector(pr.getCenter())
1380  if self.containsPoint(cv):
1381  return True
1382  else:
1383  minSep = INF
1384  for i in xrange(len(self.vertices)):
1385  s = minEdgeSep(cv, self.edges[i],
1386  self.vertices[i - 1], self.vertices[i])
1387  minSep = min(minSep, s)
1388  return minSep <= pr.getRadius()
1389  elif isinstance(pr, SphericalBox):
1390  minTheta = math.radians(pr.getMin()[0])
1391  maxTheta = math.radians(pr.getMax()[0])
1392  bzMin = math.sin(math.radians(pr.getMin()[1]))
1393  bzMax = math.sin(math.radians(pr.getMax()[1]))
1394  p = self.clip((-math.sin(minTheta), math.cos(minTheta), 0.0))
1395  if pr.getThetaExtent() > 180.0:
1396  if p != None:
1397  zMin, zMax = p.getZRange()
1398  if zMin <= bzMax and zMax >= bzMin:
1399  return True
1400  p = self.clip((math.sin(maxTheta), -math.cos(maxTheta), 0.0))
1401  else:
1402  if p != None:
1403  p = p.clip((math.sin(maxTheta), -math.cos(maxTheta), 0.0))
1404  if p == None:
1405  return False
1406  zMin, zMax = p.getZRange()
1407  return zMin <= bzMax and zMax >= bzMin
1408  else:
1409  return self.containsPoint(cartesianUnitVector(pr))
1410 
1411  def __repr__(self):
1412  """Returns a string representation of this polygon.
1413  """
1414  return ''.join([self.__class__.__name__ , '(',
1415  repr(map(sphericalCoords, self.vertices)), ')'])
1416 
1417  def __eq__(self, other):
1418  if not isinstance(other, SphericalConvexPolygon):
1419  return False
1420  n = len(self.vertices)
1421  if n != len(other.vertices):
1422  return False
1423  # Test for equality up to cyclic permutation of vertices/edges
1424  try:
1425  offset = other.vertices.index(self.vertices[0])
1426  except ValueError:
1427  return False
1428  for i in xrange(0, n):
1429  j = offset + i
1430  if j >= n:
1431  j -= n
1432  if (self.vertices[i] != self.vertices[j] or
1433  self.edges[i] != self.edges[j]):
1434  return False
1435  return True
1436 
1437 
1438 # -- Finding the median element of an array in linear time ----
1439 #
1440 # This is a necessary primitive for Megiddo's linear time
1441 # 2d linear programming algorithm.
1442 
1443 def _partition(array, left, right, i):
1444  """Partitions an array around the pivot value at index i,
1445  returning the new index of the pivot value.
1446  """
1447  pivot = array[i]
1448  array[i] = array[right - 1]
1449  j = left
1450  for k in xrange(left, right - 1):
1451  if array[k] < pivot:
1452  tmp = array[j]
1453  array[j] = array[k]
1454  array[k] = tmp
1455  j += 1
1456  array[right - 1] = array[j]
1457  array[j] = pivot
1458  return j
1459 
1460 def _medianOfN(array, i, n):
1461  """Finds the median of n consecutive elements in an array starting
1462  at index i (efficient for small n). The index of the median element
1463  is returned.
1464  """
1465  if n == 1:
1466  return i
1467  k = n >> 1
1468  e1 = i + k + 1
1469  e2 = i + n
1470  for j in xrange(i, e1):
1471  minIndex = j
1472  minValue = array[j]
1473  for s in xrange(j + 1, e2):
1474  if array[s] < minValue:
1475  minIndex = s
1476  minValue = array[s]
1477  tmp = array[j]
1478  array[j] = array[minIndex]
1479  array[minIndex] = tmp
1480  return i + k
1481 
1482 def _medianOfMedians(array, left, right):
1483  """Returns the "median of medians" for an array. This primitive is used
1484  for pivot selection in the median finding algorithm.
1485  """
1486  while True:
1487  if right - left <= 5:
1488  return _medianOfN(array, left, right - left)
1489  i = left
1490  j = left
1491  while i + 4 < right:
1492  k = _medianOfN(array, i, 5)
1493  tmp = array[j]
1494  array[j] = array[k]
1495  array[k] = tmp
1496  j += 1
1497  i += 5
1498  right = j
1499 
1500 def median(array):
1501  """Finds the median element of the given array in linear time.
1502  """
1503  left = 0
1504  right = len(array)
1505  if right == 0:
1506  return None
1507  k = right >> 1
1508  while True:
1509  i = _medianOfMedians(array, left, right)
1510  i = _partition(array, left, right, i)
1511  if k == i:
1512  return array[k]
1513  elif k < i:
1514  right = i
1515  else:
1516  left = i + 1
1517 
1518 
1519 # -- Testing whether a set of points is hemispherical ----
1520 #
1521 # This test is used by the convexity test and convex hull algorithm. It is
1522 # implemented using Megiddo's algorithm for linear programming in R2, see:
1523 #
1524 # Megiddo, N. 1982. Linear-time algorithms for linear programming in R3 and related problems.
1525 # In Proceedings of the 23rd Annual Symposium on Foundations of Computer Science (November 03 - 05, 1982).
1526 # SFCS. IEEE Computer Society, Washington, DC, 329-338. DOI= http://dx.doi.org/10.1109/SFCS.1982.74
1527 
1528 def _prune(constraints, xmin, xmax, op):
1529  """Removes redundant constraints and reports intersection points
1530  of non-redundant pairs. The constraint list is modified in place.
1531  """
1532  intersections = []
1533  i = 0
1534  while i < len(constraints) - 1:
1535  a1, b1 = constraints[i]
1536  a2, b2 = constraints[i + 1]
1537  # if constraints are near parallel or intersect to the left or right
1538  # of the feasible x range, remove the higher/lower constraint
1539  da = a1 - a2
1540  if abs(da) < MIN_FLOAT / EPSILON:
1541  xi = INF
1542  else:
1543  xi = (b2 - b1) / da
1544  xierr = 2.0 * EPSILON * abs(xi)
1545  if isinf(xi):
1546  if op(b1, b2):
1547  constraints[i + 1] = constraints[-1]
1548  else:
1549  constraints[i] = constraints[-1]
1550  constraints.pop()
1551  else:
1552  if xi + xierr <= xmin:
1553  if op(a1, a2):
1554  constraints[i + 1] = constraints[-1]
1555  else:
1556  constraints[i] = constraints[-1]
1557  constraints.pop()
1558  elif xi - xierr >= xmax:
1559  if op(a1, a2):
1560  constraints[i] = constraints[-1]
1561  else:
1562  constraints[i + 1] = constraints[-1]
1563  constraints.pop()
1564  else:
1565  # save intersection for later
1566  intersections.append((xi, xierr))
1567  i += 2
1568  return intersections
1569 
1570 def _vrange(x, xerr, a, b):
1571  p = a * (x - xerr)
1572  v = p + b
1573  verr = EPSILON * abs(p) + EPSILON * abs(v)
1574  vmin = v - verr
1575  vmax = v + verr
1576  p = a * (x + xerr)
1577  v = p + b
1578  verr = EPSILON * abs(p) + EPSILON * abs(v)
1579  vmin = min(vmin, v - verr)
1580  vmax = max(vmax, v + verr)
1581  return vmin, vmax
1582 
1583 def _gh(constraints, x, xerr, op):
1584  a, b = constraints[0]
1585  amin, amax = a, a
1586  vmin, vmax = _vrange(x, xerr, a, b)
1587  for i in xrange(1, len(constraints)):
1588  a, b = constraints[i]
1589  vimin, vimax = _vrange(x, xerr, a, b)
1590  if vimax < vmin or vimin > vmax:
1591  if op(vimin, vmin):
1592  amin = a
1593  amax = a
1594  vmin = vimin
1595  vmax = vimax
1596  else:
1597  amin = min(amin, a)
1598  amax = max(amax, a)
1599  return vmin, vmax, amin, amax
1600 
1601 def _feasible2D(points, z):
1602  I1 = []
1603  I2 = []
1604  xmin = NEG_INF
1605  xmax = INF
1606  # transform each constraint of the form x*v[0] + y*v[1] + z*v[2] > 0
1607  # into y op a*x + b or x op c, where op is < or >
1608  for v in points:
1609  if abs(v[1]) <= MIN_FLOAT:
1610  if abs(v[0]) <= MIN_FLOAT:
1611  if z * v[2] <= 0.0:
1612  # inequalities trivially lack a solution
1613  return False
1614  # current inequality is trivially true, skip it
1615  else:
1616  xlim = - z * v[2] / v[0]
1617  if v[0] > 0.0:
1618  xmin = max(xmin, xlim)
1619  else:
1620  xmax = min(xmax, xlim)
1621  if xmax <= xmin:
1622  # inequalities trivially lack a solution
1623  return False
1624  else:
1625  # finite since |z|, |v[i]| <= 1 and 1/MIN_FLOAT is finite
1626  coeffs = (v[0] / v[1], - z * v[2] / v[1])
1627  if v[1] > 0.0:
1628  I1.append(coeffs)
1629  else:
1630  I2.append(coeffs)
1631  # At this point (xmin, xmax) is non-empty - if either I1 or I2 is empty
1632  # then a solution trivially exists
1633  if len(I1) == 0 or len(I2) == 0:
1634  return True
1635  # Check for a feasible solution to the inequalities I1 of the form
1636  # form y > a*x + b, I2 of the form y < a*x + b, x > xmin and x < xmax
1637  numConstraints = 0
1638  while True:
1639  intersections = _prune(I1, xmin, xmax, operator.gt)
1640  intersections.extend(_prune(I2, xmin, xmax, operator.lt))
1641  if len(intersections) == 0:
1642  # I1 and I2 each contain exactly one constraint
1643  a1, b1 = I1[0]
1644  a2, b2 = I2[0]
1645  if a1 == a2:
1646  xi = INF
1647  else:
1648  xi = (b2 - b1) / (a1 - a2)
1649  if isinf(xi):
1650  return b1 < b2
1651  return (xi > xmin or a1 < a2) and (xi < xmax or a1 > a2)
1652  elif numConstraints == len(I1) + len(I2):
1653  # No constraints were pruned during search interval refinement,
1654  # and g was not conclusively less than h. Conservatively return
1655  # False to avoid an infinite loop.
1656  return False
1657  numConstraints = len(I1) + len(I2)
1658  x, xerr = median(intersections)
1659  # If g(x) < h(x), x is a feasible solution. Otherwise, refine the
1660  # search interval by examining the one-sided derivates of g/h.
1661  gmin, gmax, sg, Sg = _gh(I1, x, xerr, operator.gt)
1662  hmin, hmax, sh, Sh = _gh(I2, x, xerr, operator.lt)
1663  if gmax <= hmin:
1664  return True
1665  elif sg > Sh:
1666  xmax = x + xerr
1667  elif Sg < sh:
1668  xmin = x - xerr
1669  else:
1670  return False
1671 
1672 def _feasible1D(points, y):
1673  xmin = NEG_INF
1674  xmax = INF
1675  for v in points:
1676  if abs(v[0]) <= MIN_FLOAT:
1677  if y * v[1] <= 0.0:
1678  return False
1679  # inequality is trivially true, skip it
1680  else:
1681  xlim = - y * v[1] / v[0]
1682  if v[0] > 0.0:
1683  xmin = max(xmin, xlim)
1684  else:
1685  xmax = min(xmax, xlim)
1686  if xmax <= xmin:
1687  return False
1688  return True
1689 
1690 def hemispherical(points):
1691  """Tests whether a set of points is hemispherical, i.e. whether a plane
1692  exists such that all points are strictly on one side of the plane. The
1693  algorithm used is Megiddo's algorithm for linear programming in R2 and
1694  has run-time O(n), where n is the number of points. Points must be passed
1695  in as a list of cartesian unit vectors.
1696  """
1697  # Check whether the set of linear equations
1698  # x * v[0] + y * v[1] + z * v[2] > 0.0 (for v in points)
1699  # has a solution (x, y, z). If (x,y,z) is a solution (is feasible),
1700  # so is C*(x,y,z), C > 0. Therefore we can fix z to 1, -1 and
1701  # perform 2D feasibility tests.
1702  if _feasible2D(points, 1.0):
1703  return True
1704  if _feasible2D(points, -1.0):
1705  return True
1706  # At this point a feasible solution must have z = 0. Fix y to 1, -1 and
1707  # perform 1D feasibility tests.
1708  if _feasible1D(points, 1.0):
1709  return True
1710  if _feasible1D(points, -1.0):
1711  return True
1712  # At this point a feasible solution must have y = z = 0. If all v[0]
1713  # are non-zero and have the same sign, then there is a feasible solution.
1714  havePos = False
1715  haveNeg = False
1716  for v in points:
1717  if v[0] > 0.0:
1718  if haveNeg:
1719  return False
1720  havePos = True
1721  elif v[0] < 0.0:
1722  if havePos:
1723  return False
1724  haveNeg = True
1725  else:
1726  return False
1727  return True
1728 
1729 
1730 # -- Convexity test and convex hull algorithm ----
1731 
1732 def convex(vertices):
1733  """Tests whether an ordered list of vertices (which must be specified
1734  as cartesian unit vectors) form a spherical convex polygon and determines
1735  their winding order. Returns a 2-tuple as follows:
1736 
1737  (True, True): The vertex list forms a spherical convex polygon and is in
1738  counter-clockwise order when viewed from outside the unit
1739  sphere in a right handed coordinate system.
1740  (True, False): The vertex list forms a spherical convex polygon and is in
1741  in clockwise order.
1742  (False, msg): The vertex list does not form a spherical convex polygon -
1743  msg is a string describing why the test failed.
1744 
1745  The algorithm completes in O(n) time, where n is the number of
1746  input vertices.
1747  """
1748  if len(vertices) < 3:
1749  return (False, '3 or more vertices must be specified')
1750  if not hemispherical(vertices):
1751  return (False, 'vertices are not hemispherical')
1752  center = centroid(vertices)
1753  windingAngle = 0.0
1754  clockwise = False
1755  counterClockwise = False
1756  p1 = cross(center, vertices[-1])
1757  n2 = dot(p1, p1)
1758  if abs(n2) < CROSS_N2MIN:
1759  return (False, 'centroid of vertices is too close to a vertex')
1760  for i in xrange(len(vertices)):
1761  beg = vertices[i - 2]
1762  mid = vertices[i - 1]
1763  end = vertices[i]
1764  plane = cross(mid, end)
1765  n2 = dot(plane, plane)
1766  if dot(mid, end) >= COS_MAX or n2 < CROSS_N2MIN:
1767  return (False, 'vertex list contains [near-]duplicates')
1768  plane = invScale(plane, math.sqrt(n2))
1769  d = dot(plane, beg)
1770  if d > SIN_MIN:
1771  if clockwise:
1772  return (False, 'vertices wind around centroid in both ' +
1773  'clockwise and counter-clockwise order')
1774  counterClockwise = True
1775  elif d < -SIN_MIN:
1776  if counterClockwise:
1777  return (False, 'vertices wind around centroid in both ' +
1778  'clockwise and counter-clockwise order')
1779  clockwise = True
1780  # center must be inside polygon formed by vertices if they form a
1781  # convex polygon - an equivalent check is to test that polygon
1782  # vertices always wind around center in the same direction.
1783  d = dot(plane, center)
1784  if d < SIN_MIN and counterClockwise or d > -SIN_MIN and clockwise:
1785  return (False, 'centroid of vertices is not conclusively ' +
1786  'inside all edges')
1787  # sum up winding angle for edge (mid, end)
1788  p2 = cross(center, end)
1789  n2 = dot(p2, p2)
1790  if abs(n2) < CROSS_N2MIN:
1791  return (False, 'centroid of vertices is too close to a vertex')
1792  windingAngle += cartesianAngularSep(p1, p2)
1793  p1 = p2
1794  # For convex polygons, the closest multiple of 360 to
1795  # windingAngle is 1.
1796  m = math.floor(windingAngle / 360.0)
1797  if m == 0.0 and windingAngle > 180.0 or m == 1.0 and windingAngle < 540.0:
1798  return (True, counterClockwise)
1799  return (False, 'vertices do not completely wind around centroid, or ' +
1800  'wind around it multiple times')
1801 
1802 def convexHull(points):
1803  """Computes the convex hull (a spherical convex polygon) of an unordered
1804  list of points on the unit sphere, which must be passed in as cartesian
1805  unit vectors. The algorithm takes O(n log n) time, where n is the number
1806  of points.
1807  """
1808  if len(points) < 3:
1809  return None
1810  if not hemispherical(points):
1811  return None
1812  center = centroid(points)
1813  maxSep = 0.0
1814  extremum = 0
1815  # Vertex furthest from the center is on the hull
1816  for i in xrange(len(points)):
1817  sep = cartesianAngularSep(points[i], center)
1818  if sep > maxSep:
1819  extremum = i
1820  maxSep = sep
1821  anchor = points[extremum]
1822  refPlane = cross(center, anchor)
1823  n2 = dot(refPlane, refPlane)
1824  if n2 < CROSS_N2MIN:
1825  # vertex and center are too close
1826  return None
1827  refPlane = invScale(refPlane, math.sqrt(n2))
1828  # Order points by winding angle from the first (extreme) vertex
1829  av = [(0.0, anchor)]
1830  for i in xrange(0, len(points)):
1831  if i == extremum:
1832  continue
1833  v = points[i]
1834  plane = cross(center, v)
1835  n2 = dot(plane, plane)
1836  if n2 >= CROSS_N2MIN:
1837  plane = invScale(plane, math.sqrt(n2))
1838  p = cross(refPlane, plane)
1839  sa = math.sqrt(dot(p, p))
1840  if dot(p, center) < 0.0:
1841  sa = -sa
1842  angle = math.atan2(sa, dot(refPlane, plane))
1843  if angle < 0.0:
1844  angle += 2.0 * math.pi
1845  av.append((angle, v))
1846  if len(av) < 3:
1847  return None
1848  av.sort(key=lambda t: t[0]) # stable, so av[0] still contains anchor
1849  # Loop over vertices using a Graham scan adapted for spherical geometry.
1850  # Chan's algorithm would be an improvement, but seems likely to be slower
1851  # for small vertex counts (the expected case).
1852  verts = [anchor]
1853  edges = [None]
1854  edge = None
1855  i = 1
1856  while i < len(av):
1857  v = av[i][1]
1858  p = cross(anchor, v)
1859  n2 = dot(p, p)
1860  if dot(anchor, v) < COS_MAX and n2 >= CROSS_N2MIN:
1861  if edge == None:
1862  # Compute first edge
1863  edge = invScale(p, math.sqrt(n2))
1864  verts.append(v)
1865  edges.append(edge)
1866  anchor = v
1867  else:
1868  d = dot(v, edge)
1869  if d > SIN_MIN:
1870  # v is inside the edge defined by the last
1871  # 2 vertices on the hull
1872  edge = invScale(p, math.sqrt(n2))
1873  verts.append(v)
1874  edges.append(edge)
1875  anchor = v
1876  elif d < -SIN_MIN:
1877  # backtrack - the most recently added hull vertex
1878  # is not actually on the hull.
1879  verts.pop()
1880  edges.pop()
1881  anchor = verts[-1]
1882  edge = edges[-1]
1883  # reprocess v to decide whether to add it to the hull
1884  # or whether further backtracking is necessary
1885  continue
1886  # v is coplanar with edge, skip it
1887  i += 1
1888  # Handle backtracking necessary for last edge
1889  if len(verts) < 3:
1890  return None
1891  v = verts[0]
1892  while True:
1893  p = cross(anchor, v)
1894  n2 = dot(p, p)
1895  if dot(anchor, v) < COS_MAX and n2 >= CROSS_N2MIN:
1896  if dot(v, edge) > SIN_MIN:
1897  edges[0] = invScale(p, math.sqrt(n2))
1898  break
1899  verts.pop()
1900  edges.pop()
1901  anchor = verts[-1]
1902  edge = edges[-1]
1903  if len(verts) < 3:
1904  return None
1905  return SphericalConvexPolygon(verts, edges)
1906 
1907 
1908 # -- Generic unit sphere partitioning scheme base class ----
1909 
1910 # TODO: make this an ABC once python 2.6 becomes the default
1911 class PartitionMap(object):
1912  """Base class for partitioning schemes.
1913  """
1914  pass
1915 
1916 
1917 # -- Utilities for the spherical box partitioning scheme ----
1918 
1919 class _SubList(object):
1920  """Class that maintains a sub-list of a backing list in
1921  insertion order. Elements can be deleted in O(1) time.
1922  """
1923  def __init__(self, backingList):
1924  self.backingList = backingList
1925  self.active = []
1926  self.head = -1
1927  self.tail = -1
1928  self.freeTail = -1
1929  self.len = 0
1930 
1931  def append(self, i):
1932  if self.freeTail == -1:
1933  j = len(self.active)
1934  self.active.append([i, self.tail, -1])
1935  else:
1936  j = self.freeTail
1937  self.freeTail = self.active[self.freeTail][2]
1938  self.active[j] = [i, self.tail, -1]
1939  if self.tail != -1:
1940  self.active[self.tail][2] = j
1941  if self.head == -1:
1942  self.head = j
1943  self.tail = j
1944  self.len += 1
1945 
1946  def filter(self, predicate):
1947  """Removes all elements E in the sub-list for which predicate(E)
1948  evaluates to True.
1949  """
1950  h = self.head
1951  while h != -1:
1952  t = self.active[h]
1953  if predicate(self.backingList[t[0]]):
1954  # Remove element h
1955  self.backingList[t[0]] = None
1956  self.len -= 1
1957  prev = t[1]
1958  next = t[2]
1959  if next != -1:
1960  self.active[next][1] = prev
1961  else:
1962  self.tail = prev
1963  if prev != -1:
1964  self.active[prev][2] = next
1965  else:
1966  self.head = next
1967  t[2] = self.freeTail
1968  self.freeTail = h
1969  h = next
1970  else:
1971  h = t[2]
1972 
1973  def __len__(self):
1974  return self.len
1975 
1976  def __iter__(self):
1977  """Returns an iterator over all elements in the sub-list.
1978  """
1979  h = self.head
1980  while h != -1:
1981  t = self.active[h]
1982  yield self.backingList[t[0]]
1983  h = t[2]
1984 
1985 
1986 # -- Spherical box partitioning scheme ----
1987 
1989  """A simple partitioning scheme that breaks the unit sphere into fixed
1990  height latitude angle stripes. These are in turn broken up into fixed
1991  width longitude angle chunks (each stripe has a variable number of chunks
1992  to account for distortion at the poles). Chunks are in turn broken up
1993  into fixed height sub-stripes, and each sub-stripe is then divided into
1994  fixed width sub-chunks. Again, the number of sub-chunks per sub-stripe is
1995  variable to account for polar distortion.
1996  """
1997  def __init__(self, numStripes, numSubStripesPerStripe):
1998  if (not isinstance(numStripes, (int, long)) or
1999  not isinstance(numSubStripesPerStripe, (int, long))):
2000  raise TypeError('Number of stripes and sub-stripes per stripe ' +
2001  'must be integers')
2002  if numStripes < 1 or numSubStripesPerStripe < 1:
2003  raise RuntimeError('Number of stripes and sub-stripes per ' +
2004  'stripe must be positive')
2005  self.numStripes = numStripes
2006  self.numSSPerStripe = numSubStripesPerStripe
2007  self.numSubStripes = numStripes * numSubStripesPerStripe
2008  h = 180.0 / numStripes
2009  hs = 180.0 / self.numSubStripes
2010  self.stripeHeight = h
2012  self.numChunks = [segments(i * h - 90.0, (i + 1) * h - 90.0, h)
2013  for i in xrange(numStripes)]
2014  self.numSCPerChunk = []
2015  self.subChunkWidth = []
2016  for i in xrange(self.numSubStripes):
2017  nc = self.numChunks[i / numSubStripesPerStripe]
2018  n = segments(i * hs - 90.0, (i + 1) * hs - 90.0, hs) / nc
2019  self.numSCPerChunk.append(n)
2020  w = 360.0 / (n * nc)
2021  self.subChunkWidth.append(w)
2022  self.maxSCPerChunk = max(self.numSCPerChunk)
2023 
2024  def getSubStripe(self, phi):
2025  """Returns the sub-stripe number of the sub-stripe containing points
2026  with the given latitude angle.
2027  """
2028  assert phi >= -90.0 and phi <= 90.0
2029  ss = int(math.floor((phi + 90.0) / self.subStripeHeight))
2030  if ss >= self.numSubStripes:
2031  ss = self.numSubStripes - 1
2032  return ss
2033 
2034  def getStripe(self, phi):
2035  """Returns the stripe number of the stripe containing all points
2036  with the given latitude angle.
2037  """
2038  ss = self.getSubStripe(phi)
2039  return ss / self.numSSPerStripe
2040 
2041  def getSubChunk(self, subStripe, theta):
2042  """Returns the sub-chunk number of the sub-chunk containing all points
2043  in the given sub-stripe with the given longitude angle.
2044  """
2045  assert subStripe >= 0 and theta >= 0.0 and theta <= 360.0
2046  sc = int(math.floor(theta / self.subChunkWidth[subStripe]))
2047  nsc = (self.numSCPerChunk[subStripe] *
2048  self.numChunks[subStripe / self.numSSPerStripe])
2049  if sc >= nsc:
2050  sc = nsc - 1
2051  return sc
2052 
2053  def getChunk(self, stripe, theta):
2054  """Returns the chunk number of the chunk containing all points
2055  in the given stripe with the given longitude angle.
2056  """
2057  assert stripe >= 0 and theta >= 0.0 and theta <= 360.0
2058  ss = stripe * self.numSSPerStripe
2059  sc = self.getSubChunk(ss, theta)
2060  return sc / self.numSCPerChunk[ss]
2061 
2062  def _getChunkBoundingBox(self, stripe, chunk):
2063  assert stripe >= 0 and stripe < self.numStripes
2064  assert chunk >= 0 and chunk < self.numChunks[stripe]
2065  ss = stripe * self.numSSPerStripe
2066  minPhi = ss * self.subStripeHeight - 90.0
2067  maxPhi = (ss + self.numSSPerStripe) * self.subStripeHeight - 90.0
2068  # Why all the epsilons? Because chunk bounding boxes are defined
2069  # as the union of all sub-chunk bounding boxes: minTheta and maxTheta
2070  # are not guaranteed to be identical across all the sub-stripes in
2071  # the chunk. Furthermore, the computation of the boundaries
2072  # themselves is inexact - points very slightly outside of them
2073  # could still be assigned to the corresponding chunk.
2074  if maxPhi >= 90.0 - ANGLE_EPSILON:
2075  maxPhi = 90.0
2076  else:
2077  maxPhi += ANGLE_EPSILON
2078  if minPhi > -90.0 + ANGLE_EPSILON:
2079  minPhi -= ANGLE_EPSILON
2080  nscpc = self.numSCPerChunk[ss]
2081  sc = chunk * nscpc
2082  scw = self.subChunkWidth[ss]
2083  minTheta = max(0.0, sc * scw - ANGLE_EPSILON)
2084  maxTheta = min((sc + nscpc) * scw + ANGLE_EPSILON, 360.0)
2085  # avoid range reduction in SphericalBox constructor
2086  box = SphericalBox()
2087  box.min = (minTheta, minPhi)
2088  box.max = (maxTheta, maxPhi)
2089  return box
2090 
2091  def getChunkBoundingBox(self, chunkId):
2092  """Returns a SphericalBox bounding the given chunk. Note that
2093  this is a bounding box only - not an exact representation! In
2094  particular, for a point p and a chunk C with bounding box B,
2095  B.contains(p) == True does not imply that p is assigned to C.
2096  However, for all points p assigned to C, B.contains(p) is True.
2097  """
2098  s = chunkId / (self.numStripes * 2)
2099  c = chunkId - s * self.numStripes * 2
2100  return self._getChunkBoundingBox(s, c)
2101 
2102  def _getSubChunkBoundingBox(self, subStripe, subChunk):
2103  assert subStripe >= 0
2104  nsc = (self.numSCPerChunk[subStripe] *
2105  self.numChunks[subStripe / self.numSSPerStripe])
2106  assert subChunk >= 0 and subChunk < nsc
2107  scw = self.subChunkWidth[subStripe]
2108  minPhi = subStripe * self.subStripeHeight - 90.0
2109  maxPhi = (subStripe + 1) * self.subStripeHeight - 90.0
2110  # Why all the epsilons? Because the computation of the boundaries
2111  # themselves is inexact - points very slightly outside of them
2112  # could still be assigned to the corresponding chunk.
2113  if maxPhi >= 90.0 - ANGLE_EPSILON:
2114  maxPhi = 90.0
2115  else:
2116  maxPhi += ANGLE_EPSILON
2117  if minPhi > -90.0 + ANGLE_EPSILON:
2118  minPhi -= ANGLE_EPSILON
2119  minTheta = max(0.0, subChunk * scw - ANGLE_EPSILON)
2120  maxTheta = min((subChunk + 1) * scw + ANGLE_EPSILON, 360.0)
2121  # avoid range reduction in SphericalBox constructor
2122  box = SphericalBox()
2123  box.min = (minTheta, minPhi)
2124  box.max = (maxTheta, maxPhi)
2125  return box
2126 
2127  def getSubChunkBoundingBox(self, chunkId, subChunkId):
2128  """Returns a SphericalBox bounding the given sub-chunk. Note that
2129  this is a bounding box only - not an exact representation! In
2130  particular, for a point p and a sub-chunk SC with bounding box B,
2131  B.contains(p) == True does not imply that p is assigned to SC.
2132  However, for all points p assigned to SC, B.contains(p) is True.
2133  """
2134  s = chunkId / (self.numStripes * 2)
2135  c = chunkId - s * self.numStripes * 2
2136  ssc = subChunkId / self.maxSCPerChunk
2137  scc = subChunkId - ssc * self.maxSCPerChunk
2138  ss = s * self.numSSPerStripe + ssc
2139  sc = c * self.numSCPerChunk[ss] + scc
2140  return self._getSubChunkBoundingBox(ss, sc)
2141 
2142  def getChunkId(self, stripe, chunk):
2143  """Returns the chunk ID of the chunk with the given
2144  stripe/chunk numbers.
2145  """
2146  return stripe * self.numStripes * 2 + chunk
2147 
2148  def getSubChunkId(self, subStripe, subChunk):
2149  """Returns the sub-chunk ID of the sub-chunk with the given
2150  sub-stripe/sub-chunk numbers.
2151  """
2152  ss = (subStripe % self.numSSPerStripe) * self.maxSCPerChunk
2153  sc = (subChunk % self.numSCPerChunk[subStripe])
2154  return ss + sc
2155 
2156  def _allSubChunks(self, stripe, withEmptySet):
2157  assert stripe >= 0 and stripe < self.numStripes
2158  emptySet = set()
2159  for i in xrange(self.numSSPerStripe):
2160  nsc = self.numSCPerChunk[stripe * self.numSSPerStripe + i]
2161  scId = i * self.maxSCPerChunk
2162  if withEmptySet:
2163  for j in xrange(nsc): yield (scId + j, emptySet)
2164  else:
2165  for j in xrange(nsc): yield scId + j
2166 
2167  def _processChunk(self, minS, minC, c, cOverlap):
2168  ss = minS * self.numSSPerStripe
2169  while minC < c and len(cOverlap) > 0:
2170  bbox = self._getChunkBoundingBox(minS, minC)
2171  if any(br[1].contains(bbox) for br in cOverlap):
2172  # if a constraint contains the whole chunk,
2173  # yield a fast sub-chunk iterator
2174  yield (self.getChunkId(minS, minC),
2175  self._allSubChunks(minS, True))
2176  else:
2177  yield (self.getChunkId(minS, minC),
2178  self._subIntersect(minS, minC, list(cOverlap)))
2179  # Finished processing minC - advance and remove any regions
2180  # no longer intersecting the current chunk
2181  minC += 1
2182  theta = (minC * self.numSCPerChunk[ss]) * self.subChunkWidth[ss]
2183  if theta >= 360.0 - ANGLE_EPSILON:
2184  theta = 360.0 + ANGLE_EPSILON
2185  else:
2186  theta -= ANGLE_EPSILON
2187  cOverlap.filter(lambda br: br[0].getMax()[0] < theta)
2188 
2189  def _processStripe(self, minS, s, sOverlap):
2190  while minS < s and len(sOverlap) > 0:
2191  # Sort stripe regions by minimum bounding box longitude angle
2192  sRegions = list(sOverlap)
2193  sRegions.sort(key=lambda x: x[0].getMin()[0])
2194  minC = self.getChunk(minS, max(0.0,
2195  sRegions[0][0].getMin()[0] - ANGLE_EPSILON))
2196  cOverlap = _SubList(sRegions)
2197  cOverlap.append(0)
2198  for j in xrange(1, len(sRegions)):
2199  c = self.getChunk(minS, max(0.0,
2200  sRegions[j][0].getMin()[0] - ANGLE_EPSILON))
2201  if c == minC:
2202  cOverlap.append(j)
2203  continue
2204  # All regions overlapping minC have been accumulated
2205  for x in self._processChunk(minS, minC, c, cOverlap):
2206  yield x
2207  minC = c
2208  cOverlap.append(j)
2209  maxC = self.numChunks[minS]
2210  for x in self._processChunk(minS, minC, maxC, cOverlap):
2211  yield x
2212  # Finished processing minS - remove any regions not
2213  # intersecting stripes greater than minS and advance
2214  minS += 1
2215  phi = (minS * self.numSSPerStripe) * self.subStripeHeight - 90.0
2216  if phi >= 90.0 - ANGLE_EPSILON:
2217  phi = 90.0 + ANGLE_EPSILON
2218  else:
2219  phi -= ANGLE_EPSILON
2220  sOverlap.filter(lambda br: br[0].getMax()[1] < phi)
2221 
2222  def intersect(self, *args):
2223  """Computes the intersection of a spherical box partitioning of the
2224  unit sphere and one or more SphericalRegions. Results are
2225  returned as an iterator over (chunkId, SubIterator) tuples. These
2226  correspond to all chunks overlapping at least one input region,
2227  and contain sub-iterators over all sub-chunks intersecting at least
2228  one input region. The sub-iterators return (subChunkId, Regions)
2229  tuples, where Regions is a set of the regions that intersect with
2230  (but do not completely contain) a particular sub-chunk. Note that
2231  Regions can be an empty set - this means that the sub-chunk
2232  is completely contained in at least one of the input regions.
2233  """
2234  if len(args) == 0:
2235  return
2236  elif len(args) == 1 and not isinstance(args[0], SphericalRegion):
2237  # accept arbitrary sequences of SphericalRegion objects
2238  args = args[0]
2239  if not all(isinstance(r, SphericalRegion) for r in args):
2240  raise TypeError(
2241  'Input must consist of one or more SphericalRegion objects')
2242  # Construct list of (bounding box, region) tuples
2243  regions = []
2244  for r in args:
2245  b = r.getBoundingBox()
2246  if b.wraps():
2247  # Split boxes that wrap
2248  bMin = (0.0, b.getMin()[1])
2249  bMax = (360.0, b.getMax()[1])
2250  regions.append((SphericalBox(bMin, b.getMax()), r))
2251  # Cannot use SphericalBox constructor: 360.0 would get
2252  # range reduced to 0!
2253  b2 = SphericalBox()
2254  b2.min = b.getMin()
2255  b2.max = bMax
2256  regions.append((b2, r))
2257  else:
2258  regions.append((b, r))
2259  # Sort regions by minimum bounding box latitude angle
2260  regions.sort(key=lambda x: x[0].getMin()[1])
2261  minS = self.getStripe(
2262  max(-90.0, regions[0][0].getMin()[1] - ANGLE_EPSILON))
2263  sOverlap = _SubList(regions)
2264  sOverlap.append(0)
2265  # Loop over regions
2266  for i in xrange(1, len(regions)):
2267  s = self.getStripe(
2268  max(-90.0, regions[i][0].getMin()[1] - ANGLE_EPSILON))
2269  if s == minS:
2270  sOverlap.append(i)
2271  continue
2272  # All regions overlapping minS have been accumulated
2273  for x in self._processStripe(minS, s, sOverlap):
2274  yield x
2275  minS = s
2276  sOverlap.append(i)
2277  for x in self._processStripe(minS, self.numStripes, sOverlap):
2278  yield x
2279 
2280  def _processSubChunk(self, minSS, minSC, sc, scOverlap):
2281  while minSC < sc and len(scOverlap) > 0:
2282  partial = None
2283  bbox = self._getSubChunkBoundingBox(minSS, minSC)
2284  for br in scOverlap:
2285  if br[1].contains(bbox):
2286  partial = set()
2287  break
2288  elif br[1].intersects(bbox):
2289  if partial == None:
2290  partial = set()
2291  partial.add(br[1])
2292  if partial != None:
2293  yield (self.getSubChunkId(minSS, minSC), partial)
2294  # Finished processing minSC - remove any regions not
2295  # intersecting sub-chunks > minSC and advance
2296  minSC += 1
2297  theta = minSC * self.subChunkWidth[minSS]
2298  if theta >= 360.0 - ANGLE_EPSILON:
2299  theta = 360.0 + ANGLE_EPSILON
2300  else:
2301  theta -= ANGLE_EPSILON
2302  scOverlap.filter(lambda br: br[0].getMax()[0] < theta)
2303 
2304  def _processSubStripe(self, minSS, ss, chunk, ssOverlap):
2305  while minSS < ss and len(ssOverlap) > 0:
2306  firstSC = chunk * self.numSCPerChunk[minSS]
2307  ssRegions = list(ssOverlap)
2308  # Sort sub-stripe regions by minimum bounding box longitude
2309  ssRegions.sort(key=lambda x: x[0].getMin()[0])
2310  minSC = max(firstSC, self.getSubChunk(minSS, max(0.0,
2311  ssRegions[0][0].getMin()[0] - ANGLE_EPSILON)))
2312  scOverlap = _SubList(ssRegions)
2313  scOverlap.append(0)
2314  for j in xrange(1, len(ssRegions)):
2315  sc = max(firstSC, self.getSubChunk(minSS, max(0.0,
2316  ssRegions[j][0].getMin()[0] - ANGLE_EPSILON)))
2317  if sc == minSC:
2318  scOverlap.append(j)
2319  continue
2320  # All regions overlapping minSC have been accumulated
2321  for x in self._processSubChunk(minSS, minSC, sc, scOverlap):
2322  yield x
2323  minSC = sc
2324  scOverlap.append(j)
2325  maxSC = firstSC + self.numSCPerChunk[minSS]
2326  for x in self._processSubChunk(minSS, minSC, maxSC, scOverlap):
2327  yield x
2328  # Finished processing minSS - remove any regions not
2329  # intersecting stripes greater than minSS and advance
2330  minSS += 1
2331  phi = minSS * self.subStripeHeight - 90.0
2332  if phi >= 90.0 - ANGLE_EPSILON:
2333  phi = 90.0 + ANGLE_EPSILON
2334  else:
2335  phi -= ANGLE_EPSILON
2336  ssOverlap.filter(lambda br: br[0].getMax()[1] < phi)
2337 
2338  def _subIntersect(self, stripe, chunk, regions):
2339  """Returns an iterator over (subChunkId, Regions) tuples, where
2340  Regions is a set of all regions that intersect with (but do not
2341  completely contain) a particular sub-chunk.
2342  """
2343  # Sort regions by minimum bounding box latitude angle
2344  regions.sort(key=lambda x: x[0].getMin()[1])
2345  firstSS = stripe * self.numSSPerStripe
2346  minSS = max(firstSS, self.getSubStripe(max(-90.0,
2347  regions[0][0].getMin()[1] - ANGLE_EPSILON)))
2348  ssOverlap = _SubList(regions)
2349  ssOverlap.append(0)
2350  # Loop over regions
2351  for i in xrange(1, len(regions)):
2352  ss = max(firstSS, self.getSubStripe(max(-90.0,
2353  regions[i][0].getMin()[1] - ANGLE_EPSILON)))
2354  if ss == minSS:
2355  ssOverlap.append(i)
2356  continue
2357  for x in self._processSubStripe(minSS, ss, chunk, ssOverlap):
2358  yield x
2359  minSS = ss
2360  ssOverlap.append(i)
2361  maxSS = firstSS + self.numSSPerStripe
2362  for x in self._processSubStripe(minSS, maxSS, chunk, ssOverlap):
2363  yield x
2364 
2365  def __iter__(self):
2366  """Returns an iterator over (chunkId, SubIterator) tuples - one for
2367  each chunk in the partition map. Each SubIterator is an iterator over
2368  subChunkIds for the corresponding chunk.
2369  """
2370  for s in xrange(self.numStripes):
2371  for c in xrange(self.numChunks[s]):
2372  yield (self.getChunkId(s, c), self._allSubChunks(s, False))
2373 
2374  def __eq__(self, other):
2375  if isinstance(other, SphericalBoxPartitionMap):
2376  return (self.numStripes == other.numStripes and
2377  self.numSSPerStripe == other.numSSPerStripe)
2378  return False
2379 
2380  def __repr__(self):
2381  return ''.join([self.__class__.__name__ , '(', repr(self.numStripes),
2382  ', ', repr(self.numSSPerStripe), ')'])
2383 
bool all(CoordinateExpr< N > const &expr)
Return true if all elements are true.
bool any(CoordinateExpr< N > const &expr)
Return true if any elements are true.