/*
* 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/>
*/
/*
* The org.opensourcephysics.media.core package defines the Open Source Physics
* media framework for working with video and other media.
*
* Copyright (c) 2014 Douglas Brown and Wolfgang Christian.
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* 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
*
* For additional information and documentation on Open Source Physics,
* please see <http://www.opensourcephysics.org/>.
*/
package org.opensourcephysics.media.core;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.TreeSet;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JColorChooser;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.controls.XMLControl;
import org.opensourcephysics.controls.XMLControlElement;
import org.opensourcephysics.display.DrawingPanel;
import org.opensourcephysics.display.Interactive;
/**
* This is a Filter that corrects perspective in the source image.
*
* @author Douglas Brown
* @version 1.0
*/
public class PerspectiveFilter extends Filter {
// static fields
private static Color defaultColor = Color.RED;
private static FontRenderContext frc
= new FontRenderContext(null, // no AffineTransform
false, // no antialiasing
false); // no fractional metrics
// instance fields
private int[] pixelsIn, pixelsOut; // pixel color values
private double[][] matrix = new double[3][3]; // perspective transform matrix
private double[][] temp1 = new double[3][3]; // intermediate matrix
private double[][] temp2 = new double[3][3]; // intermediate matrix
private double[] xOut, yOut, xIn, yIn; // pixel positions on input and output images
private int interpolation = 2; // neighborhood size for color interpolation
private Quadrilateral quad;
private QuadEditor inputEditor, outputEditor;
private Point2D[][] inCornerPoints = new Point2D[10][];
private Point2D[][] outCornerPoints = new Point2D[10][];
private TreeSet<Integer> inKeyFrames = new TreeSet<Integer>();
private TreeSet<Integer> outKeyFrames = new TreeSet<Integer>();
private boolean fixedIn = false, fixedOut = true;
private int fixedKey = 0;
private PropertyChangeListener videoListener;
// inspector fields
private Inspector inspector;
private boolean disposing = false;
/**
* Constructs a PerspectiveFilter object.
*/
public PerspectiveFilter() {
quad = new Quadrilateral();
refresh();
hasInspector = true;
videoListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent e) {
int n = (Integer)e.getNewValue();
refreshCorners(n);
}
};
}
/**
* Applies the filter to a source image and returns the result.
*
* @param sourceImage the source image
* @return the filtered image
*/
public BufferedImage getFilteredImage(BufferedImage sourceImage) {
if(!isEnabled()) {
return sourceImage;
}
if(sourceImage!=source) {
initialize(sourceImage);
}
if(sourceImage!=input) {
gIn.drawImage(source, 0, 0, null);
}
setOutputToTransformed(input);
return output;
}
/**
* Gets whether this filter is enabled.
*
* @return <code>true</code> if this is enabled.
*/
@Override
public boolean isEnabled() {
boolean disabled = !super.isEnabled();
boolean editingInput = inspector!=null && inspector.isVisible()
&& inspector.tabbedPane.getSelectedComponent()==inputEditor;
if (disabled || editingInput)
return false;
return true;
}
/**
* Gets whether the super-class of this filter is enabled.
*
* @return <code>true</code> if super is enabled.
*/
public boolean isSuperEnabled() {
return super.isEnabled();
}
/**
* Gets the inspector for this filter.
*
* @return the inspector
*/
public synchronized JDialog getInspector() {
Inspector myInspector = inspector;
if (myInspector==null) {
myInspector = new Inspector();
}
if (myInspector.isModal() && vidPanel!=null) {
frame = JOptionPane.getFrameForComponent(vidPanel);
myInspector.dispose();
myInspector = new Inspector();
}
inspector = myInspector;
inspector.initialize();
return inspector;
}
/**
* Refreshes this filter's GUI
*/
public void refresh() {
super.refresh();
if(inspector!=null) {
inspector.setTitle(MediaRes.getString("Filter.Perspective.Title")); //$NON-NLS-1$
inspector.tabbedPane.setTitleAt(0, MediaRes.getString("PerspectiveFilter.Tab.Input")); //$NON-NLS-1$
inspector.tabbedPane.setTitleAt(1, MediaRes.getString("PerspectiveFilter.Tab.Output")); //$NON-NLS-1$
inspector.helpButton.setText(MediaRes.getString("PerspectiveFilter.Button.Help")); //$NON-NLS-1$
inspector.colorButton.setText(MediaRes.getString("PerspectiveFilter.Button.Color")); //$NON-NLS-1$
ableButton.setText(super.isEnabled() ? MediaRes.getString("Filter.Button.Disable") : //$NON-NLS-1$
MediaRes.getString("Filter.Button.Enable")); //$NON-NLS-1$
inputEditor.refreshGUI();
outputEditor.refreshGUI();
inputEditor.refreshFields();
outputEditor.refreshFields();
}
}
@Override
public void dispose() {
super.dispose();
if (vidPanel!=null && vidPanel.getVideo()!=null) {
vidPanel.removePropertyChangeListener("selectedpoint", quad); //$NON-NLS-1$
Video video = vidPanel.getVideo();
video.removePropertyChangeListener("nextframe", videoListener); //$NON-NLS-1$
removePropertyChangeListener("visible", vidPanel); //$NON-NLS-1$
}
source = input = output = null;
pixelsOut = null;
pixelsIn = null;
}
/**
* Sets the video panel.
*
* @param panel the video panel
*/
public void setVideoPanel(VideoPanel panel) {
VideoPanel prevPanel = vidPanel;
super.setVideoPanel(panel);
if (vidPanel!=null) {
// filter added
Video video = vidPanel.getVideo();
video.removePropertyChangeListener("nextframe", videoListener); //$NON-NLS-1$
video.addPropertyChangeListener("nextframe", videoListener); //$NON-NLS-1$
vidPanel.propertyChange(new PropertyChangeEvent(this, "perspective", null, this)); //$NON-NLS-1$
}
else if (prevPanel!=null) {
// filter removed
prevPanel.removeDrawable(quad);
Video video = prevPanel.getVideo();
video.removePropertyChangeListener("nextframe", videoListener); //$NON-NLS-1$
prevPanel.propertyChange(new PropertyChangeEvent(this, "perspective", this, null)); //$NON-NLS-1$
}
}
/**
* Sets the fixed position behavior (all frames identical).
*
* @param fix true to set the corner positions the same in all frames
* @param in true for input corners, false for output
*/
public void setFixed(boolean fix, boolean in) {
if (isFixed(in)!=fix) {
String filterState = new XMLControlElement(this).toXML();
if (in) fixedIn = fix;
else fixedOut = fix;
if (isFixed(in)) {
TreeSet<Integer> keyFrames = in? inKeyFrames: outKeyFrames;
keyFrames.clear();
saveCorners(fixedKey, in); // save input corners
}
support.firePropertyChange("fixed", filterState, null); //$NON-NLS-1$
}
}
/**
* Gets the fixed position behavior.
*
* @return true if fixed
*/
public boolean isFixed(boolean in) {
return in? fixedIn: fixedOut;
}
/**
* Sets the location of a corner.
*
* @param frameNumber the video frame number
* @param cornerIndex the corner index (0-3)
* @param x the x-position
* @param y the y-position
*/
public void setCornerLocation(int frameNumber, int cornerIndex, double x, double y) {
boolean in = cornerIndex<4;
Corner[] corners = in? quad.inCorners: quad.outCorners;
corners[cornerIndex].setXY(x, y);
}
/**
* Gets the color.
*
* @return the color
*/
public Color getColor() {
return quad.color;
}
/**
* Gets the index associated with a corner point.
* @param corner the corner
* @return the index
*/
public int getCornerIndex(Corner corner) {
for (int i=0; i<4; i++) {
if (corner==quad.inCorners[i]) return i;
if (corner==quad.outCorners[i]) return i+4;
}
return -1;
}
/**
* Gets the corner associated with an index.
* @param index the index (0-7)
* @return the corner
*/
public Corner getCorner(int index) {
Corner[] corners = index<4? quad.inCorners: quad.outCorners;
return corners[index%4];
}
/**
* Deletes the key frame associated with a corner.
* @param frameNumber the frame number
* @param corner the corner
*/
public void deleteKeyFrame(int frameNumber, Corner corner) {
int index = getCornerIndex(corner);
if (index==-1) return;
boolean isInput = index<4? true: false;
TreeSet<Integer> keyFrames = isInput? inKeyFrames: outKeyFrames;
int key = getKeyFrame(frameNumber, isInput);
if (key==0) return; // can't delete frame 0
keyFrames.remove(key);
Point2D[][] cornerPoints = index<4? inCornerPoints: outCornerPoints;
cornerPoints[key] = null;
refreshCorners(vidPanel.getFrameNumber());
}
/**
* Sets the inspector tab to input or output.
* @param enable true to show the input tab, false to show the output tab
*/
public void setInputEnabled(boolean enable) {
if (inspector==null) return;
inspector.tabbedPane.setSelectedComponent(enable? inputEditor: outputEditor);
}
/**
* Determines if the inspector tab is the input tab.
* @return true if the input tab is shown
*/
public boolean isInputEnabled() {
if (inspector==null) return false;
return inspector.tabbedPane.getSelectedComponent()==inputEditor;
}
/**
* Determines if the inspector is active.
* @return true if the active
*/
public boolean isActive() {
if (inspector==null) return false;
return inspector.tabbedPane.isEnabled();
}
/**
* Determines if the inspector has been instantiated.
* @return true if the inspector exists
*/
public boolean hasInspector() {
return inspector!=null;
}
//_____________________________ private methods _______________________
/**
* Creates the input and output images and ColorConvertOp.
*
* @param image a new input image
*/
private void initialize(BufferedImage image) {
source = image;
w = source.getWidth();
h = source.getHeight();
output = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
pixelsIn = new int[w*h];
pixelsOut = new int[w*h];
xIn = new double[w*h];
yIn = new double[w*h];
// output positions are integer pixels
xOut = new double[w*h];
yOut = new double[w*h];
for (int i=0; i<w; i++) {
for (int j=0; j<h; j++) {
xOut[j*w+i] = i;
yOut[j*w+i] = j;
}
}
if(source.getType()==BufferedImage.TYPE_INT_RGB) {
input = source;
} else {
input = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
gIn = input.createGraphics();
}
// initialize corner positions
if (inKeyFrames.isEmpty()) {
quad.inCorners[0].setLocation(w/4, h/4);
quad.inCorners[1].setLocation(3*w/4, h/4);
quad.inCorners[2].setLocation(3*w/4, 3*h/4);
quad.inCorners[3].setLocation(w/4, 3*h/4);
quad.outCorners[0].setLocation(w/4, h/4);
quad.outCorners[1].setLocation(3*w/4, h/4);
quad.outCorners[2].setLocation(3*w/4, 3*h/4);
quad.outCorners[3].setLocation(w/4, 3*h/4);
saveCorners(0, true);
saveCorners(0, false);
}
}
/**
* Sets the output image pixels to a rotated version of the input pixels.
*
* @param image the input image
*/
private void setOutputToTransformed(BufferedImage image) {
image.getRaster().getDataElements(0, 0, w, h, pixelsIn);
// set up transform matrix based on quad input/output corners
// set temp1 to transform output to square
getQuadToSquare(temp1,
quad.outCorners[0].getX(), quad.outCorners[0].getY(),
quad.outCorners[1].getX(), quad.outCorners[1].getY(),
quad.outCorners[2].getX(), quad.outCorners[2].getY(),
quad.outCorners[3].getX(), quad.outCorners[3].getY());
// set temp2 to transform square to input
getSquareToQuad(temp2,
quad.inCorners[0].getX(), quad.inCorners[0].getY(),
quad.inCorners[1].getX(), quad.inCorners[1].getY(),
quad.inCorners[2].getX(), quad.inCorners[2].getY(),
quad.inCorners[3].getX(), quad.inCorners[3].getY());
// concatenate temp2 to temp1 to obtain transform matrix output->input
concatenate(temp1, temp2);
// transform the output (pixel) positions to input positions
transform(xOut, yOut, xIn, yIn);
// find output pixel values by interpolating input pixels
for (int i=0; i<pixelsOut.length; i++) {
pixelsOut[i] = getColor(xIn[i], yIn[i], w, h, pixelsIn);
}
output.getRaster().setDataElements(0, 0, w, h, pixelsOut);
}
/**
* Transforms arrays of position coordinates using the current transform matrix.
*
* @param xSource array of source x-coordinates
* @param ySource array of source y-coordinates
* @param xTrans array of transformed x-coordinates
* @param yTrans array of transformed y-coordinates
*/
private void transform(double[] xSource, double[] ySource,
double[] xTrans, double[] yTrans) {
int n = xSource.length;
for (int i=0; i<n; i++) {
double w = matrix[2][0]*xSource[i] + matrix[2][1]*ySource[i] + matrix[2][2];
if (w==0) {
xTrans[i] = xSource[i];
yTrans[i] = ySource[i];
}
else {
xTrans[i] = (matrix[0][0]*xSource[i] + matrix[0][1]*ySource[i] + matrix[0][2])/w;
yTrans[i] = (matrix[1][0]*xSource[i] + matrix[1][1]*ySource[i] + matrix[1][2])/w;
}
}
}
/**
* Get the interpolated color at a non-integer position (between pixels points).
*
* @param x the x-coordinate of the position
* @param y the y-coordinate of the position
* @param w the width of the image
* @param h the height of the image
* @param pixelValues the color values of the pixels in the image
*/
private int getColor(double x, double y, int w, int h, int[] pixelValues) {
// get base pixel position
int col = (int)Math.floor(x);
int row = (int)Math.floor(y);
if (col<0 || col>=w || row<0 || row>=h) {
return 0; // black if not in image
}
if (col+1==w || row+1==h) {
return pixelValues[row*w+col];
}
double u = col==0? x: x%col;
double v = row==0? y: y%row;
if (interpolation==2) {
// get 2x2 neighborhood pixel values
int[] values = new int[] {pixelValues[row*w+col], pixelValues[row*w+col+1],
pixelValues[(row+1)*w+col], pixelValues[(row+1)*w+col+1]};
int[] rgb = new int[4];
for (int j=0; j<4; j++) {
rgb[j] = (values[j]>>16)&0xff; // red
}
int r = bilinearInterpolation(u, v, rgb);
for (int j=0; j<4; j++) {
rgb[j] = (values[j]>>8)&0xff; // green
}
int g = bilinearInterpolation(u, v, rgb);
for (int j=0; j<4; j++) {
rgb[j] = (values[j])&0xff; // blue
}
int b = bilinearInterpolation(u, v, rgb);
return (r<<16)|(g<<8)|b;
}
// if not interpolating, return value of nearest neighbor
return u<0.5? v<0.5? pixelValues[row*w+col]: pixelValues[(row+1)*w+col]:
v<0.5? pixelValues[row*w+col+1]: pixelValues[(row+1)*w+col+1];
}
/**
* Returns a bilinear interpolated pixel color (int value) at a given point relative to (0,0).
*
* @param x the x-position relative to 0,0 (0<=x<1)
* @param x the y-position relative to 0,0 (0<=y<1)
* @param values array of pixels color values [value(0,0), value(0,1), value(1,0), value(1,1)]
* @return the interpolated color value
*/
private int bilinearInterpolation(double x, double y, int[] values) {
return (int)((1-y)*((1-x)*values[0] + x*values[2]) + y*((1-x)*values[1] + x*values[3]));
}
/**
* Creates a transform matrix to map a unit square onto a quadrilateral.
*
* (0, 0) -> (x0, y0)
* (1, 0) -> (x1, y1)
* (1, 1) -> (x2, y2)
* (0, 1) -> (x3, y3)
*/
private void getSquareToQuad(double[][] matrix, double x0, double y0, double x1, double y1,
double x2, double y2, double x3, double y3) {
double dx3 = x0 - x1 + x2 - x3;
double dy3 = y0 - y1 + y2 - y3;
matrix[2][2] = 1.0F;
if ((dx3 == 0.0F) && (dy3 == 0.0F)) {
matrix[0][0] = x1 - x0;
matrix[0][1] = x2 - x1;
matrix[0][2] = x0;
matrix[1][0] = y1 - y0;
matrix[1][1] = y2 - y1;
matrix[1][2] = y0;
matrix[2][0] = 0.0F;
matrix[2][1] = 0.0F;
}
else {
double dx1 = x1 - x2;
double dy1 = y1 - y2;
double dx2 = x3 - x2;
double dy2 = y3 - y2;
double invdet = 1.0F/(dx1*dy2 - dx2*dy1);
matrix[2][0] = (dx3*dy2 - dx2*dy3)*invdet;
matrix[2][1] = (dx1*dy3 - dx3*dy1)*invdet;
matrix[0][0] = x1 - x0 + matrix[2][0]*x1;
matrix[0][1] = x3 - x0 + matrix[2][1]*x3;
matrix[0][2] = x0;
matrix[1][0] = y1 - y0 + matrix[2][0]*y1;
matrix[1][1] = y3 - y0 + matrix[2][1]*y3;
matrix[1][2] = y0;
}
}
/**
* Creates a transform matrix to map a quadrilateral onto a unit square.
*
* (x0, y0) -> (0, 0)
* (x1, y1) -> (1, 0)
* (x2, y2) -> (1, 1)
* (x3, y3) -> (0, 1)
*/
private void getQuadToSquare(double[][] matrix, double x0, double y0, double x1, double y1,
double x2, double y2, double x3, double y3) {
// get square to quad and convert to adjoint
getSquareToQuad(matrix, x0, y0, x1, y1, x2, y2, x3, y3);
// get sub-determinants
double m00 = matrix[1][1]*matrix[2][2] - matrix[1][2]*matrix[2][1];
double m01 = matrix[1][2]*matrix[2][0] - matrix[1][0]*matrix[2][2];
double m02 = matrix[1][0]*matrix[2][1] - matrix[1][1]*matrix[2][0];
double m10 = matrix[0][2]*matrix[2][1] - matrix[0][1]*matrix[2][2];
double m11 = matrix[0][0]*matrix[2][2] - matrix[0][2]*matrix[2][0];
double m12 = matrix[0][1]*matrix[2][0] - matrix[0][0]*matrix[2][1];
double m20 = matrix[0][1]*matrix[1][2] - matrix[0][2]*matrix[1][1];
double m21 = matrix[0][2]*matrix[1][0] - matrix[0][0]*matrix[1][2];
double m22 = matrix[0][0]*matrix[1][1] - matrix[0][1]*matrix[1][0];
// transpose
matrix[0][0] = m00;
matrix[0][1] = m10;
matrix[0][2] = m20;
matrix[1][0] = m01;
matrix[1][1] = m11;
matrix[1][2] = m21;
matrix[2][0] = m02;
matrix[2][1] = m12;
matrix[2][2] = m22;
}
/**
* Concatenates two transform matrices and puts the result into the transform matrix.
*/
private void concatenate(double[][] m1, double[][] m2) {
matrix[0][0] = m1[0][0]*m2[0][0] + m1[1][0]*m2[0][1] + m1[2][0]*m2[0][2];
matrix[1][0] = m1[0][0]*m2[1][0] + m1[1][0]*m2[1][1] + m1[2][0]*m2[1][2];
matrix[2][0] = m1[0][0]*m2[2][0] + m1[1][0]*m2[2][1] + m1[2][0]*m2[2][2];
matrix[0][1] = m1[0][1]*m2[0][0] + m1[1][1]*m2[0][1] + m1[2][1]*m2[0][2];
matrix[1][1] = m1[0][1]*m2[1][0] + m1[1][1]*m2[1][1] + m1[2][1]*m2[1][2];
matrix[2][1] = m1[0][1]*m2[2][0] + m1[1][1]*m2[2][1] + m1[2][1]*m2[2][2];
matrix[0][2] = m1[0][2]*m2[0][0] + m1[1][2]*m2[0][1] + m1[2][2]*m2[0][2];
matrix[1][2] = m1[0][2]*m2[1][0] + m1[1][2]*m2[1][1] + m1[2][2]*m2[1][2];
matrix[2][2] = m1[0][2]*m2[2][0] + m1[1][2]*m2[2][1] + m1[2][2]*m2[2][2];
}
private double[][] getCornerData(Point2D[] cornerPoints) {
double[][] data = new double[4][2];
for (int i=0; i<4; i++) {
data[i][0] = cornerPoints[i].getX();
data[i][1] = cornerPoints[i].getY();
}
return data;
}
private void refreshCorners(int frameNumber) {
// gIn can be null if memory limit was exceeded when trying to instantiate with large images
if (gIn==null && source!=null && input!=source) return;
int key = getKeyFrame(frameNumber, true); // input
for (int i=0; i<4; i++) {
quad.inCorners[i].setLocation(inCornerPoints[key][i]);
}
key = getKeyFrame(frameNumber, false); // output
for (int i=0; i<4; i++) {
quad.outCorners[i].setLocation(outCornerPoints[key][i]);
}
}
private void saveCorners(int frameNumber, boolean in) {
if (isFixed(in)) frameNumber = fixedKey;
ensureCornerCapacity(frameNumber);
TreeSet<Integer> keyFrames = in? inKeyFrames: outKeyFrames;
Point2D[][] cornerPoints = in? inCornerPoints: outCornerPoints;
Corner[] corners = in? quad.inCorners: quad.outCorners;
keyFrames.add(frameNumber);
if (cornerPoints[frameNumber]==null) {
cornerPoints[frameNumber] = new Point2D[4];
for (int i=0; i<4; i++) {
cornerPoints[frameNumber][i] = new Point2D.Double();
}
}
for (int i=0; i<4; i++) {
cornerPoints[frameNumber][i].setLocation(corners[i]);
}
}
private void loadCornerData(double[][][] cornerData, boolean in) {
ensureCornerCapacity(cornerData.length);
TreeSet<Integer> keyFrames = in? inKeyFrames: outKeyFrames;
keyFrames.clear();
Point2D[][] cornerPoints = in? inCornerPoints: outCornerPoints;
for (int j=0; j<cornerData.length; j++) {
if (cornerData[j]==null) continue;
keyFrames.add(j);
if (cornerPoints[j]==null) {
cornerPoints[j] = new Point2D[4];
for (int i=0; i<4; i++) {
cornerPoints[j][i] = new Point2D.Double();
}
}
for (int i=0; i<4; i++) {
cornerPoints[j][i].setLocation(cornerData[j][i][0], cornerData[j][i][1]);
}
}
}
private void ensureCornerCapacity(int index) {
int length = inCornerPoints.length;
if (length<index+1) {
Point2D[][] newArray = new Point2D[index+10][];
System.arraycopy(inCornerPoints, 0, newArray, 0, length);
inCornerPoints = newArray;
}
length = outCornerPoints.length;
if (length<index+1) {
Point2D[][] newArray = new Point2D[index+10][];
System.arraycopy(outCornerPoints, 0, newArray, 0, length);
outCornerPoints = newArray;
}
}
private void trimCornerPoints() {
int length = inCornerPoints.length;
for (int i=length; i>0; i--) {
if (inCornerPoints[i-1]!=null) {
Point2D[][] newArray = new Point2D[i][];
System.arraycopy(inCornerPoints, 0, newArray, 0, i);
inCornerPoints = newArray;
break;
}
}
length = outCornerPoints.length;
for (int i=length; i>0; i--) {
if (outCornerPoints[i-1]!=null) {
Point2D[][] newArray = new Point2D[i][];
System.arraycopy(outCornerPoints, 0, newArray, 0, i);
outCornerPoints = newArray;
break;
}
}
}
private int getKeyFrame(int frameNumber, boolean in) {
if (isFixed(in)) return fixedKey;
TreeSet<Integer> keyFrames = in? inKeyFrames: outKeyFrames;
int key = 0;
for (int i: keyFrames) {
if (i<=frameNumber)
key = i;
}
return key;
}
/**
* Inner Inspector class to control filter parameters
*/
private class Inspector extends JDialog {
JButton helpButton, colorButton;
JTabbedPane tabbedPane;
JPanel contentPane;
/**
* Constructs the Inspector.
*/
public Inspector() {
super(frame, !(frame instanceof org.opensourcephysics.display.OSPFrame));
inspector = this;
setResizable(false);
createGUI();
refresh();
pack();
// center on screen
Rectangle rect = getBounds();
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
int x = (dim.width-rect.width)/2;
int y = (dim.height-rect.height)/2;
setLocation(x, y);
}
/**
* Creates the visible components.
*/
void createGUI() {
tabbedPane = new JTabbedPane();
inputEditor = new QuadEditor(true);
outputEditor = new QuadEditor(false);
outputEditor.selectedShapeIndex = 1;
tabbedPane.addTab("", inputEditor); //$NON-NLS-1$
tabbedPane.addTab("", outputEditor); //$NON-NLS-1$
tabbedPane.setSelectedComponent(inputEditor);
// add change listener after adding tabs to prevent start-up event firing
tabbedPane.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
if (disposing) return;
refresh();
PerspectiveFilter.this.support.firePropertyChange("image", null, null); //$NON-NLS-1$
PerspectiveFilter.this.support.firePropertyChange("tab", null, null); //$NON-NLS-1$
}
});
helpButton = new JButton();
helpButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
String s = MediaRes.getString("PerspectiveFilter.Help.Message1") //$NON-NLS-1$
+"\n"+MediaRes.getString("PerspectiveFilter.Help.Message2") //$NON-NLS-1$ //$NON-NLS-2$
+"\n"+MediaRes.getString("PerspectiveFilter.Help.Message3") //$NON-NLS-1$ //$NON-NLS-2$
+"\n"+MediaRes.getString("PerspectiveFilter.Help.Message4") //$NON-NLS-1$ //$NON-NLS-2$
+"\n\n"+MediaRes.getString("PerspectiveFilter.Help.Message5") //$NON-NLS-1$ //$NON-NLS-2$
+"\n "+MediaRes.getString("PerspectiveFilter.Help.Message6") //$NON-NLS-1$ //$NON-NLS-2$
+"\n "+MediaRes.getString("PerspectiveFilter.Help.Message7") //$NON-NLS-1$ //$NON-NLS-2$
+"\n "+MediaRes.getString("PerspectiveFilter.Help.Message8") //$NON-NLS-1$ //$NON-NLS-2$
+"\n "+MediaRes.getString("PerspectiveFilter.Help.Message9"); //$NON-NLS-1$ //$NON-NLS-2$
JOptionPane.showMessageDialog(JOptionPane.getFrameForComponent(vidPanel), s,
MediaRes.getString("PerspectiveFilter.Help.Title"), //$NON-NLS-1$
JOptionPane.INFORMATION_MESSAGE);
}
});
colorButton = new JButton();
colorButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// show color chooser dialog with color of this filter's quad
Color newColor = JColorChooser.showDialog(null,
MediaRes.getString("PerspectiveFilter.Dialog.Color.Title"), //$NON-NLS-1$
quad.color);
if (newColor != null) {
quad.color = newColor;
support.firePropertyChange("color", null, newColor); //$NON-NLS-1$
}
}
});
// ableButton already has action listener to enable/disable this filter
ableButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// refresh button state
boolean enable = !PerspectiveFilter.super.isEnabled();
colorButton.setEnabled(enable);
tabbedPane.setEnabled(enable);
inputEditor.setEnabled(enable);
outputEditor.setEnabled(enable);
}
});
// add components to content pane
contentPane = new JPanel(new BorderLayout());
setContentPane(contentPane);
JPanel buttonbar = new JPanel(new FlowLayout());
buttonbar.add(helpButton);
buttonbar.add(colorButton);
buttonbar.add(ableButton);
buttonbar.add(closeButton);
contentPane.add(buttonbar, BorderLayout.SOUTH);
contentPane.add(tabbedPane, BorderLayout.CENTER);
}
/**
* Initializes this inspector
*/
void initialize() {
refresh();
pack();
}
@Override
public void dispose() {
disposing = true;
contentPane.remove(tabbedPane);
tabbedPane.removeAll();
super.dispose();
disposing = false;
}
@Override
public void setVisible(boolean vis) {
if (vis==isVisible()) return;
super.setVisible(vis);
if (vidPanel!=null) {
if (vis) {
vidPanel.addDrawable(quad);
support.firePropertyChange("visible", null, null); //$NON-NLS-1$
PerspectiveFilter.this.removePropertyChangeListener("visible", vidPanel); //$NON-NLS-1$
PerspectiveFilter.this.addPropertyChangeListener("visible", vidPanel); //$NON-NLS-1$
vidPanel.removePropertyChangeListener("selectedpoint", quad); //$NON-NLS-1$
vidPanel.addPropertyChangeListener("selectedpoint", quad); //$NON-NLS-1$
}
else {
support.firePropertyChange("visible", null, null); //$NON-NLS-1$
PerspectiveFilter.this.removePropertyChangeListener("visible", vidPanel); //$NON-NLS-1$
vidPanel.removePropertyChangeListener("selectedpoint", quad); //$NON-NLS-1$
vidPanel.removeDrawable(quad);
// fire MOUSE_RELEASED event to ensure full deselection in Tracker
java.awt.Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(new MouseEvent(
vidPanel,
MouseEvent.MOUSE_RELEASED,
0,
MouseEvent.BUTTON1_MASK,
-100, -100,
1,
false
));
}
}
boolean enable = PerspectiveFilter.super.isEnabled();
colorButton.setEnabled(enable);
tabbedPane.setEnabled(enable);
inputEditor.setEnabled(enable);
outputEditor.setEnabled(enable);
support.firePropertyChange("image", null, null); //$NON-NLS-1$
}
}
@SuppressWarnings("javadoc")
public class Corner extends TPoint {
/**
* Overrides TPoint setXY method.
*
* @param x the x coordinate
* @param y the y coordinate
*/
public void setXY(double x, double y) {
super.setXY(x, y);
boolean in = !PerspectiveFilter.this.isEnabled();
Corner[] corners = in? quad.inCorners: quad.outCorners;
QuadEditor editor = in? inputEditor: outputEditor;
if (editor.shapes[editor.selectedShapeIndex].equals("Rectangle")) { //$NON-NLS-1$
if (this==corners[0]) {
corners[3].x = x;
corners[1].y = y;
}
else if (this==corners[1]) {
corners[2].x = x;
corners[0].y = y;
}
else if (this==corners[2]) {
corners[1].x = x;
corners[3].y = y;
}
else if (this==corners[3]) {
corners[0].x = x;
corners[2].y = y;
}
}
saveCorners(vidPanel==null? 0: vidPanel.getFrameNumber(), in);
editor.refreshFields();
if (editor==outputEditor) {
PerspectiveFilter.this.support.firePropertyChange("image", null, null); //$NON-NLS-1$
}
// fire cornerlocation event
PerspectiveFilter.this.support.firePropertyChange("cornerlocation", null, this); //$NON-NLS-1$
if (vidPanel!=null) vidPanel.repaint();
}
}
/**
* Inner Quadrilateral class draws and provides interactive control
* of input and output quadrilateral shapes.
*/
private class Quadrilateral implements Trackable, Interactive,
PropertyChangeListener {
private Corner[] inCorners = new Corner[4]; // input image corner positions (image units)
private Corner[] outCorners = new Corner[4]; // output image corner positions
private Point2D[] screenPts = new Point2D[4];
private GeneralPath path = new GeneralPath();
private Stroke stroke = new BasicStroke(2);
private Stroke cornerStroke = new BasicStroke();
private Shape selectionShape = new Rectangle(-4, -4, 8, 8);
private Shape cornerShape = new Ellipse2D.Double(-5, -5, 10, 10);
private Shape[] hitShapes = new Shape[4];
private Shape[] drawShapes = new Shape[5];
private Corner selectedCorner;
private AffineTransform transform = new AffineTransform();
private TextLayout[] textLayouts = new TextLayout[4];
private Font font = new JTextField().getFont();
private Point p = new Point();
private Color color = defaultColor;
public Quadrilateral() {
for (int i = 0; i< inCorners.length; i++) {
inCorners[i] = new Corner();
outCorners[i] = new Corner();
textLayouts[i] = new TextLayout(String.valueOf(i), font, frc);
}
}
/**
* Responds to property change events.
*
* @param e the property change event
*/
public void propertyChange(PropertyChangeEvent e) {
Corner prev = selectedCorner;
selectedCorner = null;
for (int i=0; i<4; i++) {
if (e.getNewValue()==inCorners[i])
selectedCorner=inCorners[i];
else if (e.getNewValue()==outCorners[i])
selectedCorner=outCorners[i];
}
if (selectedCorner!=prev && vidPanel!=null)
vidPanel.repaint();
}
public void draw(DrawingPanel panel, Graphics g) {
if (!PerspectiveFilter.super.isEnabled()) return;
VideoPanel vidPanel = (VideoPanel)panel;
Corner[] corners = PerspectiveFilter.this.isEnabled()? outCorners: inCorners;
for (int i=0; i<4; i++) {
screenPts[i] = corners[i].getScreenPosition(vidPanel);
transform.setToTranslation(screenPts[i].getX(), screenPts[i].getY());
Shape s = corners[i]==selectedCorner? selectionShape: cornerShape;
Stroke sk = corners[i]==selectedCorner? stroke: cornerStroke;
hitShapes[i] = transform.createTransformedShape(s);
drawShapes[i] = sk.createStrokedShape(hitShapes[i]);
}
path.reset();
path.moveTo((float)screenPts[0].getX(), (float)screenPts[0].getY());
path.lineTo((float)screenPts[1].getX(), (float)screenPts[1].getY());
path.lineTo((float)screenPts[2].getX(), (float)screenPts[2].getY());
path.lineTo((float)screenPts[3].getX(), (float)screenPts[3].getY());
path.closePath();
drawShapes[4] = stroke.createStrokedShape(path);
Graphics2D g2 = (Graphics2D)g;
Color gcolor = g2.getColor();
g2.setColor(color);
Font gfont = g.getFont();
g2.setFont(font);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
for (int i=0; i< drawShapes.length; i++) {
g2.fill(drawShapes[i]);
}
for (int i=0; i<textLayouts.length; i++) {
p.setLocation(screenPts[i].getX()-4-font.getSize(), screenPts[i].getY()-6);
textLayouts[i].draw(g2, p.x, p.y);
}
g2.setFont(gfont);
g2.setColor(gcolor);
}
/**
* Finds the interactive drawable object located at the specified
* pixel position.
*
* @param panel the drawing panel
* @param xpix the x pixel position on the panel
* @param ypix the y pixel position on the panel
* @return the first corner that is hit
*/
public Interactive findInteractive(
DrawingPanel panel, int xpix, int ypix) {
if (!PerspectiveFilter.super.isEnabled()) return null;
if (!(panel instanceof VideoPanel) || !isEnabled()) return null;
for (int i = 0; i < hitShapes.length; i++) {
if (hitShapes[i]!=null && hitShapes[i].contains(xpix, ypix)) {
if (!PerspectiveFilter.this.isEnabled())
return inCorners[i];
return outCorners[i];
}
}
return null;
}
/**
* Return value is ignored since not measured
*
* @return 0
*/
public double getX () {
return 0;
}
/**
* Return value is ignored since not measured
*
* @return 0
*/
public double getY () {
return 0;
}
/**
* Empty setX method.
*
* @param x the x position
*/
public void setX(double x) {}
/**
* Empty setY method.
*
* @param y the y position
*/
public void setY(double y) {}
/**
* Empty setXY method.
*
* @param x the x position
* @param y the y position
*/
public void setXY(double x, double y) {}
/**
* Sets whether this responds to mouse hits.
*
* @param enabled <code>true</code> if this responds to mouse hits.
*/
public void setEnabled(boolean enabled) {
}
/**
* Gets whether this responds to mouse hits.
*
* @return <code>true</code> if this responds to mouse hits.
*/
public boolean isEnabled() {
return true;
}
/**
* Reports whether information is available to set min/max values.
*
* @return <code>false</code> since Quadrilateral knows only image coordinates
*/
public boolean isMeasured() {
return false;
}
/**
* Gets the minimum x needed to draw this object.
*
* @return 0
*/
public double getXMin() {
return getX();
}
/**
* Gets the maximum x needed to draw this object.
*
* @return 0
*/
public double getXMax() {
return getX();
}
/**
* Gets the minimum y needed to draw this object.
*
* @return 0
*/
public double getYMin() {
return getY();
}
/**
* Gets the maximum y needed to draw this object.
*
* @return 0
*/
public double getYMax() {
return getY();
}
}
private class QuadEditor extends JPanel {
DecimalField[][] fields = new DecimalField[4][2];
boolean isInput;
String[] shapes = {"Any", "Rectangle"}; //$NON-NLS-1$ //$NON-NLS-2$
int selectedShapeIndex;
JComboBox shapeDropdown = new JComboBox();
JLabel shapeLabel = new JLabel();
JCheckBox fixedCheckbox;
boolean refreshing;
Box[] boxes = new Box[4];
TitledBorder cornersBorder = BorderFactory.createTitledBorder(""); //$NON-NLS-1$
QuadEditor(boolean input) {
super(new BorderLayout());
isInput = input;
ActionListener cornerSetter = new AbstractAction() {
public void actionPerformed(ActionEvent e) {
TPoint[] corners = isInput? quad.inCorners: quad.outCorners;
for (int i=0; i< fields.length; i++) {
if (e.getSource()==fields[i][0]
|| e.getSource()==fields[i][1]) {
corners[i].setXY(fields[i][0].getValue(), fields[i][1].getValue());
}
}
refreshFields();
PerspectiveFilter.this.support.firePropertyChange("image", null, null); //$NON-NLS-1$
}
};
JPanel fieldPanel = new JPanel(new GridLayout(2, 2));
fieldPanel.setBorder(cornersBorder);
for (int i=0; i< fields.length; i++) {
fields[i][0] = new DecimalField(4, 1);
fields[i][0].addActionListener(cornerSetter);
fields[i][1] = new DecimalField(4, 1);
fields[i][1].addActionListener(cornerSetter);
boxes[i] = Box.createHorizontalBox();
JLabel label = new JLabel(i+": x"); //$NON-NLS-1$
label.setBorder(BorderFactory.createEmptyBorder(2, 4, 2, 2));
boxes[i].add(label);
boxes[i].add(fields[i][0]);
label = new JLabel("y"); //$NON-NLS-1$
label.setBorder(BorderFactory.createEmptyBorder(2, 4, 2, 2));
boxes[i].add(label);
boxes[i].add(fields[i][1]);
boxes[i].setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
}
fieldPanel.add(boxes[0]);
fieldPanel.add(boxes[1]);
fieldPanel.add(boxes[3]);
fieldPanel.add(boxes[2]);
shapeLabel.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 4));
shapeDropdown.setBorder(BorderFactory.createEmptyBorder(4, 0, 4, 8));
shapeDropdown.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (refreshing) return;
selectedShapeIndex = shapeDropdown.getSelectedIndex();
if (shapes[selectedShapeIndex].equals("Rectangle")) { //$NON-NLS-1$
TPoint[] corners = isInput? quad.inCorners: quad.outCorners;
for (int i=0; i<4; i++) {
corners[i].setXY(corners[i].x, corners[i].y);
}
if (vidPanel!=null) vidPanel.repaint();
}
}
});
fixedCheckbox = new JCheckBox();
// fixedCheckbox.setSelected(isFixed(isInput));
fixedCheckbox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (refreshing || isFixed(isInput)==fixedCheckbox.isSelected()) return;
setFixed(fixedCheckbox.isSelected(), isInput);
}
});
// assemble editor
Box box = Box.createHorizontalBox();
box.add(shapeLabel);
box.add(shapeDropdown);
box.add(fixedCheckbox);
add(box, BorderLayout.NORTH);
add(fieldPanel, BorderLayout.CENTER);
refreshGUI();
refreshFields();
}
void refreshGUI() {
refreshing = true;
shapeLabel.setText(MediaRes.getString("PerspectiveFilter.Label.Shape")); //$NON-NLS-1$
cornersBorder.setTitle(MediaRes.getString("PerspectiveFilter.Corners.Title")); //$NON-NLS-1$
fixedCheckbox.setText(MediaRes.getString("PerspectiveFilter.Checkbox.Fixed")); //$NON-NLS-1$
shapeDropdown.removeAllItems();
if (this==inputEditor) {
shapeDropdown.addItem(MediaRes.getString("PerspectiveFilter.Shape.Any")); //$NON-NLS-1$
}
else {
for (int i=0; i<shapes.length; i++) {
shapeDropdown.addItem(MediaRes.getString("PerspectiveFilter.Shape."+shapes[i])); //$NON-NLS-1$
}
}
shapeDropdown.setSelectedIndex(selectedShapeIndex);
fixedCheckbox.setSelected(isFixed(isInput));
refreshing = false;
}
void refreshFields() {
Corner[] corners = this==outputEditor? quad.outCorners: quad.inCorners;
for (int i=0; i<4; i++) {
fields[i][0].setValue(corners[i].x);
fields[i][1].setValue(corners[i].y);
}
}
public void setEnabled(boolean b) {
shapeDropdown.setEnabled(b);
shapeLabel.setEnabled(b);
fixedCheckbox.setEnabled(b);
for (int i=0; i<boxes.length; i++) {
for (Component c: boxes[i].getComponents()) {
c.setEnabled(b);
}
}
cornersBorder.setTitleColor(b? shapeLabel.getForeground(): new Color(153,153,153));
}
}
/**
* Returns an XML.ObjectLoader to save and load filter data.
*
* @return the object loader
*/
public static XML.ObjectLoader getLoader() {
return new Loader();
}
/**
* A class to save and load filter data.
*/
static class Loader implements XML.ObjectLoader {
/**
* Saves data to an XMLControl.
*
* @param control the control to save to
* @param obj the filter to save
*/
public void saveObject(XMLControl control, Object obj) {
PerspectiveFilter filter = (PerspectiveFilter) obj;
filter.trimCornerPoints();
double[][][] data = new double[filter.inCornerPoints.length][][];
for (int i: filter.inKeyFrames) {
filter.refreshCorners(i);
data[i] = filter.getCornerData(filter.inCornerPoints[i]);
}
control.setValue("in_corners", data); //$NON-NLS-1$
data = new double[filter.outCornerPoints.length][][];
for (int i: filter.outKeyFrames) {
filter.refreshCorners(i);
data[i] = filter.getCornerData(filter.outCornerPoints[i]);
}
control.setValue("out_corners", data); //$NON-NLS-1$
if (filter.vidPanel!=null) {
VideoClip clip = filter.vidPanel.getPlayer().getVideoClip();
control.setValue("startframe", clip.getStartFrameNumber()); //$NON-NLS-1$
filter.refreshCorners(filter.vidPanel.getFrameNumber());
}
if (!filter.quad.color.equals(defaultColor)) {
control.setValue("color", filter.quad.color); //$NON-NLS-1$
}
if((filter.frame!=null)&&(filter.inspector!=null)&&filter.inspector.isVisible()) {
int x = filter.inspector.getLocation().x-filter.frame.getLocation().x;
int y = filter.inspector.getLocation().y-filter.frame.getLocation().y;
control.setValue("inspector_x", x); //$NON-NLS-1$
control.setValue("inspector_y", y); //$NON-NLS-1$
}
control.setValue("disabled", !filter.isSuperEnabled()); //$NON-NLS-1$
control.setValue("fixed_in", filter.fixedIn); //$NON-NLS-1$
control.setValue("fixed_out", filter.fixedOut); //$NON-NLS-1$
}
/**
* Creates a new filter.
*
* @param control the control
* @return the new filter
*/
public Object createObject(XMLControl control) {
return new PerspectiveFilter();
}
/**
* Loads a filter with data from an XMLControl.
*
* @param control the control
* @param obj the filter
* @return the loaded object
*/
public Object loadObject(XMLControl control, Object obj) {
final PerspectiveFilter filter = (PerspectiveFilter) obj;
if (control.getPropertyNames().contains("fixed_out")) { //$NON-NLS-1$
filter.fixedIn = control.getBoolean("fixed_in"); //$NON-NLS-1$
filter.fixedOut = control.getBoolean("fixed_out"); //$NON-NLS-1$
}
double[][][] data = (double[][][])control.getObject("in_corners"); //$NON-NLS-1$
if (data!=null) {
filter.loadCornerData(data, true);
}
data = (double[][][])control.getObject("out_corners"); //$NON-NLS-1$
if (data!=null) {
filter.loadCornerData(data, false);
}
for (int i: filter.inKeyFrames) {
filter.refreshCorners(i);
}
for (int i: filter.outKeyFrames) {
filter.refreshCorners(i);
}
int frame = control.getInt("startframe"); //$NON-NLS-1$
if (frame!=Integer.MIN_VALUE) {
filter.refreshCorners(frame);
}
if (filter.vidPanel!=null) {
filter.refreshCorners(filter.vidPanel.getFrameNumber());
}
if (control.getPropertyNames().contains("color")) { //$NON-NLS-1$
filter.quad.color = (Color)control.getObject("color"); //$NON-NLS-1$
}
filter.inspectorX = control.getInt("inspector_x"); //$NON-NLS-1$
filter.inspectorY = control.getInt("inspector_y"); //$NON-NLS-1$
boolean disable = control.getBoolean("disabled"); //$NON-NLS-1$
if (disable && filter.isSuperEnabled() || (!disable && !filter.isSuperEnabled())) {
filter.ableButton.doClick(0);
}
filter.refresh();
return obj;
}
}
}
/*
* 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
*/