// base.java (C) 2001 by Paul Falstad, www.falstad.com
// this file must be run through cpp with one of BUILD_E, BUILD_V,
// BUILD_M defined before compiling with java compiler.  This was done
// to reduce class file sizes.

import java.io.InputStream;
import java.awt.*;
import java.awt.image.*;
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.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.awt.event.*;

#ifdef BUILD_E
#define BUILD_CASE_EMV(E,M,V) E
#elif defined(BUILD_M)
#define BUILD_CASE_EMV(E,M,V) M
#else
#define BUILD_CASE_EMV(E,M,V) V
#endif

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

class VecDemoLayout implements LayoutManager {
    public VecDemoLayout() {}
    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) {
	int barwidth = 0;
	int i;
	for (i = 1; i < target.getComponentCount(); i++) {
	    Component m = target.getComponent(i);
	    if (m.isVisible()) {
		Dimension d = m.getPreferredSize();
		if (d.width > barwidth)
		    barwidth = d.width;
	    }
	}
	Insets insets = target.insets();
	int targetw = target.size().width - insets.left - insets.right;
	int cw = targetw-barwidth;
	int targeth = target.size().height - (insets.top+insets.bottom);
	target.getComponent(0).move(insets.left, insets.top);
	target.getComponent(0).resize(cw, targeth);
	cw += insets.left;
	int h = insets.top;
	for (i = 1; i < target.getComponentCount(); i++) {
	    Component m = target.getComponent(i);
	    if (m.isVisible()) {
		Dimension d = m.getPreferredSize();
		if (m instanceof DecentScrollbar || m instanceof TextComponent)
		    d.width = barwidth;
		if (m instanceof Choice && d.width > barwidth)
		    d.width = barwidth;
		if (m instanceof Label) {
		    h += d.height/5;
		    d.width = barwidth;
		}
		m.move(cw, h);
		m.resize(d.width, d.height);
		h += d.height;
	    }
	}
    }
};

public class VecDemo extends Applet implements ComponentListener {
    VecDemoFrame ogf;
    void destroyFrame() {
	if (ogf != null)
	    ogf.dispose();
	ogf = null;
	repaint();
    }
    boolean started = false;
    public void init() {
	addComponentListener(this);
    }
    
    void showFrame() {
	if (ogf == null) {
	    started = true;
	    ogf = new VecDemoFrame(this);
	    ogf.init();
	    repaint();
	}
    }
    
    public void paint(Graphics g) {
	String s = "Applet is open in a separate window.";
	if (!started)
	    s = "Applet is starting.";
	else if (ogf == null)
	    s = "Applet is finished.";
	else
	    ogf.show();
	g.drawString(s, 10, 30);
    }
    
    public void componentHidden(ComponentEvent e){}
    public void componentMoved(ComponentEvent e){}
    public void componentShown(ComponentEvent e) { showFrame(); }
    public void componentResized(ComponentEvent e) {}
    
