28 ARCSEC_PER_DEG = 3600.0
29 DEG_PER_ARCSEC = 1.0 / 3600.0
32 ANGLE_EPSILON = 0.001 * DEG_PER_ARCSEC
35 POLE_EPSILON = 1.0 * DEG_PER_ARCSEC
39 COS_MAX = 1.0 - 1.0e-15
47 SIN_MIN = math.sqrt(CROSS_N2MIN)
51 NEG_INF = float(
'-inf')
54 from sys
import float_info
55 MAX_FLOAT = float_info.max
56 MIN_FLOAT = float_info.min
57 EPSILON = float_info.epsilon
60 MAX_FLOAT = 1.7976931348623157e+308
61 MIN_FLOAT = 2.2250738585072014e-308
62 EPSILON = 2.2204460492503131e-16
64 if hasattr(math,
'isinf'):
69 return x == INF
or x == NEG_INF
74 """Returns the dot product of cartesian 3-vectors v1 and v2.
76 return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]
79 """Returns the cross product of cartesian 3-vectors v1 and v2.
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])
86 """Returns a copy of the cartesian 3-vector v scaled by 1 / s.
88 return (v[0] / s, v[1] / s, v[2] / s)
91 """Returns a normalized copy of the cartesian 3-vector v.
93 n = math.sqrt(
dot(v, v))
95 raise RuntimeError(
'Cannot normalize a 3-vector with 0 magnitude')
96 return (v[0] / n, v[1] / n, v[2] / n)
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.
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')
119 theta = math.degrees(math.atan2(y, x))
125 phi = math.degrees(math.atan2(z, math.sqrt(d2)))
127 raise TypeError(
'Expecting 2 or 3 coordinates, or a tuple/list ' +
128 'containing 2 or 3 coordinates')
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
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,
150 n = math.sqrt(x * x + y * y + z * z)
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')
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.
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)
169 return 2.0 * math.degrees(math.asin(s))
172 """Clamps the input latitude angle phi to [-90.0, 90.0] deg.
181 """Range reduces the given longitude angle to lie in the range
184 theta = math.fmod(theta, 360.0)
191 """Returns unit N,E basis vectors for a point v, which must be a
194 north = (-v[0] * v[2],
196 v[0] * v[0] + v[1] * v[1])
197 if north == (0.0, 0.0, 0.0):
199 north = (-1.0, 0.0, 0.0)
200 east = (0.0, 1.0, 0.0)
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.
211 if phi < centerPhi - r
or phi > centerPhi + r:
213 a = abs(centerPhi - phi)
214 if a <= r - (90.0 - phi)
or a <= r - (90.0 + phi):
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)))
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].
230 assert r >= 0.0
and r <= 90.0
234 if abs(centerPhi) + r > 90.0 - POLE_EPSILON:
237 c = math.radians(centerPhi)
239 x = math.sqrt(abs(math.cos(c - r) * math.cos(c + r)))
240 return math.degrees(abs(math.atan(y / x)))
243 """Returns the angular separation in degrees between points v1 and v2,
244 which must both be specified as cartesian 3-vectors.
248 ss = math.sqrt(
dot(n, n))
249 if cs == 0.0
and ss == 0.0:
251 return math.degrees(math.atan2(ss, cs))
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.
260 if dot(p1, p) >= 0.0
and dot(p2, p) <= 0.0:
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.
271 if minTheta > maxTheta:
272 if p[0] >= minTheta
or p[0] <= maxTheta:
273 return abs(p[1] - phi)
275 if p[0] >= minTheta
and p[0] <= maxTheta:
276 return abs(p[1] - phi)
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.
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.
298 x, y, z = 0.0, 0.0, 0.0
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.
313 return dot(p1, p) >= 0.0
and dot(p2, p) <= 0.0
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.
321 p = max(abs(phiMin), abs(phiMax))
322 if p > 90.0 - 1.0 * DEG_PER_ARCSEC:
326 elif width < 1.0 * DEG_PER_ARCSEC:
327 width = 1.0 * DEG_PER_ARCSEC
329 cw = math.cos(math.radians(width))
334 y = math.sqrt(abs(u * u - x * x))
335 return int(math.floor((2.0 * math.pi) / abs(math.atan2(y, x))))
342 """Base class for regions on the unit sphere.
347 class SphericalBox(SphericalRegion):
348 """A spherical coordinate space bounding box.
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.
356 Note that points falling exactly on spherical box edges are
357 considered to be inside (contained by) the box.
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
384 if isinstance(args[0], SphericalRegion):
386 self.
min = tuple(bbox.getMin())
387 self.
max = tuple(bbox.getMax())
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]:
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)):
402 'Longitude angle minimum is greater than maximum')
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])
412 """Returns True if this spherical box wraps across the 0/360
413 degree longitude angle discontinuity.
415 return self.
min[0] > self.
max[0]
418 """Returns a bounding box for this spherical region.
423 """Returns the minimum longitude and latitude angles of this
424 spherical box as a 2-tuple of floats (in units of degrees).
429 """Returns the maximum longitude and latitude angles of this
430 spherical box as a 2-tuple of floats (in units of degrees).
435 """Returns the extent in longitude angle of this box.
438 return 360.0 - self.
min[0] + self.
max[0]
440 return self.
max[0] - self.
min[0]
443 """Returns an 2-tuple of floats corresponding to the longitude/latitude
444 angles (in degrees) of the center of this spherical box.
446 centerTheta = 0.5 * (self.
min[0] + self.
max[0])
447 centerPhi = 0.5 * (self.
min[1] + self.
max[1])
449 if centerTheta >= 180.0:
453 return (centerTheta, centerPhi)
456 """Returns True if this spherical box contains no points.
458 return self.
min[1] > self.
max[1]
461 """Returns True if this spherical box contains every point
464 return self.
min == (0.0, -90.0)
and self.
max == (360.0, 90.0)
467 """Returns True if this spherical box contains the given point,
468 which must be specified in spherical coordinates.
470 if p[1] < self.
min[1]
or p[1] > self.
max[1]:
474 return theta >= self.
min[0]
or theta <= self.
max[0]
476 return theta >= self.
min[0]
and theta <= self.
max[0]
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.
486 if isinstance(pointOrRegion, SphericalRegion):
487 b = pointOrRegion.getBoundingBox()
490 if b.min[1] < self.
min[1]
or b.max[1] > self.
max[1]:
494 return b.min[0] >= self.
min[0]
and b.max[0] <= self.
max[0]
496 return b.min[0] >= self.
min[0]
or b.max[0] <= self.
max[0]
499 return self.
min[0] == 0.0
and self.
max[0] == 360.0
501 return b.min[0] >= self.
min[0]
and b.max[0] <= self.
max[0]
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
513 if isinstance(pointOrRegion, SphericalBox):
517 if b.min[1] > self.
max[1]
or b.max[1] < self.
min[1]:
523 return b.min[0] <= self.
max[0]
or b.max[0] >= self.
min[0]
526 return self.
min[0] <= b.max[0]
or self.
max[0] >= b.min[0]
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)
535 """Extends this box to the smallest spherical box S containing
536 the union of this box with the specified point or spherical region.
538 if self == pointOrRegion:
540 if isinstance(pointOrRegion, SphericalRegion):
541 b = pointOrRegion.getBoundingBox()
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]
553 minMinRa =
min(self.
min[0], b.min[0])
554 maxMaxRa =
max(self.
max[0], b.max[0])
555 if maxMaxRa >= minMinRa:
562 if b.min[0] <= self.
max[0]
and b.max[0] >= self.
min[0]:
565 elif b.min[0] - self.
max[0] > self.
min[0] - b.max[0]:
571 if self.
min[0] <= b.max[0]
and self.
max[0] >= b.min[0]:
574 elif self.
min[0] - b.max[0] > b.min[0] - self.
max[0]:
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]):
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]):
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)
602 self.
min = (theta, phi)
603 self.
max = (theta, phi)
605 minPhi =
min(self.
min[1], phi)
606 maxPhi =
max(self.
max[1], phi)
607 minTheta = self.
min[0]
608 maxTheta = self.
max[0]
610 if self.
min[0] - theta > theta - self.
max[0]:
614 elif theta < self.
min[0]:
615 if self.
min[0] - theta <= 360.0 - self.
max[0] + theta:
619 elif theta - self.
max[0] <= 360.0 - theta + self.
min[0]:
623 self.
min = (minTheta, minPhi)
624 self.
max = (maxTheta, maxPhi)
628 """Shrinks this box to the smallest spherical box containing
629 the intersection of this box and the specified one.
632 if not isinstance(b, SphericalBox):
633 raise TypeError(
'Expecting a SphericalBox object')
634 if self == b
or self.
isEmpty():
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]
644 minTheta =
max(minTheta, b.min[0])
645 maxTheta =
min(maxTheta, b.max[0])
647 if b.max[0] >= minTheta:
648 if b.min[0] <= maxTheta:
649 if b.max[0] - b.min[0] <= 360.0 - minTheta + maxTheta:
653 minTheta =
max(minTheta, b.min[0])
655 elif b.min[0] <= maxTheta:
657 maxTheta =
min(maxTheta, b.max[0])
663 if maxTheta >= b.min[0]:
664 if minTheta <= b.max[0]:
665 if maxTheta - minTheta > 360.0 - b.min[0] + b.max[0]:
669 minTheta =
max(minTheta, b.min[0])
670 elif minTheta <= b.max[0]:
675 elif minTheta > b.max[0]
or maxTheta < b.min[0]:
679 minTheta =
max(minTheta, b.min[0])
680 maxTheta =
min(maxTheta, b.max[0])
681 self.
min = (minTheta, minPhi)
682 self.
max = (maxTheta, maxPhi)
686 """Empties this spherical box.
688 self.
min = (0.0, 90.0)
689 self.
max = (0.0, -90.0)
693 """Expands this spherical box to fill the unit sphere.
695 self.
min = (0.0, -90.0)
696 self.
max = (360.0, 90.0)
700 """Returns a string representation of this spherical box.
703 return ''.join([self.__class__.__name__,
'(',
')'])
704 return ''.join([self.__class__.__name__,
'(',
705 repr(self.
min),
', ', repr(self.
max),
')'])
708 if isinstance(other, SphericalBox):
709 if self.
isEmpty()
and other.isEmpty():
711 return self.
min == other.min
and self.
max == other.max
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.
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:
728 if abs(n[2]) <= SIN_MIN:
729 ex = (0.0, 0.0, -1.0)
731 ex = (n[0] * n[2] / d, n[1] * n[2] / d, -d)
735 ex = (-ex[0], -ex[1], -ex[2])
739 if abs(n[2]) <= SIN_MIN:
741 d =
min(abs(theta1 - theta2), abs(360.0 - theta1 + theta2))
742 if d >= 90.0
and d <= 270.0:
748 minTheta =
min(theta1, theta2)
749 maxTheta =
max(theta1, theta2)
750 if maxTheta - minTheta > 180.0:
763 return SphericalBox((minTheta, minPhi), (maxTheta, maxPhi))
767 """A circle on the unit sphere. Points falling exactly on the
768 circle are considered to be inside (contained by) the circle.
771 """Creates a new spherical circle with the given center and radius.
778 'Circle radius is negative or greater than 180 deg')
782 """Returns a bounding box for this spherical circle.
789 self.boundingBox.setFull()
794 if alpha > 180.0 - ANGLE_EPSILON:
798 minTheta = self.
center[0] - alpha
799 maxTheta = self.
center[0] + alpha
808 """Returns an (ra, dec) 2-tuple of floats corresponding to the
809 center of this circle.
814 """Returns the radius (degrees) of this circle.
819 """Returns True if this circle contains no points.
824 """Returns True if this spherical box contains every point
827 return self.
radius >= 180.0
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.
840 if isinstance(pr, SphericalBox):
850 a =
alpha(r, c[1], minp[1])
852 if (pr.containsPoint((c[0] + a, minp[1]))
or
853 pr.containsPoint((c[0] - a, minp[1]))):
855 a =
alpha(r, c[1], maxp[1])
857 if (pr.containsPoint((c[0] + a, maxp[1]))
or
858 pr.containsPoint((c[0] - a, maxp[1]))):
861 elif isinstance(pr, SphericalCircle):
865 elif isinstance(pr, SphericalEllipse):
866 bc = pr.getBoundingCircle()
868 elif isinstance(pr, SphericalConvexPolygon):
870 for v
in pr.getVertices():
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.
888 if isinstance(pr, SphericalBox):
891 elif pr.containsPoint(c):
901 elif isinstance(pr, SphericalCircle):
905 elif isinstance(pr, SphericalEllipse):
906 bc = pr.getBoundingCircle()
908 elif isinstance(pr, SphericalConvexPolygon):
909 return pr.intersects(self)
914 """Returns a string representation of this circle.
916 return ''.join([self.__class__.__name__ ,
'(', repr(self.
center),
917 ', ', repr(self.
radius),
')'])
920 if isinstance(other, SphericalCircle):
921 if self.
isEmpty()
and other.isEmpty():
923 if self.
radius == other.radius:
924 if self.
center[1] == other.center[1]:
925 if abs(self.
center[1]) == 90.0:
927 return self.
center[0] == other.center[0]
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
938 semiMajorAxisLength, semiMinorAxisLength, majorAxisAngle):
942 a = math.fmod(float(majorAxisAngle), 180.0)
949 raise RuntimeError(
'Negative semi-minor axis length')
952 'Semi-major axis length is less than semi-minor axis length')
956 'Semi-major axis length must be less than or equal to 10 deg')
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.
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.
972 r = math.degrees(math.asin(math.radians(
978 """Returns the circle of maximum radius having the same center as
979 this ellipse and which is completely contained in the ellipse.
982 r = math.degrees(math.asin(math.radians(
988 """Returns an (ra, dec) 2-tuple of floats corresponding to the center
994 """Return the major axis angle (east of north, in degrees) for this
1000 """Returns the semi-major axis length of this ellipse. Units
1001 are in arcsec since ellipses are typically small.
1006 """Returns the semi-minor axis length of this ellipse. Units
1007 are in arcsec since ellipses are typically small.
1012 theta = math.radians(self.
center[0])
1013 phi = math.radians(self.
center[1])
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)
1022 n = cosPhi * v[2] - sinPhi * (sinTheta * v[1] + cosTheta * v[0])
1023 e = cosTheta * v[1] - sinTheta * v[0]
1025 x = sinAng * e + cosAng * n
1026 y = cosAng * e - sinAng * n
1031 return x * x + y * y <= 1.0
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.
1039 if isinstance(pointOrRegion, (tuple, list)):
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
1051 if isinstance(pointOrRegion, (tuple, list)):
1058 """Returns a string representation of this ellipse.
1061 self.__class__.__name__ ,
'(', repr(self.
center),
', ',
1067 if isinstance(other, SphericalEllipse):
1068 return (self.
center == other.center
and
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
1081 """Creates a new polygon. Arguments must be either:
1083 1. a SphericalConvexPolygon
1084 2. a list of vertices
1085 3. a list of vertices and a list of corresponding edges
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.
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.
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).
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.
1112 raise RuntimeError(
'Expecting at least one argument')
1113 elif len(args) == 1:
1114 if isinstance(args[0], SphericalConvexPolygon):
1120 elif len(args) == 2:
1122 self.
edges = list(args[1])
1125 'number of edges does not match number of vertices')
1131 'spherical polygon must contain at least 3 vertices')
1134 """Computes edge plane normals from vertices.
1144 """Returns the list of polygon vertices.
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.
1155 """Returns a bounding circle (not necessarily minimal) for this
1156 spherical convex polygon.
1167 """Returns a bounding box for this spherical convex polygon.
1171 for i
in xrange(0, len(self.
vertices)):
1172 self.boundingBox.extend(SphericalBox.edge(
1177 """Returns the z coordinate range spanned by this spherical
1181 return (math.sin(math.radians(bbox.getMin()[1])),
1182 math.sin(math.radians(bbox.getMax()[1])))
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.
1190 Clipping is performed using the Sutherland-Hodgman algorithm,
1191 adapted for spherical polygons.
1193 vertices, edges, classification = [], [], []
1194 vin, vout =
False,
False
1195 for i
in xrange(len(self.
vertices)):
1197 if d > SIN_MIN: vin =
True
1198 elif d < -SIN_MIN: vout =
True
1200 classification.append(d)
1201 if not vin
and not vout:
1209 d0 = classification[-1]
1210 for i
in xrange(len(self.
vertices)):
1212 d1 = classification[i]
1218 edges.append(self.
edges[i])
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))
1226 edges.append(self.
edges[i])
1232 edges.append(self.
edges[i])
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))
1249 edges.append(tuple(plane))
1251 edges.append(self.
edges[i])
1256 edges.append(tuple(plane))
1263 """Returns the intersection of poly with this polygon, or
1264 None if the intersection does not exist.
1266 if not isinstance(poly, SphericalConvexPolygon):
1267 raise TypeError(
'Expecting a SphericalConvexPolygon object')
1269 for edge
in poly.getEdges():
1276 """Returns the area of this spherical convex polygon.
1280 for i
in xrange(numVerts):
1282 sina = math.sqrt(
dot(tmp, tmp))
1284 a = math.atan2(sina, cosa)
1286 return angleSum - (numVerts - 2) * math.pi
1289 for e
in self.
edges:
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.
1301 if isinstance(pr, SphericalConvexPolygon):
1302 for v
in pr.getVertices():
1306 elif isinstance(pr, SphericalEllipse):
1307 return self.
contains(pr.getBoundingCircle())
1308 elif isinstance(pr, SphericalCircle):
1314 for i
in xrange(len(self.
vertices)):
1317 minSep = min(minSep, s)
1318 return minSep >= pr.getRadius()
1319 elif isinstance(pr, SphericalBox):
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])))
1331 for i
in xrange(len(self.
vertices)):
1335 d = e[0] * e[0] + e[1] * e[1]
1336 if abs(e[2]) >= COS_MAX
or d < MIN_FLOAT:
1341 D = d - bzMin * bzMin
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)
1352 D = d - bzMax * bzMax
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)
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.
1374 if isinstance(pr, SphericalConvexPolygon):
1376 elif isinstance(pr, SphericalEllipse):
1377 return self.
intersects(pr.getBoundingCircle())
1378 elif isinstance(pr, SphericalCircle):
1384 for i
in xrange(len(self.
vertices)):
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:
1397 zMin, zMax = p.getZRange()
1398 if zMin <= bzMax
and zMax >= bzMin:
1400 p = self.
clip((math.sin(maxTheta), -math.cos(maxTheta), 0.0))
1403 p = p.clip((math.sin(maxTheta), -math.cos(maxTheta), 0.0))
1406 zMin, zMax = p.getZRange()
1407 return zMin <= bzMax
and zMax >= bzMin
1412 """Returns a string representation of this polygon.
1414 return ''.join([self.__class__.__name__ ,
'(',
1415 repr(map(sphericalCoords, self.
vertices)),
')'])
1418 if not isinstance(other, SphericalConvexPolygon):
1421 if n != len(other.vertices):
1425 offset = other.vertices.index(self.
vertices[0])
1428 for i
in xrange(0, n):
1444 """Partitions an array around the pivot value at index i,
1445 returning the new index of the pivot value.
1448 array[i] = array[right - 1]
1450 for k
in xrange(left, right - 1):
1451 if array[k] < pivot:
1456 array[right - 1] = array[j]
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
1470 for j
in xrange(i, e1):
1473 for s
in xrange(j + 1, e2):
1474 if array[s] < minValue:
1478 array[j] = array[minIndex]
1479 array[minIndex] = tmp
1483 """Returns the "median of medians" for an array. This primitive is used
1484 for pivot selection in the median finding algorithm.
1487 if right - left <= 5:
1491 while i + 4 < right:
1501 """Finds the median element of the given array in linear time.
1529 """Removes redundant constraints and reports intersection points
1530 of non-redundant pairs. The constraint list is modified in place.
1534 while i < len(constraints) - 1:
1535 a1, b1 = constraints[i]
1536 a2, b2 = constraints[i + 1]
1540 if abs(da) < MIN_FLOAT / EPSILON:
1544 xierr = 2.0 * EPSILON * abs(xi)
1547 constraints[i + 1] = constraints[-1]
1549 constraints[i] = constraints[-1]
1552 if xi + xierr <= xmin:
1554 constraints[i + 1] = constraints[-1]
1556 constraints[i] = constraints[-1]
1558 elif xi - xierr >= xmax:
1560 constraints[i] = constraints[-1]
1562 constraints[i + 1] = constraints[-1]
1566 intersections.append((xi, xierr))
1568 return intersections
1573 verr = EPSILON * abs(p) + EPSILON * abs(v)
1578 verr = EPSILON * abs(p) + EPSILON * abs(v)
1579 vmin = min(vmin, v - verr)
1580 vmax = max(vmax, v + verr)
1583 def _gh(constraints, x, xerr, op):
1584 a, b = constraints[0]
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:
1599 return vmin, vmax, amin, amax
1609 if abs(v[1]) <= MIN_FLOAT:
1610 if abs(v[0]) <= MIN_FLOAT:
1616 xlim = - z * v[2] / v[0]
1618 xmin = max(xmin, xlim)
1620 xmax = min(xmax, xlim)
1626 coeffs = (v[0] / v[1], - z * v[2] / v[1])
1633 if len(I1) == 0
or len(I2) == 0:
1639 intersections =
_prune(I1, xmin, xmax, operator.gt)
1640 intersections.extend(
_prune(I2, xmin, xmax, operator.lt))
1641 if len(intersections) == 0:
1648 xi = (b2 - b1) / (a1 - a2)
1651 return (xi > xmin
or a1 < a2)
and (xi < xmax
or a1 > a2)
1652 elif numConstraints == len(I1) + len(I2):
1657 numConstraints = len(I1) + len(I2)
1658 x, xerr =
median(intersections)
1661 gmin, gmax, sg, Sg =
_gh(I1, x, xerr, operator.gt)
1662 hmin, hmax, sh, Sh =
_gh(I2, x, xerr, operator.lt)
1676 if abs(v[0]) <= MIN_FLOAT:
1681 xlim = - y * v[1] / v[0]
1683 xmin = max(xmin, xlim)
1685 xmax = min(xmax, xlim)
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.
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:
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
1742 (False, msg): The vertex list does not form a spherical convex polygon -
1743 msg is a string describing why the test failed.
1745 The algorithm completes in O(n) time, where n is the number of
1748 if len(vertices) < 3:
1749 return (
False,
'3 or more vertices must be specified')
1751 return (
False,
'vertices are not hemispherical')
1755 counterClockwise =
False
1756 p1 =
cross(center, vertices[-1])
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]
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))
1772 return (
False,
'vertices wind around centroid in both ' +
1773 'clockwise and counter-clockwise order')
1774 counterClockwise =
True
1776 if counterClockwise:
1777 return (
False,
'vertices wind around centroid in both ' +
1778 'clockwise and counter-clockwise order')
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 ' +
1788 p2 =
cross(center, end)
1790 if abs(n2) < CROSS_N2MIN:
1791 return (
False,
'centroid of vertices is too close to a vertex')
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')
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
1816 for i
in xrange(len(points)):
1821 anchor = points[extremum]
1822 refPlane =
cross(center, anchor)
1823 n2 =
dot(refPlane, refPlane)
1824 if n2 < CROSS_N2MIN:
1827 refPlane =
invScale(refPlane, math.sqrt(n2))
1829 av = [(0.0, anchor)]
1830 for i
in xrange(0, len(points)):
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:
1842 angle = math.atan2(sa,
dot(refPlane, plane))
1844 angle += 2.0 * math.pi
1845 av.append((angle, v))
1848 av.sort(key=
lambda t: t[0])
1858 p =
cross(anchor, v)
1860 if dot(anchor, v) < COS_MAX
and n2 >= CROSS_N2MIN:
1893 p =
cross(anchor, v)
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))
1912 """Base class for partitioning schemes.
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.
1934 self.active.append([i, self.
tail, -1])
1947 """Removes all elements E in the sub-list for which predicate(E)
1960 self.
active[next][1] = prev
1964 self.
active[prev][2] = next
1977 """Returns an iterator over all elements in the sub-list.
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.
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 ' +
2002 if numStripes < 1
or numSubStripesPerStripe < 1:
2003 raise RuntimeError(
'Number of stripes and sub-stripes per ' +
2004 'stripe must be positive')
2008 h = 180.0 / numStripes
2013 for i
in xrange(numStripes)]
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)
2025 """Returns the sub-stripe number of the sub-stripe containing points
2026 with the given latitude angle.
2028 assert phi >= -90.0
and phi <= 90.0
2035 """Returns the stripe number of the stripe containing all points
2036 with the given latitude angle.
2042 """Returns the sub-chunk number of the sub-chunk containing all points
2043 in the given sub-stripe with the given longitude angle.
2045 assert subStripe >= 0
and theta >= 0.0
and theta <= 360.0
2054 """Returns the chunk number of the chunk containing all points
2055 in the given stripe with the given longitude angle.
2057 assert stripe >= 0
and theta >= 0.0
and theta <= 360.0
2063 assert stripe >= 0
and stripe < self.
numStripes
2064 assert chunk >= 0
and chunk < self.
numChunks[stripe]
2074 if maxPhi >= 90.0 - ANGLE_EPSILON:
2077 maxPhi += ANGLE_EPSILON
2078 if minPhi > -90.0 + ANGLE_EPSILON:
2079 minPhi -= ANGLE_EPSILON
2083 minTheta = max(0.0, sc * scw - ANGLE_EPSILON)
2084 maxTheta = min((sc + nscpc) * scw + ANGLE_EPSILON, 360.0)
2087 box.min = (minTheta, minPhi)
2088 box.max = (maxTheta, maxPhi)
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.
2103 assert subStripe >= 0
2106 assert subChunk >= 0
and subChunk < nsc
2113 if maxPhi >= 90.0 - ANGLE_EPSILON:
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)
2123 box.min = (minTheta, minPhi)
2124 box.max = (maxTheta, maxPhi)
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.
2143 """Returns the chunk ID of the chunk with the given
2144 stripe/chunk numbers.
2149 """Returns the sub-chunk ID of the sub-chunk with the given
2150 sub-stripe/sub-chunk numbers.
2157 assert stripe >= 0
and stripe < self.
numStripes
2163 for j
in xrange(nsc):
yield (scId + j, emptySet)
2165 for j
in xrange(nsc):
yield scId + j
2169 while minC < c
and len(cOverlap) > 0:
2171 if any(br[1].contains(bbox)
for br
in cOverlap):
2183 if theta >= 360.0 - ANGLE_EPSILON:
2184 theta = 360.0 + ANGLE_EPSILON
2186 theta -= ANGLE_EPSILON
2187 cOverlap.filter(
lambda br: br[0].getMax()[0] < theta)
2190 while minS < s
and len(sOverlap) > 0:
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))
2198 for j
in xrange(1, len(sRegions)):
2200 sRegions[j][0].getMin()[0] - ANGLE_EPSILON))
2216 if phi >= 90.0 - ANGLE_EPSILON:
2217 phi = 90.0 + ANGLE_EPSILON
2219 phi -= ANGLE_EPSILON
2220 sOverlap.filter(
lambda br: br[0].getMax()[1] < phi)
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.
2236 elif len(args) == 1
and not isinstance(args[0], SphericalRegion):
2239 if not all(isinstance(r, SphericalRegion)
for r
in args):
2241 'Input must consist of one or more SphericalRegion objects')
2245 b = r.getBoundingBox()
2248 bMin = (0.0, b.getMin()[1])
2249 bMax = (360.0, b.getMax()[1])
2256 regions.append((b2, r))
2258 regions.append((b, r))
2260 regions.sort(key=
lambda x: x[0].getMin()[1])
2262 max(-90.0, regions[0][0].getMin()[1] - ANGLE_EPSILON))
2266 for i
in xrange(1, len(regions)):
2268 max(-90.0, regions[i][0].getMin()[1] - ANGLE_EPSILON))
2281 while minSC < sc
and len(scOverlap) > 0:
2284 for br
in scOverlap:
2285 if br[1].contains(bbox):
2288 elif br[1].intersects(bbox):
2298 if theta >= 360.0 - ANGLE_EPSILON:
2299 theta = 360.0 + ANGLE_EPSILON
2301 theta -= ANGLE_EPSILON
2302 scOverlap.filter(
lambda br: br[0].getMax()[0] < theta)
2305 while minSS < ss
and len(ssOverlap) > 0:
2307 ssRegions = list(ssOverlap)
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)))
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)))
2332 if phi >= 90.0 - ANGLE_EPSILON:
2333 phi = 90.0 + ANGLE_EPSILON
2335 phi -= ANGLE_EPSILON
2336 ssOverlap.filter(
lambda br: br[0].getMax()[1] < phi)
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.
2344 regions.sort(key=
lambda x: x[0].getMin()[1])
2347 regions[0][0].getMin()[1] - ANGLE_EPSILON)))
2351 for i
in xrange(1, len(regions)):
2353 regions[i][0].getMin()[1] - ANGLE_EPSILON)))
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.
2375 if isinstance(other, SphericalBoxPartitionMap):
2376 return (self.
numStripes == other.numStripes
and
2381 return ''.join([self.__class__.__name__ ,
'(', repr(self.
numStripes),
bool all(CoordinateExpr< N > const &expr)
Return true if all elements are true.
def getSemiMajorAxisLength
bool any(CoordinateExpr< N > const &expr)
Return true if any elements are true.
def getSemiMinorAxisLength
def _getSubChunkBoundingBox
def getSubChunkBoundingBox