// DiffEq.java (C) 2001 by Paul Falstad, www.falstad.com

import java.io.InputStream;
import java.awt.*;
import java.awt.image.ImageProducer;
import java.applet.Applet;
import java.applet.AudioClip;
import java.util.Vector;
import java.util.Hashtable;
import java.util.Enumeration;
import java.io.File;
import java.net.URL;
import java.util.Random;
import java.awt.image.MemoryImageSource;
import java.lang.Math;
import java.text.NumberFormat;
import java.awt.event.*;

class DiffEqCanvas extends Canvas {
    DiffEqFrame pg;
    DiffEqCanvas(DiffEqFrame p) {
	pg = p;
    }
    public Dimension getPreferredSize() {
	return new Dimension(300,400);
    }
    public void update(Graphics g) {
	pg.updateDiffEq(g);
    }
    public void paint(Graphics g) {
	pg.updateDiffEq(g);
    }
};

class DiffEqLayout implements LayoutManager {
    public DiffEqLayout() {}
    public void addLayoutComponent(String name, Component c) {}
    public void removeLayoutComponent(Component c) {}
    public Dimension preferredLayoutSize(Container target) {
	return new Dimension(500, 500);
    }
    public Dimension minimumLayoutSize(Container target) {
	return new Dimension(100,100);
    }
    public void layoutContainer(Container target) {
	Insets insets = target.insets();
	int targetw = target.size().width - insets.left - insets.right;
	int targeth = target.size().height - (insets.top+insets.bottom);
	int ch = targeth*2/3;
	Component m = target.getComponent(0);
	m.move(insets.left, insets.top);
	m.resize(targetw, ch);
	int x = insets.left;
	int i;
	int mh = 0;
	ch += insets.top;
	for (i = 1; i != 4; i++) {
	    m = target.getComponent(i);
	    Dimension d = m.getPreferredSize();
	    m.move(x, ch);
	    m.resize(d.width, d.height);
	    x += d.width;
	    if (d.height > mh)
		mh = d.height;
	}
	ch += mh;
	int h = ch;
	int cw = target.size().width/3;
	x = insets.left;
	int cn = 0;
	for (; i < target.getComponentCount(); i++) {
	    m = target.getComponent(i);
	    if (m.isVisible()) {
		Dimension d = m.getPreferredSize();
		//if (m instanceof Scrollbar)
		//  d.width = target.size().width - cw;
		d.width = cw;
		/*if (m instanceof Label) {
		    h += d.height/3;
		    c = (target.size().width-cw-d.width)/2;
		}*/
		m.move(x, h);
		m.resize(d.width, d.height);
		h += d.height;
	    }
	    cn++;
	    if (cn == 5) {
		cn = 0;
		x += cw;
		h = ch;
	    }
	}
    }
};


public class DiffEq extends Applet {
    DiffEqFrame mf;
    void destroyFrame() {
	if (mf != null)
	    mf.dispose();
	mf = null;
    }
    public void init() {
	mf = new DiffEqFrame(this);
	mf.init();
    }
    public void destroy() {
	if (mf != null)
	    mf.dispose();
	mf = null;
    }
};

