From d130f3452e60ca2762546ad4d0cb89d41d7d8ff8 Mon Sep 17 00:00:00 2001 From: Divyank Shah Date: Mon, 20 Nov 2023 19:01:40 -0400 Subject: [PATCH 1/7] Introduced explaining variable. --- .idea/misc.xml | 3 +++ .../java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java | 9 +++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 868906cb8..e19e539b2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,5 +8,8 @@ + + \ No newline at end of file diff --git a/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java b/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java index d2fa2950a..2d56e2d8c 100644 --- a/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java +++ b/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java @@ -53,6 +53,7 @@ public class LoadTrackMatePlugIn extends TrackMatePlugIn { + private String pluginTitle = TrackMate.PLUGIN_NAME_STR + " v" + TrackMate.PLUGIN_NAME_VERSION; /** * Loads a TrackMate file in the GUI. @@ -82,13 +83,13 @@ public void run( final String filePath ) file = new File( filePath ); if ( !file.exists() ) { - IJ.error( TrackMate.PLUGIN_NAME_STR + " v" + TrackMate.PLUGIN_NAME_VERSION, + IJ.error( pluginTitle, "Could not find file with path " + filePath + "." ); return; } if ( !file.canRead() ) { - IJ.error( TrackMate.PLUGIN_NAME_STR + " v" + TrackMate.PLUGIN_NAME_VERSION, + IJ.error( pluginTitle, "Could not read file with path " + filePath + "." ); return; } @@ -98,7 +99,7 @@ public void run( final String filePath ) final TmXmlReader reader = createReader( file ); if ( !reader.isReadingOk() ) { - IJ.error( TrackMate.PLUGIN_NAME_STR + " v" + TrackMate.PLUGIN_NAME_VERSION, reader.getErrorMessage() ); + IJ.error( pluginTitle, reader.getErrorMessage() ); return; } @@ -232,7 +233,7 @@ public void run( final String filePath ) } logger2.log( "File loaded on " + TMUtils.getCurrentTimeString() + '\n', Logger.BLUE_COLOR ); - final String welcomeMessage = TrackMate.PLUGIN_NAME_STR + " v" + TrackMate.PLUGIN_NAME_VERSION + '\n'; + final String welcomeMessage = pluginTitle + '\n'; // Log GUI processing start logger2.log( welcomeMessage, Logger.BLUE_COLOR ); logger2.log( "Please note that TrackMate is available through Fiji, and is based on a publication. " From 7fbd189d4ff214f40beafd5cf3b9d204a1b4473d Mon Sep 17 00:00:00 2001 From: Divyank Shah Date: Mon, 20 Nov 2023 19:57:03 -0400 Subject: [PATCH 2/7] Decomposed complex conditional. --- .../fiji/plugin/trackmate/SpotCollection.java | 6 +++++- .../TrackSchemeGraphComponent.java | 20 ++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/SpotCollection.java b/src/main/java/fiji/plugin/trackmate/SpotCollection.java index 6b399b681..f4c38d572 100644 --- a/src/main/java/fiji/plugin/trackmate/SpotCollection.java +++ b/src/main/java/fiji/plugin/trackmate/SpotCollection.java @@ -313,7 +313,7 @@ public void run() final double tval = featureFilter.value; final boolean isAbove = featureFilter.isAbove; - if ( null == val || isAbove && val.compareTo( tval ) < 0 || !isAbove && val.compareTo( tval ) > 0 ) + if (isSpotVisible(val, tval, isAbove)) { shouldNotBeVisible = true; break; @@ -340,6 +340,10 @@ public void run() } } + private boolean isSpotVisible(Double val, double tval, boolean isAbove){ + return (null == val || isAbove && val.compareTo( tval ) < 0 || !isAbove && val.compareTo( tval ) > 0); + } + /** * Returns the closest {@link Spot} to the given location (encoded as a * Spot), contained in the frame frame. If the frame has no diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeGraphComponent.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeGraphComponent.java index 88f6bd687..23d49b517 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeGraphComponent.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeGraphComponent.java @@ -47,6 +47,7 @@ import com.mxgraph.canvas.mxGraphics2DCanvas; import com.mxgraph.model.mxCell; +import com.mxgraph.swing.handler.mxMovePreview; import com.mxgraph.swing.mxGraphComponent; import com.mxgraph.swing.handler.mxGraphHandler; import com.mxgraph.swing.view.mxCellEditor; @@ -55,6 +56,7 @@ import com.mxgraph.util.mxEventObject; import com.mxgraph.util.mxEventSource.mxIEventListener; import com.mxgraph.util.mxPoint; +import com.mxgraph.util.mxRectangle; import com.mxgraph.view.mxCellState; import com.mxgraph.view.mxGraph; import com.mxgraph.view.mxGraphView; @@ -190,7 +192,7 @@ protected mxGraphHandler createGraphHandler() @Override public void mousePressed( final MouseEvent e ) { - if ( graphComponent.isEnabled() && isEnabled() && !e.isConsumed() && !graphComponent.isForceMarqueeEvent( e ) ) + if (shouldStartMove(e, graphComponent)) { cell = graphComponent.getCellAt( e.getX(), e.getY(), false ); initialCell = cell; @@ -219,13 +221,13 @@ public void mousePressed( final MouseEvent e ) @Override public void mouseReleased( final MouseEvent e ) { - if ( graphComponent.isEnabled() && isEnabled() && !e.isConsumed() ) + if (shouldProcessInput(e, graphComponent)) { final mxGraph lGraph = graphComponent.getGraph(); double dx = 0; double dy = 0; - if ( first != null && ( cellBounds != null || movePreview.isActive() ) ) + if (shouldProcessCellBounds(first, cellBounds, movePreview)) { final double scale = lGraph.getView().getScale(); final mxPoint trans = lGraph.getView().getTranslate(); @@ -341,6 +343,18 @@ else if ( isVisible() ) }; } + private boolean shouldStartMove(MouseEvent e, mxGraphComponent graphComponent) { + return graphComponent.isEnabled() && isEnabled() && !e.isConsumed() && !graphComponent.isForceMarqueeEvent(e); + } + + private boolean shouldProcessInput(MouseEvent e, mxGraphComponent graphComponent) { + return graphComponent.isEnabled() && isEnabled() && !e.isConsumed(); + } + + private boolean shouldProcessCellBounds(Point first, mxRectangle cellBounds, mxMovePreview movePreview) { + return first != null && (cellBounds != null || movePreview.isActive()); + } + /** * Override this so as to paint the background with colored rows and * columns. From 0b78f2d26c141d1e9f7a29a79ad0c2679bffb1f3 Mon Sep 17 00:00:00 2001 From: Divyank Shah Date: Mon, 20 Nov 2023 20:00:30 -0400 Subject: [PATCH 3/7] renamed variables with proper self explanatory varble name. --- .../plugin/trackmate/detection/MaskUtils.java | 88 ++++++++----------- 1 file changed, 36 insertions(+), 52 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java b/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java index a926145bd..64a72c219 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java +++ b/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java @@ -102,72 +102,56 @@ public static final < T extends RealType< T > > double otsuThreshold( final Rand return val.getRealDouble(); } - public static final long getThreshold( final Histogram1d< ? > hist ) - { - final long[] histogram = hist.toLongArray(); + public static final long getThreshold(final Histogram1d histogram) { + final long[] intensityHistogram = histogram.toLongArray(); // Otsu's threshold algorithm // C++ code by Jordan Bevik // ported to ImageJ plugin by G.Landini - int k, kStar; // k = the current threshold; kStar = optimal threshold - final int L = histogram.length; // The total intensity of the image - long N1, N; // N1 = # points with intensity <=k; N = total number of - // points - long Sk; // The total intensity for all histogram points <=k - long S; - double BCV, BCVmax; // The current Between Class Variance and maximum - // BCV - double num, denom; // temporary bookkeeping + int currentThreshold, optimalThreshold; // currentThreshold = the current threshold; optimalThreshold = optimal threshold + final int totalIntensity = intensityHistogram.length; // The total intensity of the image + long pointsBelowThreshold, totalPoints; // pointsBelowThreshold = # points with intensity <= currentThreshold; totalPoints = total number of points + long totalIntensityBelowThreshold; // The total intensity for all histogram points <= currentThreshold + long totalIntensityOverall; + double betweenClassVariance, maxBetweenClassVariance; // The current Between Class Variance and maximum BCV + double numerator, denominator; // temporary bookkeeping // Initialize values: - S = 0; - N = 0; - for ( k = 0; k < L; k++ ) - { - S += k * histogram[ k ]; // Total histogram intensity - N += histogram[ k ]; // Total number of data points + totalIntensityOverall = 0; + totalPoints = 0; + for (currentThreshold = 0; currentThreshold < totalIntensity; currentThreshold++) { + totalIntensityOverall += currentThreshold * intensityHistogram[currentThreshold]; // Total histogram intensity + totalPoints += intensityHistogram[currentThreshold]; // Total number of data points } - Sk = 0; - N1 = histogram[ 0 ]; // The entry for zero intensity - BCV = 0; - BCVmax = 0; - kStar = 0; + totalIntensityBelowThreshold = 0; + pointsBelowThreshold = intensityHistogram[0]; // The entry for zero intensity + betweenClassVariance = 0; + maxBetweenClassVariance = 0; + optimalThreshold = 0; // Look at each possible threshold value, // calculate the between-class variance, and decide if it's a max - for ( k = 1; k < L - 1; k++ ) - { // No need to check endpoints k = 0 or k = L-1 - Sk += k * histogram[ k ]; - N1 += histogram[ k ]; - - // The float casting here is to avoid compiler warning about loss of - // precision and - // will prevent overflow in the case of large saturated images - denom = ( double ) ( N1 ) * ( N - N1 ); // Maximum value of denom is - // (N^2)/4 = - // approx. 3E10 - - if ( denom != 0 ) - { - // Float here is to avoid loss of precision when dividing - num = ( ( double ) N1 / N ) * S - Sk; // Maximum value of num = - // 255*N = - // approx 8E7 - BCV = ( num * num ) / denom; + for (currentThreshold = 1; currentThreshold < totalIntensity - 1; currentThreshold++) { + totalIntensityBelowThreshold += currentThreshold * intensityHistogram[currentThreshold]; + pointsBelowThreshold += intensityHistogram[currentThreshold]; + + denominator = (double) (pointsBelowThreshold) * (totalPoints - pointsBelowThreshold); + + if (denominator != 0) { + numerator = ((double) pointsBelowThreshold / totalPoints) * totalIntensityOverall - totalIntensityBelowThreshold; + betweenClassVariance = (numerator * numerator) / denominator; + } else { + betweenClassVariance = 0; } - else - BCV = 0; - if ( BCV >= BCVmax ) - { // Assign the best threshold found so far - BCVmax = BCV; - kStar = k; + if (betweenClassVariance >= maxBetweenClassVariance) { + maxBetweenClassVariance = betweenClassVariance; + optimalThreshold = currentThreshold; } } - // kStar += 1; // Use QTI convention that intensity -> 1 if intensity >= - // k - // (the algorithm was developed for I-> 1 if I <= k.) - return kStar; + // optimalThreshold += 1; // Use QTI convention that intensity -> 1 if intensity >= optimalThreshold + // (the algorithm was developed for I-> 1 if I <= optimalThreshold.) + return optimalThreshold; } /** From 860083d8233848b8681146c4482e6ce228070308 Mon Sep 17 00:00:00 2001 From: Divyank Shah Date: Tue, 21 Nov 2023 13:37:24 -0400 Subject: [PATCH 4/7] Added Move method refactoring method for moving selectTrack method from TrackScheme class to JGraphXAdapter class, as there was feature envy smell which indicated that selectTrack method was more interested in members of JGraphXAdapter class. --- .idea/misc.xml | 1 - .../trackscheme/JGraphXAdapter.java | 450 ++-- .../trackscheme/TrackScheme.java | 2391 ++++++++--------- .../trackscheme/TrackSchemePopupMenu.java | 717 +++-- 4 files changed, 1626 insertions(+), 1933 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index e19e539b2..941073a8e 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/JGraphXAdapter.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/JGraphXAdapter.java index 32bc06070..d2a0c7a56 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/JGraphXAdapter.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/JGraphXAdapter.java @@ -8,12 +8,12 @@ * 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 * . @@ -21,242 +21,228 @@ */ package fiji.plugin.trackmate.visualization.trackscheme; -import java.util.HashMap; -import java.util.Set; - -import org.jgrapht.event.GraphEdgeChangeEvent; -import org.jgrapht.event.GraphListener; -import org.jgrapht.event.GraphVertexChangeEvent; -import org.jgrapht.graph.DefaultWeightedEdge; - import com.mxgraph.model.mxCell; import com.mxgraph.model.mxGeometry; import com.mxgraph.model.mxICell; import com.mxgraph.view.mxGraph; - import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; +import org.jgrapht.event.GraphEdgeChangeEvent; +import org.jgrapht.event.GraphListener; +import org.jgrapht.event.GraphVertexChangeEvent; +import org.jgrapht.graph.DefaultWeightedEdge; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; -public class JGraphXAdapter extends mxGraph implements GraphListener< Spot, DefaultWeightedEdge > -{ - - private final HashMap< Spot, mxCell > vertexToCellMap = new HashMap<>(); - - private final HashMap< DefaultWeightedEdge, mxCell > edgeToCellMap = new HashMap<>(); - - private final HashMap< mxCell, Spot > cellToVertexMap = new HashMap<>(); - - private final HashMap< mxCell, DefaultWeightedEdge > cellToEdgeMap = new HashMap<>(); - - private final Model tmm; - - /* - * CONSTRUCTOR - */ - - public JGraphXAdapter( final Model tmm ) - { - super(); - this.tmm = tmm; - insertTrackCollection( tmm ); - } - - /* - * METHODS - */ - - /** - * Overridden method so that when a label is changed, we change the target - * spot's name. - */ - @Override - public void cellLabelChanged( final Object cell, final Object value, final boolean autoSize ) - { - model.beginUpdate(); - try - { - final Spot spot = cellToVertexMap.get( cell ); - if ( null == spot ) - return; - final String str = ( String ) value; - spot.setName( str ); - getModel().setValue( cell, str ); - - if ( autoSize ) - { - cellSizeUpdated( cell, false ); - } - } - finally - { - model.endUpdate(); - } - } - - public mxCell addJGraphTVertex( final Spot vertex ) - { - if ( vertexToCellMap.containsKey( vertex ) ) - { - // cell for Spot already existed, skip creation and return original - // cell. - return vertexToCellMap.get( vertex ); - } - mxCell cell = null; - getModel().beginUpdate(); - try - { - cell = new mxCell( vertex, new mxGeometry(), "" ); - cell.setVertex( true ); - cell.setId( null ); - cell.setValue( vertex.getName() ); - addCell( cell, defaultParent ); - vertexToCellMap.put( vertex, cell ); - cellToVertexMap.put( cell, vertex ); - } - finally - { - getModel().endUpdate(); - } - return cell; - } - - public mxCell addJGraphTEdge( final DefaultWeightedEdge edge ) - { - if ( edgeToCellMap.containsKey( edge ) ) - { - // cell for edge already existed, skip creation and return original - // cell. - return edgeToCellMap.get( edge ); - } - mxCell cell = null; - getModel().beginUpdate(); - try - { - final Spot source = tmm.getTrackModel().getEdgeSource( edge ); - final Spot target = tmm.getTrackModel().getEdgeTarget( edge ); - cell = new mxCell( edge ); - cell.setEdge( true ); - cell.setId( null ); - cell.setValue( String.format( "%.1f", tmm.getTrackModel().getEdgeWeight( edge ) ) ); - cell.setGeometry( new mxGeometry() ); - cell.getGeometry().setRelative( true ); - addEdge( cell, defaultParent, vertexToCellMap.get( source ), vertexToCellMap.get( target ), null ); - edgeToCellMap.put( edge, cell ); - cellToEdgeMap.put( cell, edge ); - } - finally - { - getModel().endUpdate(); - } - return cell; - } - - public void mapEdgeToCell( final DefaultWeightedEdge edge, final mxCell cell ) - { - cellToEdgeMap.put( cell, edge ); - edgeToCellMap.put( edge, cell ); - } - - public Spot getSpotFor( final mxICell cell ) - { - return cellToVertexMap.get( cell ); - } - - public DefaultWeightedEdge getEdgeFor( final mxICell cell ) - { - return cellToEdgeMap.get( cell ); - } - - public mxCell getCellFor( final Spot spot ) - { - return vertexToCellMap.get( spot ); - } - - public mxCell getCellFor( final DefaultWeightedEdge edge ) - { - return edgeToCellMap.get( edge ); - } - - public Set< mxCell > getVertexCells() - { - return cellToVertexMap.keySet(); - } - - public Set< mxCell > getEdgeCells() - { - return cellToEdgeMap.keySet(); - } - - public void removeMapping( final Spot spot ) - { - final mxICell cell = vertexToCellMap.remove( spot ); - cellToVertexMap.remove( cell ); - } - - public void removeMapping( final DefaultWeightedEdge edge ) - { - final mxICell cell = edgeToCellMap.remove( edge ); - cellToEdgeMap.remove( cell ); - } - - /* - * GRAPH LISTENER - */ - - @Override - public void vertexAdded( final GraphVertexChangeEvent< Spot > e ) - { - addJGraphTVertex( e.getVertex() ); - } - - @Override - public void vertexRemoved( final GraphVertexChangeEvent< Spot > e ) - { - final mxCell cell = vertexToCellMap.remove( e.getVertex() ); - removeCells( new Object[] { cell } ); - } - - @Override - public void edgeAdded( final GraphEdgeChangeEvent< Spot, DefaultWeightedEdge > e ) - { - addJGraphTEdge( e.getEdge() ); - } - - @Override - public void edgeRemoved( final GraphEdgeChangeEvent< Spot, DefaultWeightedEdge > e ) - { - final mxICell cell = edgeToCellMap.remove( e.getEdge() ); - removeCells( new Object[] { cell } ); - } - - /* - * PRIVATE METHODS - */ - - /** - * Only insert spot and edges belonging to visible tracks. Any other spot or - * edges will be ignored by the whole trackscheme framework, and if they are - * needed, they will have to be imported "by hand". - */ - private void insertTrackCollection( final Model lTmm ) - { - model.beginUpdate(); - try - { - for ( final Integer trackID : lTmm.getTrackModel().trackIDs( true ) ) - { - for ( final Spot vertex : lTmm.getTrackModel().trackSpots( trackID ) ) - addJGraphTVertex( vertex ); - - for ( final DefaultWeightedEdge edge : lTmm.getTrackModel().trackEdges( trackID ) ) - addJGraphTEdge( edge ); - } - } - finally - { - model.endUpdate(); - } - - } +public class JGraphXAdapter extends mxGraph implements GraphListener { + + protected final SelectionModel selectionModel; + private final HashMap vertexToCellMap = new HashMap<>(); + private final HashMap edgeToCellMap = new HashMap<>(); + private final HashMap cellToVertexMap = new HashMap<>(); + private final HashMap cellToEdgeMap = new HashMap<>(); + private final Model tmm; + + /* + * CONSTRUCTOR + */ + + public JGraphXAdapter(final Model tmm, final SelectionModel selectionModel) { + super(); + this.tmm = tmm; + this.selectionModel = selectionModel; + insertTrackCollection(tmm); + } + + /* + * METHODS + */ + + /** + * Overridden method so that when a label is changed, we change the target + * spot's name. + */ + @Override + public void cellLabelChanged(final Object cell, final Object value, final boolean autoSize) { + model.beginUpdate(); + try { + final Spot spot = cellToVertexMap.get(cell); + if (null == spot) + return; + final String str = (String) value; + spot.setName(str); + getModel().setValue(cell, str); + + if (autoSize) { + cellSizeUpdated(cell, false); + } + } finally { + model.endUpdate(); + } + } + + public mxCell addJGraphTVertex(final Spot vertex) { + if (vertexToCellMap.containsKey(vertex)) { + // cell for Spot already existed, skip creation and return original + // cell. + return vertexToCellMap.get(vertex); + } + mxCell cell = null; + getModel().beginUpdate(); + try { + cell = new mxCell(vertex, new mxGeometry(), ""); + cell.setVertex(true); + cell.setId(null); + cell.setValue(vertex.getName()); + addCell(cell, defaultParent); + vertexToCellMap.put(vertex, cell); + cellToVertexMap.put(cell, vertex); + } finally { + getModel().endUpdate(); + } + return cell; + } + + public mxCell addJGraphTEdge(final DefaultWeightedEdge edge) { + if (edgeToCellMap.containsKey(edge)) { + // cell for edge already existed, skip creation and return original + // cell. + return edgeToCellMap.get(edge); + } + mxCell cell = null; + getModel().beginUpdate(); + try { + final Spot source = tmm.getTrackModel().getEdgeSource(edge); + final Spot target = tmm.getTrackModel().getEdgeTarget(edge); + cell = new mxCell(edge); + cell.setEdge(true); + cell.setId(null); + cell.setValue(String.format("%.1f", tmm.getTrackModel().getEdgeWeight(edge))); + cell.setGeometry(new mxGeometry()); + cell.getGeometry().setRelative(true); + addEdge(cell, defaultParent, vertexToCellMap.get(source), vertexToCellMap.get(target), null); + edgeToCellMap.put(edge, cell); + cellToEdgeMap.put(cell, edge); + } finally { + getModel().endUpdate(); + } + return cell; + } + + public void mapEdgeToCell(final DefaultWeightedEdge edge, final mxCell cell) { + cellToEdgeMap.put(cell, edge); + edgeToCellMap.put(edge, cell); + } + + public Spot getSpotFor(final mxICell cell) { + return cellToVertexMap.get(cell); + } + + public DefaultWeightedEdge getEdgeFor(final mxICell cell) { + return cellToEdgeMap.get(cell); + } + + public mxCell getCellFor(final Spot spot) { + return vertexToCellMap.get(spot); + } + + public mxCell getCellFor(final DefaultWeightedEdge edge) { + return edgeToCellMap.get(edge); + } + + public Set getVertexCells() { + return cellToVertexMap.keySet(); + } + + public Set getEdgeCells() { + return cellToEdgeMap.keySet(); + } + + public void removeMapping(final Spot spot) { + final mxICell cell = vertexToCellMap.remove(spot); + cellToVertexMap.remove(cell); + } + + public void removeMapping(final DefaultWeightedEdge edge) { + final mxICell cell = edgeToCellMap.remove(edge); + cellToEdgeMap.remove(cell); + } + + /* + * GRAPH LISTENER + */ + + @Override + public void vertexAdded(final GraphVertexChangeEvent e) { + addJGraphTVertex(e.getVertex()); + } + + @Override + public void vertexRemoved(final GraphVertexChangeEvent e) { + final mxCell cell = vertexToCellMap.remove(e.getVertex()); + removeCells(new Object[]{cell}); + } + + @Override + public void edgeAdded(final GraphEdgeChangeEvent e) { + addJGraphTEdge(e.getEdge()); + } + + @Override + public void edgeRemoved(final GraphEdgeChangeEvent e) { + final mxICell cell = edgeToCellMap.remove(e.getEdge()); + removeCells(new Object[]{cell}); + } + + /* + * PRIVATE METHODS + */ + + /** + * Only insert spot and edges belonging to visible tracks. Any other spot or + * edges will be ignored by the whole trackscheme framework, and if they are + * needed, they will have to be imported "by hand". + */ + private void insertTrackCollection(final Model lTmm) { + model.beginUpdate(); + try { + for (final Integer trackID : lTmm.getTrackModel().trackIDs(true)) { + for (final Spot vertex : lTmm.getTrackModel().trackSpots(trackID)) + addJGraphTVertex(vertex); + + for (final DefaultWeightedEdge edge : lTmm.getTrackModel().trackEdges(trackID)) + addJGraphTEdge(edge); + } + } finally { + model.endUpdate(); + } + + } + + public void selectTrack(final Collection vertices, final Collection edges, final int direction) { + // Look for spot and edges matching given mxCells + final Set inspectionSpots = new HashSet<>(vertices.size()); + for (final mxCell cell : vertices) { + final Spot spot = getSpotFor(cell); + if (null == spot) + continue; + + inspectionSpots.add(spot); + } + final Set inspectionEdges = new HashSet<>(edges.size()); + for (final mxCell cell : edges) { + final DefaultWeightedEdge dwe = getEdgeFor(cell); + if (null == dwe) + continue; + + inspectionEdges.add(dwe); + } + // Forward to selection model + selectionModel.selectTrack(inspectionSpots, inspectionEdges, direction); + } } diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackScheme.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackScheme.java index d5f5c0cdd..2d92cb9d8 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackScheme.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackScheme.java @@ -8,12 +8,12 @@ * 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 * . @@ -21,1316 +21,1099 @@ */ package fiji.plugin.trackmate.visualization.trackscheme; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Graphics2D; -import java.awt.Point; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.awt.image.BufferedImage; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; - -import javax.swing.JViewport; -import javax.swing.SwingUtilities; - -import org.jgrapht.graph.DefaultWeightedEdge; - import com.mxgraph.model.mxCell; import com.mxgraph.model.mxGeometry; import com.mxgraph.model.mxICell; import com.mxgraph.model.mxIGraphModel; -import com.mxgraph.util.mxCellRenderer; -import com.mxgraph.util.mxConstants; -import com.mxgraph.util.mxEvent; -import com.mxgraph.util.mxEventObject; +import com.mxgraph.util.*; import com.mxgraph.util.mxEventSource.mxIEventListener; -import com.mxgraph.util.mxRectangle; -import com.mxgraph.util.mxStyleUtils; import com.mxgraph.view.mxGraphSelectionModel; - -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.ModelChangeEvent; -import fiji.plugin.trackmate.SelectionChangeEvent; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.*; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.visualization.AbstractTrackMateModelView; import ij.ImagePlus; +import org.jgrapht.graph.DefaultWeightedEdge; -public class TrackScheme extends AbstractTrackMateModelView -{ - public static final String INFO_TEXT = "" - + "TrackScheme displays the tracking results as track lanes,
" - + "ignoring the spot actual position. " - + "

" + "Tracks can be edited through link creation and removal." - + ""; - - static final int Y_COLUMN_SIZE = 96; - - static final int X_COLUMN_SIZE = 160; - - static final int DEFAULT_CELL_WIDTH = 128; - - static final int DEFAULT_CELL_HEIGHT = 40; - - public static final String DEFAULT_COLOR = "#FF00FF"; - - private static final Dimension DEFAULT_SIZE = new Dimension( 800, 600 ); - - static final int TABLE_CELL_WIDTH = 40; - - static final Color GRID_COLOR = Color.GRAY; - - /** - * Are linking costs displayed by default? Can be changed in the toolbar. - */ - static final boolean DEFAULT_DO_DISPLAY_COSTS_ON_EDGES = false; - - /** Do we display the background decorations by default? */ - static final int DEFAULT_PAINT_DECORATION_LEVEL = 1; - - /** Do we toggle linking mode by default? */ - static final boolean DEFAULT_LINKING_ENABLED = false; - - /** Do we capture thumbnails by default? */ - static final boolean DEFAULT_THUMBNAILS_ENABLED = false; - - public static final String KEY = "TRACKSCHEME"; - - /* - * FIELDS - */ - - /** The frame in which we display the TrackScheme GUI. */ - private final TrackSchemeFrame gui; - - /** The JGraphX object that displays the graph. */ - private JGraphXAdapter graph; - - /** The graph layout in charge of re-aligning the cells. */ - private TrackSchemeGraphLayout graphLayout; - - /** - * A flag used to prevent double event firing when setting the selection - * programmatically. - */ - private boolean doFireSelectionChangeEvent = true; - - /** - * A flag used to prevent double event firing when setting the selection - * programmatically. - */ - private boolean doFireModelChangeEvent = true; - - /** - * The current row length for each frame. That is, for frame i, - * the number of cells on the row corresponding to frame i is - * rowLength.get(i). - */ - private Map< Integer, Integer > rowLengths = new HashMap<>(); - - /** - * Stores the column index that is the first one after all the track - * columns. - */ - private int unlaidSpotColumn = 2; - - /** - * The instance in charge of generating the string image representation of - * spots imported in this view. If null, nothing is done. - */ - private SpotImageUpdater spotImageUpdater; - - TrackSchemeStylist stylist; - - /** - * If true, thumbnail will be captured and displayed with - * styles allowing it. - */ - private boolean doThumbnailCapture = DEFAULT_THUMBNAILS_ENABLED; - - /* - * CONSTRUCTORS - */ - - public TrackScheme( final Model model, final SelectionModel selectionModel, final DisplaySettings displaySettings ) - { - super( model, selectionModel, displaySettings ); - this.gui = new TrackSchemeFrame( this, displaySettings ); - final String title = "TrackScheme"; - gui.setTitle( title ); - gui.setSize( DEFAULT_SIZE ); - - displaySettings.listeners().add( () -> doTrackStyle() ); - gui.addWindowListener( new WindowAdapter() - { - @Override - public void windowClosing( final WindowEvent e ) - { - model.removeModelChangeListener( TrackScheme.this ); - } - } ); - gui.setLocationByPlatform( true ); - gui.setLocationRelativeTo( null ); - gui.setVisible( true ); - } - - /* - * METHODS - */ - - public void setSpotImageUpdater( final SpotImageUpdater spotImageUpdater ) - { - this.spotImageUpdater = spotImageUpdater; - } - - public SelectionModel getSelectionModel() - { - return selectionModel; - } - - /** - * @return the column index that is the first one after all the track - * columns. - */ - public int getUnlaidSpotColumn() - { - return unlaidSpotColumn; - } - - /** - * @return the first free column for the target row. - */ - public int getNextFreeColumn( final int frame ) - { - Integer columnIndex = rowLengths.get( frame ); - if ( null == columnIndex ) - { - columnIndex = 2; - } - return columnIndex + 1; - } - - /** - * Returns the GUI frame controlled by this class. - */ - public TrackSchemeFrame getGUI() - { - return gui; - } - - /** - * Returns the {@link JGraphXAdapter} that serves as a model for the graph - * displayed in this frame. - */ - public JGraphXAdapter getGraph() - { - return graph; - } - - /** - * Returns the graph layout in charge of arranging the cells on the graph. - */ - public TrackSchemeGraphLayout getGraphLayout() - { - return graphLayout; - } - - /* - * PRIVATE METHODS - */ - - /** - * Used to instantiate and configure the {@link JGraphXAdapter} that will be - * used for display. - */ - private JGraphXAdapter createGraph() - { - gui.logger.setStatus( "Creating graph adapter." ); - - final JGraphXAdapter lGraph = new JGraphXAdapter( model ); - lGraph.setAllowLoops( false ); - lGraph.setAllowDanglingEdges( false ); - lGraph.setCellsCloneable( false ); - lGraph.setCellsSelectable( true ); - lGraph.setCellsDisconnectable( false ); - lGraph.setCellsMovable( true ); - lGraph.setGridEnabled( false ); - lGraph.setLabelsVisible( true ); - lGraph.setDropEnabled( false ); - - // Cells removed from JGraphX - lGraph.addListener( mxEvent.CELLS_REMOVED, new CellRemovalListener() ); - - // Cell selection change - lGraph.getSelectionModel().addListener( mxEvent.CHANGE, new SelectionChangeListener() ); - - // Return graph - return lGraph; - } - - /** - * Updates or creates a cell for the target spot. Is called after the user - * modified a spot (location, radius, ...) somewhere else. - * - * @param spot - * the spot that was modified. - */ - private mxICell updateCellOf( final Spot spot ) - { - - mxICell cell = graph.getCellFor( spot ); - graph.getModel().beginUpdate(); - try - { - if ( null == cell ) - { - /* - * mxCell not present in graph. Most likely because the - * corresponding spot belonged to an invisible track, and a cell - * was not created for it when TrackScheme was launched. So we - * create one on the fly now. - */ - final int row = getUnlaidSpotColumn(); - cell = insertSpotInGraph( spot, row ); - final int frame = spot.getFeature( Spot.FRAME ).intValue(); - rowLengths.put( frame, row + 1 ); - } - - // Update cell look - if ( spotImageUpdater != null && doThumbnailCapture ) - { - String style = cell.getStyle(); - final double radiusFactor = displaySettings.getSpotDisplayRadius(); - final String imageStr = spotImageUpdater.getImageString( spot, radiusFactor ); - style = mxStyleUtils.setStyle( style, mxConstants.STYLE_IMAGE, "data:image/base64," + imageStr ); - graph.getModel().setStyle( cell, style ); - } - } - finally - { - graph.getModel().endUpdate(); - } - return cell; - } - - /** - * Insert a spot in the {@link TrackSchemeFrame}, by creating a - * {@link mxCell} in the graph model of this frame and position it according - * to its feature. - */ - private mxICell insertSpotInGraph( final Spot spot, final int targetColumn ) - { - mxICell cellAdded = graph.getCellFor( spot ); - if ( cellAdded != null ) - { - // cell for spot already exist, do nothing and return original spot - return cellAdded; - } - // Instantiate JGraphX cell - cellAdded = graph.addJGraphTVertex( spot ); - // Position it - final int row = spot.getFeature( Spot.FRAME ).intValue(); - final double x = ( targetColumn - 1 ) * X_COLUMN_SIZE - DEFAULT_CELL_WIDTH / 2; - final double y = ( 0.5 + row ) * Y_COLUMN_SIZE - DEFAULT_CELL_HEIGHT / 2; - final mxGeometry geometry = new mxGeometry( x, y, DEFAULT_CELL_WIDTH, DEFAULT_CELL_HEIGHT ); - cellAdded.setGeometry( geometry ); - // Set its style - final double radiusFactor = displaySettings.getSpotDisplayRadius(); - if ( null != spotImageUpdater && doThumbnailCapture ) - { - final String imageStr = spotImageUpdater.getImageString( spot, radiusFactor ); - graph.getModel().setStyle( cellAdded, mxConstants.STYLE_IMAGE + "=" + "data:image/base64," + imageStr ); - } - return cellAdded; - } - - /** - * Import a whole track from the {@link Model} and make it visible. - * - * @param trackIndex - * the index of the track to show in TrackScheme - */ - private void importTrack( final int trackIndex ) - { - model.beginUpdate(); - graph.getModel().beginUpdate(); - try - { - // Flag original track as visible - model.setTrackVisibility( trackIndex, true ); - // Find adequate column - final int targetColumn = getUnlaidSpotColumn(); - // Create cells for track - final Set< Spot > trackSpots = model.getTrackModel().trackSpots( trackIndex ); - for ( final Spot trackSpot : trackSpots ) - { - final int frame = trackSpot.getFeature( Spot.FRAME ).intValue(); - final int column = Math.max( targetColumn, getNextFreeColumn( frame ) ); - insertSpotInGraph( trackSpot, column ); - rowLengths.put( frame, column ); - } - final Set< DefaultWeightedEdge > trackEdges = model.getTrackModel().trackEdges( trackIndex ); - for ( final DefaultWeightedEdge trackEdge : trackEdges ) - { - graph.addJGraphTEdge( trackEdge ); - } - } - finally - { - model.endUpdate(); - graph.getModel().endUpdate(); - } - } - - /** - * This method is called when the user has created manually an edge in the - * graph, by dragging a link between two spot cells. It checks whether the - * matching edge in the model exists, and tune what should be done - * accordingly. - * - * @param cell - * the mxCell of the edge that has been manually created. - */ - protected void addEdgeManually( mxCell cell ) - { - if ( cell.isEdge() ) - { - final mxIGraphModel graphModel = graph.getModel(); - cell.setValue( "New" ); - model.beginUpdate(); - graphModel.beginUpdate(); - try - { - - Spot source = graph.getSpotFor( cell.getSource() ); - Spot target = graph.getSpotFor( cell.getTarget() ); - - if ( Spot.frameComparator.compare( source, target ) == 0 ) - { - /* - * Prevent adding edges between spots that belong to the - * same frame - */ - graph.removeCells( new Object[] { cell } ); - - } - else - { - /* - * We can add it to the model Put them right in order: since - * we use a oriented graph, we want the source spot to - * precede in time. - */ - if ( Spot.frameComparator.compare( source, target ) > 0 ) - { - final Spot tmp = source; - source = target; - target = tmp; - } - /* - * We add a new jGraphT edge to the underlying model, if it - * does not exist yet. - */ - DefaultWeightedEdge edge = model.getTrackModel().getEdge( source, target ); - if ( null == edge ) - { - edge = model.addEdge( source, target, -1 ); - } - else - { - /* - * Ah. There was an existing edge in the model we were - * trying to re-add there, from the graph. We remove the - * graph edge we have added, - */ - graph.removeCells( new Object[] { cell } ); - // And re-create a graph edge from the model edge. - cell = graph.addJGraphTEdge( edge ); - cell.setValue( String.format( "%.1f", model.getTrackModel().getEdgeWeight( edge ) ) ); - /* - * We also need now to check if the edge belonged to a - * visible track. If not, we make it visible. - */ - final int ID = model.getTrackModel().trackIDOf( edge ); - /* - * This will work, because track indices will be - * reprocessed only after the graphModel.endUpdate() - * reaches 0. So now, it's like we are dealing with the - * track indices priori to modification. - */ - if ( !model.getTrackModel().isVisible( ID ) ) - importTrack( ID ); - } - graph.mapEdgeToCell( edge, cell ); - } - - } - finally - { - graphModel.endUpdate(); - model.endUpdate(); - selectionModel.clearEdgeSelection(); - } - } - } - - /* - * OVERRIDEN METHODS - */ - - @Override - public void selectionChanged( final SelectionChangeEvent event ) - { - if ( !doFireSelectionChangeEvent ) - return; - - doFireSelectionChangeEvent = false; - - final ArrayList< Object > newSelection = new ArrayList<>( selectionModel.getSpotSelection().size() + selectionModel.getEdgeSelection().size() ); - final Iterator< DefaultWeightedEdge > edgeIt = selectionModel.getEdgeSelection().iterator(); - while ( edgeIt.hasNext() ) - { - final mxICell cell = graph.getCellFor( edgeIt.next() ); - if ( null != cell ) - newSelection.add( cell ); - } - - final Iterator< Spot > spotIt = selectionModel.getSpotSelection().iterator(); - while ( spotIt.hasNext() ) - { - final mxICell cell = graph.getCellFor( spotIt.next() ); - if ( null != cell ) - newSelection.add( cell ); - } - final mxGraphSelectionModel mGSmodel = graph.getSelectionModel(); - mGSmodel.setCells( newSelection.toArray() ); - - // Center on selection if we added one spot exactly - final Map< Spot, Boolean > spotsAdded = event.getSpots(); - if ( spotsAdded != null && spotsAdded.size() == 1 ) - { - final boolean added = spotsAdded.values().iterator().next(); - if ( added ) - { - final Spot spot = spotsAdded.keySet().iterator().next(); - centerViewOn( spot ); - } - } - doFireSelectionChangeEvent = true; - } - - @Override - public void centerViewOn( final Spot spot ) - { - gui.centerViewOn( graph.getCellFor( spot ) ); - } - - /** - * Used to catch spot creation events that occurred elsewhere, for instance - * by manual editing in the {@link AbstractTrackMateModelView}. - *

- * We have to deal with the graph modification ourselves here, because the - * {@link Model} model holds a non-listenable JGraphT instance. A - * modification made to the model would not be reflected on the graph here. - */ - @Override - public void modelChanged( final ModelChangeEvent event ) - { - // Only catch model changes - if ( event.getEventID() != ModelChangeEvent.MODEL_MODIFIED ) - return; - - graph.getModel().beginUpdate(); - try - { - final ArrayList< mxICell > cellsToRemove = new ArrayList<>(); - - final int targetColumn = getUnlaidSpotColumn(); - - // Deal with spots - if ( !event.getSpots().isEmpty() ) - { - - final Collection< mxCell > spotsWithStyleToUpdate = new HashSet<>(); - - for ( final Spot spot : event.getSpots() ) - { - - if ( event.getSpotFlag( spot ) == ModelChangeEvent.FLAG_SPOT_ADDED ) - { - - final int frame = spot.getFeature( Spot.FRAME ).intValue(); - // Put in the graph - final int column = Math.max( targetColumn, getNextFreeColumn( frame ) ); - final mxICell newCell = insertSpotInGraph( spot, column ); - rowLengths.put( frame, column ); - spotsWithStyleToUpdate.add( ( mxCell ) newCell ); - - } - else if ( event.getSpotFlag( spot ) == ModelChangeEvent.FLAG_SPOT_MODIFIED ) - { - - // Change the look of the cell - final mxICell cell = updateCellOf( spot ); - spotsWithStyleToUpdate.add( ( mxCell ) cell ); - - } - else if ( event.getSpotFlag( spot ) == ModelChangeEvent.FLAG_SPOT_REMOVED ) - { - - final mxICell cell = graph.getCellFor( spot ); - cellsToRemove.add( cell ); - - } - } - graph.removeCells( cellsToRemove.toArray(), true ); - stylist.updateVertexStyle( spotsWithStyleToUpdate ); - } - - } - finally - { - graph.getModel().endUpdate(); - } - - // Deal with edges - if ( !event.getEdges().isEmpty() ) - { - - graph.getModel().beginUpdate(); - try - { - - if ( event.getEdges().size() > 0 ) - { - - /* - * Here we keep track of the spot and edge cells which style - * we need to update. - */ - final Collection< mxCell > edgesToUpdate = new ArrayList<>(); - final Collection< mxCell > spotsWithStyleToUpdate = new ArrayList<>(); - - for ( final DefaultWeightedEdge edge : event.getEdges() ) - { - - if ( event.getEdgeFlag( edge ) == ModelChangeEvent.FLAG_EDGE_ADDED ) - { - - mxCell edgeCell = graph.getCellFor( edge ); - if ( null == edgeCell ) - { - - // Make sure target & source cells exist - final Spot source = model.getTrackModel().getEdgeSource( edge ); - final mxCell sourceCell = graph.getCellFor( source ); - final Spot target = model.getTrackModel().getEdgeTarget( edge ); - final mxCell targetCell = graph.getCellFor( target ); - - if ( sourceCell == null || targetCell == null ) - { - /* - * Is this missing cell missing because it - * belongs to an invisible track? We then - * have to import all the spot and edges. - */ - final Integer trackID = model.getTrackModel().trackIDOf( edge ); - final Set< Spot > trackSpots = model.getTrackModel().trackSpots( trackID ); - for ( final Spot trackSpot : trackSpots ) - { - final mxCell spotCell = graph.getCellFor( trackSpot ); - if ( spotCell == null ) - { - final int frame = trackSpot.getFeature( Spot.FRAME ).intValue(); - // Put in the graph - final int targetColumn = getUnlaidSpotColumn(); - final int column = Math.max( targetColumn, getNextFreeColumn( frame ) ); - // move in right+1 free column - final mxCell spotCellAdded = ( mxCell ) insertSpotInGraph( trackSpot, column ); - rowLengths.put( frame, column ); - spotsWithStyleToUpdate.add( spotCellAdded ); - } - } - - final Set< DefaultWeightedEdge > trackEdges = model.getTrackModel().trackEdges( trackID ); - /* - * Keep track of edges which style must be - * updated. - */ - - /* - * Loop over edges. Those who do not have a - * cell get a cell. - */ - for ( final DefaultWeightedEdge trackEdge : trackEdges ) - { - mxCell edgeCellToAdd = graph.getCellFor( trackEdge ); - if ( null == edgeCellToAdd ) - { - edgeCellToAdd = graph.addJGraphTEdge( trackEdge ); - graph.getModel().add( graph.getDefaultParent(), edgeCellToAdd, 0 ); - edgesToUpdate.add( edgeCellToAdd ); - } - } - } - - // And finally create the edge cell - edgeCell = graph.addJGraphTEdge( edge ); - } - - graph.getModel().add( graph.getDefaultParent(), edgeCell, 0 ); - edgesToUpdate.add( edgeCell ); - } - else if ( event.getEdgeFlag( edge ) == ModelChangeEvent.FLAG_EDGE_MODIFIED ) - { - // Add it to the map of cells to recolor - edgesToUpdate.add( graph.getCellFor( edge ) ); - - } - else if ( event.getEdgeFlag( edge ) == ModelChangeEvent.FLAG_EDGE_REMOVED ) - { - - final mxCell cell = graph.getCellFor( edge ); - graph.removeCells( new Object[] { cell } ); - } - } - - stylist.updateEdgeStyle( edgesToUpdate ); - stylist.updateVertexStyle( spotsWithStyleToUpdate ); - SwingUtilities.invokeLater( new Runnable() - { - @Override - public void run() - { - gui.graphComponent.refresh(); - gui.graphComponent.repaint(); - } - } ); - - } - } - finally - { - graph.getModel().endUpdate(); - } - } - } - - @Override - public void render() - { - final long start = System.currentTimeMillis(); - // Graph to mirror model - this.graph = createGraph(); - gui.logger.setProgress( 0.5 ); - - SwingUtilities.invokeLater( new Runnable() - { - @Override - public void run() - { - // Pass graph to GUI - gui.logger.setStatus( "Generating GUI components." ); - gui.init( graph ); - - // Init functions that set look and position - gui.logger.setStatus( "Creating style manager." ); - TrackScheme.this.stylist = new TrackSchemeStylist( model, graph, displaySettings ); - gui.logger.setStatus( "Creating layout manager." ); - TrackScheme.this.graphLayout = new TrackSchemeGraphLayout( graph, model, gui.graphComponent ); - - // Execute style and layout - gui.logger.setProgress( 0.75 ); - doTrackStyle(); - - gui.logger.setStatus( "Executing layout." ); - doTrackLayout(); - - gui.logger.setProgress( 0.9 ); - - gui.logger.setStatus( "Refreshing display." ); - gui.graphComponent.refresh(); - final mxRectangle bounds = graph.getView().validateCellState( graph.getDefaultParent(), false ); - - // This happens when there is not track to display - if ( null == bounds ) - return; - - final Dimension dim = new Dimension(); - dim.setSize( bounds.getRectangle().width + bounds.getRectangle().x, bounds.getRectangle().height + bounds.getRectangle().y ); - gui.graphComponent.getGraphControl().setPreferredSize( dim ); - gui.logger.setStatus( "" ); - - gui.graphComponent.zoomOut(); - gui.graphComponent.zoomOut(); - - gui.logger.setProgress( 0 ); - final long end = System.currentTimeMillis(); - gui.logger.log( String.format( "TrackScheme rendering done in %.1f s.", ( end - start ) / 1000d ) ); - gui.revalidate(); - } - } ); - } - - @Override - public void refresh() - {} - - @Override - public void clear() - { - System.out.println( "[TrackScheme] clear() called" ); - } - - @Override - public Model getModel() - { - return model; - } - - /* - * PRIVATE METHODS - */ - - /** - * Called when the user makes a selection change in the graph. Used to - * forward this event to the {@link InfoPane} and to other - * {@link SelectionChangeListener}s. - * - * @param added - * the cells removed from selection (careful, inverted) - * @param removed - * the cells added to selection (careful, inverted) - */ - private void userChangedSelection( final Collection< Object > added, final Collection< Object > removed ) - { // Seems to be inverted - if ( !doFireSelectionChangeEvent ) - { return; } - final Collection< Spot > spotsToAdd = new ArrayList<>(); - final Collection< Spot > spotsToRemove = new ArrayList<>(); - final Collection< DefaultWeightedEdge > edgesToAdd = new ArrayList<>(); - final Collection< DefaultWeightedEdge > edgesToRemove = new ArrayList<>(); - - if ( null != added ) - { - for ( final Object obj : added ) - { - final mxCell cell = ( mxCell ) obj; - - if ( cell.getChildCount() > 0 ) - { - - for ( int i = 0; i < cell.getChildCount(); i++ ) - { - final mxICell child = cell.getChildAt( i ); - if ( child.isVertex() ) - { - final Spot spot = graph.getSpotFor( child ); - spotsToRemove.add( spot ); - } - else - { - final DefaultWeightedEdge edge = graph.getEdgeFor( child ); - edgesToRemove.add( edge ); - } - } - - } - else - { - - if ( cell.isVertex() ) - { - final Spot spot = graph.getSpotFor( cell ); - spotsToRemove.add( spot ); - } - else - { - final DefaultWeightedEdge edge = graph.getEdgeFor( cell ); - edgesToRemove.add( edge ); - } - } - } - } - - if ( null != removed ) - { - for ( final Object obj : removed ) - { - final mxCell cell = ( mxCell ) obj; - - if ( cell.getChildCount() > 0 ) - { - - for ( int i = 0; i < cell.getChildCount(); i++ ) - { - final mxICell child = cell.getChildAt( i ); - if ( child.isVertex() ) - { - final Spot spot = graph.getSpotFor( child ); - spotsToAdd.add( spot ); - } - else - { - final DefaultWeightedEdge edge = graph.getEdgeFor( child ); - edgesToAdd.add( edge ); - } - } - - } - else - { - - if ( cell.isVertex() ) - { - final Spot spot = graph.getSpotFor( cell ); - spotsToAdd.add( spot ); - } - else - { - final DefaultWeightedEdge edge = graph.getEdgeFor( cell ); - edgesToAdd.add( edge ); - } - } - } - } - - doFireSelectionChangeEvent = false; - - if ( !edgesToAdd.isEmpty() ) - selectionModel.addEdgeToSelection( edgesToAdd ); - - if ( !spotsToAdd.isEmpty() ) - selectionModel.addSpotToSelection( spotsToAdd ); - - if ( !edgesToRemove.isEmpty() ) - selectionModel.removeEdgeFromSelection( edgesToRemove ); - - if ( !spotsToRemove.isEmpty() ) - selectionModel.removeSpotFromSelection( spotsToRemove ); - - doFireSelectionChangeEvent = true; - } - - /* - * INNER CLASSES - */ - - private class CellRemovalListener implements mxIEventListener - { - - @Override - public void invoke( final Object sender, final mxEventObject evt ) - { - if ( !doFireModelChangeEvent ) - return; - - // Separate spots from edges - final Object[] objects = ( Object[] ) evt.getProperty( "cells" ); - final HashSet< Spot > spotsToRemove = new HashSet<>(); - final ArrayList< DefaultWeightedEdge > edgesToRemove = new ArrayList<>(); - for ( final Object obj : objects ) - { - final mxCell cell = ( mxCell ) obj; - if ( null != cell ) - { - if ( cell.isVertex() ) - { - // Build list of removed spots - final Spot spot = graph.getSpotFor( cell ); - spotsToRemove.add( spot ); - // Clean maps - graph.removeMapping( spot ); - } - else if ( cell.isEdge() ) - { - // Build list of removed edges - final DefaultWeightedEdge edge = graph.getEdgeFor( cell ); - if ( null == edge ) - continue; - - edgesToRemove.add( edge ); - // Clean maps - graph.removeMapping( edge ); - } - } - } - - evt.consume(); - - // Clean model - doFireModelChangeEvent = false; - model.beginUpdate(); - try - { - selectionModel.clearSelection(); - /* - * We remove edges first so that we ensure we do not end having - * orphan edges. Normally JGraphT handles that well, but we - * enforce things here. To be sure. - */ - for ( final DefaultWeightedEdge edge : edgesToRemove ) - model.removeEdge( edge ); - - for ( final Spot spot : spotsToRemove ) - model.removeSpot( spot ); - - } - finally - { - model.endUpdate(); - } - doFireModelChangeEvent = true; - } - } - - private class SelectionChangeListener implements mxIEventListener - { - - @Override - @SuppressWarnings( "unchecked" ) - public void invoke( final Object sender, final mxEventObject evt ) - { - if ( !doFireSelectionChangeEvent || sender != graph.getSelectionModel() ) - return; - - final Collection< Object > added = ( Collection< Object > ) evt.getProperty( "added" ); - final Collection< Object > removed = ( Collection< Object > ) evt.getProperty( "removed" ); - userChangedSelection( added, removed ); - } - } - - /* - * ACTIONS called from gui parts - */ - - /** - * Toggles whether drag-&-drop linking is allowed. - * - * @return the current settings value, after toggling. - */ - public boolean toggleLinking() - { - final boolean enabled = gui.graphComponent.getConnectionHandler().isEnabled(); - gui.graphComponent.getConnectionHandler().setEnabled( !enabled ); - return !enabled; - } - - /** - * Toggles whether thumbnail capture is enabled. - * - * @return the current settings value, after toggling. - */ - public boolean toggleThumbnail() - { - if ( !doThumbnailCapture ) - createThumbnails(); - - doThumbnailCapture = !doThumbnailCapture; - return doThumbnailCapture; - } - - public void zoomIn() - { - gui.graphComponent.zoomIn(); - } - - public void zoomOut() - { - gui.graphComponent.zoomOut(); - } - - public void resetZoom() - { - gui.graphComponent.zoomActual(); - } - - public void doTrackStyle() - { - if ( null == stylist ) - return; - - gui.logger.setStatus( "Setting style." ); - graph.getModel().beginUpdate(); - try - { - stylist.updateEdgeStyle( graph.getEdgeCells() ); - stylist.updateVertexStyle( graph.getVertexCells() ); - } - finally - { - graph.getModel().endUpdate(); - } - } - - /** - * Captures and stores the thumbnail image that will be displayed in each - * spot cell, when using styles that can display images. - */ - private void createThumbnails() - { - // Group spots per frame - final Set< Integer > frames = model.getSpots().keySet(); - final HashMap< Integer, HashSet< Spot > > spotPerFrame = new HashMap<>( frames.size() ); - for ( final Integer frame : frames ) - spotPerFrame.put( frame, new HashSet< Spot >( model.getSpots().getNSpots( frame, true ) ) ); // max - - for ( final Integer trackID : model.getTrackModel().trackIDs( true ) ) - { - for ( final Spot spot : model.getTrackModel().trackSpots( trackID ) ) - { - final int frame = spot.getFeature( Spot.FRAME ).intValue(); - spotPerFrame.get( frame ).add( spot ); - } - } - - // Set spot image to cell style - if ( null != spotImageUpdater ) - { - gui.logger.setStatus( "Collecting spot thumbnails." ); - final double radiusFactor = displaySettings.getSpotDisplayRadius(); - int index = 0; - try - { - graph.getModel().beginUpdate(); - - // Iterate per frame - for ( final Integer frame : frames ) - { - for ( final Spot spot : spotPerFrame.get( frame ) ) - { - final mxICell cell = graph.getCellFor( spot ); - final String imageStr = spotImageUpdater.getImageString( spot, radiusFactor ); - String style = cell.getStyle(); - style = mxStyleUtils.setStyle( style, mxConstants.STYLE_IMAGE, "data:image/base64," + imageStr ); - graph.getModel().setStyle( cell, style ); - - } - gui.logger.setProgress( ( double ) index++ / frames.size() ); - } - } - finally - { - graph.getModel().endUpdate(); - gui.logger.setProgress( 0d ); - gui.logger.setStatus( "" ); - } - } - } - - public void doTrackLayout() - { - // Position cells - graphLayout.execute( null ); - rowLengths = graphLayout.getRowLengths(); - int maxLength = 2; - for ( final int rowLength : rowLengths.values() ) - { - if ( maxLength < rowLength ) - maxLength = rowLength; - } - unlaidSpotColumn = maxLength; - gui.graphComponent.refresh(); - gui.graphComponent.repaint(); - } - - public void captureUndecorated() - { - final BufferedImage image = mxCellRenderer.createBufferedImage( graph, null, 1, Color.WHITE, true, null, gui.graphComponent.getCanvas() ); - final ImagePlus imp = new ImagePlus( "TrackScheme capture", image ); - imp.show(); - } - - public void captureDecorated() - { - final JViewport view = gui.graphComponent.getViewport(); - final Point currentPos = view.getViewPosition(); - view.setViewPosition( new Point( 0, 0 ) ); - // We have to do that otherwise, top left is not painted. - final Dimension size = view.getViewSize(); - final BufferedImage image = ( BufferedImage ) view.createImage( size.width, size.height ); - final Graphics2D captureG = image.createGraphics(); - view.paintComponents( captureG ); - view.setViewPosition( currentPos ); - final ImagePlus imp = new ImagePlus( "TrackScheme capture", image ); - imp.show(); - } - - public void toggleDisplayDecoration() - { - gui.graphComponent.loopPaintDecorationLevel(); - gui.graphComponent.repaint(); - } - - /** - * Create links between all the spots currently in the {@link Model} - * selection. We update simultaneously the {@link Model} and the - * {@link JGraphXAdapter}. - */ - public void linkSpots() - { - - // Sort spots by time - final TreeMap< Integer, Spot > spotsInTime = new TreeMap<>(); - for ( final Spot spot : selectionModel.getSpotSelection() ) - spotsInTime.put( spot.getFeature( Spot.FRAME ).intValue(), spot ); - - // Find adequate column - final int targetColumn = getUnlaidSpotColumn(); - - // Then link them in this order - model.beginUpdate(); - graph.getModel().beginUpdate(); - try - { - final Iterator< Integer > it = spotsInTime.keySet().iterator(); - final Integer previousTime = it.next(); - Spot previousSpot = spotsInTime.get( previousTime ); - // If this spot belong to an invisible track, we make it visible - Integer ID = model.getTrackModel().trackIDOf( previousSpot ); - if ( ID != null && !model.getTrackModel().isVisible( ID ) ) - importTrack( ID ); - - while ( it.hasNext() ) - { - final Integer currentTime = it.next(); - final Spot currentSpot = spotsInTime.get( currentTime ); - // If this spot belong to an invisible track, we make it visible - ID = model.getTrackModel().trackIDOf( currentSpot ); - if ( ID != null && !model.getTrackModel().isVisible( ID ) ) - importTrack( ID ); - - // Check that the cells matching the 2 spots exist in the graph - mxICell currentCell = graph.getCellFor( currentSpot ); - if ( null == currentCell ) - currentCell = insertSpotInGraph( currentSpot, targetColumn ); - - mxICell previousCell = graph.getCellFor( previousSpot ); - if ( null == previousCell ) - { - final int frame = previousSpot.getFeature( Spot.FRAME ).intValue(); - final int column = Math.max( targetColumn, getNextFreeColumn( frame ) ); - rowLengths.put( frame, column ); - previousCell = insertSpotInGraph( previousSpot, column ); - } - - /* - * Check if the model does not have already a edge for these 2 - * spots (that is the case if the 2 spot are in an invisible - * track, which track scheme does not know of). - */ - DefaultWeightedEdge edge = model.getTrackModel().getEdge( previousSpot, currentSpot ); - if ( null == edge ) - { - /* - * We create a new edge between 2 spots, and pair it with a - * new cell edge. - */ - edge = model.addEdge( previousSpot, currentSpot, -1 ); - final mxCell cell = graph.addJGraphTEdge( edge ); - cell.setValue( "New" ); - } - else - { - // We retrieve the edge, and pair it with a new cell edge. - final mxCell cell = graph.addJGraphTEdge( edge ); - cell.setValue( String.format( "%.1f", model.getTrackModel().getEdgeWeight( edge ) ) ); - /* - * Also, if the existing edge belonged to an existing - * invisible track, we make it visible. - */ - ID = model.getTrackModel().trackIDOf( edge ); - if ( ID != null && !model.getTrackModel().isVisible( ID ) ) - importTrack( ID ); - } - previousSpot = currentSpot; - } - } - finally - { - graph.getModel().endUpdate(); - model.endUpdate(); - } - } - - /** - * Removes the cell selected by the user in the GUI. - */ - public void removeSelectedCells() - { - graph.getModel().beginUpdate(); - try - { - graph.removeCells( graph.getSelectionCells() ); - // Will be caught by the graph listeners - } - finally - { - graph.getModel().endUpdate(); - } - } - - public void removeSelectedLinkCells() - { - List< Object > edgeCells = new ArrayList<>(); - for ( Object obj : graph.getSelectionCells() ) - { - DefaultWeightedEdge e = graph.getEdgeFor( ( mxICell ) obj ); - if ( e == null ) - continue; - - edgeCells.add( obj ); - } - - graph.getModel().beginUpdate(); - try - { - graph.removeCells( edgeCells.toArray() ); - // Will be caught by the graph listeners - } - finally - { - graph.getModel().endUpdate(); - } - } - - public void selectTrack( final Collection< mxCell > vertices, final Collection< mxCell > edges, final int direction ) - { - // Look for spot and edges matching given mxCells - final Set< Spot > inspectionSpots = new HashSet<>( vertices.size() ); - for ( final mxCell cell : vertices ) - { - final Spot spot = graph.getSpotFor( cell ); - if ( null == spot ) - continue; - - inspectionSpots.add( spot ); - } - final Set< DefaultWeightedEdge > inspectionEdges = new HashSet<>( edges.size() ); - for ( final mxCell cell : edges ) - { - final DefaultWeightedEdge dwe = graph.getEdgeFor( cell ); - if ( null == dwe ) - continue; - - inspectionEdges.add( dwe ); - } - // Forward to selection model - selectionModel.selectTrack( inspectionSpots, inspectionEdges, direction ); - } - - @Override - public String getKey() - { - return KEY; - } +import javax.swing.*; +import java.awt.Dimension; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.util.List; +import java.util.*; + +public class TrackScheme extends AbstractTrackMateModelView { + public static final String INFO_TEXT = "" + + "TrackScheme displays the tracking results as track lanes,
" + + "ignoring the spot actual position. " + + "

" + "Tracks can be edited through link creation and removal." + + ""; + public static final String DEFAULT_COLOR = "#FF00FF"; + public static final String KEY = "TRACKSCHEME"; + static final int Y_COLUMN_SIZE = 96; + static final int X_COLUMN_SIZE = 160; + static final int DEFAULT_CELL_WIDTH = 128; + static final int DEFAULT_CELL_HEIGHT = 40; + static final int TABLE_CELL_WIDTH = 40; + + static final Color GRID_COLOR = Color.GRAY; + + /** + * Are linking costs displayed by default? Can be changed in the toolbar. + */ + static final boolean DEFAULT_DO_DISPLAY_COSTS_ON_EDGES = false; + + /** + * Do we display the background decorations by default? + */ + static final int DEFAULT_PAINT_DECORATION_LEVEL = 1; + + /** + * Do we toggle linking mode by default? + */ + static final boolean DEFAULT_LINKING_ENABLED = false; + + /** + * Do we capture thumbnails by default? + */ + static final boolean DEFAULT_THUMBNAILS_ENABLED = false; + private static final Dimension DEFAULT_SIZE = new Dimension(800, 600); + + /* + * FIELDS + */ + /** + * The frame in which we display the TrackScheme GUI. + */ + private final TrackSchemeFrame gui; + TrackSchemeStylist stylist; + /** + * The JGraphX object that displays the graph. + */ + private JGraphXAdapter graph; + /** + * The graph layout in charge of re-aligning the cells. + */ + private TrackSchemeGraphLayout graphLayout; + /** + * A flag used to prevent double event firing when setting the selection + * programmatically. + */ + private boolean doFireSelectionChangeEvent = true; + /** + * A flag used to prevent double event firing when setting the selection + * programmatically. + */ + private boolean doFireModelChangeEvent = true; + /** + * The current row length for each frame. That is, for frame i, + * the number of cells on the row corresponding to frame i is + * rowLength.get(i). + */ + private Map rowLengths = new HashMap<>(); + /** + * Stores the column index that is the first one after all the track + * columns. + */ + private int unlaidSpotColumn = 2; + /** + * The instance in charge of generating the string image representation of + * spots imported in this view. If null, nothing is done. + */ + private SpotImageUpdater spotImageUpdater; + /** + * If true, thumbnail will be captured and displayed with + * styles allowing it. + */ + private boolean doThumbnailCapture = DEFAULT_THUMBNAILS_ENABLED; + + /* + * CONSTRUCTORS + */ + + public TrackScheme(final Model model, final SelectionModel selectionModel, final DisplaySettings displaySettings) { + super(model, selectionModel, displaySettings); + this.gui = new TrackSchemeFrame(this, displaySettings); + final String title = "TrackScheme"; + gui.setTitle(title); + gui.setSize(DEFAULT_SIZE); + + displaySettings.listeners().add(() -> doTrackStyle()); + gui.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(final WindowEvent e) { + model.removeModelChangeListener(TrackScheme.this); + } + }); + gui.setLocationByPlatform(true); + gui.setLocationRelativeTo(null); + gui.setVisible(true); + } + + /* + * METHODS + */ + + public void setSpotImageUpdater(final SpotImageUpdater spotImageUpdater) { + this.spotImageUpdater = spotImageUpdater; + } + + public SelectionModel getSelectionModel() { + return selectionModel; + } + + /** + * @return the column index that is the first one after all the track + * columns. + */ + public int getUnlaidSpotColumn() { + return unlaidSpotColumn; + } + + /** + * @return the first free column for the target row. + */ + public int getNextFreeColumn(final int frame) { + Integer columnIndex = rowLengths.get(frame); + if (null == columnIndex) { + columnIndex = 2; + } + return columnIndex + 1; + } + + /** + * Returns the GUI frame controlled by this class. + */ + public TrackSchemeFrame getGUI() { + return gui; + } + + /** + * Returns the {@link JGraphXAdapter} that serves as a model for the graph + * displayed in this frame. + */ + public JGraphXAdapter getGraph() { + return graph; + } + + /** + * Returns the graph layout in charge of arranging the cells on the graph. + */ + public TrackSchemeGraphLayout getGraphLayout() { + return graphLayout; + } + + /* + * PRIVATE METHODS + */ + + /** + * Used to instantiate and configure the {@link JGraphXAdapter} that will be + * used for display. + */ + private JGraphXAdapter createGraph() { + gui.logger.setStatus("Creating graph adapter."); + + final JGraphXAdapter lGraph = new JGraphXAdapter(model, selectionModel); + lGraph.setAllowLoops(false); + lGraph.setAllowDanglingEdges(false); + lGraph.setCellsCloneable(false); + lGraph.setCellsSelectable(true); + lGraph.setCellsDisconnectable(false); + lGraph.setCellsMovable(true); + lGraph.setGridEnabled(false); + lGraph.setLabelsVisible(true); + lGraph.setDropEnabled(false); + + // Cells removed from JGraphX + lGraph.addListener(mxEvent.CELLS_REMOVED, new CellRemovalListener()); + + // Cell selection change + lGraph.getSelectionModel().addListener(mxEvent.CHANGE, new SelectionChangeListener()); + + // Return graph + return lGraph; + } + + /** + * Updates or creates a cell for the target spot. Is called after the user + * modified a spot (location, radius, ...) somewhere else. + * + * @param spot the spot that was modified. + */ + private mxICell updateCellOf(final Spot spot) { + + mxICell cell = graph.getCellFor(spot); + graph.getModel().beginUpdate(); + try { + if (null == cell) { + /* + * mxCell not present in graph. Most likely because the + * corresponding spot belonged to an invisible track, and a cell + * was not created for it when TrackScheme was launched. So we + * create one on the fly now. + */ + final int row = getUnlaidSpotColumn(); + cell = insertSpotInGraph(spot, row); + final int frame = spot.getFeature(Spot.FRAME).intValue(); + rowLengths.put(frame, row + 1); + } + + // Update cell look + if (spotImageUpdater != null && doThumbnailCapture) { + String style = cell.getStyle(); + final double radiusFactor = displaySettings.getSpotDisplayRadius(); + final String imageStr = spotImageUpdater.getImageString(spot, radiusFactor); + style = mxStyleUtils.setStyle(style, mxConstants.STYLE_IMAGE, "data:image/base64," + imageStr); + graph.getModel().setStyle(cell, style); + } + } finally { + graph.getModel().endUpdate(); + } + return cell; + } + + /** + * Insert a spot in the {@link TrackSchemeFrame}, by creating a + * {@link mxCell} in the graph model of this frame and position it according + * to its feature. + */ + private mxICell insertSpotInGraph(final Spot spot, final int targetColumn) { + mxICell cellAdded = graph.getCellFor(spot); + if (cellAdded != null) { + // cell for spot already exist, do nothing and return original spot + return cellAdded; + } + // Instantiate JGraphX cell + cellAdded = graph.addJGraphTVertex(spot); + // Position it + final int row = spot.getFeature(Spot.FRAME).intValue(); + final double x = (targetColumn - 1) * X_COLUMN_SIZE - DEFAULT_CELL_WIDTH / 2; + final double y = (0.5 + row) * Y_COLUMN_SIZE - DEFAULT_CELL_HEIGHT / 2; + final mxGeometry geometry = new mxGeometry(x, y, DEFAULT_CELL_WIDTH, DEFAULT_CELL_HEIGHT); + cellAdded.setGeometry(geometry); + // Set its style + final double radiusFactor = displaySettings.getSpotDisplayRadius(); + if (null != spotImageUpdater && doThumbnailCapture) { + final String imageStr = spotImageUpdater.getImageString(spot, radiusFactor); + graph.getModel().setStyle(cellAdded, mxConstants.STYLE_IMAGE + "=" + "data:image/base64," + imageStr); + } + return cellAdded; + } + + /** + * Import a whole track from the {@link Model} and make it visible. + * + * @param trackIndex the index of the track to show in TrackScheme + */ + private void importTrack(final int trackIndex) { + model.beginUpdate(); + graph.getModel().beginUpdate(); + try { + // Flag original track as visible + model.setTrackVisibility(trackIndex, true); + // Find adequate column + final int targetColumn = getUnlaidSpotColumn(); + // Create cells for track + final Set trackSpots = model.getTrackModel().trackSpots(trackIndex); + for (final Spot trackSpot : trackSpots) { + final int frame = trackSpot.getFeature(Spot.FRAME).intValue(); + final int column = Math.max(targetColumn, getNextFreeColumn(frame)); + insertSpotInGraph(trackSpot, column); + rowLengths.put(frame, column); + } + final Set trackEdges = model.getTrackModel().trackEdges(trackIndex); + for (final DefaultWeightedEdge trackEdge : trackEdges) { + graph.addJGraphTEdge(trackEdge); + } + } finally { + model.endUpdate(); + graph.getModel().endUpdate(); + } + } + + /** + * This method is called when the user has created manually an edge in the + * graph, by dragging a link between two spot cells. It checks whether the + * matching edge in the model exists, and tune what should be done + * accordingly. + * + * @param cell the mxCell of the edge that has been manually created. + */ + protected void addEdgeManually(mxCell cell) { + if (cell.isEdge()) { + final mxIGraphModel graphModel = graph.getModel(); + cell.setValue("New"); + model.beginUpdate(); + graphModel.beginUpdate(); + try { + + Spot source = graph.getSpotFor(cell.getSource()); + Spot target = graph.getSpotFor(cell.getTarget()); + + if (Spot.frameComparator.compare(source, target) == 0) { + /* + * Prevent adding edges between spots that belong to the + * same frame + */ + graph.removeCells(new Object[]{cell}); + + } else { + /* + * We can add it to the model Put them right in order: since + * we use a oriented graph, we want the source spot to + * precede in time. + */ + if (Spot.frameComparator.compare(source, target) > 0) { + final Spot tmp = source; + source = target; + target = tmp; + } + /* + * We add a new jGraphT edge to the underlying model, if it + * does not exist yet. + */ + DefaultWeightedEdge edge = model.getTrackModel().getEdge(source, target); + if (null == edge) { + edge = model.addEdge(source, target, -1); + } else { + /* + * Ah. There was an existing edge in the model we were + * trying to re-add there, from the graph. We remove the + * graph edge we have added, + */ + graph.removeCells(new Object[]{cell}); + // And re-create a graph edge from the model edge. + cell = graph.addJGraphTEdge(edge); + cell.setValue(String.format("%.1f", model.getTrackModel().getEdgeWeight(edge))); + /* + * We also need now to check if the edge belonged to a + * visible track. If not, we make it visible. + */ + final int ID = model.getTrackModel().trackIDOf(edge); + /* + * This will work, because track indices will be + * reprocessed only after the graphModel.endUpdate() + * reaches 0. So now, it's like we are dealing with the + * track indices priori to modification. + */ + if (!model.getTrackModel().isVisible(ID)) + importTrack(ID); + } + graph.mapEdgeToCell(edge, cell); + } + + } finally { + graphModel.endUpdate(); + model.endUpdate(); + selectionModel.clearEdgeSelection(); + } + } + } + + /* + * OVERRIDEN METHODS + */ + + @Override + public void selectionChanged(final SelectionChangeEvent event) { + if (!doFireSelectionChangeEvent) + return; + + doFireSelectionChangeEvent = false; + + final ArrayList newSelection = new ArrayList<>(selectionModel.getSpotSelection().size() + selectionModel.getEdgeSelection().size()); + final Iterator edgeIt = selectionModel.getEdgeSelection().iterator(); + while (edgeIt.hasNext()) { + final mxICell cell = graph.getCellFor(edgeIt.next()); + if (null != cell) + newSelection.add(cell); + } + + final Iterator spotIt = selectionModel.getSpotSelection().iterator(); + while (spotIt.hasNext()) { + final mxICell cell = graph.getCellFor(spotIt.next()); + if (null != cell) + newSelection.add(cell); + } + final mxGraphSelectionModel mGSmodel = graph.getSelectionModel(); + mGSmodel.setCells(newSelection.toArray()); + + // Center on selection if we added one spot exactly + final Map spotsAdded = event.getSpots(); + if (spotsAdded != null && spotsAdded.size() == 1) { + final boolean added = spotsAdded.values().iterator().next(); + if (added) { + final Spot spot = spotsAdded.keySet().iterator().next(); + centerViewOn(spot); + } + } + doFireSelectionChangeEvent = true; + } + + @Override + public void centerViewOn(final Spot spot) { + gui.centerViewOn(graph.getCellFor(spot)); + } + + /** + * Used to catch spot creation events that occurred elsewhere, for instance + * by manual editing in the {@link AbstractTrackMateModelView}. + *

+ * We have to deal with the graph modification ourselves here, because the + * {@link Model} model holds a non-listenable JGraphT instance. A + * modification made to the model would not be reflected on the graph here. + */ + @Override + public void modelChanged(final ModelChangeEvent event) { + // Only catch model changes + if (event.getEventID() != ModelChangeEvent.MODEL_MODIFIED) + return; + + graph.getModel().beginUpdate(); + try { + final ArrayList cellsToRemove = new ArrayList<>(); + + final int targetColumn = getUnlaidSpotColumn(); + + // Deal with spots + if (!event.getSpots().isEmpty()) { + + final Collection spotsWithStyleToUpdate = new HashSet<>(); + + for (final Spot spot : event.getSpots()) { + + if (event.getSpotFlag(spot) == ModelChangeEvent.FLAG_SPOT_ADDED) { + + final int frame = spot.getFeature(Spot.FRAME).intValue(); + // Put in the graph + final int column = Math.max(targetColumn, getNextFreeColumn(frame)); + final mxICell newCell = insertSpotInGraph(spot, column); + rowLengths.put(frame, column); + spotsWithStyleToUpdate.add((mxCell) newCell); + + } else if (event.getSpotFlag(spot) == ModelChangeEvent.FLAG_SPOT_MODIFIED) { + + // Change the look of the cell + final mxICell cell = updateCellOf(spot); + spotsWithStyleToUpdate.add((mxCell) cell); + + } else if (event.getSpotFlag(spot) == ModelChangeEvent.FLAG_SPOT_REMOVED) { + + final mxICell cell = graph.getCellFor(spot); + cellsToRemove.add(cell); + + } + } + graph.removeCells(cellsToRemove.toArray(), true); + stylist.updateVertexStyle(spotsWithStyleToUpdate); + } + + } finally { + graph.getModel().endUpdate(); + } + + // Deal with edges + if (!event.getEdges().isEmpty()) { + + graph.getModel().beginUpdate(); + try { + + if (event.getEdges().size() > 0) { + + /* + * Here we keep track of the spot and edge cells which style + * we need to update. + */ + final Collection edgesToUpdate = new ArrayList<>(); + final Collection spotsWithStyleToUpdate = new ArrayList<>(); + + for (final DefaultWeightedEdge edge : event.getEdges()) { + + if (event.getEdgeFlag(edge) == ModelChangeEvent.FLAG_EDGE_ADDED) { + + mxCell edgeCell = graph.getCellFor(edge); + if (null == edgeCell) { + + // Make sure target & source cells exist + final Spot source = model.getTrackModel().getEdgeSource(edge); + final mxCell sourceCell = graph.getCellFor(source); + final Spot target = model.getTrackModel().getEdgeTarget(edge); + final mxCell targetCell = graph.getCellFor(target); + + if (sourceCell == null || targetCell == null) { + /* + * Is this missing cell missing because it + * belongs to an invisible track? We then + * have to import all the spot and edges. + */ + final Integer trackID = model.getTrackModel().trackIDOf(edge); + final Set trackSpots = model.getTrackModel().trackSpots(trackID); + for (final Spot trackSpot : trackSpots) { + final mxCell spotCell = graph.getCellFor(trackSpot); + if (spotCell == null) { + final int frame = trackSpot.getFeature(Spot.FRAME).intValue(); + // Put in the graph + final int targetColumn = getUnlaidSpotColumn(); + final int column = Math.max(targetColumn, getNextFreeColumn(frame)); + // move in right+1 free column + final mxCell spotCellAdded = (mxCell) insertSpotInGraph(trackSpot, column); + rowLengths.put(frame, column); + spotsWithStyleToUpdate.add(spotCellAdded); + } + } + + final Set trackEdges = model.getTrackModel().trackEdges(trackID); + /* + * Keep track of edges which style must be + * updated. + */ + + /* + * Loop over edges. Those who do not have a + * cell get a cell. + */ + for (final DefaultWeightedEdge trackEdge : trackEdges) { + mxCell edgeCellToAdd = graph.getCellFor(trackEdge); + if (null == edgeCellToAdd) { + edgeCellToAdd = graph.addJGraphTEdge(trackEdge); + graph.getModel().add(graph.getDefaultParent(), edgeCellToAdd, 0); + edgesToUpdate.add(edgeCellToAdd); + } + } + } + + // And finally create the edge cell + edgeCell = graph.addJGraphTEdge(edge); + } + + graph.getModel().add(graph.getDefaultParent(), edgeCell, 0); + edgesToUpdate.add(edgeCell); + } else if (event.getEdgeFlag(edge) == ModelChangeEvent.FLAG_EDGE_MODIFIED) { + // Add it to the map of cells to recolor + edgesToUpdate.add(graph.getCellFor(edge)); + + } else if (event.getEdgeFlag(edge) == ModelChangeEvent.FLAG_EDGE_REMOVED) { + + final mxCell cell = graph.getCellFor(edge); + graph.removeCells(new Object[]{cell}); + } + } + + stylist.updateEdgeStyle(edgesToUpdate); + stylist.updateVertexStyle(spotsWithStyleToUpdate); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + gui.graphComponent.refresh(); + gui.graphComponent.repaint(); + } + }); + + } + } finally { + graph.getModel().endUpdate(); + } + } + } + + @Override + public void render() { + final long start = System.currentTimeMillis(); + // Graph to mirror model + this.graph = createGraph(); + gui.logger.setProgress(0.5); + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + // Pass graph to GUI + gui.logger.setStatus("Generating GUI components."); + gui.init(graph); + + // Init functions that set look and position + gui.logger.setStatus("Creating style manager."); + TrackScheme.this.stylist = new TrackSchemeStylist(model, graph, displaySettings); + gui.logger.setStatus("Creating layout manager."); + TrackScheme.this.graphLayout = new TrackSchemeGraphLayout(graph, model, gui.graphComponent); + + // Execute style and layout + gui.logger.setProgress(0.75); + doTrackStyle(); + + gui.logger.setStatus("Executing layout."); + doTrackLayout(); + + gui.logger.setProgress(0.9); + + gui.logger.setStatus("Refreshing display."); + gui.graphComponent.refresh(); + final mxRectangle bounds = graph.getView().validateCellState(graph.getDefaultParent(), false); + + // This happens when there is not track to display + if (null == bounds) + return; + + final Dimension dim = new Dimension(); + dim.setSize(bounds.getRectangle().width + bounds.getRectangle().x, bounds.getRectangle().height + bounds.getRectangle().y); + gui.graphComponent.getGraphControl().setPreferredSize(dim); + gui.logger.setStatus(""); + + gui.graphComponent.zoomOut(); + gui.graphComponent.zoomOut(); + + gui.logger.setProgress(0); + final long end = System.currentTimeMillis(); + gui.logger.log(String.format("TrackScheme rendering done in %.1f s.", (end - start) / 1000d)); + gui.revalidate(); + } + }); + } + + @Override + public void refresh() { + } + + @Override + public void clear() { + System.out.println("[TrackScheme] clear() called"); + } + + @Override + public Model getModel() { + return model; + } + + /* + * PRIVATE METHODS + */ + + /** + * Called when the user makes a selection change in the graph. Used to + * forward this event to the {@link InfoPane} and to other + * {@link SelectionChangeListener}s. + * + * @param added the cells removed from selection (careful, inverted) + * @param removed the cells added to selection (careful, inverted) + */ + private void userChangedSelection(final Collection added, final Collection removed) { // Seems to be inverted + if (!doFireSelectionChangeEvent) { + return; + } + final Collection spotsToAdd = new ArrayList<>(); + final Collection spotsToRemove = new ArrayList<>(); + final Collection edgesToAdd = new ArrayList<>(); + final Collection edgesToRemove = new ArrayList<>(); + + if (null != added) { + for (final Object obj : added) { + final mxCell cell = (mxCell) obj; + + if (cell.getChildCount() > 0) { + + for (int i = 0; i < cell.getChildCount(); i++) { + final mxICell child = cell.getChildAt(i); + if (child.isVertex()) { + final Spot spot = graph.getSpotFor(child); + spotsToRemove.add(spot); + } else { + final DefaultWeightedEdge edge = graph.getEdgeFor(child); + edgesToRemove.add(edge); + } + } + + } else { + + if (cell.isVertex()) { + final Spot spot = graph.getSpotFor(cell); + spotsToRemove.add(spot); + } else { + final DefaultWeightedEdge edge = graph.getEdgeFor(cell); + edgesToRemove.add(edge); + } + } + } + } + + if (null != removed) { + for (final Object obj : removed) { + final mxCell cell = (mxCell) obj; + + if (cell.getChildCount() > 0) { + + for (int i = 0; i < cell.getChildCount(); i++) { + final mxICell child = cell.getChildAt(i); + if (child.isVertex()) { + final Spot spot = graph.getSpotFor(child); + spotsToAdd.add(spot); + } else { + final DefaultWeightedEdge edge = graph.getEdgeFor(child); + edgesToAdd.add(edge); + } + } + + } else { + + if (cell.isVertex()) { + final Spot spot = graph.getSpotFor(cell); + spotsToAdd.add(spot); + } else { + final DefaultWeightedEdge edge = graph.getEdgeFor(cell); + edgesToAdd.add(edge); + } + } + } + } + + doFireSelectionChangeEvent = false; + + if (!edgesToAdd.isEmpty()) + selectionModel.addEdgeToSelection(edgesToAdd); + + if (!spotsToAdd.isEmpty()) + selectionModel.addSpotToSelection(spotsToAdd); + + if (!edgesToRemove.isEmpty()) + selectionModel.removeEdgeFromSelection(edgesToRemove); + + if (!spotsToRemove.isEmpty()) + selectionModel.removeSpotFromSelection(spotsToRemove); + + doFireSelectionChangeEvent = true; + } + + /* + * INNER CLASSES + */ + + /** + * Toggles whether drag-&-drop linking is allowed. + * + * @return the current settings value, after toggling. + */ + public boolean toggleLinking() { + final boolean enabled = gui.graphComponent.getConnectionHandler().isEnabled(); + gui.graphComponent.getConnectionHandler().setEnabled(!enabled); + return !enabled; + } + + /** + * Toggles whether thumbnail capture is enabled. + * + * @return the current settings value, after toggling. + */ + public boolean toggleThumbnail() { + if (!doThumbnailCapture) + createThumbnails(); + + doThumbnailCapture = !doThumbnailCapture; + return doThumbnailCapture; + } + + /* + * ACTIONS called from gui parts + */ + + public void zoomIn() { + gui.graphComponent.zoomIn(); + } + + public void zoomOut() { + gui.graphComponent.zoomOut(); + } + + public void resetZoom() { + gui.graphComponent.zoomActual(); + } + + public void doTrackStyle() { + if (null == stylist) + return; + + gui.logger.setStatus("Setting style."); + graph.getModel().beginUpdate(); + try { + stylist.updateEdgeStyle(graph.getEdgeCells()); + stylist.updateVertexStyle(graph.getVertexCells()); + } finally { + graph.getModel().endUpdate(); + } + } + + /** + * Captures and stores the thumbnail image that will be displayed in each + * spot cell, when using styles that can display images. + */ + private void createThumbnails() { + // Group spots per frame + final Set frames = model.getSpots().keySet(); + final HashMap> spotPerFrame = new HashMap<>(frames.size()); + for (final Integer frame : frames) + spotPerFrame.put(frame, new HashSet(model.getSpots().getNSpots(frame, true))); // max + + for (final Integer trackID : model.getTrackModel().trackIDs(true)) { + for (final Spot spot : model.getTrackModel().trackSpots(trackID)) { + final int frame = spot.getFeature(Spot.FRAME).intValue(); + spotPerFrame.get(frame).add(spot); + } + } + + // Set spot image to cell style + if (null != spotImageUpdater) { + gui.logger.setStatus("Collecting spot thumbnails."); + final double radiusFactor = displaySettings.getSpotDisplayRadius(); + int index = 0; + try { + graph.getModel().beginUpdate(); + + // Iterate per frame + for (final Integer frame : frames) { + for (final Spot spot : spotPerFrame.get(frame)) { + final mxICell cell = graph.getCellFor(spot); + final String imageStr = spotImageUpdater.getImageString(spot, radiusFactor); + String style = cell.getStyle(); + style = mxStyleUtils.setStyle(style, mxConstants.STYLE_IMAGE, "data:image/base64," + imageStr); + graph.getModel().setStyle(cell, style); + + } + gui.logger.setProgress((double) index++ / frames.size()); + } + } finally { + graph.getModel().endUpdate(); + gui.logger.setProgress(0d); + gui.logger.setStatus(""); + } + } + } + + public void doTrackLayout() { + // Position cells + graphLayout.execute(null); + rowLengths = graphLayout.getRowLengths(); + int maxLength = 2; + for (final int rowLength : rowLengths.values()) { + if (maxLength < rowLength) + maxLength = rowLength; + } + unlaidSpotColumn = maxLength; + gui.graphComponent.refresh(); + gui.graphComponent.repaint(); + } + + public void captureUndecorated() { + final BufferedImage image = mxCellRenderer.createBufferedImage(graph, null, 1, Color.WHITE, true, null, gui.graphComponent.getCanvas()); + final ImagePlus imp = new ImagePlus("TrackScheme capture", image); + imp.show(); + } + + public void captureDecorated() { + final JViewport view = gui.graphComponent.getViewport(); + final Point currentPos = view.getViewPosition(); + view.setViewPosition(new Point(0, 0)); + // We have to do that otherwise, top left is not painted. + final Dimension size = view.getViewSize(); + final BufferedImage image = (BufferedImage) view.createImage(size.width, size.height); + final Graphics2D captureG = image.createGraphics(); + view.paintComponents(captureG); + view.setViewPosition(currentPos); + final ImagePlus imp = new ImagePlus("TrackScheme capture", image); + imp.show(); + } + + public void toggleDisplayDecoration() { + gui.graphComponent.loopPaintDecorationLevel(); + gui.graphComponent.repaint(); + } + + /** + * Create links between all the spots currently in the {@link Model} + * selection. We update simultaneously the {@link Model} and the + * {@link JGraphXAdapter}. + */ + public void linkSpots() { + + // Sort spots by time + final TreeMap spotsInTime = new TreeMap<>(); + for (final Spot spot : selectionModel.getSpotSelection()) + spotsInTime.put(spot.getFeature(Spot.FRAME).intValue(), spot); + + // Find adequate column + final int targetColumn = getUnlaidSpotColumn(); + + // Then link them in this order + model.beginUpdate(); + graph.getModel().beginUpdate(); + try { + final Iterator it = spotsInTime.keySet().iterator(); + final Integer previousTime = it.next(); + Spot previousSpot = spotsInTime.get(previousTime); + // If this spot belong to an invisible track, we make it visible + Integer ID = model.getTrackModel().trackIDOf(previousSpot); + if (ID != null && !model.getTrackModel().isVisible(ID)) + importTrack(ID); + + while (it.hasNext()) { + final Integer currentTime = it.next(); + final Spot currentSpot = spotsInTime.get(currentTime); + // If this spot belong to an invisible track, we make it visible + ID = model.getTrackModel().trackIDOf(currentSpot); + if (ID != null && !model.getTrackModel().isVisible(ID)) + importTrack(ID); + + // Check that the cells matching the 2 spots exist in the graph + mxICell currentCell = graph.getCellFor(currentSpot); + if (null == currentCell) + currentCell = insertSpotInGraph(currentSpot, targetColumn); + + mxICell previousCell = graph.getCellFor(previousSpot); + if (null == previousCell) { + final int frame = previousSpot.getFeature(Spot.FRAME).intValue(); + final int column = Math.max(targetColumn, getNextFreeColumn(frame)); + rowLengths.put(frame, column); + previousCell = insertSpotInGraph(previousSpot, column); + } + + /* + * Check if the model does not have already a edge for these 2 + * spots (that is the case if the 2 spot are in an invisible + * track, which track scheme does not know of). + */ + DefaultWeightedEdge edge = model.getTrackModel().getEdge(previousSpot, currentSpot); + if (null == edge) { + /* + * We create a new edge between 2 spots, and pair it with a + * new cell edge. + */ + edge = model.addEdge(previousSpot, currentSpot, -1); + final mxCell cell = graph.addJGraphTEdge(edge); + cell.setValue("New"); + } else { + // We retrieve the edge, and pair it with a new cell edge. + final mxCell cell = graph.addJGraphTEdge(edge); + cell.setValue(String.format("%.1f", model.getTrackModel().getEdgeWeight(edge))); + /* + * Also, if the existing edge belonged to an existing + * invisible track, we make it visible. + */ + ID = model.getTrackModel().trackIDOf(edge); + if (ID != null && !model.getTrackModel().isVisible(ID)) + importTrack(ID); + } + previousSpot = currentSpot; + } + } finally { + graph.getModel().endUpdate(); + model.endUpdate(); + } + } + + /** + * Removes the cell selected by the user in the GUI. + */ + public void removeSelectedCells() { + graph.getModel().beginUpdate(); + try { + graph.removeCells(graph.getSelectionCells()); + // Will be caught by the graph listeners + } finally { + graph.getModel().endUpdate(); + } + } + + public void removeSelectedLinkCells() { + List edgeCells = new ArrayList<>(); + for (Object obj : graph.getSelectionCells()) { + DefaultWeightedEdge e = graph.getEdgeFor((mxICell) obj); + if (e == null) + continue; + + edgeCells.add(obj); + } + + graph.getModel().beginUpdate(); + try { + graph.removeCells(edgeCells.toArray()); + // Will be caught by the graph listeners + } finally { + graph.getModel().endUpdate(); + } + } + + @Override + public String getKey() { + return KEY; + } + + private class CellRemovalListener implements mxIEventListener { + + @Override + public void invoke(final Object sender, final mxEventObject evt) { + if (!doFireModelChangeEvent) + return; + + // Separate spots from edges + final Object[] objects = (Object[]) evt.getProperty("cells"); + final HashSet spotsToRemove = new HashSet<>(); + final ArrayList edgesToRemove = new ArrayList<>(); + for (final Object obj : objects) { + final mxCell cell = (mxCell) obj; + if (null != cell) { + if (cell.isVertex()) { + // Build list of removed spots + final Spot spot = graph.getSpotFor(cell); + spotsToRemove.add(spot); + // Clean maps + graph.removeMapping(spot); + } else if (cell.isEdge()) { + // Build list of removed edges + final DefaultWeightedEdge edge = graph.getEdgeFor(cell); + if (null == edge) + continue; + + edgesToRemove.add(edge); + // Clean maps + graph.removeMapping(edge); + } + } + } + + evt.consume(); + + // Clean model + doFireModelChangeEvent = false; + model.beginUpdate(); + try { + selectionModel.clearSelection(); + /* + * We remove edges first so that we ensure we do not end having + * orphan edges. Normally JGraphT handles that well, but we + * enforce things here. To be sure. + */ + for (final DefaultWeightedEdge edge : edgesToRemove) + model.removeEdge(edge); + + for (final Spot spot : spotsToRemove) + model.removeSpot(spot); + + } finally { + model.endUpdate(); + } + doFireModelChangeEvent = true; + } + } + + private class SelectionChangeListener implements mxIEventListener { + + @Override + @SuppressWarnings("unchecked") + public void invoke(final Object sender, final mxEventObject evt) { + if (!doFireSelectionChangeEvent || sender != graph.getSelectionModel()) + return; + + final Collection added = (Collection) evt.getProperty("added"); + final Collection removed = (Collection) evt.getProperty("removed"); + userChangedSelection(added, removed); + } + } } diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemePopupMenu.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemePopupMenu.java index dff4f5471..1f876c6d5 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemePopupMenu.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemePopupMenu.java @@ -8,12 +8,12 @@ * 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 * . @@ -21,408 +21,333 @@ */ package fiji.plugin.trackmate.visualization.trackscheme; -import java.awt.Color; -import java.awt.Point; -import java.awt.event.ActionEvent; -import java.util.ArrayList; -import java.util.EventObject; - -import javax.swing.AbstractAction; -import javax.swing.Action; -import javax.swing.JColorChooser; -import javax.swing.JPopupMenu; -import javax.swing.SwingUtilities; - -import org.jgrapht.graph.DefaultWeightedEdge; - import com.mxgraph.model.mxCell; import com.mxgraph.swing.mxGraphComponent; import com.mxgraph.util.mxEvent; import com.mxgraph.util.mxEventObject; import com.mxgraph.util.mxEventSource.mxIEventListener; - import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.features.manual.ManualEdgeColorAnalyzer; import fiji.plugin.trackmate.features.manual.ManualSpotColorAnalyzerFactory; +import org.jgrapht.graph.DefaultWeightedEdge; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.EventObject; -public class TrackSchemePopupMenu extends JPopupMenu -{ - - private static final long serialVersionUID = -1L; - - /** - * The cell where the right-click was made, null if the - * right-click is made out of a cell. - */ - private final Object cell; - - /** The TrackScheme instance. */ - private final TrackScheme trackScheme; - - /** The right-click location. */ - private final Point point; - - private static Color previousColor = Color.RED; - - public TrackSchemePopupMenu( final TrackScheme trackScheme, final Object cell, final Point point ) - { - this.trackScheme = trackScheme; - this.cell = cell; - this.point = point; - init(); - } - - /* - * ACTIONS - */ - - private void manualColorEdges( final ArrayList< mxCell > edges ) - { - for ( final mxCell mxCell : edges ) - { - final DefaultWeightedEdge edge = trackScheme.getGraph().getEdgeFor( mxCell ); - final Double value = Double.valueOf( previousColor.getRGB() ); - trackScheme.getModel().getFeatureModel().putEdgeFeature( edge, ManualEdgeColorAnalyzer.FEATURE, value ); - } - } - - private void manualColorVertices( final ArrayList< mxCell > vertices ) - { - for ( final mxCell mxCell : vertices ) - { - final Spot spot = trackScheme.getGraph().getSpotFor( mxCell ); - final Double value = Double.valueOf( previousColor.getRGB() ); - spot.putFeature( ManualSpotColorAnalyzerFactory.FEATURE, value ); - } - } - - private void selectWholeTrack( final ArrayList< mxCell > vertices, final ArrayList< mxCell > edges ) - { - trackScheme.selectTrack( vertices, edges, 0 ); - } - - private void selectTrackDownwards( final ArrayList< mxCell > vertices, final ArrayList< mxCell > edges ) - { - trackScheme.selectTrack( vertices, edges, -1 ); - } - - private void selectTrackUpwards( final ArrayList< mxCell > vertices, final ArrayList< mxCell > edges ) - { - trackScheme.selectTrack( vertices, edges, 1 ); - } - - private void editSpotName() - { - trackScheme.getGUI().graphComponent.startEditingAtCell( cell ); - } - - @SuppressWarnings( "unused" ) - private void toggleBranchFolding() - { - Object parent; - if ( trackScheme.getGraph().isCellFoldable( cell, true ) ) - { - parent = cell; - } - else - { - parent = trackScheme.getGraph().getModel().getParent( cell ); - } - trackScheme.getGraph().foldCells( !trackScheme.getGraph().isCellCollapsed( parent ), false, new Object[] { parent } ); - } - - private void multiEditSpotName( final ArrayList< mxCell > vertices, final EventObject triggerEvent ) - { - /* - * We want to display the editing window in the cell that is the closer - * to where the user clicked. That is not perfect, because we can - * imagine the click is made for from the selected cells, and that the - * editing window will not even be displayed on the screen. No idea for - * that yet, because JGraphX is expecting to receive a cell as location - * for the editing window. - */ - final mxCell tc = getClosestCell( vertices ); - vertices.remove( tc ); - final mxGraphComponent graphComponent = trackScheme.getGUI().graphComponent; - graphComponent.startEditingAtCell( tc, triggerEvent ); - graphComponent.addListener( mxEvent.LABEL_CHANGED, new mxIEventListener() - { - - @Override - public void invoke( final Object sender, final mxEventObject evt ) - { - for ( final mxCell lCell : vertices ) - { - lCell.setValue( tc.getValue() ); - trackScheme.getGraph().getSpotFor( lCell ).setName( tc.getValue().toString() ); - } - graphComponent.refresh(); - graphComponent.removeListener( this ); - } - } ); - } - - /** - * Return, from the given list of cell, the one which is the closer to the - * {@link #point} of this instance. - */ - private mxCell getClosestCell( final Iterable< mxCell > vertices ) - { - double min_dist = Double.POSITIVE_INFINITY; - mxCell target_cell = null; - for ( final mxCell lCell : vertices ) - { - final Point location = lCell.getGeometry().getPoint(); - final double dist = location.distanceSq( point ); - if ( dist < min_dist ) - { - min_dist = dist; - target_cell = lCell; - } - } - return target_cell; - } - - private void linkSpots() - { - trackScheme.linkSpots(); - } - - private void remove() - { - trackScheme.removeSelectedCells(); - } - - private void removeLinks() - { - trackScheme.removeSelectedLinkCells(); - } - - /* - * MENU COMPOSITION - */ - - @SuppressWarnings( "serial" ) - private void init() - { - - // Build selection categories - final Object[] selection = trackScheme.getGraph().getSelectionCells(); - final ArrayList< mxCell > vertices = new ArrayList<>(); - final ArrayList< mxCell > edges = new ArrayList<>(); - for ( final Object obj : selection ) - { - final mxCell lCell = ( mxCell ) obj; - if ( lCell.isVertex() ) - vertices.add( lCell ); - else if ( lCell.isEdge() ) - edges.add( lCell ); - } - - // Select whole tracks - if ( vertices.size() > 0 || edges.size() > 0 ) - { - - add( new AbstractAction( "Select whole track" ) - { - @Override - public void actionPerformed( final ActionEvent e ) - { - selectWholeTrack( vertices, edges ); - } - } ); - - add( new AbstractAction( "Select track downwards" ) - { - @Override - public void actionPerformed( final ActionEvent e ) - { - selectTrackDownwards( vertices, edges ); - } - } ); - - add( new AbstractAction( "Select track upwards" ) - { - @Override - public void actionPerformed( final ActionEvent e ) - { - selectTrackUpwards( vertices, edges ); - } - } ); - } - - if ( cell != null ) - { - // Edit - add( new AbstractAction( "Edit spot name" ) - { - @Override - public void actionPerformed( final ActionEvent e ) - { - editSpotName(); - } - } ); - - } - else - { - - if ( vertices.size() > 1 ) - { - - // Multi edit - add( new AbstractAction( "Edit " + vertices.size() + " spot names" ) - { - @Override - public void actionPerformed( final ActionEvent e ) - { - multiEditSpotName( vertices, e ); - } - } ); - } - - // Link - final Action linkAction = new AbstractAction( "Link " + trackScheme.getSelectionModel().getSpotSelection().size() + " spots" ) - { - @Override - public void actionPerformed( final ActionEvent e ) - { - linkSpots(); - } - }; - if ( trackScheme.getSelectionModel().getSpotSelection().size() > 1 ) - { - add( linkAction ); - } - } - - /* - * Edges and spot manual coloring - */ - - if ( edges.size() > 0 || vertices.size() > 0 ) - { - addSeparator(); - } - - if ( vertices.size() > 0 ) - { - final String str = "Manual color for " + ( vertices.size() == 1 ? " one spot" : vertices.size() + " spots" ); - add( new AbstractAction( str ) - { - @Override - public void actionPerformed( final ActionEvent e ) - { - previousColor = JColorChooser.showDialog( trackScheme.getGUI(), "Choose Color", previousColor ); - manualColorVertices( vertices ); - SwingUtilities.invokeLater( new Runnable() - { - @Override - public void run() - { - trackScheme.doTrackStyle(); - } - } ); - } - } ); - } - - if ( edges.size() > 0 ) - { - final String str = "Manual color for " + ( edges.size() == 1 ? " one edge" : edges.size() + " edges" ); - add( new AbstractAction( str ) - { - @Override - public void actionPerformed( final ActionEvent e ) - { - previousColor = JColorChooser.showDialog( trackScheme.getGUI(), "Choose Color", previousColor ); - manualColorEdges( edges ); - SwingUtilities.invokeLater( new Runnable() - { - @Override - public void run() - { - trackScheme.doTrackStyle(); - } - } ); - } - } ); - } - - if ( edges.size() > 0 && vertices.size() > 0 ) - { - final String str = "Manual color for " + ( vertices.size() == 1 ? " one spot and " : vertices.size() + " spots and " ) + ( edges.size() == 1 ? " one edge" : edges.size() + " edges" ); - add( new AbstractAction( str ) - { - @Override - public void actionPerformed( final ActionEvent e ) - { - previousColor = JColorChooser.showDialog( trackScheme.getGUI(), "Choose Color", previousColor ); - manualColorVertices( vertices ); - manualColorEdges( edges ); - SwingUtilities.invokeLater( new Runnable() - { - @Override - public void run() - { - trackScheme.doTrackStyle(); - } - } ); - } - } ); - } - - add( new AbstractAction( "Clear manual color of selection" ) - { - @Override - public void actionPerformed( final ActionEvent e ) - { - for ( final mxCell mxCell : vertices ) - { - final Spot spot = trackScheme.getGraph().getSpotFor( mxCell ); - spot.getFeatures().remove( ManualSpotColorAnalyzerFactory.FEATURE ); - } - for ( final mxCell mxCell : edges ) - { - final DefaultWeightedEdge edge = trackScheme.getGraph().getEdgeFor( mxCell ); - trackScheme.getModel().getFeatureModel().removeEdgeFeature( edge, ManualEdgeColorAnalyzer.FEATURE ); - } - - SwingUtilities.invokeLater( new Runnable() - { - @Override - public void run() - { - trackScheme.doTrackStyle(); - } - } ); - } - } ); - - // Remove - if ( selection.length > 0 ) - { - addSeparator(); - final Action removeAction = new AbstractAction( "Remove spots and links" ) - { - @Override - public void actionPerformed( final ActionEvent e ) - { - remove(); - } - }; - add( removeAction ); - final Action removeLinkAction = new AbstractAction( "Remove only links" ) - { - @Override - public void actionPerformed( final ActionEvent e ) - { - removeLinks(); - } - }; - add( removeLinkAction ); - - } - } +public class TrackSchemePopupMenu extends JPopupMenu { + + private static final long serialVersionUID = -1L; + private static Color previousColor = Color.RED; + /** + * The cell where the right-click was made, null if the + * right-click is made out of a cell. + */ + private final Object cell; + /** + * The TrackScheme instance. + */ + private final TrackScheme trackScheme; + /** + * The right-click location. + */ + private final Point point; + private JGraphXAdapter jGraphXAdapter; + + public TrackSchemePopupMenu(final TrackScheme trackScheme, final Object cell, final Point point) { + this.trackScheme = trackScheme; + this.cell = cell; + this.point = point; + init(); + } + + /* + * ACTIONS + */ + + private void manualColorEdges(final ArrayList edges) { + for (final mxCell mxCell : edges) { + final DefaultWeightedEdge edge = trackScheme.getGraph().getEdgeFor(mxCell); + final Double value = Double.valueOf(previousColor.getRGB()); + trackScheme.getModel().getFeatureModel().putEdgeFeature(edge, ManualEdgeColorAnalyzer.FEATURE, value); + } + } + + private void manualColorVertices(final ArrayList vertices) { + for (final mxCell mxCell : vertices) { + final Spot spot = trackScheme.getGraph().getSpotFor(mxCell); + final Double value = Double.valueOf(previousColor.getRGB()); + spot.putFeature(ManualSpotColorAnalyzerFactory.FEATURE, value); + } + } + + private void selectWholeTrack(final ArrayList vertices, final ArrayList edges) { + jGraphXAdapter = new JGraphXAdapter(trackScheme.getModel(), trackScheme.getSelectionModel()); + jGraphXAdapter.selectTrack(vertices, edges, 0); + } + + private void selectTrackDownwards(final ArrayList vertices, final ArrayList edges) { + jGraphXAdapter = new JGraphXAdapter(trackScheme.getModel(), trackScheme.getSelectionModel()); + jGraphXAdapter.selectTrack(vertices, edges, -1); + } + + private void selectTrackUpwards(final ArrayList vertices, final ArrayList edges) { + jGraphXAdapter = new JGraphXAdapter(trackScheme.getModel(), trackScheme.getSelectionModel()); + jGraphXAdapter.selectTrack(vertices, edges, 1); + } + + private void editSpotName() { + trackScheme.getGUI().graphComponent.startEditingAtCell(cell); + } + + @SuppressWarnings("unused") + private void toggleBranchFolding() { + Object parent; + if (trackScheme.getGraph().isCellFoldable(cell, true)) { + parent = cell; + } else { + parent = trackScheme.getGraph().getModel().getParent(cell); + } + trackScheme.getGraph().foldCells(!trackScheme.getGraph().isCellCollapsed(parent), false, new Object[]{parent}); + } + + private void multiEditSpotName(final ArrayList vertices, final EventObject triggerEvent) { + /* + * We want to display the editing window in the cell that is the closer + * to where the user clicked. That is not perfect, because we can + * imagine the click is made for from the selected cells, and that the + * editing window will not even be displayed on the screen. No idea for + * that yet, because JGraphX is expecting to receive a cell as location + * for the editing window. + */ + final mxCell tc = getClosestCell(vertices); + vertices.remove(tc); + final mxGraphComponent graphComponent = trackScheme.getGUI().graphComponent; + graphComponent.startEditingAtCell(tc, triggerEvent); + graphComponent.addListener(mxEvent.LABEL_CHANGED, new mxIEventListener() { + + @Override + public void invoke(final Object sender, final mxEventObject evt) { + for (final mxCell lCell : vertices) { + lCell.setValue(tc.getValue()); + trackScheme.getGraph().getSpotFor(lCell).setName(tc.getValue().toString()); + } + graphComponent.refresh(); + graphComponent.removeListener(this); + } + }); + } + + /** + * Return, from the given list of cell, the one which is the closer to the + * {@link #point} of this instance. + */ + private mxCell getClosestCell(final Iterable vertices) { + double min_dist = Double.POSITIVE_INFINITY; + mxCell target_cell = null; + for (final mxCell lCell : vertices) { + final Point location = lCell.getGeometry().getPoint(); + final double dist = location.distanceSq(point); + if (dist < min_dist) { + min_dist = dist; + target_cell = lCell; + } + } + return target_cell; + } + + private void linkSpots() { + trackScheme.linkSpots(); + } + + private void remove() { + trackScheme.removeSelectedCells(); + } + + private void removeLinks() { + trackScheme.removeSelectedLinkCells(); + } + + /* + * MENU COMPOSITION + */ + + @SuppressWarnings("serial") + private void init() { + + // Build selection categories + final Object[] selection = trackScheme.getGraph().getSelectionCells(); + final ArrayList vertices = new ArrayList<>(); + final ArrayList edges = new ArrayList<>(); + for (final Object obj : selection) { + final mxCell lCell = (mxCell) obj; + if (lCell.isVertex()) + vertices.add(lCell); + else if (lCell.isEdge()) + edges.add(lCell); + } + + // Select whole tracks + if (vertices.size() > 0 || edges.size() > 0) { + + add(new AbstractAction("Select whole track") { + @Override + public void actionPerformed(final ActionEvent e) { + selectWholeTrack(vertices, edges); + } + }); + + add(new AbstractAction("Select track downwards") { + @Override + public void actionPerformed(final ActionEvent e) { + selectTrackDownwards(vertices, edges); + } + }); + + add(new AbstractAction("Select track upwards") { + @Override + public void actionPerformed(final ActionEvent e) { + selectTrackUpwards(vertices, edges); + } + }); + } + + if (cell != null) { + // Edit + add(new AbstractAction("Edit spot name") { + @Override + public void actionPerformed(final ActionEvent e) { + editSpotName(); + } + }); + + } else { + + if (vertices.size() > 1) { + + // Multi edit + add(new AbstractAction("Edit " + vertices.size() + " spot names") { + @Override + public void actionPerformed(final ActionEvent e) { + multiEditSpotName(vertices, e); + } + }); + } + + // Link + final Action linkAction = new AbstractAction("Link " + trackScheme.getSelectionModel().getSpotSelection().size() + " spots") { + @Override + public void actionPerformed(final ActionEvent e) { + linkSpots(); + } + }; + if (trackScheme.getSelectionModel().getSpotSelection().size() > 1) { + add(linkAction); + } + } + + /* + * Edges and spot manual coloring + */ + + if (edges.size() > 0 || vertices.size() > 0) { + addSeparator(); + } + + if (vertices.size() > 0) { + final String str = "Manual color for " + (vertices.size() == 1 ? " one spot" : vertices.size() + " spots"); + add(new AbstractAction(str) { + @Override + public void actionPerformed(final ActionEvent e) { + previousColor = JColorChooser.showDialog(trackScheme.getGUI(), "Choose Color", previousColor); + manualColorVertices(vertices); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + trackScheme.doTrackStyle(); + } + }); + } + }); + } + + if (edges.size() > 0) { + final String str = "Manual color for " + (edges.size() == 1 ? " one edge" : edges.size() + " edges"); + add(new AbstractAction(str) { + @Override + public void actionPerformed(final ActionEvent e) { + previousColor = JColorChooser.showDialog(trackScheme.getGUI(), "Choose Color", previousColor); + manualColorEdges(edges); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + trackScheme.doTrackStyle(); + } + }); + } + }); + } + + if (edges.size() > 0 && vertices.size() > 0) { + final String str = "Manual color for " + (vertices.size() == 1 ? " one spot and " : vertices.size() + " spots and ") + (edges.size() == 1 ? " one edge" : edges.size() + " edges"); + add(new AbstractAction(str) { + @Override + public void actionPerformed(final ActionEvent e) { + previousColor = JColorChooser.showDialog(trackScheme.getGUI(), "Choose Color", previousColor); + manualColorVertices(vertices); + manualColorEdges(edges); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + trackScheme.doTrackStyle(); + } + }); + } + }); + } + + add(new AbstractAction("Clear manual color of selection") { + @Override + public void actionPerformed(final ActionEvent e) { + for (final mxCell mxCell : vertices) { + final Spot spot = trackScheme.getGraph().getSpotFor(mxCell); + spot.getFeatures().remove(ManualSpotColorAnalyzerFactory.FEATURE); + } + for (final mxCell mxCell : edges) { + final DefaultWeightedEdge edge = trackScheme.getGraph().getEdgeFor(mxCell); + trackScheme.getModel().getFeatureModel().removeEdgeFeature(edge, ManualEdgeColorAnalyzer.FEATURE); + } + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + trackScheme.doTrackStyle(); + } + }); + } + }); + + // Remove + if (selection.length > 0) { + addSeparator(); + final Action removeAction = new AbstractAction("Remove spots and links") { + @Override + public void actionPerformed(final ActionEvent e) { + remove(); + } + }; + add(removeAction); + final Action removeLinkAction = new AbstractAction("Remove only links") { + @Override + public void actionPerformed(final ActionEvent e) { + removeLinks(); + } + }; + add(removeLinkAction); + + } + } } From 82a277d042297ea584b8457c1f54a87773c75d73 Mon Sep 17 00:00:00 2001 From: Divyank Shah Date: Tue, 21 Nov 2023 15:52:29 -0400 Subject: [PATCH 5/7] Added Extract class refactoring and created new HistogramUtils class and moved methods related to histogram from TMUtils clss to HistogramUtils class. --- .../trackmate/gui/components/FilterPanel.java | 974 +++++----- .../kdtree/NearestNeighborTracker.java | 454 +++-- .../plugin/trackmate/util/HistogramUtils.java | 185 ++ .../trackmate/util/QualityHistogramChart.java | 694 ++++---- .../fiji/plugin/trackmate/util/TMUtils.java | 1572 ++++++++--------- 5 files changed, 1896 insertions(+), 1983 deletions(-) create mode 100644 src/main/java/fiji/plugin/trackmate/util/HistogramUtils.java diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/FilterPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/FilterPanel.java index 7083ed193..51d96b7e8 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/FilterPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/FilterPanel.java @@ -8,12 +8,12 @@ * 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 * . @@ -21,44 +21,11 @@ */ package fiji.plugin.trackmate.gui.components; -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.geom.Rectangle2D; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -import javax.swing.ButtonGroup; -import javax.swing.ComboBoxModel; -import javax.swing.DefaultComboBoxModel; -import javax.swing.DefaultListCellRenderer; -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JRadioButton; -import javax.swing.UIManager; -import javax.swing.border.LineBorder; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - +import fiji.plugin.trackmate.features.FeatureFilter; +import fiji.plugin.trackmate.gui.GuiUtils; +import fiji.plugin.trackmate.util.HistogramUtils; +import fiji.plugin.trackmate.util.Threads; +import fiji.util.NumberParser; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; @@ -68,505 +35,444 @@ import org.jfree.chart.renderer.xy.StandardXYBarPainter; import org.jfree.chart.renderer.xy.XYBarRenderer; -import fiji.plugin.trackmate.features.FeatureFilter; -import fiji.plugin.trackmate.gui.GuiUtils; -import fiji.plugin.trackmate.util.Threads; -import fiji.plugin.trackmate.util.TMUtils; -import fiji.util.NumberParser; +import javax.swing.*; +import javax.swing.border.LineBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; /** - * * Revised December 2020. * * @author Jean-Yves Tinevez - * */ -public class FilterPanel extends javax.swing.JPanel -{ - - static final Font FONT = new Font( "Arial", Font.PLAIN, 11 ); - - static final Font SMALL_FONT = FONT.deriveFont( 10f ); - - private static final Color annotationColor; - static - { - final Color bgColor = UIManager.getColor( "Panel.background" ); - final boolean bgIsDark = GuiUtils.colorDistance( Color.WHITE, bgColor ) > 0.5; - annotationColor = bgIsDark - ? new java.awt.Color( 252, 117, 0 ).brighter() - : new java.awt.Color( 252, 117, 0 ); - } - - private static final long serialVersionUID = 1L; - - private static final String DATA_SERIES_NAME = "Data"; - - private final ChangeEvent CHANGE_EVENT = new ChangeEvent( this ); - - private final XYPlot plot; - - private final IntervalMarker intervalMarker; - - private double threshold; - - private final Function< String, double[] > valueCollector; - - private final XYTextSimpleAnnotation annotation; - - private final ArrayList< ChangeListener > listeners = new ArrayList<>(); - - final JRadioButton rdbtnAbove; - - final JRadioButton rdbtnBelow; - - final JComboBox< String > cmbboxFeatureKeys; - - - /* - * CONSTRUCTOR - */ - - public FilterPanel( - final Map< String, String > keyNames, - final Function< String, double[] > valueCollector, - final FeatureFilter filter ) - { - this.valueCollector = valueCollector; - - final Dimension panelSize = new java.awt.Dimension( 250, 140 ); - final Dimension panelMaxSize = new java.awt.Dimension( 1000, 140 ); - final GridBagLayout thisLayout = new GridBagLayout(); - thisLayout.rowWeights = new double[] { 0.0, 1.0, 0.0 }; - thisLayout.rowHeights = new int[] { 10, 7, 15 }; - thisLayout.columnWeights = new double[] { 0.0, 0.0, 1.0 }; - thisLayout.columnWidths = new int[] { 7, 20, 7 }; - this.setLayout( thisLayout ); - this.setPreferredSize( panelSize ); - this.setMaximumSize( panelMaxSize ); - this.setBorder( new LineBorder( annotationColor, 1, true ) ); - - /* - * Feature selection box. - */ - - final ComboBoxModel< String > cmbboxFeatureNameModel = new DefaultComboBoxModel<>( keyNames.keySet().toArray( new String[] {} ) ); - cmbboxFeatureKeys = new JComboBox<>( cmbboxFeatureNameModel ); - cmbboxFeatureKeys.setRenderer( new DefaultListCellRenderer() - { - - private static final long serialVersionUID = 1L; - - @Override - public Component getListCellRendererComponent( final JList< ? > list, final Object value, final int index, final boolean isSelected, final boolean cellHasFocus ) - { - final JLabel lbl = ( JLabel ) super.getListCellRendererComponent( list, value, index, isSelected, cellHasFocus ); - lbl.setText( keyNames.get( value ) ); - return lbl; - } - } ); - cmbboxFeatureKeys.setFont( FONT ); - this.add( cmbboxFeatureKeys, new GridBagConstraints( 0, 0, 3, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets( 2, 5, 2, 5 ), 0, 0 ) ); - - /* - * Create histogram plot. - */ - - final LogHistogramDataset dataset = new LogHistogramDataset(); - final JFreeChart chart = ChartFactory.createHistogram( null, null, null, dataset, PlotOrientation.VERTICAL, false, false, false ); - - plot = chart.getXYPlot(); - final XYBarRenderer renderer = ( XYBarRenderer ) plot.getRenderer(); - renderer.setShadowVisible( false ); - renderer.setMargin( 0 ); - renderer.setBarPainter( new StandardXYBarPainter() ); - renderer.setDrawBarOutline( true ); - renderer.setSeriesOutlinePaint( 0, new Color( 0.2f, 0.2f, 0.2f ) ); - renderer.setSeriesPaint( 0, new Color( 0.3f, 0.3f, 0.3f, 0.5f ) ); - - plot.setBackgroundPaint( new Color( 1, 1, 1, 0 ) ); - plot.setOutlineVisible( false ); - plot.setDomainCrosshairVisible( false ); - plot.setDomainGridlinesVisible( false ); - plot.setRangeCrosshairVisible( false ); - plot.setRangeGridlinesVisible( false ); - - plot.getRangeAxis().setVisible( false ); - plot.getDomainAxis().setVisible( false ); - - chart.setBorderVisible( false ); - chart.setBackgroundPaint( new Color( 0.6f, 0.6f, 0.7f ) ); - - intervalMarker = new IntervalMarker( 0, 0, new Color( 0.3f, 0.5f, 0.8f ), new BasicStroke(), new Color( 0, 0, 0.5f ), new BasicStroke( 1.5f ), 0.5f ); - plot.addDomainMarker( intervalMarker ); - - final ChartPanel chartPanel = new ChartPanel( chart ); - final MouseListener[] mls = chartPanel.getMouseListeners(); - for ( final MouseListener ml : mls ) - chartPanel.removeMouseListener( ml ); - - chartPanel.addMouseListener( new MouseAdapter() - { - @Override - public void mouseClicked( final MouseEvent e ) - { - chartPanel.requestFocusInWindow(); - threshold = getXFromChartEvent( e, chartPanel ); - redrawThresholdMarker(); - } - } ); - chartPanel.addMouseMotionListener( new MouseAdapter() - { - @Override - public void mouseDragged( final MouseEvent e ) - { - threshold = getXFromChartEvent( e, chartPanel ); - redrawThresholdMarker(); - } - } ); - chartPanel.setFocusable( true ); - chartPanel.addFocusListener( new FocusListener() - { - - @Override - public void focusLost( final FocusEvent e ) - { - annotation.setColor( annotationColor.darker() ); - } - - @Override - public void focusGained( final FocusEvent e ) - { - annotation.setColor( Color.RED.darker() ); - } - } ); - chartPanel.addKeyListener( new MyKeyListener() ); - - annotation = new XYTextSimpleAnnotation( chartPanel ); - annotation.setFont( SMALL_FONT.deriveFont( Font.BOLD ) ); - annotation.setColor( annotationColor.darker() ); - plot.addAnnotation( annotation ); - - chartPanel.setPreferredSize( new Dimension( 0, 0 ) ); - this.add( chartPanel, new GridBagConstraints( 0, 1, 3, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets( 0, 0, 0, 0 ), 0, 0 ) ); - chartPanel.setOpaque( false ); - - /* - * Threshold. - */ - - final JButton btnAutoThreshold = new JButton(); - this.add( btnAutoThreshold, new GridBagConstraints( 2, 2, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets( 0, 0, 0, 10 ), 0, 0 ) ); - btnAutoThreshold.setText( "Auto" ); - btnAutoThreshold.setFont( SMALL_FONT ); - btnAutoThreshold.addActionListener( e -> autoThreshold() ); - - rdbtnAbove = new JRadioButton(); - this.add( rdbtnAbove, new GridBagConstraints( 0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets( 0, 10, 0, 0 ), 0, 0 ) ); - rdbtnAbove.setText( "Above" ); - rdbtnAbove.setFont( SMALL_FONT ); - rdbtnAbove.addActionListener( e -> redrawThresholdMarker() ); - - rdbtnBelow = new JRadioButton(); - this.add( rdbtnBelow, new GridBagConstraints( 1, 2, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets( 0, 5, 0, 0 ), 0, 0 ) ); - rdbtnBelow.setText( "Below" ); - rdbtnBelow.addActionListener( e -> redrawThresholdMarker() ); - rdbtnBelow.setFont( SMALL_FONT ); - - final ButtonGroup buttonGroup = new ButtonGroup(); - buttonGroup.add( rdbtnAbove ); - buttonGroup.add( rdbtnBelow ); - - /* - * Listeners & co. - */ - - cmbboxFeatureKeys.addActionListener( e -> comboBoxSelectionChanged() ); - - /* - * Current values. - */ - - cmbboxFeatureKeys.setSelectedItem( filter.feature ); - rdbtnAbove.setSelected( filter.isAbove ); - rdbtnBelow.setSelected( !filter.isAbove ); - if ( Double.isNaN( filter.value ) ) - autoThreshold(); - else - this.threshold = filter.value; - redrawThresholdMarker(); - } - - /* - * PUBLIC METHODS - */ - - public FeatureFilter getFilter() - { - return new FeatureFilter( ( String ) cmbboxFeatureKeys.getSelectedItem(), threshold, rdbtnAbove.isSelected() ); - } - - /** - * Add an {@link ChangeListener} to this panel. The {@link ChangeListener} - * will be notified when a change happens to the threshold displayed by this - * panel, whether due to the slider being move, the auto-threshold button - * being pressed, or the combo-box selection being changed. - */ - public void addChangeListener( final ChangeListener listener ) - { - listeners.add( listener ); - } - - /** - * Remove an ChangeListener. - * - * @return true if the listener was in listener collection of this instance. - */ - public boolean removeChangeListener( final ChangeListener listener ) - { - return listeners.remove( listener ); - } - - public Collection< ChangeListener > getChangeListeners() - { - return listeners; - } - - /** - * Refreshes the histogram content. Call this method when the values in the - * values map changed to update histogram display. - */ - public void refresh() - { - final double old = threshold; - final String key = ( String ) cmbboxFeatureKeys.getSelectedItem(); - final double[] values = valueCollector.apply( key ); - - final LogHistogramDataset dataset; - if ( null == values || 0 == values.length ) - { - dataset = new LogHistogramDataset(); - annotation.setLocation( 0.5f, 0.5f ); - annotation.setText( "No data" ); - } - else - { - final int nBins = TMUtils.getNBins( values, 8, 100 ); - dataset = new LogHistogramDataset(); - if ( nBins > 1 ) - dataset.addSeries( DATA_SERIES_NAME, values, nBins ); - } - plot.setDataset( dataset ); - threshold = old; - repaint(); - redrawThresholdMarker(); - } - - /* - * PRIVATE METHODS - */ - - private void fireThresholdChanged() - { - for ( final ChangeListener al : listeners ) - al.stateChanged( CHANGE_EVENT ); - } - - private void comboBoxSelectionChanged() - { - final String key = ( String ) cmbboxFeatureKeys.getSelectedItem(); - final double[] values = valueCollector.apply( key ); - - final LogHistogramDataset dataset; - if ( null == values || 0 == values.length ) - { - dataset = new LogHistogramDataset(); - threshold = Double.NaN; - annotation.setLocation( 0.5f, 0.5f ); - annotation.setText( "No data" ); - fireThresholdChanged(); - } - else - { - final int nBins = TMUtils.getNBins( values, 8, 100 ); - dataset = new LogHistogramDataset(); - - if ( nBins > 1 ) - dataset.addSeries( DATA_SERIES_NAME, values, nBins ); - } - plot.setDataset( dataset ); - resetAxes(); - autoThreshold(); // Will fire the fireThresholdChanged(); - } - - private void autoThreshold() - { - final String key = ( String ) cmbboxFeatureKeys.getSelectedItem(); - final double[] values = valueCollector.apply( key ); - if ( null != values && values.length > 0 ) - { - threshold = TMUtils.otsuThreshold( values ); - redrawThresholdMarker(); - } - } - - private double getXFromChartEvent( final MouseEvent mouseEvent, final ChartPanel chartPanel ) - { - final Rectangle2D plotArea = chartPanel.getScreenDataArea(); - return plot.getDomainAxis().java2DToValue( mouseEvent.getX(), plotArea, plot.getDomainAxisEdge() ); - } - - private void redrawThresholdMarker() - { - final String key = ( String ) cmbboxFeatureKeys.getSelectedItem(); - final double[] values = valueCollector.apply( key ); - if ( null == values ) - return; - - if ( rdbtnAbove.isSelected() ) - { - intervalMarker.setStartValue( threshold ); - intervalMarker.setEndValue( plot.getDomainAxis().getUpperBound() ); - } - else - { - intervalMarker.setStartValue( plot.getDomainAxis().getLowerBound() ); - intervalMarker.setEndValue( threshold ); - } - - final float x; - if ( threshold > 0.85 * plot.getDomainAxis().getUpperBound() ) - x = ( float ) ( threshold - 0.15 * plot.getDomainAxis().getRange().getLength() ); - else - x = ( float ) ( threshold + 0.05 * plot.getDomainAxis().getRange().getLength() ); - - final float y = ( float ) ( 0.85 * plot.getRangeAxis().getUpperBound() ); - annotation.setText( String.format( "%.2f", threshold ) ); - annotation.setLocation( x, y ); - fireThresholdChanged(); - } - - private void resetAxes() - { - plot.getRangeAxis().setLowerMargin( 0 ); - plot.getRangeAxis().setUpperMargin( 0 ); - plot.getDomainAxis().setLowerMargin( 0 ); - plot.getDomainAxis().setUpperMargin( 0 ); - } - - /** - * A class that listen to the user typing a number, building a string - * representation as he types, then converting the string to a double after - * a wait time. The number typed is used to set the threshold in the chart - * panel. - * - * @author Jean-Yves Tinevez - */ - private final class MyKeyListener implements KeyListener - { - - private static final long WAIT_DELAY = 1; // s - - private static final double INCREASE_FACTOR = 0.1; - - private static final double SLOW_INCREASE_FACTOR = 0.005; - - private String strNumber = ""; - - private ScheduledExecutorService ex; - - private ScheduledFuture< ? > future; - - private boolean dotAdded = false; - - private final Runnable command = new Runnable() - { - @Override - public void run() - { - // Convert to double and pass it to threshold value - try - { - final double typedThreshold = NumberParser.parseDouble( strNumber ); - threshold = typedThreshold; - redrawThresholdMarker(); - } - catch ( final NumberFormatException nfe ) - {} - // Reset - ex = null; - strNumber = ""; - dotAdded = false; - } - }; - - @Override - public void keyPressed( final KeyEvent e ) - { - // Is it arrow keys? - if ( e.getKeyCode() == KeyEvent.VK_LEFT || e.getKeyCode() == KeyEvent.VK_KP_LEFT ) - { - threshold -= ( e.isControlDown() ? SLOW_INCREASE_FACTOR : INCREASE_FACTOR ) * plot.getDomainAxis().getRange().getLength(); - redrawThresholdMarker(); - return; - } - else if ( e.getKeyCode() == KeyEvent.VK_RIGHT || e.getKeyCode() == KeyEvent.VK_KP_RIGHT ) - { - threshold += ( e.isControlDown() ? SLOW_INCREASE_FACTOR : INCREASE_FACTOR ) * plot.getDomainAxis().getRange().getLength(); - redrawThresholdMarker(); - return; - } - else if ( e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_KP_UP ) - { - threshold = plot.getDomainAxis().getRange().getUpperBound(); - redrawThresholdMarker(); - return; - } - else if ( e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_KP_DOWN ) - { - threshold = plot.getDomainAxis().getRange().getLowerBound(); - redrawThresholdMarker(); - return; - } - } - - @Override - public void keyReleased( final KeyEvent e ) - {} - - @Override - public void keyTyped( final KeyEvent e ) - { - - if ( e.getKeyChar() < '0' || e.getKeyChar() > '9' ) - { - // Ok then it's number - - if ( !dotAdded && e.getKeyChar() == '.' ) - { - // User added a decimal dot for the first and only time - dotAdded = true; - } - else - { - return; - } - } - - if ( ex == null ) - { - // Create new waiting line - ex = Threads.newSingleThreadScheduledExecutor(); - future = ex.schedule( command, WAIT_DELAY, TimeUnit.SECONDS ); - } - else - { - // Reset waiting line - future.cancel( false ); - future = ex.schedule( command, WAIT_DELAY, TimeUnit.SECONDS ); - } - strNumber += e.getKeyChar(); - } - } +public class FilterPanel extends javax.swing.JPanel { + + static final Font FONT = new Font("Arial", Font.PLAIN, 11); + + static final Font SMALL_FONT = FONT.deriveFont(10f); + + private static final Color annotationColor; + private static final long serialVersionUID = 1L; + private static final String DATA_SERIES_NAME = "Data"; + + static { + final Color bgColor = UIManager.getColor("Panel.background"); + final boolean bgIsDark = GuiUtils.colorDistance(Color.WHITE, bgColor) > 0.5; + annotationColor = bgIsDark + ? new java.awt.Color(252, 117, 0).brighter() + : new java.awt.Color(252, 117, 0); + } + + final JRadioButton rdbtnAbove; + final JRadioButton rdbtnBelow; + final JComboBox cmbboxFeatureKeys; + private final ChangeEvent CHANGE_EVENT = new ChangeEvent(this); + private final XYPlot plot; + private final IntervalMarker intervalMarker; + private final Function valueCollector; + private final XYTextSimpleAnnotation annotation; + private final ArrayList listeners = new ArrayList<>(); + private double threshold; + + + /* + * CONSTRUCTOR + */ + + public FilterPanel( + final Map keyNames, + final Function valueCollector, + final FeatureFilter filter) { + this.valueCollector = valueCollector; + + final Dimension panelSize = new java.awt.Dimension(250, 140); + final Dimension panelMaxSize = new java.awt.Dimension(1000, 140); + final GridBagLayout thisLayout = new GridBagLayout(); + thisLayout.rowWeights = new double[]{0.0, 1.0, 0.0}; + thisLayout.rowHeights = new int[]{10, 7, 15}; + thisLayout.columnWeights = new double[]{0.0, 0.0, 1.0}; + thisLayout.columnWidths = new int[]{7, 20, 7}; + this.setLayout(thisLayout); + this.setPreferredSize(panelSize); + this.setMaximumSize(panelMaxSize); + this.setBorder(new LineBorder(annotationColor, 1, true)); + + /* + * Feature selection box. + */ + + final ComboBoxModel cmbboxFeatureNameModel = new DefaultComboBoxModel<>(keyNames.keySet().toArray(new String[]{})); + cmbboxFeatureKeys = new JComboBox<>(cmbboxFeatureNameModel); + cmbboxFeatureKeys.setRenderer(new DefaultListCellRenderer() { + + private static final long serialVersionUID = 1L; + + @Override + public Component getListCellRendererComponent(final JList list, final Object value, final int index, final boolean isSelected, final boolean cellHasFocus) { + final JLabel lbl = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + lbl.setText(keyNames.get(value)); + return lbl; + } + }); + cmbboxFeatureKeys.setFont(FONT); + this.add(cmbboxFeatureKeys, new GridBagConstraints(0, 0, 3, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(2, 5, 2, 5), 0, 0)); + + /* + * Create histogram plot. + */ + + final LogHistogramDataset dataset = new LogHistogramDataset(); + final JFreeChart chart = ChartFactory.createHistogram(null, null, null, dataset, PlotOrientation.VERTICAL, false, false, false); + + plot = chart.getXYPlot(); + final XYBarRenderer renderer = (XYBarRenderer) plot.getRenderer(); + renderer.setShadowVisible(false); + renderer.setMargin(0); + renderer.setBarPainter(new StandardXYBarPainter()); + renderer.setDrawBarOutline(true); + renderer.setSeriesOutlinePaint(0, new Color(0.2f, 0.2f, 0.2f)); + renderer.setSeriesPaint(0, new Color(0.3f, 0.3f, 0.3f, 0.5f)); + + plot.setBackgroundPaint(new Color(1, 1, 1, 0)); + plot.setOutlineVisible(false); + plot.setDomainCrosshairVisible(false); + plot.setDomainGridlinesVisible(false); + plot.setRangeCrosshairVisible(false); + plot.setRangeGridlinesVisible(false); + + plot.getRangeAxis().setVisible(false); + plot.getDomainAxis().setVisible(false); + + chart.setBorderVisible(false); + chart.setBackgroundPaint(new Color(0.6f, 0.6f, 0.7f)); + + intervalMarker = new IntervalMarker(0, 0, new Color(0.3f, 0.5f, 0.8f), new BasicStroke(), new Color(0, 0, 0.5f), new BasicStroke(1.5f), 0.5f); + plot.addDomainMarker(intervalMarker); + + final ChartPanel chartPanel = new ChartPanel(chart); + final MouseListener[] mls = chartPanel.getMouseListeners(); + for (final MouseListener ml : mls) + chartPanel.removeMouseListener(ml); + + chartPanel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(final MouseEvent e) { + chartPanel.requestFocusInWindow(); + threshold = getXFromChartEvent(e, chartPanel); + redrawThresholdMarker(); + } + }); + chartPanel.addMouseMotionListener(new MouseAdapter() { + @Override + public void mouseDragged(final MouseEvent e) { + threshold = getXFromChartEvent(e, chartPanel); + redrawThresholdMarker(); + } + }); + chartPanel.setFocusable(true); + chartPanel.addFocusListener(new FocusListener() { + + @Override + public void focusLost(final FocusEvent e) { + annotation.setColor(annotationColor.darker()); + } + + @Override + public void focusGained(final FocusEvent e) { + annotation.setColor(Color.RED.darker()); + } + }); + chartPanel.addKeyListener(new MyKeyListener()); + + annotation = new XYTextSimpleAnnotation(chartPanel); + annotation.setFont(SMALL_FONT.deriveFont(Font.BOLD)); + annotation.setColor(annotationColor.darker()); + plot.addAnnotation(annotation); + + chartPanel.setPreferredSize(new Dimension(0, 0)); + this.add(chartPanel, new GridBagConstraints(0, 1, 3, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); + chartPanel.setOpaque(false); + + /* + * Threshold. + */ + + final JButton btnAutoThreshold = new JButton(); + this.add(btnAutoThreshold, new GridBagConstraints(2, 2, 1, 1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(0, 0, 0, 10), 0, 0)); + btnAutoThreshold.setText("Auto"); + btnAutoThreshold.setFont(SMALL_FONT); + btnAutoThreshold.addActionListener(e -> autoThreshold()); + + rdbtnAbove = new JRadioButton(); + this.add(rdbtnAbove, new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0, 10, 0, 0), 0, 0)); + rdbtnAbove.setText("Above"); + rdbtnAbove.setFont(SMALL_FONT); + rdbtnAbove.addActionListener(e -> redrawThresholdMarker()); + + rdbtnBelow = new JRadioButton(); + this.add(rdbtnBelow, new GridBagConstraints(1, 2, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 5, 0, 0), 0, 0)); + rdbtnBelow.setText("Below"); + rdbtnBelow.addActionListener(e -> redrawThresholdMarker()); + rdbtnBelow.setFont(SMALL_FONT); + + final ButtonGroup buttonGroup = new ButtonGroup(); + buttonGroup.add(rdbtnAbove); + buttonGroup.add(rdbtnBelow); + + /* + * Listeners & co. + */ + + cmbboxFeatureKeys.addActionListener(e -> comboBoxSelectionChanged()); + + /* + * Current values. + */ + + cmbboxFeatureKeys.setSelectedItem(filter.feature); + rdbtnAbove.setSelected(filter.isAbove); + rdbtnBelow.setSelected(!filter.isAbove); + if (Double.isNaN(filter.value)) + autoThreshold(); + else + this.threshold = filter.value; + redrawThresholdMarker(); + } + + /* + * PUBLIC METHODS + */ + + public FeatureFilter getFilter() { + return new FeatureFilter((String) cmbboxFeatureKeys.getSelectedItem(), threshold, rdbtnAbove.isSelected()); + } + + /** + * Add an {@link ChangeListener} to this panel. The {@link ChangeListener} + * will be notified when a change happens to the threshold displayed by this + * panel, whether due to the slider being move, the auto-threshold button + * being pressed, or the combo-box selection being changed. + */ + public void addChangeListener(final ChangeListener listener) { + listeners.add(listener); + } + + /** + * Remove an ChangeListener. + * + * @return true if the listener was in listener collection of this instance. + */ + public boolean removeChangeListener(final ChangeListener listener) { + return listeners.remove(listener); + } + + public Collection getChangeListeners() { + return listeners; + } + + /** + * Refreshes the histogram content. Call this method when the values in the + * values map changed to update histogram display. + */ + public void refresh() { + final double old = threshold; + final String key = (String) cmbboxFeatureKeys.getSelectedItem(); + final double[] values = valueCollector.apply(key); + + final LogHistogramDataset dataset; + if (null == values || 0 == values.length) { + dataset = new LogHistogramDataset(); + annotation.setLocation(0.5f, 0.5f); + annotation.setText("No data"); + } else { + final int nBins = HistogramUtils.getNBins(values, 8, 100); + dataset = new LogHistogramDataset(); + if (nBins > 1) + dataset.addSeries(DATA_SERIES_NAME, values, nBins); + } + plot.setDataset(dataset); + threshold = old; + repaint(); + redrawThresholdMarker(); + } + + /* + * PRIVATE METHODS + */ + + private void fireThresholdChanged() { + for (final ChangeListener al : listeners) + al.stateChanged(CHANGE_EVENT); + } + + private void comboBoxSelectionChanged() { + final String key = (String) cmbboxFeatureKeys.getSelectedItem(); + final double[] values = valueCollector.apply(key); + + final LogHistogramDataset dataset; + if (null == values || 0 == values.length) { + dataset = new LogHistogramDataset(); + threshold = Double.NaN; + annotation.setLocation(0.5f, 0.5f); + annotation.setText("No data"); + fireThresholdChanged(); + } else { + final int nBins = HistogramUtils.getNBins(values, 8, 100); + dataset = new LogHistogramDataset(); + + if (nBins > 1) + dataset.addSeries(DATA_SERIES_NAME, values, nBins); + } + plot.setDataset(dataset); + resetAxes(); + autoThreshold(); // Will fire the fireThresholdChanged(); + } + + private void autoThreshold() { + final String key = (String) cmbboxFeatureKeys.getSelectedItem(); + final double[] values = valueCollector.apply(key); + if (null != values && values.length > 0) { + threshold = HistogramUtils.otsuThreshold(values); + redrawThresholdMarker(); + } + } + + private double getXFromChartEvent(final MouseEvent mouseEvent, final ChartPanel chartPanel) { + final Rectangle2D plotArea = chartPanel.getScreenDataArea(); + return plot.getDomainAxis().java2DToValue(mouseEvent.getX(), plotArea, plot.getDomainAxisEdge()); + } + + private void redrawThresholdMarker() { + final String key = (String) cmbboxFeatureKeys.getSelectedItem(); + final double[] values = valueCollector.apply(key); + if (null == values) + return; + + if (rdbtnAbove.isSelected()) { + intervalMarker.setStartValue(threshold); + intervalMarker.setEndValue(plot.getDomainAxis().getUpperBound()); + } else { + intervalMarker.setStartValue(plot.getDomainAxis().getLowerBound()); + intervalMarker.setEndValue(threshold); + } + + final float x; + if (threshold > 0.85 * plot.getDomainAxis().getUpperBound()) + x = (float) (threshold - 0.15 * plot.getDomainAxis().getRange().getLength()); + else + x = (float) (threshold + 0.05 * plot.getDomainAxis().getRange().getLength()); + + final float y = (float) (0.85 * plot.getRangeAxis().getUpperBound()); + annotation.setText(String.format("%.2f", threshold)); + annotation.setLocation(x, y); + fireThresholdChanged(); + } + + private void resetAxes() { + plot.getRangeAxis().setLowerMargin(0); + plot.getRangeAxis().setUpperMargin(0); + plot.getDomainAxis().setLowerMargin(0); + plot.getDomainAxis().setUpperMargin(0); + } + + /** + * A class that listen to the user typing a number, building a string + * representation as he types, then converting the string to a double after + * a wait time. The number typed is used to set the threshold in the chart + * panel. + * + * @author Jean-Yves Tinevez + */ + private final class MyKeyListener implements KeyListener { + + private static final long WAIT_DELAY = 1; // s + + private static final double INCREASE_FACTOR = 0.1; + + private static final double SLOW_INCREASE_FACTOR = 0.005; + + private String strNumber = ""; + + private ScheduledExecutorService ex; + + private ScheduledFuture future; + + private boolean dotAdded = false; + + private final Runnable command = new Runnable() { + @Override + public void run() { + // Convert to double and pass it to threshold value + try { + final double typedThreshold = NumberParser.parseDouble(strNumber); + threshold = typedThreshold; + redrawThresholdMarker(); + } catch (final NumberFormatException nfe) { + } + // Reset + ex = null; + strNumber = ""; + dotAdded = false; + } + }; + + @Override + public void keyPressed(final KeyEvent e) { + // Is it arrow keys? + if (e.getKeyCode() == KeyEvent.VK_LEFT || e.getKeyCode() == KeyEvent.VK_KP_LEFT) { + threshold -= (e.isControlDown() ? SLOW_INCREASE_FACTOR : INCREASE_FACTOR) * plot.getDomainAxis().getRange().getLength(); + redrawThresholdMarker(); + } else if (e.getKeyCode() == KeyEvent.VK_RIGHT || e.getKeyCode() == KeyEvent.VK_KP_RIGHT) { + threshold += (e.isControlDown() ? SLOW_INCREASE_FACTOR : INCREASE_FACTOR) * plot.getDomainAxis().getRange().getLength(); + redrawThresholdMarker(); + } else if (e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_KP_UP) { + threshold = plot.getDomainAxis().getRange().getUpperBound(); + redrawThresholdMarker(); + } else if (e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_KP_DOWN) { + threshold = plot.getDomainAxis().getRange().getLowerBound(); + redrawThresholdMarker(); + } + } + + @Override + public void keyReleased(final KeyEvent e) { + } + + @Override + public void keyTyped(final KeyEvent e) { + + if (e.getKeyChar() < '0' || e.getKeyChar() > '9') { + // Ok then it's number + + if (!dotAdded && e.getKeyChar() == '.') { + // User added a decimal dot for the first and only time + dotAdded = true; + } else { + return; + } + } + + if (ex == null) { + // Create new waiting line + ex = Threads.newSingleThreadScheduledExecutor(); + future = ex.schedule(command, WAIT_DELAY, TimeUnit.SECONDS); + } else { + // Reset waiting line + future.cancel(false); + future = ex.schedule(command, WAIT_DELAY, TimeUnit.SECONDS); + } + strNumber += e.getKeyChar(); + } + } } diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTracker.java b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTracker.java index 98e268a60..ccb4fc5e3 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTracker.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTracker.java @@ -8,12 +8,12 @@ * 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 * . @@ -21,252 +21,224 @@ */ package fiji.plugin.trackmate.tracking.kdtree; -import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_LINKING_MAX_DISTANCE; -import static fiji.plugin.trackmate.util.TMUtils.checkMapKeys; -import static fiji.plugin.trackmate.util.TMUtils.checkParameter; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.TreeSet; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicInteger; - -import org.jgrapht.graph.DefaultWeightedEdge; -import org.jgrapht.graph.SimpleWeightedGraph; -import org.scijava.Cancelable; - import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.tracking.SpotTracker; +import fiji.plugin.trackmate.util.HistogramUtils; import fiji.plugin.trackmate.util.Threads; -import fiji.plugin.trackmate.util.TMUtils; import net.imglib2.KDTree; import net.imglib2.RealPoint; import net.imglib2.algorithm.MultiThreadedBenchmarkAlgorithm; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.SimpleWeightedGraph; +import org.scijava.Cancelable; + +import java.util.*; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; + +import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_LINKING_MAX_DISTANCE; +import static fiji.plugin.trackmate.util.TMUtils.checkMapKeys; +import static fiji.plugin.trackmate.util.TMUtils.checkParameter; + +public class NearestNeighborTracker extends MultiThreadedBenchmarkAlgorithm implements SpotTracker, Cancelable { -public class NearestNeighborTracker extends MultiThreadedBenchmarkAlgorithm implements SpotTracker, Cancelable -{ - - /* - * FIELDS - */ - - protected final SpotCollection spots; - - protected final Map< String, Object > settings; - - protected Logger logger = Logger.VOID_LOGGER; - - protected SimpleWeightedGraph< Spot, DefaultWeightedEdge > graph; - - private boolean isCanceled; - - private String cancelReason; - - /* - * CONSTRUCTOR - */ - - public NearestNeighborTracker( final SpotCollection spots, final Map< String, Object > settings ) - { - this.spots = spots; - this.settings = settings; - } - - /* - * PUBLIC METHODS - */ - - @Override - public boolean checkInput() - { - final StringBuilder errrorHolder = new StringBuilder(); - final boolean ok = checkInput( settings, errrorHolder ); - if ( !ok ) - errorMessage = errrorHolder.toString(); - - return ok; - } - - @Override - public boolean process() - { - final long start = System.currentTimeMillis(); - - isCanceled = false; - cancelReason = null; - - reset(); - - final double maxLinkingDistance = ( Double ) settings.get( KEY_LINKING_MAX_DISTANCE ); - final double maxDistSquare = maxLinkingDistance * maxLinkingDistance; - final TreeSet< Integer > frames = new TreeSet<>( spots.keySet() ); - - // Prepare executors. - final AtomicInteger progress = new AtomicInteger( 0 ); - final ExecutorService executors = Threads.newFixedThreadPool( numThreads ); - final List< Future< Void > > futures = new ArrayList<>( frames.size() ); - for ( int i = frames.first(); i < frames.last(); i++ ) - { - final int frame = i; - final Future< Void > future = executors.submit( new Callable< Void >() - { - - @Override - public Void call() throws Exception - { - if ( isCanceled() ) - return null; - - // Build frame pair - final int sourceFrame = frame; - final int targetFrame = frames.higher( frame ); - - final int nTargetSpots = spots.getNSpots( targetFrame, true ); - if ( nTargetSpots < 1 ) - { - logger.setProgress( progress.incrementAndGet() / ( double ) frames.size() ); - return null; - } - - final List< RealPoint > targetCoords = new ArrayList<>( nTargetSpots ); - final List< FlagNode< Spot > > targetNodes = new ArrayList<>( nTargetSpots ); - final Iterator< Spot > targetIt = spots.iterator( targetFrame, true ); - while ( targetIt.hasNext() ) - { - final double[] coords = new double[ 3 ]; - final Spot spot = targetIt.next(); - TMUtils.localize( spot, coords ); - targetCoords.add( new RealPoint( coords ) ); - targetNodes.add( new FlagNode<>( spot ) ); - } - - final KDTree< FlagNode< Spot > > tree = new KDTree<>( targetNodes, targetCoords ); - final NearestNeighborFlagSearchOnKDTree< Spot > search = new NearestNeighborFlagSearchOnKDTree<>( tree ); - - /* - * For each spot in the source frame, find its nearest - * neighbor in the target frame. - */ - final Iterator< Spot > sourceIt = spots.iterator( sourceFrame, true ); - while ( sourceIt.hasNext() ) - { - final Spot source = sourceIt.next(); - final double[] coords = new double[ 3 ]; - TMUtils.localize( source, coords ); - final RealPoint sourceCoords = new RealPoint( coords ); - search.search( sourceCoords ); - - final double squareDist = search.getSquareDistance(); - final FlagNode< Spot > targetNode = search.getSampler().get(); - - /* - * The closest we could find is too far. We skip this - * source spot and do not create a link - */ - if ( squareDist > maxDistSquare ) - continue; - - /* - * Everything is ok. This node is free and below max - * dist. We create a link and mark this node as - * assigned. - */ - - targetNode.setVisited( true ); - synchronized ( graph ) - { - final DefaultWeightedEdge edge = graph.addEdge( source, targetNode.getValue() ); - graph.setEdgeWeight( edge, squareDist ); - } - } - logger.setProgress( progress.incrementAndGet() / ( double ) frames.size() ); - return null; - } - } ); - futures.add( future ); - } - - logger.setStatus( "Tracking..." ); - logger.setProgress( 0 ); - - try - { - for ( final Future< Void > future : futures ) - future.get(); - - executors.shutdown(); - } - catch ( InterruptedException | ExecutionException e ) - { - e.printStackTrace(); - errorMessage = e.getMessage(); - return false; - } - finally - { - logger.setProgress( 1 ); - logger.setStatus( "" ); - - final long end = System.currentTimeMillis(); - processingTime = end - start; - } - return true; - } - - @Override - public SimpleWeightedGraph< Spot, DefaultWeightedEdge > getResult() - { - return graph; - } - - public void reset() - { - graph = new SimpleWeightedGraph<>( DefaultWeightedEdge.class ); - final Iterator< Spot > it = spots.iterator( true ); - while ( it.hasNext() ) - graph.addVertex( it.next() ); - } - - public static boolean checkInput( final Map< String, Object > settings, final StringBuilder errrorHolder ) - { - boolean ok = checkParameter( settings, KEY_LINKING_MAX_DISTANCE, Double.class, errrorHolder ); - final List< String > mandatoryKeys = new ArrayList<>(); - mandatoryKeys.add( KEY_LINKING_MAX_DISTANCE ); - ok = ok & checkMapKeys( settings, mandatoryKeys, null, errrorHolder ); - return ok; - } - - @Override - public void setLogger( final Logger logger ) - { - this.logger = logger; - } - - // --- org.scijava.Cancelable methods --- - - @Override - public boolean isCanceled() - { - return isCanceled; - } - - @Override - public void cancel( final String reason ) - { - isCanceled = true; - cancelReason = reason; - } - - @Override - public String getCancelReason() - { - return cancelReason; - } + /* + * FIELDS + */ + + protected final SpotCollection spots; + + protected final Map settings; + + protected Logger logger = Logger.VOID_LOGGER; + + protected SimpleWeightedGraph graph; + + private boolean isCanceled; + + private String cancelReason; + + /* + * CONSTRUCTOR + */ + + public NearestNeighborTracker(final SpotCollection spots, final Map settings) { + this.spots = spots; + this.settings = settings; + } + + /* + * PUBLIC METHODS + */ + + public static boolean checkInput(final Map settings, final StringBuilder errrorHolder) { + boolean ok = checkParameter(settings, KEY_LINKING_MAX_DISTANCE, Double.class, errrorHolder); + final List mandatoryKeys = new ArrayList<>(); + mandatoryKeys.add(KEY_LINKING_MAX_DISTANCE); + ok = ok & checkMapKeys(settings, mandatoryKeys, null, errrorHolder); + return ok; + } + + @Override + public boolean checkInput() { + final StringBuilder errrorHolder = new StringBuilder(); + final boolean ok = checkInput(settings, errrorHolder); + if (!ok) + errorMessage = errrorHolder.toString(); + + return ok; + } + + @Override + public boolean process() { + final long start = System.currentTimeMillis(); + + isCanceled = false; + cancelReason = null; + + reset(); + + final double maxLinkingDistance = (Double) settings.get(KEY_LINKING_MAX_DISTANCE); + final double maxDistSquare = maxLinkingDistance * maxLinkingDistance; + final TreeSet frames = new TreeSet<>(spots.keySet()); + + // Prepare executors. + final AtomicInteger progress = new AtomicInteger(0); + final ExecutorService executors = Threads.newFixedThreadPool(numThreads); + final List> futures = new ArrayList<>(frames.size()); + for (int i = frames.first(); i < frames.last(); i++) { + final int frame = i; + final Future future = executors.submit(new Callable() { + + @Override + public Void call() throws Exception { + if (isCanceled()) + return null; + + // Build frame pair + final int sourceFrame = frame; + final int targetFrame = frames.higher(frame); + + final int nTargetSpots = spots.getNSpots(targetFrame, true); + if (nTargetSpots < 1) { + logger.setProgress(progress.incrementAndGet() / (double) frames.size()); + return null; + } + + final List targetCoords = new ArrayList<>(nTargetSpots); + final List> targetNodes = new ArrayList<>(nTargetSpots); + final Iterator targetIt = spots.iterator(targetFrame, true); + while (targetIt.hasNext()) { + final double[] coords = new double[3]; + final Spot spot = targetIt.next(); + HistogramUtils.localize(spot, coords); + targetCoords.add(new RealPoint(coords)); + targetNodes.add(new FlagNode<>(spot)); + } + + final KDTree> tree = new KDTree<>(targetNodes, targetCoords); + final NearestNeighborFlagSearchOnKDTree search = new NearestNeighborFlagSearchOnKDTree<>(tree); + + /* + * For each spot in the source frame, find its nearest + * neighbor in the target frame. + */ + final Iterator sourceIt = spots.iterator(sourceFrame, true); + while (sourceIt.hasNext()) { + final Spot source = sourceIt.next(); + final double[] coords = new double[3]; + HistogramUtils.localize(source, coords); + final RealPoint sourceCoords = new RealPoint(coords); + search.search(sourceCoords); + + final double squareDist = search.getSquareDistance(); + final FlagNode targetNode = search.getSampler().get(); + + /* + * The closest we could find is too far. We skip this + * source spot and do not create a link + */ + if (squareDist > maxDistSquare) + continue; + + /* + * Everything is ok. This node is free and below max + * dist. We create a link and mark this node as + * assigned. + */ + + targetNode.setVisited(true); + synchronized (graph) { + final DefaultWeightedEdge edge = graph.addEdge(source, targetNode.getValue()); + graph.setEdgeWeight(edge, squareDist); + } + } + logger.setProgress(progress.incrementAndGet() / (double) frames.size()); + return null; + } + }); + futures.add(future); + } + + logger.setStatus("Tracking..."); + logger.setProgress(0); + + try { + for (final Future future : futures) + future.get(); + + executors.shutdown(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + errorMessage = e.getMessage(); + return false; + } finally { + logger.setProgress(1); + logger.setStatus(""); + + final long end = System.currentTimeMillis(); + processingTime = end - start; + } + return true; + } + + @Override + public SimpleWeightedGraph getResult() { + return graph; + } + + public void reset() { + graph = new SimpleWeightedGraph<>(DefaultWeightedEdge.class); + final Iterator it = spots.iterator(true); + while (it.hasNext()) + graph.addVertex(it.next()); + } + + @Override + public void setLogger(final Logger logger) { + this.logger = logger; + } + + // --- org.scijava.Cancelable methods --- + + @Override + public boolean isCanceled() { + return isCanceled; + } + + @Override + public void cancel(final String reason) { + isCanceled = true; + cancelReason = reason; + } + + @Override + public String getCancelReason() { + return cancelReason; + } } diff --git a/src/main/java/fiji/plugin/trackmate/util/HistogramUtils.java b/src/main/java/fiji/plugin/trackmate/util/HistogramUtils.java new file mode 100644 index 000000000..7253dfe49 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/util/HistogramUtils.java @@ -0,0 +1,185 @@ +package fiji.plugin.trackmate.util; + +import fiji.plugin.trackmate.Spot; + +import java.util.Arrays; + +public class HistogramUtils { + /** + * Returns an estimate of the pth percentile of the values in + * the values array. Taken from commons-math. + */ + public static final double getPercentile(final double[] values, final double p) { + + final int size = values.length; + if ((p > 1) || (p <= 0)) + throw new IllegalArgumentException("invalid quantile value: " + p); + // always return single value for n = 1 + if (size == 0) + return Double.NaN; + if (size == 1) + return values[0]; + final double n = size; + final double pos = p * (n + 1); + final double fpos = Math.floor(pos); + final int intPos = (int) fpos; + final double dif = pos - fpos; + final double[] sorted = new double[size]; + System.arraycopy(values, 0, sorted, 0, size); + Arrays.sort(sorted); + + if (pos < 1) + return sorted[0]; + if (pos >= n) + return sorted[size - 1]; + final double lower = sorted[intPos - 1]; + final double upper = sorted[intPos]; + return lower + dif * (upper - lower); + } + + /** + * Returns [range, min, max] of the given double array. + * + * @return A double[] of length 3, where index 0 is the range, index 1 is + * the min, and index 2 is the max. + */ + private static final double[] getRange(final double[] data) { + if (data.length == 0) + return new double[]{1., 0., 1.}; + + final double min = Arrays.stream(data).min().getAsDouble(); + final double max = Arrays.stream(data).max().getAsDouble(); + return new double[]{(max - min), min, max}; + } + + /** + * Store the x, y, z coordinates of the specified spot in the first 3 + * elements of the specified double array. + */ + public static final void localize(final Spot spot, final double[] coords) { + coords[0] = spot.getFeature(Spot.POSITION_X).doubleValue(); + coords[1] = spot.getFeature(Spot.POSITION_Y).doubleValue(); + coords[2] = spot.getFeature(Spot.POSITION_Z).doubleValue(); + } + + /** + * Return the optimal bin number for a histogram of the data given in array, + * using the Freedman and Diaconis rule (bin_space = 2*IQR/n^(1/3)). It is + * ensured that the bin number returned is not smaller and no bigger than + * the bounds given in argument. + */ + public static final int getNBins(final double[] values, final int minBinNumber, final int maxBinNumber) { + final int size = values.length; + final double q1 = getPercentile(values, 0.25); + final double q3 = getPercentile(values, 0.75); + final double iqr = q3 - q1; + final double binWidth = 2 * iqr * Math.pow(size, -0.33); + final double[] range = getRange(values); + int nBin = (int) (range[0] / binWidth + 1); + + if (nBin > maxBinNumber) + nBin = maxBinNumber; + else if (nBin < minBinNumber) + nBin = minBinNumber; + + return nBin; + } + + /** + * Return the optimal bin number for a histogram of the data given in array, + * using the Freedman and Diaconis rule (bin_space = 2*IQR/n^(1/3)). It is + * ensured that the bin number returned is not smaller than 8 and no bigger + * than 256. + */ + private static final int getNBins(final double[] values) { + return getNBins(values, 8, 256); + } + + /** + * Create a histogram from the data given. + */ + private static final int[] histogram(final double[] data, final int nBins) { + final double[] range = getRange(data); + final double binWidth = range[0] / nBins; + final int[] hist = new int[nBins]; + int index; + + if (nBins > 0) { + for (int i = 0; i < data.length; i++) { + index = Math.min((int) Math.floor((data[i] - range[1]) / binWidth), nBins - 1); + hist[index]++; + } + } + return hist; + } + + /** + * Return a threshold for the given data, using an Otsu histogram + * thresholding method. + */ + public static final double otsuThreshold(final double[] data) { + return otsuThreshold(data, getNBins(data)); + } + + /** + * Return a threshold for the given data, using an Otsu histogram + * thresholding method with a given bin number. + */ + private static final double otsuThreshold(final double[] data, final int nBins) { + final int[] hist = histogram(data, nBins); + final int thresholdIndex = otsuThresholdIndex(hist, data.length); + final double[] range = getRange(data); + final double binWidth = range[0] / nBins; + return range[1] + binWidth * thresholdIndex; + } + + /** + * Given a histogram array hist, built with an initial amount + * of nPoints data item, this method return the bin index that + * thresholds the histogram in 2 classes. The threshold is performed using + * the Otsu Threshold Method. + * + * @param hist the histogram array + * @param nPoints the number of data items this histogram was built on + * @return the bin index of the histogram that thresholds it + */ + private static final int otsuThresholdIndex(final int[] hist, final int nPoints) { + final int total = nPoints; + + double sum = 0; + for (int t = 0; t < hist.length; t++) + sum += t * hist[t]; + + double sumB = 0; + int wB = 0; + int wF = 0; + + double varMax = 0; + int threshold = 0; + + for (int t = 0; t < hist.length; t++) { + wB += hist[t]; // Weight Background + if (wB == 0) + continue; + + wF = total - wB; // Weight Foreground + if (wF == 0) + break; + + sumB += (t * hist[t]); + + final double mB = sumB / wB; // Mean Background + final double mF = (sum - sumB) / wF; // Mean Foreground + + // Calculate Between Class Variance + final double varBetween = wB * wF * (mB - mF) * (mB - mF); + + // Check if new maximum found + if (varBetween > varMax) { + varMax = varBetween; + threshold = t; + } + } + return threshold; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/util/QualityHistogramChart.java b/src/main/java/fiji/plugin/trackmate/util/QualityHistogramChart.java index 0a121b55f..dfff7dbc7 100644 --- a/src/main/java/fiji/plugin/trackmate/util/QualityHistogramChart.java +++ b/src/main/java/fiji/plugin/trackmate/util/QualityHistogramChart.java @@ -8,12 +8,12 @@ * 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 * . @@ -21,25 +21,12 @@ */ package fiji.plugin.trackmate.util; -import java.awt.BasicStroke; -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.MouseWheelEvent; -import java.awt.event.MouseWheelListener; -import java.awt.geom.Rectangle2D; -import java.text.DecimalFormat; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.function.DoubleConsumer; - -import javax.swing.JPanel; - +import fiji.plugin.trackmate.Logger; +import fiji.plugin.trackmate.gui.Fonts; +import fiji.plugin.trackmate.gui.components.LogHistogramDataset; +import fiji.plugin.trackmate.gui.components.XYTextSimpleAnnotation; +import fiji.util.NumberParser; +import net.imglib2.util.Util; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; @@ -52,359 +39,316 @@ import org.jfree.chart.renderer.xy.XYBarRenderer; import org.jfree.chart.ui.RectangleInsets; -import fiji.plugin.trackmate.Logger; -import fiji.plugin.trackmate.gui.Fonts; -import fiji.plugin.trackmate.gui.components.LogHistogramDataset; -import fiji.plugin.trackmate.gui.components.XYTextSimpleAnnotation; -import fiji.util.NumberParser; -import net.imglib2.util.Util; +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.Rectangle2D; +import java.text.DecimalFormat; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.DoubleConsumer; -public class QualityHistogramChart extends JPanel -{ - - private static final long serialVersionUID = 1L; - - private static final Color ANNOTATION_COLOR = new java.awt.Color( 252, 117, 0 ); - - private static final String DATA_SERIES_NAME = "Data"; - - private final JFreeChart chart; - - private final XYPlot plot; - - private final XYTextSimpleAnnotation annotation; - - private final IntervalMarker intervalMarker; - - private final ChartPanel chartPanel; - - private double threshold; - - private double autoThreshold = Double.NaN; - - private final DoubleConsumer thresholdSetter; - - - public QualityHistogramChart( final DoubleConsumer thresholdSetter, final String axisLabel ) - { - this.thresholdSetter = thresholdSetter; - this.chart = ChartFactory.createHistogram( null, null, null, null, PlotOrientation.VERTICAL, false, false, false ); - this.plot = chart.getXYPlot(); - this.threshold = 0.; - final XYBarRenderer renderer = ( XYBarRenderer ) plot.getRenderer(); - renderer.setShadowVisible( false ); - renderer.setMargin( 0 ); - renderer.setBarPainter( new StandardXYBarPainter() ); - renderer.setDrawBarOutline( true ); - renderer.setSeriesOutlinePaint( 0, Color.BLACK ); - renderer.setSeriesPaint( 0, new Color( 1, 1, 1, 0 ) ); - - plot.setBackgroundPaint( null ); - plot.setOutlineVisible( false ); - plot.setDomainCrosshairVisible( false ); - plot.setDomainGridlinesVisible( false ); - plot.setRangeCrosshairVisible( false ); - plot.setRangeGridlinesVisible( false ); - plot.setRangeAxisLocation( AxisLocation.TOP_OR_RIGHT ); - - plot.getRangeAxis().setVisible( true ); - plot.getRangeAxis().setTickMarksVisible( false ); - plot.getRangeAxis().setTickLabelsVisible( false ); - plot.getRangeAxis().setLabelPaint( Logger.NORMAL_COLOR ); - plot.getRangeAxis().setLabelFont( Fonts.SMALL_FONT ); - plot.getRangeAxis().setLabel( ( axisLabel == null ) ? "Quality histogram" : axisLabel ); - plot.getRangeAxis().setLabelInsets( new RectangleInsets( 0., 0., 0., 0. ) ); - plot.getRangeAxis().setTickLabelInsets( new RectangleInsets( 0., 0., 0., 0. ) ); - plot.getRangeAxis().setAxisLineVisible( false ); - plot.getDomainAxis().setVisible( true ); - plot.getDomainAxis().setTickLabelsVisible( true ); - plot.getDomainAxis().setTickLabelPaint( Logger.NORMAL_COLOR ); - plot.getDomainAxis().setTickLabelFont( Fonts.SMALL_FONT ); - ( ( NumberAxis ) plot.getDomainAxis() ).setNumberFormatOverride( new DecimalFormat( "#.###" ) ); - - chart.setBorderVisible( false ); - chart.setBackgroundPaint( null ); - this.chartPanel = new ChartPanel( chart ); - - this.intervalMarker = new IntervalMarker( - Double.NEGATIVE_INFINITY, 0., - chartPanel.getBackground(), - new BasicStroke(), - chartPanel.getForeground(), - new BasicStroke(), 0.8f ); - - this.annotation = new XYTextSimpleAnnotation( chartPanel, true ); - annotation.setFont( Fonts.SMALL_FONT ); - annotation.setColor( ANNOTATION_COLOR.darker() ); - - plot.setDataset( null ); - chartPanel.setVisible( false ); - chartPanel.setMinimumDrawHeight( 80 ); - chartPanel.setMinimumDrawWidth( 80 ); - - /* - * Listeners. - */ - - final MouseListener[] mls = chartPanel.getMouseListeners(); - for ( final MouseListener ml : mls ) - chartPanel.removeMouseListener( ml ); - - chartPanel.addMouseListener( new MouseAdapter() - { - @Override - public void mouseClicked( final MouseEvent e ) - { - chartPanel.requestFocusInWindow(); - if ( e.getButton() == MouseEvent.BUTTON3 && !Double.isNaN( autoThreshold ) ) - threshold = autoThreshold; - else - threshold = getXFromChartEvent( e, chartPanel ); - redrawThresholdMarker(); - } - } ); - chartPanel.addMouseMotionListener( new MouseAdapter() - { - @Override - public void mouseDragged( final MouseEvent e ) - { - threshold = getXFromChartEvent( e, chartPanel ); - redrawThresholdMarker(); - } - } ); - chartPanel.addMouseWheelListener( new MouseWheelListener() - { - - @Override - public void mouseWheelMoved( final MouseWheelEvent e ) - { - moveThreshold( e.getWheelRotation() ); - } - } ); - chartPanel.setFocusable( true ); - chartPanel.addKeyListener( new MyKeyListener() ); - - setLayout( new BorderLayout() ); - add( chartPanel, BorderLayout.CENTER ); - } - - public void displayHistogram( final double[] values ) - { - displayHistogram( values, Double.NaN ); - } - - public void displayHistogram( final double[] values, final double threshold ) - { - this.threshold = threshold; - this.autoThreshold = TMUtils.otsuThreshold( values ); - if ( values.length > 0 ) - { - final int nBins = getNBins( values, 8, 100 ); - if ( nBins > 1 ) - { - final LogHistogramDataset dataset = new LogHistogramDataset(); - dataset.addSeries( DATA_SERIES_NAME, values, nBins ); - plot.setDataset( dataset ); - - plot.removeDomainMarker( intervalMarker ); - plot.removeAnnotation( annotation ); - if ( !Double.isNaN( threshold ) ) - { - redrawThresholdMarker(); - plot.addDomainMarker( intervalMarker ); - plot.addAnnotation( annotation ); - } - - chartPanel.setVisible( true ); - return; - } - } - chartPanel.setVisible( false ); - plot.setDataset( null ); - } - - private double getXFromChartEvent( final MouseEvent mouseEvent, final ChartPanel chartPanel ) - { - final Rectangle2D plotArea = chartPanel.getScreenDataArea(); - return plot.getDomainAxis().java2DToValue( mouseEvent.getX(), plotArea, plot.getDomainAxisEdge() ); - } - - private void moveThreshold( final int amount ) - { - if ( Double.isNaN( threshold ) ) - return; - - threshold += ( double ) amount / 100 * plot.getDomainAxis().getRange().getLength(); - redrawThresholdMarker(); - - } - - private void redrawThresholdMarker() - { - if ( Double.isNaN( threshold ) ) - return; - - intervalMarker.setEndValue( threshold ); - - final float x; - if ( threshold > 0.85 * plot.getDomainAxis().getUpperBound() ) - x = ( float ) ( threshold - 0.15 * plot.getDomainAxis().getRange().getLength() ); - else - x = ( float ) ( threshold + 0.05 * plot.getDomainAxis().getRange().getLength() ); - - final float y = ( float ) ( 0.85 * plot.getRangeAxis().getUpperBound() ); - annotation.setText( String.format( "%.1f", threshold ) ); - annotation.setLocation( x, y ); - thresholdChanged(); - } - - private void thresholdChanged() - { - if ( thresholdSetter != null ) - thresholdSetter.accept( threshold ); - } - - /** - * Return the optimal bin number for a histogram of the data given in array, - * using the Freedman and Diaconis rule (bin_space = 2*IQR/n^(1/3)). It is - * ensured that the bin number returned is not smaller and no bigger than - * the bounds given in argument. - * - * @param values - * the values to bin. - * @param minBinNumber - * the minimal desired number of bins. - * @param maxBinNumber - * the maximal desired number of bins. - * @return the number of bins. - */ - private static final int getNBins( final double[] values, final int minBinNumber, final int maxBinNumber ) - { - final int size = values.length; - final double q1 = Util.percentile( values, 0.25 ); - final double q3 = Util.percentile( values, 0.75 ); - final double iqr = q3 - q1; - final double binWidth = 2 * iqr * Math.pow( size, -0.33 ); - - final double max = Util.max( values ); - final double min = Util.min( values ); - final double range = max - min; - - int nBin = ( int ) ( range / binWidth + 1 ); - if ( nBin > maxBinNumber ) - nBin = maxBinNumber; - else if ( nBin < minBinNumber ) - nBin = minBinNumber; - return nBin; - } - - /** - * A class that listen to the user typing a number, building a string - * representation as he types, then converting the string to a double after - * a wait time. The number typed is used to set the threshold in the chart - * panel. - * - * @author Jean-Yves Tinevez - */ - private final class MyKeyListener implements KeyListener - { - - private static final long WAIT_DELAY = 1; // s - - private static final double INCREASE_FACTOR = 0.1; - - private static final double SLOW_INCREASE_FACTOR = 0.005; - - private String strNumber = ""; - - private ScheduledExecutorService ex; - - private ScheduledFuture< ? > future; - - private boolean dotAdded = false; - - private final Runnable command = new Runnable() - { - @Override - public void run() - { - // Convert to double and pass it to threshold value - try - { - final double typedThreshold = NumberParser.parseDouble( strNumber ); - threshold = typedThreshold; - redrawThresholdMarker(); - } - catch ( final NumberFormatException nfe ) - {} - // Reset - ex = null; - strNumber = ""; - dotAdded = false; - } - }; - - @Override - public void keyPressed( final KeyEvent e ) - { - // Is it arrow keys? - if ( e.getKeyCode() == KeyEvent.VK_LEFT || e.getKeyCode() == KeyEvent.VK_KP_LEFT ) - { - threshold -= ( e.isControlDown() ? SLOW_INCREASE_FACTOR : INCREASE_FACTOR ) * plot.getDomainAxis().getRange().getLength(); - redrawThresholdMarker(); - return; - } - else if ( e.getKeyCode() == KeyEvent.VK_RIGHT || e.getKeyCode() == KeyEvent.VK_KP_RIGHT ) - { - threshold += ( e.isControlDown() ? SLOW_INCREASE_FACTOR : INCREASE_FACTOR ) * plot.getDomainAxis().getRange().getLength(); - redrawThresholdMarker(); - return; - } - else if ( e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_KP_UP ) - { - threshold = plot.getDomainAxis().getRange().getUpperBound(); - redrawThresholdMarker(); - return; - } - else if ( e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_KP_DOWN ) - { - threshold = plot.getDomainAxis().getRange().getLowerBound(); - redrawThresholdMarker(); - return; - } - } - - @Override - public void keyReleased( final KeyEvent e ) - {} - - @Override - public void keyTyped( final KeyEvent e ) - { - - if ( e.getKeyChar() < '0' || e.getKeyChar() > '9' ) - { - // Ok then it's number - // User added a decimal dot for the first and only time - if ( !dotAdded && e.getKeyChar() == '.' ) - dotAdded = true; - else - return; - } - - if ( ex == null ) - { - // Create new waiting line - ex = Threads.newSingleThreadScheduledExecutor(); - future = ex.schedule( command, WAIT_DELAY, TimeUnit.SECONDS ); - } - else - { - // Reset waiting line - future.cancel( false ); - future = ex.schedule( command, WAIT_DELAY, TimeUnit.SECONDS ); - } - strNumber += e.getKeyChar(); - } - } +public class QualityHistogramChart extends JPanel { + + private static final long serialVersionUID = 1L; + + private static final Color ANNOTATION_COLOR = new java.awt.Color(252, 117, 0); + + private static final String DATA_SERIES_NAME = "Data"; + + private final JFreeChart chart; + + private final XYPlot plot; + + private final XYTextSimpleAnnotation annotation; + + private final IntervalMarker intervalMarker; + + private final ChartPanel chartPanel; + private final DoubleConsumer thresholdSetter; + private double threshold; + private double autoThreshold = Double.NaN; + + + public QualityHistogramChart(final DoubleConsumer thresholdSetter, final String axisLabel) { + this.thresholdSetter = thresholdSetter; + this.chart = ChartFactory.createHistogram(null, null, null, null, PlotOrientation.VERTICAL, false, false, false); + this.plot = chart.getXYPlot(); + this.threshold = 0.; + final XYBarRenderer renderer = (XYBarRenderer) plot.getRenderer(); + renderer.setShadowVisible(false); + renderer.setMargin(0); + renderer.setBarPainter(new StandardXYBarPainter()); + renderer.setDrawBarOutline(true); + renderer.setSeriesOutlinePaint(0, Color.BLACK); + renderer.setSeriesPaint(0, new Color(1, 1, 1, 0)); + + plot.setBackgroundPaint(null); + plot.setOutlineVisible(false); + plot.setDomainCrosshairVisible(false); + plot.setDomainGridlinesVisible(false); + plot.setRangeCrosshairVisible(false); + plot.setRangeGridlinesVisible(false); + plot.setRangeAxisLocation(AxisLocation.TOP_OR_RIGHT); + + plot.getRangeAxis().setVisible(true); + plot.getRangeAxis().setTickMarksVisible(false); + plot.getRangeAxis().setTickLabelsVisible(false); + plot.getRangeAxis().setLabelPaint(Logger.NORMAL_COLOR); + plot.getRangeAxis().setLabelFont(Fonts.SMALL_FONT); + plot.getRangeAxis().setLabel((axisLabel == null) ? "Quality histogram" : axisLabel); + plot.getRangeAxis().setLabelInsets(new RectangleInsets(0., 0., 0., 0.)); + plot.getRangeAxis().setTickLabelInsets(new RectangleInsets(0., 0., 0., 0.)); + plot.getRangeAxis().setAxisLineVisible(false); + plot.getDomainAxis().setVisible(true); + plot.getDomainAxis().setTickLabelsVisible(true); + plot.getDomainAxis().setTickLabelPaint(Logger.NORMAL_COLOR); + plot.getDomainAxis().setTickLabelFont(Fonts.SMALL_FONT); + ((NumberAxis) plot.getDomainAxis()).setNumberFormatOverride(new DecimalFormat("#.###")); + + chart.setBorderVisible(false); + chart.setBackgroundPaint(null); + this.chartPanel = new ChartPanel(chart); + + this.intervalMarker = new IntervalMarker( + Double.NEGATIVE_INFINITY, 0., + chartPanel.getBackground(), + new BasicStroke(), + chartPanel.getForeground(), + new BasicStroke(), 0.8f); + + this.annotation = new XYTextSimpleAnnotation(chartPanel, true); + annotation.setFont(Fonts.SMALL_FONT); + annotation.setColor(ANNOTATION_COLOR.darker()); + + plot.setDataset(null); + chartPanel.setVisible(false); + chartPanel.setMinimumDrawHeight(80); + chartPanel.setMinimumDrawWidth(80); + + /* + * Listeners. + */ + + final MouseListener[] mls = chartPanel.getMouseListeners(); + for (final MouseListener ml : mls) + chartPanel.removeMouseListener(ml); + + chartPanel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(final MouseEvent e) { + chartPanel.requestFocusInWindow(); + if (e.getButton() == MouseEvent.BUTTON3 && !Double.isNaN(autoThreshold)) + threshold = autoThreshold; + else + threshold = getXFromChartEvent(e, chartPanel); + redrawThresholdMarker(); + } + }); + chartPanel.addMouseMotionListener(new MouseAdapter() { + @Override + public void mouseDragged(final MouseEvent e) { + threshold = getXFromChartEvent(e, chartPanel); + redrawThresholdMarker(); + } + }); + chartPanel.addMouseWheelListener(new MouseWheelListener() { + + @Override + public void mouseWheelMoved(final MouseWheelEvent e) { + moveThreshold(e.getWheelRotation()); + } + }); + chartPanel.setFocusable(true); + chartPanel.addKeyListener(new MyKeyListener()); + + setLayout(new BorderLayout()); + add(chartPanel, BorderLayout.CENTER); + } + + /** + * Return the optimal bin number for a histogram of the data given in array, + * using the Freedman and Diaconis rule (bin_space = 2*IQR/n^(1/3)). It is + * ensured that the bin number returned is not smaller and no bigger than + * the bounds given in argument. + * + * @param values the values to bin. + * @param minBinNumber the minimal desired number of bins. + * @param maxBinNumber the maximal desired number of bins. + * @return the number of bins. + */ + private static final int getNBins(final double[] values, final int minBinNumber, final int maxBinNumber) { + final int size = values.length; + final double q1 = Util.percentile(values, 0.25); + final double q3 = Util.percentile(values, 0.75); + final double iqr = q3 - q1; + final double binWidth = 2 * iqr * Math.pow(size, -0.33); + + final double max = Util.max(values); + final double min = Util.min(values); + final double range = max - min; + + int nBin = (int) (range / binWidth + 1); + if (nBin > maxBinNumber) + nBin = maxBinNumber; + else if (nBin < minBinNumber) + nBin = minBinNumber; + return nBin; + } + + public void displayHistogram(final double[] values) { + displayHistogram(values, Double.NaN); + } + + public void displayHistogram(final double[] values, final double threshold) { + this.threshold = threshold; + this.autoThreshold = HistogramUtils.otsuThreshold(values); + if (values.length > 0) { + final int nBins = getNBins(values, 8, 100); + if (nBins > 1) { + final LogHistogramDataset dataset = new LogHistogramDataset(); + dataset.addSeries(DATA_SERIES_NAME, values, nBins); + plot.setDataset(dataset); + + plot.removeDomainMarker(intervalMarker); + plot.removeAnnotation(annotation); + if (!Double.isNaN(threshold)) { + redrawThresholdMarker(); + plot.addDomainMarker(intervalMarker); + plot.addAnnotation(annotation); + } + + chartPanel.setVisible(true); + return; + } + } + chartPanel.setVisible(false); + plot.setDataset(null); + } + + private double getXFromChartEvent(final MouseEvent mouseEvent, final ChartPanel chartPanel) { + final Rectangle2D plotArea = chartPanel.getScreenDataArea(); + return plot.getDomainAxis().java2DToValue(mouseEvent.getX(), plotArea, plot.getDomainAxisEdge()); + } + + private void moveThreshold(final int amount) { + if (Double.isNaN(threshold)) + return; + + threshold += (double) amount / 100 * plot.getDomainAxis().getRange().getLength(); + redrawThresholdMarker(); + + } + + private void redrawThresholdMarker() { + if (Double.isNaN(threshold)) + return; + + intervalMarker.setEndValue(threshold); + + final float x; + if (threshold > 0.85 * plot.getDomainAxis().getUpperBound()) + x = (float) (threshold - 0.15 * plot.getDomainAxis().getRange().getLength()); + else + x = (float) (threshold + 0.05 * plot.getDomainAxis().getRange().getLength()); + + final float y = (float) (0.85 * plot.getRangeAxis().getUpperBound()); + annotation.setText(String.format("%.1f", threshold)); + annotation.setLocation(x, y); + thresholdChanged(); + } + + private void thresholdChanged() { + if (thresholdSetter != null) + thresholdSetter.accept(threshold); + } + + /** + * A class that listen to the user typing a number, building a string + * representation as he types, then converting the string to a double after + * a wait time. The number typed is used to set the threshold in the chart + * panel. + * + * @author Jean-Yves Tinevez + */ + private final class MyKeyListener implements KeyListener { + + private static final long WAIT_DELAY = 1; // s + + private static final double INCREASE_FACTOR = 0.1; + + private static final double SLOW_INCREASE_FACTOR = 0.005; + + private String strNumber = ""; + + private ScheduledExecutorService ex; + + private ScheduledFuture future; + + private boolean dotAdded = false; + + private final Runnable command = new Runnable() { + @Override + public void run() { + // Convert to double and pass it to threshold value + try { + final double typedThreshold = NumberParser.parseDouble(strNumber); + threshold = typedThreshold; + redrawThresholdMarker(); + } catch (final NumberFormatException nfe) { + } + // Reset + ex = null; + strNumber = ""; + dotAdded = false; + } + }; + + @Override + public void keyPressed(final KeyEvent e) { + // Is it arrow keys? + if (e.getKeyCode() == KeyEvent.VK_LEFT || e.getKeyCode() == KeyEvent.VK_KP_LEFT) { + threshold -= (e.isControlDown() ? SLOW_INCREASE_FACTOR : INCREASE_FACTOR) * plot.getDomainAxis().getRange().getLength(); + redrawThresholdMarker(); + } else if (e.getKeyCode() == KeyEvent.VK_RIGHT || e.getKeyCode() == KeyEvent.VK_KP_RIGHT) { + threshold += (e.isControlDown() ? SLOW_INCREASE_FACTOR : INCREASE_FACTOR) * plot.getDomainAxis().getRange().getLength(); + redrawThresholdMarker(); + } else if (e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_KP_UP) { + threshold = plot.getDomainAxis().getRange().getUpperBound(); + redrawThresholdMarker(); + } else if (e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_KP_DOWN) { + threshold = plot.getDomainAxis().getRange().getLowerBound(); + redrawThresholdMarker(); + } + } + + @Override + public void keyReleased(final KeyEvent e) { + } + + @Override + public void keyTyped(final KeyEvent e) { + + if (e.getKeyChar() < '0' || e.getKeyChar() > '9') { + // Ok then it's number + // User added a decimal dot for the first and only time + if (!dotAdded && e.getKeyChar() == '.') + dotAdded = true; + else + return; + } + + if (ex == null) { + // Create new waiting line + ex = Threads.newSingleThreadScheduledExecutor(); + future = ex.schedule(command, WAIT_DELAY, TimeUnit.SECONDS); + } else { + // Reset waiting line + future.cancel(false); + future = ex.schedule(command, WAIT_DELAY, TimeUnit.SECONDS); + } + strNumber += e.getKeyChar(); + } + } } diff --git a/src/main/java/fiji/plugin/trackmate/util/TMUtils.java b/src/main/java/fiji/plugin/trackmate/util/TMUtils.java index 25f707e45..121531c79 100644 --- a/src/main/java/fiji/plugin/trackmate/util/TMUtils.java +++ b/src/main/java/fiji/plugin/trackmate/util/TMUtils.java @@ -8,12 +8,12 @@ * 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 * . @@ -21,31 +21,9 @@ */ package fiji.plugin.trackmate.util; -import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; - -import java.io.File; -import java.nio.file.FileSystems; -import java.nio.file.Paths; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import org.scijava.Context; -import org.scijava.util.DoubleArray; - import fiji.plugin.trackmate.Dimension; import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Settings; -import fiji.plugin.trackmate.Spot; import ij.IJ; import ij.ImagePlus; import net.imagej.ImgPlus; @@ -58,817 +36,745 @@ import net.imglib2.type.Type; import net.imglib2.type.numeric.real.DoubleType; import net.imglib2.util.Util; +import org.scijava.Context; +import org.scijava.util.DoubleArray; + +import java.io.File; +import java.nio.file.FileSystems; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.Map.Entry; + +import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; /** * List of static utilities for {@link fiji.plugin.trackmate.TrackMate}. */ -public class TMUtils -{ - - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat( "EEE, d MMM yyyy HH:mm:ss" ); - - private static Context context; - - /* - * STATIC METHODS - */ - - /** - * Return a new map sorted by its values. - */ - public static < K, V extends Comparable< ? super V > > Map< K, V > sortByValue( final Map< K, V > map, final Comparator< V > comparator ) - { - final List< Map.Entry< K, V > > list = new ArrayList<>( map.entrySet() ); - final Comparator< Map.Entry< K, V > > c = new Comparator< Map.Entry< K, V > >() - { - - @Override - public int compare( final Entry< K, V > o1, final Entry< K, V > o2 ) - { - final V val1 = o1.getValue(); - final V val2 = o2.getValue(); - return comparator.compare( val1, val2 ); - } - }; - Collections.sort( list, c ); - final LinkedHashMap< K, V > result = new LinkedHashMap<>(); - for ( final Map.Entry< K, V > entry : list ) - result.put( entry.getKey(), entry.getValue() ); - return result; - } - - /** - * Generate a string representation of a map, typically a settings map. - */ - public static final String echoMap( final Map< String, Object > map, final int indent ) - { - // Build string - final StringBuilder builder = new StringBuilder(); - for ( final String key : map.keySet() ) - { - for ( int i = 0; i < indent; i++ ) - builder.append( " " ); - - builder.append( "- " ); - builder.append( key.toLowerCase().replace( "_", " " ) ); - builder.append( ": " ); - final Object obj = map.get( key ); - if ( obj instanceof Map ) - { - builder.append( '\n' ); - @SuppressWarnings( "unchecked" ) - final Map< String, Object > submap = ( Map< String, Object > ) obj; - builder.append( echoMap( submap, indent + 2 ) ); - } - else if ( obj instanceof Logger ) - { - builder.append( obj.getClass().getSimpleName() ); - builder.append( '\n' ); - } - else - { - builder.append( obj.toString() ); - builder.append( '\n' ); - } - } - return builder.toString(); - } - - /** - * Wraps an IJ {@link ImagePlus} in an imglib2 {@link ImgPlus}, without - * parameterized types. The only way I have found to beat javac constraints - * on bounded multiple wildcard. - */ - @SuppressWarnings( "rawtypes" ) - public static final ImgPlus rawWraps( final ImagePlus imp ) - { - final ImgPlus< DoubleType > img = ImagePlusAdapter.wrapImgPlus( imp ); - final ImgPlus raw = img; - return raw; - } - - /** - * Check that the given map has all some keys. Two String collection allows - * specifying that some keys are mandatory, other are optional. - * - * @param map - * the map to inspect. - * @param mandatoryKeys - * the collection of keys that are expected to be in the map. Can - * be null. - * @param optionalKeys - * the collection of keys that can be - or not - in the map. Can - * be null. - * @param errorHolder - * will be appended with an error message. - * @return if all mandatory keys are found in the map, and possibly some - * optional ones, but no others. - */ - public static final < T > boolean checkMapKeys( final Map< T, ? > map, Collection< T > mandatoryKeys, Collection< T > optionalKeys, final StringBuilder errorHolder ) - { - if ( null == optionalKeys ) - optionalKeys = new ArrayList<>(); - - if ( null == mandatoryKeys ) - mandatoryKeys = new ArrayList<>(); - - boolean ok = true; - final Set< T > keySet = map.keySet(); - for ( final T key : keySet ) - { - if ( !( mandatoryKeys.contains( key ) || optionalKeys.contains( key ) ) ) - { - ok = false; - errorHolder.append( "Map contains unexpected key: " + key + ".\n" ); - } - } - - for ( final T key : mandatoryKeys ) - { - if ( !keySet.contains( key ) ) - { - ok = false; - errorHolder.append( "Mandatory key " + key + " was not found in the map.\n" ); - } - } - return ok; - - } - - /** - * Check the presence and the validity of a key in a map, and test it is of - * the desired class. - * - * @param map - * the map to inspect. - * @param key - * the key to find. - * @param expectedClass - * the expected class of the target value . - * @param errorHolder - * will be appended with an error message. - * @return true if the key is found in the map, and map a value of the - * desired class. - */ - public static final boolean checkParameter( final Map< String, Object > map, final String key, final Class< ? > expectedClass, final StringBuilder errorHolder ) - { - final Object obj = map.get( key ); - if ( null == obj ) - { - errorHolder.append( "Parameter " + key + " could not be found in settings map, or is null.\n" ); - return false; - } - if ( !expectedClass.isInstance( obj ) ) - { - errorHolder.append( "Value for parameter " + key + " is not of the right class. Expected " + expectedClass.getName() + ", got " + obj.getClass().getName() + ".\n" ); - return false; - } - return true; - } - - /** - * Returns the mapping in a map that is targeted by a list of keys, in the - * order given in the list. - */ - public static final < J, K > List< K > getArrayFromMaping( final Collection< J > keys, final Map< J, K > mapping ) - { - final List< K > names = new ArrayList<>( keys.size() ); - for ( final J key : keys ) - names.add( mapping.get( key ) ); - return names; - } - - /* - * ImgPlus & calibration & axes - */ - - /** - * Return the xyz calibration stored in an {@link ImgPlusMetadata} in a - * 3-elements double array. Calibration is ordered as X, Y, Z. If one axis - * is not found, then the calibration for this axis takes the value of 1. - */ - public static final double[] getSpatialCalibration( final ImgPlusMetadata img ) - { - final double[] calibration = Util.getArrayFromValue( 1d, 3 ); - - for ( int d = 0; d < img.numDimensions(); d++ ) - { - if ( img.axis( d ).type() == Axes.X ) - calibration[ 0 ] = img.averageScale( d ); - else if ( img.axis( d ).type() == Axes.Y ) - calibration[ 1 ] = img.averageScale( d ); - else if ( img.axis( d ).type() == Axes.Z ) - calibration[ 2 ] = img.averageScale( d ); - } - return calibration; - } - - public static double[] getSpatialCalibration( final ImagePlus imp ) - { - final double[] calibration = Util.getArrayFromValue( 1d, 3 ); - calibration[ 0 ] = imp.getCalibration().pixelWidth; - calibration[ 1 ] = imp.getCalibration().pixelHeight; - if ( imp.getNSlices() > 1 ) - calibration[ 2 ] = imp.getCalibration().pixelDepth; - - return calibration; - } - - /** - * Returns an estimate of the pth percentile of the values in - * the values array. Taken from commons-math. - */ - public static final double getPercentile( final double[] values, final double p ) - { - - final int size = values.length; - if ( ( p > 1 ) || ( p <= 0 ) ) - throw new IllegalArgumentException( "invalid quantile value: " + p ); - // always return single value for n = 1 - if ( size == 0 ) - return Double.NaN; - if ( size == 1 ) - return values[ 0 ]; - final double n = size; - final double pos = p * ( n + 1 ); - final double fpos = Math.floor( pos ); - final int intPos = ( int ) fpos; - final double dif = pos - fpos; - final double[] sorted = new double[ size ]; - System.arraycopy( values, 0, sorted, 0, size ); - Arrays.sort( sorted ); - - if ( pos < 1 ) - return sorted[ 0 ]; - if ( pos >= n ) - return sorted[ size - 1 ]; - final double lower = sorted[ intPos - 1 ]; - final double upper = sorted[ intPos ]; - return lower + dif * ( upper - lower ); - } - - /** - * Returns [range, min, max] of the given double array. - * - * @return A double[] of length 3, where index 0 is the range, index 1 is - * the min, and index 2 is the max. - */ - private static final double[] getRange( final double[] data ) - { - if ( data.length == 0 ) - return new double[] { 1., 0., 1. }; - - final double min = Arrays.stream( data ).min().getAsDouble(); - final double max = Arrays.stream( data ).max().getAsDouble(); - return new double[] { ( max - min ), min, max }; - } - - /** - * Store the x, y, z coordinates of the specified spot in the first 3 - * elements of the specified double array. - */ - public static final void localize( final Spot spot, final double[] coords ) - { - coords[ 0 ] = spot.getFeature( Spot.POSITION_X ).doubleValue(); - coords[ 1 ] = spot.getFeature( Spot.POSITION_Y ).doubleValue(); - coords[ 2 ] = spot.getFeature( Spot.POSITION_Z ).doubleValue(); - } - - /** - * Return the optimal bin number for a histogram of the data given in array, - * using the Freedman and Diaconis rule (bin_space = 2*IQR/n^(1/3)). It is - * ensured that the bin number returned is not smaller and no bigger than - * the bounds given in argument. - */ - public static final int getNBins( final double[] values, final int minBinNumber, final int maxBinNumber ) - { - final int size = values.length; - final double q1 = getPercentile( values, 0.25 ); - final double q3 = getPercentile( values, 0.75 ); - final double iqr = q3 - q1; - final double binWidth = 2 * iqr * Math.pow( size, -0.33 ); - final double[] range = getRange( values ); - int nBin = ( int ) ( range[ 0 ] / binWidth + 1 ); - - if ( nBin > maxBinNumber ) - nBin = maxBinNumber; - else if ( nBin < minBinNumber ) - nBin = minBinNumber; - - return nBin; - } - - /** - * Return the optimal bin number for a histogram of the data given in array, - * using the Freedman and Diaconis rule (bin_space = 2*IQR/n^(1/3)). It is - * ensured that the bin number returned is not smaller than 8 and no bigger - * than 256. - */ - private static final int getNBins( final double[] values ) - { - return getNBins( values, 8, 256 ); - } - - /** - * Create a histogram from the data given. - */ - private static final int[] histogram( final double data[], final int nBins ) - { - final double[] range = getRange( data ); - final double binWidth = range[ 0 ] / nBins; - final int[] hist = new int[ nBins ]; - int index; - - if ( nBins > 0 ) - { - for ( int i = 0; i < data.length; i++ ) - { - index = Math.min( ( int ) Math.floor( ( data[ i ] - range[ 1 ] ) / binWidth ), nBins - 1 ); - hist[ index ]++; - } - } - return hist; - } - - /** - * Return a threshold for the given data, using an Otsu histogram - * thresholding method. - */ - public static final double otsuThreshold( final double[] data ) - { - return otsuThreshold( data, getNBins( data ) ); - } - - /** - * Return a threshold for the given data, using an Otsu histogram - * thresholding method with a given bin number. - */ - private static final double otsuThreshold( final double[] data, final int nBins ) - { - final int[] hist = histogram( data, nBins ); - final int thresholdIndex = otsuThresholdIndex( hist, data.length ); - final double[] range = getRange( data ); - final double binWidth = range[ 0 ] / nBins; - return range[ 1 ] + binWidth * thresholdIndex; - } - - /** - * Given a histogram array hist, built with an initial amount - * of nPoints data item, this method return the bin index that - * thresholds the histogram in 2 classes. The threshold is performed using - * the Otsu Threshold Method. - * - * @param hist - * the histogram array - * @param nPoints - * the number of data items this histogram was built on - * @return the bin index of the histogram that thresholds it - */ - private static final int otsuThresholdIndex( final int[] hist, final int nPoints ) - { - final int total = nPoints; - - double sum = 0; - for ( int t = 0; t < hist.length; t++ ) - sum += t * hist[ t ]; - - double sumB = 0; - int wB = 0; - int wF = 0; - - double varMax = 0; - int threshold = 0; - - for ( int t = 0; t < hist.length; t++ ) - { - wB += hist[ t ]; // Weight Background - if ( wB == 0 ) - continue; - - wF = total - wB; // Weight Foreground - if ( wF == 0 ) - break; - - sumB += ( t * hist[ t ] ); - - final double mB = sumB / wB; // Mean Background - final double mF = ( sum - sumB ) / wF; // Mean Foreground - - // Calculate Between Class Variance - final double varBetween = wB * wF * ( mB - mF ) * ( mB - mF ); - - // Check if new maximum found - if ( varBetween > varMax ) - { - varMax = varBetween; - threshold = t; - } - } - return threshold; - } - - /** - * Return a String unit for the given dimension. When suitable, the unit is - * taken from the settings field, which contains the spatial and time units. - * Otherwise, default units are used. - */ - public static final String getUnitsFor( final Dimension dimension, final String spaceUnits, final String timeUnits ) - { - switch ( dimension ) - { - case ANGLE: - return "radians"; - case INTENSITY: - return "counts"; - case INTENSITY_SQUARED: - return "counts^2"; - case NONE: - return ""; - case POSITION: - case LENGTH: - return spaceUnits; - case AREA: - return spaceUnits + "^2"; - case QUALITY: - return "quality"; - case COST: - return "cost"; - case TIME: - return timeUnits; - case VELOCITY: - return spaceUnits + "/" + timeUnits; - case RATE: - return "/" + timeUnits; - case ANGLE_RATE: - return "rad/" + timeUnits; - default: - case STRING: - return null; - } - } - - public static final String getCurrentTimeString() - { - return DATE_FORMAT.format( new Date() ); - } - - public static < T extends Type< T > > ImgPlus< T > hyperSlice( final ImgPlus< T > img, final long channel, final long frame ) - { - final int timeDim = img.dimensionIndex( Axes.TIME ); - final ImgPlus< T > imgT = timeDim < 0 ? img : ImgPlusViews.hyperSlice( img, timeDim, frame ); - - final int channelDim = imgT.dimensionIndex( Axes.CHANNEL ); - final ImgPlus< T > imgTC = channelDim < 0 ? imgT : ImgPlusViews.hyperSlice( imgT, channelDim, channel ); - - // Squeeze Z dimension if its size is 1. - final int zDim = imgTC.dimensionIndex( Axes.Z ); - final ImgPlus< T > imgTCZ; - if ( zDim >= 0 && imgTC.dimension( zDim ) <= 1 ) - imgTCZ = ImgPlusViews.hyperSlice( imgTC, zDim, imgTC.min( zDim ) ); - else - imgTCZ = imgTC; - - return imgTCZ; - } - - /** - * Returns an interval object that slices in the specified {@link ImgPlus} - * in a single channel (the channel dimension is dropped). - *

- * The specified {@link Settings} object is used to determine a crop-cube - * that will determine the X,Y,Z size of the interval. The channel dimension - * will be dropped. - *

- * If the specified {@link ImgPlus} has a time axis, it will be included, - * using the {@link Settings#tstart} and {@link Settings#tend} as bounds. If - * it is a singleton dimension (1 time-point) it won't be dropped. - * - * @param img - * the source image into which the interval is to be defined. - * @param settings - * the settings object that will determine the interval size. - * @return a new interval. - */ - public static final Interval getIntervalWithTime( final ImgPlus< ? > img, final Settings settings ) - { - final long[] max = new long[ img.numDimensions() ]; - final long[] min = new long[ img.numDimensions() ]; - - // X, we must have it. - final int xindex = img.dimensionIndex( Axes.X ); - min[ xindex ] = settings.getXstart(); - max[ xindex ] = settings.getXend(); - - // Y, we must have it. - final int yindex = img.dimensionIndex( Axes.Y ); - min[ yindex ] = settings.getYstart(); - max[ yindex ] = settings.getYend(); - - // Z, we MIGHT have it. - final int zindex = img.dimensionIndex( Axes.Z ); - if ( zindex >= 0 ) - { - min[ zindex ] = settings.zstart; - max[ zindex ] = settings.zend; - } - - // TIME, we might have it, but anyway we leave the start & end - // management to elsewhere. - final int tindex = img.dimensionIndex( Axes.TIME ); - if ( tindex >= 0 ) - { - min[ tindex ] = settings.tstart; - max[ tindex ] = settings.tend; - } - - // CHANNEL, we might have it, we drop it. - final long[] max2; - final long[] min2; - final int cindex = img.dimensionIndex( Axes.CHANNEL ); - if ( cindex >= 0 ) - { - max2 = new long[ img.numDimensions() - 1 ]; - min2 = new long[ img.numDimensions() - 1 ]; - int d2 = 0; - for ( int d = 0; d < min.length; d++ ) - { - if ( d != cindex ) - { - min2[ d2 ] = Math.max( 0l, min[ d ] ); - max2[ d2 ] = Math.min( img.max( d ), max[ d ] ); - d2++; - } - } - } - else - { - min2 = new long[ min.length ]; - max2 = new long[ min.length ]; - for ( int d = 0; d < min.length; d++ ) - { - min2[ d ] = Math.max( 0l, min[ d ] ); - max2[ d ] = Math.min( img.max( d ), max[ d ] ); - } - } - - final FinalInterval interval = new FinalInterval( min2, max2 ); - return interval; - } - - /** - * Returns an interval object that in the specified {@link ImgPlus} slice - * in a single time frame. - *

- * The specified {@link Settings} object is used to determine a crop-cube - * that will determine the X,Y,Z size of the interval. A single channel will - * be taken in the case of a multi-channel image. If the detector set in the - * settings object has a parameter for the target channel - * {@link fiji.plugin.trackmate.detection.DetectorKeys#KEY_TARGET_CHANNEL}, - * it will be used; otherwise the first channel will be taken. - *

- * If the specified {@link ImgPlus} has a time axis, it will be dropped and - * the returned interval will have one dimension less. - * - * @param img - * the source image into which the interval is to be defined. - * @param settings - * the settings object that will determine the interval size. - * @return a new interval. - */ - public static final Interval getInterval( final ImgPlus< ? > img, final Settings settings ) - { - final long[] max = new long[ img.numDimensions() ]; - final long[] min = new long[ img.numDimensions() ]; - - // X, we must have it. - final int xindex = img.dimensionIndex( Axes.X ); - min[ xindex ] = settings.getXstart(); - max[ xindex ] = settings.getXend(); - - // Y, we must have it. - final int yindex = img.dimensionIndex( Axes.Y ); - min[ yindex ] = settings.getYstart(); - max[ yindex ] = settings.getYend(); - - // Z, we MIGHT have it. - final int zindex = img.dimensionIndex( Axes.Z ); - if ( zindex >= 0 ) - { - min[ zindex ] = settings.zstart; - max[ zindex ] = settings.zend; - } - - // CHANNEL, we might have it. - final int cindex = img.dimensionIndex( Axes.CHANNEL ); - if ( cindex >= 0 ) - { - Integer c = ( Integer ) settings.detectorSettings.get( KEY_TARGET_CHANNEL ); // 1-based. - if ( null == c ) - c = 1; - - min[ cindex ] = c - 1; // 0-based. - max[ cindex ] = min[ cindex ]; - } - - // TIME, we might have it, but anyway we leave the start & end - // management to elsewhere. - final int tindex = img.dimensionIndex( Axes.TIME ); - - /* - * We want to exclude time (if we have it) from out interval and source, - * so that we can provide the detector instance with a hyperslice that - * does NOT have time as a dimension. - */ - final long[] intervalMin; - final long[] intervalMax; - if ( tindex >= 0 ) - { - intervalMin = new long[ min.length - 1 ]; - intervalMax = new long[ min.length - 1 ]; - int nindex = -1; - for ( int d = 0; d < min.length; d++ ) - { - if ( d == tindex ) - continue; - - nindex++; - intervalMin[ nindex ] = Math.max( 0l, min[ d ] ); - intervalMax[ nindex ] = Math.min( img.max( d ), max[ d ] ); - } - } - else - { - intervalMin = new long[ min.length ]; - intervalMax = new long[ min.length ]; - for ( int d = 0; d < min.length; d++ ) - { - intervalMin[ d ] = Math.max( 0l, min[ d ] ); - intervalMax[ d ] = Math.min( img.max( d ), max[ d ] ); - } - } - final FinalInterval interval = new FinalInterval( intervalMin, intervalMax ); - return interval; - } - - /** Obtains the SciJava {@link Context} in use by ImageJ. */ - public static Context getContext() - { - final Context localContext = context; - if ( localContext != null ) - return localContext; - - synchronized ( TMUtils.class ) - { - if ( context == null ) - context = ( Context ) IJ.runPlugIn( "org.scijava.Context", "" ); - return context; - } - } - - /** - * Creates a default file path to save the TrackMate session to, based on - * the image TrackMate works on. - * - * @param settings - * the settings object from which to read the image, its folder, - * etc. - * @param logger - * a logger instance in which to echo problems if any. - * @return a new file. - */ - public static File proposeTrackMateSaveFile( final Settings settings, final Logger logger ) - { - File folder; - if ( null != settings.imp && null != settings.imp.getOriginalFileInfo() && null != settings.imp.getOriginalFileInfo().directory ) - { - final String directory = settings.imp.getOriginalFileInfo().directory; - folder = Paths.get( directory ).toAbsolutePath().toFile(); - /* - * Update the settings field with the image file location now, - * because it's valid. - */ - settings.imageFolder = settings.imp.getOriginalFileInfo().directory; - } - else if ( !settings.imageFolder.isEmpty() ) - { - final String absolutePath = FileSystems.getDefault().getPath( settings.imageFolder ).normalize().toAbsolutePath().toString(); - folder = new File( absolutePath ); - } - else - { - folder = new File( System.getProperty( "user.dir" ) ); - /* - * Warn the user that the file cannot be reloaded properly because - * the source image does not match a file. - */ - logger.error( "Warning: The source image does not match a file on the system." + "TrackMate won't be able to reload it when opening this XML file.\n" + "To fix this, save the source image to a TIF file before saving the TrackMate session.\n" ); - settings.imageFolder = ""; - } - - File file; - try - { - file = new File( folder.getPath(), settings.imp.getShortTitle() + ".xml" ); - } - catch ( final NullPointerException npe ) - { - if ( settings.imageFileName.isEmpty() ) - file = new File( folder, "TrackMateData.xml" ); - else - { - final String imName = settings.imageFileName; - final int i = imName.lastIndexOf( '.' ); - String xmlName; - if ( i < 0 ) - xmlName = imName + ".xml"; - else - xmlName = imName.substring( 0, i ) + ".xml"; - file = new File( folder, xmlName ); - } - } - return file; - } - - public static final double variance( final double[] data ) - { - final double mean = Util.average( data ); - double variance = 0; - for ( int i = 0; i < data.length; i++ ) - { - final double dx = data[ i ] - mean; - variance += dx * dx; - } - variance /= ( data.length - 1 ); - return variance; - } - - public static final double standardDeviation( final double[] data ) - { - return Math.sqrt( variance( data ) ); - } - - public static double sum( final double[] data ) - { - return Arrays.stream( data ).sum(); - } - - public static double average( final DoubleArray data ) - { - return sum( data ) / data.size(); - } - - public static double sum( final DoubleArray data ) - { - double sum = 0.; - for ( int i = 0; i < data.size(); i++ ) - sum += data.getArray()[ i ]; - return sum; - } - - public static final double variance( final DoubleArray data ) - { - final double mean = average( data ); - double variance = 0; - for ( int i = 0; i < data.size(); i++ ) - { - final double dx = data.getArray()[ i ] - mean; - variance += dx * dx; - } - variance /= ( data.size() - 1 ); - return variance; - } - - public static double standardDeviation( final DoubleArray data ) - { - return Math.sqrt( variance( data ) ); - } - - /** - * Returns a string of the name of the image without the extension, with the - * full path - * - * @return full name of the image without the extension - */ - public static String getImagePathWithoutExtension( final Settings settings ) - { - final String imageFolder = ( settings.imageFolder == null ) - ? System.getProperty( "user.home" ) - : settings.imageFolder; - - final String imageFileName = settings.imageFileName; - if ( imageFileName != null ) - { - final int lastIndexOf = imageFileName.lastIndexOf( "." ); - if ( lastIndexOf > 0 ) - return imageFolder + imageFileName.substring( 0, imageFileName.lastIndexOf( "." ) ); - return imageFolder + imageFileName; - } - else - { - return imageFolder + File.separator + "TrackMate"; - } - } - - private TMUtils() - {} +public class TMUtils { + + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss"); + + private static Context context; + + /* + * STATIC METHODS + */ + + private TMUtils() { + } + + /** + * Return a new map sorted by its values. + */ + public static > Map sortByValue(final Map map, final Comparator comparator) { + final List> list = new ArrayList<>(map.entrySet()); + final Comparator> c = new Comparator>() { + + @Override + public int compare(final Entry o1, final Entry o2) { + final V val1 = o1.getValue(); + final V val2 = o2.getValue(); + return comparator.compare(val1, val2); + } + }; + Collections.sort(list, c); + final LinkedHashMap result = new LinkedHashMap<>(); + for (final Map.Entry entry : list) + result.put(entry.getKey(), entry.getValue()); + return result; + } + + /** + * Generate a string representation of a map, typically a settings map. + */ + public static final String echoMap(final Map map, final int indent) { + // Build string + final StringBuilder builder = new StringBuilder(); + for (final String key : map.keySet()) { + for (int i = 0; i < indent; i++) + builder.append(" "); + + builder.append("- "); + builder.append(key.toLowerCase().replace("_", " ")); + builder.append(": "); + final Object obj = map.get(key); + if (obj instanceof Map) { + builder.append('\n'); + @SuppressWarnings("unchecked") final Map submap = (Map) obj; + builder.append(echoMap(submap, indent + 2)); + } else if (obj instanceof Logger) { + builder.append(obj.getClass().getSimpleName()); + builder.append('\n'); + } else { + builder.append(obj.toString()); + builder.append('\n'); + } + } + return builder.toString(); + } + + /** + * Wraps an IJ {@link ImagePlus} in an imglib2 {@link ImgPlus}, without + * parameterized types. The only way I have found to beat javac constraints + * on bounded multiple wildcard. + */ + @SuppressWarnings("rawtypes") + public static final ImgPlus rawWraps(final ImagePlus imp) { + final ImgPlus img = ImagePlusAdapter.wrapImgPlus(imp); + final ImgPlus raw = img; + return raw; + } + + /** + * Check that the given map has all some keys. Two String collection allows + * specifying that some keys are mandatory, other are optional. + * + * @param map the map to inspect. + * @param mandatoryKeys the collection of keys that are expected to be in the map. Can + * be null. + * @param optionalKeys the collection of keys that can be - or not - in the map. Can + * be null. + * @param errorHolder will be appended with an error message. + * @return if all mandatory keys are found in the map, and possibly some + * optional ones, but no others. + */ + public static final boolean checkMapKeys(final Map map, Collection mandatoryKeys, Collection optionalKeys, final StringBuilder errorHolder) { + if (null == optionalKeys) + optionalKeys = new ArrayList<>(); + + if (null == mandatoryKeys) + mandatoryKeys = new ArrayList<>(); + + boolean ok = true; + final Set keySet = map.keySet(); + for (final T key : keySet) { + if (!(mandatoryKeys.contains(key) || optionalKeys.contains(key))) { + ok = false; + errorHolder.append("Map contains unexpected key: " + key + ".\n"); + } + } + + for (final T key : mandatoryKeys) { + if (!keySet.contains(key)) { + ok = false; + errorHolder.append("Mandatory key " + key + " was not found in the map.\n"); + } + } + return ok; + + } + + /** + * Check the presence and the validity of a key in a map, and test it is of + * the desired class. + * + * @param map the map to inspect. + * @param key the key to find. + * @param expectedClass the expected class of the target value . + * @param errorHolder will be appended with an error message. + * @return true if the key is found in the map, and map a value of the + * desired class. + */ + public static final boolean checkParameter(final Map map, final String key, final Class expectedClass, final StringBuilder errorHolder) { + final Object obj = map.get(key); + if (null == obj) { + errorHolder.append("Parameter " + key + " could not be found in settings map, or is null.\n"); + return false; + } + if (!expectedClass.isInstance(obj)) { + errorHolder.append("Value for parameter " + key + " is not of the right class. Expected " + expectedClass.getName() + ", got " + obj.getClass().getName() + ".\n"); + return false; + } + return true; + } + + /* + * ImgPlus & calibration & axes + */ + + /** + * Returns the mapping in a map that is targeted by a list of keys, in the + * order given in the list. + */ + public static final List getArrayFromMaping(final Collection keys, final Map mapping) { + final List names = new ArrayList<>(keys.size()); + for (final J key : keys) + names.add(mapping.get(key)); + return names; + } + + /** + * Return the xyz calibration stored in an {@link ImgPlusMetadata} in a + * 3-elements double array. Calibration is ordered as X, Y, Z. If one axis + * is not found, then the calibration for this axis takes the value of 1. + */ + public static final double[] getSpatialCalibration(final ImgPlusMetadata img) { + final double[] calibration = Util.getArrayFromValue(1d, 3); + + for (int d = 0; d < img.numDimensions(); d++) { + if (img.axis(d).type() == Axes.X) + calibration[0] = img.averageScale(d); + else if (img.axis(d).type() == Axes.Y) + calibration[1] = img.averageScale(d); + else if (img.axis(d).type() == Axes.Z) + calibration[2] = img.averageScale(d); + } + return calibration; + } + +// /** +// * Returns an estimate of the pth percentile of the values in +// * the values array. Taken from commons-math. +// */ +// public static final double getPercentile( final double[] values, final double p ) +// { +// +// final int size = values.length; +// if ( ( p > 1 ) || ( p <= 0 ) ) +// throw new IllegalArgumentException( "invalid quantile value: " + p ); +// // always return single value for n = 1 +// if ( size == 0 ) +// return Double.NaN; +// if ( size == 1 ) +// return values[ 0 ]; +// final double n = size; +// final double pos = p * ( n + 1 ); +// final double fpos = Math.floor( pos ); +// final int intPos = ( int ) fpos; +// final double dif = pos - fpos; +// final double[] sorted = new double[ size ]; +// System.arraycopy( values, 0, sorted, 0, size ); +// Arrays.sort( sorted ); +// +// if ( pos < 1 ) +// return sorted[ 0 ]; +// if ( pos >= n ) +// return sorted[ size - 1 ]; +// final double lower = sorted[ intPos - 1 ]; +// final double upper = sorted[ intPos ]; +// return lower + dif * ( upper - lower ); +// } +// +// /** +// * Returns [range, min, max] of the given double array. +// * +// * @return A double[] of length 3, where index 0 is the range, index 1 is +// * the min, and index 2 is the max. +// */ +// private static final double[] getRange( final double[] data ) +// { +// if ( data.length == 0 ) +// return new double[] { 1., 0., 1. }; +// +// final double min = Arrays.stream( data ).min().getAsDouble(); +// final double max = Arrays.stream( data ).max().getAsDouble(); +// return new double[] { ( max - min ), min, max }; +// } +// +// /** +// * Store the x, y, z coordinates of the specified spot in the first 3 +// * elements of the specified double array. +// */ +// public static final void localize( final Spot spot, final double[] coords ) +// { +// coords[ 0 ] = spot.getFeature( Spot.POSITION_X ).doubleValue(); +// coords[ 1 ] = spot.getFeature( Spot.POSITION_Y ).doubleValue(); +// coords[ 2 ] = spot.getFeature( Spot.POSITION_Z ).doubleValue(); +// } +// +// /** +// * Return the optimal bin number for a histogram of the data given in array, +// * using the Freedman and Diaconis rule (bin_space = 2*IQR/n^(1/3)). It is +// * ensured that the bin number returned is not smaller and no bigger than +// * the bounds given in argument. +// */ +// public static final int getNBins( final double[] values, final int minBinNumber, final int maxBinNumber ) +// { +// final int size = values.length; +// final double q1 = getPercentile( values, 0.25 ); +// final double q3 = getPercentile( values, 0.75 ); +// final double iqr = q3 - q1; +// final double binWidth = 2 * iqr * Math.pow( size, -0.33 ); +// final double[] range = getRange( values ); +// int nBin = ( int ) ( range[ 0 ] / binWidth + 1 ); +// +// if ( nBin > maxBinNumber ) +// nBin = maxBinNumber; +// else if ( nBin < minBinNumber ) +// nBin = minBinNumber; +// +// return nBin; +// } +// +// /** +// * Return the optimal bin number for a histogram of the data given in array, +// * using the Freedman and Diaconis rule (bin_space = 2*IQR/n^(1/3)). It is +// * ensured that the bin number returned is not smaller than 8 and no bigger +// * than 256. +// */ +// private static final int getNBins( final double[] values ) +// { +// return getNBins( values, 8, 256 ); +// } +// +// /** +// * Create a histogram from the data given. +// */ +// private static final int[] histogram( final double data[], final int nBins ) +// { +// final double[] range = getRange( data ); +// final double binWidth = range[ 0 ] / nBins; +// final int[] hist = new int[ nBins ]; +// int index; +// +// if ( nBins > 0 ) +// { +// for ( int i = 0; i < data.length; i++ ) +// { +// index = Math.min( ( int ) Math.floor( ( data[ i ] - range[ 1 ] ) / binWidth ), nBins - 1 ); +// hist[ index ]++; +// } +// } +// return hist; +// } +// +// /** +// * Return a threshold for the given data, using an Otsu histogram +// * thresholding method. +// */ +// public static final double otsuThreshold( final double[] data ) +// { +// return otsuThreshold( data, getNBins( data ) ); +// } +// +// /** +// * Return a threshold for the given data, using an Otsu histogram +// * thresholding method with a given bin number. +// */ +// private static final double otsuThreshold( final double[] data, final int nBins ) +// { +// final int[] hist = histogram( data, nBins ); +// final int thresholdIndex = otsuThresholdIndex( hist, data.length ); +// final double[] range = getRange( data ); +// final double binWidth = range[ 0 ] / nBins; +// return range[ 1 ] + binWidth * thresholdIndex; +// } +// +// /** +// * Given a histogram array hist, built with an initial amount +// * of nPoints data item, this method return the bin index that +// * thresholds the histogram in 2 classes. The threshold is performed using +// * the Otsu Threshold Method. +// * +// * @param hist +// * the histogram array +// * @param nPoints +// * the number of data items this histogram was built on +// * @return the bin index of the histogram that thresholds it +// */ +// private static final int otsuThresholdIndex( final int[] hist, final int nPoints ) +// { +// final int total = nPoints; +// +// double sum = 0; +// for ( int t = 0; t < hist.length; t++ ) +// sum += t * hist[ t ]; +// +// double sumB = 0; +// int wB = 0; +// int wF = 0; +// +// double varMax = 0; +// int threshold = 0; +// +// for ( int t = 0; t < hist.length; t++ ) +// { +// wB += hist[ t ]; // Weight Background +// if ( wB == 0 ) +// continue; +// +// wF = total - wB; // Weight Foreground +// if ( wF == 0 ) +// break; +// +// sumB += ( t * hist[ t ] ); +// +// final double mB = sumB / wB; // Mean Background +// final double mF = ( sum - sumB ) / wF; // Mean Foreground +// +// // Calculate Between Class Variance +// final double varBetween = wB * wF * ( mB - mF ) * ( mB - mF ); +// +// // Check if new maximum found +// if ( varBetween > varMax ) +// { +// varMax = varBetween; +// threshold = t; +// } +// } +// return threshold; +// } + + public static double[] getSpatialCalibration(final ImagePlus imp) { + final double[] calibration = Util.getArrayFromValue(1d, 3); + calibration[0] = imp.getCalibration().pixelWidth; + calibration[1] = imp.getCalibration().pixelHeight; + if (imp.getNSlices() > 1) + calibration[2] = imp.getCalibration().pixelDepth; + + return calibration; + } + + /** + * Return a String unit for the given dimension. When suitable, the unit is + * taken from the settings field, which contains the spatial and time units. + * Otherwise, default units are used. + */ + public static final String getUnitsFor(final Dimension dimension, final String spaceUnits, final String timeUnits) { + switch (dimension) { + case ANGLE: + return "radians"; + case INTENSITY: + return "counts"; + case INTENSITY_SQUARED: + return "counts^2"; + case NONE: + return ""; + case POSITION: + case LENGTH: + return spaceUnits; + case AREA: + return spaceUnits + "^2"; + case QUALITY: + return "quality"; + case COST: + return "cost"; + case TIME: + return timeUnits; + case VELOCITY: + return spaceUnits + "/" + timeUnits; + case RATE: + return "/" + timeUnits; + case ANGLE_RATE: + return "rad/" + timeUnits; + default: + case STRING: + return null; + } + } + + public static final String getCurrentTimeString() { + return DATE_FORMAT.format(new Date()); + } + + public static > ImgPlus hyperSlice(final ImgPlus img, final long channel, final long frame) { + final int timeDim = img.dimensionIndex(Axes.TIME); + final ImgPlus imgT = timeDim < 0 ? img : ImgPlusViews.hyperSlice(img, timeDim, frame); + + final int channelDim = imgT.dimensionIndex(Axes.CHANNEL); + final ImgPlus imgTC = channelDim < 0 ? imgT : ImgPlusViews.hyperSlice(imgT, channelDim, channel); + + // Squeeze Z dimension if its size is 1. + final int zDim = imgTC.dimensionIndex(Axes.Z); + final ImgPlus imgTCZ; + if (zDim >= 0 && imgTC.dimension(zDim) <= 1) + imgTCZ = ImgPlusViews.hyperSlice(imgTC, zDim, imgTC.min(zDim)); + else + imgTCZ = imgTC; + + return imgTCZ; + } + + /** + * Returns an interval object that slices in the specified {@link ImgPlus} + * in a single channel (the channel dimension is dropped). + *

+ * The specified {@link Settings} object is used to determine a crop-cube + * that will determine the X,Y,Z size of the interval. The channel dimension + * will be dropped. + *

+ * If the specified {@link ImgPlus} has a time axis, it will be included, + * using the {@link Settings#tstart} and {@link Settings#tend} as bounds. If + * it is a singleton dimension (1 time-point) it won't be dropped. + * + * @param img the source image into which the interval is to be defined. + * @param settings the settings object that will determine the interval size. + * @return a new interval. + */ + public static final Interval getIntervalWithTime(final ImgPlus img, final Settings settings) { + final long[] max = new long[img.numDimensions()]; + final long[] min = new long[img.numDimensions()]; + + // X, we must have it. + final int xindex = img.dimensionIndex(Axes.X); + min[xindex] = settings.getXstart(); + max[xindex] = settings.getXend(); + + // Y, we must have it. + final int yindex = img.dimensionIndex(Axes.Y); + min[yindex] = settings.getYstart(); + max[yindex] = settings.getYend(); + + // Z, we MIGHT have it. + final int zindex = img.dimensionIndex(Axes.Z); + if (zindex >= 0) { + min[zindex] = settings.zstart; + max[zindex] = settings.zend; + } + + // TIME, we might have it, but anyway we leave the start & end + // management to elsewhere. + final int tindex = img.dimensionIndex(Axes.TIME); + if (tindex >= 0) { + min[tindex] = settings.tstart; + max[tindex] = settings.tend; + } + + // CHANNEL, we might have it, we drop it. + final long[] max2; + final long[] min2; + final int cindex = img.dimensionIndex(Axes.CHANNEL); + if (cindex >= 0) { + max2 = new long[img.numDimensions() - 1]; + min2 = new long[img.numDimensions() - 1]; + int d2 = 0; + for (int d = 0; d < min.length; d++) { + if (d != cindex) { + min2[d2] = Math.max(0L, min[d]); + max2[d2] = Math.min(img.max(d), max[d]); + d2++; + } + } + } else { + min2 = new long[min.length]; + max2 = new long[min.length]; + for (int d = 0; d < min.length; d++) { + min2[d] = Math.max(0L, min[d]); + max2[d] = Math.min(img.max(d), max[d]); + } + } + + final FinalInterval interval = new FinalInterval(min2, max2); + return interval; + } + + /** + * Returns an interval object that in the specified {@link ImgPlus} slice + * in a single time frame. + *

+ * The specified {@link Settings} object is used to determine a crop-cube + * that will determine the X,Y,Z size of the interval. A single channel will + * be taken in the case of a multi-channel image. If the detector set in the + * settings object has a parameter for the target channel + * {@link fiji.plugin.trackmate.detection.DetectorKeys#KEY_TARGET_CHANNEL}, + * it will be used; otherwise the first channel will be taken. + *

+ * If the specified {@link ImgPlus} has a time axis, it will be dropped and + * the returned interval will have one dimension less. + * + * @param img the source image into which the interval is to be defined. + * @param settings the settings object that will determine the interval size. + * @return a new interval. + */ + public static final Interval getInterval(final ImgPlus img, final Settings settings) { + final long[] max = new long[img.numDimensions()]; + final long[] min = new long[img.numDimensions()]; + + // X, we must have it. + final int xindex = img.dimensionIndex(Axes.X); + min[xindex] = settings.getXstart(); + max[xindex] = settings.getXend(); + + // Y, we must have it. + final int yindex = img.dimensionIndex(Axes.Y); + min[yindex] = settings.getYstart(); + max[yindex] = settings.getYend(); + + // Z, we MIGHT have it. + final int zindex = img.dimensionIndex(Axes.Z); + if (zindex >= 0) { + min[zindex] = settings.zstart; + max[zindex] = settings.zend; + } + + // CHANNEL, we might have it. + final int cindex = img.dimensionIndex(Axes.CHANNEL); + if (cindex >= 0) { + Integer c = (Integer) settings.detectorSettings.get(KEY_TARGET_CHANNEL); // 1-based. + if (null == c) + c = 1; + + min[cindex] = c - 1; // 0-based. + max[cindex] = min[cindex]; + } + + // TIME, we might have it, but anyway we leave the start & end + // management to elsewhere. + final int tindex = img.dimensionIndex(Axes.TIME); + + /* + * We want to exclude time (if we have it) from out interval and source, + * so that we can provide the detector instance with a hyperslice that + * does NOT have time as a dimension. + */ + final long[] intervalMin; + final long[] intervalMax; + if (tindex >= 0) { + intervalMin = new long[min.length - 1]; + intervalMax = new long[min.length - 1]; + int nindex = -1; + for (int d = 0; d < min.length; d++) { + if (d == tindex) + continue; + + nindex++; + intervalMin[nindex] = Math.max(0L, min[d]); + intervalMax[nindex] = Math.min(img.max(d), max[d]); + } + } else { + intervalMin = new long[min.length]; + intervalMax = new long[min.length]; + for (int d = 0; d < min.length; d++) { + intervalMin[d] = Math.max(0L, min[d]); + intervalMax[d] = Math.min(img.max(d), max[d]); + } + } + final FinalInterval interval = new FinalInterval(intervalMin, intervalMax); + return interval; + } + + /** + * Obtains the SciJava {@link Context} in use by ImageJ. + */ + public static Context getContext() { + final Context localContext = context; + if (localContext != null) + return localContext; + + synchronized (TMUtils.class) { + if (context == null) + context = (Context) IJ.runPlugIn("org.scijava.Context", ""); + return context; + } + } + + /** + * Creates a default file path to save the TrackMate session to, based on + * the image TrackMate works on. + * + * @param settings the settings object from which to read the image, its folder, + * etc. + * @param logger a logger instance in which to echo problems if any. + * @return a new file. + */ + public static File proposeTrackMateSaveFile(final Settings settings, final Logger logger) { + File folder; + if (null != settings.imp && null != settings.imp.getOriginalFileInfo() && null != settings.imp.getOriginalFileInfo().directory) { + final String directory = settings.imp.getOriginalFileInfo().directory; + folder = Paths.get(directory).toAbsolutePath().toFile(); + /* + * Update the settings field with the image file location now, + * because it's valid. + */ + settings.imageFolder = settings.imp.getOriginalFileInfo().directory; + } else if (!settings.imageFolder.isEmpty()) { + final String absolutePath = FileSystems.getDefault().getPath(settings.imageFolder).normalize().toAbsolutePath().toString(); + folder = new File(absolutePath); + } else { + folder = new File(System.getProperty("user.dir")); + /* + * Warn the user that the file cannot be reloaded properly because + * the source image does not match a file. + */ + logger.error("Warning: The source image does not match a file on the system." + "TrackMate won't be able to reload it when opening this XML file.\n" + "To fix this, save the source image to a TIF file before saving the TrackMate session.\n"); + settings.imageFolder = ""; + } + + File file; + try { + file = new File(folder.getPath(), settings.imp.getShortTitle() + ".xml"); + } catch (final NullPointerException npe) { + if (settings.imageFileName.isEmpty()) + file = new File(folder, "TrackMateData.xml"); + else { + final String imName = settings.imageFileName; + final int i = imName.lastIndexOf('.'); + String xmlName; + if (i < 0) + xmlName = imName + ".xml"; + else + xmlName = imName.substring(0, i) + ".xml"; + file = new File(folder, xmlName); + } + } + return file; + } + + public static final double variance(final double[] data) { + final double mean = Util.average(data); + double variance = 0; + for (int i = 0; i < data.length; i++) { + final double dx = data[i] - mean; + variance += dx * dx; + } + variance /= (data.length - 1); + return variance; + } + + public static final double standardDeviation(final double[] data) { + return Math.sqrt(variance(data)); + } + + public static double sum(final double[] data) { + return Arrays.stream(data).sum(); + } + + public static double average(final DoubleArray data) { + return sum(data) / data.size(); + } + + public static double sum(final DoubleArray data) { + double sum = 0.; + for (int i = 0; i < data.size(); i++) + sum += data.getArray()[i]; + return sum; + } + + public static final double variance(final DoubleArray data) { + final double mean = average(data); + double variance = 0; + for (int i = 0; i < data.size(); i++) { + final double dx = data.getArray()[i] - mean; + variance += dx * dx; + } + variance /= (data.size() - 1); + return variance; + } + + public static double standardDeviation(final DoubleArray data) { + return Math.sqrt(variance(data)); + } + + /** + * Returns a string of the name of the image without the extension, with the + * full path + * + * @return full name of the image without the extension + */ + public static String getImagePathWithoutExtension(final Settings settings) { + final String imageFolder = (settings.imageFolder == null) + ? System.getProperty("user.home") + : settings.imageFolder; + + final String imageFileName = settings.imageFileName; + if (imageFileName != null) { + final int lastIndexOf = imageFileName.lastIndexOf("."); + if (lastIndexOf > 0) + return imageFolder + imageFileName.substring(0, imageFileName.lastIndexOf(".")); + return imageFolder + imageFileName; + } else { + return imageFolder + File.separator + "TrackMate"; + } + } } From 21ed6502eea5374cc57c72ac9f2d0cab7ad92536 Mon Sep 17 00:00:00 2001 From: Divyank Shah Date: Tue, 21 Nov 2023 15:57:53 -0400 Subject: [PATCH 6/7] Removed commented code from previous commit code. --- .../fiji/plugin/trackmate/util/TMUtils.java | 193 ------------------ 1 file changed, 193 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/util/TMUtils.java b/src/main/java/fiji/plugin/trackmate/util/TMUtils.java index 121531c79..217268385 100644 --- a/src/main/java/fiji/plugin/trackmate/util/TMUtils.java +++ b/src/main/java/fiji/plugin/trackmate/util/TMUtils.java @@ -223,199 +223,6 @@ else if (img.axis(d).type() == Axes.Z) return calibration; } -// /** -// * Returns an estimate of the pth percentile of the values in -// * the values array. Taken from commons-math. -// */ -// public static final double getPercentile( final double[] values, final double p ) -// { -// -// final int size = values.length; -// if ( ( p > 1 ) || ( p <= 0 ) ) -// throw new IllegalArgumentException( "invalid quantile value: " + p ); -// // always return single value for n = 1 -// if ( size == 0 ) -// return Double.NaN; -// if ( size == 1 ) -// return values[ 0 ]; -// final double n = size; -// final double pos = p * ( n + 1 ); -// final double fpos = Math.floor( pos ); -// final int intPos = ( int ) fpos; -// final double dif = pos - fpos; -// final double[] sorted = new double[ size ]; -// System.arraycopy( values, 0, sorted, 0, size ); -// Arrays.sort( sorted ); -// -// if ( pos < 1 ) -// return sorted[ 0 ]; -// if ( pos >= n ) -// return sorted[ size - 1 ]; -// final double lower = sorted[ intPos - 1 ]; -// final double upper = sorted[ intPos ]; -// return lower + dif * ( upper - lower ); -// } -// -// /** -// * Returns [range, min, max] of the given double array. -// * -// * @return A double[] of length 3, where index 0 is the range, index 1 is -// * the min, and index 2 is the max. -// */ -// private static final double[] getRange( final double[] data ) -// { -// if ( data.length == 0 ) -// return new double[] { 1., 0., 1. }; -// -// final double min = Arrays.stream( data ).min().getAsDouble(); -// final double max = Arrays.stream( data ).max().getAsDouble(); -// return new double[] { ( max - min ), min, max }; -// } -// -// /** -// * Store the x, y, z coordinates of the specified spot in the first 3 -// * elements of the specified double array. -// */ -// public static final void localize( final Spot spot, final double[] coords ) -// { -// coords[ 0 ] = spot.getFeature( Spot.POSITION_X ).doubleValue(); -// coords[ 1 ] = spot.getFeature( Spot.POSITION_Y ).doubleValue(); -// coords[ 2 ] = spot.getFeature( Spot.POSITION_Z ).doubleValue(); -// } -// -// /** -// * Return the optimal bin number for a histogram of the data given in array, -// * using the Freedman and Diaconis rule (bin_space = 2*IQR/n^(1/3)). It is -// * ensured that the bin number returned is not smaller and no bigger than -// * the bounds given in argument. -// */ -// public static final int getNBins( final double[] values, final int minBinNumber, final int maxBinNumber ) -// { -// final int size = values.length; -// final double q1 = getPercentile( values, 0.25 ); -// final double q3 = getPercentile( values, 0.75 ); -// final double iqr = q3 - q1; -// final double binWidth = 2 * iqr * Math.pow( size, -0.33 ); -// final double[] range = getRange( values ); -// int nBin = ( int ) ( range[ 0 ] / binWidth + 1 ); -// -// if ( nBin > maxBinNumber ) -// nBin = maxBinNumber; -// else if ( nBin < minBinNumber ) -// nBin = minBinNumber; -// -// return nBin; -// } -// -// /** -// * Return the optimal bin number for a histogram of the data given in array, -// * using the Freedman and Diaconis rule (bin_space = 2*IQR/n^(1/3)). It is -// * ensured that the bin number returned is not smaller than 8 and no bigger -// * than 256. -// */ -// private static final int getNBins( final double[] values ) -// { -// return getNBins( values, 8, 256 ); -// } -// -// /** -// * Create a histogram from the data given. -// */ -// private static final int[] histogram( final double data[], final int nBins ) -// { -// final double[] range = getRange( data ); -// final double binWidth = range[ 0 ] / nBins; -// final int[] hist = new int[ nBins ]; -// int index; -// -// if ( nBins > 0 ) -// { -// for ( int i = 0; i < data.length; i++ ) -// { -// index = Math.min( ( int ) Math.floor( ( data[ i ] - range[ 1 ] ) / binWidth ), nBins - 1 ); -// hist[ index ]++; -// } -// } -// return hist; -// } -// -// /** -// * Return a threshold for the given data, using an Otsu histogram -// * thresholding method. -// */ -// public static final double otsuThreshold( final double[] data ) -// { -// return otsuThreshold( data, getNBins( data ) ); -// } -// -// /** -// * Return a threshold for the given data, using an Otsu histogram -// * thresholding method with a given bin number. -// */ -// private static final double otsuThreshold( final double[] data, final int nBins ) -// { -// final int[] hist = histogram( data, nBins ); -// final int thresholdIndex = otsuThresholdIndex( hist, data.length ); -// final double[] range = getRange( data ); -// final double binWidth = range[ 0 ] / nBins; -// return range[ 1 ] + binWidth * thresholdIndex; -// } -// -// /** -// * Given a histogram array hist, built with an initial amount -// * of nPoints data item, this method return the bin index that -// * thresholds the histogram in 2 classes. The threshold is performed using -// * the Otsu Threshold Method. -// * -// * @param hist -// * the histogram array -// * @param nPoints -// * the number of data items this histogram was built on -// * @return the bin index of the histogram that thresholds it -// */ -// private static final int otsuThresholdIndex( final int[] hist, final int nPoints ) -// { -// final int total = nPoints; -// -// double sum = 0; -// for ( int t = 0; t < hist.length; t++ ) -// sum += t * hist[ t ]; -// -// double sumB = 0; -// int wB = 0; -// int wF = 0; -// -// double varMax = 0; -// int threshold = 0; -// -// for ( int t = 0; t < hist.length; t++ ) -// { -// wB += hist[ t ]; // Weight Background -// if ( wB == 0 ) -// continue; -// -// wF = total - wB; // Weight Foreground -// if ( wF == 0 ) -// break; -// -// sumB += ( t * hist[ t ] ); -// -// final double mB = sumB / wB; // Mean Background -// final double mF = ( sum - sumB ) / wF; // Mean Foreground -// -// // Calculate Between Class Variance -// final double varBetween = wB * wF * ( mB - mF ) * ( mB - mF ); -// -// // Check if new maximum found -// if ( varBetween > varMax ) -// { -// varMax = varBetween; -// threshold = t; -// } -// } -// return threshold; -// } - public static double[] getSpatialCalibration(final ImagePlus imp) { final double[] calibration = Util.getArrayFromValue(1d, 3); calibration[0] = imp.getCalibration().pixelWidth; From 870efd13dfec91dac08193342090b3cb50f692a4 Mon Sep 17 00:00:00 2001 From: Divyank Shah Date: Sat, 25 Nov 2023 20:28:11 -0400 Subject: [PATCH 7/7] This commit introduces a refactoring to replace a switch statement with a polymorphic approach, enhancing the maintainability and extensibility of the code. By using polymorphism, the code becomes more modular, and the responsibilities of each case in the switch statement are encapsulated within separate classes or methods. --- .../plugin/trackmate/features/Defaults.java | 28 + .../fiji/plugin/trackmate/features/Edges.java | 34 + .../trackmate/features/FeatureUtils.java | 703 +++++------ .../fiji/plugin/trackmate/features/Spots.java | 38 + .../plugin/trackmate/features/Tracks.java | 35 + .../components/FeatureDisplaySelector.java | 1090 ++++++++--------- .../gui/components/FilterGuiPanel.java | 776 ++++++------ .../gui/components/GrapherPanel.java | 519 ++++---- .../wizard/descriptors/GrapherDescriptor.java | 70 +- 9 files changed, 1590 insertions(+), 1703 deletions(-) create mode 100644 src/main/java/fiji/plugin/trackmate/features/Defaults.java create mode 100644 src/main/java/fiji/plugin/trackmate/features/Edges.java create mode 100644 src/main/java/fiji/plugin/trackmate/features/Spots.java create mode 100644 src/main/java/fiji/plugin/trackmate/features/Tracks.java diff --git a/src/main/java/fiji/plugin/trackmate/features/Defaults.java b/src/main/java/fiji/plugin/trackmate/features/Defaults.java new file mode 100644 index 000000000..6a4b9f32e --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/features/Defaults.java @@ -0,0 +1,28 @@ +package fiji.plugin.trackmate.features; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; + +import java.util.*; + +public class Defaults extends FeatureUtils { + private static final String USE_UNIFORM_COLOR_NAME = "Uniform color"; + private static final String USE_RANDOM_COLOR_NAME = "Random color"; + + public Map collectFeatureKeys(DisplaySettings.TrackMateObject target, Model model, Settings settings) { + final Map inverseMap = new HashMap<>(); + inverseMap.put(USE_UNIFORM_COLOR_NAME, USE_UNIFORM_COLOR_KEY); + inverseMap.put(USE_RANDOM_COLOR_NAME, USE_RANDOM_COLOR_KEY); + + // Sort by feature name. + final List featureNameList = new ArrayList<>(inverseMap.keySet()); + featureNameList.sort(null); + + final Map featureNames = new LinkedHashMap<>(featureNameList.size()); + for (final String featureName : featureNameList) + featureNames.put(inverseMap.get(featureName), featureName); + + return featureNames; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/features/Edges.java b/src/main/java/fiji/plugin/trackmate/features/Edges.java new file mode 100644 index 000000000..2dfd68a03 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/features/Edges.java @@ -0,0 +1,34 @@ +package fiji.plugin.trackmate.features; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.features.edges.EdgeAnalyzer; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; + +import java.util.*; + +public class Edges extends FeatureUtils { + + + public Map collectFeatureKeys(DisplaySettings.TrackMateObject target, Model model, Settings settings) { + final Map inverseMap = new HashMap<>(); + if (model != null) { + for (final String featureKey : model.getFeatureModel().getEdgeFeatureNames().keySet()) + inverseMap.put(model.getFeatureModel().getEdgeFeatureNames().get(featureKey), featureKey); + } + if (settings != null) { + for (final EdgeAnalyzer ea : settings.getEdgeAnalyzers()) + for (final String featureKey : ea.getFeatureNames().keySet()) + inverseMap.put(ea.getFeatureNames().get(featureKey), featureKey); + } + // Sort by feature name. + final List featureNameList = new ArrayList<>(inverseMap.keySet()); + featureNameList.sort(null); + + final Map featureNames = new LinkedHashMap<>(featureNameList.size()); + for (final String featureName : featureNameList) + featureNames.put(inverseMap.get(featureName), featureName); + + return featureNames; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/features/FeatureUtils.java b/src/main/java/fiji/plugin/trackmate/features/FeatureUtils.java index 30243a53d..10cbef2e7 100644 --- a/src/main/java/fiji/plugin/trackmate/features/FeatureUtils.java +++ b/src/main/java/fiji/plugin/trackmate/features/FeatureUtils.java @@ -8,12 +8,12 @@ * 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 * . @@ -21,432 +21,293 @@ */ package fiji.plugin.trackmate.features; -import java.awt.Color; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; - -import org.jgrapht.graph.DefaultWeightedEdge; -import org.scijava.util.DoubleArray; - import fiji.plugin.trackmate.FeatureModel; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.features.edges.EdgeAnalyzer; import fiji.plugin.trackmate.features.manual.ManualEdgeColorAnalyzer; import fiji.plugin.trackmate.features.manual.ManualSpotColorAnalyzerFactory; -import fiji.plugin.trackmate.features.spot.SpotAnalyzerFactoryBase; -import fiji.plugin.trackmate.features.track.TrackAnalyzer; import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; -import fiji.plugin.trackmate.visualization.FeatureColorGenerator; -import fiji.plugin.trackmate.visualization.ManualEdgeColorGenerator; -import fiji.plugin.trackmate.visualization.ManualEdgePerSpotColorGenerator; -import fiji.plugin.trackmate.visualization.ManualSpotColorGenerator; -import fiji.plugin.trackmate.visualization.ManualSpotPerEdgeColorGenerator; -import fiji.plugin.trackmate.visualization.PerEdgeFeatureColorGenerator; -import fiji.plugin.trackmate.visualization.PerSpotFeatureColorGenerator; -import fiji.plugin.trackmate.visualization.PerTrackFeatureColorGenerator; -import fiji.plugin.trackmate.visualization.RandomSpotColorGenerator; -import fiji.plugin.trackmate.visualization.SpotColorGenerator; -import fiji.plugin.trackmate.visualization.SpotColorGeneratorPerEdgeFeature; -import fiji.plugin.trackmate.visualization.SpotColorGeneratorPerTrackFeature; -import fiji.plugin.trackmate.visualization.UniformSpotColorGenerator; -import fiji.plugin.trackmate.visualization.UniformTrackColorGenerator; -import fiji.plugin.trackmate.visualization.WholeTrackFeatureColorGenerator; - -public class FeatureUtils -{ - - private static final String USE_UNIFORM_COLOR_NAME = "Uniform color"; - - public static final String USE_UNIFORM_COLOR_KEY = "UNIFORM_COLOR"; - - private static final String USE_RANDOM_COLOR_NAME = "Random color"; - - public static final String USE_RANDOM_COLOR_KEY = "RANDOM_COLOR"; - - public static final Map< String, String > collectFeatureKeys( final TrackMateObject target, final Model model, final Settings settings ) - { - final Map< String, String > inverseMap = new HashMap<>(); - // will be used to sort. - - switch ( target ) - { - case SPOTS: - { - - // Collect all. - if ( model != null ) - { - for ( final String featureKey : model.getFeatureModel().getSpotFeatureNames().keySet() ) - inverseMap.put( model.getFeatureModel().getSpotFeatureNames().get( featureKey ), featureKey ); - } - else - { - // If we have no model, we still want to add spot features. - for ( final String featureKey : Spot.FEATURE_NAMES.keySet() ) - inverseMap.put( Spot.FEATURE_NAMES.get( featureKey ), featureKey ); - } - if ( settings != null ) - { - for ( final SpotAnalyzerFactoryBase< ? > sf : settings.getSpotAnalyzerFactories() ) - for ( final String featureKey : sf.getFeatureNames().keySet() ) - inverseMap.put( sf.getFeatureNames().get( featureKey ), featureKey ); - } - break; - } - - case EDGES: - { - if ( model != null ) - { - for ( final String featureKey : model.getFeatureModel().getEdgeFeatureNames().keySet() ) - inverseMap.put( model.getFeatureModel().getEdgeFeatureNames().get( featureKey ), featureKey ); - } - if ( settings != null ) - { - for ( final EdgeAnalyzer ea : settings.getEdgeAnalyzers() ) - for ( final String featureKey : ea.getFeatureNames().keySet() ) - inverseMap.put( ea.getFeatureNames().get( featureKey ), featureKey ); - } - break; - } - - case TRACKS: - { - if ( model != null ) - { - for ( final String featureKey : model.getFeatureModel().getTrackFeatureNames().keySet() ) - inverseMap.put( model.getFeatureModel().getTrackFeatureNames().get( featureKey ), featureKey ); - } - if ( settings != null ) - { - for ( final TrackAnalyzer ta : settings.getTrackAnalyzers() ) - for ( final String featureKey : ta.getFeatureNames().keySet() ) - inverseMap.put( ta.getFeatureNames().get( featureKey ), featureKey ); - } - break; - } - - case DEFAULT: - { - inverseMap.put( USE_UNIFORM_COLOR_NAME, USE_UNIFORM_COLOR_KEY ); - inverseMap.put( USE_RANDOM_COLOR_NAME, USE_RANDOM_COLOR_KEY ); - break; - } - - default: - throw new IllegalArgumentException( "Unknown object type: " + target ); - } - - // Sort by feature name. - final List< String > featureNameList = new ArrayList<>( inverseMap.keySet() ); - featureNameList.sort( null ); - - final Map< String, String > featureNames = new LinkedHashMap<>( featureNameList.size() ); - for ( final String featureName : featureNameList ) - featureNames.put( inverseMap.get( featureName ), featureName ); - - return featureNames; - } - - /** - * Missing or undefined values are not included. - * - * @param featureKey - * @param target - * @param model - * @param visibleOnly - * @return a new double[] array containing the numerical - * feature values. - */ - public static double[] collectFeatureValues( - final String featureKey, - final TrackMateObject target, - final Model model, - final boolean visibleOnly ) - { - final FeatureModel fm = model.getFeatureModel(); - switch ( target ) - { - case DEFAULT: - return new double[] {}; - - case EDGES: - { - final DoubleArray val = new DoubleArray(); - for ( final Integer trackID : model.getTrackModel().trackIDs( visibleOnly ) ) - { - for ( final DefaultWeightedEdge edge : model.getTrackModel().trackEdges( trackID ) ) - { - final Double ef = fm.getEdgeFeature( edge, featureKey ); - if ( ef != null && !ef.isNaN() ) - val.add( ef.doubleValue() ); - } - } - return val.copyArray(); - } - case SPOTS: - { - - final DoubleArray val = new DoubleArray(); - for ( final Spot spot : model.getSpots().iterable( visibleOnly ) ) - { - final Double sf = spot.getFeature( featureKey ); - if ( sf != null && !sf.isNaN() ) - val.add( sf.doubleValue() ); - } - return val.copyArray(); - } - case TRACKS: - { - final DoubleArray val = new DoubleArray(); - for ( final Integer trackID : model.getTrackModel().trackIDs( visibleOnly ) ) - { - final Double tf = fm.getTrackFeature( trackID, featureKey ); - if ( tf != null && !tf.isNaN() ) - val.add( tf.doubleValue() ); - } - return val.copyArray(); - } - default: - throw new IllegalArgumentException( "Unknown object type: " + target ); - } - } - - public static final FeatureColorGenerator< Spot > createSpotColorGenerator( final Model model, final DisplaySettings displaySettings ) - { - switch ( displaySettings.getSpotColorByType() ) - { - case DEFAULT: - switch ( displaySettings.getSpotColorByFeature() ) - { - case FeatureUtils.USE_RANDOM_COLOR_KEY: - return new RandomSpotColorGenerator(); - default: - case FeatureUtils.USE_UNIFORM_COLOR_KEY: - return new UniformSpotColorGenerator( displaySettings.getSpotUniformColor() ); - } - - case EDGES: - - if ( displaySettings.getSpotColorByFeature().equals( ManualEdgeColorAnalyzer.FEATURE ) ) - return new ManualSpotPerEdgeColorGenerator( model, displaySettings.getMissingValueColor() ); - - return new SpotColorGeneratorPerEdgeFeature( - model, - displaySettings.getSpotColorByFeature(), - displaySettings.getMissingValueColor(), - displaySettings.getUndefinedValueColor(), - displaySettings.getColormap(), - displaySettings.getSpotMin(), - displaySettings.getSpotMax() ); - - case SPOTS: - - if ( displaySettings.getSpotColorByFeature().equals( ManualSpotColorAnalyzerFactory.FEATURE ) ) - return new ManualSpotColorGenerator( displaySettings.getMissingValueColor() ); - - return new SpotColorGenerator( - displaySettings.getSpotColorByFeature(), - displaySettings.getMissingValueColor(), - displaySettings.getUndefinedValueColor(), - displaySettings.getColormap(), - displaySettings.getSpotMin(), - displaySettings.getSpotMax() ); - - case TRACKS: - return new SpotColorGeneratorPerTrackFeature( - model, - displaySettings.getSpotColorByFeature(), - displaySettings.getMissingValueColor(), - displaySettings.getUndefinedValueColor(), - displaySettings.getColormap(), - displaySettings.getSpotMin(), - displaySettings.getSpotMax() ); - - default: - throw new IllegalArgumentException( "Unknown type: " + displaySettings.getSpotColorByType() ); - } - } - - public static final FeatureColorGenerator< DefaultWeightedEdge > createTrackColorGenerator( final Model model, final DisplaySettings displaySettings ) - { - switch ( displaySettings.getTrackColorByType() ) - { - case DEFAULT: - switch ( displaySettings.getTrackColorByFeature() ) - { - case FeatureUtils.USE_RANDOM_COLOR_KEY: - return new PerTrackFeatureColorGenerator( - model, - TrackIndexAnalyzer.TRACK_INDEX, - displaySettings.getMissingValueColor(), - displaySettings.getUndefinedValueColor(), - displaySettings.getColormap(), - displaySettings.getTrackMin(), - displaySettings.getTrackMax() ); - default: - case FeatureUtils.USE_UNIFORM_COLOR_KEY: - return new UniformTrackColorGenerator( displaySettings.getTrackUniformColor() ); - } - - case EDGES: - - if ( displaySettings.getTrackColorByFeature().equals( ManualEdgeColorAnalyzer.FEATURE ) ) - return new ManualEdgeColorGenerator( model, displaySettings.getMissingValueColor() ); - - return new PerEdgeFeatureColorGenerator( - model, - displaySettings.getTrackColorByFeature(), - displaySettings.getMissingValueColor(), - displaySettings.getUndefinedValueColor(), - displaySettings.getColormap(), - displaySettings.getTrackMin(), - displaySettings.getTrackMax() ); - - case SPOTS: - - if ( displaySettings.getTrackColorByFeature().equals( ManualSpotColorAnalyzerFactory.FEATURE ) ) - return new ManualEdgePerSpotColorGenerator( model, displaySettings.getMissingValueColor() ); - - return new PerSpotFeatureColorGenerator( - model, - displaySettings.getTrackColorByFeature(), - displaySettings.getMissingValueColor(), - displaySettings.getUndefinedValueColor(), - displaySettings.getColormap(), - displaySettings.getTrackMin(), - displaySettings.getTrackMax() ); - - case TRACKS: - return new PerTrackFeatureColorGenerator( - model, - displaySettings.getTrackColorByFeature(), - displaySettings.getMissingValueColor(), - displaySettings.getUndefinedValueColor(), - displaySettings.getColormap(), - displaySettings.getTrackMin(), - displaySettings.getTrackMax() ); - - default: - throw new IllegalArgumentException( "Unknown type: " + displaySettings.getTrackColorByType() ); - } - } - - public static final FeatureColorGenerator< Integer > createWholeTrackColorGenerator( final Model model, final DisplaySettings displaySettings ) - { - switch ( displaySettings.getTrackColorByType() ) - { - case DEFAULT: - case SPOTS: - return id -> Color.WHITE; - - case EDGES: - case TRACKS: - return new WholeTrackFeatureColorGenerator( - model, - displaySettings.getTrackColorByFeature(), - displaySettings.getMissingValueColor(), - displaySettings.getUndefinedValueColor(), - displaySettings.getColormap(), - displaySettings.getTrackMin(), - displaySettings.getTrackMax() ); - - default: - throw new IllegalArgumentException( "Unknown type: " + displaySettings.getTrackColorByType() ); - } - } - - public static final Model DUMMY_MODEL = new Model(); - static - { - final Random ran = new Random(); - DUMMY_MODEL.beginUpdate(); - try - { - - for ( int i = 0; i < 100; i++ ) - { - Spot previous = null; - for ( int t = 0; t < 20; t++ ) - { - - final double x = ran.nextDouble(); - final double y = ran.nextDouble(); - final double z = ran.nextDouble(); - final double r = ran.nextDouble(); - final double q = ran.nextDouble(); - final Spot spot = new Spot( x, y, z, r, q ); - DUMMY_MODEL.addSpotTo( spot, t ); - if ( previous != null ) - DUMMY_MODEL.addEdge( previous, spot, ran.nextDouble() ); - - previous = spot; - } - } - } - finally - { - DUMMY_MODEL.endUpdate(); - } - } - - public static final double[] autoMinMax( final Model model, final TrackMateObject type, final String feature ) - { - switch ( type ) - { - case DEFAULT: - return new double[] { 0., 0. }; - - case EDGES: - case SPOTS: - case TRACKS: - { - final double[] values = collectFeatureValues( feature, type, model, true ); - double min = Double.POSITIVE_INFINITY; - double max = Double.NEGATIVE_INFINITY; - for ( final double val : values ) - { - if ( val < min ) - min = val; - - if ( val > max ) - max = val; - } - return new double[] { min, max }; - } - - default: - throw new IllegalArgumentException( "Unexpected TrackMate object type: " + type ); - } - } - - public static final int nObjects( final Model model, final TrackMateObject target, final boolean visibleOnly ) - { - switch ( target ) - { - case DEFAULT: - throw new UnsupportedOperationException( "Cannot return the number of objects for type DEFAULT." ); - case EDGES: - { - int nEdges = 0; - for ( final Integer trackID : model.getTrackModel().unsortedTrackIDs( visibleOnly ) ) - nEdges += model.getTrackModel().trackEdges( trackID ).size(); - return nEdges; - } - case SPOTS: - return model.getSpots().getNSpots( visibleOnly ); - case TRACKS: - return model.getTrackModel().nTracks( visibleOnly ); - default: - throw new IllegalArgumentException( "Unknown TrackMate object: " + target ); - } - } +import fiji.plugin.trackmate.visualization.*; +import org.jgrapht.graph.DefaultWeightedEdge; +import org.scijava.util.DoubleArray; + +import java.awt.*; +import java.util.Map; +import java.util.Random; + +public abstract class FeatureUtils { + + public static final String USE_UNIFORM_COLOR_KEY = "UNIFORM_COLOR"; + public static final String USE_RANDOM_COLOR_KEY = "RANDOM_COLOR"; + public static final Model DUMMY_MODEL = new Model(); + private static final String USE_UNIFORM_COLOR_NAME = "Uniform color"; + private static final String USE_RANDOM_COLOR_NAME = "Random color"; + + static { + final Random ran = new Random(); + DUMMY_MODEL.beginUpdate(); + try { + + for (int i = 0; i < 100; i++) { + Spot previous = null; + for (int t = 0; t < 20; t++) { + + final double x = ran.nextDouble(); + final double y = ran.nextDouble(); + final double z = ran.nextDouble(); + final double r = ran.nextDouble(); + final double q = ran.nextDouble(); + final Spot spot = new Spot(x, y, z, r, q); + DUMMY_MODEL.addSpotTo(spot, t); + if (previous != null) + DUMMY_MODEL.addEdge(previous, spot, ran.nextDouble()); + + previous = spot; + } + } + } finally { + DUMMY_MODEL.endUpdate(); + } + } + + /** + * Missing or undefined values are not included. + * + * @param featureKey + * @param target + * @param model + * @param visibleOnly + * @return a new double[] array containing the numerical + * feature values. + */ + public static double[] collectFeatureValues( + final String featureKey, + final TrackMateObject target, + final Model model, + final boolean visibleOnly) { + final FeatureModel fm = model.getFeatureModel(); + switch (target) { + case DEFAULT: + return new double[]{}; + + case EDGES: { + final DoubleArray val = new DoubleArray(); + for (final Integer trackID : model.getTrackModel().trackIDs(visibleOnly)) { + for (final DefaultWeightedEdge edge : model.getTrackModel().trackEdges(trackID)) { + final Double ef = fm.getEdgeFeature(edge, featureKey); + if (ef != null && !ef.isNaN()) + val.add(ef.doubleValue()); + } + } + return val.copyArray(); + } + case SPOTS: { + + final DoubleArray val = new DoubleArray(); + for (final Spot spot : model.getSpots().iterable(visibleOnly)) { + final Double sf = spot.getFeature(featureKey); + if (sf != null && !sf.isNaN()) + val.add(sf.doubleValue()); + } + return val.copyArray(); + } + case TRACKS: { + final DoubleArray val = new DoubleArray(); + for (final Integer trackID : model.getTrackModel().trackIDs(visibleOnly)) { + final Double tf = fm.getTrackFeature(trackID, featureKey); + if (tf != null && !tf.isNaN()) + val.add(tf.doubleValue()); + } + return val.copyArray(); + } + default: + throw new IllegalArgumentException("Unknown object type: " + target); + } + } + + public static final FeatureColorGenerator createSpotColorGenerator(final Model model, final DisplaySettings displaySettings) { + switch (displaySettings.getSpotColorByType()) { + case DEFAULT: + switch (displaySettings.getSpotColorByFeature()) { + case FeatureUtils.USE_RANDOM_COLOR_KEY: + return new RandomSpotColorGenerator(); + default: + case FeatureUtils.USE_UNIFORM_COLOR_KEY: + return new UniformSpotColorGenerator(displaySettings.getSpotUniformColor()); + } + + case EDGES: + + if (displaySettings.getSpotColorByFeature().equals(ManualEdgeColorAnalyzer.FEATURE)) + return new ManualSpotPerEdgeColorGenerator(model, displaySettings.getMissingValueColor()); + + return new SpotColorGeneratorPerEdgeFeature( + model, + displaySettings.getSpotColorByFeature(), + displaySettings.getMissingValueColor(), + displaySettings.getUndefinedValueColor(), + displaySettings.getColormap(), + displaySettings.getSpotMin(), + displaySettings.getSpotMax()); + + case SPOTS: + + if (displaySettings.getSpotColorByFeature().equals(ManualSpotColorAnalyzerFactory.FEATURE)) + return new ManualSpotColorGenerator(displaySettings.getMissingValueColor()); + + return new SpotColorGenerator( + displaySettings.getSpotColorByFeature(), + displaySettings.getMissingValueColor(), + displaySettings.getUndefinedValueColor(), + displaySettings.getColormap(), + displaySettings.getSpotMin(), + displaySettings.getSpotMax()); + + case TRACKS: + return new SpotColorGeneratorPerTrackFeature( + model, + displaySettings.getSpotColorByFeature(), + displaySettings.getMissingValueColor(), + displaySettings.getUndefinedValueColor(), + displaySettings.getColormap(), + displaySettings.getSpotMin(), + displaySettings.getSpotMax()); + + default: + throw new IllegalArgumentException("Unknown type: " + displaySettings.getSpotColorByType()); + } + } + + public static final FeatureColorGenerator createTrackColorGenerator(final Model model, final DisplaySettings displaySettings) { + switch (displaySettings.getTrackColorByType()) { + case DEFAULT: + switch (displaySettings.getTrackColorByFeature()) { + case FeatureUtils.USE_RANDOM_COLOR_KEY: + return new PerTrackFeatureColorGenerator( + model, + TrackIndexAnalyzer.TRACK_INDEX, + displaySettings.getMissingValueColor(), + displaySettings.getUndefinedValueColor(), + displaySettings.getColormap(), + displaySettings.getTrackMin(), + displaySettings.getTrackMax()); + default: + case FeatureUtils.USE_UNIFORM_COLOR_KEY: + return new UniformTrackColorGenerator(displaySettings.getTrackUniformColor()); + } + + case EDGES: + + if (displaySettings.getTrackColorByFeature().equals(ManualEdgeColorAnalyzer.FEATURE)) + return new ManualEdgeColorGenerator(model, displaySettings.getMissingValueColor()); + + return new PerEdgeFeatureColorGenerator( + model, + displaySettings.getTrackColorByFeature(), + displaySettings.getMissingValueColor(), + displaySettings.getUndefinedValueColor(), + displaySettings.getColormap(), + displaySettings.getTrackMin(), + displaySettings.getTrackMax()); + + case SPOTS: + + if (displaySettings.getTrackColorByFeature().equals(ManualSpotColorAnalyzerFactory.FEATURE)) + return new ManualEdgePerSpotColorGenerator(model, displaySettings.getMissingValueColor()); + + return new PerSpotFeatureColorGenerator( + model, + displaySettings.getTrackColorByFeature(), + displaySettings.getMissingValueColor(), + displaySettings.getUndefinedValueColor(), + displaySettings.getColormap(), + displaySettings.getTrackMin(), + displaySettings.getTrackMax()); + + case TRACKS: + return new PerTrackFeatureColorGenerator( + model, + displaySettings.getTrackColorByFeature(), + displaySettings.getMissingValueColor(), + displaySettings.getUndefinedValueColor(), + displaySettings.getColormap(), + displaySettings.getTrackMin(), + displaySettings.getTrackMax()); + + default: + throw new IllegalArgumentException("Unknown type: " + displaySettings.getTrackColorByType()); + } + } + + public static final FeatureColorGenerator createWholeTrackColorGenerator(final Model model, final DisplaySettings displaySettings) { + switch (displaySettings.getTrackColorByType()) { + case DEFAULT: + case SPOTS: + return id -> Color.WHITE; + + case EDGES: + case TRACKS: + return new WholeTrackFeatureColorGenerator( + model, + displaySettings.getTrackColorByFeature(), + displaySettings.getMissingValueColor(), + displaySettings.getUndefinedValueColor(), + displaySettings.getColormap(), + displaySettings.getTrackMin(), + displaySettings.getTrackMax()); + + default: + throw new IllegalArgumentException("Unknown type: " + displaySettings.getTrackColorByType()); + } + } + + public static final double[] autoMinMax(final Model model, final TrackMateObject type, final String feature) { + switch (type) { + case DEFAULT: + return new double[]{0., 0.}; + + case EDGES: + case SPOTS: + case TRACKS: { + final double[] values = collectFeatureValues(feature, type, model, true); + double min = Double.POSITIVE_INFINITY; + double max = Double.NEGATIVE_INFINITY; + for (final double val : values) { + if (val < min) + min = val; + + if (val > max) + max = val; + } + return new double[]{min, max}; + } + + default: + throw new IllegalArgumentException("Unexpected TrackMate object type: " + type); + } + } + + public static final int nObjects(final Model model, final TrackMateObject target, final boolean visibleOnly) { + switch (target) { + case DEFAULT: + throw new UnsupportedOperationException("Cannot return the number of objects for type DEFAULT."); + case EDGES: { + int nEdges = 0; + for (final Integer trackID : model.getTrackModel().unsortedTrackIDs(visibleOnly)) + nEdges += model.getTrackModel().trackEdges(trackID).size(); + return nEdges; + } + case SPOTS: + return model.getSpots().getNSpots(visibleOnly); + case TRACKS: + return model.getTrackModel().nTracks(visibleOnly); + default: + throw new IllegalArgumentException("Unknown TrackMate object: " + target); + } + } + + public abstract Map collectFeatureKeys(final TrackMateObject target, final Model model, final Settings settings); } diff --git a/src/main/java/fiji/plugin/trackmate/features/Spots.java b/src/main/java/fiji/plugin/trackmate/features/Spots.java new file mode 100644 index 000000000..717360b6c --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/features/Spots.java @@ -0,0 +1,38 @@ +package fiji.plugin.trackmate.features; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.features.spot.SpotAnalyzerFactoryBase; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; + +import java.util.*; + +public class Spots extends FeatureUtils { + public Map collectFeatureKeys(DisplaySettings.TrackMateObject target, Model model, Settings settings) { + final Map inverseMap = new HashMap<>(); + // Collect all. + if (model != null) { + for (final String featureKey : model.getFeatureModel().getSpotFeatureNames().keySet()) + inverseMap.put(model.getFeatureModel().getSpotFeatureNames().get(featureKey), featureKey); + } else { + // If we have no model, we still want to add spot features. + for (final String featureKey : Spot.FEATURE_NAMES.keySet()) + inverseMap.put(Spot.FEATURE_NAMES.get(featureKey), featureKey); + } + if (settings != null) { + for (final SpotAnalyzerFactoryBase sf : settings.getSpotAnalyzerFactories()) + for (final String featureKey : sf.getFeatureNames().keySet()) + inverseMap.put(sf.getFeatureNames().get(featureKey), featureKey); + } + // Sort by feature name. + final List featureNameList = new ArrayList<>(inverseMap.keySet()); + featureNameList.sort(null); + + final Map featureNames = new LinkedHashMap<>(featureNameList.size()); + for (final String featureName : featureNameList) + featureNames.put(inverseMap.get(featureName), featureName); + + return featureNames; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/features/Tracks.java b/src/main/java/fiji/plugin/trackmate/features/Tracks.java new file mode 100644 index 000000000..18ed6824e --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/features/Tracks.java @@ -0,0 +1,35 @@ +package fiji.plugin.trackmate.features; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.features.track.TrackAnalyzer; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; + +import java.util.*; + +public class Tracks extends FeatureUtils { + + public Map collectFeatureKeys(DisplaySettings.TrackMateObject target, Model model, Settings settings) { + final Map inverseMap = new HashMap<>(); + + if (model != null) { + for (final String featureKey : model.getFeatureModel().getTrackFeatureNames().keySet()) + inverseMap.put(model.getFeatureModel().getTrackFeatureNames().get(featureKey), featureKey); + } + if (settings != null) { + for (final TrackAnalyzer ta : settings.getTrackAnalyzers()) + for (final String featureKey : ta.getFeatureNames().keySet()) + inverseMap.put(ta.getFeatureNames().get(featureKey), featureKey); + } + + // Sort by feature name. + final List featureNameList = new ArrayList<>(inverseMap.keySet()); + featureNameList.sort(null); + + final Map featureNames = new LinkedHashMap<>(featureNameList.size()); + for (final String featureName : featureNameList) + featureNames.put(inverseMap.get(featureName), featureName); + + return featureNames; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/FeatureDisplaySelector.java b/src/main/java/fiji/plugin/trackmate/gui/components/FeatureDisplaySelector.java index b7cfc5086..ad2612499 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/FeatureDisplaySelector.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/FeatureDisplaySelector.java @@ -8,12 +8,12 @@ * 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 * . @@ -21,48 +21,10 @@ */ package fiji.plugin.trackmate.gui.components; -import static fiji.plugin.trackmate.features.FeatureUtils.collectFeatureKeys; -import static fiji.plugin.trackmate.gui.Fonts.SMALL_FONT; -import static fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject.DEFAULT; -import static fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject.EDGES; -import static fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject.SPOTS; -import static fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject.TRACKS; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.FontMetrics; -import java.awt.Graphics; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.beans.PropertyChangeListener; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.JButton; -import javax.swing.JComponent; -import javax.swing.JFormattedTextField; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JMenuItem; -import javax.swing.JPanel; -import javax.swing.JPopupMenu; -import javax.swing.SwingConstants; - import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.ModelChangeEvent; import fiji.plugin.trackmate.Settings; -import fiji.plugin.trackmate.features.FeatureUtils; +import fiji.plugin.trackmate.features.*; import fiji.plugin.trackmate.features.manual.ManualEdgeColorAnalyzer; import fiji.plugin.trackmate.features.manual.ManualSpotColorAnalyzerFactory; import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; @@ -71,527 +33,529 @@ import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; -public class FeatureDisplaySelector -{ - - private static final List< String > FEATURES_WITHOUT_MIN_MAX = Arrays.asList( new String[] { - FeatureUtils.USE_UNIFORM_COLOR_KEY, - FeatureUtils.USE_RANDOM_COLOR_KEY, - TrackIndexAnalyzer.TRACK_INDEX, - ManualEdgeColorAnalyzer.FEATURE, - ManualSpotColorAnalyzerFactory.FEATURE - } ); - - private final Model model; - - private final Settings settings; - - private final DisplaySettings ds; - - public FeatureDisplaySelector( final Model model, final Settings settings, final DisplaySettings displaySettings ) - { - this.model = model; - this.settings = settings; - this.ds = displaySettings; - } - - public JPanel createSelectorForSpots() - { - return createSelectorFor( TrackMateObject.SPOTS ); - } - - public JPanel createSelectorForTracks() - { - return createSelectorFor( TRACKS ); - } - - public JPanel createSelectorFor( final TrackMateObject target ) - { - return new FeatureSelectorPanel( target ); - } - - private TrackMateObject getColorByType( final TrackMateObject target ) - { - return target == SPOTS ? ds.getSpotColorByType() : ds.getTrackColorByType(); - } - - private String getColorByFeature( final TrackMateObject target ) - { - return target == SPOTS ? ds.getSpotColorByFeature() : ds.getTrackColorByFeature(); - } - - private double getMin( final TrackMateObject target ) - { - return target == SPOTS ? ds.getSpotMin() : ds.getTrackMin(); - } - - private double getMax( final TrackMateObject target ) - { - return target == SPOTS ? ds.getSpotMax() : ds.getTrackMax(); - } - - private double[] autoMinMax( final TrackMateObject target ) - { - final TrackMateObject type = getColorByType( target ); - final String feature = getColorByFeature( target ); - return FeatureUtils.autoMinMax( model, type, feature ); - } - - /** - * Return a {@link CategoryJComboBox} that lets a user select among all - * available features in TrackMate. - * - * @return a new {@link CategoryJComboBox}. - */ - public static final CategoryJComboBox< TrackMateObject, String > createComboBoxSelector( final Model model, final Settings settings ) - { - final List< TrackMateObject > categoriesIn = Arrays.asList( TrackMateObject.values() ); - final LinkedHashMap< TrackMateObject, Collection< String > > features = new LinkedHashMap<>( categoriesIn.size() ); - final HashMap< TrackMateObject, String > categoryNames = new HashMap<>( categoriesIn.size() ); - final HashMap< String, String > featureNames = new HashMap<>(); - - for ( final TrackMateObject category : categoriesIn ) - { - final Map< String, String > featureKeys = collectFeatureKeys( category, model, settings ); - features.put( category, featureKeys.keySet() ); - featureNames.putAll( featureKeys ); - - switch ( category ) - { - case SPOTS: - categoryNames.put( SPOTS, "Spot features:" ); - break; - - case EDGES: - categoryNames.put( EDGES, "Edge features:" ); - break; - - case TRACKS: - categoryNames.put( TRACKS, "Track features:" ); - break; - - case DEFAULT: - categoryNames.put( DEFAULT, "Default:" ); - break; - - default: - throw new IllegalArgumentException( "Unknown object type: " + category ); - } - } - final CategoryJComboBox< TrackMateObject, String > cb = new CategoryJComboBox<>( features, featureNames, categoryNames ); - - /* - * Listen to new features appearing. - */ - - if ( null != model ) - model.addModelChangeListener( ( event ) -> { - if ( event.getEventID() == ModelChangeEvent.FEATURES_COMPUTED ) - { - final LinkedHashMap< TrackMateObject, Collection< String > > features2 = new LinkedHashMap<>( categoriesIn.size() ); - final HashMap< TrackMateObject, String > categoryNames2 = new HashMap<>( categoriesIn.size() ); - final HashMap< String, String > featureNames2 = new HashMap<>(); - - for ( final TrackMateObject category : categoriesIn ) - { - final Map< String, String > featureKeys = collectFeatureKeys( category, model, settings ); - features2.put( category, featureKeys.keySet() ); - featureNames2.putAll( featureKeys ); - - switch ( category ) - { - case SPOTS: - categoryNames2.put( SPOTS, "Spot features:" ); - break; - - case EDGES: - categoryNames2.put( EDGES, "Edge features:" ); - break; - - case TRACKS: - categoryNames2.put( TRACKS, "Track features:" ); - break; - - case DEFAULT: - categoryNames2.put( DEFAULT, "Default:" ); - break; - - default: - throw new IllegalArgumentException( "Unknown object type: " + category ); - } - } - cb.setItems( features2, featureNames2, categoryNames2 ); - } - } ); - - return cb; - } - - /* - * Inner classes. - */ - - private class FeatureSelectorPanel extends JPanel - { - - private static final long serialVersionUID = 1L; - - public FeatureSelectorPanel( final TrackMateObject target ) - { - - final GridBagLayout layout = new GridBagLayout(); - layout.rowHeights = new int[] { 0, 0, 20 }; - layout.columnWeights = new double[] { 0.0, 1.0 }; - layout.rowWeights = new double[] { 0.0, 0.0, 0.0 }; - setLayout( layout ); - - final JLabel lblColorBy = new JLabel( "Color " + target.toString() + " by:" ); - lblColorBy.setFont( SMALL_FONT ); - final GridBagConstraints gbcLblColorBy = new GridBagConstraints(); - gbcLblColorBy.anchor = GridBagConstraints.EAST; - gbcLblColorBy.fill = GridBagConstraints.VERTICAL; - gbcLblColorBy.insets = new Insets( 0, 0, 5, 5 ); - gbcLblColorBy.gridx = 0; - gbcLblColorBy.gridy = 0; - add( lblColorBy, gbcLblColorBy ); - - final CategoryJComboBox< TrackMateObject, String > cmbboxColor = createComboBoxSelector( model, settings ); - final GridBagConstraints gbcCmbboxColor = new GridBagConstraints(); - gbcCmbboxColor.fill = GridBagConstraints.HORIZONTAL; - gbcCmbboxColor.gridx = 1; - gbcCmbboxColor.gridy = 0; - add( cmbboxColor, gbcCmbboxColor ); - - final JPanel panelColorMap = new JPanel(); - final GridBagConstraints gbcPanelColorMap = new GridBagConstraints(); - gbcPanelColorMap.gridwidth = 2; - gbcPanelColorMap.fill = GridBagConstraints.BOTH; - gbcPanelColorMap.gridx = 0; - gbcPanelColorMap.gridy = 2; - add( panelColorMap, gbcPanelColorMap ); - - final CanvasColor canvasColor = new CanvasColor( target ); - panelColorMap.setLayout( new BorderLayout() ); - panelColorMap.add( canvasColor, BorderLayout.CENTER ); - - final JPanel panelMinMax = new JPanel(); - final GridBagConstraints gbcPanelMinMax = new GridBagConstraints(); - gbcPanelMinMax.gridwidth = 2; - gbcPanelMinMax.fill = GridBagConstraints.BOTH; - gbcPanelMinMax.gridx = 0; - gbcPanelMinMax.gridy = 1; - gbcPanelMinMax.insets = new Insets( 2, 0, 0, 0 ); - add( panelMinMax, gbcPanelMinMax ); - panelMinMax.setLayout( new BoxLayout( panelMinMax, BoxLayout.X_AXIS ) ); - - final JButton btnAutoMinMax = new JButton( "auto" ); - btnAutoMinMax.setFont( SMALL_FONT ); - panelMinMax.add( btnAutoMinMax ); - - panelMinMax.add( Box.createHorizontalGlue() ); - - final JLabel lblMin = new JLabel( "min" ); - lblMin.setFont( SMALL_FONT ); - panelMinMax.add( lblMin ); - - final JFormattedTextField ftfMin = new JFormattedTextField( Double.valueOf( getMin( target ) ) ); - ftfMin.setMaximumSize( new Dimension( 180, 2147483647 ) ); - GuiUtils.selectAllOnFocus( ftfMin ); - ftfMin.setHorizontalAlignment( SwingConstants.CENTER ); - ftfMin.setFont( SMALL_FONT ); - ftfMin.setColumns( 7 ); - panelMinMax.add( ftfMin ); - - panelMinMax.add( Box.createHorizontalGlue() ); - - final JLabel lblMax = new JLabel( "max" ); - lblMax.setFont( SMALL_FONT ); - panelMinMax.add( lblMax ); - - final JFormattedTextField ftfMax = new JFormattedTextField( Double.valueOf( getMax( target ) ) ); - ftfMax.setMaximumSize( new Dimension( 180, 2147483647 ) ); - GuiUtils.selectAllOnFocus( ftfMax ); - ftfMax.setHorizontalAlignment( SwingConstants.CENTER ); - ftfMax.setFont( SMALL_FONT ); - ftfMax.setColumns( 7 ); - panelMinMax.add( ftfMax ); - - /* - * Listeners. - */ - - /* - * Colormap menu. - */ - - final JPopupMenu colormapMenu = new JPopupMenu(); - final List< Colormap > cmaps = Colormap.getAvailableLUTs(); - for ( final Colormap cmap : cmaps ) - { - final Colormap lut = cmap; - final JMenuItem item = new JMenuItem(); - item.setPreferredSize( new Dimension( 100, 20 ) ); - final BoxLayout itemlayout = new BoxLayout( item, BoxLayout.LINE_AXIS ); - item.setLayout( itemlayout ); - item.add( new JLabel( lut.getName() ) ); - item.add( Box.createHorizontalGlue() ); - item.add( new JComponent() - { - - private static final long serialVersionUID = 1L; - - @Override - public void paint( final Graphics g ) - { - final int width = getWidth(); - final int height = getHeight(); - for ( int i = 0; i < width; i++ ) - { - final double beta = ( double ) i / ( width - 1 ); - g.setColor( lut.getPaint( beta ) ); - g.drawLine( i, 0, i, height ); - } - g.setColor( this.getParent().getBackground() ); - g.drawRect( 0, 0, width, height ); - } - - @Override - public Dimension getMaximumSize() - { - return new Dimension( 50, 20 ); - } - - @Override - public Dimension getPreferredSize() - { - return getMaximumSize(); - } - - } ); - item.addActionListener( e -> ds.setColormap( cmap ) ); - colormapMenu.add( item ); - } - canvasColor.addMouseListener( new MouseAdapter() - { - @Override - public void mouseClicked( final MouseEvent e ) - { - colormapMenu.show( canvasColor, e.getX(), e.getY() ); - } - } ); - - // Auto min max. - switch ( target ) - { - case SPOTS: - { - cmbboxColor.addActionListener( e -> { - ds.setSpotColorBy( cmbboxColor.getSelectedCategory(), cmbboxColor.getSelectedItem() ); - final boolean hasMinMax = !FEATURES_WITHOUT_MIN_MAX.contains( getColorByFeature( target ) ); - ftfMin.setEnabled( hasMinMax ); - ftfMax.setEnabled( hasMinMax ); - btnAutoMinMax.setEnabled( hasMinMax ); - if ( hasMinMax && !cmbboxColor.getSelectedItem().equals( getColorByFeature( target ) ) ) - { - final double[] minmax = autoMinMax( target ); - ftfMin.setValue( Double.valueOf( minmax[ 0 ] ) ); - ftfMax.setValue( Double.valueOf( minmax[ 1 ] ) ); - } - } ); - - final PropertyChangeListener pcl = e -> { - final double v1 = ( ( Number ) ftfMin.getValue() ).doubleValue(); - final double v2 = ( ( Number ) ftfMax.getValue() ).doubleValue(); - ds.setSpotMinMax( v1, v2 ); - }; - ftfMin.addPropertyChangeListener( "value", pcl ); - ftfMax.addPropertyChangeListener( "value", pcl ); - break; - } - case TRACKS: - - cmbboxColor.addActionListener( e -> { - ds.setTrackColorBy( cmbboxColor.getSelectedCategory(), cmbboxColor.getSelectedItem() ); - final boolean hasMinMax = !FEATURES_WITHOUT_MIN_MAX.contains( getColorByFeature( target ) ); - ftfMin.setEnabled( hasMinMax ); - ftfMax.setEnabled( hasMinMax ); - btnAutoMinMax.setEnabled( hasMinMax ); - if ( hasMinMax && !cmbboxColor.getSelectedItem().equals( getColorByFeature( target ) ) ) - { - final double[] minmax = autoMinMax( target ); - ftfMin.setValue( Double.valueOf( minmax[ 0 ] ) ); - ftfMax.setValue( Double.valueOf( minmax[ 1 ] ) ); - } - } ); - - final PropertyChangeListener pcl = e -> { - final double v1 = ( ( Number ) ftfMin.getValue() ).doubleValue(); - final double v2 = ( ( Number ) ftfMax.getValue() ).doubleValue(); - ds.setTrackMinMax( v1, v2 ); - }; - ftfMin.addPropertyChangeListener( "value", pcl ); - ftfMax.addPropertyChangeListener( "value", pcl ); - break; - - default: - throw new IllegalArgumentException( "Unexpected selector target: " + target ); - } - - btnAutoMinMax.addActionListener( e -> { - final double[] minmax = autoMinMax( target ); - ftfMin.setValue( Double.valueOf( minmax[ 0 ] ) ); - ftfMax.setValue( Double.valueOf( minmax[ 1 ] ) ); - } ); - - ds.listeners().add( () -> { - ftfMin.setValue( Double.valueOf( getMin( target ) ) ); - ftfMax.setValue( Double.valueOf( getMax( target ) ) ); - final String feature = getColorByFeature( target ); - if ( feature != cmbboxColor.getSelectedItem() ) - cmbboxColor.setSelectedItem( feature ); - - canvasColor.repaint(); - } ); - - /* - * Set current values. - */ - - cmbboxColor.setSelectedItem( getColorByFeature( target ) ); - } - } - - private final class CanvasColor extends JComponent - { - - private static final long serialVersionUID = 1L; - - private final TrackMateObject target; - - public CanvasColor( final TrackMateObject target ) - { - this.target = target; - } - - @Override - public void paint( final Graphics g ) - { - final String feature = getColorByFeature( target ); - if ( !isEnabled() || FEATURES_WITHOUT_MIN_MAX.contains( feature ) ) - { - g.setColor( this.getParent().getBackground() ); - g.fillRect( 0, 0, getWidth(), getHeight() ); - return; - } - - /* - * The color scale. - */ - - final double[] autoMinMax = autoMinMax( target ); - final double min = getMin( target ); - final double max = getMax( target ); - final double dataMin = autoMinMax[ 0 ]; - final double dataMax = autoMinMax[ 1 ]; - final Colormap colormap = ds.getColormap(); - final double alphaMin = ( ( min - dataMin ) / ( dataMax - dataMin ) ); - final double alphaMax = ( ( max - dataMin ) / ( dataMax - dataMin ) ); - final int width = getWidth(); - final int height = getHeight(); - for ( int i = 0; i < width; i++ ) - { - final double alpha = ( double ) i / ( width - 1 ); - final double beta = ( alpha - alphaMin ) / ( alphaMax - alphaMin ); - - g.setColor( colormap.getPaint( beta ) ); - g.drawLine( i, 0, i, height ); - } - - /* - * Print values as text. - */ - - g.setColor( Color.WHITE ); - g.setFont( SMALL_FONT.deriveFont( Font.BOLD ) ); - final FontMetrics fm = g.getFontMetrics(); - - final boolean isInt; - switch ( getColorByType( target ) ) - { - case TRACKS: - isInt = model.getFeatureModel().getTrackFeatureIsInt().get( feature ); - break; - case EDGES: - isInt = model.getFeatureModel().getEdgeFeatureIsInt().get( feature ); - break; - case SPOTS: - isInt = model.getFeatureModel().getSpotFeatureIsInt().get( feature ); - break; - default: - isInt = false; - } - - final String dataMinStr; - final String dataMaxStr; - final String minStr; - final String maxStr; - if ( isInt ) - { - dataMinStr = String.format( "%d", ( int ) dataMin ); - dataMaxStr = String.format( "%d", ( int ) dataMax ); - minStr = String.format( "%d", ( int ) min ); - maxStr = String.format( "%d", ( int ) max ); - } - else - { - dataMinStr = String.format( "%.1f", dataMin ); - dataMaxStr = String.format( "%.1f", dataMax ); - minStr = String.format( "%.1f", min ); - maxStr = String.format( "%.1f", max ); - } - - final int dataMinStrWidth = fm.stringWidth( dataMinStr ); - final int dataMaxStrWidth = fm.stringWidth( dataMaxStr ); - final int minStrWidth = fm.stringWidth( minStr ); - final int maxStrWidth = fm.stringWidth( maxStr ); - - g.setColor( GuiUtils.textColorForBackground( colormap.getPaint( -alphaMin / ( alphaMax - alphaMin ) ) ) ); - g.drawString( dataMinStr, 1, height / 2 + fm.getHeight() / 2 ); - - g.setColor( GuiUtils.textColorForBackground( colormap.getPaint( ( 1. - alphaMin ) / ( alphaMax - alphaMin ) ) ) ); - g.drawString( dataMaxStr, width - dataMaxStrWidth - 1, height / 2 + fm.getHeight() / 2 ); - - final int iMin = ( int ) ( ( width - 1 ) * ( min - dataMin ) / ( dataMax - dataMin ) ); - final int iMax = ( int ) ( ( width - 1 ) * ( max - dataMin ) / ( dataMax - dataMin ) ); - - if ( ( iMin - minStrWidth ) > dataMinStrWidth + 2 && iMin < ( width - dataMaxStrWidth - 2 ) ) - { - g.setColor( GuiUtils.textColorForBackground( colormap.getPaint( 0. ) ) ); - g.drawString( minStr, iMin - minStrWidth, height / 2 ); - } - if ( ( iMax + maxStrWidth ) < ( width - dataMaxStrWidth - 2 ) && iMax > dataMinStrWidth + 2 ) - { - g.setColor( GuiUtils.textColorForBackground( colormap.getPaint( 1. ) ) ); - g.drawString( maxStr, iMax, height / 2 ); - } - } - } - - /* - * For debugging only. - */ - - public static void main( final String[] args ) - { - GuiUtils.setSystemLookAndFeel(); - - final DisplaySettings ds = DisplaySettings.defaultStyle().copy(); +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.beans.PropertyChangeListener; +import java.util.List; +import java.util.*; + +import static fiji.plugin.trackmate.gui.Fonts.SMALL_FONT; +import static fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject.*; + +public class FeatureDisplaySelector { + + private static final List FEATURES_WITHOUT_MIN_MAX = Arrays.asList(FeatureUtils.USE_UNIFORM_COLOR_KEY, + FeatureUtils.USE_RANDOM_COLOR_KEY, + TrackIndexAnalyzer.TRACK_INDEX, + ManualEdgeColorAnalyzer.FEATURE, + ManualSpotColorAnalyzerFactory.FEATURE); + + private final Model model; + + private final Settings settings; + + private final DisplaySettings ds; + + public FeatureDisplaySelector(final Model model, final Settings settings, final DisplaySettings displaySettings) { + this.model = model; + this.settings = settings; + this.ds = displaySettings; + } + + /** + * Return a {@link CategoryJComboBox} that lets a user select among all + * available features in TrackMate. + * + * @return a new {@link CategoryJComboBox}. + */ + public static final CategoryJComboBox createComboBoxSelector(final Model model, final Settings settings) { + final List categoriesIn = Arrays.asList(TrackMateObject.values()); + final LinkedHashMap> features = new LinkedHashMap<>(categoriesIn.size()); + final HashMap categoryNames = new HashMap<>(categoriesIn.size()); + final HashMap featureNames = new HashMap<>(); + + + for (final TrackMateObject category : categoriesIn) { + FeatureUtils featureUtils; + final Map featureKeys; +// features.put(category, featureKeys.keySet()); +// featureNames.putAll(featureKeys); + + switch (category) { + case SPOTS: + featureUtils = new Spots(); + featureKeys = featureUtils.collectFeatureKeys(category, model, settings); + features.put(category, featureKeys.keySet()); + featureNames.putAll(featureKeys); + categoryNames.put(SPOTS, "Spot features:"); + break; + + case EDGES: + featureUtils = new Edges(); + featureKeys = featureUtils.collectFeatureKeys(category, model, settings); + features.put(category, featureKeys.keySet()); + featureNames.putAll(featureKeys); + categoryNames.put(EDGES, "Edge features:"); + break; + + case TRACKS: + featureUtils = new Tracks(); + featureKeys = featureUtils.collectFeatureKeys(category, model, settings); + features.put(category, featureKeys.keySet()); + featureNames.putAll(featureKeys); + categoryNames.put(TRACKS, "Track features:"); + break; + + case DEFAULT: + featureUtils = new Defaults(); + featureKeys = featureUtils.collectFeatureKeys(category, model, settings); + features.put(category, featureKeys.keySet()); + featureNames.putAll(featureKeys); + categoryNames.put(DEFAULT, "Default:"); + break; + + default: + throw new IllegalArgumentException("Unknown object type: " + category); + } + } + final CategoryJComboBox cb = new CategoryJComboBox<>(features, featureNames, categoryNames); + + /* + * Listen to new features appearing. + */ + + if (null != model) + model.addModelChangeListener((event) -> { + if (event.getEventID() == ModelChangeEvent.FEATURES_COMPUTED) { + final LinkedHashMap> features2 = new LinkedHashMap<>(categoriesIn.size()); + final HashMap categoryNames2 = new HashMap<>(categoriesIn.size()); + final HashMap featureNames2 = new HashMap<>(); + FeatureUtils featureUtils; + + for (final TrackMateObject category : categoriesIn) { + final Map featureKeys; +// features2.put(category, featureKeys.keySet()); +// featureNames2.putAll(featureKeys); + + switch (category) { + case SPOTS: + featureUtils = new Spots(); + featureKeys = featureUtils.collectFeatureKeys(category, model, settings); + features2.put(category, featureKeys.keySet()); + featureNames2.putAll(featureKeys); + categoryNames2.put(SPOTS, "Spot features:"); + break; + + case EDGES: + featureUtils = new Edges(); + featureKeys = featureUtils.collectFeatureKeys(category, model, settings); + features2.put(category, featureKeys.keySet()); + featureNames2.putAll(featureKeys); + categoryNames2.put(EDGES, "Edge features:"); + break; + + case TRACKS: + featureUtils = new Tracks(); + featureKeys = featureUtils.collectFeatureKeys(category, model, settings); + features2.put(category, featureKeys.keySet()); + featureNames2.putAll(featureKeys); + categoryNames2.put(TRACKS, "Track features:"); + break; + + case DEFAULT: + featureUtils = new Defaults(); + featureKeys = featureUtils.collectFeatureKeys(category, model, settings); + features2.put(category, featureKeys.keySet()); + featureNames2.putAll(featureKeys); + categoryNames2.put(DEFAULT, "Default:"); + break; + + default: + throw new IllegalArgumentException("Unknown object type: " + category); + } + } + cb.setItems(features2, featureNames2, categoryNames2); + } + }); + + return cb; + } + + public static void main(final String[] args) { + GuiUtils.setSystemLookAndFeel(); + + final DisplaySettings ds = DisplaySettings.defaultStyle().copy(); // ds.listeners().add( () -> System.out.println( "\n" + new Date() + "\nDisplay settings changed:\n" + ds ) ); - final FeatureDisplaySelector featureSelector = new FeatureDisplaySelector( FeatureUtils.DUMMY_MODEL, null, ds ); - final JFrame frame = new JFrame(); - frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); - frame.getContentPane().add( featureSelector.createSelectorForSpots() ); - frame.pack(); - frame.setVisible( true ); - } + final FeatureDisplaySelector featureSelector = new FeatureDisplaySelector(FeatureUtils.DUMMY_MODEL, null, ds); + final JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.getContentPane().add(featureSelector.createSelectorForSpots()); + frame.pack(); + frame.setVisible(true); + } + + public JPanel createSelectorForSpots() { + return createSelectorFor(TrackMateObject.SPOTS); + } + + public JPanel createSelectorForTracks() { + return createSelectorFor(TRACKS); + } + + public JPanel createSelectorFor(final TrackMateObject target) { + return new FeatureSelectorPanel(target); + } + + private TrackMateObject getColorByType(final TrackMateObject target) { + return target == SPOTS ? ds.getSpotColorByType() : ds.getTrackColorByType(); + } + + private String getColorByFeature(final TrackMateObject target) { + return target == SPOTS ? ds.getSpotColorByFeature() : ds.getTrackColorByFeature(); + } + + private double getMin(final TrackMateObject target) { + return target == SPOTS ? ds.getSpotMin() : ds.getTrackMin(); + } + + private double getMax(final TrackMateObject target) { + return target == SPOTS ? ds.getSpotMax() : ds.getTrackMax(); + } + + /* + * Inner classes. + */ + + private double[] autoMinMax(final TrackMateObject target) { + final TrackMateObject type = getColorByType(target); + final String feature = getColorByFeature(target); + return FeatureUtils.autoMinMax(model, type, feature); + } + + private class FeatureSelectorPanel extends JPanel { + + private static final long serialVersionUID = 1L; + + public FeatureSelectorPanel(final TrackMateObject target) { + + final GridBagLayout layout = new GridBagLayout(); + layout.rowHeights = new int[]{0, 0, 20}; + layout.columnWeights = new double[]{0.0, 1.0}; + layout.rowWeights = new double[]{0.0, 0.0, 0.0}; + setLayout(layout); + + final JLabel lblColorBy = new JLabel("Color " + target.toString() + " by:"); + lblColorBy.setFont(SMALL_FONT); + final GridBagConstraints gbcLblColorBy = new GridBagConstraints(); + gbcLblColorBy.anchor = GridBagConstraints.EAST; + gbcLblColorBy.fill = GridBagConstraints.VERTICAL; + gbcLblColorBy.insets = new Insets(0, 0, 5, 5); + gbcLblColorBy.gridx = 0; + gbcLblColorBy.gridy = 0; + add(lblColorBy, gbcLblColorBy); + + final CategoryJComboBox cmbboxColor = createComboBoxSelector(model, settings); + final GridBagConstraints gbcCmbboxColor = new GridBagConstraints(); + gbcCmbboxColor.fill = GridBagConstraints.HORIZONTAL; + gbcCmbboxColor.gridx = 1; + gbcCmbboxColor.gridy = 0; + add(cmbboxColor, gbcCmbboxColor); + + final JPanel panelColorMap = new JPanel(); + final GridBagConstraints gbcPanelColorMap = new GridBagConstraints(); + gbcPanelColorMap.gridwidth = 2; + gbcPanelColorMap.fill = GridBagConstraints.BOTH; + gbcPanelColorMap.gridx = 0; + gbcPanelColorMap.gridy = 2; + add(panelColorMap, gbcPanelColorMap); + + final CanvasColor canvasColor = new CanvasColor(target); + panelColorMap.setLayout(new BorderLayout()); + panelColorMap.add(canvasColor, BorderLayout.CENTER); + + final JPanel panelMinMax = new JPanel(); + final GridBagConstraints gbcPanelMinMax = new GridBagConstraints(); + gbcPanelMinMax.gridwidth = 2; + gbcPanelMinMax.fill = GridBagConstraints.BOTH; + gbcPanelMinMax.gridx = 0; + gbcPanelMinMax.gridy = 1; + gbcPanelMinMax.insets = new Insets(2, 0, 0, 0); + add(panelMinMax, gbcPanelMinMax); + panelMinMax.setLayout(new BoxLayout(panelMinMax, BoxLayout.X_AXIS)); + + final JButton btnAutoMinMax = new JButton("auto"); + btnAutoMinMax.setFont(SMALL_FONT); + panelMinMax.add(btnAutoMinMax); + + panelMinMax.add(Box.createHorizontalGlue()); + + final JLabel lblMin = new JLabel("min"); + lblMin.setFont(SMALL_FONT); + panelMinMax.add(lblMin); + + final JFormattedTextField ftfMin = new JFormattedTextField(Double.valueOf(getMin(target))); + ftfMin.setMaximumSize(new Dimension(180, 2147483647)); + GuiUtils.selectAllOnFocus(ftfMin); + ftfMin.setHorizontalAlignment(SwingConstants.CENTER); + ftfMin.setFont(SMALL_FONT); + ftfMin.setColumns(7); + panelMinMax.add(ftfMin); + + panelMinMax.add(Box.createHorizontalGlue()); + + final JLabel lblMax = new JLabel("max"); + lblMax.setFont(SMALL_FONT); + panelMinMax.add(lblMax); + + final JFormattedTextField ftfMax = new JFormattedTextField(Double.valueOf(getMax(target))); + ftfMax.setMaximumSize(new Dimension(180, 2147483647)); + GuiUtils.selectAllOnFocus(ftfMax); + ftfMax.setHorizontalAlignment(SwingConstants.CENTER); + ftfMax.setFont(SMALL_FONT); + ftfMax.setColumns(7); + panelMinMax.add(ftfMax); + + /* + * Listeners. + */ + + /* + * Colormap menu. + */ + + final JPopupMenu colormapMenu = new JPopupMenu(); + final List cmaps = Colormap.getAvailableLUTs(); + for (final Colormap cmap : cmaps) { + final Colormap lut = cmap; + final JMenuItem item = new JMenuItem(); + item.setPreferredSize(new Dimension(100, 20)); + final BoxLayout itemlayout = new BoxLayout(item, BoxLayout.LINE_AXIS); + item.setLayout(itemlayout); + item.add(new JLabel(lut.getName())); + item.add(Box.createHorizontalGlue()); + item.add(new JComponent() { + + private static final long serialVersionUID = 1L; + + @Override + public void paint(final Graphics g) { + final int width = getWidth(); + final int height = getHeight(); + for (int i = 0; i < width; i++) { + final double beta = (double) i / (width - 1); + g.setColor(lut.getPaint(beta)); + g.drawLine(i, 0, i, height); + } + g.setColor(this.getParent().getBackground()); + g.drawRect(0, 0, width, height); + } + + @Override + public Dimension getMaximumSize() { + return new Dimension(50, 20); + } + + @Override + public Dimension getPreferredSize() { + return getMaximumSize(); + } + + }); + item.addActionListener(e -> ds.setColormap(cmap)); + colormapMenu.add(item); + } + canvasColor.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(final MouseEvent e) { + colormapMenu.show(canvasColor, e.getX(), e.getY()); + } + }); + + // Auto min max. + switch (target) { + case SPOTS: { + cmbboxColor.addActionListener(e -> { + ds.setSpotColorBy(cmbboxColor.getSelectedCategory(), cmbboxColor.getSelectedItem()); + final boolean hasMinMax = !FEATURES_WITHOUT_MIN_MAX.contains(getColorByFeature(target)); + ftfMin.setEnabled(hasMinMax); + ftfMax.setEnabled(hasMinMax); + btnAutoMinMax.setEnabled(hasMinMax); + if (hasMinMax && !cmbboxColor.getSelectedItem().equals(getColorByFeature(target))) { + final double[] minmax = autoMinMax(target); + ftfMin.setValue(Double.valueOf(minmax[0])); + ftfMax.setValue(Double.valueOf(minmax[1])); + } + }); + + final PropertyChangeListener pcl = e -> { + final double v1 = ((Number) ftfMin.getValue()).doubleValue(); + final double v2 = ((Number) ftfMax.getValue()).doubleValue(); + ds.setSpotMinMax(v1, v2); + }; + ftfMin.addPropertyChangeListener("value", pcl); + ftfMax.addPropertyChangeListener("value", pcl); + break; + } + case TRACKS: + + cmbboxColor.addActionListener(e -> { + ds.setTrackColorBy(cmbboxColor.getSelectedCategory(), cmbboxColor.getSelectedItem()); + final boolean hasMinMax = !FEATURES_WITHOUT_MIN_MAX.contains(getColorByFeature(target)); + ftfMin.setEnabled(hasMinMax); + ftfMax.setEnabled(hasMinMax); + btnAutoMinMax.setEnabled(hasMinMax); + if (hasMinMax && !cmbboxColor.getSelectedItem().equals(getColorByFeature(target))) { + final double[] minmax = autoMinMax(target); + ftfMin.setValue(Double.valueOf(minmax[0])); + ftfMax.setValue(Double.valueOf(minmax[1])); + } + }); + + final PropertyChangeListener pcl = e -> { + final double v1 = ((Number) ftfMin.getValue()).doubleValue(); + final double v2 = ((Number) ftfMax.getValue()).doubleValue(); + ds.setTrackMinMax(v1, v2); + }; + ftfMin.addPropertyChangeListener("value", pcl); + ftfMax.addPropertyChangeListener("value", pcl); + break; + + default: + throw new IllegalArgumentException("Unexpected selector target: " + target); + } + + btnAutoMinMax.addActionListener(e -> { + final double[] minmax = autoMinMax(target); + ftfMin.setValue(Double.valueOf(minmax[0])); + ftfMax.setValue(Double.valueOf(minmax[1])); + }); + + ds.listeners().add(() -> { + ftfMin.setValue(Double.valueOf(getMin(target))); + ftfMax.setValue(Double.valueOf(getMax(target))); + final String feature = getColorByFeature(target); + if (feature != cmbboxColor.getSelectedItem()) + cmbboxColor.setSelectedItem(feature); + + canvasColor.repaint(); + }); + + /* + * Set current values. + */ + + cmbboxColor.setSelectedItem(getColorByFeature(target)); + } + } + + /* + * For debugging only. + */ + + private final class CanvasColor extends JComponent { + + private static final long serialVersionUID = 1L; + + private final TrackMateObject target; + + public CanvasColor(final TrackMateObject target) { + this.target = target; + } + + @Override + public void paint(final Graphics g) { + final String feature = getColorByFeature(target); + if (!isEnabled() || FEATURES_WITHOUT_MIN_MAX.contains(feature)) { + g.setColor(this.getParent().getBackground()); + g.fillRect(0, 0, getWidth(), getHeight()); + return; + } + + /* + * The color scale. + */ + + final double[] autoMinMax = autoMinMax(target); + final double min = getMin(target); + final double max = getMax(target); + final double dataMin = autoMinMax[0]; + final double dataMax = autoMinMax[1]; + final Colormap colormap = ds.getColormap(); + final double alphaMin = ((min - dataMin) / (dataMax - dataMin)); + final double alphaMax = ((max - dataMin) / (dataMax - dataMin)); + final int width = getWidth(); + final int height = getHeight(); + for (int i = 0; i < width; i++) { + final double alpha = (double) i / (width - 1); + final double beta = (alpha - alphaMin) / (alphaMax - alphaMin); + + g.setColor(colormap.getPaint(beta)); + g.drawLine(i, 0, i, height); + } + + /* + * Print values as text. + */ + + g.setColor(Color.WHITE); + g.setFont(SMALL_FONT.deriveFont(Font.BOLD)); + final FontMetrics fm = g.getFontMetrics(); + + final boolean isInt; + switch (getColorByType(target)) { + case TRACKS: + isInt = model.getFeatureModel().getTrackFeatureIsInt().get(feature); + break; + case EDGES: + isInt = model.getFeatureModel().getEdgeFeatureIsInt().get(feature); + break; + case SPOTS: + isInt = model.getFeatureModel().getSpotFeatureIsInt().get(feature); + break; + default: + isInt = false; + } + + final String dataMinStr; + final String dataMaxStr; + final String minStr; + final String maxStr; + if (isInt) { + dataMinStr = String.format("%d", (int) dataMin); + dataMaxStr = String.format("%d", (int) dataMax); + minStr = String.format("%d", (int) min); + maxStr = String.format("%d", (int) max); + } else { + dataMinStr = String.format("%.1f", dataMin); + dataMaxStr = String.format("%.1f", dataMax); + minStr = String.format("%.1f", min); + maxStr = String.format("%.1f", max); + } + + final int dataMinStrWidth = fm.stringWidth(dataMinStr); + final int dataMaxStrWidth = fm.stringWidth(dataMaxStr); + final int minStrWidth = fm.stringWidth(minStr); + final int maxStrWidth = fm.stringWidth(maxStr); + + g.setColor(GuiUtils.textColorForBackground(colormap.getPaint(-alphaMin / (alphaMax - alphaMin)))); + g.drawString(dataMinStr, 1, height / 2 + fm.getHeight() / 2); + + g.setColor(GuiUtils.textColorForBackground(colormap.getPaint((1. - alphaMin) / (alphaMax - alphaMin)))); + g.drawString(dataMaxStr, width - dataMaxStrWidth - 1, height / 2 + fm.getHeight() / 2); + + final int iMin = (int) ((width - 1) * (min - dataMin) / (dataMax - dataMin)); + final int iMax = (int) ((width - 1) * (max - dataMin) / (dataMax - dataMin)); + + if ((iMin - minStrWidth) > dataMinStrWidth + 2 && iMin < (width - dataMaxStrWidth - 2)) { + g.setColor(GuiUtils.textColorForBackground(colormap.getPaint(0.))); + g.drawString(minStr, iMin - minStrWidth, height / 2); + } + if ((iMax + maxStrWidth) < (width - dataMaxStrWidth - 2) && iMax > dataMinStrWidth + 2) { + g.setColor(GuiUtils.textColorForBackground(colormap.getPaint(1.))); + g.drawString(maxStr, iMax, height / 2); + } + } + } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/FilterGuiPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/FilterGuiPanel.java index f5790ab38..51e25a419 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/FilterGuiPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/FilterGuiPanel.java @@ -8,12 +8,12 @@ * 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 * . @@ -21,416 +21,384 @@ */ package fiji.plugin.trackmate.gui.components; -import static fiji.plugin.trackmate.features.FeatureUtils.collectFeatureKeys; -import static fiji.plugin.trackmate.features.FeatureUtils.collectFeatureValues; -import static fiji.plugin.trackmate.features.FeatureUtils.nObjects; -import static fiji.plugin.trackmate.gui.Fonts.BIG_FONT; -import static fiji.plugin.trackmate.gui.Fonts.SMALL_FONT; -import static fiji.plugin.trackmate.gui.Icons.ADD_ICON; -import static fiji.plugin.trackmate.gui.Icons.REMOVE_ICON; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.event.ActionEvent; -import java.util.ArrayList; -import java.util.Collection; -import java.util.EmptyStackException; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Stack; -import java.util.function.Function; - -import javax.swing.BorderFactory; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JProgressBar; -import javax.swing.JScrollPane; -import javax.swing.ScrollPaneConstants; -import javax.swing.SwingUtilities; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Settings; -import fiji.plugin.trackmate.features.FeatureFilter; +import fiji.plugin.trackmate.features.*; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; import fiji.plugin.trackmate.util.OnRequestUpdater; -public class FilterGuiPanel extends JPanel implements ChangeListener -{ - - private static final long serialVersionUID = -1L; - - private final ChangeEvent CHANGE_EVENT = new ChangeEvent( this ); - - public ActionEvent COLOR_FEATURE_CHANGED = null; - - private final OnRequestUpdater updater; - - private final Stack< FilterPanel > filterPanels = new Stack<>(); - - private final Stack< Component > struts = new Stack<>(); - - private final List< FeatureFilter > featureFilters = new ArrayList<>(); - - private final List< ChangeListener > changeListeners = new ArrayList<>(); - - private final Model model; - - private final JPanel allThresholdsPanel; - - private final JLabel lblInfo; - - private final TrackMateObject target; - - private final Settings settings; - - private final String defaultFeature; - - private final ProgressBarLogger logger; - - private final JLabel lblTop; - - private final JProgressBar progressBar; - - /* - * CONSTRUCTOR - */ - - public FilterGuiPanel( - final Model model, - final Settings settings, - final TrackMateObject target, - final List< FeatureFilter > filters, - final String defaultFeature, - final FeatureDisplaySelector featureSelector ) - { - - this.model = model; - this.settings = settings; - this.target = target; - this.defaultFeature = defaultFeature; - this.updater = new OnRequestUpdater( () -> refresh() ); - - this.setLayout( new BorderLayout() ); - setPreferredSize( new Dimension( 270, 500 ) ); - - final JPanel topPanel = new JPanel(); - add( topPanel, BorderLayout.NORTH ); - topPanel.setLayout( new BorderLayout( 0, 0 ) ); +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.util.List; +import java.util.*; +import java.util.function.Function; - lblTop = new JLabel( " Set filters on " + target ); - lblTop.setFont( BIG_FONT ); - lblTop.setPreferredSize( new Dimension( 300, 40 ) ); - topPanel.add( lblTop, BorderLayout.NORTH ); +import static fiji.plugin.trackmate.features.FeatureUtils.collectFeatureValues; +import static fiji.plugin.trackmate.features.FeatureUtils.nObjects; +import static fiji.plugin.trackmate.gui.Fonts.BIG_FONT; +import static fiji.plugin.trackmate.gui.Fonts.SMALL_FONT; +import static fiji.plugin.trackmate.gui.Icons.ADD_ICON; +import static fiji.plugin.trackmate.gui.Icons.REMOVE_ICON; - progressBar = new JProgressBar(); - progressBar.setStringPainted( true ); - progressBar.setPreferredSize( new Dimension( 1300, 40 ) ); - topPanel.add( progressBar ); +public class FilterGuiPanel extends JPanel implements ChangeListener { + + private static final long serialVersionUID = -1L; + + private final ChangeEvent CHANGE_EVENT = new ChangeEvent(this); + private final OnRequestUpdater updater; + private final Stack filterPanels = new Stack<>(); + private final Stack struts = new Stack<>(); + private final List featureFilters = new ArrayList<>(); + private final List changeListeners = new ArrayList<>(); + private final Model model; + private final JPanel allThresholdsPanel; + private final JLabel lblInfo; + private final TrackMateObject target; + private final Settings settings; + private final String defaultFeature; + private final ProgressBarLogger logger; + private final JLabel lblTop; + private final JProgressBar progressBar; + public ActionEvent COLOR_FEATURE_CHANGED = null; + + /* + * CONSTRUCTOR + */ + + public FilterGuiPanel( + final Model model, + final Settings settings, + final TrackMateObject target, + final List filters, + final String defaultFeature, + final FeatureDisplaySelector featureSelector) { + + this.model = model; + this.settings = settings; + this.target = target; + this.defaultFeature = defaultFeature; + this.updater = new OnRequestUpdater(() -> refresh()); + + this.setLayout(new BorderLayout()); + setPreferredSize(new Dimension(270, 500)); + + final JPanel topPanel = new JPanel(); + add(topPanel, BorderLayout.NORTH); + topPanel.setLayout(new BorderLayout(0, 0)); + + lblTop = new JLabel(" Set filters on " + target); + lblTop.setFont(BIG_FONT); + lblTop.setPreferredSize(new Dimension(300, 40)); + topPanel.add(lblTop, BorderLayout.NORTH); + + progressBar = new JProgressBar(); + progressBar.setStringPainted(true); + progressBar.setPreferredSize(new Dimension(1300, 40)); + topPanel.add(progressBar); + + final JScrollPane scrollPaneThresholds = new JScrollPane(); + this.add(scrollPaneThresholds, BorderLayout.CENTER); + scrollPaneThresholds.setPreferredSize(new java.awt.Dimension(250, 389)); + scrollPaneThresholds.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scrollPaneThresholds.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + + allThresholdsPanel = new JPanel(); + final BoxLayout jPanelAllThresholdsLayout = new BoxLayout(allThresholdsPanel, BoxLayout.Y_AXIS); + allThresholdsPanel.setLayout(jPanelAllThresholdsLayout); + scrollPaneThresholds.setViewportView(allThresholdsPanel); + + final JPanel bottomPanel = new JPanel(); + bottomPanel.setLayout(new BorderLayout()); + this.add(bottomPanel, BorderLayout.SOUTH); + + final JPanel buttonsPanel = new JPanel(); + bottomPanel.add(buttonsPanel, BorderLayout.NORTH); + final BoxLayout jPanelButtonsLayout = new BoxLayout(buttonsPanel, javax.swing.BoxLayout.X_AXIS); + buttonsPanel.setLayout(jPanelButtonsLayout); + buttonsPanel.setPreferredSize(new java.awt.Dimension(270, 22)); + buttonsPanel.setSize(270, 25); + buttonsPanel.setMaximumSize(new java.awt.Dimension(32767, 25)); + + buttonsPanel.add(Box.createHorizontalStrut(5)); + final JButton btnAddThreshold = new JButton(); + buttonsPanel.add(btnAddThreshold); + btnAddThreshold.setIcon(ADD_ICON); + btnAddThreshold.setFont(SMALL_FONT); + btnAddThreshold.setPreferredSize(new java.awt.Dimension(24, 24)); + btnAddThreshold.setSize(24, 24); + btnAddThreshold.setMinimumSize(new java.awt.Dimension(24, 24)); + + buttonsPanel.add(Box.createHorizontalStrut(5)); + final JButton btnRemoveThreshold = new JButton(); + buttonsPanel.add(btnRemoveThreshold); + btnRemoveThreshold.setIcon(REMOVE_ICON); + btnRemoveThreshold.setFont(SMALL_FONT); + btnRemoveThreshold.setPreferredSize(new java.awt.Dimension(24, 24)); + btnRemoveThreshold.setSize(24, 24); + btnRemoveThreshold.setMinimumSize(new java.awt.Dimension(24, 24)); + + buttonsPanel.add(Box.createHorizontalGlue()); + buttonsPanel.add(Box.createHorizontalStrut(5)); + + lblInfo = new JLabel(); + lblInfo.setFont(SMALL_FONT); + buttonsPanel.add(lblInfo); + + /* + * Color for spots. + */ + + final JPanel coloringPanel = featureSelector.createSelectorFor(target); + coloringPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + bottomPanel.add(coloringPanel, BorderLayout.CENTER); + + /* + * Listeners & co. + */ + + btnAddThreshold.addActionListener(e -> addFilterPanel()); + btnRemoveThreshold.addActionListener(e -> removeThresholdPanel()); + + /* + * Initial values. + */ + + for (final FeatureFilter ft : filters) + addFilterPanel(ft); + + lblTop.setVisible(false); // For now + logger = new ProgressBarLogger(); + + // On close + GuiUtils.addOnClosingEvent(this, () -> updater.quit()); + } + + /* + * PUBLIC METHODS + */ + + /** + * Refresh the histograms displayed in the filter panels. + */ + public void refreshValues() { + for (final FilterPanel filterPanel : filterPanels) + filterPanel.refresh(); + } + + /** + * Called when one of the {@link FilterPanel} is changed by the user. + */ + @Override + public void stateChanged(final ChangeEvent e) { + updater.doUpdate(); + } + + /** + * Returns the thresholds currently set by this GUI. + */ + public List getFeatureFilters() { + return featureFilters; + } + + /** + * Add an {@link ChangeListener} to this panel. The {@link ChangeListener} + * will be notified when a change happens to the thresholds displayed by + * this panel, whether due to the slider being move, the auto-threshold + * button being pressed, or the combo-box selection being changed. + */ + public void addChangeListener(final ChangeListener listener) { + changeListeners.add(listener); + } + + /** + * Remove a ChangeListener from this panel. + * + * @return true if the listener was in listener collection of this instance. + */ + public boolean removeChangeListener(final ChangeListener listener) { + return changeListeners.remove(listener); + } + + public Collection getChangeListeners() { + return changeListeners; + } + + public void addFilterPanel() { + addFilterPanel(guessNextFeature()); + } + + public void addFilterPanel(final String feature) { + // NaN will signal making an auto-threshold. + final FeatureFilter filter = new FeatureFilter(feature, Double.NaN, true); + addFilterPanel(filter); + } + + public void checkTargetType(final TrackMateObject target, final Model model, final Settings settings) { +// switch() + } + + public Map trackMateObjectChecker(final TrackMateObject target, final Model model, final Settings settings) { + Map featureNames; + FeatureUtils featureUtils; + switch (target) { + case SPOTS: + featureUtils = new Spots(); + featureNames = featureUtils.collectFeatureKeys(target, model, settings); + break; + case EDGES: + featureUtils = new Edges(); + featureNames = featureUtils.collectFeatureKeys(target, model, settings); + break; + case TRACKS: + featureUtils = new Tracks(); + featureNames = featureUtils.collectFeatureKeys(target, model, settings); + break; + case DEFAULT: + featureUtils = new Defaults(); + featureNames = featureUtils.collectFeatureKeys(target, model, settings); + break; + default: + throw new IllegalArgumentException("Unknown object type: " + target); + } + return featureNames; + } + + public void addFilterPanel(final FeatureFilter filter) { + final Map featureNames = trackMateObjectChecker(target, model, settings); + final Function valueCollector = (featureKey) -> collectFeatureValues(featureKey, target, model, false); + final FilterPanel tp = new FilterPanel(featureNames, valueCollector, filter); + + tp.addChangeListener(this); + final Component strut = Box.createVerticalStrut(5); + struts.push(strut); + filterPanels.push(tp); + allThresholdsPanel.add(tp); + allThresholdsPanel.add(strut); + allThresholdsPanel.revalidate(); + stateChanged(CHANGE_EVENT); + } + + public void showProgressBar(final boolean show) { + progressBar.setVisible(show); + lblTop.setVisible(!show); + } + + public Logger getLogger() { + return logger; + } + + /* + * PRIVATE METHODS + */ + + /** + * Notify change listeners. + * + * @param e the event. + */ + private void fireThresholdChanged(final ChangeEvent e) { + for (final ChangeListener cl : changeListeners) + cl.stateChanged(e); + } + + private String guessNextFeature() { + final Map featureNames = trackMateObjectChecker(target, model, settings); + final Iterator it = featureNames.keySet().iterator(); + if (!it.hasNext()) + return ""; // It's likely something is not right. + + if (featureFilters.isEmpty()) + return (defaultFeature == null || !featureNames.containsKey(defaultFeature)) ? it.next() : defaultFeature; + + final FeatureFilter lastFilter = featureFilters.get(featureFilters.size() - 1); + final String lastFeature = lastFilter.feature; + while (it.hasNext()) + if (it.next().equals(lastFeature) && it.hasNext()) + return it.next(); + + return featureNames.keySet().iterator().next(); + } + + private void removeThresholdPanel() { + try { + final FilterPanel tp = filterPanels.pop(); + tp.removeChangeListener(this); + final Component strut = struts.pop(); + allThresholdsPanel.remove(strut); + allThresholdsPanel.remove(tp); + allThresholdsPanel.repaint(); + stateChanged(CHANGE_EVENT); + } catch (final EmptyStackException ese) { + } + } + + /** + * Refresh the {@link #featureFilters} field, notify change listeners and + * display the number of selected items. + */ + private void refresh() { + featureFilters.clear(); + for (final FilterPanel tp : filterPanels) + featureFilters.add(tp.getFilter()); + + fireThresholdChanged(null); + updateInfoText(); + } + + private void updateInfoText() { + final Map featureNames = trackMateObjectChecker(target, model, settings); + if (featureNames.isEmpty()) { + lblInfo.setText("No features found."); + return; + } + + final int nobjects = nObjects(model, target, false); + if (featureFilters == null || featureFilters.isEmpty()) { + final String info = "Keep all " + nobjects + " " + target + "."; + lblInfo.setText(info); + return; + } + + final int nselected = nObjects(model, target, true); + final String info = "Keep " + nselected + " " + target + " out of " + nobjects + "."; + lblInfo.setText(info); + } + + /* + * INNER CLASSES + */ + + private final class ProgressBarLogger extends Logger { + + @Override + public void error(final String message) { + log(message, Logger.ERROR_COLOR); + } + + @Override + public void log(final String message, final Color color) { + SwingUtilities.invokeLater(() -> progressBar.setString(message)); + } + + @Override + public void setStatus(final String status) { + SwingUtilities.invokeLater(() -> progressBar.setString(status)); + } + + @Override + public void setProgress(double val) { + if (val < 0) + val = 0; + if (val > 1) + val = 1; + final int intVal = (int) (val * 100); + SwingUtilities.invokeLater(() -> progressBar.setValue(intVal)); + } + } - final JScrollPane scrollPaneThresholds = new JScrollPane(); - this.add( scrollPaneThresholds, BorderLayout.CENTER ); - scrollPaneThresholds.setPreferredSize( new java.awt.Dimension( 250, 389 ) ); - scrollPaneThresholds.setHorizontalScrollBarPolicy( ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER ); - scrollPaneThresholds.setVerticalScrollBarPolicy( ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS ); - - allThresholdsPanel = new JPanel(); - final BoxLayout jPanelAllThresholdsLayout = new BoxLayout( allThresholdsPanel, BoxLayout.Y_AXIS ); - allThresholdsPanel.setLayout( jPanelAllThresholdsLayout ); - scrollPaneThresholds.setViewportView( allThresholdsPanel ); - - final JPanel bottomPanel = new JPanel(); - bottomPanel.setLayout( new BorderLayout() ); - this.add( bottomPanel, BorderLayout.SOUTH ); - - final JPanel buttonsPanel = new JPanel(); - bottomPanel.add( buttonsPanel, BorderLayout.NORTH ); - final BoxLayout jPanelButtonsLayout = new BoxLayout( buttonsPanel, javax.swing.BoxLayout.X_AXIS ); - buttonsPanel.setLayout( jPanelButtonsLayout ); - buttonsPanel.setPreferredSize( new java.awt.Dimension( 270, 22 ) ); - buttonsPanel.setSize( 270, 25 ); - buttonsPanel.setMaximumSize( new java.awt.Dimension( 32767, 25 ) ); - - buttonsPanel.add( Box.createHorizontalStrut( 5 ) ); - final JButton btnAddThreshold = new JButton(); - buttonsPanel.add( btnAddThreshold ); - btnAddThreshold.setIcon( ADD_ICON ); - btnAddThreshold.setFont( SMALL_FONT ); - btnAddThreshold.setPreferredSize( new java.awt.Dimension( 24, 24 ) ); - btnAddThreshold.setSize( 24, 24 ); - btnAddThreshold.setMinimumSize( new java.awt.Dimension( 24, 24 ) ); - - buttonsPanel.add( Box.createHorizontalStrut( 5 ) ); - final JButton btnRemoveThreshold = new JButton(); - buttonsPanel.add( btnRemoveThreshold ); - btnRemoveThreshold.setIcon( REMOVE_ICON ); - btnRemoveThreshold.setFont( SMALL_FONT ); - btnRemoveThreshold.setPreferredSize( new java.awt.Dimension( 24, 24 ) ); - btnRemoveThreshold.setSize( 24, 24 ); - btnRemoveThreshold.setMinimumSize( new java.awt.Dimension( 24, 24 ) ); - - buttonsPanel.add( Box.createHorizontalGlue() ); - buttonsPanel.add( Box.createHorizontalStrut( 5 ) ); - - lblInfo = new JLabel(); - lblInfo.setFont( SMALL_FONT ); - buttonsPanel.add( lblInfo ); - - /* - * Color for spots. - */ - - final JPanel coloringPanel = featureSelector.createSelectorFor( target ); - coloringPanel.setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) ); - bottomPanel.add( coloringPanel, BorderLayout.CENTER ); - - /* - * Listeners & co. - */ - - btnAddThreshold.addActionListener( e -> addFilterPanel() ); - btnRemoveThreshold.addActionListener( e -> removeThresholdPanel() ); - - /* - * Initial values. - */ - - for ( final FeatureFilter ft : filters ) - addFilterPanel( ft ); - - lblTop.setVisible( false ); // For now - logger = new ProgressBarLogger(); - - // On close - GuiUtils.addOnClosingEvent( this, () -> updater.quit() ); - } - - /* - * PUBLIC METHODS - */ - - /** - * Refresh the histograms displayed in the filter panels. - */ - public void refreshValues() - { - for ( final FilterPanel filterPanel : filterPanels ) - filterPanel.refresh(); - } - - - /** - * Called when one of the {@link FilterPanel} is changed by the user. - */ - @Override - public void stateChanged( final ChangeEvent e ) - { - updater.doUpdate(); - } - - /** - * Returns the thresholds currently set by this GUI. - */ - public List< FeatureFilter > getFeatureFilters() - { - return featureFilters; - } - - /** - * Add an {@link ChangeListener} to this panel. The {@link ChangeListener} - * will be notified when a change happens to the thresholds displayed by - * this panel, whether due to the slider being move, the auto-threshold - * button being pressed, or the combo-box selection being changed. - */ - public void addChangeListener( final ChangeListener listener ) - { - changeListeners.add( listener ); - } - - /** - * Remove a ChangeListener from this panel. - * - * @return true if the listener was in listener collection of this instance. - */ - public boolean removeChangeListener( final ChangeListener listener ) - { - return changeListeners.remove( listener ); - } - - public Collection< ChangeListener > getChangeListeners() - { - return changeListeners; - } - - public void addFilterPanel() - { - addFilterPanel( guessNextFeature() ); - } - - public void addFilterPanel( final String feature ) - { - // NaN will signal making an auto-threshold. - final FeatureFilter filter = new FeatureFilter( feature, Double.NaN, true ); - addFilterPanel( filter ); - } - - public void addFilterPanel( final FeatureFilter filter ) - { - final Map< String, String > featureNames = collectFeatureKeys( target, model, settings ); - final Function< String, double[] > valueCollector = ( featureKey ) -> collectFeatureValues( featureKey, target, model, false ); - final FilterPanel tp = new FilterPanel( featureNames, valueCollector, filter ); - - tp.addChangeListener( this ); - final Component strut = Box.createVerticalStrut( 5 ); - struts.push( strut ); - filterPanels.push( tp ); - allThresholdsPanel.add( tp ); - allThresholdsPanel.add( strut ); - allThresholdsPanel.revalidate(); - stateChanged( CHANGE_EVENT ); - } - - public void showProgressBar( final boolean show ) - { - progressBar.setVisible( show ); - lblTop.setVisible( !show ); - } - - public Logger getLogger() - { - return logger; - } - - /* - * PRIVATE METHODS - */ - - /** - * Notify change listeners. - * - * @param e - * the event. - */ - private void fireThresholdChanged( final ChangeEvent e ) - { - for ( final ChangeListener cl : changeListeners ) - cl.stateChanged( e ); - } - - private String guessNextFeature() - { - final Map< String, String > featureNames = collectFeatureKeys( target, model, settings ); - final Iterator< String > it = featureNames.keySet().iterator(); - if ( !it.hasNext() ) - return ""; // It's likely something is not right. - - if ( featureFilters.isEmpty() ) - return ( defaultFeature == null || !featureNames.keySet().contains( defaultFeature ) ) ? it.next() : defaultFeature; - - final FeatureFilter lastFilter = featureFilters.get( featureFilters.size() - 1 ); - final String lastFeature = lastFilter.feature; - while ( it.hasNext() ) - if ( it.next().equals( lastFeature ) && it.hasNext() ) - return it.next(); - - return featureNames.keySet().iterator().next(); - } - - private void removeThresholdPanel() - { - try - { - final FilterPanel tp = filterPanels.pop(); - tp.removeChangeListener( this ); - final Component strut = struts.pop(); - allThresholdsPanel.remove( strut ); - allThresholdsPanel.remove( tp ); - allThresholdsPanel.repaint(); - stateChanged( CHANGE_EVENT ); - } - catch ( final EmptyStackException ese ) - {} - } - - /** - * Refresh the {@link #featureFilters} field, notify change listeners and - * display the number of selected items. - */ - private void refresh() - { - featureFilters.clear(); - for ( final FilterPanel tp : filterPanels ) - featureFilters.add( tp.getFilter() ); - - fireThresholdChanged( null ); - updateInfoText(); - } - - private void updateInfoText() - { - final Map< String, String > featureNames = collectFeatureKeys( target, model, settings ); - if ( featureNames.isEmpty() ) - { - lblInfo.setText( "No features found." ); - return; - } - - final int nobjects = nObjects( model, target, false ); - if ( featureFilters == null || featureFilters.isEmpty() ) - { - final String info = "Keep all " + nobjects + " " + target + "."; - lblInfo.setText( info ); - return; - } - - final int nselected = nObjects( model, target, true ); - final String info = "Keep " + nselected + " " + target + " out of " + nobjects + "."; - lblInfo.setText( info ); - } - - /* - * INNER CLASSES - */ - - private final class ProgressBarLogger extends Logger - { - - @Override - public void error( final String message ) - { - log( message, Logger.ERROR_COLOR ); - } - - @Override - public void log( final String message, final Color color ) - { - SwingUtilities.invokeLater( () -> progressBar.setString( message ) ); - } - - @Override - public void setStatus( final String status ) - { - SwingUtilities.invokeLater( () -> progressBar.setString( status ) ); - } - - @Override - public void setProgress( double val ) - { - if ( val < 0 ) - val = 0; - if ( val > 1 ) - val = 1; - final int intVal = ( int ) ( val * 100 ); - SwingUtilities.invokeLater( () -> progressBar.setValue( intVal ) ); - } - }; } diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/GrapherPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/GrapherPanel.java index 6aee926ad..ba51f0fee 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/GrapherPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/GrapherPanel.java @@ -8,12 +8,12 @@ * 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 * . @@ -21,314 +21,269 @@ */ package fiji.plugin.trackmate.gui.components; -import static fiji.plugin.trackmate.gui.Icons.EDGE_ICON_64x64; -import static fiji.plugin.trackmate.gui.Icons.SPOT_ICON_64x64; -import static fiji.plugin.trackmate.gui.Icons.TRACK_ICON_64x64; - -import java.awt.BorderLayout; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.swing.BoxLayout; -import javax.swing.ButtonGroup; -import javax.swing.JCheckBox; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JRadioButton; -import javax.swing.JSeparator; -import javax.swing.JTabbedPane; -import javax.swing.SwingConstants; -import javax.swing.SwingUtilities; - -import org.jgrapht.graph.DefaultWeightedEdge; - import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.features.EdgeFeatureGrapher; -import fiji.plugin.trackmate.features.FeatureUtils; -import fiji.plugin.trackmate.features.SpotFeatureGrapher; -import fiji.plugin.trackmate.features.TrackFeatureGrapher; +import fiji.plugin.trackmate.features.*; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; import fiji.plugin.trackmate.util.Threads; +import org.jgrapht.graph.DefaultWeightedEdge; + +import javax.swing.*; +import java.awt.*; +import java.util.List; +import java.util.*; + +import static fiji.plugin.trackmate.gui.Icons.*; + +public class GrapherPanel extends JPanel { + + private static final long serialVersionUID = 1L; -public class GrapherPanel extends JPanel -{ + private final TrackMate trackmate; - private static final long serialVersionUID = 1L; + private final JPanel panelSpot; - private final TrackMate trackmate; + private final JPanel panelEdges; - private final JPanel panelSpot; + private final JPanel panelTracks; - private final JPanel panelEdges; + private final FeaturePlotSelectionPanel spotFeatureSelectionPanel; - private final JPanel panelTracks; + private final FeaturePlotSelectionPanel edgeFeatureSelectionPanel; - private final FeaturePlotSelectionPanel spotFeatureSelectionPanel; + private final FeaturePlotSelectionPanel trackFeatureSelectionPanel; - private final FeaturePlotSelectionPanel edgeFeatureSelectionPanel; + private final DisplaySettings displaySettings; - private final FeaturePlotSelectionPanel trackFeatureSelectionPanel; + private final SelectionModel selectionModel; - private final DisplaySettings displaySettings; + private final JPanel panelSelection; - private final SelectionModel selectionModel; + private final JRadioButton rdbtnAll; - private final JPanel panelSelection; + private final JRadioButton rdbtnSelection; - private final JRadioButton rdbtnAll; + private final JRadioButton rdbtnTracks; - private final JRadioButton rdbtnSelection; + private final JCheckBox chkboxConnectDots; - private final JRadioButton rdbtnTracks; + /* + * CONSTRUCTOR + */ - private final JCheckBox chkboxConnectDots; + public GrapherPanel(final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings) { + this.trackmate = trackmate; + this.selectionModel = selectionModel; + this.displaySettings = displaySettings; - /* - * CONSTRUCTOR - */ + FeatureUtils featureUtils; - public GrapherPanel( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings ) - { - this.trackmate = trackmate; - this.selectionModel = selectionModel; - this.displaySettings = displaySettings; + setLayout(new BorderLayout(0, 0)); - setLayout( new BorderLayout( 0, 0 ) ); + final JTabbedPane tabbedPane = new JTabbedPane(SwingConstants.TOP); + add(tabbedPane, BorderLayout.CENTER); - final JTabbedPane tabbedPane = new JTabbedPane( SwingConstants.TOP ); - add( tabbedPane, BorderLayout.CENTER ); + panelSpot = new JPanel(); + tabbedPane.addTab("Spots", SPOT_ICON_64x64, panelSpot, null); + panelSpot.setLayout(new BorderLayout(0, 0)); - panelSpot = new JPanel(); - tabbedPane.addTab( "Spots", SPOT_ICON_64x64, panelSpot, null ); - panelSpot.setLayout( new BorderLayout( 0, 0 ) ); - - panelEdges = new JPanel(); - tabbedPane.addTab( "Links", EDGE_ICON_64x64, panelEdges, null ); - panelEdges.setLayout( new BorderLayout( 0, 0 ) ); - - panelTracks = new JPanel(); - tabbedPane.addTab( "Tracks", TRACK_ICON_64x64, panelTracks, null ); - panelTracks.setLayout( new BorderLayout( 0, 0 ) ); - - final Map< String, String > spotFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.SPOTS, trackmate.getModel(), trackmate.getSettings() ); - final Set< String > spotFeatures = spotFeatureNames.keySet(); - spotFeatureSelectionPanel = new FeaturePlotSelectionPanel( - "T", - "Mean intensity ch1", - spotFeatures, - spotFeatureNames, - ( xKey, yKeys ) -> Threads.run( () -> plotSpotFeatures( xKey, yKeys ) ) ); - panelSpot.add( spotFeatureSelectionPanel ); - - // regen edge features - panelEdges.removeAll(); - final Map< String, String > edgeFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.EDGES, trackmate.getModel(), trackmate.getSettings() ); - final Set< String > edgeFeatures = edgeFeatureNames.keySet(); - edgeFeatureSelectionPanel = new FeaturePlotSelectionPanel( - "Edge time", - "Speed", - edgeFeatures, - edgeFeatureNames, - ( xKey, yKeys ) -> Threads.run( () -> plotEdgeFeatures( xKey, yKeys ) ) ); - panelEdges.add( edgeFeatureSelectionPanel ); - - // regen trak features - panelTracks.removeAll(); - final Map< String, String > trackFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.TRACKS, trackmate.getModel(), trackmate.getSettings() ); - final Set< String > trackFeatures = trackFeatureNames.keySet(); - trackFeatureSelectionPanel = new FeaturePlotSelectionPanel( - "Track index", - "Number of spots in track", - trackFeatures, - trackFeatureNames, - ( xKey, yKeys ) -> Threads.run( () -> plotTrackFeatures( xKey, yKeys ) ) ); - panelTracks.add( trackFeatureSelectionPanel ); - - panelSelection = new JPanel(); - panelSelection.setLayout( new BoxLayout( panelSelection, BoxLayout.LINE_AXIS ) ); - add( panelSelection, BorderLayout.SOUTH ); - - rdbtnAll = new JRadioButton( "All" ); - rdbtnAll.setFont( rdbtnAll.getFont().deriveFont( rdbtnAll.getFont().getSize() - 2f ) ); - panelSelection.add( rdbtnAll ); - - rdbtnSelection = new JRadioButton( "Selection" ); - rdbtnSelection.setFont( rdbtnSelection.getFont().deriveFont( rdbtnSelection.getFont().getSize() - 2f ) ); - panelSelection.add( rdbtnSelection ); - - rdbtnTracks = new JRadioButton( "Tracks of selection" ); - rdbtnTracks.setFont( rdbtnTracks.getFont().deriveFont( rdbtnTracks.getFont().getSize() - 2f ) ); - panelSelection.add( rdbtnTracks ); - - final ButtonGroup btngrp = new ButtonGroup(); - btngrp.add( rdbtnAll ); - btngrp.add( rdbtnSelection ); - btngrp.add( rdbtnTracks ); - rdbtnAll.setSelected( true ); - - panelSelection.add( new JSeparator( SwingConstants.VERTICAL ) ); - - chkboxConnectDots = new JCheckBox( "Connect" ); - chkboxConnectDots.setFont( chkboxConnectDots.getFont().deriveFont( chkboxConnectDots.getFont().getSize() - 2f ) ); - chkboxConnectDots.setSelected( true ); - panelSelection.add( chkboxConnectDots ); - } - - public FeaturePlotSelectionPanel getSpotFeatureSelectionPanel() - { - return spotFeatureSelectionPanel; - } - - public FeaturePlotSelectionPanel getEdgeFeatureSelectionPanel() - { - return edgeFeatureSelectionPanel; - } - - public FeaturePlotSelectionPanel getTrackFeatureSelectionPanel() - { - return trackFeatureSelectionPanel; - } - - private void plotSpotFeatures( final String xFeature, final List< String > yFeatures ) - { - final EverythingDisablerAndReenabler enabler = new EverythingDisablerAndReenabler( this, new Class[] { JLabel.class } ); - enabler.disable(); - try - { - final List< Spot > spots; - if ( rdbtnAll.isSelected() ) - { - spots = new ArrayList<>( trackmate.getModel().getSpots().getNSpots( true ) ); - for ( final Integer trackID : trackmate.getModel().getTrackModel().trackIDs( true ) ) - spots.addAll( trackmate.getModel().getTrackModel().trackSpots( trackID ) ); - } - else if ( rdbtnSelection.isSelected() ) - { - spots = new ArrayList<>( selectionModel.getSpotSelection() ); - } - else - { - selectionModel.selectTrack( - selectionModel.getSpotSelection(), - selectionModel.getEdgeSelection(), 0 ); - spots = new ArrayList<>( selectionModel.getSpotSelection() ); - } - final boolean addLines = chkboxConnectDots.isSelected(); - - final SpotFeatureGrapher grapher = new SpotFeatureGrapher( - spots, - xFeature, - yFeatures, - trackmate.getModel(), - selectionModel, - displaySettings, - addLines ); - final JFrame frame = grapher.render(); - frame.setIconImage( Icons.PLOT_ICON.getImage() ); - frame.setTitle( trackmate.getSettings().imp.getShortTitle() + " spot features" ); - GuiUtils.positionWindow( frame, SwingUtilities.getWindowAncestor( this ) ); - frame.setVisible( true ); - } - finally - { - enabler.reenable(); - } - } - - private void plotEdgeFeatures( final String xFeature, final List< String > yFeatures ) - { - final EverythingDisablerAndReenabler enabler = new EverythingDisablerAndReenabler( this, new Class[] { JLabel.class } ); - enabler.disable(); - try - { - final List< DefaultWeightedEdge > edges; - if ( rdbtnAll.isSelected() ) - { - edges = new ArrayList<>(); - for ( final Integer trackID : trackmate.getModel().getTrackModel().trackIDs( true ) ) - edges.addAll( trackmate.getModel().getTrackModel().trackEdges( trackID ) ); - } - else if ( rdbtnSelection.isSelected() ) - { - edges = new ArrayList<>( selectionModel.getEdgeSelection() ); - } - else - { - selectionModel.selectTrack( - selectionModel.getSpotSelection(), - selectionModel.getEdgeSelection(), 0 ); - edges = new ArrayList<>( selectionModel.getEdgeSelection() ); - } - final boolean addLines = chkboxConnectDots.isSelected(); - - final EdgeFeatureGrapher grapher = new EdgeFeatureGrapher( - edges, - xFeature, - yFeatures, - trackmate.getModel(), - selectionModel, - displaySettings, - addLines ); - final JFrame frame = grapher.render(); - frame.setIconImage( Icons.PLOT_ICON.getImage() ); - frame.setTitle( trackmate.getSettings().imp.getShortTitle() + " edge features" ); - GuiUtils.positionWindow( frame, SwingUtilities.getWindowAncestor( this ) ); - frame.setVisible( true ); - edgeFeatureSelectionPanel.setEnabled( true ); - } - finally - { - enabler.reenable(); - } - } - - private void plotTrackFeatures( final String xFeature, final List< String > yFeatures ) - { - final EverythingDisablerAndReenabler enabler = new EverythingDisablerAndReenabler( this, new Class[] { JLabel.class } ); - enabler.disable(); - try - { - final List< Integer > trackIDs; - if ( rdbtnAll.isSelected() ) - { - trackIDs = new ArrayList<>( trackmate.getModel().getTrackModel().unsortedTrackIDs( true ) ); - } - else - { - final Set< Integer > set = new HashSet<>(); - for ( final Spot spot : selectionModel.getSpotSelection() ) - set.add( trackmate.getModel().getTrackModel().trackIDOf( spot ) ); - for ( final DefaultWeightedEdge edge : selectionModel.getEdgeSelection() ) - set.add( trackmate.getModel().getTrackModel().trackIDOf( edge ) ); - trackIDs = new ArrayList< >( set ); - } - - final TrackFeatureGrapher grapher = new TrackFeatureGrapher( - trackIDs, - xFeature, - yFeatures, - trackmate.getModel(), - selectionModel, - displaySettings ); - final JFrame frame = grapher.render(); - frame.setIconImage( Icons.PLOT_ICON.getImage() ); - frame.setTitle( trackmate.getSettings().imp.getShortTitle() + " track features" ); - GuiUtils.positionWindow( frame, SwingUtilities.getWindowAncestor( this ) ); - frame.setVisible( true ); - } - finally - { - enabler.reenable(); - } - } + panelEdges = new JPanel(); + tabbedPane.addTab("Links", EDGE_ICON_64x64, panelEdges, null); + panelEdges.setLayout(new BorderLayout(0, 0)); + + panelTracks = new JPanel(); + tabbedPane.addTab("Tracks", TRACK_ICON_64x64, panelTracks, null); + panelTracks.setLayout(new BorderLayout(0, 0)); + + featureUtils = new Spots(); + final Map spotFeatureNames = featureUtils.collectFeatureKeys(TrackMateObject.SPOTS, trackmate.getModel(), trackmate.getSettings()); + final Set spotFeatures = spotFeatureNames.keySet(); + spotFeatureSelectionPanel = new FeaturePlotSelectionPanel( + "T", + "Mean intensity ch1", + spotFeatures, + spotFeatureNames, + (xKey, yKeys) -> Threads.run(() -> plotSpotFeatures(xKey, yKeys))); + panelSpot.add(spotFeatureSelectionPanel); + + // regen edge features + panelEdges.removeAll(); + featureUtils = new Edges(); + final Map edgeFeatureNames = featureUtils.collectFeatureKeys(TrackMateObject.EDGES, trackmate.getModel(), trackmate.getSettings()); + final Set edgeFeatures = edgeFeatureNames.keySet(); + edgeFeatureSelectionPanel = new FeaturePlotSelectionPanel( + "Edge time", + "Speed", + edgeFeatures, + edgeFeatureNames, + (xKey, yKeys) -> Threads.run(() -> plotEdgeFeatures(xKey, yKeys))); + panelEdges.add(edgeFeatureSelectionPanel); + + // regen trak features + panelTracks.removeAll(); + featureUtils = new Tracks(); + final Map trackFeatureNames = featureUtils.collectFeatureKeys(TrackMateObject.TRACKS, trackmate.getModel(), trackmate.getSettings()); + final Set trackFeatures = trackFeatureNames.keySet(); + trackFeatureSelectionPanel = new FeaturePlotSelectionPanel( + "Track index", + "Number of spots in track", + trackFeatures, + trackFeatureNames, + (xKey, yKeys) -> Threads.run(() -> plotTrackFeatures(xKey, yKeys))); + panelTracks.add(trackFeatureSelectionPanel); + + panelSelection = new JPanel(); + panelSelection.setLayout(new BoxLayout(panelSelection, BoxLayout.LINE_AXIS)); + add(panelSelection, BorderLayout.SOUTH); + + rdbtnAll = new JRadioButton("All"); + rdbtnAll.setFont(rdbtnAll.getFont().deriveFont(rdbtnAll.getFont().getSize() - 2f)); + panelSelection.add(rdbtnAll); + + rdbtnSelection = new JRadioButton("Selection"); + rdbtnSelection.setFont(rdbtnSelection.getFont().deriveFont(rdbtnSelection.getFont().getSize() - 2f)); + panelSelection.add(rdbtnSelection); + + rdbtnTracks = new JRadioButton("Tracks of selection"); + rdbtnTracks.setFont(rdbtnTracks.getFont().deriveFont(rdbtnTracks.getFont().getSize() - 2f)); + panelSelection.add(rdbtnTracks); + + final ButtonGroup btngrp = new ButtonGroup(); + btngrp.add(rdbtnAll); + btngrp.add(rdbtnSelection); + btngrp.add(rdbtnTracks); + rdbtnAll.setSelected(true); + + panelSelection.add(new JSeparator(SwingConstants.VERTICAL)); + + chkboxConnectDots = new JCheckBox("Connect"); + chkboxConnectDots.setFont(chkboxConnectDots.getFont().deriveFont(chkboxConnectDots.getFont().getSize() - 2f)); + chkboxConnectDots.setSelected(true); + panelSelection.add(chkboxConnectDots); + } + + public FeaturePlotSelectionPanel getSpotFeatureSelectionPanel() { + return spotFeatureSelectionPanel; + } + + public FeaturePlotSelectionPanel getEdgeFeatureSelectionPanel() { + return edgeFeatureSelectionPanel; + } + + public FeaturePlotSelectionPanel getTrackFeatureSelectionPanel() { + return trackFeatureSelectionPanel; + } + + private void plotSpotFeatures(final String xFeature, final List yFeatures) { + final EverythingDisablerAndReenabler enabler = new EverythingDisablerAndReenabler(this, new Class[]{JLabel.class}); + enabler.disable(); + try { + final List spots; + if (rdbtnAll.isSelected()) { + spots = new ArrayList<>(trackmate.getModel().getSpots().getNSpots(true)); + for (final Integer trackID : trackmate.getModel().getTrackModel().trackIDs(true)) + spots.addAll(trackmate.getModel().getTrackModel().trackSpots(trackID)); + } else if (rdbtnSelection.isSelected()) { + spots = new ArrayList<>(selectionModel.getSpotSelection()); + } else { + selectionModel.selectTrack( + selectionModel.getSpotSelection(), + selectionModel.getEdgeSelection(), 0); + spots = new ArrayList<>(selectionModel.getSpotSelection()); + } + final boolean addLines = chkboxConnectDots.isSelected(); + + final SpotFeatureGrapher grapher = new SpotFeatureGrapher( + spots, + xFeature, + yFeatures, + trackmate.getModel(), + selectionModel, + displaySettings, + addLines); + final JFrame frame = grapher.render(); + frame.setIconImage(Icons.PLOT_ICON.getImage()); + frame.setTitle(trackmate.getSettings().imp.getShortTitle() + " spot features"); + GuiUtils.positionWindow(frame, SwingUtilities.getWindowAncestor(this)); + frame.setVisible(true); + } finally { + enabler.reenable(); + } + } + + private void plotEdgeFeatures(final String xFeature, final List yFeatures) { + final EverythingDisablerAndReenabler enabler = new EverythingDisablerAndReenabler(this, new Class[]{JLabel.class}); + enabler.disable(); + try { + final List edges; + if (rdbtnAll.isSelected()) { + edges = new ArrayList<>(); + for (final Integer trackID : trackmate.getModel().getTrackModel().trackIDs(true)) + edges.addAll(trackmate.getModel().getTrackModel().trackEdges(trackID)); + } else if (rdbtnSelection.isSelected()) { + edges = new ArrayList<>(selectionModel.getEdgeSelection()); + } else { + selectionModel.selectTrack( + selectionModel.getSpotSelection(), + selectionModel.getEdgeSelection(), 0); + edges = new ArrayList<>(selectionModel.getEdgeSelection()); + } + final boolean addLines = chkboxConnectDots.isSelected(); + + final EdgeFeatureGrapher grapher = new EdgeFeatureGrapher( + edges, + xFeature, + yFeatures, + trackmate.getModel(), + selectionModel, + displaySettings, + addLines); + final JFrame frame = grapher.render(); + frame.setIconImage(Icons.PLOT_ICON.getImage()); + frame.setTitle(trackmate.getSettings().imp.getShortTitle() + " edge features"); + GuiUtils.positionWindow(frame, SwingUtilities.getWindowAncestor(this)); + frame.setVisible(true); + edgeFeatureSelectionPanel.setEnabled(true); + } finally { + enabler.reenable(); + } + } + + private void plotTrackFeatures(final String xFeature, final List yFeatures) { + final EverythingDisablerAndReenabler enabler = new EverythingDisablerAndReenabler(this, new Class[]{JLabel.class}); + enabler.disable(); + try { + final List trackIDs; + if (rdbtnAll.isSelected()) { + trackIDs = new ArrayList<>(trackmate.getModel().getTrackModel().unsortedTrackIDs(true)); + } else { + final Set set = new HashSet<>(); + for (final Spot spot : selectionModel.getSpotSelection()) + set.add(trackmate.getModel().getTrackModel().trackIDOf(spot)); + for (final DefaultWeightedEdge edge : selectionModel.getEdgeSelection()) + set.add(trackmate.getModel().getTrackModel().trackIDOf(edge)); + trackIDs = new ArrayList<>(set); + } + + final TrackFeatureGrapher grapher = new TrackFeatureGrapher( + trackIDs, + xFeature, + yFeatures, + trackmate.getModel(), + selectionModel, + displaySettings); + final JFrame frame = grapher.render(); + frame.setIconImage(Icons.PLOT_ICON.getImage()); + frame.setTitle(trackmate.getSettings().imp.getShortTitle() + " track features"); + GuiUtils.positionWindow(frame, SwingUtilities.getWindowAncestor(this)); + frame.setVisible(true); + } finally { + enabler.reenable(); + } + } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/GrapherDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/GrapherDescriptor.java index 4447c9840..b07a13973 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/GrapherDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/GrapherDescriptor.java @@ -8,12 +8,12 @@ * 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 * . @@ -21,51 +21,55 @@ */ package fiji.plugin.trackmate.gui.wizard.descriptors; -import java.util.Map; -import java.util.Set; - import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.features.Edges; import fiji.plugin.trackmate.features.FeatureUtils; +import fiji.plugin.trackmate.features.Spots; +import fiji.plugin.trackmate.features.Tracks; import fiji.plugin.trackmate.gui.components.FeaturePlotSelectionPanel; import fiji.plugin.trackmate.gui.components.GrapherPanel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; import fiji.plugin.trackmate.gui.wizard.WizardPanelDescriptor; -public class GrapherDescriptor extends WizardPanelDescriptor -{ +import java.util.Map; +import java.util.Set; + +public class GrapherDescriptor extends WizardPanelDescriptor { - private static final String KEY = "GraphFeatures"; + private static final String KEY = "GraphFeatures"; - private final TrackMate trackmate; + private final TrackMate trackmate; - public GrapherDescriptor( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings ) - { - super( KEY ); - this.trackmate = trackmate; - this.targetPanel = new GrapherPanel( trackmate, selectionModel, displaySettings ); - } + public GrapherDescriptor(final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings) { + super(KEY); + this.trackmate = trackmate; + this.targetPanel = new GrapherPanel(trackmate, selectionModel, displaySettings); + } - @Override - public void aboutToDisplayPanel() - { - // Regen features. - final GrapherPanel panel = ( GrapherPanel ) targetPanel; + @Override + public void aboutToDisplayPanel() { + FeatureUtils featureUtils; + // Regen features. + final GrapherPanel panel = (GrapherPanel) targetPanel; - final Map< String, String > spotFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.SPOTS, trackmate.getModel(), trackmate.getSettings() ); - final Set< String > spotFeatures = spotFeatureNames.keySet(); - final FeaturePlotSelectionPanel spotFeatureSelectionPanel = panel.getSpotFeatureSelectionPanel(); - spotFeatureSelectionPanel.setFeatures( spotFeatures, spotFeatureNames ); + featureUtils = new Spots(); + final Map spotFeatureNames = featureUtils.collectFeatureKeys(TrackMateObject.SPOTS, trackmate.getModel(), trackmate.getSettings()); + final Set spotFeatures = spotFeatureNames.keySet(); + final FeaturePlotSelectionPanel spotFeatureSelectionPanel = panel.getSpotFeatureSelectionPanel(); + spotFeatureSelectionPanel.setFeatures(spotFeatures, spotFeatureNames); - final Map< String, String > edgeFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.EDGES, trackmate.getModel(), trackmate.getSettings() ); - final Set< String > edgeFeatures = edgeFeatureNames.keySet(); - final FeaturePlotSelectionPanel edgeFeatureSelectionPanel = panel.getEdgeFeatureSelectionPanel(); - edgeFeatureSelectionPanel.setFeatures( edgeFeatures, edgeFeatureNames ); + featureUtils = new Edges(); + final Map edgeFeatureNames = featureUtils.collectFeatureKeys(TrackMateObject.EDGES, trackmate.getModel(), trackmate.getSettings()); + final Set edgeFeatures = edgeFeatureNames.keySet(); + final FeaturePlotSelectionPanel edgeFeatureSelectionPanel = panel.getEdgeFeatureSelectionPanel(); + edgeFeatureSelectionPanel.setFeatures(edgeFeatures, edgeFeatureNames); - final Map< String, String > trackFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.TRACKS, trackmate.getModel(), trackmate.getSettings() ); - final Set< String > trackFeatures = trackFeatureNames.keySet(); - final FeaturePlotSelectionPanel trackFeatureSelectionPanel = panel.getTrackFeatureSelectionPanel(); - trackFeatureSelectionPanel.setFeatures( trackFeatures, trackFeatureNames ); - } + featureUtils = new Tracks(); + final Map trackFeatureNames = featureUtils.collectFeatureKeys(TrackMateObject.TRACKS, trackmate.getModel(), trackmate.getSettings()); + final Set trackFeatures = trackFeatureNames.keySet(); + final FeaturePlotSelectionPanel trackFeatureSelectionPanel = panel.getTrackFeatureSelectionPanel(); + trackFeatureSelectionPanel.setFeatures(trackFeatures, trackFeatureNames); + } }