001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.geometry.euclidean.twod.shape; 018 019import java.text.MessageFormat; 020import java.util.List; 021import java.util.stream.Collectors; 022import java.util.stream.Stream; 023 024import org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule; 025import org.apache.commons.geometry.euclidean.AbstractNSphere; 026import org.apache.commons.geometry.euclidean.twod.Line; 027import org.apache.commons.geometry.euclidean.twod.LineConvexSubset; 028import org.apache.commons.geometry.euclidean.twod.LinecastPoint2D; 029import org.apache.commons.geometry.euclidean.twod.Linecastable2D; 030import org.apache.commons.geometry.euclidean.twod.Lines; 031import org.apache.commons.geometry.euclidean.twod.PolarCoordinates; 032import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D; 033import org.apache.commons.geometry.euclidean.twod.Vector2D; 034import org.apache.commons.numbers.angle.Angle; 035import org.apache.commons.numbers.core.Precision; 036 037/** Class representing a circle in 2 dimensional Euclidean space. 038 */ 039public final class Circle extends AbstractNSphere<Vector2D> implements Linecastable2D { 040 041 /** Construct a new circle from its component parts. 042 * @param center the center of the circle 043 * @param radius the circle radius 044 * @param precision precision context used to compare floating point numbers 045 * @throws IllegalArgumentException if center is not finite or radius is not finite or is 046 * less than or equal to zero as evaluated by the given precision context 047 */ 048 private Circle(final Vector2D center, final double radius, final Precision.DoubleEquivalence precision) { 049 super(center, radius, precision); 050 } 051 052 /** {@inheritDoc} */ 053 @Override 054 public double getSize() { 055 final double r = getRadius(); 056 return Math.PI * r * r; 057 } 058 059 /** {@inheritDoc} */ 060 @Override 061 public double getBoundarySize() { 062 return Angle.TWO_PI * getRadius(); 063 } 064 065 /** {@inheritDoc} */ 066 @Override 067 public Vector2D project(final Vector2D pt) { 068 return project(pt, Vector2D.Unit.PLUS_X); 069 } 070 071 /** Return a {@link RegionBSPTree2D} representing an approximation of the circle. 072 * All points in the approximation are contained in the circle (ie, they lie inside 073 * or on the boundary). No guarantees are made regarding the internal structure of 074 * the returned tree. Non-boundary split nodes may be used in order to balance the tree 075 * and improve performance. 076 * 077 * <p>Choosing an appropriate number of segments for an approximation is a trade-off 078 * between size and accuracy: approximations with large numbers of segments more closely 079 * match the geometric properties of the circle but at the cost of using larger tree 080 * structures. In general, the smallest number of segments that produces an acceptable 081 * result should be used. 082 * @param segments number of line segments to use for the boundary of 083 * the circle approximation 084 * @return a BSP tree approximation of the circle 085 * @throws IllegalArgumentException if {@code segments} is less than 3 086 */ 087 public RegionBSPTree2D toTree(final int segments) { 088 return new CircleApproximationBuilder(this, segments).build(); 089 } 090 091 /** Get the intersections of the given line with this circle. The returned list will 092 * contain either 0, 1, or 2 points. 093 * <ul> 094 * <li><strong>2 points</strong> - The line is a secant line and intersects the circle at two 095 * distinct points. The points are ordered such that the first point in the list is the first point 096 * encountered when traveling in the direction of the line. (In other words, the points are ordered 097 * by increasing abscissa value.) 098 * </li> 099 * <li><strong>1 point</strong> - The line is a tangent line and only intersects the circle at a 100 * single point (as evaluated by the circle's precision context). 101 * </li> 102 * <li><strong>0 points</strong> - The line does not intersect the circle.</li> 103 * </ul> 104 * @param line line to intersect with the circle 105 * @return a list of intersection points between the given line and this circle 106 */ 107 public List<Vector2D> intersections(final Line line) { 108 return intersections(line, Line::abscissa, Line::distance); 109 } 110 111 /** Get the first intersection point between the given line and this circle, or null 112 * if no such point exists. The "first" intersection point is the first such point 113 * encountered when traveling in the direction of the line from infinity. 114 * @param line line to intersect with the circle 115 * @return the first intersection point between the given line and this instance or 116 * null if no such point exists 117 */ 118 public Vector2D firstIntersection(final Line line) { 119 return firstIntersection(line, Line::abscissa, Line::distance); 120 } 121 122 /** {@inheritDoc} */ 123 @Override 124 public List<LinecastPoint2D> linecast(final LineConvexSubset segment) { 125 return getLinecastStream(segment) 126 .collect(Collectors.toList()); 127 } 128 129 /** {@inheritDoc} */ 130 @Override 131 public LinecastPoint2D linecastFirst(final LineConvexSubset segment) { 132 return getLinecastStream(segment) 133 .findFirst() 134 .orElse(null); 135 } 136 137 /** Get a stream containing the linecast intersection points of the given 138 * segment with this instance. 139 * @param segment segment to intersect against this instance 140 * @return a stream containing linecast intersection points 141 */ 142 private Stream<LinecastPoint2D> getLinecastStream(final LineConvexSubset segment) { 143 return intersections(segment.getLine()).stream() 144 .filter(segment::contains) 145 .map(pt -> new LinecastPoint2D(pt, getCenter().directionTo(pt), segment.getLine())); 146 } 147 148 /** Construct a circle from a center point and radius. 149 * @param center the center point of the circle 150 * @param radius the circle radius 151 * @param precision precision precision context used to compare floating point numbers 152 * @return a circle with the given center and radius 153 * @throws IllegalArgumentException if center is not finite or radius is not finite or is 154 * less than or equal to zero as evaluated by the given precision context 155 */ 156 public static Circle from(final Vector2D center, final double radius, final Precision.DoubleEquivalence precision) { 157 return new Circle(center, radius, precision); 158 } 159 160 /** Class used to build BSP tree circle approximations. Structural BSP tree cuts are 161 * used to help balance the tree and improve performance. 162 */ 163 private static class CircleApproximationBuilder { 164 165 /** The minimum number of segments required to create a circle approximation. 166 */ 167 private static final int MIN_SEGMENTS = 3; 168 169 /** Minimum number of line segments in a portion of the approximation in order 170 * to allow a structural BSP split. 171 */ 172 private static final int SPLIT_THRESHOLD = 4; 173 174 /** Circle being approximated. */ 175 private final Circle circle; 176 177 /** Number of boundary segments in the approximation. */ 178 private final int segments; 179 180 /** Angle delta between vertex points. */ 181 private final double angleDelta; 182 183 /** Create a new instance for approximating the given circle. 184 * @param circle circle to approximate 185 * @param segments number of boundary segments in the approximation 186 * @throws IllegalArgumentException if {@code segments} is less than 3 187 */ 188 CircleApproximationBuilder(final Circle circle, final int segments) { 189 if (segments < MIN_SEGMENTS) { 190 throw new IllegalArgumentException(MessageFormat.format( 191 "Circle approximation segment number must be greater than or equal to {0}; was {1}", 192 MIN_SEGMENTS, segments)); 193 } 194 195 this.circle = circle; 196 197 this.segments = segments; 198 this.angleDelta = Angle.TWO_PI / segments; 199 } 200 201 /** Build the BSP tree circle approximation. 202 * @return the BSP tree circle approximation 203 */ 204 public RegionBSPTree2D build() { 205 final RegionBSPTree2D tree = RegionBSPTree2D.empty(); 206 final RegionBSPTree2D.RegionNode2D root = tree.getRoot(); 207 208 if (segments < SPLIT_THRESHOLD) { 209 insert(root, 0, segments); 210 } else { 211 // split the circle in half (or mostly in half if an odd number of segments) 212 final int splitIdx = segments / 2; 213 final Vector2D p0 = pointAt(0); 214 final Vector2D p1 = pointAt(splitIdx); 215 216 root.cut(Lines.fromPoints(p0, p1, circle.getPrecision()), RegionCutRule.INHERIT); 217 218 splitAndInsert(root.getPlus(), 0, splitIdx); 219 splitAndInsert(root.getMinus(), splitIdx, segments); 220 } 221 222 return tree; 223 } 224 225 /** Split the given node if possible and recursively add boundary segments. 226 * @param node current tree node 227 * @param startIdx index of the start point for this node's boundary segments 228 * @param stopIdx index of the end point for this node's boundary segments 229 */ 230 private void splitAndInsert(final RegionBSPTree2D.RegionNode2D node, final int startIdx, final int stopIdx) { 231 if (stopIdx - startIdx >= SPLIT_THRESHOLD) { 232 final int splitIdx = ((stopIdx - startIdx + 1) / 2) + startIdx; 233 final Vector2D p0 = circle.getCenter(); 234 final Vector2D p1 = pointAt(splitIdx); 235 236 node.cut(Lines.fromPoints(p0, p1, circle.getPrecision()), RegionCutRule.INHERIT); 237 238 splitAndInsert(node.getPlus(), startIdx, splitIdx); 239 splitAndInsert(node.getMinus(), splitIdx, stopIdx); 240 } else { 241 insert(node, startIdx, stopIdx); 242 } 243 } 244 245 /** Insert boundary segments into the given node. No structural splits are created. 246 * @param node current tree node 247 * @param startIdx index of the start point for this node's boundary segments 248 * @param stopIdx index of the end point for this node's boundary segments 249 */ 250 private void insert(final RegionBSPTree2D.RegionNode2D node, final int startIdx, final int stopIdx) { 251 252 RegionBSPTree2D.RegionNode2D currNode = node; 253 Vector2D currPt; 254 Vector2D prevPt = pointAt(startIdx); 255 for (int i = startIdx + 1; i <= stopIdx; ++i) { 256 currPt = pointAt(i); 257 258 currNode = currNode.cut(Lines.fromPoints(prevPt, currPt, circle.getPrecision())) 259 .getMinus(); 260 261 prevPt = currPt; 262 } 263 } 264 265 /** Get the boundary vertex point at the given index. 266 * @param idx vertex point index 267 * @return the vertex point at the given index 268 */ 269 private Vector2D pointAt(final int idx) { 270 return PolarCoordinates.toCartesian(circle.getRadius(), idx * angleDelta) 271 .add(circle.getCenter()); 272 } 273 } 274}