/* File: Simulation.java Contains basic classes for the physics lab simulation */ /* Part of the www.MyPhysicsLab.com physics simulation applet. Copyright (c) 2001 Erik Neumann This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Contact Erik Neumann at erikn@MyPhysicsLab.com or 610 N. 65th St. Seattle WA 98103 */ import java.util.Vector; import java.awt.*; import java.awt.event.*; import java.applet.*; import java.text.DecimalFormat; import java.text.NumberFormat; ///////////////////////////////////////////////////////////////////////////// // CoordMap class /* Provides the coordinate mapping between screen and simulation world. Calculates the scaling and origin coordinates. QUICK GUIDE: new CoordMap(int y_dir, double x1, double x2, double y1, double y2, int align_x, int align_y) x1,x2,y1,y2 give the simulation coords of simulation area's L,R,T,B This is different from the screen coords!!! We assure that the rect x1,x2,y1,y2 fits in the screen area, but there is usually some excess area. So you can find the actual screen boundaries in simulation coords, but it is better to stick to using the simulation area -- because the screen area can change due to resizing of the window. After a resize, we are guaranteed to have the same simulation area showing in the window. If you want to use the entire screen area, try using the expand() method to grow the simulation area. Then you can use the whole screen area, but if there is any later resizing of the window your simulation area won't change. y_dir is either CoordMap.INCREASE_UP or CoordMap.INCREASE_DOWN Note that always y10) && (height>0)) { screen_top = top; screen_left = left; screen_width = width; screen_height = height; recalc(); //pvars("setScreen"); } } public int simToScreenScaleX(double x) { /* does only scaling in x direction, no offsetting */ return (int)(x*pixel_per_unit_x+0.5); } public int simToScreenX(double x) { /* Returns the screen coords of x, given the various globals. */ return origin_x + (int)(x*pixel_per_unit_x+0.5); } public int simToScreenY(double y) { /* Returns the screen coords of y, given the various globals. */ return origin_y + y_direction*(int)(y*pixel_per_unit_y+0.5); } public double screenToSimX(int scr_x) { /* scr_x is in screen coords, returns simulation coords */ return (double)(scr_x - origin_x)/pixel_per_unit_x; } public double screenToSimY(int scr_y) { /* scr_y is in screen coords, returns simulation coords */ return y_direction*(double)(scr_y - origin_y)/pixel_per_unit_y; } /* 'Box' is the simulation area defined by sim_x1, sim_x2, sim_y1, sim_y2. The following methods return the screen coords of the box. */ public int leftBox() { return boxLeft; } public int rightBox() { return boxRight; } public int topBox() { return boxTop; } public int bottomBox() { return boxBottom; } public int widthBox() { return boxWidth; } public int heightBox() { return boxHeight; } public boolean intersectRect(Rectangle r) { int x1, y1, x2, y2; x1 = r.x; y1 = r.y; x2 = x1 + r.width; y2 = y1 + r.height; int sx1 = screen_left; int sy1 = screen_top; int sx2 = screen_left + screen_width; int sy2 = screen_top + screen_height; if (sx1 >= x2) return false; if (x1 >= sx2) return false; if (sy1 >= y2) return false; if (y1 >= sy2) return false; //System.out.println(x1+" "+y1+" "+x2+" "+y2+" "+sx1+" "+sy1+" "+sx2+" "+sy2); return true; } } ///////////////////////////////////////////////////////////////////////////// // CElement class class CElement { // class constants public static final int MODE_RECT = 1; public static final int MODE_CIRCLE = 2; public static final int MODE_SPRING = 3; public static final int MODE_LINE = 4; public static final int MODE_CIRCLE_FILLED = 5; /* About bounds (m_X1, m_Y1, m_X2, m_Y2) It should always be true that m_X1 < m_X2 and m_Y1 < m_Y2 Therefore m_X1, m_Y1 is either top-left or bottom-left depending on whether the CoordMap has INCREASE_UP or INCREASE_DOWN set. */ public double m_X1, m_Y1, m_X2, m_Y2; // bounds (simulation coords) /* ix & ivx are for collision checking */ public int ix = -1; //index of position in array of vars public int ivx = -1; //index of velocity in array of vars public int m_DrawMode; // drawing mode (rect, circle, ...) public double m_Mass; // the mass in kilograms public boolean m_Draggable = false; // whether it is mouse draggable public Color m_Color; public CElement () { m_X1 = m_Y1 = m_X2 = m_Y2 = 0; m_DrawMode = 0; m_Mass = 0; m_Color = Color.black; } public CElement (double X1, double Y1, double X2, double Y2) { m_X1 = X1; m_Y1 = Y1; m_X2 = X2; m_Y2 = Y2; m_DrawMode = 0; m_Mass = 0; m_Color = Color.black; } /* Distance is meant to give the distance squared from the "center" of the object to the given x, y point */ public double distanceSquared(double x, double y) { double dx = (m_X2 + m_X1)/2 - x; double dy = (m_Y2 + m_Y1)/2 - y; return dx*dx+dy*dy; } public boolean hitCheck(double x, double y) { if (m_Draggable && ((x>=m_X1) && (x<=m_X2) && (y>=m_Y1) && (y<=m_Y2))) return true; else return false; } public void setX1(double p) { m_X1 = p; } public void setX2(double p) { m_X2 = p; } public void setY1(double p) { m_Y1 = p; } public void setY2(double p) { m_Y2 = p; } public void draw (Graphics g, CoordMap map) { } public double getCenterX() { return (m_X1 + m_X2)/2; } public double getCenterY() { return (m_Y1 + m_Y2)/2; } } ///////////////////////////////////////////////////////////////////////////// // CMass class class CMass extends CElement { public double m_Height; public double m_Width; public double m_Damping = 0; // slowing from friction, viscosity, etc. public double m_Elasticity = 1; // bounciness, from 0 (blob) to 1 (superball) /* public CMass() { super(0, 0, 1, 1); m_Height = 1; m_Width = 1; } */ public CMass (double X1, double Y1, double width, double height, int drawMode) { super(X1, Y1, X1+width, Y1+height); m_Height = height; m_Width = width; m_Mass = 1; m_DrawMode = drawMode; m_Draggable = true; m_Color = Color.red; } public void setX1(double Xpos) { m_X1 = Xpos; m_X2 = Xpos + m_Width; } public void setY1(double Ypos) { m_Y1 = Ypos; m_Y2 = Ypos + m_Height; } public void setCenterX(double Xpos) { double w = m_Width/2; m_X1 = Xpos - w; m_X2 = Xpos + w; } public void setCenterY(double Ypos) { double w = m_Width/2; m_Y1 = Ypos - w; m_Y2 = Ypos + w; } public void draw (Graphics g, CoordMap map) { int x1, y1, x2, y2; x1 = map.simToScreenX(m_X1); y1 = map.simToScreenY(m_Y1); x2 = map.simToScreenX(m_X2); y2 = map.simToScreenY(m_Y2); if (y2 < y1) // adjust for INCREASE_UP mode { int d = y2; y2 = y1; y1 = d; } g.setColor(m_Color); switch (m_DrawMode) { case CElement.MODE_RECT: g.drawRect(x1, y1, x2-x1, y2-y1); break; case CElement.MODE_CIRCLE: g.drawOval(x1, y1, x2-x1, y2-y1); break; case CElement.MODE_CIRCLE_FILLED: g.fillOval(x1, y1, x2-x1, y2-y1); break; //default: // throw "unknown draw mode"; } } } ///////////////////////////////////////////////////////////////////////////// // CArc class class CArc extends CElement { // X1, Y1 are the center of the arc // X2, Y2 will be calculated if necessary? public double m_Radius; public double m_Angle; // degrees public double m_StartAngle; // where the curve starts from; 0 = "east" public double m_HeadLength = 0.2; // length of arrowhead public CArc (double X1, double Y1, double r, double angle0, double angle) { super(X1, Y1, 0, 0); m_Radius = r; m_StartAngle = angle0; m_Angle = angle; m_Draggable = false; } public void setX1(double Xpos) { m_X1 = Xpos; } public void setY1(double Ypos) { m_Y1 = Ypos; } public void draw (Graphics g, CoordMap map) { int x1, y1, x2, y2, r; x1 = map.simToScreenX(m_X1); y1 = map.simToScreenY(m_Y1); r = map.simToScreenScaleX(m_Radius); // assumes x & y are scaled same! g.setColor(Color.black); /* parameters to drawArc are: x, y (top-left corner), width, height (bounding rect) startAngle (degrees, 0 = 3 o'clock), arcAngle (degrees relative to startAngle) */ int ang = (int)(m_Angle+0.5); // bug: seems that drawArc sometimes draws a circle for // angle = +1 or -1 degrees!!! // BUG: this bug is still happening for small radius in driven // pendulum (eg. radius 0.2). Seems to be related to small radius and // small angle at same time. To really check out this bug // would need to set up a test program where I can interactively // give static inputs to g.drawArc, and try to determine where the // bug is happening exactly. if (Math.abs(ang)>1) g.drawArc(x1-r, y1-r, 2*r, 2*r, (int)m_StartAngle, ang); if ((ang != 0) && (r > 0)) { // arrowhead // find tip of arrowhead double x,y; double a0, a1, a; // startangle & angle in radians a0 = Math.PI*(double)m_StartAngle/(double)180; a1 = Math.PI*(double)m_Angle/(double)180; a = -(a0 + a1); x = m_X1 + m_Radius*Math.cos(a); y = m_Y1 + m_Radius*Math.sin(a); double h = Math.min(m_HeadLength, 0.5*m_Radius); if (a1 > 0) h = -h; // find endpoint of first arrowhead, and draw it double xp, yp; xp = x + h*Math.cos(Math.PI/2 - a - Math.PI/6); yp = y - h*Math.sin(Math.PI/2 - a - Math.PI/6); x1 = map.simToScreenX(x); y1 = map.simToScreenY(y); x2 = map.simToScreenX(xp); y2 = map.simToScreenY(yp); g.drawLine(x1, y1, x2, y2); // find endpoint of 2nd arrowhead, and draw it xp = x + h*Math.cos(Math.PI/2 - a + Math.PI/6); yp = y - h*Math.sin(Math.PI/2 - a + Math.PI/6); x2 = map.simToScreenX(xp); y2 = map.simToScreenY(yp); g.drawLine(x1, y1, x2, y2); } } } ///////////////////////////////////////////////////////////////////////////// // CSpring class class CSpring extends CElement { public double m_RestLength = 1.0; // the unstretched (slack) length in meters public double m_Thickness = 0.5; // the thickness (width) of coil in meters public double m_SpringConst = 1.0; // the spring constant in Newtons/meter public Color m_Color2 = null; // expanded color public CSpring (double X1, double Y1, double restLen, double thick) { // X1, and Y1 are the attachment points of the left part of spring // default is horizontal spring at rest super(X1, Y1, X1+restLen, Y1); m_Thickness = thick; m_Mass = 0; m_SpringConst = 1; m_RestLength = restLen; m_DrawMode = CElement.MODE_SPRING; // default m_Color = Color.red; // compressed m_Color2 = Color.green; } public void draw (Graphics g, CoordMap map) { if (m_Color2 == null) m_Color2 = m_Color.brighter(); int cycles = 3; double x1=m_X1; double x2=m_X2; double y1=m_Y1; double y2=m_Y2; double cos_theta, sin_theta; // find angle of rotation // note: if x2==x1 then slope is infinite, which is a valid double { double theta=Math.atan((y2-y1)/(x2-x1)); if (x2 map.screen_left + map.screen_width) return false; if (scr_y < map.screen_top) return false; if (scr_y > map.screen_top + map.screen_height) return false; return true; } public boolean intersectRect(Rectangle r) { boolean t = map.intersectRect(r); /* if (t && !name.equalsIgnoreCase("sim")) { System.out.println(name+" intersectRect "+r.x+" "+r.y+" "+r.width+" "+r.height); System.out.println(name+" map rect "+map.screen_left+" "+map.screen_top+" "+map.screen_width+" "+map.screen_height); } */ return t; } public double get(int param) { return 0; } public void set(int param, double value) { } public void setScreen(int left, int top, int width, int height) { map.setScreen(left, top, width, height); } public void reset() { } public void connect(CCanvas c) { } } // end CCanvas class ////////////////////////////////////////////////////////////////// // CPhase class /* Phase graph... graphs one variable against another */ class CPhase extends CCanvas { public static final int DOTS = 0; public static final int LINES = 1; private CSimulation sim = null; private double last_x = -100000; // last position drawn in XOR private double last_y = -100000; private Font numFont = null; private FontMetrics numFM = null; private boolean dirty = true; // whether we need to redraw private boolean XOR_visible = false; // whether there is an XOR dot visible private int x_var = 0; // index of x variable in simulation's vars[] private int y_var = 1; // index of y variable in simulation's vars[] //private double x1 = -5; // lo range of x //private double x2 = 5; // hi range of x //private double y1 = -5; // lo range of y //private double y2 = 5; // hi range of y private int drawMode = LINES; private int dotSize = 1; private DecimalFormat df = null; private double axis_x, axis_y; // coord position of x & y axis private boolean auto_scale = true; private boolean range_set = false; private boolean range_dirty = false; private double range_x_hi, range_x_lo, range_y_hi, range_y_lo; private double range_time = 0; private static final int mem_len = 1500; private double[] mem_x = new double[mem_len]; // memory of x coords private double[] mem_y = new double[mem_len]; // memory of y coords private int mem_index = 0; // index for next entry in memory list private int mem_size = 0; // number of items in memory list /* how the circular memory list works: we write new entries into the array mem_x & mem_y until it fills. Then we wrap around and start writing to the beginning again. mem_index always points to the next location to write to. If mem_size < mem_len, then we have entries at 0,1,2,...,mem_index-1 If mem_size = mem_len, then the order of entries is: mem_index, mem_index+1, ..., mem_len-1, 0, 1, 2, ..., mem_index-1 */ public CPhase(Applet app) { super(app); // CoordMap inputs are direction, x1, x2, y1, y2, align_x, align_y map = new CoordMap(CoordMap.INCREASE_UP, -5, 5, -5, 5, CoordMap.ALIGN_MIDDLE, CoordMap.ALIGN_MIDDLE); map.setFillScreen(true); name = "phase"; df = new DecimalFormat("0.#"); range_time = (double)System.currentTimeMillis()/1000; } public void target(CSimulation s) { if (s == null) sim = null; // if target sim doesn't provide names, then don't connect to it else if (s.var_names == null) { System.out.println("graph unable to connect to sim"); sim = null; } else { sim = s; sim.addListener(this); setXVar(x_var); // defaults setYVar(y_var); } } public void disconnect() { sim.removeListener(this); } public void setXVar(int n) { x_var = n; if (sim == null) return; double[] r = sim.getRange(x_var); //x1 = r[0]; //x2 = r[1]; map.setRange(r[0], r[1], map.sim_y1, map.sim_y2); reset(); } public void setYVar(int n) { y_var = n; if (sim == null) return; double[] r = sim.getRange(y_var); //y1 = r[0]; //y2 = r[1]; map.setRange(map.sim_x1, map.sim_x2, r[0], r[1]); reset(); } public void setDrawMode(int mode, int size) { drawMode = mode; dotSize = size; } public void setAutoScale(boolean auto) { System.out.println("auto scale set to "+auto); auto_scale = auto; range_set = false; range_dirty = false; range_time = (double)System.currentTimeMillis()/1000; } public void repaint() { dirty = true; super.repaint(); } public void reset() { last_x = -100000; last_y = -100000; range_set = false; range_dirty = false; range_time = (double)System.currentTimeMillis()/1000; mem_index = mem_size = 0; // clear out the memory repaint(); } private void setFont(Graphics g) { if (numFont != null) return; numFont = new Font("SansSerif", Font.PLAIN, 10); numFM = g.getFontMetrics(numFont); } public void drawArrow(Graphics g, double x, double y, double th, double len) { // draw an arrow from sim point x,y at angle th of sim length len double x2, y2; x2 = x + len*Math.cos(th); y2 = y + len*Math.sin(th); g.setColor(Color.red); g.fillRect(map.simToScreenX(x),map.simToScreenY(y),2,2); g.setColor(Color.pink); g.drawLine(map.simToScreenX(x), map.simToScreenY(y), map.simToScreenX(x2), map.simToScreenY(y2)); } public void draw(Graphics g) { // copy from the offscreen buffer if we are up to date? XOR_visible = false; // XOR dot will be wiped out by this draw // only redraw into offscreen if our state is wrong (=dirty) // otherwise we will just update from the offscreen if (!dirty) return; super.draw(g); if (sim==null) { dirty = false; return; } // figure where to draw axes // NOTE: need to offset from left or top so that they are visible! double x0, y0; // sim coords of axes if ((map.sim_x1 > 0) || (map.sim_x2 < 0)) x0 = map.sim_x1 + 0.2; else x0 = 0; if ((map.sim_y1 > 0) || (map.sim_y2 < 0)) y0 = map.sim_y1 + 0.2; else y0 = 0; axis_x = x0; axis_y = y0; // draw horizontal axis, with different colors indicating shrink/zoom regions double span = map.sim_x2 - map.sim_x1; double a = map.sim_x1; g.setColor(Color.darkGray); g.drawLine(map.simToScreenX(a), map.simToScreenY(y0), map.simToScreenX(a + span), map.simToScreenY(y0)); // draw vertical axis, with different colors indicating shrink/zoom regions span = map.sim_y2 - map.sim_y1; a = map.sim_y1; g.setColor(Color.darkGray); g.drawLine(map.simToScreenX(x0), map.simToScreenY(a), map.simToScreenX(x0), map.simToScreenY(a + span)); // get a font setFont(g); g.setFont(numFont); // draw tick marks on horizontal axis // make scale*width be between 4 and 16 //double scale = 1; //int w = (int)Math.ceil(map.sim_width); //if ((w<4)||(w>16)) // scale = 8.0/w; //int numTick = (int)Math.floor(w*scale); //double tickWidth = w/(double)numTick; //int ti; // tick index // get y-coords of the tick int y1 = map.simToScreenY(y0) - 4; int y2 = y1 + 8; double incr = Math.ceil(map.sim_width/12); double x_sim = Math.ceil(map.sim_x1); while (x_sim < map.sim_x2) { int x_screen = map.simToScreenX(x_sim); g.setColor(Color.black); g.drawLine(x_screen,y1,x_screen,y2); // draw tick mark if (Math.abs(x_sim) > .001) // skip printing zero { g.setColor(Color.gray); String s; if (x_sim - Math.floor(x_sim) <.001) s = "" + (int)x_sim; // whole number else s = df.format(x_sim); // fraction //s = "" + x_sim; // fraction g.drawString(s, x_screen - 4, y2+10); } x_sim += incr; } // draw name of the horizontal axis //String hname = "position"; String hname = sim.var_names[x_var]; int w = numFM.stringWidth(hname); g.drawString(hname, map.simToScreenX(map.sim_x2) - w - 5, map.simToScreenY(y0) - 8); // draw tick marks on vertical axis // make scale*height be between 4 and 16 //scale = 1; //w = (int)Math.ceil(map.sim_height); //if ((w<4)||(w>16)) // scale = 8.0/w; //numTick = (int)Math.floor(w*scale); //tickWidth = w/(double)numTick; // get x-coords of the tick int x1 = map.simToScreenX(x0) - 4; int x2 = x1 + 8; incr = Math.ceil(map.sim_height/12); //System.out.println("graph incr "+incr); double y_sim = Math.ceil(map.sim_y1); while (y_sim < map.sim_y2) { int y_screen = map.simToScreenY(y_sim); g.setColor(Color.black); g.drawLine(x1,y_screen,x2,y_screen); if (Math.abs(y_sim) > .001) // skip printing zero { g.setColor(Color.gray); String s; if (y_sim - Math.floor(y_sim) <.001) s = "" + (int)y_sim; else s = "" + y_sim; g.drawString(s, x2+6, y_screen + 5); } y_sim += incr; } // draw name of the vertical axis String vname = sim.var_names[y_var]; w = numFM.stringWidth(vname); g.drawString(vname, map.simToScreenX(x0) + 6, map.simToScreenY(map.sim_y2) + 13); /* // draw vector field double dx, dy; // increments double x, y; int xi, yi; // counters double len = map.sim_width * 0.05; dx = map.sim_width/10; dy = map.sim_height/20; x = map.sim_x1; for (xi=0; xi<10; xi++) { y = map.sim_y1; for (yi=0; yi<20; yi++) { // need a way to communicate this equation from sim to graph! double th = Math.atan2(-(3.0/0.5)*(x-2.5) -0.2*y, y); drawArrow(g, x, y, th, len); y += dy; } x += dx; } */ // draw the memorized points // see note above on how the circular memory list works if (mem_size>0) { // make list of indices into memory list, in order from oldest to newest int[] ind = new int[mem_size]; int i; // find the first entry in the memory list if (mem_size < mem_len) { for (i=0; i -10000) { if (XOR_visible) // erase the last position { screen.setXORMode(Color.yellow); screen.fillRect(map.simToScreenX(last_x)-1, map.simToScreenY(last_y)-1, 4, 4); XOR_visible = false; } // draw a permanent pixel at last position screen.setPaintMode(); screen.setColor(Color.black); if (drawMode == DOTS) screen.fillRect(map.simToScreenX(last_x), map.simToScreenY(last_y), dotSize, dotSize); else screen.drawLine(map.simToScreenX(last_x), map.simToScreenY(last_y), map.simToScreenX(sim.vars[x_var]), map.simToScreenY(sim.vars[y_var])); // draw permanent pixel in offscreen also offscreen.setClip(map.screen_left, map.screen_top, map.screen_width, map.screen_height); offscreen.setColor(Color.black); if (drawMode == DOTS) offscreen.fillRect(map.simToScreenX(last_x), map.simToScreenY(last_y), dotSize, dotSize); else offscreen.drawLine(map.simToScreenX(last_x), map.simToScreenY(last_y), map.simToScreenX(sim.vars[x_var]), map.simToScreenY(sim.vars[y_var])); } // draw current position in XOR mode last_x = sim.vars[x_var]; last_y = sim.vars[y_var]; memorize(last_x, last_y); if (auto_scale) rangeCheck(last_x, last_y); screen.setXORMode(Color.yellow); screen.fillRect(map.simToScreenX(last_x)-1, map.simToScreenY(last_y)-1, 4, 4); screen.setPaintMode(); XOR_visible = true; } // remember these values in a big list private void memorize(double last_x, double last_y) { mem_x[mem_index] = last_x; mem_y[mem_index] = last_y; mem_index++; if (mem_size < mem_len) mem_size++; if (mem_index >= mem_len) // wrap around at end mem_index = 0; } // for auto-scaling, see if we need to expand the range of the graph private void rangeCheck(double last_x, double last_y) { if (!range_set) { range_x_hi = last_x; range_x_lo = last_x; range_y_hi = last_y; range_y_lo = last_y; range_set = true; } else { double xspan = range_x_hi - range_x_lo; double yspan = range_y_hi - range_y_lo; if (last_x <= range_x_lo) { range_x_lo = last_x - 0.10*xspan; range_dirty = true; } if (last_x >= range_x_hi) { range_x_hi = last_x + 0.10*xspan; range_dirty = true; } if (last_y <= range_y_lo) { range_y_lo = last_y - 0.10*yspan; range_dirty = true; } if (last_y >= range_y_hi) { range_y_hi = last_y + 0.10*yspan; range_dirty = true; } } double now = (double)System.currentTimeMillis()/1000; if (range_dirty && now > range_time + 5) { range_time = now; //System.out.println("old range: x "+map.sim_x1+" "+map.sim_x2+" y "+map.sim_y1+" "+map.sim_y2); map.setRange(range_x_lo, range_x_hi, range_y_lo, range_y_hi); //System.out.println("new range: x "+map.sim_x1+" "+map.sim_x2+" y "+map.sim_y1+" "+map.sim_y2); last_x = -100000; last_y = -100000; repaint(); range_dirty = false; } } public void mouseClicked(MouseEvent evt) { System.out.println("mouse click in phase "+evt.getX() + " "+evt.getY()); double sim_x = map.screenToSimX(evt.getX()); double sim_y = map.screenToSimY(evt.getY()); //sim.vars[x_var] = sim_x; //sim.vars[y_var] = sim_y; // did we click nearer to x or y axis? (in screen coords) int dx = Math.abs(evt.getX() - map.simToScreenX(axis_x)); int dy = Math.abs(evt.getY() - map.simToScreenY(axis_y)); reset(); // if canvas has a pointer to Lab, then could repaint here!!! } } // end CPhase class ////////////////////////////////////////////////////////////////// // CSimulation class /* */ class CSimulation extends CCanvas { public static final double NONSENSE = -999999.99; public static final int MAX_VARS = 40; // MOVE TO SUBCLASS!!! public static final int CLOCKS_PER_SEC = 1000; public double m_time = -1; // time of last simulation step public double sim_time = 0; // simulation time (cumulative) public double time_offset = 0; // offset of simtime from real-time public int numVars; // number of variables in vars[] public double[] vars; // array of variables used in the diffeqs public double[] old_vars; // (only used in modifyobjects) public boolean[] calc; // for each var, true if calc'ed, false if dragging public String[] var_names; // names of these variables public Vector m_oba; // object-array: contains elements (eg. mass, spring...) public boolean m_Animating; // set to false to halt animation public boolean initial_draw = true; // true during the first time drawing private CElement dragObj = null; private int offset_x = 0; private int offset_y = 0; public boolean m_debug = false; public CSimulation(Applet app) { super(app); m_oba = new Vector(10); numVars = 0; m_Animating = true; name = "sim"; vars = new double[CSimulation.MAX_VARS]; old_vars = new double[CSimulation.MAX_VARS]; calc = new boolean[CSimulation.MAX_VARS]; int i; for (i=0;i 0.25) h = 0.25; } if ((h<0) || (h>3)) { System.out.println("*** trouble with time step h = "+h+" ***"); h = 0.1; // pick a more reasonable number } // record time of this simulation step m_time = now; /* In this loop, we attempt to step directly forward by time h. If a collision happened over that time-step, then we back up and re-advance to the time of the collision. Continue this process until we have advanced the entire time-step by h. */ int ctr = 0; do { // save variables int i = numVars; while (i-- > 0) old_vars[i] = vars[i]; // advance vars by delta h solve(sim_time, h); sim_time += h; // collision? returns tc = estimated time of collision double tc = collisionTime(old_vars, h); if ((tc >= 0) && (tc < h)) { /* collision detected, so back up to that earlier time */ ctr++; // reset variables to start time i = numVars; while (i-- > 0) vars[i] = old_vars[i]; sim_time -= h; // advance to time of collision if (tc > 0) solve(sim_time, tc); else System.out.println("zero collision "+ctr); sim_time += tc; h -= tc; // adjust vars to reflect the collision adjustCollision(); } else // no collision, so done { h = 0; } } while (h>0); if (ctr > 1) System.out.println(ctr+" collisions"); // Modify the Element objects based on the new values in vars doModifyObjects(); } } public void adjustCollision() { } public void doModifyObjects() {} public double collisionTime(double[] ov, double h) { return 99999999; } public void startDrag(CElement e) { m_Animating = false; } public CElement hitCheckElements(double x, double y) { // Returns the draggable object that is NEAREST the specified point double distance = 1000000000; int ind = m_oba.size(); CElement nearest = null; CElement pElement; while (--ind >= 0) { pElement = (CElement)m_oba.elementAt(ind); if (pElement.m_Draggable) { double d = pElement.distanceSquared(x,y); if (distance > d) { distance = d; nearest = pElement; } } } return nearest; } /* set the given element to the given x & y position, subject to whatever constraints that the simulation wants to impose. */ void constrainedSet(CElement e, double x, double y) { } public void mousePressed(MouseEvent evt) { int scr_x = evt.getX(); // screen coords int scr_y = evt.getY(); // which object did mouse click on? double sim_x = map.screenToSimX(scr_x); // simulation coords double sim_y = map.screenToSimY(scr_y); dragObj = hitCheckElements(sim_x, sim_y); if (dragObj != null) { startDrag(dragObj); offset_x = scr_x - map.simToScreenX(dragObj.m_X1); offset_y = scr_y - map.simToScreenY(dragObj.m_Y1); } //System.out.println("mouse pressed at "+scr_x+" "+scr_y); } public void mouseDragged(MouseEvent evt) { if (dragObj != null) { double sim_x = map.screenToSimX(evt.getX() - offset_x); double sim_y = map.screenToSimY(evt.getY() - offset_y); // sim_x, sim_y should correspond to the new m_X1, m_Y1 for obj // let the simulation modify the object constrainedSet(dragObj, sim_x, sim_y); } //System.out.println("mouse dragged to "+x+" "+y); } public void mouseReleased(MouseEvent evt) { if (dragObj != null) { m_Animating = true; // turn animation on m_time = -1; // ERN: NEEDED? restarts simulation from current state // reset the globals used in animation resetVariables(dragObj); dragObj = null; // go back to RK calculation for all vars int i; for (i=0;i