Commit 7eb83cde authored by Matthias Betz's avatar Matthias Betz
Browse files

fixes

parent 0a760af4
Pipeline #12384 failed with stage
in 1 minute and 35 seconds
package de.hft.stuttgart.citydoctor2.datastructure.bht; package de.hft.stuttgart.citydoctor2.datastructure.bht;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon; import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
/** /**
* Generic Bounding Volume Hierarchy (BVH) for AABBs. * Generic Bounding Volume Hierarchy (BVH) for AABBs.
* *
* Supports: * Supports: - binary BVH (degree 2) - octonary / 8-ary BVH (degree 8)
* - binary BVH (degree 2)
* - octonary / 8-ary BVH (degree 8)
* *
* Split strategies: * Split strategies: - binary: object median, object mean, spatial median -
* - binary: object median, object mean, spatial median * octonary: object median, object mean, spatial median
* - octonary: object median, object mean, spatial median
* *
* Notes: * Notes: - This implementation caches AABBs and centers during build time via
* - This implementation caches AABBs and centers during build time via BuildItem. * BuildItem. - The input list is NOT reordered. - The octonary variant is
* - The input list is NOT reordered. * implemented as an 8-ary BVH with tight child AABBs built from bucket
* - The octonary variant is implemented as an 8-ary BVH with tight child AABBs * contents, not as a strict spatial octree with fixed cell boxes.
* built from bucket contents, not as a strict spatial octree with fixed cell boxes.
* *
* @param <E> element type * @param <E> element type
*/ */
public class BoundingVolumeHierarchyTree<E> { public class BoundingVolumeHierarchyTree<E> {
private Node<E> root; private Node<E> root;
private final Function<E, AABB> aabbFunction; private final Function<E, AABB> aabbFunction;
private static final int DEFAULT_MIN_DEPTH = 4; private static final int DEFAULT_MIN_DEPTH = 4;
private static final int DEFAULT_MAX_DEPTH = 32; private static final int DEFAULT_MAX_DEPTH = 32;
private static final int DEFAULT_BINARY_LEAF_SIZE = 1; private static final int DEFAULT_BINARY_LEAF_SIZE = 1;
private static final int DEFAULT_OCTONARY_LEAF_SIZE = 8; private static final int DEFAULT_OCTONARY_LEAF_SIZE = 8;
private static final double DEFAULT_DEGENERATE_TOL = 1e-12; private static final double DEFAULT_DEGENERATE_TOL = 1e-12;
public enum SplitStrategy { public enum SplitStrategy {
AUTO, AUTO,
BINARY_OBJECT_MEDIAN, BINARY_OBJECT_MEDIAN, BINARY_OBJECT_MEAN, BINARY_SPATIAL_MEDIAN,
BINARY_OBJECT_MEAN,
BINARY_SPATIAL_MEDIAN, OCTONARY_OBJECT_MEDIAN, OCTONARY_OBJECT_MEAN, OCTONARY_SPATIAL_MEDIAN
}
OCTONARY_OBJECT_MEDIAN,
OCTONARY_OBJECT_MEAN, public static final class Builder<E> {
OCTONARY_SPATIAL_MEDIAN private int degree = 2;
} private SplitStrategy splitStrategy = SplitStrategy.AUTO;
private int maxLeafSize = DEFAULT_BINARY_LEAF_SIZE;
public static final class BuildConfig { private int maxDepth = DEFAULT_MAX_DEPTH;
private final int degree; private double degenerateTolerance = DEFAULT_DEGENERATE_TOL;
private final SplitStrategy splitStrategy; private List<E> elements;
private final int maxLeafSize; private Function<E, AABB> aabbFunction;
private final int maxDepth;
private final double degenerateTolerance; public Builder() {
}
private BuildConfig(Builder b) {
this.degree = b.degree; public Builder<E> elements(List<E> elements) {
this.splitStrategy = Objects.requireNonNull(b.splitStrategy, "splitStrategy"); this.elements = elements;
this.maxLeafSize = b.maxLeafSize; return this;
this.maxDepth = b.maxDepth; }
this.degenerateTolerance = b.degenerateTolerance;
} public Builder<E> function(Function<E, AABB> aabbFunction) {
this.aabbFunction = aabbFunction;
public int getDegree() { return this;
return degree; }
}
public Builder<E> degree(int degree) {
public SplitStrategy getSplitStrategy() { this.degree = degree;
return splitStrategy; return this;
} }
public int getMaxLeafSize() { public Builder<E> splitStrategy(SplitStrategy splitStrategy) {
return maxLeafSize; this.splitStrategy = splitStrategy;
} return this;
}
public int getMaxDepth() {
return maxDepth; public Builder<E> maxLeafSize(int maxLeafSize) {
} this.maxLeafSize = maxLeafSize;
return this;
public double getDegenerateTolerance() { }
return degenerateTolerance;
} public Builder<E> maxDepth(int maxDepth) {
this.maxDepth = maxDepth;
public static Builder builder() { return this;
return new Builder(); }
}
public Builder<E> degenerateTolerance(double degenerateTolerance) {
public static BuildConfig binaryDefault() { this.degenerateTolerance = degenerateTolerance;
return builder() return this;
.degree(2) }
.splitStrategy(SplitStrategy.AUTO)
.maxLeafSize(DEFAULT_BINARY_LEAF_SIZE) public Function<E, AABB> getAabbFunction() {
.maxDepth(DEFAULT_MAX_DEPTH) return aabbFunction;
.degenerateTolerance(DEFAULT_DEGENERATE_TOL) }
.build();
} public double getDegenerateTolerance() {
return degenerateTolerance;
public static BuildConfig octonaryDefault() { }
return builder()
.degree(8) public int getDegree() {
.splitStrategy(SplitStrategy.AUTO) return degree;
.maxLeafSize(DEFAULT_OCTONARY_LEAF_SIZE) }
.maxDepth(DEFAULT_MAX_DEPTH)
.degenerateTolerance(DEFAULT_DEGENERATE_TOL) public List<E> getElements() {
.build(); return elements;
} }
public static final class Builder { public int getMaxDepth() {
private int degree = 2; return maxDepth;
private SplitStrategy splitStrategy = SplitStrategy.AUTO; }
private int maxLeafSize = DEFAULT_BINARY_LEAF_SIZE;
private int maxDepth = DEFAULT_MAX_DEPTH; public int getMaxLeafSize() {
private double degenerateTolerance = DEFAULT_DEGENERATE_TOL; return maxLeafSize;
}
public Builder degree(int degree) {
this.degree = degree; public SplitStrategy getSplitStrategy() {
return this; return splitStrategy;
} }
public Builder splitStrategy(SplitStrategy splitStrategy) { public Builder<E> binaryDefault() {
this.splitStrategy = splitStrategy; return new Builder<E>().degree(2).splitStrategy(SplitStrategy.AUTO).maxLeafSize(DEFAULT_BINARY_LEAF_SIZE)
return this; .maxDepth(DEFAULT_MAX_DEPTH).degenerateTolerance(DEFAULT_DEGENERATE_TOL);
} }
public Builder maxLeafSize(int maxLeafSize) { public Builder<E> octonaryDefault() {
this.maxLeafSize = maxLeafSize; return new Builder<E>().degree(8).splitStrategy(SplitStrategy.AUTO).maxLeafSize(DEFAULT_OCTONARY_LEAF_SIZE)
return this; .maxDepth(DEFAULT_MAX_DEPTH).degenerateTolerance(DEFAULT_DEGENERATE_TOL);
} }
public Builder maxDepth(int maxDepth) { public BoundingVolumeHierarchyTree<E> build() {
this.maxDepth = maxDepth; if (degree != 2 && degree != 8) {
return this; throw new IllegalArgumentException("Only degree 2 and 8 are supported.");
} }
if (maxLeafSize < 1) {
public Builder degenerateTolerance(double degenerateTolerance) { throw new IllegalArgumentException("maxLeafSize must be >= 1.");
this.degenerateTolerance = degenerateTolerance; }
return this; if (maxDepth < 1) {
} throw new IllegalArgumentException("maxDepth must be >= 1.");
}
public BuildConfig build() { if (degenerateTolerance < 0.0) {
if (degree != 2 && degree != 8) { throw new IllegalArgumentException("degenerateTolerance must be >= 0.");
throw new IllegalArgumentException("Only degree 2 and 8 are supported."); }
} return new BoundingVolumeHierarchyTree<E>(this);
if (maxLeafSize < 1) { }
throw new IllegalArgumentException("maxLeafSize must be >= 1."); }
}
if (maxDepth < 1) { /**
throw new IllegalArgumentException("maxDepth must be >= 1."); * Internal build-time cache.
} */
if (degenerateTolerance < 0.0) { private static final class BuildItem<E> {
throw new IllegalArgumentException("degenerateTolerance must be >= 0."); final E element;
} final AABB aabb;
return new BuildConfig(this); final double centerX;
} final double centerY;
} final double centerZ;
}
BuildItem(E element, AABB aabb) {
/** this.element = element;
* Internal build-time cache. this.aabb = aabb;
*/ this.centerX = 0.5 * (aabb.getMinX() + aabb.getMaxX());
private static final class BuildItem<E> { this.centerY = 0.5 * (aabb.getMinY() + aabb.getMaxY());
final E element; this.centerZ = 0.5 * (aabb.getMinZ() + aabb.getMaxZ());
final AABB aabb; }
final double centerX;
final double centerY; double center(int axis) {
final double centerZ; switch (axis) {
case 0:
BuildItem(E element, AABB aabb) { return centerX;
this.element = element; case 1:
this.aabb = aabb; return centerY;
this.centerX = 0.5 * (aabb.getMinX() + aabb.getMaxX()); case 2:
this.centerY = 0.5 * (aabb.getMinY() + aabb.getMaxY()); return centerZ;
this.centerZ = 0.5 * (aabb.getMinZ() + aabb.getMaxZ()); default:
} throw new IllegalArgumentException("axis must be 0, 1, or 2");
}
double center(int axis) { }
switch (axis) { }
case 0: return centerX;
case 1: return centerY; private BoundingVolumeHierarchyTree(Builder<E> builder) {
case 2: return centerZ; Objects.requireNonNull(builder, "config");
default: throw new IllegalArgumentException("axis must be 0, 1, or 2"); this.aabbFunction = Objects.requireNonNull(builder.aabbFunction);
} List<E> elements = Objects.requireNonNull(builder.getElements());
} if (elements == null || elements.isEmpty()) {
} this.root = null;
return;
/** }
* Creates an empty BVH.
*/ List<BuildItem<E>> items = toBuildItems(elements);
public BoundingVolumeHierarchyTree(Function<E, AABB> aabbFunction) { SplitStrategy resolvedStrategy = resolveSplitStrategy(builder);
this.root = null;
this.aabbFunction = Objects.requireNonNull(aabbFunction, "aabbFunction"); if (builder.getDegree() == 2) {
} this.root = buildBinaryRecursive(items, 0, resolvedStrategy, builder);
} else {
/** this.root = buildOctonaryRecursive(items, 0, resolvedStrategy, builder);
* Creates a binary BVH with default config (AUTO -> binary spatial median). }
*/ }
public BoundingVolumeHierarchyTree(List<E> elements, Function<E, AABB> aabbFunction) {
this(elements, aabbFunction, BuildConfig.binaryDefault()); /**
} * Convenience factory: binary BVH with AUTO/defaults.
*/
/** public static <E> BoundingVolumeHierarchyTree<E> newBinary(List<E> elements, Function<E, AABB> aabbFunction) {
* Creates a BVH with a full config. Builder<E> binaryDefault = new Builder<E>().binaryDefault();
*/ binaryDefault.elements(elements);
public BoundingVolumeHierarchyTree(List<E> elements, Function<E, AABB> aabbFunction, BuildConfig config) { binaryDefault.function(aabbFunction);
this.aabbFunction = Objects.requireNonNull(aabbFunction, "aabbFunction"); return new BoundingVolumeHierarchyTree<>(binaryDefault);
Objects.requireNonNull(config, "config"); }
if (elements == null || elements.isEmpty()) { /**
this.root = null; * Convenience factory: octonary BVH with AUTO/defaults.
return; */
} public static <E> BoundingVolumeHierarchyTree<E> newOctonary(List<E> elements, Function<E, AABB> aabbFunction) {
Builder<E> octonaryDefault = new Builder<E>().octonaryDefault();
List<BuildItem<E>> items = toBuildItems(elements); octonaryDefault.elements(elements);
SplitStrategy resolvedStrategy = resolveSplitStrategy(config); octonaryDefault.function(aabbFunction);
return new BoundingVolumeHierarchyTree<>(octonaryDefault);
if (config.getDegree() == 2) { }
this.root = buildBinaryRecursive(items, 0, resolvedStrategy, config);
} else { /**
this.root = buildOctonaryRecursive(items, 0, resolvedStrategy, config); * Convenience factory: octonary BVH with a custom maxLeafSize.
} */
} public static <E> BoundingVolumeHierarchyTree<E> newOctonary(List<E> elements, Function<E, AABB> aabbFunction,
int maxLeafSize) {
/** Builder<E> octonaryDefault = new Builder<E>().octonaryDefault();
* Convenience factory: binary BVH with AUTO/defaults. octonaryDefault.elements(elements);
*/ octonaryDefault.function(aabbFunction);
public static <E> BoundingVolumeHierarchyTree<E> newBinary( octonaryDefault.maxLeafSize(maxLeafSize);
List<E> elements, octonaryDefault.maxDepth(computeDefaultMaxDepth(elements != null ? elements.size() : 0));
Function<E, AABB> aabbFunction) { return new BoundingVolumeHierarchyTree<>(octonaryDefault);
return new BoundingVolumeHierarchyTree<>(elements, aabbFunction, BuildConfig.binaryDefault()); }
}
public Node<E> getRoot() {
/** return root;
* Convenience factory: octonary BVH with AUTO/defaults. }
*/
public static <E> BoundingVolumeHierarchyTree<E> newOctonary( public void setRoot(Node<E> root) {
List<E> elements, this.root = root;
Function<E, AABB> aabbFunction) { }
return new BoundingVolumeHierarchyTree<>(elements, aabbFunction, BuildConfig.octonaryDefault());
} /**
* NOTE: generic version already exists.
/** */
* Convenience factory: octonary BVH with a custom maxLeafSize. public static AABB getAggregateAABB(List<ConcretePolygon> polygons) {
*/ if (polygons.size() == 1) {
public static <E> BoundingVolumeHierarchyTree<E> newOctonary( return AABB.of(polygons.get(0));
List<E> elements, }
Function<E, AABB> aabbFunction, return getAggregateAABB(polygons, AABB::of);
int maxLeafSize) { }
BuildConfig cfg = BuildConfig.builder()
.degree(8) public static <E> AABB getAggregateAABB(List<E> elements, Function<E, AABB> aabbFunc) {
.splitStrategy(SplitStrategy.AUTO) double minX = Double.POSITIVE_INFINITY;
.maxLeafSize(maxLeafSize) double minY = Double.POSITIVE_INFINITY;
.maxDepth(computeDefaultMaxDepth(elements != null ? elements.size() : 0)) double minZ = Double.POSITIVE_INFINITY;
.degenerateTolerance(DEFAULT_DEGENERATE_TOL) double maxX = Double.NEGATIVE_INFINITY;
.build(); double maxY = Double.NEGATIVE_INFINITY;
return new BoundingVolumeHierarchyTree<>(elements, aabbFunction, cfg); double maxZ = Double.NEGATIVE_INFINITY;
}
for (E e : elements) {
public Node<E> getRoot() { AABB aabb = aabbFunc.apply(e);
return root; minX = Math.min(minX, aabb.getMinX());
} minY = Math.min(minY, aabb.getMinY());
minZ = Math.min(minZ, aabb.getMinZ());
public void setRoot(Node<E> root) { maxX = Math.max(maxX, aabb.getMaxX());
this.root = root; maxY = Math.max(maxY, aabb.getMaxY());
} maxZ = Math.max(maxZ, aabb.getMaxZ());
}
/**
* NOTE: generic version already exists. return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
*/ }
public static AABB getAggregateAABB(List<ConcretePolygon> polygons) {
if (polygons.size() == 1) { public List<E> getAllIntersectingElements(AABB query) {
return AABB.of(polygons.get(0)); List<E> result = new ArrayList<>();
} getAllIntersectingElementsRecursive(root, query, result);
return getAggregateAABB(polygons, AABB::of); return result;
} }
public static <E> AABB getAggregateAABB(List<E> elements, Function<E, AABB> aabbFunc) { private void getAllIntersectingElementsRecursive(Node<E> node, AABB query, List<E> result) {
double minX = Double.POSITIVE_INFINITY; if (node == null) {
double minY = Double.POSITIVE_INFINITY; return;
double minZ = Double.POSITIVE_INFINITY; }
double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY; if (!node.getAabb().overlaps(query)) {
double maxZ = Double.NEGATIVE_INFINITY; return;
}
for (E e : elements) {
AABB aabb = aabbFunc.apply(e); if (node.isLeaf()) {
minX = Math.min(minX, aabb.getMinX()); result.add(node.getElement());
minY = Math.min(minY, aabb.getMinY()); return;
minZ = Math.min(minZ, aabb.getMinZ()); }
maxX = Math.max(maxX, aabb.getMaxX());
maxY = Math.max(maxY, aabb.getMaxY()); for (Node<E> child : node.getChildren()) {
maxZ = Math.max(maxZ, aabb.getMaxZ()); getAllIntersectingElementsRecursive(child, query, result);
} }
}
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
} // ---------------------------------------------------------------------
// Build
public List<E> getAllIntersectingElements(AABB query) { // ---------------------------------------------------------------------
List<E> result = new ArrayList<>();
getAllIntersectingElementsRecursive(root, query, result); private List<BuildItem<E>> toBuildItems(List<E> elements) {
return result; List<BuildItem<E>> items = new ArrayList<>(elements.size());
} for (E e : elements) {
AABB aabb = Objects.requireNonNull(aabbFunction.apply(e), "aabbFunction returned null");
private void getAllIntersectingElementsRecursive(Node<E> node, AABB query, List<E> result) { items.add(new BuildItem<>(e, aabb));
if (node == null) { }
return; return items;
} }
if (!node.getAabb().overlaps(query)) { private SplitStrategy resolveSplitStrategy(Builder<E> config) {
return; if (config.getSplitStrategy() != SplitStrategy.AUTO) {
} validateStrategyMatchesDegree(config.getDegree(), config.getSplitStrategy());
return config.getSplitStrategy();
if (node.isLeaf()) { }
result.add(node.getElement());
return; return (config.getDegree() == 2) ? SplitStrategy.BINARY_SPATIAL_MEDIAN : SplitStrategy.OCTONARY_OBJECT_MEAN;
} }
for (Node<E> child : node.getChildren()) { private void validateStrategyMatchesDegree(int degree, SplitStrategy strategy) {
getAllIntersectingElementsRecursive(child, query, result); boolean binary = strategy == SplitStrategy.BINARY_OBJECT_MEDIAN || strategy == SplitStrategy.BINARY_OBJECT_MEAN
} || strategy == SplitStrategy.BINARY_SPATIAL_MEDIAN;
}
boolean octonary = strategy == SplitStrategy.OCTONARY_OBJECT_MEDIAN
// --------------------------------------------------------------------- || strategy == SplitStrategy.OCTONARY_OBJECT_MEAN || strategy == SplitStrategy.OCTONARY_SPATIAL_MEDIAN;
// Build
// --------------------------------------------------------------------- if (degree == 2 && !binary) {
throw new IllegalArgumentException("Strategy " + strategy + " does not match degree 2.");
private List<BuildItem<E>> toBuildItems(List<E> elements) { }
List<BuildItem<E>> items = new ArrayList<>(elements.size()); if (degree == 8 && !octonary) {
for (E e : elements) { throw new IllegalArgumentException("Strategy " + strategy + " does not match degree 8.");
AABB aabb = Objects.requireNonNull(aabbFunction.apply(e), "aabbFunction returned null"); }
items.add(new BuildItem<>(e, aabb)); }
}
return items; private Node<E> buildBinaryRecursive(List<BuildItem<E>> items, int depth, SplitStrategy strategy,
} Builder<E> config) {
private SplitStrategy resolveSplitStrategy(BuildConfig config) { AABB totalAabb = getAggregateAABBFromItems(items);
if (config.getSplitStrategy() != SplitStrategy.AUTO) {
validateStrategyMatchesDegree(config.getDegree(), config.getSplitStrategy()); if (shouldStop(items, depth, config, totalAabb)) {
return config.getSplitStrategy(); return packLeafFromItems(items, totalAabb);
} }
return (config.getDegree() == 2) int axis = totalAabb.findLongestAxis();
? SplitStrategy.BINARY_SPATIAL_MEDIAN SplitResult<E> split;
: SplitStrategy.OCTONARY_OBJECT_MEAN;
} switch (strategy) {
case BINARY_OBJECT_MEDIAN:
private void validateStrategyMatchesDegree(int degree, SplitStrategy strategy) { split = splitBinaryObjectMedian(items, axis);
boolean binary = strategy == SplitStrategy.BINARY_OBJECT_MEDIAN break;
|| strategy == SplitStrategy.BINARY_OBJECT_MEAN case BINARY_OBJECT_MEAN:
|| strategy == SplitStrategy.BINARY_SPATIAL_MEDIAN; split = splitBinaryObjectMean(items, axis);
break;
boolean octonary = strategy == SplitStrategy.OCTONARY_OBJECT_MEDIAN case BINARY_SPATIAL_MEDIAN:
|| strategy == SplitStrategy.OCTONARY_OBJECT_MEAN split = splitBinarySpatialMedian(items, axis, totalAabb);
|| strategy == SplitStrategy.OCTONARY_SPATIAL_MEDIAN; break;
default:
if (degree == 2 && !binary) { throw new IllegalStateException("Unexpected binary strategy: " + strategy);
throw new IllegalArgumentException("Strategy " + strategy + " does not match degree 2."); }
}
if (degree == 8 && !octonary) { if (!split.valid()) {
throw new IllegalArgumentException("Strategy " + strategy + " does not match degree 8."); return packLeafFromItems(items, totalAabb);
} }
}
Node<E> node = new Node<>(null, totalAabb);
private Node<E> buildBinaryRecursive( node.getChildren().add(buildBinaryRecursive(split.left, depth + 1, strategy, config));
List<BuildItem<E>> items, node.getChildren().add(buildBinaryRecursive(split.right, depth + 1, strategy, config));
int depth, return node;
SplitStrategy strategy, }
BuildConfig config) {
private Node<E> buildOctonaryRecursive(List<BuildItem<E>> items, int depth, SplitStrategy strategy,
AABB totalAabb = getAggregateAABBFromItems(items); Builder<E> config) {
if (shouldStop(items, depth, config, totalAabb)) { AABB totalAabb = getAggregateAABBFromItems(items);
return packLeafFromItems(items, totalAabb);
} if (shouldStop(items, depth, config, totalAabb)) {
return packLeafFromItems(items, totalAabb);
int axis = totalAabb.findLongestAxis(); }
SplitResult<E> split;
OctSplit<E> split;
switch (strategy) { switch (strategy) {
case BINARY_OBJECT_MEDIAN: case OCTONARY_OBJECT_MEDIAN:
split = splitBinaryObjectMedian(items, axis); split = splitOctonaryObjectMedian(items);
break; break;
case BINARY_OBJECT_MEAN: case OCTONARY_OBJECT_MEAN:
split = splitBinaryObjectMean(items, axis); split = splitOctonaryObjectMean(items);
break; break;
case BINARY_SPATIAL_MEDIAN: case OCTONARY_SPATIAL_MEDIAN:
split = splitBinarySpatialMedian(items, axis, totalAabb); split = splitOctonarySpatialMedian(items, totalAabb);
break; break;
default: default:
throw new IllegalStateException("Unexpected binary strategy: " + strategy); throw new IllegalStateException("Unexpected octonary strategy: " + strategy);
} }
if (!split.valid()) { if (!split.valid()) {
return packLeafFromItems(items, totalAabb); return packLeafFromItems(items, totalAabb);
} }
Node<E> node = new Node<>(null, totalAabb); Node<E> node = new Node<>(null, totalAabb);
node.getChildren().add(buildBinaryRecursive(split.left, depth + 1, strategy, config));
node.getChildren().add(buildBinaryRecursive(split.right, depth + 1, strategy, config)); for (int i = 0; i < 8; i++) {
return node; List<BuildItem<E>> bucket = split.buckets[i];
} if (bucket.isEmpty()) {
continue;
private Node<E> buildOctonaryRecursive( }
List<BuildItem<E>> items, node.getChildren().add(buildOctonaryRecursive(bucket, depth + 1, strategy, config));
int depth, }
SplitStrategy strategy,
BuildConfig config) { return node;
}
AABB totalAabb = getAggregateAABBFromItems(items);
private boolean shouldStop(List<BuildItem<E>> items, int depth, Builder<E> config, AABB totalAabb) {
if (shouldStop(items, depth, config, totalAabb)) { return items.size() <= config.getMaxLeafSize() || depth >= config.getMaxDepth()
return packLeafFromItems(items, totalAabb); || totalAabb.isDegenerate(config.getDegenerateTolerance());
} }
OctSplit<E> split; private Node<E> packLeafFromItems(List<BuildItem<E>> items, AABB totalAabb) {
switch (strategy) { if (items.size() == 1) {
case OCTONARY_OBJECT_MEDIAN: BuildItem<E> item = items.get(0);
split = splitOctonaryObjectMedian(items); return new Node<>(item.element, item.aabb);
break; }
case OCTONARY_OBJECT_MEAN:
split = splitOctonaryObjectMean(items); Node<E> leaf = new Node<>(null, totalAabb);
break; for (BuildItem<E> item : items) {
case OCTONARY_SPATIAL_MEDIAN: leaf.getChildren().add(new Node<>(item.element, item.aabb));
split = splitOctonarySpatialMedian(items, totalAabb); }
break; return leaf;
default: }
throw new IllegalStateException("Unexpected octonary strategy: " + strategy);
} private static <E> AABB getAggregateAABBFromItems(List<BuildItem<E>> items) {
double minX = Double.POSITIVE_INFINITY;
if (!split.valid()) { double minY = Double.POSITIVE_INFINITY;
return packLeafFromItems(items, totalAabb); double minZ = Double.POSITIVE_INFINITY;
} double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
Node<E> node = new Node<>(null, totalAabb); double maxZ = Double.NEGATIVE_INFINITY;
for (int i = 0; i < 8; i++) { for (BuildItem<E> item : items) {
List<BuildItem<E>> bucket = split.buckets[i]; AABB aabb = item.aabb;
if (bucket.isEmpty()) { minX = Math.min(minX, aabb.getMinX());
continue; minY = Math.min(minY, aabb.getMinY());
} minZ = Math.min(minZ, aabb.getMinZ());
node.getChildren().add(buildOctonaryRecursive(bucket, depth + 1, strategy, config)); maxX = Math.max(maxX, aabb.getMaxX());
} maxY = Math.max(maxY, aabb.getMaxY());
maxZ = Math.max(maxZ, aabb.getMaxZ());
return node; }
}
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
private boolean shouldStop(List<BuildItem<E>> items, int depth, BuildConfig config, AABB totalAabb) { }
return items.size() <= config.getMaxLeafSize()
|| depth >= config.getMaxDepth() // ---------------------------------------------------------------------
|| totalAabb.isDegenerate(config.getDegenerateTolerance()); // Binary splits
} // ---------------------------------------------------------------------
private Node<E> packLeafFromItems(List<BuildItem<E>> items, AABB totalAabb) { private SplitResult<E> splitBinaryObjectMedian(List<BuildItem<E>> items, int axis) {
if (items.size() == 1) { items.sort(Comparator.comparingDouble(it -> it.center(axis)));
BuildItem<E> item = items.get(0); int mid = items.size() / 2;
return new Node<>(item.element, item.aabb);
} if (mid <= 0 || mid >= items.size()) {
return SplitResult.invalid();
Node<E> leaf = new Node<>(null, totalAabb); }
for (BuildItem<E> item : items) {
leaf.getChildren().add(new Node<>(item.element, item.aabb)); return SplitResult.of(items.subList(0, mid), items.subList(mid, items.size()));
} }
return leaf;
} private SplitResult<E> splitBinaryObjectMean(List<BuildItem<E>> items, int axis) {
double sum = 0.0;
private static <E> AABB getAggregateAABBFromItems(List<BuildItem<E>> items) { for (BuildItem<E> item : items) {
double minX = Double.POSITIVE_INFINITY; sum += item.center(axis);
double minY = Double.POSITIVE_INFINITY; }
double minZ = Double.POSITIVE_INFINITY; double splitValue = sum / items.size();
double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY; List<BuildItem<E>> left = new ArrayList<>();
double maxZ = Double.NEGATIVE_INFINITY; List<BuildItem<E>> right = new ArrayList<>();
for (BuildItem<E> item : items) { for (BuildItem<E> item : items) {
AABB aabb = item.aabb; if (item.center(axis) < splitValue) {
minX = Math.min(minX, aabb.getMinX()); left.add(item);
minY = Math.min(minY, aabb.getMinY()); } else {
minZ = Math.min(minZ, aabb.getMinZ()); right.add(item);
maxX = Math.max(maxX, aabb.getMaxX()); }
maxY = Math.max(maxY, aabb.getMaxY()); }
maxZ = Math.max(maxZ, aabb.getMaxZ());
} if (left.isEmpty() || right.isEmpty()) {
return splitBinaryObjectMedian(items, axis);
return new AABB(minX, minY, minZ, maxX, maxY, maxZ); }
}
return SplitResult.of(left, right);
// --------------------------------------------------------------------- }
// Binary splits
// --------------------------------------------------------------------- private SplitResult<E> splitBinarySpatialMedian(List<BuildItem<E>> items, int axis, AABB totalAabb) {
double splitValue;
private SplitResult<E> splitBinaryObjectMedian(List<BuildItem<E>> items, int axis) { switch (axis) {
items.sort(Comparator.comparingDouble(it -> it.center(axis))); case 0:
int mid = items.size() / 2; splitValue = totalAabb.getCenterX();
break;
if (mid <= 0 || mid >= items.size()) { case 1:
return SplitResult.invalid(); splitValue = totalAabb.getCenterY();
} break;
case 2:
return SplitResult.of(items.subList(0, mid), items.subList(mid, items.size())); splitValue = totalAabb.getCenterZ();
} break;
default:
private SplitResult<E> splitBinaryObjectMean(List<BuildItem<E>> items, int axis) { throw new IllegalArgumentException("axis must be 0, 1, or 2");
double sum = 0.0; }
for (BuildItem<E> item : items) {
sum += item.center(axis); List<BuildItem<E>> left = new ArrayList<>();
} List<BuildItem<E>> right = new ArrayList<>();
double splitValue = sum / items.size();
for (BuildItem<E> item : items) {
List<BuildItem<E>> left = new ArrayList<>(); if (item.center(axis) < splitValue) {
List<BuildItem<E>> right = new ArrayList<>(); left.add(item);
} else {
for (BuildItem<E> item : items) { right.add(item);
if (item.center(axis) < splitValue) { }
left.add(item); }
} else {
right.add(item); if (left.isEmpty() || right.isEmpty()) {
} return splitBinaryObjectMean(items, axis);
} }
if (left.isEmpty() || right.isEmpty()) { return SplitResult.of(left, right);
return splitBinaryObjectMedian(items, axis); }
}
private static final class SplitResult<E> {
return SplitResult.of(left, right); final List<BuildItem<E>> left;
} final List<BuildItem<E>> right;
private SplitResult<E> splitBinarySpatialMedian(List<BuildItem<E>> items, int axis, AABB totalAabb) { private SplitResult(List<BuildItem<E>> left, List<BuildItem<E>> right) {
double splitValue; this.left = left;
switch (axis) { this.right = right;
case 0: splitValue = totalAabb.getCenterX(); break; }
case 1: splitValue = totalAabb.getCenterY(); break;
case 2: splitValue = totalAabb.getCenterZ(); break; static <E> SplitResult<E> of(List<BuildItem<E>> left, List<BuildItem<E>> right) {
default: throw new IllegalArgumentException("axis must be 0, 1, or 2"); return new SplitResult<>(left, right);
} }
List<BuildItem<E>> left = new ArrayList<>(); static <E> SplitResult<E> invalid() {
List<BuildItem<E>> right = new ArrayList<>(); return new SplitResult<>(null, null);
}
for (BuildItem<E> item : items) {
if (item.center(axis) < splitValue) { boolean valid() {
left.add(item); return left != null && right != null && !left.isEmpty() && !right.isEmpty();
} else { }
right.add(item); }
}
} // ---------------------------------------------------------------------
// Octonary / 8-ary splits
if (left.isEmpty() || right.isEmpty()) { // ---------------------------------------------------------------------
return splitBinaryObjectMean(items, axis);
} private OctSplit<E> splitOctonaryObjectMean(List<BuildItem<E>> items) {
double sumX = 0.0;
return SplitResult.of(left, right); double sumY = 0.0;
} double sumZ = 0.0;
private static final class SplitResult<E> { for (BuildItem<E> item : items) {
final List<BuildItem<E>> left; sumX += item.centerX;
final List<BuildItem<E>> right; sumY += item.centerY;
sumZ += item.centerZ;
private SplitResult(List<BuildItem<E>> left, List<BuildItem<E>> right) { }
this.left = left;
this.right = right; double sx = sumX / items.size();
} double sy = sumY / items.size();
double sz = sumZ / items.size();
static <E> SplitResult<E> of(List<BuildItem<E>> left, List<BuildItem<E>> right) {
return new SplitResult<>(left, right); return bucketizeOctonary(items, sx, sy, sz, true);
} }
static <E> SplitResult<E> invalid() { private OctSplit<E> splitOctonarySpatialMedian(List<BuildItem<E>> items, AABB totalAabb) {
return new SplitResult<>(null, null); double sx = totalAabb.getCenterX();
} double sy = totalAabb.getCenterY();
double sz = totalAabb.getCenterZ();
boolean valid() {
return left != null && right != null && !left.isEmpty() && !right.isEmpty(); OctSplit<E> split = bucketizeOctonary(items, sx, sy, sz, false);
} if (!split.valid()) {
} return splitOctonaryObjectMean(items);
}
// --------------------------------------------------------------------- return split;
// Octonary / 8-ary splits }
// ---------------------------------------------------------------------
private OctSplit<E> splitOctonaryObjectMedian(List<BuildItem<E>> items) {
private OctSplit<E> splitOctonaryObjectMean(List<BuildItem<E>> items) { items.sort(Comparator.comparingDouble(it -> it.centerX));
double sumX = 0.0; double sx = medianValue(items, 0);
double sumY = 0.0;
double sumZ = 0.0; items.sort(Comparator.comparingDouble(it -> it.centerY));
double sy = medianValue(items, 1);
for (BuildItem<E> item : items) {
sumX += item.centerX; items.sort(Comparator.comparingDouble(it -> it.centerZ));
sumY += item.centerY; double sz = medianValue(items, 2);
sumZ += item.centerZ;
} return bucketizeOctonary(items, sx, sy, sz, true);
}
double sx = sumX / items.size();
double sy = sumY / items.size(); private double medianValue(List<BuildItem<E>> items, int axis) {
double sz = sumZ / items.size(); int n = items.size();
int mid = n / 2;
return bucketizeOctonary(items, sx, sy, sz, true);
} if ((n & 1) == 1) {
return items.get(mid).center(axis);
private OctSplit<E> splitOctonarySpatialMedian(List<BuildItem<E>> items, AABB totalAabb) { }
double sx = totalAabb.getCenterX(); return 0.5 * (items.get(mid - 1).center(axis) + items.get(mid).center(axis));
double sy = totalAabb.getCenterY(); }
double sz = totalAabb.getCenterZ();
@SuppressWarnings("unchecked")
OctSplit<E> split = bucketizeOctonary(items, sx, sy, sz, false); private OctSplit<E> bucketizeOctonary(List<BuildItem<E>> items, double sx, double sy, double sz,
if (!split.valid()) { boolean fallbackToMeanOnFailure) {
return splitOctonaryObjectMean(items); List<BuildItem<E>>[] buckets = new List[8];
} for (int i = 0; i < 8; i++) {
return split; buckets[i] = new ArrayList<>();
} }
private OctSplit<E> splitOctonaryObjectMedian(List<BuildItem<E>> items) { int nonEmptyCount = 0;
items.sort(Comparator.comparingDouble(it -> it.centerX)); boolean[] seen = new boolean[8];
double sx = medianValue(items, 0);
for (BuildItem<E> item : items) {
items.sort(Comparator.comparingDouble(it -> it.centerY)); int idx = octantIndex(item.centerX, item.centerY, item.centerZ, sx, sy, sz);
double sy = medianValue(items, 1); buckets[idx].add(item);
if (!seen[idx]) {
items.sort(Comparator.comparingDouble(it -> it.centerZ)); seen[idx] = true;
double sz = medianValue(items, 2); nonEmptyCount++;
}
return bucketizeOctonary(items, sx, sy, sz, true); }
}
if (nonEmptyCount <= 1) {
private double medianValue(List<BuildItem<E>> items, int axis) { if (fallbackToMeanOnFailure) {
int n = items.size(); // caller may already be object mean, so just mark invalid here
int mid = n / 2; return OctSplit.invalid();
}
if ((n & 1) == 1) { return OctSplit.invalid();
return items.get(mid).center(axis); }
}
return 0.5 * (items.get(mid - 1).center(axis) + items.get(mid).center(axis)); return OctSplit.of(buckets);
} }
@SuppressWarnings("unchecked") private int octantIndex(double x, double y, double z, double sx, double sy, double sz) {
private OctSplit<E> bucketizeOctonary(List<BuildItem<E>> items, double sx, double sy, double sz, boolean fallbackToMeanOnFailure) { int idx = 0;
List<BuildItem<E>>[] buckets = new List[8]; if (x >= sx)
for (int i = 0; i < 8; i++) { idx |= 1;
buckets[i] = new ArrayList<>(); if (y >= sy)
} idx |= 2;
if (z >= sz)
int nonEmptyCount = 0; idx |= 4;
boolean[] seen = new boolean[8]; return idx;
}
for (BuildItem<E> item : items) {
int idx = octantIndex(item.centerX, item.centerY, item.centerZ, sx, sy, sz); private static final class OctSplit<E> {
buckets[idx].add(item); final List<BuildItem<E>>[] buckets;
if (!seen[idx]) {
seen[idx] = true; private OctSplit(List<BuildItem<E>>[] buckets) {
nonEmptyCount++; this.buckets = buckets;
} }
}
static <E> OctSplit<E> of(List<BuildItem<E>>[] buckets) {
if (nonEmptyCount <= 1) { return new OctSplit<>(buckets);
if (fallbackToMeanOnFailure) { }
// caller may already be object mean, so just mark invalid here
return OctSplit.invalid(); static <E> OctSplit<E> invalid() {
} return new OctSplit<>(null);
return OctSplit.invalid(); }
}
boolean valid() {
return OctSplit.of(buckets); if (buckets == null) {
} return false;
}
private int octantIndex(double x, double y, double z, double sx, double sy, double sz) { int nonEmpty = 0;
int idx = 0; for (List<BuildItem<E>> bucket : buckets) {
if (x >= sx) idx |= 1; if (bucket != null && !bucket.isEmpty()) {
if (y >= sy) idx |= 2; nonEmpty++;
if (z >= sz) idx |= 4; }
return idx; }
} return nonEmpty > 1;
}
private static final class OctSplit<E> { }
final List<BuildItem<E>>[] buckets;
// ---------------------------------------------------------------------
private OctSplit(List<BuildItem<E>>[] buckets) { // helpers: to be continued with more heuristics
this.buckets = buckets; // ---------------------------------------------------------------------
}
private static int computeDefaultMaxDepth(int n) {
static <E> OctSplit<E> of(List<BuildItem<E>>[] buckets) { if (n <= 1) {
return new OctSplit<>(buckets); return DEFAULT_MIN_DEPTH;
} }
double log2n = Math.log(n) / Math.log(2.0);
@SuppressWarnings("unchecked") int depth = (int) Math.ceil(2.0 * log2n);
static <E> OctSplit<E> invalid() { return clamp(depth, DEFAULT_MIN_DEPTH, DEFAULT_MAX_DEPTH);
return new OctSplit<>(null); }
}
private static int clamp(int value, int min, int max) {
boolean valid() { return Math.max(min, Math.min(max, value));
if (buckets == null) { }
return false;
}
int nonEmpty = 0;
for (List<BuildItem<E>> bucket : buckets) {
if (bucket != null && !bucket.isEmpty()) {
nonEmpty++;
}
}
return nonEmpty > 1;
}
}
// ---------------------------------------------------------------------
// helpers: to be continued with more heuristics
// ---------------------------------------------------------------------
private static int computeDefaultMaxDepth(int n) {
if (n <= 1) {
return DEFAULT_MIN_DEPTH;
}
double log2n = Math.log(n) / Math.log(2.0);
int depth = (int) Math.ceil(2.0 * log2n);
return clamp(depth, DEFAULT_MIN_DEPTH, DEFAULT_MAX_DEPTH);
}
private static int clamp(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
} }
\ No newline at end of file
...@@ -88,8 +88,11 @@ public class SolidSelfIntCheckAABB extends Check { ...@@ -88,8 +88,11 @@ public class SolidSelfIntCheckAABB extends Check {
CheckResult cr; CheckResult cr;
// Build BVH on polygons, but compute AABBs from the *original* polygons // Build BVH on polygons, but compute AABBs from the *original* polygons
BoundingVolumeHierarchyTree<Polygon> tree = BoundingVolumeHierarchyTree<Polygon> tree =
new BoundingVolumeHierarchyTree<>(polys, p -> AABB.of(p.getOriginal())); new BoundingVolumeHierarchyTree.Builder<Polygon>()
.elements(polys)
.function(p -> AABB.of(p.getOriginal()))
.build();
// TODO: comparison with older version without tree // TODO: comparison with older version without tree
List<PolygonIntersection> intersections = List<PolygonIntersection> intersections =
SelfIntersectionUtil.calculateSolidSelfIntersection(g, 0.001, tree); SelfIntersectionUtil.calculateSolidSelfIntersection(g, 0.001, tree);
......
...@@ -84,7 +84,7 @@ public class NestedRingsCheck extends Check { ...@@ -84,7 +84,7 @@ public class NestedRingsCheck extends Check {
@Override @Override
public void check(Polygon p) { public void check(Polygon p) {
if (useAabbFilter) { if (p.getInnerRings().size() > 3) {
checkWithBoundingBoxFilter(p); checkWithBoundingBoxFilter(p);
} else { } else {
checkOriginal(p); checkOriginal(p);
......
...@@ -204,10 +204,8 @@ public class RingSelfIntCheck extends Check { ...@@ -204,10 +204,8 @@ public class RingSelfIntCheck extends Check {
} }
} }
BoundingVolumeHierarchyTree<Edge> edgeTree = new BoundingVolumeHierarchyTree<>( BoundingVolumeHierarchyTree<Edge> edgeTree = BoundingVolumeHierarchyTree.newBinary(
edges, edges, e -> AABB.of(e.getFrom(), e.getTo(), epsilon));
e -> AABB.of(e.getFrom(), e.getTo(), epsilon),
BoundingVolumeHierarchyTree.BuildConfig.binaryDefault());
for (int i = 0; i < edges.size(); i++) { for (int i = 0; i < edges.size(); i++) {
Edge e1 = edges.get(i); Edge e1 = edges.get(i);
...@@ -263,11 +261,9 @@ public class RingSelfIntCheck extends Check { ...@@ -263,11 +261,9 @@ public class RingSelfIntCheck extends Check {
} }
List<Edge> edges = getEdgesForRing(lr); List<Edge> edges = getEdgesForRing(lr);
BoundingVolumeHierarchyTree<Vertex> vertexTree = new BoundingVolumeHierarchyTree<>( BoundingVolumeHierarchyTree<Vertex> vertexTree =
vertices, BoundingVolumeHierarchyTree.newBinary(vertices, v -> AABB.of(v, epsilon));
v -> AABB.of(v, epsilon),
BoundingVolumeHierarchyTree.BuildConfig.binaryDefault());
for (Edge e : edges) { for (Edge e : edges) {
if (checkForPointsTouchingEdgeTree(lr, e, vertexTree)) { if (checkForPointsTouchingEdgeTree(lr, e, vertexTree)) {
...@@ -275,10 +271,8 @@ public class RingSelfIntCheck extends Check { ...@@ -275,10 +271,8 @@ public class RingSelfIntCheck extends Check {
} }
} }
BoundingVolumeHierarchyTree<Edge> edgeTree = new BoundingVolumeHierarchyTree<>( BoundingVolumeHierarchyTree<Edge> edgeTree = BoundingVolumeHierarchyTree.newBinary(
edges, edges, e -> AABB.of(e.getFrom(), e.getTo(), epsilon));
e -> AABB.of(e.getFrom(), e.getTo(), epsilon),
BoundingVolumeHierarchyTree.BuildConfig.binaryDefault());
for (int i = 0; i < edges.size(); i++) { for (int i = 0; i < edges.size(); i++) {
Edge e1 = edges.get(i); Edge e1 = edges.get(i);
......
...@@ -133,21 +133,18 @@ public class SelfIntersectionUtil { ...@@ -133,21 +133,18 @@ public class SelfIntersectionUtil {
public static List<PolygonIntersection> calculateSolidSelfIntersectionWithTree( public static List<PolygonIntersection> calculateSolidSelfIntersectionWithTree(
Geometry g, Geometry g,
double delta, double delta,
BoundingVolumeHierarchyTree.BuildConfig treeConfig) { BoundingVolumeHierarchyTree.Builder<Integer> treeConfig) {
List<TesselatedPolygon> tesselatedPolygons = tesselateAndFilter(g, delta); List<TesselatedPolygon> tesselatedPolygons = tesselateAndFilter(g, delta);
List<Integer> indices = new ArrayList<>(tesselatedPolygons.size()); List<Integer> indices = new ArrayList<>(tesselatedPolygons.size());
for (int i = 0; i < tesselatedPolygons.size(); i++) { for (int i = 0; i < tesselatedPolygons.size(); i++) {
indices.add(i); indices.add(i);
} }
treeConfig.elements(indices).function(index -> AABB.of(tesselatedPolygons.get(index).getOriginal()));
// Build BVH on polygon indices, while computing AABBs from the original polygons // Build BVH on polygon indices, while computing AABBs from the original polygons
BoundingVolumeHierarchyTree<Integer> tree = BoundingVolumeHierarchyTree<Integer> tree = treeConfig.build();
new BoundingVolumeHierarchyTree<>(
indices,
index -> AABB.of(tesselatedPolygons.get(index).getOriginal()),
treeConfig
);
List<PolygonIntersection> intersections = new ArrayList<>(); List<PolygonIntersection> intersections = new ArrayList<>();
...@@ -187,7 +184,7 @@ public class SelfIntersectionUtil { ...@@ -187,7 +184,7 @@ public class SelfIntersectionUtil {
return calculateSolidSelfIntersectionWithTree( return calculateSolidSelfIntersectionWithTree(
g, g,
delta, delta,
BoundingVolumeHierarchyTree.BuildConfig.binaryDefault() new BoundingVolumeHierarchyTree.Builder<Integer>().binaryDefault()
); );
} }
......
package de.hft.stuttgart.citydoctor2.checks.util; package de.hft.stuttgart.citydoctor2.checks.util;
import static org.junit.Assert.*; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.util.List; import java.util.List;
import org.junit.Test; import org.citygml4j.core.model.CityGMLVersion;
import org.citygml4j.core.model.core.CityModel;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration; import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration;
import de.hft.stuttgart.citydoctor2.database.UnconnectedCache;
import de.hft.stuttgart.citydoctor2.datastructure.Building; import de.hft.stuttgart.citydoctor2.datastructure.Building;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel; import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry; import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
...@@ -15,13 +20,30 @@ import de.hft.stuttgart.citydoctor2.datastructure.Lod; ...@@ -15,13 +20,30 @@ import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon; import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB; import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree; import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree;
import de.hft.stuttgart.citydoctor2.exceptions.CityDoctorWriteException;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParseException; import de.hft.stuttgart.citydoctor2.parser.CityGmlParseException;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParser; import de.hft.stuttgart.citydoctor2.parser.CityGmlParser;
import de.hft.stuttgart.citydoctor2.parser.InvalidGmlFileException; import de.hft.stuttgart.citydoctor2.parser.InvalidGmlFileException;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.utils.PolygonIntersection; import de.hft.stuttgart.citydoctor2.utils.PolygonIntersection;
public class SolidSelfIntersectionBVHUtilTest { public class SolidSelfIntersectionBVHUtilTest {
@Test
public void testWriteModel() throws CityDoctorWriteException {
Building b = new Building();
b.addGeometry(GeometryTestUtils.createGoodGeometry());
b.setGmlObject(new org.citygml4j.core.model.building.Building());
UnconnectedCache unconnectedCache = new UnconnectedCache();
CityDoctorModel model = new CityDoctorModel(
new ParserConfiguration(8, false), new File("test.gml"), unconnectedCache);
model.setParsedCityGMLVersion(CityGMLVersion.v2_0);
model.setCityModel(new CityModel());
model.addBuilding(b);
model.saveAs("test.gml", false);
}
@Test @Test
public void testBVHCalculateOnKnownGoodModel() throws CityGmlParseException, InvalidGmlFileException { public void testBVHCalculateOnKnownGoodModel() throws CityGmlParseException, InvalidGmlFileException {
...@@ -42,7 +64,7 @@ public class SolidSelfIntersectionBVHUtilTest { ...@@ -42,7 +64,7 @@ public class SolidSelfIntersectionBVHUtilTest {
assertTrue("Expected at least 2 polygons", polys.size() > 1); assertTrue("Expected at least 2 polygons", polys.size() > 1);
BoundingVolumeHierarchyTree<Polygon> tree = BoundingVolumeHierarchyTree<Polygon> tree =
new BoundingVolumeHierarchyTree<>(polys, p -> AABB.of(p.getOriginal())); BoundingVolumeHierarchyTree.newBinary(polys, p -> AABB.of(p.getOriginal()));
double delta = 0.001; double delta = 0.001;
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment