001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2015 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 http://www.gnu.org/licenses. 027 */ 028 029package edu.wisc.ssec.mcidasv.control; 030 031import static java.util.Objects.requireNonNull; 032 033import java.awt.Color; 034import java.awt.Component; 035import java.awt.Container; 036import java.awt.Dimension; 037import java.awt.Graphics; 038import java.awt.GridBagConstraints; 039import java.awt.Insets; 040import java.awt.Rectangle; 041import java.awt.event.ActionEvent; 042import java.awt.event.ActionListener; 043import java.awt.event.MouseEvent; 044import java.awt.geom.Rectangle2D; 045import java.rmi.RemoteException; 046import java.text.DecimalFormat; 047import java.util.ArrayList; 048import java.util.Collections; 049import java.util.Hashtable; 050import java.util.LinkedHashMap; 051import java.util.List; 052import java.util.Map; 053 054import javax.swing.AbstractCellEditor; 055import javax.swing.BorderFactory; 056import javax.swing.JButton; 057import javax.swing.JCheckBox; 058import javax.swing.JColorChooser; 059import javax.swing.JComboBox; 060import javax.swing.JComponent; 061import javax.swing.JDialog; 062import javax.swing.JLabel; 063import javax.swing.JList; 064import javax.swing.JPanel; 065import javax.swing.JScrollPane; 066import javax.swing.JTabbedPane; 067import javax.swing.JTable; 068import javax.swing.JTextField; 069import javax.swing.ListCellRenderer; 070import javax.swing.border.Border; 071import javax.swing.event.ListSelectionEvent; 072import javax.swing.event.ListSelectionListener; 073import javax.swing.event.MouseInputListener; 074import javax.swing.plaf.basic.BasicTableUI; 075import javax.swing.table.AbstractTableModel; 076import javax.swing.table.TableCellEditor; 077import javax.swing.table.TableCellRenderer; 078 079import org.slf4j.Logger; 080import org.slf4j.LoggerFactory; 081 082import ucar.unidata.idv.IdvConstants; 083import visad.DataReference; 084import visad.DataReferenceImpl; 085import visad.FlatField; 086import visad.RealTuple; 087import visad.Unit; 088import visad.VisADException; 089import visad.georef.MapProjection; 090 091import ucar.unidata.data.DataChoice; 092import ucar.unidata.data.DataSelection; 093import ucar.unidata.idv.DisplayControl; 094import ucar.unidata.idv.DisplayConventions; 095import ucar.unidata.idv.ViewManager; 096import ucar.unidata.idv.control.ControlWidget; 097import ucar.unidata.idv.control.WrapperWidget; 098import ucar.unidata.idv.ui.ParamDefaultsEditor; 099import ucar.unidata.util.ColorTable; 100import ucar.unidata.util.GuiUtils; 101import ucar.unidata.util.LogUtil; 102import ucar.unidata.util.Range; 103import ucar.visad.display.DisplayMaster; 104import ucar.visad.display.DisplayableData; 105 106import edu.wisc.ssec.mcidasv.Constants; 107import edu.wisc.ssec.mcidasv.McIDASV; 108import edu.wisc.ssec.mcidasv.data.hydra.HydraRGBDisplayable; 109import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralData; 110import edu.wisc.ssec.mcidasv.data.hydra.MultiSpectralDataSource; 111import edu.wisc.ssec.mcidasv.data.hydra.SpectrumAdapter; 112import edu.wisc.ssec.mcidasv.display.hydra.MultiSpectralDisplay; 113import edu.wisc.ssec.mcidasv.probes.ProbeEvent; 114import edu.wisc.ssec.mcidasv.probes.ProbeListener; 115import edu.wisc.ssec.mcidasv.probes.ReadoutProbe; 116 117public class MultiSpectralControl extends HydraControl { 118 119 private static final Logger logger = LoggerFactory.getLogger(MultiSpectralControl.class); 120 121 private String PARAM = "BrightnessTemp"; 122 123 // So MultiSpectralDisplay can consistently update the wavelength label 124 // Note hacky leading spaces - needed because GUI builder does not 125 // accept a horizontal strut component. 126 public static String WAVENUMLABEL = " Wavelength: "; 127 private JLabel wavelengthLabel = new JLabel(); 128 129 private static final int DEFAULT_FLAGS = 130 FLAG_COLORTABLE | FLAG_ZPOSITION; 131 132 private MultiSpectralDisplay display; 133 134 private DisplayMaster displayMaster; 135 136 private final JTextField wavenumbox = 137 new JTextField(Float.toString(0f), 12); 138 139 final JTextField minBox = new JTextField(6); 140 final JTextField maxBox = new JTextField(6); 141 142 private final List<Hashtable<String, Object>> spectraProperties = new ArrayList<>(); 143 private final List<Spectrum> spectra = new ArrayList<>(); 144 145 private McIDASVHistogramWrapper histoWrapper; 146 147 private float rangeMin; 148 private float rangeMax; 149 150 // REALLY not thrilled with this... 151 private int probesSeen = 0; 152 153 // boring UI stuff 154 private final JTable probeTable = new JTable(new ProbeTableModel(this, spectra)); 155 private final JScrollPane scrollPane = new JScrollPane(probeTable); 156 private final JButton addProbe = new JButton("Add Probe"); 157 private final JButton removeProbe = new JButton("Remove Probe"); 158 private JCheckBox use360Box; 159 160 public MultiSpectralControl() { 161 super(); 162 setHelpUrl("idv.controls.hydra.multispectraldisplaycontrol"); 163 } 164 165 @Override public boolean init(final DataChoice choice) 166 throws VisADException, RemoteException 167 { 168 ((McIDASV)getIdv()).getMcvDataManager().setHydraControl(choice, this); 169 Hashtable props = choice.getProperties(); 170 PARAM = (String) props.get(MultiSpectralDataSource.paramKey); 171 172 List<DataChoice> choices = Collections.singletonList(choice); 173 histoWrapper = new McIDASVHistogramWrapper("histo", choices, this); 174 175 Float fieldSelectorChannel = 176 (Float)getDataSelection().getProperty(Constants.PROP_CHAN); 177 178 display = new MultiSpectralDisplay(this); 179 180 if (fieldSelectorChannel != null) { 181 display.setWaveNumber(fieldSelectorChannel); 182 } 183 184 displayMaster = getViewManager().getMaster(); 185 186 // map the data choice to display. 187 ((McIDASV)getIdv()).getMcvDataManager().setHydraDisplay(choice, display); 188 189 // initialize the Displayable with data before adding to DisplayControl 190 DisplayableData imageDisplay = display.getImageDisplay(); 191 FlatField image = display.getImageData(); 192 193 float[] rngvals = (image.getFloats(false))[0]; 194 float[] minmax = minmax(rngvals); 195 rangeMin = minmax[0]; 196 rangeMax = minmax[1]; 197 198 imageDisplay.setData(display.getImageData()); 199 addDisplayable(imageDisplay, DEFAULT_FLAGS); 200 201 // put the multispectral display into the layer controls 202 addViewManager(display.getViewManager()); 203 204 // tell the idv what options to give the user 205 setAttributeFlags(DEFAULT_FLAGS); 206 207 setProjectionInView(true); 208 209 // handle the user trying to add a new probe 210 addProbe.addActionListener(new ActionListener() { 211 public void actionPerformed(final ActionEvent e) { 212 addSpectrum(Color.YELLOW); 213 probeTable.revalidate(); 214 } 215 }); 216 217 // handle the user trying to remove an existing probe 218 removeProbe.addActionListener(new ActionListener() { 219 public void actionPerformed(final ActionEvent e) { 220 int index = probeTable.getSelectedRow(); 221 if (index == -1) { 222 return; 223 } 224 225 removeSpectrum(index); 226 } 227 }); 228 removeProbe.setEnabled(false); 229 230 // set up the table. in particular, enable/disable the remove button 231 // depending on whether or not there is a selected probe to remove. 232 probeTable.setDefaultRenderer(Color.class, new ColorRenderer(true)); 233 probeTable.setDefaultEditor(Color.class, new ColorEditor()); 234 probeTable.setPreferredScrollableViewportSize(new Dimension(500, 200)); 235 probeTable.setUI(new HackyDragDropRowUI()); 236 probeTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { 237 public void valueChanged(final ListSelectionEvent e) { 238 if (!probeTable.getSelectionModel().isSelectionEmpty()) { 239 removeProbe.setEnabled(true); 240 } else { 241 removeProbe.setEnabled(false); 242 } 243 } 244 }); 245 246 final boolean use360 = getIdv().getStore().get(Constants.PROP_HYDRA_360, false); 247 use360Box = new JCheckBox("0-360 Longitude Format", use360); 248 use360Box.addActionListener(new ActionListener() { 249 @Override public void actionPerformed(ActionEvent e) { 250 getIdv().getStore().put(Constants.PROP_HYDRA_360, use360Box.isSelected()); 251 ProbeTableModel model = (ProbeTableModel)probeTable.getModel(); 252 model.updateWith(spectra); 253 model.fireTableDataChanged(); 254 } 255 }); 256 257 setShowInDisplayList(false); 258 259 return true; 260 } 261 262 /** 263 * Updates the Wavelength label when user manipulates drag line UI 264 * 265 * @param s full label text, prefix and numeric value 266 * 267 */ 268 public void setWavelengthLabel(String s) { 269 if (s != null) { 270 wavelengthLabel.setText(s); 271 } 272 return; 273 } 274 275 @Override public void initDone() { 276 try { 277 display.showChannelSelector(); 278 279 // TODO: this is ugly. 280 Float fieldSelectorChannel = 281 (Float)getDataSelection().getProperty(Constants.PROP_CHAN); 282 if (fieldSelectorChannel == null) { 283 fieldSelectorChannel = 0f; 284 } 285 handleChannelChange(fieldSelectorChannel, false); 286 287 displayMaster.setDisplayInactive(); 288 289 // this if-else block is detecting whether or not a bundle is 290 // being loaded; if true, then we'll have a list of spectra props. 291 // otherwise just throw two default spectrums/probes on the screen. 292 if (!spectraProperties.isEmpty()) { 293 for (Hashtable<String, Object> table : spectraProperties) { 294 Color c = (Color)table.get("color"); 295 Spectrum s = addSpectrum(c); 296 s.setProperties(table); 297 } 298 spectraProperties.clear(); 299 } else { 300 addSpectra(Color.MAGENTA, Color.CYAN); 301 } 302 displayMaster.setDisplayActive(); 303 } catch (Exception e) { 304 logException("MultiSpectralControl.initDone", e); 305 } 306 } 307 308 /** 309 * Overridden by McIDAS-V so that {@literal "hide"} probes when their display 310 * is turned off. Otherwise users can wind up with probes on the screen which 311 * aren't associated with any displayed data. 312 * 313 * @param on {@code true} if we're visible, {@code false} otherwise. 314 * 315 * @see DisplayControl#setDisplayVisibility(boolean) 316 */ 317 318 @Override public void setDisplayVisibility(boolean on) { 319 super.setDisplayVisibility(on); 320 for (Spectrum s : spectra) { 321 if (s.isVisible()) { 322 s.getProbe().quietlySetVisible(on); 323 } 324 } 325 } 326 327 // this will get called before init() by the IDV's bundle magic. 328 public void setSpectraProperties(final List<Hashtable<String, Object>> props) { 329 spectraProperties.clear(); 330 spectraProperties.addAll(props); 331 } 332 333 public List<Hashtable<String, Object>> getSpectraProperties() { 334 List<Hashtable<String, Object>> props = new ArrayList<>(spectra.size()); 335 for (Spectrum s : spectra) { 336 props.add(s.getProperties()); 337 } 338 return props; 339 } 340 341 protected void updateList(final List<Spectrum> updatedSpectra) { 342 spectra.clear(); 343 344 List<String> dataRefIds = new ArrayList<>(updatedSpectra.size()); 345 for (Spectrum spectrum : updatedSpectra) { 346 dataRefIds.add(spectrum.getSpectrumRefName()); 347 spectra.add(spectrum); 348 } 349 display.reorderDataRefsById(dataRefIds); 350 } 351 352 /** 353 * Uses a variable-length array of {@link Color}s to create new readout 354 * probes using the specified colors. 355 * 356 * @param colors Variable length array of {@code Color}s. Shouldn't be 357 * {@code null}. 358 */ 359 // TODO(jon): check for null. 360 protected void addSpectra(final Color... colors) { 361 Spectrum currentSpectrum = null; 362 try { 363 for (int i = colors.length-1; i >= 0; i--) { 364 probesSeen++; 365 Color color = colors[i]; 366 String id = "Probe "+probesSeen; 367 currentSpectrum = new Spectrum(this, color, id); 368 spectra.add(currentSpectrum); 369 } 370 ((ProbeTableModel)probeTable.getModel()).updateWith(spectra); 371 } catch (Exception e) { 372 LogUtil.logException("MultiSpectralControl.addSpectra: error while adding spectra", e); 373 } 374 } 375 376 /** 377 * Creates a new {@link ReadoutProbe} with the specified {@link Color}. 378 * 379 * @param color {@code Color} of the new {@code ReadoutProbe}. 380 * {@code null} values are not allowed. 381 * 382 * @return {@link Spectrum} wrapper for the newly created 383 * {@code ReadoutProbe}. 384 * 385 * @throws NullPointerException if {@code color} is {@code null}. 386 */ 387 public Spectrum addSpectrum(final Color color) { 388 Spectrum spectrum = null; 389 try { 390 probesSeen++; 391 String id = "Probe "+probesSeen; 392 spectrum = new Spectrum(this, color, id); 393 spectra.add(spectrum); 394 } catch (Exception e) { 395 LogUtil.logException("MultiSpectralControl.addSpectrum: error creating new spectrum", e); 396 } 397 ((ProbeTableModel)probeTable.getModel()).updateWith(spectra); 398 return spectrum; 399 } 400 401 /** 402 * Attempts to remove the {@link Spectrum} at the given {@code index}. 403 * 404 * @param index Index of the probe to be removed (within {@link #spectra}). 405 */ 406 public void removeSpectrum(final int index) { 407 List<Spectrum> newSpectra = new ArrayList<>(spectra); 408 int mappedIndex = newSpectra.size() - (index + 1); 409 Spectrum removed = newSpectra.get(mappedIndex); 410 newSpectra.remove(mappedIndex); 411 try { 412 removed.removeValueDisplay(); 413 } catch (Exception e) { 414 LogUtil.logException("MultiSpectralControl.removeSpectrum: error removing spectrum", e); 415 } 416 417 updateList(newSpectra); 418 419 // need to signal that the table should update? 420 ProbeTableModel model = (ProbeTableModel)probeTable.getModel(); 421 model.updateWith(newSpectra); 422 probeTable.revalidate(); 423 } 424 425 /** 426 * Iterates through the list of {@link Spectrum}s that manage each 427 * {@link ReadoutProbe} associated with this display control and calls 428 * {@link Spectrum#removeValueDisplay()} in an effort to remove this 429 * control's probes. 430 * 431 * @see #spectra 432 */ 433 public void removeSpectra() { 434 try { 435 for (Spectrum s : spectra) { 436 s.removeValueDisplay(); 437 } 438 } catch (Exception e) { 439 LogUtil.logException("MultiSpectralControl.removeSpectra: error removing spectrum", e); 440 } 441 } 442 443 /** 444 * Makes each {@link ReadoutProbe} in this display control attempt to 445 * redisplay its readout value. 446 * 447 * <p>Sometimes the probes don't initialize correctly and this method is 448 * a stop-gap solution. 449 */ 450 public void pokeSpectra() { 451 for (Spectrum s : spectra) { 452 s.pokeValueDisplay(); 453 } 454 try { 455 //-display.refreshDisplay(); 456 } catch (Exception e) { 457 LogUtil.logException("MultiSpectralControl.pokeSpectra: error refreshing display", e); 458 } 459 } 460 461 @Override public DataSelection getDataSelection() { 462 DataSelection selection = super.getDataSelection(); 463 if (display != null) { 464 selection.putProperty(Constants.PROP_CHAN, display.getWaveNumber()); 465 try { 466 selection.putProperty(SpectrumAdapter.channelIndex_name, display.getChannelIndex()); 467 } catch (Exception e) { 468 LogUtil.logException("MultiSpectralControl.getDataSelection", e); 469 } 470 } 471 return selection; 472 } 473 474 @Override public void setDataSelection(final DataSelection newSelection) { 475 super.setDataSelection(newSelection); 476 } 477 478 @Override public MapProjection getDataProjection() { 479 MapProjection mp = null; 480 Rectangle2D rect = 481 MultiSpectralData.getLonLatBoundingBox(display.getImageData()); 482 483 try { 484 mp = new LambertAEA(rect); 485 } catch (Exception e) { 486 logException("MultiSpectralControl.getDataProjection", e); 487 } 488 489 return mp; 490 } 491 492 public static float[] minmax(float[] values) { 493 float min = Float.MAX_VALUE; 494 float max = -Float.MAX_VALUE; 495 for (int k = 0; k < values.length; k++) { 496 float val = values[k]; 497 if ((val == val) && (val < Float.POSITIVE_INFINITY) && (val > Float.NEGATIVE_INFINITY)) { 498 if (val < min) { 499 min = val; 500 } 501 if (val > max) { 502 max = val; 503 } 504 } 505 } 506 return new float[] { min, max }; 507 } 508 509 /** 510 * Convenience method for extracting the parameter name. 511 * 512 * @return Results from {@link DataChoice#getName()}, or {@link #PARAM} if 513 * the {@code DataChoice} is (somehow) {@code null}. 514 */ 515 private String getParameterName() { 516 String parameterName = PARAM; 517 DataChoice choice = getDataChoice(); 518 if (choice != null) { 519 parameterName = choice.getName(); 520 } 521 return parameterName; 522 } 523 524 /** 525 * Get the initial {@link Range} for the data and color table. 526 * 527 * <p>Note: if there is a parameter default range associated with the 528 * current parameter name, that will be returned. If there is <b>not</b> a 529 * parameter default range match, a {@code Range} consisting of 530 * {@link #rangeMin} and {@link #rangeMax} will be returned. 531 * </p> 532 * 533 * @return Initial {@code Range} for data and color table. 534 * 535 * @throws VisADException if VisAD had problems. 536 * @throws RemoteException if there was a Java RMI problem. 537 */ 538 @Override protected Range getInitialRange() throws VisADException, 539 RemoteException 540 { 541 String parameterName = getParameterName(); 542 Unit dispUnit = getDisplayUnit(); 543 DisplayConventions conventions = getDisplayConventions(); 544 Range paramRange = conventions.getParamRange(parameterName, dispUnit); 545 if (paramRange == null) { 546 paramRange = new Range(rangeMin, rangeMax); 547 } 548 return paramRange; 549 } 550 551 /** 552 * Get the initial {@link ColorTable} associated with this control's 553 * parameter name. 554 * 555 * <p>Note: if there is a parameter default color table associated with 556 * the parameter name, that color table will be returned. If there are 557 * <b>no</b> parameter defaults associated with the parameter name, 558 * then the {@code ColorTable} associated with {@literal "BrightnessTemp"} 559 * is returned (this is a {@literal "legacy"} behavior). 560 * </p> 561 * 562 * @return {@code ColorTable} to use. 563 */ 564 @Override protected ColorTable getInitialColorTable() { 565 String parameterName = getParameterName(); 566 DisplayConventions conventions = getDisplayConventions(); 567 ParamDefaultsEditor defaults = conventions.getParamDefaultsEditor(); 568 ColorTable ct = defaults.getParamColorTable(parameterName, false); 569 if (ct == null) { 570 ct = conventions.getParamColorTable(PARAM); 571 } 572 return ct; 573 } 574 575 @Override public Container doMakeContents() { 576 try { 577 JTabbedPane pane = new JTabbedPane(); 578 pane.add("Display", GuiUtils.inset(getDisplayTab(), 5)); 579 pane.add("Settings", 580 GuiUtils.inset(GuiUtils.top(doMakeWidgetComponent()), 5)); 581 pane.add("Histogram", GuiUtils.inset(GuiUtils.top(getHistogramTabComponent()), 5)); 582 GuiUtils.handleHeavyWeightComponentsInTabs(pane); 583 return pane; 584 } catch (Exception e) { 585 logException("MultiSpectralControl.doMakeContents", e); 586 } 587 return null; 588 } 589 590 @Override public void doRemove() throws VisADException, RemoteException { 591 // forcibly clear the value displays when the user has elected to kill 592 // the display. the readouts will persist otherwise. 593 removeSpectra(); 594 super.doRemove(); 595 } 596 597 /** 598 * Runs through the list of ViewManager-s and tells each to destroy. 599 * Creates a new viewManagers list. 600 */ 601 @Override protected void clearViewManagers() { 602 if (viewManagers == null) { 603 return; 604 } 605 606 List<ViewManager> tmp = new ArrayList<>(viewManagers); 607 viewManagers = null; 608 for (ViewManager vm : tmp) { 609 if (vm != null) { 610 vm.destroy(); 611 } 612 } 613 } 614 615 @SuppressWarnings("unchecked") 616 @Override protected JComponent doMakeWidgetComponent() { 617 List<Component> widgetComponents; 618 try { 619 List<ControlWidget> controlWidgets = new ArrayList<>(5); 620 getControlWidgets(controlWidgets); 621 controlWidgets.add(new WrapperWidget(this, GuiUtils.rLabel("Readout Probes:"), scrollPane)); 622 controlWidgets.add(new WrapperWidget(this, GuiUtils.rLabel(" "), GuiUtils.hbox(addProbe, removeProbe, GuiUtils.right(use360Box)))); 623 widgetComponents = ControlWidget.fillList(controlWidgets); 624 } catch (Exception e) { 625 LogUtil.logException("Problem building the MultiSpectralControl settings", e); 626 widgetComponents = new ArrayList<>(5); 627 widgetComponents.add(new JLabel("Error building component...")); 628 } 629 630 GuiUtils.tmpInsets = new Insets(4, 8, 4, 8); 631 GuiUtils.tmpFill = GridBagConstraints.HORIZONTAL; 632 return GuiUtils.doLayout(widgetComponents, 2, GuiUtils.WT_NY, GuiUtils.WT_N); 633 } 634 635 protected MultiSpectralDisplay getMultiSpectralDisplay() { 636 return display; 637 } 638 639 public boolean updateImage(final float newChan) { 640 if (!display.setWaveNumber(newChan)) { 641 return false; 642 } 643 644 DisplayableData imageDisplay = display.getImageDisplay(); 645 646 // mark the color map as needing an auto scale, these calls 647 // are needed because a setRange could have been called which 648 // locks out auto scaling. 649 ((HydraRGBDisplayable)imageDisplay).getColorMap().resetAutoScale(); 650 displayMaster.reScale(); 651 652 try { 653 FlatField image = display.getImageData(); 654 displayMaster.setDisplayInactive(); //try to consolidate display transforms 655 imageDisplay.setData(image); 656 pokeSpectra(); 657 displayMaster.setDisplayActive(); 658 updateHistogramTab(); 659 } catch (Exception e) { 660 LogUtil.logException("MultiSpectralControl.updateImage", e); 661 return false; 662 } 663 664 return true; 665 } 666 667 // be sure to update the displayed image even if a channel change 668 // originates from the msd itself. 669 @Override public void handleChannelChange(final float newChan) { 670 handleChannelChange(newChan, true); 671 } 672 673 public void handleChannelChange(final float newChan, boolean update) { 674 if (update) { 675 if (updateImage(newChan)) { 676 wavenumbox.setText(Float.toString(newChan)); 677 } 678 } else { 679 wavenumbox.setText(Float.toString(newChan)); 680 } 681 } 682 683 private JComponent getDisplayTab() { 684 List<JComponent> compList = new ArrayList<>(5); 685 686 if (display.getBandSelectComboBox() == null) { 687 final JLabel nameLabel = GuiUtils.rLabel("Wavenumber: "); 688 689 wavenumbox.addActionListener(new ActionListener() { 690 public void actionPerformed(ActionEvent e) { 691 String tmp = wavenumbox.getText().trim(); 692 updateImage(Float.valueOf(tmp)); 693 } 694 }); 695 compList.add(nameLabel); 696 compList.add(wavenumbox); 697 } else { 698 final JComboBox bandBox = display.getBandSelectComboBox(); 699 bandBox.addActionListener(new ActionListener() { 700 public void actionPerformed(ActionEvent e) { 701 String bandName = (String) bandBox.getSelectedItem(); 702 Float channel = (Float)display.getMultiSpectralData().getBandNameMap().get(bandName); 703 updateImage(channel.floatValue()); 704 } 705 }); 706 JLabel nameLabel = new JLabel("Band: "); 707 compList.add(nameLabel); 708 compList.add(bandBox); 709 compList.add(wavelengthLabel); 710 } 711 712 JPanel waveNo = GuiUtils.center(GuiUtils.doLayout(compList, 3, GuiUtils.WT_N, GuiUtils.WT_N)); 713 return GuiUtils.centerBottom(display.getDisplayComponent(), waveNo); 714 } 715 716 private JComponent getHistogramTabComponent() { 717 updateHistogramTab(); 718 JComponent histoComp = histoWrapper.doMakeContents(); 719 JLabel rangeLabel = GuiUtils.rLabel("Range "); 720 JLabel minLabel = GuiUtils.rLabel("Min"); 721 JLabel maxLabel = GuiUtils.rLabel(" Max"); 722 List<JComponent> rangeComps = new ArrayList<>(); 723 rangeComps.add(rangeLabel); 724 rangeComps.add(minLabel); 725 rangeComps.add(minBox); 726 rangeComps.add(maxLabel); 727 rangeComps.add(maxBox); 728 minBox.addActionListener(new ActionListener() { 729 public void actionPerformed(ActionEvent ae) { 730 rangeMin = Float.valueOf(minBox.getText().trim()); 731 rangeMax = Float.valueOf(maxBox.getText().trim()); 732 histoWrapper.modifyRange((int)rangeMin, (int)rangeMax); 733 } 734 }); 735 maxBox.addActionListener(new ActionListener() { 736 public void actionPerformed(ActionEvent ae) { 737 rangeMin = Float.valueOf(minBox.getText().trim()); 738 rangeMax = Float.valueOf(maxBox.getText().trim()); 739 histoWrapper.modifyRange((int)rangeMin, (int)rangeMax); 740 } 741 }); 742 JPanel rangePanel = 743 GuiUtils.center(GuiUtils.doLayout(rangeComps, 5, GuiUtils.WT_N, GuiUtils.WT_N)); 744 JButton resetButton = new JButton("Reset"); 745 resetButton.addActionListener(new ActionListener() { 746 public void actionPerformed(ActionEvent ae) { 747 resetColorTable(); 748 } 749 }); 750 751 JPanel resetPanel = 752 GuiUtils.center(GuiUtils.inset(GuiUtils.wrap(resetButton), 4)); 753 754 return GuiUtils.topCenterBottom(histoComp, rangePanel, resetPanel); 755 } 756 757 private void updateHistogramTab() { 758 try { 759 histoWrapper.loadData(display.getImageData()); 760 org.jfree.data.Range range = histoWrapper.getRange(); 761 rangeMin = (float)range.getLowerBound(); 762 rangeMax = (float)range.getUpperBound(); 763 minBox.setText(Integer.toString((int)rangeMin)); 764 maxBox.setText(Integer.toString((int)rangeMax)); 765 } catch (Exception e) { 766 logException("MultiSpectralControl.getHistogramTabComponent", e); 767 } 768 } 769 770 public void resetColorTable() { 771 histoWrapper.doReset(); 772 } 773 774 protected void contrastStretch(final double low, final double high) { 775 try { 776 org.jfree.data.Range range = histoWrapper.getRange(); 777 rangeMin = (float)range.getLowerBound(); 778 rangeMax = (float)range.getUpperBound(); 779 minBox.setText(Integer.toString((int)rangeMin)); 780 maxBox.setText(Integer.toString((int)rangeMax)); 781 setRange(getInitialColorTable().getName(), new Range(low, high)); 782 } catch (Exception e) { 783 logException("MultiSpectralControl.contrastStretch", e); 784 } 785 } 786 787 private static class Spectrum implements ProbeListener { 788 789 private static final Logger logger = LoggerFactory.getLogger(Spectrum.class); 790 791 private final MultiSpectralControl control; 792 793 /** 794 * Display that is displaying the spectrum associated with 795 * {@code probe}'s location. 796 */ 797 private final MultiSpectralDisplay display; 798 799 /** VisAD's reference to this spectrum. */ 800 private final DataReference spectrumRef; 801 802 /** 803 * Probe that appears in the {@literal "image display"} associated with 804 * the current display control. 805 */ 806 private ReadoutProbe probe; 807 808 /** Whether or not {@code probe} is visible. */ 809 private boolean isVisible = true; 810 811 /** 812 * Human-friendly ID for this spectrum and probe. Used in 813 * {@link MultiSpectralControl#probeTable}. 814 */ 815 private final String myId; 816 817 /** 818 * Initializes a new Spectrum that is {@literal "bound"} to 819 * {@code control} and whose color is {@code color}. 820 * 821 * @param control Display control that contains this spectrum and the 822 * associated {@link ReadoutProbe}. Cannot be null. 823 * @param color Color of {@code probe}. Cannot be {@code null}. 824 * @param myId Human-friendly ID used a reference for this 825 * spectrum/probe. Cannot be {@code null}. 826 * 827 * @throws NullPointerException if {@code control}, {@code color}, or 828 * {@code myId} is {@code null}. 829 * @throws VisADException if VisAD-land had some problems. 830 * @throws RemoteException if VisAD's RMI stuff had problems. 831 */ 832 public Spectrum(final MultiSpectralControl control, final Color color, final String myId) throws VisADException, RemoteException { 833 this.control = control; 834 this.display = control.getMultiSpectralDisplay(); 835 this.myId = myId; 836 spectrumRef = new DataReferenceImpl(hashCode() + "_spectrumRef"); 837 display.addRef(spectrumRef, color); 838 String pattern = (String)control.getStore().get(IdvConstants.PREF_LATLON_FORMAT, "##0.0"); 839 probe = new ReadoutProbe(control.getNavigatedDisplay(), display.getImageData(), color, pattern, control.getDisplayVisibility()); 840 this.updatePosition(probe.getEarthPosition()); 841 probe.addProbeListener(this); 842 } 843 844 public void probePositionChanged(final ProbeEvent<RealTuple> e) { 845 RealTuple position = e.getNewValue(); 846 updatePosition(position); 847 } 848 849 public void probeFormatPatternChanged(final ProbeEvent<String> e) { 850 851 } 852 853 public void updatePosition(RealTuple position) { 854 try { 855 FlatField spectrum = display.getMultiSpectralData().getSpectrum(position); 856 spectrumRef.setData(spectrum); 857 } catch (Exception ex) { 858 logger.error("Error updating postion.", ex); 859 } 860 } 861 862 public String getValue() { 863 return probe.getValue(); 864 } 865 866 public double getLatitude() { 867 return probe.getLatitude(); 868 } 869 870 public double getLongitude() { 871 return probe.getLongitude(); 872 } 873 874 public Color getColor() { 875 return probe.getColor(); 876 } 877 878 public String getId() { 879 return myId; 880 } 881 882 public DataReference getSpectrumRef() { 883 return spectrumRef; 884 } 885 886 public String getSpectrumRefName() { 887 return hashCode() + "_spectrumRef"; 888 } 889 890 public void setColor(final Color color) { 891 if (color == null) { 892 throw new NullPointerException("Can't use a null color"); 893 } 894 895 try { 896 display.updateRef(spectrumRef, color); 897 probe.quietlySetColor(color); 898 } catch (Exception ex) { 899 logger.error("Error setting color", ex); 900 } 901 } 902 903 /** 904 * Shows and hides this spectrum/probe. Note that an {@literal "hidden"} 905 * spectrum merely uses an alpha value of zero for the spectrum's 906 * color--nothing is actually removed! 907 * 908 * <p>Also note that if our {@link MultiSpectralControl} has its visibility 909 * toggled {@literal "off"}, the probe itself will not be shown. 910 * <b>It will otherwise behave as if it is visible!</b> 911 * 912 * @param visible {@code true} for {@literal "visible"}, {@code false} otherwise. 913 */ 914 public void setVisible(final boolean visible) { 915 isVisible = visible; 916 Color c = probe.getColor(); 917 int alpha = visible ? 255 : 0; 918 c = new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha); 919 try { 920 display.updateRef(spectrumRef, c); 921 // only bother actually *showing* the probe if its display is 922 // actually visible. 923 if (control.getDisplayVisibility()) { 924 probe.quietlySetVisible(visible); 925 } 926 } catch (Exception e) { 927 LogUtil.logException("There was a problem setting the visibility of probe \""+spectrumRef+"\" to "+visible, e); 928 } 929 } 930 931 public boolean isVisible() { 932 return isVisible; 933 } 934 935 protected ReadoutProbe getProbe() { 936 return probe; 937 } 938 939 public void probeColorChanged(final ProbeEvent<Color> e) { 940 logger.trace("color change event={}", e); 941 } 942 943 public void probeVisibilityChanged(final ProbeEvent<Boolean> e) { 944 logger.trace("probe event={}", e); 945 Boolean newVal = e.getNewValue(); 946 if (newVal != null) { 947 isVisible = newVal; 948 } 949 } 950 951 public Hashtable<String, Object> getProperties() { 952 Hashtable<String, Object> table = new Hashtable<>(); 953 table.put("color", probe.getColor()); 954 table.put("visibility", isVisible); 955 table.put("lat", probe.getLatitude()); 956 table.put("lon", probe.getLongitude()); 957 return table; 958 } 959 960 public void setProperties(final Hashtable<String, Object> table) { 961 if (table == null) { 962 throw new NullPointerException("properties table cannot be null"); 963 } 964 Color color = (Color)table.get("color"); 965 Double lat = (Double)table.get("lat"); 966 Double lon = (Double)table.get("lon"); 967 Boolean visibility = (Boolean)table.get("visibility"); 968 probe.setLatLon(lat, lon); 969 probe.setColor(color); 970 setVisible(visibility); 971 } 972 973 public void pokeValueDisplay() { 974 probe.setField(display.getImageData()); 975 try { 976 //FlatField spectrum = display.getMultiSpectralData().getSpectrum(probe.getEarthPosition()); 977 //spectrumRef.setData(spectrum); 978 } catch (Exception e) { } 979 } 980 981 public void removeValueDisplay() throws VisADException, RemoteException { 982 probe.handleProbeRemoval(); 983 display.removeRef(spectrumRef); 984 } 985 } 986 987 // TODO(jon): MultiSpectralControl should become the table model. 988 private static class ProbeTableModel extends AbstractTableModel implements ProbeListener { 989// private static final String[] COLUMNS = { 990// "Visibility", "Probe ID", "Value", "Spectrum", "Latitude", "Longitude", "Color" 991// }; 992 993 private static final String[] COLUMNS = { 994 "Visibility", "Probe ID", "Value", "Latitude", "Longitude", "Color" 995 }; 996 997 private final Map<ReadoutProbe, Integer> probeToIndex = new LinkedHashMap<>(); 998 private final Map<Integer, Spectrum> indexToSpectrum = new LinkedHashMap<>(); 999 private final MultiSpectralControl control; 1000 1001 public ProbeTableModel(final MultiSpectralControl control, final List<Spectrum> probes) { 1002 this.control = requireNonNull(control); 1003 updateWith(requireNonNull(probes)); 1004 } 1005 1006 public void probeColorChanged(final ProbeEvent<Color> e) { 1007 ReadoutProbe probe = e.getProbe(); 1008 if (!probeToIndex.containsKey(probe)) { 1009 return; 1010 } 1011 int index = probeToIndex.get(probe); 1012 fireTableCellUpdated(index, 5); 1013 } 1014 1015 public void probeVisibilityChanged(final ProbeEvent<Boolean> e) { 1016 ReadoutProbe probe = e.getProbe(); 1017 if (!probeToIndex.containsKey(probe)) { 1018 return; 1019 } 1020 int index = probeToIndex.get(probe); 1021 fireTableCellUpdated(index, 0); 1022 } 1023 1024 public void probePositionChanged(final ProbeEvent<RealTuple> e) { 1025 ReadoutProbe probe = e.getProbe(); 1026 if (!probeToIndex.containsKey(probe)) { 1027 return; 1028 } 1029 int index = probeToIndex.get(probe); 1030 fireTableRowsUpdated(index, index); 1031 } 1032 1033 public void probeFormatPatternChanged(final ProbeEvent<String> e) { 1034 ReadoutProbe probe = e.getProbe(); 1035 if (!probeToIndex.containsKey(probe)) { 1036 return; 1037 } 1038 int index = probeToIndex.get(probe); 1039 fireTableRowsUpdated(index, index); 1040 } 1041 1042 public void updateWith(final List<Spectrum> updatedSpectra) { 1043 requireNonNull(updatedSpectra); 1044 1045 probeToIndex.clear(); 1046 indexToSpectrum.clear(); 1047 1048 for (int i = 0, j = updatedSpectra.size()-1; i < updatedSpectra.size(); i++, j--) { 1049 Spectrum spectrum = updatedSpectra.get(j); 1050 ReadoutProbe probe = spectrum.getProbe(); 1051 if (!probe.hasListener(this)) { 1052 probe.addProbeListener(this); 1053 } 1054 probeToIndex.put(spectrum.getProbe(), i); 1055 indexToSpectrum.put(i, spectrum); 1056 } 1057 } 1058 1059 public int getColumnCount() { 1060 return COLUMNS.length; 1061 } 1062 1063 public int getRowCount() { 1064 if (probeToIndex.size() != indexToSpectrum.size()) { 1065 throw new AssertionError(""); 1066 } 1067 return probeToIndex.size(); 1068 } 1069 1070// public Object getValueAt(final int row, final int column) { 1071// Spectrum spectrum = indexToSpectrum.get(row); 1072// switch (column) { 1073// case 0: return spectrum.isVisible(); 1074// case 1: return spectrum.getId(); 1075// case 2: return spectrum.getValue(); 1076// case 3: return "notyet"; 1077// case 4: return formatPosition(spectrum.getLatitude()); 1078// case 5: return formatPosition(spectrum.getLongitude()); 1079// case 6: return spectrum.getColor(); 1080// default: throw new AssertionError("uh oh"); 1081// } 1082// } 1083 public Object getValueAt(final int row, final int column) { 1084 DecimalFormat format = new DecimalFormat(control.getIdv().getStore().get(Constants.PREF_LATLON_FORMAT, "##0.0")); 1085 boolean use360 = control.use360Box.isSelected(); 1086 Spectrum spectrum = indexToSpectrum.get(row); 1087 switch (column) { 1088 case 0: return spectrum.isVisible(); 1089 case 1: return spectrum.getId(); 1090 case 2: return spectrum.getValue(); 1091 case 3: return format.format(spectrum.getLatitude()); 1092 case 4: return format.format(use360 ? clamp360(spectrum.getLongitude()) : clamp180(spectrum.getLongitude())); 1093 case 5: return spectrum.getColor(); 1094 default: throw new AssertionError("uh oh"); 1095 } 1096 } 1097 1098 public boolean isCellEditable(final int row, final int column) { 1099 switch (column) { 1100 case 0: return true; 1101 case 5: return true; 1102 default: return false; 1103 } 1104 } 1105 1106 public void setValueAt(final Object value, final int row, final int column) { 1107 Spectrum spectrum = indexToSpectrum.get(row); 1108 boolean didUpdate = true; 1109 switch (column) { 1110 case 0: spectrum.setVisible((Boolean)value); break; 1111 case 5: spectrum.setColor((Color)value); break; 1112 default: didUpdate = false; break; 1113 } 1114 1115 if (didUpdate) { 1116 fireTableCellUpdated(row, column); 1117 } 1118 } 1119 1120 public void moveRow(final int origin, final int destination) { 1121 // get the dragged spectrum (and probe) 1122 Spectrum dragged = indexToSpectrum.get(origin); 1123 ReadoutProbe draggedProbe = dragged.getProbe(); 1124 1125 // get the current spectrum (and probe) 1126 Spectrum current = indexToSpectrum.get(destination); 1127 ReadoutProbe currentProbe = current.getProbe(); 1128 1129 // update references in indexToSpetrum 1130 indexToSpectrum.put(destination, dragged); 1131 indexToSpectrum.put(origin, current); 1132 1133 // update references in probeToIndex 1134 probeToIndex.put(draggedProbe, destination); 1135 probeToIndex.put(currentProbe, origin); 1136 1137 // build a list of the spectra, ordered by index 1138 List<Spectrum> updated = new ArrayList<>(indexToSpectrum.size()); 1139 for (int i = indexToSpectrum.size()-1; i >= 0; i--) { 1140 updated.add(indexToSpectrum.get(i)); 1141 } 1142 1143 // send it to control. 1144 control.updateList(updated); 1145 } 1146 1147 public String getColumnName(final int column) { 1148 return COLUMNS[column]; 1149 } 1150 1151 public Class<?> getColumnClass(final int column) { 1152 return getValueAt(0, column).getClass(); 1153 } 1154 1155 public static double clamp180(double value) { 1156 return ((((value + 180.0) % 360.0) + 360.0) % 360.0) - 180.0; 1157 } 1158 1159 public static double clamp360(double value) { 1160 boolean positive = value > 0.0; 1161 value = ((value % 360.0) + 360.0) % 360.0; 1162 if ((value == 0.0) && positive) { 1163 value = 360.0; 1164 } 1165 return value; 1166 } 1167 } 1168 1169 public class ColorEditor extends AbstractCellEditor implements TableCellEditor, ActionListener { 1170 private Color currentColor = Color.CYAN; 1171 private final JButton button = new JButton(); 1172 private final JColorChooser colorChooser = new JColorChooser(); 1173 private JDialog dialog; 1174 protected static final String EDIT = "edit"; 1175 1176// private final JComboBox combobox = new JComboBox(GuiUtils.COLORS); 1177 1178 public ColorEditor() { 1179 button.setActionCommand(EDIT); 1180 button.addActionListener(this); 1181 button.setBorderPainted(false); 1182 1183// combobox.setActionCommand(EDIT); 1184// combobox.addActionListener(this); 1185// combobox.setBorder(new EmptyBorder(0, 0, 0, 0)); 1186// combobox.setOpaque(true); 1187// ColorRenderer whut = new ColorRenderer(true); 1188// combobox.setRenderer(whut); 1189// 1190// dialog = JColorChooser.createDialog(combobox, "pick a color", true, colorChooser, this, null); 1191 dialog = JColorChooser.createDialog(button, "pick a color", true, colorChooser, this, null); 1192 } 1193 public void actionPerformed(ActionEvent e) { 1194 if (EDIT.equals(e.getActionCommand())) { 1195 //The user has clicked the cell, so 1196 //bring up the dialog. 1197// button.setBackground(currentColor); 1198 colorChooser.setColor(currentColor); 1199 dialog.setVisible(true); 1200 1201 //Make the renderer reappear. 1202 fireEditingStopped(); 1203 1204 } else { //User pressed dialog's "OK" button. 1205 currentColor = colorChooser.getColor(); 1206 } 1207 } 1208 1209 //Implement the one CellEditor method that AbstractCellEditor doesn't. 1210 public Object getCellEditorValue() { 1211 return currentColor; 1212 } 1213 1214 //Implement the one method defined by TableCellEditor. 1215 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 1216 currentColor = (Color)value; 1217 return button; 1218// return combobox; 1219 } 1220 } 1221 1222 public class ColorRenderer extends JLabel implements TableCellRenderer, ListCellRenderer { 1223 Border unselectedBorder = null; 1224 Border selectedBorder = null; 1225 boolean isBordered = true; 1226 1227 public ColorRenderer(boolean isBordered) { 1228 this.isBordered = isBordered; 1229 setHorizontalAlignment(CENTER); 1230 setVerticalAlignment(CENTER); 1231 setOpaque(true); 1232 } 1233 1234 public Component getTableCellRendererComponent(JTable table, Object color, boolean isSelected, boolean hasFocus, int row, int column) { 1235 Color newColor = (Color)color; 1236 setBackground(newColor); 1237 if (isBordered) { 1238 if (isSelected) { 1239 if (selectedBorder == null) { 1240 selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, table.getSelectionBackground()); 1241 } 1242 setBorder(selectedBorder); 1243 } else { 1244 if (unselectedBorder == null) { 1245 unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, table.getBackground()); 1246 } 1247 setBorder(unselectedBorder); 1248 } 1249 } 1250 1251 setToolTipText(String.format("RGB: red=%d, green=%d, blue=%d", newColor.getRed(), newColor.getGreen(), newColor.getBlue())); 1252 return this; 1253 } 1254 1255 public Component getListCellRendererComponent(JList list, Object color, int index, boolean isSelected, boolean cellHasFocus) { 1256 Color newColor = (Color)color; 1257 setBackground(newColor); 1258 if (isBordered) { 1259 if (isSelected) { 1260 if (selectedBorder == null) { 1261 selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, list.getSelectionBackground()); 1262 } 1263 setBorder(selectedBorder); 1264 } else { 1265 if (unselectedBorder == null) { 1266 unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5, list.getBackground()); 1267 } 1268 setBorder(unselectedBorder); 1269 } 1270 } 1271 setToolTipText(String.format("RGB: red=%d, green=%d, blue=%d", newColor.getRed(), newColor.getGreen(), newColor.getBlue())); 1272 return this; 1273 } 1274 } 1275 1276 public class HackyDragDropRowUI extends BasicTableUI { 1277 1278 private boolean inDrag = false; 1279 private int start; 1280 private int offset; 1281 1282 protected MouseInputListener createMouseInputListener() { 1283 return new HackyMouseInputHandler(); 1284 } 1285 1286 public void paint(Graphics g, JComponent c) { 1287 super.paint(g, c); 1288 1289 if (!inDrag) { 1290 return; 1291 } 1292 1293 int width = table.getWidth(); 1294 int height = table.getRowHeight(); 1295 g.setColor(table.getParent().getBackground()); 1296 Rectangle rect = table.getCellRect(table.getSelectedRow(), 0, false); 1297 g.copyArea(rect.x, rect.y, width, height, rect.x, offset); 1298 1299 if (offset < 0) { 1300 g.fillRect(rect.x, rect.y + (height + offset), width, (offset * -1)); 1301 } else { 1302 g.fillRect(rect.x, rect.y, width, offset); 1303 } 1304 } 1305 1306 class HackyMouseInputHandler extends MouseInputHandler { 1307 1308 public void mouseDragged(MouseEvent e) { 1309 int row = table.getSelectedRow(); 1310 if (row < 0) { 1311 return; 1312 } 1313 1314 inDrag = true; 1315 1316 int height = table.getRowHeight(); 1317 int middleOfSelectedRow = (height * row) + (height / 2); 1318 1319 int toRow = -1; 1320 int yLoc = (int)e.getPoint().getY(); 1321 1322 // goin' up? 1323 if (yLoc < (middleOfSelectedRow - height)) { 1324 toRow = row - 1; 1325 } else if (yLoc > (middleOfSelectedRow + height)) { 1326 toRow = row + 1; 1327 } 1328 1329 ProbeTableModel model = (ProbeTableModel)table.getModel(); 1330 if ((toRow >= 0) && (toRow < table.getRowCount())) { 1331 model.moveRow(row, toRow); 1332 table.setRowSelectionInterval(toRow, toRow); 1333 start = yLoc; 1334 } 1335 1336 offset = (start - yLoc) * -1; 1337 table.repaint(); 1338 } 1339 1340 public void mousePressed(MouseEvent e) { 1341 super.mousePressed(e); 1342 start = (int)e.getPoint().getY(); 1343 } 1344 1345 public void mouseReleased(MouseEvent e){ 1346 super.mouseReleased(e); 1347 inDrag = false; 1348 table.repaint(); 1349 } 1350 } 1351 } 1352}