001 package edu.nrao.sss.astronomy; 002 003 import static edu.nrao.sss.astronomy.CelestialCoordinateSystem.*; 004 005 import java.util.ArrayList; 006 import java.util.HashMap; 007 import java.util.HashSet; 008 import java.util.List; 009 import java.util.Map; 010 import java.util.Set; 011 012 import edu.nrao.sss.geom.EarthPosition; 013 import edu.nrao.sss.measure.LocalSiderealTime; 014 015 /** 016 * A coordinate converter that delegates its work to other 017 * converters. 018 * <p> 019 * <b>Version Info:</b> 020 * <table style="margin-left:2em"> 021 * <tr><td>$Revision: 1147 $</td></tr> 022 * <tr><td>$Date: 2008-03-06 08:50:11 -0700 (Thu, 06 Mar 2008) $</td></tr> 023 * <tr><td>$Author: dharland $ (last person to modify)</td></tr> 024 * </table></p> 025 * 026 * @author David M. Harland 027 * @since 2008-02-29 028 */ 029 public class CompositeConverter 030 implements CelestialCoordinateConverter 031 { 032 private static final String FROM_TO_SEP = "->"; 033 034 //These are the component converters keyed by a string that 035 //contains the "from" and "to" systems. See createKey method. 036 private Map<String, CelestialCoordinateConverter> converters; 037 038 //These are chains of converters that we can use in the event 039 //that we have no direct converter. Eg, if we have a direct 040 //converter from A->B another from B->C, we can chain these 041 //together to get an indirect conversion from A->C. 042 private Set<CcsEpochChain> converterChains; 043 044 /** 045 * Creates a new composite converter with no component converters. 046 * @see #getDefaultConverter() 047 */ 048 public CompositeConverter() 049 { 050 converters = new HashMap<String, CelestialCoordinateConverter>(); 051 converterChains = new HashSet<CcsEpochChain>(); 052 } 053 054 /** 055 * Creates a new converter that is preequipped with component converters. 056 * The component converters can handle these conversions directly: 057 * <table border="1" cellpadding="5"> 058 * <tr><th>From</th><th>To</th></tr> 059 * <tr><td>ECLIPTIC</td><td>EQUATORIAL (J2000)</td></tr> 060 * <tr><td>ECLIPTIC</td><td>GALACTIC</td></tr> 061 * <tr><td>EQUATORIAL (B1950)</td><td>EQUATORIAL (J2000)</td></tr> 062 * <tr><td>EQUATORIAL (J2000)</td><td>ECLIPTIC</td></tr> 063 * <tr><td>EQUATORIAL (J2000)</td><td>EQUATORIAL (B1950)</td></tr> 064 * <tr><td>EQUATORIAL (J2000)</td><td>GALACTIC</td></tr> 065 * <tr><td>EQUATORIAL (J2000)</td><td>HORIZONTAL</td></tr> 066 * <tr><td>GALACTIC</td><td>ECLIPTIC</td></tr> 067 * <tr><td>GALACTIC</td><td>EQUATORIAL (J2000)</td></tr> 068 * </table> 069 * A conversion from any one of the five systems listed above can be 070 * made to any of the others, if not directly, then by chaining two 071 * conversions together. <b>Note</b>: any conversion involving 072 * B1950 coordinates requires internet connectivity. 073 * 074 * @return a new composite converter preequipped with component converters. 075 */ 076 public static CompositeConverter getDefaultConverter() 077 { 078 CompositeConverter cc = new CompositeConverter(); 079 080 HeasarcCoordConverter heasarc = new HeasarcCoordConverter(); 081 EquatorialHorizontalConverter eqHorz = new EquatorialHorizontalConverter(); 082 StarlinkPalConverter starlink = new StarlinkPalConverter(); 083 084 cc.setConverter(eqHorz, EQUATORIAL, Epoch.J2000, HORIZONTAL, Epoch.J2000); 085 cc.setConverter(eqHorz, HORIZONTAL, Epoch.J2000, EQUATORIAL, Epoch.J2000); 086 087 cc.setConverter(heasarc, EQUATORIAL, Epoch.J2000, EQUATORIAL, Epoch.B1950); 088 cc.setConverter(heasarc, EQUATORIAL, Epoch.B1950, EQUATORIAL, Epoch.J2000); 089 090 cc.setConverter(starlink, EQUATORIAL, Epoch.J2000, GALACTIC, Epoch.J2000); 091 cc.setConverter(starlink, EQUATORIAL, Epoch.J2000, ECLIPTIC, Epoch.J2000); 092 cc.setConverter(starlink, ECLIPTIC, Epoch.J2000, GALACTIC, Epoch.J2000); 093 cc.setConverter(starlink, ECLIPTIC, Epoch.J2000, EQUATORIAL, Epoch.J2000); 094 cc.setConverter(starlink, GALACTIC, Epoch.J2000, EQUATORIAL, Epoch.J2000); 095 cc.setConverter(starlink, GALACTIC, Epoch.J2000, ECLIPTIC, Epoch.J2000); 096 097 return cc; 098 } 099 100 //============================================================================ 101 // PERFORMING CONVERSIONS 102 //============================================================================ 103 104 public SkyPosition createFrom(SkyPosition position, 105 CelestialCoordinateSystem toSystem, 106 Epoch toEpoch, 107 EarthPosition observer, 108 LocalSiderealTime lst) 109 throws CoordinateConversionException 110 { 111 SkyPosition convertedPosition = null; 112 113 CcsEpoch to = new CcsEpoch(toSystem, toEpoch); 114 CcsEpoch from = new CcsEpoch(position.getCoordinateSystem(), 115 position.getEpoch()); 116 117 CelestialCoordinateConverter directConverter = 118 converters.get(createKey(from, to)); 119 120 //This is the most typical case 121 if (directConverter != null) 122 { 123 convertedPosition = directConverter.createFrom(position, toSystem, 124 toEpoch, observer, lst); 125 } 126 //Special logic for self-conversions. These normally just update 127 //based on time. Note: we do NOT handle az/el -> az/el here. 128 else if (from.equals(to) && !from.system.equals(HORIZONTAL)) 129 { 130 convertedPosition = selfConversion(position, observer, lst); 131 } 132 //No direct converter and not a self conversion, unless it's az/el -> az/el 133 else 134 { 135 CcsEpochChain conversionChain = findChainFor(from, to); 136 137 if (conversionChain != null) 138 { 139 convertedPosition = executeChain(conversionChain, position, 140 observer, lst); 141 } 142 else 143 { 144 StringBuilder errMsg = new StringBuilder(); 145 errMsg.append("No converter found for position (ccs="); 146 errMsg.append(position.getCoordinateSystem()).append(",epoch="); 147 errMsg.append(position.getEpoch()).append(") to "); 148 errMsg.append(toSystem).append(',').append(toEpoch).append('.'); 149 throw new CoordinateConversionException(errMsg.toString(), 150 new IllegalArgumentException(errMsg.toString())); 151 } 152 } 153 154 return convertedPosition; 155 } 156 157 /** 158 * Handles conversions where the from/to CCS and Epoch are same, 159 * unless system is az/el. 160 */ 161 private SkyPosition selfConversion(SkyPosition position, 162 EarthPosition observer, 163 LocalSiderealTime lst) 164 { 165 if (position.getCoordinateSystem().equals(HORIZONTAL)) 166 throw new IllegalArgumentException("PROGRAMMER ERROR: " + 167 "This method should not have been used to convert AZ/EL to AZ/EL."); 168 169 return SimpleSkyPosition.copy(position, lst.toDate()); 170 } 171 172 /** Executes all the conversions in the chain. */ 173 public SkyPosition executeChain(CcsEpochChain chain, 174 SkyPosition position, 175 EarthPosition observer, 176 LocalSiderealTime lst) 177 throws CoordinateConversionException 178 { 179 SkyPosition result = position; 180 181 int linkCount = chain.links.size(); 182 183 //Do a conversion using consecutive links in the chain. 184 for (int i=1; i < linkCount; i++) 185 { 186 CcsEpoch from = chain.links.get(i-1); 187 CcsEpoch to = chain.links.get(i); 188 189 CelestialCoordinateConverter converter = converters.get(createKey(from, 190 to)); 191 result = converter.createFrom(result, to.system, to.epoch, observer, lst); 192 } 193 194 return result; 195 } 196 197 /** Returns the execution chain to use, if any, for from->to. */ 198 private CcsEpochChain findChainFor(CcsEpoch from, CcsEpoch to) 199 { 200 CcsEpochChain result = null; 201 int size = Integer.MAX_VALUE; 202 203 //Find smallest chain that starts w/ from & ends w/ to 204 for (CcsEpochChain chain : converterChains) 205 { 206 if (chain.startsWith(from) && chain.endsWith(to)) 207 { 208 if (chain.links.size() < size) 209 { 210 result = chain; 211 size = chain.links.size(); 212 } 213 } 214 } 215 216 //Do some just-in-time clean up 217 removeChains(from, to, size); 218 219 return result; 220 } 221 222 //============================================================================ 223 // ADDING NEW CONVERTERS AND FORMING CHAINS 224 //============================================================================ 225 226 /** 227 * Sets the converter to use for the given conversion. 228 * Adding a new conversion pathway may open up other pathways. 229 * For example, if this converter already handles A->B conversions 230 * and this method adds a B->C conversion, it will also construct 231 * an A->B->C conversion pathway, unless a direct A->C pathway 232 * already exists. 233 * 234 * @param converter 235 * the converter to use for the given from/to system. 236 * @param fromSystem 237 * the celestial coordinate system that the given converter 238 * can take as input. 239 * @param fromEpoch 240 * the epoch that the given converter can take as input. 241 * This is usually important only for the Equatorial coordinate system. 242 * @param toSystem 243 * the celestial coordinate system that the given converter 244 * can produce as output. 245 * @param toEpoch 246 * the epoch that the given converter can produce as output. 247 * This is usually important only for the Equatorial coordinate system. 248 */ 249 public void setConverter(CelestialCoordinateConverter converter, 250 CelestialCoordinateSystem fromSystem, Epoch fromEpoch, 251 CelestialCoordinateSystem toSystem, Epoch toEpoch) 252 { 253 if (converter == this) 254 throw new IllegalArgumentException("Cannot add CompositeConverter to itself."); 255 256 CcsEpoch from = new CcsEpoch(fromSystem, fromEpoch); 257 CcsEpoch to = new CcsEpoch(toSystem, toEpoch); 258 259 String newKey = createKey(from, to); 260 261 //If we already have the from/to combo, go ahead and add to the converters 262 //map, because the converter itself may be new. However, do NOT go 263 //through the process of creating new chains, because we've already 264 //been down that road for this combo. 265 boolean newCombo = !converters.containsKey(newKey); 266 267 converters.put(newKey, converter); //Add the direct path BEFORE chains 268 269 if (newCombo) 270 addToChains(from, to); 271 272 //It's possible that we had had indirect chains for this from/to combo 273 //that we now no longer need. While it might be harmless to keep those 274 //chains, we instead tidy up a bit. 275 removeChains(from, to, 2); 276 } 277 278 /** Gets rid of chains that start w/ from & end w/ to. */ 279 private void removeChains(CcsEpoch from, CcsEpoch to, int linkCountMin) 280 { 281 //Gather the unwanted chains here so we don't disturb the loop control 282 List<CcsEpochChain> unwantedChains = new ArrayList<CcsEpochChain>(); 283 284 for (CcsEpochChain oldChain : converterChains) 285 { 286 if (oldChain.links.size() > linkCountMin && 287 oldChain.startsWith(from) && oldChain.endsWith(to)) 288 unwantedChains.add(oldChain); 289 } 290 291 converterChains.removeAll(unwantedChains); 292 } 293 294 /** 295 * Tries to create new conversion pathways from the population of 296 * current pathways and the new direct from->to conversion. 297 */ 298 private void addToChains(CcsEpoch from, CcsEpoch to) 299 { 300 //Add the new direct pair. This needed so that other chains 301 //may use this as a staring point. 302 converterChains.add(new CcsEpochChain(from, to)); 303 304 //Gather the new chains here so we don't disturb the loop control 305 List<CcsEpochChain> newChains = new ArrayList<CcsEpochChain>(); 306 307 //Don't make a chain if from and to are same system 308 if (!from.equals(to)) 309 { 310 for (CcsEpochChain oldChain : converterChains) 311 { 312 //It doesn't make sense for us to create new chains w/ the from->to 313 //pair inserted into the middle. Why not? Because we would then have 314 //a new, longer, chain w/ the exact same start and end points, and doing 315 //extra conversions is wasteful. Thus we look to create new chains only 316 //by appending or prepending. 317 318 //Make new chain by prepending "from" to the beginning 319 if (okToStartWith(from, to, oldChain)) 320 { 321 CcsEpochChain newChain = oldChain.clone(); 322 newChain.links.add(0, from); 323 newChains.add(newChain); 324 } 325 326 //Make new chain by appending "to" to the end 327 if (okToEndWith(from, to, oldChain)) 328 { 329 CcsEpochChain newChain = oldChain.clone(); 330 newChain.links.add(to); 331 newChains.add(newChain); 332 } 333 } 334 } 335 336 //Add the new indirect chains 337 for (CcsEpochChain newChain : newChains) 338 { 339 if (!converterChains.contains(newChain)) 340 converterChains.add(newChain); 341 } 342 } 343 344 /** 345 * Returns true if we can form a new chain from oldChain and have it start 346 * with from->to. 347 */ 348 private boolean okToStartWith(CcsEpoch from, CcsEpoch to, CcsEpochChain oldChain) 349 { 350 //Quick exit on no match for prepending 351 if (!oldChain.startsWith(to)) 352 return false; 353 354 int chainLength = oldChain.links.size(); 355 CcsEpoch lastLink = oldChain.links.get(chainLength-1); 356 357 //Do not make new chain if we already have a direct conversion from->to 358 if (converters.containsKey(createKey(from, lastLink))) 359 return false; 360 361 //Special logic to allow HORZ->HORZ conversions. We roll the dice a little 362 //and assume we'll be able to do this w/ a 3-link chain. 363 if (from.system.equals(HORIZONTAL) && lastLink.system.equals(HORIZONTAL) && 364 oldChain.links.size() == 2) 365 return true; 366 367 //In general, do not allow self conversions -- 368 //in fact, do not allow chain if the "from" element is 369 //anywhere in the chain. 370 if (oldChain.links.contains(from)) 371 return false; 372 373 return true; 374 } 375 376 /** 377 * Returns true if we can form a new chain from oldChain and have it end 378 * with from->to. 379 */ 380 private boolean okToEndWith(CcsEpoch from, CcsEpoch to, CcsEpochChain oldChain) 381 { 382 //Quick exit on no match for prepending 383 if (!oldChain.endsWith(from)) 384 return false; 385 386 CcsEpoch firstLink = oldChain.links.get(0); 387 388 //Do not make new chain if we already have a direct conversion from->to 389 if (converters.containsKey(createKey(firstLink, to))) 390 return false; 391 392 //Special logic to allow HORZ->HORZ conversions. We roll the dice a little 393 //and assume we'll be able to do this w/ a 3-link chain. 394 if (firstLink.system.equals(HORIZONTAL) && to.system.equals(HORIZONTAL) && 395 oldChain.links.size() == 2) 396 return true; 397 398 //In general, do not allow self conversions -- 399 //in fact, do not allow chain if the "to" element is 400 //anywhere in the chain. 401 if (oldChain.links.contains(to)) 402 return false; 403 404 return true; 405 } 406 407 /** Used for keeping map of converters. */ 408 private String createKey(CcsEpoch from, CcsEpoch to) 409 { 410 return from.toString() + FROM_TO_SEP + to.toString(); 411 } 412 413 //============================================================================ 414 // PRIVATE HELPER CLASSES 415 //============================================================================ 416 417 /** Marries a CelestialCoordinateSystem and an Epoch. */ 418 private class CcsEpoch 419 { 420 CelestialCoordinateSystem system; 421 Epoch epoch; 422 423 CcsEpoch(CelestialCoordinateSystem ccs, Epoch e) 424 { 425 system = ccs; 426 epoch = e; 427 } 428 429 public String toString() 430 { 431 String text = system.name(); 432 433 if (system.equals(EQUATORIAL)) 434 text = text + "." + epoch.name(); 435 436 return text; 437 } 438 439 public boolean equals(Object o) 440 { 441 CcsEpoch other = (CcsEpoch)o; 442 return other.system.equals(this.system) && 443 other.epoch.equals(this.epoch); 444 } 445 } 446 447 448 private class CcsEpochChain implements Cloneable 449 { 450 List<CcsEpoch> links = new ArrayList<CcsEpoch>(); 451 452 CcsEpochChain() { } 453 454 CcsEpochChain(CcsEpoch link0, CcsEpoch link1) 455 { 456 links.add(link0); 457 links.add(link1); 458 } 459 460 boolean startsWith(CcsEpoch link) 461 { 462 return links.get(0).equals(link); 463 } 464 465 boolean endsWith(CcsEpoch link) 466 { 467 return links.get(links.size()-1).equals(link); 468 } 469 470 public boolean equals(Object o) 471 { 472 return ((CcsEpochChain)o).links.equals(this.links); 473 } 474 475 public CcsEpochChain clone() 476 { 477 CcsEpochChain clone = null; 478 479 try 480 { 481 clone = (CcsEpochChain)super.clone(); 482 //We don't need to clone the elements 483 clone.links = new ArrayList<CcsEpoch>(links); 484 } 485 catch (Exception ex) 486 { 487 throw new RuntimeException(ex); 488 } 489 490 return clone; 491 } 492 493 public String toString() { return links.toString(); } 494 } 495 496 //============================================================================ 497 // 498 //============================================================================ 499 /* 500 public static void main(String... args) throws Exception 501 { 502 CompositeConverter cc = CompositeConverter.getDefaultConverter(); 503 504 LocalSiderealTime lst = new LocalSiderealTime(); 505 EarthPosition observer = new EarthPosition(lst.getLocation(), 506 Latitude.parse("34d 04' 43.497\""), 507 lst.getTimeZone()); 508 509 SkyPosition eqPos = new SimpleSkyPosition(EQUATORIAL, Epoch.J2000); 510 ((SimpleSkyPosition)eqPos).setLatitude(new Latitude(50.0)); 511 ((SimpleSkyPosition)eqPos).setLongitude(new Longitude(60.0)); 512 displayPosition(eqPos); 513 514 SkyPosition glPos = cc.createFrom(eqPos, GALACTIC, Epoch.J2000, observer, lst); 515 displayPosition(glPos); 516 517 SkyPosition hzPos = cc.createFrom(eqPos, HORIZONTAL, Epoch.J2000, observer, lst); 518 displayPosition(hzPos); 519 520 SkyPosition hzPos2 = cc.createFrom(glPos, HORIZONTAL, Epoch.J2000, observer, lst); 521 displayPosition(hzPos2); 522 523 SkyPosition eqPos2 = cc.createFrom(hzPos, EQUATORIAL, Epoch.J2000, observer, lst); 524 displayPosition(eqPos2); 525 526 SkyPosition eqPos3 = cc.createFrom(glPos, EQUATORIAL, Epoch.J2000, observer, lst); 527 displayPosition(eqPos3); 528 529 SkyPosition eqPos4 = cc.createFrom(hzPos2, EQUATORIAL, Epoch.J2000, observer, lst); 530 displayPosition(eqPos4); 531 } 532 private static void displayPosition(SkyPosition pos) 533 { 534 System.out.print(pos.getCoordinateSystem()); 535 if (pos.getCoordinateSystem().equals(EQUATORIAL)) 536 System.out.print("-"+pos.getEpoch()); 537 System.out.print(", "); 538 System.out.print(pos.getLongitude().toStringHms()); 539 System.out.print(", "); 540 System.out.print(pos.getLatitude().toStringDms()); 541 System.out.println(); 542 } 543 */ 544 /* 545 public static void main(String... args) throws Exception 546 { 547 CompositeConverter cc = new CompositeConverter(); 548 549 HeasarcCoordConverter heasarc = new HeasarcCoordConverter(); 550 EquatorialHorizontalConverter eqHorz = new EquatorialHorizontalConverter(); 551 StarlinkPalConverter starlink = new StarlinkPalConverter(); 552 553 cc.setConverter(eqHorz, EQUATORIAL, Epoch.J2000, HORIZONTAL, Epoch.J2000); 554 System.out.println(cc.converterChains); 555 cc.setConverter(eqHorz, HORIZONTAL, Epoch.J2000, EQUATORIAL, Epoch.J2000); 556 System.out.println(cc.converterChains); 557 558 cc.setConverter(heasarc, EQUATORIAL, Epoch.J2000, EQUATORIAL, Epoch.B1950); 559 System.out.println(cc.converterChains); 560 cc.setConverter(heasarc, EQUATORIAL, Epoch.B1950, EQUATORIAL, Epoch.J2000); 561 System.out.println(cc.converterChains); 562 563 cc.setConverter(starlink, EQUATORIAL, Epoch.J2000, GALACTIC, Epoch.J2000); 564 System.out.println(cc.converterChains); 565 cc.setConverter(starlink, EQUATORIAL, Epoch.J2000, ECLIPTIC, Epoch.J2000); 566 System.out.println(cc.converterChains); 567 cc.setConverter(starlink, ECLIPTIC, Epoch.J2000, GALACTIC, Epoch.J2000); 568 System.out.println(cc.converterChains); 569 cc.setConverter(starlink, ECLIPTIC, Epoch.J2000, EQUATORIAL, Epoch.J2000); 570 System.out.println(cc.converterChains); 571 cc.setConverter(starlink, GALACTIC, Epoch.J2000, EQUATORIAL, Epoch.J2000); 572 System.out.println(cc.converterChains); 573 cc.setConverter(starlink, GALACTIC, Epoch.J2000, ECLIPTIC, Epoch.J2000); 574 System.out.println(cc.converterChains); 575 576 cc.findChainFor(cc.new CcsEpoch(HORIZONTAL, Epoch.J2000), 577 cc.new CcsEpoch(ECLIPTIC, Epoch.J2000)); 578 System.out.println("\n"+cc.converterChains); 579 } 580 */ 581 }