001 package edu.nrao.sss.astronomy; 002 003 import java.math.BigDecimal; 004 import java.util.ArrayList; 005 import java.util.Collection; 006 import java.util.Date; 007 008 import edu.nrao.sss.measure.Angle; 009 import edu.nrao.sss.measure.ArcUnits; 010 import edu.nrao.sss.measure.Distance; 011 import edu.nrao.sss.measure.Latitude; 012 import edu.nrao.sss.measure.Longitude; 013 import edu.nrao.sss.measure.LongitudeInterval; 014 import edu.nrao.sss.util.Filter; 015 016 /** 017 * A filter that operates on {@link SkyPosition}s. 018 * <p> 019 * This filter allows you to select positions based on their latitudes, 020 * longitudes, and distances from some origin. In addition, this 021 * filter has special {@link #setCone(SkyPosition, Angle) setCone} 022 * methods that can be used separately or in conjunction with the 023 * typical min/max ranges available for latitude and longitude. 024 * That is, you could select only those positions that were both 025 * in a given cone search and within given min/max latitude/longitude 026 * values.</p> 027 * <p> 028 * <b>Revision Info:</b> 029 * <table style="margin-left:2em"> 030 * <tr><td>$Revision: 1239 $</td></tr> 031 * <tr><td>$Date: 2008-04-25 10:34:57 -0600 (Fri, 25 Apr 2008) $</td></tr> 032 * <tr><td>$Author: dharland $ (last person to modify)</td></tr> 033 * </table></p> 034 * 035 * @author David M. Harland 036 * @since 2006-06-27 037 */ 038 public class SkyPositionFilter 039 implements Filter<SkyPosition> 040 { 041 /** 042 * A constant that may be used to indicate that the minimum or maximum 043 * longitude is unbounded. 044 */ 045 public static final Longitude ANY_LONGITUDE = null; 046 047 /** 048 * A constant that may be used to indicate that the minimum or maximum 049 * latitude is unbounded. 050 */ 051 public static final Latitude ANY_LATITUDE = null; 052 053 /** 054 * A constant that may be used to indicate that the minimum or maximum 055 * distance is unbounded. 056 */ 057 public static final Distance ANY_DISTANCE = null; 058 059 /** 060 * A constant that indicates the current time should be used when evaluating 061 * the information held by a position. 062 */ 063 public static final Date CURRENT_TIME = null; 064 065 //Filtering criteria 066 private LongitudeInterval lonRange; 067 private Latitude latMin; 068 private Latitude latMax; 069 private Distance distMin; 070 private Distance distMax; 071 072 private boolean coneSearch; 073 private boolean coneTouchesPole; 074 private Angle coneRadius; 075 private SimpleSkyPosition coneCenter; 076 private LongitudeInterval coneLonRange; 077 private Latitude coneLatMin, coneLatMax; 078 079 private Date queryTime; 080 081 /** 082 * Creates a new wide-open filter that allows all positions to pass. 083 */ 084 public SkyPositionFilter() 085 { 086 coneCenter = new SimpleSkyPosition(); 087 coneLonRange = new LongitudeInterval(); 088 lonRange = new LongitudeInterval(); 089 090 clearAll(); 091 } 092 093 //============================================================================ 094 // CLEARING THE FILTERING CRITERIA 095 //============================================================================ 096 097 /** 098 * Sets this filter to a wide-open state. 099 * After this call all filtering criteria will be in their wide-open 100 * states and this filter will allow all positions to pass. 101 */ 102 public void clearAll() 103 { 104 clearCone(); 105 clearLongitude(); 106 clearLatitude(); 107 clearDistance(); 108 clearQueryTime(); 109 } 110 111 /** 112 * Stops this filter from applying a cone search. 113 * 114 * @see #setCone(Latitude, Longitude, Angle) 115 */ 116 public void clearCone() 117 { 118 coneSearch = false; 119 //It's not necessary to clear the other cone variables 120 } 121 122 /** 123 * Sets the longitude criterion to its wide-open state. 124 * 125 * @see #setLongitude(Longitude, Angle) 126 * @see #setLongitudeRange(Longitude, Longitude) 127 */ 128 public void clearLongitude() 129 { 130 lonRange.reset(); 131 } 132 133 /** 134 * Sets the latitude criterion to its wide-open state. 135 * 136 * @see #setLatitude(Latitude, Angle) 137 * @see #setLatitudeRange(Latitude, Latitude) 138 */ 139 public void clearLatitude() 140 { 141 latMin = ANY_LATITUDE; 142 latMax = ANY_LATITUDE; 143 } 144 145 /** 146 * Sets the distance criterion to its wide-open state. 147 * 148 * @see #setDistance(Distance, Distance) 149 * @see #setDistanceRange(Distance, Distance) 150 */ 151 public void clearDistance() 152 { 153 distMin = ANY_DISTANCE; 154 distMax = ANY_DISTANCE; 155 } 156 157 /** 158 * Sets this filter so that it will use the current time when querying 159 * positions sent to the {@link #allows(SkyPosition)} or 160 * {@link #blocks(SkyPosition)} methods. 161 */ 162 public void clearQueryTime() 163 { 164 queryTime = CURRENT_TIME; 165 } 166 167 //============================================================================ 168 // SETTING THE FILTER CRITERIA 169 //============================================================================ 170 171 /** 172 * Sets this filter so that only positions within the cone described 173 * by the center and the search radius may pass. 174 * Clients may further narrow the search by explicitly 175 * setting minimum and maximum latitudes, longitudes, and distances. 176 * <p/> 177 * This is a convenience method that is equivalent to calling: 178 * <pre> 179 * setCone(center.getLatitude(), center.getLongitude(), searchRadius); 180 * </pre> 181 * 182 * @param center the center of the search cone. 183 * 184 * @param searchRadius the radius of the cone. The radius may not be 185 * larger than one-quarter circle (e.g., 90 degrees). 186 * If it is, an illegal argument exception is thrown. 187 */ 188 public void setCone(SkyPosition center, Angle searchRadius) 189 { 190 setCone(center.getLatitude(), center.getLongitude(), searchRadius); 191 } 192 193 private static final BigDecimal POS_90 = new BigDecimal("90"); 194 private static final BigDecimal NEG_90 = new BigDecimal("-90"); 195 196 /** 197 * Sets this filter so that only positions within the cone described 198 * by the center latitude / longitude pair and the search radius 199 * may pass. Clients may further narrow the search by explicitly 200 * setting minimum and maximum latitudes, longitudes, and distances. 201 * 202 * @param centerLatitude the latitude of the center point of the cone. 203 * 204 * @param centerLongitude the longitude of the center point of the cone. 205 * 206 * @param searchRadius the radius of the cone. The radius may not be 207 * larger than one-quarter circle (e.g., 90 degrees). 208 * If it is, an illegal argument exception is thrown. 209 */ 210 public void setCone(Latitude centerLatitude, Longitude centerLongitude, 211 Angle searchRadius) 212 { 213 //Keep diameter <= 180 degrees 214 if (searchRadius.getValue().compareTo(searchRadius.getUnits().toQuarterCircle()) > 0) 215 throw new IllegalArgumentException( 216 "searchRadius may not be greater than 1/4 circle. Found: " + 217 searchRadius); 218 219 coneSearch = true; 220 221 coneCenter.setLatitude(centerLatitude.clone()); 222 coneCenter.setLongitude(centerLongitude.clone()); 223 224 coneRadius = searchRadius.clone(); 225 226 //Performing a cone search involves lots of expensive trigonometry calcs. 227 //We set these latitude and longitude ranges so that we can quickly 228 //weed out the obvious mismatches. 229 230 //For latitude, we can be exact. 231 //Need to beware crossing pole. 232 coneTouchesPole = false; 233 234 BigDecimal degrees = 235 centerLatitude.toUnits(ArcUnits.DEGREE) 236 .subtract(searchRadius.toUnits(ArcUnits.DEGREE)); 237 238 if (degrees.compareTo(NEG_90) <= 0) 239 { 240 degrees = NEG_90; 241 coneTouchesPole = true; 242 } 243 coneLatMin = new Latitude(degrees, ArcUnits.DEGREE); 244 245 degrees = centerLatitude.toUnits(ArcUnits.DEGREE) 246 .add(searchRadius.toUnits(ArcUnits.DEGREE)); 247 248 if (degrees.compareTo(POS_90) >= 0) 249 { 250 degrees = POS_90; 251 coneTouchesPole = true; 252 } 253 coneLatMax = new Latitude(degrees, ArcUnits.DEGREE); 254 255 //We cannot use the same simple formula for longitude. 256 //To see this imagine placing a small circle on the equator. 257 //This circle spans a certain longitude range. As you move that circle 258 //closer to one of the poles, it covers a larger and larger range until 259 //it covers all lines of longitude once it touches the pole. 260 //Since we are restricting the radius to 90 degrees, we can at least 261 //trim the longitude range to 180d, unless our cone touches a pole. 262 if (coneTouchesPole) 263 { 264 coneLonRange.reset(); 265 } 266 else 267 { 268 Angle quarterCircle = new Angle(POS_90); 269 270 coneLonRange.set(centerLongitude.clone().subtract(quarterCircle), 271 centerLongitude.clone().add( quarterCircle)); 272 } 273 } 274 275 /** 276 * Sets that latitude and longitude ranges for this filter. 277 * Calling this method is equivalent to calling: 278 * <pre> 279 * setLatitude(centerLatitude, halfWidth); 280 * setLongitude(centerLongitude, halfWidth); 281 * </pre> 282 * 283 * @param centerLatitude the central value of the latitude range. 284 * @param centerLongitude the central value of the longitude range. 285 * @param halfWidth half the width of both the latitude and longitude ranges. 286 */ 287 public void setLatitudeLongitudeRectangle(Latitude centerLatitude, 288 Longitude centerLongitude, 289 Angle halfWidth) 290 { 291 setLatitude (centerLatitude, halfWidth); 292 setLongitude(centerLongitude, halfWidth); 293 } 294 295 /** 296 * Sets the longitude range for this filter. 297 * The range is from <tt>center</tt> <i>minus</i> <tt>halfWidth</tt> through 298 * <tt>center</tt> <i>plus</i> <tt>halfWidth</tt>. 299 * 300 * @param center the central point of the range. 301 * @param halfWidth half the width of the range. 302 */ 303 public void setLongitude(Longitude center, Angle halfWidth) 304 { 305 if ((center == null) || (center == ANY_LONGITUDE)) 306 { 307 clearLongitude(); 308 } 309 else 310 { 311 lonRange.set(center.clone().subtract(halfWidth), 312 center.clone().add (halfWidth)); 313 } 314 } 315 316 /** 317 * Sets the longitude range for this filter. 318 * Note that the {@code from} value is allowed to be numerically larger 319 * than the {@code to} value. This would be the case, for example, if 320 * you wanted to select positions whose longitudes where in the range 321 * 355 to 10 degrees. 322 * 323 * @param from 324 * the start of the range of longitude values that are allowed to 325 * pass through this filter. If this value is <i>null</i> or 326 * <tt>ANY_LONGITUDE</tt>, a longitude of zero will be used. 327 * @param to 328 * the end of the range of longitude values that are allowed to 329 * pass through this filter. If this value is <i>null</i> or 330 * <tt>ANY_LONGITUDE</tt>, a longitude whose value is equal 331 * to a full circle will be used. 332 */ 333 public void setLongitudeRange(Longitude from, Longitude to) 334 { 335 Longitude intervalStart = new Longitude("0.0"); 336 337 if (from != ANY_LONGITUDE) 338 intervalStart.set(from.getValue(), from.getUnits()); 339 340 Longitude intervalEnd = new Longitude(); 341 342 if (to == ANY_LONGITUDE) 343 { 344 intervalEnd.setToFullCircle(); 345 } 346 else 347 { 348 BigDecimal value = to.getValue(); 349 ArcUnits units = to.getUnits(); 350 351 if (value.compareTo(units.toFullCircle()) == 0) 352 intervalEnd.setToFullCircle(); 353 else 354 intervalEnd.set(value, units); 355 } 356 357 lonRange.set(intervalStart, intervalEnd); 358 } 359 360 /** 361 * Sets the longitude range for this filter. 362 * @param newInterval the new range. 363 */ 364 public void setLongitudeRange(LongitudeInterval newInterval) 365 { 366 lonRange.set(newInterval.getStart(), newInterval.getEnd()); 367 } 368 369 /** 370 * Sets the latitude range for this filter. 371 * The range is from <tt>center</tt> <i>minus</i> <tt>halfWidth</tt> through 372 * <tt>center</tt> <i>plus</i> <tt>halfWidth</tt>. 373 * 374 * @param center the central point of the range. 375 * @param halfWidth half the width of the range. 376 */ 377 public void setLatitude(Latitude center, Angle halfWidth) 378 { 379 if ((center == null) || (center == ANY_LATITUDE)) 380 { 381 clearLatitude(); 382 } 383 else 384 { 385 latMin = center.clone().subtract(halfWidth); 386 latMax = center.clone().add (halfWidth); 387 } 388 } 389 390 /** 391 * Sets the latitude range for this filter. 392 * To specify an unbounded minimum and/or maximum, use the 393 * constant {@link #ANY_LATITUDE}. 394 * 395 * @param min the minimum latitude that is allowed to pass 396 * through this filter. 397 * @param max the maximum latitude that is allowed to pass 398 * through this filter. 399 */ 400 public void setLatitudeRange(Latitude min, Latitude max) 401 { 402 latMin = (min == null) ? ANY_LATITUDE : min.clone(); 403 latMax = (max == null) ? ANY_LATITUDE : max.clone(); 404 } 405 406 /** 407 * Sets the distance range for this filter. 408 * The range is from <tt>center</tt> <i>minus</i> <tt>halfWidth</tt> through 409 * <tt>center</tt> <i>plus</i> <tt>halfWidth</tt>. 410 * 411 * @param center the central point of the range. 412 * @param halfWidth half the width of the range. 413 */ 414 public void setDistance(Distance center, Distance halfWidth) 415 { 416 if ((center == null) || (center == ANY_DISTANCE)) 417 { 418 clearDistance(); 419 } 420 else 421 { 422 distMin = center.clone().subtract(halfWidth); 423 distMax = center.clone().add (halfWidth); 424 } 425 } 426 427 /** 428 * Sets the distance range for this filter. 429 * To specify an unbounded minimum and/or maximum, use the 430 * constant {@link #ANY_DISTANCE}. 431 * 432 * @param min the minimum distance that is allowed to pass 433 * through this filter. 434 * @param max the maximum distance that is allowed to pass 435 * through this filter. 436 */ 437 public void setDistanceRange(Distance min, Distance max) 438 { 439 distMin = (min == null) ? ANY_DISTANCE : min.clone(); 440 distMax = (max == null) ? ANY_DISTANCE : max.clone(); 441 } 442 443 /** 444 * Sets the date and time at which the filtered position is 445 * queried for its longitude, latitude, and distance. 446 * 447 * @param time the time used by the {@link #allows(SkyPosition)} 448 * and {@link #blocks(SkyPosition)} methods when 449 * querying a position. 450 */ 451 public void setQueryTime(Date time) 452 { 453 queryTime = (time == null) ? CURRENT_TIME : time; 454 } 455 456 //============================================================================ 457 // APPLYING THIS FILTER 458 //============================================================================ 459 460 /** 461 * Returns <i>true</i> if this filter blocks the given position. 462 * <i>Null</i> positions are always blocked. 463 * 464 * @param sp the position to be filtered. 465 * 466 * @return <i>true</i> if this filter blocks {@code sp}. 467 */ 468 public boolean blocks(SkyPosition sp) 469 { 470 //Filter blocks all null positions 471 if (sp == null) 472 return true; 473 474 //Set up the query time 475 Date qt = (queryTime == CURRENT_TIME) ? new Date() : queryTime; 476 477 Longitude ra = sp.getLongitude(qt); 478 479 //Block sp if its longitude is out of range 480 if (!lonRange.contains(ra)) 481 return true; 482 483 Latitude dec = sp.getLatitude(qt); 484 485 //Block sp if its latitude is less than minimum 486 if ((latMin != ANY_LATITUDE) && (dec.compareTo(latMin) < 0)) 487 return true; 488 489 //Block sp if its latitude is greater than maximum 490 if ((latMax != ANY_LATITUDE) && (dec.compareTo(latMax) > 0)) 491 return true; 492 493 Distance dist = sp.getDistance(qt); 494 495 //Block sp if its distance is less than minimum 496 if ((distMin != ANY_DISTANCE) && (dist.compareTo(distMin) < 0)) 497 return true; 498 499 //Block sp if its distance is greater than maximum 500 if ((distMax != ANY_DISTANCE) && (dist.compareTo(distMax) > 0)) 501 return true; 502 503 //If we survived all the other tests, see if a cone search was requested 504 if (coneSearch) 505 { 506 //Block if sp is outside calculated latitude range 507 if (dec.compareTo(coneLatMin) < 0 || dec.compareTo(coneLatMax) > 0) 508 return true; 509 510 //Block if sp is outside calculated longitude range 511 if (!coneTouchesPole) 512 { 513 if (!coneLonRange.contains(ra)) 514 return true; 515 } 516 517 //Block if sp is outside of search cone 518 if (coneCenter.getAngularSeparation(sp).compareTo(coneRadius) > 0) 519 return true; 520 } 521 522 //sp was not blocked 523 return false; 524 } 525 526 /** 527 * Returns <i>true</i> if this filter allows the given position 528 * to pass through it. 529 * <i>Null</i> positions are always blocked. 530 * 531 * @param sp the position to be filtered. 532 * 533 * @return <i>true</i> if this filter allows {@code sp} to pass through it. 534 */ 535 public boolean allows(SkyPosition sp) 536 { 537 return !blocks(sp); 538 } 539 540 /** 541 * Removes from {@code flow} any position that is blocked by this filter. 542 * 543 * @param flow a collection of positions that this filter will alter by 544 * removing blocked positions. 545 * 546 * @return {@code flow}, after the removal of blocked positions. 547 * If {@code flow} is <i>null</i>, a new empty collection is returned. 548 */ 549 public Collection<? extends SkyPosition> 550 removeAllBlockedParticlesFrom(Collection<? extends SkyPosition> flow) 551 { 552 //Quick exit if no flow 553 if (flow == null) 554 return new ArrayList<SkyPosition>(); 555 556 ArrayList<SkyPosition> blockedPositions = new ArrayList<SkyPosition>(); 557 558 for (SkyPosition position : flow) 559 if (this.blocks(position)) 560 blockedPositions.add(position); 561 562 flow.removeAll(blockedPositions); 563 564 return flow; 565 } 566 }