001    package fang;
002    
003    import java.awt.Shape;
004    import java.awt.geom.*;
005    import java.util.ArrayList;
006    
007    import static java.awt.geom.FlatteningPathIterator.*;
008    
009    
010    /**
011     * This tracker enables a path to be created from any shape or Sprite.
012     * 
013     * @author Jam Jenkins
014     * 
015     */
016    public class OutlineTransformer extends Transformer
017    {
018        /** the distance around the outline */
019        private double perimeter;
020    
021        /**
022         * whether to keep moving around the outline once the shape has been traversed
023         */
024        private boolean looping = false;
025    
026        /** the shape to use the outline of */
027        private Shape shape;
028    
029        /** how fast to move and which direction */
030        private double speed;
031    
032        /**
033         * where the tracker currently is in the outline of the shape
034         */
035        private Point2D.Double currentPoint;
036    
037        /** how far to move the next time step */
038        private Point2D.Double delta;
039    
040        /** the points along the outline */
041        private double[][] allPoints;
042    
043        /** the type of each point, SEG_MOVETO or SEG_LINETO */
044        private int[] pointType;
045    
046        /** which is the current point in the array */
047        private int pointIndex;
048    
049        /**
050         * creates the tracker given a Sprite to move around and a speed to travel. If
051         * you want to make the sprite travel along the actual Sprite instead of just
052         * in the shape of the Sprite, you also need to set the original location of
053         * the Sprite moving to the current location of the OutlineTracker using the
054         * method getCurrentPoint().
055         * 
056         * @param outline
057         *          the sprite on which to traverse the outline
058         * @param speed
059         *          the portion of the screen to move per second
060         */
061        public OutlineTransformer(Sprite outline, double speed)
062        {
063            this(outline.getShape(), speed);
064        }
065    
066        /**
067         * creates the tracker given a Shape to move around and a speed to travel. If
068         * you want to make the sprite travel along the actual Sprite instead of just
069         * in the shape of the Sprite, you also need to set the original location of
070         * the Sprite moving to the current location of the OutlineTracker using the
071         * method getCurrentPoint().
072         * 
073         * @param outline
074         *          the shape on which to traverse the outline
075         * @param speed
076         *          the portion of the screen to move per second
077         */
078        public OutlineTransformer(Shape outline, double speed)
079        {
080            shape = outline;
081            currentPoint = new Point2D.Double();
082            delta = new Point2D.Double();
083            initializeReversablePath();
084            setSpeed(speed);
085        }
086    
087        /**
088         * creates the tracker given a path to move on and a speed to travel. If you
089         * want to make the sprite travel along the actual path instead of just in the
090         * shape of the path, you also need to set the original location of the Sprite
091         * moving to the current location of the OutlineTracker using the method
092         * getCurrentPoint().
093         * 
094         * @param speed
095         *          the portion of the screen to move per second
096         * @param vertices
097         *          the location of the vertices along the path
098         */
099        public OutlineTransformer(double speed, double... vertices)
100        {
101            currentPoint = new Point2D.Double();
102            delta = new Point2D.Double();
103            setShape(vertices);
104            setSpeed(speed);
105        }
106    
107        /**
108         * gets the current location along the outline. This method is useful to call
109         * if you want the Sprite that is moving to stay on the outline of the
110         * original Shape/Sprite.
111         * 
112         * @return the location along the outline
113         */
114        public Location2D getCurrentPoint()
115        {
116            return new Location2D(currentPoint.x, currentPoint.y);
117        }
118    
119        /**
120         * makes a path around the Shape/Sprite such that the points can be traversed
121         * both forward and backward. This is done in two primary ways. First, every
122         * SEG_CLOSE is replaced by a SEG_LINETO since closing would be different when
123         * traversing in the opposite direction. Secondly, each SEG_MOVETO is replaced
124         * by a SEG_MOVETO and SEG_LINETO so that lines to close shapes actually draw
125         * the final closing line and not just move toward it.
126         */
127        private void initializeReversablePath()
128        {
129            pointIndex = 0;
130            perimeter = 0;
131            Point2D.Double startPoint = new Point2D.Double();
132            double[] curve = new double[6];
133            PathIterator path;
134            path = shape.getPathIterator(new AffineTransform(), 0.001);
135            path.currentSegment(curve);
136            currentPoint.x = curve[0];
137            currentPoint.y = curve[1];
138            startPoint.x = curve[0];
139            startPoint.y = curve[1];
140    
141            ArrayList<Integer> pointsType = new ArrayList<Integer>();
142            ArrayList < double[] > points = new ArrayList < double[] > ();
143            pointsType.add(SEG_MOVETO);
144            points.add(new double[] { curve[0], curve[1] });
145            pointsType.add(SEG_LINETO);
146            points.add(new double[] { curve[0], curve[1] });
147            path.next();
148            Point2D.Double last = new Point2D.Double(curve[0], curve[1]);
149            boolean fixReverse = false;
150            while (!path.isDone())
151            {
152                int curveType = path.currentSegment(curve);
153                if (curveType == SEG_CLOSE)
154                {
155                    curve[0] = startPoint.x;
156                    curve[1] = startPoint.y;
157                    if (!fixReverse)
158                    {
159                        curveType = SEG_LINETO;
160                        fixReverse = true;
161                    }
162                    else
163                    {
164                        fixReverse = false;
165                        curveType = SEG_MOVETO;
166                    }
167                }
168                else if (curveType == SEG_MOVETO)
169                {
170                    startPoint.x = curve[0];
171                    startPoint.y = curve[1];
172                    if (!fixReverse)
173                    {
174                        fixReverse = true;
175                    }
176                    else
177                    {
178                        fixReverse = false;
179                        curveType = SEG_LINETO;
180                    }
181                }
182                if (curveType == SEG_LINETO)
183                {
184                    perimeter += last.distance(curve[0], curve[1]);
185                }
186                last.x = curve[0];
187                last.y = curve[1];
188                pointsType.add(curveType);
189                points.add(new double[] { curve[0], curve[1] });
190                if (!fixReverse)
191                {
192                    path.next();
193                }
194            }
195            allPoints = new double[pointsType.size()][2];
196            pointType = new int[pointsType.size()];
197            for (int i = 0; i < pointType.length; i++)
198            {
199                pointType[i] = pointsType.get(i);
200                allPoints[i] = points.get(i);
201            }
202        }
203    
204        /**
205         * sets whether the tracker should stop moving once it goes all the way around
206         * the shape or if it should move continuously
207         * 
208         * @param looping
209         *          true indicates move continuously and false means stop when the
210         *          shape have bee traversed. Calling skipDistance can cause less than
211         *          the entire shape to be traversed if looping is false.
212         */
213        public void setLooping(boolean looping)
214        {
215            this.looping = looping;
216        }
217    
218        /**
219         * tells whether the tracker should stop moving once it goes all the way
220         * around the shape or if it should move continuously
221         * 
222         * @return true if the tracker moves continously or false if it stops once the
223         *         shape has been traversed
224         */
225        public boolean isLooping()
226        {
227            return looping;
228        }
229    
230        /**
231         * gets the distance around the entire outline of the shape
232         * 
233         * @return the perimeter of the shape
234         */
235        public double getPathDistance()
236        {
237            return perimeter;
238        }
239    
240        /**
241         * skips past a part of the outline. If the amount skipped past moves past the
242         * end of the outline and looping is false, the tracker will continue moving
243         * around the shape again until it reaches the end. Calling this method does
244         * not move the tracker, it just changes where it starts and alters the
245         * currrentPoint.
246         * 
247         * @param distance
248         *          the amount to skip. This can be used in conjunction with
249         *          getPathDistance.
250         */
251        public void skipDistance(double distance)
252        {
253            double originalX = delta.x;
254            double originalY = delta.y;
255            advanceDistance(distance);
256            delta.x = originalX;
257            delta.y = originalY;
258        }
259    
260        /**
261         * moves along the outline a given distance. Unlike shipDistance, this has the
262         * effect of moving the tracker and it changes currentPoint.
263         * 
264         * @param distance
265         *          the amount to move around the outline
266         */
267        public void advanceDistance(double distance)
268        {
269            while (distance > 0)
270            {
271                distance -= moveAlongCurrentLine(distance);
272                if (distance > 0)
273                {
274                    if (!advancePointIndex())
275                    {
276                        distance = 0;
277                    }
278                }
279            }
280        }
281    
282        /**
283         * moves up to a given distance along the current line. If the line length is
284         * less than the distance to travel, then the whole line is traversed. If the
285         * line length is more than the distance to travel then the line is only
286         * partially traversed.
287         * 
288         * @param distance
289         *          the maximum amount to travel along the current line
290         * @return the actual amount traveled along the current line
291         */
292        private double moveAlongCurrentLine(double distance)
293        {
294            double originalDistance = distance;
295            if (pointType[pointIndex] == SEG_MOVETO)
296            {
297                delta.x += allPoints[pointIndex][0] - currentPoint.x;
298                delta.y += allPoints[pointIndex][1] - currentPoint.y;
299                currentPoint.x = allPoints[pointIndex][0];
300                currentPoint.y = allPoints[pointIndex][1];
301            }
302            else
303            {
304                double lineDistance = currentPoint.distance(allPoints[pointIndex][0],
305                                      allPoints[pointIndex][1]);
306                if (lineDistance < distance)
307                {
308                    delta.x += allPoints[pointIndex][0] - currentPoint.x;
309                    delta.y += allPoints[pointIndex][1] - currentPoint.y;
310                    currentPoint.x = allPoints[pointIndex][0];
311                    currentPoint.y = allPoints[pointIndex][1];
312                    distance -= lineDistance;
313                }
314                else
315                {
316                    double part = distance / lineDistance;
317                    delta.x += part * (allPoints[pointIndex][0] - currentPoint.x);
318                    delta.y += part * (allPoints[pointIndex][1] - currentPoint.y);
319                    currentPoint.x += part * (allPoints[pointIndex][0] - currentPoint.x);
320                    currentPoint.y += part * (allPoints[pointIndex][1] - currentPoint.y);
321                    distance = 0;
322                }
323            }
324            return originalDistance - distance;
325        }
326    
327        /**
328         * moves the current pointIndex forward or backward depending on the speed and
329         * whether it is looping or not.
330         * 
331         * @return true if there is a next point, false otherwise
332         */
333        private boolean advancePointIndex()
334        {
335            boolean didAdvance = true;
336            if (speed > 0)
337            {
338                if (pointIndex < allPoints.length - 1)
339                {
340                    pointIndex++;
341                }
342                else if (looping)
343                {
344                    pointIndex = 0;
345                }
346                else
347                {
348                    didAdvance = false;
349                }
350            }
351            else
352            {
353                if (pointIndex > 0)
354                {
355                    pointIndex--;
356                }
357                else if (looping)
358                {
359                    pointIndex = allPoints.length - 1;
360                }
361                else
362                {
363                    didAdvance = false;
364                }
365            }
366            return didAdvance;
367        }
368    
369        /**
370         * sets the shape to traverse
371         * 
372         * @param sprite
373         *          the sprite to trace the outline of
374         */
375        public void setShape(Sprite sprite)
376        {
377            setShape(sprite.getShape());
378        }
379    
380        /**
381         * sets the shape to traverse
382         * 
383         * @param shape
384         *          the shape to trace the outline of
385         */
386        public void setShape(Shape shape)
387        {
388            this.shape = shape;
389            initializeReversablePath();
390        }
391    
392        public void setShape(double... vertices)
393        {
394            GeneralPath path = new GeneralPath();
395            path.moveTo((float) vertices[0], (float) vertices[1]);
396            for (int i = 2; i < vertices.length - 1; i += 2)
397            {
398                path.lineTo((float) vertices[i], (float) vertices[i + 1]);
399            }
400            shape = path;
401            initializeReversablePath();
402        }
403    
404        /**
405         * resets the tracker to the beginning of tracing the current shape/path
406         */
407        public void reset()
408        {
409            if (speed < 0)
410            {
411                pointIndex = allPoints.length - 1;
412            }
413            else
414            {
415                pointIndex = 0;
416            }
417        }
418    
419        public boolean isMoving()
420        {
421            boolean moving;
422            // no speed meand not moving
423            if (speed == 0)
424            {
425                moving = false;
426            }
427            // speed and looping means moving
428            else if (isLooping())
429            {
430                moving = true;
431            }
432            // speed and not at the first or last line means moving
433            else if ((pointIndex > 0) || (pointIndex < allPoints.length - 1))
434            {
435                moving = true;
436            }
437            // moving forward from the beginning or backward from
438            // the end means moving
439            else if (((pointIndex == 0) && (speed > 0))
440                     || ((pointIndex == allPoints.length - 1) && (speed < 0)))
441            {
442                moving = true;
443            }
444            // only option here is on the last line
445            // if there is any distance left to travel
446            // we are still moving
447            else
448            {
449                double lineDistance = currentPoint.distance(allPoints[pointIndex][0],
450                                      allPoints[pointIndex][1]);
451                moving = lineDistance > 1e-4;
452            }
453            return moving;
454        }
455    
456        /**
457         * sets the speed of the tracker
458         * 
459         * @param speed
460         *          the speed of the tracker in screens per second
461         */
462        public void setSpeed(double speed)
463        {
464            if ((speed < 0) && (pointIndex == 0))
465            {
466                pointIndex = allPoints.length - 1;
467            }
468            if ((speed > 0) && (pointIndex == allPoints.length - 1))
469            {
470                pointIndex = 0;
471            }
472            this.speed = speed;
473        }
474    
475        /**
476         * sets the speed of the tracker
477         * 
478         * @return the speed of the tracker in screens per second
479         */
480        public double getSpeed()
481        {
482            return speed;
483        }
484    
485        @Override
486        public Location2D getTranslation()
487        {
488            return new Location2D(delta.x, delta.y);
489        }
490    
491        public void advance(double elapsedSeconds)
492        {
493            delta.x = 0;
494            delta.y = 0;
495            double distanceLeft = elapsedSeconds * Math.abs(speed);
496            advanceDistance(distanceLeft);
497        }
498    }