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.spherical.twod;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.List;
022import java.util.stream.Stream;
023import java.util.stream.StreamSupport;
024
025import org.apache.commons.geometry.core.partitioning.Hyperplane;
026import org.apache.commons.geometry.core.partitioning.Split;
027import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
028import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
029import org.apache.commons.geometry.euclidean.threed.Vector3D;
030import org.apache.commons.numbers.core.Precision;
031import org.apache.commons.numbers.core.Sum;
032
033/** BSP tree representing regions in 2D spherical space.
034 */
035public class RegionBSPTree2S extends AbstractRegionBSPTree<Point2S, RegionBSPTree2S.RegionNode2S>
036    implements BoundarySource2S {
037    /** Constant containing the area of the full spherical space. */
038    private static final double FULL_SIZE = 4 * Math.PI;
039
040    /** List of great arc path comprising the region boundary. */
041    private List<GreatArcPath> boundaryPaths;
042
043    /** Create a new, empty instance.
044     */
045    public RegionBSPTree2S() {
046        this(false);
047    }
048
049    /** Create a new region. If {@code full} is true, then the region will
050     * represent the entire 2-sphere. Otherwise, it will be empty.
051     * @param full whether or not the region should contain the entire
052     *      2-sphere or be empty
053     */
054    public RegionBSPTree2S(final boolean full) {
055        super(full);
056    }
057
058    /** Return a deep copy of this instance.
059     * @return a deep copy of this instance.
060     * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
061     */
062    public RegionBSPTree2S copy() {
063        final RegionBSPTree2S result = RegionBSPTree2S.empty();
064        result.copy(this);
065
066        return result;
067    }
068
069    /** {@inheritDoc} */
070    @Override
071    public Iterable<GreatArc> boundaries() {
072        return createBoundaryIterable(GreatArc.class::cast);
073    }
074
075    /** {@inheritDoc} */
076    @Override
077    public Stream<GreatArc> boundaryStream() {
078        return StreamSupport.stream(boundaries().spliterator(), false);
079    }
080
081    /** {@inheritDoc} */
082    @Override
083    public List<GreatArc> getBoundaries() {
084        return createBoundaryList(GreatArc.class::cast);
085    }
086
087    /** Get the boundary of the region as a list of connected great arc paths. The
088     * arcs are oriented such that their minus (left) side lies on the interior of
089     * the region.
090     * @return great arc paths representing the region boundary
091     */
092    public List<GreatArcPath> getBoundaryPaths() {
093        if (boundaryPaths == null) {
094            boundaryPaths = Collections.unmodifiableList(computeBoundaryPaths());
095        }
096        return boundaryPaths;
097    }
098
099    /** 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<? super 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 Precision.DoubleEquivalence precision = ((GreatArc) getRoot().getCut()).getPrecision();
172
173        final Sum sizeSum = Sum.create();
174        final Vector3D.Sum centroidVectorSum = Vector3D.Sum.create();
175
176        double maxCentroidVectorWeightSq = 0.0;
177
178        for (final ConvexArea2S area : areas) {
179            sizeSum.add(area.getSize());
180
181            final Vector3D areaCentroidVector = area.getWeightedCentroidVector();
182            maxCentroidVectorWeightSq = Math.max(maxCentroidVectorWeightSq, areaCentroidVector.normSq());
183
184            centroidVectorSum.add(areaCentroidVector);
185        }
186
187        final double size = sizeSum.getAsDouble();
188        final Vector3D centroidVector = centroidVectorSum.get();
189
190        // Convert the weighted centroid vector to a point on the sphere surface. If the centroid vector
191        // length is less than the max length of the combined convex areas and the vector itself is
192        // equivalent to zero, then we know that there are opposing and approximately equal areas in the
193        // region, resulting in an indeterminate centroid. This would occur, for example, if there were
194        // equal areas around each pole.
195        final Point2S centroid;
196        if (centroidVector.normSq() < maxCentroidVectorWeightSq &&
197                centroidVector.eq(Vector3D.ZERO, precision)) {
198            centroid = null;
199        } else {
200            centroid = Point2S.from(centroidVector);
201        }
202
203        return new RegionSizeProperties<>(size, centroid);
204    }
205
206    /** {@inheritDoc} */
207    @Override
208    protected RegionNode2S createNode() {
209        return new RegionNode2S(this);
210    }
211
212    /** {@inheritDoc} */
213    @Override
214    protected void invalidate() {
215        super.invalidate();
216
217        boundaryPaths = null;
218    }
219
220    /** Compute the great arc paths comprising the region boundary.
221     * @return the great arc paths comprising the region boundary
222     */
223    private List<GreatArcPath> computeBoundaryPaths() {
224        final InteriorAngleGreatArcConnector connector = new InteriorAngleGreatArcConnector.Minimize();
225        return connector.connectAll(boundaries());
226    }
227
228    /** Return a new, empty BSP tree.
229     * @return a new, empty BSP tree.
230     */
231    public static RegionBSPTree2S empty() {
232        return new RegionBSPTree2S(false);
233    }
234
235    /** Return a new, full BSP tree. The returned tree represents the
236     * full space.
237     * @return a new, full BSP tree.
238     */
239    public static RegionBSPTree2S full() {
240        return new RegionBSPTree2S(true);
241    }
242
243    /** Construct a new tree from the given boundaries. If no boundaries
244     * are present, the returned tree is empty.
245     * @param boundaries boundaries to construct the tree from
246     * @return a new tree instance constructed from the given boundaries
247     * @see #from(Iterable, boolean)
248     */
249    public static RegionBSPTree2S from(final Iterable<GreatArc> boundaries) {
250        return from(boundaries, false);
251    }
252
253    /** Construct a new tree from the given boundaries. If {@code full} is true, then
254     * the initial tree before boundary insertion contains the entire space. Otherwise,
255     * it is empty.
256     * @param boundaries boundaries to construct the tree from
257     * @param full if true, the initial tree will contain the entire space
258     * @return a new tree instance constructed from the given boundaries
259     */
260    public static RegionBSPTree2S from(final Iterable<GreatArc> boundaries, final boolean full) {
261        final RegionBSPTree2S tree = new RegionBSPTree2S(full);
262        tree.insert(boundaries);
263
264        return tree;
265    }
266
267    /** BSP tree node for two dimensional spherical space.
268     */
269    public static final class RegionNode2S extends AbstractRegionBSPTree.AbstractRegionNode<Point2S, RegionNode2S> {
270        /** Simple constructor.
271         * @param tree tree owning the instance.
272         */
273        private RegionNode2S(final AbstractBSPTree<Point2S, RegionNode2S> tree) {
274            super(tree);
275        }
276
277        /** Get the region represented by this node. The returned region contains
278         * the entire area contained in this node, regardless of the attributes of
279         * any child nodes.
280         * @return the region represented by this node
281         */
282        public ConvexArea2S getNodeRegion() {
283            ConvexArea2S area = ConvexArea2S.full();
284
285            RegionNode2S child = this;
286            RegionNode2S parent;
287
288            while ((parent = child.getParent()) != null) {
289                final Split<ConvexArea2S> split = area.split(parent.getCutHyperplane());
290
291                area = child.isMinus() ? split.getMinus() : split.getPlus();
292
293                child = parent;
294            }
295
296            return area;
297        }
298
299        /** {@inheritDoc} */
300        @Override
301        protected RegionNode2S getSelf() {
302            return this;
303        }
304    }
305
306    /** Class used to project points onto the region boundary.
307     */
308    private static final class BoundaryProjector2S extends BoundaryProjector<Point2S, RegionNode2S> {
309        /** Simple constructor.
310         * @param point the point to project onto the region's boundary
311         */
312        BoundaryProjector2S(final Point2S point) {
313            super(point);
314        }
315
316        /** {@inheritDoc} */
317        @Override
318        protected Point2S disambiguateClosestPoint(final Point2S target, final Point2S a, final Point2S b) {
319            // return the point with the smallest coordinate values
320            final int cmp = Point2S.POLAR_AZIMUTH_ASCENDING_ORDER.compare(a, b);
321            return cmp < 0 ? a : b;
322        }
323    }
324}