Circle.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.shape;

  18. import java.text.MessageFormat;
  19. import java.util.List;
  20. import java.util.stream.Collectors;
  21. import java.util.stream.Stream;

  22. import org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule;
  23. import org.apache.commons.geometry.euclidean.AbstractNSphere;
  24. import org.apache.commons.geometry.euclidean.twod.Line;
  25. import org.apache.commons.geometry.euclidean.twod.LineConvexSubset;
  26. import org.apache.commons.geometry.euclidean.twod.LinecastPoint2D;
  27. import org.apache.commons.geometry.euclidean.twod.Linecastable2D;
  28. import org.apache.commons.geometry.euclidean.twod.Lines;
  29. import org.apache.commons.geometry.euclidean.twod.PolarCoordinates;
  30. import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
  31. import org.apache.commons.geometry.euclidean.twod.Vector2D;
  32. import org.apache.commons.numbers.angle.Angle;
  33. import org.apache.commons.numbers.core.Precision;

  34. /** Class representing a circle in 2 dimensional Euclidean space.
  35.  */
  36. public final class Circle extends AbstractNSphere<Vector2D> implements Linecastable2D {

  37.     /** Construct a new circle from its component parts.
  38.      * @param center the center of the circle
  39.      * @param radius the circle radius
  40.      * @param precision precision context used to compare floating point numbers
  41.      * @throws IllegalArgumentException if center is not finite or radius is not finite or is
  42.      *      less than or equal to zero as evaluated by the given precision context
  43.      */
  44.     private Circle(final Vector2D center, final double radius, final Precision.DoubleEquivalence precision) {
  45.         super(center, radius, precision);
  46.     }

  47.     /** {@inheritDoc} */
  48.     @Override
  49.     public double getSize() {
  50.         final double r = getRadius();
  51.         return Math.PI * r * r;
  52.     }

  53.     /** {@inheritDoc} */
  54.     @Override
  55.     public double getBoundarySize() {
  56.         return Angle.TWO_PI * getRadius();
  57.     }

  58.     /** {@inheritDoc} */
  59.     @Override
  60.     public Vector2D project(final Vector2D pt) {
  61.         return project(pt, Vector2D.Unit.PLUS_X);
  62.     }

  63.     /** Return a {@link RegionBSPTree2D} representing an approximation of the circle.
  64.      * All points in the approximation are contained in the circle (ie, they lie inside
  65.      * or on the boundary). No guarantees are made regarding the internal structure of
  66.      * the returned tree. Non-boundary split nodes may be used in order to balance the tree
  67.      * and improve performance.
  68.      *
  69.      * <p>Choosing an appropriate number of segments for an approximation is a trade-off
  70.      * between size and accuracy: approximations with large numbers of segments more closely
  71.      * match the geometric properties of the circle but at the cost of using larger tree
  72.      * structures. In general, the smallest number of segments that produces an acceptable
  73.      * result should be used.
  74.      * @param segments number of line segments to use for the boundary of
  75.      *      the circle approximation
  76.      * @return a BSP tree approximation of the circle
  77.      * @throws IllegalArgumentException if {@code segments} is less than 3
  78.      */
  79.     public RegionBSPTree2D toTree(final int segments) {
  80.         return new CircleApproximationBuilder(this, segments).build();
  81.     }

  82.     /** Get the intersections of the given line with this circle. The returned list will
  83.      * contain either 0, 1, or 2 points.
  84.      * <ul>
  85.      *      <li><strong>2 points</strong> - The line is a secant line and intersects the circle at two
  86.      *      distinct points. The points are ordered such that the first point in the list is the first point
  87.      *      encountered when traveling in the direction of the line. (In other words, the points are ordered
  88.      *      by increasing abscissa value.)
  89.      *      </li>
  90.      *      <li><strong>1 point</strong> - The line is a tangent line and only intersects the circle at a
  91.      *      single point (as evaluated by the circle's precision context).
  92.      *      </li>
  93.      *      <li><strong>0 points</strong> - The line does not intersect the circle.</li>
  94.      * </ul>
  95.      * @param line line to intersect with the circle
  96.      * @return a list of intersection points between the given line and this circle
  97.      */
  98.     public List<Vector2D> intersections(final Line line) {
  99.         return intersections(line, Line::abscissa, Line::distance);
  100.     }

  101.     /** Get the first intersection point between the given line and this circle, or null
  102.      * if no such point exists. The "first" intersection point is the first such point
  103.      * encountered when traveling in the direction of the line from infinity.
  104.      * @param line line to intersect with the circle
  105.      * @return the first intersection point between the given line and this instance or
  106.      *      null if no such point exists
  107.      */
  108.     public Vector2D firstIntersection(final Line line) {
  109.         return firstIntersection(line, Line::abscissa, Line::distance);
  110.     }

  111.     /** {@inheritDoc} */
  112.     @Override
  113.     public List<LinecastPoint2D> linecast(final LineConvexSubset segment) {
  114.         return getLinecastStream(segment)
  115.                 .collect(Collectors.toList());
  116.     }

  117.     /** {@inheritDoc} */
  118.     @Override
  119.     public LinecastPoint2D linecastFirst(final LineConvexSubset segment) {
  120.         return getLinecastStream(segment)
  121.                 .findFirst()
  122.                 .orElse(null);
  123.     }

  124.     /** Get a stream containing the linecast intersection points of the given
  125.      * segment with this instance.
  126.      * @param segment segment to intersect against this instance
  127.      * @return a stream containing linecast intersection points
  128.      */
  129.     private Stream<LinecastPoint2D> getLinecastStream(final LineConvexSubset segment) {
  130.         return intersections(segment.getLine()).stream()
  131.             .filter(segment::contains)
  132.             .map(pt -> new LinecastPoint2D(pt, getCenter().directionTo(pt), segment.getLine()));
  133.     }

  134.     /** Construct a circle from a center point and radius.
  135.      * @param center the center point of the circle
  136.      * @param radius the circle radius
  137.      * @param precision precision precision context used to compare floating point numbers
  138.      * @return a circle with the given center and radius
  139.      * @throws IllegalArgumentException if center is not finite or radius is not finite or is
  140.      *      less than or equal to zero as evaluated by the given precision context
  141.      */
  142.     public static Circle from(final Vector2D center, final double radius, final Precision.DoubleEquivalence precision) {
  143.         return new Circle(center, radius, precision);
  144.     }

  145.     /** Class used to build BSP tree circle approximations. Structural BSP tree cuts are
  146.      * used to help balance the tree and improve performance.
  147.      */
  148.     private static class CircleApproximationBuilder {

  149.         /** The minimum number of segments required to create a circle approximation.
  150.          */
  151.         private static final int MIN_SEGMENTS = 3;

  152.         /** Minimum number of line segments in a portion of the approximation in order
  153.          * to allow a structural BSP split.
  154.          */
  155.         private static final int SPLIT_THRESHOLD = 4;

  156.         /** Circle being approximated. */
  157.         private final Circle circle;

  158.         /** Number of boundary segments in the approximation. */
  159.         private final int segments;

  160.         /** Angle delta between vertex points. */
  161.         private final double angleDelta;

  162.         /** Create a new instance for approximating the given circle.
  163.          * @param circle circle to approximate
  164.          * @param segments number of boundary segments in the approximation
  165.          * @throws IllegalArgumentException if {@code segments} is less than 3
  166.          */
  167.         CircleApproximationBuilder(final Circle circle, final int segments) {
  168.             if (segments < MIN_SEGMENTS) {
  169.                 throw new IllegalArgumentException(MessageFormat.format(
  170.                         "Circle approximation segment number must be greater than or equal to {0}; was {1}",
  171.                         MIN_SEGMENTS, segments));
  172.             }

  173.             this.circle = circle;

  174.             this.segments = segments;
  175.             this.angleDelta = Angle.TWO_PI / segments;
  176.         }

  177.         /** Build the BSP tree circle approximation.
  178.          * @return the BSP tree circle approximation
  179.          */
  180.         public RegionBSPTree2D build() {
  181.             final RegionBSPTree2D tree = RegionBSPTree2D.empty();
  182.             final RegionBSPTree2D.RegionNode2D root = tree.getRoot();

  183.             if (segments < SPLIT_THRESHOLD) {
  184.                 insert(root, 0, segments);
  185.             } else {
  186.                 // split the circle in half (or mostly in half if an odd number of segments)
  187.                 final int splitIdx = segments / 2;
  188.                 final Vector2D p0 = pointAt(0);
  189.                 final Vector2D p1 = pointAt(splitIdx);

  190.                 root.cut(Lines.fromPoints(p0, p1, circle.getPrecision()), RegionCutRule.INHERIT);

  191.                 splitAndInsert(root.getPlus(), 0, splitIdx);
  192.                 splitAndInsert(root.getMinus(), splitIdx, segments);
  193.             }

  194.             return tree;
  195.         }

  196.         /** Split the given node if possible and recursively add boundary segments.
  197.          * @param node current tree node
  198.          * @param startIdx index of the start point for this node's boundary segments
  199.          * @param stopIdx index of the end point for this node's boundary segments
  200.          */
  201.         private void splitAndInsert(final RegionBSPTree2D.RegionNode2D node, final int startIdx, final int stopIdx) {
  202.             if (stopIdx - startIdx >= SPLIT_THRESHOLD) {
  203.                 final int splitIdx = ((stopIdx - startIdx + 1) / 2) + startIdx;
  204.                 final Vector2D p0 = circle.getCenter();
  205.                 final Vector2D p1 = pointAt(splitIdx);

  206.                 node.cut(Lines.fromPoints(p0, p1, circle.getPrecision()), RegionCutRule.INHERIT);

  207.                 splitAndInsert(node.getPlus(), startIdx, splitIdx);
  208.                 splitAndInsert(node.getMinus(), splitIdx, stopIdx);
  209.             } else {
  210.                 insert(node, startIdx, stopIdx);
  211.             }
  212.         }

  213.         /** Insert boundary segments into the given node. No structural splits are created.
  214.          * @param node current tree node
  215.          * @param startIdx index of the start point for this node's boundary segments
  216.          * @param stopIdx index of the end point for this node's boundary segments
  217.          */
  218.         private void insert(final RegionBSPTree2D.RegionNode2D node, final int startIdx, final int stopIdx) {

  219.             RegionBSPTree2D.RegionNode2D currNode = node;
  220.             Vector2D currPt;
  221.             Vector2D prevPt = pointAt(startIdx);
  222.             for (int i = startIdx + 1; i <= stopIdx; ++i) {
  223.                 currPt = pointAt(i);

  224.                 currNode = currNode.cut(Lines.fromPoints(prevPt, currPt, circle.getPrecision()))
  225.                         .getMinus();

  226.                 prevPt = currPt;
  227.             }
  228.         }

  229.         /** Get the boundary vertex point at the given index.
  230.          * @param idx vertex point index
  231.          * @return the vertex point at the given index
  232.          */
  233.         private Vector2D pointAt(final int idx) {
  234.             return PolarCoordinates.toCartesian(circle.getRadius(), idx * angleDelta)
  235.                     .add(circle.getCenter());
  236.         }
  237.     }
  238. }