AbstractRegionBSPTreeBooleanTest.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.geometry.core.partitioning.bsp;
import java.util.Arrays;
import java.util.function.Supplier;
import org.apache.commons.geometry.core.partitioning.test.PartitionTestUtils;
import org.apache.commons.geometry.core.partitioning.test.TestLine;
import org.apache.commons.geometry.core.partitioning.test.TestLineSegment;
import org.apache.commons.geometry.core.partitioning.test.TestPoint2D;
import org.apache.commons.geometry.core.partitioning.test.TestRegionBSPTree;
import org.junit.jupiter.api.Test;
class AbstractRegionBSPTreeBooleanTest {
@Test
void testUnion_singleNodeTrees() {
// act/assert
unionChecker(AbstractRegionBSPTreeBooleanTest::emptyTree, AbstractRegionBSPTreeBooleanTest::emptyTree)
.full(false)
.empty(true)
.count(1)
.check();
unionChecker(AbstractRegionBSPTreeBooleanTest::fullTree, AbstractRegionBSPTreeBooleanTest::emptyTree)
.full(true)
.empty(false)
.count(1)
.check();
unionChecker(AbstractRegionBSPTreeBooleanTest::emptyTree, AbstractRegionBSPTreeBooleanTest::fullTree)
.full(true)
.empty(false)
.count(1)
.check();
unionChecker(AbstractRegionBSPTreeBooleanTest::fullTree, AbstractRegionBSPTreeBooleanTest::fullTree)
.full(true)
.empty(false)
.count(1)
.check();
}
@Test
void testUnion_simpleCrossingCuts() {
// act/assert
unionChecker(AbstractRegionBSPTreeBooleanTest::xAxisTree, AbstractRegionBSPTreeBooleanTest::emptyTree)
.full(false)
.empty(false)
.count(3)
.check();
unionChecker(AbstractRegionBSPTreeBooleanTest::emptyTree, AbstractRegionBSPTreeBooleanTest::xAxisTree)
.full(false)
.empty(false)
.count(3)
.check();
unionChecker(AbstractRegionBSPTreeBooleanTest::yAxisTree, AbstractRegionBSPTreeBooleanTest::fullTree)
.full(true)
.empty(false)
.count(1)
.check();
unionChecker(AbstractRegionBSPTreeBooleanTest::fullTree, AbstractRegionBSPTreeBooleanTest::yAxisTree)
.full(true)
.empty(false)
.count(1)
.check();
unionChecker(AbstractRegionBSPTreeBooleanTest::xAxisTree, AbstractRegionBSPTreeBooleanTest::yAxisTree)
.full(false)
.empty(false)
.inside(new TestPoint2D(1, 1), new TestPoint2D(-1, 1), new TestPoint2D(-1, -1))
.outside(new TestPoint2D(1, -1))
.boundary(TestPoint2D.ZERO)
.count(5)
.check(t -> {
final TestLineSegment seg = (TestLineSegment) t.getRoot().getPlus().getCut();
PartitionTestUtils.assertPointsEqual(new TestPoint2D(0.0, Double.NEGATIVE_INFINITY), seg.getStartPoint());
PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, seg.getEndPoint());
});
}
@Test
void testUnion_mixedCutRules() {
// arrange
final Supplier<TestRegionBSPTree> r1 = () -> {
final TestRegionBSPTree tree = new TestRegionBSPTree(false);
tree.insert(TestLine.X_AXIS.span(), RegionCutRule.PLUS_INSIDE);
tree.insert(new TestLine(new TestPoint2D(5, 0), new TestPoint2D(5, 1)).span(), RegionCutRule.INHERIT);
return tree;
};
final Supplier<TestRegionBSPTree> r2 = () -> {
final TestRegionBSPTree tree = new TestRegionBSPTree(false);
tree.insert(TestLine.Y_AXIS.span(), RegionCutRule.MINUS_INSIDE);
return tree;
};
// act/assert
// Note that the tree node count is different from other tests because one input tree is not condensed.
// However, the resulting regions are equivalent.
unionChecker(r1, r2)
.full(false)
.empty(false)
.inside(new TestPoint2D(1, -1), new TestPoint2D(-1, 1), new TestPoint2D(-1, -1))
.outside(new TestPoint2D(1, 1), new TestPoint2D(10, 10))
.boundary(TestPoint2D.ZERO)
.count(7)
.check(t -> {
final TestLineSegment seg = (TestLineSegment) t.getRoot().getMinus().getMinus().getCut();
PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, seg.getStartPoint());
PartitionTestUtils.assertPointsEqual(new TestPoint2D(0.0, Double.POSITIVE_INFINITY), seg.getEndPoint());
});
}
@Test
void testUnion_boxTreeWithSingleCutTree() {
// arrange
final Supplier<TestRegionBSPTree> boxFactory = () -> {
final TestRegionBSPTree box = fullTree();
insertBox(box, TestPoint2D.ZERO, new TestPoint2D(2, -4));
return box;
};
final Supplier<TestRegionBSPTree> horizontalFactory = () -> {
final TestRegionBSPTree horizontal = fullTree();
horizontal.getRoot().insertCut(new TestLine(new TestPoint2D(2, 2), new TestPoint2D(0, 2)));
return horizontal;
};
// act/assert
unionChecker(horizontalFactory, boxFactory)
.count(3)
.inside(TestPoint2D.ZERO, new TestPoint2D(1, 1), new TestPoint2D(1, -1))
.outside(new TestPoint2D(0, 3), new TestPoint2D(3, 3))
.boundary(new TestPoint2D(-1, 2), new TestPoint2D(3, 2))
.check();
}
@Test
void testUnion_treeWithComplement() {
// arrange
final Supplier<TestRegionBSPTree> treeFactory = () -> {
final TestRegionBSPTree t = fullTree();
insertSkewedBowtie(t);
return t;
};
final Supplier<TestRegionBSPTree> complementFactory = () -> {
final TestRegionBSPTree t = treeFactory.get();
t.complement();
return t;
};
// act/assert
unionChecker(treeFactory, complementFactory)
.full(true)
.empty(false)
.count(1)
.check();
}
@Test
void testIntersection_singleNodeTrees() {
// act/assert
intersectionChecker(AbstractRegionBSPTreeBooleanTest::emptyTree, AbstractRegionBSPTreeBooleanTest::emptyTree)
.full(false)
.empty(true)
.count(1)
.check();
intersectionChecker(AbstractRegionBSPTreeBooleanTest::fullTree, AbstractRegionBSPTreeBooleanTest::emptyTree)
.full(false)
.empty(true)
.count(1)
.check();
intersectionChecker(AbstractRegionBSPTreeBooleanTest::emptyTree, AbstractRegionBSPTreeBooleanTest::fullTree)
.full(false)
.empty(true)
.count(1)
.check();
intersectionChecker(AbstractRegionBSPTreeBooleanTest::fullTree, AbstractRegionBSPTreeBooleanTest::fullTree)
.full(true)
.empty(false)
.count(1)
.check();
}
@Test
void testIntersection_simpleCrossingCuts() {
// act/assert
intersectionChecker(AbstractRegionBSPTreeBooleanTest::xAxisTree, AbstractRegionBSPTreeBooleanTest::emptyTree)
.full(false)
.empty(true)
.count(1)
.check();
intersectionChecker(AbstractRegionBSPTreeBooleanTest::emptyTree, AbstractRegionBSPTreeBooleanTest::xAxisTree)
.full(false)
.empty(true)
.count(1)
.check();
intersectionChecker(AbstractRegionBSPTreeBooleanTest::yAxisTree, AbstractRegionBSPTreeBooleanTest::fullTree)
.full(false)
.empty(false)
.inside(new TestPoint2D(-1, 1), new TestPoint2D(-1, -1))
.outside(new TestPoint2D(1, 1), new TestPoint2D(1, -1))
.boundary(new TestPoint2D(0, 1), new TestPoint2D(0, -1))
.count(3)
.check();
intersectionChecker(AbstractRegionBSPTreeBooleanTest::fullTree, AbstractRegionBSPTreeBooleanTest::yAxisTree)
.full(false)
.empty(false)
.inside(new TestPoint2D(-1, 1), new TestPoint2D(-1, -1))
.outside(new TestPoint2D(1, 1), new TestPoint2D(1, -1))
.boundary(new TestPoint2D(0, 1), new TestPoint2D(0, -1))
.count(3)
.check();
intersectionChecker(AbstractRegionBSPTreeBooleanTest::xAxisTree, AbstractRegionBSPTreeBooleanTest::yAxisTree)
.full(false)
.empty(false)
.inside(new TestPoint2D(-1, 1))
.outside(new TestPoint2D(1, 1), new TestPoint2D(1, -1), new TestPoint2D(-1, -1))
.boundary(TestPoint2D.ZERO)
.count(5)
.check(t -> {
final TestLineSegment seg = (TestLineSegment) t.getRoot().getMinus().getCut();
PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, seg.getStartPoint());
PartitionTestUtils.assertPointsEqual(new TestPoint2D(0.0, Double.POSITIVE_INFINITY), seg.getEndPoint());
});
}
@Test
void testIntersection_boxTreeWithSingleCutTree() {
// arrange
final Supplier<TestRegionBSPTree> boxFactory = () -> {
final TestRegionBSPTree box = fullTree();
insertBox(box, TestPoint2D.ZERO, new TestPoint2D(2, -4));
return box;
};
final Supplier<TestRegionBSPTree> horizontalFactory = () -> {
final TestRegionBSPTree horizontal = fullTree();
horizontal.getRoot().insertCut(new TestLine(new TestPoint2D(2, -2), new TestPoint2D(0, -2)));
return horizontal;
};
// act/assert
intersectionChecker(horizontalFactory, boxFactory)
.inside(new TestPoint2D(1, -3))
.outside(new TestPoint2D(1, -1), new TestPoint2D(-1, -3),
new TestPoint2D(1, -5), new TestPoint2D(3, -3))
.boundary(new TestPoint2D(0, -2), new TestPoint2D(2, -2),
new TestPoint2D(0, -4), new TestPoint2D(2, -4))
.count(9)
.check();
}
@Test
void testIntersection_treeWithComplement() {
// arrange
final Supplier<TestRegionBSPTree> treeFactory = () -> {
final TestRegionBSPTree t = fullTree();
insertSkewedBowtie(t);
return t;
};
final Supplier<TestRegionBSPTree> complementFactory = () -> {
final TestRegionBSPTree t = treeFactory.get();
t.complement();
return t;
};
// act/assert
intersectionChecker(treeFactory, complementFactory)
.full(false)
.empty(true)
.count(1)
.check();
}
@Test
void testDifference_singleNodeTrees() {
// act/assert
differenceChecker(AbstractRegionBSPTreeBooleanTest::emptyTree, AbstractRegionBSPTreeBooleanTest::emptyTree)
.full(false)
.empty(true)
.count(1)
.check();
differenceChecker(AbstractRegionBSPTreeBooleanTest::fullTree, AbstractRegionBSPTreeBooleanTest::emptyTree)
.full(true)
.empty(false)
.count(1)
.check();
differenceChecker(AbstractRegionBSPTreeBooleanTest::emptyTree, AbstractRegionBSPTreeBooleanTest::fullTree)
.full(false)
.empty(true)
.count(1)
.check();
differenceChecker(AbstractRegionBSPTreeBooleanTest::fullTree, AbstractRegionBSPTreeBooleanTest::fullTree)
.full(false)
.empty(true)
.count(1)
.check();
}
@Test
void testDifference_simpleCrossingCuts() {
// act/assert
differenceChecker(AbstractRegionBSPTreeBooleanTest::xAxisTree, AbstractRegionBSPTreeBooleanTest::emptyTree)
.full(false)
.empty(false)
.inside(new TestPoint2D(0, 1))
.outside(new TestPoint2D(0, -1))
.boundary(TestPoint2D.ZERO)
.count(3)
.check();
differenceChecker(AbstractRegionBSPTreeBooleanTest::emptyTree, AbstractRegionBSPTreeBooleanTest::xAxisTree)
.full(false)
.empty(true)
.count(1)
.check();
differenceChecker(AbstractRegionBSPTreeBooleanTest::yAxisTree, AbstractRegionBSPTreeBooleanTest::fullTree)
.full(false)
.empty(true)
.count(1)
.check();
differenceChecker(AbstractRegionBSPTreeBooleanTest::fullTree, AbstractRegionBSPTreeBooleanTest::yAxisTree)
.full(false)
.empty(false)
.inside(new TestPoint2D(1, 1), new TestPoint2D(1, -1))
.outside(new TestPoint2D(-1, 1), new TestPoint2D(-1, -1))
.boundary(new TestPoint2D(0, 1), new TestPoint2D(0, -1))
.count(3)
.check();
differenceChecker(AbstractRegionBSPTreeBooleanTest::xAxisTree, AbstractRegionBSPTreeBooleanTest::yAxisTree)
.full(false)
.empty(false)
.inside(new TestPoint2D(1, 1))
.outside(new TestPoint2D(-1, 1), new TestPoint2D(1, -1), new TestPoint2D(-1, -1))
.boundary(TestPoint2D.ZERO)
.count(5)
.check(t -> {
final TestLineSegment seg = (TestLineSegment) t.getRoot().getMinus().getCut();
PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, seg.getStartPoint());
PartitionTestUtils.assertPointsEqual(new TestPoint2D(0.0, Double.POSITIVE_INFINITY), seg.getEndPoint());
});
}
@Test
void testDifference_boxTreeWithSingleCutTree() {
// arrange
final Supplier<TestRegionBSPTree> boxFactory = () -> {
final TestRegionBSPTree box = fullTree();
insertBox(box, TestPoint2D.ZERO, new TestPoint2D(2, -4));
return box;
};
final Supplier<TestRegionBSPTree> horizontalFactory = () -> {
final TestRegionBSPTree horizontal = fullTree();
horizontal.getRoot().insertCut(new TestLine(new TestPoint2D(2, -2), new TestPoint2D(0, -2)));
return horizontal;
};
// act/assert
differenceChecker(horizontalFactory, boxFactory)
.inside(new TestPoint2D(-1, -3), new TestPoint2D(-1, -5),
new TestPoint2D(1, -5), new TestPoint2D(3, -5),
new TestPoint2D(4, -3))
.outside(new TestPoint2D(1, -1), new TestPoint2D(1, -1),
new TestPoint2D(3, -1), new TestPoint2D(1, -3))
.boundary(new TestPoint2D(0, -2), new TestPoint2D(0, -4),
new TestPoint2D(2, -4), new TestPoint2D(2, -2))
.count(9)
.check();
}
@Test
void testDifference_treeWithCopy() {
// arrange
final Supplier<TestRegionBSPTree> treeFactory = () -> {
final TestRegionBSPTree t = fullTree();
insertSkewedBowtie(t);
return t;
};
// act/assert
differenceChecker(treeFactory, treeFactory)
.full(false)
.empty(true)
.count(1)
.check();
}
@Test
void testXor_singleNodeTrees() {
// act/assert
xorChecker(AbstractRegionBSPTreeBooleanTest::emptyTree, AbstractRegionBSPTreeBooleanTest::emptyTree)
.full(false)
.empty(true)
.count(1)
.check();
xorChecker(AbstractRegionBSPTreeBooleanTest::fullTree, AbstractRegionBSPTreeBooleanTest::emptyTree)
.full(true)
.empty(false)
.count(1)
.check();
xorChecker(AbstractRegionBSPTreeBooleanTest::emptyTree, AbstractRegionBSPTreeBooleanTest::fullTree)
.full(true)
.empty(false)
.count(1)
.check();
xorChecker(AbstractRegionBSPTreeBooleanTest::fullTree, AbstractRegionBSPTreeBooleanTest::fullTree)
.full(false)
.empty(true)
.count(1)
.check();
}
@Test
void testXor_simpleCrossingCuts() {
// act/assert
xorChecker(AbstractRegionBSPTreeBooleanTest::xAxisTree, AbstractRegionBSPTreeBooleanTest::emptyTree)
.full(false)
.empty(false)
.inside(new TestPoint2D(0, 1))
.outside(new TestPoint2D(0, -1))
.boundary(TestPoint2D.ZERO)
.count(3)
.check();
xorChecker(AbstractRegionBSPTreeBooleanTest::emptyTree, AbstractRegionBSPTreeBooleanTest::xAxisTree)
.full(false)
.empty(false)
.inside(new TestPoint2D(0, 1))
.outside(new TestPoint2D(0, -1))
.boundary(TestPoint2D.ZERO)
.count(3)
.check();
xorChecker(AbstractRegionBSPTreeBooleanTest::yAxisTree, AbstractRegionBSPTreeBooleanTest::fullTree)
.full(false)
.empty(false)
.inside(new TestPoint2D(1, 1), new TestPoint2D(1, -1))
.outside(new TestPoint2D(-1, 1), new TestPoint2D(-1, -1))
.boundary(new TestPoint2D(0, 1), new TestPoint2D(0, -1))
.count(3)
.check();
xorChecker(AbstractRegionBSPTreeBooleanTest::fullTree, AbstractRegionBSPTreeBooleanTest::yAxisTree)
.full(false)
.empty(false)
.inside(new TestPoint2D(1, 1), new TestPoint2D(1, -1))
.outside(new TestPoint2D(-1, 1), new TestPoint2D(-1, -1))
.boundary(new TestPoint2D(0, 1), new TestPoint2D(0, -1))
.count(3)
.check();
xorChecker(AbstractRegionBSPTreeBooleanTest::xAxisTree, AbstractRegionBSPTreeBooleanTest::yAxisTree)
.full(false)
.empty(false)
.inside(new TestPoint2D(1, 1), new TestPoint2D(-1, -1))
.outside(new TestPoint2D(-1, 1), new TestPoint2D(1, -1))
.boundary(TestPoint2D.ZERO)
.count(7)
.check(t -> {
final TestLineSegment minusSeg = (TestLineSegment) t.getRoot().getMinus().getCut();
PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, minusSeg.getStartPoint());
PartitionTestUtils.assertPointsEqual(new TestPoint2D(0.0, Double.POSITIVE_INFINITY), minusSeg.getEndPoint());
final TestLineSegment plusSeg = (TestLineSegment) t.getRoot().getPlus().getCut();
PartitionTestUtils.assertPointsEqual(new TestPoint2D(0.0, Double.NEGATIVE_INFINITY), plusSeg.getStartPoint());
PartitionTestUtils.assertPointsEqual(TestPoint2D.ZERO, plusSeg.getEndPoint());
});
}
@Test
void testXor_boxTreeWithSingleCutTree() {
// arrange
final Supplier<TestRegionBSPTree> boxFactory = () -> {
final TestRegionBSPTree box = fullTree();
insertBox(box, TestPoint2D.ZERO, new TestPoint2D(2, -4));
return box;
};
final Supplier<TestRegionBSPTree> horizontalFactory = () -> {
final TestRegionBSPTree horizontal = fullTree();
horizontal.getRoot().insertCut(new TestLine(new TestPoint2D(2, -2), new TestPoint2D(0, -2)));
return horizontal;
};
// act/assert
xorChecker(horizontalFactory, boxFactory)
.inside(new TestPoint2D(-1, -3), new TestPoint2D(-1, -5),
new TestPoint2D(1, -5), new TestPoint2D(3, -5),
new TestPoint2D(4, -3), new TestPoint2D(1, -1))
.outside(new TestPoint2D(3, -1), new TestPoint2D(1, -3),
new TestPoint2D(1, 1), new TestPoint2D(5, -1))
.boundary(new TestPoint2D(0, -2), new TestPoint2D(0, -4),
new TestPoint2D(2, -4), new TestPoint2D(2, -2),
TestPoint2D.ZERO, new TestPoint2D(2, 0))
.count(15)
.check();
}
@Test
void testXor_treeWithComplement() {
// arrange
final Supplier<TestRegionBSPTree> treeFactory = () -> {
final TestRegionBSPTree t = fullTree();
insertSkewedBowtie(t);
return t;
};
final Supplier<TestRegionBSPTree> complementFactory = () -> {
final TestRegionBSPTree t = treeFactory.get();
t.complement();
return t;
};
// act/assert
xorChecker(treeFactory, complementFactory)
.full(true)
.empty(false)
.count(1)
.check();
}
private static TestRegionBSPTree emptyTree() {
return new TestRegionBSPTree(false);
}
private static TestRegionBSPTree fullTree() {
return new TestRegionBSPTree(true);
}
private static TestRegionBSPTree xAxisTree() {
final TestRegionBSPTree tree = fullTree();
tree.getRoot().cut(TestLine.X_AXIS);
return tree;
}
private static TestRegionBSPTree yAxisTree() {
final TestRegionBSPTree tree = fullTree();
tree.getRoot().cut(TestLine.Y_AXIS);
return tree;
}
private static void insertBox(final TestRegionBSPTree tree, final TestPoint2D upperLeft,
final TestPoint2D lowerRight) {
final TestPoint2D upperRight = new TestPoint2D(lowerRight.getX(), upperLeft.getY());
final TestPoint2D lowerLeft = new TestPoint2D(upperLeft.getX(), lowerRight.getY());
tree.insert(Arrays.asList(
new TestLineSegment(lowerRight, upperRight),
new TestLineSegment(upperRight, upperLeft),
new TestLineSegment(upperLeft, lowerLeft),
new TestLineSegment(lowerLeft, lowerRight)
));
}
private static void insertSkewedBowtie(final TestRegionBSPTree tree) {
tree.insert(Arrays.asList(
new TestLineSegment(TestPoint2D.ZERO, new TestPoint2D(1, 0)),
new TestLineSegment(new TestPoint2D(4, 0), new TestPoint2D(4, 1)),
new TestLineSegment(new TestPoint2D(-4, 0), new TestPoint2D(-4, -1)),
new TestLineSegment(new TestPoint2D(4, 5), new TestPoint2D(-1, 0)),
new TestLineSegment(new TestPoint2D(-4, -5), new TestPoint2D(1, 0))));
}
private static MergeChecker unionChecker(
final Supplier<TestRegionBSPTree> r1,
final Supplier<TestRegionBSPTree> r2) {
final MergeChecker.Operation constOperation = (a, b) -> {
final TestRegionBSPTree result = fullTree();
result.union(a, b);
return result;
};
final MergeChecker.Operation inPlaceOperation = (a, b) -> {
a.union(b);
return a;
};
return new MergeChecker(r1, r2, constOperation, inPlaceOperation);
}
private static MergeChecker intersectionChecker(
final Supplier<TestRegionBSPTree> tree1Factory,
final Supplier<TestRegionBSPTree> tree2Factory) {
final MergeChecker.Operation constOperation = (a, b) -> {
final TestRegionBSPTree result = fullTree();
result.intersection(a, b);
return result;
};
final MergeChecker.Operation inPlaceOperation = (a, b) -> {
a.intersection(b);
return a;
};
return new MergeChecker(tree1Factory, tree2Factory, constOperation, inPlaceOperation);
}
private static MergeChecker differenceChecker(
final Supplier<TestRegionBSPTree> tree1Factory,
final Supplier<TestRegionBSPTree> tree2Factory) {
final MergeChecker.Operation constOperation = (a, b) -> {
final TestRegionBSPTree result = fullTree();
result.difference(a, b);
return result;
};
final MergeChecker.Operation inPlaceOperation = (a, b) -> {
a.difference(b);
return a;
};
return new MergeChecker(tree1Factory, tree2Factory, constOperation, inPlaceOperation);
}
private static MergeChecker xorChecker(
final Supplier<TestRegionBSPTree> tree1Factory,
final Supplier<TestRegionBSPTree> tree2Factory) {
final MergeChecker.Operation constOperation = (a, b) -> {
final TestRegionBSPTree result = fullTree();
result.xor(a, b);
return result;
};
final MergeChecker.Operation inPlaceOperation = (a, b) -> {
a.xor(b);
return a;
};
return new MergeChecker(tree1Factory, tree2Factory, constOperation, inPlaceOperation);
}
}