Commit e6f4979d authored by Numanoglu's avatar Numanoglu
Browse files

Add BVH variants and validation tests

parent 54c56f2d
package de.hft.stuttgart.citydoctor2.datastructure.bht;
import java.util.List;
final class BinarySplitResult<E> {
final List<BvhBuildItem<E>> left;
final List<BvhBuildItem<E>> right;
private BinarySplitResult(List<BvhBuildItem<E>> left, List<BvhBuildItem<E>> right) {
this.left = left;
this.right = right;
}
static <E> BinarySplitResult<E> of(List<BvhBuildItem<E>> left, List<BvhBuildItem<E>> right) {
return new BinarySplitResult<>(left, right);
}
static <E> BinarySplitResult<E> invalid() {
return new BinarySplitResult<>(null, null);
}
boolean valid() {
return left != null && right != null && !left.isEmpty() && !right.isEmpty();
}
}
package de.hft.stuttgart.citydoctor2.datastructure.bht;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
final class BinarySplitters {
private BinarySplitters() {
}
static <E> BinarySplitResult<E> objectMedian(List<BvhBuildItem<E>> items, int axis) {
items.sort(Comparator.comparingDouble(it -> it.center(axis)));
int mid = items.size() / 2;
if (mid <= 0 || mid >= items.size()) {
return BinarySplitResult.invalid();
}
return BinarySplitResult.of(items.subList(0, mid), items.subList(mid, items.size()));
}
static <E> BinarySplitResult<E> objectMean(List<BvhBuildItem<E>> items, int axis) {
double sum = 0.0;
for (BvhBuildItem<E> item : items) {
sum += item.center(axis);
}
double splitValue = sum / items.size();
List<BvhBuildItem<E>> left = new ArrayList<>();
List<BvhBuildItem<E>> right = new ArrayList<>();
for (BvhBuildItem<E> item : items) {
if (item.center(axis) < splitValue) {
left.add(item);
} else {
right.add(item);
}
}
if (left.isEmpty() || right.isEmpty()) {
return objectMedian(items, axis);
}
return BinarySplitResult.of(left, right);
}
static <E> BinarySplitResult<E> spatialMedian(List<BvhBuildItem<E>> items, int axis, AABB totalAabb) {
double splitValue;
switch (axis) {
case 0:
splitValue = totalAabb.getCenterX();
break;
case 1:
splitValue = totalAabb.getCenterY();
break;
case 2:
splitValue = totalAabb.getCenterZ();
break;
default:
throw new IllegalArgumentException("axis must be 0, 1, or 2");
}
List<BvhBuildItem<E>> left = new ArrayList<>();
List<BvhBuildItem<E>> right = new ArrayList<>();
for (BvhBuildItem<E> item : items) {
if (item.center(axis) < splitValue) {
left.add(item);
} else {
right.add(item);
}
}
if (left.isEmpty() || right.isEmpty()) {
return objectMean(items, axis);
}
return BinarySplitResult.of(left, right);
}
}
package de.hft.stuttgart.citydoctor2.datastructure.bht;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
final class BoundingVolumeHierarchyBuilder<E> {
private final BoundingVolumeHierarchyTree.Builder<E> config;
private final Function<E, AABB> aabbFunction;
BoundingVolumeHierarchyBuilder(BoundingVolumeHierarchyTree.Builder<E> config) {
this.config = Objects.requireNonNull(config, "config");
this.aabbFunction = Objects.requireNonNull(config.getAabbFunction(), "aabbFunction");
}
Node<E> buildRoot() {
List<E> elements = Objects.requireNonNull(config.getElements(), "elements");
if (elements.isEmpty()) {
return null;
}
List<BvhBuildItem<E>> items = toBuildItems(elements);
SplitStrategy resolvedStrategy = resolveSplitStrategy();
return config.getDegree() == 2
? buildBinaryRecursive(items, 0, resolvedStrategy)
: buildOctonaryRecursive(items, 0, resolvedStrategy);
}
private List<BvhBuildItem<E>> toBuildItems(List<E> elements) {
List<BvhBuildItem<E>> items = new ArrayList<>(elements.size());
for (E e : elements) {
AABB aabb = Objects.requireNonNull(aabbFunction.apply(e), "aabbFunction returned null");
items.add(new BvhBuildItem<>(e, aabb));
}
return items;
}
private SplitStrategy resolveSplitStrategy() {
if (config.getSplitStrategy() != SplitStrategy.AUTO) {
validateStrategyMatchesDegree(config.getDegree(), config.getSplitStrategy());
return config.getSplitStrategy();
}
return config.getDegree() == 2
? SplitStrategy.BINARY_SPATIAL_MEDIAN
: SplitStrategy.OCTONARY_OBJECT_MEAN;
}
private void validateStrategyMatchesDegree(int degree, SplitStrategy strategy) {
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;
if (degree == 2 && !binary) {
throw new IllegalArgumentException("Strategy " + strategy + " does not match degree 2.");
}
if (degree == 8 && !octonary) {
throw new IllegalArgumentException("Strategy " + strategy + " does not match degree 8.");
}
}
private Node<E> buildBinaryRecursive(
List<BvhBuildItem<E>> items,
int depth,
SplitStrategy strategy) {
AABB totalAabb = getAggregateAABBFromItems(items);
if (shouldStop(items, depth, totalAabb)) {
return packTerminalNode(items, totalAabb);
}
int axis = totalAabb.findLongestAxis();
BinarySplitResult<E> split;
switch (strategy) {
case BINARY_OBJECT_MEDIAN:
split = BinarySplitters.objectMedian(items, axis);
break;
case BINARY_OBJECT_MEAN:
split = BinarySplitters.objectMean(items, axis);
break;
case BINARY_SPATIAL_MEDIAN:
split = BinarySplitters.spatialMedian(items, axis, totalAabb);
break;
default:
throw new IllegalStateException("Unexpected binary strategy: " + strategy);
}
if (!split.valid()) {
return packTerminalNode(items, totalAabb);
}
Node<E> node = new Node<>(null, totalAabb);
node.getChildren().add(buildBinaryRecursive(split.left, depth + 1, strategy));
node.getChildren().add(buildBinaryRecursive(split.right, depth + 1, strategy));
return node;
}
private Node<E> buildOctonaryRecursive(
List<BvhBuildItem<E>> items,
int depth,
SplitStrategy strategy) {
AABB totalAabb = getAggregateAABBFromItems(items);
if (shouldStop(items, depth, totalAabb)) {
return packTerminalNode(items, totalAabb);
}
OctonarySplitResult<E> split;
switch (strategy) {
case OCTONARY_OBJECT_MEDIAN:
split = OctonarySplitters.objectMedian(items);
break;
case OCTONARY_OBJECT_MEAN:
split = OctonarySplitters.objectMean(items);
break;
case OCTONARY_SPATIAL_MEDIAN:
split = OctonarySplitters.spatialMedian(items, totalAabb);
break;
default:
throw new IllegalStateException("Unexpected octonary strategy: " + strategy);
}
if (!split.valid()) {
return packTerminalNode(items, totalAabb);
}
Node<E> node = new Node<>(null, totalAabb);
for (int i = 0; i < 8; i++) {
List<BvhBuildItem<E>> bucket = split.buckets[i];
if (bucket.isEmpty()) {
continue;
}
node.getChildren().add(buildOctonaryRecursive(bucket, depth + 1, strategy));
}
return node;
}
private boolean shouldStop(List<BvhBuildItem<E>> items, int depth, AABB totalAabb) {
return items.size() <= config.getMaxLeafSize()
|| depth >= config.getMaxDepth()
|| totalAabb.isDegenerate(config.getDegenerateTolerance());
}
private Node<E> packTerminalNode(List<BvhBuildItem<E>> items, AABB totalAabb) {
if (items.size() == 1) {
BvhBuildItem<E> item = items.get(0);
return new Node<>(item.element, item.aabb);
}
Node<E> leafGroup = new Node<>(null, totalAabb);
for (BvhBuildItem<E> item : items) {
leafGroup.getChildren().add(new Node<>(item.element, item.aabb));
}
return leafGroup;
}
private static <E> AABB getAggregateAABBFromItems(List<BvhBuildItem<E>> items) {
double minX = Double.POSITIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY;
double minZ = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
double maxZ = Double.NEGATIVE_INFINITY;
for (BvhBuildItem<E> item : items) {
AABB aabb = item.aabb;
minX = Math.min(minX, aabb.getMinX());
minY = Math.min(minY, aabb.getMinY());
minZ = Math.min(minZ, aabb.getMinZ());
maxX = Math.max(maxX, aabb.getMaxX());
maxY = Math.max(maxY, aabb.getMaxY());
maxZ = Math.max(maxZ, aabb.getMaxZ());
}
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
}
static int computeDefaultMaxDepth(int n) {
if (n <= 1) {
return BoundingVolumeHierarchyTree.DEFAULT_MIN_DEPTH;
}
double log2n = Math.log(n) / Math.log(2.0);
int depth = (int) Math.ceil(2.0 * log2n);
return clamp(
depth,
BoundingVolumeHierarchyTree.DEFAULT_MIN_DEPTH,
BoundingVolumeHierarchyTree.DEFAULT_MAX_DEPTH);
}
private static int clamp(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
}
package de.hft.stuttgart.citydoctor2.datastructure.bht;
final class BvhBuildItem<E> {
final E element;
final AABB aabb;
final double centerX;
final double centerY;
final double centerZ;
BvhBuildItem(E element, AABB aabb) {
this.element = element;
this.aabb = aabb;
this.centerX = 0.5 * (aabb.getMinX() + aabb.getMaxX());
this.centerY = 0.5 * (aabb.getMinY() + aabb.getMaxY());
this.centerZ = 0.5 * (aabb.getMinZ() + aabb.getMaxZ());
}
double center(int axis) {
switch (axis) {
case 0:
return centerX;
case 1:
return centerY;
case 2:
return centerZ;
default:
throw new IllegalArgumentException("axis must be 0, 1, or 2");
}
}
}
package de.hft.stuttgart.citydoctor2.datastructure.bht;
import java.util.List;
final class OctonarySplitResult<E> {
final List<BvhBuildItem<E>>[] buckets;
private OctonarySplitResult(List<BvhBuildItem<E>>[] buckets) {
this.buckets = buckets;
}
static <E> OctonarySplitResult<E> of(List<BvhBuildItem<E>>[] buckets) {
return new OctonarySplitResult<>(buckets);
}
static <E> OctonarySplitResult<E> invalid() {
return new OctonarySplitResult<>(null);
}
boolean valid() {
if (buckets == null) {
return false;
}
int nonEmpty = 0;
for (List<BvhBuildItem<E>> bucket : buckets) {
if (bucket != null && !bucket.isEmpty()) {
nonEmpty++;
}
}
return nonEmpty > 1;
}
}
package de.hft.stuttgart.citydoctor2.datastructure.bht;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
final class OctonarySplitters {
private OctonarySplitters() {
}
static <E> OctonarySplitResult<E> objectMean(List<BvhBuildItem<E>> items) {
double sumX = 0.0;
double sumY = 0.0;
double sumZ = 0.0;
for (BvhBuildItem<E> item : items) {
sumX += item.centerX;
sumY += item.centerY;
sumZ += item.centerZ;
}
double sx = sumX / items.size();
double sy = sumY / items.size();
double sz = sumZ / items.size();
return bucketize(items, sx, sy, sz);
}
static <E> OctonarySplitResult<E> spatialMedian(List<BvhBuildItem<E>> items, AABB totalAabb) {
double sx = totalAabb.getCenterX();
double sy = totalAabb.getCenterY();
double sz = totalAabb.getCenterZ();
OctonarySplitResult<E> split = bucketize(items, sx, sy, sz);
if (!split.valid()) {
return objectMean(items);
}
return split;
}
static <E> OctonarySplitResult<E> objectMedian(List<BvhBuildItem<E>> items) {
items.sort(Comparator.comparingDouble(it -> it.centerX));
double sx = medianValue(items, 0);
items.sort(Comparator.comparingDouble(it -> it.centerY));
double sy = medianValue(items, 1);
items.sort(Comparator.comparingDouble(it -> it.centerZ));
double sz = medianValue(items, 2);
OctonarySplitResult<E> split = bucketize(items, sx, sy, sz);
if (!split.valid()) {
return objectMean(items);
}
return split;
}
private static <E> double medianValue(List<BvhBuildItem<E>> items, int axis) {
int n = items.size();
int mid = n / 2;
if ((n & 1) == 1) {
return items.get(mid).center(axis);
}
return 0.5 * (items.get(mid - 1).center(axis) + items.get(mid).center(axis));
}
@SuppressWarnings("unchecked")
private static <E> OctonarySplitResult<E> bucketize(List<BvhBuildItem<E>> items, double sx, double sy, double sz) {
List<BvhBuildItem<E>>[] buckets = new List[8];
for (int i = 0; i < 8; i++) {
buckets[i] = new ArrayList<>();
}
int nonEmptyCount = 0;
boolean[] seen = new boolean[8];
for (BvhBuildItem<E> item : items) {
int idx = octantIndex(item.centerX, item.centerY, item.centerZ, sx, sy, sz);
buckets[idx].add(item);
if (!seen[idx]) {
seen[idx] = true;
nonEmptyCount++;
}
}
if (nonEmptyCount <= 1) {
return OctonarySplitResult.invalid();
}
return OctonarySplitResult.of(buckets);
}
private static int octantIndex(double x, double y, double z, double sx, double sy, double sz) {
int idx = 0;
if (x >= sx) {
idx |= 1;
}
if (y >= sy) {
idx |= 2;
}
if (z >= sz) {
idx |= 4;
}
return idx;
}
}
package de.hft.stuttgart.citydoctor2.datastructure.bht;
public enum SplitStrategy {
AUTO,
BINARY_OBJECT_MEDIAN,
BINARY_OBJECT_MEAN,
BINARY_SPATIAL_MEDIAN,
OCTONARY_OBJECT_MEDIAN,
OCTONARY_OBJECT_MEAN,
OCTONARY_SPATIAL_MEDIAN
}
package de.hft.stuttgart.citydoctor2.datastructure.bht;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.Test;
public class BoundingVolumeHierarchyTreeVariantsTest {
@Test
public void allConcreteSplitStrategiesReturnSameCandidatesAsBruteForce() {
List<TestBox> boxes = createSyntheticBoxes();
List<AABB> queries = createQueries();
for (SplitStrategy strategy : concreteStrategies()) {
BoundingVolumeHierarchyTree<TestBox> tree =
BoundingVolumeHierarchyTree.newWithStrategy(boxes, box -> box.aabb, strategy);
assertTrue("Expected root for " + strategy, tree.getRoot() != null);
for (AABB query : queries) {
Set<String> expected = bruteForceCandidates(boxes, query);
Set<String> actual = names(tree.getAllIntersectingElements(query));
assertFalse("Synthetic query should hit at least one box for " + strategy, actual.isEmpty());
assertEquals("Candidate set differs for " + strategy, expected, actual);
}
}
}
@Test(expected = IllegalArgumentException.class)
public void autoStrategyIsNotAConcreteFactoryVariant() {
BoundingVolumeHierarchyTree.newWithStrategy(createSyntheticBoxes(), box -> box.aabb, SplitStrategy.AUTO);
}
private static SplitStrategy[] concreteStrategies() {
return new SplitStrategy[] {
SplitStrategy.BINARY_OBJECT_MEDIAN,
SplitStrategy.BINARY_OBJECT_MEAN,
SplitStrategy.BINARY_SPATIAL_MEDIAN,
SplitStrategy.OCTONARY_OBJECT_MEDIAN,
SplitStrategy.OCTONARY_OBJECT_MEAN,
SplitStrategy.OCTONARY_SPATIAL_MEDIAN
};
}
private static List<TestBox> createSyntheticBoxes() {
List<TestBox> boxes = new ArrayList<>();
boxes.add(new TestBox("a", new AABB(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)));
boxes.add(new TestBox("b", new AABB(0.8, 0.8, 0.0, 1.8, 1.8, 1.0)));
boxes.add(new TestBox("c", new AABB(3.0, 0.0, 0.0, 4.0, 1.0, 1.0)));
boxes.add(new TestBox("d", new AABB(0.0, 3.0, 0.0, 1.0, 4.0, 1.0)));
boxes.add(new TestBox("e", new AABB(3.0, 3.0, 0.0, 4.0, 4.0, 1.0)));
boxes.add(new TestBox("f", new AABB(6.0, 6.0, 0.0, 7.0, 7.0, 1.0)));
return boxes;
}
private static List<AABB> createQueries() {
List<AABB> queries = new ArrayList<>();
queries.add(new AABB(0.5, 0.5, 0.0, 1.2, 1.2, 1.0));
queries.add(new AABB(2.9, 2.9, 0.0, 4.1, 4.1, 1.0));
queries.add(new AABB(5.5, 5.5, 0.0, 7.5, 7.5, 1.0));
return queries;
}
private static Set<String> bruteForceCandidates(List<TestBox> boxes, AABB query) {
Set<String> candidates = new HashSet<>();
for (TestBox box : boxes) {
if (box.aabb.overlaps(query)) {
candidates.add(box.name);
}
}
return candidates;
}
private static Set<String> names(List<TestBox> boxes) {
Set<String> names = new HashSet<>();
for (TestBox box : boxes) {
names.add(box.name);
}
return names;
}
private static final class TestBox {
final String name;
final AABB aabb;
TestBox(String name, AABB aabb) {
this.name = name;
this.aabb = aabb;
}
}
}
...@@ -89,9 +89,9 @@ public class SolidSelfIntCheckAABB extends Check { ...@@ -89,9 +89,9 @@ public class SolidSelfIntCheckAABB extends Check {
// 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.Builder<Polygon>() BoundingVolumeHierarchyTree.<Polygon>binaryBuilder()
.elements(polys) .elements(polys)
.function(p -> AABB.of(p.getOriginal())) .aabbFunction(p -> AABB.of(p.getOriginal()))
.build(); .build();
// TODO: comparison with older version without tree // TODO: comparison with older version without tree
List<PolygonIntersection> intersections = List<PolygonIntersection> intersections =
......
...@@ -20,7 +20,10 @@ package de.hft.stuttgart.citydoctor2.checks.geometry; ...@@ -20,7 +20,10 @@ package de.hft.stuttgart.citydoctor2.checks.geometry;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import de.hft.stuttgart.citydoctor2.check.Check; import de.hft.stuttgart.citydoctor2.check.Check;
...@@ -36,6 +39,8 @@ import de.hft.stuttgart.citydoctor2.datastructure.LinearRing; ...@@ -36,6 +39,8 @@ import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon; import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex; import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
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.SplitStrategy;
/** /**
* Checks whether a inner ring is completely contained in another inner ring * Checks whether a inner ring is completely contained in another inner ring
...@@ -59,32 +64,77 @@ public class NestedRingsCheck extends Check { ...@@ -59,32 +64,77 @@ public class NestedRingsCheck extends Check {
dependencies = Collections.unmodifiableList(deps); dependencies = Collections.unmodifiableList(deps);
} }
/** public enum Variant {
* FilterSwitch @Numanoglu AUTO,
* true -> use AABB prefilter OLD,
* false -> use original double-loop exact version AABB_FILTER,
*/ BVH_BINARY_OBJECT_MEDIAN(SplitStrategy.BINARY_OBJECT_MEDIAN),
private boolean useAabbFilter = true; BVH_BINARY_OBJECT_MEAN(SplitStrategy.BINARY_OBJECT_MEAN),
BVH_BINARY_SPATIAL_MEDIAN(SplitStrategy.BINARY_SPATIAL_MEDIAN),
BVH_OCTONARY_OBJECT_MEDIAN(SplitStrategy.OCTONARY_OBJECT_MEDIAN),
BVH_OCTONARY_OBJECT_MEAN(SplitStrategy.OCTONARY_OBJECT_MEAN),
BVH_OCTONARY_SPATIAL_MEDIAN(SplitStrategy.OCTONARY_SPATIAL_MEDIAN);
private final SplitStrategy splitStrategy;
Variant() {
this.splitStrategy = null;
}
Variant(SplitStrategy splitStrategy) {
this.splitStrategy = splitStrategy;
}
public boolean isBvh() {
return splitStrategy != null;
}
public SplitStrategy getSplitStrategy() {
if (splitStrategy == null) {
throw new IllegalStateException("Variant " + this + " has no BVH split strategy.");
}
return splitStrategy;
}
}
private Variant variant = Variant.AUTO;
public NestedRingsCheck() { public NestedRingsCheck() {
} }
public NestedRingsCheck(boolean useAabbFilter) { public NestedRingsCheck(boolean useAabbFilter) {
this.useAabbFilter = useAabbFilter; this.variant = useAabbFilter ? Variant.AABB_FILTER : Variant.OLD;
}
public NestedRingsCheck(Variant variant) {
this.variant = Objects.requireNonNull(variant, "variant");
} }
public void setUseAabbFilter(boolean useAabbFilter) { public void setUseAabbFilter(boolean useAabbFilter) {
this.useAabbFilter = useAabbFilter; this.variant = useAabbFilter ? Variant.AABB_FILTER : Variant.OLD;
} }
public boolean isUseAabbFilter() { public boolean isUseAabbFilter() {
return useAabbFilter; return variant != Variant.OLD;
}
public void setVariant(Variant variant) {
this.variant = Objects.requireNonNull(variant, "variant");
}
public Variant getVariant() {
return variant;
} }
//
@Override @Override
public void check(Polygon p) { public void check(Polygon p) {
if (p.getInnerRings().size() > 3) { if (variant == Variant.OLD) {
checkOriginal(p);
} else if (variant == Variant.AABB_FILTER) {
checkWithBoundingBoxFilter(p);
} else if (variant.isBvh()) {
checkWithBvhFilter(p, variant.getSplitStrategy());
} else if (p.getInnerRings().size() > 3) {
checkWithBoundingBoxFilter(p); checkWithBoundingBoxFilter(p);
} else { } else {
checkOriginal(p); checkOriginal(p);
...@@ -112,6 +162,46 @@ public class NestedRingsCheck extends Check { ...@@ -112,6 +162,46 @@ public class NestedRingsCheck extends Check {
p.addCheckResult(cr); p.addCheckResult(cr);
} }
public void checkWithBvhFilter(Polygon p, SplitStrategy splitStrategy) {
Objects.requireNonNull(splitStrategy, "splitStrategy");
List<LinearRing> innerRings = p.getInnerRings();
if (innerRings == null || innerRings.size() < 2) {
CheckResult cr = new CheckResult(this, ResultStatus.OK, null);
p.addCheckResult(cr);
return;
}
Map<LinearRing, AABB> ringBoxes = new HashMap<>(innerRings.size());
for (LinearRing ring : innerRings) {
ringBoxes.put(ring, AABB.of(ring));
}
BoundingVolumeHierarchyTree<LinearRing> ringTree =
BoundingVolumeHierarchyTree.newWithStrategy(innerRings, ringBoxes::get, splitStrategy);
for (LinearRing interiorRing : innerRings) {
AABB interiorBox = ringBoxes.get(interiorRing);
List<LinearRing> candidates = ringTree.getAllElementsContainedIn(interiorBox);
for (LinearRing checkRing : candidates) {
if (checkRing == interiorRing) {
continue;
}
if (areAllPointsInside(interiorRing, checkRing)) {
CheckError err = new NestedRingError(p, interiorRing, checkRing);
CheckResult cr = new CheckResult(this, ResultStatus.ERROR, err);
p.addCheckResult(cr);
return;
}
}
}
CheckResult cr = new CheckResult(this, ResultStatus.OK, null);
p.addCheckResult(cr);
}
/** /**
* Alternative implementation using cached AABBs as broad-phase filter. * Alternative implementation using cached AABBs as broad-phase filter.
* Exact geometry check is still done via areAllPointsInside(...). * Exact geometry check is still done via areAllPointsInside(...).
......
...@@ -43,6 +43,7 @@ import de.hft.stuttgart.citydoctor2.datastructure.LinearRing; ...@@ -43,6 +43,7 @@ import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex; import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
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.datastructure.bht.SplitStrategy;
import de.hft.stuttgart.citydoctor2.math.CovarianceMatrix; import de.hft.stuttgart.citydoctor2.math.CovarianceMatrix;
import de.hft.stuttgart.citydoctor2.math.DistanceResult; import de.hft.stuttgart.citydoctor2.math.DistanceResult;
import de.hft.stuttgart.citydoctor2.math.Matrix3x3d; import de.hft.stuttgart.citydoctor2.math.Matrix3x3d;
...@@ -64,8 +65,8 @@ import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration; ...@@ -64,8 +65,8 @@ import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
* Variants: * Variants:
* - OLD: only BoundingBox replaced by AABB, logic otherwise unchanged * - OLD: only BoundingBox replaced by AABB, logic otherwise unchanged
* (the most efficient for little models) * (the most efficient for little models)
* - TREE_1_EDGE_BVH: edge-edge broad phase via BVH *
* - TREE_2_EDGE_AND_VERTEX_BVH: edge-edge via edge BVH, point-edge via vertex BVH * - BVH_*: edge-edge and point-edge broad phase via BVH with the named split strategy
* *
* @author Baris Numanoglu * @author Baris Numanoglu
*/ */
...@@ -89,8 +90,33 @@ public class RingSelfIntCheck extends Check { ...@@ -89,8 +90,33 @@ public class RingSelfIntCheck extends Check {
public enum Variant { public enum Variant {
OLD, OLD,
TREE_1_EDGE_BVH, BVH_BINARY_OBJECT_MEDIAN(SplitStrategy.BINARY_OBJECT_MEDIAN),
TREE_2_EDGE_AND_VERTEX_BVH BVH_BINARY_OBJECT_MEAN(SplitStrategy.BINARY_OBJECT_MEAN),
BVH_BINARY_SPATIAL_MEDIAN(SplitStrategy.BINARY_SPATIAL_MEDIAN),
BVH_OCTONARY_OBJECT_MEDIAN(SplitStrategy.OCTONARY_OBJECT_MEDIAN),
BVH_OCTONARY_OBJECT_MEAN(SplitStrategy.OCTONARY_OBJECT_MEAN),
BVH_OCTONARY_SPATIAL_MEDIAN(SplitStrategy.OCTONARY_SPATIAL_MEDIAN);
private final SplitStrategy splitStrategy;
Variant() {
this.splitStrategy = null;
}
Variant(SplitStrategy splitStrategy) {
this.splitStrategy = splitStrategy;
}
public boolean isBvh() {
return splitStrategy != null;
}
public SplitStrategy getSplitStrategy() {
if (splitStrategy == null) {
throw new IllegalStateException("Variant " + this + " has no BVH split strategy.");
}
return splitStrategy;
}
} }
private Variant variant = Variant.OLD; private Variant variant = Variant.OLD;
...@@ -123,20 +149,11 @@ public class RingSelfIntCheck extends Check { ...@@ -123,20 +149,11 @@ public class RingSelfIntCheck extends Check {
@Override @Override
public void check(LinearRing lr) { public void check(LinearRing lr) {
switch (variant) { if (variant == Variant.OLD) {
case OLD:
checkRingOld(lr);
break;
case TREE_1_EDGE_BVH:
checkRingTree1(lr);
break;
case TREE_2_EDGE_AND_VERTEX_BVH:
checkRingTree2(lr);
break;
default:
checkRingOld(lr); checkRingOld(lr);
break; return;
} }
checkRingBvh(lr, variant.getSplitStrategy());
} }
/** /**
...@@ -184,75 +201,11 @@ public class RingSelfIntCheck extends Check { ...@@ -184,75 +201,11 @@ public class RingSelfIntCheck extends Check {
} }
/** /**
* TREE_1: * BVH:
* - point-edge remains old
* - edge-edge uses Edge-BVH
*/
private void checkRingTree1(LinearRing lr) {
List<Vertex> vertices = lr.getVertices();
Vector3d centroid = CovarianceMatrix.getCentroid(vertices);
EigenvalueDecomposition ed = OrthogonalRegressionPlane.decompose(vertices, centroid);
if (checkEigenvalues(lr, vertices, ed)) {
return;
}
List<Edge> edges = getEdgesForRing(lr);
for (Edge e : edges) {
if (checkForPointsTouchingEdgeOld(lr, e)) {
return;
}
}
BoundingVolumeHierarchyTree<Edge> edgeTree = BoundingVolumeHierarchyTree.newBinary(
edges, e -> AABB.of(e.getFrom(), e.getTo(), epsilon));
for (int i = 0; i < edges.size(); i++) {
Edge e1 = edges.get(i);
AABB q = AABB.of(e1.getFrom(), e1.getTo(), epsilon);
List<Edge> candidates = edgeTree.getAllIntersectingElements(q);
if (candidates.isEmpty()) {
continue;
}
Segment3d s1 = new Segment3d(e1.getFrom(), e1.getTo());
for (Edge e2 : candidates) {
if (e1 == e2) {
continue;
}
if (e1.getConnectionPoint(e2) != null) {
continue;
}
// avoid double pairwise checks
int j = edges.indexOf(e2);
if (j <= i) {
continue;
}
Segment3d s2 = new Segment3d(e2.getFrom(), e2.getTo());
DistanceResult dr = s1.getDistanceResult(s2);
if (dr.distance() < epsilon) {
CheckError err = new RingEdgeIntersectionError(lr, e1, e2, dr.point1());
CheckResult cr = new CheckResult(this, ResultStatus.ERROR, err);
lr.addCheckResult(cr);
return;
}
}
}
CheckResult cr = new CheckResult(this, ResultStatus.OK, null);
lr.addCheckResult(cr);
}
/**
* TREE_2:
* - point-edge uses Vertex-BVH * - point-edge uses Vertex-BVH
* - edge-edge uses Edge-BVH * - edge-edge uses Edge-BVH
*/ */
private void checkRingTree2(LinearRing lr) { private void checkRingBvh(LinearRing lr, SplitStrategy splitStrategy) {
List<Vertex> vertices = lr.getVertices(); List<Vertex> vertices = lr.getVertices();
Vector3d centroid = CovarianceMatrix.getCentroid(vertices); Vector3d centroid = CovarianceMatrix.getCentroid(vertices);
EigenvalueDecomposition ed = OrthogonalRegressionPlane.decompose(vertices, centroid); EigenvalueDecomposition ed = OrthogonalRegressionPlane.decompose(vertices, centroid);
...@@ -263,7 +216,7 @@ public class RingSelfIntCheck extends Check { ...@@ -263,7 +216,7 @@ public class RingSelfIntCheck extends Check {
List<Edge> edges = getEdgesForRing(lr); List<Edge> edges = getEdgesForRing(lr);
BoundingVolumeHierarchyTree<Vertex> vertexTree = BoundingVolumeHierarchyTree<Vertex> vertexTree =
BoundingVolumeHierarchyTree.newBinary(vertices, v -> AABB.of(v, epsilon)); BoundingVolumeHierarchyTree.newWithStrategy(vertices, v -> AABB.of(v, epsilon), splitStrategy);
for (Edge e : edges) { for (Edge e : edges) {
if (checkForPointsTouchingEdgeTree(lr, e, vertexTree)) { if (checkForPointsTouchingEdgeTree(lr, e, vertexTree)) {
...@@ -271,8 +224,8 @@ public class RingSelfIntCheck extends Check { ...@@ -271,8 +224,8 @@ public class RingSelfIntCheck extends Check {
} }
} }
BoundingVolumeHierarchyTree<Edge> edgeTree = BoundingVolumeHierarchyTree.newBinary( BoundingVolumeHierarchyTree<Edge> edgeTree = BoundingVolumeHierarchyTree.newWithStrategy(
edges, e -> AABB.of(e.getFrom(), e.getTo(), epsilon)); edges, e -> AABB.of(e.getFrom(), e.getTo(), epsilon), splitStrategy);
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);
......
...@@ -141,7 +141,7 @@ public class SelfIntersectionUtil { ...@@ -141,7 +141,7 @@ public class SelfIntersectionUtil {
indices.add(i); indices.add(i);
} }
treeConfig.elements(indices).function(index -> AABB.of(tesselatedPolygons.get(index).getOriginal())); treeConfig.elements(indices).aabbFunction(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 = treeConfig.build(); BoundingVolumeHierarchyTree<Integer> tree = treeConfig.build();
...@@ -184,7 +184,7 @@ public class SelfIntersectionUtil { ...@@ -184,7 +184,7 @@ public class SelfIntersectionUtil {
return calculateSolidSelfIntersectionWithTree( return calculateSolidSelfIntersectionWithTree(
g, g,
delta, delta,
new BoundingVolumeHierarchyTree.Builder<Integer>().binaryDefault() BoundingVolumeHierarchyTree.<Integer>binaryBuilder()
); );
} }
......
...@@ -135,7 +135,6 @@ public class GeometryChecksWithAABBTest { ...@@ -135,7 +135,6 @@ public class GeometryChecksWithAABBTest {
)); ));
p2.setExteriorRing(r2); p2.setExteriorRing(r2);
// TODO Check Orientation<-OUTWARD again
Geometry solid = new Geometry(GeometryType.SOLID, Lod.LOD2, Geometry.Orientation.OUTWARD); Geometry solid = new Geometry(GeometryType.SOLID, Lod.LOD2, Geometry.Orientation.OUTWARD);
solid.getPolygons().addAll(List.of(p1, p2)); solid.getPolygons().addAll(List.of(p1, p2));
......
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckResult;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration;
import de.hft.stuttgart.citydoctor2.check.error.NestedRingError;
import de.hft.stuttgart.citydoctor2.checks.geometry.NestedRingsCheck;
import de.hft.stuttgart.citydoctor2.datastructure.Building;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParseException;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParser;
import de.hft.stuttgart.citydoctor2.parser.InvalidGmlFileException;
public class NestedRingCheckBvhVariantCityGmlTest {
private static final String ST_PETRUS_GML =
"Extensions/CityDoctorHealer/src/test/resources/bht/LoD2_32_344_5675_1_NW.gml";
@Test
public void bvhVariantsMatchOriginalOnStPetrusChurch()
throws CityGmlParseException, InvalidGmlFileException {
List<Polygon> originalPolygons = parsePolygons(resolveStPetrusPath());
assertFalse("Expected polygons from St. Petrus test model", originalPolygons.isEmpty());
List<CheckSummary> originalSummaries = runCheck(originalPolygons, NestedRingsCheck.Variant.OLD);
for (NestedRingsCheck.Variant bvhVariant : bvhVariants()) {
List<CheckSummary> bvhSummaries = runCheck(originalPolygons, bvhVariant);
assertEquals("Different polygon count for OLD/" + bvhVariant,
originalSummaries.size(), bvhSummaries.size());
for (int i = 0; i < originalSummaries.size(); i++) {
CheckSummary original = originalSummaries.get(i);
CheckSummary bvh = bvhSummaries.get(i);
assertEquals("OLD vs " + bvhVariant + " status differs for polygon " + i,
original.status, bvh.status);
assertEquals("OLD vs " + bvhVariant + " error type differs for polygon " + i,
original.errorType, bvh.errorType);
assertEquals("OLD vs " + bvhVariant + " nested ring pair differs for polygon " + i,
original.nestedPairKey, bvh.nestedPairKey);
}
}
}
private static List<CheckSummary> runCheck(List<Polygon> polygons, NestedRingsCheck.Variant variant) {
List<CheckSummary> summaries = new ArrayList<>(polygons.size());
for (Polygon polygon : polygons) {
NestedRingsCheck check = new NestedRingsCheck(variant);
check.check(polygon);
CheckResult result = polygon.getCheckResult(check);
assertNotNull("Expected NestedRingsCheck result for " + variant, result);
summaries.add(summaryFromResult(polygon, result));
}
return summaries;
}
private static CheckSummary summaryFromResult(Polygon polygon, CheckResult result) {
CheckError error = result.getError();
Class<?> errorType = error == null ? null : error.getClass();
String nestedPairKey = null;
if (error instanceof NestedRingError) {
NestedRingError nestedRingError = (NestedRingError) error;
nestedPairKey = nestedPairKey(
polygon,
nestedRingError.getInnerRing(),
nestedRingError.getWithinRing());
}
return new CheckSummary(result.getResultStatus(), errorType, nestedPairKey);
}
private static String nestedPairKey(Polygon polygon, LinearRing outerRing, LinearRing innerRing) {
return ringIndex(polygon, outerRing) + "|" + ringIndex(polygon, innerRing);
}
private static int ringIndex(Polygon polygon, LinearRing ring) {
List<LinearRing> innerRings = polygon.getInnerRings();
for (int i = 0; i < innerRings.size(); i++) {
if (innerRings.get(i) == ring) {
return i;
}
}
throw new AssertionError("Nested-ring result references a ring outside the tested polygon.");
}
private static List<Polygon> parsePolygons(String gmlPath)
throws CityGmlParseException, InvalidGmlFileException {
ValidationConfiguration config = ValidationConfiguration.loadStandardValidationConfig();
config.setSchematronFilePathInGlobalParameters(null);
CityDoctorModel model =
CityGmlParser.parseCityGmlFile(gmlPath, config.getParserConfiguration());
List<Polygon> polygons = new ArrayList<>();
Set<Polygon> seenPolygons = Collections.newSetFromMap(new IdentityHashMap<>());
model.getBuildings()
.filter(building -> building != null)
.forEach(building -> collectBuildingPolygons(building, polygons, seenPolygons));
return polygons;
}
private static void collectBuildingPolygons(
Building building,
List<Polygon> polygons,
Set<Polygon> seenPolygons) {
for (Geometry geometry : building.getGeometries()) {
collectGeometryPolygons(geometry, polygons, seenPolygons);
}
building.getBuildingParts().forEach(part -> {
if (part == null) {
return;
}
for (Geometry geometry : part.getGeometries()) {
collectGeometryPolygons(geometry, polygons, seenPolygons);
}
});
}
private static void collectGeometryPolygons(
Geometry geometry,
List<Polygon> polygons,
Set<Polygon> seenPolygons) {
if (geometry == null) {
return;
}
for (Polygon polygon : geometry.getPolygons()) {
if (polygon != null && seenPolygons.add(polygon)) {
polygons.add(polygon);
}
}
}
private static String resolveStPetrusPath() {
Path parentRelative = Path.of(ST_PETRUS_GML);
if (Files.exists(parentRelative)) {
return parentRelative.toString();
}
Path moduleRelative = Path.of("..", ST_PETRUS_GML);
if (Files.exists(moduleRelative)) {
return moduleRelative.toString();
}
throw new AssertionError("Could not find St. Petrus CityGML test model at " + ST_PETRUS_GML);
}
private static NestedRingsCheck.Variant[] bvhVariants() {
return new NestedRingsCheck.Variant[] {
NestedRingsCheck.Variant.BVH_BINARY_OBJECT_MEDIAN,
NestedRingsCheck.Variant.BVH_BINARY_OBJECT_MEAN,
NestedRingsCheck.Variant.BVH_BINARY_SPATIAL_MEDIAN,
NestedRingsCheck.Variant.BVH_OCTONARY_OBJECT_MEDIAN,
NestedRingsCheck.Variant.BVH_OCTONARY_OBJECT_MEAN,
NestedRingsCheck.Variant.BVH_OCTONARY_SPATIAL_MEDIAN
};
}
private static final class CheckSummary {
final ResultStatus status;
final Class<?> errorType;
final String nestedPairKey;
CheckSummary(ResultStatus status, Class<?> errorType, String nestedPairKey) {
this.status = status;
this.errorType = errorType;
this.nestedPairKey = nestedPairKey;
}
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckResult;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.error.NestedRingError;
import de.hft.stuttgart.citydoctor2.checks.geometry.NestedRingsCheck;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
public class NestedRingCheckBvhVariantSyntheticTest {
@Test
public void bvhVariantsMatchOriginalOnComplexSyntheticGeometry() {
Geometry originalGeometry = SyntheticNestedRingGeometryFactory.complexNestedRingGeometry();
List<Polygon> originalPolygons = originalGeometry.getPolygons();
assertFalse("Expected synthetic nested-ring polygons", originalPolygons.isEmpty());
List<CheckSummary> originalSummaries = runCheck(originalPolygons, NestedRingsCheck.Variant.OLD);
assertEquals("Synthetic fixture should contain three OK polygons",
3, countStatus(originalSummaries, ResultStatus.OK));
assertEquals("Synthetic fixture should contain three nested-ring errors",
3, countStatus(originalSummaries, ResultStatus.ERROR));
for (NestedRingsCheck.Variant bvhVariant : bvhVariants()) {
Geometry bvhGeometry = SyntheticNestedRingGeometryFactory.complexNestedRingGeometry();
List<Polygon> bvhPolygons = bvhGeometry.getPolygons();
List<CheckSummary> bvhSummaries = runCheck(bvhPolygons, bvhVariant);
assertEquals("Different synthetic polygon count for OLD/" + bvhVariant,
originalSummaries.size(), bvhSummaries.size());
for (int i = 0; i < originalSummaries.size(); i++) {
CheckSummary original = originalSummaries.get(i);
CheckSummary bvh = bvhSummaries.get(i);
assertEquals("OLD vs " + bvhVariant + " status differs for synthetic polygon " + i,
original.status, bvh.status);
assertEquals("OLD vs " + bvhVariant + " error type differs for synthetic polygon " + i,
original.errorType, bvh.errorType);
if (original.hasSingleNestedPair) {
assertEquals("OLD vs " + bvhVariant + " nested ring pair differs for synthetic polygon " + i,
original.nestedPairKey, bvh.nestedPairKey);
}
}
}
}
private static List<CheckSummary> runCheck(List<Polygon> polygons, NestedRingsCheck.Variant variant) {
List<CheckSummary> summaries = new ArrayList<>(polygons.size());
for (Polygon polygon : polygons) {
NestedRingsCheck check = new NestedRingsCheck(variant);
check.check(polygon);
CheckResult result = polygon.getCheckResult(check);
assertNotNull("Expected NestedRingsCheck result for " + variant, result);
summaries.add(summaryFromResult(polygon, result));
}
return summaries;
}
private static int countStatus(List<CheckSummary> summaries, ResultStatus status) {
int count = 0;
for (CheckSummary summary : summaries) {
if (summary.status == status) {
count++;
}
}
return count;
}
private static CheckSummary summaryFromResult(Polygon polygon, CheckResult result) {
CheckError error = result.getError();
Class<?> errorType = error == null ? null : error.getClass();
String nestedPairKey = null;
if (error instanceof NestedRingError) {
NestedRingError nestedRingError = (NestedRingError) error;
nestedPairKey = nestedPairKey(
polygon,
nestedRingError.getInnerRing(),
nestedRingError.getWithinRing());
}
return new CheckSummary(
result.getResultStatus(),
errorType,
nestedPairKey,
countNestedPairs(polygon) == 1);
}
private static int countNestedPairs(Polygon polygon) {
int count = 0;
List<LinearRing> innerRings = polygon.getInnerRings();
for (LinearRing outerRing : innerRings) {
for (LinearRing innerRing : innerRings) {
if (outerRing == innerRing) {
continue;
}
if (areAllPointsInside(outerRing, innerRing)) {
count++;
}
}
}
return count;
}
private static boolean areAllPointsInside(LinearRing outerRing, LinearRing innerRing) {
for (Vertex vertex : innerRing.getVertices()) {
if (!outerRing.isPointInside(vertex)) {
return false;
}
}
return true;
}
private static String nestedPairKey(Polygon polygon, LinearRing outerRing, LinearRing innerRing) {
return ringIndex(polygon, outerRing) + "|" + ringIndex(polygon, innerRing);
}
private static int ringIndex(Polygon polygon, LinearRing ring) {
List<LinearRing> innerRings = polygon.getInnerRings();
for (int i = 0; i < innerRings.size(); i++) {
if (innerRings.get(i) == ring) {
return i;
}
}
throw new AssertionError("Nested-ring result references a ring outside the tested polygon.");
}
private static NestedRingsCheck.Variant[] bvhVariants() {
return new NestedRingsCheck.Variant[] {
NestedRingsCheck.Variant.BVH_BINARY_OBJECT_MEDIAN,
NestedRingsCheck.Variant.BVH_BINARY_OBJECT_MEAN,
NestedRingsCheck.Variant.BVH_BINARY_SPATIAL_MEDIAN,
NestedRingsCheck.Variant.BVH_OCTONARY_OBJECT_MEDIAN,
NestedRingsCheck.Variant.BVH_OCTONARY_OBJECT_MEAN,
NestedRingsCheck.Variant.BVH_OCTONARY_SPATIAL_MEDIAN
};
}
private static final class CheckSummary {
final ResultStatus status;
final Class<?> errorType;
final String nestedPairKey;
final boolean hasSingleNestedPair;
CheckSummary(
ResultStatus status,
Class<?> errorType,
String nestedPairKey,
boolean hasSingleNestedPair) {
this.status = status;
this.errorType = errorType;
this.nestedPairKey = nestedPairKey;
this.hasSingleNestedPair = hasSingleNestedPair;
}
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckResult;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration;
import de.hft.stuttgart.citydoctor2.checks.geometry.RingSelfIntCheck;
import de.hft.stuttgart.citydoctor2.datastructure.Building;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParseException;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParser;
import de.hft.stuttgart.citydoctor2.parser.InvalidGmlFileException;
/**
* Compares RingSelfIntCheck variants on the same input model.
*
* Compared variants:
* - OLD
* - six concrete BVH split-strategy variants
*/
public class RingSelfIntCheckBvhVariantCityGmlTest {
private static final String TEST_GML =
"src/test/resources/SimpleSolid_SrefBS-GE-gml-LR-0004-T0004.gml";
private static final double EPSILON = 0.001;
@Test
public void oldVsBvhVariants_sameErrorCounts()
throws CityGmlParseException, InvalidGmlFileException {
Geometry geometryOld = parseGeometry(TEST_GML);
long start = System.nanoTime();
int oldCount = runCheckAndCountErrors(geometryOld, RingSelfIntCheck.Variant.OLD);
long oldTime = System.nanoTime() - start;
System.out.println("RingSelfIntCheck OLD count=" + oldCount + " time(ns)=" + oldTime);
for (RingSelfIntCheck.Variant bvhVariant : bvhVariants()) {
Geometry geometryBvh = parseGeometry(TEST_GML);
start = System.nanoTime();
int bvhCount = runCheckAndCountErrors(geometryBvh, bvhVariant);
long bvhTime = System.nanoTime() - start;
System.out.println("RingSelfIntCheck " + bvhVariant + " count=" + bvhCount + " time(ns)=" + bvhTime);
assertEquals("OLD vs " + bvhVariant + " differs", oldCount, bvhCount);
}
}
@Test
public void perRingResultsMatch_oldVsBvhVariants()
throws CityGmlParseException, InvalidGmlFileException {
Geometry geometryOld = parseGeometry(TEST_GML);
List<LinearRing> oldRings = collectRings(geometryOld);
List<CheckSummary> oldSummaries = new ArrayList<>();
for (LinearRing oldRing : oldRings) {
oldSummaries.add(runCheck(oldRing, RingSelfIntCheck.Variant.OLD));
}
for (RingSelfIntCheck.Variant bvhVariant : bvhVariants()) {
Geometry geometryBvh = parseGeometry(TEST_GML);
List<LinearRing> bvhRings = collectRings(geometryBvh);
assertEquals("Different number of rings in old/" + bvhVariant + " geometry",
oldRings.size(), bvhRings.size());
for (int i = 0; i < bvhRings.size(); i++) {
CheckSummary oldSummary = oldSummaries.get(i);
CheckSummary bvhSummary = runCheck(bvhRings.get(i), bvhVariant);
assertEquals("OLD vs " + bvhVariant + " status differs for ring index " + i,
oldSummary.status, bvhSummary.status);
assertEquals("OLD vs " + bvhVariant + " error type differs for ring index " + i,
oldSummary.errorType, bvhSummary.errorType);
}
}
}
private CheckSummary runCheck(LinearRing ring, RingSelfIntCheck.Variant variant) {
RingSelfIntCheck check = createCheck(variant);
check.check(ring);
CheckResult result = ring.getCheckResult(check);
assertNotNull("CheckResult must not be null", result);
CheckError error = result.getError();
Class<?> errorType = error == null ? null : error.getClass();
return new CheckSummary(result.getResultStatus(), errorType);
}
private int runCheckAndCountErrors(Geometry geometry, RingSelfIntCheck.Variant variant) {
int count = 0;
for (LinearRing ring : collectRings(geometry)) {
RingSelfIntCheck check = createCheck(variant);
check.check(ring);
CheckResult result = ring.getCheckResult(check);
assertNotNull("CheckResult must not be null", result);
if (result.getResultStatus() == ResultStatus.ERROR) {
count++;
}
}
return count;
}
private RingSelfIntCheck createCheck(RingSelfIntCheck.Variant variant) {
RingSelfIntCheck check = new RingSelfIntCheck(variant);
check.init(Collections.singletonMap("minVertexDistance", String.valueOf(EPSILON)), null);
return check;
}
private RingSelfIntCheck.Variant[] bvhVariants() {
return new RingSelfIntCheck.Variant[] {
RingSelfIntCheck.Variant.BVH_BINARY_OBJECT_MEDIAN,
RingSelfIntCheck.Variant.BVH_BINARY_OBJECT_MEAN,
RingSelfIntCheck.Variant.BVH_BINARY_SPATIAL_MEDIAN,
RingSelfIntCheck.Variant.BVH_OCTONARY_OBJECT_MEDIAN,
RingSelfIntCheck.Variant.BVH_OCTONARY_OBJECT_MEAN,
RingSelfIntCheck.Variant.BVH_OCTONARY_SPATIAL_MEDIAN
};
}
private List<LinearRing> collectRings(Geometry geometry) {
assertNotNull("geometry must not be null", geometry);
List<LinearRing> rings = new ArrayList<>();
for (Polygon polygon : geometry.getPolygons()) {
if (polygon.getExteriorRing() != null) {
rings.add(polygon.getExteriorRing());
}
rings.addAll(polygon.getInnerRings());
}
return rings;
}
private Geometry parseGeometry(String gmlPath)
throws CityGmlParseException, InvalidGmlFileException {
ValidationConfiguration config = ValidationConfiguration.loadStandardValidationConfig();
config.setSchematronFilePathInGlobalParameters(null);
CityDoctorModel model =
CityGmlParser.parseCityGmlFile(gmlPath, config.getParserConfiguration());
Building building = model.getBuildings().findFirst().orElseThrow();
Geometry geometry = building.getGeometry(GeometryType.SOLID, Lod.LOD2);
assertNotNull("Expected SOLID LOD2 geometry in test model: " + gmlPath, geometry);
return geometry;
}
private static final class CheckSummary {
final ResultStatus status;
final Class<?> errorType;
CheckSummary(ResultStatus status, Class<?> errorType) {
this.status = status;
this.errorType = errorType;
}
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckResult;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.checks.geometry.RingSelfIntCheck;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
public class RingSelfIntCheckBvhVariantSyntheticTest {
private static final double EPSILON = 0.001;
@Test
public void bvhVariantsMatchOriginalOnSyntheticRings() {
Geometry oldGeometry = syntheticRingGeometry();
List<LinearRing> oldRings = collectExteriorRings(oldGeometry);
List<CheckSummary> oldSummaries = new ArrayList<>();
for (LinearRing oldRing : oldRings) {
oldSummaries.add(runCheck(oldRing, RingSelfIntCheck.Variant.OLD));
}
for (RingSelfIntCheck.Variant bvhVariant : bvhVariants()) {
Geometry bvhGeometry = syntheticRingGeometry();
List<LinearRing> bvhRings = collectExteriorRings(bvhGeometry);
assertEquals(oldRings.size(), bvhRings.size());
for (int i = 0; i < bvhRings.size(); i++) {
CheckSummary oldSummary = oldSummaries.get(i);
CheckSummary bvhSummary = runCheck(bvhRings.get(i), bvhVariant);
assertEquals("OLD vs " + bvhVariant + " status differs for synthetic ring " + i,
oldSummary.status, bvhSummary.status);
assertEquals("OLD vs " + bvhVariant + " error type differs for synthetic ring " + i,
oldSummary.errorType, bvhSummary.errorType);
}
}
}
private static RingSelfIntCheck.Variant[] bvhVariants() {
return new RingSelfIntCheck.Variant[] {
RingSelfIntCheck.Variant.BVH_BINARY_OBJECT_MEDIAN,
RingSelfIntCheck.Variant.BVH_BINARY_OBJECT_MEAN,
RingSelfIntCheck.Variant.BVH_BINARY_SPATIAL_MEDIAN,
RingSelfIntCheck.Variant.BVH_OCTONARY_OBJECT_MEDIAN,
RingSelfIntCheck.Variant.BVH_OCTONARY_OBJECT_MEAN,
RingSelfIntCheck.Variant.BVH_OCTONARY_SPATIAL_MEDIAN
};
}
private static CheckSummary runCheck(LinearRing ring, RingSelfIntCheck.Variant variant) {
RingSelfIntCheck check = new RingSelfIntCheck(variant);
check.init(Collections.singletonMap("minVertexDistance", String.valueOf(EPSILON)), null);
check.check(ring);
CheckResult result = ring.getCheckResult(check);
assertNotNull("Expected CheckResult for " + variant, result);
CheckError error = result.getError();
Class<?> errorType = error == null ? null : error.getClass();
return new CheckSummary(result.getResultStatus(), errorType);
}
private static Geometry syntheticRingGeometry() {
Geometry geometry = new Geometry(GeometryType.SOLID, Lod.LOD2, Orientation.OUTWARD);
addRingPolygon(geometry, rectangle());
addRingPolygon(geometry, bowTie());
addRingPolygon(geometry, pointNearEdge());
addRingPolygon(geometry, largeConvexRing(80, 20.0, 40.0, 0.0));
addRingPolygon(geometry, zigZagCorridor(30));
geometry.updateEdgesAndVertices();
return geometry;
}
private static List<LinearRing> collectExteriorRings(Geometry geometry) {
List<LinearRing> rings = new ArrayList<>();
for (Polygon polygon : geometry.getPolygons()) {
rings.add(polygon.getExteriorRing());
}
return rings;
}
private static void addRingPolygon(Geometry geometry, double[][] coordinates) {
ConcretePolygon polygon = new ConcretePolygon();
LinearRing ring = new LinearRing(LinearRingType.EXTERIOR);
polygon.setExteriorRing(ring);
geometry.addPolygon(polygon);
Vertex firstVertex = null;
for (int i = 0; i < coordinates.length; i++) {
double[] coordinate = coordinates[i];
if (i == coordinates.length - 1 && sameCoordinate(coordinate, coordinates[0])) {
ring.addVertex(firstVertex);
continue;
}
Vertex vertex = new Vertex(coordinate[0], coordinate[1], coordinate[2]);
if (i == 0) {
firstVertex = vertex;
}
ring.addVertex(vertex);
}
}
private static boolean sameCoordinate(double[] a, double[] b) {
return Double.compare(a[0], b[0]) == 0
&& Double.compare(a[1], b[1]) == 0
&& Double.compare(a[2], b[2]) == 0;
}
private static double[][] rectangle() {
return new double[][] {
{0.0, 0.0, 0.0},
{10.0, 0.0, 0.0},
{10.0, 10.0, 0.0},
{0.0, 10.0, 0.0},
{0.0, 0.0, 0.0}
};
}
private static double[][] bowTie() {
return new double[][] {
{20.0, 0.0, 0.0},
{30.0, 10.0, 0.0},
{30.0, 0.0, 0.0},
{20.0, 10.0, 0.0},
{20.0, 0.0, 0.0}
};
}
private static double[][] pointNearEdge() {
return new double[][] {
{40.0, 0.0, 0.0},
{50.0, 0.0, 0.0},
{50.0, 10.0, 0.0},
{45.0, EPSILON * 0.5, 0.0},
{40.0, 10.0, 0.0},
{40.0, 0.0, 0.0}
};
}
private static double[][] largeConvexRing(int vertexCount, double centerX, double centerY, double z) {
double[][] coordinates = new double[vertexCount + 1][3];
for (int i = 0; i < vertexCount; i++) {
double angle = 2.0 * Math.PI * i / vertexCount;
coordinates[i][0] = centerX + Math.cos(angle) * 8.0;
coordinates[i][1] = centerY + Math.sin(angle) * 5.0;
coordinates[i][2] = z;
}
coordinates[vertexCount][0] = coordinates[0][0];
coordinates[vertexCount][1] = coordinates[0][1];
coordinates[vertexCount][2] = coordinates[0][2];
return coordinates;
}
private static double[][] zigZagCorridor(int segments) {
double[][] coordinates = new double[(segments * 2) + 3][3];
int index = 0;
for (int i = 0; i <= segments; i++) {
coordinates[index++] = new double[] {60.0 + i, i % 2 == 0 ? 0.0 : 1.0, 0.0};
}
for (int i = segments; i >= 0; i--) {
coordinates[index++] = new double[] {60.0 + i, i % 2 == 0 ? 4.0 : 5.0, 0.0};
}
coordinates[index] = new double[] {60.0, 0.0, 0.0};
return coordinates;
}
private static final class CheckSummary {
final ResultStatus status;
final Class<?> errorType;
CheckSummary(ResultStatus status, Class<?> errorType) {
this.status = status;
this.errorType = errorType;
}
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.util.List;
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.database.UnconnectedCache;
import de.hft.stuttgart.citydoctor2.datastructure.Building;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree;
import de.hft.stuttgart.citydoctor2.exceptions.CityDoctorWriteException;
import de.hft.stuttgart.citydoctor2.checks.util.GeometryTestUtils;
import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParseException;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParser;
import de.hft.stuttgart.citydoctor2.parser.InvalidGmlFileException;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.utils.PolygonIntersection;
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
public void testBVHCalculateOnKnownGoodModel() throws CityGmlParseException, InvalidGmlFileException {
ValidationConfiguration config = ValidationConfiguration.loadStandardValidationConfig();
config.setSchematronFilePathInGlobalParameters(null);
CityDoctorModel m = CityGmlParser.parseCityGmlFile(
"src/test/resources/SolidSelfIntTest1.gml",
config.getParserConfiguration()
);
Building building = m.getBuildings().findFirst().orElseThrow();
Geometry g = building.getGeometry(GeometryType.SOLID, Lod.LOD2);
assertNotNull("Expected SOLID LOD2 geometry in test model", g);
List<Polygon> polys = g.getPolygons();
assertNotNull(polys);
assertTrue("Expected at least 2 polygons", polys.size() > 1);
BoundingVolumeHierarchyTree<Polygon> tree =
BoundingVolumeHierarchyTree.newBinary(polys, p -> AABB.of(p.getOriginal()));
double delta = 0.001;
// calls new method with trees
List<PolygonIntersection> intersections =
SelfIntersectionUtil.calculateSolidSelfIntersection(g, delta, tree);
// This file is a good example (no self intersection)
assertTrue("No self-intersections expected for SolidSelfIntTest1.gml",
intersections.isEmpty());
}
}
package de.hft.stuttgart.citydoctor2.checks.bht;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.List;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.datastructure.Building;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree;
import de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil;
import de.hft.stuttgart.citydoctor2.utils.PolygonIntersection;
public class SolidSelfIntersectionBuildingTest {
private static final double DELTA = 0.001;
private static final int MIN_EXPECTED_POLYGONS = 60;
@Test
public void testBvhBuildAndQueryOnLod2AndLod3() {
Building building = createDenseBuildingWithoutIntersections();
assertBvhBuildAndQuery(building.getGeometry(GeometryType.SOLID, Lod.LOD2), "LOD2");
assertBvhBuildAndQuery(building.getGeometry(GeometryType.SOLID, Lod.LOD3), "LOD3");
}
@Test
public void testOldVsNewSameResultCountOnIntersectingLod2AndLod3() {
Building building = createDenseBuildingWithIntersections();
assertOldVsNewComparison(building.getGeometry(GeometryType.SOLID, Lod.LOD2), "LOD2");
assertOldVsNewComparison(building.getGeometry(GeometryType.SOLID, Lod.LOD3), "LOD3");
}
private void assertBvhBuildAndQuery(Geometry geometry, String lodLabel) {
List<Polygon> polygons = requireValidGeometry(geometry, lodLabel);
BoundingVolumeHierarchyTree<Polygon> tree =
BoundingVolumeHierarchyTree.newBinary(polygons, p -> AABB.of(p.getOriginal()));
Polygon probe = polygons.get(0);
AABB probeAabb = AABB.of(probe.getOriginal());
assertNotNull("Probe AABB must not be null for " + lodLabel, probeAabb);
List<Polygon> candidates = tree.getAllIntersectingElements(probeAabb);
int nonEmptyQueries = 0;
int totalCandidates = 0;
int maxCandidates = 0;
for (Polygon polygon : polygons) {
AABB query = AABB.of(polygon.getOriginal());
assertNotNull("Query AABB must not be null for " + lodLabel, query);
List<Polygon> perPolygonCandidates = tree.getAllIntersectingElements(query);
if (!perPolygonCandidates.isEmpty()) {
nonEmptyQueries++;
}
int currentSize = perPolygonCandidates.size();
totalCandidates += currentSize;
if (currentSize > maxCandidates) {
maxCandidates = currentSize;
}
}
printBvhStats(
lodLabel,
polygons.size(),
candidates.size(),
nonEmptyQueries,
totalCandidates,
maxCandidates);
assertTrue(
"Expected at least one non-empty BVH query on " + lodLabel,
nonEmptyQueries > 0);
}
private void assertOldVsNewComparison(Geometry geometry, String lodLabel) {
List<Polygon> polygons = requireValidGeometry(geometry, lodLabel);
List<PolygonIntersection> oldRes = SelfIntersectionUtil.calculateSolidSelfIntersection0(geometry, DELTA);
assertNotNull("Old result list must not be null for " + lodLabel, oldRes);
BoundingVolumeHierarchyTree<Polygon> externalTree =
BoundingVolumeHierarchyTree.newBinary(polygons, p -> AABB.of(p.getOriginal()));
List<PolygonIntersection> oldTreeRes = SelfIntersectionUtil.calculateSolidSelfIntersection(
geometry,
DELTA,
externalTree);
assertNotNull("Old+tree result list must not be null for " + lodLabel, oldTreeRes);
List<PolygonIntersection> newRes = SelfIntersectionUtil.calculateSolidSelfIntersectionWithTree(geometry, DELTA);
assertNotNull("New result list must not be null for " + lodLabel, newRes);
printComparisonStats(lodLabel, polygons.size(), oldRes.size(), oldTreeRes.size(), newRes.size());
assertEquals("Old vs external-tree differs for " + lodLabel, oldRes.size(), oldTreeRes.size());
assertEquals("Old vs new-tree differs for " + lodLabel, oldRes.size(), newRes.size());
}
private List<Polygon> requireValidGeometry(Geometry geometry, String lodLabel) {
assertNotNull("Expected geometry for " + lodLabel, geometry);
List<Polygon> polygons = geometry.getPolygons();
assertNotNull("Polygon list must not be null for " + lodLabel, polygons);
assertFalse("Polygon list must not be empty for " + lodLabel, polygons.isEmpty());
assertTrue(
"Expected many polygons for " + lodLabel + " (got " + polygons.size() + ")",
polygons.size() >= MIN_EXPECTED_POLYGONS);
return polygons;
}
private void printBvhStats(
String lodLabel,
int polygonCount,
int probeCandidateCount,
int nonEmptyQueries,
int totalCandidates,
int maxCandidates) {
System.out.printf(
"[BVH][%s] polygons=%d, probeCandidates=%d, nonEmptyQueries=%d/%d, totalCandidates=%d, maxPerQuery=%d%n",
lodLabel,
polygonCount,
probeCandidateCount,
nonEmptyQueries,
polygonCount,
totalCandidates,
maxCandidates);
}
private void printComparisonStats(
String lodLabel,
int polygonCount,
int oldCount,
int oldTreeCount,
int newCount) {
System.out.printf(
"[SelfInt][%s] polygons=%d, old=%d, old+tree=%d, new=%d%n",
lodLabel,
polygonCount,
oldCount,
oldTreeCount,
newCount);
}
private Building createDenseBuildingWithoutIntersections() {
Building b = new Building();
b.addGeometry(createGridGeometry(Lod.LOD2, 4, 4, false));
b.addGeometry(createGridGeometry(Lod.LOD3, 6, 6, false));
return b;
}
private Building createDenseBuildingWithIntersections() {
Building b = new Building();
b.addGeometry(createGridGeometry(Lod.LOD2, 4, 4, true));
b.addGeometry(createGridGeometry(Lod.LOD3, 6, 6, true));
return b;
}
private Geometry createGridGeometry(Lod lod, int xCount, int yCount, boolean addIntersections) {
Geometry g = new Geometry(GeometryType.SOLID, lod, Orientation.OUTWARD);
double spacing = 6.0;
double width = 4.0;
double depth = 4.0;
double height = 4.0;
for (int ix = 0; ix < xCount; ix++) {
for (int iy = 0; iy < yCount; iy++) {
double x = ix * spacing;
double y = iy * spacing;
addBox(g, x, y, 0.0, width, depth, height);
}
}
// TODO Re-validate the Intersction-Idee
if (addIntersections) {
// Two additional boxes overlap each other and the grid neighborhood.
addBox(g, spacing * 1.2, spacing * 1.2, 0.5, 6.0, 2.8, 3.5);
addBox(g, spacing * 1.4, spacing * 1.0, 0.0, 2.8, 6.0, 4.2);
}
g.updateEdgesAndVertices();
return g;
}
///----------------------------------- add geometric sub-entities ---------------------------///
private void addBox(Geometry geometry, double x, double y, double z, double width, double depth, double height) {
Vertex v000 = new Vertex(x, y, z);
Vertex v100 = new Vertex(x + width, y, z);
Vertex v110 = new Vertex(x + width, y + depth, z);
Vertex v010 = new Vertex(x, y + depth, z);
Vertex v001 = new Vertex(x, y, z + height);
Vertex v101 = new Vertex(x + width, y, z + height);
Vertex v111 = new Vertex(x + width, y + depth, z + height);
Vertex v011 = new Vertex(x, y + depth, z + height);
addQuad(geometry, v000, v100, v110, v010); // bottom
addQuad(geometry, v001, v011, v111, v101); // top
addQuad(geometry, v000, v001, v101, v100); // front
addQuad(geometry, v100, v101, v111, v110); // right
addQuad(geometry, v110, v111, v011, v010); // back
addQuad(geometry, v010, v011, v001, v000); // left
}
private void addQuad(Geometry geometry, Vertex a, Vertex b, Vertex c, Vertex d) {
ConcretePolygon polygon = new ConcretePolygon();
LinearRing ring = new LinearRing(LinearRingType.EXTERIOR);
polygon.setExteriorRing(ring);
// ACHTUNG :Ensure polygon->geometry parent relation exists before adding vertices.
// LinearRing.addVertex() updates vertex adjacency via parent geometry.
geometry.addPolygon(polygon);
ring.addVertex(a);
ring.addVertex(b);
ring.addVertex(c);
ring.addVertex(d);
ring.addVertex(a);
}
}
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