blob: ee76ff923b92bde4110ec973a04c8c4a801f0f18 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.geometry.euclidean.twod;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.commons.geometry.core.partitioning.AbstractRegion;
import org.apache.commons.geometry.core.partitioning.AbstractSubHyperplane;
import org.apache.commons.geometry.core.partitioning.BSPTree;
import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
import org.apache.commons.geometry.core.partitioning.BoundaryAttribute;
import org.apache.commons.geometry.core.partitioning.Hyperplane;
import org.apache.commons.geometry.core.partitioning.Side;
import org.apache.commons.geometry.core.partitioning.SubHyperplane;
import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
import org.apache.commons.geometry.euclidean.oned.Interval;
import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
import org.apache.commons.geometry.euclidean.oned.Vector1D;
import org.apache.commons.numbers.core.Precision;
/** This class represents a 2D region: a set of polygons.
*/
public class PolygonsSet extends AbstractRegion<Vector2D, Vector1D> {
/** Vertices organized as boundary loops. */
private Vector2D[][] vertices;
/** Build a polygons set representing the whole plane.
* @param precision precision context used to compare floating point values
*/
public PolygonsSet(final DoublePrecisionContext precision) {
super(precision);
}
/** Build a polygons set from a BSP tree.
* <p>The leaf nodes of the BSP tree <em>must</em> have a
* {@code Boolean} attribute representing the inside status of
* the corresponding cell (true for inside cells, false for outside
* cells). In order to avoid building too many small objects, it is
* recommended to use the predefined constants
* {@code Boolean.TRUE} and {@code Boolean.FALSE}</p>
* <p>
* This constructor is aimed at expert use, as building the tree may
* be a difficult task. It is not intended for general use and for
* performances reasons does not check thoroughly its input, as this would
* require walking the full tree each time. Failing to provide a tree with
* the proper attributes, <em>will</em> therefore generate problems like
* {@link NullPointerException} or {@link ClassCastException} only later on.
* This limitation is known and explains why this constructor is for expert
* use only. The caller does have the responsibility to provided correct arguments.
* </p>
* @param tree inside/outside BSP tree representing the region
* @param precision precision context used to compare floating point values
*/
public PolygonsSet(final BSPTree<Vector2D> tree, final DoublePrecisionContext precision) {
super(tree, precision);
}
/** Build a polygons set from a Boundary REPresentation (B-rep).
* <p>The boundary is provided as a collection of {@link
* SubHyperplane sub-hyperplanes}. Each sub-hyperplane has the
* interior part of the region on its minus side and the exterior on
* its plus side.</p>
* <p>The boundary elements can be in any order, and can form
* several non-connected sets (like for example polygons with holes
* or a set of disjoint polygons considered as a whole). In
* fact, the elements do not even need to be connected together
* (their topological connections are not used here). However, if the
* boundary does not really separate an inside open from an outside
* open (open having here its topological meaning), then subsequent
* calls to the {@link
* org.apache.commons.geometry.core.partitioning.Region#checkPoint(org.apache.commons.geometry.core.Point)
* checkPoint} method will not be meaningful anymore.</p>
* <p>If the boundary is empty, the region will represent the whole
* space.</p>
* @param boundary collection of boundary elements, as a
* collection of {@link SubHyperplane SubHyperplane} objects
* @param precision precision context used to compare floating point values
*/
public PolygonsSet(final Collection<SubHyperplane<Vector2D>> boundary, final DoublePrecisionContext precision) {
super(boundary, precision);
}
/** Build a parallellepipedic box.
* @param xMin low bound along the x direction
* @param xMax high bound along the x direction
* @param yMin low bound along the y direction
* @param yMax high bound along the y direction
* @param precision precision context used to compare floating point values
*/
public PolygonsSet(final double xMin, final double xMax,
final double yMin, final double yMax,
final DoublePrecisionContext precision) {
super(boxBoundary(xMin, xMax, yMin, yMax, precision), precision);
}
/** Build a polygon from a simple list of vertices.
* <p>The boundary is provided as a list of points considering to
* represent the vertices of a simple loop. The interior part of the
* region is on the left side of this path and the exterior is on its
* right side.</p>
* <p>This constructor does not handle polygons with a boundary
* forming several disconnected paths (such as polygons with holes).</p>
* <p>For cases where this simple constructor applies, it is expected to
* be numerically more robust than the {@link #PolygonsSet(Collection, DoublePrecisionContext) general
* constructor} using {@link SubHyperplane subhyperplanes}.</p>
* <p>If the list is empty, the region will represent the whole
* space.</p>
* <p>
* Polygons with thin pikes or dents are inherently difficult to handle because
* they involve lines with almost opposite directions at some vertices. Polygons
* whose vertices come from some physical measurement with noise are also
* difficult because an edge that should be straight may be broken in lots of
* different pieces with almost equal directions. In both cases, computing the
* lines intersections is not numerically robust due to the almost 0 or almost
* &pi; angle. Such cases need to carefully adjust the {@code hyperplaneThickness}
* parameter. A too small value would often lead to completely wrong polygons
* with large area wrongly identified as inside or outside. Large values are
* often much safer. As a rule of thumb, a value slightly below the size of the
* most accurate detail needed is a good value for the {@code hyperplaneThickness}
* parameter.
* </p>
* @param precision precision context used to compare floating point values
* @param vertices vertices of the simple loop boundary
*/
public PolygonsSet(final DoublePrecisionContext precision, final Vector2D ... vertices) {
super(verticesToTree(precision, vertices), precision);
}
/** Create a list of hyperplanes representing the boundary of a box.
* @param xMin low bound along the x direction
* @param xMax high bound along the x direction
* @param yMin low bound along the y direction
* @param yMax high bound along the y direction
* @param precision precision context used to compare floating point values
* @return boundary of the box
*/
private static Line[] boxBoundary(final double xMin, final double xMax,
final double yMin, final double yMax,
final DoublePrecisionContext precision) {
if (precision.eq(xMin, xMax) || precision.eq(yMin, yMax)) {
// too thin box, build an empty polygons set
return null;
}
final Vector2D minMin = Vector2D.of(xMin, yMin);
final Vector2D minMax = Vector2D.of(xMin, yMax);
final Vector2D maxMin = Vector2D.of(xMax, yMin);
final Vector2D maxMax = Vector2D.of(xMax, yMax);
return new Line[] {
Line.fromPoints(minMin, maxMin, precision),
Line.fromPoints(maxMin, maxMax, precision),
Line.fromPoints(maxMax, minMax, precision),
Line.fromPoints(minMax, minMin, precision)
};
}
/** Build the BSP tree of a polygons set from a simple list of vertices.
* <p>The boundary is provided as a list of points considering to
* represent the vertices of a simple loop. The interior part of the
* region is on the left side of this path and the exterior is on its
* right side.</p>
* <p>This constructor does not handle polygons with a boundary
* forming several disconnected paths (such as polygons with holes).</p>
* <p>For cases where this simple constructor applies, it is expected to
* be numerically more robust than the {@link #PolygonsSet(Collection,double) general
* constructor} using {@link SubHyperplane subhyperplanes}.</p>
* @param precision precision context used to compare floating point values
* @param vertices vertices of the simple loop boundary
* @return the BSP tree of the input vertices
*/
private static BSPTree<Vector2D> verticesToTree(final DoublePrecisionContext precision,
final Vector2D ... vertices) {
final int n = vertices.length;
if (n == 0) {
// the tree represents the whole space
return new BSPTree<>(Boolean.TRUE);
}
// build the vertices
final Vertex[] vArray = new Vertex[n];
for (int i = 0; i < n; ++i) {
vArray[i] = new Vertex(vertices[i]);
}
// build the edges
List<Edge> edges = new ArrayList<>(n);
for (int i = 0; i < n; ++i) {
// get the endpoints of the edge
final Vertex start = vArray[i];
final Vertex end = vArray[(i + 1) % n];
// get the line supporting the edge, taking care not to recreate it
// if it was already created earlier due to another edge being aligned
// with the current one
Line line = start.sharedLineWith(end);
if (line == null) {
line = Line.fromPoints(start.getLocation(), end.getLocation(), precision);
}
// create the edge and store it
edges.add(new Edge(start, end, line));
// check if another vertex also happens to be on this line
for (final Vertex vertex : vArray) {
if (vertex != start && vertex != end &&
precision.eqZero(line.getOffset(vertex.getLocation()))) {
vertex.bindWith(line);
}
}
}
// build the tree top-down
final BSPTree<Vector2D> tree = new BSPTree<>();
insertEdges(precision, tree, edges);
return tree;
}
/** Recursively build a tree by inserting cut sub-hyperplanes.
* @param precision precision context used to compare floating point values
* @param node current tree node (it is a leaf node at the beginning
* of the call)
* @param edges list of edges to insert in the cell defined by this node
* (excluding edges not belonging to the cell defined by this node)
*/
private static void insertEdges(final DoublePrecisionContext precision,
final BSPTree<Vector2D> node,
final List<Edge> edges) {
// find an edge with an hyperplane that can be inserted in the node
int index = 0;
Edge inserted =null;
while (inserted == null && index < edges.size()) {
inserted = edges.get(index++);
if (inserted.getNode() == null) {
if (node.insertCut(inserted.getLine())) {
inserted.setNode(node);
} else {
inserted = null;
}
} else {
inserted = null;
}
}
if (inserted == null) {
// no suitable edge was found, the node remains a leaf node
// we need to set its inside/outside boolean indicator
final BSPTree<Vector2D> parent = node.getParent();
if (parent == null || node == parent.getMinus()) {
node.setAttribute(Boolean.TRUE);
} else {
node.setAttribute(Boolean.FALSE);
}
return;
}
// we have split the node by inserting an edge as a cut sub-hyperplane
// distribute the remaining edges in the two sub-trees
final List<Edge> plusList = new ArrayList<>();
final List<Edge> minusList = new ArrayList<>();
for (final Edge edge : edges) {
if (edge != inserted) {
final double startOffset = inserted.getLine().getOffset(edge.getStart().getLocation());
final double endOffset = inserted.getLine().getOffset(edge.getEnd().getLocation());
Side startSide = precision.eqZero(Math.abs(startOffset)) ?
Side.HYPER : ((startOffset < 0) ? Side.MINUS : Side.PLUS);
Side endSide = precision.eqZero(endOffset) ?
Side.HYPER : ((endOffset < 0) ? Side.MINUS : Side.PLUS);
switch (startSide) {
case PLUS:
if (endSide == Side.MINUS) {
// we need to insert a split point on the hyperplane
final Vertex splitPoint = edge.split(inserted.getLine());
minusList.add(splitPoint.getOutgoing());
plusList.add(splitPoint.getIncoming());
} else {
plusList.add(edge);
}
break;
case MINUS:
if (endSide == Side.PLUS) {
// we need to insert a split point on the hyperplane
final Vertex splitPoint = edge.split(inserted.getLine());
minusList.add(splitPoint.getIncoming());
plusList.add(splitPoint.getOutgoing());
} else {
minusList.add(edge);
}
break;
default:
if (endSide == Side.PLUS) {
plusList.add(edge);
} else if (endSide == Side.MINUS) {
minusList.add(edge);
}
break;
}
}
}
// recurse through lower levels
if (!plusList.isEmpty()) {
insertEdges(precision, node.getPlus(), plusList);
} else {
node.getPlus().setAttribute(Boolean.FALSE);
}
if (!minusList.isEmpty()) {
insertEdges(precision, node.getMinus(), minusList);
} else {
node.getMinus().setAttribute(Boolean.TRUE);
}
}
/** Internal class for holding vertices while they are processed to build a BSP tree. */
private static class Vertex {
/** Vertex location. */
private final Vector2D location;
/** Incoming edge. */
private Edge incoming;
/** Outgoing edge. */
private Edge outgoing;
/** Lines bound with this vertex. */
private final List<Line> lines;
/** Build a non-processed vertex not owned by any node yet.
* @param location vertex location
*/
Vertex(final Vector2D location) {
this.location = location;
this.incoming = null;
this.outgoing = null;
this.lines = new ArrayList<>();
}
/** Get Vertex location.
* @return vertex location
*/
public Vector2D getLocation() {
return location;
}
/** Bind a line considered to contain this vertex.
* @param line line to bind with this vertex
*/
public void bindWith(final Line line) {
lines.add(line);
}
/** Get the common line bound with both the instance and another vertex, if any.
* <p>
* When two vertices are both bound to the same line, this means they are
* already handled by node associated with this line, so there is no need
* to create a cut hyperplane for them.
* </p>
* @param vertex other vertex to check instance against
* @return line bound with both the instance and another vertex, or null if the
* two vertices do not share a line yet
*/
public Line sharedLineWith(final Vertex vertex) {
for (final Line line1 : lines) {
for (final Line line2 : vertex.lines) {
if (line1 == line2) {
return line1;
}
}
}
return null;
}
/** Set incoming edge.
* <p>
* The line supporting the incoming edge is automatically bound
* with the instance.
* </p>
* @param incoming incoming edge
*/
public void setIncoming(final Edge incoming) {
this.incoming = incoming;
bindWith(incoming.getLine());
}
/** Get incoming edge.
* @return incoming edge
*/
public Edge getIncoming() {
return incoming;
}
/** Set outgoing edge.
* <p>
* The line supporting the outgoing edge is automatically bound
* with the instance.
* </p>
* @param outgoing outgoing edge
*/
public void setOutgoing(final Edge outgoing) {
this.outgoing = outgoing;
bindWith(outgoing.getLine());
}
/** Get outgoing edge.
* @return outgoing edge
*/
public Edge getOutgoing() {
return outgoing;
}
}
/** Internal class for holding edges while they are processed to build a BSP tree. */
private static class Edge {
/** Start vertex. */
private final Vertex start;
/** End vertex. */
private final Vertex end;
/** Line supporting the edge. */
private final Line line;
/** Node whose cut hyperplane contains this edge. */
private BSPTree<Vector2D> node;
/** Build an edge not contained in any node yet.
* @param start start vertex
* @param end end vertex
* @param line line supporting the edge
*/
Edge(final Vertex start, final Vertex end, final Line line) {
this.start = start;
this.end = end;
this.line = line;
this.node = null;
// connect the vertices back to the edge
start.setOutgoing(this);
end.setIncoming(this);
}
/** Get start vertex.
* @return start vertex
*/
public Vertex getStart() {
return start;
}
/** Get end vertex.
* @return end vertex
*/
public Vertex getEnd() {
return end;
}
/** Get the line supporting this edge.
* @return line supporting this edge
*/
public Line getLine() {
return line;
}
/** Set the node whose cut hyperplane contains this edge.
* @param node node whose cut hyperplane contains this edge
*/
public void setNode(final BSPTree<Vector2D> node) {
this.node = node;
}
/** Get the node whose cut hyperplane contains this edge.
* @return node whose cut hyperplane contains this edge
* (null if edge has not yet been inserted into the BSP tree)
*/
public BSPTree<Vector2D> getNode() {
return node;
}
/** Split the edge.
* <p>
* Once split, this edge is not referenced anymore by the vertices,
* it is replaced by the two half-edges and an intermediate splitting
* vertex is introduced to connect these two halves.
* </p>
* @param splitLine line splitting the edge in two halves
* @return split vertex (its incoming and outgoing edges are the two halves)
*/
public Vertex split(final Line splitLine) {
final Vertex splitVertex = new Vertex(line.intersection(splitLine));
splitVertex.bindWith(splitLine);
final Edge startHalf = new Edge(start, splitVertex, line);
final Edge endHalf = new Edge(splitVertex, end, line);
startHalf.node = node;
endHalf.node = node;
return splitVertex;
}
}
/** {@inheritDoc} */
@Override
public PolygonsSet buildNew(final BSPTree<Vector2D> tree) {
return new PolygonsSet(tree, getPrecision());
}
/** {@inheritDoc} */
@Override
protected void computeGeometricalProperties() {
final Vector2D[][] v = getVertices();
if (v.length == 0) {
final BSPTree<Vector2D> tree = getTree(false);
if (tree.getCut() == null && (Boolean) tree.getAttribute()) {
// the instance covers the whole space
setSize(Double.POSITIVE_INFINITY);
setBarycenter(Vector2D.NaN);
} else {
setSize(0);
setBarycenter(Vector2D.NaN);
}
} else if (v[0][0] == null) {
// there is at least one open-loop: the polygon is infinite
setSize(Double.POSITIVE_INFINITY);
setBarycenter(Vector2D.NaN);
} else {
// all loops are closed, we compute some integrals around the shape
double sum = 0;
double sumX = 0;
double sumY = 0;
for (Vector2D[] loop : v) {
double x1 = loop[loop.length - 1].getX();
double y1 = loop[loop.length - 1].getY();
for (final Vector2D point : loop) {
final double x0 = x1;
final double y0 = y1;
x1 = point.getX();
y1 = point.getY();
final double factor = x0 * y1 - y0 * x1;
sum += factor;
sumX += factor * (x0 + x1);
sumY += factor * (y0 + y1);
}
}
if (sum < 0) {
// the polygon as a finite outside surrounded by an infinite inside
setSize(Double.POSITIVE_INFINITY);
setBarycenter(Vector2D.NaN);
} else {
setSize(sum / 2);
setBarycenter(Vector2D.of(sumX / (3 * sum), sumY / (3 * sum)));
}
}
}
/** Get the vertices of the polygon.
* <p>The polygon boundary can be represented as an array of loops,
* each loop being itself an array of vertices.</p>
* <p>In order to identify open loops which start and end by
* infinite edges, the open loops arrays start with a null point. In
* this case, the first non null point and the last point of the
* array do not represent real vertices, they are dummy points
* intended only to get the direction of the first and last edge. An
* open loop consisting of a single infinite line will therefore be
* represented by a three elements array with one null point
* followed by two dummy points. The open loops are always the first
* ones in the loops array.</p>
* <p>If the polygon has no boundary at all, a zero length loop
* array will be returned.</p>
* <p>All line segments in the various loops have the inside of the
* region on their left side and the outside on their right side
* when moving in the underlying line direction. This means that
* closed loops surrounding finite areas obey the direct
* trigonometric orientation.</p>
* @return vertices of the polygon, organized as oriented boundary
* loops with the open loops first (the returned value is guaranteed
* to be non-null)
*/
public Vector2D[][] getVertices() {
if (vertices == null) {
if (getTree(false).getCut() == null) {
vertices = new Vector2D[0][];
} else {
// build the unconnected segments
final SegmentsBuilder visitor = new SegmentsBuilder(getPrecision());
getTree(true).visit(visitor);
final List<ConnectableSegment> segments = visitor.getSegments();
// connect all segments, using topological criteria first
// and using Euclidean distance only as a last resort
int pending = segments.size();
pending -= naturalFollowerConnections(segments);
if (pending > 0) {
pending -= splitEdgeConnections(segments);
}
if (pending > 0) {
pending -= closeVerticesConnections(segments);
}
// create the segment loops
final ArrayList<List<Segment>> loops = new ArrayList<>();
for (ConnectableSegment s = getUnprocessed(segments); s != null; s = getUnprocessed(segments)) {
final List<Segment> loop = followLoop(s);
if (loop != null) {
// an open loop is one that has fewer than two segments or has a null
// start point; the case where we have two segments in a closed loop
// (ie, an infinitely thin, degenerate loop) will result in null being
// returned from the followLoops method
if (loop.size() < 2 || loop.get(0).getStart() == null) {
// this is an open loop, we put it on the front
loops.add(0, loop);
} else {
// this is a closed loop, we put it on the back
loops.add(loop);
}
}
}
// transform the loops in an array of arrays of points
vertices = new Vector2D[loops.size()][];
int i = 0;
for (final List<Segment> loop : loops) {
if (loop.size() < 2 ||
(loop.size() == 2 && loop.get(0).getStart() == null && loop.get(1).getEnd() == null)) {
// single infinite line
final Line line = loop.get(0).getLine();
vertices[i++] = new Vector2D[] {
null,
line.toSpace(Vector1D.of(-Float.MAX_VALUE)),
line.toSpace(Vector1D.of(+Float.MAX_VALUE))
};
} else if (loop.get(0).getStart() == null) {
// open loop with at least one real point
final Vector2D[] array = new Vector2D[loop.size() + 2];
int j = 0;
for (Segment segment : loop) {
if (j == 0) {
// null point and first dummy point
double x = segment.getLine().toSubSpace(segment.getEnd()).getX();
x -= Math.max(1.0, Math.abs(x / 2));
array[j++] = null;
array[j++] = segment.getLine().toSpace(Vector1D.of(x));
}
if (j < (array.length - 1)) {
// current point
array[j++] = segment.getEnd();
} else if (j == (array.length - 1)) {
// last dummy point
double x = segment.getLine().toSubSpace(segment.getStart()).getX();
x += Math.max(1.0, Math.abs(x / 2));
array[j++] = segment.getLine().toSpace(Vector1D.of(x));
}
}
vertices[i++] = array;
} else {
final Vector2D[] array = new Vector2D[loop.size()];
int j = 0;
for (Segment segment : loop) {
array[j++] = segment.getStart();
}
vertices[i++] = array;
}
}
}
}
return vertices.clone();
}
/** Connect the segments using only natural follower information.
* @param segments segments complete segments list
* @return number of connections performed
*/
private int naturalFollowerConnections(final List<ConnectableSegment> segments) {
int connected = 0;
for (final ConnectableSegment segment : segments) {
if (segment.getNext() == null) {
final BSPTree<Vector2D> node = segment.getNode();
final BSPTree<Vector2D> end = segment.getEndNode();
for (final ConnectableSegment candidateNext : segments) {
if (candidateNext.getPrevious() == null &&
candidateNext.getNode() == end &&
candidateNext.getStartNode() == node) {
// connect the two segments
segment.setNext(candidateNext);
candidateNext.setPrevious(segment);
++connected;
break;
}
}
}
}
return connected;
}
/** Connect the segments resulting from a line splitting a straight edge.
* @param segments segments complete segments list
* @return number of connections performed
*/
private int splitEdgeConnections(final List<ConnectableSegment> segments) {
int connected = 0;
for (final ConnectableSegment segment : segments) {
if (segment.getNext() == null && segment.getEndNode() != null) {
final Hyperplane<Vector2D> hyperplane = segment.getNode().getCut().getHyperplane();
final BSPTree<Vector2D> end = segment.getEndNode();
for (final ConnectableSegment candidateNext : segments) {
if (candidateNext.getPrevious() == null &&
candidateNext.getNode().getCut().getHyperplane().equals(hyperplane) &&
candidateNext.getStartNode() == end) {
// connect the two segments
segment.setNext(candidateNext);
candidateNext.setPrevious(segment);
++connected;
break;
}
}
}
}
return connected;
}
/** Connect the segments using Euclidean distance.
* <p>
* This connection heuristic should be used last, as it relies
* only on a fuzzy distance criterion.
* </p>
* @param segments segments complete segments list
* @return number of connections performed
*/
private int closeVerticesConnections(final List<ConnectableSegment> segments) {
int connected = 0;
for (final ConnectableSegment segment : segments) {
if (segment.getNext() == null && segment.getEnd() != null) {
final Vector2D end = segment.getEnd();
ConnectableSegment selectedNext = null;
double min = Double.POSITIVE_INFINITY;
for (final ConnectableSegment candidateNext : segments) {
if (candidateNext.getPrevious() == null && candidateNext.getStart() != null) {
final double distance = end.distance(candidateNext.getStart());
if (distance < min) {
selectedNext = candidateNext;
min = distance;
}
}
}
if (getPrecision().eqZero(min)) {
// connect the two segments
segment.setNext(selectedNext);
selectedNext.setPrevious(segment);
++connected;
}
}
}
return connected;
}
/** Get first unprocessed segment from a list.
* @param segments segments list
* @return first segment that has not been processed yet
* or null if all segments have been processed
*/
private ConnectableSegment getUnprocessed(final List<ConnectableSegment> segments) {
for (final ConnectableSegment segment : segments) {
if (!segment.isProcessed()) {
return segment;
}
}
return null;
}
/** Build the loop containing a segment.
* <p>
* The segment put in the loop will be marked as processed.
* </p>
* @param defining segment used to define the loop
* @return loop containing the segment (may be null if the loop is a
* degenerated infinitely thin 2 points loop
*/
private List<Segment> followLoop(final ConnectableSegment defining) {
final List<Segment> loop = new ArrayList<>();
loop.add(defining);
defining.setProcessed(true);
// add segments in connection order
ConnectableSegment next = defining.getNext();
while (next != defining && next != null) {
loop.add(next);
next.setProcessed(true);
next = next.getNext();
}
if (next == null) {
// the loop is open and we have found its end,
// we need to find its start too
ConnectableSegment previous = defining.getPrevious();
while (previous != null) {
loop.add(0, previous);
previous.setProcessed(true);
previous = previous.getPrevious();
}
}
// filter out spurious vertices
filterSpuriousVertices(loop);
if (loop.size() == 2 && loop.get(0).getStart() != null) {
// this is a degenerated infinitely thin closed loop, we simply ignore it
return null;
} else {
return loop;
}
}
/** Filter out spurious vertices on straight lines (at machine precision).
* @param loop segments loop to filter (will be modified in-place)
*/
private void filterSpuriousVertices(final List<Segment> loop) {
// we need at least 2 segments in order for one of the contained vertices
// to be unnecessary
if (loop.size() > 1) {
// Go through the list and compare each segment with the next
// one in line. We can remove the shared vertex if the segments
// are not infinite and they lie on the same line.
for (int i = 0; i < loop.size(); ++i) {
final Segment previous = loop.get(i);
int j = (i + 1) % loop.size();
final Segment next = loop.get(j);
if (next != null &&
previous.getStart() != null && next.getEnd() != null &&
Precision.equals(previous.getLine().getAngle(), next.getLine().getAngle(), Precision.EPSILON)) {
// the vertex between the two edges is a spurious one
// replace the two segments by a single one
loop.set(j, new Segment(previous.getStart(), next.getEnd(), previous.getLine()));
loop.remove(i--);
}
}
}
}
/** Private extension of Segment allowing connection. */
private static class ConnectableSegment extends Segment {
/** Node containing segment. */
private final BSPTree<Vector2D> node;
/** Node whose intersection with current node defines start point. */
private final BSPTree<Vector2D> startNode;
/** Node whose intersection with current node defines end point. */
private final BSPTree<Vector2D> endNode;
/** Previous segment. */
private ConnectableSegment previous;
/** Next segment. */
private ConnectableSegment next;
/** Indicator for completely processed segments. */
private boolean processed;
/** Build a segment.
* @param start start point of the segment
* @param end end point of the segment
* @param line line containing the segment
* @param node node containing the segment
* @param startNode node whose intersection with current node defines start point
* @param endNode node whose intersection with current node defines end point
*/
ConnectableSegment(final Vector2D start, final Vector2D end, final Line line,
final BSPTree<Vector2D> node,
final BSPTree<Vector2D> startNode,
final BSPTree<Vector2D> endNode) {
super(start, end, line);
this.node = node;
this.startNode = startNode;
this.endNode = endNode;
this.previous = null;
this.next = null;
this.processed = false;
}
/** Get the node containing segment.
* @return node containing segment
*/
public BSPTree<Vector2D> getNode() {
return node;
}
/** Get the node whose intersection with current node defines start point.
* @return node whose intersection with current node defines start point
*/
public BSPTree<Vector2D> getStartNode() {
return startNode;
}
/** Get the node whose intersection with current node defines end point.
* @return node whose intersection with current node defines end point
*/
public BSPTree<Vector2D> getEndNode() {
return endNode;
}
/** Get the previous segment.
* @return previous segment
*/
public ConnectableSegment getPrevious() {
return previous;
}
/** Set the previous segment.
* @param previous previous segment
*/
public void setPrevious(final ConnectableSegment previous) {
this.previous = previous;
}
/** Get the next segment.
* @return next segment
*/
public ConnectableSegment getNext() {
return next;
}
/** Set the next segment.
* @param next previous segment
*/
public void setNext(final ConnectableSegment next) {
this.next = next;
}
/** Set the processed flag.
* @param processed processed flag to set
*/
public void setProcessed(final boolean processed) {
this.processed = processed;
}
/** Check if the segment has been processed.
* @return true if the segment has been processed
*/
public boolean isProcessed() {
return processed;
}
}
/** Visitor building segments. */
private static class SegmentsBuilder implements BSPTreeVisitor<Vector2D> {
/** Object used to determine floating point equality */
private final DoublePrecisionContext precision;
/** Built segments. */
private final List<ConnectableSegment> segments;
/** Simple constructor.
* @param precision precision context used to compare floating point values
*/
SegmentsBuilder(final DoublePrecisionContext precision) {
this.precision = precision;
this.segments = new ArrayList<>();
}
/** {@inheritDoc} */
@Override
public Order visitOrder(final BSPTree<Vector2D> node) {
return Order.MINUS_SUB_PLUS;
}
/** {@inheritDoc} */
@Override
public void visitInternalNode(final BSPTree<Vector2D> node) {
@SuppressWarnings("unchecked")
final BoundaryAttribute<Vector2D> attribute = (BoundaryAttribute<Vector2D>) node.getAttribute();
final Iterable<BSPTree<Vector2D>> splitters = attribute.getSplitters();
if (attribute.getPlusOutside() != null) {
addContribution(attribute.getPlusOutside(), node, splitters, false);
}
if (attribute.getPlusInside() != null) {
addContribution(attribute.getPlusInside(), node, splitters, true);
}
}
/** {@inheritDoc} */
@Override
public void visitLeafNode(final BSPTree<Vector2D> node) {
}
/** Add the contribution of a boundary facet.
* @param sub boundary facet
* @param node node containing segment
* @param splitters splitters for the boundary facet
* @param reversed if true, the facet has the inside on its plus side
*/
private void addContribution(final SubHyperplane<Vector2D> sub,
final BSPTree<Vector2D> node,
final Iterable<BSPTree<Vector2D>> splitters,
final boolean reversed) {
@SuppressWarnings("unchecked")
final AbstractSubHyperplane<Vector2D, Vector1D> absSub =
(AbstractSubHyperplane<Vector2D, Vector1D>) sub;
final Line line = (Line) sub.getHyperplane();
final List<Interval> intervals = ((IntervalsSet) absSub.getRemainingRegion()).asList();
for (final Interval i : intervals) {
// find the 2D points
final Vector2D startV = Double.isInfinite(i.getInf()) ?
null : line.toSpace(Vector1D.of(i.getInf()));
final Vector2D endV = Double.isInfinite(i.getSup()) ?
null : line.toSpace(Vector1D.of(i.getSup()));
// recover the connectivity information
final BSPTree<Vector2D> startN = selectClosest(startV, splitters);
final BSPTree<Vector2D> endN = selectClosest(endV, splitters);
if (reversed) {
segments.add(new ConnectableSegment(endV, startV, line.reverse(),
node, endN, startN));
} else {
segments.add(new ConnectableSegment(startV, endV, line,
node, startN, endN));
}
}
}
/** Select the node whose cut sub-hyperplane is closest to specified point.
* @param point reference point
* @param candidates candidate nodes
* @return node closest to point, or null if point is null or no node is closer than tolerance
*/
private BSPTree<Vector2D> selectClosest(final Vector2D point, final Iterable<BSPTree<Vector2D>> candidates) {
if (point != null) {
BSPTree<Vector2D> selected = null;
double min = Double.POSITIVE_INFINITY;
for (final BSPTree<Vector2D> node : candidates) {
final double distance = Math.abs(node.getCut().getHyperplane().getOffset(point));
if (distance < min) {
selected = node;
min = distance;
}
}
if (precision.eqZero(min)) {
return selected;
}
}
return null;
}
/** Get the segments.
* @return built segments
*/
public List<ConnectableSegment> getSegments() {
return segments;
}
}
}