//Trajectory.java Final major change: 5/26/99 Last update:5/28/99 /* *Program writen by Gary R. Dyrkacz * *Copyright (c) 1999, All rights reserved. * *This software may not be used for commercial purposes without direct consent *of the author. It may be used for personnel use at your own risk. It *has not been extensively tested. * * This program calculates the trajectory of a sphere, particularly a paintball. It take into account drag and spin forces. It uses Layout manager which has scrollbars, TextFields(editable and noneditable), three canvases for graphs. Calculations are updated more or less in real time. *Notes: I am a self taught programmer who dabbles in enough coding to do my professional work. (Part of the reason for undertaking this project at all.) Although not totally a beginner at programing, this was my first attempt at a Java or any OOP program. In that light, it was an overenthusiastic and overambitious first attempt to learn and use Java to write a a Trajectory calculator for paintballs. The idea was to write a program that would calculate, plot and display the data on the fly. The result, in my estimation, has been moderately successful, but there have been annoying compromises. One of the worst, is that the graphs needed to be replotted for each calculation update. Thus, as the calculation proceeds the painting slows down. For neophyte Java programmers looking for ideas how to juggle threads, different Layouts, updating scrollbars, updating textfields, and graphics there may be some helpful stuff here. (You do not want to know how many hours I spent reading three Java textbooks, looking at other peoples java sources, and scouring the java newsgroups via www.Deja.com.) I have extensively annotated this source. This is for myself more than for others, but you may find it useful. Be aware that this code may not be the most efficient or the most robust. I would dearly love to have a real Java programmer go over this, and show me the error of my ways. If you do find any of this helpful to you, please drop me a note at dyrgcmn@mcs.net. At least I will feel I wasn't alone stumbling over this stuff. To be honest, at this writing there are still some things that I did where the logic of why it must be that way eludes me. */ import java.awt.*; import java.applet.*; import java.awt.event.*; import java.awt.event.AdjustmentListener; import java.awt.event.ActionListener; import java.awt.Graphics; import java.awt.Component; import java.lang.Math; public class Trajectory extends Applet implements Runnable { // Coordinate system // x axis is direction ball moving // y axis is direction out of plane xz // z axis is vertical direction //used by calculation thread to control its running boolean StopCalc = false; //Define lots of variables double V, Vo, Vx, Vy, Vz, X, Y, Z; double Cd,dt2, REV, VU; double DiamM, REo, HeightM, B, t, dt; double Cl, SpinV, K, DiamCm, AirDensCm, VCm; final double PI = Math.PI; Graphics g; Dimension TSize; int Count; //The following are the plot sizes in layout. //Couldn't find sizes from getSize which would have been neater. int TWd = 375; int THt = 185; int LWd = 375; int LHt = 75; int VWd = 130; int VHt = 75; //Controls number of points to plot int PtIndex; Scrollbar VelBar,AnglBar,SpnBar,SpnAnglBar; Button ResetBtn,StopBtn,FireBtn; TextField VelText,SpnText,SpnAnglText,AnglText,DistText,HtText,VelCText; TextField TimeText; //Next set of objects handle control panel and plotting //There are five subclasses defined within the Trajectory class BallControls BC = new BallControls(this); TPlot tp = new TPlot(this,BC); LPlot lp = new LPlot(this,BC); VPlot vp = new VPlot(this,BC); //Starting ball velocity (ft/s) double VelStart = 280; //Starting spin rate (rpm) double SpnStart = 0; //Starting Angl of barrel double AnglStart = 0; //Starting spin angle -90 is bottom spin, 0 is topspin 90 is topspin double SpnAnglStart = 90; String str = new String(); //Spin rate double Spn = SpnStart; //Spin angle double SpnAngl = SpnAnglStart; //Barrel angle measured from horizontal (x axis, xy plane) double Angl = AnglStart; Thread CalcCycle = null; Thread CalcThread; // English (feet) to metric (meters) conversion factor final double MC = 0.3048; //Atmosphere and gravity information final double gg = 9.8; // mks system final double AirVis = 0.000185; //poises at 25 C and 760 mm final double AirDens = 1.18; //g/L = kg/m3; moist air is close //Paintball and initial parameters final double Wt = 0.003188; //Weight of painball in kgs final double DiamIn = 0.679; //Diameter of paintball in inches final double HeightFt = 5; //Starting height(z) of ball in ft. //The statememnt below is strictly not need. Its a reminder that //if using only English system would have to use Wt/g. final double m = Wt; public void init(){ //we need to get all the stuff in BallControls on screen //because it is a panel we can "add" it here //Following wouldn't work to get sizes of columns in layout; count always 0 //have no clue why //int array[][] = BC.gbl.getLayoutDimensions(); //count=array[0].length; //for (int i=0; i= 0 && Count < 70000 && StopCalc==false){ EulerTrajCalc(); // calculation subroutine // If iterations/printcycle has been reached, dump data. if ((Count - PrintCount) == 50) { //Increment array index PtIndex++; /*There is a big problem with the "real time" graphics approach. *Java? Browser? cue requests to paint until it can update, or * something forces a screen update. Then it may *drop all but the last request. However, CalcCycle keeps running and *advancing PtIndex. Unfortunately, the point positions are calculated *in the methods called by paint. By the time paint was executed the *PtIndex may have advanced. The result was that filling of the *xpt and ypt arrays in tpplot etc was haphazard, and 0,0 points *were often found in the array. There were several ways to fix this: 1. *Do all pixel calculations in main thread, and then let Java update when *it feels in the mood. 2.Run a sychronized process that protects something. *The problem was what to protect and how. Also this could be badly hung up *depending what the else the user is doing 3. Force a delay in the *CalcCycle thread so that paint has time to do its job. I coped out and *used a thread sleep delay. On some slow systems, this still could cause *trouble. Its a balance between the program efficiency and the sleep time. */ //Dump current point data totextfields and plots textUpdate(); try{Thread.sleep(100);} catch (InterruptedException e){} BC.vp.repaint(); try{Thread.sleep(100);} catch (InterruptedException e){} BC.lp.repaint(); try{Thread.sleep(100);} catch (InterruptedException e){} BC.tp.repaint(); try{Thread.sleep(100);} catch (InterruptedException e){} PrintCount = Count; } //end if Count++; } //end while loop //stops calculation thread after StopCalc forces while to end CalcCycle = null; } //end TrajCalc //The next two functions calculate the drag(CD) and lift(CL) coefficients double CD1(double Vnum, double SpinVVal){ /* * The fitting equation is based on Achenbach, E., J. Fluid Mech. 54,565 (1972) * This is a "rational equation fit" that was found using SigmaPlot after * manually reading off points from the published graph. The second equation * is a modification to fit Achenbach data to a combined Davies and Maccoll spin data. * The fit is good to at least a V/U of 3.0 and probably somewhat beyond. To be safe the * spin rate is maxed at 60,000 rpm. */ double CD; REV = Vnum*100*DiamCm*AirDensCm/AirVis; //Calc current reynolds number CD = (0.4274794 + 0.000001146254*REV - 7.559635E-12*Math.pow(REV,2) - 3.817309E-18*Math.pow(REV,3) + 2.389417E-23*Math.pow(REV,4))/(1 - 0.000002120623*REV + 2.952772E-11*Math.pow(REV,2) - 1.914687E-16*Math.pow(REV,3) + 3.125996E-22*Math.pow(REV,4)); if (SpinVVal > 0){ VU = SpinVVal/Vnum; CD = (CD + 2.2132291*VU - 10.345178*Math.pow(VU,2) + 16.157030*Math.pow(VU,3) - 5.27306480*Math.pow(VU,4)) / (1 + 3.1077276*VU - 13.6598678*Math.pow(VU,2) + 24.00539887*Math.pow(VU,3) - 8.340493152*Math.pow(VU,4) + 0.07910093*Math.pow(VU,5)); } //end if return CD; //this returns CD to the Cd } //end CD1 double CL1(double Vnum, double SpinVVal){ double CL; //This data is taken from Mehta's paper reviewing sports balls VU = SpinVVal/Vnum; CL = (-0.0020907 - 0.208056226*VU + 0.768791456*Math.pow(VU,2) - 0.84865215*Math.pow(VU,3) + 0.75365982*Math.pow(VU,4)) / (1 - 4.82629033*VU + 9.95459464*Math.pow(VU,2) - 7.85649742*Math.pow(VU,3) + 3.273765328*Math.pow(VU,4)); return CL; //returns CL to Cl } //end CL1 //method to dump latest data to right side textfields public void textUpdate(){ //MC is english to metric conversion factor //Need two different cycles because initial numbers are not same length as //later values. Therefore won't parse properly. if (Count==0){ DistText.setText(String.valueOf(X/MC)); HtText.setText(String.valueOf(Z/MC)); VelCText.setText(String.valueOf(V/MC)); TimeText.setText(String.valueOf(t));} else{ DistText.setText(String.valueOf(X/MC).substring(0,5)); HtText.setText(String.valueOf(Z/MC).substring(0,5)); VelCText.setText(String.valueOf(V/MC).substring(0,5)); TimeText.setText(String.valueOf(t).substring(0,5));} } //end of textUpdate // The trajectory calculation uses Euler-Richardson method // to approximate the trajectories. The heart of the method is below. public void EulerTrajCalc(){ double Vxmid,Vymid,Vzmid,Vmid,Azmid,Aymid,Axmid,Xmid,Ymid,Zmid,Az,Ay,Ax; //V is velocity V = Math.sqrt(Vx*Vx + Vy*Vy + Vz*Vz); Cd = CD1(V, Spn); //drag coefficient Cl = CL1(V, Spn); //lift coefficient double CosSpinAng = Math.cos((SpnAngl+90)*PI/180); double SinSpinAng = Math.sin((SpnAngl+90)*PI/180); //The equations of projectile motion //As = accelerations in direction s. These are the dynamics equations. Ax = -K * V * (Cd * Vx - Cl * (Vy * SinSpinAng - Vz * CosSpinAng)); Ay = -K * V * (Cd * Vy + Cl * Vx * SinSpinAng); Az = K * V * (Cl * Vx * CosSpinAng - Cd * Vz) - gg; //Now calculate mid t interval velocities, distances and accelerations Vxmid = Vx + Ax*dt2; Vymid = Vy + Ay*dt2; Vzmid = Vz + Az*dt2; Xmid = X + Vx*dt2; Ymid = Y + Vy*dt2; Zmid = Z + Vz*dt2; Vmid =Math.sqrt(Vxmid*Vxmid + Vymid*Vymid + Vzmid*Vzmid); Axmid = -K*Vmid*(Cd*Vxmid - Cl*(Vymid*SinSpinAng - Vzmid*CosSpinAng)); Aymid = -K*Vmid*(Cd*Vymid + Cl*Vxmid*SinSpinAng); Azmid = K*Vmid*(Cl*Vxmid*CosSpinAng - Cd*Vzmid) - gg; //Calculate end of interval values Vx = Vx + Axmid*dt; Vy = Vy + Aymid*dt; Vz = Vz + Azmid*dt; X = X + Vxmid*dt; Y = Y + Vymid*dt; Z = Z + Vzmid*dt; t = t + dt; // Now have X,Y,Z at end of interval t }// end EulerTrajCalc /*There are 3 plots as canvases. There are 2 ways to do the *painting to the canvases. Either set up a class for each *canvas with is own paint procedure or set up a single class with *complex control statements in paint. First seemed less complex. */ class TPlot extends Canvas{ //need to override default constructor so TPlot methods seen by //Trajectory and BallControls. Still don't completely understand this. //Seems like some kind of pipeline Trajectory t; BallControls bc; public TPlot(Trajectory parent, BallControls b){ bc = b; t=parent; } //end constructor //Set margins: l = left, r = right, t= top, b= bottom int ml = 20; int mr = 5; int mt = 5; int mb = 15; //Point arrays to hold the calculated integer points int xpt[] = new int[200]; int ypt[] = new int[200]; // Get axis lengths. int Txaxis = TWd-ml-mr; //length of x axis int Tyaxis = THt-mt-mb; //length of y axis int txmax = 350; //max value of x axis int txmin = 0; //min value of x axis int txrange = txmax - txmin; //numerical range of x axis values int tymax = 350; //do same for y int tymin = 0; int tyrange = tymax - tymin; //For tick marks: there are 8 ticks (@50's), need 7 ticks showing //The ticks will be 2 units long. xtickpos is the increments between ticks. int xtickpos = (int)Txaxis/7; public void Inittplot(Graphics g){ //Make sure Background and Foreground correct color and draw x axis setBackground(Color.white); setForeground(Color.black); g.drawLine(ml,THt-mb,TWd-mr,THt-mb); //Draw vertical tick marks. x Axis 0-350, ticks at 50's start at left for(int i = 1; i<8; i++){ g.drawLine(xtickpos*i+ml,THt-mb,xtickpos*i+ml,THt-mb-2);} //end of loop //Place numbers on x axis g.drawString("0",ml-2,THt-mb+13); g.drawString("100",xtickpos*2+ml-11,THt-mb+13); g.drawString("200",xtickpos*4+ml-11,THt-mb+13); g.drawString("300",xtickpos*6+ml-11,THt-mb+13); g.drawString("Dist. (ft)",ml+(Txaxis/2)-40,THt-2); } //end of inittplot //Set y axis for tplot separately since it depends on conditions. public void tplotyaxis(Graphics g){ //Use three y ranges to fit all. Find out what range to use. if (Angl <= 30) tyrange = 150; else {if (Angl <=60) tyrange = 250; else tyrange = 350;} int ytickpos = (int)(Tyaxis/(tyrange/50)); //At outset get the position of the first point //Note the order for multiplication here must be strictly followed. //(HeightM/Mc) etc give wrong answers if (Count==0) { xpt[0] = ml; ypt[0] = mt+Tyaxis-(int)((HeightM*Tyaxis)/(MC*tyrange)); } //Draw y axis (starting at top) g.drawLine(ml,mt,ml,mt+Tyaxis); for (int i=0; i< (int)(tyrange/50) +1; i++){ g.drawLine(ml,ytickpos*i+mt,ml+2,ytickpos*i+mt);} //Add axis title and labels g.drawString(String.valueOf(tyrange),ml-21,mt+6); g.drawString("0",ml-10,Tyaxis+mt+6); g.drawString("ht",2,ml+(Tyaxis/2)-20); } //end of tplotyaxis public void tplotpt(Graphics g) { //Get new pt from calculated X & Y values //but watch out! Doing (X/Mc)*(Txaxis/txrange) gave wrong ineger values. //Most likely due to type conversion problems xpt[PtIndex] = ml+(int)((X*Txaxis)/(MC*txrange)); ypt[PtIndex] = mt+Tyaxis -(int)((Z*Tyaxis)/(MC*tyrange)); //Plot data over and over and over g.setColor(Color.red); for (int i=1; i 320.0)) {MsgBox(); //Reset both the textfield and scrollbar to intial values if outside VelText.setText(String.valueOf(VelStart)); VelBar.setValue((int)VelStart); }else {Vo =n*MC; VelBar.setValue((int)n);}} if (aTextField == AnglText){ if ((n < 0.0) || (n > 89.99)) {MsgBox(); AnglText.setText(String.valueOf(AnglStart)); AnglBar.setValue((int)AnglStart); }else {Angl = n; AnglBar.setValue((int)n);}} if (aTextField == SpnAnglText){ if ((n < -90) || (n > 90)) {MsgBox(); SpnAnglText.setText(String.valueOf(SpnAnglStart)); SpnAnglBar.setValue((int)SpnAnglStart); }else {SpnAngl = n; SpnAnglBar.setValue((int)n);}} if (aTextField == SpnText){ if ((n < 0) || (n > 50000)) {MsgBox(); SpnText.setText(String.valueOf(SpnStart)); SpnBar.setValue((int)SpnStart); }else {Spn = n*PI*DiamM/60; SpnBar.setValue((int)n);}} }// end of else from button check } //end of try for actionperformed //Handle improper number this occurs if value is not a number catch (NumberFormatException ns) { //Find out what textfield the user has messed up TextField FixField = (TextField)e.getSource(); //Fix or reset the field if (FixField == AnglText) FixField.setText(String.valueOf(AnglStart)); else {if (FixField == VelText) FixField.setText(String.valueOf(VelStart)); else {if (FixField == SpnText) FixField.setText(String.valueOf(SpnStart)); else {if (FixField == SpnAnglText) FixField.setText(String.valueOf(SpnAnglStart)); }}} } //end of NumberFormatException } //for actionperformed //Override standard method for Bar changes with our own twists // No exceptions thrown here, hopefully user can't mess them up. public void adjustmentValueChanged(AdjustmentEvent e) { Scrollbar aBar; double nv; //Find what Bar called this by taking getSource object and cast it to bar aBar = (Scrollbar)e.getSource(); int anum; //Set the calculation parameter based on what was called nv = aBar.getValue(); //Now set the parameter that was changed if (aBar ==VelBar) {Vo =nv*MC; VelText.setText(String.valueOf(VelBar.getValue()));} else{if (aBar == AnglBar) {Angl = nv; AnglText.setText(String.valueOf(AnglBar.getValue()));} else{if (aBar == SpnBar) {Spn = 2*nv*PI*DiamM/60; anum = 2*SpnBar.getValue(); SpnText.setText(String.valueOf(anum));} else{if (aBar == SpnAnglBar) {SpnAngl = nv; SpnAnglText.setText(String.valueOf(SpnAnglBar.getValue()));} }}} } //end of adjustment... //Method used if textfield number is out of range, but still a number public void MsgBox() { Frame f = new Frame("InfoDialog Test"); f.setSize(100, 50); f.setVisible(true); InfoDialog d = new InfoDialog(f, "Input Error", "Out of range number: Please re-enter"); d.show(); } } //end of class BallControls // The following dialog class is heavily modifed from the book _Java in a //Nutshell_ by David Flanagan. The code needed to be updated for jdk1.2 //It is used to print out a message indicating an out of range number public class InfoDialog extends Dialog implements ActionListener { protected Button OKbutton; protected Label label; Frame pr; public InfoDialog(Frame parent, String title, String message) { // Create a dialog with the specified title super(parent, title, false); pr = parent; //a frame is made because a dialog requires a frame setModal(true); //box must be closed before anything else can run // Create and use a BorderLayout manager with specified margins this.setLayout(new BorderLayout(15, 15)); // Create the message and add it to the window Label label = new Label(message); this.add("Center", label); // Create an Okay button in a Panel; add the Panel to the window // Use a FlowLayout to center the button and give it margins. OKbutton = new Button("OK"); Panel p = new Panel(); p.setLayout(new FlowLayout(FlowLayout.CENTER, 15, 15)); p.add(OKbutton); this.add("South", p); OKbutton.addActionListener(this); // Resize the window to the preferred size of its components this.pack(); } public void actionPerformed(ActionEvent e) { this.setVisible(false); this.dispose(); pr.setVisible(false); pr.dispose(); } } //end of Dialog } //end of Trajectory