    public void destroy() {
	if (ogf != null)
	    ogf.dispose();
	ogf = null;
	repaint();
    }
};

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

    Dimension winSize;
    Rectangle viewMain, viewAxes;
    Image dbimage;
    Image backimage;
    MemoryImageSource imageSource;
    int pixels[];
    
    VecDemo applet;
    Random random;
    
    public String getAppletInfo() {
	return "VecDemo by Paul Falstad";
    }

    static final double pi = 3.14159265358979323846;

    int getrand(int x) {
	int q = random.nextInt();
	if (q < 0) q = -q;
	return q % x;
    }
    VecDemoCanvas cv;
    Checkbox stoppedCheck;
    Button resetButton;
    Button kickButton;
    Checkbox reverseCheck;
    Button infoButton;
    Choice functionChooser;
    Choice dispChooser;
    static final int DISP_PART_VELOC    = 0;
    static final int DISP_PART_FORCE    = 1;
    static final int DISP_VECTORS       = 2;
    static final int DISP_NONE          = 3;
    static final int DISP_CURLERS       = 4;
    Label partCountLabel;
    Label textFieldLabel;
    Label strengthLabel;
    DecentScrollbar partCountBar;
    DecentScrollbar strengthBar;
    DecentScrollbar aux1Bar;
    DecentScrollbar aux2Bar;
    DecentScrollbar aux3Bar;
    double fieldStrength, barFieldStrength;
    Color darkYellow = new Color(144, 144, 0);
    final double lineWidth = .001;
    class AuxBar {
	DecentScrollbar bar;
	Label label;
	AuxBar(Label l, DecentScrollbar b) { label = l; bar = b; }
    };
    AuxBar auxBars[];
    Label vecDensityLabel;
    DecentScrollbar vecDensityBar;
    Label potentialLabel;
    DecentScrollbar potentialBar;
    Label lineDensityLabel;
    DecentScrollbar lineDensityBar;
    Choice modeChooser;
    Choice floorColorChooser;
    Choice floorLineChooser;

    TextField textFields[];
    int reverse;
    int xpoints[];
    int ypoints[];
    GridElement grid[][];
    Particle particles[];
    FieldVector vectors[];
    int vecCount;
    int density[][];
    Checkbox flatCheck;
    boolean isFlat;
    double viewAngle, viewAngleDragStart;
    double viewZoom = 1.6, viewZoomDragStart;
    double viewAngleCos = 1, viewAngleSin = 0;
    double viewHeight = 2, viewHeightDragStart;
    double viewDistance = 5;
    int integralX = -1, integralY;
    int vectorSpacing = 16;
    int currentStep;
    boolean showA;
    boolean parseError;
    Color fieldColors[];
    static final int gridsize = 80;

    static final int densitygridsize = 16;
    static final double densitygroupsize = 2./densitygridsize;
    static final int maxParticleCount = 2500;
    boolean functionChanged;
    boolean backgroundChanged;
    boolean dragging, draggingView;
    int oldDragX, oldDragY, dragX, dragY, dragStartX, dragStartY;
    double dragZoomStart;
    Vector functionList;
    VecFunction curfunc;
    int pause = 20;
    static final int MOT_VELOCITY = 0;
    static final int MOT_FORCE    = 1;
    static final int MOT_CURLERS  = 2;
    static final int MOT_EQUIPOTENTIAL = 3;
    static final int FC_FIELD     = 0;
    static final int FC_POTENTIAL = 1;
    static final int FC_NONE      = 2;
    static final int FC_DIV       = 3;
    static final int FC_CURL      = 4;
    static final int FL_NONE      = 0;
    static final int FL_GRID      = 1;
    static final int FL_EQUIP     = 2;
    static final int FL_LINES     = 3;

    static final int MODE_VIEW_ROTATE  = 0;
    static final int MODE_VIEW_ZOOM    = 1;
    static final int MODE_LINE_INT     = 2;
    static final int MODE_SURF_INT     = 3;

    boolean useBufferedImage = false;
    
    VecDemoFrame(VecDemo a) {
	super(BUILD_CASE_EMV("2-D Electrostatic Fields Applet v1.4", null,
			     "2-D Vector Fields Applet v1.4"));
	applet = a;
    }

    public void init() {
	try {
	    String param = applet.getParameter("PAUSE");
	    if (param != null)
		pause = Integer.parseInt(param);
	} catch (Exception e) { }

        String jv = System.getProperty("java.class.version");
        double jvf = new Double(jv).doubleValue();
        if (jvf >= 48)
	    useBufferedImage = true;
	
	functionList = new Vector();
	VecFunction vf = new InverseRadial();
	int ct = 0;
	while (vf != null) {
	    functionList.addElement(vf);
	    vf = vf.createNext();
	    if (ct == 1000) {
		System.out.print("setup loop\n");
		return;
	    }
	}

	Color particleColors[] = new Color[27];
	int i;
	for (i = 0; i != 27; i++)
	    particleColors[i] = new Color(((i%3)+1)*85,
					  ((i/3)%3+1)*85,
					  ((i/9)%3+1)*85);

	random = new Random();
	particles = new Particle[maxParticleCount];
	for (i = 0; i != maxParticleCount; i++) {
	    particles[i] = new Particle();
	    particles[i].color = particleColors[i % 27];
	}
	xpoints = new int[4];
	ypoints = new int[4];
	density = new int[densitygridsize][densitygridsize];
	setLayout(new VecDemoLayout());
	cv = new VecDemoCanvas(this);
	cv.addComponentListener(this);
	cv.addMouseMotionListener(this);
	cv.addMouseListener(this);
	add(cv);

	/*infoButton = new Button("Function Info");
	add(infoButton);
	infoButton.addActionListener(this);*/

	functionChooser = new Choice();
	for (i = 0; i != functionList.size(); i++)
	    functionChooser.add("Setup: " +
	       ((VecFunction) functionList.elementAt(i)).getName());
	add(functionChooser);
	functionChooser.addItemListener(this);

	floorColorChooser = new Choice();
	floorColorChooser.add("Color: field magnitude");
	floorColorChooser.add("Color: potential");
	floorColorChooser.add("Color: none");
#ifdef BUILD_E
	floorColorChooser.add("Color: charge");
#else
	floorColorChooser.add("Color: divergence");
	floorColorChooser.add("Color: curl z");
#endif
	floorColorChooser.addItemListener(this);
	add(floorColorChooser);

	floorLineChooser = new Choice();
	floorLineChooser.add("Floor: no lines");
	floorLineChooser.add("Floor: grid");
	floorLineChooser.add("Floor: equipotentials");
#ifdef BUILD_V
	floorLineChooser.add("Floor: streamlines");
#else
	floorLineChooser.add("Floor: field lines");
#endif
	floorLineChooser.addItemListener(this);
	add(floorLineChooser);
	floorLineChooser.select(FL_EQUIP);

	flatCheck = new Checkbox("Flat View");
	flatCheck.addItemListener(this);
	add(flatCheck);

	dispChooser = new Choice();
	dispChooser.addItemListener(this);
	setupDispChooser(true);
	add(dispChooser);

	modeChooser = new Choice();
	modeChooser.add("Mouse = Adjust Angle");
	modeChooser.add("Mouse = Adjust Zoom");
	modeChooser.add("Mouse = Line Integral");
	modeChooser.add("Mouse = Surface Integral");
	modeChooser.addItemListener(this);
	add(modeChooser);
	
	stoppedCheck = new Checkbox("Stopped");
	stoppedCheck.addItemListener(this);
	add(stoppedCheck);

	reverseCheck = new Checkbox("Reverse");
	reverseCheck.addItemListener(this);
	add(reverseCheck);

	resetButton = new Button("Reset");
	add(resetButton);
	resetButton.addActionListener(this);
	kickButton = new Button("Kick");
	add(kickButton);
	kickButton.addActionListener(this);
	kickButton.disable();

	add(strengthLabel = new Label("Field Strength", Label.CENTER));
	add(strengthBar = new DecentScrollbar(this, 80, 1, 120));

	add(partCountLabel = new Label("Number of Particles", Label.CENTER));
	add(partCountBar = new DecentScrollbar(this, 500, 1, maxParticleCount));

	add(vecDensityLabel = new Label("Vector Density", Label.CENTER));
	add(vecDensityBar = new DecentScrollbar(this, 32, 2, 64));

	/*add(lineDensityLabel = new Label("Line Density", Label.CENTER));
	  add(lineDensityBar = new DecentScrollbar(this, 5, 3, 8));*/

	add(potentialLabel = new Label("Potential", Label.CENTER));
	add(potentialBar = new DecentScrollbar(this, 250, 0, 1000));

	Label lb;
	auxBars = new AuxBar[3];
	add(lb = new Label("Aux 1", Label.CENTER));
	add(aux1Bar = new DecentScrollbar(this, 0, 0, 100));
	auxBars[0] = new AuxBar(lb, aux1Bar);

	add(lb = new Label("Aux 2", Label.CENTER));
	add(aux2Bar = new DecentScrollbar(this, 0, 0, 100));
	auxBars[1] = new AuxBar(lb, aux2Bar);

	add(lb = new Label("Aux 3", Label.CENTER));
	add(aux3Bar = new DecentScrollbar(this, 0, 0, 100));
	auxBars[2] = new AuxBar(lb, aux3Bar);

#ifdef BUILD_V
	add(textFieldLabel = new Label("", Label.CENTER));
#endif

	textFields = new TextField[2];
	for (i = 0; i != 2; i++) {
	    add(textFields[i] = new TextField());
	    textFields[i].addActionListener(this);
	}
	fieldColors = new Color[513];
	int grayLevel = 76;
	for (i = 0; i != 256; i++) {
	    int rb = grayLevel+(128-grayLevel)*i/255;
	    int g = grayLevel+(255-grayLevel)*i/255;
	    int col = (255<<24) | (g<<8) | (rb<<16) | (rb);
	    fieldColors[i] = new Color(col);
	}
	for (i = 0; i != 256; i++) {
	    int col = (255<<24) | (255<<8) | ((i/2+128) * (0x10001));
	    fieldColors[i+256] = new Color(col);
	}
	fieldColors[512] = fieldColors[511];

	add(new Label("http://www.falstad.com", Label.CENTER));

	reinit();
	cv.setBackground(Color.black);
	cv.setForeground(Color.lightGray);

	resize(650, 500);
	handleResize();
	Dimension screen = getToolkit().getScreenSize();
	Dimension x = getSize();
	setLocation((screen.width  - x.width)/2,
		    (screen.height - x.height)/2);
	functionChanged();
	dispChooserChanged();
	show();
    }

    void handleResize() {
        Dimension d = winSize = cv.getSize();
	if (winSize.width == 0)
	    return;
	dbimage = createImage(d.width, d.height);
	scaleworld();
	viewMain = new Rectangle(winSize);
	viewAxes = new Rectangle(winSize.width-100, 0, 100, 100);
	backgroundChanged = true;
	
	pixels = null;
	if (useBufferedImage) {
	    try {
		/* simulate the following code using reflection:
		   dbimage = new BufferedImage(d.width, d.height,
		   BufferedImage.TYPE_INT_RGB);
		   DataBuffer db = (DataBuffer)(((BufferedImage)dbimage).
		   getRaster().getDataBuffer());
		   DataBufferInt dbi = (DataBufferInt) db;
		   pixels = dbi.getData();
		*/
		Class biclass = Class.forName("java.awt.image.BufferedImage");
		Class dbiclass = Class.forName("java.awt.image.DataBufferInt");
		Class rasclass = Class.forName("java.awt.image.Raster");
		Constructor cstr = biclass.getConstructor(
		    new Class[] { int.class, int.class, int.class });
		backimage = (Image) cstr.newInstance(new Object[] {
		    new Integer(d.width), new Integer(d.height),
		    new Integer(BufferedImage.TYPE_INT_RGB)});
		Method m = biclass.getMethod("getRaster", null);
		Object ras = m.invoke(backimage, null);
		Object db = rasclass.getMethod("getDataBuffer", null).
		    invoke(ras, null);
		pixels = (int[])
		    dbiclass.getMethod("getData", null).invoke(db, null);
	    } catch (Exception ee) {
		// ee.printStackTrace();
		System.out.println("BufferedImage failed");
	    }
	}
	if (pixels == null) {
	    pixels = new int[d.width*d.height];
	    int i;
	    for (i = 0; i != d.width*d.height; i++)
		pixels[i] = 0xFF000000;
	    imageSource = new MemoryImageSource(d.width, d.height, pixels, 0,
						d.width);
	    imageSource.setAnimated(true);
	    imageSource.setFullBufferUpdates(true);
	    backimage = cv.createImage(imageSource);
	}
    }

    void resetDensityGroups() {
	int i, j, k;
	for (i = 0; i != densitygridsize; i++)
	    for (j = 0; j != densitygridsize; j++)
		density[i][j] = 0;
	int pcount = getParticleCount();
	for (i = 0; i != pcount; i++) {
	    Particle p = particles[i];
	    addToDensityGroup(p);
	}

	// invalidate all unused particles so they will be
	// repositioned if the particle slider is moved to the right,
	// rather than just falling in where they happened to be last time
	for (; i != maxParticleCount; i++) {
	    Particle p = particles[i];
	    p.lifetime = -100;
	}
    }

    int addToDensityGroup(Particle p) {
	int a = (int)((p.pos[0]+1)*(densitygridsize/2));
	int b = (int)((p.pos[1]+1)*(densitygridsize/2));
	int n = 0;
	try {
	n = ++density[a][b];
	if (n > maxParticleCount)
	    System.out.print(a + " " + b + " " + density[a][b] + "\n");
	} catch (Exception e) {
	    System.out.print(p.pos[0] + " " + p.pos[1] + "\n");
	    e.printStackTrace();
	}
	return n;
    }

    void removeFromDensityGroup(Particle p) {
	int a = (int)((p.pos[0]+1)*(densitygridsize/2));
	int b = (int)((p.pos[1]+1)*(densitygridsize/2));
	try {
	    if (--density[a][b] < 0)
		System.out.print(a + " " + b + " " + density[a][b] + "\n");
	} catch (Exception e) {
	    System.out.print(p.pos[0] + " " + p.pos[1] + "\n");
	    e.printStackTrace();
	}
    }

    void positionParticle(Particle p) {
	int x, y;
	int bestx = 0, besty = 0;
	int best = 10000;

	// we avoid scanning the grid in the same order every time
	// so that we treat equal-density squares as equally as possible.
	int randaddx = getrand(densitygridsize);
	int randaddy = getrand(densitygridsize);
	for (x = 0; x != densitygridsize; x++)
	    for (y = 0; y != densitygridsize; y++) {
		int ix = (randaddx + x) % densitygridsize;
		int iy = (randaddy + y) % densitygridsize;
		if (density[ix][iy] <= best) {
		    bestx = ix;
		    besty = iy;
		    best = density[ix][iy];
		}
	    }
	p.pos[0] = bestx*densitygroupsize +
	    getrand(100)*densitygroupsize/100.0 - 1;
	p.pos[1] = besty*densitygroupsize +
	    getrand(100)*densitygroupsize/100.0 - 1;
	p.lifetime = curfunc.redistribute() ? 500 : 5000;
	p.stepsize = 1;
	p.theta = (getrand(101)-50)*pi/50.;
	p.phi   = (getrand(101)-50)*pi/50.;
	int j;
	for (j = 0; j != 3; j++)
	    p.vel[j] = 0;
    }

    int getParticleCount() {
	return partCountBar.getValue();
    }

    void resetParticles() {
	int pcount = getParticleCount();
	int i, j;
	for (i = 0; i != pcount; i++) {
	    Particle p = particles[i];
	    for (j = 0; j != 2; j++) {
		p.pos[j] = getrand(200)/100.0 - 1;
		p.vel[j] = 0;
	    }
	    p.pos[2] = 0;
	    p.lifetime = i*2;
	    p.stepsize = 1;
	}
	integralX = -1;
	resetDensityGroups();
    }

    void kickParticles() {
	int i, j;
	for (i = 0; i != getParticleCount(); i++) {
	    Particle p = particles[i];
	    for (j = 0; j != 2; j++)
		p.vel[j] += (getrand(100)/99.0 - .5) * .04;
	}
    }

    void generateFunction() {
	int x, y;
	if (grid == null)
	    grid = new GridElement[gridsize+1][gridsize+1];
	curfunc.setupFrame();
	divOffset = curfunc.getDivOffset();
	divRange = curfunc.getDivRange();
	double mu, xx, xx2, yy, yy2, r, r1, r2, r3, r4;
	double levelheight = curfunc.getLevelHeight();
	//System.out.print("gf1\n");
	for (x = 0; x != gridsize+1; x++)
	    for (y = 0; y != gridsize+1; y++) {
		GridElement ge = grid[x][y] = new GridElement();
		ge.curl = ge.div = ge.height = 0;
		curfunc.setGrid(ge, x, y);
		/*ge.normdot = (ge.vecX+ge.vecY+1)*(1/1.73)/
		  java.lang.Math.sqrt(ge.vecX*ge.vecX+ge.vecY*ge.vecY+1);*/
	    }
	curfunc.calcDivergence();
	double zval = 2.0/gridsize;
	for (y = 0; y != gridsize; y++)
	    for (x = 0; x != gridsize; x++) {
		GridElement ge = grid[x][y];
		double vecx = grid[x+1][y].height-ge.height;
		double vecy = grid[x][y+1].height-ge.height;
		// light vector = (1,1,1)/1.73
		// norm vector = (-dzdx, -dzdy, 1)
		ge.normdot = (vecx+vecy+zval)*(1/1.73)/
		    java.lang.Math.sqrt(vecx*vecx+vecy*vecy+zval*zval);
	    }
	for (x = 0; x != gridsize+1; x++) {
	    grid[gridsize][x] = grid[gridsize-1][x];
	    grid[x][gridsize] = grid[x][gridsize-1];
	}
	//System.out.print("gf2\n");
	functionChanged = false;
	backgroundChanged = true;
    }

    double divOffset, divRange;

    int computeColor(GridElement ge, double c) {
	if (c < 0)
	    c = 0;
	if (c > 1)
	    c = 1;
	//System.out.print(c + "\n");
	c = .5 + c * .5;
	double value = 0;
	double range = 10;
	double offset = 4;
	switch (floorColorChooser.getSelectedIndex()) {
	case FC_FIELD:
	    value = ge.vecX*ge.vecX+ge.vecY*ge.vecY;
	    offset = 10;
	    range = 16;
	    if (!ge.valid)
		return 0xFF000080;
	    break;
	case FC_POTENTIAL:
	    value = ge.height-curfunc.getLevelHeight();
	    offset = 1;
	    range = 2;
	    break;
	case FC_CURL:
	    value = ge.curl;
	    offset = 4;
	    range = 10;
	    break;
	case FC_DIV:
	    value = ge.div;
	    offset = divOffset;
	    range = divRange;
	    break;
	case FC_NONE:
	    if (!ge.valid)
		return 0xFF000080;
	    break;
	}
	value *= floorBrightMult;
	double redness =
	    (value < 0) ? (java.lang.Math.log(-value)+offset)/range : 0;
	double grnness =
	    (value > 0) ? (java.lang.Math.log( value)+offset)/range : 0;
	if (redness > 1)
	    redness = 1;
	if (grnness > 1)
	    grnness = 1;
	if (grnness < 0)
	    grnness = 0;
	if (redness < 0)
	    redness = 0;
	/*if (redness > 0)
	  System.out.print(redness + "\n");*/
	double grayness = (1-(redness+grnness))*c;
	double gray = .6;
	int r = (int) ((c*redness+gray*grayness)*255);
	int g = (int) ((c*grnness+gray*grayness)*255);
	int b = (int) ((gray*grayness)*255);
	return (255<<24) | (r<<16) | (g<<8) | b;
    }

    
    void reinit() {
	handleResize();
	resetParticles();
	functionChanged = backgroundChanged = true;
    }

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

    int shadowBufferTop[], shadowBufferBottom[],
	shadowBufferTop2[], shadowBufferBottom2[];

    final double floorBrightMult = 2;

    void drawBackground() {
	if (isFlat) {
	    int x, y;
	    for (y = 0; y < gridsize; y++)
		for (x = 0; x < gridsize; x++) {
		    GridElement ge = grid[x][y];
		    int nx  = x*winSize.width/gridsize;
		    int ny  = winSize.height-(y+1)*winSize.height/gridsize;
		    int nx1 = (x+1)*winSize.width/gridsize;
		    int ny1 = winSize.height-y*winSize.height/gridsize;
		    int col = computeColor(ge, 0);
		    fillRectangle(nx, ny, nx1, ny1, col);
		    ge.visible = true;
		}
	    drawFloor();
	    functionChanged = backgroundChanged = false;
	    if (imageSource != null)
		imageSource.newPixels();
	    return;
	}

	scaleworld();
	int x, y;

	int xdir, xstart, xend;
	int ydir, ystart, yend;
	int sc = gridsize;
	// figure out what order to render the grid elements so that
	// the ones in back are rendered first.
	if (viewAngleCos < 0) {
	    ystart = sc;
	    yend = 0;
	    ydir = -1;
	} else {
	    ystart = 0;
	    yend = sc;
	    ydir = 1;
	}
	if (viewAngleSin < 0) {
	    xstart = 0;
	    xend = sc;
	    xdir = 1;
	} else {
	    xstart = sc;
	    xend = 0;
	    xdir = -1;
	}
	boolean xFirst = (-viewAngleSin * xdir > viewAngleCos * ydir);

	shadowBufferBottom = new int[winSize.width];
	shadowBufferTop = new int[winSize.width];
	shadowBufferBottom2 = new int[winSize.width];
	shadowBufferTop2 = new int[winSize.width];
	for (x = 0; x != winSize.width; x++) {
	    shadowBufferBottom[x] = shadowBufferBottom2[x] = 0;
	    shadowBufferTop[x] = shadowBufferTop2[x] = winSize.height-1;
	}
	for (x = 0; x != winSize.width*winSize.height; x++)
	    pixels[x] = 0xFF000000;

	int goffx = (xdir == 1) ? 0 : -1;
	int goffy = (ydir == 1) ? 0 : -1;

	//System.out.print("drawing 1\n");
	for (x = xstart; x != xend; x += xdir) {
	    for (y = ystart; y != yend; y += ydir) {
		if (!xFirst)
		    x = xstart;
		for (; x != xend; x += xdir) {
		    double nx  = x*(2.0/gridsize)-1;
		    double ny  = y*(2.0/gridsize)-1;
		    double nx1 = (x+xdir)*(2.0/gridsize)-1;
		    double ny1 = (y+ydir)*(2.0/gridsize)-1;
		    map3d(nx,  ny,  grid[x]  [y].height, xpoints, ypoints, 0);
		    map3d(nx1, ny,  grid[x+xdir][y].height, xpoints, ypoints, 1);
		    map3d(nx,  ny1, grid[x][y+ydir].height, xpoints, ypoints, 2);
		    map3d(nx1, ny1, grid[x+xdir][y+ydir].height, xpoints, ypoints, 3);
		    GridElement ge = grid[x+goffx][y+goffy];
		    int col = computeColor(ge, ge.normdot);
		    // if (((x&1)^(y&1)) == 0) col ^= 0xff;
		    fillTriangle(xpoints[0], ypoints[0],
				 xpoints[1], ypoints[1],
				 xpoints[3], ypoints[3], col);
		    fillTriangle(xpoints[0], ypoints[0],
				 xpoints[2], ypoints[2],
				 xpoints[3], ypoints[3], col);

		    int cx = (xpoints[0]+xpoints[3])/2;
		    int cy = (ypoints[0]+ypoints[3])/2;
		    boolean vis = false;
		    if (cx >= 0 && cx < winSize.width &&
			cy <= shadowBufferTop[cx] && cy >= 0)
			vis = true;
		    ge.visible = vis;

		    if (xFirst)
			break;
		}
		if (!xFirst) {
		    int i;
		    for (i = 0; i != winSize.width; i++) {
			shadowBufferTop[i] = shadowBufferTop2[i];
			shadowBufferBottom[i] = shadowBufferBottom2[i];
		    }
		}
	    }
	    if (!xFirst)
		break;
	    int i;
	    for (i = 0; i != winSize.width; i++) {
		shadowBufferTop[i] = shadowBufferTop2[i];
		shadowBufferBottom[i] = shadowBufferBottom2[i];
	    }
	}

	//System.out.print("drawing 2\n");
	drawFloor();
	//System.out.print("drawing 3\n");

	functionChanged = backgroundChanged = false;
	if (imageSource != null)
	    imageSource.newPixels();
    }

    void drawFloor() {
	int x, y;
	switch (floorLineChooser.getSelectedIndex()) {
	case FL_NONE:
	    break;
	case FL_GRID:
	    for (x = 0; x != gridsize; x++)
		for (y = 0; y != gridsize; y += 10) {
		    double nx  = x*(2.0/gridsize)-1;
		    double nx1 = (x+1)*(2.0/gridsize)-1;
		    double ny  = y*(2.0/gridsize)-1;
		    if (grid[x][y].visible) {
			map3d(nx,  ny, grid[x]  [y].height,
			      xpoints, ypoints, 0);
			map3d(nx1, ny, grid[x+1][y].height,
			      xpoints, ypoints, 1);
			drawLine(xpoints[0], ypoints[0],
				 xpoints[1], ypoints[1]);
		    }
		    if (grid[y][x].visible) {
			map3d(ny, nx,  grid[y][x].height,
			      xpoints, ypoints, 0);
			map3d(ny, nx1, grid[y][x+1].height,
			      xpoints, ypoints, 1);
			drawLine(xpoints[0], ypoints[0],
				 xpoints[1], ypoints[1]);
		    }
		}
	    break;
	case FL_EQUIP:
	    if (!curfunc.nonGradient())
		renderEquips(); 
	    break;
	case FL_LINES:
	   genLines();
	   break;
	}
    }

    void fillTriangle(int x1, int y1, int x2, int y2, int x3, int y3,
		      int col) {
	if (x1 > x2) {
	    if (x2 > x3) {
		// x1 > x2 > x3
		int ay = interp(x1, y1, x3, y3, x2);
		fillTriangle1(x3, y3, x2, y2, ay, col);
		fillTriangle1(x1, y1, x2, y2, ay, col);
	    } else if (x1 > x3) {
		// x1 > x3 > x2
		int ay = interp(x1, y1, x2, y2, x3);
		fillTriangle1(x2, y2, x3, y3, ay, col);
		fillTriangle1(x1, y1, x3, y3, ay, col);
	    } else {
		// x3 > x1 > x2
		int ay = interp(x3, y3, x2, y2, x1);
		fillTriangle1(x2, y2, x1, y1, ay, col);
		fillTriangle1(x3, y3, x1, y1, ay, col);
	    }
	} else {
	    if (x1 > x3) {
		// x2 > x1 > x3
		int ay = interp(x2, y2, x3, y3, x1);
		fillTriangle1(x3, y3, x1, y1, ay, col);
		fillTriangle1(x2, y2, x1, y1, ay, col);
	    } else if (x2 > x3) {
		// x2 > x3 > x1
		int ay = interp(x2, y2, x1, y1, x3);
		fillTriangle1(x1, y1, x3, y3, ay, col);
		fillTriangle1(x2, y2, x3, y3, ay, col);
	    } else {
		// x3 > x2 > x1
		int ay = interp(x3, y3, x1, y1, x2);
		fillTriangle1(x1, y1, x2, y2, ay, col);
		fillTriangle1(x3, y3, x2, y2, ay, col);
	    }
	}
    }

    int interp(int x1, int y1, int x2, int y2, int x) {
	if (x1 == x2)
	    return y1;
	if (x < x1 && x < x2 || x > x1 && x > x2)
	    System.out.print("interp out of bounds\n");
	return (int) (y1+((double) x-x1)*(y2-y1)/(x2-x1));
    }

    void fillTriangle1(int x1, int y1, int x2, int y2, int y3, int col) {
	// x2 == x3
	int dir = (x1 > x2) ? -1 : 1;
	int x = x1;
	if (x < 0) {
	    x = 0;
	    if (x2 < 0)
		return;
	}
	if (x >= winSize.width) {
	    x = winSize.width-1;
	    if (x2 >= winSize.width)
		return;
	}
	if (y2 > y3) {
	    int q = y2;
	    y2 = y3; y3 = q;
	}
	// y2 < y3
	while (x != x2+dir) {
	    // XXX this could be speeded up
	    int ya = interp(x1, y1, x2, y2, x);
	    int yb = interp(x1, y1, x2, y3, x);
	    if (ya < 0)
		ya = 0;
	    if (yb >= winSize.height)
		yb = winSize.height-1;

	    if (shadowBufferTop2[x] > ya)
		shadowBufferTop2[x] = ya;
	    if (shadowBufferBottom2[x] < yb)
		shadowBufferBottom2[x] = yb;

	    int sb1 = shadowBufferTop[x];
	    int sb2 = shadowBufferBottom[x];
	    if (!(ya >= sb1 && yb <= sb2)) {
		for (; ya <= yb; ya++) {
		    if (ya < sb1 || ya > sb2)
			pixels[x+ya*winSize.width] = col;
		}
	    }
	    x += dir;
	    if (x < 0 || x >= winSize.width)
		return;
	}
    }

    void fillRectangle(int x1, int y1, int x2, int y2, int col) {
	int x, y;
	for (y = y1; y < y2; y++)
	    for (x = x1; x < x2; x++)
		pixels[x+y*winSize.width] = col;
    }

    void drawLine(int x1, int y1, int x2, int y2) {
	if (x1 == x2 && y1 == y2)
	    return;
	if (abs(y2-y1) > abs(x2-x1)) {
	    // y difference is greater, so we step along y's
	    // from min to max y and calculate x for each step
	    int sgn = sign(y2-y1);
	    int x, y;
	    for (y = y1; y != y2+sgn; y += sgn) {
		x = x1+(x2-x1)*(y-y1)/(y2-y1);
		if (x >= 0 && y >= 0 &&
		    x < winSize.width && y < winSize.height)
		    pixels[x+y*winSize.width] = 0xFFC0C0C0;
	    }
	} else {
	    // x difference is greater, so we step along x's
	    // from min to max x and calculate y for each step
	    int sgn = sign(x2-x1);
	    int x, y;
	    for (x = x1; x != x2+sgn; x += sgn) {
		y = y1+(y2-y1)*(x-x1)/(x2-x1);
		if (x >= 0 && y >= 0 &&
		    x < winSize.width && y < winSize.height)
		    pixels[x+y*winSize.width] = 0xFFC0C0C0;
	    }
	}
    }

    int abs(int x) { return x < 0 ? -x : x; }
    int sign(int x) { return (x < 0) ? -1 : (x == 0) ? 0 : 1; }
    int min(int a, int b) { return (a < b) ? a : b; }
    int max(int a, int b) { return (a > b) ? a : b; }
    double min(double a, double b) { return (a < b) ? a : b; }
    double max(double a, double b) { return (a > b) ? a : b; }

    // render equipotentials
    void renderEquips() {
	int x, y;
	for (x = 0; x != gridsize; x++)
	    for (y = 0; y != gridsize; y++) {
		if (!grid[x][y].visible)
		    continue;
		// try all possible edge combinations
		tryEdge(x, y,  x+1, y,  x, y+1,  x+1, y+1);
		tryEdge(x, y,  x+1, y,  x, y,    x, y+1);
		tryEdge(x, y,  x+1, y,  x+1, y,  x+1, y+1);
		tryEdge(x, y,  x, y+1,  x+1, y,  x+1, y+1);
		tryEdge(x, y,  x, y+1,  x, y+1,  x+1, y+1);
		tryEdge(x+1, y, x+1, y+1, x, y+1, x+1, y+1);
	    }
    }

    class FloatPair { public double x, y; }

    // interpolate between two points
    void interpPoint(GridElement ep1, GridElement ep2,
		     int x1, int y1, int x2, int y2, double pval,
		     FloatPair pos) {
	double interp2 = (pval-ep1.height)/(ep2.height-ep1.height);
	double interp1 = 1-interp2;
	pos.x = (x1*interp1 + x2*interp2) * 2./gridsize - 1;
	pos.y = (y1*interp1 + y2*interp2) * 2./gridsize - 1;
    }

    // check to see if pval is between the potential at ep1 and ep2
    boolean spanning(GridElement ep1, GridElement ep2, double pval) {
	if (ep1.height == ep2.height)
	    return false;
	return !((ep1.height < pval && ep2.height < pval) ||
		 (ep1.height > pval && ep2.height > pval));
    }

    // try to draw any equipotentials between edge (x1,y1)-(x2,y2)
    // and edge (x3,y3)-(x4,y4).
    void tryEdge(int x1, int y1, int x2, int y2,
		 int x3, int y3, int x4, int y4) {
	int i;
	double emult = 5;
	double mult = 1/(40 * emult * .1);
	GridElement ep1 = grid[x1][y1];
	GridElement ep2 = grid[x2][y2];
	GridElement ep3 = grid[x3][y3];
	GridElement ep4 = grid[x4][y4];
	double pmin = min(min(ep1.height, ep2.height), min(ep3.height, ep4.height));
	double pmax = max(max(ep1.height, ep2.height), max(ep3.height, ep4.height));
	if (pmin < -5) // was 10
	    pmin = -5;
	if (pmax > 5)
	    pmax = 5;
	int imin = (int) (pmin/mult);
	int imax = (int) (pmax/mult);
	for (i = imin; i <= imax; i++) {
	    double pval = i*mult;
	    if (!(spanning(ep1, ep2, pval) && spanning(ep3, ep4, pval)))
		continue;
	    FloatPair pa = new FloatPair();
	    FloatPair pb = new FloatPair();
	    interpPoint(ep1, ep2, x1, y1, x2, y2, pval, pa);
	    interpPoint(ep3, ep4, x3, y3, x4, y4, pval, pb);
	    map3d(pa.x, pa.y, pval, xpoints, ypoints, 0);
	    map3d(pb.x, pb.y, pval, xpoints, ypoints, 1);
	    drawLine(xpoints[0], ypoints[0], xpoints[1], ypoints[1]);
	}
    }

    void drawLineBackground(Graphics g) {
	int x, y;
	for (x = 0; x != gridsize; x++)
	    for (y = 0; y != gridsize; y++)
		grid[x][y].visible = true;
	if (isFlat)
	    return;
	for (y = gridsize-1; y >= 0; y--) {
	    for (x = 0; x < gridsize; x += 5) {
		double ny  = y*(2.0/gridsize)-1;
		double nx1 = (x+1)*(2.0/gridsize)-1;
		double ny1 = (y+1)*(2.0/gridsize)-1;
		map3d(nx1, ny,  grid[x][y].height, xpoints, ypoints, 1);
		map3d(nx1, ny1, grid[x][y+1].height, xpoints, ypoints, 2);
		ypoints[1] = bound_y(ypoints[1]);
		ypoints[2] = bound_y(ypoints[2]);
		g.drawLine(xpoints[1], ypoints[1], xpoints[2], ypoints[2]);
	    }
	}
	for (y = 0; y < gridsize; y += 5) {
	    for (x = gridsize-1; x >= 0; x--) {
		double nx  = x*(2.0/gridsize)-1;
		double nx1 = (x+1)*(2.0/gridsize)-1;
		double ny1 = (y+1)*(2.0/gridsize)-1;
		map3d(nx,  ny1, grid[x]  [y].height, xpoints, ypoints, 3);
		map3d(nx1, ny1, grid[x+1][y].height, xpoints, ypoints, 2);
		ypoints[3] = bound_y(ypoints[3]);
		ypoints[2] = bound_y(ypoints[2]);
		g.drawLine(xpoints[3], ypoints[3], xpoints[2], ypoints[2]);
	    }
	}
    }

    int bound_y(int y) {
	if (y < -100)
	    y = -100;
	if (y > winSize.height+100)
	    y = winSize.height+100;
	return y;
    }

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

    static final double root2 = 1.4142135623730950488016887242096981;
    double scalex, scaley;

    void map3d(double x, double y, double z, int xpoints[], int ypoints[],
	       int pt) {
	map3d(x, y, z, xpoints, ypoints, pt, viewMain);
    }

    void map3d(double x, double y, double z, int xpoints[], int ypoints[],
	       int pt, Rectangle view) {
	if (isFlat) {
	    xpoints[pt] = view.x+(int) ((x+1)*view.width/2);
	    ypoints[pt] = view.y+(int) ((1-y)*view.height/2);
	    return;
	}
	if (z < -1000)
	    z = -1000;
	if (z > 1000)
	    z = 1000;
	/*if (!(z > -2000 && z < 2000))
	  System.out.print(z + " in map3d\n");*/
	double realx = x*viewAngleCos + y*viewAngleSin; // range: [-10,10]
	double realy = z-viewHeight;
	double realz = y*viewAngleCos - x*viewAngleSin + viewDistance;
	scalex = viewZoom * (view.width/4) * viewDistance;
	scaley = scalex;
	int yoff = (int)
	    (scaley*(viewHeight-curfunc.getLevelHeight())/viewDistance);
	xpoints[pt] = view.x+view.width/2 + (int) (scalex*realx/realz);
	ypoints[pt] = view.y+view.height/2 - yoff - (int) (scaley*realy/realz);
    }

    void scaleworld() {
	// scalex = viewZoom * (winSize.width/4) * viewDistance;
	// scaley = scalex;
    }

    double getHeight(double x, double y) {
	x = (x+1)*(gridsize/2);
	y = (y+1)*(gridsize/2);
	int ix = (int) x;
	int iy = (int) y;
	if (ix >= gridsize || iy >= gridsize)
	    return grid[ix][iy].height;
	double fracx = x-ix;
	double fracy = y-iy;
	return grid[ix][iy].height * (1-fracx) * (1-fracy) +
	    grid[ix+1][iy].height * fracx * (1-fracy) +
	    grid[ix][iy+1].height * (1-fracx) * fracy +
	    grid[ix+1][iy+1].height * fracx * fracy;
    }

    void sayCalculating(Graphics realg) {
	realg.setColor(cv.getBackground());
	FontMetrics fm = realg.getFontMetrics();
	String s = "Calculating...";
	realg.fillRect(0, winSize.height-30,
		       20+fm.stringWidth(s), 30);
	realg.setColor(Color.white);
	realg.drawString(s, 10, winSize.height-10);
    }

    long lastTime;
    double timeStep, partMult;
    boolean slowDragView = true;
    
    public void updateVecDemo(Graphics realg) {
	Graphics g = dbimage.getGraphics();
	if (winSize == null || winSize.width == 0)
	    return;
	if (xpoints == null)
	    return;
	checkFlatState();
	barFieldStrength = fieldStrength =
	    java.lang.Math.exp((strengthBar.getValue()-50)/10.);
	if (functionChanged || backgroundChanged) {
	    if (functionChanged) { // && !(draggingView && isFlat)) {
		sayCalculating(realg);
		generateFunction();
	    }
	    if (!slowDragView || !draggingView) {
		long tm1 = System.currentTimeMillis();
		sayCalculating(realg);
		drawBackground();
		long tm2 = System.currentTimeMillis();
		slowDragView = (tm2-tm1 > 40);
	    }
	}

	scaleworld();
	if ((draggingView && slowDragView) || functionChanged) {
	    g.setColor(isFlat ? fieldColors[0] : cv.getBackground());
	    g.fillRect(0, 0, winSize.width, winSize.height);
	    g.setColor(cv.getForeground());
	    drawLineBackground(g);
	} else
	    g.drawImage(backimage, 0, 0, this);

	boolean allquiet = true;

	curfunc.setupFrame();
	fieldStrength = barFieldStrength;
	partMult = fieldStrength * reverse * timeStep;
	int disp = dispChooser.getSelectedIndex();
	timeStep = 1;
	if (!stoppedCheck.getState()) {
	    if (lastTime > 0)
		timeStep = (System.currentTimeMillis()-lastTime)*.03;
	    if (timeStep > 3)
		timeStep = 3;
	    lastTime = System.currentTimeMillis();
	    if (disp != DISP_VECTORS && disp != DISP_NONE) {
		moveParticles();
		allquiet = false;
	    }
	    currentStep += reverse;
	    if (currentStep < 0)
		currentStep += 800;
	} else
	    lastTime = 0;

	if (disp == DISP_VECTORS)
	    drawVectors(g);
	else if (disp != DISP_NONE)
	    drawParticles(g);

	g.setColor(Color.gray);

	if (!isFlat)
	    drawAxes(g);
	curfunc.finishFrame();

	int mode = modeChooser.getSelectedIndex();
	if (mode == MODE_LINE_INT)
	    lineIntegral(g, true);
	else if (mode == MODE_SURF_INT)
	    lineIntegral(g, false);

	if (parseError)
	    centerString(g, "Can't parse expression", winSize.height-20);

	//g.drawString("FR: " + framerate, 0, 10);
	realg.drawImage(dbimage, 0, 0, this);
	long t = System.currentTimeMillis();
	frames++;
	if (firsttime == 0)
	    firsttime = t;
	else if (t-firsttime > 1000) {
	    framerate = frames;
	    firsttime = t;
	    frames = 0;
	}
	if (!stoppedCheck.getState() && !allquiet)
	    cv.repaint(pause);
    }

    void drawAxes(Graphics g) {
	g.setColor(Color.white);
	map3d(0, 0, 0, xpoints, ypoints, 0, viewAxes);
	map3d(1, 0, 0, xpoints, ypoints, 1, viewAxes);
	drawArrow(g, "x", xpoints[0], ypoints[0], xpoints[1], ypoints[1]);
	map3d(0, 1, 0, xpoints, ypoints, 1, viewAxes);
	drawArrow(g, "y", xpoints[0], ypoints[0], xpoints[1], ypoints[1]);
	map3d(0, 0, 1, xpoints, ypoints, 1, viewAxes);
	drawArrow(g, "z", xpoints[0], ypoints[0], xpoints[1], ypoints[1]);
    }

    void drawVectors(Graphics g) {
	int x, y, z;
	DrawData dd = new DrawData();
	dd.mult = barFieldStrength*40;
	dd.g = g;
	dd.field = new double[3];
	dd.vv = new double[3];
	vectorSpacing = vecDensityBar.getValue();
	double vec[] = new double[3];

	vecCount = 0;

	for (x = 0; x != vectorSpacing; x++) {
	    vec[0] = x*(2.0/(vectorSpacing-1))-1;
	    for (y = 0; y != vectorSpacing; y++) {
		vec[1] = y*(2.0/(vectorSpacing-1))-1;
		drawVector(dd, vec);
	    }
	}
    }

    void lineIntegral(Graphics g, boolean line) {
	if (integralX == -1)
	    return;
	if (dragStartX == integralX || dragStartY == integralY)
	    return;
	int x1 = min(dragStartX, integralX);
	int y1 = min(dragStartY, integralY);
	int x2 = max(dragStartX, integralX);
	int y2 = max(dragStartY, integralY);
	int step = 15;
	int x;
	double pos[] = rk_k2;
	if (!line) {
	    g.setColor(Color.white);
	    g.drawRect(x1, y1, x2-x1+1, y2-y1+1);
	}
	double y1p = 1 - 2.*y1/winSize.height;
	double y2p = 1 - 2.*y2/winSize.height;
	for (x = x1; x <= x2; x += step) {
	    int step1 = x2-x;
	    if (step1 > step)
		step1 = step;
	    pos[0] = 2.*x/winSize.width - 1;
	    pos[1] = y1p;
	    lineIntegralStep(g, x, y1, pos, step1, 0, line);
	    pos[1] = y2p;
	    lineIntegralStep(g, x+step1, y2, pos, -step1, 0, line);
	}
	int y;
	double x1p = 2.*x1/winSize.width - 1;
	double x2p = 2.*x2/winSize.width - 1;
	for (y = y2; y >= y1; y -= step) {
	    int step1 = y-y1;
	    if (step1 > step)
		step1 = step;
	    pos[0] = x1p;
	    pos[1] = 1 - 2.*y/winSize.height;
	    lineIntegralStep(g, x1, y, pos, 0, step1, line);
	    pos[0] = x2p;
	    lineIntegralStep(g, x2, y-step1, pos, 0, -step1, line);
	}
	boundCheck = false;
	pos[1] = y1p;
	double iv1 = numIntegrate(pos, 0, x1p, x2p, line);
	pos[1] = y2p;
	double iv2 = numIntegrate(pos, 0, x1p, x2p, line);
	pos[0] = x1p;
	double iv3 = numIntegrate(pos, 1, y1p, y2p, line);
	pos[0] = x2p;
	double iv4 = numIntegrate(pos, 1, y1p, y2p, line);
	double ivtot = -iv1+iv2+iv3-iv4;
	NumberFormat nf = NumberFormat.getInstance();
	nf.setMaximumFractionDigits(3);
	if (ivtot < 1e-7 && ivtot > -1e-7)
	    ivtot = 0;
	ivtot *= reverse;
	String s = ((!line) ? "Flux = " : "Circulation = ");
	s += nf.format(ivtot*1e5);
	g.setColor(cv.getBackground());
	FontMetrics fm = g.getFontMetrics();
	g.fillRect(0, winSize.height-30, 20+fm.stringWidth(s), 30);
	g.setColor(Color.white);
	g.drawString(s, 10, winSize.height-10);
    }

    double numIntegrate(double pos[], int n1,
			double x1, double x2, boolean line) {
	int steps = 8;
	double lastres = 0;
	double res = 0;
	int n2 = (line) ? n1 : 1-n1;
	while (true) {
	    int i;
	    double h = (x2-x1)/steps;
	    res = 0;
	    for (i = 0; i <= steps; i++) {
		pos[n1] = x1+i*h;
		double field[] = rk_k1;
		curfunc.getField(field, pos);
		int ss = (i == 0 || i == steps) ? 1 :
		    ((i & 1) == 1) ? 4 : 2;
		res += field[n2]*h*ss;
	    }
	    res /= 3;
	    if (java.lang.Math.abs(lastres-res) < 1e-7)
		break;
	    lastres = res;
	    steps *= 2;
	    if (steps == 65536)
		break;
	}
	if (!line && n1 == 0)
	    res = -res;
	return res;
    }

    void lineIntegralStep(Graphics g, int x, int y,
			  double pos[], int dx, int dy, boolean line) {
	double field[] = rk_k1;
	curfunc.getField(field, pos);
	double f = (line) ? field[0]*dx+field[1]*dy :
	    field[0]*dy - field[1]*dx;
	f *= reverse;
	double dn = java.lang.Math.abs(f*100);
	if (dn > 1)
	    dn = 1;
	int col1 = (int) (dn*128+127);
	int col2 = (int) (127-dn*127);
	if (!line) {
	    x += dx/2;
	    y -= dy/2;
	}
	if (f == 0) {
	    g.setColor(new Color(col2, col2, col2));
	    g.drawLine(x, y, x+dx, y-dy);
	} else if (f > 0) {
	    g.setColor(new Color(col1, col2, col2));
	    if (line)
		drawArrow(g, null, x, y, x+dx, y-dy);
	    else
		drawArrow(g, null, x, y, x+dy, y+dx);
	} else {
	    g.setColor(new Color(col2, col1, col2));
	    if (line)
		drawArrow(g, null, x+dx, y-dy, x, y);
	    else
		drawArrow(g, null, x, y, x-dy, y-dx);
	}
    }

    void genLines() {
	int i;
	
	int lineGridSize = 8; // lineDensityBar.getValue();
	if (lineGridSize < 3)
	    lineGridSize = 3;
	if (lineGridSize > 8)
	    lineGridSize = 8;
	lineGridSize *= 2;
	int ct = 30*lineGridSize*lineGridSize;
	double brightmult = 80*barFieldStrength;
	fieldStrength = 10;

	boolean lineGrid[][] =
	    new boolean[lineGridSize][lineGridSize];
	double lineGridMult = lineGridSize/2.;

	double origp[] = new double[3];
	double field[] = new double[3];
	Particle p = new Particle();
	p.lifetime = -1;
	p.stepsize = 10;
	int dir = -1;
	int segs = 0;
	double lastdist = 0;
	for (i = 0; i != ct; i++) {
	    if (p.lifetime < 0) {
		p.lifetime = 1;
		p.stepsize = 10;
		segs = 0;
		lastdist = 0;
		if (dir == 1) {
		    int j;
		    for (j = 0; j != 3; j++)
			p.pos[j] = origp[j];
		    dir = -1;
		    continue;
		}
		dir = 1;
		int px = 0, py = 0;
		while (true) {
		    if (!lineGrid[px][py])
			break;
		    if (++px < lineGridSize)
			continue;
		    px = 0;
		    if (++py < lineGridSize)
			continue;
		    break;
		}
		if (py == lineGridSize)
		    break;
		lineGrid[px][py] = true;
		double offs = .5/lineGridMult;
		origp[0] = p.pos[0] = px/lineGridMult-1+offs;
		origp[1] = p.pos[1] = py/lineGridMult-1+offs;
	    }

	    double p1x = p.pos[0];
	    double p1y = p.pos[1];
	    double p1z = getHeight(p1x, p1y);
	    GridElement ge = grid[(int) ((p1x+1)*gridsize/2)]
		[(int) ((p1y+1)*gridsize/2)];
	    if (!ge.visible) {
		p.lifetime = -1;
		continue;
	    }
	    
	    double x[] = p.pos;
	    lineSegment(p, dir);
	    //System.out.print(x[0] + " " + x[1] + " " + x[2] + "\n");
	    if (p.lifetime < 0)
		continue;
	    int gx = (int) ((x[0]+1)*lineGridMult);
	    int gy = (int) ((x[1]+1)*lineGridMult);
	    if (!lineGrid[gx][gy])
		segs--;
	    lineGrid[gx][gy] = true;

	    ge = grid[(int) ((p.pos[0]+1)*gridsize/2)]
		[(int) ((p.pos[1]+1)*gridsize/2)];
	    if (!ge.visible) {
		p.lifetime = -1;
		continue;
	    }
	    
	    double dn = brightmult*p.phi;
	    if (dn > 2)
		dn = 2;
	    // fv.col = (int) (dn*255);
	    map3d(p1x, p1y, p1z, xpoints, ypoints, 0);
	    map3d(p.pos[0], p.pos[1], getHeight(p.pos[0], p.pos[1]),
		  xpoints, ypoints, 1);
	    drawLine(xpoints[0], ypoints[0], xpoints[1], ypoints[1]);

	    double d2 = dist2(origp, x);
	    if (d2 > lastdist)
		lastdist = d2;
	    else
		segs++;
	    if (segs > 10 || d2 < .001)
		p.lifetime = -1;
	}
	//System.out.print("vc " + vecCount + " " + ct + "\n");
    }

    // draw the appropriate field vector at xx,yy,zz
    void drawVector(DrawData dd, double vec[]) {
	double field[] = dd.field;

	// calculate field vector
	curfunc.getField(field, vec);
	double dn = java.lang.Math.sqrt(field[0]*field[0]+field[1]*field[1]);
	double dnr = dn*reverse;
	if (dn > 0) {
	    field[0] /= dnr;
	    field[1] /= dnr;
	}
	dn *= dd.mult;
	if (dn > 2)
	    dn = 2;
	int col = (int) (dn*255);
	double sw2 = 1./(vectorSpacing-1);
	map3d(vec[0], vec[1], 0, xpoints, ypoints, 0);
	map3d(vec[0]+sw2*field[0], vec[1]+sw2*field[1], 0,
	      xpoints, ypoints, 1);
	dd.g.setColor(fieldColors[col]);
	drawArrow(dd.g, null,
		  xpoints[0], ypoints[0], xpoints[1], ypoints[1], 2);
    }

    void moveParticles() {
	int bestd = 0;
	int i;
	int pcount = getParticleCount();
	for (i = 0; i != pcount; i++) {
	    Particle pt = particles[i];
	    removeFromDensityGroup(pt);
	    moveParticle(pt);
	    double x[] = pt.pos;
	    if (!(x[0] >= -1 && x[0] < 1 &&
		  x[1] >= -1 && x[1] < 1) ||
		(pt.lifetime -= timeStep) < 0)
		positionParticle(pt);
	    int d = addToDensityGroup(pt);
	    if (d > bestd)
		bestd = d;
	}
	boolean withforce =
	    (dispChooser.getSelectedIndex() == DISP_PART_FORCE);
	int maxd =  (6*getParticleCount()/(densitygridsize*densitygridsize));
	if (!withforce && curfunc.redistribute() && bestd > maxd)
	    redistribute(bestd);
    }

    static int frames = 0;
    static int framerate = 0;
    static long firsttime = 0;

    void drawParticles(Graphics g) {
	g.setColor(Color.white);
	int disp = dispChooser.getSelectedIndex();
	if (disp == DISP_VECTORS) {
	    int i;
	    for (i = 0; i != vecCount; i++) {
		FieldVector fv = vectors[i];
		g.setColor(fieldColors[fv.col]);
		drawArrow(g, null, fv.sx1, fv.sy1, fv.sx2, fv.sy2, 2);
	    }
	    return;
	}
	int pcount = getParticleCount();
	int i;
	wooft += .3;
	if (disp == DISP_CURLERS)
	    pcount = (pcount+4)/5;
	for (i = 0; i < pcount; i++) {
	    Particle p = particles[i];
	    double pos[] = p.pos;
	    GridElement ge = grid[(int) ((pos[0]+1)*gridsize/2)]
		[(int) ((pos[1]+1)*gridsize/2)];
	    map3d(pos[0], pos[1], getHeight(pos[0], pos[1]),
					    xpoints, ypoints, 0);
	    if (xpoints[0] < 0 || xpoints[0] >= winSize.width ||
		ypoints[0] < 0 || ypoints[0] >= winSize.height)
		continue;
	    if (disp == DISP_CURLERS) {
		g.setColor(p.color);
		final double len = .02;
		double ax = java.lang.Math.cos(p.theta)*len;
		double ay = java.lang.Math.sin(p.theta)*len;
		double offx = ax;
		double offy = ay;
		double a1 = curlcalc(p.pos[0]+offx, p.pos[1]+offy, -ay, ax);
		double a2 = curlcalc(p.pos[0]-offy, p.pos[1]+offx, -ax, -ay);
		double a3 = curlcalc(p.pos[0]-offx, p.pos[1]-offy, ay, -ax);
		double a4 = curlcalc(p.pos[0]+offy, p.pos[1]-offx, ax, ay);
		p.theta += (a1+a2+a3+a4)/(4*len*len);
		map3d(p.pos[0]-offx, p.pos[1]-offy, 0, xpoints, ypoints, 0);
		map3d(p.pos[0]+offx, p.pos[1]+offy, 0, xpoints, ypoints, 1);
		map3d(p.pos[0]-offy, p.pos[1]+offx, 0, xpoints, ypoints, 2);
		map3d(p.pos[0]+offy, p.pos[1]-offx, 0, xpoints, ypoints, 3);
		g.drawLine(xpoints[0], ypoints[0], xpoints[1], ypoints[1]);
		g.drawLine(xpoints[2], ypoints[2], xpoints[3], ypoints[3]);
		g.fillOval(xpoints[0]-1, ypoints[0]-1, 3, 3);
	    } else if (ge.visible && ge.valid)
		g.fillRect(xpoints[0], ypoints[0]-1, 2, 2);
	}
    }

	double wooft = 0;

    void drawPlane(Graphics g, double sizex, double sizey, double z) {
	g.setColor(darkYellow);
	map3d(-sizex, -sizey, z, xpoints, ypoints, 0);
	map3d(+sizex, -sizey, z, xpoints, ypoints, 1);
	map3d(+sizex, +sizey, z, xpoints, ypoints, 2);
	map3d(-sizex, +sizey, z, xpoints, ypoints, 3);
	g.fillPolygon(xpoints, ypoints, 4);
    }

    void drawArrow(Graphics g, String text, int x1, int y1, int x2, int y2) {
	drawArrow(g, text, x1, y1, x2, y2, 5);
    }

    void drawArrow(Graphics g, String text,
		   int x1, int y1, int x2, int y2, int as) {
	g.drawLine(x1, y1, x2, y2);
	double l = java.lang.Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1));
	if (l > as/2) {  // was as
	    double hatx = (x2-x1)/l;
	    double haty = (y2-y1)/l;
	    g.drawLine(x2, y2,
		       (int) (haty*as-hatx*as+x2),
		       (int) (-hatx*as-haty*as+y2));
	    g.drawLine(x2, y2,
		       (int) (-haty*as-hatx*as+x2),
		       (int) (hatx*as-haty*as+y2));
	    if (text != null)
		g.drawString(text, (int) (x2+hatx*10), (int) (y2+haty*10));
	}
    }

    int rediscount;
    void redistribute(int mostd) {
	if (mostd < 5)
	    return;
	rediscount++;
	int maxd = (6*getParticleCount()/
		    (densitygridsize*densitygridsize));
	int i;
	int pn = 0;
	int pcount = getParticleCount();
	for (i = rediscount % 4; i < pcount; i+=4) {
	    Particle p = particles[i];
	    int a = (int)((p.pos[0]+1)*(densitygridsize/2));
	    int b = (int)((p.pos[1]+1)*(densitygridsize/2));
	    if (density[a][b] <= maxd)
		continue;
	    p.lifetime = -1;
	    pn++;
	}
	//System.out.print("redist " + mostd + " " + pn + "\n");
    }

    double curlcalc(double x, double y, double ax, double ay) {
	rk_yn[0] = x;
	rk_yn[1] = y;
	curfunc.getField(rk_k1, rk_yn);
	return partMult*(rk_k1[0]*ax + rk_k1[1]*ay);
    }

    double distance(Particle p) {
	return distance(p.pos[0], p.pos[1]);
    }

    double distance(double y[]) {
	return distance(y[0], y[1]);
    }

    double distance(double x, double y) {
	return java.lang.Math.sqrt(x*x+y*y+.000000001);
    }

    void rotateParticleAdd(double result[], double y[], double mult,
			double cx, double cy) { 
	result[0] += -mult*(y[1]-cy);
	result[1] +=  mult*(y[0]-cx);
    }

    void rotateParticle(double result[], double y[], double mult) {
	result[0] = -mult*y[1];
	result[1] =  mult*y[0];
	result[2] = 0;
    }

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

    void editView(int x, int y) {
	if (modeChooser.getSelectedIndex() == MODE_VIEW_ROTATE) {
	    if (isFlat)
		return;
	    viewAngle = (dragStartX-x)/40. + viewAngleDragStart;
	    while (viewAngle < 0)
		viewAngle += 2*pi;
	    while (viewAngle >= 2*pi)
		viewAngle -= 2*pi;
	    viewAngleCos = java.lang.Math.cos(viewAngle);
	    viewAngleSin = java.lang.Math.sin(viewAngle);
	    viewHeight = -(dragStartY-y)/10. + viewHeightDragStart;
	    if (viewHeight > 9)
		viewHeight = 9;
	    if (viewHeight < -9)
		viewHeight = -9;
	    draggingView = backgroundChanged = true;
	    cv.repaint(pause);
	    return;
	}
	if (modeChooser.getSelectedIndex() == MODE_VIEW_ZOOM) {
	    if (isFlat)
		return;
	    viewZoom = (x-dragStartX)/40. + viewZoomDragStart;
	    if (viewZoom < .1)
		viewZoom = .1;
	    draggingView = backgroundChanged = true;
	    cv.repaint(pause);
	    return;
	}
	if (modeChooser.getSelectedIndex() == MODE_LINE_INT ||
	    modeChooser.getSelectedIndex() == MODE_SURF_INT) {
	    integralX = x;
	    integralY = y;
	    cv.repaint(pause);
	}
    }

    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) {
	vectors = null;
	if (e.getSource() == resetButton)
	    resetParticles();
	if (e.getSource() == kickButton)
	    kickParticles();
	if (e.getSource() == infoButton) {
	    String s = curfunc.getClass().getName();
	    try {
		s = s.substring(s.lastIndexOf('.')+1);
		applet.getAppletContext().showDocument(
			new URL(applet.getCodeBase(),
				"functions.html" + '#' + s),
			"functionHelp");
	    } catch (Exception ex) {
	    }
	}
	curfunc.actionPerformed();
	cv.repaint(pause);
    }

    public boolean handleEvent(Event ev) {
	if (ev.id == Event.WINDOW_DESTROY) {
	    applet.destroyFrame();
	    return true;
	}
	return super.handleEvent(ev);
    }
    
    public void scrollbarValueChanged(DecentScrollbar ds) {
	vectors = null;
	System.out.print(ds.getValue() + "\n");
	if (ds == partCountBar)
	    resetDensityGroups();
	if (ds == aux1Bar || ds == aux2Bar || ds == aux3Bar) {
	    functionChanged = true;
	    draggingView = true;
	}
	cv.repaint(pause);
    }

    public void scrollbarFinished(DecentScrollbar ds) {
	draggingView = false;
	cv.repaint(pause);
    }

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

    public void mouseClicked(MouseEvent e) {
    }
    public void mouseEntered(MouseEvent e) {
    }
    public void mouseExited(MouseEvent e) {
    }
    public void mousePressed(MouseEvent e) {
	if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) == 0)
	    return;
	dragStartX = e.getX();
	dragStartY = e.getY();
	viewAngleDragStart = viewAngle;
	viewHeightDragStart = viewHeight;
	viewZoomDragStart = viewZoom;
	dragging = true;
	edit(e);
    }
    public void mouseReleased(MouseEvent e) {
	if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) == 0)
	    return;
	dragging = draggingView = false;
	cv.repaint(pause);
    }

    void dispChooserChanged() {
	int disp = dispChooser.getSelectedIndex();
	showA = false; // XXX
	if (disp == DISP_PART_FORCE)
	    kickButton.enable();
	else
	    kickButton.disable();
	potentialLabel.hide();
	potentialBar.hide();
	vecDensityLabel.hide();
	vecDensityBar.hide();
	/*lineDensityLabel.hide();
	  lineDensityBar.hide();*/
	partCountLabel.hide();
	partCountBar.hide();
	strengthLabel.show();
	strengthBar.show();
	if (disp == DISP_VECTORS) {
	    vecDensityLabel.show();
	    vecDensityBar.show();
	} else {
	    partCountLabel.show();
	    partCountBar.show();
	}
	validate();
	resetParticles();
    }

    public void itemStateChanged(ItemEvent e) {
	vectors = null;
	cv.repaint(pause);
	reverse = (reverseCheck.getState()) ? -1 : 1;
	if (e.getItemSelectable() == dispChooser) {
	    dispChooserChanged();
	    resetParticles();
	}
	if (e.getItemSelectable() == functionChooser)
	    functionChanged();
	if (e.getItemSelectable() == reverseCheck)
	    functionChanged = true;
	if (e.getItemSelectable() == floorColorChooser ||
	    e.getItemSelectable() == floorLineChooser)
	    backgroundChanged = true;
    }

    void checkFlatState() {
	boolean oldFlat = isFlat;
	int disp = dispChooser.getSelectedIndex();
	isFlat = flatCheck.getState() || curfunc.nonGradient() ||
	    disp == DISP_VECTORS || disp == DISP_CURLERS;
	int mode = modeChooser.getSelectedIndex();
	if (mode == MODE_LINE_INT || mode == MODE_SURF_INT)
	    isFlat = true;
	if (isFlat != oldFlat)
	    backgroundChanged = true;
    }

    void functionChanged() {
	reverse = 1;
	reverseCheck.setState(false);
	parseError = false;
	curfunc = (VecFunction)
	    functionList.elementAt(functionChooser.getSelectedIndex());
	int i;
	for (i = 0; i != 3; i++) {
	    auxBars[i].label.hide();
	    auxBars[i].bar.hide();
	}
	for (i = 0; i != 2; i++)
	    textFields[i].hide();
	if (textFieldLabel != null)
	    textFieldLabel.hide();
	strengthBar.setValue(80);
	//setupDispChooser(!curfunc.nonGradient());
	curfunc.setup();
	validate();
	resetParticles();
	dispChooserChanged();
	functionChanged = true;
	integralX = -1;
    }

    void setupDispChooser(boolean potential) {
	dispChooser.removeAll();
	dispChooser.add("Display: Particles (Vel.)");
	dispChooser.add("Display: Particles (Force)");
	dispChooser.add("Display: Field Vectors");
	dispChooser.add("Display: None");
#ifdef BUILD_V
	dispChooser.add("Display: Curl Detectors");
#endif
    }

    void setupBar(int n, String text, int val) {
	auxBars[n].label.setText(text);
	auxBars[n].label.show();
	auxBars[n].bar.setValue(val);
	auxBars[n].bar.show();
    }

    void cross(double res[], double v1[], double v2[]) {
	res[0] = v1[1]*v2[2] - v1[2]*v2[1];
	res[1] = v1[2]*v2[0] - v1[0]*v2[2];
	res[2] = v1[0]*v2[1] - v1[1]*v2[0];
    }

    double dot(double v1[], double v2[]) {
	return v1[0]*v2[0] + v1[1]*v2[1];
    }

    boolean boundCheck;
    double oldY[];
    double rk_k1[] = new double[6];
    double rk_k2[] = new double[6];
    double rk_k3[] = new double[6];
    double rk_k4[] = new double[6];
    double rk_yn[] = new double[6];

    void rk(int order, double x, double Y[], double stepsize) {
	int i;

	// x is not used...

	if (order == 2) {
	    // velocity-based motion
	    double fmult = stepsize * partMult;
	    for (i = 0; i != order; i++)
		rk_yn[i] = Y[i];
	    curfunc.getField(rk_k1, rk_yn);
	    for (i = 0; i != order; i++)
		rk_yn[i] = (Y[i] + 0.5*fmult*rk_k1[i]);
	    curfunc.getField(rk_k2, rk_yn);
	    for (i = 0; i != order; i++)
		rk_yn[i] = (Y[i] + 0.5*fmult*rk_k2[i]);
	    curfunc.getField(rk_k3, rk_yn);
	    
	    for (i = 0; i != order; i++)
		rk_yn[i] = (Y[i] + fmult*rk_k3[i]);
	    curfunc.getField(rk_k4, rk_yn);
	    for (i = 0; i != order; i++)
		Y[i] = Y[i] + fmult*(rk_k1[i]+2*(rk_k2[i]+rk_k3[i])+rk_k4[i])/6;
	    Y[2] = rk_k4[2];
	} else {
	    // force-based (could share more code with above, but this is
	    // called a lot so we want it to be fast)
	    double fmult = stepsize * partMult;
	    for (i = 0; i != order; i++)
		rk_yn[i] = Y[i];
	    getForceField(rk_k1, rk_yn, stepsize, fmult);
	    for (i = 0; i != order; i++)
		rk_yn[i] = (Y[i] + 0.5*rk_k1[i]);
	    getForceField(rk_k2, rk_yn, stepsize, fmult);
	    for (i = 0; i != order; i++)
		rk_yn[i] = (Y[i] + 0.5*rk_k2[i]);
	    getForceField(rk_k3, rk_yn, stepsize, fmult);
	    
	    for (i = 0; i != order; i++)
		rk_yn[i] = (Y[i] + rk_k3[i]);
	    getForceField(rk_k4, rk_yn, stepsize, fmult);
	    for (i = 0; i != order; i++)
		Y[i] = Y[i] + (rk_k1[i]+2*(rk_k2[i]+rk_k3[i])+rk_k4[i])/6;
	    Y[4] = rk_k4[4];
	}
    }

    void getForceField(double result[], double y[],
		       double stepsize, double fmult) {
	// get the field vector for the current function
	curfunc.getField(result, y);

	// copy height information
	result[4] = result[2];

	// the field data has been written into result[0:1] where it will
	// directly influence the position (y[0:1]), but we want it to
	// influence the velocity (y[2:3]), so we move it.  In the
	// process, we multiply by fmult (to get the reverse button
	// and field strength slider to work).  It would be nice if we
	// could just say getField(result+2, y), but that's java for you.
	int i;
	for (i = 0; i != 2; i++)
	    result[i+2] = fmult*result[i]*.1;

	// here we fill in result[0:1] so that the particle position will
	// change according to the velocity.
	for (i = 0; i != 2; i++)
	    result[i] = stepsize*timeStep*rk_yn[i+2];
    }

    double rk_Y[] = new double[6];
    double rk_Yhalf[] = new double[6];
    double rk_oldY[] = new double[6];
    double ls_fieldavg[] = new double[3];

    void moveParticle(Particle p)
    {
	int disp = dispChooser.getSelectedIndex();
	int numIter=0;
	double maxh=1;
	double error=0.0, E = .001, localError;
	boolean useForce = (disp == DISP_PART_FORCE);
	int order = useForce ? 4 : 2;
	double Y[] = rk_Y;
	double Yhalf[] = rk_Yhalf;
	oldY = rk_oldY;
	int i;

	for (i = 0; i != 2; i++)
	    oldY[i] = Y[i] = Yhalf[i] = p.pos[i];
	if (useForce)
	    for (i = 0; i != 2; i++)
		Y[i+2] = Yhalf[i+2] = p.vel[i];
	double t = 0;

	if (!curfunc.useRungeKutta()) {
	    boundCheck = false;
	    curfunc.getField(Yhalf, Y);
	    if (boundCheck && (!useForce || curfunc.checkBoundsWithForce())) {
		p.pos[0] = -100;
		return;
	    }
	    double fmult = partMult;
	    if (useForce) {
		fmult *= .1;
		for (i = 0; i != 2; i++) {
		    p.vel[i] += fmult*Yhalf[i];
		    p.pos[i] += p.vel[i]*timeStep;
		}
	    } else {
		for (i = 0; i != 2; i++)
		    p.pos[i] += fmult*Yhalf[i];
	    }
	    p.pos[2] = Yhalf[2];
	    for (i = 0; i != 2; i++)
		Y[i] = p.pos[i];
	    if (curfunc.checkBounds(Y, oldY))
		p.pos[0] = -100;
	    return;
	}

	boolean adapt = curfunc.useAdaptiveRungeKutta();
	double h = (adapt) ? p.stepsize : 1;

	int steps = 0;
	double minh = .0001;
	while (t >= 0 && t < 1) {
	    if (t+h > 1)
		h = 1-t;

	    boundCheck = false;

	    // estimate one full step
	    rk(order, 0, Y, h);
	    
	    // bail out after one iteration for some slow fields
	    if (!adapt)
		break;

	    // estimate two half steps
	    rk(order, 0, Yhalf, h*0.5);
	    rk(order, 0, Yhalf, h*0.5);

	    if (boundCheck && (!useForce || curfunc.checkBoundsWithForce())) {
		p.pos[0] = -100;
		return;
	    }

	    // estimate the local error
	    localError = java.lang.Math.abs(Y[0] - Yhalf[0]) +
		java.lang.Math.abs(Y[1] - Yhalf[1]);
	    
	    if (localError > E && h > minh) {
		//System.out.print(E + " " + t + " " + localError + " " + h + "\n");
		h *= 0.75; // decrease the step size
		if (h < minh)
		    h = minh;
		for (i = 0; i != order; i++)
		    Y[i] = Yhalf[i] = oldY[i]; 
		continue;
	    } else if (localError < (E * 0.5)) {
		h *= 1.25; // increase the step size
		if (h > maxh)
		    h = maxh;
	    }
	    
	    for (i = 0; i != order; i++)
		oldY[i] = Yhalf[i] = Y[i]; 

	    t += h;
	    steps++;
	}
	
	if (boundCheck && (!useForce || curfunc.checkBoundsWithForce())) {
	    p.pos[0] = -100;
	    return;
	}
	
	p.stepsize = h;
	for (i = 0; i != 3; i++)
	    p.pos[i] = Y[i];
	if (useForce) {
	    for (i = 0; i != 2; i++)
		p.vel[i] = Y[i+2];
	    p.pos[2] = Y[4];
	}
    }

    double dist2(double a[], double b[]) {
	double c0 = a[0]-b[0];
	double c1 = a[1]-b[1];
	return c0*c0+c1*c1;
    }

    void lineSegment(Particle p, int dir)
    {
	int numIter=0;
	double maxh=20;
	double error=0.0, E = .001, localError;
	int order = 2;
	double Y[] = rk_Y;
	double Yhalf[] = rk_Yhalf;
	oldY = rk_oldY;
	int i;

	for (i = 0; i != 2; i++)
	    oldY[i] = Y[i] = Yhalf[i] = p.pos[i];
	double h = p.stepsize;
	ls_fieldavg[0] = ls_fieldavg[1] = ls_fieldavg[2] = 0;

	int steps = 0;
	double minh = .1;
	double segSize2max = 1./gridsize;
	double segSize2min = segSize2max/4;
	double lastd = 0;
	int avgct = 0;
	while (true) {
	    boundCheck = false;
	    steps++;
	    if (steps > 100) {
		System.out.print("maxsteps\n");
		p.lifetime = -1;
		return;
	    }
	    //System.out.print(h + " " + boundCheck + "/\n");

	    // estimate one full step
	    rk(order, 0, Y, dir*h);

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

	    //System.out.print(h + " " + boundCheck + "\n");
	    if (boundCheck) {
		for (i = 0; i != order; i++)
		    Y[i] = Yhalf[i] = oldY[i]; 
		h /= 2;
		if (h < minh) {
		    p.lifetime = -1;
		    return;
		}
		continue;
	    }
	    if (Y[0] < -1 || Y[0] >= .999 ||
		Y[1] < -1 || Y[1] >= .999) {
		for (i = 0; i != order; i++)
		    Y[i] = Yhalf[i] = oldY[i]; 
		h /= 2;
		if (h < minh) {
		    p.lifetime = -1;
		    return;
		}
		continue;
	    }

	    // estimate the local error
	    localError =
		java.lang.Math.abs(Y[0] - Yhalf[0]) +
		java.lang.Math.abs(Y[1] - Yhalf[1]);
	    
	    if (localError > E && h > minh) {
		h *= 0.75; // decrease the step size
		if (h < minh)
		    h = minh;
		for (i = 0; i != order; i++)
		    Y[i] = Yhalf[i] = oldY[i]; 
		continue;
	    } else if (localError < (E * 0.5)) {
		h *= 1.25; // increase the step size
		if (h > maxh)
		    h = maxh;
	    }
	    
	    double d = dist2(p.pos, Y);
	    if (!(d-lastd > 1e-10)) {
		// we're not getting anywhere!
		//System.out.print("nga " + d + " " + lastd + "\n");
		p.lifetime = -1;
		return;
	    }
	    if (d > segSize2max) {
		h /= 2;
		if (h < minh) {
		    p.lifetime = -1;
		    return;
		}
		for (i = 0; i != order; i++)
		    Y[i] = Yhalf[i] = oldY[i]; 
		continue;
	    }
	    ls_fieldavg[0] += rk_k1[0];
	    ls_fieldavg[1] += rk_k1[1];
	    avgct++;

	    if (d > segSize2min)
		break;
	    lastd = d;

	    for (i = 0; i != order; i++)
		oldY[i] = Yhalf[i] = Y[i]; 
	}

	//System.out.print(steps + " ss\n");
	p.stepsize = h;
	for (i = 0; i != 3; i++)
	    p.pos[i] = Y[i];
	p.phi = java.lang.Math.sqrt(ls_fieldavg[0]*ls_fieldavg[0]+
				    ls_fieldavg[1]*ls_fieldavg[1])/avgct;
    }

    int doubleToGrid(double x) {
	return (int) ((x+1)*gridsize/2);
    }

    double gridToDouble(int x) {
	return (x*2./gridsize)-1;
    }

    abstract class VecFunction {
	abstract String getName();
	abstract VecFunction createNext();
	boolean nonGradient() { return false; }
	boolean useRungeKutta() { return true; }
	boolean useAdaptiveRungeKutta() { return true; }
	boolean checkBoundsWithForce() { return true; }
	boolean checkBounds(double y[], double oldY[]) { return false; }
	abstract void getField(double result[], double y[]);
	boolean redistribute() { return true; }
	void setup() {}
	void setupFrame() {}
	void finishFrame() {}
	void actionPerformed() {}
	void calcDivergence() {}
	double getLevelHeight() { return 0; }
	void setGrid(GridElement ge, int x, int y) {
	    double xx[] = rk_k1;
	    double res[] = rk_k2;
	    double res1[] = rk_k3;
	    xx[0] = (x*2./gridsize)-1;
	    xx[1] = (y*2./gridsize)-1;
	    xx[2] = 0;
	    boundCheck = false;
	    getField(res, xx);
	    ge.vecX = reverse*res[0]*70;
	    ge.vecY = reverse*res[1]*70;
	    ge.height = reverse*res[2]*.625;
	    ge.valid = !boundCheck;
	    /*if (!(ge.height >= -9999 && ge.height <= 9999))
		System.out.print(xx[0] + " " + xx[1] + " " +
		ge.height + "\n"); */
	    double xorig0 = xx[0];
	    xx[0] += 1e-8;
	    getField(res1, xx);
	    ge.div = res1[0] - res[0];
	    ge.curl = res1[1] - res[1];
	    xx[0] = xorig0;
	    xx[1] += 1e-8;
	    getField(res1, xx);
	    ge.div = (ge.div + res1[1] - res[1])*1e10*reverse;
	    ge.curl = (ge.curl - (res1[0] - res[0]))*1e10*reverse;
	}
	double getDivOffset() { return 4; }
	double getDivRange() { return 11; }
    };

    class InverseRadial extends VecFunction {
	double lineLen;
	String getName() { return BUILD_CASE_EMV("charged line",
						 null, "1/r single line"); }
	void getField(double result[], double y[]) {
	    double r = distance(y[0], y[1]);
	    if (r < lineWidth)
		boundCheck = true;
	    double r2 = r*r;
	    result[0] = -.0002*y[0]/r2;
	    result[1] = -.0002*y[1]/r2;
	    result[2] = .4*java.lang.Math.log(r+1e-300);
	}
	void setup() {
	    lineLen = 1;
	}
	VecFunction createNext() { return new InverseRadialDouble(); }
    };

    class InverseRadialDouble extends VecFunction {
	InverseRadialDouble() { sign = 1; }
	String getName() { return BUILD_CASE_EMV("line charge double",
						 null, "1/r double lines"); }
	double sign;
	void getField(double result[], double y[]) {
	    // we want to make sure sep is a multiple of the grid spacing
	    // so that the divergence gets calculated correctly.
	    double sep = gridToDouble(gridsize/2+
				      aux1Bar.getValue()*gridsize/200);
	    double xx1 = y[0] - sep;
	    double xx2 = y[0] + sep;
	    double r1 = distance(xx1, y[1]);
	    double r2 = distance(xx2, y[1]);
	    if (r1 < lineWidth || r2 < lineWidth)
		boundCheck = true;
	    double q = .0002;
	    double r1s = 1/(r1*r1);
	    double r2s = 1/(r2*r2*sign);
	    result[0] = q*(-xx1 *r1s-xx2 *r2s);
	    result[1] = q*(-y[1]*r1s-y[1]*r2s);
	    result[2] = .2*(java.lang.Math.log(r1+1e-20)+
			    sign*java.lang.Math.log(r2+1e-20));
	}
	void setup() {
	    setupBar(0, "Line Separation", 30);
	}
	VecFunction createNext() { return
	    BUILD_CASE_EMV(new InverseRadialDipole(), null,
			   new InverseSquaredRadial()); }
    };

