/*
* Open Source Physics software is free software as described near the bottom of this code file.
*
* For additional information and documentation on Open Source Physics please see:
* <http://www.opensourcephysics.org/>
*/
package org.opensourcephysics.tools;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.rmi.RemoteException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.swing.AbstractAction;
import javax.swing.AbstractSpinnerModel;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JSplitPane;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.ListSelectionModel;
import javax.swing.LookAndFeel;
import javax.swing.ScrollPaneConstants;
import javax.swing.SpinnerModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.MouseInputAdapter;
import javax.swing.event.MouseInputListener;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import javax.swing.table.TableModel;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;
import javax.swing.undo.UndoableEditSupport;
import org.opensourcephysics.controls.OSPLog;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.controls.XMLControl;
import org.opensourcephysics.controls.XMLControlElement;
import org.opensourcephysics.controls.XMLProperty;
import org.opensourcephysics.display.Data;
import org.opensourcephysics.display.DataFunction;
import org.opensourcephysics.display.DataTable;
import org.opensourcephysics.display.Dataset;
import org.opensourcephysics.display.DatasetManager;
import org.opensourcephysics.display.DisplayColors;
import org.opensourcephysics.display.Drawable;
import org.opensourcephysics.display.DrawingPanel;
import org.opensourcephysics.display.FunctionDrawer;
import org.opensourcephysics.display.GUIUtils;
import org.opensourcephysics.display.HighlightableDataset;
import org.opensourcephysics.display.Interactive;
import org.opensourcephysics.display.OSPRuntime;
import org.opensourcephysics.display.PlottingPanel;
import org.opensourcephysics.display.Selectable;
import org.opensourcephysics.display.TeXParser;
import org.opensourcephysics.display.axes.CartesianInteractive;
import org.opensourcephysics.media.core.TPoint;
import org.opensourcephysics.tools.DataToolTable.TableEdit;
import org.opensourcephysics.tools.DataToolTable.WorkingDataset;
import org.opensourcephysics.tools.DatasetCurveFitter.NumberField;
/**
* This tab displays and analyzes a single Data object in a DataTool.
*
* @author Douglas Brown
* @version 1.0
*/
public class DataToolTab extends JPanel implements Tool, PropertyChangeListener {
// static fields
public final static String SHIFTED = "'"; //$NON-NLS-1$
protected static NumberFormat correlationFormat = NumberFormat.getInstance();
private static final Cursor SELECT_CURSOR, SELECT_REMOVE_CURSOR, SELECT_ZOOM_CURSOR;
static {
if(correlationFormat instanceof DecimalFormat) {
DecimalFormat format = (DecimalFormat) correlationFormat;
format.applyPattern("0.000"); //$NON-NLS-1$
}
// create cursors
String imageFile = "/org/opensourcephysics/resources/tools/images/selectcursor.gif"; //$NON-NLS-1$
Image im = ResourceLoader.getImage(imageFile);
SELECT_CURSOR = GUIUtils.createCustomCursor(im, new Point(0, 0),
"Add points", Cursor.CROSSHAIR_CURSOR); //$NON-NLS-1$
imageFile = "/org/opensourcephysics/resources/tools/images/selectremovecursor.gif"; //$NON-NLS-1$
im = ResourceLoader.getImage(imageFile);
SELECT_REMOVE_CURSOR = GUIUtils.createCustomCursor(im, new Point(0, 0),
"Remove points", Cursor.CROSSHAIR_CURSOR); //$NON-NLS-1$
imageFile = "/org/opensourcephysics/resources/tools/images/selectzoomcursor.gif"; //$NON-NLS-1$
im = ResourceLoader.getImage(imageFile);
SELECT_ZOOM_CURSOR = GUIUtils.createCustomCursor(im, new Point(8, 8),
"Zoom", Cursor.CROSSHAIR_CURSOR); //$NON-NLS-1$
}
// instance fields
protected DataTool dataTool; // the DataTool that displays this tab
protected int originatorID = 0; // the ID of the Data object that owns this tab
protected DatasetManager dataManager = new DatasetManager(); // datasets in this tab
protected JSplitPane[] splitPanes;
protected DataToolPlotter plot;
protected DataToolTable dataTable;
protected DataToolStatsTable statsTable;
protected DataToolPropsTable propsTable;
protected JScrollPane dataScroller, statsScroller, propsScroller, tableScroller;
protected JToolBar toolbar;
protected JCheckBoxMenuItem statsCheckbox, fitterCheckbox, propsCheckbox, fourierCheckbox;
protected DatasetCurveFitter curveFitter;
protected FourierPanel fourierPanel;
protected JDialog fourierDialog;
protected JButton measureButton, analyzeButton, dataBuilderButton, newColumnButton, refreshDataButton;
protected JCheckBoxMenuItem valueCheckbox, slopeCheckbox, areaCheckbox;
protected Action fitterAction, propsAndStatsAction;
protected String fileName, ownerName;
protected Map<String, String[]> ownedColumns = new TreeMap<String, String[]>();
protected JButton helpButton;
protected int colorIndex = 0;
protected boolean tabChanged;
protected boolean userEditable = false;
protected UndoableEditSupport undoSupport;
protected UndoManager undoManager;
protected FunctionTool dataBuilder;
protected JobManager jobManager = new JobManager(this);
protected JLabel statusLabel, editableLabel;
protected CartesianInteractive plotAxes;
protected boolean positionVisible = false;
protected boolean slopeVisible = false;
protected boolean areaVisible = false;
protected boolean originShiftEnabled = false;
protected boolean measureFit = false;
protected JPopupMenu varPopup;
protected boolean isHorzVarPopup;
protected Action setVarAction;
protected boolean isInitialized = false;
protected Object[][] constantsLoadedFromXML;
protected boolean replaceColumnsWithMatchingNames = true;
protected JCheckBoxMenuItem measureFitCheckbox, originShiftCheckbox;
protected double prevShiftX, prevShiftY;
protected NumberField shiftXField, shiftYField, selectedXField, selectedYField;
protected JSpinner shiftXSpinner, shiftYSpinner;
protected ShiftEditListener shiftEditListener;
protected JLabel shiftXLabel, shiftYLabel, selectedXLabel, selectedYLabel;
protected int selectedDataIndex = -1;
protected boolean toggleMeasurement, freezeMeasurement;
/**
* Constructs a DataToolTab for the specified Data.
*
* @param data the Data object
* @param tool the DataTool
*/
public DataToolTab(Data data, DataTool tool) {
dataTool = tool;
dataTable = new DataToolTable(this);
createGUI();
String name = ToolsRes.getString("DataToolTab.DefaultName"); //$NON-NLS-1$
if(data!=null) {
String s = data.getName();
if((s!=null)&&!s.equals("")) { //$NON-NLS-1$
name = s;
}
}
setName(name);
loadData(data, false);
tabChanged(false);
}
/**
* Loads data into this tab.
*
* @param data the data to load
* @param replaceIfSameName true to replace existing data, if any
* @return true if loaded
*/
public ArrayList<DataColumn> loadData(final Data data, boolean replaceIfSameName) {
ArrayList<DataColumn> loadedColumns = new ArrayList<DataColumn>();
if(data==null) {
return loadedColumns;
}
ArrayList<DataColumn> inputColumns = DataTool.getAllDataColumns(data);
if(inputColumns==null) {
return loadedColumns;
}
boolean updatedColumns = false;
// case 1: tab contains no data
if(dataManager.getDatasets().isEmpty()) {
originatorID = data.getID();
for(DataColumn next : inputColumns) {
addColumn(next);
loadedColumns.add(next);
}
}
// case 2: tab already contains data
else {
// for each local column, find matching input column
for(Dataset local : dataManager.getDatasets()) {
DataColumn match = getIDMatch(local, inputColumns);
if (match!=null) {
// if match is found, compare with local column and remove match from input
// get y-column names
String localName = local.getYColumnName();
String name = match.getYColumnName();
// temporarily set local y-column name to "" to get unique name for match
local.setXYColumnNames("row", ""); //$NON-NLS-1$ //$NON-NLS-2$
name = getUniqueYColumnName(match, name, false);
local.setXYColumnNames("row", localName); //$NON-NLS-1$
// update local if incoming points or name is different
if(!Arrays.equals(local.getYPoints(), match.getYPoints())||!name.equals(localName)) {
local.clear();
double[] rows = DataTool.getRowArray(match.getIndex());
local.append(rows, match.getYPoints());
local.setXYColumnNames("row", name); //$NON-NLS-1$
updatedColumns = true;
}
inputColumns.remove(match);
}
else if (replaceIfSameName) { // no match found
// see if name matches an existing column
match = getNameMatch(local, inputColumns);
if(match!=null) {
// if match is found, compare with local column and remove match from input
// update local if incoming points are different
if(!Arrays.equals(local.getYPoints(), match.getYPoints())) {
local.clear();
double[] rows = DataTool.getRowArray(match.getIndex());
local.append(rows, match.getYPoints());
updatedColumns = true;
}
inputColumns.remove(match);
}
}
}
// add non-matching columns
for(DataColumn next : inputColumns) {
addColumn(next);
loadedColumns.add(next);
}
}
if(updatedColumns||!loadedColumns.isEmpty()) {
dataTable.refreshTable();
statsTable.refreshStatistics();
refreshPlot();
refreshGUI();
tabChanged(true);
varPopup = null;
}
return loadedColumns;
}
/**
* Adds new dataColumns to this tab.
*
* @param source the Data source of the columns
* @param deletable true to allow added columns to be deleted
* @param addDuplicates true to add duplicate IDs
* @param postEdit true to post an undoable edit
*/
public void addColumns(Data source, boolean deletable, boolean addDuplicates, boolean postEdit) {
// look for independent variable column and duplicate input column
ArrayList<Dataset> datasets = dataManager.getDatasets();
// independent variable column
Dataset indepVar = datasets.isEmpty() ? null : datasets.get(0);
double[] indepVarPts = (indepVar==null) ? null : indepVar.getYPoints();
// remove Double.NaN from end of indepVarPts
if(indepVarPts!=null) {
while((indepVarPts.length>0)&&Double.isNaN(indepVarPts[indepVarPts.length-1])) {
double[] newVals = new double[indepVarPts.length-1];
System.arraycopy(indepVarPts, 0, newVals, 0, newVals.length);
indepVarPts = newVals;
}
}
// indepVarPts cannot contain duplicates
indepVar = ((indepVarPts==null)||DataTool.containsDuplicateValues(indepVarPts)) ? null : indepVar;
ArrayList<DataColumn> inputColumns = DataTool.getDataColumns(source);
Dataset duplicate = null; // duplicate input column
if(indepVar!=null) {
String indepVarName = indepVar.getYColumnName();
for(DataColumn next : inputColumns) {
if((duplicate==null)&&next.getYColumnName().equals(indepVarName)) {
// found matching column name, now compare their points
double[] inputPts = next.getYPoints();
// remove Double.NaN from end of inputPts
while((inputPts.length>0)&&Double.isNaN(inputPts[inputPts.length-1])) {
double[] newVals = new double[inputPts.length-1];
System.arraycopy(inputPts, 0, newVals, 0, newVals.length);
inputPts = newVals;
}
// inputPts also can't contain duplicate points
if(DataTool.containsDuplicateValues(inputPts)) {
continue;
}
// does at least one point in inputPts match a point in indepVarPts?
boolean foundMatchingPoint = false;
for(double value : inputPts) {
if(DataTool.getIndex(value, indepVarPts, -1)>-1) {
foundMatchingPoint = true;
break;
}
}
if(!foundMatchingPoint) {
continue;
}
// found a duplicate
duplicate = next;
// are indepVarPts in ascending or descending order?
int trend = 1; // positive trend = ascending order
double prev = -Double.MAX_VALUE;
for(double d : indepVarPts) {
if(d>prev) {
prev = d;
} else {
trend = -1; // negative trend = descending order
break;
}
}
if(trend==-1) {
prev = Double.MAX_VALUE;
for(double d : indepVarPts) {
if(d<prev) {
prev = d;
} else {
trend = 0; // neither ascending nor descending
break;
}
}
}
// add new indepVar rows, if any, to table
// first combine inputPts with indepVarPts into newIndepVarPts
double[] newIndepVarPts = new double[indepVarPts.length];
System.arraycopy(indepVarPts, 0, newIndepVarPts, 0, indepVarPts.length);
// keep track of which values are inserted
double[] valuesInserted = new double[inputPts.length];
int len = 0; // number of inserted values
for(int i = 0; i<inputPts.length; i++) {
int index = DataTool.getIndex(inputPts[i], indepVarPts, -1);
if(index==-1) { // need to insert inputPts[i]
valuesInserted[len] = inputPts[i];
len++;
newIndepVarPts = DataTool.insert(inputPts[i], newIndepVarPts, trend);
}
}
if(len>0) {
// determine where insertions were made
double[] rowsInserted = new double[len]; // double[] needed for getIndex()
int[] rowsToInsert = new int[len];
for(int i = 0; i<len; i++) {
double val = valuesInserted[i];
int index = DataTool.getIndex(val, newIndepVarPts, -1);
rowsInserted[i] = index;
rowsToInsert[i] = index;
}
// arrange rowsToInsert in ascending order
Arrays.sort(rowsToInsert);
// assemble valuesToInsert array for executing insertRows()
double[] valuesToInsert = new double[len];
for(int i = 0; i<len; i++) {
int row = rowsToInsert[i];
int index = DataTool.getIndex(row, rowsInserted, -1);
valuesToInsert[i] = valuesInserted[index];
}
// prepare map of column names to double[] values to insert
dataTable.pasteValues.clear();
dataTable.pasteValues.put(indepVarName, valuesToInsert);
HashMap<String, double[]> prevState = dataTable.insertRows(rowsToInsert, dataTable.pasteValues);
// post edit: target is rows, value is map
TableEdit edit = dataTable.new TableEdit(DataToolTable.INSERT_ROWS_EDIT, null, rowsToInsert, prevState);
undoSupport.postEdit(edit);
// rearrange non-duplicate columns
for(DataColumn d : inputColumns) {
if(d==duplicate) {
continue;
}
double[] prevY = d.getYPoints();
double[] rows = DataTool.getRowArray(newIndepVarPts.length);
double[] newY = new double[rows.length];
Arrays.fill(newY, Double.NaN);
int k = Math.min(inputPts.length, prevY.length);
for(int i = 0; i<k; i++) {
int index = DataTool.getIndex(inputPts[i], newIndepVarPts, -1);
newY[index] = prevY[i];
}
d.clear();
d.append(rows, newY);
}
}
}
}
}
// finished processing input--now add input columns to tab
inputColumns.remove(duplicate);
addColumns(inputColumns, deletable, addDuplicates, postEdit);
}
/**
* Adds DataColumns to this tab.
*
* @param columns the columns to add
* @param deletable true to allow added columns to be deleted
* @param addDuplicates true to add duplicate IDs
* @param postEdit true to post an undoable edit
*/
protected void addColumns(ArrayList<DataColumn> columns, boolean deletable, boolean addDuplicates, boolean postEdit) {
for(DataColumn next : columns) {
int id = next.getID();
if(addDuplicates) {
// change ID so column always added
next.setID(-id);
}
ArrayList<DataColumn> loadedColumns = loadData(next, false);
// restore original ID
next.setID(id);
if(!loadedColumns.isEmpty()) {
for(DataColumn dc : loadedColumns) {
dc.deletable = deletable;
}
if(postEdit) {
int col = dataTable.getColumnCount()-1;
// post edit: target is column, value is data column
TableEdit edit = dataTable.new TableEdit(DataToolTable.INSERT_COLUMN_EDIT, next.getYColumnName(), new Integer(col), next);
undoSupport.postEdit(edit);
}
refreshDataBuilder();
}
}
dataTable.refreshUndoItems();
refreshGUI();
}
/**
* Sets the x and y columns by name.
*
* @param xColName the name of the horizontal axis variable
* @param yColName the name of the vertical axis variable
*/
public void setWorkingColumns(String xColName, String yColName) {
dataTable.setWorkingColumns(xColName, yColName);
}
@Override
public void setName(String name) {
name = replaceSpacesWithUnderscores(name);
super.setName(name);
if(dataTool!=null) {
dataTool.refreshTabTitles();
}
}
/**
* Sets the userEditable flag.
*
* @param editable true to enable user editing
*/
public void setUserEditable(boolean editable) {
if(userEditable==editable) {
return;
}
userEditable = editable;
refreshGUI();
}
/**
* Returns true if this tab is user editable.
*
* @return true if user editable
*/
public boolean isUserEditable() {
return userEditable && !originShiftEnabled;
}
/**
* Gets the data builder for defining custom data functions.
*
* @return the data builder
*/
public FunctionTool getDataBuilder() {
if(dataTool!=null) {
return dataTool.getDataBuilder();
}
if(dataBuilder==null) { // create new tool if none exists
dataBuilder = new FunctionTool(this) {
protected void refreshGUI() {
super.refreshGUI();
dropdown.setToolTipText(ToolsRes.getString
("DataTool.DataBuilder.Dropdown.Tooltip")); //$NON-NLS-1$
setTitle(ToolsRes.getString("DataTool.DataBuilder.Title")); //$NON-NLS-1$
}
};
dataBuilder.setFontLevel(FontSizer.getLevel());
dataBuilder.setHelpPath("data_builder_help.html"); //$NON-NLS-1$
dataBuilder.addPropertyChangeListener("function", this); //$NON-NLS-1$
}
refreshDataBuilder();
return dataBuilder;
}
/**
* Listens for property change "function".
*
* @param e the event
*/
public void propertyChange(PropertyChangeEvent e) {
String name = e.getPropertyName();
if(name.equals("function")) { //$NON-NLS-1$
tabChanged(true);
dataTable.refreshTable();
statsTable.refreshStatistics();
if(e.getNewValue() instanceof DataFunction) { // new function has been created
String funcName = e.getNewValue().toString();
dataTable.getWorkingData(funcName);
}
if(e.getOldValue() instanceof DataFunction) { // function has been deleted
String funcName = e.getOldValue().toString();
dataTable.removeWorkingData(funcName);
}
if(e.getNewValue() instanceof String) {
String funcName = e.getNewValue().toString();
if(e.getOldValue() instanceof String) { // function name has changed
String prevName = e.getOldValue().toString();
columnNameChanged(prevName, funcName);
} else {
dataTable.getWorkingData(funcName);
}
}
refreshPlot();
varPopup = null;
}
}
/**
* Sends a job to this tool and specifies a tool to reply to.
*
* @param job the Job
* @param replyTo the tool to notify when the job is complete (may be null)
* @throws RemoteException
*/
public void send(Job job, Tool replyTo) throws RemoteException {
XMLControlElement control = new XMLControlElement(job.getXML());
if(control.failedToRead()||(control.getObjectClass()==Object.class)) {
return;
}
// log the job in
jobManager.log(job, replyTo);
// if control is for a Data object, load it into this tab
if(Data.class.isAssignableFrom(control.getObjectClass())) {
Data data = (Data) control.loadObject(null, true, true);
loadData(data, replaceColumnsWithMatchingNames);
jobManager.associate(job, dataManager);
refreshGUI();
}
// if control is for DataToolTab class, load this tab from control
else if(DataToolTab.class.isAssignableFrom(control.getObjectClass())) {
control.loadObject(this);
refreshGUI();
}
}
/**
* Adds a fit function. UserFunctions can optionally be added to the fit builder.
*
* @param f the fit function to add
* @param addToFitBuilder true to add a UserFunction to the fit builder
*/
public void addFitFunction(KnownFunction f, boolean addToFitBuilder) {
curveFitter.addFitFunction(f, addToFitBuilder);
}
/**
* Clears all data.
*/
public void clearData() {
ArrayList<String> colNames = new ArrayList<String>();
for(Dataset next : dataManager.getDatasets()) {
colNames.add(next.getYColumnName());
}
dataTable.setSelectedColumnNames(colNames);
dataTable.deleteSelectedColumns(); // also posts undoable edits
}
/**
* Sets the replaceColumnsWithMatchingNames flag.
*
* @param replace true to replace columns with same name but different ID
*/
public void setReplaceColumnsWithMatchingNames(boolean replace) {
replaceColumnsWithMatchingNames = replace;
}
/**
* Returns true if this tab is interested in a Data object.
*
* @param data the Data object
* @return true if data is of interest
*/
public boolean isInterestedIn(Data data) {
if (data==null) return false;
if (isOwnedBy(data)) return true;
Collection<Tool> tools = jobManager.getTools(dataManager);
for(Tool tool : tools) {
if(tool instanceof DataRefreshTool) {
DataRefreshTool refresher = (DataRefreshTool)tool;
if (refresher.moreData.contains(data)) return true;
}
}
return false;
}
/**
* Sets DataColumn IDs to corresponding column owner IDs based on saved names.
* Call this after loading this tab from XML to set column IDs to column owner IDs.
* @param columnOwnerName the guest name
* @param data the guest Data
* @return true if any column IDs were changed
*/
public boolean setOwnedColumnIDs(String columnOwnerName, Data data) {
// only match column names associated with this column owner name
Set<String> namesToMatch = new HashSet<String>();
for (String colName: ownedColumns.keySet()) {
String[] dataNames = ownedColumns.get(colName);
if (dataNames!=null && dataNames[0].equals(columnOwnerName)) {
namesToMatch.add(colName);
}
}
Map<DataColumn, Dataset> matches = getColumnMatchesByName(namesToMatch, data);
for (DataColumn column: matches.keySet()) {
Dataset match = matches.get(column);
column.setID(match.getID());
}
return !matches.isEmpty();
}
/**
* Saves DataColumn names with associated column owner and Data object.
* Call this before saving this tab so owned columns will be saved in XML.
* @param columnOwnerName the guest name
* @param data the guest Data
*/
public void saveOwnedColumnNames(String columnOwnerName, Data data) {
Map<DataColumn, Dataset> matches = getColumnMatchesByID(data);
for (DataColumn column: matches.keySet()) {
Dataset match = matches.get(column);
ownedColumns.put(column.getYColumnName(), new String[] {columnOwnerName, match.getYColumnName()});
}
}
/**
* Gets the column name for the first DataColumn with a given ID.
* @param ID the ID number of the desired column
* @return the tab column name, or null if not found
*/
public String getColumnName(int ID) {
for (Dataset column: dataManager.getDatasets()) {
if (column.getID()==ID) return column.getYColumnName();
}
return null;
}
/**
* Returns true if (a) the Data ID is this tab owner's ID
* or (b) the Data name is this tab's name.
*
* @param data the Data object
* @return true if data owns this tab
*/
public boolean isOwnedBy(Data data) {
if(data==null) return false;
// return true if data name is the name of this tab
String name = data.getName();
if((name!=null)&&replaceSpacesWithUnderscores(name).equals(getName())) {
return true;
}
// return true if data ID is the originator of this tab
return data.getID()==originatorID;
}
/**
* Sets the owner of this tab. This method is used before saving and after loading this tab
* so the tab can refresh its data from a new owner.
* @param name the owner name
* @param data the owner Data
*/
public void setOwner(String name, Data data) {
ownerName = name;
originatorID = data.getID();
}
/**
* Gets the name of the owner of this tab. May return null, even if an owner exists.
* @return the name of the owner
*/
public String getOwnerName() {
return ownerName;
}
/**
* Refreshes the data by sending a request to the source. Note that this only works
* if the data was received from a DataRefreshTool.
*/
public void refreshData() {
// set dataManager name to tab name so reply will be recognized
dataManager.setName(getName());
jobManager.sendReplies(dataManager);
}
// _______________________ protected & private methods __________________________
/**
* Adds a DataColumn to this tab.
*
* @param column the column to add
*/
protected void addColumn(DataColumn column) {
String name = column.getYColumnName();
String yName = getUniqueYColumnName(column, name, false);
if(!name.equals(yName)) {
String xName = column.getXColumnName();
column.setXYColumnNames(xName, yName);
}
if(dataManager.getDatasets().isEmpty()) {
column.setMarkerColor(Color.BLACK);
column.setLineColor(Color.BLACK);
}
dataManager.addDataset(column);
dataTable.getWorkingData(yName);
}
/**
* Determines if a dataset is deletable.
*
* @param data the dataset
* @return true if deletable
*/
protected boolean isDeletable(Dataset data) {
if(data==null) {
return false;
}
// commented out by D Brown Mar 2011 so all columns are deletable
// if(!userEditable&&(data instanceof DataColumn)) {
// DataColumn column = (DataColumn) data;
// if(!column.deletable) {
// return false;
// }
// }
return true;
}
/**
* Replaces spaces with underscores in a name.
*
* @param name the name with spaces
* @return the name with underscores
*/
protected String replaceSpacesWithUnderscores(String name) {
name.trim();
int n = name.indexOf(" "); //$NON-NLS-1$
while(n>-1) {
name = name.substring(0, n)+"_"+name.substring(n+1); //$NON-NLS-1$
n = name.indexOf(" "); //$NON-NLS-1$
}
return name;
}
/**
* Refreshes the data builder.
*/
protected void refreshDataBuilder() {
if(dataTool!=null) {
dataTool.refreshDataBuilder();
return;
}
if(dataBuilder==null) {
return;
}
if(dataBuilder.getPanel(getName())==null) {
FunctionPanel panel = new DataFunctionPanel(dataManager);
dataBuilder.addPanel(getName(), panel);
}
for(String name : dataBuilder.panels.keySet()) {
if(!name.equals(getName())) {
dataBuilder.removePanel(name);
}
}
}
/**
* Sets the font level.
*
* @param level the level
*/
protected void setFontLevel(int level) {
FontSizer.setFonts(this, level);
plot.setFontLevel(level);
FontSizer.setFonts(statsTable, level);
FontSizer.setFonts(propsTable, level);
curveFitter.setFontLevel(level);
double factor = FontSizer.getFactor(level);
plot.getAxes().resizeFonts(factor, plot);
FontSizer.setFonts(plot.getPopupMenu(), level);
if(propsTable.styleDialog!=null) {
FontSizer.setFonts(propsTable.styleDialog, level);
propsTable.styleDialog.pack();
}
if(dataBuilder!=null) {
dataBuilder.setFontLevel(level);
}
fitterAction.actionPerformed(null);
propsTable.refreshTable();
// set shift field and label fonts in case they are not currently displayed
FontSizer.setFonts(shiftXLabel, level);
FontSizer.setFonts(shiftYLabel, level);
FontSizer.setFonts(selectedXLabel, level);
FontSizer.setFonts(selectedYLabel, level);
FontSizer.setFonts(shiftXField, level);
FontSizer.setFonts(shiftYField, level);
FontSizer.setFonts(selectedXField, level);
FontSizer.setFonts(selectedYField, level);
shiftXField.refreshPreferredWidth();
shiftYField.refreshPreferredWidth();
selectedXField.refreshPreferredWidth();
selectedYField.refreshPreferredWidth();
toolbar.revalidate();
refreshStatusBar(null);
// kludge to display tables correctly: do propsAndStatsAction now, again after a millisecond!
propsAndStatsAction.actionPerformed(null);
Timer timer = new Timer(1, new ActionListener() {
public void actionPerformed(ActionEvent e) {
propsAndStatsAction.actionPerformed(null);
}
});
timer.setRepeats(false);
timer.start();
}
/**
* Sets the tabChanged flag.
*
* @param changed true if tab is changed
*/
protected void tabChanged(boolean changed) {
tabChanged = changed;
}
/**
* Gets the working dataset.
*
* @return the first two data columns in the datatable (x-y order)
*/
protected WorkingDataset getWorkingData() {
// dataTable.getSelectedData();
return dataTable.workingData;
}
/**
* Returns a column name that is unique to this tab, contains
* no spaces, and is not reserved by the OSP parser.
*
* @param d the dataset
* @param proposed the proposed name for the column
* @param askUser true to ask user to approve changes
* @return unique name
*/
protected String getUniqueYColumnName(Dataset d, String proposed, boolean askUser) {
if(proposed==null) {
return null;
}
// remove all spaces
proposed = proposed.replaceAll(" ", ""); //$NON-NLS-1$ //$NON-NLS-2$
boolean containsOperators = containsOperators(proposed);
// check for duplicate or reserved names
if(askUser || containsOperators) {
int tries = 0, maxTries = 3;
while(tries<maxTries) {
tries++;
if(isDuplicateName(d, proposed)) {
Object response = JOptionPane.showInputDialog(this, "\""+proposed+"\" "+ //$NON-NLS-1$ //$NON-NLS-2$
ToolsRes.getString("DataFunctionPanel.Dialog.DuplicateName.Message"), //$NON-NLS-1$
ToolsRes.getString("DataFunctionPanel.Dialog.DuplicateName.Title"), //$NON-NLS-1$
JOptionPane.WARNING_MESSAGE, null, null,
proposed);
proposed = (response==null)? null: response.toString();
}
if((proposed==null)||proposed.equals("")) { //$NON-NLS-1$
return null;
}
if(isReservedName(proposed)) {
Object response = JOptionPane.showInputDialog(this, "\""+proposed+"\" "+ //$NON-NLS-1$ //$NON-NLS-2$
ToolsRes.getString("DataToolTab.Dialog.ReservedName.Message"), //$NON-NLS-1$
ToolsRes.getString("DataToolTab.Dialog.ReservedName.Title"), //$NON-NLS-1$
JOptionPane.WARNING_MESSAGE, null, null,
proposed);
proposed = (response==null)? null: response.toString();
}
if((proposed==null)||proposed.equals("")) { //$NON-NLS-1$
return null;
}
containsOperators = containsOperators(proposed);
if(containsOperators) {
Object response = JOptionPane.showInputDialog(this,
ToolsRes.getString("DataToolTab.Dialog.OperatorInName.Message"), //$NON-NLS-1$
ToolsRes.getString("DataToolTab.Dialog.OperatorInName.Title"), //$NON-NLS-1$
JOptionPane.WARNING_MESSAGE, null, null,
proposed);
proposed = (response==null)? null: response.toString();
}
if((proposed==null)||proposed.equals("")) { //$NON-NLS-1$
return null;
}
}
}
if (containsOperators) return null;
int i = 0;
// trap for names that are numbers
try {
Double.parseDouble(proposed);
proposed = ToolsRes.getString("DataToolTab.NewColumn.Name"); //$NON-NLS-1$
} catch(NumberFormatException ex) {}
// remove existing number subscripts, if any, from duplicate names
boolean subscriptRemoved = false;
if(isDuplicateName(d, proposed)) {
String subscript = TeXParser.getSubscript(proposed);
try {
i = Integer.parseInt(subscript);
proposed = TeXParser.removeSubscript(proposed);
subscriptRemoved = true;
} catch(Exception ex) {}
}
String name = proposed;
while(subscriptRemoved||isDuplicateName(d, name)||isReservedName(name)) {
i++;
name = TeXParser.addSubscript(proposed, String.valueOf(i));
subscriptRemoved = false;
}
return name;
}
/**
* Returns true if name is a duplicate of an existing dataset.
*
* @param d the dataset
* @param name the proposed name for the dataset
* @return true if duplicate
*/
protected boolean isDuplicateName(Dataset d, String name) {
if(dataManager.getDatasets().isEmpty()) {
return false;
}
if(dataManager.getDataset(0).getXColumnName().equals(name)) {
return true;
}
name = TeXParser.removeSubscripting(name);
Iterator<Dataset> it = dataManager.getDatasets().iterator();
while(it.hasNext()) {
Dataset next = it.next();
if(next==d) {
continue;
}
String s = TeXParser.removeSubscripting(next.getYColumnName());
if(s.equals(name)) {
return true;
}
}
return false;
}
/**
* Returns true if name is reserved by the OSP parser.
*
* @param name the proposed name
* @return true if reserved
*/
protected boolean isReservedName(String name) {
// check for parser terms
String[] s = FunctionTool.parserNames;
for(int i = 0; i<s.length; i++) {
if(s[i].equals(name)) {
return true;
}
}
// check for localized "row" name
if(DataTable.rowName.equals(name)) {
return true;
}
// check for dummy variables
s = UserFunction.dummyVars;
for(int i = 0; i<s.length; i++) {
if(s[i].equals(name)) {
return true;
}
}
// check for numbers
try {
Double.parseDouble(name);
return true;
} catch(NumberFormatException ex) {}
return false;
}
/**
* Determines if the name contains any FunctionTool.parserOperators.
*
* @param name the name
* @return true if the name contains one or more operators
*/
protected boolean containsOperators(String name) {
for (String next: FunctionTool.parserOperators) {
if (name.indexOf(next)>-1) return true;
}
return false;
}
/**
* Responds to a changed column name.
*
* @param oldName the previous name
* @param newName the new name
*/
protected void columnNameChanged(String oldName, String newName) {
tabChanged(true);
varPopup = null;
String pattern = dataTable.getFormatPattern(oldName);
dataTable.removeWorkingData(oldName);
dataTable.getWorkingData(newName);
dataTable.setFormatPattern(newName, pattern);
if((propsTable.styleDialog!=null)&&propsTable.styleDialog.isVisible()&&propsTable.styleDialog.getName().equals(oldName)) {
propsTable.styleDialog.setName(newName);
String title = ToolsRes.getString("DataToolPropsTable.Dialog.Title"); //$NON-NLS-1$
String var = TeXParser.removeSubscripting(newName);
propsTable.styleDialog.setTitle(title+" \""+var+"\""); //$NON-NLS-1$ //$NON-NLS-2$
}
statsTable.refreshStatistics();
Dataset working = getWorkingData();
if(working==null) {
return;
}
refreshPlot();
}
/**
* Creates a new empty DataColumn.
*
* @return the column
*/
protected DataColumn createDataColumn() {
Color markerColor = DisplayColors.getMarkerColor(colorIndex);
Color lineColor = DisplayColors.getLineColor(colorIndex);
if(!dataManager.getDatasets().isEmpty()) {
colorIndex++;
}
DataColumn column = new DataColumn();
column.setMarkerColor(markerColor);
column.setLineColor(lineColor);
column.setConnected(false);
int rowCount = Math.max(1, dataTable.getRowCount());
double[] y = new double[rowCount];
Arrays.fill(y, Double.NaN);
column.setPoints(y);
column.setXColumnVisible(false);
return column;
}
/**
* Saves the selected table data to a file selected with a fileChooser.
*
* @return the path of the saved file or null if failed
*/
protected String saveTableDataToFile() {
String tabName = getName();
OSPLog.finest("saving tabe data from "+tabName); //$NON-NLS-1$
JFileChooser chooser = OSPRuntime.getChooser();
chooser.setSelectedFile(new File(tabName+".txt")); //$NON-NLS-1$
FontSizer.setFonts(chooser, FontSizer.getLevel());
int result = chooser.showSaveDialog(this);
if(result==JFileChooser.APPROVE_OPTION) {
OSPRuntime.chooserDir = chooser.getCurrentDirectory().toString();
String fileName = chooser.getSelectedFile().getAbsolutePath();
fileName = XML.getRelativePath(fileName);
String data = getSelectedTableData();
return DataTool.write(data, fileName);
}
return null;
}
/**
* Copies the selected table data to the clipboard.
*/
protected void copyTableDataToClipboard() {
OSPLog.finest("copying table data from "+getName()); //$NON-NLS-1$
DataTool.copy(getSelectedTableData());
}
/**
* Gets the table cells selected by the user.
* The tab name and column names precede the data.
* Data rows are delimited by new lines ("\n"), columns by tabs.
*
* @return a String containing the data.
*/
protected String getSelectedTableData() {
StringBuffer buf = new StringBuffer();
if(getName()!=null) {
buf.append(getName()+"\n"); //$NON-NLS-1$
}
if((dataTable.getColumnCount()==1)||(dataTable.getRowCount()==0)) {
return buf.toString();
}
dataTable.clearSelectionIfEmptyEndRow();
// get selected rows and columns
int[] rows = dataTable.getSelectedRows();
// if no rows selected, select all
if(rows.length==0) {
dataTable.selectAllCells();
rows = dataTable.getSelectedRows();
}
int[] columns = dataTable.getSelectedColumns();
// copy column headings
for(int j = 0; j<columns.length; j++) {
int col = columns[j];
// ignore row heading
int modelCol = dataTable.convertColumnIndexToModel(col);
if(dataTable.isRowNumberVisible()&&(modelCol==0)) {
continue;
}
buf.append(dataTable.getColumnName(col));
buf.append("\t"); // tab after each column //$NON-NLS-1$
}
buf.setLength(buf.length()-1); // remove last tab
buf.append("\n"); //$NON-NLS-1$
java.text.DateFormat df = java.text.DateFormat.getInstance();
for(int i = 0; i<rows.length; i++) {
for(int j = 0; j<columns.length; j++) {
int col = columns[j];
int modelCol = dataTable.convertColumnIndexToModel(col);
// don't copy row numbers
if(dataTable.isRowNumberVisible()&&(modelCol==0)) {
continue;
}
Object value = dataTable.getValueAt(rows[i], col);
if(value!=null) {
if(value instanceof java.util.Date) {
value = df.format(value);
}
buf.append(value);
}
buf.append("\t"); // tab after each column //$NON-NLS-1$
}
buf.setLength(buf.length()-1); // remove last tab
buf.append("\n"); // new line after each row //$NON-NLS-1$
}
return buf.toString();
}
/**
* Creates the GUI.
*/
protected void createGUI() {
ToolsRes.addPropertyChangeListener("locale", new PropertyChangeListener() { //$NON-NLS-1$
public void propertyChange(PropertyChangeEvent e) {
refreshGUI();
}
});
setLayout(new BorderLayout());
splitPanes = new JSplitPane[3];
// splitPanes[0] is plot/fitter on left, tables on right
splitPanes[0] = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
splitPanes[0].setResizeWeight(0.7);
splitPanes[0].setOneTouchExpandable(true);
// splitPanes[1] is plot on top, fitter on bottom
splitPanes[1] = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
splitPanes[1].setResizeWeight(1);
splitPanes[1].setDividerSize(0);
// splitPanes[2] is stats/props tables on top, data table on bottom
splitPanes[2] = new JSplitPane(JSplitPane.VERTICAL_SPLIT) {
public Dimension getPreferredSize() {
Dimension dim = super.getPreferredSize();
dim.width = dataTable.getMinimumTableWidth()+6;
JScrollBar scrollbar = dataScroller.getVerticalScrollBar();
if (scrollbar.isVisible()) {
dim.width += scrollbar.getWidth();
}
dim.height = 1;
return dim;
}
};
splitPanes[2].setDividerSize(0);
splitPanes[2].setEnabled(false);
// add ancestor listener to initialize
this.addAncestorListener(new AncestorListener() {
public void ancestorAdded(AncestorEvent e) {
OSPLog.getOSPLog(); // workaround needed for consistent initialization!
if(getSize().width>0) {
init();
}
}
public void ancestorRemoved(AncestorEvent event) {}
public void ancestorMoved(AncestorEvent event) {}
});
// add component listener for resizing
addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent e) {
fitterAction.actionPerformed(null);
}
});
// add window listener to dataTool to display curvefitter properly
dataTool.addWindowListener(new java.awt.event.WindowAdapter() {
public void windowOpened(java.awt.event.WindowEvent e) {
fitterAction.actionPerformed(null);
}
});
// configure data table
dataTable.setRowNumberVisible(true);
dataScroller = new JScrollPane(dataTable);
dataTable.refreshTable();
dataTable.addPropertyChangeListener("format", new PropertyChangeListener() { //$NON-NLS-1$
public void propertyChange(PropertyChangeEvent e) {
refreshShiftFields();
}
});
dataScroller.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
dataTable.clearSelection();
}
});
dataScroller.setToolTipText(ToolsRes.getString("DataToolTab.Scroller.Tooltip")); //$NON-NLS-1$
dataTable.getColumnModel().addColumnModelListener(new TableColumnModelListener() {
public void columnAdded(TableColumnModelEvent e) {}
public void columnRemoved(TableColumnModelEvent e) {}
public void columnSelectionChanged(ListSelectionEvent e) {}
public void columnMarginChanged(ChangeEvent e) {}
public void columnMoved(TableColumnModelEvent e) {
Dataset prev = dataTable.workingData;
Dataset working = getWorkingData();
if(working!=prev && dataTool.fitBuilder!=null) {
tabChanged(true);
}
if((working==null)||(working==prev)) {
return;
}
plot.selectionBox.setSize(0, 0);
refreshPlot();
refreshShiftFields();
}
});
// create bottom pane action, fit and fourier checkboxes
fitterAction = new AbstractAction() {
public void actionPerformed(ActionEvent e) {
if(fitterCheckbox==null) {
return;
}
// remove curveFitter
splitPanes[1].remove(curveFitter);
splitPanes[1].setDividerSize(splitPanes[2].getDividerSize());
splitPanes[1].setDividerLocation(1.0);
plot.removeDrawables(FunctionDrawer.class);
// restore if fit checkbox is checked
boolean fitterVis = fitterCheckbox.isSelected();
splitPanes[1].setEnabled(fitterVis);
curveFitter.setActive(fitterVis);
if(fitterVis) {
splitPanes[1].setBottomComponent(curveFitter);
splitPanes[1].setDividerSize(splitPanes[0].getDividerSize());
splitPanes[1].setDividerLocation(-1);
plot.addDrawable(curveFitter.getDrawer());
}
if(e!=null) {
refreshPlot();
}
}
};
fitterCheckbox = new JCheckBoxMenuItem();
fitterCheckbox.setSelected(false);
fitterCheckbox.addActionListener(fitterAction);
fourierCheckbox = new JCheckBoxMenuItem();
fourierCheckbox.setSelected(false);
fourierCheckbox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (fourierPanel==null && dataTool!=null) {
// create fourier panel
fourierPanel = new FourierPanel();
fourierDialog = new JDialog(dataTool, false) {
public void setVisible(boolean vis) {
super.setVisible(vis);
fourierCheckbox.setSelected(vis);
}
};
fourierDialog.setContentPane(fourierPanel);
Dimension dim = new Dimension(640, 400);
fourierDialog.setSize(dim);
fourierPanel.splitPane.setDividerLocation(dim.width/2);
fourierPanel.refreshFourierData(dataTable.getSelectedData(), DataToolTab.this.getName());
fourierDialog.setLocationRelativeTo(dataTool);
}
fourierDialog.setVisible(fourierCheckbox.isSelected());
}
});
originShiftCheckbox = new JCheckBoxMenuItem();
originShiftCheckbox.setSelected(originShiftEnabled);
originShiftCheckbox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
boolean previouslyEnabled = originShiftEnabled;
originShiftEnabled = originShiftCheckbox.isSelected();
double shiftX = 0;
// set all columns except row column to shifted
for(int i = 1; i<dataTable.getColumnCount(); i++) {
String colName = dataTable.getColumnName(i);
Dataset data = dataTable.getDataset(colName);
if (data!=null && data instanceof DataColumn) {
DataColumn dataCol = (DataColumn)data;
dataCol.setShifted(originShiftEnabled);
if (i==1) {
shiftX = dataCol.getShift();
}
}
}
if (originShiftEnabled) {
toolbar.add(shiftXLabel, 2);
toolbar.add(shiftXSpinner, 3);
toolbar.add(shiftYLabel, 4);
toolbar.add(shiftYSpinner, 5);
if (!previouslyEnabled) {
// shift area limits
if (plot.areaLimits[0].pointIndex>-1 && plot.areaLimits[1].pointIndex>-1) {
plot.areaLimits[0].refreshX();
plot.areaLimits[1].refreshX();
}
else {
plot.areaLimits[0].setX(plot.areaLimits[0].getX()+shiftX);
plot.areaLimits[1].setX(plot.areaLimits[1].getX()+shiftX);
}
}
((CrawlerSpinnerModel)shiftXSpinner.getModel()).refreshDelta();
((CrawlerSpinnerModel)shiftYSpinner.getModel()).refreshDelta();
}
else {
toolbar.remove(shiftXLabel);
toolbar.remove(shiftXSpinner);
toolbar.remove(shiftYLabel);
toolbar.remove(shiftYSpinner);
toolbar.remove(selectedXLabel);
toolbar.remove(selectedXField);
toolbar.remove(selectedYLabel);
toolbar.remove(selectedYField);
if (previouslyEnabled) {
// shift area limits
if (plot.areaLimits[0].pointIndex>-1 && plot.areaLimits[1].pointIndex>-1) {
plot.areaLimits[0].refreshX();
plot.areaLimits[1].refreshX();
}
else {
plot.areaLimits[0].setX(plot.areaLimits[0].getX()-shiftX);
plot.areaLimits[1].setX(plot.areaLimits[1].getX()-shiftX);
}
}
}
toolbar.validate();
refreshAll();
prevShiftX = -shiftXField.getValue();
prevShiftY = -shiftYField.getValue();
}
});
measureFitCheckbox = new JCheckBoxMenuItem();
measureFitCheckbox.setSelected(false);
measureFitCheckbox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
measureFit = measureFitCheckbox.isSelected();
if (areaVisible) {
plot.refreshArea();
}
plot.refreshMeasurements();
plot.repaint();
}
});
// create newColumnButton button
newColumnButton = DataTool.createButton(""); //$NON-NLS-1$
newColumnButton.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
DataColumn column = createDataColumn();
String proposed = ToolsRes.getString("DataToolTab.NewColumn.Name"); //$NON-NLS-1$
proposed = getUniqueYColumnName(column, proposed, false);
Object input = JOptionPane.showInputDialog(DataToolTab.this, ToolsRes.getString("DataToolTab.Dialog.NameColumn.Message"), //$NON-NLS-1$
ToolsRes.getString("DataToolTab.Dialog.NameColumn.Title"), //$NON-NLS-1$
JOptionPane.QUESTION_MESSAGE, null, null, proposed);
if(input==null) {
return;
}
String newName = getUniqueYColumnName(column, input.toString(), true);
if(newName==null) {
return;
}
if(newName.equals("")) { //$NON-NLS-1$
String colName = ToolsRes.getString("DataToolTab.NewColumn.Name"); //$NON-NLS-1$
newName = getUniqueYColumnName(column, colName, false);
}
OSPLog.finer("adding new column \""+newName+"\""); //$NON-NLS-1$ //$NON-NLS-2$
column.setXYColumnNames("row", newName); //$NON-NLS-1$
ArrayList<DataColumn> loadedColumns = loadData(column, false);
if(!loadedColumns.isEmpty()) {
for(DataColumn next : loadedColumns) {
next.deletable = true;
}
}
int col = dataTable.getColumnCount()-1;
// post edit: target is column, value is dataset
TableEdit edit = dataTable.new TableEdit(DataToolTable.INSERT_COLUMN_EDIT, newName, new Integer(col), column);
undoSupport.postEdit(edit);
dataTable.refreshUndoItems();
Runnable runner = new Runnable() {
public synchronized void run() {
int col = dataTable.getColumnCount()-1;
dataTable.changeSelection(0, col, false, false);
dataTable.editCellAt(0, col, e);
dataTable.editor.field.requestFocus();
}
};
SwingUtilities.invokeLater(runner);
}
});
// create dataBuilderButton
dataBuilderButton = DataTool.createButton(ToolsRes.getString("DataToolTab.Button.DataBuilder.Text")); //$NON-NLS-1$
dataBuilderButton.setToolTipText(ToolsRes.getString("DataToolTab.Button.DataBuilder.Tooltip")); //$NON-NLS-1$
dataBuilderButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
getDataBuilder().setSelectedPanel(getName());
getDataBuilder().setVisible(true);
}
});
// create refreshDataButton
refreshDataButton = DataTool.createButton(ToolsRes.getString("DataToolTab.Button.Refresh.Text")); //$NON-NLS-1$
refreshDataButton.setToolTipText(ToolsRes.getString("DataToolTab.Button.Refresh.Tooltip")); //$NON-NLS-1$
refreshDataButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
refreshData();
}
});
// create help button
helpButton = DataTool.createButton(ToolsRes.getString("Tool.Button.Help")); //$NON-NLS-1$
helpButton.setToolTipText(ToolsRes.getString("Tool.Button.Help.ToolTip")); //$NON-NLS-1$
helpButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
DataTool.showHelp();
}
});
// create valueCheckbox
valueCheckbox = new JCheckBoxMenuItem(ToolsRes.getString("DataToolTab.Checkbox.Position")); //$NON-NLS-1$
valueCheckbox.setSelected(false);
valueCheckbox.setToolTipText(ToolsRes.getString("DataToolTab.Checkbox.Position.Tooltip")); //$NON-NLS-1$
valueCheckbox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
freezeMeasurement = false;
positionVisible = valueCheckbox.isSelected();
plot.setMessage(plot.createMessage());
plot.repaint();
refreshStatusBar(null);
}
});
// create slopeCheckbox
slopeCheckbox = new JCheckBoxMenuItem(ToolsRes.getString("DataToolTab.Checkbox.Slope")); //$NON-NLS-1$
slopeCheckbox.setToolTipText(ToolsRes.getString("DataToolTab.Checkbox.Slope.Tooltip")); //$NON-NLS-1$
slopeCheckbox.setSelected(false);
slopeCheckbox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
freezeMeasurement = false;
slopeVisible = slopeCheckbox.isSelected();
plot.setMessage(plot.createMessage());
plot.repaint();
refreshStatusBar(null);
}
});
// create areaCheckbox
areaCheckbox = new JCheckBoxMenuItem(ToolsRes.getString("DataToolTab.Checkbox.Area")); //$NON-NLS-1$
areaCheckbox.setToolTipText(ToolsRes.getString("DataToolTab.Checkbox.Area.Tooltip")); //$NON-NLS-1$
areaCheckbox.setSelected(false);
areaCheckbox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
plot.setAreaVisible(areaCheckbox.isSelected());
}
});
// create measureButton
measureButton = DataTool.createButton(ToolsRes.getString("DataToolTab.Button.Measure.Label")); //$NON-NLS-1$
measureButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// build a popup menu with measure items
JPopupMenu popup = new JPopupMenu();
popup.add(valueCheckbox);
popup.add(slopeCheckbox);
popup.add(areaCheckbox);
popup.addSeparator();
measureFitCheckbox.setEnabled(fitterCheckbox.isSelected());
popup.add(measureFitCheckbox);
popup.add(originShiftCheckbox);
FontSizer.setFonts(popup, FontSizer.getLevel());
popup.show(measureButton, 0, measureButton.getHeight());
}
});
// create analyzeButton
analyzeButton = DataTool.createButton(ToolsRes.getString("DataToolTab.Button.Analyze.Label")); //$NON-NLS-1$
analyzeButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// build a popup menu with analyze items
JPopupMenu popup = new JPopupMenu();
popup.add(statsCheckbox);
popup.add(fitterCheckbox);
popup.add(fourierCheckbox);
FontSizer.setFonts(popup, FontSizer.getLevel());
popup.show(analyzeButton, 0, analyzeButton.getHeight());
}
});
// create propsAndStatsAction
propsAndStatsAction = new AbstractAction() {
public void actionPerformed(ActionEvent e) {
// lay out the table bar split panes
boolean statsVis = statsCheckbox.isSelected();
boolean propsVis = propsCheckbox.isSelected();
if(statsVis) {
statsTable.refreshStatistics();
}
refreshStatusBar(null);
int statsHeight = statsTable.getPreferredSize().height;
int propsHeight = propsTable.getPreferredSize().height;
LookAndFeel currentLF = UIManager.getLookAndFeel();
int h = (currentLF.getClass().getName().indexOf("Nimbus")>-1) //$NON-NLS-1$
? 8 : 4;
if(statsVis&&propsVis) {
Box box = Box.createVerticalBox();
box.add(statsScroller);
box.add(propsScroller);
splitPanes[2].setTopComponent(box);
splitPanes[2].setDividerLocation(statsHeight+propsHeight+2*h);
} else if(statsVis) {
splitPanes[2].setTopComponent(statsScroller);
splitPanes[2].setDividerLocation(statsHeight+h);
} else if(propsVis) {
splitPanes[2].setTopComponent(propsScroller);
splitPanes[2].setDividerLocation(propsHeight+h);
} else {
splitPanes[2].setDividerLocation(0);
}
}
};
// create stats checkbox
statsCheckbox = new JCheckBoxMenuItem(ToolsRes.getString("Checkbox.Statistics.Label"), false); //$NON-NLS-1$
statsCheckbox.setToolTipText(ToolsRes.getString("Checkbox.Statistics.ToolTip")); //$NON-NLS-1$
statsCheckbox.addActionListener(propsAndStatsAction);
// create style properties checkbox
propsCheckbox = new JCheckBoxMenuItem(ToolsRes.getString("DataToolTab.Checkbox.Properties.Text"), true); //$NON-NLS-1$
propsCheckbox.setToolTipText(ToolsRes.getString("DataToolTab.Checkbox.Properties.Tooltip")); //$NON-NLS-1$
propsCheckbox.addActionListener(propsAndStatsAction);
// create curve fitter
FitBuilder fitBuilder = dataTool.getFitBuilder();
curveFitter = new DatasetCurveFitter(getWorkingData(), fitBuilder);
curveFitter.setDataToolTab(this);
fitBuilder.curveFitters.add(curveFitter);
fitBuilder.removePropertyChangeListener(curveFitter.fitListener);
fitBuilder.addPropertyChangeListener(curveFitter.fitListener);
curveFitter.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent e) {
if(e.getPropertyName().equals("changed")) { //$NON-NLS-1$
tabChanged(true);
return;
}
if(e.getPropertyName().equals("drawer") //$NON-NLS-1$
&& fitterCheckbox!=null && fitterCheckbox.isSelected()) {
plot.removeDrawables(FunctionDrawer.class);
// add fit drawer to plot drawable
plot.addDrawable((FunctionDrawer) e.getNewValue());
}
plot.repaint();
}
});
// create plotting panel and axes
plot = new DataToolPlotter(getWorkingData());
plotAxes = new DataToolAxes(plot);
plot.setAxes(plotAxes);
if(getWorkingData()!=null) {
plot.addDrawable(getWorkingData());
plot.setTitle(getWorkingData().getName());
}
// create mouse listener for selecting data points in plot
MouseInputListener mouseSelector = new MouseInputAdapter() {
TreeSet<Integer> rowsInside = new TreeSet<Integer>(); // points inside selectionBox
TreeSet<Integer> recent = new TreeSet<Integer>(); // points recently added or removed
boolean boxActive, selectionChanged, readyToFindHits, selectionBoxChanged;
Interactive ia;
Timer timerToFindHits;
boolean removeHits;
@Override
public void mousePressed(MouseEvent e) {
if (OSPRuntime.isPopupTrigger(e)) {
boxActive = false;
plot.setMouseCursor(SELECT_ZOOM_CURSOR);
return;
}
ia = plot.getInteractive();
if (ia==plot.origin) {
plot.selectionBox.visible = false;
plot.origin.mouseDownPt = e.getPoint();
plot.lockScale(true);
return;
}
// add or remove point if Interactive is dataset
if (ia instanceof HighlightableDataset) {
HighlightableDataset data = (HighlightableDataset) ia;
int index = data.getHitIndex();
ListSelectionModel model = dataTable.getColumnModel().getSelectionModel();
int col = dataTable.getXColumn();
model.setSelectionInterval(col, col);
col = dataTable.getYColumn();
model.addSelectionInterval(col, col);
TableModel tableModel = dataTable.getModel();
for(int i = 1; i<tableModel.getColumnCount(); i++) {
if(data.getYColumnName().equals(dataTable.getColumnName(i))) {
model.addSelectionInterval(i, i);
if (col!=i)
data.setHighlightColor(data.getFillColor());
data.setHighlighted(index, true);
break;
}
}
if(!e.isControlDown()) {
dataTable.setSelectedModelRows(new int[] {index});
} else {
int[] rows = dataTable.getSelectedModelRows();
boolean needsAdding = true;
for(int row : rows) {
if(row==index) {
needsAdding = false;
}
}
int[] newRows = new int[needsAdding ? rows.length+1 : rows.length-1];
if(needsAdding) {
System.arraycopy(rows, 0, newRows, 0, rows.length);
newRows[rows.length] = index;
} else {
int j = 0;
for(int row : rows) {
if(row==index) {
continue;
}
newRows[j] = row;
j++;
}
}
dataTable.setSelectedModelRows(newRows);
}
dataTable.getSelectedData();
plot.repaint();
boxActive = false;
selectionChanged = true;
return;
}
else if(ia!=null) {
boxActive = false;
return;
}
boxActive = !OSPRuntime.isPopupTrigger(e);
if(boxActive) {
if (timerToFindHits==null) {
timerToFindHits = new Timer(200, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
findHits(removeHits);
}
});
}
// prepare to drag
if(!(e.isControlDown()||e.isShiftDown())) {
dataTable.clearSelection();
}
// prefill rowsInside with currently selected rows
rowsInside.clear();
for(int row : dataTable.getSelectedModelRows()) {
rowsInside.add(row);
}
recent.clear();
Point p = e.getPoint();
plot.selectionBox.xstart = p.x;
plot.selectionBox.ystart = p.y;
readyToFindHits = true;
removeHits = e.isShiftDown() && e.isControlDown();
timerToFindHits.start();
plot.setMouseCursor(removeHits? SELECT_REMOVE_CURSOR: SELECT_CURSOR);
}
}
@Override
public void mouseDragged(MouseEvent e) {
selectionChanged = true;
if (ia==plot.origin) {
plot.selectionBox.visible = false;
double deltaX = 0, deltaY = 0;
int dx = plot.origin.mouseDownPt.x-e.getPoint().x;
int dy = plot.origin.mouseDownPt.y-e.getPoint().y;
if (e.isShiftDown()) {
if (Math.abs(dx)>=Math.abs(dy)) dy = 0;
else dx = 0;
}
Dataset data = dataTable.getDataset(plot.xVar);
if (data!=null && data instanceof DataColumn && plot.origin.isVertHit) {
deltaX = dx/plot.getXPixPerUnit();
double shift = prevShiftX+deltaX;
DataColumn col = (DataColumn)data;
double prev = col.getShift();
col.setShift(shift);
tabChanged(true);
// shift area limits
if (plot.areaLimits[0].pointIndex>-1 && plot.areaLimits[1].pointIndex>-1) {
plot.areaLimits[0].refreshX();
plot.areaLimits[1].refreshX();
}
else {
plot.areaLimits[0].setX(plot.areaLimits[0].getX()+shift-prev);
plot.areaLimits[1].setX(plot.areaLimits[1].getX()+shift-prev);
}
}
data = dataTable.getDataset(plot.yVar);
if (data!=null && data instanceof DataColumn && plot.origin.isHorzHit) {
deltaY = -dy/plot.getYPixPerUnit();
double shiftY = prevShiftY+deltaY;
DataColumn col = (DataColumn)data;
col.setShift(shiftY);
tabChanged(true);
}
refreshAll();
plot.lockedXMin = plot.mouseDownXMin + deltaX;
plot.lockedXMax = plot.mouseDownXMax + deltaX;
plot.lockedYMin = plot.mouseDownYMin + deltaY;
plot.lockedYMax = plot.mouseDownYMax + deltaY;
plot.repaint();
return;
}
if(!boxActive) {
return;
}
Dataset data = getWorkingData();
if(data==null) {
return;
}
Point mouse = e.getPoint();
plot.selectionBox.visible = true;
plot.selectionBox.setSize(mouse.x-plot.selectionBox.xstart, mouse.y-plot.selectionBox.ystart);
selectionBoxChanged = true;
removeHits = e.isShiftDown() && e.isControlDown();
plot.setMouseCursor(removeHits? SELECT_REMOVE_CURSOR: SELECT_CURSOR);
plot.repaint();
}
@Override
public void mouseReleased(MouseEvent e) {
if (!selectionChanged && freezeMeasurement) {
freezeMeasurement = false;
plot.measurementX = e.getX();
plot.measurementIndex = -1;
plot.refreshMeasurements();
}
selectionChanged = false;
plot.lockScale(false);
plot.selectionBox.visible = false;
if(ia!=null) {
if (ia==plot.origin) {
postShiftEdit();
((CrawlerSpinnerModel)shiftXSpinner.getModel()).refreshDelta();
((CrawlerSpinnerModel)shiftYSpinner.getModel()).refreshDelta();
}
if(ia instanceof Selectable) {
plot.setMouseCursor(((Selectable) ia).getPreferredCursor());
}
else {
plot.setMouseCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
}
if(ia instanceof HighlightableDataset) {
HighlightableDataset data = (HighlightableDataset) ia;
TableModel tableModel = dataTable.getModel();
int yCol = dataTable.getYColumn();
for(int i = 1; i<tableModel.getColumnCount(); i++) {
if(data.getYColumnName().equals(dataTable.getColumnName(i))
&& yCol!=i) {
data.clearHighlights();
data.setHighlightColor(Color.YELLOW);
ListSelectionModel model = dataTable.getColumnModel().getSelectionModel();
model.removeSelectionInterval(i, i);
break;
}
}
}
}
plot.repaint();
if (timerToFindHits!=null) {
timerToFindHits.stop();
}
if (selectionBoxChanged) {
findHits(removeHits);
selectionBoxChanged = false;
}
}
@Override
public void mouseMoved(MouseEvent e) {
if (!freezeMeasurement) {
plot.measurementX = e.getX();
plot.measurementIndex = -1;
}
plot.refreshMeasurements();
}
@Override
public void mouseEntered(MouseEvent e) {
dataTable.dataToolTab.refreshStatusBar(null);
}
private void findHits(final boolean subtract) {
if (!readyToFindHits || !selectionBoxChanged) return;
selectionBoxChanged = false;
Runnable runner = new Runnable() {
public void run() {
HighlightableDataset data = dataTable.workingData;
Map<Integer, Integer> workingRows = dataTable.workingRows;
if (data==null || workingRows==null) return;
double[][] screenPoints = data.getScreenCoordinates();
ListSelectionModel columnSelectionModel = dataTable.getColumnModel().getSelectionModel();
for(int i = 0; i<screenPoints[0].length; i++) {
Integer row = workingRows.get(i);
if (row==null) {
readyToFindHits = true;
return;
}
// if a data point is inside the box, add/remove it
if(!Double.isNaN(screenPoints[1][i])
&& plot.selectionBox.contains(screenPoints[0][i], screenPoints[1][i])) {
if (rowsInside.isEmpty()) {
columnSelectionModel.setSelectionInterval(1, 2);
}
if (subtract) {
rowsInside.remove(row);
}
else {
rowsInside.add(row);
}
recent.add(row);
}
// if a recently added data point is outside the box, remove it
else if (recent.contains(row)) {
if (subtract) {
rowsInside.add(row);
}
else {
rowsInside.remove(row);
}
recent.remove(row);
}
}
if (rowsInside.isEmpty()) {
columnSelectionModel.removeSelectionInterval(0, dataTable.getColumnCount()-1);
dataTable.getSelectionModel().clearSelection();
dataTable.getSelectedData(); // updates highlights
}
else {
int[] rows = new int[rowsInside.size()];
int i = 0;
for (int next: rowsInside) {
rows[i] = next;
i++;
}
dataTable.setSelectedModelRows(rows);
}
plot.repaint();
readyToFindHits = true;
}
};
// pig should this be in separate thread?
runner.run();
// new Thread(runner).start();
}
};
plot.addMouseListener(mouseSelector);
plot.addMouseMotionListener(mouseSelector);
// create toolbar
toolbar = new JToolBar();
toolbar.setFloatable(false);
toolbar.setBorder(BorderFactory.createEtchedBorder());
toolbar.add(measureButton);
toolbar.add(analyzeButton);
toolbar.add(Box.createGlue());
toolbar.add(newColumnButton);
toolbar.add(dataBuilderButton);
toolbar.add(refreshDataButton);
toolbar.add(helpButton);
// create statistics table
statsTable = new DataToolStatsTable(dataTable);
statsScroller = new JScrollPane(statsTable) {
public Dimension getPreferredSize() {
Dimension dim = statsTable.getPreferredSize();
return dim;
}
};
statsScroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
statsScroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
// create properties table
propsTable = new DataToolPropsTable(dataTable);
propsTable.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent e) {
if(e.getPropertyName().equals("display")) { //$NON-NLS-1$
refreshPlot();
}
}
});
propsScroller = new JScrollPane(propsTable) {
public Dimension getPreferredSize() {
Dimension dim = propsTable.getPreferredSize();
return dim;
}
};
propsScroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
propsScroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
// create laels
statusLabel = new JLabel(" ", SwingConstants.LEADING); //$NON-NLS-1$
statusLabel.setFont(new JTextField().getFont());
statusLabel.setBorder(BorderFactory.createEmptyBorder(1, 2, 1, 2));
editableLabel = new JLabel(" ", SwingConstants.TRAILING); //$NON-NLS-1$
editableLabel.setFont(statusLabel.getFont());
editableLabel.setBorder(BorderFactory.createEmptyBorder(1, 12, 1, 2));
// assemble components
add(toolbar, BorderLayout.NORTH);
add(splitPanes[0], BorderLayout.CENTER);
JPanel south = new JPanel(new BorderLayout());
south.add(statusLabel, BorderLayout.WEST);
south.add(editableLabel, BorderLayout.EAST);
add(south, BorderLayout.SOUTH);
tableScroller = new JScrollPane(splitPanes[2]);
tableScroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
splitPanes[0].setLeftComponent(splitPanes[1]);
splitPanes[0].setRightComponent(tableScroller);
splitPanes[1].setTopComponent(plot);
splitPanes[1].setBottomComponent(curveFitter);
splitPanes[2].setBottomComponent(dataScroller);
curveFitter.splitPane.setDividerLocation(0);
// set up the undo system
undoManager = new UndoManager();
undoSupport = new UndoableEditSupport();
undoSupport.addUndoableEditListener(undoManager);
// create origin shift labels and fields
shiftXLabel = new JLabel();
shiftXLabel.setBorder(BorderFactory.createEmptyBorder(2, 12, 2, 2));
shiftYLabel = new JLabel();
shiftYLabel.setBorder(BorderFactory.createEmptyBorder(2, 8, 2, 2));
selectedXLabel = new JLabel();
selectedXLabel.setBorder(BorderFactory.createEmptyBorder(2, 12, 2, 2));
selectedYLabel = new JLabel();
selectedYLabel.setBorder(BorderFactory.createEmptyBorder(2, 8, 2, 2));
// create shift spinner and field listeners
shiftEditListener = new ShiftEditListener();
KeyAdapter numberFieldKeyListener = new KeyAdapter() {
public void keyPressed(KeyEvent e) {
JComponent comp = (JComponent) e.getSource();
if(e.getKeyCode()==KeyEvent.VK_ENTER) {
comp.setBackground(Color.white);
} else {
comp.setBackground(Color.yellow);
}
}
};
FocusAdapter numberFieldFocusListener = new FocusAdapter() {
public void focusLost(FocusEvent e) {
NumberField field = (NumberField) e.getSource();
if(field.getBackground()!=Color.white) {
field.setBackground(Color.white);
field.postActionEvent();
}
}
public void focusGained(FocusEvent e) {
NumberField field = (NumberField) e.getSource();
field.selectAll();
}
};
shiftXField = new NumberField(4) {
@Override
public Dimension getMaximumSize() {
Dimension dim = getPreferredSize();
dim.height = super.getMaximumSize().height;
return dim;
}
};
shiftXField.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Dataset data = dataTable.getDataset(plot.xVar);
if (data !=null && data instanceof DataColumn) {
DataColumn col = (DataColumn)data;
double prevX = col.getShift();
double shiftX = -shiftXField.getValue();
if (col.setShift(shiftX)) {
tabChanged(true);
// shift area limits
if (plot.areaLimits[0].pointIndex>-1 && plot.areaLimits[1].pointIndex>-1) {
plot.areaLimits[0].refreshX();
plot.areaLimits[1].refreshX();
}
else {
plot.areaLimits[0].setX(plot.areaLimits[0].getX()+shiftX-prevX);
plot.areaLimits[1].setX(plot.areaLimits[1].getX()+shiftX-prevX);
}
refreshAll();
((CrawlerSpinnerModel)shiftXSpinner.getModel()).refreshDelta();
}
}
shiftXField.selectAll();
}
});
shiftXField.addKeyListener(numberFieldKeyListener);
shiftXField.addFocusListener(numberFieldFocusListener);
SpinnerModel spinModel = new CrawlerSpinnerModel();
shiftXSpinner = new JSpinner(spinModel) {
@Override
public Dimension getMaximumSize() {
Dimension dim = super.getMaximumSize();
dim.width = getPreferredSize().width;
return dim;
}
@Override
public Dimension getPreferredSize() {
Dimension dim = super.getPreferredSize();
for (Component c: this.getComponents()) {
if (c instanceof JButton) {
dim.width = shiftXField.getPreferredSize().width+c.getWidth()-2;
return dim;
}
}
return dim;
}
};
shiftXSpinner.setEditor(shiftXField);
ChangeListener xChangeListener = new ChangeListener() {
public void stateChanged(ChangeEvent e) {
Dataset data = dataTable.getDataset(plot.xVar);
if (data !=null && data instanceof DataColumn) {
DataColumn col = (DataColumn)data;
double prevX = col.getShift();
double shiftX = -(Double)shiftXSpinner.getValue();
if (col.setShift(shiftX)) {
tabChanged(true);
// shift area limits
if (plot.areaLimits[0].pointIndex>-1 && plot.areaLimits[1].pointIndex>-1) {
plot.areaLimits[0].refreshX();
plot.areaLimits[1].refreshX();
}
else {
plot.areaLimits[0].setX(plot.areaLimits[0].getX()+shiftX-prevX);
plot.areaLimits[1].setX(plot.areaLimits[1].getX()+shiftX-prevX);
}
refreshAll();
}
}
}
};
shiftXSpinner.addChangeListener(xChangeListener);
shiftXSpinner.addChangeListener(shiftEditListener);
shiftYField = new NumberField(4) {
@Override
public Dimension getMaximumSize() {
Dimension dim = getPreferredSize();
dim.height = super.getMaximumSize().height;
return dim;
}
};
shiftYField.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Dataset data = dataTable.getDataset(plot.yVar);
if (data !=null && data instanceof DataColumn) {
DataColumn col = (DataColumn)data;
if (col.setShift(-shiftYField.getValue())) {
tabChanged(true);
refreshAll();
((CrawlerSpinnerModel)shiftYSpinner.getModel()).refreshDelta();
}
}
shiftYField.selectAll();
}
});
shiftYField.addKeyListener(numberFieldKeyListener);
shiftYField.addFocusListener(numberFieldFocusListener);
spinModel = new CrawlerSpinnerModel();
shiftYSpinner = new JSpinner(spinModel) {
@Override
public Dimension getMaximumSize() {
Dimension dim = super.getMaximumSize();
dim.width = getPreferredSize().width;
return dim;
}
@Override
public Dimension getPreferredSize() {
Dimension dim = super.getPreferredSize();
for (Component c: this.getComponents()) {
if (c instanceof JButton) {
dim.width = shiftYField.getPreferredSize().width+c.getWidth()-2;
return dim;
}
}
return dim;
}
};
shiftYSpinner.setEditor(shiftYField);
ChangeListener yChangeListener = new ChangeListener() {
public void stateChanged(ChangeEvent e) {
Dataset data = dataTable.getDataset(plot.yVar);
if (data !=null && data instanceof DataColumn) {
DataColumn col = (DataColumn)data;
double shiftY = -(Double)shiftYSpinner.getValue();
if (col.setShift(shiftY)) {
tabChanged(true);
refreshAll();
}
}
}
};
shiftYSpinner.addChangeListener(yChangeListener);
shiftYSpinner.addChangeListener(shiftEditListener);
selectedXField = new NumberField(4) {
@Override
public Dimension getMaximumSize() {
Dimension dim = getPreferredSize();
dim.height = super.getMaximumSize().height;
return dim;
}
};
selectedXField.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Dataset data = dataTable.getDataset(plot.xVar);
if (data !=null && data instanceof DataColumn) {
DataColumn col = (DataColumn)data;
double prev = col.getShift();
double val = selectedXField.getValue();
if (col.setShiftedValue(selectedDataIndex, val)) {
tabChanged(true);
// shift area limits
if (plot.areaLimits[0].pointIndex>-1 && plot.areaLimits[1].pointIndex>-1) {
plot.areaLimits[0].refreshX();
plot.areaLimits[1].refreshX();
}
else {
double shift = col.getShift();
plot.areaLimits[0].setX(plot.areaLimits[0].getX()+shift-prev);
plot.areaLimits[1].setX(plot.areaLimits[1].getX()+shift-prev);
}
refreshAll();
((CrawlerSpinnerModel)shiftXSpinner.getModel()).refreshDelta();
}
}
selectedXField.requestFocusInWindow();
selectedXField.selectAll();
shiftEditListener.stateChanged(null);
}
});
selectedXField.addKeyListener(numberFieldKeyListener);
selectedXField.addFocusListener(numberFieldFocusListener);
selectedYField = new NumberField(4) {
@Override
public Dimension getMaximumSize() {
Dimension dim = getPreferredSize();
dim.height = super.getMaximumSize().height;
return dim;
}
};
selectedYField.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Dataset data = dataTable.getDataset(plot.yVar);
if (data !=null && data instanceof DataColumn) {
DataColumn col = (DataColumn)data;
double val = selectedYField.getValue();
if (col.setShiftedValue(selectedDataIndex, val)) {
tabChanged(true);
refreshAll();
((CrawlerSpinnerModel)shiftYSpinner.getModel()).refreshDelta();
}
}
selectedYField.requestFocusInWindow();
selectedYField.selectAll();
shiftEditListener.stateChanged(null);
}
});
selectedYField.addKeyListener(numberFieldKeyListener);
selectedYField.addFocusListener(numberFieldFocusListener);
}
/**
* Refreshes the GUI.
*/
protected void refreshGUI() {
Runnable runner = new Runnable() {
public void run() {
boolean changed = tabChanged;
newColumnButton.setText(ToolsRes.getString("DataToolTab.Button.NewColumn.Text")); //$NON-NLS-1$
newColumnButton.setToolTipText(ToolsRes.getString("DataToolTab.Button.NewColumn.Tooltip")); //$NON-NLS-1$
dataBuilderButton.setText(ToolsRes.getString("DataToolTab.Button.DataBuilder.Text")); //$NON-NLS-1$
dataBuilderButton.setToolTipText(ToolsRes.getString("DataToolTab.Button.DataBuilder.Tooltip")); //$NON-NLS-1$
dataBuilderButton.setEnabled(originatorID!=0);
refreshDataButton.setText(ToolsRes.getString("DataToolTab.Button.Refresh.Text")); //$NON-NLS-1$
refreshDataButton.setToolTipText(ToolsRes.getString("DataToolTab.Button.Refresh.Tooltip")); //$NON-NLS-1$
measureButton.setText(ToolsRes.getString("DataToolTab.Button.Measure.Label")); //$NON-NLS-1$
measureButton.setToolTipText(ToolsRes.getString("DataToolTab.Button.Measure.Tooltip")); //$NON-NLS-1$
analyzeButton.setText(ToolsRes.getString("DataToolTab.Button.Analyze.Label")); //$NON-NLS-1$
analyzeButton.setToolTipText(ToolsRes.getString("DataToolTab.Button.Analyze.Tooltip")); //$NON-NLS-1$
statsCheckbox.setText(ToolsRes.getString("Checkbox.Statistics.Label")); //$NON-NLS-1$
statsCheckbox.setToolTipText(ToolsRes.getString("Checkbox.Statistics.ToolTip")); //$NON-NLS-1$
fitterCheckbox.setText(ToolsRes.getString("Checkbox.Fits.Label")); //$NON-NLS-1$
fitterCheckbox.setToolTipText(ToolsRes.getString("Checkbox.Fits.ToolTip")); //$NON-NLS-1$
fourierCheckbox.setText(ToolsRes.getString("DataToolTab.Checkbox.Fourier.Label")); //$NON-NLS-1$
fourierCheckbox.setToolTipText(ToolsRes.getString("DataToolTab.Checkbox.Fourier.ToolTip")); //$NON-NLS-1$
originShiftCheckbox.setText(ToolsRes.getString("DataToolTab.Checkbox.DataShift.Label")); //$NON-NLS-1$
originShiftCheckbox.setToolTipText(ToolsRes.getString("DataToolTab.Checkbox.DataShift.ToolTip")); //$NON-NLS-1$
originShiftCheckbox.setEnabled(!plot.getDrawables(WorkingDataset.class).isEmpty());
measureFitCheckbox.setText(ToolsRes.getString("DataToolTab.Checkbox.MeasureFit.Label")); //$NON-NLS-1$
measureFitCheckbox.setToolTipText(ToolsRes.getString("DataToolTab.Checkbox.MeasureFit.ToolTip")); //$NON-NLS-1$
propsCheckbox.setText(ToolsRes.getString("DataToolTab.Checkbox.Properties.Text")); //$NON-NLS-1$
propsCheckbox.setToolTipText(ToolsRes.getString("DataToolTab.Checkbox.Properties.Tooltip")); //$NON-NLS-1$
valueCheckbox.setText(ToolsRes.getString("DataToolTab.Checkbox.Position")); //$NON-NLS-1$
valueCheckbox.setToolTipText(ToolsRes.getString("DataToolTab.Checkbox.Position.Tooltip")); //$NON-NLS-1$
slopeCheckbox.setText(ToolsRes.getString("DataToolTab.Checkbox.Slope")); //$NON-NLS-1$
slopeCheckbox.setToolTipText(ToolsRes.getString("DataToolTab.Checkbox.Slope.Tooltip")); //$NON-NLS-1$
areaCheckbox.setText(ToolsRes.getString("DataToolTab.Checkbox.Area")); //$NON-NLS-1$
areaCheckbox.setToolTipText(ToolsRes.getString("DataToolTab.Checkbox.Area.Tooltip")); //$NON-NLS-1$
helpButton.setText(ToolsRes.getString("Tool.Button.Help")); //$NON-NLS-1$
helpButton.setToolTipText(ToolsRes.getString("Tool.Button.Help.ToolTip")); //$NON-NLS-1$
// set origin and selected point shift labels
String label = ToolsRes.getString("DataToolTab.Origin.Label")+": "; //$NON-NLS-1$ //$NON-NLS-2$
shiftXLabel.setText(label+plot.xVar);
shiftYLabel.setText(plot.yVar);
shiftXLabel.setToolTipText(ToolsRes.getString("DataToolTab.Origin.Tooltip")); //$NON-NLS-1$
label = ToolsRes.getString("DataToolTab.Selection.Label")+": "; //$NON-NLS-1$ //$NON-NLS-2$
selectedXLabel.setText(label+plot.xVar);
selectedXLabel.setToolTipText(ToolsRes.getString("DataToolTab.Selection.Tooltip")); //$NON-NLS-1$
selectedYLabel.setText(plot.yVar);
toolbar.remove(newColumnButton);
if(userEditable) {
int n = toolbar.getComponentIndex(helpButton);
toolbar.add(newColumnButton, n);
toolbar.validate();
}
toolbar.remove(refreshDataButton);
Collection<Tool> tools = jobManager.getTools(dataManager);
for(Tool tool : tools) {
if(tool instanceof DataRefreshTool) {
int n = toolbar.getComponentIndex(helpButton);
toolbar.add(refreshDataButton, n);
toolbar.validate();
break;
}
}
curveFitter.refreshGUI();
statsTable.refreshGUI();
propsTable.refreshGUI();
refreshPlot();
refreshStatusBar(null);
tabChanged = changed;
}
};
if(SwingUtilities.isEventDispatchThread()) {
runner.run();
} else {
SwingUtilities.invokeLater(runner);
}
}
/**
* Initializes this panel.
*/
private void init() {
if(isInitialized) {
return;
}
splitPanes[1].setDividerLocation(1.0);
propsAndStatsAction.actionPerformed(null);
for(int i = 0; i<dataTable.getColumnCount(); i++) {
String colName = dataTable.getColumnName(i);
dataTable.getWorkingData(colName);
}
refreshPlot();
refreshGUI();
isInitialized = true;
}
/**
* Builds the axis variables popup menu.
*/
protected void buildVarPopup() {
if(setVarAction==null) {
// create action to set axis variable
setVarAction = new AbstractAction() {
public void actionPerformed(ActionEvent e) {
JMenuItem item = (JMenuItem) e.getSource();
// get desired variable for targeted axis
String var = item.getActionCommand();
// get current variable on other axis
String otherVar = isHorzVarPopup ? plot.yVar : plot.xVar;
// get current label column
int labelCol = dataTable.convertColumnIndexToView(0);
// find specified variable and move to x or y column
int col = isHorzVarPopup ? dataTable.getXColumn() : dataTable.getYColumn();
TableModel model = dataTable.getModel();
for(int i = 0; i<model.getColumnCount(); i++) {
if(var.equals(dataTable.getColumnName(i))) {
if(i==col) {
return; // no change
}
dataTable.getColumnModel().moveColumn(i, col);
break;
}
}
// restore other variable if needed
if(!var.equals(otherVar)) {
col = isHorzVarPopup ? dataTable.getYColumn() : dataTable.getXColumn();
for(int i = 0; i<model.getColumnCount(); i++) {
if(otherVar.equals(dataTable.getColumnName(i))) {
dataTable.getColumnModel().moveColumn(i, col);
break;
}
}
}
// restore labels
col = dataTable.convertColumnIndexToView(0);
dataTable.getColumnModel().moveColumn(col, labelCol);
}
};
}
varPopup = new JPopupMenu();
Font font = new JTextField().getFont();
for(Dataset next : dataManager.getDatasets()) {
String s = TeXParser.removeSubscripting(next.getYColumnName());
JMenuItem item = new JMenuItem(s);
item.setActionCommand(next.getYColumnName());
item.addActionListener(setVarAction);
item.setFont(font);
varPopup.add(item);
}
}
/**
* Returns the column with matching ID and columnID in the specified list.
* May return null.
*
* @param local the Dataset to match
* @param columnsToSearch the Datasets to search
* @return the matching Dataset, if any
*/
private DataColumn getIDMatch(Dataset local, ArrayList<DataColumn> columnsToSearch) {
if((columnsToSearch==null)||(local==null)) {
return null;
}
for(Iterator<DataColumn> it = columnsToSearch.iterator(); it.hasNext(); ) {
DataColumn next = it.next();
// next is match if has same ID and columnID
if((local.getID()==next.getID())&&(local.getColumnID()==next.getColumnID())) {
return next;
}
}
return null;
}
/**
* Returns the column with matching name in the specified list.
* May return null.
*
* @param local the Dataset to match
* @param columnsToSearch the Datasets to search
* @return the matching DataColumn, if any
*/
private DataColumn getNameMatch(Dataset local, ArrayList<DataColumn> columnsToSearch) {
if((columnsToSearch==null)||(local==null)) {
return null;
}
for(Iterator<DataColumn> it = columnsToSearch.iterator(); it.hasNext(); ) {
DataColumn next = it.next();
// next is match if has same y-column name
if(local.getYColumnName().equals(next.getYColumnName())) {
return next;
}
}
return null;
}
/**
* Returns true if the name and data duplicate an existing column.
*
* @param name the name
* @param data the data array
* @return true if a duplicate is found
*/
protected boolean isDuplicateColumn(String name, double[] data) {
Iterator<Dataset> it = dataManager.getDatasets().iterator();
while(it.hasNext()) {
Dataset next = it.next();
double[] y = next.getYPoints();
if(name.equals(next.getYColumnName())&&isDuplicate(data, next.getYPoints())) {
// next is duplicate column: add new points if any
if(data.length>y.length) {
next.clear();
next.append(data, data);
}
return true;
}
}
return false;
}
/**
* Returns true if two data arrays have identical values.
*
* @param data0 data array 0
* @param data1 data array 1
* @return true if identical
*/
private boolean isDuplicate(double[] data0, double[] data1) {
int len = Math.min(data0.length, data1.length);
for(int i = 0; i<len; i++) {
if(Double.isNaN(data0[i])&&Double.isNaN(data1[i])) {
continue;
}
if(data0[i]!=data1[i]) {
return false;
}
}
return true;
}
/**
* Gets datasets matching columns by ID in this tab.
* @param data Data object with datasets to match
* @return map of column to dataset
*/
protected Map<DataColumn, Dataset> getColumnMatchesByID(Data data) {
Map<DataColumn, Dataset> matches = new HashMap<DataColumn, Dataset>();
ArrayList<Dataset> datasets = DataTool.getDatasets(data);
for (Dataset next: dataManager.getDatasets()) {
if (next instanceof DataColumn) {
DataColumn column = (DataColumn)next;
Dataset match = getMatchByID(column, datasets);
if (match!=null) {
matches.put(column, match);
}
}
}
return matches;
}
/**
* Gets datasets matching columns by name in this tab.
* @param columnNames set of column names
* @param data Data object with datasets to match
* @return map of column to dataset
*/
protected Map<DataColumn, Dataset> getColumnMatchesByName(Set<String> columnNames, Data data) {
Map<DataColumn, Dataset> matches = new HashMap<DataColumn, Dataset>();
ArrayList<Dataset> datasets = DataTool.getDatasets(data);
for (Dataset next: dataManager.getDatasets()) {
if (next instanceof DataColumn) {
DataColumn column = (DataColumn)next;
if (columnNames!=null && !columnNames.contains(column.getYColumnName()))
continue;
Dataset match = getMatchByName(column, datasets);
if (match!=null) {
matches.put(column, match);
}
}
}
return matches;
}
/**
* Gets a matching Dataset by name.
* @param column the DataColumn to match
* @param datasets the Datasets to search
* @return the matching Dataset
*/
protected Dataset getMatchByName(DataColumn column, ArrayList<Dataset> datasets) {
// convert DataColumn name to dataset column name
String[] dataNames = ownedColumns.get(column.getYColumnName());
if (dataNames==null) return null;
String dataName = dataNames[1];
for (int i=0; i< datasets.size(); i++) {
Dataset next = datasets.get(i);
if (next==null) continue;
if (i==0 && dataName.equals(next.getXColumnName())) return next;
if (dataName.equals(next.getYColumnName())) return next;
}
return null;
}
/**
* Gets a matching Dataset by ID.
* @param column the DataColumn to match
* @param datasets the Datasets to search
* @return the matching Dataset
*/
protected Dataset getMatchByID(DataColumn column, ArrayList<Dataset> datasets) {
for (Dataset next: datasets) {
if (next==null) continue;
if (column.getID()==next.getID()) return next;
}
return null;
}
/**
* Sets the selected data in the curve fitter and fourier panel.
* @param selectedData the Dataset to pass to the fitter and fourier panel
*/
protected void setSelectedData(Dataset selectedData) {
curveFitter.setData(selectedData);
if (fourierPanel!=null) {
fourierPanel.refreshFourierData(selectedData, getName());
}
if (originShiftEnabled && selectedData!=null) {
if (selectedData.getIndex()==1) {
selectedDataIndex = dataTable.getSelectedRow();
toolbar.add(selectedXLabel, 6);
toolbar.add(selectedXField, 7);
toolbar.add(selectedYLabel, 8);
toolbar.add(selectedYField, 9);
selectedXField.setValue(selectedData.getXPoints()[0]);
selectedXField.refreshPreferredWidth();
selectedYField.setValue(selectedData.getYPoints()[0]);
selectedYField.refreshPreferredWidth();
toolbar.revalidate();
}
else {
toolbar.remove(selectedXLabel);
toolbar.remove(selectedXField);
toolbar.remove(selectedYLabel);
toolbar.remove(selectedYField);
toolbar.revalidate();
selectedDataIndex = -1;
}
}
if (positionVisible || slopeVisible) {
plot.refreshMeasurements();
}
if (areaVisible) {
plot.refreshArea();
}
}
/**
* Refreshes the plot.
*/
protected void refreshPlot() {
// refresh data for curve fitting and plotting
setSelectedData(dataTable.getSelectedData());
plot.removeDrawables(Dataset.class);
WorkingDataset workingData = getWorkingData();
valueCheckbox.setEnabled((workingData!=null)&&(workingData.getIndex()>0));
if(!valueCheckbox.isEnabled()) {
valueCheckbox.setSelected(false);
positionVisible = false;
}
slopeCheckbox.setEnabled((workingData!=null)&&(workingData.getIndex()>2));
if(!slopeCheckbox.isEnabled()) {
slopeCheckbox.setSelected(false);
slopeVisible = false;
}
areaCheckbox.setEnabled((workingData!=null)&&(workingData.getIndex()>1));
if(!areaCheckbox.isEnabled()) {
areaCheckbox.setSelected(false);
areaVisible = false;
}
plot.dataPresent = false;
if (workingData!=null) {
plot.dataPresent = workingData.getIndex()>0;
int labelCol = dataTable.convertColumnIndexToView(0);
String xName = dataTable.getColumnName((labelCol==0) ? 1 : 0);
Map<String, WorkingDataset> datasets = dataTable.workingMap;
for(Iterator<WorkingDataset> it = datasets.values().iterator(); it.hasNext(); ) {
DataToolTable.WorkingDataset next = it.next();
next.setXSource(workingData.getXSource());
String colName = next.getYColumnName();
if (next==workingData || colName.equals(xName)
|| (originShiftEnabled && (colName+SHIFTED).equals(xName))) {
continue;
}
if (next.isMarkersVisible()||next.isConnected()) {
next.clearHighlights();
if (!next.isMarkersVisible()) {
next.setMarkerShape(Dataset.NO_MARKER);
}
plot.addDrawable(next);
plot.dataPresent = plot.dataPresent || next.getIndex()>0;
}
}
plot.addDrawable(workingData);
// // keep area limits within dataset limits
// if (areaVisible) {
// plot.areaLimits[0].x = Math.max(plot.areaLimits[0].x, workingData.getXMin());
// plot.areaLimits[0].x = Math.min(plot.areaLimits[0].x, workingData.getXMax());
// plot.areaLimits[1].x = Math.max(plot.areaLimits[1].x, workingData.getXMin());
// plot.areaLimits[1].x = Math.min(plot.areaLimits[1].x, workingData.getXMax());
// }
workingData.restoreHighlights();
// draw curve fit on top of dataset if curve fitter is visible
if((fitterCheckbox!=null)&&fitterCheckbox.isSelected()) {
plot.removeDrawable(curveFitter.getDrawer());
plot.addDrawable(curveFitter.getDrawer());
}
// set axis labels
String xLabel = workingData.getColumnName(0);
String yLabel = workingData.getColumnName(1);
plot.setAxisLabels(xLabel, yLabel);
// construct equation string
if (curveFitter.fit!=null) {
String depVar = TeXParser.removeSubscripting(workingData.getColumnName(1));
String indepVar = TeXParser.removeSubscripting(workingData.getColumnName(0));
if (originShiftEnabled) {
depVar += DataToolTab.SHIFTED;
indepVar += DataToolTab.SHIFTED;
}
if (curveFitter.fit instanceof UserFunction) {
curveFitter.eqnField.setText(depVar+" = "+ //$NON-NLS-1$
((UserFunction) curveFitter.fit).getFullExpression(new String[] {indepVar}));
} else {
curveFitter.eqnField.setText(depVar+" = "+ //$NON-NLS-1$
curveFitter.fit.getExpression(indepVar));
}
}
}
else { // working data is null
plot.setXLabel(""); //$NON-NLS-1$
plot.setYLabel(""); //$NON-NLS-1$
}
if(dataTool!=null) {
dataTool.refreshTabTitles();
}
// refresh crossbars, slope line and area if visible
if (positionVisible || slopeVisible) {
plot.refreshMeasurements();
}
if (areaVisible) {
plot.refreshArea();
}
repaint();
}
/**
* Refreshes the status bar.
*
* @param hint an optional hint to display (may be null)
*/
protected void refreshStatusBar(String hint) {
if (hint!=null) {
statusLabel.setText(hint);
}
else if (slopeCheckbox.isSelected()) {
String s = ToolsRes.getString("DataToolTab.Status.Slope"); //$NON-NLS-1$
if (fitterCheckbox.isSelected()) {
s += " "+ToolsRes.getString("DataToolTab.Status.MeasureFit"); //$NON-NLS-1$ //$NON-NLS-2$
}
statusLabel.setText(s);
}
else if (areaCheckbox.isSelected()) {
String s = ToolsRes.getString("DataToolTab.Status.Area"); //$NON-NLS-1$
if (fitterCheckbox.isSelected()) {
s += " "+ToolsRes.getString("DataToolTab.Status.MeasureFit"); //$NON-NLS-1$ //$NON-NLS-2$
}
statusLabel.setText(s);
}
else if (valueCheckbox.isSelected()) {
String s = ToolsRes.getString("DataToolTab.Status.Value"); //$NON-NLS-1$
if (fitterCheckbox.isSelected()) {
s += " "+ToolsRes.getString("DataToolTab.Status.MeasureFit"); //$NON-NLS-1$ //$NON-NLS-2$
}
statusLabel.setText(s);
}
else if (originShiftCheckbox.isSelected()) {
statusLabel.setText(ToolsRes.getString("DataToolTab.Status.ShiftOrigin")); //$NON-NLS-1$
}
else if (statsCheckbox.isSelected()) {
statusLabel.setText(getCorrelationString());
}
else {
if(dataManager.getDatasets().size()<2) {
statusLabel.setText(userEditable ? ToolsRes.getString("DataToolTab.StatusBar.Text.CreateColumns") //$NON-NLS-1$
: ToolsRes.getString("DataToolTab.StatusBar.Text.PasteColumns")); //$NON-NLS-1$
} else {
statusLabel.setText(ToolsRes.getString("DataToolTab.StatusBar.Text.DragColumns")); //$NON-NLS-1$
}
}
editableLabel.setText(isUserEditable()? ToolsRes.getString("DataTool.MenuItem.Editable").toLowerCase() //$NON-NLS-1$
: ToolsRes.getString("DataTool.MenuItem.Noneditable").toLowerCase()); //$NON-NLS-1$
editableLabel.setForeground(isUserEditable()? Color.GREEN.darker(): Color.RED.darker());
}
/**
* Gets a correlation string to display in the status bar.
*/
protected String getCorrelationString() {
String s = ToolsRes.getString("DataToolTab.Status.Correlation"); //$NON-NLS-1$
if (Double.isNaN(curveFitter.correlation)) {
s += " "+ ToolsRes.getString("DataToolTab.Status.Correlation.Undefined"); //$NON-NLS-1$ //$NON-NLS-2$
}
else {
s += " = "+ correlationFormat.format(curveFitter.correlation); //$NON-NLS-1$
}
return s;
}
/**
* Refreshes the origin shift fields.
*/
public void refreshShiftFields() {
Dataset data = dataTable.getDataset(plot.xVar);
if (data !=null && data instanceof DataColumn) {
// check format pattern
String pattern = dataTable.getFormatPattern(plot.xVar);
if (pattern==null || "".equals(pattern)) { //$NON-NLS-1$
pattern = "#0.0#"; //$NON-NLS-1$
}
String existing = shiftXField.getPattern();
if (!pattern.equals(existing)) {
shiftXField.applyPattern(pattern);
selectedXField.applyPattern(pattern);
}
// set values
double shift = ((DataColumn)data).getShift();
shiftXField.setValue(shift==0? 0: -shift);
shiftXSpinner.setValue(shift==0? 0: -shift);
if (selectedDataIndex>-1) {
selectedXField.setValue(data.getYPoints()[selectedDataIndex]);
}
if (shift!=prevShiftX || !pattern.equals(existing)) {
shiftXField.refreshPreferredWidth();
selectedXField.refreshPreferredWidth();
toolbar.revalidate();
}
}
data = dataTable.getDataset(plot.yVar);
if (data !=null && data instanceof DataColumn) {
// check format pattern
String pattern = dataTable.getFormatPattern(plot.yVar);
if (pattern==null || "".equals(pattern)) { //$NON-NLS-1$
pattern = "#0.0#"; //$NON-NLS-1$
}
String existing = shiftYField.getPattern();
if (!pattern.equals(existing)) {
shiftYField.applyPattern(pattern);
selectedYField.applyPattern(pattern);
}
// set values
double shift = ((DataColumn)data).getShift();
shiftYField.setValue(shift==0? 0: -shift);
shiftYSpinner.setValue(shift==0? 0: -shift);
if (selectedDataIndex>-1) {
selectedYField.setValue(data.getYPoints()[selectedDataIndex]);
}
if (shift!=prevShiftY || !pattern.equals(existing)) {
shiftYField.refreshPreferredWidth();
selectedYField.refreshPreferredWidth();
toolbar.revalidate();
}
}
}
/**
* Refreshes all.
*/
public void refreshAll() {
refreshShiftFields();
refreshPlot();
curveFitter.fit(curveFitter.fit);
plot.refreshArea();
plot.refreshMeasurements();
dataTable.refreshTable();
}
/**
* Refreshes the undo and redo menu items.
*/
protected void refreshUndoItems() {
if(dataTool!=null) {
dataTool.undoItem.setEnabled(undoManager.canUndo());
dataTool.redoItem.setEnabled(undoManager.canRedo());
}
}
protected void postShiftEdit() {
// check for change
double shiftX = -shiftXField.getValue();
double shiftY = -shiftYField.getValue();
if (prevShiftX==shiftX && prevShiftY==shiftY)
return;
// post undoable edit
double[] newShift = new double[] {shiftX, shiftY};
double[] prevShift = new double[] {prevShiftX, prevShiftY};
String[] colNames = new String[] {plot.xVar, plot.yVar};
ShiftEdit edit = new ShiftEdit(colNames, newShift, prevShift);
undoSupport.postEdit(edit);
refreshUndoItems();
prevShiftX = shiftX;
prevShiftY = shiftY;
}
//_____________________________ inner classes ____________________________
/**
* An interactive axes class that returns popup menus for x and y-variables.
*/
protected class DataToolAxes extends CartesianInteractive {
/**
* Constructor.
*
* @param panel a PlottingPanel
*/
protected DataToolAxes(PlottingPanel panel) {
super(panel);
}
@Override
protected boolean hasHorzVariablesPopup() {
return dataTable.workingData!=null;
}
@Override
protected javax.swing.JPopupMenu getHorzVariablesPopup() {
if(varPopup==null) {
buildVarPopup();
}
isHorzVarPopup = true;
FontSizer.setFonts(varPopup, FontSizer.getLevel());
for(Component c : varPopup.getComponents()) {
JMenuItem item = (JMenuItem) c;
if(xLine.getText().equals(item.getActionCommand())) {
item.setFont(item.getFont().deriveFont(Font.BOLD));
} else {
item.setFont(item.getFont().deriveFont(Font.PLAIN));
}
}
return varPopup;
}
@Override
protected boolean hasVertVariablesPopup() {
return dataTable.workingData!=null;
}
@Override
protected javax.swing.JPopupMenu getVertVariablesPopup() {
if(varPopup==null) {
buildVarPopup();
}
isHorzVarPopup = false;
FontSizer.setFonts(varPopup, FontSizer.getLevel());
for(Component c : varPopup.getComponents()) {
JMenuItem item = (JMenuItem) c;
if(yLine.getText().equals(item.getActionCommand())) {
item.setFont(item.getFont().deriveFont(Font.BOLD));
} else {
item.setFont(item.getFont().deriveFont(Font.PLAIN));
}
}
return varPopup;
}
} // end DataToolAxes class
/**
* A class to plot datasets, value crossbars, slope lines, areas, and axes.
*/
protected class DataToolPlotter extends PlottingPanel {
SelectionBox selectionBox;
Crossbars valueCrossbars;
SlopeLine slopeLine;
XYAxes origin;
LimitLine[] areaLimits = new LimitLine[2];
Dataset areaDataset;
double value = Double.NaN, slope = Double.NaN, area;
DecimalFormat sciFormat = new DecimalFormat("0.00E0"); //$NON-NLS-1$
DecimalFormat fixedFormat = new DecimalFormat("0.00"); //$NON-NLS-1$
String xVar, yVar, message;
boolean scaleLocked, dataPresent;
double lockedXMin, lockedXMax, lockedYMin, lockedYMax;
double mouseDownXMin, mouseDownXMax, mouseDownYMin, mouseDownYMax;
int measurementIndex = -1, measurementX = -1;
/**
* Constructor
*
* @param dataset the initial dataset to plot
*/
protected DataToolPlotter(Dataset dataset) {
super((dataset==null) ? "x" //$NON-NLS-1$
: dataset.getColumnName(0), (dataset==null) ? "y" //$NON-NLS-1$
: dataset.getColumnName(1), ""); //$NON-NLS-1$
setAntialiasShapeOn(true);
selectionBox = new SelectionBox();
valueCrossbars = new Crossbars();
slopeLine = new SlopeLine();
origin = new XYAxes();
areaLimits[0] = new LimitLine();
areaLimits[1] = new LimitLine();
addDrawable(areaLimits[0]);
addDrawable(areaLimits[1]);
addDrawable(selectionBox);
addDrawable(origin);
// create key listener to move origin, fix measurements and extend slope line
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (plot.getCursor()==SELECT_CURSOR && e.isControlDown() && e.isShiftDown()) {
plot.setMouseCursor(SELECT_REMOVE_CURSOR);
}
if(e.getKeyCode()==KeyEvent.VK_CONTROL) {
if (toggleMeasurement) return;
toggleMeasurement = true;
refreshMeasurements();
refreshArea();
return;
}
if(e.getKeyCode()==KeyEvent.VK_SPACE) {
if (!freezeMeasurement && e.isShiftDown()) {
freezeMeasurement = true;
}
else {
freezeMeasurement = false;
};
if (!freezeMeasurement && mouseEvent!=null) {
plot.measurementX = mouseEvent.getX();
plot.measurementIndex = -1;
plot.refreshMeasurements();
}
return;
}
if(e.getKeyCode()==KeyEvent.VK_S && e.isShiftDown()) {
dataTool.slopeExtended = !dataTool.slopeExtended;
plot.refreshMeasurements();
return;
}
if (!originShiftEnabled) return;
double dy = Double.NaN, dx = Double.NaN;
if (e.getKeyCode()==KeyEvent.VK_UP) {
if (e.isShiftDown()) {
dy = -10/getYPixPerUnit();
}
else {
dy = -1/getYPixPerUnit();
}
}
else if (e.getKeyCode()==KeyEvent.VK_DOWN) {
if (e.isShiftDown()) {
dy = 10/getYPixPerUnit();
}
else {
dy = 1/getYPixPerUnit();
}
}
else if (e.getKeyCode()==KeyEvent.VK_LEFT) {
if (e.isShiftDown()) {
dx = -10/getXPixPerUnit();
}
else {
dx = -1/getXPixPerUnit();
}
}
else if (e.getKeyCode()==KeyEvent.VK_RIGHT) {
if (e.isShiftDown()) {
dx = 10/getXPixPerUnit();
}
else {
dx = 1/getXPixPerUnit();
}
}
if (!Double.isNaN(dx) || !Double.isNaN(dy)) {
if (!Double.isNaN(dx)) {
Dataset data = dataTable.getDataset(plot.xVar);
if (data !=null && data instanceof DataColumn) {
DataColumn col = (DataColumn)data;
col.setShift(col.getShift()-dx);
tabChanged(true);
// shift area limits
if (plot.areaLimits[0].pointIndex>-1 && plot.areaLimits[1].pointIndex>-1) {
plot.areaLimits[0].refreshX();
plot.areaLimits[1].refreshX();
}
else {
plot.areaLimits[0].setX(plot.areaLimits[0].getX()-dx);
plot.areaLimits[1].setX(plot.areaLimits[1].getX()-dx);
}
}
}
else if (!Double.isNaN(dy)) {
Dataset data = dataTable.getDataset(plot.yVar);
if (data !=null && data instanceof DataColumn) {
DataColumn col = (DataColumn)data;
col.setShift(col.getShift()+dy);
tabChanged(true);
}
}
refreshAll();
((CrawlerSpinnerModel)shiftXSpinner.getModel()).refreshDelta();
((CrawlerSpinnerModel)shiftYSpinner.getModel()).refreshDelta();
}
}
@Override
public void keyReleased(KeyEvent e) {
if (plot.getCursor()==SELECT_REMOVE_CURSOR && (!e.isControlDown() || !e.isShiftDown())) {
plot.setMouseCursor(SELECT_CURSOR);
}
if (e.getKeyCode()==KeyEvent.VK_CONTROL) {
if (!toggleMeasurement) return;
toggleMeasurement = false;
refreshMeasurements();
refreshArea();
return;
}
if (!originShiftEnabled) return;
if (e.getKeyCode()==KeyEvent.VK_UP
|| e.getKeyCode()==KeyEvent.VK_DOWN
|| e.getKeyCode()==KeyEvent.VK_LEFT
|| e.getKeyCode()==KeyEvent.VK_RIGHT) {
postShiftEdit();
}
}
});
}
/**
* Locks the scale so it can be manipulated when shifting the origin.
*
* @param lock true to lock, false to unlock
*/
protected void lockScale(boolean lock) {
scaleLocked = lock;
if (lock) {
lockedXMax = mouseDownXMax = xmax;
lockedXMin = mouseDownXMin = xmin;
lockedYMax = mouseDownYMax = ymax;
lockedYMin = mouseDownYMin = ymin;
}
}
@Override
protected void scale(ArrayList<Drawable> tempList) {
if (scaleLocked) {
xminPreferred = lockedXMin;
xmaxPreferred = lockedXMax;
yminPreferred = lockedYMin;
ymaxPreferred = lockedYMax;
}
else {
super.scale(tempList);
}
}
@Override
protected void paintDrawableList(Graphics g, ArrayList<Drawable> tempList) {
super.paintDrawableList(g, tempList);
if(tempList.contains(curveFitter.getDrawer())) {
double[] ylimits = curveFitter.getDrawer().getYRange();
if((ylimits[0]>=this.getYMax())||(ylimits[1]<=this.getYMin())) {
String s = ToolsRes.getString("DataToolTab.Plot.Message.FitNotVisible"); //$NON-NLS-1$
if (message!=null && !"".equals(message)) { //$NON-NLS-1$
s += " "+message; //$NON-NLS-1$
}
setMessage(s);
} else {
setMessage(message);
}
} else {
setMessage(message);
}
slopeLine.draw(g);
valueCrossbars.draw(g);
}
/**
* Sets the visibility of the area limits and dataset.
*
* @param visible true to show the area
*/
protected void setAreaVisible(boolean visible) {
areaVisible = visible;
if(areaDataset==null) { // first time shown
areaDataset = new Dataset();
areaDataset.setMarkerShape(Dataset.AREA);
areaDataset.setConnected(false);
areaDataset.setMarkerColor(new Color(102, 102, 102, 51));
Dataset data = dataTable.workingData;
if((data!=null)&&(data.getIndex()>1)) {
areaLimits[0].x = data.getXMin();
areaLimits[1].x = data.getXMax();
// set initial point indices
double[] pts = data.getXPoints();
for (int i = 0; i<pts.length; i++) {
if(pts[i]==areaLimits[0].x) {
areaLimits[0].pointIndex = i;
}
if(pts[i]==areaLimits[1].x) {
areaLimits[1].pointIndex = i;
}
if (areaLimits[0].pointIndex>-1 && areaLimits[1].pointIndex>-1) {
break;
}
}
}
}
refreshPlot();
setMessage(createMessage());
}
/**
* Refreshes the coordinate, slope and area measurements.
*/
protected void refreshMeasurements() {
HighlightableDataset data = dataTable.workingData;
try {
// catch exception thrown if mouseEvent is null
Interactive ia = plot.getInteractive();
if(ia instanceof HighlightableDataset) {
data = (HighlightableDataset) ia;
}
} catch (Exception e) {
}
plot.slope = plot.value = Double.NaN;
double[] xpoints = null;
double[] ypoints = null;
int j = measurementIndex;
double x = plot.pixToX(measurementX);
if (data!=null && (positionVisible || slopeVisible || areaVisible)) {
if (data.getIndex()>0 && j<0) {
measurementIndex = j = plot.findIndexNearestX(x, data);
}
xpoints = data.getXPoints();
ypoints = data.getYPoints();
boolean measureData = !fitterCheckbox.isSelected()
|| (measureFit && toggleMeasurement)
|| (!measureFit && !toggleMeasurement);
FunctionDrawer drawer = curveFitter.getDrawer();
if (positionVisible) {
if (measureData && j>-1 && !Double.isNaN(ypoints[j])) {
plot.value = ypoints[j];
plot.valueCrossbars.x = xpoints[j];
plot.valueCrossbars.y = ypoints[j];
plot.xVar = data.getXColumnName();
plot.yVar = data.getYColumnName();
}
else if (!measureData) {
plot.value = drawer.evaluate(x);
plot.valueCrossbars.x = x;
plot.valueCrossbars.y = drawer.evaluate(x);
plot.xVar = data.getXColumnName();
plot.yVar = data.getYColumnName();
}
}
if (slopeVisible) {
if (measureData && j>0 && j<data.getIndex()-1 && !Double.isNaN(ypoints[j])) {
plot.slopeLine.x = xpoints[j];
plot.slopeLine.y = ypoints[j];
plot.slope = (ypoints[j+1]-ypoints[j-1])/(xpoints[j+1]-xpoints[j-1]);
}
else if (!measureData) {
plot.slopeLine.x = x;
plot.slopeLine.y = drawer.evaluate(x);
double dx = 1/plot.getXPixPerUnit();
plot.slope = (drawer.evaluate(x+dx)-drawer.evaluate(x-dx))/(2*dx);
}
}
plot.setMessage(plot.createMessage());
}
plot.repaint();
}
/**
* Fills the areaDataset with points whose x values are between the limit lines.
*/
protected void refreshArea() {
if(!areaVisible) {
return;
}
area = 0;
Dataset data = dataTable.workingData;
if(data==null) {
areaVisible = false;
setMessage(createMessage());
return;
}
boolean measureData = !fitterCheckbox.isSelected()
|| (measureFit && toggleMeasurement)
|| (!measureFit && !toggleMeasurement);
FunctionDrawer drawer = curveFitter.getDrawer();
areaLimits[0].refreshX();
areaLimits[1].refreshX();
double lower = Math.min(areaLimits[0].x, areaLimits[1].x);
double upper = Math.max(areaLimits[0].x, areaLimits[1].x);
double del = (upper-lower)/200000;
if (del>0) {
lower -= del;
upper += del;
}
double[] xpoints, ypoints;
if (measureData) {
xpoints = data.getXPoints();
ypoints = data.getYPoints();
}
else {
int numpts = plot.xToPix(upper)-plot.xToPix(lower);
double delta = (upper-lower)/numpts;
xpoints = new double[numpts];
ypoints = new double[numpts];
for (int i=0; i<numpts; i++) {
xpoints[i] = lower+i*delta;
ypoints[i] = drawer.evaluate(xpoints[i]);
}
}
areaDataset.clear();
// find data points within range
ArrayList<Double> x = new ArrayList<Double>();
ArrayList<Double> y = new ArrayList<Double>();
for (int i = 0; i<xpoints.length; i++) {
if(xpoints[i]>=lower && xpoints[i]<=upper && !Double.isNaN(ypoints[i])) {
x.add(xpoints[i]);
y.add(ypoints[i]);
}
}
if (!x.isEmpty()) {
xpoints = new double[x.size()];
ypoints = new double[x.size()];
for(int i = 0; i<xpoints.length; i++) {
xpoints[i] = x.get(i);
ypoints[i] = y.get(i);
}
areaDataset.append(xpoints[0], 0);
areaDataset.append(xpoints, ypoints);
areaDataset.append(xpoints[xpoints.length-1], 0);
int n = xpoints.length;
if(n>1) {
plot.addDrawable(areaDataset);
// determine area under the curve
area = ypoints[0]*(xpoints[1]-xpoints[0]);
area += ypoints[n-1]*(xpoints[n-1]-xpoints[n-2]);
for(int i = 1; i<n-1; i++) {
area += ypoints[i]*(xpoints[i+1]-xpoints[i-1]);
}
area /= 2;
}
}
// set true limits
areaLimits[0].trueLimit = areaDataset.getXMin();
areaLimits[1].trueLimit = areaDataset.getXMax();
setMessage(createMessage());
}
/**
* Returns the index of the data point nearest the specified x on the plot.
*
* @param x the x-value on the plot
* @param data the dataset to search
* @return the index, or -1 if none found
*/
protected int findIndexNearestX(double x, Dataset data) {
if(data==null) {
return -1; // no dataset
}
int last = data.getIndex()-1;
if(last==-1) {
return -1; // dataset has no points
}
// limit x to plot area
x = Math.max(plot.getXMin(), x);
x = Math.min(plot.getXMax(), x);
double[] xpoints = data.getXPoints();
double[] ypoints = data.getYPoints();
// sort x data, keeping only points for which the y-value is not NaN
ArrayList<Double> valid = new ArrayList<Double>();
for (int i=0; i<xpoints.length; i++) {
if (Double.isNaN(ypoints[i])) continue;
valid.add(xpoints[i]);
}
Double[] sorted = valid.toArray(new Double[valid.size()]);
java.util.Arrays.sort(sorted);
last = sorted.length-1;
// check if pixel outside data range
if(x<sorted[0]) {
return 0;
}
if(x>=sorted[last]) {
return last;
}
// look thru sorted data to find point nearest x
for(int i = 1; i<sorted.length; i++) {
if (x>=sorted[i-1] && x<sorted[i]) {
// found it
if (sorted[i-1]<plot.getXMin()) {
x = sorted[i];
} else if(sorted[i]>plot.getXMax()) {
x = sorted[i-1];
} else {
x = (Math.abs(x-sorted[i-1])<Math.abs(x-sorted[i])) ? sorted[i-1] : sorted[i];
}
// find index of first data point with this value of x
for(int j = 0; j<xpoints.length; j++) {
if(xpoints[j]==x && !Double.isNaN(ypoints[j])) {
return j;
}
}
return -1;
}
}
return -1; // none found (should never get here)
}
/**
* Creates a message showing the current coordinates, slope and/or area.
*/
protected String createMessage() {
String xAxis = xVar, yAxis = yVar;
if (originShiftEnabled) {
xAxis += DataToolTab.SHIFTED;
yAxis += DataToolTab.SHIFTED;
}
StringBuffer buf = new StringBuffer();
if(positionVisible&&!Double.isNaN(value)) {
buf.append(TeXParser.removeSubscripting(xAxis)+"="); //$NON-NLS-1$
buf.append(format(plot.valueCrossbars.x, getXMax()-getXMin()));
buf.append(" "); //$NON-NLS-1$
buf.append(TeXParser.removeSubscripting(yAxis)+"="); //$NON-NLS-1$
buf.append(format(plot.valueCrossbars.y, getYMax()-getYMin()));
}
if(slopeVisible&&!Double.isNaN(slope)) {
if(buf.length()>0) {
buf.append(" "); //$NON-NLS-1$
}
buf.append(ToolsRes.getString("DataToolPlotter.Message.Slope")); //$NON-NLS-1$
buf.append(format(plot.slope, 0));
}
if(areaVisible) {
if(buf.length()>0) {
buf.append(" "); //$NON-NLS-1$
}
buf.append(ToolsRes.getString("DataToolPlotter.Message.Area")); //$NON-NLS-1$
buf.append(format(plot.area, 0));
}
message = buf.toString();
return message;
}
/**
* Formats a number.
*
* @param value the number
* @param range a min-max range of values
* @return the formatted string
*/
protected String format(double value, double range) {
double zero = Math.min(1, range)/1000;
if(Math.abs(value)<zero) {
value = 0;
}
if((range<1)&&(value!=0)) {
return sciFormat.format(value);
}
return(Math.abs(value)<=10) ? fixedFormat.format(value) : sciFormat.format(value);
}
/**
* Sets the plot axis and related labels.
*
* @param xAxis the x-axis label
* @param yAxis the y-axis label
*/
protected void setAxisLabels(String xAxis, String yAxis) {
if (xAxis==null || yAxis==null) return;
xVar = xAxis;
yVar = yAxis;
xAxis = TeXParser.removeSubscripting(xAxis);
yAxis = TeXParser.removeSubscripting(yAxis);
if (originShiftEnabled) {
xAxis = xAxis + DataToolTab.SHIFTED;
yAxis = yAxis + DataToolTab.SHIFTED;
}
// set axis labels
setXLabel(xAxis);
setYLabel(yAxis);
// set coordinate string builder variables
coordinateStrBuilder.setCoordinateLabels(xAxis+"=", " "+yAxis+"="); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
// set origin and selected point shift labels
String label = ToolsRes.getString("DataToolTab.Origin.Label")+": "; //$NON-NLS-1$ //$NON-NLS-2$
shiftXLabel.setText(label+xVar);
shiftYLabel.setText(yVar);
label = ToolsRes.getString("DataToolTab.Selection.Label")+": "; //$NON-NLS-1$ //$NON-NLS-2$
selectedXLabel.setText(label+xVar);
selectedYLabel.setText(yVar);
}
@Override
protected void setFontLevel(int level) {
super.setFontLevel(level);
}
/**
* Gets the mouse controller.
*
* @return the current mouse controller
*/
protected MouseInputAdapter getMouseController() {
return mouseController;
}
/**
* An inner class for selecting points on this plot.
*/
protected class SelectionBox extends Rectangle implements Drawable {
boolean visible = true;
int xstart, ystart;
Color color = new Color(0, 255, 0, 127);
@Override
public void setSize(int w, int h) {
int xoffset = Math.min(0, w);
int yoffset = Math.min(0, h);
w = Math.abs(w);
h = Math.abs(h);
super.setLocation(xstart+xoffset, ystart+yoffset);
super.setSize(w, h);
}
@Override
public void draw(DrawingPanel drawingPanel, Graphics g) {
if(visible) {
Graphics2D g2 = (Graphics2D) g;
g2.setColor(color);
g2.draw(this);
}
}
} // end SelectionBox class
/**
* An inner class to draw crossbars on the measured point.
*/
protected class Crossbars {
double x, y;
Color color = new Color(0, 0, 0);
/**
* Draws the crossbars on the specified Graphics.
*
* @param g the Graphics object
*/
public void draw(Graphics g) {
if(!positionVisible||java.lang.Double.isNaN(value)) {
return;
}
Color c = g.getColor();
g.setColor(color);
g.drawLine(getLeftGutter(), yToPix(y), getWidth()-getRightGutter()-1, yToPix(y));
g.drawLine(xToPix(x), getTopGutter(), xToPix(x), getHeight()-getBottomGutter()-1);
g.setColor(c);
}
} // end Crossbars class
/**
* An inner class to draw a slope line on the measured point.
*/
protected class SlopeLine extends Line2D.Double {
double x, y;
Stroke stroke = new BasicStroke(1.5f);
int length = 30;
Color color = new Color(102, 102, 102);
/**
* Draws this slope line on the specified Graphics.
*
* @param g the Graphics object
*/
public void draw(Graphics g) {
if(!slopeVisible||java.lang.Double.isNaN(slope)) {
return;
}
double dxPix = 1*getXPixPerUnit();
double dyPix = slope*getYPixPerUnit();
double hyp = Math.sqrt(dxPix*dxPix+dyPix*dyPix);
double sin = dyPix/hyp;
double cos = dxPix/hyp;
int xCenter = xToPix(x);
int yCenter = yToPix(y);
int len = length;
if (dataTool.slopeExtended) {
len *= 40;
int w = plot.getWidth()-plot.getRightGutter()-plot.getLeftGutter();
int h = plot.getHeight()-plot.getTopGutter()-plot.getBottomGutter();
Rectangle rect = new Rectangle(plot.getLeftGutter(), plot.getTopGutter(), w, h);
g.setClip(rect);
}
setLine(xCenter-len*cos+1, yCenter+len*sin+1, xCenter+len*cos+1, yCenter-len*sin+1);
Color gcolor = g.getColor();
g.setColor(color);
((Graphics2D) g).fill(stroke.createStrokedShape(this));
g.setColor(gcolor);
}
} // end SlopeLine class
/**
* An inner class that draws a vertical limit line for areas.
*/
protected class LimitLine extends Line2D.Double implements Selectable {
int pointIndex = -1;
double x, trueLimit;
Stroke stroke = new BasicStroke(1.0f);
Rectangle hitRect = new Rectangle();
Color color = new Color(51, 51, 51);
Color trueLimitColor;
Cursor move;
@Override
public void draw(DrawingPanel panel, Graphics g) {
if(!areaVisible) {
return;
}
if (trueLimitColor==null) {
trueLimitColor = color.brighter().brighter().brighter();
}
Color gcolor = g.getColor();
g.setColor(color);
int y0 = plot.getTopGutter();
int y1 = plot.getBounds().height-plot.getBottomGutter();
int x1 = plot.xToPix(x);
setLine(x1+1, y0, x1+1, y1);
((Graphics2D) g).fill(stroke.createStrokedShape(this));
// draw true limit
x1 = plot.xToPix(trueLimit);
g.setColor(trueLimitColor);
g.drawLine(x1+1, y0, x1+1, y1);
g.setColor(gcolor);
hitRect.setBounds(x1-2, y0, 6, y1-y0-20);
}
@Override
public Interactive findInteractive(DrawingPanel panel, int xpix, int ypix) {
if(areaVisible&&hitRect.contains(xpix, ypix)) {
return this;
}
return null;
}
@Override
public Cursor getPreferredCursor() {
if(move==null) {
// create cursor
String imageFile = "/org/opensourcephysics/resources/tools/images/limitcursor.gif"; //$NON-NLS-1$
Image im = ResourceLoader.getImage(imageFile);
move = GUIUtils.createCustomCursor(im, new Point(16, 16),
"Move Integration Limit", Cursor.MOVE_CURSOR); //$NON-NLS-1$
}
return move;
}
@Override
public void setXY(double x, double y) {
setX(x);
}
@Override
public void setX(double x) {
Dataset data = dataTable.workingData;
pointIndex = -1;
if (mouseEvent!=null && mouseEvent.isShiftDown()) {
pointIndex = findIndexNearestX(x, data);
}
this.x = (pointIndex==-1) ? x : data.getXPoints()[pointIndex];
refreshArea();
createMessage();
plot.setMessage(message);
}
@Override
public boolean isMeasured() {
return areaVisible;
}
@Override
public double getXMin() {
Dataset data = dataTable.workingData;
double dx = 0, min = 0;
if((data!=null)&&(data.getIndex()>1)) {
dx = Math.abs(data.getXMax()-data.getXMin());
min = Math.min(data.getXMax(), data.getXMin());
} else {
dx = Math.abs(areaLimits[0].x-areaLimits[1].x);
min = Math.min(areaLimits[0].x, areaLimits[1].x);
}
return min-0.02*dx;
}
@Override
public double getXMax() {
Dataset data = dataTable.workingData;
double dx = 0, max = 0;
if((data!=null)&&(data.getIndex()>1)) {
dx = Math.abs(data.getXMax()-data.getXMin());
max = Math.max(data.getXMax(), data.getXMin());
} else {
dx = Math.abs(areaLimits[0].x-areaLimits[1].x);
max = Math.max(areaLimits[0].x, areaLimits[1].x);
}
return max+0.02*dx;
}
@Override
public double getYMin() {
return(plot.getYMin()+plot.getYMax())/2;
}
@Override
public double getYMax() {
return(plot.getYMin()+plot.getYMax())/2;
}
/**
* refreshes the value of x based on current pointIndex.
*/
public void refreshX() {
double[] data = dataTable.workingData.getXPoints();
if (pointIndex>-1 && pointIndex<data.length
&& !java.lang.Double.isNaN(data[pointIndex])) {
x = data[pointIndex];
}
}
// the following methods are required by Selectable but not used
@Override
public void setY(double y) {}
@Override
public double getX() {
return x;
}
@Override
public double getY() {
return 0;
}
@Override
public void setSelected(boolean selectable) {}
@Override
public boolean isSelected() {
return false;
}
@Override
public void toggleSelected() {}
@Override
public boolean isEnabled() {
return true;
}
@Override
public void setEnabled(boolean enable) {}
} // end LimitLine class
/**
* An inner class that draws coordinate axes on the plot.
*/
protected class XYAxes extends TPoint {
Line2D axisLine = new Line2D.Double();
Stroke stroke = new BasicStroke(1.0f);
Color color = Color.green.darker();
Rectangle hitRectVert = new Rectangle(), hitRectHorz = new Rectangle();
Ellipse2D hitOrigin = new Ellipse2D.Double();
Point mouseDownPt;
double mouseDownShiftX, mouseDownShiftY;
boolean isHorzHit, isVertHit;
@Override
public void draw(DrawingPanel panel, Graphics g) {
Color gcolor = g.getColor();
g.setColor(color);
Graphics2D g2 = (Graphics2D)g;
int top = plot.getTopGutter();
int h = plot.getBounds().height;
int bottom = h-plot.getBottomGutter();
int xx = plot.xToPix(0);
int left = plot.getLeftGutter();
int w = plot.getBounds().width;
int right = w-plot.getRightGutter();
int yy = plot.yToPix(0);
g2.drawLine(xx, top, xx, bottom);
g2.drawLine(left, yy, right, yy);
if (originShiftEnabled) {
g2.drawOval(xx-6, yy-6, 12, 12);
}
g.setColor(gcolor);
// set up hit shapes
hitRectHorz.setBounds(left, yy-4, w, 8);
hitRectVert.setBounds(xx-4, top, 8, h);
hitOrigin.setFrameFromCenter(xx, yy, xx+6, yy+6);
}
@Override
public Interactive findInteractive(DrawingPanel panel, int xpix, int ypix) {
isHorzHit = false;
isVertHit = false;
if (originShiftEnabled) {
isHorzHit = hitRectHorz.contains(xpix, ypix);
isVertHit = hitRectVert.contains(xpix, ypix);
if (hitOrigin.contains(xpix, ypix)) {
isHorzHit = isVertHit = true;
}
if (isHorzHit || isVertHit) {
return this;
}
}
return null;
}
@Override
public boolean isMeasured() {
return originShiftEnabled;
}
@Override
public void setXY(double x, double y) {
}
@Override
public double getXMin() {
return getX() - getXSetback();
}
@Override
public double getXMax() {
return getX() + getXSetback();
}
@Override
public double getYMin() {
return getY() - getYSetback();
}
@Override
public double getYMax() {
return getY() + getYSetback();
}
/**
* Gets the minimum setback of the x-axis from the plot edges.
*
* @return the minimum setback
*/
private double getXSetback() {
if (originShiftEnabled && plot.dataPresent) {
Dataset data = dataTable.workingData;
double w = Math.abs(data.getXMax()-data.getXMin());
w = Math.max(w, Math.abs(getX()-data.getXMax()));
w = Math.max(w, Math.abs(getX()-data.getXMin()));
return w/20;
}
return 0;
}
/**
* Gets the minimum setback of the y-axis from the plot edges.
*
* @return the minimum setback
*/
private double getYSetback() {
if (originShiftEnabled && plot.dataPresent) {
Dataset data = dataTable.workingData;
double h = Math.abs(data.getYMax()-data.getYMin());
h = Math.max(h, Math.abs(getY()-data.getYMax()));
h = Math.max(h, Math.abs(getY()-data.getYMin()));
return h/20;
}
return 0;
}
} // end XYAxes class
} // end DataToolPlotter class
/**
* A class to undo/redo origin shift edits.
*/
protected class ShiftEdit extends AbstractUndoableEdit {
double[] redoShift, undoShift;
String[] columnName;
/**
* Contructor.
*
* @param colName the column names {xCol, yCol}
* @param newShift the new shift values {xShift, yShift}
* @param prevShift the previous shift values {xShift, yShift}
*/
public ShiftEdit(String[] colNames, double[] newShifts, double[] prevShifts) {
columnName = colNames;
redoShift = newShifts;
undoShift = prevShifts;
}
@Override
public void undo() throws CannotUndoException {
super.undo();
for (int i=0; i<columnName.length; i++) {
Dataset data = dataTable.getDataset(columnName[i]);
if (data!=null && data instanceof DataColumn) {
DataColumn dataCol = (DataColumn)data;
dataCol.setShift(undoShift[i]);
// shift area limits
if (i==0) {
if (plot.areaLimits[0].pointIndex>-1 && plot.areaLimits[1].pointIndex>-1) {
plot.areaLimits[0].refreshX();
plot.areaLimits[1].refreshX();
}
else {
plot.areaLimits[0].setX(plot.areaLimits[0].getX()+undoShift[0]-redoShift[0]);
plot.areaLimits[1].setX(plot.areaLimits[1].getX()+undoShift[0]-redoShift[0]);
}
}
}
}
refreshAll();
shiftEditListener.valueChanged = false;
}
@Override
public void redo() throws CannotUndoException {
super.redo();
for (int i=0; i<columnName.length; i++) {
Dataset data = dataTable.getDataset(columnName[i]);
if (data!=null && data instanceof DataColumn) {
DataColumn dataCol = (DataColumn)data;
dataCol.setShift(redoShift[i]);
// shift area limits
if (i==0) {
if (plot.areaLimits[0].pointIndex>-1 && plot.areaLimits[1].pointIndex>-1) {
plot.areaLimits[0].refreshX();
plot.areaLimits[1].refreshX();
}
else {
plot.areaLimits[0].setX(plot.areaLimits[0].getX()+redoShift[0]-undoShift[0]);
plot.areaLimits[1].setX(plot.areaLimits[1].getX()+redoShift[0]-undoShift[0]);
}
}
}
}
refreshAll();
shiftEditListener.valueChanged = false;
}
}
/**
* A number spinner model with a settable delta.
*/
class CrawlerSpinnerModel extends AbstractSpinnerModel {
double val = 0;
double delta = 1;
double percentDelta = 1;
public Object getValue() {
return new Double(val);
}
public Object getNextValue() {
return new Double(val+delta);
}
public Object getPreviousValue() {
return new Double(val-delta);
}
public void setValue(Object value) {
if (value!=null) {
val = ((Double) value).doubleValue();
fireStateChanged();
}
}
// public void setPercentDelta(double percent) {
// percentDelta = percent;
// }
//
// public double getPercentDelta() {
// return percentDelta;
// }
//
// refresh delta based on current value and percent
public void refreshDelta() {
if(val!=0) {
delta = Math.abs(val*percentDelta/100);
}
else {
if (shiftXSpinner.getModel()==this) {
Dataset dataset = dataTable.getDataset(plot.xVar);
if (dataset!=null) {
double range = dataset.getYMax()-dataset.getYMin();
delta = range*percentDelta/100;
}
}
else if (shiftYSpinner.getModel()==this) {
Dataset dataset = dataTable.getDataset(plot.yVar);
if (dataset!=null) {
double range = dataset.getYMax()-dataset.getYMin();
delta = range*percentDelta/100;
}
}
}
}
}
class ShiftEditListener implements ChangeListener, ActionListener {
// minimum time in ms between update requests
static final int MIN_TIME = 400;
long lastChange = System.currentTimeMillis();
boolean valueChanged = false;
Timer repeatTimer;
public ShiftEditListener(){
// timer polling every 200ms
repeatTimer = new Timer(200, this);
repeatTimer.start();
}
public void stateChanged(ChangeEvent e) {
valueChanged=true;
lastChange = System.currentTimeMillis();
}
// action called by timer
public void actionPerformed(ActionEvent e) {
if (valueChanged && (System.currentTimeMillis()-lastChange)>MIN_TIME){
valueChanged=false;
if (shiftXField.hasFocus() || shiftYField.hasFocus()) {
// post undoable edit
postShiftEdit();
}
else if (selectedXField.hasFocus() || selectedYField.hasFocus()) {
// post undoable edit
postShiftEdit();
}
}
}
}
//__________________________ static methods and classes ___________________________
/**
* Returns an ObjectLoader to save and load data for this class.
*
* @return the object loader
*/
public static XML.ObjectLoader getLoader() {
return new Loader();
}
/**
* A class to save and load data for this class.
*/
static class Loader implements XML.ObjectLoader {
@Override
public void saveObject(XMLControl control, Object obj) {
DataToolTab tab = (DataToolTab) obj;
// save name and owner name
control.setValue("name", tab.getName()); //$NON-NLS-1$
control.setValue("owner_name", tab.getOwnerName()); //$NON-NLS-1$
// save owned columns
if (!tab.ownedColumns.isEmpty()) {
String[][] columns = new String[tab.ownedColumns.size()][3];
// each element is {tab column name, owner/guest name, owner/guest dataset y-column name}
int i = 0;
for (String key: tab.ownedColumns.keySet()) {
String[] data = tab.ownedColumns.get(key);
columns[i] = new String[] {key, data[0], data[1]};
i++;
}
control.setValue("owned_columns", columns); //$NON-NLS-1$
}
// save userEditable
control.setValue("editable", tab.userEditable); //$NON-NLS-1$
// save data columns but leave out data functions
DatasetManager data = new DatasetManager();
ArrayList<Dataset> functions = new ArrayList<Dataset>();
for(Iterator<Dataset> it = tab.dataManager.getDatasets().iterator(); it.hasNext(); ) {
Dataset next = it.next();
if(next instanceof DataFunction) {
functions.add(next);
} else {
data.addDataset(next);
}
}
control.setValue("data", data); //$NON-NLS-1$
// save function parameters
String[] paramNames = tab.dataManager.getConstantNames();
if (paramNames.length>0) {
Object[][] paramArray = new Object[paramNames.length][4];
int i = 0;
for (String name: paramNames) {
paramArray[i][0] = name;
paramArray[i][1] = tab.dataManager.getConstantValue(name);
paramArray[i][2] = tab.dataManager.getConstantExpression(name);
paramArray[i][3] = tab.dataManager.getConstantDescription(name);
i++;
}
control.setValue("constants", paramArray); //$NON-NLS-1$
}
// save data functions
if(!functions.isEmpty()) {
DataFunction[] f = functions.toArray(new DataFunction[0]);
control.setValue("data_functions", f); //$NON-NLS-1$
}
// save origin shifted status
if (tab.originShiftEnabled) {
control.setValue("origin_shifted", tab.originShiftEnabled); //$NON-NLS-1$
}
// save selected fit function panel
// note: as of Dec 2014, no longer save ALL fitBuilder panels since most
// are for default or autoloaded functions
if (tab.dataTool.fitBuilder!=null && tab.curveFitter!=null) {
String fitName = tab.curveFitter.fit.getName();
FitFunctionPanel panel = (FitFunctionPanel)tab.dataTool.fitBuilder.getPanel(fitName);
if (panel!=null) {
ArrayList<FunctionPanel> fits = new ArrayList<FunctionPanel>();
fits.add(panel);
control.setValue("fits", fits); //$NON-NLS-1$
}
}
// save selected fit name
control.setValue("selected_fit", tab.curveFitter.fit.getName()); //$NON-NLS-1$
// save autofit status
control.setValue("autofit", tab.curveFitter.autofitCheckBox.isSelected()); //$NON-NLS-1$
// save fit parameters
// if (!tab.curveFitter.autofitCheckBox.isSelected()) {
double[] params = new double[tab.curveFitter.paramModel.getRowCount()];
for(int i = 0; i<params.length; i++) {
Double val = (Double) tab.curveFitter.paramModel.getValueAt(i, 1);
params[i] = val.doubleValue();
}
control.setValue("fit_parameters", params); //$NON-NLS-1$
// }
// save fit color
control.setValue("fit_color", tab.curveFitter.color); //$NON-NLS-1$
// save fit visibility
control.setValue("fit_visible", tab.fitterCheckbox.isSelected()); //$NON-NLS-1$
// save props visibility
control.setValue("props_visible", tab.propsCheckbox.isSelected()); //$NON-NLS-1$
// save statistics visibility
control.setValue("stats_visible", tab.statsCheckbox.isSelected()); //$NON-NLS-1$
// save splitPane locations
int loc = tab.splitPanes[0].getDividerLocation();
control.setValue("split_pane", loc); //$NON-NLS-1$
loc = tab.curveFitter.splitPane.getDividerLocation();
control.setValue("fit_split_pane", loc); //$NON-NLS-1$
// save model column order
int[] cols = tab.dataTable.getModelColumnOrder();
control.setValue("column_order", cols); //$NON-NLS-1$
// save hidden markers
String[] hidden = tab.dataTable.getHiddenMarkers();
control.setValue("hidden_markers", hidden); //$NON-NLS-1$
// save column format patterns, if any
String[] patternColumns = tab.dataTable.getFormattedColumnNames();
if(patternColumns.length>0) {
ArrayList<String[]> patterns = new ArrayList<String[]>();
for(int i=0; i<patternColumns.length; i++) {
String colName = patternColumns[i];
String pattern = tab.dataTable.getFormatPattern(colName);
patterns.add(new String[] {colName, pattern});
}
control.setValue("format_patterns", patterns); //$NON-NLS-1$
}
}
@Override
public Object createObject(XMLControl control) {
// get DataTool from control
DataTool dataTool = (DataTool)control.getObject("datatool"); //$NON-NLS-1$
// load data
DatasetManager data = (DatasetManager) control.getObject("data"); //$NON-NLS-1$
if(data==null) {
return new DataToolTab(null, dataTool);
}
for(Dataset next : data.getDatasets()) {
next.setXColumnVisible(false);
}
return new DataToolTab(data, dataTool);
}
@Override
public Object loadObject(XMLControl control, Object obj) {
final DataToolTab tab = (DataToolTab) obj;
// load tab name and owner name, if any
tab.setName(control.getString("name")); //$NON-NLS-1$
tab.ownerName = control.getString("owner_name"); //$NON-NLS-1$
// load owned columns
String[][] columns = (String[][])control.getObject("owned_columns"); //$NON-NLS-1$
if (columns!=null) {
tab.ownedColumns.clear();
for (String[] next: columns) {
// next is {tab column name, owner/guest name, owner/guest dataset y-column name}
// column name becomes key in map to owner/guest data
String[] data = new String[] {next[1], next[2]};
tab.ownedColumns.put(next[0], data);
}
}
// load data functions and constants
Object[][] constants = (Object[][])control.getObject("constants"); //$NON-NLS-1$
if (constants!=null) {
for (int i=0; i<constants.length; i++) {
String name = (String)constants[i][0];
double val = (Double)constants[i][1];
String expression = (String)constants[i][2];
if (constants[i].length>=4) {
String desc = (String)constants[i][3];
tab.dataManager.setConstant(name, val, expression, desc);
}
else tab.dataManager.setConstant(name, val, expression);
}
}
Iterator<?> it = control.getPropertyContent().iterator();
while(it.hasNext()) {
XMLProperty prop = (XMLProperty) it.next();
if(prop.getPropertyName().equals("data_functions")) { //$NON-NLS-1$
XMLControl[] children = prop.getChildControls();
for(int i = 0; i<children.length; i++) {
DataFunction f = new DataFunction(tab.dataManager);
children[i].loadObject(f);
f.setXColumnVisible(false);
tab.dataManager.addDataset(f);
}
// refresh dataFunctions
ArrayList<Dataset> datasets = tab.dataManager.getDatasets();
for(int i = 0; i<datasets.size(); i++) {
if(datasets.get(i) instanceof DataFunction) {
((DataFunction) datasets.get(i)).refreshFunctionData();
}
}
tab.dataTable.refreshTable();
break;
}
}
// load userEditable
tab.userEditable = control.getBoolean("editable"); //$NON-NLS-1$
// load user fit function panels
ArrayList<?> fits = (ArrayList<?>) control.getObject("fits"); //$NON-NLS-1$
if(fits!=null) {
for(it = fits.iterator(); it.hasNext(); ) {
FitFunctionPanel panel = (FitFunctionPanel) it.next();
tab.dataTool.fitBuilder.addPanel(panel.getName(), panel);
}
}
// select fit
String fitName = control.getString("selected_fit"); //$NON-NLS-1$
tab.curveFitter.fitDropDown.setSelectedItem(fitName);
tab.curveFitter.selectFit(fitName);
// load fit parameters
final double[] params = (double[]) control.getObject("fit_parameters"); //$NON-NLS-1$
if (params!=null) {
for(int i = 0; i<params.length; i++) {
tab.curveFitter.setParameterValue(i, params[i]);
}
}
// load autofit
boolean autofit = control.getBoolean("autofit"); //$NON-NLS-1$
tab.curveFitter.autofitCheckBox.setSelected(autofit);
// load fit color
Color color = (Color) control.getObject("fit_color"); //$NON-NLS-1$
tab.curveFitter.setColor(color);
// load fit visibility
boolean vis = control.getBoolean("fit_visible"); //$NON-NLS-1$
tab.fitterCheckbox.setSelected(vis);
// // don't load load props visibility: always visible!
// vis = control.getBoolean("props_visible"); //$NON-NLS-1$
// tab.propsCheckbox.setSelected(vis);
// load stats visibility
vis = control.getBoolean("stats_visible"); //$NON-NLS-1$
tab.statsCheckbox.setSelected(vis);
// load splitPane locations
final int loc = control.getInt("split_pane"); //$NON-NLS-1$
final int fitLoc = control.getInt("fit_split_pane"); //$NON-NLS-1$
// load model column order
int[] cols = (int[]) control.getObject("column_order"); //$NON-NLS-1$
tab.dataTable.setModelColumnOrder(cols);
if(cols==null) { // for legacy files: load working columns
String[] names = (String[]) control.getObject("working_columns"); //$NON-NLS-1$
if(names!=null) {
tab.dataTable.setWorkingColumns(names[0], names[1]);
}
}
// load hidden markers
String[] hidden = (String[]) control.getObject("hidden_markers"); //$NON-NLS-1$
tab.dataTable.hideMarkers(hidden);
// load format patterns
ArrayList<?> patterns = (ArrayList<?>) control.getObject("format_patterns"); //$NON-NLS-1$
if(patterns!=null) {
for(it = patterns.iterator(); it.hasNext(); ) {
String[] next = (String[]) it.next();
tab.dataTable.setFormatPattern(next[0], next[1]);
}
}
// load origin_shited
final boolean origin_shifted = control.getBoolean("origin_shifted"); //$NON-NLS-1$
Runnable runner = new Runnable() {
public synchronized void run() {
tab.fitterAction.actionPerformed(null);
tab.propsAndStatsAction.actionPerformed(null);
tab.splitPanes[0].setDividerLocation(loc);
tab.curveFitter.splitPane.setDividerLocation(fitLoc);
if (origin_shifted) {
tab.originShiftCheckbox.doClick(0);
if (params!=null) {
for(int i = 0; i<params.length; i++) {
tab.curveFitter.setParameterValue(i, params[i]);
}
}
}
tab.dataTable.refreshTable();
tab.propsTable.refreshTable();
tab.tabChanged(false);
}
};
SwingUtilities.invokeLater(runner);
return obj;
}
} // end Loader class
}
/*
* Open Source Physics software is free software; you can redistribute
* it and/or modify it under the terms of the GNU General Public License (GPL) as
* published by the Free Software Foundation; either version 2 of the License,
* or(at your option) any later version.
* Code that uses any portion of the code in the org.opensourcephysics package
* or any subpackage (subdirectory) of this package must must also be be released
* under the GNU GPL license.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
* or view the license online at http://www.gnu.org/copyleft/gpl.html
*
* Copyright (c) 2007 The Open Source Physics project
* http://www.opensourcephysics.org
*/