All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.randomdwi.polygonclipping.BooleanOperation Maven / Gradle / Ivy

There is a newer version: 1.0.2
Show newest version
package com.github.randomdwi.polygonclipping;

import com.github.randomdwi.polygonclipping.enums.EdgeType;
import com.github.randomdwi.polygonclipping.enums.PolygonType;
import com.github.randomdwi.polygonclipping.geometry.BoundingBox;
import com.github.randomdwi.polygonclipping.geometry.Contour;
import com.github.randomdwi.polygonclipping.geometry.Intersection;
import com.github.randomdwi.polygonclipping.geometry.Point;
import com.github.randomdwi.polygonclipping.segment.Segment;
import com.github.randomdwi.polygonclipping.segment.SegmentComparator;
import com.github.randomdwi.polygonclipping.sweepline.SweepEvent;
import com.github.randomdwi.polygonclipping.sweepline.SweepEventComparator;
import com.github.randomdwi.polygonclipping.sweepline.SweepLine;

import java.util.*;

import static com.github.randomdwi.polygonclipping.BooleanOperation.Type.*;

public class BooleanOperation {

    public enum Type {
        INTERSECTION,
        UNION,
        DIFFERENCE,
        XOR
    }

    private Polygon subject;
    private Polygon clipping;
    private Polygon result;
    private Type operation;

    private SweepEventComparator sweepEventComparator = new SweepEventComparator(false); // to compare events
    private SweepLine sweepLine = new SweepLine(new SegmentComparator(false));
    private Deque sortedEvents = new LinkedList<>();

    /**
     * Instantiates a new Boolean operation.
     *
     * @param subject   the subject polygon
     * @param clip      the clipping polygon
     * @param operation the operation
     */
    BooleanOperation(Polygon subject, Polygon clip, BooleanOperation.Type operation) {
        this.subject = subject.copy();
        this.clipping = clip.copy();
        this.operation = operation;
        this.result = new Polygon();
    }

    /**
     * Execute boolean operation.
     *
     * @return result polygon
     */
    public Polygon execute() {

        BoundingBox subjectBB = subject.boundingBox();     // for optimizations 1 and 2
        BoundingBox clippingBB = clipping.boundingBox();   // for optimizations 1 and 2
        double MINMAXX = Math.min(subjectBB.xMax, clippingBB.xMax); // for optimization 2

        if (trivialOperation(subjectBB, clippingBB)) {
            // trivial cases can be quickly resolved without sweeping the plane
            return result;
        }

        for (int i = 0; i < subject.contourCount(); i++) {
            for (int j = 0; j < subject.contour(i).pointCount(); j++) {
                processSegment(subject.contour(i).segment(j), PolygonType.SUBJECT);
            }
        }

        for (int i = 0; i < clipping.contourCount(); i++) {
            for (int j = 0; j < clipping.contour(i).pointCount(); j++) {
                processSegment(clipping.contour(i).segment(j), PolygonType.CLIPPING);
            }
        }

        while (!sweepLine.eventQueue.isEmpty()) {
            SweepEvent se = sweepLine.eventQueue.poll();
            // optimization 2
            if ((INTERSECTION.equals(operation) && se.point.x > MINMAXX) ||
                    (DIFFERENCE.equals(operation) && se.point.x > subjectBB.xMax)) {
                connectEdges();
                return result;
            }
            sortedEvents.add(se);

            if (se.left) { // the line segment must be inserted into sl

                sweepLine.statusLine.addEvent(se);

                SweepEvent prev = sweepLine.statusLine.getPreviousEvent(se);
                SweepEvent next = sweepLine.statusLine.getNextEvent(se);

                computeFields(se, prev);
                // Process a possible intersection between "se" and its next neighbor in sl
                if (next != null) {
                    if (possibleIntersection(se, next) == 2) {
                        computeFields(se, prev);
                        computeFields(next, se);
                    }
                }
                // Process a possible intersection between "se" and its previous neighbor in sl
                if (prev != null) {
                    if (possibleIntersection(prev, se) == 2) {
                        SweepEvent prevPrev = sweepLine.statusLine.getPreviousEvent(prev);
                        computeFields(prev, prevPrev);
                        computeFields(se, prev);
                    }
                }
            } else {
                // the line segment must be removed from sl
                se = se.otherEvent; // we work with the left event

                SweepEvent prev = sweepLine.statusLine.getPreviousEvent(se);
                SweepEvent next = sweepLine.statusLine.getNextEvent(se);

                // delete line segment associated to "se" from sl
                sweepLine.statusLine.removeEvent(se);

                if (prev != null && next != null) {
                    //check for intersection between the neighbors of "se" in sl
                    possibleIntersection(prev, next);
                }
            }
        }
        connectEdges();
        return result;
    }