#ifdef BUILD_E
    class InverseRadialDipole extends InverseRadialDouble {
	InverseRadialDipole() { sign = -1; }
	String getName() { return "dipole lines"; }
	VecFunction createNext() { return new InverseRadialQuad(); }
    };

    class InverseRadialQuad extends VecFunction {
	String getName() { return "quad lines"; }
	void getField(double result[], double y[]) {
	    // we want to make sure sep is a multiple of the grid spacing
	    // so that the divergence gets calculated correctly.
	    double sep = gridToDouble(gridsize/2+
				      aux1Bar.getValue()*gridsize/200);
	    double xx1 = y[0] + sep;
	    double xx2 = y[0] - sep;
	    double yy1 = y[1] + sep;
	    double yy2 = y[1] - sep;
	    double r1 = distance(xx1, yy1);
	    double r2 = distance(xx2, yy1);
	    double r3 = distance(xx1, yy2);
	    double r4 = distance(xx2, yy2);
	    if (r1 < lineWidth || r2 < lineWidth ||
		r3 < lineWidth || r4 < lineWidth)
		boundCheck = true;
	    double q = .0003;
	    result[0] = q*(-xx1/(r1*r1)-xx2/(r4*r4) +xx2/(r2*r2)+xx1/(r3*r3));
	    result[1] = q*(-yy1/(r1*r1)-yy2/(r4*r4) +yy1/(r2*r2)+yy2/(r3*r3));
	    result[2] = .2*(+java.lang.Math.log(r1+1e-20)
			    -java.lang.Math.log(r2+1e-20)
			    -java.lang.Math.log(r3+1e-20)
			    +java.lang.Math.log(r4+1e-20));
	}
	void setup() {
	    setupBar(0, "Line Separation", 30);
	}
	VecFunction createNext() { return new InverseSquaredRadial(); }
    };
