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