    private boolean trivialOperation(BoundingBox subjectBB, BoundingBox clippingBB) {

        // Test 1 for trivial result case (at least one of the polygons is empty)
        if (subject.isEmpty() || clipping.isEmpty()) {
            if (DIFFERENCE.equals(operation)) {
                result = subject;
            }
            if (UNION.equals(operation) || XOR.equals(operation)) {
                result = subject.isEmpty() ? clipping : subject;
            }
            return true;
        }
        // Test 2 for trivial result case (the bounding boxes do not overlap)
        if (subjectBB.xMin > clippingBB.xMax || clippingBB.xMin > subjectBB.xMax ||
                subjectBB.yMin > clippingBB.yMax || clippingBB.yMin > subjectBB.yMax) {
            if (DIFFERENCE.equals(operation)) {
                result = subject;
            }
            if (UNION.equals(operation) || XOR.equals(operation)) {
                result = subject;
                result.join(clipping);
            }
            return true;
        }
        return false;
    }

    /**
     * Compute the events associated to segment s, and insert them into pq and eq
     */
    private void processSegment(Segment s, PolygonType pt) {
//        // if the two edge endpoints are equal the segment is dicarded
//        if (s.degenerate ()) {
//            // This can be done as preprocessing to avoid "polygons" with less than 3 edges */
//            return;
//        }
        SweepEvent e1 = new SweepEvent(s.pBegin, true, null, pt);
        SweepEvent e2 = new SweepEvent(s.pEnd, true, e1, pt);
        e1.otherEvent = e2;

        if (s.min().equals(s.pBegin)) {
            e2.left = false;
        } else {
            e1.left = false;
        }
        sweepLine.eventQueue.add(e1);
        sweepLine.eventQueue.add(e2);
    }

    /**
     * Process a possible intersection between the edges associated to the left events le1 and le2
     */
    private int possibleIntersection(SweepEvent le1, SweepEvent le2) {

        // you can uncomment these two lines if self-intersecting polygons are not allowed
//        if (le1.polygonType.equals(le2.polygonType)) {
//            // self intersection
//            return 0;
//        }

        Intersection intersections = new Intersection(le1.segment(), le2.segment());

        if (Intersection.Type.NO_INTERSECTION.equals(intersections.type)) {
            // no intersection
            return 0;
        }

        if ((Intersection.Type.POINT.equals(intersections.type)) && ((le1.point.equals(le2.point)) || (le1.otherEvent.point.equals(le2.otherEvent.point)))) {
            // the line segments intersect at an endpoint of both line segments
            return 0;
        }

        if (Intersection.Type.OVERLAPPING.equals(intersections.type) && le1.polygonType.equals(le2.polygonType)) {
            throw new IllegalStateException("edges of the same polygon overlap");
        }

        // The line segments associated to le1 and le2 intersect
        if (Intersection.Type.POINT.equals(intersections.type)) {
            if (!le1.point.equals(intersections.point) && !le1.otherEvent.point.equals(intersections.point)) {
                // if the intersection point is not an endpoint of le1.segment ()
                divideSegment(le1, intersections.point);
            }
            if (!le2.point.equals(intersections.point) && !le2.otherEvent.point.equals(intersections.point)) {
                // if the intersection point is not an endpoint of le2.segment ()
                divideSegment(le2, intersections.point);
            }
            return 1;
        }
        // The line segments associated to le1 and le2 overlap
        List sortedEvents = new ArrayList<>();

        if (le1.point.equals(le2.point)) {
            sortedEvents.add(null);
        } else if (sweepEventComparator.compare(le1, le2) < 0) {
            sortedEvents.add(le2);
            sortedEvents.add(le1);
        } else {
            sortedEvents.add(le1);
            sortedEvents.add(le2);
        }
        if (le1.otherEvent.point.equals(le2.otherEvent.point)) {
            sortedEvents.add(null);
        } else if (sweepEventComparator.compare(le1.otherEvent, le2.otherEvent) < 0) {
            sortedEvents.add(le2.otherEvent);
            sortedEvents.add(le1.otherEvent);
        } else {
            sortedEvents.add(le1.otherEvent);
            sortedEvents.add(le2.otherEvent);
        }

        if ((sortedEvents.size() == 2) || (sortedEvents.size() == 3 && sortedEvents.get(2) != null)) {
            // both line segments are equal or share the left endpoint
            le1.type = EdgeType.NON_CONTRIBUTING;
            le2.type = (le1.inOut == le2.inOut) ? EdgeType.SAME_TRANSITION : EdgeType.DIFFERENT_TRANSITION;
            if (sortedEvents.size() == 3) {
                divideSegment(sortedEvents.get(2).otherEvent, sortedEvents.get(1).point);
            }
            return 2;
        }
        if (sortedEvents.size() == 3) { // the line segments share the right endpoint
            divideSegment(sortedEvents.get(0), sortedEvents.get(1).point);
            return 3;
        }
        if (sortedEvents.get(0) != sortedEvents.get(3).otherEvent) {
            // no line segment includes totally the other one
            divideSegment(sortedEvents.get(0), sortedEvents.get(1).point);
            divideSegment(sortedEvents.get(1), sortedEvents.get(2).point);
            return 3;
        }
        // one line segment includes the other one
        divideSegment(sortedEvents.get(0), sortedEvents.get(1).point);
        divideSegment(sortedEvents.get(3).otherEvent, sortedEvents.get(2).point);
        return 3;
    }