#endif /* BUILD_E */

    class InverseSquaredRadial extends VecFunction {
	String getName() { return BUILD_CASE_EMV("point charge",
						 null, "1/r^2 single"); }
	void getField(double result[], double y[]) {
	    double r = distance(y);
	    if (r < chargeSize)
		boundCheck = true;
	    double r3 = r*r*r;
	    double q = .0003/r3;
	    result[0] = -y[0]*q;
	    result[1] = -y[1]*q;
	    result[2] = -.3/r;
	}

	static final double chargeSize = .001;

	VecFunction createNext() { return new InverseSquaredRadialDouble(); }
    };

    class InverseSquaredRadialDouble extends InverseSquaredRadial {
	String getName() { return BUILD_CASE_EMV("point charge double", null,
						 "1/r^2 double"); }
	double sign2;
	void getField(double result[], double y[]) {
	    // we want to make sure sep is a multiple of the grid spacing
	    // so that the divergence gets calculated correctly.
	    double sep = gridToDouble(gridsize/2+
				      aux1Bar.getValue()*gridsize/200);
	    double xx1 = y[0]-sep;
	    double xx2 = y[0]+sep;
	    double r1 = distance(xx1, y[1]);
	    if (r1 < chargeSize)
		boundCheck = true;
	    double r2 = distance(xx2, y[1]);
	    if (r2 < chargeSize)
		boundCheck = true;
	    double q = .0003;
	    double rq1 = q/(r1*r1*r1);
	    double rq2 = q/(r2*r2*r2) * sign2;
	    result[0] = -xx1 *rq1-xx2 *rq2;
	    result[1] = -y[1]*rq1-y[1]*rq2;
	    result[2] = -.05/r1 - .05*sign2/r2;
	    if (sign2 == -1)
		result[2] *= 2;
	}
	void setup() {
	    sign2 = 1;
	    setupBar(0, "Charge Separation", 30);
	}
	VecFunction createNext() { return
	    BUILD_CASE_EMV(new InverseSquaredRadialDipole(),
			   null, new InverseRotational()); }
    };

