blob: f694a898c047f52c5ca1f97beab14249a57d90bf [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.geometry.core.partitioning;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.geometry.core.Point;
import org.apache.commons.geometry.core.partitioning.Region.Location;
/** Local tree visitor to compute projection on boundary.
* @param <P> Point type defining the space
* @param <S> Point type defining the sub-space
*/
class BoundaryProjector<P extends Point<P>, S extends Point<S>> implements BSPTreeVisitor<P> {
/** Original point. */
private final P original;
/** Current best projected point. */
private P projected;
/** Leaf node closest to the test point. */
private BSPTree<P> leaf;
/** Current offset. */
private double offset;
/** Simple constructor.
* @param original original point
*/
BoundaryProjector(final P original) {
this.original = original;
this.projected = null;
this.leaf = null;
this.offset = Double.POSITIVE_INFINITY;
}
/** {@inheritDoc} */
@Override
public Order visitOrder(final BSPTree<P> node) {
// we want to visit the tree so that the first encountered
// leaf is the one closest to the test point
if (node.getCut().getHyperplane().getOffset(original) <= 0) {
return Order.MINUS_SUB_PLUS;
} else {
return Order.PLUS_SUB_MINUS;
}
}
/** {@inheritDoc} */
@Override
public void visitInternalNode(final BSPTree<P> node) {
// project the point on the cut sub-hyperplane
final Hyperplane<P> hyperplane = node.getCut().getHyperplane();
final double signedOffset = hyperplane.getOffset(original);
if (Math.abs(signedOffset) < offset) {
// project point
final P regular = hyperplane.project(original);
// get boundary parts
final List<Region<S>> boundaryParts = boundaryRegions(node);
// check if regular projection really belongs to the boundary
boolean regularFound = false;
for (final Region<S> part : boundaryParts) {
if (!regularFound && belongsToPart(regular, hyperplane, part)) {
// the projected point lies in the boundary
projected = regular;
offset = Math.abs(signedOffset);
regularFound = true;
}
}
if (!regularFound) {
// the regular projected point is not on boundary,
// so we have to check further if a singular point
// (i.e. a vertex in 2D case) is a possible projection
for (final Region<S> part : boundaryParts) {
final P spI = singularProjection(regular, hyperplane, part);
if (spI != null) {
final double distance = original.distance(spI);
if (distance < offset) {
projected = spI;
offset = distance;
}
}
}
}
}
}
/** {@inheritDoc} */
@Override
public void visitLeafNode(final BSPTree<P> node) {
if (leaf == null) {
// this is the first leaf we visit,
// it is the closest one to the original point
leaf = node;
}
}
/** Get the projection.
* @return projection
*/
public BoundaryProjection<P> getProjection() {
// fix offset sign
offset = Math.copySign(offset, (Boolean) leaf.getAttribute() ? -1 : +1);
return new BoundaryProjection<>(original, projected, offset);
}
/** Extract the regions of the boundary on an internal node.
* @param node internal node
* @return regions in the node sub-hyperplane
*/
private List<Region<S>> boundaryRegions(final BSPTree<P> node) {
final List<Region<S>> regions = new ArrayList<>(2);
@SuppressWarnings("unchecked")
final BoundaryAttribute<P> ba = (BoundaryAttribute<P>) node.getAttribute();
addRegion(ba.getPlusInside(), regions);
addRegion(ba.getPlusOutside(), regions);
return regions;
}
/** Add a boundary region to a list.
* @param sub sub-hyperplane defining the region
* @param list to fill up
*/
private void addRegion(final SubHyperplane<P> sub, final List<Region<S>> list) {
if (sub != null) {
@SuppressWarnings("unchecked")
final Region<S> region = ((AbstractSubHyperplane<P, S>) sub).getRemainingRegion();
if (region != null) {
list.add(region);
}
}
}
/** Check if a projected point lies on a boundary part.
* @param point projected point to check
* @param hyperplane hyperplane into which the point was projected
* @param part boundary part
* @return true if point lies on the boundary part
*/
private boolean belongsToPart(final P point, final Hyperplane<P> hyperplane,
final Region<S> part) {
// there is a non-null sub-space, we can dive into smaller dimensions
@SuppressWarnings("unchecked")
final Embedding<P, S> embedding = (Embedding<P, S>) hyperplane;
return part.checkPoint(embedding.toSubSpace(point)) != Location.OUTSIDE;
}
/** Get the projection to the closest boundary singular point.
* @param point projected point to check
* @param hyperplane hyperplane into which the point was projected
* @param part boundary part
* @return projection to a singular point of boundary part (may be null)
*/
private P singularProjection(final P point, final Hyperplane<P> hyperplane,
final Region<S> part) {
// there is a non-null sub-space, we can dive into smaller dimensions
@SuppressWarnings("unchecked")
final Embedding<P, S> embedding = (Embedding<P, S>) hyperplane;
final BoundaryProjection<S> bp = part.projectToBoundary(embedding.toSubSpace(point));
// back to initial dimension
return (bp.getProjected() == null) ? null : embedding.toSpace(bp.getProjected());
}
}