001 package edu.nrao.sss.model.project.scan; 002 003 import java.math.BigDecimal; 004 import java.util.ArrayList; 005 import java.util.Collections; 006 import java.util.Comparator; 007 import java.util.List; 008 009 import javax.xml.bind.annotation.XmlElement; 010 import javax.xml.bind.annotation.XmlElementWrapper; 011 import javax.xml.bind.annotation.XmlRootElement; 012 import javax.xml.bind.annotation.XmlType; 013 014 import edu.nrao.sss.measure.Angle; 015 import edu.nrao.sss.measure.ArcUnits; 016 import edu.nrao.sss.measure.TimeDuration; 017 import edu.nrao.sss.model.source.SourceCatalogEntry; 018 019 /** 020 * A scan that tips the telescope to several elevations along a given 021 * azimuth. 022 * <p> 023 * <b>CVS Info:</b> 024 * <table style="margin-left:2em"> 025 * <tr><td>$Revision: 2146 $</td></tr> 026 * <tr><td>$Date: 2009-04-01 11:21:10 -0600 (Wed, 01 Apr 2009) $</td></tr> 027 * <tr><td>$Author: btruitt $</td></tr> 028 * </table></p> 029 * 030 * @author David M. Harland 031 * @since 2006-07-18 032 */ 033 @XmlRootElement 034 @XmlType(propOrder= {"azimuth", "order", "allowReverseOrder", "elevations"}) 035 public class TippingScan 036 extends Scan 037 { 038 //Persisted attributes 039 private Angle azimuth; 040 private List<TippingPosition> elevations; 041 private TippingOrder order; 042 private boolean allowReverseOrder; 043 044 //Other attributes 045 046 /** Creates a new instance. */ 047 TippingScan() 048 { 049 super(); 050 051 azimuth = new Angle("165"); 052 elevations = new ArrayList<TippingPosition>(); 053 order = TippingOrder.getDefault(); 054 allowReverseOrder = true; 055 } 056 057 /** Does nothing. Tipping scans may not have sources. */ 058 @Override 059 public void setSourceCatalogEntry(SourceCatalogEntry sourceOrTable) 060 { 061 super.setSourceCatalogEntry(null); 062 } 063 064 /** 065 * Sets the azimuth along which the tipping will be done. 066 * @param newAzimuth the azimuth along which the tipping will be done. 067 */ 068 public void setAzimuth(Angle newAzimuth) 069 { 070 azimuth = (newAzimuth == null) ? new Angle() : newAzimuth; 071 } 072 073 /** 074 * Returns the azimuth along which the tipping will be done. 075 * @return the azimuth along which the tipping will be done. 076 */ 077 public Angle getAzimuth() 078 { 079 return azimuth; 080 } 081 082 /** 083 * Sets the tipping order to either low-to-high or high-to-low. 084 * 085 * @param newOrder the order in which the tipping should be 086 * performed. A value of <i>null</i> is treated as 087 * a signal to perform the tipping in a default 088 * order. 089 */ 090 public void setOrder(TippingOrder newOrder) 091 { 092 order = (newOrder == null) ? TippingOrder.getDefault() : newOrder; 093 } 094 095 /** 096 * Returns the order (low-to-high or high-to-low) in which the 097 * tipping is performed. 098 * @return the order in which the tipping is performed. 099 */ 100 public TippingOrder getOrder() 101 { 102 return order; 103 } 104 105 /** 106 * Configures this scan so that even numbered iterations of this scan 107 * proceed in either the opposite or same order as the first iteration. 108 * <p> 109 * Example: An observer sets the tipping order to low-to-high. There is 110 * enough time in this scan to do multiple iterations of the tipping. 111 * If {@code allow} is <i>true</i>, then the second (and all other even-numbered) 112 * tipping(s) will be from high-to-low -- the reverse order of the first. 113 * If {@code allow} is <i>false</i>, all tippings will be from low-to-high.</p> 114 * 115 * @param allow allows subsequent iterations of this scan to cycle through 116 * the elevations in reverse order, relative to the prior 117 * iteration. 118 */ 119 public void setAllowReverseOrder(boolean allow) 120 { 121 allowReverseOrder = allow; 122 } 123 124 /** 125 * Returns <i>true</i> if successive iterations of this scan are allowed 126 * to cycle through the elevations in opposite orders. 127 * 128 * @see #setAllowReverseOrder(boolean) 129 * 130 * @return <i>true</i> if successive iterations of this scan are allowed 131 * to cycle through the elevations in opposite orders. 132 */ 133 public boolean getAllowReverseOrder() 134 { 135 return allowReverseOrder; 136 } 137 138 /** 139 * Adds a series of tipping elevations to this scan's list. 140 * The number of new elevations added is {@code elevationCount}. 141 * The lowest new elevation is {@code minEl}, the largest is 142 * {@code maxEl}. The other elevations are derived in such 143 * a way that the graph of elevation index versus the secant 144 * of the zenith angle is linear. 145 * 146 * @param minEl the minimum elevation to be added. 147 * While a minimum elevation of zero is permitted, 148 * using a zero elevation is unlikely to give 149 * useful results as the algorithm used approaches 150 * infinity for one of its intermediate values. 151 * 152 * @param maxEl the maximum elevation to be added. 153 * 154 * @param elevationCount the number of new elevations to be added. 155 * Special cases:<ul> 156 * <li>elevationCount < 1: No new elevations will be added.</li> 157 * <li>elevationCount == 1: Only the {@code minEl} will be 158 * added.</li> 159 * <li>elevationCount == 2: Only the {@code minEl} and 160 * {@code maxEl} will be added.</li> 161 * </ul> 162 * 163 * @throws IllegalArgumentException if either {@code minEl} or 164 * {@code maxEl} are not within the range [0,quarterCircle]. 165 */ 166 public void addElevations(Angle minEl, Angle maxEl, int elevationCount, 167 TimeDuration timePerElevation) 168 throws IllegalArgumentException 169 { 170 //Throw illegal arg exception if either elevation is invalid 171 validateElevationArgument(minEl, "minEl"); 172 validateElevationArgument(maxEl, "maxEl"); 173 174 //Special cases 175 if (elevationCount < 1) //No action 176 { 177 return; 178 } 179 else if (elevationCount <= 2) //Add only min (& possibly max) elev 180 { 181 TippingPosition lowestPos = new TippingPosition(); 182 lowestPos.setElevation(minEl.clone()); 183 lowestPos.getTimeAtPosition().set(timePerElevation); 184 elevations.add(lowestPos); 185 186 if (elevationCount == 2) //Add only min & max elevations 187 { 188 TippingPosition highestPos = new TippingPosition(); 189 highestPos.setElevation(maxEl.clone()); 190 highestPos.getTimeAtPosition().set(timePerElevation); 191 elevations.add(highestPos); 192 } 193 } 194 //Normal logic 195 else 196 { 197 double[] radianAltitudes = 198 calculateElevations(minEl.toUnits(ArcUnits.RADIAN).doubleValue(), 199 maxEl.toUnits(ArcUnits.RADIAN).doubleValue(), 200 elevationCount); 201 202 for (double altitude : radianAltitudes) 203 { 204 TippingPosition tippingPos = new TippingPosition(); 205 tippingPos.setElevation(new Angle(BigDecimal.valueOf(altitude), 206 ArcUnits.RADIAN)); 207 tippingPos.getTimeAtPosition().set(timePerElevation); 208 elevations.add(tippingPos); 209 } 210 } 211 } 212 213 /** 214 * Calculates and returns a set of elevations, in radians. 215 * NOTE: This method does no error checking on its params. Since this 216 * is a private method under the control of this class, this is 217 * OK. HOWEVER, if you broaden the scope of this method, you 218 * will need to incorporate error checking. 219 */ 220 private double[] calculateElevations(double minEl, double maxEl, int count) 221 { 222 double[] result = new double[count]; //Elevations, in radians 223 224 //Work with zenith angles 225 final double HALF_PI = Math.PI / 2.0; 226 227 double minZA = HALF_PI - maxEl; 228 double maxZA = HALF_PI - minEl; 229 230 double secMin = 1.0 / Math.cos(minZA); 231 double secMax = 1.0 / Math.cos(maxZA); 232 233 double secSpread = (secMax - secMin) / (count - 1); 234 235 double secant = secMin; 236 237 for (int a=1; a < (count-1); a++) 238 { 239 secant += secSpread; 240 double zenithAngle = Math.acos(1.0 / secant); 241 result[a] = HALF_PI - zenithAngle; //Convert back to elevation 242 } 243 244 //End points 245 result[0] = minEl; 246 result[count-1] = maxEl; 247 248 return result; 249 } 250 251 /** 252 * Used for checking elevation parameters of other methods. 253 * Throws IllegalArgumentException if elev not in valid range. 254 */ 255 private void validateElevationArgument(Angle elev, String name) 256 throws IllegalArgumentException 257 { 258 double angle = elev.getValue().doubleValue(); 259 double quarterCircle = elev.getUnits().toQuarterCircle().doubleValue(); 260 261 if ((angle < 0.0) || (angle > quarterCircle)) 262 { 263 StringBuilder buff = new StringBuilder("Illegal value ("); 264 265 buff.append(elev).append(") for parameter ").append(name); 266 buff.append(". Elevation must be >= 0 && <= ").append(quarterCircle); 267 buff.append(elev.getUnits().getSymbol()); 268 269 throw new IllegalArgumentException(buff.toString()); 270 } 271 } 272 273 /** 274 * Sets the list of tipping elevations held by this scan. 275 * A <i>null</i> {@code replacementList} will be interpreted 276 * as a new empty list. 277 * <p> 278 * This scan will hold a reference to {@code replacementList} 279 * (unless it is <i>null</i>), so any changes made to the list 280 * after calling this method will be reflected in this object.</p> 281 * 282 * @param replacementList a list of tipping offsets to be held by this 283 * scan. 284 */ 285 public void setElevations(List<TippingPosition> replacementList) 286 { 287 elevations = (replacementList == null) ? new ArrayList<TippingPosition>() 288 : replacementList; 289 } 290 291 /** 292 * Returns a sorted list of tipping elevations. The returned list, 293 * which is guaranteed to be non-null, is 294 * ordered either from low-to-high or high-to-low, depending on the 295 * value of the {@link #getOrder() tipping order}. 296 * <p> 297 * The returned list is the actual list held by this scan, 298 * so changes made to the list will be reflected in this 299 * object.</p> 300 * 301 * @return a sorted list of tipping elevations. 302 */ 303 @XmlElementWrapper 304 @XmlElement(name="tippingPosition") 305 public List<TippingPosition> getElevations() 306 { 307 //Just-in-time sorting of list 308 Collections.sort(elevations, getComparatorFor(getOrder())); 309 310 return elevations; 311 } 312 313 /** 314 * Returns a list of tipping elevations that has been sorted in a way 315 * that is appropriate for the given iteration of this scan. 316 * <p> 317 * The appropriate sorting order is determined by the value of the 318 * {@link #getAllowReverseOrder()} property and the iteration number. 319 * If {@code getAllowReverseOrder()} is true and the iteration is 320 * an even number, then the sorting is in the opposite direction of 321 * that specified by {@link #getOrder()}.</p> 322 * <p> 323 * Note that the returned list is <i>not</i> the list held internally 324 * by the scan, so changes to the list will not be reflected in this 325 * object. However, the elevations held in the returned list are the 326 * same elevations held by this object's internal list, so changing 327 * one of those objects will impact this one.</p> 328 * 329 * @param iteration one more than the number of times this scan has 330 * already been run. The minimum acceptable value 331 * is one. 332 * 333 * @return a sorted list of tipping elevations. 334 * 335 * @throws IllegalArgumentException 336 */ 337 public List<TippingPosition> getElevations(int iteration) 338 { 339 if (iteration < 1) 340 throw new IllegalArgumentException("Iteration must be >= 1."); 341 342 //Clone the elevation list. (Don't need to clone elements.) 343 List<TippingPosition> result = new ArrayList<TippingPosition>(); 344 result.addAll(elevations); 345 346 //Determine the tipping order 347 TippingOrder tipOrd = getOrder(); 348 349 if (getAllowReverseOrder() && (iteration % 2 == 0)) 350 tipOrd = tipOrd.getReverseOrder(); 351 352 //Sort the cloned list according to the tipping order for this iteration 353 Collections.sort(result, getComparatorFor(tipOrd)); 354 355 return result; 356 } 357 358 /** 359 * Returns a comparator for sorting a list of angles that is appropriate 360 * for the given tipping order. 361 */ 362 private Comparator<TippingPosition> getComparatorFor(TippingOrder tipOrder) 363 { 364 //Low-to-high is the natural order of angle. 365 //Null is a signal to use natural order. 366 Comparator<TippingPosition> comparator = null; 367 368 //If client wants high-to-low, use a reverse-order comparator 369 if (tipOrder.equals(TippingOrder.HIGH_TO_LOW)) 370 comparator = Collections.reverseOrder(); 371 372 return comparator; 373 } 374 375 //============================================================================ 376 // 377 //============================================================================ 378 379 /* (non-Javadoc) 380 * @see edu.nrao.sss.model.project.scan.ScanLoopElement#toString() 381 */ 382 public String toSummaryString() 383 { 384 StringBuilder buff = new StringBuilder(super.toSummaryString()); 385 386 buff.append(", az=").append(azimuth); 387 buff.append(", order=").append(order); 388 buff.append(", allowRev=").append(allowReverseOrder); 389 buff.append(", elevCnt=").append(elevations.size()); 390 391 return buff.toString(); 392 } 393 394 /** 395 * Returns a tipping scan that is a copy of this one. 396 * <p> 397 * The returned scan is, for the most part, a deep copy of this one. 398 * However, there are a few exceptions noted in the 399 * {@link ScanLoop#clone() clone method} of this class's parent.</p> 400 * <p> 401 * If anything goes wrong during the cloning procedure, 402 * a {@code RuntimeException} will be thrown.</p> 403 */ 404 public TippingScan clone() 405 { 406 TippingScan clone = null; 407 408 try 409 { 410 //This line takes care of the primitive & immutable fields properly 411 clone = (TippingScan)super.clone(); 412 413 clone.azimuth = this.azimuth.clone(); 414 415 //Need to clone set AND contained elements. 416 clone.elevations = new ArrayList<TippingPosition>(); 417 for (TippingPosition tp : this.elevations) 418 clone.elevations.add(tp.clone()); 419 } 420 catch (Exception ex) 421 { 422 throw new RuntimeException(ex); 423 } 424 425 return clone; 426 } 427 428 /** 429 * Returns <i>true</i> if {@code o} is equal to this tipping scan. 430 * <p> 431 * In order for {@code o} to be equal to this scan, it must have 432 * equal tipping positions in the same order as those of this scan. 433 * It must also follow the rules set forth in the 434 * {@link ScanLoop#equals(Object) equals method} of this class's parent.</p> 435 */ 436 public boolean equals(Object o) 437 { 438 //Quick exit if o is this object 439 if (o == this) 440 return true; 441 442 //Not equal if super class says not equal 443 if (!super.equals(o)) 444 return false; 445 446 //Super class tested for Class equality, so cast is safe 447 TippingScan other = (TippingScan)o; 448 449 //Compare attributes 450 return other.azimuth.equals(this.azimuth) && 451 other.order.equals(this.order) && 452 other.allowReverseOrder == this.allowReverseOrder && 453 other.elevations.equals(this.elevations); 454 } 455 456 /* (non-Javadoc) 457 * @see edu.nrao.sss.model.project.scan.Scan#hashCode() 458 */ 459 public int hashCode() 460 { 461 //Taken from the Effective Java book by Joshua Bloch. 462 //The constant 37 is arbitrary & carries no meaning. 463 int result = 37 * super.hashCode(); 464 465 result = 37 * result + azimuth.hashCode(); 466 result = 37 * result + order.hashCode(); 467 result = 37 * result + new Boolean(allowReverseOrder).hashCode(); 468 result = 37 * result + elevations.hashCode(); 469 470 return result; 471 } 472 473 /* 474 public static void main(String[] args) 475 { 476 TippingScan ts = new TippingScan(); 477 478 ts.addElevations(new Angle(20.0), new Angle(70.0), 7, new TimeDuration(0.02)); 479 480 //ts.setOrder(TippingOrder.HIGH_TO_LOW); 481 482 System.out.println("Tip #; Elev (deg); ZA (deg); sec(ZA); delta(sec(ZA));"); 483 double prevSec = -1.0; 484 double currSec = -1.0; 485 int tip=0; 486 for (TippingPosition tp : ts.getElevations()) 487 { 488 Angle a = tp.getElevation(); 489 System.out.printf("%1$4s", ++tip + "; "); 490 //Elev, in degrees 491 System.out.printf("%1$.3f", a.toUnits(ArcUnits.DEGREE)); 492 System.out.print("; "); 493 //ZA, in deg 494 System.out.printf("%1$.3f", (90.0 - a.toUnits(ArcUnits.DEGREE))); 495 System.out.print("; "); 496 //Secant of zenith angle 497 Angle ZA = a.clone().negate().add(Math.PI/2.0); 498 currSec = 1.0/Math.cos(ZA.toUnits(ArcUnits.RADIAN)); 499 System.out.printf("%1$.10f", currSec); 500 System.out.print("; "); 501 if (prevSec >= 0.0) 502 System.out.printf("%1$.10f", currSec-prevSec); 503 else 504 System.out.print(" "); 505 System.out.println(';'); 506 507 prevSec = currSec; 508 } 509 } 510 */ 511 }