class DiffEqFrame extends Frame
  implements ComponentListener, ActionListener, AdjustmentListener,
             MouseMotionListener, MouseListener, ItemListener {
    
    Thread engine = null;

    Dimension winSize;
    Image dbimage;
    
    Random random;
    int maxTerms = 30;
    int maxMaxTerms = 500;
    int sampleCount;
    int midy;
    double ymult;
    DiffEq applet;
    DiffEqFrame(DiffEq a) {
	super("Differential Equation applet");
	applet = a;
    }

    double sinTable[][];
    public static final double epsilon = .00001;
    public static final double epsilon2 = .003;
    static final double yMax = 10;
    
    public String getAppletInfo() {
	return "DiffEq by Paul Falstad";
    }

    Button clearButton;
    ValueEditCanvas lhsValues[], rhsValues[], initialValues[],
	            solutionValues[];
    int selectedCoef;
    int magnitudesY;
    boolean funcSelected;
    static final int SEL_NONE = 0;
    static final int SEL_FUNC = 1;
    static final int SEL_MAG = 2;
    static final int MODE_PLUCK = 0;
    static final int MODE_SHAPE = 1;
    static final int MODE_TOUCH = 2;
    static final int MODE_FORCE = 3;
    static final int MODE_BOW = 999; // disabled
    static final int DISP_PHASE = 0;
    static final int DISP_LEFTRIGHT = 1;
    static final int DISP_PHASECOS = 2;
    static final int DISP_PHASORS = 3;
    static final int DISP_MODES = 4;
    static final int VEC_NONNEG      = (1<<0);
    static final int VEC_INTEGER     = (1<<1);
    static final int VEC_NONZERO     = (1<<2);
    static final int VEC_CONSTANT    = (1<<3);
    static final int VEC_HALFINTEGER = (1<<4);
    int selection;
    int dragX, dragY;
    boolean dragging;
    boolean bowing;
    boolean bowCaught;
    boolean forceApplied;
    double t;
    double forceMag;
    double points[][];
    int forceBarValue;
    double forceTimeZero;
    int tensionBarValue;
    NumberFormat nf;
    double func[], forceFunc[];
    LhsFunc lhsfunc;
    RhsFunc rhsfunc;
    Choice lhsChooser, rhsChooser;
    double xRangeStart, xRangeEnd, xRangeWidth;
    Vector lhsList, rhsList;
    boolean lhsChanged, rhsChanged;
    static final int pause = 0;

    int getrand(int x) {
	int q = random.nextInt();
	if (q < 0) q = -q;
	return q % x;
    }
    DiffEqCanvas cv;

    public void init() {
	lhsList = new Vector();
	LhsFunc lhs = new OscillatorLhsFunc();
	while (lhs != null) {
	    lhsList.addElement(lhs);
	    lhs = lhs.createNext();
	}
	rhsList = new Vector();
	RhsFunc rhs = new ZeroRhsFunc();
	while (rhs != null) {
	    rhsList.addElement(rhs);
	    rhs = rhs.createNext();
	}
	
	selectedCoef = -1;
	setLayout(new DiffEqLayout());
	cv = new DiffEqCanvas(this);
	cv.addComponentListener(this);
	cv.addMouseMotionListener(this);
	cv.addMouseListener(this);
	add(cv);
	
	lhsChooser = new Choice();
	int i;
	for (i = 0; i != lhsList.size(); i++)
	    lhsChooser.add("LHS = " +
			   ((LhsFunc) lhsList.elementAt(i)).getName());
	add(lhsChooser);
	lhsfunc = (LhsFunc) lhsList.elementAt(0);
	lhsChooser.addItemListener(this);

	rhsChooser = new Choice();
	for (i = 0; i != rhsList.size(); i++)
	    rhsChooser.add("RHS = " +
			   ((RhsFunc) rhsList.elementAt(i)).getName());
	add(rhsChooser);
	rhsfunc = (RhsFunc) rhsList.elementAt(0);
	rhsChooser.addItemListener(this);

	add(clearButton = new Button("Clear Function"));
	clearButton.addActionListener(this);

	lhsValues = new ValueEditCanvas[5];
	rhsValues = new ValueEditCanvas[2];
	initialValues = new ValueEditCanvas[3];
	solutionValues = new ValueEditCanvas[4];
	for (i = 0; i != 5; i++)
	    add(lhsValues[i] = new ValueEditCanvas());
	for (i = 0; i != 2; i++)
	    add(rhsValues[i] = new ValueEditCanvas());
	for (i = 0; i != 3; i++) {
	    add(initialValues[i] = new ValueEditCanvas());
	    initialValues[i].setValue(0);
	}
	for (i = 0; i != 4; i++)
	    add(solutionValues[i] = new ValueEditCanvas());
	lhsChanged = rhsChanged = true;

	setLoadCount();
	points = new double[1][2];
	points[0][0] = 1; points[0][1] = 1;

	nf = NumberFormat.getInstance();
	nf.setMaximumFractionDigits(5);

	random = new Random();
	reinit();
	cv.setBackground(Color.black);
	cv.setForeground(Color.lightGray);
	resize(500,400);
	handleResize();
	show();
    }

    void handleResize() {
	reinit();
    }

    void reinit() {
        Dimension d = winSize = cv.getSize();
	if (winSize.width == 0)
	    return;
	dbimage = createImage(d.width, d.height);
    }
    
    int getPanelHeight() { return winSize.height-40; }

    void centerString(Graphics g, String s, int y) {
	FontMetrics fm = g.getFontMetrics();
        g.drawString(s, (winSize.width-fm.stringWidth(s))/2, y);
    }

    public void paint(Graphics g) {
	cv.repaint();
    }

    void doClear() {
	int i;
	for (i = 0; i != sampleCount; i++)
	    forceFunc[i] = 0;
    }

    void valueChanged(ValueEditCanvas vec) {
	int i;
	for (i = 0; i != 4; i++)
	    if (vec == solutionValues[i]) {
		lhsfunc.setSolution();
		break;
	    }
	cv.repaint();
    }

    void rk(double x, double Y[], double stepsize) {
	int order = lhsfunc.getOrder();
	double yn[] = new double[order];
	double k1[] = new double[order];
	double k2[] = new double[order];
	double k3[] = new double[order];
	double k4[] = new double[order];
	int i;

	double rscale = lhsfunc.getRhsScale();
	for (i = 0; i != order; i++)
	    yn[i] = Y[i];
	for (i = 0; i != order-1; i++)
	    k1[i] = stepsize * yn[i+1];
	k1[order-1] =
	    stepsize * lhsfunc.calculateDiffs(x, yn,
					      rscale*rhsfunc.calculate(x, yn));
	for (i = 0; i != order; i++)
	    yn[i] = (Y[i] + 0.5*k1[i]);
	for (i = 0; i != order-1; i++)
	    k2[i] = stepsize * yn[i+1];
	double x2 = x + stepsize*.5;
	k2[order-1] =
	    stepsize * lhsfunc.calculateDiffs(x2, yn,
				            rscale*rhsfunc.calculate(x2, yn));
	for (i = 0; i != order; i++)
	    yn[i] = (Y[i] + 0.5*k2[i]);
	for (i = 0; i != order-1; i++)
	    k3[i] = stepsize * yn[i+1];
	k3[order-1] =
	    stepsize * lhsfunc.calculateDiffs(x2, yn,
					    rscale*rhsfunc.calculate(x2, yn));

	double x3 = x+stepsize;
	for (i = 0; i != order; i++)
	    yn[i] = (Y[i] + k3[i]);
	for (i = 0; i != order-1; i++)
	    k4[i] = stepsize * yn[i+1];
	k4[order-1] =
	    stepsize * lhsfunc.calculateDiffs(x3, yn,
					   rscale*rhsfunc.calculate(x3, yn));
	for (i = 0; i != order; i++)
	    Y[i] = Y[i] + (k1[i]+2*(k2[i]+k3[i])+k4[i])/6;
    }

    void rungeKutta(double start, double dir)
    {
	int numIter=0;
	double maxh=xRangeWidth/sampleCount;
	double error=0.0, E = .001, localError;
	int order = lhsfunc.getOrder();
	double Y[] = new double[order];
	double Yhalf[] = new double[order];
	double h = maxh;
	
	Y[0] = Yhalf[0] = points[0][1];
	if (order > 1)
	    Y[1] = Yhalf[1] = initialValues[0].getValue();
	if (order > 2)
	    Y[2] = Yhalf[2] = initialValues[1].getValue();
	if (order > 3)
	    Y[3] = Yhalf[3] = initialValues[2].getValue();
	double t = start;

	int steps = 0;
	int pos = (int) ((t-xRangeStart)*sampleCount/xRangeWidth);
	func[pos] = Y[0];
	double minh = .01;
	while (t >= xRangeStart && t <= xRangeEnd) {
	    // estimate one full step
	    rk(t, Y, h*dir);

	    // estimate two half steps
	    rk(t, Yhalf, h*0.5*dir);
	    rk(t, Yhalf, h*0.5*dir);

	    // estimate the local error
	    localError = java.lang.Math.abs(Y[0] - Yhalf[0]);
	    
	    if (Y[0] > 1e6 || Y[0] < -1e6) {
		// who cares about the error
	    } else if (localError > E) {
		h *= 0.75; // decrease the step size
		if (h < minh)
		    h = minh;
	    } else if (localError < (E * 0.5)) {
		h *= 1.25; // increase the step size
		if (h > maxh)
		    h = maxh;
	    }
	    
	    int i;
	    for (i = 0; i != order; i++)
		Yhalf[i] = Y[i]; 
	    int newpos = (int) ((t-xRangeStart)*sampleCount/xRangeWidth);
	    if (dir > 0) {
		if (newpos > pos) {
		    if (newpos > sampleCount)
			newpos = sampleCount;
		    while (pos < newpos)
			func[++pos] = Y[0];
		}
	    } else {
		if (newpos < pos) {
		    if (newpos < 0)
			newpos = 0;
		    while (pos > newpos)
			func[--pos] = Y[0];
		}
	    }
	    
	    t += h*dir;
	    steps++;
	}
    }

    public void updateDiffEq(Graphics realg) {
	Graphics g = dbimage.getGraphics();
	if (winSize == null || winSize.width == 0)
	    return;
	boolean allQuiet = true;
	Color gray1 = new Color(76,  76,  76);
	Color gray2 = new Color(127, 127, 127);
	g.setColor(cv.getBackground());
	g.fillRect(0, 0, winSize.width, winSize.height);
	g.setColor(cv.getForeground());

	lhsfunc = (LhsFunc)
	    lhsList.elementAt(lhsChooser.getSelectedIndex());
	if (lhsChanged) {
	    int i;
	    for (i = 0; i != 5; i++)
		lhsValues[i].hide();
	    for (i = 0; i != 3; i++) {
		initialValues[i].hide();
		initialValues[i].setValue(0);
	    }
	    lhsfunc.newFunc();
	    if (lhsfunc.getOrder() > 1)
		initialValues[0].setLabel("Initial y'");
	    if (lhsfunc.getOrder() > 2)
		initialValues[1].setLabel("Initial y''");
	    if (lhsfunc.getOrder() > 3)
		initialValues[2].setLabel("Initial y'''");
	    rhsChooser.select(0);
	    rhsChanged = true;
	}
	lhsfunc.setup();
	rhsfunc = (RhsFunc)
	    rhsList.elementAt(rhsChooser.getSelectedIndex());
	if (rhsChanged) {
	    int i;
	    for (i = 0; i != 2; i++)
		rhsValues[i].hide();
	    rhsfunc.newFunc();
	    if (rhsfunc.isCustom())
		clearButton.enable();
	    else
		clearButton.disable();
	    for (i = 0; i != 4; i++)
		solutionValues[i].hide();
	    if (rhsfunc instanceof ZeroRhsFunc)
		lhsfunc.newFuncSolution();
	}
	rhsfunc.setup();
	validate();
	lhsChanged = rhsChanged = false;

	int i;
	int panelHeight = getPanelHeight();
	midy = panelHeight / 2;
	int halfPanel = panelHeight / 2;
	double ymult0 = .75 * halfPanel;
	ymult = ymult0 / yMax;
	for (i = -1; i <= 1; i++) {
	    g.setColor((i == 0) ? gray2 : gray1);
	    g.drawLine(0,             midy+(i*(int) ymult0),
		       winSize.width, midy+(i*(int) ymult0));
	}
	g.setColor(gray2);
	if (!lhsfunc.positiveOnly()) {
	    g.drawLine(winSize.width/2, midy-(int) ymult0,
		       winSize.width/2, midy+(int) ymult0);
	}

	g.setColor(Color.white);
	xRangeWidth = lhsfunc.getRangeWidth();
	xRangeStart = lhsfunc.getRangeStart();
	xRangeEnd   = lhsfunc.getRangeEnd();
	boolean useRungeKutta = lhsfunc.useRungeKutta();
	if (useRungeKutta) {
	    rungeKutta(points[0][0],  1);
	    rungeKutta(points[0][0], -1);
	}

	int ox = -1, oy = -1;
	int ox2 = -1, oy2 = -1;
	for (i = 0; i != sampleCount; i++) {
	    double dx = (i*xRangeWidth/sampleCount)+xRangeStart;
	    double dy = rhsfunc.calculate(dx, null);
	    forceFunc[i] = dy;

	    if (!useRungeKutta)
		func[i] = lhsfunc.calculate(dx);
	    // System.out.print(dx + " " + func[i] + "\n");
	}
	g.setColor(Color.red);
	drawGraph(g, forceFunc);
	g.setColor(Color.white);
	drawGraph(g, func);
	g.setColor(Color.red);
	for (i = 0; i != 1; i++) {
	    int x = (int) ((points[i][0] - xRangeStart) /xRangeWidth * winSize.width);
	    int y = midy - (int) (points[i][1] * ymult);
	    g.drawLine(x-3, y, x+3, y);
	    g.drawLine(x, y-3, x, y+3);
	}
	g.setColor(Color.white);
	centerString(g, lhsfunc.getDescription() + " = " +
		        lhsfunc.getRhsScaleDescription() +
		     rhsfunc.getDescription(), panelHeight+10);
	if (rhsfunc instanceof ZeroRhsFunc)
	    centerString(g, lhsfunc.getSolution(), panelHeight+30);
	realg.drawImage(dbimage, 0, 0, this);
    }

    void drawGraph(Graphics g, double f[]) {
	int i;
	int ox = -1, oy = -1;
	for (i = 0; i != sampleCount; i++) {
	    int x = winSize.width * i / sampleCount;
	    double dx = (i*xRangeWidth/sampleCount)+xRangeStart;
	    double dy = f[i];
	    int y = midy - (int) (ymult * dy);
	    if (Double.isNaN(dy) || Double.isInfinite(dy)) {
		ox = x;
		if (y < midy)
		    y = 0;
		else
		    y = midy*2;
		continue;
	    }
	    if (y < 0 || dy > 1e6) {
		if (oy == 0) {
		    ox = x;
		    continue;
		}
		y = 0;
	    }
	    if (y > midy*2 || dy < -1e6) {
		if (oy == midy*2) {
		    ox = x;
		    continue;
		}
		y = midy*2;
	    }
	    if (ox != -1)
		g.drawLine(ox, oy, x, y);
	    ox = x;
	    oy = y;
	}
    }

    void setPoint(double x, double y) {
	points[0][0] = x;
	points[0][1] = y;
    }

    void edit(MouseEvent e) {
	int x = e.getX();
	int y = e.getY();

	if (funcSelected) {
	    if (x == dragX) {
		editFuncPoint(x, y);
		dragY = y;
	    } else {
		// need to draw a line from old x,y to new x,y and
		// call editFuncPoint for each point on that line.  yuck.
		int x1 = (x < dragX) ? x : dragX;
		int y1 = (x < dragX) ? y : dragY;
		int x2 = (x > dragX) ? x : dragX;
		int y2 = (x > dragX) ? y : dragY;
		dragX = x;
		dragY = y;
		for (x = x1; x <= x2; x++) {
		    y = y1+(y2-y1)*(x-x1)/(x2-x1);
		    editFuncPoint(x, y);
		}
	    }
	    cv.repaint(pause);
	    return;
	}
	double dx = x*xRangeWidth/winSize.width + xRangeStart;
	double dy = (midy-y)/ymult;
	if (dx < xRangeStart)
	    dx = xRangeStart;
	if (dx >= xRangeEnd)
	    dx = xRangeEnd;

	points[0][0] = dx;
	points[0][1] = dy;
	cv.repaint(pause);
    }

    void editFuncPoint(int x, int y) {
	// XXXXX
	double dy = (midy-y)/ymult;
	int lox = x*sampleCount/winSize.width;
	int hix = (x+1)*sampleCount/winSize.width;
	if (hix > sampleCount)
	    hix = sampleCount;
	if (lox < 0)
	    lox = 0;
	for (; lox < hix; lox++)
	    forceFunc[lox] = dy;
    }

    public void componentHidden(ComponentEvent e){}
    public void componentMoved(ComponentEvent e){}
    public void componentShown(ComponentEvent e) {
	cv.repaint(pause);
    }

    public void componentResized(ComponentEvent e) {
	handleResize();
	cv.repaint(pause);
    }
    public void actionPerformed(ActionEvent e) {
	if (e.getSource() == clearButton) {
	    doClear();
	    cv.repaint();
	}
    }

    public boolean handleEvent(Event ev) {
        if (ev.id == Event.WINDOW_DESTROY) {
            applet.destroyFrame();
            return true;
        }
        return super.handleEvent(ev);
    }
    
    public void adjustmentValueChanged(AdjustmentEvent e) {
	cv.repaint(pause);
    }

    void setLoadCount() {
	sampleCount = maxTerms = maxMaxTerms;
	func = new double[sampleCount+1];
	forceFunc = new double[sampleCount+1];
    }

    public void mouseDragged(MouseEvent e) {
	dragging = true;
	edit(e);
    }

    public void mouseMoved(MouseEvent e) {
	dragX = e.getX(); dragY = e.getY();
	if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0)
	    return;
	if (rhsfunc.isCustom()) {
	    int px = (int) ((points[0][0] - xRangeStart) /xRangeWidth *
			    winSize.width);
	    int py = midy - (int) (points[0][1] * ymult);
	    int dx = px-e.getX();
	    int dy = py-e.getY();
	    funcSelected = (dx*dx+dy*dy > 10);
	} else {
	    funcSelected = false;
	}
    }
    public void mouseClicked(MouseEvent e) {
    }
    public void mouseEntered(MouseEvent e) {
    }
    public void mouseExited(MouseEvent e) {
	if (!dragging && selectedCoef != -1) {
	    selectedCoef = -1;
	    cv.repaint(pause);
	}
    }
    public void mousePressed(MouseEvent e) {
	if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) == 0)
	    return;
	dragging = true;
	edit(e);
    }
    public void mouseReleased(MouseEvent e) {
	if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) == 0)
	    return;
	dragging = false;
	cv.repaint(pause);
    }
    public void itemStateChanged(ItemEvent e) {
	if (e.getItemSelectable() == lhsChooser)
	    lhsChanged = true;
	if (e.getItemSelectable() == rhsChooser)
	    rhsChanged = true;
	cv.repaint(pause);
    }

    // solve these equations for c1 and c2:
    //   y  = c1 m1 + c2 m2
    //   y' = c1 m3 + c4 m4
    // m1, m2 are typically the values of the two solutions at point[0][0].
    // m3, m4 are the derivatives of the two solutions at point[0][0].
    void solveForConstants(double m1, double m2, double m3, double m4) {
	double det = m1*m4-m2*m3;
	if (det == 0) {
	    System.out.print("solveForConstants: det = 0\n");
	    return;
	}
	double c1 = ( m4*points[0][1]-m2*initialValues[0].getValue())/det;
	double c2 = (-m3*points[0][1]+m1*initialValues[0].getValue())/det;
	solutionValues[0].setValue(c1);
	solutionValues[1].setValue(c2);
    }

    abstract class LhsFunc {
	void setup() {}
	void newFunc() {}
	void newFuncSolution() {}
	boolean positiveOnly() { return false; }
	boolean useRungeKutta() { return true; }
	double getRangeWidth() { return 100; }
	double calculate(double x) { return 0; }
	double calculateDiffs(double x, double y[], double rs) { return 0; }
	double getRhsScale() { return 1; }
	String getRhsScaleDescription() { return ""; }
	abstract int getOrder();
	abstract String getDescription();
	abstract LhsFunc createNext();
	abstract String getName();
	double getRangeStart() {
	    return (lhsfunc.positiveOnly()) ? 0 : -getRangeWidth()/2;
	}
	double getRangeEnd() {
	    return getRangeStart() + getRangeWidth();
	}
	String getSolution() { return ""; }
	void setSolution() { } // XXX
    }

    class Order2LhsFunc extends LhsFunc {
	String getName() { return "ay''+by'+cy"; }
	double va, vb, vc;
	boolean complexDisc, linear;
	int getOrder() { return 2; }
	void setup() {
	    va = lhsValues[0].getValue();
	    vb = lhsValues[1].getValue();
	    vc = lhsValues[2].getValue();
	}

	double calculateDiffs(double x, double y[], double rs) {
	    return -(vb*y[1]+vc*y[0]-rs)/va;
	}

	String getDescription() {
	    return "ay'' + by' + cy";
	}

	void newFunc() {
	    lhsValues[0].setup("a",   1, VEC_NONZERO);
	    lhsValues[1].setup("b", .07, 0);
	    lhsValues[2].setup("c",   1, 0);
	    setPoint(getRangeStart(), yMax);
	}

	void newFuncSolution() {
	    solutionValues[0].setLabel("c1");
	    solutionValues[1].setLabel("c2");
	    solutionValues[2].setLabel("c3");
	    solutionValues[2].setFlags(VEC_CONSTANT);
	    solutionValues[3].setLabel("c4");
	    solutionValues[3].setFlags(VEC_CONSTANT);
	}

	String getSolution() {
	    double disc = vb*vb-4*va*vc;
	    complexDisc = false;
	    linear = false;
	    if (disc < 0) {
		disc = -disc;
		complexDisc = true;
	    }
	    disc = java.lang.Math.sqrt(disc);
	    double a1 = 0, a2 = 0;
	    double c1, c2;
	    double sarg = 0;
	    int pointcount = 2;
	    double m1, m2, m3, m4;
	    double x = points[0][0];
	    if (complexDisc) {
		a1 = a2 = -vb/(2*va);
		sarg = disc/(2*va);
		m1 = java.lang.Math.exp(a1*x) * java.lang.Math.sin(sarg*x);
		m2 = java.lang.Math.exp(a1*x) * java.lang.Math.cos(sarg*x);
		m3 = java.lang.Math.exp(a1*x)*
		    (a1*java.lang.Math.sin(sarg*x) +
		     sarg*java.lang.Math.cos(sarg*x));
		m4 = java.lang.Math.exp(a1*x)*
		    (a1*java.lang.Math.cos(sarg*x) -
		     sarg*java.lang.Math.sin(sarg*x));
	    } else if (vb == 0 && vc == 0) {
		linear = true;
		// solve two equations: y=mx+b, y'=0x+m
		m1 = points[0][0];
		m2 = m3 = 1;
		m4 = 0;
	    } else {
		a1 = (-vb+disc)/(2*va);
		a2 = (-vb-disc)/(2*va);
		m1 = java.lang.Math.exp(a1*x);
		m2 = java.lang.Math.exp(a2*x);
		m3 = a1*java.lang.Math.exp(a1*x);
		m4 = a2*java.lang.Math.exp(a2*x);
	    }
	    solveForConstants(m1, m2, m3, m4);
	    if (complexDisc) {
		solutionValues[2].setValue(a1);
		solutionValues[3].setValue(sarg);
		return "y = exp(c3 x) (c1 sin(c4 x)+c2 cos(c4 x))";
	    } else if (linear) {
		return "y = c1 x + c2";
	    } else {
		solutionValues[2].setValue(a1);
		solutionValues[3].setValue(a2);
		if (a1 == 0)
		    return "y = c1 + c2 exp(c4 x)";
		return "y = c1 exp(c3 x) + c2 exp(c4 x)";
	    }
	}

	void setSolution() {
	    double x = points[0][0];
	    if (complexDisc) {
		double c1 =   solutionValues[0].getValue();
		double c2 =   solutionValues[1].getValue();
		double a1 =   solutionValues[2].getValue();
		double sarg = solutionValues[3].getValue();
		points[0][1] = java.lang.Math.exp(a1*x) * (
		    c1*java.lang.Math.sin(sarg*x) +
		    c2*java.lang.Math.cos(sarg*x));
		double q;
		initialValues[0].setValue(q =
		    java.lang.Math.exp(a1*x)*
		    ((c1*a1-c2*sarg)*java.lang.Math.sin(sarg*x) +
		     (c2*a1+c1*sarg)*java.lang.Math.cos(sarg*x)));
	    } else if (linear) {
		double m = solutionValues[0].getValue();
		double b = solutionValues[1].getValue();
		points[0][1] = m*x+b;
		initialValues[0].setValue(m);
	    } else {
		double c1 = solutionValues[0].getValue();
		double c2 = solutionValues[1].getValue();
		double a1 = solutionValues[2].getValue();
		double a2 = solutionValues[3].getValue();
		points[0][1] = c1*java.lang.Math.exp(a1*x) +
		               c2*java.lang.Math.exp(a2*x);
		initialValues[0].setValue(
		    c1*a1*java.lang.Math.exp(a1*x) +
		    c2*a2*java.lang.Math.exp(a2*x));
	    }
	}

	LhsFunc createNext() { return new Order1LhsFunc(); }
    }

    class OscillatorLhsFunc extends Order2LhsFunc {
	String getName() { return "harmonic oscillator"; }

	double getRhsScale() { return vc; }
	String getRhsScaleDescription() { return "w\u00b2 "; }

	void setup() {
	    double beta  = lhsValues[0].getValue();
	    double omega = lhsValues[1].getValue();
	    va = 1;
	    vb = 2*beta;
	    vc = omega*omega;
	}

	String getDescription() {
	    return "y'' + 2by' + w\u00b2 y";
	}

	void newFunc() {
	    lhsValues[0].setup("b", .035, VEC_NONNEG);
	    lhsValues[1].setup("w",    1, VEC_NONNEG|VEC_NONZERO);
	    lhsValues[1].setMax(7);
	    setPoint(getRangeStart(), yMax);
	}

	void newFuncSolution() {
	    solutionValues[0].setLabel("c1");
	    solutionValues[1].setLabel("c2");
	    solutionValues[2].setLabel("c3");
	    solutionValues[3].setLabel("c4");
	    solutionValues[2].setFlags(VEC_CONSTANT);
	    solutionValues[3].setFlags(VEC_CONSTANT);
	}

	LhsFunc createNext() { return new Order2LhsFunc(); }
    }

    class Order1LhsFunc extends LhsFunc {
	String getName() { return "ay'+by"; }
	double va, vb;
	int getOrder() { return 1; }
	void setup() {
	    va = lhsValues[0].getValue();
	    vb = lhsValues[1].getValue();
	}

	double getRhsScale() { return vb; }
	String getRhsScaleDescription() { return "b "; }

	double calculateDiffs(double x, double y[], double rs) {
	    return (-vb*y[0]+rs)/va;
	}

	String getDescription() {
	    return "ay' + by";
	}

	void newFunc() {
	    lhsValues[0].setup("a", 23, VEC_NONZERO);
	    lhsValues[1].setup("b",  1, 0);
	    setPoint(0, 1);
	}

	void newFuncSolution() {
	    solutionValues[0].setLabel("c");
	    solutionValues[1].setLabel("d");
	    solutionValues[1].setFlags(VEC_CONSTANT);
	}

	String getSolution() {
	    double d = -vb/va;
	    double x = points[0][0];
	    double y1 = java.lang.Math.exp(d*x);
	    double c = points[0][1] / y1;
	    solutionValues[0].setValue(c);
	    solutionValues[1].setValue(d);
	    return "y = c exp(d x)";
	}

	void setSolution() {
	    double x = points[0][0];
	    double c = solutionValues[0].getValue();
	    double d = solutionValues[1].getValue();
	    points[0][1] = c*java.lang.Math.exp(d*x);
	}

	LhsFunc createNext() { return new FreeFallLhsFunc(); }
    }

    class FreeFallLhsFunc extends LhsFunc {
	String getName() { return "falling object"; }
	double va, vb, vc;
	int getOrder() { return 2; }
	void setup() {
	    va = lhsValues[0].getValue();
	    vb = lhsValues[1].getValue();
	    vc = lhsValues[2].getValue()*va;
	}

	double calculateDiffs(double x, double y[], double rs) {
	    return -(vb*y[1]+vc-rs)/va;
	}

	String getDescription() {
	    return "my'' + by' + mg";
	}

	void newFunc() {
	    lhsValues[0].setup("m", 1,  VEC_NONZERO|VEC_NONNEG);
	    lhsValues[1].setup("b", .07, VEC_NONNEG);
	    lhsValues[2].setup("g", .098, 0);
	    setPoint(getRangeStart(), yMax);
	}

	void newFuncSolution() {
	    solutionValues[0].setLabel("c1");
	    solutionValues[1].setLabel("c2");
	    solutionValues[1].setFlags(VEC_CONSTANT);
	    solutionValues[2].setLabel("c3");
	    solutionValues[2].setFlags(VEC_CONSTANT);
	    solutionValues[3].setLabel("c4");
	}

	String getSolution() {
	    double x = points[0][0];

	    double c2 = -vb/va;
	    double c3 = -vc/vb;
	    double c1 = 0;
	    double c4 = 0;
	 
	    if (vb == 0) {
		c1 = 0;
		c2 = -vc/2;
		c3 = initialValues[0].getValue()-2*c2*x;
		c4 = points[0][1]-c3*x-c2*x*x;
	    } else {
		c1 = (initialValues[0].getValue()-c3)/
		    (c2*java.lang.Math.exp(c2*x));
		c4 = points[0][1]-c3*x-c1*java.lang.Math.exp(c2*x);
	    }
	    solutionValues[0].setValue(c1);
	    solutionValues[1].setValue(c2);
	    solutionValues[2].setValue(c3);
	    solutionValues[3].setValue(c4);

	    if (vb == 0)
		return "y = c2 x\u00b2 + c3 x + c4";
	    return "y = c1 exp(c2 x) + c3 x + c4";
	}

	void setSolution() {
	    double x = points[0][0];
	    double c1 = solutionValues[0].getValue();
	    double c2 = solutionValues[1].getValue();
	    double c3 = solutionValues[2].getValue();
	    double c4 = solutionValues[3].getValue();
	    if (vb == 0) {
		points[0][1] = c2*x*x + c3*x + c4;
		initialValues[0].setValue(2*c2*x + c3);
	    } else {
		points[0][1] = c1*java.lang.Math.exp(c2*x) + c3*x + c4;
		initialValues[0].setValue(c1*c2*java.lang.Math.exp(c2*x) + c3);
	    }
	}

	LhsFunc createNext() { return new BesselLhsFunc(); }
    }

    class BesselLhsFunc extends LhsFunc {
	String getName() { return "Bessel"; }
	double va, c1, c2;
	int getOrder() { return 2; }
	boolean positiveOnly() { return true; }
	double vals[];

	// had bad luck with Runge-Kutta on Bessel equation...
	boolean useRungeKutta() { return false; }

	void setup() {
	    va = lhsValues[0].getValue();
	    va *= va;
	    vals = new double[4];
	    getSolution();
	    c1 = solutionValues[0].getValue();
	    c2 = solutionValues[1].getValue();
	}

	double calculate(double x) {
	    double p = lhsValues[0].getValue();
	    bessjy(x, p, vals);
	    return vals[0]*c1 + vals[2]*c2;
	}

	String getDescription() {
	    return "x\u00b2 y'' + xy' + (x\u00b2-p\u00b2)y";
	}
	
	void newFunc() {
	    lhsValues[0].setup("p", 0, VEC_NONNEG);
	    setPoint(0, yMax);
	}

	void newFuncSolution() {
	    solutionValues[0].setLabel("c1");
	    solutionValues[1].setLabel("c2");
	}

	String getSolution() {
	    double x = points[0][0];
	    double p = lhsValues[0].getValue();
	    double vals[] = new double[4];
	    bessjy(x, p, vals);
	    // m1 = y1, m2 = y2, m3 = y'1, m4 = y'2
	    solveForConstants(vals[0], vals[2], vals[1], vals[3]);
	    return "y = c1 Jp(x) + c2 Yp(x)";
	}

	void setSolution() {
	    double x = points[0][0];
	    double p = lhsValues[0].getValue();
	    double vals[] = new double[4];
	    bessjy(x, p, vals);
	    double c1 = solutionValues[0].getValue();
	    double c2 = solutionValues[1].getValue();
	    points[0][1] = vals[0]*c1 + vals[2]*c2;
	    initialValues[0].setValue(vals[1]*c1 + vals[3]*c2);
	}

	LhsFunc createNext() { return new BesselIntegerLhsFunc(); }
    }

    class BesselIntegerLhsFunc extends BesselLhsFunc {
	String getName() { return "Bessel (integer p)"; }
	void newFunc() {
	    super.newFunc();
	    lhsValues[0].setFlags(VEC_NONNEG|VEC_INTEGER);
	}
	LhsFunc createNext() { return new BesselHalfIntegerLhsFunc(); }
    }

    class BesselHalfIntegerLhsFunc extends BesselLhsFunc {
	String getName() { return "Bessel (half-integer p)"; }
	void newFunc() {
	    super.newFunc();
	    lhsValues[0].setValue(.5);
	    lhsValues[0].setFlags(VEC_NONNEG|VEC_HALFINTEGER);
	    setPoint(getRangeWidth()/2, 1);
	}
	LhsFunc createNext() { return new LegendreLhsFunc(); }
    }

    class LegendreLhsFunc extends LhsFunc {
	String getName() { return "Legendre"; }
	double va;
	int getOrder() { return 2; }
	double getRangeWidth() { return 2; }
	void setup() {
	    va = lhsValues[0].getValue();
	    va = va*(va+1);
	}
	double calculateDiffs(double x, double y[], double rs) {
	    return -(-2*x*y[1]+va*y[0]-rs)/(1-x*x);
	}
	String getDescription() {
	    return "(1-x\u00b2)y'' -2xy' + p(p+1)y";
	}
	void newFunc() {
	    lhsValues[0].setup("p", 7, VEC_NONNEG);
	    lhsValues[0].setMax(40);
	    setPoint(0, -yMax/2);
	}
	LhsFunc createNext() { return new LegendreIntegerLhsFunc(); }
    }

    class LegendreIntegerLhsFunc extends LegendreLhsFunc {
	String getName() { return "Legendre (integer order)"; }
	void newFunc() {
	    super.newFunc();
	    lhsValues[0].setup("n", 7, VEC_NONNEG|VEC_INTEGER);
	    lhsValues[0].setMax(40);
	}
	void newFuncSolution() {
	    solutionValues[0].setLabel("c1");
	    solutionValues[1].setLabel("c2");
	}

	String getSolution() {
	    double x = points[0][0];
	    int p = (int) lhsValues[0].getValue();
	    double vn[] = new double[p+2];
	    double vd[] = new double[p+2];
	    legendreP(p, x, vn, vd);
	    double pn = vn[p];
	    double pd = vd[p];
	    legendreQ(p, x, vn, vd);
	    double qn = vn[p];
	    double qd = vd[p];
	    // m1 = y1, m2 = y2, m3 = y'1, m4 = y'2
	    solveForConstants(pn, qn, pd, qd);
	    return "y = c1 Pn(x) + c2 Qn(x)";
	}

	void setSolution() {
	    double x = points[0][0];
	    int p = (int) lhsValues[0].getValue();
	    double vn[] = new double[p+2];
	    double vd[] = new double[p+2];
	    legendreP(p, x, vn, vd);
	    double pn = vn[p];
	    double pd = vd[p];
	    legendreQ(p, x, vn, vd);
	    double qn = vn[p];
	    double qd = vd[p];
	    double c1 = solutionValues[0].getValue();
	    double c2 = solutionValues[1].getValue();
	    points[0][1] = pn*c1 + qn*c2;
	    initialValues[0].setValue(pd*c1 + qd*c2);
	}

	LhsFunc createNext() { return new HermiteLhsFunc(); }
    }

    class HermiteLhsFunc extends LhsFunc {
	String getName() { return "Hermite"; }
	double va, vb, vc;
	int getOrder() { return 2; }
	double getRangeWidth() { return 4; }
	void setup() {
	    va = lhsValues[0].getValue();
	}
	//boolean positiveOnly() { return true; }
	double calculateDiffs(double x, double y[], double rs) {
	    return 2*x*y[1]-2*va*y[0]+rs;
	}
	String getDescription() {
	    return "y''-2xy'+2ny";
	}
	void newFunc() {
	    lhsValues[0].setup("n", 12, VEC_NONNEG);
	    lhsValues[0].setMax(2000);
	    setPoint(0, -yMax/2);
	}
	LhsFunc createNext() { return new JacobiLhsFunc(); }
    }

    class JacobiLhsFunc extends LhsFunc {
	String getName() { return "Jacobi"; }
	double va, vb, vn;
	int getOrder() { return 2; }
	double getRangeWidth() { return 2; }
	void setup() {
	    vn = lhsValues[0].getValue();
	    va = lhsValues[1].getValue();
	    vb = lhsValues[2].getValue();
	}
	//boolean positiveOnly() { return true; }
	double calculateDiffs(double x, double y[], double rs) {
	    return -((vb-va-(va+vb+2)*x)*y[1] +
		    vn*(vn+va+vb+1)*y[0]+rs)/(1-x*x);
	}
	String getDescription() {
	    return "(1-x\u00b2)y''+(b-a-(a+b+2)x)y'+n(n+a+b+1)y";
	}
	void newFunc() {
	    lhsValues[0].setup("n", 10, 0);
	    lhsValues[1].setup("a",  1, 0);
	    lhsValues[2].setup("b",  1, 0);
	    setPoint(0, 2);
	}
	LhsFunc createNext() { return new Order4LhsFunc(); }
    }

    class Order4LhsFunc extends LhsFunc {
	String getName() { return "ay''''+by'''+cy''+dy'+ey"; }
	double va, vb, vc, vd, ve;
	int getOrder() { return 4; }
	void setup() {
	    va = lhsValues[0].getValue();
	    vb = lhsValues[1].getValue();
	    vc = lhsValues[2].getValue();
	    vd = lhsValues[3].getValue();
	    ve = lhsValues[4].getValue();
	}
	double calculateDiffs(double x, double y[], double rs) {
	    return -(vb*y[3]+vc*y[2]+vd*y[1]+ve*y[0]-rs)/va;
	}
	String getDescription() {
	    return "ay'''' + by''' + cy'' + dy' + ey";
	}
	void newFunc() {
	    lhsValues[0].setup("a", 6, 0);
	    lhsValues[1].setup("b", 0, 0);
	    lhsValues[2].setup("c", 1, 0);
	    lhsValues[3].setup("d", 0, 0);
	    lhsValues[4].setup("e", 0, 0);
	    setPoint(0, 0);
	    initialValues[0].setValue(.1);
	    initialValues[1].setValue(.5);
	}
	LhsFunc createNext() { return new EquidimensionalOrder2LhsFunc(); }
    }

    class EquidimensionalOrder2LhsFunc extends LhsFunc {
	String getName() { return "equidimensional"; }
	double va, vb, vc;
	int getOrder() { return 2; }
	boolean positiveOnly() { return true; }
	void setup() {
	    va = lhsValues[0].getValue();
	    vb = lhsValues[1].getValue();
	    vc = lhsValues[2].getValue();
	}
	double calculateDiffs(double x, double y[], double rs) {
	    return -(vb*y[1]*x+vc*y[0]-rs)/(va*x*x);
	}
	String getDescription() {
	    return "ax\u00b2 y'' + bx y' + c y"; // XXX
	}
	void newFunc() {
	    lhsValues[0].setup("a", .5, 0);
	    lhsValues[1].setup("b",  0, 0);
	    lhsValues[2].setup("c", 20, 0);
	    setPoint(getRangeWidth()/2, yMax/2);
	}
	LhsFunc createNext() { return new PendulumLhsFunc(); }
    }

    class PendulumLhsFunc extends LhsFunc {
	String getName() { return "pendulum"; }
	double beta, omega, omega2;
	int getOrder() { return 2; }
	
	void setup() {
	    beta  = lhsValues[0].getValue();
	    omega = lhsValues[1].getValue();
	    omega2 = omega*omega;
	}

	double getRhsScale() { return omega2; }
	String getRhsScaleDescription() { return "w\u00b2 "; }

	double calculateDiffs(double x, double y[], double rs) {
	    return -2*beta*y[1]-omega2*java.lang.Math.sin(y[0])+rs;
	}

	String getDescription() {
	    return "y'' + 2by' + w\u00b2 sin(y)";
	}

	void newFunc() {
	    lhsValues[0].setup("b", .035, VEC_NONNEG);
	    lhsValues[1].setup("w",    1, VEC_NONNEG|VEC_NONZERO);
	    setPoint(getRangeStart(), 3.1);
	}

	LhsFunc createNext() { return new DuffingLhsFunc(); }
    }

    class DuffingLhsFunc extends LhsFunc {
	String getName() { return "Duffing"; }
	double va, vb, vc, vd;
	int getOrder() { return 2; }
	void setup() {
	    va = lhsValues[0].getValue();
	    vb = lhsValues[1].getValue();
	    vc = lhsValues[2].getValue();
	    vd = lhsValues[3].getValue();
	}
	double calculateDiffs(double x, double y[], double rs) {
	    return -(vb*y[1]+vc*y[0]+vd*y[0]*y[0]*y[0]-rs)/va;
	}
	String getDescription() {
	    return "ay'' + by' + cy + dy\u00b3";
	}
	void newFunc() {
	    lhsValues[0].setup("a",   6, 0);
	    lhsValues[1].setup("b",  .6, 0);
	    lhsValues[2].setup("c",   1, 0);
	    lhsValues[3].setup("d", .77, 0);
	    setPoint(getRangeStart(), yMax);
	}
	LhsFunc createNext() { return new VanDerPolFunc(); }
    }

    class VanDerPolFunc extends LhsFunc {
	String getName() { return "Van der Pol"; }
	double va, vb, vc, vd;
	int getOrder() { return 2; }
	void setup() {
	    va = lhsValues[0].getValue();
	    vb = lhsValues[1].getValue();
	    vc = lhsValues[2].getValue();
	    vd = lhsValues[3].getValue();
	}
	double calculateDiffs(double x, double y[], double rs) {
	    return (vb*(vc*vc-y[0]*y[0])*y[1]-vd*y[0]+rs)/va;
	}
	String getDescription() {
	    return "ay''+b(c\u00b2-y\u00b2)y'+dy";
	}
	void newFunc() {
	    lhsValues[0].setup("a",   6, 0);
	    lhsValues[1].setup("b", .04, 0);
	    lhsValues[2].setup("c",   3, 0);
	    lhsValues[3].setup("d",  21, 0);
	    setPoint(getRangeStart(), 1);
	}
	LhsFunc createNext() { return null; }
    }

    abstract class RhsFunc {
	void setup() {}
	boolean isCustom() { return false; }
	abstract double calculate(double x, double y[]);
	abstract RhsFunc createNext();
	abstract String getName();
	abstract String getDescription();
	void newFunc() {}
    }

    class ZeroRhsFunc extends RhsFunc {
	String getName() { return "zero"; }
	double calculate(double x, double y[]) { return 0; }
	RhsFunc createNext() { return new ImpulseRhsFunc(); }
	String getDescription() { return "0"; }
    }

    class ImpulseRhsFunc extends RhsFunc {
	String getName() { return "impulse"; }
	double vd, ve;
	void setup() {
	    vd = rhsValues[0].getValue();
	    ve = rhsValues[1].getValue();
	}
	double calculate(double x, double y[]) {
	    return x < 0 ? 0 : (x < vd) ? ve : 0;
	}
	RhsFunc createNext() { return new StepRhsFunc(); }
	String getDescription() { return "h(H(x-g)-H(x))"; }
	void newFunc() {
	    rhsValues[0].setup("g", 20, 0);
	    rhsValues[1].setup("h",  3, 0);
	}
    }

    class StepRhsFunc extends RhsFunc {
	double ve;
	void setup() {
	    ve = rhsValues[0].getValue();
	}
	String getName() { return "step"; }
	double calculate(double x, double y[]) {
	    return x > 0 ? ve : 0;
	}
	String getDescription() { return "h H(x)"; }
	RhsFunc createNext() { return new SquareWaveRhsFunc(); }
	void newFunc() {
	    rhsValues[0].setup("h", 3, 0);
	}
    }

    class SquareWaveRhsFunc extends RhsFunc {
	String getName() { return "square wave"; }
	double vd, ve;
	void setup() {
	    vd = 3.14159265*2/rhsValues[0].getValue();
	    ve = rhsValues[1].getValue();
	}
	double calculate(double x, double y[]) {
	    return (((int) x+100) % vd > (vd/2)) ? -ve : ve;
	}
	String getDescription() { return "h Square(gx)"; }
	RhsFunc createNext() { return new SineWaveRhsFunc(); }
	void newFunc() {
	    rhsValues[0].setup("g", .25, 0);
	    rhsValues[1].setup("h",   3, 0);
	}
    }

    class SineWaveRhsFunc extends RhsFunc {
	String getName() { return "sine wave"; }
	double vd, ve;
	void setup() {
	    vd = rhsValues[0].getValue();
	    ve = rhsValues[1].getValue();
	}
	double calculate(double x, double y[]) {
	    return java.lang.Math.sin(x*vd)*ve;
	}
	String getDescription() { return "h sin(gx)"; }
	RhsFunc createNext() { return new SawtoothRhsFunc(); }
	void newFunc() {
	    rhsValues[0].setup("g", .25, 0);
	    rhsValues[1].setup("h",   3, 0);
	}
    }
    class SawtoothRhsFunc extends RhsFunc {
	String getName() { return "sawtooth"; }
	double vd, ve;
	void setup() {
	    vd = 3.14159265*2/rhsValues[0].getValue();
	    ve = rhsValues[1].getValue();
	}
	double calculate(double x, double y[]) {
	    x = (x+100) % vd;
	    return (2*x/vd - 1) * ve;
	}
	String getDescription() { return "h Saw(gx)"; }
	RhsFunc createNext() { return new TriangleRhsFunc(); }
	void newFunc() {
	    rhsValues[0].setup("g", .25, 0);
	    rhsValues[1].setup("h",   3, 0);
	}
    }
    class TriangleRhsFunc extends RhsFunc {
	String getName() { return "triangle"; }
	double vd, ve;
	void setup() {
	    vd = 3.14159265/rhsValues[0].getValue();
	    ve = rhsValues[1].getValue();
	}
	double calculate(double x, double y[]) {
	    x = (x+100) % (vd*2);
	    if (x >= vd)
		return -(2*(x-vd)/vd - 1)*ve;
	    return (2*x/vd - 1) * ve;
	}
	String getDescription() { return "h Tri(gx)"; }
	void newFunc() {
	    rhsValues[0].setup("g", .25, 0);
	    rhsValues[1].setup("h",   3, 0);
	}
	RhsFunc createNext() { return new LinearRhsFunc(); }
    }
    class LinearRhsFunc extends RhsFunc {
	String getName() { return "linear"; }
	double vd;
	void setup() {
	    vd = rhsValues[0].getValue();
	}
	double calculate(double x, double y[]) {
	    return vd*x;
	}
	RhsFunc createNext() { return new ExponentialRhsFunc(); }
	String getDescription() { return "hx"; }
	void newFunc() {
	    rhsValues[0].setup("h", .1, 0);
	}
    }
    class ExponentialRhsFunc extends RhsFunc {
	String getName() { return "exponential"; }
	double vg, vh;
	void setup() {
	    vg = rhsValues[0].getValue();
	    vh = rhsValues[1].getValue();
	}
	double calculate(double x, double y[]) {
	    return java.lang.Math.exp(x*vg)*vh;
	}
	RhsFunc createNext() { return new CustomRhsFunc(); }
	String getDescription() { return "h exp(gx)"; }
	void newFunc() {
	    rhsValues[0].setup("g", .035, 0);
	    rhsValues[1].setup("h", .33,  0);
	}
    }

    class CustomRhsFunc extends RhsFunc {
	String getName() { return "custom function"; }
	boolean isCustom() { return true; }
	RhsFunc createNext() { return new CustomYRhsFunc(); }
	double calculate(double x, double y[]) {
	    int xx = (int) ((x - xRangeStart) /xRangeWidth * sampleCount);
	    if (xx < 0)
		xx = 0;
	    if (xx >= sampleCount)
		xx = sampleCount-1;
	    return forceFunc[xx];
	}
	String getDescription() { return "f(x)"; }
    }

    class CustomYRhsFunc extends RhsFunc {
	String getName() { return "custom function * y"; }
	boolean isCustom() { return true; }
	RhsFunc createNext() { return new CustomYPRhsFunc(); }
	double calculate(double x, double y[]) {
	    int xx = (int) ((x - xRangeStart) /xRangeWidth * sampleCount);
	    if (xx < 0)
		xx = 0;
	    if (xx >= sampleCount)
		xx = sampleCount-1;
	    return y == null ? forceFunc[xx] : forceFunc[xx] * y[0];
	}
	String getDescription() { return "f(x)y"; }
    }

    class CustomYPRhsFunc extends RhsFunc {
	String getName() { return "custom function * y'"; }
	boolean isCustom() { return true; }
	RhsFunc createNext() { return null; }
	double calculate(double x, double y[]) {
	    int xx = (int) ((x - xRangeStart) /xRangeWidth * sampleCount);
	    if (xx < 0)
		xx = 0;
	    if (xx >= sampleCount)
		xx = sampleCount-1;
	    return y == null ? forceFunc[xx] : forceFunc[xx] * y[1];
	}
	String getDescription() { return "f(x)y'"; }
    }

    class ValueEditCanvas extends Canvas
	  implements MouseListener, MouseMotionListener {

	boolean selectedNumber, selectedSign, dragging;
	int dragX, dragY;
	int flags;
	int labelWidth, signWidth;
	int storedChange;
	double value, oldValue, max;
	String label;
	static final int intscale = 4;

	ValueEditCanvas() {
	    addMouseMotionListener(this);
	    addMouseListener(this);
	    value = 1;
	    label = "";
	}

	void setLabel(String s) {
	    label = s + " = ";
	    flags = 0;
	    max = 1e80;
	    show();
	    repaint();
	}

	void setMax(double d) { max = d; }

	void setup(String s, double x, int f) {
	    setLabel(s);
	    setFlags(f);
	    setValue(x);
	}

	double getValue() {
	    return value;
	}

	void setValue(double x) {
	    // setValue() doesn't do anything if we are in the middle
	    // of editing the value.
	    if (dragging)
		return;
	    /*double xabs = java.lang.Math.abs(x);
	    if (xabs > 0 && xabs < 1e-6)
	    x = 0;*/ // XXX
	    value = x;
	    round();
	    repaint();
	}

	void round() {
	    if (isInteger() || isHalfInteger()) {
		value = (int) value;
		if (isHalfInteger()) {
		    if (value < 0)
			value -= .5;
		    else
			value += .5;
		}
	    }
	}

	boolean isNonNegative() {
	    return (flags & VEC_NONNEG) != 0;
	}

	boolean isInteger() {
	    return (flags & VEC_INTEGER) != 0;
	}

	boolean isHalfInteger() {
	    return (flags & VEC_HALFINTEGER) != 0;
	}

	boolean isNonZero() {
	    return (flags & VEC_NONZERO) != 0;
	}

	boolean isConst() {
	    return (flags & VEC_CONSTANT) != 0;
	}

	void setFlags(int f) {
	    flags = f;
	    if (isNonNegative() && value < 0)
		value = -value;
	    if (isNonZero() && value == 0)
		value = 1;
	    repaint();
	}
	public Dimension getPreferredSize() {
	    return new Dimension(200,20);
	}
	public void paint(Graphics g) {
	    FontMetrics fm = g.getFontMetrics();
	    labelWidth = fm.stringWidth(label);
	    signWidth = fm.stringWidth("+ ");
	    g.drawString(label, 0, 15);
	    g.setColor(selectedSign ? Color.red : Color.blue);
	    g.drawString(value > 0 ? "+" :
			 value == 0 ? "0" : "-", labelWidth, 15);
	    if (value != 0) {
		g.setColor(selectedNumber || dragging ?
			   Color.red : Color.blue);
		double v = java.lang.Math.abs(value);
		String s = nf.format(v);
		if (s.equals("0")) {
		    s = Double.toString(v);
		    int n = s.indexOf('E');
		    s = s.substring(0, 5) + s.substring(n);
		}
		g.drawString(s, labelWidth+signWidth, 15);
	    }
	}
	public void mouseDragged(MouseEvent e) {
	    if (isConst())
		return;
	    int change = e.getX() - dragX;
	    if (value == 0) {
		value = (change > 0) ? .01 : -.01;
		if (isNonNegative() && change < 0)
		    value = 0;
	    }
	    if (isInteger() || isHalfInteger()) {
		change += storedChange;
		storedChange = (change % intscale);
		value += change/intscale;
		if (isNonNegative() && value < 0)
		    value = 0;
		if (isNonZero() && value == 0)
		    value = 1;
	    } else {
		if (value < 0)
		    change = -change;
		value *= java.lang.Math.exp(change*.05);
	    }
	    if (value > max)
		value = max;
	    if (java.lang.Math.abs(value) < .00001 && !isNonZero())
		value = 0;
	    round();
	    if (value != 0)
		oldValue = value;
	    valueChanged(this);
	    dragX = e.getX();
	    dragY = e.getY();
	    dragging = true;
	    repaint();
	}
	public void mouseMoved(MouseEvent e) {
	    if (isConst())
		return;
	    if (e.getX() < labelWidth+signWidth) {
		if (!selectedSign) {
		    selectedSign = true;
		    selectedNumber = false;
		    repaint();
		}
	    } else {
		if (!selectedNumber) {
		    selectedNumber = true;
		    selectedSign = false;
		    repaint();
		}
	    }
	}
	public void mouseClicked(MouseEvent e) {
	    if (selectedSign) {
		if (e.getClickCount() == 2 && !isNonZero()) {
		    oldValue = value;
		    value = 0;
		} else if (value == 0)
		    value = oldValue;
		else if (!isNonNegative())
		    value = -value;
		valueChanged(this);
		repaint();
	    }
	}
	public void mouseEntered(MouseEvent e) {
	    if (isConst())
		return;
	    if (e.getX() < labelWidth+signWidth)
		selectedSign = true;
	    else
		selectedNumber = true;
	    repaint();
	}
	public void mouseExited(MouseEvent e) {
	    if (isConst())
		return;
	    selectedNumber = selectedSign = false;
	    repaint();
	}
	public void mousePressed(MouseEvent e) {
	    dragX = e.getX();
	    dragY = e.getY();
	    storedChange = 0;
	}
	public void mouseReleased(MouseEvent e) {
	    if (isConst())
		return;
	    dragging = false;
	    repaint();
	}
    }
    
    // returns rj, rjp, ry, ryp in values[]
    void bessjy(double x, double xnu, double values[]) {
	final double EPS = 1.0e-16;
	final double FPMIN = 1.0e-30;
	final double MAXIT = 10000;
	final double XMIN = 2.0;
	final double PI = 3.141592653589793;
	
	if (x == 0)
	    x = 1e-20;
	int i,isign,l,nl;
	double a,b,br,bi,c,cr,ci,d,del,del1,den,di,dlr,dli,dr,e,f,fact,fact2,
	    fact3,ff,gam,gam1,gam2,gammi,gampl,h,p,pimu,pimu2,q,r,rjl,
	    rjl1,rjmu,rjp1,rjpl,rjtemp,ry1,rymu,rymup,rytemp,sum,sum1,
	    temp,w,x2,xi,xi2,xmu,xmu2;
  
	if (x <= 0.0 || xnu < 0.0) {
	    System.out.print("bad arguments in bessjy\n");
	    return;
	}
	nl=(x < XMIN ? (int)(xnu+0.5) : (int)(xnu-x+1.5));
	if (nl < 0)
	    nl = 0;
	xmu=xnu-nl;
	xmu2=xmu*xmu;
	xi=1.0/x;
	xi2=2.0*xi;
	w=xi2/PI;
	isign=1;
	h=xnu*xi;
	if (h < FPMIN) h=FPMIN;
	b=xi2*xnu;
	d=0.0;
	c=h;
	for (i=1;i<=MAXIT;i++) {
	    b += xi2;
	    d=b-d;
	    if (java.lang.Math.abs(d) < FPMIN) d=FPMIN;
	    c=b-1.0/c;
	    if (java.lang.Math.abs(c) < FPMIN) c=FPMIN;
	    d=1.0/d;
	    del=c*d;
	    h=del*h;
	    if (d < 0.0) isign = -isign;
	    if (java.lang.Math.abs(del-1.0) < EPS) break;
	}
	if (i > MAXIT) {
	    System.out.print("x too large in bessjy; try asymptotic expansion\n");
	    return;
	}
	rjl=isign*FPMIN;
	rjpl=h*rjl;
	rjl1=rjl;
	rjp1=rjpl;
	fact=xnu*xi;
	for (l=nl;l>=1;l--) {
	    rjtemp=fact*rjl+rjpl;
	    fact -= xi;
	    rjpl=fact*rjtemp-rjl;
	    rjl=rjtemp;
	}
	if (rjl == 0.0) rjl=EPS;
	f=rjpl/rjl;
	if (x < XMIN) {
	    x2=0.5*x;
	    pimu=PI*xmu;
	    fact = (java.lang.Math.abs(pimu) < EPS ? 1.0 : pimu/java.lang.Math.sin(pimu));
	    d = -java.lang.Math.log(x2);
	    e=xmu*d;
	    fact2 = (java.lang.Math.abs(e) < EPS ? 1.0 :
		     (java.lang.Math.exp(e)-java.lang.Math.exp(-e))/(2*e));
	    double gvalues[] = new double[4];
	    beschb(xmu,gvalues);
	    ff=2.0/PI*fact*(gvalues[0]*
			    (java.lang.Math.exp(e)+java.lang.Math.exp(-e))/2+
			    gvalues[1]*fact2*d);
	    e=java.lang.Math.exp(e);
	    p=e/(gvalues[2]*PI);
	    q=1.0/(e*PI*gvalues[3]);
	    pimu2=0.5*pimu;
	    fact3 = (java.lang.Math.abs(pimu2) < EPS ? 1.0 : java.lang.Math.sin(pimu2)/pimu2);
	    r=PI*pimu2*fact3*fact3;
	    c=1.0;
	    d = -x2*x2;
	    sum=ff+r*q;
	    sum1=p;
	    for (i=1;i<=MAXIT;i++) {
		ff=(i*ff+p+q)/(i*i-xmu2);
		c *= (d/i);
		p /= (i-xmu);
		q /= (i+xmu);
		del=c*(ff+r*q);
		sum += del;
		del1=c*p-i*del;
		sum1 += del1;
		if (java.lang.Math.abs(del) < (1.0+java.lang.Math.abs(sum))*EPS) break;
	    }
	    if (i > MAXIT) {
		System.out.print("bessy series failed to converge\n");
		return;
	    }
	    rymu = -sum;
	    ry1 = -sum1*xi2;
	    rymup=xmu*xi*rymu-ry1;
	    rjmu=w/(rymup-f*rymu);
	} else {
	    a=0.25-xmu2;
	    p = -0.5*xi;
	    q=1.0;
	    br=2.0*x;
	    bi=2.0;
	    fact=a*xi/(p*p+q*q);
	    cr=br+q*fact;
	    ci=bi+p*fact;
	    den=br*br+bi*bi;
	    dr=br/den;
	    di = -bi/den;
	    dlr=cr*dr-ci*di;
	    dli=cr*di+ci*dr;
	    temp=p*dlr-q*dli;
	    q=p*dli+q*dlr;
	    p=temp;
	    for (i=2;i<=MAXIT;i++) {
		a += 2*(i-1);
		bi += 2.0;
		dr=a*dr+br;
		di=a*di+bi;
		if (java.lang.Math.abs(dr)+java.lang.Math.abs(di) < FPMIN) dr=FPMIN;
		fact=a/(cr*cr+ci*ci);
		cr=br+cr*fact;
		ci=bi-ci*fact;
		if (java.lang.Math.abs(cr)+java.lang.Math.abs(ci) < FPMIN) cr=FPMIN;
		den=dr*dr+di*di;
		dr /= den;
		di /= -den;
		dlr=cr*dr-ci*di;
		dli=cr*di+ci*dr;
		temp=p*dlr-q*dli;
		q=p*dli+q*dlr;
		p=temp;
		if (java.lang.Math.abs(dlr-1.0)+java.lang.Math.abs(dli) < EPS) break;
	    }
	    if (i > MAXIT) {
		System.out.print("cf2 failed in bessjy\n");
		return;
	    }
	    gam=(p-f)/q;
	    rjmu=java.lang.Math.sqrt(w/((p-f)*gam+q));
	    rjmu=(rjl > 0) ? rjmu : -rjmu;
	    rymu=rjmu*gam;
	    rymup=rymu*(p+q/gam);
	    ry1=xmu*xi*rymu-rymup;
	}
	fact=rjmu/rjl;
	values[0] = rjl1*fact;
	values[1] = rjp1*fact;
	for (i=1;i<=nl;i++) {
	    rytemp=(xmu+i)*xi2*ry1-rymu;
	    rymu=ry1;
	    ry1=rytemp;
	}
	values[2] = rymu;
	values[3] = xnu*xi*rymu-ry1;
    }
    
    // returns gam1, gam2, gampl, gammi in values[]
    void beschb(double x, double values[]) {
	double xx;
	final double c1[] = {
	    -1.142022680371172e0,6.516511267076e-3,
		3.08709017308e-4,-3.470626964e-6,6.943764e-9,
		3.6780e-11,-1.36e-13};
	final double c2[] = {
		1.843740587300906e0,-0.076852840844786e0,
		1.271927136655e-3,-4.971736704e-6,-3.3126120e-8,
		2.42310e-10,-1.70e-13,-1.0e-15};

	xx=8.0*x*x-1.0;
	values[0] = chebev(-1.0,1.0,c1,7,xx);
	values[1] = chebev(-1.0,1.0,c2,8,xx);
	values[2] = values[1]-x*values[0];
	values[3] = values[1]+x*values[0];
    }

    double chebev(double a,double b,double c[],int m,double x) {
	double d=0.0,dd=0.0,sv,y,y2;
	int j;
	
	if ((x-a)*(x-b) > 0.0) {
	    System.out.print("x not in range in routine CHEBEV\n");
	    return 0;
	}
	y2=2.0*(y=(2.0*x-a-b)/(b-a));
	for (j=m-1;j>=1;j--) {
	    sv=d;
	    d=y2*d-dd+c[j];
	    dd=sv;
	}
	return y*d-dd+0.5*c[0];
    }

    void legendreP(int n, double x, double pn[], double pd[])
    {
	double p0 = 1;
	double p1 = x;
	int k;
	pn[0] = 1; pn[1] = x;
	pd[0] = 0; pd[1] = 1;
	for (k = 2; k <= n; k++) {
	    double pf = (2*k-1.)/k*x*p1-(k-1.)/k*p0;
	    pn[k] = pf;
	    if (x == 1 || x == -1)
		pd[k] = .5*java.lang.Math.pow(x, k+1)*k*(k+1);
	    else
		pd[k] = k*(p1-x*pf)/(1-x*x);
	    p0 = p1;
	    p1 = pf;
	}
    }
    
    void legendreQ(int n, double x, double qn[], double qd[])
    {
	double q0, q1;
	int k;
	if (x == 1)
	    x = .999999999999999;
	if (x == -1)
	    x = -.99999999999999;
	q0 = .5*java.lang.Math.log((1+x)/(1-x));
	q1 = x*q0 - 1;
	qn[0] = q0;
	qn[1] = q1;
	qd[0] = 1/(1-x*x);
	qd[1] = qn[0]+x*qd[0];
	for (k = 2; k <= n; k++) {
	    double qf = ((2*k-1.)*x*q1-(k-1)*q0)/k;
	    qn[k] = qf;
	    qd[k] = (qn[k-1]-x*qf)*k/(1-x*x);
	    q0 = q1;
	    q1 = qf;
	}
    }
}
