001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.util;
020
021import icy.painter.Anchor2D;
022import icy.painter.PathAnchor2D;
023
024import java.awt.Color;
025import java.awt.Graphics;
026import java.awt.Graphics2D;
027import java.awt.Shape;
028import java.awt.geom.Area;
029import java.awt.geom.CubicCurve2D;
030import java.awt.geom.Line2D;
031import java.awt.geom.Path2D;
032import java.awt.geom.PathIterator;
033import java.awt.geom.QuadCurve2D;
034import java.awt.geom.Rectangle2D;
035import java.awt.geom.RectangularShape;
036import java.util.ArrayList;
037import java.util.Arrays;
038import java.util.List;
039
040/**
041 * @author Stephane
042 */
043public class ShapeUtil
044{
045    public static interface PathConsumer
046    {
047        /**
048         * Consume the specified path.<br>
049         * Return false to interrupt consumption.
050         */
051        public boolean consumePath(Path2D path, boolean closed);
052    }
053
054    public static interface ShapeConsumer
055    {
056        /**
057         * Consume the specified Shape.<br>
058         * Return false to interrupt consumption.
059         */
060        public boolean consume(Shape shape);
061    }
062
063    public static enum BooleanOperator
064    {
065        OR, AND, XOR
066    }
067
068    /**
069     * @deprecated Use {@link BooleanOperator} instead.
070     */
071    @Deprecated
072    public static enum ShapeOperation
073    {
074        OR
075        {
076            @Override
077            public BooleanOperator getBooleanOperator()
078            {
079                return BooleanOperator.OR;
080            }
081        },
082        AND
083        {
084            @Override
085            public BooleanOperator getBooleanOperator()
086            {
087                return BooleanOperator.AND;
088            }
089        },
090        XOR
091        {
092            @Override
093            public BooleanOperator getBooleanOperator()
094            {
095                return BooleanOperator.XOR;
096            }
097        };
098
099        public abstract BooleanOperator getBooleanOperator();
100    }
101
102    /**
103     * Use the {@link Graphics} clip area and {@link Shape} bounds informations to determine if
104     * the specified {@link Shape} is visible in the specified Graphics object.
105     */
106    public static boolean isVisible(Graphics g, Shape shape)
107    {
108        if (shape == null)
109            return false;
110
111        return GraphicsUtil.isVisible(g, shape.getBounds2D());
112    }
113
114    /**
115     * Returns <code>true</code> if the specified Shape define a closed Shape (Area).<br>
116     * Returns <code>false</code> if the specified Shape define a open Shape (Path).<br>
117     */
118    public static boolean isClosed(Shape shape)
119    {
120        final PathIterator path = shape.getPathIterator(null);
121        final double crd[] = new double[6];
122
123        while (!path.isDone())
124        {
125            if (path.currentSegment(crd) == PathIterator.SEG_CLOSE)
126                return true;
127
128            path.next();
129        }
130
131        return false;
132    }
133
134    /**
135     * Merge the specified list of {@link Shape} with the given {@link BooleanOperator}.<br>
136     * 
137     * @param shapes
138     *        Shapes we want to merge.
139     * @param operator
140     *        {@link BooleanOperator} to apply.
141     * @return {@link Area} shape representing the result of the merge operation.
142     */
143    public static Shape merge(List<Shape> shapes, BooleanOperator operator)
144    {
145        Shape result = new Area();
146
147        // merge shapes
148        for (Shape shape : shapes)
149        {
150            switch (operator)
151            {
152                case OR:
153                    result = union(result, shape);
154                    break;
155
156                case AND:
157                    result = intersect(result, shape);
158                    break;
159
160                case XOR:
161                    result = exclusiveUnion(result, shape);
162                    break;
163            }
164        }
165
166        return result;
167    }
168
169    /**
170     * @deprecated Use {@link #merge(List, BooleanOperator)} instead.
171     */
172    @Deprecated
173    public static Shape merge(Shape[] shapes, ShapeOperation operation)
174    {
175        return merge(Arrays.asList(shapes), operation.getBooleanOperator());
176    }
177
178    /**
179     * Process union between the 2 shapes and return result in a new Shape.
180     */
181    public static Shape union(Shape shape1, Shape shape2)
182    {
183        // first compute closed area union
184        final Area area = new Area(getClosedPath(shape1));
185        area.add(new Area(getClosedPath(shape2)));
186        // then compute open path (polyline) union
187        final Path2D result = new Path2D.Double(getOpenPath(shape1));
188        result.append(getOpenPath(shape2), false);
189        // then append result
190        result.append(area, false);
191
192        return result;
193    }
194
195    /**
196     * @deprecated Use {@link #union(Shape, Shape)} instead
197     */
198    @Deprecated
199    public static Shape add(Shape shape1, Shape shape2)
200    {
201        return union(shape1, shape2);
202    }
203
204    /**
205     * Intersects 2 shapes and return result in an {@link Area} type shape.<br>
206     * If one of the specified Shape is not an Area (do not contains any pixel) then an empty Area is returned.
207     */
208    public static Area intersect(Shape shape1, Shape shape2)
209    {
210        // trivial optimization
211        if (!isClosed(shape1) || !isClosed(shape2))
212            return new Area();
213
214        final Area result = new Area(getClosedPath(shape1));
215
216        result.intersect(new Area(getClosedPath(shape2)));
217
218        return result;
219    }
220
221    /**
222     * Do exclusive union between the 2 shapes and return result in an {@link Area} type shape.<br>
223     * If one of the specified Shape is not an Area (do not contains any pixel) then it just return the other Shape in
224     * Area format. If both Shape are not Area then an empty Area is returned.
225     */
226    public static Area exclusiveUnion(Shape shape1, Shape shape2)
227    {
228        // trivial optimization
229        if (!isClosed(shape1))
230        {
231            if (!isClosed(shape2))
232                return new Area();
233
234            return new Area(shape2);
235        }
236
237        // trivial optimization
238        if (!isClosed(shape2))
239            return new Area(shape1);
240
241        final Area result = new Area(getClosedPath(shape1));
242
243        result.exclusiveOr(new Area(getClosedPath(shape2)));
244
245        return result;
246    }
247
248    /**
249     * @deprecated Use {@link #exclusiveUnion(Shape, Shape)} instead.
250     */
251    @Deprecated
252    public static Area xor(Shape shape1, Shape shape2)
253    {
254        return exclusiveUnion(shape1, shape2);
255    }
256
257    /**
258     * Subtract shape2 from shape1 return result in an {@link Area} type shape.
259     */
260    public static Area subtract(Shape shape1, Shape shape2)
261    {
262        // trivial optimization
263        if (!isClosed(shape1))
264            return new Area();
265        if (!isClosed(shape2))
266            return new Area(shape1);
267
268        final Area result = new Area(getClosedPath(shape1));
269
270        result.subtract(new Area(getClosedPath(shape2)));
271
272        return result;
273    }
274
275    /**
276     * Scale the specified {@link RectangularShape} by specified factor.
277     * 
278     * @param shape
279     *        the {@link RectangularShape} to scale
280     * @param factor
281     *        the scale factor
282     * @param centered
283     *        if true then scaling is centered (shape location is modified)
284     * @param scalePosition
285     *        if true then position is also 'rescaled' (shape location is modified)
286     */
287    public static void scale(RectangularShape shape, double factor, boolean centered, boolean scalePosition)
288    {
289        final double w = shape.getWidth();
290        final double h = shape.getHeight();
291        final double newW = w * factor;
292        final double newH = h * factor;
293        final double newX;
294        final double newY;
295
296        if (scalePosition)
297        {
298            newX = shape.getX() * factor;
299            newY = shape.getY() * factor;
300        }
301        else
302        {
303            newX = shape.getX();
304            newY = shape.getY();
305        }
306
307        if (centered)
308        {
309            final double deltaW = (newW - w) / 2;
310            final double deltaH = (newH - h) / 2;
311
312            shape.setFrame(newX - deltaW, newY - deltaH, newW, newH);
313        }
314        else
315            shape.setFrame(newX, newY, newW, newH);
316    }
317
318    /**
319     * Scale the specified {@link RectangularShape} by specified factor.
320     * 
321     * @param shape
322     *        the {@link RectangularShape} to scale
323     * @param factor
324     *        the scale factor
325     * @param centered
326     *        if true then scaling is centered (shape location is modified)
327     */
328    public static void scale(RectangularShape shape, double factor, boolean centered)
329    {
330        scale(shape, factor, centered, false);
331    }
332
333    /**
334     * Enlarge the specified {@link RectangularShape} by specified width and height.
335     * 
336     * @param shape
337     *        the {@link RectangularShape} to scale
338     * @param width
339     *        the width to add
340     * @param height
341     *        the height to add
342     * @param centered
343     *        if true then enlargement is centered (shape location is modified)
344     */
345    public static void enlarge(RectangularShape shape, double width, double height, boolean centered)
346    {
347        final double w = shape.getWidth();
348        final double h = shape.getHeight();
349        final double newW = w + width;
350        final double newH = h + height;
351
352        if (centered)
353        {
354            final double deltaW = (newW - w) / 2;
355            final double deltaH = (newH - h) / 2;
356
357            shape.setFrame(shape.getX() - deltaW, shape.getY() - deltaH, newW, newH);
358        }
359        else
360            shape.setFrame(shape.getX(), shape.getY(), newW, newH);
361    }
362
363    /**
364     * Translate a rectangular shape by the specified dx and dy value
365     */
366    public static void translate(RectangularShape shape, int dx, int dy)
367    {
368        shape.setFrame(shape.getX() + dx, shape.getY() + dy, shape.getWidth(), shape.getHeight());
369    }
370
371    /**
372     * Translate a rectangular shape by the specified dx and dy value
373     */
374    public static void translate(RectangularShape shape, double dx, double dy)
375    {
376        shape.setFrame(shape.getX() + dx, shape.getY() + dy, shape.getWidth(), shape.getHeight());
377    }
378
379    /**
380     * Permit to describe any PathIterator in a list of Shape which are returned
381     * to the specified ShapeConsumer
382     */
383    public static boolean consumeShapeFromPath(PathIterator path, ShapeConsumer consumer)
384    {
385        final Line2D.Double line = new Line2D.Double();
386        final QuadCurve2D.Double quadCurve = new QuadCurve2D.Double();
387        final CubicCurve2D.Double cubicCurve = new CubicCurve2D.Double();
388        double lastX, lastY, curX, curY, movX, movY;
389        final double crd[] = new double[6];
390
391        curX = 0;
392        curY = 0;
393        movX = 0;
394        movY = 0;
395
396        while (!path.isDone())
397        {
398            final int segType = path.currentSegment(crd);
399
400            lastX = curX;
401            lastY = curY;
402
403            switch (segType)
404            {
405                case PathIterator.SEG_MOVETO:
406                    curX = crd[0];
407                    curY = crd[1];
408                    movX = curX;
409                    movY = curY;
410                    break;
411
412                case PathIterator.SEG_LINETO:
413                    curX = crd[0];
414                    curY = crd[1];
415                    line.setLine(lastX, lastY, curX, curY);
416                    if (!consumer.consume(line))
417                        return false;
418                    break;
419
420                case PathIterator.SEG_QUADTO:
421                    curX = crd[2];
422                    curY = crd[3];
423                    quadCurve.setCurve(lastX, lastY, crd[0], crd[1], curX, curY);
424                    if (!consumer.consume(quadCurve))
425                        return false;
426                    break;
427
428                case PathIterator.SEG_CUBICTO:
429                    curX = crd[4];
430                    curY = crd[5];
431                    cubicCurve.setCurve(lastX, lastY, crd[0], crd[1], crd[2], crd[3], curX, curY);
432                    if (!consumer.consume(cubicCurve))
433                        return false;
434                    break;
435
436                case PathIterator.SEG_CLOSE:
437                    line.setLine(lastX, lastY, movX, movY);
438                    if (!consumer.consume(line))
439                        return false;
440                    break;
441            }
442
443            path.next();
444        }
445
446        return true;
447    }
448
449    /**
450     * Consume all sub path of the specified {@link PathIterator}.<br>
451     * We consider a new sub path when we meet both a {@link PathIterator#SEG_MOVETO} segment or after a
452     * {@link PathIterator#SEG_CLOSE} segment (except the ending one).
453     */
454    public static void consumeSubPath(PathIterator pathIt, PathConsumer consumer)
455    {
456        final double crd[] = new double[6];
457        Path2D current = null;
458
459        while (!pathIt.isDone())
460        {
461            switch (pathIt.currentSegment(crd))
462            {
463                case PathIterator.SEG_MOVETO:
464                    // had a previous not closed path ? --> consume it
465                    if (current != null)
466                        consumer.consumePath(current, false);
467
468                    // create new path
469                    current = new Path2D.Double(pathIt.getWindingRule());
470                    current.moveTo(crd[0], crd[1]);
471                    break;
472
473                case PathIterator.SEG_LINETO:
474                    current.lineTo(crd[0], crd[1]);
475                    break;
476
477                case PathIterator.SEG_QUADTO:
478                    current.quadTo(crd[0], crd[1], crd[2], crd[3]);
479                    break;
480
481                case PathIterator.SEG_CUBICTO:
482                    current.curveTo(crd[0], crd[1], crd[2], crd[3], crd[4], crd[5]);
483                    break;
484
485                case PathIterator.SEG_CLOSE:
486                    // close path and consume it
487                    current.closePath();
488                    consumer.consumePath(current, true);
489
490                    // clear path
491                    current = null;
492                    break;
493            }
494
495            pathIt.next();
496        }
497
498        // have a last not closed path ? --> consume it
499        if (current != null)
500            consumer.consumePath(current, false);
501    }
502
503    /**
504     * Consume all sub path of the specified {@link Shape}.<br>
505     * We consider a new sub path when we meet both a {@link PathIterator#SEG_MOVETO} segment or after a
506     * {@link PathIterator#SEG_CLOSE} segment (except the ending one).
507     */
508    public static void consumeSubPath(Shape shape, PathConsumer consumer)
509    {
510        consumeSubPath(shape.getPathIterator(null), consumer);
511    }
512
513    /**
514     * Returns only the open path part of the specified Shape.<br>
515     * By default all sub path inside a Shape are considered closed which can be a problem when drawing or using
516     * {@link Path2D#contains(double, double)} method.
517     */
518    public static Path2D getOpenPath(Shape shape)
519    {
520        final PathIterator pathIt = shape.getPathIterator(null);
521        final Path2D result = new Path2D.Double(pathIt.getWindingRule());
522
523        consumeSubPath(pathIt, new PathConsumer()
524        {
525            @Override
526            public boolean consumePath(Path2D path, boolean closed)
527            {
528                if (!closed)
529                    result.append(path, false);
530
531                return true;
532            }
533        });
534
535        return result;
536    }
537
538    /**
539     * Returns only the closed path part of the specified Shape.<br>
540     * By default all sub path inside a Shape are considered closed which can be a problem when drawing or using
541     * {@link Path2D#contains(double, double)} method.
542     */
543    public static Path2D getClosedPath(Shape shape)
544    {
545        final PathIterator pathIt = shape.getPathIterator(null);
546        final Path2D result = new Path2D.Double(pathIt.getWindingRule());
547
548        consumeSubPath(pathIt, new PathConsumer()
549        {
550            @Override
551            public boolean consumePath(Path2D path, boolean closed)
552            {
553                if (closed)
554                    result.append(path, false);
555
556                return true;
557            }
558        });
559
560        return result;
561    }
562
563    /**
564     * Return all PathAnchor points from the specified shape
565     */
566    public static ArrayList<PathAnchor2D> getAnchorsFromShape(Shape shape, Color color, Color selectedColor)
567    {
568        final PathIterator pathIt = shape.getPathIterator(null);
569        final ArrayList<PathAnchor2D> result = new ArrayList<PathAnchor2D>();
570        final double crd[] = new double[6];
571        final double mov[] = new double[2];
572
573        while (!pathIt.isDone())
574        {
575            final int segType = pathIt.currentSegment(crd);
576            PathAnchor2D pt = null;
577
578            switch (segType)
579            {
580                case PathIterator.SEG_MOVETO:
581                    mov[0] = crd[0];
582                    mov[1] = crd[1];
583
584                case PathIterator.SEG_LINETO:
585                    pt = new PathAnchor2D(crd[0], crd[1], color, selectedColor, segType);
586                    break;
587
588                case PathIterator.SEG_QUADTO:
589                    pt = new PathAnchor2D(crd[0], crd[1], crd[2], crd[3], color, selectedColor);
590                    break;
591
592                case PathIterator.SEG_CUBICTO:
593                    pt = new PathAnchor2D(crd[0], crd[1], crd[2], crd[3], crd[4], crd[5], color, selectedColor);
594                    break;
595
596                case PathIterator.SEG_CLOSE:
597                    pt = new PathAnchor2D(mov[0], mov[1], color, selectedColor, segType);
598                    // CLOSE points aren't visible
599                    pt.setVisible(false);
600                    break;
601            }
602
603            if (pt != null)
604                result.add(pt);
605
606            pathIt.next();
607        }
608
609        return result;
610    }
611
612    /**
613     * Return all PathAnchor points from the specified shape
614     */
615    public static ArrayList<PathAnchor2D> getAnchorsFromShape(Shape shape)
616    {
617        return getAnchorsFromShape(shape, Anchor2D.DEFAULT_NORMAL_COLOR, Anchor2D.DEFAULT_SELECTED_COLOR);
618    }
619
620    /**
621     * Update specified path from the specified list of PathAnchor2D
622     */
623    public static Path2D buildPathFromAnchors(Path2D path, List<PathAnchor2D> points, boolean closePath)
624    {
625        path.reset();
626
627        for (PathAnchor2D pt : points)
628        {
629            switch (pt.getType())
630            {
631                case PathIterator.SEG_MOVETO:
632                    path.moveTo(pt.getX(), pt.getY());
633                    break;
634
635                case PathIterator.SEG_LINETO:
636                    path.lineTo(pt.getX(), pt.getY());
637                    break;
638
639                case PathIterator.SEG_QUADTO:
640                    path.quadTo(pt.getPosQExtX(), pt.getPosQExtY(), pt.getX(), pt.getY());
641                    break;
642
643                case PathIterator.SEG_CUBICTO:
644                    path.curveTo(pt.getPosCExtX(), pt.getPosCExtY(), pt.getPosQExtX(), pt.getPosQExtY(), pt.getX(),
645                            pt.getY());
646                    break;
647
648                case PathIterator.SEG_CLOSE:
649                    path.closePath();
650                    break;
651            }
652        }
653
654        if ((points.size() > 1) && closePath)
655            path.closePath();
656
657        return path;
658    }
659
660    /**
661     * Update specified path from the specified list of PathAnchor2D
662     */
663    public static Path2D buildPathFromAnchors(Path2D path, List<PathAnchor2D> points)
664    {
665        return buildPathFromAnchors(path, points, true);
666    }
667
668    /**
669     * Create and return a path from the specified list of PathAnchor2D
670     */
671    public static Path2D getPathFromAnchors(List<PathAnchor2D> points, boolean closePath)
672    {
673        return buildPathFromAnchors(new Path2D.Double(), points, closePath);
674    }
675
676    /**
677     * Create and return a path from the specified list of PathAnchor2D
678     */
679    public static Path2D getPathFromAnchors(List<PathAnchor2D> points)
680    {
681        return buildPathFromAnchors(new Path2D.Double(), points, true);
682    }
683
684    /**
685     * @deprecated Use {@link GraphicsUtil#drawPathIterator(PathIterator, Graphics2D)} instead
686     */
687    @Deprecated
688    public static void drawFromPath(PathIterator path, final Graphics2D g)
689    {
690        GraphicsUtil.drawPathIterator(path, g);
691    }
692
693    /**
694     * Return true if the specified PathIterator intersects with the specified Rectangle
695     */
696    public static boolean pathIntersects(PathIterator path, final Rectangle2D rect)
697    {
698        return !consumeShapeFromPath(path, new ShapeConsumer()
699        {
700            @Override
701            public boolean consume(Shape shape)
702            {
703                if (shape.intersects(rect))
704                    return false;
705
706                return true;
707            }
708        });
709    }
710
711}