View Javadoc
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.spherical.twod;
18  
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  import java.util.stream.Stream;
23  import java.util.stream.StreamSupport;
24  
25  import org.apache.commons.geometry.core.partitioning.Hyperplane;
26  import org.apache.commons.geometry.core.partitioning.Split;
27  import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
28  import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
29  import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
30  import org.apache.commons.geometry.euclidean.threed.Vector3D;
31  import org.apache.commons.numbers.angle.PlaneAngleRadians;
32  
33  /** BSP tree representing regions in 2D spherical space.
34   */
35  public class RegionBSPTree2S extends AbstractRegionBSPTree<Point2S, RegionBSPTree2S.RegionNode2S>
36      implements BoundarySource2S {
37      /** Constant containing the area of the full spherical space. */
38      private static final double FULL_SIZE = 4 * PlaneAngleRadians.PI;
39  
40      /** List of great arc path comprising the region boundary. */
41      private List<GreatArcPath> boundaryPaths;
42  
43      /** Create a new, empty instance.
44       */
45      public RegionBSPTree2S() {
46          this(false);
47      }
48  
49      /** Create a new region. If {@code full} is true, then the region will
50       * represent the entire 2-sphere. Otherwise, it will be empty.
51       * @param full whether or not the region should contain the entire
52       *      2-sphere or be empty
53       */
54      public RegionBSPTree2S(final boolean full) {
55          super(full);
56      }
57  
58      /** Return a deep copy of this instance.
59       * @return a deep copy of this instance.
60       * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
61       */
62      public RegionBSPTree2S copy() {
63          final RegionBSPTree2S result = RegionBSPTree2S.empty();
64          result.copy(this);
65  
66          return result;
67      }
68  
69      /** {@inheritDoc} */
70      @Override
71      public Iterable<GreatArc> boundaries() {
72          return createBoundaryIterable(b -> (GreatArc) b);
73      }
74  
75      /** {@inheritDoc} */
76      @Override
77      public Stream<GreatArc> boundaryStream() {
78          return StreamSupport.stream(boundaries().spliterator(), false);
79      }
80  
81      /** {@inheritDoc} */
82      @Override
83      public List<GreatArc> getBoundaries() {
84          return createBoundaryList(b -> (GreatArc) b);
85      }
86  
87      /** Get the boundary of the region as a list of connected great arc paths. The
88       * arcs are oriented such that their minus (left) side lies on the interior of
89       * the region.
90       * @return great arc paths representing the region boundary
91       */
92      public List<GreatArcPath> getBoundaryPaths() {
93          if (boundaryPaths == null) {
94              boundaryPaths = Collections.unmodifiableList(computeBoundaryPaths());
95          }
96          return boundaryPaths;
97      }
98  
99      /** Return a list of {@link ConvexArea2S}s representing the same region
100      * as this instance. One convex area is returned for each interior leaf
101      * node in the tree.
102      * @return a list of convex areas representing the same region as this
103      *      instance
104      */
105     public List<ConvexArea2S> toConvex() {
106         final List<ConvexArea2S> result = new ArrayList<>();
107 
108         toConvexRecursive(getRoot(), ConvexArea2S.full(), result);
109 
110         return result;
111     }
112 
113     /** Recursive method to compute the convex areas of all inside leaf nodes in the subtree rooted at the given
114      * node. The computed convex areas are added to the given list.
115      * @param node root of the subtree to compute the convex areas for
116      * @param nodeArea the convex area for the current node; this will be split by the node's cut hyperplane to
117      *      form the convex areas for any child nodes
118      * @param result list containing the results of the computation
119      */
120     private void toConvexRecursive(final RegionNode2S node, final ConvexArea2S nodeArea,
121             final List<ConvexArea2S> result) {
122         if (node.isLeaf()) {
123             // base case; only add to the result list if the node is inside
124             if (node.isInside()) {
125                 result.add(nodeArea);
126             }
127         } else {
128             // recurse
129             final Split<ConvexArea2S> split = nodeArea.split(node.getCutHyperplane());
130 
131             toConvexRecursive(node.getMinus(), split.getMinus(), result);
132             toConvexRecursive(node.getPlus(), split.getPlus(), result);
133         }
134     }
135 
136     /** {@inheritDoc} */
137     @Override
138     public Split<RegionBSPTree2S> split(final Hyperplane<Point2S> splitter) {
139         return split(splitter, empty(), empty());
140     }
141 
142     /** {@inheritDoc} */
143     @Override
144     public Point2S project(final Point2S pt) {
145         // use our custom projector so that we can disambiguate points that are
146         // actually equidistant from the target point
147         final BoundaryProjector2S projector = new BoundaryProjector2S(pt);
148         accept(projector);
149 
150         return projector.getProjected();
151     }
152 
153     /** Return the current instance.
154      */
155     @Override
156     public RegionBSPTree2S toTree() {
157         return this;
158     }
159 
160     /** {@inheritDoc} */
161     @Override
162     protected RegionSizeProperties<Point2S> computeRegionSizeProperties() {
163         // handle simple cases
164         if (isFull()) {
165             return new RegionSizeProperties<>(FULL_SIZE, null);
166         } else if (isEmpty()) {
167             return new RegionSizeProperties<>(0, null);
168         }
169 
170         final List<ConvexArea2S> areas = toConvex();
171         final DoublePrecisionContext precision = ((GreatArc) getRoot().getCut()).getPrecision();
172 
173         double sizeSum = 0;
174         Vector3D centroidVectorSum = Vector3D.ZERO;
175 
176         Vector3D centroidVector;
177         double maxCentroidVectorWeightSq = 0.0;
178 
179         for (final ConvexArea2S area : areas) {
180             sizeSum += area.getSize();
181 
182             centroidVector = area.getWeightedCentroidVector();
183             maxCentroidVectorWeightSq = Math.max(maxCentroidVectorWeightSq, centroidVector.normSq());
184 
185             centroidVectorSum = centroidVectorSum.add(centroidVector);
186         }
187 
188         // Convert the weighted centroid vector to a point on the sphere surface. If the centroid vector
189         // length is less than the max length of the combined convex areas and the vector itself is
190         // equivalent to zero, then we know that there are opposing and approximately equal areas in the
191         // region, resulting in an indeterminate centroid. This would occur, for example, if there were
192         // equal areas around each pole.
193         final Point2S centroid;
194         if (centroidVectorSum.normSq() < maxCentroidVectorWeightSq &&
195                 centroidVectorSum.eq(Vector3D.ZERO, precision)) {
196             centroid = null;
197         } else {
198             centroid = Point2S.from(centroidVectorSum);
199         }
200 
201         return new RegionSizeProperties<>(sizeSum, centroid);
202     }
203 
204     /** {@inheritDoc} */
205     @Override
206     protected RegionNode2S createNode() {
207         return new RegionNode2S(this);
208     }
209 
210     /** {@inheritDoc} */
211     @Override
212     protected void invalidate() {
213         super.invalidate();
214 
215         boundaryPaths = null;
216     }
217 
218     /** Compute the great arc paths comprising the region boundary.
219      * @return the great arc paths comprising the region boundary
220      */
221     private List<GreatArcPath> computeBoundaryPaths() {
222         final InteriorAngleGreatArcConnector connector = new InteriorAngleGreatArcConnector.Minimize();
223         return connector.connectAll(boundaries());
224     }
225 
226     /** Return a new, empty BSP tree.
227      * @return a new, empty BSP tree.
228      */
229     public static RegionBSPTree2S empty() {
230         return new RegionBSPTree2S(false);
231     }
232 
233     /** Return a new, full BSP tree. The returned tree represents the
234      * full space.
235      * @return a new, full BSP tree.
236      */
237     public static RegionBSPTree2S full() {
238         return new RegionBSPTree2S(true);
239     }
240 
241     /** Construct a new tree from the given boundaries. If no boundaries
242      * are present, the returned tree is empty.
243      * @param boundaries boundaries to construct the tree from
244      * @return a new tree instance constructed from the given boundaries
245      * @see #from(Iterable, boolean)
246      */
247     public static RegionBSPTree2S from(final Iterable<GreatArc> boundaries) {
248         return from(boundaries, false);
249     }
250 
251     /** Construct a new tree from the given boundaries. If {@code full} is true, then
252      * the initial tree before boundary insertion contains the entire space. Otherwise,
253      * it is empty.
254      * @param boundaries boundaries to construct the tree from
255      * @param full if true, the initial tree will contain the entire space
256      * @return a new tree instance constructed from the given boundaries
257      */
258     public static RegionBSPTree2S from(final Iterable<GreatArc> boundaries, final boolean full) {
259         final RegionBSPTree2S tree = new RegionBSPTree2S(full);
260         tree.insert(boundaries);
261 
262         return tree;
263     }
264 
265     /** BSP tree node for two dimensional spherical space.
266      */
267     public static final class RegionNode2S extends AbstractRegionBSPTree.AbstractRegionNode<Point2S, RegionNode2S> {
268         /** Simple constructor.
269          * @param tree tree owning the instance.
270          */
271         private RegionNode2S(final AbstractBSPTree<Point2S, RegionNode2S> tree) {
272             super(tree);
273         }
274 
275         /** Get the region represented by this node. The returned region contains
276          * the entire area contained in this node, regardless of the attributes of
277          * any child nodes.
278          * @return the region represented by this node
279          */
280         public ConvexArea2S getNodeRegion() {
281             ConvexArea2S area = ConvexArea2S.full();
282 
283             RegionNode2S child = this;
284             RegionNode2S parent;
285 
286             while ((parent = child.getParent()) != null) {
287                 final Split<ConvexArea2S> split = area.split(parent.getCutHyperplane());
288 
289                 area = child.isMinus() ? split.getMinus() : split.getPlus();
290 
291                 child = parent;
292             }
293 
294             return area;
295         }
296 
297         /** {@inheritDoc} */
298         @Override
299         protected RegionNode2S getSelf() {
300             return this;
301         }
302     }
303 
304     /** Class used to project points onto the region boundary.
305      */
306     private static final class BoundaryProjector2S extends BoundaryProjector<Point2S, RegionNode2S> {
307         /** Simple constructor.
308          * @param point the point to project onto the region's boundary
309          */
310         BoundaryProjector2S(final Point2S point) {
311             super(point);
312         }
313 
314         /** {@inheritDoc} */
315         @Override
316         protected Point2S disambiguateClosestPoint(final Point2S target, final Point2S a, final Point2S b) {
317             // return the point with the smallest coordinate values
318             final int cmp = Point2S.POLAR_AZIMUTH_ASCENDING_ORDER.compare(a, b);
319             return cmp < 0 ? a : b;
320         }
321     }
322 }