#ifdef BUILD_E
    class InverseSquaredRadialDipole extends InverseSquaredRadialDouble {
	String getName() { return "dipole"; }
	void setup() {
	    super.setup();
	    sign2 = -1;
	}
	VecFunction createNext() { return new InverseSquaredRadialQuad(); }
    };
    class InverseSquaredRadialQuad extends InverseSquaredRadial {
	String getName() { return "quadrupole"; }
	void getField(double result[], double y[]) {
	    // we want to make sure sep is a multiple of the grid spacing
	    // so that the divergence gets calculated correctly.
	    double sep = gridToDouble(gridsize/2+
				      aux1Bar.getValue()*gridsize/200);
	    double xx1 = y[0] - sep;
	    double xx2 = y[0] + sep;
	    double yy1 = y[1] - sep;
	    double yy2 = y[1] + sep;
	    double r1 = distance(xx1, yy1);
	    double r2 = distance(xx2, yy1);
	    double r3 = distance(xx1, yy2);
	    double r4 = distance(xx2, yy2);
	    if (r1 < chargeSize || r2 < chargeSize ||
		r3 < chargeSize || r4 < chargeSize)
		boundCheck = true;
	    double q = .0003;
	    double rq1 = q/(r1*r1*r1);
	    double rq2 = q/(r2*r2*r2);
	    double rq3 = q/(r3*r3*r3);
	    double rq4 = q/(r4*r4*r4);
	    result[0] = -xx1*rq1-xx2*rq4+xx2*rq2+xx1*rq3;
	    result[1] = -yy1*rq1-yy2*rq4+yy1*rq2+yy2*rq3;
	    result[2] = .05*(-1/r1+1/r2+1/r3-1/r4);
	}
	void setup() {
	    super.setup();
	    setupBar(0, "Charge Separation", 30);
	}
	VecFunction createNext() { return new ConductingPlate(); }
    };

    class ConductingPlate extends VecFunction {
	String getName() { return "conducting plate"; }
	Complex z, z2;
	boolean plate;
	double a, base;
	ConductingPlate() {
	    z = new Complex();
	    z2 = new Complex();
	    plate = true;
	}
	void setupFrame() {
	    a = (aux1Bar.getValue()+1)/100.;
	    z.set(0, 1/a);
	    z.arcsin();
	    base = z.b;
	}
	void getField(double result[], double y[]) {
	    // smythe p89
	    if (y[1] >= -.02 && y[1] <= .02) {
		if ((plate  && y[0] >= -a && y[0] <= a) ||
		    (!plate && (y[0] >= a || y[0] <= -a)))
		boundCheck = true;
	    }
	    z.set(y[0]/a, y[1]/a);
	    if (y[1] < 0 && plate)
		z.b = -z.b;
	    z2.set(z);
	    z2.arcsin();
	    result[2] = (plate) ? z2.b/base-1 : -z2.a*.6;

	    // here we calculate ((1-(z/a)^2)^(-1/2))/a, which is
	    // d/dz arcsin(z/a)
	    z.square();
	    z.mult(-1);
	    z.add(1);
	    z.pow(-.5);
	    z.mult(1/a);
	    if (plate) {
		// field = (Im dw/dz, Re dw/dz)
		result[1] = z.a * -.0007;
		result[0] = z.b * -.0007;
		if (y[1] <= 0)
		    result[1] = -result[1];
	    } else {
		// field = (Re dw/dz, -Im dw/dz)
		result[0] = z.a * .0007;
		result[1] = -z.b * .0007;
		if (y[1] == 0)
		    result[1] = -result[1];
	    }
	}
	void setup() {
	    setupBar(0, "Plate Size", 60);
	}
	double getDivOffset() { return -17.3; }
	double getDivRange() { return 2.5; }
	VecFunction createNext() { return new ChargedPlate(); }
    };
    class ChargedPlate extends ConductingPlate {
	Complex cz;
	ChargedPlate() { cz = new Complex(); }
	String getName() { return "charged plate"; }
	double getDivOffset() { return 4; }
	double getDivRange() { return 11; }
	double getPot(double a1, double a2, double y) {
	    cz.set (y, -a1);
	    cz.mult(y,  a2);
	    cz.log();
	    double b1 = cz.b;
	    cz.set (y,  a1);
	    cz.mult(y, -a2);
	    cz.log();
	    double y2 = y*y;
	    if (y2 == 0)
		y2 = 1e-8;
	    return .3*(2*(a1-a2) + (b1-cz.b)*y +
		       a2*java.lang.Math.log(a2*a2+y2) -
		       a1*java.lang.Math.log(a1*a1+y2));
	}
	void calcDivergence() {
	    double sep = aux2Bar.getValue()/100.;
	    int x, y;
	    for (x = 0; x != gridsize; x++) {
		double xx = gridToDouble(x);
		if (xx < -a || xx > a)
		    continue;
		grid[x][gridsize/2].div = -reverse;
	    }
	}
	void getField(double result[], double y[]) {
	    if (y[1] >= -.01 && y[1] <= .01 &&
		(y[0] >= -a && y[0] <= a))
		boundCheck = true;
	    double a1 = -a-y[0];
	    double a2 =  a-y[0];
	    double y2 = y[1]*y[1];
	    if (y2 == 0)
		y2 = 1e-8;
	    double q = .0003/a;
	    result[0] = .5*q*
	        java.lang.Math.log((y2+a2*a2)/(y2+a1*a1));
	    result[1] = q*
		(java.lang.Math.atan(a1/y[1])-
		 java.lang.Math.atan(a2/y[1]));
	    result[2] = .4*getPot(a1, a2, y[1])/a;
	}
	VecFunction createNext() { return new ChargedPlatePair(); }
    };

    class ChargedPlatePair extends ChargedPlate {
	String getName() { return "charged plate pair"; }
	boolean useRungeKutta() { return false; }
	double dipole;
	ChargedPlatePair() { dipole = 1; }
	void getField(double result[], double y[]) {
	    double sep = aux2Bar.getValue()/100.;
	    if ((y[1] >= -.01+sep && y[1] <= .01+sep ||
		 y[1] >= -.01-sep && y[1] <= .01-sep) &&
		y[0] >= -a && y[0] <= a)
		boundCheck = true;
	    double a1 = -a-y[0];
	    double a2 =  a-y[0];
	    double y1 = y[1]-sep;
	    double y12 = y1*y1;
	    if (y12 == 0)
		y12 = 1e-8;
	    double y2 = y[1]+sep;
	    double y22 = y2*y2;
	    if (y22 == 0)
		y22 = 1e-8;

	    double q = .0003/a;
	    result[0] = .5*q*
	        (java.lang.Math.log((y12+a2*a2)/(y12+a1*a1)) + dipole*
		 java.lang.Math.log((y22+a2*a2)/(y22+a1*a1)));
	    result[1] = q*
		(java.lang.Math.atan(a1/y1)
		-java.lang.Math.atan(a2/y1)
		+dipole*(java.lang.Math.atan(a1/y2)
			-java.lang.Math.atan(a2/y2)));
	    result[2] = .4*(getPot(a1, a2, y1)+dipole*getPot(a1, a2, y2))/a;
	}
	void calcDivergence() {
	    double sep = aux2Bar.getValue()/100.;
	    int x, y;
	    for (x = 0; x != gridsize; x++) {
		double xx = gridToDouble(x);
		if (xx < -a || xx > a)
		    continue;
		y = doubleToGrid(sep);
		grid[x][y].div = -reverse;
		y = doubleToGrid(-sep);
		grid[x][y].div = -dipole*reverse;
	    }
	}
	void setup() {
	    setupBar(0, "Sheet Size", 60);
	    setupBar(1, "Sheet Separation", 33);
	}
	boolean checkBounds(double y[], double oldY[]) {
	    double size = aux1Bar.getValue()/100.;
	    double sep = aux2Bar.getValue()/100.;
	    // check to see if particle has passed through one of plates
	    if (y[0] >= -size && y[0] <= size) {
		if (y[1] > sep) {
		    if (oldY[1] < sep)
			return true;
		} else if (y[1] < -sep) {
		    if (oldY[1] > -sep)
			return true;
		} else if (oldY[1] > sep || oldY[1] < -sep)
		    return true;
	    }
	    return false;
	}
	VecFunction createNext() { return new ChargedPlateDipole(); }
    };

    class ChargedPlateDipole extends ChargedPlatePair {
	String getName() { return "charged plate dipole"; }
	ChargedPlateDipole() { dipole = -1; }
	VecFunction createNext() { return new InfiniteChargedPlane(); }
    };

    class InfiniteChargedPlane extends VecFunction {
	String getName() { return "infinite plane"; }
	void getField(double result[], double y[]) {
	    double alpha = .0004;
	    if (y[1] > -.01 && y[1] < .01)
		boundCheck = true;
	    result[0] = 0;
	    result[1] = (y[1] <= 0) ? alpha : -alpha;
	    result[2] = java.lang.Math.abs(y[1])-1;
	}
	boolean checkBoundsWithForce() { return false; }
	VecFunction createNext() { return new Cylinder(); }
    };
    
    class Cylinder extends VecFunction {
	String getName() { return "conducting cylinder"; }
	double getCylRadius() { return (aux1Bar.getValue()+1)/110.; }
	void setup() {
	    setupBar(0, "Cylinder Size", 30);
	    setupBar(1, "Cylinder Potential", 1);
	}
	void getField(double result[], double y[]) {
	    // Smythe p70
	    double a = getCylRadius();
	    double farpt = 4;

	    double pot = 2*(aux2Bar.getValue()/50.-1);
	    final int mult = 4000;

	    // add in charge that will bring cylinder to specified potential
	    double cq = -pot/
		(mult*(java.lang.Math.log(a)-java.lang.Math.log(farpt)));
	    double pot0 = mult*cq*java.lang.Math.log(farpt);

	    double y0 = y[0];
	    double y1 = y[1];
	    double r1 = distance(y0, y1);
	    if (r1 < a) {
		result[0] = result[1] = 0;
		result[2] = pot;
		boundCheck = true;
		return;
	    }
	    double a1 = 5*cq/(r1*r1);
	    result[0] = y0*a1;
	    result[1] = y1*a1;
	    result[2] = pot0-cq*mult*java.lang.Math.log(r1);
	}
	void calcDivergence() {
	    double a = getCylRadius();
	    int i;
	    for (i = 0; i != 100; i++) {
		double th = 2*pi*i/100.;
		double xx = java.lang.Math.cos(th)*a;
		double yy = java.lang.Math.sin(th)*a;
		grid[doubleToGrid(xx)][doubleToGrid(yy)].div -= reverse/20.;
	    }
	}
	VecFunction createNext() { return new CylinderAndLineCharge(); }
    };

    class CylinderAndLineCharge extends VecFunction {
	String getName() { return "cyl + line charge"; }
	double getCylRadius() { return (aux1Bar.getValue()+1)/110.; }
	double getSeparation() { return aux2Bar.getValue()/100.; }
	double getCylPos() { return getSeparation()/2; }
	double getPointPos() { return -getSeparation()/2-getCylRadius(); }
	void setup() {
	    setupBar(0, "Cylinder Size", 30);
	    setupBar(1, "Separation", 30);
	    setupBar(2, "Cylinder Potential", 50);
	}

	void setupFrame() {
	    // Smythe p70
	    q = -.0003;
	    a = getCylRadius();
	    b = getSeparation() + a;
	    spos = getCylPos();
	    imagePos = spos - a*a/b;

	    double r2_0 = spos+a-imagePos;
	    double r3_0 = spos+a-getPointPos();

	    // Find charge that will eliminate field at far end cylinder.
	    // That's our definition of "ground".  (We can't use the
	    // potential at infinity as ground, since that diverges.)
	    cq = a*a*(q/(r2_0*r2_0) - q/(r3_0*r3_0));

	    // calculate potential on cylinder with that charge.  we will
	    // subtract that value out of the potential everywhere.
	    pot0 = -cq*java.lang.Math.log(a)
		+q*java.lang.Math.log(r2_0)
		-q*java.lang.Math.log(r3_0);

	    // add in charge that will bring cylinder to specified potential
	    cq -= (aux3Bar.getValue()/50.-1)*.0006/java.lang.Math.log(a);
	}

	double q, a, b, spos, imagePos, cq, pot0;

	void calcDivergence() {
	    int i;
	    double pos[] = rk_k1;
	    double res[] = rk_k2;
	    double a1 = getCylRadius()+.001;
	    int x, y;
	    for (x = 0; x != gridsize; x++)
		for (y = 0; y != gridsize; y++)
		    grid[x][y].div = 0;
	    grid[doubleToGrid(getPointPos())][doubleToGrid(0)].div = -reverse;
	    for (i = 0; i != 200; i++) {
		double th = 2*pi*i/200.;
		double costh = java.lang.Math.cos(th);
		double sinth = java.lang.Math.sin(th);
		pos[0] = costh*a1+getCylPos();
		pos[1] = sinth*a1;
		pos[2] = 0;
		curfunc.getField(res, pos);
		grid[doubleToGrid(costh*a+getCylPos())]
		    [doubleToGrid(sinth*a)].div +=
		    (costh*res[0] + sinth*res[1])*60*reverse;
	    }
	}
	void getField(double result[], double y[]) {
	    // Smythe p70
	    double x1 = y[0]-spos;
	    double r1 = distance(x1, y[1]);
	    final int mult = 4000;
	    double y0 = y[0];
	    double y1 = y[1];
	    if (r1 < a) {
		y0 = spos+a;
		y1 = 0;
		x1 = r1 = a;
		boundCheck = true;
	    }
	    double x2 = y0-imagePos;
	    double r2 = distance(x2, y1);
	    double x3 = y0-getPointPos();
	    double r3 = distance(x3, y1);
	    double chargeSize = .001;
	    if (r3 < chargeSize)
		boundCheck = true;
	    double a1 = cq/(r1*r1);
	    double a2 = -q/(r2*r2);
	    double a3 = q/(r3*r3);
	    result[0] = x1*a1 + x2*a2 + x3*a3;
	    result[1] = y1*(a1+a2+a3);
	    result[2] = mult*(-pot0
			      -cq*java.lang.Math.log(r1)
			      +q*java.lang.Math.log(r2+1e-20)
			      -q*java.lang.Math.log(r3+1e-20));
	    if (r1 == a)
		result[0] = result[1] = 0;
	}
	VecFunction createNext() { return new CylinderInField(); }
    };

    class CylinderInField extends VecFunction {
	CylinderInField() { conducting = true; showD = false; }
	String getName() { return "cylinder in field"; }
	boolean conducting, showD;
	double a;
	void setupFrame() {
	    a = aux1Bar.getValue()/100.;
	}
	void getField(double result[], double y[]) {
	    // smythe p67
	    double a2 = a*a;
	    double r = distance(y[0], y[1]);
	    double e1 = aux2Bar.getValue()/10. + 1;
	    double dimult = (conducting) ? 1 : (e1-1)/(e1+1);
	    double fmult = .0006;
	    final double potmult = 3;
	    if (r < a) {
		result[0] = result[1] = result[2] = 0;
		if (conducting)
		    boundCheck = true;
		else
		    result[0] = (showD) ? e1*fmult*(1-dimult) :
		        fmult*(1-dimult);
		result[2] = -potmult*(1-dimult)*y[0];
		return;
	    }
	    double costh = y[0]/r;
	    double sinth = y[1]/r;
	    double r_2 = 1/(r*r);
	    double er  = (1+dimult*a2*r_2)*costh*fmult;
	    double eth = -(1-dimult*a2*r_2)*sinth*fmult;
	    er /= r;
	    result[0] = y[0]*er - eth*sinth;
	    result[1] = y[1]*er + eth*costh;
	    result[2] = -potmult*(1-dimult*a2*r_2)*y[0];
	}
	void setup() {
	    setupBar(0, "Cylinder Size", 40);
	}
	void calcDivergence() {
	    int i;
	    double pos[] = rk_k1;
	    double res[] = rk_k2;
	    double a1 = a+.001;
	    int x, y;
	    for (x = 0; x != gridsize; x++)
		for (y = 0; y != gridsize; y++)
		    grid[x][y].div = 0;
	    for (i = 0; i != 200; i++) {
		double th = 2*pi*i/200.;
		double costh = java.lang.Math.cos(th);
		double sinth = java.lang.Math.sin(th);
		pos[0] = costh*a1;
		pos[1] = sinth*a1;
		pos[2] = 0;
		curfunc.getField(res, pos);
		double rx = res[0];
		double ry = res[1];
		double a2 = a-.001;
		pos[0] = costh*a2;
		pos[1] = sinth*a2;
		pos[2] = 0;
		curfunc.getField(res, pos);
		grid[doubleToGrid(costh*a)][doubleToGrid(sinth*a)].div +=
		    (costh*(rx-res[0]) + sinth*(ry-res[1]))*4e2*reverse;
	    }
	}
	VecFunction createNext() { return new DielectricCylinderInFieldE(); }
    };

    class DielectricCylinderInFieldE extends CylinderInField {
	DielectricCylinderInFieldE() { conducting = false; showD = false; }
	String getName() { return "dielec cyl in field"; }
	void setup() {
	    setupBar(0, "Cylinder Size", 40);
	    setupBar(1, "Dielectric Strength", 60);
	}
	VecFunction createNext() { return new SlottedPlane(); }
    };

    /*class DielectricBoundaryE extends InverseSquaredRadial {
	boolean showD, conducting;
	double planeY;
	DielectricBoundaryE() { conducting = false; showD = false; }
	String getName() { return "dielec boundary"; }
	void setup() {
	    setupBar(0, "Charge Location", 60);
	    if (!conducting)
		setupBar(1, "Dielectric Strength", 60);
	}
	void getField(double result[], double y[]) {
	    double cx = aux1Bar.getValue()/50.-1.001;
	    double r1 = distance(y[0], y[1]-cx);
	    if (r1 < chargeSize)
		boundCheck = true;
	    double eps1 = 1;
	    double eps2 = aux2Bar.getValue()/10. + 1;
	    if (conducting)
		eps2 = 1e8;
	    if (cx < planeY) {
		eps1 = eps2;
		eps2 = 1;
	    }
	    double q1 = .0003;
	    double q2 = -(eps2-eps1)/(eps2+eps1)*q1;
	    double ep = eps1;
	    if (cx > planeY && y[1] < planeY || cx < planeY && y[1] > planeY) {
		q1 = 2*eps2*q1/(eps2+eps1);
		q2 = 0;
		ep = eps2;
	    }
	    double r2 = distance(y[0], y[1]-planeY*2+cx);
	    result[2] = -2000*(q1/(r1*ep) + q2/(r2*ep));
	    if (!showD) {
		q1 /= ep;
		q2 /= ep;
	    }
	    double rq1 = q1/(r1*r1*r1);
	    double rq2 = q2/(r2*r2*r2);
	    result[0] = -y[0]*(rq1+rq2);
	    result[1] = -(y[1]-cx)*rq1-(y[1]-planeY*2+cx)*rq2;
	}
	VecFunction createNext() { return new ConductingPlane(); }
    };
    class ConductingPlane extends DielectricBoundaryE {
	ConductingPlane() { conducting = true; showD = false; planeY = -1; }
	String getName() { return "conducting plane + pt"; }
	VecFunction createNext() { return new SlottedPlane(); }
	};*/

    class SlottedPlane extends VecFunction {
	String getName() { return "slotted conducting plane"; }
	Complex z, z2, z3;
	double getDivOffset() { return -17.3; }
	double getDivRange() { return 2.5; }
	SlottedPlane() {
	    z = new Complex();
	    z2 = new Complex();
	    z3 = new Complex();
	}
	void getField(double result[], double y[]) {
	    // W = -.5E (z +- (z^2-a^2)^1/2)
	    // dw/dz = -.5E (1 +- z/(z^2-a^2)^1/2)
	    // Smythe p92
	    double a = (aux1Bar.getValue()+1)/101.;
	    z.set(y[0], y[1]);
	    if (y[1] >= -.01 && y[1] <= .01 &&
		(y[0] < -a || y[0] > a)) {
		boundCheck = true;
		if (z.b == 0)
		    z.b = -1e-8;
	    }
	    z2.set(z);
	    z2.square();
	    z2.add(-a*a);
	    z3.set(z2);
	    z3.pow(.5);
	    if (z3.b < 0)
		z3.mult(-1);
	    z3.add(z.a, z.b);
	    result[2] = z3.b*2;

	    z2.pow(-.5);
	    // I can barely understand what Smythe is talking about but
	    // I think he wants the square root to always have a positive
	    // imaginary part.  Here we already took the reciprocal
	    // (by calling pow(-.5) instead of pow(.5)) so we want the
	    // root to have a negative imaginary part instead.
	    if (z2.b > 0)
		z2.mult(-1);
	    z2.mult(z);
	    // field = (Im dw/dz, Re dw/dz)
	    result[1] = -(1+z2.a) * .003;
	    result[0] = -(z2.b) * .003;
	}
	void setup() {
	    setupBar(0, "Slot Size", 30);
	}
	VecFunction createNext() { return new PlanePair(); }
    };
    class PlanePair extends ConductingPlate {
	String getName() { return "conducting planes w/ gap"; }
	PlanePair() { super(); plate = false; }
	void setup() {
	    setupBar(0, "Gap Size", 20);
	}
	double getDivOffset() { return -17; }
	VecFunction createNext() { return null; }
    };