    /**
     * Divide the segment associated to left event le, updating pq and (implicitly) the status line
     */
    private void divideSegment(SweepEvent le, Point p) {

        // "Right event" of the "left line segment" resulting from dividing le->segment ()
        SweepEvent r = new SweepEvent(p, false, le, le.polygonType);
        // "Left event" of the "right line segment" resulting from dividing le->segment ()
        SweepEvent l = new SweepEvent(p, true, le.otherEvent, le.polygonType);
        if (sweepEventComparator.compare(l, le.otherEvent) < 0) { // avoid a rounding error. The left event would be processed after the right event
            le.otherEvent.left = true;
            l.left = false;
        }
        le.otherEvent.otherEvent = l;
        le.otherEvent = r;
        sweepLine.eventQueue.add(l);
        sweepLine.eventQueue.add(r);
    }

    /**
     * return if the left event le belongs to the result of the boolean operation
     */
    private boolean inResult(SweepEvent le) {
        switch (le.type) {
            case NORMAL:
                switch (operation) {
                    case INTERSECTION:
                        return !le.otherInOut;
                    case UNION:
                        return le.otherInOut;
                    case DIFFERENCE:
                        return (PolygonType.SUBJECT.equals(le.polygonType) && le.otherInOut) || (PolygonType.CLIPPING.equals(le.polygonType) && !le.otherInOut);
                    case XOR:
                        return true;
                }
            case SAME_TRANSITION:
                return operation == INTERSECTION || operation == UNION;
            case DIFFERENT_TRANSITION:
                return operation == DIFFERENCE;
            case NON_CONTRIBUTING:
                return false;
        }
        throw new IllegalStateException("unexpected event type");
    }

    /**
     * compute several fields of left event le
     */
    private void computeFields(SweepEvent le, SweepEvent prev) {
        // compute inOut and otherInOut fields
        if (prev == null) {
            le.inOut = false;
            le.otherInOut = true;
        } else if (le.polygonType.equals(prev.polygonType)) {
            // previous line segment in sl belongs to the same polygon that "se" belongs to
            le.inOut = !prev.inOut;
            le.otherInOut = prev.otherInOut;
        } else {
            // previous line segment in sl belongs to a different polygon that "se" belongs to
            le.inOut = !prev.otherInOut;
            le.otherInOut = prev.vertical() != prev.inOut;
        }
        // compute prevInResult field
        if (prev != null) {
            le.prevInResult = (!inResult(prev) || prev.vertical()) ? prev.prevInResult : prev;
        }
        // check if the line segment belongs to the Boolean operation
        le.inResult = inResult(le);
    }

