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 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 FeatureColorGeneratorp
th 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
* 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 p
th 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
+ * 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
* " + "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
- * 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, " + "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 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
"
- + "ignoring the spot actual position. "
- + "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}.
- *
"
+ + "ignoring the spot actual position. "
+ + "i
,
+ * the number of cells on the row corresponding to frame i
is
+ * rowLength.get(i)
.
+ */
+ private Mapnull
, 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