1  """ 
  2  Computing bboxes for STC geometries. 
  3   
  4  A bbox coming out of this module is a 4-tuple of (ra0, de0, ra1, de1) in 
  5  ICRS degrees. 
  6   
  7  (You're right, this should be part of the dm classes; but it's enough 
  8  messy custom code that I found it nicer to break it out). 
  9  """ 
 10   
 11   
 12   
 13   
 14   
 15   
 16   
 17  import math 
 18  from math import sin, cos, tan, atan, acos 
 19   
 20  from gavo import utils 
 21  from gavo.stc import common 
 22  from gavo.stc import conform 
 23  from gavo.stc import stcsast 
 24   
 25  from gavo.utils import DEG 
 26   
 27   
 28   
 29   
 30   
 31   
 32 -def getHeading(long1, lat1, long2, lat2): 
  33          """returns the initial heading when going from one spherical position to 
 34          another along a great circle. 
 35   
 36          Everything is in rad, angle is counted north over east. 
 37          """ 
 38          if abs(long1-long2)<=1e-10: 
 39                   
 40                  if lat1<lat2: 
 41                          return 0 
 42                  else: 
 43                          return math.pi 
 44   
 45          if lat1<=-math.pi/2+1e-10: 
 46                   
 47                  return 0 
 48          elif lat1>=math.pi/2-1e-10:  
 49                   
 50                  return math.pi 
 51   
 52          zeta = utils.spherDist( 
 53                  utils.spherToCart(long1, lat1), 
 54                  utils.spherToCart(long2, lat2)) 
 55          cosPhi = (sin(lat2)-sin(lat1)*cos(zeta))/(sin(zeta)*cos(lat1)) 
 56          if abs(cosPhi-1)<1e-10: 
 57                  return 0 
 58          elif abs(cosPhi+1)<1e-10: 
 59                  return math.pi 
 60          elif sin(long2-long1)>0: 
 61                  return acos(cosPhi) 
 62          else: 
 63                  return common.TWO_PI-acos(cosPhi) 
  64   
 67          """A great circle segment on a sphere.  It is assumed that it's no larger 
 68          than pi. 
 69   
 70          Construction is long,lat, long, lat in rad (or use the fromDegrees 
 71          class method. 
 72   
 73          Very small (<1e-8 rad, say) segments are not supported.  You should 
 74          probably work in the tangential plane on that kind of scale. 
 75          """ 
 76 -        def __init__(self, long1, lat1, long2, lat2): 
  77                  self.long1, self.lat1 = long1, lat1 
 78                  self.long2, self.lat2 = long2, lat2 
 79                  self._normalize() 
 80                  if self.long1>self.long2: 
 81                          self.long1, self.long2 = self.long2, self.long1 
 82                          self.lat1, self.lat2 = self.lat2, self.lat1 
 83                   
 84                  vertical = abs(self.long1-self.long2)<1e-10 
 85                  if vertical and abs(self.lat1-self.lat2)<1e-10: 
 86                          raise ValueError("Null segment: start and end are identical") 
 87   
 88                  self.overPole = vertical or cos(lat1)*cos(lat2)<1e-10 
 89                  self.overStich = 2*math.pi<=self.long1+self.long2<3*math.pi 
  90   
 91          @classmethod 
 94   
 96                  return "<Great Circle through (%f %f), (%f %f)"%( 
 97                          self.long1/DEG, self.lat1/DEG, self.long2/DEG, self.lat2/DEG) 
  98   
107   
109                  """returns a 4-tuple bounding box for a great circle segment. 
110   
111                  This is restricted to GCs not crossing the stitch line; furthermore, it 
112                  still depends on self. 
113                  """ 
114                  if self.overPole: 
115                          latMin, latMax  = min(lat1, lat2), max(lat1, lat2) 
116                          if cos(lat1)*cos(lat2)<1e-10: 
117                                   
118                                  long1, long2 = 0, common.TWO_PI 
119   
120                  else: 
121                           
122                          theta1 = getHeading(long1, lat1, long2, lat2) 
123                          theta2 = getHeading(long2, lat2, long1, lat1) 
124   
125                           
126                           
127                          quad1, quad2 = int(2*theta1/math.pi), int(2*theta2/math.pi) 
128                          if abs(quad1-quad2)==2: 
129                                  latExt = lat1 
130                          else: 
131                                   
132                                  latExt = acos(abs(sin(theta1))*cos(lat1)) 
133                                   
134                                   
135                                  if lat1<0: 
136                                          latExt = -latExt 
137   
138                          latMin = min(latExt, lat1, lat2) 
139                          latMax = max(latExt, lat1, lat2) 
140                  return (long1, latMin, long2, latMax) 
 141   
143                  """returns the latitude the great circle is on for a given longitude. 
144   
145                  Input and output is in rad. 
146   
147                  If the pole is part of the great circle, a constant (but fairly 
148                  meaningless) value is returned. 
149                  """ 
150                  if self.overPole: 
151                          return self.long1 
152                  return atan( 
153                          tan(self.lat1)*(sin(long-self.long2)/sin(self.long1-self.long2)) 
154                          -tan(self.lat2)*(sin(long-self.long1)/sin(self.long1-self.long2))) 
 155   
