diff --git a/src/test/java/sc/fiji/snt/analysis/PathStatisticsTest.java b/src/test/java/sc/fiji/snt/analysis/PathStatisticsTest.java new file mode 100644 index 000000000..678197eae --- /dev/null +++ b/src/test/java/sc/fiji/snt/analysis/PathStatisticsTest.java @@ -0,0 +1,190 @@ +/*- + * #%L + * Fiji distribution of ImageJ for the life sciences. + * %% + * Copyright (C) 2010 - 2026 Fiji developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +package sc.fiji.snt.analysis; + +import static org.junit.Assert.*; +import static org.junit.Assume.assumeNotNull; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import sc.fiji.snt.Path; +import sc.fiji.snt.SNTService; +import sc.fiji.snt.Tree; +import sc.fiji.snt.util.PointInImage; + +/** + * Tests for {@link PathStatistics}. + */ +public class PathStatisticsTest { + + private static final double DELTA = 1e-6; + private Tree tree; + private Path primaryPath; + + @Before + public void setUp() { + tree = new SNTService().demoTree("fractal"); + assumeNotNull(tree); + // Get the primary (root) path + final TreeStatistics tStats = new TreeStatistics(tree); + final List primaries = tStats.getPrimaryPaths(); + assumeNotNull(primaries); + assertFalse(primaries.isEmpty()); + primaryPath = primaries.get(0); + } + + @Test + public void testConstructor_singlePath() { + final PathStatistics stats = new PathStatistics(primaryPath); + assertNotNull(stats); + assertEquals(1, stats.getNBranches()); + } + + @Test + public void testConstructor_pathCollection() { + final PathStatistics stats = new PathStatistics(tree.list(), "test"); + assertNotNull(stats); + assertEquals(tree.size(), stats.getNBranches()); + } + + @Test + public void testGetBranches_count() { + final PathStatistics stats = new PathStatistics(tree.list(), "test"); + assertEquals(tree.size(), stats.getBranches().size()); + } + + @Test + public void testGetNBranches() { + final PathStatistics stats = new PathStatistics(primaryPath); + assertEquals(1, stats.getNBranches()); + } + + @Test + public void testGetMetric_pathId_singlePath() throws UnknownMetricException { + final PathStatistics stats = new PathStatistics(primaryPath); + final Number id = stats.getMetric("Path ID"); + assertNotNull(id); + assertEquals(primaryPath.getID(), id.intValue()); + } + + @Test + public void testGetMetric_pathId_multiPath() throws UnknownMetricException { + final PathStatistics stats = new PathStatistics(tree.list(), "test"); + final Number id = stats.getMetric("Path ID"); + assertTrue("Multi-path should return NaN for Path ID", Double.isNaN(id.doubleValue())); + } + + @Test + public void testGetMetric_byPath_length() throws UnknownMetricException { + final PathStatistics stats = new PathStatistics(primaryPath); + final Number length = stats.getMetric(PathStatistics.PATH_LENGTH, primaryPath); + assertNotNull(length); + assertEquals(primaryPath.getLength(), length.doubleValue(), DELTA); + } + + @Test + public void testGetMetric_byPath_nNodes() throws UnknownMetricException { + final PathStatistics stats = new PathStatistics(primaryPath); + final Number nNodes = stats.getMetric(PathStatistics.N_PATH_NODES, primaryPath); + assertNotNull(nNodes); + assertEquals(primaryPath.size(), nNodes.intValue()); + } + + @Test + public void testGetMetric_byPath_nChildren() throws UnknownMetricException { + final PathStatistics stats = new PathStatistics(primaryPath); + final Number nChildren = stats.getMetric(PathStatistics.N_CHILDREN, primaryPath); + assertNotNull(nChildren); + assertEquals(primaryPath.getChildren().size(), nChildren.intValue()); + } + + @Test + public void testGetPrimaryBranches_containsPrimary() { + final PathStatistics stats = new PathStatistics(tree.list(), "test"); + final List primaries = stats.getPrimaryBranches(); + assertNotNull(primaries); + assertTrue(primaries.contains(primaryPath)); + } + + @Test + public void testGetTerminalBranches_nonEmpty() { + final PathStatistics stats = new PathStatistics(tree.list(), "test"); + final List terminals = stats.getTerminalBranches(); + assertNotNull(terminals); + // In PathStatistics, terminal branches are those with children + for (final Path p : terminals) { + assertFalse("Terminal branch should have children", p.getChildren().isEmpty()); + } + } + + @Test + public void testGetInnerBranches_samePrimary() { + final PathStatistics stats = new PathStatistics(tree.list(), "test"); + assertEquals(stats.getPrimaryBranches(), stats.getInnerBranches()); + } + + @Test + public void testGetPrimaryLength() { + final PathStatistics stats = new PathStatistics(tree.list(), "test"); + final double primaryLen = stats.getPrimaryLength(); + assertTrue("Primary length should be >= 0", primaryLen >= 0); + } + + @Test + public void testGetTerminalLength() { + final PathStatistics stats = new PathStatistics(tree.list(), "test"); + final double terminalLen = stats.getTerminalLength(); + assertTrue("Terminal length should be >= 0", terminalLen >= 0); + } + + @Test + public void testGetInnerLength_equalsGetPrimaryLength() { + final PathStatistics stats = new PathStatistics(tree.list(), "test"); + assertEquals(stats.getPrimaryLength(), stats.getInnerLength(), DELTA); + } + + @Test + public void testGetCableLength_multiPath() { + final PathStatistics stats = new PathStatistics(tree.list(), "test"); + final double totalLength = tree.list().stream().mapToDouble(Path::getLength).sum(); + assertEquals("Cable length should equal sum of path lengths", totalLength, stats.getCableLength(), DELTA); + } + + @Test + public void testSinglePathCableLength_matchesPathLength() { + final PathStatistics stats = new PathStatistics(primaryPath); + assertEquals(primaryPath.getLength(), stats.getCableLength(), DELTA); + } + + @Test + public void testManualPathWithKnownLength() { + final Path p = new Path(1.0, 1.0, 1.0, "um"); + p.addNode(new PointInImage(0, 0, 0)); + p.addNode(new PointInImage(3, 4, 0)); + final PathStatistics stats = new PathStatistics(p); + assertEquals(5.0, stats.getCableLength(), DELTA); + } +} diff --git a/src/test/java/sc/fiji/snt/analysis/StrahlerAnalyzerTest.java b/src/test/java/sc/fiji/snt/analysis/StrahlerAnalyzerTest.java new file mode 100644 index 000000000..52073dba1 --- /dev/null +++ b/src/test/java/sc/fiji/snt/analysis/StrahlerAnalyzerTest.java @@ -0,0 +1,209 @@ +/*- + * #%L + * Fiji distribution of ImageJ for the life sciences. + * %% + * Copyright (C) 2010 - 2026 Fiji developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +package sc.fiji.snt.analysis; + +import static org.junit.Assert.*; +import static org.junit.Assume.assumeNotNull; + +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import sc.fiji.snt.Path; +import sc.fiji.snt.SNTService; +import sc.fiji.snt.Tree; + +/** + * Tests for {@link StrahlerAnalyzer}. + */ +public class StrahlerAnalyzerTest { + + private static final double DELTA = 1e-6; + private Tree tree; + private StrahlerAnalyzer analyzer; + + @Before + public void setUp() { + tree = new SNTService().demoTree("fractal"); + assumeNotNull(tree); + analyzer = new StrahlerAnalyzer(tree); + } + + @Test + public void testGetRootNumber() { + final int rootNumber = analyzer.getRootNumber(); + assertTrue("Root number should be >= 1", rootNumber >= 1); + // For the fractal demo tree we know the Strahler number is 5 + assertEquals(5, rootNumber); + } + + @Test + public void testGetHighestBranchOrder() { + final int highest = analyzer.getHighestBranchOrder(); + assertTrue("Highest branch order should be >= 1", highest >= 1); + assertTrue("Highest branch order should be <= root number", highest <= analyzer.getRootNumber()); + } + + @Test + public void testGetBranchCounts_keysMatchOrders() { + final Map counts = analyzer.getBranchCounts(); + assertFalse("Branch counts map should not be empty", counts.isEmpty()); + for (final int order : counts.keySet()) { + assertTrue("Order should be >= 1", order >= 1); + assertTrue("Order should be <= root number", order <= analyzer.getRootNumber()); + } + } + + @Test + public void testGetBranchCounts_valuesPositive() { + final Map counts = analyzer.getBranchCounts(); + for (final double count : counts.values()) { + assertTrue("Branch count should be >= 0", count >= 0); + } + } + + @Test + public void testGetLengths_keysMatchOrders() { + final Map lengths = analyzer.getLengths(); + assertFalse("Lengths map should not be empty", lengths.isEmpty()); + for (final int order : lengths.keySet()) { + assertTrue("Order should be >= 1", order >= 1); + } + } + + @Test + public void testGetLengths_sumEqualsOrLessThanCableLength() { + final Map lengths = analyzer.getLengths(); + final double sumLength = lengths.values().stream().mapToDouble(d -> d).sum(); + final double cableLength = new TreeStatistics(tree).getCableLength(); + // The sum of per-order cable lengths should be <= total cable length + // (some edges between different orders are not counted in per-order sums) + assertTrue("Sum of per-order lengths should be <= cable length", sumLength <= cableLength + DELTA); + } + + @Test + public void testGetBranchPointCounts_keysMatchOrders() { + final Map bpCounts = analyzer.getBranchPointCounts(); + assertFalse("Branch point counts map should not be empty", bpCounts.isEmpty()); + for (final int order : bpCounts.keySet()) { + assertTrue("Order should be >= 1", order >= 1); + } + } + + @Test + public void testGetBranchPointCounts_valuesNonNegative() { + final Map bpCounts = analyzer.getBranchPointCounts(); + for (final double count : bpCounts.values()) { + assertTrue("Branch point count should be >= 0", count >= 0); + } + } + + @Test + public void testGetBifurcationRatios_containsNaN() { + final Map ratios = analyzer.getBifurcationRatios(); + // The highest order should have NaN ratio (no higher order to compare with) + final int highest = analyzer.getHighestBranchOrder(); + assertTrue("Highest order bifurcation ratio should be NaN", + Double.isNaN(ratios.get(highest))); + } + + @Test + public void testGetBifurcationRatios_positiveForLowerOrders() { + final Map ratios = analyzer.getBifurcationRatios(); + final int highest = analyzer.getHighestBranchOrder(); + for (final Map.Entry entry : ratios.entrySet()) { + if (entry.getKey() < highest) { + assertFalse("Non-highest order ratio should not be NaN", Double.isNaN(entry.getValue())); + assertTrue("Bifurcation ratio should be > 0", entry.getValue() > 0); + } + } + } + + @Test + public void testGetAvgBifurcationRatio_fractalTree() { + // For a perfect binary fractal tree the bifurcation ratio is 2 + final double avgRatio = analyzer.getAvgBifurcationRatio(); + assertFalse("Average bifurcation ratio should not be NaN", Double.isNaN(avgRatio)); + assertEquals("Expected bifurcation ratio of 2 for fractal tree", 2.0, avgRatio, DELTA); + } + + @Test + public void testGetBranches_allOrders() { + final Map> branches = analyzer.getBranches(); + assertFalse("Branches map should not be empty", branches.isEmpty()); + for (final Map.Entry> entry : branches.entrySet()) { + assertNotNull("Branch list should not be null", entry.getValue()); + } + } + + @Test + public void testGetBranches_byOrder_validRange() { + final int highest = analyzer.getHighestBranchOrder(); + for (int order = 1; order <= highest; order++) { + final List branches = analyzer.getBranches(order); + assertNotNull("Branches should not be null for order " + order, branches); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testGetBranches_invalidOrder_zero() { + analyzer.getBranches(0); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetBranches_invalidOrder_tooHigh() { + analyzer.getBranches(analyzer.getHighestBranchOrder() + 1); + } + + @Test + public void testGetAvgFragmentations_allOrders() { + final Map frags = analyzer.getAvgFragmentations(); + assertFalse("Fragmentations map should not be empty", frags.isEmpty()); + } + + @Test + public void testGetAvgContractions_allOrders() { + final Map contractions = analyzer.getAvgContractions(); + assertFalse("Contractions map should not be empty", contractions.isEmpty()); + } + + @Test + public void testGetGraph_notNull() { + assertNotNull("Graph should not be null", analyzer.getGraph()); + } + + @Test + public void testGetRootAssociatedBranches_notNull() { + final List rootBranches = analyzer.getRootAssociatedBranches(); + assertNotNull("Root-associated branches list should not be null", rootBranches); + } + + @Test + public void testOrderConsistencyWithTreeStatistics() { + // The Strahler root number should match TreeStatistics.getStrahlerNumber() + final int strahlerFromStats = new TreeStatistics(tree).getStrahlerNumber(); + assertEquals("Strahler numbers should match", strahlerFromStats, analyzer.getRootNumber()); + } +} diff --git a/src/test/java/sc/fiji/snt/util/BoundingBoxTest.java b/src/test/java/sc/fiji/snt/util/BoundingBoxTest.java new file mode 100644 index 000000000..5c5b2d61d --- /dev/null +++ b/src/test/java/sc/fiji/snt/util/BoundingBoxTest.java @@ -0,0 +1,474 @@ +/*- + * #%L + * Fiji distribution of ImageJ for the life sciences. + * %% + * Copyright (C) 2010 - 2026 Fiji developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +package sc.fiji.snt.util; + +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +/** + * Tests for {@link BoundingBox}. + */ +public class BoundingBoxTest { + + private static final double DELTA = 1e-9; + + @Test + public void testDefaultConstructor_nanDimensions() { + final BoundingBox bb = new BoundingBox(); + assertFalse(bb.hasDimensions()); + assertTrue(Double.isNaN(bb.origin().x)); + assertTrue(Double.isNaN(bb.originOpposite().x)); + } + + @Test + public void testDefaultSpacing() { + final BoundingBox bb = new BoundingBox(); + assertEquals(1.0, bb.xSpacing, DELTA); + assertEquals(1.0, bb.ySpacing, DELTA); + assertEquals(1.0, bb.zSpacing, DELTA); + } + + @Test + public void testConstructorFromPoints() { + final List pts = Arrays.asList( + new PointInImage(1, 2, 3), + new PointInImage(4, 6, 9), + new PointInImage(2, 0, 5) + ); + final BoundingBox bb = new BoundingBox(pts); + assertEquals(1.0, bb.origin().x, DELTA); + assertEquals(0.0, bb.origin().y, DELTA); + assertEquals(3.0, bb.origin().z, DELTA); + assertEquals(4.0, bb.originOpposite().x, DELTA); + assertEquals(6.0, bb.originOpposite().y, DELTA); + assertEquals(9.0, bb.originOpposite().z, DELTA); + } + + @Test + public void testCompute() { + final BoundingBox bb = new BoundingBox(); + final List pts = Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 5, 2) + ); + bb.compute(pts.iterator()); + assertEquals(0.0, bb.origin().x, DELTA); + assertEquals(10.0, bb.originOpposite().x, DELTA); + assertEquals(5.0, bb.originOpposite().y, DELTA); + } + + @Test + public void testAppend_empty() { + final BoundingBox bb = new BoundingBox(); + final List pts = Arrays.asList( + new PointInImage(2, 3, 4), + new PointInImage(8, 7, 6) + ); + bb.append(pts.iterator()); + assertEquals(2.0, bb.origin().x, DELTA); + assertEquals(8.0, bb.originOpposite().x, DELTA); + } + + @Test + public void testAppend_extends() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(5, 5, 5) + )); + final List more = Arrays.asList( + new PointInImage(-1, 10, 3) + ); + bb.append(more.iterator()); + assertEquals(-1.0, bb.origin().x, DELTA); + assertEquals(10.0, bb.originOpposite().y, DELTA); + } + + @Test + public void testHasDimensions_true() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(1, 1, 1) + )); + assertTrue(bb.hasDimensions()); + } + + @Test + public void testHasDimensions_singlePoint() { + final BoundingBox bb = new BoundingBox(Collections.singletonList( + new PointInImage(5, 5, 5) + )); + assertFalse(bb.hasDimensions()); + } + + @Test + public void testWidth() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(2, 3, 4), + new PointInImage(7, 8, 9) + )); + assertEquals(5.0, bb.width(), DELTA); + } + + @Test + public void testHeight() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(2, 3, 4), + new PointInImage(7, 8, 9) + )); + assertEquals(5.0, bb.height(), DELTA); + } + + @Test + public void testDepth() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(2, 3, 4), + new PointInImage(7, 8, 9) + )); + assertEquals(5.0, bb.depth(), DELTA); + } + + @Test + public void testGetDimensions_scaled() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 20, 30) + )); + final double[] dims = bb.getDimensions(true); + assertEquals(10.0, dims[0], DELTA); + assertEquals(20.0, dims[1], DELTA); + assertEquals(30.0, dims[2], DELTA); + } + + @Test + public void testGetDiagonal() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(3, 4, 0) + )); + assertEquals(5.0, bb.getDiagonal(), DELTA); + } + + @Test + public void testGetCentroid() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 10, 10) + )); + final SNTPoint centroid = bb.getCentroid(); + assertEquals(5.0, centroid.getX(), DELTA); + assertEquals(5.0, centroid.getY(), DELTA); + assertEquals(5.0, centroid.getZ(), DELTA); + } + + @Test + public void testCombine() { + final BoundingBox bb1 = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(5, 5, 5) + )); + final BoundingBox bb2 = new BoundingBox(Arrays.asList( + new PointInImage(3, 3, 3), + new PointInImage(10, 10, 10) + )); + bb1.combine(bb2); + assertEquals(0.0, bb1.origin().x, DELTA); + assertEquals(10.0, bb1.originOpposite().x, DELTA); + } + + @Test + public void testIntersection() { + final BoundingBox bb1 = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 10, 10) + )); + final BoundingBox bb2 = new BoundingBox(Arrays.asList( + new PointInImage(5, 5, 5), + new PointInImage(15, 15, 15) + )); + final BoundingBox inter = bb1.intersection(bb2); + assertEquals(5.0, inter.origin().x, DELTA); + assertEquals(10.0, inter.originOpposite().x, DELTA); + } + + @Test + public void testContainsBoundingBox_true() { + final BoundingBox outer = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 10, 10) + )); + final BoundingBox inner = new BoundingBox(Arrays.asList( + new PointInImage(2, 2, 2), + new PointInImage(8, 8, 8) + )); + assertTrue(outer.contains(inner)); + } + + @Test + public void testContainsBoundingBox_false() { + final BoundingBox outer = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(5, 5, 5) + )); + final BoundingBox outside = new BoundingBox(Arrays.asList( + new PointInImage(3, 3, 3), + new PointInImage(10, 10, 10) + )); + assertFalse(outer.contains(outside)); + } + + @Test + public void testContainsPoint_inside() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 10, 10) + )); + assertTrue(bb.contains(new PointInImage(5, 5, 5))); + } + + @Test + public void testContainsPoint_outside() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 10, 10) + )); + assertFalse(bb.contains(new PointInImage(11, 5, 5))); + } + + @Test + public void testContainsPoint_onBoundary() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 10, 10) + )); + assertTrue(bb.contains(new PointInImage(0, 0, 0))); + assertTrue(bb.contains(new PointInImage(10, 10, 10))); + } + + @Test + public void testContains2D_inside() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 10, 10) + )); + assertTrue(bb.contains2D(new PointInImage(5, 5, 999))); + } + + @Test + public void testContains2D_outsideZ_stillTrue() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 10, 10) + )); + // contains2D ignores Z, so far-out Z should still return true + assertTrue(bb.contains2D(new PointInImage(5, 5, 9999))); + } + + @Test + public void testSetSpacing() { + final BoundingBox bb = new BoundingBox(); + bb.setSpacing(0.5, 0.5, 1.0, "um"); + assertEquals(0.5, bb.xSpacing, DELTA); + assertEquals(0.5, bb.ySpacing, DELTA); + assertEquals(1.0, bb.zSpacing, DELTA); + } + + @Test + public void testIsScaled_afterSetSpacing() { + final BoundingBox bb = new BoundingBox(); + assertFalse(bb.isScaled()); + bb.setSpacing(0.5, 0.5, 1.0, "um"); + assertTrue(bb.isScaled()); + } + + @Test + public void testIsScaled_defaultNotScaled() { + final BoundingBox bb = new BoundingBox(); + assertFalse(bb.isScaled()); + } + + @Test + public void testSetDimensions() { + final BoundingBox bb = new BoundingBox(); + bb.setOrigin(new PointInImage(0, 0, 0)); + bb.setSpacing(2, 2, 2, "um"); + bb.setDimensions(5, 5, 5); + assertEquals(10.0, bb.originOpposite().x, DELTA); + assertEquals(10.0, bb.originOpposite().y, DELTA); + assertEquals(10.0, bb.originOpposite().z, DELTA); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetDimensions_noOrigin() { + final BoundingBox bb = new BoundingBox(); + bb.setDimensions(5, 5, 5); + } + + @Test + public void testSanitizedUnit_null() { + assertEquals("? units", BoundingBox.sanitizedUnit(null)); + } + + @Test + public void testSanitizedUnit_pixel() { + assertEquals("? units", BoundingBox.sanitizedUnit("pixels")); + } + + @Test + public void testSanitizedUnit_empty() { + assertEquals("? units", BoundingBox.sanitizedUnit(" ")); + } + + @Test + public void testSanitizedUnit_um() { + final String result = BoundingBox.sanitizedUnit("um"); + assertNotNull(result); + assertFalse(result.isEmpty()); + } + + @Test + public void testSanitizedUnit_micron() { + final String result = BoundingBox.sanitizedUnit("micron"); + assertEquals(BoundingBox.sanitizedUnit("um"), result); + } + + @Test + public void testSanitizedUnit_custom() { + assertEquals("mm", BoundingBox.sanitizedUnit("mm")); + } + + @Test + public void testScale() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(2, 4, 6), + new PointInImage(4, 8, 12) + )); + final BoundingBox scaled = bb.scale(new double[]{2, 2, 2}); + assertEquals(4.0, scaled.origin().x, DELTA); + assertEquals(8.0, scaled.origin().y, DELTA); + assertEquals(8.0, scaled.originOpposite().x, DELTA); + } + + @Test + public void testShift() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(5, 5, 5) + )); + final BoundingBox shifted = bb.shift(new double[]{10, 20, 30}); + assertEquals(10.0, shifted.origin().x, DELTA); + assertEquals(20.0, shifted.origin().y, DELTA); + assertEquals(30.0, shifted.origin().z, DELTA); + assertEquals(15.0, shifted.originOpposite().x, DELTA); + } + + @Test + public void testEquals_sameBounds() { + final BoundingBox bb1 = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 10, 10) + )); + final BoundingBox bb2 = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 10, 10) + )); + assertEquals(bb1, bb2); + } + + @Test + public void testEquals_differentBounds() { + final BoundingBox bb1 = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 10, 10) + )); + final BoundingBox bb2 = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(11, 10, 10) + )); + assertNotEquals(bb1, bb2); + } + + @Test + public void testHashCode_equalObjects() { + final BoundingBox bb1 = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 10, 10) + )); + final BoundingBox bb2 = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 10, 10) + )); + assertEquals(bb1.hashCode(), bb2.hashCode()); + } + + @Test + public void testClone() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(1, 2, 3), + new PointInImage(4, 5, 6) + )); + bb.setSpacing(0.5, 0.5, 1.0, "um"); + final BoundingBox clone = bb.clone(); + assertNotSame(bb, clone); + assertEquals(bb, clone); + } + + @Test + public void testUnscaledOrigin() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(2, 4, 6), + new PointInImage(10, 10, 10) + )); + bb.setSpacing(2, 2, 2, "um"); + final PointInImage unscaled = bb.unscaledOrigin(); + assertEquals(1.0, unscaled.x, DELTA); + assertEquals(2.0, unscaled.y, DELTA); + assertEquals(3.0, unscaled.z, DELTA); + } + + @Test + public void testUnscaledOriginOpposite() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(10, 20, 30) + )); + bb.setSpacing(2, 4, 6, "um"); + final PointInImage unscaled = bb.unscaledOriginOpposite(); + assertEquals(5.0, unscaled.x, DELTA); + assertEquals(5.0, unscaled.y, DELTA); + assertEquals(5.0, unscaled.z, DELTA); + } + + @Test + public void testToString_notNull() { + final BoundingBox bb = new BoundingBox(Arrays.asList( + new PointInImage(0, 0, 0), + new PointInImage(1, 1, 1) + )); + assertNotNull(bb.toString()); + } +} diff --git a/src/test/java/sc/fiji/snt/util/LinAlgUtilsTest.java b/src/test/java/sc/fiji/snt/util/LinAlgUtilsTest.java new file mode 100644 index 000000000..080eb1d94 --- /dev/null +++ b/src/test/java/sc/fiji/snt/util/LinAlgUtilsTest.java @@ -0,0 +1,151 @@ +/*- + * #%L + * Fiji distribution of ImageJ for the life sciences. + * %% + * Copyright (C) 2010 - 2026 Fiji developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +package sc.fiji.snt.util; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Tests for {@link LinAlgUtils}. + */ +public class LinAlgUtilsTest { + + private static final double DELTA = 1e-9; + + @Test + public void testReflectionMatrix_xyPlane() { + // Reflect across the XY plane (z=0). Normal = (0,0,1), point = (0,0,0) + final double[] planePoint = {0, 0, 0}; + final double[] planeNormal = {0, 0, 1}; + final double[][] M = LinAlgUtils.reflectionMatrix(planePoint, planeNormal); + + // Reflecting (0,0,1) through z=0 should give (0,0,-1) + // M is 4x4 homogeneous; apply to (0,0,1,1) + final double x = M[0][0]*0 + M[0][1]*0 + M[0][2]*1 + M[0][3]*1; + final double y = M[1][0]*0 + M[1][1]*0 + M[1][2]*1 + M[1][3]*1; + final double z = M[2][0]*0 + M[2][1]*0 + M[2][2]*1 + M[2][3]*1; + + assertEquals(0.0, x, DELTA); + assertEquals(0.0, y, DELTA); + assertEquals(-1.0, z, DELTA); + } + + @Test + public void testReflectionMatrix_xzPlane() { + // Reflect across the XZ plane (y=0). Normal = (0,1,0), point = (0,0,0) + final double[] planePoint = {0, 0, 0}; + final double[] planeNormal = {0, 1, 0}; + final double[][] M = LinAlgUtils.reflectionMatrix(planePoint, planeNormal); + + // Reflecting (0,1,0) through y=0 should give (0,-1,0) + final double x = M[0][0]*0 + M[0][1]*1 + M[0][2]*0 + M[0][3]*1; + final double y = M[1][0]*0 + M[1][1]*1 + M[1][2]*0 + M[1][3]*1; + final double z = M[2][0]*0 + M[2][1]*1 + M[2][2]*0 + M[2][3]*1; + + assertEquals(0.0, x, DELTA); + assertEquals(-1.0, y, DELTA); + assertEquals(0.0, z, DELTA); + } + + @Test + public void testReflectionMatrix_pointOnPlane_unchanged() { + // A point on the plane should be unchanged after reflection + // Plane: z=5, normal = (0,0,1), point = (0,0,5) + final double[] planePoint = {0, 0, 5}; + final double[] planeNormal = {0, 0, 1}; + final double[][] M = LinAlgUtils.reflectionMatrix(planePoint, planeNormal); + + // Point (3,4,5) is on the plane z=5 + final double px = 3, py = 4, pz = 5; + final double rx = M[0][0]*px + M[0][1]*py + M[0][2]*pz + M[0][3]*1; + final double ry = M[1][0]*px + M[1][1]*py + M[1][2]*pz + M[1][3]*1; + final double rz = M[2][0]*px + M[2][1]*py + M[2][2]*pz + M[2][3]*1; + + assertEquals(px, rx, DELTA); + assertEquals(py, ry, DELTA); + assertEquals(pz, rz, DELTA); + } + + @Test + public void testReflectionMatrix_isInvolutory() { + // Reflecting twice should return the original point + final double[] planePoint = {0, 0, 0}; + final double[] planeNormal = {0, 0, 1}; + final double[][] M = LinAlgUtils.reflectionMatrix(planePoint, planeNormal); + + final double px = 2, py = 3, pz = 7; + // First reflection + final double rx1 = M[0][0]*px + M[0][1]*py + M[0][2]*pz + M[0][3]; + final double ry1 = M[1][0]*px + M[1][1]*py + M[1][2]*pz + M[1][3]; + final double rz1 = M[2][0]*px + M[2][1]*py + M[2][2]*pz + M[2][3]; + // Second reflection + final double rx2 = M[0][0]*rx1 + M[0][1]*ry1 + M[0][2]*rz1 + M[0][3]; + final double ry2 = M[1][0]*rx1 + M[1][1]*ry1 + M[1][2]*rz1 + M[1][3]; + final double rz2 = M[2][0]*rx1 + M[2][1]*ry1 + M[2][2]*rz1 + M[2][3]; + + assertEquals(px, rx2, DELTA); + assertEquals(py, ry2, DELTA); + assertEquals(pz, rz2, DELTA); + } + + @Test + public void testReflectionMatrix_matrixSize() { + final double[] planePoint = {0, 0, 0}; + final double[] planeNormal = {0, 0, 1}; + final double[][] M = LinAlgUtils.reflectionMatrix(planePoint, planeNormal); + assertEquals(4, M.length); + for (final double[] row : M) { + assertEquals(4, row.length); + } + } + + @Test + public void testReflectionMatrix_lastRow() { + // Last row of homogeneous matrix should be [0, 0, 0, 1] + final double[] planePoint = {1, 2, 3}; + final double[] planeNormal = {1, 0, 0}; + final double[][] M = LinAlgUtils.reflectionMatrix(planePoint, planeNormal); + assertEquals(0.0, M[3][0], DELTA); + assertEquals(0.0, M[3][1], DELTA); + assertEquals(0.0, M[3][2], DELTA); + assertEquals(1.0, M[3][3], DELTA); + } + + @Test + public void testReflectionMatrix_yzPlane() { + // Reflect across the YZ plane (x=0). Normal = (1,0,0), point = (0,0,0) + final double[] planePoint = {0, 0, 0}; + final double[] planeNormal = {1, 0, 0}; + final double[][] M = LinAlgUtils.reflectionMatrix(planePoint, planeNormal); + + // Reflecting (1,0,0) through x=0 should give (-1,0,0) + final double x = M[0][0]*1 + M[0][1]*0 + M[0][2]*0 + M[0][3]*1; + final double y = M[1][0]*1 + M[1][1]*0 + M[1][2]*0 + M[1][3]*1; + final double z = M[2][0]*1 + M[2][1]*0 + M[2][2]*0 + M[2][3]*1; + + assertEquals(-1.0, x, DELTA); + assertEquals(0.0, y, DELTA); + assertEquals(0.0, z, DELTA); + } +} diff --git a/src/test/java/sc/fiji/snt/util/PointInImageTest.java b/src/test/java/sc/fiji/snt/util/PointInImageTest.java new file mode 100644 index 000000000..88cf5b491 --- /dev/null +++ b/src/test/java/sc/fiji/snt/util/PointInImageTest.java @@ -0,0 +1,264 @@ +/*- + * #%L + * Fiji distribution of ImageJ for the life sciences. + * %% + * Copyright (C) 2010 - 2026 Fiji developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +package sc.fiji.snt.util; + +import static org.junit.Assert.*; + +import org.junit.Test; +import sc.fiji.snt.Tree; + +/** + * Tests for {@link PointInImage}. + */ +public class PointInImageTest { + + private static final double DELTA = 1e-9; + + @Test + public void testDefaultConstructor() { + final PointInImage p = new PointInImage(); + assertEquals(0.0, p.x, DELTA); + assertEquals(0.0, p.y, DELTA); + assertEquals(0.0, p.z, DELTA); + } + + @Test + public void testCoordinateConstructor() { + final PointInImage p = new PointInImage(1.0, 2.0, 3.0); + assertEquals(1.0, p.x, DELTA); + assertEquals(2.0, p.y, DELTA); + assertEquals(3.0, p.z, DELTA); + } + + @Test + public void testDistanceSquaredTo_coordinates() { + final PointInImage p = new PointInImage(0, 0, 0); + assertEquals(14.0, p.distanceSquaredTo(1.0, 2.0, 3.0), DELTA); + } + + @Test + public void testDistanceSquaredTo_point() { + final PointInImage p = new PointInImage(1, 1, 1); + final PointInImage q = new PointInImage(4, 5, 1); + assertEquals(25.0, p.distanceSquaredTo(q), DELTA); + } + + @Test + public void testDistanceTo() { + final PointInImage p = new PointInImage(0, 0, 0); + final PointInImage q = new PointInImage(3, 4, 0); + assertEquals(5.0, p.distanceTo(q), DELTA); + } + + @Test + public void testDistanceTo_samePoint() { + final PointInImage p = new PointInImage(5, 6, 7); + assertEquals(0.0, p.distanceTo(p), DELTA); + } + + @Test + public void testEuclideanDxTo() { + final PointInImage p = new PointInImage(0, 0, 0); + final PointInImage q = new PointInImage(3, 4, 0); + assertEquals(p.distanceTo(q), p.euclideanDxTo(q), DELTA); + } + + @Test + public void testChebyshevXYdxTo() { + final PointInImage p = new PointInImage(0, 0, 0); + final PointInImage q = new PointInImage(3, 7, 100); + assertEquals(7.0, p.chebyshevXYdxTo(q), DELTA); + } + + @Test + public void testChebyshevZdxTo() { + final PointInImage p = new PointInImage(0, 0, 0); + final PointInImage q = new PointInImage(3, 7, 5); + assertEquals(5.0, p.chebyshevZdxTo(q), DELTA); + } + + @Test + public void testChebyshevDxTo() { + final PointInImage p = new PointInImage(0, 0, 0); + final PointInImage q = new PointInImage(3, 7, 10); + // max(max(|3|,|7|), |10|) = 10 + assertEquals(10.0, p.chebyshevDxTo(q), DELTA); + } + + @Test + public void testIsReal_validPoint() { + final PointInImage p = new PointInImage(1, 2, 3); + assertTrue(p.isReal()); + } + + @Test + public void testIsReal_nanX() { + final PointInImage p = new PointInImage(Double.NaN, 2, 3); + assertFalse(p.isReal()); + } + + @Test + public void testIsReal_nanY() { + final PointInImage p = new PointInImage(1, Double.NaN, 3); + assertFalse(p.isReal()); + } + + @Test + public void testIsReal_nanZ() { + final PointInImage p = new PointInImage(1, 2, Double.NaN); + assertFalse(p.isReal()); + } + + @Test + public void testIsReal_infiniteX() { + final PointInImage p = new PointInImage(Double.POSITIVE_INFINITY, 2, 3); + assertFalse(p.isReal()); + } + + @Test + public void testIsReal_zero() { + final PointInImage p = new PointInImage(0, 0, 0); + assertTrue(p.isReal()); + } + + @Test + public void testIsSameLocation_equal() { + final PointInImage p = new PointInImage(1, 2, 3); + final PointInImage q = new PointInImage(1, 2, 3); + assertTrue(p.isSameLocation(q)); + } + + @Test + public void testIsSameLocation_different() { + final PointInImage p = new PointInImage(1, 2, 3); + final PointInImage q = new PointInImage(1, 2, 4); + assertFalse(p.isSameLocation(q)); + } + + @Test + public void testScale() { + final PointInImage p = new PointInImage(2, 3, 4); + p.scale(2, 3, 4); + assertEquals(4.0, p.x, DELTA); + assertEquals(9.0, p.y, DELTA); + assertEquals(16.0, p.z, DELTA); + } + + @Test + public void testScaleByOne_unchanged() { + final PointInImage p = new PointInImage(5, 6, 7); + p.scale(1, 1, 1); + assertEquals(5.0, p.x, DELTA); + assertEquals(6.0, p.y, DELTA); + assertEquals(7.0, p.z, DELTA); + } + + @Test + public void testGetX() { + final PointInImage p = new PointInImage(10, 20, 30); + assertEquals(10.0, p.getX(), DELTA); + } + + @Test + public void testGetY() { + final PointInImage p = new PointInImage(10, 20, 30); + assertEquals(20.0, p.getY(), DELTA); + } + + @Test + public void testGetZ() { + final PointInImage p = new PointInImage(10, 20, 30); + assertEquals(30.0, p.getZ(), DELTA); + } + + @Test + public void testGetCoordinateOnAxis() { + final PointInImage p = new PointInImage(10, 20, 30); + assertEquals(10.0, p.getCoordinateOnAxis(Tree.X_AXIS), DELTA); + assertEquals(20.0, p.getCoordinateOnAxis(Tree.Y_AXIS), DELTA); + assertEquals(30.0, p.getCoordinateOnAxis(Tree.Z_AXIS), DELTA); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetCoordinateOnAxis_invalidAxis() { + final PointInImage p = new PointInImage(1, 2, 3); + p.getCoordinateOnAxis(99); + } + + @Test + public void testEquals_sameObject() { + final PointInImage p = new PointInImage(1, 2, 3); + assertEquals(p, p); + } + + @Test + public void testEquals_equalPoints() { + final PointInImage p = new PointInImage(1, 2, 3); + final PointInImage q = new PointInImage(1, 2, 3); + assertEquals(p, q); + } + + @Test + public void testEquals_differentPoints() { + final PointInImage p = new PointInImage(1, 2, 3); + final PointInImage q = new PointInImage(1, 2, 4); + assertNotEquals(p, q); + } + + @Test + public void testEquals_null() { + final PointInImage p = new PointInImage(1, 2, 3); + assertNotEquals(p, null); + } + + @Test + public void testHashCode_consistency() { + final PointInImage p = new PointInImage(1, 2, 3); + assertEquals(p.hashCode(), p.hashCode()); + } + + @Test + public void testHashCode_equalObjects() { + final PointInImage p = new PointInImage(1, 2, 3); + final PointInImage q = new PointInImage(1, 2, 3); + assertEquals(p.hashCode(), q.hashCode()); + } + + @Test + public void testClone() { + final PointInImage p = new PointInImage(1, 2, 3); + final PointInImage clone = p.clone(); + assertNotSame(p, clone); + assertEquals(p.x, clone.x, DELTA); + assertEquals(p.y, clone.y, DELTA); + assertEquals(p.z, clone.z, DELTA); + } + + @Test + public void testClone_independence() { + final PointInImage p = new PointInImage(1, 2, 3); + final PointInImage clone = p.clone(); + clone.x = 99; + assertEquals(1.0, p.x, DELTA); + } +} diff --git a/src/test/java/sc/fiji/snt/util/SNTColorTest.java b/src/test/java/sc/fiji/snt/util/SNTColorTest.java new file mode 100644 index 000000000..000f1c977 --- /dev/null +++ b/src/test/java/sc/fiji/snt/util/SNTColorTest.java @@ -0,0 +1,371 @@ +/*- + * #%L + * Fiji distribution of ImageJ for the life sciences. + * %% + * Copyright (C) 2010 - 2026 Fiji developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +package sc.fiji.snt.util; + +import static org.junit.Assert.*; + +import java.awt.Color; +import java.util.Arrays; + +import org.junit.Test; +import org.scijava.util.ColorRGB; + +/** + * Tests for {@link SNTColor}. + */ +public class SNTColorTest { + + private static final double DELTA = 1.0; + + // ---- SNTColor instance tests ---- + + @Test + public void testConstructor_withSWCType() { + final SNTColor c = new SNTColor(Color.RED, 2); + assertEquals(Color.RED, c.color()); + assertEquals(2, c.type()); + assertTrue(c.isTypeDefined()); + } + + @Test + public void testConstructor_withoutSWCType() { + final SNTColor c = new SNTColor(Color.BLUE); + assertEquals(Color.BLUE, c.color()); + assertEquals(SNTColor.SWC_TYPE_IGNORED, c.type()); + assertFalse(c.isTypeDefined()); + } + + @Test + public void testSetAWTColor() { + final SNTColor c = new SNTColor(Color.RED); + c.setAWTColor(Color.GREEN); + assertEquals(Color.GREEN, c.color()); + } + + @Test + public void testSetSWCType() { + final SNTColor c = new SNTColor(Color.RED); + c.setSWCType(3); + assertEquals(3, c.type()); + assertTrue(c.isTypeDefined()); + } + + @Test + public void testEquals_sameColorAndType() { + final SNTColor c1 = new SNTColor(Color.RED, 2); + final SNTColor c2 = new SNTColor(Color.RED, 2); + assertEquals(c1, c2); + } + + @Test + public void testEquals_differentType() { + final SNTColor c1 = new SNTColor(Color.RED, 2); + final SNTColor c2 = new SNTColor(Color.RED, 3); + assertNotEquals(c1, c2); + } + + @Test + public void testEquals_differentColor() { + final SNTColor c1 = new SNTColor(Color.RED, 2); + final SNTColor c2 = new SNTColor(Color.BLUE, 2); + assertNotEquals(c1, c2); + } + + @Test + public void testHashCode_equalObjects() { + final SNTColor c1 = new SNTColor(Color.RED, 2); + final SNTColor c2 = new SNTColor(Color.RED, 2); + assertEquals(c1.hashCode(), c2.hashCode()); + } + + // ---- colorToString tests ---- + + @Test + public void testColorToString_null() { + assertEquals("null color", SNTColor.colorToString(null)); + } + + @Test + public void testColorToString_awtColor() { + final String result = SNTColor.colorToString(Color.RED); + assertNotNull(result); + assertTrue(result.startsWith("#")); + assertEquals(9, result.length()); // #rrggbbaa + } + + @Test + public void testColorToString_awtColorBlack() { + assertEquals("#000000ff", SNTColor.colorToString(Color.BLACK)); + } + + @Test + public void testColorToString_awtColorWhite() { + assertEquals("#ffffffff", SNTColor.colorToString(Color.WHITE)); + } + + @Test + public void testColorToString_colorRGB() { + final ColorRGB c = new ColorRGB(255, 0, 0); + final String result = SNTColor.colorToString(c); + assertNotNull(result); + assertTrue(result.startsWith("#")); + } + + // ---- fromHex tests ---- + + @Test + public void testFromHex_sixDigit() { + final Color c = SNTColor.fromHex("#ff0000"); + assertEquals(255, c.getRed()); + assertEquals(0, c.getGreen()); + assertEquals(0, c.getBlue()); + } + + @Test + public void testFromHex_eightDigit() { + final Color c = SNTColor.fromHex("#ff000080"); + assertEquals(255, c.getRed()); + assertEquals(0, c.getGreen()); + assertEquals(0, c.getBlue()); + assertEquals(128, c.getAlpha(), DELTA); + } + + @Test + public void testFromHex_noHash() { + final Color c = SNTColor.fromHex("00ff00"); + assertEquals(0, c.getRed()); + assertEquals(255, c.getGreen()); + assertEquals(0, c.getBlue()); + } + + @Test + public void testFromHex_colorName() { + // CSS / named color strings should also work + final Color c = SNTColor.fromHex("blue"); + assertNotNull(c); + assertEquals(0, c.getRed()); + // "blue" should have a non-zero blue channel + assertTrue("Blue channel should be positive for 'blue'", c.getBlue() > 0); + } + + @Test + public void testFromString_sameAsFromHex() { + final Color c1 = SNTColor.fromString("#aabbcc"); + final Color c2 = SNTColor.fromHex("#aabbcc"); + assertEquals(c1, c2); + } + + // ---- average tests ---- + + @Test + public void testAverage_empty() { + final Color avg = SNTColor.average(Arrays.asList()); + assertEquals(Color.BLACK, avg); + } + + @Test + public void testAverage_null() { + final Color avg = SNTColor.average(null); + assertEquals(Color.BLACK, avg); + } + + @Test + public void testAverage_singleColor() { + final Color avg = SNTColor.average(Arrays.asList(new Color(100, 150, 200))); + assertEquals(100, avg.getRed()); + assertEquals(150, avg.getGreen()); + assertEquals(200, avg.getBlue()); + } + + @Test + public void testAverage_twoColors() { + final Color avg = SNTColor.average(Arrays.asList( + new Color(0, 0, 0), + new Color(100, 200, 255) + )); + assertEquals(50, avg.getRed()); + assertEquals(100, avg.getGreen()); + assertEquals(127, avg.getBlue(), DELTA); + } + + @Test + public void testAverage_withNullEntries() { + // null entries in the collection should be skipped + final Color avg = SNTColor.average(Arrays.asList(null, new Color(100, 0, 0), null)); + assertEquals(100, avg.getRed()); + } + + // ---- interpolateNullEntries tests ---- + + @Test + public void testInterpolateNullEntries_noNulls() { + final Color[] colors = {Color.RED, Color.GREEN, Color.BLUE}; + SNTColor.interpolateNullEntries(colors); + assertEquals(Color.RED, colors[0]); + assertEquals(Color.GREEN, colors[1]); + assertEquals(Color.BLUE, colors[2]); + } + + @Test + public void testInterpolateNullEntries_nullInMiddle() { + final Color[] colors = {Color.BLACK, null, Color.WHITE}; + SNTColor.interpolateNullEntries(colors); + assertNotNull(colors[1]); + // The middle should be an average of black (0,0,0) and white (255,255,255) + assertEquals(127, colors[1].getRed(), DELTA); + } + + @Test + public void testInterpolateNullEntries_nullArray() { + // should not throw + SNTColor.interpolateNullEntries(null); + } + + @Test + public void testInterpolateNullEntries_allNull() { + final Color[] colors = {null, null, null}; + SNTColor.interpolateNullEntries(colors); + // All flanking colors are null, so result is black + for (final Color c : colors) { + assertEquals(Color.BLACK, c); + } + } + + // ---- alphaColor tests ---- + + @Test + public void testAlphaColor_fullOpacity() { + final Color result = SNTColor.alphaColor(Color.RED, 100.0); + assertEquals(255, result.getAlpha()); + assertEquals(Color.RED.getRed(), result.getRed()); + } + + @Test + public void testAlphaColor_halfOpacity() { + final Color result = SNTColor.alphaColor(Color.RED, 50.0); + assertEquals(128, result.getAlpha(), DELTA); + } + + @Test + public void testAlphaColor_transparent() { + final Color result = SNTColor.alphaColor(Color.RED, 0.0); + assertEquals(0, result.getAlpha()); + } + + @Test + public void testAlphaColor_nullInput() { + assertNull(SNTColor.alphaColor(null, 100.0)); + } + + // ---- contrastColor tests ---- + + @Test + public void testContrastColor_black() { + assertEquals(Color.WHITE, SNTColor.contrastColor(Color.BLACK)); + } + + @Test + public void testContrastColor_white() { + assertEquals(Color.BLACK, SNTColor.contrastColor(Color.WHITE)); + } + + @Test + public void testContrastColor_dark() { + // Dark color should return white + final Color dark = new Color(50, 50, 50); + assertEquals(Color.WHITE, SNTColor.contrastColor(dark)); + } + + @Test + public void testContrastColor_light() { + // Light color should return black + final Color light = new Color(200, 200, 200); + assertEquals(Color.BLACK, SNTColor.contrastColor(light)); + } + + // ---- getDistinctColors tests ---- + + @Test + public void testGetDistinctColors_count() { + final ColorRGB[] colors = SNTColor.getDistinctColors(5); + assertEquals(5, colors.length); + } + + @Test + public void testGetDistinctColors_moreThanKelly() { + // Kelly has 20 colors; requesting more should repeat them + final ColorRGB[] colors = SNTColor.getDistinctColors(25); + assertEquals(25, colors.length); + assertNotNull(colors[24]); + } + + @Test + public void testGetDistinctColors_single() { + final ColorRGB[] colors = SNTColor.getDistinctColors(1); + assertEquals(1, colors.length); + assertNotNull(colors[0]); + } + + @Test + public void testGetDistinctColorsHex_format() { + final String[] hexColors = SNTColor.getDistinctColorsHex(3); + assertEquals(3, hexColors.length); + for (final String hex : hexColors) { + assertNotNull(hex); + assertTrue("Expected hex string: " + hex, hex.startsWith("#")); + } + } + + @Test + public void testGetDistinctColorsAWT_count() { + final Color[] colors = SNTColor.getDistinctColorsAWT(4); + assertEquals(4, colors.length); + for (final Color c : colors) { + assertNotNull(c); + } + } + + @Test + public void testGetDistinctColors_excludedHue_red() { + final ColorRGB[] colors = SNTColor.getDistinctColors(5, "red"); + assertEquals(5, colors.length); + } + + @Test + public void testGetDistinctColors_excludedHue_green() { + final ColorRGB[] colors = SNTColor.getDistinctColors(5, "green"); + assertEquals(5, colors.length); + } + + @Test + public void testGetDistinctColors_excludedHue_blue() { + final ColorRGB[] colors = SNTColor.getDistinctColors(5, "blue"); + assertEquals(5, colors.length); + } + + @Test + public void testGetDistinctColors_excludedHue_dim() { + final ColorRGB[] colors = SNTColor.getDistinctColors(5, "dim"); + assertEquals(5, colors.length); + } +}