/** * PositionGraph.java * This object displays the current position of a Projectile. * Andrew Dolgert for Michael Fowler 12 August 1998 */ import java.applet.*; import java.awt.*; import java.awt.image.*; // for the label business far below. import java.text.NumberFormat; import java.text.DecimalFormat; import java.util.*; import java.text.ParseException; // There won't be any threading here. The only trick in this graph panel is // double buffering. // This graph extends Panel so it can be a component of the main applet. // By implementing Projectile.ProjectileListener, it is able to accept messages // from Projectile. public class PositionGraph extends Panel implements Projectile.ProjectileListener { Projectile dart; // We record the projectile we are supposed to work with. Image buffer; // The buffer and its Graphics component, bufferGraphics, are our back buffer. Dimension size; // The current size of the panel. Saved so we know when the size has changed. Graphics bufferGraphics; ResourceBundle rb; String sDistance, sHeight, sVelocity, sTime, sMeters, sMPS, sSec; String sXAxis, sYAxis; // a DrawableAxis is a home brew object that draws an axis. DrawableAxis ordinate, abscissa; // This is the pixel location of the origin of the axes. Point origin; // How long the x-axis needs to be. This changes when you change the max velocity. double maxDistance = 280.0; // -v0^2/g // The changeSize() method decides the length of arms of the axes. int graphLengthX, graphLengthY; // This scales between distance in meters and distance in pixels. double scaleDistance; // The current position of the projectile. int dartX, dartY; double realX, realY; // The current position of the projectile in meters. // The next place we will draw projectile (in pixels). int newDartX, newDartY; // This rectangle is the white area inside the axes. We record this for // redraws. If the area we need to redraw is outside the main area of the // graph, then we need to take time to redraw the axes. Rectangle graphArea; boolean exists; // Whether there is any projectile. It is false before the first shot. boolean bPleaseRedraw; // Whether to redraw the whole screen. boolean bWantTrails; // Whether we want trails for the next shot. boolean bTrails; // Whether to draw trails behind balls for this shot. boolean bStatistics; // At the end of a shot and before the next shot, we put shot stats in the corner. // These are those shot stats. double endDistance, maxHeight, endVelocity, endTime; // Each shot gets a new color from this list. static final Color[] colorList = {Color.red, Color.blue, Color.magenta, Color.orange, Color.green}; // currentColor is just that. newColor is the next color we will use. They need to be different. // The newColor gets chosen after a shot hits the ground b/c you can interrupt a shot in midair // with another shot, and that other shot should be the same color. But we don't change the currentColor // until the next shot is fired b/c we still need to draw the projectile on the ground in its correct // color. int currentColor, newColor; // This vector will hold information about every shot whose trail we want to show. // We save the last maxShots trails. We save this list in addition to the pixel positions // of the trails themselves in case the window gets resized. Vector shotList; static final int maxShots = 5; // Initializer public PositionGraph(Projectile vomit, ResourceBundle rBundle) { // There is an implicit call to super() here to initialize the Panel. dart = vomit; bWantTrails = true; changeSize(); // every time our size may change, we re-initialize. dart.addListener(this); // Tell the Projectile dart to inform us of shots fired, postions, and hits. exists = false; // There is no projectile until the first shot. bStatistics = false; // There are no statistics until a shot hits the ground. currentColor = newColor = 0; // The first color is red. shotList = new Vector(maxShots); // This shotList is available to save past shots for trails. rb = rBundle; if (rb != null) { sDistance = rb.getString("distance"); sHeight = rb.getString("height"); sVelocity = rb.getString("endvelocity"); sTime = rb.getString("time"); sMeters = rb.getString("meters"); sMPS = rb.getString("meterspersecond"); sSec = rb.getString("seconds"); sXAxis = rb.getString("xaxis"); sYAxis = rb.getString("yaxis"); } else { sDistance = "Max distance: "; sHeight = "Max height: "; sVelocity = "End velocity: "; sTime = "Total time: "; sMeters = "m"; sMPS = "m/s"; sSec = "s"; sXAxis = "Distance [m]"; sYAxis = "Height [m]"; } } // This is where we draw it all. We draw first onto the back buffer, bufferGraphics, then // copy that buffer to the screen. public void paint(Graphics g) { // If the size of the panel has changed, re-initialize with a method below. // size is a Dimension storing the previously known size. if (!size.equals(this.getSize())) changeSize(); // Set our basic font for the axes. //Font f = new Font("SansSerif",Font.PLAIN,10); //bufferGraphics.setFont(f); // When the system calls paint, it sets a clipping rectangle which contains only the // part of the panel it thinks needs to be updated. Our drawing operations in the // background buffer should be faster if we set the clipping rectangle in the background // buffer to be the same as that of the main panel. Rectangle trect = g.getClipBounds(); bufferGraphics.setClip(trect); bufferGraphics.setColor(Color.white); // graphArea is a rectangle that goes from the origin of the axes to the upper right corner // of the panel. If the lower left point of the clipping rectangle is inside the graphArea, // we don't need to do a bunch of time consuming tasks like draw the axes and labels. if (!graphArea.contains(trect.x,trect.y+trect.height)) { // To redraw the panel, first clear it, then draw the axes. bufferGraphics.fillRect(0,0,size.width,size.height); bufferGraphics.setColor(Color.black); ordinate.draw(bufferGraphics,origin.x,origin.y); abscissa.draw(bufferGraphics,origin.x, origin.y); } else { bufferGraphics.fillRect(graphArea.x,graphArea.y,graphArea.width,graphArea.height); } if (bTrails) { // If there are trails, loop through the saved shots and draw them on the screen. shotSummary sS; for (int j=0; jmaxShots-1) currentColor = 0; setPosition(dartX,dartY,0,0,0,false); if (bWantTrails) bTrails = true; else bTrails = false; // Erase any trails just in case someone just turned off trails. if (!bTrails) shotList.setSize(0); else { if (shotList.size() > maxShots-1) { shotList.removeElementAt(0); } shotList.addElement(new shotSummary(shotStats,colorList[currentColor])); } double firemax = velocity*velocity/9.81; firemax = 1.1*firemax; // Check whether the new size is much different from the current one. if (firemax>maxDistance || firemax<0.4*maxDistance) { maxDistance = firemax; // Here we change the axis size. changeSize automatically repaints. changeSize(); } else { // Repaint the whole screen for the new shot b/c the axes may have changed. bPleaseRedraw = true; repaint(); } } // Projectile calls this every time it moves the projectile. // We will also call this when the screen is resized in order to make sure // the pixel position of the ball agrees with new axes. // mark tells us whether this is an update for trails of balls. Time is the current time. public void setPosition(double x, double y, double vx, double vy,double time,boolean mark) { if (mark) { int tx, ty; if (!bTrails) return; // System.out.println("mark at "+x+", "+y+" t "+time); tx = (int) (x*scaleDistance+origin.x); ty = (int) (origin.y-y*scaleDistance); ((shotSummary) shotList.lastElement()).addPoint(tx,ty,time); return; } // We need to know what part of the screen to erase, so first save // the previous location of the ball. Rectangle oldRect = new Rectangle(dartX-3,dartY-3,9,9); realX = x; realY = y; // These are the new ball location in meters. // Convert from meters to pixel location for the current graph. newDartX = (int) (x*scaleDistance+origin.x); newDartY = (int) (origin.y-y*scaleDistance); // Find what pixels are affected. Rectangle newRect = new Rectangle(newDartX-3,newDartY-3,9,9); // The clipping rectangle will be the union of pixels to destroy an // pixels to draw. Setting this clipping rectangle will speed // drawing considerably. Rectangle theRect = oldRect.union(newRect); this.repaint(theRect.x,theRect.y,theRect.width,theRect.height); } // Projectile calls this when a shot hits. Look at projectile to see what these // things are. public void endFiring(double[] endStats) { endDistance = endStats[0]; endVelocity = endStats[1]; maxHeight = endStats[2]; endTime = endStats[3]; bStatistics = true; // We have stats. Tell paint() to display them. bPleaseRedraw = true; repaint(); // repaint the whole screen. } // This is a subclass of PositionGraph. It is a new feature in java 1.1. I could make // it another class, but it is private to this graph, which is good. private class shotSummary { // Shotstats is a length 5 vector with (kappa,gamma, vxScale, vyScale, tScale). public double[] shotStats; public Color currentColor; public int[][] shotPoints; // array of trail points public double[] shotTimes; // array of times of those points (for reconstruction if need be). public int iPoints; // current number of points. private int maxPoints; // max allocation available. public shotSummary(double[] sS, Color c) { shotStats = sS; currentColor = c; maxPoints = 10; // why not start with ten? shotPoints = new int[maxPoints][2]; shotTimes = new double[maxPoints]; iPoints = 0; } // x and y are the current pixel positions and t is the real time in m/s. public void addPoint(int x, int y, double t) { if (iPoints>maxPoints-1) { maxPoints += 10; int[][] tp = new int[maxPoints][2]; double[] tt = new double[maxPoints]; for (int i=0; i graphLengthY*2) scaleDistance = 2*graphLengthY/maxDistance; else scaleDistance = graphLengthX/maxDistance; // Create a new axis. DrawableAxis is my own routine. Font f = new Font("SansSerif",Font.PLAIN,10); ordinate = new DrawableAxis(graphLengthX,DrawableAxis.HORIZONTAL, 0,graphLengthX/scaleDistance,f); ordinate.setLabel(sXAxis, new Font("SansSerif",Font.BOLD,12)); abscissa = new DrawableAxis(graphLengthY,DrawableAxis.VERTICAL, 0,graphLengthY/scaleDistance,f); makeLabelForAxis(abscissa,sYAxis,new Font("SansSerif",Font.BOLD,12)); // Set the position of the Projectile if it exists. if (exists) setPosition(realX,realY,0,0,0,false); // Calculate the new pixel positions of trails from their shotList descriptions. if (bTrails) calculateTrails(); bPleaseRedraw = true; repaint(); // Paint it all. } // This subroutine makes an image for the drawableaxis to use as a label. // We do it here instead of inside the axis because the axis won't allow // me to create an image. Only components with a nonzero clipping region // can create an image, and the DrawableAxis is never officially shown on the // screen. The real solution is to make the DrawableAxis a lightweight // component (and the labels, and the floating statistics bar, and the // graph points themselves) and put it all in its own GraphLayoutManager. // It's a battle for another day. private void makeLabelForAxis(DrawableAxis da,String sLabel,Font labelFont) { FontMetrics lfm = getToolkit().getFontMetrics(labelFont); int width = lfm.stringWidth(sLabel); //System.out.println("width "+width+" height "+lfm.getLeading()); // the height should be getHeight()+getLeading(), but getLeading is 0. Image ti = createImage(width,lfm.getHeight()+5); Graphics tb = ti.getGraphics(); tb.setFont(labelFont); tb.drawString(sLabel,0,lfm.getAscent()); // Is the ImageProducer this Canvas? ImageProducer ip = ti.getSource(); // The RotFilter is mine. It rotates an image 90 degrees. RotFilter rf = new RotFilter(); ip = new FilteredImageSource(ip,rf); Image rotatedImage = Toolkit.getDefaultToolkit().createImage(ip); da.setLabel(rotatedImage); } // shotList has initial value information for past shots. here we recalculate // pixel locations along the trails for the current coordinate system. private void calculateTrails() { double t,ttotal, kap,gam,vx0,vy0,t0; double[] retLocs; shotSummary tSum; // loop over all shots whose trails we stored. for (int j=0; j