/*  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 vmm3d.ode;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;

import org.freehep.graphics2d.VectorGraphics;

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 vmm3d.actions.AbstractActionVMM;
import vmm3d.actions.ActionList;
import vmm3d.actions.ActionRadioGroup;
import vmm3d.actions.ToggleAction;
import vmm3d.core.BasicMouseTask2D;
import vmm3d.core.Decoration;
import vmm3d.core.Display;
import vmm3d.core.Exhibit;
import vmm3d.core.I18n;
import vmm3d.core.MouseTask;
import vmm3d.core.SaveAndRestore;
import vmm3d.core.TimerAnimation;
import vmm3d.core.Transform;
import vmm3d.core.Util;
import vmm3d.core.VMMSave;
import vmm3d.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(org.freehep.graphics2d.VectorGraphics 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(org.freehep.graphics2d.VectorGraphics 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(org.freehep.graphics2d.VectorGraphics 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(org.freehep.graphics2d.VectorGraphics 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);
			}
		}
	}
	
	
	
}

 

Ads help maintain this website.