diff --git a/.github/build.sh b/.github/build.sh index 523abeb..9be6895 100755 --- a/.github/build.sh +++ b/.github/build.sh @@ -1,3 +1,5 @@ #!/bin/sh +export DISPLAY=:99 +sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/main/ci-build.sh sh ci-build.sh diff --git a/src/main/java/org/mastodon/mamut/tomancak/divisioncount/SpotAndDivisionCount.java b/src/main/java/org/mastodon/mamut/tomancak/divisioncount/SpotAndDivisionCount.java index 90ae99e..3b6a93b 100644 --- a/src/main/java/org/mastodon/mamut/tomancak/divisioncount/SpotAndDivisionCount.java +++ b/src/main/java/org/mastodon/mamut/tomancak/divisioncount/SpotAndDivisionCount.java @@ -32,8 +32,11 @@ import java.util.List; import org.apache.commons.lang3.tuple.Triple; +import org.mastodon.mamut.ProjectModel; +import org.mastodon.mamut.model.Link; import org.mastodon.mamut.model.Model; import org.mastodon.mamut.model.Spot; +import org.mastodon.model.SelectionModel; import org.mastodon.util.TreeUtils; public class SpotAndDivisionCount @@ -46,23 +49,33 @@ private SpotAndDivisionCount() /** * Calculates the number of spots and divisions per time point in the given model. * - * @param model The model containing the spots and edges. + * @param projectModel The project model containing the spots and edges. + * @param onlySelectedSpots If {@code true}, only counts spots that are selected. * @return A list of triples, where each triple contains (in that order) the timepoint, the number of spots at that timepoint, and the number of divisions at that timepoint. */ - public static List< Triple< Integer, Integer, Integer > > getSpotAndDivisionsPerTimepoint( final Model model ) + public static List< Triple< Integer, Integer, Integer > > getSpotAndDivisionsPerTimepoint( final ProjectModel projectModel, + final boolean onlySelectedSpots ) { + final Model model = projectModel.getModel(); + final SelectionModel< Spot, Link > selectionModel = projectModel.getSelectionModel(); int minTimepoint = TreeUtils.getMinTimepoint( model ); int maxTimepoint = TreeUtils.getMaxTimepoint( model ); List< Triple< Integer, Integer, Integer > > timepointAndDivisions = new ArrayList<>(); for ( int timepoint = minTimepoint; timepoint <= maxTimepoint; timepoint++ ) { + int spots = 0; int divisions = 0; for ( Spot spot : model.getSpatioTemporalIndex().getSpatialIndex( timepoint ) ) { + if ( onlySelectedSpots && !selectionModel.isSelected( spot ) ) + continue; if ( spot.outgoingEdges().size() > 1 ) divisions++; + if ( onlySelectedSpots ) + spots++; } - int spots = model.getSpatioTemporalIndex().getSpatialIndex( timepoint ).size(); + if ( !onlySelectedSpots ) + spots = model.getSpatioTemporalIndex().getSpatialIndex( timepoint ).size(); timepointAndDivisions.add( Triple.of( timepoint, spots, divisions ) ); } return timepointAndDivisions; diff --git a/src/main/java/org/mastodon/mamut/tomancak/divisioncount/SpotAndDivisionCountChart.java b/src/main/java/org/mastodon/mamut/tomancak/divisioncount/SpotAndDivisionCountChart.java index ede59b7..e486213 100644 --- a/src/main/java/org/mastodon/mamut/tomancak/divisioncount/SpotAndDivisionCountChart.java +++ b/src/main/java/org/mastodon/mamut/tomancak/divisioncount/SpotAndDivisionCountChart.java @@ -38,12 +38,14 @@ import java.awt.geom.Rectangle2D; import java.util.List; +import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JColorChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JRadioButton; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import javax.swing.WindowConstants; @@ -62,9 +64,10 @@ import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; import org.mastodon.mamut.ProjectModel; +import org.mastodon.model.SelectionListener; import org.scijava.prefs.PrefService; -public class SpotAndDivisionCountChart extends JFrame +public class SpotAndDivisionCountChart extends JFrame implements SelectionListener { private static final String SPOT_COLOR = "spotColor"; @@ -83,19 +86,25 @@ public class SpotAndDivisionCountChart extends JFrame private static final String DIVISION_COUNT_SLIDING_AVERAGE_WINDOW_SIZE = "divisionCountSlidingAverageWindowSize"; + private static final String ONLY_SELECTED_SPOTS = "onlySelectedSpots"; + private static final int SPOT_COUNT_DEFAULT_COLOR = new Color( 230, 159, 0 ).getRGB(); // Dark Orange private static final int DIVISION_COUNT_DEFAULT_COLOR = new Color( 86, 180, 233 ).getRGB(); // Light Blue private static final int DEFAULT_SLIDING_WINDOW_SIZE = 10; + public static final String GROW_X = "growx"; + private Color spotCountColor; // Dark Orange private Color divisionCountColor; // Light Blue - private final static String TITLE = "Spot and Division Counts over Time"; - private final static String SPOTS_COUNT_SERIES_NAME = "Spot Counts"; - private final static String DIVISION_COUNT_SERIES_NAME = "Division Counts"; + private static final String TITLE = "Spot and Division Counts over Time"; + + private static final String SPOTS_COUNT_SERIES_NAME = "Spot Counts"; + + private static final String DIVISION_COUNT_SERIES_NAME = "Division Counts"; private final XYPlot plot; @@ -107,11 +116,20 @@ public class SpotAndDivisionCountChart extends JFrame private int divisionWindowSize; + private boolean onlySelectedSpots; + private final PrefService prefs; - SpotAndDivisionCountChart( final double[] timepoints, final double[] spotCounts, final double[] divisionCounts, - final PrefService prefs ) + private final ProjectModel projectModel; + + private XYSeriesCollection spotCountsSeries; + + private XYSeriesCollection divisionCountsSeries; + + SpotAndDivisionCountChart( final ProjectModel projectModel, final PrefService prefs ) { + this.projectModel = projectModel; + this.projectModel.getSelectionModel().listeners().add( this ); this.prefs = prefs; this.spotCountColor = new Color( @@ -122,12 +140,9 @@ public class SpotAndDivisionCountChart extends JFrame prefs.getInt( SpotAndDivisionCountChart.class, SPOT_COUNT_SLIDING_AVERAGE_WINDOW_SIZE, DEFAULT_SLIDING_WINDOW_SIZE ); this.divisionWindowSize = prefs.getInt( SpotAndDivisionCountChart.class, DIVISION_COUNT_SLIDING_AVERAGE_WINDOW_SIZE, DEFAULT_SLIDING_WINDOW_SIZE ); + this.onlySelectedSpots = prefs.getBoolean( SpotAndDivisionCountChart.class, ONLY_SELECTED_SPOTS, false ); - XYSeriesCollection spotCountsSeries = createSeries( - timepoints, spotCounts, SPOTS_COUNT_SERIES_NAME, spotWindowSize ); - - XYSeriesCollection divisionCountsSeries = createSeries( - timepoints, divisionCounts, DIVISION_COUNT_SERIES_NAME, divisionWindowSize ); + updateChartData(); JFreeChart chart = ChartFactory.createXYLineChart( TITLE, @@ -168,7 +183,7 @@ public class SpotAndDivisionCountChart extends JFrame chartPanel.setPreferredSize( new Dimension( 800, 600 ) ); // Add color chooser and visibility controls - JPanel controlPanel = createControlPanel( timepoints, spotCounts, divisionCounts ); + JPanel controlPanel = createControlPanel(); // Set up the frame layout setLayout( new BorderLayout() ); @@ -184,15 +199,26 @@ public class SpotAndDivisionCountChart extends JFrame repaint(); } - public static void show( final ProjectModel projectModel, final PrefService prefService ) + private void updateChartData() { - List< Triple< Integer, Integer, Integer > > divisionCounts = - SpotAndDivisionCount.getSpotAndDivisionsPerTimepoint( projectModel.getModel() ); - double[] timepoints = divisionCounts.stream().mapToDouble( Triple::getLeft ).toArray(); - double[] spots = divisionCounts.stream().mapToDouble( Triple::getMiddle ).toArray(); - double[] divisions = divisionCounts.stream().mapToDouble( Triple::getRight ).toArray(); + List< Triple< Integer, Integer, Integer > > counts = + SpotAndDivisionCount.getSpotAndDivisionsPerTimepoint( projectModel, this.onlySelectedSpots ); + double[] timepoints = counts.stream().mapToDouble( Triple::getLeft ).toArray(); + double[] spotCounts = counts.stream().mapToDouble( Triple::getMiddle ).toArray(); + double[] divisionCounts = counts.stream().mapToDouble( Triple::getRight ).toArray(); + spotCountsSeries = createSeries( timepoints, spotCounts, SPOTS_COUNT_SERIES_NAME, spotWindowSize ); + divisionCountsSeries = createSeries( timepoints, divisionCounts, DIVISION_COUNT_SERIES_NAME, divisionWindowSize ); + + if ( plot != null ) + { + plot.setDataset( 0, spotCountsSeries ); + plot.setDataset( 1, divisionCountsSeries ); + } + } - SpotAndDivisionCountChart chart = new SpotAndDivisionCountChart( timepoints, spots, divisions, prefService ); + public static void show( final ProjectModel projectModel, final PrefService prefService ) + { + SpotAndDivisionCountChart chart = new SpotAndDivisionCountChart( projectModel, prefService ); chart.setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE ); chart.setVisible( true ); } @@ -200,7 +226,7 @@ public static void show( final ProjectModel projectModel, final PrefService pref /** * Creates a control panel with color choosers and checkboxes for visibility controls. */ - private JPanel createControlPanel( final double[] timepoints, final double[] spotCounts, final double[] divisionCounts ) + private JPanel createControlPanel() { JPanel controlPanel = new JPanel( new MigLayout( "fill, wrap 5", "[grow]", "[]10[]10[]10[]10[]" ) ); @@ -237,7 +263,7 @@ private JPanel createControlPanel( final double[] timepoints, final double[] spo spotWindowSpinner.addChangeListener( e -> { spotWindowSize = ( int ) spotWindowSpinner.getValue(); prefs.put( SpotAndDivisionCountChart.class, SPOT_COUNT_SLIDING_AVERAGE_WINDOW_SIZE, spotWindowSize ); - updateSlidingAverage( timepoints, spotCounts, divisionCounts ); + updateChartData(); } ); // Division-related controls @@ -273,22 +299,43 @@ private JPanel createControlPanel( final double[] timepoints, final double[] spo divisionWindowSpinner.addChangeListener( e -> { divisionWindowSize = ( int ) divisionWindowSpinner.getValue(); prefs.put( SpotAndDivisionCountChart.class, DIVISION_COUNT_SLIDING_AVERAGE_WINDOW_SIZE, divisionWindowSize ); - updateSlidingAverage( timepoints, spotCounts, divisionCounts ); + updateChartData(); + } ); + + JRadioButton allSpots = new JRadioButton( "All spots", !onlySelectedSpots ); + JRadioButton selectedSpots = new JRadioButton( "Only selected spots", onlySelectedSpots ); + ButtonGroup group = new ButtonGroup(); + group.add( allSpots ); + group.add( selectedSpots ); + allSpots.addActionListener( e -> { + onlySelectedSpots = false; + prefs.put( SpotAndDivisionCountChart.class, ONLY_SELECTED_SPOTS, onlySelectedSpots ); + updateChartData(); + repaint(); + } ); + selectedSpots.addActionListener( e -> { + this.onlySelectedSpots = true; + prefs.put( SpotAndDivisionCountChart.class, ONLY_SELECTED_SPOTS, onlySelectedSpots ); + updateChartData(); + repaint(); } ); // Add components to the control panel - controlPanel.add( spotColorButton, "growx" ); - controlPanel.add( showSpotCounts, "growx" ); - controlPanel.add( showSpotAverage, "growx" ); + controlPanel.add( spotColorButton, GROW_X ); + controlPanel.add( showSpotCounts, GROW_X ); + controlPanel.add( showSpotAverage, GROW_X ); controlPanel.add( new JLabel( "Window Size:" ), "align right" ); controlPanel.add( spotWindowSpinner, "wmax 50" ); - controlPanel.add( divisionColorButton, "growx" ); - controlPanel.add( showDivisionCounts, "growx" ); - controlPanel.add( showDivisionAverage, "growx" ); + controlPanel.add( divisionColorButton, GROW_X ); + controlPanel.add( showDivisionCounts, GROW_X ); + controlPanel.add( showDivisionAverage, GROW_X ); controlPanel.add( new JLabel( "Window Size:" ), "align right" ); controlPanel.add( divisionWindowSpinner, "wmax 50" ); + controlPanel.add( allSpots, GROW_X ); + controlPanel.add( selectedSpots, "span, growx" ); + // Add description controlPanel.add( new JLabel( "This windows shows the number of spots and divisions at each timepoint together with a sliding average.
A division is defined as a spot with more than one outgoing edge." ), @@ -331,27 +378,23 @@ private void updateChartColors() */ private void updateAxisVisibility() { - XYItemRenderer spotCountRenderer = plot.getRenderer( 0 ); - Boolean spotCountVisible = spotCountRenderer.getSeriesVisible( 0 ); - Boolean spotAverageVisible = spotCountRenderer.getSeriesVisible( 1 ); - boolean isSpotCountVisible = spotCountVisible != null && spotCountVisible; - boolean isSpotAverageVisible = spotAverageVisible != null && spotAverageVisible; - boolean showSpotAxis = isSpotCountVisible || isSpotAverageVisible; - leftAxis.setLabel( showSpotAxis ? SPOTS_COUNT_SERIES_NAME : null ); - leftAxis.setVisible( showSpotAxis ); - - XYItemRenderer divisionCountRenderer = plot.getRenderer( 1 ); - Boolean divisionCountVisible = divisionCountRenderer.getSeriesVisible( 0 ); - Boolean divisionAverageVisible = divisionCountRenderer.getSeriesVisible( 1 ); - boolean isDivisionCountVisible = divisionCountVisible != null && divisionCountVisible; - boolean isDivisionAverageVisible = divisionAverageVisible != null && divisionAverageVisible; - boolean showDivisionAxis = isDivisionCountVisible || isDivisionAverageVisible; - rightAxis.setLabel( showDivisionAxis ? DIVISION_COUNT_SERIES_NAME : null ); - rightAxis.setVisible( showDivisionAxis ); - + updateAxisVisibility( plot.getRenderer( 0 ), leftAxis, SPOTS_COUNT_SERIES_NAME ); + updateAxisVisibility( plot.getRenderer( 1 ), rightAxis, DIVISION_COUNT_SERIES_NAME ); repaint(); } + private void updateAxisVisibility( final XYItemRenderer renderer, final NumberAxis axis, + final String labelIfVisible ) + { + Boolean countVisible = renderer.getSeriesVisible( 0 ); + Boolean averageVisible = renderer.getSeriesVisible( 1 ); + boolean isCountVisible = countVisible != null && countVisible; + boolean isAverageVisible = averageVisible != null && averageVisible; + boolean showAxis = isCountVisible || isAverageVisible; + axis.setLabel( showAxis ? labelIfVisible : null ); + axis.setVisible( showAxis ); + } + private XYLineAndShapeRenderer createRenderer( final Color color, final Shape shape ) { XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(); @@ -406,20 +449,6 @@ private static XYSeriesCollection createSeries( final double[] xValues, final do return dataset; } - /** - * Updates the sliding average series based on the new window sizes. - */ - private void updateSlidingAverage( double[] timepoints, double[] spotCounts, double[] divisionCounts ) - { - XYSeriesCollection spotCountsSeries = createSeries( - timepoints, spotCounts, SPOTS_COUNT_SERIES_NAME, spotWindowSize ); - XYSeriesCollection divisionCountsSeries = createSeries( - timepoints, divisionCounts, DIVISION_COUNT_SERIES_NAME, divisionWindowSize ); - - plot.setDataset( 0, spotCountsSeries ); - plot.setDataset( 1, divisionCountsSeries ); - } - private static double[] calculateSlidingAverage( double[] values, int windowSize ) { double[] result = new double[ values.length ]; @@ -433,4 +462,10 @@ private static double[] calculateSlidingAverage( double[] values, int windowSize } return result; } + + @Override + public void selectionChanged() + { + updateChartData(); + } } diff --git a/src/main/java/org/mastodon/mamut/tomancak/export/ExportDivisionCountsPerTimepointCommand.java b/src/main/java/org/mastodon/mamut/tomancak/export/ExportDivisionCountsPerTimepointCommand.java index 3d28b94..85fbb0b 100644 --- a/src/main/java/org/mastodon/mamut/tomancak/export/ExportDivisionCountsPerTimepointCommand.java +++ b/src/main/java/org/mastodon/mamut/tomancak/export/ExportDivisionCountsPerTimepointCommand.java @@ -37,7 +37,6 @@ import org.apache.commons.lang3.tuple.Triple; import org.mastodon.mamut.ProjectModel; -import org.mastodon.mamut.model.Model; import org.mastodon.mamut.tomancak.divisioncount.SpotAndDivisionCount; import org.scijava.Context; import org.scijava.ItemVisibility; @@ -75,7 +74,7 @@ public void run() { try { - writeDivisionCountsToFile( projectModel.getModel(), saveTo, context.service( StatusService.class ) ); + writeDivisionCountsToFile( projectModel, saveTo, context.service( StatusService.class ) ); } catch ( IOException e ) { @@ -93,7 +92,8 @@ public void run() *
  • The first line is the header.
  • * */ - public static void writeDivisionCountsToFile( final Model model, final File file, final StatusService statusService ) throws IOException + public static void writeDivisionCountsToFile( final ProjectModel projectModel, final File file, final StatusService statusService ) + throws IOException { if ( file == null ) throw new IllegalArgumentException( "Cannot write division counts to file. Given file is null." ); @@ -102,7 +102,7 @@ public static void writeDivisionCountsToFile( final Model model, final File file { csvWriter.writeNext( new String[] { "timepoint", "divisions" } ); List< Triple< Integer, Integer, Integer > > timepointAndDivisions = - SpotAndDivisionCount.getSpotAndDivisionsPerTimepoint( model ); + SpotAndDivisionCount.getSpotAndDivisionsPerTimepoint( projectModel, false ); for ( Triple< Integer, Integer, Integer > pair : timepointAndDivisions ) { csvWriter.writeNext( new String[] { String.valueOf( pair.getLeft() ), String.valueOf( pair.getRight() ) }, false ); diff --git a/src/test/java/org/mastodon/mamut/tomancak/export/ExportDivisionCountsPerTimepointCommandTest.java b/src/test/java/org/mastodon/mamut/tomancak/export/ExportDivisionCountsPerTimepointCommandTest.java index a102253..d4f8378 100644 --- a/src/test/java/org/mastodon/mamut/tomancak/export/ExportDivisionCountsPerTimepointCommandTest.java +++ b/src/test/java/org/mastodon/mamut/tomancak/export/ExportDivisionCountsPerTimepointCommandTest.java @@ -34,8 +34,14 @@ import java.io.IOException; import java.nio.charset.Charset; +import net.imglib2.img.Img; +import net.imglib2.img.array.ArrayImgs; +import net.imglib2.type.numeric.real.FloatType; + import org.apache.commons.io.FileUtils; import org.junit.Test; +import org.mastodon.mamut.ProjectModel; +import org.mastodon.mamut.ProjectModelTestUtils; import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph2; import org.mastodon.mamut.model.Model; import org.scijava.Context; @@ -52,12 +58,16 @@ public void testWriteDivisionCountsToFile() throws IOException { try (Context context = new Context()) { + final Img< FloatType > img = ArrayImgs.floats( 1, 1, 1 ); Model model = new ExampleGraph2().getModel(); + File mastodonFile = File.createTempFile( "test", ".mastodon" ); + ProjectModel appModel = ProjectModelTestUtils.wrapAsAppModel( img, model, context, mastodonFile ); + File outputFile = File.createTempFile( "divisioncounts", ".csv" ); outputFile.deleteOnExit(); StatusService service = context.service( StatusService.class ); - ExportDivisionCountsPerTimepointCommand.writeDivisionCountsToFile( model, outputFile, service ); + ExportDivisionCountsPerTimepointCommand.writeDivisionCountsToFile( appModel, outputFile, service ); String content = FileUtils.readFileToString( outputFile, Charset.defaultCharset() ); String expected = "\"timepoint\",\"divisions\"\n"