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