    // connect the solution edges to build the result polygon
    private void connectEdges() {
        // copy the events in the result polygon to resultEvents array
        List resultEvents = new ArrayList<>(sortedEvents.size());

        for (SweepEvent event : sortedEvents) {
            if ((event.left && event.inResult) || (!event.left && event.otherEvent.inResult)) {
                resultEvents.add(event);
            }
        }

        //TODO refactor sweepEventComparator
        SweepEventComparator sec2 = new SweepEventComparator(true);                    // to compare events

        // Due to overlapping edges the resultEvents array can be not wholly sorted
        boolean sorted = false;
        while (!sorted) {
            sorted = true;

            for (int i = 0; i < resultEvents.size() - 1; ++i) {
                SweepEvent event = resultEvents.get(i);
                SweepEvent nextEvent = resultEvents.get(i + 1);

                if (sec2.compare(event, nextEvent) >= 0) {
                    // swap
                    resultEvents.set(i, nextEvent);
                    resultEvents.set(i + 1, event);
                    sorted = false;
                }
            }
        }

        for (int i = 0; i < resultEvents.size(); ++i) {
            SweepEvent event = resultEvents.get(i);

            if (event.left) {
                event.pos = i;
            } else {
                event.pos = event.otherEvent.pos;
                event.otherEvent.pos = i;
            }
        }

        Set processed = new HashSet<>(resultEvents.size());
        List depth = new ArrayList<>();
        List holeOf = new ArrayList<>();

        for (int i = 0; i < resultEvents.size(); i++) {

            SweepEvent event = resultEvents.get(i);

            if (processed.contains(i)) {
                continue;
            }

            Contour contour = new Contour();
            result.addContour(contour);

            int contourId = result.contourCount() - 1;
            depth.add(0);
            holeOf.add(-1);

            if (event.prevInResult != null) {
                int lowerContourId = event.prevInResult.contourId;
                if (!event.prevInResult.resultInOut) {
                    result.contour(lowerContourId).addHole(contourId);
                    holeOf.set(contourId, lowerContourId);
                    depth.set(contourId, depth.get(lowerContourId) + 1);
                    contour.setIsHole(true);
                } else if (result.contour(lowerContourId).isHole()) {
                    result.contour(holeOf.get(lowerContourId)).addHole(contourId);
                    holeOf.set(contourId, holeOf.get(lowerContourId));
                    depth.set(contourId, depth.get(lowerContourId));
                    contour.setIsHole(true);
                }
            }

            int pos = i;
            Point initial = event.point;
            contour.add(initial);

            while (!resultEvents.get(pos).otherEvent.point.equals(initial)) {
                processed.add(pos);
                if (resultEvents.get(pos).left) {
                    resultEvents.get(pos).resultInOut = false;
                    resultEvents.get(pos).contourId = contourId;
                } else {
                    resultEvents.get(pos).otherEvent.resultInOut = true;
                    resultEvents.get(pos).otherEvent.contourId = contourId;
                }
                pos = resultEvents.get(pos).pos;
                processed.add(pos);
                contour.add(resultEvents.get(pos).point);
                pos = nextPos(pos, resultEvents, processed);
            }
            processed.add(resultEvents.get(pos).pos);
            processed.add(pos);
            resultEvents.get(pos).otherEvent.resultInOut = true;
            resultEvents.get(pos).otherEvent.contourId = contourId;
            if (depth.get(contourId) % 2 == 1) {
                contour.changeOrientation();
            }
        }
    }

    private int nextPos(int pos, List resultEvents, Set processed) {
        int newPos = pos + 1;
        while (newPos < resultEvents.size() && resultEvents.get(newPos).point.equals(resultEvents.get(pos).point)) {
            if (!processed.contains(newPos)) {
                return newPos;
            } else {
                ++newPos;
            }
        }
        newPos = pos - 1;
        while (processed.contains(newPos)) {
            --newPos;
        }
        return newPos;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy