package gov.nasa.arc.mct.fastplot.bridge; import java.awt.Shape; import java.util.ArrayList; import java.util.List; import org.testng.Assert; import org.testng.annotations.Test; public class TestPlotLineShapePalette { // The number of samples to be taken along each axis private static final double RESOLUTION = 250.0; // The number of distinct angles (radial slices) to consider private static final int ANGLES = 8; // The ratio at which two slices are considered similar private static final double SIMILAR = 0.75; @Test public void testShapeCount() { Assert.assertTrue(PlotLineShapePalette.getShapeCount() >= PlotConstants.MAX_NUMBER_OF_DATA_ITEMS_ON_A_PLOT); try { PlotLineShapePalette.getShape(PlotLineShapePalette.getShapeCount() + 1); Assert.fail("Expected to hit IllegalArgumentException for excess shapes"); } catch (IllegalArgumentException e) { // Expected! } } /* * To ensure that shapes are visually unique, shapes are compared using a similarity metric. * The shape is considered along ANGLES directions (up, up-right, right, etc) relative to its center of mass. * Samples are taken and assigned to their nearest slice to accumulate counts for each slice. * Shapes are considered dissimilar if the ratio between corresponding slices is less than SIMILAR. * Note that all shapes are sampled at the same resolution, effectively normalizing the accumulated counts * (making them directly comparable between shapes). */ @Test public void testShapeUniqueness() { int count = PlotLineShapePalette.getShapeCount(); List<ShapeProfile> profiles = new ArrayList<ShapeProfile>(); for (int i = 0; i < count; i++) { profiles.add(new ShapeProfile(PlotLineShapePalette.getShape(i))); } // Try all combinations to ensure all shapes are distinct for (int i = 0; i < count; i++) { for (int j = i + 1; j < count; j++) { Assert.assertFalse(same(profiles.get(i), profiles.get(j))); } } } private boolean same(ShapeProfile a, ShapeProfile b) { for (int i = 0; i < ANGLES; i++) { double low = Math.min(a.concentration[i], b.concentration[i]); double high = Math.max(a.concentration[i], b.concentration[i]); if (low / high < SIMILAR) { return false; // Dissimilar within some slice of interest } } return true; } private static class ShapeProfile { private double xMin; private double xMax; private double yMin; private double yMax; private double xAvg; private double yAvg; private int concentration[] = new int[ANGLES]; public ShapeProfile(Shape shape) { for (int i = 0; i < ANGLES; i++) concentration[i] = 0; // Initialize // These may not be realistic min and max bounds double x1 = shape.getBounds().getMinX(); double y1 = shape.getBounds().getMinY(); double x2 = shape.getBounds().getMaxX(); double y2 = shape.getBounds().getMaxY(); double xSum = 0.0; double ySum = 0.0; double count = 0.0; // Find real min, max, and average xMin = x2; xMax = x1; yMin = y2; yMax = y1; for (double x = x1; x < x2; x += (x2-x1)/RESOLUTION) { for (double y = y1; y < y2; y += (y2-y1)/RESOLUTION) { if (shape.contains(x, y)) { xMin = Math.min(x, xMin); yMin = Math.min(y, yMin); xMax = Math.max(x, xMax); yMax = Math.max(y, yMax); xSum += x; ySum += y; count += 1.0; } } } // Center of mass xAvg = xSum / count; yAvg = ySum / count; // Another pass to accumulate the density at various angles for (double x = xMin; x < xMax; x += (xMax-xMin)/RESOLUTION) { for (double y = yMin; y < yMax; y += (yMax-yMin)/RESOLUTION) { if (shape.contains(x, y)) { if (x != xAvg || y != yAvg) { double radians = Math.atan2(y-yAvg,x-xAvg); double rotations = radians / (Math.PI * 2); while (rotations < 0) rotations += 1.0; //No negatives! int slice = ((int) Math.round(rotations * ANGLES)) % ANGLES; concentration[slice]++; } } } } } } }