001    /*
002     * DotPanel.java 
003     * Part of the Spirograph problem set
004     *
005     * Developed for "Rethinking CS101", a project of Lynn Andrea Stein's AP Group.
006     * For more information, see <a href="http://www.ai.mit.edu/projects/cs101/">the
007     * CS101 homepage</a> or email <las@ai.mit.edu>.
008     *
009     * Copyright (C) 1998 Massachusetts Institute of Technology.
010     * Please do not redistribute without obtaining permission.
011     */
012    
013    package spirograph;
014    
015    import java.awt.*;
016    import java.util.*;
017    
018    /**
019     * This class keeps track of the coordinates of the Dot. It has a seperate
020     * Thread to update the "state" of the two coordinates and to repaint
021     * itself. It also handles the dot bouncing off either the walls of the
022     * square or the circle. It also keeps track of all of the gravity points
023     * and all of the places where the dot has been before and draws the trail.
024     *
025     * <p>Copyright © 1998 Massachusetts Institute of Technology.<br />
026     * Copyright © 2002-2003 Franklin W. Olin College of Engineering.</p>
027     *
028     *
029     * @author Luis Sarmenta, lfgs@cag.lcs.mit.edu
030     * @author Henry Wong, henryw@mit.edu
031     * @author Patrick G. Heck, gus.heck@olin.edu
032     * @version $Id: DotPanel.java,v 1.5 2004/02/09 20:55:03 gus Exp $
033     * @see Coord
034     * @see Spirograph
035     * @see AccelHandler
036     */
037    public class DotPanel extends Canvas implements Runnable {
038        
039      // This vector holds all of the points representing the ball's
040      // various positions.    
041      private Vector v = new Vector(100);
042        
043      // All of the gravitational points that have been added
044      private Vector grav = new Vector(5);
045        
046      // This image is used for the double buffering.
047      private Image buf;
048    
049      // These fields represent the size of the Panel. They should be
050      // changed using the setter methods whenever the window is resized.    
051      private int width = Spirograph.WIDTH;
052      private int height = Spirograph.HEIGHT;
053    
054      // The coords represent the ball's coordinates along the x and
055      // y axis.
056      private Coord x;
057      private Coord y;
058    
059      // Whether or not the DotPanel is in circular mode.
060      private boolean circMode = false;
061    
062      // The two focii of the ellipse when in circular mode
063      private Point focusA = new Point(0,0);
064      private Point focusB = new Point(0,0);
065    
066      // Bounce and Wrap Modes
067      private boolean bounceOn = false;
068      private boolean wrapOn = false;
069             
070      // Mode corresponding to accelMode for AccelHandlers
071      private int myMode = AccelHandler.POSMODE;
072        
073      /**
074       * Create a new dot panel that displays a dot by poling two {@link Coord} objects.
075       *
076       * @param x Defines the position and movement atributes along the x axis
077       * @param y Defines the position and movement atributes along the y axis
078       */  
079      public DotPanel(Coord x, Coord y) {
080        this.x = x;
081        this.y = y;
082        this.setBackground (Color.white);
083        this.setSize(width,height);
084      }
085    
086      /**
087       * Set the operational mode for both coordinate axis.
088       *
089       * @param mode {@link AccelHandler#POSMODE}, {@link AccelHandler#VELMODE},
090       *              or {@link AccelHandler#ACCELMODE}
091       */  
092      public void setMode( int mode )
093      {
094        this.myMode = mode;
095        x.setMode( mode );
096        y.setMode( mode );
097      }
098    
099      /**
100       * Overides the superclass to return our constants.
101       *
102       * @return new Dimension({@link Spirograph#WIDTH},{@link Spirograph#HEIGHT})
103       */  
104      public Dimension getPreferredSize() {
105        return (new Dimension(Spirograph.WIDTH,Spirograph.HEIGHT));
106      }
107    
108      /**
109       * Overide the parent class to return the prefered size as the minimum size.
110       *
111       * @return {@link DotPanel#getPreferredSize()}
112       */  
113      public Dimension getMinimumSize() {
114        return getPreferredSize();
115      }
116             
117      /**
118       * The maximum distance from the center of the drawing area that the ball can be
119       * placed in the X direction.
120       *
121       * @return half the width minus the size of the ball
122       */  
123      public double getMaxX()
124      {
125        return ( width/2 - Spirograph.BALLSIZE );
126      }
127      
128      /**
129       * The maximum distance from the center of the drawing area that the ball can be
130       * placed in the X direction.
131       *
132       * @return half the height minus the size of the ball
133       */  
134      public double getMaxY()
135      {
136        return ( height/2 - Spirograph.BALLSIZE );
137      }
138      
139      /**
140       * Conducts the drawing of all objects in the DotPanel.
141       *
142       */  
143      public void run() {
144          // In order to make sure that the x and y axis go through the
145          // same number of "timesteps" I use a seperate Thread to handle
146          // painting and timestepping.
147          
148          // If you put the nextStep command in the AccelHandler threads
149          // than you can't guarentee that the two timesteps will be
150          // called with equal frequency.
151          
152          
153          while (true) {
154              double step;
155              
156              // I scale the timesteps so that there are more of them as
157              // the dot get's nearer the edge. This way, the dot is
158              // much more acccurate near the edge and it cuts down on
159              // the flickering of the screen near the middle, where the
160              // dot doesn't need to be as accurate
161              
162              if (circMode) {
163                  step = Spirograph.TIMESTEP*Math.abs(1
164                         - (Math.pow(2*x.getPos()/height,2)
165                         + Math.pow(2*y.getPos()/width,2))/2);
166              } else {
167                  step = 0.25;
168              }
169              
170              // getDistVect creates a Vector containing the distance of
171              // the dot from every gravitational point (see below)
172              x.nextStep(getDistVect(true),step);
173              y.nextStep(getDistVect(false),step);
174              
175              double xVel = x.getVel();
176              double yVel = y.getVel();
177              
178              if (Math.sqrt(Math.pow(xVel,2) + Math.pow(yVel,2)) > 
179                  Spirograph.MAXVEL) {
180                      xVel = Spirograph.MAXVEL*xVel/(xVel+yVel);
181                      yVel = Spirograph.MAXVEL*yVel/(xVel+yVel);
182                      x.setVel(xVel);
183                      y.setVel(yVel);
184              }
185                
186              double xPos = x.getPos();
187              double yPos = y.getPos();
188              
189              if (circMode) {
190                  // This is a lot of math to figure out whether or not
191                  // the dot hit the wall and if it did, where it should
192                  // have gone and what it's new velocity should be.
193                  
194                  // from (x/a)^2 + (y/b)^2 = 1
195                  double a = width/2;
196                  double b = height/2;
197                
198                  if (!SpiroUtils.inEllipse(xPos,yPos,a,b)) {
199                      
200                      // Need to figure out where dot went out of circle.
201                                        
202                      double outXPos;
203                      double outYPos;                   
204                      
205                      if (xVel == 0) {
206                          // If this is true will get div 0 errors
207                          
208                          outXPos = xPos;
209                          
210                          if (yVel > 0) {
211                              outYPos = b*Math.sqrt(1-Math.pow(xPos/a,2));
212                          } else {
213                              outYPos = -b*Math.sqrt(1-Math.pow(xPos/a,2));
214                          }
215                      } else {
216                          
217                          // Figure out "m" and "c" from (y = mx + c)
218                          double m = yVel/xVel;
219                          double c = yPos - (yVel*xPos/xVel);
220                          
221                          // Figure out b^2-4ac...
222                          double det = 4*Math.pow(a*b,2)*(Math.pow(b,2) +
223                          Math.pow(a*m,2) - Math.pow(c,2));
224                          
225                          
226                          //Figure out +/-
227                                            
228                          if (xVel > 0) {
229                              outXPos = (-2*m*c*Math.pow(a,2) +
230                              Math.sqrt(det)) /
231                              (2*(Math.pow(b,2)+(Math.pow(m*a,2))));
232                          } else {
233                              outXPos = (-2*m*c*Math.pow(a,2) -
234                              Math.sqrt(det)) /
235                              (2*(Math.pow(b,2)+(Math.pow(m*a,2))));
236                          }
237                          
238                          // now do same for y...
239                          
240                          det = 4*Math.pow(a*m*b,2)*(Math.pow(a*m,2)+
241                          Math.pow(b,2)-Math.pow(c,2));
242                          
243                          if (yVel > 0) {
244                              outYPos = (2*c*Math.pow(b,2) + Math.sqrt(det))/
245                              (2*(Math.pow(b,2)+Math.pow(m*a,2)));
246                          } else {
247                              outYPos = (2*c*Math.pow(b,2) - Math.sqrt(det))/
248                              (2*(Math.pow(b,2)+Math.pow(m*a,2)));
249                          }
250                          
251                      } // matches else from if (xVel == 0)
252                      
253                      if (outXPos == 0) {
254                          //Special case, refSlope = infinity
255                          
256                          y.setVel(-y.getVel());
257                          if (yPos >  0) {
258                              y.setPos(height - yPos);
259                          } else {
260                              y.setPos(-height - yPos);
261                          }
262                          
263                      } else {
264                          
265                          double refSlope = -1*(outYPos*Math.pow(a,2)/
266                          (-outXPos*Math.pow(b,2)));
267                          double coef = (xVel + yVel * refSlope)/
268                          (1 + Math.pow(refSlope,2));
269                          
270                          x.setVel(xVel- 2 * coef);
271                          y.setVel(yVel- 2 * coef * refSlope);
272                          
273                          coef = ((xPos - outXPos) + (yPos - outYPos) * refSlope)
274                                   /(1 + Math.pow(refSlope,2));
275                          
276                          x.setPos(outXPos - 2 * coef);
277                          y.setPos(outYPos - 2 * coef * refSlope);
278                          
279                      } // closes else from if (outX == 0)
280                      
281                  } // ends circMode out of bounds
282                  
283              } else if ( bounceOn ) {
284                  // not circMode
285                  
286                  // If the ball moved past one of the boundaries,
287                  // calculate where it would have gone if it had bounced
288                  // and negate it's velocity.
289                  
290                  if (xPos > getMaxX() ) {
291                      x.setPos(width - xPos - (Spirograph.BALLSIZE * 2));
292                      x.setVel(-xVel);
293                  }
294                  
295                  if (xPos < -getMaxX() ) {
296                      x.setPos(-width - xPos + (Spirograph.BALLSIZE * 2));
297                      x.setVel(-xVel);
298                  }
299                  
300                  if (yPos > getMaxY() ) {
301                      y.setPos(height - yPos - (Spirograph.BALLSIZE * 2));
302                      y.setVel(-yVel);
303                  }
304                  
305                  if (yPos < -getMaxY() ) {
306                      y.setPos(-height - yPos + (Spirograph.BALLSIZE * 2));
307                      y.setVel(-yVel);
308                  }
309              } else if (wrapOn) {
310                  if(xPos == getMaxX())
311                      x.setPos(-getMaxX());
312                  else
313                      if(xPos == -getMaxX())
314                          x.setPos(getMaxX());
315                  if(yPos == getMaxY())
316                      y.setPos(-getMaxY());
317                  else
318                      if(yPos == -getMaxY())
319                          y.setPos(getMaxY());
320              } else {
321                  if(xPos == getMaxX())
322                      x.setPos(getMaxX());
323                  else
324                      if(xPos == -getMaxX())
325                          x.setPos(-getMaxX());
326                  if(yPos == getMaxY())
327                      y.setPos(getMaxY());
328                  else
329                      if(yPos == -getMaxY())
330                          y.setPos(-getMaxY());
331              } 
332              
333              paintBuf();
334              try {
335                  Thread.sleep((int)(100*step));
336                  //Thread.yield();
337              } catch (InterruptedException e) {
338                  System.out.println ("Error sleeping.");
339              }
340          }
341      }
342    
343      /**
344       * Convenience wrapper for {@link SpiroUtils#inEllipse}. This is called by the
345       * AdvArg to see whether or not the ball is in the circle. If the ball isn't
346       * inside the circle, AdvArg won't turn on circle mode.
347       *
348       * @return True if the ball is currently inside the elipse used by circular mode,
349       *            false otherwise
350       */
351      public boolean inEllipse() {
352        return SpiroUtils.inEllipse(x.getPos(), y.getPos(), width/2, height/2);
353      }
354    
355      /** 
356       * This method returns a vector of Points. The x coordinate of each
357       * Point represents the distance of the point from the gravitational
358       * dot along the appropriate axis, the y coordinate represents the
359       * total distance of the dot from each point.
360       *
361       * @see Coord  
362       */
363      private Vector getDistVect(boolean xDim) {
364        double xPos = x.getPos();
365        double yPos = y.getPos();
366        Vector temp = new Vector();
367            
368        for (Enumeration e = grav.elements(); e.hasMoreElements();) {
369                
370          Point o = (Point)e.nextElement();
371    
372          if (xDim) {
373            temp.addElement(new Point((int)(o.x-xPos),
374                                      (int)SpiroUtils.dist(o.x,o.y,xPos,yPos)));
375          } else {
376            temp.addElement(new Point((int)(o.y-yPos),
377                                      (int)SpiroUtils.dist(o.x,o.y,xPos,yPos)));
378          }
379        }
380    
381        return temp;
382      }
383    
384      /**
385       * This method paints an off-screen image to be used for the double
386       * buffering.<p>
387       *
388       * It adds the balls current position to the vector of Points, then
389       * reconstructs the balls path from the Points.
390       *
391       */
392      public void paintBuf() {
393            
394        Graphics g;
395    
396        if (buf == null) {
397          buf = createImage(width, height);
398        }
399            
400        try {
401          g = buf.getGraphics();
402        } catch (NullPointerException e) {
403          return;
404        }   
405    
406        g.clearRect(0,0,width,height);
407    
408        myMode = x.getMode();
409        g.drawString("Pos: (" + x.getPos() + "," + y.getPos() + ")",5,15);
410        if ( myMode != AccelHandler.POSMODE) 
411          g.drawString("Vel: (" + x.getVel() + "," + y.getVel() + ")",5,30);
412        if ( myMode == AccelHandler.ACCELMODE)
413          g.drawString("Accel: ("+x.getAccel() +","+ y.getAccel() + ")",5,45);
414            
415        // Draw the ellipse
416        if (circMode) {
417          g.setColor(Color.black);
418          g.drawOval(0,0,width,height);
419    
420          // Draw the focii
421          g.drawOval(focusA.x+width/2-Spirograph.FOCUSSIZE/2,
422                     height/2-focusA.y-Spirograph.FOCUSSIZE/2,
423                     Spirograph.FOCUSSIZE, Spirograph.FOCUSSIZE);
424          g.drawOval(focusB.x+width/2-Spirograph.FOCUSSIZE/2,
425                     height/2-focusB.y-Spirograph.FOCUSSIZE/2,
426                     Spirograph.FOCUSSIZE, Spirograph.FOCUSSIZE);
427        }
428    
429        // Draw the gravity sources
430        g.setColor(Color.blue);
431    
432        for (Enumeration e = grav.elements(); e.hasMoreElements();) {
433                
434          Point o = (Point)e.nextElement();
435    
436          g.fillOval(o.x + width/2 - Spirograph.BALLSIZE/2,
437                     height/2 - o.y - Spirograph.BALLSIZE/2,
438                     Spirograph.BALLSIZE, Spirograph.BALLSIZE);
439        }
440    
441        // Draw the dot's trail
442        double oldX;
443        double oldY;
444    
445        // Set up the beginning of the trail.
446        if (v.isEmpty()) {
447          oldX = x.getPos();
448          oldY = y.getPos();
449          v.addElement(new Point((int)x.getPos(),(int)y.getPos()) );  // add element now so call to v.lastElement later will work
450        } else {
451          oldX = ((Point)(v.elementAt(0))).x;
452          oldY = ((Point)(v.elementAt(0))).y;
453        }
454            
455        // Add the position of the dot to the vector of Points.
456        Point newPoint = new Point((int)x.getPos(),(int)y.getPos());
457        Point prevPoint = (Point)v.lastElement();  
458            
459        if ( ! (( Math.abs(newPoint.x-prevPoint.x) < 1.0 ) &&
460                ( Math.abs(newPoint.y-prevPoint.y) < 1.0 )) )
461          v.addElement ( newPoint );
462        // else no need to add a new point
463            
464        g.setColor (Color.red);
465    
466        // Step through the vector and draw the a line representing the
467        // dot's path.
468        for (Enumeration e = v.elements(); e.hasMoreElements();) {
469                
470          Point o = (Point)e.nextElement();
471          // if the difference between the old and new position
472          // is almost as big as the width/height of the box,
473          // then a wraparound probably happened, so don't draw a line.
474          // otherwise, draw a line
475          if( (Math.abs(o.x - oldX) < 2*(getMaxX()-Spirograph.BALLSIZE))
476              && 
477              (Math.abs(o.y - oldY) < 2*(getMaxY()-Spirograph.BALLSIZE))
478              ) {
479            g.drawLine((int)(oldX+width/2), (int)(-oldY+height/2),
480                       (int)(o.x+width/2), (int)(-o.y+height/2));
481          }
482            
483          oldX = o.x;
484          oldY = o.y;
485    
486        }
487            
488        g.setColor(Color.black);
489    
490        // Draw the oval
491        g.fillOval ((int)(x.getPos()-Spirograph.BALLSIZE/2
492                          +(width/2)),
493                    (int)(-y.getPos()-Spirograph.BALLSIZE/2
494                          +(height/2)),
495                    Spirograph.BALLSIZE,Spirograph.BALLSIZE);       
496    
497        //  System.out.println ("NOW AT: " + x.getPos() + "," + y.getPos());
498    
499        repaint();
500        g.dispose();
501      } // end of paintbuf();
502            
503      /**
504       * Overide the superclass paint method, to do our own drawing.
505       *
506       * @param out The <code>Graphics</code> object for this component.
507       */  
508      public void paint (Graphics out) {
509        // Draw the ball on screen. If buf hasn't been initialized yet,
510        // initialized it.
511            
512        try {
513          out.drawImage(buf,0,0,this);
514        } catch (NullPointerException e) {
515          paintBuf();
516        }
517            
518      }
519    
520      /**
521       * Recalculate the foci and repaint of the elipse before calling the
522       * super.setSize(). Called by the DotFrame whenever the DotFrame is resized
523       *
524       * @param width The new width
525       * @param height The new height
526       */
527      public void setSize(int width, int height) {
528    
529        buf = null;
530            
531        this.width = width;
532        this.height = height;
533            
534        x.setMaxPos( getMaxX() );
535        y.setMaxPos( getMaxY() );
536    
537        // get new focii
538    
539        if (width < height) {
540          focusA.x = 0;
541          focusA.y = (int)Math.sqrt(Math.pow(height/2,2)-
542                                    Math.pow(width/2,2));
543          focusB.x = 0;
544          focusB.y = -(int)Math.sqrt(Math.pow(height/2,2)-
545                                     Math.pow(width/2,2));
546        } else {
547          focusA.x = (int)Math.sqrt(Math.pow(width/2,2)-
548                                    Math.pow(height/2,2));
549          focusA.y = 0;
550          focusB.x = -(int)Math.sqrt(Math.pow(width/2,2)-
551                                     Math.pow(height/2,2));
552          focusB.y = 0;
553        }
554    
555        if (circMode) {     
556          System.out.println ("Focus A at: "+focusA.x+":"+focusA.y);
557          System.out.println ("Focus B at: "+focusB.x+":"+focusB.y);
558        }
559            
560        paintBuf();
561        super.setSize(width,height);
562      }
563    
564      /**
565       * Delete the trail of the dot. The points defining the trail of the dot
566       * are removed from the containing <code>Vector</code>. The position of the dot
567       * is uneffected, and the panel is repainted.
568       *
569       */  
570      public void flushLines() {
571        v.removeAllElements();
572        paintBuf();
573      }
574    
575      /**
576       * Delete all gravity sources, and repaint.
577       *
578       */  
579      public void flushGrav() {
580        grav.removeAllElements();
581        paintBuf();
582      }
583        
584      /**
585       * Place a gravity source at the coordinates specified. Usually this is called
586       * by a <code>MouseListener</code> that has been added to the DotPanel.
587       *
588       * @param x The horizontal coordinate for the gravity source.
589       * @param y The vertical coordinate for the gravity source.
590       */  
591      public void addGrav(int x, int y) {
592        grav.addElement(new Point(x,y));
593      }
594    
595      /**
596       * Turn circular mode on or off.
597       *
598       * @param circMode <code>true</code> to turn circular mode on, <code>false</code> to turn it off
599       */  
600      public void setCirc(boolean circMode) {
601        this.circMode = circMode;
602        paintBuf();
603      }
604    
605      /**
606       * Query the state of circular mode.
607       *
608       * @return True if circular mode is on, false otherwise
609       */  
610      public boolean getCirc() {
611        return circMode;
612      }
613        
614      /**
615       * Get the current height of the DotPanel. This method reports the value of the
616       * height variable, which in turn shadows the same value as getHeight() in the
617       * component class.
618       *
619       * @return The current height of the panel
620       * @deprecated This method and the associated shadow variable violate the DRY
621       * (Do not Repeat Yourself) principle. The only visible benefit is a
622       * slightly reduced typing. With modern compilers this won't even yield
623       * a performance increase because the getWidth or getHeight methods will
624       * be inlined. The Sun code for these methods in <code>Component</code>
625       * is:<code>
626       *
627       *    public int getWidth() {
628       *    return width;
629       *    }
630       * </code>
631       * For this reason, this method and the associated variables will be removed.
632       */  
633      public int curHeight() {
634        return height;
635      }
636    
637      /**
638       * Get the current width of the DotPanel. This method reports the value of the
639       * height variable, which in turn shadows the same value as getHeight() in the
640       * component class.
641       *
642       * @return The current width of the panel
643       * @deprecated This method and the associated shadow variable violate the DRY
644       * (Do not Repeat Yourself) principle. The only visible benefit is a
645       * slightly reduced typing. With modern compilers this won't even yield
646       * a performance increase because the getWidth or getHeight methods will
647       * be inlined. The Sun code for these methods in <code>Component</code>
648       * is:<code>
649       *
650       *    public int getWidth() {
651       *    return width;
652       *    }
653       * </code>
654       * For this reason, this method and the associated variable will be removed.
655       */  
656      public int curWidth() {
657        return width;
658      }
659    
660      /**
661       * Query the state of bounce mode.
662       *
663       * @return True if bounce mode is on, false otherwise
664       */  
665      public boolean getBounce()
666      {
667        return bounceOn;
668      }
669    
670      /**
671       * Turn bounce mode on or off.
672       *
673       * @param bounceOn <code>true</code> to turn bounce mode on, <code>false</code> to turn it off
674       */  
675      public void setBounce(boolean bounceOn)
676      {
677        this.bounceOn = bounceOn;
678      }
679    
680      /**
681       * Query the state of wrap mode.
682       *
683       * @return True if wrap mode is on, false otherwise
684       */  
685      public boolean getWrap()
686      {
687        return wrapOn;
688      }
689    
690      /**
691       * Turn wrap mode on or off.
692       *
693       * @param wrapOn <code>true</code> to turn wrap mode on, <code>false</code> to turn it off
694       */  
695      public void setWrap(boolean wrapOn)
696      {
697        this.wrapOn = wrapOn;
698      }
699    }
700    
701    /*
702     * $Log: DotPanel.java,v $
703     * Revision 1.5  2004/02/09 20:55:03  gus
704     * javadoc fixes
705     *
706     * Revision 1.4  2003/01/15 17:36:10  gus
707     * adding log keywords to files that don't have them
708     *
709     */ 
710