RegionBSPTree2D.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.geometry.euclidean.twod;

  18. import java.util.ArrayList;
  19. import java.util.Collections;
  20. import java.util.List;
  21. import java.util.stream.Collectors;
  22. import java.util.stream.Stream;
  23. import java.util.stream.StreamSupport;

  24. import org.apache.commons.geometry.core.partitioning.Hyperplane;
  25. import org.apache.commons.geometry.core.partitioning.Split;
  26. import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
  27. import org.apache.commons.geometry.core.partitioning.bsp.AbstractPartitionedRegionBuilder;
  28. import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
  29. import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor;
  30. import org.apache.commons.geometry.core.partitioning.bsp.RegionCutBoundary;
  31. import org.apache.commons.geometry.euclidean.twod.path.InteriorAngleLinePathConnector;
  32. import org.apache.commons.geometry.euclidean.twod.path.LinePath;
  33. import org.apache.commons.numbers.core.Precision;

  34. /** Binary space partitioning (BSP) tree representing a region in two dimensional
  35.  * Euclidean space.
  36.  */
  37. public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, RegionBSPTree2D.RegionNode2D>
  38.     implements BoundarySource2D {

  39.     /** List of line subset paths comprising the region boundary. */
  40.     private List<LinePath> boundaryPaths;

  41.     /** Create a new, empty region.
  42.      */
  43.     public RegionBSPTree2D() {
  44.         this(false);
  45.     }

  46.     /** Create a new region. If {@code full} is true, then the region will
  47.      * represent the entire 2D space. Otherwise, it will be empty.
  48.      * @param full whether or not the region should contain the entire
  49.      *      2D space or be empty
  50.      */
  51.     public RegionBSPTree2D(final boolean full) {
  52.         super(full);
  53.     }

  54.     /** Return a deep copy of this instance.
  55.      * @return a deep copy of this instance.
  56.      * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
  57.      */
  58.     public RegionBSPTree2D copy() {
  59.         final RegionBSPTree2D result = RegionBSPTree2D.empty();
  60.         result.copy(this);

  61.         return result;
  62.     }

  63.     /** {@inheritDoc} */
  64.     @Override
  65.     public Iterable<LineConvexSubset> boundaries() {
  66.         return createBoundaryIterable(LineConvexSubset.class::cast);
  67.     }

  68.     /** {@inheritDoc} */
  69.     @Override
  70.     public Stream<LineConvexSubset> boundaryStream() {
  71.         return StreamSupport.stream(boundaries().spliterator(), false);
  72.     }

  73.     /** {@inheritDoc} */
  74.     @Override
  75.     public List<LineConvexSubset> getBoundaries() {
  76.         return createBoundaryList(LineConvexSubset.class::cast);
  77.     }

  78.     /** Get the boundary of the region as a list of connected line subset paths.
  79.      * The line subset are oriented such that their minus (left) side lies on the
  80.      * interior of the region.
  81.      * @return line subset paths representing the region boundary
  82.      */
  83.     public List<LinePath> getBoundaryPaths() {
  84.         if (boundaryPaths == null) {
  85.             boundaryPaths = Collections.unmodifiableList(computeBoundaryPaths());
  86.         }
  87.         return boundaryPaths;
  88.     }

  89.     /** Add a convex area to this region. The resulting region will be the
  90.      * union of the convex area and the region represented by this instance.
  91.      * @param area the convex area to add
  92.      */
  93.     public void add(final ConvexArea area) {
  94.         union(area.toTree());
  95.     }

  96.     /** Return a list of {@link ConvexArea}s representing the same region
  97.      * as this instance. One convex area is returned for each interior leaf
  98.      * node in the tree.
  99.      * @return a list of convex areas representing the same region as this
  100.      *      instance
  101.      */
  102.     public List<ConvexArea> toConvex() {
  103.         final List<ConvexArea> result = new ArrayList<>();

  104.         toConvexRecursive(getRoot(), ConvexArea.full(), result);

  105.         return result;
  106.     }

  107.     /** Recursive method to compute the convex areas of all inside leaf nodes in the subtree rooted at the given
  108.      * node. The computed convex areas are added to the given list.
  109.      * @param node root of the subtree to compute the convex areas for
  110.      * @param nodeArea the convex area for the current node; this will be split by the node's cut hyperplane to
  111.      *      form the convex areas for any child nodes
  112.      * @param result list containing the results of the computation
  113.      */
  114.     private void toConvexRecursive(final RegionNode2D node, final ConvexArea nodeArea,
  115.             final List<? super ConvexArea> result) {
  116.         if (node.isLeaf()) {
  117.             // base case; only add to the result list if the node is inside
  118.             if (node.isInside()) {
  119.                 result.add(nodeArea);
  120.             }
  121.         } else {
  122.             // recurse
  123.             final Split<ConvexArea> split = nodeArea.split(node.getCutHyperplane());

  124.             toConvexRecursive(node.getMinus(), split.getMinus(), result);
  125.             toConvexRecursive(node.getPlus(), split.getPlus(), result);
  126.         }
  127.     }

  128.     /** {@inheritDoc} */
  129.     @Override
  130.     public Split<RegionBSPTree2D> split(final Hyperplane<Vector2D> splitter) {
  131.         return split(splitter, RegionBSPTree2D.empty(), RegionBSPTree2D.empty());
  132.     }

  133.     /** {@inheritDoc} */
  134.     @Override
  135.     public Vector2D project(final Vector2D pt) {
  136.         // use our custom projector so that we can disambiguate points that are
  137.         // actually equidistant from the target point
  138.         final BoundaryProjector2D projector = new BoundaryProjector2D(pt);
  139.         accept(projector);

  140.         return projector.getProjected();
  141.     }

  142.     /** Return the current instance.
  143.      */
  144.     @Override
  145.     public RegionBSPTree2D toTree() {
  146.         return this;
  147.     }

  148.     /** {@inheritDoc} */
  149.     @Override
  150.     public List<LinecastPoint2D> linecast(final LineConvexSubset subset) {
  151.         final LinecastVisitor visitor = new LinecastVisitor(subset, false);
  152.         accept(visitor);

  153.         return visitor.getResults();
  154.     }

  155.     /** {@inheritDoc} */
  156.     @Override
  157.     public LinecastPoint2D linecastFirst(final LineConvexSubset subset) {
  158.         final LinecastVisitor visitor = new LinecastVisitor(subset, true);
  159.         accept(visitor);

  160.         return visitor.getFirstResult();
  161.     }

  162.     /** Compute the line subset paths comprising the region boundary.
  163.      * @return the line subset paths comprising the region boundary
  164.      */
  165.     private List<LinePath> computeBoundaryPaths() {
  166.         final InteriorAngleLinePathConnector connector = new InteriorAngleLinePathConnector.Minimize();
  167.         connector.connect(boundaries());

  168.         return connector.connectAll().stream()
  169.                 .map(LinePath::simplify).collect(Collectors.toList());
  170.     }

  171.     /** {@inheritDoc} */
  172.     @Override
  173.     protected RegionSizeProperties<Vector2D> computeRegionSizeProperties() {
  174.         // handle simple cases
  175.         if (isFull()) {
  176.             return new RegionSizeProperties<>(Double.POSITIVE_INFINITY, null);
  177.         } else if (isEmpty()) {
  178.             return new RegionSizeProperties<>(0, null);
  179.         }

  180.         // compute the size based on the boundary line subsets
  181.         double quadrilateralAreaSum = 0.0;

  182.         double scaledSumX = 0.0;
  183.         double scaledSumY = 0.0;

  184.         Vector2D startPoint;
  185.         Vector2D endPoint;
  186.         double signedArea;

  187.         for (final LineConvexSubset boundary : boundaries()) {

  188.             if (boundary.isInfinite()) {
  189.                 // at least on boundary is infinite, meaning that
  190.                 // the size is also infinite
  191.                 quadrilateralAreaSum = Double.POSITIVE_INFINITY;

  192.                 break;
  193.             }

  194.             startPoint = boundary.getStartPoint();
  195.             endPoint = boundary.getEndPoint();

  196.             // compute the area
  197.             signedArea = startPoint.signedArea(endPoint);

  198.             quadrilateralAreaSum += signedArea;

  199.             // compute scaled coordinate values for the centroid
  200.             scaledSumX += signedArea * (startPoint.getX() + endPoint.getX());
  201.             scaledSumY += signedArea * (startPoint.getY() + endPoint.getY());
  202.         }

  203.         double size = Double.POSITIVE_INFINITY;
  204.         Vector2D centroid = null;

  205.         // The area is finite only if the computed quadrilateral area is finite and non-negative.
  206.         // Negative areas indicate that the region is inside-out, with a finite outside surrounded
  207.         // by an infinite inside.
  208.         if (quadrilateralAreaSum >= 0 && Double.isFinite(quadrilateralAreaSum)) {
  209.             size = 0.5 * quadrilateralAreaSum;

  210.             if (quadrilateralAreaSum > 0) {
  211.                 centroid = Vector2D.of(scaledSumX, scaledSumY).multiply(1.0 / (3.0 * quadrilateralAreaSum));
  212.             }
  213.         }

  214.         return new RegionSizeProperties<>(size, centroid);
  215.     }

  216.     /** {@inheritDoc} */
  217.     @Override
  218.     protected void invalidate() {
  219.         super.invalidate();

  220.         boundaryPaths = null;
  221.     }

  222.     /** {@inheritDoc} */
  223.     @Override
  224.     protected RegionNode2D createNode() {
  225.         return new RegionNode2D(this);
  226.     }

  227.     /** Return a new {@link RegionBSPTree2D} instance containing the entire space.
  228.      * @return a new {@link RegionBSPTree2D} instance containing the entire space
  229.      */
  230.     public static RegionBSPTree2D full() {
  231.         return new RegionBSPTree2D(true);
  232.     }

  233.     /** Return a new, empty {@link RegionBSPTree2D} instance.
  234.      * @return a new, empty {@link RegionBSPTree2D} instance
  235.      */
  236.     public static RegionBSPTree2D empty() {
  237.         return new RegionBSPTree2D(false);
  238.     }

  239.     /** Construct a new tree from the given boundaries. If no boundaries
  240.      * are present, the returned tree is empty.
  241.      * @param boundaries boundaries to construct the tree from
  242.      * @return a new tree instance constructed from the given boundaries
  243.      * @see #from(Iterable, boolean)
  244.      */
  245.     public static RegionBSPTree2D from(final Iterable<? extends LineConvexSubset> boundaries) {
  246.         return from(boundaries, false);
  247.     }

  248.     /** Construct a new tree from the given boundaries. If {@code full} is true, then
  249.      * the initial tree before boundary insertion contains the entire space. Otherwise,
  250.      * it is empty.
  251.      * @param boundaries boundaries to construct the tree from
  252.      * @param full if true, the initial tree will contain the entire space
  253.      * @return a new tree instance constructed from the given boundaries
  254.      */
  255.     public static RegionBSPTree2D from(final Iterable<? extends LineConvexSubset> boundaries, final boolean full) {
  256.         final RegionBSPTree2D tree = new RegionBSPTree2D(full);
  257.         tree.insert(boundaries);

  258.         return tree;
  259.     }

  260.     /** Create a new {@link PartitionedRegionBuilder2D} instance which can be used to build balanced
  261.      * BSP trees from region boundaries.
  262.      * @return a new {@link PartitionedRegionBuilder2D} instance
  263.      */
  264.     public static PartitionedRegionBuilder2D partitionedRegionBuilder() {
  265.         return new PartitionedRegionBuilder2D();
  266.     }

  267.     /** BSP tree node for two dimensional Euclidean space.
  268.      */
  269.     public static final class RegionNode2D extends AbstractRegionBSPTree.AbstractRegionNode<Vector2D, RegionNode2D> {
  270.         /** Simple constructor.
  271.          * @param tree the owning tree instance
  272.          */
  273.         private RegionNode2D(final AbstractBSPTree<Vector2D, RegionNode2D> tree) {
  274.             super(tree);
  275.         }

  276.         /** Get the region represented by this node. The returned region contains
  277.          * the entire area contained in this node, regardless of the attributes of
  278.          * any child nodes.
  279.          * @return the region represented by this node
  280.          */
  281.         public ConvexArea getNodeRegion() {
  282.             ConvexArea area = ConvexArea.full();

  283.             RegionNode2D child = this;
  284.             RegionNode2D parent;

  285.             while ((parent = child.getParent()) != null) {
  286.                 final Split<ConvexArea> split = area.split(parent.getCutHyperplane());

  287.                 area = child.isMinus() ? split.getMinus() : split.getPlus();

  288.                 child = parent;
  289.             }

  290.             return area;
  291.         }

  292.         /** {@inheritDoc} */
  293.         @Override
  294.         protected RegionNode2D getSelf() {
  295.             return this;
  296.         }
  297.     }

  298.     /** Class used to build regions in Euclidean 2D space by inserting boundaries into a BSP
  299.      * tree containing "partitions", i.e. structural cuts where both sides of the cut have the same region location.
  300.      * When partitions are chosen that effectively divide the region boundaries at each partition level, the
  301.      * constructed tree is shallower and more balanced than one constructed from the region boundaries alone,
  302.      * resulting in improved performance. For example, consider a line segment approximation of a circle. The region is
  303.      * convex so each boundary has all of the other boundaries on its minus side; the plus sides are all empty.
  304.      * When these boundaries are inserted directly into a tree, the tree degenerates into a simple linked list of
  305.      * nodes with a height directly proportional to the number of boundaries. This means that many operations on the
  306.      * tree, such as inside/outside testing of points, involve iterating through each and every region boundary. In
  307.      * contrast, if a partition is first inserted that passes through the circle center, the first BSP tree node
  308.      * contains region nodes on its plus <em>and</em> minus sides, cutting the height of the tree in half. Operations
  309.      * such as inside/outside testing are then able to skip half of the tree nodes with a single test on the
  310.      * root node, resulting in drastically improved performance. Insertion of additional partitions (using a grid
  311.      * layout, for example) can produce even shallower trees, although there is a point unique to each boundary set at
  312.      * which the addition of more partitions begins to decrease instead of increase performance.
  313.      *
  314.      * <h2>Usage</h2>
  315.      * <p>Usage of this class consists of two phases: (1) <em>partition insertion</em> and (2) <em>boundary
  316.      * insertion</em>. Instances begin in the <em>partition insertion</em> phase. Here, partitions can be inserted
  317.      * into the empty tree using {@link PartitionedRegionBuilder2D#insertPartition(LineConvexSubset) insertPartition}
  318.      * or similar methods. The {@link org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule#INHERIT INHERIT}
  319.      * cut rule is used internally to insert the cut so the represented region remains empty even as partitions are
  320.      * inserted.
  321.      * </p>
  322.      *
  323.      * <p>The instance moves into the <em>boundary insertion</em> phase when the caller inserts the first region
  324.      * boundary, using {@link PartitionedRegionBuilder2D#insertBoundary(LineConvexSubset) insertBoundary} or
  325.      * similar methods. Attempting to insert a partition after this point results in an {@code IllegalStateException}.
  326.      * This ensures that partitioning cuts are always located higher up the tree than boundary cuts.</p>
  327.      *
  328.      * <p>After all boundaries are inserted, the {@link PartitionedRegionBuilder2D#build() build} method is used
  329.      * to perform final processing and return the computed tree.</p>
  330.      */
  331.     public static final class PartitionedRegionBuilder2D
  332.         extends AbstractPartitionedRegionBuilder<Vector2D, RegionNode2D> {

  333.         /** Construct a new builder instance.
  334.          */
  335.         private PartitionedRegionBuilder2D() {
  336.             super(RegionBSPTree2D.empty());
  337.         }

  338.         /** Insert a partition line.
  339.          * @param partition partition to insert
  340.          * @return this instance
  341.          * @throws IllegalStateException if a boundary has previously been inserted
  342.          */
  343.         public PartitionedRegionBuilder2D insertPartition(final Line partition) {
  344.             return insertPartition(partition.span());
  345.         }

  346.         /** Insert a line convex subset as a partition.
  347.          * @param partition partition to insert
  348.          * @return this instance
  349.          * @throws IllegalStateException if a boundary has previously been inserted
  350.          */
  351.         public PartitionedRegionBuilder2D insertPartition(final LineConvexSubset partition) {
  352.             insertPartitionInternal(partition);

  353.             return this;
  354.         }

  355.         /** Insert two axis aligned lines intersecting at the given point as partitions.
  356.          * The lines each contain the {@code center} point and have the directions {@code +x} and {@code +y}
  357.          * in that order. If inserted into an empty tree, this will partition the space
  358.          * into 4 sections.
  359.          * @param center center point for the partitions; the inserted lines intersect at this point
  360.          * @param precision precision context used to construct the lines
  361.          * @return this instance
  362.          * @throws IllegalStateException if a boundary has previously been inserted
  363.          */
  364.         public PartitionedRegionBuilder2D insertAxisAlignedPartitions(final Vector2D center,
  365.                 final Precision.DoubleEquivalence precision) {

  366.             insertPartition(Lines.fromPointAndDirection(center, Vector2D.Unit.PLUS_X, precision));
  367.             insertPartition(Lines.fromPointAndDirection(center, Vector2D.Unit.PLUS_Y, precision));

  368.             return this;
  369.         }

  370.         /** Insert a grid of partitions. The partitions are constructed recursively: at each level two axis-aligned
  371.          * partitioning lines are inserted using
  372.          * {@link #insertAxisAlignedPartitions(Vector2D, Precision.DoubleEquivalence) insertAxisAlignedPartitions}.
  373.          * The algorithm then recurses using bounding boxes from the min point to the center and from the center
  374.          * point to the max. Note that this means no partitions are ever inserted directly on the boundaries of
  375.          * the given bounding box. This is intentional and done to allow this method to be called directly with the
  376.          * bounding box from a set of boundaries to be inserted without unnecessarily adding partitions that will
  377.          * never have region boundaries on both sides.
  378.          * @param bounds bounding box for the grid
  379.          * @param level recursion level for the grid; each level subdivides each grid cube into 4 sections, making the
  380.          *      total number of grid cubes equal to {@code 4 ^ level}
  381.          * @param precision precision context used to construct the partition lines
  382.          * @return this instance
  383.          * @throws IllegalStateException if a boundary has previously been inserted
  384.          */
  385.         public PartitionedRegionBuilder2D insertAxisAlignedGrid(final Bounds2D bounds, final int level,
  386.                 final Precision.DoubleEquivalence precision) {

  387.             insertAxisAlignedGridRecursive(bounds.getMin(), bounds.getMax(), level, precision);

  388.             return this;
  389.         }

  390.         /** Recursively insert axis-aligned grid partitions.
  391.          * @param min min point for the grid square to partition
  392.          * @param max max point for the grid square to partition
  393.          * @param level current recursion level
  394.          * @param precision precision context used to construct the partition planes
  395.          */
  396.         private void insertAxisAlignedGridRecursive(final Vector2D min, final Vector2D max, final int level,
  397.                 final Precision.DoubleEquivalence precision) {
  398.             if (level > 0) {
  399.                 final Vector2D center = min.lerp(max, 0.5);

  400.                 insertAxisAlignedPartitions(center, precision);

  401.                 final int nextLevel = level - 1;
  402.                 insertAxisAlignedGridRecursive(min, center, nextLevel, precision);
  403.                 insertAxisAlignedGridRecursive(center, max, nextLevel, precision);
  404.             }
  405.         }

  406.         /** Insert a region boundary.
  407.          * @param boundary region boundary to insert
  408.          * @return this instance
  409.          */
  410.         public PartitionedRegionBuilder2D insertBoundary(final LineConvexSubset boundary) {
  411.             insertBoundaryInternal(boundary);

  412.             return this;
  413.         }

  414.         /** Insert a collection of region boundaries.
  415.          * @param boundaries boundaries to insert
  416.          * @return this instance
  417.          */
  418.         public PartitionedRegionBuilder2D insertBoundaries(final Iterable<? extends LineConvexSubset> boundaries) {
  419.             for (final LineConvexSubset boundary : boundaries) {
  420.                 insertBoundaryInternal(boundary);
  421.             }

  422.             return this;
  423.         }

  424.         /** Insert all boundaries from the given source.
  425.          * @param boundarySrc source of boundaries to insert
  426.          * @return this instance
  427.          */
  428.         public PartitionedRegionBuilder2D insertBoundaries(final BoundarySource2D boundarySrc) {
  429.             try (Stream<LineConvexSubset> stream = boundarySrc.boundaryStream()) {
  430.                 stream.forEach(this::insertBoundaryInternal);
  431.             }

  432.             return this;
  433.         }

  434.         /** Build and return the region BSP tree.
  435.          * @return the region BSP tree
  436.          */
  437.         public RegionBSPTree2D build() {
  438.             return (RegionBSPTree2D) buildInternal();
  439.         }
  440.     }

  441.     /** Class used to project points onto the 2D region boundary.
  442.      */
  443.     private static final class BoundaryProjector2D extends BoundaryProjector<Vector2D, RegionNode2D> {
  444.         /** Simple constructor.
  445.          * @param point the point to project onto the region's boundary
  446.          */
  447.         BoundaryProjector2D(final Vector2D point) {
  448.             super(point);
  449.         }

  450.         /** {@inheritDoc} */
  451.         @Override
  452.         protected Vector2D disambiguateClosestPoint(final Vector2D target, final Vector2D a, final Vector2D b) {
  453.             // return the point with the smallest coordinate values
  454.             final int cmp = Vector2D.COORDINATE_ASCENDING_ORDER.compare(a, b);
  455.             return cmp < 0 ? a : b;
  456.         }
  457.     }

  458.     /** BSP tree visitor that performs a linecast operation against the boundaries of the visited tree.
  459.      */
  460.     private static final class LinecastVisitor implements BSPTreeVisitor<Vector2D, RegionNode2D> {

  461.         /** The line subset to intersect with the boundaries of the BSP tree. */
  462.         private final LineConvexSubset linecastSubset;

  463.         /** If true, the visitor will stop visiting the tree once the first linecast
  464.          * point is determined.
  465.          */
  466.         private final boolean firstOnly;

  467.         /** The minimum abscissa found during the search. */
  468.         private double minAbscissa = Double.POSITIVE_INFINITY;

  469.         /** List of results from the linecast operation. */
  470.         private final List<LinecastPoint2D> results = new ArrayList<>();

  471.         /** Create a new instance with the given intersecting line subset.
  472.          * @param linecastSubset line subset to intersect with the BSP tree region boundary
  473.          * @param firstOnly if true, the visitor will stop visiting the tree once the first
  474.          *      linecast point is determined
  475.          */
  476.         LinecastVisitor(final LineConvexSubset linecastSubset, final boolean firstOnly) {
  477.             this.linecastSubset = linecastSubset;
  478.             this.firstOnly = firstOnly;
  479.         }

  480.         /** Get the first {@link LinecastPoint2D} resulting from the linecast operation.
  481.          * @return the first linecast result point
  482.          */
  483.         public LinecastPoint2D getFirstResult() {
  484.             final List<LinecastPoint2D> sortedResults = getResults();

  485.             return sortedResults.isEmpty() ?
  486.                     null :
  487.                     sortedResults.get(0);
  488.         }

  489.         /** Get a list containing the results of the linecast operation. The list is
  490.          * sorted and filtered.
  491.          * @return list of sorted and filtered results from the linecast operation
  492.          */
  493.         public List<LinecastPoint2D> getResults() {
  494.             LinecastPoint2D.sortAndFilter(results);

  495.             return results;
  496.         }

  497.         /** {@inheritDoc} */
  498.         @Override
  499.         public Order visitOrder(final RegionNode2D internalNode) {
  500.             final Line cut = (Line) internalNode.getCutHyperplane();
  501.             final Line line = linecastSubset.getLine();

  502.             final boolean plusIsNear = line.getDirection().dot(cut.getOffsetDirection()) < 0;

  503.             return plusIsNear ?
  504.                     Order.PLUS_NODE_MINUS :
  505.                     Order.MINUS_NODE_PLUS;
  506.         }

  507.         /** {@inheritDoc} */
  508.         @Override
  509.         public Result visit(final RegionNode2D node) {
  510.             if (node.isInternal()) {
  511.                 // check if the line subset intersects the node cut
  512.                 final Line line = linecastSubset.getLine();
  513.                 final Vector2D pt = ((Line) node.getCutHyperplane()).intersection(line);

  514.                 if (pt != null) {
  515.                     if (firstOnly && !results.isEmpty() &&
  516.                             line.getPrecision().compare(minAbscissa, line.abscissa(pt)) < 0) {
  517.                         // we have results and we are now sure that no other intersection points will be
  518.                         // found that are closer or at the same position on the intersecting line.
  519.                         return Result.TERMINATE;
  520.                     } else if (linecastSubset.contains(pt)) {
  521.                         // we've potentially found a new linecast point; add it to the list of potential
  522.                         // results
  523.                         final LinecastPoint2D potentialResult = computeLinecastPoint(pt, node);
  524.                         if (potentialResult != null) {
  525.                             results.add(potentialResult);

  526.                             // update the min abscissa
  527.                             minAbscissa = Math.min(minAbscissa, potentialResult.getAbscissa());
  528.                         }
  529.                     }
  530.                 }
  531.             }

  532.             return Result.CONTINUE;
  533.         }

  534.         /** Compute the linecast point for the given intersection point and tree node, returning null
  535.          * if the point does not actually lie on the region boundary.
  536.          * @param pt intersection point
  537.          * @param node node containing the cut that the linecast line intersected with
  538.          * @return a new linecast point instance or null if the intersection point does not lie
  539.          *      on the region boundary
  540.          */
  541.         private LinecastPoint2D computeLinecastPoint(final Vector2D pt, final RegionNode2D node) {
  542.             final Line cut = (Line) node.getCutHyperplane();
  543.             final RegionCutBoundary<Vector2D> boundary = node.getCutBoundary();

  544.             boolean onBoundary = false;
  545.             boolean negateNormal = false;

  546.             if (boundary.containsInsideFacing(pt)) {
  547.                 // on inside-facing boundary
  548.                 onBoundary = true;
  549.                 negateNormal = true;
  550.             } else  if (boundary.containsOutsideFacing(pt)) {
  551.                 // on outside-facing boundary
  552.                 onBoundary = true;
  553.             }

  554.             if (onBoundary) {
  555.                 Vector2D normal = cut.getOffsetDirection();
  556.                 if (negateNormal) {
  557.                     normal = normal.negate();
  558.                 }

  559.                 return new LinecastPoint2D(pt, normal, linecastSubset.getLine());
  560.             }

  561.             return null;
  562.         }
  563.     }
  564. }