001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2016
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 java.awt.Color;
032import java.awt.event.ActionEvent;
033import java.awt.event.ActionListener;
034import java.rmi.RemoteException;
035import java.util.ArrayList;
036import java.util.Hashtable;
037import java.util.List;
038
039import javax.swing.JComponent;
040import javax.swing.JMenuItem;
041
042import org.jfree.chart.ChartFactory;
043import org.jfree.chart.ChartPanel;
044import org.jfree.chart.axis.NumberAxis;
045import org.jfree.chart.axis.ValueAxis;
046import org.jfree.chart.event.AxisChangeEvent;
047import org.jfree.chart.event.AxisChangeListener;
048import org.jfree.chart.plot.PlotOrientation;
049import org.jfree.chart.plot.XYPlot;
050import org.jfree.chart.renderer.xy.StackedXYBarRenderer;
051import org.jfree.chart.renderer.xy.XYBarRenderer;
052import org.jfree.chart.renderer.xy.XYItemRenderer;
053import org.jfree.data.Range;
054import org.jfree.data.statistics.HistogramType;
055
056import org.slf4j.Logger;
057import org.slf4j.LoggerFactory;
058
059import ucar.unidata.data.DataChoice;
060import ucar.unidata.data.grid.GridUtil;
061import ucar.unidata.idv.DisplayControl;
062import ucar.unidata.idv.control.DisplayControlImpl;
063import ucar.unidata.idv.control.chart.DataChoiceWrapper;
064import ucar.unidata.idv.control.chart.HistogramWrapper;
065import ucar.unidata.idv.control.chart.MyHistogramDataset;
066import ucar.unidata.idv.control.multi.DisplayGroup;
067import ucar.unidata.util.GuiUtils;
068import ucar.unidata.util.LogUtil;
069
070import visad.ErrorEstimate;
071import visad.FieldImpl;
072import visad.FlatField;
073import visad.Unit;
074import visad.VisADException;
075
076/**
077 * Wraps a JFreeChart histogram to ease working with VisAD data.
078 */
079public class McIDASVHistogramWrapper extends HistogramWrapper {
080
081    private static final Logger logger = LoggerFactory.getLogger(McIDASVHistogramWrapper.class);
082
083    private DisplayControl imageControl;
084
085    /** The plot */
086    private XYPlot plot;
087
088    private double low;
089
090    private double high;
091
092    /**
093     * Default ctor
094     */
095    public McIDASVHistogramWrapper() {}
096
097    /**
098     * Ctor
099     *
100     * @param name The name.
101     * @param dataChoices List of data choices.
102     * @param control {@literal "Parent"} control.
103     */
104    public McIDASVHistogramWrapper(String name, List dataChoices, DisplayControlImpl control) {
105        super(name, dataChoices);
106        imageControl = control;
107    }
108
109    /**
110     * Create the chart.
111     */
112    private void createChart() {
113        if (chartPanel != null) {
114            return;
115        }
116
117        MyHistogramDataset dataset = new MyHistogramDataset();
118        chart = ChartFactory.createHistogram("Histogram", null, null,
119                                             dataset,
120                                             PlotOrientation.VERTICAL, false,
121                                             false, false);
122        chart.getXYPlot().setForegroundAlpha(0.75f);
123        plot = (XYPlot) chart.getPlot();
124        initXYPlot(plot);
125        chartPanel = doMakeChartPanel(chart);
126    }
127
128    public JComponent doMakeContents() {
129        return super.doMakeContents();
130    }
131
132    /**
133     * Clear the histogram.
134     */
135    public void clearHistogram() {
136        if (chartPanel != null) {
137            XYPlot tempPlot = chartPanel.getChart().getXYPlot();
138            for (int i = 0; i < tempPlot.getDatasetCount(); i++) {
139                MyHistogramDataset dataset =
140                        (MyHistogramDataset)tempPlot.getDataset(i);
141                if (dataset != null) {
142                    dataset.removeAllSeries();
143                }
144            }
145        }
146    }
147
148    /**
149     * Create the histogram.
150     *
151     * @param data Data to use in histogram.
152     *
153     * @throws IllegalArgumentException if {@code data} is all NaNs.
154     * @throws RemoteException On badness
155     * @throws VisADException On badness
156     */
157    public void loadData(FlatField data) throws IllegalArgumentException, RemoteException, VisADException {
158        if ((data != null) && !GridUtil.isAllMissing(data)) {
159            reallyLoadData(data);
160        } else {
161            throw new IllegalArgumentException("Nothing to show in histogram");
162        }
163     }
164
165    /**
166     * Assumes that {@code data} has been validated and is okay to actually try
167     * loading.
168     *
169     * @param data Data to use in histogram. Cannot be {@code null} or all NaNs.
170     *
171     * @throws VisADException
172     * @throws RemoteException
173     */
174    private void reallyLoadData(FlatField data) throws VisADException, RemoteException {
175        createChart();
176        List dataChoiceWrappers = getDataChoiceWrappers();
177
178        try {
179            clearHistogram();
180
181            Hashtable props = new Hashtable();
182            ErrorEstimate[] errOut = new ErrorEstimate[1];
183            for (int paramIdx = 0; paramIdx < dataChoiceWrappers.size(); paramIdx++) {
184                DataChoiceWrapper wrapper = (DataChoiceWrapper)dataChoiceWrappers.get(paramIdx);
185
186                DataChoice dataChoice = wrapper.getDataChoice();
187                props = dataChoice.getProperties();
188                Unit defaultUnit = ucar.visad.Util.getDefaultRangeUnits((FlatField) data)[0];
189                Unit unit = ((DisplayControlImpl)imageControl).getDisplayUnit();
190                double[][] samples = data.getValues(false);
191                double[] actualValues = filterData(samples[0], getTimeValues(samples, data))[0];
192                if ((defaultUnit != null) && !defaultUnit.equals(unit)) {
193                    actualValues = Unit.transformUnits(unit, errOut, defaultUnit, null, actualValues);
194                }
195                final NumberAxis domainAxis = new NumberAxis(wrapper.getLabel(unit));
196
197                domainAxis.setAutoRangeIncludesZero(false);
198
199                XYItemRenderer renderer;
200                if (getStacked()) {
201                    renderer = new StackedXYBarRenderer();
202                } else {
203                    renderer = new XYBarRenderer();
204                }
205                if ((plot == null) && (chartPanel != null)) {
206                    plot = chartPanel.getChart().getXYPlot();
207                }
208                plot.setRenderer(paramIdx, renderer);
209                Color c = wrapper.getColor(paramIdx);
210                domainAxis.setLabelPaint(c);
211                renderer.setSeriesPaint(0, c);
212
213                MyHistogramDataset dataset = new MyHistogramDataset();
214                dataset.setType(HistogramType.FREQUENCY);
215                dataset.addSeries(dataChoice.getName() + " [" + unit + ']',
216                    actualValues, getBins());
217                samples = null;
218                actualValues = null;
219                plot.setDomainAxis(paramIdx, domainAxis, false);
220                plot.mapDatasetToDomainAxis(paramIdx, paramIdx);
221                plot.setDataset(paramIdx, dataset);
222
223                domainAxis.addChangeListener(new AxisChangeListener() {
224                    public void axisChanged(AxisChangeEvent ae) {
225                        if (!imageControl.isInitDone()) {
226                            return;
227                        }
228
229                        Range range = domainAxis.getRange();
230                        double newLow = Math.floor(range.getLowerBound()+0.5);
231                        double newHigh = Math.floor(range.getUpperBound()+0.5);
232                        double prevLow = getLow();
233                        double prevHigh = getHigh();
234                        try {
235                            ucar.unidata.util.Range newRange;
236                            if (prevLow > prevHigh) {
237                                newRange = new ucar.unidata.util.Range(newHigh, newLow);
238                            } else {
239                                newRange = new ucar.unidata.util.Range(newLow, newHigh);
240                            }
241                            ((DisplayControlImpl) imageControl).setRange(newRange);
242                        } catch (Exception e) {
243                            logger.error("Cannot change range", e);
244                        }
245                    }
246                });
247
248                Range range = domainAxis.getRange();
249                low = range.getLowerBound();
250                high = range.getUpperBound();
251            }
252
253        } catch (Exception exc) {
254            System.out.println("Exception exc=" + exc);
255            LogUtil.logException("Error creating data set", exc);
256        }
257    }
258
259    /**
260     * Modify the low and high values of the domain axis.
261     *
262     * @param lowVal Low value.
263     * @param hiVal High value.
264     *
265     * @return {@code false} if {@link #plot} is {@code null}. {@code true}
266     * otherwise.
267     */
268    protected boolean modifyRange(double lowVal, double hiVal) {
269        return modifyRange(lowVal, hiVal, true);
270    }
271
272    /**
273     * Modify the low and high values of the domain axis.
274     *
275     * @param lowVal Low value.
276     * @param hiVal High value.
277     * @param notify Whether or not listeners should be notified.
278     *
279     * @return {@code false} if {@link #plot} is {@code null}. {@code true}
280     * otherwise.
281     */
282    protected boolean modifyRange(double lowVal, double hiVal, boolean notify) {
283        try {
284            if (plot == null) {
285                return false;
286            }
287            ValueAxis domainAxis = plot.getDomainAxis();
288            org.jfree.data.Range newRange = new org.jfree.data.Range(lowVal, hiVal);
289            domainAxis.setRange(newRange, domainAxis.isAutoRange(), notify);
290            return true;
291        } catch (Exception e) {
292            return true;
293        }
294    }
295
296    protected Range getRange() {
297        ValueAxis domainAxis = plot.getDomainAxis();
298        return domainAxis.getRange();
299    }
300
301    protected void doReset() {
302        resetPlot();
303    }
304
305    /**
306     * reset the histogram to its previous range
307     */
308    public void resetPlot() {
309        if (chart == null) {
310            return;
311        }
312        if (!(chart.getPlot() instanceof XYPlot)) {
313            return;
314        }
315        XYPlot plot = (XYPlot) chart.getPlot();
316        int    rcnt = plot.getRangeAxisCount();
317        for (int i = 0; i < rcnt; i++) {
318            ValueAxis axis = plot.getRangeAxis(i);
319            axis.setAutoRange(true);
320        }
321        int dcnt = plot.getDomainAxisCount();
322        for (int i = 0; i < dcnt; i++) {
323            ValueAxis axis = plot.getDomainAxis(i);
324            try {
325                axis.setRange(low, high);
326            } catch (Exception e) {
327                logger.warn("jfreechart does not like ranges to be high -> low", e);
328            }
329        }
330    }
331
332    public double getLow() {
333        return low;
334    }
335
336    public void setLow(double val) {
337        low = val;
338    }
339
340    public double getHigh() {
341        return high;
342    }
343
344    public void setHigh(double val) {
345        high = val;
346    }
347
348    /**
349     * SHow the popup menu
350     *
351     * @param where component to show near to
352     * @param x x
353     * @param y y
354     */
355    @Override public void showPopup(JComponent where, int x, int y) {
356        List items = new ArrayList();
357        items = getPopupMenuItems(items);
358        if (items.isEmpty()) {
359            return;
360        }
361        GuiUtils.makePopupMenu(items).show(where, x, y);
362    }
363
364    /**
365     * Add the default menu items
366     * 
367     * @param items List of menu items
368     * 
369     * @return The items list
370     */
371    @Override protected List getPopupMenuItems(List items) {
372        items = super.getPopupMenuItems(items);
373        for (Object o : items) {
374            if (o instanceof JMenuItem) {
375                JMenuItem menuItem = (JMenuItem)o;
376                if ("Properties...".equals(menuItem.getText())) {
377                    menuItem.setActionCommand(ChartPanel.PROPERTIES_COMMAND);
378                    menuItem.addActionListener(buildHistoPropsListener());
379                }
380            }
381        }
382        return items;
383    }
384
385    /**
386     * @return {@link ActionListener} that listens for
387     * {@link ChartPanel#PROPERTIES_COMMAND} events and shows the histogram
388     * properties.
389     */
390    private ActionListener buildHistoPropsListener() {
391        return new ActionListener() {
392            @Override public void actionPerformed(ActionEvent event) {
393                String command = event.getActionCommand();
394                if (ChartPanel.PROPERTIES_COMMAND.equals(command)) {
395                    McIDASVHistogramWrapper.this.showProperties();
396                    return;
397                }
398            }
399        };
400    }
401
402    /**
403     * Show the properties dialog
404     *
405     * @return Was it ok
406     */
407    @Override public boolean showProperties() {
408        boolean result;
409        if (!hasDisplayControl()) {
410            result = showProperties(null, 0, 0);
411        } else {
412            result = super.showProperties();
413        }
414        return result;
415    }
416
417    public boolean hasDisplayControl() {
418        return getDisplayControl() != null;
419    }
420
421    /**
422     * Remove me
423     *
424     * @return was removed
425     */
426    public boolean removeDisplayComponent() {
427        if (GuiUtils.askYesNo("Remove Display",
428                              "Are you sure you want to remove: "
429                              + toString())) {
430            DisplayGroup displayGroup = getDisplayGroup();
431            if (displayGroup != null) {
432                displayGroup.removeDisplayComponent(this);
433            }
434
435            if (hasDisplayControl()) {
436                getDisplayControl().removeDisplayComponent(this);
437            }
438            return true;
439        } else {
440            return false;
441        }
442    }
443
444    /**
445     * Apply the properties
446     *
447     * @return Success
448     */
449    @Override protected boolean doApplyProperties() {
450        applyProperties();
451        
452//        try {
453//            // need to deal with the data being an imageseq 
454//            loadData((FlatField)imageControl.getDataChoice().getData(null));
455//        } catch (RemoteException e) {
456//            logger.error("trying to reload data", e);
457//        } catch (DataCancelException e) {
458//            logger.error("trying to reload data", e);
459//        } catch (VisADException e) {
460//            logger.error("trying to reload data", e);
461//        }
462
463        return true;
464    }
465
466    /**
467     * Been removed, do any cleanup
468     */
469    public void doRemove() {
470        isRemoved = true;
471        List displayables = getDisplayables();
472        if (hasDisplayControl() && !displayables.isEmpty()) {
473            getDisplayControl().removeDisplayables(displayables);
474        }
475        firePropertyChange(PROP_REMOVED, null, this);
476    }
477}
478