package jas.hist;
import jas.plot.DataArea;
import jas.plot.DateAxis;
import jas.plot.DoubleAxis;
import jas.plot.Legend;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Enumeration;
import java.util.Vector;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
/**
* Binned data manager is used for all 1D histograms.
*/
abstract class BinnedDataManager
extends OneDDataManager
implements SupportsFunctions
{
private int m_defaultNumberOfBins;
BinnedDataManager(JASHist plot, final DataArea da, final Legend l, StatisticsBlock stats, final int defaultNumberOfBins)
{
super(plot,da,l, stats);
m_defaultNumberOfBins = defaultNumberOfBins;
}
void XAxisUpdated()
{
for (Enumeration e = data.elements(); e.hasMoreElements();)
{
JASHist1DHistogramData dw = (JASHist1DHistogramData) e.nextElement();
if (dw.isShowing()) dw.setXRange(xm.getBins(),xLow,xHigh);
}
for (Enumeration e = funcs.elements(); e.hasMoreElements();)
{
JASHist1DFunctionData dw = (JASHist1DFunctionData) e.nextElement();
if (dw.isShowing()) dw.setXRange(xLow,xHigh);
}
ym[0].setAttentionNeeded();
if (ym[1] != null) ym[1].setAttentionNeeded();
}
public JASHist1DFunctionData addFunction(Basic1DFunction d)
{
JASHist1DFunctionData dw = new JASHist1DFunctionData(this,d);
funcs.addElement(dw);
return dw;
}
public void removeFunction(JASHist1DFunctionData d)
{
d.setShowing(false);
d.destroy();
funcs.removeElement(d);
//The line below is to force the plot to invoke setChanged().
//It should be replaced by something better.
plot.setTitle( plot.getTitle() );
}
public void destroy()
{
super.destroy();
removeAllFunctions();
}
public void removeAllFunctions()
{
final Enumeration e = funcs.elements();
while (e.hasMoreElements())
{
JASHist1DFunctionData d = (JASHist1DFunctionData) e.nextElement();
d.setShowing(false);
d.destroy();
}
funcs.removeAllElements();
}
public int numberOfFunctions()
{
return funcs.size();
}
public Enumeration getFunctions()
{
return funcs.elements();
}
final public void update(final HistogramUpdate hu, final JASHistData data)
{
// I dont see any point to this??
//if (hu.isFinalUpdate() || hu.isReset())
//{
// if (m_defaultNumberOfBins != -1 && !xm.getIsFixed()) xm.bins = m_defaultNumberOfBins;
//}
super.update(hu,data);
}
final public void update(JASHist1DFunctionData data)
{
// Danger: likely to be run in a different thread
SwingUtilities.invokeLater(this);
}
private void writeObject(ObjectOutputStream out) throws IOException
{
// first get rid of excess IO volume...
funcs.trimToSize();
// then write out the vector
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
{
data = new Vector();
in.defaultReadObject();
}
protected final Vector funcs = new Vector();
final protected class DateAxisListener extends AxisListener
{
DateAxisListener(final ManagedAxis axis)
{
super(axis);
axisType = (DateAxis) axis.getType();
}
public void mouseDragged(MouseEvent me)
{
// Scale the axis so the correct point is under the mouse;
int min = axis.getMinLocation();
int max = axis.getMaxLocation();
double f = ((double) me.getX()) / (max - min);
if (grabType == 1) // move xHigh
{
xHigh = xLow + (grab-xLow)/f;
axisType.setMax((long) (xHigh*1000));
}
else if (grabType == 0) // move xLow
{
xLow = xHigh - (xHigh - grab)/(1-f);
axisType.setMin((long) (xLow*1000));
}
else // keep the range the same but move the axis
{
double off = (grab-f)*(xHigh - xLow);
xHigh = xLowOld + off + xHigh - xLow;
xLow = xLowOld + off;
axisType.setMin((long) (xLow*1000));
axisType.setMax((long) (xHigh*1000));
}
axis.setRangeAutomatic(false);
XAxisUpdated();
computeYAxisRange();
//yAxis.getAxis().processRangeUpdate();
da.repaint();
}
private DateAxis axisType;
}
final protected class DoubleAxisListener extends AxisListener
{
DoubleAxisListener(final ManagedAxis axis)
{
super(axis);
axisType = (DoubleAxis) axis.getType();
}
public void mouseDragged(MouseEvent me)
{
// Scale the axis so the correct point is under the mouse;
int min = axis.getMinLocation();
int max = axis.getMaxLocation();
double f = ((double) me.getX()) / (max - min);
if (grabType == 1) // move xHigh
{
xHigh = xLow + (grab-xLow)/f;
axisType.setMax(xHigh);
}
else if (grabType == 0) // move xLow
{
xLow = xHigh - (xHigh - grab)/(1-f);
axisType.setMin(xLow);
}
else // keep the range the same but move the axis
{
double off = (grab-f)*(xHigh - xLow);
xHigh = xLowOld + off + xHigh - xLow;
xLow = xLowOld + off;
axisType.setMin(xLow);
axisType.setMax(xHigh);
}
axis.setRangeAutomatic(false);
XAxisUpdated();
computeYAxisRange();
//yAxis.getAxis().processRangeUpdate();
da.repaint();
}
private DoubleAxis axisType;
}
abstract class AxisListener extends MouseAdapter implements MouseMotionListener
{
AxisListener(final ManagedAxis axis)
{
this.axis = axis;
axis.addMouseListener(this);
axis.addMouseMotionListener(this);
axis.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
}
public void mouseEntered(MouseEvent me)
{
}
public void mouseMoved(MouseEvent me)
{
}
public void mousePressed(MouseEvent me)
{
int min = axis.getMinLocation();
int max = axis.getMaxLocation();
double rgrab = ((double) me.getX()) / (max - min);
oldCursor = axis.getCursor();
// The type of grab depends on where on the axis the grab occurs;
if (rgrab < .2)
{
grabType = 0;
grab = xLow + rgrab * (xHigh - xLow);
axis.setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
}
else if (rgrab > .8)
{
grabType = 1;
grab = xLow + rgrab * (xHigh - xLow);
axis.setCursor(Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
}
else
{
grabType = 2;
grab = rgrab;
xLowOld = xLow;
axis.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
}
}
public void mouseReleased(MouseEvent me)
{
axis.setCursor(oldCursor);
}
protected double xLowOld;
protected double grab;
protected int grabType;
protected ManagedAxis axis;
protected Cursor oldCursor;
}
void modifyPopupMenu(JPopupMenu menu, Component source)
{
super.modifyPopupMenu(menu,source);
if (menu.getComponentCount() > 0) menu.addSeparator();
menu.add(new AddFunctionMenu());
menu.add(new RemoveFunctionMenu());
menu.add(new FitMenu());
menu.add(new AdvancedOptionsMenu());
}
public void fillFunctionMenu(JMenu menu)
{
menu.add(new AddFunctionMenu());
menu.add(new RemoveFunctionMenu());
menu.add(new FitMenu());
menu.add(new AdvancedOptionsMenu());
}
final private class AddFunctionMenu extends JMenu
{
public AddFunctionMenu()
{
super("Add function");
setMnemonic('A');
// we need to build the menu next time it's selected
m_build = FitterRegistry.instance().getDefaultFitter() != null &&
FunctionRegistry.instance().size() > 0;
setEnabled(m_build);
}
private void buildMenu()
{
removeAll();
int i = 0;
final Enumeration e = FunctionRegistry.instance().elements();
while (e.hasMoreElements())
this.add(new AddFunctionMenuItem((FunctionFactory) e.nextElement(), ++i));
m_build = false; // we don't need to build the menu next time it's selected
}
protected void fireMenuSelected()
{
if (m_build) buildMenu();
// super.fireMenuSelected();
}
private boolean m_build;
final private class AddFunctionMenuItem extends JMenuItem
/*
* This class is for an item on the AddFunctionMenu.
* Each is a function from the FuntionRegistry
* that can be added to a window.
*/
{
AddFunctionMenuItem(final FunctionFactory func, final int i)
{
super(new StringBuffer(String.valueOf(i)).append(' ').append(func.getFunctionName()).toString());
m_func = func;
setMnemonic('0' + (char) i);
}
protected void fireActionPerformed(final ActionEvent evt)
{
try
{
//This line replaces the one below. This is so that the adding of the
//function goes through the JASHist. This allows to control the title and
//the name of the function
plot.addData(m_func.createFunction(plot)).show(true);
// BinnedDataManager.this.addFunction(m_func.createFunction(plot)).show(true);
}
catch (FunctionFactoryError ffe)
{
JOptionPane.showMessageDialog(this,"Could not add function.");
}
}
private FunctionFactory m_func;
}
}
final private class RemoveFunctionMenu extends JMenu
/*
* Has on it all functions that are on the
* selected hist.
*/
{
public RemoveFunctionMenu()
{
super("Remove function");
setMnemonic('R');
// use the same remove all item for all menus
m_removeAll = new RemoveAll();
m_build = numberOfFunctions() > 0; // we need to build the menu next time it's selected
setEnabled(m_build);
}
private void buildMenu()
{
m_build = false;
removeAll();
int i = 0;
final Enumeration e = getFunctions();
while (e.hasMoreElements())
this.add(new RemoveFunctionMenuItem((JASHist1DFunctionData) e.nextElement(), ++i));
if (i > 1) // at least 2 menu items
{
addSeparator();
this.add(m_removeAll);
}
}
protected void fireMenuSelected()
{
if (m_build) buildMenu();
// super.fireMenuSelected();
}
private boolean m_build; // indicates whether it should be built when selected
private RemoveAll m_removeAll;
final private class RemoveFunctionMenuItem extends JMenuItem
{
RemoveFunctionMenuItem(final JASHist1DFunctionData func, final int i)
{
super(new StringBuffer(String.valueOf(i)).append(' ').append(func.getTitle()).toString());
m_func = func;
setMnemonic('0' + (char) i);
}
protected void fireActionPerformed(final ActionEvent evt)
{
BinnedDataManager.this.removeFunction(m_func);
}
private JASHist1DFunctionData m_func;
}
final private class RemoveAll extends JMenuItem
{
RemoveAll()
{
super("Remove all");
}
protected void fireActionPerformed(final ActionEvent evt)
{
removeAllFunctions();
}
}
}
final private class FitMenu extends JMenu
/*
* This is a dynamic menu whose contents depend
* on what data sets are on the window, and
* what fittable functions have already been
* added. It is only enabled if there is at
* least one fittable function on the window,
* and at least one data set displayed.
*/
{
public FitMenu()
{
super("Fit");
setMnemonic('F');
m_build = false;
if (numberOfDataSources() > 0)
{
final Enumeration e = getFunctions();
while (e.hasMoreElements())
{
final Basic1DFunction func = ((JASHist1DFunctionData) e.nextElement()).getFunction();
if (func instanceof Fittable1DFunction && ((Fittable1DFunction) func).getFit() == null)
{
m_build = true;
break;
}
}
}
setEnabled(m_build);
}
private void buildMenu()
{
removeAll();
m_build = false;
if (numberOfDataSources() > 1)
// there are multiple data sets to choose from
{
int i = 0;
final Enumeration e = getDataSources();
while ( e.hasMoreElements() )
this.add( new FitMenuDataMenu( (JASHist1DHistogramData) e.nextElement(), ++i ) );
// The FitMenu menu will contain a selection
// of FitMenuDataMenu menus (one for each data
// source). Each menu will offer a list of
// selectable functions.
}
else
// there is only one data set so it's ovbious
// which data set to use
{
m_selectedDataSet = (JASHist1DHistogramData) getDataSources().nextElement();
// the only data set is selected by default
final Enumeration e = getFunctions();
int i = 0;
while ( e.hasMoreElements() )
{
final Basic1DFunction func = ((JASHist1DFunctionData) e.nextElement()).getFunction();
if (func instanceof Fittable1DFunction && ((Fittable1DFunction) func).getFit() == null)
this.add( new FitMenuFunctionItem((Fittable1DFunction) func, ++i) );
}
// the FitMenu will offer a choice of all of
// the available functions to fit
}
}
protected void fireMenuSelected()
{
if (m_build) buildMenu();
// super.fireMenuSelected();
}
private JASHist1DHistogramData m_selectedDataSet;
// This is the data that the user has selected
// to fit the function to. If only one choice
// of data is available, this is selected
// automatically.
private boolean m_build;
final private class FitMenuFunctionItem extends JMenuItem
{
FitMenuFunctionItem(final Fittable1DFunction func, final int i)
{
super( String.valueOf(i) +" "+ func.getTitle() );
m_func = func;
setMnemonic('0' + (char) i);
}
protected void fireActionPerformed(final ActionEvent evt)
{
final Fitter fitter = FitterRegistry.instance().getDefaultFitter();
fitter.setFunction(m_func);
fitter.setData((XYDataSource) m_selectedDataSet.getFittableDataSource());
plot.notifyFitListeners(fitter);
fitter.start();
}
private Fittable1DFunction m_func;
}
final private class FitMenuDataMenu extends JMenu
{
FitMenuDataMenu(final JASHist1DHistogramData data, final int i)
{
super( String.valueOf(i) +" "+ data.getTitle() );
m_data = data;
setMnemonic('0' + (char) i);
}
protected void fireMenuSelected()
{
m_selectedDataSet = m_data;
if (!m_built)
{
m_built = true;
// build menu:
this.removeAll();
final Enumeration e = getFunctions();
int i = 0;
while ( e.hasMoreElements() )
{
JASHist1DFunctionData d = (JASHist1DFunctionData) e.nextElement();
Object f = d.getDataSource();
if (f instanceof Fittable1DFunction)
this.add( new FitMenuFunctionItem((Fittable1DFunction) f, ++i) );
}
// Users select an FitMenuDataMenu on the
// basis of which data set they want. Once a
// FitMenuDataMenu is selected, the
// selectedDataSet is set. This menu offers a
// list of possible functions to fit. Once
// a function is selected, the method
// FitMenuFunctionItem.fireActionPerformed()
// is called, and a fit is added based on the
// selected function and data.
// super.fireMenuSelected();
}
}
private boolean m_built = false;
private JASHist1DHistogramData m_data;
}
}
final private class AdvancedOptionsMenu extends JMenu
{
public AdvancedOptionsMenu()
{
super("Advanced options...");
setMnemonic('o');
final Enumeration e = getFunctions();
m_build = false;
if (e != null)
while (e.hasMoreElements())
{
if (((JASHist1DFunctionData) e.nextElement()).getFunction() instanceof FunctionAdvancedOptions)
{
m_build = true;
break;
}
}
setEnabled(m_build);
}
protected void fireMenuSelected()
{
if (m_build)
{
m_build = false;
removeAll();
final Enumeration e = getFunctions();
int i = 0;
while ( e.hasMoreElements() )
{
final Basic1DFunction func = ((JASHist1DFunctionData) e.nextElement()).getFunction();
if (func instanceof FunctionAdvancedOptions)
this.add( new AdvancedOptionsMenuItem( (FunctionAdvancedOptions) func, ++i ) );
}
}
// super.fireMenuSelected();
}
private boolean m_build;
final private class AdvancedOptionsMenuItem extends JMenuItem
{
AdvancedOptionsMenuItem(final FunctionAdvancedOptions function, final int i)
{
super(String.valueOf(i) +" "+ ((Basic1DFunction) function).getTitle());
m_function = function;
setMnemonic('0' + (char) i);
}
protected void fireActionPerformed(final ActionEvent e)
{
m_function.openAdvancedDialog((Frame) SwingUtilities.getAncestorOfClass(Frame.class, plot), plot);
}
private FunctionAdvancedOptions m_function;
}
}
void computeXAxisRange()
{
if (!xm.needsAttention()) return;
xm.payingAttention(); // do first to avoid race conditions
if (data.isEmpty()) return;
// Note, when we add items which are rebinnable, we coerce them to adopt the binning preferred by
// the x-axis. However, when we add items which are non-rebinnable, they are displayed without regard
// to the binning preferred by the axis.
if (!xm.getRangeAutomatic())
{
xLow = xm.getMin();
xHigh = xm.getMax();
return;
}
int nShowing = 0;
xLow = 0;
xHigh = 0;
boolean hasRebinnables = false;
for (Enumeration e = data.elements(); e.hasMoreElements();)
{
JASHist1DHistogramData dw = (JASHist1DHistogramData) e.nextElement();
if (!dw.isShowing()) continue;
if (Double.isNaN(dw.getXMin())) continue;
if (nShowing++ == 0)
{
xLow = dw.getXMin();
xHigh = dw.getXMax();
}
else
{
xLow = Math.min(xLow,dw.getXMin());
xHigh = Math.max(xHigh,dw.getXMax());
}
if (dw.isRebinnable()) hasRebinnables = true;
}
if (nShowing == 0) return;
xm.setBinned(hasRebinnables);
if (!xm.getAllowSuppressedZero())
{
if (xLow > 0) xLow = 0;
if (xHigh < 0) xHigh = 0;
}
if (xHigh <= xLow) xHigh = xLow + 1;
calcMinMaxBins(xLow,xHigh);
}
void computeYAxisRange()
{
for (int i=0; i<ym.length; i++)
{
double ymin = 0;
double ymax = 0;
if (ym[i] == null) continue;
if (!ym[i].needsAttention()) continue;
ym[i].payingAttention(); // do first to avoid race conditions
DoubleAxis yAxis = (DoubleAxis) ym[i].getType();
if (data.isEmpty()) return;
if (!ym[i].getRangeAutomatic())
{
yAxis.setUseSuggestedRange(false);
yAxis.getAxis().invalidate();
for (Enumeration e = data.elements(); e.hasMoreElements();)
{
JASHist1DHistogramData dw = (JASHist1DHistogramData) e.nextElement();
if (dw.isShowing() && dw.getYAxis() == i) dw.validate();
}
}
else
{
boolean first = true;
for (Enumeration e = data.elements(); e.hasMoreElements();)
{
JASHist1DHistogramData dw = (JASHist1DHistogramData) e.nextElement();
if (!dw.isShowing() || dw.getYAxis() != i) continue;
if (first)
{
ymin = dw.getYMin();
ymax = dw.getYMax();
first = false;
}
else
{
ymin = Math.min(ymin,dw.getYMin());
ymax = Math.max(ymax,dw.getYMax());
}
}
if (!ym[i].getAllowSuppressedZero())
{
if (ymin > 0) ymin = 0;
if (ymax < 0) ymax = 0;
}
if (ymax <= ymin) ymax = ymin + 1;
if (ym[i].isLogarithmic()) {
//Fix to JAIDA-85, JAP-59, JAP-53
double min = Double.NaN;
for (Enumeration e = data.elements(); e.hasMoreElements();) {
JASHist1DHistogramData dw = (JASHist1DHistogramData) e.nextElement();
if (dw.isShowing()) {
XYDataSource ds = (XYDataSource)dw.getFittableDataSource();
for ( int j = 0; j < ds.getNPoints(); j++ ) {
double tmpMin = ds.getY(j) - ds.getMinusError(j);
if ( tmpMin > 0 ) {
if ( Double.isNaN(min) )
min = tmpMin;
else
min = Math.min( tmpMin, min );
}
}
}
}
if ( Double.isNaN( min ) )
ymin = Math.max(ymin, 0.5);
else
ymin = 0.8*min;
// if Y Axis is logarithmic and ymin > 0.1 and allowSuppressedZero=false, force ymin=0.1
if (!ym[i].getAllowSuppressedZero()) ymin = Math.min(ymin, 0.1);
}
double oldYMin = yAxis.getPlotMin();
double oldYMax = yAxis.getPlotMax();
if (ymin < oldYMin || ymax > oldYMax || (ymax - ymin) / (oldYMax - oldYMin) < .75)
{
yAxis.setUseSuggestedRange(true);
yAxis.setMin(ymin);
yAxis.setMax(ymax);
yAxis.getAxis().revalidate(); // Why does this have to be revalidate??
}
}
}
}
abstract void calcMinMaxBins(double x1, double x2);
}