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}