#endif
#ifndef BUILD_E
    class InverseRotational extends InverseRadial {
	String getName() { return BUILD_CASE_EMV(null, "current line",
				  "1/r rotational"); }
	void getField(double result[], double y[]) {
	    double r = distance(y[0], y[1]);
	    if (showA) {
		result[0] = result[1] = 0;
		result[2] = -.001*(java.lang.Math.log(r)-.5);
	    } else {
		if (r < lineWidth*2)
		    boundCheck = true;
		rotateParticle(result, y, .0001/(r*r));
	    }
	}
	boolean nonGradient() { return true; }
	VecFunction createNext() { return new InverseRotationalPotential(); }
    };

    class InverseRotationalPotential extends VecFunction {
	String getName() { return "1/r rotational potential"; }
	void getField(double result[], double y[]) {
	    double r = distance(y[0], y[1]);
	    rotateParticle(result, y, .0001/(r*r));
	    if (r < lineWidth*2)
		boundCheck = true;
	    else if (y[0] >= 0 && y[1] < .001 && y[1] > -.025) {
		boundCheck = true;
		if (y[1] == 0)
		    result[1] = 1e8;
	    }
	    double ang = java.lang.Math.atan2(y[1], y[0]);
	    if (ang < 0)
		ang += 2*pi;
	    result[2] = (pi-ang)*.3;
	}
	VecFunction createNext() { return new InverseRotationalDouble(); }
    };

    class InverseRotationalDouble extends InverseRadialDouble {
	InverseRotationalDouble() { dir2 = 1; ext = false; }
	int dir2;
	boolean ext;
	String getName() { return BUILD_CASE_EMV(null, "current line double",
						 "1/r rotational double"); }
	void getField(double result[], double y[]) {
	    // we want to make sure sep is a multiple of the grid spacing
	    // so that the divergence gets calculated correctly.
	    double sep = gridToDouble(gridsize/2+
				      aux1Bar.getValue()*gridsize/200);
	    double r  = distance(y[0] - sep, y[1]);
	    double r2 = distance(y[0] + sep, y[1]);
	    if (ext) {
		double p = aux3Bar.getValue()*pi/50.;
		double s = aux2Bar.getValue()/6.;
		getDirectionField(result, y, p);
		result[0] *= s;
		result[1] *= s;
		result[2] *= s;
	    } else
		result[0] = result[1] = result[2] = 0;
	    if (showA) {
		if (dir2 == 1)
		    result[2] += -.001*(java.lang.Math.log(r)+
				       java.lang.Math.log(r2)-1);
		else
		    result[2] += .001*(java.lang.Math.log(r)-
				       java.lang.Math.log(r2));
	    } else {
		if (r < lineWidth*2)
		    boundCheck = true;
		rotateParticleAdd(result, y, .0001/(r*r),  sep, 0);
		if (r2 < lineWidth*2)
		    boundCheck = true;
		rotateParticleAdd(result, y, dir2*.0001/(r2*r2), -sep, 0);
	    }
	}
	void setup() {
	    setupBar(0, "Line Separation", 30);
	    if (ext) {
		setupBar(1, "Ext. Strength", 7);
		setupBar(2, "Ext. Direction", 0);
	    }
	}
	boolean nonGradient() { return true; }
	VecFunction createNext() { return new InverseRotationalDoubleExt(); }
    };
    class InverseRotationalDoubleExt extends InverseRotationalDouble {
	InverseRotationalDoubleExt() { ext = true; }
	String getName() { return BUILD_CASE_EMV(null, "cur line double + ext",
						 "1/r rot double + ext"); }
	VecFunction createNext() { return new InverseRotationalDipole(); }
    };
    class InverseRotationalDipole extends InverseRotationalDouble {
	InverseRotationalDipole() { dir2 = -1; }
	String getName() { return BUILD_CASE_EMV(null, "current line dipole",
						 "1/r rotational dipole"); }
	VecFunction createNext() { return new InverseRotationalDipoleExt(); }
    };
    class InverseRotationalDipoleExt extends InverseRotationalDouble {
	InverseRotationalDipoleExt() { dir2 = -1; ext = true; }
	void setup() {
	    super.setup();
	    aux2Bar.setValue(3);
	    aux3Bar.setValue(25);
	}
	String getName() { return BUILD_CASE_EMV(null, "cur line dipole + ext",
						 "1/r rot dipole + ext"); }
	VecFunction createNext() { return new OneDirectionFunction(); }
    };
    class OneDirectionFunction extends VecFunction {
	String getName() { return BUILD_CASE_EMV(null, "uniform field",
						 "one direction"); }
	void getField(double result[], double y[]) {
	    double th = aux1Bar.getValue() * pi /50.;
	    getDirectionField(result, y, th);
	}

	void setup() {
	    setupBar(0, "Theta", 0);
	}
	VecFunction createNext() { return
	    BUILD_CASE_EMV(null, new MovingChargeField(),
			   new InverseSquaredRadialSphere()); }
    };

    void getDirectionField(double result[], double y[], double th) {
	double sinth = java.lang.Math.sin(th);
	double costh = java.lang.Math.cos(th);
	if (!showA) {
	    result[0] = .0003*costh;
	    result[1] = .0003*sinth;
	    result[2] = -.4*(y[0]*costh + y[1]*sinth);
	} else {
	    // The A for this field is a linear rotation around
	    // an axis pointing in the direction of the field.
	    // First calculate the axis.
	    double axis[] = new double[3];
	    axis[0] = costh;
	    axis[1] = sinth;
	    axis[2] = 0;
	    // now project the point we are looking at onto the axis
	    double d = dot(axis, y);
	    // subtract out this projection so we can get the vector
	    // from the point to the axis
	    double r[] = new double[3];
	    int i;
	    for (i = 0; i != 2; i++)
		r[i] = .0006*(y[i] - axis[i]*d);
	    // cross this vector with the axis vector to get the result
	    cross(result, axis, r);
	}
    }

