/*******************************************************************************
* Copyright (c) 2012, 2015 itemis AG and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Matthias Wienand (itemis AG) - initial API and implementation
*
*******************************************************************************/
package org.eclipse.gef.geometry.planar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.function.BiFunction;
import org.eclipse.gef.geometry.internal.utils.PrecisionUtils;
/**
* <p>
* The {@link AbstractMultiShape} class contains an algorithm to find the outer
* segments of all outline segments of all the outlines of the internal
* {@link IShape}s of an {@link IMultiShape}. (see {@link #getOutlineSegments()}
* )
* </p>
* <p>
* Moreover, an algorithm to create closed outline objects for an
* {@link IMultiShape} is provided. (see {@link #getOutlines()})
* </p>
*
* @author mwienand
*
*/
abstract class AbstractMultiShape extends AbstractGeometry
implements IMultiShape {
private static final long serialVersionUID = 1L;
/**
* <p>
* Compares two {@link Point}s by their coordinate values. A {@link Point}
* is regarded to be "lower" than another {@link Point} if the first
* {@link Point}'s x coordinate is smaller than the x coordinate of the
* other {@link Point}. In case of equal x coordinates, the y coordinates
* are compared.
* </p>
* <p>
* Returns 0 if the {@link Point}s are equal to each other (see
* {@link Point#equals(Object)}).
* </p>
*
* @param o1
* @param o2
* @return <code>0</code> if the {@link Point}s are equal (Point
* {@link #equals(Object)}), <code>-1</code> if the first
* {@link Point} is "lower" than the second {@link Point}, otherwise
* <code>1</code>
*/
private static int comparePoints(Point o1, Point o2) {
if (o1.equals(o2)) {
return 0;
}
if (o1.x < o2.x) {
return -1;
}
if (o1.x == o2.x) {
if (o1.y < o2.y) {
return -1;
}
}
return 1;
}
private void assignRemainingSegment(HashMap<Line, Integer> seen,
Stack<Line> addends, Line toAdd, Point start, Point end) {
if (!start.equals(end)) {
Line rest = new Line(start, end);
if (start.equals(toAdd.getP1()) || start.equals(toAdd.getP2())) {
addends.push(rest);
} else {
seen.put(rest,
seen.containsKey(rest) && seen.get(rest) == 2 ? 2 : 1);
}
}
}
@Override
public boolean contains(Point p) {
for (IShape s : getShapes()) {
if (s.contains(p)) {
return true;
}
}
return false;
}
/**
* Inner segments are identified by a segment count of exactly 2.
*
* @param seen
*/
private void filterOutInnerSegments(HashMap<Line, Integer> seen) {
for (Line seg : new HashSet<>(seen.keySet())) {
if (seen.get(seg) == 2) {
seen.remove(seg);
}
}
}
private Polyline findOutline(Set<Line> outlineSegments,
Map<Point, List<Line>> segsAt) {
// System.out.println("findOutline");
Set<Point> visited = new HashSet<>();
Line initial = outlineSegments.iterator().next();
List<Point> way = findWay(segsAt, visited, initial.getP1(),
initial.getP2(), 1);
if (way == null) {
// System.out.println("Cannot find outline!");
return new Polyline(new Line[] { initial });
}
way.add(0, initial.getP1());
return new Polyline(
CurveUtils.toSegmentsArray(way.toArray(new Point[] {}), true));
}
/**
* Searches for the longest cycle-free way from the given start
* {@link Point} to the given end {@link Point} on the given segments.
*
* @param segmentsByEndPoints
* @param visited
* @param start
* @param end
* @param indent
* @return
*/
private List<Point> findWay(Map<Point, List<Line>> segmentsByEndPoints,
Set<Point> visited, Point start, Point end, int indent) {
// System.out.printf("%" + indent + "s", " ");
// System.out.println("findWay from " + start + " to " + end);
if (segmentsByEndPoints.get(end) == segmentsByEndPoints.get(start)) {
// System.out.printf("%" + indent + "s", " ");
// System.out.println("#closed");
return new ArrayList<>(0);
}
visited.add(start);
// find unvisited neighbors
@SuppressWarnings("unchecked")
List<Line> nextSegs = (List<Line>) ((ArrayList<Line>) segmentsByEndPoints
.get(start)).clone();
for (Iterator<Line> i = nextSegs.iterator(); i.hasNext();) {
Line l = i.next();
// System.out.printf("%" + indent + "s", " ");
// System.out.print(l + "? ");
if (l.getP1().equals(start)) {
if (visited.contains(l.getP2())) {
// System.out.print("delete");
i.remove();
}
} else if (visited.contains(l.getP1())) {
// System.out.print("delete");
i.remove();
}
// System.out.println();
}
if (nextSegs.size() == 0) {
// System.out.printf("%" + indent + "s", " ");
// System.out.println("#null");
return null;
} else if (nextSegs.size() == 1) {
// System.out.printf("%" + indent + "s", " ");
// System.out.println("#single");
Line nextSeg = nextSegs.get(0);
Point nextPoint = start.equals(nextSeg.getP1()) ? nextSeg.getP2()
: nextSeg.getP1();
List<Point> way = findWay(segmentsByEndPoints, visited, nextPoint,
end, indent + 1);
if (way != null) {
way.add(0, nextPoint);
}
return way;
}
// System.out.printf("%" + indent + "s", " ");
// System.out.println("#multiple");
// multiple possibilities, save visited
int longestWayLength = -1;
List<Point> longestWay = null;
for (Line nextSeg : nextSegs) {
@SuppressWarnings("unchecked")
Set<Point> visitedCopy = (Set<Point>) ((HashSet<Point>) visited)
.clone();
Point nextPoint = start.equals(nextSeg.getP1()) ? nextSeg.getP2()
: nextSeg.getP1();
List<Point> way = findWay(segmentsByEndPoints, visitedCopy,
nextPoint, end, indent + 1);
if (way != null && way.size() >= longestWayLength) {
way.add(0, nextPoint);
longestWay = way;
longestWayLength = way.size();
// System.out.printf("%" + indent + "s", " ");
// System.out.println("#longest = " + longestWayLength);
}
}
// is it possible to have longestWay == null here?
return longestWay;
}
/**
* Collects all edges of the internal {@link IShape}s. For a {@link Region}
* the internal {@link IShape}s are {@link Rectangle}s. For a {@link Ring}
* the internal {@link IShape}s are {@link Polygon}s (triangles).
*
* The internal edges are needed to determine inner and outer segments of
* the {@link IMultiShape}. Based on the outline of the {@link IMultiShape},
* the outline intersections can be computed. These outline intersections
* are required to test if an {@link ICurve} is fully-contained by the
* {@link IMultiShape}.
*
* @return the edges of all internal {@link IShape}s
*/
abstract protected Line[] getAllEdges();
@Override
public Polyline[] getOutlines() {
List<Polyline> outlines = new ArrayList<>();
Map<Point, List<Line>> segmentsByEndPoints = new HashMap<>();
Set<Line> outlineSegments = new HashSet<>();
for (Line seg : getOutlineSegments()) {
// if (comparePoints(seg.getP1(), seg.getP2()) == 1) {
// seg = new Line(seg.getP2(), seg.getP1());
// }
outlineSegments.add(seg);
}
// constructs segments tree
for (Line seg : outlineSegments) {
if (!segmentsByEndPoints.containsKey(seg.getP1())) {
ArrayList<Line> segList = new ArrayList<>();
segmentsByEndPoints.put(seg.getP1(), segList);
}
if (!segmentsByEndPoints.containsKey(seg.getP2())) {
ArrayList<Line> segList = new ArrayList<>();
segmentsByEndPoints.put(seg.getP2(), segList);
}
segmentsByEndPoints.get(seg.getP1()).add(seg);
segmentsByEndPoints.get(seg.getP2()).add(seg);
}
// search for broken end points
// List<Point> unconnectedEndPoints = new ArrayList<Point>();
for (Point p : segmentsByEndPoints.keySet()) {
List<Line> segments = segmentsByEndPoints.get(p);
if (segments.size() < 2) {
throw new IllegalStateException("There is an end point (" + p
+ ") which is not connected to two segments!");
// // unconnectedEndPoints.add(p);
// if (segments.size() == 0) {
// System.out.println("error: unconnected end point " + p);
// } else {
// assert segments.size() == 1;
// System.out.println("error: loose end point " + p
// + ", segment = " + segments.get(0));
// // unconnectedEndPoints.add(segments.get(0).getP1());
// // unconnectedEndPoints.add(segments.get(0).getP2());
// }
// System.out.println(" | remove point/segment from tree");
}
}
while (!outlineSegments.isEmpty()) {
Polyline outline = findOutline(outlineSegments,
segmentsByEndPoints);
// System.out.println("outline: " + outline);
outlines.add(outline);
// Remove the segments of the previously found outline from the set
// of remaining outline segments.
for (Line outlineSeg : CurveUtils
.toSegmentsArray(outline.getPoints(), false)) {
if (comparePoints(outlineSeg.getP1(),
outlineSeg.getP2()) == 1) {
outlineSeg = new Line(outlineSeg.getP2(),
outlineSeg.getP1());
}
outlineSegments.remove(outlineSeg);
}
}
// System.out.println("Found " + outlines.size() + " outlines.");
return outlines.toArray(new Polyline[] {});
}
/**
* <p>
* Computes the outline segments of this {@link AbstractMultiShape}.
* </p>
* <p>
* The outline segments of this {@link AbstractMultiShape} are those outline
* segments of the internal {@link IShape}s that only exist once.
* </p>
*
* @return the outline segments of this {@link AbstractMultiShape}
*/
@Override
public Line[] getOutlineSegments() {
HashMap<Line, Integer> seen = new HashMap<>();
Stack<Line> elementsToAdd = new Stack<>();
for (Line e : getAllEdges()) {
elementsToAdd.push(e);
}
addingElements: while (!elementsToAdd.empty()) {
Line toAdd = elementsToAdd.pop();
for (Line seg : new HashSet<>(seen.keySet())) {
if (seg.overlaps(toAdd)) {
Point[] p = getSortedEndpoints(toAdd, seg);
seen.remove(seg);
assignRemainingSegment(seen, elementsToAdd, toAdd, p[0],
p[1]);
assignRemainingSegment(seen, elementsToAdd, toAdd, p[3],
p[2]);
markOverlap(seen, p[1], p[2]);
continue addingElements;
}
}
seen.put(toAdd, 1);
}
filterOutInnerSegments(seen);
return seen.keySet().toArray(new Line[] {});
}
/**
* Sorts the end {@link Point}s of two {@link Line}s that do overlap by
* their coordinate values.
*
* @param toAdd
* @param seg
* @return the sorted {@link Point}s
*/
private Point[] getSortedEndpoints(Line toAdd, Line seg) {
final Point[] p = new Point[] { seg.getP1(), seg.getP2(), toAdd.getP1(),
toAdd.getP2() };
Arrays.sort(p, new Comparator<Point>() {
@Override
public int compare(Point p1, Point p2) {
if (PrecisionUtils.equal(p1.x, p2.x)) {
return p1.y < p2.y ? 1 : -1;
}
return p1.x < p2.x ? 1 : -1;
}
});
return p;
}
/**
* Marks a given segment from start to end {@link Point} as an overlap in
* the seen {@link HashMap} if the segment is not degenerated, i.e. it is
* not just a single {@link Point}.
*
* @param seen
* @param start
* @param end
*/
private void markOverlap(HashMap<Line, Integer> seen, Point start,
Point end) {
if (!start.equals(end)) {
// Count an overlapping segment twice to assure that it is going to
// get deleted afterwards.
Line overlap = new Line(start, end);
seen.put(overlap, 2);
}
}
@Override
public Path toPath() {
return toPath(Path::exclusiveOr);
}
/**
* Computes a {@link Path} for this {@link AbstractMultiShape} by combining
* the {@link Path} representations of the individual {@link #getOutlines()
* outlines} using the given <i>pathCombinator</i> ({@link BiFunction}). The
* <i>pathCombinator</i> is used as a folding operator, i.e. for three
* outlines A, B, and C, the <i>pathCombinator</i> is used as follows:
* <ol>
* <li><code>path = A.toPath();</code></li>
* <li><code>path = pathCombinator(path, B);</code></li>
* <li><code>path = pathCombinator(path, C);</code></li>
* </ol>
*
* @param pathCombinator
* The {@link BiFunction} that is used to combine two consecutive
* {@link Path}s to a result {@link Path}.
* @return The result of folding the (<i>closed</i>) {@link #getOutlines()
* outlines} of this {@link AbstractMultiShape} using the given
* <i>pathCombinator</i>.
*/
private Path toPath(BiFunction<Path, Path, Path> pathCombinator) {
Polyline[] outlines = getOutlines();
if (outlines == null || outlines.length < 1) {
return new Path();
}
Path path = outlines[0].toPath().close();
for (int i = 1; i < outlines.length; i++) {
path = pathCombinator.apply(path, outlines[i].toPath().close());
}
return path;
}
}