Commit b073d9bc authored by Numanoglu's avatar Numanoglu
Browse files

Remove old BVH test locations

parent e6f4979d
package de.hft.stuttgart.citydoctor2.checks.geometry;
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.CheckResult;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration;
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
* - TREE_1_EDGE_BVH
* - TREE_2_EDGE_AND_VERTEX_BVH
*/
public class RingSelfIntCheckVariantComparisonTest {
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 oldVsTree1VsTree2_sameErrorCounts()
throws CityGmlParseException, InvalidGmlFileException {
Geometry geometryOld = parseGeometry(TEST_GML);
Geometry geometryTree1 = parseGeometry(TEST_GML);
Geometry geometryTree2 = 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);
start = System.nanoTime();
int tree1Count = runCheckAndCountErrors(geometryTree1, RingSelfIntCheck.Variant.TREE_1_EDGE_BVH);
long tree1Time = System.nanoTime() - start;
System.out.println("RingSelfIntCheck TREE_1_EDGE_BVH count=" + tree1Count + " time(ns)=" + tree1Time);
start = System.nanoTime();
int tree2Count = runCheckAndCountErrors(geometryTree2, RingSelfIntCheck.Variant.TREE_2_EDGE_AND_VERTEX_BVH);
long tree2Time = System.nanoTime() - start;
System.out.println("RingSelfIntCheck TREE_2_EDGE_AND_VERTEX_BVH count=" + tree2Count + " time(ns)=" + tree2Time);
assertEquals("OLD vs TREE_1_EDGE_BVH differs", oldCount, tree1Count);
assertEquals("OLD vs TREE_2_EDGE_AND_VERTEX_BVH differs", oldCount, tree2Count);
}
@Test
public void perRingResultsMatch_oldVsTree1VsTree2()
throws CityGmlParseException, InvalidGmlFileException {
Geometry geometryOld = parseGeometry(TEST_GML);
Geometry geometryTree1 = parseGeometry(TEST_GML);
Geometry geometryTree2 = parseGeometry(TEST_GML);
List<LinearRing> oldRings = collectRings(geometryOld);
List<LinearRing> tree1Rings = collectRings(geometryTree1);
List<LinearRing> tree2Rings = collectRings(geometryTree2);
assertEquals("Different number of rings in old/tree1 geometry", oldRings.size(), tree1Rings.size());
assertEquals("Different number of rings in old/tree2 geometry", oldRings.size(), tree2Rings.size());
for (int i = 0; i < oldRings.size(); i++) {
LinearRing oldRing = oldRings.get(i);
LinearRing tree1Ring = tree1Rings.get(i);
LinearRing tree2Ring = tree2Rings.get(i);
RingSelfIntCheck oldCheck = createCheck(RingSelfIntCheck.Variant.OLD);
RingSelfIntCheck tree1Check = createCheck(RingSelfIntCheck.Variant.TREE_1_EDGE_BVH);
RingSelfIntCheck tree2Check = createCheck(RingSelfIntCheck.Variant.TREE_2_EDGE_AND_VERTEX_BVH);
oldCheck.check(oldRing);
tree1Check.check(tree1Ring);
tree2Check.check(tree2Ring);
CheckResult oldResult = oldRing.getCheckResult(oldCheck);
CheckResult tree1Result = tree1Ring.getCheckResult(tree1Check);
CheckResult tree2Result = tree2Ring.getCheckResult(tree2Check);
assertNotNull("OLD result is null for ring index " + i, oldResult);
assertNotNull("TREE_1 result is null for ring index " + i, tree1Result);
assertNotNull("TREE_2 result is null for ring index " + i, tree2Result);
assertEquals("OLD vs TREE_1 status differs for ring index " + i,
oldResult.getResultStatus(), tree1Result.getResultStatus());
assertEquals("OLD vs TREE_2 status differs for ring index " + i,
oldResult.getResultStatus(), tree2Result.getResultStatus());
}
}
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 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;
}
}
\ No newline at end of file
package de.hft.stuttgart.citydoctor2.checks.util;
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.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());
}
}
\ No newline at end of file
package de.hft.stuttgart.citydoctor2.checks.util;
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.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 = new BoundingVolumeHierarchyTree<>(
polygons,
p -> AABB.of(p.getOriginal()),
BoundingVolumeHierarchyTree.BuildConfig.binaryDefault());
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 = new BoundingVolumeHierarchyTree<>(
polygons,
p -> AABB.of(p.getOriginal()),
BoundingVolumeHierarchyTree.BuildConfig.binaryDefault());
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);
}
}
package de.hft.stuttgart.citydoctor2.checks.util;
import static org.junit.Assert.*;
import java.util.List;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration;
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.parser.CityGmlParseException;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParser;
import de.hft.stuttgart.citydoctor2.parser.InvalidGmlFileException;
import de.hft.stuttgart.citydoctor2.utils.PolygonIntersection;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree;
/*
* First comparison SolidSelfIntersection version with Bounding Volume Tree vs old version
*
* @ Baris Numanoglu
* */
public class SolidSelfIntersectionOldVsNewTest {
// @Test
// public void testOldVsNewSameResultCount() throws CityGmlParseException, InvalidGmlFileException {
// compareOnFile("src/test/resources/SolidSelfIntTest-known_false_positive_Big_Mesh2.gml", 0.001);
// }// dubious Test data: may be TP and not FP?
//
// @Test
// public void testOldVsNewSameResultCountFalsePositiveExample1() throws CityGmlParseException, InvalidGmlFileException {
// compareOnFile("src/test/resources/SolidSelfIntTest-known_false_positive1.gml", 0.001);
// }
@Test
public void testOldVsNewSameResultCountFalsePositiveExample2() throws CityGmlParseException, InvalidGmlFileException {
compareOnFile("src/test/resources/SolidSelfIntTest-known_false_positive2.gml", 0.001);
}
private void compareOnFile(String gmlPath, double delta) throws CityGmlParseException, InvalidGmlFileException {
ValidationConfiguration config = ValidationConfiguration.loadStandardValidationConfig();
config.setSchematronFilePathInGlobalParameters(null);
CityDoctorModel m = CityGmlParser.parseCityGmlFile(gmlPath, config.getParserConfiguration());
Building building = m.getBuildings().findFirst().orElseThrow();
Geometry g = building.getGeometry(GeometryType.SOLID, Lod.LOD2);
assertNotNull("Expected SOLID LOD2 geometry in test model: " + gmlPath, g);
List<Polygon> polys = g.getPolygons();
assertNotNull(polys);
assertTrue("Expected at least 2 polygons in: " + gmlPath, polys.size() > 1);
/// Without Tree
long start = System.nanoTime();
List<PolygonIntersection> oldRes = SelfIntersectionUtil.calculateSolidSelfIntersection0(g, delta);
long dif = System.nanoTime() - start;
System.out.println("Alt: " + dif);
/// With IdentityHashMap
start = System.nanoTime();
BoundingVolumeHierarchyTree<Polygon> polygonTree =
new BoundingVolumeHierarchyTree<>(
g.getPolygons(),
AABB::of,
BoundingVolumeHierarchyTree.BuildConfig.binaryDefault()
);
List<PolygonIntersection> oldTreeRes =
SelfIntersectionUtil.calculateSolidSelfIntersection(g, delta, polygonTree);
dif = System.nanoTime() - start;
System.out.println("Alt + external polygon tree: " + dif);
/// With Polygon Indices
start = System.nanoTime();
List<PolygonIntersection> newRes =
SelfIntersectionUtil.calculateSolidSelfIntersectionWithTree(g, delta);
dif = System.nanoTime() - start;
System.out.println("Neu: " + dif);
///
System.out.println("oldRes.size() = " + oldRes.size());
System.out.println("oldTreeRes.size() = " + oldTreeRes.size());
System.out.println("newRes.size() = " + newRes.size());
assertFalse("oldRes is empty for: " + gmlPath, oldRes.isEmpty());
assertFalse("oldTreeRes is empty for: " + gmlPath, oldTreeRes.isEmpty());
assertFalse("newRes is empty for: " + gmlPath, newRes.isEmpty());
assertEquals("Old vs oldTree differs for: " + gmlPath,
oldRes.size(), oldTreeRes.size());
assertEquals("Old vs new self-intersection result count differs for: " + gmlPath, oldRes.size(), newRes.size());
}
}
\ No newline at end of file
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