#endif
#ifndef BUILD_V
    class MovingChargeField extends InverseSquaredRadial {
	String getName() { return "moving charge"; }
	void getField(double result[], double y[]) {
	    double rz = distance(y);
	    if (showA) {
		result[0] = result[1] = 0;
		result[2] = .0003/rz;
	    } else {
		double r = distance(y[0], y[1]);
		if (rz < chargeSize)
		    boundCheck = true;
		rotateParticle(result, y, .0001/(rz*rz*rz));
	    }
	}
	boolean nonGradient() { return true; }
	VecFunction createNext() { return
	       BUILD_CASE_EMV(null, new FastChargeField(), null); }
    };
#endif
#ifdef BUILD_V
    class InverseSquaredRadialSphere extends VecFunction {
	String getName() { return "1/r^2 sphere"; }
	double getSize() { return (aux1Bar.getValue()+1)/110.; }
	void getField(double result[], double y[]) {
	    double r = distance(y);
	    if (r < .01)
		boundCheck = true;
	    double a = getSize();
	    result[2] = .2*((r > a) ? -1/r : -3/(2*a)+r*r/(2*a*a*a));
	    if (r < a)
		r = a;
	    double alpha = .0003/(r*r*r);
	    result[0] = -y[0]*alpha;
	    result[1] = -y[1]*alpha;
	}
	void setup() {
	    setupBar(0, "Sphere Size", 30);
	}

	boolean checkBoundsWithForce() { return false; }
	VecFunction createNext() { return new ConstRadial(); }
    };
    class ConstRadial extends InverseSquaredRadial {
	String getName() { return "const radial"; }
	void getField(double result[], double y[]) {
	    double r = distance(y);
	    if (r < chargeSize)
		boundCheck = true;
	    double q = .0006/r;
	    result[0] = -q*y[0];
	    result[1] = -q*y[1];
	    result[2] = r;
	}
	boolean checkBoundsWithForce() { return false; }
	VecFunction createNext() { return new LinearRadial(); }
    };

    class LinearRadial extends InverseSquaredRadial {
	String getName() { return "linear radial"; }
	void getField(double result[], double y[]) {
	    double r = distance(y);
	    if (r < chargeSize)
		boundCheck = true;
	    double k = .0009;
	    result[0] = -y[0]*k;
	    result[1] = -y[1]*k;
	    result[2] = r*r-1;
	}
	boolean checkBoundsWithForce() { return false; }
	VecFunction createNext() { return new ConstantToYAxis(); }
    };

    class ConstantToYAxis extends InverseRadial {
	String getName() { return "constant to y axis"; }
	void getField(double result[], double y[]) {
	    double alpha = .0004;
	    if (y[0] > -.01 && y[0] < .01)
		boundCheck = true;
	    result[0] = (y[0] <= 0) ? alpha : -alpha;
	    result[1] = 0;
	    result[2] = java.lang.Math.abs(y[0])-1;
	}
	boolean checkBoundsWithForce() { return false; }
	VecFunction createNext() { return new LinearToYAxis(); }
    };
    class LinearToYAxis extends InverseRadial {
	String getName() { return "linear to y axis"; }
	void getField(double result[], double y[]) {
	    double r = java.lang.Math.abs(y[0]);
	    if (r < lineWidth)
		boundCheck = true;
	    double q = .0009;
	    result[0] = -y[0]*q;
	    result[1] = 0;
	    result[2] = r*r-1;
	}
	boolean checkBoundsWithForce() { return false; }
	VecFunction createNext() { return new LinearToXYAxes(); }
    };
    class LinearToXYAxes extends VecFunction {
	String getName() { return "2-dimensional oscillator"; }
	void getField(double result[], double y[]) {
	    double alpha = .0006;
	    double r = java.lang.Math.sqrt((aux1Bar.getValue()+1)/51.);
	    result[0] = -alpha*r*y[0];
	    result[1] = -alpha/r*y[1];
	    result[2] = (y[0]*y[0]*r+y[1]*y[1]/r)-1;
	}
	void setup() {
	    setupBar(0, "X/Y Ratio", 50);
	}
	boolean checkBoundsWithForce() { return false; }
	VecFunction createNext() { return new InverseToYAxis(); }
    };
    class InverseToYAxis extends VecFunction {
	String getName() { return "inverse to y axis"; }
	void getField(double result[], double y[]) {
	    if (y[0] > -.01 && y[0] < .01)
		boundCheck = true;
	    double alpha = .0003;
	    double zz = y[0];
	    if (zz == 0)
		zz = .00001;
	    result[0] = -alpha/zz;
	    result[1] = 0;
	    result[2] = -.01/(zz*zz);
	}
	VecFunction createNext() { return new InverseSquareRotational(); }
    };
    class InverseSquareRotational extends InverseRadial {
	String getName() { return "1/r^2 rotational"; }
	void getField(double result[], double y[]) {
	    double r = distance(y[0], y[1]);
	    if (r < lineWidth*2)
		boundCheck = true;
	    rotateParticle(result, y, .0001/(r*r*r));
	}
	boolean nonGradient() { return true; }
	VecFunction createNext() { return new LinearRotational(); }
    };
    class LinearRotational extends InverseRadial {
	String getName() { return "linear rotational"; }
	void getField(double result[], double y[]) {
	    double q = .0006;
	    result[0] = -q*y[1];
	    result[1] =  q*y[0];
	    result[2] = 0;
	}
	boolean nonGradient() { return true; }
	VecFunction createNext() { return new ConstantRotational(); }
    };

    class ConstantRotational extends InverseRadial {
	String getName() { return "constant rotational"; }
	void getField(double result[], double y[]) {
	    double r = distance(y[0], y[1]);
	    rotateParticle(result, y, .0006/r);
	}
	boolean nonGradient() { return true; }
	VecFunction createNext() { return new FxEqualsYField(); }
    };

    class FxEqualsYField extends VecFunction {
	String getName() { return "(y,0)"; }
	void getField(double result[], double y[]) {
	    result[2] = result[1] = 0;
	    result[0] = y[1] * .0009;
	}
	boolean nonGradient() { return true; }
	VecFunction createNext() { return new FxEqualsY2(); }
    };

    class FxEqualsY2 extends VecFunction {
	String getName() { return "(y^2,0)"; }
	void getField(double result[], double y[]) {
	    result[2] = result[1] = 0;
	    result[0] = y[1]*y[1] * .002;
	}
	boolean nonGradient() { return true; }
	VecFunction createNext() { return new Saddle(); }
    };

    class Saddle extends VecFunction {
	String getName() { return "saddle"; }
	void getField(double result[], double y[]) {
	    double q = .001;
	    result[0] = -q*y[0];
	    result[1] =  q*y[1]*.5;
	    result[2] = 1*(2*y[0]*y[0] - y[1]*y[1]);
	}
	VecFunction createNext() { return new RotationalExpansion(); }
    };

    class RotationalExpansion extends VecFunction {
	String getName() { return "rotation + expansion"; }
	void getField(double result[], double y[]) {
	    double q = .0006;
	    result[0] = q*(y[0]-y[1]);
	    result[1] = q*(y[0]+y[1]);
	    result[2] = 0;
	}
	boolean nonGradient() { return true; }
	VecFunction createNext() { return new Function4Field(); }
    };
    
    class Function4Field extends VecFunction {
	String getName() { return "(x^2-y,x+y^2)"; }
	void getField(double result[], double y[]) {
	    double q = .0003;
	    result[0] = q*(y[0]*y[0]-y[1]);
	    result[1] = q*(y[0]+y[1]*y[1]);
	    result[2] = 0;
	}
	boolean nonGradient() { return true; }
	VecFunction createNext() { return new Function5Field(); }
    };

    class Function5Field extends VecFunction {
	String getName() { return "(x+y^2,x^2-y)"; }
	void getField(double result[], double y[]) {
	    double q = .0003;
	    result[0] = q*(y[0]+y[1]*y[1]);
	    result[1] = q*(y[0]*y[0]-y[1]);
	    result[2] = 0;
	}
	boolean nonGradient() { return true; }
	VecFunction createNext() { return new Function6Field(); }
    };

    class Function6Field extends VecFunction {
	String getName() { return "(x,x^2)"; }
	void getField(double result[], double y[]) {
	    double q = .0006;
	    result[0] = q*y[0];
	    result[1] = q*y[0]*y[0];
	    result[2] = 0;
	}
	boolean nonGradient() { return true; }
	VecFunction createNext() { return new Function7Field(); }
    };

    class Function7Field extends VecFunction {
	String getName() { return "u=x^2+y"; }
	void getField(double result[], double y[]) {
	    double q = .0003;
	    result[0] = -2*y[0]*q;
	    result[1] = -q;
	    result[2] = (y[0]*y[0]+y[1])*1;
	}
	VecFunction createNext() { return new PendulumPotential(); }
    };

    class PendulumPotential extends VecFunction {
	String getName() { return "pendulum potential"; }
	void getField(double result[], double y[]) {
	    double q = .0006;
	    double xx  = y[0]*3.1;
	    double yy  = y[1]*3.1;
	    double cosx = java.lang.Math.cos(xx);
	    double cosy = java.lang.Math.cos(yy);
	    double sinx = java.lang.Math.sin(xx);
	    double siny = java.lang.Math.sin(yy);
	    result[0] = -q*sinx*cosy;
	    result[1] = -q*cosx*siny;
	    result[2] = -cosx*cosy*.5;
	}
	VecFunction createNext() { return new Function8Field(); }
    };

    class Function8Field extends VecFunction {
	String getName() { return "sin(r2)/r2"; }
	void getField(double result[], double y[]) {
	    double r2 = 23*(y[0]*y[0]+y[1]*y[1])+.00000001;
	    result[2] = java.lang.Math.sin(r2)/r2;
	    double r= java.lang.Math.sqrt(r2);
	    double sinr2 = java.lang.Math.sin(r2);
	    double cosr2 = java.lang.Math.cos(r2);
	    double r4 = r2*r2;
	    double vecR = (sinr2/r4-cosr2/r2)*.01;
	    result[0] = y[0]*vecR;
	    result[1] = y[1]*vecR;
	}
	VecFunction createNext() { return new UserDefinedPotential(); }
    };

    class UserDefinedPotential extends VecFunction {
	Expr expr;
	double y0[];
	String getName() { return "user-defined potential"; }
	void setup() {
	    textFields[0].setText("x*x");
	    textFields[0].show();
	    textFieldLabel.setText("Potential Function");
	    textFieldLabel.show();
	    actionPerformed();
	    y0 = new double[3];
	}
	void actionPerformed() {
	    parseError = false;
	    ExprParser ep = new ExprParser(textFields[0].getText());
	    expr = ep.parseExpression();
	    if (ep.gotError())
		parseError = true;
	    functionChanged = true;
	}
	void getField(double result[], double y[]) {
	    double k = .00001;
	    int i;
	    for (i = 0; i != 3; i++)
		y0[i] = y[i];
	    double pot0 = expr.eval(y0);

	    y0[0] += k;
	    result[0] = pot0-expr.eval(y0);
	    y0[0] = y[0];

	    y0[1] += k;
	    result[1] = pot0-expr.eval(y0);
	    y0[1] = y[1];
	    
	    result[2] = pot0*.01;
	    for (i = 0; i != 2; i++)
		if (!(result[i] > -10 && result[i] < 10))
		    boundCheck = true;
	}
	VecFunction createNext() { return new UserDefinedFunction(); }
    };
    class UserDefinedFunction extends VecFunction {
	Expr exprs[];
	String getName() { return "user-defined field"; }
	void setup() {
	    exprs = new Expr[3];
	    textFields[0].setText("x");
	    textFields[1].setText("y");
	    textFieldLabel.setText("Field Functions");
	    textFieldLabel.show();
	    int i;
	    for (i = 0; i != 2; i++)
		textFields[i].show();
	    actionPerformed();
	}
	void actionPerformed() {
	    int i;
	    parseError = false;
	    for (i = 0; i != 2; i++) {
		ExprParser ep = new ExprParser(textFields[i].getText());
		exprs[i] = ep.parseExpression();
		if (ep.gotError())
		    parseError = true;
	    }
	    functionChanged = true;
	}
	void getField(double result[], double y[]) {
	    double k = .0002;
	    int i;
	    for (i = 0; i != 2; i++) {
		result[i] = k*exprs[i].eval(y);
		if (!(result[i] > -10 && result[i] < 10))
		    boundCheck = true;
	    }
	    result[2] = 0;
	}
	boolean nonGradient() { return true; }
	VecFunction createNext() { return null; }
    };
