001 package edu.nrao.sss.model.source.experiment; 002 003 import java.awt.Color; 004 import java.awt.Dimension; 005 import java.awt.FontMetrics; 006 import java.awt.Graphics2D; 007 import java.awt.Rectangle; 008 import java.awt.geom.AffineTransform; 009 import java.awt.geom.Ellipse2D; 010 import java.awt.geom.Line2D; 011 import java.awt.geom.NoninvertibleTransformException; 012 import java.awt.geom.Point2D; 013 import java.awt.geom.Rectangle2D; 014 import java.awt.image.BufferedImage; 015 import java.awt.image.RenderedImage; 016 import java.math.BigDecimal; 017 import java.util.ArrayList; 018 import java.util.Collection; 019 import java.util.HashMap; 020 import java.util.List; 021 import java.util.Map; 022 import java.util.Random; 023 024 import edu.nrao.sss.astronomy.SimpleSkyPosition; 025 import edu.nrao.sss.astronomy.SkyPosition; 026 import edu.nrao.sss.astronomy.SkyPositionFilter; 027 import edu.nrao.sss.geom.AzimuthalEquidistantProjector; 028 import edu.nrao.sss.geom.Circle; 029 import edu.nrao.sss.geom.SphericalPosition; 030 import edu.nrao.sss.measure.Angle; 031 import edu.nrao.sss.measure.ArcUnits; 032 import edu.nrao.sss.measure.Latitude; 033 import edu.nrao.sss.measure.Longitude; 034 import edu.nrao.sss.model.source.FileBasedBrightness; 035 import edu.nrao.sss.model.source.Source; 036 import edu.nrao.sss.model.source.SourceBrightness; 037 import edu.nrao.sss.model.source.SourceFilter; 038 import edu.nrao.sss.model.source.SourceImageLink; 039 import edu.nrao.sss.model.source.SourceVelocity; 040 import edu.nrao.sss.model.source.Subsource; 041 import edu.nrao.sss.util.StringUtil; 042 043 /** 044 * A map of the celestial sphere. 045 * <p> 046 * <b>Version Info:</b> 047 * <table style="margin-left:2em"> 048 * <tr><td>$Revision: 1711 $</td></tr> 049 * <tr><td>$Date: 2008-11-14 13:00:45 -0700 (Fri, 14 Nov 2008) $</td></tr> 050 * <tr><td>$Author: dharland $ (last person to modify)</td></tr> 051 * </table></p> 052 * 053 * @author David M. Harland 054 * @since 2007-07-30 055 */ 056 public class SourceMap 057 { 058 /** Creates a new instance. */ 059 public SourceMap() 060 { 061 initializeColors(); 062 063 height = DEFAULT_HEIGHT; 064 width = DEFAULT_WIDTH; 065 066 allSources = new HashMap<String, Collection<SourceWrapper>>(); 067 visibleSources = new ArrayList<SourceWrapper>(); 068 sourceCircles = new HashMap<SourceWrapper, Circle>(); 069 sourceFilter = new SourceFilter(); 070 positionFilter = new SkyPositionFilter(); 071 sourceListChanged = false; 072 073 sourceFilter.setPositionFilter(positionFilter); 074 075 initRaDecBins(); 076 077 centerPosition = new SimpleSkyPosition(); 078 centerMoved = true; 079 skyRadius = new Angle("5.0", ArcUnits.DEGREE); //TODO: Replace this magic # 080 radiusGrew = true; 081 082 projector = new AzimuthalEquidistantProjector(); 083 skyToPixel = new AffineTransform(); 084 circleSpacingInSkyUnits = calcSkyCircleSpacing(skyRadius.getValue().doubleValue()); 085 086 updatePixelSizeAndLocation(); 087 updateCenterRadiusVariables(); 088 } 089 090 //============================================================================ 091 // SECTION: COLORS & FONTS 092 //============================================================================ 093 094 private static final Color FILL_COLOR = new Color(40, 40, 80); 095 private static final Color SPACE_DISK_COLOR = Color.BLACK; 096 private static final Color OUTER_CIRCLE_COLOR = Color.GREEN; 097 private static final Color INNER_CIRCLES_COLOR = new Color(255, 255, 0, 128); 098 private static final Color LAT_LON_COLOR = Color.RED; 099 //private static final Color SOURCE_COLOR = Color.WHITE; 100 101 private static final int DEFAULT_HEIGHT = 500; //pixels 102 private static final int DEFAULT_WIDTH = 500; //pixels 103 104 private Color colorFill; 105 private Color colorSpaceDisk; 106 private Color colorOuterCircle; 107 private Color colorInnerCircles; 108 private Color colorLatLon; 109 110 private Map<String, Color> sourceColors; 111 112 float fontSize; 113 114 /** Sets colors to default values. */ 115 private void initializeColors() 116 { 117 colorFill = FILL_COLOR; 118 colorSpaceDisk = SPACE_DISK_COLOR; 119 colorOuterCircle = OUTER_CIRCLE_COLOR; 120 colorInnerCircles = INNER_CIRCLES_COLOR; 121 colorLatLon = LAT_LON_COLOR; 122 123 sourceColors = new HashMap<String, Color>(); 124 125 int alpha = 255; 126 sourceColors.put("GBT", new Color(180,255,180,alpha)); 127 sourceColors.put("VLA", new Color(255,150,255,alpha)); 128 sourceColors.put("VLBA", new Color(255,255,150,alpha)); 129 } 130 131 //TODO clients should be able to manipulate all the colors 132 133 /** Should be called whenever map size changes. */ 134 private void updateFontSize() 135 { 136 //TODO: Replace magic #s. 137 138 fontSize = Math.max(8.0f, Math.min(16.0f, (float)pixelDimMin / (500.0f/12.0f))); 139 140 if (pixelDimMin < 150) 141 fontSize = 0.0f; 142 } 143 144 /** 145 * @param collectionName 146 * @param color 147 */ 148 public void setColorSourceCollection(String collectionName, Color color) 149 { 150 String name = collectionName.toUpperCase(); 151 152 sourceColors.put(name, color); 153 } 154 155 //============================================================================ 156 // SECTION: SIZE 157 //============================================================================ 158 159 private int height; 160 private int width; 161 162 public int getWidth() 163 { 164 return width; 165 } 166 167 public int getHeight() 168 { 169 return height; 170 } 171 172 public void setSize(Dimension size) 173 { 174 setSize(size.width, size.height); 175 } 176 177 public void setSize(int width, int height) 178 { 179 if (this.width != width || this.height != height) 180 { 181 this.width = width; 182 this.height = height; 183 184 updatePixelSizeAndLocation(); 185 calculateVisibleSourceLocationAndSize(); 186 } 187 } 188 189 /** Updates the variables that deal with pixel dimensions and center. */ 190 private void updatePixelSizeAndLocation() 191 { 192 //TODO: Replace magic #s. 193 194 pixelDimMin = Math.min(width, height); 195 pixelBorder = pixelDimMin / 30; 196 pixelRadiusMax = pixelDimMin / 2.0 - pixelBorder; 197 pixelCenter = pixelRadiusMax + pixelBorder; 198 199 updateFontSize(); 200 201 updatePixelToSkyUnits(); 202 } 203 204 //============================================================================ 205 // SECTION: SOLAR SYSTEM 206 //============================================================================ 207 208 //============================================================================ 209 // SECTION: SOURCES 210 //============================================================================ 211 212 private Map<String, Collection<SourceWrapper>> allSources; 213 private Collection<SourceWrapper> visibleSources; 214 215 private Map<SourceWrapper, Circle> sourceCircles; 216 private Map<SourceWrapper, Color> visibleSourceColor; 217 218 private boolean sourceListChanged; 219 220 //The purpose of this variable is to speed up updates of the map 221 //when moving its center or increasing its radius. By keeping the 222 //sources in RA & Dec bins, we can present fewer of them to the 223 //source filter. 224 private List<List<List<SourceWrapper>>> raDecBins; 225 226 private SourceFilter sourceFilter; 227 private SkyPositionFilter positionFilter; 228 229 public void addSource(Source newSource) 230 { 231 addSource(newSource, "Anonymous"); 232 } 233 234 /** 235 * @param newSources 236 */ 237 public void addSources(Collection<? extends Source> newSources) 238 { 239 addSources(newSources, "Anonymous"); 240 } 241 242 /** 243 * @param newSource 244 * @param collectionName 245 */ 246 public void addSource(Source newSource, String collectionName) 247 { 248 String name = collectionName.toUpperCase(); 249 250 //Get the collection of sources stored under the given name 251 Collection<SourceWrapper> namedCollection = getSourceCollection(name); 252 253 //Add the new source to our collection, and to the RA/Dec bins 254 SourceWrapper wrapper = new SourceWrapper(newSource); 255 namedCollection.add(wrapper); 256 addToBins(wrapper); 257 258 sourceListChanged = true; 259 } 260 261 /** 262 * @param newSources 263 * @param collectionName 264 */ 265 public void addSources(Collection<? extends Source> newSources, 266 String collectionName) 267 { 268 String name = collectionName.toUpperCase(); 269 270 //TODO: Think about using a separate thread for this method -- 271 // or using one only if size > N. 272 273 //Get the collection of sources stored under the given name 274 Collection<SourceWrapper> namedCollection = getSourceCollection(name); 275 276 //Add the new sources to our collection, and to the RA/Dec bins 277 for (Source newSource : newSources) 278 { 279 SourceWrapper wrapper = new SourceWrapper(newSource); 280 namedCollection.add(wrapper); 281 addToBins(wrapper); 282 } 283 284 sourceListChanged = true; 285 } 286 287 //public void mapSources(String sourceCollectionName, boolean show) 288 289 //public void removeSources(String collectionName) 290 291 /** 292 * Returns a collection of sources for the given name. 293 * If this map has no such collection, one is created, stored, and returned. 294 */ 295 private Collection<SourceWrapper> getSourceCollection(String name) 296 { 297 name = name.toUpperCase(); 298 299 Collection<SourceWrapper> namedCollection = allSources.get(name); 300 301 if (namedCollection == null) 302 { 303 namedCollection = new ArrayList<SourceWrapper>(); 304 allSources.put(name, namedCollection); 305 } 306 307 return namedCollection; 308 } 309 310 /** Adds a new source to the RA/Dec cache of this map. */ 311 private void addToBins(SourceWrapper newSource) 312 { 313 //TODO: coord conversion 314 315 SkyPosition srcPos = newSource.source.getPosition(); 316 317 double raDeg = srcPos.getLongitude().toUnits(ArcUnits.DEGREE).doubleValue(); 318 double decDeg = srcPos.getLatitude().toUnits(ArcUnits.DEGREE).doubleValue(); 319 320 int raIndex = getSourceBinIndexRa(raDeg); 321 int decIndex = getSourceBinIndexDec(decDeg); 322 323 raDecBins.get(decIndex).get(raIndex).add(newSource); 324 } 325 326 /** Clears the RA/Dec cache of this map. 327 328 not yet used 329 330 private void clearBins() 331 { 332 for (List<List<SourceWrapper>> decList : raDecBins) 333 for (List<SourceWrapper> raList : decList) 334 raList.clear(); 335 }*/ 336 337 /** 338 * Updates this map's source filter and applies it to its sources in order 339 * to decide which sources are visibile on the map. This method is called 340 * whenever sources are added or removed, when the center of the map 341 * is moved, or when the radial size of the map is changed. 342 * 343 * The logic is a little more complex than it need be for merely having 344 * it work; the extra complexity exists to speed up the screen updates 345 * when dealing with thousands of sources. 346 */ 347 private void updateAndApplySourceFilter() 348 { 349 //System.out.print("Updating viewable sources from list of "); 350 //long ms = System.currentTimeMillis(); //TODO TEMP 351 352 //Update position filter 353 //(We're intentionally NOT setting the filter's cone.) 354 355 ArcUnits units = skyRadius.getUnits(); 356 357 //Latitude range 358 Latitude latFrom = new Latitude(new BigDecimal(skyLatMin), units); 359 Latitude latTo = new Latitude(new BigDecimal(skyLatMax), units); 360 361 positionFilter.setLatitudeRange(latFrom, latTo); 362 363 //Longitude range 364 Longitude lonFrom = new Longitude(new BigDecimal(skyLonMin), units); 365 Longitude lonTo = new Longitude(new BigDecimal(skyLonMax), units); 366 367 if (poleInView) 368 lonTo.setToFullCircle(); 369 370 positionFilter.setLongitudeRange(lonFrom, lonTo); 371 372 //Quickly trim list of candidates on which to filter 373 ArrayList<SourceWrapper> candidates = new ArrayList<SourceWrapper>(); 374 375 //Take a subset of allSources 376 if (centerMoved || radiusGrew || sourceListChanged) 377 { 378 int latIndexMin = getSourceBinIndexDec(latFrom.toUnits(ArcUnits.DEGREE).doubleValue()); 379 int latIndexMax = getSourceBinIndexDec(latTo.toUnits(ArcUnits.DEGREE).doubleValue()); 380 381 int lonIndexMin1 = getSourceBinIndexRa(lonFrom.toUnits(ArcUnits.DEGREE).doubleValue()); 382 int lonIndexMax1 = getSourceBinIndexRa(lonTo.toUnits(ArcUnits.DEGREE).doubleValue()); 383 384 final int MAX_LON_INDEX = 35; //360/10 - 1 385 lonIndexMax1 = Math.min(lonIndexMax1, MAX_LON_INDEX); 386 387 int lonIndexMin2 = -1; 388 int lonIndexMax2 = -2; 389 390 //See if lon range goes through zero point 391 if (lonIndexMax1 < lonIndexMin1) 392 { 393 lonIndexMin2 = lonIndexMin1; 394 lonIndexMax2 = MAX_LON_INDEX; 395 396 lonIndexMin1 = 0; 397 //lonIndexMax1 = lonIndexMax1; 398 } 399 400 //Trim list of candidates to those that are in the min/max lon/lat box 401 for (int latIndex=latIndexMin; latIndex <= latIndexMax; latIndex++) 402 { 403 List<List<SourceWrapper>> sourceLists = raDecBins.get(latIndex); 404 405 for (int lonIndex=lonIndexMin1; lonIndex <= lonIndexMax1; lonIndex++) 406 candidates.addAll(sourceLists.get(lonIndex)); 407 408 for (int lonIndex=lonIndexMin2; lonIndex <= lonIndexMax2; lonIndex++) 409 candidates.addAll(sourceLists.get(lonIndex)); 410 } 411 } 412 //Look only at currently displayed sources. 413 //(Use case: zooming-in on center.) 414 else 415 { 416 candidates.addAll(visibleSources); 417 } 418 419 //Apply to filter to candidate sources 420 visibleSources.clear(); 421 for (SourceWrapper candidate : candidates) 422 { 423 if (sourceFilter.allows(candidate.source)) 424 visibleSources.add(candidate); 425 } 426 427 calculateVisibleSourceLocationAndSize(); 428 updateVisibleSourceColors(); 429 430 centerMoved = false; 431 radiusGrew = false; 432 sourceListChanged = false; 433 434 //ms = System.currentTimeMillis() - ms; //TODO TEMP 435 //System.out.println(candidates.size() + " sources took " + ms/1000.0 + " seconds."); 436 } 437 438 /** 439 * 440 */ 441 private void calculateVisibleSourceLocationAndSize() 442 { 443 //TODO Calculate radius based on brightness 444 final double RADIUS = 6; 445 446 Map<SourceWrapper, Circle> oldMap = sourceCircles; 447 sourceCircles = new HashMap<SourceWrapper, Circle>(); 448 449 for (SourceWrapper vs : visibleSources) 450 { 451 if (false) //(oldMap.containsKey(vs)) 452 { 453 sourceCircles.put(vs, oldMap.get(vs)); 454 } 455 else 456 { 457 Point2D xySkyPoint = projector.getXyFor(vs.source.getPosition()); 458 Point2D xyPixelPoint = skyToPixel.transform(xySkyPoint, null); 459 460 sourceCircles.put(vs, new Circle(xyPixelPoint, RADIUS)); 461 } 462 } 463 } 464 465 /** 466 * 467 */ 468 private void updateVisibleSourceColors() 469 { 470 Map<SourceWrapper, Color> oldMap = visibleSourceColor; 471 visibleSourceColor = new HashMap<SourceWrapper, Color>(); 472 473 for (SourceWrapper vs : visibleSources) 474 { 475 if (oldMap.containsKey(vs)) 476 { 477 visibleSourceColor.put(vs, oldMap.get(vs)); 478 } 479 else 480 { 481 for (String collectionName : allSources.keySet()) 482 { 483 if (allSources.get(collectionName).contains(vs)) 484 { 485 Color color = sourceColors.get(collectionName); 486 487 if (color == null) 488 color = assignColorTo(collectionName); 489 490 visibleSourceColor.put(vs, color); 491 492 break; //inner collectionName loop 493 } 494 } 495 } 496 } 497 } 498 499 /** 500 * @param sourceCollectionName 501 * @return 502 */ 503 private Color assignColorTo(String sourceCollectionName) 504 { 505 String name = sourceCollectionName.toUpperCase(); 506 507 Random rand = new Random(); 508 509 int red = rand.nextInt(56) + 200; 510 int green = rand.nextInt(56) + 200; 511 int blue = 655 - red - green; 512 513 Color c = new Color(red, green, blue, 255); //TODO allow client to set alpha 514 515 sourceColors.put(name, c); 516 517 return c; 518 } 519 520 /** 521 * Prepares raDecBins variable for presorting of sources into 522 * RA/Dec blocks. This presorting helps the speed with which 523 * this map is updated as the user zooms out or moves the center. 524 */ 525 private void initRaDecBins() 526 { 527 raDecBins = new ArrayList<List<List<SourceWrapper>>>(); 528 529 for (int dec=-90; dec <= +90; dec+=10) 530 { 531 List<List<SourceWrapper>> decList = new ArrayList<List<SourceWrapper>>(); 532 533 for (int ra=0; ra < 360; ra+=10) 534 decList.add(new ArrayList<SourceWrapper>()); 535 536 raDecBins.add(decList); 537 } 538 } 539 540 /** Calculates an index into sourceBins for the given RA. */ 541 private int getSourceBinIndexRa(double raDegrees) 542 { 543 return (int)Math.floor(raDegrees / 10.0); 544 } 545 546 /** Calculates an index into sourceBins for the given Declination. */ 547 private int getSourceBinIndexDec(double decDegrees) 548 { 549 return (int)Math.floor((decDegrees + 90.0) / 10.0); 550 } 551 552 /** 553 * Allows our collections of sources to use reference, not value, 554 * equality. This makes it possible to put multiple instances 555 * of the "same" source into the map, as would be the case for 556 * catalogs that have overlap of their sources. It also makes 557 * it easy to remove one copy of the source when an entire 558 * catalog is removed. 559 */ 560 private class SourceWrapper 561 { 562 Source source; 563 564 SourceWrapper(Source s) { source = s; } 565 566 public boolean equals(Object other) { return other == this; } 567 } 568 569 //============================================================================ 570 // SECTION: MAP CENTER & RADIUS 571 //============================================================================ 572 //---------------------------------------------------------------------------- 573 // Direct Client Input 574 //---------------------------------------------------------------------------- 575 576 SimpleSkyPosition centerPosition; 577 Angle skyRadius; 578 579 //---------------------------------------------------------------------------- 580 // Variables Derived at the Time of Client Input 581 //---------------------------------------------------------------------------- 582 583 //TODO change from double to BigDecimal? 584 585 private double circleSpacingInSkyUnits; 586 587 //Maps points so that distances from center on screen show true relative dists 588 AzimuthalEquidistantProjector projector; 589 590 //The variables below are calculated once each time the center or radius 591 //changes for the convenience of several methods. 592 593 //A quarter circle in the units of skyRadius 594 double skyQuarterCircle; 595 596 //All in units of skyRadius 597 private double skyLatMin, skyLatCtr, skyLatMax; 598 private double skyLonMin, skyLonCtr, skyLonMax; 599 600 //True if the outermost circle includes a pole 601 private boolean poleInView; 602 603 private boolean centerMoved; 604 private boolean radiusGrew; 605 606 //---------------------------------------------------------------------------- 607 // Variables Updated Just-in-Time 608 //---------------------------------------------------------------------------- 609 610 //Transforms sky units to pixels 611 AffineTransform skyToPixel; 612 613 //Lesser of height and width 614 private int pixelDimMin; 615 616 //The distance between the circle representing the map's radius 617 //and the edge of the black outerspace circle. 618 private int pixelBorder; 619 620 //The radius, in pixels, of the outermost of the concentric circles. 621 //This circle represents the map's radius in sky units. 622 private double pixelRadiusMax; 623 624 //The center of the circle, in pixels, for x & y 625 double pixelCenter; 626 627 //The ratio of pixels to sky-units (degrees, radians, etc.) 628 private double pixelsPerSkyUnit; 629 630 //---------------------------------------------------------------------------- 631 // Methods 632 //---------------------------------------------------------------------------- 633 634 /** 635 * Sets {@code newPosition} as the center of this map. 636 * 637 * @param newPosition 638 * the new center of this map. 639 */ 640 public void setCenterPosition(SphericalPosition newPosition) 641 { 642 //Sometimes this class updates centerPosition and sends it here. 643 //No need copying from centerPostion into centerPosition. 644 if (newPosition != centerPosition) //intentional reference comparison 645 { 646 centerPosition.setLongitude(newPosition.getLongitude().clone()); 647 centerPosition.setLatitude(newPosition.getLatitude().clone()); 648 } 649 650 centerMoved = true; 651 652 updateCenterRadiusVariables(); 653 } 654 655 656 //TODO public void setCenterSource(Source centerSource) 657 658 /** 659 * Sets the radius, in angular sky units, of this map. 660 * 661 * @param newRadius 662 * the radius, in angular sky units, of this map. 663 */ 664 public void setSkyRadius(Angle newRadius) 665 { 666 radiusGrew = (newRadius.compareTo(skyRadius) > 0); 667 668 skyRadius = newRadius.clone(); 669 670 circleSpacingInSkyUnits = calcSkyCircleSpacing(skyRadius.getValue().doubleValue()); 671 672 updateCenterRadiusVariables(); 673 } 674 675 676 /** 677 * Updates internal variables that depend on centerPosition and skyRadius. 678 * 679 * Must be called when either the SKY RADIUS or the SKY CENTER is changed. 680 */ 681 private void updateCenterRadiusVariables() 682 { 683 updatePixelToSkyUnits(); 684 updateLatLonVars(); 685 updateXyTransformer(); 686 updateAndApplySourceFilter(); 687 } 688 689 690 /** 691 * Configures the affine transform of the mapping projector to return values 692 * that are scaled to the current sky radius based on this map's center 693 * position. 694 */ 695 private void updateXyTransformer() 696 { 697 //Reset the transformer for a new calc 698 projector.setCenter(centerPosition); 699 projector.getXyTransformer().setToIdentity(); 700 701 double skyRadiusValue = skyRadius.getValue().doubleValue(); 702 703 //Set scaling in sky units, ignoring pixels for now 704 Latitude refLat = centerPosition.getLatitude().clone(); 705 Latitude latLen = new Latitude(new BigDecimal(skyRadiusValue), skyRadius.getUnits()); 706 if (refLat.isNorthOfEquator()) 707 refLat.subtract(latLen); 708 else 709 refLat.add(latLen); 710 711 //We pick a point at the same longitude as the center, but at a latitude 712 //that is one radius, in sky units, north or south. We then send that 713 //reference position to the mapping projector and get its y coordinate, 714 //which is a distance from the center. By taking the ratio of the 715 //sky radius to the x-y radius, we get a multiplier that we can use 716 //for transforming the rest of the points. 717 SimpleSkyPosition refPos = new SimpleSkyPosition(); 718 719 refPos.setLongitude(centerPosition.getLongitude()); 720 refPos.setLatitude(refLat); 721 722 double skyScale = 723 skyRadiusValue / Math.abs(projector.getXyFor(refPos).getY()); 724 725 projector.getXyTransformer().setToScale(skyScale, skyScale); 726 727 //TODO: firePropertyChange("projection", null, null); ? 728 } 729 730 /** Updates convenience variables based on centerPosition & skyRadius. */ 731 private void updateLatLonVars() 732 { 733 ArcUnits units = skyRadius.getUnits(); 734 double radius = skyRadius.getValue().doubleValue(); 735 736 skyLatCtr = centerPosition.getLatitude().toUnits(units).doubleValue(); 737 skyLonCtr = centerPosition.getLongitude().toUnits(units).doubleValue(); 738 739 skyQuarterCircle = units.toQuarterCircle().doubleValue(); 740 741 skyLatMin = Math.max(-skyQuarterCircle, skyLatCtr - radius); 742 skyLatMax = Math.min(+skyQuarterCircle, skyLatCtr + radius); 743 744 poleInView = (skyLatMin <= -skyQuarterCircle || skyLatMax >= +skyQuarterCircle); 745 746 //If our outermost circle touches a pole, special logic 747 if (poleInView) 748 { 749 skyLonMin = 0.0; 750 skyLonMax = units.toFullCircle().doubleValue(); 751 } 752 else //not touching either pole 753 { 754 skyLatMin = skyLatCtr - radius; 755 skyLatMax = skyLatCtr + radius; 756 757 radius = radius / 758 Math.cos(centerPosition.getLatitude().toUnits(ArcUnits.RADIAN).doubleValue()); 759 760 skyLonMin = skyLonCtr - radius; 761 skyLonMax = skyLonCtr + radius; 762 } 763 } 764 765 /** Updates variables that deal with ratio of pixels to sky units. */ 766 private void updatePixelToSkyUnits() 767 { 768 pixelsPerSkyUnit = pixelRadiusMax / skyRadius.getValue().doubleValue(); 769 770 skyToPixel.setToTranslation(pixelCenter, pixelCenter); 771 skyToPixel.scale(pixelsPerSkyUnit, -pixelsPerSkyUnit); 772 } 773 774 //============================================================================ 775 // SECTION: PAINTING 776 //============================================================================ 777 778 /** 779 * Paints this map on the given graphics context. 780 * @param g 781 */ 782 public void paint(Graphics2D g) 783 { 784 //The list of visible sources is updated elsewhere when the center 785 //or radius of the map is changed. However, it is NOT updated when 786 //a new source is added, so we must update it here. 787 if (sourceListChanged) 788 updateAndApplySourceFilter(); 789 790 g.setFont(g.getFont().deriveFont(fontSize)); 791 g.setColor(colorFill); 792 g.fill(new Rectangle(width, height)); 793 794 paintConcentricCircles(g); 795 796 paintLatLonLines(g); 797 798 paintSources(g); 799 800 //Draw central "+" 801 g.setColor(colorOuterCircle); 802 803 int center = (int)(pixelRadiusMax + pixelBorder); 804 int halfWidth = 3; 805 806 g.drawLine(center - halfWidth, center, center + halfWidth, center); 807 g.drawLine(center, center - halfWidth, center, center + halfWidth); 808 } 809 810 /** 811 * Takes a snapshot of this map in its current state and returns it as an 812 * image. 813 * @return an image of the current state of this map. 814 */ 815 public RenderedImage takeSnapshot() 816 { 817 BufferedImage image = 818 new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 819 820 paint((Graphics2D)image.getGraphics()); 821 822 return image; 823 } 824 825 //---------------------------------------------------------------------------- 826 // Sources 827 //---------------------------------------------------------------------------- 828 829 /** Represents each visible source as a small circle. */ 830 private void paintSources(Graphics2D g) 831 { 832 for (SourceWrapper s : sourceCircles.keySet()) 833 { 834 g.setColor(visibleSourceColor.get(s)); 835 836 Circle c = sourceCircles.get(s); 837 838 g.fill(c); 839 840 Rectangle2D bounds = c.getBounds2D(); 841 842 //if (false) //TODO see if & when we want to use labels 843 g.drawString(s.source.getName(), Math.round(bounds.getMaxX()), 844 Math.round(bounds.getMinY())); 845 } 846 } 847 848 //---------------------------------------------------------------------------- 849 // Bullseye Circles 850 //---------------------------------------------------------------------------- 851 852 /** 853 * Creates a black "outerspace" circle and a series of concentric unfilled 854 * circles, the outermost of which represents the radius in sky-units 855 * requested by the client. 856 */ 857 private void paintConcentricCircles(Graphics2D g2) 858 { 859 //Draw & fill the black outerspace circle 860 //Ellipse2D circle = new Ellipse2D.Double(0, 0, pixelDimMin, pixelDimMin); 861 Ellipse2D circle = new Ellipse2D.Double(pixelBorder, pixelBorder, pixelDimMin-2*pixelBorder, pixelDimMin-2*pixelBorder); 862 g2.setColor(colorSpaceDisk); 863 g2.fill(circle); 864 865 //g2.setClip(circle.getBounds()); 866 g2.setClip(0, 0, pixelDimMin, pixelDimMin); 867 868 //Prepare to draw concentric circles 869 g2.setColor(colorInnerCircles); 870 871 BigDecimal circleSpacingInSkyUnits_BD = 872 BigDecimal.valueOf(circleSpacingInSkyUnits); 873 874 Angle mapAngle = new Angle(circleSpacingInSkyUnits_BD, skyRadius.getUnits()); 875 876 //Determines how many concentric circles are drawn 877 double pixelSpacing = circleSpacingInSkyUnits * pixelsPerSkyUnit; 878 879 double radiusPixels = pixelSpacing; 880 881 //Draw the inner concentric circles 882 while (radiusPixels < pixelRadiusMax) 883 { 884 paintCircle(g2, radiusPixels, 885 pixelRadiusMax + pixelBorder - radiusPixels, pixelCenter, 886 mapAngle); 887 888 radiusPixels += pixelSpacing; 889 mapAngle.add(circleSpacingInSkyUnits_BD); 890 } 891 892 //Draw the outermost circle 893 g2.setColor(colorOuterCircle); 894 paintCircle(g2, pixelRadiusMax, pixelBorder, pixelCenter, skyRadius); 895 } 896 897 /** 898 * Paints a circle. Double values are in pixels. Angle in sky units. 899 */ 900 private void paintCircle(Graphics2D g2, double radius, double border, 901 double center, Angle keyAngle) 902 { 903 double diameter = 2.0 * radius; 904 905 g2.draw(new Ellipse2D.Double(border, border, diameter, diameter)); 906 907 FontMetrics fm = g2.getFontMetrics(); 908 909 //Key text in map units 910 String keyText = keyAngle.toString(1,4); 911 double textWidth = fm.getStringBounds(keyText, g2).getWidth(); 912 int textX = (int) (center - textWidth / 2.0); 913 int textY = (int) (center - radius); 914 915 g2.drawString(keyText, textX, textY); 916 } 917 918 /** Tries to derive a "good" spacing for the concentric circles. */ 919 private double calcSkyCircleSpacing(double searchRadiusMax) 920 { 921 //Get a number between 10 & 100 922 double expMinusOne = Math.floor(Math.log10(searchRadiusMax)) - 1; 923 double tenToX = Math.pow(10.0, expMinusOne); 924 int x = (int) Math.round(searchRadiusMax / tenToX); 925 926 //Bring it down to between 2 & 20 927 int y = Math.round((float) x / 5.0f); 928 929 //Values: 1,2,3,4,5,10,15,20 930 int s = y; 931 932 if (s >= 17) s = 20; 933 else if (s >= 13) s = 15; 934 else if (s >= 8) s = 10; 935 else if (s > 6) s = 5; 936 //else leave as is 937 938 //Put back into original scale 939 return s * tenToX; 940 } 941 942 //---------------------------------------------------------------------------- 943 // Longitude & Latitude Lines 944 //---------------------------------------------------------------------------- 945 946 /** Creates dashed lines for latitude and longitude. */ 947 private void paintLatLonLines(Graphics2D g2) 948 { 949 g2.setColor(colorLatLon); 950 951 paintLatitudeLines(g2, 6); 952 paintLongitudeLines(g2, 6); 953 } 954 955 /** Creates dashed lines for latitude. */ 956 private void paintLatitudeLines(Graphics2D g2, int numLines) 957 { 958 ArcUnits units = skyRadius.getUnits(); 959 960 //Set up positions with latitudes from min to max 961 SimpleSkyPosition[] pos = new SimpleSkyPosition[numLines]; 962 double angle = skyLatMin; 963 double angleIncr = (skyLatMax - skyLatMin) / (double) (numLines - 1); 964 for (int p = 0; p < numLines; p++) 965 { 966 pos[p] = new SimpleSkyPosition(); 967 pos[p].getLatitude().set(BigDecimal.valueOf(angle), units); 968 angle += angleIncr; 969 } 970 971 //Draw the latitude lines by varying longitude 972 boolean writeLabels = false; // true; 973 double incr = (skyLonMax - skyLonMin) / 100.0; 974 for (double lon = skyLonMin; lon <= skyLonMax; lon += incr) 975 { 976 for (int p = 0; p < numLines; p++) 977 { 978 pos[p].getLongitude().set(BigDecimal.valueOf(lon), units); 979 Point2D xySkyPoint = projector.getXyFor(pos[p]); 980 Point2D xyPixelPoint = skyToPixel.transform(xySkyPoint, null); 981 g2.draw(new Line2D.Double(xyPixelPoint, xyPixelPoint)); 982 983 if (writeLabels) 984 g2.drawString(pos[p].getLatitude().toStringDms(), 985 (int)xyPixelPoint.getX(), 986 (int)xyPixelPoint.getY()); 987 } 988 writeLabels = false; 989 } 990 } 991 992 /** Creates dashed lines for longitude. */ 993 private void paintLongitudeLines(Graphics2D g2, int numLines) 994 { 995 ArcUnits units = skyRadius.getUnits(); 996 997 //Set up positions with longitudes from min to max 998 SimpleSkyPosition[] pos = new SimpleSkyPosition[numLines]; 999 double angle = skyLonMin; 1000 double angleIncr = (skyLonMax - skyLonMin) / (double) (numLines - 1); 1001 for (int p = 0; p < numLines; p++) 1002 { 1003 pos[p] = new SimpleSkyPosition(); 1004 pos[p].getLongitude().set(BigDecimal.valueOf(angle), units); 1005 angle += angleIncr; 1006 } 1007 1008 //Draw the longitude lines by varying latitude 1009 double incr = (skyLatMax - skyLatMin) / 100.0; 1010 for (double lat = skyLatMin; lat <= skyLatMax; lat += incr) 1011 { 1012 for (int p = 0; p < numLines; p++) 1013 { 1014 pos[p].getLatitude().set(BigDecimal.valueOf(lat), units); 1015 Point2D xySkyPoint = projector.getXyFor(pos[p]); 1016 Point2D xyPixelPoint = skyToPixel.transform(xySkyPoint, null); 1017 g2.draw(new Line2D.Double(xyPixelPoint, xyPixelPoint)); 1018 } 1019 } 1020 } 1021 1022 //============================================================================ 1023 // 1024 //============================================================================ 1025 1026 /** 1027 * Calculates and returns a position on the celestial sphere given an 1028 * x,y point on the display. 1029 * 1030 * @param x the x-coordinate of a pixel on the display. 1031 * @param y the y-coordinate of a pixel on the display. 1032 * @return a position on the celestial sphere. 1033 * @throws NoninvertibleTransformException if (x,y) cannot be converted to 1034 * a position on the celestial sphere. 1035 */ 1036 SphericalPosition calculatePositionFor(int x, int y) 1037 throws NoninvertibleTransformException 1038 { 1039 Point2D skyPos = skyToPixel.inverseTransform(new Point2D.Double(x,y), null); 1040 1041 return projector.getLatLonFor(skyPos); 1042 } 1043 1044 //============================================================================ 1045 // 1046 //============================================================================ 1047 1048 /** 1049 * This method probably does not belong here; just experimenting for now. 1050 */ 1051 public StringBuilder toHtmlImageMap(String mapName) 1052 { 1053 //The list of visible sources is updated elsewhere when the center 1054 //or radius of the map is changed. However, it is NOT updated when 1055 //a new source is added, so we must update it here. 1056 if (sourceListChanged) 1057 updateAndApplySourceFilter(); 1058 1059 final String jsMethod = "writeText"; 1060 final String EOL = StringUtil.EOL; 1061 1062 StringBuilder buff = new StringBuilder(); 1063 1064 buff.append("<map id=\"").append(mapName) 1065 .append("\" name=\"").append(mapName).append("\">") 1066 .append(EOL); 1067 1068 for (SourceWrapper s : sourceCircles.keySet()) 1069 { 1070 Circle c = sourceCircles.get(s); 1071 Point2D center = c.getCenter(); 1072 1073 buff.append(" <area shape=\"circle\" coords=\""); 1074 buff.append(Math.round(center.getX())).append(','); 1075 buff.append(Math.round(center.getY())).append(','); 1076 buff.append(Math.round(c.getRadius())).append('\"'); 1077 1078 buff.append(" nohref=\"true\"").append(EOL); 1079 1080 buff.append(" onMouseOver=\"").append(jsMethod).append("('"); 1081 fillSourceInfo(s, buff); 1082 buff.append("')\""); 1083 1084 //buff.append(EOL); 1085 //buff.append(" onMouseOut=\"").append(jsMethod).append("('')\""); 1086 1087 buff.append("/>").append(EOL); 1088 } 1089 1090 buff.append("</map>").append(EOL); 1091 1092 return buff; 1093 } 1094 1095 //Another method that doesn't belong; just an experiment 1096 private void fillSourceInfo(SourceWrapper sw, StringBuilder buff) 1097 { 1098 Source s = sw.source; 1099 1100 buff.append("<h2>").append(s.getName()).append("</h2>"); 1101 1102 if (s.getAliases().size() > 0) 1103 { 1104 buff.append("<i>Aliases:</i>"); 1105 1106 for (String alias : s.getAliases()) 1107 buff.append(' ').append(alias); 1108 1109 buff.append("<br/>"); 1110 } 1111 1112 SkyPosition pos = s.getPosition(); 1113 1114 String lonAbbr = pos.getCoordinateSystem().getAbbreviationForLongitude(); 1115 buff.append("<i>").append(lonAbbr).append(":</i> "); 1116 buff.append(pos.getLongitude().toStringHms(3,3)); 1117 buff.append("<br/>"); 1118 1119 String latAbbr = pos.getCoordinateSystem().getAbbreviationForLatitude(); 1120 buff.append("<i>").append(latAbbr).append(":</i> "); 1121 buff.append(pos.getLatitude().toStringDmsHtml(3,3)).append("<br/>"); 1122 buff.insert(buff.lastIndexOf("'"), '\\'); 1123 1124 buff.append("<i>Uncertainties (mas):</i>").append(" "); 1125 buff.append(lonAbbr).append(" "); 1126 buff.append(pos.getLongitudeUncertainty().toUnits(ArcUnits.MILLI_ARC_SECOND)); 1127 buff.append(", "); 1128 buff.append(latAbbr).append(" "); 1129 buff.append(pos.getLatitudeUncertainty().toUnits(ArcUnits.MILLI_ARC_SECOND)); 1130 buff.append("<br/>"); 1131 1132 buff.append("<i>Flux Densities:</i>").append("<br/>"); 1133 Subsource ss = s.getCentralSubsource(); 1134 List<SourceBrightness> sbs = ss.getBrightnesses(); 1135 boolean foundSb = false; 1136 for (SourceBrightness sb : sbs) 1137 { 1138 if (sb instanceof FileBasedBrightness) 1139 continue; 1140 1141 buff.append(" "); 1142 buff.append(sb.getTotalFluxDensity()); 1143 buff.append(" (").append(sb.getValidFrequency()).append(")<br/>"); 1144 1145 foundSb = true; 1146 } 1147 if (!foundSb) 1148 buff.append(" No Information<br/>"); 1149 1150 buff.append("<i>Velocities:</i>").append("<br/>"); 1151 List<SourceVelocity> svs = ss.getVelocities(); 1152 if (svs.size() == 0) 1153 { 1154 buff.append(" No Information<br/>"); 1155 } 1156 else 1157 { 1158 for (SourceVelocity sv : svs) 1159 { 1160 buff.append(" "); 1161 buff.append(sv.getRadialVelocity()); 1162 buff.append(" (").append(sv.getValidFrequency()).append(")<br/>"); 1163 } 1164 } 1165 1166 buff.append("<i>Images:</i>").append("<br/>"); 1167 List<SourceImageLink> images = s.getImageLinks(); 1168 if (images.size() == 0) 1169 { 1170 buff.append(" None<br/>"); 1171 } 1172 else 1173 { 1174 for (SourceImageLink image : images) 1175 { 1176 String anchor = image.toHtmlAnchor().replaceAll("\"", """); 1177 buff.append(" ").append(anchor).append("<br/>"); 1178 } 1179 } 1180 } 1181 }