/* This file is part of the source code for 3D-XplorMath-J, Version 1.0 (January 2008).
* Copyright (c) 2008 The 3D-XplorMath Consortium (http://3d-xplormath.org).
* This source code is released under a BSD License, which allows redistribution
* in source and binary form, with or without modification, provided copyright
* and license information are included, and with no warranty or guarantee of
* any kind. For details, see http://3d-xplormath.org/j/source/BSDLicense.txt
*/
package vmm.ode;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.ArrayList;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JTextField;
import javax.swing.event.ChangeEvent;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import vmm.actions.AbstractActionVMM;
import vmm.actions.ActionList;
import vmm.actions.ActionRadioGroup;
import vmm.actions.ToggleAction;
import vmm.core.BasicMouseTask2D;
import vmm.core.Decoration;
import vmm.core.Display;
import vmm.core.Exhibit;
import vmm.core.I18n;
import vmm.core.MouseTask;
import vmm.core.SaveAndRestore;
import vmm.core.TimerAnimation;
import vmm.core.Transform;
import vmm.core.Util;
import vmm.core.VMMSave;
import vmm.core.View;
/**
* Represents an ODE exhibit that will be displayed in two dimensions.
*/
abstract public class ODE_2D extends Exhibit {
public final static int ORBIT_TYPE_RUNGE_KUTTA = 0, ORBIT_TYPE_BOTH = 1, ORBIT_TYPE_EULER = 2; // (actual values are important)
private final static int DF_SPACING = 30; // number of pixels between points in direction field
private final static Color DIRECTION_FIELD_COLOR = Color.GRAY;
private final static Color RUNGE_KUTTA_ORBIT_COLOR = Color.RED;
private final static Color EULER_ORBIT_COLOR = Color.GREEN;
private final static Color OLD_RUNGE_KUTTA_ORBIT_COLOR = new Color(150,80,80);
private final static Color OLD_EULER_ORBIT_COLOR = new Color(80,150,80);
private final static Color RUNGE_KUTTA_PROJECTED_ORBIT_X_COLOR = Color.CYAN;
private final static Color RUNGE_KUTTA_PROJECTED_ORBIT_Y_COLOR = Color.MAGENTA;
private final static Color EULER_PROJECTED_ORBIT_X_COLOR = new Color(100,150,150);
private final static Color EULER_PROJECTED_ORBIT_Y_COLOR = new Color(150,100,150);
/**
* Tells whether the ODE is autonomous (i.e. independent of time). This final
* variable is set by the constructor {@link #ODE_2D(boolean, boolean, String[])}, with the actual value
* coming from the constructor of each subclass. Autonomous and non-autonomous
* vector fields have different behaviors.
*/
protected final boolean isAutonomous;
/**
* Tells whether it is possible to draw a vector field for the exhibit. This is
* set in the contructor {@link #ODE_2D(boolean, boolean, String[])}, with the actual value coming
* from the contructor of each subclass. If the value is false, then the
* methods {@link #vectorField_x(double, double, double)} and
* {@link #vectorField_y(double, double, double)} are never called. If the
* value is true, then these methods must be overridden to return the correct
* values. For an autonomous ODE, the display of the vector field
* is under control of the user. For a non-autonomous ODE, the vector field
* is shown only when an integral curve is being constructed. (Actually,
* a direction field rather than a vector field is drawn.)
*/
protected final boolean canShowVectorField;
/**
* The names used for the input boxes where the user can type in the
* initial values for an integeral curves. This array is set from the
* String parameters to the constructor {@link #ODE_2D(boolean, boolean, String[])}. There
* must be exactly as many strings as there are data items in the
* pointData array that is passed to {@link #nextEulerPoint(double[], double)},
* {@link #nextRungeKuttaPoint(double[], double)} and {@link #extractPointFromData(double[])}.
*/
protected final String[] inputLabelNames;
/**
* If set to false in the constructor of a subclass, then the radio buttons
* for selecting RungeKutta/Euler/Both will not be added to the control
* pandl (although they will still be in the Action menu).
*/
protected boolean addOrbitTypesToControlPanel = true;
/**
* If set to false in the constructor of a subclass, then the check box
* for turning animatino of orbit drawing will not be added to the control
* pandl (although the command will still be in the Action menu).
*/
protected boolean addAnimateCheckBoxToControlPanel = true;
/**
* If set to false in the constructor of a subclass, then the check box
* for controling whether orbits are drawn as solid lines will not be added to the control
* pandl (although the command will still be in the Action menu).
*/
protected boolean addLinesCheckBoxToControlPanel = true;
/**
* If set to false in the constructor of a subclass, then the command
* for showing projected orbits is not added to the Action menu.
*/
protected boolean addProjectedOrbitsAction = true;
/**
* Computes the next point on an integral curve, using Euler's method.
* The pointData parameter array contains the data for the current point.
* This data should be replaced with the data for the next point. The
* size of the array and the meaning of the data that it contains will
* vary from one subclass to another. However, for non-autonomous ODEs,
* the time should always be the first parameter in the array.
* The second parameter, dt, gives the time step from the current
* point to the next point.
*/
abstract protected void nextEulerPoint(double[] pointData, double dt);
/**
* Computes the next point on an integral curve, using the Runge-Kutta method.
* The pointData parameter array contains the data for the current point.
* This data should be replaced with the data for the next point. The
* size of the array and the meaning of the data that it contains will
* vary from one subclass to another. But for the non-autonmous case,
* the time should always be the first value in the array.
* The second parameter, dt, gives the time step from the current point to the next point.
*/
abstract protected void nextRungeKuttaPoint(double[] pointData, double dt);
/**
* Extract from the data for a point on the integral curve the actual
* point that is to be displayed on the screen. The default implementation
* in this class takes the point from the first two components of the
* array for an autonomous ODE and from the second and third components
* for a non-autonomous ODE, allowing the first spot for the current time.
* Note that for non-autonomous ODEs, the time must be in the
* first spot in the array.
*/
protected Point2D extractPointFromData(double[] pointData) {
if (isAutonomous)
return new Point2D.Double(pointData[0],pointData[1]);
else
return new Point2D.Double(pointData[1],pointData[2]);
}
/**
* Returns the x-component of the vector field at point (x,y) and time t.
* For autonomous ODEs, the result should be independent of t. The
* default implementation here simply returns 0. An exhibit that
* sets the {@link #canShowVectorField} propery to true should
* override this method to compute the correct value.
*/
protected double vectorField_x(double x, double y, double t) {
return 0;
}
/**
* Returns the x-component of the vector field at point (x,y) and time t.
* For autonomous ODEs, the result should be independent of t. The
* default implementation here simply returns 0. An exhibit that
* sets the {@link #canShowVectorField} propery to true should
* override this method to compute the correct value.
*/
protected double vectorField_y(double x, double y, double t) {
return 0;
}
/**
* Should construct and return a mouse task appropriate for this exhibit, to be used
* in the specified view. The default return value is a standard
* BasicMouseTask2D; subclasses can redefine this to add the ability to input
* the initial conditions with the mouse.
*/
protected MouseTask makeDefaultMouseTask(ODEView view) {
return new BasicMouseTask2D();
}
/**
* The value of this variable is used as the initial value of dt in the Control Panel
* of any view of this exhbit. The default is 0.05, but it can be reset in the constructor
* of a subclass.
*/
protected double dtDefault = 0.05;
/**
* The value of this variable is used as the initial value of Time Span in the Control Panel
* of any view of this exhbit. The default is 10.0, but it can be reset in the constructor
* of a subclass.
*/
protected double timeSpanDefault = 10;
/**
* Contains the initial conditions that are placed in the control panel when the
* exhibit is first created. The value in this class is null, which means that
* the initial condition input boxed in the control panel will be empty when it
* first appears. Subclasses can set a different value. The value should be
* an array of doubles, where the first several items have the same meaning as
* the parameter in such methods as {@link #nextEulerPoint(double[], double)} and
* {@link #extractPointFromData(double[])}. In addition there can be up to
* two additional items. The first additional item, if present and greater than
* zero, specifies a dt for the initial orbit. The second additional item, if
* present and greater than zero, specifies a timeSpan for the initial orbit.
* If these values are not present or are less than or equal to zero, then
* the default dt/TimeSpan of the exhbit is used.
*/
protected double[] initialDataDefault = null;
/**
* Constructor sets the default background to be black, and sets the
* value of the final "isAutonomous" property and the lables to be used
* for the input boxes for initial conditions. For a non-autonomous ODE,
* the first lable should be the label for the initial time input, generally
* just "t".
*/
protected ODE_2D(boolean canShowVectorField, boolean isAutonomous, String... inputLabelName) {
setDefaultBackground(Color.BLACK);
this.canShowVectorField = canShowVectorField;
this.isAutonomous = isAutonomous;
this.inputLabelNames = inputLabelName;
}
/**
* For an ODE view, returns the animation that redraws the current orbit, if there is one,
* or draws the default orbit as defined by {@link #initialDataDefault}, if the value of
* that variable is non-null. Otherwise, the return value is null.
*/
public TimerAnimation getCreateAnimation(View view) {
if (view instanceof ODEView)
return ((ODEView)view).makeCreateAnimation();
else
return null;
}
/**
* Draws a direction field, but only for an exhbit in which the {@link #canShowVectorField}
* property is true and then only if the exhibit is being displayed in an ODEView whose
* showDirectionField property is turned on (or in the unlikely event that it is being
* displayed in some other type of View).
* @see ODEView#setShowDirectionField(boolean)
*/
protected void doDraw(Graphics2D g, View view, Transform transform) {
if ( ! canShowVectorField )
return;
if ( !(view instanceof ODEView) || ((ODEView)view).showDirectionField)
drawDirectionField(view,transform);
}
private void drawDirectionField(View view, Transform transform) { // draws the direction field
double spacing = DF_SPACING * transform.getPixelWidth(); // spacing between points, in terms of xy-coords, not pixels.
int startX, endX, startY, endY; // limits for the for loops, computed so that the direction field fills the view
startX = (int)( transform.getXmin()/spacing ) - 1;
endX = (int)( transform.getXmax()/spacing ) + 1;
startY = (int)( transform.getYmin()/spacing ) - 1;
endY = (int)( transform.getYmax()/spacing ) + 1;
Color c = view.getColor();
view.setColor(DIRECTION_FIELD_COLOR);
double currentTime = 0;
if (view instanceof ODEView)
currentTime = ((ODEView)view).currentTime;
for (int i = startX; i <= endX; i++) {
double x = i*spacing;
for (int j = startY; j <= endY; j++) {
double y = j*spacing;
double dx = vectorField_x(x,y,currentTime);
double dy = vectorField_y(x, y, currentTime);
double length = Math.sqrt(dx*dx + dy*dy);
if (Double.isNaN(length)) {
}
else if (Math.abs(length) < 0.000001) {
view.drawPixel(x,y);
}
else {
dx = dx/length * (spacing/4);
dy = dy/length * (spacing/4);
view.drawLine(x-dx,y-dy,x+dx,y+dy);
}
}
}
view.setColor(c);
}
/**
* Returns a view of type {@link ODEView}.
*/
public View getDefaultView() {
return new ODEView();
}
/**
* Represents the default view for an ODEFirstOrder2D.
*/
public class ODEView extends View {
private boolean showDirectionField; // (Saved, by hand, for the autonomous case only).
@VMMSave private boolean showProjectedOrbits = false;
@VMMSave private boolean showControlPanel = true;
@VMMSave private int orbitType = ORBIT_TYPE_RUNGE_KUTTA;
@VMMSave private boolean connectDotsOnOrbit; // Should orbits be drawn as dots or as solid lines?
private boolean animateDrawing = true;
private Orbit currentOrbit; // The most recently added orbit, shown in green; some commands operate on this orbit.
private ControlPanel controlPanel;
private ProjectedOrbitView projectedOrbitView;
private double[] initialDataForCreateAnimation;
private double currentTime; // used only for non-autonomous ODES, while drawing an orbit;
protected ToggleAction showDirectionFieldToggle;
protected ToggleAction showProjectedOrbitsToggle = new ToggleAction(I18n.tr("vmm.ode.command.ShowProjectedOrbits"),false) {
public void actionPerformed(ActionEvent evt) {
setShowProjectedOrbits(getState());
}
};
protected ToggleAction animateDrawingToggle = new ToggleAction(I18n.tr("vmm.ode.command.AnimateDrawing"),true) {
public void actionPerformed(ActionEvent evt) {
setAnimateDrawing(getState());
}
};
protected ToggleAction showControlPanelToggle = new ToggleAction(I18n.tr("vmm.ode.command.ShowControlPanel"),true) {
public void actionPerformed(ActionEvent evt) {
setShowControlPanel(getState());
}
};
protected AbstractActionVMM continueOrbitAction = new AbstractActionVMM(I18n.tr("vmm.ode.command.ContinueOrbit")) {
public void actionPerformed(ActionEvent evt) {
Orbit orbit = getCurrentOrbit();
if (orbit != null) {
double timeSpan;
try {
timeSpan = Double.parseDouble(controlPanel.timeSpanInput.getText());
}
catch (Exception e) {
timeSpan = timeSpanDefault;
controlPanel.timeSpanInput.setText(""+timeSpanDefault);
}
int numberOfPoints = (int)(timeSpan/orbit.dt + 0.5);
controlPanel.dtInput.setText(""+orbit.dt);
if (animateDrawing)
getDisplay().installAnimation(new ExtendOrbitAnimation(ODEView.this,orbit,numberOfPoints));
else
orbit.setPointCount(orbit.getPointCount() + numberOfPoints);
}
}
};
protected ToggleAction connectDotsToggle = new ToggleAction(I18n.tr("vmm.ode.command.ConnectDotsOnOrbit"),false) {
public void actionPerformed(ActionEvent evt) {
setConnectDotsOnOrbit(getState());
}
};
protected AbstractActionVMM eraseOrbitsAction = new AbstractActionVMM(I18n.tr("vmm.ode.command.EraseOrbits")) {
public void actionPerformed(ActionEvent evt) {
if (getDisplay() != null)
getDisplay().stopAnimation();
setCurrentOrbit(null);
Decoration[] decorations = getDecorations();
for (Decoration dec : decorations)
if (dec instanceof Orbit)
removeDecoration(dec);
}
};
protected ActionRadioGroup orbitTypeSelect = new ActionRadioGroup( new String[] {
I18n.tr("vmm.ode.command.orbitType.RungeKutta"),
I18n.tr("vmm.ode.command.orbitType.Euler"),
I18n.tr("vmm.ode.command.orbitType.Both"),
}, 0) {
public void optionSelected(int option) {
int style;
if (option == 0)
style = ORBIT_TYPE_RUNGE_KUTTA;
else if (option == 1)
style = ORBIT_TYPE_EULER;
else
style = ORBIT_TYPE_BOTH;
setOrbitType(style);
}
};
public ODEView() {
setShowAxes(true);
setAntialiased(true);
continueOrbitAction.setEnabled( getCurrentOrbit() != null );
controlPanel = new ControlPanel(this);
showDirectionField = isAutonomous && canShowVectorField;
if (isAutonomous && canShowVectorField)
showDirectionFieldToggle = new ToggleAction(I18n.tr("vmm.ode.command.ShowDirectionField"),true) {
public void actionPerformed(ActionEvent evt) {
setShowDirectionField(getState());
}
};
}
public boolean getShowDirectionField() {
return showDirectionField;
}
/**
* The showDirectionField property determines whether a direction field is drawn for the exhibit in this view.
* This property is true by default. This method has no effect if the exhbit can't show direction fields.
*/
public void setShowDirectionField(boolean showDirectionField) {
if (!canShowVectorField)
return;
if (this.showDirectionField == showDirectionField)
return;
this.showDirectionField = showDirectionField;
if (showDirectionFieldToggle != null)
showDirectionFieldToggle.setState(showDirectionField);
forceRedraw();
}
public boolean getAnimateDrawing() {
return animateDrawing;
}
public void setAnimateDrawing(boolean animateDrawing) {
this.animateDrawing = animateDrawing;
animateDrawingToggle.setState(animateDrawing);
}
/**
* Returns the current orbit (shown in green), if any.
*/
/**
* Returns the current orbit (shown in green), if any.
*/
protected Orbit getCurrentOrbit() {
return currentOrbit;
}
/**
* Sets the current orbit (shown in green). When a new orbit is added to the exhibit,
* this method is called, so that the new orbit becomes the current orbit.
*/
protected void setCurrentOrbit(Orbit orbit) {
if (currentOrbit == orbit)
return;
if (currentOrbit != null)
currentOrbit.setIsCurrentOrbit(false);
currentOrbit = orbit;
if (currentOrbit != null)
currentOrbit.setIsCurrentOrbit(true);
continueOrbitAction.setEnabled( currentOrbit != null);
}
/**
* Tells whether orbits should be drawn as dots or as solid lines.
* @see #setConnectDotsOnOrbit(boolean)
*/
public boolean getConnectDotsOnOrbit() {
return connectDotsOnOrbit;
}
/**
* Set the property that tells whether an orbit should be drawn simply as a sequence of dots,
* or the dots should be connected to make a solid curve. If there is a current (green) orbit
* when the property is changed, the new value is applied immediately to the current orbit.
* It also applies to new orbits created in the future. It does not change the appearance
* of old (red) orbits that exist when the value of the property is set.
*/
public void setConnectDotsOnOrbit(boolean connectDotsOnOrbit) {
if (this.connectDotsOnOrbit == connectDotsOnOrbit)
return;
this.connectDotsOnOrbit = connectDotsOnOrbit;
connectDotsToggle.setState(connectDotsOnOrbit);
if (currentOrbit != null)
currentOrbit.setStyle(connectDotsOnOrbit ? OrbitPoints2D.LINES : OrbitPoints2D.DOTS);
}
public boolean getShowProjectedOrbits() {
return showProjectedOrbits;
}
public int getOrbitType() {
return orbitType;
}
public void setOrbitType(int type) {
if (type == orbitType)
return;
orbitType = type;
if (currentOrbit != null) {
currentOrbit.setOrbitType(orbitType);
if (showProjectedOrbits)
projectedOrbitView.forceRedraw();
}
}
public boolean getShowControlPanel() {
return showControlPanel;
}
public void setShowControlPanel(boolean showControlPanel) {
if (this.showControlPanel == showControlPanel)
return;
this.showControlPanel = showControlPanel;
showControlPanelToggle.setState(showControlPanel);
if (showControlPanel) {
if (getDisplay() != null) {
getDisplay().getHolder().add(controlPanel,BorderLayout.EAST);
getDisplay().getHolder().validate();
}
}
else {
if (getDisplay() != null) {
getDisplay().getHolder().remove(controlPanel);
getDisplay().getHolder().validate();
}
}
}
/**
* When the showProjectedOrbits property is true, an auxiliary view is added to the bottom of the
* display where the x- and y-coordinates of the points on the current orbit are plotted.
* This property is false by default.
*/
public void setShowProjectedOrbits(boolean showProjectedOrbits) {
if (this.showProjectedOrbits == showProjectedOrbits)
return;
this.showProjectedOrbits = showProjectedOrbits;
showProjectedOrbitsToggle.setState(showProjectedOrbits);
if (showProjectedOrbits) {
projectedOrbitView = new ProjectedOrbitView(this);
if (getCurrentOrbit() != null) {
projectedOrbitView.setMaxPoints(getCurrentOrbit().getPointCount());
projectedOrbitView.resetPointsFromOrbit(getCurrentOrbit());
}
if (getDisplay() != null)
getDisplay().installAuxiliaryView(this, projectedOrbitView, Display.AUX_VIEW_ON_BOTTOM, 0.2, true);
}
else {
projectedOrbitView = null;
if (getDisplay() != null)
getDisplay().installAuxiliaryView(this, null);
}
}
public ActionList getActions() {
ActionList actions = super.getActions();
actions.add(continueOrbitAction);
actions.add(eraseOrbitsAction);
actions.add(connectDotsToggle);
actions.add(animateDrawingToggle);
actions.add(null);
actions.add(orbitTypeSelect);
actions.add(null);
if (showDirectionFieldToggle != null)
actions.add(showDirectionFieldToggle);
actions.add(showControlPanelToggle);
if (addProjectedOrbitsAction)
actions.add(showProjectedOrbitsToggle);
return actions;
}
public void setDisplay(Display display) {
super.setDisplay(display);
if (display != null)
display.setStopAnimationsOnResize(false);
if (display != null && projectedOrbitView != null)
display.installAuxiliaryView(this, projectedOrbitView, Display.AUX_VIEW_ON_BOTTOM, 0.2, true);
if (display != null && showControlPanel) {
display.getHolder().add(controlPanel,BorderLayout.EAST);
display.getHolder().validate();
}
}
public void setExhibit(Exhibit ex) {
super.setExhibit(ex);
if (ex != null && ex == ODE_2D.this) // It better equal ODE_2D.this!
initialDataForCreateAnimation = initialDataDefault;
}
public void stateChanged(ChangeEvent evt) {
super.stateChanged(evt);
Object source = evt.getSource();
if (projectedOrbitView != null && source instanceof Transform)
projectedOrbitView.forceRedraw();
}
public MouseTask getDefaultMouseTask() {
return makeDefaultMouseTask(this);
}
public double getCurrentTimeFromControlPanel() { // for use by mouse tasks to construct initialPointData
if (isAutonomous)
return 0;
try {
return Double.parseDouble(controlPanel.icInputs[0].getText());
}
catch (NumberFormatException e) {
controlPanel.icInputs[0].setText("0");
return 0;
}
}
public void startOrbitAtPoint(double[] initialPointData) {
double dt, timeSpan;
try {
dt = Double.parseDouble(controlPanel.dtInput.getText());
if (dt <= 0)
throw new Exception();
}
catch (Exception e) {
dt = dtDefault;
controlPanel.dtInput.setText(""+dtDefault);
}
try {
timeSpan = Double.parseDouble(controlPanel.timeSpanInput.getText());
if (timeSpan <= 0)
throw new Exception();
}
catch (Exception e) {
timeSpan = timeSpanDefault;
controlPanel.timeSpanInput.setText(""+timeSpanDefault);
}
controlPanel.resetStartPointInputText(initialPointData);
startOrbitAtPoint(initialPointData, dt, timeSpan);
}
private TimerAnimation makeCreateAnimation() {
if (!animateDrawing)
return null;
if (currentOrbit == null && initialDataForCreateAnimation == null)
return null;
Orbit orbit;
double dt;
double timeSpan;
if (currentOrbit != null) {
orbit = currentOrbit;
dt = currentOrbit.dt;
timeSpan = dt * currentOrbit.pointCount;
currentOrbit.removePoints();
}
else {
int dataCt = inputLabelNames.length; // number of intial condition values in initialDataDefault
if (initialDataForCreateAnimation.length > dataCt && initialDataForCreateAnimation[dataCt] > 0)
dt = initialDataForCreateAnimation[dataCt];
else
dt = dtDefault;
if (initialDataForCreateAnimation.length > dataCt + 1 && initialDataForCreateAnimation[dataCt+1] > 0)
timeSpan = initialDataForCreateAnimation[dataCt+1];
else
timeSpan = timeSpanDefault;
double[] data = new double[dataCt];
for (int i = 0; i < dataCt; i++)
data[i] = initialDataForCreateAnimation[i];
orbit = new Orbit(this,data,ORBIT_TYPE_RUNGE_KUTTA,dt);
controlPanel.resetStartPointInputText(initialDataForCreateAnimation);
addDecoration(orbit);
setCurrentOrbit(orbit);
}
initialDataForCreateAnimation = null;
int numberOfPoints = (int)(timeSpan/dt) + 1; // how many points should be added to curve during animation
controlPanel.dtInput.setText(""+dt);
controlPanel.timeSpanInput.setText(""+timeSpan);
if (!isAutonomous)
currentTime = orbit.initialData[0];
return new ExtendOrbitAnimation(this,orbit,numberOfPoints);
}
private void startOrbitAtPoint(double[] initialPointData, double dt, double timeSpan) {
Orbit orbit = new Orbit(this,initialPointData,orbitType,dt);
addDecoration(orbit);
setCurrentOrbit(orbit);
if (getConnectDotsOnOrbit())
orbit.setStyle(OrbitPoints2D.LINES);
int numberOfPoints = (int)(timeSpan/dt + 0.5); // how many points should be added to curve during animation
if (!isAutonomous)
currentTime = initialPointData[0];
if (animateDrawing)
getDisplay().installAnimation( new ExtendOrbitAnimation(this,orbit,numberOfPoints));
else
orbit.setPointCount(numberOfPoints+1);
}
public void addExtraXML(Document containingDocument, Element viewElement) {
super.addExtraXML(containingDocument, viewElement);
if (isAutonomous && canShowVectorField)
SaveAndRestore.addProperty(this, "showDirectionField", containingDocument, viewElement);
for (Decoration d : getDecorations())
if (d instanceof Orbit) {
Orbit orbit = (Orbit)d;
Element orbElm = containingDocument.createElement("orbit");
orbElm.setAttribute("start", Util.toExternalString(orbit.initialData));
orbElm.setAttribute("type", Util.toExternalString(orbit.orbitType));
orbElm.setAttribute("dt", Util.toExternalString(orbit.dt));
orbElm.setAttribute("points", Util.toExternalString(orbit.pointCount));
orbElm.setAttribute("isCurrentOrbit", Util.toExternalString(orbit.isCurrentOrbit));
viewElement.appendChild(orbElm);
}
}
public void readExtraXML(Element viewInfo) throws IOException {
super.readExtraXML(viewInfo);
NodeList nodes = viewInfo.getElementsByTagName("orbit");
for (int i = 0; i < nodes.getLength(); i++) {
Element orbElm = (Element)nodes.item(i);
double[] startPt = (double[])Util.externalStringToValue(orbElm.getAttribute("start"), double[].class);
double dt = (Double)Util.externalStringToValue(orbElm.getAttribute("dt"), Double.TYPE);
int type = (Integer)Util.externalStringToValue(orbElm.getAttribute("type"), Integer.TYPE);
int pointCount = (Integer)Util.externalStringToValue(orbElm.getAttribute("points"), Integer.TYPE);
boolean isCurrentOrbit = (Boolean)Util.externalStringToValue(orbElm.getAttribute("isCurrentOrbit"), Boolean.TYPE);
Orbit orbit = new Orbit(this,startPt,type,dt);
orbit.pointCount = pointCount;
orbit.setIsCurrentOrbit(isCurrentOrbit);
addDecoration(orbit);
if (isCurrentOrbit) {
controlPanel.dtInput.setText(""+dt);
controlPanel.resetStartPointInputText(startPt);
currentOrbit = orbit;
continueOrbitAction.setEnabled(true);
}
}
}
}
/**
* This class defines the auxiliary view that can be added to the bottom of the display,
* where the x- and y-coordinates of the current orbit are plotted. It is a rather kludgy hack.
*/
private class ProjectedOrbitView extends View {
ArrayList eulerPoints;
ArrayList rungeKuttaPoints;
int maxNumberOfPoints; // The number of points that CAN be drawn.
ODEView owner;
ProjectedOrbitView(ODEView owner) {
this.owner = owner;
setPreserveAspect(false);
setApplyGraphics2DTransform(false);
setAntialiased(true);
eulerPoints = new ArrayList();
rungeKuttaPoints = new ArrayList();
setExhibit(new Exhibit(){ // I need to have an Exhbit in this view; otherwise, its render method is never called.
protected void doDraw(Graphics2D g, View view, Transform transform) {
draw(g,transform);
}
public Color getDefaultBackground() {
return Color.BLACK;
}
public Transform getDefaultTransform(View view) {
return new POVTransform();
}
});
}
private class POVTransform extends Transform {
void getLimitsFromOwner() {
Transform tr = owner.getTransform();
resetLimits(-1,1,tr.getYmin(),tr.getYmax()); // x-limits are not important; y limits should match those on main view
}
}
public MouseTask getDefaultMouseTask() {
return null;
}
void setMaxPoints(int ct) {
maxNumberOfPoints = ct;
forceRedraw();
}
void resetPointsFromOrbit(Orbit orbit) {
eulerPoints.clear();
rungeKuttaPoints.clear();
if (orbit.getOrbitType() >= ORBIT_TYPE_BOTH) {
OrbitPoints2D euler = orbit.getEulerPoints();
int ct = euler.getPointCount();
for (int i = 0; i < ct; i++)
eulerPoints.add(euler.getPoint(i));
}
if (orbit.getOrbitType() <= ORBIT_TYPE_BOTH) {
OrbitPoints2D rk = orbit.getRungeKuttaPoints();
int ct = rk.getPointCount();
for (int i = 0; i < ct; i++)
rungeKuttaPoints.add(rk.getPoint(i));
}
forceRedraw();
}
void clear() {
eulerPoints.clear();
rungeKuttaPoints.clear();
forceRedraw();
}
void addPoints(Point2D eulerPt, Point2D rkPoint) {
eulerPoints.add(eulerPt);
rungeKuttaPoints.add(rkPoint);
if (eulerPt == null && rkPoint == null)
return;
if (eulerPt != null && eulerPoints.size() > 1) {
int ptNum = eulerPoints.size()-1;
if (! drawLineNow(ptNum, eulerPoints.get(ptNum-1), eulerPoints.get(ptNum), true)) {
forceRedraw();
return;
}
}
if (rkPoint != null && rungeKuttaPoints.size() > 1) {
int ptNum = rungeKuttaPoints.size()-1;
if (! drawLineNow(ptNum, rungeKuttaPoints.get(ptNum-1), rungeKuttaPoints.get(ptNum), false))
forceRedraw();
}
}
boolean drawLineNow(int ptNum, Point2D pt1, Point2D pt2, boolean eulerColors) {
if (pt1 == null || pt2 == null)
return true;
if (!beginDrawToOffscreenImage())
return false;
Transform transform = getTransform();
((POVTransform)transform).getLimitsFromOwner();
double pixelWidth = (transform.getXmax() - transform.getXmin()) / transform.getWidth();
double leftSpace = 15 * pixelWidth;
double left = transform.getXmin() + 3*leftSpace;
double dt = (transform.getXmax() - transform.getXmin() - 3*leftSpace)/(maxNumberOfPoints - 1);
double t = left + ptNum*dt;
if (eulerColors) {
setColor(EULER_PROJECTED_ORBIT_X_COLOR);
drawLine(t-dt,pt1.getX(),t,pt2.getX());
setColor(EULER_PROJECTED_ORBIT_Y_COLOR);
drawLine(t-dt,pt1.getY(),t,pt2.getY());
}
else {
setColor(RUNGE_KUTTA_PROJECTED_ORBIT_X_COLOR);
drawLine(t-dt,pt1.getX(),t,pt2.getX());
setColor(RUNGE_KUTTA_PROJECTED_ORBIT_Y_COLOR);
drawLine(t-dt,pt1.getY(),t,pt2.getY());
}
return true;
}
void draw(Graphics2D g, Transform transform) {
((POVTransform)transform).getLimitsFromOwner();
// Note: The previous line changes the y-limits in the transform to match those in the main
// view. Canging the limits in the draw method will not work, unless preserveAspect and
// applyTransform2D have been turned off, as they are here. This is becuase by the time
// draw() is called, these two properties have already been applied to the limits and to
// the graphics context respectively.
double pixelWidth = (transform.getXmax() - transform.getXmin()) / transform.getWidth();
double leftSpace = 15 * pixelWidth;
g.setColor(Color.LIGHT_GRAY);
drawLine(transform.getXmin() + 2*leftSpace,0,transform.getXmax(),0);
drawLine(transform.getXmin() + 3*leftSpace,transform.getYmin(),transform.getXmin() + 3*leftSpace,transform.getYmax());
double height = transform.getYmax() - transform.getYmin();
g.setColor(RUNGE_KUTTA_PROJECTED_ORBIT_X_COLOR);
drawString("x", transform.getXmin() + leftSpace, transform.getYmax() - height/3 - 3*pixelWidth);
g.setColor(RUNGE_KUTTA_PROJECTED_ORBIT_Y_COLOR);
drawString("y", transform.getXmin() + leftSpace, transform.getYmax() - 2*height/3 - 3*pixelWidth);
double left = transform.getXmin() + 3*leftSpace;
double dt = (transform.getXmax() - transform.getXmin() - 3*leftSpace)/(maxNumberOfPoints - 1);
if (owner.getOrbitType() >= ORBIT_TYPE_BOTH && eulerPoints.size() > 0) {
Point2D pt1 = eulerPoints.get(0);
for (int i = 1; i < eulerPoints.size(); i++) {
double t = left + i*dt;
Point2D pt2 = eulerPoints.get(i);
if (pt1 != null && pt2 != null) {
g.setColor(EULER_PROJECTED_ORBIT_X_COLOR);
drawLine(t-dt,pt1.getX(),t,pt2.getX());
g.setColor(EULER_PROJECTED_ORBIT_Y_COLOR);
drawLine(t-dt,pt1.getY(),t,pt2.getY());
}
pt1 = pt2;
}
}
if (owner.getOrbitType() <= ORBIT_TYPE_BOTH && rungeKuttaPoints.size() > 0) {
Point2D pt1 = rungeKuttaPoints.get(0);
for (int i = 1; i < rungeKuttaPoints.size(); i++) {
double t = left + i*dt;
Point2D pt2 = rungeKuttaPoints.get(i);
if (pt1 != null && pt2 != null) {
g.setColor(RUNGE_KUTTA_PROJECTED_ORBIT_X_COLOR);
drawLine(t-dt,pt1.getX(),t,pt2.getX());
g.setColor(RUNGE_KUTTA_PROJECTED_ORBIT_Y_COLOR);
drawLine(t-dt,pt1.getY(),t,pt2.getY());
}
pt1 = pt2;
}
}
}
}
/**
* Represents one integral curve of the vector field.
*/
private class Orbit extends Decoration {
double[] initialData;
double[] currentEulerData;
double[] currentRungeKuttaData;
OrbitPoints2D eulerPoints, rungeKuttaPoints;
ODEView view;
boolean isCurrentOrbit;
int orbitType;
double dt;
int pointCount;
Orbit(ODEView view, double[] initialData, int orbitType, double dt) {
this.view = view;
this.initialData = initialData;
this.orbitType = orbitType;
this.dt = dt;
eulerPoints = new OrbitPoints2D();
rungeKuttaPoints = new OrbitPoints2D();
eulerPoints.setColor(EULER_ORBIT_COLOR);
eulerPoints.setStyle(OrbitPoints2D.DOTS);
rungeKuttaPoints.setColor(RUNGE_KUTTA_ORBIT_COLOR);
rungeKuttaPoints.setStyle(OrbitPoints2D.DOTS);
Point2D startPoint = extractPointFromData(initialData);
eulerPoints.addPoint(startPoint);
rungeKuttaPoints.addPoint(startPoint);
currentEulerData = orbitType == ORBIT_TYPE_RUNGE_KUTTA ? null : initialData.clone();
currentRungeKuttaData = orbitType == ORBIT_TYPE_EULER ? null : initialData.clone();
pointCount = 1;
}
void setStyle(int style) {
eulerPoints.setStyle(style);
rungeKuttaPoints.setStyle(style);
fireDecorationChangeEvent();
}
int getOrbitType() {
return orbitType;
}
OrbitPoints2D getEulerPoints() {
return eulerPoints;
}
OrbitPoints2D getRungeKuttaPoints() {
return rungeKuttaPoints;
}
int getPointCount() {
return pointCount;
}
void setPointCount(int ct) {
pointCount = ct;
if (isCurrentOrbit && view.projectedOrbitView != null)
view.projectedOrbitView.setMaxPoints(pointCount);
forceRedraw();
}
void removePoints() { // called only when the Create animation is going to redraw the curve.
eulerPoints.clear();
rungeKuttaPoints.clear();
Point2D startPoint = extractPointFromData(initialData);
eulerPoints.addPoint(startPoint);
rungeKuttaPoints.addPoint(startPoint);
currentEulerData = orbitType == ORBIT_TYPE_RUNGE_KUTTA ? null : initialData.clone();
currentRungeKuttaData = orbitType == ORBIT_TYPE_EULER ? null : initialData.clone();
pointCount = 1;
forceRedraw();
}
Point2D getEulerPoint(int i) {
if (i >= eulerPoints.getPointCount())
return null;
else
return eulerPoints.getPoint(i);
}
Point2D getRungeKuttaPoint(int i) {
if (i >= rungeKuttaPoints.getPointCount())
return null;
else
return rungeKuttaPoints.getPoint(i);
}
void setOrbitType(int type) {
orbitType = type;
forceRedraw();
}
void setIsCurrentOrbit(boolean b) {
if (isCurrentOrbit && view.projectedOrbitView != null)
view.projectedOrbitView.clear();
isCurrentOrbit = b;
eulerPoints.setColor( isCurrentOrbit ? EULER_ORBIT_COLOR : OLD_EULER_ORBIT_COLOR);
rungeKuttaPoints.setColor( isCurrentOrbit ? RUNGE_KUTTA_ORBIT_COLOR : OLD_RUNGE_KUTTA_ORBIT_COLOR);
if (isCurrentOrbit && view.projectedOrbitView != null) {
view.projectedOrbitView.setMaxPoints(pointCount);
view.projectedOrbitView.resetPointsFromOrbit(this);
}
}
boolean addNextPoint() {
Point2D eulerPt = null, rkPoint = null;
if (orbitType >= ORBIT_TYPE_BOTH) {
if (currentEulerData != null) {
nextEulerPoint(currentEulerData, dt);
for (double d : currentEulerData) {
if (Double.isNaN(d) || Double.isInfinite(d)) {
currentEulerData = null;
break;
}
}
if (currentEulerData != null)
eulerPt = extractPointFromData(currentEulerData);
}
}
if (orbitType <= ORBIT_TYPE_BOTH) {
if (currentRungeKuttaData != null) {
nextRungeKuttaPoint(currentRungeKuttaData, dt);
for (double d : currentRungeKuttaData) {
if (Double.isNaN(d) || Double.isInfinite(d)) {
currentRungeKuttaData = null;
break;
}
}
if (currentRungeKuttaData != null)
rkPoint = extractPointFromData(currentRungeKuttaData);
}
}
if (eulerPt == null && rkPoint == null)
return false;
if (!isAutonomous) {
double d = Double.NEGATIVE_INFINITY;
if (currentEulerData != null)
d = currentEulerData[0];
if (currentRungeKuttaData != null && currentRungeKuttaData[0] > d)
d = currentRungeKuttaData[0];
view.currentTime = d;
}
pointCount++;
boolean ok = true;
if (eulerPt != null)
ok = eulerPoints.addNow(view,eulerPt);
if (rkPoint != null)
ok = ok && rungeKuttaPoints.addNow(view,rkPoint);
if (!ok)
view.forceRedraw();
if (isCurrentOrbit && view.projectedOrbitView != null)
view.projectedOrbitView.addPoints(eulerPt,rkPoint);
return true;
}
public void computeDrawData(View v, boolean exhibitNeedsRedraw, Transform previousTransform, Transform newTransform) {
if ((decorationNeedsRedraw || exhibitNeedsRedraw) && pointCount > 1) {
// Recompute all the points on the orbit when exhibit properties change
currentEulerData = currentRungeKuttaData = null;
if (orbitType >= ORBIT_TYPE_BOTH) {
currentEulerData = initialData.clone();
eulerPoints.clear();
eulerPoints.addPoint(extractPointFromData(initialData));
for (int i = 1; i < pointCount; i++) {
nextEulerPoint(currentEulerData, dt);
for (double d : currentEulerData) {
if (Double.isNaN(d) || Double.isInfinite(d)) {
currentEulerData = null;
break;
}
}
if (currentEulerData == null)
break;
else
eulerPoints.addPoint(extractPointFromData(currentEulerData));
}
}
if (orbitType <= ORBIT_TYPE_BOTH) {
currentRungeKuttaData = initialData.clone();
rungeKuttaPoints.clear();
rungeKuttaPoints.addPoint(extractPointFromData(initialData));
for (int i = 1; i < pointCount; i++) {
nextRungeKuttaPoint(currentRungeKuttaData, dt);
for (double d : currentRungeKuttaData) {
if (Double.isNaN(d) || Double.isInfinite(d)) {
currentRungeKuttaData = null;
break;
}
}
if (currentRungeKuttaData == null)
break;
else
rungeKuttaPoints.addPoint(extractPointFromData(currentRungeKuttaData));
}
}
if (isCurrentOrbit && view.projectedOrbitView != null)
view.projectedOrbitView.resetPointsFromOrbit(this);
}
}
public void doDraw(Graphics2D g, View view, Transform transform) {
if (orbitType >= ORBIT_TYPE_BOTH)
eulerPoints.draw(g,view,transform);
if (orbitType <= ORBIT_TYPE_BOTH)
rungeKuttaPoints.draw(g,view,transform);
}
}
/**
* An animation that extends an integral curve, point-by-point over a period of time, for a
* specified number of points. An animation of this type is used when the user starts a
* new orbit or uses the "Continue Orbit" command.
*/
private class ExtendOrbitAnimation extends TimerAnimation {
ODEView view;
Orbit orbit;
int finalNumberOfPoints;
ExtendOrbitAnimation(ODEView view, Orbit orbit, int numberOfPointsToAdd) {
super(numberOfPointsToAdd,15);
this.view = view;
this.orbit = orbit;
this.finalNumberOfPoints = orbit.getPointCount() + numberOfPointsToAdd;
if (orbit.isCurrentOrbit && view.projectedOrbitView != null)
view.projectedOrbitView.setMaxPoints(orbit.getPointCount() + numberOfPointsToAdd + 1);
}
protected void drawFrame() {
if (!view.getAnimateDrawing()) { // in case animation is turned off while this animation is in progress.
orbit.setPointCount(finalNumberOfPoints);
cancel();
return;
}
boolean nextPoint = orbit.addNextPoint();
if (nextPoint == false)
cancel();
if (canShowVectorField && !isAutonomous)
view.forceRedraw();
}
protected void animationStarting() {
if (canShowVectorField && !isAutonomous)
view.setShowDirectionField(true);
}
protected void animationEnding() {
orbit.forceRedraw(); // Make sure orbit appears in all views after it is fully drawn.
if (canShowVectorField && !isAutonomous)
view.setShowDirectionField(false);
}
}
private class ControlPanel extends JPanel {
ODEView owner;
JTextField[] icInputs; // initial condition inputs
JTextField dtInput, timeSpanInput;
JButton startOrbitButton;
ControlPanel(ODEView view) {
setBorder(BorderFactory.createLineBorder(Color.BLACK,1));
JPanel componentPanel = new JPanel();
componentPanel.setLayout(new GridLayout(0,1));
add(componentPanel);
owner = view;
Font font = new Font("SansSerif", Font.BOLD, 10);
Font inputFont = new Font("SansSerif", Font.PLAIN, 10);
startOrbitButton = new JButton(I18n.tr("vmm.ode.command.StartOrbitAt"));
startOrbitButton.setFont(font);
componentPanel.add(startOrbitButton);
icInputs = new JTextField[inputLabelNames.length];
for (int i = 0; i < inputLabelNames.length; i++) {
icInputs[i] = new JTextField(6);
icInputs[i].setFont(inputFont);
JPanel p = new JPanel();
JLabel lbl = new JLabel(inputLabelNames[i] + " =");
lbl.setFont(font);
p.add(lbl);
p.add(icInputs[i]);
componentPanel.add(p);
}
dtInput = new JTextField(""+dtDefault,4);
timeSpanInput = new JTextField(""+timeSpanDefault,3);
dtInput.setFont(inputFont);
timeSpanInput.setFont(inputFont);
JPanel p = new JPanel();
JLabel lbl = new JLabel(I18n.tr("vmm.ode.StepSize") + "=");
lbl.setFont(font);
p.add(lbl);
p.add(dtInput);
componentPanel.add(p);
p = new JPanel();
lbl = new JLabel(I18n.tr("vmm.ode.TimeSpan") + "=");
lbl.setFont(font);
p.add(lbl);
p.add(timeSpanInput);
componentPanel.add(p);
componentPanel.add(Box.createVerticalStrut(1));
JButton b = new JButton(view.continueOrbitAction);
b.setFont(font);
componentPanel.add(b);
b = new JButton(view.eraseOrbitsAction);
b.setFont(font);
componentPanel.add(b);
if (addAnimateCheckBoxToControlPanel) {
JCheckBox cb = owner.animateDrawingToggle.createCheckBox();
cb.setFont(font);
cb.setText(I18n.tr("vmm.ode.command.AnimateDrawing.short"));
componentPanel.add(cb);
}
if (addLinesCheckBoxToControlPanel) {
JCheckBox cb = owner.connectDotsToggle.createCheckBox();
cb.setFont(font);
cb.setText(I18n.tr("vmm.ode.command.ConnectDotsOnOrbit.short"));
componentPanel.add(cb);
}
if (addOrbitTypesToControlPanel) {
JRadioButton[] radioButtons = view.orbitTypeSelect.createRadioButtons();
for (JRadioButton rb : radioButtons) {
rb.setFont(font);
componentPanel.add(rb);
}
}
startOrbitButton.addActionListener( new ActionListener() {
public void actionPerformed(ActionEvent evt) {
owner.getDisplay().stopAnimation();
double[] ic;
double dt,timeSpan;
ic = new double[icInputs.length];
for (int i = 0; i < icInputs.length; i++) {
try{
ic[i] = Double.parseDouble(icInputs[i].getText());
}
catch (NumberFormatException e) {
JOptionPane.showMessageDialog(owner.getDisplay(),
I18n.tr("vmm.ode.error.BadNumberInput",inputLabelNames[i]));
return;
}
}
try {
dt = Double.parseDouble(dtInput.getText());
if (dt <= 0)
throw new NumberFormatException();
}
catch (NumberFormatException e) {
JOptionPane.showMessageDialog(owner.getDisplay(), I18n.tr("vmm.ode.error.BadPositiveNumberInput","dt"));
return;
}
try {
timeSpan = Double.parseDouble(timeSpanInput.getText());
if (timeSpan <= 0)
throw new NumberFormatException();
}
catch (NumberFormatException e) {
JOptionPane.showMessageDialog(owner.getDisplay(), I18n.tr("vmm.ode.error.BadPositiveNumberInput",I18n.tr("vmm.ode.TimeSpan")));
return;
}
owner.startOrbitAtPoint(ic,dt,timeSpan);
}
});
}
void resetStartPointInputText(double[] ic) {
for (int i = 0; i < icInputs.length; i++) {
try {
if ( Math.abs( ic[i] - Double.parseDouble(icInputs[i].getText())) < 5e-10 )
continue;
}
catch (Exception e) {
}
String str = String.format("%.4g", ic[i]); // 4 significant digits
icInputs[i].setText(str);
}
}
}
}