#endif

    class DrawData {
	public Graphics g;
	public double mult;
	public double field[], vv[];
    };

    class Particle {
	public double pos[];
	public double vel[];
	public double lifetime;
	public double phi, theta, phiv, thetav;
	public double stepsize;
	public Color color;
	Particle() {
	    pos = new double[3]; vel = new double[3];
	    stepsize = 1;
	}
    };

    class FieldVector {
	public int sx1, sy1, sx2, sy2;
	public double p1[], p2[];
	public int col;
	public int viewPri;
    };

    class GridElement {
	public double height, div, curl, normdot, vecX, vecY;
	public boolean visible, valid;
    };

#ifdef BUILD_E    
    // this is a complex number class that avoids creating new objects
    // whenever possible.  Takes a little more effort to use, but it is
    // much faster because we don't have to do constant garbage
    // collection.
    class Complex {
	public double a, b;
	Complex() { a = b = 0; }
	void set(double aa, double bb) { a = aa; b = bb; }
	void set(Complex c) { set(c.a, c.b); }
	void add(double r) { a += r; }
	void add(double r, double i) { a += r; b += i; }
	void square() { set(a*a-b*b, 2*a*b); }
	void mult(double c, double d) { set(a*c-b*d, a*d+b*c); }
	void mult(double c) { a *= c; b *= c; }
	void mult(Complex c) { mult(c.a, c.b); }
	void recip() {
	    double n = a*a+b*b;
	    set(a/n, -b/n);
	}
	void pow(double p) {
	    double arg = java.lang.Math.atan2(b, a);
	    arg *= p;
	    double abs = java.lang.Math.pow(a*a+b*b, p*.5);
	    set(abs*java.lang.Math.cos(arg),
		abs*java.lang.Math.sin(arg));
	}
	void sin() {
	    set(cosh(b)*java.lang.Math.sin(a),
		java.lang.Math.cos(a)*sinh(b));
	}
	void cos() {
	    set(cosh(b)*java.lang.Math.cos(a),
		java.lang.Math.sin(a)*sinh(b));
	}
	void log() {
	    set(java.lang.Math.log(a*a+b*b),
		java.lang.Math.atan2(b, a));
	}
	void arcsin() {
	    Complex z2 = new Complex();
	    z2.set(a, b);
	    z2.square();
	    z2.mult(-1);
	    z2.add(1);
	    z2.pow(.5);
	    mult(0, 1);
	    add(z2.a, z2.b);
	    log();
	    mult(0, -1);
	}
    };

    double cosh(double a) {
	return .5*(java.lang.Math.exp(a)+java.lang.Math.exp(-a));
    }
    double sinh(double a) {
	return .5*(java.lang.Math.exp(a)-java.lang.Math.exp(-a));
    }
#endif
};

#ifdef BUILD_V
class ExprState {
    public double x, y, z;
}

class Expr {
    Expr(Expr e1, Expr e2, int v) {
	left = e1;
	right = e2;
	type = v;
    }
    Expr(int v, double vv) {
	type = v;
	value = vv;
    }
    Expr(int v) {
	type = v;
    }
    double eval(double es[]) {
	switch (type) {
	case E_ADD: return left.eval(es)+right.eval(es);
	case E_SUB: return left.eval(es)-right.eval(es);
	case E_MUL: return left.eval(es)*right.eval(es);
	case E_DIV: return left.eval(es)/right.eval(es);
	case E_POW: return java.lang.Math.pow(left.eval(es), right.eval(es));
	case E_UMINUS: return -left.eval(es);
	case E_VAL: return value;
	case E_X:   return es[0]*10;
	case E_Y:   return es[1]*10;
	case E_R:   return java.lang.Math.sqrt(
			      es[0]*es[0]+es[1]*es[1])*10;
	case E_SIN: return java.lang.Math.sin(left.eval(es));
	case E_COS: return java.lang.Math.cos(left.eval(es));
	case E_ABS: return java.lang.Math.abs(left.eval(es));
	case E_EXP: return java.lang.Math.exp(left.eval(es));
	case E_LOG: return java.lang.Math.log(left.eval(es));
	case E_SQRT: return java.lang.Math.sqrt(left.eval(es));
	case E_TAN: return java.lang.Math.tan(left.eval(es));
	default: System.out.print("unknown\n");
	}
	return 0;
    }
    Expr left, right;
    double value;
    int type;
    static final int E_ADD = 1;
    static final int E_SUB = 2;
    static final int E_X = 3;
    static final int E_Y = 4;
    static final int E_Z = 5;
    static final int E_VAL = 6;
    static final int E_MUL = 7;
    static final int E_DIV = 8;
    static final int E_POW = 9;
    static final int E_UMINUS = 10;
    static final int E_SIN = 11;
    static final int E_COS = 12;
    static final int E_ABS = 13;
    static final int E_EXP = 14;
    static final int E_LOG = 15;
    static final int E_SQRT = 16;
    static final int E_TAN = 17;
    static final int E_R = 18;
};

class ExprParser {
    String text;
    String token;
    int pos;
    int tlen;
    boolean err;

    void getToken() {
	while (pos < tlen && text.charAt(pos) == ' ')
	    pos++;
	if (pos == tlen) {
	    token = "";
	    return;
	}
	int i = pos;
	int c = text.charAt(i);
	if ((c >= '0' && c <= '9') || c == '.') {
	    for (i = pos; i != tlen; i++) {
		if (!((text.charAt(i) >= '0' && text.charAt(i) <= '9') ||
		      text.charAt(i) == '.'))
		    break;
	    }
	} else if (c >= 'a' && c <= 'z') {
	    for (i = pos; i != tlen; i++) {
		if (!(text.charAt(i) >= 'a' && text.charAt(i) <= 'z'))
		    break;
	    }
	} else {
	    i++;
	}
	token = text.substring(pos, i);
	pos = i;
    }

    boolean skip(String s) {
	if (token.compareTo(s) != 0)
	    return false;
	getToken();
	return true;
    }

    void skipOrError(String s) {
	if (!skip(s))
	    err = true;
    }

    Expr parseExpression() {
	if (token.length() == 0)
	    return new Expr(Expr.E_VAL, 0.);
	Expr e = parse();
	if (token.length() > 0)
	    err = true;
	return e;
    }

    Expr parse() {
	Expr e = parseMult();
	while (true) {
	    if (skip("+"))
		e = new Expr(e, parseMult(), Expr.E_ADD);
	    else if (skip("-"))
		e = new Expr(e, parseMult(), Expr.E_SUB);
	    else
		break;
	}
	return e;
    }

    Expr parseMult() {
	Expr e = parseUminus();
	while (true) {
	    if (skip("*"))
		e = new Expr(e, parseUminus(), Expr.E_MUL);
	    else if (skip("/"))
		e = new Expr(e, parseUminus(), Expr.E_DIV);
	    else
		break;
	}
	return e;
    }

    Expr parseUminus() {
	skip("+");
	if (skip("-"))
	    return new Expr(parsePow(), null, Expr.E_UMINUS);
	return parsePow();
    }

    Expr parsePow() {
	Expr e = parseTerm();
	while (true) {
	    if (skip("^"))
		e = new Expr(e, parseTerm(), Expr.E_POW);
	    else
		break;
	}
	return e;
    }

    Expr parseFunc(int t) {
	skipOrError("(");
	Expr e = parse();
	skipOrError(")");
	return new Expr(e, null, t);
    }

    Expr parseTerm() {
	if (skip("(")) {
	    Expr e = parse();
	    skipOrError(")");
	    return e;
	}
	if (skip("x"))
	    return new Expr(Expr.E_X);
	if (skip("y"))
	    return new Expr(Expr.E_Y);
	if (skip("r"))
	    return new Expr(Expr.E_R);
	if (skip("pi"))
	    return new Expr(Expr.E_VAL, 3.14159265358979323846);
	if (skip("e"))
	    return new Expr(Expr.E_VAL, 2.7182818284590452354);
	if (skip("sin"))
	    return parseFunc(Expr.E_SIN);
	if (skip("cos"))
	    return parseFunc(Expr.E_COS);
	if (skip("abs"))
	    return parseFunc(Expr.E_ABS);
	if (skip("exp"))
	    return parseFunc(Expr.E_EXP);
	if (skip("log"))
	    return parseFunc(Expr.E_LOG);
	if (skip("sqrt"))
	    return parseFunc(Expr.E_SQRT);
	if (skip("tan"))
	    return parseFunc(Expr.E_TAN);
	try {
	    Expr e = new Expr(Expr.E_VAL, Double.valueOf(token).doubleValue());
	    getToken();
	    return e;
	} catch (Exception e) {
	    err = true;
	    System.out.print("unrecognized token: " + token + "\n");
	    return new Expr(Expr.E_VAL, 0);
	}
    }

    ExprParser(String s) {
	text = s;
	tlen = text.length();
	pos = 0;
	err = false;
	getToken();
    }
    boolean gotError() { return err; }
};
#endif


interface DecentScrollbarListener {
    abstract void scrollbarValueChanged(DecentScrollbar ds);
    abstract void scrollbarFinished(DecentScrollbar dc);
}

// this is a scrollbar that notifies us when the user is _done_ fiddling
// with the value.
class DecentScrollbar extends Canvas
  implements MouseListener, MouseMotionListener {
    int value, lo, hi;
    DecentScrollbarListener listener;
    DecentScrollbar(DecentScrollbarListener parent, 
		    int start, int lo_, int hi_) {
	value = start;
	lo = lo_;
	hi = hi_;
	listener = parent;
	addMouseListener(this);
	addMouseMotionListener(this);
	gray1 = new Color(104, 104, 104);
	gray2 = new Color(168, 168, 168);
	gray3 = new Color(192, 192, 192);
	gray4 = new Color(224, 224, 224);
    }
    Color gray1, gray2, gray3, gray4;
    public Dimension getPreferredSize() {
	return new Dimension(20,20);
    }
    static final int tw = 8;
    boolean dragging;
    int thumbpos, dragoffset;
    public void paint(Graphics g) {
	Dimension size = getSize();
	int w = size.width;
	int h = size.height;
	int x = thumbpos = (value-lo)*(w-2-tw)/(hi-lo)+1;
	g.setColor(gray2);
	g.fillRect(0, 0, w, h);
	g.setColor(gray3);
	g.fillRect(x, 2, tw, h-4);
	g.setColor(gray4);
	g.drawLine(0, h-1, w, h-1);
	g.drawLine(w-1, 1, w-1, h-1);
	g.drawLine(x, 1, x+tw-1, 1);
	g.drawLine(x, 1, x, h-2);
	g.setColor(gray1);
	g.drawLine(0, 0, w-1, 0);
	g.drawLine(0, 0, 0, h-1);
	g.drawLine(x+tw-1, 2, x+tw-1, h-2);
	g.drawLine(x+1, h-2, x+tw-1, h-2);
    }
    int getValue() { return value; }
    boolean setValue(int v) {
	if (v < lo)
	    v = lo;
	if (v > hi)
	    v = hi;
	if (value == v)
	    return false;
	value = v;
	repaint();
	return true;
    }
    public void mousePressed(MouseEvent e) {
	if (thumbpos <= e.getX() && thumbpos+tw >= e.getX()) {
	    dragging = true;
	    dragoffset = e.getX()-thumbpos;
	}
    }
    public void mouseReleased(MouseEvent e) {
	if (dragging)
	    listener.scrollbarFinished(this);
	dragging = false;
    }
    public void mouseClicked(MouseEvent e) {
    }
    public void mouseEntered(MouseEvent e) {
    }
    public void mouseExited(MouseEvent e) {
    }
    public void mouseDragged(MouseEvent e) {
	if (!dragging)
	    return;
	int x = e.getX()-dragoffset;
	Dimension size = getSize();
	int v = (x-1)*(hi-lo)/(size.width-2-tw)+lo;
	if (setValue(v))
	    listener.scrollbarValueChanged(this);
    }
    public void mouseMoved(MouseEvent e) {
    }
};