157                  """returns a sequence of bounding boxes for this great circle segment. 
158   
159                  Actually, the sequence contains one box for a segment that does not 
160                  cross the stitching line, two boxes for those that do. 
161                  """ 
162                  if not self.overPole and common.TWO_PI<=self.long1+self.long2<3*math.pi: 
163                           
164                          latAtStich = self.latForLong(0) 
165                          return [ 
166                                  self._computeBBFor(0, latAtStich, self.long1, self.lat1), 
167                                  self._computeBBFor(self.long2, self.lat2, 2*math.pi, latAtStich)] 
168                  else: 
169                          return [ 
170                                  self._computeBBFor(self.long1, self.lat1, self.long2, self.lat2)] 
  171   
182   
185          """yields one or two bboxes from for spherical coordinates. 
186   
187          This handles crossing the stich line as well as shooting over the pole. 
188   
189          Everything here is in degrees. 
190   
191          This function assumes that -360<minRA<maxRA<720 and that at least one 
192          of minRA, maxRA is between 0 and 360. 
193          """ 
194          if minDec<-90: 
195                   
196                  maxDec = max(maxDec, -minDec-180) 
197                  minDec = -90 
198                  minRA, maxRA = 0, 360 
199   
200          if maxDec>90: 
201                   
202                  minDec = min(minDec, 180-maxDec) 
203                  maxDec = 90 
204                  minRA, maxRA = 0, 360 
205   
206          if minRA<0: 
207                  yield (0, minDec, maxRA, maxDec) 
208                  yield (360+minRA, minDec, 360, maxDec) 
209          elif maxRA>360: 
210                  yield (0, minDec, maxRA-360, maxDec) 
211                  yield (minRA, minDec, 360, maxDec) 
212          else: 
213                  yield (minRA, minDec, maxRA, maxDec) 
 214   
217          """returns the intersection of the two bboxes. 
218          """ 
219          return ( 
220                  max(bbox1[0], bbox2[0]), 
221                  max(bbox1[1], bbox2[1]), 
222                  min(bbox1[2], bbox2[2]), 
223                  min(bbox1[3], bbox2[3])) 
 224   
227          """returns true if bbox is empty. 
228          """ 
229          return bbox[0]>=bbox[2] or bbox[1]>=bbox[3] 
 230   
233          """helps _computeIntersection; see comment there. 
234          """ 
235          if seq1 is None: 
236                  return seq2 
237          if seq2 is None: 
238                  return seq1 
239   
240          commonBoxes = [] 
241          for box1 in seq1: 
242                  for box2 in seq2: 
243                          commonBox = _intersectBboxes(box1, box2) 
244                          if not _isEmpty(commonBox): 
245                                  commonBoxes.append(commonBox) 
246          return commonBoxes 
 247   
250          """helps _getBboxesFor. 
251          """ 
252          return _makeSphericalBbox( 
253                  circle.center[0]-circle.radius, 
254                  circle.center[1]-circle.radius, 
255                  circle.center[0]+circle.radius, 
256                  circle.center[1]+circle.radius) 
 257   
260          """helps _getBboxesFor. 
261   
262          TODO: Make tighter by actually using minor axis. 
263          """ 
264          return _makeSphericalBbox( 
265                  ellipse.center[0]-ellipse.smajAxis, 
266                  ellipse.center[1]-ellipse.smajAxis, 
267                  ellipse.center[0]+ellipse.smajAxis, 
268                  ellipse.center[1]+ellipse.smajAxis) 
 269   
272          """helps _getBboxesFor. 
273   
274          Note that the box boundaries are great circles. 
275          """ 
276          maxDec = max(-90, min(90, box.center[1]+box.boxsize[1])) 
277          minDec = max(-90, min(90, box.center[1]-box.boxsize[1])) 
278   
279          if maxDec>90-1e-10: 
280                  yield (0, minDec, 360, 90) 
281                  return 
282          if minDec<-90+1e-10: 
283                  yield (0, -90, 360, maxDec) 
284                  return 
285   
286          upperBBs = GCSegment.fromDegrees( 
287                  box.center[0]-box.boxsize[0], maxDec, 
288                  box.center[0]+box.boxsize[0], maxDec 
289                  ).getBBs() 
290          lowerBBs = GCSegment.fromDegrees( 
291                  box.center[0]-box.boxsize[0], minDec, 
292                  box.center[0]+box.boxsize[0], minDec 
293                  ).getBBs() 
294   
295          for bbox in _makeSphericalBbox( 
296                          *joinBboxes(fromRad(upperBBs[0]), fromRad(lowerBBs[0]))): 
297                  yield bbox 
298          if len(upperBBs)==2:  
299                  assert len(lowerBBs)==2 
300                  for bbox in _makeSphericalBbox( 
301                                  *joinBboxes(fromRad(upperBBs[1]), fromRad(lowerBBs[1]))): 
302                          yield bbox 
 303   
