diff --git a/.idea/misc.xml b/.idea/misc.xml index 868906cb8..941073a8e 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - @@ -8,5 +7,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. " 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/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; } /** 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/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/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); + } } 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..217268385 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,552 @@ 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; + } + + 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"; + } + } } 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/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. 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); + + } + } }