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.core.partitioning;
18  
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  import java.util.function.Function;
23  
24  import org.apache.commons.geometry.core.Point;
25  import org.apache.commons.geometry.core.RegionLocation;
26  import org.apache.commons.geometry.core.Transform;
27  
28  /** Base class for convex hyperplane-bounded regions. This class provides generic implementations of many
29   * algorithms related to convex regions.
30   * @param <P> Point implementation type
31   * @param <S> Hyperplane convex subset implementation type
32   */
33  public abstract class AbstractConvexHyperplaneBoundedRegion<P extends Point<P>, S extends HyperplaneConvexSubset<P>>
34      implements HyperplaneBoundedRegion<P> {
35      /** List of boundaries for the region. */
36      private final List<S> boundaries;
37  
38      /** Simple constructor. Callers are responsible for ensuring that the given list of boundaries
39       * define a convex region. No validation is performed.
40       * @param boundaries the boundaries of the convex region
41       */
42      protected AbstractConvexHyperplaneBoundedRegion(final List<S> boundaries) {
43          this.boundaries = Collections.unmodifiableList(boundaries);
44      }
45  
46      /** Get the boundaries of the convex region. The exact ordering of the boundaries
47       * is not guaranteed.
48       * @return the boundaries of the convex region
49       */
50      public List<S> getBoundaries() {
51          return boundaries;
52      }
53  
54      /** {@inheritDoc} */
55      @Override
56      public boolean isFull() {
57          // no boundaries => no outside
58          return boundaries.isEmpty();
59      }
60  
61      /** {@inheritDoc}
62       *
63       * <p>This method always returns false.</p>
64       */
65      @Override
66      public boolean isEmpty() {
67          return false;
68      }
69  
70      /** {@inheritDoc} */
71      @Override
72      public double getBoundarySize() {
73          double sum = 0.0;
74          for (final S boundary : boundaries) {
75              sum += boundary.getSize();
76          }
77  
78          return sum;
79      }
80  
81      /** {@inheritDoc} */
82      @Override
83      public RegionLocation classify(final P pt) {
84          boolean isOn = false;
85  
86          HyperplaneLocation loc;
87          for (final S boundary : boundaries) {
88              loc = boundary.getHyperplane().classify(pt);
89  
90              if (loc == HyperplaneLocation.PLUS) {
91                  return RegionLocation.OUTSIDE;
92              } else if (loc == HyperplaneLocation.ON) {
93                  isOn = true;
94              }
95          }
96  
97          return isOn ? RegionLocation.BOUNDARY : RegionLocation.INSIDE;
98      }
99  
100     /** {@inheritDoc} */
101     @Override
102     public P project(final P pt) {
103 
104         P projected;
105         double dist;
106 
107         P closestPt = null;
108         double closestDist = Double.POSITIVE_INFINITY;
109 
110         for (final S boundary : boundaries) {
111             projected = boundary.closest(pt);
112             dist = pt.distance(projected);
113 
114             if (projected != null && (closestPt == null || dist < closestDist)) {
115                 closestPt = projected;
116                 closestDist = dist;
117             }
118         }
119 
120         return closestPt;
121     }
122 
123     /** Trim the given hyperplane subset to the portion contained inside this instance.
124      * @param sub hyperplane subset to trim. Null is returned if the subset does not intersect the instance.
125      * @return portion of the argument that lies entirely inside the region represented by
126      *      this instance, or null if it does not intersect.
127      */
128     public HyperplaneConvexSubset<P> trim(final HyperplaneConvexSubset<P> sub) {
129         HyperplaneConvexSubset<P> remaining = sub;
130         for (final S boundary : boundaries) {
131             remaining = remaining.split(boundary.getHyperplane()).getMinus();
132             if (remaining == null) {
133                 break;
134             }
135         }
136 
137         return remaining;
138     }
139 
140     /** {@inheritDoc} */
141     @Override
142     public String toString() {
143         final StringBuilder sb = new StringBuilder();
144         sb.append(this.getClass().getSimpleName())
145             .append("[boundaries= ")
146             .append(boundaries);
147 
148         return sb.toString();
149     }
150 
151     /** Generic, internal transform method. Subclasses should use this to implement their own transform methods.
152      * @param transform the transform to apply to the instance
153      * @param thisInstance a reference to the current instance; this is passed as
154      *      an argument in order to allow it to be a generic type
155      * @param boundaryType the type used for the boundary hyperplane subsets
156      * @param factory function used to create new convex region instances
157      * @param <R> Region implementation type
158      * @return the result of the transform operation
159      */
160     protected <R extends AbstractConvexHyperplaneBoundedRegion<P, S>> R transformInternal(
161             final Transform<P> transform, final R thisInstance, final Class<S> boundaryType,
162             final Function<List<S>, R> factory) {
163 
164         if (isFull()) {
165             return thisInstance;
166         }
167 
168         final List<S> origBoundaries = getBoundaries();
169 
170         final int size = origBoundaries.size();
171         final List<S> tBoundaries = new ArrayList<>(size);
172 
173         // determine if the hyperplanes should be reversed
174         final S boundary = origBoundaries.get(0);
175         HyperplaneConvexSubset<P> tBoundary = boundary.transform(transform);
176 
177         final boolean reverseDirection = swapsInsideOutside(transform);
178 
179         // transform all of the segments
180         if (reverseDirection) {
181             tBoundary = tBoundary.reverse();
182         }
183         tBoundaries.add(boundaryType.cast(tBoundary));
184 
185         for (int i = 1; i < origBoundaries.size(); ++i) {
186             tBoundary = origBoundaries.get(i).transform(transform);
187 
188             if (reverseDirection) {
189                 tBoundary = tBoundary.reverse();
190             }
191 
192             tBoundaries.add(boundaryType.cast(tBoundary));
193         }
194 
195         return factory.apply(tBoundaries);
196     }
197 
198     /** Return true if the given transform swaps the inside and outside of
199      * the region.
200      *
201      * <p>The default behavior of this method is to return true if the transform
202      * does not preserve spatial orientation (ie, {@link Transform#preservesOrientation()}
203      * is false). Subclasses may need to override this method to implement the correct
204      * behavior for their space and dimension.</p>
205      * @param transform transform to check
206      * @return true if the given transform swaps the interior and exterior of
207      *      the region
208      */
209     protected boolean swapsInsideOutside(final Transform<P> transform) {
210         return !transform.preservesOrientation();
211     }
212 
213     /** Generic, internal split method. Subclasses should call this from their {@link #split(Hyperplane)} methods.
214      * @param splitter splitting hyperplane
215      * @param thisInstance a reference to the current instance; this is passed as
216      *      an argument in order to allow it to be a generic type
217      * @param boundaryType the type used for the boundary hyperplane subsets
218      * @param factory function used to create new convex region instances
219      * @param <R> Region implementation type
220      * @return the result of the split operation
221      */
222     protected <R extends AbstractConvexHyperplaneBoundedRegion<P, S>> Split<R> splitInternal(
223             final Hyperplane<P> splitter, final R thisInstance, final Class<S> boundaryType,
224             final Function<List<S>, R> factory) {
225 
226         if (isFull()) {
227             final R minus = factory.apply(Collections.singletonList(boundaryType.cast(splitter.span())));
228             final R plus = factory.apply(Collections.singletonList(boundaryType.cast(splitter.reverse().span())));
229 
230             return new Split<>(minus, plus);
231         } else {
232             final HyperplaneConvexSubset<P> trimmedSplitter = trim(splitter.span());
233 
234             if (trimmedSplitter == null) {
235                 // The splitter lies entirely outside of the region; we need
236                 // to determine whether we lie on the plus or minus side of the splitter.
237 
238                 final SplitLocation regionLoc = determineRegionPlusMinusLocation(splitter);
239                 return regionLoc == SplitLocation.MINUS ?
240                         new Split<>(thisInstance, null) :
241                         new Split<>(null, thisInstance);
242             }
243 
244             // the splitter passes through the region; split the other region boundaries
245             // by the splitter
246             final ArrayList<S> minusBoundaries = new ArrayList<>();
247             final ArrayList<S> plusBoundaries = new ArrayList<>();
248 
249             splitBoundaries(splitter, boundaryType, minusBoundaries, plusBoundaries);
250 
251             // if the splitter was trimmed by the region boundaries, double-check that the split boundaries
252             // actually lie on both sides of the splitter; this is another case where floating point errors
253             // can cause a discrepancy between the results of splitting the splitter by the boundaries and
254             // splitting the boundaries by the splitter
255             if (!trimmedSplitter.isFull()) {
256                 if (minusBoundaries.isEmpty()) {
257                     if (plusBoundaries.isEmpty()) {
258                         return new Split<>(null, null);
259                     }
260                     return new Split<>(null, thisInstance);
261                 } else if (plusBoundaries.isEmpty()) {
262                     return new Split<>(thisInstance, null);
263                 }
264             }
265 
266             // we have a consistent region split; create the new plus and minus regions
267             minusBoundaries.add(boundaryType.cast(trimmedSplitter));
268             plusBoundaries.add(boundaryType.cast(trimmedSplitter.reverse()));
269 
270             minusBoundaries.trimToSize();
271             plusBoundaries.trimToSize();
272 
273             return new Split<>(factory.apply(minusBoundaries), factory.apply(plusBoundaries));
274         }
275     }
276 
277     /** Determine whether the region lies on the plus or minus side of the given splitter. It is assumed
278      * that (1) the region is not full, and (2) the given splitter does not pass through the region.
279      *
280      * <p>In theory, this is a very simple operation: one need only test a single region boundary
281      * to see if it lies on the plus or minus side of the splitter. In practice, however, accumulated
282      * floating point errors can cause discrepancies between the splitting operations, causing
283      * boundaries to be classified as lying on both sides of the splitter when they should only lie on one.
284      * Therefore, this method examines as many boundaries as needed in order to determine the best response.
285      * The algorithm proceeds as follows:
286      * <ol>
287      *  <li>If any boundary lies completely on the minus or plus side of the splitter, then
288      *      {@link SplitLocation#MINUS MINUS} or {@link SplitLocation#PLUS PLUS} is returned, respectively.</li>
289      *  <li>If any boundary is coincident with the splitter ({@link SplitLocation#NEITHER NEITHER}), then
290      *      {@link SplitLocation#MINUS MINUS} is returned if the boundary hyperplane has the same orientation
291      *      as the splitter, otherwise {@link SplitLocation#PLUS PLUS}.</li>
292      *  <li>If no boundaries match the above conditions, then the sizes of the split boundaries are compared. If
293      *      the sum of the sizes of the boundaries on the minus side is greater than the sum of the sizes of
294      *      the boundaries on the plus size, then {@link SplitLocation#MINUS MINUS} is returned. Otherwise,
295      *      {@link SplitLocation#PLUS PLUS} is returned.
296      * </ol>
297      * @param splitter splitter to classify the region against; the splitter is assumed to lie
298      *      completely outside of the region
299      * @return {@link SplitLocation#MINUS} if the region lies on the minus side of the splitter and
300      *      {@link SplitLocation#PLUS} if the region lies on the plus side of the splitter
301      */
302     private SplitLocation determineRegionPlusMinusLocation(final Hyperplane<P> splitter) {
303         double minusSize = 0;
304         double plusSize = 0;
305 
306         Split<? extends HyperplaneConvexSubset<P>> split;
307         SplitLocation loc;
308 
309         for (final S boundary : boundaries) {
310             split = boundary.split(splitter);
311             loc = split.getLocation();
312 
313             if (loc == SplitLocation.MINUS || loc == SplitLocation.PLUS) {
314                 return loc;
315             } else if (loc == SplitLocation.NEITHER) {
316                 return splitter.similarOrientation(boundary.getHyperplane()) ?
317                         SplitLocation.MINUS :
318                         SplitLocation.PLUS;
319             } else {
320                 minusSize += split.getMinus().getSize();
321                 plusSize += split.getPlus().getSize();
322             }
323         }
324 
325         return minusSize > plusSize ? SplitLocation.MINUS : SplitLocation.PLUS;
326     }
327 
328     /** Split the boundaries of the region by the given hyperplane, adding the split parts into the
329      * corresponding lists.
330      * @param splitter splitting hyperplane
331      * @param boundaryType the type used for the boundary hyperplane subsets
332      * @param minusBoundaries list that will contain the portions of the boundaries on the minus side
333      *      of the splitting hyperplane
334      * @param plusBoundaries list that will contain the portions of the boundaries on the plus side of
335      *      the splitting hyperplane
336      */
337     private void splitBoundaries(final Hyperplane<P> splitter, final Class<S> boundaryType,
338             final List<S> minusBoundaries, final List<S> plusBoundaries) {
339 
340         Split<? extends HyperplaneConvexSubset<P>> split;
341         HyperplaneConvexSubset<P> minusBoundary;
342         HyperplaneConvexSubset<P> plusBoundary;
343 
344         for (final S boundary : boundaries) {
345             split = boundary.split(splitter);
346 
347             minusBoundary = split.getMinus();
348             plusBoundary = split.getPlus();
349 
350             if (minusBoundary != null) {
351                 minusBoundaries.add(boundaryType.cast(minusBoundary));
352             }
353 
354             if (plusBoundary != null) {
355                 plusBoundaries.add(boundaryType.cast(plusBoundary));
356             }
357         }
358     }
359 
360     /** Internal class encapsulating the logic for building convex region boundaries from collections of hyperplanes.
361      * @param <P> Point implementation type
362      * @param <S> Hyperplane convex subset implementation type
363      */
364     protected static class ConvexRegionBoundaryBuilder<P extends Point<P>, S extends HyperplaneConvexSubset<P>> {
365 
366         /** Hyperplane convex subset implementation type. */
367         private final Class<S> subsetType;
368 
369         /** Construct a new instance for building convex region boundaries with the given hyperplane
370          * convex subset implementation type.
371          * @param subsetType Hyperplane convex subset implementation type
372          */
373         public ConvexRegionBoundaryBuilder(final Class<S> subsetType) {
374             this.subsetType = subsetType;
375         }
376 
377         /** Compute a list of hyperplane convex subsets representing the boundaries of the convex region
378          * bounded by the given collection of hyperplanes.
379          * @param bounds hyperplanes defining the convex region
380          * @return a list of hyperplane convex subsets representing the boundaries of the convex region
381          * @throws IllegalArgumentException if the given hyperplanes do not form a convex region
382          */
383         public List<S> build(final Iterable<? extends Hyperplane<P>> bounds) {
384 
385             final List<S> boundaries = new ArrayList<>();
386 
387             // cut each hyperplane by every other hyperplane in order to get the region boundaries
388             int boundIdx = 0;
389             HyperplaneConvexSubset<P> boundary;
390 
391             for (final Hyperplane<P> currentBound : bounds) {
392                 ++boundIdx;
393 
394                 boundary = splitBound(currentBound, bounds, boundIdx);
395                 if (boundary != null) {
396                     boundaries.add(subsetType.cast(boundary));
397                 }
398             }
399 
400             if (boundIdx > 0 && boundaries.isEmpty()) {
401                 // nothing was added
402                 throw nonConvexException(bounds);
403             }
404 
405             return boundaries;
406         }
407 
408         /** Split the given bounding hyperplane by all of the other hyperplanes in the given collection, returning the
409          * remaining hyperplane subset.
410          * @param currentBound the bound to split; this value is assumed to have come from {@code bounds}
411          * @param bounds collection of bounds to use to split {@code currentBound}
412          * @param currentBoundIdx the index of {@code currentBound} in {@code bounds}
413          * @return the part of {@code currentBound}'s hyperplane subset that lies on the minus side of all of the
414          *      splitting hyperplanes
415          * @throws IllegalArgumentException if the hyperplanes do not form a convex region
416          */
417         private HyperplaneConvexSubset<P> splitBound(final Hyperplane<P> currentBound,
418                 final Iterable<? extends Hyperplane<P>> bounds, final int currentBoundIdx) {
419 
420             HyperplaneConvexSubset<P> boundary = currentBound.span();
421 
422             int splitterIdx = 0;
423             for (final Hyperplane<P> splitter : bounds) {
424                 ++splitterIdx;
425 
426                 if (currentBound == splitter) {
427                     // do not split the bound with itself
428 
429                     if (currentBoundIdx > splitterIdx) {
430                         // this hyperplane is duplicated in the list; skip all but the
431                         // first insertion of its hyperplane subset
432                         return null;
433                     }
434                 } else {
435                     // split the boundary
436                     final Split<? extends HyperplaneConvexSubset<P>> split = boundary.split(splitter);
437 
438                     if (split.getLocation() == SplitLocation.NEITHER) {
439                         // the boundary lies directly on the splitter
440 
441                         if (!currentBound.similarOrientation(splitter)) {
442                             // two or more splitters are coincident and have opposite
443                             // orientations, meaning that no area is on the minus side
444                             // of both
445                             throw nonConvexException(bounds);
446                         } else if (currentBoundIdx > splitterIdx) {
447                             // two or more hyperplanes are equivalent; only use the boundary
448                             // from the first one and return null for this one
449                             return null;
450                         }
451                     } else {
452                         // retain the minus portion of the split
453                         boundary = split.getMinus();
454                     }
455                 }
456 
457                 if (boundary == null) {
458                     break;
459                 }
460             }
461 
462             return boundary;
463         }
464 
465         /** Return an exception indicating that the given collection of hyperplanes do not produce a convex region.
466          * @param bounds collection of hyperplanes
467          * @return an exception indicating that the given collection of hyperplanes do not produce a convex region
468          */
469         private IllegalArgumentException nonConvexException(final Iterable<? extends Hyperplane<P>> bounds) {
470             return new IllegalArgumentException("Bounding hyperplanes do not produce a convex region: " + bounds);
471         }
472     }
473 }