306          """helps _getBboxesFor. 
307          """ 
308          leftBoxes, rightBoxes = [], [] 
309   
310          def addBoxes(b): 
311                  rightBoxes.append(fromRad(b[0])) 
312                  if len(b)>1: 
313                          leftBoxes.append(fromRad(b[1])) 
 314   
315          firstPoint = lastPoint = poly.vertices[0] 
316          for point in poly.vertices[1:]: 
317                  curSegment = GCSegment.fromDegrees( 
318                          lastPoint[0], lastPoint[1], point[0], point[1]) 
319                  addBoxes(list(curSegment.getBBs())) 
320                  lastPoint = point 
321   
322          try: 
323                  curSegment = GCSegment.fromDegrees( 
324                          lastPoint[0], lastPoint[1], firstPoint[0], firstPoint[1]) 
325          except ValueError:  
326                              
327                  pass 
328          else: 
329                  addBoxes(list(curSegment.getBBs())) 
330   
331          yield joinBboxes(*rightBoxes) 
332          if leftBoxes: 
333                  yield joinBboxes(*leftBoxes) 
334   
337          """helps _getBboxesFor. 
338          """ 
339          yield (0, -90, 360, 90) 
 340   
343          """helps _getBboxesFor. 
344   
345          PositionInterval could have all kinds of weird coordinate systems. 
346          We only check there's exactly two dimensions and otherwise hope for the 
347          best. 
348          """ 
349          if geo.frame.nDim!=2: 
350                  raise ValueError("Only PositionsIntervals with nDim=2 are supported" 
351                          " for bboxes.") 
352   
353          lowerLimit = geo.lowerLimit 
354          if lowerLimit is None: 
355                  lowerLimit = (0, -90) 
356   
357          upperLimit = geo.upperLimit 
358          if upperLimit is None: 
359                  upperLimit = (360, 90) 
360          return _makeSphericalBbox(lowerLimit[0], lowerLimit[1], 
361                  upperLimit[0], upperLimit[1]) 
 362   
365          """helps _getBboxesFor. 
366   
367          Union is just returning all bboxes from our child geometries. 
368          """ 
369          for child in geo.children: 
370                  for bbox in _getBboxesFor(child): 
371                          yield bbox 
 372   
375          """helps _getBboxesFor. 
376          """ 
377   
378   
379   
380   
381          commonBboxes = None 
382          for child in geo.children: 
383                  commonBboxes = _computeIntersectionForSequence(commonBboxes, 
384                          list(_getBboxesFor(child))) 
385          for bbox in commonBboxes: 
386                  yield bbox 
 387   
389          """helps _getBboxesFor. 
390   
391          (we ignore the cut-out). 
392          """ 
393          return _getBboxesFor(geo.children[0]) 
 394   
396          """helps _getBboxesFor. 
397   
398          (we ignore the cut-out and always return the entire sky) 
399          """ 
400   
401   
402   
403          return _computeAllSkyBbox(geo) 
 404   
405   
406   
407  _BBOX_COMPUTERS = { 
408          "Circle": _computeCircleBbox, 
409          "Ellipse": _computeEllipseBbox, 
410          "Box": _computeBoxBbox, 
411          "Polygon": _computePolygonBbox, 
412          "AllSky": _computeAllSkyBbox, 
413          "SpaceInterval": _computeSpaceIntervalBbox, 
414          "Union": _computeUnionBbox, 
415          "Intersection": _computeIntersectionBbox, 
416          "Difference": _computeDifferenceBbox, 
417          "Not": _computeNotBbox, 
418  } 
422          """yields one or two bboxes for a conformed geometry. 
423   
424          Two bboxes are returned when the geometry overlaps the stitching point. 
425   
426          A geometry is conformed if it's been conformed to what's coming back 
427          from getStandardFrame() above. 
428          """ 
429          geoName = geo.__class__.__name__ 
430          if geoName not in _BBOX_COMPUTERS: 
431                  raise common.STCInternalError("Do now know how to compute the bbox of" 
432                          " %s."%geoName) 
433           
434          for bbox in _BBOX_COMPUTERS[geoName](geo): 
435                  yield bbox 
 436   
440   
443          """returns a bounding box encompassing all the the bboxes passed in. 
444   
445          No input bbox must cross the stitching line; they must be normalized, 
446          i.e., lower left and upper right corners. 
447          """ 
448          if not bboxes: 
449                  raise common.InternalError("bbox join without bbox") 
450          minRA, maxRA = utils.Supremum, utils.Infimum 
451          minDec, maxDec = utils.Supremum, utils.Infimum 
452   
453          for ra0, dec0, ra1, dec1 in bboxes: 
454                  minRA = min(minRA, ra0) 
455                  minDec = min(minDec, dec0) 
456                  maxRA = max(maxRA, ra1) 
457                  maxDec = max(maxDec, dec1) 
458           
459          return (minRA, minDec, maxRA, maxDec) 
 460   
463          """iterates over the bboxes of the areas within ast. 
464   
465          bboxes are (ra0, de0, ra1, de1) in ICRS degrees. 
466          """ 
467          astc = conform.conform(ast, getStandardFrame()) 
468          for area in astc.areas: 
469                  for bbox in _getBboxesFor(area): 
470                          yield bbox 
 471