001/*
002 * $Id: BusyPainter.java 4156 2012-02-02 19:54:38Z kschaefe $
003 *
004 * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 */
021
022package org.jdesktop.swingx.painter;
023
024import java.awt.Color;
025import java.awt.Graphics2D;
026import java.awt.Rectangle;
027import java.awt.Shape;
028import java.awt.geom.Ellipse2D;
029import java.awt.geom.PathIterator;
030import java.awt.geom.Point2D;
031import java.awt.geom.Point2D.Float;
032import java.awt.geom.RoundRectangle2D;
033import java.util.ArrayList;
034import java.util.List;
035import java.util.NoSuchElementException;
036
037import org.jdesktop.beans.JavaBean;
038import org.jdesktop.swingx.util.PaintUtils;
039
040/**
041 * A specific painter that paints an "infinite progress" like animation.
042 */
043@JavaBean
044@SuppressWarnings("nls")
045public class BusyPainter extends AbstractPainter<Object> {
046
047    /**
048     * Direction is used to set the initial direction in which the
049     * animation starts.
050     * 
051     * @see BusyPainter#setDirection(Direction)
052     */
053    public static enum Direction {
054        /**
055         * cycle proceeds forward
056         */
057        RIGHT,
058        /** cycle proceeds backward */
059        LEFT,
060    }
061
062    private int frame = -1;
063
064    private int points = 8;
065
066    private Color baseColor = new Color(200, 200, 200);
067
068    private Color highlightColor = Color.BLACK;
069
070    private int trailLength = 4;
071
072    private Shape pointShape;
073
074    private Shape trajectory;
075
076    private Direction direction = Direction.RIGHT;
077
078    private boolean paintCentered;
079
080    /**
081     * Creates new busy painter initialized to the shape of circle and bounds size 26x26 points.
082     */
083    public BusyPainter() {
084        this(26);
085    }
086
087    /**
088     * Creates new painter initialized to the shape of circle and bounds of square of specified height.
089     * @param height Painter height.
090     */
091    public BusyPainter(int height) {
092        this(getScaledDefaultPoint(height), getScaledDefaultTrajectory(height));
093    }
094    
095    /**
096     * Initializes painter to the specified trajectory and and point shape. Bounds are dynamically calculated to so the specified trajectory fits in.
097     * @param point Point shape.
098     * @param trajectory Trajectory shape.
099     */
100    public BusyPainter(Shape point, Shape trajectory) {
101        init(point, trajectory, Color.LIGHT_GRAY, Color.BLACK);
102    }
103
104    protected static Shape getScaledDefaultTrajectory(int height) {
105        return new Ellipse2D.Float(((height * 8) / 26) / 2, ((height * 8) / 26) / 2, height
106                - ((height * 8) / 26), height - ((height * 8) / 26));
107    }
108
109    protected static Shape getScaledDefaultPoint(int height) {
110        return new RoundRectangle2D.Float(0, 0, (height * 8) / 26, 4,
111                4, 4);
112    }
113
114    /**
115     * Initializes painter to provided shapes and default colors.
116     * @param point Point shape.
117     * @param trajectory Trajectory shape.
118     */
119    protected void init(Shape point, Shape trajectory, Color baseColor, Color highlightColor) {
120        this.baseColor = baseColor;
121        this.highlightColor = highlightColor;
122        this.pointShape = point;
123        this.trajectory = trajectory;
124    }
125
126    /**
127     * @inheritDoc
128     */
129    @Override
130    protected void doPaint(Graphics2D g, Object t, int width, int height) {
131        Rectangle r = getTrajectory().getBounds();
132        int tw = width - r.width - 2*r.x;
133        int th = height - r.height - 2*r.y;
134        if (isPaintCentered()) {
135            g.translate(tw/2, th/2);
136        }
137
138        PathIterator pi = trajectory.getPathIterator(null);
139        float[] coords = new float[6];
140        Float cp = new Point2D.Float();
141        Point2D.Float sp = new Point2D.Float();
142        int ret;
143        float totalDist = 0;
144        List<float[]> segStack = new ArrayList<float[]>();
145        do {
146            try {
147                ret = pi.currentSegment(coords);
148            } catch (NoSuchElementException e) {
149                // invalid object definition - one of the bounds is zero or less
150                return;
151            }
152            if (ret == PathIterator.SEG_LINETO || (ret == PathIterator.SEG_CLOSE && (sp.x != cp.x || sp.y != cp.y))) {
153                //close by line
154                float c = calcLine(coords, cp);
155                totalDist += c;
156                // move the point to the end (just so it is same for all of them
157                segStack.add(new float[] { c, 0, 0, 0, 0, coords[0], coords[1], ret });
158                cp.x = coords[0];
159                cp.y = coords[1];
160            }
161            if (ret == PathIterator.SEG_MOVETO) {
162                sp.x = cp.x = coords[0];
163                sp.y = cp.y = coords[1];
164
165            }
166            if (ret == PathIterator.SEG_CUBICTO) {
167                float c = calcCube(coords, cp);
168                totalDist += c;
169                segStack.add(new float[] { c, coords[0], coords[1], coords[2],
170                        coords[3], coords[4], coords[5], ret });
171                cp.x = coords[4];
172                cp.y = coords[5];
173            }
174            if (ret == PathIterator.SEG_QUADTO) {
175                float c = calcLengthOfQuad(coords, cp);
176                totalDist += c;
177                segStack.add(new float[] { c, coords[0], coords[1], 0 ,0 , coords[2],
178                        coords[3], ret });
179                cp.x = coords[2];
180                cp.y = coords[3];
181            }
182            // got a starting point, center point on it.
183            pi.next();
184        } while (!pi.isDone());
185        float nxtP = totalDist / getPoints();
186        List<Point2D.Float> pList = new ArrayList<Point2D.Float>();
187        pList.add(new Float(sp.x, sp.y));
188        int sgIdx = 0;
189        float[] sgmt = segStack.get(sgIdx);
190        float len = sgmt[0];
191        float travDist = nxtP;
192        Float center = new Float(sp.x, sp.y);
193        for (int i = 1; i < getPoints(); i++) {
194            while (len < nxtP) {
195                sgIdx++;
196                // Be carefull when messing around with points.
197                sp.x = sgmt[5];
198                sp.y = sgmt[6];
199                sgmt = segStack.get(sgIdx);
200                travDist = nxtP - len;
201                len += sgmt[0];
202            }
203            len -= nxtP;
204            Float p = calcPoint(travDist, sp, sgmt, width, height);
205            pList.add(p);
206            center.x += p.x;
207            center.y += p.y;
208            travDist += nxtP;
209        }
210        // calculate center
211        center.x = ((float) width) / 2;
212        center.y = ((float) height) / 2;
213
214        // draw the stuff
215        int i = 0;
216        g.translate(center.x, center.y);
217        for (Point2D.Float p : pList) {
218            drawAt(g, i++, p, center);
219        }
220        g.translate(-center.x, -center.y);
221
222        if (isPaintCentered()) {
223            g.translate(-tw/2, -th/2);
224        }
225    }
226
227    /**
228     * Gets value of centering hint. If true, shape will be positioned in the center of painted area.
229     * @return Whether shape will be centered over painting area or not.
230     */
231    public boolean isPaintCentered() {
232        return this.paintCentered;
233    }
234
235    /**
236     * Centers shape in the area covered by the painter.
237     * @param paintCentered Centering hint.
238     */
239    public void setPaintCentered(boolean paintCentered) {
240        boolean old = isPaintCentered();
241        this.paintCentered = paintCentered;
242        firePropertyChange("paintCentered", old, isPaintCentered());
243    }
244
245    private void drawAt(Graphics2D g, int i, Point2D.Float p, Float c) {
246        g.setColor(calcFrameColor(i));
247        paintRotatedCenteredShapeAtPoint(p, c, g);
248    }
249
250    private void paintRotatedCenteredShapeAtPoint(Float p, Float c, Graphics2D g) {
251        Shape s = getPointShape();
252        double hh = s.getBounds().getHeight() / 2;
253        double wh = s.getBounds().getWidth() / 2;
254        double t, x, y;
255        double a = c.y - p.y;
256        double b = p.x - c.x;
257        double sa = Math.signum(a);
258        double sb = Math.signum(b);
259        sa = sa == 0 ? 1 : sa;
260        sb = sb == 0 ? 1 : sb;
261        a = Math.abs(a);
262        b = Math.abs(b);
263        t = Math.atan(a / b);
264        t = sa > 0 ? sb > 0 ? -t : -Math.PI + t : sb > 0 ? t : Math.PI - t;
265        x = Math.sqrt(a * a + b * b) - wh;
266        y = -hh;
267        g.rotate(t);
268        g.translate(x, y);
269        g.fill(s);
270        g.translate(-x, -y);
271        g.rotate(-t);
272
273    }
274
275    private Point2D.Float calcPoint(float dist2go, Point2D.Float startPoint,
276            float[] sgmt, int w, int h) {
277        Float f = new Point2D.Float();
278        if (sgmt[7] == PathIterator.SEG_LINETO) {
279            // linear
280            float a = sgmt[5] - startPoint.x;
281            float b = sgmt[6] - startPoint.y;
282            float pathLen = sgmt[0];
283            f.x = startPoint.x + a * dist2go / pathLen;
284            f.y = startPoint.y + b * dist2go / pathLen;
285        } else if (sgmt[7] == PathIterator.SEG_QUADTO) {
286            // quadratic curve
287            Float ctrl = new Point2D.Float(sgmt[1]/w, sgmt[2]/h);
288            Float end = new Point2D.Float(sgmt[5]/w, sgmt[6]/h);
289            Float start = new Float(startPoint.x/w, startPoint.y/h);
290
291            // trans coords from abs to rel
292            f = getXY(dist2go / sgmt[0], start, ctrl, end);
293            f.x *= w;
294            f.y *= h;
295
296        } else if (sgmt[7] == PathIterator.SEG_CUBICTO) {
297            // bezier curve
298            float x = Math.abs(startPoint.x - sgmt[5]);
299            float y = Math.abs(startPoint.y - sgmt[6]);
300
301            // trans coords from abs to rel
302            float c1rx = Math.abs(startPoint.x - sgmt[1]) / x;
303            float c1ry = Math.abs(startPoint.y - sgmt[2]) / y;
304            float c2rx = Math.abs(startPoint.x - sgmt[3]) / x;
305            float c2ry = Math.abs(startPoint.y - sgmt[4]) / y;
306            f = getXY(dist2go / sgmt[0], c1rx, c1ry, c2rx, c2ry);
307
308            float a = startPoint.x - sgmt[5];
309            float b = startPoint.y - sgmt[6];
310
311            f.x = startPoint.x - f.x * a;
312            f.y = startPoint.y - f.y * b;
313        }
314        return f;
315    }
316
317    
318    /**
319     * Calculates length of the linear segment.
320     * @param coords Segment coordinates.
321     * @param cp Start point.
322     * @return Length of the segment.
323     */
324    private float calcLine(float[] coords, Float cp) {
325        float a = cp.x - coords[0];
326        float b = cp.y - coords[1];
327        float c = (float) Math.sqrt(a * a + b * b);
328        return c;
329    }
330
331    /**
332     * Claclulates length of the cubic segment.
333     * @param coords Segment coordinates.
334     * @param cp Start point.
335     * @return Length of the segment.
336     */
337    private float calcCube(float[] coords, Float cp) {
338        float x = Math.abs(cp.x - coords[4]);
339        float y = Math.abs(cp.y - coords[5]);
340
341        // trans coords from abs to rel
342        float c1rx = Math.abs(cp.x - coords[0]) / x;
343        float c1ry = Math.abs(cp.y - coords[1]) / y;
344        float c2rx = Math.abs(cp.x - coords[2]) / x;
345        float c2ry = Math.abs(cp.y - coords[3]) / y;
346        float prevLength = 0, prevX = 0, prevY = 0;
347        for (float t = 0.01f; t <= 1.0f; t += .01f) {
348            Point2D.Float xy = getXY(t, c1rx, c1ry, c2rx, c2ry);
349            prevLength += (float) Math.sqrt((xy.x - prevX) * (xy.x - prevX)
350                    + (xy.y - prevY) * (xy.y - prevY));
351            prevX = xy.x;
352            prevY = xy.y;
353        }
354        // prev len is a fraction num of the real path length
355        float z = ((Math.abs(x) + Math.abs(y)) / 2) * prevLength;
356        return z;
357    }
358
359    /**
360     * Calculates length of the quadratic segment
361     * @param coords Segment coordinates
362     * @param cp Start point.
363     * @return Length of the segment.
364     */
365    private float calcLengthOfQuad(float[] coords, Point2D.Float cp) {
366        Float ctrl = new Point2D.Float(coords[0], coords[1]);
367        Float end = new Point2D.Float(coords[2], coords[3]);
368        // get abs values
369        // ctrl1
370        float c1ax = Math.abs(cp.x - ctrl.x) ;
371        float c1ay = Math.abs(cp.y - ctrl.y) ;
372        // end1
373        float e1ax = Math.abs(cp.x - end.x) ;
374        float e1ay = Math.abs(cp.y - end.y) ;
375        // get max value on each axis
376        float maxX = Math.max(c1ax, e1ax);
377        float maxY = Math.max(c1ay, e1ay);
378
379        // trans coords from abs to rel
380        // ctrl1
381        ctrl.x = c1ax / maxX;
382        ctrl.y = c1ay / maxY;
383        // end1
384        end.x = e1ax / maxX;
385        end.y = e1ay / maxY;
386
387        // claculate length
388        float prevLength = 0, prevX = 0, prevY = 0;
389        for (float t = 0.01f; t <= 1.0f; t += .01f) {
390            Point2D.Float xy = getXY(t, new Float(0,0), ctrl, end);
391            prevLength += (float) Math.sqrt((xy.x - prevX) * (xy.x - prevX)
392                    + (xy.y - prevY) * (xy.y - prevY));
393            prevX = xy.x;
394            prevY = xy.y;
395        }
396        // prev len is a fraction num of the real path length
397        float a = Math.abs(coords[2] - cp.x);
398        float b = Math.abs(coords[3] - cp.y);
399        float dist = (float) Math.sqrt(a*a+b*b);
400        return prevLength * dist;
401    }
402
403    /**
404     * Calculates the XY point for a given t value.
405     * 
406     * The general spline equation is: x = b0*x0 + b1*x1 + b2*x2 + b3*x3 y =
407     * b0*y0 + b1*y1 + b2*y2 + b3*y3 where: b0 = (1-t)^3 b1 = 3 * t * (1-t)^2 b2 =
408     * 3 * t^2 * (1-t) b3 = t^3 We know that (x0,y0) == (0,0) and (x1,y1) ==
409     * (1,1) for our splines, so this simplifies to: x = b1*x1 + b2*x2 + b3 y =
410     * b1*x1 + b2*x2 + b3
411     * 
412     * @author chet
413     * 
414     * @param t parametric value for spline calculation
415     */
416    private Point2D.Float getXY(float t, float x1, float y1, float x2, float y2) {
417        Point2D.Float xy;
418        float invT = (1 - t);
419        float b1 = 3 * t * (invT * invT);
420        float b2 = 3 * (t * t) * invT;
421        float b3 = t * t * t;
422        xy = new Point2D.Float((b1 * x1) + (b2 * x2) + b3, (b1 * y1)
423                + (b2 * y2) + b3);
424        return xy;
425    }
426
427    /**
428     * Calculates relative position of the point on the quad curve in time t&lt;0,1&gt;.
429     * @param t distance on the curve
430     * @param ctrl Control point in rel coords
431     * @param end End point in rel coords
432     * @return Solution of the quad equation for time T in non complex space in rel coords.
433     */
434    public static Point2D.Float getXY(float t, Point2D.Float begin, Point2D.Float ctrl, Point2D.Float end) {
435        /*
436         *     P1 = (x1, y1) - start point of curve
437         *     P2 = (x2, y2) - end point of curve
438         *     Pc = (xc, yc) - control point
439         *
440         *     Pq(t) = P1*(1 - t)^2 + 2*Pc*t*(1 - t) + P2*t^2 =
441         *           = (P1 - 2*Pc + P2)*t^2 + 2*(Pc - P1)*t + P1
442         *     t = [0:1]
443         *     // thx Jim ...
444         *     
445         *     b0 = (1 -t)^2, b1 = 2*t*(1-t), b2 = t^2
446         */
447        Point2D.Float xy;
448        float invT = (1 - t);
449        float b0 = invT * invT;
450        float b1 = 2 * t * invT ;
451        float b2 = t * t;
452        xy = new Point2D.Float(b0 * begin.x + (b1 * ctrl.x) + b2* end.x, b0 * begin.y +  (b1 * ctrl.y) + b2* end.y);
453        
454        return xy;
455    }
456    
457    /**
458     * Selects appropriate color for given frame based on the frame position and gradient difference.
459     * @param i Frame.
460     * @return Frame color.
461     */
462    private Color calcFrameColor(final int i) {
463        if (frame == -1) {
464            return getBaseColor();
465        }
466
467        for (int t = 0; t < getTrailLength(); t++) {
468            if (direction == Direction.RIGHT
469                    && i == (frame - t + getPoints()) % getPoints()) {
470                float terp = 1 - ((float) (getTrailLength() - t))
471                        / (float) getTrailLength();
472                return PaintUtils.interpolate(getBaseColor(),
473                        getHighlightColor(), terp);
474            } else if (direction == Direction.LEFT
475                    && i == (frame + t) % getPoints()) {
476                float terp = ((float) (t)) / (float) getTrailLength();
477                return PaintUtils.interpolate(getBaseColor(),
478                        getHighlightColor(), terp);
479            }
480        }
481        return getBaseColor();
482    }
483
484    /**
485     * Gets current frame.
486     * @return Current frame.
487     */
488    public int getFrame() {
489        return frame;
490    }
491
492    /**Sets current frame.
493     * @param frame Current frame.
494     */
495    public void setFrame(int frame) {
496        int old = getFrame();
497        this.frame = frame;
498        firePropertyChange("frame", old, getFrame());
499    }
500
501    /**
502     * Gets base color.
503     * @return Base color.
504     */
505    public Color getBaseColor() {
506        return baseColor;
507    }
508
509    /**
510     * Sets new base color. Bound property.
511     * @param baseColor Base color.
512     */
513    public void setBaseColor(Color baseColor) {
514        Color old = getBaseColor();
515        this.baseColor = baseColor;
516        firePropertyChange("baseColor", old, getBaseColor());
517    }
518
519    /**
520     * Gets highlight color.
521     * @return Current highlight color.
522     */
523    public Color getHighlightColor() {
524        return highlightColor;
525    }
526
527    /**
528     * Sets new highlight color. Bound property.
529     * @param highlightColor New highlight color.
530     */
531    public void setHighlightColor(Color highlightColor) {
532        Color old = getHighlightColor();
533        this.highlightColor = highlightColor;
534        firePropertyChange("highlightColor", old, getHighlightColor());
535    }
536
537    /**
538     * Gets total amount of distinct points in spinner.
539     * @return Total amount of points.
540     */
541    public int getPoints() {
542        return points;
543    }
544
545    /**
546     * Sets total amount of points in spinner. Bound property.
547     * @param points Total amount of points.
548     */
549    public void setPoints(int points) {
550        int old = getPoints();
551        this.points = points;
552        firePropertyChange("points", old, getPoints());
553    }
554
555    /**
556     * Gets length of trail in number of points.
557     * @return Trail lenght.
558     */
559    public int getTrailLength() {
560        return trailLength;
561    }
562
563    /**
564     * Sets length of the trail in points. Bound property.
565     * @param trailLength Trail length in points.
566     */
567    public void setTrailLength(int trailLength) {
568        int old = getTrailLength();
569        this.trailLength = trailLength;
570        firePropertyChange("trailLength", old, getTrailLength());
571    }
572
573    /**
574     * Gets shape of current point.
575     * @return Shape of the point.
576     */
577    public final Shape getPointShape() {
578        return pointShape;
579    }
580
581    /**
582     * Sets new point shape. Bound property.
583     * @param pointShape new Shape.
584     */
585    public final void setPointShape(Shape pointShape) {
586        Shape old = getPointShape();
587        this.pointShape = pointShape;
588        firePropertyChange("pointShape", old, getPointShape());
589    }
590
591    /**
592     * Gets current trajectory.
593     * @return Current spinner trajectory .
594     */
595    public final Shape getTrajectory() {
596        return trajectory;
597    }
598
599    /**
600     * Sets new trajectory. Expected trajectory have to be closed shape. Bound property.
601     * @param trajectory New trajectory.
602     */
603    public final void setTrajectory(Shape trajectory) {
604        Shape old = getTrajectory();
605        this.trajectory = trajectory;
606        firePropertyChange("trajectory", old, getTrajectory());
607    }
608    
609    /**
610     * Gets current direction of spinning.
611     * @return Current spinning direction.
612     */
613    public Direction getDirection() {
614        return direction;
615    }
616
617    /**
618     * Sets new spinning direction.
619     * @param dir Spinning direction.
620     */
621    public void setDirection(Direction dir) {
622        Direction old = getDirection();
623        this.direction = dir;
624        firePropertyChange("direction", old, getDirection());
625    }
626}