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.oned;
018
019import java.util.ArrayList;
020import java.util.Comparator;
021import java.util.List;
022import java.util.Objects;
023import java.util.function.BiConsumer;
024
025import org.apache.commons.geometry.core.RegionLocation;
026import org.apache.commons.geometry.core.Transform;
027import org.apache.commons.geometry.core.partitioning.Hyperplane;
028import org.apache.commons.geometry.core.partitioning.Split;
029import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
030import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
031import org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule;
032
033/** Binary space partitioning (BSP) tree representing a region in one dimensional
034 * Euclidean space.
035 */
036public final class RegionBSPTree1D extends AbstractRegionBSPTree<Vector1D, RegionBSPTree1D.RegionNode1D> {
037    /** Comparator used to sort BoundaryPairs by ascending location.  */
038    private static final Comparator<BoundaryPair> BOUNDARY_PAIR_COMPARATOR =
039            Comparator.comparingDouble(BoundaryPair::getMinValue);
040
041    /** Create a new, empty region.
042     */
043    public RegionBSPTree1D() {
044        this(false);
045    }
046
047    /** Create a new region. If {@code full} is true, then the region will
048     * represent the entire number line. Otherwise, it will be empty.
049     * @param full whether or not the region should contain the entire
050     *      number line or be empty
051     */
052    public RegionBSPTree1D(final boolean full) {
053        super(full);
054    }
055
056    /** Return a deep copy of this instance.
057     * @return a deep copy of this instance.
058     * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
059     */
060    public RegionBSPTree1D copy() {
061        final RegionBSPTree1D result = RegionBSPTree1D.empty();
062        result.copy(this);
063
064        return result;
065    }
066
067    /** Add an interval to this region. The resulting region will be the
068     * union of the interval and the region represented by this instance.
069     * @param interval the interval to add
070     */
071    public void add(final Interval interval) {
072        union(intervalToTree(interval));
073    }
074
075    /** Classify a point location with respect to the region.
076     * @param x the point to classify
077     * @return the location of the point with respect to the region
078     * @see #classify(org.apache.commons.geometry.core.Point)
079     */
080    public RegionLocation classify(final double x) {
081        return classify(Vector1D.of(x));
082    }
083
084    /** Return true if the given point location is on the inside or boundary
085     * of the region.
086     * @param x the location to test
087     * @return true if the location is on the inside or boundary of the region
088     * @see #contains(org.apache.commons.geometry.core.Point)
089     */
090    public boolean contains(final double x) {
091        return contains(Vector1D.of(x));
092    }
093
094    /** {@inheritDoc}
095    *
096    *  <p>This method simply returns 0 because boundaries in one dimension do not
097    *  have any size.</p>
098    */
099    @Override
100    public double getBoundarySize() {
101        return 0;
102    }
103
104    /** {@inheritDoc} */
105    @Override
106    public Vector1D project(final Vector1D pt) {
107        // use our custom projector so that we can disambiguate points that are
108        // actually equidistant from the target point
109        final BoundaryProjector1D projector = new BoundaryProjector1D(pt);
110        accept(projector);
111
112        return projector.getProjected();
113    }
114
115    /** {@inheritDoc}
116     *
117     * <p>When splitting trees representing single points with a splitter lying directly
118     * on the point, the result point is placed on one side of the splitter based on its
119     * orientation: if the splitter is positive-facing, the point is placed on the plus
120     * side of the split; if the splitter is negative-facing, the point is placed on the
121     * minus side of the split.</p>
122     */
123    @Override
124    public Split<RegionBSPTree1D> split(final Hyperplane<Vector1D> splitter) {
125        return split(splitter, RegionBSPTree1D.empty(), RegionBSPTree1D.empty());
126    }
127
128    /** Get the minimum value on the inside of the region; returns {@link Double#NEGATIVE_INFINITY}
129     * if the region does not have a minimum value and {@link Double#POSITIVE_INFINITY} if
130     * the region is empty.
131     * @return the minimum value on the inside of the region
132     */
133    public double getMin() {
134        double min = Double.POSITIVE_INFINITY;
135
136        RegionNode1D node = getRoot();
137        OrientedPoint pt;
138
139        while (!node.isLeaf()) {
140            pt = (OrientedPoint) node.getCutHyperplane();
141
142            min = pt.getLocation();
143            node = pt.isPositiveFacing() ? node.getMinus() : node.getPlus();
144        }
145
146        return node.isInside() ? Double.NEGATIVE_INFINITY : min;
147    }
148
149    /** Get the maximum value on the inside of the region; returns {@link Double#POSITIVE_INFINITY}
150     * if the region does not have a maximum value and {@link Double#NEGATIVE_INFINITY} if
151     * the region is empty.
152     * @return the maximum value on the inside of the region
153     */
154    public double getMax() {
155        double max = Double.NEGATIVE_INFINITY;
156
157        RegionNode1D node = getRoot();
158        OrientedPoint pt;
159
160        while (!node.isLeaf()) {
161            pt = (OrientedPoint) node.getCutHyperplane();
162
163            max = pt.getLocation();
164            node = pt.isPositiveFacing() ? node.getPlus() : node.getMinus();
165        }
166
167        return node.isInside() ? Double.POSITIVE_INFINITY : max;
168    }
169
170    /** Convert the region represented by this tree into a list of separate
171     * {@link Interval}s, arranged in order of ascending min value.
172     * @return list of {@link Interval}s representing this region in order of
173     *      ascending min value
174     */
175    public List<Interval> toIntervals() {
176
177        final List<BoundaryPair> boundaryPairs = new ArrayList<>();
178
179        visitInsideIntervals((min, max) -> boundaryPairs.add(new BoundaryPair(min, max)));
180        boundaryPairs.sort(BOUNDARY_PAIR_COMPARATOR);
181
182        final List<Interval> intervals = new ArrayList<>();
183
184        BoundaryPair start = null;
185        BoundaryPair end = null;
186
187        for (final BoundaryPair current : boundaryPairs) {
188            if (start == null) {
189                start = current;
190                end = current;
191            } else if (Objects.equals(end.getMax(), current.getMin())) {
192                // these intervals should be merged
193                end = current;
194            } else {
195                // these intervals should not be merged
196                intervals.add(createInterval(start, end));
197
198                // queue up the next pair
199                start = current;
200                end = current;
201            }
202        }
203
204        if (start != null) {
205            intervals.add(createInterval(start, end));
206        }
207
208        return intervals;
209    }
210
211    /** Create an interval instance from the min boundary from the start boundary pair and
212     * the max boundary from the end boundary pair. The hyperplane directions are adjusted
213     * as needed.
214     * @param start starting boundary pair
215     * @param end ending boundary pair
216     * @return an interval created from the min boundary of the given start pair and the
217     *      max boundary from the given end pair
218     */
219    private Interval createInterval(final BoundaryPair start, final BoundaryPair end) {
220        OrientedPoint min = start.getMin();
221        OrientedPoint max = end.getMax();
222
223        // flip the hyperplanes if needed since there's no
224        // guarantee that the inside will be on the minus side
225        // of the hyperplane (for example, if the region is complemented)
226
227        if (min != null && min.isPositiveFacing()) {
228            min = min.reverse();
229        }
230        if (max != null && !max.isPositiveFacing()) {
231            max = max.reverse();
232        }
233
234        return Interval.of(min, max);
235    }
236
237    /** Compute the min/max intervals for all interior convex regions in the tree and
238     * pass the values to the given visitor function.
239     * @param visitor the object that will receive the calculated min and max boundary for each
240     *      insides node's convex region
241     */
242    private void visitInsideIntervals(final BiConsumer<OrientedPoint, OrientedPoint> visitor) {
243        for (final RegionNode1D node : nodes()) {
244            if (node.isInside()) {
245                node.visitNodeInterval(visitor);
246            }
247        }
248    }
249
250    /** {@inheritDoc} */
251    @Override
252    protected RegionNode1D createNode() {
253        return new RegionNode1D(this);
254    }
255
256    /** {@inheritDoc} */
257    @Override
258    protected RegionSizeProperties<Vector1D> computeRegionSizeProperties() {
259        final RegionSizePropertiesVisitor visitor = new RegionSizePropertiesVisitor();
260
261        visitInsideIntervals(visitor);
262
263        return visitor.getRegionSizeProperties();
264    }
265
266    /** Returns true if the given transform would result in a swapping of the interior
267     * and exterior of the region if applied.
268     *
269     * <p>This method always returns false since no swapping of this kind occurs in
270     * 1D.</p>
271     */
272    @Override
273    protected boolean swapsInsideOutside(final Transform<Vector1D> transform) {
274        return false;
275    }
276
277    /** Return a new {@link RegionBSPTree1D} instance containing the entire space.
278     * @return a new {@link RegionBSPTree1D} instance containing the entire space
279     */
280    public static RegionBSPTree1D full() {
281        return new RegionBSPTree1D(true);
282    }
283
284    /** Return a new, empty {@link RegionBSPTree1D} instance.
285     * @return a new, empty {@link RegionBSPTree1D} instance
286     */
287    public static RegionBSPTree1D empty() {
288        return new RegionBSPTree1D(false);
289    }
290
291    /** Construct a new instance from one or more intervals. The returned tree
292     * represents the same region as the union of all of the input intervals.
293     * @param interval the input interval
294     * @param more additional intervals to add to the region
295     * @return a new instance representing the same region as the union
296     *      of all of the given intervals
297     */
298    public static RegionBSPTree1D from(final Interval interval, final Interval... more) {
299        final RegionBSPTree1D tree = intervalToTree(interval);
300
301        for (final Interval additional : more) {
302            tree.add(additional);
303        }
304
305        return tree;
306    }
307
308    /** Construct a new instance from the given collection of intervals.
309     * @param intervals the intervals to populate the region with
310     * @return a new instance constructed from the given collection of intervals
311     */
312    public static RegionBSPTree1D from(final Iterable<Interval> intervals) {
313        final RegionBSPTree1D tree = new RegionBSPTree1D(false);
314
315        for (final Interval interval : intervals) {
316            tree.add(interval);
317        }
318
319        return tree;
320    }
321
322    /** Return a tree representing the same region as the given interval.
323     * @param interval interval to create a tree from
324     * @return a tree representing the same region as the given interval
325     */
326    private static RegionBSPTree1D intervalToTree(final Interval interval) {
327        final OrientedPoint minBoundary = interval.getMinBoundary();
328        final OrientedPoint maxBoundary = interval.getMaxBoundary();
329
330        final RegionBSPTree1D tree = full();
331
332        RegionNode1D node = tree.getRoot();
333
334        if (minBoundary != null) {
335            tree.setNodeCut(node, minBoundary.span(), tree.getSubtreeInitializer(RegionCutRule.MINUS_INSIDE));
336
337            node = node.getMinus();
338        }
339
340        if (maxBoundary != null) {
341            tree.setNodeCut(node, maxBoundary.span(), tree.getSubtreeInitializer(RegionCutRule.MINUS_INSIDE));
342        }
343
344        return tree;
345    }
346
347    /** BSP tree node for one dimensional Euclidean space.
348     */
349    public static final class RegionNode1D extends AbstractRegionBSPTree.AbstractRegionNode<Vector1D, RegionNode1D> {
350        /** Simple constructor.
351         * @param tree the owning tree instance
352         */
353        private RegionNode1D(final AbstractBSPTree<Vector1D, RegionNode1D> tree) {
354            super(tree);
355        }
356
357        /** Get the region represented by this node. The returned region contains
358         * the entire area contained in this node, regardless of the attributes of
359         * any child nodes.
360         * @return the region represented by this node
361         */
362        public Interval getNodeRegion() {
363            final NodeRegionVisitor visitor = new NodeRegionVisitor();
364            visitNodeInterval(visitor);
365
366            return visitor.getInterval();
367        }
368
369        /** Determine the min/max boundaries for the convex region represented by this node and pass
370         * the values to the visitor function.
371         * @param visitor the object that will receive the min and max boundaries for the node's
372         *      convex region
373         */
374        private void visitNodeInterval(final BiConsumer<? super OrientedPoint, ? super OrientedPoint> visitor) {
375            OrientedPoint min = null;
376            OrientedPoint max = null;
377
378            OrientedPoint pt;
379            RegionNode1D child = this;
380            RegionNode1D parent;
381
382            while ((min == null || max == null) && (parent = child.getParent()) != null) {
383                pt = (OrientedPoint) parent.getCutHyperplane();
384
385                if ((pt.isPositiveFacing() && child.isMinus()) ||
386                        (!pt.isPositiveFacing() && child.isPlus())) {
387
388                    if (max == null) {
389                        max = pt;
390                    }
391                } else if (min == null) {
392                    min = pt;
393                }
394
395                child = parent;
396            }
397
398            visitor.accept(min, max);
399        }
400
401        /** {@inheritDoc} */
402        @Override
403        protected RegionNode1D getSelf() {
404            return this;
405        }
406    }
407
408    /** Internal class containing pairs of interval boundaries.
409     */
410    private static final class BoundaryPair {
411
412        /** The min boundary. */
413        private final OrientedPoint min;
414
415        /** The max boundary. */
416        private final OrientedPoint max;
417
418        /** Simple constructor.
419         * @param min min boundary hyperplane
420         * @param max max boundary hyperplane
421         */
422        BoundaryPair(final OrientedPoint min, final OrientedPoint max) {
423            this.min = min;
424            this.max = max;
425        }
426
427        /** Get the minimum boundary hyperplane.
428         * @return the minimum boundary hyperplane.
429         */
430        public OrientedPoint getMin() {
431            return min;
432        }
433
434        /** Get the maximum boundary hyperplane.
435         * @return the maximum boundary hyperplane.
436         */
437        public OrientedPoint getMax() {
438            return max;
439        }
440
441        /** Get the minimum value of the interval or {@link Double#NEGATIVE_INFINITY}
442         * if no minimum value exists.
443         * @return the minimum value of the interval or {@link Double#NEGATIVE_INFINITY}
444         *      if no minimum value exists.
445         */
446        public double getMinValue() {
447            return (min != null) ? min.getLocation() : Double.NEGATIVE_INFINITY;
448        }
449    }
450
451    /** Class used to project points onto the region boundary.
452     */
453    private static final class BoundaryProjector1D extends BoundaryProjector<Vector1D, RegionNode1D> {
454        /** Simple constructor.
455         * @param point the point to project onto the region's boundary
456         */
457        BoundaryProjector1D(final Vector1D point) {
458            super(point);
459        }
460
461        /** {@inheritDoc} */
462        @Override
463        protected Vector1D disambiguateClosestPoint(final Vector1D target, final Vector1D a, final Vector1D b) {
464            final int cmp = Vector1D.COORDINATE_ASCENDING_ORDER.compare(a, b);
465
466            if (target.isInfinite() && target.getX() > 0) {
467                // return the largest value (closest to +Infinity)
468                return cmp < 0 ? b : a;
469            }
470
471            // return the smallest value
472            return cmp < 0 ? a : b;
473        }
474    }
475
476    /** Internal class for calculating the region of a single tree node.
477     */
478    private static final class NodeRegionVisitor implements BiConsumer<OrientedPoint, OrientedPoint> {
479
480        /** The min boundary for the region. */
481        private OrientedPoint min;
482
483        /** The max boundary for the region. */
484        private OrientedPoint max;
485
486        /** {@inheritDoc} */
487        @Override
488        public void accept(final OrientedPoint minBoundary, final OrientedPoint maxBoundary) {
489            // reverse the oriented point directions if needed
490            this.min = (minBoundary != null && minBoundary.isPositiveFacing()) ? minBoundary.reverse() : minBoundary;
491            this.max = (maxBoundary != null && !maxBoundary.isPositiveFacing()) ? maxBoundary.reverse() : maxBoundary;
492        }
493
494        /** Return the computed interval.
495         * @return the computed interval.
496         */
497        public Interval getInterval() {
498            return Interval.of(min, max);
499        }
500    }
501
502    /** Internal class for calculating size-related properties for a {@link RegionBSPTree1D}.
503     */
504    private static final class RegionSizePropertiesVisitor implements BiConsumer<OrientedPoint, OrientedPoint> {
505        /** Number of inside intervals visited. */
506        private int count;
507
508        /** Total computed size of all inside regions. */
509        private double size;
510
511        /** Raw sum of the centroids of each inside interval. */
512        private double rawCentroidSum;
513
514        /** The sum of the centroids of each inside interval, scaled by the size of the interval. */
515        private double scaledCentroidSum;
516
517        /** {@inheritDoc} */
518        @Override
519        public void accept(final OrientedPoint min, final OrientedPoint max) {
520            ++count;
521
522            final double minLoc = (min != null) ? min.getLocation() : Double.NEGATIVE_INFINITY;
523            final double maxLoc = (max != null) ? max.getLocation() : Double.POSITIVE_INFINITY;
524
525            final double intervalSize = maxLoc - minLoc;
526            final double intervalCentroid = 0.5 * (maxLoc + minLoc);
527
528            size += intervalSize;
529            rawCentroidSum += intervalCentroid;
530            scaledCentroidSum += intervalSize * intervalCentroid;
531        }
532
533        /** Get the computed properties for the region. This must only be called after
534         * every inside interval has been visited.
535         * @return properties for the region
536         */
537        public RegionSizeProperties<Vector1D> getRegionSizeProperties() {
538            Vector1D centroid = null;
539
540            if (count > 0 && Double.isFinite(size)) {
541                if (size > 0) {
542                    // use the scaled sum if we have a non-zero size
543                    centroid = Vector1D.of(scaledCentroidSum / size);
544                } else {
545                    // use the raw sum if we don't have a size; this will be
546                    // the case if the region only contains points with zero size
547                    centroid = Vector1D.of(rawCentroidSum / count);
548                }
549            }
550
551            return new RegionSizeProperties<>(size, centroid);
552        }
553    }
554}