001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2025 005 * Space Science and Engineering Center (SSEC) 006 * University of Wisconsin - Madison 007 * 1225 W. Dayton Street, Madison, WI 53706, USA 008 * https://www.ssec.wisc.edu/mcidas/ 009 * 010 * All Rights Reserved 011 * 012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and 013 * some McIDAS-V source code is based on IDV and VisAD source code. 014 * 015 * McIDAS-V is free software; you can redistribute it and/or modify 016 * it under the terms of the GNU Lesser Public License as published by 017 * the Free Software Foundation; either version 3 of the License, or 018 * (at your option) any later version. 019 * 020 * McIDAS-V is distributed in the hope that it will be useful, 021 * but WITHOUT ANY WARRANTY; without even the implied warranty of 022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 023 * GNU Lesser Public License for more details. 024 * 025 * You should have received a copy of the GNU Lesser Public License 026 * along with this program. If not, see https://www.gnu.org/licenses/. 027 */ 028 029package edu.wisc.ssec.mcidasv.control; 030 031import static edu.wisc.ssec.mcidasv.probes.ReadoutProbe.makeEarth2dTuple; 032import static java.util.Objects.requireNonNull; 033 034import java.awt.BorderLayout; 035import java.awt.Color; 036import java.awt.Component; 037import java.awt.Container; 038import java.awt.Dimension; 039import java.awt.FlowLayout; 040import java.awt.Graphics; 041import java.awt.GridBagConstraints; 042import java.awt.GridLayout; 043import java.awt.Insets; 044import java.awt.Rectangle; 045import java.awt.Window; 046import java.awt.event.ActionEvent; 047import java.awt.event.ActionListener; 048import java.awt.event.MouseEvent; 049import java.awt.geom.Rectangle2D; 050import java.io.FileWriter; 051import java.io.IOException; 052import java.rmi.RemoteException; 053import java.text.DecimalFormat; 054import java.util.ArrayList; 055import java.util.Collections; 056import java.util.Hashtable; 057import java.util.LinkedHashMap; 058import java.util.List; 059import java.util.Map; 060import java.util.Objects; 061import java.util.concurrent.CancellationException; 062import java.util.concurrent.ExecutionException; 063 064import javax.swing.*; 065import javax.swing.border.Border; 066import javax.swing.event.MouseInputListener; 067import javax.swing.plaf.basic.BasicTableUI; 068import javax.swing.table.AbstractTableModel; 069import javax.swing.table.TableCellEditor; 070import javax.swing.table.TableCellRenderer; 071 072import org.slf4j.Logger; 073import org.slf4j.LoggerFactory; 074 075import ucar.unidata.idv.ControlContext; 076import ucar.unidata.idv.IdvConstants; 077import ucar.unidata.idv.MapViewManager; 078import ucar.unidata.idv.control.FlaggedDisplayable; 079import ucar.unidata.idv.control.McVHistogramWrapper; 080import ucar.unidata.idv.ui.IdvWindow; 081import ucar.visad.display.XYDisplay; 082 083import visad.Data; 084import visad.DataReference; 085import visad.DataReferenceImpl; 086import visad.DisplayRealType; 087import visad.FlatField; 088import visad.Real; 089import visad.RealTuple; 090import visad.Unit; 091import visad.VisADException; 092import visad.georef.MapProjection; 093 094import ucar.unidata.data.DataChoice; 095import ucar.unidata.data.DataSelection; 096import ucar.unidata.idv.DisplayControl; 097import ucar.unidata.idv.DisplayConventions; 098import ucar.unidata.idv.ViewManager; 099import ucar.unidata.idv.control.ControlWidget; 100import ucar.unidata.idv.control.WrapperWidget; 101import ucar.unidata.idv.ui.ParamDefaultsEditor; 102import ucar.unidata.util.ColorTable; 103import ucar.unidata.util.FileManager; 104import ucar.unidata.util.GuiUtils; 105import ucar.unidata.util.LogUtil; 106import ucar.unidata.util.Misc; 107import ucar.unidata.util.Range; 108import ucar.visad.display.DisplayMaster; 109import ucar.visad.display.DisplayableData; 110 111import edu.wisc.ssec.mcidasv.Constants; 112import edu.wisc.ssec.mcidasv.McIDASV; 113import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralData; 114import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralDataSource; 115import edu.wisc.ssec.mcidasv.data.hydra.SpectrumAdapter; 116import edu.wisc.ssec.mcidasv.display.hydra.MultiSpectralDisplay; 117import edu.wisc.ssec.mcidasv.probes.ProbeEvent; 118import edu.wisc.ssec.mcidasv.probes.ProbeListener; 119import edu.wisc.ssec.mcidasv.probes.ReadoutProbe; 120import edu.wisc.ssec.mcidasv.ui.UIManager; 121 122public class MultiSpectralControl extends HydraControl { 123 124 private static final Logger logger = 125 LoggerFactory.getLogger(MultiSpectralControl.class); 126 127 private String PARAM = "BrightnessTemp"; 128 129 // So MultiSpectralDisplay can consistently update the wavelength label 130 // Note hacky leading spaces - needed because GUI builder does not 131 // accept a horizontal strut component. 132 public static String WAVENUMLABEL = " Wavelength: "; 133 134 private JLabel wavelengthLabel = new JLabel(); 135 136 private static final int DEFAULT_FLAGS = 137 FLAG_COLORTABLE | FLAG_ZPOSITION; 138 139 private MultiSpectralDisplay display; 140 141 private DisplayMaster displayMaster; 142 143 private final JTextField wavenumbox = 144 new JTextField(Float.toString(0f), 12); 145 146 final JTextField minBox = new JTextField(6); 147 final JTextField maxBox = new JTextField(6); 148 149 private final List<Hashtable<String, Object>> spectraProperties = new ArrayList<>(); 150 private final List<Spectrum> spectra = new ArrayList<>(); 151 152 private McVHistogramWrapper histoWrapper; 153 154 private float rangeMin; 155 private float rangeMax; 156 157 private float origRangeMin; 158 private float origRangeMax; 159 160 // REALLY not thrilled with this... 161 private int probesSeen = 0; 162 163 // boring UI stuff 164 private final JTable probeTable = new JTable(new ProbeTableModel(this, spectra)); 165 private final JScrollPane scrollPane = new JScrollPane(probeTable); 166 private final JButton addProbe = new JButton("Add Probe"); 167 private final JButton removeProbe = new JButton("Remove Probe"); 168 private JCheckBox use360Box; 169 170 private boolean blackBackground = true; 171 private JRadioButton bgBlack; 172 private JRadioButton bgWhite; 173 private ButtonGroup bgColorGroup; 174 175 /** Used to trigger the CSV export process. */ 176 private JButton saveAsCsv; 177 178 /** Dialog that allows users to see export progress as well as cancel current export. */ 179 private CsvDialog exportCsvDialog; 180 181 public MultiSpectralControl() { 182 super(); 183 setHelpUrl("idv.controls.hydra.multispectraldisplaycontrol"); 184 } 185 186 @Override public boolean init(final DataChoice choice) 187 throws VisADException, RemoteException 188 { 189 ((McIDASV)getIdv()).getMcvDataManager().setHydraControl(choice, this); 190 Hashtable props = choice.getProperties(); 191 PARAM = (String) props.get(MultiSpectralDataSource.paramKey); 192 193 List<DataChoice> choices = Collections.singletonList(choice); 194 histoWrapper = new McVHistogramWrapper("Histogram", choices, this); 195 196 Float fieldSelectorChannel = 197 (Float)getDataSelection().getProperty(Constants.PROP_CHAN); 198 199 display = new MultiSpectralDisplay(this); 200 201 if (fieldSelectorChannel != null) { 202 display.setWaveNumber(fieldSelectorChannel); 203 } 204 205 displayMaster = getViewManager().getMaster(); 206 207 // map the data choice to display. 208 ((McIDASV) getIdv()).getMcvDataManager().setHydraDisplay(choice, display); 209 210 // initialize the Displayable with data before adding to DisplayControl 211 DisplayableData imageDisplay = display.getImageDisplay(); 212 FlatField image = display.getImageData(); 213 214 float[] rngvals = (image.getFloats(false))[0]; 215 float[] minmax = minmax(rngvals); 216 rangeMin = minmax[0]; 217 rangeMax = minmax[1]; 218 219 origRangeMin = rangeMin; 220 origRangeMax = rangeMax; 221 222 imageDisplay.setData(display.getImageData()); 223 addDisplayable(imageDisplay, DEFAULT_FLAGS); 224 225 // put the multispectral display into the layer controls 226 addViewManager(display.getViewManager()); 227 228 // tell the idv what options to give the user 229 setAttributeFlags(DEFAULT_FLAGS); 230 231 setProjectionInView(true); 232 233 // handle the user trying to add a new probe 234 addProbe.addActionListener(e -> { 235 addSpectrum(Color.YELLOW); 236 probeTable.revalidate(); 237 }); 238 239 // handle the user trying to remove an existing probe 240 removeProbe.addActionListener(e -> { 241 int index = probeTable.getSelectedRow(); 242 if (index == -1) { 243 return; 244 } 245 246 removeSpectrum(index); 247 }); 248 removeProbe.setEnabled(false); 249 250 // set up the table. in particular, enable/disable the remove button 251 // depending on whether or not there is a selected probe to remove. 252 probeTable.setDefaultRenderer(Color.class, new ColorRenderer(true)); 253 probeTable.setDefaultEditor(Color.class, new ColorEditor()); 254 probeTable.setPreferredScrollableViewportSize(new Dimension(500, 200)); 255 probeTable.setUI(new HackyDragDropRowUI()); 256 probeTable.getSelectionModel().addListSelectionListener(e -> { 257 if (!probeTable.getSelectionModel().isSelectionEmpty()) { 258 removeProbe.setEnabled(true); 259 } else { 260 removeProbe.setEnabled(false); 261 } 262 }); 263 264 final boolean use360 = getIdv().getStore().get(Constants.PROP_HYDRA_360, false); 265 use360Box = new JCheckBox("0-360 Longitude Format", use360); 266 use360Box.addActionListener(e -> { 267 getIdv().getStore().put(Constants.PROP_HYDRA_360, use360Box.isSelected()); 268 ProbeTableModel model = (ProbeTableModel)probeTable.getModel(); 269 model.updateWith(spectra); 270 model.fireTableDataChanged(); 271 }); 272 273 bgBlack = new JRadioButton("Black"); 274 bgBlack.addActionListener(e -> { 275 logger.trace("selected black background"); 276 XYDisplay master = display.getMaster(); 277 master.setBackground(Color.black); 278 master.setForeground(Color.white); 279 setBlackBackground(true); 280 }); 281 282 bgWhite = new JRadioButton("White"); 283 bgWhite.addActionListener(e -> { 284 logger.trace("selected white background"); 285 XYDisplay master = display.getMaster(); 286 master.setBackground(Color.white); 287 master.setForeground(Color.black); 288 setBlackBackground(false); 289 }); 290 291 bgColorGroup = new ButtonGroup(); 292 bgColorGroup.add(bgBlack); 293 bgColorGroup.add(bgWhite); 294 295 bgBlack.setSelected(getBlackBackground()); 296 bgWhite.setSelected(!getBlackBackground()); 297 298 setShowInDisplayList(true); 299 300 return true; 301 } 302 303 /** 304 * Updates the Wavelength label when user manipulates drag line UI 305 * 306 * @param s full label text, prefix and numeric value 307 * 308 */ 309 public void setWavelengthLabel(String s) { 310 if (s != null) { 311 wavelengthLabel.setText(s); 312 } 313 } 314 315 @Override public void initAfterUnPersistence(ControlContext vc, 316 Hashtable properties, 317 List preSelectedDataChoices) 318 { 319 super.initAfterUnPersistence(vc, properties, preSelectedDataChoices); 320 321 XYDisplay master = display.getMaster(); 322 if (getBlackBackground()) { 323 master.setBackground(Color.black); 324 master.setForeground(Color.white); 325 } else { 326 master.setBackground(Color.white); 327 master.setForeground(Color.black); 328 } 329 } 330 331 @Override public void initDone() { 332 try { 333 display.showChannelSelector(); 334 335 // TODO: this is ugly. 336 Float fieldSelectorChannel = 337 (Float)getDataSelection().getProperty(Constants.PROP_CHAN); 338 if (fieldSelectorChannel == null) { 339 fieldSelectorChannel = 0f; 340 } 341 handleChannelChange(fieldSelectorChannel, false); 342 343 displayMaster.setDisplayInactive(); 344 345 // this if-else block is detecting whether or not a bundle is 346 // being loaded; if true, then we'll have a list of spectra props. 347 // otherwise just throw two default spectrums/probes on the screen. 348 if (!spectraProperties.isEmpty()) { 349 for (Hashtable<String, Object> table : spectraProperties) { 350 Color c = (Color)table.get("color"); 351 Spectrum s = addSpectrum(c); 352 s.setProperties(table); 353 } 354 spectraProperties.clear(); 355 } else { 356 addSpectra(Color.MAGENTA, Color.CYAN); 357 } 358 displayMaster.setDisplayActive(); 359 } catch (Exception e) { 360 logException("MultiSpectralControl.initDone", e); 361 } 362 } 363 364 /** 365 * Overridden by McIDAS-V so that {@literal "hide"} probes when their display 366 * is turned off. Otherwise users can wind up with probes on the screen which 367 * aren't associated with any displayed data. 368 * 369 * @param on {@code true} if we're visible, {@code false} otherwise. 370 * 371 * @see DisplayControl#setDisplayVisibility(boolean) 372 */ 373 374 @Override public void setDisplayVisibility(boolean on) { 375 super.setDisplayVisibility(on); 376 for (Spectrum s : spectra) { 377 if (s.isVisible()) { 378 s.getProbe().quietlySetVisible(on); 379 } 380 } 381 } 382 383 /** 384 * Overridden so that the probes in the main display window can handle 385 * changes to z-axis. 386 * 387 * @throws VisADException Problem creating VisAD object. 388 * @throws RemoteException RemoteException Java RMI error. 389 */ 390 @Override public void applyZPosition() 391 throws VisADException, RemoteException 392 { 393 deactivateDisplays(); 394 double zpos = getVerticalValue(getZPosition()); 395 DisplayRealType drt = getNavigatedDisplay().getDisplayAltitudeType(); 396 for (int i = 0, n = displayables.size(); i < n; i++) { 397 FlaggedDisplayable fd = (FlaggedDisplayable) displayables.get(i); 398 if (!fd.ok(FLAG_ZPOSITION)) { 399 continue; 400 } 401 fd.displayable.setConstantPosition(zpos, drt); 402 } 403 for (Spectrum s : spectra) { 404 s.getProbe().getPointSelector().setZ(zpos); 405 } 406 activateDisplays(); 407 } 408 409 /** 410 * Overridden so that the probes can re-apply their current locations to 411 * their {@link ReadoutProbe.PointSelector PointSelectors}. 412 */ 413 @Override public void projectionChanged() { 414 super.projectionChanged(); 415 MapProjection projection = 416 ((MapViewManager)getViewManager()).getMainProjection(); 417 for (Spectrum s : spectra) { 418 ReadoutProbe rp = s.getProbe(); 419 rp.projectionChanged(projection); 420 } 421 } 422 423 // this will get called before init() by the IDV's bundle magic. 424 public void setSpectraProperties(final List<Hashtable<String, Object>> props) { 425 spectraProperties.clear(); 426 spectraProperties.addAll(props); 427 } 428 429 public List<Hashtable<String, Object>> getSpectraProperties() { 430 List<Hashtable<String, Object>> props = new ArrayList<>(spectra.size()); 431 for (Spectrum s : spectra) { 432 props.add(s.getProperties()); 433 } 434 return props; 435 } 436 437 protected void updateList(final List<Spectrum> updatedSpectra) { 438 spectra.clear(); 439 440 List<String> dataRefIds = new ArrayList<>(updatedSpectra.size()); 441 for (Spectrum spectrum : updatedSpectra) { 442 dataRefIds.add(spectrum.getSpectrumRefName()); 443 spectra.add(spectrum); 444 } 445 display.reorderDataRefsById(dataRefIds); 446 } 447 448 /** 449 * Uses a variable-length array of {@link Color Colors} to create new 450 * readout probes using the specified colors. 451 * 452 * @param colors Variable length array of {@code Colors}. 453 * Shouldn't be {@code null}. 454 */ 455 // TODO(jon): check for null. 456 protected void addSpectra(final Color... colors) { 457 Spectrum currentSpectrum = null; 458 try { 459 for (int i = colors.length-1; i >= 0; i--) { 460 probesSeen++; 461 Color color = colors[i]; 462 String id = "Probe "+probesSeen; 463 currentSpectrum = new Spectrum(this, color, id); 464 spectra.add(currentSpectrum); 465 } 466 ((ProbeTableModel)probeTable.getModel()).updateWith(spectra); 467 } catch (Exception e) { 468 LogUtil.logException("MultiSpectralControl.addSpectra: error while adding spectra", e); 469 } 470 } 471 472 /** 473 * Creates a new {@link ReadoutProbe} with the specified {@link Color}. 474 * 475 * @param color {@code Color} of the new {@code ReadoutProbe}. 476 * {@code null} values are not allowed. 477 * 478 * @return {@link Spectrum} wrapper for the newly created 479 * {@code ReadoutProbe}. 480 * 481 * @throws NullPointerException if {@code color} is {@code null}. 482 */ 483 public Spectrum addSpectrum(final Color color) { 484 Spectrum spectrum = null; 485 try { 486 probesSeen++; 487 String id = "Probe "+probesSeen; 488 spectrum = new Spectrum(this, color, id); 489 spectra.add(spectrum); 490 } catch (Exception e) { 491 LogUtil.logException("MultiSpectralControl.addSpectrum: error creating new spectrum", e); 492 } 493 ((ProbeTableModel)probeTable.getModel()).updateWith(spectra); 494 return spectrum; 495 } 496 497 /** 498 * Attempts to remove the {@link Spectrum} at the given {@code index}. 499 * 500 * @param index Index of the probe to be removed (within {@link #spectra}). 501 */ 502 public void removeSpectrum(final int index) { 503 List<Spectrum> newSpectra = new ArrayList<>(spectra); 504 int mappedIndex = newSpectra.size() - (index + 1); 505 Spectrum removed = newSpectra.get(mappedIndex); 506 newSpectra.remove(mappedIndex); 507 try { 508 removed.removeValueDisplay(); 509 } catch (Exception e) { 510 LogUtil.logException("MultiSpectralControl.removeSpectrum: error removing spectrum", e); 511 } 512 513 updateList(newSpectra); 514 515 // need to signal that the table should update? 516 ProbeTableModel model = (ProbeTableModel)probeTable.getModel(); 517 model.updateWith(newSpectra); 518 probeTable.revalidate(); 519 } 520 521 /** 522 * Iterates through the list of {@link Spectrum Spectrums} that manage each 523 * {@link ReadoutProbe} associated with this display control and calls 524 * {@link Spectrum#removeValueDisplay()} in an effort to remove this 525 * control's probes. 526 * 527 * @see #spectra 528 */ 529 public void removeSpectra() { 530 try { 531 for (Spectrum s : spectra) { 532 s.removeValueDisplay(); 533 } 534 } catch (Exception e) { 535 LogUtil.logException("MultiSpectralControl.removeSpectra: error removing spectrum", e); 536 } 537 } 538 539 /** 540 * Makes each {@link ReadoutProbe} in this display control attempt to 541 * redisplay its readout value. 542 * 543 * <p>Sometimes the probes don't initialize correctly and this method is 544 * a stop-gap solution. 545 */ 546 public void pokeSpectra() { 547 for (Spectrum s : spectra) { 548 s.pokeValueDisplay(); 549 } 550 try { 551 //-display.refreshDisplay(); 552 } catch (Exception e) { 553 LogUtil.logException("MultiSpectralControl.pokeSpectra: error refreshing display", e); 554 } 555 } 556 557 @Override public DataSelection getDataSelection() { 558 DataSelection selection = super.getDataSelection(); 559 if (display != null) { 560 selection.putProperty(Constants.PROP_CHAN, display.getWaveNumber()); 561 try { 562 selection.putProperty(SpectrumAdapter.channelIndex_name, display.getChannelIndex()); 563 } catch (Exception e) { 564 LogUtil.logException("MultiSpectralControl.getDataSelection", e); 565 } 566 } 567 return selection; 568 } 569 570 @Override public void setDataSelection(final DataSelection newSelection) { 571 super.setDataSelection(newSelection); 572 } 573 574 @Override public MapProjection getDataProjection() { 575 MapProjection mp = null; 576 Rectangle2D rect = 577 MultiSpectralData.getLonLatBoundingBox(display.getImageData()); 578 579 try { 580 mp = new LambertAEA(rect); 581 } catch (Exception e) { 582 logException("MultiSpectralControl.getDataProjection", e); 583 } 584 585 return mp; 586 } 587 588 public static float[] minmax(float[] values) { 589 float min = Float.MAX_VALUE; 590 float max = -Float.MAX_VALUE; 591 for (int k = 0; k < values.length; k++) { 592 float val = values[k]; 593 if ((val == val) && (val < Float.POSITIVE_INFINITY) && (val > Float.NEGATIVE_INFINITY)) { 594 if (val < min) { 595 min = val; 596 } 597 if (val > max) { 598 max = val; 599 } 600 } 601 } 602 return new float[] { min, max }; 603 } 604 605 /** 606 * Convenience method for extracting the parameter name. 607 * 608 * @return Results from {@link DataChoice#getName()}, or {@link #PARAM} if 609 * the {@code DataChoice} is (somehow) {@code null}. 610 */ 611 private String getParameterName() { 612 String parameterName = PARAM; 613 DataChoice choice = getDataChoice(); 614 if (choice != null) { 615 parameterName = choice.getName(); 616 } 617 return parameterName; 618 } 619 620 /** 621 * Get the initial {@link Range} for the data and color table. 622 * 623 * <p>Note: if there is a parameter default range associated with the 624 * current parameter name, that will be returned. If there is <b>not</b> a 625 * parameter default range match, a {@code Range} consisting of 626 * {@link #rangeMin} and {@link #rangeMax} will be returned. 627 * </p> 628 * 629 * @return Initial {@code Range} for data and color table. 630 * 631 * @throws VisADException if VisAD had problems. 632 * @throws RemoteException if there was a Java RMI problem. 633 */ 634 @Override protected Range getInitialRange() throws VisADException, 635 RemoteException 636 { 637 String parameterName = getParameterName(); 638 Unit dispUnit = getDisplayUnit(); 639 DisplayConventions conventions = getDisplayConventions(); 640 Range paramRange = conventions.getParamRange(parameterName, dispUnit); 641 if (paramRange == null) { 642 paramRange = new Range(rangeMin, rangeMax); 643 } 644 return paramRange; 645 } 646 647 /** 648 * Get the initial {@link ColorTable} associated with this control's 649 * parameter name. 650 * 651 * <p>Note: if there is a parameter default color table associated with 652 * the parameter name, that color table will be returned. If there are 653 * <b>no</b> parameter defaults associated with the parameter name, 654 * then the {@code ColorTable} associated with {@literal "BrightnessTemp"} 655 * is returned (this is a {@literal "legacy"} behavior). 656 * </p> 657 * 658 * @return {@code ColorTable} to use. 659 */ 660 @Override protected ColorTable getInitialColorTable() { 661 String parameterName = getParameterName(); 662 DisplayConventions conventions = getDisplayConventions(); 663 ParamDefaultsEditor defaults = conventions.getParamDefaultsEditor(); 664 ColorTable ct = defaults.getParamColorTable(parameterName, false); 665 if (ct == null) { 666 ct = conventions.getParamColorTable(PARAM); 667 } 668 return ct; 669 } 670 671 @Override public Container doMakeContents() { 672 try { 673 JTabbedPane pane = new JTabbedPane(); 674 pane.add("Display", GuiUtils.inset(getDisplayTab(), 5)); 675 pane.add("Settings", 676 GuiUtils.inset(GuiUtils.top(doMakeWidgetComponent()), 5)); 677 pane.add("Histogram", GuiUtils.inset(GuiUtils.top(getHistogramTabComponent()), 5)); 678 GuiUtils.handleHeavyWeightComponentsInTabs(pane); 679 return pane; 680 } catch (Exception e) { 681 logException("MultiSpectralControl.doMakeContents", e); 682 } 683 return null; 684 } 685 686 @Override public void doRemove() throws VisADException, RemoteException { 687 // forcibly clear the value displays when the user has elected to kill 688 // the display. the readouts will persist otherwise. 689 removeSpectra(); 690 super.doRemove(); 691 } 692 693 /** 694 * Runs through the list of ViewManager-s and tells each to destroy. 695 * Creates a new viewManagers list. 696 */ 697 @Override protected void clearViewManagers() { 698 if (viewManagers == null) { 699 return; 700 } 701 702 List<ViewManager> tmp = new ArrayList<>(viewManagers); 703 viewManagers = null; 704 for (ViewManager vm : tmp) { 705 if (vm != null) { 706 vm.destroy(); 707 } 708 } 709 } 710 711 @SuppressWarnings("unchecked") 712 @Override protected JComponent doMakeWidgetComponent() { 713 List<Component> widgetComponents; 714 try { 715 List<ControlWidget> controlWidgets = new ArrayList<>(15); 716 getControlWidgets(controlWidgets); 717 controlWidgets.add(new WrapperWidget(this, GuiUtils.rLabel("Background Color:"), GuiUtils.hbox(bgBlack, bgWhite))); 718 controlWidgets.add(new WrapperWidget(this, GuiUtils.rLabel("Readout Probes:"), scrollPane)); 719 controlWidgets.add(new WrapperWidget(this, GuiUtils.rLabel(" "), GuiUtils.hbox(addProbe, removeProbe, GuiUtils.right(use360Box)))); 720 widgetComponents = ControlWidget.fillList(controlWidgets); 721 } catch (Exception e) { 722 LogUtil.logException("Problem building the MultiSpectralControl settings", e); 723 widgetComponents = new ArrayList<>(5); 724 widgetComponents.add(new JLabel("Error building component...")); 725 } 726 727 GuiUtils.tmpInsets = new Insets(4, 8, 4, 8); 728 GuiUtils.tmpFill = GridBagConstraints.HORIZONTAL; 729 return GuiUtils.doLayout(widgetComponents, 2, GuiUtils.WT_NY, GuiUtils.WT_N); 730 } 731 732 protected MultiSpectralDisplay getMultiSpectralDisplay() { 733 return display; 734 } 735 736 public boolean updateImage(final float newChan) { 737 if (!display.setWaveNumber(newChan)) { 738 return false; 739 } 740 741 DisplayableData imageDisplay = display.getImageDisplay(); 742 743 try { 744 FlatField image = display.getImageData(); 745 displayMaster.setDisplayInactive(); //try to consolidate display transforms 746 imageDisplay.setData(image); 747 pokeSpectra(); 748 displayMaster.setDisplayActive(); 749 updateHistogramTab(); 750 751 // Inquiry 2784 Request 3 752 // NOTE: updateHistogramTab updates the rangeMin/rangeMax fields, 753 // so it *must* be called before the following setRange call. 754 // this ensures the color table/map updates as expected. 755 setRange(new Range(rangeMin, rangeMax)); 756 // end Inquiry 2784 Request 3 stuff 757 } catch (Exception e) { 758 LogUtil.logException("MultiSpectralControl.updateImage", e); 759 return false; 760 } 761 762 return true; 763 } 764 765 // be sure to update the displayed image even if a channel change 766 // originates from the msd itself. 767 @Override public void handleChannelChange(final float newChan) { 768 handleChannelChange(newChan, true); 769 } 770 771 public void handleChannelChange(final float newChan, boolean update) { 772 if (update) { 773 if (updateImage(newChan)) { 774 wavenumbox.setText(Float.toString(newChan)); 775 } 776 } else { 777 wavenumbox.setText(Float.toString(newChan)); 778 } 779 } 780 781 private JComponent getDisplayTab() { 782 List<JComponent> compList = new ArrayList<>(5); 783 784 saveAsCsv = new JButton("Save..."); 785 saveAsCsv.addActionListener(e-> writeToCSV()); 786 787 if (display.getBandSelectComboBox() == null) { 788 final JButton nameLabel = new JButton("Wavenumber"); 789 nameLabel.addActionListener(e -> { 790 if (nameLabel.getText().equals("Wavenumber")) { 791 nameLabel.setText("Wavelength"); 792 String tmp = wavenumbox.getText().trim(); 793 wavenumbox.setText(String.valueOf(1/Float.valueOf(tmp))); 794 } else { 795 nameLabel.setText("Wavenumber"); 796 String tmp = wavenumbox.getText().trim(); 797 wavenumbox.setText(String.valueOf(1/Float.valueOf(tmp))); 798 } 799 }); 800 801 wavenumbox.addActionListener(e -> { 802 if (nameLabel.getText().equals("Wavenumber")) { 803 String tmp = wavenumbox.getText().trim(); 804 updateImage(Float.valueOf(tmp)); 805 } else { 806 String tmp = wavenumbox.getText().trim(); 807 updateImage(1/Float.valueOf(tmp)); 808 } 809 }); 810 811 compList.add(nameLabel); 812 compList.add(wavenumbox); 813 } else { 814 final JComboBox bandBox = display.getBandSelectComboBox(); 815 bandBox.addActionListener(e -> { 816 String bandName = (String) bandBox.getSelectedItem(); 817 Float channel = (Float)display.getMultiSpectralData().getBandNameMap().get(bandName); 818 updateImage(channel.floatValue()); 819 }); 820 JLabel nameLabel = new JLabel("Band: "); 821 822 compList.add(nameLabel); 823 compList.add(bandBox); 824 } 825 compList.add(saveAsCsv); 826 827 JPanel waveNo = GuiUtils.center(GuiUtils.doLayout(compList, compList.size(), GuiUtils.WT_N, GuiUtils.WT_N)); 828 return GuiUtils.centerBottom(display.getDisplayComponent(), waveNo); 829 } 830 831 /** 832 * Write multispectral data to a CSV file. 833 * 834 * <p>Now with file choosers!</p> 835 */ 836 public void writeToCSV() { 837 // McIDAS Inquiry #2535-3141 838 String filename = FileManager.getWriteFile(Misc.newList(FileManager.FILTER_CSV), FileManager.SUFFIX_CSV); 839 if (filename == null) { 840 return; 841 } 842 843 CsvTask task = new CsvTask(filename); 844 task.addPropertyChangeListener(evt -> { 845// logger.trace("property changed: {}", evt); 846 switch (evt.getPropertyName()) { 847 case "state": 848 if (evt.getNewValue() == SwingWorker.StateValue.DONE) { 849 saveAsCsv.setEnabled(true); 850 if (exportCsvDialog != null) { 851 SwingUtilities.invokeLater(() -> exportCsvDialog.taskOver()); 852 } 853 } else { 854 saveAsCsv.setEnabled(false); 855 if (exportCsvDialog != null) { 856 SwingUtilities.invokeLater(() -> exportCsvDialog.setVisible(true)); 857 } 858 } 859 break; 860 case "progress": 861 int value = (Integer)evt.getNewValue(); 862 if (exportCsvDialog != null) { 863 exportCsvDialog.setProgress(value); 864 SwingUtilities.invokeLater(() -> exportCsvDialog.setProgress(value)); 865 } 866 break; 867 default: 868// logger.trace("unknown evt type: {}", evt); 869 if (exportCsvDialog != null) { 870 SwingUtilities.invokeLater(() -> exportCsvDialog.taskOver()); 871 } else { 872 task.cancel(true); 873 } 874 saveAsCsv.setEnabled(true); 875 break; 876 } 877 }); 878 879 createCsvDialog(task); 880 task.execute(); 881 } 882 883 /** 884 * Create the dialog used to show our CSV export progress. 885 * 886 * <p>Be aware that the dialog will not be visible until {@link CsvTask#execute() execute} is called.</p> 887 * 888 * <p>The {@link CsvDialog} will automatically close upon completion or the user cancelling.</p> 889 * 890 * @param task CSV export task. Cannot be {@code null}. 891 */ 892 private void createCsvDialog(CsvTask task) { 893 // attempting to center the modal CsvDialog over the data explorer window. 894 // if it somehow does not exist, try main display window. 895 UIManager mcvUI = (UIManager)McIDASV.getStaticMcv().getIdvUIManager(); 896 IdvWindow window = mcvUI.getDashboardWindow(); 897 if (window == null) { 898 // not sure how this would be possible, given that the multispectralcontrol 899 // is embedded within the Data Explorer window! 900 window = IdvWindow.getActiveWindow(); 901 } 902 exportCsvDialog = new CsvDialog( 903 window.getWindow(), 904 getOuterContents(), 905 "Exporting values...", 906 task); 907 } 908 909 910 private JComponent getHistogramTabComponent() { 911 updateHistogramTab(); 912 JComponent histoComp = histoWrapper.doMakeContents(); 913 JLabel rangeLabel = GuiUtils.rLabel("Range "); 914 JLabel minLabel = GuiUtils.rLabel("Min"); 915 JLabel maxLabel = GuiUtils.rLabel(" Max"); 916 List<JComponent> rangeComps = new ArrayList<>(); 917 rangeComps.add(rangeLabel); 918 rangeComps.add(minLabel); 919 rangeComps.add(minBox); 920 rangeComps.add(maxLabel); 921 rangeComps.add(maxBox); 922 minBox.addActionListener(ae -> { 923 rangeMin = Float.valueOf(minBox.getText().trim()); 924 rangeMax = Float.valueOf(maxBox.getText().trim()); 925 histoWrapper.modifyRange((int) rangeMin, (int) rangeMax); 926 }); 927 maxBox.addActionListener(ae -> { 928 rangeMin = Float.valueOf(minBox.getText().trim()); 929 rangeMax = Float.valueOf(maxBox.getText().trim()); 930 histoWrapper.modifyRange((int) rangeMin, (int) rangeMax); 931 }); 932 JPanel rangePanel = 933 GuiUtils.center(GuiUtils.doLayout(rangeComps, 5, GuiUtils.WT_N, GuiUtils.WT_N)); 934 JButton resetButton = new JButton("Reset"); 935 resetButton.addActionListener(ae -> resetColorTable()); 936 937 JPanel resetPanel = 938 GuiUtils.center(GuiUtils.inset(GuiUtils.wrap(resetButton), 4)); 939 940 return GuiUtils.topCenterBottom(histoComp, rangePanel, resetPanel); 941 } 942 943 private void updateHistogramTab() { 944 try { 945 FlatField ff = display.getImageData(); 946 histoWrapper.loadData(ff); 947 org.jfree.data.Range range = histoWrapper.getRange(); 948 rangeMin = (float)range.getLowerBound(); 949 rangeMax = (float)range.getUpperBound(); 950 minBox.setText(Integer.toString((int)rangeMin)); 951 maxBox.setText(Integer.toString((int)rangeMax)); 952 } catch (IllegalArgumentException e) { 953 histoWrapper.clearHistogram(); 954 histoWrapper.resetPlot(); 955 rangeMin = Float.NaN; 956 rangeMax = Float.NaN; 957 minBox.setText("NaN"); 958 maxBox.setText("NaN"); 959 } catch (RemoteException | VisADException e) { 960 logException("MultiSpectralControl.getHistogramTabComponent", e); 961 } 962 } 963 964 public void resetColorTable() { 965 histoWrapper.doReset(); 966 histoWrapper.modifyRange((int) origRangeMin, (int) origRangeMax); 967 // TJJ Jan 2019 - make sure and reset the min/max input boxes too 968 minBox.setText(Integer.toString((int) origRangeMin)); 969 maxBox.setText(Integer.toString((int) origRangeMax)); 970 } 971 972 protected void contrastStretch(final double low, final double high) { 973 try { 974 org.jfree.data.Range range = histoWrapper.getRange(); 975 rangeMin = (float)range.getLowerBound(); 976 rangeMax = (float)range.getUpperBound(); 977 minBox.setText(Integer.toString((int)rangeMin)); 978 maxBox.setText(Integer.toString((int)rangeMax)); 979 setRange(getInitialColorTable().getName(), new Range(low, high)); 980 } catch (Exception e) { 981 logException("MultiSpectralControl.contrastStretch", e); 982 } 983 } 984 985 // sole use is for persistence! 986 public boolean getBlackBackground() { 987 return blackBackground; 988 } 989 990 // sole use is for persistence! 991 public void setBlackBackground(boolean value) { 992 blackBackground = value; 993 } 994 995 class CsvDialog extends JDialog { 996 private final JProgressBar progressBar; 997 998 CsvDialog(Window parentWindow, Component parentComponent, 999 String title, CsvTask task) { 1000 super(parentWindow, title); 1001 setModalityType(ModalityType.APPLICATION_MODAL); 1002 setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); 1003 1004 getContentPane().setLayout(new GridLayout(1,1)); 1005 1006 JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); 1007 mainPanel.setBorder(BorderFactory.createEmptyBorder(15, 10, 5, 10)); 1008 1009 JPanel centerPanel = new JPanel(new BorderLayout()); 1010 mainPanel.add(centerPanel, BorderLayout.CENTER); 1011 1012 JPanel southPanel = new JPanel(new BorderLayout()); 1013 mainPanel.add(southPanel, BorderLayout.SOUTH); 1014 1015 this.progressBar = new JProgressBar(0, 100); 1016 this.progressBar.setStringPainted(true); 1017 southPanel.add(progressBar, BorderLayout.CENTER); 1018 1019 JPanel buttonsPanel = new JPanel(new FlowLayout()); 1020 southPanel.add(buttonsPanel, BorderLayout.SOUTH); 1021 1022 JButton cancelButton = new JButton("Cancel"); 1023 cancelButton.addActionListener(e -> task.cancel(true)); 1024 buttonsPanel.add(cancelButton); 1025 getContentPane().add(mainPanel); 1026 pack(); 1027 setLocationRelativeTo(parentComponent); 1028 } 1029 1030 void setProgress(int percent) { 1031 this.progressBar.setValue(percent); 1032 } 1033 1034 void taskOver() { 1035 setVisible(false); 1036 dispose(); 1037 } 1038 } 1039 1040 class CsvTask extends SwingWorker<String, Object> { 1041 private final String path; 1042 1043 /** 1044 * Creates a CSV exporting task that's meant to store its values in the file specified by {@code path}. 1045 * 1046 * @param path File to write to. Cannot be {@code null}. 1047 */ 1048 public CsvTask(String path) { 1049 this.path = Objects.requireNonNull(path, "path cannot be null"); 1050 } 1051 1052 /** 1053 * Does the work of exporting and formatting CSV values from a {@link MultiSpectralControl} in a 1054 * background thread. 1055 * 1056 * @return String representing the contents to write to a CSV file. May be {@code null}. 1057 */ 1058 @Override public String doInBackground() { 1059 String csvContents = null; 1060 try { 1061 csvContents = exportSpectra(); 1062 } catch (Exception e) { 1063 logger.warn("Something went wrong extracting values", e); 1064 } 1065 return csvContents; 1066 } 1067 1068 /** 1069 * Called (on the event dispatch thread) when {@link #doInBackground()} has completed. 1070 */ 1071 @Override protected void done() { 1072 try { 1073 String contents = get(); 1074 if (contents == null) { 1075 logger.error("Could not extract CSV values, contents are null."); 1076 } else { 1077 writeToPath(contents); 1078 } 1079 } catch (CancellationException e) { 1080 firePropertyChange("state", StateValue.STARTED, SwingWorker.StateValue.DONE); 1081 } catch (InterruptedException e) { 1082 throw new RuntimeException(e); 1083 } catch (ExecutionException e) { 1084 String why = null; 1085 Throwable cause = e.getCause(); 1086 why = Objects.requireNonNullElse(cause, e).getMessage(); 1087 logger.warn("Error writing to path: " + path, why); 1088 } 1089 } 1090 1091 /** 1092 * Write the given string to {@link #path}. 1093 * 1094 * @param csvContents CSV file contents. Should not be {@code null}. 1095 */ 1096 private void writeToPath(String csvContents) { 1097 try (FileWriter writer = new FileWriter(this.path)) { 1098 writer.write(csvContents); 1099 writer.flush(); 1100 } catch (IOException ex) { 1101 logger.warn("Could not write to file", ex); 1102 } 1103 } 1104 1105 /** 1106 * Iterates through each channel/wavenumber and extracts the value of the existing ReadoutProbes 1107 * at their current positions. 1108 * 1109 * <p>Be warned, this can be slow when there are thousands of channels. Definitely do not 1110 * run this method on the event dispatch thread.</p> 1111 * 1112 * @param wavelengths Array containing all the channels/wavenumbers. Cannot be {@code null}. 1113 * 1114 * @return Two-dimensional array of doubles. First array represents the probes, 1115 * second dimension is the value of a probe at each channel/wavenumber. 1116 * 1117 * @throws VisADException if there was a problem 1118 * @throws RemoteException if there was somehow an RMI problem 1119 */ 1120 private double[][] collectValues(double[] wavelengths) throws VisADException, RemoteException{ 1121 double[][] values = new double[spectra.size()][wavelengths.length]; 1122 1123 RealTuple[] probeLocations = new RealTuple[spectra.size()]; 1124 for (int i = 0; i < spectra.size(); i++) { 1125 Spectrum s = spectra.get(i); 1126 RealTuple location = s.getProbe().getEarthPosition(); 1127 double[] locVals = location.getValues(); 1128 if (locVals[1] < -180) { 1129 locVals[1] += 360f; 1130 } 1131 1132 if (locVals[0] > 180) { 1133 locVals[0] -= 360f; 1134 } 1135 probeLocations[i] = makeEarth2dTuple(locVals[0], locVals[1]); 1136 } 1137 for (int i = 0; (i < wavelengths.length) && !isCancelled(); i++) { 1138 FlatField imageData = display.getImageDataFrom((float)wavelengths[i]); 1139 for (int j = 0; j < probeLocations.length; j++) { 1140 RealTuple probeLoc = probeLocations[j]; 1141 Real realVal = (Real)imageData.evaluate(probeLoc, Data.NEAREST_NEIGHBOR, Data.NO_ERRORS); 1142 values[j][i] = realVal.getValue(); 1143 } 1144 // set percentage complete. iterating via display.getImageDataFrom(...) 1145 // is easily the slowest part of the CSV export process, so simply 1146 // figuring out the number of wavelengths processed likely suffices for 1147 // a simple progress bar. 1148 setProgress((int)(100 * ((float)i / (float)wavelengths.length))); 1149 } 1150 return values; 1151 } 1152 1153 /** 1154 * Coordinates the collecting of probe data as well as creating a string representing 1155 * the contents of the CSV file. 1156 * 1157 * @return String value that can be written to a CSV file. 1158 * 1159 * @throws Exception if there was some problem 1160 */ 1161 private String exportSpectra() throws Exception { 1162 DecimalFormat format = new DecimalFormat(getStore().get(Constants.PREF_LATLON_FORMAT, "##0.0")); 1163 boolean use360 = false; 1164 if (use360Box != null) { 1165 use360 = use360Box.isSelected(); 1166 } 1167 1168 double[] wavelengths = display.getDomainSet().getDoubles()[0]; 1169 double[][] values = collectValues(wavelengths); 1170 1171 // each wavelen,value1,value2 generally less than 50 chars, 1172 // will need wavelengths.length of 'em. 1173 // +200 to account for the length of the column headers 1174 StringBuilder builder = new StringBuilder(wavelengths.length * 50 + 200); 1175 // Wavelen, Probe 1 LATLON BrightnessTemp, Probe 2 LATLON BrightnessTemp, Probe 3 LatLon BrightnessTemp 1176 builder.append("Wavelength,"); 1177 for (int i = 0; i < spectra.size(); i++) { 1178 Spectrum s = spectra.get(i); 1179 double lon = use360 1180 ? ProbeTableModel.clamp360(s.getLongitude()) 1181 : ProbeTableModel.clamp180(s.getLongitude()); 1182 builder.append(s.getId()) 1183 .append(" (Latitude: ").append(format.format(s.getLatitude())) 1184 .append("; Longitude: ").append(format.format(lon)).append(')'); 1185 if (i < spectra.size() - 1) { 1186 builder.append(','); 1187 } 1188 } 1189 builder.append('\n'); 1190 for (int i = 0; i < wavelengths.length; i++) { 1191 builder.append(wavelengths[i]).append(','); 1192 for (int j = 0; j < spectra.size(); j++) { 1193 builder.append(values[j][i]); 1194 if (j < spectra.size() - 1) { 1195 builder.append(','); 1196 } 1197 } 1198 builder.append('\n'); 1199 } 1200 return builder.toString(); 1201 } 1202 } 1203 1204 private static class Spectrum implements ProbeListener { 1205 1206 private static final Logger logger = LoggerFactory.getLogger(Spectrum.class); 1207 1208 private final MultiSpectralControl control; 1209 1210 /** 1211 * Display that is displaying the spectrum associated with 1212 * {@code probe}'s location. 1213 */ 1214 private final MultiSpectralDisplay display; 1215 1216 /** VisAD's reference to this spectrum. */ 1217 private final DataReference spectrumRef; 1218 1219 /** 1220 * Probe that appears in the {@literal "image display"} associated with 1221 * the current display control. 1222 */ 1223 private ReadoutProbe probe; 1224 1225 /** Whether or not {@code probe} is visible. */ 1226 private boolean isVisible = true; 1227 1228 /** 1229 * Human-friendly ID for this spectrum and probe. Used in 1230 * {@link MultiSpectralControl#probeTable}. 1231 */ 1232 private final String myId; 1233 1234 /** 1235 * Initializes a new Spectrum that is {@literal "bound"} to 1236 * {@code control} and whose color is {@code color}. 1237 * 1238 * @param control Display control that contains this spectrum and the 1239 * associated {@link ReadoutProbe}. Cannot be null. 1240 * @param color Color of {@code probe}. Cannot be {@code null}. 1241 * @param myId Human-friendly ID used a reference for this 1242 * spectrum/probe. Cannot be {@code null}. 1243 * 1244 * @throws NullPointerException if {@code control}, {@code color}, or 1245 * {@code myId} is {@code null}. 1246 * @throws VisADException if VisAD-land had some problems. 1247 * @throws RemoteException if VisAD's RMI stuff had problems. 1248 */ 1249 public Spectrum(final MultiSpectralControl control, final Color color, final String myId) throws VisADException, RemoteException { 1250 this.control = control; 1251 this.display = control.getMultiSpectralDisplay(); 1252 this.myId = myId; 1253 spectrumRef = new DataReferenceImpl(hashCode() + "_spectrumRef"); 1254 display.addRef(spectrumRef, color); 1255 String pattern = control.getStore().get(IdvConstants.PREF_LATLON_FORMAT, "##0.0"); 1256 probe = new ReadoutProbe(control, display.getImageData(), color, pattern, control.getDisplayVisibility()); 1257 this.updatePosition(probe.getEarthPosition()); 1258 probe.addProbeListener(this); 1259 1260 control.addAttributedDisplayable(probe.getValueDisplay(), FLAG_ZPOSITION); 1261 } 1262 1263 public void probePositionChanged(final ProbeEvent<RealTuple> e) { 1264 RealTuple position = e.getNewValue(); 1265 updatePosition(position); 1266 } 1267 1268 public void probeFormatPatternChanged(final ProbeEvent<String> e) { 1269 1270 } 1271 1272 public void updatePosition(RealTuple position) { 1273 try { 1274 FlatField spectrum = display.getMultiSpectralData().getSpectrum(position); 1275 spectrumRef.setData(spectrum); 1276 } catch (Exception ex) { 1277 logger.error("Error updating postion.", ex); 1278 } 1279 } 1280 1281 public String getValue() { 1282 return probe.getValue(); 1283 } 1284 1285 public double getLatitude() { 1286 return probe.getLatitude(); 1287 } 1288 1289 public double getLongitude() { 1290 return probe.getLongitude(); 1291 } 1292 1293 public Color getColor() { 1294 return probe.getColor(); 1295 } 1296 1297 public String getId() { 1298 return myId; 1299 } 1300 1301 public DataReference getSpectrumRef() { 1302 return spectrumRef; 1303 } 1304 1305 public String getSpectrumRefName() { 1306 return hashCode() + "_spectrumRef"; 1307 } 1308 1309 public void setColor(final Color color) { 1310 if (color == null) { 1311 throw new NullPointerException("Can't use a null color"); 1312 } 1313 1314 try { 1315 display.updateRef(spectrumRef, color); 1316 probe.quietlySetColor(color); 1317 } catch (Exception ex) { 1318 logger.error("Error setting color", ex); 1319 } 1320 } 1321 1322 /** 1323 * Shows and hides this spectrum/probe. Note that an {@literal "hidden"} 1324 * spectrum merely uses an alpha value of zero for the spectrum's 1325 * color--nothing is actually removed! 1326 * 1327 * <p>Also note that if our {@link MultiSpectralControl} has its visibility 1328 * toggled {@literal "off"}, the probe itself will not be shown. 1329 * <b>It will otherwise behave as if it is visible!</b> 1330 * 1331 * @param visible {@code true} for {@literal "visible"}, {@code false} otherwise. 1332 */ 1333 public void setVisible(final boolean visible) { 1334 isVisible = visible; 1335 Color c = probe.getColor(); 1336 int alpha = visible ? 255 : 0; 1337 c = new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha); 1338 try { 1339 display.updateRef(spectrumRef, c); 1340 // only bother actually *showing* the probe if its display is 1341 // actually visible. 1342 if (control.getDisplayVisibility()) { 1343 probe.quietlySetVisible(visible); 1344 } 1345 } catch (Exception e) { 1346 LogUtil.logException("There was a problem setting the visibility of probe \""+spectrumRef+"\" to "+visible, e); 1347 } 1348 } 1349 1350 public boolean isVisible() { 1351 return isVisible; 1352 } 1353 1354 protected ReadoutProbe getProbe() { 1355 return probe; 1356 } 1357 1358 public void probeColorChanged(final ProbeEvent<Color> e) { 1359 logger.trace("color change event={}", e); 1360 } 1361 1362 public void probeVisibilityChanged(final ProbeEvent<Boolean> e) { 1363 logger.trace("probe event={}", e); 1364 Boolean newVal = e.getNewValue(); 1365 if (newVal != null) { 1366 isVisible = newVal; 1367 } 1368 } 1369 1370 public Hashtable<String, Object> getProperties() { 1371 Hashtable<String, Object> table = new Hashtable<>(); 1372 table.put("color", probe.getColor()); 1373 table.put("visibility", isVisible); 1374 table.put("lat", probe.getLatitude()); 1375 table.put("lon", probe.getLongitude()); 1376 return table; 1377 } 1378 1379 public void setProperties(final Hashtable<String, Object> table) { 1380 if (table == null) { 1381 throw new NullPointerException("properties table cannot be null"); 1382 } 1383 Color color = (Color)table.get("color"); 1384 Double lat = (Double)table.get("lat"); 1385 Double lon = (Double)table.get("lon"); 1386 Boolean visibility = (Boolean)table.get("visibility"); 1387 probe.setLatLon(lat, lon); 1388 probe.setColor(color); 1389 setVisible(visibility); 1390 } 1391 1392 public void pokeValueDisplay() { 1393 probe.setField(display.getImageData()); 1394 try { 1395 //FlatField spectrum = display.getMultiSpectralData().getSpectrum(probe.getEarthPosition()); 1396 //spectrumRef.setData(spectrum); 1397 } catch (Exception e) { } 1398 } 1399 1400 public void removeValueDisplay() throws VisADException, RemoteException { 1401 probe.handleProbeRemoval(); 1402 display.removeRef(spectrumRef); 1403 } 1404 } 1405 1406 // TODO(jon): MultiSpectralControl should become the table model. 1407 private static class ProbeTableModel extends AbstractTableModel implements ProbeListener { 1408// private static final String[] COLUMNS = { 1409// "Visibility", "Probe ID", "Value", "Spectrum", "Latitude", "Longitude", "Color" 1410// }; 1411 1412 private static final String[] COLUMNS = { 1413 "Visibility", "Probe ID", "Value", "Latitude", "Longitude", "Color" 1414 }; 1415 1416 private final Map<ReadoutProbe, Integer> probeToIndex = new LinkedHashMap<>(); 1417 private final Map<Integer, Spectrum> indexToSpectrum = new LinkedHashMap<>(); 1418 private final MultiSpectralControl control; 1419 1420 public ProbeTableModel(final MultiSpectralControl control, final List<Spectrum> probes) { 1421 this.control = requireNonNull(control); 1422 updateWith(requireNonNull(probes)); 1423 } 1424 1425 public void probeColorChanged(final ProbeEvent<Color> e) { 1426 ReadoutProbe probe = e.getProbe(); 1427 if (!probeToIndex.containsKey(probe)) { 1428 return; 1429 } 1430 int index = probeToIndex.get(probe); 1431 fireTableCellUpdated(index, 5); 1432 } 1433 1434 public void probeVisibilityChanged(final ProbeEvent<Boolean> e) { 1435 ReadoutProbe probe = e.getProbe(); 1436 if (!probeToIndex.containsKey(probe)) { 1437 return; 1438 } 1439 int index = probeToIndex.get(probe); 1440 fireTableCellUpdated(index, 0); 1441 } 1442 1443 public void probePositionChanged(final ProbeEvent<RealTuple> e) { 1444 ReadoutProbe probe = e.getProbe(); 1445 if (!probeToIndex.containsKey(probe)) { 1446 return; 1447 } 1448 int index = probeToIndex.get(probe); 1449 fireTableRowsUpdated(index, index); 1450 } 1451 1452 public void probeFormatPatternChanged(final ProbeEvent<String> e) { 1453 ReadoutProbe probe = e.getProbe(); 1454 if (!probeToIndex.containsKey(probe)) { 1455 return; 1456 } 1457 int index = probeToIndex.get(probe); 1458 fireTableRowsUpdated(index, index); 1459 } 1460 1461 public void updateWith(final List<Spectrum> updatedSpectra) { 1462 requireNonNull(updatedSpectra); 1463 1464 probeToIndex.clear(); 1465 indexToSpectrum.clear(); 1466 1467 for (int i = 0, j = updatedSpectra.size()-1; i < updatedSpectra.size(); i++, j--) { 1468 Spectrum spectrum = updatedSpectra.get(j); 1469 ReadoutProbe probe = spectrum.getProbe(); 1470 if (!probe.hasListener(this)) { 1471 probe.addProbeListener(this); 1472 } 1473 probeToIndex.put(spectrum.getProbe(), i); 1474 indexToSpectrum.put(i, spectrum); 1475 } 1476 } 1477 1478 public int getColumnCount() { 1479 return COLUMNS.length; 1480 } 1481 1482 public int getRowCount() { 1483 if (probeToIndex.size() != indexToSpectrum.size()) { 1484 throw new AssertionError(""); 1485 } 1486 return probeToIndex.size(); 1487 } 1488 1489// public Object getValueAt(final int row, final int column) { 1490// Spectrum spectrum = indexToSpectrum.get(row); 1491// switch (column) { 1492// case 0: return spectrum.isVisible(); 1493// case 1: return spectrum.getId(); 1494// case 2: return spectrum.getValue(); 1495// case 3: return "notyet"; 1496// case 4: return formatPosition(spectrum.getLatitude()); 1497// case 5: return formatPosition(spectrum.getLongitude()); 1498// case 6: return spectrum.getColor(); 1499// default: throw new AssertionError("uh oh"); 1500// } 1501// } 1502 public Object getValueAt(final int row, final int column) { 1503 DecimalFormat format = new DecimalFormat(control.getIdv().getStore().get(Constants.PREF_LATLON_FORMAT, "##0.0")); 1504 boolean use360 = control.use360Box.isSelected(); 1505 Spectrum spectrum = indexToSpectrum.get(row); 1506 switch (column) { 1507 case 0: return spectrum.isVisible(); 1508 case 1: return spectrum.getId(); 1509 case 2: return spectrum.getValue(); 1510 case 3: return format.format(spectrum.getLatitude()); 1511 case 4: return format.format(use360 ? clamp360(spectrum.getLongitude()) : clamp180(spectrum.getLongitude())); 1512 case 5: return spectrum.getColor(); 1513 default: throw new AssertionError("uh oh"); 1514 } 1515 } 1516 1517 public boolean isCellEditable(final int row, final int column) { 1518 switch (column) { 1519 case 0: return true; 1520 case 5: return true; 1521 default: return false; 1522 } 1523 } 1524 1525 public void setValueAt(final Object value, final int row, final int column) { 1526 Spectrum spectrum = indexToSpectrum.get(row); 1527 boolean didUpdate = true; 1528 switch (column) { 1529 case 0: spectrum.setVisible((Boolean)value); break; 1530 case 5: spectrum.setColor((Color)value); break; 1531 default: didUpdate = false; break; 1532 } 1533 1534 if (didUpdate) { 1535 fireTableCellUpdated(row, column); 1536 } 1537 } 1538 1539 public void moveRow(final int origin, final int destination) { 1540 // get the dragged spectrum (and probe) 1541 Spectrum dragged = indexToSpectrum.get(origin); 1542 ReadoutProbe draggedProbe = dragged.getProbe(); 1543 1544 // get the current spectrum (and probe) 1545 Spectrum current = indexToSpectrum.get(destination); 1546 ReadoutProbe currentProbe = current.getProbe(); 1547 1548 // update references in indexToSpetrum 1549 indexToSpectrum.put(destination, dragged); 1550 indexToSpectrum.put(origin, current); 1551 1552 // update references in probeToIndex 1553 probeToIndex.put(draggedProbe, destination); 1554 probeToIndex.put(currentProbe, origin); 1555 1556 // build a list of the spectra, ordered by index 1557 List<Spectrum> updated = new ArrayList<>(indexToSpectrum.size()); 1558 for (int i = indexToSpectrum.size()-1; i >= 0; i--) { 1559 updated.add(indexToSpectrum.get(i)); 1560 } 1561 1562 // send it to control. 1563 control.updateList(updated); 1564 } 1565 1566 public String getColumnName(final int column) { 1567 return COLUMNS[column]; 1568 } 1569 1570 public Class<?> getColumnClass(final int column) { 1571 return getValueAt(0, column).getClass(); 1572 } 1573 1574 public static double clamp180(double value) { 1575 return ((((value + 180.0) % 360.0) + 360.0) % 360.0) - 180.0; 1576 } 1577 1578 public static double clamp360(double value) { 1579 boolean positive = value > 0.0; 1580 value = ((value % 360.0) + 360.0) % 360.0; 1581 if ((value == 0.0) && positive) { 1582 value = 360.0; 1583 } 1584 return value; 1585 } 1586 } 1587 1588 public class ColorEditor extends AbstractCellEditor implements TableCellEditor, ActionListener { 1589 private Color currentColor = Color.CYAN; 1590 private final JButton button = new JButton(); 1591 private final JColorChooser colorChooser = new JColorChooser(); 1592 private JDialog dialog; 1593 protected static final String EDIT = "edit"; 1594 1595// private final JComboBox combobox = new JComboBox(GuiUtils.COLORS); 1596 1597 public ColorEditor() { 1598 button.setActionCommand(EDIT); 1599 button.addActionListener(this); 1600 button.setBorderPainted(false); 1601 1602// combobox.setActionCommand(EDIT); 1603// combobox.addActionListener(this); 1604// combobox.setBorder(new EmptyBorder(0, 0, 0, 0)); 1605// combobox.setOpaque(true); 1606// ColorRenderer whut = new ColorRenderer(true); 1607// combobox.setRenderer(whut); 1608// 1609// dialog = JColorChooser.createDialog(combobox, "pick a color", true, colorChooser, this, null); 1610 dialog = JColorChooser.createDialog(button, "pick a color", true, colorChooser, this, null); 1611 } 1612 public void actionPerformed(ActionEvent e) { 1613 if (EDIT.equals(e.getActionCommand())) { 1614 //The user has clicked the cell, so 1615 //bring up the dialog. 1616// button.setBackground(currentColor); 1617 colorChooser.setColor(currentColor); 1618 dialog.setVisible(true); 1619 1620 //Make the renderer reappear. 1621 fireEditingStopped(); 1622 1623 } else { //User pressed dialog's "OK" button. 1624 currentColor = colorChooser.getColor(); 1625 } 1626 } 1627 1628 //Implement the one CellEditor method that AbstractCellEditor doesn't. 1629 public Object getCellEditorValue() { 1630 return currentColor; 1631 } 1632 1633 //Implement the one method defined by TableCellEditor. 1634 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 1635 currentColor = (Color)value; 1636 return button; 1637// return combobox; 1638 } 1639 } 1640 1641 public class ColorRenderer extends JLabel implements TableCellRenderer, ListCellRenderer { 1642 Border unselectedBorder = null; 1643 Border selectedBorder = null; 1644 boolean isBordered = true; 1645 1646 public ColorRenderer(boolean isBordered) { 1647 this.isBordered = isBordered; 1648 setHorizontalAlignment(CENTER); 1649 setVerticalAlignment(CENTER); 1650 setOpaque(true); 1651 } 1652 1653 public Component getTableCellRendererComponent(JTable table, Object color, boolean isSelected, boolean hasFocus, int row, int column) { 1654 Color newColor = (Color)color; 1655 setBackground(newColor); 1656 if (isBordered) { 1657 if (isSelected) { 1658 if (selectedBorder == null) { 1659 selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, table.getSelectionBackground()); 1660 } 1661 setBorder(selectedBorder); 1662 } else { 1663 if (unselectedBorder == null) { 1664 unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, table.getBackground()); 1665 } 1666 setBorder(unselectedBorder); 1667 } 1668 } 1669 1670 setToolTipText(String.format("RGB: red=%d, green=%d, blue=%d", newColor.getRed(), newColor.getGreen(), newColor.getBlue())); 1671 return this; 1672 } 1673 1674 public Component getListCellRendererComponent(JList list, Object color, int index, boolean isSelected, boolean cellHasFocus) { 1675 Color newColor = (Color)color; 1676 setBackground(newColor); 1677 if (isBordered) { 1678 if (isSelected) { 1679 if (selectedBorder == null) { 1680 selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, list.getSelectionBackground()); 1681 } 1682 setBorder(selectedBorder); 1683 } else { 1684 if (unselectedBorder == null) { 1685 unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, list.getBackground()); 1686 } 1687 setBorder(unselectedBorder); 1688 } 1689 } 1690 setToolTipText(String.format("RGB: red=%d, green=%d, blue=%d", newColor.getRed(), newColor.getGreen(), newColor.getBlue())); 1691 return this; 1692 } 1693 } 1694 1695 public class HackyDragDropRowUI extends BasicTableUI { 1696 1697 private boolean inDrag = false; 1698 private int start; 1699 private int offset; 1700 1701 protected MouseInputListener createMouseInputListener() { 1702 return new HackyMouseInputHandler(); 1703 } 1704 1705 public void paint(Graphics g, JComponent c) { 1706 super.paint(g, c); 1707 1708 if (!inDrag) { 1709 return; 1710 } 1711 1712 int width = table.getWidth(); 1713 int height = table.getRowHeight(); 1714 g.setColor(table.getParent().getBackground()); 1715 Rectangle rect = table.getCellRect(table.getSelectedRow(), 0, false); 1716 g.copyArea(rect.x, rect.y, width, height, rect.x, offset); 1717 1718 if (offset < 0) { 1719 g.fillRect(rect.x, rect.y + (height + offset), width, (offset * -1)); 1720 } else { 1721 g.fillRect(rect.x, rect.y, width, offset); 1722 } 1723 } 1724 1725 class HackyMouseInputHandler extends MouseInputHandler { 1726 1727 public void mouseDragged(MouseEvent e) { 1728 int row = table.getSelectedRow(); 1729 if (row < 0) { 1730 return; 1731 } 1732 1733 inDrag = true; 1734 1735 int height = table.getRowHeight(); 1736 int middleOfSelectedRow = (height * row) + (height / 2); 1737 1738 int toRow = -1; 1739 int yLoc = (int)e.getPoint().getY(); 1740 1741 // goin' up? 1742 if (yLoc < (middleOfSelectedRow - height)) { 1743 toRow = row - 1; 1744 } else if (yLoc > (middleOfSelectedRow + height)) { 1745 toRow = row + 1; 1746 } 1747 1748 ProbeTableModel model = (ProbeTableModel)table.getModel(); 1749 if ((toRow >= 0) && (toRow < table.getRowCount())) { 1750 model.moveRow(row, toRow); 1751 table.setRowSelectionInterval(toRow, toRow); 1752 start = yLoc; 1753 } 1754 1755 offset = (start - yLoc) * -1; 1756 table.repaint(); 1757 } 1758 1759 public void mousePressed(MouseEvent e) { 1760 super.mousePressed(e); 1761 start = (int)e.getPoint().getY(); 1762 } 1763 1764 public void mouseReleased(MouseEvent e){ 1765 super.mouseReleased(e); 1766 inDrag = false; 1767 table.repaint(); 1768 } 1769 } 1770 } 1771}