diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 02c9c4e6c..851b4521a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,6 +28,7 @@ jobs: - name: Execute the build run: .github/build.sh env: + MAVEN_OPTS: -Djava.awt.headless=true GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} MAVEN_USER: ${{ secrets.MAVEN_USER }} diff --git a/pom.xml b/pom.xml index 41da46dd9..19d045462 100644 --- a/pom.xml +++ b/pom.xml @@ -1,17 +1,19 @@ - + 4.0.0 org.scijava pom-scijava - 43.0.0 + 45.0.0 sc.fiji TrackMate - 8.1.7-SNAPSHOT + 9.0.0-SNAPSHOT TrackMate TrackMate plugin for Fiji. @@ -164,6 +166,7 @@ + true fiji.plugin.trackmate gpl_v3 TrackMate developers. @@ -176,28 +179,42 @@ (https://github.com/scijava/scijava-coding-style) --> imglib2 - 2.5.2 0.11.1 - 8.0.0 - 10.6.7 + 1.2.0 - - 21 - [21,) - https://docs.oracle.com/en/java/javase/21/docs/api/ - + 21 + [21,) + + https://docs.oracle.com/en/java/javase/21/docs/api/ + - false + which is a depdency of Labkit.' --> + false + + + org.scijava + config-ui + 0.0.6 + + + org.slf4j slf4j-simple + + + org.litt + geff + 1.1.1-SNAPSHOT + + sc.fiji @@ -231,6 +248,32 @@ net.imagej imagej-common + + net.imglib2 + imglib2-mesh + + + sc.fiji + bigvolumeviewer + + + org.jogamp.jogl + jogl-all + + + org.jogamp.gluegen + gluegen-rt + + + org.jogamp.gluegen + gluegen-rt + ${scijava.natives.classifier.gluegen} + + + org.jogamp.jogl + jogl-all + ${scijava.natives.classifier.jogl} + @@ -271,16 +314,16 @@ org.scijava scijava-listeners + + org.scijava + ui-behaviour + com.github.vlsi.mxgraph jgraphx - com.itextpdf itextpdf @@ -346,11 +389,6 @@ javaGeom ${javaGeom.version} - - org.drjekyll - fontchooser - ${fontchooser.version} - @@ -358,6 +396,12 @@ junit test + + org.assertj + assertj-core + 3.27.7 + test + diff --git a/src/main/java/fiji/plugin/trackmate/Dimension.java b/src/main/java/fiji/plugin/trackmate/Dimension.java index 1ccc61d70..e7c8280b4 100644 --- a/src/main/java/fiji/plugin/trackmate/Dimension.java +++ b/src/main/java/fiji/plugin/trackmate/Dimension.java @@ -31,11 +31,64 @@ public enum Dimension POSITION, VELOCITY, LENGTH, - AREA, TIME, ANGLE, RATE, // count per frames - ANGLE_RATE, STRING; // for non-numeric features + AREA, + VOLUME, + TIME, + ANGLE, + RATE, // count per frames + ANGLE_RATE, + STRING; // for non-numeric features /* * We separated length and position so that x,y,z are plotted on a different * graph from spot sizes. */ + + /** + * Returns 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. + * + * @param spaceUnits + * the space units. + * @param timeUnits + * the time units. + * @return the units for the specified dimension. + */ + public String units( final String spaceUnits, final String timeUnits ) + { + switch ( this ) + { + 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 VOLUME: + return spaceUnits + "^3"; + 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; + } + } } diff --git a/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java b/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java index 70493a79f..478fcb51f 100644 --- a/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java +++ b/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java @@ -34,6 +34,7 @@ import fiji.plugin.trackmate.features.edges.EdgeAnalyzer; import fiji.plugin.trackmate.features.spot.SpotAnalyzerFactoryBase; import fiji.plugin.trackmate.features.track.TrackAnalyzer; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.components.LogPanel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; @@ -45,9 +46,7 @@ import fiji.plugin.trackmate.io.SettingsPersistence; import fiji.plugin.trackmate.io.TmXmlReader; import fiji.plugin.trackmate.util.TMUtils; -import fiji.plugin.trackmate.visualization.TrackMateModelView; import fiji.plugin.trackmate.visualization.ViewUtils; -import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; import ij.IJ; import ij.ImageJ; import ij.ImagePlus; @@ -135,7 +134,7 @@ public void run( final String filePath ) ImagePlus imp = reader.readImage(); if ( null == imp ) - imp = ViewUtils.makeEmpytImagePlus( model ); + imp = ViewUtils.makeEmptyImagePlus( model ); /* * Read settings. @@ -180,20 +179,17 @@ public void run( final String filePath ) analyzer.getFeatureDimensions(), analyzer.getIsIntFeature() ); + // Display settings. + final DisplaySettings displaySettings = reader.getDisplaySettings(); + /* - * Create TrackMate. + * Create GuiModel. */ - final TrackMate trackmate = createTrackMate( model, settings ); + final GuiModel guiModel = new GuiModel( model, settings, displaySettings ); // Hook actions - postRead( trackmate ); - - // Display settings. - final DisplaySettings displaySettings = reader.getDisplaySettings(); - - // Selection model. - final SelectionModel selectionModel = new SelectionModel( model ); + postRead( guiModel ); if ( !reader.isReadingOk() ) { @@ -202,8 +198,7 @@ public void run( final String filePath ) } // Main view. - final TrackMateModelView displayer = new HyperStackDisplayer( model, selectionModel, settings.imp, displaySettings ); - displayer.render(); + guiModel.getWindowManager().createHyperStackDisplayer(); // GUI state String panelIdentifier = reader.getGUIState(); @@ -212,7 +207,7 @@ public void run( final String filePath ) panelIdentifier = ConfigureViewsDescriptor.KEY; // Wizard. - final WizardSequence sequence = createSequence( trackmate, selectionModel, displaySettings ); + final WizardSequence sequence = createSequence( guiModel ); sequence.setCurrent( panelIdentifier ); final JFrame frame = sequence.run( "TrackMate on " + settings.imp.getShortTitle() ); frame.setIconImage( TRACKMATE_ICON.getImage() ); @@ -268,13 +263,13 @@ public void run( final String filePath ) /** * Hook for subclassers:
- * The {@link TrackMate} object is loaded and properly configured. This + * The {@link guiModel} object is loaded and properly configured. This * method is called just before the controller and GUI are launched. * * @param trackmate - * the {@link TrackMate} instance that was fledged after loading. + * the {@link guiModel} instance that was fledged after loading. */ - protected void postRead( final TrackMate trackmate ) + protected void postRead( final GuiModel guiModel ) {} /** @@ -297,13 +292,20 @@ protected TmXmlReader createReader( final File lFile ) public static void main( final String[] args ) { - GuiUtils.setSystemLookAndFeel(); - ImageJ.main( args ); - final LoadTrackMatePlugIn plugIn = new LoadTrackMatePlugIn(); + try + { + GuiUtils.setSystemLookAndFeel(); + ImageJ.main( args ); + final LoadTrackMatePlugIn plugIn = new LoadTrackMatePlugIn(); // plugIn.run( null ); // plugIn.run( "samples/FakeTracks.xml" ); - plugIn.run( "samples/MAX_Merged.xml" ); + plugIn.run( "samples/MAX_Merged.xml" ); // plugIn.run( "c:/Users/tinevez/Development/TrackMateWS/TrackMate-Cellpose/samples/R2_multiC.xml" ); // plugIn.run( "/Users/tinevez/Desktop/230901_DeltaRcsB-ZipA-mCh_timestep5min_Stage9_reg/230901_DeltaRcsB-ZipA-mCh_timestep5min_Stage9_reg_merge65.xml" ); + } + catch ( final Throwable t ) + { + t.printStackTrace(); + } } } diff --git a/src/main/java/fiji/plugin/trackmate/ManualTrackingPlugIn.java b/src/main/java/fiji/plugin/trackmate/ManualTrackingPlugIn.java index 354f4d7cb..5d90e0210 100644 --- a/src/main/java/fiji/plugin/trackmate/ManualTrackingPlugIn.java +++ b/src/main/java/fiji/plugin/trackmate/ManualTrackingPlugIn.java @@ -22,7 +22,7 @@ package fiji.plugin.trackmate; import fiji.plugin.trackmate.detection.ManualDetectorFactory; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.wizard.WizardSequence; import fiji.plugin.trackmate.gui.wizard.descriptors.ConfigureViewsDescriptor; import fiji.plugin.trackmate.tracking.manual.ManualTrackerFactory; @@ -33,9 +33,17 @@ public class ManualTrackingPlugIn extends TrackMatePlugIn { @Override - protected WizardSequence createSequence( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings ) + protected WizardSequence createSequence( final GuiModel guiModel ) { - final WizardSequence sequence = super.createSequence( trackmate, selectionModel, displaySettings ); + // Trigger computation of features so that they analyzers are declared + // in the model. + final TrackMate trackmate = guiModel.getTrackMate(); + trackmate.computeSpotFeatures( false ); + trackmate.computeEdgeFeatures( false ); + trackmate.computeTrackFeatures( false ); + + // Create sequence and position it on the ConfigureViewsDescriptor. + final WizardSequence sequence = super.createSequence( guiModel ); sequence.setCurrent( ConfigureViewsDescriptor.KEY ); return sequence; } @@ -54,18 +62,6 @@ protected Settings createSettings( final ImagePlus imp ) return lSettings; } - @Override - protected TrackMate createTrackMate( final Model model, final Settings settings ) - { - final TrackMate trackmate = super.createTrackMate( model, settings ); - // Trigger computation of features so that they analyzers are declared - // in the model. - trackmate.computeSpotFeatures( false ); - trackmate.computeEdgeFeatures( false ); - trackmate.computeTrackFeatures( false ); - return trackmate; - } - public static void main( final String[] args ) { ImageJ.main( args ); diff --git a/src/main/java/fiji/plugin/trackmate/Model.java b/src/main/java/fiji/plugin/trackmate/Model.java index 3c4289686..b4e963ef8 100644 --- a/src/main/java/fiji/plugin/trackmate/Model.java +++ b/src/main/java/fiji/plugin/trackmate/Model.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 * . @@ -32,6 +32,7 @@ import org.jgrapht.graph.SimpleWeightedGraph; import fiji.plugin.trackmate.features.FeatureFilter; +import fiji.plugin.trackmate.undo.UndoRedoStack; /** * The model for the data managed by TrackMate. @@ -43,7 +44,7 @@ * built in coherent sets. *

* - * @author Jean-Yves Tinevez <tinevez@pasteur.fr> - 2010-2013 + * @author Jean-Yves Tinevez - 2010-2026 */ public class Model { @@ -81,13 +82,18 @@ public class Model */ private int updateLevel = 0; - private final HashSet< Spot > spotsAdded = new HashSet< >(); + private final HashSet< Spot > spotsAdded = new HashSet<>(); - private final HashSet< Spot > spotsRemoved = new HashSet< >(); + private final HashSet< Spot > spotsRemoved = new HashSet<>(); - private final HashSet< Spot > spotsMoved = new HashSet< >(); + private final HashSet< Spot > spotsMoved = new HashSet<>(); - private final HashSet< Spot > spotsUpdated = new HashSet< >(); + private final HashSet< Spot > spotsUpdated = new HashSet<>(); + + /** + * Track IDs whose names were modified during the current transaction. + */ + private final HashSet< Integer > tracksNamedModified = new HashSet<>(); /** * The event cache. During a transaction, some modifications might trigger @@ -96,15 +102,15 @@ public class Model * the event ID in this cache in the meantime. The event cache contains only * the int IDs of the events listed in {@link ModelChangeEvent}, namely *
    - *
  • {@link ModelChangeEvent#SPOTS_COMPUTED} - *
  • {@link ModelChangeEvent#TRACKS_COMPUTED} - *
  • {@link ModelChangeEvent#TRACKS_VISIBILITY_CHANGED} + *
  • {@link ModelChangeEvent#SPOTS_COMPUTED} + *
  • {@link ModelChangeEvent#TRACKS_COMPUTED} + *
  • {@link ModelChangeEvent#TRACKS_VISIBILITY_CHANGED} *
* The {@link ModelChangeEvent#MODEL_MODIFIED} cannot be cached this way, * for it needs to be configured with modification spot and edge targets, so * it uses a different system (see {@link #flushUpdate()}). */ - private final HashSet< Integer > eventCache = new HashSet< >(); + private final HashSet< Integer > eventCache = new HashSet<>(); // OTHERS @@ -120,7 +126,9 @@ public class Model /** * The list of listeners listening to model content change. */ - Set< ModelChangeListener > modelChangeListeners = new LinkedHashSet< >(); + Set< ModelChangeListener > modelChangeListeners = new LinkedHashSet<>(); + + private final UndoRedoStack undoRedoStack; /* * CONSTRUCTOR @@ -128,8 +136,10 @@ public class Model public Model() { - featureModel = createFeatureModel(); - trackModel = createTrackModel(); + this.featureModel = createFeatureModel(); + this.trackModel = createTrackModel(); + this.undoRedoStack = new UndoRedoStack( this ); // TODO + addModelChangeListener( new SpotMeshSliceCacheInvalidator() ); } /* @@ -154,7 +164,7 @@ protected TrackModel createTrackModel() *

* Subclassers can override this method to have the model work with their * own subclass of {@link FeatureModel}. - * + * * @return a new instance of {@link FeatureModel}. */ protected FeatureModel createFeatureModel() @@ -321,7 +331,7 @@ public void clearTracks( final boolean doNotify ) /** * Returns the {@link TrackModel} that manages the tracks for this model. - * + * * @return the track model. */ public TrackModel getTrackModel() @@ -447,7 +457,7 @@ public void notifyFeaturesComputed() /** * Set the logger that will receive the messages from the processes * occurring within this trackmate. - * + * * @param logger * the {@link Logger} to use. */ @@ -458,7 +468,7 @@ public void setLogger( final Logger logger ) /** * Return the logger currently set for this model. - * + * * @return the {@link Logger} used. */ public Logger getLogger() @@ -544,7 +554,7 @@ public synchronized Spot moveSpotFrom( final Spot spotToMove, final Integer from * model.endUpdate(); * } * - * + * * @param spotToAdd * the spot to add. * @param toFrame @@ -593,8 +603,11 @@ public synchronized Spot removeSpot( final Spot spotToRemove ) if ( DEBUG ) System.out.println( "[TrackMateModel] Removing spot " + spotToRemove + " from frame " + fromFrame ); - trackModel.removeSpot( spotToRemove ); - // changes to edges will be caught automatically by the TrackGraphModel + // Flag all tracks for undo before removing spot (may split tracks) + undoRedoStack.flagAllTracksForUndo(); + trackModel.removeSpot( spotToRemove ); + // changes to edges will be caught automatically by the + // TrackGraphModel return spotToRemove; } if ( DEBUG ) @@ -603,37 +616,6 @@ public synchronized Spot removeSpot( final Spot spotToRemove ) return null; } - /** - * Mark the specified spot for update. At the end of the model transaction, - * its features will be recomputed, and other edge and track features that - * depends on it will be as well. - *

- * For the model update to happen correctly and listeners to be notified - * properly, a call to this method must happen within a transaction, as in: - * - *

-	 * model.beginUpdate();
-	 * try {
-	 * 	... // model modifications here
-	 * } finally {
-	 * 	model.endUpdate();
-	 * }
-	 * 
- * - * @param spotToUpdate - * the spot to mark for update - */ - public synchronized void updateFeatures( final Spot spotToUpdate ) - { - spotsUpdated.add( spotToUpdate ); // Enlist for feature update when - // transaction is marked as finished - final Set< DefaultWeightedEdge > touchingEdges = trackModel.edgesOf( spotToUpdate ); - if ( null != touchingEdges ) - { - trackModel.edgesModified.addAll( touchingEdges ); - } - } - /** * Creates a new edge between two spots, with the specified weight. *

@@ -659,6 +641,15 @@ public synchronized void updateFeatures( final Spot spotToUpdate ) */ public synchronized DefaultWeightedEdge addEdge( final Spot source, final Spot target, final double weight ) { + // Check if this edge will merge two tracks + final Integer sourceTrackId = trackModel.trackIDOf( source ); + final Integer targetTrackId = trackModel.trackIDOf( target ); + if ( sourceTrackId != null && targetTrackId != null && !sourceTrackId.equals( targetTrackId ) ) + { + // Flag both tracks for undo before merging + undoRedoStack.flagTrackForUndo( sourceTrackId ); + undoRedoStack.flagTrackForUndo( targetTrackId ); + } return trackModel.addEdge( source, target, weight ); } @@ -674,6 +665,8 @@ public synchronized DefaultWeightedEdge addEdge( final Spot source, final Spot t */ public synchronized DefaultWeightedEdge removeEdge( final Spot source, final Spot target ) { + // Flag all tracks for undo before removing edge (may split tracks) + undoRedoStack.flagAllTracksForUndo(); return trackModel.removeEdge( source, target ); } @@ -699,6 +692,8 @@ public synchronized DefaultWeightedEdge removeEdge( final Spot source, final Spo */ public synchronized boolean removeEdge( final DefaultWeightedEdge edge ) { + // Flag all tracks for undo before removing edge (may split tracks) + undoRedoStack.flagAllTracksForUndo(); return trackModel.removeEdge( edge ); } @@ -760,13 +755,44 @@ public synchronized boolean setTrackVisibility( final Integer trackID, final boo return oldvis; } + /** + * Sets the name of the track with the specified ID. + *

+ * This method captures the current track state for undo support before + * changing the name. For the model update to happen correctly and listeners + * to be notified properly, a call to this method must happen within a + * transaction, as in: + * + *

+	 * model.beginUpdate();
+	 * try {
+	 * 	model.setTrackName( trackId, "MyTrackName" );
+	 * } finally {
+	 * 	model.endUpdate();
+	 * }
+	 * 
+ * + * @param trackID + * the track ID. + * @param name + * the name for the track. + */ + public synchronized void setTrackName( final Integer trackID, final String name ) + { + // Capture current track state for undo + undoRedoStack.flagTrackForUndo( trackID ); + trackModel.setName( trackID, name ); + // Mark track as having its name modified for the event system + tracksNamedModified.add( trackID ); + } + /** * Returns a copy of this model. *

* The copy is made of the same spot objects but on a different graph, that * can be safely edited. The copy does not include the feature values for * edges and tracks, but the features are declared. - * + * * @return a new model. */ public Model copy() @@ -810,7 +836,7 @@ public Model copy() featureModel.getTrackFeatureShortNames(), featureModel.getTrackFeatureDimensions(), featureModel.getTrackFeatureIsInt() ); - + // Feature values are not copied. return copy; } @@ -824,12 +850,11 @@ public Model copy() */ private void flushUpdate() { - if ( DEBUG ) { System.out.println( "[TrackMateModel] #flushUpdate()." ); System.out.println( "[TrackMateModel] #flushUpdate(): Event cache is :" + eventCache ); - System.out.println( "[TrackMateModel] #flushUpdate(): Track content is:\n" + trackModel.echo() ); +// System.out.println( "[TrackMateModel] #flushUpdate(): Track content is:\n" + trackModel.echo() ); } /* @@ -841,7 +866,7 @@ private void flushUpdate() final int nEdgesToSignal = trackModel.edgesAdded.size() + trackModel.edgesRemoved.size() + trackModel.edgesModified.size(); // Do we have tracks to update? - final HashSet< Integer > tracksToUpdate = new HashSet< >( trackModel.tracksUpdated ); + final HashSet< Integer > tracksToUpdate = new HashSet<>( trackModel.tracksUpdated ); // We also want to update the tracks that have edges that were modified for ( final DefaultWeightedEdge modifiedEdge : trackModel.edgesModified ) @@ -849,11 +874,14 @@ private void flushUpdate() tracksToUpdate.add( trackModel.trackIDOf( modifiedEdge ) ); } + // Add tracks whose names were modified + tracksToUpdate.addAll( tracksNamedModified ); + // Deal with new or moved spots: we need to update their features. final int nSpotsToUpdate = spotsAdded.size() + spotsMoved.size() + spotsUpdated.size(); if ( nSpotsToUpdate > 0 ) { - final HashSet< Spot > spotsToUpdate = new HashSet< >( nSpotsToUpdate ); + final HashSet< Spot > spotsToUpdate = new HashSet<>( nSpotsToUpdate ); spotsToUpdate.addAll( spotsAdded ); spotsToUpdate.addAll( spotsMoved ); spotsToUpdate.addAll( spotsUpdated ); @@ -877,7 +905,10 @@ private void flushUpdate() } for ( final Spot spot : spotsRemoved ) { - event.putSpotFlag( spot, ModelChangeEvent.FLAG_SPOT_REMOVED ); + // Skip spots that were both added and removed in the same transaction + // (they are transient and should not be flagged as removed) + if ( !spotsAdded.contains( spot ) ) + event.putSpotFlag( spot, ModelChangeEvent.FLAG_SPOT_REMOVED ); } for ( final Spot spot : spotsMoved ) { @@ -913,9 +944,12 @@ private void flushUpdate() // Configure it with the tracks we found need updating event.setTracksUpdated( tracksToUpdate ); + // Fire the event if there are any changes to signal + final boolean hasChanges = nEdgesToSignal + nSpotsToSignal > 0 || !tracksNamedModified.isEmpty(); + try { - if ( nEdgesToSignal + nSpotsToSignal > 0 ) + if ( hasChanges ) { if ( DEBUG ) { @@ -950,6 +984,7 @@ private void flushUpdate() spotsRemoved.clear(); spotsMoved.clear(); spotsUpdated.clear(); + tracksNamedModified.clear(); trackModel.edgesAdded.clear(); trackModel.edgesRemoved.clear(); trackModel.edgesModified.clear(); @@ -958,4 +993,88 @@ private void flushUpdate() } } + private static class SpotMeshSliceCacheInvalidator implements ModelChangeListener + { + + @Override + public void modelChanged( final ModelChangeEvent event ) + { + if ( event.getEventID() != ModelChangeEvent.MODEL_MODIFIED ) + return; + + event.getSpots() + .stream() + .filter( s -> event.getSpotFlag( s ) == ModelChangeEvent.FLAG_SPOT_MODIFIED ) + .filter( s -> ( s instanceof SpotMesh ) ) + .forEach( s -> ( ( SpotMesh ) s ).resetZSliceCache() ); + } + } + + /** + * Pauses the undo recording. + *

+ * This is useful when a process is going to make a lot of changes to the + * model that we don't want to be recorded in the undo stack. + */ + public void pauseUndo() + { + undoRedoStack.pauseUndo(); + } + + /** + * Resumes the undo recording. + */ + public void resumeUndo() + { + undoRedoStack.resumeUndo(); + } + + /** + * Undo the last action. + */ + public void undo() + { + undoRedoStack.undo(); + } + + /** + * Redo the last undone action. + */ + public void redo() + { + undoRedoStack.redo(); + } + + /** + * Starts the edition of a spot. + *

+ * This method must be called before a spot is modified (moving it, + * changing a feature value, editing its name, ...), so that the undo stack + * can record its current state. For the model update to happen correctly + * and listeners to be notified properly, a call to this method must happen + * within a transaction, as in: + * + *

+	 * model.beginUpdate();
+	 * try {
+	 * 	model.beforeEdit( spot );
+	 * 	... // model modifications here
+	 * } finally {
+	 * 	model.endUpdate();
+	 * }
+	 * 
+ * + * @param spot + * the spot to mark for update + */ + public void beforeEdit( final Spot spot ) + { + // Capture current state of the spot for undo + undoRedoStack.flagForUndo( spot ); + // Enlist for feature update when transaction is marked as finished + spotsUpdated.add( spot ); + final Set< DefaultWeightedEdge > touchingEdges = trackModel.edgesOf( spot ); + if ( null != touchingEdges ) + trackModel.edgesModified.addAll( touchingEdges ); + } } diff --git a/src/main/java/fiji/plugin/trackmate/SelectionModel.java b/src/main/java/fiji/plugin/trackmate/SelectionModel.java index 6c5fea292..7ac5a4951 100644 --- a/src/main/java/fiji/plugin/trackmate/SelectionModel.java +++ b/src/main/java/fiji/plugin/trackmate/SelectionModel.java @@ -28,7 +28,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.Stack; import org.jgrapht.graph.DefaultWeightedEdge; import org.jgrapht.traverse.GraphIterator; @@ -314,67 +313,26 @@ public void selectTrack( final Collection< Spot > spots, final Collection< Defau final HashSet< Spot > lSpotSelection = new HashSet<>(); final HashSet< DefaultWeightedEdge > lEdgeSelection = new HashSet<>(); - if ( direction == 0 ) - { // Unconditionally - for ( final Spot spot : inspectionSpots ) - { - lSpotSelection.add( spot ); - final GraphIterator< Spot, DefaultWeightedEdge > walker = model.getTrackModel().getDepthFirstIterator( spot, false ); - while ( walker.hasNext() ) - { - final Spot target = walker.next(); - lSpotSelection.add( target ); - // Deal with edges - final Set< DefaultWeightedEdge > targetEdges = model.getTrackModel().edgesOf( target ); - for ( final DefaultWeightedEdge targetEdge : targetEdges ) - { - lEdgeSelection.add( targetEdge ); - } - } - } + for ( final Spot spot : inspectionSpots ) + { + lSpotSelection.add( spot ); - } - else - { // Only upward or backward in time - for ( final Spot spot : inspectionSpots ) + final GraphIterator< Spot, DefaultWeightedEdge > walker; + if ( direction == 0 ) + walker = model.getTrackModel().getDepthFirstIterator( spot ); + else if ( direction > 0 ) + walker = model.getTrackModel().getDirectedDepthFirstIterator( spot, true ); + else + walker = model.getTrackModel().getDirectedDepthFirstIterator( spot, false ); + + while ( walker.hasNext() ) { - lSpotSelection.add( spot ); - - /* - * A bit more complicated: we want to walk in only one - * direction, when branching is occurring, we do not want to get - * back in time. - */ - final Stack< Spot > stack = new Stack<>(); - stack.add( spot ); - while ( !stack.isEmpty() ) - { - final Spot inspected = stack.pop(); - final Set< DefaultWeightedEdge > targetEdges = model.getTrackModel().edgesOf( inspected ); - for ( final DefaultWeightedEdge targetEdge : targetEdges ) - { - Spot other; - if ( direction > 0 ) - { - /* - * Upward in time: we just have to search through - * edges using their source spots. - */ - other = model.getTrackModel().getEdgeSource( targetEdge ); - } - else - { - other = model.getTrackModel().getEdgeTarget( targetEdge ); - } - - if ( other != inspected ) - { - lSpotSelection.add( other ); - lEdgeSelection.add( targetEdge ); - stack.add( other ); - } - } - } + final Spot target = walker.next(); + lSpotSelection.add( target ); + // Deal with edges + final Set< DefaultWeightedEdge > targetEdges = model.getTrackModel().edgesOf( target ); + for ( final DefaultWeightedEdge targetEdge : targetEdges ) + lEdgeSelection.add( targetEdge ); } } @@ -388,9 +346,7 @@ public void selectTrack( final Collection< Spot > spots, final Collection< Defau final Spot source = model.getTrackModel().getEdgeSource( edge ); final Spot target = model.getTrackModel().getEdgeTarget( edge ); if ( !( lSpotSelection.contains( source ) && lSpotSelection.contains( target ) ) ) - { edgesToRemove.add( edge ); - } } lEdgeSelection.removeAll( edgesToRemove ); @@ -398,5 +354,4 @@ public void selectTrack( final Collection< Spot > spots, final Collection< Defau addSpotToSelection( lSpotSelection ); addEdgeToSelection( lEdgeSelection ); } - } diff --git a/src/main/java/fiji/plugin/trackmate/Settings.java b/src/main/java/fiji/plugin/trackmate/Settings.java index 79e925cd7..10bcc4fa3 100644 --- a/src/main/java/fiji/plugin/trackmate/Settings.java +++ b/src/main/java/fiji/plugin/trackmate/Settings.java @@ -35,8 +35,9 @@ import fiji.plugin.trackmate.features.spot.SpotAnalyzerFactoryBase; import fiji.plugin.trackmate.features.track.TrackAnalyzer; import fiji.plugin.trackmate.providers.EdgeAnalyzerProvider; +import fiji.plugin.trackmate.providers.Spot2DMorphologyAnalyzerProvider; +import fiji.plugin.trackmate.providers.Spot3DMorphologyAnalyzerProvider; import fiji.plugin.trackmate.providers.SpotAnalyzerProvider; -import fiji.plugin.trackmate.providers.SpotMorphologyAnalyzerProvider; import fiji.plugin.trackmate.providers.TrackAnalyzerProvider; import fiji.plugin.trackmate.tracking.SpotTrackerFactory; import ij.ImagePlus; @@ -532,24 +533,37 @@ public String getErrorMessage() */ public void addAllAnalyzers() { + // Base spot analyzers. final SpotAnalyzerProvider spotAnalyzerProvider = new SpotAnalyzerProvider( imp == null ? 1 : imp.getNChannels() ); final List< String > spotAnalyzerKeys = spotAnalyzerProvider.getKeys(); for ( final String key : spotAnalyzerKeys ) addSpotAnalyzerFactory( spotAnalyzerProvider.getFactory( key ) ); + // Shall we add 2D morphology analyzers? if ( imp != null && DetectionUtils.is2D( imp ) && detectorFactory != null && detectorFactory.has2Dsegmentation() ) { - final SpotMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider = new SpotMorphologyAnalyzerProvider( imp.getNChannels() ); + final Spot2DMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider = new Spot2DMorphologyAnalyzerProvider( imp.getNChannels() ); final List< String > spotMorphologyAnaylyzerKeys = spotMorphologyAnalyzerProvider.getKeys(); for ( final String key : spotMorphologyAnaylyzerKeys ) addSpotAnalyzerFactory( spotMorphologyAnalyzerProvider.getFactory( key ) ); } + // Shall we add 3D morphology analyzers? + if ( imp != null && !DetectionUtils.is2D( imp ) && detectorFactory != null && detectorFactory.has3Dsegmentation() ) + { + final Spot3DMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider = new Spot3DMorphologyAnalyzerProvider( imp.getNChannels() ); + final List< String > spotMorphologyAnaylyzerKeys = spotMorphologyAnalyzerProvider.getKeys(); + for ( final String key : spotMorphologyAnaylyzerKeys ) + addSpotAnalyzerFactory( spotMorphologyAnalyzerProvider.getFactory( key ) ); + } + + // Edge analyzers. final EdgeAnalyzerProvider edgeAnalyzerProvider = new EdgeAnalyzerProvider(); final List< String > edgeAnalyzerKeys = edgeAnalyzerProvider.getKeys(); for ( final String key : edgeAnalyzerKeys ) addEdgeAnalyzer( edgeAnalyzerProvider.getFactory( key ) ); + // Track analyzers. final TrackAnalyzerProvider trackAnalyzerProvider = new TrackAnalyzerProvider(); final List< String > trackAnalyzerKeys = trackAnalyzerProvider.getKeys(); for ( final String key : trackAnalyzerKeys ) diff --git a/src/main/java/fiji/plugin/trackmate/Spot.java b/src/main/java/fiji/plugin/trackmate/Spot.java index e0e1ffb37..70ae40247 100644 --- a/src/main/java/fiji/plugin/trackmate/Spot.java +++ b/src/main/java/fiji/plugin/trackmate/Spot.java @@ -23,39 +23,44 @@ import static fiji.plugin.trackmate.SpotCollection.VISIBILITY; -import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Comparator; -import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import com.google.common.collect.ImmutableMap; + import fiji.plugin.trackmate.util.AlphanumComparator; -import net.imglib2.AbstractEuclideanSpace; +import fiji.plugin.trackmate.util.TMUtils; +import net.imagej.ImgPlus; +import net.imglib2.EuclideanSpace; +import net.imglib2.IterableInterval; +import net.imglib2.Localizable; +import net.imglib2.RandomAccessible; +import net.imglib2.RealInterval; import net.imglib2.RealLocalizable; +import net.imglib2.RealPositionable; +import net.imglib2.type.numeric.RealType; import net.imglib2.util.Util; +import net.imglib2.view.Views; /** - * A {@link RealLocalizable} implementation, used in TrackMate to represent a - * detection. + * Interface for spots, used in TrackMate to represent a detection, or an object + * to be tracked. *

- * On top of being a {@link RealLocalizable}, it can store additional numerical - * named features, with a {@link Map}-like syntax. Constructors enforce the - * specification of the spot location in 3D space (if Z is unused, put 0), the - * spot radius, and the spot quality. This somewhat cumbersome syntax is made to - * avoid any bad surprise with missing features in a subsequent use. The spot - * temporal features ({@link #FRAME} and {@link #POSITION_T}) are set upon - * adding to a {@link SpotCollection}. + * This interface privileges a map of String->Double organization of + * numerical feature, with the X, Y and Z coordinates stored in this map. This + * allows for default implementations for many of the {@link RealLocalizable} + * and {@link RealPositionable} methods of this interface. *

- * Each spot received at creation a unique ID (as an int), used - * later for saving, retrieving and loading. Interfering with this value will - * predictively cause undesired behavior. - * - * @author Jean-Yves Tinevez <jeanyves.tinevez@gmail.com> 2010, 2013 + * They are mainly a 3D {@link RealLocalizable}, that store the object position + * in physical coordinates (um, mm, etc). 2D detections are treated by setting + * the Z coordinate to 0. Time is treated separately, as a feature. * + * @author Jean-Yves Tinevez */ -public class Spot extends AbstractEuclideanSpace implements RealLocalizable, Comparable< Spot > +public interface Spot extends RealLocalizable, RealPositionable, RealInterval, Comparable< Spot >, EuclideanSpace { /* @@ -64,196 +69,70 @@ public class Spot extends AbstractEuclideanSpace implements RealLocalizable, Com public static AtomicInteger IDcounter = new AtomicInteger( -1 ); - /** Store the individual features, and their values. */ - private final ConcurrentHashMap< String, Double > features = new ConcurrentHashMap<>(); - - /** A user-supplied name for this spot. */ - private String name; - - /** This spot ID. */ - private final int ID; - - /** - * The polygon that represents the 2D roi around the spot. Can be - * null if the detector that created this spot does not support - * ROIs or for 3D images. - */ - private SpotRoi roi; - /* - * CONSTRUCTORS + * PUBLIC METHODS */ - /** - * Creates a new spot. - * - * @param x - * the spot X coordinates, in image units. - * @param y - * the spot Y coordinates, in image units. - * @param z - * the spot Z coordinates, in image units. - * @param radius - * the spot radius, in image units. - * @param quality - * the spot quality. - * @param name - * the spot name. - */ - public Spot( final double x, final double y, final double z, final double radius, final double quality, final String name ) - { - super( 3 ); - this.ID = IDcounter.incrementAndGet(); - putFeature( POSITION_X, Double.valueOf( x ) ); - putFeature( POSITION_Y, Double.valueOf( y ) ); - putFeature( POSITION_Z, Double.valueOf( z ) ); - putFeature( RADIUS, Double.valueOf( radius ) ); - putFeature( QUALITY, Double.valueOf( quality ) ); - if ( null == name ) - { - this.name = "ID" + ID; - } - else - { - this.name = name; - } - } + public void accept( SpotVisitor v ); - /** - * Creates a new spot, and gives it a default name. - * - * @param x - * the spot X coordinates, in image units. - * @param y - * the spot Y coordinates, in image units. - * @param z - * the spot Z coordinates, in image units. - * @param radius - * the spot radius, in image units. - * @param quality - * the spot quality. - */ - public Spot( final double x, final double y, final double z, final double radius, final double quality ) + @Override + public default int compareTo( final Spot o ) { - this( x, y, z, radius, quality, null ); + return ID() - o.ID(); } /** - * Creates a new spot, taking its 3D coordinates from a - * {@link RealLocalizable}. The {@link RealLocalizable} must have at least 3 - * dimensions, and must return coordinates in image units. - * - * @param location - * the {@link RealLocalizable} that contains the spot locatiob. - * @param radius - * the spot radius, in image units. - * @param quality - * the spot quality. - * @param name - * the spot name. + * Returns a copy of this spot. The class and all fields will be identical, + * except for the {@link #ID()}. + * + * @return a new spot. */ - public Spot( final RealLocalizable location, final double radius, final double quality, final String name ) - { - this( location.getDoublePosition( 0 ), location.getDoublePosition( 1 ), location.getDoublePosition( 2 ), radius, quality, name ); - } + public Spot copy(); /** - * Creates a new spot, taking its 3D coordinates from a - * {@link RealLocalizable}. The {@link RealLocalizable} must have at least 3 - * dimensions, and must return coordinates in image units. The spot will get - * a default name. - * - * @param location - * the {@link RealLocalizable} that contains the spot locatiob. - * @param radius - * the spot radius, in image units. - * @param quality - * the spot quality. + * Scales the size of this spot by the specified ratio. + * + * @param alpha + * the scale. */ - public Spot( final RealLocalizable location, final double radius, final double quality ) - { - this( location, radius, quality, null ); - } + public void scale( double alpha ); /** - * Creates a new spot, taking its location, its radius, its quality value - * and its name from the specified spot. - * - * @param spot - * the spot to read from. + * Returns an iterable that will iterate over all the pixels contained in + * this spot. + * + * @param ra + * the {@link RandomAccessible} to iterate over. It's the caller + * responsibility to ensure that the {@link RandomAccessible} can + * return values over all the pixels in this spot. + * @param calibration + * the pixel size array, use to map pixel integer coordinates to + * the spot physical coordinates. + * @param + * the type of pixels in the {@link RandomAccessible}. + * @return an iterable. */ - public Spot( final Spot spot ) - { - this( spot, spot.getFeature( RADIUS ), spot.getFeature( QUALITY ), spot.getName() ); - } + public < T extends RealType< T > > IterableInterval< T > iterable( RandomAccessible< T > ra, double calibration[] ); /** - * Blank constructor meant to be used when loading a spot collection from a - * file. Will mess with the {@link #IDcounter} field, so this - * constructor should not be used for normal spot creation. - * - * @param ID - * the spot ID to set + * Returns an iterable that will iterate over all the pixels contained in + * this spot. + * + * @param img + * the ImgPlus to iterate over. + * @param + * the type of pixels in the {@link RandomAccessible}. + * @return an iterable. */ - public Spot( final int ID ) - { - super( 3 ); - this.ID = ID; - synchronized ( IDcounter ) - { - if ( IDcounter.get() < ID ) - { - IDcounter.set( ID ); - } - } - } - - /* - * PUBLIC METHODS - */ - - @Override - public int hashCode() - { - return ID; - } - - @Override - public int compareTo( final Spot o ) + public default < T extends RealType< T > > IterableInterval< T > iterable( final ImgPlus< T > img ) { - return ID - o.ID; - } - - @Override - public boolean equals( final Object other ) - { - if ( other == null ) - return false; - if ( other == this ) - return true; - if ( !( other instanceof Spot ) ) - return false; - final Spot os = ( Spot ) other; - return os.ID == this.ID; - } - - public void setRoi( final SpotRoi roi ) - { - this.roi = roi; - } - - public SpotRoi getRoi() - { - return roi; + return iterable( Views.extendMirrorSingle( img ), TMUtils.getSpatialCalibration( img ) ); } /** * @return the name for this Spot. */ - public String getName() - { - return this.name; - } + public String getName(); /** * Set the name of this Spot. @@ -261,36 +140,24 @@ public String getName() * @param name * the name to use. */ - public void setName( final String name ) - { - this.name = name; - } - - public int ID() - { - return ID; - } + public void setName( final String name ); - @Override - public String toString() - { - String str; - if ( null == name || name.equals( "" ) ) - str = "ID" + ID; - else - str = name; - return str; - } + /** + * Returns the unique ID of this spot. The ID is unique within a session. + * + * @return the spot ID. + */ + public int ID(); /** * Return a string representation of this spot, with calculated features. * * @return a string representation of the spot. */ - public String echo() + public default String echo() { final StringBuilder s = new StringBuilder(); - + final String name = getName(); // Name if ( null == name ) s.append( "Spot: \n" ); @@ -306,6 +173,7 @@ public String echo() s.append( "Position: " + Util.printCoordinates( coordinates ) + "\n" ); // Feature list + final Map< String, Double > features = getFeatures(); if ( null == features || features.size() < 1 ) s.append( "No features calculated\n" ); else @@ -336,10 +204,7 @@ public String echo() * * @return a map of {@link String}s to {@link Double}s. */ - public Map< String, Double > getFeatures() - { - return features; - } + public Map< String, Double > getFeatures(); /** * Returns the value corresponding to the specified spot feature. @@ -349,10 +214,7 @@ public Map< String, Double > getFeatures() * @return the feature value, as a {@link Double}. Will be null * if it has not been set. */ - public Double getFeature( final String feature ) - { - return features.get( feature ); - } + public Double getFeature( final String feature ); /** * Stores the specified feature value for this spot. @@ -363,28 +225,36 @@ public Double getFeature( final String feature ) * the value to store, as a {@link Double}. Using * null will have unpredicted outcomes. */ - public void putFeature( final String feature, final Double value ) - { - features.put( feature, value ); - } + public void putFeature( final String feature, final Double value ); /** - * Copy the listed features of the source spot to this spot. - * + * Copy some of the features values of the specified spot to this spot. + * * @param src - * the source spot. + * the spot to copy feature values from. * @param features - * the features to copy. + * the collection of feature keys to copy. */ - public void copyFeatures( final Spot src, final Map< String, Double > features ) + public default void copyFeaturesFrom( final Spot src, final Collection< String > features ) { if ( null == features || features.isEmpty() ) return; - for ( final String feat : features.keySet() ) + for ( final String feat : features ) putFeature( feat, src.getFeature( feat ) ); } + /** + * Copy all the features value from the specified spot to this spot. + * + * @param src + * the spot to copy feature values from. + */ + public default void copyFeaturesFrom( final Spot src ) + { + copyFeaturesFrom( src, src.getFeatures().keySet() ); + } + /** * Returns the difference of the feature value for this spot with the one of * the specified spot. By construction, this operation is anti-symmetric ( @@ -399,9 +269,9 @@ public void copyFeatures( final Spot src, final Map< String, Double > features ) * the name of the feature to use for calculation. * @return the difference in feature value. */ - public double diffTo( final Spot s, final String feature ) + public default double diffTo( final Spot s, final String feature ) { - final double f1 = features.get( feature ).doubleValue(); + final double f1 = getFeature( feature ).doubleValue(); final double f2 = s.getFeature( feature ).doubleValue(); return f1 - f2; } @@ -427,9 +297,9 @@ public double diffTo( final Spot s, final String feature ) * the name of the feature to use for calculation. * @return the absolute normalized difference feature value. */ - public double normalizeDiffTo( final Spot s, final String feature ) + public default double normalizeDiffTo( final Spot s, final String feature ) { - final double a = features.get( feature ).doubleValue(); + final double a = getFeature( feature ).doubleValue(); final double b = s.getFeature( feature ).doubleValue(); if ( a == -b ) return 0d; @@ -444,7 +314,7 @@ public double normalizeDiffTo( final Spot s, final String feature ) * the spot to compute the square distance to. * @return the square distance as a double. */ - public double squareDistanceTo( final RealLocalizable s ) + public default double squareDistanceTo( final RealLocalizable s ) { double sumSquared = 0d; for ( int d = 0; d < 3; d++ ) @@ -488,97 +358,232 @@ public double squareDistanceTo( final RealLocalizable s ) public final static String[] POSITION_FEATURES = new String[] { POSITION_X, POSITION_Y, POSITION_Z }; /** - * The 7 privileged spot features that must be set by a spot detector: + * The 8 privileged spot features that must be set by a spot detector: * {@link #QUALITY}, {@link #POSITION_X}, {@link #POSITION_Y}, - * {@link #POSITION_Z}, {@link #POSITION_Z}, {@link #RADIUS}, {@link #FRAME} - * . + * {@link #POSITION_Z}, {@link #POSITION_Z}, {@link #RADIUS}, + * {@link #FRAME}, {@link SpotCollection#VISIBILITY}. */ - public final static Collection< String > FEATURES = new ArrayList<>( 7 ); - - /** The 7 privileged spot feature names. */ - public final static Map< String, String > FEATURE_NAMES = new HashMap<>( 7 ); - - /** The 7 privileged spot feature short names. */ - public final static Map< String, String > FEATURE_SHORT_NAMES = new HashMap<>( 7 ); - - /** The 7 privileged spot feature dimensions. */ - public final static Map< String, Dimension > FEATURE_DIMENSIONS = new HashMap<>( 7 ); - - /** The 7 privileged spot feature isInt flags. */ - public final static Map< String, Boolean > IS_INT = new HashMap<>( 7 ); - - static - { - FEATURES.add( QUALITY ); - FEATURES.add( POSITION_X ); - FEATURES.add( POSITION_Y ); - FEATURES.add( POSITION_Z ); - FEATURES.add( POSITION_T ); - FEATURES.add( FRAME ); - FEATURES.add( RADIUS ); - FEATURES.add( SpotCollection.VISIBILITY ); - - FEATURE_NAMES.put( POSITION_X, "X" ); - FEATURE_NAMES.put( POSITION_Y, "Y" ); - FEATURE_NAMES.put( POSITION_Z, "Z" ); - FEATURE_NAMES.put( POSITION_T, "T" ); - FEATURE_NAMES.put( FRAME, "Frame" ); - FEATURE_NAMES.put( RADIUS, "Radius" ); - FEATURE_NAMES.put( QUALITY, "Quality" ); - FEATURE_NAMES.put( VISIBILITY, "Visibility" ); - - FEATURE_SHORT_NAMES.put( POSITION_X, "X" ); - FEATURE_SHORT_NAMES.put( POSITION_Y, "Y" ); - FEATURE_SHORT_NAMES.put( POSITION_Z, "Z" ); - FEATURE_SHORT_NAMES.put( POSITION_T, "T" ); - FEATURE_SHORT_NAMES.put( FRAME, "Frame" ); - FEATURE_SHORT_NAMES.put( RADIUS, "R" ); - FEATURE_SHORT_NAMES.put( QUALITY, "Quality" ); - FEATURE_SHORT_NAMES.put( VISIBILITY, "Visibility" ); - - FEATURE_DIMENSIONS.put( POSITION_X, Dimension.POSITION ); - FEATURE_DIMENSIONS.put( POSITION_Y, Dimension.POSITION ); - FEATURE_DIMENSIONS.put( POSITION_Z, Dimension.POSITION ); - FEATURE_DIMENSIONS.put( POSITION_T, Dimension.TIME ); - FEATURE_DIMENSIONS.put( FRAME, Dimension.NONE ); - FEATURE_DIMENSIONS.put( RADIUS, Dimension.LENGTH ); - FEATURE_DIMENSIONS.put( QUALITY, Dimension.QUALITY ); - FEATURE_DIMENSIONS.put( VISIBILITY, Dimension.NONE ); - - IS_INT.put( POSITION_X, Boolean.FALSE ); - IS_INT.put( POSITION_Y, Boolean.FALSE ); - IS_INT.put( POSITION_Z, Boolean.FALSE ); - IS_INT.put( POSITION_T, Boolean.FALSE ); - IS_INT.put( FRAME, Boolean.TRUE ); - IS_INT.put( RADIUS, Boolean.FALSE ); - IS_INT.put( QUALITY, Boolean.FALSE ); - IS_INT.put( VISIBILITY, Boolean.TRUE ); + public final static Collection< String > FEATURES = Arrays.asList( QUALITY, + POSITION_X, POSITION_Y, POSITION_Z, POSITION_T, FRAME, RADIUS, SpotCollection.VISIBILITY ); + + /** The 8 privileged spot feature names. */ + public final static Map< String, String > FEATURE_NAMES = ImmutableMap.of( + POSITION_X, "X", + POSITION_Y, "Y", + POSITION_Z, "Z", + POSITION_T, "T", + FRAME, "Frame", + RADIUS, "Radius", + QUALITY, "Quality", + VISIBILITY, "Visibility" ); + + /** The 8 privileged spot feature short names. */ + public final static Map< String, String > FEATURE_SHORT_NAMES = ImmutableMap.of( + POSITION_X, "X", + POSITION_Y, "Y", + POSITION_Z, "Z", + POSITION_T, "T", + FRAME, "Frame", + RADIUS, "R", + QUALITY, "Quality", + VISIBILITY, "Visibility" ); + + /** The 8 privileged spot feature dimensions. */ + public final static Map< String, Dimension > FEATURE_DIMENSIONS = ImmutableMap.of( + POSITION_X, Dimension.POSITION, + POSITION_Y, Dimension.POSITION, + POSITION_Z, Dimension.POSITION, + POSITION_T, Dimension.TIME, + FRAME, Dimension.NONE, + RADIUS, Dimension.LENGTH, + QUALITY, Dimension.QUALITY, + VISIBILITY, Dimension.NONE ); + + /** The 8 privileged spot feature isInt flags. */ + public final static Map< String, Boolean > IS_INT = ImmutableMap.of( + POSITION_X, Boolean.FALSE, + POSITION_Y, Boolean.FALSE, + POSITION_Z, Boolean.FALSE, + POSITION_T, Boolean.FALSE, + FRAME, Boolean.TRUE, + RADIUS, Boolean.FALSE, + QUALITY, Boolean.FALSE, + VISIBILITY, Boolean.TRUE ); + + /* + * REALPOSITIONABLE, REAlLOCALIZABLE + */ + + @Override + default int numDimensions() + { + return 3; + } + + @Override + public default void move( final float distance, final int d ) + { + putFeature( POSITION_FEATURES[ d ], getFeature( POSITION_FEATURES[ d ] ) + distance ); + } + + @Override + public default void move( final double distance, final int d ) + { + putFeature( POSITION_FEATURES[ d ], getFeature( POSITION_FEATURES[ d ] ) + distance ); } @Override - public void localize( final float[] position ) + public default void move( final RealLocalizable distance ) { - assert ( position.length >= n ); - for ( int d = 0; d < n; ++d ) + for ( int d = 0; d < 3; d++ ) + putFeature( POSITION_FEATURES[ d ], getFeature( POSITION_FEATURES[ d ] ) + distance.getDoublePosition( d ) ); + } + + @Override + public default void move( final float[] distance ) + { + for ( int d = 0; d < 3; d++ ) + putFeature( POSITION_FEATURES[ d ], getFeature( POSITION_FEATURES[ d ] ) + distance[ d ] ); + } + + @Override + public default void move( final double[] distance ) + { + for ( int d = 0; d < 3; d++ ) + putFeature( POSITION_FEATURES[ d ], getFeature( POSITION_FEATURES[ d ] ) + distance[ d ] ); + } + + @Override + public default void setPosition( final RealLocalizable position ) + { + for ( int d = 0; d < 3; d++ ) + putFeature( POSITION_FEATURES[ d ], position.getDoublePosition( d ) ); + } + + @Override + public default void setPosition( final float[] position ) + { + for ( int d = 0; d < 3; d++ ) + putFeature( POSITION_FEATURES[ d ], ( double ) position[ d ] ); + } + + @Override + public default void setPosition( final double[] position ) + { + for ( int d = 0; d < 3; d++ ) + putFeature( POSITION_FEATURES[ d ], position[ d ] ); + } + + @Override + public default void setPosition( final float position, final int d ) + { + putFeature( POSITION_FEATURES[ d ], ( double ) position ); + } + + @Override + public default void setPosition( final double position, final int d ) + { + putFeature( POSITION_FEATURES[ d ], position ); + } + + @Override + public default void fwd( final int d ) + { + move( 1., d ); + } + + @Override + public default void bck( final int d ) + { + move( -1., d ); + } + + @Override + public default void move( final int distance, final int d ) + { + move( ( double ) distance, d ); + } + + @Override + public default void move( final long distance, final int d ) + { + move( ( double ) distance, d ); + } + + @Override + public default void move( final Localizable distance ) + { + move( ( RealLocalizable ) distance ); + } + + @Override + public default void move( final int[] distance ) + { + for ( int d = 0; d < 3; d++ ) + putFeature( POSITION_FEATURES[ d ], getFeature( POSITION_FEATURES[ d ] + distance[ d ] ) ); + } + + @Override + public default void move( final long[] distance ) + { + for ( int d = 0; d < 3; d++ ) + putFeature( POSITION_FEATURES[ d ], getFeature( POSITION_FEATURES[ d ] + distance[ d ] ) ); + } + + @Override + public default void setPosition( final Localizable position ) + { + setPosition( ( RealLocalizable ) position ); + } + + @Override + public default void setPosition( final int[] position ) + { + for ( int d = 0; d < 3; d++ ) + putFeature( POSITION_FEATURES[ d ], ( double ) position[ d ] ); + } + + @Override + public default void setPosition( final long[] position ) + { + for ( int d = 0; d < 3; d++ ) + putFeature( POSITION_FEATURES[ d ], ( double ) position[ d ] ); + } + + @Override + public default void setPosition( final int position, final int d ) + { + putFeature( POSITION_FEATURES[ d ], ( double ) position ); + } + + @Override + public default void setPosition( final long position, final int d ) + { + putFeature( POSITION_FEATURES[ d ], ( double ) position ); + } + + @Override + public default void localize( final float[] position ) + { + for ( int d = 0; d < 3; ++d ) position[ d ] = getFloatPosition( d ); } @Override - public void localize( final double[] position ) + public default void localize( final double[] position ) { - assert ( position.length >= n ); - for ( int d = 0; d < n; ++d ) + for ( int d = 0; d < 3; ++d ) position[ d ] = getDoublePosition( d ); } @Override - public float getFloatPosition( final int d ) + public default float getFloatPosition( final int d ) { return ( float ) getDoublePosition( d ); } @Override - public double getDoublePosition( final int d ) + public default double getDoublePosition( final int d ) { return getFeature( POSITION_FEATURES[ d ] ); } @@ -596,7 +601,7 @@ public double getDoublePosition( final int d ) * feature. * @return a new {@link Comparator}. */ - public final static Comparator< Spot > featureComparator( final String feature ) + public static Comparator< Spot > featureComparator( final String feature ) { final Comparator< Spot > comparator = new Comparator< Spot >() { @@ -635,4 +640,23 @@ public int compare( final Spot o1, final Spot o2 ) return comparator.compare( o1.getName(), o2.getName() ); } }; + + public static interface SpotVisitor + { + + default void visit( final SpotBase spot ) + { + throw new UnsupportedOperationException( "SpotBase not supported." ); + } + + default void visit( final SpotRoi spot ) + { + throw new UnsupportedOperationException( "SpotRoi not supported." ); + } + + default void visit( final SpotMesh spot ) + { + throw new UnsupportedOperationException( "SpotMesh not supported." ); + } + } } diff --git a/src/main/java/fiji/plugin/trackmate/SpotBase.java b/src/main/java/fiji/plugin/trackmate/SpotBase.java new file mode 100644 index 000000000..c92adceaa --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/SpotBase.java @@ -0,0 +1,336 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import fiji.plugin.trackmate.util.SpotNeighborhood; +import net.imglib2.AbstractEuclideanSpace; +import net.imglib2.FinalInterval; +import net.imglib2.Interval; +import net.imglib2.IterableInterval; +import net.imglib2.RandomAccessible; +import net.imglib2.RealLocalizable; +import net.imglib2.type.numeric.RealType; +import net.imglib2.view.Views; + +/** + * A {@link RealLocalizable} implementation of {@link Spot}, used in TrackMate + * to represent a detection. This concrete implementation has the simplest + * shape: a spot is a sphere of fixed radius. + *

+ * On top of being a {@link RealLocalizable}, it can store additional numerical + * named features, with a {@link Map}-like syntax. Constructors enforce the + * specification of the spot location in 3D space (if Z is unused, put 0), the + * spot radius, and the spot quality. This somewhat cumbersome syntax is made to + * avoid any bad surprise with missing features in a subsequent use. The spot + * temporal features ({@link #FRAME} and {@link #POSITION_T}) are set upon + * adding to a {@link SpotCollection}. + *

+ * Each spot received at creation a unique ID (as an int), used + * later for saving, retrieving and loading. Interfering with this value will + * predictively cause undesired behavior. + * + * @author Jean-Yves Tinevez + * + */ +public class SpotBase extends AbstractEuclideanSpace implements Spot +{ + + /* + * FIELDS + */ + + public static AtomicInteger IDcounter = new AtomicInteger( -1 ); + + /** Store the individual features, and their values. */ + private final ConcurrentHashMap< String, Double > features = new ConcurrentHashMap<>(); + + /** A user-supplied name for this spot. */ + private String name; + + /** This spot ID. */ + private final int ID; + + /* + * CONSTRUCTORS + */ + + /** + * Creates a new spot. + * + * @param x + * the spot X coordinates, in image units. + * @param y + * the spot Y coordinates, in image units. + * @param z + * the spot Z coordinates, in image units. + * @param radius + * the spot radius, in image units. + * @param quality + * the spot quality. + * @param name + * the spot name. + */ + public SpotBase( final double x, final double y, final double z, final double radius, final double quality, final String name ) + { + super( 3 ); + this.ID = IDcounter.incrementAndGet(); + putFeature( POSITION_X, Double.valueOf( x ) ); + putFeature( POSITION_Y, Double.valueOf( y ) ); + putFeature( POSITION_Z, Double.valueOf( z ) ); + putFeature( RADIUS, Double.valueOf( radius ) ); + putFeature( QUALITY, Double.valueOf( quality ) ); + if ( null == name ) + { + this.name = "ID" + ID; + } + else + { + this.name = name; + } + } + + /** + * Creates a new spot, and gives it a default name. + * + * @param x + * the spot X coordinates, in image units. + * @param y + * the spot Y coordinates, in image units. + * @param z + * the spot Z coordinates, in image units. + * @param radius + * the spot radius, in image units. + * @param quality + * the spot quality. + */ + public SpotBase( final double x, final double y, final double z, final double radius, final double quality ) + { + this( x, y, z, radius, quality, null ); + } + + /** + * Creates a new spot, taking its 3D coordinates from a + * {@link RealLocalizable}. The {@link RealLocalizable} must have at least 3 + * dimensions, and must return coordinates in image units. + * + * @param location + * the {@link RealLocalizable} that contains the spot locatiob. + * @param radius + * the spot radius, in image units. + * @param quality + * the spot quality. + * @param name + * the spot name. + */ + public SpotBase( final RealLocalizable location, final double radius, final double quality, final String name ) + { + this( location.getDoublePosition( 0 ), location.getDoublePosition( 1 ), location.getDoublePosition( 2 ), radius, quality, name ); + } + + /** + * Creates a new spot, taking its 3D coordinates from a + * {@link RealLocalizable}. The {@link RealLocalizable} must have at least 3 + * dimensions, and must return coordinates in image units. The spot will get + * a default name. + * + * @param location + * the {@link RealLocalizable} that contains the spot locatiob. + * @param radius + * the spot radius, in image units. + * @param quality + * the spot quality. + */ + public SpotBase( final RealLocalizable location, final double radius, final double quality ) + { + this( location, radius, quality, null ); + } + + /** + * Creates a new spot, taking its location, its radius, its quality value + * and its name from the specified spot. + * + * @param oldSpot + * the spot to read from. + */ + public SpotBase( final Spot oldSpot ) + { + this( oldSpot, oldSpot.getFeature( RADIUS ), oldSpot.getFeature( QUALITY ), oldSpot.getName() ); + } + + /** + * Blank constructor meant to be used when loading a spot collection from a + * file. Will mess with the {@link #IDcounter} field, so this + * constructor should not be used for normal spot creation. + * + * @param ID + * the spot ID to set + */ + public SpotBase( final int ID ) + { + super( 3 ); + this.ID = ID; + synchronized ( IDcounter ) + { + if ( IDcounter.get() < ID ) + { + IDcounter.set( ID ); + } + } + } + + /* + * PUBLIC METHODS + */ + + @Override + public void accept( final SpotVisitor v ) + { + v.visit( this ); + } + + @Override + public SpotBase copy() + { + final SpotBase o = new SpotBase( this ); + o.copyFeaturesFrom( this ); + return o; + } + + @Override + public void scale( final double alpha ) + { + final double radius = getFeature( Spot.RADIUS ); + final double newRadius = radius * alpha; + putFeature( Spot.RADIUS, newRadius ); + } + + @Override + public int hashCode() + { + return ID; + } + + @Override + public boolean equals( final Object other ) + { + if ( other == null ) + return false; + if ( other == this ) + return true; + if ( !( other instanceof SpotBase ) ) + return false; + final SpotBase os = ( SpotBase ) other; + return os.ID == this.ID; + } + + @Override + public String getName() + { + return this.name; + } + + @Override + public void setName( final String name ) + { + this.name = name; + } + + @Override + public int ID() + { + return ID; + } + + @Override + public String toString() + { + String str; + if ( null == name || name.equals( "" ) ) + str = "ID" + ID; + else + str = name; + return str; + } + + /* + * FEATURE RELATED METHODS + */ + + @Override + public Map< String, Double > getFeatures() + { + return features; + } + + @Override + public Double getFeature( final String feature ) + { + return features.get( feature ); + } + + @Override + public void putFeature( final String feature, final Double value ) + { + features.put( feature, value ); + } + + @Override + public double realMin( final int d ) + { + return getDoublePosition( d ) - getFeature( SpotBase.RADIUS ); + } + + @Override + public double realMax( final int d ) + { + return getDoublePosition( d ) + getFeature( SpotBase.RADIUS ); + } + + @Override + public < T extends RealType< T > > IterableInterval< T > iterable( final RandomAccessible< T > ra, final double[] calibration ) + { + final double r = features.get( Spot.RADIUS ).doubleValue(); + if ( r / calibration[ 0 ] <= 1. && r / calibration[ 2 ] <= 1. ) + return makeSinglePixelIterable( this, ra, calibration ); + + return new SpotNeighborhood<>( this, ra, calibration ); + } + + private static < T > IterableInterval< T > makeSinglePixelIterable( final RealLocalizable center, final RandomAccessible< T > img, final double[] calibration ) + { + final long[] min = new long[ img.numDimensions() ]; + final long[] max = new long[ img.numDimensions() ]; + for ( int d = 0; d < min.length; d++ ) + { + final long cx = Math.round( center.getDoublePosition( d ) / calibration[ d ] ); + min[ d ] = cx; + max[ d ] = cx + 1; + } + + final Interval interval = new FinalInterval( min, max ); + return Views.interval( img, interval ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/SpotCollection.java b/src/main/java/fiji/plugin/trackmate/SpotCollection.java index 98d9c64f2..c94d872cc 100644 --- a/src/main/java/fiji/plugin/trackmate/SpotCollection.java +++ b/src/main/java/fiji/plugin/trackmate/SpotCollection.java @@ -35,6 +35,7 @@ import fiji.plugin.trackmate.features.FeatureFilter; import fiji.plugin.trackmate.util.Threads; +import net.imglib2.RealLocalizable; import net.imglib2.algorithm.MultiThreaded; /** @@ -379,7 +380,7 @@ public final Spot getClosestSpot( final Spot location, final int frame, final bo } /** - * Returns the {@link Spot} at the given location (encoded as a Spot), + * Returns the {@link Spot} at the given location (in world coordinates), * contained in the frame frame. A spot is returned only * if there exists a spot such that the given location is within the spot * radius. Otherwise null is returned. @@ -395,7 +396,7 @@ public final Spot getClosestSpot( final Spot location, final int frame, final bo * radius, member of this collection, or null is such a * spots cannot be found. */ - public final Spot getSpotAt( final Spot location, final int frame, final boolean visibleSpotsOnly ) + public final Spot getSpotAt( final RealLocalizable location, final int frame, final boolean visibleSpotsOnly ) { final Set< Spot > spots = content.get( frame ); if ( null == spots || spots.isEmpty() ) diff --git a/src/main/java/fiji/plugin/trackmate/SpotMesh.java b/src/main/java/fiji/plugin/trackmate/SpotMesh.java new file mode 100644 index 000000000..b2f2de0fd --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/SpotMesh.java @@ -0,0 +1,385 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import fiji.plugin.trackmate.util.mesh.SpotMeshIterable; +import net.imglib2.IterableInterval; +import net.imglib2.RandomAccessible; +import net.imglib2.RealInterval; +import net.imglib2.RealLocalizable; +import net.imglib2.RealPoint; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.MeshStats; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.Vertices; +import net.imglib2.mesh.alg.zslicer.RamerDouglasPeucker; +import net.imglib2.mesh.alg.zslicer.Slice; +import net.imglib2.mesh.alg.zslicer.ZSlicer; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.mesh.view.ReadOnlyMesh; +import net.imglib2.type.numeric.RealType; + +public class SpotMesh extends SpotBase +{ + + /** + * The mesh representing the 3D contour of the spot. The mesh is centered on + * (0, 0, 0) and the true position of its vertices is obtained by adding the + * spot center. + */ + private BufferMesh mesh; + + private Map< Integer, Slice > sliceMap; + + /** The bounding-box, centered on (0,0,0) of this object. */ + private RealInterval boundingBox; + + public SpotMesh( + final Mesh mesh, + final double quality ) + { + this( mesh, quality, null ); + } + + /** + * Creates a new spot from the specified mesh. Its position and radius are + * calculated from the mesh. + * + * @param quality + * the spot quality. + * @param name + * the spot name. + * @param m + * the mesh to create the spot from. + */ + public SpotMesh( + final Mesh m, + final double quality, + final String name ) + { + // Dummy coordinates and radius. + super( 0., 0., 0., 0., quality, name ); + setMesh( m ); + } + + @Override + public void accept( final SpotVisitor v ) + { + v.visit( this ); + } + + public void setMesh( final Mesh m ) + { + // Store a copy in a Buffer mesh. + final BufferMesh mesh = new BufferMesh( m.vertices().size(), m.triangles().size() ); + Meshes.calculateNormals( m, mesh ); + this.mesh = mesh; + + // Compute new true center. + final RealPoint center = Meshes.center( mesh ); + + // Reposition the spot. + setPosition( center ); + + // Shift mesh to (0, 0, 0). + final net.imglib2.mesh.Vertices vertices = mesh.vertices(); + final long nVertices = vertices.size(); + for ( long i = 0; i < nVertices; i++ ) + vertices.setPositionf( i, + vertices.xf( i ) - center.getFloatPosition( 0 ), + vertices.yf( i ) - center.getFloatPosition( 1 ), + vertices.zf( i ) - center.getFloatPosition( 2 ) ); + + // Compute radius. + final double r = radius( mesh ); + putFeature( Spot.RADIUS, r ); + + // Bounding box, also centered on (0,0,0) + this.boundingBox = Meshes.boundingBox( mesh ); + + // Slice cache. + resetZSliceCache(); + } + + public RealInterval getBoundingBox() + { + return boundingBox; + } + + /** + * This constructor is only used for deserializing a model from a TrackMate + * file. It messes with the ID of the spots and should be not used + * otherwise. + * + * @param ID + * the ID to create the spot with. + * @param mesh + * the mesh used to create the spot. + */ + public SpotMesh( final int ID, final BufferMesh mesh ) + { + super( ID ); + this.mesh = mesh; + final RealPoint center = Meshes.center( mesh ); + + // Reposition the spot. + setPosition( center ); + + // Shift mesh to (0, 0, 0). + final Vertices vertices = mesh.vertices(); + final long nVertices = vertices.size(); + for ( long i = 0; i < nVertices; i++ ) + vertices.setPositionf( i, + vertices.xf( i ) - center.getFloatPosition( 0 ), + vertices.yf( i ) - center.getFloatPosition( 1 ), + vertices.zf( i ) - center.getFloatPosition( 2 ) ); + + // Compute radius. + final double r = radius( mesh ); + putFeature( Spot.RADIUS, r ); + + // Bounding box, also centered on (0,0,0) + this.boundingBox = Meshes.boundingBox( mesh ); + } + + /** + * Returns a read-only view of the mesh stored in this spot. + *

+ * The coordinates of the vertices are relative to the spot center. That is: + * the coordinates are centered on (0,0,0). + * + * @return the mesh. + */ + public Mesh getMesh() + { + return ReadOnlyMesh.readOnly( mesh ); + } + + @Override + public double realMax( final int d ) + { + return getDoublePosition( d ) + boundingBox.realMax( d ); + } + + @Override + public double realMin( final int d ) + { + return getDoublePosition( d ) + boundingBox.realMin( d ); + } + + @Override + public < T extends RealType< T > > IterableInterval< T > iterable( final RandomAccessible< T > ra, final double[] calibration ) + { + return new SpotMeshIterable<>( ra, this, calibration ); + } + + /** + * Gets the slice resulting from the intersection of the mesh with the XY + * plane with the specified z position, in pixel coordinates, 0-based. + *

+ * Relies on a sort of Z-slice cache. To regenerate it if needed, we need + * the specification of a scale in XY and Z specified here. + * + * @param zSlice + * the Z position of the slice, in pixel coordinates, 0-based. + * @param xyScale + * a measure of the mesh scale along XY, for instance the pixel + * size in XY that it was generated from. Used to correct and + * simplify the slice contours. + * @param zScale + * the pixel size in Z, used to generate the Z planes spacing. + * @return the slice, or null if the mesh does not intersect + * with the specified XY plane. The slice XY coordinates are + * centered so (0,0) corresponds to the mesh center. + */ + public Slice getZSlice( final int zSlice, final double xyScale, final double zScale ) + { + if ( sliceMap == null ) + sliceMap = buildSliceMap( mesh, boundingBox, this, xyScale, zScale ); + + return sliceMap.get( Integer.valueOf( zSlice ) ); + } + + /** + * Invalidates the Z-slices cache. This will force its recomputation. To be + * called after the spot has changed size or Z position. + */ + public void resetZSliceCache() + { + sliceMap = null; + } + + /** + * Returns the radius of the equivalent sphere with the same volume that of + * the specified mesh. + * + * @param mesh + * the mesh. + * @return the radius in physical units. + */ + public static final double radius( final Mesh mesh ) + { + return Math.pow( 3. * MeshStats.volume( mesh ) / ( 4 * Math.PI ), 1. / 3. ); + } + + public double radius() + { + return radius( mesh ); + } + + /** + * Returns the volume of this mesh. + * + * @return the volume in physical units. + */ + public double volume() + { + return MeshStats.volume( mesh ); + } + + @Override + public void scale( final double alpha ) + { + super.scale( alpha ); + final net.imglib2.mesh.Vertices vertices = mesh.vertices(); + final long nVertices = vertices.size(); + final float fAlpha = ( float ) alpha; + for ( int v = 0; v < nVertices; v++ ) + { + // Exploit the fact that the mesh is centered on (0,0,0). + final float xa = fAlpha * vertices.xf( v ); + final float ya = fAlpha * vertices.yf( v ); + final float za = fAlpha * vertices.zf( v ); + vertices.setPositionf( v, xa, ya, za ); + } + this.boundingBox = Meshes.boundingBox( mesh ); + resetZSliceCache(); + } + + @Override + public SpotMesh copy() + { + final BufferMesh meshCopy = new BufferMesh( mesh.vertices().size(), mesh.triangles().size() ); + Meshes.copy( this.mesh, meshCopy ); + return new SpotMesh( meshCopy, getFeature( Spot.QUALITY ), getName() ); + } + + @Override + public String toString() + { + final StringBuilder str = new StringBuilder( super.toString() ); + + str.append( "\nBounding-box" ); + str.append( String.format( "\n%5s: %7.2f -> %7.2f", "X", boundingBox.realMin( 0 ), boundingBox.realMax( 0 ) ) ); + str.append( String.format( "\n%5s: %7.2f -> %7.2f", "Y", boundingBox.realMin( 1 ), boundingBox.realMax( 1 ) ) ); + str.append( String.format( "\n%5s: %7.2f -> %7.2f", "Z", boundingBox.realMin( 2 ), boundingBox.realMax( 2 ) ) ); + + final net.imglib2.mesh.Vertices vertices = mesh.vertices(); + final long nVertices = vertices.size(); + str.append( "\nV (" + nVertices + "):" ); + for ( long i = 0; i < nVertices; i++ ) + str.append( String.format( "\n%5d: %7.2f %7.2f %7.2f", + i, vertices.x( i ), vertices.y( i ), vertices.z( i ) ) ); + + final net.imglib2.mesh.Triangles triangles = mesh.triangles(); + final long nTriangles = triangles.size(); + str.append( "\nF (" + nTriangles + "):" ); + for ( long i = 0; i < nTriangles; i++ ) + str.append( String.format( "\n%5d: %5d %5d %5d", + i, triangles.vertex0( i ), triangles.vertex1( i ), triangles.vertex2( i ) ) ); + + return str.toString(); + } + + /** + * Computes the intersections of the specified mesh with the multiple + * Z-slice at integer coordinates corresponding to 1-pixel spacing in + * Z. This is why we need to have the calibration array. The + * slices are centered on (0,0) the mesh center. + * + * @param mesh + * the mesh to reslice, centered on (0,0,0). + * @param boundingBox + * its bounding box, also centered on (0,0,0). + * @param center + * the mesh center true position. Needed to reposition it in Z. + * @param calibration + * the pixel size array, needed to compute the 1-pixel spacing. + * @return a map from slice position (integer, pixel coordinates) to slices. + */ + private static final Map< Integer, Slice > buildSliceMap( + final Mesh mesh, + final RealInterval boundingBox, + final RealLocalizable center, + final double xyScale, + final double zScale ) + { + /* + * Let's try to have everything relative to (0,0,0), so that we do not + * have to recompute the Z slices when the mesh is moved in X and Y. + */ + + /* + * Compute the Z integers, in pixel coordinates, of the mesh + * intersection. These coordinates are absolute value (relative to mesh + * center). + */ + final double zc = center.getDoublePosition( 2 ); + final int minZ = ( int ) Math.ceil( ( boundingBox.realMin( 2 ) + zc ) / zScale ); + final int maxZ = ( int ) Math.floor( ( boundingBox.realMax( 2 ) + zc ) / zScale ); + final int[] zSlices = new int[ maxZ - minZ + 1 ]; + for ( int i = 0; i < zSlices.length; i++ ) + zSlices[ i ] = ( minZ + i );// pixel coords, absolute value + + /* + * Compute equivalent Z positions in physical units, relative to + * (0,0,0), of these intersections. + */ + final double[] zPos = new double[ zSlices.length ]; + for ( int i = 0; i < zPos.length; i++ ) + zPos[ i ] = zSlices[ i ] * zScale - zc; + + // Compute the slices. They will be centered on (0,0) in XY. + final List< Slice > slices = ZSlicer.slices( + mesh, + zPos, + zScale ); + + // Simplify below 1/4th of a pixel. + final double epsilon = xyScale * 0.25; + final List< Slice > simplifiedSlices = slices.stream() + .map( s -> RamerDouglasPeucker.simplify( s, epsilon ) ) + .collect( Collectors.toList() ); + + // Store in a map of Z slice -> slice. + final Map< Integer, Slice > sliceMap = new HashMap<>(); + for ( int i = 0; i < zSlices.length; i++ ) + sliceMap.put( Integer.valueOf( zSlices[ i ] ), simplifiedSlices.get( i ) ); + + return sliceMap; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/SpotRoi.java b/src/main/java/fiji/plugin/trackmate/SpotRoi.java index 3f081e8c1..ecfff1d45 100644 --- a/src/main/java/fiji/plugin/trackmate/SpotRoi.java +++ b/src/main/java/fiji/plugin/trackmate/SpotRoi.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 * . @@ -22,120 +22,281 @@ package fiji.plugin.trackmate; import java.util.Arrays; +import java.util.Iterator; -import net.imagej.ImgPlus; +import gnu.trove.list.array.TDoubleArrayList; +import net.imglib2.Cursor; +import net.imglib2.FinalInterval; import net.imglib2.IterableInterval; -import net.imglib2.RandomAccessibleInterval; -import net.imglib2.roi.IterableRegion; -import net.imglib2.roi.Masks; -import net.imglib2.roi.Regions; -import net.imglib2.roi.geom.GeomMasks; -import net.imglib2.roi.geom.real.WritablePolygon2D; -import net.imglib2.type.logic.BoolType; +import net.imglib2.Localizable; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessible; +import net.imglib2.type.numeric.RealType; +import net.imglib2.util.Intervals; +import net.imglib2.util.Util; +import net.imglib2.view.IntervalView; import net.imglib2.view.Views; -public class SpotRoi +public class SpotRoi extends SpotBase { - /** - * Polygon points X coordinates, in physical units. - */ - public final double[] x; + /** Polygon points X coordinates, in physical units, centered (0,0). */ + private final double[] x; + + /** Polygon points Y coordinates, in physical units, centered (0,0). */ + private final double[] y; + + public SpotRoi( + final double xc, + final double yc, + final double zc, + final double r, + final double quality, + final String name, + final double[] x, + final double[] y ) + { + super( xc, yc, zc, r, quality, name ); + this.x = x; + this.y = y; + } /** - * Polygon points Y coordinates, in physical units. + * This constructor is only used for deserializing a model from a TrackMate + * file. It messes with the ID of the spots and should be not used + * otherwise. + * + * @param ID + * the ID to use when creating the spot. + * @param x + * the spot contour X coordinates. + * @param y + * the spot contour Y coordinates. */ - public final double[] y; - - public SpotRoi( final double[] x, final double[] y ) + public SpotRoi( + final int ID, + final double[] x, + final double[] y ) { + super( ID ); this.x = x; this.y = y; } + @Override + public void accept( final SpotVisitor v ) + { + v.visit( this ); + } + + @Override public SpotRoi copy() { - return new SpotRoi( x.clone(), y.clone() ); + final double xc = getDoublePosition( 0 ); + final double yc = getDoublePosition( 1 ); + final double zc = getDoublePosition( 2 ); + final double r = getFeature( Spot.RADIUS ); + final double quality = getFeature( Spot.QUALITY ); + return new SpotRoi( xc, yc, zc, r, quality, getName(), x.clone(), y.clone() ); + } + + /** + * Returns the X coordinates of the ith vertex of the polygon, in physical + * coordinates. + * + * @param i + * the index of the vertex. + * @return the vertex X position. + */ + public double x( final int i ) + { + return x[ i ] + getDoublePosition( 0 ); + } + + /** + * Returns the Y coordinates of the ith vertex of the polygon, in physical + * coordinates. + * + * @param i + * the index of the vertex. + * @return the vertex Y position. + */ + public double y( final int i ) + { + return y[ i ] + getDoublePosition( 1 ); + } + + /** + * Returns the X coordinates of the ith vertex of the polygon, relative + * to the spot center, in physical coordinates. + * + * @param i + * the index of the vertex. + * @return the vertex X position. + */ + public double xr( final int i ) + { + return x[ i ]; + } + + /** + * Sets the X coordinates of the ith vertex of the polygon, relative to + * the spot center, in physical coordinates. This method is meant to be + * used for undo / redo operations, and should not be used otherwise. + * + * @param i + * the index of the vertex. + * @param x + * the vertex X position. + */ + public void setXr( final int i, final double x ) + { + this.x[ i ] = x; + } + + /** + * Returns the Y coordinates of the ith vertex of the polygon, relative + * to the spot center, in physical coordinates. + * + * @param i + * the index of the vertex. + * @return the vertex Y position. + */ + public double yr( final int i ) + { + return y[ i ]; } /** - * Returns a new int array containing the X pixel coordinates - * to which to paint this polygon. + * Sets the Y coordinates of the ith vertex of the polygon, relative to + * the spot center, in physical coordinates. This method is meant to be + * used for undo / redo operations, and should not be used otherwise. * - * @param calibration - * the pixel size in X, to convert physical coordinates to pixel - * coordinates. - * @param xcorner - * the top-left X corner of the view in the image to paint. - * @param spotXCenter - * the X coordinate of the spot center. - * @param magnification - * the magnification of the view. - * @return a new int array. + * @param i + * the index of the vertex. + * @param y + * the vertex Y position. */ - public double[] toPolygonX( final double calibration, final double xcorner, final double spotXCenter, final double magnification ) + public void setYr( final int i, final double y ) { - final double[] xp = new double[ x.length ]; - for ( int i = 0; i < xp.length; i++ ) + this.y[ i ] = y; + } + + public int nPoints() + { + return x.length; + } + + @Override + public double realMin( final int d ) + { + if ( d > 1 ) + return 0; + final double[] arr = ( d == 0 ) ? x : y; + return getDoublePosition( d ) + Util.min( arr ); + } + + @Override + public double realMax( final int d ) + { + if ( d > 1 ) + return 0; + final double[] arr = ( d == 0 ) ? x : y; + return getDoublePosition( d ) + Util.max( arr ); + } + + /** + * Convenience method that returns the X and Y coordinates of the polygon on + * this spot, possibly shifted and scale by a specified amount. Such that: + * + *

+	 * xout = x * sx + cx
+	 * yout = y * sy + cy
+	 * 
+ * + * @param cx + * the shift in X to apply after scaling coordinates. + * @param cy + * the shift in Y to apply after scaling coordinates. + * @param sx + * the scale to apply in X. + * @param sy + * the scale to apply in Y. + * @param xout + * a list in which to write resulting X coordinates. Reset by + * this call. + * @param yout + * a list in which to write resulting Y coordinates. Reset by + * this call. + */ + public void toArray( final double cx, final double cy, final double sx, final double sy, final TDoubleArrayList xout, final TDoubleArrayList yout ) + { + xout.resetQuick(); + yout.resetQuick(); + for ( int i = 0; i < x.length; i++ ) { - final double xc = ( spotXCenter + x[ i ] ) / calibration; - xp[ i ] = ( xc - xcorner ) * magnification; + xout.add( x( i ) + sx + cx ); + yout.add( y( i ) + sx + cy ); } - return xp; } /** - * Returns a new int array containing the Y pixel coordinates - * to which to paint this polygon. + * Convenience method that returns the X and Y coordinates of the polygon on + * this spot, possibly shifted and scale by a specified amount. Such that: + * + *
+	 * xout = x * sx + cx
+	 * yout = y * sy + cy
+	 * 
* - * @param calibration - * the pixel size in Y, to convert physical coordinates to pixel - * coordinates. - * @param ycorner - * the top-left Y corner of the view in the image to paint. - * @param spotYCenter - * the spot Y center in physical units. - * @param magnification - * the magnification of the view. - * @return a new int array. + * @param cx + * the shift in X to apply after scaling coordinates. + * @param cy + * the shift in Y to apply after scaling coordinates. + * @param sx + * the scale to apply in X. + * @param sy + * the scale to apply in Y. + * @return a new 2D double array, with the array of X values as the first + * element, and the array of Y values as a second element. */ - public double[] toPolygonY( final double calibration, final double ycorner, final double spotYCenter, final double magnification ) + public double[][] toArray( final double cx, final double cy, final double sx, final double sy ) { - final double[] yp = new double[ y.length ]; - for ( int i = 0; i < yp.length; i++ ) + final double[] xout = new double[ x.length ]; + final double[] yout = new double[ x.length ]; + for ( int i = 0; i < x.length; i++ ) { - final double yc = ( spotYCenter + y[ i ] ) / calibration; - yp[ i ] = ( yc - ycorner ) * magnification; + xout[ i ] = x( i ) * sx + cx; + yout[ i ] = y( i ) * sy + cy; } - return yp; + return new double[][] { xout, yout }; } - public < T > IterableInterval< T > sample( final Spot spot, final ImgPlus< T > img ) + @Override + public < T extends RealType< T > > IterableInterval< T > iterable( final RandomAccessible< T > ra, final double[] calibration ) { - return sample( spot.getDoublePosition( 0 ), spot.getDoublePosition( 1 ), img, img.averageScale( 0 ), img.averageScale( 1 ) ); + return new SpotRoiIterable<>( this, ra, calibration ); } - public < T > IterableInterval< T > sample( final double spotXCenter, final double spotYCenter, final RandomAccessibleInterval< T > img, final double xScale, final double yScale ) + private static double radius( final double[] x, final double[] y ) { - final double[] xp = toPolygonX( xScale, 0, spotXCenter, 1. ); - final double[] yp = toPolygonY( yScale, 0, spotYCenter, 1. ); - final WritablePolygon2D polygon = GeomMasks.closedPolygon2D( xp, yp ); - final IterableRegion< BoolType > region = Masks.toIterableRegion( polygon ); - return Regions.sample( region, Views.extendMirrorDouble( Views.dropSingletonDimensions( img ) ) ); + return Math.sqrt( area( x, y ) / Math.PI ); } - public double radius() + private static double area( final double[] x, final double[] y ) { - return Math.sqrt( area() / Math.PI ); + return Math.abs( signedArea( x, y ) ); } public double area() { - return Math.abs( signedArea( x, y ) ); + return area( x, y ); } + @Override public void scale( final double alpha ) { + super.scale( alpha ); for ( int i = 0; i < x.length; i++ ) { final double x = this.x[ i ]; @@ -148,7 +309,7 @@ public void scale( final double alpha ) } } - public static Spot createSpot( final double[] x, final double[] y, final double quality ) + public static SpotRoi createSpot( final double[] x, final double[] y, final double quality ) { // Put polygon coordinates with respect to centroid. final double[] centroid = centroid( x, y ); @@ -157,15 +318,10 @@ public static Spot createSpot( final double[] x, final double[] y, final double final double[] xr = Arrays.stream( x ).map( x0 -> x0 - xc ).toArray(); final double[] yr = Arrays.stream( y ).map( y0 -> y0 - yc ).toArray(); - // Create roi. - final SpotRoi roi = new SpotRoi( xr, yr ); - // Create spot. final double z = 0.; - final double r = roi.radius(); - final Spot spot = new Spot( xc, yc, z, r, quality ); - spot.setRoi( roi ); - return spot; + final double r = radius( xr, yr ); + return new SpotRoi( xc, yc, z, r, quality, null, xr, yr ); } /* @@ -178,16 +334,14 @@ private static final double[] centroid( final double[] x, final double[] y ) double ax = 0.0; double ay = 0.0; final int n = x.length; - for ( int i = 0; i < n - 1; i++ ) + int i; + int j; + for ( i = 0, j = n - 1; i < n; j = i++ ) { - final double w = x[ i ] * y[ i + 1 ] - x[ i + 1 ] * y[ i ]; - ax += ( x[ i ] + x[ i + 1 ] ) * w; - ay += ( y[ i ] + y[ i + 1 ] ) * w; + final double w = x[ j ] * y[ i ] - x[ i ] * y[ j ]; + ax += ( x[ j ] + x[ i ] ) * w; + ay += ( y[ j ] + y[ i ] ) * w; } - - final double w0 = x[ n - 1 ] * y[ 0 ] - x[ 0 ] * y[ n - 1 ]; - ax += ( x[ n - 1 ] + x[ 0 ] ) * w0; - ay += ( y[ n - 1 ] + y[ 0 ] ) * w0; return new double[] { ax / 6. / area, ay / 6. / area }; } @@ -195,9 +349,261 @@ private static final double signedArea( final double[] x, final double[] y ) { final int n = x.length; double a = 0.0; - for ( int i = 0; i < n - 1; i++ ) - a += x[ i ] * y[ i + 1 ] - x[ i + 1 ] * y[ i ]; + int i; + int j; + for ( i = 0, j = n - 1; i < n; j = i++ ) + a += x[ j ] * y[ i ] - x[ i ] * y[ j ]; + + return a / 2.; + } + + /* + * ITERABLE and ITERATOR. + */ + + private static final class SpotRoiIterable< T extends RealType< T > > implements IterableInterval< T > + { + + private final FinalInterval interval; + + /** Polygon X coords in pixel units. */ + private final double[] x; + + /** Polygon Y coords in pixel units. */ + private final double[] y; + + private final RandomAccessible< T > ra; + + public SpotRoiIterable( final SpotRoi roi, final RandomAccessible< T > ra, final double[] calibration ) + { + this.ra = ra; + final double[][] xy = roi.toArray( 0., 0., 1 / calibration[ 0 ], 1 / calibration[ 1 ] ); + this.x = xy[ 0 ]; + this.y = xy[ 1 ]; + final long minX = ( long ) Math.floor( Util.min( x ) ); + final long maxX = ( long ) Math.ceil( Util.max( x ) ); + final long minY = ( long ) Math.floor( Util.min( y ) ); + final long maxY = ( long ) Math.ceil( Util.max( y ) ); + interval = Intervals.createMinMax( minX, minY, maxX, maxY ); + } + + @Override + public long size() + { + int n = 0; + final Cursor< T > cursor = cursor(); + while ( cursor.hasNext() ) + { + cursor.fwd(); + n++; + } + return n; + } + + @Override + public T firstElement() + { + return cursor().next(); + } + + @Override + public Object iterationOrder() + { + return this; + } + + @Override + public double realMin( final int d ) + { + return interval.realMin( d ); + } + + @Override + public double realMax( final int d ) + { + return interval.realMax( d ); + } + + @Override + public int numDimensions() + { + return 2; + } + + @Override + public long min( final int d ) + { + return interval.min( d ); + } + + @Override + public long max( final int d ) + { + return interval.max( d ); + } + + @Override + public Cursor< T > cursor() + { + return new MyCursor< T >( x, y, ra ); + } - return ( a + x[ n - 1 ] * y[ 0 ] - x[ 0 ] * y[ n - 1 ] ) / 2.0; + @Override + public Cursor< T > localizingCursor() + { + return cursor(); + } + + @Override + public Iterator< T > iterator() + { + return cursor(); + } + } + + /** + * Iterates inside a close polygon given by X & Y in pixel coordinates. + * + * @param + * the type of pixel in the image. + */ + private static final class MyCursor< T extends RealType< T > > implements Cursor< T > + { + + private final FinalInterval interval; + + private Cursor< T > cursor; + + private final double[] x; + + private final double[] y; + + private boolean hasNext; + + private final RandomAccessible< T > rae; + + private RandomAccess< T > ra; + + public MyCursor( final double[] x, final double[] y, final RandomAccessible< T > rae ) + { + this.x = x; + this.y = y; + this.rae = rae; + final long minX = ( long ) Math.floor( Util.min( x ) ); + final long maxX = ( long ) Math.ceil( Util.max( x ) ); + final long minY = ( long ) Math.floor( Util.min( y ) ); + final long maxY = ( long ) Math.ceil( Util.max( y ) ); + interval = Intervals.createMinMax( minX, minY, maxX, maxY ); + reset(); + } + + @Override + public T get() + { + return ra.get(); + } + + @Override + public void fwd() + { + ra.setPosition( cursor ); + fetch(); + } + + private void fetch() + { + while ( cursor.hasNext() ) + { + cursor.fwd(); + if ( isInside( cursor, x, y ) ) + { + hasNext = cursor.hasNext(); + return; + } + } + hasNext = false; + } + + private static final boolean isInside( final Localizable localizable, final double[] x, final double[] y ) + { + // Taken from Imglib2-roi GeomMaths. No edge case. + final double xl = localizable.getDoublePosition( 0 ); + final double yl = localizable.getDoublePosition( 1 ); + + int i; + int j; + boolean inside = false; + for ( i = 0, j = x.length - 1; i < x.length; j = i++ ) + { + final double xj = x[ j ]; + final double yj = y[ j ]; + + final double xi = x[ i ]; + final double yi = y[ i ]; + + if ( ( yi > yl ) != ( yj > yl ) && ( xl < ( xj - xi ) * ( yl - yi ) / ( yj - yi ) + xi ) ) + inside = !inside; + } + return inside; + } + + @Override + public void reset() + { + final IntervalView< T > view = Views.interval( rae, interval ); + cursor = view.localizingCursor(); + ra = rae.randomAccess( interval ); + fetch(); + } + + @Override + public double getDoublePosition( final int d ) + { + return ra.getDoublePosition( d ); + } + + @Override + public int numDimensions() + { + return 2; + } + + @Override + public void jumpFwd( final long steps ) + { + for ( int i = 0; i < steps; i++ ) + fwd(); + } + + @Override + public boolean hasNext() + { + return hasNext; + } + + @Override + public T next() + { + fwd(); + return get(); + } + + @Override + public long getLongPosition( final int d ) + { + return ra.getLongPosition( d ); + } + + @Override + public Cursor< T > copy() + { + return new MyCursor<>( x, y, rae ); + } + + @Override + public Cursor< T > copyCursor() + { + return copy(); + } } } diff --git a/src/main/java/fiji/plugin/trackmate/TrackMateFactoryBase.java b/src/main/java/fiji/plugin/trackmate/TrackMateFactoryBase.java index 5c13b0e4d..fcc5319bc 100644 --- a/src/main/java/fiji/plugin/trackmate/TrackMateFactoryBase.java +++ b/src/main/java/fiji/plugin/trackmate/TrackMateFactoryBase.java @@ -2,18 +2,18 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * 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 * . @@ -53,7 +53,7 @@ public interface TrackMateFactoryBase< F extends TrackMateFactoryBase< F > > ext * {@link SpotDetectorFactory} key. * * @param settings - * the map to marshal. + * the settings map to marshal. * @param element * the JDom element to update. * @return an error message if marshaling was unsuccessful. If successful, @@ -136,6 +136,22 @@ else if ( klass == Boolean.class ) { value = att.getBooleanValue(); } + else if ( klass.isEnum() ) + { + @SuppressWarnings( "unchecked" ) + final Class< Enum< ? > > enumClass = ( Class< Enum< ? > > ) klass; + final Enum< ? >[] enums = enumClass.getEnumConstants(); + Enum< ? > tmp = null; + for ( final Enum< ? > e : enums ) + { + if ( e.toString().equals( att.getValue() ) ) + { + tmp = e; + break; + } + } + value = tmp; + } else { return "When unmarshalling: Unsupported type " + klass.getSimpleName() + " for parameter " + keyString + " in factory " + getName(); diff --git a/src/main/java/fiji/plugin/trackmate/TrackMatePlugIn.java b/src/main/java/fiji/plugin/trackmate/TrackMatePlugIn.java index 3d3287bf4..173dcf2e6 100644 --- a/src/main/java/fiji/plugin/trackmate/TrackMatePlugIn.java +++ b/src/main/java/fiji/plugin/trackmate/TrackMatePlugIn.java @@ -2,18 +2,18 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * 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 * . @@ -27,21 +27,18 @@ import javax.swing.JFrame; -import org.scijava.object.ObjectService; - +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsIO; +import fiji.plugin.trackmate.gui.featureselector.AnalyzerSelection; +import fiji.plugin.trackmate.gui.featureselector.AnalyzerSelectionIO; import fiji.plugin.trackmate.gui.wizard.TrackMateWizardSequence; import fiji.plugin.trackmate.gui.wizard.WizardSequence; import fiji.plugin.trackmate.io.SettingsPersistence; -import fiji.plugin.trackmate.util.TMUtils; -import fiji.plugin.trackmate.visualization.TrackMateModelView; -import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; import ij.IJ; import ij.ImageJ; import ij.ImagePlus; -import ij.Prefs; import ij.WindowManager; import ij.plugin.PlugIn; @@ -96,16 +93,14 @@ else if ( imp.getType() == ImagePlus.COLOR_RGB ) // Main objects. final Settings settings = createSettings( imp ); final Model model = createModel( imp ); - final TrackMate trackmate = createTrackMate( model, settings ); - final SelectionModel selectionModel = new SelectionModel( model ); final DisplaySettings displaySettings = createDisplaySettings(); + final GuiModel guiModel = new GuiModel( model, settings, displaySettings ); // Main view. - final TrackMateModelView displayer = new HyperStackDisplayer( model, selectionModel, imp, displaySettings ); - displayer.render(); + guiModel.getWindowManager().createHyperStackDisplayer(); // Wizard. - final WizardSequence sequence = createSequence( trackmate, selectionModel, displaySettings ); + final WizardSequence sequence = createSequence( guiModel ); final JFrame frame = sequence.run( "TrackMate on " + imp.getShortTitle() ); frame.setIconImage( TRACKMATE_ICON.getImage() ); GuiUtils.positionWindow( frame, imp.getWindow() ); @@ -126,18 +121,16 @@ else if ( imp.getType() == ImagePlus.COLOR_RGB ) * Hook for subclassers:
* Will create and position the sequence that will be played by the wizard * launched by this plugin. + * + * @param guiModel + * the {@link GuiModel} that will be used to store the data of + * the wizard. * - * @param trackmate - * the {@link TrackMate} instance to use. - * @param selectionModel - * the {@link SelectionModel} to use. - * @param displaySettings - * the {@link DisplaySettings} to use. * @return a new sequence. */ - protected WizardSequence createSequence( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings ) + protected WizardSequence createSequence( final GuiModel guiModel ) { - return new TrackMateWizardSequence( trackmate, selectionModel, displaySettings ); + return new TrackMateWizardSequence( guiModel ); } /** @@ -146,7 +139,7 @@ protected WizardSequence createSequence( final TrackMate trackmate, final Select * {@link TrackMate} instance. * * @param imp - * the {@link ImagePlus} to operate on. + * the image the tracking data will be created on. * @return a new {@link Model} instance. */ protected Model createModel( final ImagePlus imp ) @@ -171,41 +164,11 @@ protected Model createModel( final ImagePlus imp ) protected Settings createSettings( final ImagePlus imp ) { // Persistence. - final Settings ls = SettingsPersistence.readLastUsedSettings( imp, Logger.DEFAULT_LOGGER ); - // Force adding analyzers found at runtime - ls.addAllAnalyzers(); - return ls; - } - - /** - * Hook for subclassers:
- * Creates the TrackMate instance that will be controlled in the GUI. - * - * @param model - * the model to use. - * @param settings - * the settings to use. - * @return a new {@link TrackMate} instance. - */ - protected TrackMate createTrackMate( final Model model, final Settings settings ) - { - /* - * Since we are now sure that we will be working on this model with this - * settings, we need to pass to the model the units from the settings. - */ - final String spaceUnits = settings.imp.getCalibration().getXUnit(); - final String timeUnits = settings.imp.getCalibration().getTimeUnit(); - model.setPhysicalUnits( spaceUnits, timeUnits ); - - final TrackMate trackmate = new TrackMate( model, settings ); - final ObjectService objectService = TMUtils.getContext().service( ObjectService.class ); - if ( objectService != null ) - objectService.addObject( trackmate ); - - // Set the num of threads from IJ prefs. - trackmate.setNumThreads( Prefs.getThreads() ); - - return trackmate; + final Settings settings = SettingsPersistence.readLastUsedSettings( imp, Logger.DEFAULT_LOGGER ); + // Add the analyzers configured by the user. + final AnalyzerSelection analyzerSelection = AnalyzerSelectionIO.readUserDefault(); + analyzerSelection.configure( settings ); + return settings; } protected DisplaySettings createDisplaySettings() diff --git a/src/main/java/fiji/plugin/trackmate/TrackMateRunner.java b/src/main/java/fiji/plugin/trackmate/TrackMateRunner.java index 55f65daeb..365f2f0f6 100644 --- a/src/main/java/fiji/plugin/trackmate/TrackMateRunner.java +++ b/src/main/java/fiji/plugin/trackmate/TrackMateRunner.java @@ -40,6 +40,7 @@ import fiji.plugin.trackmate.detection.DetectorKeys; import fiji.plugin.trackmate.features.FeatureFilter; import fiji.plugin.trackmate.features.track.TrackBranchingAnalyzer; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.components.LogPanel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; @@ -53,6 +54,7 @@ import fiji.plugin.trackmate.visualization.TrackMateModelView; import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; import fiji.util.SplitString; +import ij.IJ; import ij.ImageJ; import ij.ImagePlus; import ij.Macro; @@ -296,15 +298,14 @@ else if ( macroOptions.containsKey( ARG_INPUT_IMAGE_PATH ) ) } /* - * Instantiate TrackMate. + * Instantiate GUImodel. */ final Settings settings = createSettings( imp ); final Model model = createModel( imp ); - final SelectionModel selectionModel = new SelectionModel( model ); model.setLogger( logger ); - final TrackMate trackmate = createTrackMate( model, settings ); final DisplaySettings displaySettings = createDisplaySettings(); + final GuiModel guiModel = new GuiModel( model, settings, displaySettings ); /* * Configure default settings. @@ -402,12 +403,12 @@ else if ( macroOptions.containsKey( ARG_INPUT_IMAGE_PATH ) ) imp.show(); } GuiUtils.userCheckImpDimensions( imp ); + // Main view. - final TrackMateModelView displayer = new HyperStackDisplayer( model, selectionModel, imp, displaySettings ); - displayer.render(); + guiModel.getWindowManager().createHyperStackDisplayer(); // Wizard. - final WizardSequence sequence = createSequence( trackmate, selectionModel, displaySettings ); + final WizardSequence sequence = createSequence( guiModel ); final JFrame frame = sequence.run( "TrackMate on " + imp.getShortTitle() ); frame.setIconImage( TRACKMATE_ICON.getImage() ); GuiUtils.positionWindow( frame, imp.getWindow() ); @@ -415,6 +416,7 @@ else if ( macroOptions.containsKey( ARG_INPUT_IMAGE_PATH ) ) return; } + final TrackMate trackmate = guiModel.getTrackMate(); final String welcomeMessage = TrackMate.PLUGIN_NAME_STR + " v" + TrackMate.PLUGIN_NAME_VERSION + " started on:\n" + TMUtils.getCurrentTimeString() + '\n'; logger.log( welcomeMessage ); if ( !trackmate.checkInput() || !trackmate.process() ) @@ -434,8 +436,8 @@ else if ( macroOptions.containsKey( ARG_INPUT_IMAGE_PATH ) ) final TmXmlWriter writer = new TmXmlWriter( save_path, logger ); writer.appendLog( logger.toString() ); - writer.appendModel( trackmate.getModel() ); - writer.appendSettings( trackmate.getSettings() ); + writer.appendModel( model ); + writer.appendSettings( settings ); try { @@ -501,11 +503,11 @@ else if ( macroOptions.containsKey( ARG_INPUT_IMAGE_PATH ) ) */ // Main view. - final TrackMateModelView displayer = new HyperStackDisplayer( model, selectionModel, imp, displaySettings ); + final TrackMateModelView displayer = new HyperStackDisplayer( guiModel ); displayer.render(); // Wizard. - final WizardSequence sequence = createSequence( trackmate, selectionModel, displaySettings ); + final WizardSequence sequence = createSequence( guiModel ); sequence.setCurrent( ConfigureViewsDescriptor.KEY ); final JFrame frame = sequence.run( "TrackMate on " + imp.getShortTitle() ); frame.setIconImage( TRACKMATE_ICON.getImage() ); diff --git a/src/main/java/fiji/plugin/trackmate/TrackModel.java b/src/main/java/fiji/plugin/trackmate/TrackModel.java index bd7b8cb4c..8b104b854 100644 --- a/src/main/java/fiji/plugin/trackmate/TrackModel.java +++ b/src/main/java/fiji/plugin/trackmate/TrackModel.java @@ -312,7 +312,7 @@ void setEdgeWeight( final DefaultWeightedEdge edge, final double weight ) edgesModified.add( edge ); } - Boolean setVisibility( final Integer trackID, final boolean visible ) + public boolean setVisibility( final Integer trackID, final boolean visible ) { return visibility.put( trackID, Boolean.valueOf( visible ) ); } @@ -518,6 +518,14 @@ public boolean isVisible( final Integer ID ) /** * Returns the set of track IDs managed by this model, ordered by track * names (alpha-numerically sorted). + *

+ * The set maintains the order of the track IDs, so that iterating over it + * will return the track IDs in the order of their names. It is possible to + * add it to a {@link java.util.List} to facilitate navigating the ids: + *

+	 * List< String > trackIDList = new ArrayList<>( trackIDs );
+	 * ListIterator< String > iterator = trackIDList.listIterator();
+	 * 
* * @param visibleOnly * if true, only visible track IDs will be returned. @@ -733,27 +741,43 @@ public String echo() */ /** - * Returns a new depth first iterator over the spots connected by links in - * this model. A boolean flag allow to set whether the returned iterator - * does take into account the edge direction. If true, the iterator will not - * be able to iterate backward in time. + * Returns a new, undirected depth first iterator over the spots connected + * by links in this model. The iterator will iterate backward and forward in + * time. * * @param start * the spot to start iteration with. Can be null, * then the start will be taken randomly and will traverse all * the links. - * @param directed - * if true returns a directed iterator, undirected if false. * @return a new depth-first iterator. */ - public GraphIterator< Spot, DefaultWeightedEdge > getDepthFirstIterator( final Spot start, final boolean directed ) + public GraphIterator< Spot, DefaultWeightedEdge > getDepthFirstIterator( final Spot start ) { - if ( directed ) - return new TimeDirectedDepthFirstIterator( graph, start ); - return new DepthFirstIterator<>( graph, start ); } + /** + * Returns a new, directed depth first iterator over the spots connected by + * links in this model. + *

+ * The iterator will iterate forward in time only. If the boolean flag + * reversed is set to true, the iterator will + * iterate backward in time only. + * + * @param start + * the spot to start iteration with. Can be null, + * then the start will be taken randomly and will traverse all + * the links. + * @param reversed + * if true, the iterator will iterate backward in + * time only, otherwise it will iterate forward in time only. + * @return a new depth-first iterator. + */ + public GraphIterator< Spot, DefaultWeightedEdge > getDirectedDepthFirstIterator( final Spot start, final boolean reversed ) + { + return new TimeDirectedDepthFirstIterator( graph, start, reversed ); + } + /** * Returns a new depth first iterator over the spots connected by links in * this model. This iterator is sorted: when branching, it chooses the next @@ -1288,7 +1312,7 @@ public void edgeRemoved( final GraphEdgeChangeEvent< Spot, DefaultWeightedEdge > for ( final Spot v : targetVCS ) vertexToID.put( v, newid ); - final Boolean targetVisibility = visibility.get( id ); + final boolean targetVisibility = visibility.get( id ); visibility.put( newid, targetVisibility ); names.put( newid, nameGenerator.next() ); // Transaction: both children tracks are marked for diff --git a/src/main/java/fiji/plugin/trackmate/action/CTCExporter.java b/src/main/java/fiji/plugin/trackmate/action/CTCExporter.java index 14a285638..e532311a2 100644 --- a/src/main/java/fiji/plugin/trackmate/action/CTCExporter.java +++ b/src/main/java/fiji/plugin/trackmate/action/CTCExporter.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -50,9 +50,10 @@ import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.SpotRoi; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.TrackModel; -import fiji.plugin.trackmate.action.LabelImgExporter.SpotRoiWriter; +import fiji.plugin.trackmate.action.LabelImgExporter.SpotShapeWriter; import fiji.plugin.trackmate.graph.ConvexBranchesDecomposition; import fiji.plugin.trackmate.graph.ConvexBranchesDecomposition.TrackBranchDecomposition; import fiji.plugin.trackmate.graph.GraphUtils; @@ -77,7 +78,7 @@ * Cell-Tracking-Challenge convention. *

* See http://celltrackingchallenge.net/ - * + * * @author Jean-Yves Tinevez * */ @@ -173,7 +174,7 @@ public static String exportAll( final String exportRootFolder, final TrackMate t /** * Saves the settings part as XML for reference. - * + * * @param exportRootFolder * the root folder for exporting. * @param saveId @@ -184,7 +185,7 @@ public static String exportAll( final String exportRootFolder, final TrackMate t * @param logger * a logger to report progress. * @throws IOException - * if a problem happens during writing. + * if there's any problem writing. */ public static void exportSettingsFile( final String exportRootFolder, final int saveId, final TrackMate trackmate, final Logger logger ) throws IOException { @@ -204,7 +205,7 @@ public static void exportSettingsFile( final String exportRootFolder, final int * For instance the first id return will be '1', which means that the * original image data will be saved under the folder '01'. If '01' already * exists, then this method will return 2, etc. - * + * * @param exportRootFolder * the root folder in which to export the data. * @return an integer id that can be passed in the other method of this @@ -271,7 +272,7 @@ public static void exportOriginalImageData( final String exportRootFolder, final *

* Only exports the spots that have a ROI, and write only the frames that * have at least one spot with a ROI. - * + * * @param exportRootFolder * the root of the export folder. * @param saveId @@ -316,15 +317,16 @@ public static void exportSegmentationData( final String exportRootFolder, final for ( int frame = 0; frame < dims[ 3 ]; frame++ ) { final ImgPlus< UnsignedShortType > imgCT = TMUtils.hyperSlice( labelImg, 0, frame ); - final SpotRoiWriter< UnsignedShortType > spotWriter = new SpotRoiWriter<>( imgCT ); + final SpotShapeWriter< UnsignedShortType > spotWriter = new SpotShapeWriter<>( imgCT ); for ( final Spot spot : model.getSpots().iterable( frame, true ) ) { - if ( spot.getRoi() == null ) - continue; - final int id = idGen.getAndIncrement(); - spotWriter.write( spot, id ); - framesToWrite.add( Integer.valueOf( frame ) ); + if ( spot instanceof SpotRoi ) + { + final int id = idGen.getAndIncrement(); + spotWriter.write( spot, id ); + framesToWrite.add( Integer.valueOf( frame ) ); + } } } @@ -340,7 +342,7 @@ public static void exportSegmentationData( final String exportRootFolder, final final Function< Long, String > tifNameGen = nFrames > 999 ? i -> String.format( "man_seg%04d.tif", i ) : i -> String.format( "man_seg%03d.tif", i ); - + // Only save frames with spots in. for ( final int frame : framesToWrite ) { @@ -411,8 +413,8 @@ public static String exportTrackingData( final String exportRootFolder, final in Files.createDirectories( path.getParent() ); logger.log( "Exporting tracking text file to " + path.toString() ); - try (FileOutputStream fos = new FileOutputStream( path.toFile() ); - BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( fos ) )) + try (final FileOutputStream fos = new FileOutputStream( path.toFile() ); + final BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( fos ) )) { for ( final Integer trackID : trackModel.trackIDs( true ) ) @@ -448,7 +450,7 @@ public static String exportTrackingData( final String exportRootFolder, final in { final long frame = spot.getFeature( Spot.FRAME ).longValue(); final ImgPlus< UnsignedShortType > imgCT = TMUtils.hyperSlice( labelImg, 0, frame ); - final SpotRoiWriter< UnsignedShortType > spotRoiWriter = new SpotRoiWriter<>( imgCT ); + final SpotShapeWriter< UnsignedShortType > spotRoiWriter = new SpotShapeWriter<>( imgCT ); spotRoiWriter.write( spot, currentID ); } @@ -510,7 +512,7 @@ public static String exportTrackingData( final String exportRootFolder, final in /** * Returns the folder in which the tracking data will be exported. - * + * * @param exportRootFolder * the root folder to export in. * @param saveId @@ -561,7 +563,7 @@ private static final Model sanitizeAndCopy( final Model model ) final Spot s2 = spots.get( j ); final double r2 = s2.getFeature( Spot.RADIUS ).doubleValue(); final double d = Math.sqrt( s1.squareDistanceTo( s2 ) ); - + if ( fudgeFactor * r1 > ( d + r2 ) || fudgeFactor * r2 > ( d + r1 ) ) { // They overlap too much. We must fix this. @@ -611,7 +613,7 @@ private static final Model sanitizeAndCopy( final Model model ) else sources.add( trackModel.getEdgeSource( edge ) ); } - + model.beginUpdate(); try { diff --git a/src/main/java/fiji/plugin/trackmate/action/CTCExporterAction.java b/src/main/java/fiji/plugin/trackmate/action/CTCExporterAction.java index 56b634c41..b58f4b07c 100644 --- a/src/main/java/fiji/plugin/trackmate/action/CTCExporterAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/CTCExporterAction.java @@ -32,10 +32,8 @@ import org.scijava.plugin.Plugin; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.action.CTCExporter.ExportType; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import ij.ImagePlus; import ij.gui.GenericDialog; @@ -53,11 +51,11 @@ public class CTCExporterAction extends AbstractTMAction ""; @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { final GenericDialog dialog = new GenericDialog( "CTC exporter", parent ); - final ImagePlus imp = trackmate.getSettings().imp; + final ImagePlus imp = guiModel.getSettings().imp; String defaultPath; if ( imp == null || imp.getOriginalFileInfo() == null ) defaultPath = System.getProperty( "user.home" ); @@ -80,7 +78,7 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo try { - CTCExporter.exportAll( exportRootFolder, trackmate, ExportType.values()[ choiceIndex ], logger ); + CTCExporter.exportAll( exportRootFolder, guiModel.getTrackMate(), ExportType.values()[ choiceIndex ], logger ); } catch ( final IOException e ) { diff --git a/src/main/java/fiji/plugin/trackmate/action/CaptureOverlayAction.java b/src/main/java/fiji/plugin/trackmate/action/CaptureOverlayAction.java index 1361f375c..9fa72de6d 100644 --- a/src/main/java/fiji/plugin/trackmate/action/CaptureOverlayAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/CaptureOverlayAction.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -34,9 +34,9 @@ import org.scijava.plugin.Plugin; import fiji.plugin.trackmate.Logger; -import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.util.TMUtils; import fiji.plugin.trackmate.visualization.ViewUtils; import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; @@ -74,9 +74,9 @@ public class CaptureOverlayAction extends AbstractTMAction private static boolean whiteBackground = false; @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame gui ) + public void execute( final GuiModel guiModel, final Frame gui ) { - final ImagePlus imp = trackmate.getSettings().imp; + final ImagePlus imp = guiModel.getSettings().imp; if ( firstFrame < 0 ) firstFrame = 1; @@ -127,7 +127,9 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo imp2.updateAndRepaintWindow(); } // Add overlay to it. - final HyperStackDisplayer displayer = new HyperStackDisplayer( trackmate.getModel(), new SelectionModel( trackmate.getModel() ), imp2, displaySettings ); + final Settings settings2 = new Settings( imp2 ); + final GuiModel guiModel2 = new GuiModel( guiModel.getModel(), settings2, guiModel.getDisplaySettings() ); + final HyperStackDisplayer displayer = guiModel2.getWindowManager().createHyperStackDisplayer(); displayer.render(); final ImagePlus capture = capture( imp2, firstFrame, lastFrame, logger ); imp2.close(); @@ -135,7 +137,7 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo } else { - final ImagePlus capture = capture( trackmate, firstFrame, lastFrame, logger ); + final ImagePlus capture = capture( guiModel.getTrackMate(), firstFrame, lastFrame, logger ); capture.show(); } @@ -154,7 +156,7 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo * @param last * the last frame, inclusive, to capture. * @param logger - * a {@link Logger} to report capture progress. + * a logger instance to echo capture progress. * @return a new ImagePlus. */ public static ImagePlus capture( final TrackMate trackmate, final int first, final int last, final Logger logger ) diff --git a/src/main/java/fiji/plugin/trackmate/action/ComputeDistanceToRoiAction.java b/src/main/java/fiji/plugin/trackmate/action/ComputeDistanceToRoiAction.java index 4dcbf1534..f413035d4 100644 --- a/src/main/java/fiji/plugin/trackmate/action/ComputeDistanceToRoiAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/ComputeDistanceToRoiAction.java @@ -37,14 +37,12 @@ import fiji.plugin.trackmate.Dimension; import fiji.plugin.trackmate.FeatureModel; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; -import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.features.spot.SpotAnalyzer; import fiji.plugin.trackmate.features.spot.SpotAnalyzerFactory; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.util.TMUtils; import ij.gui.Roi; import ij.plugin.frame.RoiManager; @@ -76,11 +74,7 @@ public class ComputeDistanceToRoiAction extends AbstractTMAction private static final String KEY = "COMPUTE_DIST_TO_ROI"; @Override - public void execute( - final TrackMate trackmate, - final SelectionModel selectionModel, - final DisplaySettings displaySettings, - final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { logger.log( "Computing distance from visible spots to closest ROI.\n" ); // Get Roi Manager. @@ -92,9 +86,9 @@ public void execute( } // Get spatial calibration. final double[] calibration; - if ( trackmate.getSettings() != null ) + if ( guiModel.getSettings() != null ) { - final Settings settings = trackmate.getSettings(); + final Settings settings = guiModel.getSettings(); if ( settings.imp != null ) { calibration = TMUtils.getSpatialCalibration( settings.imp ); @@ -113,7 +107,7 @@ public void execute( } // Compute - final Model model = trackmate.getModel(); + final Model model = guiModel.getModel(); computeDistance( model, rm, calibration ); logger.log( "Done.\n" ); } diff --git a/src/main/java/fiji/plugin/trackmate/action/ExportAllSpotsStatsAction.java b/src/main/java/fiji/plugin/trackmate/action/ExportAllSpotsStatsAction.java index 89c9f6ea3..048ab2f98 100644 --- a/src/main/java/fiji/plugin/trackmate/action/ExportAllSpotsStatsAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/ExportAllSpotsStatsAction.java @@ -29,10 +29,7 @@ import org.scijava.plugin.Plugin; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.util.TMUtils; import fiji.plugin.trackmate.visualization.table.AllSpotsTableView; @@ -50,14 +47,14 @@ public class ExportAllSpotsStatsAction extends AbstractTMAction + ""; @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { - createSpotsTable( trackmate.getModel(), selectionModel, displaySettings, TMUtils.getImagePathWithoutExtension( trackmate.getSettings() ) ).render(); + createSpotsTable( guiModel, TMUtils.getImagePathWithoutExtension( guiModel.getSettings() ) ).render(); } - public static final AllSpotsTableView createSpotsTable( final Model model, final SelectionModel selectionModel, final DisplaySettings displaySettings, final String imageFileName ) + public static final AllSpotsTableView createSpotsTable( final GuiModel guiModel, final String imageFileName ) { - return new AllSpotsTableView( model, selectionModel, displaySettings, imageFileName ); + return new AllSpotsTableView( guiModel, imageFileName ); } // Invisible because called on the view config panel. diff --git a/src/main/java/fiji/plugin/trackmate/action/ExportStatsTablesAction.java b/src/main/java/fiji/plugin/trackmate/action/ExportStatsTablesAction.java index f73b7c1e7..59138f387 100644 --- a/src/main/java/fiji/plugin/trackmate/action/ExportStatsTablesAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/ExportStatsTablesAction.java @@ -29,10 +29,7 @@ import org.scijava.plugin.Plugin; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.util.TMUtils; import fiji.plugin.trackmate.visualization.table.TrackTableView; @@ -56,14 +53,14 @@ public class ExportStatsTablesAction extends AbstractTMAction + ""; @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { - createTrackTables( trackmate.getModel(), selectionModel, displaySettings, TMUtils.getImagePathWithoutExtension( trackmate.getSettings() ) ).render(); + createTrackTables( guiModel, TMUtils.getImagePathWithoutExtension( guiModel.getSettings() ) ).render(); } - public static TrackTableView createTrackTables( final Model model, final SelectionModel selectionModel, final DisplaySettings displaySettings, final String imageFileName ) + public static TrackTableView createTrackTables( final GuiModel guiModel, final String imageFileName ) { - return new TrackTableView( model, selectionModel, displaySettings, imageFileName ); + return new TrackTableView( guiModel, imageFileName ); } // Invisible because called on the view config panel. diff --git a/src/main/java/fiji/plugin/trackmate/action/ExportTracksToXML.java b/src/main/java/fiji/plugin/trackmate/action/ExportTracksToXML.java index 11ff46f52..98be4814f 100644 --- a/src/main/java/fiji/plugin/trackmate/action/ExportTracksToXML.java +++ b/src/main/java/fiji/plugin/trackmate/action/ExportTracksToXML.java @@ -41,11 +41,10 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.io.IOUtils; import fiji.plugin.trackmate.util.TMUtils; @@ -95,10 +94,10 @@ public static void export( final Model model, final Settings settings, final Fil } @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { logger.log( "Exporting tracks to simple XML format.\n" ); - final Model model = trackmate.getModel(); + final Model model = guiModel.getModel(); final int ntracks = model.getTrackModel().nTracks( true ); if ( ntracks == 0 ) { @@ -107,12 +106,12 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo } logger.log( " Preparing XML data.\n" ); - final Element root = marshall( model, trackmate.getSettings(), logger ); + final Element root = marshall( model, guiModel.getSettings(), logger ); File folder; try { - folder = new File( trackmate.getSettings().imp.getOriginalFileInfo().directory ); + folder = new File( guiModel.getSettings().imp.getOriginalFileInfo().directory ); } catch ( final NullPointerException npe ) { @@ -122,7 +121,7 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo File file; try { - String filename = trackmate.getSettings().imageFileName; + String filename = guiModel.getSettings().imageFileName; final int dot = filename.indexOf( "." ); filename = dot < 0 ? filename : filename.substring( 0, dot ); file = new File( folder.getPath() + File.separator + filename + "_Tracks.xml" ); diff --git a/src/main/java/fiji/plugin/trackmate/action/ExtractTrackStackAction.java b/src/main/java/fiji/plugin/trackmate/action/ExtractTrackStackAction.java index 5d3e9c4fe..3b63495fb 100644 --- a/src/main/java/fiji/plugin/trackmate/action/ExtractTrackStackAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/ExtractTrackStackAction.java @@ -42,8 +42,7 @@ import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.util.TMUtils; import fiji.plugin.trackmate.visualization.trackscheme.SpotIconGrabber; import ij.CompositeImage; @@ -92,11 +91,7 @@ public class ExtractTrackStackAction extends AbstractTMAction private static final float RESIZE_FACTOR = 1.5f; @Override - public void execute( - final TrackMate trackmate, - final SelectionModel selectionModel, - final DisplaySettings displaySettings, - final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { // Show dialog. final GenericDialog dialog = new GenericDialog( "Extract track stack", parent ); @@ -119,7 +114,9 @@ public void execute( logger.log( "Capturing " + ( do3d ? "3D" : "2D" ) + " track stack.\n" ); - final Model model = trackmate.getModel(); + final Model model = guiModel.getModel(); + final Settings settings = guiModel.getSettings(); + final SelectionModel selectionModel = guiModel.getSelectionModel(); final Set< Spot > selection = selectionModel.getSpotSelection(); final int nspots = selection.size(); if ( nspots != 2 ) @@ -146,7 +143,7 @@ public void execute( selectionModel.addEdgeToSelection( edges ); // Get stack. - final ImagePlus imp = trackStack( trackmate, spot, do3d, logger ); + final ImagePlus imp = trackStack( model, settings, spot, do3d, logger ); imp.show(); imp.setZ( imp.getNSlices() / 2 + 1 ); imp.resetDisplayRange(); @@ -188,7 +185,7 @@ public void execute( selectionModel.addEdgeToSelection( edges ); // Get stack. - final ImagePlus imp = trackStack( trackmate, start1, end1, do3d, logger ); + final ImagePlus imp = trackStack( model, settings, start1, end1, do3d, logger ); imp.show(); imp.setZ( imp.getNSlices() / 2 + 1 ); imp.resetDisplayRange(); @@ -196,28 +193,28 @@ public void execute( } public static final ImagePlus trackStack( - final TrackMate trackmate, + final Model model, + final Settings settings, final Spot spot, final boolean do3d, final Logger logger ) { - final Model model = trackmate.getModel(); final Integer trackID = model.getTrackModel().trackIDOf( spot ); final List< Spot > spots = new ArrayList<>( model.getTrackModel().trackSpots( trackID ) ); Collections.sort( spots, Spot.frameComparator ); final Spot start = spots.get( 0 ); final Spot end = spots.get( spots.size() - 1 ); - return trackStack( trackmate, start, end, do3d, logger ); + return trackStack( model, settings, start, end, do3d, logger ); } public static final ImagePlus trackStack( - final TrackMate trackmate, + final Model model, + final Settings settings, final Spot start, final Spot end, final boolean do3d, final Logger logger ) { - final Model model = trackmate.getModel(); final Spot start1; final Spot end1; if ( start.getFeature( Spot.POSITION_T ) > end.getFeature( Spot.POSITION_T ) ) @@ -265,7 +262,7 @@ public static final ImagePlus trackStack( // Sort spot by ascending frame number final TreeSet< Spot > sortedSpots = new TreeSet<>( Spot.timeComparator ); sortedSpots.addAll( path ); - return trackStack( trackmate.getSettings(), path, radius, do3d, logger ); + return trackStack( settings, path, radius, do3d, logger ); } @SuppressWarnings( { "unchecked", "rawtypes" } ) diff --git a/src/main/java/fiji/plugin/trackmate/action/IJRoiExporter.java b/src/main/java/fiji/plugin/trackmate/action/IJRoiExporter.java index aa1924702..dd88ff363 100644 --- a/src/main/java/fiji/plugin/trackmate/action/IJRoiExporter.java +++ b/src/main/java/fiji/plugin/trackmate/action/IJRoiExporter.java @@ -32,10 +32,9 @@ import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotRoi; -import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.detection.DetectionUtils; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.Icons; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import ij.ImagePlus; import ij.gui.GenericDialog; import ij.gui.OvalRoi; @@ -86,14 +85,13 @@ public void export( final Iterable< Spot > spots ) public void export( final Spot spot ) { - final SpotRoi sroi = spot.getRoi(); final Roi roi; - if ( sroi != null ) + if ( spot instanceof SpotRoi ) { - final double[] xs = sroi.toPolygonX( dx, 0., spot.getDoublePosition( 0 ), 1. ); - final double[] ys = sroi.toPolygonY( dy, 0., spot.getDoublePosition( 1 ), 1. ); - final float[] xp = toFloat( xs ); - final float[] yp = toFloat( ys ); + final SpotRoi sroi = ( SpotRoi ) spot; + final double[][] out = sroi.toArray( 0., 0., 1 / dx, 1 / dy ); + final float[] xp = toFloat( out[ 0 ] ); + final float[] yp = toFloat( out[ 1 ] ); roi = new PolygonRoi( xp, yp, PolygonRoi.POLYGON ); } else @@ -132,7 +130,7 @@ public static class IJRoiExporterAction extends AbstractTMAction { @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { // Show dialog. final GenericDialog dialog = new GenericDialog( "Export spots to IJ ROIs", parent ); @@ -147,10 +145,11 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo return; // Execute. + final SelectionModel selectionModel = guiModel.getSelectionModel(); final Iterable< Spot > spots; final int choice = Arrays.asList( choices ).indexOf( dialog.getNextRadioButton() ); if ( choice == 0 ) - spots = trackmate.getModel().getSpots().iterable( true ); + spots = guiModel.getModel().getSpots().iterable( true ); else if ( choice == 1 ) spots = selectionModel.getSpotSelection(); else @@ -161,7 +160,7 @@ else if ( choice == 1 ) spots = selectionModel.getSpotSelection(); } - final IJRoiExporter exporter = new IJRoiExporter( trackmate.getSettings().imp, logger ); + final IJRoiExporter exporter = new IJRoiExporter( guiModel.getSettings().imp, logger ); exporter.export( spots ); } } diff --git a/src/main/java/fiji/plugin/trackmate/action/ISBIChallengeExporter.java b/src/main/java/fiji/plugin/trackmate/action/ISBIChallengeExporter.java index c065990d6..7fc68000a 100644 --- a/src/main/java/fiji/plugin/trackmate/action/ISBIChallengeExporter.java +++ b/src/main/java/fiji/plugin/trackmate/action/ISBIChallengeExporter.java @@ -44,11 +44,9 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.io.IOUtils; public class ISBIChallengeExporter extends AbstractTMAction @@ -68,15 +66,16 @@ public class ISBIChallengeExporter extends AbstractTMAction ""; @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { logger.log( "Exporting tracks to ISBI challenge file format.\n" ); - final Model model = trackmate.getModel(); + final Model model = guiModel.getModel(); + final Settings settings = guiModel.getSettings(); File file; final File folder = new File( System.getProperty( "user.dir" ) ).getParentFile().getParentFile(); try { - String filename = trackmate.getSettings().imageFileName; + String filename = settings.imageFileName; filename = filename.substring( 0, filename.indexOf( "." ) ); file = new File( folder.getPath() + File.separator + filename + "_ISBI.xml" ); } @@ -91,7 +90,7 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo return; } - exportToFile( model, trackmate.getSettings(), file, logger ); + exportToFile( model, settings, file, logger ); } public static void exportToFile( final Model model, final Settings settings, final File file ) diff --git a/src/main/java/fiji/plugin/trackmate/action/IcyTrackExporter.java b/src/main/java/fiji/plugin/trackmate/action/IcyTrackExporter.java index 7a69943df..8a3ef33fd 100644 --- a/src/main/java/fiji/plugin/trackmate/action/IcyTrackExporter.java +++ b/src/main/java/fiji/plugin/trackmate/action/IcyTrackExporter.java @@ -31,9 +31,7 @@ import org.scijava.plugin.Plugin; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.io.IOUtils; import fiji.plugin.trackmate.io.IcyTrackFormatWriter; @@ -51,10 +49,10 @@ public class IcyTrackExporter extends AbstractTMAction private static final String KEY = "ICY_EXPORTER"; @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { logger.log( "Exporting tracks to Icy format.\n" ); - final Model model = trackmate.getModel(); + final Model model = guiModel.getModel(); final int ntracks = model.getTrackModel().nTracks( true ); if ( ntracks == 0 ) { @@ -65,7 +63,7 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo File folder; try { - folder = new File( trackmate.getSettings().imp.getOriginalFileInfo().directory ); + folder = new File( guiModel.getSettings().imp.getOriginalFileInfo().directory ); } catch ( final NullPointerException npe ) { @@ -75,7 +73,7 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo File file; try { - String filename = trackmate.getSettings().imageFileName; + String filename = guiModel.getSettings().imageFileName; final int dotLoca = filename.indexOf( "." ); if ( dotLoca > 0 ) filename = filename.substring( 0, dotLoca ); @@ -95,9 +93,9 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo logger.log( " Writing to file.\n" ); final double[] calibration = new double[ 3 ]; - calibration[ 0 ] = trackmate.getSettings().dx; - calibration[ 1 ] = trackmate.getSettings().dy; - calibration[ 2 ] = trackmate.getSettings().dz; + calibration[ 0 ] = guiModel.getSettings().dx; + calibration[ 1 ] = guiModel.getSettings().dy; + calibration[ 2 ] = guiModel.getSettings().dz; final IcyTrackFormatWriter writer = new IcyTrackFormatWriter( file, model, calibration ); if ( !writer.checkInput() || !writer.process() ) diff --git a/src/main/java/fiji/plugin/trackmate/action/LabelImgExporter.java b/src/main/java/fiji/plugin/trackmate/action/LabelImgExporter.java index e7ab744c3..f468ad51a 100644 --- a/src/main/java/fiji/plugin/trackmate/action/LabelImgExporter.java +++ b/src/main/java/fiji/plugin/trackmate/action/LabelImgExporter.java @@ -2,18 +2,18 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * 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 * . @@ -35,13 +35,10 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; -import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.TrackModel; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; -import fiji.plugin.trackmate.util.SpotUtil; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.util.TMUtils; import fiji.plugin.trackmate.visualization.GlasbeyLut; import ij.ImagePlus; @@ -79,7 +76,7 @@ public class LabelImgExporter extends AbstractTMAction public static final String NAME = "Export label image"; @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame gui ) + public void execute( final GuiModel guiModel, final Frame gui ) { /* * Ask use for option. @@ -117,78 +114,9 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo * Generate label image. */ - createLabelImagePlus( trackmate, exportSpotsAsDots, exportTracksOnly, labelIdPainting, logger ).show(); - } - - /** - * Creates a new label {@link ImagePlus} where the spots of the specified - * model are painted with their shape, with their track ID as pixel value. - * - * @param trackmate - * the trackmate instance from which we takes the spots to paint. - * The label image will have the same calibration, name and - * dimension from the input image stored in the trackmate - * settings. The output label image will have the same size that - * of this input image, except for the number of channels, which - * will be 1. - * @param exportSpotsAsDots - * if true, spots will be painted as single dots. If - * false they will be painted with their shape. - * @param exportTracksOnly - * if true, only the spots belonging to visible - * tracks will be painted. If false, spots not - * belonging to a track will be painted with a unique ID, - * different from the track IDs and different for each spot. - * @param labeIdPainting - * specifies how to paint the label ID of spots. - * - * @return a new {@link ImagePlus}. - */ - public static final ImagePlus createLabelImagePlus( - final TrackMate trackmate, - final boolean exportSpotsAsDots, - final boolean exportTracksOnly, - final LabelIdPainting labeIdPainting - ) - { - return createLabelImagePlus( trackmate, exportSpotsAsDots, exportTracksOnly, labeIdPainting, Logger.VOID_LOGGER ); - } - - /** - * Creates a new label {@link ImagePlus} where the spots of the specified - * model are painted with their shape, with their track ID as pixel value. - * - * @param trackmate - * the trackmate instance from which we takes the spots to paint. - * The label image will have the same calibration, name and - * dimension from the input image stored in the trackmate - * settings. The output label image will have the same size that - * of this input image, except for the number of channels, which - * will be 1. - * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead their shape. - * @param exportTracksOnly - * if true, only the spots belonging to visible - * tracks will be painted. If false, spots not - * belonging to a track will be painted with a unique ID, - * different from the track IDs and different for each spot. - * @param labelIdPainting - * specifies how to paint the label ID of spots. - * @param logger - * a {@link Logger} instance, to report progress of the export - * process. - * - * @return a new {@link ImagePlus}. - */ - public static final ImagePlus createLabelImagePlus( - final TrackMate trackmate, - final boolean exportSpotsAsDots, - final boolean exportTracksOnly, - final LabelIdPainting labelIdPainting, - final Logger logger ) - { - return createLabelImagePlus( trackmate.getModel(), trackmate.getSettings().imp, exportSpotsAsDots, exportTracksOnly, labelIdPainting, logger ); + final Model model = guiModel.getModel(); + final ImagePlus imp = guiModel.getSettings().imp; + createLabelImagePlus( model, imp, exportSpotsAsDots, exportTracksOnly, labelIdPainting, logger ).show(); } /** @@ -203,8 +131,8 @@ public static final ImagePlus createLabelImagePlus( * source image, except for the number of channels, which will be * 1. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param exportTracksOnly * if true, only the spots belonging to visible * tracks will be painted. If false, spots not @@ -237,8 +165,8 @@ public static final ImagePlus createLabelImagePlus( * source image, except for the number of channels, which will be * 1. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param exportTracksOnly * if true, only the spots belonging to visible * tracks will be painted. If false, spots not @@ -286,12 +214,11 @@ public static final ImagePlus createLabelImagePlus( * nZSlices, nFrames) as a 4 element long array. Spots outside * these dimensions are ignored. * @param calibration - * the desired calibration of the output image (pixel width, - * pixel height, pixel depth, frame interval) as a 4 element - * double array. + * the pixel size to map physical spot coordinates to pixel + * coordinates. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param exportTracksOnly * if true, only the spots belonging to visible * tracks will be painted. If false, spots not @@ -324,12 +251,11 @@ public static final ImagePlus createLabelImagePlus( * nZSlices, nFrames) as a 4 element int array. Spots outside * these dimensions are ignored. * @param calibration - * the desired calibration of the output image (pixel width, - * pixel height, pixel depth, frame interval) as a 4 element - * double array. + * the pixel size to map physical spot coordinates to pixel + * coordinates. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param exportTracksOnly * if true, only the spots belonging to visible * tracks will be painted. If false, spots not @@ -376,12 +302,11 @@ public static final ImagePlus createLabelImagePlus( * nZSlices, nFrames) as a 4 element long array. Spots outside * these dimensions are ignored. * @param calibration - * the desired calibration of the output image (pixel width, - * pixel height, pixel depth, frame interval) as a 4 element - * double array. + * the pixel size to map physical spot coordinates to pixel + * coordinates. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param exportTracksOnly * if true, only the spots belonging to visible * tracks will be painted. If false, spots not @@ -414,12 +339,11 @@ public static final Img< FloatType > createLabelImg( * nZSlices, nFrames) as a 4 element long array. Spots outside * these dimensions are ignored. * @param calibration - * the desired calibration of the output image (pixel width, - * pixel height, pixel depth, frame interval) as a 4 element - * double. + * the pixel size to map physical spot coordinates to pixel + * coordinates. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param exportTracksOnly * if true, only the spots belonging to visible * tracks will be painted. If false, spots not @@ -470,8 +394,7 @@ public static final Img< FloatType > createLabelImg( final ImgPlus< FloatType > imgCT = TMUtils.hyperSlice( imgPlus, 0, frame ); final SpotWriter spotWriter = exportSpotsAsDots ? new SpotAsDotWriter<>( imgCT ) - : new SpotRoiWriter<>( imgCT ); - idGenerator.nextFrame(); + : new SpotShapeWriter<>( imgCT ); for ( final Spot spot : model.getSpots().iterable( frame, true ) ) { @@ -487,9 +410,9 @@ public static final Img< FloatType > createLabelImg( /** - * Creates a new label {@link ImgPlus} of specified pixel type where the - * spots are painted with an ID. All visible spots are painted, whether they - * are in a track or not. + * Creates a new label {@link ImgPlus} of {@link FloatType} where the spots + * are painted with an ID. All visible spots are painted, whether they are + * in a track or not. * * @param spots * the spots to paint. @@ -498,24 +421,23 @@ public static final Img< FloatType > createLabelImg( * nZSlices, nFrames) as a 4 element long array. Spots outside * these dimensions are ignored. * @param calibration - * the desired calibration of the output image (pixel width, - * pixel height, pixel depth, frame interval) as a 4 element - * double array. + * the pixel size to map physical spot coordinates to pixel + * coordinates. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead of their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param labelIdPainting * specifies how to paint the label ID of spots. The * {@link LabelIdPainting#LABEL_IS_TRACK_ID} is not supported and * defaults to {@link LabelIdPainting#LABEL_IS_SPOT_ID}. * @param outputType - * the output pixel type. + * the pixel type of the output image. * @param logger * a {@link Logger} instance, to report progress of the export * process. - * @param - * the pixel type of the output image. * @return a new {@link ImgPlus}. + * @param + * the pixel type. */ public static < T extends RealType< T > & NativeType< T > > ImgPlus< T > createLabelImg( final SpotCollection spots, @@ -569,7 +491,7 @@ public static < T extends RealType< T > & NativeType< T > > ImgPlus< T > createL final ImgPlus< T > imgCT = TMUtils.hyperSlice( imgPlus, 0, frame ); final SpotWriter spotWriter = exportSpotsAsDots ? new SpotAsDotWriter<>( imgCT ) - : new SpotRoiWriter<>( imgCT ); + : new SpotShapeWriter<>( imgCT ); idGenerator.nextFrame(); for ( final Spot spot : spots.iterable( frame, true ) ) @@ -627,12 +549,12 @@ public static interface SpotWriter public void write( Spot spot, int id ); } - public static final class SpotRoiWriter< T extends RealType< T > > implements SpotWriter + public static final class SpotShapeWriter< T extends RealType< T > > implements SpotWriter { private final ImgPlus< T > img; - public SpotRoiWriter( final ImgPlus< T > img ) + public SpotShapeWriter( final ImgPlus< T > img ) { this.img = img; } @@ -640,7 +562,7 @@ public SpotRoiWriter( final ImgPlus< T > img ) @Override public void write( final Spot spot, final int id ) { - for ( final T pixel : SpotUtil.iterable( spot, img ) ) + for ( final T pixel : spot.iterable( img ) ) pixel.setReal( id ); } } diff --git a/src/main/java/fiji/plugin/trackmate/action/MergeFileAction.java b/src/main/java/fiji/plugin/trackmate/action/MergeFileAction.java index 917243db4..fd3e299c7 100644 --- a/src/main/java/fiji/plugin/trackmate/action/MergeFileAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/MergeFileAction.java @@ -37,10 +37,9 @@ import org.scijava.plugin.Plugin; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.io.IOUtils; import fiji.plugin.trackmate.io.TmXmlReader; import fiji.plugin.trackmate.util.TMUtils; @@ -66,9 +65,9 @@ public class MergeFileAction extends AbstractTMAction + ""; @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { - File file = TMUtils.proposeTrackMateSaveFile( trackmate.getSettings(), logger ); + File file = TMUtils.proposeTrackMateSaveFile( guiModel.getSettings(), logger ); if ( null == file ) { final File folder = new File( System.getProperty( "user.dir" ) ).getParentFile().getParentFile(); @@ -91,7 +90,7 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo // Model final Model modelToMerge = reader.getModel(); - final Model model = trackmate.getModel(); + final Model model = guiModel.getModel(); final int nNewTracks = modelToMerge.getTrackModel().nTracks( true ); int progress = 0; @@ -117,7 +116,7 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo * An awkward way to avoid spot ID conflicts after loading * two files */ - newSpot = new Spot( oldSpot ); + newSpot = new SpotBase( oldSpot ); for ( final String feature : oldSpot.getFeatures().keySet() ) newSpot.putFeature( feature, oldSpot.getFeature( feature ) ); @@ -148,7 +147,7 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo final String trackName = modelToMerge.getTrackModel().name( id ); final int newId = model.getTrackModel().trackIDOf( newSpot ); - model.getTrackModel().setName( newId, trackName ); + model.setTrackName( newId, trackName ); progress++; logger.setProgress( ( double ) progress / nNewTracks ); diff --git a/src/main/java/fiji/plugin/trackmate/action/MeshSeriesExporter.java b/src/main/java/fiji/plugin/trackmate/action/MeshSeriesExporter.java new file mode 100644 index 000000000..8562d747d --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/action/MeshSeriesExporter.java @@ -0,0 +1,174 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.action; + +import static fiji.plugin.trackmate.gui.Icons.ORANGE_ASTERISK_ICON; + +import java.awt.Frame; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.NavigableSet; + +import javax.swing.ImageIcon; + +import org.scijava.plugin.Plugin; + +import fiji.plugin.trackmate.Logger; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.io.IOUtils; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.mesh.io.ply.PLYMeshIO; +import net.imglib2.mesh.view.TranslateMesh; + +public class MeshSeriesExporter extends AbstractTMAction +{ + + public static final String NAME = "Export spot 3D meshes to a file series"; + + public static final String KEY = "MESH_SERIES_EXPORTER"; + + public static final String INFO_TEXT = "" + + "Export the 3D meshes in the spot of the current model " + + "to a PLY file series. " + + "

" + + "A folder is created with the file name, in which " + + "there will be one PLY file per time-point. " + + "The series can be easily imported in mesh visualization " + + "softwares, such as ParaView. " + + "

" + + "Only the visible spots containing 3D meshes are exported. " + + "If there are no such spots, no file is created. " + + ""; + + @Override + public void execute( final GuiModel guiModel, final Frame parent ) + { + logger.log( "Exporting spot 3D meshes to a file series.\n" ); + final Model model = guiModel.getModel(); + File file; + final File folder = new File( System.getProperty( "user.dir" ) ).getParentFile().getParentFile(); + try + { + String filename = guiModel.getSettings().imageFileName; + int i = filename.indexOf( "." ); + if ( i < 0 ) + i = filename.length(); + filename = filename.substring( 0, i ); + file = new File( folder.getPath() + File.separator + filename + "-meshes.ply" ); + } + catch ( final NullPointerException npe ) + { + file = new File( folder.getPath() + File.separator + "TrackMateMeshes.ply" ); + } + file = IOUtils.askForFileForSaving( file, parent ); + if ( null == file ) + { + logger.log( "Aborted.\n" ); + return; + } + + exportMeshesToFileSeries( model.getSpots(), file, logger ); + } + + public static void exportMeshesToFileSeries( final SpotCollection spots, final File file, final Logger logger ) + { + String folderName = file.getAbsolutePath(); + folderName = folderName.substring( 0, folderName.indexOf( "." ) ); + final File folder = new File( folderName ); + folder.mkdirs(); + + final NavigableSet< Integer > frames = spots.keySet(); + for ( final Integer frame : frames ) + { + final String fileName = folder.getName() + '_' + frame + ".ply"; + final File targetFile = new File( folder, fileName ); + final List< Mesh > meshes = new ArrayList<>(); + for ( final Spot spot : spots.iterable( frame, true ) ) + { + if ( spot instanceof SpotMesh ) + { + final SpotMesh sm = ( SpotMesh ) spot; + meshes.add( TranslateMesh.translate( sm.getMesh(), spot ) ); + } + } + logger.log( " - Found " + meshes.size() + " meshes in frame " + frame + "." ); + final Mesh merged = Meshes.merge( meshes ); + final BufferMesh mesh = new BufferMesh( merged.vertices().size(), merged.triangles().size() ); + Meshes.calculateNormals( merged, mesh ); + try + { + PLYMeshIO.save( mesh, targetFile.getAbsolutePath() ); + } + catch ( final IOException e ) + { + logger.error( "\nProblem writing to " + targetFile + '\n' + e.getMessage() + '\n' ); + e.printStackTrace(); + continue; + } + logger.log( " Saved.\n" ); + } + logger.log( "Done. Meshes saved to folder " + folder + '\n' ); + } + + @Plugin( type = TrackMateActionFactory.class, visible = true ) + public static class Factory implements TrackMateActionFactory + { + + @Override + public String getInfoText() + { + return INFO_TEXT; + } + + @Override + public String getName() + { + return NAME; + } + + @Override + public String getKey() + { + return KEY; + } + + @Override + public ImageIcon getIcon() + { + return ORANGE_ASTERISK_ICON; + } + + @Override + public TrackMateAction create() + { + return new MeshSeriesExporter(); + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/action/MotilityLabExporter.java b/src/main/java/fiji/plugin/trackmate/action/MotilityLabExporter.java index 4a7760b1b..58026a127 100644 --- a/src/main/java/fiji/plugin/trackmate/action/MotilityLabExporter.java +++ b/src/main/java/fiji/plugin/trackmate/action/MotilityLabExporter.java @@ -28,7 +28,6 @@ import java.awt.Component; import java.awt.Dimension; import java.awt.Frame; -import java.io.File; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.ArrayList; @@ -53,16 +52,9 @@ import org.scijava.plugin.Plugin; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.TrackModel; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsIO; -import fiji.plugin.trackmate.io.TmXmlReader; -import ij.ImageJ; -import ij.ImagePlus; +import fiji.plugin.trackmate.gui.GuiModel; public class MotilityLabExporter extends AbstractTMAction { @@ -96,13 +88,9 @@ public class MotilityLabExporter extends AbstractTMAction Double.class ); @Override - public void execute( - final TrackMate trackmate, - final SelectionModel selectionModel, - final DisplaySettings displaySettings, - final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { - final JPanel panel = createSpotTable( trackmate.getModel() ); + final JPanel panel = createSpotTable( guiModel.getModel() ); final JFrame frame = new JFrame( "TrackMate MotilityLab table export" ); frame.setIconImage( TRACK_TABLES_ICON.getImage() ); @@ -304,20 +292,4 @@ public ImageIcon getIcon() return TRACK_TABLES_ICON; } } - - public static void main( final String[] args ) - { - ImageJ.main( args ); - - final TmXmlReader reader = new TmXmlReader( new File("samples/MAX_Merged.xml" )); - final Model model = reader.getModel(); - final ImagePlus imp = reader.readImage(); - final Settings settings = reader.readSettings( imp ); - new MotilityLabExporter().execute( - new TrackMate( model, settings ), - new SelectionModel( model ), - DisplaySettingsIO.readUserDefault(), - null ); - } - } diff --git a/src/main/java/fiji/plugin/trackmate/action/PlotNSpotsVsTimeAction.java b/src/main/java/fiji/plugin/trackmate/action/PlotNSpotsVsTimeAction.java index 65333699c..f035156fd 100644 --- a/src/main/java/fiji/plugin/trackmate/action/PlotNSpotsVsTimeAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/PlotNSpotsVsTimeAction.java @@ -43,9 +43,9 @@ import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; -import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.features.AbstractFeatureGrapher; import fiji.plugin.trackmate.features.ModelDataset; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; @@ -63,15 +63,11 @@ public class PlotNSpotsVsTimeAction extends AbstractTMAction ""; @Override - public void execute( - final TrackMate trackmate, - final SelectionModel selectionModel, - final DisplaySettings displaySettings, - final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { // Collect data - final Model model = trackmate.getModel(); - final Settings settings = trackmate.getSettings(); + final Model model = guiModel.getModel(); + final Settings settings = guiModel.getSettings(); final SpotCollection spots = model.getSpots(); final int maxFrame = spots.keySet().stream().mapToInt( Integer::intValue ).max().getAsInt(); @@ -83,6 +79,8 @@ public void execute( nSpots[ frame ] = spots.getNSpots( frame, true ); time[ frame ] = frame * settings.dt; } + final SelectionModel selectionModel = guiModel.getSelectionModel(); + final DisplaySettings displaySettings = guiModel.getDisplaySettings(); final NSpotPerFrameDataset dataset = new NSpotPerFrameDataset( model, selectionModel, displaySettings, time, nSpots ); final String yFeature = "N spots"; final Map< String, Dimension > dimMap = new HashMap<>( 2 ); diff --git a/src/main/java/fiji/plugin/trackmate/action/RecomputeFeatureAction.java b/src/main/java/fiji/plugin/trackmate/action/RecomputeFeatureAction.java index b5f9f9c34..9af450a88 100644 --- a/src/main/java/fiji/plugin/trackmate/action/RecomputeFeatureAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/RecomputeFeatureAction.java @@ -31,11 +31,10 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; public class RecomputeFeatureAction extends AbstractTMAction { @@ -52,9 +51,9 @@ public class RecomputeFeatureAction extends AbstractTMAction @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { - recompute( trackmate, logger ); + recompute( guiModel.getTrackMate(), logger ); } @Plugin( type = TrackMateActionFactory.class ) diff --git a/src/main/java/fiji/plugin/trackmate/action/ResetSpotTimeFeatureAction.java b/src/main/java/fiji/plugin/trackmate/action/ResetSpotTimeFeatureAction.java index 2e320e4cd..1bfabf9d7 100644 --- a/src/main/java/fiji/plugin/trackmate/action/ResetSpotTimeFeatureAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/ResetSpotTimeFeatureAction.java @@ -34,11 +34,9 @@ import org.scijava.plugin.Plugin; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; -import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; public class ResetSpotTimeFeatureAction extends AbstractTMAction { @@ -53,14 +51,14 @@ public class ResetSpotTimeFeatureAction extends AbstractTMAction { @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { logger.log("Reset spot time.\n"); - double dt = trackmate.getSettings().dt; + double dt = guiModel.getSettings().dt; if (dt == 0) { dt = 1; } - final SpotCollection spots = trackmate.getModel().getSpots(); + final SpotCollection spots = guiModel.getModel().getSpots(); final Set frames = spots.keySet(); for(final int frame : frames) { for (final Iterator iterator = spots.iterator(frame, true); iterator.hasNext();) { diff --git a/src/main/java/fiji/plugin/trackmate/action/TrackBranchAnalysis.java b/src/main/java/fiji/plugin/trackmate/action/TrackBranchAnalysis.java index ea50ab8c0..64202281c 100644 --- a/src/main/java/fiji/plugin/trackmate/action/TrackBranchAnalysis.java +++ b/src/main/java/fiji/plugin/trackmate/action/TrackBranchAnalysis.java @@ -29,10 +29,7 @@ import org.scijava.plugin.Plugin; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.util.TMUtils; import fiji.plugin.trackmate.visualization.table.BranchTableView; @@ -56,14 +53,14 @@ public class TrackBranchAnalysis extends AbstractTMAction public static final String DOC_URL = "https://imagej.net/plugins/trackmate/actions/branch-hierarchy-analysis"; @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { - createBranchTable( trackmate.getModel(), selectionModel, TMUtils.getImagePathWithoutExtension( trackmate.getSettings() ) ).render(); + createBranchTable( guiModel, TMUtils.getImagePathWithoutExtension( guiModel.getSettings() ) ).render(); } - public static final BranchTableView createBranchTable( final Model model, final SelectionModel selectionModel, final String imageFileName ) + public static final BranchTableView createBranchTable( final GuiModel guiModel, final String imageFileName ) { - return new BranchTableView( model, selectionModel, imageFileName ); + return new BranchTableView( guiModel, imageFileName ); } @Plugin( type = TrackMateActionFactory.class, enabled = true ) diff --git a/src/main/java/fiji/plugin/trackmate/action/TrackMateAction.java b/src/main/java/fiji/plugin/trackmate/action/TrackMateAction.java index 6408dc6a1..a9f658129 100644 --- a/src/main/java/fiji/plugin/trackmate/action/TrackMateAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/TrackMateAction.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -24,9 +24,8 @@ import java.awt.Frame; import fiji.plugin.trackmate.Logger; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; /** * This interface describe a track mate action, that can be run on a @@ -38,25 +37,20 @@ public interface TrackMateAction { /** - * Executes this action within an application specified by the parameters. + * Executes this action. * - * @param trackmate - * the {@link TrackMate} instance to use to execute the action. - * @param selectionModel - * the {@link SelectionModel} currently used in the application, - * @param displaySettings - * the {@link DisplaySettings} used to render the views in the - * application. + * @param guiModel + * the {@link GuiModel} that contains the required data. * @param parent * the user-interface parent window. */ - public void execute( TrackMate trackmate, SelectionModel selectionModel, DisplaySettings displaySettings, Frame parent ); + public void execute( GuiModel guiModel, Frame parent ); /** * Sets the logger that will receive logs when this action is executed. - * + * * @param logger - * the logger to use. + * the logger. */ public void setLogger( Logger logger ); } diff --git a/src/main/java/fiji/plugin/trackmate/action/TrimNotVisibleAction.java b/src/main/java/fiji/plugin/trackmate/action/TrimNotVisibleAction.java index 45f7c9fad..9be77bfb8 100644 --- a/src/main/java/fiji/plugin/trackmate/action/TrimNotVisibleAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/TrimNotVisibleAction.java @@ -32,12 +32,10 @@ import org.scijava.plugin.Plugin; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; -import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.TrackModel; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; public class TrimNotVisibleAction extends AbstractTMAction { @@ -60,13 +58,13 @@ public class TrimNotVisibleAction extends AbstractTMAction public static final String NAME = "Trim non-visible data"; @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { - final Model model = trackmate.getModel(); + final Model model = guiModel.getModel(); final TrackModel tm = model.getTrackModel(); final SpotCollection spots = new SpotCollection(); - spots.setNumThreads( trackmate.getNumThreads() ); + spots.setNumThreads( guiModel.getTrackMate().getNumThreads() ); final Collection< Spot > toRemove = new ArrayList<>(); for ( final Integer trackID : tm.unsortedTrackIDs( false ) ) diff --git a/src/main/java/fiji/plugin/trackmate/action/autonaming/AutoNamingAction.java b/src/main/java/fiji/plugin/trackmate/action/autonaming/AutoNamingAction.java index 5ca105d2b..55db72782 100644 --- a/src/main/java/fiji/plugin/trackmate/action/autonaming/AutoNamingAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/autonaming/AutoNamingAction.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 * . @@ -25,25 +25,23 @@ import org.scijava.plugin.Plugin; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.action.AbstractTMAction; import fiji.plugin.trackmate.action.TrackMateAction; import fiji.plugin.trackmate.action.TrackMateActionFactory; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.Icons; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; public class AutoNamingAction extends AbstractTMAction { public static final String INFO_TEXT = "" + "Rename individual spots based on auto-naming rules. " - + "All spot names are changed. There is no undo."; + + "All spot names are changed. Can be undone."; @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final java.awt.Frame parent ) + public void execute( final GuiModel guiModel, final java.awt.Frame parent ) { - final AutoNamingController controller = new AutoNamingController( trackmate, logger ); + final AutoNamingController controller = new AutoNamingController( guiModel, logger ); controller.show(); } diff --git a/src/main/java/fiji/plugin/trackmate/action/autonaming/AutoNamingController.java b/src/main/java/fiji/plugin/trackmate/action/autonaming/AutoNamingController.java index f19e8852b..6bd26dff5 100644 --- a/src/main/java/fiji/plugin/trackmate/action/autonaming/AutoNamingController.java +++ b/src/main/java/fiji/plugin/trackmate/action/autonaming/AutoNamingController.java @@ -28,24 +28,26 @@ import javax.swing.JLabel; import fiji.plugin.trackmate.Logger; -import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; import fiji.plugin.trackmate.util.Threads; +import ij.ImagePlus; public class AutoNamingController { - private final TrackMate trackmate; - private final AutoNamingPanel gui; private final Logger logger; - public AutoNamingController( final TrackMate trackmate, final Logger logger ) + private final GuiModel guiModel; + + public AutoNamingController( final GuiModel guiModel, final Logger logger ) { - this.trackmate = trackmate; + this.guiModel = guiModel; this.logger = logger; final Collection< AutoNamingRule > namingRules = new ArrayList<>( 3 ); @@ -60,6 +62,7 @@ public AutoNamingController( final TrackMate trackmate, final Logger logger ) private void run( final AutoNamingRule autoNaming ) { + final Model model = guiModel.getModel(); final EverythingDisablerAndReenabler disabler = new EverythingDisablerAndReenabler( gui, new Class[] { JLabel.class } ); disabler.disable(); Threads.run( "TrackMateAutoNamingThread", () -> @@ -68,8 +71,8 @@ private void run( final AutoNamingRule autoNaming ) { logger.log( "Applying naming rule: " + autoNaming.toString() + ".\n" ); logger.setStatus( "Spot auto-naming" ); - AutoNamingPerformer.autoNameSpots( trackmate.getModel(), autoNaming ); - trackmate.getModel().notifyFeaturesComputed(); + AutoNamingPerformer.autoNameSpots( model, autoNaming ); + model.notifyFeaturesComputed(); logger.log( "Spot auto-naming done.\n" ); } finally @@ -88,7 +91,11 @@ public void show() frame.setIconImage( Icons.TRACK_SCHEME_ICON.getImage() ); frame.setSize( 500, 400 ); frame.getContentPane().add( gui ); - GuiUtils.positionWindow( frame, trackmate.getSettings().imp.getCanvas() ); + final ImagePlus imp = guiModel.getSettings().imp; + if ( imp != null ) + GuiUtils.positionWindow( frame, imp.getCanvas() ); + else + frame.setLocationRelativeTo( null ); frame.setVisible( true ); } } diff --git a/src/main/java/fiji/plugin/trackmate/action/autonaming/AutoNamingPerformer.java b/src/main/java/fiji/plugin/trackmate/action/autonaming/AutoNamingPerformer.java index a9e6537a9..9eeaa9346 100644 --- a/src/main/java/fiji/plugin/trackmate/action/autonaming/AutoNamingPerformer.java +++ b/src/main/java/fiji/plugin/trackmate/action/autonaming/AutoNamingPerformer.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 * . @@ -44,20 +44,29 @@ public class AutoNamingPerformer public static void autoNameSpots( final Model model, final AutoNamingRule rule ) { - final TimeDirectedNeighborIndex neighborIndex = model.getTrackModel().getDirectedNeighborIndex(); - for ( final Integer trackID : model.getTrackModel().unsortedTrackIDs( true ) ) + model.beginUpdate(); + try { - final TrackBranchDecomposition branchDecomposition = ConvexBranchesDecomposition.processTrack( trackID, model.getTrackModel(), neighborIndex, true, false ); - final SimpleDirectedGraph< List< Spot >, DefaultEdge > branchGraph = ConvexBranchesDecomposition.buildBranchGraph( branchDecomposition ); - processTrack( rule, model.getTrackModel(), branchGraph ); + final TimeDirectedNeighborIndex neighborIndex = model.getTrackModel().getDirectedNeighborIndex(); + for ( final Integer trackID : model.getTrackModel().unsortedTrackIDs( true ) ) + { + final TrackBranchDecomposition branchDecomposition = ConvexBranchesDecomposition.processTrack( trackID, model.getTrackModel(), neighborIndex, true, false ); + final SimpleDirectedGraph< List< Spot >, DefaultEdge > branchGraph = ConvexBranchesDecomposition.buildBranchGraph( branchDecomposition ); + processTrack( rule, model, branchGraph ); + } + } + finally + { + model.endUpdate(); } } private static void processTrack( - final AutoNamingRule rule, - final TrackModel model, + final AutoNamingRule rule, + final Model model, final SimpleDirectedGraph< List< Spot >, DefaultEdge > graph ) { + final TrackModel trackModel = model.getTrackModel(); // Find the roots. Might be several. final List< List< Spot > > roots = graph.vertexSet().stream() .filter( key -> graph.incomingEdgesOf( key ).size() == 0 ) @@ -67,13 +76,15 @@ private static void processTrack( { // Name the spots in the root branch. final Spot first = root.get( 0 ); - rule.nameRoot( first, model ); + model.beforeEdit( first ); // undo name changes. + rule.nameRoot( first, trackModel ); // Other spots in the root branch. Spot predecessor = first; for ( int i = 1; i < root.size(); i++ ) { final Spot current = root.get( i ); + model.beforeEdit( current ); // undo name changes. rule.nameSpot( current, predecessor ); predecessor = current; } @@ -102,8 +113,10 @@ private static void processTrack( final Spot mother = currentBranch.get( currentBranch.size() - 1 ); // Name the branch first spots. + for ( final Spot sibling : siblings ) + model.beforeEdit( sibling ); // undo name changes. rule.nameBranches( mother, siblings ); - + // Name the spots inside each branch. for ( final List< Spot > cb : childrenBranches ) { @@ -111,6 +124,7 @@ private static void processTrack( for ( int i = 1; i < cb.size(); i++ ) { final Spot current = cb.get( i ); + model.beforeEdit( current ); // undo name changes. rule.nameSpot( current, parent ); parent = current; } diff --git a/src/main/java/fiji/plugin/trackmate/action/closegaps/CloseGapsAction.java b/src/main/java/fiji/plugin/trackmate/action/closegaps/CloseGapsAction.java index 484f1852c..91016e44f 100644 --- a/src/main/java/fiji/plugin/trackmate/action/closegaps/CloseGapsAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/closegaps/CloseGapsAction.java @@ -27,13 +27,11 @@ import org.scijava.plugin.Plugin; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.action.AbstractTMAction; import fiji.plugin.trackmate.action.TrackMateAction; import fiji.plugin.trackmate.action.TrackMateActionFactory; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.Icons; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; public class CloseGapsAction extends AbstractTMAction { @@ -61,9 +59,9 @@ public class CloseGapsAction extends AbstractTMAction public static final String DOC_URL = "https://imagej.net/plugins/trackmate/actions/close-gaps-action"; @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + public void execute( final GuiModel guiModel, final Frame parent ) { - final CloseGapsController controller = new CloseGapsController( trackmate, logger ); + final CloseGapsController controller = new CloseGapsController( guiModel.getTrackMate(), logger ); controller.show(); } diff --git a/src/main/java/fiji/plugin/trackmate/action/closegaps/CloseGapsByDetection.java b/src/main/java/fiji/plugin/trackmate/action/closegaps/CloseGapsByDetection.java index 1c455621a..637ee88bf 100644 --- a/src/main/java/fiji/plugin/trackmate/action/closegaps/CloseGapsByDetection.java +++ b/src/main/java/fiji/plugin/trackmate/action/closegaps/CloseGapsByDetection.java @@ -21,7 +21,6 @@ */ package fiji.plugin.trackmate.action.closegaps; -import java.io.File; import java.util.ArrayDeque; import java.util.Collections; import java.util.List; @@ -30,21 +29,10 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.TrackModel; -import fiji.plugin.trackmate.detection.DetectorKeys; -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.gui.displaysettings.DisplaySettingsIO; -import fiji.plugin.trackmate.io.TmXmlReader; -import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; -import fiji.plugin.trackmate.visualization.trackscheme.TrackScheme; -import ij.ImageJ; -import ij.ImagePlus; import net.imglib2.util.Util; /** @@ -188,37 +176,4 @@ public String toString() { return NAME; } - - public static void main( final String[] args ) - { - ImageJ.main( args ); - -// final String filePath = "/Users/tinevez/Desktop/GaelleGapClosingWeirdBug/Simple2-211116MovieFDBYFP_Movie0-01-Scene-64-TR114.xml"; - final String filePath = "/Users/tinevez/Desktop/GaelleGapClosingWeirdBug/211116 Movie FDB YFP_Movie 0-01-Scene-64-TR114-zeroed.xml"; - final TmXmlReader reader = new TmXmlReader( new File( filePath ) ); - if ( !reader.isReadingOk() ) - { - System.err.println( reader.getErrorMessage() ); - return; - } - final Model model = reader.getModel(); - final ImagePlus imp = reader.readImage(); - final Settings settings = reader.readSettings( imp ); - final TrackMate trackmate = new TrackMate( model, settings ); - trackmate.setNumThreads( 5 ); - - settings.detectorSettings.put( DetectorKeys.KEY_RADIUS, 8. ); - - final CloseGapsByDetection gapCloser = new CloseGapsByDetection(); - gapCloser.getParameters().get( 0 ).value = 2.; - gapCloser.execute( trackmate, Logger.DEFAULT_LOGGER ); - - final SelectionModel selectionModel = new SelectionModel( model ); - final DisplaySettings ds = DisplaySettingsIO.readUserDefault(); - ds.setSpotColorBy( TrackMateObject.TRACKS, TrackIndexAnalyzer.TRACK_INDEX ); - ds.setTrackColorBy( TrackMateObject.TRACKS, TrackIndexAnalyzer.TRACK_INDEX ); - - new TrackScheme( model, selectionModel, ds ).render(); - new HyperStackDisplayer( model, selectionModel, settings.imp, ds ).render(); - } } diff --git a/src/main/java/fiji/plugin/trackmate/action/closegaps/CloseGapsPanel.java b/src/main/java/fiji/plugin/trackmate/action/closegaps/CloseGapsPanel.java index 2012beb98..ae4d7d45a 100644 --- a/src/main/java/fiji/plugin/trackmate/action/closegaps/CloseGapsPanel.java +++ b/src/main/java/fiji/plugin/trackmate/action/closegaps/CloseGapsPanel.java @@ -39,11 +39,12 @@ import javax.swing.SwingConstants; import javax.swing.border.EmptyBorder; +import org.scijava.ui.config.visitors.gui.elements.SliderPanelDouble; +import org.scijava.ui.config.visitors.gui.elements.StyleElements; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.BoundedDoubleElement; + import fiji.plugin.trackmate.action.closegaps.GapClosingMethod.GapClosingParameter; import fiji.plugin.trackmate.gui.Fonts; -import fiji.plugin.trackmate.gui.displaysettings.SliderPanelDouble; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.BoundedDoubleElement; /** * A basic UI to let a TrackMate user choose between several techniques for gap diff --git a/src/main/java/fiji/plugin/trackmate/action/closegaps/GapClosingMethod.java b/src/main/java/fiji/plugin/trackmate/action/closegaps/GapClosingMethod.java index 4a2d3b916..90e73da00 100644 --- a/src/main/java/fiji/plugin/trackmate/action/closegaps/GapClosingMethod.java +++ b/src/main/java/fiji/plugin/trackmate/action/closegaps/GapClosingMethod.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -33,6 +33,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.TrackModel; import fiji.plugin.trackmate.detection.DetectionUtils; @@ -67,7 +68,7 @@ public GapClosingParameter( final String name, final double value, final double * Returns the list of parameters required to configure this method. *

* The list will be used to autogenerate a configuration panel. - * + * * @return a list of parameters. */ public default List< GapClosingParameter > getParameters() @@ -85,11 +86,11 @@ public default List< GapClosingParameter > getParameters() /** * Performs the gap closing. - * + * * @param trackmate - * the TrackMate instance to operate on. + * the trackmate instance to operate on. * @param logger - * a logger to log messages to. + * a logger instance to echoes the gap-closing process. */ public void execute( TrackMate trackmate, Logger logger ); @@ -100,7 +101,7 @@ public default List< GapClosingParameter > getParameters() * frames within a track. Gaps are returned as a list of edges. Each edge in * this list has a source spot and a target spot separated by strictly more * than 1 frame. - * + * * @param model * the model to search for gaps. * @return a list of edges corresponding to gaps. @@ -141,7 +142,7 @@ public static List< DefaultWeightedEdge > getAllGaps( final Model model ) * linked be edges. The spots are added in time order, from the frame * just after the source spot, to the frame just before the target spot. The * source and target spot are not in the list. - * + * * @param model * the model. * @param edge @@ -161,7 +162,7 @@ public static List< Spot > interpolate( final Model model, final DefaultWeighted final double[] tPos = new double[ 3 ]; target.localize( tPos ); final int tt = target.getFeature( Spot.FRAME ).intValue(); - + final List< Spot > interpolatedSpots = new ArrayList<>( Math.abs( tt - st ) - 1 ); final int presign = tt > st ? 1 : -1; @@ -175,7 +176,7 @@ public static List< Spot > interpolate( final Model model, final DefaultWeighted position[ d ] = weight * sPos[ d ] + ( 1.0 - weight ) * tPos[ d ]; final RealPoint rp = new RealPoint( position ); - final Spot newSpot = new Spot( rp, 0, 0 ); + final Spot newSpot = new SpotBase( rp, 0, 0 ); newSpot.putFeature( Spot.FRAME, Double.valueOf( f ) ); // Set some properties of the new spot @@ -202,7 +203,7 @@ static void interpolateFeature( final Spot targetSpot, final Spot spot1, final S * but configured with a small ROI centered on the specified spot, with a * radius proportional to the radius of the specified spot, and set to * operate only on the frame in which the specified spot it. - * + * * @param spot * the spot to read the coordinates and the frame from. * @param neighborhoodFactor diff --git a/src/main/java/fiji/plugin/trackmate/action/fit/AbstractSpotFitter.java b/src/main/java/fiji/plugin/trackmate/action/fit/AbstractSpotFitter.java index 22088dd65..b2c08ea1e 100644 --- a/src/main/java/fiji/plugin/trackmate/action/fit/AbstractSpotFitter.java +++ b/src/main/java/fiji/plugin/trackmate/action/fit/AbstractSpotFitter.java @@ -69,7 +69,6 @@ public abstract class AbstractSpotFitter implements SpotFitter private long processingTime = -1; - @SuppressWarnings( "unchecked" ) public AbstractSpotFitter( final ImagePlus imp, final int channel ) { this.channel = channel; diff --git a/src/main/java/fiji/plugin/trackmate/action/fit/SpotFitterController.java b/src/main/java/fiji/plugin/trackmate/action/fit/SpotFitterController.java index fa356d065..d1d04c9cd 100644 --- a/src/main/java/fiji/plugin/trackmate/action/fit/SpotFitterController.java +++ b/src/main/java/fiji/plugin/trackmate/action/fit/SpotFitterController.java @@ -36,6 +36,7 @@ import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.detection.DetectionUtils; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; @@ -45,24 +46,21 @@ public class SpotFitterController { - private final TrackMate trackmate; - - private final SelectionModel selectionModel; - private final SpotFitterPanel gui; private final Logger logger; private final Map< Spot, double[] > undo; - public SpotFitterController( final TrackMate trackmate, final SelectionModel selectionModel, final Logger logger ) + private final GuiModel guiModel; + + public SpotFitterController( final GuiModel guiModel, final Logger logger ) { - this.trackmate = trackmate; - this.selectionModel = selectionModel; + this.guiModel = guiModel; this.logger = logger; this.undo = new HashMap<>(); - final Settings settings = trackmate.getSettings(); + final Settings settings = guiModel.getSettings(); final List< String > fits = getAvailableFits( DetectionUtils.is2D( settings.imp ) ); final List< String > docs = getDocs( DetectionUtils.is2D( settings.imp ) ); this.gui = new SpotFitterPanel( fits, docs, settings.imp.getNChannels() ); @@ -94,13 +92,14 @@ private void undo() } logger.setProgress( 0. ); // Recompute features. + final TrackMate trackmate = guiModel.getTrackMate(); trackmate.computeSpotFeatures( true ); trackmate.computeEdgeFeatures( true ); trackmate.computeTrackFeatures( true ); logger.log( "Undoing done.\n" ); // Notify changes happened. - trackmate.getModel().getModelChangeListener().forEach( l -> l.modelChanged( new ModelChangeEvent( this, ModelChangeEvent.MODEL_MODIFIED ) ) ); + guiModel.getModel().getModelChangeListener().forEach( l -> l.modelChanged( new ModelChangeEvent( this, ModelChangeEvent.MODEL_MODIFIED ) ) ); } finally { @@ -117,7 +116,7 @@ private void fit() { try { - final ImagePlus imp = trackmate.getSettings().imp; + final ImagePlus imp = guiModel.getSettings().imp; // 1-based to 0-based. final int channel = gui.getSelectedChannel() - 1; final int index = gui.getSelectedFitIndex(); @@ -141,12 +140,13 @@ else if ( index == 1 ) else throw new IllegalArgumentException( "Index points to an unknown fit model: " + index ); } - fitter.setNumThreads( trackmate.getNumThreads() ); + fitter.setNumThreads( guiModel.getTrackMate().getNumThreads() ); // Get spots to fit. + final SelectionModel selectionModel = guiModel.getSelectionModel(); final Iterable< Spot > spots; if ( gui.rdbtnAll.isSelected() ) - spots = trackmate.getModel().getSpots().iterable( true ); + spots = guiModel.getModel().getSpots().iterable( true ); else if ( gui.rdbtnSelection.isSelected() ) spots = selectionModel.getSpotSelection(); else @@ -173,12 +173,13 @@ else if ( gui.rdbtnSelection.isSelected() ) fitter.process( spots, logger ); // Recompute features. + final TrackMate trackmate = guiModel.getTrackMate(); trackmate.computeSpotFeatures( true ); trackmate.computeEdgeFeatures( true ); trackmate.computeTrackFeatures( true ); // Notify changes happened. - trackmate.getModel().getModelChangeListener().forEach( l -> l.modelChanged( new ModelChangeEvent( this, ModelChangeEvent.MODEL_MODIFIED ) ) ); + guiModel.getModel().getModelChangeListener().forEach( l -> l.modelChanged( new ModelChangeEvent( this, ModelChangeEvent.MODEL_MODIFIED ) ) ); } finally { @@ -196,7 +197,7 @@ public void show() frame.setIconImage( Icons.SPOT_ICON.getImage() ); frame.setSize( 300, 300 ); frame.getContentPane().add( gui ); - GuiUtils.positionWindow( frame, trackmate.getSettings().imp.getCanvas() ); + GuiUtils.positionWindow( frame, guiModel.getSettings().imp.getCanvas() ); frame.setVisible( true ); } diff --git a/src/main/java/fiji/plugin/trackmate/action/fit/SpotFitterPanel.java b/src/main/java/fiji/plugin/trackmate/action/fit/SpotFitterPanel.java index eca2cc43f..38c20c166 100644 --- a/src/main/java/fiji/plugin/trackmate/action/fit/SpotFitterPanel.java +++ b/src/main/java/fiji/plugin/trackmate/action/fit/SpotFitterPanel.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -200,9 +200,9 @@ public SpotFitterPanel( final List< String > fits, final List< String > docs, fi } /** - * Get the selected channel from the slider. 1-based. - * - * @return the selected channel index. + * Returns the selected channel. 1-based. + * + * @return the selected channel. */ public int getSelectedChannel() { diff --git a/src/main/java/fiji/plugin/trackmate/action/fit/SpotGaussianFittingAction.java b/src/main/java/fiji/plugin/trackmate/action/fit/SpotGaussianFittingAction.java index 075bc917a..0d0089162 100644 --- a/src/main/java/fiji/plugin/trackmate/action/fit/SpotGaussianFittingAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/fit/SpotGaussianFittingAction.java @@ -25,21 +25,19 @@ import org.scijava.plugin.Plugin; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.action.AbstractTMAction; import fiji.plugin.trackmate.action.TrackMateAction; import fiji.plugin.trackmate.action.TrackMateActionFactory; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.Icons; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; public class SpotGaussianFittingAction extends AbstractTMAction { @Override - public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final java.awt.Frame parent ) + public void execute( final GuiModel guiModel, final java.awt.Frame parent ) { - final SpotFitterController controller = new SpotFitterController( trackmate, selectionModel, logger ); + final SpotFitterController controller = new SpotFitterController( guiModel, logger ); controller.show(); } diff --git a/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmoother.java b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmoother.java new file mode 100644 index 000000000..bba7e8908 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmoother.java @@ -0,0 +1,169 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.action.meshtools; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import fiji.plugin.trackmate.Logger; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.util.Threads; +import net.imglib2.algorithm.MultiThreaded; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.alg.TaubinSmoothing; +import net.imglib2.mesh.alg.TaubinSmoothing.TaubinWeightType; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.mesh.view.TranslateMesh; + +public class MeshSmoother implements MultiThreaded +{ + + private static final long TIME_OUT_DELAY = 2; + + private static final TimeUnit TIME_OUT_UNITS = TimeUnit.HOURS; + + private final Logger logger; + + private int numThreads; + + private final Model model; + + public MeshSmoother( final Model model, final Logger logger ) + { + this.model = model; + this.logger = logger; + setNumThreads(); + } + + public List< Spot > smooth( final MeshSmootherModel smootherModel, final Iterable< Spot > spots ) + { + final double mu = smootherModel.getMu(); + final double lambda = smootherModel.getLambda(); + final int nIters = smootherModel.getNIters(); + final TaubinWeightType weightType = smootherModel.getWeightType(); + + final int nSpots = count( spots ); + logger.setStatus( "Taubin smoothing" ); + logger.log( "Started Taubin smoothing over " + nSpots + " spots with parameters:\n" ); + logger.log( String.format( " - %s: %.2f\n", "µ", mu ) ); + logger.log( String.format( " - %s: %.2f\n", "λ", lambda ) ); + logger.log( String.format( " - %s: %d\n", "N iterations", nIters ) ); + logger.log( String.format( " - %s: %s\n", "weights", weightType ) ); + + model.beginUpdate(); + try + { + final AtomicInteger ai = new AtomicInteger( 0 ); + final ExecutorService executors = Threads.newFixedThreadPool( numThreads ); + final List< Spot > modifiedSpots = new ArrayList<>(); + for ( final Spot spot : spots ) + { + if ( SpotMesh.class.isInstance( spot ) ) + { + final SpotMesh sm = ( SpotMesh ) spot; + model.beforeEdit( sm ); + executors.execute( process( sm, nIters, mu, lambda, weightType, ai, nSpots ) ); + modifiedSpots.add( sm ); + } + } + + executors.shutdown(); + final boolean ok = executors.awaitTermination( TIME_OUT_DELAY, TIME_OUT_UNITS ); + if ( !ok ) + logger.error( "Timeout of " + TIME_OUT_DELAY + " " + TIME_OUT_UNITS + " reached while smoothing.\n" ); + + logger.log( "Done.\n" ); + return modifiedSpots; + } + catch ( final InterruptedException e ) + { + logger.error( e.getMessage() ); + e.printStackTrace(); + } + finally + { + logger.setProgress( 1 ); + logger.setStatus( "" ); + model.endUpdate(); + } + return null; + } + + private static final int count( final Iterable< Spot > spots ) + { + if ( Collection.class.isInstance( spots ) ) + return ( ( Collection< ? > ) spots ).size(); + + int n = 0; + for ( @SuppressWarnings( "unused" ) + final Spot spot : spots ) + n++; + return n; + } + + private Runnable process( + final SpotMesh sm, + final int nIters, + final double mu, + final double lambda, + final TaubinWeightType weightType, + final AtomicInteger ai, + final int nSpots ) + { + return new Runnable() + { + @Override + public void run() + { + final Mesh mesh = sm.getMesh(); + final BufferMesh smoothedMesh = TaubinSmoothing.smooth( mesh, nIters, lambda, mu, weightType ); + sm.setMesh( TranslateMesh.translate( smoothedMesh, sm ) ); + + logger.setProgress( ( double ) ai.incrementAndGet() / nSpots ); + } + }; + } + + @Override + public void setNumThreads() + { + this.numThreads = Math.max( 1, Runtime.getRuntime().availableProcessors() / 2 ); + } + + @Override + public void setNumThreads( final int numThreads ) + { + this.numThreads = numThreads; + } + + @Override + public int getNumThreads() + { + return numThreads; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherAction.java b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherAction.java new file mode 100644 index 000000000..1fe9c209b --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherAction.java @@ -0,0 +1,90 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.action.meshtools; + +import java.awt.Frame; + +import javax.swing.ImageIcon; + +import org.scijava.plugin.Plugin; + +import fiji.plugin.trackmate.action.AbstractTMAction; +import fiji.plugin.trackmate.action.TrackMateAction; +import fiji.plugin.trackmate.action.TrackMateActionFactory; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.gui.Icons; + +public class MeshSmootherAction extends AbstractTMAction +{ + + @Override + public void execute( final GuiModel guiModel, final Frame parent ) + { + final MeshSmootherController controller = new MeshSmootherController( guiModel.getModel(), guiModel.getSelectionModel(), logger ); + controller.setNumThreads( guiModel.getTrackMate().getNumThreads() ); + controller.show( parent ); + } + + @Plugin( type = TrackMateActionFactory.class ) + public static class Factory implements TrackMateActionFactory + { + + public static final String NAME = "Smooth 3D meshes"; + + public static final String KEY = "MESH_SMOOTHER"; + + public static final String INFO_TEXT = "" + + "Displays a tool to smooth the 3D mesh present in " + + "the data, using the Taubin smoothing algorithm."; + + @Override + public String getInfoText() + { + return INFO_TEXT; + } + + @Override + public String getKey() + { + return KEY; + } + + @Override + public TrackMateAction create() + { + return new MeshSmootherAction(); + } + + @Override + public ImageIcon getIcon() + { + return Icons.VECTOR_ICON; + } + + @Override + public String getName() + { + return NAME; + } + } + +} diff --git a/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherController.java b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherController.java new file mode 100644 index 000000000..4ed8a3a12 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherController.java @@ -0,0 +1,113 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.action.meshtools; + +import java.awt.Component; + +import javax.swing.JFrame; +import javax.swing.JLabel; + +import fiji.plugin.trackmate.Logger; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.gui.GuiUtils; +import fiji.plugin.trackmate.gui.Icons; +import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; +import net.imglib2.algorithm.MultiThreaded; + +public class MeshSmootherController implements MultiThreaded +{ + + private final Model model; + + private final SelectionModel selectionModel; + + private final MeshSmootherPanel gui; + + private final MeshSmoother smoother; + + public MeshSmootherController( final Model model, final SelectionModel selectionModel, final Logger logger ) + { + this.model = model; + this.selectionModel = selectionModel; + this.gui = new MeshSmootherPanel(); + this.smoother = new MeshSmoother( model, logger ); + + gui.btnRun.addActionListener( e -> run( gui.getModel() ) ); + gui.btnUndo.addActionListener( e -> model.undo() ); + } + + public void show( final Component parent ) + { + final JFrame frame = new JFrame( "Smoothing params" ); + frame.getContentPane().add( gui ); + frame.setSize( 400, 300 ); + frame.setIconImage( Icons.TRACKMATE_ICON.getImage() ); + GuiUtils.positionWindow( frame, parent ); + frame.setVisible( true ); + } + + private void run( final MeshSmootherModel smootherModel ) + { + final Iterable< Spot > spots; + if ( gui.rdbtnAll.isSelected() ) + spots = model.getSpots().iterable( true ); + else + spots = selectionModel.getSpotSelection(); + + new Thread( () -> { + final EverythingDisablerAndReenabler enabler = new EverythingDisablerAndReenabler( gui, new Class[] { JLabel.class } ); + try + { + enabler.disable(); + smoother.smooth( smootherModel, spots ); + } + catch ( final Exception err ) + { + err.printStackTrace(); + } + finally + { + enabler.reenable(); + } + }, "TrackMate mesh smoother thread" ).start(); + } + + @Override + public void setNumThreads() + { + smoother.setNumThreads(); + } + + @Override + public void setNumThreads( final int numThreads ) + { + smoother.setNumThreads( numThreads ); + } + + @Override + public int getNumThreads() + { + return smoother.getNumThreads(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherModel.java b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherModel.java new file mode 100644 index 000000000..abc4ac371 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherModel.java @@ -0,0 +1,100 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.action.meshtools; + +import net.imglib2.mesh.alg.TaubinSmoothing.TaubinWeightType; + +public class MeshSmootherModel +{ + + private int nIters = 10; + + private double mu = 0.5; + + private double lambda = -0.53; + + private TaubinWeightType weightType = TaubinWeightType.NAIVE; + + public void setWeightType( final TaubinWeightType weightType ) + { + this.weightType = weightType; + } + + public void setMu( final double mu ) + { + this.mu = Math.min( 1., Math.max( 0, mu ) ); + } + + public void setLambda( final double lambda ) + { + this.lambda = Math.min( 0., Math.max( -1., lambda ) ); + } + + public void setNIters( final int nIters ) + { + this.nIters = Math.max( 0, nIters ); + } + + public double getMu() + { + return mu; + } + + public double getLambda() + { + return lambda; + } + + public int getNIters() + { + return nIters; + } + + public TaubinWeightType getWeightType() + { + return weightType; + } + + /** + * Ad-hoc method setting parameters for little smoothing (close to 0) or a + * lot of smoothing (close to 1). + * + * @param smoothing + * the smoothing parameter. + */ + public void setSmoothing( final double smoothing ) + { + setMu( Math.max( 0, Math.min( 0.97, smoothing ) ) ); + setLambda( -mu - 0.03 ); + } + + @Override + public String toString() + { + final StringBuilder str = new StringBuilder( super.toString() + '\n' ); + str.append( String.format( " - %s: %.2f\n", "µ", mu ) ); + str.append( String.format( " - %s: %.2f\n", "λ", lambda ) ); + str.append( String.format( " - %s: %d\n", "N iterations", nIters ) ); + str.append( String.format( " - %s: %s\n", "weights", weightType ) ); + return str.toString(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherPanel.java b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherPanel.java new file mode 100644 index 000000000..c8c47d638 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherPanel.java @@ -0,0 +1,256 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.action.meshtools; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.util.Arrays; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JTabbedPane; + +import org.scijava.ui.config.visitors.gui.elements.SliderPanel; +import org.scijava.ui.config.visitors.gui.elements.SliderPanelDouble; +import org.scijava.ui.config.visitors.gui.elements.StyleElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElementVisitor; +import org.scijava.ui.config.visitors.gui.elements.StyleElements; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.BoundedDoubleElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.EnumElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.IntElement; + +import net.imglib2.mesh.alg.TaubinSmoothing.TaubinWeightType; + +public class MeshSmootherPanel extends JPanel +{ + + private static final long serialVersionUID = 1L; + + final JButton btnRun; + + final JButton btnUndo; + + final JRadioButton rdbtnSelection; + + final JRadioButton rdbtnAll; + + private final MeshSmootherModel modelBasic; + + private final MeshSmootherModel modelSimple; + + private final MeshSmootherModel modelAdvanced; + + private final JTabbedPane mainPanel; + + public MeshSmootherPanel() + { + this.modelBasic = new MeshSmootherModel(); + modelBasic.setMu( 1. ); + modelBasic.setLambda( 0. ); + modelBasic.setWeightType( TaubinWeightType.NAIVE ); + final IntElement nItersBasic = StyleElements.intElement( "N iterations", 1, 50, modelBasic::getNIters, modelBasic::setNIters ); + final List< StyleElement > superSimpleElements = Arrays.asList( nItersBasic ); + + this.modelSimple = new MeshSmootherModel(); + final BoundedDoubleElement smoothing = StyleElements.boundedDoubleElement( "Smoothing (%)", 0., 100., () -> modelSimple.getMu() * 100., v -> modelSimple.setSmoothing( v / 100. ) ); + final IntElement nItersSimple = StyleElements.intElement( "N iterations", 1, 50, modelSimple::getNIters, modelSimple::setNIters ); + final List< StyleElement > simpleElements = Arrays.asList( smoothing, nItersSimple ); + + this.modelAdvanced = new MeshSmootherModel(); + final BoundedDoubleElement mu = StyleElements.boundedDoubleElement( "µ", 0., 1., modelAdvanced::getMu, modelAdvanced::setMu ); + final BoundedDoubleElement lambda = StyleElements.boundedDoubleElement( "-λ", 0., 1., () -> -modelAdvanced.getLambda(), l -> modelAdvanced.setLambda( -l ) ); + final EnumElement< TaubinWeightType > weightType = StyleElements.enumElement( "weight type", TaubinWeightType.values(), modelAdvanced::getWeightType, modelAdvanced::setWeightType ); + final IntElement nItersAdvanced = StyleElements.intElement( "N iterations", 1, 50, modelAdvanced::getNIters, modelAdvanced::setNIters ); + final List< StyleElement > advancedElements = Arrays.asList( mu, lambda, nItersAdvanced, weightType ); + + setLayout( new BorderLayout( 0, 0 ) ); + + final JPanel bottomPanel = new JPanel(); + add( bottomPanel, BorderLayout.SOUTH ); + bottomPanel.setLayout( new BoxLayout( bottomPanel, BoxLayout.Y_AXIS ) ); + + final JPanel selectionPanel = new JPanel(); + selectionPanel.setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) ); + bottomPanel.add( selectionPanel ); + selectionPanel.setLayout( new BoxLayout( selectionPanel, BoxLayout.X_AXIS ) ); + + final JLabel lblRunOn = new JLabel( "Run on:" ); + selectionPanel.add( lblRunOn ); + + selectionPanel.add( Box.createHorizontalGlue() ); + + rdbtnSelection = new JRadioButton( "selection only" ); + selectionPanel.add( rdbtnSelection ); + + rdbtnAll = new JRadioButton( "all visible spots" ); + selectionPanel.add( rdbtnAll ); + + final JPanel buttonPanel = new JPanel(); + bottomPanel.add( buttonPanel ); + buttonPanel.setLayout( new BoxLayout( buttonPanel, BoxLayout.X_AXIS ) ); + + this.btnUndo = new JButton( "Undo" ); + buttonPanel.add( btnUndo ); + + buttonPanel.add( Box.createHorizontalGlue() ); + + this.btnRun = new JButton( "Run" ); + buttonPanel.add( btnRun ); + + this.mainPanel = new JTabbedPane( JTabbedPane.TOP ); + add( mainPanel, BorderLayout.CENTER ); + + final JPanel panelSuperSimple = new JPanel(); + panelSuperSimple.setOpaque( false ); + panelSuperSimple.setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) ); + final MyStyleElementVisitors superSimplePanelVisitor = new MyStyleElementVisitors( panelSuperSimple ); + superSimpleElements.forEach( el -> el.accept( superSimplePanelVisitor ) ); + mainPanel.addTab( "Basic", null, panelSuperSimple, null ); + + final JPanel panelSimple = new JPanel(); + panelSimple.setOpaque( false ); + panelSimple.setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) ); + final MyStyleElementVisitors simplePanelVisitor = new MyStyleElementVisitors( panelSimple ); + simpleElements.forEach( el -> el.accept( simplePanelVisitor ) ); + mainPanel.addTab( "Simple", null, panelSimple, null ); + + final JPanel panelAdvanced = new JPanel(); + panelAdvanced.setOpaque( false ); + panelAdvanced.setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) ); + final MyStyleElementVisitors advancedPanelVisitor = new MyStyleElementVisitors( panelAdvanced ); + advancedElements.forEach( el -> el.accept( advancedPanelVisitor ) ); + mainPanel.addTab( "Advanced", null, panelAdvanced, null ); + + final ButtonGroup buttonGroup = new ButtonGroup(); + buttonGroup.add( rdbtnAll ); + buttonGroup.add( rdbtnSelection ); + rdbtnSelection.setSelected( true ); + } + + public MeshSmootherModel getModel() + { + switch ( mainPanel.getSelectedIndex() ) + { + case 0: + return modelBasic; + case 1: + return modelSimple; + case 2: + return modelAdvanced; + } + throw new IllegalStateException( "Cannot handle mesh smoothing settings type number " + ( mainPanel.getSelectedIndex() ) ); + } + + private static class MyStyleElementVisitors implements StyleElementVisitor + { + + private final JPanel panel; + + private final GridBagConstraints gbcs; + + public MyStyleElementVisitors( final JPanel panel ) + { + this.panel = panel; + final GridBagLayout layout = new GridBagLayout(); + layout.columnWidths = new int[] { 0, 0, 0 }; + layout.rowHeights = new int[] { 40, 40, 40, 40 }; + layout.columnWeights = new double[] { 0., 1., Double.MIN_VALUE }; + layout.rowWeights = new double[] { 0., 0., 0., 0., 1. }; + panel.setLayout( layout ); + + this.gbcs = new GridBagConstraints(); + gbcs.fill = GridBagConstraints.HORIZONTAL; + gbcs.gridx = 0; + gbcs.gridy = 0; + } + + @Override + public < E > void visit( final EnumElement< E > el ) + { + gbcs.gridx = 0; + final JLabel lbl = new JLabel( el.getLabel() ); + lbl.setHorizontalAlignment( JLabel.RIGHT ); + lbl.setFont( getFont().deriveFont( getFont().getSize2D() - 1f ) ); + panel.add( lbl, gbcs ); + gbcs.gridx++; + panel.add( StyleElements.linkedComboBoxEnumSelector( el ), gbcs ); + gbcs.gridy++; + } + + @Override + public void visit( final BoundedDoubleElement el ) + { + gbcs.gridx = 0; + final JLabel lbl = new JLabel( el.getLabel() ); + lbl.setHorizontalAlignment( JLabel.RIGHT ); + lbl.setFont( getFont().deriveFont( getFont().getSize2D() - 1f ) ); + panel.add( lbl, gbcs ); + gbcs.gridx++; + final SliderPanelDouble sliderPanel = StyleElements.linkedSliderPanel( el, 3 ); + sliderPanel.setOpaque( false ); + panel.add( sliderPanel, gbcs ); + gbcs.gridy++; + } + + @Override + public void visit( final IntElement el ) + { + gbcs.gridx = 0; + final JLabel lbl = new JLabel( el.getLabel() ); + lbl.setHorizontalAlignment( JLabel.RIGHT ); + lbl.setFont( getFont().deriveFont( getFont().getSize2D() - 1f ) ); + panel.add( lbl, gbcs ); + gbcs.gridx++; + final SliderPanel sliderPanel = StyleElements.linkedSliderPanel( el, 3 ); + sliderPanel.setOpaque( false ); + panel.add( sliderPanel, gbcs ); + gbcs.gridy++; + } + + private Font getFont() + { + return panel.getFont(); + } + } + + public static void main( final String[] args ) + { + final MeshSmootherPanel panel = new MeshSmootherPanel(); + + final JFrame frame = new JFrame( "Smoothing params" ); + frame.getContentPane().add( panel ); + frame.setSize( 400, 300 ); + frame.setLocationRelativeTo( null ); + frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); + frame.setVisible( true ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java b/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java index c9c8e6404..f10edac04 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java +++ b/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java @@ -2,18 +2,18 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * 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 * . @@ -38,6 +38,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.detection.util.MedianFilter2D; @@ -294,11 +295,11 @@ public static final Img< FloatType > createLoGKernel( final double radius, final * the interval in the source image to copy. * @param factory * a factory used to build the float image. - * @param - * the pixel type of the source image. * @return a new float Img. Careful: even if the specified interval does not * start at (0, 0), the new image will have its first pixel at * coordinates (0, 0). + * @param + * the pixel type of the input image. */ public static final < T extends RealType< T > > Img< FloatType > copyToFloatImg( final RandomAccessible< T > img, final Interval interval, final ImgFactory< FloatType > factory ) { @@ -353,17 +354,18 @@ public static final Interval squeeze( final Interval interval ) /** * Applies a simple 3x3 median filter to the target image. * - * @param image - * the input image. * @param - * the pixel type of the input and output images. - * @return a new image, or null if the filtering failed. + * the pixel type in the image. + * @param image + * the image to filter. + * @return the filtered image, as a new image, or null if there + * was a problem during processing. */ public static final < R extends RealType< R > & NativeType< R > > Img< R > applyMedianFilter( final RandomAccessibleInterval< R > image ) { final MedianFilter2D< R > medFilt = new MedianFilter2D<>( image, 1 ); if ( !medFilt.checkInput() || !medFilt.process() ) - return null; + { return null; } return medFilt.getResult(); } @@ -443,7 +445,7 @@ public static final < T extends RealType< T > > List< Spot > findLocalMaxima( final double x = refinedPeak.getDoublePosition( 0 ) * calibration[ 0 ]; final double y = refinedPeak.getDoublePosition( 1 ) * calibration[ 1 ]; final double z = refinedPeak.getDoublePosition( 2 ) * calibration[ 2 ]; - final Spot spot = new Spot( x, y, z, radius, quality ); + final Spot spot = new SpotBase( x, y, z, radius, quality ); spots.add( spot ); } } @@ -456,7 +458,7 @@ else if ( source.numDimensions() > 1 ) final double quality = ra.get().getRealDouble(); final double x = refinedPeak.getDoublePosition( 0 ) * calibration[ 0 ]; final double y = refinedPeak.getDoublePosition( 1 ) * calibration[ 1 ]; - final Spot spot = new Spot( x, y, z, radius, quality ); + final Spot spot = new SpotBase( x, y, z, radius, quality ); spots.add( spot ); } } @@ -469,7 +471,7 @@ else if ( source.numDimensions() > 1 ) ra.setPosition( refinedPeak.getOriginalPeak() ); final double quality = ra.get().getRealDouble(); final double x = refinedPeak.getDoublePosition( 0 ) * calibration[ 0 ]; - final Spot spot = new Spot( x, y, z, radius, quality ); + final Spot spot = new SpotBase( x, y, z, radius, quality ); spots.add( spot ); } @@ -488,7 +490,7 @@ else if ( source.numDimensions() > 1 ) final double x = peak.getDoublePosition( 0 ) * calibration[ 0 ]; final double y = peak.getDoublePosition( 1 ) * calibration[ 1 ]; final double z = peak.getDoublePosition( 2 ) * calibration[ 2 ]; - final Spot spot = new Spot( x, y, z, radius, quality ); + final Spot spot = new SpotBase( x, y, z, radius, quality ); spots.add( spot ); } } @@ -501,7 +503,7 @@ else if ( source.numDimensions() > 1 ) final double quality = ra.get().getRealDouble(); final double x = peak.getDoublePosition( 0 ) * calibration[ 0 ]; final double y = peak.getDoublePosition( 1 ) * calibration[ 1 ]; - final Spot spot = new Spot( x, y, z, radius, quality ); + final Spot spot = new SpotBase( x, y, z, radius, quality ); spots.add( spot ); } } @@ -514,10 +516,9 @@ else if ( source.numDimensions() > 1 ) ra.setPosition( peak ); final double quality = ra.get().getRealDouble(); final double x = peak.getDoublePosition( 0 ) * calibration[ 0 ]; - final Spot spot = new Spot( x, y, z, radius, quality ); + final Spot spot = new SpotBase( x, y, z, radius, quality ); spots.add( spot ); } - } } @@ -593,9 +594,10 @@ public static final < T extends RealType< T > > void normalize( final Iterable< * * @param img * the image to wrap. - * @param - * the type of pixel in the input image. * @return a new ImagePlus. + * @param + * the type of pixels in the image. Must extend {@link RealType} + * and {@link NativeType}. */ public static < T extends RealType< T > & NativeType< T > > ImagePlus wrap( final ImgPlus< T > img ) { diff --git a/src/main/java/fiji/plugin/trackmate/detection/DogDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/detection/DogDetectorFactory.java index 3c0d9726e..d55073727 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/DogDetectorFactory.java +++ b/src/main/java/fiji/plugin/trackmate/detection/DogDetectorFactory.java @@ -32,6 +32,7 @@ import org.scijava.plugin.Plugin; import fiji.plugin.trackmate.util.TMUtils; +import ij.ImagePlus; import net.imagej.ImgPlus; import net.imglib2.Interval; import net.imglib2.RandomAccessible; @@ -77,6 +78,26 @@ public SpotDetector< T > getDetector( final ImgPlus< T > img, final Map< String, return detector; } + @Override + public DogDetectorConfig createConfig( final ImagePlus imp ) + { + final int nChannels = ( imp == null ) ? 1 : imp.getNChannels(); + final String units = ( imp == null ) ? "no image" : imp.getCalibration().getUnit(); + return new DogDetectorConfig( nChannels, units ); + } + + /** + * Specifies what are the parameters of the DoG detector. + */ + public static class DogDetectorConfig extends LogDetectorConfig + { + + public DogDetectorConfig( final int nChannels, final String units ) + { + super( THIS_NAME, THIS_INFO_TEXT, nChannels, units ); + } + } + @Override public String getKey() { diff --git a/src/main/java/fiji/plugin/trackmate/detection/HessianDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/detection/HessianDetectorFactory.java index f399922ad..1baef2dab 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/HessianDetectorFactory.java +++ b/src/main/java/fiji/plugin/trackmate/detection/HessianDetectorFactory.java @@ -33,6 +33,7 @@ import java.util.Map; import org.scijava.plugin.Plugin; +import org.scijava.ui.config.Parameters.DoubleParam; import fiji.plugin.trackmate.util.TMUtils; import ij.ImagePlus; @@ -114,7 +115,7 @@ public String getName() } @Override - public HessianDetectorCLI getConfigurator( final ImagePlus imp ) + public HessianDetectorCLI createConfig( final ImagePlus imp ) { final int nChannels = ( imp == null ) ? 1 : imp.getNChannels(); final String units = ( imp == null ) ? "no image" : imp.getCalibration().getUnit(); @@ -122,18 +123,18 @@ public HessianDetectorCLI getConfigurator( final ImagePlus imp ) } /** - * Specifies what are the parameters of the {@link LogDetector}. + * Specifies what are the parameters of the Hessian detector. * * @author Jean-Yves Tinevez */ - public static class HessianDetectorCLI extends LogDetectorCLI + public static class HessianDetectorCLI extends LogDetectorConfig { public HessianDetectorCLI( final int nChannels, final String units ) { - super( nChannels, units ); + super( NAME, INFO_TEXT, nChannels, units ); // Diameter in Z - final DoubleArgument diameterZ = addDoubleArgument() + final DoubleParam diameterZ = addDoubleParameter() .key( KEY_RADIUS_Z ) .name( "Diameter along Z" ) .units( units ) @@ -143,10 +144,9 @@ public HessianDetectorCLI( final int nChannels, final String units ) // Convert to diameter for display purposes. setDisplayTranslator( diameterZ, r -> r * 2., d -> d / 2. ); // Change order - arguments.remove( diameterZ ); - arguments.add( 2, diameterZ ); + reorder( diameterZ, 2 ); // Normalize quality values - addFlag() + addBooleanParameter() .key( KEY_NORMALIZE ) .name( "Normalize quality values" ) .defaultValue( DEFAULT_NORMALIZE ) diff --git a/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetector.java b/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetector.java index fc1e522ea..c5e76f893 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetector.java +++ b/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetector.java @@ -73,6 +73,8 @@ public class LabelImageDetector< T extends RealType< T > & NativeType< T > > imp */ protected final boolean simplify; + private final double smoothingScale; + /* * CONSTRUCTORS */ @@ -81,12 +83,14 @@ public LabelImageDetector( final RandomAccessible< T > input, final Interval interval, final double[] calibration, - final boolean simplify ) + final boolean simplify, + final double smoothingScale ) { this.input = input; this.interval = DetectionUtils.squeeze( interval ); this.calibration = calibration; this.simplify = simplify; + this.smoothingScale = smoothingScale; } @Override @@ -135,9 +139,31 @@ private < R extends IntegerType< R > > void processIntegerImg( final RandomAcces final ImgLabeling< Integer, R > labeling = ImgLabeling.fromImageAndLabels( rai, indices ); if ( input.numDimensions() == 2 ) - spots = MaskUtils.fromLabelingWithROI( labeling, interval, calibration, simplify, null ); + { + spots = SpotRoiUtils.from2DLabelingWithROI( + labeling, + interval.minAsDoubleArray(), + calibration, + simplify, + smoothingScale, + null ); + } + else if ( input.numDimensions() == 3 ) + { + spots = SpotMeshUtils.from3DLabelingWithROI( + labeling, + interval.minAsDoubleArray(), + calibration, + simplify, + smoothingScale, + null ); + } else - spots = MaskUtils.fromLabeling( labeling, interval, calibration ); + { + throw new IllegalArgumentException( BASE_ERROR_MESSAGE + "Can only process 2D or 3D images. Got a " + + input.numDimensions() + "D image over: " + + Util.printInterval( interval ) ); + } } @Override diff --git a/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetectorFactory.java index 0e498eb06..6a5a297f7 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetectorFactory.java +++ b/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetectorFactory.java @@ -24,6 +24,7 @@ import static fiji.plugin.trackmate.detection.DetectorKeys.DEFAULT_TARGET_CHANNEL; import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS; +import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_SMOOTHING_SCALE; import java.util.HashMap; import java.util.Map; @@ -66,8 +67,8 @@ public class LabelImageDetectorFactory< T extends RealType< T > & NativeType< T + "that is unique to the object." + "

" + "This detector reads such an image and create spots from each object. " - + "In 2D the contour of a label is imported. In 3D, spherical spots " - + "of the same volume that the label are created." + + "In 2D the contour of a label is imported. In 3D, a mesh around the " + + "label is imported." + "

" + "The spot quality stores the object area or volume in pixels." + ""; @@ -82,6 +83,7 @@ public class LabelImageDetectorFactory< T extends RealType< T > & NativeType< T public SpotDetector< T > getDetector( final ImgPlus< T > img, final Map< String, Object > settings, final Interval interval, final int frame ) { final boolean simplifyContours = ( Boolean ) settings.get( KEY_SIMPLIFY_CONTOURS ); + final double smoothingScale = ( Double ) settings.get( KEY_SMOOTHING_SCALE ); final double[] calibration = TMUtils.getSpatialCalibration( img ); final int channel = ( Integer ) settings.get( KEY_TARGET_CHANNEL ) - 1; final RandomAccessible< T > imFrame = DetectionUtils.prepareFrameImg( img, channel, frame ); @@ -90,7 +92,8 @@ public SpotDetector< T > getDetector( final ImgPlus< T > img, final Map< String, imFrame, interval, calibration, - simplifyContours ); + simplifyContours, + smoothingScale ); return detector; } @@ -115,6 +118,12 @@ public boolean has2Dsegmentation() return true; } + @Override + public boolean has3Dsegmentation() + { + return true; + } + @Override public String getKey() { @@ -144,5 +153,4 @@ public ImageIcon getIcon() { return ThresholdDetectorFactory.ICON; } - } diff --git a/src/main/java/fiji/plugin/trackmate/detection/LogDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/detection/LogDetectorFactory.java index 27bf5498f..dd1ded507 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/LogDetectorFactory.java +++ b/src/main/java/fiji/plugin/trackmate/detection/LogDetectorFactory.java @@ -26,11 +26,6 @@ import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_RADIUS; import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_THRESHOLD; -import static fiji.plugin.trackmate.util.cli.CommonTrackMateArguments.addDiameter; -import static fiji.plugin.trackmate.util.cli.CommonTrackMateArguments.addMedianFiltering; -import static fiji.plugin.trackmate.util.cli.CommonTrackMateArguments.addSubpixelLocalization; -import static fiji.plugin.trackmate.util.cli.CommonTrackMateArguments.addTargetChannel; -import static fiji.plugin.trackmate.util.cli.CommonTrackMateArguments.addThreshold; import java.util.Map; @@ -38,11 +33,11 @@ import org.scijava.plugin.Plugin; -import fiji.plugin.trackmate.detection.LogDetectorFactory.LogDetectorCLI; +import fiji.plugin.trackmate.detection.LogDetectorFactory.LogDetectorConfig; import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.util.TMUtils; -import fiji.plugin.trackmate.util.cli.Configurator; -import fiji.plugin.trackmate.util.cli.HasInteractivePreview; +import fiji.plugin.trackmate.util.config.HasInteractivePreview; +import fiji.plugin.trackmate.util.config.TrackMateConfigurator; import ij.ImagePlus; import net.imagej.ImgPlus; import net.imglib2.Interval; @@ -51,7 +46,7 @@ import net.imglib2.type.numeric.RealType; @Plugin( type = SpotDetectorFactory.class ) -public class LogDetectorFactory< T extends RealType< T > & NativeType< T > > implements SpotDetectorFactory< T >, SpotDetectorFactoryGenericConfig< T, LogDetectorCLI > +public class LogDetectorFactory< T extends RealType< T > & NativeType< T > > implements SpotDetectorFactory< T >, SpotDetectorConfigFactory< T, LogDetectorConfig > { /** A string key identifying this factory. */ @@ -72,6 +67,7 @@ public class LogDetectorFactory< T extends RealType< T > & NativeType< T > > imp public static final ImageIcon ICON = new ImageIcon( Icons.class.getResource( "images/LoG-icon-64px.png" ) ); + @Override public SpotDetector< T > getDetector( final ImgPlus< T > img, final Map< String, Object > settings, final Interval interval, final int frame ) { @@ -113,29 +109,31 @@ public ImageIcon getIcon() } @Override - public LogDetectorCLI getConfigurator( final ImagePlus imp ) + public LogDetectorConfig createConfig( final ImagePlus imp ) { final int nChannels = ( imp == null ) ? 1 : imp.getNChannels( ); final String units = ( imp == null ) ? "no image" : imp.getCalibration().getUnit(); - return new LogDetectorCLI( nChannels, units ); + return new LogDetectorConfig( NAME, INFO_TEXT, nChannels, units ); } /** - * Specifies what are the parameters of the {@link LogDetector} and - * DogDetector. + * Specifies what are the parameters of the LogDetector. * * @author Jean-Yves Tinevez */ - public static class LogDetectorCLI extends Configurator implements HasInteractivePreview + public static class LogDetectorConfig extends TrackMateConfigurator implements HasInteractivePreview { - public LogDetectorCLI( final int nChannels, final String units ) + public LogDetectorConfig( final String name, final String infoText, final int nChannels, final String units ) { - addTargetChannel( this, nChannels ); - addDiameter( this, units ); - addThreshold( this ); - addMedianFiltering( this ); - addSubpixelLocalization( this ); + super( name, infoText ); + addTargetChannel( nChannels ); + addDiameter( units ); + addThreshold(); + addMedianFiltering(); + addSubpixelLocalization(); + + addIcon( new ImageIcon( Icons.class.getResource( "images/LoG-icon-64px.png" ) ).getImage() ); } @Override diff --git a/src/main/java/fiji/plugin/trackmate/detection/MaskDetector.java b/src/main/java/fiji/plugin/trackmate/detection/MaskDetector.java new file mode 100644 index 000000000..43141e3ef --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/detection/MaskDetector.java @@ -0,0 +1,66 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.detection; + +import net.imglib2.Interval; +import net.imglib2.RandomAccessible; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; + +public class MaskDetector< T extends RealType< T > & NativeType< T > > extends ThresholdDetector< T > +{ + + private final static String BASE_ERROR_MESSAGE = "MaskDetector: "; + + /* + * CONSTRUCTORS + */ + + public MaskDetector( + final RandomAccessible< T > input, + final Interval interval, + final double[] calibration, + final boolean simplify, + final double smoothingScale ) + { + super( input, interval, calibration, Double.NaN, simplify, smoothingScale ); + baseErrorMessage = BASE_ERROR_MESSAGE; + } + + + @Override + public boolean process() + { + final long start = System.currentTimeMillis(); + spots = MaskUtils.fromMaskWithROI( + input, + interval, + calibration, + simplify, + smoothingScale, + numThreads, + null ); + final long end = System.currentTimeMillis(); + this.processingTime = end - start; + return true; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/detection/MaskDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/detection/MaskDetectorFactory.java index edd685cb3..101432c0c 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/MaskDetectorFactory.java +++ b/src/main/java/fiji/plugin/trackmate/detection/MaskDetectorFactory.java @@ -37,6 +37,8 @@ import net.imagej.ImgPlus; import net.imglib2.Interval; import net.imglib2.RandomAccessible; +import net.imglib2.converter.Converter; +import net.imglib2.converter.Converters; import net.imglib2.type.NativeType; import net.imglib2.type.numeric.RealType; @@ -62,52 +64,69 @@ public class MaskDetectorFactory< T extends RealType< T > & NativeType< T > > ex + "a value strictly larger than 0 are " + "considered as part of the foreground, " + "and used to build connected regions. In 2D, spots are created with " - + "the (possibly simplified) contour of the region. In 3D, a spherical " - + "spot is created for each region in its center, with a volume equal to the " - + "region volume." + + "the (possibly simplified) contour of the region. In 3D, a mesh is " + + "created for each region." + "

" + "The spot quality stores the object area or volume in pixels." + ""; public static final String URL_DOC = "https://imagej.net/plugins/trackmate/detectors/trackmate-mask-detector"; - @Override - public boolean has2Dsegmentation() - { - return true; - } - @Override public SpotDetector< T > getDetector( final ImgPlus< T > img, final Map< String, Object > settings, final Interval interval, final int frame ) { - final double intensityThreshold = 0.; final boolean simplifyContours = ( Boolean ) settings.get( KEY_SIMPLIFY_CONTOURS ); + final double smoothingScale = ( Double ) settings.get( KEY_SMOOTHING_SCALE ); final double[] calibration = TMUtils.getSpatialCalibration( img ); final int channel = ( Integer ) settings.get( KEY_TARGET_CHANNEL ) - 1; final RandomAccessible< T > imFrame = DetectionUtils.prepareFrameImg( img, channel, frame ); + final RandomAccessible< T > mask = mask( imFrame ); - final ThresholdDetector< T > detector = new ThresholdDetector<>( - imFrame, + final MaskDetector< T > detector = new MaskDetector<>( + mask, interval, calibration, - intensityThreshold, - simplifyContours ); + simplifyContours, + smoothingScale ); + detector.setNumThreads( 1 ); return detector; } - @Override - public String getKey() + /** + * Return a view of the input image where all pixels with values strictly + * larger than 0 are set to 1, and set to 0 otherwise. + * + * @param input + * the image to wrap. + * @return a view of the image. + */ + protected RandomAccessible< T > mask( final RandomAccessible< T > input ) { - return DETECTOR_KEY; + final Converter< T, T > c = new Converter< T, T >() + { + @Override + public void convert( final T input, final T output ) + { + output.setReal( input.getRealDouble() > 0. ? 1. : 0. ); + } + }; + return Converters.convert( input, c, input.getType() ); } + @Override public ConfigurationPanel getDetectorConfigurationPanel( final Settings lSettings, final Model model ) { return new MaskDetectorConfigurationPanel( lSettings, model ); } + @Override + public String getKey() + { + return DETECTOR_KEY; + } + @Override public String getInfoText() { @@ -132,6 +151,7 @@ public Map< String, Object > getDefaultSettings() final Map< String, Object > lSettings = new HashMap<>(); lSettings.put( KEY_TARGET_CHANNEL, DEFAULT_TARGET_CHANNEL ); lSettings.put( KEY_SIMPLIFY_CONTOURS, true ); + lSettings.put( KEY_SMOOTHING_SCALE, -1. ); return lSettings; } } diff --git a/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java b/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java index 1943be802..1a467b580 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java +++ b/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -21,29 +21,20 @@ */ package fiji.plugin.trackmate.detection; -import java.awt.Polygon; import java.util.ArrayList; -import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.concurrent.ExecutorService; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.SpotRoi; -import fiji.plugin.trackmate.util.SpotUtil; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.util.Threads; -import ij.gui.PolygonRoi; -import ij.process.FloatPolygon; -import net.imagej.ImgPlus; -import net.imagej.axis.Axes; -import net.imagej.axis.AxisType; import net.imglib2.Cursor; import net.imglib2.Interval; -import net.imglib2.IterableInterval; import net.imglib2.RandomAccess; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; +import net.imglib2.algorithm.gauss3.Gauss3; import net.imglib2.algorithm.labeling.ConnectedComponents; import net.imglib2.algorithm.labeling.ConnectedComponents.StructuringElement; import net.imglib2.converter.Converter; @@ -52,15 +43,16 @@ import net.imglib2.histogram.Real1dBinMapper; import net.imglib2.img.Img; import net.imglib2.img.ImgFactory; +import net.imglib2.parallel.Parallelization; import net.imglib2.roi.labeling.ImgLabeling; import net.imglib2.roi.labeling.LabelRegion; import net.imglib2.roi.labeling.LabelRegions; -import net.imglib2.type.BooleanType; +import net.imglib2.type.NativeType; import net.imglib2.type.logic.BitType; import net.imglib2.type.logic.BoolType; -import net.imglib2.type.numeric.IntegerType; import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.IntType; +import net.imglib2.type.numeric.real.FloatType; import net.imglib2.util.Util; import net.imglib2.view.IntervalView; import net.imglib2.view.Views; @@ -68,16 +60,6 @@ public class MaskUtils { - /** - * Smoothing interval for ROIs. - */ - private static final double SMOOTH_INTERVAL = 2.; - - /** - * Douglas-Peucker polygon simplification max distance. - */ - private static final double DOUGLAS_PEUCKER_MAX_DISTANCE = 0.5; - public static final < T extends RealType< T > > double otsuThreshold( final RandomAccessibleInterval< T > img ) { // Min & max @@ -116,11 +98,11 @@ public static final long getThreshold( final Histogram1d< ? > hist ) 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 + // points long Sk; // The total intensity for all histogram points <=k long S; double BCV, BCVmax; // The current Between Class Variance and maximum - // BCV + // BCV double num, denom; // temporary bookkeeping // Initialize values: @@ -149,15 +131,15 @@ public static final long getThreshold( final Histogram1d< ? > hist ) // 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 + // (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 + // 255*N = + // approx 8E7 BCV = ( num * num ) / denom; } else @@ -182,8 +164,6 @@ public static final long getThreshold( final Histogram1d< ? > hist ) * the type of the input image. Must be real, scalar. * @param input * the input image. - * @param interval - * the interval in the input image to analyze. * @param threshold * the threshold to apply to the input image. * @param numThreads @@ -191,20 +171,17 @@ public static final long getThreshold( final Histogram1d< ? > hist ) * @return a new label image. */ public static final < T extends RealType< T > > ImgLabeling< Integer, IntType > toLabeling( - final RandomAccessible< T > input, - final Interval interval, + final RandomAccessibleInterval< T > input, final double threshold, final int numThreads ) { - // Crop. - final IntervalView< T > crop = Views.interval( input, interval ); - final IntervalView< T > in = Views.zeroMin( crop ); + // To mask. final Converter< T, BitType > converter = ( a, b ) -> b.set( a.getRealDouble() > threshold ); - final RandomAccessible< BitType > bitMask = Converters.convertRAI( in, converter, new BitType() ); + final RandomAccessible< BitType > bitMask = Converters.convertRAI( input, converter, new BitType() ); // Prepare output. - final ImgFactory< IntType > factory = Util.getArrayOrCellImgFactory( in, new IntType() ); - final Img< IntType > out = factory.create( in ); + final ImgFactory< IntType > factory = Util.getArrayOrCellImgFactory( input, new IntType() ); + final Img< IntType > out = factory.create( input ); final ImgLabeling< Integer, IntType > labeling = new ImgLabeling<>( out ); // Structuring element. @@ -226,108 +203,15 @@ public static final < T extends RealType< T > > ImgLabeling< Integer, IntType > } /** - * Creates spots from a grayscale image, thresholded to create a mask. A - * spot is created for each connected-component of the mask, with a size - * that matches the mask size. - * - * @param - * the type of the input image. Must be real, scalar. - * @param input - * the input image. - * @param interval - * the interval in the input image to analyze. - * @param calibration - * the physical calibration. - * @param threshold - * the threshold to apply to the input image. - * @param numThreads - * how many threads to use for multithreaded computation. - * @return a list of spots, without ROI. - */ - public static < T extends RealType< T > > List< Spot > fromThreshold( - final RandomAccessible< T > input, - final Interval interval, - final double[] calibration, - final double threshold, - final int numThreads ) - { - // Get labeling from mask. - final ImgLabeling< Integer, IntType > labeling = toLabeling( input, interval, threshold, numThreads ); - return fromLabeling( - labeling, - interval, - calibration ); - } - - /** - * Creates spots from a label image. - * - * @param - * the type that backs-up the labeling. - * @param labeling - * the labeling, must be zero-min. - * @param interval - * the interval, used to reposition the spots from the zero-min - * labeling to the proper coordinates. - * @param calibration - * the physical calibration. - * @return a list of spots, without ROI. - */ - public static < R extends IntegerType< R > > List< Spot > fromLabeling( - final ImgLabeling< Integer, R > labeling, - final Interval interval, - final double[] calibration ) - { - // Parse each component. - final LabelRegions< Integer > regions = new LabelRegions<>( labeling ); - final Iterator< LabelRegion< Integer > > iterator = regions.iterator(); - final List< Spot > spots = new ArrayList<>( regions.getExistingLabels().size() ); - while ( iterator.hasNext() ) - { - final LabelRegion< Integer > region = iterator.next(); - final Cursor< BoolType > cursor = region.localizingCursor(); - final int[] cursorPos = new int[ labeling.numDimensions() ]; - final long[] sum = new long[ 3 ]; - while ( cursor.hasNext() ) - { - cursor.fwd(); - cursor.localize( cursorPos ); - for ( int d = 0; d < sum.length; d++ ) - sum[ d ] += cursorPos[ d ]; - } - - final double[] pos = new double[ 3 ]; - for ( int d = 0; d < pos.length; d++ ) - pos[ d ] = sum[ d ] / ( double ) region.size(); - - final double x = calibration[ 0 ] * ( interval.min( 0 ) + pos[ 0 ] ); - final double y = calibration[ 1 ] * ( interval.min( 1 ) + pos[ 1 ] ); - final double z = calibration[ 2 ] * ( interval.min( 2 ) + pos[ 2 ] ); - - double volume = region.size(); - for ( int d = 0; d < calibration.length; d++ ) - if ( calibration[ d ] > 0 ) - volume *= calibration[ d ]; - final double radius = ( labeling.numDimensions() == 2 ) - ? Math.sqrt( volume / Math.PI ) - : Math.pow( 3. * volume / ( 4. * Math.PI ), 1. / 3. ); - final double quality = region.size(); - spots.add( new Spot( x, y, z, radius, quality ) ); - } - - return spots; - } - - /** - * Creates spots from a grayscale image, thresholded to create a mask. A - * spot is created for each connected-component of the mask, with a size + * Creates spots by thresholding a grayscale image. A spot is created for + * each connected-component object in the thresholded input, with a size * that matches the mask size. The quality of the spots is read from another * image, by taking the max pixel value of this image with the ROI. * * @param - * the type of the input image. Must be real, scalar. + * the pixel type of the input image. Must be real, scalar. * @param - * the type of the quality image. Must be real, scalar. + * the pixel type of the quality image. Must be real, scalar. * @param input * the input image. * @param interval @@ -350,8 +234,15 @@ public static < T extends RealType< T >, R extends RealType< R > > List< Spot > final int numThreads, final RandomAccessibleInterval< R > qualityImage ) { + // Crop. + final IntervalView< T > crop = Views.interval( input, interval ); + final IntervalView< T > in = Views.zeroMin( crop ); + // Get labeling from mask. - final ImgLabeling< Integer, IntType > labeling = toLabeling( input, interval, threshold, numThreads ); + final ImgLabeling< Integer, IntType > labeling = toLabeling( + in, + threshold, + numThreads ); // Crop of the quality image. final IntervalView< R > cropQuality = Views.interval( qualityImage, interval ); @@ -401,343 +292,170 @@ public static < T extends RealType< T >, R extends RealType< R > > List< Spot > final double radius = ( labeling.numDimensions() == 2 ) ? Math.sqrt( volume / Math.PI ) : Math.pow( 3. * volume / ( 4. * Math.PI ), 1. / 3. ); - spots.add( new Spot( x, y, z, radius, quality ) ); + spots.add( new SpotBase( x, y, z, radius, quality ) ); } return spots; } /** - * Creates spots with their ROIs from a 2D grayscale image, - * thresholded to create a mask. A spot is created for each - * connected-component of the mask, with a size that matches the mask size. - * The quality of the spots is read from another image, by taking the max - * pixel value of this image with the ROI. + * Creates spots with their ROIs or meshes from a 2D or 3D + * mask. A spot is created for each connected-component of the mask, with a + * size that matches the mask size. The quality of the spots is read from + * another image, by taking the max pixel value of this image with the ROI. * * @param * the type of the input image. Must be real, scalar. * @param * the type of the quality image. Must be real, scalar. * @param input - * the input image. Must be 2D. + * the input mask image. Can be 2D or 3D. It does not have to be + * of boolean type: every pixel with a real value strictly larger + * than 0.5 will be considered true and + * false otherwise. * @param interval * the interval in the input image to analyze. * @param calibration * the physical calibration. - * @param threshold - * the threshold to apply to the input image. * @param simplify * if true the polygon will be post-processed to be * smoother and contain less points. * @param numThreads * how many threads to use for multithreaded computation. + * @param smoothingScale + * if strictly larger than 0, the mask will be smoothed before + * creating the mesh, resulting in smoother meshes. The scale + * value sets the (Gaussian) filter radius and is specified in + * physical units. If 0 or lower than 0, no smoothing is applied. * @param qualityImage * the image in which to read the quality value. * @return a list of spots, with ROI. */ - public static final < T extends RealType< T >, S extends RealType< S > > List< Spot > fromThresholdWithROI( - final RandomAccessible< T > input, - final Interval interval, - final double[] calibration, - final double threshold, - final boolean simplify, - final int numThreads, + public static < T extends RealType< T > & NativeType< T >, S extends RealType< S > > List< Spot > fromMaskWithROI( + final RandomAccessible< T > input, + final Interval interval, + final double[] calibration, + final boolean simplify, + final double smoothingScale, + final int numThreads, final RandomAccessibleInterval< S > qualityImage ) { - if ( input.numDimensions() != 2 ) - throw new IllegalArgumentException( "Can only process 2D images with this method, but got " + input.numDimensions() + "D." ); - - // Get labeling. - final ImgLabeling< Integer, IntType > labeling = toLabeling( input, interval, threshold, numThreads ); - return fromLabelingWithROI( labeling, interval, calibration, simplify, qualityImage ); + final double threshold = 0.5; + return fromThresholdWithROI( + input, + interval, + calibration, + threshold, + simplify, + smoothingScale, + numThreads, + qualityImage ); } /** - * Creates spots with ROIs from a 2D label image. The quality - * value is read from a secondary image, by taking the max value in each - * ROI. + * Creates spots with their ROIs or meshes from a 2D or 3D by + * thresholding a grayscale image. A spot is created for each object in the + * thresholded image. The quality of the spots is read from another image, + * by taking the max pixel value of this image with the ROI. * - * @param - * the type that backs-up the labeling. + * @param + * the type of the input image. Must be real, scalar. * @param * the type of the quality image. Must be real, scalar. - * @param labeling - * the labeling, must be zero-min and 2D.. + * @param input + * the input image. Can be 2D or 3D. * @param interval - * the interval, used to reposition the spots from the zero-min - * labeling to the proper coordinates. + * the interval in the input image to analyze. * @param calibration * the physical calibration. + * @param threshold + * the threshold to apply to the input image. * @param simplify * if true the polygon will be post-processed to be * smoother and contain less points. + * @param smoothingScale + * if strictly larger than 0, the input will be smoothed before + * creating the contour, resulting in smoother contours. The + * scale value sets the (Gaussian) filter radius and is specified + * in physical units. If 0 or lower than 0, no smoothing is + * applied. + * @param numThreads + * how many threads to use for multithreaded computation. * @param qualityImage * the image in which to read the quality value. * @return a list of spots, with ROI. */ - public static < R extends IntegerType< R >, S extends RealType< S > > List< Spot > fromLabelingWithROI( - final ImgLabeling< Integer, R > labeling, - final Interval interval, - final double[] calibration, - final boolean simplify, - final RandomAccessibleInterval< S > qualityImage ) - { - final Map< Integer, List< Spot > > map = fromLabelingWithROIMap( labeling, interval, calibration, simplify, qualityImage ); - final List spots = new ArrayList<>(); - for ( final List< Spot > s : map.values() ) - spots.addAll( s ); - - return spots; - } - - /** - * Creates spots with ROIs from a 2D label image. The quality - * value is read from a secondary image, by taking the max value in each - * ROI. - *

- * The spots are returned in a map, where the key is the integer value of - * the label they correspond to in the label image. Because one spot - * corresponds to one connected component in the label image, there might be - * several spots for a label, hence the values of the map are list of spots. - * - * @param - * the type that backs-up the labeling. - * @param - * the type of the quality image. Must be real, scalar. - * @param labeling - * the labeling, must be zero-min and 2D.. - * @param interval - * the interval, used to reposition the spots from the zero-min - * labeling to the proper coordinates. - * @param calibration - * the physical calibration. - * @param simplify - * if true the polygon will be post-processed to be - * smoother and contain less points. - * @param qualityImage - * the image in which to read the quality value. - * @return a map linking the label integer value to the list of spots, with - * ROI, it corresponds to. - */ - public static < R extends IntegerType< R >, S extends RealType< S > > Map< Integer, List< Spot > > fromLabelingWithROIMap( - final ImgLabeling< Integer, R > labeling, + @SuppressWarnings( { "unchecked", "rawtypes" } ) + public static final < T extends RealType< T > & NativeType< T >, S extends RealType< S > > List< Spot > fromThresholdWithROI( + final RandomAccessible< T > input, final Interval interval, final double[] calibration, + final double threshold, final boolean simplify, + final double smoothingScale, + final int numThreads, final RandomAccessibleInterval< S > qualityImage ) { - if ( labeling.numDimensions() != 2 ) - throw new IllegalArgumentException( "Can only process 2D images with this method, but got " + labeling.numDimensions() + "D." ); - - final LabelRegions< Integer > regions = new LabelRegions< Integer >( labeling ); + /* + * Crop. + */ + final IntervalView< T > crop = Views.interval( input, interval ); + final IntervalView< T > in = Views.zeroMin( crop ); /* - * Map of label in the label image to a collection of polygons around - * this label. Because 1 polygon correspond to 1 connected component, - * there might be several polygons for a label. + * Possibly filter. */ - final Map< Integer, List< Polygon > > polygonsMap = new HashMap<>( regions.getExistingLabels().size() ); - final Iterator< LabelRegion< Integer > > iterator = regions.iterator(); - // Parse regions to create polygons on boundaries. - while ( iterator.hasNext() ) + final RandomAccessibleInterval< T > filtered; + if ( smoothingScale > 0. ) { - final LabelRegion< Integer > region = iterator.next(); - // Analyze in zero-min region. - final List< Polygon > pp = maskToPolygons( Views.zeroMin( region ) ); - // Translate back to interval coords. - for ( final Polygon polygon : pp ) - polygon.translate( ( int ) region.min( 0 ), ( int ) region.min( 1 ) ); + final double[] sigmas = new double[ in.numDimensions() ]; + for ( int d = 0; d < sigmas.length; d++ ) + sigmas[ d ] = smoothingScale / Math.sqrt( in.numDimensions() ) / calibration[ d ]; - final Integer label = region.getLabel(); - polygonsMap.put( label, pp ); + filtered = ( RandomAccessibleInterval ) Util.getArrayOrCellImgFactory( in, new FloatType() ).create( in ); + Parallelization.runWithNumThreads( numThreads, + () -> Gauss3.gauss( sigmas, Views.extendMirrorDouble( in ), filtered ) ); } - - - // Storage for results. - final Map< Integer, List< Spot > > output = new HashMap<>( polygonsMap.size() ); - - // Simplify them and compute a quality. - for ( final Integer label : polygonsMap.keySet() ) + else { - final List< Spot > spots = new ArrayList<>( polygonsMap.size() ); - output.put( label, spots ); - - final List< Polygon > polygons = polygonsMap.get( label ); - for ( final Polygon polygon : polygons ) - { - final PolygonRoi roi = new PolygonRoi( polygon, PolygonRoi.POLYGON ); - - // Create Spot ROI. - final PolygonRoi fRoi; - if ( simplify ) - fRoi = simplify( roi, SMOOTH_INTERVAL, DOUGLAS_PEUCKER_MAX_DISTANCE ); - else - fRoi = roi; - - // Don't include ROIs that have been shrunk to < 1 pixel. - if ( fRoi.getNCoordinates() < 3 || fRoi.getStatistics().area <= 0. ) - continue; - - final Polygon fPolygon = fRoi.getPolygon(); - final double[] xpoly = new double[ fPolygon.npoints ]; - final double[] ypoly = new double[ fPolygon.npoints ]; - for ( int i = 0; i < fPolygon.npoints; i++ ) - { - xpoly[ i ] = calibration[ 0 ] * ( interval.min( 0 ) + fPolygon.xpoints[ i ] - 0.5 ); - ypoly[ i ] = calibration[ 1 ] * ( interval.min( 1 ) + fPolygon.ypoints[ i ] - 0.5 ); - } - - final Spot spot = SpotRoi.createSpot( xpoly, ypoly, -1. ); - - // Measure quality. - final double quality; - if ( null == qualityImage ) - { - quality = fRoi.getStatistics().area; - } - else - { - final String name = "QualityImage"; - final AxisType[] axes = new AxisType[] { Axes.X, Axes.Y }; - final double[] cal = new double[] { calibration[ 0 ], calibration[ 1 ] }; - final String[] units = new String[] { "unitX", "unitY" }; - final ImgPlus< S > qualityImgPlus = new ImgPlus<>( ImgPlus.wrapToImg( qualityImage ), name, axes, cal, units ); - final IterableInterval< S > iterable = SpotUtil.iterable( spot, qualityImgPlus ); - double max = Double.NEGATIVE_INFINITY; - for ( final S s : iterable ) - { - final double val = s.getRealDouble(); - if ( val > max ) - max = val; - } - quality = max; - } - spot.putFeature( Spot.QUALITY, quality ); - spots.add( spot ); - } + filtered = in; } - return output; - } - - private static final double distanceSquaredBetweenPoints( final double vx, final double vy, final double wx, final double wy ) - { - final double deltax = ( vx - wx ); - final double deltay = ( vy - wy ); - return deltax * deltax + deltay * deltay; - } - private static final double distanceToSegmentSquared( final double px, final double py, final double vx, final double vy, final double wx, final double wy ) - { - final double l2 = distanceSquaredBetweenPoints( vx, vy, wx, wy ); - if ( l2 == 0 ) - return distanceSquaredBetweenPoints( px, py, vx, vy ); - final double t = ( ( px - vx ) * ( wx - vx ) + ( py - vy ) * ( wy - vy ) ) / l2; - if ( t < 0 ) - return distanceSquaredBetweenPoints( px, py, vx, vy ); - if ( t > 1 ) - return distanceSquaredBetweenPoints( px, py, wx, wy ); - return distanceSquaredBetweenPoints( px, py, ( vx + t * ( wx - vx ) ), ( vy + t * ( wy - vy ) ) ); - } - - private static final double perpendicularDistance( final double px, final double py, final double vx, final double vy, final double wx, final double wy ) - { - return Math.sqrt( distanceToSegmentSquared( px, py, vx, vy, wx, wy ) ); - } - - private static final void douglasPeucker( final List< double[] > list, final int s, final int e, final double epsilon, final List< double[] > resultList ) - { - // Find the point with the maximum distance - double dmax = 0; - int index = 0; - - final int start = s; - final int end = e - 1; - for ( int i = start + 1; i < end; i++ ) + if ( input.numDimensions() == 2 ) { - // Point - final double px = list.get( i )[ 0 ]; - final double py = list.get( i )[ 1 ]; - // Start - final double vx = list.get( start )[ 0 ]; - final double vy = list.get( start )[ 1 ]; - // End - final double wx = list.get( end )[ 0 ]; - final double wy = list.get( end )[ 1 ]; - final double d = perpendicularDistance( px, py, vx, vy, wx, wy ); - if ( d > dmax ) - { - index = i; - dmax = d; - } + /* + * In 2D: Threshold, make a labeling, then create contours. + */ + return SpotRoiUtils.from2DThresholdWithROI( + filtered, + interval.minAsDoubleArray(), + calibration, + threshold, + simplify, + qualityImage ); } - // If max distance is greater than epsilon, recursively simplify - if ( dmax > epsilon ) + else if ( input.numDimensions() == 3 ) { - // Recursive call - douglasPeucker( list, s, index, epsilon, resultList ); - douglasPeucker( list, index, e, epsilon, resultList ); + /* + * In 3D: Directly operate on grayscale to create a big mesh, + * separate it in connected components, remerge them based on + * bounding-box before creating spots. We want to use the grayscale + * version of marching-cubes to have nice, smooth meshes. + */ + return SpotMeshUtils.from3DThresholdWithROI( + filtered, + interval.minAsDoubleArray(), + calibration, + threshold, + simplify, + qualityImage ); } else { - if ( ( end - start ) > 0 ) - { - resultList.add( list.get( start ) ); - resultList.add( list.get( end ) ); - } - else - { - resultList.add( list.get( start ) ); - } + throw new IllegalArgumentException( "Can only process 2D or 3D images with this method, but got " + input.numDimensions() + "D." ); } } - /** - * Given a curve composed of line segments find a similar curve with fewer - * points. - *

- * The Ramer–Douglas–Peucker algorithm (RDP) is an algorithm for reducing - * the number of points in a curve that is approximated by a series of - * points. - *

- * - * @see Ramer–Douglas–Peucker - * Algorithm (Wikipedia) - * @author Justin Wetherell - * @param list - * List of Double[] points (x,y) - * @param epsilon - * Distance dimension - * @return Similar curve with fewer points - */ - public static final List< double[] > douglasPeucker( final List< double[] > list, final double epsilon ) - { - final List< double[] > resultList = new ArrayList<>(); - douglasPeucker( list, 0, list.size(), epsilon, resultList ); - return resultList; - } - - public static final PolygonRoi simplify( final PolygonRoi roi, final double smoothInterval, final double epsilon ) - { - final FloatPolygon fPoly = roi.getInterpolatedPolygon( smoothInterval, true ); - - final List< double[] > points = new ArrayList<>( fPoly.npoints ); - for ( int i = 0; i < fPoly.npoints; i++ ) - points.add( new double[] { fPoly.xpoints[ i ], fPoly.ypoints[ i ] } ); - - final List< double[] > simplifiedPoints = douglasPeucker( points, epsilon ); - final float[] sX = new float[ simplifiedPoints.size() ]; - final float[] sY = new float[ simplifiedPoints.size() ]; - for ( int i = 0; i < sX.length; i++ ) - { - sX[ i ] = ( float ) simplifiedPoints.get( i )[ 0 ]; - sY[ i ] = ( float ) simplifiedPoints.get( i )[ 1 ]; - } - final FloatPolygon simplifiedPolygon = new FloatPolygon( sX, sY ); - final PolygonRoi fRoi = new PolygonRoi( simplifiedPolygon, PolygonRoi.POLYGON ); - return fRoi; - } - /** * Start at 1. * @@ -764,455 +482,4 @@ public boolean hasNext() } }; } - - /** - * Parse a 2D mask and return a list of polygons for the external contours - * of white objects. - *

- * Warning: cannot deal with holes, they are simply ignored. - *

- * Copied and adapted from ImageJ1 code by Wayne Rasband. - * - * @param - * the type of the mask. - * @param mask - * the mask image. - * @return a new list of polygons. - */ - private static final < T extends BooleanType< T > > List< Polygon > maskToPolygons( final RandomAccessibleInterval< T > mask ) - { - final int w = ( int ) mask.dimension( 0 ); - final int h = ( int ) mask.dimension( 1 ); - final RandomAccess< T > ra = mask.randomAccess( mask ); - - final List< Polygon > polygons = new ArrayList<>(); - boolean[] prevRow = new boolean[ w + 2 ]; - boolean[] thisRow = new boolean[ w + 2 ]; - final Outline[] outline = new Outline[ w + 1 ]; - - for ( int y = 0; y <= h; y++ ) - { - ra.setPosition( y, 1 ); - - final boolean[] b = prevRow; - prevRow = thisRow; - thisRow = b; - int xAfterLowerRightCorner = -1; - Outline oAfterLowerRightCorner = null; - - ra.setPosition( 0, 0 ); - thisRow[ 1 ] = y < h ? ra.get().get() : false; - - for ( int x = 0; x <= w; x++ ) - { - // we need to read one pixel ahead - ra.setPosition( x + 1, 0 ); - if ( y < h && x < w - 1 ) - thisRow[ x + 2 ] = ra.get().get(); - else if ( x < w - 1 ) - thisRow[ x + 2 ] = false; - - if ( thisRow[ x + 1 ] ) - { // i.e., pixel (x,y) is selected - if ( !prevRow[ x + 1 ] ) - { - // Upper edge of selected area: - // - left and right outlines are null: new outline - // - left null: append (line to left) - // - right null: prepend (line to right), or - // prepend&append (after lower right corner, two borders - // from one corner) - // - left == right: close (end of hole above) unless we - // can continue at the right - // - left != right: merge (prepend) unless we can - // continue at the right - if ( outline[ x ] == null ) - { - if ( outline[ x + 1 ] == null ) - { - outline[ x + 1 ] = outline[ x ] = new Outline(); - outline[ x ].append( x + 1, y ); - outline[ x ].append( x, y ); - } - else - { - outline[ x ] = outline[ x + 1 ]; - outline[ x + 1 ] = null; - outline[ x ].append( x, y ); - } - } - else if ( outline[ x + 1 ] == null ) - { - if ( x == xAfterLowerRightCorner ) - { - outline[ x + 1 ] = outline[ x ]; - outline[ x ] = oAfterLowerRightCorner; - outline[ x ].append( x, y ); - outline[ x + 1 ].prepend( x + 1, y ); - } - else - { - outline[ x + 1 ] = outline[ x ]; - outline[ x ] = null; - outline[ x + 1 ].prepend( x + 1, y ); - } - } - else if ( outline[ x + 1 ] == outline[ x ] ) - { - if ( x < w - 1 && y < h && x != xAfterLowerRightCorner - && !thisRow[ x + 2 ] && prevRow[ x + 2 ] ) - { // at lower right corner & next pxl deselected - outline[ x ] = null; - // outline[x+1] unchanged - outline[ x + 1 ].prepend( x + 1, y ); - xAfterLowerRightCorner = x + 1; - oAfterLowerRightCorner = outline[ x + 1 ]; - } - else - { - // MINUS (add inner hole) - // We cannot handle holes in TrackMate. -// polygons.add( outline[ x ].getPolygon() ); - outline[ x + 1 ] = null; - outline[ x ] = ( x == xAfterLowerRightCorner ) ? oAfterLowerRightCorner : null; - } - } - else - { - outline[ x ].prepend( outline[ x + 1 ] ); - for ( int x1 = 0; x1 <= w; x1++ ) - if ( x1 != x + 1 && outline[ x1 ] == outline[ x + 1 ] ) - { - outline[ x1 ] = outline[ x ]; - outline[ x + 1 ] = null; - outline[ x ] = ( x == xAfterLowerRightCorner ) ? oAfterLowerRightCorner : null; - break; - } - if ( outline[ x + 1 ] != null ) - throw new RuntimeException( "assertion failed" ); - } - } - if ( !thisRow[ x ] ) - { - // left edge - if ( outline[ x ] == null ) - throw new RuntimeException( "assertion failed" ); - outline[ x ].append( x, y + 1 ); - } - } - else - { // !thisRow[x + 1], i.e., pixel (x,y) is deselected - if ( prevRow[ x + 1 ] ) - { - // Lower edge of selected area: - // - left and right outlines are null: new outline - // - left == null: prepend - // - right == null: append, or append&prepend (after - // lower right corner, two borders from one corner) - // - right == left: close unless we can continue at the - // right - // - right != left: merge (append) unless we can - // continue at the right - if ( outline[ x ] == null ) - { - if ( outline[ x + 1 ] == null ) - { - outline[ x ] = outline[ x + 1 ] = new Outline(); - outline[ x ].append( x, y ); - outline[ x ].append( x + 1, y ); - } - else - { - outline[ x ] = outline[ x + 1 ]; - outline[ x + 1 ] = null; - outline[ x ].prepend( x, y ); - } - } - else if ( outline[ x + 1 ] == null ) - { - if ( x == xAfterLowerRightCorner ) - { - outline[ x + 1 ] = outline[ x ]; - outline[ x ] = oAfterLowerRightCorner; - outline[ x ].prepend( x, y ); - outline[ x + 1 ].append( x + 1, y ); - } - else - { - outline[ x + 1 ] = outline[ x ]; - outline[ x ] = null; - outline[ x + 1 ].append( x + 1, y ); - } - } - else if ( outline[ x + 1 ] == outline[ x ] ) - { - // System.err.println("add " + outline[x]); - if ( x < w - 1 && y < h && x != xAfterLowerRightCorner - && thisRow[ x + 2 ] && !prevRow[ x + 2 ] ) - { // at lower right corner & next pxl selected - outline[ x ] = null; - // outline[x+1] unchanged - outline[ x + 1 ].append( x + 1, y ); - xAfterLowerRightCorner = x + 1; - oAfterLowerRightCorner = outline[ x + 1 ]; - } - else - { - polygons.add( outline[ x ].getPolygon() ); - outline[ x + 1 ] = null; - outline[ x ] = x == xAfterLowerRightCorner ? oAfterLowerRightCorner : null; - } - } - else - { - if ( x < w - 1 && y < h && x != xAfterLowerRightCorner - && thisRow[ x + 2 ] && !prevRow[ x + 2 ] ) - { // at lower right corner && next pxl selected - outline[ x ].append( x + 1, y ); - outline[ x + 1 ].prepend( x + 1, y ); - xAfterLowerRightCorner = x + 1; - oAfterLowerRightCorner = outline[ x ]; - // outline[x + 1] unchanged (the one at the - // right-hand side of (x, y-1) to the top) - outline[ x ] = null; - } - else - { - outline[ x ].append( outline[ x + 1 ] ); // merge - for ( int x1 = 0; x1 <= w; x1++ ) - if ( x1 != x + 1 && outline[ x1 ] == outline[ x + 1 ] ) - { - outline[ x1 ] = outline[ x ]; - outline[ x + 1 ] = null; - outline[ x ] = ( x == xAfterLowerRightCorner ) ? oAfterLowerRightCorner : null; - break; - } - if ( outline[ x + 1 ] != null ) - throw new RuntimeException( "assertion failed" ); - } - } - } - if ( thisRow[ x ] ) - { - // right edge - if ( outline[ x ] == null ) - throw new RuntimeException( "assertion failed" ); - outline[ x ].prepend( x, y + 1 ); - } - } - } - } - return polygons; - } - - /** - * This class implements a Cartesian polygon in progress. The edges are - * supposed to be parallel to the x or y axis. It is implemented as a deque - * to be able to add points to both sides. - */ - private static class Outline - { - - private int[] x, y; - - private int first, last, reserved; - - /** - * Default extra (spare) space when enlarging arrays (similar - * performance with 6-20) - */ - private final int GROW = 10; - - public Outline() - { - reserved = GROW; - x = new int[ reserved ]; - y = new int[ reserved ]; - first = last = GROW / 2; - } - - /** - * Makes sure that enough free space is available at the beginning and - * end of the list, by enlarging the arrays if required - */ - private void needs( final int neededAtBegin, final int neededAtEnd ) - { - if ( neededAtBegin > first || neededAtEnd > reserved - last ) - { - final int extraSpace = Math.max( GROW, Math.abs( x[ last - 1 ] - x[ first ] ) ); - final int newSize = reserved + neededAtBegin + neededAtEnd + extraSpace; - final int newFirst = neededAtBegin + extraSpace / 2; - final int[] newX = new int[ newSize ]; - final int[] newY = new int[ newSize ]; - System.arraycopy( x, first, newX, newFirst, last - first ); - System.arraycopy( y, first, newY, newFirst, last - first ); - x = newX; - y = newY; - last += newFirst - first; - first = newFirst; - reserved = newSize; - } - } - - /** Adds point x, y at the end of the list */ - public void append( final int x, final int y ) - { - if ( last - first >= 2 && collinear( this.x[ last - 2 ], this.y[ last - 2 ], this.x[ last - 1 ], this.y[ last - 1 ], x, y ) ) - { - this.x[ last - 1 ] = x; // replace previous point - this.y[ last - 1 ] = y; - } - else - { - needs( 0, 1 ); // new point - this.x[ last ] = x; - this.y[ last ] = y; - last++; - } - } - - /** Adds point x, y at the beginning of the list */ - public void prepend( final int x, final int y ) - { - if ( last - first >= 2 && collinear( this.x[ first + 1 ], this.y[ first + 1 ], this.x[ first ], this.y[ first ], x, y ) ) - { - this.x[ first ] = x; // replace previous point - this.y[ first ] = y; - } - else - { - needs( 1, 0 ); // new point - first--; - this.x[ first ] = x; - this.y[ first ] = y; - } - } - - /** - * Merge with another Outline by adding it at the end. Thereafter, the - * other outline must not be used any more. - */ - public void append( final Outline o ) - { - final int size = last - first; - final int oSize = o.last - o.first; - if ( size <= o.first && oSize > reserved - last ) - { // we don't have enough space in our own array but in that of 'o' - System.arraycopy( x, first, o.x, o.first - size, size ); - System.arraycopy( y, first, o.y, o.first - size, size ); - x = o.x; - y = o.y; - first = o.first - size; - last = o.last; - reserved = o.reserved; - } - else - { // append to our own array - needs( 0, oSize ); - System.arraycopy( o.x, o.first, x, last, oSize ); - System.arraycopy( o.y, o.first, y, last, oSize ); - last += oSize; - } - } - - /** - * Merge with another Outline by adding it at the beginning. Thereafter, - * the other outline must not be used any more. - */ - public void prepend( final Outline o ) - { - final int size = last - first; - final int oSize = o.last - o.first; - if ( size <= o.reserved - o.last && oSize > first ) - { /* - * We don't have enough space in our own array but in that of - * 'o' so append our own data to that of 'o' - */ - System.arraycopy( x, first, o.x, o.last, size ); - System.arraycopy( y, first, o.y, o.last, size ); - x = o.x; - y = o.y; - first = o.first; - last = o.last + size; - reserved = o.reserved; - } - else - { // prepend to our own array - needs( oSize, 0 ); - first -= oSize; - System.arraycopy( o.x, o.first, x, first, oSize ); - System.arraycopy( o.y, o.first, y, first, oSize ); - } - } - - public Polygon getPolygon() - { - /* - * optimize out intermediate points of straight lines (created, - * e.g., by merging outlines) - */ - int i, j = first + 1; - for ( i = first + 1; i + 1 < last; j++ ) - { - if ( collinear( x[ j - 1 ], y[ j - 1 ], x[ j ], y[ j ], x[ j + 1 ], y[ j + 1 ] ) ) - { - // merge i + 1 into i - last--; - continue; - } - if ( i != j ) - { - x[ i ] = x[ j ]; - y[ i ] = y[ j ]; - } - i++; - } - // wraparound - if ( collinear( x[ j - 1 ], y[ j - 1 ], x[ j ], y[ j ], x[ first ], y[ first ] ) ) - last--; - else - { - x[ i ] = x[ j ]; - y[ i ] = y[ j ]; - } - if ( last - first > 2 && collinear( x[ last - 1 ], y[ last - 1 ], x[ first ], y[ first ], x[ first + 1 ], y[ first + 1 ] ) ) - first++; - - final int count = last - first; - final int[] xNew = new int[ count ]; - final int[] yNew = new int[ count ]; - System.arraycopy( x, first, xNew, 0, count ); - System.arraycopy( y, first, yNew, 0, count ); - return new Polygon( xNew, yNew, count ); - } - - /** Returns whether three points are on one straight line */ - public boolean collinear( final int x1, final int y1, final int x2, final int y2, final int x3, final int y3 ) - { - return ( x2 - x1 ) * ( y3 - y2 ) == ( y2 - y1 ) * ( x3 - x2 ); - } - - @Override - public String toString() - { - String res = "[first:" + first + ",last:" + last + - ",reserved:" + reserved + ":"; - if ( last > x.length ) - System.err.println( "ERROR!" ); - int nmax = 10; // don't print more coordinates than this - for ( int i = first; i < last && i < x.length; i++ ) - { - if ( last - first > nmax && i - first > nmax / 2 ) - { - i = last - nmax / 2; - res += "..."; - nmax = last - first; // dont check again - } - else - res += "(" + x[ i ] + "," + y[ i ] + ")"; - } - return res + "]"; - } - } - } diff --git a/src/main/java/fiji/plugin/trackmate/detection/Process2DZ.java b/src/main/java/fiji/plugin/trackmate/detection/Process2DZ.java new file mode 100644 index 000000000..b3f764d3e --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/detection/Process2DZ.java @@ -0,0 +1,315 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.detection; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.scijava.Cancelable; + +import fiji.plugin.trackmate.Logger; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.TrackModel; +import fiji.plugin.trackmate.action.LabelImgExporter; +import fiji.plugin.trackmate.action.LabelImgExporter.LabelIdPainting; +import fiji.plugin.trackmate.util.TMUtils; +import ij.ImagePlus; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imglib2.Interval; +import net.imglib2.RandomAccess; +import net.imglib2.algorithm.MultiThreadedBenchmarkAlgorithm; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.mesh.alg.TaubinSmoothing; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.mesh.view.TranslateMesh; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; +import net.imglib2.view.IntervalView; +import net.imglib2.view.Views; + +/** + * A {@link SpotDetector} for 3D images that work by running a spot segmentation + * algorithm on 2D slices, and merging results using a tracker. This yield a + * label image that is then converted to 3D meshes using the + * {@link LabelImageDetector}. + *

+ * This is a convenience class, made to be used in specialized + * {@link SpotDetectorFactory} with specific choices of detector and merging + * strategy. + * + * @author Jean-Yves Tinevez, 2023 + * + * @param + * the pixel type in the image processed. + */ +public class Process2DZ< T extends RealType< T > & NativeType< T > > + extends MultiThreadedBenchmarkAlgorithm + implements SpotDetector< T >, Cancelable +{ + + private static final String BASE_ERROR_MESSAGE = "[Process2DZ] "; + + private final ImgPlus< T > img; + + private final Interval interval; + + private final double[] calibration; + + private final Settings settings; + + private final boolean simplify; + + private List< Spot > spots; + + private final double smoothingScale; + + private boolean isCanceled; + + private String cancelReason; + + private TrackMate trackmate; + + /** + * Creates a new {@link Process2DZ} detector. + * + * @param img + * the input data. Must be 3D or 4D (3D plus possibly channels) + * and the 3 spatial dimensions must be X, Y and Z. + * @param interval + * the interval in the input data to process. Must have the same + * number of dimensions that the input data. + * @param calibration + * the pixel size array. + * @param settings + * a TrackMate settings object, configured to operate on the + * (cropped) input data as if it was a 2D(+C)+T image. + * @param simplifyMeshes + * whether or not to smooth and simplify meshes resulting from + * merging the 2D contours. + * @param smoothingScale + * if positive, will smooth the 3D mask by a gaussian of + * specified sigma to yield smooth meshes. + */ + public Process2DZ( + final ImgPlus< T > img, + final Interval interval, + final double[] calibration, + final Settings settings, + final boolean simplifyMeshes, + final double smoothingScale ) + { + this.img = img; + this.interval = interval; + this.calibration = calibration; + this.settings = settings; + this.simplify = simplifyMeshes; + this.smoothingScale = smoothingScale; + } + + @Override + public boolean checkInput() + { + if ( !( img.numDimensions() == 3 || img.numDimensions() == 4 ) ) + { + errorMessage = BASE_ERROR_MESSAGE + "Source image is not 3D or 4D, but " + img.numDimensions() + "D.\n"; + return false; + } + if ( img.dimensionIndex( Axes.TIME ) >= 0 ) + { + errorMessage = BASE_ERROR_MESSAGE + "Source image has a time dimension, but should not.\n"; + return false; + } + if ( img.dimensionIndex( Axes.Z ) < 0 ) + { + errorMessage = BASE_ERROR_MESSAGE + "Source image does not have a Z dimension.\n"; + return false; + } + if ( interval.numDimensions() != img.numDimensions() ) + { + errorMessage = BASE_ERROR_MESSAGE + "Provided interval does not have the same dimensionality that of the source image. " + + "Interval is " + interval.numDimensions() + "D and the image is " + img.numDimensions() + "D.\n"; + return false; + } + return true; + } + + @Override + public boolean process() + { + isCanceled = false; + cancelReason = null; + spots = null; + + /* + * Segment and track as a 2D+T image with the specified detector and + * settings. + */ + + // Make the final single T 3D image, a 2D + T image final by making Z->T + final IntervalView< T > cropped = Views.interval( img, interval ); + final ImagePlus imp = ImageJFunctions.wrap( cropped, null ); + final int nFrames = ( int ) interval.dimension( img.dimensionIndex( Axes.Z ) ); + final int cDim = img.dimensionIndex( Axes.CHANNEL ); + final int nChannels = cDim < 0 ? 1 : ( int ) interval.dimension( cDim ); + imp.setDimensions( nChannels, 1, nFrames ); + imp.getCalibration().pixelWidth = calibration[ 0 ]; + imp.getCalibration().pixelHeight = calibration[ 1 ]; + imp.getCalibration().pixelDepth = calibration[ 2 ]; + + // Execute segmentation and tracking. + final Settings settingsFrame = settings.copyOn( imp ); + this.trackmate = new TrackMate( settingsFrame ); + trackmate.setNumThreads( numThreads ); + trackmate.getModel().setLogger( Logger.VOID_LOGGER ); + if ( !trackmate.checkInput() || !trackmate.process() ) + { + errorMessage = BASE_ERROR_MESSAGE + trackmate.getErrorMessage(); + return false; + } + + // Get 2D+T masks + final ImagePlus lblImp = LabelImgExporter.createLabelImagePlus( trackmate.getModel(), imp, false, true, LabelIdPainting.LABEL_IS_TRACK_ID ); + + /* + * Exposes tracked labels as a 3D image and segment them again with + * label image detector. + */ + + // Back to a 3D single time-point image. + lblImp.setDimensions( lblImp.getNChannels(), lblImp.getNFrames(), lblImp.getNSlices() ); + + // Convert labels to 3D meshes. + final ImgPlus< T > lblImg = TMUtils.rawWraps( lblImp ); + final LabelImageDetector< T > detector = new LabelImageDetector<>( + lblImg, + lblImg, + calibration, + simplify, + smoothingScale ); + if ( !detector.checkInput() || !detector.process() ) + { + errorMessage = BASE_ERROR_MESSAGE + detector.getErrorMessage(); + return false; + } + + final List< Spot > results = detector.getResult(); + spots = new ArrayList<>( results.size() ); + + // To read the label value (=trackID) later. + final RandomAccess< T > ra = lblImg.randomAccess(); + final TrackModel tm = trackmate.getModel().getTrackModel(); + + for ( final Spot spot : results ) + { + + /* + * Smooth spot? + */ + + final Spot newSpot; + if ( !simplify || !spot.getClass().isAssignableFrom( SpotMesh.class ) ) + { + newSpot = spot; + } + else + { + final SpotMesh sm = ( SpotMesh ) spot; + final BufferMesh out = TaubinSmoothing.smooth( TranslateMesh.translate( sm.getMesh(), sm ) ); + newSpot = SpotMeshUtils.meshToSpotMesh( out, simplify, new double[] { 1., 1., 1. }, null, new double[] { 0., 0., 0. } ); + if ( newSpot == null ) + continue; + } + + /* + * Try to get quality from the tracks resulting from the 2D+T image. + */ + + // Position RA where the spot is. + for ( int d = 0; d < 3; d++ ) + ra.setPosition( Math.round( spot.getDoublePosition( d ) / calibration[ d ] ), d ); + + // Read track ID from label value. + final int trackID = ( int ) ra.get().getRealDouble() - 1; + + // Average quality from the corresponding track. + final Set< Spot > trackSpots = tm.trackSpots( trackID ); + final double avgQuality; + if ( trackSpots != null ) + { + avgQuality = trackSpots.stream() + .mapToDouble( s -> s.getFeature( Spot.QUALITY ).doubleValue() ) + .average() + .getAsDouble(); + } + else + { + // default if something goes wrong. + avgQuality = spot.getFeature( Spot.QUALITY ); + } + + // Pass quality to new spot. + newSpot.putFeature( Spot.QUALITY, Double.valueOf( avgQuality ) ); + + // Shift them by interval min. + newSpot.move( interval.min( img.dimensionIndex( Axes.X ) ) * calibration[ 0 ], 0 ); + newSpot.move( interval.min( img.dimensionIndex( Axes.Y ) ) * calibration[ 1 ], 1 ); + newSpot.move( interval.min( img.dimensionIndex( Axes.Z ) ) * calibration[ 2 ], 2 ); + + spots.add( newSpot ); + } + return true; + } + + @Override + public List< Spot > getResult() + { + return spots; + } + + // --- org.scijava.Cancelable methods --- + + @Override + public boolean isCanceled() + { + return isCanceled; + } + + @Override + public void cancel( final String reason ) + { + isCanceled = true; + cancelReason = reason; + if ( trackmate != null ) + trackmate.cancel( reason ); + } + + @Override + public String getCancelReason() + { + return cancelReason; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/detection/SpotDetector.java b/src/main/java/fiji/plugin/trackmate/detection/SpotDetector.java index d687a2bc3..eb22e109a 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/SpotDetector.java +++ b/src/main/java/fiji/plugin/trackmate/detection/SpotDetector.java @@ -30,15 +30,13 @@ import net.imglib2.type.numeric.RealType; /** - * Interface for Spot detector classes, that are able to segment spots of a - * given estimated radius within a 2D or 3D image. + * Interface for Spot detector classes, that are able to detect or segment spots + * in a single time-point 2D or 3D image. *

- * Normally, concrete implementation are not expected to be multi-threaded. - * Indeed, the {@link fiji.plugin.trackmate.TrackMate} trackmate generates one - * instance of the concrete implementation per thread, to process multiple - * frames simultaneously. + * Concrete implementation can be multithreaded. In that case TrackMate will + * possible allocate some threads to each instance of this class. * - * @author Jean-Yves Tinevez <jeanyves.tinevez@gmail.com> 2010 - 2012 + * @author Jean-Yves Tinevez, 2010 - 2012 * */ public interface SpotDetector< T extends RealType< T > & NativeType< T > > extends OutputAlgorithm< List< Spot > >, Benchmark diff --git a/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactoryGenericConfig.java b/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorConfigFactory.java similarity index 52% rename from src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactoryGenericConfig.java rename to src/main/java/fiji/plugin/trackmate/detection/SpotDetectorConfigFactory.java index c0790501f..9f246d88a 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactoryGenericConfig.java +++ b/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorConfigFactory.java @@ -2,18 +2,18 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2021 - 2026 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * 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 * . @@ -23,49 +23,47 @@ import java.util.Map; +import org.scijava.ui.config.Configurator; +import org.scijava.ui.config.visitors.Maps; + import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.gui.components.ConfigurationPanel; -import fiji.plugin.trackmate.util.cli.Configurator; -import fiji.plugin.trackmate.util.cli.FactoryGenericConfig; -import fiji.plugin.trackmate.util.cli.GenericDetectionConfigurationPanel; -import fiji.plugin.trackmate.util.cli.TrackMateSettingsBuilder; +import fiji.plugin.trackmate.util.config.FactoryGenericConfig; +import fiji.plugin.trackmate.util.config.GenericConfigPanelPreview; +import ij.ImagePlus; import net.imglib2.type.NativeType; import net.imglib2.type.numeric.RealType; /** - * Interface for detector factories that need to be configured with a + * Base interface for detector factories that need to be configured with a * {@link Configurator} instance. + *

+ * Automatically generates a config panel, default settings and settings + * serialization based on the {@link Configurator} instance. Subclasses need to + * implement at least the {@link #createConfig(ImagePlus)} method that + * instantiates a config based on the input image. + *

+ * In addition they also need to implement {@link SpotDetectorFactory} or + * {@link SpotGlobalDetectorFactory} to create the actual detector. * * @author Jean-Yves Tinevez * - * @param - * the type of pixels in the input image, which must implement - * {@link RealType} and {@link NativeType}. * @param - * the type of {@link Configurator} used to configure the detector - * factory. + * the type of {@link Configurator} used to configure the factory. */ -public interface SpotDetectorFactoryGenericConfig< T extends RealType< T > & NativeType< T >, C extends Configurator > extends SpotDetectorFactoryBase< T >, FactoryGenericConfig< C > +public interface SpotDetectorConfigFactory< T extends RealType< T > & NativeType< T >, C extends Configurator > extends SpotDetectorFactoryBase< T >, FactoryGenericConfig< C > { @Override public default ConfigurationPanel getDetectorConfigurationPanel( final Settings settings, final Model model ) { - final C config = getConfigurator( settings.imp ); - return new GenericDetectionConfigurationPanel( - settings, - model, - config, - getName(), - getIcon(), - getUrl(), - () -> this ); + return new GenericConfigPanelPreview( settings, model, createConfig( settings.imp ), () -> this ); } @Override - default Map< String, Object > getDefaultSettings() + public default Map< String, Object > getDefaultSettings() { - return TrackMateSettingsBuilder.getDefaultSettings( getConfigurator() ); + return Maps.toMap( createConfig() ); } } diff --git a/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactory.java index 5bb5d9afd..b67a1884d 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactory.java +++ b/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactory.java @@ -2,18 +2,18 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * 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 * . @@ -39,7 +39,7 @@ * @author Jean-Yves Tinevez * * @param - * the pixel type. + * the pixel type in the image processed by the detector. */ public interface SpotDetectorFactory< T extends RealType< T > & NativeType< T > > extends SpotDetectorFactoryBase< T > { @@ -60,9 +60,8 @@ public interface SpotDetectorFactory< T extends RealType< T > & NativeType< T > * is 3D without time, then the interval must be 3D), not * channel. * @param frame - * the frame index in the source image to operate on. - * @return a new {@link SpotDetector} configured to operate on the given - * target frame. + * the frame index in the source image to operate on + * @return a new detector. */ public SpotDetector< T > getDetector( final ImgPlus< T > img, final Map< String, Object > settings, final Interval interval, int frame ); } diff --git a/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactoryBase.java b/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactoryBase.java index ae4d44083..10d182de1 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactoryBase.java +++ b/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactoryBase.java @@ -58,7 +58,7 @@ public interface SpotDetectorFactoryBase< T extends RealType< T > & NativeType< *

* This flag may be used by clients to exploit the fact that the spots * created with this detector will have a contour that can be used - * e.g. to compute morphological features. The default is + * e.g. to compute 2D morphological features. The default is * false, indicating that this detector provides spots as a X, * Y, Z, radius tuple. * @@ -69,4 +69,22 @@ public default boolean has2Dsegmentation() { return false; } + + /** + * Returns true for the detectors that can provide a spot with + * a 3D SpotMesh when they operate on 3D images. + *

+ * This flag may be used by clients to exploit the fact that the spots + * created with this detector will have a 3D mesh that can be used + * e.g. to compute 3D morphological features. The default is + * false, indicating that this detector provides spots as a X, + * Y, Z, radius tuple. + * + * @return true if the spots created by this detector have a 3D + * mesh. + */ + public default boolean has3Dsegmentation() + { + return false; + } } diff --git a/src/main/java/fiji/plugin/trackmate/detection/SpotGlobalDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/detection/SpotGlobalDetectorFactory.java index efafcc3ee..840b3ffe3 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/SpotGlobalDetectorFactory.java +++ b/src/main/java/fiji/plugin/trackmate/detection/SpotGlobalDetectorFactory.java @@ -2,18 +2,18 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * 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 * . @@ -23,6 +23,7 @@ import java.util.Map; +import fiji.plugin.trackmate.util.TMUtils; import net.imagej.ImgPlus; import net.imglib2.Interval; import net.imglib2.type.NativeType; @@ -36,7 +37,7 @@ * @author Jean-Yves Tinevez * * @param - * the pixel type. + * the pixel type in the image processed by the detector. */ public interface SpotGlobalDetectorFactory< T extends RealType< T > & NativeType< T > > extends SpotDetectorFactoryBase< T > { @@ -51,12 +52,14 @@ public interface SpotGlobalDetectorFactory< T extends RealType< T > & NativeType * the settings map, used to configure the detector. * @param interval * the interval that determines the region in the source image to - * operate on. This must not have a dimension for time + * operate on. This must have a dimension for time, to + * specify what time-points to process, but not for channels * (e.g. if the source image is 2D+T (3D), then the - * interval must be 2D; if the source image is 3D without time, - * then the interval must be 3D), not channel. - * @return a new {@link SpotGlobalDetector}. + * interval must be 3D; if the source image is 3D without time, + * then the interval must be 4D). + * @return a new detector. + * @see TMUtils#getIntervalWithTime(net.imagej.ImgPlus, + * fiji.plugin.trackmate.Settings) */ public SpotGlobalDetector< T > getDetector( final ImgPlus< T > img, final Map< String, Object > settings, final Interval interval ); - } diff --git a/src/main/java/fiji/plugin/trackmate/detection/SpotMeshUtils.java b/src/main/java/fiji/plugin/trackmate/detection/SpotMeshUtils.java new file mode 100644 index 000000000..341376d58 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/detection/SpotMeshUtils.java @@ -0,0 +1,466 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.detection; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import net.imglib2.Interval; +import net.imglib2.IterableInterval; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.RealInterval; +import net.imglib2.algorithm.gauss3.Gauss3; +import net.imglib2.img.Img; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.MeshStats; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.alg.MeshConnectedComponents; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.roi.labeling.ImgLabeling; +import net.imglib2.roi.labeling.LabelRegion; +import net.imglib2.roi.labeling.LabelRegions; +import net.imglib2.type.NativeType; +import net.imglib2.type.logic.BoolType; +import net.imglib2.type.numeric.IntegerType; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.util.Intervals; +import net.imglib2.util.Util; +import net.imglib2.view.Views; + +/** + * Utility classes to create 3D {@link fiji.plugin.trackmate.SpotMesh}es from + * single time-point, single channel images. + * + * @author Jean-Yves Tinevez, 2023 + */ +public class SpotMeshUtils +{ + + /** Number of triangles below which not to simplify a mesh. */ + private static final int MIN_N_TRIANGLES = 100; + + /** Quadratic mesh decimation aggressiveness for simplification. */ + private static final float SIMPLIFY_AGGRESSIVENESS = 10f; + + /** Minimal volume, in pixels, below which we discard meshes. */ + private static final double MIN_MESH_PIXEL_VOLUME = 15.; + + /** + * Precision for the vertex duplicate removal step. A value of 2 means that + * the vertices with coordinates (in pixel units) equal up to the second + * decimal will be considered duplicates and merged. + */ + private static final int VERTEX_DUPLICATE_REMOVAL_PRECISION = 2; + + /** + * Creates spots with meshes from a 3D grayscale image. The + * quality value is read from a secondary image, by taking the max value in + * each object, or the volume if the quality image is null. + *

+ * The grayscale marching-cube algorithm is used to create one big mesh from + * the source image. It is then split in connected-components to create + * single spot objects. However, to deal with possible holes in objects, + * meshes are possibly re-merged based on full inclusion of their bounding + * box. For instance, a hollow sphere would be represented by two + * connected-components, yielding two meshes. But because the small one is + * included in the big one, they are merged in this method. + * + * @param + * the type of the source image. Must be real, scalar. + * @param + * the type of the quality image. Must be real, scalar. + * @param input + * the source image, must be zero-min and 3D. + * @param origin + * the origin (min pos) of the interval the labeling was + * generated from, used to reposition the spots from the zero-min + * labeling to the proper coordinates. + * @param calibration + * the physical calibration. + * @param threshold + * the threshold to apply to the input image. + * @param simplify + * if true the meshes will be post-processed to be + * smoother and contain less points. + * @param qualityImage + * the image in which to read the quality value. + * @return a list of spots, with meshes. + */ + public static < T extends RealType< T > & NativeType< T >, S extends RealType< S > > List< Spot > from3DThresholdWithROI( + final RandomAccessibleInterval< T > input, + final double[] origin, + final double[] calibration, + final double threshold, + final boolean simplify, + final RandomAccessibleInterval< S > qualityImage ) + { + if ( input.numDimensions() != 3 ) + throw new IllegalArgumentException( "Can only process 3D images with this method, but got " + input.numDimensions() + "D." ); + + // Get big mesh. + final Mesh mc = Meshes.marchingCubes( input, threshold ); + final Mesh bigMesh = Meshes.removeDuplicateVertices( mc, VERTEX_DUPLICATE_REMOVAL_PRECISION ); + + // Split into connected components. + final List< Mesh > meshes = new ArrayList<>(); + final List< RealInterval > boundingBoxes = new ArrayList<>(); + for ( final BufferMesh m : MeshConnectedComponents.iterable( bigMesh ) ) + { + meshes.add( m ); + boundingBoxes.add( Meshes.boundingBox( m ) ); + } + + // Merge if bb is included in one another. + final List< Mesh > out = new ArrayList<>(); + MESH_I: for ( int i = 0; i < meshes.size(); i++ ) + { + final RealInterval bbi = boundingBoxes.get( i ); + final Mesh meshi = meshes.get( i ); + + /* + * FIXME revise this. Improper and incorrect. + */ + + // Can we put it inside another? + for ( int j = i + 1; j < meshes.size(); j++ ) + { + final RealInterval bbj = boundingBoxes.get( j ); + if ( Intervals.contains( bbj, bbi ) ) + { + final Mesh meshj = meshes.get( j ); + + // Merge the ith into the jth. + final Mesh merged = Meshes.merge( Arrays.asList( meshi, meshj ) ); + meshes.set( j, merged ); + continue MESH_I; + } + } + + // We could not, retain it for later. + out.add( meshi ); + } + + // Create spot from merged meshes. + final List< Spot > spots = new ArrayList<>( out.size() ); + for ( final Mesh mesh : out ) + { + final SpotMesh spot = meshToSpotMesh( + mesh, + simplify, + calibration, + qualityImage, + origin ); + if ( spot != null ) + spots.add( spot ); + } + return spots; + } + + /** + * Creates spots with meshes from a 3D label image. The labels + * are possibly smoothed before creating the mesh. The quality value is read + * from a secondary image, by taking the max value inside the mesh. + * + * @param + * the type that backs-up the labeling. + * @param + * the type of the quality image. Must be real, scalar. + * @param labeling + * the labeling, must be zero-min and 3D. + * @param origin + * the origin (min pos) of the interval the labeling was + * generated from, used to reposition the spots from the zero-min + * labeling to the proper coordinates. + * @param calibration + * the physical calibration. + * @param simplify + * if true the meshes will be post-processed to + * contain less verrtices. + * @param smoothingScale + * if strictly larger than 0, the mask will be smoothed before + * creating the mesh, resulting in smoother meshes. The scale + * value sets the (Gaussian) filter radius and is specified in + * physical units. If 0 or lower than 0, no smoothing is applied. + * @param qualityImage + * the image in which to read the quality value. + * @return a list of spots, with meshes. + */ + public static < R extends IntegerType< R >, S extends RealType< S > > List< Spot > from3DLabelingWithROI( + final ImgLabeling< Integer, R > labeling, + final double[] origin, + final double[] calibration, + final boolean simplify, + final double smoothingScale, + final RandomAccessibleInterval< S > qualityImage ) + { + final Map< Integer, List< Spot > > map = from3DLabelingWithROIMap( labeling, origin, calibration, simplify, smoothingScale, qualityImage ); + final List< Spot > spots = new ArrayList<>(); + for ( final List< Spot > s : map.values() ) + spots.addAll( s ); + + return spots; + } + + /** + * Creates spots with meshes from a 3D label image. The labels + * are possibly smoothed before creating the mesh. The quality value is read + * from a secondary image, by taking the max value inside the mesh. + *

+ * The spots are returned in a map, where the key is the integer value of + * the label they correspond to in the label image. In 3D, there is one spot + * per label, even for disconnected components, so the lists are made of one + * element for now. + * + * @param + * the type that backs-up the labeling. + * @param + * the type of the quality image. Must be real, scalar. + * @param labeling + * the labeling, must be zero-min and 3D. + * @param origin + * the origin (min pos) of the interval the labeling was + * generated from, used to reposition the spots from the zero-min + * labeling to the proper coordinates. + * @param calibration + * the physical calibration. + * @param simplify + * if true the meshes will be post-processed to + * contain less verrtices. + * @param smoothingScale + * if strictly larger than 0, the mask will be smoothed before + * creating the mesh, resulting in smoother meshes. The scale + * value sets the (Gaussian) filter radius and is specified in + * physical units. If 0 or lower than 0, no smoothing is applied. + * @param qualityImage + * the image in which to read the quality value. + * @return a map linking the label integer value to the list of spots, with + * meshes, it corresponds to. + */ + public static < R extends IntegerType< R >, S extends RealType< S > > Map< Integer, List< Spot > > from3DLabelingWithROIMap( + final ImgLabeling< Integer, R > labeling, + final double[] origin, + final double[] calibration, + final boolean simplify, + final double smoothingScale, + final RandomAccessibleInterval< S > qualityImage ) + { + if ( labeling.numDimensions() != 3 ) + throw new IllegalArgumentException( "Can only process 3D images with this method, but got " + labeling.numDimensions() + "D." ); + + // Parse regions to create meshes on label. + final LabelRegions< Integer > regions = new LabelRegions< Integer >( labeling ); + final Iterator< LabelRegion< Integer > > iterator = regions.iterator(); + final Map< Integer, List< Spot > > spots = new HashMap<>( regions.getExistingLabels().size() ); + while ( iterator.hasNext() ) + { + final LabelRegion< Integer > region = iterator.next(); + final Spot spot = regionToSpotMesh( + region, + simplify, + calibration, + smoothingScale, + origin, + qualityImage ); + if ( spot == null ) + continue; + + spots.put( region.getLabel(), Collections.singletonList( spot ) ); + } + return spots; + } + + /** + * Returns a new {@link Spot} with a {@link SpotMesh} as shape, built from + * the specified bit-mask. + * + * @param + * the type of pixels in the quality image. + * @param region + * the bit-mask to build the mesh from. + * @param simplify + * if true the mesh will be simplified. + * @param calibration + * the pixel size array, used to scale the mesh to physical + * coordinates. + * @param qualityImage + * an image from which to read the quality value. If not + * null, the quality of the spot will be the max + * value of this image inside the mesh. If null, the + * quality will be the mesh volume. + * @param minInterval + * the origin in image coordinates of the ROI used for detection. + * @param smoothingScale + * if strictly larger than 0, the mask will be smoothed before + * creating the mesh, resulting in smoother meshes. The scale + * value sets the (Gaussian) filter radius and is specified in + * physical units. If 0 or lower than 0, no smoothing is applied. + * + * @return a new spot. + */ + private static < S extends RealType< S > > Spot regionToSpotMesh( + final RandomAccessibleInterval< BoolType > region, + final boolean simplify, + final double[] calibration, + final double smoothingScale, + final double[] minInterval, + final RandomAccessibleInterval< S > qualityImage ) + { + final RandomAccessibleInterval< BoolType > box = Views.zeroMin( region ); + final Mesh mesh; + + // Possibly filter. + final long[] borders; + if ( smoothingScale > 0 ) + { + final double[] sigmas = new double[ 3 ]; + for ( int d = 0; d < 3; d++ ) + sigmas[ d ] = smoothingScale / Math.sqrt( 3. ) / calibration[ d ]; + + // Increase the output size. + final int[] halfkernelsizes = Gauss3.halfkernelsizes( sigmas );; + borders = Arrays.stream( halfkernelsizes ).asLongStream().toArray(); + final Interval outputSize = Intervals.expand( box, borders ); + final Img< FloatType > img = Util.getArrayOrCellImgFactory( outputSize, new FloatType() ).create( outputSize ); + final RandomAccessibleInterval< FloatType > filtered = Views.translateInverse( img, borders ); + Gauss3.gauss( sigmas, Views.extendZero( box ), filtered ); + mesh = Meshes.marchingCubes( img, 0.5 ); + } + else + { + mesh = Meshes.marchingCubes( box ); + borders = new long[] { 0, 0, 0 }; + } + + // To mesh. + final Mesh cleaned = Meshes.removeDuplicateVertices( mesh, VERTEX_DUPLICATE_REMOVAL_PRECISION ); + // Shift coords. + final double[] origin = region.minAsDoubleArray(); + for ( int d = 0; d < 3; d++ ) + origin[ d ] += minInterval[ d ] - borders[ d ]; + // To spot. + return meshToSpotMesh( + cleaned, + simplify, + calibration, + qualityImage, + origin ); + } + + /** + * Creates a {@link SpotMesh} from a {@link Mesh}. + * + * @param + * the type of the quality image. + * @param mesh + * the mesh to create a spot from. + * @param simplify + * whether the simplify the mesh. + * @param calibration + * the pixel size array, to map pixel coords to physical coords. + * @param qualityImage + * the quality image. If not null the quality he + * quality of the spot will be the max value of this image inside + * the mesh. If null, the quality will be the mesh + * volume. + * @param origin + * the origin of the interval the mesh was created on. This is + * used to put back the mesh coordinates with respect to the + * initial source image (same referential that for the quality + * image). + * @return a new spot. + */ + public static < S extends RealType< S > > SpotMesh meshToSpotMesh( + final Mesh mesh, + final boolean simplify, + final double[] calibration, + final RandomAccessibleInterval< S > qualityImage, + final double[] origin ) + { + final Mesh simplified; + if ( simplify ) + { + // Dont't go below a certain number of triangles. + final int nTriangles = mesh.triangles().size(); + if ( nTriangles < MIN_N_TRIANGLES ) + { + simplified = mesh; + } + else + { + // Crude heuristics. + final float targetRatio; + if ( nTriangles < 2 * MIN_N_TRIANGLES ) + targetRatio = 0.5f; + else if ( nTriangles < 10_000 ) + targetRatio = 0.2f; + else if ( nTriangles < 1_000_000 ) + targetRatio = 0.1f; + else + targetRatio = 0.05f; + simplified = Meshes.simplify( mesh, targetRatio, SIMPLIFY_AGGRESSIVENESS ); + } + } + else + { + simplified = mesh; + } + // Remove meshes that are too small + final double volumeThreshold = MIN_MESH_PIXEL_VOLUME * calibration[ 0 ] * calibration[ 1 ] * calibration[ 2 ]; + if ( MeshStats.volume( simplified ) < volumeThreshold ) + return null; + + // Translate back to interval coords & scale to physical coords. + Meshes.translateScale( simplified, origin, calibration ); + + // Make spot with default quality. + final SpotMesh spot = new SpotMesh( simplified, 0. ); + + // Measure quality. + final double quality; + if ( null == qualityImage ) + { + quality = MeshStats.volume( simplified ); + } + else + { + final IterableInterval< S > iterable = spot.iterable( qualityImage, calibration ); + double max = Double.NEGATIVE_INFINITY; + for ( final S s : iterable ) + { + final double val = s.getRealDouble(); + if ( val > max ) + max = val; + } + quality = max; + } + spot.putFeature( Spot.QUALITY, Double.valueOf( quality ) ); + return spot; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/detection/SpotRoiUtils.java b/src/main/java/fiji/plugin/trackmate/detection/SpotRoiUtils.java new file mode 100644 index 000000000..453578718 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/detection/SpotRoiUtils.java @@ -0,0 +1,888 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.detection; + +import java.awt.Polygon; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotRoi; +import ij.gui.PolygonRoi; +import ij.process.FloatPolygon; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imagej.axis.AxisType; +import net.imglib2.Interval; +import net.imglib2.IterableInterval; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.algorithm.gauss3.Gauss3; +import net.imglib2.converter.Converters; +import net.imglib2.img.Img; +import net.imglib2.roi.labeling.ImgLabeling; +import net.imglib2.roi.labeling.LabelRegion; +import net.imglib2.roi.labeling.LabelRegions; +import net.imglib2.type.BooleanType; +import net.imglib2.type.NativeType; +import net.imglib2.type.logic.BoolType; +import net.imglib2.type.numeric.IntegerType; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.numeric.integer.IntType; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.util.Intervals; +import net.imglib2.util.Util; +import net.imglib2.view.IntervalView; +import net.imglib2.view.Views; + +/** + * Utility classes to create 2D {@link fiji.plugin.trackmate.SpotRoi}s from + * single time-point, single channel images. + * + * @author Jean-Yves Tinevez, 2023 + */ +public class SpotRoiUtils +{ + + /** Smoothing interval for ROIs. */ + private static final double SMOOTH_INTERVAL = 2.; + + /** Douglas-Peucker polygon simplification max distance. */ + private static final double DOUGLAS_PEUCKER_MAX_DISTANCE = 0.5; + + public static < T extends RealType< T > & NativeType< T >, S extends RealType< S > > List< Spot > from2DThresholdWithROI( + final RandomAccessibleInterval< T > input, + final double[] origin, + final double[] calibration, + final double threshold, + final boolean simplify, + final RandomAccessibleInterval< S > qualityImage ) + { + final ImgLabeling< Integer, IntType > labeling = MaskUtils.toLabeling( + input, + threshold, + 1 ); + return from2DLabelingWithROI( + labeling, + origin, + calibration, + simplify, + -1., + qualityImage ); + } + + /** + * Creates spots with ROIs from a 2D label image. The quality + * value is read from a secondary image, by taking the max value in each + * ROI. + * + * @param + * the type that backs-up the labeling. + * @param + * the type of the quality image. Must be real, scalar. + * @param labeling + * the labeling, must be zero-min and 2D. + * @param origin + * the origin (min pos) of the interval the labeling was + * generated from, used to reposition the spots from the zero-min + * labeling to the proper coordinates. + * @param calibration + * the physical calibration. + * @param simplify + * if true the polygon will be post-processed to be + * smoother and contain less points. + * @param smoothingScale + * if strictly larger than 0, the mask will be smoothed before + * creating the mesh, resulting in smoother meshes. The scale + * value sets the (Gaussian) filter radius and is specified in + * physical units. If 0 or lower than 0, no smoothing is applied. + * @param qualityImage + * the image in which to read the quality value. + * @return a list of spots, with ROI. + */ + public static < R extends IntegerType< R >, S extends RealType< S > > List< Spot > from2DLabelingWithROI( + final ImgLabeling< Integer, R > labeling, + final double[] origin, + final double[] calibration, + final boolean simplify, + final double smoothingScale, + final RandomAccessibleInterval< S > qualityImage ) + { + final Map< Integer, List< Spot > > map = from2DLabelingWithROIMap( + labeling, + origin, + calibration, + simplify, + smoothingScale, + qualityImage ); + final List< Spot > spots = new ArrayList<>(); + for ( final List< Spot > s : map.values() ) + spots.addAll( s ); + + return spots; + } + + /** + * Creates spots with ROIs from a 2D label image. The quality + * value is read from a secondary image, by taking the max value in each + * ROI. + *

+ * The spots are returned in a map, where the key is the integer value of + * the label they correspond to in the label image. Because one spot + * corresponds to one connected component in the label image, there might be + * several spots for a label, hence the values of the map are list of spots. + * + * @param + * the type that backs-up the labeling. + * @param + * the type of the quality image. Must be real, scalar. + * @param labeling + * the labeling, must be zero-min and 2D. + * @param origin + * the origin (min pos) of the interval the labeling was + * generated from, used to reposition the spots from the zero-min + * labeling to the proper coordinates. + * @param calibration + * the physical calibration. + * @param simplify + * if true the polygon will be post-processed to be + * smoother and contain less points. + * @param smoothingScale + * if strictly larger than 0, the mask will be smoothed before + * creating the mesh, resulting in smoother meshes. The scale + * value sets the (Gaussian) filter radius and is specified in + * physical units. If 0 or lower than 0, no smoothing is applied. + * @param qualityImage + * the image in which to read the quality value. + * @return a map linking the label integer value to the list of spots, with + * ROI, it corresponds to. + */ + public static < R extends IntegerType< R >, S extends RealType< S > > Map< Integer, List< Spot > > from2DLabelingWithROIMap( + final ImgLabeling< Integer, R > labeling, + final double[] origin, + final double[] calibration, + final boolean simplify, + final double smoothingScale, + final RandomAccessibleInterval< S > qualityImage ) + { + if ( labeling.numDimensions() != 2 ) + throw new IllegalArgumentException( "Can only process 2D images with this method, but got " + labeling.numDimensions() + "D." ); + + final LabelRegions< Integer > regions = new LabelRegions< Integer >( labeling ); + + /* + * Map of label in the label image to a collection of polygons around + * this label. Because 1 polygon correspond to 1 connected component, + * there might be several polygons for a label. + */ + final Map< Integer, List< Polygon > > polygonsMap = new HashMap<>( regions.getExistingLabels().size() ); + final Iterator< LabelRegion< Integer > > iterator = regions.iterator(); + // Parse regions to create polygons on boundaries. + while ( iterator.hasNext() ) + { + final LabelRegion< Integer > region = iterator.next(); + // Analyze in zero-min region. + final RandomAccessibleInterval< BoolType > mask = Views.zeroMin( region ); + final List< Polygon > pp; + // Smooth if requested. + if ( smoothingScale > 0 ) + pp = maskToPolygons( smoothMask( mask, smoothingScale, calibration ) ); + else + pp = maskToPolygons( mask ); + // Translate back to interval coords. + for ( final Polygon polygon : pp ) + polygon.translate( ( int ) region.min( 0 ), ( int ) region.min( 1 ) ); + + final Integer label = region.getLabel(); + polygonsMap.put( label, pp ); + } + + // Storage for results. + final Map< Integer, List< Spot > > output = new HashMap<>( polygonsMap.size() ); + + // Simplify them and compute a quality. + for ( final Integer label : polygonsMap.keySet() ) + { + final List< Spot > spots = new ArrayList<>( polygonsMap.size() ); + output.put( label, spots ); + + final List< Polygon > polygons = polygonsMap.get( label ); + for ( final Polygon polygon : polygons ) + { + final PolygonRoi roi = new PolygonRoi( polygon, PolygonRoi.POLYGON ); + + // Create Spot ROI. + final PolygonRoi fRoi; + if ( simplify ) + fRoi = simplify( roi, SMOOTH_INTERVAL, DOUGLAS_PEUCKER_MAX_DISTANCE ); + else + fRoi = roi; + + // Don't include ROIs that have been shrunk to < 1 pixel. + if ( fRoi.getNCoordinates() < 3 || fRoi.getStatistics().area <= 0. ) + continue; + + final Polygon fPolygon = fRoi.getPolygon(); + final double[] xpoly = new double[ fPolygon.npoints ]; + final double[] ypoly = new double[ fPolygon.npoints ]; + for ( int i = 0; i < fPolygon.npoints; i++ ) + { + xpoly[ i ] = calibration[ 0 ] * ( origin[ 0 ] + fPolygon.xpoints[ i ] - 0.5 ); + ypoly[ i ] = calibration[ 1 ] * ( origin[ 1 ] + fPolygon.ypoints[ i ] - 0.5 ); + } + + final Spot spot = SpotRoi.createSpot( xpoly, ypoly, -1. ); + + // Measure quality. + final double quality; + if ( null == qualityImage ) + { + quality = fRoi.getStatistics().area; + } + else + { + final String name = "QualityImage"; + final AxisType[] axes = new AxisType[] { Axes.X, Axes.Y }; + final double[] cal = new double[] { calibration[ 0 ], calibration[ 1 ] }; + final String[] units = new String[] { "unitX", "unitY" }; + final ImgPlus< S > qualityImgPlus = new ImgPlus<>( ImgPlus.wrapToImg( qualityImage ), name, axes, cal, units ); + final IterableInterval< S > iterable = spot.iterable( qualityImgPlus ); + double max = Double.NEGATIVE_INFINITY; + for ( final S s : iterable ) + { + final double val = s.getRealDouble(); + if ( val > max ) + max = val; + } + quality = max; + } + spot.putFeature( Spot.QUALITY, quality ); + spots.add( spot ); + } + } + return output; + } + + private static final double distanceSquaredBetweenPoints( final double vx, final double vy, final double wx, final double wy ) + { + final double deltax = ( vx - wx ); + final double deltay = ( vy - wy ); + return deltax * deltax + deltay * deltay; + } + + private static final double distanceToSegmentSquared( final double px, final double py, final double vx, final double vy, final double wx, final double wy ) + { + final double l2 = distanceSquaredBetweenPoints( vx, vy, wx, wy ); + if ( l2 == 0 ) + return distanceSquaredBetweenPoints( px, py, vx, vy ); + final double t = ( ( px - vx ) * ( wx - vx ) + ( py - vy ) * ( wy - vy ) ) / l2; + if ( t < 0 ) + return distanceSquaredBetweenPoints( px, py, vx, vy ); + if ( t > 1 ) + return distanceSquaredBetweenPoints( px, py, wx, wy ); + return distanceSquaredBetweenPoints( px, py, ( vx + t * ( wx - vx ) ), ( vy + t * ( wy - vy ) ) ); + } + + private static final double perpendicularDistance( final double px, final double py, final double vx, final double vy, final double wx, final double wy ) + { + return Math.sqrt( distanceToSegmentSquared( px, py, vx, vy, wx, wy ) ); + } + + private static final void douglasPeucker( final List< double[] > list, final int s, final int e, final double epsilon, final List< double[] > resultList ) + { + // Find the point with the maximum distance + double dmax = 0; + int index = 0; + + final int start = s; + final int end = e - 1; + for ( int i = start + 1; i < end; i++ ) + { + // Point + final double px = list.get( i )[ 0 ]; + final double py = list.get( i )[ 1 ]; + // Start + final double vx = list.get( start )[ 0 ]; + final double vy = list.get( start )[ 1 ]; + // End + final double wx = list.get( end )[ 0 ]; + final double wy = list.get( end )[ 1 ]; + final double d = perpendicularDistance( px, py, vx, vy, wx, wy ); + if ( d > dmax ) + { + index = i; + dmax = d; + } + } + // If max distance is greater than epsilon, recursively simplify + if ( dmax > epsilon ) + { + // Recursive call + douglasPeucker( list, s, index, epsilon, resultList ); + douglasPeucker( list, index, e, epsilon, resultList ); + } + else + { + if ( ( end - start ) > 0 ) + { + resultList.add( list.get( start ) ); + resultList.add( list.get( end ) ); + } + else + { + resultList.add( list.get( start ) ); + } + } + } + + /** + * Given a curve composed of line segments find a similar curve with fewer + * points. + *

+ * The Ramer–Douglas–Peucker algorithm (RDP) is an algorithm for reducing + * the number of points in a curve that is approximated by a series of + * points. + *

+ * + * @see Ramer–Douglas–Peucker + * Algorithm (Wikipedia) + * @author Justin Wetherell + * @param list + * List of double[] points (x,y) + * @param epsilon + * Distance dimension + * @return Similar curve with fewer points + */ + public static final List< double[] > douglasPeucker( final List< double[] > list, final double epsilon ) + { + final List< double[] > resultList = new ArrayList<>(); + douglasPeucker( list, 0, list.size(), epsilon, resultList ); + return resultList; + } + + public static final PolygonRoi simplify( final PolygonRoi roi, final double smoothInterval, final double epsilon ) + { + final FloatPolygon fPoly = roi.getInterpolatedPolygon( smoothInterval, true ); + + final List< double[] > points = new ArrayList<>( fPoly.npoints ); + for ( int i = 0; i < fPoly.npoints; i++ ) + points.add( new double[] { fPoly.xpoints[ i ], fPoly.ypoints[ i ] } ); + + final List< double[] > simplifiedPoints = douglasPeucker( points, epsilon ); + final float[] sX = new float[ simplifiedPoints.size() ]; + final float[] sY = new float[ simplifiedPoints.size() ]; + for ( int i = 0; i < sX.length; i++ ) + { + sX[ i ] = ( float ) simplifiedPoints.get( i )[ 0 ]; + sY[ i ] = ( float ) simplifiedPoints.get( i )[ 1 ]; + } + final FloatPolygon simplifiedPolygon = new FloatPolygon( sX, sY ); + final PolygonRoi fRoi = new PolygonRoi( simplifiedPolygon, PolygonRoi.POLYGON ); + return fRoi; + } + + /** + * Smooths a 2D binary mask using a Gaussian filter. + * + * @param mask + * the binary mask to smooth. + * @param smoothingScale + * the Gaussian sigma in physical units. + * @param calibration + * the pixel calibration. + * @return a new smoothed mask as a binary image. + */ + private static RandomAccessibleInterval< BoolType > smoothMask( + final RandomAccessibleInterval< BoolType > mask, + final double smoothingScale, + final double[] calibration ) + { + final double[] sigmas = new double[ 2 ]; + for ( int d = 0; d < 2; d++ ) + sigmas[ d ] = smoothingScale / calibration[ d ]; + + final int[] halfkernelsizes = Gauss3.halfkernelsizes( sigmas ); + final long[] borders = Arrays.stream( halfkernelsizes ).asLongStream().toArray(); + final Interval outputSize = Intervals.expand( mask, borders ); + final Img< FloatType > img = Util.getArrayOrCellImgFactory( outputSize, new FloatType() ).create( outputSize ); + final RandomAccessibleInterval< FloatType > filtered = Views.translateInverse( img, borders ); + Gauss3.gauss( sigmas, Views.extendZero( mask ), filtered ); + final IntervalView< FloatType > crop = Views.interval( filtered, mask ); + final RandomAccessibleInterval< BoolType > smoothedMask = Converters.convert( crop, ( i, o ) -> o.set( i.get() > 0.5f ), new BoolType() ); + return smoothedMask; + } + + /** + * Parse a 2D mask and return a list of polygons for the external contours + * of white objects. + *

+ * Warning: cannot deal with holes, they are simply ignored. + *

+ * Copied and adapted from ImageJ1 code by Wayne Rasband. + * + * @param + * the type of the mask. + * @param mask + * the mask image. + * @return a new list of polygons. + */ + private static final < T extends BooleanType< T > > List< Polygon > maskToPolygons( final RandomAccessibleInterval< T > mask ) + { + final int w = ( int ) mask.dimension( 0 ); + final int h = ( int ) mask.dimension( 1 ); + final RandomAccess< T > ra = mask.randomAccess( mask ); + + final List< Polygon > polygons = new ArrayList<>(); + boolean[] prevRow = new boolean[ w + 2 ]; + boolean[] thisRow = new boolean[ w + 2 ]; + final Outline[] outline = new Outline[ w + 1 ]; + + for ( int y = 0; y <= h; y++ ) + { + ra.setPosition( y, 1 ); + + final boolean[] b = prevRow; + prevRow = thisRow; + thisRow = b; + int xAfterLowerRightCorner = -1; + Outline oAfterLowerRightCorner = null; + + ra.setPosition( 0, 0 ); + thisRow[ 1 ] = y < h ? ra.get().get() : false; + + for ( int x = 0; x <= w; x++ ) + { + // we need to read one pixel ahead + ra.setPosition( x + 1, 0 ); + if ( y < h && x < w - 1 ) + thisRow[ x + 2 ] = ra.get().get(); + else if ( x < w - 1 ) + thisRow[ x + 2 ] = false; + + if ( thisRow[ x + 1 ] ) + { // i.e., pixel (x,y) is selected + if ( !prevRow[ x + 1 ] ) + { + // Upper edge of selected area: + // - left and right outlines are null: new outline + // - left null: append (line to left) + // - right null: prepend (line to right), or + // prepend&append (after lower right corner, two borders + // from one corner) + // - left == right: close (end of hole above) unless we + // can continue at the right + // - left != right: merge (prepend) unless we can + // continue at the right + if ( outline[ x ] == null ) + { + if ( outline[ x + 1 ] == null ) + { + outline[ x + 1 ] = outline[ x ] = new Outline(); + outline[ x ].append( x + 1, y ); + outline[ x ].append( x, y ); + } + else + { + outline[ x ] = outline[ x + 1 ]; + outline[ x + 1 ] = null; + outline[ x ].append( x, y ); + } + } + else if ( outline[ x + 1 ] == null ) + { + if ( x == xAfterLowerRightCorner ) + { + outline[ x + 1 ] = outline[ x ]; + outline[ x ] = oAfterLowerRightCorner; + outline[ x ].append( x, y ); + outline[ x + 1 ].prepend( x + 1, y ); + } + else + { + outline[ x + 1 ] = outline[ x ]; + outline[ x ] = null; + outline[ x + 1 ].prepend( x + 1, y ); + } + } + else if ( outline[ x + 1 ] == outline[ x ] ) + { + if ( x < w - 1 && y < h && x != xAfterLowerRightCorner + && !thisRow[ x + 2 ] && prevRow[ x + 2 ] ) + { // at lower right corner & next pxl deselected + outline[ x ] = null; + // outline[x+1] unchanged + outline[ x + 1 ].prepend( x + 1, y ); + xAfterLowerRightCorner = x + 1; + oAfterLowerRightCorner = outline[ x + 1 ]; + } + else + { + // MINUS (add inner hole) + // We cannot handle holes in TrackMate. +// polygons.add( outline[ x ].getPolygon() ); + outline[ x + 1 ] = null; + outline[ x ] = ( x == xAfterLowerRightCorner ) ? oAfterLowerRightCorner : null; + } + } + else + { + outline[ x ].prepend( outline[ x + 1 ] ); + for ( int x1 = 0; x1 <= w; x1++ ) + if ( x1 != x + 1 && outline[ x1 ] == outline[ x + 1 ] ) + { + outline[ x1 ] = outline[ x ]; + outline[ x + 1 ] = null; + outline[ x ] = ( x == xAfterLowerRightCorner ) ? oAfterLowerRightCorner : null; + break; + } + if ( outline[ x + 1 ] != null ) + throw new RuntimeException( "assertion failed" ); + } + } + if ( !thisRow[ x ] ) + { + // left edge + if ( outline[ x ] == null ) + throw new RuntimeException( "assertion failed" ); + outline[ x ].append( x, y + 1 ); + } + } + else + { // !thisRow[x + 1], i.e., pixel (x,y) is deselected + if ( prevRow[ x + 1 ] ) + { + // Lower edge of selected area: + // - left and right outlines are null: new outline + // - left == null: prepend + // - right == null: append, or append&prepend (after + // lower right corner, two borders from one corner) + // - right == left: close unless we can continue at the + // right + // - right != left: merge (append) unless we can + // continue at the right + if ( outline[ x ] == null ) + { + if ( outline[ x + 1 ] == null ) + { + outline[ x ] = outline[ x + 1 ] = new Outline(); + outline[ x ].append( x, y ); + outline[ x ].append( x + 1, y ); + } + else + { + outline[ x ] = outline[ x + 1 ]; + outline[ x + 1 ] = null; + outline[ x ].prepend( x, y ); + } + } + else if ( outline[ x + 1 ] == null ) + { + if ( x == xAfterLowerRightCorner ) + { + outline[ x + 1 ] = outline[ x ]; + outline[ x ] = oAfterLowerRightCorner; + outline[ x ].prepend( x, y ); + outline[ x + 1 ].append( x + 1, y ); + } + else + { + outline[ x + 1 ] = outline[ x ]; + outline[ x ] = null; + outline[ x + 1 ].append( x + 1, y ); + } + } + else if ( outline[ x + 1 ] == outline[ x ] ) + { + // System.err.println("add " + outline[x]); + if ( x < w - 1 && y < h && x != xAfterLowerRightCorner + && thisRow[ x + 2 ] && !prevRow[ x + 2 ] ) + { // at lower right corner & next pxl selected + outline[ x ] = null; + // outline[x+1] unchanged + outline[ x + 1 ].append( x + 1, y ); + xAfterLowerRightCorner = x + 1; + oAfterLowerRightCorner = outline[ x + 1 ]; + } + else + { + polygons.add( outline[ x ].getPolygon() ); + outline[ x + 1 ] = null; + outline[ x ] = x == xAfterLowerRightCorner ? oAfterLowerRightCorner : null; + } + } + else + { + if ( x < w - 1 && y < h && x != xAfterLowerRightCorner + && thisRow[ x + 2 ] && !prevRow[ x + 2 ] ) + { // at lower right corner && next pxl selected + outline[ x ].append( x + 1, y ); + outline[ x + 1 ].prepend( x + 1, y ); + xAfterLowerRightCorner = x + 1; + oAfterLowerRightCorner = outline[ x ]; + // outline[x + 1] unchanged (the one at the + // right-hand side of (x, y-1) to the top) + outline[ x ] = null; + } + else + { + outline[ x ].append( outline[ x + 1 ] ); // merge + for ( int x1 = 0; x1 <= w; x1++ ) + if ( x1 != x + 1 && outline[ x1 ] == outline[ x + 1 ] ) + { + outline[ x1 ] = outline[ x ]; + outline[ x + 1 ] = null; + outline[ x ] = ( x == xAfterLowerRightCorner ) ? oAfterLowerRightCorner : null; + break; + } + if ( outline[ x + 1 ] != null ) + throw new RuntimeException( "assertion failed" ); + } + } + } + if ( thisRow[ x ] ) + { + // right edge + if ( outline[ x ] == null ) + throw new RuntimeException( "assertion failed" ); + outline[ x ].prepend( x, y + 1 ); + } + } + } + } + return polygons; + } + + /** + * This class implements a Cartesian polygon in progress. The edges are + * supposed to be parallel to the x or y axis. It is implemented as a deque + * to be able to add points to both sides. + */ + private static class Outline + { + + private int[] x, y; + + private int first, last, reserved; + + /** + * Default extra (spare) space when enlarging arrays (similar + * performance with 6-20) + */ + private final int GROW = 10; + + public Outline() + { + reserved = GROW; + x = new int[ reserved ]; + y = new int[ reserved ]; + first = last = GROW / 2; + } + + /** + * Makes sure that enough free space is available at the beginning and + * end of the list, by enlarging the arrays if required + */ + private void needs( final int neededAtBegin, final int neededAtEnd ) + { + if ( neededAtBegin > first || neededAtEnd > reserved - last ) + { + final int extraSpace = Math.max( GROW, Math.abs( x[ last - 1 ] - x[ first ] ) ); + final int newSize = reserved + neededAtBegin + neededAtEnd + extraSpace; + final int newFirst = neededAtBegin + extraSpace / 2; + final int[] newX = new int[ newSize ]; + final int[] newY = new int[ newSize ]; + System.arraycopy( x, first, newX, newFirst, last - first ); + System.arraycopy( y, first, newY, newFirst, last - first ); + x = newX; + y = newY; + last += newFirst - first; + first = newFirst; + reserved = newSize; + } + } + + /** Adds point x, y at the end of the list */ + public void append( final int x, final int y ) + { + if ( last - first >= 2 && collinear( this.x[ last - 2 ], this.y[ last - 2 ], this.x[ last - 1 ], this.y[ last - 1 ], x, y ) ) + { + this.x[ last - 1 ] = x; // replace previous point + this.y[ last - 1 ] = y; + } + else + { + needs( 0, 1 ); // new point + this.x[ last ] = x; + this.y[ last ] = y; + last++; + } + } + + /** Adds point x, y at the beginning of the list */ + public void prepend( final int x, final int y ) + { + if ( last - first >= 2 && collinear( this.x[ first + 1 ], this.y[ first + 1 ], this.x[ first ], this.y[ first ], x, y ) ) + { + this.x[ first ] = x; // replace previous point + this.y[ first ] = y; + } + else + { + needs( 1, 0 ); // new point + first--; + this.x[ first ] = x; + this.y[ first ] = y; + } + } + + /** + * Merge with another Outline by adding it at the end. Thereafter, the + * other outline must not be used any more. + */ + public void append( final Outline o ) + { + final int size = last - first; + final int oSize = o.last - o.first; + if ( size <= o.first && oSize > reserved - last ) + { // we don't have enough space in our own array but in that of 'o' + System.arraycopy( x, first, o.x, o.first - size, size ); + System.arraycopy( y, first, o.y, o.first - size, size ); + x = o.x; + y = o.y; + first = o.first - size; + last = o.last; + reserved = o.reserved; + } + else + { // append to our own array + needs( 0, oSize ); + System.arraycopy( o.x, o.first, x, last, oSize ); + System.arraycopy( o.y, o.first, y, last, oSize ); + last += oSize; + } + } + + /** + * Merge with another Outline by adding it at the beginning. Thereafter, + * the other outline must not be used any more. + */ + public void prepend( final Outline o ) + { + final int size = last - first; + final int oSize = o.last - o.first; + if ( size <= o.reserved - o.last && oSize > first ) + { /* + * We don't have enough space in our own array but in that of + * 'o' so append our own data to that of 'o' + */ + System.arraycopy( x, first, o.x, o.last, size ); + System.arraycopy( y, first, o.y, o.last, size ); + x = o.x; + y = o.y; + first = o.first; + last = o.last + size; + reserved = o.reserved; + } + else + { // prepend to our own array + needs( oSize, 0 ); + first -= oSize; + System.arraycopy( o.x, o.first, x, first, oSize ); + System.arraycopy( o.y, o.first, y, first, oSize ); + } + } + + public Polygon getPolygon() + { + /* + * optimize out intermediate points of straight lines (created, + * e.g., by merging outlines) + */ + int i, j = first + 1; + for ( i = first + 1; i + 1 < last; j++ ) + { + if ( collinear( x[ j - 1 ], y[ j - 1 ], x[ j ], y[ j ], x[ j + 1 ], y[ j + 1 ] ) ) + { + // merge i + 1 into i + last--; + continue; + } + if ( i != j ) + { + x[ i ] = x[ j ]; + y[ i ] = y[ j ]; + } + i++; + } + // wraparound + if ( collinear( x[ j - 1 ], y[ j - 1 ], x[ j ], y[ j ], x[ first ], y[ first ] ) ) + last--; + else + { + x[ i ] = x[ j ]; + y[ i ] = y[ j ]; + } + if ( last - first > 2 && collinear( x[ last - 1 ], y[ last - 1 ], x[ first ], y[ first ], x[ first + 1 ], y[ first + 1 ] ) ) + first++; + + final int count = last - first; + final int[] xNew = new int[ count ]; + final int[] yNew = new int[ count ]; + System.arraycopy( x, first, xNew, 0, count ); + System.arraycopy( y, first, yNew, 0, count ); + return new Polygon( xNew, yNew, count ); + } + + /** Returns whether three points are on one straight line */ + public boolean collinear( final int x1, final int y1, final int x2, final int y2, final int x3, final int y3 ) + { + return ( x2 - x1 ) * ( y3 - y2 ) == ( y2 - y1 ) * ( x3 - x2 ); + } + + @Override + public String toString() + { + String res = "[first:" + first + ",last:" + last + + ",reserved:" + reserved + ":"; + if ( last > x.length ) + System.err.println( "ERROR!" ); + int nmax = 10; // don't print more coordinates than this + for ( int i = first; i < last && i < x.length; i++ ) + { + if ( last - first > nmax && i - first > nmax / 2 ) + { + i = last - nmax / 2; + res += "..."; + nmax = last - first; // dont check again + } + else + res += "(" + x[ i ] + "," + y[ i ] + ")"; + } + return res + "]"; + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetector.java b/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetector.java index 82e3f14a2..65bd1e83a 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetector.java +++ b/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetector.java @@ -61,10 +61,16 @@ public class ThresholdDetector< T extends RealType< T > & NativeType< T > > impl protected final double threshold; + /** If true, the contours will be simplified. */ + protected final boolean simplify; + /** - * If true, the contours will be smoothed and simplified. + * If strictly larger than 0, the mask will be smoothed before creating the + * mesh, resulting in smoother meshes. The scale value sets the (Gaussian) + * filter radius and is specified in physical units. If 0 or lower than 0, + * no smoothing is applied. */ - protected final boolean simplify; + protected final double smoothingScale; /* * CONSTRUCTORS @@ -75,9 +81,11 @@ public ThresholdDetector( final Interval interval, final double[] calibration, final double threshold, - final boolean simplify ) + final boolean simplify, + final double smoothingScale ) { this.input = input; + this.smoothingScale = smoothingScale; this.interval = DetectionUtils.squeeze( interval ); this.calibration = calibration; this.threshold = threshold; @@ -110,27 +118,15 @@ public boolean checkInput() public boolean process() { final long start = System.currentTimeMillis(); - if ( input.numDimensions() == 2 ) - { - /* - * 2D: we compute and store the contour. - */ - spots = MaskUtils.fromThresholdWithROI( input, interval, calibration, threshold, simplify, numThreads, null ); - - } - else if ( input.numDimensions() == 3 ) - { - /* - * 3D: We create spots of the same volume that of the region. - */ - spots = MaskUtils.fromThreshold( input, interval, calibration, threshold, numThreads ); - } - else - { - errorMessage = baseErrorMessage + "Required a 2D or 3D input, got " + input.numDimensions() + "D."; - return false; - } - + spots = MaskUtils.fromThresholdWithROI( + input, + interval, + calibration, + threshold, + simplify, + smoothingScale, + numThreads, + null ); final long end = System.currentTimeMillis(); this.processingTime = end - start; diff --git a/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetectorFactory.java index c9d07ab49..c1d585a00 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetectorFactory.java +++ b/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetectorFactory.java @@ -64,9 +64,8 @@ public class ThresholdDetectorFactory< T extends RealType< T > & NativeType< T > + "Pixels in the designated channel that have " + "a value larger than the threshold are considered as part of the foreground, " + "and used to build connected regions. In 2D, spots are created with " - + "the (possibly simplified) contour of the region. In 3D, a spherical " - + "spot is created for each region in its center, with a volume equal to the " - + "region volume." + + "the (possibly simplified) contour of the region. In 3D, a mesh is " + + "created for each region." + "

" + "The spot quality stores the object area or volume in pixels." + ""; @@ -77,6 +76,14 @@ public class ThresholdDetectorFactory< T extends RealType< T > & NativeType< T > public static final String KEY_SIMPLIFY_CONTOURS = "SIMPLIFY_CONTOURS"; + /** + * If strictly larger than 0, the mask will be smoothed before creating the + * mesh, resulting in smoother meshes. The scale value sets the (Gaussian) + * filter radius and is specified in physical units. If 0 or lower than 0, + * no smoothing is applied. + */ + public static final String KEY_SMOOTHING_SCALE = "SMOOTHING_SCALE"; + public static final String KEY_INTENSITY_THRESHOLD = "INTENSITY_THRESHOLD"; /* @@ -88,6 +95,7 @@ public SpotDetector< T > getDetector( final ImgPlus< T > img, final Map< String, { final double intensityThreshold = ( Double ) settings.get( KEY_INTENSITY_THRESHOLD ); final boolean simplifyContours = ( Boolean ) settings.get( KEY_SIMPLIFY_CONTOURS ); + final double smoothingScale = ( Double ) settings.getOrDefault( KEY_SMOOTHING_SCALE, -1. ); final double[] calibration = TMUtils.getSpatialCalibration( img ); final int channel = ( Integer ) settings.get( KEY_TARGET_CHANNEL ) - 1; final RandomAccessible< T > imFrame = DetectionUtils.prepareFrameImg( img, channel, frame ); @@ -97,7 +105,8 @@ public SpotDetector< T > getDetector( final ImgPlus< T > img, final Map< String, interval, calibration, intensityThreshold, - simplifyContours ); + simplifyContours, + smoothingScale ); detector.setNumThreads( 1 ); return detector; } @@ -108,6 +117,12 @@ public boolean has2Dsegmentation() return true; } + @Override + public boolean has3Dsegmentation() + { + return true; + } + @Override public String getKey() { @@ -151,6 +166,7 @@ public Map< String, Object > getDefaultSettings() lSettings.put( KEY_TARGET_CHANNEL, DEFAULT_TARGET_CHANNEL ); lSettings.put( KEY_INTENSITY_THRESHOLD, 0. ); lSettings.put( KEY_SIMPLIFY_CONTOURS, true ); + lSettings.put( KEY_SMOOTHING_SCALE, -1. ); return lSettings; } } diff --git a/src/main/java/fiji/plugin/trackmate/detection/semiauto/SemiAutoTracker.java b/src/main/java/fiji/plugin/trackmate/detection/semiauto/SemiAutoTracker.java index 326232c7d..adc538588 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/semiauto/SemiAutoTracker.java +++ b/src/main/java/fiji/plugin/trackmate/detection/semiauto/SemiAutoTracker.java @@ -45,7 +45,6 @@ public class SemiAutoTracker< T extends RealType< T > & NativeType< T > > extend private final ImagePlus imp; - @SuppressWarnings( "unchecked" ) public SemiAutoTracker( final Model model, final SelectionModel selectionModel, final ImagePlus imp, final Logger logger ) { super( model, selectionModel, logger ); diff --git a/src/main/java/fiji/plugin/trackmate/features/AbstractFeatureGrapher.java b/src/main/java/fiji/plugin/trackmate/features/AbstractFeatureGrapher.java index 5ed7a9c4e..f2dfd6aa8 100644 --- a/src/main/java/fiji/plugin/trackmate/features/AbstractFeatureGrapher.java +++ b/src/main/java/fiji/plugin/trackmate/features/AbstractFeatureGrapher.java @@ -49,7 +49,6 @@ import fiji.plugin.trackmate.Dimension; import fiji.plugin.trackmate.util.ExportableChartPanel; -import fiji.plugin.trackmate.util.TMUtils; public abstract class AbstractFeatureGrapher { @@ -96,7 +95,7 @@ public AbstractFeatureGrapher( public JFrame render() { // X label - final String xAxisLabel = featureNames.get( xFeature ) + " (" + TMUtils.getUnitsFor( xDimension, spaceUnits, timeUnits ) + ")"; + final String xAxisLabel = featureNames.get( xFeature ) + " (" + xDimension.units( spaceUnits, timeUnits ) + ")"; // Find how many different dimensions final Set< Dimension > dimensions = getUniqueValues( yFeatures, yDimensions ); @@ -107,7 +106,7 @@ public JFrame render() { // Y label - final String yAxisLabel = TMUtils.getUnitsFor( dimension, spaceUnits, timeUnits ); + final String yAxisLabel = dimension.units( spaceUnits, timeUnits ); // Collect suitable feature for this dimension final List< String > featuresThisDimension = getCommonKeys( dimension, yFeatures, yDimensions ); diff --git a/src/main/java/fiji/plugin/trackmate/features/EdgeFeatureGrapher.java b/src/main/java/fiji/plugin/trackmate/features/EdgeFeatureGrapher.java index bfad53b4d..ab313c8f5 100644 --- a/src/main/java/fiji/plugin/trackmate/features/EdgeFeatureGrapher.java +++ b/src/main/java/fiji/plugin/trackmate/features/EdgeFeatureGrapher.java @@ -25,44 +25,34 @@ import org.jgrapht.graph.DefaultWeightedEdge; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; public class EdgeFeatureGrapher extends AbstractFeatureGrapher { private final List< DefaultWeightedEdge > edges; - private final Model model; - - private final SelectionModel selectionModel; - - private final DisplaySettings ds; - private final boolean addLines; + private final GuiModel guiModel; + public EdgeFeatureGrapher( + final GuiModel guiModel, final List< DefaultWeightedEdge > edges, final String xFeature, final List< String > yFeatures, - final Model model, - final SelectionModel selectionModel, - final DisplaySettings displaySettings, final boolean addLines ) { super( xFeature, yFeatures, - model.getFeatureModel().getEdgeFeatureDimensions().get( xFeature ), - model.getFeatureModel().getEdgeFeatureDimensions(), - model.getFeatureModel().getEdgeFeatureNames(), - model.getSpaceUnits(), - model.getTimeUnits() ); + guiModel.getModel().getFeatureModel().getEdgeFeatureDimensions().get( xFeature ), + guiModel.getModel().getFeatureModel().getEdgeFeatureDimensions(), + guiModel.getModel().getFeatureModel().getEdgeFeatureNames(), + guiModel.getModel().getSpaceUnits(), + guiModel.getModel().getTimeUnits() ); + this.guiModel = guiModel; this.edges = edges; - this.model = model; - this.selectionModel = selectionModel; - this.ds = displaySettings; this.addLines = addLines; } @@ -70,9 +60,9 @@ public EdgeFeatureGrapher( protected ModelDataset buildMainDataSet( final List< String > targetYFeatures ) { return new EdgeCollectionDataset( - model, - selectionModel, - ds, + guiModel.getModel(), + guiModel.getSelectionModel(), + guiModel.getDisplaySettings(), xFeature, targetYFeatures, edges, diff --git a/src/main/java/fiji/plugin/trackmate/features/FeatureAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/FeatureAnalyzer.java index 27af7bfad..8f22b8513 100644 --- a/src/main/java/fiji/plugin/trackmate/features/FeatureAnalyzer.java +++ b/src/main/java/fiji/plugin/trackmate/features/FeatureAnalyzer.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -32,29 +32,29 @@ public interface FeatureAnalyzer extends TrackMateModule /** * Returns the list of features this analyzer can compute. - * + * * @return the list of features. */ public List< String > getFeatures(); /** * Returns the map of short names for any feature the analyzer can compute. - * + * * @return the map of feature short names. */ public Map< String, String > getFeatureShortNames(); /** * Returns the map of names for any feature this analyzer can compute. - * + * * @return the map of feature names. */ public Map< String, String > getFeatureNames(); /** * Returns the map of feature dimension this analyzer can compute. - * - * @return the map of feature dimensions. + * + * @return the map of feature dimension. */ public Map< String, Dimension > getFeatureDimensions(); @@ -62,8 +62,8 @@ public interface FeatureAnalyzer extends TrackMateModule * Returns the map that states whether the key feature is a feature that * returns integers. If true, then special treatment is applied * when saving/loading, etc. for clarity and precision. - * - * @return the map of isIntFeature flags. + * + * @return the map. */ public Map< String, Boolean > getIsIntFeature(); diff --git a/src/main/java/fiji/plugin/trackmate/features/FeatureUtils.java b/src/main/java/fiji/plugin/trackmate/features/FeatureUtils.java index 96b09c25f..70972ef24 100644 --- a/src/main/java/fiji/plugin/trackmate/features/FeatureUtils.java +++ b/src/main/java/fiji/plugin/trackmate/features/FeatureUtils.java @@ -2,18 +2,18 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * 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 * . @@ -36,6 +36,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.features.edges.EdgeAnalyzer; import fiji.plugin.trackmate.features.manual.ManualEdgeColorAnalyzer; import fiji.plugin.trackmate.features.manual.ManualSpotColorAnalyzerFactory; @@ -162,17 +163,18 @@ public static final Map< String, String > collectFeatureKeys( final TrackMateObj } /** - * Collects all defined feature values for the given feature key and object - * type. Missing or undefined values are not included. + * Collect feature values from the specified model. Missing or undefined + * values are not included. * * @param featureKey - * the feature key to collect values for. + * the key of the feature to collect values from. * @param target - * the TrackMate object type. + * the type of object the feature is defined for. * @param model - * the TrackMate model. + * the model to read from. * @param visibleOnly - * if true, only values for visible objects are collected. + * if true feature values will be collected only + * from the objects marked as visible. * @return a new double[] array containing the numerical * feature values. */ @@ -385,6 +387,7 @@ public static final FeatureColorGenerator< Integer > createWholeTrackColorGenera public static final Model DUMMY_MODEL = new Model(); static { + DUMMY_MODEL.pauseUndo(); final Random ran = new Random(); DUMMY_MODEL.beginUpdate(); try @@ -401,7 +404,7 @@ public static final FeatureColorGenerator< Integer > createWholeTrackColorGenera 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 ); + final Spot spot = new SpotBase( x, y, z, r, q ); DUMMY_MODEL.addSpotTo( spot, t ); if ( previous != null ) DUMMY_MODEL.addEdge( previous, spot, ran.nextDouble() ); diff --git a/src/main/java/fiji/plugin/trackmate/features/SpotFeatureCalculator.java b/src/main/java/fiji/plugin/trackmate/features/SpotFeatureCalculator.java index f882bf744..74977f052 100644 --- a/src/main/java/fiji/plugin/trackmate/features/SpotFeatureCalculator.java +++ b/src/main/java/fiji/plugin/trackmate/features/SpotFeatureCalculator.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -127,11 +127,11 @@ public boolean process() * Calculates all the spot features configured in the {@link Settings} * object, but only for the spots in the specified collection. Features are * calculated for each spot, using their location, and the raw image. - * + * * @param toCompute - * the spots to compute features for. + * the spot collection. * @param doLogIt - * whether we should report progress to model's logger. + * if true the computation will be logged. */ public void computeSpotFeatures( final SpotCollection toCompute, final boolean doLogIt ) { diff --git a/src/main/java/fiji/plugin/trackmate/features/SpotFeatureGrapher.java b/src/main/java/fiji/plugin/trackmate/features/SpotFeatureGrapher.java index 52da8386e..b4e18040b 100644 --- a/src/main/java/fiji/plugin/trackmate/features/SpotFeatureGrapher.java +++ b/src/main/java/fiji/plugin/trackmate/features/SpotFeatureGrapher.java @@ -23,45 +23,35 @@ import java.util.List; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; public class SpotFeatureGrapher extends AbstractFeatureGrapher { private final List< Spot > spots; - private final SelectionModel selectionModel; - - private final Model model; - - private final DisplaySettings ds; - private final boolean addLines; + private final GuiModel guiModel; + public SpotFeatureGrapher( + final GuiModel guiModel, final List< Spot > spots, final String xFeature, final List< String > yFeatures, - final Model model, - final SelectionModel selectionModel, - final DisplaySettings displaySettings, final boolean addLines ) { super( xFeature, yFeatures, - model.getFeatureModel().getSpotFeatureDimensions().get( xFeature ), - model.getFeatureModel().getSpotFeatureDimensions(), - model.getFeatureModel().getSpotFeatureNames(), - model.getSpaceUnits(), - model.getTimeUnits() ); + guiModel.getModel().getFeatureModel().getSpotFeatureDimensions().get( xFeature ), + guiModel.getModel().getFeatureModel().getSpotFeatureDimensions(), + guiModel.getModel().getFeatureModel().getSpotFeatureNames(), + guiModel.getModel().getSpaceUnits(), + guiModel.getModel().getTimeUnits() ); + this.guiModel = guiModel; this.spots = spots; - this.model = model; - this.selectionModel = selectionModel; - this.ds = displaySettings; this.addLines = addLines; } @@ -69,9 +59,9 @@ public SpotFeatureGrapher( protected ModelDataset buildMainDataSet( final List< String > targetYFeatures ) { return new SpotCollectionDataset( - model, - selectionModel, - ds, + guiModel.getModel(), + guiModel.getSelectionModel(), + guiModel.getDisplaySettings(), xFeature, targetYFeatures, spots, diff --git a/src/main/java/fiji/plugin/trackmate/features/TrackCollectionDataset.java b/src/main/java/fiji/plugin/trackmate/features/TrackCollectionDataset.java index b3786de53..8567b586e 100644 --- a/src/main/java/fiji/plugin/trackmate/features/TrackCollectionDataset.java +++ b/src/main/java/fiji/plugin/trackmate/features/TrackCollectionDataset.java @@ -69,7 +69,15 @@ public String getItemLabel( final int item ) @Override public void setItemLabel( final int item, final String label ) { - model.getTrackModel().setName( trackIDs.get( item ), label ); + model.beginUpdate(); + try + { + model.setTrackName( trackIDs.get( item ), label ); + } + finally + { + model.endUpdate(); + } } @Override diff --git a/src/main/java/fiji/plugin/trackmate/features/TrackFeatureCalculator.java b/src/main/java/fiji/plugin/trackmate/features/TrackFeatureCalculator.java index 80f46fffe..a103b427d 100644 --- a/src/main/java/fiji/plugin/trackmate/features/TrackFeatureCalculator.java +++ b/src/main/java/fiji/plugin/trackmate/features/TrackFeatureCalculator.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -114,12 +114,11 @@ public boolean process() /** * Calculates all the track features configured in the {@link Settings} * object for the specified tracks. - * + * * @param trackIDs - * the ids of the tracks to compute features for. + * the IDs of the track to compute the features of. * @param doLogIt - * whether to log the feature computation progress to the model's - * logger. + * if true the computation will be logged. */ public void computeTrackFeatures( final Collection< Integer > trackIDs, final boolean doLogIt ) { diff --git a/src/main/java/fiji/plugin/trackmate/features/TrackFeatureGrapher.java b/src/main/java/fiji/plugin/trackmate/features/TrackFeatureGrapher.java index 05a8440ff..2964243bc 100644 --- a/src/main/java/fiji/plugin/trackmate/features/TrackFeatureGrapher.java +++ b/src/main/java/fiji/plugin/trackmate/features/TrackFeatureGrapher.java @@ -23,50 +23,40 @@ import java.util.List; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; public class TrackFeatureGrapher extends AbstractFeatureGrapher { private final List< Integer > trackIDs; - private final Model model; - - private final SelectionModel selectionModel; - - private final DisplaySettings ds; + private final GuiModel guiModel; public TrackFeatureGrapher( + final GuiModel guiModel, final List< Integer > trackIDs, final String xFeature, - final List< String > yFeatures, - final Model model, - final SelectionModel selectionModel, - final DisplaySettings displaySettings ) + final List< String > yFeatures ) { super( xFeature, yFeatures, - model.getFeatureModel().getTrackFeatureDimensions().get( xFeature ), - model.getFeatureModel().getTrackFeatureDimensions(), - model.getFeatureModel().getTrackFeatureNames(), - model.getSpaceUnits(), - model.getTimeUnits() ); + guiModel.getModel().getFeatureModel().getTrackFeatureDimensions().get( xFeature ), + guiModel.getModel().getFeatureModel().getTrackFeatureDimensions(), + guiModel.getModel().getFeatureModel().getTrackFeatureNames(), + guiModel.getModel().getSpaceUnits(), + guiModel.getModel().getTimeUnits() ); + this.guiModel = guiModel; this.trackIDs = trackIDs; - this.model = model; - this.selectionModel = selectionModel; - this.ds = displaySettings; } @Override protected ModelDataset buildMainDataSet( final List< String > targetYFeatures ) { return new TrackCollectionDataset( - model, - selectionModel, - ds, + guiModel.getModel(), + guiModel.getSelectionModel(), + guiModel.getDisplaySettings(), xFeature, targetYFeatures, trackIDs ); diff --git a/src/main/java/fiji/plugin/trackmate/features/edges/EdgeAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/edges/EdgeAnalyzer.java index 5350db7dc..a1a062ecc 100644 --- a/src/main/java/fiji/plugin/trackmate/features/edges/EdgeAnalyzer.java +++ b/src/main/java/fiji/plugin/trackmate/features/edges/EdgeAnalyzer.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -35,7 +35,7 @@ * edge of a TrackMate model. An edge, or a link, is the single link that exists * between two spots after tracking. * - * @author Jean-Yves Tinevez + * @author Jean-Yves Tinevez <jeanyves.tinevez@gmail.com> */ public interface EdgeAnalyzer extends Benchmark, FeatureAnalyzer, MultiThreaded { @@ -66,8 +66,8 @@ public interface EdgeAnalyzer extends Benchmark, FeatureAnalyzer, MultiThreaded *

* Example of non-local edge feature: the local curvature of the trajectory, * which depends on the neighbor edges. - * - * @return true if this is a local edge analyzer. + * + * @return whether this analyzer is a local analyzer. */ public boolean isLocal(); diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/ConvexHull.java b/src/main/java/fiji/plugin/trackmate/features/spot/ConvexHull2D.java similarity index 86% rename from src/main/java/fiji/plugin/trackmate/features/spot/ConvexHull.java rename to src/main/java/fiji/plugin/trackmate/features/spot/ConvexHull2D.java index b8eac30b9..1fb4d8758 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/ConvexHull.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/ConvexHull2D.java @@ -25,13 +25,14 @@ import java.util.Collections; import java.util.List; +import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotRoi; /** * Adapted from a code by Kirill Artemov, * https://github.com/DoctorGester/cia-stats. */ -public final class ConvexHull +public final class ConvexHull2D { private static List< Point > makeHull( final List< Point > points ) @@ -121,9 +122,9 @@ public int compareTo( final Point other ) public static SpotRoi convexHull( final SpotRoi roi ) { - final List< Point > points = new ArrayList<>( roi.x.length ); - for ( int i = 0; i < roi.x.length; i++ ) - points.add( new Point( roi.x[ i ], roi.y[ i ] ) ); + final List< Point > points = new ArrayList<>( roi.nPoints() ); + for ( int i = 0; i < roi.nPoints(); i++ ) + points.add( new Point( roi.xr( i ), roi.yr( i ) ) ); final List< Point > hull = makeHull( points ); final double[] xhull = new double[ hull.size() ]; @@ -133,6 +134,11 @@ public static SpotRoi convexHull( final SpotRoi roi ) xhull[ i ] = hull.get( i ).x; yhull[ i ] = hull.get( i ).y; } - return new SpotRoi( xhull, yhull ); + final double xc = roi.getDoublePosition( 0 ); + final double yc = roi.getDoublePosition( 1 ); + final double zc = roi.getDoublePosition( 2 ); + final double r = roi.getFeature( Spot.RADIUS ); + final double quality = roi.getFeature( Spot.QUALITY ); + return new SpotRoi( xc, yc, zc, r, quality, roi.getName(), xhull, yhull ); } } diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotFitEllipseAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DFitEllipseAnalyzer.java similarity index 83% rename from src/main/java/fiji/plugin/trackmate/features/spot/SpotFitEllipseAnalyzer.java rename to src/main/java/fiji/plugin/trackmate/features/spot/Spot2DFitEllipseAnalyzer.java index 65c87bc45..dfbb4fdc4 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotFitEllipseAnalyzer.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DFitEllipseAnalyzer.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -27,14 +27,13 @@ import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotRoi; import net.imglib2.type.numeric.RealType; -import net.imglib2.util.Util; -public class SpotFitEllipseAnalyzer< T extends RealType< T > > extends AbstractSpotFeatureAnalyzer< T > +public class Spot2DFitEllipseAnalyzer< T extends RealType< T > > extends AbstractSpotFeatureAnalyzer< T > { private final boolean is2D; - public SpotFitEllipseAnalyzer( final boolean is2D ) + public Spot2DFitEllipseAnalyzer( final boolean is2D ) { this.is2D = is2D; } @@ -51,13 +50,13 @@ public void process( final Spot spot ) if ( is2D ) { - final SpotRoi roi = spot.getRoi(); - if ( roi != null ) + if ( spot instanceof SpotRoi ) { - final double[] Q = fitEllipse( roi.x, roi.y ); + final SpotRoi roi = ( SpotRoi ) spot; + final double[] Q = fitEllipse( roi ); final double[] A = quadraticToCartesian( Q ); - x0 = A[ 0 ]; - y0 = A[ 1 ]; + x0 = A[ 0 ] - roi.getDoublePosition( 0 ); + y0 = A[ 1 ] - roi.getDoublePosition( 1 ); major = A[ 2 ]; minor = A[ 3 ]; theta = A[ 4 ]; @@ -83,12 +82,12 @@ public void process( final Spot spot ) theta = Double.NaN; aspectRatio = Double.NaN; } - spot.putFeature( SpotFitEllipseAnalyzerFactory.X0, x0 ); - spot.putFeature( SpotFitEllipseAnalyzerFactory.Y0, y0 ); - spot.putFeature( SpotFitEllipseAnalyzerFactory.MAJOR, major ); - spot.putFeature( SpotFitEllipseAnalyzerFactory.MINOR, minor ); - spot.putFeature( SpotFitEllipseAnalyzerFactory.THETA, theta ); - spot.putFeature( SpotFitEllipseAnalyzerFactory.ASPECTRATIO, aspectRatio ); + spot.putFeature( Spot2DFitEllipseAnalyzerFactory.X0, x0 ); + spot.putFeature( Spot2DFitEllipseAnalyzerFactory.Y0, y0 ); + spot.putFeature( Spot2DFitEllipseAnalyzerFactory.MAJOR, major ); + spot.putFeature( Spot2DFitEllipseAnalyzerFactory.MINOR, minor ); + spot.putFeature( Spot2DFitEllipseAnalyzerFactory.THETA, theta ); + spot.putFeature( Spot2DFitEllipseAnalyzerFactory.ASPECTRATIO, aspectRatio ); } /** @@ -112,17 +111,17 @@ public void process( final Spot spot ) * script * @author Michael Doube */ - private static double[] fitEllipse( final double[] x, final double[] y ) + private static double[] fitEllipse( final SpotRoi roi ) { - final int nPoints = x.length; - final double[] centroid = getCentroid( x, y ); - final double xC = centroid[ 0 ]; - final double yC = centroid[ 1 ]; + final double xC = roi.getDoublePosition( 0 ); + final double yC = roi.getDoublePosition( 1 ); + + final int nPoints = roi.nPoints(); final double[][] d1 = new double[ nPoints ][ 3 ]; for ( int i = 0; i < nPoints; i++ ) { - final double xixC = x[ i ] - xC; - final double yiyC = y[ i ] - yC; + final double xixC = roi.xr( i ); + final double yiyC = roi.yr( i ); d1[ i ][ 0 ] = xixC * xixC; d1[ i ][ 1 ] = xixC * yiyC; d1[ i ][ 2 ] = yiyC * yiyC; @@ -131,8 +130,8 @@ private static double[] fitEllipse( final double[] x, final double[] y ) final double[][] d2 = new double[ nPoints ][ 3 ]; for ( int i = 0; i < nPoints; i++ ) { - d2[ i ][ 0 ] = x[ i ] - xC; - d2[ i ][ 1 ] = y[ i ] - yC; + d2[ i ][ 0 ] = roi.xr( i ); + d2[ i ][ 1 ] = roi.yr( i ); d2[ i ][ 2 ] = 1; } final Matrix D2 = new Matrix( d2 ); @@ -182,11 +181,6 @@ private static double[] fitEllipse( final double[] x, final double[] y ) return A.getColumnPackedCopy(); } - private static double[] getCentroid( final double[] x, final double[] y ) - { - return new double[] { Util.average( x ), Util.average( y ) }; - } - /** * Convert to cartesian coordnates for the ellipse. Return [ x0 y0 a b theta * ]. We always have a > b. theta in radians measure the angle of the @@ -241,13 +235,12 @@ else if ( A < 0 ) } /** - * Computes the Moore–Penrose pseudoinverse using the SVD method. - * - * Modified version of the original implementation by Kim van der Linde. - * + * Computes the Moore–Penrose pseudoinverse using the SVD method. Modified + * version of the original implementation by Kim van der Linde. + * * @param x - * the input matrix - * @return the pseudoinverse of the input matrix + * the matrix. + * @return the pseudo-inverse as a new matrix. */ public static Matrix pinv( final Matrix x ) { diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotFitEllipseAnalyzerFactory.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DFitEllipseAnalyzerFactory.java similarity index 94% rename from src/main/java/fiji/plugin/trackmate/features/spot/SpotFitEllipseAnalyzerFactory.java rename to src/main/java/fiji/plugin/trackmate/features/spot/Spot2DFitEllipseAnalyzerFactory.java index 68015185c..9b3685525 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotFitEllipseAnalyzerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DFitEllipseAnalyzerFactory.java @@ -36,8 +36,8 @@ import net.imglib2.type.NativeType; import net.imglib2.type.numeric.RealType; -@Plugin( type = SpotMorphologyAnalyzerFactory.class ) -public class SpotFitEllipseAnalyzerFactory< T extends RealType< T > & NativeType< T > > implements SpotMorphologyAnalyzerFactory< T > +@Plugin( type = Spot2DMorphologyAnalyzerFactory.class ) +public class Spot2DFitEllipseAnalyzerFactory< T extends RealType< T > & NativeType< T > > implements Spot2DMorphologyAnalyzerFactory< T > { public static final String KEY = "Spot fit 2D ellipse"; @@ -94,7 +94,7 @@ public SpotAnalyzer< T > getAnalyzer( final ImgPlus< T > img, final int frame, f if ( channel != 0 ) return SpotAnalyzer.dummyAnalyzer(); - return new SpotFitEllipseAnalyzer<>( DetectionUtils.is2D( img ) ); + return new Spot2DFitEllipseAnalyzer<>( DetectionUtils.is2D( img ) ); } @Override diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotMorphologyAnalyzerFactory.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DMorphologyAnalyzerFactory.java similarity index 86% rename from src/main/java/fiji/plugin/trackmate/features/spot/SpotMorphologyAnalyzerFactory.java rename to src/main/java/fiji/plugin/trackmate/features/spot/Spot2DMorphologyAnalyzerFactory.java index fe0bb57d5..5305bf924 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotMorphologyAnalyzerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DMorphologyAnalyzerFactory.java @@ -26,9 +26,9 @@ /** * Special interface for spot analyzers that can compute feature values based on - * the contour of spots. + * the 2D contour of spots. * * @author Jean-Yves Tinevez - 2020 */ -public interface SpotMorphologyAnalyzerFactory< T extends RealType< T > & NativeType< T > > extends SpotAnalyzerFactoryBase< T > +public interface Spot2DMorphologyAnalyzerFactory< T extends RealType< T > & NativeType< T > > extends SpotAnalyzerFactoryBase< T > {} diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotShapeAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DShapeAnalyzer.java similarity index 67% rename from src/main/java/fiji/plugin/trackmate/features/spot/SpotShapeAnalyzer.java rename to src/main/java/fiji/plugin/trackmate/features/spot/Spot2DShapeAnalyzer.java index 09ecab5cc..3cd161a0e 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotShapeAnalyzer.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DShapeAnalyzer.java @@ -25,12 +25,12 @@ import fiji.plugin.trackmate.SpotRoi; import net.imglib2.type.numeric.RealType; -public class SpotShapeAnalyzer< T extends RealType< T > > extends AbstractSpotFeatureAnalyzer< T > +public class Spot2DShapeAnalyzer< T extends RealType< T > > extends AbstractSpotFeatureAnalyzer< T > { private final boolean is2D; - public SpotShapeAnalyzer( final boolean is2D ) + public Spot2DShapeAnalyzer( final boolean is2D ) { this.is2D = is2D; } @@ -44,12 +44,12 @@ public void process( final Spot spot ) if ( is2D ) { - final SpotRoi roi = spot.getRoi(); - if ( roi != null ) + if ( spot instanceof SpotRoi ) { + final SpotRoi roi = ( SpotRoi ) spot; area = roi.area(); perimeter = getLength( roi ); - final SpotRoi convexHull = ConvexHull.convexHull( roi ); + final SpotRoi convexHull = ConvexHull2D.convexHull( roi ); convexArea = convexHull.area(); } else @@ -71,33 +71,28 @@ public void process( final Spot spot ) final double solidity = area / convexArea; final double shapeIndex = ( area <= 0. ) ? Double.NaN : perimeter / Math.sqrt( area ); - spot.putFeature( SpotShapeAnalyzerFactory.AREA, area ); - spot.putFeature( SpotShapeAnalyzerFactory.PERIMETER, perimeter ); - spot.putFeature( SpotShapeAnalyzerFactory.CIRCULARITY, circularity ); - spot.putFeature( SpotShapeAnalyzerFactory.SOLIDITY, solidity ); - spot.putFeature( SpotShapeAnalyzerFactory.SHAPE_INDEX, shapeIndex ); + spot.putFeature( Spot2DShapeAnalyzerFactory.AREA, area ); + spot.putFeature( Spot2DShapeAnalyzerFactory.PERIMETER, perimeter ); + spot.putFeature( Spot2DShapeAnalyzerFactory.CIRCULARITY, circularity ); + spot.putFeature( Spot2DShapeAnalyzerFactory.SOLIDITY, solidity ); + spot.putFeature( Spot2DShapeAnalyzerFactory.SHAPE_INDEX, shapeIndex ); } private static final double getLength( final SpotRoi roi ) { - final double[] x = roi.x; - final double[] y = roi.y; - final int npoints = x.length; - if ( npoints < 2 ) + final int nPoints = roi.nPoints(); + if ( nPoints < 2 ) return 0; double length = 0; - for ( int i = 0; i < npoints - 1; i++ ) + int i; + int j; + for ( i = 0, j = nPoints - 1; i < nPoints; j = i++ ) { - final double dx = x[ i + 1 ] - x[ i ]; - final double dy = y[ i + 1 ] - y[ i ]; + final double dx = roi.x( i ) - roi.x( j ); + final double dy = roi.y( i ) - roi.y( j ); length += Math.sqrt( dx * dx + dy * dy ); } - - final double dx0 = x[ 0 ] - x[ npoints - 1 ]; - final double dy0 = y[ 0 ] - y[ npoints - 1 ]; - length += Math.sqrt( dx0 * dx0 + dy0 * dy0 ); - return length; } } diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotShapeAnalyzerFactory.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DShapeAnalyzerFactory.java similarity index 94% rename from src/main/java/fiji/plugin/trackmate/features/spot/SpotShapeAnalyzerFactory.java rename to src/main/java/fiji/plugin/trackmate/features/spot/Spot2DShapeAnalyzerFactory.java index 5a2348904..00be635b3 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotShapeAnalyzerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DShapeAnalyzerFactory.java @@ -36,8 +36,8 @@ import net.imglib2.type.NativeType; import net.imglib2.type.numeric.RealType; -@Plugin( type = SpotMorphologyAnalyzerFactory.class ) -public class SpotShapeAnalyzerFactory< T extends RealType< T > & NativeType< T > > implements SpotMorphologyAnalyzerFactory< T > +@Plugin( type = Spot2DMorphologyAnalyzerFactory.class ) +public class Spot2DShapeAnalyzerFactory< T extends RealType< T > & NativeType< T > > implements Spot2DMorphologyAnalyzerFactory< T > { public static final String KEY = "Spot 2D shape descriptors"; @@ -88,7 +88,7 @@ public SpotAnalyzer< T > getAnalyzer( final ImgPlus< T > img, final int frame, f if ( channel != 0 ) return SpotAnalyzer.dummyAnalyzer(); - return new SpotShapeAnalyzer<>( DetectionUtils.is2D( img ) ); + return new Spot2DShapeAnalyzer<>( DetectionUtils.is2D( img ) ); } @Override diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DFitEllipsoidAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DFitEllipsoidAnalyzer.java new file mode 100644 index 000000000..648af607a --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DFitEllipsoidAnalyzer.java @@ -0,0 +1,179 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.features.spot; + +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.ASPECTRATIO; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.ELLIPSOID_SHAPE; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MAJOR; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MAJOR_PHI; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MAJOR_THETA; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MEDIAN; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MEDIAN_PHI; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MEDIAN_THETA; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MINOR; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MINOR_PHI; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MINOR_THETA; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.SHAPE_CLASS_TOLERANCE; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.SHAPE_ELLIPSOID; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.SHAPE_OBLATE; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.SHAPE_PROLATE; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.SHAPE_SPHERE; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.X0; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.Y0; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.Z0; + +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import net.imglib2.RealLocalizable; +import net.imglib2.mesh.alg.EllipsoidFitter; +import net.imglib2.mesh.alg.EllipsoidFitter.Ellipsoid; +import net.imglib2.type.numeric.RealType; + +public class Spot3DFitEllipsoidAnalyzer< T extends RealType< T > > extends AbstractSpotFeatureAnalyzer< T > +{ + + private final boolean is3D; + + public Spot3DFitEllipsoidAnalyzer( final boolean is3D ) + { + this.is3D = is3D; + } + + @Override + public void process( final Spot spot ) + { + final double x0; + final double y0; + final double z0; + final double rA; + final double rB; + final double rC; + final double phiA; + final double thetaA; + final double phiB; + final double thetaB; + final double phiC; + final double thetaC; + final double aspectRatio; + final int shapeIndex; + + if ( is3D ) + { + if ( spot instanceof SpotMesh ) + { + final SpotMesh sm = ( SpotMesh ) spot; + final Ellipsoid fit = EllipsoidFitter.fit( sm.getMesh() ); + x0 = fit.center.getDoublePosition( 0 ); + y0 = fit.center.getDoublePosition( 1 ); + z0 = fit.center.getDoublePosition( 2 ); + rA = Math.abs( fit.r1 ); + rB = Math.abs( fit.r2 ); + rC = Math.abs( fit.r3 ); + aspectRatio = rA / rC; + final double drAB = ( rB - rA ) / rB; + final double drBC = ( rC - rB ) / rC; + if ( drAB < SHAPE_CLASS_TOLERANCE && drBC < SHAPE_CLASS_TOLERANCE ) + shapeIndex = SHAPE_SPHERE; + else if ( drBC < SHAPE_CLASS_TOLERANCE ) + shapeIndex = SHAPE_OBLATE; + else if ( drAB < SHAPE_CLASS_TOLERANCE ) + shapeIndex = SHAPE_PROLATE; + else + shapeIndex = SHAPE_ELLIPSOID; + + phiA = phi( fit.ev1 ); + phiB = phi( fit.ev2 ); + phiC = phi( fit.ev3 ); + thetaA = theta( fit.ev1 ); + thetaB = theta( fit.ev2 ); + thetaC = theta( fit.ev3 ); + + } + else + { + // Assume plain sphere. + x0 = 0.; + y0 = 0.; + z0 = 0.; + final double radius = spot.getFeature( Spot.RADIUS ); + rA = radius; + rB = radius; + rC = radius; + aspectRatio = 1.; + shapeIndex = SHAPE_ELLIPSOID; + + phiA = 0.; + phiB = 0.; + phiC = 0.; + thetaA = 0.; + thetaB = 0.; + thetaC = 0.; + } + } + else + { + // Undefined for 2D: default to NaN. + x0 = Double.NaN; + y0 = Double.NaN; + z0 = Double.NaN; + rA = Double.NaN; + rB = Double.NaN; + rC = Double.NaN; + aspectRatio = Double.NaN; + shapeIndex = SHAPE_ELLIPSOID; + + phiA = Double.NaN; + phiB = Double.NaN; + phiC = Double.NaN; + thetaA = Double.NaN; + thetaB = Double.NaN; + thetaC = Double.NaN; + } + spot.putFeature( X0, x0 ); + spot.putFeature( Y0, y0 ); + spot.putFeature( Z0, z0 ); + spot.putFeature( MINOR, rA ); + spot.putFeature( MEDIAN, rB ); + spot.putFeature( MAJOR, rC ); + spot.putFeature( MINOR_PHI, phiA ); + spot.putFeature( MEDIAN_PHI, phiB ); + spot.putFeature( MAJOR_PHI, phiC ); + spot.putFeature( MINOR_THETA, thetaA ); + spot.putFeature( MEDIAN_THETA, thetaB ); + spot.putFeature( MAJOR_THETA, thetaC ); + spot.putFeature( ASPECTRATIO, aspectRatio ); + spot.putFeature( ELLIPSOID_SHAPE, ( double ) shapeIndex ); + } + + private double theta( final RealLocalizable v ) + { + final double z = v.getDoublePosition( 2 ); + return Math.acos( z ); + } + + private static final double phi( final RealLocalizable v ) + { + final double x = v.getDoublePosition( 0 ); + final double y = v.getDoublePosition( 1 ); + return Math.atan2( y, x ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DFitEllipsoidAnalyzerFactory.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DFitEllipsoidAnalyzerFactory.java new file mode 100644 index 000000000..064c74b02 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DFitEllipsoidAnalyzerFactory.java @@ -0,0 +1,237 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.features.spot; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.ImageIcon; + +import org.scijava.plugin.Plugin; + +import fiji.plugin.trackmate.Dimension; +import fiji.plugin.trackmate.detection.DetectionUtils; +import net.imagej.ImgPlus; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; + +@Plugin( type = Spot3DMorphologyAnalyzerFactory.class ) +public class Spot3DFitEllipsoidAnalyzerFactory< T extends RealType< T > & NativeType< T > > implements Spot3DMorphologyAnalyzerFactory< T > +{ + + public static final String KEY = "Spot fit 3D ellipsoid"; + + public static final String X0 = "ELLIPSOID_X0"; + public static final String Y0 = "ELLIPSOID_Y0"; + public static final String Z0 = "ELLIPSOID_Z0"; + public static final String MAJOR = "ELLIPSOID_MAJOR_LENGTH"; + public static final String MEDIAN = "ELLIPSOID_MEDIAN_LENGTH"; + public static final String MINOR = "ELLIPSOID_MINOR_LENGTH"; + public static final String MAJOR_PHI = "ELLIPSOID_MAJOR_PHI"; + public static final String MAJOR_THETA = "ELLIPSOID_MAJOR_THETA"; + public static final String MEDIAN_PHI = "ELLIPSOID_MEDIAN_PHI"; + public static final String MEDIAN_THETA = "ELLIPSOID_MEDIAN_THETA"; + public static final String MINOR_PHI = "ELLIPSOID_MINOR_PHI"; + public static final String MINOR_THETA = "ELLIPSOID_MINOR_THETA"; + public static final String ASPECTRATIO = "ELLIPSOID_ASPECTRATIO"; + public static final String ELLIPSOID_SHAPE = "ELLIPSOID_SHAPE"; + + /** Denotes an ellipsoid with no particular shape. */ + public static final int SHAPE_ELLIPSOID = 0; + + /** + * Denotes an ellipsoid with the oblate shape. The two largest radii are + * roughly equal. Resembles a lentil. + */ + public static final int SHAPE_OBLATE = 1; + + /** + * Denotes an ellipsoid with the prolate shape. The two smallest radii are + * roughly equal. Resembles a rugby balloon. + */ + public static final int SHAPE_PROLATE = 2; + + /** + * Denotes an ellipsoid with the spherical shape. The three radii are + * roughly equal. Resembles a sphere + */ + public static final int SHAPE_SPHERE = 3; + + /** + * Tolerance in percentage on the radii values to be considered roughly + * equals. + * + * @see #SHAPE_ELLIPSOID + * @see #SHAPE_OBLATE + * @see #SHAPE_PROLATE + * @see #SHAPE_SPHERE + */ + public static final double SHAPE_CLASS_TOLERANCE = 0.1; + + private static final List< String > FEATURES = Arrays.asList( new String[] { + X0, Y0, Z0, + MAJOR, MEDIAN, MINOR, + MAJOR_PHI, MAJOR_THETA, + MEDIAN_PHI, MEDIAN_THETA, + MINOR_PHI, MINOR_THETA, + ASPECTRATIO, + ELLIPSOID_SHAPE } ); + private static final Map< String, String > FEATURE_SHORTNAMES = new HashMap< >(); + private static final Map< String, String > FEATURE_NAMES = new HashMap< >(); + private static final Map< String, Dimension > FEATURE_DIMENSIONS = new HashMap< >(); + private static final Map< String, Boolean > FEATURE_ISINTS = new HashMap< >(); + static + { + FEATURE_SHORTNAMES.put( X0, "El. x0" ); + FEATURE_SHORTNAMES.put( Y0, "El. y0" ); + FEATURE_SHORTNAMES.put( Z0, "El. z0" ); + FEATURE_SHORTNAMES.put( MAJOR, "El. long axis" ); + FEATURE_SHORTNAMES.put( MEDIAN, "El. med. axis" ); + FEATURE_SHORTNAMES.put( MINOR, "El. sh. axis" ); + FEATURE_SHORTNAMES.put( MAJOR_PHI, "El. l.a. phi" ); + FEATURE_SHORTNAMES.put( MEDIAN_PHI, "El. m.a. phi" ); + FEATURE_SHORTNAMES.put( MINOR_PHI, "El. s.a. phi" ); + FEATURE_SHORTNAMES.put( MAJOR_THETA, "El. l.a. theta" ); + FEATURE_SHORTNAMES.put( MEDIAN_THETA, "El. m.a. theta" ); + FEATURE_SHORTNAMES.put( MINOR_THETA, "El. s.a. theta" ); + FEATURE_SHORTNAMES.put( ASPECTRATIO, "El. a.r." ); + FEATURE_SHORTNAMES.put( ELLIPSOID_SHAPE, "El. shape" ); + + FEATURE_NAMES.put( X0, "Ellipsoid center x0" ); + FEATURE_NAMES.put( Y0, "Ellipsoid center y0" ); + FEATURE_NAMES.put( Z0, "Ellipsoid center z0" ); + FEATURE_NAMES.put( MAJOR, "Ellipsoid long axis" ); + FEATURE_NAMES.put( MEDIAN, "Ellipsoid long axis" ); + FEATURE_NAMES.put( MINOR, "Ellipsoid short axis" ); + FEATURE_NAMES.put( MAJOR_PHI, "Ellipsoid long axis phi" ); + FEATURE_NAMES.put( MEDIAN_PHI, "Ellipsoid long axis. phi" ); + FEATURE_NAMES.put( MINOR_PHI, "Ellipsoid short axis phi" ); + FEATURE_NAMES.put( MAJOR_THETA, "Ellipsoid long axis theta" ); + FEATURE_NAMES.put( MEDIAN_THETA, "Ellipsoid long axis theta" ); + FEATURE_NAMES.put( MINOR_THETA, "Ellipsoid short axis theta" ); + FEATURE_NAMES.put( ASPECTRATIO, "Ellipsoid aspect ratio" ); + FEATURE_NAMES.put( ELLIPSOID_SHAPE, "Ellipsoid shape class" ); + + FEATURE_DIMENSIONS.put( X0, Dimension.LENGTH ); + FEATURE_DIMENSIONS.put( Y0, Dimension.LENGTH ); + FEATURE_DIMENSIONS.put( Z0, Dimension.LENGTH ); + FEATURE_DIMENSIONS.put( MAJOR, Dimension.LENGTH ); + FEATURE_DIMENSIONS.put( MEDIAN, Dimension.LENGTH ); + FEATURE_DIMENSIONS.put( MINOR, Dimension.LENGTH ); + FEATURE_DIMENSIONS.put( MAJOR_PHI, Dimension.ANGLE ); + FEATURE_DIMENSIONS.put( MAJOR_THETA, Dimension.ANGLE ); + FEATURE_DIMENSIONS.put( MEDIAN_PHI, Dimension.ANGLE ); + FEATURE_DIMENSIONS.put( MEDIAN_THETA, Dimension.ANGLE ); + FEATURE_DIMENSIONS.put( MINOR_PHI, Dimension.ANGLE ); + FEATURE_DIMENSIONS.put( MINOR_THETA, Dimension.ANGLE ); + FEATURE_DIMENSIONS.put( ASPECTRATIO, Dimension.NONE ); + FEATURE_DIMENSIONS.put( ELLIPSOID_SHAPE, Dimension.NONE ); + + FEATURE_ISINTS.put( X0, Boolean.FALSE ); + FEATURE_ISINTS.put( Y0, Boolean.FALSE ); + FEATURE_ISINTS.put( Z0, Boolean.FALSE ); + FEATURE_ISINTS.put( MAJOR, Boolean.FALSE ); + FEATURE_ISINTS.put( MEDIAN, Boolean.FALSE ); + FEATURE_ISINTS.put( MINOR, Boolean.FALSE ); + FEATURE_ISINTS.put( MAJOR_PHI, Boolean.FALSE ); + FEATURE_ISINTS.put( MAJOR_THETA, Boolean.FALSE ); + FEATURE_ISINTS.put( MEDIAN_PHI, Boolean.FALSE ); + FEATURE_ISINTS.put( MEDIAN_THETA, Boolean.FALSE ); + FEATURE_ISINTS.put( MINOR_PHI, Boolean.FALSE ); + FEATURE_ISINTS.put( MINOR_THETA, Boolean.FALSE ); + FEATURE_ISINTS.put( ASPECTRATIO, Boolean.FALSE ); + FEATURE_ISINTS.put( ELLIPSOID_SHAPE, Boolean.TRUE ); + } + + + @Override + public SpotAnalyzer< T > getAnalyzer( final ImgPlus< T > img, final int frame, final int channel ) + { + // Don't run more than once. + if ( channel != 0 ) + return SpotAnalyzer.dummyAnalyzer(); + + return new Spot3DFitEllipsoidAnalyzer<>( !DetectionUtils.is2D( img ) ); + } + + @Override + public List< String > getFeatures() + { + return FEATURES; + } + + @Override + public Map< String, String > getFeatureShortNames() + { + return FEATURE_SHORTNAMES; + } + + @Override + public Map< String, String > getFeatureNames() + { + return FEATURE_NAMES; + } + + @Override + public Map< String, Dimension > getFeatureDimensions() + { + return FEATURE_DIMENSIONS; + } + + @Override + public Map< String, Boolean > getIsIntFeature() + { + return FEATURE_ISINTS; + } + + @Override + public boolean isManualFeature() + { + return false; + } + + @Override + public String getInfoText() + { + return null; + } + + @Override + public ImageIcon getIcon() + { + return null; + } + + @Override + public String getKey() + { + return KEY; + } + + @Override + public String getName() + { + return KEY; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/graph/StringFormater.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DMorphologyAnalyzerFactory.java similarity index 61% rename from src/main/java/fiji/plugin/trackmate/graph/StringFormater.java rename to src/main/java/fiji/plugin/trackmate/features/spot/Spot3DMorphologyAnalyzerFactory.java index 9a41935f4..e6fdfe20f 100644 --- a/src/main/java/fiji/plugin/trackmate/graph/StringFormater.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DMorphologyAnalyzerFactory.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -19,25 +19,16 @@ * . * #L% */ -package fiji.plugin.trackmate.graph; +package fiji.plugin.trackmate.features.spot; + +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; /** - * Interface for function that can build a human-readable string representation - * of an object - * - * @author JeanYves + * Special interface for spot analyzers that can compute feature values based on + * the 3D mesh building the 3D shape of spots. * + * @author Jean-Yves Tinevez - 2023 */ -public interface StringFormater< V > -{ - - /** - * Converts the given instance to a string representation. - * - * @param instance - * the instance to convert. - * @return the string representation. - */ - public String toString( V instance ); - -} +public interface Spot3DMorphologyAnalyzerFactory< T extends RealType< T > & NativeType< T > > extends SpotAnalyzerFactoryBase< T > +{} diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DShapeAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DShapeAnalyzer.java new file mode 100644 index 000000000..890bbb2ae --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DShapeAnalyzer.java @@ -0,0 +1,91 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.features.spot; + +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import net.imglib2.mesh.MeshStats; +import net.imglib2.mesh.alg.hull.ConvexHull; +import net.imglib2.mesh.impl.naive.NaiveDoubleMesh; +import net.imglib2.type.numeric.RealType; + +public class Spot3DShapeAnalyzer< T extends RealType< T > > extends AbstractSpotFeatureAnalyzer< T > +{ + + private final boolean is3D; + + public Spot3DShapeAnalyzer( final boolean is3D ) + { + this.is3D = is3D; + } + + @Override + public void process( final Spot spot ) + { + double volume; + double sa; + double solidity; + double convexity; + double sphericity; + if ( is3D ) + { + if ( spot instanceof SpotMesh ) + { + final SpotMesh sm = ( SpotMesh ) spot; + final NaiveDoubleMesh ch = ConvexHull.calculate( sm.getMesh() ); + volume = sm.volume(); + final double volumeCH = MeshStats.volume( ch ); + solidity = volume / volumeCH; + + sa = MeshStats.surfaceArea( sm.getMesh() ); + final double saCH = MeshStats.surfaceArea( ch ); + convexity = sa / saCH; + + final double sphereArea = Math.pow( Math.PI, 1. / 3. ) + * Math.pow( 6. * volume, 2. / 3. ); + sphericity = sphereArea / sa; + } + else + { + final double radius = spot.getFeature( Spot.RADIUS ); + volume = 4. / 3. * Math.PI * radius * radius * radius; + sa = 4. * Math.PI * radius * radius; + solidity = 1.; + convexity = 1.; + sphericity = 1.; + } + } + else + { + volume = Double.NaN; + sa = Double.NaN; + solidity = Double.NaN; + convexity = Double.NaN; + sphericity = Double.NaN; + } + spot.putFeature( Spot3DShapeAnalyzerFactory.VOLUME, volume ); + spot.putFeature( Spot3DShapeAnalyzerFactory.SURFACE_AREA, sa ); + spot.putFeature( Spot3DShapeAnalyzerFactory.SPHERICITY, sphericity ); + spot.putFeature( Spot3DShapeAnalyzerFactory.SOLIDITY, solidity ); + spot.putFeature( Spot3DShapeAnalyzerFactory.CONVEXITY, convexity ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DShapeAnalyzerFactory.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DShapeAnalyzerFactory.java new file mode 100644 index 000000000..228261fed --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DShapeAnalyzerFactory.java @@ -0,0 +1,153 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.features.spot; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.ImageIcon; + +import org.scijava.plugin.Plugin; + +import fiji.plugin.trackmate.Dimension; +import fiji.plugin.trackmate.detection.DetectionUtils; +import net.imagej.ImgPlus; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; + +@Plugin( type = Spot3DMorphologyAnalyzerFactory.class ) +public class Spot3DShapeAnalyzerFactory< T extends RealType< T > & NativeType< T > > implements Spot3DMorphologyAnalyzerFactory< T > +{ + + public static final String KEY = "Spot 3D shape descriptors"; + + public static final String VOLUME = "VOLUME"; + public static final String SURFACE_AREA = "SURFACE_AREA"; + public static final String SPHERICITY = "SPHERICITY"; + public static final String SOLIDITY = "SOLIDITY"; + public static final String CONVEXITY = "CONVEXITY"; + + private static final List< String > FEATURES = Arrays.asList( new String[] { + VOLUME, SURFACE_AREA, SPHERICITY, SOLIDITY, CONVEXITY } ); + private static final Map< String, String > FEATURE_SHORTNAMES = new HashMap< >(); + private static final Map< String, String > FEATURE_NAMES = new HashMap< >(); + private static final Map< String, Dimension > FEATURE_DIMENSIONS = new HashMap< >(); + private static final Map< String, Boolean > FEATURE_ISINTS = new HashMap< >(); + static + { + FEATURE_SHORTNAMES.put( VOLUME, "Volume" ); + FEATURE_SHORTNAMES.put( SURFACE_AREA, "Surf. area" ); + FEATURE_SHORTNAMES.put( SPHERICITY, "Sphericity" ); + FEATURE_SHORTNAMES.put( CONVEXITY, "Conv." ); + FEATURE_SHORTNAMES.put( SOLIDITY, "Solidity" ); + + FEATURE_NAMES.put( VOLUME, "Volume" ); + FEATURE_NAMES.put( SURFACE_AREA, "Surface area" ); + FEATURE_NAMES.put( SPHERICITY, "Sphericity" ); + FEATURE_NAMES.put( CONVEXITY, "Convexity" ); + FEATURE_NAMES.put( SOLIDITY, "Solidity" ); + + FEATURE_DIMENSIONS.put( SURFACE_AREA, Dimension.AREA ); + FEATURE_DIMENSIONS.put( VOLUME, Dimension.VOLUME ); + FEATURE_DIMENSIONS.put( SPHERICITY, Dimension.NONE ); + FEATURE_DIMENSIONS.put( CONVEXITY, Dimension.NONE ); + FEATURE_DIMENSIONS.put( SOLIDITY, Dimension.NONE ); + + FEATURE_ISINTS.put( VOLUME, Boolean.FALSE ); + FEATURE_ISINTS.put( SURFACE_AREA, Boolean.FALSE ); + FEATURE_ISINTS.put( SPHERICITY, Boolean.FALSE ); + FEATURE_ISINTS.put( CONVEXITY, Boolean.FALSE ); + FEATURE_ISINTS.put( SOLIDITY, Boolean.FALSE ); + } + + @Override + public SpotAnalyzer< T > getAnalyzer( final ImgPlus< T > img, final int frame, final int channel ) + { + // Don't run more than once. + if ( channel != 0 ) + return SpotAnalyzer.dummyAnalyzer(); + + return new Spot3DShapeAnalyzer<>( !DetectionUtils.is2D( img ) ); + } + + @Override + public List< String > getFeatures() + { + return FEATURES; + } + + @Override + public Map< String, String > getFeatureShortNames() + { + return FEATURE_SHORTNAMES; + } + + @Override + public Map< String, String > getFeatureNames() + { + return FEATURE_NAMES; + } + + @Override + public Map< String, Dimension > getFeatureDimensions() + { + return FEATURE_DIMENSIONS; + } + + @Override + public Map< String, Boolean > getIsIntFeature() + { + return FEATURE_ISINTS; + } + + @Override + public boolean isManualFeature() + { + return false; + } + + @Override + public String getInfoText() + { + return null; + } + + @Override + public ImageIcon getIcon() + { + return null; + } + + @Override + public String getKey() + { + return KEY; + } + + @Override + public String getName() + { + return KEY; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotAnalyzerFactoryBase.java b/src/main/java/fiji/plugin/trackmate/features/spot/SpotAnalyzerFactoryBase.java index 2b5836a43..80004d10e 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotAnalyzerFactoryBase.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/SpotAnalyzerFactoryBase.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -30,7 +30,7 @@ * Top-level interface for spot analyzer factories, both classical and * morphological. They are separated so that clients can deal separately with * classical spots (spheres) and spots with ROIs. - * + * * @author Jean-Yves Tinevez - 2020 */ public interface SpotAnalyzerFactoryBase< T extends RealType< T > & NativeType< T > > extends FeatureAnalyzer @@ -53,9 +53,7 @@ public interface SpotAnalyzerFactoryBase< T extends RealType< T > & NativeType< * the target frame to operate on. * @param channel * the target channel to operate on. - * - * @return a {@link SpotAnalyzer} ready to operate on the given frame and - * channel. + * @return a new spot analyzer. */ public SpotAnalyzer< T > getAnalyzer( ImgPlus< T > img, int frame, int channel ); diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotContrastAndSNRAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/spot/SpotContrastAndSNRAnalyzer.java index c385b1c71..fe95c1cfc 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotContrastAndSNRAnalyzer.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/SpotContrastAndSNRAnalyzer.java @@ -29,11 +29,6 @@ import static fiji.plugin.trackmate.features.spot.SpotIntensityMultiCAnalyzerFactory.makeFeatureKey; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.SpotRoi; -import fiji.plugin.trackmate.detection.DetectionUtils; -import fiji.plugin.trackmate.util.SpotNeighborhood; -import fiji.plugin.trackmate.util.SpotNeighborhoodCursor; -import fiji.plugin.trackmate.util.SpotUtil; import net.imagej.ImgPlus; import net.imglib2.IterableInterval; import net.imglib2.type.numeric.RealType; @@ -54,7 +49,7 @@ * Important: this analyzer relies on some results provided by the * {@link SpotIntensityMultiCAnalyzer} analyzer. Thus, it must be run * after it. - * + * * @author Jean-Yves Tinevez, 2011 - 2012. Revised December 2020. */ public class SpotContrastAndSNRAnalyzer< T extends RealType< T > > extends AbstractSpotFeatureAnalyzer< T > @@ -72,7 +67,7 @@ public class SpotContrastAndSNRAnalyzer< T extends RealType< T > > extends Abstr /** * Instantiates an analyzer for contrast and SNR. - * + * * @param img * the 2D or 3D image of the desired time-point and channel to * operate on, @@ -99,76 +94,33 @@ public final void process( final Spot spot ) final double radius = spot.getFeature( Spot.RADIUS ); final double outterRadius = 2. * radius; - // Operate on ROI only if we have one and the image is 2D. - final double meanOut; - final SpotRoi roi = spot.getRoi(); - if ( null != roi && DetectionUtils.is2D( img ) ) - { - final double alpha = outterRadius / radius; - final SpotRoi outterRoi = roi.copy(); - outterRoi.scale( alpha ); - final IterableInterval< T > neighborhood = SpotUtil.iterable( outterRoi, spot, img ); - double totalSum = 0.; - int nTotal = 0; // Total number of non-NaN pixels - - // Iterate over the big ROI. - for ( final T t : neighborhood ) - { - final double val = t.getRealDouble(); - if ( Double.isNaN( val ) ) - continue; - nTotal++; - totalSum += val; - } - - // Sum intensity inside (over non-NaN pixels). - final String sumFeature = makeFeatureKey( TOTAL_INTENSITY, channel ); - final double innerSum = spot.getFeature( sumFeature ); - - // Compute number of non-NaN pixels in the inner roi. - final int nInner = ( int ) ( innerSum / meanIn ); - - // Total number of non-NaN pixels in the outer roi. - final int nOut = nTotal - nInner; - - final double outterSum = totalSum - innerSum; - meanOut = outterSum / nOut; - } - else + final double alpha = outterRadius / radius; + final Spot outterRoi = spot.copy(); + outterRoi.scale( alpha ); + final IterableInterval< T > neighborhood = outterRoi.iterable( img ); + double totalSum = 0.; + int nTotal = 0; + for ( final T t : neighborhood ) { - // Otherwise default to circle / sphere. - final Spot largeSpot = new Spot( spot ); - largeSpot.putFeature( Spot.RADIUS, outterRadius ); - final SpotNeighborhood< T > neighborhood = new SpotNeighborhood<>( largeSpot, img ); - if ( neighborhood.size() <= 1 ) - { - spot.putFeature( makeFeatureKey( CONTRAST, channel ), Double.NaN ); - spot.putFeature( makeFeatureKey( SNR, channel ), Double.NaN ); - return; - } - - final double radius2 = radius * radius; - int nOut = 0; // Outer number of non-NaN pixels. - double sumOut = 0; - - // Compute mean in the outer ring - final SpotNeighborhoodCursor< T > cursor = neighborhood.cursor(); - while ( cursor.hasNext() ) - { - cursor.fwd(); - final double dist2 = cursor.getDistanceSquared(); - if ( dist2 > radius2 ) - { - final double val = cursor.get().getRealDouble(); - if ( Double.isNaN( val ) ) - continue; - nOut++; - sumOut += val; - } - } - meanOut = sumOut / nOut; + final double val = t.getRealDouble(); + if ( Double.isNaN( val ) ) + continue; + nTotal++; + totalSum += val; } + final String sumFeature = makeFeatureKey( TOTAL_INTENSITY, channel ); + final double innerSum = spot.getFeature( sumFeature ); + + // Compute number of non-NaN pixels in the inner roi. + final int nInner = ( int ) ( innerSum / meanIn ); + + // Total number of non-NaN pixels in the outer roi. + final int nOut = nTotal - nInner; + + final double outterSum = totalSum - innerSum; + final double meanOut = outterSum / nOut; + // Compute contrast final double contrast = ( meanIn - meanOut ) / ( meanIn + meanOut ); @@ -179,4 +131,3 @@ public final void process( final Spot spot ) spot.putFeature( makeFeatureKey( SNR, channel ), snr ); } } - diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotIntensityMultiCAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/spot/SpotIntensityMultiCAnalyzer.java index d3716d8a9..ea3e995bb 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotIntensityMultiCAnalyzer.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/SpotIntensityMultiCAnalyzer.java @@ -31,7 +31,6 @@ import org.scijava.util.DoubleArray; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.util.SpotUtil; import fiji.plugin.trackmate.util.TMUtils; import net.imagej.ImgPlus; import net.imglib2.IterableInterval; @@ -54,7 +53,7 @@ public SpotIntensityMultiCAnalyzer( final ImgPlus< T > imgCT, final int channel @Override public void process( final Spot spot ) { - final IterableInterval< T > neighborhood = SpotUtil.iterable( spot, imgCT ); + final IterableInterval< T > neighborhood = spot.iterable( imgCT ); final DoubleArray intensities = new DoubleArray(); for ( final T pixel : neighborhood ) diff --git a/src/main/java/fiji/plugin/trackmate/features/track/TrackAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/track/TrackAnalyzer.java index 3758e88c3..6e207a605 100644 --- a/src/main/java/fiji/plugin/trackmate/features/track/TrackAnalyzer.java +++ b/src/main/java/fiji/plugin/trackmate/features/track/TrackAnalyzer.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -80,8 +80,8 @@ public interface TrackAnalyzer extends Benchmark, FeatureAnalyzer, MultiThreaded *

* Example of a non-local track feature: the rank of the track sorted by its * number of spots, compared to other tracks. - * - * @return true if this is a local track analyzer. + * + * @return whether this analyzer is a local analyzer. */ public boolean isLocal(); diff --git a/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java b/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java index 5de140303..5b4ba37b6 100644 --- a/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java +++ b/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java @@ -2,18 +2,18 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * 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 * . @@ -32,6 +32,7 @@ import java.util.TreeSet; import java.util.function.Supplier; +import org.jgrapht.alg.util.NeighborCache; import org.jgrapht.graph.DefaultWeightedEdge; import org.jgrapht.graph.SimpleDirectedWeightedGraph; import org.jgrapht.graph.SimpleWeightedGraph; @@ -48,13 +49,13 @@ public class GraphUtils * * @param directedGraph * the {@link SimpleDirectedWeightedGraph} to be converted + * @return a {@link SimpleWeightedGraph} with the same vertices and edges as + * the input graph, but with undirected edges and the maximum weight + * between any two vertices * @param * the vertex type. * @param * the edge type. - * @return a {@link SimpleWeightedGraph} with the same vertices and edges as - * the input graph, but with undirected edges and the maximum weight - * between any two vertices */ public static < V, E > SimpleWeightedGraph< V, E > convertToSimpleWeightedGraph( final SimpleDirectedWeightedGraph< V, E > directedGraph ) { @@ -88,12 +89,13 @@ public static < V, E > SimpleWeightedGraph< V, E > convertToSimpleWeightedGraph( } /** - * Returns a pretty-print string representation of a {@link TrackModel}, as - * long it is a tree (each spot must not have more than one predecessor). + * Pretty-prints a model. * * @param model - * the track model to represent as a string. - * @return a string representation of the given track model. + * the model. + * @return a pretty-print string representation of a {@link TrackModel}, as + * long it is a tree (each spot must not have more than one + * predecessor). * @throws IllegalArgumentException * if the given graph is not a tree. */ @@ -425,4 +427,25 @@ private static char[] makeChars( final int width, final char c ) Arrays.fill( chars, c ); return chars; } + + /** + * Returns the siblings of a spot. That is: all the spots that have the same + * predecessor. + * + * @param cache + * a neighbor cache. + * @param spot + * the spot to inspect. + * @return a new set made of the spot siblings. Includes the spot. + */ + public static final Set< Spot > getSibblings( final NeighborCache< Spot, DefaultWeightedEdge > cache, final Spot spot ) + { + final HashSet< Spot > sibblings = new HashSet<>(); + final Set< Spot > predecessors = cache.predecessorsOf( spot ); + for ( final Spot predecessor : predecessors ) + sibblings.addAll( cache.successorsOf( predecessor ) ); + + return sibblings; + } + } diff --git a/src/main/java/fiji/plugin/trackmate/graph/SortedDepthFirstIterator.java b/src/main/java/fiji/plugin/trackmate/graph/SortedDepthFirstIterator.java index d99fb901c..c97cb348d 100644 --- a/src/main/java/fiji/plugin/trackmate/graph/SortedDepthFirstIterator.java +++ b/src/main/java/fiji/plugin/trackmate/graph/SortedDepthFirstIterator.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -133,7 +133,8 @@ private static enum VisitColor * @param startVertex * the vertex iteration to be started. * @param comparator - * the comparator to sort the vertices when branching. + * used to compare the several children of a vertex, and + * specifies in what order they are iterated. * @throws IllegalArgumentException * if g==null or does not contain * startVertex @@ -277,11 +278,8 @@ private static < V, E > Specifics< V, E > createGraphSpecifics( final Graph< V, return new UndirectedSpecifics<>( g ); } - /** + /* * This is where we add the multiple children in proper sorted order. - * - * @param vertex - * the vertex whose unseen children to add. */ protected void addUnseenChildrenOf( final V vertex ) { @@ -356,29 +354,12 @@ private boolean isConnectedComponentExhausted() } } - /** - * Executed when we encounter a vertex for the first time. - * - * @param vertex - * the vertex encountered. - * @param edge - * the edge via which we encountered it. - */ protected void encounterVertex( final V vertex, final E edge ) { seen.put( vertex, VisitColor.WHITE ); stack.addLast( vertex ); } - /** - * Executed when we encounter a vertex that has already been seen, but is - * still WHITE (meaning it is on the stack). - * - * @param vertex - * the vertex encountered again. - * @param edge - * the edge via which we encountered it. - */ protected void encounterVertexAgain( final V vertex, final E edge ) { final VisitColor color = seen.get( vertex ); diff --git a/src/main/java/fiji/plugin/trackmate/graph/TimeDirectedDepthFirstIterator.java b/src/main/java/fiji/plugin/trackmate/graph/TimeDirectedDepthFirstIterator.java index 68a42a757..eebdcb391 100644 --- a/src/main/java/fiji/plugin/trackmate/graph/TimeDirectedDepthFirstIterator.java +++ b/src/main/java/fiji/plugin/trackmate/graph/TimeDirectedDepthFirstIterator.java @@ -24,47 +24,47 @@ */ package fiji.plugin.trackmate.graph; -import fiji.plugin.trackmate.Spot; - import org.jgrapht.Graph; import org.jgrapht.Graphs; import org.jgrapht.graph.DefaultWeightedEdge; +import fiji.plugin.trackmate.Spot; + public class TimeDirectedDepthFirstIterator extends SortedDepthFirstIterator< Spot, DefaultWeightedEdge > { - public TimeDirectedDepthFirstIterator( Graph< Spot, DefaultWeightedEdge > g, Spot startVertex ) + private final boolean reversed; + + public TimeDirectedDepthFirstIterator( final Graph< Spot, DefaultWeightedEdge > g, final Spot startVertex ) + { + this( g, startVertex, false ); + } + + public TimeDirectedDepthFirstIterator( final Graph< Spot, DefaultWeightedEdge > g, final Spot startVertex, final boolean reversed ) { super( g, startVertex, null ); + this.reversed = reversed; } @Override - protected void addUnseenChildrenOf( Spot vertex ) + protected void addUnseenChildrenOf( final Spot vertex ) { - int ts = vertex.getFeature( Spot.FRAME ).intValue(); - for ( DefaultWeightedEdge edge : specifics.edgesOf( vertex ) ) + final int ts = vertex.getFeature( Spot.FRAME ).intValue(); + for ( final DefaultWeightedEdge edge : specifics.edgesOf( vertex ) ) { if ( nListeners != 0 ) - { fireEdgeTraversed( createEdgeTraversalEvent( edge ) ); - } - Spot oppositeV = Graphs.getOppositeVertex( graph, edge, vertex ); - int tt = oppositeV.getFeature( Spot.FRAME ).intValue(); - if ( tt <= ts ) - { + final Spot oppositeV = Graphs.getOppositeVertex( graph, edge, vertex ); + final int tt = oppositeV.getFeature( Spot.FRAME ).intValue(); + if ( reversed ? tt >= ts : tt <= ts ) continue; - } if ( seen.containsKey( oppositeV ) ) - { encounterVertexAgain( oppositeV, edge ); - } else - { encounterVertex( oppositeV, edge ); - } } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/GuiModel.java b/src/main/java/fiji/plugin/trackmate/gui/GuiModel.java new file mode 100644 index 000000000..e60042f5c --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/GuiModel.java @@ -0,0 +1,266 @@ +package fiji.plugin.trackmate.gui; + +import static fiji.plugin.trackmate.gui.editor.labkit.component.TMLabKitFrame.EDITOR_KEYMAP_HOME; + +import org.scijava.object.ObjectService; +import org.scijava.ui.behaviour.KeyPressedManager; +import org.scijava.ui.behaviour.util.Actions; + +import bdv.ui.appearance.AppearanceManager; +import bdv.ui.keymap.Keymap; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsManager; +import fiji.plugin.trackmate.gui.editor.labkit.component.EditorKeymapManager; +import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.visualization.hyperstack.behaviours.semiautotracking.SemiAutoTrackingParams; +import fiji.plugin.trackmate.visualization.hyperstack.behaviours.semiautotracking.SemiAutoTrackingParamsIO; +import fiji.plugin.trackmate.visualization.bvv.BVVKeymapManager; +import fiji.plugin.trackmate.visualization.ui.KeyConfigContexts; +import fiji.plugin.trackmate.visualization.ui.TrackMateActions; +import fiji.plugin.trackmate.visualization.ui.TrackMateKeymapManager; +import ij.ImagePlus; +import ij.Prefs; + +/** + * GUI model for TrackMate views. + */ +public class GuiModel +{ + + private final Model model; + + private final SelectionModel selectionModel; + + private final DisplaySettings displaySettings; + + private final SemiAutoTrackingParams semiAutoTrackingparams; + + private final Actions globalActions; + + private final KeyPressedManager keyPressedManager; + + private final TrackMateKeymapManager keymapManager; + + private final BVVKeymapManager bvvKeymapManager; + + private final EditorKeymapManager editorKeymapManager; + + private final AppearanceManager appearanceManager; + + private final Settings settings; + + private final TrackMate trackmate; + + private final WindowManager windowManager; + + private final DisplaySettingsManager dsManager; + + /** + * Creates a new GuiModel. + * + * @param model + * the data model. + * @param settings + * the settings objects. Viewer that can display an image will + * use the {@link Settings#imp} field to do so. + * @param displaySettings + * the display settings to use for the views. + */ + public GuiModel( final Model model, final Settings settings, final DisplaySettings displaySettings ) + { + this.model = model; + this.settings = settings; + this.selectionModel = new SelectionModel( model ); + this.displaySettings = displaySettings; + this.trackmate = createTrackMate( model, settings ); + this.semiAutoTrackingparams = createSemiAutoTrackingParams(); + + // Keymap and actions + this.editorKeymapManager = new EditorKeymapManager(); + this.bvvKeymapManager = new BVVKeymapManager(); + this.appearanceManager = new AppearanceManager( EDITOR_KEYMAP_HOME ); + this.keyPressedManager = new KeyPressedManager(); + this.keymapManager = new TrackMateKeymapManager(); + final Keymap keymap = keymapManager.getForwardSelectedKeymap(); + keymap.updateListeners().add( () -> getGlobalActions().updateKeyConfig( keymap.getConfig() ) ); + this.globalActions = new Actions( keymapManager.getForwardSelectedKeymap().getConfig(), KeyConfigContexts.TRACKMATE ); + TrackMateActions.install( globalActions, model, selectionModel ); + + // Other managers + this.dsManager = new DisplaySettingsManager( displaySettings, true ); + + // Window manager + this.windowManager = new WindowManager( this ); + } + + /** + * Creates a new GuiModel with default display settings. + * + * @param model + * the data model. + * @param settings + * the settings objects. Viewer that can display an image will + * use the {@link Settings#imp} field to do so. + */ + public GuiModel( final Model model, final Settings settings ) + { + this( model, settings, DisplaySettings.defaultStyle().copy() ); + } + + /** + * Creates a new GuiModel with default settings and display settings, and + * without an image. + * + * @param model + * the data model. + */ + public GuiModel( final Model model ) + { + this( model, new Settings() ); + } + + /** + * Creates a new GuiModel with default settings, and without an image. + * + * @param model + * the data model. + * @param displaySettings + * the display settings to use for the views. + */ + public GuiModel( final Model model, final DisplaySettings displaySettings ) + { + this( model, new Settings(), displaySettings ); + } + + /** + * Creates a new GuiModel with default display settings, and default + * settings set to use the given image. + * + * @param model + * the data model. + * @param imp + * the image to use in the settings. + */ + public GuiModel( final Model model, final ImagePlus imp ) + { + this( model, new Settings( imp ) ); + } + + /** + * Hook for subclassers:
+ * Creates the TrackMate instance that will be controlled in the GUI. + * + * @param model + * the model to create the TrackMate instance with. + * @param settings + * the settings to create the TrackMate instance with. + * @return a new {@link TrackMate} instance. + */ + protected TrackMate createTrackMate( final Model model, final Settings settings ) + { + /* + * Since we are now sure that we will be working on this model with this + * settings, we need to pass to the model the units from the settings. + */ + final String spaceUnits = settings.imp.getCalibration().getXUnit(); + final String timeUnits = settings.imp.getCalibration().getTimeUnit(); + model.setPhysicalUnits( spaceUnits, timeUnits ); + + final TrackMate trackmate = new TrackMate( model, settings ); + final ObjectService objectService = TMUtils.getContext().service( ObjectService.class ); + if ( objectService != null ) + objectService.addObject( trackmate ); + + // Set the num of threads from IJ prefs. + trackmate.setNumThreads( Prefs.getThreads() ); + + return trackmate; + } + + protected SemiAutoTrackingParams createSemiAutoTrackingParams() + { + return SemiAutoTrackingParamsIO.readPrefs(); + } + + /** + * Actions that operates on the whole TrackMate session. + *

+ * For instance, saving, importing, creating a new view, showing the + * preference window, etc. + * + * @return the global actions. + */ + public Actions getGlobalActions() + { + return globalActions; + } + + public KeyPressedManager getKeyPressedManager() + { + return keyPressedManager; + } + + public TrackMateKeymapManager getKeymapManager() + { + return keymapManager; + } + + public EditorKeymapManager getEditorKeymapManager() + { + return editorKeymapManager; + } + + public BVVKeymapManager getBvvKeymapManager() + { + return bvvKeymapManager; + } + + public Model getModel() + { + return model; + } + + public Settings getSettings() + { + return settings; + } + + public SelectionModel getSelectionModel() + { + return selectionModel; + } + + public DisplaySettings getDisplaySettings() + { + return displaySettings; + } + + public TrackMate getTrackMate() + { + return trackmate; + } + + public WindowManager getWindowManager() + { + return windowManager; + } + + public AppearanceManager getAppearanceManager() + { + return appearanceManager; + } + + public SemiAutoTrackingParams getSemiAutoTrackingParams() + { + return semiAutoTrackingparams; + } + + public DisplaySettingsManager getDisplaySettingsManager() + { + return dsManager; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java b/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java index fa9608cb1..f19dd537b 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java +++ b/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java @@ -2,18 +2,18 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * 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 * . @@ -117,7 +117,7 @@ public static Color textColorForBackground( final Color backgroundColor ) } /** - * Distance between two colors in CIELab space. + * Distance between two colors. *

* Adapted from * https://stackoverflow.com/questions/9018016/how-to-compare-two-colors-for-similarity-difference @@ -178,12 +178,12 @@ public static final float[] toCIELab( final Color color ) } /** - * Positions a JFrame more or less cleverly next a {@link Component}. - * + * Positions a window more or less cleverly next a {@link Component}. + * * @param gui * the window to position. * @param component - * the component to position next to. + * the component to position the window with respect to. */ public static void positionWindow( final Window gui, final Component component ) { diff --git a/src/main/java/fiji/plugin/trackmate/gui/Icons.java b/src/main/java/fiji/plugin/trackmate/gui/Icons.java index e29747c2c..0c1d8e5e3 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/Icons.java +++ b/src/main/java/fiji/plugin/trackmate/gui/Icons.java @@ -41,6 +41,8 @@ public class Icons public static final ImageIcon TRACKMATE_ICON_16x16; + public static final ImageIcon TRACKMATE_ICON_64x64; + public static final ImageIcon TRACK_SCHEME_ICON_16x16; public static final ImageIcon SPOT_ICON_64x64; @@ -81,6 +83,10 @@ public class Icons final Image image7 = EDGE_ICON.getImage(); final Image newimg7 = image7.getScaledInstance( 16, 16, java.awt.Image.SCALE_SMOOTH ); EDGE_ICON_16x16 = new ImageIcon( newimg7 ); + + final Image image8 = TRACKMATE_ICON.getImage(); + final Image newimg8 = image8.getScaledInstance( 64, 64, java.awt.Image.SCALE_SMOOTH ); + TRACKMATE_ICON_64x64 = new ImageIcon( newimg8 ); } public static final ImageIcon SPOT_ICON_16x16 = new ImageIcon( Icons.class.getResource( "images/spot_icon_16x16.png" ) ); @@ -207,4 +213,9 @@ public class Icons public static final ImageIcon SEGMENTATION_EDITOR_ICON_64x64 = new ImageIcon( Icons.class.getResource( "images/Segmentation-editor-logo_v2-64px.png" ) ); + public static final ImageIcon BULLET_GREEN_ICON = new ImageIcon( Icons.class.getResource( "images/bullet_green.png" ) ); + + public static final ImageIcon QUESTION_ICON = new ImageIcon( Icons.class.getResource( "images/help.png" ) ); + + public static final ImageIcon BVV_ICON = new ImageIcon( Icons.class.getResource( "images/TrackMateBVV-logo-16x16.png" ) ); } diff --git a/src/main/java/fiji/plugin/trackmate/gui/WindowManager.java b/src/main/java/fiji/plugin/trackmate/gui/WindowManager.java new file mode 100644 index 000000000..343f4dafd --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/WindowManager.java @@ -0,0 +1,207 @@ +package fiji.plugin.trackmate.gui; + +import static fiji.plugin.trackmate.visualization.ui.KeyConfigContexts.ALL_SPOTS_TABLE; +import static fiji.plugin.trackmate.visualization.ui.KeyConfigContexts.HYPERSTACK_DISPLAYER; +import static fiji.plugin.trackmate.visualization.ui.KeyConfigContexts.KEY_CONFIG_SCOPE; +import static fiji.plugin.trackmate.visualization.ui.KeyConfigContexts.TRACKMATE; +import static fiji.plugin.trackmate.visualization.ui.KeyConfigContexts.TRACKSCHEME; +import static fiji.plugin.trackmate.visualization.ui.KeyConfigContexts.TRACK_TABLE; + +import java.awt.Window; +import java.awt.event.WindowEvent; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.scijava.plugin.Plugin; +import org.scijava.ui.behaviour.io.gui.CommandDescriptionProvider; +import org.scijava.ui.behaviour.io.gui.CommandDescriptions; +import org.scijava.ui.behaviour.util.Actions; + +import bdv.BigDataViewerActions; +import bdv.tools.CloseWindowActions; +import bdv.tools.PreferencesDialog; +import bdv.ui.appearance.AppearanceSettingsPage; +import bdv.ui.keymap.Keymap; +import bdv.ui.keymap.KeymapSettingsPage; +import bdv.util.InvokeOnEDT; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsConfigPage; +import fiji.plugin.trackmate.gui.editor.LabkitLauncher; +import fiji.plugin.trackmate.gui.editor.labkit.component.TMLabKitFrame; +import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.visualization.TrackMateModelView; +import fiji.plugin.trackmate.visualization.bvv.TrackMateBVV; +import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; +import fiji.plugin.trackmate.visualization.hyperstack.behaviours.semiautotracking.SpotEditToolSettingsPage; +import fiji.plugin.trackmate.visualization.table.AllSpotsTableView; +import fiji.plugin.trackmate.visualization.table.TrackTableView; +import fiji.plugin.trackmate.visualization.trackscheme.SpotImageUpdater; +import fiji.plugin.trackmate.visualization.trackscheme.TrackScheme; +import fiji.plugin.trackmate.visualization.ui.TrackMateKeymapManager; +import ij.ImagePlus; + +public class WindowManager +{ + + private final GuiModel guiModel; + + private final List< TrackMateModelView > views = new ArrayList<>(); + + private final List< Window > windows = new ArrayList<>(); + + private HyperStackDisplayer mainView; + + public WindowManager( final GuiModel guiModel ) + { + this.guiModel = guiModel; + + // Unwrap + final TrackMateKeymapManager keymapManager = guiModel.getKeymapManager(); + final Keymap keymap = keymapManager.getForwardSelectedKeymap(); + final Actions globalActions = guiModel.getGlobalActions(); + + // Preferences dialog + final PreferencesDialog preferencesDialog = new PreferencesDialog( null, keymap, + new String[] { TRACKMATE, HYPERSTACK_DISPLAYER, TRACKSCHEME, ALL_SPOTS_TABLE, TRACK_TABLE } ); + + preferencesDialog.setTitle( "TrackMate Preferences" ); + preferencesDialog.setLocationRelativeTo( null ); + BigDataViewerActions.toggleDialogAction( globalActions, preferencesDialog, BigDataViewerActions.PREFERENCES_DIALOG, BigDataViewerActions.PREFERENCES_DIALOG_KEYS ); + preferencesDialog.addPage( new DisplaySettingsConfigPage( "Display settings", guiModel.getDisplaySettingsManager() ) ); + preferencesDialog.addPage( new KeymapSettingsPage( "Global keymap", keymapManager, keymapManager.getCommandDescriptions() ) ); + preferencesDialog.addPage( new KeymapSettingsPage( "Spot editor keymap", guiModel.getEditorKeymapManager(), guiModel.getEditorKeymapManager().getCommandDescriptions() ) ); + preferencesDialog.addPage( new KeymapSettingsPage( "3D View keymap", guiModel.getBvvKeymapManager(), guiModel.getBvvKeymapManager().getCommandDescriptions() ) ); + preferencesDialog.addPage( new SpotEditToolSettingsPage( "Semi-auto tracking", guiModel.getSemiAutoTrackingParams() ) ); + preferencesDialog.addPage( new AppearanceSettingsPage( "BDVs appearance", guiModel.getAppearanceManager() ) ); + } + + public HyperStackDisplayer createHyperStackDisplayer() + { + if ( mainView != null ) + throw new IllegalStateException( "There can be only one main view." ); + this.mainView = new HyperStackDisplayer( guiModel ); + mainView.render(); + return mainView; + } + + public TMLabKitFrame createSpotEditor( final boolean singleTimepoint ) + { + final ImagePlus imp = guiModel.getSettings().imp; + int timepoint; + if ( imp == null ) + timepoint = -1; + else + timepoint = singleTimepoint ? imp.getFrame() - 1 : -1; + final TMLabKitFrame frame = LabkitLauncher.launch( guiModel, timepoint ); + registerWindow( frame ); + return frame; + } + + public TrackScheme createTrackScheme() + { + final TrackScheme trackscheme = new TrackScheme( guiModel ); + final SpotImageUpdater thumbnailUpdater = new SpotImageUpdater( guiModel.getSettings() ); + trackscheme.setSpotImageUpdater( thumbnailUpdater ); + registerView( trackscheme ); + trackscheme.render(); + return trackscheme; + } + + public TrackMateBVV< ? > createBVV() + { + final ImagePlus imp = guiModel.getSettings().imp; + if ( imp != null ) + { + final TrackMateBVV< ? > tbvv = new TrackMateBVV<>( guiModel, imp ); + registerView( tbvv ); + tbvv.render(); + return tbvv; + } + return null; + } + + public AllSpotsTableView createAllSpotsTable() + { + final String imageFileName = TMUtils.getImagePathWithoutExtension( guiModel.getSettings() ); + final AllSpotsTableView view = new AllSpotsTableView( guiModel, imageFileName ); + registerView( view ); + view.render(); + return view; + } + + public TrackTableView createTrackTable() + { + final String imageFileName = TMUtils.getImagePathWithoutExtension( guiModel.getSettings() ); + final TrackTableView view = new TrackTableView( guiModel, imageFileName ); + registerView( view ); + view.render(); + return view; + } + + /** + * Registers a TrackMate view created by this window manager. + * + * @param view + * the view to register. + */ + private void registerView( final TrackMateModelView view ) + { + views.add( view ); + } + + /** + * Registers a frame in this window manager. This is useful for windows and + * dialogs that are not TrackMateModelView. + * + * @param frame + * the frame to register. + */ + private void registerWindow( final Window window ) + { + windows.add( window ); + } + + @Plugin( type = CommandDescriptionProvider.class ) + public static class Descriptions extends CommandDescriptionProvider + { + public Descriptions() + { + super( KEY_CONFIG_SCOPE, TRACKMATE ); + } + + @Override + public void getCommandDescriptions( final CommandDescriptions descriptions ) + { + descriptions.add( CloseWindowActions.CLOSE_DIALOG, CloseWindowActions.CLOSE_DIALOG_KEYS, "Close the active dialog." ); + descriptions.add( BigDataViewerActions.PREFERENCES_DIALOG, BigDataViewerActions.PREFERENCES_DIALOG_KEYS, "Open the preferences dialog." ); + } + } + + /** + * Close all opened views and dialogs. + */ + public void closeAll() + { + final ArrayList< Window > windowsToClose = new ArrayList<>(); + views.forEach( v -> windowsToClose.add( v.getWindow() ) ); + windowsToClose.addAll( windows ); + try + { + InvokeOnEDT.invokeAndWait( + () -> windowsToClose.stream() + .filter( Objects::nonNull ) + .forEach( window -> window + .dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_CLOSING ) ) ) ); + } + catch ( final InvocationTargetException e ) + { + e.printStackTrace(); + } + catch ( final InterruptedException e ) + { + Thread.currentThread().interrupt(); + e.printStackTrace(); + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/ActionChooserPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/ActionChooserPanel.java index 36860ba25..81c4eee4a 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/ActionChooserPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/ActionChooserPanel.java @@ -35,12 +35,10 @@ import javax.swing.SwingUtilities; import fiji.plugin.trackmate.Logger; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.action.CaptureOverlayAction; import fiji.plugin.trackmate.action.TrackMateAction; import fiji.plugin.trackmate.action.TrackMateActionFactory; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.providers.ActionProvider; import fiji.plugin.trackmate.util.Threads; @@ -49,7 +47,7 @@ public class ActionChooserPanel extends ModuleChooserPanel< TrackMateActionFacto private static final long serialVersionUID = 1L; - public ActionChooserPanel( final ActionProvider actionProvider, final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings ) + public ActionChooserPanel( final ActionProvider actionProvider, final GuiModel guiModel ) { super( actionProvider, "action", CaptureOverlayAction.KEY ); @@ -94,11 +92,7 @@ public void actionPerformed( final ActionEvent e ) else { action.setLogger( logger ); - action.execute( - trackmate, - selectionModel, - displaySettings, - ( JFrame ) SwingUtilities.getWindowAncestor( ActionChooserPanel.this ) ); + action.execute( guiModel, ( JFrame ) SwingUtilities.getWindowAncestor( ActionChooserPanel.this ) ); } } finally diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigurationPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigurationPanel.java index 902ece95f..583206e2a 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigurationPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigurationPanel.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -28,7 +28,7 @@ /** * The mother class for all the configuration panels. - * + * * @author Jean-Yves Tinevez * */ @@ -46,16 +46,17 @@ public abstract class ConfigurationPanel extends JPanel /** * Echoes the parameters of the given settings on this panel. - * + * * @param settings - * the settings map to use to set the values of this panel. + * the settings as a map. */ public abstract void setSettings( final Map< String, Object > settings ); /** - * Collects the current values of this panel into a settings map. - * - * @return a new settings map object with its values set by this panel. + * Returns a new settings map of string-object with its values set by this + * panel. + * + * @return a new map. */ public abstract Map< String, Object > getSettings(); diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java index e2184cfbf..afe09ce76 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java @@ -24,7 +24,10 @@ import static fiji.plugin.trackmate.gui.Fonts.BIG_FONT; import static fiji.plugin.trackmate.gui.Fonts.FONT; import static fiji.plugin.trackmate.gui.Fonts.SMALL_FONT; -import static fiji.plugin.trackmate.gui.Icons.EDIT_SETTINGS_ICON; +import static fiji.plugin.trackmate.gui.Icons.BVV_ICON; +import static fiji.plugin.trackmate.gui.Icons.SPOT_TABLE_ICON; +import static fiji.plugin.trackmate.gui.Icons.TRACK_SCHEME_ICON_16x16; +import static fiji.plugin.trackmate.gui.Icons.TRACK_TABLES_ICON; import java.awt.Color; import java.awt.Component; @@ -34,29 +37,36 @@ import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; +import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import javax.swing.Action; +import javax.swing.AbstractAction; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JFormattedTextField; -import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JRootPane; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; import javax.swing.border.LineBorder; +import fiji.plugin.trackmate.detection.DetectionUtils; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.Icons; -import fiji.plugin.trackmate.gui.displaysettings.ConfigTrackMateDisplaySettings; +import fiji.plugin.trackmate.gui.WindowManager; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackDisplayMode; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.UpdateListener; -import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.gui.editor.labkit.component.TMLabKitFrame; +import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; +import fiji.plugin.trackmate.util.Threads; import fiji.plugin.trackmate.util.WrapLayout; +import ij.ImagePlus; /** * A configuration panel used to tune the aspect of spots and tracks in multiple @@ -70,21 +80,21 @@ public class ConfigureViewsPanel extends JPanel private static final long serialVersionUID = 1L; - private static final Color BORDER_COLOR = new java.awt.Color( 192, 192, 192 ); + private static final Color BORDER_COLOR = new Color( 192, 192, 192 ); + + private final WindowManager windowManager; + + private final GuiModel guiModel; /* * CONSTRUCTOR */ - public ConfigureViewsPanel( - final DisplaySettings ds, - final FeatureDisplaySelector featureSelector, - final String spaceUnits, - final Action launchTrackSchemeAction, - final Action showTrackTablesAction, - final Action showSpotTableAction, - final Action launchLabKitAction ) + public ConfigureViewsPanel( final GuiModel guiModel, final FeatureDisplaySelector featureSelector ) { + this.guiModel = guiModel; + this.windowManager = guiModel.getWindowManager(); + final DisplaySettings ds = guiModel.getDisplaySettings(); this.setPreferredSize( new Dimension( 300, 521 ) ); this.setSize( 300, 500 ); @@ -109,27 +119,6 @@ public ConfigureViewsPanel( gbcLabelDisplayOptions.gridy = 0; add( lblDisplayOptions, gbcLabelDisplayOptions ); - /* - * Settings editor. - */ - - final JFrame editor = ConfigTrackMateDisplaySettings.editor( ds, - "Configure the display settings used in this current session.", - "TrackMate display settings" ); - editor.setLocationRelativeTo( this.getParent() ); - editor.setDefaultCloseOperation( JFrame.HIDE_ON_CLOSE ); - - final JButton btnEditSettings = new JButton( "Edit settings", EDIT_SETTINGS_ICON ); - btnEditSettings.addActionListener( e -> editor.setVisible( !editor.isVisible() ) ); - - final GridBagConstraints gbcBtnEditSettings = new GridBagConstraints(); - gbcBtnEditSettings.fill = GridBagConstraints.NONE; - gbcBtnEditSettings.insets = new Insets( 5, 5, 5, 5 ); - gbcBtnEditSettings.anchor = GridBagConstraints.EAST; - gbcBtnEditSettings.gridx = 1; - gbcBtnEditSettings.gridy = 0; - add( btnEditSettings, gbcBtnEditSettings ); - /* * Display spot checkbox. */ @@ -347,6 +336,7 @@ public ConfigureViewsPanel( spinnerDrawingZDepth.setFont( SMALL_FONT ); panelDrawingZDepth.add( spinnerDrawingZDepth ); + final String spaceUnits = guiModel.getModel().getSpaceUnits(); final JLabel lblDrawingZDepthUnits = new JLabel( spaceUnits ); lblDrawingZDepthUnits.setFont( SMALL_FONT ); panelDrawingZDepth.add( lblDrawingZDepthUnits ); @@ -358,37 +348,32 @@ public ConfigureViewsPanel( final JPanel panelButtons = new JPanel(); panelButtons.setLayout( new WrapLayout() ); + // BVV button. + final JButton btnShowBVV = new JButton( new LaunchBVVAction() ); + panelButtons.add( btnShowBVV ); + btnShowBVV.setFont( FONT ); + // TrackScheme button. - final JButton btnShowTrackScheme = new JButton( launchTrackSchemeAction ); + final JButton btnShowTrackScheme = new JButton( new LaunchTrackSchemeAction() ); panelButtons.add( btnShowTrackScheme ); btnShowTrackScheme.setFont( FONT ); // Do analysis button. - final JButton btnShowTrackTables = new JButton( showTrackTablesAction ); + final JButton btnShowTrackTables = new JButton( new ShowTrackTablesAction() ); panelButtons.add( btnShowTrackTables ); btnShowTrackTables.setFont( FONT ); - final JButton btnShowSpotTable = new JButton( showSpotTableAction ); + final JButton btnShowSpotTable = new JButton( new ShowSpotTableAction() ); panelButtons.add( btnShowSpotTable ); btnShowSpotTable.setFont( FONT ); // Labkit button. - // Is labkit available? - if ( TMUtils.isClassPresent( "sc.fiji.labkit.ui.LabkitFrame" ) && launchLabKitAction.isEnabled() ) - { - final JButton btnLabKit = new JButton( launchLabKitAction ); - btnLabKit.setFont( FONT ); - btnLabKit.setText( "Launch spot editor" ); - btnLabKit.setIcon( GuiUtils.scaleImage( Icons.SEGMENTATION_EDITOR_ICON_64x64, 16, 16 ) ); - btnLabKit.setToolTipText( "" - + "Launch the Labkit editor to edit spot segmentation
" - + "on the time-point currently displayed in the main
" - + "view." - + "

" - + "Shift + click will launch the editor on all the
" - + "time-points in the movie." ); - panelButtons.add( btnLabKit ); - } + final JButton btnLabKit = new JButton( new LaunchSpotEditorAction() ); + btnLabKit.setFont( FONT ); + btnLabKit.setText( "Launch spot editor" ); + btnLabKit.setIcon( GuiUtils.scaleImage( Icons.SEGMENTATION_EDITOR_ICON_64x64, 16, 16 ) ); + btnLabKit.setToolTipText( SPOT_EDITOR_TOOLTIP ); + panelButtons.add( btnLabKit ); panelButtons.setSize( new Dimension( 300, 1 ) ); final GridBagConstraints gbcPanelButtons = new GridBagConstraints(); @@ -479,4 +464,143 @@ private static final void setEnabled( final Container container, final boolean e setEnabled( ( Container ) component, enabled ); } } + + /* + * Actions that launches the different views. + */ + + private class LaunchSpotEditorAction extends AbstractAction + { + private static final long serialVersionUID = 1L; + + private LaunchSpotEditorAction() + { + super( "Launch spot editor", Icons.SEGMENTATION_EDITOR_ICON_64x64 ); + putValue( SHORT_DESCRIPTION, "Launch the Labkit editor to edit spot segmentation." ); + // TODO when we go for 3D editor. + setEnabled( DetectionUtils.is2D( guiModel.getSettings().imp ) ); + } + + @Override + public void actionPerformed( final ActionEvent ae ) + { + Threads.run( "Launching spot editor thread", () -> { + final JRootPane parent = SwingUtilities.getRootPane( ( Component ) ae.getSource() ); + final EverythingDisablerAndReenabler disabler = new EverythingDisablerAndReenabler( parent, new Class[] { JLabel.class } ); + disabler.disable(); + try + { + // Is shift pressed? + final int mod = ae.getModifiers(); + final boolean shiftPressed = ( mod & ActionEvent.SHIFT_MASK ) > 0; + final TMLabKitFrame spotEditor = windowManager.createSpotEditor( !shiftPressed ); + spotEditor.onCloseListeners().addListener( disabler::reenable ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + disabler.reenable(); + } + } ); + } + } + + private class LaunchBVVAction extends AbstractAction + { + private static final long serialVersionUID = 1L; + + private LaunchBVVAction() + { + super( "3D view", BVV_ICON ); + putValue( SHORT_DESCRIPTION, BVV_BUTTON_TOOLTIP ); + final ImagePlus imp = guiModel.getSettings().imp; + final boolean enabled = ( imp != null ) && !DetectionUtils.is2D( imp ); + setEnabled( enabled ); + } + + @Override + public void actionPerformed( final ActionEvent e ) + { + Threads.run( "Launching BVV thread", () -> { + setEnabled( false ); + windowManager.createBVV(); + setEnabled( true ); + } ); + } + } + + private class LaunchTrackSchemeAction extends AbstractAction + { + private static final long serialVersionUID = 1L; + + private LaunchTrackSchemeAction() + { + super( "TrackScheme", TRACK_SCHEME_ICON_16x16 ); + putValue( SHORT_DESCRIPTION, TRACKSCHEME_BUTTON_TOOLTIP ); + } + + @Override + public void actionPerformed( final ActionEvent e ) + { + Threads.run( "Launching TrackScheme thread", () -> windowManager.createTrackScheme() ); + } + } + + private class ShowTrackTablesAction extends AbstractAction + { + private static final long serialVersionUID = 1L; + + private ShowTrackTablesAction() + { + super( "Tracks", TRACK_TABLES_ICON ); + putValue( SHORT_DESCRIPTION, TRACK_TABLES_BUTTON_TOOLTIP ); + } + + @Override + public void actionPerformed( final ActionEvent e ) + { + Threads.run( "Launching tracks table thread", () -> windowManager.createTrackTable() ); + } + } + + private class ShowSpotTableAction extends AbstractAction + { + private static final long serialVersionUID = 1L; + + private ShowSpotTableAction() + { + super( "Spots", SPOT_TABLE_ICON ); + putValue( SHORT_DESCRIPTION, SPOT_TABLE_BUTTON_TOOLTIP ); + } + + @Override + public void actionPerformed( final ActionEvent e ) + { + Threads.run( "Launching all spots table thread", () -> windowManager.createAllSpotsTable() ); + } + } + + private static final String SPOT_TABLE_BUTTON_TOOLTIP = "Export the features of all spots to ImageJ tables."; + + private static final String TRACKSCHEME_BUTTON_TOOLTIP = "Launch a new instance of TrackScheme."; + + private static final String BVV_BUTTON_TOOLTIP = "Launch a new 3D viewer."; + + private static final String TRACK_TABLES_BUTTON_TOOLTIP = "" + + "Export the features of all tracks, edges and all
" + + "spots belonging to a track to ImageJ tables." + + ""; + + private static final String SPOT_EDITOR_TOOLTIP = "" + + "Launch the Labkit editor to edit spot segmentation
" + + "on the time-point currently displayed in the main
" + + "view." + + "

" + + "If a ROI is present in the image, only the spots and the
" + + "image in the ROI will be opened for edition in LabKit
" + + "(this can speed up editing large images)." + + "

" + + "Shift + click will launch the editor on all the
" + + "time-points in the movie."; + } 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 0f7a06468..512aff85d 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/FeatureDisplaySelector.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/FeatureDisplaySelector.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -59,6 +59,8 @@ import javax.swing.JPopupMenu; import javax.swing.SwingConstants; +import org.scijava.ui.config.visitors.gui.elements.colormap.Colormap; + import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.ModelChangeEvent; import fiji.plugin.trackmate.Settings; @@ -67,7 +69,6 @@ import fiji.plugin.trackmate.features.manual.ManualSpotColorAnalyzerFactory; import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; import fiji.plugin.trackmate.gui.GuiUtils; -import fiji.plugin.trackmate.gui.displaysettings.Colormap; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; @@ -139,12 +140,14 @@ private double[] autoMinMax( final TrackMateObject target ) /** * Returns a {@link CategoryJComboBox} that lets a user select among all - * available features in TrackMate. + * available features in TrackMate. The features are read from the model and + * settings, and the model is listened to so that the combo-box is updated + * when new features are added to the model. * * @param model - * the {@link Model} to read features already computed. + * the model to read existing features from. * @param settings - * the {@link Settings} to read features that can be computed. + * the settings to read configured features from. * @return a new {@link CategoryJComboBox}. */ public static final CategoryJComboBox< TrackMateObject, String > createComboBoxSelector( final Model model, final Settings settings ) 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 5f51e0dfb..89b6f7410 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/FilterGuiPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/FilterGuiPanel.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -105,10 +105,10 @@ public class FilterGuiPanel extends JPanel implements ChangeListener * CONSTRUCTOR */ - public FilterGuiPanel( - final Model model, - final Settings settings, - final TrackMateObject target, + public FilterGuiPanel( + final Model model, + final Settings settings, + final TrackMateObject target, final List< FeatureFilter > filters, final String defaultFeature, final FeatureDisplaySelector featureSelector ) @@ -184,11 +184,11 @@ public FilterGuiPanel( 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 ); @@ -209,7 +209,7 @@ public FilterGuiPanel( lblTop.setVisible( false ); // For now logger = new ProgressBarLogger(); - + // On close GuiUtils.addOnClosingEvent( this, () -> updater.quit() ); } @@ -238,9 +238,9 @@ public void stateChanged( final ChangeEvent e ) } /** - * Returns the thresholds currently set by this GUI. - * - * @return the thresholds. + * Returns the filters currently set by this GUI. + * + * @return the list of filters. */ public List< FeatureFilter > getFeatureFilters() { @@ -252,7 +252,7 @@ public List< FeatureFilter > getFeatureFilters() * 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. - * + * * @param listener * the listener to add. */ @@ -266,7 +266,8 @@ public void addChangeListener( final ChangeListener listener ) * * @param listener * the listener to remove. - * @return true if the listener was in listener collection of this instance. + * @return true if the listener was in listener collection of + * this instance. */ public boolean removeChangeListener( final ChangeListener listener ) { 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 dd0ad6a30..9b5d23047 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/FilterPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/FilterPanel.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -311,7 +311,7 @@ public FeatureFilter getFilter() * 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. - * + * * @param listener * the listener to add. */ 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 e994471ae..586dd6e83 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/GrapherPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/GrapherPanel.java @@ -46,27 +46,26 @@ import org.jgrapht.graph.DefaultWeightedEdge; +import fiji.plugin.trackmate.Model; 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.gui.GuiModel; 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 ij.ImagePlus; public class GrapherPanel extends JPanel { private static final long serialVersionUID = 1L; - private final TrackMate trackmate; - private final JPanel panelSpot; private final JPanel panelEdges; @@ -79,10 +78,6 @@ public class GrapherPanel extends JPanel private final FeaturePlotSelectionPanel trackFeatureSelectionPanel; - private final DisplaySettings displaySettings; - - private final SelectionModel selectionModel; - private final JPanel panelSelection; private final JRadioButton rdbtnAll; @@ -93,36 +88,35 @@ public class GrapherPanel extends JPanel private final JCheckBox chkboxConnectDots; + private final GuiModel guiModel; + /* * CONSTRUCTOR */ - public GrapherPanel( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings ) + public GrapherPanel( final GuiModel guiModel ) { - this.trackmate = trackmate; - this.selectionModel = selectionModel; - this.displaySettings = displaySettings; + this.guiModel = guiModel; setLayout( new BorderLayout( 0, 0 ) ); - final JTabbedPane tabbedPane = new JTabbedPane( SwingConstants.TOP ); add( tabbedPane, BorderLayout.CENTER ); - panelSpot = new JPanel(); + this.panelSpot = new JPanel(); tabbedPane.addTab( "Spots", SPOT_ICON_64x64, panelSpot, null ); panelSpot.setLayout( new BorderLayout( 0, 0 ) ); - panelEdges = new JPanel(); + this.panelEdges = new JPanel(); tabbedPane.addTab( "Links", EDGE_ICON_64x64, panelEdges, null ); panelEdges.setLayout( new BorderLayout( 0, 0 ) ); - panelTracks = new JPanel(); + this.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 Map< String, String > spotFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.SPOTS, guiModel.getModel(), guiModel.getSettings() ); final Set< String > spotFeatures = spotFeatureNames.keySet(); - spotFeatureSelectionPanel = new FeaturePlotSelectionPanel( + this.spotFeatureSelectionPanel = new FeaturePlotSelectionPanel( "T", "Mean intensity ch1", spotFeatures, @@ -132,9 +126,9 @@ public GrapherPanel( final TrackMate trackmate, final SelectionModel selectionMo // regen edge features panelEdges.removeAll(); - final Map< String, String > edgeFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.EDGES, trackmate.getModel(), trackmate.getSettings() ); + final Map< String, String > edgeFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.EDGES, guiModel.getModel(), guiModel.getSettings() ); final Set< String > edgeFeatures = edgeFeatureNames.keySet(); - edgeFeatureSelectionPanel = new FeaturePlotSelectionPanel( + this.edgeFeatureSelectionPanel = new FeaturePlotSelectionPanel( "Edge time", "Speed", edgeFeatures, @@ -144,9 +138,9 @@ public GrapherPanel( final TrackMate trackmate, final SelectionModel selectionMo // regen trak features panelTracks.removeAll(); - final Map< String, String > trackFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.TRACKS, trackmate.getModel(), trackmate.getSettings() ); + final Map< String, String > trackFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.TRACKS, guiModel.getModel(), guiModel.getSettings() ); final Set< String > trackFeatures = trackFeatureNames.keySet(); - trackFeatureSelectionPanel = new FeaturePlotSelectionPanel( + this.trackFeatureSelectionPanel = new FeaturePlotSelectionPanel( "Track index", "Number of spots in track", trackFeatures, @@ -154,19 +148,19 @@ public GrapherPanel( final TrackMate trackmate, final SelectionModel selectionMo ( xKey, yKeys ) -> Threads.run( () -> plotTrackFeatures( xKey, yKeys ) ) ); panelTracks.add( trackFeatureSelectionPanel ); - panelSelection = new JPanel(); + this.panelSelection = new JPanel(); panelSelection.setLayout( new BoxLayout( panelSelection, BoxLayout.LINE_AXIS ) ); add( panelSelection, BorderLayout.SOUTH ); - rdbtnAll = new JRadioButton( "All" ); + this.rdbtnAll = new JRadioButton( "All" ); rdbtnAll.setFont( rdbtnAll.getFont().deriveFont( rdbtnAll.getFont().getSize() - 2f ) ); panelSelection.add( rdbtnAll ); - rdbtnSelection = new JRadioButton( "Selection" ); + this.rdbtnSelection = new JRadioButton( "Selection" ); rdbtnSelection.setFont( rdbtnSelection.getFont().deriveFont( rdbtnSelection.getFont().getSize() - 2f ) ); panelSelection.add( rdbtnSelection ); - rdbtnTracks = new JRadioButton( "Tracks of selection" ); + this.rdbtnTracks = new JRadioButton( "Tracks of selection" ); rdbtnTracks.setFont( rdbtnTracks.getFont().deriveFont( rdbtnTracks.getFont().getSize() - 2f ) ); panelSelection.add( rdbtnTracks ); @@ -178,7 +172,7 @@ public GrapherPanel( final TrackMate trackmate, final SelectionModel selectionMo panelSelection.add( new JSeparator( SwingConstants.VERTICAL ) ); - chkboxConnectDots = new JCheckBox( "Connect" ); + this.chkboxConnectDots = new JCheckBox( "Connect" ); chkboxConnectDots.setFont( chkboxConnectDots.getFont().deriveFont( chkboxConnectDots.getFont().getSize() - 2f ) ); chkboxConnectDots.setSelected( true ); panelSelection.add( chkboxConnectDots ); @@ -201,6 +195,9 @@ public FeaturePlotSelectionPanel getTrackFeatureSelectionPanel() private void plotSpotFeatures( final String xFeature, final List< String > yFeatures ) { + final Model model = guiModel.getModel(); + final SelectionModel selectionModel = guiModel.getSelectionModel(); + final EverythingDisablerAndReenabler enabler = new EverythingDisablerAndReenabler( this, new Class[] { JLabel.class } ); enabler.disable(); try @@ -208,9 +205,9 @@ private void plotSpotFeatures( final String xFeature, final List< String > yFeat 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 ) ); + spots = new ArrayList<>( model.getSpots().getNSpots( true ) ); + for ( final Integer trackID : model.getTrackModel().trackIDs( true ) ) + spots.addAll( model.getTrackModel().trackSpots( trackID ) ); } else if ( rdbtnSelection.isSelected() ) { @@ -226,16 +223,17 @@ else if ( rdbtnSelection.isSelected() ) final boolean addLines = chkboxConnectDots.isSelected(); final SpotFeatureGrapher grapher = new SpotFeatureGrapher( + guiModel, 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" ); + + final ImagePlus imp = guiModel.getSettings().imp; + final String title = imp != null ? imp.getShortTitle() : "TrackMate"; + frame.setTitle( title + " spot features" ); GuiUtils.positionWindow( frame, SwingUtilities.getWindowAncestor( this ) ); frame.setVisible( true ); } @@ -247,6 +245,9 @@ else if ( rdbtnSelection.isSelected() ) private void plotEdgeFeatures( final String xFeature, final List< String > yFeatures ) { + final Model model = guiModel.getModel(); + final SelectionModel selectionModel = guiModel.getSelectionModel(); + final EverythingDisablerAndReenabler enabler = new EverythingDisablerAndReenabler( this, new Class[] { JLabel.class } ); enabler.disable(); try @@ -255,8 +256,8 @@ private void plotEdgeFeatures( final String xFeature, final List< String > yFeat if ( rdbtnAll.isSelected() ) { edges = new ArrayList<>(); - for ( final Integer trackID : trackmate.getModel().getTrackModel().trackIDs( true ) ) - edges.addAll( trackmate.getModel().getTrackModel().trackEdges( trackID ) ); + for ( final Integer trackID : model.getTrackModel().trackIDs( true ) ) + edges.addAll( model.getTrackModel().trackEdges( trackID ) ); } else if ( rdbtnSelection.isSelected() ) { @@ -272,16 +273,17 @@ else if ( rdbtnSelection.isSelected() ) final boolean addLines = chkboxConnectDots.isSelected(); final EdgeFeatureGrapher grapher = new EdgeFeatureGrapher( + guiModel, 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" ); + + final ImagePlus imp = guiModel.getSettings().imp; + final String title = imp != null ? imp.getShortTitle() : "TrackMate"; + frame.setTitle( title + " edge features" ); GuiUtils.positionWindow( frame, SwingUtilities.getWindowAncestor( this ) ); frame.setVisible( true ); edgeFeatureSelectionPanel.setEnabled( true ); @@ -294,6 +296,9 @@ else if ( rdbtnSelection.isSelected() ) private void plotTrackFeatures( final String xFeature, final List< String > yFeatures ) { + final Model model = guiModel.getModel(); + final SelectionModel selectionModel = guiModel.getSelectionModel(); + final EverythingDisablerAndReenabler enabler = new EverythingDisablerAndReenabler( this, new Class[] { JLabel.class } ); enabler.disable(); try @@ -301,28 +306,29 @@ private void plotTrackFeatures( final String xFeature, final List< String > yFea final List< Integer > trackIDs; if ( rdbtnAll.isSelected() ) { - trackIDs = new ArrayList<>( trackmate.getModel().getTrackModel().unsortedTrackIDs( true ) ); + trackIDs = new ArrayList<>( model.getTrackModel().unsortedTrackIDs( true ) ); } else { final Set< Integer > set = new HashSet<>(); for ( final Spot spot : selectionModel.getSpotSelection() ) - set.add( trackmate.getModel().getTrackModel().trackIDOf( spot ) ); + set.add( model.getTrackModel().trackIDOf( spot ) ); for ( final DefaultWeightedEdge edge : selectionModel.getEdgeSelection() ) - set.add( trackmate.getModel().getTrackModel().trackIDOf( edge ) ); + set.add( model.getTrackModel().trackIDOf( edge ) ); trackIDs = new ArrayList< >( set ); } final TrackFeatureGrapher grapher = new TrackFeatureGrapher( + guiModel, trackIDs, xFeature, - yFeatures, - trackmate.getModel(), - selectionModel, - displaySettings ); + yFeatures ); final JFrame frame = grapher.render(); frame.setIconImage( Icons.PLOT_ICON.getImage() ); - frame.setTitle( trackmate.getSettings().imp.getShortTitle() + " track features" ); + + final ImagePlus imp = guiModel.getSettings().imp; + final String title = imp != null ? imp.getShortTitle() : "TrackMate"; + frame.setTitle( title + " track features" ); GuiUtils.positionWindow( frame, SwingUtilities.getWindowAncestor( this ) ); frame.setVisible( true ); } diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/InitFilterPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/InitFilterPanel.java index b6d695542..247cc96bb 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/InitFilterPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/InitFilterPanel.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -59,13 +59,13 @@ public class InitFilterPanel extends JPanel private double[] values; /** - * Creates a panel to set an initial feature filter on quality. + * Default constructor, initialize component. * * @param filter - * the initial feature filter to display. + * the filter to initialize the panel with. * @param valueCollector - * a function that, given a feature key, returns an array of - * double values for all spots for that feature. + * a function that can return the value collection of a specified + * feature. */ public InitFilterPanel( final FeatureFilter filter, final Function< String, double[] > valueCollector ) { @@ -139,8 +139,8 @@ public void refresh() /** * Returns the feature threshold on quality set by this panel. - * - * @return the feature threshold on quality set by this panel. + * + * @return the feature threshold. */ public FeatureFilter getFeatureThreshold() { diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/PanelProbaThreshold.java b/src/main/java/fiji/plugin/trackmate/gui/components/PanelProbaThreshold.java new file mode 100644 index 000000000..923828d37 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/components/PanelProbaThreshold.java @@ -0,0 +1,88 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.gui.components; + +import static fiji.plugin.trackmate.gui.Fonts.SMALL_FONT; + +import java.util.function.Consumer; +import java.util.function.DoubleSupplier; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import org.scijava.ui.config.visitors.gui.elements.SliderPanelDouble; +import org.scijava.ui.config.visitors.gui.elements.StyleElements; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.BoundedDoubleElement; + +/** + * A utility widget that lets a user specify a threshold on a probability value, + * from 0 to 1. + */ +public class PanelProbaThreshold extends JPanel +{ + + private static final long serialVersionUID = 1L; + + private double threshold; + + private final SliderPanelDouble sliderPanel; + + private final BoundedDoubleElement thresholdEl; + + public PanelProbaThreshold( final double threshold ) + { + this.threshold = threshold; + setLayout( new BoxLayout( this, BoxLayout.X_AXIS ) ); + + final JLabel chckbxSmooth = new JLabel( "Proba threshold" ); + chckbxSmooth.setFont( SMALL_FONT ); + add( chckbxSmooth ); + add( Box.createHorizontalGlue() ); + + final DoubleSupplier getter = () -> getThreshold(); + final Consumer< Double > setter = v -> setThresholdPrivate( v ); + thresholdEl = StyleElements.boundedDoubleElement( "threshold", 0., 1., getter, setter ); + sliderPanel = StyleElements.linkedSliderPanel( thresholdEl, 3, 0.1 ); + sliderPanel.setFont( SMALL_FONT ); + + add( sliderPanel ); + } + + private void setThresholdPrivate( final double threshold ) + { + this.threshold = threshold; + } + + public void setThreshold( final double threshold ) + { + setThresholdPrivate( threshold ); + thresholdEl.getValue().setCurrentValue( threshold ); + sliderPanel.update(); + } + + public double getThreshold() + { + return threshold; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/PanelSmoothContour.java b/src/main/java/fiji/plugin/trackmate/gui/components/PanelSmoothContour.java new file mode 100644 index 000000000..741e66cf9 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/components/PanelSmoothContour.java @@ -0,0 +1,105 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.gui.components; + +import static fiji.plugin.trackmate.gui.Fonts.SMALL_FONT; + +import java.util.function.Consumer; +import java.util.function.DoubleSupplier; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import org.scijava.ui.config.visitors.gui.elements.SliderPanelDouble; +import org.scijava.ui.config.visitors.gui.elements.StyleElements; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.BoundedDoubleElement; + +public class PanelSmoothContour extends JPanel +{ + + private static final long serialVersionUID = 1L; + + private double scale; + + private final SliderPanelDouble sliderPanel; + + private final JCheckBox chckbxSmooth; + + private final BoundedDoubleElement scaleEl; + + public PanelSmoothContour( final double scale, final String units ) + { + this.scale = scale; + setLayout( new BoxLayout( this, BoxLayout.X_AXIS ) ); + + chckbxSmooth = new JCheckBox( "Smooth" ); + chckbxSmooth.setFont( SMALL_FONT ); + add( chckbxSmooth ); + add( Box.createHorizontalGlue() ); + + final DoubleSupplier getter = () -> getScale(); + final Consumer< Double > setter = v -> setScalePrivate( v ); + scaleEl = StyleElements.boundedDoubleElement( "scale", 0., 20., getter, setter ); + sliderPanel = StyleElements.linkedSliderPanel( scaleEl, 2 ); + sliderPanel.setFont( SMALL_FONT ); + + add( sliderPanel ); + add( Box.createHorizontalStrut( 5 ) ); + final JLabel lblUnits = new JLabel( units ); + lblUnits.setFont( SMALL_FONT ); + add( lblUnits ); + + chckbxSmooth.addActionListener( e -> sliderPanel.setEnabled( chckbxSmooth.isSelected() ) ); + setOnOff(); + if ( scale > 0. ) + scaleEl.set( scale ); + } + + private void setOnOff() + { + chckbxSmooth.setSelected( scale > 0. ); + sliderPanel.setEnabled( scale > 0. ); + } + + private void setScalePrivate( final double scale ) + { + this.scale = scale; + } + + public void setScale( final double scale ) + { + setScalePrivate( scale ); + setOnOff(); + scaleEl.getValue().setCurrentValue( scale ); + sliderPanel.update(); + } + + public double getScale() + { + if ( chckbxSmooth.isSelected() ) + return scale; + return -1.; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/detector/LabelImageDetectorConfigurationPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/detector/LabelImageDetectorConfigurationPanel.java index 454f142a1..061379c08 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/detector/LabelImageDetectorConfigurationPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/detector/LabelImageDetectorConfigurationPanel.java @@ -21,7 +21,7 @@ */ package fiji.plugin.trackmate.gui.components.detector; -import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; +import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_INTENSITY_THRESHOLD; import java.util.Map; @@ -29,7 +29,6 @@ import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.detection.LabelImageDetectorFactory; import fiji.plugin.trackmate.detection.SpotDetectorFactory; -import fiji.plugin.trackmate.detection.ThresholdDetectorFactory; /** * Configuration panel for spot detectors based on label images. @@ -59,15 +58,14 @@ public LabelImageDetectorConfigurationPanel( public Map< String, Object > getSettings() { final Map< String, Object > lSettings = super.getSettings(); - lSettings.remove( ThresholdDetectorFactory.KEY_INTENSITY_THRESHOLD ); + lSettings.remove( KEY_INTENSITY_THRESHOLD ); return lSettings; } @Override public void setSettings( final Map< String, Object > settings ) { - sliderChannel.setValue( ( Integer ) settings.get( KEY_TARGET_CHANNEL ) ); - chkboxSimplify.setSelected( ( Boolean ) settings.get( ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS ) ); + setSettingsNonIntensity( settings ); } /** diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/detector/MaskDetectorConfigurationPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/detector/MaskDetectorConfigurationPanel.java index 1cb691cf0..fc6bf261c 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/detector/MaskDetectorConfigurationPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/detector/MaskDetectorConfigurationPanel.java @@ -21,8 +21,6 @@ */ package fiji.plugin.trackmate.gui.components.detector; -import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; - import java.util.Map; import fiji.plugin.trackmate.Model; @@ -66,8 +64,7 @@ public Map< String, Object > getSettings() @Override public void setSettings( final Map< String, Object > settings ) { - sliderChannel.setValue( ( Integer ) settings.get( KEY_TARGET_CHANNEL ) ); - chkboxSimplify.setSelected( ( Boolean ) settings.get( ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS ) ); + setSettingsNonIntensity( settings ); } /** diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/detector/ThresholdDetectorConfigurationPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/detector/ThresholdDetectorConfigurationPanel.java index 0cafdde7d..8a1a3c7e1 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/detector/ThresholdDetectorConfigurationPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/detector/ThresholdDetectorConfigurationPanel.java @@ -22,6 +22,9 @@ package fiji.plugin.trackmate.gui.components.detector; import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; +import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_INTENSITY_THRESHOLD; +import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS; +import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_SMOOTHING_SCALE; import static fiji.plugin.trackmate.gui.Fonts.BIG_FONT; import static fiji.plugin.trackmate.gui.Fonts.FONT; import static fiji.plugin.trackmate.gui.Fonts.SMALL_FONT; @@ -51,6 +54,7 @@ import fiji.plugin.trackmate.detection.ThresholdDetectorFactory; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.components.ConfigurationPanel; +import fiji.plugin.trackmate.gui.components.PanelSmoothContour; import fiji.plugin.trackmate.util.DetectionPreview; import fiji.plugin.trackmate.util.TMUtils; import fiji.plugin.trackmate.util.Threads; @@ -87,6 +91,8 @@ public class ThresholdDetectorConfigurationPanel extends ConfigurationPanel protected final JLabel lblIntensityThreshold; + protected final PanelSmoothContour smoothingPanel; + /* * CONSTRUCTOR */ @@ -129,10 +135,10 @@ protected ThresholdDetectorConfigurationPanel( setPreferredSize( new Dimension( 300, 511 ) ); final GridBagLayout gridBagLayout = new GridBagLayout(); - gridBagLayout.rowHeights = new int[] { 0, 0, 0, 0, 0, 0, 0, 150 }; + gridBagLayout.rowHeights = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 150 }; gridBagLayout.columnWidths = new int[] { 0, 0, 20 }; gridBagLayout.columnWeights = new double[] { 0.0, 1.0, 0.0 }; - gridBagLayout.rowWeights = new double[] { 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.1 }; + gridBagLayout.rowWeights = new double[] { 0., 1., 0., 0., 0., 0., 0., 0.1 }; setLayout( gridBagLayout ); final JLabel jLabelDetectorName = new JLabel( detectorName, ThresholdDetectorFactory.ICON, JLabel.CENTER ); @@ -229,6 +235,16 @@ protected ThresholdDetectorConfigurationPanel( chkboxSimplify.setText( "Simplify contours." ); chkboxSimplify.setFont( FONT ); + smoothingPanel = new PanelSmoothContour( -1., model.getSpaceUnits() ); + final GridBagConstraints gbSmoothPanel = new GridBagConstraints(); + gbSmoothPanel.anchor = GridBagConstraints.NORTHWEST; + gbSmoothPanel.insets = new Insets( 5, 5, 5, 5 ); + gbSmoothPanel.gridwidth = 3; + gbSmoothPanel.gridx = 0; + gbSmoothPanel.gridy = 6; + gbSmoothPanel.fill = GridBagConstraints.HORIZONTAL; + this.add( smoothingPanel, gbSmoothPanel ); + final DetectionPreview detectionPreview = DetectionPreview.create() .model( model ) .settings( settings ) @@ -243,7 +259,7 @@ protected ThresholdDetectorConfigurationPanel( gbcBtnPreview.insets = new Insets( 5, 5, 5, 5 ); gbcBtnPreview.gridwidth = 3; gbcBtnPreview.gridx = 0; - gbcBtnPreview.gridy = 7; + gbcBtnPreview.gridy = 8; this.add( detectionPreview.getPanel(), gbcBtnPreview ); /* @@ -276,11 +292,9 @@ protected ThresholdDetectorConfigurationPanel( private < T extends RealType< T > & NativeType< T > > void autoThreshold() { btnAutoThreshold.setEnabled( false ); - Threads.run( "TrackMate compute threshold thread", () -> - { + Threads.run( "TrackMate compute threshold thread", () -> { try { - @SuppressWarnings( "unchecked" ) final ImgPlus< T > img = TMUtils.rawWraps( settings.imp ); final int channel = ( ( Number ) sliderChannel.getValue() ).intValue() - 1; final int frame = settings.imp.getT() - 1; @@ -303,21 +317,30 @@ public Map< String, Object > getSettings() final int targetChannel = sliderChannel.getValue(); final boolean simplify = chkboxSimplify.isSelected(); final double intensityThreshold = ( ( Number ) ftfIntensityThreshold.getValue() ).doubleValue(); + final double scale = smoothingPanel.getScale(); final HashMap< String, Object > lSettings = new HashMap<>( 3 ); lSettings.put( KEY_TARGET_CHANNEL, targetChannel ); - lSettings.put( ThresholdDetectorFactory.KEY_INTENSITY_THRESHOLD, intensityThreshold ); - lSettings.put( ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS, simplify ); + lSettings.put( KEY_INTENSITY_THRESHOLD, intensityThreshold ); + lSettings.put( KEY_SIMPLIFY_CONTOURS, simplify ); + lSettings.put( KEY_SMOOTHING_SCALE, scale ); return lSettings; } - @Override - public void setSettings( final Map< String, Object > settings ) + protected void setSettingsNonIntensity( final Map< String, Object > settings ) { sliderChannel.setValue( ( Integer ) settings.get( KEY_TARGET_CHANNEL ) ); chkboxSimplify.setSelected( ( Boolean ) settings.get( ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS ) ); + final Object scaleObj = settings.get( KEY_SMOOTHING_SCALE ); + final double scale = scaleObj == null ? -1. : ( ( Number ) scaleObj ).doubleValue(); + smoothingPanel.setScale( scale ); + } - final Double intensityThreshold = Double.valueOf( ( Double ) settings.get( ThresholdDetectorFactory.KEY_INTENSITY_THRESHOLD ) ); + @Override + public void setSettings( final Map< String, Object > settings ) + { + setSettingsNonIntensity( settings ); + final Double intensityThreshold = Double.valueOf( ( Double ) settings.get( KEY_INTENSITY_THRESHOLD ) ); if ( intensityThreshold == null || intensityThreshold == 0. ) autoThreshold(); else diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelFeatureSelectionGui.java b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelFeatureSelectionGui.java index fb25a4987..e8697378d 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelFeatureSelectionGui.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelFeatureSelectionGui.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -84,13 +84,13 @@ public JPanelFeatureSelectionGui() */ /** - * Sets the features and their names that should be presented by this GUI. + * Set the features and their names that should be presented by this GUI. * The user will be allowed to choose amongst the given features. - * + * * @param features - * the keys of the features. + * the features to add in the GUI. * @param featureNames - * a map from feature key to feature name + * the feature names that will be displayed. */ public void setDisplayFeatures( final Collection< String > features, final Map< String, String > featureNames ) { diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/BoundedValue.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/BoundedValue.java deleted file mode 100644 index 4c0efd3bc..000000000 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/BoundedValue.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.gui.displaysettings; - -/** - * A int variable that can take any value in a given range. A - * {@link #setUpdateListener(UpdateListener) listener} is notified when the - * value or its allowed range is changed. - * - * @author Tobias Pietzsch - */ -public class BoundedValue -{ - private int rangeMin; - - private int rangeMax; - - private int currentValue; - - public interface UpdateListener - { - void update(); - } - - private UpdateListener updateListener; - - public BoundedValue( final int rangeMin, final int rangeMax, final int currentValue ) - { - this.rangeMin = rangeMin; - this.rangeMax = rangeMax; - this.currentValue = currentValue; - updateListener = null; - } - - public int getRangeMin() - { - return rangeMin; - } - - public int getRangeMax() - { - return rangeMax; - } - - public int getCurrentValue() - { - return currentValue; - } - - public void setRange( final int min, final int max ) - { - assert min <= max; - rangeMin = min; - rangeMax = max; - currentValue = Math.min( Math.max( currentValue, min ), max ); - - if ( updateListener != null ) - updateListener.update(); - } - - public void setCurrentValue( final int value ) - { - currentValue = value; - - if ( currentValue < rangeMin ) - currentValue = rangeMin; - else if ( currentValue > rangeMax ) - currentValue = rangeMax; - - if ( updateListener != null ) - updateListener.update(); - } - - public void setUpdateListener( final UpdateListener l ) - { - updateListener = l; - } -} diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/BoundedValueDouble.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/BoundedValueDouble.java deleted file mode 100644 index 61f987d28..000000000 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/BoundedValueDouble.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.gui.displaysettings; - -/** - * A {@code double} variable that can take any value in a given range. A - * {@link #setUpdateListener(UpdateListener) listener} is notified when the - * value or its allowed range is changed. - * - * @author Tobias Pietzsch - */ -public class BoundedValueDouble -{ - private double rangeMin; - - private double rangeMax; - - private double currentValue; - - public interface UpdateListener - { - void update(); - } - - private UpdateListener updateListener; - - public BoundedValueDouble( final double rangeMin, final double rangeMax, final double currentValue ) - { - this.rangeMin = rangeMin; - this.rangeMax = rangeMax; - this.currentValue = currentValue; - updateListener = null; - } - - public double getRangeMin() - { - return rangeMin; - } - - public double getRangeMax() - { - return rangeMax; - } - - public double getCurrentValue() - { - return currentValue; - } - - public void setRange( final double min, final double max ) - { - assert min <= max; - rangeMin = min; - rangeMax = max; - currentValue = Math.min( Math.max( currentValue, min ), max ); - - if ( updateListener != null ) - updateListener.update(); - } - - public void setCurrentValue( final double value ) - { - currentValue = value; - - if ( currentValue < rangeMin ) - currentValue = rangeMin; - else if ( currentValue > rangeMax ) - currentValue = rangeMax; - - if ( updateListener != null ) - updateListener.update(); - } - - public void setUpdateListener( final UpdateListener l ) - { - updateListener = l; - } -} diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/ColorIcon.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/ColorIcon.java deleted file mode 100644 index 6356a4252..000000000 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/ColorIcon.java +++ /dev/null @@ -1,93 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.gui.displaysettings; - -import java.awt.Color; -import java.awt.Component; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.RenderingHints; -import java.awt.geom.RoundRectangle2D; - -import javax.swing.Icon; - -/** - * Adapted from http://stackoverflow.com/a/3072979/230513 - */ -public class ColorIcon implements Icon -{ - private static final int DEFAULT_PAD = 0; - - private static final int DEFAUL_SIZE = 16; - - private final int size; - - private Color color; - - private final int pad; - - public ColorIcon( final Color color, final int size, final int pad ) - { - this.color = color; - this.size = size; - this.pad = pad; - } - - public ColorIcon( final Color color, final int size ) - { - this( color, size, DEFAULT_PAD ); - } - - public ColorIcon( final Color color ) - { - this( color, DEFAUL_SIZE ); - } - - @Override - public void paintIcon( final Component c, final Graphics g, final int x, final int y ) - { - final Graphics2D g2d = ( Graphics2D ) g; - g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); - g2d.setColor( color ); - final RoundRectangle2D.Float shape = new RoundRectangle2D.Float( x + pad, y + pad, size, size, 5, 5 ); - g2d.fill( shape ); - g2d.setColor( Color.BLACK ); - g2d.draw( shape ); - } - - public void setColor( final Color color ) - { - this.color = color; - } - - @Override - public int getIconWidth() - { - return size + 2 * pad; - } - - @Override - public int getIconHeight() - { - return size + 2 * pad; - } -} diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/Colormap.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/Colormap.java deleted file mode 100644 index a1b1f6a9d..000000000 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/Colormap.java +++ /dev/null @@ -1,314 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.gui.displaysettings; - -import java.awt.Color; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.TreeMap; - -import org.jfree.chart.renderer.PaintScale; - -/** - * This class implements a {@link PaintScale} that generate colors interpolated - * within a list of given color, using a linear scale. - * - * @author Jean-Yves Tinevez - Sept 2010 - */ -public class Colormap implements PaintScale, Serializable -{ - - private static final long serialVersionUID = 2977884191627862512L; - - private static final Color DEFAULT_COLOR = Color.BLACK; - - private final double lowerBound; - - private final double upperBound; - - private final TreeMap< Double, Color > colors = new TreeMap<>(); - - private final Color defaultColor; - - /* - * INNER CLASSES - */ - - - - /** - * An {@link Colormap} that maps a typical "Jet" colormap going - * from blue to red to the range [0, 1]. - */ - public static final Colormap Jet; - static - { - Jet = new Colormap( "Jet", 0., 1. ); - Jet.add( 0.00, new Color( 0.0f, 0.0f, 1.0f ) ); - Jet.add( 0.16, new Color( 0.0f, 0.5f, 1.0f ) ); - Jet.add( 0.33, new Color( 0.0f, 1.0f, 1.0f ) ); - Jet.add( 0.50, new Color( 0.5f, 1.0f, 0.5f ) ); - Jet.add( 0.66, new Color( 1.0f, 1.0f, 0.0f ) ); - Jet.add( 0.83, new Color( 1.0f, 0.5f, 0.0f ) ); - Jet.add( 1.00, new Color( 1.0f, 0.0f, 0.0f ) ); - } - - /** - * An {@link Colormap} that replicates the matplotlib "Viridis" - * colormap. - */ - public static final Colormap Viridis; - static - { - Viridis = new Colormap( "Viridis", 0., 1. ); - Viridis.add( 0.00, new Color( 68, 1, 84 ) ); - Viridis.add( 0.05, new Color( 71, 18, 101 ) ); - Viridis.add( 0.10, new Color( 72, 35, 116 ) ); - Viridis.add( 0.15, new Color( 69, 52, 127 ) ); - Viridis.add( 0.20, new Color( 64, 67, 135 ) ); - Viridis.add( 0.25, new Color( 58, 82, 139 ) ); - Viridis.add( 0.30, new Color( 52, 94, 141 ) ); - Viridis.add( 0.35, new Color( 46, 107, 142 ) ); - Viridis.add( 0.40, new Color( 41, 120, 142 ) ); - Viridis.add( 0.45, new Color( 36, 132, 141 ) ); - Viridis.add( 0.50, new Color( 32, 144, 140 ) ); - Viridis.add( 0.55, new Color( 30, 155, 137 ) ); - Viridis.add( 0.60, new Color( 34, 167, 132 ) ); - Viridis.add( 0.65, new Color( 47, 179, 123 ) ); - Viridis.add( 0.70, new Color( 68, 190, 112 ) ); - Viridis.add( 0.75, new Color( 94, 201, 97 ) ); - Viridis.add( 0.80, new Color( 121, 209, 81 ) ); - Viridis.add( 0.85, new Color( 154, 216, 60 ) ); - Viridis.add( 0.90, new Color( 189, 222, 38 ) ); - Viridis.add( 0.95, new Color( 223, 227, 24 ) ); - Viridis.add( 1.00, new Color( 253, 231, 36 ) ); - } - - /** - * The TURBO color-map, from Google LLC, Anton Mikhailov. - * https://gist.github.com/mikhailov-work/ee72ba4191942acecc03fe6da94fc73f - */ - public static final Colormap Turbo; - - private final String name; - static - { - final double[][] triplets = new double[][] { - { 0.18995, 0.07176, 0.23217 }, { 0.19483, 0.08339, 0.26149 }, { 0.19956, 0.09498, 0.29024 }, { 0.20415, 0.10652, 0.31844 }, { 0.20860, 0.11802, 0.34607 }, { 0.21291, 0.12947, 0.37314 }, { 0.21708, 0.14087, 0.39964 }, { 0.22111, 0.15223, 0.42558 }, { 0.22500, 0.16354, 0.45096 }, { 0.22875, 0.17481, 0.47578 }, { 0.23236, 0.18603, 0.50004 }, { 0.23582, 0.19720, 0.52373 }, { 0.23915, 0.20833, 0.54686 }, { 0.24234, 0.21941, 0.56942 }, { 0.24539, 0.23044, 0.59142 }, { 0.24830, 0.24143, 0.61286 }, { 0.25107, 0.25237, 0.63374 }, { 0.25369, 0.26327, 0.65406 }, { 0.25618, 0.27412, 0.67381 }, { 0.25853, 0.28492, 0.69300 }, { 0.26074, 0.29568, 0.71162 }, { 0.26280, 0.30639, 0.72968 }, { 0.26473, 0.31706, 0.74718 }, { 0.26652, 0.32768, 0.76412 }, - { 0.26816, 0.33825, 0.78050 }, { 0.26967, 0.34878, 0.79631 }, { 0.27103, 0.35926, 0.81156 }, { 0.27226, 0.36970, 0.82624 }, { 0.27334, 0.38008, 0.84037 }, { 0.27429, 0.39043, 0.85393 }, { 0.27509, 0.40072, 0.86692 }, { 0.27576, 0.41097, 0.87936 }, { 0.27628, 0.42118, 0.89123 }, { 0.27667, 0.43134, 0.90254 }, { 0.27691, 0.44145, 0.91328 }, { 0.27701, 0.45152, 0.92347 }, { 0.27698, 0.46153, 0.93309 }, { 0.27680, 0.47151, 0.94214 }, { 0.27648, 0.48144, 0.95064 }, { 0.27603, 0.49132, 0.95857 }, { 0.27543, 0.50115, 0.96594 }, { 0.27469, 0.51094, 0.97275 }, { 0.27381, 0.52069, 0.97899 }, { 0.27273, 0.53040, 0.98461 }, { 0.27106, 0.54015, 0.98930 }, { 0.26878, 0.54995, 0.99303 }, { 0.26592, 0.55979, 0.99583 }, { 0.26252, 0.56967, 0.99773 }, { 0.25862, 0.57958, 0.99876 }, - { 0.25425, 0.58950, 0.99896 }, { 0.24946, 0.59943, 0.99835 }, { 0.24427, 0.60937, 0.99697 }, { 0.23874, 0.61931, 0.99485 }, { 0.23288, 0.62923, 0.99202 }, { 0.22676, 0.63913, 0.98851 }, { 0.22039, 0.64901, 0.98436 }, { 0.21382, 0.65886, 0.97959 }, { 0.20708, 0.66866, 0.97423 }, { 0.20021, 0.67842, 0.96833 }, { 0.19326, 0.68812, 0.96190 }, { 0.18625, 0.69775, 0.95498 }, { 0.17923, 0.70732, 0.94761 }, { 0.17223, 0.71680, 0.93981 }, { 0.16529, 0.72620, 0.93161 }, { 0.15844, 0.73551, 0.92305 }, { 0.15173, 0.74472, 0.91416 }, { 0.14519, 0.75381, 0.90496 }, { 0.13886, 0.76279, 0.89550 }, { 0.13278, 0.77165, 0.88580 }, { 0.12698, 0.78037, 0.87590 }, { 0.12151, 0.78896, 0.86581 }, { 0.11639, 0.79740, 0.85559 }, { 0.11167, 0.80569, 0.84525 }, { 0.10738, 0.81381, 0.83484 }, - { 0.10357, 0.82177, 0.82437 }, { 0.10026, 0.82955, 0.81389 }, { 0.09750, 0.83714, 0.80342 }, { 0.09532, 0.84455, 0.79299 }, { 0.09377, 0.85175, 0.78264 }, { 0.09287, 0.85875, 0.77240 }, { 0.09267, 0.86554, 0.76230 }, { 0.09320, 0.87211, 0.75237 }, { 0.09451, 0.87844, 0.74265 }, { 0.09662, 0.88454, 0.73316 }, { 0.09958, 0.89040, 0.72393 }, { 0.10342, 0.89600, 0.71500 }, { 0.10815, 0.90142, 0.70599 }, { 0.11374, 0.90673, 0.69651 }, { 0.12014, 0.91193, 0.68660 }, { 0.12733, 0.91701, 0.67627 }, { 0.13526, 0.92197, 0.66556 }, { 0.14391, 0.92680, 0.65448 }, { 0.15323, 0.93151, 0.64308 }, { 0.16319, 0.93609, 0.63137 }, { 0.17377, 0.94053, 0.61938 }, { 0.18491, 0.94484, 0.60713 }, { 0.19659, 0.94901, 0.59466 }, { 0.20877, 0.95304, 0.58199 }, { 0.22142, 0.95692, 0.56914 }, - { 0.23449, 0.96065, 0.55614 }, { 0.24797, 0.96423, 0.54303 }, { 0.26180, 0.96765, 0.52981 }, { 0.27597, 0.97092, 0.51653 }, { 0.29042, 0.97403, 0.50321 }, { 0.30513, 0.97697, 0.48987 }, { 0.32006, 0.97974, 0.47654 }, { 0.33517, 0.98234, 0.46325 }, { 0.35043, 0.98477, 0.45002 }, { 0.36581, 0.98702, 0.43688 }, { 0.38127, 0.98909, 0.42386 }, { 0.39678, 0.99098, 0.41098 }, { 0.41229, 0.99268, 0.39826 }, { 0.42778, 0.99419, 0.38575 }, { 0.44321, 0.99551, 0.37345 }, { 0.45854, 0.99663, 0.36140 }, { 0.47375, 0.99755, 0.34963 }, { 0.48879, 0.99828, 0.33816 }, { 0.50362, 0.99879, 0.32701 }, { 0.51822, 0.99910, 0.31622 }, { 0.53255, 0.99919, 0.30581 }, { 0.54658, 0.99907, 0.29581 }, { 0.56026, 0.99873, 0.28623 }, { 0.57357, 0.99817, 0.27712 }, { 0.58646, 0.99739, 0.26849 }, - { 0.59891, 0.99638, 0.26038 }, { 0.61088, 0.99514, 0.25280 }, { 0.62233, 0.99366, 0.24579 }, { 0.63323, 0.99195, 0.23937 }, { 0.64362, 0.98999, 0.23356 }, { 0.65394, 0.98775, 0.22835 }, { 0.66428, 0.98524, 0.22370 }, { 0.67462, 0.98246, 0.21960 }, { 0.68494, 0.97941, 0.21602 }, { 0.69525, 0.97610, 0.21294 }, { 0.70553, 0.97255, 0.21032 }, { 0.71577, 0.96875, 0.20815 }, { 0.72596, 0.96470, 0.20640 }, { 0.73610, 0.96043, 0.20504 }, { 0.74617, 0.95593, 0.20406 }, { 0.75617, 0.95121, 0.20343 }, { 0.76608, 0.94627, 0.20311 }, { 0.77591, 0.94113, 0.20310 }, { 0.78563, 0.93579, 0.20336 }, { 0.79524, 0.93025, 0.20386 }, { 0.80473, 0.92452, 0.20459 }, { 0.81410, 0.91861, 0.20552 }, { 0.82333, 0.91253, 0.20663 }, { 0.83241, 0.90627, 0.20788 }, { 0.84133, 0.89986, 0.20926 }, - { 0.85010, 0.89328, 0.21074 }, { 0.85868, 0.88655, 0.21230 }, { 0.86709, 0.87968, 0.21391 }, { 0.87530, 0.87267, 0.21555 }, { 0.88331, 0.86553, 0.21719 }, { 0.89112, 0.85826, 0.21880 }, { 0.89870, 0.85087, 0.22038 }, { 0.90605, 0.84337, 0.22188 }, { 0.91317, 0.83576, 0.22328 }, { 0.92004, 0.82806, 0.22456 }, { 0.92666, 0.82025, 0.22570 }, { 0.93301, 0.81236, 0.22667 }, { 0.93909, 0.80439, 0.22744 }, { 0.94489, 0.79634, 0.22800 }, { 0.95039, 0.78823, 0.22831 }, { 0.95560, 0.78005, 0.22836 }, { 0.96049, 0.77181, 0.22811 }, { 0.96507, 0.76352, 0.22754 }, { 0.96931, 0.75519, 0.22663 }, { 0.97323, 0.74682, 0.22536 }, { 0.97679, 0.73842, 0.22369 }, { 0.98000, 0.73000, 0.22161 }, { 0.98289, 0.72140, 0.21918 }, { 0.98549, 0.71250, 0.21650 }, { 0.98781, 0.70330, 0.21358 }, - { 0.98986, 0.69382, 0.21043 }, { 0.99163, 0.68408, 0.20706 }, { 0.99314, 0.67408, 0.20348 }, { 0.99438, 0.66386, 0.19971 }, { 0.99535, 0.65341, 0.19577 }, { 0.99607, 0.64277, 0.19165 }, { 0.99654, 0.63193, 0.18738 }, { 0.99675, 0.62093, 0.18297 }, { 0.99672, 0.60977, 0.17842 }, { 0.99644, 0.59846, 0.17376 }, { 0.99593, 0.58703, 0.16899 }, { 0.99517, 0.57549, 0.16412 }, { 0.99419, 0.56386, 0.15918 }, { 0.99297, 0.55214, 0.15417 }, { 0.99153, 0.54036, 0.14910 }, { 0.98987, 0.52854, 0.14398 }, { 0.98799, 0.51667, 0.13883 }, { 0.98590, 0.50479, 0.13367 }, { 0.98360, 0.49291, 0.12849 }, { 0.98108, 0.48104, 0.12332 }, { 0.97837, 0.46920, 0.11817 }, { 0.97545, 0.45740, 0.11305 }, { 0.97234, 0.44565, 0.10797 }, { 0.96904, 0.43399, 0.10294 }, { 0.96555, 0.42241, 0.09798 }, - { 0.96187, 0.41093, 0.09310 }, { 0.95801, 0.39958, 0.08831 }, { 0.95398, 0.38836, 0.08362 }, { 0.94977, 0.37729, 0.07905 }, { 0.94538, 0.36638, 0.07461 }, { 0.94084, 0.35566, 0.07031 }, { 0.93612, 0.34513, 0.06616 }, { 0.93125, 0.33482, 0.06218 }, { 0.92623, 0.32473, 0.05837 }, { 0.92105, 0.31489, 0.05475 }, { 0.91572, 0.30530, 0.05134 }, { 0.91024, 0.29599, 0.04814 }, { 0.90463, 0.28696, 0.04516 }, { 0.89888, 0.27824, 0.04243 }, { 0.89298, 0.26981, 0.03993 }, { 0.88691, 0.26152, 0.03753 }, { 0.88066, 0.25334, 0.03521 }, { 0.87422, 0.24526, 0.03297 }, { 0.86760, 0.23730, 0.03082 }, { 0.86079, 0.22945, 0.02875 }, { 0.85380, 0.22170, 0.02677 }, { 0.84662, 0.21407, 0.02487 }, { 0.83926, 0.20654, 0.02305 }, { 0.83172, 0.19912, 0.02131 }, { 0.82399, 0.19182, 0.01966 }, - { 0.81608, 0.18462, 0.01809 }, { 0.80799, 0.17753, 0.01660 }, { 0.79971, 0.17055, 0.01520 }, { 0.79125, 0.16368, 0.01387 }, { 0.78260, 0.15693, 0.01264 }, { 0.77377, 0.15028, 0.01148 }, { 0.76476, 0.14374, 0.01041 }, { 0.75556, 0.13731, 0.00942 }, { 0.74617, 0.13098, 0.00851 }, { 0.73661, 0.12477, 0.00769 }, { 0.72686, 0.11867, 0.00695 }, { 0.71692, 0.11268, 0.00629 }, { 0.70680, 0.10680, 0.00571 }, { 0.69650, 0.10102, 0.00522 }, { 0.68602, 0.09536, 0.00481 }, { 0.67535, 0.08980, 0.00449 }, { 0.66449, 0.08436, 0.00424 }, { 0.65345, 0.07902, 0.00408 }, { 0.64223, 0.07380, 0.00401 }, { 0.63082, 0.06868, 0.00401 }, { 0.61923, 0.06367, 0.00410 }, { 0.60746, 0.05878, 0.00427 }, { 0.59550, 0.05399, 0.00453 }, { 0.58336, 0.04931, 0.00486 }, { 0.57103, 0.04474, 0.00529 }, - { 0.55852, 0.04028, 0.00579 }, { 0.54583, 0.03593, 0.00638 }, { 0.53295, 0.03169, 0.00705 }, { 0.51989, 0.02756, 0.00780 }, { 0.50664, 0.02354, 0.00863 }, { 0.49321, 0.01963, 0.00955 }, { 0.47960, 0.01583, 0.01055 } }; - Turbo = new Colormap( "Turbo", 0., 1. ); - final int nTriplets = triplets.length; - for ( int i = 0; i < nTriplets; i++ ) - { - final double alpha = ( i ) / ( nTriplets - 1. ); - final double[] triplet = triplets[ i ]; - Turbo.add( alpha, new Color( ( float ) triplet[ 0 ], ( float ) triplet[ 1 ], ( float ) triplet[ 2 ] ) ); - } - } - - private static final List< Colormap > LUTS; - static - { - final List< Colormap > tmpLUTS = new ArrayList<>(); - tmpLUTS.add( Jet ); - tmpLUTS.add( Turbo ); - tmpLUTS.add( Viridis ); - tmpLUTS.addAll( ColormapIO.getLUTs() ); - LUTS = Collections.unmodifiableList( tmpLUTS ); - } - - public static List< Colormap > getAvailableLUTs() - { - return LUTS; - } - - /* - * CONSTRUCTORS - */ - - /** - * Creates a paint scale with given lower and upper bound, and a specified - * default color. - * - * @param name - * the name of this colormap. - * @param lowerBound - * the lower bound of the scale. - * @param upperBound - * the upper bound of the scale. - * @param defaultColor - * the default color to return when no color is defined in the - * scale. - */ - public Colormap( final String name, final double lowerBound, final double upperBound, final Color defaultColor ) - { - this.name = name; - this.lowerBound = lowerBound; - this.upperBound = upperBound; - this.defaultColor = defaultColor; - } - - /** - * Creates a paint scale with a given lower and upper bound and a default - * black color. - * - * @param name - * the name of this colormap. - * @param lowerBound - * the lower bound of the scale. - * @param upperBound - * the upper bound of the scale. - */ - public Colormap( final String name, final double lowerBound, final double upperBound ) - { - this( name, lowerBound, upperBound, DEFAULT_COLOR ); - } - - /** - * Creates a paint scale with a lower bound of 0, an upper bound of 1 and a - * default black color. - * - * @param name - * the name of this colormap. - */ - public Colormap( final String name ) - { - this( name, 0, 1 ); - } - - /* - * PUBLIC METHODS - */ - - public String getName() - { - return name; - } - - /** - * Adds a color to the color list of this paint scale, at the position given - * by value. If value is greater than the upper - * bound or lower than the lower bound set at construction, this call will - * be ignored. - * - * @param value - * the value at which to add the color. - * @param color - * the color to add. - */ - public void add( final double value, final Color color ) - { - if ( value > upperBound ) - return; - if ( value < lowerBound ) - return; - colors.put( value, color ); - } - - @Override - public double getLowerBound() - { - return lowerBound; - } - - /** - * Return a color interpolated within the color list of this paint scale. - * The interpolation is a linear one between the two colors in the list - * whose associated values frame the one given. - */ - @Override - public Color getPaint( double value ) - { - if ( colors.isEmpty() ) - return defaultColor; - if ( colors.size() == 1 ) - return colors.get( colors.firstKey() ); - - if ( value > upperBound ) - value = upperBound; - if ( value < lowerBound ) - value = lowerBound; - final Set< Double > keys = colors.keySet(); - double bottom = colors.firstKey(); - double top = colors.lastKey(); - for ( final double key : keys ) - { - top = key; - if ( value < key ) - break; - - bottom = top; - } - - double alpha; - if ( top == bottom ) - alpha = 0; // we reached the end of the list - else - alpha = ( value - bottom ) / ( top - bottom ); - - final Color colorBottom = colors.get( bottom ); - final Color colorTop = colors.get( top ); - final int red = ( int ) ( ( 1 - alpha ) * colorBottom.getRed() + alpha * colorTop.getRed() ); - final int green = ( int ) ( ( 1 - alpha ) * colorBottom.getGreen() + alpha * colorTop.getGreen() ); - final int blue = ( int ) ( ( 1 - alpha ) * colorBottom.getBlue() + alpha * colorTop.getBlue() ); - return new Color( red, green, blue ); - } - - @Override - public double getUpperBound() - { - return upperBound; - } - - @Override - public Colormap clone() - { - final Colormap ips = new Colormap( name, lowerBound, upperBound ); - for ( final double key : colors.keySet() ) - ips.add( key, colors.get( key ) ); - return ips; - } - - - public static void main( final String[] args ) - { - final StringBuilder str = new StringBuilder(); - str.append( "{ " ); - getAvailableLUTs().forEach( ( cm ) -> str.append( '"' + cm.getName() + "\", " ) ); - str.deleteCharAt( str.length() - 1 ); - str.deleteCharAt( str.length() - 1 ); - str.append( " }" ); - System.out.println( str ); - } -} diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/ColormapIO.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/ColormapIO.java deleted file mode 100644 index 69864fc1b..000000000 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/ColormapIO.java +++ /dev/null @@ -1,169 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.gui.displaysettings; - -import java.awt.Color; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.DirectoryStream; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Scanner; -import java.util.concurrent.atomic.AtomicInteger; - -import org.scijava.util.IntArray; - -/** - * Loat LUTS for {@link Colormap}. Code adapted from what we did in Mastodon. - * - * @author Jean-Yves Tinevez 2019 - */ -public class ColormapIO -{ - - private static final List< URI > LUT_FOLDERS = new ArrayList<>(); - static - { - try - { - final URI BUILTIN_LUT_FOLDER = ColormapIO.class.getResource( "luts/" ).toURI(); - LUT_FOLDERS.add( BUILTIN_LUT_FOLDER ); - } - catch ( final URISyntaxException e ) - { - e.printStackTrace(); - } - } - - static List< Colormap > getLUTs() - { - return loadLUTs(); - } - - private static List< Colormap > loadLUTs() - { - final List< Colormap > luts = new ArrayList<>(); - for ( final URI lutFolder : LUT_FOLDERS ) - { - try - { - luts.addAll( loadLUTs( lutFolder ) ); - } - catch ( final IOException e ) - { - e.printStackTrace(); - } - } - return luts; - } - - private static List< Colormap > loadLUTs( final URI folder ) throws IOException - { - if ( folder.getScheme().equals( "jar" ) ) - { - // Try to read from within the jar file. - final String[] array = folder.toString().split( "!" ); - try (FileSystem fileSystem = FileSystems.newFileSystem( URI.create( array[ 0 ] ), Collections.emptyMap() )) - { - final Path folderPath = fileSystem.getPath( array[ 1 ] ); - return loadLUTs( folderPath ); - } - } - else - { - // Read from a standard folder. - final Path folderPath = Paths.get( folder ); - return loadLUTs( folderPath ); - } - } - - private static List< Colormap > loadLUTs( final Path folderPath ) throws IOException - { - final List< Colormap > luts = new ArrayList<>(); - if ( Files.exists( folderPath ) ) - { - final String glob = "*.lut"; - try (final DirectoryStream< Path > folderStream = Files.newDirectoryStream( folderPath, glob )) - { - for ( final Path path : folderStream ) - { - - final Colormap lut = importLUT( path ); - if ( null == lut ) - System.err.println( "Could not read LUT file: " + path + ". Skipping." ); - - luts.add( lut ); - } - } - } - return luts; - } - - private static final Colormap importLUT( final Path path ) throws IOException - { - final String fileName = path.getFileName().toString(); - final String lutName = fileName.substring( 0, fileName.indexOf( '.' ) ); - - try (final Scanner scanner = new Scanner( path )) - { - - final List< Color > colors = new ArrayList<>(); - final IntArray intAlphas = new IntArray(); - final AtomicInteger nLines = new AtomicInteger( 0 ); - - final Colormap ips = new Colormap( lutName, 0., 1. ); - while ( scanner.hasNext() ) - { - if ( !scanner.hasNextInt() ) - { - scanner.next(); - continue; - } - intAlphas.addValue( scanner.nextInt() ); - final Color color = new Color( scanner.nextInt(), scanner.nextInt(), scanner.nextInt() ); - colors.add( color ); - nLines.incrementAndGet(); - } - - if ( nLines.get() < 2 ) - return null; - - final double[] alphas = new double[ intAlphas.size() ]; - for ( int i = 0; i < alphas.length; i++ ) - { - final double alpha = ( double ) intAlphas.get( i ) / ( nLines.get() - 1 ); - final Color color = colors.get( i ); - ips.add( alpha, color ); - } - - return ips; - } - } - -} diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/ConfigTrackMateDisplaySettings.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/ConfigTrackMateDisplaySettings.java deleted file mode 100644 index 9e90ce88e..000000000 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/ConfigTrackMateDisplaySettings.java +++ /dev/null @@ -1,150 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.gui.displaysettings; - -import static fiji.plugin.trackmate.gui.Icons.APPLY_ICON; -import static fiji.plugin.trackmate.gui.Icons.RESET_ICON; -import static fiji.plugin.trackmate.gui.Icons.REVERT_ICON; -import static fiji.plugin.trackmate.gui.Icons.TRACKMATE_ICON; - -import java.awt.BorderLayout; -import java.awt.Dimension; - -import javax.swing.BorderFactory; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.JButton; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.UIManager; -import javax.swing.UnsupportedLookAndFeelException; - -import org.scijava.command.Command; -import org.scijava.plugin.Plugin; - -import ij.ImageJ; - -@Plugin( type = Command.class, - label = "Configure TrackMate display settings...", - iconPath = "/icons/commands/information.png", - menuPath = "Edit > Options > Configure TrackMate display settings..." ) -public class ConfigTrackMateDisplaySettings implements Command -{ - - private static final String APPLY_TOOLTIP = "Save the current settings to the user default settings. " - + "They will be used in all the following TrackMate sessions."; - private static final String REVERT_TOOLTIP = "Revert the current settings to the ones saved in the " - + "user default settings file."; - private static final String RESET_TOOLTIP = "Reset the current settings to the built-in defaults."; - - @Override - public void run() - { - editor( DisplaySettingsIO.readUserDefault(), - "Configure the default settings to be used by TrackMate.", - "TrackMate user default settings" ).setVisible( true ); - } - - public static JFrame editor( final DisplaySettings ds, final String titleStr, final String frameName ) - { - final JPanel configPanel = new JPanel(); - configPanel.setLayout( new BorderLayout() ); - - /* - * Title. - */ - - final JLabel title = new JLabel( "" - + titleStr - + "" ); - title.setBorder( BorderFactory.createEmptyBorder( 10, 5, 10, 5 ) ); - configPanel.add( title, BorderLayout.NORTH ); - - /* - * Buttons. - */ - - final JPanel panelButton = new JPanel(); - final BoxLayout panelButtonLayout = new BoxLayout( panelButton, BoxLayout.LINE_AXIS ); - panelButton.setLayout( panelButtonLayout ); - final JButton btnReset = new JButton( "Reset", RESET_ICON ); - btnReset.setToolTipText( RESET_TOOLTIP ); - final JButton btnRevert = new JButton( "Revert", REVERT_ICON ); - btnRevert.setToolTipText( REVERT_TOOLTIP ); - final JButton btnApply = new JButton( "Save to user defaults", APPLY_ICON ); - btnApply.setToolTipText( APPLY_TOOLTIP ); - panelButton.add( btnReset ); - panelButton.add( Box.createHorizontalStrut( 5 ) ); - panelButton.add( btnRevert ); - panelButton.add( Box.createHorizontalGlue() ); - panelButton.add( btnApply ); - panelButton.setBorder( BorderFactory.createEmptyBorder( 10, 5, 10, 5 ) ); - configPanel.add( panelButton, BorderLayout.SOUTH ); - - /* - * Display settings editor. - */ - - final DisplaySettingsPanel editor = new DisplaySettingsPanel( ds ); - final JScrollPane scrollPane = new JScrollPane( editor, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER ); - scrollPane.setPreferredSize( new Dimension( 350, 500 ) ); - scrollPane.getVerticalScrollBar().setUnitIncrement( 16 ); - configPanel.add( scrollPane, BorderLayout.CENTER ); - - /* - * Listeners. - */ - - btnReset.addActionListener( e -> { - ds.set( DisplaySettings.defaultStyle().copy( "User-default" ) ); - title.setText( "Reset the current settings to the built-in defaults." ); - } ); - btnRevert.addActionListener( e -> { - ds.set( DisplaySettingsIO.readUserDefault() ); - title.setText( "Reverted the current settings to the user defaults." ); - } ); - btnApply.addActionListener( e -> { - DisplaySettingsIO.saveToUserDefault( ds ); - title.setText( "Saved the current settings to the user defaults file." ); - } ); - - /* - * Create and show frame. - */ - - final JFrame frame = new JFrame( frameName ); - frame.setIconImage( TRACKMATE_ICON.getImage() ); - frame.getContentPane().add( configPanel ); - frame.pack(); - frame.setLocationRelativeTo( null ); - return frame; - } - - public static void main( final String[] args ) throws ClassNotFoundException, InstantiationException, IllegalAccessException, UnsupportedLookAndFeelException - { - UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); - ImageJ.main( args ); - new ConfigTrackMateDisplaySettings().run(); - } -} diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettings.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettings.java index e6172f42b..e616b583c 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettings.java +++ b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettings.java @@ -28,10 +28,12 @@ import java.util.Objects; import org.scijava.listeners.Listeners; +import org.scijava.ui.config.visitors.gui.elements.colormap.Colormap; +import bdv.ui.settings.style.Style; import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; -public class DisplaySettings +public class DisplaySettings implements Style< DisplaySettings > { /** The display settings name. */ @@ -148,6 +150,7 @@ private DisplaySettings( final String name ) * the name for the copied render settings. * @return a new {@link DisplaySettings} instance. */ + @Override public DisplaySettings copy( final String name ) { final DisplaySettings rs = new DisplaySettings(); @@ -157,6 +160,7 @@ public DisplaySettings copy( final String name ) return rs; } + @Override public DisplaySettings copy() { return copy( null ); @@ -208,11 +212,13 @@ synchronized void set( final DisplaySettings ds ) notifyListeners(); } + @Override public String getName() { return name; } + @Override public synchronized void setName( final String name ) { if ( !Objects.equals( this.name, name ) ) @@ -788,7 +794,7 @@ public enum TrackMateObject { DEFAULT( "Default" ), SPOTS( "spots" ), EDGES( "edges" ), TRACKS( "tracks" ); - private String name; + private final String name; private TrackMateObject( final String name ) { diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettingsConfigPage.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettingsConfigPage.java new file mode 100644 index 000000000..0918ba7fd --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettingsConfigPage.java @@ -0,0 +1,53 @@ +package fiji.plugin.trackmate.gui.displaysettings; + +import java.awt.BorderLayout; +import java.awt.Frame; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.JDialog; +import javax.swing.WindowConstants; + +import bdv.ui.settings.SelectAndEditProfileSettingsPage; +import bdv.ui.settings.SettingsPanel; +import bdv.ui.settings.style.StyleProfile; +import bdv.ui.settings.style.StyleProfileManager; + +public class DisplaySettingsConfigPage extends SelectAndEditProfileSettingsPage< StyleProfile< DisplaySettings > > +{ + + public DisplaySettingsConfigPage( final String treePath, final DisplaySettingsManager displaySettingsManager ) + { + super( + treePath, + new StyleProfileManager<>( displaySettingsManager, new DisplaySettingsManager( null, false ) ), + new DisplaySettingsPanel( displaySettingsManager.getSelectedStyle() ) ); + } + + public static void main( final String[] args ) + { + final DisplaySettingsManager styleManager = new DisplaySettingsManager(); + + final SettingsPanel settings = new SettingsPanel(); + settings.addPage( new DisplaySettingsConfigPage( "Display settings", styleManager ) ); + + final JDialog dialog = new JDialog( ( Frame ) null, "Settings" ); + dialog.getContentPane().add( settings, BorderLayout.CENTER ); + dialog.pack(); + dialog.setLocationRelativeTo( null ); + + settings.onOk( () -> dialog.setVisible( false ) ); + settings.onCancel( () -> dialog.setVisible( false ) ); + + dialog.setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE ); + dialog.addWindowListener( new WindowAdapter() + { + @Override + public void windowClosing( final WindowEvent e ) + { + settings.cancel(); + } + } ); + dialog.setVisible( true ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettingsIO.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettingsIO.java index 2e597c044..a2cbf1b37 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettingsIO.java +++ b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettingsIO.java @@ -24,7 +24,6 @@ import java.awt.Color; import java.awt.Font; import java.io.File; -import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; @@ -34,6 +33,7 @@ import java.util.stream.Collectors; import org.jdom2.Element; +import org.scijava.ui.config.visitors.gui.elements.colormap.Colormap; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -51,8 +51,6 @@ public class DisplaySettingsIO { - private static File userDefaultFile = new File( new File( System.getProperty( "user.home" ), ".trackmate" ), "userdefaultsettings.json" ); - public static void toXML( final DisplaySettings ds, final Element dsel ) { dsel.setText( toJson( ds ) ); @@ -65,7 +63,9 @@ public static String toJson( final DisplaySettings ds ) public static DisplaySettings fromJson( final String str ) { - final DisplaySettings ds = ( str == null || str.isEmpty() ) ? readUserDefault() : getGson().fromJson( str, DisplaySettings.class ); + final DisplaySettings ds = ( str == null || str.isEmpty() ) + ? DisplaySettings.defaultStyle().copy() + : getGson().fromJson( str, DisplaySettings.class ); // Sanitize min and max. final double spotMin = ds.getSpotMin(); @@ -88,53 +88,46 @@ private static Gson getGson() return builder.setPrettyPrinting().create(); } - public static void saveToUserDefault( final DisplaySettings ds ) + public static void write( final DisplaySettings ds, final String path ) { final String str = toJson( ds ); + final File file = new File( path ); - if ( !userDefaultFile.exists() ) - userDefaultFile.getParentFile().mkdirs(); + if ( !file.exists() ) + file.getParentFile().mkdirs(); - try (FileWriter writer = new FileWriter( userDefaultFile )) + try (FileWriter writer = new FileWriter( file )) { writer.append( str ); } catch ( final IOException e ) { - System.err.println( "Could not write the user default settings to " + userDefaultFile ); + System.err.println( "Could not write the settings to " + file ); e.printStackTrace(); } } - public static DisplaySettings readUserDefault() + public static DisplaySettings read( final String path ) { - if ( !userDefaultFile.exists() ) - { - final DisplaySettings ds = DisplaySettings.defaultStyle().copy( "User-default" ); - saveToUserDefault( ds ); - return ds; - } - - try (FileReader reader = new FileReader( userDefaultFile )) + try (FileReader reader = new FileReader( path )) { - final String str = Files.lines( Paths.get( userDefaultFile.getAbsolutePath() ) ) + final String str = Files.lines( Paths.get( path ) ) .collect( Collectors.joining( System.lineSeparator() ) ); return fromJson( str ); } - catch ( final FileNotFoundException e ) - { - System.err.println( "Could not find the user default settings file: " + userDefaultFile - + ". Using built-in default setting." ); - e.printStackTrace(); - } catch ( final IOException e ) { - System.err.println( "Could not read the user default settings file: " + userDefaultFile - + ". Using built-in default setting." ); + System.err.println( "Could not read the file: " + path ); e.printStackTrace(); } - return DisplaySettings.defaultStyle().copy(); + return null; + } + + public static DisplaySettings readUserDefault() + { + return new DisplaySettingsManager().getInstance().copy(); + } /** @@ -254,9 +247,4 @@ public Color deserialize( final JsonElement json, final Type typeOfT, final Json } } } - - public static void main( final String[] args ) - { - System.out.println( readUserDefault() ); - } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettingsManager.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettingsManager.java new file mode 100644 index 000000000..c739185a1 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettingsManager.java @@ -0,0 +1,199 @@ +package fiji.plugin.trackmate.gui.displaysettings; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import bdv.ui.settings.style.AbstractStyleManager; +import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackDisplayMode; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; + +public class DisplaySettingsManager extends AbstractStyleManager< DisplaySettingsManager, DisplaySettings > +{ + + private static final String DISPLAY_SETTINGS_FOLDER = new File( new File( System.getProperty( "user.home" ), ".trackmate" ), "displaysettings" ).getAbsolutePath(); + + private static final String SELECTED_STYLE_FILENAME = "selected.txt"; + + private final DisplaySettings forwardDefaultStyle; + + private final DisplaySettings.UpdateListener updateForwardDefaultListeners; + + /** + * Creates a new DisplaySettingsManager. + * + * @param instance + * the style that will be managed by this manager. If + * null, a new instance will be created with the + * default style. + * @param loadStyles + * if true, the styles will be loaded from the + * {@link #DISPLAY_SETTINGS_FOLDER} folder. + */ + public DisplaySettingsManager( final DisplaySettings instance, final boolean loadStyles ) + { + final boolean instanceProvided = ( null != instance ); + if ( instanceProvided ) + forwardDefaultStyle = instance; + else + forwardDefaultStyle = DisplaySettings.defaultStyle().copy(); + + updateForwardDefaultListeners = () -> forwardDefaultStyle.set( selectedStyle ); + selectedStyle.listeners().add( updateForwardDefaultListeners ); + if ( loadStyles ) + loadStyles( instanceProvided ); + } + + /** + * Creates a new DisplaySettingsManager and loads the styles from the + * {@link #DISPLAY_SETTINGS_FOLDER} folder. The {@link #getInstance()} will + * be set to the last configured style by the user. + */ + public DisplaySettingsManager() + { + this( null, true ); + } + + /** + * Exposes the style that is managed by this manager. This instance will be + * modified by the settings editor using this manager. + * + * @return the style that is managed by this manager. + */ + public DisplaySettings getInstance() + { + return forwardDefaultStyle; + } + + @Override + public synchronized void setSelectedStyle( final DisplaySettings ds ) + { + selectedStyle.listeners().remove( updateForwardDefaultListeners ); + selectedStyle = ds; + forwardDefaultStyle.set( selectedStyle ); + selectedStyle.listeners().add( updateForwardDefaultListeners ); + } + + @Override + protected List< DisplaySettings > loadBuiltinStyles() + { + final DisplaySettings ds1 = DisplaySettings.defaultStyle(); + + final DisplaySettings ds2 = ds1.copy( "Color by track" ); + ds2.setTrackColorBy( TrackMateObject.TRACKS, TrackIndexAnalyzer.TRACK_INDEX ); + ds2.setSpotColorBy( TrackMateObject.TRACKS, TrackIndexAnalyzer.TRACK_INDEX ); + + final DisplaySettings ds3 = ds2.copy( "Dragon tail" ); + ds3.setLineThickness( 2. ); + ds3.setTrackDisplayMode( TrackDisplayMode.LOCAL_BACKWARD ); + ds3.setSpotFilled( true ); + ds3.setSpotTransparencyAlpha( 0.8 ); + + return List.of( ds1, ds2, ds3 ); + } + + public void loadStyles( final boolean instanceProvided ) + { + loadStyles( DISPLAY_SETTINGS_FOLDER, instanceProvided ); + } + + @Override + public void saveStyles() + { + saveStyles( DISPLAY_SETTINGS_FOLDER ); + } + + public void loadStyles( final String folder, final boolean instanceProvided ) + { + // Load the selected style name from the text file + final File selectedFile = new File( folder, SELECTED_STYLE_FILENAME ); + String selectedName = null; + try + { + selectedName = Files.readString( selectedFile.toPath() ).trim(); + } + catch ( final IOException e ) + {} + + if ( !instanceProvided ) + setSelectedStyle( builtinStyles.get( 0 ) ); + + userStyles.clear(); + final Set< String > names = builtinStyles.stream().map( DisplaySettings::getName ).collect( Collectors.toSet() ); + + // Get all JSon files in the folder + final File[] files = new File( folder ).listFiles( ( dir, name ) -> name.toLowerCase().endsWith( ".json" ) ); + if ( files == null ) + return; + + // Read each file and add it if it is valid and has a unique name. + for ( final File file : files ) + { + final DisplaySettings ds = DisplaySettingsIO.read( file.getAbsolutePath() ); + if ( ds == null ) + continue; + if ( names.contains( ds.getName() ) ) + { + System.err.println( "Discarded settings with duplicate name \"" + ds.getName() + "\"." ); + continue; + } + userStyles.add( ds ); + if ( ds.getName().equals( selectedName ) && !instanceProvided ) + setSelectedStyle( ds ); + } + + if ( !instanceProvided ) + { + for ( final DisplaySettings ds : builtinStyles ) + { + if ( ds.getName().equals( selectedName ) ) + setSelectedStyle( ds ); + } + } + else + { + final DisplaySettings copy = forwardDefaultStyle.copy(); + userStyles.removeIf( ds -> ds.getName().equals( copy.getName() ) ); + userStyles.add( copy ); + setSelectedStyle( copy ); + } + } + + public void saveStyles( final String folder ) + { + new File( folder ).mkdirs(); + + // Save what style is selected in a text file + final File selectedFile = new File( folder, SELECTED_STYLE_FILENAME ); + try + { + Files.writeString( selectedFile.toPath(), selectedStyle.getName() ); + } + catch ( final IOException e ) + { + e.printStackTrace(); + } + + // List all json files in the folder and delete those that do not + // correspond to a user style. + final File[] files = new File( folder ).listFiles( ( dir, name ) -> name.toLowerCase().endsWith( ".json" ) ); + if ( files != null ) + { + final Set< String > userStyleNames = userStyles.stream().map( DisplaySettings::getName ).collect( Collectors.toSet() ); + for ( final File file : files ) + { + final String filename = file.getName().substring( 0, file.getName().length() - 5 ); + if ( !userStyleNames.contains( filename ) ) + file.delete(); + } + } + + // Save all user styles to the folder + for ( final DisplaySettings ds : userStyles ) + DisplaySettingsIO.write( ds, new File( folder, ds.getName() + ".json" ).getAbsolutePath() ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettingsPanel.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettingsPanel.java index a8863f485..7c96ee44f 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettingsPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/DisplaySettingsPanel.java @@ -21,26 +21,28 @@ */ package fiji.plugin.trackmate.gui.displaysettings; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.booleanElement; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.boundedDoubleElement; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.colorElement; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.colormapElement; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.doubleElement; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.enumElement; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.featureElement; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.fontElement; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.intElement; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.label; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.linkedCheckBox; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.linkedColorButton; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.linkedColormapChooser; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.linkedComboBoxEnumSelector; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.linkedFeatureSelector; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.linkedFontButton; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.linkedFormattedTextField; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.linkedSliderPanel; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.separator; - +import static fiji.plugin.trackmate.gui.displaysettings.TrackMateStyleElements.featureElement; +import static fiji.plugin.trackmate.gui.displaysettings.TrackMateStyleElements.linkedFeatureSelector; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.booleanElement; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.boundedDoubleElement; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.colorElement; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.colormapElement; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.doubleElement; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.enumElement; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.fontElement; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.intElement; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.label; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.linkedCheckBox; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.linkedColorButton; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.linkedColormapChooser; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.linkedComboBoxEnumSelector; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.linkedFontButton; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.linkedFormattedTextField; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.linkedSliderPanel; +import static org.scijava.ui.config.visitors.gui.elements.StyleElements.separator; + +import java.awt.BorderLayout; +import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; @@ -53,194 +55,105 @@ import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JScrollPane; import javax.swing.JSeparator; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; +import org.scijava.listeners.Listeners; +import org.scijava.ui.config.visitors.gui.elements.StyleElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.BooleanElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.BoundedDoubleElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.ColorElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.ColormapElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.DoubleElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.EnumElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.FontElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.IntElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.LabelElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.Separator; + import com.itextpdf.text.Font; +import bdv.ui.settings.ModificationListener; +import bdv.ui.settings.SelectAndEditProfileSettingsPage.ProfileEditPanel; +import bdv.ui.settings.style.StyleProfile; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackDisplayMode; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.BooleanElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.BoundedDoubleElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.ColorElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.ColormapElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.DoubleElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.EnumElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.FeatureElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.FontElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.IntElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.LabelElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.Separator; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.StyleElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.StyleElementVisitor; - -public class DisplaySettingsPanel extends JPanel +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.UpdateListener; +import fiji.plugin.trackmate.gui.displaysettings.TrackMateStyleElements.FeatureElement; +import fiji.plugin.trackmate.gui.displaysettings.TrackMateStyleElements.TrackMateStyleElementVisitor; + +public class DisplaySettingsPanel extends JPanel implements ProfileEditPanel< StyleProfile< DisplaySettings > >, UpdateListener { private static final long serialVersionUID = 1L; private static final int tfCols = 4; - private final JColorChooser colorChooser; + private final Listeners.SynchronizedList< ModificationListener > modificationListeners; private final List< StyleElement > styleElements; + private final DisplaySettings editedStyle; + public DisplaySettingsPanel( final DisplaySettings ds ) { - super( new GridBagLayout() ); + super( new BorderLayout() ); + + this.editedStyle = ds.copy( "Edited" ); + this.styleElements = styleElements( editedStyle ); + this.modificationListeners = new Listeners.SynchronizedList<>(); + editedStyle.listeners().add( this ); + + final JPanel panel = new JPanel( new GridBagLayout() ); + final GuiVisitor visitor = new GuiVisitor( panel ); + styleElements.forEach( element -> element.accept( visitor ) ); + final JScrollPane scrollPane = new JScrollPane( panel, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER ); + scrollPane.getVerticalScrollBar().setUnitIncrement( 8 ); + add( scrollPane, BorderLayout.CENTER ); + setPreferredSize( new Dimension( 350, 500 ) ); + } - colorChooser = new JColorChooser(); - styleElements = styleElements( ds ); + @Override + public Listeners< ModificationListener > modificationListeners() + { + return modificationListeners; + } - ds.listeners().add( () -> { - styleElements.forEach( StyleElement::update ); - repaint(); - } ); - - final GridBagConstraints c = new GridBagConstraints(); - c.fill = GridBagConstraints.HORIZONTAL; - c.weightx = 1.0; - c.gridwidth = 1; - c.gridx = 0; - c.gridy = 0; - - c.insets = new Insets( 2, 5, 2, 5 ); - - styleElements.forEach( element -> element.accept( - new StyleElementVisitor() - { - @Override - public void visit( final Separator element ) - { - add( Box.createVerticalStrut( 10 ), c ); - ++c.gridy; - addToLayout( new JSeparator( JSeparator.HORIZONTAL ) ); - } - - @Override - public void visit( final LabelElement element ) - { - final JLabel label = new JLabel( element.getLabel() ); - label.setFont( getFont().deriveFont( Font.BOLD ).deriveFont( getFont().getSize() + 2f ) ); - addToLayout( label ); - } - - @Override - public void visit( final BooleanElement element ) - { - final JCheckBox checkbox = linkedCheckBox( element, "" ); - checkbox.setHorizontalAlignment( SwingConstants.TRAILING ); - addToLayout( - checkbox, - new JLabel( element.getLabel() ) ); - } - - @Override - public void visit( final BoundedDoubleElement element ) - { - addToLayout( - linkedSliderPanel( element, tfCols, 0.1 ), - new JLabel( element.getLabel() ) ); - } - - @Override - public void visit( final DoubleElement element ) - { - addToLayout( - linkedFormattedTextField( element ), - new JLabel( element.getLabel() ) ); - } - - @Override - public void visit( final IntElement element ) - { - addToLayout( - linkedSliderPanel( element, tfCols ), - new JLabel( element.getLabel() ) ); - } - - @Override - public void visit( final ColorElement element ) - { - addToLayoutFlushRight( - linkedColorButton( element, colorChooser ), - new JLabel( element.getLabel() ) ); - } - - @Override - public void visit( final FeatureElement element ) - { - addToLayout( - linkedFeatureSelector( element ), - new JLabel( element.getLabel() ) ); - } - - @Override - public < E > void visit( final EnumElement< E > element ) - { - addToLayout( - linkedComboBoxEnumSelector( element ), - new JLabel( element.getLabel() ) ); - } - - @Override - public void visit( final ColormapElement element ) - { - addToLayout( - linkedColormapChooser( element ), - new JLabel( element.getLabel() ) ); - } - - @Override - public void visit( final FontElement element ) - { - addToLayout( - linkedFontButton( element, SwingUtilities.getWindowAncestor( DisplaySettingsPanel.this ) ), - new JLabel( element.getLabel() ) ); - } - - private void addToLayout( final JComponent comp1, final JComponent comp2 ) - { - c.gridwidth = 1; - c.anchor = GridBagConstraints.LINE_END; - add( comp1, c ); - c.gridx++; - c.weightx = 0.0; - c.anchor = GridBagConstraints.LINE_START; - add( comp2, c ); - c.gridx = 0; - c.weightx = 1.0; - c.gridy++; - } - - private void addToLayoutFlushRight( final JComponent comp1, final JComponent comp2 ) - { - c.fill = - c.gridwidth = 1; - c.fill = GridBagConstraints.NONE; - c.anchor = GridBagConstraints.EAST; - add( comp1, c ); - c.gridx++; - c.weightx = 0.0; - c.fill = GridBagConstraints.HORIZONTAL; - c.anchor = GridBagConstraints.LINE_START; - add( comp2, c ); - c.gridx = 0; - c.weightx = 1.0; - c.gridy++; - } - - private void addToLayout( final JComponent comp ) - { - c.gridwidth = 2; - c.anchor = GridBagConstraints.LINE_START; - c.gridx = 0; - c.weightx = 1.0; - add( comp, c ); - c.gridy++; - } - } ) ); + @Override + public JPanel getJPanel() + { + return this; } + private boolean trackModifications = true; + + @Override + public void loadProfile( final StyleProfile< DisplaySettings > profile ) + { + trackModifications = false; + editedStyle.set( profile.getStyle() ); + trackModifications = true; + } + + @Override + public void storeProfile( final StyleProfile< DisplaySettings > profile ) + { + trackModifications = false; + editedStyle.setName( profile.getStyle().getName() ); + trackModifications = true; + profile.getStyle().set( editedStyle ); + } + + @Override + public void displaySettingsChanged() + { + styleElements.forEach( StyleElement::update ); + if ( trackModifications ) + { + repaint(); + modificationListeners.list.forEach( ModificationListener::setModified ); + } + } private List< StyleElement > styleElements( final DisplaySettings ds ) { @@ -303,4 +216,157 @@ private List< StyleElement > styleElements( final DisplaySettings ds ) separator() ); } + + private static class GuiVisitor implements TrackMateStyleElementVisitor + { + + private final JPanel panel; + + private final GridBagConstraints c; + + private final JColorChooser colorChooser; + + public GuiVisitor( final JPanel panel ) + { + this.panel = panel; + this.colorChooser = new JColorChooser(); + this.c = new GridBagConstraints(); + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1.0; + c.gridwidth = 1; + c.gridx = 0; + c.gridy = 0; + c.insets = new Insets( 2, 5, 2, 5 ); + } + + @Override + public void visit( final Separator element ) + { + panel.add( Box.createVerticalStrut( 10 ), c ); + ++c.gridy; + addToLayout( new JSeparator( JSeparator.HORIZONTAL ) ); + } + + @Override + public void visit( final LabelElement element ) + { + final JLabel label = new JLabel( element.getLabel() ); + label.setFont( panel.getFont().deriveFont( Font.BOLD ).deriveFont( panel.getFont().getSize() + 2f ) ); + addToLayout( label ); + } + + @Override + public void visit( final BooleanElement element ) + { + final JCheckBox checkbox = linkedCheckBox( element, "" ); + checkbox.setHorizontalAlignment( SwingConstants.TRAILING ); + addToLayout( + checkbox, + new JLabel( element.getLabel() ) ); + } + + @Override + public void visit( final BoundedDoubleElement element ) + { + addToLayout( + linkedSliderPanel( element, tfCols, 0.1 ), + new JLabel( element.getLabel() ) ); + } + + @Override + public void visit( final DoubleElement element ) + { + addToLayout( + linkedFormattedTextField( element, null, null ), + new JLabel( element.getLabel() ) ); + } + + @Override + public void visit( final IntElement element ) + { + addToLayout( + linkedSliderPanel( element, tfCols ), + new JLabel( element.getLabel() ) ); + } + + @Override + public void visit( final ColorElement element ) + { + addToLayoutFlushRight( + linkedColorButton( element, colorChooser ), + new JLabel( element.getLabel() ) ); + } + + @Override + public void visit( final FeatureElement element ) + { + addToLayout( + linkedFeatureSelector( element ), + new JLabel( element.getLabel() ) ); + } + + @Override + public < E > void visit( final EnumElement< E > element ) + { + addToLayout( + linkedComboBoxEnumSelector( element ), + new JLabel( element.getLabel() ) ); + } + + @Override + public void visit( final ColormapElement element ) + { + addToLayout( + linkedColormapChooser( element ), + new JLabel( element.getLabel() ) ); + } + + @Override + public void visit( final FontElement element ) + { + addToLayout( + linkedFontButton( element, SwingUtilities.getWindowAncestor( panel ) ), + new JLabel( element.getLabel() ) ); + } + + private void addToLayout( final JComponent comp1, final JComponent comp2 ) + { + c.gridwidth = 1; + c.anchor = GridBagConstraints.LINE_END; + panel.add( comp1, c ); + c.gridx++; + c.weightx = 0.0; + c.anchor = GridBagConstraints.LINE_START; + panel.add( comp2, c ); + c.gridx = 0; + c.weightx = 1.0; + c.gridy++; + } + + private void addToLayoutFlushRight( final JComponent comp1, final JComponent comp2 ) + { + c.fill = c.gridwidth = 1; + c.fill = GridBagConstraints.NONE; + c.anchor = GridBagConstraints.EAST; + panel.add( comp1, c ); + c.gridx++; + c.weightx = 0.0; + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.LINE_START; + panel.add( comp2, c ); + c.gridx = 0; + c.weightx = 1.0; + c.gridy++; + } + + private void addToLayout( final JComponent comp ) + { + c.gridwidth = 2; + c.anchor = GridBagConstraints.LINE_START; + c.gridx = 0; + c.weightx = 1.0; + panel.add( comp, c ); + c.gridy++; + } + } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/SliderPanel.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/SliderPanel.java deleted file mode 100644 index bcb0361fb..000000000 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/SliderPanel.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.gui.displaysettings; - -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.event.MouseWheelEvent; -import java.awt.event.MouseWheelListener; - -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSlider; -import javax.swing.JSpinner; -import javax.swing.SpinnerNumberModel; -import javax.swing.SwingConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -/** - * A {@link JSlider} with a {@link JSpinner} next to it, both modifying the same - * {@link BoundedValue value}. - */ -public class SliderPanel extends JPanel implements BoundedValue.UpdateListener -{ - private static final long serialVersionUID = 6444334522127424416L; - - public static final Dimension PANEL_SIZE = new Dimension( 150, 20 ); - - private final JSlider slider; - - private final JSpinner spinner; - - private final BoundedValue model; - - /** - * Create a {@link SliderPanel} to modify a given {@link BoundedValue - * value}. - * - * @param name - * label to show next to the slider. - * @param model - * the value that is modified. - * @param spinnerStepSize - * the step size for the spinner. - */ - public SliderPanel( final String name, final BoundedValue model, final int spinnerStepSize ) - { - super(); - setLayout( new BorderLayout( 10, 10 ) ); - setPreferredSize( PANEL_SIZE ); - - slider = new JSlider( SwingConstants.HORIZONTAL, model.getRangeMin(), model.getRangeMax(), model.getCurrentValue() ); - spinner = new JSpinner(); - - final int min = model.getRangeMin(); - final int max = model.getRangeMax(); - final int val = Math.max( Math.min( model.getCurrentValue(), max ), min ); - spinner.setModel( new SpinnerNumberModel( val, min, max, spinnerStepSize ) ); - - slider.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( final ChangeEvent e ) - { - final int value = slider.getValue(); - model.setCurrentValue( value ); - } - } ); - - final MouseWheelListener mwl = new MouseWheelListener() - { - - @Override - public void mouseWheelMoved( final MouseWheelEvent e ) - { - final int notches = e.getWheelRotation(); - int value = slider.getValue(); - value -= notches * spinnerStepSize; - if ( value < slider.getMinimum() ) - value = slider.getMinimum(); - else if ( value > slider.getMaximum() ) - value = slider.getMaximum(); - slider.setValue( value ); - } - }; - slider.addMouseWheelListener( mwl ); - spinner.addMouseWheelListener( mwl ); - - spinner.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( final ChangeEvent e ) - { - final int value = ( ( Integer ) spinner.getValue() ).intValue(); - model.setCurrentValue( value ); - } - } ); - - if ( name != null ) - { - final JLabel label = new JLabel( name, SwingConstants.CENTER ); - label.setAlignmentX( Component.CENTER_ALIGNMENT ); - add( label, BorderLayout.WEST ); - } - - add( slider, BorderLayout.CENTER ); - add( spinner, BorderLayout.EAST ); - - this.model = model; - model.setUpdateListener( this ); - } - - public void setNumColummns( final int cols ) - { - ( ( JSpinner.NumberEditor ) spinner.getEditor() ).getTextField().setColumns( cols ); - } - - @Override - public void setFont( final Font font ) - { - super.setFont( font ); - if ( spinner != null ) - spinner.setFont( font ); - if ( slider != null ) - slider.setFont( font ); - } - - @Override - public void setToolTipText( final String text ) - { - super.setToolTipText( text ); - if ( spinner != null ) - spinner.setToolTipText( text ); - if ( slider != null ) - slider.setToolTipText( text ); - } - - @Override - public void update() - { - final int value = model.getCurrentValue(); - final int min = model.getRangeMin(); - final int max = model.getRangeMax(); - if ( slider.getMaximum() != max || slider.getMinimum() != min ) - { - slider.setMinimum( min ); - slider.setMaximum( max ); - final SpinnerNumberModel spinnerModel = ( SpinnerNumberModel ) spinner.getModel(); - spinnerModel.setMinimum( min ); - spinnerModel.setMaximum( max ); - } - slider.setValue( value ); - spinner.setValue( value ); - } -} diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/SliderPanelDouble.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/SliderPanelDouble.java deleted file mode 100644 index 9016aa70f..000000000 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/SliderPanelDouble.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.gui.displaysettings; - -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.Font; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.awt.event.MouseWheelEvent; -import java.awt.event.MouseWheelListener; - -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSlider; -import javax.swing.JSpinner; -import javax.swing.JSpinner.NumberEditor; -import javax.swing.SpinnerNumberModel; -import javax.swing.SwingConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -/** - * A {@link JSlider} with a {@link JSpinner} next to it, both modifying the same - * {@link BoundedValue value}. - */ -public class SliderPanelDouble extends JPanel implements BoundedValueDouble.UpdateListener -{ - private static final long serialVersionUID = 6444334522127424416L; - - private static final int sliderLength = 50; - - private final JSlider slider; - - private final JSpinner spinner; - - private final BoundedValueDouble model; - - private double dmin; - - private double dmax; - - private boolean userDefinedNumberFormat = false; - - private RangeListener rangeListener; - - public interface RangeListener - { - void rangeChanged(); - } - - /** - * Create a {@link SliderPanelDouble} to modify a given - * {@link BoundedValueDouble value}. - * - * @param name - * label to show next to the slider. - * @param model - * the value that is modified. - * @param spinnerStepSize - * the step size of the spinner. - */ - public SliderPanelDouble( - final String name, - final BoundedValueDouble model, - final double spinnerStepSize ) - { - super(); - setLayout( new BorderLayout( 10, 10 ) ); - setPreferredSize( SliderPanel.PANEL_SIZE ); - - dmin = model.getRangeMin(); - dmax = model.getRangeMax(); - final double val = Math.min( Math.max( model.getCurrentValue(), dmin ), dmax ); - - slider = new JSlider( SwingConstants.HORIZONTAL, 0, sliderLength, toSlider( val ) ); - spinner = new JSpinner(); - spinner.setModel( new SpinnerNumberModel( val, dmin, dmax, spinnerStepSize ) ); - - slider.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( final ChangeEvent e ) - { - final int value = slider.getValue(); - model.setCurrentValue( fromSlider( value ) ); - } - } ); - - slider.addComponentListener( new ComponentAdapter() - { - @Override - public void componentResized( final ComponentEvent e ) - { - updateNumberFormat(); - } - } ); - - final MouseWheelListener mwl = new MouseWheelListener() - { - - @Override - public void mouseWheelMoved( final MouseWheelEvent e ) - { - final int notches = e.getWheelRotation(); - double value = ( ( Number ) spinner.getValue() ).doubleValue(); - value -= notches * spinnerStepSize; - if ( value < dmin ) - value = dmin; - else if ( value > dmax ) - value = dmax; - spinner.setValue( value ); - } - }; - slider.addMouseWheelListener( mwl ); - spinner.addMouseWheelListener( mwl ); - - spinner.addChangeListener( new ChangeListener() - { - @Override - public void stateChanged( final ChangeEvent e ) - { - final double value = ( ( Double ) spinner.getValue() ).doubleValue(); - model.setCurrentValue( value ); - } - } ); - - if ( name != null ) - { - final JLabel label = new JLabel( name, SwingConstants.CENTER ); - label.setAlignmentX( Component.CENTER_ALIGNMENT ); - add( label, BorderLayout.WEST ); - } - - add( slider, BorderLayout.CENTER ); - add( spinner, BorderLayout.EAST ); - - this.model = model; - model.setUpdateListener( this ); - } - - public void setDecimalFormat( final String pattern ) - { - if ( pattern == null ) - { - userDefinedNumberFormat = false; - updateNumberFormat(); - } - else - { - userDefinedNumberFormat = true; - ( ( JSpinner.NumberEditor ) spinner.getEditor() ).getFormat().applyPattern( pattern ); - } - } - - public void setNumColummns( final int cols ) - { - ( ( JSpinner.NumberEditor ) spinner.getEditor() ).getTextField().setColumns( cols ); - } - - @Override - public void setFont( final Font font ) - { - super.setFont( font ); - if ( spinner != null ) - spinner.setFont( font ); - if ( slider != null ) - slider.setFont( font ); - } - - @Override - public void setToolTipText( final String text ) - { - super.setToolTipText( text ); - if ( spinner != null ) - spinner.setToolTipText( text ); - if ( slider != null ) - slider.setToolTipText( text ); - } - - @Override - public void update() - { - final double value = model.getCurrentValue(); - final double min = model.getRangeMin(); - final double max = model.getRangeMax(); - - final boolean rangeChanged = ( dmax != max || dmin != min ); - if ( rangeChanged ) - { - dmin = min; - dmax = max; - final SpinnerNumberModel spinnerModel = ( SpinnerNumberModel ) spinner.getModel(); - spinnerModel.setMinimum( min ); - spinnerModel.setMaximum( max ); - } - slider.setValue( toSlider( value ) ); - spinner.setValue( value ); - - if ( rangeChanged ) - updateNumberFormat(); - - if ( rangeChanged && rangeListener != null ) - rangeListener.rangeChanged(); - - } - - public void setRangeListener( final RangeListener listener ) - { - this.rangeListener = listener; - } - - private void updateNumberFormat() - { - if ( userDefinedNumberFormat ) - return; - - final int sw = slider.getWidth(); - if ( sw > 0 ) - { - final double range = dmax - dmin; - final int digits = ( int ) Math.ceil( Math.log10( sw / range ) ); - final NumberEditor numberEditor = ( ( JSpinner.NumberEditor ) spinner.getEditor() ); - numberEditor.getFormat().setMaximumFractionDigits( digits ); - numberEditor.stateChanged( new ChangeEvent( spinner ) ); - } - } - - private int toSlider( final double value ) - { - return ( int ) Math.round( ( value - dmin ) * sliderLength / ( dmax - dmin ) ); - } - - private double fromSlider( final int value ) - { - return ( value * ( dmax - dmin ) / sliderLength ) + dmin; - } -} diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/StyleElements.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/StyleElements.java deleted file mode 100644 index c531dbb34..000000000 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/StyleElements.java +++ /dev/null @@ -1,1207 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.gui.displaysettings; - -import java.awt.Color; -import java.awt.Component; -import java.awt.Dialog.ModalityType; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.Graphics; -import java.awt.Insets; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.FocusAdapter; -import java.text.DecimalFormat; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.List; -import java.util.Vector; -import java.util.function.BiConsumer; -import java.util.function.BooleanSupplier; -import java.util.function.Consumer; -import java.util.function.DoubleSupplier; -import java.util.function.IntSupplier; -import java.util.function.Supplier; - -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.DefaultComboBoxModel; -import javax.swing.DefaultListCellRenderer; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JColorChooser; -import javax.swing.JComboBox; -import javax.swing.JComponent; -import javax.swing.JDialog; -import javax.swing.JFormattedTextField; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.JSpinner; -import javax.swing.JSpinner.DefaultEditor; -import javax.swing.JTextField; -import javax.swing.ListCellRenderer; -import javax.swing.SpinnerListModel; -import javax.swing.SpinnerNumberModel; -import javax.swing.SwingConstants; -import javax.swing.WindowConstants; -import javax.swing.border.EmptyBorder; - -import org.drjekyll.fontchooser.FontDialog; - -import fiji.plugin.trackmate.Settings; -import fiji.plugin.trackmate.gui.Fonts; -import fiji.plugin.trackmate.gui.GuiUtils; -import fiji.plugin.trackmate.gui.Icons; -import fiji.plugin.trackmate.gui.components.CategoryJComboBox; -import fiji.plugin.trackmate.gui.components.FeatureDisplaySelector; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; - -public class StyleElements -{ - private static final DecimalFormat format = new DecimalFormat( "#.###" ); - - public static Separator separator() - { - return new Separator(); - } - - public static LabelElement label( final String label ) - { - return new LabelElement( label ); - } - - public static StringElement stringElement( final String label, final Supplier< String > get, final Consumer< String > set ) - { - return new StringElement( label ) - { - - @Override - public String get() - { - return get.get(); - } - - @Override - public void set( final String s ) - { - set.accept( s ); - } - }; - } - - public static BooleanElement booleanElement( final String label, final BooleanSupplier get, final Consumer< Boolean > set ) - { - return new BooleanElement( label ) - { - @Override - public boolean get() - { - return get.getAsBoolean(); - } - - @Override - public void set( final boolean b ) - { - set.accept( b ); - } - }; - } - - public static ColorElement colorElement( final String label, final Supplier< Color > get, final Consumer< Color > set ) - { - return new ColorElement( label ) - { - @Override - public Color getColor() - { - return get.get(); - } - - @Override - public void setColor( final Color c ) - { - set.accept( c ); - } - }; - } - - public static ColormapElement colormapElement( final String label, final Supplier< Colormap > get, final Consumer< Colormap > set ) - { - return new ColormapElement( label ) - { - - @Override - public Colormap get() - { - return get.get(); - } - - @Override - public void set( final Colormap v ) - { - set.accept( v ); - } - }; - } - - public static BoundedDoubleElement boundedDoubleElement( final String label, final double rangeMin, final double rangeMax, final DoubleSupplier get, final Consumer< Double > set ) - { - return new BoundedDoubleElement( label, rangeMin, rangeMax ) - { - @Override - public double get() - { - return get.getAsDouble(); - } - - @Override - public void set( final double v ) - { - set.accept( v ); - } - }; - } - - public static DoubleElement doubleElement( final String label, final DoubleSupplier get, final Consumer< Double > set ) - { - return new DoubleElement( label ) - { - @Override - public double get() - { - return get.getAsDouble(); - } - - @Override - public void set( final double v ) - { - set.accept( v ); - } - }; - } - - public static IntElement intElement( final String label, final int rangeMin, final int rangeMax, final IntSupplier get, final Consumer< Integer > set ) - { - return new IntElement( label, rangeMin, rangeMax ) - { - @Override - public int get() - { - return get.getAsInt(); - } - - @Override - public void set( final int v ) - { - set.accept( v ); - } - }; - } - - public static < E > EnumElement< E > enumElement( final String label, final E[] values, final Supplier< E > get, final Consumer< E > set ) - { - return new EnumElement< E >( label, values ) - { - - @Override - public E getValue() - { - return get.get(); - } - - @Override - public void setValue( final E e ) - { - set.accept( e ); - } - }; - } - - public static < E > ListElement< E > listElement( final String label, final List< E > values, final Supplier< E > get, final Consumer< E > set ) - { - return new ListElement< E >( label, values ) - { - - @Override - public E getValue() - { - return get.get(); - } - - @Override - public void setValue( final E e ) - { - set.accept( e ); - } - }; - } - - public static FeatureElement featureElement( final String label, final Supplier< TrackMateObject > typeGet, final Supplier< String > featureGet, final BiConsumer< TrackMateObject, String > set ) - { - return new FeatureElement( label ) - { - - @Override - public void setValue( final TrackMateObject type, final String feature ) - { - set.accept( type, feature ); - } - - @Override - public TrackMateObject getType() - { - return typeGet.get(); - } - - @Override - public String getFeature() - { - return featureGet.get(); - } - }; - } - - public static FontElement fontElement( final String label, final Supplier< Font > get, final Consumer< Font > set ) - { - return new FontElement( label ) - { - - @Override - public void set( final Font font ) - { - set.accept( font ); - } - - @Override - public Font get() - { - return get.get(); - } - }; - } - - /* - * Visitor interface. - */ - - public interface StyleElementVisitor - { - public default void visit( final Separator element ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final LabelElement label ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final ColorElement colorElement ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final BooleanElement booleanElement ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final BoundedDoubleElement doubleElement ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final DoubleElement doubleElement ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final IntElement intElement ) - { - throw new UnsupportedOperationException(); - } - - public default < E > void visit( final EnumElement< E > enumElement ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final FeatureElement featureElement ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final ColormapElement element ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final FontElement fontElement ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final StringElement stringElement ) - { - throw new UnsupportedOperationException(); - } - - public default < E > void visit( final ListElement< E > listElement ) - { - throw new UnsupportedOperationException(); - } - } - - /* - * - * =============================================================== - * - */ - - public interface StyleElement - { - public default void update() - {} - - public void accept( StyleElementVisitor visitor ); - } - - public static class Separator implements StyleElement - { - @Override - public void accept( final StyleElementVisitor visitor ) - { - visitor.visit( this ); - } - } - - public static class LabelElement implements StyleElement - { - private final String label; - - public LabelElement( final String label ) - { - this.label = label; - } - - public String getLabel() - { - return label; - } - - @Override - public void accept( final StyleElementVisitor visitor ) - { - visitor.visit( this ); - } - } - - public static abstract class StringElement implements StyleElement - { - - private final ArrayList< Consumer< String > > onSet = new ArrayList<>(); - - private final String label; - - private String value; - - public StringElement( final String label ) - { - this.label = label; - this.value = ""; - } - - public String getLabel() - { - return label; - } - - @Override - public void accept( final StyleElementVisitor visitor ) - { - visitor.visit( this ); - } - - public abstract String get(); - - public abstract void set( String s ); - - public void onSet( final Consumer< String > set ) - { - onSet.add( set ); - } - - @Override - public void update() - { - if ( get() != value ) - value = get(); - onSet.forEach( c -> c.accept( get() ) ); - } - } - - public static abstract class EnumElement< E > implements StyleElement - { - private final ArrayList< Consumer< E > > onSet = new ArrayList<>(); - - private final String label; - - private final E[] values; - - public EnumElement( final String label, final E[] values ) - { - this.label = label; - this.values = values; - } - - public String getLabel() - { - return label; - } - - @Override - public void accept( final StyleElementVisitor visitor ) - { - visitor.visit( this ); - } - - public void onSet( final Consumer< E > set ) - { - onSet.add( set ); - } - - @Override - public void update() - { - onSet.forEach( c -> c.accept( getValue() ) ); - } - - public abstract E getValue(); - - public abstract void setValue( E e ); - - public E[] getValues() - { - return values; - } - } - - public static abstract class ListElement< E > implements StyleElement - { - private final ArrayList< Consumer< E > > onSet = new ArrayList<>(); - - private final String label; - - private final List< E > values; - - public ListElement( final String label, final List< E > values ) - { - this.label = label; - this.values = values; - } - - public String getLabel() - { - return label; - } - - @Override - public void accept( final StyleElementVisitor visitor ) - { - visitor.visit( this ); - } - - public void onSet( final Consumer< E > set ) - { - onSet.add( set ); - } - - @Override - public void update() - { - onSet.forEach( c -> c.accept( getValue() ) ); - } - - public abstract E getValue(); - - public abstract void setValue( E e ); - - public List< E > getValues() - { - return values; - } - } - - public static abstract class FeatureElement implements StyleElement - { - private final ArrayList< BiConsumer< TrackMateObject, String > > onSet = new ArrayList<>(); - - private final String label; - - public FeatureElement( final String label ) - { - this.label = label; - } - - public String getLabel() - { - return label; - } - - @Override - public void accept( final StyleElementVisitor visitor ) - { - visitor.visit( this ); - } - - public void onSet( final BiConsumer< TrackMateObject, String > set ) - { - onSet.add( set ); - } - - @Override - public void update() - { - onSet.forEach( c -> c.accept( getType(), getFeature() ) ); - } - - public abstract TrackMateObject getType(); - - public abstract String getFeature(); - - public abstract void setValue( TrackMateObject type, String feature ); - } - - public static abstract class ColorElement implements StyleElement - { - private final ArrayList< Consumer< Color > > onSet = new ArrayList<>(); - - private final String label; - - public ColorElement( final String label ) - { - this.label = label; - } - - public String getLabel() - { - return label; - } - - @Override - public void accept( final StyleElementVisitor visitor ) - { - visitor.visit( this ); - } - - public void onSet( final Consumer< Color > set ) - { - onSet.add( set ); - } - - @Override - public void update() - { - onSet.forEach( c -> c.accept( getColor() ) ); - } - - public abstract Color getColor(); - - public abstract void setColor( Color c ); - } - - public static abstract class BooleanElement implements StyleElement - { - private final String label; - - private final ArrayList< Consumer< Boolean > > onSet = new ArrayList<>(); - - public BooleanElement( final String label ) - { - this.label = label; - } - - public String getLabel() - { - return label; - } - - @Override - public void accept( final StyleElementVisitor visitor ) - { - visitor.visit( this ); - } - - public void onSet( final Consumer< Boolean > set ) - { - onSet.add( set ); - } - - @Override - public void update() - { - onSet.forEach( c -> c.accept( get() ) ); - } - - public abstract boolean get(); - - public abstract void set( boolean b ); - } - - public static abstract class BoundedDoubleElement implements StyleElement - { - private final BoundedValueDouble value; - - private final String label; - - public BoundedDoubleElement( final String label, final double rangeMin, final double rangeMax ) - { - final double currentValue = Math.max( rangeMin, Math.min( rangeMax, get() ) ); - value = new BoundedValueDouble( rangeMin, rangeMax, currentValue ) - { - @Override - public void setCurrentValue( final double value ) - { - super.setCurrentValue( value ); - if ( get() != getCurrentValue() ) - set( getCurrentValue() ); - } - }; - this.label = label; - } - - public BoundedValueDouble getValue() - { - return value; - } - - public String getLabel() - { - return label; - } - - @Override - public void accept( final StyleElementVisitor visitor ) - { - visitor.visit( this ); - } - - public abstract double get(); - - public abstract void set( double v ); - - @Override - public void update() - { - if ( get() != value.getCurrentValue() ) - value.setCurrentValue( get() ); - } - } - - public static abstract class DoubleElement implements StyleElement - { - - private final ArrayList< Consumer< Double > > onSet = new ArrayList<>(); - - private double value; - - private final String label; - - public DoubleElement( final String label ) - { - value = 0.; - this.label = label; - } - - public double getValue() - { - return value; - } - - public String getLabel() - { - return label; - } - - @Override - public void accept( final StyleElementVisitor visitor ) - { - visitor.visit( this ); - } - - public abstract double get(); - - public abstract void set( double v ); - - public void onSet( final Consumer< Double > set ) - { - onSet.add( set ); - } - - @Override - public void update() - { - if ( get() != value ) - value = get(); - onSet.forEach( c -> c.accept( get() ) ); - } - } - - public static abstract class IntElement implements StyleElement - { - private final BoundedValue value; - - private final String label; - - public IntElement( final String label, final int rangeMin, final int rangeMax ) - { - final int currentValue = Math.max( rangeMin, Math.min( rangeMax, get() ) ); - value = new BoundedValue( rangeMin, rangeMax, currentValue ) - { - @Override - public void setCurrentValue( final int value ) - { - super.setCurrentValue( value ); - if ( get() != getCurrentValue() ) - set( getCurrentValue() ); - } - }; - this.label = label; - } - - public BoundedValue getValue() - { - return value; - } - - public String getLabel() - { - return label; - } - - @Override - public void accept( final StyleElementVisitor visitor ) - { - visitor.visit( this ); - } - - public abstract int get(); - - public abstract void set( int v ); - - @Override - public void update() - { - if ( get() != value.getCurrentValue() ) - value.setCurrentValue( get() ); - } - } - - public static abstract class ColormapElement implements StyleElement - { - private final ArrayList< Consumer< Colormap > > onSet = new ArrayList<>(); - - private final String label; - - public ColormapElement( final String label ) - { - this.label = label; - } - - public String getLabel() - { - return label; - } - - @Override - public void accept( final StyleElementVisitor visitor ) - { - visitor.visit( this ); - } - - public abstract Colormap get(); - - public abstract void set( Colormap v ); - - public void onSet( final Consumer< Colormap > set ) - { - onSet.add( set ); - } - - @Override - public void update() - { - onSet.forEach( c -> c.accept( get() ) ); - } - } - - public static abstract class FontElement implements StyleElement - { - - private final ArrayList< Consumer< Font > > onSet = new ArrayList<>(); - - private Font value; - - private final String label; - - public FontElement( final String label ) - { - this.label = label; - } - - public Font getValue() - { - return value; - } - - public String getLabel() - { - return label; - } - - @Override - public void accept( final StyleElementVisitor visitor ) - { - visitor.visit( this ); - } - - public abstract Font get(); - - public abstract void set( Font font ); - - public void onSet( final Consumer< Font > set ) - { - onSet.add( set ); - } - - @Override - public void update() - { - if ( get() != value ) - value = get(); - } - } - - /* - * - * =============================================================== - * - */ - - public static JLabel linkedLabel( final LabelElement element ) - { - return new JLabel( element.getLabel() ); - } - - public static CategoryJComboBox< TrackMateObject, String > linkedFeatureSelector( final FeatureElement element ) - { - final Settings settings = new Settings(); - settings.addAllAnalyzers(); - final CategoryJComboBox< TrackMateObject, String > selector = FeatureDisplaySelector.createComboBoxSelector( null, settings ); - selector.setSelectedItem( element.getFeature() ); - selector.addActionListener( e -> element.setValue( selector.getSelectedCategory(), selector.getSelectedItem() ) ); - element.onSet( ( type, feature ) -> { - if ( !feature.equals( selector.getSelectedItem() ) ) - selector.setSelectedItem( feature ); - } ); - return selector; - } - - public static JComboBox< Colormap > linkedColormapChooser( final ColormapElement element ) - { - final JComboBox< Colormap > cb = new JComboBox< Colormap >( - Colormap.getAvailableLUTs().toArray( new Colormap[] {} ) ); - cb.setRenderer( new ColormapRenderer() ); - cb.setSelectedItem( element.get() ); - cb.addActionListener( e -> element.set( ( Colormap ) cb.getSelectedItem() ) ); - element.onSet( cm -> { - if ( cm != cb.getSelectedItem() ) - cb.setSelectedItem( cm ); - } ); - return cb; - } - - private static final class ColormapRenderer extends JPanel implements ListCellRenderer< Colormap > - { - - private static final long serialVersionUID = 1L; - - private Colormap lut = Colormap.Jet; - - private final DefaultListCellRenderer lbl; - - public ColormapRenderer() - { - setPreferredSize( new Dimension( 150, 20 ) ); - final BoxLayout itemlayout = new BoxLayout( this, BoxLayout.LINE_AXIS ); - this.lbl = new DefaultListCellRenderer(); - setLayout( itemlayout ); - add( lbl ); - add( Box.createHorizontalGlue() ); - 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( 100, 20 ); - } - - @Override - public Dimension getPreferredSize() - { - return getMaximumSize(); - } - } ); - } - - @Override - public Component getListCellRendererComponent( - final JList< ? extends Colormap > list, - final Colormap value, - final int index, - final boolean isSelected, - final boolean cellHasFocus ) - { - this.lut = value; - lbl.getListCellRendererComponent( list, value.getName(), index, isSelected, cellHasFocus ); - setBackground( lbl.getBackground() ); - return this; - } - } - - public static JCheckBox linkedCheckBox( final BooleanElement element, final String label ) - { - final JCheckBox checkbox = new JCheckBox( label, element.get() ); - checkbox.addActionListener( ( e ) -> element.set( checkbox.isSelected() ) ); - element.onSet( b -> { - if ( b != checkbox.isSelected() ) - checkbox.setSelected( b ); - } ); - return checkbox; - } - - public static JButton linkedColorButton( final ColorElement element, final JColorChooser colorChooser ) - { - final ColorIcon icon = new ColorIcon( element.getColor(), 16, 0 ); - final JButton button = new JButton( icon ); - button.setOpaque( false ); - button.setContentAreaFilled( false ); - button.setBorderPainted( false ); - button.setFont( new JButton().getFont() ); - button.setMargin( new Insets( 0, 0, 0, 0 ) ); - button.setBorder( new EmptyBorder( 2, 5, 2, 2 ) ); - button.setHorizontalAlignment( SwingConstants.LEFT ); - button.addActionListener( e -> { - colorChooser.setColor( element.getColor() ); - final JDialog d = JColorChooser.createDialog( button, "Choose a color", true, colorChooser, new ActionListener() - { - @Override - public void actionPerformed( final ActionEvent arg0 ) - { - final Color c = colorChooser.getColor(); - if ( c != null ) - { - icon.setColor( c ); - button.repaint(); - element.setColor( c ); - } - } - }, null ); - d.setVisible( true ); - } ); - element.onSet( icon::setColor ); - return button; - } - - public static SliderPanel linkedSliderPanel( final IntElement element, final int tfCols ) - { - final SliderPanel slider = new SliderPanel( null, element.getValue(), 1 ); - slider.setNumColummns( tfCols ); - slider.setBorder( new EmptyBorder( 0, 0, 0, 0 ) ); - return slider; - } - - public static JSpinner linkedSpinner( final IntElement element ) - { - final BoundedValue value = element.getValue(); - final SpinnerNumberModel model = new SpinnerNumberModel( element.get(), value.getRangeMin(), value.getRangeMax(), 1 ); - final JSpinner spinner = new JSpinner( model ); - spinner.setMaximumSize( new Dimension( 80, spinner.getMaximumSize().height ) ); - model.addChangeListener( e -> element.set( ( ( Number ) model.getValue() ).intValue() ) ); - return spinner; - } - - public static SliderPanelDouble linkedSliderPanel( final BoundedDoubleElement element, final int tfCols ) - { - return linkedSliderPanel( element, tfCols, 1. ); - } - - public static SliderPanelDouble linkedSliderPanel( final BoundedDoubleElement element, final int tfCols, final double stepSize ) - { - final SliderPanelDouble slider = new SliderPanelDouble( null, element.getValue(), stepSize ); - slider.setDecimalFormat( "0.####" ); - slider.setNumColummns( tfCols ); - slider.setBorder( new EmptyBorder( 0, 0, 0, 0 ) ); - return slider; - } - - @SuppressWarnings( "unchecked" ) - public static < E > JSpinner linkedSpinnerEnumSelector( final EnumElement< E > element ) - { - final SpinnerListModel model = new SpinnerListModel( element.getValues() ); - final JSpinner spinner = new JSpinner( model ); - spinner.setFont( Fonts.SMALL_FONT ); - ( ( DefaultEditor ) spinner.getEditor() ).getTextField().setEditable( false ); - model.setValue( element.getValue() ); - model.addChangeListener( e -> element.setValue( ( E ) model.getValue() ) ); - element.onSet( e -> { - if ( e != model.getValue() ) - model.setValue( e ); - } ); - return spinner; - } - - @SuppressWarnings( "unchecked" ) - public static < E > JComboBox< E > linkedComboBoxEnumSelector( final EnumElement< E > element ) - { - final DefaultComboBoxModel< E > model = new DefaultComboBoxModel<>( element.values ); - final JComboBox< E > cb = new JComboBox<>( model ); - cb.setFont( Fonts.SMALL_FONT ); - cb.addActionListener( e -> element.setValue( ( E ) model.getSelectedItem() ) ); - element.onSet( e -> { - if ( e != model.getSelectedItem() ) - model.setSelectedItem( e ); - } ); - return cb; - } - - @SuppressWarnings( "unchecked" ) - public static < E > JComboBox< E > linkedComboBoxSelector( final ListElement< E > element ) - { - final DefaultComboBoxModel< E > model = new DefaultComboBoxModel<>( new Vector<>( element.values ) ); - final JComboBox< E > cb = new JComboBox<>( model ); - cb.setFont( Fonts.SMALL_FONT ); - cb.addActionListener( e -> element.setValue( ( E ) model.getSelectedItem() ) ); - element.onSet( e -> { - if ( e != model.getSelectedItem() ) - model.setSelectedItem( e ); - } ); - return cb; - } - - public static JFormattedTextField linkedFormattedTextField( final DoubleElement element ) - { - final JFormattedTextField ftf = new JFormattedTextField( format ); - ftf.setHorizontalAlignment( JFormattedTextField.RIGHT ); - ftf.setValue( Double.valueOf( element.get() ) ); - - ftf.addActionListener( e -> element.set( ( ( Number ) ftf.getValue() ).doubleValue() ) ); - ftf.addFocusListener( new FocusAdapter() - { - @Override - public void focusLost( final java.awt.event.FocusEvent e ) - { - try - { - ftf.commitEdit(); - element.set( ( ( Number ) ftf.getValue() ).doubleValue() ); - } - catch ( final ParseException e1 ) - {} - } - } ); - GuiUtils.selectAllOnFocus( ftf ); - element.onSet( d -> { - if ( d != ( ( Number ) ftf.getValue() ).doubleValue() ) - ftf.setValue( Double.valueOf( element.value ) ); - } ); - - return ftf; - } - - public static JTextField linkedTextField( final StringElement element ) - { - final JTextField tf = new JTextField( element.get() ); - tf.setHorizontalAlignment( JFormattedTextField.LEFT ); - - tf.addActionListener( e -> element.set( tf.getText() ) ); - GuiUtils.selectAllOnFocus( tf ); - tf.addFocusListener( new FocusAdapter() - { - @Override - public void focusLost( final java.awt.event.FocusEvent e ) - { - element.set( tf.getText() ); - } - } ); - element.onSet( d -> { - if ( d != ( tf.getText() ) ) - tf.setText( element.value ); - } ); - - return tf; - } - - public static JButton linkedFontButton( final FontElement element, final Window parent ) - { - final JButton btn = new JButton( "Select font" ); - btn.setFont( element.get() ); - btn.addPropertyChangeListener( "font", e -> element.set( btn.getFont() ) ); - element.onSet( font -> { - if ( !font.equals( btn.getFont() ) ) - btn.setFont( font ); - } ); - btn.addActionListener( e -> { - final FontDialog dialog = new FontDialog( parent, "Select font for TrackMate display", ModalityType.APPLICATION_MODAL ); - dialog.setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE ); - dialog.setSelectedFont( btn.getFont() ); - GuiUtils.positionWindow( dialog, parent ); - dialog.setIconImage( Icons.TRACKMATE_ICON.getImage() ); - dialog.setVisible( true ); - if ( !dialog.isCancelSelected() ) - btn.setFont( dialog.getSelectedFont() ); - } ); - return btn; - } -} diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/TrackMateStyleElements.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/TrackMateStyleElements.java new file mode 100644 index 000000000..97c3265e4 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/TrackMateStyleElements.java @@ -0,0 +1,118 @@ +package fiji.plugin.trackmate.gui.displaysettings; + +import java.util.ArrayList; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import org.scijava.ui.config.visitors.gui.elements.StyleElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElementVisitor; + +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.gui.components.CategoryJComboBox; +import fiji.plugin.trackmate.gui.components.FeatureDisplaySelector; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; + +public class TrackMateStyleElements +{ + + public static CategoryJComboBox< TrackMateObject, String > linkedFeatureSelector( final FeatureElement element ) + { + final Settings settings = new Settings(); + settings.addAllAnalyzers(); + final CategoryJComboBox< TrackMateObject, String > selector = FeatureDisplaySelector.createComboBoxSelector( null, settings ); + selector.setSelectedItem( element.getFeature() ); + selector.addActionListener( e -> element.setValue( selector.getSelectedCategory(), selector.getSelectedItem() ) ); + element.onSet( ( type, feature ) -> { + if ( !feature.equals( selector.getSelectedItem() ) ) + selector.setSelectedItem( feature ); + } ); + return selector; + } + + public static FeatureElement featureElement( final String label, final Supplier< TrackMateObject > typeGet, final Supplier< String > featureGet, final BiConsumer< TrackMateObject, String > set ) + { + return new FeatureElement( label ) + { + + @Override + public void setValue( final TrackMateObject type, final String feature ) + { + set.accept( type, feature ); + } + + @Override + public TrackMateObject getType() + { + return typeGet.get(); + } + + @Override + public String getFeature() + { + return featureGet.get(); + } + + @Override + public void accept( final StyleElementVisitor visitor ) + { + if ( visitor instanceof final TrackMateStyleElementVisitor extendedVisitor ) + extendedVisitor.visit( this ); + else + throw new UnsupportedOperationException( "Visitor " + visitor.getClass().getName() + " does not support " + this.getClass().getName() ); + } + }; + } + + public static interface TrackMateStyleElement extends StyleElement + { + void accept( TrackMateStyleElementVisitor visitor ); + } + + public static interface TrackMateStyleElementVisitor extends StyleElementVisitor + { + public default void visit( final FeatureElement element ) + { + throw new UnsupportedOperationException(); + } + } + + public static abstract class FeatureElement implements TrackMateStyleElement + { + private final ArrayList< BiConsumer< TrackMateObject, String > > onSet = new ArrayList<>(); + + private final String label; + + public FeatureElement( final String label ) + { + this.label = label; + } + + public String getLabel() + { + return label; + } + + @Override + public void accept( final TrackMateStyleElementVisitor visitor ) + { + visitor.visit( this ); + } + + public void onSet( final BiConsumer< TrackMateObject, String > set ) + { + onSet.add( set ); + } + + @Override + public void update() + { + onSet.forEach( c -> c.accept( getType(), getFeature() ) ); + } + + public abstract TrackMateObject getType(); + + public abstract String getFeature(); + + public abstract void setValue( TrackMateObject type, String feature ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index e8ad9963e..b1edfeb82 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -21,69 +21,53 @@ */ package fiji.plugin.trackmate.gui.editor; -import java.awt.Component; -import java.awt.event.ActionEvent; import java.io.File; import javax.swing.JCheckBox; import javax.swing.JFrame; -import javax.swing.JLabel; import javax.swing.JOptionPane; -import javax.swing.JRootPane; import javax.swing.JSeparator; -import javax.swing.SwingUtilities; - -import org.scijava.Context; -import org.scijava.ui.behaviour.util.AbstractNamedAction; +import bdv.ui.appearance.AppearanceManager; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Settings; -import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.detection.DetectionUtils; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.editor.labkit.component.EditorKeymapManager; import fiji.plugin.trackmate.gui.editor.labkit.component.TMLabKitFrame; import fiji.plugin.trackmate.gui.editor.labkit.model.TMLabKitModel; import fiji.plugin.trackmate.io.TmXmlReader; -import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; -import fiji.plugin.trackmate.util.TMUtils; -import fiji.plugin.trackmate.visualization.TrackMateModelView; import fiji.plugin.trackmate.visualization.ViewUtils; -import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; import ij.ImagePlus; +import ij.gui.Roi; import net.imagej.axis.Axes; import net.imagej.axis.CalibratedAxis; -import net.imglib2.Interval; import sc.fiji.labkit.ui.labeling.Labeling; public class LabkitLauncher { - private static final boolean ENABLE_SPOT_EDITOR = true; - private static boolean simplify = true; - public static final TMLabKitFrame launch( final TrackMate trackmate, final DisplaySettings displaySettings, final int timepoint ) + public static final TMLabKitFrame launch( final GuiModel guiModel, final int timepoint ) { // Input model. - final Model model = trackmate.getModel(); + final Model model = guiModel.getModel(); // Input image. - ImagePlus imp = trackmate.getSettings().imp; + ImagePlus imp = guiModel.getSettings().imp; if ( null == imp ) - imp = ViewUtils.makeEmpytImagePlus( model ); - - // ROI & interval. - final Interval interval = TMUtils.createROIInterval( imp ); + imp = ViewUtils.makeEmptyImagePlus( model ); // Create the LabKit model. - final Context context = TMUtils.getContext(); - final TMLabKitModel lbModel = TMLabKitModel.create( model, imp, interval, displaySettings, timepoint, context ); + final TMLabKitModel lbModel = TMLabKitModel.create( guiModel, timepoint ); // Create the UI for editing. - final TMLabKitFrame labkit = new TMLabKitFrame( lbModel ); + final EditorKeymapManager keymapManager = guiModel.getEditorKeymapManager(); + final AppearanceManager appearanceManager = guiModel.getAppearanceManager(); + final TMLabKitFrame labkit = new TMLabKitFrame( lbModel, keymapManager, appearanceManager ); GuiUtils.positionWindow( labkit, imp.getWindow() ); labkit.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE ); @@ -157,61 +141,10 @@ public void run() }.start(); } - public static final AbstractNamedAction getLaunchAction( final TrackMate trackmate, final DisplaySettings ds ) - { - final AbstractNamedAction action = new AbstractNamedAction( "launch labkit editor" ) - { - - private static final long serialVersionUID = 1L; - - @Override - public void actionPerformed( final ActionEvent ae ) - { - new Thread( "TrackMate editor thread" ) - { - @Override - - public void run() - { - final JRootPane parent = SwingUtilities.getRootPane( ( Component ) ae.getSource() ); - final EverythingDisablerAndReenabler disabler = new EverythingDisablerAndReenabler( parent, new Class[] { JLabel.class } ); - disabler.disable(); - try - { - // Is shift pressed? - final int mod = ae.getModifiers(); - final boolean shiftPressed = ( mod & ActionEvent.SHIFT_MASK ) > 0; - final boolean singleTimepoint = !shiftPressed; - final ImagePlus imp = trackmate.getSettings().imp; - int timepoint; - if ( imp == null ) - timepoint = -1; - else - timepoint = singleTimepoint ? imp.getFrame() - 1 : -1; - - final TMLabKitFrame labKitFrame = LabkitLauncher.launch( trackmate, ds, timepoint ); - labKitFrame.onCloseListeners().addListener( disabler::reenable ); - } - catch ( final Exception e ) - { - e.printStackTrace(); - disabler.reenable(); - } - }; - }.start(); - } - }; // Disable if the image is not 2D. - if ( !DetectionUtils.is2D( trackmate.getSettings().imp ) ) - action.setEnabled( false ); - else - action.setEnabled( ENABLE_SPOT_EDITOR ); - return action; - } - public static void main( final String[] args ) { -// final String filename = "samples/MAX_Merged.xml"; - final String filename = "samples/221031_Stat_Stage55_561nm_part1Conf_crop_f4.xml"; + final String filename = "samples/MAX_Merged.xml"; +// final String filename = "samples/221031_Stat_Stage55_561nm_part1Conf_crop_f4.xml"; final TmXmlReader reader = new TmXmlReader( new File( filename ) ); if ( !reader.isReadingOk() ) { @@ -221,17 +154,16 @@ public static void main( final String[] args ) final Model model = reader.getModel(); final ImagePlus imp = reader.readImage(); + imp.setRoi( new Roi( 10, 30, 100, 100 ) ); final Settings settings = reader.readSettings( imp ); final DisplaySettings ds = reader.getDisplaySettings(); - final TrackMate trackmate = new TrackMate( model, settings ); - final SelectionModel selectionModel = new SelectionModel( model ); + final GuiModel guiModel = new GuiModel( model, settings, ds ); // Main view. - final TrackMateModelView displayer = new HyperStackDisplayer( model, selectionModel, settings.imp, ds ); - displayer.render(); + guiModel.getWindowManager().createHyperStackDisplayer(); imp.setSlice( 7 ); // Editor - LabkitLauncher.launch( trackmate, ds, 6 ); + LabkitLauncher.launch( guiModel, -1 ); } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMKeymapManager.java b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/EditorKeymapManager.java similarity index 91% rename from src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMKeymapManager.java rename to src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/EditorKeymapManager.java index d05709b61..38f5a0f64 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMKeymapManager.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/EditorKeymapManager.java @@ -21,7 +21,7 @@ */ package fiji.plugin.trackmate.gui.editor.labkit.component; -import static fiji.plugin.trackmate.gui.editor.labkit.component.TMLabKitFrame.KEYMAP_HOME; +import static fiji.plugin.trackmate.gui.editor.labkit.component.TMLabKitFrame.EDITOR_KEYMAP_HOME; import static fiji.plugin.trackmate.gui.editor.labkit.component.TMLabKitFrame.KEY_CONFIG_SCOPE; import java.io.IOException; @@ -37,19 +37,19 @@ import bdv.ui.keymap.KeymapManager; import fiji.plugin.trackmate.util.TMUtils; -public class TMKeymapManager extends KeymapManager +public class EditorKeymapManager extends KeymapManager { private static final String DEFAULT_KEYMAP_PATH = "/keymaps/Default-BDV.yaml"; - public TMKeymapManager() + public EditorKeymapManager() { - super( KEYMAP_HOME ); + super( EDITOR_KEYMAP_HOME ); } static Keymap loadBDVKeymap() { - final InputStream inputStream = TMKeymapManager.class.getResourceAsStream( DEFAULT_KEYMAP_PATH ); + final InputStream inputStream = EditorKeymapManager.class.getResourceAsStream( DEFAULT_KEYMAP_PATH ); if ( inputStream == null ) { diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMBasicLabelingComponent.java b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMBasicLabelingComponent.java index d3a153ea7..9b9646494 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMBasicLabelingComponent.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMBasicLabelingComponent.java @@ -58,13 +58,13 @@ import fiji.plugin.trackmate.detection.DetectionUtils; import fiji.plugin.trackmate.gui.editor.labkit.component.TMFloodFillController.FloodEraseMode; import fiji.plugin.trackmate.gui.editor.labkit.component.TMFloodFillController.FloodFillMode; +import fiji.plugin.trackmate.gui.editor.labkit.model.TMImageLabelingModel; import net.miginfocom.swing.MigLayout; import sc.fiji.labkit.ui.bdv.BdvAutoContrast; import sc.fiji.labkit.ui.bdv.BdvLayer; import sc.fiji.labkit.ui.labeling.Label; import sc.fiji.labkit.ui.labeling.LabelsLayer; import sc.fiji.labkit.ui.models.Holder; -import sc.fiji.labkit.ui.models.ImageLabelingModel; import sc.fiji.labkit.ui.models.LabelingModel; /** @@ -81,7 +81,7 @@ public class TMBasicLabelingComponent extends JPanel implements AutoCloseable private final JFrame dialogBoxOwner; - private final ImageLabelingModel model; + private final TMImageLabelingModel model; private JSlider zSlider; @@ -95,7 +95,7 @@ public class TMBasicLabelingComponent extends JPanel implements AutoCloseable public TMBasicLabelingComponent( final JFrame dialogBoxOwner, - final ImageLabelingModel model, + final TMImageLabelingModel model, final BdvOptions options ) { this.model = model; diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMFloodFillController.java b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMFloodFillController.java index f2602ebe1..dda63f2c0 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMFloodFillController.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMFloodFillController.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 * . @@ -46,9 +46,11 @@ import bdv.util.BdvHandle; import bdv.viewer.ViewerPanel; +import fiji.plugin.trackmate.gui.editor.labkit.model.TMImageLabelingModel; import fiji.plugin.trackmate.util.TMUtils; import gnu.trove.map.TIntIntMap; import gnu.trove.map.hash.TIntIntHashMap; +import net.imglib2.Interval; import net.imglib2.Localizable; import net.imglib2.Point; import net.imglib2.RandomAccess; @@ -65,7 +67,6 @@ import sc.fiji.labkit.ui.brush.BdvMouseBehaviourUtils; import sc.fiji.labkit.ui.brush.FloodFillController; import sc.fiji.labkit.ui.labeling.Label; -import sc.fiji.labkit.ui.models.LabelingModel; /** * Copied from FloodFillController @@ -143,7 +144,7 @@ public String getTooltip() private final ViewerPanel viewer; - private final LabelingModel model; + private final TMImageLabelingModel model; private final BdvHandle bdv; @@ -155,8 +156,7 @@ public String getTooltip() private Collection< Label > visibleLabels() { - return model.labeling().get().getLabels().stream().filter( Label::isVisible ) - .collect( Collectors.toList() ); + return model.labeling().get().getLabels().stream().filter( Label::isVisible ).collect( Collectors.toList() ); } private final FloodFillClick floodFillBehaviour = new FloodFillClick( () -> { @@ -191,7 +191,7 @@ private Collection< Label > visibleLabels() } } ); - public TMFloodFillController( final BdvHandle bdv, final LabelingModel model ) + public TMFloodFillController( final BdvHandle bdv, final TMImageLabelingModel model ) { this.bdv = bdv; this.viewer = bdv.getViewerPanel(); @@ -286,7 +286,17 @@ protected void floodFill( final RealLocalizable imageCoordinates ) final Point seed = roundAndReduceDimension( imageCoordinates, frame.numDimensions() ); final Consumer< Set< Label > > operation = operationFactory.get(); if ( askUser( frame, seed, operation ) ) - FloodFill.doFloodFillOnActiveLabels( frame, seed, operation ); + { + // Start undo before modifying + final int frameIndex = viewer.state().getCurrentTimepoint(); + model.undoRedo().startUndo( frameIndex ); + + // Execute flood fill and get the affected region + final Interval region = FloodFill.doFloodFillOnActiveLabels( frame, seed, operation ); + + // Set undo point after modifying + model.undoRedo().setUndoPoint( region ); + } } } @@ -297,8 +307,12 @@ private boolean askUser( final RandomAccessibleInterval< LabelingType< Label > > { final String message = "Are you sure to flood fill the background of this 3d image?" + "\n(This may take a while to compute.)"; - final int result = JOptionPane.showConfirmDialog( viewer, message, "Flood Fill 3D Image", - JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE ); + final int result = JOptionPane.showConfirmDialog( + viewer, + message, + "Flood Fill 3D Image", + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE ); return result == JOptionPane.OK_OPTION; } return true; @@ -323,11 +337,9 @@ public void click( final int x, final int y ) private RandomAccessibleInterval< LabelingType< Label > > labeling() { - final RandomAccessibleInterval< LabelingType< Label > > label = model.labeling() - .get(); + final RandomAccessibleInterval< LabelingType< Label > > label = model.labeling().get(); if ( model.isTimeSeries() ) - return Views.hyperSlice( label, label - .numDimensions() - 1, viewer.state().getCurrentTimepoint() ); + return Views.hyperSlice( label, label.numDimensions() - 1, viewer.state().getCurrentTimepoint() ); return label; } @@ -348,30 +360,35 @@ private static class FloodFill * @param seed * Seed point. * @param operation - * Operation that es performed for the flood filled pixels. + * Operation that is performed for the flood filled pixels. + * @return the bounding box of the filled region as a {@link Interval}. */ - public static void doFloodFillOnActiveLabels( - final RandomAccessibleInterval< LabelingType< Label > > labeling, final Point seed, + public static Interval doFloodFillOnActiveLabels( + final RandomAccessibleInterval< LabelingType< Label > > labeling, + final Point seed, final Consumer< ? super LabelingType< Label > > operation ) { final Set< Label > seedValue = getPixel( labeling, seed ).copy(); - final Predicate< LabelingType< Label > > visit = value -> activeLabelsAreEquals( value, - seedValue ); - cachedFloodFill( labeling, seed, visit, operation ); + final Predicate< LabelingType< Label > > visit = value -> activeLabelsAreEquals( value, seedValue ); + return cachedFloodFill( labeling, seed, visit, operation ); } // package-private to allow testing - static < T > void cachedFloodFill( - final RandomAccessibleInterval< LabelingType< T > > image, final Localizable seed, - final Predicate< ? super LabelingType< T > > visit, final Consumer< ? super LabelingType< T > > operation ) + static < T > Interval cachedFloodFill( + final RandomAccessibleInterval< LabelingType< T > > image, + final Localizable seed, + final Predicate< ? super LabelingType< T > > visit, + final Consumer< ? super LabelingType< T > > operation ) { final Predicate< LabelingType< T > > cachedVisit = new CacheForPredicateLabelingType<>( visit ); final Consumer< LabelingType< T > > cachedOperation = new CacheForOperationLabelingType<>( operation ); - doFloodFill( image, seed, cachedVisit, cachedOperation ); + return doFloodFill( image, seed, cachedVisit, cachedOperation ); } - private static < T extends Type< T > > void doFloodFill( - final RandomAccessibleInterval< T > image, final Localizable seed, final Predicate< T > visit, + private static < T extends Type< T > > Interval doFloodFill( + final RandomAccessibleInterval< T > image, + final Localizable seed, + final Predicate< T > visit, final Consumer< T > operation ) { final RandomAccess< T > ra = image.randomAccess(); @@ -380,36 +397,33 @@ private static < T extends Type< T > > void doFloodFill( final T seedValueChanged = seedValue.copy(); operation.accept( seedValueChanged ); if ( visit.test( seedValueChanged ) ) - return; + return null; final BiPredicate< T, T > filter = ( f, s ) -> visit.test( f ); @SuppressWarnings( "deprecation" ) - final ExtendedRandomAccessibleInterval< T, RandomAccessibleInterval< T > > target = - Views.extendValue( image, seedValueChanged ); + final ExtendedRandomAccessibleInterval< T, RandomAccessibleInterval< T > > target = Views.extendValue( image, seedValueChanged ); final DiamondShape shape = new DiamondShape( 1 ); - net.imglib2.algorithm.fill.FloodFill.fill( target, target, seed, shape, - filter, operation ); + + return fiji.plugin.trackmate.gui.editor.labkit.util.FloodFill.fill( target, target, seed, shape, filter, operation ); } - private static boolean activeLabelsAreEquals( final LabelingType< Label > a, - final Set< Label > b ) + private static boolean activeLabelsAreEquals( final LabelingType< Label > a, final Set< Label > b ) { - final boolean bIsSubSetOfA = b.stream().filter( Label::isVisible ).allMatch( - a::contains ); - final boolean aIsSubSetOfB = a.stream().filter( Label::isVisible ).allMatch( - b::contains ); + final boolean bIsSubSetOfA = b.stream().filter( Label::isVisible ).allMatch( a::contains ); + final boolean aIsSubSetOfB = a.stream().filter( Label::isVisible ).allMatch( b::contains ); return aIsSubSetOfB && bIsSubSetOfA; } - private static < T > T getPixel( final RandomAccessible< T > image, - final Localizable position ) + private static < T > T getPixel( final RandomAccessible< T > image, final Localizable position ) { final RandomAccess< T > ra = image.randomAccess(); ra.setPosition( position ); return ra.get(); } - public static boolean isBackgroundFloodFill( final RandomAccessibleInterval< LabelingType< Label > > frame, - final Point seed, final Consumer< Set< Label > > operation ) + public static boolean isBackgroundFloodFill( + final RandomAccessibleInterval< LabelingType< Label > > frame, + final Point seed, + final Consumer< Set< Label > > operation ) { final LabelingType< Label > seedValue = frame.randomAccess().setPositionAndGet( seed ); final LabelingType< Label > changedSeedValue = seedValue.copy(); @@ -421,8 +435,7 @@ public static boolean isBackgroundFloodFill( final RandomAccessibleInterval< Lab return isBackgroundFill && operationHasEffect; } - private static class CacheForPredicateLabelingType< T > implements - Predicate< LabelingType< T > > + private static class CacheForPredicateLabelingType< T > implements Predicate< LabelingType< T > > { private final Predicate< ? super LabelingType< T > > predicate; @@ -451,8 +464,7 @@ public boolean test( final LabelingType< T > ts ) } } - private static class CacheForOperationLabelingType< T > implements - Consumer< LabelingType< T > > + private static class CacheForOperationLabelingType< T > implements Consumer< LabelingType< T > > { private final Consumer< ? super LabelingType< T > > operation; diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMLabKitActions.java b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMLabKitActions.java index 0de248f0c..c3883c5b7 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMLabKitActions.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMLabKitActions.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 * . @@ -24,6 +24,9 @@ import static fiji.plugin.trackmate.gui.editor.labkit.component.TMLabKitFrame.KEY_CONFIG_CONTEXT; import static fiji.plugin.trackmate.gui.editor.labkit.component.TMLabKitFrame.KEY_CONFIG_SCOPE; +import java.awt.Font; +import java.awt.Frame; + import javax.swing.SwingUtilities; import org.scijava.plugin.Plugin; @@ -40,8 +43,12 @@ import bdv.ui.keymap.Keymap; import bdv.ui.keymap.KeymapManager; import bdv.ui.keymap.KeymapSettingsPage; +import bdv.viewer.ViewerPanel; +import bdv.viewer.animate.MessageOverlayAnimator; import fiji.plugin.trackmate.gui.editor.labkit.model.TMLabKitModel; import fiji.plugin.trackmate.gui.editor.labkit.model.TMTransformationModel; +import fiji.plugin.trackmate.gui.editor.labkit.model.UndoRedoStack; +import net.imglib2.Interval; public class TMLabKitActions { @@ -49,7 +56,8 @@ public class TMLabKitActions public static void install( final Actions actions, final TMLabKitModel model, - final TMLabKitFrame frame, + final Frame frame, + final ViewerPanel viewerPanel, final InputActionBindings keybindings, final KeymapManager keymapManager, final AppearanceManager appearanceManager ) @@ -61,11 +69,12 @@ public static void install( */ final PreferencesDialog preferencesDialog = new PreferencesDialog( frame, keymap, new String[] { KEY_CONFIG_CONTEXT } ); + preferencesDialog.setTitle( "Editor Preferences" ); fiji.plugin.trackmate.gui.GuiUtils.positionWindow( preferencesDialog, frame ); BigDataViewerActions.toggleDialogAction( actions, preferencesDialog, BigDataViewerActions.PREFERENCES_DIALOG, BigDataViewerActions.PREFERENCES_DIALOG_KEYS ); - preferencesDialog.addPage( new KeymapSettingsPage( "Keymap", keymapManager, keymapManager.getCommandDescriptions() ) ); - preferencesDialog.addPage( new AppearanceSettingsPage( "Appearance", appearanceManager ) ); + preferencesDialog.addPage( new KeymapSettingsPage( "Editor keymap", keymapManager, keymapManager.getCommandDescriptions() ) ); + preferencesDialog.addPage( new AppearanceSettingsPage( "Editor appearance", appearanceManager ) ); appearanceManager.appearance().updateListeners().add( frame::repaint ); SwingUtilities.invokeLater( () -> appearanceManager.updateLookAndFeel() ); @@ -75,12 +84,62 @@ public static void install( final TMTransformationModel transformationModel = ( TMTransformationModel ) model.imageLabelingModel().transformationModel(); actions.runnableAction( () -> transformationModel.resetView(), RESET_VIEW, RESET_VIEW_KEYS ); + + /* + * Undo / redo actions + */ + + final boolean hasTime = model.imageLabelingModel().isTimeSeries(); + final MessageOverlayAnimator messages = new MessageOverlayAnimator( 2000, 0.01, 0.2, new Font( "Arial", Font.PLAIN, 12 ) ); + viewerPanel.addOverlayAnimator( messages ); + final UndoRedoStack undoRedo = model.imageLabelingModel().undoRedo(); + actions.runnableAction( () -> undo( undoRedo, messages, hasTime ), UNDO, UNDO_KEYS ); + actions.runnableAction( () -> redo( undoRedo, messages, hasTime ), REDO, REDO_KEYS ); + } + + private static final void undo( final UndoRedoStack undoRedo, final MessageOverlayAnimator messages, final boolean hasTime ) + { + final Interval interval = undoRedo.undo(); + if ( interval == null ) + return; + messages.add( "Undo" + undoRedoMsg( interval, hasTime ) ); + } + + private static final void redo( final UndoRedoStack undoRedo, final MessageOverlayAnimator messages, final boolean hasTime ) + { + final Interval interval = undoRedo.redo(); + if ( interval == null ) + return; + messages.add( "Redo" + undoRedoMsg( interval, hasTime ) ); } + private static final String undoRedoMsg( final Interval interval, final boolean hasTime ) + { + String out = ( hasTime ) + ? " at frame " + interval.min( interval.numDimensions() - 1 ) + " @ " + : " @ "; + out += "[" + interval.min( 0 ); + for ( int i = 1; i < interval.numDimensions() - 1; i++ ) + out += ", " + interval.min( i ); + out += "] → [" + interval.max( 0 ); + for ( int i = 1; i < interval.numDimensions() - 1; i++ ) + out += ", " + interval.max( i ); + out += "]"; + return out; + } private static final String RESET_VIEW = "reset view"; + private static final String[] RESET_VIEW_KEYS = new String[] { "shift R " }; + private static final String UNDO = "undo"; + + private static final String[] UNDO_KEYS = new String[] { "ctrl Z", "meta Z" }; + + private static final String REDO = "redo"; + + private static final String[] REDO_KEYS = new String[] { "ctrl shift Z", "meta shift Z" }; + @Plugin( type = CommandDescriptionProvider.class ) public static class Descriptions extends CommandDescriptionProvider { @@ -95,6 +154,8 @@ public void getCommandDescriptions( final CommandDescriptions descriptions ) descriptions.add( RESET_VIEW, RESET_VIEW_KEYS, "Reset the view." ); descriptions.add( BigDataViewerActions.PREFERENCES_DIALOG, BigDataViewerActions.PREFERENCES_DIALOG_KEYS, "Show the Preferences dialog." ); descriptions.add( CloseWindowActions.CLOSE_DIALOG, CloseWindowActions.CLOSE_DIALOG_KEYS, "Close the active dialog." ); + descriptions.add( UNDO, UNDO_KEYS, "Undo the last edit." ); + descriptions.add( REDO, REDO_KEYS, "Redo the last undone edit." ); } } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMLabKitFrame.java b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMLabKitFrame.java index 2a178f23c..c8589ac25 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMLabKitFrame.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMLabKitFrame.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 * . @@ -52,7 +52,6 @@ import javax.swing.SwingUtilities; import javax.swing.UIManager; -import org.scijava.ui.behaviour.MouseAndKeyHandler; import org.scijava.ui.behaviour.io.InputTriggerConfig; import org.scijava.ui.behaviour.io.gui.CommandDescriptionProvider.Scope; import org.scijava.ui.behaviour.util.Actions; @@ -64,7 +63,9 @@ import bdv.ui.keymap.Keymap; import bdv.ui.keymap.KeymapManager; import bdv.util.BdvOptions; +import bdv.viewer.ViewerPanel; import fiji.plugin.trackmate.gui.Icons; +import fiji.plugin.trackmate.gui.editor.labkit.model.TMImageLabelingModel; import fiji.plugin.trackmate.gui.editor.labkit.model.TMLabKitModel; import net.imglib2.Dimensions; import net.imglib2.util.Intervals; @@ -85,7 +86,7 @@ public class TMLabKitFrame extends JFrame private static final long serialVersionUID = 1L; - static final String KEYMAP_HOME = new File( new File( System.getProperty( "user.home" ), ".trackmate" ), "editor" ).getAbsolutePath(); + public static final String EDITOR_KEYMAP_HOME = new File( new File( System.getProperty( "user.home" ), ".trackmate" ), "editor" ).getAbsolutePath(); static final String KEY_CONFIG_CONTEXT = "trackmate-labkit"; @@ -93,9 +94,11 @@ public class TMLabKitFrame extends JFrame private final Notifier onCloseListeners = new Notifier(); - public TMLabKitFrame( final TMLabKitModel model ) + public TMLabKitFrame( final TMLabKitModel model, final EditorKeymapManager kmp, final AppearanceManager am ) { - final ImageLabelingModel imageLabelingModel = model.imageLabelingModel(); + final TMImageLabelingModel imageLabelingModel = model.imageLabelingModel(); + final EditorKeymapManager keymapManager = ( kmp == null ) ? new EditorKeymapManager() : kmp; + final AppearanceManager appearanceManager = ( am == null ) ? new AppearanceManager( EDITOR_KEYMAP_HOME ) : am; /* * Here we create a specific config for BDV, so that we can use a custom @@ -105,10 +108,9 @@ public TMLabKitFrame( final TMLabKitModel model ) * So the only solution is to initialize the BDV window with a custom * keymap, configured rationally, and leave it as is. */ - final AppearanceManager appearanceManager = new AppearanceManager( KEYMAP_HOME ); final KeymapManager bdvKeymapManager = new KeymapManager(); final Keymap bdvKeymap = bdvKeymapManager.getForwardSelectedKeymap(); - bdvKeymap.set( TMKeymapManager.loadBDVKeymap() ); + bdvKeymap.set( EditorKeymapManager.loadBDVKeymap() ); final BdvOptions options = BdvOptions.options() .inputTriggerConfig( bdvKeymap.getConfig() ) @@ -120,6 +122,7 @@ public TMLabKitFrame( final TMLabKitModel model ) // Main central panel, config specific for BDV. final TMBasicLabelingComponent mainPanel = new TMBasicLabelingComponent( this, imageLabelingModel, options ); + final ViewerPanel viewerPanel = mainPanel.getBdvHandle().getViewerPanel(); // Left side bar. final JPanel leftPanel = new JPanel(); @@ -153,12 +156,7 @@ public TMLabKitFrame( final TMLabKitModel model ) final TriggerBehaviourBindings triggerbindings = new TriggerBehaviourBindings(); SwingUtilities.replaceUIActionMap( getRootPane(), keybindings.getConcatenatedActionMap() ); SwingUtilities.replaceUIInputMap( getRootPane(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, keybindings.getConcatenatedInputMap() ); - final MouseAndKeyHandler mouseAndKeyHandler = new MouseAndKeyHandler(); - mouseAndKeyHandler.setInputMap( triggerbindings.getConcatenatedInputTriggerMap() ); - mouseAndKeyHandler.setBehaviourMap( triggerbindings.getConcatenatedBehaviourMap() ); - addHandler( mouseAndKeyHandler ); - final TMKeymapManager keymapManager = new TMKeymapManager(); final InputTriggerConfig inputTriggerConfig = keymapManager.getForwardSelectedKeymap().getConfig(); // Actions instance @@ -184,6 +182,7 @@ public TMLabKitFrame( final TMLabKitModel model ) myActions, model, this, + viewerPanel, keybindings, keymapManager, appearanceManager ); diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMLabelBrushController.java b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMLabelBrushController.java index 5ad21f4d7..dcf012d61 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMLabelBrushController.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/component/TMLabelBrushController.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 * . @@ -27,10 +27,7 @@ import java.awt.Cursor; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; -import java.util.Arrays; -import java.util.List; import java.util.function.Consumer; -import java.util.stream.Collectors; import javax.swing.Timer; @@ -47,8 +44,11 @@ import bdv.util.Affine3DHelpers; import bdv.util.BdvHandle; import bdv.viewer.ViewerPanel; +import fiji.plugin.trackmate.gui.editor.labkit.model.TMImageLabelingModel; +import fiji.plugin.trackmate.gui.editor.labkit.model.UndoRedoStack; import fiji.plugin.trackmate.util.TMUtils; import net.imglib2.FinalInterval; +import net.imglib2.Interval; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealLocalizable; @@ -66,7 +66,6 @@ import sc.fiji.labkit.ui.brush.neighborhood.Ellipsoid; import sc.fiji.labkit.ui.brush.neighborhood.RealPoints; import sc.fiji.labkit.ui.labeling.Label; -import sc.fiji.labkit.ui.models.LabelingModel; import sc.fiji.labkit.ui.panel.GuiUtils; import sc.fiji.labkit.ui.utils.Notifier; @@ -85,7 +84,9 @@ public enum PaintBrushMode REPLACE( "Replace", "Paint over existing labels." ), /** Add the painted label to existing labels. */ ADD( "Add", "Add selected label to existing ones." ), - /** Only paint over the background. If a label exists, don't change it.*/ + /** + * Only paint over the background. If a label exists, don't change it. + */ DONT_OVERWRITE( "Preserve", "Don't overwrite existing labels, only paint on background." ); private final String name; @@ -152,7 +153,7 @@ public String getTooltip() private final ViewerPanel viewer; - private final LabelingModel model; + private final TMImageLabelingModel model; private final BrushCursor brushCursor; @@ -160,9 +161,9 @@ public String getTooltip() private final MouseAdapter moveBrushAdapter = GuiUtils.toMouseListener( moveBrushBehaviour ); - private final PaintBehavior paintBehaviour = new PaintBehavior( true ); + private final PaintBehavior paintBehaviour; - private final PaintBehavior eraseBehaviour = new PaintBehavior( false ); + private final PaintBehavior eraseBehaviour; private PaintBrushMode paintBrushMode; @@ -176,8 +177,13 @@ public String getTooltip() private boolean planarMode = false; - public TMLabelBrushController( final BdvHandle bdv, final LabelingModel model ) + public TMLabelBrushController( final BdvHandle bdv, final TMImageLabelingModel model ) { + // Behaviors + final boolean is2D = model.labeling().get().numDimensions() == 2; + this.paintBehaviour = new PaintBehavior( true, is2D ); + this.eraseBehaviour = new PaintBehavior( false, is2D ); + this.bdv = bdv; this.viewer = bdv.getViewerPanel(); this.brushCursor = new BrushCursor( model ); @@ -186,6 +192,7 @@ public TMLabelBrushController( final BdvHandle bdv, final LabelingModel model ) viewer.getDisplay().overlays().add( brushCursor ); viewer.transformListeners().add( affineTransform3D -> updateBrushOverlayRadius() ); + // Load defaults from prefs. final PrefService prefs = TMUtils.getContext().getService( PrefService.class ); brushDiameter = prefs.getDouble( TMLabelBrushController.class, PREF_KEY_BRUSH_DIAMETER, 1. ); @@ -286,46 +293,62 @@ public void setPlanarMode( final boolean planarMode ) private class PaintBehavior implements DragBehaviour { - /** - * If true we paint. If false we erase. - */ + /** If true we paint. If false we erase. */ private final boolean paint; private RealPoint before; - public PaintBehavior( final boolean paint ) + /** + * The bounding box of the current stroke, used for undo/redo region. + */ + private Interval strokeRegion; + + private final boolean is2D; + + public PaintBehavior( final boolean paint, final boolean is2D ) { this.paint = paint; + this.is2D = is2D; } private void paint( final RealLocalizable screenCoordinates ) { synchronized ( viewer ) { - RandomAccessible< LabelingType< Label > > extended = extendLabelingType( getFrame() ); final double radius = Math.max( 0, ( brushDiameter - 1 ) * 0.5 ); - final AffineTransform3D m = displayToImageTransformation(); - final double[] screen = { screenCoordinates.getDoublePosition( 0 ), screenCoordinates - .getDoublePosition( 1 ), 0 }; - double[] center = new double[ 3 ]; - m.apply( screen, center ); - if ( extended.numDimensions() == 3 && planarMode ) + final double[] center = posToImageCoords( screenCoordinates ); + final double[] axes = radiusToImageCoords( radius ); + + RandomAccessible< LabelingType< Label > > extended = extendLabelingType( getFrameLabeling() ); + if ( !is2D && planarMode ) extended = Views.hyperSlice( extended, 2, Math.round( center[ 2 ] ) ); - final AffineTransform3D labelTransform = model.labelTransformation(); - final double pixelWidth = RealPoints.length( labelTransform.d( 0 ) ); - final double pixelHeight = RealPoints.length( labelTransform.d( 1 ) ); - final double pixelDepth = RealPoints.length( labelTransform.d( 2 ) ); - double[] axes = { radius, radius * pixelWidth / pixelHeight, radius * pixelWidth / - pixelDepth }; - if ( extended.numDimensions() == 2 ) - { - center = Arrays.copyOf( center, 2 ); - axes = Arrays.copyOf( axes, 2 ); - } + final IterableRegion< BitType > region = Ellipsoid.asIterableRegion( center, axes ); Regions.sample( region, extended ).forEach( pixelOperation() ); } + } + private double[] radiusToImageCoords( final double radius ) + { + final AffineTransform3D labelTransform = model.labelTransformation(); + final double pixelWidth = RealPoints.length( labelTransform.d( 0 ) ); + final double pixelHeight = RealPoints.length( labelTransform.d( 1 ) ); + if ( is2D ) + return new double[] { radius, radius * pixelWidth / pixelHeight }; + + final double pixelDepth = RealPoints.length( labelTransform.d( 2 ) ); + return new double[] { radius, radius * pixelWidth / pixelHeight, radius * pixelWidth / pixelDepth }; + } + + private double[] posToImageCoords( final RealLocalizable screenCoordinates ) + { + final AffineTransform3D m = displayToImageTransformation(); + final double[] screen = { screenCoordinates.getDoublePosition( 0 ), screenCoordinates.getDoublePosition( 1 ), 0 }; + final double[] center = new double[ 3 ]; + m.apply( screen, center ); + if ( is2D ) + return new double[] { center[ 0 ], center[ 1 ] }; + return center; } private Consumer< LabelingType< Label > > pixelOperation() @@ -366,28 +389,16 @@ private Consumer< LabelingType< Label > > pixelOperation() if ( label != null ) // otherwise fall through return pixel -> pixel.remove( label ); case REMOVE_ALL: - final List< Label > visibleLabels = getVisibleLabels(); - return pixel -> pixel.removeAll( visibleLabels ); + return pixel -> pixel.clear(); default: throw new IllegalArgumentException( "Unknown erase brush mode: " + eraseBrushMode ); } } } - private List< Label > getVisibleLabels() - { - final List< Label > visibleLabels = - model.labeling().get().getLabels().stream() - .filter( Label::isVisible ) - .collect( Collectors.toList() ); - return visibleLabels; - } - - private RandomAccessible< LabelingType< Label > > extendLabelingType( - final RandomAccessibleInterval< LabelingType< Label > > slice ) + private static final RandomAccessible< LabelingType< Label > > extendLabelingType( final RandomAccessibleInterval< LabelingType< Label > > slice ) { - final LabelingType< Label > variable = slice.randomAccess() - .setPositionAndGet( Intervals.minAsLongArray( slice ) ).createVariable(); + final LabelingType< Label > variable = slice.randomAccess().setPositionAndGet( Intervals.minAsLongArray( slice ) ).createVariable(); variable.clear(); @SuppressWarnings( "deprecation" ) final RandomAccessible< LabelingType< Label > > extended = Views.extendValue( slice, variable ); @@ -411,32 +422,19 @@ private AffineTransform3D viewerTransformation() private void paint( final RealLocalizable a, final RealLocalizable b ) { - final long distance = ( long ) ( 4 * ( distance( a, b ) + 1 ) ); + final double dist = LinAlgHelpers.distance( a.positionAsDoubleArray(), b.positionAsDoubleArray() ); + final long distance = ( long ) ( 4 * ( dist + 1 ) ); final long step = ( long ) Math.max( brushDiameter, 1.0 ); - for ( long i = 0; i < distance; i += step ) - paint( interpolate( ( double ) i / ( double ) distance, a, b ) ); - } - RealLocalizable interpolate( final double ratio, final RealLocalizable a, - final RealLocalizable b ) - { - final RealPoint result = new RealPoint( a.numDimensions() ); - for ( int d = 0; d < result.numDimensions(); d++ ) - result.setPosition( ratio * a.getDoublePosition( d ) + ( 1 - ratio ) * b - .getDoublePosition( d ), d ); - return result; - } - - double distance( final RealLocalizable a, final RealLocalizable b ) - { - return LinAlgHelpers.distance( asArray( a ), asArray( b ) ); - } + final RealPoint location = new RealPoint( a.numDimensions() ); + for ( long i = 0; i < distance; i += step ) + { + final double ratio = ( double ) i / ( double ) distance; + for ( int d = 0; d < location.numDimensions(); d++ ) + location.setPosition( ratio * a.getDoublePosition( d ) + ( 1 - ratio ) * b.getDoublePosition( d ), d ); - private double[] asArray( final RealLocalizable a ) - { - final double[] result = new double[ a.numDimensions() ]; - a.localize( result ); - return result; + paint( location ); + } } @Override @@ -447,8 +445,15 @@ public void init( final int x, final int y ) makeLabelVisible(); final RealPoint coords = new RealPoint( x, y ); this.before = coords; - paint( coords ); + + // Initialize stroke region final double radius = getBrushDisplayRadius(); + strokeRegion = createStrokeRegion( posToImageCoords( coords ), ( int ) Math.ceil( brushDiameter / 2. ) ); + + // Snapshot the current state for undo/redo + model.undoRedo().startUndo( viewer.state().getCurrentTimepoint() ); + + paint( coords ); fireBitmapChanged( coords, coords, radius ); } @@ -460,6 +465,13 @@ public void drag( final int x, final int y ) paint( before, coords ); final double radius = getBrushDisplayRadius(); fireBitmapChanged( before, coords, radius ); + + // Expand stroke region + strokeRegion = expandStrokeRegion( + strokeRegion, + posToImageCoords( before ), + posToImageCoords( coords ), + radiusToImageCoords( radius ) ); this.before = coords; } @@ -468,6 +480,35 @@ public void end( final int x, final int y ) { brushCursor.setPosition( x, y ); brushCursor.setFontVisible( true ); + + final UndoRedoStack undo = model.undoRedo(); + undo.setUndoPoint( Intervals.intersect( getFrameLabeling(), strokeRegion ) ); + } + + /** Creates an interval representing the initial brush stroke region. */ + private static final FinalInterval createStrokeRegion( final double[] center, final int radius ) + { + final long[] min = new long[ 2 ]; + final long[] max = new long[ 2 ]; + for ( int d = 0; d < 2; d++ ) + { + min[ d ] = ( long ) Math.floor( center[ d ] - radius ); + max[ d ] = ( long ) Math.ceil( center[ d ] + radius ); + } + return new FinalInterval( min, max ); + } + + /** Expands the stroke region to include a new brush position. */ + private static final Interval expandStrokeRegion( final Interval current, final double[] centerA, final double[] centerB, final double[] radius ) + { + final long[] min = new long[ current.numDimensions() ]; + final long[] max = new long[ current.numDimensions() ]; + for ( int d = 0; d < current.numDimensions(); d++ ) + { + min[ d ] = Math.min( current.min( d ), ( long ) Math.floor( Math.min( centerA[ d ], centerB[ d ] ) - radius[ d ] ) ); + max[ d ] = Math.max( current.max( d ), ( long ) Math.ceil( Math.max( centerA[ d ], centerB[ d ] ) + radius[ d ] ) ); + } + return new FinalInterval( min, max ); } } @@ -485,23 +526,21 @@ private void makeLabelVisible() private double getBrushDisplayRadius() { - return brushDiameter * 0.5 * getScale( model.labelTransformation() ) * - getScale( paintBehaviour.viewerTransformation() ); - } - - // TODO: find a good place - private double getScale( final AffineTransform3D transformation ) - { - return Affine3DHelpers.extractScale( transformation, 0 ); + final double labelScale = Affine3DHelpers.extractScale( model.labelTransformation(), 0 ); + final double viewScale = Affine3DHelpers.extractScale( paintBehaviour.viewerTransformation(), 0 ); + return brushDiameter * 0.5 * labelScale * viewScale; } - private RandomAccessibleInterval< LabelingType< Label > > getFrame() + /** + * Returns the labeling of the current frame. + * + * @return the labeling of the current frame + */ + private RandomAccessibleInterval< LabelingType< Label > > getFrameLabeling() { - final RandomAccessibleInterval< LabelingType< Label > > frame = model.labeling() - .get(); + final RandomAccessibleInterval< LabelingType< Label > > frame = model.labeling().get(); if ( this.model.isTimeSeries() ) - return Views.hyperSlice( frame, frame - .numDimensions() - 1, viewer.state().getCurrentTimepoint() ); + return Views.hyperSlice( frame, frame.numDimensions() - 1, viewer.state().getCurrentTimepoint() ); return frame; } @@ -512,10 +551,8 @@ private void fireBitmapChanged( final RealPoint a, final RealPoint b, double rad final long[] max = new long[ 2 ]; for ( int d = 0; d < 2; d++ ) { - min[ d ] = ( long ) ( Math.min( a.getDoublePosition( d ), b.getDoublePosition( - d ) ) - radius ); - max[ d ] = ( long ) ( Math.ceil( Math.max( a.getDoublePosition( d ), b - .getDoublePosition( d ) ) ) + radius ); + min[ d ] = ( long ) ( Math.min( a.getDoublePosition( d ), b.getDoublePosition( d ) ) - radius ); + max[ d ] = ( long ) ( Math.ceil( Math.max( a.getDoublePosition( d ), b.getDoublePosition( d ) ) ) + radius ); } model.dataChangedNotifier().notifyListeners( new FinalInterval( min, max ) ); } @@ -524,8 +561,7 @@ private class ChangeBrushRadius implements ScrollBehaviour { @Override - public void scroll( final double wheelRotation, final boolean isHorizontal, - final int x, final int y ) + public void scroll( final double wheelRotation, final boolean isHorizontal, final int x, final int y ) { if ( !isHorizontal ) { diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/ImpBdvShowable.java b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/ImpBdvShowable.java index f6f114804..38e610cd8 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/ImpBdvShowable.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/ImpBdvShowable.java @@ -20,6 +20,27 @@ * #L% */ package fiji.plugin.trackmate.gui.editor.labkit.model; +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ import java.awt.Color; import java.util.Arrays; @@ -78,7 +99,6 @@ public class ImpBdvShowable implements BdvShowable */ public static < T extends NumericType< T > > ImpBdvShowable fromImp( final ImagePlus imp ) { - @SuppressWarnings( "unchecked" ) final ImgPlus< T > src = TMUtils.rawWraps( imp ); if ( src.dimensionIndex( Axes.CHANNEL ) < 0 ) Views.addDimension( src ); diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/LabkitImporter.java index c25197656..f0259bb11 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/LabkitImporter.java @@ -185,7 +185,8 @@ public void run() for ( final Label label : modifiedLabels ) { final IterableRegion< BitType > region = regions.get( label ); - final List< Spot > spots = MaskUtils.fromThresholdWithROI( region, region, calibration, threshold, simplifyContours, numThreads, null ); + final double smoothingScale = -1.; // no smoothing + final List< Spot > spots = MaskUtils.fromThresholdWithROI( region, region, calibration, threshold, simplifyContours, smoothingScale, numThreads, null ); map.put( label, spots ); } allModifiedSpots.put( lt, map ); @@ -200,7 +201,8 @@ public void run() for ( long t = minT; t <= maxT; t++ ) { final IntervalView< BitType > slice = Views.hyperSlice( region, timeAxis, t ); - final List< Spot > spots = MaskUtils.fromThresholdWithROI( slice, slice, calibration, threshold, simplifyContours, numThreads, null ); + final double smoothingScale = -1.; // no smoothing + final List< Spot > spots = MaskUtils.fromThresholdWithROI( slice, slice, calibration, threshold, simplifyContours, smoothingScale, numThreads, null ); Map< Label, List< Spot > > map = allModifiedSpots.get( Integer.valueOf( ( int ) t ) ); if ( map == null ) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/TMImageLabelingModel.java b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/TMImageLabelingModel.java index 06fb80b0f..a850048bf 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/TMImageLabelingModel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/TMImageLabelingModel.java @@ -44,12 +44,15 @@ public class TMImageLabelingModel extends ImageLabelingModel private RandomAccessibleInterval< UnsignedIntType > initialIndexImg; + private final UndoRedoStack undoStack; + public TMImageLabelingModel( final InputImage inputImage ) { super( inputImage ); final ImgPlus< ? > image = inputImage.imageForSegmentation(); + this.undoStack = new UndoRedoStack( this ); final boolean isTimeSeries = ImgPlusViewsOld.hasAxis( image, Axes.TIME ); - tmTranslationModel = new TMTransformationModel( isTimeSeries ); + this.tmTranslationModel = new TMTransformationModel( isTimeSeries ); } @Override @@ -73,4 +76,14 @@ void setInitialState( final Map< Label, Spot > initialMapping, final RandomAcces this.initialMapping = initialMapping; this.initialIndexImg = initialIndexImg; } + + /** + * Returns the undo/redo stack associated with this model. + * + * @return the undo/redo stack. + */ + public UndoRedoStack undoRedo() + { + return undoStack; + } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/TMLabKitModel.java b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/TMLabKitModel.java index 6ae51e8b2..065c1d4ea 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/TMLabKitModel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/TMLabKitModel.java @@ -33,9 +33,9 @@ import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.detection.DetectionUtils; import fiji.plugin.trackmate.features.FeatureUtils; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; -import fiji.plugin.trackmate.util.SpotUtil; import fiji.plugin.trackmate.util.TMUtils; import fiji.plugin.trackmate.visualization.FeatureColorGenerator; import ij.ImagePlus; @@ -134,6 +134,16 @@ public boolean isTrained() throw new UnsupportedOperationException( "TrackMate editor does not have segmenting capabilities" ); } + public final static TMLabKitModel create( final GuiModel guiModel, final int timepoint ) + { + final Model model = guiModel.getModel(); + final ImagePlus imp = guiModel.getSettings().imp; + final Interval interval = TMUtils.createROIInterval( imp ); + final DisplaySettings displaySettings = guiModel.getDisplaySettings(); + final Context context = TMUtils.getContext(); + return create( model, imp, interval, displaySettings, timepoint, context ); + } + /** * Creates a LabKit {@link TMLabKitModel} from the specified TrackMate * model. @@ -489,7 +499,7 @@ private static void processFrame( { final Label label = labeling.addLabel( spot.getName() ); label.setColor( new ARGBType( colorGen.color( spot ).getRGB() ) ); - final Cursor< UnsignedIntType > c = SpotUtil.iterable( spot, img ).localizingCursor(); + final Cursor< UnsignedIntType > c = spot.iterable( img ).localizingCursor(); while ( c.hasNext() ) { c.fwd(); @@ -526,7 +536,7 @@ private static void processFrame( final Label label = labeling.addLabel( spot.getName() ); label.setColor( new ARGBType( colorGen.color( spot ).getRGB() ) ); - final Cursor< UnsignedIntType > c = SpotUtil.iterable( spot, img ).localizingCursor(); + final Cursor< UnsignedIntType > c = spot.iterable( img ).localizingCursor(); while ( c.hasNext() ) { c.fwd(); diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/TMLabKitUtils.java b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/TMLabKitUtils.java index a8a0e1448..9af3c6e20 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/TMLabKitUtils.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/TMLabKitUtils.java @@ -27,10 +27,10 @@ import java.util.concurrent.atomic.AtomicBoolean; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.SpotRoi; import fiji.plugin.trackmate.util.TMUtils; import net.imagej.ImgPlus; import net.imagej.axis.Axes; +import net.imagej.axis.AxisType; import net.imagej.axis.CalibratedAxis; import net.imglib2.RandomAccessibleInterval; import net.imglib2.img.Img; @@ -57,34 +57,22 @@ static final int timeAxis( final Labeling labeling ) static final void boundingBox( final Spot spot, final ImgPlus< UnsignedIntType > img, final long[] min, final long[] max ) { + final double[] rmin = spot.minAsDoubleArray(); + final double[] rmax = spot.maxAsDoubleArray(); final double[] calibration = TMUtils.getSpatialCalibration( img ); - final SpotRoi roi = spot.getRoi(); - if ( roi == null ) - { - final double cx = spot.getDoublePosition( 0 ); - final double cy = spot.getDoublePosition( 1 ); - final double r = spot.getFeature( Spot.RADIUS ).doubleValue(); - min[ 0 ] = ( long ) Math.floor( ( cx - r ) / calibration[ 0 ] ); - min[ 1 ] = ( long ) Math.floor( ( cy - r ) / calibration[ 1 ] ); - max[ 0 ] = ( long ) Math.ceil( ( cx + r ) / calibration[ 0 ] ); - max[ 1 ] = ( long ) Math.ceil( ( cy + r ) / calibration[ 1 ] ); - } - else + + final AxisType[] axes = new AxisType[] { Axes.X, Axes.Y, Axes.Z }; + for ( int d = 0; d < max.length; d++ ) { - final double[] x = roi.toPolygonX( calibration[ 0 ], 0, spot.getDoublePosition( 0 ), 1. ); - final double[] y = roi.toPolygonY( calibration[ 1 ], 0, spot.getDoublePosition( 1 ), 1. ); - min[ 0 ] = ( long ) Math.floor( Util.min( x ) ); - min[ 1 ] = ( long ) Math.floor( Util.min( y ) ); - max[ 0 ] = ( long ) Math.ceil( Util.max( x ) ); - max[ 1 ] = ( long ) Math.ceil( Util.max( y ) ); - } + min[ d ] = Math.round( rmin[ d ] / calibration[ d ] ); + min[ d ] = Math.max( 0, min[ d ] ); - min[ 0 ] = Math.max( 0, min[ 0 ] ); - min[ 1 ] = Math.max( 0, min[ 1 ] ); - final long width = img.min( img.dimensionIndex( Axes.X ) ) + img.dimension( img.dimensionIndex( Axes.X ) ); - final long height = img.min( img.dimensionIndex( Axes.Y ) ) + img.dimension( img.dimensionIndex( Axes.Y ) ); - max[ 0 ] = Math.min( width, max[ 0 ] ); - max[ 1 ] = Math.min( height, max[ 1 ] ); + max[ d ] = Math.round( rmax[ d ] / calibration[ d ] ); + final AxisType axis = axes[ d ]; + final int axisDim = img.dimensionIndex( axis ); + final long imgMax = img.min( axisDim ) + img.dimension( axisDim ); + max[ d ] = Math.min( imgMax, max[ d ] ); + } } static final Img< UnsignedIntType > copy( final RandomAccessibleInterval< UnsignedIntType > in ) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/UndoRedoStack.java b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/UndoRedoStack.java new file mode 100644 index 000000000..43ac768be --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/UndoRedoStack.java @@ -0,0 +1,202 @@ +package fiji.plugin.trackmate.gui.editor.labkit.model; + +import java.util.ArrayDeque; +import java.util.Deque; + +import net.imglib2.Interval; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.Img; +import net.imglib2.type.numeric.integer.UnsignedIntType; +import net.imglib2.util.ImgUtil; +import net.imglib2.util.Intervals; +import net.imglib2.util.Util; +import net.imglib2.view.Views; + +/** + * Manages undo/redo history for label editing operations. + */ +public class UndoRedoStack +{ + private final Deque< UndoableCommand > undoStack = new ArrayDeque<>(); + + private final Deque< UndoableCommand > redoStack = new ArrayDeque<>(); + + private final int maxSize; + + private final TMImageLabelingModel model; + + /** + * Snapshot of the index image before painting, used to store state of the + * labeling before an edit. + */ + private RandomAccessibleInterval< UnsignedIntType > snapshot; + + private int currentFrame = -1; + + /** + * Creates a new UndoRedoStack, set to operate on the specified model, with + * the specified maximum size. + * + * @param model + * the model to operate on. + * @param maxSize + * the maximum number of commands to keep in history. + */ + public UndoRedoStack( final TMImageLabelingModel model, final int maxSize ) + { + this.model = model; + this.maxSize = maxSize; + } + + /** + * Creates a new UndoRedoStack for the specified model, with a default + * maximum size of 50 commands. + * + * @param model + * the model to operate on. + */ + public UndoRedoStack( final TMImageLabelingModel model ) + { + this( model, 50 ); + } + + /** + * Starts an undo operation. + *

+ * This method must be called before any edit operation is performed, + * to capture the state of the labeling before the edit. After the edit is + * performed, call {@link #setUndoPoint(int, Interval)} to record the + * edit operation. + * + * @param frame + * the time point of the labeling on which the edit operation is + * performed. + */ + public void startUndo( final int frame ) + { + if ( currentFrame >= 0 ) + throw new IllegalStateException( "UndoRedoStack: startUndo called before previous undo operation was completed. Call setUndoPoint after performing the edit operation." ); + snapshot( frame ); + } + + /** + * Records an undo point after an edit operation is performed. + *

+ * This method must be called after an edit operation is performed, + * to record the state of the labeling after the edit. The region parameter + * specifies the region of the labeling that was affected by the edit. + * + * @param frame + * the time point of the labeling on which the edit operation was + * performed. + * @param region + * the region of the labeling that was affected by the edit. + */ + public void setUndoPoint( final Interval region ) + { + final UndoableCommand current = new UndoableCommand( getFrame( currentFrame ), region, currentFrame ); + current.captureBefore( snapshot ); + current.captureAfter( getFrame( currentFrame ) ); + push( current ); + currentFrame = -1; + } + + private void push( final UndoableCommand command ) + { + redoStack.clear(); + if ( undoStack.size() >= maxSize ) + undoStack.removeFirst(); + + undoStack.addLast( command ); + } + + /** + * Undo the last operation. + * + * @return the region affected by the undo, or null if nothing + * was undone. The last dimension of the returned interval is the + * time point of the labeling on which the undo operation was + * performed. + */ + public Interval undo() + { + if ( undoStack.isEmpty() ) + return null; + + final UndoableCommand command = undoStack.removeLast(); + command.restoreBefore( getFrame( command.frame ) ); + redoStack.addLast( command ); + model.dataChangedNotifier().notifyListeners( null ); + return Intervals.addDimension( command.region, command.frame, command.frame ); + } + + /** + * Redo the last undone operation. + * + * @return the region affected by the redo, or null if nothing + * was redone. The last dimension of the returned interval is the + * time point of the labeling on which the redo operation was + * performed. + */ + public Interval redo() + { + if ( redoStack.isEmpty() ) + return null; + + final UndoableCommand command = redoStack.removeLast(); + command.restoreAfter( getFrame( command.frame ) ); + undoStack.addLast( command ); + model.dataChangedNotifier().notifyListeners( null ); + return Intervals.addDimension( command.region, command.frame, command.frame ); + } + + /** + * Returns whether undo is possible. + * + * @return {@code true} if there are commands to undo + */ + public boolean canUndo() + { + return !undoStack.isEmpty(); + } + + /** + * Returns whether redo is possible. + * + * @return {@code true} if there are commands to redo + */ + public boolean canRedo() + { + return !redoStack.isEmpty(); + } + + /** + * Clears all history. + */ + public void clear() + { + undoStack.clear(); + redoStack.clear(); + } + + private void snapshot( final int frame ) + { + this.currentFrame = frame; + final RandomAccessibleInterval< UnsignedIntType > current = getFrame( frame ); + if ( snapshot == null ) + { + final Img< UnsignedIntType > img = Util.getArrayOrCellImgFactory( current, new UnsignedIntType() ).create( current ); + this.snapshot = img.view().translate( current.minAsLongArray() ); + } + ImgUtil.copy( current, snapshot ); + } + + private RandomAccessibleInterval< UnsignedIntType > getFrame( final int frame ) + { + @SuppressWarnings( "unchecked" ) + final RandomAccessibleInterval< UnsignedIntType > indexImg = ( RandomAccessibleInterval< UnsignedIntType > ) model.labeling().get().getIndexImg(); + if ( model.isTimeSeries() ) + return Views.hyperSlice( indexImg, indexImg.numDimensions() - 1, frame ); + return indexImg; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/UndoableCommand.java b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/UndoableCommand.java new file mode 100644 index 000000000..3b3391bc5 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/model/UndoableCommand.java @@ -0,0 +1,102 @@ +package fiji.plugin.trackmate.gui.editor.labkit.model; + +import net.imglib2.Cursor; +import net.imglib2.Interval; +import net.imglib2.RandomAccessible; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.type.numeric.integer.UnsignedIntType; +import net.imglib2.util.Intervals; +import net.imglib2.view.Views; + +/** + * An {@link EditCommand} that stores before/after snapshots of label indices. + *

+ * The command captures the pixel values in a region before and after an edit + * operation (brush stroke or flood fill). Undo restores the before state, redo + * restores the after state. + */ +public class UndoableCommand +{ + final Interval region; + + final int frame; + + private final int[] beforeValues; + + private final int[] afterValues; + + /** + * Creates a new LabelEditCommand for the specified region. + * + * @param indexImg + * the live index image that will be modified by undo/redo + * @param region + * the interval affected by the edit + * @param frame + */ + UndoableCommand( final RandomAccessible< UnsignedIntType > indexImg, final Interval region, final int frame ) + { + this.region = region; + this.frame = frame; + this.beforeValues = new int[ ( int ) Intervals.numElements( region ) ]; + this.afterValues = new int[ beforeValues.length ]; + } + + /** + * Captures the current state of the index image as the "before" state. Must + * be called BEFORE applying the edit operation. + */ + void captureBefore( final RandomAccessible< UnsignedIntType > before ) + { + captureState( before, beforeValues ); + } + + /** + * Captures the current state of the index image as the "after" state. Must + * be called AFTER applying the edit operation. + */ + void captureAfter( final RandomAccessible< UnsignedIntType > after ) + { + captureState( after, afterValues ); + } + + /** + * Restores the before state to the specified index image. + * + * @param to + * the index image to restore to + */ + void restoreBefore( final RandomAccessible< UnsignedIntType > to ) + { + restoreState( to, beforeValues ); + } + + /** + * Restores the after state to the specified index image. + * + * @param to + * the index image to restore to + */ + void restoreAfter( final RandomAccessible< UnsignedIntType > to ) + { + restoreState( to, afterValues ); + } + + private final void captureState( final RandomAccessible< UnsignedIntType > from, final int[] values ) + { + final RandomAccessibleInterval< UnsignedIntType > view = Views.interval( from, region ); + final Cursor< UnsignedIntType > cursor = Views.flatIterable( view ).cursor(); + int i = 0; + while ( cursor.hasNext() ) + values[ i++ ] = cursor.next().getInteger(); + } + + private final void restoreState( final RandomAccessible< UnsignedIntType > to, final int[] values ) + { + final RandomAccessibleInterval< UnsignedIntType > view = Views.interval( to, region ); + final Cursor< UnsignedIntType > cursor = Views.flatIterable( view ).cursor(); + int i = 0; + while ( cursor.hasNext() ) + cursor.next().setInt( values[ i++ ] ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/util/FloodFill.java b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/util/FloodFill.java new file mode 100644 index 000000000..5e9027673 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/labkit/util/FloodFill.java @@ -0,0 +1,233 @@ +package fiji.plugin.trackmate.gui.editor.labkit.util; + +import java.util.function.BiPredicate; +import java.util.function.Consumer; + +import gnu.trove.list.TLongList; +import gnu.trove.list.array.TLongArrayList; +import net.imglib2.Cursor; +import net.imglib2.FinalInterval; +import net.imglib2.Interval; +import net.imglib2.Localizable; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessible; +import net.imglib2.algorithm.neighborhood.Neighborhood; +import net.imglib2.algorithm.neighborhood.Shape; +import net.imglib2.type.Type; +import net.imglib2.util.Pair; +import net.imglib2.view.Views; + +/** + * Iterative n-dimensional flood fill for arbitrary neighborhoods. + *

+ * This class is modifed from the original + * {@link net.imglib2.algorithm.floodfill.FloodFill} class to be able to return + * the bounding-box (as interval) of the filled region. + * + * @author Philipp Hanslovsky + * @author Stephan Saalfeld + * @author Jean-Yves Tinevez + */ +public class FloodFill +{ + // int or long? current TLongList cannot store more than Integer.MAX_VALUE + private static final int CLEANUP_THRESHOLD = ( int ) 1e5; + + /** + * Iterative n-dimensional flood fill for arbitrary neighborhoods: Starting + * at seed location, write fillLabel into target at current location and + * continue for each pixel in neighborhood defined by shape if neighborhood + * pixel is in the same connected component and fillLabel has not been + * written into that location yet. + * + * Convenience call to + * {@link #fill(RandomAccessible, RandomAccessible, Localizable, Type, Shape, BiPredicate)}. + * seedLabel is extracted from source at seed location. + * + * @param source + * input + * @param target + * {@link RandomAccessible} to be written into. May be the same + * as input. + * @param seed + * Start flood fill at this location. + * @param fillLabel + * Immutable. Value to be written into valid flood fill + * locations. + * @param shape + * Defines neighborhood that is considered for connected + * components, e.g. + * {@link net.imglib2.algorithm.neighborhood.DiamondShape} + * @param + * input pixel type + * @param + * fill label type + * @return the bounding box of the filled region as a {@link Interval}. + */ + public static < T extends Type< T >, U extends Type< U > > Interval fill( + final RandomAccessible< T > source, + final RandomAccessible< U > target, + final Localizable seed, + final U fillLabel, + final Shape shape ) + { + final RandomAccess< T > access = source.randomAccess(); + access.setPosition( seed ); + final T seedValue = access.get().copy(); + final BiPredicate< T, U > filter = ( t, u ) -> t.valueEquals( seedValue ) && !u.valueEquals( fillLabel ); + return fill( source, target, seed, fillLabel, shape, filter ); + } + + /** + * Iterative n-dimensional flood fill for arbitrary neighborhoods: Starting + * at seed location, write fillLabel into target at current location and + * continue for each pixel in neighborhood defined by shape if neighborhood + * pixel is in the same connected component and fillLabel has not been + * written into that location yet. + * + * Convenience call to + * {@link FloodFill#fill(RandomAccessible, RandomAccessible, Localizable, Shape, BiPredicate, Consumer)} + * with {@link Type#set} as writer. + * + * @param source + * input + * @param target + * {@link RandomAccessible} to be written into. May be the same + * as input. + * @param seed + * Start flood fill at this location. + * @param fillLabel + * Immutable. Value to be written into valid flood fill + * locations. + * @param shape + * Defines neighborhood that is considered for connected + * components, e.g. + * {@link net.imglib2.algorithm.neighborhood.DiamondShape} + * @param filter + * Returns true if pixel has not been visited yet and should be + * written into. Returns false if target pixel has been visited + * or source pixel is not part of the same connected component. + * @param + * input pixel type + * @param + * fill label type + * @return the bounding box of the filled region as a {@link Interval}. + */ + public static < T, U extends Type< U > > Interval fill( + final RandomAccessible< T > source, + final RandomAccessible< U > target, + final Localizable seed, + final U fillLabel, + final Shape shape, + final BiPredicate< T, U > filter ) + { + return fill( source, target, seed, shape, filter, targetPixel -> targetPixel.set( fillLabel ) ); + } + + /** + * + * Iterative n-dimensional flood fill for arbitrary neighborhoods: Starting + * at seed location, write fillLabel into target at current location and + * continue for each pixel in neighborhood defined by shape if neighborhood + * pixel is in the same connected component and fillLabel has not been + * written into that location yet. + * + * @param source + * input + * @param target + * {@link RandomAccessible} to be written into. May be the same + * as input. + * @param seed + * Start flood fill at this location. + * @param shape + * Defines neighborhood that is considered for connected + * components, e.g. + * {@link net.imglib2.algorithm.neighborhood.DiamondShape} + * @param filter + * Returns true if pixel has not been visited yet and should be + * written into. Returns false if target pixel has been visited + * or source pixel is not part of the same connected component. + * @param writer + * Defines how fill label is written into target at current + * location. + * @param + * input pixel type + * @param + * fill label type + * @return the bounding box of the filled region as a {@link Interval}. + */ + public static < T, U > Interval fill( + final RandomAccessible< T > source, + final RandomAccessible< U > target, + final Localizable seed, + final Shape shape, + final BiPredicate< T, U > filter, + final Consumer< U > writer ) + { + final int n = source.numDimensions(); + + final RandomAccessible< Pair< T, U > > paired = Views.pair( source, target ); + + TLongList coordinates = new TLongArrayList(); + for ( int d = 0; d < n; ++d ) + { + coordinates.add( seed.getLongPosition( d ) ); + } + + // Initialize bounding box at seed position + final long[] min = new long[ n ]; + final long[] max = new long[ n ]; + for ( int d = 0; d < n; ++d ) + { + final long seedPos = seed.getLongPosition( d ); + coordinates.add( seedPos ); + min[ d ] = seedPos; + max[ d ] = seedPos; + } + + final int cleanupThreshold = n * CLEANUP_THRESHOLD; + + final RandomAccessible< Neighborhood< Pair< T, U > > > neighborhood = shape.neighborhoodsRandomAccessible( paired ); + final RandomAccess< Neighborhood< Pair< T, U > > > neighborhoodAccess = neighborhood.randomAccess(); + + final RandomAccess< U > targetAccess = target.randomAccess(); + targetAccess.setPosition( seed ); + writer.accept( targetAccess.get() ); + + for ( int i = 0; i < coordinates.size(); i += n ) + { + for ( int d = 0; d < n; ++d ) + neighborhoodAccess.setPosition( coordinates.get( i + d ), d ); + + final Cursor< Pair< T, U > > neighborhoodCursor = neighborhoodAccess.get().cursor(); + + while ( neighborhoodCursor.hasNext() ) + { + final Pair< T, U > p = neighborhoodCursor.next(); + if ( filter.test( p.getA(), p.getB() ) ) + { + writer.accept( p.getB() ); + for ( int d = 0; d < n; ++d ) + { + final long pos = neighborhoodCursor.getLongPosition( d ); + coordinates.add( pos ); + // Expand bounding box + if ( pos < min[ d ] ) + min[ d ] = pos; + if ( pos > max[ d ] ) + max[ d ] = pos; + } + } + } + + if ( i > cleanupThreshold ) + { + // TODO should it start from i + n? + coordinates = coordinates.subList( i, coordinates.size() ); + i = 0; + } + + } + return new FinalInterval( min, max ); + } +} \ No newline at end of file diff --git a/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelection.java b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelection.java new file mode 100644 index 000000000..3afc0cbd3 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelection.java @@ -0,0 +1,269 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.gui.featureselector; + +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.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; + +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.detection.DetectionUtils; +import fiji.plugin.trackmate.features.edges.EdgeAnalyzer; +import fiji.plugin.trackmate.features.spot.Spot2DMorphologyAnalyzerFactory; +import fiji.plugin.trackmate.features.spot.Spot3DMorphologyAnalyzerFactory; +import fiji.plugin.trackmate.features.spot.SpotAnalyzerFactory; +import fiji.plugin.trackmate.features.spot.SpotContrastAndSNRAnalyzerFactory; +import fiji.plugin.trackmate.features.track.TrackAnalyzer; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; +import fiji.plugin.trackmate.providers.EdgeAnalyzerProvider; +import fiji.plugin.trackmate.providers.Spot2DMorphologyAnalyzerProvider; +import fiji.plugin.trackmate.providers.Spot3DMorphologyAnalyzerProvider; +import fiji.plugin.trackmate.providers.SpotAnalyzerProvider; +import fiji.plugin.trackmate.providers.TrackAnalyzerProvider; + +public class AnalyzerSelection +{ + + static final List< TrackMateObject > objs = Arrays.asList( new TrackMateObject[] { SPOTS, EDGES, TRACKS } ); + + private final Map< TrackMateObject, Map< String, Boolean > > allAnalyzers = new LinkedHashMap<>(); + + private AnalyzerSelection() + { + allAnalyzers.put( SPOTS, new TreeMap<>() ); + allAnalyzers.put( EDGES, new TreeMap<>() ); + allAnalyzers.put( TRACKS, new TreeMap<>() ); + } + + public boolean isSelected( final TrackMateObject obj, final String key ) + { + final Map< String, Boolean > map = allAnalyzers.get( obj ); + if ( map == null ) + return false; + return map.getOrDefault( key, false ); + } + + public void setSelected( final TrackMateObject obj, final String key, final boolean selected ) + { + final Map< String, Boolean > map = allAnalyzers.get( obj ); + if ( map == null ) + return; + + map.put( key, selected ); + } + + public List< String > getKeys( final TrackMateObject obj ) + { + final Map< String, Boolean > map = allAnalyzers.get( obj ); + if ( map == null ) + return Collections.emptyList(); + + return new ArrayList<>( map.keySet() ); + } + + public List< String > getSelectedAnalyzers( final TrackMateObject obj ) + { + final Map< String, Boolean > map = allAnalyzers.get( obj ); + if ( map == null ) + return Collections.emptyList(); + + return map.entrySet() + .stream() + .filter( e -> e.getValue() ) + .map( e -> e.getKey() ) + .collect( Collectors.toList() ); + } + + /** + * Configure the specified settings object so that it includes only all the + * analyzers in this selection. + * + * @param settings + * the settings to configure. + */ + public void configure( final Settings settings ) + { + settings.clearSpotAnalyzerFactories(); + settings.clearEdgeAnalyzers(); + settings.clearTrackAnalyzers(); + + final List< String > selectionSpotAnalyzers = getSelectedAnalyzers( SPOTS ); + + // Base spot analyzers, in priority order. + final SpotAnalyzerProvider spotAnalyzerProvider = new SpotAnalyzerProvider( settings.imp == null + ? 1 : settings.imp.getNChannels() ); + for ( final String key : spotAnalyzerProvider.getVisibleKeys() ) + { + if ( selectionSpotAnalyzers.contains( key ) ) + { + final SpotAnalyzerFactory< ? > factory = spotAnalyzerProvider.getFactory( key ); + if ( factory != null ) + settings.addSpotAnalyzerFactory( factory ); + } + } + + // Shall we add 2D morphology analyzers? + if ( settings.imp != null + && DetectionUtils.is2D( settings.imp ) + && settings.detectorFactory != null + && settings.detectorFactory.has2Dsegmentation() ) + { + final Spot2DMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider = new Spot2DMorphologyAnalyzerProvider( settings.imp.getNChannels() ); + for ( final String key : spotMorphologyAnalyzerProvider.getVisibleKeys() ) + { + if ( selectionSpotAnalyzers.contains( key ) ) + { + final Spot2DMorphologyAnalyzerFactory< ? > factory = spotMorphologyAnalyzerProvider.getFactory( key ); + if ( factory != null ) + settings.addSpotAnalyzerFactory( factory ); + } + } + } + + // Shall we add 3D morphology analyzers? + if ( settings.imp != null + && !DetectionUtils.is2D( settings.imp ) + && settings.detectorFactory != null + && settings.detectorFactory.has3Dsegmentation() ) + { + final Spot3DMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider = new Spot3DMorphologyAnalyzerProvider( settings.imp.getNChannels() ); + for ( final String key : spotMorphologyAnalyzerProvider.getVisibleKeys() ) + { + if ( selectionSpotAnalyzers.contains( key ) ) + { + final Spot3DMorphologyAnalyzerFactory< ? > factory = spotMorphologyAnalyzerProvider.getFactory( key ); + if ( factory != null ) + settings.addSpotAnalyzerFactory( factory ); + } + } + } + + // Edge analyzers. + final List< String > selectedEdgeAnalyzers = getSelectedAnalyzers( EDGES ); + final EdgeAnalyzerProvider edgeAnalyzerProvider = new EdgeAnalyzerProvider(); + for ( final String key : edgeAnalyzerProvider.getVisibleKeys() ) + { + if ( selectedEdgeAnalyzers.contains( key ) ) + { + final EdgeAnalyzer factory = edgeAnalyzerProvider.getFactory( key ); + if ( factory != null ) + settings.addEdgeAnalyzer( factory ); + } + } + + // Track analyzers. + final List< String > selectedTrackAnalyzers = getSelectedAnalyzers( TRACKS ); + final TrackAnalyzerProvider trackAnalyzerProvider = new TrackAnalyzerProvider(); + for ( final String key : trackAnalyzerProvider.getVisibleKeys() ) + { + if ( selectedTrackAnalyzers.contains( key ) ) + { + final TrackAnalyzer factory = trackAnalyzerProvider.getFactory( key ); + if ( factory != null ) + settings.addTrackAnalyzer( factory ); + } + } + } + + /** + * Possibly adds the analyzers that are discovered at runtime, but not + * present in the analyzer selection, with the 'selected' flag. + */ + public void mergeWithDefault() + { + final AnalyzerSelection df = defaultSelection(); + for ( final TrackMateObject obj : objs ) + { + final Map< String, Boolean > source = df.allAnalyzers.get( obj ); + final Map< String, Boolean > target = allAnalyzers.get( obj ); + for ( final String key : source.keySet() ) + target.putIfAbsent( key, true ); + } + } + + @Override + public String toString() + { + final StringBuilder str = new StringBuilder( super.toString() ); + for ( final TrackMateObject obj : objs ) + { + str.append( "\n" + toName( obj ) + " analyzers:" ); + + final Map< String, Boolean > map = allAnalyzers.get( obj ); + for ( final String key : map.keySet() ) + str.append( String.format( "\n\t%25s \t-> %s", key, ( map.get( key ).booleanValue() ? "selected" : "deselected" ) ) ); + } + return str.toString(); + } + + public static AnalyzerSelection defaultSelection() + { + final AnalyzerSelection fs = new AnalyzerSelection(); + + for ( final String key : new SpotAnalyzerProvider( 1 ).getVisibleKeys() ) + fs.setSelected( SPOTS, key, true ); + + for ( final String key : new Spot2DMorphologyAnalyzerProvider( 1 ).getVisibleKeys() ) + fs.setSelected( SPOTS, key, true ); + + for ( final String key : new Spot3DMorphologyAnalyzerProvider( 1 ).getVisibleKeys() ) + fs.setSelected( SPOTS, key, true ); + + for ( final String key : new EdgeAnalyzerProvider().getVisibleKeys() ) + fs.setSelected( EDGES, key, true ); + + for ( final String key : new TrackAnalyzerProvider().getVisibleKeys() ) + fs.setSelected( TRACKS, key, true ); + + // Fine tune. + fs.setSelected( SPOTS, SpotContrastAndSNRAnalyzerFactory.KEY, false ); + + return fs; + } + + public void set( final AnalyzerSelection o ) + { + allAnalyzers.clear(); + for ( final TrackMateObject obj : objs ) + allAnalyzers.put( obj, new TreeMap<>( o.allAnalyzers.get( obj ) ) ); + + mergeWithDefault(); + } + + public static final String toName( final TrackMateObject obj ) + { + final String str = obj.toString(); + return StringUtils.capitalize( str ).substring( 0, str.length() - 1 ); + } + +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelectionIO.java b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelectionIO.java new file mode 100644 index 000000000..d899e729d --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelectionIO.java @@ -0,0 +1,112 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.gui.featureselector; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.stream.Collectors; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public class AnalyzerSelectionIO +{ + + private static File userSelectionFile = new File( new File( System.getProperty( "user.home" ), ".trackmate" ), "featureselection.json" ); + + public static AnalyzerSelection readUserDefault() + { + if ( !userSelectionFile.exists() ) + { + final AnalyzerSelection fs = AnalyzerSelection.defaultSelection(); + saveToUserDefault( fs ); + return fs; + } + + try (FileReader reader = new FileReader( userSelectionFile )) + { + final String str = Files.lines( Paths.get( userSelectionFile.getAbsolutePath() ) ) + .collect( Collectors.joining( System.lineSeparator() ) ); + + return fromJson( str ); + } + catch ( final FileNotFoundException e ) + { + System.err.println( "Could not find the user feature selection file: " + userSelectionFile + + ". Using built-in default setting." ); + e.printStackTrace(); + } + catch ( final IOException e ) + { + System.err.println( "Could not read the user feature selection file: " + userSelectionFile + + ". Using built-in default setting." ); + e.printStackTrace(); + } + return AnalyzerSelection.defaultSelection(); + } + + public static AnalyzerSelection fromJson( final String str ) + { + final AnalyzerSelection fs = ( str == null || str.isEmpty() ) ? readUserDefault() : getGson().fromJson( str, AnalyzerSelection.class ); + fs.mergeWithDefault(); + return fs; + } + + public static void saveToUserDefault( final AnalyzerSelection fs ) + { + final String str = toJson( fs ); + + if ( !userSelectionFile.exists() ) + userSelectionFile.getParentFile().mkdirs(); + + try (FileWriter writer = new FileWriter( userSelectionFile )) + { + writer.append( str ); + } + catch ( final IOException e ) + { + System.err.println( "Could not write the user default settings to " + userSelectionFile ); + e.printStackTrace(); + } + } + + public static String toJson( final AnalyzerSelection fs ) + { + return getGson().toJson( fs ); + } + + private static Gson getGson() + { + final GsonBuilder builder = new GsonBuilder(); + return builder.setPrettyPrinting().create(); + } + + public static void main( final String[] args ) + { + System.out.println( readUserDefault() ); // DEBUG + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelector.java b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelector.java new file mode 100644 index 000000000..ac70fe09a --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelector.java @@ -0,0 +1,72 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.gui.featureselector; + +import static fiji.plugin.trackmate.gui.Icons.TRACKMATE_ICON; + +import javax.swing.JDialog; +import javax.swing.JFrame; + +import org.scijava.command.Command; +import org.scijava.plugin.Plugin; + +@Plugin( type = Command.class, + label = "Configure TrackMate feature analyzers...", + iconPath = "/icons/commands/information.png", + menuPath = "Edit > Options > Configure TrackMate feature analyzers...", + description = "Shows a dialog that allows configuring what feature analyzers will be used " + + "in the next TrackMate session." ) + +public class AnalyzerSelector implements Command +{ + + private final JDialog dialog; + + private final AnalyzerSelectorPanel gui; + + public AnalyzerSelector() + { + dialog = new JDialog( ( JFrame ) null, "TrackMate feature analyzers selection" ); + dialog.setLocationByPlatform( true ); + dialog.setLocationRelativeTo( null ); + gui = new AnalyzerSelectorPanel( AnalyzerSelectionIO.readUserDefault() ); + dialog.getContentPane().add( gui ); + dialog.setIconImage( TRACKMATE_ICON.getImage() ); + dialog.pack(); + } + + public JDialog getDialog() + { + return dialog; + } + + @Override + public void run() + { + dialog.setVisible( true ); + } + + public static void main( final String[] args ) + { + new AnalyzerSelector().run(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelectorPanel.java b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelectorPanel.java new file mode 100644 index 000000000..ec7bca318 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelectorPanel.java @@ -0,0 +1,303 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.gui.featureselector; + +import static fiji.plugin.trackmate.gui.Icons.APPLY_ICON; +import static fiji.plugin.trackmate.gui.Icons.RESET_ICON; +import static fiji.plugin.trackmate.gui.Icons.REVERT_ICON; +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.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; + +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.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.ScrollPaneConstants; + +import fiji.plugin.trackmate.features.FeatureAnalyzer; +import fiji.plugin.trackmate.features.spot.SpotAnalyzerFactoryBase; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; +import fiji.plugin.trackmate.providers.AbstractProvider; +import fiji.plugin.trackmate.providers.EdgeAnalyzerProvider; +import fiji.plugin.trackmate.providers.TrackAnalyzerProvider; + +public class AnalyzerSelectorPanel extends JPanel +{ + private static final long serialVersionUID = 1L; + + private static final String APPLY_TOOLTIP = "Save the current analyzer selection to the user default settings. " + + "The selection be used in all the following TrackMate sessions."; + + private static final String REVERT_TOOLTIP = "Revert the current analyzer selection to the ones saved in the " + + "user default settings file."; + + private static final String RESET_TOOLTIP = "Reset the current analyzer selection to the built-in defaults."; + + final JPanel panelConfig; + + public AnalyzerSelectorPanel( final AnalyzerSelection selection ) + { + setLayout( new BorderLayout( 0, 0 ) ); + + final JPanel panelTable = new JPanel(); + add( panelTable, BorderLayout.SOUTH ); + + final GridBagLayout gblPanelTable = new GridBagLayout(); + gblPanelTable.columnWeights = new double[] { 0.0, 1.0 }; + gblPanelTable.rowWeights = new double[] { 1.0 }; + panelTable.setLayout( gblPanelTable ); + + final JPanel panelButton = new JPanel(); + final GridBagConstraints gbcPanelButton = new GridBagConstraints(); + gbcPanelButton.gridwidth = 2; + gbcPanelButton.insets = new Insets( 10, 10, 10, 10 ); + gbcPanelButton.fill = GridBagConstraints.BOTH; + gbcPanelButton.gridx = 0; + gbcPanelButton.gridy = 0; + panelTable.add( panelButton, gbcPanelButton ); + panelButton.setLayout( new BoxLayout( panelButton, BoxLayout.X_AXIS ) ); + + final BoxLayout panelButtonLayout = new BoxLayout( panelButton, BoxLayout.LINE_AXIS ); + panelButton.setLayout( panelButtonLayout ); + final JButton btnReset = new JButton( "Reset", RESET_ICON ); + btnReset.setToolTipText( RESET_TOOLTIP ); + final JButton btnRevert = new JButton( "Revert", REVERT_ICON ); + btnRevert.setToolTipText( REVERT_TOOLTIP ); + final JButton btnApply = new JButton( "Save to user defaults", APPLY_ICON ); + btnApply.setToolTipText( APPLY_TOOLTIP ); + panelButton.add( btnReset ); + panelButton.add( Box.createHorizontalStrut( 5 ) ); + panelButton.add( btnRevert ); + panelButton.add( Box.createHorizontalGlue() ); + panelButton.add( btnApply ); + panelButton.setBorder( BorderFactory.createEmptyBorder( 10, 5, 10, 5 ) ); + + final JPanel panelTitle = new JPanel( new FlowLayout( FlowLayout.LEADING ) ); + add( panelTitle, BorderLayout.NORTH ); + + final JLabel title = new JLabel( "Configure TrackMate feature analyzers:" ); + title.setFont( getFont().deriveFont( Font.BOLD ) ); + panelTitle.add( title ); + + final JSplitPane splitPane = new JSplitPane(); + splitPane.setBorder( null ); + splitPane.setResizeWeight( 0.5 ); + add( splitPane, BorderLayout.CENTER ); + + final JPanel panelLeft = new JPanel(); + splitPane.setLeftComponent( panelLeft ); + panelLeft.setLayout( new BorderLayout( 0, 0 ) ); + + final JScrollPane scrollPaneFeatures = new JScrollPane(); + panelLeft.add( scrollPaneFeatures ); + scrollPaneFeatures.setViewportBorder( null ); + scrollPaneFeatures.setHorizontalScrollBarPolicy( ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER ); + scrollPaneFeatures.getVerticalScrollBar().setUnitIncrement( 20 ); + + final JPanel panelFeatures = new JPanel(); + panelFeatures.setBorder( BorderFactory.createEmptyBorder( 10, 10, 10, 10 ) ); + scrollPaneFeatures.setViewportView( panelFeatures ); + final BoxLayout boxLayout = new BoxLayout( panelFeatures, BoxLayout.PAGE_AXIS ); + panelFeatures.setLayout( boxLayout ); + + final JPanel panelRight = new JPanel(); + panelRight.setPreferredSize( new Dimension( 300, 300 ) ); + splitPane.setRightComponent( panelRight ); + panelRight.setLayout( new BorderLayout( 0, 0 ) ); + + this.panelConfig = new JPanel(); + panelConfig.setLayout( new BorderLayout() ); + final JScrollPane scrollPane = new JScrollPane( panelConfig ); + scrollPane.getVerticalScrollBar().setUnitIncrement( 16 ); + panelRight.add( scrollPane, BorderLayout.CENTER ); + + // Feed the feature panel. + final FeatureTable.Tables aggregator = new FeatureTable.Tables(); + + // Providers to test presence of an analyzer and get info. + final Map< TrackMateObject, AbstractProvider< ? > > allProviders = new LinkedHashMap<>( 3 ); + allProviders.put( SPOTS, new MySpotAnalyzerProvider() ); + allProviders.put( EDGES, new EdgeAnalyzerProvider() ); + allProviders.put( TRACKS, new TrackAnalyzerProvider() ); + + for ( final TrackMateObject target : AnalyzerSelection.objs ) + { + @SuppressWarnings( "unchecked" ) + final AbstractProvider< FeatureAnalyzer > provider = ( AbstractProvider< FeatureAnalyzer > ) allProviders.get( target ); + + final JPanel headerPanel = new JPanel(); + final BoxLayout hpLayout = new BoxLayout( headerPanel, BoxLayout.LINE_AXIS ); + headerPanel.setLayout( hpLayout ); + + final JLabel lbl = new JLabel( AnalyzerSelection.toName( target ) + " analyzers:" ); + lbl.setFont( panelFeatures.getFont().deriveFont( Font.BOLD ) ); + lbl.setAlignmentX( Component.LEFT_ALIGNMENT ); + + headerPanel.add( lbl ); + + panelFeatures.add( headerPanel ); + headerPanel.setAlignmentX( Component.LEFT_ALIGNMENT ); + panelFeatures.add( Box.createVerticalStrut( 5 ) ); + + final List< String > analyzerKeys = selection.getKeys( target ); + final Function< String, String > getName = k -> provider.getFactory( k ).getName(); + final Predicate< String > isSelected = k -> selection.isSelected( target, k ); + final BiConsumer< String, Boolean > setSelected = ( k, b ) -> selection.setSelected( target, k, b ); + final Predicate< String > isAnalyzerPresent = k -> provider.getKeys().contains( k ); + + final FeatureTable< List< String >, String > featureTable = + new FeatureTable<>( + analyzerKeys, + List::size, + List::get, + getName, + isSelected, + setSelected, + isAnalyzerPresent ); + + featureTable.getComponent().setAlignmentX( Component.LEFT_ALIGNMENT ); + featureTable.getComponent().setBackground( panelFeatures.getBackground() ); + panelFeatures.add( featureTable.getComponent() ); + panelFeatures.add( Box.createVerticalStrut( 10 ) ); + + aggregator.add( featureTable ); + + final FeatureTable.SelectionListener< String > sl = key -> displayConfigPanel( provider.getFactory( key ) ); + featureTable.selectionListeners().add( sl ); + } + scrollPaneFeatures.setPreferredSize( new Dimension( 300, 300 ) ); + + /* + * Listeners. + */ + + btnReset.addActionListener( e -> { + selection.set( AnalyzerSelection.defaultSelection() ); + title.setText( "Reset the current settings to the built-in defaults." ); + repaint(); + } ); + btnRevert.addActionListener( e -> { + selection.set( AnalyzerSelectionIO.readUserDefault() ); + title.setText( "Reverted the current settings to the user defaults." ); + repaint(); + } ); + btnApply.addActionListener( e -> { + AnalyzerSelectionIO.saveToUserDefault( selection ); + title.setText( "Saved the current settings to the user defaults file." ); + } ); + } + + private void displayConfigPanel( final FeatureAnalyzer factory ) + { + panelConfig.removeAll(); + if ( null == factory ) + return; + + final JPanel infoPanel = new JPanel(); + infoPanel.setLayout( new GridBagLayout() ); + final GridBagConstraints c = new GridBagConstraints(); + c.insets = new Insets( 5, 5, 5, 5 ); + c.anchor = GridBagConstraints.LINE_START; + c.fill = GridBagConstraints.BOTH; + c.weightx = 1.; + c.weighty = 1.; + + final JLabel title = new JLabel( factory.getName(), factory.getIcon(), JLabel.CENTER ); + title.setFont( getFont().deriveFont( Font.BOLD ) ); + c.gridy = 0; + infoPanel.add( title, c ); + + final JLabel infoLbl = new JLabel(); + infoLbl.setFont( getFont().deriveFont( Font.ITALIC ) ); + final String infoText = factory.getInfoText(); + infoLbl.setText( "" + ( ( infoText != null ) ? infoText : "No documentation." + "" ) ); + c.gridy++; + infoPanel.add( infoLbl, c ); + + final StringBuilder infoStr = new StringBuilder( "" ); + + infoStr.append( "Features included:

    " ); + for ( final String featureKey : factory.getFeatures() ) + { + infoStr.append( "
  • " + factory.getFeatureNames().get( featureKey ) ); + infoStr.append( "
    - Short name: " + factory.getFeatureShortNames().get( featureKey ) ); + infoStr.append( "
    - Is integer valued: " + factory.getIsIntFeature().get( featureKey ) ); + infoStr.append( "
    - Dimension: " + factory.getFeatureDimensions().get( featureKey ) ); + infoStr.append( "
    - Key: " + featureKey ); + infoStr.append( "
  • " ); + infoStr.append( "
    " ); + } + infoStr.append( "

" ); + + infoStr.append( "Details:

    " ); + infoStr.append( String.format( "
  • %25s: %s
  • ", "Key", + factory.getKey() ) ); + infoStr.append( String.format( "
  • %25s: %s
  • ", "Can use multithreading", + !factory.forbidMultithreading() ) ); + infoStr.append( String.format( "
  • %25s: %s
  • ", "Is manual", + factory.isManualFeature() ) ); + infoStr.append( "
" ); + c.gridy++; + final JLabel infoLabel = new JLabel( infoStr.toString() ); + infoLabel.setFont( getFont().deriveFont( Font.PLAIN ) ); + infoPanel.add( infoLabel, c ); + + panelConfig.add( infoPanel, BorderLayout.NORTH ); + panelConfig.revalidate(); + panelConfig.repaint(); + } + + /** + * A private provider, that return all spot providers, regardless of whether + * they act on 2D shape, 3D shape or dont use shape information. + */ + @SuppressWarnings( "rawtypes" ) + private static class MySpotAnalyzerProvider extends AbstractProvider< SpotAnalyzerFactoryBase > + { + + public MySpotAnalyzerProvider() + { + super( SpotAnalyzerFactoryBase.class ); + } + + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/featureselector/FeatureTable.java b/src/main/java/fiji/plugin/trackmate/gui/featureselector/FeatureTable.java new file mode 100644 index 000000000..b7e6698a6 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/featureselector/FeatureTable.java @@ -0,0 +1,402 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.gui.featureselector; + +import static javax.swing.JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.ToIntFunction; + +import javax.swing.Action; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.SwingConstants; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableCellRenderer; + +import org.scijava.listeners.Listeners; +import org.scijava.ui.behaviour.io.InputTriggerConfig; +import org.scijava.ui.behaviour.util.Actions; + +import fiji.plugin.trackmate.gui.Icons; + +/** + * + * @param collection-of-elements type + * @param element type + */ +public class FeatureTable< C, T > +{ + public static class Tables implements ListSelectionListener + { + private final List< FeatureTable< ?, ? > > tables = new ArrayList<>(); + + public void add( final FeatureTable< ?, ? > table ) + { + tables.add( table ); + table.tables = this; + table.table.getSelectionModel().addListSelectionListener( this ); + } + + @Override + public void valueChanged( final ListSelectionEvent event ) + { + final ListSelectionModel source = ( ListSelectionModel ) event.getSource(); + for ( final FeatureTable< ?, ? > table : tables ) + { + final ListSelectionModel lsm = table.table.getSelectionModel(); + if ( lsm.equals( source ) ) + continue; + + lsm.removeListSelectionListener( this ); + lsm.clearSelection(); + lsm.addListSelectionListener( this ); + } + } + + boolean selectNextTable( final FeatureTable< ?, ? > table ) + { + final int i = tables.indexOf( table ); + if ( i < 0 || i >= tables.size() - 1 ) + return false; + + final JTable next = tables.get( i + 1 ).table; + if ( next.getRowCount() > 0 ) + { + table.clearSelectionQuiet(); + next.setRowSelectionInterval( 0, 0 ); + next.requestFocusInWindow(); + } + return true; + } + + boolean selectPreviousTable( final FeatureTable< ?, ? > table ) + { + final int i = tables.indexOf( table ); + if ( i <= 0 ) + return false; + + final JTable previous = tables.get( i - 1 ).table; + final int rows = previous.getRowCount(); + if ( rows > 0 ) + { + table.clearSelectionQuiet(); + previous.setRowSelectionInterval( rows - 1, rows - 1 ); + previous.requestFocusInWindow(); + } + return true; + } + } + + private static final ImageIcon UP_TO_DATE_ICON = Icons.BULLET_GREEN_ICON; + private static final ImageIcon NOT_UP_TO_DATE_ICON = Icons.QUESTION_ICON; + + private C elements; + private final ToIntFunction< C > size; + private final BiFunction< C, Integer, T > get; + private final Function< T, String > getName; + private final Predicate< T > isSelected; + private final BiConsumer< T, Boolean > setSelected; + private final Predicate< T > isUptodate; + + private final Listeners.List< SelectionListener< T > > selectionListeners; + + private final ListSelectionListener listSelectionListener; + + private final MyTableModel tableModel; + + private final JTable table; + + private Tables tables; + + /** + * Creates a new feature table. + * + * @param elements + * collection of elements. + * @param size + * given collection returns number of elements. + * @param get + * given collection and index returns element at index. + * @param getName + * given element returns name. + * @param isSelected + * given element returns whether it is selected. + * @param setSelected + * given element and boolean sets selected state of element. + * @param isUptodate + * given element returns whether it is up-to-date. + */ + public FeatureTable( + final C elements, + final ToIntFunction< C > size, + final BiFunction< C, Integer, T > get, + final Function< T, String > getName, + final Predicate< T > isSelected, + final BiConsumer< T, Boolean > setSelected, + final Predicate< T > isUptodate ) + { + this.elements = elements; + this.size = size; + this.get = get; + this.getName = getName; + this.isSelected = isSelected; + this.setSelected = setSelected; + this.isUptodate = isUptodate; + + selectionListeners = new Listeners.SynchronizedList<>(); + + tableModel = new MyTableModel(); + table = new JTable( tableModel ); + table.setSelectionMode( ListSelectionModel.SINGLE_SELECTION ); + table.setTableHeader( null ); + table.setFillsViewportHeight( true ); + table.setAutoResizeMode( JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS ); + table.setRowHeight( 30 ); + listSelectionListener = e -> { + if ( e.getValueIsAdjusting() ) + return; + final int row = table.getSelectedRow(); + final T selected = ( this.elements != null && row >= 0 && row < this.size.applyAsInt( this.elements ) ) + ? this.get.apply( this.elements, row ) + : null; + selectionListeners.list.forEach( l -> l.selectionChanged( selected ) ); + }; + table.getSelectionModel().addListSelectionListener( listSelectionListener ); + table.setIntercellSpacing( new Dimension( 0, 0 ) ); + table.setSurrendersFocusOnKeystroke( true ); + table.setFocusTraversalKeys( KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, null ); + table.setFocusTraversalKeys( KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, null ); + table.getColumnModel().getColumn( 0 ).setMaxWidth( 30 ); + table.getColumnModel().getColumn( 2 ).setMaxWidth( 64 ); + table.getColumnModel().getColumn( 2 ).setCellRenderer( new UpdatedCellRenderer() ); + table.setShowGrid( false ); + + final Actions actions = new Actions( table.getInputMap( WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ), table.getActionMap(), new InputTriggerConfig() ); + actions.runnableAction( this::toggleSelectedRow, "toggle selected row", "SPACE", "ENTER" ); + actions.runnableAction( this::nextRowOrTable, "select next row or table", "DOWN" ); + actions.runnableAction( this::previousRowOrTable, "select previous row or table", "UP" ); + + setElements( elements ); + } + + private void toggleSelectedRow() + { + final int row = table.getSelectedRow(); + if ( row >= 0 ) + { + final T feature = get.apply( elements, row ); + setSelected.accept( feature, !isSelected.test( feature ) ); + tableModel.fireTableCellUpdated( row, 0 ); + } + } + + private void nextRowOrTable() + { + final int row = table.getSelectedRow(); + if ( elements == null || tables == null || row != table.getRowCount() - 1 || !tables.selectNextTable( this ) ) + { + final Action action = table.getActionMap().get( "selectNextRow" ); + if ( action != null ) + action.actionPerformed( new ActionEvent( table, 0, null ) ); + } + } + + private void previousRowOrTable() + { + final int row = table.getSelectedRow(); + if ( elements == null || tables == null || row != 0 || !tables.selectPreviousTable( this ) ) + { + final Action action = table.getActionMap().get( "selectPreviousRow" ); + if ( action != null ) + action.actionPerformed( new ActionEvent( table, 0, null ) ); + } + } + + private void clearSelectionQuiet() + { + table.getSelectionModel().removeListSelectionListener( listSelectionListener ); + table.clearSelection(); + table.getSelectionModel().addListSelectionListener( listSelectionListener ); + + } + + /** + * Exposes the component in which the elements are displayed. + * + * @return the component. + */ + public JComponent getComponent() + { + return table; + } + + /** + * Sets the collection of elements to show. + * + * @param elements + * the collection of elements to show. + */ + public void setElements( final C elements ) + { + this.elements = elements; + if ( elements == null ) + selectionListeners.list.forEach( l -> l.selectionChanged( null ) ); + else + tableModel.fireTableDataChanged(); + } + + public void selectFirstRow() + { + if ( table.getRowCount() > 0 ) + table.setRowSelectionInterval( 0, 0 ); + } + + public interface SelectionListener< T > + { + void selectionChanged( T selected ); + } + + public Listeners< SelectionListener< T > > selectionListeners() + { + return selectionListeners; + } + + private class MyTableModel extends DefaultTableModel + { + + private static final long serialVersionUID = 1L; + + @Override + public int getColumnCount() + { + return 3; + } + + @Override + public int getRowCount() + { + return ( null == elements ) ? 0 : size.applyAsInt( elements ); + } + + public T get( final int index ) + { + return get.apply( elements, Integer.valueOf( index ) ); + } + + @Override + public Object getValueAt( final int rowIndex, final int columnIndex ) + { + switch ( columnIndex ) + { + case 0: + return isSelected.test( get( rowIndex ) ); + case 1: + return getName.apply( get( rowIndex ) ); + case 2: + return isUptodate.test( get( rowIndex ) ); + } + throw new IllegalArgumentException( "Cannot return value for colum index larger than " + getColumnCount() ); + } + + @Override + public Class< ? > getColumnClass( final int columnIndex ) + { + switch ( columnIndex ) + { + case 0: + return Boolean.class; + case 1: + return String.class; + case 2: + return Boolean.class; + } + throw new IllegalArgumentException( "Cannot return value for colum index larger than " + getColumnCount() ); + } + + @Override + public boolean isCellEditable( final int rowIndex, final int columnIndex ) + { + return columnIndex == 0; + } + + @Override + public void setValueAt( final Object aValue, final int rowIndex, final int columnIndex ) + { + final boolean selected = (columnIndex == 0) + ? ( boolean ) aValue + : !isSelected.test( get( rowIndex ) ); + if ( selected != isSelected.test( get( rowIndex ) ) ) + { + setSelected.accept( get( rowIndex ), ( Boolean ) aValue ); + fireTableRowsUpdated( rowIndex, rowIndex ); + for ( final SelectionListener< T > listener : selectionListeners.list ) + listener.selectionChanged( get( rowIndex ) ); + } + } + } + + private class UpdatedCellRenderer implements TableCellRenderer + { + + private final DefaultTableCellRenderer renderer; + + public UpdatedCellRenderer() + { + this.renderer = new DefaultTableCellRenderer(); + final JLabel label = ( JLabel ) renderer.getTableCellRendererComponent( null, null, false, false, 0, 0 ); + label.setHorizontalAlignment( SwingConstants.CENTER ); + } + + @Override + public Component getTableCellRendererComponent( + final JTable table, + final Object value, + final boolean isSelected, + final boolean hasFocus, + final int row, + final int column ) + { + final JLabel label = ( JLabel ) renderer.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column ); + label.setIcon( isUptodate.test( get.apply( elements, Integer.valueOf( row ) ) ) + ? UP_TO_DATE_ICON + : NOT_UP_TO_DATE_ICON ); + label.setText( "" ); + return label; + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/graph/OutputFunction.java b/src/main/java/fiji/plugin/trackmate/gui/featureselector/package-info.java similarity index 70% rename from src/main/java/fiji/plugin/trackmate/graph/OutputFunction.java rename to src/main/java/fiji/plugin/trackmate/gui/featureselector/package-info.java index 9bc0f3448..f18999b5a 100644 --- a/src/main/java/fiji/plugin/trackmate/graph/OutputFunction.java +++ b/src/main/java/fiji/plugin/trackmate/gui/featureselector/package-info.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -19,17 +19,4 @@ * . * #L% */ -package fiji.plugin.trackmate.graph; - -/** - * Interface for functions that return a new object, computed from two input - * arguments. - * - * @author Jean-Yves Tinevez - */ -public interface OutputFunction< E > -{ - - public E compute( E input1, E input2 ); - -} +package fiji.plugin.trackmate.gui.featureselector; diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java index c602aa296..a8ea75cb5 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java @@ -21,36 +21,36 @@ */ package fiji.plugin.trackmate.gui.wizard; -import static fiji.plugin.trackmate.gui.Icons.SPOT_TABLE_ICON; -import static fiji.plugin.trackmate.gui.Icons.TRACK_SCHEME_ICON_16x16; -import static fiji.plugin.trackmate.gui.Icons.TRACK_TABLES_ICON; - -import java.awt.event.ActionEvent; +import java.awt.Window; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.BooleanSupplier; -import javax.swing.AbstractAction; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.WindowConstants; import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.ModelChangeEvent; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.action.AbstractTMAction; -import fiji.plugin.trackmate.action.ExportAllSpotsStatsAction; -import fiji.plugin.trackmate.action.ExportStatsTablesAction; import fiji.plugin.trackmate.detection.ManualDetectorFactory; import fiji.plugin.trackmate.detection.SpotDetectorFactoryBase; import fiji.plugin.trackmate.features.FeatureFilter; import fiji.plugin.trackmate.features.ModelFeatureUpdater; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.gui.components.ConfigurationPanel; import fiji.plugin.trackmate.gui.components.FeatureDisplaySelector; import fiji.plugin.trackmate.gui.components.LogPanel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; -import fiji.plugin.trackmate.gui.editor.LabkitLauncher; import fiji.plugin.trackmate.gui.wizard.descriptors.ActionChooserDescriptor; import fiji.plugin.trackmate.gui.wizard.descriptors.ChooseDetectorDescriptor; import fiji.plugin.trackmate.gui.wizard.descriptors.ChooseTrackerDescriptor; @@ -72,19 +72,13 @@ import fiji.plugin.trackmate.tracking.SpotImageTrackerFactory; import fiji.plugin.trackmate.tracking.SpotTrackerFactory; import fiji.plugin.trackmate.tracking.manual.ManualTrackerFactory; -import fiji.plugin.trackmate.util.Threads; -import fiji.plugin.trackmate.visualization.trackscheme.SpotImageUpdater; -import fiji.plugin.trackmate.visualization.trackscheme.TrackScheme; +import fiji.plugin.trackmate.util.ImpCloseWindowListener; +import fiji.plugin.trackmate.visualization.AbstractTrackMateModelJFrameView; +import ij.gui.ImageWindow; -public class TrackMateWizardSequence implements WizardSequence +public class TrackMateWizardSequence extends AbstractTrackMateModelJFrameView implements WizardSequence { - private final TrackMate trackmate; - - private final SelectionModel selectionModel; - - private final DisplaySettings displaySettings; - private WizardPanelDescriptor current; private final StartDialogDescriptor startDialogDescriptor; @@ -117,13 +111,15 @@ public class TrackMateWizardSequence implements WizardSequence private final SaveDescriptor saveDescriptor; - public TrackMateWizardSequence( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings ) + private JFrame frame; + + public TrackMateWizardSequence( final GuiModel guiModel ) { - this.trackmate = trackmate; - this.selectionModel = selectionModel; - this.displaySettings = displaySettings; - final Settings settings = trackmate.getSettings(); - final Model model = trackmate.getModel(); + super( guiModel ); + final Settings settings = guiModel.getSettings(); + final Model model = guiModel.getModel(); + final TrackMate trackmate = guiModel.getTrackMate(); + final DisplaySettings displaySettings = guiModel.getDisplaySettings(); // Listen to changes in the model and update features accordingly. final ModelFeatureUpdater modelFeatureUpdater = new ModelFeatureUpdater( model, settings ); @@ -140,36 +136,23 @@ public TrackMateWizardSequence( final TrackMate trackmate, final SelectionModel logDescriptor = new LogPanelDescriptor2( logPanel ); startDialogDescriptor = new StartDialogDescriptor( settings, logger ); - chooseDetectorDescriptor = new ChooseDetectorDescriptor( new DetectorProvider(), trackmate ); - executeDetectionDescriptor = new ExecuteDetectionDescriptor( trackmate, logPanel ); - initFilterDescriptor = new InitFilterDescriptor( trackmate, initialFilter ); - spotFilterDescriptor = new SpotFilterDescriptor( trackmate, spotFilters, featureSelector ); - chooseTrackerDescriptor = new ChooseTrackerDescriptor( new TrackerProvider(), trackmate ); - executeTrackingDescriptor = new ExecuteTrackingDescriptor( trackmate, logPanel ); - trackFilterDescriptor = new TrackFilterDescriptor( trackmate, trackFilters, featureSelector, displaySettings ); - configureViewsDescriptor = new ConfigureViewsDescriptor( - displaySettings, - featureSelector, - new LaunchTrackSchemeAction(), - new ShowTrackTablesAction(), - new ShowSpotTableAction(), - LabkitLauncher.getLaunchAction( trackmate, displaySettings ), - model.getSpaceUnits() ); - grapherDescriptor = new GrapherDescriptor( trackmate, selectionModel, displaySettings ); - actionChooserDescriptor = new ActionChooserDescriptor( new ActionProvider(), trackmate, selectionModel, displaySettings ); - saveDescriptor = new SaveDescriptor( trackmate, displaySettings, this ); + chooseDetectorDescriptor = new ChooseDetectorDescriptor( new DetectorProvider(), guiModel ); + executeDetectionDescriptor = new ExecuteDetectionDescriptor( guiModel, logPanel ); + initFilterDescriptor = new InitFilterDescriptor( guiModel, initialFilter ); + spotFilterDescriptor = new SpotFilterDescriptor( guiModel, spotFilters, featureSelector ); + chooseTrackerDescriptor = new ChooseTrackerDescriptor( new TrackerProvider(), guiModel ); + executeTrackingDescriptor = new ExecuteTrackingDescriptor( guiModel, logPanel ); + trackFilterDescriptor = new TrackFilterDescriptor( guiModel, trackFilters, featureSelector ); + configureViewsDescriptor = new ConfigureViewsDescriptor( guiModel, featureSelector ); + grapherDescriptor = new GrapherDescriptor( guiModel ); + actionChooserDescriptor = new ActionChooserDescriptor( new ActionProvider(), guiModel ); + saveDescriptor = new SaveDescriptor( guiModel, this ); this.next = getForwardSequence(); this.previous = getBackwardSequence(); current = startDialogDescriptor; } - @Override - public void onClose() - { - trackmate.getModel().setLogger( Logger.IJ_LOGGER ); - } - @Override public WizardPanelDescriptor next() { @@ -312,8 +295,10 @@ public void setCurrent( final String panelIdentifier ) */ private SpotDetectorDescriptor getDetectorConfigDescriptor() { - final SpotDetectorFactoryBase< ? > detectorFactory = trackmate.getSettings().detectorFactory; + final Model model = guiModel.getModel(); + final Settings settings = guiModel.getSettings(); + final SpotDetectorFactoryBase< ? > detectorFactory = settings.detectorFactory; /* * Special case: are we dealing with the manual detector? If yes, no * config, no detection. @@ -333,7 +318,7 @@ private SpotDetectorDescriptor getDetectorConfigDescriptor() * descriptor. */ // From settings. - final Map< String, Object > oldSettings1 = new HashMap<>( trackmate.getSettings().detectorSettings ); + final Map< String, Object > oldSettings1 = new HashMap<>( settings.detectorSettings ); // From previous panel. final Map< String, Object > oldSettings2 = new HashMap<>(); final WizardPanelDescriptor previousDescriptor = next.get( chooseDetectorDescriptor ); @@ -354,10 +339,10 @@ private SpotDetectorDescriptor getDetectorConfigDescriptor() defaultSettings.put( skey, previousValue ); } - final ConfigurationPanel detectorConfigurationPanel = detectorFactory.getDetectorConfigurationPanel( trackmate.getSettings(), trackmate.getModel() ); + final ConfigurationPanel detectorConfigurationPanel = detectorFactory.getDetectorConfigurationPanel( settings, model ); detectorConfigurationPanel.setSettings( defaultSettings ); - trackmate.getSettings().detectorSettings = defaultSettings; - final SpotDetectorDescriptor configDescriptor = new SpotDetectorDescriptor( trackmate.getSettings(), detectorConfigurationPanel, trackmate.getModel().getLogger() ); + settings.detectorSettings = defaultSettings; + final SpotDetectorDescriptor configDescriptor = new SpotDetectorDescriptor( settings, detectorConfigurationPanel, model.getLogger() ); // Position sequence next and previous. next.put( chooseDetectorDescriptor, configDescriptor ); @@ -378,13 +363,15 @@ private SpotDetectorDescriptor getDetectorConfigDescriptor() */ private SpotTrackerDescriptor getTrackerConfigDescriptor() { - final SpotTrackerFactory trackerFactory = trackmate.getSettings().trackerFactory; + final Model model = guiModel.getModel(); + final Settings settings = guiModel.getSettings(); + final SpotTrackerFactory trackerFactory = settings.trackerFactory; /* * Special case: are we dealing with the manual tracker? If yes, no * config, no detection. */ - if ( trackerFactory.getKey().equals( ManualTrackerFactory.TRACKER_KEY ) ) + if ( trackerFactory == null || trackerFactory.getKey().equals( ManualTrackerFactory.TRACKER_KEY ) ) { // Position sequence next and previous. next.put( chooseTrackerDescriptor, trackFilterDescriptor ); @@ -397,7 +384,7 @@ private SpotTrackerDescriptor getTrackerConfigDescriptor() * descriptor. */ // From settings. - final Map< String, Object > oldSettings1 = new HashMap<>( trackmate.getSettings().trackerSettings ); + final Map< String, Object > oldSettings1 = new HashMap<>( settings.trackerSettings ); // From previous panel. final Map< String, Object > oldSettings2 = new HashMap<>(); final WizardPanelDescriptor previousDescriptor = next.get( chooseTrackerDescriptor ); @@ -419,19 +406,17 @@ private SpotTrackerDescriptor getTrackerConfigDescriptor() } final ConfigurationPanel trackerConfigurationPanel; - if (trackerFactory instanceof SpotImageTrackerFactory) + if ( trackerFactory instanceof SpotImageTrackerFactory ) { - trackerConfigurationPanel = ((SpotImageTrackerFactory)trackerFactory).getTrackerConfigurationPanel( - trackmate.getModel(), trackmate.getSettings().imp ); + trackerConfigurationPanel = ( ( SpotImageTrackerFactory ) trackerFactory ).getTrackerConfigurationPanel( model, settings.imp ); } else { - trackerConfigurationPanel= trackerFactory.getTrackerConfigurationPanel( - trackmate.getModel() ); + trackerConfigurationPanel = trackerFactory.getTrackerConfigurationPanel( model ); } trackerConfigurationPanel.setSettings( defaultSettings ); - trackmate.getSettings().trackerSettings = defaultSettings; - final SpotTrackerDescriptor configDescriptor = new SpotTrackerDescriptor( trackmate.getSettings(), trackerConfigurationPanel, trackmate.getModel().getLogger() ); + settings.trackerSettings = defaultSettings; + final SpotTrackerDescriptor configDescriptor = new SpotTrackerDescriptor( settings, trackerConfigurationPanel, model.getLogger() ); // Position sequence next and previous. next.put( chooseTrackerDescriptor, configDescriptor ); @@ -443,83 +428,84 @@ private SpotTrackerDescriptor getTrackerConfigDescriptor() return configDescriptor; } - private static final String TRACK_TABLES_BUTTON_TOOLTIP = "" - + "Export the features of all tracks, edges and all
" - + "spots belonging to a track to ImageJ tables." - + ""; - - private static final String SPOT_TABLE_BUTTON_TOOLTIP = "Export the features of all spots to ImageJ tables."; - - private static final String TRACKSCHEME_BUTTON_TOOLTIP = "Launch a new instance of TrackScheme."; - - private class LaunchTrackSchemeAction extends AbstractAction + @Override + public JFrame run( final String title ) { - private static final long serialVersionUID = 1L; - - private LaunchTrackSchemeAction() + this.frame = WizardSequence.super.run( title ); + final ImageWindow window = guiModel.getSettings().imp.getWindow(); + + // Build a confirmation dialog, add it to the wizard window and the + // image window. + final BooleanSupplier confirmClose = () -> { + final int choice = JOptionPane.showOptionDialog( window, "" + + "This will close the image and\n" + + "terminate this TrackMate session. \n" + + "Close window?", + "End TrackMate session?", + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE, + Icons.TRACKMATE_ICON_64x64, + null, + JOptionPane.NO_OPTION ); + return ( choice == JOptionPane.YES_OPTION ); + }; + final Runnable onClosed = () -> { + guiModel.getWindowManager().closeAll(); + frame.dispose(); + }; + + // Intercept closing the image -> ask for confirmation. + ImpCloseWindowListener.wrap( window, confirmClose, onClosed ); + + // Intercept closing the main window. + frame.setDefaultCloseOperation( WindowConstants.DO_NOTHING_ON_CLOSE ); + final WindowListener closeConfirm = new WindowAdapter() { - super( "TrackScheme", TRACK_SCHEME_ICON_16x16 ); - putValue( SHORT_DESCRIPTION, TRACKSCHEME_BUTTON_TOOLTIP ); - } - @Override - public void actionPerformed( final ActionEvent e ) - { - Threads.run( "Launching TrackScheme thread", () -> + @Override + public void windowClosing( final WindowEvent e ) { - final TrackScheme trackscheme = new TrackScheme( trackmate.getModel(), selectionModel, displaySettings ); - final SpotImageUpdater thumbnailUpdater = new SpotImageUpdater( trackmate.getSettings() ); - trackscheme.setSpotImageUpdater( thumbnailUpdater ); - trackscheme.render(); - } ); - } + if ( confirmClose.getAsBoolean() ) + { + onClosed.run(); + window.dispose(); + } + }; + }; + frame.addWindowListener( closeConfirm ); + setWindow( frame ); + return frame; } - private class ShowTrackTablesAction extends AbstractAction - { - private static final long serialVersionUID = 1L; - - private ShowTrackTablesAction() - { - super( "Tracks", TRACK_TABLES_ICON ); - putValue( SHORT_DESCRIPTION, TRACK_TABLES_BUTTON_TOOLTIP ); - } + @Override + public void render() + {} - @Override - public void actionPerformed( final ActionEvent e ) - { - showTables( false ); - } - } + @Override + public void refresh() + {} - private class ShowSpotTableAction extends AbstractAction - { - private static final long serialVersionUID = 1L; + @Override + public void clear() + {} - private ShowSpotTableAction() - { - super( "Spots", SPOT_TABLE_ICON ); - putValue( SHORT_DESCRIPTION, SPOT_TABLE_BUTTON_TOOLTIP ); - } + @Override + public void centerViewOn( final Spot spot ) + {} - @Override - public void actionPerformed( final ActionEvent e ) - { - showTables( true ); - } + @Override + public String getKey() + { + return "TRACKMATE_WIZARD"; } - private void showTables( final boolean showSpotTable ) + @Override + public Window getWindow() { - Threads.run( "TrackMate table thread.", () -> - { - AbstractTMAction action; - if ( showSpotTable ) - action = new ExportAllSpotsStatsAction(); - else - action = new ExportStatsTablesAction(); - - action.execute( trackmate, selectionModel, displaySettings, null ); - } ); + return frame; } + + @Override + public void modelChanged( final ModelChangeEvent event ) + {} } diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardController.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardController.java index 3d2a41d97..abf89bbcf 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardController.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardController.java @@ -218,7 +218,13 @@ private void exec( final Runnable runnable ) public void init() { - final WizardPanelDescriptor descriptor = sequence.current(); + WizardPanelDescriptor descriptor = sequence.current(); + if ( descriptor == null ) + { + sequence.setCurrent( sequence.configDescriptor().panelIdentifier ); + descriptor = sequence.configDescriptor(); + } + wizardPanel.btnPrevious.setEnabled( sequence.hasPrevious() ); wizardPanel.btnNext.setEnabled( sequence.hasNext() ); descriptor.aboutToDisplayPanel(); diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardSequence.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardSequence.java index 6109e43f7..b0814d3ff 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardSequence.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardSequence.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -21,9 +21,6 @@ */ package fiji.plugin.trackmate.gui.wizard; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; - import javax.swing.JFrame; /** @@ -39,7 +36,7 @@ public interface WizardSequence * Launches the wizard to play this sequence. * * @param title - * the title to show in the wizard window. + * the title of the frame in which the wizard is displayed. * @return the {@link JFrame} in which the wizard is displayed. */ public default JFrame run( final String title ) @@ -51,23 +48,9 @@ public default JFrame run( final String title ) frame.setSize( 350, 560 ); frame.setTitle( title ); controller.init(); - frame.addWindowListener( new WindowAdapter() - { - @Override - public void windowClosing( final WindowEvent e ) - { - onClose(); - }; - } ); return frame; } - /** - * Method called when the wizard is closed. - */ - public default void onClose() - {} - /** * Returns the descriptor currently displayed. * @@ -94,7 +77,7 @@ public default void onClose() /** * Returns the descriptor in charge of logging events. It can be accessed * out of the normal sequence by a special button in the wizard. - * + * * @return the descriptor in charge of logging events. */ public WizardPanelDescriptor logDescriptor(); @@ -102,7 +85,7 @@ public default void onClose() /** * Returns the descriptor in charge of configure the views. It can be * accessed out of the normal sequence by a special button in the wizard. - * + * * @return the descriptor in charge of configuring the views. */ public WizardPanelDescriptor configDescriptor(); @@ -127,7 +110,7 @@ public default void onClose() /** * Returns the panel in charge of saving the data. - * + * * @return the panel in charge of saving the data. */ public WizardPanelDescriptor save(); @@ -136,7 +119,7 @@ public default void onClose() * Position the sequence so that its current descriptor is the one with the * specified identifier. If the identifier is unknown to the sequence, do * nothing. - * + * * @param panelIdentifier * the descriptor identifier. */ diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ActionChooserDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ActionChooserDescriptor.java index b40125068..475bfd11b 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ActionChooserDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ActionChooserDescriptor.java @@ -21,10 +21,8 @@ */ package fiji.plugin.trackmate.gui.wizard.descriptors; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.components.ActionChooserPanel; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.wizard.WizardPanelDescriptor; import fiji.plugin.trackmate.providers.ActionProvider; @@ -33,9 +31,9 @@ public class ActionChooserDescriptor extends WizardPanelDescriptor private static final String KEY = "Actions"; - public ActionChooserDescriptor( final ActionProvider actionProvider, final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings ) + public ActionChooserDescriptor( final ActionProvider actionProvider, final GuiModel guiModel ) { super( KEY ); - this.targetPanel = new ActionChooserPanel( actionProvider, trackmate, selectionModel, displaySettings ); + this.targetPanel = new ActionChooserPanel( actionProvider, guiModel ); } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ChooseDetectorDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ChooseDetectorDescriptor.java index 753430c0c..f86a99394 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ChooseDetectorDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ChooseDetectorDescriptor.java @@ -23,9 +23,11 @@ import java.util.Map; -import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.detection.LogDetectorFactory; import fiji.plugin.trackmate.detection.SpotDetectorFactoryBase; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.components.ModuleChooserPanel; import fiji.plugin.trackmate.gui.wizard.WizardPanelDescriptor; import fiji.plugin.trackmate.io.SettingsPersistence; @@ -36,19 +38,19 @@ public class ChooseDetectorDescriptor extends WizardPanelDescriptor private static final String KEY = "ChooseDetector"; - private final TrackMate trackmate; - private final DetectorProvider detectorProvider; - public ChooseDetectorDescriptor( final DetectorProvider detectorProvider, final TrackMate trackmate ) + private final GuiModel guiModel; + + public ChooseDetectorDescriptor( final DetectorProvider detectorProvider, final GuiModel guiModel ) { super( KEY ); - this.trackmate = trackmate; this.detectorProvider = detectorProvider; + this.guiModel = guiModel; String selectedDetector = LogDetectorFactory.DETECTOR_KEY; // default - if ( null != trackmate.getSettings().detectorFactory ) - selectedDetector = trackmate.getSettings().detectorFactory.getKey(); + if ( null != guiModel.getSettings().detectorFactory ) + selectedDetector = guiModel.getSettings().detectorFactory.getKey(); this.targetPanel = new ModuleChooserPanel<>( detectorProvider, "detector", selectedDetector ); } @@ -56,8 +58,8 @@ public ChooseDetectorDescriptor( final DetectorProvider detectorProvider, final private void setCurrentChoiceFromPlugin() { String key = LogDetectorFactory.DETECTOR_KEY; // back to default - if ( null != trackmate.getSettings().detectorFactory ) - key = trackmate.getSettings().detectorFactory.getKey(); + if ( null != guiModel.getSettings().detectorFactory ) + key = guiModel.getSettings().detectorFactory.getKey(); @SuppressWarnings( { "rawtypes", "unchecked" } ) final ModuleChooserPanel< SpotDetectorFactoryBase > component = ( fiji.plugin.trackmate.gui.components.ModuleChooserPanel< SpotDetectorFactoryBase > ) targetPanel; @@ -73,6 +75,9 @@ public void displayingPanel() @Override public void aboutToHidePanel() { + final Model model = guiModel.getModel(); + final Settings settings = guiModel.getSettings(); + // Configure the detector provider with choice made in panel @SuppressWarnings( { "rawtypes", "unchecked" } ) final ModuleChooserPanel< SpotDetectorFactoryBase > component = ( fiji.plugin.trackmate.gui.components.ModuleChooserPanel< SpotDetectorFactoryBase > ) targetPanel; @@ -83,37 +88,37 @@ public void aboutToHidePanel() if ( null == factory ) { - trackmate.getModel().getLogger().error( "[ChooseDetectorDescriptor] Cannot find detector named " + detectorKey + " in current TrackMate modules." ); + model.getLogger().error( "[ChooseDetectorDescriptor] Cannot find detector named " + detectorKey + " in current TrackMate modules." ); return; } - trackmate.getSettings().detectorFactory = factory; + settings.detectorFactory = factory; /* * Compare current settings with default ones, and substitute default * ones only if the old ones are absent or not compatible with it. */ - final Map< String, Object > currentSettings = trackmate.getSettings().detectorSettings; + final Map< String, Object > currentSettings = settings.detectorSettings; if ( factory.checkSettings( currentSettings ) != null ) { final String error = factory.checkSettings( currentSettings ); if ( error == null ) { - trackmate.getSettings().detectorSettings = currentSettings; + settings.detectorSettings = currentSettings; } else { final Map< String, Object > defaultSettings = factory.getDefaultSettings(); - trackmate.getSettings().detectorSettings = defaultSettings; + settings.detectorSettings = defaultSettings; } } // Settings persistence. - SettingsPersistence.saveLastUsedSettings( trackmate.getSettings(), trackmate.getModel().getLogger() ); + SettingsPersistence.saveLastUsedSettings( settings, model.getLogger() ); } @Override public Runnable getBackwardRunnable() { - return () -> trackmate.getModel().clearSpots( true ); + return () -> guiModel.getModel().clearSpots( true ); } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ChooseTrackerDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ChooseTrackerDescriptor.java index b900ad89d..011272f69 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ChooseTrackerDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ChooseTrackerDescriptor.java @@ -23,8 +23,13 @@ import java.util.Map; -import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.features.FeatureUtils; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.components.ModuleChooserPanel; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; import fiji.plugin.trackmate.gui.wizard.WizardPanelDescriptor; import fiji.plugin.trackmate.io.SettingsPersistence; import fiji.plugin.trackmate.providers.TrackerProvider; @@ -36,19 +41,20 @@ public class ChooseTrackerDescriptor extends WizardPanelDescriptor private static final String KEY = "ChooseTracker"; - private final TrackMate trackmate; - private final TrackerProvider trackerProvider; - public ChooseTrackerDescriptor( final TrackerProvider trackerProvider, final TrackMate trackmate ) + private final GuiModel guiModel; + + public ChooseTrackerDescriptor( final TrackerProvider trackerProvider, final GuiModel guiModel ) { super( KEY ); - this.trackmate = trackmate; this.trackerProvider = trackerProvider; + this.guiModel = guiModel; + final Settings settings = guiModel.getSettings(); String selectedTracker = SimpleSparseLAPTrackerFactory.THIS2_TRACKER_KEY; // default - if ( null != trackmate.getSettings().trackerFactory ) - selectedTracker = trackmate.getSettings().trackerFactory.getKey(); + if ( null != settings.trackerFactory ) + selectedTracker = settings.trackerFactory.getKey(); this.targetPanel = new ModuleChooserPanel<>( trackerProvider, "tracker", selectedTracker ); } @@ -56,8 +62,8 @@ public ChooseTrackerDescriptor( final TrackerProvider trackerProvider, final Tra private void setCurrentChoiceFromPlugin() { String key = SimpleSparseLAPTrackerFactory.THIS2_TRACKER_KEY; // default - if ( null != trackmate.getSettings().trackerFactory ) - key = trackmate.getSettings().trackerFactory.getKey(); + if ( null != guiModel.getSettings().trackerFactory ) + key = guiModel.getSettings().trackerFactory.getKey(); @SuppressWarnings( "unchecked" ) final ModuleChooserPanel< SpotTrackerFactory > component = ( fiji.plugin.trackmate.gui.components.ModuleChooserPanel< SpotTrackerFactory > ) targetPanel; @@ -73,6 +79,9 @@ public void displayingPanel() @Override public void aboutToHidePanel() { + final Model model = guiModel.getModel(); + final Settings settings = guiModel.getSettings(); + // Configure the detector provider with choice made in panel @SuppressWarnings( "unchecked" ) final ModuleChooserPanel< SpotTrackerFactory > component = ( fiji.plugin.trackmate.gui.components.ModuleChooserPanel< SpotTrackerFactory > ) targetPanel; @@ -83,30 +92,37 @@ public void aboutToHidePanel() if ( null == factory ) { - trackmate.getModel().getLogger().error( "[ChooseTrackerDescriptor] Cannot find tracker named " + trackerKey + " in current TrackMate modules." ); + model.getLogger().error( "[ChooseTrackerDescriptor] Cannot find tracker named " + trackerKey + " in current TrackMate modules." ); return; } - trackmate.getSettings().trackerFactory = factory; + settings.trackerFactory = factory; /* * Compare current settings with default ones, and substitute default * ones only if the old ones are absent or not compatible with it. */ - final Map< String, Object > currentSettings = trackmate.getSettings().trackerSettings; + final Map< String, Object > currentSettings = settings.trackerSettings; if ( factory.checkSettings( currentSettings ) != null ) { final Map< String, Object > defaultSettings = factory.getDefaultSettings(); - trackmate.getSettings().trackerSettings = defaultSettings; + settings.trackerSettings = defaultSettings; } // Settings persistence. - SettingsPersistence.saveLastUsedSettings( trackmate.getSettings(), trackmate.getModel().getLogger() ); + SettingsPersistence.saveLastUsedSettings( settings, model.getLogger() ); } @Override public Runnable getBackwardRunnable() { - // Delete tracks. - return () -> trackmate.getModel().clearTracks( true ); + // Delete tracks and put back default coloring if needed. + return () -> { + final Model model = guiModel.getModel(); + final DisplaySettings displaySettings = guiModel.getDisplaySettings(); + if ( displaySettings.getSpotColorByType() == TrackMateObject.TRACKS + || displaySettings.getSpotColorByType() == TrackMateObject.EDGES ) + displaySettings.setSpotColorBy( TrackMateObject.DEFAULT, FeatureUtils.USE_UNIFORM_COLOR_KEY ); + model.clearTracks( true ); + }; } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ConfigureViewsDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ConfigureViewsDescriptor.java index 4af33ba7e..0acb2e4a8 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ConfigureViewsDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ConfigureViewsDescriptor.java @@ -21,11 +21,9 @@ */ package fiji.plugin.trackmate.gui.wizard.descriptors; -import javax.swing.Action; - +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.components.ConfigureViewsPanel; import fiji.plugin.trackmate.gui.components.FeatureDisplaySelector; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.wizard.WizardPanelDescriptor; public class ConfigureViewsDescriptor extends WizardPanelDescriptor @@ -33,23 +31,9 @@ public class ConfigureViewsDescriptor extends WizardPanelDescriptor public static final String KEY = "ConfigureViews"; - public ConfigureViewsDescriptor( - final DisplaySettings ds, - final FeatureDisplaySelector featureSelector, - final Action launchTrackSchemeAction, - final Action showTrackTablesAction, - final Action showSpotTableAction, - final Action launchLabkitAction, - final String spaceUnits ) + public ConfigureViewsDescriptor( final GuiModel guiModel, final FeatureDisplaySelector featureSelector ) { super( KEY ); - this.targetPanel = new ConfigureViewsPanel( - ds, - featureSelector, - spaceUnits, - launchTrackSchemeAction, - showTrackTablesAction, - showSpotTableAction, - launchLabkitAction ); + this.targetPanel = new ConfigureViewsPanel( guiModel, featureSelector ); } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ExecuteDetectionDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ExecuteDetectionDescriptor.java index a91d97dda..3cf7b3833 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ExecuteDetectionDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ExecuteDetectionDescriptor.java @@ -23,8 +23,10 @@ import org.scijava.Cancelable; +import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.components.LogPanel; import fiji.plugin.trackmate.gui.wizard.WizardPanelDescriptor; @@ -33,12 +35,12 @@ public class ExecuteDetectionDescriptor extends WizardPanelDescriptor public static final String KEY = "ExecuteDetection"; - private final TrackMate trackmate; + private final GuiModel guiModel; - public ExecuteDetectionDescriptor( final TrackMate trackmate, final LogPanel logPanel ) + public ExecuteDetectionDescriptor( final GuiModel guiModel, final LogPanel logPanel ) { super( KEY ); - this.trackmate = trackmate; + this.guiModel = guiModel; this.targetPanel = logPanel; } @@ -46,23 +48,24 @@ public ExecuteDetectionDescriptor( final TrackMate trackmate, final LogPanel log public Runnable getForwardRunnable() { return () -> { - + final Model model = guiModel.getModel(); + final Settings settings = guiModel.getSettings(); + final TrackMate trackmate = guiModel.getTrackMate(); // Read ROI from imp NOW. - final Settings settings = trackmate.getSettings(); settings.setRoi( settings.imp.getRoi() ); // Exec detection. final long start = System.currentTimeMillis(); final boolean ok = trackmate.execDetection(); if ( !ok ) - trackmate.getModel().getLogger().error( trackmate.getErrorMessage() + '\n' ); + model.getLogger().error( trackmate.getErrorMessage() + '\n' ); final long end = System.currentTimeMillis(); - trackmate.getModel().getLogger().log( String.format( "Detection done in %.1f s.\n", ( end - start ) / 1e3f ) ); + model.getLogger().log( String.format( "Detection done in %.1f s.\n", ( end - start ) / 1e3f ) ); }; } @Override public Cancelable getCancelable() { - return trackmate; + return guiModel.getTrackMate(); } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ExecuteTrackingDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ExecuteTrackingDescriptor.java index 825f74480..219113149 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ExecuteTrackingDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ExecuteTrackingDescriptor.java @@ -26,9 +26,15 @@ import org.scijava.Cancelable; import fiji.plugin.trackmate.Logger; +import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.TrackModel; +import fiji.plugin.trackmate.features.FeatureUtils; +import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.components.LogPanel; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; import fiji.plugin.trackmate.gui.wizard.WizardPanelDescriptor; public class ExecuteTrackingDescriptor extends WizardPanelDescriptor @@ -36,12 +42,12 @@ public class ExecuteTrackingDescriptor extends WizardPanelDescriptor public static final String KEY = "ExecuteTracking"; - private final TrackMate trackmate; + private final GuiModel guiModel; - public ExecuteTrackingDescriptor( final TrackMate trackmate, final LogPanel logPanel ) + public ExecuteTrackingDescriptor( final GuiModel guiModel, final LogPanel logPanel ) { super( KEY ); - this.trackmate = trackmate; + this.guiModel = guiModel; this.targetPanel = logPanel; } @@ -49,15 +55,17 @@ public ExecuteTrackingDescriptor( final TrackMate trackmate, final LogPanel logP public Runnable getForwardRunnable() { return () -> { + final TrackMate trackmate = guiModel.getTrackMate(); + final Model model = guiModel.getModel(); final long start = System.currentTimeMillis(); final boolean ok = trackmate.execTracking(); if ( !ok ) - trackmate.getModel().getLogger().error( trackmate.getErrorMessage() + '\n' ); + model.getLogger().error( trackmate.getErrorMessage() + '\n' ); final long end = System.currentTimeMillis(); - final Logger logger = trackmate.getModel().getLogger(); + final Logger logger = model.getLogger(); logger.log( String.format( "Tracking done in %.1f s.\n", ( end - start ) / 1e3f ) ); - final TrackModel trackModel = trackmate.getModel().getTrackModel(); + final TrackModel trackModel = model.getTrackModel(); final int nTracks = trackModel.nTracks( false ); final IntSummaryStatistics stats = trackModel.unsortedTrackIDs( false ).stream() .mapToInt( id -> trackModel.trackSpots( id ).size() ) @@ -66,12 +74,18 @@ public Runnable getForwardRunnable() logger.log( String.format( " - avg size: %.1f spots.\n", stats.getAverage() ) ); logger.log( String.format( " - min size: %d spots.\n", stats.getMin() ) ); logger.log( String.format( " - max size: %d spots.\n", stats.getMax() ) ); + + // Possibly tweak display settings: color spots by track id. + final DisplaySettings displaySettings = guiModel.getDisplaySettings(); + if ( displaySettings.getSpotColorByType() == TrackMateObject.DEFAULT ) + if ( displaySettings.getSpotColorByFeature().equals( FeatureUtils.USE_UNIFORM_COLOR_KEY ) ) + displaySettings.setSpotColorBy( TrackMateObject.TRACKS, TrackIndexAnalyzer.TRACK_INDEX ); }; } @Override public Cancelable getCancelable() { - return trackmate; + return guiModel.getTrackMate(); } } 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 6379d4551..adab1258f 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 @@ -24,12 +24,10 @@ import java.util.Map; import java.util.Set; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.features.FeatureUtils; +import fiji.plugin.trackmate.gui.GuiModel; 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; @@ -38,13 +36,13 @@ public class GrapherDescriptor extends WizardPanelDescriptor private static final String KEY = "GraphFeatures"; - private final TrackMate trackmate; + private final GuiModel guiModel; - public GrapherDescriptor( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings ) + public GrapherDescriptor( final GuiModel guiModel ) { super( KEY ); - this.trackmate = trackmate; - this.targetPanel = new GrapherPanel( trackmate, selectionModel, displaySettings ); + this.guiModel = guiModel; + this.targetPanel = new GrapherPanel( guiModel ); } @Override @@ -53,17 +51,17 @@ public void aboutToDisplayPanel() // Regen features. final GrapherPanel panel = ( GrapherPanel ) targetPanel; - final Map< String, String > spotFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.SPOTS, trackmate.getModel(), trackmate.getSettings() ); + final Map< String, String > spotFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.SPOTS, guiModel.getModel(), guiModel.getSettings() ); final Set< String > 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 Map< String, String > edgeFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.EDGES, guiModel.getModel(), guiModel.getSettings() ); final Set< String > 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 Map< String, String > trackFeatureNames = FeatureUtils.collectFeatureKeys( TrackMateObject.TRACKS, guiModel.getModel(), guiModel.getSettings() ); final Set< String > trackFeatures = trackFeatureNames.keySet(); final FeaturePlotSelectionPanel trackFeatureSelectionPanel = panel.getTrackFeatureSelectionPanel(); trackFeatureSelectionPanel.setFeatures( trackFeatures, trackFeatureNames ); diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/InitFilterDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/InitFilterDescriptor.java index 4c98c973a..934829447 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/InitFilterDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/InitFilterDescriptor.java @@ -25,9 +25,9 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.features.FeatureFilter; import fiji.plugin.trackmate.features.FeatureUtils; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.components.InitFilterPanel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; import fiji.plugin.trackmate.gui.wizard.WizardPanelDescriptor; @@ -38,14 +38,14 @@ public class InitFilterDescriptor extends WizardPanelDescriptor public static final String KEY = "InitialFiltering"; - private final TrackMate trackmate; + private final GuiModel guiModel; - public InitFilterDescriptor( final TrackMate trackmate, final FeatureFilter filter ) + public InitFilterDescriptor( final GuiModel guiModel, final FeatureFilter filter ) { super( KEY ); - this.trackmate = trackmate; + this.guiModel = guiModel; final Function< String, double[] > valuesCollector = key -> FeatureUtils.collectFeatureValues( - Spot.QUALITY, TrackMateObject.SPOTS, trackmate.getModel(), false ); + Spot.QUALITY, TrackMateObject.SPOTS, guiModel.getModel(), false ); this.targetPanel = new InitFilterPanel( filter, valuesCollector ); } @@ -58,7 +58,7 @@ public Runnable getForwardRunnable() @Override public void run() { - trackmate.getModel().getLogger().log( "\nComputing spot quality histogram...\n", Logger.BLUE_COLOR ); + guiModel.getModel().getLogger().log( "\nComputing spot quality histogram...\n", Logger.BLUE_COLOR ); final InitFilterPanel component = ( InitFilterPanel ) targetPanel; component.refresh(); } @@ -69,9 +69,9 @@ public void run() public void aboutToHidePanel() { final InitFilterPanel component = ( InitFilterPanel ) targetPanel; - trackmate.getSettings().initialSpotFilterValue = component.getFeatureThreshold().value; + guiModel.getSettings().initialSpotFilterValue = component.getFeatureThreshold().value; // Settings persistence. - SettingsPersistence.saveLastUsedSettings( trackmate.getSettings(), trackmate.getModel().getLogger() ); + SettingsPersistence.saveLastUsedSettings( guiModel.getSettings(), guiModel.getModel().getLogger() ); } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/SaveDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/SaveDescriptor.java index ca0275740..919467fda 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/SaveDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/SaveDescriptor.java @@ -31,8 +31,8 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.TrackMatePlugIn; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.components.LogPanel; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.wizard.WizardPanelDescriptor; import fiji.plugin.trackmate.gui.wizard.WizardSequence; import fiji.plugin.trackmate.io.IOUtils; @@ -44,18 +44,15 @@ public class SaveDescriptor extends WizardPanelDescriptor private static final String KEY = "Saving"; - private final TrackMate trackmate; - - private final DisplaySettings displaySettings; - private final WizardSequence sequence; - public SaveDescriptor( final TrackMate trackmate, final DisplaySettings displaySettings, final WizardSequence sequence ) + private final GuiModel guiModel; + + public SaveDescriptor( final GuiModel guiModel, final WizardSequence sequence ) { super( KEY ); + this.guiModel = guiModel; this.targetPanel = sequence.logDescriptor().getPanelComponent(); - this.trackmate = trackmate; - this.displaySettings = displaySettings; this.sequence = sequence; } @@ -70,14 +67,15 @@ public void displayingPanel() if ( TrackMatePlugIn.lastLoadedFile != null && TrackMatePlugIn.lastLoadedFile.exists() ) file = TrackMatePlugIn.lastLoadedFile; else - file = TMUtils.proposeTrackMateSaveFile( trackmate.getSettings(), logger ); + file = TMUtils.proposeTrackMateSaveFile( guiModel.getSettings(), logger ); /* * If we are to save tracks, we better ensures that track and edge * features are there, even if we have to enforce it. */ - if ( trackmate.getModel().getTrackModel().nTracks( false ) > 0 ) + if ( guiModel.getModel().getTrackModel().nTracks( false ) > 0 ) { + final TrackMate trackmate = guiModel.getTrackMate(); trackmate.computeEdgeFeatures( true ); trackmate.computeTrackFeatures( true ); } @@ -97,10 +95,10 @@ public void displayingPanel() final TmXmlWriter writer = new TmXmlWriter( file, logger ); writer.appendLog( logPanel.getTextContent() ); - writer.appendModel( trackmate.getModel() ); - writer.appendSettings( trackmate.getSettings() ); + writer.appendModel( guiModel.getModel() ); + writer.appendSettings( guiModel.getSettings() ); writer.appendGUIState( sequence.current().getPanelDescriptorIdentifier() ); - writer.appendDisplaySettings( displaySettings ); + writer.appendDisplaySettings( guiModel.getDisplaySettings() ); try { diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/SpotFilterDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/SpotFilterDescriptor.java index ddd7a4113..2ea163d22 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/SpotFilterDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/SpotFilterDescriptor.java @@ -23,7 +23,6 @@ import java.awt.Container; import java.util.List; -import java.util.stream.Collectors; import javax.swing.JLabel; @@ -34,15 +33,16 @@ import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.detection.DetectionUtils; import fiji.plugin.trackmate.features.FeatureFilter; -import fiji.plugin.trackmate.features.spot.SpotMorphologyAnalyzerFactory; +import fiji.plugin.trackmate.features.FeatureUtils; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.components.FeatureDisplaySelector; import fiji.plugin.trackmate.gui.components.FilterGuiPanel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; +import fiji.plugin.trackmate.gui.featureselector.AnalyzerSelection; +import fiji.plugin.trackmate.gui.featureselector.AnalyzerSelectionIO; import fiji.plugin.trackmate.gui.wizard.WizardPanelDescriptor; import fiji.plugin.trackmate.io.SettingsPersistence; -import fiji.plugin.trackmate.providers.SpotMorphologyAnalyzerProvider; import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; public class SpotFilterDescriptor extends WizardPanelDescriptor @@ -50,18 +50,18 @@ public class SpotFilterDescriptor extends WizardPanelDescriptor private static final String KEY = "SpotFilter"; - private final TrackMate trackmate; + private final GuiModel guiModel; public SpotFilterDescriptor( - final TrackMate trackmate, + final GuiModel guiModel, final List< FeatureFilter > filters, final FeatureDisplaySelector featureSelector ) { super( KEY ); - this.trackmate = trackmate; + this.guiModel = guiModel; final FilterGuiPanel component = new FilterGuiPanel( - trackmate.getModel(), - trackmate.getSettings(), + guiModel.getModel(), + guiModel.getSettings(), TrackMateObject.SPOTS, filters, Spot.QUALITY, @@ -74,7 +74,8 @@ public SpotFilterDescriptor( private void filterSpots() { final FilterGuiPanel component = ( FilterGuiPanel ) targetPanel; - trackmate.getSettings().setSpotFilters( component.getFeatureFilters() ); + guiModel.getSettings().setSpotFilters( component.getFeatureFilters() ); + final TrackMate trackmate = guiModel.getTrackMate(); trackmate.execSpotFiltering( false ); } @@ -90,11 +91,12 @@ public void run() disabler.disable(); try { - - final Model model = trackmate.getModel(); + final Model model = guiModel.getModel(); + final Settings settings = guiModel.getSettings(); + final TrackMate trackmate = guiModel.getTrackMate(); final Logger logger = model.getLogger(); final String str = "Initial thresholding with a quality threshold above " - + String.format( "%.1f", trackmate.getSettings().initialSpotFilterValue ) + + String.format( "%.1f", settings.initialSpotFilterValue ) + " ...\n"; logger.log( str, Logger.BLUE_COLOR ); final int ntotal = model.getSpots().getNSpots( false ); @@ -103,27 +105,16 @@ public void run() logger.log( String.format( "Retained %d spots out of %d.\n", nselected, ntotal ) ); /* - * Should we add morphology feature analyzers? + * Add analyzers in the user selection and possible the + * morphology ones in 2D or 3D. */ - if ( trackmate.getSettings().detectorFactory != null - && trackmate.getSettings().detectorFactory.has2Dsegmentation() - && DetectionUtils.is2D( trackmate.getSettings().imp ) ) - { - logger.log( "\nAdding morphology analyzers...\n", Logger.BLUE_COLOR ); - final Settings settings = trackmate.getSettings(); - final SpotMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider = new SpotMorphologyAnalyzerProvider( settings.imp.getNChannels() ); - @SuppressWarnings( "rawtypes" ) - final List< SpotMorphologyAnalyzerFactory > factories = spotMorphologyAnalyzerProvider - .getKeys() - .stream() - .map( key -> spotMorphologyAnalyzerProvider.getFactory( key ) ) - .collect( Collectors.toList() ); - factories.forEach( settings::addSpotAnalyzerFactory ); - final StringBuilder strb = new StringBuilder(); - Settings.prettyPrintFeatureAnalyzer( factories, strb ); - logger.log( strb.toString() ); - } + final AnalyzerSelection analyzerSelection = AnalyzerSelectionIO.readUserDefault(); + analyzerSelection.configure( settings ); + logger.log( "\nAdding the following spot feature analyzers...\n", Logger.BLUE_COLOR ); + final StringBuilder strb = new StringBuilder(); + Settings.prettyPrintFeatureAnalyzer( settings.getSpotAnalyzerFactories(), strb ); + logger.log( strb.toString() ); /* * Show and log to progress bar in the filter GUI panel. @@ -140,16 +131,21 @@ public void run() // Calculate features final long start = System.currentTimeMillis(); - final Logger oldLogger = trackmate.getModel().getLogger(); - trackmate.getModel().setLogger( panel.getLogger() ); + final Logger oldLogger = model.getLogger(); + model.setLogger( panel.getLogger() ); trackmate.computeSpotFeatures( true ); final long end = System.currentTimeMillis(); - trackmate.getModel().setLogger( oldLogger ); + model.setLogger( oldLogger ); if ( trackmate.isCanceled() ) logger.log( "Spot feature calculation canceled.\nSome spots will have missing feature values.\n" ); logger.log( String.format( "Calculating features done in %.1f s.\n", ( end - start ) / 1e3f ) ); panel.showProgressBar( false ); + // If spots are not very visible because of the display + // settings, make them visible. + guiModel.getDisplaySettings().setSpotVisible( true ); + guiModel.getDisplaySettings().setSpotColorBy( TrackMateObject.SPOTS, FeatureUtils.USE_UNIFORM_COLOR_KEY ); + // Refresh component. panel.refreshValues(); filterSpots(); @@ -166,19 +162,21 @@ public void run() public void displayingPanel() { final FilterGuiPanel component = ( FilterGuiPanel ) targetPanel; - trackmate.getSettings().setSpotFilters( component.getFeatureFilters() ); - trackmate.execSpotFiltering( false ); + guiModel.getSettings().setSpotFilters( component.getFeatureFilters() ); + guiModel.getTrackMate().execSpotFiltering( false ); } @Override public void aboutToHidePanel() { - final Logger logger = trackmate.getModel().getLogger(); + final Model model = guiModel.getModel(); + final Settings settings = guiModel.getSettings(); + final TrackMate trackmate = guiModel.getTrackMate(); + final Logger logger = model.getLogger(); logger.log( "\nPerforming spot filtering on the following features:\n", Logger.BLUE_COLOR ); - final Model model = trackmate.getModel(); final FilterGuiPanel component = ( FilterGuiPanel ) targetPanel; final List< FeatureFilter > featureFilters = component.getFeatureFilters(); - trackmate.getSettings().setSpotFilters( featureFilters ); + settings.setSpotFilters( featureFilters ); trackmate.execSpotFiltering( false ); final int ntotal = model.getSpots().getNSpots( false ); @@ -190,7 +188,7 @@ public void aboutToHidePanel() { for ( final FeatureFilter ft : featureFilters ) { - String str = " - on " + trackmate.getModel().getFeatureModel().getSpotFeatureNames().get( ft.feature ); + String str = " - on " + model.getFeatureModel().getSpotFeatureNames().get( ft.feature ); if ( ft.isAbove ) str += " above "; else @@ -204,12 +202,12 @@ public void aboutToHidePanel() } // Settings persistence. - SettingsPersistence.saveLastUsedSettings( trackmate.getSettings(), logger ); + SettingsPersistence.saveLastUsedSettings( settings, logger ); } @Override public Cancelable getCancelable() { - return trackmate; + return guiModel.getTrackMate(); } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/TrackFilterDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/TrackFilterDescriptor.java index 99b9f1a48..88337e608 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/TrackFilterDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/TrackFilterDescriptor.java @@ -28,10 +28,12 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.features.FeatureFilter; import fiji.plugin.trackmate.features.FeatureUtils; import fiji.plugin.trackmate.features.track.TrackBranchingAnalyzer; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.components.FeatureDisplaySelector; import fiji.plugin.trackmate.gui.components.FilterGuiPanel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; @@ -45,22 +47,18 @@ public class TrackFilterDescriptor extends WizardPanelDescriptor private static final String KEY = "TrackFilter"; - private final TrackMate trackmate; - - private final DisplaySettings displaySettings; + private final GuiModel guiModel; public TrackFilterDescriptor( - final TrackMate trackmate, + final GuiModel guiModel, final List< FeatureFilter > filters, - final FeatureDisplaySelector featureSelector, - final DisplaySettings displaySettings ) + final FeatureDisplaySelector featureSelector ) { super( KEY ); - this.trackmate = trackmate; - this.displaySettings = displaySettings; + this.guiModel = guiModel; final FilterGuiPanel component = new FilterGuiPanel( - trackmate.getModel(), - trackmate.getSettings(), + guiModel.getModel(), + guiModel.getSettings(), TrackMateObject.TRACKS, filters, TrackBranchingAnalyzer.NUMBER_SPOTS, @@ -73,8 +71,8 @@ public TrackFilterDescriptor( private void filterTracks() { final FilterGuiPanel component = ( FilterGuiPanel ) targetPanel; - trackmate.getSettings().setTrackFilters( component.getFeatureFilters() ); - trackmate.execTrackFiltering( false ); + guiModel.getSettings().setTrackFilters( component.getFeatureFilters() ); + guiModel.getTrackMate().execTrackFiltering( false ); } @Override @@ -89,7 +87,8 @@ public void run() disabler.disable(); try { - final Model model = trackmate.getModel(); + final Model model = guiModel.getModel(); + final TrackMate trackmate = guiModel.getTrackMate(); final Logger logger = model.getLogger(); /* @@ -106,18 +105,19 @@ public void run() logger.log( "\n" ); // Calculate features final long start = System.currentTimeMillis(); - final Logger oldLogger = trackmate.getModel().getLogger(); - trackmate.getModel().setLogger( panel.getLogger() ); + final Logger oldLogger = model.getLogger(); + model.setLogger( panel.getLogger() ); trackmate.computeEdgeFeatures( true ); trackmate.computeTrackFeatures( true ); final long end = System.currentTimeMillis(); - trackmate.getModel().setLogger( oldLogger ); + model.setLogger( oldLogger ); if ( trackmate.isCanceled() ) logger.log( "Spot feature calculation canceled.\nSome spots will have missing feature values.\n" ); logger.log( String.format( "Calculating features done in %.1f s.\n", ( end - start ) / 1e3f ) ); panel.showProgressBar( false ); // Default color spots by track index. + final DisplaySettings displaySettings = guiModel.getDisplaySettings(); displaySettings.setSpotColorBy( TrackMateObject.TRACKS, FeatureUtils.USE_TRACK_INDEX_COLOR_KEY ); // Refresh component. @@ -136,19 +136,22 @@ public void run() public void displayingPanel() { final FilterGuiPanel component = ( FilterGuiPanel ) targetPanel; - trackmate.getSettings().setTrackFilters( component.getFeatureFilters() ); - trackmate.execTrackFiltering( false ); + guiModel.getSettings().setTrackFilters( component.getFeatureFilters() ); + guiModel.getTrackMate().execTrackFiltering( false ); } @Override public void aboutToHidePanel() { - final Logger logger = trackmate.getModel().getLogger(); + final Model model = guiModel.getModel(); + final Settings settings = guiModel.getSettings(); + final TrackMate trackmate = guiModel.getTrackMate(); + + final Logger logger = model.getLogger(); logger.log( "\nPerforming track filtering on the following features:\n", Logger.BLUE_COLOR ); - final Model model = trackmate.getModel(); final FilterGuiPanel component = ( FilterGuiPanel ) targetPanel; final List< FeatureFilter > featureFilters = component.getFeatureFilters(); - trackmate.getSettings().setTrackFilters( featureFilters ); + settings.setTrackFilters( featureFilters ); trackmate.execTrackFiltering( false ); final int ntotal = model.getTrackModel().nTracks( false ); @@ -160,7 +163,7 @@ public void aboutToHidePanel() { for ( final FeatureFilter ft : featureFilters ) { - String str = " - on " + trackmate.getModel().getFeatureModel().getTrackFeatureNames().get( ft.feature ); + String str = " - on " + model.getFeatureModel().getTrackFeatureNames().get( ft.feature ); if ( ft.isAbove ) str += " above "; else @@ -174,6 +177,6 @@ public void aboutToHidePanel() } // Settings persistence. - SettingsPersistence.saveLastUsedSettings( trackmate.getSettings(), logger ); + SettingsPersistence.saveLastUsedSettings( settings, logger ); } } diff --git a/src/main/java/fiji/plugin/trackmate/io/CSVExporter.java b/src/main/java/fiji/plugin/trackmate/io/CSVExporter.java index 11504c93a..bcb5f51fd 100644 --- a/src/main/java/fiji/plugin/trackmate/io/CSVExporter.java +++ b/src/main/java/fiji/plugin/trackmate/io/CSVExporter.java @@ -41,7 +41,6 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.TrackModel; -import fiji.plugin.trackmate.util.TMUtils; public class CSVExporter { @@ -188,7 +187,7 @@ private static void writeHeaderSpots( final CSVWriter writer, final Model model, for ( final String feature : features ) { final Dimension dimension = model.getFeatureModel().getSpotFeatureDimensions().get( feature ); - final String units = TMUtils.getUnitsFor( dimension, model.getSpaceUnits(), model.getTimeUnits() ); + final String units = dimension.units( model.getSpaceUnits(), model.getTimeUnits() ); featureUnits.put( feature, units ); } writeHeader( writer, features, featureNames, featureShortNames, featureUnits, extra ); diff --git a/src/main/java/fiji/plugin/trackmate/io/IOUtils.java b/src/main/java/fiji/plugin/trackmate/io/IOUtils.java index 11931f03f..40ac3e8b5 100644 --- a/src/main/java/fiji/plugin/trackmate/io/IOUtils.java +++ b/src/main/java/fiji/plugin/trackmate/io/IOUtils.java @@ -2,18 +2,18 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * 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 * . @@ -306,18 +306,17 @@ protected JDialog createDialog( final Component lParent ) throws HeadlessExcepti } /** - * Reads and return an integer attribute from a JDom {@link Element}, and + * Read and return an integer attribute from a JDom {@link Element}, and * substitute a default value of 0 if the attribute is not found or of the * wrong type. - * + * * @param element - * the JDom element to read from. + * the element to read from. * @param name - * the name of the attribute to read. + * the name of the integer attribute. * @param logger - * a {@link Logger} to report errors to. - * @return the integer value of the attribute, or 0 if not found or of the - * wrong type. + * error messages will be logged via this logger. + * @return the int value. */ public static final int readIntAttribute( final Element element, final String name, final Logger logger ) { @@ -499,12 +498,11 @@ public static final boolean readStringAttribute( final Element element, final Ma * double, an error is returned. * * @param element - * the JDom element to read from. + * the element to unmarshall. * @param map - * the map to populate. + * the map the unmarshalled info will be added to. * @param errorHolder - * a string builder to append error messages to if something goes - * wrong. + * error messages will be appended to this buffer. * @return true if all values were found and mapped as doubles, * false otherwise and the error holder is updated. */ @@ -564,7 +562,7 @@ public static final boolean writeDownsamplingFactor( final Map< String, Object > } /** - * Adds a parameter attribute to the given element, taken from the given + * Add a parameter attribute to the given element, taken from the given * settings map. Basic checks are made to ensure that the parameter value * can be found and is of the right class. * @@ -577,8 +575,7 @@ public static final boolean writeDownsamplingFactor( final Map< String, Object > * @param expectedClass * the expected class for the value * @param errorHolder - * a string builder to append error messages to if something goes - * wrong. + * a buffer to append possible errors to. * @return true if the parameter was found, of the right class, * and was successfully added to the element, false if * not, and updated the specified error holder. @@ -606,15 +603,29 @@ public static final boolean writeAttribute( final Map< String, Object > settings /** * Stores the given mapping in a given JDom element, using attributes in a * KEY="VALUE" fashion. - * + * * @param map - * the map to marshall. + * the map. * @param element - * the JDom element to update. + * the element to write the map into. */ public static void marshallMap( final Map< String, Double > map, final Element element ) { for ( final String key : map.keySet() ) element.setAttribute( key, map.get( key ).toString() ); } + + /** + * Possibly creates the whole directories needed to save a file with the + * specified path. + * + * @param path + * the path. + * @return true if folders have actually been created. + */ + public static boolean mkdirs( final String path ) + { + final File dir = new File( path ).getParentFile(); + return dir == null ? false : dir.mkdirs(); + } } diff --git a/src/main/java/fiji/plugin/trackmate/io/TGMMImporter.java b/src/main/java/fiji/plugin/trackmate/io/TGMMImporter.java index ae6bdb22c..bfbc94448 100644 --- a/src/main/java/fiji/plugin/trackmate/io/TGMMImporter.java +++ b/src/main/java/fiji/plugin/trackmate/io/TGMMImporter.java @@ -32,12 +32,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import net.imglib2.algorithm.Benchmark; -import net.imglib2.algorithm.OutputAlgorithm; -import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.util.LinAlgHelpers; -import net.imglib2.util.Util; - import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; @@ -50,7 +44,13 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; +import net.imglib2.algorithm.Benchmark; +import net.imglib2.algorithm.OutputAlgorithm; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.util.LinAlgHelpers; +import net.imglib2.util.Util; public class TGMMImporter implements OutputAlgorithm< Model >, Benchmark { @@ -377,7 +377,7 @@ public boolean process() * Make a spot and add it to this frame collection. */ - final Spot spot = new Spot( mx, my, mz, radius, score, lineage + " (" + id + ")" ); + final Spot spot = new SpotBase( mx, my, mz, radius, score, lineage + " (" + id + ")" ); spots.add( spot ); currentSpotID.put( Integer.valueOf( id ), spot ); diff --git a/src/main/java/fiji/plugin/trackmate/io/TmGeffWriter.java b/src/main/java/fiji/plugin/trackmate/io/TmGeffWriter.java new file mode 100644 index 000000000..98fd0909a --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/io/TmGeffWriter.java @@ -0,0 +1,256 @@ +package fiji.plugin.trackmate.io; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jgrapht.graph.DefaultWeightedEdge; +import org.mastodon.geff.GeffAxis; +import org.mastodon.geff.GeffEdge; +import org.mastodon.geff.GeffMetadata; +import org.mastodon.geff.GeffNode; +import org.mastodon.geff.PropMetadata; + +import fiji.plugin.trackmate.Dimension; +import fiji.plugin.trackmate.FeatureModel; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.Spot.SpotVisitor; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.SpotRoi; +import fiji.plugin.trackmate.TrackModel; +import gnu.trove.map.TObjectIntMap; +import gnu.trove.map.hash.TObjectIntHashMap; + +public class TmGeffWriter +{ + + public static void write( final Model model, final String zarrPath ) throws IOException + { + + /* + * Serialize spots + */ + final SpotCollection spots = model.getSpots(); + final Map< String, Boolean > isInt = model.getFeatureModel().getSpotFeatureIsInt(); + final GeffSpotVisitor visitor = new GeffSpotVisitor( isInt ); + spots.iterable( true ).forEach( spot -> spot.accept( visitor ) ); + + /* + * Serialize edges + */ + final TrackModel trackModel = model.getTrackModel(); + final Set< DefaultWeightedEdge > edges = trackModel.edgeSet(); + final FeatureModel fm = model.getFeatureModel(); + + final List< GeffEdge > geffEdges = new ArrayList<>(); + int edgeId = 0; + for ( final DefaultWeightedEdge edge : edges ) + { + final Spot source = trackModel.getEdgeSource( edge ); + final Integer srcId = visitor.spotToId.get( source ); + final Spot target = trackModel.getEdgeTarget( edge ); + final Integer tgtId = visitor.spotToId.get( target ); + if ( srcId == null || tgtId == null ) + continue; + + final boolean swap = source.getFeature( Spot.FRAME ) > target.getFeature( Spot.FRAME ); + final GeffEdge edgeNode = new GeffEdge.Builder() + .setId( edgeId++ ) + .setSourceNodeId( swap ? tgtId : srcId ) + .setTargetNodeId( swap ? srcId : tgtId ) + .setScore( trackModel.getEdgeWeight( edge ) ) + .setDistance( Math.sqrt( source.squareDistanceTo( target ) ) ) + .build(); + + // Feature + for ( final String edgeFeature : fm.getEdgeFeatures() ) + { + final Double ef = fm.getEdgeFeature( edge, edgeFeature ); + if ( ef == null ) + continue; + final Object val = ( fm.getEdgeFeatureIsInt().get( edgeFeature ) + ? ef.intValue() + : ef.doubleValue() ); + edgeNode.setProp( edgeFeature, val ); + } + geffEdges.add( edgeNode ); + } + + /* + * Serialize tracks + */ + // TODO -> nodes with a specific path. + + /* + * Metadata + */ + + // Axes + final List< GeffAxis > axes = buildAxes( model.getTimeUnits(), model.getSpaceUnits() ); + final GeffMetadata metadata = new GeffMetadata( "1.0.0", true, axes ); + + // Spot features + final Map< String, PropMetadata > nodePropsMetadata = new HashMap<>(); + for ( final String spotFeature : fm.getSpotFeatures() ) + { + final String dType = isInt.get( spotFeature ) ? "int32" : "float64"; + final Dimension dimension = fm.getSpotFeatureDimensions().get( spotFeature ); + final String unit = dimension.units( model.getSpaceUnits(), model.getTimeUnits() ); + final String name = fm.getSpotFeatureNames().get( spotFeature ); + // Description stores short names. + final String description = fm.getSpotFeatureShortNames().get( spotFeature ); + final PropMetadata propMetadata = new PropMetadata( spotFeature, dType, false, unit, name, description ); + nodePropsMetadata.put( spotFeature, propMetadata ); + } + metadata.setNodePropsMetadata( nodePropsMetadata ); + + // Edge features + final Map< String, PropMetadata > edgePropsMetadata = new HashMap<>(); + for ( final String edgeFeature : fm.getEdgeFeatures() ) + { + final String dType = fm.getEdgeFeatureIsInt().get( edgeFeature ) ? "int32" : "float64"; + final Dimension dimension = fm.getEdgeFeatureDimensions().get( edgeFeature ); + final String unit = dimension.units( model.getSpaceUnits(), model.getTimeUnits() ); + final String name = fm.getEdgeFeatureNames().get( edgeFeature ); + final String description = fm.getEdgeFeatureShortNames().get( edgeFeature ); + final PropMetadata propMetadata = new PropMetadata( edgeFeature, dType, false, unit, name, description ); + edgePropsMetadata.put( edgeFeature, propMetadata ); + } + metadata.setEdgePropsMetadata( edgePropsMetadata ); + + /* + * Write to disk + */ + + GeffNode.writeToZarr( visitor.nodes, zarrPath, metadata ); + GeffEdge.writeToZarr( geffEdges, zarrPath, metadata ); + GeffMetadata.writeToZarr( metadata, zarrPath ); + } + + private static List< GeffAxis > buildAxes( final String timeUnit, final String spaceUnit ) + { + return Arrays.asList( + GeffAxis.createTimeAxis( GeffAxis.NAME_TIME, timeUnit, null, null ), + GeffAxis.createSpaceAxis( GeffAxis.NAME_SPACE_X, normalizeSpaceUnit( spaceUnit ), null, null ), + GeffAxis.createSpaceAxis( GeffAxis.NAME_SPACE_Y, normalizeSpaceUnit( spaceUnit ), null, null ), + GeffAxis.createSpaceAxis( GeffAxis.NAME_SPACE_Z, normalizeSpaceUnit( spaceUnit ), null, null ) ); + } + + /** + * Maps common unit abbreviations to OME-Zarr compliant names. See + * https://ngff.openmicroscopy.org/latest/#axes-md for the valid set. + */ + static String normalizeSpaceUnit( final String unit ) + { + if ( unit == null || unit.isEmpty() ) + return "pixel"; + switch ( unit.trim() ) + { + case "um": + case "µm": + case "μm": + case "micron": + case "microns": + return "micrometer"; + case "nm": + return "nanometer"; + case "mm": + return "millimeter"; + case "cm": + return "centimeter"; + case "m": + return "meter"; + case "km": + return "kilometer"; + case "pm": + return "picometer"; + case "Å": + return "angstrom"; + default: + return unit; + } + } + + public static class GeffSpotVisitor implements SpotVisitor + { + + /** Features not in the general prop, because they are the core node. */ + private static final Set< String > SLOP_PROPS = Set.of( "FRAME", "POSITION_X", "POSITION_Y", "POSITION_Z", "RADIUS" ); + + private int geffId = 0; + + private final TObjectIntMap< Spot > spotToId = new TObjectIntHashMap< Spot >(); + + private final List< GeffNode > nodes = new ArrayList<>(); + + private final Map< String, Boolean > isInt; + + public GeffSpotVisitor( final Map< String, Boolean > isInt ) + { + this.isInt = isInt; + } + + private void serializeFeatures( final Spot spot, final GeffNode node ) + { + final Map< String, Double > features = spot.getFeatures(); + for ( final Map.Entry< String, Double > entry : features.entrySet() ) + { + final String name = entry.getKey(); + if ( SLOP_PROPS.contains( name ) ) + continue; + + final Object val = ( isInt.get( name ) ? entry.getValue().intValue() : entry.getValue() ); + node.setProp( name, val ); + } + } + + @Override + public void visit( final SpotBase spot ) + { + final GeffNode node = new GeffNode.Builder() + .id( geffId ) + .timepoint( spot.getFeature( Spot.FRAME ).intValue() ) + .x( spot.getDoublePosition( 0 ) ) + .y( spot.getDoublePosition( 1 ) ) + .z( spot.getDoublePosition( 2 ) ) + .radius( spot.getFeature( Spot.RADIUS ).doubleValue() ) + .build(); + serializeFeatures( spot, node ); + nodes.add( node ); + spotToId.put( spot, geffId++ ); + } + + @Override + public void visit( final SpotRoi spot ) + { + final int nPoints = spot.nPoints(); + final double[] polygonX = new double[ nPoints ]; + final double[] polygonY = new double[ nPoints ]; + for ( int i = 0; i < nPoints; i++ ) + { + polygonX[ i ] = spot.x( i ); + polygonY[ i ] = spot.y( i ); + } + + final GeffNode node = new GeffNode.Builder() + .id( geffId ) + .timepoint( spot.getFeature( Spot.FRAME ).intValue() ) + .x( spot.getDoublePosition( 0 ) ) + .y( spot.getDoublePosition( 1 ) ) + .z( spot.getDoublePosition( 2 ) ) + .radius( spot.getFeature( Spot.RADIUS ).doubleValue() ) + .polygonX( polygonX ) + .polygonY( polygonY ) + .build(); + serializeFeatures( spot, node ); + nodes.add( node ); + spotToId.put( spot, geffId++ ); + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/io/TmXmlReader.java b/src/main/java/fiji/plugin/trackmate/io/TmXmlReader.java index 66ac1739b..9703b9ce5 100644 --- a/src/main/java/fiji/plugin/trackmate/io/TmXmlReader.java +++ b/src/main/java/fiji/plugin/trackmate/io/TmXmlReader.java @@ -2,18 +2,18 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * 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 * . @@ -52,8 +52,6 @@ import static fiji.plugin.trackmate.io.TmXmlKeys.FRAME_ATTRIBUTE_NAME; import static fiji.plugin.trackmate.io.TmXmlKeys.GUI_STATE_ATTRIBUTE; import static fiji.plugin.trackmate.io.TmXmlKeys.GUI_STATE_ELEMENT_KEY; -import static fiji.plugin.trackmate.io.TmXmlKeys.GUI_VIEW_ATTRIBUTE; -import static fiji.plugin.trackmate.io.TmXmlKeys.GUI_VIEW_ELEMENT_KEY; import static fiji.plugin.trackmate.io.TmXmlKeys.IMAGE_ELEMENT_KEY; import static fiji.plugin.trackmate.io.TmXmlKeys.IMAGE_FILENAME_ATTRIBUTE_NAME; import static fiji.plugin.trackmate.io.TmXmlKeys.IMAGE_FOLDER_ATTRIBUTE_NAME; @@ -91,6 +89,7 @@ import static fiji.plugin.trackmate.io.TmXmlKeys.TRACK_FILTER_COLLECTION_ELEMENT_KEY; import static fiji.plugin.trackmate.io.TmXmlKeys.TRACK_ID_ELEMENT_KEY; import static fiji.plugin.trackmate.io.TmXmlKeys.TRACK_NAME_ATTRIBUTE_NAME; +import static fiji.plugin.trackmate.io.TmXmlWriter.MESH_FILE_EXTENSION; import static fiji.plugin.trackmate.tracking.TrackerKeys.XML_ATTRIBUTE_TRACKER_NAME; import java.io.File; @@ -104,6 +103,10 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; import org.jdom2.Attribute; import org.jdom2.DataConversionException; @@ -119,10 +122,11 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Logger.StringBuilderLogger; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.SpotMesh; import fiji.plugin.trackmate.SpotRoi; import fiji.plugin.trackmate.detection.SpotDetectorFactoryBase; import fiji.plugin.trackmate.features.FeatureFilter; @@ -136,17 +140,18 @@ import fiji.plugin.trackmate.gui.wizard.descriptors.ConfigureViewsDescriptor; import fiji.plugin.trackmate.providers.DetectorProvider; import fiji.plugin.trackmate.providers.EdgeAnalyzerProvider; +import fiji.plugin.trackmate.providers.Spot2DMorphologyAnalyzerProvider; +import fiji.plugin.trackmate.providers.Spot3DMorphologyAnalyzerProvider; import fiji.plugin.trackmate.providers.SpotAnalyzerProvider; -import fiji.plugin.trackmate.providers.SpotMorphologyAnalyzerProvider; import fiji.plugin.trackmate.providers.TrackAnalyzerProvider; import fiji.plugin.trackmate.providers.TrackerProvider; -import fiji.plugin.trackmate.providers.ViewProvider; import fiji.plugin.trackmate.tracking.SpotTrackerFactory; -import fiji.plugin.trackmate.visualization.TrackMateModelView; -import fiji.plugin.trackmate.visualization.ViewFactory; -import fiji.plugin.trackmate.visualization.trackscheme.TrackScheme; import ij.IJ; import ij.ImagePlus; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.mesh.io.ply.PLYMeshIO; public class TmXmlReader { @@ -184,7 +189,7 @@ public class TmXmlReader */ /** - * Initialize this reader to read the specified file. + * Initializes this reader to read the file given in argument. * * @param file * the file to read. @@ -222,7 +227,7 @@ public TmXmlReader( final File file ) * Returns the log text saved in the file, or null if log text * was not saved. * - * @return the saved log text. + * @return the log. */ public String getLog() { @@ -270,81 +275,6 @@ public DisplaySettings getDisplaySettings() return DisplaySettingsIO.fromJson( dsel.getText() ); } - /** - * Returns the collection of views that were saved in this file. The views - * returned are not rendered yet. - * - * @param provider - * the {@link ViewProvider} to instantiate the view. Each saved - * view must be known by the specified provider. - * @param model - * the model to display in the views. - * @param settings - * the settings to build the views. - * @param selectionModel - * the {@link SelectionModel} model that will be shared with the - * new views. - * @param displaySettings - * the display settings to pass to the view. - * @return the collection of views. - * @see TrackMateModelView#render() - */ - public Collection< TrackMateModelView > getViews( - final ViewProvider provider, - final Model model, - final Settings settings, - final SelectionModel selectionModel, - final DisplaySettings displaySettings ) - { - final Element guiel = root.getChild( GUI_STATE_ELEMENT_KEY ); - if ( null != guiel ) - { - - final List< Element > children = guiel.getChildren( GUI_VIEW_ELEMENT_KEY ); - final Collection< TrackMateModelView > views = new ArrayList<>( children.size() ); - - for ( final Element child : children ) - { - final String viewKey = child.getAttributeValue( GUI_VIEW_ATTRIBUTE ); - if ( null == viewKey ) - { - logger.error( "Could not find view key attribute for element " + child + ".\n" ); - ok = false; - } - else - { - // Do not instantiate TrackScheme if found in the file. - if ( viewKey.equals( TrackScheme.KEY ) ) - continue; - - final ViewFactory factory = provider.getFactory( viewKey ); - if ( null == factory ) - { - logger.error( "Unknown view factory for key " + viewKey + ".\n" ); - ok = false; - continue; - } - - final TrackMateModelView view = factory.create( model, settings, selectionModel, displaySettings ); - if ( null == view ) - { - logger.error( "Unknown view for key " + viewKey + ".\n" ); - ok = false; - } - else - { - views.add( view ); - } - } - } - return views; - } - - logger.error( "Could not find GUI state element.\n" ); - ok = false; - return new ArrayList<>(); - } - /** * Returns the model saved in the file, or null if a saved * model cannot be found in the xml file. @@ -358,6 +288,7 @@ public Model getModel() return null; final Model model = createModel(); + model.pauseUndo(); // TODO // Physical units final String spaceUnits = modelElement.getAttributeValue( SPATIAL_UNITS_ATTRIBUTE_NAME ); @@ -376,7 +307,6 @@ public Model getModel() ok = false; // Track features - try { final Map< Integer, Map< String, Double > > savedFeatureMap = readTrackFeatures( modelElement ); @@ -396,6 +326,7 @@ public Model getModel() } // That's it + model.resumeUndo(); return model; } @@ -428,7 +359,8 @@ public Settings readSettings( final ImagePlus imp ) new SpotAnalyzerProvider( ( imp == null ) ? 1 : imp.getNChannels() ), new EdgeAnalyzerProvider(), new TrackAnalyzerProvider(), - new SpotMorphologyAnalyzerProvider( ( imp == null ) ? 1 : imp.getNChannels() ) ); + new Spot2DMorphologyAnalyzerProvider( ( imp == null ) ? 1 : imp.getNChannels() ), + new Spot3DMorphologyAnalyzerProvider( ( imp == null ) ? 1 : imp.getNChannels() ) ); } /** @@ -438,8 +370,7 @@ public Settings readSettings( final ImagePlus imp ) * file. * * @param imp - * the image to create the settings for, may be - * null. + * the image to store in the new Settings object. * @param detectorProvider * the detector provider, required to configure the settings with * a correct SpotDetectorFactory. If @@ -460,11 +391,11 @@ public Settings readSettings( final ImagePlus imp ) * the track analyzer provider, required to instantiates the * saved {@link TrackAnalyzer}s. If null, will skip * reading track analyzers. - * @param spotMorphologyAnalyzerProvider - * the spot morphology analyzer provider, required to instantiate - * the saved SpotMorphologyAnalyzers. If null, will - * skip reading spot morphology analyzers. - * @return a new, configured {@link Settings} object. + * @param spot2DMorphologyAnalyzerProvider + * the spot 2D morphology provider. + * @param spot3DMorphologyAnalyzerProvider + * the spot 3D morphology provider. + * @return a new Settings object. */ public Settings readSettings( final ImagePlus imp, @@ -473,7 +404,8 @@ public Settings readSettings( final SpotAnalyzerProvider spotAnalyzerProvider, final EdgeAnalyzerProvider edgeAnalyzerProvider, final TrackAnalyzerProvider trackAnalyzerProvider, - final SpotMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider ) + final Spot2DMorphologyAnalyzerProvider spot2DMorphologyAnalyzerProvider, + final Spot3DMorphologyAnalyzerProvider spot3DMorphologyAnalyzerProvider ) { final Element settingsElement = root.getChild( SETTINGS_ELEMENT_KEY ); if ( null == settingsElement ) @@ -520,7 +452,8 @@ public Settings readSettings( spotAnalyzerProvider, edgeAnalyzerProvider, trackAnalyzerProvider, - spotMorphologyAnalyzerProvider ); + spot2DMorphologyAnalyzerProvider, + spot3DMorphologyAnalyzerProvider ); return settings; } @@ -528,7 +461,7 @@ public Settings readSettings( /** * Returns the version string stored in the file. * - * @return the version string. + * @return the version string stored in the file. */ public String getVersion() { @@ -926,7 +859,6 @@ private SpotCollection getSpots( final Element modelElement ) final Map< Integer, Set< Spot > > content = new HashMap<>( frameContent.size() ); for ( final Element currentFrameContent : frameContent ) { - currentFrame = readIntAttribute( currentFrameContent, FRAME_ATTRIBUTE_NAME, logger ); final List< Element > spotContent = currentFrameContent.getChildren( SPOT_ELEMENT_KEY ); final Set< Spot > spotSet = new HashSet<>( spotContent.size() ); @@ -938,20 +870,84 @@ private SpotCollection getSpots( final Element modelElement ) } content.put( currentFrame, spotSet ); } + + // Do we have a mesh file? + final File meshFile = new File( file.getAbsolutePath() + MESH_FILE_EXTENSION ); + if ( meshFile.exists() ) + { + // Matcher for zipped file name. + final String regex = "(\\d+)\\.ply"; + final Pattern pattern = Pattern.compile( regex ); + // Iterate through entries. + try (final ZipFile zipFile = new ZipFile( meshFile )) + { + zipFile.stream().forEach( entry -> { + final String name = entry.getName(); + final Matcher matcher = pattern.matcher( name ); + if ( matcher.matches() ) + { + // Get corresponding spot. + final int id = Integer.parseInt( matcher.group( 1 ) ); + final Spot spot = cache.get( id ); + // Deserialize mesh. + try + { + final Mesh m = PLYMeshIO.open( zipFile.getInputStream( entry ) ); + final BufferMesh mesh = new BufferMesh( m.vertices().size(), m.triangles().size() ); + Meshes.calculateNormals( m, mesh ); + + // Create new spot in the mesh and replace it in the + // cache. + final SpotMesh spotMesh = new SpotMesh( id, mesh ); + spotMesh.copyFeaturesFrom( spot ); + spotMesh.setName( spot.getName() ); + cache.put( id, spotMesh ); + + // And in the content. + final Set< Spot > spots = content.get( spot.getFeature( Spot.FRAME ).intValue() ); + spots.remove( spot ); + spots.add( spotMesh ); + } + catch ( final Exception e ) + { + ok = false; + logger.error( "Problem reading mesh for spot " + id + ":\n" + + e.getMessage() + '\n' ); + e.printStackTrace(); + } + } + + } ); + } + catch ( final ZipException e ) + { + ok = false; + logger.error( "Issues reading the mesh file:\n" + e.getMessage() + '\n' ); + e.printStackTrace(); + } + catch ( final IOException e ) + { + ok = false; + logger.error( "Issues reading the mesh file:\n" + e.getMessage() + '\n' ); + e.printStackTrace(); + } + } + final SpotCollection allSpots = SpotCollection.fromMap( content ); return allSpots; } /** - * Loads the tracks, the track features and the ID of the filtered tracks + * Load the tracks, the track features and the ID of the filtered tracks * into the model specified. The track collection element is expected to be * found as a child of the specified element. * * @param modelElement - * the xml element containing the track collection data. + * the element to read from. * @param model - * the model to populate with tracks. - * @return true if reading tracks was successful, false otherwise. + * the model to add to. + * @return true if reading tracks was successful, + * false otherwise. */ protected boolean readTracks( final Element modelElement, final Model model ) { @@ -1152,23 +1148,15 @@ private Spot createSpotFrom( final Element spotEl ) { // Read id. final int ID = readIntAttribute( spotEl, SPOT_ID_ATTRIBUTE_NAME, logger ); - final Spot spot = new Spot( ID ); - +// final List< Attribute > atts = spotEl.getAttributes(); removeAttributeFromName( atts, SPOT_ID_ATTRIBUTE_NAME ); - // Read name. - String name = spotEl.getAttributeValue( SPOT_NAME_ATTRIBUTE_NAME ); - if ( null == name || name.equals( "" ) ) - name = "ID" + ID; - - spot.setName( name ); - removeAttributeFromName( atts, SPOT_NAME_ATTRIBUTE_NAME ); - /* * Try to read ROI if any. */ final int roiNPoints = readIntAttribute( spotEl, ROI_N_POINTS_ATTRIBUTE_NAME, Logger.VOID_LOGGER ); + final Spot spot; if ( roiNPoints > 2 ) { final double[] xrois = new double[ roiNPoints ]; @@ -1183,10 +1171,22 @@ private Spot createSpotFrom( final Element spotEl ) final double y = Double.parseDouble( vals[ index++ ] ); yrois[ i ] = y; } - spot.setRoi( new SpotRoi( xrois, yrois ) ); + spot = new SpotRoi( ID, xrois, yrois ); + } + else + { + spot = new SpotBase( ID ); } removeAttributeFromName( atts, ROI_N_POINTS_ATTRIBUTE_NAME ); + // Read name. + String name = spotEl.getAttributeValue( SPOT_NAME_ATTRIBUTE_NAME ); + if ( null == name || name.equals( "" ) ) + name = "ID" + ID; + + spot.setName( name ); + removeAttributeFromName( atts, SPOT_NAME_ATTRIBUTE_NAME ); + /* * Read all other attributes -> features. */ @@ -1316,7 +1316,8 @@ private void readAnalyzers( final SpotAnalyzerProvider spotAnalyzerProvider, final EdgeAnalyzerProvider edgeAnalyzerProvider, final TrackAnalyzerProvider trackAnalyzerProvider, - final SpotMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider ) + final Spot2DMorphologyAnalyzerProvider spot2DMorphologyAnalyzerProvider, + final Spot3DMorphologyAnalyzerProvider spot3DMorphologyAnalyzerProvider ) { final Element analyzersEl = settingsElement.getChild( ANALYZER_COLLECTION_ELEMENT_KEY ); @@ -1367,11 +1368,15 @@ private void readAnalyzers( /* * Special case: if we cannot find a matching * analyzer for a declared factory, then we will try - * to see whether it is a morphology spot analyzer, - * that are treated separately. If it is not, we - * give up. + * to see whether it is a morphology spot analyzer + * in 2D then in 3D, that are treated separately. If + * it is not, we give up. */ - spotAnalyzer = spotMorphologyAnalyzerProvider.getFactory( key ); + spotAnalyzer = spot2DMorphologyAnalyzerProvider.getFactory( key ); + if ( spotAnalyzer == null ) + { + spotAnalyzer = spot3DMorphologyAnalyzerProvider.getFactory( key ); + } } if ( null == spotAnalyzer ) diff --git a/src/main/java/fiji/plugin/trackmate/io/TmXmlWriter.java b/src/main/java/fiji/plugin/trackmate/io/TmXmlWriter.java index 4735340a7..d92f048cd 100644 --- a/src/main/java/fiji/plugin/trackmate/io/TmXmlWriter.java +++ b/src/main/java/fiji/plugin/trackmate/io/TmXmlWriter.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -104,6 +104,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import org.jdom2.Attribute; import org.jdom2.Document; @@ -119,6 +121,7 @@ import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.SpotMesh; import fiji.plugin.trackmate.SpotRoi; import fiji.plugin.trackmate.features.FeatureFilter; import fiji.plugin.trackmate.features.edges.EdgeAnalyzer; @@ -128,10 +131,20 @@ import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsIO; +import gnu.trove.map.hash.TIntIntHashMap; +import gnu.trove.procedure.TIntIntProcedure; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.io.ply.PLYMeshIO; +import net.imglib2.mesh.view.TranslateMesh; public class TmXmlWriter { + static final String MESH_FILE_EXTENSION = ".meshes"; + + /** Zip compression level (0-9) */ + private static final int COMPRESSION_LEVEL = 5; + /* * FIELD */ @@ -163,7 +176,7 @@ public TmXmlWriter( final File file ) * @param file * the xml file to write to, will be overwritten. * @param logger - * the logger to use to report progress and write errors. + * a logger instance to log writing progress and errors. */ public TmXmlWriter( final File file, final Logger logger ) { @@ -180,13 +193,15 @@ public TmXmlWriter( final File file, final Logger logger ) /** * Writes the document to the file. Content must be appended first. * + * @throws FileNotFoundException + * if the file exists but is a directory rather than a regular + * file, does not exist but cannot be created, or cannot be + * opened for any other reason. + * @throws IOException + * if there's any problem writing. * @see #appendLog(String) * @see #appendModel(Model) * @see #appendSettings(Settings) - * @throws FileNotFoundException - * if the file cannot be created or opened. - * @throws IOException - * if an I/O error occurs. */ public void writeToFile() throws FileNotFoundException, IOException { @@ -242,6 +257,8 @@ public void appendModel( final Model model ) modelElement.addContent( filteredTrackElement ); root.addContent( modelElement ); + + writeSpotMeshes( model.getSpots().iterable( false ) ); } /** @@ -701,11 +718,7 @@ protected Element echoAnalyzers( final Settings settings ) return analyzersElement; } - /* - * STATIC METHODS - */ - - private static final Element marshalSpot( final Spot spot, final FeatureModel fm ) + private final Element marshalSpot( final Spot spot, final FeatureModel fm ) { final Collection< Attribute > attributes = new ArrayList<>(); final Attribute IDattribute = new Attribute( SPOT_ID_ATTRIBUTE_NAME, "" + spot.ID() ); @@ -729,23 +742,92 @@ private static final Element marshalSpot( final Spot spot, final FeatureModel fm } final Element spotElement = new Element( SPOT_ELEMENT_KEY ); - final SpotRoi roi = spot.getRoi(); - if ( roi != null ) + if ( spot instanceof SpotRoi ) { - final int nPoints = roi.x.length; + final SpotRoi roi = ( SpotRoi ) spot; + final int nPoints = roi.nPoints(); attributes.add( new Attribute( ROI_N_POINTS_ATTRIBUTE_NAME, Integer.toString( nPoints ) ) ); final StringBuilder str = new StringBuilder(); for ( int i = 0; i < nPoints; i++ ) { - str.append( Double.toString( roi.x[ i ] ) ); + str.append( Double.toString( roi.xr( i ) ) ); str.append( ' ' ); - str.append( Double.toString( roi.y[ i ] ) ); + str.append( Double.toString( roi.yr( i ) ) ); str.append( ' ' ); } spotElement.setText( str.toString() ); } - spotElement.setAttributes( attributes ); return spotElement; } + + protected void writeSpotMeshes( final Iterable< Spot > spots ) + { + // Only create the meshes file if at least one spot has a mesh. + boolean hasMesh = false; + for ( final Spot spot : spots ) + { + if ( spot instanceof SpotMesh ) + { + hasMesh = true; + break; + } + } + if ( !hasMesh ) + return; + + // Holder for map spot -> frame + final TIntIntHashMap frameMap = new TIntIntHashMap(); + + // Create zip output stream and write to it. + final File meshFile = new File( file.getAbsolutePath() + MESH_FILE_EXTENSION ); + logger.log( " Writing spot meshes to " + meshFile.getName() + "\n" ); + + try (final ZipOutputStream zos = new ZipOutputStream( new FileOutputStream( meshFile ) )) + { + zos.setMethod( ZipOutputStream.DEFLATED ); + zos.setLevel( COMPRESSION_LEVEL ); + + // Write spot meshes. + for ( final Spot spot : spots ) + { + if ( spot instanceof SpotMesh ) + { + // Save mesh in true coordinates. + final SpotMesh sm = ( SpotMesh ) spot; + final Mesh mesh = sm.getMesh(); + final Mesh translated = TranslateMesh.translate( mesh, spot ); + final byte[] bs = PLYMeshIO.writeBinary( translated ); + + final String entryName = spot.ID() + ".ply"; + zos.putNextEntry( new ZipEntry( entryName ) ); + zos.write( bs ); + zos.closeEntry(); + + frameMap.put( spot.ID(), spot.getFeature( Spot.FRAME ).intValue() ); + } + } + + // Write dict text file. + final StringBuilder str = new StringBuilder(); + str.append( "frame,ID\n" ); + frameMap.forEachEntry( new TIntIntProcedure() + { + + @Override + public boolean execute( final int ID, final int t ) + { + str.append( String.format( "%d,%d\n", t, ID ) ); + return true; + } + } ); + zos.putNextEntry( new ZipEntry( "mesh-info.txt" ) ); + zos.write( str.toString().getBytes() ); + } + catch ( final IOException e ) + { + logger.error( "Problem writing the mesh file:\n" + e.getMessage() ); + e.printStackTrace(); + } + } } diff --git a/src/main/java/fiji/plugin/trackmate/providers/SpotMorphologyAnalyzerProvider.java b/src/main/java/fiji/plugin/trackmate/providers/Spot2DMorphologyAnalyzerProvider.java similarity index 68% rename from src/main/java/fiji/plugin/trackmate/providers/SpotMorphologyAnalyzerProvider.java rename to src/main/java/fiji/plugin/trackmate/providers/Spot2DMorphologyAnalyzerProvider.java index 970beeeab..5da91ae5f 100644 --- a/src/main/java/fiji/plugin/trackmate/providers/SpotMorphologyAnalyzerProvider.java +++ b/src/main/java/fiji/plugin/trackmate/providers/Spot2DMorphologyAnalyzerProvider.java @@ -21,24 +21,24 @@ */ package fiji.plugin.trackmate.providers; -import fiji.plugin.trackmate.features.spot.SpotMorphologyAnalyzerFactory; +import fiji.plugin.trackmate.features.spot.Spot2DMorphologyAnalyzerFactory; @SuppressWarnings( "rawtypes" ) -public class SpotMorphologyAnalyzerProvider extends AbstractProvider< SpotMorphologyAnalyzerFactory > +public class Spot2DMorphologyAnalyzerProvider extends AbstractProvider< Spot2DMorphologyAnalyzerFactory > { private final int nChannels; - public SpotMorphologyAnalyzerProvider( final int nChannels ) + public Spot2DMorphologyAnalyzerProvider( final int nChannels ) { - super( SpotMorphologyAnalyzerFactory.class ); + super( Spot2DMorphologyAnalyzerFactory.class ); this.nChannels = nChannels; } @Override - public SpotMorphologyAnalyzerFactory getFactory( final String key ) + public Spot2DMorphologyAnalyzerFactory getFactory( final String key ) { - final SpotMorphologyAnalyzerFactory factory = super.getFactory( key ); + final Spot2DMorphologyAnalyzerFactory factory = super.getFactory( key ); if ( factory == null ) return null; @@ -48,7 +48,7 @@ public SpotMorphologyAnalyzerFactory getFactory( final String key ) public static void main( final String[] args ) { - final SpotMorphologyAnalyzerProvider provider = new SpotMorphologyAnalyzerProvider( 2 ); + final Spot2DMorphologyAnalyzerProvider provider = new Spot2DMorphologyAnalyzerProvider( 2 ); System.out.println( provider.echo() ); } } diff --git a/src/main/java/fiji/plugin/trackmate/providers/Spot3DMorphologyAnalyzerProvider.java b/src/main/java/fiji/plugin/trackmate/providers/Spot3DMorphologyAnalyzerProvider.java new file mode 100644 index 000000000..b8d605b45 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/providers/Spot3DMorphologyAnalyzerProvider.java @@ -0,0 +1,57 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.providers; + +import fiji.plugin.trackmate.features.spot.Spot3DMorphologyAnalyzerFactory; + +/** + * Provider for 3D morphology analyzers, working on SpotMesh. + */ +@SuppressWarnings( "rawtypes" ) +public class Spot3DMorphologyAnalyzerProvider extends AbstractProvider< Spot3DMorphologyAnalyzerFactory > +{ + + private final int nChannels; + + public Spot3DMorphologyAnalyzerProvider( final int nChannels ) + { + super( Spot3DMorphologyAnalyzerFactory.class ); + this.nChannels = nChannels; + } + + @Override + public Spot3DMorphologyAnalyzerFactory getFactory( final String key ) + { + final Spot3DMorphologyAnalyzerFactory factory = super.getFactory( key ); + if ( factory == null ) + return null; + + factory.setNChannels( nChannels ); + return factory; + } + + public static void main( final String[] args ) + { + final Spot3DMorphologyAnalyzerProvider provider = new Spot3DMorphologyAnalyzerProvider( 2 ); + System.out.println( provider.echo() ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/tracking/SpotTrackerFactoryGenericConfig.java b/src/main/java/fiji/plugin/trackmate/tracking/SpotTrackerConfigFactory.java similarity index 70% rename from src/main/java/fiji/plugin/trackmate/tracking/SpotTrackerFactoryGenericConfig.java rename to src/main/java/fiji/plugin/trackmate/tracking/SpotTrackerConfigFactory.java index d42d137ea..d42678070 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/SpotTrackerFactoryGenericConfig.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/SpotTrackerConfigFactory.java @@ -23,12 +23,13 @@ import java.util.Map; +import org.scijava.ui.config.Configurator; +import org.scijava.ui.config.visitors.Maps; + import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.gui.components.ConfigurationPanel; -import fiji.plugin.trackmate.util.cli.Configurator; -import fiji.plugin.trackmate.util.cli.FactoryGenericConfig; -import fiji.plugin.trackmate.util.cli.GenericConfigurationPanel; -import fiji.plugin.trackmate.util.cli.TrackMateSettingsBuilder; +import fiji.plugin.trackmate.util.config.FactoryGenericConfig; +import fiji.plugin.trackmate.util.config.GenericConfigPanel; /** * Interface for tracker factories that need to be configured with a @@ -40,23 +41,19 @@ * the type of {@link Configurator} used to configure the detector * factory. */ -public interface SpotTrackerFactoryGenericConfig< C extends Configurator > extends SpotTrackerFactory, FactoryGenericConfig< C > +public interface SpotTrackerConfigFactory< C extends Configurator > extends SpotTrackerFactory, FactoryGenericConfig< C > { @Override public default ConfigurationPanel getTrackerConfigurationPanel( final Model model ) { - final C config = getConfigurator(); - return new GenericConfigurationPanel( - config, - getName(), - getIcon(), - getUrl() ); + final C config = createConfig(); + return new GenericConfigPanel( config ); } @Override default Map< String, Object > getDefaultSettings() { - return TrackMateSettingsBuilder.getDefaultSettings( getConfigurator() ); + return Maps.toMap( createConfig() ); } } diff --git a/src/main/java/fiji/plugin/trackmate/tracking/SpotTrackerFactory.java b/src/main/java/fiji/plugin/trackmate/tracking/SpotTrackerFactory.java index 0408ed586..abf224ac7 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/SpotTrackerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/SpotTrackerFactory.java @@ -2,18 +2,18 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * 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 * . @@ -52,5 +52,4 @@ public interface SpotTrackerFactory extends TrackMateFactoryBase< SpotTrackerFac * @return a new configuration panel. */ public ConfigurationPanel getTrackerConfigurationPanel( final Model model ); - } diff --git a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/LAPUtils.java b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/LAPUtils.java index d38de90e2..09a714ed3 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/LAPUtils.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/LAPUtils.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -102,7 +102,7 @@ public class LAPUtils /** * Utility method to put a value in a map, contained in a mother map. Here * it is mainly use to feed feature penalties to LAP tracker settings map. - * + * * @param motherMap * the mother map * @param motherKey @@ -138,7 +138,7 @@ public static final boolean addFeaturePenaltyToSettings( final Map< ?, ? > mothe /** * Returns a new settings map filled with default values suitable for the * trajectory segments linking (gap/split/merge). - * + * * @return a new map. */ public static final Map< String, Object > getDefaultSegmentSettingsMap() @@ -208,18 +208,18 @@ public static String echoFeaturePenalties( final Map< String, Double > featurePe * For instance: if 2 spots differ by twice the value in a feature which is * in the penalty map with a factor of 1, they will look as if they * were twice as far. - * + * * @param s0 - * the first spot. + * the source spot. * @param s1 - * the second spot. + * the target spot. * @param distanceCutOff - * the distance cutoff beyond which the cost is set to blocking - * value. + * the distance cutoff. * @param blockingValue * the blocking value. * @param featurePenalties - * the map of feature penalties. + * the feature penalties, as a map of feature keys to penalty + * weight. * @return the linking cost. */ public static final double computeLinkingCostFor( final Spot s0, final Spot s1, final double distanceCutOff, final double blockingValue, final Map< String, Double > featurePenalties ) @@ -250,15 +250,18 @@ public static final double computeLinkingCostFor( final Spot s0, final Spot s1, * are indeed found in all spots, because if such a feature is absent from * one spot, the LAP trackers simply ignores the penalty and does not * generate an error. - * + * * @param settings * the map to test. + * @param linking + * if true will also test for the presence of the + * frame-to-frame linking keys. If false will only + * test for the segment linking keys. * @param errorHolder * a {@link StringBuilder} that will contain an error message if * the check is not successful. - * @param linking - * whether to check linking settings as well - * @return true if the settings map is valid. + * @return true if the settings map can be used with the LAP + * trackers. */ public static final boolean checkSettingsValidity( final Map< String, Object > settings, final StringBuilder errorHolder, final boolean linking ) { @@ -270,12 +273,12 @@ public static final boolean checkSettingsValidity( final Map< String, Object > s boolean ok = true; // Linking - if ( linking ) - { - ok = ok & checkParameter( settings, KEY_LINKING_MAX_DISTANCE, Double.class, errorHolder ); - ok = ok & checkFeatureMap( settings, KEY_LINKING_FEATURE_PENALTIES, errorHolder ); - } - // Gap-closing + if (linking) + { + ok = ok & checkParameter( settings, KEY_LINKING_MAX_DISTANCE, Double.class, errorHolder ); + ok = ok & checkFeatureMap( settings, KEY_LINKING_FEATURE_PENALTIES, errorHolder ); + } + // Gap-closing ok = ok & checkParameter( settings, KEY_ALLOW_GAP_CLOSING, Boolean.class, errorHolder ); ok = ok & checkParameter( settings, KEY_GAP_CLOSING_MAX_DISTANCE, Double.class, errorHolder ); ok = ok & checkParameter( settings, KEY_GAP_CLOSING_MAX_FRAME_GAP, Integer.class, errorHolder ); @@ -292,14 +295,14 @@ public static final boolean checkSettingsValidity( final Map< String, Object > s ok = ok & checkParameter( settings, KEY_CUTOFF_PERCENTILE, Double.class, errorHolder ); ok = ok & checkParameter( settings, KEY_ALTERNATIVE_LINKING_COST_FACTOR, Double.class, errorHolder ); ok = ok & checkParameter( settings, KEY_BLOCKING_VALUE, Double.class, errorHolder ); - - // Check keys + + // Check keys final List< String > mandatoryKeys = new ArrayList<>(); if ( linking ) - { - mandatoryKeys.add( KEY_LINKING_MAX_DISTANCE ); - } - mandatoryKeys.add( KEY_ALLOW_GAP_CLOSING ); + { + mandatoryKeys.add( KEY_LINKING_MAX_DISTANCE ); + } + mandatoryKeys.add( KEY_ALLOW_GAP_CLOSING ); mandatoryKeys.add( KEY_GAP_CLOSING_MAX_DISTANCE ); mandatoryKeys.add( KEY_GAP_CLOSING_MAX_FRAME_GAP ); mandatoryKeys.add( KEY_ALLOW_TRACK_SPLITTING ); @@ -311,16 +314,18 @@ public static final boolean checkSettingsValidity( final Map< String, Object > s mandatoryKeys.add( KEY_BLOCKING_VALUE ); final List< String > optionalKeys = new ArrayList<>(); if ( linking ) - { - optionalKeys.add( KEY_LINKING_FEATURE_PENALTIES ); - } - optionalKeys.add( KEY_GAP_CLOSING_FEATURE_PENALTIES ); + { + optionalKeys.add( KEY_LINKING_FEATURE_PENALTIES ); + } + optionalKeys.add( KEY_GAP_CLOSING_FEATURE_PENALTIES ); optionalKeys.add( KEY_SPLITTING_FEATURE_PENALTIES ); optionalKeys.add( KEY_MERGING_FEATURE_PENALTIES ); - optionalKeys.add( KEY_KALMAN_SEARCH_RADIUS ); + optionalKeys.add( KEY_KALMAN_SEARCH_RADIUS ); ok = ok & checkMapKeys( settings, mandatoryKeys, optionalKeys, errorHolder ); - return ok; + return ok; } + + /** * Check the validity of a feature penalty map in a settings map. @@ -404,7 +409,7 @@ public static final void echoMatrix( final double[][] m ) /** * Display the cost matrix solved by the Hungarian algorithm in the LAP * approach. - * + * * @param costs * the cost matrix * @param nSegments diff --git a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/DefaultCostMatrixCreator.java b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/DefaultCostMatrixCreator.java index 0d9eaaae4..7ff0bea19 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/DefaultCostMatrixCreator.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/DefaultCostMatrixCreator.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -31,13 +31,13 @@ /** * A {@link CostMatrixCreator} that build a cost matrix from 3 lists containing * the sources, the targets and the associated costs. - * + * * @author Jean-Yves Tinevez - 2014 - * + * * @param - * the type of the source objects (rows). + * the type of sources. * @param - * the type of the target objects (columns). + * the type of targets. */ public class DefaultCostMatrixCreator< K extends Comparable< K >, J extends Comparable< J > > implements CostMatrixCreator< K, J > { diff --git a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanLinkingCostMatrixCreator.java b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanLinkingCostMatrixCreator.java index 435249f58..4292d750e 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanLinkingCostMatrixCreator.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanLinkingCostMatrixCreator.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -31,13 +31,13 @@ * A {@link CostMatrixCreator} that can generate a cost matrix from a list of * sources, a list of targets and a {@link CostFunction} that can generate a * cost for any combination. - * + * * @author Jean-Yves Tinevez - 2014 - * + * * @param - * the type of the source objects. + * the type of sources. * @param - * the type of the target objects. + * the type of targets. */ public class JaqamanLinkingCostMatrixCreator< K extends Comparable< K >, J extends Comparable< J > > implements CostMatrixCreator< K, J > { @@ -167,7 +167,7 @@ public String getErrorMessage() * Careful, it can be null if not acceptable costs have been * found for the specified configuration. In that case, the lists returned * by {@link #getSourceList()} and {@link #getTargetList()} are empty. - * + * * @return a new {@link SparseCostMatrix} or null. */ @Override diff --git a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanSegmentCostMatrixCreator.java b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanSegmentCostMatrixCreator.java index 554e89d6a..cd1ebbb82 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanSegmentCostMatrixCreator.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanSegmentCostMatrixCreator.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -68,9 +68,9 @@ * non-infinite costs. *
  • Costs are based on square distance +/- feature penalties. * - * + * * @author Jean-Yves Tinevez - 2014 - * + * */ public class JaqamanSegmentCostMatrixCreator implements CostMatrixCreator< Spot, Spot >, MultiThreaded { @@ -100,9 +100,11 @@ public class JaqamanSegmentCostMatrixCreator implements CostMatrixCreator< Spot, * segment linking cost matrix. * * @param graph - * the track segment graph. + * the graph from which connected components (segments) will be + * extracted. * @param settings - * the settings map. + * the settings for the cost matrix, as map containing the + * Jaqaman LAP keys. */ public JaqamanSegmentCostMatrixCreator( final Graph< Spot, DefaultWeightedEdge > graph, final Map< String, Object > settings ) { diff --git a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/SparseCostMatrix.java b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/SparseCostMatrix.java index 9124f1bf2..97782786d 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/SparseCostMatrix.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/SparseCostMatrix.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -47,7 +47,7 @@ * Volgenant paper: Volgenant. Linear and semi-assignment problems: A core * oriented approach. Computers & Operations Research (1996) vol. 23 (10) pp. * 917-932 - * + * * @author Jean-Yves Tinevez - 2014 */ public class SparseCostMatrix @@ -101,12 +101,12 @@ public class SparseCostMatrix * These two arrays must be arranged row by row, starting with the first * one. And in each row, the columns must be sorted in increasing order (to * facilitate index search). Also, each row must have at least one - * non-infinte cost. If not, an {@link IllegalArgumentException} is thrown. + * non-infinite cost. If not, an {@link IllegalArgumentException} is thrown. *
      *
    1. number an int[] array, with one element per * row, that contains the number of non infinite cost for a row. *
    - * + * * @param cc * the cost array. * @param kk @@ -114,7 +114,7 @@ public class SparseCostMatrix * @param number * the number of element for each row. * @param nCols - * the number of columns in the matrix. + * the number of columns in the cost matrix. * @throws IllegalArgumentException * if the cost and column arrays are not of the same size, if * the column array is not sorted row by row, of if one row has @@ -306,7 +306,7 @@ public String toString( final List< ? > rows, final List< ? > columns ) * Computes the total cost for an assignment specified by row. It is * supposed that row i is assigned to column * rowAssignment[i]. - * + * * @param rowAssignment * the assignment, specified by row. * @return the total cost for this assignment. @@ -327,7 +327,7 @@ public double totalAssignmentCost( final int[] rowAssignment ) * Creates and returns a new double[][] matrix representing a * non-sparse version of this cost matrix. Missing costs are replace by * {@link Double#MAX_VALUE}. - * + * * @return a new double[][] */ public double[][] toFullMatrix() @@ -355,7 +355,7 @@ public double[][] toFullMatrix() * Returns the value stored by this matrix at the specified row and column. * If a value is not present in the sparse matrix, the specified missing * value is returned. - * + * * @param i * the row. * @param j @@ -376,7 +376,7 @@ public final double get( final int i, final int j, final double missingValue ) /** * Exposes the array of all the non-infinite costs. - * + * * @return the costs. */ public double[] getCosts() @@ -397,14 +397,14 @@ public int getNRows() /** * Returns the vertical concatenation of this matrix with the specified one. * So that if this matrix is A and the specified matrix is B, you get - * + * *
     	 * -----
     	 * | A |
     	 * | B |
     	 * -----
     	 * 
    - * + * * @param B * the matrix to concatenate this matrix with * @return a new sparse matrix. @@ -438,13 +438,13 @@ public final SparseCostMatrix vcat( final SparseCostMatrix B ) /** * Returns the horizontal concatenation of this matrix with the specified * one. So that if this matrix is A and the specified matrix is B, you get - * + * *
     	 * -------
     	 * | A B |
     	 * -------
     	 * 
    - * + * * @param B * the matrix to concatenate this matrix with * @return a new sparse matrix. @@ -493,7 +493,7 @@ public final SparseCostMatrix hcat( final SparseCostMatrix B ) /** * Returns the transpose of this matrix. - * + * * @return a new sparse matrix. */ public final SparseCostMatrix transpose() @@ -556,7 +556,7 @@ public final SparseCostMatrix transpose() /** * Replace all the non-infinite values of this matrix by the specified * value. - * + * * @param value * the value to write in this matrix. */ diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTracker.java b/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTracker.java index afc71de59..1cb6ce27c 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTracker.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTracker.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -36,6 +36,7 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.tracking.SpotTracker; import fiji.plugin.trackmate.tracking.jaqaman.JaqamanLinker; @@ -85,19 +86,20 @@ public class KalmanTracker implements SpotTracker, Benchmark, Cancelable */ /** - * Create a new Kalman tracker. - * + * Creates a new Kalman tracker. + * * @param spots * the spots to track. * @param maxSearchRadius - * the maximum search radius when growing a track. + * the maximal search radius to continue a track, in physical + * units. * @param maxFrameGap - * the maximum frame gap to bridge. + * the max frame gap when detections are missing, after which a + * track will be stopped. * @param initialSearchRadius - * the initial search radius to create a track. + * the initial search radius to nucleate new tracks. * @param featurePenalties - * feature penalties to use in the cost function. Can be - * null. + * the feature penalties. */ public KalmanTracker( final SpotCollection spots, final double maxSearchRadius, final int maxFrameGap, final double initialSearchRadius, final Map< String, Double > featurePenalties ) { @@ -244,17 +246,17 @@ public boolean process() { final double[] X = kf.predict(); final Spot s = kalmanFiltersMap.get( kf ); - final Spot predSpot = new Spot( X[ 0 ], X[ 1 ], X[ 2 ], s.getFeature( Spot.RADIUS ), s.getFeature( Spot.QUALITY ) ); + final Spot predSpot = new SpotBase( X[ 0 ], X[ 1 ], X[ 2 ], s.getFeature( Spot.RADIUS ), s.getFeature( Spot.QUALITY ) ); // copy the necessary features of original spot to the predicted // spot if ( null != featurePenalties ) - predSpot.copyFeatures( s, featurePenalties ); + predSpot.copyFeaturesFrom( s, featurePenalties.keySet() ); predictionMap.put( predSpot, kf ); if ( savePredictions ) { - final Spot pred = new Spot( X[ 0 ], X[ 1 ], X[ 2 ], s.getFeature( Spot.RADIUS ), s.getFeature( Spot.QUALITY ) ); + final Spot pred = new SpotBase( X[ 0 ], X[ 1 ], X[ 2 ], s.getFeature( Spot.RADIUS ), s.getFeature( Spot.QUALITY ) ); pred.setName( "Pred_" + s.getName() ); pred.putFeature( Spot.RADIUS, s.getFeature( Spot.RADIUS ) ); predictionsCollection.add( predSpot, frame ); diff --git a/src/main/java/fiji/plugin/trackmate/tracking/overlap/OverlapTracker.java b/src/main/java/fiji/plugin/trackmate/tracking/overlap/OverlapTracker.java index 4159f8cdf..eca36b32a 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/overlap/OverlapTracker.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/overlap/OverlapTracker.java @@ -25,7 +25,6 @@ import java.awt.Color; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -205,7 +204,11 @@ public boolean process() final Map< Spot, Polygon2D > targetGeometries = createGeometry( spots.iterable( targetFrame, true ), method, enlargeFactor ); if ( sourceGeometries.isEmpty() || targetGeometries.isEmpty() ) + { + sourceGeometries = targetGeometries; + logger.setProgress( ( double ) progress++ / spots.keySet().size() ); continue; + } final ExecutorService executors = Threads.newFixedThreadPool( numThreads ); final List< Future< IoULink > > futures = new ArrayList<>(); @@ -299,18 +302,17 @@ private static SimplePolygon2D toPolygon( final Spot spot, final double scale ) { final double xc = spot.getDoublePosition( 0 ); final double yc = spot.getDoublePosition( 1 ); - final SpotRoi roi = spot.getRoi(); final SimplePolygon2D poly; - if ( roi == null ) + if ( spot instanceof SpotRoi ) { - final double radius = spot.getFeature( Spot.RADIUS ).doubleValue(); - poly = new SimplePolygon2D( new Circle2D( xc, yc, radius ).asPolyline( 32 ) ); + final SpotRoi roi = ( SpotRoi ) spot; + final double[][] out = roi.toArray( 0., 0., 1., 1. ); + poly = new SimplePolygon2D( out[ 0 ], out[ 1 ] ); } else { - final double[] xcoords = roi.toPolygonX( 1., 0., xc, 1. ); - final double[] ycoords = roi.toPolygonY( 1., 0., yc, 1. ); - poly = new SimplePolygon2D( xcoords, ycoords ); + final double radius = spot.getFeature( Spot.RADIUS ).doubleValue(); + poly = new SimplePolygon2D( new Circle2D( xc, yc, radius ).asPolyline( 32 ) ); } return poly.transform( AffineTransform2D.createScaling( new Point2D( xc, yc ), scale, scale ) ); } @@ -319,19 +321,19 @@ private static Rectangle2D toBoundingBox( final Spot spot, final double scale ) { final double xc = spot.getDoublePosition( 0 ); final double yc = spot.getDoublePosition( 1 ); - final SpotRoi roi = spot.getRoi(); - if ( roi == null ) + if ( spot instanceof SpotRoi ) { - final double radius = spot.getFeature( Spot.RADIUS ).doubleValue() * scale; - return new Rectangle2D( xc - radius, yc - radius, 2 * radius, 2 * radius ); + final SpotRoi roi = ( SpotRoi ) spot; + final double minX = roi.realMin( 0 ) * scale; + final double maxX = roi.realMax( 0 ) * scale; + final double minY = roi.realMin( 1 ) * scale; + final double maxY = roi.realMax( 1 ) * scale; + return new Rectangle2D( xc + minX, yc + minY, maxX - minX, maxY - minY ); } else { - final double minX = Arrays.stream( roi.x ).min().getAsDouble() * scale; - final double maxX = Arrays.stream( roi.x ).max().getAsDouble() * scale; - final double minY = Arrays.stream( roi.y ).min().getAsDouble() * scale; - final double maxY = Arrays.stream( roi.y ).max().getAsDouble() * scale; - return new Rectangle2D( xc + minX, yc + minY, maxX - minX, maxY - minY ); + final double radius = spot.getFeature( Spot.RADIUS ).doubleValue() * scale; + return new Rectangle2D( xc - radius, yc - radius, 2 * radius, 2 * radius ); } } diff --git a/src/main/java/fiji/plugin/trackmate/undo/UndoRedoStack.java b/src/main/java/fiji/plugin/trackmate/undo/UndoRedoStack.java new file mode 100644 index 000000000..f66c1be19 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/undo/UndoRedoStack.java @@ -0,0 +1,809 @@ +package fiji.plugin.trackmate.undo; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jgrapht.graph.DefaultWeightedEdge; + +import fiji.plugin.trackmate.FeatureModel; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; +import fiji.plugin.trackmate.ModelChangeListener; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.Spot.SpotVisitor; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.SpotRoi; +import fiji.plugin.trackmate.TrackModel; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.impl.nio.BufferMesh; + +public class UndoRedoStack implements ModelChangeListener +{ + + private final Model model; + + private boolean paused = false; + + private final Deque< ModelUndoableCommand > undoStack = new ArrayDeque<>(); + + private final Deque< ModelUndoableCommand > redoStack = new ArrayDeque<>(); + + private final Map< Spot, Map< String, Double > > spotFeatureValuesBefore = new HashMap<>(); + + private final Map< SpotRoi, double[][] > spotPolygonValuesBefore = new HashMap<>(); + + private final Map< SpotMesh, BufferMesh > spotMeshValuesBefore = new HashMap<>(); + + private final Map< DefaultWeightedEdge, Map< String, Double > > edgeFeatureValuesBefore = new HashMap<>(); + + private final Map< Spot, String > spotNameBefore = new HashMap<>(); + + /** + * Track states before the operation, keyed by track ID. Only holds "before" + * values. + */ + private final Map< Integer, TrackState > trackStatesBefore = new HashMap<>(); + + private final int maxSize; + + public UndoRedoStack( final Model model ) + { + this( model, 50 ); + } + + public UndoRedoStack( final Model model, final int maxSize ) + { + this.model = model; + this.maxSize = maxSize; + model.addModelChangeListener( this ); + } + + public void pauseUndo() + { + this.paused = true; + } + + public void resumeUndo() + { + this.paused = false; + } + + public void undo() + { + if ( undoStack.isEmpty() ) + return; + + final ModelUndoableCommand command = undoStack.removeLast(); + redoStack.addLast( command ); + command.restoreBefore( model ); + } + + public void redo() + { + if ( redoStack.isEmpty() ) + return; + + final ModelUndoableCommand command = redoStack.removeLast(); + undoStack.addLast( command ); + command.restoreAfter( model ); + } + + @Override + public void modelChanged( final ModelChangeEvent event ) + { + if ( event.getEventID() != ModelChangeEvent.MODEL_MODIFIED ) + { + // Only deal with model modified events. Other events are not + // undoable and results in clearing the undo/redo stack. + + undoStack.clear(); + redoStack.clear(); + spotFeatureValuesBefore.clear(); + edgeFeatureValuesBefore.clear(); + spotPolygonValuesBefore.clear(); + spotNameBefore.clear(); + trackStatesBefore.clear(); + return; + } + + if ( paused ) + return; + + final ModelUndoableCommand command = toCommand( event ); + redoStack.clear(); + if ( undoStack.size() >= maxSize ) + undoStack.removeFirst(); + + undoStack.addLast( command ); + } + + private ModelUndoableCommand toCommand( final ModelChangeEvent event ) + { + final ModelUndoableCommand command = new ModelUndoableCommand(); + + // First, process track states that were flagged via flagTrackForUndo() + // (e.g., for track renames or before merge operations) + // We need to fill in the "after" state + for ( final Map.Entry< Integer, TrackState > entry : trackStatesBefore.entrySet() ) + { + final Integer trackId = entry.getKey(); + final TrackState beforeState = entry.getValue(); + final TrackModel trackModel = model.getTrackModel(); + + // Capture "after" state (current state after the modification) + // Track may no longer exist if it was removed (all spots deleted) + final String nameAfter = trackModel.name( trackId ); + // Check if track still exists before getting visibility (isVisible + // throws NPE for non-existent tracks) + final Boolean visibilityAfter = nameAfter != null ? trackModel.isVisible( trackId ) : null; + final Set< Spot > spotsAfter = trackModel.trackSpots( trackId ); + + final TrackState fullState = new TrackState( + beforeState.nameBefore, + beforeState.visibilityBefore, + nameAfter != null ? nameAfter : beforeState.nameBefore, + visibilityAfter != null ? visibilityAfter : beforeState.visibilityBefore, + spotsAfter != null ? new HashSet<>( spotsAfter ) : new HashSet<>( beforeState.spots ) ); + command.trackStatesBefore.put( trackId, fullState ); + } + + // Also capture track states for tracks affected by edge/spot operations + // (these only have "before" state captured, "after" will be same as + // before for now) + for ( final Integer trackId : event.getTrackUpdated() ) + { + if ( !command.trackStatesBefore.containsKey( trackId ) ) + { + final TrackModel trackModel = model.getTrackModel(); + final String name = trackModel.name( trackId ); + final boolean visibility = trackModel.isVisible( trackId ); + final Set< Spot > spots = new HashSet<>( trackModel.trackSpots( trackId ) ); + final TrackState state = new TrackState( name, visibility, name, visibility, spots ); + command.trackStatesBefore.put( trackId, state ); + } + } + + // First pass: collect spots by their flag + for ( final Spot spot : event.getSpots() ) + { + final Integer flag = event.getSpotFlag( spot ); + if ( flag == null ) + continue; + + if ( flag == ModelChangeEvent.FLAG_SPOT_ADDED ) + { + command.spotsAdded.add( spot ); + } + else if ( flag == ModelChangeEvent.FLAG_SPOT_REMOVED ) + { + command.spotsRemoved.add( spot ); + } + else if ( flag == ModelChangeEvent.FLAG_SPOT_MODIFIED ) + { + final Map< String, Double > previousFeatureValues = spotFeatureValuesBefore.get( spot ); + final Map< String, Double > currentFeatureValues = spot.getFeatures(); + final String previousName = spotNameBefore.get( spot ); + final String currentName = spot.getName(); + + // Only store features that actually changed + final Map< String, Double > changedFeaturesBefore = new HashMap<>(); + final Map< String, Double > changedFeaturesAfter = new HashMap<>(); + for ( final Map.Entry< String, Double > entry : previousFeatureValues.entrySet() ) + { + final String key = entry.getKey(); + final Double beforeValue = entry.getValue(); + final Double afterValue = currentFeatureValues.get( key ); + if ( beforeValue == null && afterValue != null || + beforeValue != null && !beforeValue.equals( afterValue ) ) + { + changedFeaturesBefore.put( key, beforeValue ); + changedFeaturesAfter.put( key, afterValue ); + } + } + + // Only store if there are actual changes + if ( !changedFeaturesBefore.isEmpty() ) + { + command.spotFeatureValuesBefore.put( spot, changedFeaturesBefore ); + command.spotFeatureValuesAfter.put( spot, changedFeaturesAfter ); + } + + // Store name only if it changed + if ( previousName != null && !previousName.equals( currentName ) ) + { + command.spotNameBefore.put( spot, previousName ); + command.spotNameAfter.put( spot, currentName ); + } + + if ( spot instanceof SpotRoi ) + { + final SpotRoi spotRoi = ( SpotRoi ) spot; + final double[][] polygonBefore = spotPolygonValuesBefore.get( spotRoi ); + final double[][] polygonAfter = copyPolygon( spotRoi ); + // Only store if polygon changed + if ( !polygonsEqual( polygonBefore, polygonAfter ) ) + { + command.spotPolygonValuesBefore.put( spotRoi, polygonBefore ); + command.spotPolygonValuesAfter.put( spotRoi, polygonAfter ); + } + } + + if ( spot instanceof SpotMesh ) + { + final SpotMesh spotMesh = ( SpotMesh ) spot; + final BufferMesh meshBefore = spotMeshValuesBefore.get( spotMesh ); + final BufferMesh meshAfter = copyMesh( spotMesh ); + // Only store if mesh changed + if ( !Meshes.equals( meshBefore, meshAfter ) ) + { + command.spotMeshValuesBefore.put( spotMesh, meshBefore ); + command.spotMeshValuesAfter.put( spotMesh, meshAfter ); + } + } + } + else if ( flag == ModelChangeEvent.FLAG_SPOT_FRAME_CHANGED ) + { + // Spot moved - capture position features that changed + final Map< String, Double > previousFeatureValues = spotFeatureValuesBefore.get( spot ); + final Map< String, Double > currentFeatureValues = spot.getFeatures(); + + if ( previousFeatureValues != null ) + { + final Map< String, Double > changedFeaturesBefore = new HashMap<>(); + final Map< String, Double > changedFeaturesAfter = new HashMap<>(); + + for ( final String posKey : new String[] { Spot.POSITION_X, Spot.POSITION_Y, Spot.POSITION_Z } ) + { + final Double beforeValue = previousFeatureValues.get( posKey ); + final Double afterValue = currentFeatureValues.get( posKey ); + if ( beforeValue != null && ( afterValue == null || !beforeValue.equals( afterValue ) ) ) + { + changedFeaturesBefore.put( posKey, beforeValue ); + changedFeaturesAfter.put( posKey, afterValue ); + } + } + + if ( !changedFeaturesBefore.isEmpty() ) + { + command.spotFeatureValuesBefore.put( spot, changedFeaturesBefore ); + command.spotFeatureValuesAfter.put( spot, changedFeaturesAfter ); + } + } + } + } + + // Second pass: handle spots that were both added and removed in the + // same transaction + // (they are transient and should be removed from both lists) + final Set< Spot > transientSpots = new HashSet<>(); + for ( final Spot removedSpot : command.spotsRemoved ) + { + for ( final Spot addedSpot : command.spotsAdded ) + { + if ( removedSpot.ID() == addedSpot.ID() ) + { + transientSpots.add( removedSpot ); + break; + } + } + } + command.spotsAdded.removeAll( transientSpots ); + command.spotsRemoved.removeAll( transientSpots ); + for ( final DefaultWeightedEdge edge : event.getEdges() ) + { + if ( event.getEdgeFlag( edge ) == ModelChangeEvent.FLAG_EDGE_ADDED ) + { + command.edgesAdded.add( new fiji.plugin.trackmate.undo.UndoRedoStack.ModelUndoableCommand.EdgeRep( + model.getTrackModel().getEdgeSource( edge ), + model.getTrackModel().getEdgeTarget( edge ), + model.getTrackModel().getEdgeWeight( edge ) ) ); + } + else if ( event.getEdgeFlag( edge ) == ModelChangeEvent.FLAG_EDGE_REMOVED ) + { + command.edgesRemoved.add( new fiji.plugin.trackmate.undo.UndoRedoStack.ModelUndoableCommand.EdgeRep( + model.getTrackModel().getEdgeSource( edge ), + model.getTrackModel().getEdgeTarget( edge ), + model.getTrackModel().getEdgeWeight( edge ) ) ); + } + else if ( event.getEdgeFlag( edge ) == ModelChangeEvent.FLAG_EDGE_MODIFIED ) + { + // Only store edge features that actually changed + // Note: beforeFeatures can be null if the edge wasn't flagged + // with beforeEdit() + final Map< String, Double > beforeFeatures = edgeFeatureValuesBefore.get( edge ); + final Map< String, Double > afterFeatures = copyEdgeFeatures( edge ); + + if ( beforeFeatures != null ) + { + final Map< String, Double > changedFeatures = new HashMap<>(); + + for ( final Map.Entry< String, Double > entry : beforeFeatures.entrySet() ) + { + final String key = entry.getKey(); + final Double beforeValue = entry.getValue(); + final Double afterValue = afterFeatures.get( key ); + if ( beforeValue == null && afterValue != null || + beforeValue != null && !beforeValue.equals( afterValue ) ) + { + changedFeatures.put( key, beforeValue ); + } + } + + if ( !changedFeatures.isEmpty() ) + { + command.edgeFeatureValuesBefore.put( edge, changedFeatures ); + } + } + else + { + // No before state captured, store all features for safety + command.edgeFeatureValuesBefore.put( edge, afterFeatures ); + } + } + } + spotFeatureValuesBefore.clear(); + edgeFeatureValuesBefore.clear(); + spotPolygonValuesBefore.clear(); + spotMeshValuesBefore.clear(); + spotNameBefore.clear(); + trackStatesBefore.clear(); + return command; + } + + /** + * Holds the state of a track before and after an operation. Used to restore + * track names and visibility after undo/redo. + */ + private static class TrackState + { + final String nameBefore; + + final String nameAfter; + + final boolean visibilityBefore; + + final boolean visibilityAfter; + + final Set< Spot > spots; + + TrackState( final String nameBefore, final boolean visibilityBefore, + final String nameAfter, final boolean visibilityAfter, + final Set< Spot > spots ) + { + this.nameBefore = nameBefore; + this.visibilityBefore = visibilityBefore; + this.nameAfter = nameAfter; + this.visibilityAfter = visibilityAfter; + this.spots = spots; + } + } + + private static class ModelUndoableCommand + { + + private static record EdgeRep( Spot source, Spot target, double weight ) + {} + + private final List< EdgeRep > edgesRemoved = new ArrayList<>(); + + private final List< EdgeRep > edgesAdded = new ArrayList<>(); + + private final List< Spot > spotsAdded = new ArrayList<>(); + + private final List< Spot > spotsRemoved = new ArrayList<>(); + + private final Map< Spot, Map< String, Double > > spotFeatureValuesBefore = new HashMap<>(); + + private final Map< Spot, Map< String, Double > > spotFeatureValuesAfter = new HashMap<>(); + + private final Map< SpotRoi, double[][] > spotPolygonValuesBefore = new HashMap<>(); + + private final Map< SpotRoi, double[][] > spotPolygonValuesAfter = new HashMap<>(); + + private final Map< SpotMesh, BufferMesh > spotMeshValuesBefore = new HashMap<>(); + + private final Map< SpotMesh, BufferMesh > spotMeshValuesAfter = new HashMap<>(); + + private final Map< DefaultWeightedEdge, Map< String, Double > > edgeFeatureValuesBefore = new HashMap<>(); + + private final Map< DefaultWeightedEdge, Map< String, Double > > edgeFeatureValuesAfter = new HashMap<>(); + + private final Map< Spot, String > spotNameAfter = new HashMap<>(); + + private final Map< Spot, String > spotNameBefore = new HashMap<>(); + + /** Track states before the operation, keyed by track ID. */ + private final Map< Integer, TrackState > trackStatesBefore = new HashMap<>(); + + public void restoreBefore( final Model model ) + { + model.pauseUndo(); + model.beginUpdate(); + try + { + for ( final EdgeRep edge : edgesAdded ) + model.removeEdge( edge.source, edge.target ); + + for ( final Spot spot : spotsAdded ) + model.removeSpot( spot ); + + for ( final Spot spot : spotsRemoved ) + model.addSpotTo( spot, spot.getFeature( Spot.FRAME ).intValue() ); + + for ( final EdgeRep edge : edgesRemoved ) + model.addEdge( edge.source, edge.target, edge.weight ); + + // Collect all spots that need restoration (features, name, + // polygon, or mesh changed) + final Set< Spot > spotsToRestore = new HashSet<>(); + spotsToRestore.addAll( spotFeatureValuesBefore.keySet() ); + spotsToRestore.addAll( spotNameBefore.keySet() ); + spotsToRestore.addAll( spotPolygonValuesBefore.keySet() ); + spotsToRestore.addAll( spotMeshValuesBefore.keySet() ); + + for ( final Spot spot : spotsToRestore ) + { + model.beforeEdit( spot ); // to notify about update + final String nameBefore = spotNameBefore.get( spot ); + if ( nameBefore != null ) + spot.setName( nameBefore ); + + final Map< String, Double > featuresBefore = spotFeatureValuesBefore.get( spot ); + if ( featuresBefore != null ) + featuresBefore.forEach( ( key, value ) -> spot.putFeature( key, value ) ); + + if ( spot instanceof SpotRoi ) + { + final SpotRoi spotRoi = ( SpotRoi ) spot; + final double[][] polygonBefore = spotPolygonValuesBefore.get( spotRoi ); + if ( polygonBefore != null ) + updatePolygon( spotRoi, polygonBefore ); + } + + if ( spot instanceof SpotMesh ) + { + final SpotMesh spotMesh = ( SpotMesh ) spot; + final BufferMesh meshBefore = spotMeshValuesBefore.get( spotMesh ); + if ( meshBefore != null ) + updateMesh( spotMesh, meshBefore ); + } + } + for ( final DefaultWeightedEdge edge : edgeFeatureValuesBefore.keySet() ) + edgeFeatureValuesBefore.get( edge ).forEach( ( key, value ) -> { + if ( value == null ) + model.getFeatureModel().removeEdgeFeature( edge, key ); + else + model.getFeatureModel().putEdgeFeature( edge, key, value ); + } ); + + // Restore track names and visibility after topology is rebuilt + // (undo = restore before state) + restoreTrackStatesFromCommand( model, true ); + } + finally + { + model.endUpdate(); + } + model.resumeUndo(); + } + + public void restoreAfter( final Model model ) + { + model.pauseUndo(); + model.beginUpdate(); + try + { + for ( final EdgeRep edge : edgesRemoved ) + model.removeEdge( edge.source, edge.target ); + + for ( final Spot spot : spotsRemoved ) + model.removeSpot( spot ); + + for ( final Spot spot : spotsAdded ) + model.addSpotTo( spot, spot.getFeature( Spot.FRAME ).intValue() ); + + for ( final EdgeRep edge : edgesAdded ) + model.addEdge( edge.source, edge.target, edge.weight ); + + // Collect all spots that need restoration (features, name, + // polygon, or mesh changed) + final Set< Spot > spotsToRestore = new HashSet<>(); + spotsToRestore.addAll( spotFeatureValuesAfter.keySet() ); + spotsToRestore.addAll( spotNameAfter.keySet() ); + spotsToRestore.addAll( spotPolygonValuesAfter.keySet() ); + spotsToRestore.addAll( spotMeshValuesAfter.keySet() ); + + for ( final Spot spot : spotsToRestore ) + { + model.beforeEdit( spot ); // to notify about update + final String nameAfter = spotNameAfter.get( spot ); + if ( nameAfter != null ) + spot.setName( nameAfter ); + + final Map< String, Double > featuresAfter = spotFeatureValuesAfter.get( spot ); + if ( featuresAfter != null ) + featuresAfter.forEach( ( key, value ) -> spot.putFeature( key, value ) ); + + if ( spot instanceof SpotRoi ) + { + final SpotRoi spotRoi = ( SpotRoi ) spot; + final double[][] polygonAfter = spotPolygonValuesAfter.get( spotRoi ); + if ( polygonAfter != null ) + updatePolygon( spotRoi, polygonAfter ); + } + + if ( spot instanceof SpotMesh ) + { + final SpotMesh spotMesh = ( SpotMesh ) spot; + final BufferMesh meshAfter = spotMeshValuesAfter.get( spotMesh ); + if ( meshAfter != null ) + updateMesh( spotMesh, meshAfter ); + } + } + for ( final DefaultWeightedEdge edge : edgeFeatureValuesAfter.keySet() ) + edgeFeatureValuesAfter.get( edge ).forEach( ( key, value ) -> { + if ( value == null ) + model.getFeatureModel().removeEdgeFeature( edge, key ); + else + model.getFeatureModel().putEdgeFeature( edge, key, value ); + } ); + + // Restore track names and visibility after topology is rebuilt + // (redo = restore after state) + restoreTrackStatesFromCommand( model, false ); + } + finally + { + model.endUpdate(); + } + model.resumeUndo(); + } + + /** + * Restores track names and visibility from the captured states in the + * command. After an undo or redo operation, the track topology is + * rebuilt by the {@link TrackModel.MyGraphListener}, but track names + * and visibility are not restored. This method finds the tracks that + * contain the spots from the captured states and restores their names + * and visibility. + * + * @param model + * the model + * @param restoreBefore + * if true, restore the "before" state (undo); if false, + * restore the "after" state (redo) + */ + private void restoreTrackStatesFromCommand( final Model model, final boolean restoreBefore ) + { + final TrackModel trackModel = model.getTrackModel(); + + // Build a map from spot ID to current track ID + final Map< Integer, Integer > spotToCurrentTrackId = new HashMap<>(); + for ( final Integer trackId : trackModel.trackIDs( false ) ) + { + for ( final Spot spot : trackModel.trackSpots( trackId ) ) + spotToCurrentTrackId.put( spot.ID(), trackId ); + } + + // Group captured track states by the current track they map to + // This handles the case where multiple tracks (e.g., from a split) + // merge back into one + final Map< Integer, Map.Entry< Integer, TrackState > > currentTrackToBestState = new HashMap<>(); + + for ( final Map.Entry< Integer, TrackState > entry : trackStatesBefore.entrySet() ) + { + final Integer oldTrackId = entry.getKey(); + final TrackState state = entry.getValue(); + + // Find a spot from the old track that still exists + Spot referenceSpot = null; + for ( final Spot spot : state.spots ) + { + if ( trackModel.vertexSet().contains( spot ) ) + { + referenceSpot = spot; + break; + } + } + + if ( referenceSpot != null ) + { + final Integer currentTrackId = spotToCurrentTrackId.get( referenceSpot.ID() ); + if ( currentTrackId != null ) + { + // If this current track already has a state, keep the + // one with the lowest oldTrackId + // (the original track before split) + if ( !currentTrackToBestState.containsKey( currentTrackId ) || oldTrackId < currentTrackToBestState.get( currentTrackId ).getKey() ) + currentTrackToBestState.put( currentTrackId, entry ); + } + } + } + + // Now restore names - each current track gets at most one name + // restoration + for ( final Map.Entry< Integer, Map.Entry< Integer, TrackState > > e : currentTrackToBestState.entrySet() ) + { + final Integer currentTrackId = e.getKey(); + final Map.Entry< Integer, TrackState > stateEntry = e.getValue(); + final TrackState state = stateEntry.getValue(); + + // Choose which state to restore based on restoreBefore flag + final String nameToRestore = restoreBefore ? state.nameBefore : state.nameAfter; + final boolean visibilityToRestore = restoreBefore ? state.visibilityBefore : state.visibilityAfter; + + // Restore name and visibility + trackModel.setName( currentTrackId, nameToRestore ); + trackModel.setVisibility( currentTrackId, visibilityToRestore ); + } + + // Don't clear trackStatesBefore - it's part of the command and may + // be needed for redo + } + } + + private class UndoStorer implements SpotVisitor + { + + @Override + public void visit( final SpotBase spot ) + { + // Spot features. + spotFeatureValuesBefore.put( spot, new HashMap<>( spot.getFeatures() ) ); + // Touching edge features. + final TrackModel trackModel = model.getTrackModel(); + final Set< DefaultWeightedEdge > touchingEdges = trackModel.edgesOf( spot ); + for ( final DefaultWeightedEdge edge : touchingEdges ) + edgeFeatureValuesBefore.put( edge, copyEdgeFeatures( edge ) ); + // Spot name. + spotNameBefore.put( spot, spot.getName() ); + } + + @Override + public void visit( final SpotRoi spot ) + { + visit( ( SpotBase ) spot ); + spotPolygonValuesBefore.put( spot, copyPolygon( spot ) ); + } + + @Override + public void visit( final SpotMesh spot ) + { + visit( ( SpotBase ) spot ); + spotMeshValuesBefore.put( spot, copyMesh( spot ) ); + } + } + + private final Map< String, Double > copyEdgeFeatures( final DefaultWeightedEdge edge ) + { + final FeatureModel featureModel = model.getFeatureModel(); + final Collection< String > edgeFeatures = featureModel.getEdgeFeatures(); + final Map< String, Double > featureValues = new HashMap<>(); + for ( final String feature : edgeFeatures ) + { + final Double value = featureModel.getEdgeFeature( edge, feature ); + featureValues.put( feature, value ); + } + return featureValues; + } + + private final UndoStorer undoStorer = new UndoStorer(); + + public void flagForUndo( final Spot spot ) + { + if ( paused ) + return; + spot.accept( undoStorer ); + } + + /** + * Flags a track for undo by capturing its current name and visibility. This + * should be called before modifying a track's name or structure. + * + * @param trackId + * the track ID to flag for undo + */ + public void flagTrackForUndo( final Integer trackId ) + { + if ( paused ) + { + + return; + } + final TrackModel trackModel = model.getTrackModel(); + final String currentName = trackModel.name( trackId ); + final boolean currentVisibility = trackModel.isVisible( trackId ); + final Set< Spot > currentSpots = new HashSet<>( trackModel.trackSpots( trackId ) ); + // Capture "before" state; "after" state will be filled in toCommand() + trackStatesBefore.put( trackId, new TrackState( currentName, currentVisibility, null, false, currentSpots ) ); + } + + /** + * Flags all tracks for undo by capturing their current names and + * visibility. This should be called before operations that may restructure + * tracks (e.g., adding/removing edges that may merge or split tracks). + */ + public void flagAllTracksForUndo() + { + if ( paused ) + return; + final TrackModel trackModel = model.getTrackModel(); + for ( final Integer trackId : trackModel.trackIDs( false ) ) + { + final String currentName = trackModel.name( trackId ); + final boolean currentVisibility = trackModel.isVisible( trackId ); + final Set< Spot > currentSpots = new HashSet<>( trackModel.trackSpots( trackId ) ); + // Capture "before" state; "after" will be filled in toCommand() + trackStatesBefore.put( trackId, new TrackState( currentName, currentVisibility, null, false, currentSpots ) ); + } + } + + private static final double[][] copyPolygon( final SpotRoi spot ) + { + final int nPoints = spot.nPoints(); + final double[] x = new double[ nPoints ]; + final double[] y = new double[ nPoints ]; + for ( int i = 0; i < nPoints; i++ ) + { + x[ i ] = spot.xr( i ); + y[ i ] = spot.yr( i ); + } + return new double[][] { x, y }; + } + + private static final void updatePolygon( final SpotRoi spot, final double[][] polygon ) + { + final int nPoints = spot.nPoints(); + for ( int i = 0; i < nPoints; i++ ) + { + spot.setXr( i, polygon[ 0 ][ i ] ); + spot.setYr( i, polygon[ 1 ][ i ] ); + } + } + + private static final boolean polygonsEqual( final double[][] a, final double[][] b ) + { + if ( a == null && b == null ) + return true; + if ( a == null || b == null ) + return false; + if ( a.length != b.length ) + return false; + if ( a.length == 0 ) + return true; + if ( a[ 0 ].length != b[ 0 ].length ) + return false; + + for ( int i = 0; i < a[ 0 ].length; i++ ) + { + if ( Double.compare( a[ 0 ][ i ], b[ 0 ][ i ] ) != 0 || Double.compare( a[ 1 ][ i ], b[ 1 ][ i ] ) != 0 ) + return false; + } + return true; + } + + private static final BufferMesh copyMesh( final SpotMesh spot ) + { + final Mesh source = spot.getMesh(); + final BufferMesh copy = new BufferMesh( source.vertices().size(), source.triangles().size() ); + Meshes.copy( source, copy ); + // This copy is centered on (0,0,0) so we need to translate it to the + // spot's position + Meshes.translate( copy, spot.positionAsDoubleArray() ); + return copy; + } + + private static final void updateMesh( final SpotMesh spot, final BufferMesh mesh ) + { + spot.setMesh( mesh ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/util/ChartExporter.java b/src/main/java/fiji/plugin/trackmate/util/ChartExporter.java index 5b8d88a03..3da46c7be 100644 --- a/src/main/java/fiji/plugin/trackmate/util/ChartExporter.java +++ b/src/main/java/fiji/plugin/trackmate/util/ChartExporter.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -43,15 +43,15 @@ /** * A collection of static utilities made to export a JPanel to various scalable * file format. - * + * * @author Jean-Yves Tinevez, 2011 - 2021 */ public class ChartExporter { /** - * Exports a JFreeChart to SVG. - * + * Export a JFreeChart to SVG. + * * @param svgFile * the target svg file. * @param chart @@ -61,9 +61,7 @@ public class ChartExporter * @param height * the height of the panel the chart is painted in. * @throws UnsupportedEncodingException - * if the encoding is not supported. - * @throws IOException - * on IO error. + * If the UTF-8 encoding is not supported. */ public static void exportChartAsSVG( final File svgFile, final JFreeChart chart, final int width, final int height ) throws UnsupportedEncodingException, IOException { @@ -86,7 +84,7 @@ public static void exportChartAsSVG( final File svgFile, final JFreeChart chart, /** * Export a JFreeChart to PDF. - * + * * @param pdfFile * the target pdf file. * @param chart diff --git a/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java b/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java index 2d6c677a8..0db3d952b 100644 --- a/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java +++ b/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java @@ -168,6 +168,8 @@ protected Pair< Model, Double > runPreviewDetection( final Settings lSettings = new Settings( settings.imp ); lSettings.tstart = frame; lSettings.tend = frame; + lSettings.zstart = settings.zstart; + lSettings.zend = settings.zend; settings.setRoi( settings.imp.getRoi() ); lSettings.detectorFactory = detectorFactory; diff --git a/src/main/java/fiji/plugin/trackmate/util/ImpCloseWindowListener.java b/src/main/java/fiji/plugin/trackmate/util/ImpCloseWindowListener.java new file mode 100644 index 000000000..2bb9251b6 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/util/ImpCloseWindowListener.java @@ -0,0 +1,111 @@ + +package fiji.plugin.trackmate.util; + +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; +import java.util.function.BooleanSupplier; + +import ij.gui.ImageWindow; + +/** + * Intercepts the ImageJ window closing event and prevents the image from being + * closed when the user clicks on the close button. Then asks for confirmation. + */ +public class ImpCloseWindowListener implements WindowListener +{ + + /** + * Adds a listener to the given ImageWindow that intercepts the close event + * and asks for confirmation before closing the window. If the user + * confirms, the window is closed and the onClosed runnable is executed. + * + * @param window + * the ImageWindow to wrap. + * @param confirmClose + * a BooleanSupplier that returns true if the window should be + * closed, false otherwise. It can be build from a JOptionPane or + * any other confirmation dialog. + * @param onClosed + * what to do when the window is closed. Can be null. + */ + public static void wrap( final ImageWindow window, final BooleanSupplier confirmClose, final Runnable onClosed ) + { + new ImpCloseWindowListener( window, confirmClose, onClosed ); + } + + private final WindowListener nativeListener; + + private final Runnable onClosed; + + private final BooleanSupplier confirmClose; + + private ImpCloseWindowListener( + final ImageWindow win, + final BooleanSupplier confirmClose, + final Runnable onClosed ) + { + this.confirmClose = confirmClose; + this.onClosed = onClosed; + // Identify and remove IJ close listener + WindowListener tmp = null; + final WindowListener[] listeners = win.getWindowListeners(); + for ( final WindowListener listener : listeners ) + { + if ( ImageWindow.class.isAssignableFrom( listener.getClass() ) ) + { + tmp = listener; + win.removeWindowListener( tmp ); + break; + } + } + if ( tmp == null ) + throw new IllegalStateException( "Could not find native ImageWindow listener." ); + this.nativeListener = tmp; + win.addWindowListener( this ); + } + + @Override + public void windowOpened( final WindowEvent e ) + { + nativeListener.windowOpened( e ); + } + + @Override + public void windowClosing( final WindowEvent e ) + { + if ( confirmClose.getAsBoolean() ) + nativeListener.windowClosing( e ); + } + + @Override + public void windowClosed( final WindowEvent e ) + { + nativeListener.windowClosed( e ); + if ( onClosed != null ) + onClosed.run(); + } + + @Override + public void windowIconified( final WindowEvent e ) + { + nativeListener.windowIconified( e ); + } + + @Override + public void windowDeiconified( final WindowEvent e ) + { + nativeListener.windowDeiconified( e ); + } + + @Override + public void windowActivated( final WindowEvent e ) + { + nativeListener.windowActivated( e ); + } + + @Override + public void windowDeactivated( final WindowEvent e ) + { + nativeListener.windowDeactivated( e ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/util/OnRequestUpdater.java b/src/main/java/fiji/plugin/trackmate/util/OnRequestUpdater.java index a9f292818..01cf189a9 100644 --- a/src/main/java/fiji/plugin/trackmate/util/OnRequestUpdater.java +++ b/src/main/java/fiji/plugin/trackmate/util/OnRequestUpdater.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -68,9 +68,9 @@ * not optimal, and that for general heavy use refreshing, another solution must * be sought. In the meantime, it is recommended that this class is used for * simple purpose. - * + * * @author Albert Cardona - * + * */ public class OnRequestUpdater extends Thread { @@ -81,9 +81,9 @@ public class OnRequestUpdater extends Thread /** * Constructor autostarts thread - * + * * @param refreshable - * the refreshable target + * the refreshable to update. */ public OnRequestUpdater( final Refreshable refreshable ) { diff --git a/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhood.java b/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhood.java index b661a55a1..eaf3a5d3c 100644 --- a/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhood.java +++ b/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhood.java @@ -22,10 +22,10 @@ package fiji.plugin.trackmate.util; import fiji.plugin.trackmate.Spot; -import net.imagej.ImgPlus; import net.imglib2.FinalInterval; import net.imglib2.Interval; import net.imglib2.Positionable; +import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealPositionable; import net.imglib2.algorithm.neighborhood.Neighborhood; @@ -35,6 +35,7 @@ import net.imglib2.algorithm.region.localneighborhood.RectangleNeighborhoodGPL; import net.imglib2.outofbounds.OutOfBoundsMirrorExpWindowingFactory; import net.imglib2.type.numeric.RealType; +import net.imglib2.view.Views; public class SpotNeighborhood< T extends RealType< T > > implements Neighborhood< T > { @@ -53,18 +54,22 @@ public class SpotNeighborhood< T extends RealType< T > > implements Neighborhood * CONSTRUCTOR */ - public SpotNeighborhood( final Spot spot, final ImgPlus< T > img ) + public SpotNeighborhood( final Spot spot, final RandomAccessible< T > ra, final double[] calibration ) { - this.calibration = TMUtils.getSpatialCalibration( img ); - // Center - this.center = new long[ img.numDimensions() ]; + this.calibration = calibration; + // Center, span and interval. + this.center = new long[ ra.numDimensions() ]; + final long[] span = new long[ ra.numDimensions() ]; + final long[] min = new long[ra.numDimensions()]; + final long[] max = new long[ ra.numDimensions() ]; for ( int d = 0; d < center.length; d++ ) - center[ d ] = Math.round( spot.getFeature( Spot.POSITION_FEATURES[ d ] ).doubleValue() / calibration[ d ] ); - - // Span - final long[] span = new long[ img.numDimensions() ]; - for ( int d = 0; d < span.length; d++ ) + { + center[ d ] = Math.round( spot.getDoublePosition( d ) / calibration[ d ] ); span[ d ] = Math.round( spot.getFeature( Spot.RADIUS ) / calibration[ d ] ); + min[d] = center[d] - span[d]; + max[d] = center[d] + span[d]; + } + final FinalInterval interval = new FinalInterval( min, max ); // Neighborhood @@ -74,28 +79,26 @@ public SpotNeighborhood( final Spot spot, final ImgPlus< T > img ) * have to test pedantically. */ + final RandomAccessibleInterval< T > rai = Views.interval( ra, interval ); final OutOfBoundsMirrorExpWindowingFactory< T, RandomAccessibleInterval< T > > oob = new OutOfBoundsMirrorExpWindowingFactory<>(); - if ( img.numDimensions() == 2 && img.dimension( 0 ) < 2 || img.dimension( 1 ) < 2 ) + if ( ra.numDimensions() == 1 ) { - if ( img.dimension( 0 ) < 2 ) - span[ 0 ] = 0; - else - span[ 1 ] = 0; - this.neighborhood = new RectangleNeighborhoodGPL<>( img, oob ); + span[ 0 ] = 0; + this.neighborhood = new RectangleNeighborhoodGPL<>( rai, oob ); neighborhood.setPosition( center ); neighborhood.setSpan( span ); } - else if ( img.numDimensions() == 2 ) + else if ( ra.numDimensions() == 2 ) { - this.neighborhood = new EllipseNeighborhood<>( img, center, span, oob ); + this.neighborhood = new EllipseNeighborhood<>( rai, center, span, oob ); } - else if ( img.numDimensions() == 3 ) + else if ( ra.numDimensions() == 3 ) { - this.neighborhood = new EllipsoidNeighborhood<>( img, center, span, oob ); + this.neighborhood = new EllipsoidNeighborhood<>( rai, center, span, oob ); } else { - throw new IllegalArgumentException( "Source input must be 1D, 2D or 3D, got nDims = " + img.numDimensions() ); + throw new IllegalArgumentException( "Source input must be 1D, 2D or 3D, got nDims = " + ra.numDimensions() ); } } diff --git a/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhoodCursor.java b/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhoodCursor.java index ac63b1e78..89185f867 100644 --- a/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhoodCursor.java +++ b/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhoodCursor.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -61,24 +61,22 @@ public SpotNeighborhoodCursor( final SpotNeighborhood< T > sn ) /** * Stores the relative calibrated position with respect to the * neighborhood center. - * + * * @param position - * the array in which to write the position. + * an array to store to relative position in. */ public void getRelativePosition( final double[] position ) { cursor.localize( pos ); for ( int d = 0; d < center.length; d++ ) - { position[ d ] = calibration[ d ] * ( pos[ d ] - center[ d ] ); - } } /** * Returns the square distance measured from the center of the domain to the * current cursor position, in calibrated units. - * - * @return the square distance in calibrated units. + * + * @return the square distance. */ public double getDistanceSquared() { @@ -99,8 +97,8 @@ public double getDistanceSquared() *

    * In spherical coordinates, the inclination is the angle between the Z axis * and the line OM where O is the sphere center and M is the point location. - * - * @return the inclination angle in radians. + * + * @return the inclination. */ public double getTheta() { @@ -117,8 +115,8 @@ public double getTheta() * In spherical coordinates, the azimuth is the angle measured in the plane * XY between the X axis and the line OH where O is the sphere center and H * is the orthogonal projection of the point M on the XY plane. - * - * @return the azimuth angle in radians. + * + * @return the azimuth. */ public double getPhi() { diff --git a/src/main/java/fiji/plugin/trackmate/util/SpotUtil.java b/src/main/java/fiji/plugin/trackmate/util/SpotUtil.java deleted file mode 100644 index d20f762ad..000000000 --- a/src/main/java/fiji/plugin/trackmate/util/SpotUtil.java +++ /dev/null @@ -1,331 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util; - -import java.util.Iterator; - -import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.SpotRoi; -import fiji.plugin.trackmate.detection.DetectionUtils; -import net.imagej.ImgPlus; -import net.imglib2.Cursor; -import net.imglib2.FinalInterval; -import net.imglib2.Interval; -import net.imglib2.IterableInterval; -import net.imglib2.Localizable; -import net.imglib2.RandomAccess; -import net.imglib2.RealLocalizable; -import net.imglib2.type.numeric.RealType; -import net.imglib2.util.Intervals; -import net.imglib2.util.Util; -import net.imglib2.view.IntervalView; -import net.imglib2.view.Views; - -public class SpotUtil -{ - - public static final < T extends RealType< T > > IterableInterval< T > iterable( final SpotRoi roi, final RealLocalizable center, final ImgPlus< T > img ) - { - final SpotRoiIterable< T > neighborhood = new SpotRoiIterable<>( roi, center, img ); - if ( neighborhood.dimension( 0 ) <= 1 && neighborhood.dimension( 1 ) <= 1 ) - return makeSinglePixelIterable( center, img ); - else - return neighborhood; - } - - public static final < T extends RealType< T > > IterableInterval< T > iterable( final Spot spot, final ImgPlus< T > img ) - { - // Prepare neighborhood - final SpotRoi roi = spot.getRoi(); - if ( null != roi && DetectionUtils.is2D( img ) ) - { - // Operate on ROI only if we have one and the image is 2D. - return iterable( roi, spot, img ); - } - else - { - // Otherwise default to circle / sphere. - final SpotNeighborhood< T > neighborhood = new SpotNeighborhood<>( spot, img ); - - final int npixels = ( int ) neighborhood.size(); - if ( npixels <= 1 ) - return makeSinglePixelIterable( spot, img ); - else - return neighborhood; - } - } - - private static < T > IterableInterval< T > makeSinglePixelIterable( final RealLocalizable center, final ImgPlus< T > img ) - { - final double[] calibration = TMUtils.getSpatialCalibration( img ); - final long[] min = new long[ img.numDimensions() ]; - final long[] max = new long[ img.numDimensions() ]; - for ( int d = 0; d < min.length; d++ ) - { - final long cx = Math.round( center.getDoublePosition( d ) / calibration[ d ] ); - min[ d ] = cx; - max[ d ] = cx + 1; - } - - final Interval interval = new FinalInterval( min, max ); - return Views.interval( img, interval ); - } - - private static final class SpotRoiIterable< T extends RealType< T > > implements IterableInterval< T > - { - - private final SpotRoi roi; - - private final RealLocalizable center; - - private final ImgPlus< T > img; - - private final FinalInterval interval; - - public SpotRoiIterable( final SpotRoi roi, final RealLocalizable center, final ImgPlus< T > img ) - { - this.roi = roi; - this.center = center; - this.img = img; - final double[] x = roi.toPolygonX( img.averageScale( 0 ), 0, center.getDoublePosition( 0 ), 1. ); - final double[] y = roi.toPolygonX( img.averageScale( 1 ), 0, center.getDoublePosition( 1 ), 1. ); - final long minX = ( long ) Math.floor( Util.min( x ) ); - final long maxX = ( long ) Math.ceil( Util.max( x ) ); - final long minY = ( long ) Math.floor( Util.min( y ) ); - final long maxY = ( long ) Math.ceil( Util.max( y ) ); - interval = Intervals.createMinMax( minX, minY, maxX, maxY ); - } - - @Override - public long size() - { - int n = 0; - final Cursor< T > cursor = cursor(); - while ( cursor.hasNext() ) - { - cursor.fwd(); - n++; - } - return n; - } - - @Override - public T firstElement() - { - return cursor().next(); - } - - @Override - public Object iterationOrder() - { - return this; - } - - @Override - public double realMin( final int d ) - { - return interval.realMin( d ); - } - - @Override - public double realMax( final int d ) - { - return interval.realMax( d ); - } - - @Override - public int numDimensions() - { - return 2; - } - - @Override - public long min( final int d ) - { - return interval.min( d ); - } - - @Override - public long max( final int d ) - { - return interval.max( d ); - } - - @Override - public Cursor< T > cursor() - { - return new MyCursor< T >( roi, center, img ); - } - - @Override - public Cursor< T > localizingCursor() - { - return cursor(); - } - - @Override - public Iterator< T > iterator() - { - return cursor(); - } - } - - private static final class MyCursor< T extends RealType< T > > implements Cursor< T > - { - - private final SpotRoi roi; - - private final RealLocalizable center; - - private final ImgPlus< T > img; - - private final FinalInterval interval; - - private Cursor< T > cursor; - - private final double[] x; - - private final double[] y; - - private boolean hasNext; - - private RandomAccess< T > ra; - - public MyCursor( final SpotRoi roi, final RealLocalizable center, final ImgPlus< T > img ) - { - this.roi = roi; - this.center = center; - this.img = img; - x = roi.toPolygonX( img.averageScale( 0 ), 0, center.getDoublePosition( 0 ), 1. ); - y = roi.toPolygonY( img.averageScale( 1 ), 0, center.getDoublePosition( 1 ), 1. ); - final long minX = ( long ) Math.floor( Util.min( x ) ); - final long maxX = ( long ) Math.ceil( Util.max( x ) ); - final long minY = ( long ) Math.floor( Util.min( y ) ); - final long maxY = ( long ) Math.ceil( Util.max( y ) ); - interval = Intervals.createMinMax( minX, minY, maxX, maxY ); - reset(); - } - - @Override - public T get() - { - return ra.get(); - } - - @Override - public void fwd() - { - ra.setPosition( cursor ); - fetch(); - } - - private void fetch() - { - while ( cursor.hasNext() ) - { - cursor.fwd(); - if ( isInside( cursor, x, y ) ) - { - hasNext = cursor.hasNext(); - return; - } - } - hasNext = false; - } - - private static final boolean isInside( final Localizable localizable, final double[] x, final double[] y ) - { - // Taken from Imglib2-roi GeomMaths. No edge case. - final double xl = localizable.getDoublePosition( 0 ); - final double yl = localizable.getDoublePosition( 1 ); - - int i; - int j; - boolean inside = false; - for ( i = 0, j = x.length - 1; i < x.length; j = i++ ) - { - final double xj = x[ j ]; - final double yj = y[ j ]; - - final double xi = x[ i ]; - final double yi = y[ i ]; - - if ( ( yi > yl ) != ( yj > yl ) && ( xl < ( xj - xi ) * ( yl - yi ) / ( yj - yi ) + xi ) ) - inside = !inside; - } - return inside; - } - - @Override - public void reset() - { - final IntervalView< T > view = Views.interval( img, interval ); - cursor = view.localizingCursor(); - ra = Views.extendMirrorSingle( img ).randomAccess(); - fetch(); - } - - @Override - public double getDoublePosition( final int d ) - { - return ra.getDoublePosition( d ); - } - - @Override - public int numDimensions() - { - return 2; - } - - @Override - public void jumpFwd( final long steps ) - { - for ( int i = 0; i < steps; i++ ) - fwd(); - } - - @Override - public boolean hasNext() - { - return hasNext; - } - - @Override - public T next() - { - fwd(); - return get(); - } - - @Override - public long getLongPosition( final int d ) - { - return ra.getLongPosition( d ); - } - - @Override - public Cursor< T > copy() - { - return new MyCursor<>( roi, center, img ); - } - } -} diff --git a/src/main/java/fiji/plugin/trackmate/util/TMUtils.java b/src/main/java/fiji/plugin/trackmate/util/TMUtils.java index 89b28aaf9..477281160 100644 --- a/src/main/java/fiji/plugin/trackmate/util/TMUtils.java +++ b/src/main/java/fiji/plugin/trackmate/util/TMUtils.java @@ -2,18 +2,18 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * 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 * . @@ -42,9 +42,9 @@ 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 fiji.plugin.trackmate.detection.DetectionUtils; import ij.IJ; import ij.ImagePlus; @@ -57,11 +57,10 @@ import net.imglib2.img.ImagePlusAdapter; import net.imglib2.img.display.imagej.ImgPlusViews; import net.imglib2.type.Type; -import net.imglib2.type.numeric.real.DoubleType; import net.imglib2.util.Util; /** - * List of static utilities for {@link fiji.plugin.trackmate.TrackMate}. + * List of static utilities for TrackMate. */ public class TMUtils { @@ -111,19 +110,16 @@ public static Interval createROIInterval( final ImagePlus imp ) /** * Returns a new map sorted by its values. - *

    - * The returned map is a {@link LinkedHashMap}, which preserves the - * ordering. * - * @param map - * the map to sort. - * @param comparator - * the comparator to use to sort the values. - * @return a new map sorted by its values. * @param - * the key type. + * the type of keys in the map. * @param - * the value type. + * the type of values in the map. + * @param map + * the map. + * @param comparator + * a comparator to sort based on values. + * @return a new map, with entries sorted by values. */ public static < K, V extends Comparable< ? super V > > Map< K, V > sortByValue( final Map< K, V > map, final Comparator< V > comparator ) { @@ -148,14 +144,12 @@ public int compare( final Entry< K, V > o1, final Entry< K, V > o2 ) /** * Generates a string representation of a map, typically a settings map. - *

    - * This method is recursive, and will indent sub-maps. * * @param map - * the map to represent as a string. + * the map. * @param indent - * the indentation level. - * @return a string representation of the map. + * the indent size to use. + * @return a representation of the map. */ public static final String echoMap( final Map< String, Object > map, final int indent ) { @@ -192,24 +186,23 @@ else if ( obj instanceof Logger ) } /** - * 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. + * Wraps an IJ {@link ImagePlus} in an imglib2 {@link ImgPlus}, abiding to a + * returned type. * + * @param + * the pixel type in the returned image. * @param imp - * the image plus to wrap. - * @return the ImgPlus wrapping the input. + * the {@link ImagePlus} to wrap. + * @return a wrapped {@link ImgPlus}. */ - @SuppressWarnings( "rawtypes" ) - public static final ImgPlus rawWraps( final ImagePlus imp ) + @SuppressWarnings( "unchecked" ) + public static final < T > ImgPlus< T > rawWraps( final ImagePlus imp ) { - final ImgPlus< DoubleType > img = ImagePlusAdapter.wrapImgPlus( imp ); - final ImgPlus raw = img; - return raw; + return ( ImgPlus< T > ) ImagePlusAdapter.wrapImgPlus( imp ); } /** - * Checks that the given map has all some keys. Two String collection allows + * Check that the given map has all some keys. Two String collection allows * specifying that some keys are mandatory, other are optional. * * @param map @@ -222,10 +215,10 @@ public static final ImgPlus rawWraps( final ImagePlus imp ) * be null. * @param errorHolder * will be appended with an error message. - * @param - * the type of the keys. * @return if all mandatory keys are found in the map, and possibly some * optional ones, but no others. + * @param + * the type of keys. */ public static final < T > boolean checkMapKeys( final Map< T, ? > map, Collection< T > mandatoryKeys, Collection< T > optionalKeys, final StringBuilder errorHolder ) { @@ -288,8 +281,39 @@ public static final String checkSettings( final Map< String, Object > settings, } /** - * Check the presence and the validity of a key in a map, and test it is of - * the desired class. + * Check the optional presence and the validity of a key in a map, and test + * it is of the desired class. If the key is not present, this method + * returns true. If it is present, it tests the value is of the + * right 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 not found in the map, or if it is found, and + * map a value of the desired class. + */ + public static final boolean checkOptionalParameter( final Map< String, Object > map, final String key, final Class< ? > expectedClass, final StringBuilder errorHolder ) + { + final Object obj = map.get( key ); + if ( null == obj ) + return true; + + 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; + } + + /** + * Check the mandatory presence and the validity of a key in a map, and test + * its value is of the desired class. * * @param map * the map to inspect. @@ -310,28 +334,22 @@ public static final boolean checkParameter( final Map< String, Object > map, fin 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; + return checkOptionalParameter( map, key, expectedClass, errorHolder ); } /** * Returns the mapping in a map that is targeted by a list of keys, in the - * order given by iterating over the key collection. + * order given in the list. * + * @param + * the type of keys in the collection and the map. + * @param + * the type of values in the map. * @param keys - * the keys. + * the collection of keys. * @param mapping * the mapping. - * @param - * the key type. - * @param - * the value type. - * @return the list of values mapped to the keys. - * + * @return a new list of values. */ public static final < J, K > List< K > getArrayFromMaping( final Collection< J > keys, final Map< J, K > mapping ) { @@ -351,9 +369,8 @@ public static final < J, K > List< K > getArrayFromMaping( final Collection< J > * is not found, then the calibration for this axis takes the value of 1. * * @param img - * the image plus metadata. - * @return a 3-elements double array with the spatial calibration in X, Y, - * Z. + * the image metadata object. + * @return a new double array. */ public static final double[] getSpatialCalibration( final ImgPlusMetadata img ) { @@ -387,14 +404,13 @@ public static double[] getSpatialCalibration( final ImagePlus imp ) * the values array. Taken from commons-math. * * @param values - * the values to compute the percentile from. + * the values. * @param p - * the percentile to compute, between 0 and 1. - * @return the pth percentile. + * the percentile. + * @return the percentile of the values. */ 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 ); @@ -437,6 +453,22 @@ private static final double[] getRange( final double[] data ) return new double[] { ( max - min ), min, max }; } + /** + * Stores the x, y, z coordinates of the specified spot in the first 3 + * elements of the specified double array. + * + * @param spot + * the spot. + * @param coords + * the array to write coordinates to. + */ + 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(); + } + /** * Returns 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)). @@ -506,8 +538,8 @@ private static final int[] histogram( final double data[], final int nBins ) * thresholding method. * * @param data - * the data to threshold. - * @return the threshold value. + * the data. + * @return the Otsu threshold. */ public static final double otsuThreshold( final double[] data ) { @@ -519,10 +551,10 @@ public static final double otsuThreshold( final double[] data ) * thresholding method with a given bin number. * * @param data - * the data to threshold. - * @param nBins - * the number of bins to use for the histogram. - * @return the threshold value. + * the data. + * @param the + * desired number of bins in the histogram. + * @return the Otsu thresold. */ private static final double otsuThreshold( final double[] data, final int nBins ) { @@ -588,53 +620,7 @@ private static final int otsuThresholdIndex( final int[] hist, final int nPoints return threshold; } - /** - * Returns 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. - * - * @param dimension - * the dimension to get the unit for. - * @param spaceUnits - * the spatial units to use for space-related dimensions. - * @param timeUnits - * the time units to use for time-related dimensions. - * @return a String representing the unit for the given dimension. - */ - 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() { @@ -840,9 +826,9 @@ public static final Interval getInterval( final ImgPlus< ? > img, final Settings } /** - * Obtains and cache the SciJava {@link Context} in use by ImageJ. + * Obtains the SciJava {@link Context} in use by ImageJ. * - * @return the SciJava context + * @return the context. */ public static Context getContext() { @@ -981,8 +967,7 @@ public static double standardDeviation( final DoubleArray data ) * full path * * @param settings - * the settings object from which to read the image folder and - * image file name. + * A {@link Settings} object referencing the image * @return full name of the image without the extension */ public static String getImagePathWithoutExtension( final Settings settings ) diff --git a/src/main/java/fiji/plugin/trackmate/util/TrackNavigator.java b/src/main/java/fiji/plugin/trackmate/util/TrackNavigator.java index f6b88d6be..72fb323a6 100644 --- a/src/main/java/fiji/plugin/trackmate/util/TrackNavigator.java +++ b/src/main/java/fiji/plugin/trackmate/util/TrackNavigator.java @@ -21,52 +21,60 @@ */ package fiji.plugin.trackmate.util; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.graph.TimeDirectedNeighborIndex; - import java.util.Iterator; import java.util.Set; import java.util.TreeSet; import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.traverse.GraphIterator; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.graph.TimeDirectedNeighborIndex; -public class TrackNavigator { +public class TrackNavigator +{ private final Model model; + private final SelectionModel selectionModel; + private final TimeDirectedNeighborIndex neighborIndex; - public TrackNavigator(final Model model, final SelectionModel selectionModel) { + public TrackNavigator( final Model model, final SelectionModel selectionModel ) + { this.model = model; this.selectionModel = selectionModel; this.neighborIndex = model.getTrackModel().getDirectedNeighborIndex(); } - public synchronized void nextTrack() { + public synchronized void nextTrack() + { final Spot spot = getASpot(); - if (null == spot) { + if ( null == spot ) return; - } - final Set trackIDs = model.getTrackModel().trackIDs(true); // if only it was navigable... - if (trackIDs.isEmpty()) { + final Set< Integer > trackIDs = model.getTrackModel().trackIDs( true ); + if ( trackIDs.isEmpty() ) return; - } - Integer trackID = model.getTrackModel().trackIDOf(spot); - if (null == trackID) { + Integer trackID = model.getTrackModel().trackIDOf( spot ); + if ( null == trackID ) + { // No track? Then move to the first one. - trackID = model.getTrackModel().trackIDs(true).iterator().next(); + trackID = model.getTrackModel().trackIDs( true ).iterator().next(); } - final Iterator it = trackIDs.iterator(); + final Iterator< Integer > it = trackIDs.iterator(); Integer nextTrackID = null; - while (it.hasNext()) { + while ( it.hasNext() ) + { final Integer id = it.next(); - if (id.equals(trackID)) { - if (it.hasNext()) { + if ( id.equals( trackID ) ) + { + if ( it.hasNext() ) + { nextTrackID = it.next(); break; } @@ -74,155 +82,202 @@ public synchronized void nextTrack() { } } - final Set spots = model.getTrackModel().trackSpots(nextTrackID); - final TreeSet ring = new TreeSet<>(Spot.frameComparator); - ring.addAll(spots); - Spot target = ring.ceiling(spot); - if (null == target) { - target = ring.floor(spot); - } + final Set< Spot > spots = model.getTrackModel().trackSpots( nextTrackID ); + final TreeSet< Spot > ring = new TreeSet<>( Spot.frameComparator ); + ring.addAll( spots ); + Spot target = ring.ceiling( spot ); + if ( null == target ) + target = ring.floor( spot ); selectionModel.clearSelection(); - selectionModel.addSpotToSelection(target); + selectionModel.addSpotToSelection( target ); } - public synchronized void previousTrack() { + public synchronized void previousTrack() + { final Spot spot = getASpot(); - if (null == spot) { + if ( null == spot ) return; - } - Integer trackID = model.getTrackModel().trackIDOf(spot); - final Set trackIDs = model.getTrackModel().trackIDs(true); // if only it was navigable... - if (trackIDs.isEmpty()) { + Integer trackID = model.getTrackModel().trackIDOf( spot ); + final Set< Integer > trackIDs = model.getTrackModel().trackIDs( true ); + if ( trackIDs.isEmpty() ) return; - } Integer lastID = null; - for (final Integer id : trackIDs) { + for ( final Integer id : trackIDs ) lastID = id; - } - if (null == trackID) { + if ( null == trackID ) + { // No track? Then take the last one. trackID = lastID; } - final Iterator it = trackIDs.iterator(); + final Iterator< Integer > it = trackIDs.iterator(); Integer previousTrackID = null; - while (it.hasNext()) { + while ( it.hasNext() ) + { final Integer id = it.next(); - if (id.equals(trackID)) { - if (previousTrackID != null) { + if ( id.equals( trackID ) ) + { + if ( previousTrackID != null ) break; - } + previousTrackID = lastID; break; } previousTrackID = id; } - final Set spots = model.getTrackModel().trackSpots(previousTrackID); - final TreeSet ring = new TreeSet<>(Spot.frameComparator); - ring.addAll(spots); - Spot target = ring.ceiling(spot); - if (null == target) { - target = ring.floor(spot); - } + final Set< Spot > spots = model.getTrackModel().trackSpots( previousTrackID ); + final TreeSet< Spot > ring = new TreeSet<>( Spot.frameComparator ); + ring.addAll( spots ); + Spot target = ring.ceiling( spot ); + if ( null == target ) + target = ring.floor( spot ); selectionModel.clearSelection(); - selectionModel.addSpotToSelection(target); + selectionModel.addSpotToSelection( target ); } - public synchronized void nextSibling() { + public synchronized void nextSibling() + { final Spot spot = getASpot(); - if (null == spot) { + if ( null == spot ) return; - } - final Integer trackID = model.getTrackModel().trackIDOf(spot); - if (null == trackID) { + final Integer trackID = model.getTrackModel().trackIDOf( spot ); + if ( null == trackID ) return; - } - final int frame = spot.getFeature(Spot.FRAME).intValue(); - final TreeSet ring = new TreeSet<>(Spot.nameComparator); + final int frame = spot.getFeature( Spot.FRAME ).intValue(); + final TreeSet< Spot > ring = new TreeSet<>( Spot.nameComparator ); - final Set spots = model.getTrackModel().trackSpots(trackID); - for (final Spot s : spots) { - final int fs = s.getFeature(Spot.FRAME).intValue(); - if (frame == fs && s != spot) { - ring.add(s); - } + final Set< Spot > spots = model.getTrackModel().trackSpots( trackID ); + for ( final Spot s : spots ) + { + final int fs = s.getFeature( Spot.FRAME ).intValue(); + if ( frame == fs && s != spot ) + ring.add( s ); } - if (!ring.isEmpty()) { - Spot nextSibling = ring.ceiling(spot); - if (null == nextSibling) { + if ( !ring.isEmpty() ) + { + Spot nextSibling = ring.ceiling( spot ); + if ( null == nextSibling ) nextSibling = ring.first(); // loop - } + selectionModel.clearSelection(); - selectionModel.addSpotToSelection(nextSibling); + selectionModel.addSpotToSelection( nextSibling ); } } - public synchronized void previousSibling() { + public synchronized void previousSibling() + { final Spot spot = getASpot(); - if (null == spot) { + if ( null == spot ) return; - } - final Integer trackID = model.getTrackModel().trackIDOf(spot); - if (null == trackID) { + final Integer trackID = model.getTrackModel().trackIDOf( spot ); + if ( null == trackID ) return; - } - final int frame = spot.getFeature(Spot.FRAME).intValue(); - final TreeSet ring = new TreeSet<>(Spot.nameComparator); + final int frame = spot.getFeature( Spot.FRAME ).intValue(); + final TreeSet< Spot > ring = new TreeSet<>( Spot.nameComparator ); - final Set spots = model.getTrackModel().trackSpots(trackID); - for (final Spot s : spots) { - final int fs = s.getFeature(Spot.FRAME).intValue(); - if (frame == fs && s != spot) { - ring.add(s); - } + final Set< Spot > spots = model.getTrackModel().trackSpots( trackID ); + for ( final Spot s : spots ) + { + final int fs = s.getFeature( Spot.FRAME ).intValue(); + if ( frame == fs && s != spot ) + ring.add( s ); } - if (!ring.isEmpty()) { - Spot previousSibling = ring.floor(spot); - if (null == previousSibling) { + if ( !ring.isEmpty() ) + { + Spot previousSibling = ring.floor( spot ); + if ( null == previousSibling ) previousSibling = ring.last(); // loop - } + selectionModel.clearSelection(); - selectionModel.addSpotToSelection(previousSibling); + selectionModel.addSpotToSelection( previousSibling ); } } - public synchronized void previousInTime() { + public synchronized void previousInTime() + { final Spot spot = getASpot(); - if (null == spot) { + if ( null == spot ) return; - } - final Set predecessors = neighborIndex.predecessorsOf(spot); - if (!predecessors.isEmpty()) { + final Set< Spot > predecessors = neighborIndex.predecessorsOf( spot ); + if ( !predecessors.isEmpty() ) + { final Spot next = predecessors.iterator().next(); selectionModel.clearSelection(); - selectionModel.addSpotToSelection(next); + selectionModel.addSpotToSelection( next ); } } - public synchronized void nextInTime() { + public synchronized void nextInTime() + { final Spot spot = getASpot(); - if (null == spot) { + if ( null == spot ) return; - } - final Set successors = neighborIndex.successorsOf(spot); - if (!successors.isEmpty()) { + final Set< Spot > successors = neighborIndex.successorsOf( spot ); + if ( !successors.isEmpty() ) + { final Spot next = successors.iterator().next(); selectionModel.clearSelection(); - selectionModel.addSpotToSelection(next); + selectionModel.addSpotToSelection( next ); + } + } + + public synchronized void root() + { + final Spot spot = getASpot(); + if ( null == spot ) + return; + + final GraphIterator< Spot, DefaultWeightedEdge > it = model.getTrackModel().getDirectedDepthFirstIterator( spot, true ); + NEXT_SPOT: while ( it.hasNext() ) + { + final Spot next = it.next(); + final Set< DefaultWeightedEdge > edges = model.getTrackModel().edgesOf( next ); + for ( final DefaultWeightedEdge edge : edges ) + { + if ( model.getTrackModel().getEdgeTarget( edge ).equals( next ) ) + continue NEXT_SPOT; + } + + selectionModel.clearSelection(); + selectionModel.addSpotToSelection( next ); + return; + } + } + + public synchronized void leaf() + { + final Spot spot = getASpot(); + if ( null == spot ) + return; + + final GraphIterator< Spot, DefaultWeightedEdge > it = model.getTrackModel().getDirectedDepthFirstIterator( spot, false ); + NEXT_SPOT: while ( it.hasNext() ) + { + final Spot next = it.next(); + final Set< DefaultWeightedEdge > edges = model.getTrackModel().edgesOf( next ); + for ( final DefaultWeightedEdge edge : edges ) + { + if ( model.getTrackModel().getEdgeSource( edge ).equals( next ) ) + continue NEXT_SPOT; + } + + selectionModel.clearSelection(); + selectionModel.addSpotToSelection( next ); + return; } } @@ -234,17 +289,21 @@ public synchronized void nextInTime() { * Return a meaningful spot from the current selection, or null * if the selection is empty. */ - private Spot getASpot() { + private Spot getASpot() + { // Get it from spot selection - final Set spotSelection = selectionModel.getSpotSelection(); - if (!spotSelection.isEmpty()) { - final Iterator it = spotSelection.iterator(); + final Set< Spot > spotSelection = selectionModel.getSpotSelection(); + if ( !spotSelection.isEmpty() ) + { + final Iterator< Spot > it = spotSelection.iterator(); Spot spot = it.next(); - int minFrame = spot.getFeature(Spot.FRAME).intValue(); - while (it.hasNext()) { + int minFrame = spot.getFeature( Spot.FRAME ).intValue(); + while ( it.hasNext() ) + { final Spot s = it.next(); - final int frame = s.getFeature(Spot.FRAME).intValue(); - if (frame < minFrame) { + final int frame = s.getFeature( Spot.FRAME ).intValue(); + if ( frame < minFrame ) + { minFrame = frame; spot = s; } @@ -253,17 +312,20 @@ private Spot getASpot() { } // Nope? Then get it from edges - final Set edgeSelection = selectionModel.getEdgeSelection(); - if (!edgeSelection.isEmpty()) { - final Iterator it = edgeSelection.iterator(); + final Set< DefaultWeightedEdge > edgeSelection = selectionModel.getEdgeSelection(); + if ( !edgeSelection.isEmpty() ) + { + final Iterator< DefaultWeightedEdge > it = edgeSelection.iterator(); final DefaultWeightedEdge edge = it.next(); - Spot spot = model.getTrackModel().getEdgeSource(edge); - int minFrame = spot.getFeature(Spot.FRAME).intValue(); - while (it.hasNext()) { + Spot spot = model.getTrackModel().getEdgeSource( edge ); + int minFrame = spot.getFeature( Spot.FRAME ).intValue(); + while ( it.hasNext() ) + { final DefaultWeightedEdge e = it.next(); - final Spot s = model.getTrackModel().getEdgeSource(e); - final int frame = s.getFeature(Spot.FRAME).intValue(); - if (frame < minFrame) { + final Spot s = model.getTrackModel().getEdgeSource( e ); + final int frame = s.getFeature( Spot.FRAME ).intValue(); + if ( frame < minFrame ) + { minFrame = frame; spot = s; } diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java b/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java deleted file mode 100644 index 3078c071c..000000000 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java +++ /dev/null @@ -1,70 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util.cli; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -/** - * Extends the {@link Configurator} for tools that can be run from the command - * line. - * - * @author Jean-Yves Tinevez - */ -public abstract class CLIConfigurator extends Configurator -{ - - /* - * COMMAND LINE TRANSLATOR. - */ - - protected final Map< Argument< ?, ? >, Function< Object, List< String > > > cliTranslators = new HashMap<>(); - - /** - * Decorates the specified argument with a translator, that will modify the - * value of the argument in the command line output. Because we focus on the - * command line output, the translator returns a list of tokens. - *

    - * This can be used for instance to deal with a diameter expressed in µm - * everywhere throughout TrackMate, then translate it to a pixel value in - * the command line output. - * - * @param arg - * the argument to translate. - * @param translator - * a function that takes the value of the argument and returns a - * list of strings to be used in the command line output. - */ - protected void setCommandTranslator( final Argument< ?, ? > arg, final Function< Object, List< String > > translator ) - { - cliTranslators.put( arg, translator ); - } - - /** - * Returns the command object of this command line tool. - * - * @return the command object, as an argument. - */ - public abstract Argument< ?, ? > getCommandArg(); -} diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CLIUtils.java b/src/main/java/fiji/plugin/trackmate/util/cli/CLIUtils.java deleted file mode 100644 index 37a6a27d5..000000000 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CLIUtils.java +++ /dev/null @@ -1,629 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util.cli; - -import java.awt.Color; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.apache.commons.io.input.Tailer; -import org.apache.commons.io.input.TailerListenerAdapter; -import org.scijava.prefs.PrefService; - -import fiji.plugin.trackmate.Logger; -import fiji.plugin.trackmate.util.TMUtils; -import ij.IJ; - -public class CLIUtils -{ - - public static final String CONDA_PATH_PREF_KEY = "trackmate.conda.path"; - - public static final String CONDA_ROOT_PREFIX_KEY = "trackmate.conda.root.prefix"; - - private static Map< String, String > envMap; - - /** - * Creates and start a process that runs the command specified in the CLI. - * - * @param cli - * the CLI configurator that specifies the command to run. - * @param logFile - * the file to which the process output is appended. - * @return the process that runs the command specified in the CLI. - * @throws IOException - * if there is an error creating the process. - */ - public static final Process createProcess( final CLIConfigurator cli, final File logFile ) throws IOException - { - final List< String > cmd = CommandBuilder.build( cli ); - final ProcessBuilder pb = new ProcessBuilder( cmd ); - if ( cli instanceof CondaCLIConfigurator ) - { - // Env variables. - final Map< String, String > env = new HashMap<>(); - final String condaRootPrefix = getCondaRootPrefix(); - env.put( "MAMBA_ROOT_PREFIX", condaRootPrefix ); - env.put( "CONDA_ROOT_PREFIX", condaRootPrefix ); - pb.environment().putAll( env ); - } - pb.redirectOutput( ProcessBuilder.Redirect.appendTo( logFile ) ); - pb.redirectError( ProcessBuilder.Redirect.appendTo( logFile ) ); - return pb.start(); - } - - /** - * Creates and starts a process that runs the command specified in the CLI, - * and redirects the process output to the logger. - *

    - * This method handles the process output by appending it to a log file and - * redirecting it to the logger. It also catches exceptions when launching - * the process and redirect errors to the logger. - * - * @param cli - * the CLI configurator that specifies the command to run. - * @param logger - * the logger to which the process output is redirected. - * @param logFile - * the file to which the process output is appended. - * @return the process that runs the command specified in the CLI, or - * null if there was an error creating the process. - */ - public static final Process createAndHandleProcess( final CLIConfigurator cli, final Logger logger, final File logFile ) - { - // Appends process output to the log file, and redirects to the logger. - final Tailer tailer = Tailer.builder() - .setFile( logFile ) - .setTailerListener( new LoggerTailerListener( logger ) ) - .setDelayDuration( Duration.ofMillis( 200 ) ) - .setTailFromEnd( true ) - .get(); - - final String executableName = cli.getClass().getSimpleName(); - try - { - final List< String > cmd = CommandBuilder.build( cli ); - logger.setStatus( "Running " + executableName ); - logger.log( "Running " + executableName + " with args:\n" ); - cmd.forEach( t -> { - if ( t.contains( File.separator ) ) - logger.log( t + ' ' ); - else - logger.log( t + ' ', Logger.GREEN_COLOR.darker() ); - } ); - logger.log( "\n" ); - - final Process process = createProcess( cli, logFile ); - return process; - } - catch ( final IOException e ) - { - final String msg = e.getMessage(); - String errorMessage; - if ( msg.matches( ".+error=13.+" ) ) - { - errorMessage = "Problem running " + executableName + ":\n" - + "The executable does not have the file permission to run.\n"; - } - else - { - errorMessage = "Problem running " + executableName + ":\n" + e.getMessage(); - } - try - { - errorMessage = errorMessage + '\n' + new String( Files.readAllBytes( logFile.toPath() ) ); - } - catch ( final IOException e1 ) - {} - e.printStackTrace(); - logger.error( errorMessage ); - } - catch ( final Exception e ) - { - String errorMessage = "Problem running " + executableName + ":\n" + e.getMessage(); - try - { - errorMessage = errorMessage + '\n' + new String( Files.readAllBytes( logFile.toPath() ) ); - } - catch ( final IOException e1 ) - {} - e.printStackTrace(); - logger.error( errorMessage ); - } - finally - { - tailer.close(); - } - return null; - } - - /** - * Creates and executes the process that runs the command specified in the - * CLI. - * - * @param cli - * the CLI configurator that specifies the command to run. - * @param logger - * the logger to which the process output is redirected. - * @param logFile - * the file to which the process output is appended. - * @return true if the process exited with code 0, - * false otherwise. - */ - public static boolean execute( final CLIConfigurator cli, final Logger logger, final File logFile ) - { - - final Process process = createAndHandleProcess( cli, logger, logFile ); - if ( process == null ) - return false; - - try - { - final int returnValue = process.waitFor(); - return returnValue == 0; - } - catch ( final InterruptedException e ) - { - logger.error( "Process interrupted: " + e.getMessage() ); - e.printStackTrace(); - } - return false; - } - - /** - * Returns the version of a Python module. - * - * The version is returned as a string, as if the command - * - *

    -	 * python -c import moduleName;
    -	 * print(moduleName.__version__)
    -	 * 
    - * - * was run. - * - * @param envName - * the name of the conda environment in which the module is - * installed - * @param moduleName - * the name of the module - * @return the version of the module as a string, or null if - * the module name does nor exist or if the conda environment does - * not exist. - */ - public static String getModuleVersion( final String envName, final String moduleName ) - { - // We protect the space between 'import' and the command with a _ - final String cmd = "" - + "python -c import_" + moduleName + ";" - + "print(" + moduleName + ".__version__)"; - final List< String > tokens = preparePythonCommand( envName, cmd ); - if ( tokens.isEmpty() ) - return null; - - // Put back the space in the token. - final ListIterator< String > it = tokens.listIterator(); - while ( it.hasNext() ) - { - final String token = it.next(); - it.set( token.replace( "t_", "t " ) ); - } - final ProcessBuilder pb = new ProcessBuilder( tokens ); - // Env variables. - final Map< String, String > env = new HashMap<>(); - final String condaRootPrefix = getCondaRootPrefix(); - env.put( "MAMBA_ROOT_PREFIX", condaRootPrefix ); - env.put( "CONDA_ROOT_PREFIX", condaRootPrefix ); - pb.environment().putAll( env ); - pb.redirectErrorStream( true ); - - try - { - final Process process = pb.start(); - final BufferedReader reader = new BufferedReader( - new InputStreamReader( process.getInputStream() ) ); - - // Return the last line of output. - String line; - String prevLine = null; - final StringBuffer errorMsg = new StringBuffer(); - while ( ( line = reader.readLine() ) != null ) - { - prevLine = line; - errorMsg.append( '\n' + line ); - } - - final int exitCode = process.waitFor(); - if ( exitCode == 0 ) - return prevLine; - else - throw new Exception( "Error running the command '" + moduleName - + "' in environment '" + envName + "'" + errorMsg ); - } - catch ( final Exception e ) - { - e.printStackTrace(); - return null; - } - } - - /** - * Generates the list of tokens to be used with {@link ProcessBuilder} to - * run a Python command. - * - * @param envName - * the name of the conda environment in which the command is - * installed. - * @param cmdName - * the name of the command to run. - * @return a list of tokens to use with {@link ProcessBuilder}. The list is - * empty if there is an error with the conda environment or with the - * command. - */ - public static List< String > preparePythonCommand( final String envName, final String cmdName ) - { - final List< String > cmd = new ArrayList<>(); - // Conda and executable stuff. - try - { - final String pythonPath = CLIUtils.getEnvMap().get( envName ); - if ( pythonPath == null ) - throw new Exception( "Unknown conda environment: " + envName ); - - final int i = pythonPath.lastIndexOf( "python" ); - final String binPath = pythonPath.substring( 0, i ); - final String executablePath = binPath + cmdName; - final String[] split = executablePath.split( " " ); - cmd.addAll( Arrays.asList( split ) ); - return cmd; - } - catch ( final IOException e ) - { - System.err.println( "Could not find the conda executable or change the conda environment.\n" - + "Please configure the path to your conda executable in Edit > Options > Configure TrackMate Conda path..." ); - e.printStackTrace(); - } - catch ( final Exception e ) - { - System.err.println( "Error running the conda executable:" ); - e.printStackTrace(); - } - return cmd; - } - - public static void clearEnvMap() - { - envMap = null; - } - - public static Map< String, String > getEnvMap() throws IOException - { - if ( envMap == null ) - { - synchronized ( CLIUtils.class ) - { - if ( envMap == null ) - { - // Prepare the command and environment variables. - // Command - final ProcessBuilder pb = new ProcessBuilder( Arrays.asList( getCondaPath(), "env", "list" ) ); - // Env variables. - final Map< String, String > env = new HashMap<>(); - final String condaRootPrefix = getCondaRootPrefix(); - env.put( "MAMBA_ROOT_PREFIX", condaRootPrefix ); - env.put( "CONDA_ROOT_PREFIX", condaRootPrefix ); - pb.environment().putAll( env ); - // Run and collect output. - final Process process = pb.start(); - final BufferedReader stdOutput = new BufferedReader( new InputStreamReader( process.getInputStream() ) ); - final BufferedReader stdError = new BufferedReader( new InputStreamReader( process.getErrorStream() ) ); - - /* - * Did we have an error? Read the error from the command - */ - String s; - String errorOutput = ""; - while ( ( s = stdError.readLine() ) != null ) - errorOutput += ( s + '\n' ); - if ( !errorOutput.isEmpty() ) - throw new IOException( "Could not retrieve environment map properly:\n" + errorOutput ); - - String line; - envMap = new HashMap<>(); - while ( ( line = stdOutput.readLine() ) != null ) - { - line = line.trim(); - line = line.replaceAll( "\\*", "" ); - if ( line.isEmpty() || line.startsWith( "#" ) || line.startsWith( "Name" ) || line.startsWith( "──────" ) ) - continue; - - final String[] parts = line.split( "\\s+" ); - if ( parts.length >= 2 ) - { - final String envName = parts[ 0 ]; - final String envPath = parts[ 1 ] + "/bin/python"; - envMap.put( envName, envPath ); - } - else if ( parts.length == 1 ) - { - /* - * When we don't have the right configuration, - * sometimes the list returns the path to the envs - * but not the name. We try then to extract the name - * from the path. - */ - - final String envRoot = parts[ 0 ]; - if ( !isValidPath( envRoot ) ) - { - continue; - } - final Path path = Paths.get( envRoot ); - final String envName = path.getFileName().toString(); - final String envPath = envRoot + "/bin/python"; - envMap.put( envName, envPath ); - } - } - } - } - } - return envMap; - } - - public static List< String > getEnvList() throws IOException - { - final List< String > l = new ArrayList<>( getEnvMap().keySet() ); - l.sort( null ); - return l; - } - - public static String getCondaPath() - { - final PrefService prefs = TMUtils.getContext().getService( PrefService.class ); - String findPath; - try - { - findPath = CLIUtils.findDefaultCondaPath(); - } - catch ( final IllegalArgumentException e ) - { - findPath = "/usr/local/opt/micromamba/bin/micromamba"; - } - return prefs.get( CLIUtils.class, CLIUtils.CONDA_PATH_PREF_KEY, findPath ); - } - - public static String getCondaRootPrefix() - { - final PrefService prefs = TMUtils.getContext().getService( PrefService.class ); - final String findPath = "/usr/local/opt/micromamba"; - return prefs.get( CLIUtils.class, CLIUtils.CONDA_ROOT_PREFIX_KEY, findPath ); - } - - public static String findDefaultCondaPath() throws IllegalArgumentException - { - final String username = System.getProperty( "user.name" ); - final String prefix = IJ.isMacOSX() - ? "/Users/" - : "/home/"; - final String anaconda1 = prefix + username + "/anaconda3/bin/conda"; - final String anaconda2 = "/opt/anaconda3/bin/conda"; - final String miniconda1 = prefix + username + "/miniconda3/bin/conda"; - final String miniconda2 = "/opt/miniconda3/bin/conda"; - final String mamba1 = prefix + username + "/mamba/bin/mamba"; - final String mamba2 = "/opt/mamba/bin/mamba"; - final String micromamba1 = prefix + username + ( IJ.isMacOSX() - ? "/Library/micromamba/bin/micromamba" - : "/.local/share/micromamba/bin/micromamba" ); - final String micromamba2 = "/usr/local/micromamba/bin/micromamba"; - final String micromamba3 = "/usr/local/opt/micromamba/bin/micromamba"; - final String micromamba4 = "/opt/micromamba/bin/micromamba"; - final String micromamba5 = prefix + username + "/mambaforge/condabin/mamba"; - final String[] toTest = new String[] { - anaconda1, - anaconda2, - miniconda1, - miniconda2, - mamba1, - mamba2, - micromamba1, - micromamba2, - micromamba3, - micromamba4, - micromamba5 - }; - for ( final String str : toTest ) - { - final Path path = Paths.get( str ); - if ( Files.isExecutable( path ) ) - return str; - } - throw new IllegalArgumentException( "Could not find a conda executable I know of, within: " + Arrays.asList( toTest ) ); - } - - /** - * Add a hook to delete the content of given path when Fiji quits. Taken - * from https://stackoverflow.com/a/20280989/201698 - * - * @param path - * the path to delete recursively on shutdown. - */ - public static void recursiveDeleteOnShutdownHook( final Path path ) - { - Runtime.getRuntime().addShutdownHook( new Thread( new Runnable() - { - @Override - public void run() - { - try - { - Files.walkFileTree( path, new SimpleFileVisitor< Path >() - { - @Override - public FileVisitResult visitFile( final Path file, final BasicFileAttributes attrs ) throws IOException - { - Files.delete( file ); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory( final Path dir, final IOException e ) throws IOException - { - if ( e == null ) - { - Files.delete( dir ); - return FileVisitResult.CONTINUE; - } - throw e; - } - } ); - } - catch ( final IOException e ) - { - throw new RuntimeException( "Failed to delete " + path, e ); - } - } - } ) ); - } - - public static class LoggerTailerListener extends TailerListenerAdapter - { - - protected final Logger logger; - - public Color COLOR = Logger.BLUE_COLOR.darker(); - - private final static Pattern PERCENTAGE_PATTERN = Pattern.compile( ".+\\D(\\d+(?:\\.\\d+)?)%.+" ); - - private final static Pattern INFO_PATTERN = Pattern.compile( "(.+\\[INFO\\]\\s+(.+)|^INFO:.*$)" ); - - public LoggerTailerListener( final Logger logger ) - { - this.logger = logger; - } - - @Override - public void handle( final String rawLine ) - { - final String line = cleanLine( rawLine ); - - // Do we have percentage? - final Matcher matcher = PERCENTAGE_PATTERN.matcher( line ); - if ( matcher.matches() ) - { - final String percent = matcher.group( 1 ); - logger.setProgress( Double.valueOf( percent ) / 100. ); - } - else - { - final Matcher matcher2 = INFO_PATTERN.matcher( line ); - if ( matcher2.matches() ) - { - final String str = matcher2.group( 1 ).trim(); - if ( str.length() > 2 ) - logger.setStatus( str - .replaceAll( "\\[INFO\\]", "" ) - .replaceAll( "INFO:", "" ) - .replaceAll( "INFO", "" ) ); - } - else if ( !line.trim().isEmpty() ) - { - logger.log( " - " + line + '\n', COLOR ); - } - } - } - - protected String cleanLine( final String line ) - { - // Remove ANSI escape sequences - String cleaned = line.replaceAll( "\u001B\\[[;\\d]*[A-Za-z]", "" ); - // Remove carriage returns and other control characters - cleaned = cleaned.replaceAll( "[\r\n\t]", " " ); - // Remove non-printable ASCII characters - cleaned = cleaned.replaceAll( "[^\\x20-\\x7E]", "" ); - // Collapse multiple spaces - cleaned = cleaned.replaceAll( "\\s+", " " ); - // Trim whitespace - return cleaned.trim(); - } - } - - public static boolean isValidPath( final String pathString ) - { - try - { - // Convert the string to a Path object - final Path path = Paths.get( pathString ); - - // Check for basic path characteristics - if ( !pathString.contains( "/" ) && !pathString.contains( "\\" ) ) - return false; - - // Check for illegal characters (Windows-specific example) - if ( System.getProperty( "os.name" ).toLowerCase().contains( "win" ) ) - { - final Pattern illegalCharsPattern = Pattern.compile( "[<>:*?\"|]" ); - if ( illegalCharsPattern.matcher( pathString ).find() ) - return false; - } - - return Files.exists( path ); - } - catch ( final InvalidPathException e ) - { - return false; - } - } - - public static void main( final String[] args ) throws Exception - { - System.out.println( "Conda path: " + getCondaPath() ); - System.out.println( "Known environments: " + getEnvList() ); - System.out.println( "Paths:" ); - getEnvMap().forEach( ( k, v ) -> System.out.println( k + " -> " + v ) ); - - System.out.println(); - System.out.println( "Testing versions" ); - - System.out.println( "1 - " + getModuleVersion( "trackastra", "trackastra" ) ); - System.out.println( "2 - " + getModuleVersion( "cellpose", "cellpose" ) ); - System.out.println( "3 - " + getModuleVersion( "cellpose", "cellposebloat" ) ); - System.out.println( "4 - " + getModuleVersion( "cellposebarf", "cellpose" ) ); - } -} diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CommandBuilder.java b/src/main/java/fiji/plugin/trackmate/util/cli/CommandBuilder.java deleted file mode 100644 index ab006ef5c..000000000 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CommandBuilder.java +++ /dev/null @@ -1,276 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util.cli; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import org.apache.commons.lang3.StringUtils; - -import fiji.plugin.trackmate.util.cli.CommandCLIConfigurator.ExecutablePath; -import fiji.plugin.trackmate.util.cli.CondaCLIConfigurator.CondaEnvironmentCommand; -import fiji.plugin.trackmate.util.cli.Configurator.AbstractStringArgument; -import fiji.plugin.trackmate.util.cli.Configurator.Argument; -import fiji.plugin.trackmate.util.cli.Configurator.ArgumentVisitor; -import fiji.plugin.trackmate.util.cli.Configurator.ChoiceArgument; -import fiji.plugin.trackmate.util.cli.Configurator.DoubleArgument; -import fiji.plugin.trackmate.util.cli.Configurator.Flag; -import fiji.plugin.trackmate.util.cli.Configurator.IntArgument; -import fiji.plugin.trackmate.util.cli.Configurator.PathArgument; -import fiji.plugin.trackmate.util.cli.Configurator.StringArgument; - -public class CommandBuilder implements ArgumentVisitor -{ - - private final List< String > tokens = new ArrayList<>(); - - private final Map< Argument< ?, ? >, Function< Object, List< String > > > translators; - - protected CommandBuilder( final Map< Argument< ?, ? >, Function< Object, List< String > > > translators ) - { - this.translators = translators; - } - - @Override - public String toString() - { - return StringUtils.join( tokens, " " ); - } - - private void check( final Argument< ?, ? > arg ) - { - if ( arg.isInCLI() && arg.getArgument() == null ) - throw new IllegalArgumentException( "Incorrect configuration for argument '" + arg.getName() - + "'. The command argument is not set." ); - } - - @Override - public void visit( final ExecutablePath executablePath ) - { - if ( executablePath.getValue() == null ) - throw new IllegalArgumentException( "Executable path is not set." ); - tokens.addAll( translators.getOrDefault( executablePath, v -> Collections.singletonList( "" + v ) ).apply( executablePath.getValue() ) ); - } - - @Override - public void visit( final CondaEnvironmentCommand condaEnv ) - { - if ( condaEnv.getValue() == null ) - throw new IllegalArgumentException( "Conda environment is not set." ); - tokens.addAll( translators.getOrDefault( condaEnv, v -> Collections.singletonList( "" + v ) ).apply( condaEnv.getValue() ) ); - } - - @Override - public void visit( final Flag flag ) - { - check( flag ); - final boolean val; - if ( flag.isSet() ) - val = flag.getValue(); - else - val = flag.getDefaultValue(); - - // Deal with flag that have a '=' vs switches. - final String a = flag.getArgument(); - final List< String > vals = translators.getOrDefault( flag, v -> Collections.singletonList( "" + v ) ).apply( val ); - if ( a.endsWith( "=" ) ) - { - tokens.add( a + String.join( ",", vals ) ); - } - else - { - if ( val ) - { - tokens.add( a ); - /* - * Only add values after token if they have been explicitely - * translated. - */ - if ( translators.containsKey( flag ) ) - tokens.addAll( vals ); - } - } - } - - @Override - public void visit( final IntArgument arg ) - { - check( arg ); - // Is it required and we have not set it? -> error - if ( arg.isRequired() && !arg.isSet() ) - throw new IllegalArgumentException( "Required argument '" + arg.getName() + "' is not set." ); - - // Is not set and we don't have a default value? -> skip - if ( !arg.isSet() && !arg.hasDefaultValue() ) - return; - - // We have a default or a value. - final int val = ( !arg.isSet() ) - ? arg.getDefaultValue() - : arg.getValue(); - - // Test for min & max - if ( arg.hasMin() && ( val < arg.getMin() ) ) - throw new IllegalArgumentException( "Value " + val + " for argument '" + arg.getName() + "' is smaller than the min: " + arg.getMin() ); - if ( arg.hasMax() && arg.getMax() != Integer.MAX_VALUE && ( val > arg.getMax() ) ) - throw new IllegalArgumentException( "Value " + val + " for argument '" + arg.getName() + "' is larger than the max: " + arg.getMax() ); - - final String a = arg.getArgument(); - final List< String > vals = translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ); - // Does the switch ends in '='? - if ( a.endsWith( "=" ) ) - { - // Concatenante with no space. - tokens.add( a + String.join( ",", vals ) ); - } - else - { - tokens.add( a ); - tokens.addAll( vals ); - } - } - - @Override - public void visit( final DoubleArgument arg ) - { - check( arg ); - // Is it required and we have not set it? -> error - if ( arg.isRequired() && !arg.isSet() ) - throw new IllegalArgumentException( "Required argument '" + arg.getName() + "' is not set." ); - - // Is not set and we don't have a default value? -> skip - if ( !arg.isSet() && !arg.hasDefaultValue() ) - return; - - // We have a default or a value. - final double val = ( !arg.isSet() ) - ? arg.getDefaultValue() - : arg.getValue(); - - // Test for min & max - if ( arg.hasMin() && ( val < arg.getMin() ) ) - throw new IllegalArgumentException( "Value " + val + " for argument '" + arg.getName() - + "' is smaller than the min: " + arg.getMin() ); - if ( arg.hasMax() && ( val > arg.getMax() ) ) - throw new IllegalArgumentException( "Value " + val + " for argument '" + arg.getName() - + "' is larger than the max: " + arg.getMax() ); - - final String a = arg.getArgument(); - final List< String > vals = translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ); - // Does the switch ends in '='? - if ( a.endsWith( "=" ) ) - { - // Concatenante with no space. - tokens.add( a + String.join( ",", vals ) ); - } - else - { - tokens.add( a ); - tokens.addAll( vals ); - } - } - - private void visitString( final AbstractStringArgument< ? > arg ) - { - check( arg ); - // Is it required and we have not set it? -> error - if ( arg.isRequired() && !arg.isSet() ) - throw new IllegalArgumentException( "Required argument '" + arg.getName() + "' is not set." ); - - // Is not set and we don't have a default value? -> skip - if ( !arg.isSet() && !arg.hasDefaultValue() ) - return; - - // We have a default or a value. - final String val = ( !arg.isSet() ) - ? arg.getDefaultValue() - : arg.getValue(); - - final String a = arg.getArgument(); - final List< String > vals = translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ); - // Does the switch ends in '='? - if ( a.endsWith( "=" ) ) - { - // Concatenante with no space. - tokens.add( a + String.join( ",", vals ) ); - } - else - { - if ( !a.isEmpty() ) - tokens.add( a ); - tokens.addAll( vals ); - } - } - - @Override - public void visit( final StringArgument stringArgument ) - { - visitString( stringArgument ); - } - - @Override - public void visit( final PathArgument pathArgument ) - { - visitString( pathArgument ); - } - - @Override - public void visit( final ChoiceArgument arg ) - { - check( arg ); - // Is it required and we have not set it? -> error - if ( arg.isRequired() && !arg.isSet() ) - throw new IllegalArgumentException( "Required argument '" + arg.getName() + "' is not set." ); - - // Is not set? -> skip - if ( !arg.isSet() ) - return; - - final String a = arg.getArgument(); - final List vals = translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( arg.getValue() ); - // Does the switch ends in '='? - if ( a.endsWith( "=" ) ) - { - // Concatenante with no space. - tokens.add( a + String.join( ",", vals ) ); - } - else - { - tokens.add( a ); - tokens.addAll( vals ); - } - } - - public static List< String > build( final CLIConfigurator cli ) - { - final CommandBuilder cb = new CommandBuilder( cli.cliTranslators ); - cli.getCommandArg().accept( cb ); - cli.getSelectedArguments() - .stream() - .filter( a -> a.isInCLI() ) - .forEach( arg -> arg.accept( cb ) ); - return cb.tokens; - } -} diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CommandCLIConfigurator.java b/src/main/java/fiji/plugin/trackmate/util/cli/CommandCLIConfigurator.java deleted file mode 100644 index 1603280af..000000000 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CommandCLIConfigurator.java +++ /dev/null @@ -1,109 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util.cli; - -import java.io.File; - -/** - * Base class for CLI config that are based on an executable, reachable by a - * path. - */ -public abstract class CommandCLIConfigurator extends CLIConfigurator -{ - - protected final ExecutablePath executable; - - public static class ExecutablePath extends AbstractStringArgument< ExecutablePath > - { - - @Override - public ExecutablePath name( final String name ) - { - return super.name( name ); - } - - @Override - public ExecutablePath help( final String help ) - { - return super.help( help ); - } - - @Override - public ExecutablePath key( final String key ) - { - return super.key( key ); - } - - @Override - public void accept( final ArgumentVisitor visitor ) - { - visitor.visit( this ); - } - } - - protected CommandCLIConfigurator() - { - this.executable = new ExecutablePath(); - } - - @Override - public ExecutablePath getCommandArg() - { - return executable; - } - - protected String checkExecutable() - { - if ( !executable.isSet() ) - { - return "Executable path is not set.\n"; - } - else - { - final String path = executable.getValue(); - final File file = new File( path ); - if ( !file.exists() ) - return "Executable path " + path + " does not exist.\n"; - if ( !file.canExecute() ) - return "Executable " + path + " cannot be run.\n"; - } - return null; - } - - @Override - public String check() - { - final String out = checkExecutable(); - if ( out != null ) - return out; - return super.check(); - } - - @Override - public String toString() - { - final StringBuilder str = new StringBuilder(); - str.append( executable.toString() ); - str.append( super.toString() + "\n" ); - return str.toString(); - } -} diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CondaCLIConfigurator.java b/src/main/java/fiji/plugin/trackmate/util/cli/CondaCLIConfigurator.java deleted file mode 100644 index d2adc2b52..000000000 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CondaCLIConfigurator.java +++ /dev/null @@ -1,194 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util.cli; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public abstract class CondaCLIConfigurator extends CLIConfigurator -{ - - public static final String KEY_CONDA_ENV = "CONDA_ENV"; - - protected final CondaEnvironmentCommand condaEnv; - - public static class CondaEnvironmentCommand extends AbstractStringArgument< CondaEnvironmentCommand > - { - - private final List< String > envs = new ArrayList<>(); - - protected CondaEnvironmentCommand() - { - name( "Conda environment" ); - help( "The conda environment in which the tool is configured." ); - key( KEY_CONDA_ENV ); - defaultValue( "base" ); - required( true ); - } - - protected CondaEnvironmentCommand addEnvironment( final String env ) - { - if ( !envs.contains( env ) ) - envs.add( env ); - return this; - } - - @Override - public void set( final String env ) - { - if ( envs.isEmpty() ) - { - System.err.println( "The list of conda environments is empty." ); - return; - } - final int sel = envs.indexOf( env ); - if ( sel < 0 ) - { - super.set( envs.get( 0 ) ); - return; - } - super.set( env ); - } - - public void set( final int selected ) - { - if ( envs.isEmpty() ) - { - System.err.println( "The list of conda environments is empty." ); - return; - } - - if ( selected < 0 || selected >= envs.size() ) - set( envs.get( 0 ) ); - else - set( envs.get( selected ) ); - } - - public List< String > getEnvironments() - { - return envs; - } - - @Override - public void accept( final ArgumentVisitor visitor ) - { - visitor.visit( this ); - } - } - - protected CondaCLIConfigurator() - { - super(); - - // Make a UI-only arg configuring the conda env. - // Default is last one (base is not interesting as a default). - final List< String > envList = new ArrayList<>(); - try - { - final List< String > l = CLIUtils.getEnvList(); - envList.addAll( l ); - } - catch ( final Exception e ) - { - // Do nothing. The list of envs will be empty. - System.err.println( "There was an error retrieving the list of conda environments.\n" - + "Did you configure Conda for TrackMate? (Edit > Options > Configure TrackMate Conda path...)" ); - e.printStackTrace(); - } - this.condaEnv = new CondaEnvironmentCommand(); - envList.forEach( condaEnv::addEnvironment ); - condaEnv.key( KEY_CONDA_ENV ); - condaEnv.set( 0 ); - - // Add the translator to make a proper cmd line calling conda first. - setCommandTranslator( condaEnv, s -> { - final List< String > cmd = new ArrayList<>(); - final String condaPath = CLIUtils.getCondaPath(); - final String os = System.getProperty( "os.name" ).toLowerCase(); - if ( os.contains( "win" ) ) - { - // In Windows: Launch a cmd.exe shell. - cmd.addAll( Arrays.asList( "cmd.exe", "/c" ) ); - } - else - { - // On Mac or Linux. - } - // Call conda run. - cmd.add( condaPath ); - cmd.add( "run" ); - cmd.add( "-n" ); - // The executable stuff. - final String envname = ( String ) s; - cmd.add( envname ); - - // The rest of the command, split by spaces. - final String executableCommand = getCommand(); - final String[] split = executableCommand.split( " " ); - cmd.addAll( Arrays.asList( split ) ); - return cmd; - } ); - } - - @Override - public CondaEnvironmentCommand getCommandArg() - { - return condaEnv; - } - - /** - * Returns the command that must be run in the configured conda environment. - * In case the command is made of several tokens, they can be returned - * separated by space (as in a normal command line). - * - * @return the command for this tool. - */ - protected abstract String getCommand(); - - /** - * Returns the version of the Python tool that this configurator is - * configured for. This method assumes that the module has the same name - * that the CLI command used to run it. - * - * @return the version string or null if the version could not - * be determined or if the command does not run on Python. - */ - public String getVersion() - { - return getVersion( getCommandArg().getValue() ); - } - - /** - * Returns the version of the Python tool that this configurator is - * configured for. - * - * @param moduleName - * the name of the module to get the version for. - * @return the version string or null if the version could not - * be determined or if the command does not run on Python. - */ - public String getVersion( final String moduleName ) - { - return CLIUtils.getModuleVersion( condaEnv.getValue(), moduleName ); - } -} diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/ConfigGuiBuilder.java b/src/main/java/fiji/plugin/trackmate/util/cli/ConfigGuiBuilder.java deleted file mode 100644 index 1a7d30b72..000000000 --- a/src/main/java/fiji/plugin/trackmate/util/cli/ConfigGuiBuilder.java +++ /dev/null @@ -1,788 +0,0 @@ -/*- -f * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util.cli; - -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.booleanElement; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.boundedDoubleElement; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.doubleElement; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.intElement; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.linkedCheckBox; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.linkedComboBoxSelector; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.linkedFormattedTextField; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.linkedSliderPanel; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.linkedTextField; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.listElement; -import static fiji.plugin.trackmate.gui.displaysettings.StyleElements.stringElement; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ItemEvent; -import java.awt.event.ItemListener; -import java.io.File; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.function.DoubleSupplier; -import java.util.function.Function; -import java.util.function.IntSupplier; -import java.util.function.Supplier; - -import javax.swing.AbstractButton; -import javax.swing.BorderFactory; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.ButtonGroup; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JComponent; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JRadioButton; -import javax.swing.JSeparator; -import javax.swing.JTextField; -import javax.swing.SwingConstants; - -import fiji.plugin.trackmate.gui.Fonts; -import fiji.plugin.trackmate.gui.displaysettings.BoundedValue; -import fiji.plugin.trackmate.gui.displaysettings.BoundedValue.UpdateListener; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.BooleanElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.BoundedDoubleElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.DoubleElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.IntElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.ListElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.StringElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.StyleElement; -import fiji.plugin.trackmate.util.FileChooser; -import fiji.plugin.trackmate.util.FileChooser.DialogType; -import fiji.plugin.trackmate.util.cli.CommandCLIConfigurator.ExecutablePath; -import fiji.plugin.trackmate.util.cli.CondaCLIConfigurator.CondaEnvironmentCommand; -import fiji.plugin.trackmate.util.cli.Configurator.Argument; -import fiji.plugin.trackmate.util.cli.Configurator.ArgumentVisitor; -import fiji.plugin.trackmate.util.cli.Configurator.ChoiceArgument; -import fiji.plugin.trackmate.util.cli.Configurator.DoubleArgument; -import fiji.plugin.trackmate.util.cli.Configurator.Flag; -import fiji.plugin.trackmate.util.cli.Configurator.IntArgument; -import fiji.plugin.trackmate.util.cli.Configurator.PathArgument; -import fiji.plugin.trackmate.util.cli.Configurator.SelectableArguments; -import fiji.plugin.trackmate.util.cli.Configurator.StringArgument; - -public class ConfigGuiBuilder implements ArgumentVisitor -{ - - private static final int tfCols = 4; - - private final ConfigPanel panel; - - private final GridBagConstraints c; - - private int topInset = 5; - - private int bottomInset = 5; - - private final Map< Argument< ?, ? >, Function< ?, ? > > forwardUITranslators; - - private final Map< Argument< ?, ? >, Function< ?, ? > > backwardUITranslators; - - private ConfigGuiBuilder( - final Map< Argument< ?, ? >, Function< ?, ? > > forwardUITranslators, - final Map< Argument< ?, ? >, Function< ?, ? > > backwardUITranslators ) - { - this.forwardUITranslators = forwardUITranslators; - this.backwardUITranslators = backwardUITranslators; - this.panel = new ConfigPanel(); - final GridBagLayout layout = new GridBagLayout(); - layout.columnWeights = new double[] { 0., 1., 0. }; - panel.setLayout( layout ); - panel.setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) ); - this.c = new GridBagConstraints(); - c.fill = GridBagConstraints.HORIZONTAL; - c.gridwidth = 1; - c.gridx = 0; - c.gridy = 0; - } - - - private void setCurrentRadioButton( final JRadioButton radioButton ) - { - if ( radioButton == null || ( panel.rdbtn != radioButton ) ) - { - topInset = 5; - bottomInset = 5; - } - else - { - topInset = 0; - bottomInset = 0; - } - panel.rdbtn = radioButton; - } - - /* - * ARGUMENT VISITOR. - */ - - @Override - public void visit( final ExecutablePath arg ) - { - if ( !arg.isSet() ) - { - if ( !arg.hasDefaultValue() ) - throw new IllegalArgumentException( "The GUI builder requires all arguments and commands " - + "to have a value or a default value. The argument '" + arg.getName() + "' misses both." ); - arg.set( arg.getDefaultValue() ); - } - - final StringElement element = stringElement( arg.getName(), arg::getValue, arg::set ); - panel.elements.put( arg.getKey(), element ); - addPathToLayout( - arg.getHelp(), - new JLabel( element.getLabel() ), - linkedTextField( element ), - null ); - - panel.add( Box.createVerticalStrut( 10 ), c ); - final JSeparator separator = new JSeparator( JSeparator.HORIZONTAL ); - separator.setMinimumSize( new Dimension( 10, 10 ) ); - addToLayout( null, separator ); - } - - @Override - public void visit( final Flag flag ) - { - if ( !flag.isSet() ) - { - if ( !flag.hasDefaultValue() ) - throw new IllegalArgumentException( "The GUI builder requires all arguments and commands " - + "to have a value or a default value. The argument '" + flag.getName() + "' misses both." ); - flag.set( flag.getDefaultValue() ); - } - - final BooleanElement element = booleanElement( flag.getName(), flag::getValue, flag::set ); - panel.elements.put( flag.getKey(), element ); - final JCheckBox checkbox = linkedCheckBox( element, "" ); - checkbox.setHorizontalAlignment( SwingConstants.LEADING ); - addToLayout( - flag.getHelp(), - new JLabel( element.getLabel() ), - checkbox, - flag ); - } - - @Override - public void visit( final IntArgument arg ) - { - if ( !arg.isSet() ) - { - if ( !arg.hasDefaultValue() ) - throw new IllegalArgumentException( "The GUI builder requires all arguments and commands " - + "to have a value or a default value. The argument '" + arg.getName() + "' misses both." ); - arg.set( arg.getDefaultValue() ); - } - - // Translate - @SuppressWarnings( "unchecked" ) - final Function< Integer, Integer > forward = ( Function< Integer, Integer > ) forwardUITranslators.getOrDefault( arg, v -> v ); - @SuppressWarnings( "unchecked" ) - final Function< Integer, Integer > backward = ( Function< Integer, Integer > ) backwardUITranslators.getOrDefault( arg, v -> v ); - final IntSupplier valueGetter = () -> { - final int value = arg.getValue(); - return forward.apply( value ); - }; - final Consumer< Integer > valueSetter = ( v ) -> { - final int value = backward.apply( v ); - arg.set( value ); - }; - final int min = forward.apply( arg.getMin() ); - final int max = forward.apply( arg.getMax() ); - - final IntElement element = intElement( arg.getName(), min, max, valueGetter, valueSetter ); - panel.elements.put( arg.getKey(), element ); - - final int numberOfColumns; - if ( arg.hasMin() && arg.hasMin() ) - { - final int largest = Math.max( Math.abs( min ), Math.abs( max ) ); - final String numberString = String.valueOf( largest ); - numberOfColumns = numberString.length() + 1; - } - else - { - numberOfColumns = tfCols; - } - addToLayout( - arg.getHelp(), - new JLabel( element.getLabel() ), - linkedSliderPanel( element, numberOfColumns ), - arg.getUnits(), - arg ); - } - - @Override - public void visit( final DoubleArgument arg ) - { - if ( !arg.isSet() ) - { - if ( !arg.hasDefaultValue() ) - throw new IllegalArgumentException( "The GUI builder requires all arguments and commands " - + "to have a value or a default value. The argument '" + arg.getName() + "' misses both." ); - arg.set( arg.getDefaultValue() ); - } - - // Translate - @SuppressWarnings( "unchecked" ) - final Function< Double, Double > forward = ( Function< Double, Double > ) forwardUITranslators.getOrDefault( arg, v -> v ); - @SuppressWarnings( "unchecked" ) - final Function< Double, Double > backward = ( Function< Double, Double > ) backwardUITranslators.getOrDefault( arg, v -> v ); - final DoubleSupplier valueGetter = () -> { - final double value = arg.getValue(); - return forward.apply( value ); - }; - final Consumer< Double > valueSetter = ( v ) -> { - final double value = backward.apply( v ); - arg.set( value ); - }; - - if ( arg.hasMin() && arg.hasMax() ) - { - final double min = forward.apply( arg.getMin() ); - final double max = forward.apply( arg.getMax() ); - final BoundedDoubleElement element = boundedDoubleElement( arg.getName(), - min, max, valueGetter, valueSetter ); - panel.elements.put( arg.getKey(), element ); - addToLayout( - arg.getHelp(), - new JLabel( element.getLabel() ), - linkedSliderPanel( element, tfCols, arg.getMax() / 50 ), - arg.getUnits(), - arg ); - } - else - { - final DoubleElement element = doubleElement( arg.getName(), valueGetter, valueSetter ); - panel.elements.put( arg.getKey(), element ); - if ( arg.isSet() ) - element.set( forward.apply( arg.getValue() ) ); - else if ( arg.hasDefaultValue() ) - element.set( forward.apply( arg.getDefaultValue() ) ); - addToLayout( - arg.getHelp(), - new JLabel( element.getLabel() ), - linkedFormattedTextField( element ), - arg.getUnits(), - arg ); - } - } - - @Override - public void visit( final StringArgument arg ) - { - if ( !arg.isSet() ) - { - if ( !arg.hasDefaultValue() ) - throw new IllegalArgumentException( "The GUI builder requires all arguments and commands " - + "to have a value or a default value. The argument '" + arg.getName() + "' misses both." ); - arg.set( arg.getDefaultValue() ); - } - - // Translate - @SuppressWarnings( "unchecked" ) - final Function< String, String > forward = ( Function< String, String > ) forwardUITranslators.getOrDefault( arg, v -> v ); - @SuppressWarnings( "unchecked" ) - final Function< String, String > backward = ( Function< String, String > ) backwardUITranslators.getOrDefault( arg, v -> v ); - final Supplier< String > valueGetter = () -> { - final String value = arg.getValue(); - return forward.apply( value ); - }; - final Consumer< String > valueSetter = ( v ) -> { - final String value = backward.apply( v ); - arg.set( value ); - }; - - final StringElement element = stringElement( arg.getName(), valueGetter, valueSetter ); - panel.elements.put( arg.getKey(), element ); - addToLayoutTwoLines( - arg.getHelp(), - new JLabel( element.getLabel() ), - linkedTextField( element ), - arg ); - } - - @Override - public void visit( final PathArgument arg ) - { - if ( !arg.isSet() ) - { - if ( !arg.hasDefaultValue() ) - throw new IllegalArgumentException( "The GUI builder requires all arguments and commands " - + "to have a value or a default value. The argument '" + arg.getName() + "' misses both." ); - arg.set( arg.getDefaultValue() ); - } - - // Translate - @SuppressWarnings( "unchecked" ) - final Function< String, String > forward = ( Function< String, String > ) forwardUITranslators.getOrDefault( arg, v -> v ); - @SuppressWarnings( "unchecked" ) - final Function< String, String > backward = ( Function< String, String > ) backwardUITranslators.getOrDefault( arg, v -> v ); - final Supplier< String > valueGetter = () -> { - final String value = arg.getValue(); - return forward.apply( value ); - }; - final Consumer< String > valueSetter = ( v ) -> { - final String value = backward.apply( v ); - arg.set( value ); - }; - - final StringElement element = stringElement( arg.getName(), valueGetter, valueSetter ); - panel.elements.put( arg.getKey(), element ); - addPathToLayout( - arg.getHelp(), - new JLabel( element.getLabel() ), - linkedTextField( element ), - arg ); - } - - @Override - public void visit( final ChoiceArgument arg ) - { - if ( !arg.isSet() ) - { - if ( !arg.hasDefaultValue() ) - throw new IllegalArgumentException( "The GUI builder requires all arguments and commands " - + "to have a value or a default value. The argument '" + arg.getName() + "' misses both." ); - arg.set( arg.getDefaultValue() ); - } - - final List< String > displays = arg.getDisplays(); - final Supplier< String > supplier = () -> { - return displays.get( arg.getSelectedIndex() ); - }; - final Consumer< String > consumer = ( s ) -> arg.set( displays.indexOf( s ) ); - - final ListElement< String > element = listElement( arg.getName(), displays, supplier, consumer ); - panel.elements.put( arg.getKey(), element ); - final JComboBox< String > comboBox = linkedComboBoxSelector( element ); - comboBox.setSelectedIndex( arg.getSelectedIndex() ); - addToLayout( - arg.getHelp(), - new JLabel( element.getLabel() ), - comboBox, - arg.getUnits(), - arg ); - } - - @Override - public void visit( final CondaEnvironmentCommand arg ) - { - if ( arg.getEnvironments().isEmpty() ) - { - // No environment found. Tell the user. - final JLabel lbl = new JLabel( "There was an error retrieving the " - + "list of conda environments. " - + "

    " - + "Did you configure Conda for TrackMate? " - + "

    " - + "(Edit > Options > Configure TrackMate Conda path...)" ); - lbl.setFont( Fonts.SMALL_FONT ); - lbl.setForeground( Color.RED ); - lbl.setPreferredSize( new Dimension( 200, 40 ) ); - addToLayout( arg.getHelp(), lbl ); - return; - } - - if ( !arg.isSet() ) - { - if ( !arg.hasDefaultValue() ) - throw new IllegalArgumentException( "The GUI builder requires all arguments and commands " - + "to have a value or a default value. The argument '" + arg.getName() + "' misses both." ); - arg.set( arg.getDefaultValue() ); - } - - final ListElement< String > element = listElement( arg.getName(), arg.getEnvironments(), arg::getValue, arg::set ); - panel.elements.put( arg.getKey(), element ); - final JComboBox< String > comboBox = linkedComboBoxSelector( element ); - comboBox.setSelectedItem( arg.getValue() ); - addToLayout( - arg.getHelp(), - new JLabel( element.getLabel() ), - comboBox, - null ); - } - - /* - * UI STUFF. - */ - - private void addToLayoutTwoLines( final String help, final JLabel lbl, final JComponent comp, final Argument< ?, ? > arg ) - { - lbl.setText( lbl.getText() + " " ); - lbl.setFont( Fonts.SMALL_FONT ); - comp.setFont( Fonts.SMALL_FONT ); - final JComponent item; - if ( panel.rdbtn != null ) - { - final JRadioButton btn = panel.rdbtn; - btn.addItemListener( e -> comp.setEnabled( btn.isSelected() ) ); - comp.setEnabled( btn.isSelected() ); - item = new JPanel(); - item.setLayout( new BoxLayout( item, BoxLayout.LINE_AXIS ) ); - item.add( btn ); - item.add( Box.createHorizontalGlue() ); - item.add( lbl ); - } - else - { - item = lbl; - } - c.insets = new Insets( 5, 0, 0, 0 ); - c.gridwidth = 3; - panel.add( item, c ); - c.gridy++; - c.anchor = GridBagConstraints.LINE_START; - c.insets = new Insets( 0, 0, 5, 0 ); - panel.add( comp, c ); - c.gridy++; - - if ( help != null ) - { - lbl.setToolTipText( help ); - comp.setToolTipText( help ); - } - } - - private void addPathToLayout( final String help, final JLabel lbl, final JTextField tf, final Argument< ?, ? > arg ) - { - final JPanel p = new JPanel(); - final BoxLayout bl = new BoxLayout( p, BoxLayout.LINE_AXIS ); - p.setLayout( bl ); - - tf.setColumns( 10 ); // Avoid long paths deforming new panels. - - lbl.setText( lbl.getText() + " " ); - lbl.setFont( Fonts.SMALL_FONT ); - tf.setFont( Fonts.SMALL_FONT ); - final JButton browseButton = new JButton( "browse" ); - browseButton.setFont( Fonts.SMALL_FONT ); - browseButton.addActionListener( e -> { - final File file = FileChooser.chooseFile( p, tf.getText(), DialogType.LOAD ); - if ( file == null ) - return; - tf.setText( file.getAbsolutePath() ); - tf.postActionEvent(); - } ); - - if ( panel.rdbtn != null ) - { - final JRadioButton btn = panel.rdbtn; - btn.addItemListener( e -> { - tf.setEnabled( btn.isSelected() ); - browseButton.setEnabled( btn.isSelected() ); - } ); - - tf.setEnabled( btn.isSelected() ); - browseButton.setEnabled( btn.isSelected() ); - p.add( btn ); - } - p.add( lbl ); - p.add( Box.createHorizontalGlue() ); - p.add( browseButton ); - - c.insets = new Insets( topInset, 0, 0, 0 ); - c.gridwidth = 3; - panel.add( p, c ); - c.gridy++; - c.anchor = GridBagConstraints.LINE_START; - c.insets = new Insets( 0, 0, bottomInset, 0 ); - panel.add( tf, c ); - c.gridy++; - - if ( help != null ) - { - lbl.setToolTipText( help ); - tf.setToolTipText( help ); - browseButton.setToolTipText( help ); - } - } - - private void addToLayout( final String help, final JLabel lbl, final JComponent comp, final Argument< ?, ? > arg ) - { - lbl.setText( lbl.getText() + " " ); - lbl.setFont( Fonts.SMALL_FONT ); - lbl.setHorizontalAlignment( JLabel.RIGHT ); - comp.setFont( Fonts.SMALL_FONT ); - - final JComponent header; - if ( arg != null && panel.rdbtn != null ) - { - final JRadioButton btn = panel.rdbtn; - btn.addItemListener( e -> comp.setEnabled( btn.isSelected() ) ); - comp.setEnabled( btn.isSelected() ); - header = new JPanel(); - header.setLayout( new BoxLayout( header, BoxLayout.LINE_AXIS ) ); - header.add( btn ); - header.add( Box.createHorizontalGlue() ); - header.add( lbl ); - } - else - { - header = lbl; - } - - c.gridwidth = 1; - c.anchor = GridBagConstraints.LINE_END; - panel.add( header, c ); - - c.gridx++; - c.gridwidth = 2; - c.anchor = GridBagConstraints.LINE_START; - panel.add( comp, c ); - - c.gridx = 0; - c.gridy++; - c.insets = new Insets( topInset, 0, bottomInset, 0 ); - - if ( help != null ) - { - lbl.setToolTipText( help ); - comp.setToolTipText( help ); - } - } - - private void addToLayout( final String help, final JLabel lbl, final JComponent comp, final String units, final Argument< ?, ? > arg ) - { - if ( units == null ) - { - addToLayout( help, lbl, comp, arg ); - return; - } - - lbl.setText( lbl.getText() + " " ); - lbl.setFont( Fonts.SMALL_FONT ); - lbl.setHorizontalAlignment( JLabel.RIGHT ); - comp.setFont( Fonts.SMALL_FONT ); - - final JComponent header; - if ( panel.rdbtn != null ) - { - final JRadioButton btn = panel.rdbtn; - btn.addItemListener( e -> comp.setEnabled( btn.isSelected() ) ); - header = new JPanel(); - header.setLayout( new BoxLayout( header, BoxLayout.LINE_AXIS ) ); - header.add( btn ); - header.add( Box.createHorizontalGlue() ); - header.add( lbl ); - } - else - { - header = lbl; - } - - c.gridwidth = 1; - c.anchor = GridBagConstraints.LINE_END; - panel.add( header, c ); - - c.gridx++; - c.anchor = GridBagConstraints.LINE_START; - panel.add( comp, c ); - - final JLabel lblUnits = new JLabel( " " + units ); - lblUnits.setFont( Fonts.SMALL_FONT ); - c.gridx++; - c.insets = new Insets( topInset, 0, bottomInset, 0 ); - panel.add( lblUnits, c ); - - c.gridx = 0; - c.gridy++; - - if ( help != null ) - { - lbl.setToolTipText( help ); - comp.setToolTipText( help ); - lblUnits.setToolTipText( help ); - } - } - - private void addToLayout( final String help, final JComponent comp ) - { - final JComponent header; - if ( panel.rdbtn != null ) - { - final JRadioButton btn = panel.rdbtn; - btn.addItemListener( e -> comp.setEnabled( btn.isSelected() ) ); - header = new JPanel(); - header.setLayout( new BoxLayout( header, BoxLayout.LINE_AXIS ) ); - header.add( btn ); - header.add( Box.createHorizontalGlue() ); - header.add( comp ); - } - else - { - header = comp; - } - - c.gridx = 0; - c.gridwidth = 3; - c.fill = GridBagConstraints.HORIZONTAL; - c.insets = new Insets( topInset, 0, bottomInset, 0 ); - panel.add( header, c ); - - c.gridx = 0; - c.gridy++; - - if ( help != null ) - comp.setToolTipText( help ); - } - - private void addLastRow() - { - c.gridx = 0; - c.gridy++; - c.weighty = 1.; - panel.add( new JLabel(), c ); - } - - public static ConfigPanel build( final Configurator config ) - { - final ConfigGuiBuilder builder = createBuilder( config ); - // Could we make something more elegant than this? - if ( config instanceof CLIConfigurator ) - ( ( CLIConfigurator ) config ).getCommandArg().accept( builder ); - return build( config, builder ); - } - - private static ConfigGuiBuilder createBuilder( final Configurator config ) - { - return new ConfigGuiBuilder( - config.forwardUITranslators, - config.backwardUITranslators ); - } - - private static ConfigPanel build( final Configurator config, final ConfigGuiBuilder builder ) - { - /* - * Iterate over arguments. - */ - - // Map a selectable group to a button group in the GUI - final Map< Argument< ?, ? >, JRadioButton > buttons = new HashMap<>(); - for ( final SelectableArguments selectable : config.getSelectables() ) - { - final List< Argument< ?, ? > > args = selectable.getArguments(); - final int nItems = args.size(); - final String label = selectable.getKey(); - final IntSupplier get = selectable::getSelected; - final Consumer< Integer > set = selectable::select; - final IntElement element = intElement( label, 0, nItems - 1, get, set ); - builder.panel.elements.put( selectable.getKey(), element ); - final ButtonGroup buttonGroup = linkedButtonGroup( element ); - // Link radio buttons to arguments. - final Enumeration< AbstractButton > enumeration = buttonGroup.getElements(); - final Iterator< Argument< ?, ? > > it = args.iterator(); - while ( enumeration.hasMoreElements() ) - { - final JRadioButton btn = ( JRadioButton ) enumeration.nextElement(); - final Argument< ?, ? > arg = it.next(); - buttons.put( arg, btn ); - btn.setSelected( selectable.getSelection().equals( arg ) ); - } - } - - // Iterate over arguments, taking care of selectable group. - for ( final Argument< ?, ? > arg : config.getArguments() ) - { - if ( !arg.isVisible() ) - continue; - - builder.setCurrentRadioButton( buttons.get( arg ) ); - arg.accept( builder ); - } - - /* - * Last row. - */ - - builder.addLastRow(); - return builder.panel; - } - - private static ButtonGroup linkedButtonGroup( final IntElement element ) - { - final BoundedValue value = element.getValue(); - final ButtonGroup buttonGroup = new ButtonGroup(); - final List< JRadioButton > buttons = new ArrayList<>(); - for ( int i = 0; i <= value.getRangeMax(); i++ ) - { - final JRadioButton btn = new JRadioButton(); - buttons.add( btn ); - final int selected = i; - btn.addItemListener( new ItemListener() - { - - @Override - public void itemStateChanged( final ItemEvent e ) - { - if (btn.isSelected()) - element.set( selected ); - } - } ); - buttonGroup.add( btn ); - } - element.getValue().setUpdateListener( new UpdateListener() - { - - @Override - public void update() - { - final int selected = element.get(); - buttons.get( selected ).setSelected( true ); - } - } ); - return buttonGroup; - } - - public class ConfigPanel extends JPanel - { - - /** - * Map of StyleElements that are created by this builder. The keys are - * the corresponding argument keys. - */ - final Map< String, StyleElement > elements = new LinkedHashMap<>(); - - private JRadioButton rdbtn; - - private static final long serialVersionUID = 1L; - - public void refresh() - { - elements.values().forEach( e -> e.update() ); - } - } -} diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/Configurator.java b/src/main/java/fiji/plugin/trackmate/util/cli/Configurator.java deleted file mode 100644 index 4c6b347fc..000000000 --- a/src/main/java/fiji/plugin/trackmate/util/cli/Configurator.java +++ /dev/null @@ -1,1378 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util.cli; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; - -import org.apache.commons.lang3.StringUtils; - -import fiji.plugin.trackmate.util.cli.CommandCLIConfigurator.ExecutablePath; -import fiji.plugin.trackmate.util.cli.CondaCLIConfigurator.CondaEnvironmentCommand; - -/** - * Base class for CLI configurator tools. The implementation of a CLI - * configurator is made by subclassing this class (or one of its specialization) - * and specifying arguments for the CLI component with the addXYX() - * builders. - * - * @author Jean-Yves Tinevez - */ -public abstract class Configurator -{ - - protected final List< Argument< ?, ? > > arguments = new ArrayList<>(); - - protected final List< SelectableArguments > selectables = new ArrayList<>(); - - /** - * The translators that will be applied to the value before displaying it in - * the UI. - */ - protected final Map< Argument< ?, ? >, Function< ?, ? > > forwardUITranslators = new HashMap<>(); - - /** - * The translators that will be applied to a value read from the UI, before - * storing it in the Argument. - */ - protected final Map< Argument< ?, ? >, Function< ?, ? > > backwardUITranslators = new HashMap<>(); - - /* - * GETTERS - */ - - /** - * Returns the list of arguments (plus the command) in this CLI config. All - * arguments are present, regardless of whether they are in - * {@link SelectableArguments}, {@link Argument#visible} or not, - * {@link Argument#inCLI} or not. - * - * @return the list of arguments. - */ - public List< Argument< ?, ? > > getArguments() - { - return Collections.unmodifiableList( arguments ); - } - - /** - * Returns the list of {@link SelectableArguments} in this CLI config. - * - * @return the list of {@link SelectableArguments}. - */ - public List< SelectableArguments > getSelectables() - { - return Collections.unmodifiableList( selectables ); - } - - /** - * Returns the list of arguments set in this CLI config. The list contains - * only the arguments that are selected if they are in a - * {@link SelectableArguments}, and those who are not in a - * {@link SelectableArguments}. - * - * @return the selected arguments. - */ - public List< Argument< ?, ? > > getSelectedArguments() - { - final List< Argument< ?, ? > > selectedArguments = new ArrayList<>( arguments ); - for ( final SelectableArguments selectable : selectables ) - selectable.filter( selectedArguments ); - return selectedArguments; - } - - /* - * SELECTABLE ARGUMENT GROUPS. - */ - - /** - * Creates a 'one or the other' relationships. The arguments that will be - * passed to the {@link SelectableArguments} will be flagged as as not to be - * used concurrently in the same command. This will be used when creating - * UIs. - * - * @return a new {@link SelectableArguments} instance. - */ - protected SelectableArguments addSelectableArguments() - { - final SelectableArguments sa = new SelectableArguments(); - selectables.add( sa ); - return sa; - } - - public static class SelectableArguments - { - - private final List< Argument< ?, ? > > args = new ArrayList<>(); - - private String key; - - private int selected = 0; - - public SelectableArguments add( final Argument< ?, ? > arg ) - { - if ( !args.contains( arg ) ) - args.add( arg ); - return this; - } - - public SelectableArguments key( final String key ) - { - this.key = key; - return this; - } - - public String getKey() - { - return key; - } - - private void filter( final List< Argument< ?, ? > > arguments ) - { - final Set< Argument< ?, ? > > toRemove = new HashSet<>(); - for ( final Argument< ?, ? > arg : arguments ) - { - if ( !args.contains( arg ) ) - continue; // Unknown of this selectable, keep it. - - if ( arg.equals( getSelection() ) ) - continue; // The one selected, keep it. - - // Not selected, remove it. - toRemove.add( arg ); - } - - arguments.removeAll( toRemove ); - } - - public void select( final int selection ) - { - this.selected = Math.max( 0, Math.min( args.size() - 1, selection ) ); - } - - public void select( final Argument< ?, ? > arg ) - { - final int sel = args.indexOf( arg ); - if ( sel < 0 ) - { - this.selected = 0; - return; - } - this.selected = sel; - } - - public void select( final String key ) - { - for ( int i = 0; i < args.size(); i++ ) - { - if ( key.equals( args.get( i ).getKey() ) ) - { - this.selected = i; - return; - } - } - this.selected = 0; - } - - public Argument< ?, ? > getSelection() - { - return args.get( selected ); - } - - public int getSelected() - { - return selected; - } - - /** - * Exposes all members of the selectable. - * - * @return the arguments in this selectable. - */ - public List< Argument< ?, ? > > getArguments() - { - return args; - } - } - - /* - * VISITOR INTERFACE. - */ - - public interface ArgumentVisitor - { - public default void visit( final Flag flag ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final StringArgument stringArgument ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final DoubleArgument doubleArgument ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final IntArgument intArgument ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final ChoiceArgument choiceArgument ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final PathArgument pathArgument ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final ExecutablePath executablePath ) - { - throw new UnsupportedOperationException(); - } - - public default void visit( final CondaEnvironmentCommand condaEnvironmentCommand ) - { - throw new UnsupportedOperationException(); - } - } - - /* - * ADDER CLASSES. - */ - - @SuppressWarnings( "unchecked" ) - abstract class Adder< A extends Argument< A, O >, T extends Adder< A, T, O >, O > - { - - protected String name; - - protected String help; - - protected String key; - - protected boolean required; - - protected String units; - - protected O defaultValue; - - protected String argument; - - protected boolean visible = true; // by default - - protected boolean inCLI = true; // by default - - /** - * Specifies the argument to use in the CLI. - * - * @param argument - * the command line argument. - * @return this adder. - */ - public T argument( final String argument ) - { - this.argument = argument; - return ( T ) this; - } - - /** - * Specifies whether this argument will be visible in user interfaces - * generated from the configurator. - * - * @param visible - * UI visibility. - * @return this adder. - */ - public T visible( final boolean visible ) - { - this.visible = visible; - return ( T ) this; - } - - /** - * Specifies a user-friendly name for the argument. - * - * @param name - * the argument name. - * @return this adder. - */ - public T name( final String name ) - { - this.name = name; - return ( T ) this; - } - - /** - * Specifies a help text for the argument. - * - * @param help - * the help text. - * @return this adder. - */ - public T help( final String help ) - { - this.help = help; - return ( T ) this; - } - - /** - * Specifies the key to use to serialize this argument in TrackMate XML - * file. If null, the argument will not be serialized. - * - * @param key - * the argument key. - * @return this adder. - */ - public T key( final String key ) - { - this.key = key; - return ( T ) this; - } - - /** - * Specifies whether this argument is required in the CLI. If - * true, if not set and if there are no default value, an - * error will be thrown. - * - * @param required - * whether this argument is required. - * @return this adder. - */ - public T required( final boolean required ) - { - this.required = required; - return ( T ) this; - } - - /** - * Specifies units for values accepted by this argument. - * - * @param units - * argument value units. - * @return this adder. - */ - public T units( final String units ) - { - this.units = units; - return ( T ) this; - } - - /** - * Specifies a default value for this argument. If the argument is not - * set, it will appear in the command line with this default value. - * - * @param defaultValue - * the argument default value. - * @return this adder. - */ - public T defaultValue( final O defaultValue ) - { - this.defaultValue = defaultValue; - return ( T ) this; - } - - /** - * Specifies whether this argument will appear in the command line. - * - * @param inCLI - * appears in the command line. - * @return this adder. - */ - public T inCLI( final boolean inCLI ) - { - this.inCLI = inCLI; - return ( T ) this; - } - - /** - * Returns the argument created by this builder. - * - * @return the argument. - */ - public abstract A get(); - } - - @SuppressWarnings( "unchecked" ) - private abstract class BoundedAdder< A extends BoundedValueArgument< A, O >, T extends BoundedAdder< A, T, O >, O > extends Adder< A, T, O > - { - protected O min; - - protected O max; - - /** - * Specifies a min for the values accepted by this argument. If the user - * sets a value below this min value, an error is thrown. - * - * @param min - * the min value. - * @return this adder. - */ - public T min( final O min ) - { - this.min = min; - return ( T ) this; - } - - /** - * Specifies a max for the values accepted by this argument. If the user - * sets a value above this max value, an error is thrown. - * - * @param max - * the max value. - * @return this adder. - */ - public T max( final O max ) - { - this.max = max; - return ( T ) this; - } - } - - protected class IntAdder extends BoundedAdder< IntArgument, IntAdder, Integer > - { - @Override - public IntArgument get() - { - final IntArgument arg = new IntArgument() - .name( name ) - .help( help ) - .argument( argument ) - .defaultValue( defaultValue ) - .max( max ) - .min( min ) - .required( required ) - .units( units ) - .visible( visible ) - .inCLI( inCLI ) - .key( key ); - Configurator.this.arguments.add( arg ); - return arg; - } - } - - protected class DoubleAdder extends BoundedAdder< DoubleArgument, DoubleAdder, Double > - { - - private DoubleAdder() - {} - - @Override - public DoubleArgument get() - { - final DoubleArgument arg = new DoubleArgument() - .name( name ) - .help( help ) - .argument( argument ) - .defaultValue( defaultValue ) - .max( max ) - .min( min ) - .required( required ) - .units( units ) - .visible( visible ) - .inCLI( inCLI ) - .key( key ); - Configurator.this.arguments.add( arg ); - return arg; - } - } - - protected class FlagAdder extends Adder< Flag, FlagAdder, Boolean > - { - - private FlagAdder() - {} - - @Override - public Flag get() - { - final Flag arg = new Flag() - .name( name ) - .help( help ) - .argument( argument ) - .defaultValue( defaultValue ) - .required( required ) - .units( units ) - .visible( visible ) - .inCLI( inCLI ) - .key( key ); - Configurator.this.arguments.add( arg ); - return arg; - } - } - - protected class StringAdder extends Adder< StringArgument, StringAdder, String > - { - - private StringAdder() - {} - - @Override - public StringArgument get() - { - final StringArgument arg = new StringArgument() - .name( name ) - .help( help ) - .argument( argument ) - .defaultValue( defaultValue ) - .required( required ) - .units( units ) - .visible( visible ) - .inCLI( inCLI ) - .key( key ); - Configurator.this.arguments.add( arg ); - return arg; - } - } - - protected class PathAdder extends Adder< PathArgument, PathAdder, String > - { - - private PathAdder() - {} - - @Override - public PathArgument get() - { - final PathArgument arg = new PathArgument() - .name( name ) - .help( help ) - .argument( argument ) - .defaultValue( defaultValue ) - .required( required ) - .units( units ) - .visible( visible ) - .inCLI( inCLI ) - .key( key ); - Configurator.this.arguments.add( arg ); - return arg; - } - } - - protected class ChoiceAdder extends Adder< ChoiceArgument, ChoiceAdder, String > - { - - private ChoiceAdder() - {} - - private final List< String > choices = new ArrayList<>(); - - private final List< String > mappeds = new ArrayList<>(); - - /** - * Adds a selectable item for this choice argument. The user will be - * able to select from the list of choices added by this method. - * - * @param choice - * the choice to add. - * @return this adder. - */ - public ChoiceAdder addChoice( final String choice ) - { - return addChoice( choice, choice ); - } - - /** - * Adds a selectable item for this choice argument. The user will be - * able to select from the list of choices added by this method. In the - * command line, this choice will be mapped to the specified string. - *

    - * Example: - * - *

    -		 * addChoice( "Bacteria phase contrast", "bact_phase_omni" )
    -		 * 
    - * - * will display Bacteria phase contrast in the UI and results in - * using bact_phase_omni in the generated command line. - * - * @param choice - * the choice to add. - * @param mapped - * the string the choice will be mapped in the command line - * argument. - * @return this adder. - */ - public ChoiceAdder addChoice( final String choice, final String mapped ) - { - if ( !choices.contains( choice ) ) - { - choices.add( choice ); - mappeds.add( mapped ); - } - return this; - } - - /** - * Adds the specified items for this choice argument. The user will be - * able to select from the list of choices added by this method. - * - * @param c - * the choices to add. - * @return this adder. - */ - public Adder< ChoiceArgument, ChoiceAdder, String > addChoiceAll( final Collection< String > c ) - { - for ( final String in : c ) - addChoice( in ); - return this; - } - - /** - * Specifies a default value for this argument. If the argument is not - * set, it will appear in the command line with this default value. - *

    - * The value specified must belong to the list of choices set with - * {@link #addChoice(String)} or {@link #addChoiceAll(Collection)}. - * - * @param defaultChoice - * the argument default value. - * @return this adder. - */ - @Override - public ChoiceAdder defaultValue( final String defaultChoice ) - { - final int sel = choices.indexOf( defaultChoice ); - if ( sel < 0 ) - throw new IllegalArgumentException( "Unknown selection '" + defaultChoice + "' for parameter '" - + name + "'. Must be one of " + StringUtils.join( choices, ", " ) + "." ); - return super.defaultValue( defaultChoice ); - } - - /** - * Specifies a default value for this argument, by selecting the - * possible value in order or addition. If the argument is not set, it - * will appear in the command line with this default value. - * - * @param selected - * the index of the default value in the list of possible - * choices. - * @return this adder. - */ - public ChoiceAdder defaultValue( final int selected ) - { - if ( selected < 0 || selected >= choices.size() ) - throw new IllegalArgumentException( "Invalid index for selection of parameter '" - + name + "'. Must be in scale " + 0 + " to " + ( choices.size() - 1 ) + " in " - + StringUtils.join( choices, ", " ) + "." ); - return defaultValue( choices.get( selected ) ); - } - - @Override - public ChoiceArgument get() - { - final ChoiceArgument arg = new ChoiceArgument() - .name( name ) - .help( help ) - .argument( argument ) - .required( required ) - .units( units ) - .visible( visible ) - .inCLI( inCLI ) - .key( key ); - for ( int i = 0; i < choices.size(); i++ ) - arg.addChoice( choices.get( i ), mappeds.get( i ) ); - - arg.defaultValue( defaultValue ); - Configurator.this.arguments.add( arg ); - return arg; - } - } - - /* - * ADDER METHODS. - */ - - /** - * Adds a boolean flag argument to the CLI, via a builder. - *

    - * In the CLI tools we have been trying to implement, they exist in two - * flavors, that are both supported. - * - * If the argument starts with a double-dash --, as in Python - * argparse syntax, then it is understood that setting this flag to true - * makes it appear in the CLI. For instance: --use-gpu. - * - * If the arguments ends with a '=' sign (e.g. "save_txt="), - * then it expects to receive a 'true' or 'false' value. - * - * @return a new flag argument builder. - */ - protected FlagAdder addFlag() - { - return new FlagAdder(); - } - - /** - * Adds a string argument to the CLI, via a builder. - * - * @return new string argument builder. - */ - protected StringAdder addStringArgument() - { - return new StringAdder(); - } - - /** - * Adds a path argument to the CLI, via a builder. - * - * @return new path argument builder. - */ - protected PathAdder addPathArgument() - { - return new PathAdder(); - } - - /** - * Adds a integer argument to the CLI, via a builder. - * - * @return new integer argument builder. - */ - protected IntAdder addIntArgument() - { - return new IntAdder(); - } - - /** - * Adds a double argument to the CLI, via a builder. - * - * @return new double argument builder. - */ - protected DoubleAdder addDoubleArgument() - { - return new DoubleAdder(); - } - - /** - * Adds a choice argument to the CLI, via a builder. Such arguments can - * accept a series of discrete values (specified by addChoice() method in - * the builder). - * - * @return new choice argument builder. - */ - protected ChoiceAdder addChoiceArgument() - { - return new ChoiceAdder(); - } - - /** - * Adds an extra argument, defined by other means than the adder methods. - * - * @param extraArg - * the argument to add to this CLI config. - * @param - * the argument type. - * @return the argument - */ - protected < T extends Argument< ?, ? > > T addExtraArgument( final T extraArg ) - { - this.arguments.add( extraArg ); - return extraArg; - } - - /* - * ARGUMENT CLASSES. - */ - - public static class Flag extends Argument< Flag, Boolean > - { - Flag() - {} - - /** - * Sets this flag argument to true. - */ - public void set() - { - set( true ); - } - - @Override - public void accept( final ArgumentVisitor visitor ) - { - visitor.visit( this ); - } - - @Override - public void setValueObject( final Object val ) - { - if ( !Boolean.class.isInstance( val ) ) - throw new IllegalArgumentException( "Argument '" + name + "' expects Boolean. Got " + val.getClass().getSimpleName() ); - - final Boolean v = ( ( Boolean ) val ); - set( v ); - } - } - - /** - * Specialization of {@link StringArgument} to be used in a GUI. - */ - public static class PathArgument extends AbstractStringArgument< PathArgument > - { - private PathArgument() - {} - - @Override - public void accept( final ArgumentVisitor visitor ) - { - visitor.visit( this ); - } - } - - public static class StringArgument extends AbstractStringArgument< StringArgument > - { - private StringArgument() - {} - - @Override - public void accept( final ArgumentVisitor visitor ) - { - visitor.visit( this ); - } - } - - public static abstract class AbstractStringArgument< T extends AbstractStringArgument< T > > extends Argument< T, String > - { - - @Override - public void setValueObject( final Object val ) - { - if ( !String.class.isInstance( val ) ) - throw new IllegalArgumentException( "Argument '" + name + "' expects String. Got " + val.getClass().getSimpleName() ); - - final String v = ( ( String ) val ); - set( v ); - } - } - - public static class IntArgument extends BoundedValueArgument< IntArgument, Integer > - { - IntArgument() - {} - - @Override - public void accept( final ArgumentVisitor visitor ) - { - visitor.visit( this ); - } - - @Override - public void setValueObject( final Object val ) - { - if ( !Integer.class.isInstance( val ) ) - throw new IllegalArgumentException( "Argument '" + name + "' expects Integer. Got " + val.getClass().getSimpleName() ); - - final Integer v = ( ( Integer ) val ); - set( v ); - } - } - - public static class DoubleArgument extends BoundedValueArgument< DoubleArgument, Double > - { - @Override - public void accept( final ArgumentVisitor visitor ) - { - visitor.visit( this ); - } - - @Override - public void setValueObject( final Object val ) - { - if ( !Double.class.isInstance( val ) ) - throw new IllegalArgumentException( "Argument '" + name + "' expects Double. Got " + val.getClass().getSimpleName() ); - - final Double v = ( ( Double ) val ); - set( v ); - } - } - - public static class ChoiceArgument extends Argument< ChoiceArgument, String > - { - - private final List< String > choices = new ArrayList<>(); - - private final List< String > displays = new ArrayList<>(); - - private int selected = -1; // -1 means no selection.; - - private ChoiceArgument() - {} - - private ChoiceArgument addChoice( final String choice, final String displayed ) - { - if ( !choices.contains( choice ) ) - { - choices.add( choice ); - displays.add( displayed ); - } - return this; - } - - /** - * The list of the display strings corresponding to the possible - * choices. - * - * @return The list of the display strings. - */ - public List< String > getDisplays() - { - return displays; - } - - @Override - public void accept( final ArgumentVisitor visitor ) - { - visitor.visit( this ); - } - - @Override - public boolean isSet() - { - return selected >= 0; - } - - @Override - public String getValue() - { - return choices.get( selected ); - } - - public int getSelectedIndex() - { - return selected; - } - - @Override - public void set( final String choice ) - { - final int sel = choices.indexOf( choice ); - if ( sel < 0 ) - throw new IllegalArgumentException( "Unknown selection '" + choice + "' for parameter '" - + name + "'. Must be one of: [ " + StringUtils.join( choices, ", " ) + " ]." ); - this.selected = sel; - } - - public void set( final int selected ) - { - if ( selected < 0 || selected >= choices.size() ) - throw new IllegalArgumentException( "Invalid index for selection of parameter '" - + name + "'. Must be in scale " + 0 + " to " + ( choices.size() - 1 ) + " in " - + StringUtils.join( choices, ", " ) + "." ); - this.selected = selected; - } - - @Override - public void setValueObject( final Object val ) - { - if ( !String.class.isInstance( val ) ) - throw new IllegalArgumentException( "Argument '" + name + "' expects String. Got " + val.getClass().getSimpleName() ); - - final String v = ( ( String ) val ); - set( v ); - } - - @Override - ChoiceArgument defaultValue( final String defaultChoice ) - { - final int sel = choices.indexOf( defaultChoice ); - if ( sel < 0 ) - throw new IllegalArgumentException( "Unknown selection '" + defaultChoice + "' for parameter '" - + name + "'. Must be one of " + StringUtils.join( choices, ", " ) + "." ); - super.defaultValue( defaultChoice ); - return this; - } - - ChoiceArgument defaultValue( final int selected ) - { - if ( selected < 0 || selected >= choices.size() ) - throw new IllegalArgumentException( "Invalid index for selection of parameter '" - + name + "'. Must be in scale " + 0 + " to " + ( choices.size() - 1 ) + " in " - + StringUtils.join( choices, ", " ) + "." ); - super.defaultValue( choices.get( selected ) ); - return this; - } - - @Override - public String toString() - { - final String str = super.toString(); - return str - + " - choices: " + choices + "\n" - + " - display strings: " + displays + "\n"; - } - - - } - - @SuppressWarnings( "unchecked" ) - public static abstract class BoundedValueArgument< T extends BoundedValueArgument< T, O >, O > extends Argument< T, O > - { - - private BoundedValueArgument() - {} - - private O min; - - private O max; - - T min( final O min ) - { - this.min = min; - return ( T ) this; - } - - public O getMax() - { - return max; - } - - T max( final O max ) - { - this.max = max; - return ( T ) this; - } - - public O getMin() - { - return min; - } - - public boolean hasMin() - { - return min != null; - } - - public boolean hasMax() - { - return max != null; - } - - @Override - public String toString() - { - final String str = super.toString(); - return str - + " - has min: " + hasMin() + "\n" - + ( hasMin() - ? " - min: " + getMin() + "\n" - : "" ) - + " - has max: " + hasMax() + "\n" - + ( hasMax() - ? " - max: " + getMax() + "\n" - : "" ); - } - } - - /** - * Mother class for command arguments. Typically in the command line they - * appear after the executable name with '--something'. - * - * @param - * the implementing type of the argument. - * @param - * the type of value this argument accepts. - */ - @SuppressWarnings( "unchecked" ) - public static abstract class Argument< T extends Argument< T, O >, O > - { - - protected boolean visible = true; - - protected String name; - - protected String help; - - private String key; - - private String argument; - - private boolean inCLI = true; - - T argument( final String argument ) - { - this.argument = argument; - return ( T ) this; - } - - public String getArgument() - { - return argument; - } - - private O value; - - private O defaultValue; - - /** - * Arguments flagged as not required, but without default value, will be - * prompted to the user. - */ - private boolean required = false; - - private String units; - - T required( final boolean required ) - { - this.required = required; - return ( T ) this; - } - - public boolean isRequired() - { - return required; - } - - T units( final String units ) - { - this.units = units; - return ( T ) this; - } - - public String getUnits() - { - return units; - } - - T defaultValue( final O defaultValue ) - { - this.defaultValue = defaultValue; - return ( T ) this; - } - - public O getDefaultValue() - { - return defaultValue; - } - - public boolean hasDefaultValue() - { - return defaultValue != null; - } - - public void set( final O value ) - { - this.value = value; - } - - public O getValue() - { - return value; - } - - public boolean isSet() - { - return value != null; - } - - public Object getValueObject() - { - return getValue(); - } - - - /** - * Sets the value of this argument via the specified object. This is - * used when deserializing TrackMate settings map. - * - * @param val - * the object to set the value from - * @see TrackMateSettingsBuilder - */ - public abstract void setValueObject( Object val ); - - /** - * If false, this argument won't be used in the command - * line generator. This is useful to add extra parameters to the GUI - * that are required by TrackMate but not by the CLI tool. - * - * @param inCLI - * whether this argument should be used when generating - * commands. By default: true. - * @see CommandBuilder - * @return the argument. - */ - T inCLI( final boolean inCLI ) - { - this.inCLI = inCLI; - return ( T ) this; - } - - public boolean isInCLI() - { - return inCLI; - } - - /** - * If false, this argument won't be shown in UIs. It will - * be used for the command line builder nonetheless. - * - * @param visible - * whether this argument should be visible in the UI or not. - * By default: true. - * @see CliGuiBuilder - * @return the argument. - */ - T visible( final boolean visible ) - { - this.visible = visible; - return ( T ) this; - } - - public boolean isVisible() - { - return visible; - } - - T name( final String name ) - { - this.name = name; - return ( T ) this; - } - - T help( final String help ) - { - this.help = help; - return ( T ) this; - } - - /** - * Sets the String key to use in TrackMate settings map - * de/serialization. - * - * @param key - * the key to use. By default: the {@link #name} of this - * argument. - * @return the argument. - * @see TrackMateSettingsBuilder - */ - T key( final String key ) - { - - this.key = key; - return ( T ) this; - } - - public String getName() - { - return name; - } - - public String getHelp() - { - return help; - } - - public String getKey() - { - return key; - } - - public abstract void accept( final ArgumentVisitor visitor ); - - @Override - public String toString() - { - return this.getClass().getSimpleName() - + " (" + getName() + ")\n" - + " - help: " + getHelp() + "\n" - + " - key: " + getKey() + "\n" - + " - argument: " + getArgument() + "\n" - + " - visible: " + isVisible() + "\n" - + " - is set: " + isSet() + "\n" - + ( isSet() - ? " - value: " + getValue() + "\n" - : "" ) - + " - has default value: " + hasDefaultValue() + "\n" - + ( hasDefaultValue() - ? " - default value: " + getDefaultValue() + "\n" - : "" ) - + " - required: " + isRequired() + "\n" - + " - units: " + getUnits() + "\n"; - } - } - - @Override - public String toString() - { - final StringBuilder str = new StringBuilder(); - str.append( super.toString() + "\n" ); - arguments.forEach( str::append ); - return str.toString(); - } - - public String check() - { - final StringBuilder str = new StringBuilder(); - for ( final Argument< ?, ? > arg : getSelectedArguments() ) - { - if ( arg.isInCLI() && arg.getArgument() == null ) - str.append( "Argument '" + arg.getName() + "' does not define the argument switch.\n" ); - - if ( arg.isRequired() && !arg.isSet() && !arg.hasDefaultValue() ) - str.append( "Argument '" + arg.getName() + "' is required but is not set and does not define a default value.\n" ); - } - return str.length() == 0 ? null : str.toString(); - } - - /** - * Decorates the specified argument with a translator, that will modify the - * value displayed in the user-interfaces built with this - * configurator. - *

    - * This can be used to translate the value of an argument into a more - * user-friendly value, for instance to translate a radius into a diameter. - *

    - * Warning: display translation is not supported for {@link Flag} and - * {@link ChoiceArgument}. - * - * @param arg - * the argument to decorate. - * @param forward - * the function to apply to the value to display it in the UI. - * @param backward - * the function to apply to the value to get the value back from - * the UI. - * @param - * the type of value the argument accepts. - */ - protected < O > void setDisplayTranslator( final Argument< ?, O > arg, final Function< O, O > forward, final Function< O, O > backward ) - { - if ( arg instanceof ChoiceArgument ) - throw new IllegalArgumentException( "ChoiceArgument does not support display translators." ); - if ( arg instanceof Flag ) - throw new IllegalArgumentException( "Flag does not support display translators." ); - - forwardUITranslators.put( arg, forward ); - backwardUITranslators.put( arg, backward ); - } -} diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/GenericConfigurationPanel.java b/src/main/java/fiji/plugin/trackmate/util/cli/GenericConfigurationPanel.java deleted file mode 100644 index fba18e4b3..000000000 --- a/src/main/java/fiji/plugin/trackmate/util/cli/GenericConfigurationPanel.java +++ /dev/null @@ -1,132 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util.cli; - -import static fiji.plugin.trackmate.gui.Fonts.BIG_FONT; - -import java.awt.BorderLayout; -import java.awt.Dimension; -import java.util.HashMap; -import java.util.Map; - -import javax.swing.BorderFactory; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.Icon; -import javax.swing.JEditorPane; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.SwingConstants; - -import fiji.plugin.trackmate.gui.GuiUtils; -import fiji.plugin.trackmate.gui.components.ConfigurationPanel; -import fiji.plugin.trackmate.util.cli.ConfigGuiBuilder.ConfigPanel; - -public class GenericConfigurationPanel extends ConfigurationPanel -{ - - private static final long serialVersionUID = 1L; - - protected final ConfigPanel mainPanel; - - protected final Configurator config; - - public GenericConfigurationPanel( - final Configurator config, - final String title, - final Icon icon, - final String docURL ) - { - this.config = config; - - final BorderLayout borderLayout = new BorderLayout(); - setLayout( borderLayout ); - - /* - * HEADER - */ - - final JPanel header = new JPanel(); - header.setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) ); - header.setLayout( new BoxLayout( header, BoxLayout.Y_AXIS ) ); - - final JLabel lblDetector = new JLabel( title, icon, JLabel.RIGHT ); - lblDetector.setFont( BIG_FONT ); - lblDetector.setHorizontalAlignment( SwingConstants.CENTER ); - lblDetector.setAlignmentX( JLabel.CENTER_ALIGNMENT ); - header.add( lblDetector ); - if ( docURL != null ) - { - final JEditorPane infoDisplay = GuiUtils.infoDisplay( "" + "Documentation for this module " - + "on the ImageJ Wiki." - + "", false ); - infoDisplay.setMaximumSize( new Dimension( 100_000, 40 ) ); - header.add( Box.createVerticalStrut( 5 ) ); - header.add( infoDisplay ); - } - add( header, BorderLayout.NORTH ); - - /* - * CONFIG - */ - - this.mainPanel = ConfigGuiBuilder.build( config ); - final JScrollPane scrollPane = new JScrollPane( mainPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER ); - scrollPane.setBorder( null ); - scrollPane.getVerticalScrollBar().setUnitIncrement( 16 ); - add( scrollPane, BorderLayout.CENTER ); - } - - @Override - public void setSettings( final Map< String, Object > settings ) - { - try - { - TrackMateSettingsBuilder.fromTrackMateSettings( settings, config ); - } - catch ( final IllegalArgumentException e ) - { - // Incompatible settings, we keep the defaults. - } - catch ( final Exception e ) - { - e.printStackTrace(); - } - finally - { - mainPanel.refresh(); - } - } - - @Override - public Map< String, Object > getSettings() - { - final Map< String, Object > map = new HashMap<>(); - TrackMateSettingsBuilder.toTrackMateSettings( map, config ); - return map; - } - - @Override - public void clean() - {} -} diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/TrackMateSettingsBuilder.java b/src/main/java/fiji/plugin/trackmate/util/cli/TrackMateSettingsBuilder.java deleted file mode 100644 index b6956ccb7..000000000 --- a/src/main/java/fiji/plugin/trackmate/util/cli/TrackMateSettingsBuilder.java +++ /dev/null @@ -1,137 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util.cli; - -import java.util.LinkedHashMap; -import java.util.Map; - -import fiji.plugin.trackmate.util.cli.Configurator.Argument; -import fiji.plugin.trackmate.util.cli.Configurator.SelectableArguments; - -public class TrackMateSettingsBuilder -{ - - private TrackMateSettingsBuilder() - {} - - private static void toMap( final Argument< ?, ? > arg, final Map< String, Object > settings ) - { - if ( arg.getKey() != null ) - settings.put( arg.getKey(), arg.getValueObject() ); - } - - private static void toMap( final SelectableArguments selectable, final Map< String, Object > settings ) - { - if ( selectable.getKey() != null ) - settings.put( selectable.getKey(), selectable.getSelection().getKey() ); - } - - private static void fromMap( final Map< String, Object > settings, final Argument< ?, ? > arg ) - { - final Object val = settings.get( arg.getKey() ); - if ( val != null ) - arg.setValueObject( val ); - } - - private static void fromMap( final Map< String, Object > settings, final SelectableArguments selectable ) - { - final Object val = settings.get( selectable.getKey() ); - if ( val != null ) - selectable.select( ( String ) val ); - } - - /** - * Serializes the specified config and extra parameters to a TrackMate - * settings map. - * - * @param settings - * the map to serialize settings to. - * @param config - * the config. - */ - public static void toTrackMateSettings( final Map< String, Object > settings, final Configurator config ) - { - if ( config instanceof CLIConfigurator ) - toMap( ( ( CLIConfigurator ) config ).getCommandArg(), settings ); - config.getArguments().forEach( arg -> toMap( arg, settings ) ); - config.getSelectables().forEach( selectable -> toMap( selectable, settings ) ); - } - - /** - * Serializes the parameters stored in a TrackMate map to a config object. - * The parameters in the map unknown to the config are simply ignored. - * - * @param settings - * the map to read parameters from. - * @param config - * the config to write parameters into. - */ - public static final void fromTrackMateSettings( final Map< String, Object > settings, final Configurator config ) - { - if ( config instanceof CLIConfigurator ) - fromMap( settings, ( ( CLIConfigurator ) config ).getCommandArg() ); - config.getArguments().forEach( arg -> fromMap( settings, arg ) ); - config.getSelectables().forEach( selectable -> fromMap( settings, selectable ) ); - } - - /** - * Returns a map containing the default settings of the specified - * Configurator. The map will contain the keys of all arguments and - * selectable arguments, and their default values. - * - * @param config - * the configurator. - * @return a new map containing the default settings. - */ - public static final Map< String, Object > getDefaultSettings( final Configurator config ) - { - final Map< String, Object > settings = new LinkedHashMap< String, Object >(); - - if ( config instanceof CLIConfigurator ) - { - final Argument< ?, ? > commandArg = ( ( CLIConfigurator ) config ).getCommandArg(); - if ( commandArg.getKey() != null && commandArg.hasDefaultValue() ) - settings.put( commandArg.getKey(), commandArg.getDefaultValue() ); - } - - config.arguments.forEach( arg -> { - final String key = arg.getKey(); - if ( key == null ) - return; - - if ( !arg.hasDefaultValue() ) - throw new IllegalArgumentException( "The argument '" + key + "' in the configurator " + config.getClass().getSimpleName() + " has no default value, which is required." ); - settings.put( key, arg.getDefaultValue() ); - } ); - config.selectables.forEach( sel -> { - final String selKey = sel.getKey(); - if ( selKey == null ) - return; - - final String argKey = sel.getSelection().getKey(); - if ( argKey == null ) - throw new IllegalArgumentException( "The selectable argument '" + selKey + "' in the configurator " + config + " has no key, which is required." ); - settings.put( selKey, argKey ); - } ); - return settings; - } -} diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/condapath/CondaDetector.java b/src/main/java/fiji/plugin/trackmate/util/cli/condapath/CondaDetector.java deleted file mode 100644 index 06a04586c..000000000 --- a/src/main/java/fiji/plugin/trackmate/util/cli/condapath/CondaDetector.java +++ /dev/null @@ -1,1549 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util.cli.condapath; - -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Random; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import ij.IJ; - -/** - * Detects conda installation and provides information needed to run conda - * commands. Returns both the conda executable path and CONDA_ROOT_PREFIX. - */ -public class CondaDetector -{ - - private static CondaInfo cachedInfo = null; - - private static long cacheTimestamp = 0; - - private static final long CACHE_TIMEOUT_MS = 60000; // 1 minute - - /** - * Container for conda installation information - */ - public static class CondaInfo - { - private final String condaExecutable; - - private final String rootPrefix; - - private final String version; - - public CondaInfo( final String condaExecutable, final String rootPrefix, final String version ) - { - this.condaExecutable = condaExecutable; - this.rootPrefix = rootPrefix; - this.version = version; - } - - /** - * Path to conda executable (for running `conda run`) - */ - public String getCondaExecutable() - { - return condaExecutable; - } - - /** - * Conda root prefix (for CONDA_ROOT_PREFIX environment variable) - */ - public String getRootPrefix() - { - return rootPrefix; - } - - /** - * Conda version (informational) - */ - public String getVersion() - { - return version; - } - - /** - * Check if this is micromamba (for command flag compatibility) - */ - public boolean isMicromamba() - { - return isMicromambaExecutable( condaExecutable ); - } - - @Override - public String toString() - { - return String.format( "CondaInfo{executable='%s', rootPrefix='%s', version='%s'}", - condaExecutable, rootPrefix, version ); - } - } - - /** - * Container for conda environment information - */ - public static class CondaEnvironment - { - private final String name; - - private final String path; - - public CondaEnvironment( final String name, final String path ) - { - this.name = name; - this.path = path; - } - - public String getName() - { - return name; - } - - public String getPath() - { - return path; - } - - @Override - public String toString() - { - return name + " (" + path + ")"; - } - } - - /** - * Custom exception for when conda cannot be found - */ - public static class CondaNotFoundException extends Exception - { - private static final long serialVersionUID = 1L; - - public CondaNotFoundException( final String message ) - { - super( message ); - } - } - - // ========== Main Detection Methods ========== - - /** - * Main entry point - detects conda and returns all necessary information - */ - public static CondaInfo detect() throws CondaNotFoundException - { - // Check cache - final long now = System.currentTimeMillis(); - if ( cachedInfo != null && ( now - cacheTimestamp ) < CACHE_TIMEOUT_MS ) - return cachedInfo; - - // Try detection methods in order of reliability - final CondaInfo info = detectCondaInfo(); - - if ( info != null ) - { - cachedInfo = info; - cacheTimestamp = now; - return info; - } - - throw new CondaNotFoundException( - "Could not find conda installation. Please ensure conda is installed and initialized.\n" + - "Suggestions:\n" + - " 1. Install Anaconda or Miniconda\n" + - " 2. Run 'conda init' in your terminal\n" + - " 3. Restart your application" ); - } - - /** - * Clear the cache (useful for testing or after conda installation changes) - */ - public static void clearCache() - { - cachedInfo = null; - cacheTimestamp = 0; - } - - // ========== Detection Logic ========== - - private static CondaInfo detectCondaInfo() - { - IJ.log( "Starting conda detection..." ); - - // Method 1: Use CONDA_EXE environment variable (fastest, most reliable) - CondaInfo info = detectFromEnvironment(); - if ( info != null ) - return info; - - // Method 2: Parse shell config files (critical for macOS/Linux GUI apps) - IJ.log( "" ); - if ( isMac() || isLinux() ) - { - info = detectFromShellConfig(); - if ( info != null ) - return info; - } - else - { - IJ.log( "Method 2: Parsing shell configuration files..." ); - IJ.log( " Skipped (only applicable on macOS/Linux)" ); - } - - // Method 3: Search in PATH - IJ.log( "" ); - info = detectFromPath(); - if ( info != null ) - return info; - - // Method 4: Check common installation locations (fallback) - IJ.log( "" ); - info = detectFromCommonLocations(); - if ( info != null ) - return info; - - IJ.log( "" ); - IJ.log( "Failed to detect conda installation" ); - return null; - } - - /** - * Method 1: Detect from CONDA_EXE environment variable - */ - private static CondaInfo detectFromEnvironment() - { - IJ.log( "Method 1: Checking CONDA_EXE environment variable..." ); - - // Check CONDA_EXE first - String condaExe = System.getenv( "CONDA_EXE" ); - - // Also check for _CONDA_EXE (sometimes set by conda) - if ( condaExe == null || condaExe.isEmpty() ) - condaExe = System.getenv( "_CONDA_EXE" ); - - if ( condaExe != null && new File( condaExe ).exists() ) - { - // Resolve symlinks/aliases - condaExe = resolveRealExecutable( condaExe ); - - if ( condaExe != null ) - { - final String rootPrefix = deriveRootPrefix( condaExe ); - final String version = getCondaVersion( condaExe ); - - if ( rootPrefix != null && version != null ) - { - IJ.log( " Found conda via CONDA_EXE: " + condaExe ); - return new CondaInfo( condaExe, rootPrefix, version ); - } - } - } - - IJ.log( " CONDA_EXE not set or invalid" ); - return null; - } - - /** - * Method 2: Parse shell config files to find conda - * Essential for macOS/Linux GUI applications that don't inherit shell - * environment - */ - private static CondaInfo detectFromShellConfig() - { - IJ.log( "Method 2: Parsing shell configuration files..." ); - final String home = System.getProperty( "user.home" ); - - // Check shell config files in order of likelihood - final String[] configFiles = { - ".zshrc", // macOS default since Catalina - ".bash_profile", // macOS bash - ".bashrc", // Linux bash - ".profile", // Generic - ".config/fish/config.fish" // Fish shell - }; - - for ( final String configFile : configFiles ) - { - final Path configPath = Paths.get( home, configFile ); - - if ( Files.exists( configPath ) ) - { - try - { - final String content = new String( Files.readAllBytes( configPath ) ); - - // Look for conda/mamba/micromamba references - // Check for explicit strings OR any conda-related content - if ( content.contains( "conda.sh" ) || - content.contains( "mamba.sh" ) || - content.contains( "conda initialize" ) || - content.contains( "conda" ) || - content.contains( "mamba" ) || - content.contains( "micromamba" ) ) - { - IJ.log( " Checking: " + configFile ); - String condaExe = extractCondaExeFromConfig( content, configPath ); - - if ( condaExe != null && new File( condaExe ).exists() ) - { - // Resolve symlinks/aliases - condaExe = resolveRealExecutable( condaExe ); - - final String rootPrefix = deriveRootPrefix( condaExe ); - final String version = getCondaVersion( condaExe ); - - if ( rootPrefix != null && version != null ) - { - IJ.log( " Found conda by parsing: " + configFile ); - return new CondaInfo( condaExe, rootPrefix, version ); - } - else - { - IJ.log( " Found executable but failed to validate: " + condaExe ); - } - } - } - } - catch ( final IOException e ) - { - // Can't read this config file, try next - IJ.log( " Could not read: " + configFile ); - } - } - } - - IJ.log( " No conda found in shell config files" ); - return null; - } - - /** - * Extract conda/mamba/micromamba executable path from shell config content - * Handles conda, mamba, mambaforge, miniforge, micromamba installations - */ - private static String extractCondaExeFromConfig( final String content, final Path configPath ) - { - // Method 1: Look for __conda_setup or __mamba_setup pattern - // Pattern: __conda_setup="$('/Users/username/mambaforge/bin/conda' - // 'shell.zsh' 'hook' 2> /dev/null)" - Pattern pattern = Pattern.compile( - "__(conda|mamba)_setup=[\"']\\$\\([\"']([^\"']+/(conda|mamba|micromamba))[\"']" ); - Matcher matcher = pattern.matcher( content ); - if ( matcher.find() ) - { - final String condaExe = matcher.group( 2 ); - if ( new File( condaExe ).exists() ) - { - IJ.log( " Found via setup variable: " + condaExe ); - return condaExe; - } - } - - // Method 2: Look for explicit CONDA_EXE, MAMBA_EXE, or - // MAMBA_ROOT_PREFIX export - pattern = Pattern.compile( - "export (CONDA_EXE|MAMBA_EXE|MAMBA_ROOT_PREFIX)=['\"]?([^'\"\\s]+)['\"]?" ); - matcher = pattern.matcher( content ); - while ( matcher.find() ) - { - final String varName = matcher.group( 1 ); - String exe = matcher.group( 2 ); - - // If MAMBA_ROOT_PREFIX, append /bin/micromamba - if ( varName.equals( "MAMBA_ROOT_PREFIX" ) ) - exe = exe + "/bin/micromamba"; - - if ( new File( exe ).exists() ) - { - IJ.log( " Found via " + varName + ": " + exe ); - return exe; - } - } - - // Method 3: Look for conda.sh, mamba.sh, or micromamba.sh source with - // path - pattern = Pattern.compile( - "[.\\s][\\s\"']*([^\"']+)/(etc/profile\\.d/(conda|mamba|micromamba)\\.sh)[\"']" ); - matcher = pattern.matcher( content ); - if ( matcher.find() ) - { - final String rootPrefix = matcher.group( 1 ); - // Try conda first, then mamba, then micromamba - final String condaExe = rootPrefix + "/bin/conda"; - if ( new File( condaExe ).exists() ) - { - IJ.log( " Found via profile.d: " + condaExe ); - return condaExe; - } - final String mambaExe = rootPrefix + "/bin/mamba"; - if ( new File( mambaExe ).exists() ) - { - IJ.log( " Found via profile.d: " + mambaExe ); - return mambaExe; - } - final String microExe = rootPrefix + "/bin/micromamba"; - if ( new File( microExe ).exists() ) - { - IJ.log( " Found via profile.d: " + microExe ); - return microExe; - } - } - - // Method 4: Look for PATH export with conda/mamba/micromamba - pattern = Pattern.compile( - "export PATH=[\"']([^\"':]+/(mambaforge|miniconda|anaconda|miniforge|micromamba)[^\"':]*)[\"':]" ); - matcher = pattern.matcher( content ); - if ( matcher.find() ) - { - final String binDir = matcher.group( 1 ); - // Try conda, mamba, then micromamba - final String condaExe = binDir + "/conda"; - if ( new File( condaExe ).exists() ) - { - IJ.log( " Found via PATH export: " + condaExe ); - return condaExe; - } - final String mambaExe = binDir + "/mamba"; - if ( new File( mambaExe ).exists() ) - { - IJ.log( " Found via PATH export: " + mambaExe ); - return mambaExe; - } - final String microExe = binDir + "/micromamba"; - if ( new File( microExe ).exists() ) - { - IJ.log( " Found via PATH export: " + microExe ); - return microExe; - } - } - - // Method 5: Look for any mamba/conda/micromamba bin directory in path - // assignments - // Pattern: path+=('/Users/username/mambaforge/bin') - // Pattern: export PATH="/Users/username/Applications/bin:$PATH" (where - // bin contains micromamba) - pattern = Pattern.compile( - "(?:path[+=]+|export PATH=)[^\"']*[\"']([^\"':]+/bin)[\"':]" ); - matcher = pattern.matcher( content ); - while ( matcher.find() ) - { - final String binDir = matcher.group( 1 ); - - // Check for conda, mamba, or micromamba in this bin directory - final String condaExe = binDir + "/conda"; - if ( new File( condaExe ).exists() ) - { - IJ.log( " Found via path assignment: " + condaExe ); - return condaExe; - } - final String mambaExe = binDir + "/mamba"; - if ( new File( mambaExe ).exists() ) - { - IJ.log( " Found via path assignment: " + mambaExe ); - return mambaExe; - } - final String microExe = binDir + "/micromamba"; - if ( new File( microExe ).exists() ) - { - IJ.log( " Found via path assignment: " + microExe ); - return microExe; - } - } - - return null; - } - - /** - * Method 3: Search for conda in system PATH - */ - private static CondaInfo detectFromPath() - { - IJ.log( "Method 3: Searching system PATH..." ); - String condaExe = findInPath( "conda" ); - if ( condaExe != null ) - { - // Resolve symlinks/aliases - condaExe = resolveRealExecutable( condaExe ); - - final String rootPrefix = deriveRootPrefix( condaExe ); - final String version = getCondaVersion( condaExe ); - - if ( rootPrefix != null && version != null ) - { - IJ.log( " Found conda in PATH: " + condaExe ); - return new CondaInfo( condaExe, rootPrefix, version ); - } - } - IJ.log( " Conda not found in PATH" ); - return null; - } - - /** - * Method 4: Check common installation directories - */ - private static CondaInfo detectFromCommonLocations() - { - IJ.log( "Method 4: Checking common installation locations..." ); - final List< String > locations = getCommonCondaLocations(); - - for ( final String location : locations ) - { - final File dir = new File( location ); - if ( !dir.exists() || !dir.isDirectory() ) - continue; - - String condaExe = buildCondaExecutablePath( location ); - if ( condaExe != null && new File( condaExe ).exists() ) - { - // Resolve symlinks/aliases - condaExe = resolveRealExecutable( condaExe ); - - final String version = getCondaVersion( condaExe ); - if ( version != null ) - { - IJ.log( " Found conda at: " + location ); - return new CondaInfo( condaExe, location, version ); - } - } - } - - IJ.log( " No conda found in common locations" ); - return null; - } - - // ========== Helper Methods ========== - - /** - * Resolve symlinks and get the actual executable path This ensures we know - * what conda/mamba/micromamba we're really using - */ - private static String resolveRealExecutable( final String executablePath ) - { - if ( executablePath == null ) - return null; - - try - { - final File file = new File( executablePath ); - - // Resolve symlinks using canonical path - String realPath = file.getCanonicalPath(); - - // On Unix, also try readlink to be thorough - if ( !isWindows() && Files.isSymbolicLink( Paths.get( executablePath ) ) ) - { - try - { - Path resolved = Files.readSymbolicLink( Paths.get( executablePath ) ); - if ( !resolved.isAbsolute() ) - { - // Resolve relative symlink - final Path parent = Paths.get( executablePath ).getParent(); - resolved = parent.resolve( resolved ).normalize(); - } - realPath = resolved.toString(); - - // Log symlink resolution - if ( !executablePath.equals( realPath ) ) - IJ.log( " Resolved symlink: " + executablePath + " -> " + realPath ); - } - catch ( final IOException e ) - { - // Fall back to canonical path - } - } - - return realPath; - } - catch ( final IOException e ) - { - IJ.log( " Could not resolve path: " + executablePath ); - return executablePath; // Return original if resolution fails - } - } - - /** - * Check if the executable is micromamba (even if aliased as conda) This is - * used to determine command compatibility - */ - private static boolean isMicromambaExecutable( final String condaExePath ) - { - // Quick check first - if path contains micromamba, it definitely is - if ( condaExePath != null && condaExePath.toLowerCase().contains( "micromamba" ) ) - return true; - - // Otherwise, run --version to check what it actually is - try - { - final List< String > command = buildSimpleCommand( condaExePath, "--version" ); - - final ProcessBuilder pb = new ProcessBuilder( command ); - pb.redirectErrorStream( true ); - final Process process = pb.start(); - - final String output = readProcessOutput( process ); - final boolean completed = process.waitFor( 5, TimeUnit.SECONDS ); - - if ( completed && process.exitValue() == 0 && output != null ) - { - // Check if output contains "micromamba" - return output.toLowerCase().contains( "micromamba" ); - } - } - catch ( final Exception e ) - { - // If we can't determine, assume it's not micromamba - } - - return false; - } - - /** - * Derive conda root prefix from executable path For micromamba, tries to - * get actual root prefix from micromamba info - */ - private static String deriveRootPrefix( final String condaExePath ) - { - try - { - final Path path = Paths.get( condaExePath ).toAbsolutePath().normalize(); - final String exeName = path.getFileName().toString(); - - // Special handling for micromamba - query it directly - if ( exeName.contains( "micromamba" ) ) - { - final String microRootPrefix = getMicromambaRootPrefix( condaExePath ); - if ( microRootPrefix != null ) - { - IJ.log( " Micromamba root prefix from 'micromamba info': " + microRootPrefix ); - return microRootPrefix; - } - } - - if ( isWindows() ) - { - // Windows logic (unchanged) - final String pathStr = path.toString(); - if ( pathStr.contains( "\\Library\\bin\\conda" ) ) - { - Path parent = path.getParent(); - if ( parent != null ) - { - parent = parent.getParent(); - if ( parent != null ) - { - final Path rootPrefix = parent.getParent(); - if ( rootPrefix != null ) - return rootPrefix.toString(); - } - } - } - else - { - final Path parent = path.getParent(); - if ( parent != null ) - { - final Path rootPrefix = parent.getParent(); - if ( rootPrefix != null ) - return rootPrefix.toString(); - } - } - } - else - { - // Unix: typically /path/to/installation/bin/conda - final Path parent = path.getParent(); // bin - if ( parent != null ) - { - // installation root - final Path rootPrefix = parent.getParent(); - if ( rootPrefix != null ) - { - // For micromamba in generic bin (like /usr/local/bin) - // Check if this looks like a proper conda root - if ( exeName.contains( "micromamba" ) ) - { - final File envsDir = new File( rootPrefix.toFile(), "envs" ); - final File pkgsDir = new File( rootPrefix.toFile(), "pkgs" ); - - if ( !envsDir.exists() && !pkgsDir.exists() ) - { - // Not a proper conda root, try default - // micromamba locations - final String home = System.getProperty( "user.home" ); - final String[] defaultMicroRoots = { - home + "/micromamba", - home + "/.mamba", - home + "/.local/share/mamba" - }; - - for ( final String defaultRoot : defaultMicroRoots ) - { - final File defaultEnvs = new File( defaultRoot, "envs" ); - if ( defaultEnvs.exists() ) - { - IJ.log( " Using default micromamba root: " + defaultRoot ); - return defaultRoot; - } - } - - // Fall back to ~/micromamba even if it doesn't - // exist yet - final String fallback = home + "/micromamba"; - IJ.log( " Using fallback micromamba root: " + fallback ); - return fallback; - } - } - - return rootPrefix.toString(); - } - } - } - } - catch ( final Exception e ) - { - IJ.log( "Failed to derive root prefix from " + condaExePath + ": " + e.getMessage() ); - } - - return null; - } - - /** - * Get micromamba root prefix by running 'micromamba info' - */ - private static String getMicromambaRootPrefix( final String micromambaPath ) - { - try - { - final List< String > command = buildSimpleCommand( micromambaPath, "info" ); - - final ProcessBuilder pb = new ProcessBuilder( command ); - pb.redirectErrorStream( true ); - final Process process = pb.start(); - - final String output = readProcessOutput( process ); - final boolean completed = process.waitFor( 10, TimeUnit.SECONDS ); - - if ( completed && process.exitValue() == 0 && output != null ) - { - // Look for "base environment : /path/to/root" - final Pattern pattern = Pattern.compile( "base environment\\s*:\\s*([^\\s]+)" ); - final Matcher matcher = pattern.matcher( output ); - if ( matcher.find() ) - { - final String rootPrefix = matcher.group( 1 ); - if ( new File( rootPrefix ).exists() ) - return rootPrefix; - } - - // Alternative: look for "root prefix : /path/to/root" - final Pattern pattern2 = Pattern.compile( "root prefix\\s*:\\s*([^\\s]+)" ); - final Matcher matcher2 = pattern2.matcher( output ); - if ( matcher2.find() ) - { - final String rootPrefix = matcher2.group( 1 ); - if ( new File( rootPrefix ).exists() ) - return rootPrefix; - } - } - } - catch ( final Exception e ) - { - // Failed to query micromamba, will fall back to other methods - } - - return null; - } - - /** - * Get conda version by running `conda --version` or `micromamba --version` - */ - private static String getCondaVersion( final String condaExePath ) - { - try - { - final List< String > command = buildSimpleCommand( condaExePath, "--version" ); - - final ProcessBuilder pb = new ProcessBuilder( command ); - pb.redirectErrorStream( true ); - final Process process = pb.start(); - - final String output = readProcessOutput( process ); - final boolean completed = process.waitFor( 10, TimeUnit.SECONDS ); - - if ( completed && process.exitValue() == 0 && output != null ) - { - // Output format: "conda 23.7.4", "mamba 1.5.8", or "micromamba - // 1.5.8" - final String version = output.trim() - .replace( "conda", "" ) - .replace( "mamba", "" ) - .replace( "micromamba", "" ) - .trim(); - return version; - } - } - catch ( final Exception e ) - { - IJ.log( "Failed to get conda version from " + condaExePath + ": " + e.getMessage() ); - } - - return null; - } - - /** - * Find executable in system PATH - */ - private static String findInPath( final String executable ) - { - final String[] command = isWindows() - ? new String[] { "where", executable } - : new String[] { "which", executable }; - - try - { - final ProcessBuilder pb = new ProcessBuilder( command ); - pb.redirectErrorStream( true ); - final Process process = pb.start(); - - final String output = readProcessOutput( process ); - final boolean completed = process.waitFor( 5, TimeUnit.SECONDS ); - - // Check exit code - 'where' returns non-zero when not found - if ( !completed ) - { - IJ.log( " Command timed out searching for '" + executable + "'" ); - return null; - } - - if ( process.exitValue() != 0 ) - { - IJ.log( " '" + executable + "' not found in PATH" ); - return null; - } - - if ( output != null && !output.isEmpty() ) - { - // 'where' on Windows may return multiple results - final String[] lines = output.split( "\n" ); - - for ( final String line : lines ) - { - String path = line.trim(); - - // Skip empty lines - if ( path.isEmpty() ) - continue; - - // Skip error messages from Windows 'where' command - if ( path.startsWith( "INFO:" ) || - path.startsWith( "ERROR:" ) || - path.startsWith( "WARNING:" ) || - path.contains( "Could not find" ) ) - { - IJ.log( " Skipping error message: " + path ); - continue; - } - - // Validate that this looks like a real path - if ( !isValidPath( path ) ) - { - IJ.log( " Skipping invalid path format: " + path ); - continue; - } - - // On Windows, skip conda in Library\bin (it's a helper - // script) - if ( isWindows() && path.contains( "\\Library\\bin\\conda" ) ) - { - IJ.log( " Skipping helper script: " + path ); - continue; - } - - // Verify the file actually exists - if ( !new File( path ).exists() ) - { - IJ.log( " Skipping non-existent path: " + path ); - continue; - } - - // Resolve symlinks on Unix - if ( !isWindows() ) - { - try - { - path = new File( path ).getCanonicalPath(); - } - catch ( final IOException e ) - { - // Keep original path - } - } - - return path; - } - } - } - catch ( final Exception e ) - { - IJ.log( " Error searching PATH: " + e.getMessage() ); - } - - return null; - } - - /** - * Validate that a string looks like a valid file path - */ - private static boolean isValidPath( final String path ) - { - if ( path == null || path.isEmpty() ) - return false; - - // Check for obvious non-path patterns (error messages) - if ( path.startsWith( "INFO:" ) || - path.startsWith( "ERROR:" ) || - path.startsWith( "WARNING:" ) || - path.toLowerCase().contains( "could not find" ) || - path.toLowerCase().contains( "not recognized" ) ) - return false; - - if ( isWindows() ) - { - // Windows paths should contain : (drive letter) or start with \\ - // (UNC) - // or at least contain \ (subdirectory) - if ( !path.contains( ":" ) && !path.startsWith( "\\\\" ) && !path.contains( "\\" ) ) - { - // Might be a relative path - check if file exists - if ( !new File( path ).exists() ) - return false; - } - - // Check for invalid Windows path characters that indicate error - // messages - final String invalidChars = "<>|\u0000"; - for ( int i = 0; i < invalidChars.length(); i++ ) - { - if ( path.indexOf( invalidChars.charAt( i ) ) >= 0 ) - return false; - } - } - else - { - // Unix paths should start with / (absolute) or contain / (relative) - // Simple heuristic: if it's long, has spaces, but no /, probably an - // error message - if ( !path.contains( "/" ) && path.contains( " " ) && path.length() > 30 ) - return false; - } - - return true; - } - - /** - * Build path to conda/mamba/micromamba executable from installation - * directory - */ - private static String buildCondaExecutablePath( final String installDir ) - { - if ( isWindows() ) - { - // Try Scripts\conda.exe first (most common for miniconda/anaconda) - String scriptsPath = Paths.get( installDir, "Scripts", "conda.exe" ).toString(); - if ( new File( scriptsPath ).exists() ) - return scriptsPath; - - // Try Scripts\mamba.exe - scriptsPath = Paths.get( installDir, "Scripts", "mamba.exe" ).toString(); - if ( new File( scriptsPath ).exists() ) - return scriptsPath; - - // Try Scripts\micromamba.exe - scriptsPath = Paths.get( installDir, "Scripts", "micromamba.exe" ).toString(); - if ( new File( scriptsPath ).exists() ) - return scriptsPath; - - // Try condabin\conda.bat (alternative for some installations) - final String condabinPath = Paths.get( installDir, "condabin", "conda.bat" ).toString(); - if ( new File( condabinPath ).exists() ) - return condabinPath; - - // Try miniforge/mambaforge location: Scripts\conda.bat - scriptsPath = Paths.get( installDir, "Scripts", "conda.bat" ).toString(); - if ( new File( scriptsPath ).exists() ) - return scriptsPath; - - // DO NOT check Library\bin - that's a helper script, not main conda - } - else - { - // Unix: bin/conda, bin/mamba, or bin/micromamba - String binPath = Paths.get( installDir, "bin", "conda" ).toString(); - if ( new File( binPath ).exists() ) - return binPath; - - binPath = Paths.get( installDir, "bin", "mamba" ).toString(); - if ( new File( binPath ).exists() ) - return binPath; - - binPath = Paths.get( installDir, "bin", "micromamba" ).toString(); - if ( new File( binPath ).exists() ) - return binPath; - } - - return null; // No valid conda executable found - } - - /** - * Get common conda installation locations based on platform - */ - private static List< String > getCommonCondaLocations() - { - final List< String > paths = new ArrayList<>(); - final String home = System.getProperty( "user.home" ); - - if ( isWindows() ) - { - // Windows locations - User installations - paths.add( home + "\\miniconda3" ); - paths.add( home + "\\anaconda3" ); - paths.add( home + "\\Miniconda3" ); - paths.add( home + "\\Anaconda3" ); - paths.add( home + "\\miniforge3" ); - paths.add( home + "\\mambaforge" ); - paths.add( home + "\\micromamba" ); - - // AppData\Local installations (common for miniforge) - paths.add( home + "\\AppData\\Local\\miniforge3" ); - paths.add( home + "\\AppData\\Local\\mambaforge" ); - paths.add( home + "\\AppData\\Local\\miniconda3" ); - paths.add( home + "\\AppData\\Local\\anaconda3" ); - paths.add( home + "\\AppData\\Local\\micromamba" ); - - // System-wide installations - paths.add( "C:\\ProgramData\\miniconda3" ); - paths.add( "C:\\ProgramData\\anaconda3" ); - paths.add( "C:\\ProgramData\\miniforge3" ); - paths.add( "C:\\ProgramData\\mambaforge" ); - paths.add( "C:\\ProgramData\\micromamba" ); - paths.add( "C:\\tools\\miniconda3" ); - paths.add( "C:\\tools\\anaconda3" ); - paths.add( "C:\\tools\\micromamba" ); - - // Check other drives - final String[] drives = { "D:", "E:", "F:" }; - for ( final String drive : drives ) - { - paths.add( drive + "\\ProgramData\\miniconda3" ); - paths.add( drive + "\\ProgramData\\anaconda3" ); - paths.add( drive + "\\ProgramData\\miniforge3" ); - paths.add( drive + "\\ProgramData\\micromamba" ); - } - - } - else if ( isMac() ) - { - // macOS locations - paths.add( home + "/miniconda3" ); - paths.add( home + "/anaconda3" ); - paths.add( home + "/miniforge3" ); - paths.add( home + "/mambaforge" ); - paths.add( home + "/micromamba" ); - paths.add( home + "/Applications" ); - paths.add( "/opt/miniconda3" ); - paths.add( "/opt/anaconda3" ); - paths.add( "/opt/conda" ); - paths.add( "/opt/micromamba" ); - paths.add( "/usr/local/miniconda3" ); - paths.add( "/usr/local/anaconda3" ); - paths.add( "/usr/local/micromamba" ); - paths.add( "/usr/local/bin" ); - - } - else - { - // Linux locations - paths.add( home + "/miniconda3" ); - paths.add( home + "/anaconda3" ); - paths.add( home + "/miniforge3" ); - paths.add( home + "/mambaforge" ); - paths.add( home + "/micromamba" ); - paths.add( home + "/.local" ); - paths.add( "/opt/conda" ); - paths.add( "/opt/miniconda3" ); - paths.add( "/opt/anaconda3" ); - paths.add( "/opt/micromamba" ); - paths.add( "/usr/local/miniconda3" ); - paths.add( "/usr/local/anaconda3" ); - paths.add( "/usr/local/micromamba" ); - } - - return paths; - } - - /** - * Build command list for executing conda - */ - private static List< String > buildSimpleCommand( final String condaExePath, final String... args ) - { - final List< String > command = new ArrayList<>(); - - if ( isWindows() && !condaExePath.endsWith( ".exe" ) ) - { - command.add( "cmd.exe" ); - command.add( "/c" ); - } - - command.add( condaExePath ); - command.addAll( Arrays.asList( args ) ); - - return command; - } - - /** - * Read all output from a process - */ - private static String readProcessOutput( final Process process ) throws IOException - { - final StringBuilder output = new StringBuilder(); - try (final BufferedReader reader = new BufferedReader( - new InputStreamReader( process.getInputStream() ) )) - { - String line; - while ( ( line = reader.readLine() ) != null ) - { - if ( output.length() > 0 ) - output.append( "\n" ); - output.append( line ); - } - } - return output.toString(); - } - - // ========== Platform Detection ========== - - private static boolean isWindows() - { - return System.getProperty( "os.name" ).toLowerCase().contains( "win" ); - } - - private static boolean isMac() - { - final String os = System.getProperty( "os.name" ).toLowerCase(); - return os.contains( "mac" ) || os.contains( "darwin" ); - } - - private static boolean isLinux() - { - return System.getProperty( "os.name" ).toLowerCase().contains( "linux" ); - } - - // ========== Environment Discovery ========== - - /** - * Simple JSON parser for conda env list output - *

    - * Format: {"envs": ["/path/to/env1", "/path/to/env2"]} - */ - private static List< CondaEnvironment > parseEnvironmentsJson( final String json ) - { - final List< CondaEnvironment > environments = new ArrayList<>(); - - // Extract paths from JSON (simple regex-based parsing) - final int envsStart = json.indexOf( "\"envs\":" ); - if ( envsStart != -1 ) - { - final int arrayStart = json.indexOf( "[", envsStart ); - final int arrayEnd = json.indexOf( "]", arrayStart ); - - if ( arrayStart != -1 && arrayEnd != -1 ) - { - final String envsArray = json.substring( arrayStart + 1, arrayEnd ); - final String[] paths = envsArray.split( "," ); - - for ( String path : paths ) - { - path = path.trim() - .replace( "\"", "" ) - .replace( "\\\\", "\\" ); - - if ( !path.isEmpty() ) - { - final File envDir = new File( path ); - String name = envDir.getName(); - - // Base environment has special name - if ( name.equals( "miniconda3" ) || name.equals( "anaconda3" ) || - name.equals( "mambaforge" ) || name.equals( "miniforge3" ) ) - name = "base"; - - environments.add( new CondaEnvironment( name, path ) ); - } - } - } - } - return environments; - } - - /** - * Find all conda environments - * - * @return List of conda environments - * @throws CondaNotFoundException if conda is not found - */ - public static List< CondaEnvironment > findAllEnvironments() throws CondaNotFoundException - { - final String condaPath = detect().getCondaExecutable(); - List< CondaEnvironment > environments = new ArrayList<>(); - try - { - final List< String > command = new ArrayList<>(); - - if ( isWindows() && !condaPath.endsWith( ".exe" ) ) - { - command.add( "cmd.exe" ); - command.add( "/c" ); - } - - command.add( condaPath ); - command.add( "env" ); - command.add( "list" ); - command.add( "--json" ); - - final ProcessBuilder pb = new ProcessBuilder( command ); - pb.redirectErrorStream( true ); - final Process process = pb.start(); - - // Read JSON output - final StringBuilder json = new StringBuilder(); - try (final BufferedReader reader = new BufferedReader( - new InputStreamReader( process.getInputStream() ) )) - { - String line; - while ( ( line = reader.readLine() ) != null ) - json.append( line ); - } - process.waitFor( 30, TimeUnit.SECONDS ); - - // Parse JSON (simple parsing - for production use a JSON library) - environments = parseEnvironmentsJson( json.toString() ); - - } - catch ( final IOException | InterruptedException e ) - { - IJ.log( "Failed to list conda environments: " + e.getMessage() ); - } - - return environments; - } - - /** - * Check if a specific environment exists - * - * @param envName Name of the environment to check - * @return true if the environment exists - * @throws CondaNotFoundException if conda is not found - */ - public static boolean environmentExists( final String envName ) throws CondaNotFoundException - { - return findAllEnvironments().stream() - .anyMatch( env -> env.getName().equals( envName ) ); - } - - /** - * Get path to a specific environment - * - * @param envName Name of the environment - * @return Path to the environment, or null if not found - * @throws CondaNotFoundException if conda is not found - */ - public static String getEnvironmentPath( final String envName ) throws CondaNotFoundException - { - return findAllEnvironments().stream() - .filter( env -> env.getName().equals( envName ) ) - .map( CondaEnvironment::getPath ) - .findFirst() - .orElse( null ); - } - - // ========== Demo Main Method ========== - - public static void main( final String[] args ) - { - diagnose(); - } - - public static void diagnose() - { - - IJ.log( "╔════════════════════════════════════════╗" ); - IJ.log( "║ Conda Detection Diagnosis System ║" ); - IJ.log( "╚════════════════════════════════════════╝" ); - IJ.log( "" ); - - IJ.log( "Starting conda detection process..." ); - IJ.log( "" ); - - // Show system information - IJ.log( "System Information:" ); - IJ.log( " OS: " + System.getProperty( "os.name" ) ); - IJ.log( " Architecture: " + System.getProperty( "os.arch" ) ); - IJ.log( " User Home: " + System.getProperty( "user.home" ) ); - IJ.log( "" ); - - // Try each detection method explicitly for the demo - IJ.log( "Attempting Detection Methods:" ); - IJ.log( "" ); - - // Method 1: Environment variable - IJ.log( "Method 1: Checking CONDA_EXE environment variable..." ); - final String condaEnvVar = System.getenv( "CONDA_EXE" ); - if ( condaEnvVar != null ) - { - IJ.log( " Found: " + condaEnvVar ); - if ( new File( condaEnvVar ).exists() ) - IJ.log( " ✓ File exists and is accessible" ); - else - IJ.log( " ✗ File does not exist" ); - } - else - { - IJ.log( " CONDA_EXE not set" ); - } - IJ.log( "" ); - - // Method 2: Shell config (only on Mac/Linux) - if ( isMac() || isLinux() ) - { - IJ.log( "Method 2: Parsing shell configuration files..." ); - final String home = System.getProperty( "user.home" ); - final String[] configFiles = { - ".zshrc", - ".bash_profile", - ".bashrc", - ".profile", - ".config/fish/config.fish" - }; - - boolean foundInConfig = false; - for ( final String configFile : configFiles ) - { - final Path configPath = Paths.get( home, configFile ); - if ( Files.exists( configPath ) ) - { - IJ.log( " Checking: " + configFile ); - try - { - final String content = new String( Files.readAllBytes( configPath ) ); - if ( content.contains( "conda" ) || content.contains( "mamba" ) ) - { - IJ.log( " → Contains conda/mamba references" ); - final String extracted = extractCondaExeFromConfig( content, configPath ); - if ( extracted != null ) - { - IJ.log( " ✓ Extracted path: " + extracted ); - foundInConfig = true; - } - else - { - IJ.log( " ✗ Could not extract conda path" ); - } - } - else - { - IJ.log( " No conda references found" ); - } - } - catch ( final IOException e ) - { - IJ.log( " ✗ Could not read file" ); - } - } - } - if ( !foundInConfig ) - IJ.log( " No conda installation found in shell configs" ); - IJ.log( "" ); - } - else - { - IJ.log( "Method 2: Parsing shell configuration files..." ); - IJ.log( " Skipped (only applicable on macOS/Linux)" ); - IJ.log( "" ); - } - - // Method 3: PATH search - IJ.log( "Method 3: Searching system PATH..." ); - final String pathResult = findInPath( "conda" ); - if ( pathResult != null ) - IJ.log( " ✓ Found in PATH: " + pathResult ); - else - IJ.log( " Conda not found in PATH" ); - IJ.log( "" ); - - // Method 4: Common locations - IJ.log( "Method 4: Checking common installation locations..." ); - final List< String > locations = getCommonCondaLocations(); - IJ.log( " Checking " + locations.size() + " potential locations:" ); - - boolean foundInCommon = false; - for ( final String location : locations ) - { - final String condaExe = buildCondaExecutablePath( location ); - if ( condaExe != null && new File( condaExe ).exists() ) - { - IJ.log( " ✓ Found: " + condaExe ); - foundInCommon = true; - } - } - if ( !foundInCommon ) - IJ.log( " No conda installation found in common locations" ); - IJ.log( "" ); - - // Now run the actual detection - IJ.log( "═══════════════════════════════════════════" ); - IJ.log( "" ); - IJ.log( "Running integrated detection..." ); - IJ.log( "" ); - - try - { - final CondaInfo info = detect(); - - IJ.log( "" ); - IJ.log( "✅ Conda detected successfully!" ); - IJ.log( "" ); - IJ.log( "Detection Results:" ); - IJ.log( " Conda Executable: " + info.getCondaExecutable() ); - IJ.log( " Root Prefix: " + info.getRootPrefix() ); - IJ.log( " Version: " + info.getVersion() ); - IJ.log( " Is Micromamba: " + info.isMicromamba() ); - - // Validate the installation - IJ.log( "" ); - IJ.log( "Validating installation..." ); - final File exeFile = new File( info.getCondaExecutable() ); - IJ.log( " Executable exists: " + exeFile.exists() ); - IJ.log( " Executable size: " + exeFile.length() + " bytes" ); - IJ.log( " Can execute: " + exeFile.canExecute() ); - - final File rootDir = new File( info.getRootPrefix() ); - IJ.log( " Root prefix exists: " + rootDir.exists() ); - IJ.log( " Root prefix is dir: " + rootDir.isDirectory() ); - - // List all environments - IJ.log( "" ); - IJ.log( "═══════════════════════════════════════════" ); - IJ.log( "" ); - IJ.log( "Discovering conda environments..." ); - - final List< CondaEnvironment > envs = findAllEnvironments(); - - if ( envs.isEmpty() ) - { - IJ.log( "" ); - IJ.log( "⚠ No environments found" ); - } - else - { - IJ.log( "" ); - IJ.log( "📦 Found " + envs.size() + " environment(s):" ); - IJ.log( "" ); - for ( int i = 0; i < envs.size(); i++ ) - { - final CondaEnvironment env = envs.get( i ); - IJ.log( " " + ( i + 1 ) + ". " + env.getName() ); - IJ.log( " Path: " + env.getPath() ); - - // Check if environment is valid - final File envDir = new File( env.getPath() ); - if ( envDir.exists() ) - { - final File envBin = new File( env.getPath(), - isWindows() ? "python.exe" : "bin/python" ); - if ( envBin.exists() ) - IJ.log( " Status: ✓ Valid" ); - else - IJ.log( " Status: ⚠ Missing Python" ); - } - else - { - IJ.log( " Status: ✗ Path does not exist" ); - } - } - - // Test environment existence check - if ( !envs.isEmpty() ) - { - final int testIndex = new Random().nextInt( envs.size() ); - final String testEnv = envs.get( testIndex ).getName(); - - IJ.log( "" ); - IJ.log( "═══════════════════════════════════════════" ); - IJ.log( "" ); - IJ.log( "Testing environment lookup for: '" + testEnv + "'" ); - - if ( environmentExists( testEnv ) ) - { - final String envPath = getEnvironmentPath( testEnv ); - IJ.log( " ✓ Environment found" ); - IJ.log( " Path: " + envPath ); - } - else - { - IJ.log( " ✗ Environment not found (unexpected)" ); - } - } - } - - IJ.log( "" ); - IJ.log( "═══════════════════════════════════════════" ); - IJ.log( "" ); - IJ.log( "✅ All checks completed successfully!" ); - - } - catch ( final CondaNotFoundException e ) - { - IJ.log( "" ); - IJ.log( "❌ Conda Detection Failed" ); - IJ.log( "" ); - IJ.log( e.getMessage() ); - IJ.log( "" ); - IJ.log( "═══════════════════════════════════════════" ); - IJ.log( "" ); - IJ.log( "Troubleshooting Tips:" ); - IJ.log( " • Ensure conda/miniconda/anaconda is installed" ); - IJ.log( " • Run 'conda init' in your terminal" ); - IJ.log( " • Check that conda is in your PATH" ); - IJ.log( " • Try running 'conda --version' in terminal" ); - IJ.log( " • If using mambaforge, ensure it's properly installed" ); - } - } -} diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/condapath/CondaPathConfigCommand.java b/src/main/java/fiji/plugin/trackmate/util/cli/condapath/CondaPathConfigCommand.java deleted file mode 100644 index a412a14a5..000000000 --- a/src/main/java/fiji/plugin/trackmate/util/cli/condapath/CondaPathConfigCommand.java +++ /dev/null @@ -1,436 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util.cli.condapath; - -import static fiji.plugin.trackmate.gui.Icons.TRACKMATE_ICON; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.Font; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Map; - -import javax.swing.BorderFactory; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JFileChooser; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import javax.swing.JTextField; -import javax.swing.SwingUtilities; -import javax.swing.WindowConstants; -import javax.swing.border.EmptyBorder; - -import org.scijava.command.Command; -import org.scijava.command.CommandService; -import org.scijava.plugin.Plugin; -import org.scijava.prefs.PrefService; - -import fiji.plugin.trackmate.gui.Fonts; -import fiji.plugin.trackmate.gui.GuiUtils; -import fiji.plugin.trackmate.gui.Icons; -import fiji.plugin.trackmate.util.TMUtils; -import fiji.plugin.trackmate.util.cli.CLIUtils; -import fiji.plugin.trackmate.util.cli.condapath.CondaDetector.CondaInfo; -import fiji.plugin.trackmate.util.cli.condapath.CondaDetector.CondaNotFoundException; -import ij.IJ; -import ij.ImageJ; - -@Plugin( type = Command.class, - label = "Configure the path to the Conda executable used in TrackMate...", - iconPath = "/icons/commands/information.png", - menuPath = "Edit > Options > Configure TrackMate Conda path..." ) -public class CondaPathConfigCommand implements Command -{ - - @Override - public void run() - { - SwingUtilities.invokeLater( () -> createAndShowDialog() ); - } - - private void createAndShowDialog() - { - final PrefService prefs = TMUtils.getContext().getService( PrefService.class ); - - // Get or detect default paths - String findPath; - try - { - findPath = CLIUtils.findDefaultCondaPath(); - } - catch ( final IllegalArgumentException e ) - { - findPath = "/usr/local/opt/micromamba/bin/micromamba"; - } - - final String condaPath = prefs.get( CLIUtils.class, CLIUtils.CONDA_PATH_PREF_KEY, findPath ); - final Path path = Paths.get( condaPath ); - final Path parent = path.getParent(); - final Path parentOfParent = ( parent != null ) ? parent.getParent() : null; - final String defaultValue = "/usr/local/opt/micromamba/"; - - String condaRootPrefix = ( parentOfParent != null ) - ? parentOfParent.toString() - : defaultValue; - condaRootPrefix = prefs.get( CLIUtils.class, CLIUtils.CONDA_ROOT_PREFIX_KEY, condaRootPrefix ); - - // Create non-modal dialog - final JDialog dialog = new JDialog( IJ.getInstance(), "TrackMate Conda Configuration", false ); - dialog.setIconImage( TRACKMATE_ICON.getImage() ); - dialog.setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE ); - - // Main panel with padding - final JPanel mainPanel = new JPanel( new BorderLayout( 10, 10 ) ); - mainPanel.setBorder( new EmptyBorder( 15, 15, 15, 15 ) ); - mainPanel.setBackground( Color.WHITE ); - - // ========== Header Panel ========== - final JPanel headerPanel = createHeaderPanel(); - mainPanel.add( headerPanel, BorderLayout.NORTH ); - - // ========== Center Panel (Form) ========== - final JPanel centerPanel = new JPanel(); - centerPanel.setLayout( new BoxLayout( centerPanel, BoxLayout.Y_AXIS ) ); - centerPanel.setBackground( Color.WHITE ); - - // Status label for feedback - final JTextArea statusArea = new JTextArea( 2, 50 ); - statusArea.setEditable( false ); - statusArea.setLineWrap( true ); - statusArea.setWrapStyleWord( true ); - statusArea.setFont( new Font( "SansSerif", Font.PLAIN, 11 ) ); - statusArea.setForeground( new Color( 60, 120, 180 ) ); - statusArea.setBackground( new Color( 240, 248, 255 ) ); - statusArea.setBorder( BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder( new Color( 180, 200, 220 ) ), - new EmptyBorder( 5, 8, 5, 8 ) ) ); - statusArea.setText( "Configure conda paths or use auto-detection" ); - - final JScrollPane statusScrollPane = new JScrollPane( statusArea ); - statusScrollPane.setBorder( BorderFactory.createEmptyBorder() ); - statusScrollPane.setMaximumSize( new Dimension( Integer.MAX_VALUE, 60 ) ); - centerPanel.add( statusScrollPane ); - centerPanel.add( Box.createVerticalStrut( 15 ) ); - - // Conda executable path - final JPanel execPanel = createPathPanel( - "Conda Executable Path", - "Path to the conda, mamba, or micromamba executable", - condaPath ); - final JTextField execField = ( JTextField ) execPanel.getClientProperty( "textfield" ); - final JButton execBrowseButton = ( JButton ) execPanel.getClientProperty( "browse" ); - centerPanel.add( execPanel ); - centerPanel.add( Box.createVerticalStrut( 10 ) ); - - // Conda root prefix - final JPanel rootPanel = createPathPanel( - "Conda Root Prefix", - "Root directory of conda installation (CONDA_ROOT_PREFIX)", - condaRootPrefix ); - final JTextField rootField = ( JTextField ) rootPanel.getClientProperty( "textfield" ); - final JButton rootBrowseButton = ( JButton ) rootPanel.getClientProperty( "browse" ); - centerPanel.add( rootPanel ); - centerPanel.add( Box.createVerticalStrut( 15 ) ); - - // Browse button actions - execBrowseButton.addActionListener( e -> browseForFile( execField, dialog ) ); - rootBrowseButton.addActionListener( e -> browseForDirectory( rootField, dialog ) ); - - mainPanel.add( centerPanel, BorderLayout.CENTER ); - - // ========== Button Panel ========== - final JPanel buttonPanel = new JPanel( new FlowLayout( FlowLayout.RIGHT, 10, 0 ) ); - buttonPanel.setBackground( Color.WHITE ); - - final JButton autoDetectButton = new JButton( "Auto-detect" ); - autoDetectButton.setIcon( Icons.PREVIEW_ICON ); - autoDetectButton.addActionListener( e -> autoDetect( execField, rootField, statusArea ) ); - - final JButton diagnoseButton = new JButton( "Diagnose" ); - diagnoseButton.setIcon( Icons.COG_ICON ); - diagnoseButton.addActionListener( e -> diagnose() ); - - final JButton testButton = new JButton( "Test" ); - testButton.setIcon( Icons.EXECUTE_ICON ); - testButton.addActionListener( e -> test( execField.getText(), rootField.getText(), statusArea ) ); - - final JButton okButton = new JButton( "OK" ); - okButton.addActionListener( e -> saveAndClose( execField.getText(), rootField.getText(), prefs, dialog ) ); - - final JButton cancelButton = new JButton( "Cancel" ); - cancelButton.addActionListener( e -> dialog.dispose() ); - - buttonPanel.add( autoDetectButton ); - buttonPanel.add( diagnoseButton ); - buttonPanel.add( testButton ); - buttonPanel.add( Box.createHorizontalStrut( 20 ) ); - buttonPanel.add( okButton ); - buttonPanel.add( cancelButton ); - - mainPanel.add( buttonPanel, BorderLayout.SOUTH ); - - // ========== Finalize Dialog ========== - dialog.add( mainPanel ); - dialog.pack(); - dialog.setLocationRelativeTo( IJ.getInstance() ); - dialog.setVisible( true ); - } - - // ========== UI Component Factories ========== - - private JPanel createHeaderPanel() - { - final JPanel headerPanel = new JPanel( new BorderLayout( 10, 5 ) ); - headerPanel.setBackground( Color.WHITE ); - - final JLabel iconLabel = new JLabel( GuiUtils.scaleImage( Icons.TRACKMATE_ICON, 48, 48 ) ); - headerPanel.add( iconLabel, BorderLayout.WEST ); - - final JPanel textPanel = new JPanel(); - textPanel.setLayout( new BoxLayout( textPanel, BoxLayout.Y_AXIS ) ); - textPanel.setBackground( Color.WHITE ); - - final JLabel titleLabel = new JLabel( "Conda Configuration" ); - titleLabel.setFont( Fonts.BIG_FONT ); - titleLabel.setAlignmentX( Component.LEFT_ALIGNMENT ); - - final JLabel subtitleLabel = new JLabel( - "Configure conda executable for TrackMate modules" ); - subtitleLabel.setFont( Fonts.SMALL_FONT ); - subtitleLabel.setForeground( Color.GRAY ); - subtitleLabel.setAlignmentX( Component.LEFT_ALIGNMENT ); - - textPanel.add( titleLabel ); - textPanel.add( Box.createVerticalStrut( 3 ) ); - textPanel.add( subtitleLabel ); - - headerPanel.add( textPanel, BorderLayout.CENTER ); - headerPanel.add( Box.createVerticalStrut( 10 ), BorderLayout.SOUTH ); - - return headerPanel; - } - - private JPanel createPathPanel( final String title, final String description, final String defaultPath ) - { - final JPanel panel = new JPanel( new BorderLayout( 5, 5 ) ); - panel.setBackground( Color.WHITE ); - panel.setBorder( BorderFactory.createCompoundBorder( - BorderFactory.createTitledBorder( title ), - new EmptyBorder( 5, 5, 5, 5 ) ) ); - - // Description - final JLabel descLabel = new JLabel( description ); - descLabel.setFont( Fonts.SMALL_FONT ); - descLabel.setForeground( Color.GRAY ); - panel.add( descLabel, BorderLayout.NORTH ); - - // Path input panel - final JPanel inputPanel = new JPanel( new BorderLayout( 5, 0 ) ); - inputPanel.setBackground( Color.WHITE ); - - final JTextField textField = new JTextField( defaultPath, 40 ); - textField.setFont( new Font( "Monospaced", Font.PLAIN, 12 ) ); - - final JButton browseButton = new JButton( "Browse..." ); - browseButton.setFocusable( false ); - - inputPanel.add( textField, BorderLayout.CENTER ); - inputPanel.add( browseButton, BorderLayout.EAST ); - - panel.add( inputPanel, BorderLayout.CENTER ); - - // Store components for later access - panel.putClientProperty( "textfield", textField ); - panel.putClientProperty( "browse", browseButton ); - - return panel; - } - - // ========== Action Handlers ========== - - private void browseForFile( final JTextField textField, final JDialog parent ) - { - final JFileChooser chooser = new JFileChooser(); - chooser.setDialogTitle( "Select Conda Executable" ); - chooser.setFileSelectionMode( JFileChooser.FILES_ONLY ); - - final String currentPath = textField.getText(); - if ( !currentPath.isEmpty() ) - { - final File currentFile = new File( currentPath ); - if ( currentFile.getParentFile() != null && currentFile.getParentFile().exists() ) - chooser.setCurrentDirectory( currentFile.getParentFile() ); - } - - if ( chooser.showOpenDialog( parent ) == JFileChooser.APPROVE_OPTION ) - { - final File selected = chooser.getSelectedFile(); - textField.setText( selected.getAbsolutePath() ); - } - } - - private void browseForDirectory( final JTextField textField, final JDialog parent ) - { - final JFileChooser chooser = new JFileChooser(); - chooser.setDialogTitle( "Select Conda Root Directory" ); - chooser.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY ); - - final String currentPath = textField.getText(); - if ( !currentPath.isEmpty() ) - { - final File currentDir = new File( currentPath ); - if ( currentDir.exists() ) - chooser.setCurrentDirectory( currentDir ); - } - - if ( chooser.showOpenDialog( parent ) == JFileChooser.APPROVE_OPTION ) - { - final File selected = chooser.getSelectedFile(); - textField.setText( selected.getAbsolutePath() ); - } - } - - private void autoDetect( final JTextField execField, final JTextField rootField, final JTextArea statusArea ) - { - statusArea.setForeground( new Color( 60, 120, 180 ) ); - statusArea.setText( "Auto-detecting conda installation..." ); - - new Thread( () -> { - try - { - final CondaInfo condaInfo = CondaDetector.detect(); - SwingUtilities.invokeLater( () -> { - execField.setText( condaInfo.getCondaExecutable() ); - rootField.setText( condaInfo.getRootPrefix() ); - statusArea.setForeground( new Color( 0, 128, 0 ) ); - statusArea.setText( String.format( - "✓ Auto-detection successful!\nFound conda %s at: %s", - condaInfo.getVersion(), - condaInfo.getCondaExecutable() ) ); - } ); - } - catch ( final CondaNotFoundException e ) - { - SwingUtilities.invokeLater( () -> { - statusArea.setForeground( new Color( 180, 0, 0 ) ); - statusArea.setText( "✗ Auto-detection failed:\n" + e.getMessage() ); - } ); - } - }, "Conda-AutoDetect" ).start(); - } - - private void diagnose() - { - new Thread( () -> { - IJ.log( "\n========== Conda Diagnostics ==========\n" ); - CondaDetector.diagnose(); - }, "Conda-Diagnose" ).start(); - } - - private void test( final String execPath, final String rootPath, final JTextArea statusArea ) - { - statusArea.setForeground( new Color( 60, 120, 180 ) ); - statusArea.setText( "Testing conda configuration..." ); - - new Thread( () -> { - try - { - // Temporarily set the paths for testing - final PrefService prefs = TMUtils.getContext().getService( PrefService.class ); - final String oldExec = prefs.get( CLIUtils.class, CLIUtils.CONDA_PATH_PREF_KEY, "" ); - final String oldRoot = prefs.get( CLIUtils.class, CLIUtils.CONDA_ROOT_PREFIX_KEY, "" ); - - prefs.put( CLIUtils.class, CLIUtils.CONDA_PATH_PREF_KEY, execPath ); - prefs.put( CLIUtils.class, CLIUtils.CONDA_ROOT_PREFIX_KEY, rootPath ); - CLIUtils.clearEnvMap(); - - final Map< String, String > map = CLIUtils.getEnvMap(); - final StringBuilder str = new StringBuilder(); - str.append( "✓ Test successful! Found " + map.size() + " environment(s):\n" ); - map.forEach( ( k, v ) -> str.append( String.format( " • %s\n", k ) ) ); - - SwingUtilities.invokeLater( () -> { - statusArea.setForeground( new Color( 0, 128, 0 ) ); - statusArea.setText( str.toString() ); - } ); - - IJ.log( "\n========== Conda Test Results ==========\n" + str.toString() ); - - // Restore old values - prefs.put( CLIUtils.class, CLIUtils.CONDA_PATH_PREF_KEY, oldExec ); - prefs.put( CLIUtils.class, CLIUtils.CONDA_ROOT_PREFIX_KEY, oldRoot ); - CLIUtils.clearEnvMap(); - } - catch ( final IOException e ) - { - SwingUtilities.invokeLater( () -> { - statusArea.setForeground( new Color( 180, 0, 0 ) ); - statusArea.setText( "✗ Test failed:\nConda executable path seems incorrect.\n" + e.getMessage() ); - } ); - IJ.error( "Conda Test Failed", - "Conda executable path seems to be incorrect.\nError: " + e.getMessage() ); - } - catch ( final Exception e ) - { - SwingUtilities.invokeLater( () -> { - statusArea.setForeground( new Color( 180, 0, 0 ) ); - statusArea.setText( "✗ Test failed:\n" + e.getMessage() ); - } ); - e.printStackTrace(); - IJ.error( "Conda Test Failed", - "Error when running conda.\nError: " + e.getMessage() ); - } - }, "Conda-Test" ).start(); - } - - private void saveAndClose( final String execPath, final String rootPath, final PrefService prefs, final JDialog dialog ) - { - prefs.put( CLIUtils.class, CLIUtils.CONDA_PATH_PREF_KEY, execPath ); - prefs.put( CLIUtils.class, CLIUtils.CONDA_ROOT_PREFIX_KEY, rootPath ); - CLIUtils.clearEnvMap(); - - IJ.log( "Conda configuration saved:" ); - IJ.log( " Executable: " + execPath ); - IJ.log( " Root Prefix: " + rootPath ); - - dialog.dispose(); - } - - // ========== Main for Testing ========== - - public static void main( final String[] args ) - { - ImageJ.main( args ); - TMUtils.getContext().getService( CommandService.class ).run( CondaPathConfigCommand.class, false ); - } -} diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/FactoryGenericConfig.java b/src/main/java/fiji/plugin/trackmate/util/config/FactoryGenericConfig.java similarity index 87% rename from src/main/java/fiji/plugin/trackmate/util/cli/FactoryGenericConfig.java rename to src/main/java/fiji/plugin/trackmate/util/config/FactoryGenericConfig.java index 00c31a88d..99341106e 100644 --- a/src/main/java/fiji/plugin/trackmate/util/cli/FactoryGenericConfig.java +++ b/src/main/java/fiji/plugin/trackmate/util/config/FactoryGenericConfig.java @@ -19,7 +19,9 @@ * . * #L% */ -package fiji.plugin.trackmate.util.cli; +package fiji.plugin.trackmate.util.config; + +import org.scijava.ui.config.Configurator; import fiji.plugin.trackmate.TrackMateModule; import fiji.plugin.trackmate.visualization.ViewUtils; @@ -47,7 +49,7 @@ public interface FactoryGenericConfig< C extends Configurator > extends TrackMat * the input image to configure the detector for. * @return a new {@link Configurator}. */ - public C getConfigurator( ImagePlus imp ); + public C createConfig( ImagePlus imp ); /** * Creates a new configurator for this detector factory, based on the @@ -57,24 +59,24 @@ public interface FactoryGenericConfig< C extends Configurator > extends TrackMat * the input image to configure the detector for. * @return a new {@link Configurator}. */ - public default C getConfigurator( final ImgPlus< ? > img ) + public default C createConfig( final ImgPlus< ? > img ) { @SuppressWarnings( { "unchecked", "rawtypes" } ) final ImagePlus imp = ImageJFunctions.wrap( ( ImgPlus ) img, "wrapped" ); - return getConfigurator( imp ); + return createConfig( imp ); } /** - * Creates a new configurator for this detector factory. + * Creates a new configurator for this factory. * * @return a new {@link Configurator}. */ - public default C getConfigurator() + public default C createConfig() { final int nZ = 2; // Force 3D final int nT = 2; // Force timelapse final double[] calibration = new double[] { 1., 1., 1. }; final ImagePlus imp = ViewUtils.makeEmptyImagePlus( 32, 32, nZ, nT, calibration ); - return getConfigurator( imp ); + return createConfig( imp ); } } diff --git a/src/main/java/fiji/plugin/trackmate/util/config/GenericConfigPanel.java b/src/main/java/fiji/plugin/trackmate/util/config/GenericConfigPanel.java new file mode 100644 index 000000000..351aad7b8 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/util/config/GenericConfigPanel.java @@ -0,0 +1,102 @@ +package fiji.plugin.trackmate.util.config; + +import static org.scijava.ui.config.utils.GuiUtils.isLikelyUrl; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.Image; +import java.util.Map; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JEditorPane; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingConstants; +import javax.swing.UIManager; + +import org.scijava.ui.config.Configurator; +import org.scijava.ui.config.visitors.Maps; +import org.scijava.ui.config.visitors.gui.GuiBuilder; +import org.scijava.ui.config.visitors.gui.GuiBuilder.ConfigPanel; + +import fiji.plugin.trackmate.gui.GuiUtils; +import fiji.plugin.trackmate.gui.components.ConfigurationPanel; + +public class GenericConfigPanel extends ConfigurationPanel +{ + + private static final long serialVersionUID = 1L; + + public static Font FONT = UIManager.getFont( "Label.font" ); + + protected final Configurator config; + + protected final ConfigPanel mainPanel; + + public GenericConfigPanel( final Configurator config ) + { + this.config = config; + + final BorderLayout borderLayout = new BorderLayout(); + setLayout( borderLayout ); + + /* + * HEADER + */ + + final JPanel header = new JPanel(); + header.setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) ); + header.setLayout( new BoxLayout( header, BoxLayout.Y_AXIS ) ); + + final ImageIcon icon = ( config.getIcon() != null ) + ? new ImageIcon( config.getIcon().getScaledInstance( 64, 64, Image.SCALE_SMOOTH ) ) + : null; + final JLabel lblDetector = new JLabel( config.getName(), icon, JLabel.RIGHT ); + lblDetector.setFont( FONT.deriveFont( Font.BOLD ) ); + lblDetector.setHorizontalAlignment( SwingConstants.CENTER ); + lblDetector.setAlignmentX( JLabel.CENTER_ALIGNMENT ); + header.add( lblDetector ); + + final String help = config.getHelp(); + final String text = help.trim(); + final JEditorPane infoDisplay; + if ( isLikelyUrl( text ) ) + infoDisplay = GuiUtils.infoDisplay( "" + text + "", false ); + else + infoDisplay = GuiUtils.infoDisplay( help, true ); + header.add( Box.createVerticalStrut( 5 ) ); + header.add( infoDisplay ); + add( header, BorderLayout.NORTH ); + + /* + * CONFIG + */ + + this.mainPanel = GuiBuilder.build( config ); + final JScrollPane scrollPane = new JScrollPane( mainPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER ); + scrollPane.setBorder( null ); + scrollPane.getVerticalScrollBar().setUnitIncrement( 16 ); + add( scrollPane, BorderLayout.CENTER ); + } + + @Override + public void setSettings( final Map< String, Object > settings ) + { + Maps.fromMap( settings, config ); + mainPanel.refresh(); + } + + @Override + public Map< String, Object > getSettings() + { + return Maps.toMap( config ); + } + + @Override + public void clean() + {} +} diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/GenericDetectionConfigurationPanel.java b/src/main/java/fiji/plugin/trackmate/util/config/GenericConfigPanelPreview.java similarity index 56% rename from src/main/java/fiji/plugin/trackmate/util/cli/GenericDetectionConfigurationPanel.java rename to src/main/java/fiji/plugin/trackmate/util/config/GenericConfigPanelPreview.java index 30e029d23..b6e269f16 100644 --- a/src/main/java/fiji/plugin/trackmate/util/cli/GenericDetectionConfigurationPanel.java +++ b/src/main/java/fiji/plugin/trackmate/util/config/GenericConfigPanelPreview.java @@ -1,63 +1,34 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util.cli; +package fiji.plugin.trackmate.util.config; import java.awt.BorderLayout; import java.util.function.DoubleConsumer; import java.util.function.Supplier; -import javax.swing.Icon; +import org.scijava.ui.config.Configurator; +import org.scijava.ui.config.visitors.gui.elements.StyleElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.BoundedDoubleElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.DoubleElement; +import org.scijava.ui.config.visitors.gui.elements.StyleElements.IntElement; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.detection.SpotDetectorFactoryBase; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.BoundedDoubleElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.DoubleElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.IntElement; -import fiji.plugin.trackmate.gui.displaysettings.StyleElements.StyleElement; import fiji.plugin.trackmate.util.DetectionPreview; import fiji.plugin.trackmate.util.DetectionPreview.Builder; import fiji.plugin.trackmate.util.DetectionPreviewPanel; -/** - * Specialization of {@link GenericConfigurationPanel} for - * {@link SpotDetectorFactoryBase}. It adds a {@link DetectionPreview} at the - * bottom of the panel. - */ -public class GenericDetectionConfigurationPanel extends GenericConfigurationPanel +public class GenericConfigPanelPreview extends GenericConfigPanel { private static final long serialVersionUID = 1L; - public GenericDetectionConfigurationPanel( + public GenericConfigPanelPreview( final Settings settings, final Model model, final Configurator config, - final String title, - final Icon icon, - final String docURL, final Supplier< SpotDetectorFactoryBase< ? > > factorySupplier ) { - super( config, title, icon, docURL ); + super( config ); final DetectionPreview detectionPreview = getDetectionPreview( model, settings, factorySupplier ); final DetectionPreviewPanel p = detectionPreview.getPanel(); @@ -65,16 +36,15 @@ public GenericDetectionConfigurationPanel( } /** - * Creates a basic {@link DetectionPreview}. Can be overridden by - * subclasses. + * Creates a basic {@link DetectionPreview}. Can be overridden by subclasses * * @param model - * the model to populate the preview with. + * the model to update with the previewed spots. * @param settings - * the settings to use to configure the preview. + * the settings to use to run the detection. * @param factorySupplier - * a supplier for the detector factory to use in the preview. - * @return a new {@link DetectionPreview}. + * a supplier for the detector factory. + * @return the detection preview object. */ protected DetectionPreview getDetectionPreview( final Model model, @@ -96,7 +66,7 @@ protected DetectionPreview getDetectionPreview( if ( key != null ) { final DoubleConsumer thresholdUpdater; - final StyleElement element = mainPanel.elements.get( key ); + final StyleElement element = mainPanel.getStyleElement( key ); if ( element instanceof DoubleElement ) { thresholdUpdater = t -> { @@ -113,7 +83,7 @@ else if ( element instanceof BoundedDoubleElement ) } else if ( element instanceof IntElement ) { - final IntElement el = ( IntElement ) element ; + final IntElement el = ( IntElement ) element; thresholdUpdater = t -> { el.set( ( int ) t ); mainPanel.refresh(); @@ -125,7 +95,6 @@ else if ( element instanceof IntElement ) } builder.thresholdUpdater( thresholdUpdater ); } - builder.axisLabel( hasPreview.getPreviewAxisLabel() ); } return builder.get(); diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/HasInteractivePreview.java b/src/main/java/fiji/plugin/trackmate/util/config/HasInteractivePreview.java similarity index 97% rename from src/main/java/fiji/plugin/trackmate/util/cli/HasInteractivePreview.java rename to src/main/java/fiji/plugin/trackmate/util/config/HasInteractivePreview.java index cb7ff2921..8b24d352f 100644 --- a/src/main/java/fiji/plugin/trackmate/util/cli/HasInteractivePreview.java +++ b/src/main/java/fiji/plugin/trackmate/util/config/HasInteractivePreview.java @@ -19,7 +19,7 @@ * . * #L% */ -package fiji.plugin.trackmate.util.cli; +package fiji.plugin.trackmate.util.config; /** * Interface for {@link Configurator}s which settings can be previewed with diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CommonTrackMateArguments.java b/src/main/java/fiji/plugin/trackmate/util/config/TrackMateConfigurator.java similarity index 52% rename from src/main/java/fiji/plugin/trackmate/util/cli/CommonTrackMateArguments.java rename to src/main/java/fiji/plugin/trackmate/util/config/TrackMateConfigurator.java index 870abb2b8..a0077f668 100644 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CommonTrackMateArguments.java +++ b/src/main/java/fiji/plugin/trackmate/util/config/TrackMateConfigurator.java @@ -1,25 +1,4 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util.cli; +package fiji.plugin.trackmate.util.config; import static fiji.plugin.trackmate.detection.DetectorKeys.DEFAULT_DO_MEDIAN_FILTERING; import static fiji.plugin.trackmate.detection.DetectorKeys.DEFAULT_DO_SUBPIXEL_LOCALIZATION; @@ -32,134 +11,150 @@ import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_THRESHOLD; import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS; +import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_SMOOTHING_SCALE; -import fiji.plugin.trackmate.util.cli.Configurator.DoubleArgument; -import fiji.plugin.trackmate.util.cli.Configurator.Flag; -import fiji.plugin.trackmate.util.cli.Configurator.IntArgument; +import org.scijava.ui.config.Configurator; +import org.scijava.ui.config.Parameters.BooleanParam; +import org.scijava.ui.config.Parameters.DoubleParam; +import org.scijava.ui.config.Parameters.IntParam; /** - * Arguments that are commonly used by TrackMate, to add to custom - * {@link fiji.plugin.trackmate.util.cli.CLIConfigurator}. + * An intermediate class to facilitate adding common TrackMate parameters to a + * {@link Configurator}. */ -public class CommonTrackMateArguments +public abstract class TrackMateConfigurator extends Configurator { + protected TrackMateConfigurator( final String name, final String help ) + { + super( name, help ); + } + + protected BooleanParam addSimplifyContour() + { + final BooleanParam param = addBooleanParameter() + .key( KEY_SIMPLIFY_CONTOURS ) + .defaultValue( true ) + .name( "Simplify contour" ) + .help( "If true contours will be simplified with fewer control points." ) + .visible( true ) + .get(); + param.set( true ); + return param; + } + + protected DoubleParam addSmoothContour( final String units ) + { + final DoubleParam param = addDoubleParameter() + .key( KEY_SMOOTHING_SCALE ) + .defaultValue( 0. ) + .min( 0. ) + .max( 20. ) + .units( units ) + .name( "Smooth contour" ) + .help( "If > 0, contours will be smoothed over this radius." ) + .visible( true ) + .get(); + param.set( 0. ); + // No need to translate: TrackMate expects physical units + return param; + } + /** * Creates an argument used to specify on what channel in the input image to - * operate on and add it to the given configurator. + * operate on and add it to this configurator. *

    * The channel index is 1-based. * - * @param config - * the configurator to which to add the argument. * @param nChannels * how many channels in the input image. - * @return the created argument. + * @return the integer target channel argument. */ - public static IntArgument addTargetChannel( final Configurator config, final int nChannels ) + protected IntParam addTargetChannel( final int nChannels ) { - final IntArgument arg = config.addIntArgument() + final IntParam param = addIntParameter() .key( KEY_TARGET_CHANNEL ) .defaultValue( DEFAULT_TARGET_CHANNEL ) .name( "Target channel" ) .help( "Index of the channel to process." ) - .inCLI( false ) .visible( true ) .min( 1 ) // 1-based .max( Integer.valueOf( nChannels ) ) .get(); - arg.set( DEFAULT_TARGET_CHANNEL ); - return arg; - } - - public static Flag addSimplifyContour( final Configurator config ) - { - final Flag arg = config.addFlag() - .key( KEY_SIMPLIFY_CONTOURS ) - .defaultValue( true ) - .name( "Simplify contour" ) - .help( "If true contours will be simplified with fewer control points." ) - .inCLI( false ) - .visible( true ) - .get(); - arg.set( true ); - return arg; + param.set( DEFAULT_TARGET_CHANNEL ); + return param; } - public static DoubleArgument addRadius( final Configurator config, final String units ) + protected DoubleParam addRadius( final String units ) { - final DoubleArgument arg = config.addDoubleArgument() + final DoubleParam param = addDoubleParameter() .key( KEY_RADIUS ) - .argument( "--radius" ) .defaultValue( DEFAULT_RADIUS ) .units( units ) .name( "Radius" ) .help( "Radius of the objects to detect, in " + units + "." ) .visible( true ) .get(); - arg.set( DEFAULT_RADIUS ); - return arg; + param.set( DEFAULT_RADIUS ); + return param; } /** - * Adds a diameter argument to the given configurator. + * Adds a diameter argument to this configurator. *

    * Here there is a gotcha: the value displayed in the UI is the diameter, * but the value stored and returned is the radius. Therefore, we add a * translator that divides the value by 2. * - * @param config - * the config to which to add the argument. * @param units * the units of the diameter to display. - * @return the created argument. + * @return the diameter double argument. */ - public static DoubleArgument addDiameter( final Configurator config, final String units ) + protected DoubleParam addDiameter( final String units ) { - final DoubleArgument arg = config.addDoubleArgument() + final DoubleParam param = addDoubleParameter() .key( KEY_RADIUS ) - .argument( "--radius" ) .defaultValue( DEFAULT_RADIUS ) .units( units ) .name( "Diameter" ) .help( "Diameter of the objects to detect." ) .visible( true ) .get(); - arg.set( DEFAULT_RADIUS ); + param.set( DEFAULT_RADIUS ); // Add a translator from radius (stored) to diameter (displayed). - config.setDisplayTranslator( arg, r -> r * 2., d -> d / 2. ); - return arg; + setDisplayTranslator( param, r -> r * 2., d -> d / 2. ); + return param; } - public static DoubleArgument addThreshold( final Configurator config ) + protected DoubleParam addThreshold() { - final DoubleArgument arg = config.addDoubleArgument() + final DoubleParam param = addDoubleParameter() .key( KEY_THRESHOLD ) .defaultValue( DEFAULT_THRESHOLD ) .name( "Threshold" ) .help( "The threshold to apply to the detector." ) .visible( true ) .get(); - arg.set( DEFAULT_THRESHOLD ); - return arg; + param.set( DEFAULT_THRESHOLD ); + return param; } - public static Flag addSubpixelLocalization( final Configurator config ) + protected BooleanParam addSubpixelLocalization() { - final Flag flag = config.addFlag() + final BooleanParam param = addBooleanParameter() .key( KEY_DO_SUBPIXEL_LOCALIZATION ) .defaultValue( DEFAULT_DO_SUBPIXEL_LOCALIZATION ) .name( "Sub-pixel localization" ) .help( "If true, the detector will try to localize spots with sub-pixel accuracy." ) .visible( true ) .get(); - flag.set( DEFAULT_DO_SUBPIXEL_LOCALIZATION ); - return flag; + param.set( DEFAULT_DO_SUBPIXEL_LOCALIZATION ); + return param; } - public static Flag addMedianFiltering( final Configurator config ) + protected BooleanParam addMedianFiltering() { - final Flag flag = config.addFlag() + final BooleanParam flag = addBooleanParameter() .key( KEY_DO_MEDIAN_FILTERING ) .defaultValue( DEFAULT_DO_MEDIAN_FILTERING ) .name( "Median filtering" ) diff --git a/src/main/java/fiji/plugin/trackmate/util/mesh/SpotMeshCursor.java b/src/main/java/fiji/plugin/trackmate/util/mesh/SpotMeshCursor.java new file mode 100644 index 000000000..aebe3496b --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/util/mesh/SpotMeshCursor.java @@ -0,0 +1,244 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.util.mesh; + +import fiji.plugin.trackmate.SpotMesh; +import gnu.trove.list.array.TDoubleArrayList; +import net.imglib2.Cursor; +import net.imglib2.RandomAccess; +import net.imglib2.RealInterval; +import net.imglib2.mesh.alg.zslicer.Slice; + +/** + * A {@link Cursor} that iterates over the pixels inside a mesh. + *

    + * It is based on an implementation of the ray casting algorithm, with some + * optimization to avoid querying the mesh for every single pixel. It does its + * best to ensure that the pixels iterated inside a mesh created from a mask are + * exactly the pixels of the original mask, but does not succeed fully (yet). + * + * @author Jean-Yves Tinevez + * + * @param + * the types of the pixels iterated. + */ +public class SpotMeshCursor< T > implements Cursor< T > +{ + + private final double[] cal; + + private final int minX; + + private final int maxX; + + private final int minY; + + private final int maxY; + + private final int minZ; + + private final int maxZ; + + private final RandomAccess< T > ra; + + private final SpotMesh sm; + + private boolean hasNext; + + private int iy; + + private int iz; + + private int ix; + + /** + * List of resolved X positions where we enter / exit the mesh. Set by the + * ray casting algorithm. + */ + private final TDoubleArrayList intersectionXs = new TDoubleArrayList(); + + private Slice slice; + + public SpotMeshCursor( final RandomAccess< T > ra, final SpotMesh sm, final double[] cal ) + { + this.ra = ra; + this.sm = sm; + this.cal = cal; + final RealInterval bb = sm.getBoundingBox(); + this.minX = ( int ) Math.floor( ( bb.realMin( 0 ) + sm.getDoublePosition( 0 ) ) / cal[ 0 ] ); + this.maxX = ( int ) Math.ceil( ( bb.realMax( 0 ) + sm.getDoublePosition( 0 ) ) / cal[ 0 ] ); + this.minY = ( int ) Math.floor( ( bb.realMin( 1 ) + sm.getDoublePosition( 1 ) ) / cal[ 1 ] ); + this.maxY = ( int ) Math.ceil( ( bb.realMax( 1 ) + sm.getDoublePosition( 1 ) ) / cal[ 1 ] ); + this.minZ = ( int ) Math.floor( ( bb.realMin( 2 ) + sm.getDoublePosition( 2 ) ) / cal[ 2 ] ); + this.maxZ = ( int ) Math.ceil( ( bb.realMax( 2 ) + sm.getDoublePosition( 2 ) ) / cal[ 2 ] ); + reset(); + } + + @Override + public void reset() + { + this.ix = maxX; // To force a new ray cast when we call fwd() + this.iy = minY - 1; // Then we will move to minY. + this.iz = minZ; + this.slice = sm.getZSlice( iz, cal[ 0 ], cal[ 2 ] ); + this.hasNext = true; + preFetch(); + } + + @Override + public void fwd() + { + ra.setPosition( ix, 0 ); + ra.setPosition( iy, 1 ); + ra.setPosition( iz, 2 ); + preFetch(); + } + + private void preFetch() + { + hasNext = false; + while ( true ) + { + // Find next position. + ix++; + if ( ix > maxX ) + { + ix = minX; + while ( true ) + { + // Next Y line, we will need to ray cast again. + ix = minX; + iy++; + if ( iy > maxY ) + { + iy = minY; + iz++; + if ( iz > maxZ ) + return; // Finished! + slice = sm.getZSlice( iz, cal[ 0 ], cal[ 2 ] ); + } + if ( slice == null ) + continue; + + // New ray cast, relative to slice center + final double y = iy * cal[ 1 ] - sm.getDoublePosition( 1 ); + slice.xRayCast( y, intersectionXs, cal[ 1 ] ); + + // No intersection? + if ( !intersectionXs.isEmpty() ) + break; + + // No intersection on this line, move to the next. + } + } + // We have found the next position. + + // Is it inside? + final double x = ix * cal[ 0 ] - sm.getDoublePosition( 0 ); + + // Special case: only one intersection. + if ( intersectionXs.size() == 1 ) + { + if ( x == intersectionXs.getQuick( 0 ) ) + { + hasNext = true; + return; + } + else + { + continue; + } + } + + final int i = intersectionXs.binarySearch( x ); + if ( i >= 0 ) + { + // Fall on an intersection exactly. + hasNext = true; + return; + } + final int ip = -( i + 1 ); + // Odd or even? + if ( ip % 2 != 0 ) + { + // Odd. We are inside. + hasNext = true; + return; + } + + // Not inside, move to the next point. + } + } + + @Override + public boolean hasNext() + { + return hasNext; + } + + @Override + public void jumpFwd( final long steps ) + { + for ( int i = 0; i < steps; i++ ) + fwd(); + } + + @Override + public T next() + { + fwd(); + return get(); + } + + @Override + public long getLongPosition( final int d ) + { + return ra.getLongPosition( d ); + } + + @Override + public Cursor< T > copyCursor() + { + return new SpotMeshCursor<>( + ra.copy(), + sm.copy(), + cal.clone() ); + } + + @Override + public Cursor< T > copy() + { + return copyCursor(); + } + + @Override + public int numDimensions() + { + return 3; + } + + @Override + public T get() + { + return ra.get(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/util/mesh/SpotMeshIterable.java b/src/main/java/fiji/plugin/trackmate/util/mesh/SpotMeshIterable.java new file mode 100644 index 000000000..58bd432c2 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/util/mesh/SpotMeshIterable.java @@ -0,0 +1,116 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.util.mesh; + +import java.util.Iterator; + +import fiji.plugin.trackmate.SpotMesh; +import net.imglib2.Cursor; +import net.imglib2.IterableInterval; +import net.imglib2.Localizable; +import net.imglib2.RandomAccessible; + +public class SpotMeshIterable< T > implements IterableInterval< T >, Localizable +{ + + private final double[] calibration; + + private final RandomAccessible< T > img; + + private final SpotMesh sm; + + public SpotMeshIterable( + final RandomAccessible< T > img, + final SpotMesh sm, + final double[] calibration ) + { + this.img = img; + this.sm = sm; + this.calibration = calibration; + } + + @Override + public int numDimensions() + { + return 3; + } + + @Override + public long getLongPosition( final int d ) + { + return Math.round( sm.getDoublePosition( d ) / calibration[ d ] ); + } + + @Override + public long size() + { + // Costly! + long size = 0; + for ( @SuppressWarnings( "unused" ) + final T t : this ) + size++; + + return size; + } + + @Override + public T firstElement() + { + return cursor().next(); + } + + @Override + public Object iterationOrder() + { + return this; + } + + @Override + public Iterator< T > iterator() + { + return cursor(); + } + + @Override + public long min( final int d ) + { + return Math.round( ( sm.getBoundingBox().realMin( d ) + sm.getFloatPosition( d ) ) / calibration[ d ] ); + } + + @Override + public long max( final int d ) + { + return Math.round( ( sm.getBoundingBox().realMax( d ) + sm.getFloatPosition( d ) ) / calibration[ d ] ); + } + + @Override + public Cursor< T > cursor() + { + return new SpotMeshCursor<>( img.randomAccess(), sm, calibration ); + } + + @Override + public Cursor< T > localizingCursor() + { + return cursor(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/AbstractTrackMateModelBvvView.java b/src/main/java/fiji/plugin/trackmate/visualization/AbstractTrackMateModelBvvView.java new file mode 100644 index 000000000..66f6abea5 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/AbstractTrackMateModelBvvView.java @@ -0,0 +1,70 @@ +package fiji.plugin.trackmate.visualization; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.scijava.ui.behaviour.MouseAndKeyHandler; +import org.scijava.ui.behaviour.util.Actions; +import org.scijava.ui.behaviour.util.Behaviours; +import org.scijava.ui.behaviour.util.InputActionBindings; +import org.scijava.ui.behaviour.util.TriggerBehaviourBindings; +import org.scijava.ui.behaviour.util.WrappedActionMap; +import org.scijava.ui.behaviour.util.WrappedInputMap; + +import bdv.ui.keymap.Keymap; +import bdv.ui.keymap.Keymap.UpdateListener; +import bdv.ui.keymap.KeymapManager; +import bvv.core.VolumeViewerFrame; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.visualization.ui.KeyConfigContexts; + +public abstract class AbstractTrackMateModelBvvView extends AbstractTrackMateModelView +{ + + protected final Behaviours behaviours; + + protected final Actions actions; + + protected AbstractTrackMateModelBvvView( final GuiModel guiModel, final KeymapManager keymapManager, final String... keyConfigContexts ) + { + super( guiModel ); + final Set< String > c = new LinkedHashSet<>( Arrays.asList( KeyConfigContexts.TRACKMATE ) ); + c.addAll( Arrays.asList( keyConfigContexts ) ); + final String[] kccs = c.toArray( new String[] {} ); + final Keymap keymap = keymapManager.getForwardSelectedKeymap(); + this.behaviours = new Behaviours( keymap.getConfig(), kccs ); + this.actions = new Actions( keymap.getConfig(), kccs ); + } + + protected void setWindow( final VolumeViewerFrame viewerFrame ) + { + // We add our actions and behaviours to the following object, so that + // they can override those defined in the BVV. + final InputActionBindings keybindings = viewerFrame.getKeybindings(); + final TriggerBehaviourBindings triggerbindings = viewerFrame.getTriggerbindings(); + actions.install( keybindings, "view2" ); + behaviours.install( triggerbindings, "view2" ); + + final Keymap keymap = guiModel.getBvvKeymapManager().getForwardSelectedKeymap(); + final UpdateListener updateListener = () -> { + behaviours.updateKeyConfig( keymap.getConfig() ); + actions.updateKeyConfig( keymap.getConfig() ); + }; + keymap.updateListeners().add( updateListener ); + onClose( () -> keymap.updateListeners().remove( updateListener ) ); + + final MouseAndKeyHandler mouseAndKeyHandler = new MouseAndKeyHandler(); + mouseAndKeyHandler.setInputMap( triggerbindings.getConcatenatedInputTriggerMap() ); + mouseAndKeyHandler.setBehaviourMap( triggerbindings.getConcatenatedBehaviourMap() ); + mouseAndKeyHandler.setKeypressManager( guiModel.getKeyPressedManager(), viewerFrame.getViewerPanel().getDisplay().getComponent() ); + + // Register global actions, if any. + final Actions globalActions = guiModel.getGlobalActions(); + if ( globalActions != null ) + { + keybindings.addActionMap( "global", new WrappedActionMap( globalActions.getActionMap() ) ); + keybindings.addInputMap( "global", new WrappedInputMap( globalActions.getInputMap() ) ); + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/AbstractTrackMateModelJFrameView.java b/src/main/java/fiji/plugin/trackmate/visualization/AbstractTrackMateModelJFrameView.java new file mode 100644 index 000000000..bf7a2fa40 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/AbstractTrackMateModelJFrameView.java @@ -0,0 +1,128 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2026 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.visualization; + +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; + +import org.scijava.ui.behaviour.MouseAndKeyHandler; +import org.scijava.ui.behaviour.util.Actions; +import org.scijava.ui.behaviour.util.Behaviours; +import org.scijava.ui.behaviour.util.InputActionBindings; +import org.scijava.ui.behaviour.util.TriggerBehaviourBindings; +import org.scijava.ui.behaviour.util.WrappedActionMap; +import org.scijava.ui.behaviour.util.WrappedInputMap; + +import bdv.ui.keymap.Keymap; +import bdv.ui.keymap.Keymap.UpdateListener; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.visualization.ui.KeyConfigContexts; + +/** + * An abstract class for TrackMate views that display content in a + * {@link JFrame}. + * + * @author Jean-Yves Tinevez + */ +public abstract class AbstractTrackMateModelJFrameView extends AbstractTrackMateModelView +{ + + private final InputActionBindings keybindings; + + private final TriggerBehaviourBindings triggerbindings; + + private final MouseAndKeyHandler mouseAndKeyHandler; + + protected final Actions actions; + + protected final Behaviours behaviours; + + protected AbstractTrackMateModelJFrameView( final GuiModel guiModel, final String... keyConfigContexts ) + { + super( guiModel ); + final Set< String > c = new LinkedHashSet<>( Arrays.asList( KeyConfigContexts.TRACKMATE ) ); + c.addAll( Arrays.asList( keyConfigContexts ) ); + final String[] kccs = c.toArray( new String[] {} ); + + this.keybindings = new InputActionBindings(); + this.triggerbindings = new TriggerBehaviourBindings(); + + final Keymap keymap = guiModel.getKeymapManager().getForwardSelectedKeymap(); + + this.actions = new Actions( keymap.getConfig(), kccs ); + actions.install( keybindings, "view" ); + + this.behaviours = new Behaviours( keymap.getConfig(), kccs ); + behaviours.install( triggerbindings, "view" ); + + final UpdateListener updateListener = () -> { + behaviours.updateKeyConfig( keymap.getConfig() ); + actions.updateKeyConfig( keymap.getConfig() ); + }; + keymap.updateListeners().add( updateListener ); + onClose( () -> keymap.updateListeners().remove( updateListener ) ); + + this.mouseAndKeyHandler = new MouseAndKeyHandler(); + mouseAndKeyHandler.setInputMap( triggerbindings.getConcatenatedInputTriggerMap() ); + mouseAndKeyHandler.setBehaviourMap( triggerbindings.getConcatenatedBehaviourMap() ); + + // Register global actions, if any. + final Actions globalActions = guiModel.getGlobalActions(); + if ( globalActions != null ) + { + keybindings.addActionMap( "global", new WrappedActionMap( globalActions.getActionMap() ) ); + keybindings.addInputMap( "global", new WrappedInputMap( globalActions.getInputMap() ) ); + } + } + + protected void setWindow( final JFrame frame ) + { + frame.addWindowListener( new WindowAdapter() + { + @Override + public void windowClosing( final WindowEvent e ) + { + close(); + } + } ); + attachKeybindings( frame.getRootPane() ); + } + + private void attachKeybindings( final JComponent component ) + { + SwingUtilities.replaceUIActionMap( component, keybindings.getConcatenatedActionMap() ); + SwingUtilities.replaceUIInputMap( component, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, keybindings.getConcatenatedInputMap() ); + mouseAndKeyHandler.setKeypressManager( guiModel.getKeyPressedManager(), component ); + component.addKeyListener( mouseAndKeyHandler ); + component.addMouseListener( mouseAndKeyHandler ); + component.addMouseMotionListener( mouseAndKeyHandler ); + component.addMouseWheelListener( mouseAndKeyHandler ); + component.addFocusListener( mouseAndKeyHandler ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/AbstractTrackMateModelView.java b/src/main/java/fiji/plugin/trackmate/visualization/AbstractTrackMateModelView.java index a90af2f7a..1a04ba9e5 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/AbstractTrackMateModelView.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/AbstractTrackMateModelView.java @@ -21,22 +21,25 @@ */ package fiji.plugin.trackmate.visualization; +import java.util.ArrayList; import java.util.Map; +import javax.swing.SwingUtilities; + import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.ModelChangeListener; import fiji.plugin.trackmate.SelectionChangeEvent; import fiji.plugin.trackmate.SelectionChangeListener; import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.UpdateListener; /** - * An abstract class for spot displayers, that can overlay detected spots and - * tracks on top of the image data. - *

    + * An abstract class for TrackMate views. * - * @author Jean-Yves Tinevez <jeanyves.tinevez@gmail.com> Jan 2011 + * @author Jean-Yves Tinevez */ public abstract class AbstractTrackMateModelView implements SelectionChangeListener, TrackMateModelView, ModelChangeListener { @@ -45,30 +48,56 @@ public abstract class AbstractTrackMateModelView implements SelectionChangeListe * FIELDS */ - /** The model displayed by this class. */ - protected final Model model; - - protected final SelectionModel selectionModel; + protected final ArrayList< Runnable > runOnClose; - protected final DisplaySettings displaySettings; + protected final GuiModel guiModel; /* * PROTECTED CONSTRUCTOR */ - protected AbstractTrackMateModelView( final Model model, final SelectionModel selectionModel, final DisplaySettings displaySettings ) + protected AbstractTrackMateModelView( final GuiModel guiModel ) { - this.selectionModel = selectionModel; - this.model = model; - this.displaySettings = displaySettings; + this.guiModel = guiModel; + runOnClose = new ArrayList<>(); + + final Model model = guiModel.getModel(); + final SelectionModel selectionModel = guiModel.getSelectionModel(); + final DisplaySettings displaySettings = guiModel.getDisplaySettings(); model.addModelChangeListener( this ); selectionModel.addSelectionChangeListener( this ); + final UpdateListener refresher = () -> SwingUtilities.invokeLater( this::refresh ); + displaySettings.listeners().add( refresher ); + onClose( () -> { + model.removeModelChangeListener( this ); + selectionModel.removeSelectionChangeListener( this ); + displaySettings.listeners().remove( refresher ); + } ); } /* * PUBLIC METHODS */ + + /** + * Adds the specified {@link Runnable} to the list of runnables to execute + * when this view is closed. + * + * @param runnable + * the {@link Runnable} to add. + */ + public synchronized void onClose( final Runnable runnable ) + { + runOnClose.add( runnable ); + } + + protected synchronized void close() + { + runOnClose.forEach( Runnable::run ); + runOnClose.clear(); + } + /** * This needs to be overridden for concrete implementation to display * selection. @@ -90,8 +119,8 @@ public void selectionChanged( final SelectionChangeEvent event ) } @Override - public Model getModel() + public GuiModel getGuiModel() { - return model; + return guiModel; } } diff --git a/src/main/java/fiji/plugin/trackmate/visualization/PerEdgeFeatureColorGenerator.java b/src/main/java/fiji/plugin/trackmate/visualization/PerEdgeFeatureColorGenerator.java index 7eebd01ac..555b4fa78 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/PerEdgeFeatureColorGenerator.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/PerEdgeFeatureColorGenerator.java @@ -24,9 +24,9 @@ import java.awt.Color; import org.jgrapht.graph.DefaultWeightedEdge; +import org.scijava.ui.config.visitors.gui.elements.colormap.Colormap; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.gui.displaysettings.Colormap; public class PerEdgeFeatureColorGenerator implements TrackColorGenerator { diff --git a/src/main/java/fiji/plugin/trackmate/visualization/PerSpotFeatureColorGenerator.java b/src/main/java/fiji/plugin/trackmate/visualization/PerSpotFeatureColorGenerator.java index e19f219f0..e8c4f8539 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/PerSpotFeatureColorGenerator.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/PerSpotFeatureColorGenerator.java @@ -24,10 +24,10 @@ import java.awt.Color; import org.jgrapht.graph.DefaultWeightedEdge; +import org.scijava.ui.config.visitors.gui.elements.colormap.Colormap; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.gui.displaysettings.Colormap; public class PerSpotFeatureColorGenerator implements TrackColorGenerator { diff --git a/src/main/java/fiji/plugin/trackmate/visualization/PerTrackFeatureColorGenerator.java b/src/main/java/fiji/plugin/trackmate/visualization/PerTrackFeatureColorGenerator.java index 224158a22..cdf05954c 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/PerTrackFeatureColorGenerator.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/PerTrackFeatureColorGenerator.java @@ -30,12 +30,12 @@ import java.util.Set; import org.jgrapht.graph.DefaultWeightedEdge; +import org.scijava.ui.config.visitors.gui.elements.colormap.Colormap; import fiji.plugin.trackmate.FeatureModel; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.TrackModel; import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; -import fiji.plugin.trackmate.gui.displaysettings.Colormap; /** * A {@link TrackColorGenerator} that generate colors based on the whole track diff --git a/src/main/java/fiji/plugin/trackmate/visualization/SpotColorGenerator.java b/src/main/java/fiji/plugin/trackmate/visualization/SpotColorGenerator.java index 41d32ce75..56216010d 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/SpotColorGenerator.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/SpotColorGenerator.java @@ -23,8 +23,9 @@ import java.awt.Color; +import org.scijava.ui.config.visitors.gui.elements.colormap.Colormap; + import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.gui.displaysettings.Colormap; public class SpotColorGenerator implements FeatureColorGenerator< Spot > { diff --git a/src/main/java/fiji/plugin/trackmate/visualization/SpotColorGeneratorPerEdgeFeature.java b/src/main/java/fiji/plugin/trackmate/visualization/SpotColorGeneratorPerEdgeFeature.java index da70b6101..f2e743e86 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/SpotColorGeneratorPerEdgeFeature.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/SpotColorGeneratorPerEdgeFeature.java @@ -25,10 +25,10 @@ import java.util.Set; import org.jgrapht.graph.DefaultWeightedEdge; +import org.scijava.ui.config.visitors.gui.elements.colormap.Colormap; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.gui.displaysettings.Colormap; public class SpotColorGeneratorPerEdgeFeature implements FeatureColorGenerator< Spot > { diff --git a/src/main/java/fiji/plugin/trackmate/visualization/SpotColorGeneratorPerTrackFeature.java b/src/main/java/fiji/plugin/trackmate/visualization/SpotColorGeneratorPerTrackFeature.java index 03538ef8d..42e4ec85c 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/SpotColorGeneratorPerTrackFeature.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/SpotColorGeneratorPerTrackFeature.java @@ -23,9 +23,10 @@ import java.awt.Color; +import org.scijava.ui.config.visitors.gui.elements.colormap.Colormap; + import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.gui.displaysettings.Colormap; public class SpotColorGeneratorPerTrackFeature implements FeatureColorGenerator< Spot > { diff --git a/src/main/java/fiji/plugin/trackmate/visualization/TrackMateModelView.java b/src/main/java/fiji/plugin/trackmate/visualization/TrackMateModelView.java index b2b9831ea..327cdbe76 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/TrackMateModelView.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/TrackMateModelView.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -21,8 +21,12 @@ */ package fiji.plugin.trackmate.visualization; -import fiji.plugin.trackmate.Model; +import java.awt.Window; + +import javax.swing.text.ViewFactory; + import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.gui.GuiModel; public interface TrackMateModelView { @@ -51,18 +55,18 @@ public interface TrackMateModelView /** * Centers the view on the given spot. - * + * * @param spot - * the spot to center on. + * the spot to center the view on. */ public void centerViewOn( final Spot spot ); /** - * Returns the model displayed in this view. - * + * Returns the GUI model used in this view. + * * @return the model. */ - public Model getModel(); + public GuiModel getGuiModel(); /** * Returns the unique key that identifies this view. @@ -75,4 +79,10 @@ public interface TrackMateModelView */ public String getKey(); + /** + * Returns the window that contains this view, if any. + * + * @return the window. + */ + public Window getWindow(); } diff --git a/src/main/java/fiji/plugin/trackmate/visualization/ViewFactory.java b/src/main/java/fiji/plugin/trackmate/visualization/ViewFactory.java deleted file mode 100644 index 1913114a6..000000000 --- a/src/main/java/fiji/plugin/trackmate/visualization/ViewFactory.java +++ /dev/null @@ -1,49 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.visualization; - -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.Settings; -import fiji.plugin.trackmate.TrackMateModule; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; - -public interface ViewFactory extends TrackMateModule -{ - - /** - * Returns a new instance of the concrete view. - * - * @param model - * the model to display in the view. - * @param settings - * a {@link Settings} object, which specific implementation might - * use to display the model. - * @param selectionModel - * the {@link SelectionModel} model to share in the created view. - * @param displaySettings - * the display settings to use to paint the view. - * @return a new view of the specified model. - */ - public TrackMateModelView create( final Model model, final Settings settings, final SelectionModel selectionModel, final DisplaySettings displaySettings ); - -} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/ViewUtils.java b/src/main/java/fiji/plugin/trackmate/visualization/ViewUtils.java index 723054a4d..095b2d447 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/ViewUtils.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/ViewUtils.java @@ -65,7 +65,7 @@ public static final ImagePlus makeEmptyImagePlus( final int width, final int hei return imp; } - public static final ImagePlus makeEmpytImagePlus( final Model model ) + public static final ImagePlus makeEmptyImagePlus( final Model model ) { double maxX = 0; diff --git a/src/main/java/fiji/plugin/trackmate/visualization/WholeTrackFeatureColorGenerator.java b/src/main/java/fiji/plugin/trackmate/visualization/WholeTrackFeatureColorGenerator.java index 9a529eda3..f5087f9cb 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/WholeTrackFeatureColorGenerator.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/WholeTrackFeatureColorGenerator.java @@ -23,8 +23,9 @@ import java.awt.Color; +import org.scijava.ui.config.visitors.gui.elements.colormap.Colormap; + import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.gui.displaysettings.Colormap; public class WholeTrackFeatureColorGenerator implements FeatureColorGenerator< Integer > { diff --git a/src/main/java/fiji/plugin/trackmate/visualization/bvv/BVVKeymapManager.java b/src/main/java/fiji/plugin/trackmate/visualization/bvv/BVVKeymapManager.java new file mode 100644 index 000000000..b5e1411e4 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/bvv/BVVKeymapManager.java @@ -0,0 +1,42 @@ +package fiji.plugin.trackmate.visualization.bvv; + +import java.io.File; + +import org.scijava.Context; +import org.scijava.ui.behaviour.io.gui.CommandDescriptionsBuilder; + +import bdv.ui.keymap.KeymapManager; +import fiji.plugin.trackmate.util.TMUtils; + +/** + * Keymap manager for BigVolumeViewer (BVV) actions in TrackMate. + *

    + * This manager is separate from the global TrackMate keymap and handles + * BVV-specific key bindings (navigation, rotation, zoom, etc.). + * It uses its own subdirectory to avoid conflicts with other keymap managers. + */ +public class BVVKeymapManager extends KeymapManager +{ + + private static final String KEYMAP_HOME = new File( + new File( System.getProperty( "user.home" ), ".trackmate" ), "bvv" + ).getAbsolutePath(); + + public BVVKeymapManager() + { + super( KEYMAP_HOME ); + } + + @Override + public synchronized void discoverCommandDescriptions() + { + final CommandDescriptionsBuilder builder = new CommandDescriptionsBuilder(); + final Context context = TMUtils.getContext(); + context.inject( builder ); + // Discover BVV-specific command descriptions + builder.discoverProviders( + bvv.core.KeyConfigScopes.BIGVOLUMEVIEWER, + bdv.KeyConfigScopes.BIGDATAVIEWER ); + setCommandDescriptions( builder.build() ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/bvv/BVVUtils.java b/src/main/java/fiji/plugin/trackmate/visualization/bvv/BVVUtils.java new file mode 100644 index 000000000..580dec9b2 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/bvv/BVVUtils.java @@ -0,0 +1,245 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.visualization.bvv; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; + +import org.scijava.ui.behaviour.io.InputTriggerConfig; + +import bdv.BigDataViewer; +import bdv.cache.CacheControl.CacheControls; +import bdv.tools.brightness.ConverterSetup; +import bdv.ui.appearance.AppearanceManager; +import bdv.util.RandomAccessibleIntervalSource; +import bdv.util.RandomAccessibleIntervalSource4D; +import bdv.viewer.ConverterSetups; +import bdv.viewer.DisplayMode; +import bdv.viewer.Source; +import bdv.viewer.SourceAndConverter; +import bvv.core.BigVolumeViewer; +import bvv.core.VolumeViewerOptions; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.util.TMUtils; +import ij.CompositeImage; +import ij.ImagePlus; +import ij.measure.Calibration; +import ij.process.LUT; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.converter.Converter; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.mesh.util.Icosahedron; +import net.imglib2.mesh.view.TranslateMesh; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.type.numeric.RealType; + +public class BVVUtils +{ + + public static final StupidMesh createMesh( final Spot spot ) + { + if ( spot instanceof SpotMesh ) + { + final SpotMesh sm = ( SpotMesh ) spot; + final Mesh mesh = TranslateMesh.translate( sm.getMesh(), spot ); + final BufferMesh bm = new BufferMesh( mesh.vertices().size(), mesh.triangles().size() ); + Meshes.copy( mesh, bm ); + return new StupidMesh( bm ); + } + return new StupidMesh( Icosahedron.sphere( spot, spot.getFeature( Spot.RADIUS ).doubleValue() ) ); + } + + public static final < T extends RealType< T > > BigVolumeViewer createBvv( final GuiModel guiModel ) + { + + /* + * Wire BVV options to TrackMate config objects. + */ + + final ImagePlus imp = guiModel.getSettings().imp; + final BVVKeymapManager bvvKeymapManager = guiModel.getBvvKeymapManager(); + final InputTriggerConfig config = bvvKeymapManager.getForwardSelectedKeymap().getConfig(); + final AppearanceManager appearanceManager = guiModel.getAppearanceManager(); + + final VolumeViewerOptions options = VolumeViewerOptions.options() + .inputTriggerConfig( config ) + .maxAllowedStepInVoxels( 0 ) + .renderWidth( 1024 ) + .renderHeight( 1024 ) + .keymapManager( bvvKeymapManager ) + .appearanceManager( appearanceManager ) + .shareKeyPressedEvents( guiModel.getKeyPressedManager() ) + .height( 512 ) + .width( 512 ); + + /* + * Create BVV sources + */ + + // Scaling + final Calibration cal = imp.getCalibration(); + final AffineTransform3D sourceTransform = new AffineTransform3D(); + sourceTransform.set( + cal.pixelWidth, 0, 0, 0, + 0, cal.pixelHeight, 0, 0, + 0, 0, cal.pixelDepth, 0 ); + + // Image data + final ImgPlus< T > img = TMUtils.rawWraps( imp ); + final int cAxis = img.dimensionIndex( Axes.CHANNEL ); + final int nChannels = ( int ) ( ( cAxis < 0 ) ? 1 : img.dimension( cAxis ) ); + final int tAxis = img.dimensionIndex( Axes.TIME ); + final int nTimePoints = ( int ) ( ( tAxis < 0 ) ? 0 : img.dimension( tAxis ) ); + + // Source and converter setup + final List< SourceAndConverter< ? > > sources = new ArrayList<>( nChannels ); + final List< ConverterSetup > setups = new ArrayList< ConverterSetup >( nChannels ); + for ( int c = 0; c < nChannels; c++ ) + { + final RandomAccessibleInterval< T > channelRai = + ( cAxis < 0 ) + ? img + : img.view().slice( cAxis, c ); + + final String sourceName = ( cAxis < 0 ) ? "" : "Ch " + ( c + 1 ); + final Source< T > source; + if ( nTimePoints > 1 ) + { + source = new RandomAccessibleIntervalSource4D<>( + channelRai, + channelRai.getType(), + sourceTransform, + sourceName ); + } + else + { + source = new RandomAccessibleIntervalSource<>( + channelRai, + channelRai.getType(), + sourceTransform, + sourceName ); + } + + final Converter< T, ARGBType > converterToARGB = BigDataViewer.createConverterToARGB( channelRai.getType() ); + final SourceAndConverter< T > soc = new SourceAndConverter< T >( source, converterToARGB ); + sources.add( soc ); + final ConverterSetup setup = BigDataViewer.createConverterSetup( soc, c ); + setups.add( setup ); + } + + final CacheControls cacheControl = new CacheControls(); + final String title = "3D view " + imp.getShortTitle(); + final BigVolumeViewer bvv = new BigVolumeViewer( setups, sources, nTimePoints, cacheControl, title, options ); + syncDisplayAndLUTs( bvv, sources, imp ); + return bvv; + } + + public static void syncDisplayAndLUTs( + final BigVolumeViewer bvv, + final List< SourceAndConverter< ? > > sources, + final ImagePlus imp ) + { + // Display mode. + final DisplayMode displayMode = getDisplayMode( imp ); + bvv.getViewer().setDisplayMode( displayMode ); + + // LUT & min max + final ConverterSetups converterSetups = bvv.getConverterSetups(); + final int nChannels = sources.size(); + for ( int c = 0; c < nChannels; c++ ) + { + final List< ConverterSetup > css = converterSetups.getConverterSetups( sources ); + final ConverterSetup setup = css.get( c ); + + double minRange; + double maxRange; + Color channelColor; + if ( imp instanceof CompositeImage ) + { + final CompositeImage ci = ( CompositeImage ) imp; + final LUT lut = ci.getChannelLut( c + 1 ); + minRange = lut.min; + maxRange = lut.max; + channelColor = new Color( lut.getRGB( 255 ) ); + } + else + { + imp.setPosition( c + 1, 1, 1 ); + minRange = imp.getDisplayRangeMin(); + maxRange = imp.getDisplayRangeMax(); + + final LUT lut = imp.getProcessor().getLut(); + if ( lut != null ) + channelColor = new Color( lut.getRGB( 255 ) ); + else + channelColor = Color.WHITE; + } + + // Apply + setup.setDisplayRange( minRange, maxRange ); + final int argb = ARGBType.rgba( + channelColor.getRed(), + channelColor.getGreen(), + channelColor.getBlue(), + 255 ); + setup.setColor( new ARGBType( argb ) ); + } + } + + /** + * Determines the BVV DisplayMode based on an ImagePlus instance. + */ + public static DisplayMode getDisplayMode( final ImagePlus imp ) + { + // Check if the ImagePlus is a CompositeImage (multi-channel UI mode) + if ( imp instanceof CompositeImage ) + { + final CompositeImage ci = ( CompositeImage ) imp; + + switch ( ci.getMode() ) + { + case CompositeImage.COMPOSITE: + return DisplayMode.FUSED; + + case CompositeImage.COLOR: + case CompositeImage.GRAYSCALE: + return DisplayMode.SINGLE; + + default: + return DisplayMode.FUSED; + } + } + + if ( imp.getNChannels() > 1 ) + return DisplayMode.FUSED; + else + return DisplayMode.SINGLE; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/bvv/Icosahedron.java b/src/main/java/fiji/plugin/trackmate/visualization/bvv/Icosahedron.java new file mode 100644 index 000000000..1b19c3a4b --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/bvv/Icosahedron.java @@ -0,0 +1,154 @@ +package fiji.plugin.trackmate.visualization.bvv; + +import fiji.plugin.trackmate.Spot; +import net.imglib2.RealLocalizable; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.Triangle; +import net.imglib2.mesh.impl.naive.NaiveDoubleMesh; +import net.imglib2.mesh.impl.nio.BufferMesh; + +/** + * Icosahedron spheres. + *

    + * Based on https://github.com/caosdoar/spheres + * + * @author Jean-Yves Tinevez + */ +public class Icosahedron +{ + + public static final Mesh core() + { + final NaiveDoubleMesh mesh = new NaiveDoubleMesh(); + + // Vertices + final double t = ( 1. + Math.sqrt( 5. ) ) / 2.; + final double[][] vs = new double[][] { + { -1.0, t, 0.0 }, + { 1.0, t, 0.0 }, + { -1.0, -t, 0.0 }, + { 1.0, -t, 0.0 }, + { 0.0, -1.0, t }, + { 0.0, 1.0, t }, + { 0.0, -1.0, -t }, + { 0.0, 1.0, -t }, + { t, 0.0, -1.0 }, + { t, 0.0, 1.0 }, + { -t, 0.0, -1.0 }, + { -t, 0.0, 1.0 } + }; + final double[] tmp = new double[ 3 ]; + for ( final double[] v : vs ) + { + normalize( v, tmp ); + mesh.vertices().add( tmp[ 0 ], tmp[ 1 ], tmp[ 2 ] ); + } + + // Faces + mesh.triangles().add( 0, 11, 5 ); + mesh.triangles().add( 0, 5, 1 ); + mesh.triangles().add( 0, 1, 7 ); + mesh.triangles().add( 0, 7, 10 ); + mesh.triangles().add( 0, 10, 11 ); + mesh.triangles().add( 1, 5, 9 ); + mesh.triangles().add( 5, 11, 4 ); + mesh.triangles().add( 11, 10, 2 ); + mesh.triangles().add( 10, 7, 6 ); + mesh.triangles().add( 7, 1, 8 ); + mesh.triangles().add( 3, 9, 4 ); + mesh.triangles().add( 3, 4, 2 ); + mesh.triangles().add( 3, 2, 6 ); + mesh.triangles().add( 3, 6, 8 ); + mesh.triangles().add( 3, 8, 9 ); + mesh.triangles().add( 4, 9, 5 ); + mesh.triangles().add( 2, 4, 11 ); + mesh.triangles().add( 6, 2, 10 ); + mesh.triangles().add( 8, 6, 7 ); + mesh.triangles().add( 9, 8, 1 ); + return mesh; + } + + public static final BufferMesh refine( final Mesh core ) + { + final int nVerticesOut = 6 * core.triangles().size(); + final int nTrianglesOut = 4 * core.triangles().size(); + final BufferMesh out = new BufferMesh( nVerticesOut, nTrianglesOut ); + + final double[] tmpIn = new double[ 3 ]; + final double[] tmpOut = new double[ 3 ]; + for ( final Triangle t : core.triangles() ) + { + final long v0 = out.vertices().add( t.v0x(), t.v0y(), t.v0z() ); + final long v1 = out.vertices().add( t.v1x(), t.v1y(), t.v1z() ); + final long v2 = out.vertices().add( t.v2x(), t.v2y(), t.v2z() ); + + tmpIn[ 0 ] = 0.5 * ( t.v0xf() + t.v1xf() ); + tmpIn[ 1 ] = 0.5 * ( t.v0yf() + t.v1yf() ); + tmpIn[ 2 ] = 0.5 * ( t.v0zf() + t.v1zf() ); + normalize( tmpIn, tmpOut ); + final long v3 = out.vertices().add( tmpOut[ 0 ], tmpOut[ 1 ], tmpOut[ 2 ] ); + + tmpIn[ 0 ] = 0.5 * ( t.v2xf() + t.v1xf() ); + tmpIn[ 1 ] = 0.5 * ( t.v2yf() + t.v1yf() ); + tmpIn[ 2 ] = 0.5 * ( t.v2zf() + t.v1zf() ); + normalize( tmpIn, tmpOut ); + final long v4 = out.vertices().add( tmpOut[ 0 ], tmpOut[ 1 ], tmpOut[ 2 ] ); + + tmpIn[ 0 ] = 0.5 * ( t.v0xf() + t.v2xf() ); + tmpIn[ 1 ] = 0.5 * ( t.v0yf() + t.v2yf() ); + tmpIn[ 2 ] = 0.5 * ( t.v0zf() + t.v2zf() ); + normalize( tmpIn, tmpOut ); + final long v5 = out.vertices().add( tmpOut[ 0 ], tmpOut[ 1 ], tmpOut[ 2 ] ); + + out.triangles().add( v0, v3, v5 ); + out.triangles().add( v3, v1, v4 ); + out.triangles().add( v5, v4, v2 ); + out.triangles().add( v3, v4, v5 ); + } + + return out; + } + + public static BufferMesh sphere( final Spot spot ) + { + return sphere( spot, spot.getFeature( Spot.RADIUS ) ); + } + + public static BufferMesh sphere( final RealLocalizable center, final double radius ) + { + return sphere( center, radius, 3 ); + } + + public static BufferMesh sphere( final RealLocalizable center, final double radius, final int nSubdivisions ) + { + Mesh mesh = core(); + for ( int i = 0; i < nSubdivisions; i++ ) + mesh = refine( mesh ); + + scale( mesh, center, radius ); + final BufferMesh out = new BufferMesh( mesh.vertices().size(), mesh.triangles().size() ); + Meshes.calculateNormals( mesh, out ); + return out; + } + + private static void scale( final Mesh mesh, final RealLocalizable center, final double radius ) + { + final long nV = mesh.vertices().size(); + for ( int i = 0; i < nV; i++ ) + { + final double x = mesh.vertices().x( i ) * radius + center.getDoublePosition( 0 ); + final double y = mesh.vertices().y( i ) * radius + center.getDoublePosition( 1 ); + final double z = mesh.vertices().z( i ) * radius + center.getDoublePosition( 2 ); + mesh.vertices().set( i, x, y, z ); + } + } + + private static void normalize( final double[] v, final double[] tmp ) + { + final double l = Math.sqrt( v[ 0 ] * v[ 0 ] + v[ 1 ] * v[ 1 ] + v[ 2 ] * v[ 2 ] ); + tmp[ 0 ] = v[ 0 ] / l; + tmp[ 1 ] = v[ 1 ] / l; + tmp[ 2 ] = v[ 2 ] / l; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/bvv/StupidMesh.java b/src/main/java/fiji/plugin/trackmate/visualization/bvv/StupidMesh.java new file mode 100644 index 000000000..1661ca793 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/bvv/StupidMesh.java @@ -0,0 +1,149 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.visualization.bvv; + +import static com.jogamp.opengl.GL.GL_FLOAT; +import static com.jogamp.opengl.GL.GL_TRIANGLES; +import static com.jogamp.opengl.GL.GL_UNSIGNED_INT; + +import java.awt.Color; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Matrix4fc; + +import com.jogamp.opengl.GL; +import com.jogamp.opengl.GL3; + +import bvv.core.backend.jogl.JoglGpuContext; +import bvv.core.shadergen.DefaultShader; +import bvv.core.shadergen.Shader; +import bvv.core.shadergen.generate.Segment; +import bvv.core.shadergen.generate.SegmentTemplate; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import net.imglib2.mesh.impl.nio.BufferMesh; + +public class StupidMesh +{ + private final Shader prog; + + private final BufferMesh mesh; + + private boolean initialized; + + private int vao; + + private final float[] carr = new float[ 4 ]; + + private final float[] scarr = new float[ 4 ]; + + public StupidMesh( final BufferMesh mesh ) + { + this.mesh = mesh; + final Segment meshVp = new SegmentTemplate( StupidMesh.class, "mesh.vp" ).instantiate(); + final Segment meshFp = new SegmentTemplate( StupidMesh.class, "mesh.fp" ).instantiate(); + prog = new DefaultShader( meshVp.getCode(), meshFp.getCode() ); + DisplaySettings.defaultStyle().getSpotUniformColor().getColorComponents( carr ); + DisplaySettings.defaultStyle().getHighlightColor().getColorComponents( scarr ); + } + + private void init( final GL3 gl ) + { + initialized = true; + + final int[] tmp = new int[ 3 ]; + gl.glGenBuffers( 3, tmp, 0 ); + final int meshPosVbo = tmp[ 0 ]; + final int meshNormalVbo = tmp[ 1 ]; + final int meshEbo = tmp[ 2 ]; + + final FloatBuffer vertices = mesh.vertices().verts(); + vertices.rewind(); + gl.glBindBuffer( GL.GL_ARRAY_BUFFER, meshPosVbo ); + gl.glBufferData( GL.GL_ARRAY_BUFFER, vertices.limit() * Float.BYTES, vertices, GL.GL_STATIC_DRAW ); + gl.glBindBuffer( GL.GL_ARRAY_BUFFER, 0 ); + + final FloatBuffer normals = mesh.vertices().normals(); + normals.rewind(); + gl.glBindBuffer( GL.GL_ARRAY_BUFFER, meshNormalVbo ); + gl.glBufferData( GL.GL_ARRAY_BUFFER, normals.limit() * Float.BYTES, normals, GL.GL_STATIC_DRAW ); + gl.glBindBuffer( GL.GL_ARRAY_BUFFER, 0 ); + + final IntBuffer indices = mesh.triangles().indices(); + indices.rewind(); + gl.glBindBuffer( GL.GL_ELEMENT_ARRAY_BUFFER, meshEbo ); + gl.glBufferData( GL.GL_ELEMENT_ARRAY_BUFFER, indices.limit() * Integer.BYTES, indices, GL.GL_STATIC_DRAW ); + gl.glBindBuffer( GL.GL_ELEMENT_ARRAY_BUFFER, 0 ); + + gl.glGenVertexArrays( 1, tmp, 0 ); + vao = tmp[ 0 ]; + gl.glBindVertexArray( vao ); + gl.glBindBuffer( GL.GL_ARRAY_BUFFER, meshPosVbo ); + gl.glVertexAttribPointer( 0, 3, GL_FLOAT, false, 3 * Float.BYTES, 0 ); + gl.glEnableVertexAttribArray( 0 ); + gl.glBindBuffer( GL.GL_ARRAY_BUFFER, meshNormalVbo ); + gl.glVertexAttribPointer( 1, 3, GL_FLOAT, false, 3 * Float.BYTES, 0 ); + gl.glEnableVertexAttribArray( 1 ); + gl.glBindBuffer( GL.GL_ELEMENT_ARRAY_BUFFER, meshEbo ); + gl.glBindVertexArray( 0 ); + } + + public void setColor( final Color color, final float alpha ) + { + color.getComponents( carr ); + carr[ 3 ] = alpha; + } + + public void setSelectionColor( final Color selectionColor, final float alpha ) + { + selectionColor.getComponents( scarr ); + scarr[ 3 ] = alpha; + } + + public void draw( final GL3 gl, final Matrix4fc pvm, final Matrix4fc vm, final boolean isSelected ) + { + if ( !initialized ) + init( gl ); + + final JoglGpuContext context = JoglGpuContext.get( gl ); + final Matrix4f itvm = vm.invert( new Matrix4f() ).transpose(); + + prog.getUniformMatrix4f( "pvm" ).set( pvm ); + prog.getUniformMatrix4f( "vm" ).set( vm ); + prog.getUniformMatrix3f( "itvm" ).set( itvm.get3x3( new Matrix3f() ) ); + prog.getUniform4f( "ObjectColor" ).set( carr[ 0 ], carr[ 1 ], carr[ 2 ], carr[ 3 ] ); + prog.getUniform1f( "IsSelected" ).set( isSelected ? 1f : 0f ); + prog.getUniform4f( "SelectionColor" ).set( scarr[ 0 ], scarr[ 1 ], scarr[ 2 ], scarr[ 3 ] ); + prog.setUniforms( context ); + prog.use( context ); + + gl.glBindVertexArray( vao ); + gl.glEnable( GL.GL_CULL_FACE ); + gl.glCullFace( GL.GL_BACK ); + gl.glFrontFace( GL.GL_CCW ); + gl.glDrawElements( GL_TRIANGLES, mesh.triangles().size() * 3, GL_UNSIGNED_INT, 0 ); + gl.glBindVertexArray( 0 ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/bvv/TrackMateBVV.java b/src/main/java/fiji/plugin/trackmate/visualization/bvv/TrackMateBVV.java new file mode 100644 index 000000000..b94df3ea5 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/bvv/TrackMateBVV.java @@ -0,0 +1,247 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.visualization.bvv; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Window; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.joml.Matrix4f; +import org.scijava.ui.config.utils.GuiUtils; + +import bdv.tools.InitializeViewerState; +import bdv.viewer.animate.TranslationAnimator; +import bvv.core.BigVolumeViewer; +import bvv.core.VolumeViewerFrame; +import bvv.core.VolumeViewerPanel; +import bvv.core.util.MatrixMath; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; +import fiji.plugin.trackmate.SelectionChangeListener; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.features.FeatureUtils; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.UpdateListener; +import fiji.plugin.trackmate.visualization.AbstractTrackMateModelBvvView; +import fiji.plugin.trackmate.visualization.FeatureColorGenerator; +import fiji.plugin.trackmate.visualization.ui.KeyConfigContexts; +import ij.ImagePlus; +import net.imglib2.RealLocalizable; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.Type; + +public class TrackMateBVV< T extends Type< T > > extends AbstractTrackMateModelBvvView +{ + + private static final String KEY = "BIGVOLUMEVIEWER"; + + private final Map< Spot, StupidMesh > meshMap; + + private final BigVolumeViewer bvvInstance; + + public TrackMateBVV( final GuiModel guiModel, final ImagePlus imp ) + { + super( guiModel, guiModel.getBvvKeymapManager(), KeyConfigContexts.BIGVOLUMEVIEWER, bvv.core.KeyConfigContexts.BIGVOLUMEVIEWER ); + this.meshMap = new HashMap<>(); + + final Model model = guiModel.getModel(); + final SelectionModel selectionModel = guiModel.getSelectionModel(); + final DisplaySettings displaySettings = guiModel.getDisplaySettings(); + final Iterable< Spot > it = model.getSpots().iterable( true ); + it.forEach( s -> meshMap.computeIfAbsent( s, BVVUtils::createMesh ) ); + updateColor(); + final UpdateListener colorUpdater = () -> updateColor(); + displaySettings.listeners().add( colorUpdater ); + final SelectionChangeListener refresher = e -> refresh(); + selectionModel.addSelectionChangeListener( refresher ); + onClose( () -> { + displaySettings.listeners().remove( colorUpdater ); + selectionModel.removeSelectionChangeListener( refresher ); + } ); + + this.bvvInstance = BVVUtils.createBvv( guiModel ); + + final VolumeViewerPanel viewer = bvvInstance.getViewer(); + viewer.setRenderScene( ( gl, data ) -> { + if ( guiModel.getDisplaySettings().isSpotVisible() ) + { + final Matrix4f pvm = new Matrix4f( data.getPv() ); + final Matrix4f view = MatrixMath.affine( data.getRenderTransformWorldToScreen(), new Matrix4f() ); + final Matrix4f vm = MatrixMath.screen( data.getDCam(), data.getScreenWidth(), data.getScreenHeight(), new Matrix4f() ).mul( view ); + + final int t = data.getTimepoint(); + final Iterable< Spot > its = guiModel.getModel().getSpots().iterable( t, true ); + its.forEach( s -> meshMap.computeIfAbsent( s, BVVUtils::createMesh ).draw( gl, pvm, vm, guiModel.getSelectionModel().getSpotSelection().contains( s ) ) ); + } + } ); + synchronized ( this ) + { + initTransformPending = true; + tryInitTransform( viewer ); + } + + final VolumeViewerFrame frame = bvvInstance.getViewerFrame(); + setWindow( frame ); + GuiUtils.positionWindow( frame, guiModel.getSettings().imp.getWindow() ); + } + + @Override + public void render() + { + bvvInstance.getViewerFrame().setVisible( true ); + } + + @Override + public void refresh() + { + if ( bvvInstance != null ) + bvvInstance.getViewer().requestRepaint(); + } + + @Override + public void clear() + {} + + @Override + public void centerViewOn( final Spot spot ) + { + if ( bvvInstance == null ) + return; + + final VolumeViewerPanel panel = bvvInstance.getViewer(); + panel.setTimepoint( spot.getFeature( Spot.FRAME ).intValue() ); + + final AffineTransform3D c = panel.state().getViewerTransform(); + final double[] translation = getTranslation( c, spot, panel.getWidth(), panel.getHeight() ); + if ( translation != null ) + { + final TranslationAnimator animator = new TranslationAnimator( c, translation, 300 ); + animator.setTime( System.currentTimeMillis() ); + panel.setTransformAnimator( animator ); + } + } + + /** + * Returns a translation vector that will put the specified position at the + * center of the panel when used with a TranslationAnimator. + * + * @param t + * the viewer panel current view transform. + * @param target + * the position to focus on. + * @param width + * the width of the panel. + * @param height + * the height of the panel. + * @return a new double[] array with 3 elements containing the + * translation to use. + */ + private static final double[] getTranslation( final AffineTransform3D t, final RealLocalizable target, final int width, final int height ) + { + final double[] pos = new double[ 3 ]; + final double[] vPos = new double[ 3 ]; + target.localize( pos ); + t.apply( pos, vPos ); + + final double dx = width / 2 - vPos[ 0 ] + t.get( 0, 3 ); + final double dy = height / 2 - vPos[ 1 ] + t.get( 1, 3 ); + final double dz = -vPos[ 2 ] + t.get( 2, 3 ); + + return new double[] { dx, dy, dz }; + } + + @Override + public String getKey() + { + return KEY; + } + + @Override + public void modelChanged( final ModelChangeEvent event ) + { + switch ( event.getEventID() ) + { + case ModelChangeEvent.SPOTS_FILTERED: + case ModelChangeEvent.SPOTS_COMPUTED: + case ModelChangeEvent.TRACKS_VISIBILITY_CHANGED: + case ModelChangeEvent.TRACKS_COMPUTED: + refresh(); + break; + case ModelChangeEvent.MODEL_MODIFIED: + { + for ( final Spot spot : event.getSpots() ) + { + final StupidMesh mesh = BVVUtils.createMesh( spot ); + meshMap.put( spot, mesh ); + } + updateColor(); + refresh(); + break; + } + } + } + + private void updateColor() + { + final FeatureColorGenerator< Spot > spotColorGenerator = FeatureUtils.createSpotColorGenerator( guiModel.getModel(), guiModel.getDisplaySettings() ); + for ( final Entry< Spot, StupidMesh > entry : meshMap.entrySet() ) + { + final StupidMesh sm = entry.getValue(); + if ( sm == null ) + continue; + + final Color color = spotColorGenerator.color( entry.getKey() ); + final float alpha = ( float ) guiModel.getDisplaySettings().getSpotTransparencyAlpha(); + sm.setColor( color, alpha ); + sm.setSelectionColor( guiModel.getDisplaySettings().getHighlightColor(), alpha ); + } + refresh(); + } + + @Override + public Window getWindow() + { + return bvvInstance.getViewerFrame(); + } + + private boolean initTransformPending; + + private synchronized void tryInitTransform( final VolumeViewerPanel viewer ) + { + if ( viewer.getDisplay().getWidth() <= 0 || viewer.getDisplay().getHeight() <= 0 ) + return; + + if ( initTransformPending ) + { + initTransformPending = false; + + final Dimension dim = viewer.getDisplay().getSize(); + final AffineTransform3D viewerTransform = InitializeViewerState.initTransform( dim.width, dim.height, false, viewer.state().snapshot() ); + viewer.state().setViewerTransform( viewerTransform ); + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/HyperStackDisplayer.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/HyperStackDisplayer.java index 83ac11865..0d53ca26e 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/HyperStackDisplayer.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/HyperStackDisplayer.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -21,14 +21,30 @@ */ package fiji.plugin.trackmate.visualization.hyperstack; +import java.awt.Window; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import org.scijava.ui.behaviour.util.Actions; +import org.scijava.ui.behaviour.util.WrappedActionMap; +import org.scijava.ui.behaviour.util.WrappedInputMap; + 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.gui.GuiModel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.UpdateListener; import fiji.plugin.trackmate.visualization.AbstractTrackMateModelView; import fiji.plugin.trackmate.visualization.ViewUtils; +import fiji.plugin.trackmate.visualization.hyperstack.behaviours.HyperStackDisplayerActions; +import fiji.plugin.trackmate.visualization.hyperstack.behaviours.ImagePlusBehavioursAdapter; +import fiji.plugin.trackmate.visualization.hyperstack.behaviours.SelectSpotsWithRoiListener; +import fiji.plugin.trackmate.visualization.hyperstack.behaviours.SpotEditBehaviours; +import fiji.plugin.trackmate.visualization.ui.KeyConfigContexts; +import fiji.plugin.trackmate.visualization.ui.TrackMateKeymapManager; import ij.ImagePlus; import ij.gui.Overlay; import ij.gui.Roi; @@ -38,11 +54,9 @@ public class HyperStackDisplayer extends AbstractTrackMateModelView protected final ImagePlus imp; - protected SpotOverlay spotOverlay; - - protected TrackOverlay trackOverlay; + protected final SpotOverlay spotOverlay; - private SpotEditTool editTool; + protected final TrackOverlay trackOverlay; public static final String KEY = "HYPERSTACKDISPLAYER"; @@ -50,60 +64,23 @@ public class HyperStackDisplayer extends AbstractTrackMateModelView * CONSTRUCTORS */ - public HyperStackDisplayer( final Model model, final SelectionModel selectionModel, final ImagePlus imp, final DisplaySettings displaySettings ) + public HyperStackDisplayer( final GuiModel guiModel ) { - super( model, selectionModel, displaySettings ); - if ( null != imp ) - this.imp = imp; + super( guiModel ); + if ( null != guiModel.getSettings().imp ) + this.imp = guiModel.getSettings().imp; else - this.imp = ViewUtils.makeEmpytImagePlus( model ); - - this.spotOverlay = createSpotOverlay( displaySettings ); - this.trackOverlay = createTrackOverlay( displaySettings ); - displaySettings.listeners().add( () -> refresh() ); - } + this.imp = ViewUtils.makeEmptyImagePlus( guiModel.getModel() ); - public HyperStackDisplayer( final Model model, final SelectionModel selectionModel, final DisplaySettings displaySettings ) - { - this( model, selectionModel, null, displaySettings ); - } - - /* - * PROTECTED METHODS - */ + final DisplaySettings displaySettings = guiModel.getDisplaySettings(); + this.spotOverlay = new SpotOverlay( guiModel.getModel(), imp, guiModel.getDisplaySettings() ); + this.trackOverlay = new TrackOverlay( guiModel.getModel(), imp, guiModel.getDisplaySettings() ); - /** - * Hook for subclassers. Instantiate here the overlay you want to use for - * the spots. - * - * @param displaySettings - * the display settings to use in the overlay. - * - * @return the spot overlay - */ - protected SpotOverlay createSpotOverlay( final DisplaySettings displaySettings ) - { - return new SpotOverlay( model, imp, displaySettings ); - } - - /** - * Hook for subclassers. Instantiate here the overlay you want to use for - * the spots. - * - * @param displaySettings - * the display settings to use in the overlay. - * - * @return the track overlay - */ - protected TrackOverlay createTrackOverlay( final DisplaySettings displaySettings ) - { - return new TrackOverlay( model, imp, displaySettings ); + final UpdateListener refresher = () -> refresh(); + displaySettings.listeners().add( refresher ); + onClose( () -> displaySettings.listeners().remove( refresher ) ); } - /* - * PUBLIC METHODS - */ - /** * Exposes the {@link ImagePlus} on which the model is drawn by this view. * @@ -133,8 +110,8 @@ public void modelChanged( final ModelChangeEvent event ) public void selectionChanged( final SelectionChangeEvent event ) { // Highlight selection - trackOverlay.setHighlight( selectionModel.getEdgeSelection() ); - spotOverlay.setSpotSelection( selectionModel.getSpotSelection() ); + trackOverlay.setHighlight( guiModel.getSelectionModel().getEdgeSelection() ); + spotOverlay.setSpotSelection( guiModel.getSelectionModel().getSpotSelection() ); // Center on last spot super.selectionChanged( event ); // Redraw @@ -158,10 +135,42 @@ public void render() if ( !imp.isVisible() ) imp.show(); + imp.getWindow().addWindowListener( new WindowAdapter() + { + @Override + public void windowClosing( final WindowEvent e ) + { + close(); + } + } ); + addOverlay( spotOverlay ); addOverlay( trackOverlay ); imp.updateAndDraw(); - registerEditTool(); + + /* + * UI behaviours and actions + */ + + try + { + final Model model = guiModel.getModel(); + final SelectionModel selectionModel = guiModel.getSelectionModel(); + final TrackMateKeymapManager keymapManager = guiModel.getKeymapManager(); + final ImagePlusBehavioursAdapter adapter = new ImagePlusBehavioursAdapter( imp, keymapManager, new String[] { KeyConfigContexts.HYPERSTACK_DISPLAYER, KeyConfigContexts.TRACKMATE } ); + SpotEditBehaviours.install( adapter.behaviours(), model, selectionModel, imp ); + HyperStackDisplayerActions.install( adapter.actions(), guiModel, imp ); + // Select spots with freehand ROI. + SelectSpotsWithRoiListener.install( model, selectionModel, imp ); + // Global actions. + final Actions globalActions = guiModel.getGlobalActions(); + adapter.keybindings().addActionMap( "global", new WrappedActionMap( globalActions.getActionMap() ) ); + adapter.keybindings().addInputMap( "global", new WrappedInputMap( globalActions.getInputMap() ) ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } } @Override @@ -189,27 +198,15 @@ public void addOverlay( final Roi overlay ) imp.getOverlay().add( overlay ); } - public SelectionModel getSelectionModel() - { - return selectionModel; - } - - /* - * PRIVATE METHODS - */ - - private void registerEditTool() + @Override + public String getKey() { - editTool = SpotEditTool.getInstance(); - if ( !SpotEditTool.isLaunched() ) - editTool.run( "" ); - - editTool.register( this ); + return KEY; } @Override - public String getKey() + public Window getWindow() { - return KEY; + return imp.getWindow(); } } diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/HyperStackDisplayerFactory.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/HyperStackDisplayerFactory.java deleted file mode 100644 index a1bcd4e6e..000000000 --- a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/HyperStackDisplayerFactory.java +++ /dev/null @@ -1,74 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.visualization.hyperstack; - -import javax.swing.ImageIcon; - -import org.scijava.plugin.Plugin; - -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.Settings; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; -import fiji.plugin.trackmate.visualization.TrackMateModelView; -import fiji.plugin.trackmate.visualization.ViewFactory; -import ij.ImagePlus; - -@Plugin( type = ViewFactory.class ) -public class HyperStackDisplayerFactory implements ViewFactory -{ - - private static final String INFO_TEXT = "" + "This displayer overlays the spots and tracks on the current
    " + "ImageJ hyperstack window.
    " + "

    " + "This displayer allows manual editing of spots, thanks to the spot
    " + "edit tool that appear in ImageJ toolbar." + "

    " + "Double-clicking in a spot toggles the editing mode: The spot can
    " + "be moved around in a XY plane by mouse dragging. To move it in Z
    " + "or in time, simply change the current plane and time-point by
    " + "using the hyperstack sliders. To change its radius, hold the
    " + "alt key down and rotate the mouse-wheel. Holding the
    " + "shift key on top changes it faster. " + "

    " + "Alternatively, keyboard can be used to edit spots:
    " + " - A creates a new spot under the mouse.
    " + " - D deletes the spot under the mouse.
    " + " - Q and E decreases and increases the radius of the spot " + "under the mouse (shift to go faster).
    " + " - Space + mouse drag moves the spot under the mouse.
    " + "

    " + "To toggle links between two spots, select two spots (Shift+Click),
    " + "then press L. " + "

    " + "Shift+L toggle the auto-linking mode on/off.
    " + "If on, every spot created will be automatically linked with the spot
    " + "currently selected, if they are in subsequent frames." + ""; - - private static final String NAME = "HyperStack Displayer"; - - @Override - public TrackMateModelView create( final Model model, final Settings settings, final SelectionModel selectionModel, final DisplaySettings displaySettings ) - { - final ImagePlus imp = ( settings == null ) ? null : settings.imp; - return new HyperStackDisplayer( model, selectionModel, imp, displaySettings ); - } - - @Override - public String getInfoText() - { - return INFO_TEXT; - } - - @Override - public String getName() - { - return NAME; - } - - @Override - public String getKey() - { - return HyperStackDisplayer.KEY; - } - - @Override - public ImageIcon getIcon() - { - return null; - } -} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/ModelEditActions.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/ModelEditActions.java deleted file mode 100644 index 1bdcb4c9a..000000000 --- a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/ModelEditActions.java +++ /dev/null @@ -1,555 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.visualization.hyperstack; - -import java.awt.MouseInfo; -import java.awt.Point; -import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.Locale; -import java.util.Set; - -import javax.swing.SwingUtilities; - -import org.jgrapht.graph.DefaultWeightedEdge; - -import fiji.plugin.trackmate.Logger; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.SpotRoi; -import fiji.plugin.trackmate.detection.semiauto.SemiAutoTracker; -import fiji.plugin.trackmate.util.ModelTools; -import fiji.plugin.trackmate.util.TMUtils; -import fiji.plugin.trackmate.util.Threads; -import fiji.plugin.trackmate.util.TrackNavigator; -import ij.IJ; -import ij.ImagePlus; -import ij.Prefs; -import ij.gui.FreehandRoi; -import ij.gui.ImageCanvas; - -public class ModelEditActions -{ - - /** - * Fall back default radius when the settings does not give a default radius - * to use. - */ - static final double FALL_BACK_RADIUS = 5.; - - private static final double COARSE_STEP = 2; - - private static final double FINE_STEP = 0.2f; - - private final Model model; - - private final SelectionModel selectionModel; - - private final Logger logger; - - private final ImagePlus imp; - - private Spot quickEditedSpot; - - private double previousRadius = FALL_BACK_RADIUS; - - private FreehandRoi roiedit; - - private final TrackNavigator trackNavigator; - - public ModelEditActions( final ImagePlus imp, final Model model, final SelectionModel selectionModel, final Logger logger ) - { - this.imp = imp; - this.model = model; - this.selectionModel = selectionModel; - this.logger = logger; - this.trackNavigator = new TrackNavigator( model, selectionModel ); - } - - private Spot makeSpot( Point mouseLocation ) - { - final ImageCanvas canvas = imp.getCanvas(); - if ( mouseLocation == null ) - { - mouseLocation = MouseInfo.getPointerInfo().getLocation(); - SwingUtilities.convertPointFromScreen( mouseLocation, canvas ); - } - final double[] calibration = TMUtils.getSpatialCalibration( imp ); - return new Spot( - ( -0.5 + canvas.offScreenXD( mouseLocation.x ) ) * calibration[ 0 ], - ( -0.5 + canvas.offScreenYD( mouseLocation.y ) ) * calibration[ 1 ], - ( imp.getSlice() - 1 ) * calibration[ 2 ], - FALL_BACK_RADIUS, - -1. ); - } - - private Spot getSpotAtMouseLocation() - { - final Spot clickLocation = makeSpot( null ); - final int frame = imp.getFrame() - 1; - return model.getSpots().getSpotAt( clickLocation, frame, true ); - } - - private void updateStatusBar( final Spot spot, final String units ) - { - if ( null == spot ) - return; - String statusString = ""; - if ( null == spot.getName() || spot.getName().equals( "" ) ) - statusString = String.format( Locale.US, "Spot ID%d, x = %.1f, y = %.1f, z = %.1f, r = %.1f %s", spot.ID(), spot.getFeature( Spot.POSITION_X ), spot.getFeature( Spot.POSITION_Y ), spot.getFeature( Spot.POSITION_Z ), spot.getFeature( Spot.RADIUS ), units ); - else - statusString = String.format( Locale.US, "Spot %s, x = %.1f, y = %.1f, z = %.1f, r = %.1f %s", spot.getName(), spot.getFeature( Spot.POSITION_X ), spot.getFeature( Spot.POSITION_Y ), spot.getFeature( Spot.POSITION_Z ), spot.getFeature( Spot.RADIUS ), units ); - IJ.showStatus( statusString ); - } - - public final void deleteSpotSelection() - { - final ArrayList< Spot > spotSelection = new ArrayList<>( selectionModel.getSpotSelection() ); - final ArrayList< DefaultWeightedEdge > edgeSelection = new ArrayList<>( selectionModel.getEdgeSelection() ); - model.beginUpdate(); - try - { - selectionModel.clearSelection(); - for ( final DefaultWeightedEdge edge : edgeSelection ) - { - model.removeEdge( edge ); - logger.log( "Removed edge " + edge + ".\n" ); - } - for ( final Spot spot : spotSelection ) - { - model.removeSpot( spot ); - logger.log( "Removed spot " + spot + ".\n" ); - } - } - finally - { - model.endUpdate(); - } - } - - public void semiAutoTracking( final double qualityThreshold, final double distanceTolerance, final int nFrames ) - { - logger.log( "Semi-automatic tracking.\n" ); - @SuppressWarnings( "rawtypes" ) - final SemiAutoTracker autotracker = new SemiAutoTracker( model, selectionModel, imp, logger ); - autotracker.setParameters( qualityThreshold, distanceTolerance, nFrames ); - autotracker.setNumThreads( Prefs.getThreads() / 2 ); - Threads.run( "TrackMate semi-automated tracking thread", () -> { - final boolean ok = autotracker.checkInput() && autotracker.process(); - if ( !ok ) - logger.error( autotracker.getErrorMessage() ); - } ); - } - - public void addSpot( final boolean autoLinkingmode ) - { - final double radius = previousRadius; - final Spot newSpot = makeSpot( null ); - final double dt = imp.getCalibration().frameInterval; - final int frame = imp.getFrame() - 1; - newSpot.putFeature( Spot.POSITION_T, frame * dt ); - newSpot.putFeature( Spot.FRAME, Double.valueOf( frame ) ); - newSpot.putFeature( Spot.RADIUS, radius ); - newSpot.putFeature( Spot.QUALITY, -1d ); - - model.beginUpdate(); - try - { - model.addSpotTo( newSpot, frame ); - logger.log( "Added spot " + newSpot + " to frame " + frame + ".\n" ); - } - finally - { - model.endUpdate(); - } - - /* - * If we are in auto-link mode, we create an edge with spot in - * selection, if there is just one and if it is in a previous frame - */ - if ( autoLinkingmode ) - { - final Set< Spot > spotSelection = selectionModel.getSpotSelection(); - if ( spotSelection.size() == 1 ) - { - final Spot source = spotSelection.iterator().next(); - if ( newSpot.diffTo( source, Spot.FRAME ) != 0 ) - { - model.beginUpdate(); - try - { - model.addEdge( source, newSpot, -1 ); - logger.log( "Created a link between " + source + " and " + newSpot + ".\n" ); - } - finally - { - model.endUpdate(); - } - } - } - selectionModel.clearSpotSelection(); - selectionModel.addSpotToSelection( newSpot ); - } - } - - public void deleteSpot() - { - final Spot target = getSpotAtMouseLocation(); - if ( null == target ) - return; - - selectionModel.removeSpotFromSelection( target ); - model.beginUpdate(); - try - { - model.removeSpot( target ); - logger.log( "Removed spot " + target + ".\n" ); - } - finally - { - model.endUpdate(); - } - } - - public void startMoveSpot() - { - if ( null == quickEditedSpot ) - quickEditedSpot = getSpotAtMouseLocation(); - } - - public void moveSpot( final Point mouseLocation ) - { - if ( quickEditedSpot == null ) - return; - - final ImageCanvas canvas = imp.getCanvas(); - final double[] calibration = TMUtils.getSpatialCalibration( imp ); - final double x = ( -0.5 + canvas.offScreenXD( mouseLocation.x ) ) * calibration[ 0 ]; - final double y = ( -0.5 + canvas.offScreenYD( mouseLocation.y ) ) * calibration[ 1 ]; - final double z = ( imp.getSlice() - 1 ) * calibration[ 2 ]; - - quickEditedSpot.putFeature( Spot.POSITION_X, x ); - quickEditedSpot.putFeature( Spot.POSITION_Y, y ); - quickEditedSpot.putFeature( Spot.POSITION_Z, z ); - imp.updateAndDraw(); - } - - public void endMoveSpot() - { - if ( null == quickEditedSpot ) - return; - model.beginUpdate(); - try - { - model.updateFeatures( quickEditedSpot ); - } - finally - { - model.endUpdate(); - } - quickEditedSpot = null; - } - - public void changeSpotRadius( final boolean increase, final boolean fast ) - { - final Spot target = getSpotAtMouseLocation(); - if ( null == target ) - return; - - final double radius = target.getFeature( Spot.RADIUS ); - final int factor = ( increase ) ? 1 : -1; - final double dx = imp.getCalibration().pixelWidth; - - final double newRadius = ( fast ) - ? radius + factor * dx * COARSE_STEP - : radius + factor * dx * FINE_STEP; - - if ( newRadius <= dx ) - return; - - // Store new value of radius for next spot creation. - previousRadius = newRadius; - - final SpotRoi roi = target.getRoi(); - if ( null == roi ) - { - target.putFeature( Spot.RADIUS, newRadius ); - } - else - { - final double alpha = newRadius / radius; - roi.scale( alpha ); - target.putFeature( Spot.RADIUS, roi.radius() ); - } - - model.beginUpdate(); - try - { - model.updateFeatures( target ); - logger.log( String.format( Locale.US, "Changed spot " + target + " radius to %.1f " + model.getSpaceUnits() + ".\n", radius ) ); - } - finally - { - model.endUpdate(); - } - } - - public void toggleLink() - { - final Set< Spot > selectedSpots = selectionModel.getSpotSelection(); - if ( selectedSpots.size() == 2 ) - { - final Iterator< Spot > it = selectedSpots.iterator(); - final Spot sourceTmp = it.next(); - final Spot targetTmp = it.next(); - - final Spot source = sourceTmp.diffTo( targetTmp, Spot.FRAME ) < 0 ? sourceTmp : targetTmp; - final Spot target = sourceTmp.diffTo( targetTmp, Spot.FRAME ) < 0 ? targetTmp : sourceTmp; - - if ( model.getTrackModel().containsEdge( source, target ) ) - { - /* - * Remove it - */ - model.beginUpdate(); - try - { - model.removeEdge( source, target ); - logger.log( "Removed edge between " + source + " and " + target + ".\n" ); - } - finally - { - model.endUpdate(); - } - - } - else - { - /* - * Create a new link - */ - final int ts = source.getFeature( Spot.FRAME ).intValue(); - final int tt = target.getFeature( Spot.FRAME ).intValue(); - - if ( tt != ts ) - { - model.beginUpdate(); - try - { - model.addEdge( source, target, -1 ); - logger.log( "Created an edge between " + source + " and " + target + ".\n" ); - } - finally - { - model.endUpdate(); - } - /* - * To emulate a kind of automatic linking, we put the last - * spot to the selection, so several spots can be tracked in - * a row without having to de-select one - */ - final Spot single = ( tt > ts ) ? target : source; - selectionModel.clearSpotSelection(); - selectionModel.addSpotToSelection( single ); - } - else - { - logger.error( "Cannot create an edge between two spots belonging to the same frame.\n" ); - } - } - - } - else - { - logger.error( "Expected selection to contain 2 spots, found " + selectedSpots.size() + ".\n" ); - } - } - - public void stepInTime( final boolean forward, final int stepwiseTimeBrowsing ) - { - // Stepwise time browsing. - final int currentT = imp.getT() - 1; - final int prevStep = ( currentT / stepwiseTimeBrowsing ) * stepwiseTimeBrowsing; - int tp; - if ( forward ) - { - tp = prevStep + stepwiseTimeBrowsing; - } - else - { - if ( currentT == prevStep ) - tp = currentT - stepwiseTimeBrowsing; - else - tp = prevStep; - } - imp.setT( tp + 1 ); - } - - public void select( final Point point, final boolean addToSelection, final boolean canClearSelection ) - { - // If no target, we clear selection - final Spot target = getSpotAtMouseLocation(); - if ( null == target ) - { - if ( canClearSelection ) - { - selectionModel.clearSelection(); - logger.log( "Cleared selection.\n" ); - } - roiedit = null; - imp.setRoi( roiedit ); - } - else - { - updateStatusBar( target, imp.getCalibration().getUnits() ); - if ( addToSelection ) - { - if ( selectionModel.getSpotSelection().contains( target ) ) - selectionModel.removeSpotFromSelection( target ); - else - selectionModel.addSpotToSelection( target ); - } - else - { - selectionModel.clearSpotSelection(); - selectionModel.addSpotToSelection( target ); - } - } - } - - public void roiEdit( final MouseEvent e ) - { - if ( null == roiedit ) - { - if ( !IJ.spaceBarDown() ) - { - roiedit = new FreehandRoi( e.getX(), e.getY(), imp ) - { - private static final long serialVersionUID = 1L; - - @Override - protected void handleMouseUp( final int screenX, final int screenY ) - { - type = FREEROI; - super.handleMouseUp( screenX, screenY ); - } - }; - imp.setRoi( roiedit ); - } - } - else - { - roiedit.mouseDragged( e ); - } - } - - public void selectInRoi( final MouseEvent e ) - { - if ( null != roiedit ) - { - Threads.run( "SpotEditTool roiedit processing", () -> { - roiedit.mouseReleased( e ); - final int frame = imp.getFrame() - 1; - - final Iterator< Spot > it; - if ( IJ.shiftKeyDown() ) - it = model.getSpots().iterator( true ); - else - it = model.getSpots().iterator( frame, true ); - - final Collection< Spot > added = new ArrayList<>(); - final double calibration[] = TMUtils.getSpatialCalibration( imp ); - - while ( it.hasNext() ) - { - final Spot spot = it.next(); - final double x = spot.getFeature( Spot.POSITION_X ); - final double y = spot.getFeature( Spot.POSITION_Y ); - // In pixel units - final int xp = ( int ) ( x / calibration[ 0 ] + 0.5f ); - final int yp = ( int ) ( y / calibration[ 1 ] + 0.5f ); - - if ( null != roiedit && roiedit.contains( xp, yp ) ) - added.add( spot ); - } - - if ( !added.isEmpty() ) - { - selectionModel.addSpotToSelection( added ); - if ( added.size() == 1 ) - logger.log( "Added one spot to selection.\n" ); - else - logger.log( "Added " + added.size() + " spots to selection.\n" ); - } - roiedit = null; - } ); - } - } - - public void selectTrackDownward() - { - ModelTools.selectTrackDownward( selectionModel ); - } - - public void selectTrackUpward() - { - ModelTools.selectTrackUpward( selectionModel ); - } - - public void selectTrack() - { - ModelTools.selectTrack( selectionModel ); - } - - public void navigateToChild() - { - trackNavigator.nextInTime(); - } - - public void navigateToParent() - { - trackNavigator.previousInTime(); - } - - public void navigateToNextSibling() - { - trackNavigator.nextSibling(); - } - - public void navigateToPreviousSibling() - { - trackNavigator.previousSibling(); - } - - public void navigateToNextTrack() - { - trackNavigator.nextTrack(); - } - - public void navigateToPreviousTrack() - { - trackNavigator.previousTrack(); - } -} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotMesh.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotMesh.java new file mode 100644 index 000000000..37d6d446d --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotMesh.java @@ -0,0 +1,159 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.visualization.hyperstack; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.Area; +import java.awt.geom.Path2D; +import java.util.function.DoubleUnaryOperator; + +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import ij.ImagePlus; +import net.imglib2.RealInterval; +import net.imglib2.RealLocalizable; +import net.imglib2.mesh.alg.zslicer.Contour; +import net.imglib2.mesh.alg.zslicer.Slice; + +/** + * Utility class to paint the {@link SpotMesh} component of spots. + * + * @author Jean-Yves Tinevez + * + */ +public class PaintSpotMesh extends TrackMatePainter< SpotMesh > +{ + + private final Path2D.Double polygon; + + private final Area shape; + + public PaintSpotMesh( final ImagePlus imp, final double[] calibration, final DisplaySettings displaySettings ) + { + super( imp, calibration, displaySettings ); + this.polygon = new Path2D.Double(); + this.shape = new Area(); + + } + + @Override + public int paint( final Graphics2D g2d, final SpotMesh spot ) + { + final RealInterval bb = spot.getBoundingBox(); + if ( !intersect( bb, spot ) ) + return -1; + + // Z plane does not cross bounding box. + final double x = spot.getFeature( Spot.POSITION_X ); + final double y = spot.getFeature( Spot.POSITION_Y ); + final double xs = toScreenX( x ); + final double ys = toScreenY( y ); + final double z = spot.getFeature( Spot.POSITION_Z ); + final int zSlice = imp.getSlice() - 1; + final double dz = zSlice * calibration[ 2 ]; + if ( bb.realMin( 2 ) + z > dz || bb.realMax( 2 ) + z < dz ) + { + paintOutOfFocus( g2d, xs, ys ); + return -1; + } + + // Convert to AWT shape. Only work in non-pathological cases, and + // because contours are sorted by decreasing area. + final Slice slice = spot.getZSlice( zSlice, calibration[ 0 ], calibration[ 2 ] ); + if ( slice == null ) + { + paintOutOfFocus( g2d, xs, ys ); + return -1; + } + + + if ( displaySettings.isSpotFilled() ) + { + // Should not be null. + shape.reset(); + for ( final Contour c : slice ) + { + toPolygon( spot, c, polygon, this::toScreenX, this::toScreenY ); + if ( c.isInterior() ) + shape.add( new Area( polygon ) ); + else + shape.subtract( new Area( polygon ) ); + } + g2d.fill( shape ); + g2d.setColor( Color.BLACK ); + g2d.draw( shape ); + } + else + { + for ( final Contour c : slice ) + { + toPolygon( spot, c, polygon, this::toScreenX, this::toScreenY ); + g2d.draw( polygon ); + } + } + final Rectangle bounds = shape.getBounds(); + final int maxTextPos = bounds.x + bounds.width; + return ( int ) ( maxTextPos - xs ); + } + + /** + * Maps the coordinates of this contour to a Path2D polygon, and return the + * max X coordinate of the produced shape. + * + * @param contour + * the contour to convert. + * @param polygon + * the polygon to write. Reset by this call. + * @param toScreenX + * a function to convert the X coordinate of this contour to + * screen coordinates. + * @param toScreenY + * a function to convert the Y coordinate of this contour to + * screen coordinates. + * @return the max X position in screen units of this shape. + */ + private static final double toPolygon( final RealLocalizable center, final Contour contour, final Path2D polygon, final DoubleUnaryOperator toScreenX, final DoubleUnaryOperator toScreenY ) + { + double maxTextPos = Double.NEGATIVE_INFINITY; + polygon.reset(); + final double x0 = toScreenX.applyAsDouble( contour.x( 0 ) + center.getDoublePosition( 0 ) ); + final double y0 = toScreenY.applyAsDouble( contour.y( 0 ) + center.getDoublePosition( 1 ) ); + polygon.moveTo( x0, y0 ); + if ( x0 > maxTextPos ) + maxTextPos = x0; + + for ( int i = 1; i < contour.size(); i++ ) + { + final double xi = toScreenX.applyAsDouble( contour.x( i ) + center.getDoublePosition( 0 ) ); + final double yi = toScreenY.applyAsDouble( contour.y( i ) + center.getDoublePosition( 1 ) ); + polygon.lineTo( xi, yi ); + + if ( xi > maxTextPos ) + maxTextPos = xi; + } + polygon.closePath(); + return maxTextPos; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotRoi.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotRoi.java new file mode 100644 index 000000000..263540812 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotRoi.java @@ -0,0 +1,135 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.visualization.hyperstack; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.geom.Path2D; +import java.util.function.DoubleUnaryOperator; + +import fiji.plugin.trackmate.SpotRoi; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import gnu.trove.list.TDoubleList; +import ij.ImagePlus; + +/** + * Utility class to paint the {@link SpotRoi} component of spots. + * + * @author Jean-Yves Tinevez + * + */ +public class PaintSpotRoi extends TrackMatePainter< SpotRoi > +{ + + private final java.awt.geom.Path2D.Double polygon; + + public PaintSpotRoi( final ImagePlus imp, final double[] calibration, final DisplaySettings displaySettings ) + { + super( imp, calibration, displaySettings ); + this.polygon = new Path2D.Double(); + } + + /** + * Paint the specified spot using its {@link SpotRoi} field. The latter must + * not be null. + * + * @param g2d + * the graphics object, configured to paint the spot with. + * @param spot + * the spot to paint. + * @return the text position X indent in pixels to use to paint a string + * next to the painted contour. + */ + @Override + public int paint( final Graphics2D g2d, final SpotRoi spot ) + { + if ( !intersect( spot ) ) + return -1; + + final double maxTextPos = toPolygon( spot, polygon, this::toScreenX, this::toScreenY ); + if ( displaySettings.isSpotFilled() ) + { + g2d.fill( polygon ); + g2d.setColor( Color.BLACK ); + g2d.draw( polygon ); + } + else + { + g2d.draw( polygon ); + } + + final double xs = toScreenX( spot.getDoublePosition( 0 ) ); + final int textPos = ( int ) ( maxTextPos - xs ); + return textPos; + } + + static final double max( final TDoubleList l ) + { + double max = Double.NEGATIVE_INFINITY; + for ( int i = 0; i < l.size(); i++ ) + { + final double v = l.get( i ); + if ( v > max ) + max = v; + } + return max; + } + + /** + * Maps the coordinates of this contour to a Path2D polygon, and return the + * max X coordinate of the produced shape. + * + * @param contour + * the contour to convert. + * @param polygon + * the polygon to write. Reset by this call. + * @param toScreenX + * a function to convert the X coordinate of this contour to + * screen coordinates. + * @param toScreenY + * a function to convert the Y coordinate of this contour to + * screen coordinates. + * @return the max X position in screen units of this shape. + */ + private static final double toPolygon( final SpotRoi roi, final Path2D polygon, final DoubleUnaryOperator toScreenX, final DoubleUnaryOperator toScreenY ) + { + double maxTextPos = Double.NEGATIVE_INFINITY; + polygon.reset(); + final double x0 = toScreenX.applyAsDouble( roi.x( 0 ) ); + final double y0 = toScreenY.applyAsDouble( roi.y( 0 ) ); + polygon.moveTo( x0, y0 ); + if ( x0 > maxTextPos ) + maxTextPos = x0; + + for ( int i = 1; i < roi.nPoints(); i++ ) + { + final double xi = toScreenX.applyAsDouble( roi.x( i ) ); + final double yi = toScreenY.applyAsDouble( roi.y( i ) ); + polygon.lineTo( xi, yi ); + + if ( xi > maxTextPos ) + maxTextPos = xi; + } + polygon.closePath(); + return maxTextPos; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotSphere.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotSphere.java new file mode 100644 index 000000000..cc62851c0 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotSphere.java @@ -0,0 +1,95 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.visualization.hyperstack; + +import java.awt.Graphics2D; + +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import ij.ImagePlus; +import net.imglib2.RealInterval; +import net.imglib2.util.Intervals; + +/** + * Utility class to paint the spots as little spheres. + * + * @author Jean-Yves Tinevez + * + */ +public class PaintSpotSphere extends TrackMatePainter< SpotBase > +{ + + public PaintSpotSphere( final ImagePlus imp, final double[] calibration, final DisplaySettings displaySettings ) + { + super( imp, calibration, displaySettings ); + } + + @Override + public int paint( final Graphics2D g2d, final SpotBase spot ) + { + if ( !intersect( boundingBox( spot ), spot ) ) + return -1; + + final double x = spot.getFeature( Spot.POSITION_X ); + final double y = spot.getFeature( Spot.POSITION_Y ); + final double z = spot.getFeature( Spot.POSITION_Z ); + final double zslice = ( imp.getSlice() - 1 ) * calibration[ 2 ]; + final double dz = zslice - z; + final double dz2 = dz * dz; + final double radiusRatio = displaySettings.getSpotDisplayRadius(); + final double radius = spot.getFeature( Spot.RADIUS ) * radiusRatio; + + final double xs = toScreenX( x ); + final double ys = toScreenY( y ); + final double magnification = getMagnification(); + + if ( dz2 >= radius * radius ) + { + paintOutOfFocus( g2d, xs, ys ); + return -1; // Do not paint spot name. + } + + final double apparentRadius = Math.sqrt( radius * radius - dz2 ) / calibration[ 0 ] * magnification; + if ( displaySettings.isSpotFilled() ) + g2d.fillOval( + ( int ) Math.round( xs - apparentRadius ), + ( int ) Math.round( ys - apparentRadius ), + ( int ) Math.round( 2 * apparentRadius ), + ( int ) Math.round( 2 * apparentRadius ) ); + else + g2d.drawOval( + ( int ) Math.round( xs - apparentRadius ), + ( int ) Math.round( ys - apparentRadius ), + ( int ) Math.round( 2 * apparentRadius ), + ( int ) Math.round( 2 * apparentRadius ) ); + + final int textPos = ( int ) apparentRadius; + return textPos; + } + + private static final RealInterval boundingBox( final Spot spot ) + { + final double r = spot.getFeature( Spot.RADIUS ).doubleValue(); + return Intervals.createMinMaxReal( -r, -r, r, r ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditTool.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditTool.java deleted file mode 100644 index 8258d7749..000000000 --- a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditTool.java +++ /dev/null @@ -1,556 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.visualization.hyperstack; - -import java.awt.Color; -import java.awt.event.InputEvent; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.MouseMotionListener; -import java.util.HashMap; -import java.util.Map; - -import fiji.plugin.trackmate.Logger; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; -import fiji.tool.AbstractTool; -import fiji.tool.ToolWithOptions; -import ij.ImageListener; -import ij.ImagePlus; -import ij.WindowManager; -import ij.gui.ImageCanvas; -import ij.gui.Toolbar; - -public class SpotEditTool extends AbstractTool implements MouseMotionListener, MouseListener, KeyListener, ToolWithOptions -{ - - private static final String TOOL_NAME = "Spot edit tool"; - - private static final String TOOL_ICON = "CeacD70Cd8bD80" - + "D71Cc69D81CfefD91" - + "CdbcD72Cb9bD82" - + "Cd9bD73Cc8aD83CfefD93" - + "CdddD54CbaaD64Cb69D74Cb59D84Cb9aD94CdddDa4" - + "CfefD25Cd9bD35Cb8aD45CaaaD55CcccD65CfdeL7585CdccD95CaaaDa5Cb8aDb5Cd7aDc5CfceDd5" - + "CfeeD26Cc69D36Cc8aD46CdacDb6Cb59Dc6CecdDd6" - + "Cb9aD37CdcdD47CeeeDb7Ca89Dc7" - + "CfefD28Cc7aD38Cd9cD48CecdDb8Cb79Dc8CfdeDd8" - + "CcabD29Cb59D39Cb69D49CedeD59CeacDb9Cc59Dc9CebdDd9" - + "CfdeD0aCc7aD1aCb8aD2aCedeD3aCcbcD4aCb7aD5aCe9cD6aCeeeDbaCa89DcaCfefDda" - + "CebdD0bCc59D1bCebdD2bCfefD4bCc7aL5b6bCeceDbbCb79DcbCfdeDdb" - + "CfeeD0cCa89D1cCfefD2cCcabL5c6cCc9bDbcCc59DccCdabDdc" - + "CedeD0dCb79D1dCedeD2dCc9bL5d6dCecdD9dCc8aDadCb9aDbdCdbcDcdCb8aDddCd8bDedCfceDfd" - + "CebdD0eCc59D1eCebdD2eCfeeD4eCc7aD5eCc6aD6eCfeeD7eCd9bD9eCc59DaeCfdeDbeCebdDdeCc59DeeCeacDfe" - + "CfefD0fCdbcD1fCdddD4fCdcdL5f6fCdddD7fCfdeD9fCdbdDafCebdDefCfefDff"; - - /** The singleton instance. */ - private static SpotEditTool instance; - - /** Stores the editor possibly attached to each {@link ImagePlus}. */ - private final Map< ImagePlus, ModelEditActions > editorMap = new HashMap<>(); - - /** Flag for the auto-linking mode. */ - private boolean autolinkingmode = false; - - private final SpotEditToolParams params = new SpotEditToolParams(); - - private final Logger logger = new MyLogger(); - - private final SpotEditToolConfigPanel configPanel; - - /* - * CONSTRUCTOR - */ - - /** - * Singleton - */ - private SpotEditTool() - { - // Config panel actions. - this.configPanel = new SpotEditToolConfigPanel( params ); - configPanel.buttonSelectTrackDown.addActionListener( e -> selectTrackDownward() ); - configPanel.buttonSelectTrackUp.addActionListener( e -> selectTrackUpward() ); - configPanel.buttonSemiAutoTracking.addActionListener( e -> semiAutoTracking() ); - configPanel.buttonSelectTrack.addActionListener( e -> selectTrack() ); - - // De-register on closing. - ImagePlus.addImageListener( new ImageListener() - { - - @Override - public void imageUpdated( final ImagePlus imp ) - {} - - @Override - public void imageOpened( final ImagePlus imp ) - {} - - @Override - public void imageClosed( final ImagePlus imp ) - { - editorMap.remove( imp ); - } - } ); - } - - /** - * Returns the singleton instance for this tool. If it was not previously - * instantiated, this calls instantiates it. - * - * @return the singleton instance of this tool. - */ - public static SpotEditTool getInstance() - { - if ( null == instance ) - instance = new SpotEditTool(); - - return instance; - } - - /** - * Returns true if the tool is currently present in ImageJ toolbar. - * - * @return true if the tool is launched. - */ - public static boolean isLaunched() - { - final Toolbar toolbar = Toolbar.getInstance(); - if ( null != toolbar && toolbar.getToolId( TOOL_NAME ) >= 0 ) - return true; - return false; - } - - /* - * METHODS - */ - - @Override - public String getToolName() - { - return TOOL_NAME; - } - - @Override - public String getToolIcon() - { - return TOOL_ICON; - } - - @Override - protected void registerTool( final ImageCanvas canvas ) - { - /* - * Double check! Since TrackMate v7 there the following bug: - * - * Sometimes the listeners of this tool get added to the target image - * canvas TWICE. This causes an unspeakable mess where all events are - * triggered twice for e.g. a single click. For instance you cannot - * shift-click on a spot to add it to the selection, because the event - * is fired TWICE, which results in the spot being de-selected - * immediately after being selected. - * - * But the double registration seems to happen randomly. Sometimes the - * listeners are added only once, *sometimes* (more often) twice. - * - * To work around this mess, we overload the registerTool(ImageCanvas) - * method and skip the registration if we find that the mouse listener - * has already been added to the canvas. It fixes the issue, regardless - * of the occurrence of the double call to this method or not. - */ - - final MouseListener[] listeners = canvas.getMouseListeners(); - for ( final MouseListener listener : listeners ) - { - if ( listener == this.mouseProxy ) - return; - } - - super.registerTool( canvas ); - } - - /** - * Registers the given {@link HyperStackDisplayer}. If this method id not - * called, the tool will not respond. - * - * @param displayer - * the displayer to register - */ - public void register( final HyperStackDisplayer displayer ) - { - final ImagePlus imp = displayer.getImp(); - final Model model = displayer.getModel(); - final SelectionModel selectionModel = displayer.getSelectionModel(); - final ModelEditActions actions = new ModelEditActions( imp, model, selectionModel, logger ); - editorMap.put( imp, actions ); - } - - /* - * MOUSE AND MOUSE MOTION - */ - - @Override - public void mouseClicked( final MouseEvent e ) - { - final ImagePlus lImp = getImagePlus( e ); - final ModelEditActions actions = editorMap.get( lImp ); - if ( null == actions ) - return; - - final int addToSelectionMask = InputEvent.SHIFT_DOWN_MASK; - final boolean addToSelection = ( e.getModifiersEx() & addToSelectionMask ) == addToSelectionMask; - actions.select( e.getPoint(), addToSelection, !autolinkingmode ); - } - - @Override - public void mousePressed( final MouseEvent e ) - {} - - @Override - public void mouseReleased( final MouseEvent e ) - { - final ImagePlus imp = getImagePlus( e ); - final ModelEditActions actions = editorMap.get( imp ); - if ( actions == null ) - return; - - actions.selectInRoi( e ); - } - - @Override - public void mouseEntered( final MouseEvent e ) - {} - - @Override - public void mouseExited( final MouseEvent e ) - {} - - @Override - public void mouseDragged( final MouseEvent e ) - { - final ImagePlus imp = getImagePlus( e ); - final ModelEditActions actions = editorMap.get( imp ); - if ( null == actions ) - return; - - actions.roiEdit( e ); - } - - @Override - public void mouseMoved( final MouseEvent e ) - { - final ImagePlus imp = getImagePlus( e ); - final ModelEditActions actions = editorMap.get( imp ); - if ( actions == null ) - return; - - actions.moveSpot( e.getPoint() ); - } - - /* - * KEYLISTENER - */ - - @Override - public void keyTyped( final KeyEvent e ) - {} - - @Override - public void keyPressed( final KeyEvent e ) - { - final ImagePlus imp = getImagePlus( e ); - final ModelEditActions actions = editorMap.get( imp ); - if ( null == actions ) - return; - - switch ( e.getKeyCode() ) - { - - // Track navigation actions. - case KeyEvent.VK_UP: - { - actions.navigateToParent(); - e.consume(); - break; - } - case KeyEvent.VK_DOWN: - { - actions.navigateToChild(); - e.consume(); - break; - } - case KeyEvent.VK_LEFT: - { - actions.navigateToPreviousSibling(); - e.consume(); - break; - } - case KeyEvent.VK_RIGHT: - { - actions.navigateToNextSibling(); - e.consume(); - break; - } - case KeyEvent.VK_PAGE_DOWN: - { - actions.navigateToNextTrack(); - e.consume(); - break; - } - case KeyEvent.VK_PAGE_UP: - { - actions.navigateToPreviousTrack(); - e.consume(); - break; - } - - // Delete currently edited spot - case KeyEvent.VK_DELETE: - { - actions.deleteSpotSelection(); - e.consume(); - break; - } - - // Quick add spot at mouse - case KeyEvent.VK_A: - { - if ( e.isShiftDown() ) - { - // Semi-auto tracking - actions.semiAutoTracking( params.qualityThreshold, params.distanceTolerance, params.nFrames ); - } - else - { - // Create and drop a new spot - actions.addSpot( autolinkingmode ); - } - e.consume(); - break; - } - - // Quick delete spot under mouse - case KeyEvent.VK_D: - { - actions.deleteSpot(); - e.consume(); - break; - } - - // Quick move spot under the mouse - case KeyEvent.VK_SPACE: - { - actions.startMoveSpot(); - break; - - } - - // Quick change spot radius - case KeyEvent.VK_Q: - case KeyEvent.VK_E: - { - e.consume(); - actions.changeSpotRadius( e.getKeyCode() == KeyEvent.VK_E, e.isShiftDown() ); - break; - } - - case KeyEvent.VK_L: - { - - if ( e.isShiftDown() ) - { - // Toggle auto-linking mode - autolinkingmode = !autolinkingmode; - logger.log( "Toggled auto-linking mode " + ( autolinkingmode ? "on.\n" : "off.\n" ) ); - - } - else - { - // Toggle a link between two spots. - actions.toggleLink(); - } - e.consume(); - break; - - } - - case KeyEvent.VK_G: - case KeyEvent.VK_F: - { - actions.stepInTime( e.getKeyCode() == KeyEvent.VK_G, params.stepwiseTimeBrowsing ); - e.consume(); - break; - } - - case KeyEvent.VK_W: - { - e.consume(); // consume it: we do not want IJ to close the window - break; - } - } - - } - - @Override - public void keyReleased( final KeyEvent e ) - { - switch ( e.getKeyCode() ) - { - case KeyEvent.VK_SPACE: - { - final ImagePlus imp = getImagePlus( e ); - final ModelEditActions actions = editorMap.get( imp ); - if ( actions != null ) - actions.endMoveSpot(); - break; - } - } - } - - @Override - public void showOptionDialog() - { - configPanel.setLocation( toolbar.getLocationOnScreen() ); - configPanel.setVisible( true ); - } - - /* - * PRIVATE METHODS - */ - - private void selectTrack() - { - final ImagePlus imp = WindowManager.getCurrentImage(); - final ModelEditActions actions = editorMap.get( imp ); - if ( null == actions ) - return; - - actions.selectTrack(); - } - - private void semiAutoTracking() - { - final ImagePlus imp = WindowManager.getCurrentImage(); - final ModelEditActions actions = editorMap.get( imp ); - if ( null == actions ) - return; - - actions.semiAutoTracking( params.qualityThreshold, params.distanceTolerance, params.nFrames ); - } - - private void selectTrackDownward() - { - final ImagePlus imp = WindowManager.getCurrentImage(); - final ModelEditActions actions = editorMap.get( imp ); - if ( null == actions ) - return; - - actions.selectTrackDownward(); - } - - private void selectTrackUpward() - { - - final ImagePlus imp = WindowManager.getCurrentImage(); - final ModelEditActions actions = editorMap.get( imp ); - if ( null == actions ) - return; - - actions.selectTrackUpward(); - } - - /* - * INNER CLASSES - */ - - static class SpotEditToolParams - { - - /* - * Semi-auto tracking parameters - */ - /** - * The fraction of the initial quality above which we keep new spots. - * The highest, the more intolerant. - */ - double qualityThreshold = 0.5; - - /** - * How close must be the new spot found to be accepted, in radius units. - */ - double distanceTolerance = 2d; - - /** - * We process at most nFrames. Make it 0 or negative to have no bounds. - */ - int nFrames = 10; - - /** - * By how many frames to jump when we do step-wide time browsing. - */ - int stepwiseTimeBrowsing = 1; - - @Override - public String toString() - { - return super.toString() + ": " + "QualityThreshold = " + qualityThreshold + ", DistanceTolerance = " + distanceTolerance + ", nFrames = " + nFrames; - } - } - - private class MyLogger extends Logger - { - - private Logger logger() - { - if ( configPanel.isVisible() ) - return configPanel.getLogger(); - - return Logger.IJTOOLBAR_LOGGER; - } - - @Override - public void log( final String message, final Color color ) - { - logger().log( message, color ); - } - - @Override - public void error( final String message ) - { - logger().error( message ); - } - - @Override - public void setProgress( final double val ) - { - logger().setProgress( val ); - } - - @Override - public void setStatus( final String status ) - { - logger().setStatus( status ); - } - } -} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditToolConfigPanel.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditToolConfigPanel.java deleted file mode 100644 index 21e3a66bf..000000000 --- a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditToolConfigPanel.java +++ /dev/null @@ -1,347 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.visualization.hyperstack; - -import static fiji.plugin.trackmate.gui.Fonts.BIG_FONT; -import static fiji.plugin.trackmate.gui.Fonts.FONT; -import static fiji.plugin.trackmate.gui.Fonts.SMALL_FONT; -import static fiji.plugin.trackmate.gui.Icons.SELECT_TRACK_ICON; -import static fiji.plugin.trackmate.gui.Icons.SELECT_TRACK_ICON_DOWNWARDS; -import static fiji.plugin.trackmate.gui.Icons.SELECT_TRACK_ICON_UPWARDS; -import static fiji.plugin.trackmate.gui.Icons.SPOT_ICON_64x64; -import static fiji.plugin.trackmate.gui.Icons.TRACK_ICON; -import static fiji.plugin.trackmate.gui.Icons.TRACK_ICON_64x64; - -import java.awt.Color; -import java.awt.Font; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; - -import javax.swing.BoxLayout; -import javax.swing.JButton; -import javax.swing.JFormattedTextField; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextPane; -import javax.swing.ScrollPaneConstants; -import javax.swing.SwingConstants; -import javax.swing.SwingUtilities; -import javax.swing.WindowConstants; -import javax.swing.border.LineBorder; -import javax.swing.text.AttributeSet; -import javax.swing.text.SimpleAttributeSet; -import javax.swing.text.StyleConstants; -import javax.swing.text.StyleContext; - -import fiji.plugin.trackmate.Logger; -import fiji.plugin.trackmate.visualization.hyperstack.SpotEditTool.SpotEditToolParams; - -public class SpotEditToolConfigPanel extends JFrame -{ - private static final long serialVersionUID = 1L; - - private final Logger logger; - - private final JFormattedTextField jNFQualityThreshold; - - private final JFormattedTextField jNFDistanceTolerance; - - private final JFormattedTextField jNFNFrames; - - - private final JFormattedTextField jNFNStepwiseTime; - - private final SpotEditToolParams params; - - final JButton buttonSelectTrackDown; - - final JButton buttonSelectTrackUp; - - final JButton buttonSemiAutoTracking; - - final JButton buttonSelectTrack; - - public SpotEditToolConfigPanel( final SpotEditToolParams params ) - { - - /* - * Listeners - */ - - this.params = params; - final ActionListener al = new ActionListener() - { - @Override - public void actionPerformed( final ActionEvent e ) - { - updateParamsFromTextFields(); - } - }; - final FocusListener fl = new FocusListener() - { - @Override - public void focusLost( final FocusEvent arg0 ) - { - updateParamsFromTextFields(); - } - - @Override - public void focusGained( final FocusEvent arg0 ) - {} - }; - - /* - * GUI - */ - - setTitle( "TrackMate tools" ); - setIconImage( TRACK_ICON.getImage() ); - setResizable( false ); - getContentPane().setLayout( new BoxLayout( getContentPane(), BoxLayout.X_AXIS ) ); - - final JPanel mainPanel = new JPanel(); - getContentPane().add( mainPanel ); - mainPanel.setLayout( null ); - - final JLabel lblTitle = new JLabel( "TrackMate tools" ); - lblTitle.setBounds( 6, 6, 395, 33 ); - lblTitle.setFont( BIG_FONT ); - lblTitle.setIcon( TRACK_ICON_64x64 ); - mainPanel.add( lblTitle ); - - final JPanel panelSemiAutoParams = new JPanel(); - panelSemiAutoParams.setBorder( new LineBorder( new Color( 252, 117, 0 ), 1, false ) ); - panelSemiAutoParams.setBounds( 6, 51, 192, 142 ); - mainPanel.add( panelSemiAutoParams ); - panelSemiAutoParams.setLayout( null ); - - final JLabel lblSemiAutoTracking = new JLabel( "Semi-automatic tracking" ); - lblSemiAutoTracking.setBounds( 6, 6, 180, 16 ); - lblSemiAutoTracking.setFont( FONT.deriveFont( Font.BOLD ) ); - panelSemiAutoParams.add( lblSemiAutoTracking ); - - final JLabel lblQualityThreshold = new JLabel( "Quality threshold" ); - lblQualityThreshold.setToolTipText( "" + - "The fraction of the initial spot quality
    " + - "found spots must have to be considered for linking.
    " + - "The higher, the more stringent." ); - lblQualityThreshold.setBounds( 6, 66, 119, 16 ); - lblQualityThreshold.setFont( SMALL_FONT ); - panelSemiAutoParams.add( lblQualityThreshold ); - - jNFQualityThreshold = new JFormattedTextField( params.qualityThreshold ); - jNFQualityThreshold.setHorizontalAlignment( SwingConstants.CENTER ); - jNFQualityThreshold.setFont( SMALL_FONT ); - jNFQualityThreshold.setBounds( 137, 64, 49, 18 ); - jNFQualityThreshold.addActionListener( al ); - jNFQualityThreshold.addFocusListener( fl ); - - panelSemiAutoParams.add( jNFQualityThreshold ); - - final JLabel lblDistanceTolerance = new JLabel( "Distance tolerance" ); - lblDistanceTolerance.setToolTipText( "" + - "The maximal distance above which found spots are rejected,
    " + - "expressed in units of the initial spot radius." ); - lblDistanceTolerance.setBounds( 6, 86, 119, 16 ); - lblDistanceTolerance.setFont( SMALL_FONT ); - panelSemiAutoParams.add( lblDistanceTolerance ); - - jNFDistanceTolerance = new JFormattedTextField( params.distanceTolerance ); - jNFDistanceTolerance.setHorizontalAlignment( SwingConstants.CENTER ); - jNFDistanceTolerance.setFont( SMALL_FONT ); - jNFDistanceTolerance.setBounds( 137, 84, 49, 18 ); - jNFDistanceTolerance.addActionListener( al ); - jNFDistanceTolerance.addFocusListener( fl ); - panelSemiAutoParams.add( jNFDistanceTolerance ); - - final JLabel lblNFrames = new JLabel( "Max nFrames" ); - lblNFrames.setToolTipText( "How many frames to process at max.
    Make it 0 or negative for no limit." ); - lblNFrames.setBounds( 6, 104, 119, 16 ); - lblNFrames.setFont( SMALL_FONT ); - panelSemiAutoParams.add( lblNFrames ); - - jNFNFrames = new JFormattedTextField( params.nFrames ); - jNFNFrames.setBounds( 137, 104, 49, 18 ); - jNFNFrames.setHorizontalAlignment( SwingConstants.CENTER ); - jNFNFrames.setFont( SMALL_FONT ); - jNFNFrames.addActionListener( al ); - jNFNFrames.addFocusListener( fl ); - panelSemiAutoParams.add( jNFNFrames ); - - buttonSemiAutoTracking = new JButton( SPOT_ICON_64x64 ); - buttonSemiAutoTracking.setBounds( 6, 31, 33, 23 ); - panelSemiAutoParams.add( buttonSemiAutoTracking ); - - final JLabel labelSemiAutoTracking = new JLabel( "Semi-automatic tracking" ); - labelSemiAutoTracking.setToolTipText( "Launch semi-automatic tracking on selected spots." ); - labelSemiAutoTracking.setFont( SMALL_FONT ); - labelSemiAutoTracking.setBounds( 49, 31, 137, 23 ); - panelSemiAutoParams.add( labelSemiAutoTracking ); - - - final JScrollPane scrollPane = new JScrollPane(); - scrollPane.setHorizontalScrollBarPolicy( ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER ); - scrollPane.setVerticalScrollBarPolicy( ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS ); - scrollPane.setBounds( 210, 51, 264, 328 ); - mainPanel.add( scrollPane ); - - final JTextPane textPane = new JTextPane(); - textPane.setFont( SMALL_FONT ); - textPane.setEditable( false ); - textPane.setBackground( this.getBackground() ); - scrollPane.setViewportView( textPane ); - - final JPanel panelButtons = new JPanel(); - panelButtons.setBounds( 6, 262, 192, 117 ); - panelButtons.setBorder( new LineBorder( new Color( 252, 117, 0 ), 1, false ) ); - mainPanel.add( panelButtons ); - panelButtons.setLayout( null ); - - final JLabel lblSelectionTools = new JLabel( "Selection tools" ); - lblSelectionTools.setFont( FONT.deriveFont( Font.BOLD ) ); - lblSelectionTools.setBounds( 6, 11, 172, 14 ); - panelButtons.add( lblSelectionTools ); - - buttonSelectTrack = new JButton( SELECT_TRACK_ICON ); - buttonSelectTrack.setBounds( 10, 36, 33, 23 ); - panelButtons.add( buttonSelectTrack ); - - final JLabel lblSelectTrack = new JLabel( "Select track" ); - lblSelectTrack.setBounds( 53, 36, 129, 23 ); - lblSelectTrack.setFont( SMALL_FONT ); - lblSelectTrack.setToolTipText( "Select the whole tracks selected spots belong to." ); - panelButtons.add( lblSelectTrack ); - - buttonSelectTrackUp = new JButton( SELECT_TRACK_ICON_UPWARDS ); - buttonSelectTrackUp.setBounds( 10, 61, 33, 23 ); - panelButtons.add( buttonSelectTrackUp ); - - final JLabel lblSelectTrackUpward = new JLabel( "Select track upward" ); - lblSelectTrackUpward.setBounds( 53, 61, 129, 23 ); - lblSelectTrackUpward.setFont( SMALL_FONT ); - lblSelectTrackUpward.setToolTipText( "" + - "Select the whole tracks selected spots
    " + - "belong to, backward in time." ); - panelButtons.add( lblSelectTrackUpward ); - - buttonSelectTrackDown = new JButton( SELECT_TRACK_ICON_DOWNWARDS ); - buttonSelectTrackDown.setBounds( 10, 86, 33, 23 ); - panelButtons.add( buttonSelectTrackDown ); - - final JLabel lblSelectTrackDown = new JLabel( "Select track downward" ); - lblSelectTrackDown.setBounds( 53, 86, 129, 23 ); - lblSelectTrackDown.setFont( SMALL_FONT ); - lblSelectTrackDown.setToolTipText( "" + - "Select the whole tracks selected spots
    " + - "belong to, forward in time." ); - panelButtons.add( lblSelectTrackDown ); - - final JPanel panel = new JPanel(); - panel.setBorder( new LineBorder( new Color( 252, 117, 0 ) ) ); - panel.setBounds( 6, 201, 192, 53 ); - mainPanel.add( panel ); - panel.setLayout( null ); - - final JLabel lblNavigationTools = new JLabel( "Navigation tools" ); - lblNavigationTools.setBounds( 6, 6, 172, 14 ); - lblNavigationTools.setFont( FONT.deriveFont( Font.BOLD ) ); - panel.add( lblNavigationTools ); - - jNFNStepwiseTime = new JFormattedTextField( params.stepwiseTimeBrowsing ); - jNFNStepwiseTime.setBounds( 137, 26, 49, 18 ); - jNFNStepwiseTime.setHorizontalAlignment( SwingConstants.CENTER ); - jNFNStepwiseTime.setFont( SMALL_FONT ); - jNFNStepwiseTime.addActionListener( al ); - jNFNStepwiseTime.addFocusListener( fl ); - panel.add( jNFNStepwiseTime ); - - final JLabel lblJumpByb = new JLabel( "Stepwise time browsing" ); - lblJumpByb.setBounds( 10, 29, 120, 14 ); - lblJumpByb.setFont( SMALL_FONT ); - panel.add( lblJumpByb ); - - logger = new 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( new Runnable() - { - @Override - public void run() - { - textPane.setEditable( true ); - final StyleContext sc = StyleContext.getDefaultStyleContext(); - final AttributeSet aset = sc.addAttribute( SimpleAttributeSet.EMPTY, StyleConstants.Foreground, color ); - final int len = textPane.getDocument().getLength(); - textPane.setCaretPosition( len ); - textPane.setCharacterAttributes( aset, false ); - textPane.replaceSelection( message ); - textPane.setEditable( false ); - } - } ); - } - - @Override - public void setStatus( final String status ) - { - log( status, Logger.GREEN_COLOR ); - } - - @Override - public void setProgress( final double val ) - {} - }; - - setSize( 487, 418 ); - setDefaultCloseOperation( WindowConstants.HIDE_ON_CLOSE ); - } - - /** - * Returns the {@link Logger} that outputs on this config panel. - * - * @return the {@link Logger} instance of this panel. - */ - public Logger getLogger() - { - return logger; - } - - private void updateParamsFromTextFields() - { - params.distanceTolerance = ( ( Number ) jNFDistanceTolerance.getValue() ).doubleValue(); - params.qualityThreshold = ( ( Number ) jNFQualityThreshold.getValue() ).doubleValue(); - params.nFrames = ( ( Number ) jNFNFrames.getValue() ).intValue(); - params.stepwiseTimeBrowsing = ( ( Number ) jNFNStepwiseTime.getValue() ).intValue(); - } -} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotOverlay.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotOverlay.java index 3c667e9a1..4e6c5e342 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotOverlay.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotOverlay.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -32,15 +32,14 @@ import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.geom.AffineTransform; -import java.awt.geom.Path2D; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.SpotMesh; import fiji.plugin.trackmate.SpotRoi; import fiji.plugin.trackmate.features.FeatureUtils; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; @@ -61,8 +60,6 @@ public class SpotOverlay extends Roi private static final long serialVersionUID = 1L; - protected Spot editingSpot; - protected final double[] calibration; protected FontMetrics fm; @@ -73,6 +70,12 @@ public class SpotOverlay extends Roi protected final Model model; + private final PaintSpotRoi paintSpotRoi; + + private final PaintSpotSphere paintSpotSphere; + + private final PaintSpotMesh paintSpotMesh; + /* * CONSTRUCTOR */ @@ -84,6 +87,9 @@ public SpotOverlay( final Model model, final ImagePlus imp, final DisplaySetting this.imp = imp; this.calibration = TMUtils.getSpatialCalibration( imp ); this.displaySettings = displaySettings; + this.paintSpotSphere = new PaintSpotSphere( imp, calibration, displaySettings ); + this.paintSpotRoi = new PaintSpotRoi( imp, calibration, displaySettings ); + this.paintSpotMesh = new PaintSpotMesh( imp, calibration, displaySettings ); } /* @@ -132,14 +138,11 @@ public void drawOverlay( final Graphics g ) g2d.setStroke( new BasicStroke( ( float ) displaySettings.getLineThickness() ) ); - if ( selectionOnly && null != spotSelection) + if ( selectionOnly && null != spotSelection ) { // Track display mode only displays selection. for ( final Spot spot : spotSelection ) { - if ( spot == editingSpot ) - continue; - final int sFrame = spot.getFeature( Spot.FRAME ).intValue(); if ( sFrame != frame ) continue; @@ -152,7 +155,6 @@ public void drawOverlay( final Graphics g ) g2d.setColor( color ); drawSpot( g2d, spot, zslice, xcorner, ycorner, lMag, filled ); } - } else { @@ -161,7 +163,7 @@ public void drawOverlay( final Graphics g ) { final Spot spot = iterator.next(); - if ( editingSpot == spot || ( spotSelection != null && spotSelection.contains( spot ) ) ) + if ( spotSelection != null && spotSelection.contains( spot ) ) continue; final Color color = colorGenerator.color( spot ); @@ -181,9 +183,6 @@ public void drawOverlay( final Graphics g ) g2d.setColor( displaySettings.getHighlightColor() ); for ( final Spot spot : spotSelection ) { - if ( spot == editingSpot ) - continue; - final int sFrame = spot.getFeature( Spot.FRAME ).intValue(); if ( sFrame != frame ) continue; @@ -195,32 +194,6 @@ public void drawOverlay( final Graphics g ) drawExtraLayer( g2d, frame ); - /* - * Deal with editing spot - we always draw it with its center at the - * current z, current t (it moves along with the current slice). - */ - if ( null != editingSpot ) - { - g2d.setColor( displaySettings.getHighlightColor() ); - g2d.setStroke( new BasicStroke( - ( float ) displaySettings.getLineThickness(), - BasicStroke.CAP_ROUND, - BasicStroke.JOIN_ROUND, - 1.0f, - new float[] { 5f, 5f }, 0 ) ); - final double x = editingSpot.getFeature( Spot.POSITION_X ); - final double y = editingSpot.getFeature( Spot.POSITION_Y ); - final double radius = editingSpot.getFeature( Spot.RADIUS ) / calibration[ 0 ] * lMag; - // In pixel units - final double xp = x / calibration[ 0 ] + 0.5d; - final double yp = y / calibration[ 1 ] + 0.5d; - // Scale to image zoom - final double xs = ( xp - xcorner ) * lMag; - final double ys = ( yp - ycorner ) * lMag; - final double radiusRatio = displaySettings.getSpotDisplayRadius(); - g2d.drawOval( ( int ) Math.round( xs - radius * radiusRatio ), ( int ) Math.round( ys - radius * radiusRatio ), ( int ) Math.round( 2 * radius * radiusRatio ), ( int ) Math.round( 2 * radius * radiusRatio ) ); - } - // Restore graphic device original settings g2d.setTransform( originalTransform ); g2d.setComposite( originalComposite ); @@ -229,15 +202,6 @@ public void drawOverlay( final Graphics g ) g2d.setFont( originalFont ); } - /** - * Draws an extra layer on top of the spots. The default implementation does - * nothing. - * - * @param g2d - * the graphics device. - * @param frame - * the frame currently drawn. - */ protected void drawExtraLayer( final Graphics2D g2d, final int frame ) {} @@ -248,86 +212,60 @@ public void setSpotSelection( final Collection< Spot > spots ) protected void drawSpot( final Graphics2D g2d, final Spot spot, final double zslice, final int xcorner, final int ycorner, final double magnification, final boolean filled ) { + // Spot center in pixel coords. final double x = spot.getFeature( Spot.POSITION_X ); final double y = spot.getFeature( Spot.POSITION_Y ); - final double z = spot.getFeature( Spot.POSITION_Z ); - final double dz2 = ( z - zslice ) * ( z - zslice ); - final double radiusRatio = displaySettings.getSpotDisplayRadius(); - final double radius = spot.getFeature( Spot.RADIUS ) * radiusRatio; - // In pixel units + // Pixel coords. final double xp = x / calibration[ 0 ] + 0.5f; final double yp = y / calibration[ 1 ] + 0.5f; - // so that spot centers are displayed on the pixel centers. - - // Scale to image zoom + // 0.5, so that spot centers are displayed on the pixel centers. + // Display window coordinates. final double xs = ( xp - xcorner ) * magnification; final double ys = ( yp - ycorner ) * magnification; - if ( dz2 >= radius * radius ) - { - g2d.fillOval( ( int ) Math.round( xs - 2 * magnification ), ( int ) Math.round( ys - 2 * magnification ), ( int ) Math.round( 4 * magnification ), ( int ) Math.round( 4 * magnification ) ); - return; - } + // Get a painter adequate for the spot and config we have. + @SuppressWarnings( "rawtypes" ) + final TrackMatePainter painter = getPainter( spot ); + @SuppressWarnings( "unchecked" ) + final int textPos = painter.paint( g2d, spot ); - final SpotRoi roi = spot.getRoi(); - if ( !displaySettings.isSpotDisplayedAsRoi() || roi == null || roi.x.length < 2 ) + if ( textPos >= 0 && displaySettings.isSpotShowName() ) { - final double apparentRadius = Math.sqrt( radius * radius - dz2 ) / calibration[ 0 ] * magnification; - final int textPos = ( int ) apparentRadius; - if ( displaySettings.isSpotShowName() ) - drawSpotName( g2d, spot, xs, ys, textPos ); - if ( filled ) - g2d.fillOval( - ( int ) Math.round( xs - apparentRadius ), - ( int ) Math.round( ys - apparentRadius ), - ( int ) Math.round( 2 * apparentRadius ), - ( int ) Math.round( 2 * apparentRadius ) ); - else - g2d.drawOval( - ( int ) Math.round( xs - apparentRadius ), - ( int ) Math.round( ys - apparentRadius ), - ( int ) Math.round( 2 * apparentRadius ), - ( int ) Math.round( 2 * apparentRadius ) ); - } - else - { - final double[] polygonX = roi.toPolygonX( calibration[ 0 ], xcorner - 0.5, x, magnification ); - final double[] polygonY = roi.toPolygonY( calibration[ 1 ], ycorner - 0.5, y, magnification ); - // The 0.5 is here so that we plot vertices at pixel centers. - final Path2D polygon = new Path2D.Double(); - polygon.moveTo( polygonX[ 0 ], polygonY[ 0 ] ); - for ( int i = 1; i < polygonX.length; ++i ) - polygon.lineTo( polygonX[ i ], polygonY[ i ] ); - polygon.closePath(); - final int textPos = ( int ) ( Arrays.stream( polygonX ).max().getAsDouble() - xs ); - - if ( filled ) - { - if ( displaySettings.isSpotShowName() ) - drawSpotName( g2d, spot, xs, ys, textPos ); - g2d.fill( polygon ); - g2d.setColor( Color.BLACK ); - g2d.draw( polygon ); - } - else - { - if ( displaySettings.isSpotShowName() ) - drawSpotName( g2d, spot, xs, ys, textPos ); - g2d.draw( polygon ); - } + final int windowWidth = imp.getWindow().getWidth(); + drawString( g2d, fm, windowWidth, spot.toString(), xs, ys, textPos ); } } - private final void drawSpotName( final Graphics2D g2d, final Spot spot, final double xs, final double ys, final int textPos ) + private TrackMatePainter< ? extends Spot > getPainter( final Spot spot ) + { + if ( !displaySettings.isSpotDisplayedAsRoi() ) + return paintSpotSphere; + + if ( spot instanceof SpotRoi ) + return paintSpotRoi; + + if ( spot instanceof SpotMesh ) + return paintSpotMesh; + + return paintSpotSphere; + } + + private static final void drawString( + final Graphics2D g2d, + final FontMetrics fm, + final int windowWidth, + final String str, + final double xs, + final double ys, + final int textPos ) { - final String str = spot.toString(); final int xindent = fm.stringWidth( str ); int xtext = ( int ) ( xs + textPos + 5 ); - if ( xtext + xindent > imp.getWindow().getWidth() ) + if ( xtext + xindent > windowWidth ) xtext = ( int ) ( xs - textPos - 5 - xindent ); final int yindent = fm.getAscent() / 2; final int ytext = ( int ) ys + yindent; - g2d.drawString( spot.toString(), xtext, ytext ); + g2d.drawString( str, xtext, ytext ); } } diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/TrackMatePainter.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/TrackMatePainter.java new file mode 100644 index 000000000..95be2bdf8 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/TrackMatePainter.java @@ -0,0 +1,156 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.visualization.hyperstack; + +import java.awt.Graphics2D; + +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import ij.ImagePlus; +import ij.gui.ImageCanvas; +import net.imglib2.RealInterval; +import net.imglib2.RealLocalizable; + +public abstract class TrackMatePainter< T extends Spot > +{ + + protected final double[] calibration; + + protected final DisplaySettings displaySettings; + + protected final ImagePlus imp; + + public TrackMatePainter( final ImagePlus imp, final double[] calibration, final DisplaySettings displaySettings ) + { + this.imp = imp; + this.calibration = calibration; + this.displaySettings = displaySettings; + } + + public abstract int paint( final Graphics2D g2d, final T spot ); + + /** + * Returns true if the specified bounding-box, shifted by the + * Specified amount, intersects with the display window. + * + * @param boundingBox + * the bounding box, centered at (0,0), in physical coordinates. + * @param center + * the center of the bounding-box, in physical coordinates. + * @return if the specified bounding-box intersects with the display window. + */ + protected boolean intersect( final RealInterval boundingBox, final RealLocalizable center ) + { + final ImageCanvas canvas = imp.getCanvas(); + if ( canvas == null ) + return false; + + if ( toScreenX( boundingBox.realMin( 0 ) + center.getDoublePosition( 0 ) ) > canvas.getWidth() ) + return false; + if ( toScreenX( boundingBox.realMax( 0 ) + center.getDoublePosition( 0 ) ) < 0 ) + return false; + if ( toScreenY( boundingBox.realMin( 1 ) + center.getDoublePosition( 1 ) ) > canvas.getHeight() ) + return false; + if ( toScreenY( boundingBox.realMax( 1 ) + center.getDoublePosition( 1 ) ) < 0 ) + return false; + return true; + } + + /** + * Returns true if the specified bounding-box intersects with + * the display window. + * + * @param boundingBox + * the bounding box, in physical coordinates. + * @return true if the specified bounding-box intersects with + * the display window. + */ + protected boolean intersect( final RealInterval boundingBox ) + { + final ImageCanvas canvas = imp.getCanvas(); + if ( canvas == null ) + return false; + + if ( toScreenX( boundingBox.realMin( 0 ) ) > canvas.getWidth() ) + return false; + if ( toScreenX( boundingBox.realMax( 0 ) ) < 0 ) + return false; + if ( toScreenY( boundingBox.realMin( 1 ) ) > canvas.getHeight() ) + return false; + if ( toScreenY( boundingBox.realMax( 1 ) ) < 0 ) + return false; + return true; + } + + /** + * Converts a X position in physical units (possible um) to screen + * coordinates to be used with the graphics object. + * + * @param x + * the X position to convert. + * @return the screen X coordinate. + */ + protected double toScreenX( final double x ) + { + final ImageCanvas canvas = imp.getCanvas(); + if ( canvas == null ) + return Double.NaN; + + final double xp = x / calibration[ 0 ] + 0.5; // pixel coords + return canvas.screenXD( xp ); + } + + /** + * Converts a Y position in physical units (possible um) to screen + * coordinates to be used with the graphics object. + * + * @param y + * the Y position to convert. + * @return the screen Y coordinate. + */ + protected double toScreenY( final double y ) + { + final ImageCanvas canvas = imp.getCanvas(); + if ( canvas == null ) + return Double.NaN; + + final double yp = y / calibration[ 0 ] + 0.5; // pixel coords + return canvas.screenYD( yp ); + } + + protected void paintOutOfFocus( final Graphics2D g2d, final double xs, final double ys ) + { + final double magnification = getMagnification(); + g2d.fillOval( + ( int ) Math.round( xs - 2 * magnification ), + ( int ) Math.round( ys - 2 * magnification ), + ( int ) Math.round( 4 * magnification ), + ( int ) Math.round( 4 * magnification ) ); + } + + protected double getMagnification() + { + if ( imp.getCanvas() == null ) + return 1.; + return imp.getCanvas().getMagnification(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/AbstractSpotEditBehaviour.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/AbstractSpotEditBehaviour.java new file mode 100644 index 000000000..8e098143e --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/AbstractSpotEditBehaviour.java @@ -0,0 +1,49 @@ +package fiji.plugin.trackmate.visualization.hyperstack.behaviours; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.util.TMUtils; +import ij.ImagePlus; +import ij.gui.ImageCanvas; +import net.imglib2.RealLocalizable; +import net.imglib2.RealPoint; + +public class AbstractSpotEditBehaviour +{ + + protected final Model model; + + protected final ImagePlus imp; + + protected final double[] calibration; + + public AbstractSpotEditBehaviour( final Model model, final ImagePlus imp ) + { + this.model = model; + this.imp = imp; + this.calibration = TMUtils.getSpatialCalibration( imp ); + } + + protected Spot getSpotAtMouseLocation( final RealLocalizable pos ) + { + final int frame = imp.getFrame() - 1; + return model.getSpots().getSpotAt( pos, frame, true ); + } + + protected RealLocalizable toWorldCoords( final int x, final int y ) + { + final ImageCanvas canvas = imp.getCanvas(); + final double xw = ( -0.5 + canvas.offScreenXD( x ) ) * calibration[ 0 ]; + final double yw = ( -0.5 + canvas.offScreenYD( y ) ) * calibration[ 1 ]; + final double zw = ( imp.getSlice() - 1 ) * calibration[ 2 ]; + return new RealPoint( xw, yw, zw ); + } + + protected RealLocalizable toScreenCoords( final RealLocalizable pos ) + { + final ImageCanvas canvas = imp.getCanvas(); + final double xs = canvas.screenXD( pos.getDoublePosition( 0 ) / calibration[ 0 ] + 0.5 ); + final double ys = canvas.screenYD( pos.getDoublePosition( 1 ) / calibration[ 1 ] + 0.5 ); + return new RealPoint( xs, ys ); + } +} \ No newline at end of file diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/AddAndLinkSpotBehaviour.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/AddAndLinkSpotBehaviour.java new file mode 100644 index 000000000..3ae5a342d --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/AddAndLinkSpotBehaviour.java @@ -0,0 +1,174 @@ +package fiji.plugin.trackmate.visualization.hyperstack.behaviours; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; +import ij.ImagePlus; +import net.imglib2.KDTree; +import net.imglib2.RealLocalizable; +import net.imglib2.neighborsearch.NearestNeighborSearchOnKDTree; + +public class AddAndLinkSpotBehaviour extends LinkSpotsBehaviour +{ + + private static final String OVERLAY_NAME = "AddAndLinkSpotActionOverlay"; + + private SpotBase newSpot; + + public AddAndLinkSpotBehaviour( final Model model, final ImagePlus imp, final boolean backward ) + { + super( model, imp, backward ); + } + + @Override + public void init( final int x, final int y ) + { + if ( source != null || target != null ) + return; + + final RealLocalizable pos = toWorldCoords( x, y ); + final Spot spot = getSpotAtMouseLocation( pos ); + final int targetFrame; + if ( spot == null ) + { + // Create a new spot, that we will move around until the user + // releases the mouse button. + this.source = null; + overlay.source = null; + targetFrame = imp.getT() - 1; + this.search = null; + } + else + { + // We have a source, link from it. + // Cannot link if at first frame and backward, etc. + final int frame = spot.getFeature( Spot.FRAME ).intValue(); + if ( backward && frame == 0 ) + return; + if ( !backward && frame == imp.getNFrames() - 1 ) + return; + + // Keep track of the source. + this.source = spot; + overlay.source = source; + overlay.sourcePixelPos[ 0 ] = x; + overlay.sourcePixelPos[ 1 ] = y; + + // Move to next frame if forward, previous frame if backward. + targetFrame = backward ? frame - 1 : frame + 1; + imp.setT( targetFrame + 1 ); + + // Build KD-tree of target frame spots. + final Iterable< Spot > targetSpots = model.getSpots().iterable( targetFrame, true ); + final int nTargetSpots = model.getSpots().getNSpots( targetFrame, true ); + if ( nTargetSpots > 0 ) + { + final KDTree< Spot > tree = new KDTree< Spot >( nTargetSpots, targetSpots, targetSpots ); + this.search = new NearestNeighborSearchOnKDTree<>( tree ); + } + else + { + this.search = null; + } + } + + // Add a new spot at the mouse location in the target frame. + final double radius = SpotEditBehaviours.ResizeSpotBehaviour.previousRadius; + this.newSpot = new SpotBase( pos, radius, -1. ); + final double dt = imp.getCalibration().frameInterval; + newSpot.putFeature( Spot.POSITION_T, targetFrame * dt ); + newSpot.putFeature( Spot.FRAME, Double.valueOf( targetFrame ) ); + + target = newSpot; + overlay.target = newSpot; + overlay.targetPixelPos[ 0 ] = x; + overlay.targetPixelPos[ 1 ] = y; + imp.getOverlay().add( overlay, OVERLAY_NAME ); + imp.updateAndDraw(); + } + + @Override + public void drag( final int x, final int y ) + { + final RealLocalizable pos = toWorldCoords( x, y ); + if ( search != null ) + { + search.search( pos ); + final Spot closestSpot = search.getSampler().get(); + final double r = closestSpot.getFeature( Spot.RADIUS ); + if ( search.getSquareDistance() < r * r ) + { + target = closestSpot; + final RealLocalizable screenPos = toScreenCoords( target ); + overlay.targetPixelPos[ 0 ] = ( int ) Math.round( screenPos.getDoublePosition( 0 ) ); + overlay.targetPixelPos[ 1 ] = ( int ) Math.round( screenPos.getDoublePosition( 1 ) ); + overlay.target = closestSpot; + + // Is there an existing link between source and target? + overlay.crossedLine.crossed = model.getTrackModel().containsEdge( source, target ); + imp.updateAndDraw(); + return; + } + } + // Simply move the created spot. + newSpot.setPosition( pos.getDoublePosition( 0 ), 0 ); + newSpot.setPosition( pos.getDoublePosition( 1 ), 1 ); + target = newSpot; + overlay.targetPixelPos[ 0 ] = x; + overlay.targetPixelPos[ 1 ] = y; + overlay.target = newSpot; + overlay.crossedLine.crossed = false; + imp.updateAndDraw(); + } + + @Override + public void end( final int x, final int y ) + { + try + { + if ( target == null ) + return; + + model.beginUpdate(); + try + { + if ( target == newSpot ) + { + model.addSpotTo( newSpot, newSpot.getFeature( Spot.FRAME ).intValue() ); + if ( source != null ) + model.addEdge( source, newSpot, -1 ); + } + else + { + // Add or remove link between source and pre-existing + // target. + if ( model.getTrackModel().containsEdge( source, target ) ) + { + model.removeEdge( source, target ); + } + else + { + if ( backward ) + model.addEdge( target, source, -1 ); + else + model.addEdge( source, target, -1 ); + } + } + } + finally + { + model.endUpdate(); + } + } + finally + { + source = null; + target = null; + search = null; + overlay.source = null; + overlay.target = null; + imp.updateAndDraw(); + imp.getOverlay().remove( OVERLAY_NAME ); + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/HyperStackDisplayerActions.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/HyperStackDisplayerActions.java new file mode 100644 index 000000000..d61ac9e4f --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/HyperStackDisplayerActions.java @@ -0,0 +1,130 @@ +package fiji.plugin.trackmate.visualization.hyperstack.behaviours; + +import java.awt.event.ActionEvent; + +import org.scijava.plugin.Plugin; +import org.scijava.ui.behaviour.io.gui.CommandDescriptionProvider; +import org.scijava.ui.behaviour.io.gui.CommandDescriptions; +import org.scijava.ui.behaviour.util.AbstractNamedAction; +import org.scijava.ui.behaviour.util.Actions; + +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.visualization.hyperstack.behaviours.semiautotracking.SemiAutoTracking; +import fiji.plugin.trackmate.visualization.hyperstack.behaviours.semiautotracking.SemiAutoTrackingParams; +import fiji.plugin.trackmate.visualization.ui.KeyConfigContexts; +import ij.ImagePlus; + +public class HyperStackDisplayerActions +{ + private static final String TOGGLE_AUTO_LINKING = "toggle auto-linking"; + + private static final String[] TOGGLE_AUTO_LINKING_KEYS = new String[] { "ctrl L" }; + + private static final String NEXT_TIMEPOINT = "next timepoint"; + + private static final String PREVIOUS_TIMEPOINT = "previous timepoint"; + + private static final String[] NEXT_TIMEPOINT_KEYS = new String[] { "G" }; + + private static final String[] PREVIOUS_TIMEPOINT_KEYS = new String[] { "F" }; + + private static final String SEMI_AUTOMATIC_TRACKING = "semi-automatic tracking"; + + private static final String[] SEMI_AUTOMATIC_TRACKING_KEYS = new String[] { "shift T" }; + + private static final String DO_NOTHING_ACTION = "do nothing"; + + private static final String[] DO_NOTHING_ACTION_KEYS = new String[] { "W" }; + + public static final void install( final Actions actions, final GuiModel guiModel, final ImagePlus imp ) + { + final SemiAutoTrackingParams params = guiModel.getSemiAutoTrackingParams(); + + // Toggle auto-linking + actions.runnableAction( () -> SpotEditBehaviours.autoLinkingmode = !SpotEditBehaviours.autoLinkingmode, TOGGLE_AUTO_LINKING, TOGGLE_AUTO_LINKING_KEYS ); + + // Avoid closing the window when pressing W + actions.runnableAction( () -> {}, DO_NOTHING_ACTION, DO_NOTHING_ACTION_KEYS ); + + // Change timepoint + actions.namedAction( new StepWiseTimeBrowsingAction( imp, params, true ), NEXT_TIMEPOINT_KEYS ); + actions.namedAction( new StepWiseTimeBrowsingAction( imp, params, false ), PREVIOUS_TIMEPOINT_KEYS ); + + // Semi-automatic tracking + final SemiAutoTracking semiAutoTracking = new SemiAutoTracking( guiModel ); + actions.runnableAction( () -> semiAutoTracking.run(), SEMI_AUTOMATIC_TRACKING, SEMI_AUTOMATIC_TRACKING_KEYS ); + } + + @Plugin( type = CommandDescriptionProvider.class ) + public static class Descriptions extends CommandDescriptionProvider + { + public Descriptions() + { + super( KeyConfigContexts.KEY_CONFIG_SCOPE, KeyConfigContexts.HYPERSTACK_DISPLAYER ); + } + + @Override + public void getCommandDescriptions( final CommandDescriptions descriptions ) + { + descriptions.add( TOGGLE_AUTO_LINKING, TOGGLE_AUTO_LINKING_KEYS, "Toggle the auto-linking mode." ); + descriptions.add( NEXT_TIMEPOINT, NEXT_TIMEPOINT_KEYS, "Go to the next timepoint." ); + descriptions.add( PREVIOUS_TIMEPOINT, PREVIOUS_TIMEPOINT_KEYS, "Go to the previous timepoint." ); + descriptions.add( SEMI_AUTOMATIC_TRACKING, SEMI_AUTOMATIC_TRACKING_KEYS, "Run semi-automatic tracking on the selected spots." ); + descriptions.add( DO_NOTHING_ACTION, DO_NOTHING_ACTION_KEYS, "Do nothing. This is to avoid closing the window when pressing W." ); + } + } + + /** + * Ensure we browse a fixed divisions of the timepoints, rather than just + * incrementing or decrementing by one. This is useful when the timepoints + * are not consecutive, e.g. when the user has selected a subset of + * timepoints to display. Taken from what we did in MaMuT. + */ + private static class StepWiseTimeBrowsingAction extends AbstractNamedAction + { + + private static final long serialVersionUID = 1L; + + private final boolean forward; + + private final ImagePlus imp; + + private final SemiAutoTrackingParams params; + + public StepWiseTimeBrowsingAction( final ImagePlus imp, final SemiAutoTrackingParams params, final boolean forward ) + { + super( forward ? NEXT_TIMEPOINT : PREVIOUS_TIMEPOINT ); + this.imp = imp; + this.params = params; + this.forward = forward; + } + + @Override + public void actionPerformed( final ActionEvent e ) + { + final int currentT = imp.getT() - 1; + final int timeStep = params.stepwiseTimeBrowsing(); + final int prevStep = ( currentT / timeStep ) * timeStep; + int tp; // 0-based + if ( forward ) + { + tp = prevStep + timeStep; + } + else + { + if ( currentT == prevStep ) + tp = currentT - timeStep; + else + tp = prevStep; + } + + if ( tp < 0 ) + tp = 0; + + if ( tp > imp.getNFrames() - 1 ) + tp = imp.getNFrames() - 1; + + imp.setT( tp + 1 ); // ImageJ is 1-based + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/ImagePlusBehavioursAdapter.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/ImagePlusBehavioursAdapter.java new file mode 100644 index 000000000..f44279adf --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/ImagePlusBehavioursAdapter.java @@ -0,0 +1,363 @@ +package fiji.plugin.trackmate.visualization.hyperstack.behaviours; + +import java.awt.event.InputEvent; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Set; + +import javax.swing.Action; +import javax.swing.ActionMap; +import javax.swing.InputMap; +import javax.swing.KeyStroke; + +import org.scijava.ui.behaviour.BehaviourMap; +import org.scijava.ui.behaviour.GlobalKeyEventDispatcher; +import org.scijava.ui.behaviour.InputTrigger; +import org.scijava.ui.behaviour.InputTriggerMap; +import org.scijava.ui.behaviour.MouseAndKeyHandler; +import org.scijava.ui.behaviour.io.InputTriggerConfig; +import org.scijava.ui.behaviour.util.Actions; +import org.scijava.ui.behaviour.util.Behaviours; +import org.scijava.ui.behaviour.util.InputActionBindings; +import org.scijava.ui.behaviour.util.TriggerBehaviourBindings; + +import bdv.ui.keymap.Keymap; +import bdv.ui.keymap.KeymapManager; +import gnu.trove.set.TIntSet; +import ij.ImagePlus; +import ij.gui.ImageCanvas; +import ij.gui.ImageWindow; + +/** + * Adapter class that connects a {@link ImagePlus} to the SciJava Behaviours + * framework, and provides access to the {@link Actions} and {@link Behaviours} + * objects. + *

    + * The trick is that the scijava ui-behaviour framework is designed for Swing + * components, and the ImagePlus canvas is an AWT component. This class bridges + * the gap by using a proxy to intercept mouse and key events and route them to + * the appropriate actions and behaviours. + *

    + * The actions and behaviours are positioned 'before' the original ImageJ key + * listener, so that the key bindings defined in the keymap override the default + * ImageJ key bindings. However, if a key binding exists for ImageJ but not for + * the actions and behaviours created here, the event will be passed to the + * original ImageJ key listener. + * + * @author Jean-Yves Tinevez + */ +public class ImagePlusBehavioursAdapter +{ + + private final Actions actions; + + private final Behaviours behaviours; + + private InputActionBindings keybindings; + + public ImagePlusBehavioursAdapter( final ImagePlus imp, final KeymapManager keymapManager, final String[] keyConfigContexts ) + { + final ImageCanvas canvas = imp.getCanvas(); + final ImageWindow window = imp.getWindow(); + + // Initialize configuration and binding registries + this.keybindings = new InputActionBindings(); + final TriggerBehaviourBindings behaviourBindings = new TriggerBehaviourBindings(); + keymapManager.discoverCommandDescriptions(); + final InputTriggerConfig config = keymapManager.getForwardSelectedKeymap().getConfig(); + + // Behaviours + this.behaviours = new Behaviours( config, keyConfigContexts ); + behaviours.install( behaviourBindings, keyConfigContexts[ 0 ] + "-behaviours" ); + + // Actions + final InputMap inputMap = keybindings.getConcatenatedInputMap(); + final ActionMap actionMap = keybindings.getConcatenatedActionMap(); + this.actions = new Actions( config, keyConfigContexts ); + actions.install( keybindings, keyConfigContexts[ 0 ] + "-actions" ); + + final Keymap keymap = keymapManager.getForwardSelectedKeymap(); + keymap.updateListeners().add( () -> { + actions.updateKeyConfig( keymap.getConfig() ); + behaviours.updateKeyConfig( keymap.getConfig() ); + } ); + actions.updateKeyConfig( keymap.getConfig() ); + behaviours.updateKeyConfig( keymap.getConfig() ); + + // Initialize the handler + final MouseAndKeyHandler handler = new MouseAndKeyHandler(); + final InputTriggerMap inputTriggerMap = behaviours.getInputTriggerMap(); + final BehaviourMap behaviourMap = behaviours.getBehaviourMap(); + handler.setInputMap( inputTriggerMap ); + handler.setBehaviourMap( behaviourMap ); + + // Put the IJ key listener at the end of the chain, so that we can + // intercept events before they reach it. + final KeyListener[] canvasKeyListeners = canvas.getKeyListeners(); + for ( final KeyListener keyListener : canvasKeyListeners ) + canvas.removeKeyListener( keyListener ); + // The same for the window + final KeyListener[] windowKeyListeners = window.getKeyListeners(); + for ( final KeyListener keyListener : windowKeyListeners ) + window.removeKeyListener( keyListener ); + + /* + * Connect the handler to the AWT component for Mouse handling We need + * the proxy because the default MouseAndKeyHandler does not consume + * events when a trigger is matched, and without it the events are sent + * to the ImageJ original KeyListener. + */ + final MouseEventProxy proxy = new MouseEventProxy( handler, inputTriggerMap ); + canvas.addKeyListener( proxy ); + canvas.addMouseListener( proxy ); + canvas.addMouseMotionListener( proxy ); + canvas.addMouseWheelListener( proxy ); + window.addKeyListener( proxy ); + window.addMouseListener( proxy ); + window.addMouseMotionListener( proxy ); + window.addMouseWheelListener( proxy ); + + /* + * Direct Key Event Proxy Bridge. Because an AWT Canvas bypasses Swing's + * ActionMap dispatch pipeline, we manually intercept the KeyStrokes and + * route them to our Action map. + */ + final KeyAdapter actionsRoutingProxy = new KeyAdapter() + { + @Override + public void keyPressed( final KeyEvent e ) + { + // Get the keystroke matching this precise key event + final KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent( e ); + if ( keyStroke == null ) + return; + + // Lookup the unique Action ID string assigned to this key combo + final Object actionKey = inputMap.get( keyStroke ); + if ( actionKey != null ) + { + // Pull the runnable action associated with that ID + final Action action = actionMap.get( actionKey ); + if ( action != null && action.isEnabled() ) + { + // Fire the shortcut action directly on the EDT + action.actionPerformed( null ); + e.consume(); // Block from propagating to IJ listener + } + } + } + }; + canvas.addKeyListener( actionsRoutingProxy ); + window.addKeyListener( actionsRoutingProxy ); + + // Re-add the original ImageJ KeyListener after all proxies + for ( final KeyListener keyListener : canvasKeyListeners ) + canvas.addKeyListener( keyListener ); + for ( final KeyListener keyListener : windowKeyListeners ) + window.addKeyListener( keyListener ); + } + + public Actions actions() + { + return actions; + } + + public Behaviours behaviours() + { + return behaviours; + } + + public InputActionBindings keybindings() + { + return keybindings; + } + + /** + * A proxy that intercepts mouse and key events, and routes them to the + * appropriate actions and behaviours. It also consumes the events if they + * match a trigger, preventing them from reaching the original ImageJ key + * listener. + */ + private static class MouseEventProxy implements MouseListener, MouseMotionListener, MouseWheelListener, KeyListener + { + private final MouseAndKeyHandler delegate; + + private final InputTriggerMap triggerMap; + + private Method getMaskMethod; + + /** + * Constructs a new MouseEventProxy. + * + * @param delegate + * The active MouseAndKeyHandler driving the interaction + * updates. + * @param triggerMap + * The mapping table containing target behavioral shortcuts. + */ + public MouseEventProxy( final MouseAndKeyHandler delegate, final InputTriggerMap triggerMap ) + { + this.delegate = delegate; + this.triggerMap = triggerMap; + try + { + // Extract ui-behaviour's internal structural input mask + // calculator via reflection + this.getMaskMethod = MouseAndKeyHandler.class.getDeclaredMethod( "getMask", InputEvent.class ); + this.getMaskMethod.setAccessible( true ); + } + catch ( final Exception ex ) + { + this.getMaskMethod = null; + System.err.println( "[ImagePlusBehavioursAdapter] Failed to extract getMask() method from MouseAndKeyHandler via reflection. Behavioral trigger matching will be disabled." ); // DEBUG + } + } + + /** + * Safely determines if the active hardware interaction features a + * behavioral binding without causing NullPointerExceptions in child + * trigger maps. + */ + private boolean hasMatchingBehaviour( final InputEvent e ) + { + if ( getMaskMethod == null || triggerMap == null ) + return false; + + try + { + // Retrieve the calculated normalization bitmask from the + // handler instance + final int mask = ( Integer ) getMaskMethod.invoke( delegate, e ); + + // Keep the primitive TIntSet collection directly without + // calling .toArray() + final TIntSet pressedKeys = GlobalKeyEventDispatcher.getInstance().pressedKeys(); + + // Safely pull the flattened trigger assignments map from our + // configuration + final Map< InputTrigger, Set< String > > bindings = triggerMap.getAllBindings(); + + if ( bindings != null ) + { + for ( final Map.Entry< InputTrigger, Set< String > > entry : bindings.entrySet() ) + { + final InputTrigger trigger = entry.getKey(); + final Set< String > keys = entry.getValue(); + + // Check if trigger conditions match and that it + // contains actual bound behaviors + if ( trigger != null && trigger.matches( mask, pressedKeys ) && keys != null && !keys.isEmpty() ) + return true; + } + } + } + catch ( final Exception ex ) + { + System.err.println( "[ImagePlusBehavioursAdapter] Exception occurred while checking for matching behaviour: " + ex.getMessage() ); // DEBUG + // Prevent crashes from faulty reflections, letting the event + // propagate unconsumed + } + return false; + } + + @Override + public void mouseClicked( final MouseEvent e ) + { + final boolean shouldConsume = hasMatchingBehaviour( e ); + delegate.mouseClicked( e ); + if ( shouldConsume ) + e.consume(); + } + + @Override + public void mousePressed( final MouseEvent e ) + { + final boolean shouldConsume = hasMatchingBehaviour( e ); + delegate.mousePressed( e ); + if ( shouldConsume ) + e.consume(); + } + + @Override + public void mouseReleased( final MouseEvent e ) + { + final boolean shouldConsume = hasMatchingBehaviour( e ); + delegate.mouseReleased( e ); + if ( shouldConsume ) + e.consume(); + } + + @Override + public void mouseEntered( final MouseEvent e ) + { + delegate.mouseEntered( e ); + } + + @Override + public void mouseExited( final MouseEvent e ) + { + delegate.mouseExited( e ); + } + + @Override + public void mouseDragged( final MouseEvent e ) + { + final boolean shouldConsume = hasMatchingBehaviour( e ); + delegate.mouseDragged( e ); + if ( shouldConsume ) + e.consume(); + } + + @Override + public void mouseMoved( final MouseEvent e ) + { + // Hover coordinates pass straight through unconsumed to ensure + // legacy UI refreshes continue + delegate.mouseMoved( e ); + } + + @Override + public void mouseWheelMoved( final MouseWheelEvent e ) + { + final boolean shouldConsume = hasMatchingBehaviour( e ); + delegate.mouseWheelMoved( e ); + if ( shouldConsume ) + e.consume(); + } + + @Override + public void keyPressed( final KeyEvent e ) + { + final boolean shouldConsume = hasMatchingBehaviour( e ); + delegate.keyPressed( e ); + if ( shouldConsume ) + e.consume(); + } + + @Override + public void keyReleased( final KeyEvent e ) + { + final boolean shouldConsume = hasMatchingBehaviour( e ); + delegate.keyReleased( e ); + if ( shouldConsume ) + e.consume(); + } + + @Override + public void keyTyped( final KeyEvent e ) + { + final boolean shouldConsume = hasMatchingBehaviour( e ); + delegate.keyTyped( e ); + if ( shouldConsume ) + e.consume(); + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/LinkSpotsBehaviour.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/LinkSpotsBehaviour.java new file mode 100644 index 000000000..6bda614f1 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/LinkSpotsBehaviour.java @@ -0,0 +1,477 @@ +package fiji.plugin.trackmate.visualization.hyperstack.behaviours; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.geom.Path2D; + +import org.scijava.ui.behaviour.DragBehaviour; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.Spot.SpotVisitor; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.SpotRoi; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.visualization.hyperstack.PaintSpotMesh; +import fiji.plugin.trackmate.visualization.hyperstack.PaintSpotRoi; +import fiji.plugin.trackmate.visualization.hyperstack.PaintSpotSphere; +import ij.ImagePlus; +import ij.gui.Roi; +import net.imglib2.KDTree; +import net.imglib2.RealLocalizable; +import net.imglib2.neighborsearch.NearestNeighborSearchOnKDTree; + +public class LinkSpotsBehaviour extends AbstractSpotEditBehaviour implements DragBehaviour +{ + + private static final String OVERLAY_NAME = "LinkSpotActionOverlay"; + + protected final boolean backward; + + protected Spot source; + + protected Spot target; + + protected NearestNeighborSearchOnKDTree< Spot > search; + + protected final LinkSpotsOverlay overlay; + + public LinkSpotsBehaviour( final Model model, final ImagePlus imp, final boolean backward ) + { + super( model, imp ); + this.backward = backward; + this.overlay = new LinkSpotsOverlay( imp ); + } + + @Override + public void init( final int x, final int y ) + { + if ( source != null ) + return; + + final RealLocalizable pos = toWorldCoords( x, y ); + final Spot spot = getSpotAtMouseLocation( pos ); + if ( spot == null ) + return; + + // Cannot link if at first frame and backward, etc. + final int frame = spot.getFeature( Spot.FRAME ).intValue(); + if ( backward && frame == 0 ) + return; + if ( !backward && frame == imp.getNFrames() - 1 ) + return; + + // Keep track of the source. + this.source = spot; + overlay.source = source; + overlay.sourcePixelPos[ 0 ] = x; + overlay.sourcePixelPos[ 1 ] = y; + + // Move to next frame if forward, previous frame if backward. + final int targetFrame = backward ? frame - 1 : frame + 1; + + // Build KD-tree of target frame spots. + final Iterable< Spot > targetSpots = model.getSpots().iterable( targetFrame, true ); + final int nTargetSpots = model.getSpots().getNSpots( targetFrame, true ); + final KDTree< Spot > tree = new KDTree< Spot >( nTargetSpots, targetSpots, targetSpots ); + this.search = new NearestNeighborSearchOnKDTree<>( tree ); + + overlay.targetPixelPos[ 0 ] = x; + overlay.targetPixelPos[ 1 ] = y; + imp.setT( targetFrame + 1 ); + imp.getOverlay().add( overlay, OVERLAY_NAME ); + imp.updateAndDraw(); + } + + @Override + public void drag( final int x, final int y ) + { + search.search( toWorldCoords( x, y ) ); + final Spot spot = search.getSampler().get(); + final double r = spot.getFeature( Spot.RADIUS ); + if ( search.getSquareDistance() < r * r ) + { + target = spot; + final RealLocalizable screenPos = toScreenCoords( target ); + overlay.targetPixelPos[ 0 ] = ( int ) Math.round( screenPos.getDoublePosition( 0 ) ); + overlay.targetPixelPos[ 1 ] = ( int ) Math.round( screenPos.getDoublePosition( 1 ) ); + + // Is there an existing link between source and target? + overlay.crossedLine.crossed = model.getTrackModel().containsEdge( source, target ); + } + else + { + target = null; + overlay.targetPixelPos[ 0 ] = x; + overlay.targetPixelPos[ 1 ] = y; + overlay.crossedLine.crossed = false; + } + overlay.target = target; + imp.updateAndDraw(); + } + + @Override + public void end( final int x, final int y ) + { + try + { + if ( target == null ) + return; + + model.beginUpdate(); + try + { + // Add or remove? + if ( model.getTrackModel().containsEdge( source, target ) ) + { + model.removeEdge( source, target ); + } + else + { + if ( backward ) + model.addEdge( target, source, -1 ); + else + model.addEdge( source, target, -1 ); + } + } + finally + { + model.endUpdate(); + } + } + finally + { + source = null; + target = null; + search = null; + overlay.source = null; + overlay.target = null; + imp.updateAndDraw(); + imp.getOverlay().remove( OVERLAY_NAME ); + } + } + + class LinkSpotsOverlay extends Roi + { + + private static final long serialVersionUID = 1L; + + private static final Stroke sourceStroke = new BasicStroke( 2f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10.0f, new float[] { 5f, 5f }, 0.0f ); + + private static final Stroke targetStroke = new BasicStroke( 2f ); + + Spot source; + + final int[] sourcePixelPos = new int[ 2 ]; + + Spot target; + + final int[] targetPixelPos = new int[ 2 ]; + + private final ArrowShape arrow = new ArrowShape(); + + final CrossedLineShape crossedLine = new CrossedLineShape(); + + private final SpotPainter painter; + + public LinkSpotsOverlay( final ImagePlus imp ) + { + super( 0, 0, imp ); + this.painter = new SpotPainter(); + } + + @Override + public void drawOverlay( final Graphics g ) + { + final Graphics2D g2d = ( Graphics2D ) g; + g2d.setColor( Color.WHITE ); + painter.setGraphics( g2d ); + + if ( source != null ) + { + // Source outline + g2d.setStroke( sourceStroke ); + source.accept( painter ); + + // Arrow to current pos. + if ( backward ) + { + arrow.x1d = targetPixelPos[ 0 ]; + arrow.y1d = targetPixelPos[ 1 ]; + arrow.x2d = sourcePixelPos[ 0 ]; + arrow.y2d = sourcePixelPos[ 1 ]; + } + else + { + arrow.x1d = sourcePixelPos[ 0 ]; + arrow.y1d = sourcePixelPos[ 1 ]; + arrow.x2d = targetPixelPos[ 0 ]; + arrow.y2d = targetPixelPos[ 1 ]; + } + crossedLine.x1d = arrow.x1d; + crossedLine.y1d = arrow.y1d; + crossedLine.x2d = arrow.x2d; + crossedLine.y2d = arrow.y2d; + + g2d.setStroke( targetStroke ); + g2d.draw( crossedLine.getPath() ); + if ( !crossedLine.crossed ) + g2d.fill( arrow.getPath() ); + } + + // Target outline + g2d.setStroke( targetStroke ); + if ( target != null ) + target.accept( painter ); + } + + private class SpotPainter implements SpotVisitor + { + + private final PaintSpotRoi paintSpotRoi; + + private final PaintSpotSphere paintSpotSphere; + + private final PaintSpotMesh paintSpotMesh; + + private final DisplaySettings displaySettings; + + private Graphics2D g2d; + + public SpotPainter() + { + this.displaySettings = DisplaySettings.defaultStyle().copy( "Link spot overlay settings" ); + displaySettings.setSpotUniformColor( Color.WHITE ); + displaySettings.setSpotShowName( true ); + displaySettings.setLineThickness( 2. ); + this.paintSpotSphere = new PaintSpotSphere( imp, calibration, displaySettings ); + this.paintSpotRoi = new PaintSpotRoi( imp, calibration, displaySettings ); + this.paintSpotMesh = new PaintSpotMesh( imp, calibration, displaySettings ); + } + + private void setGraphics( final Graphics2D g2d ) + { + this.g2d = g2d; + } + + @Override + public void visit( final SpotBase spot ) + { + paintSpotSphere.paint( g2d, spot ); + } + + @Override + public void visit( final SpotRoi spot ) + { + paintSpotRoi.paint( g2d, spot ); + } + + @Override + public void visit( final SpotMesh spot ) + { + paintSpotMesh.paint( g2d, spot ); + } + } + } + + /** + * Adapted from ImageJ, but the code below was adapted from mine: "Based on + * the method with the same name in Fiji's Arrow plugin, written by + * Jean-Yves Tinevez and Johannes Schindelin." + */ + private static class ArrowShape + { + + public static final int FILLED = 0, NOTCHED = 1, OPEN = 2, HEADLESS = 3, BAR = 4; + + private static final int style = FILLED; + + private static final boolean outline = false; + + private final double headSize = 10; // 0-30 + + private final double[] points = new double[ 2 * 5 ]; + + private double x1d, y1d, x2d, y2d; + + private final Path2D.Double path = new Path2D.Double(); + + @SuppressWarnings( "unused" ) + private void calculatePoints() + { + double tip = 0.0; + double base; + final double shaftWidth = 1.; + double length = 8 + 10 * shaftWidth * 0.5; + length = length * ( headSize / 10.0 ); + length -= shaftWidth * 1.42; + if ( style == NOTCHED ) + length *= 0.74; + if ( style == OPEN ) + length *= 1.32; + if ( length < 0.0 || style == HEADLESS ) + length = 0.0; + + double dx = x2d - x1d, dy = y2d - y1d; + final double arrowLength = Math.sqrt( dx * dx + dy * dy ); + dx = dx / arrowLength; + dy = dy / arrowLength; + if ( style != HEADLESS ) + { + points[ 0 ] = ( float ) ( x1d + dx * shaftWidth * 2.0 ); + points[ 1 ] = ( float ) ( y1d + dy * shaftWidth * 2.0 ); + } + else + { + points[ 0 ] = ( float ) x1d; + points[ 1 ] = ( float ) y1d; + } + if ( length > 0 ) + { + final double factor = style == OPEN ? 1.3 : 1.42; + points[ 2 * 3 ] = ( float ) ( x2d - dx * shaftWidth * factor ); + points[ 2 * 3 + 1 ] = ( float ) ( y2d - dy * shaftWidth * factor ); + if ( style == BAR ) + { + points[ 2 * 3 ] = ( float ) ( x2d - dx * shaftWidth * 0.5 ); + points[ 2 * 3 + 1 ] = ( float ) ( y2d - dy * shaftWidth * 0.5 ); + } + } + else + { + points[ 2 * 3 ] = ( float ) x2d; + points[ 2 * 3 + 1 ] = ( float ) y2d; + } + final double alpha = Math.atan2( points[ 2 * 3 + 1 ] - points[ 1 ], points[ 2 * 3 ] - points[ 0 ] ); + double SL = 0.0; + switch ( style ) + { + case FILLED: + case HEADLESS: + tip = Math.toRadians( 20.0 ); + base = Math.toRadians( 90.0 ); + points[ 1 * 2 ] = ( float ) ( points[ 2 * 3 ] - length * Math.cos( alpha ) ); + points[ 1 * 2 + 1 ] = ( float ) ( points[ 2 * 3 + 1 ] - length * Math.sin( alpha ) ); + SL = length * Math.sin( base ) / Math.sin( base + tip );; + break; + case NOTCHED: + tip = Math.toRadians( 20 ); + base = Math.toRadians( 120 ); + points[ 1 * 2 ] = ( float ) ( points[ 2 * 3 ] - length * Math.cos( alpha ) ); + points[ 1 * 2 + 1 ] = ( float ) ( points[ 2 * 3 + 1 ] - length * Math.sin( alpha ) ); + SL = length * Math.sin( base ) / Math.sin( base + tip );; + break; + case OPEN: + tip = Math.toRadians( 25 ); // 30 + points[ 1 * 2 ] = points[ 2 * 3 ]; + points[ 1 * 2 + 1 ] = points[ 2 * 3 + 1 ]; + SL = length; + break; + case BAR: + tip = Math.toRadians( 90 ); // 30 + points[ 1 * 2 ] = points[ 2 * 3 ]; + points[ 1 * 2 + 1 ] = points[ 2 * 3 + 1 ]; + SL = length; + break; + } + // P2 = P3 - SL*alpha+tip + points[ 2 * 2 ] = ( float ) ( points[ 2 * 3 ] - SL * Math.cos( alpha + tip ) ); + points[ 2 * 2 + 1 ] = ( float ) ( points[ 2 * 3 + 1 ] - SL * Math.sin( alpha + tip ) ); + // P4 = P3 - SL*alpha-tip + points[ 2 * 4 ] = ( float ) ( points[ 2 * 3 ] - SL * Math.cos( alpha - tip ) ); + points[ 2 * 4 + 1 ] = ( float ) ( points[ 2 * 3 + 1 ] - SL * Math.sin( alpha - tip ) ); + } + + @SuppressWarnings( "unused" ) + private Shape getPath() + { + path.reset(); + calculatePoints(); + final double tailx = points[ 0 ]; + final double taily = points[ 1 ]; + final double headbackx = points[ 2 * 1 ]; + final double headbacky = points[ 2 * 1 + 1 ]; + final double headtipx = points[ 2 * 3 ]; + final double headtipy = points[ 2 * 3 + 1 ]; + if ( outline ) + { + double dx = headtipx - tailx; + double dy = headtipy - taily; + final double shaftLength = Math.sqrt( dx * dx + dy * dy ); + dx = headtipx - headbackx; + dy = headtipy - headbacky; + final double headLength = Math.sqrt( dx * dx + dy * dy ); + } + path.moveTo( tailx, taily ); // tail + path.lineTo( headbackx, headbacky ); // head back + path.moveTo( headbackx, headbacky ); // head back + if ( style == OPEN ) + path.moveTo( points[ 2 * 2 ], points[ 2 * 2 + 1 ] ); + else + path.lineTo( points[ 2 * 2 ], points[ 2 * 2 + 1 ] ); + path.lineTo( headtipx, headtipy ); // head tip + path.lineTo( points[ 2 * 4 ], points[ 2 * 4 + 1 ] ); // right point + path.lineTo( headbackx, headbacky ); // back to the head back + return path; + } + } + + static class CrossedLineShape + { + + private final Path2D.Double path = new Path2D.Double( Path2D.WIND_NON_ZERO ); + + private final double spacing = 20.; + + private final double crossSize = 10.; + + private double x1d, y1d, x2d, y2d; + + boolean crossed = false; + + private Path2D.Double getPath() + { + path.reset(); + path.moveTo( x1d, y1d ); + path.lineTo( x2d, y2d ); + + if ( !crossed ) + return path; + + final double dx = x2d - x1d; + final double dy = y2d - y1d; + final double lineLength = Math.hypot( dx, dy ); + if ( lineLength < spacing ) + return path; + + final double ux = dx / lineLength; + final double uy = dy / lineLength; + final double px = -uy; + final double py = ux; + + final double scale = ( crossSize / 2.0 ) * Math.cos( Math.toRadians( 45 ) ); + final double d1x = ( ux + px ) * scale; + final double d1y = ( uy + py ) * scale; + final double d2x = ( ux - px ) * scale; + final double d2y = ( uy - py ) * scale; + + for ( double d = 0; d <= lineLength; d += spacing ) + { + final double cx = x1d + d * ux; + final double cy = y1d + d * uy; + + path.moveTo( cx - d1x, cy - d1y ); + path.lineTo( cx + d1x, cy + d1y ); + + path.moveTo( cx - d2x, cy - d2y ); + path.lineTo( cx + d2x, cy + d2y ); + } + return path; + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/SelectSpotsWithRoiListener.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/SelectSpotsWithRoiListener.java new file mode 100644 index 000000000..2d411ee21 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/SelectSpotsWithRoiListener.java @@ -0,0 +1,97 @@ +package fiji.plugin.trackmate.visualization.hyperstack.behaviours; + +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.List; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.util.TMUtils; +import ij.ImagePlus; +import ij.gui.Roi; +import ij.gui.RoiListener; +import ij.plugin.PlugIn; + +public class SelectSpotsWithRoiListener implements PlugIn, RoiListener +{ + + private final Model model; + + private final SelectionModel selectionModel; + + private final ImagePlus sourceImp; + + private final double[] calibration; + + public SelectSpotsWithRoiListener( final Model model, final SelectionModel selectionModel, final ImagePlus imp ) + { + this.model = model; + this.selectionModel = selectionModel; + this.sourceImp = imp; + this.calibration = TMUtils.getSpatialCalibration( sourceImp ); + + // De-register the listener when the source image is closed + sourceImp.getWindow().addWindowListener( new WindowAdapter() + { + @Override + public void windowClosing( final WindowEvent e ) + { + Roi.removeRoiListener( SelectSpotsWithRoiListener.this ); + } + } ); + } + + @Override + public void run( final String arg ) + { + Roi.addRoiListener( this ); + } + + /* + * NOTE: For some reason this does not work as expected. For instance, the + * rectangular ROI does not fire a RoiListener.COMPLETED when the user + * finishes drawing it. The PolygonRoi does fire this event, but the + * containsPoint() method does not work. So in effect, this feature only + * works with the FreehandRoi. + */ + @Override + public void roiModified( final ImagePlus imp, final int id ) + { + if ( imp != sourceImp || id != RoiListener.COMPLETED ) + return; + + final Roi roi = imp.getRoi(); + if ( roi == null ) + return; + + final List< Spot > spotsInRoi = new ArrayList<>(); + + final Iterable iterable; + final boolean isShiftDown = ( imp.getCanvas().getModifiers() & 1 ) != 0; + if ( isShiftDown ) + { + final int frame = imp.getT() - 1; + iterable = model.getSpots().iterable( frame, true ); + } + else + { + iterable = model.getSpots().iterable( true ); + } + + for ( final Spot spot : iterable ) + { + final double x = spot.getDoublePosition( 0 ) / calibration[ 0 ]; + final double y = spot.getDoublePosition( 1 ) / calibration[ 1 ]; + if ( roi.containsPoint( x, y ) ) + spotsInRoi.add( spot ); + } + selectionModel.addSpotToSelection( spotsInRoi ); + } + + public static void install( final Model model, final SelectionModel selectionModel, final ImagePlus imp ) + { + new SelectSpotsWithRoiListener( model, selectionModel, imp ).run( null ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/SpotEditBehaviours.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/SpotEditBehaviours.java new file mode 100644 index 000000000..21c0ecdd5 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/SpotEditBehaviours.java @@ -0,0 +1,370 @@ +package fiji.plugin.trackmate.visualization.hyperstack.behaviours; + +import java.util.Set; + +import org.scijava.plugin.Plugin; +import org.scijava.ui.behaviour.ClickBehaviour; +import org.scijava.ui.behaviour.DragBehaviour; +import org.scijava.ui.behaviour.io.gui.CommandDescriptionProvider; +import org.scijava.ui.behaviour.io.gui.CommandDescriptions; +import org.scijava.ui.behaviour.util.Behaviours; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.visualization.ui.KeyConfigContexts; +import ij.ImagePlus; +import net.imglib2.RealLocalizable; +import net.imglib2.util.Util; + +public class SpotEditBehaviours +{ + + private static final String MOVE_SPOT = "move spot"; + private static final String INCREASE_SPOT_RADIUS = "increase spot radius"; + private static final String INCREASE_SPOT_RADIUS_FAST = "increase spot radius fast"; + private static final String DECREASE_SPOT_RADIUS = "decrease spot radius"; + private static final String DECREASE_SPOT_RADIUS_FAST = "decrease spot radius fast"; + private static final String ADD_SPOT = "add spot"; + private static final String DELETE_SPOT = "delete spot"; + private static final String LINK_SPOTS = "link spots"; + private static final String LINK_SPOTS_BACKWARD = "link spots backward"; + private static final String ADD_AND_LINK_SPOTS_FORWARD = "add and link spots forward"; + private static final String ADD_AND_LINK_SPOTS_BACKWARD = "add and link spots backward"; + private static final String CLICK_SELECT_SPOT = "click select spot"; + private static final String CLICK_SELECT_ADD_SPOT = "click select add spot"; + + private static final String[] MOVE_SPOT_KEYS = new String[] { "SPACE" }; + private static final String[] INCREASE_SPOT_RADIUS_KEYS = new String[] { "E" }; + private static final String[] INCREASE_SPOT_RADIUS_FAST_KEYS = new String[] { "shift E" }; + private static final String[] DECREASE_SPOT_RADIUS_KEYS = new String[] { "Q" }; + private static final String[] DECREASE_SPOT_RADIUS_FAST_KEYS = new String[] { "shift Q" }; + private static final String[] ADD_SPOT_KEYS = new String[] { "not mapped" }; + private static final String[] DELETE_SPOT_KEYS = new String[] { "D" }; + private static final String[] LINK_SPOTS_KEYS = new String[] { "L" }; + private static final String[] LINK_SPOTS_BACKWARD_KEYS = new String[] { "shift L" }; + private static final String[] ADD_AND_LINK_SPOTS_FORWARD_KEYS = new String[] { "A" }; + private static final String[] ADD_AND_LINK_SPOTS_BACKWARD_KEYS = new String[] { "shift A" }; + private static final String[] CLICK_SELECT_SPOT_KEYS = new String[] { "button1" }; + private static final String[] CLICK_SELECT_ADD_SPOT_KEYS = new String[] { "shift button1" }; + + static boolean autoLinkingmode = false; + + public static final void install( final Behaviours behaviours, final Model model, final SelectionModel selectionModel, final ImagePlus imp ) + { + behaviours.behaviour( new MoveSpotBehaviour( model, imp ), MOVE_SPOT, MOVE_SPOT_KEYS ); + + behaviours.behaviour( new ResizeSpotBehaviour( model, imp, true, false ), INCREASE_SPOT_RADIUS, INCREASE_SPOT_RADIUS_KEYS ); + behaviours.behaviour( new ResizeSpotBehaviour( model, imp, true, true ), INCREASE_SPOT_RADIUS_FAST, INCREASE_SPOT_RADIUS_FAST_KEYS ); + behaviours.behaviour( new ResizeSpotBehaviour( model, imp, false, false ), DECREASE_SPOT_RADIUS, DECREASE_SPOT_RADIUS_KEYS ); + behaviours.behaviour( new ResizeSpotBehaviour( model, imp, false, true ), DECREASE_SPOT_RADIUS_FAST, DECREASE_SPOT_RADIUS_FAST_KEYS ); + + behaviours.behaviour( new AddSpotBehaviour( model, selectionModel, imp ), ADD_SPOT, ADD_SPOT_KEYS ); + behaviours.behaviour( new DeleteSpotBehaviour( model, selectionModel, imp ), DELETE_SPOT, DELETE_SPOT_KEYS ); + + behaviours.behaviour( new LinkSpotsBehaviour( model, imp, false ), LINK_SPOTS, LINK_SPOTS_KEYS ); + behaviours.behaviour( new LinkSpotsBehaviour( model, imp, true ), LINK_SPOTS_BACKWARD, LINK_SPOTS_BACKWARD_KEYS ); + + behaviours.behaviour( new AddAndLinkSpotBehaviour( model, imp, false ), ADD_AND_LINK_SPOTS_FORWARD, ADD_AND_LINK_SPOTS_FORWARD_KEYS ); + behaviours.behaviour( new AddAndLinkSpotBehaviour( model, imp, true ), ADD_AND_LINK_SPOTS_BACKWARD, ADD_AND_LINK_SPOTS_BACKWARD_KEYS ); + + behaviours.behaviour( new ClickSelectSpotBehaviour( model, selectionModel, imp ), CLICK_SELECT_SPOT, CLICK_SELECT_SPOT_KEYS ); + behaviours.behaviour( new ClickSelectAddSpotBehaviour( model, selectionModel, imp ), CLICK_SELECT_ADD_SPOT, CLICK_SELECT_ADD_SPOT_KEYS ); + } + + private static class ClickSelectAddSpotBehaviour extends AbstractSpotEditBehaviour implements ClickBehaviour + { + + private final SelectionModel selectionModel; + + public ClickSelectAddSpotBehaviour( final Model model, final SelectionModel selectionModel, final ImagePlus imp ) + { + super( model, imp ); + this.selectionModel = selectionModel; + } + + @Override + public void click( final int x, final int y ) + { + final RealLocalizable pos = toWorldCoords( x, y ); + final Spot target = getSpotAtMouseLocation( pos ); + if ( null == target ) + return; + if ( selectionModel.getSpotSelection().contains( target ) ) + selectionModel.removeSpotFromSelection( target ); + else + selectionModel.addSpotToSelection( target ); + } + } + + private static class ClickSelectSpotBehaviour extends AbstractSpotEditBehaviour implements ClickBehaviour + { + + private final SelectionModel selectionModel; + + public ClickSelectSpotBehaviour( final Model model, final SelectionModel selectionModel, final ImagePlus imp ) + { + super( model, imp ); + this.selectionModel = selectionModel; + } + + @Override + public void click( final int x, final int y ) + { + selectionModel.clearSelection(); + final RealLocalizable pos = toWorldCoords( x, y ); + final Spot target = getSpotAtMouseLocation( pos ); + if ( null == target ) + return; + selectionModel.addSpotToSelection( target ); + } + } + + private static class DeleteSpotBehaviour extends AbstractSpotEditBehaviour implements ClickBehaviour + { + + private final SelectionModel selectionModel; + + public DeleteSpotBehaviour( final Model model, final SelectionModel selectionModel, final ImagePlus imp ) + { + super( model, imp ); + this.selectionModel = selectionModel; + } + + @Override + public void click( final int x, final int y ) + { + final RealLocalizable pos = toWorldCoords( x, y ); + final Spot target = getSpotAtMouseLocation( pos ); + if ( null == target ) + return; + + selectionModel.removeSpotFromSelection( target ); + model.beginUpdate(); + try + { + model.removeSpot( target ); + } + finally + { + model.endUpdate(); + } + } + } + + private static class AddSpotBehaviour extends AbstractSpotEditBehaviour implements DragBehaviour + { + + private final SelectionModel selectionModel; + + private SpotBase newSpot; + + public AddSpotBehaviour( final Model model, final SelectionModel selectionModel, final ImagePlus imp ) + { + super( model, imp ); + this.selectionModel = selectionModel; + } + + @Override + public void init( final int x, final int y ) + { + if ( null != newSpot ) + return; + + final RealLocalizable pos = toWorldCoords( x, y ); + // Forbid adding a spot if there is already one at this location. + if ( getSpotAtMouseLocation( pos ) != null ) + return; + + final double radius = ResizeSpotBehaviour.previousRadius; + this.newSpot = new SpotBase( pos, radius, -1. ); + + final double dt = imp.getCalibration().frameInterval; + final int frame = imp.getFrame() - 1; + newSpot.putFeature( Spot.POSITION_T, frame * dt ); + newSpot.putFeature( Spot.FRAME, Double.valueOf( frame ) ); + + model.beginUpdate(); + model.addSpotTo( newSpot, frame ); + + /* + * If we are in auto-link mode, we create an edge with spot in + * selection, if there is just one and if it is in a previous frame + */ + if ( autoLinkingmode ) + { + final Set< Spot > spotSelection = selectionModel.getSpotSelection(); + if ( spotSelection.size() == 1 ) + { + final Spot source = spotSelection.iterator().next(); + if ( newSpot.diffTo( source, Spot.FRAME ) != 0 ) + model.addEdge( source, newSpot, -1 ); + } + selectionModel.clearSpotSelection(); + selectionModel.addSpotToSelection( newSpot ); + } + imp.updateAndDraw(); + } + + @Override + public void drag( final int x, final int y ) + { + final RealLocalizable pos = toWorldCoords( x, y ); + newSpot.setPosition( pos.getDoublePosition( 0 ), 0 ); + newSpot.setPosition( pos.getDoublePosition( 1 ), 1 ); + imp.updateAndDraw(); + System.out.println( Util.printCoordinates( newSpot ) ); + + } + + @Override + public void end( final int x, final int y ) + { + model.endUpdate(); + newSpot = null; + imp.updateAndDraw(); + } + } + + private static class MoveSpotBehaviour extends AbstractSpotEditBehaviour implements DragBehaviour + { + + /** + * Offset between mouse click and spot center, in world coordinates. + */ + private final double[] delta = new double[ 2 ]; + + private Spot movedSpot; + + public MoveSpotBehaviour( final Model model, final ImagePlus imp ) + { + super( model, imp ); + } + + @Override + public void init( final int x, final int y ) + { + if ( null != movedSpot ) + return; + final RealLocalizable pos = toWorldCoords( x, y ); + movedSpot = getSpotAtMouseLocation( pos ); + if ( null == movedSpot ) + return; + model.beginUpdate(); + model.beforeEdit( movedSpot ); + delta[ 0 ] = movedSpot.getDoublePosition( 0 ) - pos.getDoublePosition( 0 ); + delta[ 1 ] = movedSpot.getDoublePosition( 1 ) - pos.getDoublePosition( 1 ); + } + + @Override + public void drag( final int x, final int y ) + { + final RealLocalizable pos = toWorldCoords( x, y ); + movedSpot.setPosition( pos.getDoublePosition( 0 ) + delta[ 0 ], 0 ); + movedSpot.setPosition( pos.getDoublePosition( 1 ) + delta[ 1 ], 1 ); + imp.updateAndDraw(); + } + + @Override + public void end( final int x, final int y ) + { + model.endUpdate(); + movedSpot = null; + imp.updateAndDraw(); + } + } + + static class ResizeSpotBehaviour extends AbstractSpotEditBehaviour implements ClickBehaviour + { + + /** + * Fall back default radius when the settings does not give a default + * radius to use. + */ + private static final double FALL_BACK_RADIUS = 5.; + + private static final double COARSE_STEP = 2; + + private static final double FINE_STEP = 0.2f; + + private final boolean increase; + + private final boolean fast; + + /** The previous radius to be used for spot creation. */ + static double previousRadius = FALL_BACK_RADIUS; + + public ResizeSpotBehaviour( final Model model, final ImagePlus imp, final boolean increase, final boolean fast ) + { + super( model, imp ); + this.increase = increase; + this.fast = fast; + } + + @Override + public void click( final int x, final int y ) + { + final Spot spot = getSpotAtMouseLocation( toWorldCoords( x, y ) ); + if ( null == spot ) + return; + + // Compute new radius. + final double radius = spot.getFeature( Spot.RADIUS ); + final int factor = ( increase ) ? -1 : 1; + final double dx = imp.getCalibration().pixelWidth; + + final double newRadius = ( fast ) + ? radius + factor * dx * COARSE_STEP + : radius + factor * dx * FINE_STEP; + + if ( newRadius <= dx ) + return; + + // Actually scale the spot. + model.beginUpdate(); + try + { + model.beforeEdit( spot ); + spot.scale( radius / newRadius ); + // Store new value of radius for next spot creation. + previousRadius = newRadius; + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + finally + { + model.endUpdate(); + } + } + } + + @Plugin( type = CommandDescriptionProvider.class ) + public static class Descriptions extends CommandDescriptionProvider + { + public Descriptions() + { + super( KeyConfigContexts.KEY_CONFIG_SCOPE, KeyConfigContexts.HYPERSTACK_DISPLAYER ); + } + + @Override + public void getCommandDescriptions( final CommandDescriptions descriptions ) + { + descriptions.add( MOVE_SPOT, MOVE_SPOT_KEYS, "Move a spot by dragging it." ); + descriptions.add( INCREASE_SPOT_RADIUS, INCREASE_SPOT_RADIUS_KEYS, "Increase the radius of a spot." ); + descriptions.add( INCREASE_SPOT_RADIUS_FAST, INCREASE_SPOT_RADIUS_FAST_KEYS, "Increase the radius of a spot (fast)." ); + descriptions.add( DECREASE_SPOT_RADIUS, DECREASE_SPOT_RADIUS_KEYS, "Decrease the radius of a spot." ); + descriptions.add( DECREASE_SPOT_RADIUS_FAST, DECREASE_SPOT_RADIUS_FAST_KEYS, "Decrease the radius of a spot (fast)." ); + descriptions.add( ADD_SPOT, ADD_SPOT_KEYS, "Add a new spot at the mouse location." ); + descriptions.add( DELETE_SPOT, DELETE_SPOT_KEYS, "Delete the spot at the mouse location." ); + descriptions.add( LINK_SPOTS, LINK_SPOTS_KEYS, "Link two spots forward in time by dragging from a source spot to a target spot in the next time-point." ); + descriptions.add( LINK_SPOTS_BACKWARD, LINK_SPOTS_BACKWARD_KEYS, "Link two spots backward in time by dragging from a source spot to a target spot in the previous time-point." ); + descriptions.add( CLICK_SELECT_SPOT, CLICK_SELECT_SPOT_KEYS, "Select a spot at the mouse location." ); + descriptions.add( CLICK_SELECT_ADD_SPOT, CLICK_SELECT_ADD_SPOT_KEYS, "Add or remove a spot from the selection at the mouse location." ); + descriptions.add( ADD_AND_LINK_SPOTS_FORWARD, ADD_AND_LINK_SPOTS_FORWARD_KEYS, "Add a new spot at the mouse location or link the source spot to a new target spot in the next time-point." ); + descriptions.add( ADD_AND_LINK_SPOTS_BACKWARD, ADD_AND_LINK_SPOTS_BACKWARD_KEYS, "Add a new spot at the mouse location or link the source spot to a new target spot in the previous time-point." ); + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/semiautotracking/SemiAutoTracking.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/semiautotracking/SemiAutoTracking.java new file mode 100644 index 000000000..1e1ca0a79 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/semiautotracking/SemiAutoTracking.java @@ -0,0 +1,44 @@ +package fiji.plugin.trackmate.visualization.hyperstack.behaviours.semiautotracking; + +import fiji.plugin.trackmate.Logger; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.detection.semiauto.SemiAutoTracker; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.util.Threads; +import ij.ImagePlus; +import ij.Prefs; + +public class SemiAutoTracking implements Runnable +{ + + private final Logger logger = Logger.IJ_LOGGER; + + private final GuiModel guiModel; + + public SemiAutoTracking( final GuiModel guiModel ) + { + this.guiModel = guiModel; + } + + @Override + public void run() + { + final SemiAutoTrackingParams params = guiModel.getSemiAutoTrackingParams(); + final double qualityThreshold = params.qualityThreshold(); + final double distanceTolerance = params.distanceTolerance(); + final int nFrames = params.nFrames(); + + final Model model = guiModel.getModel(); + final SelectionModel selectionModel = guiModel.getSelectionModel(); + final ImagePlus imp = guiModel.getSettings().imp; + final SemiAutoTracker< ? > autotracker = new SemiAutoTracker<>( model, selectionModel, imp, logger ); + autotracker.setParameters( qualityThreshold, distanceTolerance, nFrames ); + autotracker.setNumThreads( Prefs.getThreads() / 2 ); + Threads.run( "TrackMate semi-automated tracking thread", () -> { + final boolean ok = autotracker.checkInput() && autotracker.process(); + if ( !ok ) + logger.error( autotracker.getErrorMessage() ); + } ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/semiautotracking/SemiAutoTrackingParams.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/semiautotracking/SemiAutoTrackingParams.java new file mode 100644 index 000000000..3cce74765 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/semiautotracking/SemiAutoTrackingParams.java @@ -0,0 +1,112 @@ +package fiji.plugin.trackmate.visualization.hyperstack.behaviours.semiautotracking; + +import org.scijava.listeners.Listeners; +import org.scijava.ui.config.Configurator; +import org.scijava.ui.config.Parameters.DoubleParam; +import org.scijava.ui.config.Parameters.IntParam; +import org.scijava.ui.config.Parameters.UpdateListener; + +public class SemiAutoTrackingParams extends Configurator +{ + + private final DoubleParam qualityThreshold; + + private final DoubleParam distanceTolerance; + + private final IntParam nFrames; + + private final IntParam stepwiseTimeBrowsing; + + private final transient Listeners.SynchronizedList< UpdateListener > updateListeners; + + public SemiAutoTrackingParams() + { + super( "Semi-automatic tracking parameters", "Parameters that configures the semi-automatic tracking tool." ); + this.updateListeners = new Listeners.SynchronizedList<>(); + final UpdateListener updateListener = () -> notifyUpdateListeners(); + + this.qualityThreshold = addDoubleParameter() + .key( "QUALITY_THRESHOLD" ) + .name( "Quality threshold" ) + .help( "The fraction of the quality of the initial spot above which we keep new spots. The highest, the more intolerant." ) + .defaultValue( 0.5d ) + .min( 0. ) + .max( 2. ) + .updateListener( updateListener ) + .get(); + + this.distanceTolerance = addDoubleParameter() + .key( "DISTANCE_TOLERANCE" ) + .name( "Distance tolerance" ) + .help( "How close must be the new spot found to be accepted, in units of the radius of the initial spot." ) + .defaultValue( 2d ) + .min( 0. ) + .max( 10. ) + .updateListener( updateListener ) + .get(); + + this.nFrames = addIntParameter() + .key( "N_FRAMES" ) + .name( "Max N frames" ) + .help( "The number of frames to process in one go. Set to 0 to have no bounds." ) + .defaultValue( 10 ) + .min( 0 ) + .updateListener( updateListener ) + .get(); + + this.stepwiseTimeBrowsing = addIntParameter() + .key( "STEPWISE_TIME_BROWSING" ) + .name( "Stepwise time browsing" ) + .help( "By how many frames to jump when we do step-wise time browsing." ) + .defaultValue( 1 ) + .min( 1 ) + .updateListener( updateListener ) + .get(); + } + + private void notifyUpdateListeners() + { + updateListeners.list.forEach( UpdateListener::parameterUpdated ); + } + + public Listeners< UpdateListener > updateListeners() + { + return updateListeners; + } + + public double qualityThreshold() + { + return qualityThreshold.getValue(); + } + + public double distanceTolerance() + { + return distanceTolerance.getValue(); + } + + public int nFrames() + { + return nFrames.getValue(); + } + + public int stepwiseTimeBrowsing() + { + return stepwiseTimeBrowsing.getValue(); + } + + public void set( final SemiAutoTrackingParams other ) + { + this.qualityThreshold.set( other.qualityThreshold() ); + this.distanceTolerance.set( other.distanceTolerance() ); + this.nFrames.set( other.nFrames() ); + this.stepwiseTimeBrowsing.set( other.stepwiseTimeBrowsing() ); + notifyUpdateListeners(); + } + + @Override + public String toString() + { + return String.format( "SemiAutoTrackingParams [qualityThreshold=%.2f, distanceTolerance=%.2f, nFrames=%d, stepwiseTimeBrowsing=%d]", + qualityThreshold(), distanceTolerance(), nFrames(), stepwiseTimeBrowsing() ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/semiautotracking/SemiAutoTrackingParamsIO.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/semiautotracking/SemiAutoTrackingParamsIO.java new file mode 100644 index 000000000..4690f67e5 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/semiautotracking/SemiAutoTrackingParamsIO.java @@ -0,0 +1,28 @@ +package fiji.plugin.trackmate.visualization.hyperstack.behaviours.semiautotracking; + +import java.io.File; + +import org.scijava.ui.config.visitors.JSon; + +public class SemiAutoTrackingParamsIO +{ + + private static File userDefaultFile = new File( new File( System.getProperty( "user.home" ), ".trackmate" ), "semiautotrackerparams.json" ); + + public static SemiAutoTrackingParams readPrefs() + { + final SemiAutoTrackingParams params = new SemiAutoTrackingParams(); + if ( !userDefaultFile.exists() ) + { + savePrefs( params ); + return params; + } + JSon.deserialize( userDefaultFile.getAbsolutePath(), params ); + return params; + } + + public static void savePrefs( final SemiAutoTrackingParams params ) + { + JSon.serialize( userDefaultFile.getAbsolutePath(), params ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/semiautotracking/SpotEditToolSettingsPage.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/semiautotracking/SpotEditToolSettingsPage.java new file mode 100644 index 000000000..54a08749e --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/behaviours/semiautotracking/SpotEditToolSettingsPage.java @@ -0,0 +1,101 @@ +package fiji.plugin.trackmate.visualization.hyperstack.behaviours.semiautotracking; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.util.ArrayList; + +import javax.swing.JButton; +import javax.swing.JPanel; + +import org.scijava.listeners.Listeners; +import org.scijava.ui.config.visitors.gui.GuiBuilder; +import org.scijava.ui.config.visitors.gui.GuiBuilder.ConfigPanel; + +import bdv.ui.settings.ModificationListener; +import bdv.ui.settings.SettingsPage; + +public class SpotEditToolSettingsPage implements SettingsPage +{ + + private final String treePath; + + private final Listeners.List< ModificationListener > modificationListeners; + + private final SemiAutoTrackingParams tmpParams; + + private final JPanel mainPanel; + + public SpotEditToolSettingsPage( final String treePath, final SemiAutoTrackingParams params ) + { + this.treePath = treePath; + this.modificationListeners = new Listeners.SynchronizedList<>(); + this.tmpParams = new SemiAutoTrackingParams(); + tmpParams.set( params ); + final ConfigPanel configPanel = GuiBuilder.build( tmpParams ); + tmpParams.updateListeners().add( () -> modificationListeners.list.forEach( ModificationListener::setModified ) ); + onApply( () -> { + params.set( tmpParams ); + SemiAutoTrackingParamsIO.savePrefs( params ); + } ); + onCancel( () -> { + tmpParams.set( params ); + configPanel.refresh(); + } ); + + this.mainPanel = new JPanel(); + mainPanel.setLayout( new BorderLayout() ); + mainPanel.add( configPanel, BorderLayout.CENTER ); + final JPanel buttonPanel = new JPanel( new FlowLayout( FlowLayout.RIGHT, 5, 5 ) ); + final JButton reset = new JButton( "Reset" ); + reset.addActionListener( e -> { + tmpParams.set( new SemiAutoTrackingParams() ); + configPanel.refresh(); + } ); + buttonPanel.add( reset ); + mainPanel.add( buttonPanel, BorderLayout.SOUTH ); + } + + @Override + public String getTreePath() + { + return treePath; + } + + @Override + public JPanel getJPanel() + { + return mainPanel; + } + + @Override + public Listeners< ModificationListener > modificationListeners() + { + return modificationListeners; + } + + protected final ArrayList< Runnable > runOnApply = new ArrayList<>(); + + public synchronized void onApply( final Runnable runnable ) + { + runOnApply.add( runnable ); + } + + protected final ArrayList< Runnable > runOnCancel = new ArrayList<>(); + + public synchronized void onCancel( final Runnable runnable ) + { + runOnCancel.add( runnable ); + } + + @Override + public void cancel() + { + runOnCancel.forEach( Runnable::run ); + } + + @Override + public void apply() + { + runOnApply.forEach( Runnable::run ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/table/AllSpotsTableView.java b/src/main/java/fiji/plugin/trackmate/visualization/table/AllSpotsTableView.java index 169c78b50..0332761d9 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/table/AllSpotsTableView.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/table/AllSpotsTableView.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 * . @@ -22,11 +22,10 @@ package fiji.plugin.trackmate.visualization.table; import static fiji.plugin.trackmate.gui.Icons.CSV_ICON; -import static fiji.plugin.trackmate.gui.Icons.TRACKMATE_ICON; import java.awt.BorderLayout; import java.awt.Color; -import java.awt.event.WindowAdapter; +import java.awt.Window; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -54,58 +53,58 @@ import fiji.plugin.trackmate.Dimension; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.ModelChangeEvent; -import fiji.plugin.trackmate.ModelChangeListener; import fiji.plugin.trackmate.SelectionChangeEvent; -import fiji.plugin.trackmate.SelectionChangeListener; import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.features.FeatureUtils; import fiji.plugin.trackmate.features.manual.ManualSpotColorAnalyzerFactory; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.UpdateListener; import fiji.plugin.trackmate.util.FileChooser; import fiji.plugin.trackmate.util.FileChooser.DialogType; import fiji.plugin.trackmate.util.FileChooser.SelectionMode; -import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.visualization.AbstractTrackMateModelJFrameView; import fiji.plugin.trackmate.visualization.FeatureColorGenerator; -import fiji.plugin.trackmate.visualization.TrackMateModelView; import fiji.plugin.trackmate.visualization.trackscheme.utils.SearchBar; -public class AllSpotsTableView extends JFrame implements TrackMateModelView, ModelChangeListener, SelectionChangeListener +public class AllSpotsTableView extends AbstractTrackMateModelJFrameView { - private static final long serialVersionUID = 1L; - private static final String KEY = "SPOT_TABLE"; private String selectedFile; - private final Model model; - private final TablePanel< Spot > spotTable; private final AtomicBoolean ignoreSelectionChange = new AtomicBoolean( false ); - private final SelectionModel selectionModel; + private final JFrame frame; - public AllSpotsTableView( final Model model, final SelectionModel selectionModel, final DisplaySettings ds, final String imageFileName ) + public AllSpotsTableView( final GuiModel guiModel, final String imageFileName ) { - super( "All spots table" ); - setIconImage( TRACKMATE_ICON.getImage() ); - this.model = model; - this.selectionModel = selectionModel; + super( guiModel ); this.selectedFile = imageFileName + "_allspots.csv"; /* * GUI. */ + // Frame. + this.frame = new JFrame( "All Spots Table" ); + frame.setIconImage( Icons.TRACKMATE_ICON.getImage() ); + + setWindow( frame ); + + // Main panel. final JPanel mainPanel = new JPanel(); mainPanel.setLayout( new BorderLayout() ); + frame.getContentPane().add( mainPanel ); // Table. + final Model model = guiModel.getModel(); + final DisplaySettings ds = guiModel.getDisplaySettings(); this.spotTable = createSpotTable( model, ds ); - mainPanel.add( spotTable.getPanel(), BorderLayout.CENTER ); // Tool bar. @@ -126,38 +125,20 @@ public AllSpotsTableView( final Model model, final SelectionModel selectionModel } ); toolbar.add( tglColoring ); mainPanel.add( toolbar, BorderLayout.NORTH ); + frame.pack(); - getContentPane().add( mainPanel ); - pack(); - - /* - * Listeners. - */ - - spotTable.getTable().getSelectionModel().addListSelectionListener( - new SpotTableSelectionListener() ); + // Listeners. + spotTable.getTable().getSelectionModel().addListSelectionListener( new SpotTableSelectionListener() ); - final UpdateListener refresher = () -> refresh(); - ds.listeners().add( refresher ); - selectionModel.addSelectionChangeListener( this ); - model.addModelChangeListener( this ); - addWindowListener( new WindowAdapter() - { - @Override - public void windowClosing( final java.awt.event.WindowEvent e ) - { - selectionModel.removeSelectionChangeListener( AllSpotsTableView.this ); - model.removeModelChangeListener( AllSpotsTableView.this ); - ds.listeners().remove( refresher ); - }; - } ); + // Actions + actions.runnableAction( () -> System.out.println( "TROLOLO" ), "trololo", "R" ); // DEBUG } public void exportToCsv() { final File file = FileChooser.chooseFile( - this, + frame, selectedFile, new FileNameExtensionFilter( "CSV files", "csv" ), "Export table to CSV", @@ -178,7 +159,7 @@ public void exportToCsv( final String csvFile ) } catch ( final IOException e ) { - model.getLogger().error( "Problem exporting to file " + guiModel.getModel().getLogger().error( "Problem exporting to file " + csvFile + "\n" + e.getMessage() ); } } @@ -192,13 +173,24 @@ public static final TablePanel< Spot > createSpotTable( final Model model, final for ( final String feature : features ) { final Dimension dimension = model.getFeatureModel().getSpotFeatureDimensions().get( feature ); - final String units = TMUtils.getUnitsFor( dimension, model.getSpaceUnits(), model.getTimeUnits() ); + final String units = dimension.units( model.getSpaceUnits(), model.getTimeUnits() ); featureUnits.put( feature, units ); } final Map< String, Boolean > isInts = model.getFeatureModel().getSpotFeatureIsInt(); final Map< String, String > infoTexts = new HashMap<>(); final Function< Spot, String > labelGenerator = spot -> spot.getName(); - final BiConsumer< Spot, String > labelSetter = ( spot, label ) -> spot.setName( label ); + final BiConsumer< Spot, String > labelSetter = ( spot, label ) -> { + model.beginUpdate(); + try + { + model.beforeEdit( spot ); // to make name change undoable + spot.setName( label ); + } + finally + { + model.endUpdate(); + } + }; /* * Feature provider. We add a fake one to show the spot ID. @@ -261,14 +253,14 @@ else if ( feature.equals( SPOT_ID ) ) @Override public void render() { - setLocationRelativeTo( null ); - setVisible( true ); + frame.setLocationRelativeTo( null ); + frame.setVisible( true ); } @Override public void refresh() { - repaint(); + frame.repaint(); } @Override @@ -281,7 +273,7 @@ public void modelChanged( final ModelChangeEvent event ) } final List< Spot > spots = new ArrayList<>(); - for ( final Spot spot : model.getSpots().iterable( true ) ) + for ( final Spot spot : guiModel.getModel().getSpots().iterable( true ) ) spots.add( spot ); spotTable.setObjects( spots ); @@ -299,7 +291,7 @@ public void selectionChanged( final SelectionChangeEvent event ) ignoreSelectionChange.set( true ); // Vertices table. - final Set< Spot > selectedVertices = selectionModel.getSpotSelection(); + final Set< Spot > selectedVertices = guiModel.getSelectionModel().getSpotSelection(); final JTable vt = spotTable.getTable(); vt.getSelectionModel().clearSelection(); for ( final Spot spot : selectedVertices ) @@ -330,22 +322,12 @@ public void centerViewOn( final Spot spot ) spotTable.scrollToObject( spot ); } - @Override - public Model getModel() - { - return model; - } - @Override public String getKey() { return KEY; } - @Override - public void clear() - {} - /** * Forward spot table selection to selection model. */ @@ -365,6 +347,7 @@ public void valueChanged( final ListSelectionEvent event ) for ( final int row : selectedRows ) toSelect.add( spotTable.getObjectForViewRow( row ) ); + final SelectionModel selectionModel = guiModel.getSelectionModel(); selectionModel.clearSelection(); selectionModel.addSpotToSelection( toSelect ); refresh(); @@ -372,4 +355,14 @@ public void valueChanged( final ListSelectionEvent event ) ignoreSelectionChange.set( false ); } } + + @Override + public void clear() + {} + + @Override + public Window getWindow() + { + return frame; + } } diff --git a/src/main/java/fiji/plugin/trackmate/visualization/table/BranchTableView.java b/src/main/java/fiji/plugin/trackmate/visualization/table/BranchTableView.java index 69468ffa2..2a656ea5a 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/table/BranchTableView.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/table/BranchTableView.java @@ -22,10 +22,10 @@ package fiji.plugin.trackmate.visualization.table; import static fiji.plugin.trackmate.gui.Icons.CSV_ICON; -import static fiji.plugin.trackmate.gui.Icons.TRACKMATE_ICON; import java.awt.BorderLayout; import java.awt.Color; +import java.awt.Window; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -58,37 +58,37 @@ import fiji.plugin.trackmate.Dimension; import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.graph.ConvexBranchesDecomposition; import fiji.plugin.trackmate.graph.ConvexBranchesDecomposition.TrackBranchDecomposition; import fiji.plugin.trackmate.graph.TimeDirectedNeighborIndex; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.util.FileChooser; import fiji.plugin.trackmate.util.FileChooser.DialogType; import fiji.plugin.trackmate.util.FileChooser.SelectionMode; -import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.visualization.AbstractTrackMateModelJFrameView; import fiji.plugin.trackmate.visualization.FeatureColorGenerator; -import fiji.plugin.trackmate.visualization.TrackMateModelView; -public class BranchTableView extends JFrame implements TrackMateModelView +public class BranchTableView extends AbstractTrackMateModelJFrameView { - private static final long serialVersionUID = 1L; - private static final String KEY = "SPOT_TABLE"; private String selectedFile = System.getProperty( "user.home" ) + File.separator + "branches.csv"; - private final Model model; - private final TablePanel< Branch > branchTable; - public BranchTableView( final Model model, final SelectionModel selectionModel, final String imageFileName ) + private final JFrame frame; + + public BranchTableView( final GuiModel guiModel, final String imageFileName ) { - super( "Branch table" ); - setIconImage( TRACKMATE_ICON.getImage() ); - this.model = model; + super( guiModel ); this.selectedFile = imageFileName + "_branches.csv"; + final Model model = guiModel.getModel(); + final SelectionModel selectionModel = guiModel.getSelectionModel(); /* * GUI. @@ -112,8 +112,10 @@ public BranchTableView( final Model model, final SelectionModel selectionModel, toolbar.add( Box.createHorizontalGlue() ); mainPanel.add( toolbar, BorderLayout.NORTH ); - getContentPane().add( mainPanel ); - pack(); + this.frame = new JFrame( "Branch table" ); + frame.setIconImage( Icons.TRACKMATE_ICON.getImage() ); + frame.getContentPane().add( mainPanel ); + frame.pack(); } public TablePanel< Branch > getBranchTable() @@ -124,7 +126,7 @@ public TablePanel< Branch > getBranchTable() public void exportToCsv() { final File file = FileChooser.chooseFile( - this, + frame, selectedFile, new FileNameExtensionFilter( "CSV files", "csv" ), "Export table to CSV", @@ -145,7 +147,7 @@ public void exportToCsv( final String csvFile ) } catch ( final IOException e ) { - model.getLogger().error( "Problem exporting to file " + guiModel.getModel().getLogger().error( "Problem exporting to file " + csvFile + "\n" + e.getMessage() ); } } @@ -294,7 +296,7 @@ public static final TablePanel< Branch > createBranchTable( final Model model, f final BiFunction< Branch, String, Double > featureFun = ( br, feature ) -> br.getFeature( feature ); final Map< String, String > featureUnits = new HashMap<>(); BRANCH_FEATURES_DIMENSIONS.forEach( - ( f, d ) -> featureUnits.put( f, TMUtils.getUnitsFor( d, model.getSpaceUnits(), model.getTimeUnits() ) ) ); + ( f, d ) -> featureUnits.put( f, d.units( model.getSpaceUnits(), model.getTimeUnits() ) ) ); final Map< String, String > infoTexts = new HashMap<>(); final Function< Branch, String > labelGenerator = b -> b.toString(); final BiConsumer< Branch, String > labelSetter = null; @@ -322,26 +324,20 @@ public static final TablePanel< Branch > createBranchTable( final Model model, f @Override public void render() { - setLocationRelativeTo( null ); - setVisible( true ); + frame.setLocationRelativeTo( null ); + frame.setVisible( true ); } @Override public void refresh() { - repaint(); + frame.repaint(); } @Override public void centerViewOn( final Spot spot ) {} - @Override - public Model getModel() - { - return model; - } - @Override public String getKey() { @@ -587,4 +583,14 @@ public int compareTo( final Branch o ) BRANCH_FEATURES_ISINTS.put( MEAN_PREDECESSORS_DELAY, Boolean.FALSE ); BRANCH_FEATURES_DIMENSIONS.put( MEAN_PREDECESSORS_DELAY, Dimension.TIME ); } + + @Override + public void modelChanged( final ModelChangeEvent event ) + {} + + @Override + public Window getWindow() + { + return frame; + } } diff --git a/src/main/java/fiji/plugin/trackmate/visualization/table/TablePanel.java b/src/main/java/fiji/plugin/trackmate/visualization/table/TablePanel.java index 83d0ba021..e0a793af2 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/table/TablePanel.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/table/TablePanel.java @@ -68,10 +68,11 @@ import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; +import org.scijava.ui.config.visitors.gui.elements.ColorIcon; + import com.opencsv.CSVWriter; import fiji.plugin.trackmate.gui.GuiUtils; -import fiji.plugin.trackmate.gui.displaysettings.ColorIcon; import fiji.plugin.trackmate.visualization.FeatureColorGenerator; import gnu.trove.map.hash.TObjectIntHashMap; diff --git a/src/main/java/fiji/plugin/trackmate/visualization/table/TrackTableView.java b/src/main/java/fiji/plugin/trackmate/visualization/table/TrackTableView.java index 7b8ccdc1d..8093b4a31 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/table/TrackTableView.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/table/TrackTableView.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 * . @@ -22,11 +22,10 @@ package fiji.plugin.trackmate.visualization.table; import static fiji.plugin.trackmate.gui.Icons.CSV_ICON; -import static fiji.plugin.trackmate.gui.Icons.TRACKMATE_ICON; import java.awt.BorderLayout; import java.awt.Color; -import java.awt.event.WindowAdapter; +import java.awt.Window; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -58,33 +57,28 @@ import fiji.plugin.trackmate.Dimension; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.ModelChangeEvent; -import fiji.plugin.trackmate.ModelChangeListener; import fiji.plugin.trackmate.SelectionChangeEvent; -import fiji.plugin.trackmate.SelectionChangeListener; import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.features.FeatureUtils; import fiji.plugin.trackmate.features.manual.ManualEdgeColorAnalyzer; import fiji.plugin.trackmate.features.manual.ManualSpotColorAnalyzerFactory; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.UpdateListener; import fiji.plugin.trackmate.util.FileChooser; import fiji.plugin.trackmate.util.FileChooser.DialogType; import fiji.plugin.trackmate.util.FileChooser.SelectionMode; -import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.visualization.AbstractTrackMateModelJFrameView; import fiji.plugin.trackmate.visualization.FeatureColorGenerator; -import fiji.plugin.trackmate.visualization.TrackMateModelView; import fiji.plugin.trackmate.visualization.trackscheme.utils.SearchBar; -public class TrackTableView extends JFrame implements TrackMateModelView, ModelChangeListener, SelectionChangeListener +public class TrackTableView extends AbstractTrackMateModelJFrameView { - private static final long serialVersionUID = 1L; - private static final String KEY = "TRACK_TABLES"; - private final Model model; - private final TablePanel< Spot > spotTable; private final TablePanel< DefaultWeightedEdge > edgeTable; @@ -93,17 +87,17 @@ public class TrackTableView extends JFrame implements TrackMateModelView, ModelC private final AtomicBoolean ignoreSelectionChange = new AtomicBoolean( false ); - private final SelectionModel selectionModel; - private String imagePath; - public TrackTableView( final Model model, final SelectionModel selectionModel, final DisplaySettings ds, final String imagePath ) + private final JFrame frame; + + public TrackTableView( final GuiModel guiModel, final String imagePath ) { - super( "Track tables" ); + super( guiModel ); this.imagePath = imagePath; - setIconImage( TRACKMATE_ICON.getImage() ); - this.model = model; - this.selectionModel = selectionModel; + final Model model = guiModel.getModel(); + final SelectionModel selectionModel = guiModel.getSelectionModel(); + final DisplaySettings ds = guiModel.getDisplaySettings(); /* * GUI. @@ -118,12 +112,9 @@ public TrackTableView( final Model model, final SelectionModel selectionModel, f this.trackTable = createTrackTable( model, ds ); // Listeners. - spotTable.getTable().getSelectionModel().addListSelectionListener( - new SpotTableSelectionListener() ); - edgeTable.getTable().getSelectionModel().addListSelectionListener( - new EdgeTableSelectionListener() ); - trackTable.getTable().getSelectionModel().addListSelectionListener( - new TrackTableSelectionListener() ); + spotTable.getTable().getSelectionModel().addListSelectionListener( new SpotTableSelectionListener() ); + edgeTable.getTable().getSelectionModel().addListSelectionListener( new EdgeTableSelectionListener() ); + trackTable.getTable().getSelectionModel().addListSelectionListener( new TrackTableSelectionListener() ); // Tabbed pane. final JTabbedPane tabbedPane = new JTabbedPane( JTabbedPane.LEFT ); @@ -155,8 +146,11 @@ public TrackTableView( final Model model, final SelectionModel selectionModel, f toolbar.add( tglColoring ); mainPanel.add( toolbar, BorderLayout.NORTH ); - getContentPane().add( mainPanel ); - pack(); + this.frame = new JFrame( "Track Tables" ); + frame.setIconImage( Icons.TRACKMATE_ICON.getImage() ); + setWindow( frame ); + frame.getContentPane().add( mainPanel ); + frame.pack(); /* * Listeners. @@ -166,15 +160,10 @@ public TrackTableView( final Model model, final SelectionModel selectionModel, f ds.listeners().add( refresher ); selectionModel.addSelectionChangeListener( this ); model.addModelChangeListener( this ); - addWindowListener( new WindowAdapter() - { - @Override - public void windowClosing( final java.awt.event.WindowEvent e ) - { - selectionModel.removeSelectionChangeListener( TrackTableView.this ); - model.removeModelChangeListener( TrackTableView.this ); - ds.listeners().remove( refresher ); - }; + onClose( () -> { + ds.listeners().remove( refresher ); + selectionModel.removeSelectionChangeListener( this ); + model.removeModelChangeListener( this ); } ); } @@ -205,7 +194,7 @@ private void exportToCsv( final int index ) } final File file = FileChooser.chooseFile( - this, + frame, selectedFile, new FileNameExtensionFilter( "CSV files", "csv" ), "Export table to CSV", @@ -221,7 +210,7 @@ private void exportToCsv( final int index ) } catch ( final IOException e ) { - model.getLogger().error( "Problem exporting to file " + guiModel.getModel().getLogger().error( "Problem exporting to file " + file + "\n" + e.getMessage() ); } imagePath = selectedFile; @@ -238,13 +227,23 @@ public static final TablePanel< Integer > createTrackTable( final Model model, f for ( final String feature : features ) { final Dimension dimension = model.getFeatureModel().getTrackFeatureDimensions().get( feature ); - final String units = TMUtils.getUnitsFor( dimension, model.getSpaceUnits(), model.getTimeUnits() ); + final String units = dimension.units( model.getSpaceUnits(), model.getTimeUnits() ); featureUnits.put( feature, units ); } final Map< String, Boolean > isInts = model.getFeatureModel().getTrackFeatureIsInt(); final Map< String, String > infoTexts = new HashMap<>(); final Function< Integer, String > labelGenerator = id -> model.getTrackModel().name( id ); - final BiConsumer< Integer, String > labelSetter = ( id, label ) -> model.getTrackModel().setName( id, label ); + final BiConsumer< Integer, String > labelSetter = ( id, label ) -> { + model.beginUpdate(); + try + { + model.setTrackName( id, label ); + } + finally + { + model.endUpdate(); + } + }; final Supplier< FeatureColorGenerator< Integer > > coloring = () -> FeatureUtils.createWholeTrackColorGenerator( model, ds ); @@ -277,7 +276,7 @@ public static final TablePanel< DefaultWeightedEdge > createEdgeTable( final Mod for ( final String feature : features ) { final Dimension dimension = model.getFeatureModel().getEdgeFeatureDimensions().get( feature ); - final String units = TMUtils.getUnitsFor( dimension, model.getSpaceUnits(), model.getTimeUnits() ); + final String units = dimension.units( model.getSpaceUnits(), model.getTimeUnits() ); featureUnits.put( feature, units ); } final Map< String, Boolean > isInts = model.getFeatureModel().getEdgeFeatureIsInt(); @@ -342,13 +341,24 @@ public static final TablePanel< Spot > createSpotTable( final Model model, final for ( final String feature : features ) { final Dimension dimension = model.getFeatureModel().getSpotFeatureDimensions().get( feature ); - final String units = TMUtils.getUnitsFor( dimension, model.getSpaceUnits(), model.getTimeUnits() ); + final String units = dimension.units( model.getSpaceUnits(), model.getTimeUnits() ); featureUnits.put( feature, units ); } final Map< String, Boolean > isInts = model.getFeatureModel().getSpotFeatureIsInt(); final Map< String, String > infoTexts = new HashMap<>(); final Function< Spot, String > labelGenerator = spot -> spot.getName(); - final BiConsumer< Spot, String > labelSetter = ( spot, label ) -> spot.setName( label ); + final BiConsumer< Spot, String > labelSetter = ( spot, label ) -> { + model.beginUpdate(); + try + { + model.beforeEdit( spot ); // to make name change undoable + spot.setName( label ); + } + finally + { + model.endUpdate(); + } + }; /* * Feature provider. We add a fake one to show the spot ID. @@ -411,14 +421,14 @@ else if ( feature.equals( SPOT_ID ) ) @Override public void render() { - setLocationRelativeTo( null ); - setVisible( true ); + frame.setLocationRelativeTo( null ); + frame.setVisible( true ); } @Override public void refresh() { - repaint(); + frame.repaint(); } @Override @@ -429,7 +439,7 @@ public void modelChanged( final ModelChangeEvent event ) refresh(); return; } - + final Model model = guiModel.getModel(); final List< Spot > spots = new ArrayList<>(); for ( final Integer trackID : model.getTrackModel().unsortedTrackIDs( true ) ) spots.addAll( model.getTrackModel().trackSpots( trackID ) ); @@ -457,6 +467,7 @@ public void selectionChanged( final SelectionChangeEvent event ) ignoreSelectionChange.set( true ); // Vertices table. + final SelectionModel selectionModel = guiModel.getSelectionModel(); final Set< Spot > selectedVertices = selectionModel.getSpotSelection(); final JTable vt = spotTable.getTable(); vt.getSelectionModel().clearSelection(); @@ -515,12 +526,6 @@ public void centerViewOn( final Spot spot ) spotTable.scrollToObject( spot ); } - @Override - public Model getModel() - { - return model; - } - @Override public String getKey() { @@ -565,6 +570,7 @@ public void valueChanged( final ListSelectionEvent event ) for ( final int row : selectedRows ) toSelect.add( spotTable.getObjectForViewRow( row ) ); + final SelectionModel selectionModel = guiModel.getSelectionModel(); selectionModel.clearSelection(); selectionModel.addSpotToSelection( toSelect ); refresh(); @@ -592,6 +598,7 @@ public void valueChanged( final ListSelectionEvent event ) for ( final int row : selectedRows ) toSelect.add( edgeTable.getObjectForViewRow( row ) ); + final SelectionModel selectionModel = guiModel.getSelectionModel(); selectionModel.clearSelection(); selectionModel.addEdgeToSelection( toSelect ); refresh(); @@ -613,6 +620,8 @@ public void valueChanged( final ListSelectionEvent event ) return; ignoreSelectionChange.set( true ); + final Model model = guiModel.getModel(); + final SelectionModel selectionModel = guiModel.getSelectionModel(); final Set< Spot > spots = new HashSet<>(); final Set< DefaultWeightedEdge > edges = new HashSet<>(); @@ -633,4 +642,10 @@ public void valueChanged( final ListSelectionEvent event ) } } + + @Override + public Window getWindow() + { + return frame; + } } 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 60636e3c3..d8c51a3e0 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 * . @@ -79,6 +79,7 @@ public void cellLabelChanged( final Object cell, final Object value, final boole if ( null == spot ) return; final String str = ( String ) value; + tmm.beforeEdit( spot ); // to make name change undoable spot.setName( str ); getModel().setValue( cell, str ); diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SaveAction.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SaveAction.java index 80c986c17..c15e1729c 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SaveAction.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SaveAction.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -79,15 +79,15 @@ public SaveAction( final TrackScheme trackScheme ) /** * Saves XML+PNG format. - * + * * @param frame - * the TrackScheme frame to capture from. + * the TrackScheme frame to capture. * @param filename * the file to save to. * @param bg - * the background color, or null for transparent. + * the color of the background in the exported image. * @throws IOException - * if something goes wrong when writing the file. + * if an error happens while writing. */ protected void saveXmlPng( final TrackSchemeFrame frame, final String filename, final Color bg ) throws IOException { diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotIconGrabber.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotIconGrabber.java index 430919bd1..7c928259a 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotIconGrabber.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotIconGrabber.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -74,7 +74,7 @@ public SpotIconGrabber( final ImgPlus< T > img ) * a factor that determines the size of the thumbnail. The * thumbnail will have a size equal to the spot diameter times * this radius. - * @return a base64-encoded PNG image string representing the spot. + * @return a Base64 representation of the spot image. */ public String getImageString( final Spot spot, final double radiusFactor ) { @@ -125,7 +125,7 @@ public String getImageString( final Spot spot, final double radiusFactor ) /** * Returns a 2D slice extract around the specified coordinates. - * + * * @param x * top-left x coordinate. * @param y @@ -172,7 +172,7 @@ public final Img< T > grabImage( final long x, final long y, final long slice, f /** * Returns a 3D cropped copy around the specified coordinates. - * + * * @param x * top-left x coordinate. * @param y diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotImageUpdater.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotImageUpdater.java index ca8dd909d..f092cbb12 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotImageUpdater.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotImageUpdater.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -58,7 +58,7 @@ public SpotImageUpdater( final Settings settings ) * group calls to this method for spots that belong to the same frame. * * @param spot - * the spot for which we want the image string. + * the spot. * @param radiusFactor * a factor that determines the size of the thumbnail. The * thumbnail will have a size equal to the spot diameter times 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 1ae255b95..96a07e2c9 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackScheme.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackScheme.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -25,8 +25,7 @@ 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.Window; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collection; @@ -61,11 +60,14 @@ import fiji.plugin.trackmate.SelectionChangeEvent; import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.visualization.AbstractTrackMateModelJFrameView; import fiji.plugin.trackmate.visualization.AbstractTrackMateModelView; +import fiji.plugin.trackmate.visualization.trackscheme.behaviours.TrackSchemeActions; +import fiji.plugin.trackmate.visualization.ui.KeyConfigContexts; import ij.ImagePlus; -public class TrackScheme extends AbstractTrackMateModelView +public class TrackScheme extends AbstractTrackMateModelJFrameView { public static final String INFO_TEXT = "" + "TrackScheme displays the tracking results as track lanes,
    " @@ -161,23 +163,15 @@ public class TrackScheme extends AbstractTrackMateModelView * CONSTRUCTORS */ - public TrackScheme( final Model model, final SelectionModel selectionModel, final DisplaySettings displaySettings ) + public TrackScheme( final GuiModel guiModel ) { - super( model, selectionModel, displaySettings ); - this.gui = new TrackSchemeFrame( this, displaySettings ); + super( guiModel, KeyConfigContexts.TRACKSCHEME ); + this.gui = new TrackSchemeFrame( this, guiModel.getDisplaySettings() ); + setWindow( gui ); 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 ); @@ -192,14 +186,11 @@ 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. + * Returns the column index that is the first one after all the track + * columns. + * + * @return the column index. */ public int getUnlaidSpotColumn() { @@ -208,26 +199,23 @@ public int getUnlaidSpotColumn() /** * Returns the first free column for the target row. - * + * * @param frame - * the target row. - * + * the row. * @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. - * - * @return the GUI frame. + * + * @return the GUI. */ public TrackSchemeFrame getGUI() { @@ -237,8 +225,8 @@ public TrackSchemeFrame getGUI() /** * Returns the {@link JGraphXAdapter} that serves as a model for the graph * displayed in this frame. - * - * @return the graph adapter. + * + * @return the adapter. */ public JGraphXAdapter getGraph() { @@ -247,7 +235,7 @@ public JGraphXAdapter getGraph() /** * Returns the graph layout in charge of arranging the cells on the graph. - * + * * @return the graph layout. */ public TrackSchemeGraphLayout getGraphLayout() @@ -267,7 +255,7 @@ private JGraphXAdapter createGraph() { gui.logger.setStatus( "Creating graph adapter." ); - final JGraphXAdapter lGraph = new JGraphXAdapter( model ); + final JGraphXAdapter lGraph = new JGraphXAdapter( guiModel.getModel() ); lGraph.setAllowLoops( false ); lGraph.setAllowDanglingEdges( false ); lGraph.setCellsCloneable( false ); @@ -297,7 +285,6 @@ private JGraphXAdapter createGraph() */ private mxICell updateCellOf( final Spot spot ) { - mxICell cell = graph.getCellFor( spot ); graph.getModel().beginUpdate(); try @@ -320,7 +307,7 @@ private mxICell updateCellOf( final Spot spot ) if ( spotImageUpdater != null && doThumbnailCapture ) { String style = cell.getStyle(); - final double radiusFactor = displaySettings.getSpotDisplayRadius(); + final double radiusFactor = guiModel.getDisplaySettings().getSpotDisplayRadius(); final String imageStr = spotImageUpdater.getImageString( spot, radiusFactor ); style = mxStyleUtils.setStyle( style, mxConstants.STYLE_IMAGE, "data:image/base64," + imageStr ); graph.getModel().setStyle( cell, style ); @@ -355,7 +342,7 @@ private mxICell insertSpotInGraph( final Spot spot, final int targetColumn ) final mxGeometry geometry = new mxGeometry( x, y, DEFAULT_CELL_WIDTH, DEFAULT_CELL_HEIGHT ); cellAdded.setGeometry( geometry ); // Set its style - final double radiusFactor = displaySettings.getSpotDisplayRadius(); + final double radiusFactor = guiModel.getDisplaySettings().getSpotDisplayRadius(); if ( null != spotImageUpdater && doThumbnailCapture ) { final String imageStr = spotImageUpdater.getImageString( spot, radiusFactor ); @@ -372,6 +359,7 @@ private mxICell insertSpotInGraph( final Spot spot, final int targetColumn ) */ private void importTrack( final int trackIndex ) { + final Model model = guiModel.getModel(); model.beginUpdate(); graph.getModel().beginUpdate(); try @@ -417,6 +405,7 @@ protected void addEdgeManually( mxCell cell ) { final mxIGraphModel graphModel = graph.getModel(); cell.setValue( "New" ); + final Model model = guiModel.getModel(); model.beginUpdate(); graphModel.beginUpdate(); try @@ -489,7 +478,7 @@ protected void addEdgeManually( mxCell cell ) { graphModel.endUpdate(); model.endUpdate(); - selectionModel.clearEdgeSelection(); + guiModel.getSelectionModel().clearEdgeSelection(); } } } @@ -506,6 +495,7 @@ public void selectionChanged( final SelectionChangeEvent event ) doFireSelectionChangeEvent = false; + final SelectionModel selectionModel = guiModel.getSelectionModel(); final ArrayList< Object > newSelection = new ArrayList<>( selectionModel.getSpotSelection().size() + selectionModel.getEdgeSelection().size() ); final Iterator< DefaultWeightedEdge > edgeIt = selectionModel.getEdgeSelection().iterator(); while ( edgeIt.hasNext() ) @@ -560,6 +550,7 @@ public void modelChanged( final ModelChangeEvent event ) if ( event.getEventID() != ModelChangeEvent.MODEL_MODIFIED ) return; + final Model model = guiModel.getModel(); graph.getModel().beginUpdate(); try { @@ -739,6 +730,9 @@ public void run() @Override public void render() { + if ( graph != null ) + return; + final long start = System.currentTimeMillis(); // Graph to mirror model this.graph = createGraph(); @@ -749,15 +743,17 @@ public void render() @Override public void run() { + final Model model = guiModel.getModel(); + // 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 ); + TrackScheme.this.stylist = new TrackSchemeStylist( guiModel.getModel(), graph, guiModel.getDisplaySettings() ); gui.logger.setStatus( "Creating layout manager." ); - TrackScheme.this.graphLayout = new TrackSchemeGraphLayout( graph, model, gui.graphComponent ); + TrackScheme.this.graphLayout = new TrackSchemeGraphLayout( graph, guiModel.getModel(), gui.graphComponent ); // Execute style and layout gui.logger.setProgress( 0.75 ); @@ -784,6 +780,11 @@ public void run() gui.graphComponent.zoomOut(); gui.graphComponent.zoomOut(); + // Actions and behaviours + TrackSchemeActions.install( actions, model, gui.graphComponent ); + // DEBUG + actions.runnableAction( () -> System.out.println( "[TrackScheme] TROLOLO" ), "trolol", "R" ); + gui.logger.setProgress( 0 ); final long end = System.currentTimeMillis(); gui.logger.log( String.format( "TrackScheme rendering done in %.1f s.", ( end - start ) / 1000d ) ); @@ -794,19 +795,13 @@ public void run() @Override public void refresh() - {} - - @Override - public void clear() { - System.out.println( "[TrackScheme] clear() called" ); + doTrackStyle(); } @Override - public Model getModel() - { - return model; - } + public void clear() + {} /* * PRIVATE METHODS @@ -916,6 +911,7 @@ private void userChangedSelection( final Collection< Object > added, final Colle } doFireSelectionChangeEvent = false; + final SelectionModel selectionModel = guiModel.getSelectionModel(); if ( !edgesToAdd.isEmpty() ) selectionModel.addEdgeToSelection( edgesToAdd ); @@ -980,6 +976,8 @@ else if ( cell.isEdge() ) // Clean model doFireModelChangeEvent = false; + final Model model = guiModel.getModel(); + final SelectionModel selectionModel = guiModel.getSelectionModel(); model.beginUpdate(); try { @@ -1090,6 +1088,7 @@ public void doTrackStyle() private void createThumbnails() { // Group spots per frame + final Model model = guiModel.getModel(); final Set< Integer > frames = model.getSpots().keySet(); final HashMap< Integer, HashSet< Spot > > spotPerFrame = new HashMap<>( frames.size() ); for ( final Integer frame : frames ) @@ -1108,7 +1107,7 @@ private void createThumbnails() if ( null != spotImageUpdater ) { gui.logger.setStatus( "Collecting spot thumbnails." ); - final double radiusFactor = displaySettings.getSpotDisplayRadius(); + final double radiusFactor = guiModel.getDisplaySettings().getSpotDisplayRadius(); int index = 0; try { @@ -1189,6 +1188,8 @@ public void toggleDisplayDecoration() */ public void linkSpots() { + final Model model = guiModel.getModel(); + final SelectionModel selectionModel = guiModel.getSelectionModel(); // Sort spots by time final TreeMap< Integer, Spot > spotsInTime = new TreeMap<>(); @@ -1301,7 +1302,7 @@ public void removeSelectedLinkCells() edgeCells.add( obj ); } - + graph.getModel().beginUpdate(); try { @@ -1336,7 +1337,7 @@ public void selectTrack( final Collection< mxCell > vertices, final Collection< inspectionEdges.add( dwe ); } // Forward to selection model - selectionModel.selectTrack( inspectionSpots, inspectionEdges, direction ); + guiModel.getSelectionModel().selectTrack( inspectionSpots, inspectionEdges, direction ); } @Override @@ -1344,4 +1345,10 @@ public String getKey() { return KEY; } + + @Override + public Window getWindow() + { + return gui; + } } diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeActions.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeActions.java deleted file mode 100644 index 0a3fb5bd0..000000000 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeActions.java +++ /dev/null @@ -1,486 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.visualization.trackscheme; - -import static fiji.plugin.trackmate.gui.Icons.ARROW_DOWNLEFT_ICON; -import static fiji.plugin.trackmate.gui.Icons.ARROW_DOWNRIGHT_ICON; -import static fiji.plugin.trackmate.gui.Icons.ARROW_DOWN_ICON; -import static fiji.plugin.trackmate.gui.Icons.ARROW_LEFT_ICON; -import static fiji.plugin.trackmate.gui.Icons.ARROW_RIGHT_ICON; -import static fiji.plugin.trackmate.gui.Icons.ARROW_UPLEFT_ICON; -import static fiji.plugin.trackmate.gui.Icons.ARROW_UPRIGHT_ICON; -import static fiji.plugin.trackmate.gui.Icons.ARROW_UP_ICON; -import static fiji.plugin.trackmate.gui.Icons.EDIT_ICON; -import static fiji.plugin.trackmate.gui.Icons.END_ICON; -import static fiji.plugin.trackmate.gui.Icons.HOME_ICON; -import static fiji.plugin.trackmate.gui.Icons.RESET_ZOOM_ICON; -import static fiji.plugin.trackmate.gui.Icons.ZOOM_IN_ICON; -import static fiji.plugin.trackmate.gui.Icons.ZOOM_OUT_ICON; - -import java.awt.Point; -import java.awt.Rectangle; -import java.awt.event.ActionEvent; -import java.awt.geom.Point2D; -import java.util.ArrayList; -import java.util.List; - -import javax.swing.AbstractAction; -import javax.swing.Action; -import javax.swing.Icon; - -import com.mxgraph.model.mxCell; -import com.mxgraph.model.mxICell; -import com.mxgraph.swing.util.mxGraphActions; -import com.mxgraph.util.mxEvent; -import com.mxgraph.util.mxEventObject; -import com.mxgraph.util.mxEventSource.mxIEventListener; -import com.mxgraph.view.mxGraph; - -import fiji.plugin.trackmate.Spot; - -public class TrackSchemeActions -{ - - /** - * When panning with the keyboard, by how much pixels to move. - */ - private static final int PAN_AMOUNT = 100; - - private static Action zoomInAction; - - static - { - zoomInAction = mxGraphActions.getZoomInAction(); - zoomInAction.putValue( Action.SMALL_ICON, ZOOM_IN_ICON ); - } - - private static Action zoomOutAction; - - static - { - zoomOutAction = mxGraphActions.getZoomOutAction(); - zoomOutAction.putValue( Action.SMALL_ICON, ZOOM_OUT_ICON ); - } - - private TrackSchemeActions() - {} - - public static Action getEditAction( final TrackSchemeGraphComponent graphComponent ) - { - return new EditAction( "edit", EDIT_ICON, graphComponent ); - } - - public static Action getHomeAction( final TrackSchemeGraphComponent graphComponent ) - { - return new HomeAction( "home", HOME_ICON, graphComponent ); - } - - public static Action getEndAction( final TrackSchemeGraphComponent graphComponent ) - { - return new EndAction( "end", END_ICON, graphComponent ); - } - - public static Action getResetZoomAction( final TrackSchemeGraphComponent graphComponent ) - { - return new ResetZoomAction( "resetZoom", RESET_ZOOM_ICON, graphComponent ); - } - - public static Action getZoomInAction( final TrackSchemeGraphComponent graphComponent ) - { - return new AbstractAction( "zoomIn", ZOOM_IN_ICON ) - { - private static final long serialVersionUID = 1L; - - @Override - public void actionPerformed( final ActionEvent e ) - { - graphComponent.zoomIn(); - } - }; - } - - public static Action getZoomOutAction( final TrackSchemeGraphComponent graphComponent ) - { - return new AbstractAction( "zoomOut", ZOOM_OUT_ICON ) - { - private static final long serialVersionUID = 1L; - - @Override - public void actionPerformed( final ActionEvent e ) - { - graphComponent.zoomOut(); - } - }; - } - - public static Action getSelectNoneAction() - { - return mxGraphActions.getSelectNoneAction(); - } - - public static Action getSelectAllAction() - { - return mxGraphActions.getSelectAllAction(); - } - - public static Action getPanDownAction( final TrackSchemeGraphComponent graphComponent ) - { - return new PanAction( "panDown", ARROW_DOWN_ICON, graphComponent, 0, PAN_AMOUNT ); - } - - public static Action getPanLeftAction( final TrackSchemeGraphComponent graphComponent ) - { - return new PanAction( "panLeft", ARROW_LEFT_ICON, graphComponent, -PAN_AMOUNT, 0 ); - } - - public static Action getPanRightAction( final TrackSchemeGraphComponent graphComponent ) - { - return new PanAction( "panRight", ARROW_RIGHT_ICON, graphComponent, PAN_AMOUNT, 0 ); - } - - public static Action getPanUpAction( final TrackSchemeGraphComponent graphComponent ) - { - return new PanAction( "panUp", ARROW_UP_ICON, graphComponent, 0, -PAN_AMOUNT ); - } - - public static Action getPanDownLeftAction( final TrackSchemeGraphComponent graphComponent ) - { - return new PanAction( "panDownLeft", ARROW_DOWNLEFT_ICON, graphComponent, -PAN_AMOUNT, PAN_AMOUNT ); - } - - public static Action getPanDownRightAction( final TrackSchemeGraphComponent graphComponent ) - { - return new PanAction( "panDownRight", ARROW_DOWNRIGHT_ICON, graphComponent, PAN_AMOUNT, PAN_AMOUNT ); - } - - public static Action getPanUpLeftAction( final TrackSchemeGraphComponent graphComponent ) - { - return new PanAction( "panUpLeft", ARROW_UPLEFT_ICON, graphComponent, -PAN_AMOUNT, -PAN_AMOUNT ); - } - - public static Action getPanUpRightAction( final TrackSchemeGraphComponent graphComponent ) - { - return new PanAction( "panUpRight", ARROW_UPRIGHT_ICON, graphComponent, PAN_AMOUNT, -PAN_AMOUNT ); - } - - /* - * ACTION CLASSES - */ - - private static class PanAction extends AbstractAction - { - - private static final long serialVersionUID = 1L; - - private final int amountx; - - private final int amounty; - - private final TrackSchemeGraphComponent graphComponent; - - public PanAction( final String name, final Icon icon, final TrackSchemeGraphComponent graphComponent, final int amountx, final int amounty ) - { - super( name, icon ); - this.graphComponent = graphComponent; - this.amountx = amountx; - this.amounty = amounty; - } - - @Override - public void actionPerformed( final ActionEvent e ) - { - final Rectangle r = graphComponent.getViewport().getViewRect(); - final int right = r.x + ( ( amountx < 0 ) ? 0 : r.width ) + amountx; - final int bottom = r.y + ( ( amounty < 0 ) ? 0 : r.height ) + amounty; - graphComponent.getGraphControl().scrollRectToVisible( new Rectangle( right, bottom, 0, 0 ) ); - } - } - - private static class ResetZoomAction extends AbstractAction - { - - private static final long serialVersionUID = 1L; - - private final TrackSchemeGraphComponent graphComponent; - - public ResetZoomAction( final String name, final Icon icon, final TrackSchemeGraphComponent graphComponent ) - { - super( name, icon ); - this.graphComponent = graphComponent; - } - - @Override - public void actionPerformed( final ActionEvent e ) - { - graphComponent.zoomTo( 1.0, false ); - } - - } - - /** - * Centers the view to the first cell in selection. - * - * @author Jean-Yves Tinevez <jeanyves.tinevez@gmail.com> Sep 12, 2013 - * - */ - private static class HomeAction extends AbstractAction - { - - private static final long serialVersionUID = 1L; - - private final TrackSchemeGraphComponent graphComponent; - - public HomeAction( final String name, final Icon icon, final TrackSchemeGraphComponent graphComponent ) - { - super( name, icon ); - this.graphComponent = graphComponent; - } - - @Override - public void actionPerformed( final ActionEvent e ) - { - - mxCell cell = null; - final JGraphXAdapter graph = graphComponent.getGraph(); - final List< mxCell > vertices = getSelectionVertices( graph ); - if ( !vertices.isEmpty() ) - { - int minFrame = Integer.MAX_VALUE; - for ( final mxCell mxCell : vertices ) - { - final int frame = graph.getSpotFor( mxCell ).getFeature( Spot.FRAME ).intValue(); - if ( frame < minFrame ) - { - minFrame = frame; - cell = mxCell; - } - } - } - else - { - final List< mxCell > edges = getSelectionEdges( graph ); - if ( !edges.isEmpty() ) - { - int minFrame = Integer.MAX_VALUE; - for ( final mxCell mxCell : edges ) - { - final mxICell target = mxCell.getTarget(); - final int frame = graph.getSpotFor( target ).getFeature( Spot.FRAME ).intValue(); - if ( frame < minFrame ) - { - minFrame = frame; - cell = mxCell; - } - } - cell = edges.get( edges.size() - 1 ); - } - else - { - return; - } - } - graphComponent.scrollCellToVisible( cell, true ); - } - } - - /** - * Centers the view to the last cell in selection, sorted by frame number. - * - * @author Jean-Yves Tinevez <jeanyves.tinevez@gmail.com> Sep 12, 2013 - * - */ - private static class EndAction extends AbstractAction - { - - private static final long serialVersionUID = 1L; - - private final TrackSchemeGraphComponent graphComponent; - - public EndAction( final String name, final Icon icon, final TrackSchemeGraphComponent graphComponent ) - { - super( name, icon ); - this.graphComponent = graphComponent; - } - - @Override - public void actionPerformed( final ActionEvent e ) - { - mxCell cell = null; - final JGraphXAdapter graph = graphComponent.getGraph(); - final List< mxCell > vertices = getSelectionVertices( graph ); - - if ( !vertices.isEmpty() ) - { - int maxFrame = Integer.MIN_VALUE; - for ( final mxCell mxCell : vertices ) - { - final int frame = graph.getSpotFor( mxCell ).getFeature( Spot.FRAME ).intValue(); - if ( frame > maxFrame ) - { - maxFrame = frame; - cell = mxCell; - } - } - } - else - { - final List< mxCell > edges = getSelectionEdges( graph ); - if ( !edges.isEmpty() ) - { - int maxFrame = Integer.MIN_VALUE; - for ( final mxCell mxCell : edges ) - { - final mxICell target = mxCell.getTarget(); - final int frame = graph.getSpotFor( target ).getFeature( Spot.FRAME ).intValue(); - if ( frame > maxFrame ) - { - maxFrame = frame; - cell = mxCell; - } - } - cell = edges.get( edges.size() - 1 ); - } - else - { - return; - } - } - graphComponent.scrollCellToVisible( cell, true ); - } - } - - public static class EditAction extends AbstractAction - { - - private static final long serialVersionUID = 1L; - - private final TrackSchemeGraphComponent graphComponent; - - public EditAction( final String name, final Icon icon, final TrackSchemeGraphComponent graphComponent ) - { - super( name, icon ); - this.graphComponent = graphComponent; - } - - @Override - public void actionPerformed( final ActionEvent e ) - { - multiEditSpotName( graphComponent, e ); - } - - private void multiEditSpotName( final TrackSchemeGraphComponent lGraphComponent, final ActionEvent triggerEvent ) - { - /* - * We want to display the editing window in the cell 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 JGraphXAdapter graph = lGraphComponent.getGraph(); - final List< mxCell > vertices = getSelectionVertices( graph ); - if ( vertices.isEmpty() ) - { return; } - - final Point mousePosition = lGraphComponent.getMousePosition(); - final mxCell tc; - if ( null != mousePosition ) - tc = getClosestCell( vertices, mousePosition ); - else - tc = vertices.get( 0 ); - vertices.remove( tc ); - - lGraphComponent.startEditingAtCell( tc, triggerEvent ); - lGraphComponent.addListener( mxEvent.LABEL_CHANGED, new mxIEventListener() - { - - @Override - public void invoke( final Object sender, final mxEventObject evt ) - { - for ( final mxCell cell : vertices ) - { - cell.setValue( tc.getValue() ); - graph.getSpotFor( cell ).setName( tc.getValue().toString() ); - } - lGraphComponent.refresh(); - lGraphComponent.removeListener( this ); - } - } ); - } - - /** - * Return, from the given list of cell, the one which is the closer to - * the point of this instance. - * - * @param point - */ - private mxCell getClosestCell( final Iterable< mxCell > vertices, final Point2D point ) - { - double min_dist = Double.POSITIVE_INFINITY; - mxCell target_cell = null; - for ( final mxCell cell : vertices ) - { - final Point location = cell.getGeometry().getPoint(); - final double dist = location.distanceSq( point ); - if ( dist < min_dist ) - { - min_dist = dist; - target_cell = cell; - } - } - return target_cell; - } - } - - /* - * PRIVATE STATIC METHODS - */ - - private static List< mxCell > getSelectionVertices( final mxGraph graph ) - { - // Build selection categories - final Object[] selection = graph.getSelectionCells(); - final ArrayList< mxCell > vertices = new ArrayList<>(); - for ( final Object obj : selection ) - { - final mxCell cell = ( mxCell ) obj; - if ( cell.isVertex() ) - vertices.add( cell ); - } - return vertices; - } - - private static List< mxCell > getSelectionEdges( final mxGraph graph ) - { - // Build selection categories - final Object[] selection = graph.getSelectionCells(); - final ArrayList< mxCell > edges = new ArrayList<>(); - for ( final Object obj : selection ) - { - final mxCell cell = ( mxCell ) obj; - if ( cell.isEdge() ) - edges.add( cell ); - } - return edges; - } -} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeFactory.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeFactory.java deleted file mode 100644 index 16201a173..000000000 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeFactory.java +++ /dev/null @@ -1,72 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.visualization.trackscheme; - -import javax.swing.ImageIcon; - -import org.scijava.plugin.Plugin; - -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.Settings; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; -import fiji.plugin.trackmate.visualization.TrackMateModelView; -import fiji.plugin.trackmate.visualization.ViewFactory; - -/* - * We annotate the TrackScheme factory to be NOT visible, - * because we do not want it to show in the GUI menu. - */ -@Plugin( type = ViewFactory.class, visible = false ) -public class TrackSchemeFactory implements ViewFactory -{ - - @Override - public TrackMateModelView create( final Model model, final Settings settings, final SelectionModel selectionModel, final DisplaySettings displaySettings ) - { - return new TrackScheme( model, selectionModel, displaySettings ); - } - - @Override - public String getName() - { - return "TrackScheme"; - } - - @Override - public String getKey() - { - return TrackScheme.KEY; - } - - @Override - public ImageIcon getIcon() - { - return null; - } - - @Override - public String getInfoText() - { - return "Not redacted!"; - } -} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeFrame.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeFrame.java index 96843636c..141eb2503 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeFrame.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeFrame.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 * . @@ -40,8 +40,8 @@ import com.mxgraph.swing.handler.mxRubberband; import fiji.plugin.trackmate.Logger; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; -import fiji.plugin.trackmate.util.TrackNavigator; public class TrackSchemeFrame extends JFrame { @@ -101,7 +101,8 @@ public void init( final JGraphXAdapter lGraph ) graphComponent = createGraphComponent(); // Add the info pane - infoPane = new InfoPane( trackScheme.getModel(), trackScheme.getSelectionModel() ); + final GuiModel guiModel = trackScheme.getGuiModel(); + infoPane = new InfoPane( guiModel.getModel(), guiModel.getSelectionModel() ); // Add the graph outline final mxGraphOutline graphOutline = new mxGraphOutline( graphComponent ); @@ -113,10 +114,6 @@ public void init( final JGraphXAdapter lGraph ) final JSplitPane splitPane = new JSplitPane( JSplitPane.HORIZONTAL_SPLIT, inner, graphComponent ); splitPane.setDividerLocation( 170 ); getContentPane().add( splitPane, BorderLayout.CENTER ); - - final TrackSchemeKeyboardHandler keyboardHandler = new TrackSchemeKeyboardHandler( graphComponent, new TrackNavigator( trackScheme.getModel(), trackScheme.getSelectionModel() ) ); - keyboardHandler.installKeyboardActions( graphComponent ); - keyboardHandler.installKeyboardActions( infoPane ); } /* 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 50017239f..0309a7b8a 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeGraphComponent.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeGraphComponent.java @@ -59,6 +59,8 @@ import com.mxgraph.view.mxGraph; import com.mxgraph.view.mxGraphView; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; public class TrackSchemeGraphComponent extends mxGraphComponent implements mxIEventListener @@ -514,7 +516,8 @@ public void mouseClicked( final MouseEvent event ) if ( column >= columnWidths.length ) return; - final String oldName = trackScheme.getModel().getTrackModel().name( columnTrackIDs[ column ] ); + final GuiModel guiModel = trackScheme.getGuiModel(); + final String oldName = guiModel.getModel().getTrackModel().name( columnTrackIDs[ column ] ); final Integer trackID = columnTrackIDs[ column ]; int cwidth = columnWidths[ column ] * xcs; @@ -552,10 +555,19 @@ public void actionPerformed( final ActionEvent arg0 ) { // Prevent setting the name of a track that has // been deleted. - if ( trackScheme.getModel().getTrackModel().unsortedTrackIDs( false ).contains( trackID ) ) + if ( guiModel.getModel().getTrackModel().unsortedTrackIDs( false ).contains( trackID ) ) { final String newname = textArea.getText(); - trackScheme.getModel().getTrackModel().setName( trackID, newname ); + final Model model = guiModel.getModel(); + model.beginUpdate(); + try + { + model.setTrackName( trackID, newname ); + } + finally + { + model.endUpdate(); + } } scrollPane.remove( textArea ); ColumnHeader.this.remove( scrollPane ); @@ -597,7 +609,8 @@ public String getToolTipText( final MouseEvent event ) if ( index == 0 ) index = 1; - String columnName = trackScheme.getModel().getTrackModel().name( columnTrackIDs[ index - 1 ] ); + final GuiModel guiModel = trackScheme.getGuiModel(); + String columnName = guiModel.getModel().getTrackModel().name( columnTrackIDs[ index - 1 ] ); if ( null == columnName ) columnName = "Name not set"; @@ -642,7 +655,8 @@ public void paint( final Graphics g ) if ( minx > paintBounds.x + paintBounds.width || maxx < paintBounds.x ) continue; - String columnName = trackScheme.getModel().getTrackModel().name( columnTrackIDs[ i ] ); + final GuiModel guiModel = trackScheme.getGuiModel(); + String columnName = guiModel.getModel().getTrackModel().name( columnTrackIDs[ i ] ); if ( null == columnName ) columnName = "Name not set"; diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeKeyboardHandler.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeKeyboardHandler.java deleted file mode 100644 index 52cc855b9..000000000 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeKeyboardHandler.java +++ /dev/null @@ -1,195 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.visualization.trackscheme; - -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; - -import javax.swing.AbstractAction; -import javax.swing.ActionMap; -import javax.swing.InputMap; -import javax.swing.JComponent; -import javax.swing.KeyStroke; -import javax.swing.SwingUtilities; -import javax.swing.UIManager; - -import com.mxgraph.swing.util.mxGraphActions; - -import fiji.plugin.trackmate.util.TrackNavigator; - -public class TrackSchemeKeyboardHandler -{ - - private final TrackNavigator navigator; - - private final TrackSchemeGraphComponent graphComponent; - - public TrackSchemeKeyboardHandler( final TrackSchemeGraphComponent graphComponent, final TrackNavigator navigator ) - { - this.graphComponent = graphComponent; - this.navigator = navigator; - } - - public void installKeyboardActions( final JComponent component ) - { - final InputMap inputMap = getInputMap( JComponent.WHEN_FOCUSED ); - SwingUtilities.replaceUIInputMap( component, JComponent.WHEN_FOCUSED, inputMap ); - SwingUtilities.replaceUIActionMap( component, createActionMap() ); - } - - protected InputMap getInputMap( final int condition ) - { - final InputMap map; - if ( condition == JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ) - map = ( InputMap ) UIManager.get( "ScrollPane.ancestorInputMap" ); - else - map = new InputMap(); - - map.put( KeyStroke.getKeyStroke( "F2" ), "edit" ); - map.put( KeyStroke.getKeyStroke( "DELETE" ), "delete" ); - - map.put( KeyStroke.getKeyStroke( "HOME" ), "home" ); - map.put( KeyStroke.getKeyStroke( "END" ), "end" ); - - map.put( KeyStroke.getKeyStroke( "ADD" ), "zoomIn" ); - map.put( KeyStroke.getKeyStroke( "EQUALS" ), "zoomIn" ); - map.put( KeyStroke.getKeyStroke( "SUBTRACT" ), "zoomOut" ); - map.put( KeyStroke.getKeyStroke( "MINUS" ), "zoomOut" ); - map.put( KeyStroke.getKeyStroke( "shift EQUALS" ), "resetZoom" ); - - map.put( KeyStroke.getKeyStroke( KeyEvent.VK_NUMPAD4, 0 ), "panLeft" ); - map.put( KeyStroke.getKeyStroke( KeyEvent.VK_NUMPAD6, 0 ), "panRight" ); - map.put( KeyStroke.getKeyStroke( KeyEvent.VK_NUMPAD8, 0 ), "panUp" ); - map.put( KeyStroke.getKeyStroke( KeyEvent.VK_NUMPAD2, 0 ), "panDown" ); - map.put( KeyStroke.getKeyStroke( KeyEvent.VK_NUMPAD9, 0 ), "panUpRight" ); - map.put( KeyStroke.getKeyStroke( KeyEvent.VK_NUMPAD3, 0 ), "panDownRight" ); - map.put( KeyStroke.getKeyStroke( KeyEvent.VK_NUMPAD1, 0 ), "panDownLeft" ); - map.put( KeyStroke.getKeyStroke( KeyEvent.VK_NUMPAD7, 0 ), "panUpLeft" ); - - map.put( KeyStroke.getKeyStroke( "control A" ), "selectAll" ); - map.put( KeyStroke.getKeyStroke( "control shift A" ), "selectNone" ); - - map.put( KeyStroke.getKeyStroke( "UP" ), "selectPreviousInTime" ); - map.put( KeyStroke.getKeyStroke( "DOWN" ), "selectNextInTime" ); - map.put( KeyStroke.getKeyStroke( "RIGHT" ), "selectNextSibling" ); - map.put( KeyStroke.getKeyStroke( "LEFT" ), "selectPreviousSibling" ); - map.put( KeyStroke.getKeyStroke( "PAGE_DOWN" ), "selectNextTrack" ); - map.put( KeyStroke.getKeyStroke( "PAGE_UP" ), "selectPreviousTrack" ); - - return map; - } - - /** - * Returns the mapping between JTree's input map and JGraph's actions. - * - * @return the action map. - */ - protected ActionMap createActionMap() - { - final ActionMap map = ( ActionMap ) UIManager.get( "ScrollPane.actionMap" ); - - map.put( "edit", TrackSchemeActions.getEditAction( graphComponent ) ); - map.put( "delete", mxGraphActions.getDeleteAction() ); - - map.put( "home", TrackSchemeActions.getHomeAction( graphComponent ) ); - map.put( "end", TrackSchemeActions.getEndAction( graphComponent ) ); - - map.put( "zoomIn", TrackSchemeActions.getZoomInAction( graphComponent ) ); - map.put( "zoomOut", TrackSchemeActions.getZoomOutAction( graphComponent ) ); - map.put( "resetZoom", TrackSchemeActions.getResetZoomAction( graphComponent ) ); - - map.put( "panUp", TrackSchemeActions.getPanUpAction( graphComponent ) ); - map.put( "panDown", TrackSchemeActions.getPanDownAction( graphComponent ) ); - map.put( "panLeft", TrackSchemeActions.getPanLeftAction( graphComponent ) ); - map.put( "panRight", TrackSchemeActions.getPanRightAction( graphComponent ) ); - map.put( "panUpLeft", TrackSchemeActions.getPanUpLeftAction( graphComponent ) ); - map.put( "panDownLeft", TrackSchemeActions.getPanDownLeftAction( graphComponent ) ); - map.put( "panUpRight", TrackSchemeActions.getPanUpRightAction( graphComponent ) ); - map.put( "panDownRight", TrackSchemeActions.getPanDownRightAction( graphComponent ) ); - - map.put( "selectNone", TrackSchemeActions.getSelectNoneAction() ); - map.put( "selectAll", TrackSchemeActions.getSelectAllAction() ); - - map.put( "selectPreviousInTime", new AbstractAction() - { - private static final long serialVersionUID = 1L; - - @Override - public void actionPerformed( final ActionEvent arg0 ) - { - navigator.previousInTime(); - } - } ); - map.put( "selectNextInTime", new AbstractAction() - { - private static final long serialVersionUID = 1L; - - @Override - public void actionPerformed( final ActionEvent arg0 ) - { - navigator.nextInTime(); - } - } ); - map.put( "selectNextSibling", new AbstractAction() - { - private static final long serialVersionUID = 1L; - - @Override - public void actionPerformed( final ActionEvent arg0 ) - { - navigator.nextSibling(); - } - } ); - map.put( "selectPreviousSibling", new AbstractAction() - { - private static final long serialVersionUID = 1L; - - @Override - public void actionPerformed( final ActionEvent arg0 ) - { - navigator.previousSibling(); - } - } ); - map.put( "selectNextTrack", new AbstractAction() - { - private static final long serialVersionUID = 1L; - - @Override - public void actionPerformed( final ActionEvent arg0 ) - { - navigator.nextTrack(); - } - } ); - map.put( "selectPreviousTrack", new AbstractAction() - { - private static final long serialVersionUID = 1L; - - @Override - public void actionPerformed( final ActionEvent arg0 ) - { - navigator.previousTrack(); - } - } ); - - return map; - } -} 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 23fcc8c74..b13d239b0 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 * . @@ -41,6 +41,7 @@ import com.mxgraph.util.mxEventObject; import com.mxgraph.util.mxEventSource.mxIEventListener; +import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.features.manual.ManualEdgeColorAnalyzer; import fiji.plugin.trackmate.features.manual.ManualSpotColorAnalyzerFactory; @@ -82,7 +83,7 @@ private void manualColorEdges( final ArrayList< mxCell > edges ) { final DefaultWeightedEdge edge = trackScheme.getGraph().getEdgeFor( mxCell ); final Double value = Double.valueOf( previousColor.getRGB() ); - trackScheme.getModel().getFeatureModel().putEdgeFeature( edge, ManualEdgeColorAnalyzer.FEATURE, value ); + trackScheme.getGuiModel().getModel().getFeatureModel().putEdgeFeature( edge, ManualEdgeColorAnalyzer.FEATURE, value ); } } @@ -151,13 +152,24 @@ private void multiEditSpotName( final ArrayList< mxCell > vertices, final EventO @Override public void invoke( final Object sender, final mxEventObject evt ) { - for ( final mxCell lCell : vertices ) + final Model model = trackScheme.getGuiModel().getModel(); + model.beginUpdate(); + try { - lCell.setValue( tc.getValue() ); - trackScheme.getGraph().getSpotFor( lCell ).setName( tc.getValue().toString() ); + for ( final mxCell lCell : vertices ) + { + lCell.setValue( tc.getValue() ); + final Spot spot = trackScheme.getGraph().getSpotFor( lCell ); + model.beforeEdit( spot ); // name change undoable + spot.setName( tc.getValue().toString() ); + } + graphComponent.refresh(); + graphComponent.removeListener( this ); + } + finally + { + model.endUpdate(); } - graphComponent.refresh(); - graphComponent.removeListener( this ); } } ); } @@ -281,7 +293,7 @@ public void actionPerformed( final ActionEvent e ) } // Link - final Action linkAction = new AbstractAction( "Link " + trackScheme.getSelectionModel().getSpotSelection().size() + " spots" ) + final Action linkAction = new AbstractAction( "Link " + trackScheme.getGuiModel().getSelectionModel().getSpotSelection().size() + " spots" ) { @Override public void actionPerformed( final ActionEvent e ) @@ -289,10 +301,8 @@ public void actionPerformed( final ActionEvent e ) linkSpots(); } }; - if ( trackScheme.getSelectionModel().getSpotSelection().size() > 1 ) - { + if ( trackScheme.getGuiModel().getSelectionModel().getSpotSelection().size() > 1 ) add( linkAction ); - } } /* @@ -384,7 +394,7 @@ public void actionPerformed( final ActionEvent e ) for ( final mxCell mxCell : edges ) { final DefaultWeightedEdge edge = trackScheme.getGraph().getEdgeFor( mxCell ); - trackScheme.getModel().getFeatureModel().removeEdgeFeature( edge, ManualEdgeColorAnalyzer.FEATURE ); + trackScheme.getGuiModel().getModel().getFeatureModel().removeEdgeFeature( edge, ManualEdgeColorAnalyzer.FEATURE ); } SwingUtilities.invokeLater( new Runnable() diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeToolbar.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeToolbar.java index fee2e40bc..282744206 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeToolbar.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeToolbar.java @@ -310,7 +310,7 @@ public void actionPerformed( final ActionEvent e ) add( loopDisplayDecorationsButton ); // Separator addSeparator(); - add( new SearchBar( trackScheme.getModel(), trackScheme ) ); + add( new SearchBar( trackScheme.getGuiModel().getModel(), trackScheme ) ); add( Box.createHorizontalGlue() ); final Dimension dim = new Dimension( 100, 30 ); diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/AbstractTrackSchemeAction.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/AbstractTrackSchemeAction.java new file mode 100644 index 000000000..c6e31533d --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/AbstractTrackSchemeAction.java @@ -0,0 +1,49 @@ +package fiji.plugin.trackmate.visualization.trackscheme.behaviours; + +import java.util.ArrayList; +import java.util.List; + +import org.scijava.ui.behaviour.util.AbstractNamedAction; + +import com.mxgraph.model.mxCell; +import com.mxgraph.view.mxGraph; + +public abstract class AbstractTrackSchemeAction extends AbstractNamedAction +{ + + private static final long serialVersionUID = 1L; + + protected AbstractTrackSchemeAction( final String name ) + { + super( name ); + } + + protected List< mxCell > getSelectionVertices( final mxGraph graph ) + { + // Build selection categories + final Object[] selection = graph.getSelectionCells(); + final ArrayList< mxCell > vertices = new ArrayList<>(); + for ( final Object obj : selection ) + { + final mxCell cell = ( mxCell ) obj; + if ( cell.isVertex() ) + vertices.add( cell ); + } + return vertices; + } + + protected List< mxCell > getSelectionEdges( final mxGraph graph ) + { + // Build selection categories + final Object[] selection = graph.getSelectionCells(); + final ArrayList< mxCell > edges = new ArrayList<>(); + for ( final Object obj : selection ) + { + final mxCell cell = ( mxCell ) obj; + if ( cell.isEdge() ) + edges.add( cell ); + } + return edges; + } + +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/EditNameAction.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/EditNameAction.java new file mode 100644 index 000000000..b1e3069bb --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/EditNameAction.java @@ -0,0 +1,113 @@ +package fiji.plugin.trackmate.visualization.trackscheme.behaviours; + +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.geom.Point2D; +import java.util.List; + +import com.mxgraph.model.mxCell; +import com.mxgraph.util.mxEvent; +import com.mxgraph.util.mxEventObject; +import com.mxgraph.util.mxEventSource.mxIEventListener; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.visualization.trackscheme.JGraphXAdapter; +import fiji.plugin.trackmate.visualization.trackscheme.TrackSchemeGraphComponent; + +public class EditNameAction extends AbstractTrackSchemeAction +{ + + private static final long serialVersionUID = 1L; + + private final TrackSchemeGraphComponent graphComponent; + + private final Model model; + + public EditNameAction( final Model model, final TrackSchemeGraphComponent graphComponent ) + { + super( TrackSchemeActions.EDIT_NAME ); + this.model = model; + this.graphComponent = graphComponent; + } + + @Override + public void actionPerformed( final ActionEvent e ) + { + multiEditSpotName( graphComponent, e ); + } + + private void multiEditSpotName( final TrackSchemeGraphComponent lGraphComponent, final ActionEvent triggerEvent ) + { + /* + * We want to display the editing window in the cell 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 JGraphXAdapter graph = lGraphComponent.getGraph(); + final List< mxCell > vertices = getSelectionVertices( graph ); + if ( vertices.isEmpty() ) + return; + + final Point mousePosition = lGraphComponent.getMousePosition(); + final mxCell tc; + if ( null != mousePosition ) + tc = getClosestCell( vertices, mousePosition ); + else + tc = vertices.get( 0 ); + vertices.remove( tc ); + + lGraphComponent.startEditingAtCell( tc, triggerEvent ); + lGraphComponent.addListener( mxEvent.LABEL_CHANGED, new mxIEventListener() + { + + @Override + public void invoke( final Object sender, final mxEventObject evt ) + { + model.beginUpdate(); + try + { + for ( final mxCell cell : vertices ) + { + cell.setValue( tc.getValue() ); + final Spot spot = graph.getSpotFor( cell ); + model.beforeEdit( spot ); // name change undoable + spot.setName( tc.getValue().toString() ); + } + lGraphComponent.refresh(); + lGraphComponent.removeListener( this ); + } + finally + { + model.endUpdate(); + } + } + } ); + } + + /** + * Return, from the given list of cell, the one which is the closer to the + * point of this instance. + * + * @param point + */ + private mxCell getClosestCell( final Iterable< mxCell > vertices, final Point2D point ) + { + double min_dist = Double.POSITIVE_INFINITY; + mxCell target_cell = null; + for ( final mxCell cell : vertices ) + { + final Point location = cell.getGeometry().getPoint(); + final double dist = location.distanceSq( point ); + if ( dist < min_dist ) + { + min_dist = dist; + target_cell = cell; + } + } + return target_cell; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/HomingActions.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/HomingActions.java new file mode 100644 index 000000000..3ecf73a46 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/HomingActions.java @@ -0,0 +1,141 @@ +package fiji.plugin.trackmate.visualization.trackscheme.behaviours; + +import java.awt.event.ActionEvent; +import java.util.List; + +import com.mxgraph.model.mxCell; +import com.mxgraph.model.mxICell; + +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.visualization.trackscheme.JGraphXAdapter; +import fiji.plugin.trackmate.visualization.trackscheme.TrackSchemeGraphComponent; + +public class HomingActions +{ + + /** + * Centers the view to the first cell in selection, sorted by frame number. + */ + public static class HomeAction extends AbstractTrackSchemeAction + { + + private static final long serialVersionUID = 1L; + + private final TrackSchemeGraphComponent graphComponent; + + public HomeAction( final TrackSchemeGraphComponent graphComponent ) + { + super( TrackSchemeActions.CENTER_ON_FIRST_SELECTED ); + this.graphComponent = graphComponent; + } + + @Override + public void actionPerformed( final ActionEvent e ) + { + + mxCell cell = null; + final JGraphXAdapter graph = graphComponent.getGraph(); + final List< mxCell > vertices = getSelectionVertices( graph ); + if ( !vertices.isEmpty() ) + { + int minFrame = Integer.MAX_VALUE; + for ( final mxCell mxCell : vertices ) + { + final int frame = graph.getSpotFor( mxCell ).getFeature( Spot.FRAME ).intValue(); + if ( frame < minFrame ) + { + minFrame = frame; + cell = mxCell; + } + } + } + else + { + final List< mxCell > edges = getSelectionEdges( graph ); + if ( !edges.isEmpty() ) + { + int minFrame = Integer.MAX_VALUE; + for ( final mxCell mxCell : edges ) + { + final mxICell target = mxCell.getTarget(); + final int frame = graph.getSpotFor( target ).getFeature( Spot.FRAME ).intValue(); + if ( frame < minFrame ) + { + minFrame = frame; + cell = mxCell; + } + } + cell = edges.get( edges.size() - 1 ); + } + else + { + return; + } + } + graphComponent.scrollCellToVisible( cell, true ); + } + } + + /** + * Centers the view to the last cell in selection, sorted by frame number. + */ + public static class EndAction extends AbstractTrackSchemeAction + { + + private static final long serialVersionUID = 1L; + + private final TrackSchemeGraphComponent graphComponent; + + public EndAction( final TrackSchemeGraphComponent graphComponent ) + { + super( TrackSchemeActions.CENTER_ON_LAST_SELECTED ); + this.graphComponent = graphComponent; + } + + @Override + public void actionPerformed( final ActionEvent e ) + { + mxCell cell = null; + final JGraphXAdapter graph = graphComponent.getGraph(); + final List< mxCell > vertices = getSelectionVertices( graph ); + + if ( !vertices.isEmpty() ) + { + int maxFrame = Integer.MIN_VALUE; + for ( final mxCell mxCell : vertices ) + { + final int frame = graph.getSpotFor( mxCell ).getFeature( Spot.FRAME ).intValue(); + if ( frame > maxFrame ) + { + maxFrame = frame; + cell = mxCell; + } + } + } + else + { + final List< mxCell > edges = getSelectionEdges( graph ); + if ( !edges.isEmpty() ) + { + int maxFrame = Integer.MIN_VALUE; + for ( final mxCell mxCell : edges ) + { + final mxICell target = mxCell.getTarget(); + final int frame = graph.getSpotFor( target ).getFeature( Spot.FRAME ).intValue(); + if ( frame > maxFrame ) + { + maxFrame = frame; + cell = mxCell; + } + } + cell = edges.get( edges.size() - 1 ); + } + else + { + return; + } + } + graphComponent.scrollCellToVisible( cell, true ); + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/PanAction.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/PanAction.java new file mode 100644 index 000000000..3a0a32020 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/PanAction.java @@ -0,0 +1,37 @@ +package fiji.plugin.trackmate.visualization.trackscheme.behaviours; + +import java.awt.Rectangle; +import java.awt.event.ActionEvent; + +import org.scijava.ui.behaviour.util.AbstractNamedAction; + +import fiji.plugin.trackmate.visualization.trackscheme.TrackSchemeGraphComponent; + +public class PanAction extends AbstractNamedAction +{ + + private static final long serialVersionUID = 1L; + + private final int amountx; + + private final int amounty; + + private final TrackSchemeGraphComponent graphComponent; + + public PanAction( final String name, final int amountx, final int amounty, final TrackSchemeGraphComponent graphComponent ) + { + super( name ); + this.graphComponent = graphComponent; + this.amountx = amountx; + this.amounty = amounty; + } + + @Override + public void actionPerformed( final ActionEvent e ) + { + final Rectangle r = graphComponent.getViewport().getViewRect(); + final int right = r.x + ( ( amountx < 0 ) ? 0 : r.width ) + amountx; + final int bottom = r.y + ( ( amounty < 0 ) ? 0 : r.height ) + amounty; + graphComponent.getGraphControl().scrollRectToVisible( new Rectangle( right, bottom, 0, 0 ) ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/TrackSchemeActions.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/TrackSchemeActions.java new file mode 100644 index 000000000..49d346e0d --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/behaviours/TrackSchemeActions.java @@ -0,0 +1,100 @@ +package fiji.plugin.trackmate.visualization.trackscheme.behaviours; + +import org.scijava.plugin.Plugin; +import org.scijava.ui.behaviour.io.gui.CommandDescriptionProvider; +import org.scijava.ui.behaviour.io.gui.CommandDescriptions; +import org.scijava.ui.behaviour.util.Actions; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.visualization.trackscheme.TrackSchemeGraphComponent; +import fiji.plugin.trackmate.visualization.ui.KeyConfigContexts; + +public class TrackSchemeActions +{ + + /** + * When panning with the keyboard, by how much pixels to move. + */ + private static final int PAN_AMOUNT = 100; + + static final String EDIT_NAME = "edit name"; + private static final String[] EDIT_NAME_KEYS = new String[] { "F2" }; + + private static final String ZOOM_IN = "zoom in"; + private static final String[] ZOOM_IN_KEYS = new String[] { "ADD", "EQUALS" }; + private static final String ZOOM_OUT = "zoom out"; + private static final String[] ZOOM_OUT_KEYS = new String[] { "SUBTRACT", "MINUS" }; + private static final String RESET_ZOOM = "reset zoom"; + private static final String[] RESET_ZOOM_KEYS = new String[] { "shift EQUALS", "R" }; + + static final String CENTER_ON_FIRST_SELECTED = "center on first selected"; + private static final String[] CENTER_ON_FIRST_SELECTED_KEYS = new String[] { "HOME", "C" }; + static final String CENTER_ON_LAST_SELECTED = "center on last selected"; + private static final String[] CENTER_ON_LAST_SELECTED_KEYS = new String[] { "END", "shift C" }; + + private static final String PAN_LEFT = "pan left"; + private static final String[] PAN_LEFT_KEYS = new String[] { "NUMPAD4" }; + private static final String PAN_RIGHT = "pan right"; + private static final String[] PAN_RIGHT_KEYS = new String[] { "NUMPAD6" }; + private static final String PAN_UP = "pan up"; + private static final String[] PAN_UP_KEYS = new String[] { "NUMPAD8" }; + private static final String PAN_DOWN = "pan down"; + private static final String[] PAN_DOWN_KEYS = new String[] { "NUMPAD2" }; + private static final String PAN_UP_RIGHT = "pan up right"; + private static final String[] PAN_UP_RIGHT_KEYS = new String[] { "NUMPAD9" }; + private static final String PAN_DOWN_RIGHT = "pan down right"; + private static final String[] PAN_DOWN_RIGHT_KEYS = new String[] { "NUMPAD3" }; + private static final String PAN_DOWN_LEFT = "pan down left"; + private static final String[] PAN_DOWN_LEFT_KEYS = new String[] { "NUMPAD1" }; + private static final String PAN_UP_LEFT = "pan up left"; + private static final String[] PAN_UP_LEFT_KEYS = new String[] { "NUMPAD7" }; + + public static final void install( final Actions actions, final Model model, final TrackSchemeGraphComponent graphComponent ) + { + actions.namedAction( new EditNameAction( model, graphComponent ), EDIT_NAME_KEYS ); + + actions.namedAction( new HomingActions.HomeAction( graphComponent ), CENTER_ON_FIRST_SELECTED_KEYS ); + actions.namedAction( new HomingActions.EndAction( graphComponent ), CENTER_ON_LAST_SELECTED_KEYS ); + + actions.runnableAction( () -> graphComponent.zoomIn(), ZOOM_IN, ZOOM_IN_KEYS ); + actions.runnableAction( () -> graphComponent.zoomOut(), ZOOM_OUT, ZOOM_OUT_KEYS ); + actions.runnableAction( () -> graphComponent.zoomActual(), RESET_ZOOM, RESET_ZOOM_KEYS ); + + actions.namedAction( new PanAction( PAN_LEFT, -PAN_AMOUNT, 0, graphComponent ), PAN_LEFT_KEYS ); + actions.namedAction( new PanAction( PAN_RIGHT, PAN_AMOUNT, 0, graphComponent ), PAN_RIGHT_KEYS ); + actions.namedAction( new PanAction( PAN_UP, 0, -PAN_AMOUNT, graphComponent ), PAN_UP_KEYS ); + actions.namedAction( new PanAction( PAN_DOWN, 0, PAN_AMOUNT, graphComponent ), PAN_DOWN_KEYS ); + actions.namedAction( new PanAction( PAN_UP_RIGHT, PAN_AMOUNT, -PAN_AMOUNT, graphComponent ), PAN_UP_RIGHT_KEYS ); + actions.namedAction( new PanAction( PAN_DOWN_RIGHT, PAN_AMOUNT, PAN_AMOUNT, graphComponent ), PAN_DOWN_RIGHT_KEYS ); + actions.namedAction( new PanAction( PAN_DOWN_LEFT, -PAN_AMOUNT, PAN_AMOUNT, graphComponent ), PAN_DOWN_LEFT_KEYS ); + actions.namedAction( new PanAction( PAN_UP_LEFT, -PAN_AMOUNT, -PAN_AMOUNT, graphComponent ), PAN_UP_LEFT_KEYS ); + } + + @Plugin( type = CommandDescriptionProvider.class ) + public static class Descriptions extends CommandDescriptionProvider + { + public Descriptions() + { + super( KeyConfigContexts.KEY_CONFIG_SCOPE, KeyConfigContexts.TRACKSCHEME ); + } + + @Override + public void getCommandDescriptions( final CommandDescriptions descriptions ) + { + descriptions.add( EDIT_NAME, EDIT_NAME_KEYS, "Edit the name of the selected spots." ); + descriptions.add( ZOOM_IN, ZOOM_IN_KEYS, "Zoom in." ); + descriptions.add( ZOOM_OUT, ZOOM_OUT_KEYS, "Zoom out." ); + descriptions.add( RESET_ZOOM, RESET_ZOOM_KEYS, "Reset zoom." ); + descriptions.add( CENTER_ON_FIRST_SELECTED, CENTER_ON_FIRST_SELECTED_KEYS, "Center the view on the first selected spot." ); + descriptions.add( CENTER_ON_LAST_SELECTED, CENTER_ON_LAST_SELECTED_KEYS, "Center the view on the last selected spot." ); + descriptions.add( PAN_LEFT, PAN_LEFT_KEYS, "Pan left." ); + descriptions.add( PAN_RIGHT, PAN_RIGHT_KEYS, "Pan right." ); + descriptions.add( PAN_UP, PAN_UP_KEYS, "Pan up." ); + descriptions.add( PAN_DOWN, PAN_DOWN_KEYS, "Pan down." ); + descriptions.add( PAN_UP_RIGHT, PAN_UP_RIGHT_KEYS, "Pan up and right." ); + descriptions.add( PAN_DOWN_RIGHT, PAN_DOWN_RIGHT_KEYS, "Pan down and right." ); + descriptions.add( PAN_DOWN_LEFT, PAN_DOWN_LEFT_KEYS, "Pan down and left." ); + descriptions.add( PAN_UP_LEFT, PAN_UP_LEFT_KEYS, "Pan up and left." ); + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/ui/KeyConfigContexts.java b/src/main/java/fiji/plugin/trackmate/visualization/ui/KeyConfigContexts.java new file mode 100644 index 000000000..1b1858ffe --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/ui/KeyConfigContexts.java @@ -0,0 +1,46 @@ +package fiji.plugin.trackmate.visualization.ui; + +import org.scijava.ui.behaviour.io.gui.CommandDescriptionProvider.Scope; + +import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; + +public interface KeyConfigContexts +{ + + /** + * The scope of the TrackMate app. + */ + Scope KEY_CONFIG_SCOPE = new Scope( "TrackMate" ); + + /** + * The action or behaviour applies to the whole app. + */ + String TRACKMATE = "trackmate"; + + /** + * The action or behaviour applies to the {@link HyperStackDisplayer} view + * (the main view). + */ + String HYPERSTACK_DISPLAYER = "trackmate-main-view"; + + /** + * The action or behaviour applies to TrackScheme views. + */ + String TRACKSCHEME = "trackscheme"; + + /** + * The action or behaviour applies to the all spot table views. + */ + String ALL_SPOTS_TABLE = "all-spots-table"; + + /** + * The action or behaviour applies to the track table views. + */ + String TRACK_TABLE = "track-table"; + + /** + * The action or behaviour applies to the BVV views. + */ + String BIGVOLUMEVIEWER = "bigvolumeviewer"; + +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/ui/TrackMateActions.java b/src/main/java/fiji/plugin/trackmate/visualization/ui/TrackMateActions.java new file mode 100644 index 000000000..317d7de0e --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/ui/TrackMateActions.java @@ -0,0 +1,172 @@ +package fiji.plugin.trackmate.visualization.ui; + +import java.awt.HeadlessException; +import java.awt.Toolkit; +import java.awt.event.InputEvent; +import java.util.ArrayList; +import java.util.List; + +import org.jgrapht.graph.DefaultWeightedEdge; +import org.scijava.plugin.Plugin; +import org.scijava.ui.behaviour.io.gui.CommandDescriptionProvider; +import org.scijava.ui.behaviour.io.gui.CommandDescriptions; +import org.scijava.ui.behaviour.util.Actions; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.util.TrackNavigator; + +public class TrackMateActions +{ + + private static final String UNDO_ACTION = "undo"; + private static final String REDO_ACTION = "redo"; + + private static final String[] UNDO_ACTION_KEYS; + private static final String[] REDO_ACTION_KEYS; + + private static final String NAVIGATE_TO_PARENT = "navigate to parent"; + private static final String NAVIGATE_TO_CHILD = "navigate to child"; + private static final String NAVIGATE_TO_PREVIOUS_SIBLING = "navigate to previous sibling"; + private static final String NAVIGATE_TO_NEXT_SIBLING = "navigate to next sibling"; + private static final String NAVIGATE_TO_ROOT = "navigate to root"; + private static final String NAVIGATE_TO_LEAF = "navigate to leaf"; + private static final String NAVIGATE_TO_PREVIOUS_TRACK = "navigate to previous track"; + private static final String NAVIGATE_TO_NEXT_TRACK = "navigate to next track"; + + private static final String[] NAVIGATE_TO_PARENT_KEYS = new String[] { "UP" }; + private static final String[] NAVIGATE_TO_CHILD_KEYS = new String[] { "DOWN" }; + private static final String[] NAVIGATE_TO_PREVIOUS_SIBLING_KEYS = new String[] { "LEFT" }; + private static final String[] NAVIGATE_TO_NEXT_SIBLING_KEYS = new String[] { "RIGHT" }; + private static final String[] NAVIGATE_TO_ROOT_KEYS = new String[] { "HOME", "meta UP" }; + private static final String[] NAVIGATE_TO_LEAF_KEYS = new String[] { "END", "meta DOWN" }; + private static final String[] NAVIGATE_TO_PREVIOUS_TRACK_KEYS = new String[] { "PAGE_UP" }; + private static final String[] NAVIGATE_TO_NEXT_TRACK_KEYS = new String[] { "PAGE_DOWN" }; + + private static final String DELETE_SELECTION = "delete selection"; + private static final String[] DELETE_SELECTION_KEYS = new String[] { "BACK_SPACE", "DELETE" }; + + private static final String SELECT_ALL = "select all"; + private static final String SELECT_ALL_SPOTS = "select all spots"; + private static final String SELECT_ALL_LINKS = "select all links"; + private static final String[] SELECT_ALL_KEYS; + private static final String[] SELECT_ALL_SPOTS_KEYS; + private static final String[] SELECT_ALL_LINKS_KEYS; + + static + { + int menuMask; + try + { + menuMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); + } + catch ( final HeadlessException e ) + { + // Default to Ctrl in headless environments (CI, tests) + menuMask = InputEvent.CTRL_DOWN_MASK; + } + final String modifier = ( menuMask == InputEvent.CTRL_DOWN_MASK ) ? "ctrl" : "meta"; + UNDO_ACTION_KEYS = new String[] { modifier + " Z" }; + REDO_ACTION_KEYS = new String[] { modifier + " Y", modifier + " shift Z" }; + SELECT_ALL_KEYS = new String[] { modifier + " A" }; + SELECT_ALL_SPOTS_KEYS = new String[] { modifier + " shift A" }; + SELECT_ALL_LINKS_KEYS = new String[] { modifier + " alt A" }; + } + + public static final void install( final Actions actions, final Model model, final SelectionModel selectionModel ) + { + final TrackNavigator trackNavigator = new TrackNavigator( model, selectionModel ); + + // Undo / redo + actions.runnableAction( () -> model.undo(), UNDO_ACTION, UNDO_ACTION_KEYS ); + actions.runnableAction( () -> model.redo(), REDO_ACTION, REDO_ACTION_KEYS ); + + // Navigate + actions.runnableAction( () -> trackNavigator.previousInTime(), NAVIGATE_TO_PARENT, NAVIGATE_TO_PARENT_KEYS ); + actions.runnableAction( () -> trackNavigator.nextInTime(), NAVIGATE_TO_CHILD, NAVIGATE_TO_CHILD_KEYS ); + actions.runnableAction( () -> trackNavigator.previousSibling(), NAVIGATE_TO_PREVIOUS_SIBLING, NAVIGATE_TO_PREVIOUS_SIBLING_KEYS ); + actions.runnableAction( () -> trackNavigator.nextSibling(), NAVIGATE_TO_NEXT_SIBLING, NAVIGATE_TO_NEXT_SIBLING_KEYS ); + actions.runnableAction( () -> trackNavigator.root(), NAVIGATE_TO_ROOT, NAVIGATE_TO_ROOT_KEYS ); + actions.runnableAction( () -> trackNavigator.leaf(), NAVIGATE_TO_LEAF, NAVIGATE_TO_LEAF_KEYS ); + actions.runnableAction( () -> trackNavigator.previousTrack(), NAVIGATE_TO_PREVIOUS_TRACK, NAVIGATE_TO_PREVIOUS_TRACK_KEYS ); + actions.runnableAction( () -> trackNavigator.nextTrack(), NAVIGATE_TO_NEXT_TRACK, NAVIGATE_TO_NEXT_TRACK_KEYS ); + + // Delete selection + actions.runnableAction( () -> deleteSelection( model, selectionModel ), DELETE_SELECTION, DELETE_SELECTION_KEYS ); + + // Select all + actions.runnableAction( () -> selectAll( model, selectionModel ), SELECT_ALL, SELECT_ALL_KEYS ); + actions.runnableAction( () -> selectAllSpots( model, selectionModel ), SELECT_ALL_SPOTS, SELECT_ALL_SPOTS_KEYS ); + actions.runnableAction( () -> selectAllLinks( model, selectionModel ), SELECT_ALL_LINKS, SELECT_ALL_LINKS_KEYS ); + } + + private static void selectAll( final Model model, final SelectionModel selectionModel ) + { + selectAllSpots( model, selectionModel ); + selectAllLinks( model, selectionModel ); + } + + private static void selectAllSpots( final Model model, final SelectionModel selectionModel ) + { + final List< Spot > spotsToAdd = new ArrayList<>(); + model.getSpots().iterable( true ).forEach( spotsToAdd::add ); + selectionModel.addSpotToSelection( spotsToAdd ); + } + + private static void selectAllLinks( final Model model, final SelectionModel selectionModel ) + { + final List< DefaultWeightedEdge > edgesToAdd = new ArrayList<>(); + model.getTrackModel().edgeSet().forEach( edgesToAdd::add ); + selectionModel.addEdgeToSelection( edgesToAdd ); + } + + private static void deleteSelection( final Model model, final SelectionModel selectionModel ) + { + final ArrayList< Spot > spotSelection = new ArrayList<>( selectionModel.getSpotSelection() ); + final ArrayList< DefaultWeightedEdge > edgeSelection = new ArrayList<>( selectionModel.getEdgeSelection() ); + model.beginUpdate(); + try + { + selectionModel.clearSelection(); + for ( final DefaultWeightedEdge edge : edgeSelection ) + model.removeEdge( edge ); + for ( final Spot spot : spotSelection ) + model.removeSpot( spot ); + } + finally + { + model.endUpdate(); + } + } + + @Plugin( type = CommandDescriptionProvider.class ) + public static class Descriptions extends CommandDescriptionProvider + { + public Descriptions() + { + super( KeyConfigContexts.KEY_CONFIG_SCOPE, KeyConfigContexts.TRACKMATE ); + } + + @Override + public void getCommandDescriptions( final CommandDescriptions descriptions ) + { + descriptions.add( UNDO_ACTION, UNDO_ACTION_KEYS, "Undo the last edit." ); + descriptions.add( REDO_ACTION, REDO_ACTION_KEYS, "Redo the last undone edit." ); + + descriptions.add( NAVIGATE_TO_PARENT, NAVIGATE_TO_PARENT_KEYS, "Navigate to the parent of the selected spot." ); + descriptions.add( NAVIGATE_TO_CHILD, NAVIGATE_TO_CHILD_KEYS, "Navigate to the child of the selected spot." ); + descriptions.add( NAVIGATE_TO_PREVIOUS_SIBLING, NAVIGATE_TO_PREVIOUS_SIBLING_KEYS, "Navigate to the previous sibling of the selected spot." ); + descriptions.add( NAVIGATE_TO_NEXT_SIBLING, NAVIGATE_TO_NEXT_SIBLING_KEYS, "Navigate to the next sibling of the selected spot." ); + descriptions.add( NAVIGATE_TO_ROOT, NAVIGATE_TO_ROOT_KEYS, "Navigate to the root of the current track." ); + descriptions.add( NAVIGATE_TO_LEAF, NAVIGATE_TO_LEAF_KEYS, "Navigate to the leaf of the current track." ); + descriptions.add( NAVIGATE_TO_PREVIOUS_TRACK, NAVIGATE_TO_PREVIOUS_TRACK_KEYS, "Navigate to the previous track." ); + descriptions.add( NAVIGATE_TO_NEXT_TRACK, NAVIGATE_TO_NEXT_TRACK_KEYS, "Navigate to the next track." ); + + descriptions.add( DELETE_SELECTION, DELETE_SELECTION_KEYS, "Delete the selected spots and edges." ); + descriptions.add( SELECT_ALL, SELECT_ALL_KEYS, "Select all spots and edges." ); + descriptions.add( SELECT_ALL_SPOTS, SELECT_ALL_SPOTS_KEYS, "Select all spots." ); + descriptions.add( SELECT_ALL_LINKS, SELECT_ALL_LINKS_KEYS, "Select all edges." ); + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/ui/TrackMateKeymapManager.java b/src/main/java/fiji/plugin/trackmate/visualization/ui/TrackMateKeymapManager.java new file mode 100644 index 000000000..6d4d2f8cb --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/ui/TrackMateKeymapManager.java @@ -0,0 +1,30 @@ +package fiji.plugin.trackmate.visualization.ui; + +import java.io.File; + +import org.scijava.Context; +import org.scijava.ui.behaviour.io.gui.CommandDescriptionsBuilder; + +import bdv.ui.keymap.KeymapManager; +import fiji.plugin.trackmate.util.TMUtils; + +public class TrackMateKeymapManager extends KeymapManager +{ + + private static final String KEYMAP_HOME = new File( System.getProperty( "user.home" ), ".trackmate" ).getAbsolutePath(); + + public TrackMateKeymapManager() + { + super( KEYMAP_HOME ); + } + + @Override + public synchronized void discoverCommandDescriptions() + { + final CommandDescriptionsBuilder builder = new CommandDescriptionsBuilder(); + final Context context = TMUtils.getContext(); + context.inject( builder ); + builder.discoverProviders( KeyConfigContexts.KEY_CONFIG_SCOPE ); + setCommandDescriptions( builder.build() ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/ui/package-info.java b/src/main/java/fiji/plugin/trackmate/visualization/ui/package-info.java new file mode 100644 index 000000000..19e6a9912 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/ui/package-info.java @@ -0,0 +1,5 @@ +/** + * This package contains the classes used in conjunction with the ui-behaviours + * framework, and that are view agnostic. + */ +package fiji.plugin.trackmate.visualization.ui; diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Algae.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Algae.lut deleted file mode 100644 index 90f95a659..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Algae.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 17 36 20 -1 17 37 20 -2 18 37 21 -3 18 38 22 -4 18 39 22 -5 19 40 23 -6 19 41 24 -7 19 42 25 -8 20 43 25 -9 20 43 26 -10 20 44 27 -11 21 45 27 -12 21 46 28 -13 21 47 29 -14 21 48 29 -15 22 49 30 -16 22 49 31 -17 22 50 31 -18 22 51 32 -19 23 52 33 -20 23 53 33 -21 23 54 34 -22 23 55 35 -23 23 55 35 -24 23 56 36 -25 24 57 36 -26 24 58 37 -27 24 59 38 -28 24 60 38 -29 24 61 39 -30 24 61 40 -31 25 62 40 -32 25 63 41 -33 25 64 41 -34 25 65 42 -35 25 66 43 -36 25 67 43 -37 25 67 44 -38 25 68 44 -39 25 69 45 -40 25 70 46 -41 25 71 46 -42 25 72 47 -43 25 73 47 -44 25 73 48 -45 25 74 49 -46 25 75 49 -47 25 76 50 -48 25 77 50 -49 25 78 51 -50 25 79 51 -51 25 80 52 -52 25 80 52 -53 25 81 53 -54 25 82 54 -55 25 83 54 -56 25 84 55 -57 25 85 55 -58 25 86 56 -59 25 87 56 -60 24 87 57 -61 24 88 57 -62 24 89 58 -63 24 90 58 -64 24 91 59 -65 23 92 59 -66 23 93 60 -67 23 94 60 -68 23 94 61 -69 23 95 61 -70 22 96 62 -71 22 97 62 -72 22 98 63 -73 22 99 63 -74 21 100 63 -75 21 101 64 -76 21 102 64 -77 20 102 65 -78 20 103 65 -79 19 104 66 -80 19 105 66 -81 19 106 67 -82 18 107 67 -83 18 108 67 -84 18 109 68 -85 17 110 68 -86 17 110 69 -87 16 111 69 -88 16 112 69 -89 15 113 70 -90 15 114 70 -91 14 115 70 -92 14 116 71 -93 13 117 71 -94 13 118 72 -95 12 119 72 -96 12 119 72 -97 11 120 73 -98 11 121 73 -99 10 122 73 -100 10 123 74 -101 9 124 74 -102 9 125 74 -103 8 126 74 -104 8 127 75 -105 7 128 75 -106 7 129 75 -107 7 129 76 -108 7 130 76 -109 6 131 76 -110 6 132 76 -111 6 133 77 -112 6 134 77 -113 6 135 77 -114 7 136 77 -115 7 137 77 -116 7 138 78 -117 8 138 78 -118 8 139 78 -119 9 140 78 -120 9 141 78 -121 10 142 79 -122 11 143 79 -123 12 144 79 -124 13 145 79 -125 15 146 79 -126 16 146 79 -127 17 147 79 -128 19 148 80 -129 20 149 80 -130 21 150 80 -131 23 151 80 -132 25 152 80 -133 26 153 80 -134 28 153 80 -135 30 154 80 -136 31 155 80 -137 33 156 81 -138 35 157 81 -139 37 157 81 -140 39 158 81 -141 41 159 81 -142 43 160 81 -143 45 161 81 -144 47 161 82 -145 50 162 82 -146 52 163 82 -147 54 164 82 -148 56 164 83 -149 58 165 83 -150 61 166 84 -151 63 167 84 -152 65 167 84 -153 67 168 85 -154 69 169 86 -155 71 169 86 -156 74 170 87 -157 76 171 87 -158 78 171 88 -159 80 172 89 -160 82 173 90 -161 83 174 90 -162 85 174 91 -163 87 175 92 -164 89 176 93 -165 91 176 94 -166 93 177 95 -167 94 178 96 -168 96 178 96 -169 98 179 97 -170 100 180 98 -171 101 180 99 -172 103 181 100 -173 104 182 101 -174 106 183 102 -175 108 183 103 -176 109 184 104 -177 111 185 105 -178 112 185 107 -179 114 186 108 -180 115 187 109 -181 117 188 110 -182 118 188 111 -183 120 189 112 -184 121 190 113 -185 123 191 114 -186 124 191 115 -187 126 192 117 -188 127 193 118 -189 129 194 119 -190 130 194 120 -191 132 195 121 -192 133 196 122 -193 134 197 124 -194 136 197 125 -195 137 198 126 -196 139 199 127 -197 140 200 128 -198 141 200 130 -199 143 201 131 -200 144 202 132 -201 145 203 133 -202 147 203 135 -203 148 204 136 -204 149 205 137 -205 151 206 138 -206 152 207 140 -207 153 207 141 -208 155 208 142 -209 156 209 143 -210 157 210 145 -211 159 211 146 -212 160 211 147 -213 161 212 149 -214 163 213 150 -215 164 214 151 -216 165 215 153 -217 166 215 154 -218 168 216 155 -219 169 217 156 -220 170 218 158 -221 172 219 159 -222 173 219 160 -223 174 220 162 -224 175 221 163 -225 177 222 165 -226 178 223 166 -227 179 224 167 -228 180 225 169 -229 182 225 170 -230 183 226 171 -231 184 227 173 -232 186 228 174 -233 187 229 176 -234 188 230 177 -235 189 231 178 -236 191 231 180 -237 192 232 181 -238 193 233 183 -239 194 234 184 -240 196 235 185 -241 197 236 187 -242 198 237 188 -243 199 238 190 -244 201 239 191 -245 202 239 193 -246 203 240 194 -247 204 241 195 -248 206 242 197 -249 207 243 198 -250 208 244 200 -251 209 245 201 -252 211 246 203 -253 212 247 204 -254 213 248 206 -255 214 249 207 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Amp.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Amp.lut deleted file mode 100644 index 401695c0e..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Amp.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 60 9 17 -1 61 9 18 -2 62 9 19 -3 63 9 19 -4 65 10 20 -5 66 10 20 -6 67 10 21 -7 69 10 22 -8 70 11 22 -9 71 11 23 -10 72 11 24 -11 74 11 24 -12 75 12 25 -13 76 12 25 -14 77 12 26 -15 79 12 27 -16 80 12 27 -17 81 13 28 -18 83 13 28 -19 84 13 29 -20 85 13 29 -21 86 13 30 -22 88 13 30 -23 89 13 31 -24 90 14 32 -25 92 14 32 -26 93 14 32 -27 94 14 33 -28 96 14 33 -29 97 14 34 -30 98 14 34 -31 100 14 35 -32 101 14 35 -33 102 14 36 -34 104 14 36 -35 105 14 37 -36 106 14 37 -37 108 14 37 -38 109 14 38 -39 110 14 38 -40 112 14 38 -41 113 14 39 -42 114 14 39 -43 116 14 39 -44 117 14 39 -45 118 14 40 -46 120 14 40 -47 121 14 40 -48 122 14 40 -49 124 14 40 -50 125 13 41 -51 126 13 41 -52 128 13 41 -53 129 14 41 -54 130 14 41 -55 132 14 41 -56 133 14 41 -57 134 14 41 -58 135 14 41 -59 137 14 41 -60 138 14 41 -61 139 15 41 -62 141 15 40 -63 142 16 40 -64 143 16 40 -65 144 16 40 -66 145 17 40 -67 147 18 40 -68 148 18 40 -69 149 19 39 -70 150 19 39 -71 151 20 39 -72 152 21 39 -73 154 22 38 -74 155 23 38 -75 156 24 38 -76 157 24 38 -77 158 25 38 -78 159 26 37 -79 160 27 37 -80 161 28 37 -81 162 30 37 -82 163 31 37 -83 164 32 36 -84 165 33 36 -85 166 34 36 -86 166 35 36 -87 167 36 36 -88 168 37 36 -89 169 39 36 -90 170 40 36 -91 171 41 36 -92 172 42 36 -93 172 44 36 -94 173 45 36 -95 174 46 36 -96 175 47 36 -97 175 49 36 -98 176 50 36 -99 177 51 37 -100 177 53 37 -101 178 54 37 -102 179 55 38 -103 179 57 38 -104 180 58 38 -105 180 59 39 -106 181 61 40 -107 181 62 40 -108 182 63 41 -109 183 65 41 -110 183 66 42 -111 184 67 43 -112 184 69 44 -113 185 70 44 -114 185 71 45 -115 186 73 46 -116 186 74 47 -117 187 75 48 -118 187 76 49 -119 188 78 50 -120 188 79 51 -121 188 80 52 -122 189 81 53 -123 189 83 54 -124 190 84 55 -125 190 85 56 -126 191 86 57 -127 191 88 58 -128 192 89 60 -129 192 90 61 -130 192 91 62 -131 193 93 63 -132 193 94 64 -133 194 95 65 -134 194 96 67 -135 194 97 68 -136 195 99 69 -137 195 100 70 -138 196 101 71 -139 196 102 73 -140 196 103 74 -141 197 105 75 -142 197 106 77 -143 198 107 78 -144 198 108 79 -145 198 109 80 -146 199 110 82 -147 199 112 83 -148 199 113 84 -149 200 114 86 -150 200 115 87 -151 201 116 88 -152 201 117 90 -153 201 119 91 -154 202 120 92 -155 202 121 94 -156 202 122 95 -157 203 123 96 -158 203 124 98 -159 204 125 99 -160 204 127 100 -161 204 128 102 -162 205 129 103 -163 205 130 105 -164 205 131 106 -165 206 132 107 -166 206 133 109 -167 206 135 110 -168 207 136 111 -169 207 137 113 -170 207 138 114 -171 208 139 116 -172 208 140 117 -173 208 141 119 -174 209 143 120 -175 209 144 121 -176 209 145 123 -177 210 146 124 -178 210 147 126 -179 211 148 127 -180 211 149 128 -181 211 151 130 -182 212 152 131 -183 212 153 133 -184 212 154 134 -185 213 155 136 -186 213 156 137 -187 213 157 138 -188 214 158 140 -189 214 160 141 -190 214 161 143 -191 215 162 144 -192 215 163 146 -193 215 164 147 -194 216 165 148 -195 216 166 150 -196 216 168 151 -197 217 169 153 -198 217 170 154 -199 217 171 156 -200 218 172 157 -201 218 173 159 -202 218 174 160 -203 219 176 161 -204 219 177 163 -205 219 178 164 -206 220 179 166 -207 220 180 167 -208 220 181 169 -209 221 182 170 -210 221 184 172 -211 222 185 173 -212 222 186 174 -213 222 187 176 -214 223 188 177 -215 223 189 179 -216 223 191 180 -217 224 192 182 -218 224 193 183 -219 224 194 185 -220 225 195 186 -221 225 196 188 -222 226 198 189 -223 226 199 190 -224 226 200 192 -225 227 201 193 -226 227 202 195 -227 228 203 196 -228 228 204 198 -229 228 206 199 -230 229 207 200 -231 229 208 202 -232 230 209 203 -233 230 210 205 -234 230 212 206 -235 231 213 208 -236 231 214 209 -237 232 215 211 -238 232 216 212 -239 233 217 213 -240 233 219 215 -241 233 220 216 -242 234 221 218 -243 234 222 219 -244 235 223 220 -245 235 225 222 -246 236 226 223 -247 236 227 225 -248 237 228 226 -249 237 229 227 -250 238 231 229 -251 238 232 230 -252 239 233 232 -253 240 234 233 -254 240 235 234 -255 241 236 236 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Balance.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Balance.lut deleted file mode 100644 index 3e39ff1da..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Balance.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 23 28 66 -1 24 29 69 -2 25 31 72 -3 26 32 75 -4 27 33 78 -5 28 35 81 -6 29 36 85 -7 30 37 88 -8 31 39 91 -9 32 40 94 -10 33 41 97 -11 34 43 101 -12 35 44 104 -13 35 45 107 -14 36 47 111 -15 37 48 114 -16 37 49 118 -17 38 51 121 -18 39 52 125 -19 39 53 128 -20 40 55 132 -21 40 56 136 -22 40 57 139 -23 41 58 143 -24 41 60 147 -25 41 61 150 -26 41 63 154 -27 40 64 158 -28 40 65 161 -29 39 67 165 -30 39 68 168 -31 38 70 172 -32 36 72 175 -33 35 74 178 -34 33 75 181 -35 30 77 184 -36 27 80 186 -37 24 82 187 -38 21 84 188 -39 18 86 189 -40 15 89 189 -41 13 91 190 -42 11 93 190 -43 10 95 189 -44 9 98 189 -45 10 100 189 -46 11 102 189 -47 12 104 188 -48 14 106 188 -49 16 108 188 -50 19 110 187 -51 21 112 187 -52 24 114 187 -53 27 116 187 -54 29 117 186 -55 32 119 186 -56 35 121 186 -57 37 123 186 -58 40 125 186 -59 43 127 186 -60 45 128 185 -61 48 130 185 -62 51 132 185 -63 53 134 185 -64 56 135 185 -65 59 137 185 -66 61 139 185 -67 64 140 185 -68 66 142 185 -69 69 144 185 -70 72 145 185 -71 74 147 186 -72 77 149 186 -73 80 150 186 -74 83 152 186 -75 86 154 186 -76 89 155 186 -77 91 157 186 -78 94 158 187 -79 97 160 187 -80 100 162 187 -81 104 163 188 -82 107 165 188 -83 110 166 188 -84 113 168 189 -85 116 169 189 -86 119 171 190 -87 123 172 190 -88 126 173 191 -89 129 175 191 -90 132 176 192 -91 135 178 193 -92 139 179 193 -93 142 181 194 -94 145 182 195 -95 148 184 196 -96 151 185 197 -97 154 186 197 -98 157 188 198 -99 160 189 199 -100 163 191 200 -101 166 192 201 -102 169 194 202 -103 172 195 203 -104 175 197 204 -105 178 198 206 -106 181 200 207 -107 184 202 208 -108 187 203 209 -109 190 205 210 -110 193 206 212 -111 196 208 213 -112 199 210 214 -113 202 211 215 -114 204 213 217 -115 207 214 218 -116 210 216 219 -117 213 218 221 -118 216 220 222 -119 219 221 223 -120 221 223 225 -121 224 225 226 -122 227 227 228 -123 230 228 229 -124 232 230 231 -125 235 232 232 -126 238 234 234 -127 240 236 235 -128 240 236 235 -129 239 233 232 -130 238 231 230 -131 237 229 227 -132 236 226 224 -133 235 224 221 -134 234 222 218 -135 233 219 215 -136 232 217 213 -137 231 214 210 -138 231 212 207 -139 230 210 204 -140 229 207 201 -141 228 205 198 -142 227 203 195 -143 227 200 192 -144 226 198 190 -145 225 196 187 -146 224 193 184 -147 223 191 181 -148 223 189 178 -149 222 186 175 -150 221 184 172 -151 221 182 169 -152 220 179 166 -153 219 177 163 -154 218 175 161 -155 218 173 158 -156 217 170 155 -157 216 168 152 -158 216 166 149 -159 215 163 146 -160 214 161 143 -161 214 159 140 -162 213 157 138 -163 212 154 135 -164 212 152 132 -165 211 150 129 -166 210 148 126 -167 210 145 123 -168 209 143 120 -169 208 141 118 -170 208 138 115 -171 207 136 112 -172 206 134 109 -173 205 132 106 -174 205 129 104 -175 204 127 101 -176 203 125 98 -177 203 122 95 -178 202 120 93 -179 201 118 90 -180 200 115 87 -181 200 113 85 -182 199 111 82 -183 198 108 79 -184 197 106 77 -185 197 104 74 -186 196 101 72 -187 195 99 69 -188 194 96 67 -189 193 94 64 -190 192 92 62 -191 192 89 60 -192 191 87 58 -193 190 84 55 -194 189 82 53 -195 188 79 51 -196 187 77 49 -197 186 74 47 -198 185 72 45 -199 184 69 44 -200 183 66 42 -201 182 64 41 -202 181 61 40 -203 180 58 39 -204 179 56 38 -205 177 53 37 -206 176 50 36 -207 175 48 36 -208 173 45 36 -209 172 43 36 -210 170 40 36 -211 168 38 36 -212 167 35 36 -213 165 33 36 -214 163 31 37 -215 161 29 37 -216 159 27 37 -217 157 25 38 -218 155 23 38 -219 153 21 39 -220 150 20 39 -221 148 18 40 -222 146 17 40 -223 143 16 40 -224 141 15 40 -225 138 15 41 -226 136 14 41 -227 133 14 41 -228 130 14 41 -229 128 13 41 -230 125 13 41 -231 122 14 40 -232 120 14 40 -233 117 14 39 -234 114 14 39 -235 112 14 38 -236 109 14 38 -237 106 14 37 -238 104 14 36 -239 101 14 35 -240 98 14 34 -241 96 14 33 -242 93 14 33 -243 90 14 32 -244 88 13 30 -245 85 13 29 -246 83 13 28 -247 80 12 27 -248 77 12 26 -249 75 12 25 -250 72 11 24 -251 70 11 22 -252 67 10 21 -253 65 10 20 -254 62 9 19 -255 60 9 17 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Curl.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Curl.lut deleted file mode 100644 index 820059123..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Curl.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 20 29 67 -1 21 31 68 -2 21 33 69 -3 22 35 70 -4 22 37 71 -5 23 39 72 -6 23 41 74 -7 24 43 75 -8 24 45 76 -9 24 47 77 -10 25 48 78 -11 25 50 79 -12 25 52 80 -13 26 54 82 -14 26 56 83 -15 26 58 84 -16 27 60 85 -17 27 61 86 -18 27 63 87 -19 27 65 89 -20 27 67 90 -21 27 69 91 -22 28 71 92 -23 28 72 93 -24 28 74 94 -25 28 76 96 -26 28 78 97 -27 27 80 98 -28 27 82 99 -29 27 83 100 -30 27 85 101 -31 27 87 102 -32 26 89 103 -33 26 91 105 -34 26 93 106 -35 25 95 107 -36 25 96 108 -37 24 98 109 -38 23 100 110 -39 23 102 111 -40 22 104 112 -41 21 106 113 -42 21 108 114 -43 20 110 115 -44 19 112 115 -45 18 113 116 -46 18 115 117 -47 17 117 118 -48 17 119 119 -49 16 121 119 -50 16 123 120 -51 16 125 121 -52 17 127 122 -53 17 128 122 -54 18 130 123 -55 20 132 123 -56 21 134 124 -57 23 136 124 -58 26 138 125 -59 28 139 125 -60 31 141 126 -61 34 143 126 -62 37 145 126 -63 41 146 127 -64 44 148 127 -65 48 150 127 -66 52 151 128 -67 56 153 128 -68 60 154 129 -69 64 156 129 -70 68 157 130 -71 72 159 130 -72 76 160 131 -73 80 162 131 -74 84 163 132 -75 88 165 133 -76 92 166 133 -77 95 168 134 -78 99 169 135 -79 103 170 136 -80 107 172 137 -81 111 173 138 -82 115 174 140 -83 118 176 141 -84 122 177 142 -85 125 178 143 -86 129 180 145 -87 133 181 146 -88 136 183 148 -89 140 184 150 -90 143 185 151 -91 146 187 153 -92 150 188 155 -93 153 189 156 -94 156 191 158 -95 160 192 160 -96 163 194 162 -97 166 195 164 -98 169 196 166 -99 173 198 169 -100 176 199 171 -101 179 201 173 -102 182 202 175 -103 185 204 177 -104 188 205 180 -105 191 207 182 -106 194 208 185 -107 197 210 187 -108 200 212 190 -109 203 213 192 -110 206 215 195 -111 209 216 197 -112 212 218 200 -113 214 220 203 -114 217 221 205 -115 220 223 208 -116 223 225 211 -117 226 226 214 -118 229 228 216 -119 231 230 219 -120 234 232 222 -121 237 233 225 -122 240 235 228 -123 243 237 231 -124 245 239 234 -125 248 241 237 -126 251 243 240 -127 253 245 243 -128 253 245 243 -129 251 242 240 -130 250 240 237 -131 249 237 233 -132 248 235 230 -133 247 233 227 -134 246 230 223 -135 245 228 220 -136 244 225 216 -137 243 223 213 -138 242 220 210 -139 241 218 206 -140 240 215 203 -141 239 213 200 -142 238 210 196 -143 237 208 193 -144 236 205 190 -145 236 203 187 -146 235 200 183 -147 234 198 180 -148 233 195 177 -149 233 193 174 -150 232 190 171 -151 231 188 168 -152 231 186 165 -153 230 183 162 -154 229 181 159 -155 229 178 156 -156 228 176 153 -157 227 173 150 -158 227 171 147 -159 226 168 145 -160 225 166 142 -161 225 163 139 -162 224 160 137 -163 223 158 134 -164 223 155 132 -165 222 153 129 -166 221 150 127 -167 221 148 125 -168 220 145 123 -169 219 143 121 -170 219 140 119 -171 218 138 117 -172 217 135 115 -173 216 133 113 -174 215 130 112 -175 215 128 110 -176 214 125 109 -177 213 123 107 -178 212 120 106 -179 211 118 105 -180 210 115 104 -181 209 113 103 -182 208 111 102 -183 206 108 101 -184 205 106 100 -185 204 104 100 -186 203 101 99 -187 201 99 98 -188 200 97 98 -189 199 94 97 -190 197 92 97 -191 196 90 97 -192 194 88 96 -193 193 86 96 -194 191 83 96 -195 189 81 96 -196 188 79 96 -197 186 77 96 -198 184 75 96 -199 183 73 95 -200 181 71 95 -201 179 69 95 -202 177 67 95 -203 176 65 95 -204 174 63 95 -205 172 61 96 -206 170 60 96 -207 168 58 96 -208 166 56 96 -209 164 54 96 -210 162 52 96 -211 160 51 96 -212 158 49 96 -213 156 47 96 -214 154 45 96 -215 151 44 96 -216 149 42 96 -217 147 41 96 -218 145 39 96 -219 143 38 96 -220 140 36 96 -221 138 35 96 -222 136 33 96 -223 133 32 95 -224 131 31 95 -225 129 30 95 -226 126 28 95 -227 124 27 94 -228 121 26 94 -229 119 25 93 -230 116 25 93 -231 114 24 92 -232 111 23 91 -233 109 22 90 -234 106 22 89 -235 103 21 88 -236 101 21 87 -237 98 20 86 -238 95 20 85 -239 93 20 83 -240 90 19 82 -241 87 19 80 -242 85 19 79 -243 82 18 77 -244 79 18 75 -245 77 18 74 -246 74 17 72 -247 72 17 70 -248 69 16 68 -249 66 16 66 -250 64 16 64 -251 61 15 61 -252 59 14 59 -253 56 14 57 -254 54 13 55 -255 51 13 53 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Deep.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Deep.lut deleted file mode 100644 index 8da16d49f..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Deep.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 39 26 44 -1 40 26 45 -2 41 27 46 -3 41 28 48 -4 42 29 49 -5 43 29 50 -6 43 30 52 -7 44 31 53 -8 45 32 55 -9 45 32 56 -10 46 33 57 -11 47 34 59 -12 47 35 60 -13 48 35 62 -14 49 36 63 -15 49 37 65 -16 50 38 66 -17 50 38 68 -18 51 39 69 -19 52 40 71 -20 52 41 72 -21 53 41 74 -22 53 42 75 -23 54 43 77 -24 55 43 78 -25 55 44 80 -26 56 45 82 -27 56 46 83 -28 57 46 85 -29 57 47 86 -30 58 48 88 -31 58 48 90 -32 59 49 91 -33 59 50 93 -34 60 51 95 -35 60 51 96 -36 61 52 98 -37 61 53 99 -38 61 54 101 -39 62 54 103 -40 62 55 105 -41 62 56 106 -42 63 57 108 -43 63 57 110 -44 63 58 111 -45 64 59 113 -46 64 60 115 -47 64 61 116 -48 64 61 118 -49 65 62 120 -50 65 63 121 -51 65 64 123 -52 65 65 124 -53 65 66 126 -54 65 67 127 -55 65 68 129 -56 65 69 130 -57 65 69 132 -58 65 70 133 -59 65 71 134 -60 64 72 135 -61 64 74 136 -62 64 75 137 -63 64 76 138 -64 64 77 139 -65 63 78 140 -66 63 79 141 -67 63 80 142 -68 63 81 142 -69 62 82 143 -70 62 83 143 -71 62 84 144 -72 62 85 144 -73 62 87 145 -74 62 88 145 -75 61 89 145 -76 61 90 146 -77 61 91 146 -78 61 92 146 -79 61 93 147 -80 61 94 147 -81 61 95 147 -82 61 96 147 -83 61 97 148 -84 61 98 148 -85 61 99 148 -86 61 100 148 -87 61 101 149 -88 61 102 149 -89 62 103 149 -90 62 105 149 -91 62 106 149 -92 62 107 150 -93 62 108 150 -94 62 109 150 -95 62 110 150 -96 63 111 150 -97 63 112 151 -98 63 113 151 -99 63 114 151 -100 64 115 151 -101 64 116 151 -102 64 117 152 -103 64 118 152 -104 64 119 152 -105 65 120 152 -106 65 121 153 -107 65 122 153 -108 66 123 153 -109 66 124 153 -110 66 125 153 -111 66 126 154 -112 67 127 154 -113 67 128 154 -114 67 129 154 -115 68 129 154 -116 68 130 155 -117 68 131 155 -118 69 132 155 -119 69 133 155 -120 69 134 156 -121 70 135 156 -122 70 136 156 -123 70 137 156 -124 71 138 157 -125 71 139 157 -126 71 140 157 -127 72 141 157 -128 72 142 157 -129 72 143 158 -130 73 144 158 -131 73 145 158 -132 73 146 158 -133 74 147 159 -134 74 148 159 -135 74 149 159 -136 75 150 159 -137 75 151 159 -138 75 152 160 -139 76 153 160 -140 76 154 160 -141 76 155 160 -142 77 156 160 -143 77 157 161 -144 78 159 161 -145 78 160 161 -146 78 161 161 -147 79 162 161 -148 79 163 162 -149 80 164 162 -150 80 165 162 -151 81 166 162 -152 81 167 162 -153 81 168 162 -154 82 169 162 -155 82 170 163 -156 83 171 163 -157 83 172 163 -158 84 173 163 -159 85 174 163 -160 85 175 163 -161 86 176 163 -162 86 177 163 -163 87 178 163 -164 88 179 163 -165 88 180 163 -166 89 181 163 -167 90 182 163 -168 91 183 163 -169 92 184 163 -170 92 185 163 -171 93 186 163 -172 94 187 163 -173 95 188 163 -174 96 189 163 -175 97 190 163 -176 98 191 163 -177 99 192 163 -178 100 193 163 -179 102 194 163 -180 103 195 163 -181 104 196 163 -182 105 197 163 -183 107 198 163 -184 108 199 163 -185 110 200 163 -186 111 201 163 -187 113 202 163 -188 114 202 163 -189 116 203 163 -190 118 204 163 -191 119 205 162 -192 121 206 162 -193 123 207 162 -194 125 208 162 -195 127 209 162 -196 129 209 162 -197 131 210 162 -198 133 211 162 -199 135 212 163 -200 137 213 163 -201 139 213 163 -202 141 214 163 -203 143 215 163 -204 145 216 163 -205 147 216 163 -206 149 217 164 -207 152 218 164 -208 154 219 164 -209 156 219 165 -210 158 220 165 -211 160 221 165 -212 163 222 166 -213 165 222 166 -214 167 223 167 -215 169 224 167 -216 171 225 168 -217 174 225 168 -218 176 226 169 -219 178 227 169 -220 180 227 170 -221 182 228 171 -222 184 229 171 -223 187 230 172 -224 189 230 173 -225 191 231 173 -226 193 232 174 -227 195 232 175 -228 197 233 176 -229 199 234 176 -230 201 235 177 -231 204 235 178 -232 206 236 179 -233 208 237 180 -234 210 237 181 -235 212 238 182 -236 214 239 183 -237 216 240 184 -238 218 240 185 -239 220 241 186 -240 222 242 187 -241 224 242 188 -242 226 243 189 -243 228 244 190 -244 230 245 191 -245 232 245 192 -246 235 246 193 -247 237 247 194 -248 239 248 195 -249 241 248 196 -250 243 249 198 -251 245 250 199 -252 247 251 200 -253 249 252 201 -254 251 252 202 -255 253 253 204 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Delta.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Delta.lut deleted file mode 100644 index 63d48561c..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Delta.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 16 31 63 -1 18 32 66 -2 19 34 69 -3 20 35 73 -4 22 37 76 -5 23 38 79 -6 24 39 82 -7 25 41 86 -8 27 42 89 -9 28 43 92 -10 29 45 96 -11 30 46 99 -12 31 47 103 -13 32 48 107 -14 33 50 110 -15 34 51 114 -16 35 52 118 -17 36 53 122 -18 37 55 125 -19 37 56 129 -20 38 57 133 -21 38 59 137 -22 38 60 141 -23 38 62 144 -24 37 64 148 -25 35 66 150 -26 34 68 152 -27 33 70 153 -28 31 72 154 -29 30 75 155 -30 29 77 155 -31 29 79 156 -32 28 81 156 -33 27 83 157 -34 27 86 157 -35 27 88 157 -36 26 90 157 -37 26 92 158 -38 26 94 158 -39 26 96 158 -40 27 98 159 -41 27 100 159 -42 27 102 159 -43 28 104 160 -44 28 106 160 -45 29 108 160 -46 30 110 161 -47 31 112 161 -48 31 114 161 -49 32 116 162 -50 33 118 162 -51 34 120 163 -52 35 121 163 -53 36 123 164 -54 38 125 164 -55 39 127 165 -56 40 129 165 -57 41 131 165 -58 42 133 166 -59 44 135 166 -60 45 137 167 -61 46 139 167 -62 48 141 168 -63 49 142 168 -64 51 144 169 -65 53 146 169 -66 54 148 170 -67 56 150 170 -68 58 152 171 -69 60 154 171 -70 62 156 172 -71 64 157 172 -72 66 159 173 -73 68 161 173 -74 71 163 174 -75 73 165 174 -76 76 167 174 -77 79 168 175 -78 82 170 175 -79 86 172 176 -80 89 173 176 -81 93 175 176 -82 96 177 177 -83 100 178 177 -84 104 180 178 -85 108 181 179 -86 112 183 179 -87 116 184 180 -88 120 185 181 -89 124 187 182 -90 128 188 183 -91 132 190 184 -92 136 191 185 -93 140 193 186 -94 143 194 187 -95 147 196 188 -96 151 197 190 -97 154 198 191 -98 158 200 192 -99 162 201 193 -100 165 203 195 -101 169 204 196 -102 172 206 197 -103 175 208 199 -104 179 209 200 -105 182 211 202 -106 185 212 203 -107 189 214 204 -108 192 215 206 -109 195 217 207 -110 199 219 209 -111 202 220 210 -112 205 222 212 -113 208 224 213 -114 211 226 215 -115 214 227 216 -116 217 229 218 -117 221 231 219 -118 224 233 220 -119 227 235 222 -120 230 237 223 -121 233 238 224 -122 236 240 225 -123 239 242 226 -124 243 244 227 -125 246 246 228 -126 249 248 229 -127 253 250 229 -128 254 252 203 -129 253 249 199 -130 251 247 195 -131 250 244 191 -132 249 242 187 -133 247 240 182 -134 246 237 178 -135 245 235 174 -136 243 233 169 -137 242 230 165 -138 241 228 161 -139 239 226 156 -140 238 223 152 -141 236 221 148 -142 235 219 143 -143 233 217 139 -144 232 215 135 -145 230 213 130 -146 229 210 126 -147 227 208 122 -148 226 206 117 -149 224 204 113 -150 222 202 109 -151 220 201 104 -152 218 199 100 -153 216 197 96 -154 214 195 91 -155 212 193 87 -156 210 192 83 -157 207 190 79 -158 205 188 75 -159 203 187 71 -160 200 185 67 -161 197 184 63 -162 195 182 59 -163 192 181 56 -164 189 179 52 -165 186 178 48 -166 183 177 45 -167 180 175 42 -168 177 174 39 -169 174 173 35 -170 170 172 32 -171 167 170 29 -172 164 169 27 -173 160 168 24 -174 157 167 21 -175 154 166 19 -176 150 164 16 -177 147 163 14 -178 143 162 12 -179 140 161 10 -180 136 160 8 -181 133 158 7 -182 129 157 6 -183 126 156 5 -184 122 155 5 -185 119 153 5 -186 115 152 5 -187 112 151 6 -188 108 150 7 -189 104 149 8 -190 101 147 9 -191 97 146 11 -192 93 145 12 -193 90 143 14 -194 86 142 15 -195 82 141 17 -196 79 139 19 -197 75 138 20 -198 71 137 22 -199 68 135 23 -200 64 134 25 -201 61 132 26 -202 57 131 28 -203 54 129 29 -204 50 128 30 -205 47 126 32 -206 43 125 33 -207 40 123 34 -208 37 122 35 -209 34 120 36 -210 31 118 37 -211 28 117 38 -212 25 115 39 -213 23 113 40 -214 20 112 40 -215 18 110 41 -216 16 108 42 -217 14 106 42 -218 13 105 43 -219 12 103 43 -220 11 101 44 -221 10 99 44 -222 10 97 44 -223 11 95 44 -224 11 94 44 -225 12 92 44 -226 12 90 44 -227 13 88 44 -228 14 86 44 -229 15 84 44 -230 16 82 43 -231 17 80 43 -232 18 78 43 -233 19 76 42 -234 20 75 42 -235 20 73 41 -236 21 71 40 -237 22 69 40 -238 22 67 39 -239 23 65 38 -240 23 63 37 -241 24 61 36 -242 24 59 35 -243 24 57 34 -244 25 55 33 -245 25 53 32 -246 25 52 31 -247 25 50 30 -248 25 48 28 -249 25 46 27 -250 24 44 26 -251 24 42 24 -252 24 40 23 -253 23 38 21 -254 23 36 20 -255 23 35 18 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Dense.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Dense.lut deleted file mode 100644 index 8dc548f29..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Dense.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 54 14 36 -1 55 14 37 -2 56 14 38 -3 57 15 39 -4 59 15 40 -5 60 15 42 -6 61 15 43 -7 62 16 44 -8 64 16 45 -9 65 16 47 -10 66 16 48 -11 67 17 49 -12 68 17 50 -13 69 17 52 -14 71 17 53 -15 72 18 55 -16 73 18 56 -17 74 18 57 -18 75 18 59 -19 76 19 60 -20 77 19 62 -21 78 19 63 -22 79 19 65 -23 80 20 66 -24 81 20 68 -25 82 20 69 -26 83 21 71 -27 84 21 73 -28 85 22 74 -29 86 22 76 -30 87 22 77 -31 88 23 79 -32 89 23 81 -33 90 24 82 -34 91 24 84 -35 92 25 86 -36 92 25 87 -37 93 26 89 -38 94 26 91 -39 95 27 92 -40 96 28 94 -41 96 28 96 -42 97 29 97 -43 98 29 99 -44 99 30 101 -45 99 31 102 -46 100 31 104 -47 101 32 106 -48 101 33 107 -49 102 34 109 -50 103 34 110 -51 103 35 112 -52 104 36 114 -53 105 37 115 -54 105 37 117 -55 106 38 119 -56 106 39 120 -57 107 40 122 -58 107 41 123 -59 108 42 125 -60 108 42 127 -61 109 43 128 -62 109 44 130 -63 110 45 131 -64 110 46 133 -65 111 47 134 -66 111 48 136 -67 112 49 138 -68 112 49 139 -69 113 50 141 -70 113 51 142 -71 113 52 144 -72 114 53 145 -73 114 54 147 -74 115 55 148 -75 115 56 150 -76 115 57 151 -77 116 58 153 -78 116 59 154 -79 116 60 155 -80 116 61 157 -81 117 62 158 -82 117 63 160 -83 117 64 161 -84 118 65 163 -85 118 66 164 -86 118 67 165 -87 118 68 167 -88 119 69 168 -89 119 70 170 -90 119 71 171 -91 119 72 172 -92 119 73 174 -93 119 74 175 -94 120 75 176 -95 120 76 178 -96 120 77 179 -97 120 78 180 -98 120 80 181 -99 120 81 183 -100 120 82 184 -101 120 83 185 -102 120 84 186 -103 121 85 188 -104 121 86 189 -105 121 87 190 -106 121 88 191 -107 121 89 192 -108 121 91 194 -109 121 92 195 -110 121 93 196 -111 121 94 197 -112 121 95 198 -113 121 96 199 -114 121 97 200 -115 120 99 201 -116 120 100 202 -117 120 101 203 -118 120 102 204 -119 120 103 205 -120 120 104 206 -121 120 105 207 -122 120 107 208 -123 120 108 209 -124 119 109 210 -125 119 110 211 -126 119 111 212 -127 119 113 213 -128 119 114 213 -129 119 115 214 -130 118 116 215 -131 118 117 216 -132 118 119 216 -133 118 120 217 -134 118 121 218 -135 118 122 218 -136 117 123 219 -137 117 125 220 -138 117 126 220 -139 117 127 221 -140 117 128 221 -141 116 129 222 -142 116 131 222 -143 116 132 223 -144 116 133 223 -145 116 134 224 -146 116 135 224 -147 115 137 225 -148 115 138 225 -149 115 139 225 -150 115 140 226 -151 115 141 226 -152 115 143 226 -153 115 144 227 -154 115 145 227 -155 115 146 227 -156 115 147 227 -157 115 148 227 -158 115 150 227 -159 115 151 228 -160 115 152 228 -161 115 153 228 -162 115 154 228 -163 116 155 228 -164 116 156 228 -165 116 158 228 -166 116 159 228 -167 117 160 228 -168 117 161 228 -169 117 162 228 -170 118 163 228 -171 118 164 228 -172 119 165 228 -173 119 166 228 -174 120 167 228 -175 120 169 228 -176 121 170 228 -177 122 171 228 -178 122 172 228 -179 123 173 227 -180 124 174 227 -181 124 175 227 -182 125 176 227 -183 126 177 227 -184 127 178 227 -185 128 179 227 -186 129 180 227 -187 130 181 227 -188 131 182 227 -189 132 183 227 -190 133 184 226 -191 134 185 226 -192 135 186 226 -193 136 187 226 -194 137 188 226 -195 138 189 226 -196 139 190 226 -197 140 190 226 -198 141 191 226 -199 143 192 226 -200 144 193 226 -201 145 194 226 -202 146 195 226 -203 148 196 226 -204 149 197 226 -205 150 198 226 -206 152 199 226 -207 153 200 226 -208 154 201 226 -209 156 201 226 -210 157 202 226 -211 159 203 226 -212 160 204 226 -213 161 205 226 -214 163 206 226 -215 164 207 226 -216 166 208 226 -217 167 209 226 -218 169 209 226 -219 170 210 226 -220 172 211 227 -221 173 212 227 -222 175 213 227 -223 176 214 227 -224 178 215 227 -225 179 215 227 -226 181 216 228 -227 183 217 228 -228 184 218 228 -229 186 219 228 -230 187 220 228 -231 189 220 229 -232 191 221 229 -233 192 222 229 -234 194 223 230 -235 196 224 230 -236 197 225 230 -237 199 225 231 -238 201 226 231 -239 202 227 231 -240 204 228 232 -241 206 229 232 -242 207 230 233 -243 209 230 233 -244 211 231 234 -245 212 232 234 -246 214 233 235 -247 216 234 235 -248 217 235 236 -249 219 235 236 -250 221 236 237 -251 223 237 237 -252 224 238 238 -253 226 239 239 -254 228 239 239 -255 230 240 240 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Gray.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Gray.lut deleted file mode 100644 index 685dcfc3a..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Gray.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 0 0 0 -1 0 0 0 -2 0 0 0 -3 0 0 0 -4 0 0 0 -5 1 1 1 -6 1 1 1 -7 2 1 2 -8 2 2 2 -9 3 3 3 -10 3 3 3 -11 4 4 4 -12 5 5 5 -13 6 6 6 -14 7 7 7 -15 8 7 7 -16 9 8 8 -17 10 10 9 -18 11 11 11 -19 12 12 12 -20 13 13 13 -21 14 14 14 -22 15 15 15 -23 16 16 16 -24 17 17 17 -25 18 18 18 -26 19 19 19 -27 20 20 20 -28 21 21 21 -29 22 22 22 -30 23 23 23 -31 24 24 24 -32 25 25 25 -33 26 26 26 -34 27 27 27 -35 28 28 28 -36 29 29 28 -37 30 30 29 -38 31 30 30 -39 32 31 31 -40 33 32 32 -41 34 33 33 -42 35 34 34 -43 36 35 35 -44 37 36 36 -45 37 37 37 -46 38 38 38 -47 39 39 39 -48 40 40 40 -49 41 41 41 -50 42 42 41 -51 43 43 42 -52 44 43 43 -53 45 44 44 -54 46 45 45 -55 47 46 46 -56 48 47 47 -57 48 48 48 -58 49 49 49 -59 50 50 50 -60 51 51 51 -61 52 52 51 -62 53 52 52 -63 54 53 53 -64 55 54 54 -65 56 55 55 -66 57 56 56 -67 57 57 57 -68 58 58 58 -69 59 59 59 -70 60 60 59 -71 61 61 60 -72 62 61 61 -73 63 62 62 -74 64 63 63 -75 65 64 64 -76 66 65 65 -77 66 66 66 -78 67 67 67 -79 68 68 67 -80 69 69 68 -81 70 70 69 -82 71 70 70 -83 72 71 71 -84 73 72 72 -85 74 73 73 -86 75 74 74 -87 75 75 75 -88 76 76 76 -89 77 77 76 -90 78 78 77 -91 79 79 78 -92 80 79 79 -93 81 80 80 -94 82 81 81 -95 83 82 82 -96 84 83 83 -97 84 84 84 -98 85 85 84 -99 86 86 85 -100 87 87 86 -101 88 88 87 -102 89 89 88 -103 90 89 89 -104 91 90 90 -105 92 91 91 -106 93 92 92 -107 94 93 93 -108 94 94 94 -109 95 95 94 -110 96 96 95 -111 97 97 96 -112 98 98 97 -113 99 99 98 -114 100 100 99 -115 101 100 100 -116 102 101 101 -117 103 102 102 -118 104 103 103 -119 105 104 104 -120 106 105 105 -121 106 106 106 -122 107 107 106 -123 108 108 107 -124 109 109 108 -125 110 110 109 -126 111 111 110 -127 112 112 111 -128 113 113 112 -129 114 114 113 -130 115 114 114 -131 116 115 115 -132 117 116 116 -133 118 117 117 -134 119 118 118 -135 120 119 119 -136 121 120 120 -137 122 121 121 -138 123 122 122 -139 124 123 122 -140 125 124 123 -141 125 125 124 -142 126 126 125 -143 127 127 126 -144 128 128 127 -145 129 129 128 -146 130 130 129 -147 131 131 130 -148 132 132 131 -149 133 133 132 -150 134 134 133 -151 135 135 134 -152 136 136 135 -153 137 137 136 -154 138 138 137 -155 139 139 138 -156 140 140 139 -157 141 141 140 -158 142 142 141 -159 143 143 142 -160 144 144 143 -161 145 145 144 -162 146 146 145 -163 147 147 146 -164 148 148 147 -165 149 149 148 -166 150 150 149 -167 151 151 150 -168 152 152 151 -169 153 153 152 -170 154 154 153 -171 155 155 154 -172 156 156 155 -173 157 157 156 -174 158 158 157 -175 160 159 158 -176 161 160 159 -177 162 161 160 -178 163 162 161 -179 164 163 163 -180 165 164 164 -181 166 165 165 -182 167 167 166 -183 168 168 167 -184 169 169 168 -185 170 170 169 -186 171 171 170 -187 172 172 171 -188 173 173 172 -189 174 174 173 -190 176 175 174 -191 177 176 175 -192 178 177 176 -193 179 178 178 -194 180 180 179 -195 181 181 180 -196 182 182 181 -197 183 183 182 -198 184 184 183 -199 185 185 184 -200 187 186 185 -201 188 187 186 -202 189 189 188 -203 190 190 189 -204 191 191 190 -205 192 192 191 -206 193 193 192 -207 194 194 193 -208 196 195 194 -209 197 197 195 -210 198 198 197 -211 199 199 198 -212 200 200 199 -213 201 201 200 -214 203 202 201 -215 204 204 202 -216 205 205 204 -217 206 206 205 -218 207 207 206 -219 209 208 207 -220 210 209 208 -221 211 211 210 -222 212 212 211 -223 213 213 212 -224 215 214 213 -225 216 216 214 -226 217 217 216 -227 218 218 217 -228 219 219 218 -229 221 220 219 -230 222 222 220 -231 223 223 222 -232 224 224 223 -233 226 225 224 -234 227 227 225 -235 228 228 227 -236 229 229 228 -237 231 230 229 -238 232 232 230 -239 233 233 232 -240 235 234 233 -241 236 236 234 -242 237 237 236 -243 238 238 237 -244 240 239 238 -245 241 241 240 -246 242 242 241 -247 244 243 242 -248 245 245 243 -249 246 246 245 -250 248 247 246 -251 249 249 247 -252 250 250 249 -253 252 251 250 -254 253 253 251 -255 254 254 253 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Haline.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Haline.lut deleted file mode 100644 index 1d97c65df..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Haline.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 41 24 107 -1 42 24 110 -2 42 24 112 -3 42 25 114 -4 43 25 117 -5 43 25 119 -6 44 25 122 -7 44 26 124 -8 44 26 127 -9 45 26 129 -10 45 26 132 -11 45 27 134 -12 45 27 137 -13 46 27 139 -14 46 28 142 -15 46 28 144 -16 46 29 147 -17 46 29 149 -18 45 30 151 -19 45 31 153 -20 45 32 155 -21 44 33 157 -22 43 34 159 -23 42 35 160 -24 41 37 161 -25 40 39 162 -26 39 40 162 -27 38 42 162 -28 37 44 162 -29 35 46 162 -30 34 48 162 -31 33 49 162 -32 31 51 161 -33 30 53 161 -34 29 55 160 -35 27 56 160 -36 26 58 159 -37 25 59 159 -38 24 61 158 -39 22 62 157 -40 21 64 157 -41 20 65 156 -42 19 66 155 -43 18 68 155 -44 17 69 154 -45 16 70 153 -46 15 71 153 -47 15 73 152 -48 14 74 152 -49 13 75 151 -50 13 76 150 -51 12 77 150 -52 12 78 149 -53 12 79 149 -54 12 81 148 -55 12 82 148 -56 12 83 147 -57 12 84 147 -58 12 85 146 -59 12 86 146 -60 13 87 145 -61 13 88 145 -62 14 89 144 -63 14 90 144 -64 15 91 144 -65 15 92 143 -66 16 93 143 -67 17 93 143 -68 18 94 142 -69 18 95 142 -70 19 96 142 -71 20 97 141 -72 21 98 141 -73 21 99 141 -74 22 100 140 -75 23 101 140 -76 24 102 140 -77 25 102 140 -78 26 103 139 -79 26 104 139 -80 27 105 139 -81 28 106 139 -82 29 107 139 -83 30 108 139 -84 31 109 138 -85 31 109 138 -86 32 110 138 -87 33 111 138 -88 34 112 138 -89 35 113 138 -90 36 114 137 -91 36 115 137 -92 37 115 137 -93 38 116 137 -94 39 117 137 -95 39 118 137 -96 40 119 137 -97 41 120 137 -98 42 121 137 -99 42 121 137 -100 43 122 137 -101 44 123 136 -102 44 124 136 -103 45 125 136 -104 46 126 136 -105 46 127 136 -106 47 127 136 -107 48 128 136 -108 48 129 136 -109 49 130 136 -110 49 131 136 -111 50 132 136 -112 51 133 136 -113 51 133 136 -114 52 134 136 -115 52 135 136 -116 53 136 136 -117 54 137 136 -118 54 138 135 -119 55 139 135 -120 55 140 135 -121 56 140 135 -122 56 141 135 -123 57 142 135 -124 57 143 135 -125 58 144 135 -126 58 145 135 -127 59 146 135 -128 59 147 135 -129 60 148 134 -130 60 148 134 -131 61 149 134 -132 61 150 134 -133 62 151 134 -134 62 152 134 -135 63 153 134 -136 64 154 133 -137 64 155 133 -138 65 156 133 -139 65 157 133 -140 66 158 132 -141 66 158 132 -142 67 159 132 -143 67 160 132 -144 68 161 131 -145 69 162 131 -146 69 163 131 -147 70 164 131 -148 70 165 130 -149 71 166 130 -150 72 167 130 -151 72 168 129 -152 73 169 129 -153 74 169 128 -154 74 170 128 -155 75 171 128 -156 76 172 127 -157 77 173 127 -158 78 174 126 -159 78 175 126 -160 79 176 125 -161 80 177 125 -162 81 178 124 -163 82 179 124 -164 83 179 123 -165 84 180 123 -166 85 181 122 -167 86 182 121 -168 87 183 121 -169 88 184 120 -170 89 185 120 -171 90 186 119 -172 91 187 118 -173 92 187 118 -174 94 188 117 -175 95 189 116 -176 96 190 115 -177 98 191 115 -178 99 192 114 -179 100 193 113 -180 102 193 112 -181 103 194 112 -182 105 195 111 -183 106 196 110 -184 108 197 109 -185 109 198 108 -186 111 198 107 -187 113 199 107 -188 114 200 106 -189 116 201 105 -190 118 201 104 -191 120 202 103 -192 122 203 102 -193 124 204 101 -194 126 204 100 -195 128 205 100 -196 130 206 99 -197 132 206 98 -198 134 207 97 -199 136 208 96 -200 138 208 96 -201 141 209 95 -202 143 210 94 -203 145 210 93 -204 148 211 93 -205 150 211 92 -206 152 212 92 -207 155 213 92 -208 157 213 91 -209 160 214 91 -210 162 214 91 -211 165 215 91 -212 167 215 91 -213 170 216 92 -214 172 216 92 -215 175 216 92 -216 177 217 93 -217 179 217 94 -218 182 218 94 -219 184 218 95 -220 186 219 96 -221 189 219 97 -222 191 220 98 -223 193 220 99 -224 195 221 100 -225 197 221 102 -226 199 222 103 -227 202 222 104 -228 204 223 106 -229 206 223 107 -230 208 224 109 -231 210 224 110 -232 212 225 112 -233 214 225 113 -234 216 226 115 -235 217 226 117 -236 219 227 118 -237 221 228 120 -238 223 228 122 -239 225 229 123 -240 227 229 125 -241 229 230 127 -242 230 230 129 -243 232 231 131 -244 234 232 132 -245 236 232 134 -246 238 233 136 -247 239 233 138 -248 241 234 140 -249 243 235 142 -250 245 235 144 -251 246 236 145 -252 248 236 147 -253 250 237 149 -254 251 238 151 -255 253 238 153 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Ice.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Ice.lut deleted file mode 100644 index a59bf0cc9..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Ice.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 3 5 18 -1 4 6 19 -2 5 7 21 -3 6 8 22 -4 6 8 24 -5 7 9 25 -6 8 10 27 -7 9 11 28 -8 10 12 30 -9 11 13 31 -10 12 14 32 -11 13 15 34 -12 14 16 35 -13 15 16 37 -14 16 17 38 -15 17 18 40 -16 18 19 41 -17 19 20 42 -18 20 21 44 -19 21 21 45 -20 22 22 47 -21 23 23 48 -22 24 24 50 -23 25 25 51 -24 26 25 53 -25 27 26 54 -26 27 27 56 -27 28 28 57 -28 29 29 59 -29 30 29 60 -30 31 30 62 -31 32 31 63 -32 33 32 65 -33 34 32 66 -34 34 33 68 -35 35 34 69 -36 36 35 71 -37 37 35 72 -38 38 36 74 -39 39 37 75 -40 39 38 77 -41 40 38 79 -42 41 39 80 -43 42 40 82 -44 42 41 83 -45 43 41 85 -46 44 42 87 -47 45 43 88 -48 45 44 90 -49 46 45 91 -50 47 45 93 -51 48 46 95 -52 48 47 96 -53 49 48 98 -54 50 48 100 -55 50 49 101 -56 51 50 103 -57 52 51 105 -58 52 51 106 -59 53 52 108 -60 53 53 110 -61 54 54 111 -62 55 55 113 -63 55 55 115 -64 56 56 116 -65 56 57 118 -66 57 58 120 -67 57 59 121 -68 58 59 123 -69 58 60 125 -70 58 61 126 -71 59 62 128 -72 59 63 130 -73 60 64 131 -74 60 65 133 -75 60 65 134 -76 60 66 136 -77 61 67 138 -78 61 68 139 -79 61 69 141 -80 61 70 142 -81 62 71 144 -82 62 72 145 -83 62 73 147 -84 62 74 148 -85 62 75 150 -86 62 76 151 -87 62 77 152 -88 62 78 154 -89 62 79 155 -90 62 80 156 -91 62 81 157 -92 62 82 159 -93 62 83 160 -94 62 84 161 -95 62 85 162 -96 62 87 163 -97 62 88 164 -98 62 89 165 -99 62 90 166 -100 62 91 167 -101 62 92 168 -102 62 93 168 -103 62 94 169 -104 62 96 170 -105 62 97 171 -106 62 98 172 -107 62 99 172 -108 62 100 173 -109 62 101 174 -110 62 102 174 -111 62 103 175 -112 62 105 175 -113 62 106 176 -114 62 107 177 -115 62 108 177 -116 62 109 178 -117 63 110 178 -118 63 111 179 -119 63 112 179 -120 63 114 180 -121 63 115 180 -122 64 116 180 -123 64 117 181 -124 64 118 181 -125 65 119 182 -126 65 120 182 -127 65 121 183 -128 66 122 183 -129 66 123 183 -130 67 125 184 -131 67 126 184 -132 68 127 185 -133 68 128 185 -134 69 129 185 -135 69 130 186 -136 70 131 186 -137 70 132 186 -138 71 133 187 -139 72 134 187 -140 72 135 188 -141 73 136 188 -142 74 137 188 -143 74 139 189 -144 75 140 189 -145 76 141 189 -146 76 142 190 -147 77 143 190 -148 78 144 190 -149 79 145 191 -150 79 146 191 -151 80 147 192 -152 81 148 192 -153 82 149 192 -154 82 150 193 -155 83 151 193 -156 84 152 193 -157 85 153 194 -158 86 154 194 -159 87 156 195 -160 88 157 195 -161 88 158 195 -162 89 159 196 -163 90 160 196 -164 91 161 197 -165 92 162 197 -166 93 163 197 -167 94 164 198 -168 95 165 198 -169 96 166 199 -170 97 167 199 -171 98 168 199 -172 99 169 200 -173 100 170 200 -174 101 171 201 -175 102 172 201 -176 103 174 201 -177 104 175 202 -178 105 176 202 -179 106 177 203 -180 107 178 203 -181 109 179 203 -182 110 180 204 -183 111 181 204 -184 112 182 205 -185 113 183 205 -186 114 184 205 -187 116 185 206 -188 117 186 206 -189 118 187 207 -190 119 188 207 -191 121 189 208 -192 122 190 208 -193 123 191 208 -194 125 192 209 -195 126 193 209 -196 128 194 210 -197 129 195 210 -198 131 196 211 -199 132 198 211 -200 134 199 211 -201 135 200 212 -202 137 201 212 -203 138 202 213 -204 140 203 213 -205 142 203 214 -206 143 204 214 -207 145 205 215 -208 147 206 215 -209 149 207 216 -210 150 208 216 -211 152 209 217 -212 154 210 217 -213 156 211 218 -214 158 212 219 -215 160 213 219 -216 161 214 220 -217 163 215 220 -218 165 216 221 -219 167 217 222 -220 169 218 222 -221 171 219 223 -222 173 220 224 -223 175 221 225 -224 176 221 225 -225 178 222 226 -226 180 223 227 -227 182 224 228 -228 184 225 228 -229 186 226 229 -230 188 227 230 -231 190 228 231 -232 192 229 232 -233 193 230 233 -234 195 231 234 -235 197 232 234 -236 199 233 235 -237 201 234 236 -238 203 235 237 -239 205 236 238 -240 207 237 239 -241 208 238 240 -242 210 239 241 -243 212 240 242 -244 214 241 243 -245 216 242 244 -246 218 243 245 -247 219 244 245 -248 221 245 246 -249 223 246 247 -250 225 247 248 -251 227 248 249 -252 228 249 250 -253 230 250 251 -254 232 251 252 -255 234 252 253 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Matter.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Matter.lut deleted file mode 100644 index 826cb16c9..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Matter.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 47 15 61 -1 48 15 62 -2 49 15 63 -3 50 16 64 -4 52 16 65 -5 53 16 66 -6 54 17 67 -7 56 17 68 -8 57 17 68 -9 58 18 69 -10 59 18 70 -11 61 18 71 -12 62 18 72 -13 63 19 73 -14 64 19 73 -15 66 19 74 -16 67 19 75 -17 68 20 76 -18 70 20 76 -19 71 20 77 -20 72 20 78 -21 74 21 79 -22 75 21 79 -23 76 21 80 -24 78 21 81 -25 79 21 82 -26 80 22 82 -27 82 22 83 -28 83 22 84 -29 84 22 84 -30 85 22 85 -31 87 22 86 -32 88 23 86 -33 89 23 87 -34 91 23 87 -35 92 23 88 -36 94 23 89 -37 95 23 89 -38 96 23 90 -39 98 23 90 -40 99 24 91 -41 100 24 91 -42 102 24 92 -43 103 24 92 -44 104 24 93 -45 106 24 93 -46 107 24 93 -47 108 25 94 -48 110 25 94 -49 111 25 95 -50 112 25 95 -51 114 25 95 -52 115 25 96 -53 116 25 96 -54 118 26 96 -55 119 26 97 -56 120 26 97 -57 122 26 97 -58 123 26 97 -59 125 26 98 -60 126 27 98 -61 127 27 98 -62 129 27 98 -63 130 27 98 -64 131 28 98 -65 133 28 99 -66 134 28 99 -67 135 28 99 -68 136 29 99 -69 138 29 99 -70 139 29 99 -71 140 30 99 -72 142 30 99 -73 143 30 99 -74 144 31 99 -75 146 31 99 -76 147 31 99 -77 148 32 99 -78 149 32 99 -79 151 33 99 -80 152 33 99 -81 153 33 98 -82 155 34 98 -83 156 34 98 -84 157 35 98 -85 158 35 98 -86 160 36 98 -87 161 36 98 -88 162 37 97 -89 163 37 97 -90 165 38 97 -91 166 39 97 -92 167 39 97 -93 168 40 96 -94 169 40 96 -95 171 41 96 -96 172 42 96 -97 173 42 95 -98 174 43 95 -99 175 44 95 -100 177 44 95 -101 178 45 94 -102 179 46 94 -103 180 46 94 -104 181 47 93 -105 182 48 93 -106 184 48 93 -107 185 49 93 -108 186 50 92 -109 187 51 92 -110 188 51 92 -111 189 52 91 -112 190 53 91 -113 191 54 91 -114 192 55 90 -115 193 56 90 -116 195 56 90 -117 196 57 89 -118 197 58 89 -119 198 59 89 -120 199 60 88 -121 200 61 88 -122 201 62 88 -123 202 63 87 -124 203 64 87 -125 204 65 87 -126 205 66 86 -127 206 67 86 -128 207 68 86 -129 208 69 85 -130 208 70 85 -131 209 71 85 -132 210 72 84 -133 211 73 84 -134 212 74 84 -135 213 75 84 -136 214 76 83 -137 215 78 83 -138 215 79 83 -139 216 80 83 -140 217 81 83 -141 218 82 83 -142 218 83 82 -143 219 85 82 -144 220 86 82 -145 221 87 82 -146 221 88 82 -147 222 89 82 -148 223 91 82 -149 223 92 82 -150 224 93 82 -151 225 95 82 -152 225 96 82 -153 226 97 82 -154 226 98 82 -155 227 100 83 -156 227 101 83 -157 228 102 83 -158 229 104 83 -159 229 105 83 -160 230 106 84 -161 230 108 84 -162 231 109 84 -163 231 110 85 -164 232 112 85 -165 232 113 85 -166 232 114 86 -167 233 116 86 -168 233 117 87 -169 234 118 87 -170 234 120 88 -171 235 121 88 -172 235 123 89 -173 235 124 89 -174 236 125 90 -175 236 127 91 -176 237 128 91 -177 237 129 92 -178 237 131 92 -179 238 132 93 -180 238 133 94 -181 238 135 94 -182 239 136 95 -183 239 138 96 -184 239 139 97 -185 240 140 97 -186 240 142 98 -187 240 143 99 -188 241 144 100 -189 241 146 101 -190 241 147 101 -191 241 148 102 -192 242 150 103 -193 242 151 104 -194 242 153 105 -195 243 154 106 -196 243 155 107 -197 243 157 108 -198 243 158 108 -199 244 159 109 -200 244 161 110 -201 244 162 111 -202 244 163 112 -203 245 165 113 -204 245 166 114 -205 245 168 115 -206 245 169 116 -207 245 170 117 -208 246 172 118 -209 246 173 119 -210 246 174 120 -211 246 176 121 -212 247 177 122 -213 247 178 124 -214 247 180 125 -215 247 181 126 -216 247 183 127 -217 247 184 128 -218 248 185 129 -219 248 187 130 -220 248 188 131 -221 248 189 132 -222 248 191 134 -223 249 192 135 -224 249 194 136 -225 249 195 137 -226 249 196 138 -227 249 198 139 -228 249 199 141 -229 250 200 142 -230 250 202 143 -231 250 203 144 -232 250 205 145 -233 250 206 147 -234 250 207 148 -235 250 209 149 -236 251 210 150 -237 251 212 152 -238 251 213 153 -239 251 214 154 -240 251 216 156 -241 251 217 157 -242 251 219 158 -243 252 220 159 -244 252 221 161 -245 252 223 162 -246 252 224 163 -247 252 226 165 -248 252 227 166 -249 252 228 167 -250 252 230 169 -251 253 231 170 -252 253 233 172 -253 253 234 173 -254 253 235 174 -255 253 237 176 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Oxy.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Oxy.lut deleted file mode 100644 index cabc53237..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Oxy.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 63 5 5 -1 65 5 5 -2 67 5 5 -3 68 5 6 -4 70 6 6 -5 72 6 7 -6 73 6 7 -7 75 6 7 -8 77 6 8 -9 78 6 8 -10 80 6 9 -11 82 6 9 -12 83 7 10 -13 85 7 10 -14 87 7 11 -15 89 7 11 -16 90 7 11 -17 92 7 12 -18 94 7 12 -19 95 7 12 -20 97 7 13 -21 99 6 13 -22 101 6 13 -23 102 6 14 -24 104 6 14 -25 106 6 14 -26 107 6 14 -27 109 6 14 -28 111 5 15 -29 113 5 15 -30 114 5 15 -31 116 5 15 -32 118 5 15 -33 119 4 14 -34 121 4 14 -35 123 4 14 -36 124 5 14 -37 126 5 13 -38 128 6 13 -39 129 6 12 -40 131 7 12 -41 132 9 11 -42 133 10 10 -43 135 12 10 -44 136 14 9 -45 137 16 9 -46 138 17 8 -47 139 19 8 -48 141 21 8 -49 142 23 7 -50 143 24 7 -51 92 68 64 -52 80 79 79 -53 81 80 80 -54 82 81 81 -55 83 82 82 -56 84 83 83 -57 85 84 84 -58 86 85 85 -59 86 86 86 -60 87 87 87 -61 88 88 87 -62 89 89 88 -63 90 90 89 -64 91 91 90 -65 92 92 91 -66 93 92 92 -67 94 93 93 -68 95 94 94 -69 96 95 95 -70 97 96 96 -71 98 97 97 -72 99 98 98 -73 100 99 99 -74 101 100 100 -75 102 101 101 -76 102 102 102 -77 103 103 102 -78 104 104 103 -79 105 105 104 -80 106 106 105 -81 107 107 106 -82 108 108 107 -83 109 109 108 -84 110 110 109 -85 111 111 110 -86 112 112 111 -87 113 113 112 -88 114 114 113 -89 115 114 114 -90 116 115 115 -91 117 116 116 -92 118 117 117 -93 119 118 118 -94 120 119 119 -95 121 120 120 -96 122 121 121 -97 123 122 122 -98 124 123 123 -99 125 124 124 -100 126 125 125 -101 127 126 126 -102 128 127 127 -103 129 128 128 -104 130 129 129 -105 131 130 130 -106 132 131 131 -107 133 132 132 -108 134 133 133 -109 135 134 134 -110 136 135 135 -111 137 136 136 -112 138 137 137 -113 139 138 138 -114 140 139 139 -115 141 140 140 -116 142 141 141 -117 143 143 142 -118 144 144 143 -119 145 145 144 -120 146 146 145 -121 147 147 146 -122 148 148 147 -123 149 149 148 -124 150 150 149 -125 151 151 150 -126 152 152 151 -127 153 153 152 -128 154 154 153 -129 156 155 154 -130 157 156 155 -131 158 157 156 -132 159 158 157 -133 160 159 159 -134 161 160 160 -135 162 162 161 -136 163 163 162 -137 164 164 163 -138 165 165 164 -139 166 166 165 -140 167 167 166 -141 168 168 167 -142 170 169 168 -143 171 170 169 -144 172 171 171 -145 173 173 172 -146 174 174 173 -147 175 175 174 -148 176 176 175 -149 177 177 176 -150 178 178 177 -151 180 179 178 -152 181 180 179 -153 182 182 181 -154 183 183 182 -155 184 184 183 -156 185 185 184 -157 186 186 185 -158 188 187 186 -159 189 188 187 -160 190 190 189 -161 191 191 190 -162 192 192 191 -163 193 193 192 -164 195 194 193 -165 196 195 194 -166 197 197 196 -167 198 198 197 -168 199 199 198 -169 201 200 199 -170 202 201 200 -171 203 203 202 -172 204 204 203 -173 205 205 204 -174 207 206 205 -175 208 208 206 -176 209 209 208 -177 210 210 209 -178 211 211 210 -179 213 212 211 -180 214 214 213 -181 215 215 214 -182 216 216 215 -183 218 217 216 -184 219 219 218 -185 220 220 219 -186 222 221 220 -187 223 223 221 -188 224 224 223 -189 225 225 224 -190 227 226 225 -191 228 228 226 -192 229 229 228 -193 231 230 229 -194 232 232 230 -195 233 233 232 -196 234 234 233 -197 236 236 234 -198 237 237 236 -199 238 238 237 -200 240 240 238 -201 241 241 240 -202 242 242 241 -203 244 244 242 -204 245 247 216 -205 247 253 103 -206 245 252 100 -207 243 251 97 -208 242 250 94 -209 240 249 91 -210 238 247 88 -211 237 246 84 -212 236 245 80 -213 235 243 76 -214 234 242 73 -215 234 240 69 -216 234 239 66 -217 233 237 63 -218 233 235 61 -219 233 233 59 -220 233 232 57 -221 233 230 55 -222 233 228 53 -223 233 226 52 -224 232 225 51 -225 232 223 49 -226 232 221 48 -227 232 220 47 -228 231 218 46 -229 231 216 45 -230 231 214 44 -231 230 213 43 -232 230 211 42 -233 230 209 41 -234 229 208 40 -235 229 206 39 -236 229 205 38 -237 228 203 38 -238 228 201 37 -239 227 200 36 -240 227 198 35 -241 227 196 34 -242 226 195 34 -243 226 193 33 -244 225 192 32 -245 225 190 32 -246 224 188 31 -247 224 187 30 -248 223 185 30 -249 223 184 29 -250 223 182 28 -251 222 181 28 -252 222 179 27 -253 221 178 26 -254 221 176 26 -255 220 174 25 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Phase.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Phase.lut deleted file mode 100644 index 39ad52cd3..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Phase.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 167 119 12 -1 169 118 14 -2 170 117 16 -3 172 116 18 -4 173 115 20 -5 175 115 22 -6 176 114 23 -7 178 113 25 -8 179 112 27 -9 180 111 28 -10 182 110 30 -11 183 109 31 -12 184 107 33 -13 186 106 35 -14 187 105 36 -15 188 104 38 -16 189 103 39 -17 190 102 41 -18 192 101 43 -19 193 100 44 -20 194 99 46 -21 195 98 47 -22 196 97 49 -23 197 96 51 -24 198 95 52 -25 199 93 54 -26 200 92 56 -27 201 91 58 -28 202 90 59 -29 203 89 61 -30 204 88 63 -31 205 86 65 -32 206 85 67 -33 207 84 68 -34 208 83 70 -35 209 81 72 -36 210 80 74 -37 211 79 76 -38 211 78 78 -39 212 76 80 -40 213 75 83 -41 214 74 85 -42 214 72 87 -43 215 71 89 -44 216 69 91 -45 216 68 94 -46 217 67 96 -47 218 65 99 -48 218 64 101 -49 219 62 104 -50 219 61 106 -51 220 59 109 -52 220 58 112 -53 220 57 114 -54 221 55 117 -55 221 54 120 -56 221 52 123 -57 222 51 126 -58 222 49 129 -59 222 48 132 -60 222 47 135 -61 222 45 138 -62 222 44 141 -63 222 43 144 -64 222 42 147 -65 222 41 150 -66 222 40 153 -67 221 39 156 -68 221 38 160 -69 221 38 163 -70 220 37 166 -71 220 37 169 -72 219 37 172 -73 219 37 175 -74 218 37 178 -75 217 37 181 -76 216 38 184 -77 216 38 187 -78 215 39 190 -79 214 40 193 -80 213 41 195 -81 212 42 198 -82 211 43 201 -83 210 44 203 -84 209 45 206 -85 207 47 208 -86 206 48 210 -87 205 50 212 -88 204 51 215 -89 202 53 217 -90 201 54 219 -91 199 56 220 -92 198 58 222 -93 196 60 224 -94 195 61 226 -95 193 63 227 -96 192 65 229 -97 190 66 230 -98 188 68 232 -99 187 70 233 -100 185 71 234 -101 183 73 235 -102 181 75 236 -103 180 77 237 -104 178 78 238 -105 176 80 239 -106 174 82 240 -107 172 83 240 -108 170 85 241 -109 168 86 242 -110 166 88 242 -111 164 90 242 -112 162 91 243 -113 160 93 243 -114 158 94 243 -115 156 96 243 -116 153 97 244 -117 151 99 244 -118 149 100 244 -119 147 102 243 -120 144 103 243 -121 142 105 243 -122 140 106 243 -123 137 107 242 -124 135 109 242 -125 132 110 242 -126 130 112 241 -127 127 113 240 -128 125 114 240 -129 122 115 239 -130 120 117 238 -131 117 118 237 -132 115 119 236 -133 112 120 235 -134 109 122 234 -135 106 123 233 -136 104 124 232 -137 101 125 230 -138 98 126 229 -139 95 127 228 -140 93 128 226 -141 90 129 225 -142 87 130 223 -143 84 131 221 -144 82 132 220 -145 79 133 218 -146 76 134 216 -147 73 135 214 -148 71 136 212 -149 68 137 210 -150 66 137 208 -151 63 138 206 -152 61 139 204 -153 58 139 202 -154 56 140 200 -155 53 141 198 -156 51 141 196 -157 49 142 194 -158 47 142 192 -159 45 143 190 -160 43 143 188 -161 42 144 185 -162 40 144 183 -163 38 144 181 -164 37 145 179 -165 35 145 177 -166 34 146 175 -167 33 146 173 -168 32 146 171 -169 31 147 169 -170 30 147 167 -171 29 147 165 -172 28 147 163 -173 27 148 161 -174 26 148 159 -175 25 148 157 -176 24 148 156 -177 23 149 154 -178 22 149 152 -179 22 149 150 -180 21 149 148 -181 20 150 146 -182 19 150 144 -183 18 150 142 -184 17 150 140 -185 16 151 138 -186 15 151 136 -187 15 151 134 -188 14 151 132 -189 13 151 130 -190 12 152 128 -191 12 152 125 -192 11 152 123 -193 11 152 121 -194 11 152 119 -195 11 153 116 -196 11 153 114 -197 12 153 112 -198 13 153 109 -199 15 153 107 -200 16 153 104 -201 18 153 102 -202 20 153 99 -203 22 153 96 -204 25 154 93 -205 27 154 91 -206 30 154 88 -207 33 154 85 -208 36 153 82 -209 39 153 79 -210 42 153 76 -211 45 153 73 -212 49 153 69 -213 52 153 66 -214 56 152 63 -215 59 152 60 -216 63 152 56 -217 67 151 53 -218 71 151 50 -219 74 150 46 -220 78 150 43 -221 82 149 40 -222 86 149 37 -223 90 148 34 -224 93 147 32 -225 97 146 29 -226 100 146 27 -227 104 145 25 -228 107 144 23 -229 110 143 21 -230 113 142 19 -231 116 141 18 -232 119 141 17 -233 121 140 16 -234 124 139 15 -235 127 138 15 -236 129 137 14 -237 131 136 14 -238 134 135 13 -239 136 135 13 -240 138 134 13 -241 140 133 13 -242 142 132 13 -243 145 131 13 -244 147 130 13 -245 149 129 13 -246 151 128 13 -247 153 127 13 -248 154 126 13 -249 156 125 13 -250 158 124 13 -251 160 123 13 -252 162 122 13 -253 164 121 13 -254 166 120 12 -255 167 119 12 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Solar.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Solar.lut deleted file mode 100644 index 1a4b19b3d..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Solar.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 51 19 23 -1 52 20 24 -2 53 20 24 -3 55 21 25 -4 56 21 25 -5 57 21 26 -6 58 22 26 -7 59 22 27 -8 61 23 27 -9 62 23 28 -10 63 23 28 -11 64 24 28 -12 66 24 29 -13 67 25 29 -14 68 25 30 -15 69 25 30 -16 71 26 30 -17 72 26 31 -18 73 27 31 -19 74 27 31 -20 76 27 32 -21 77 28 32 -22 78 28 32 -23 79 28 33 -24 81 29 33 -25 82 29 33 -26 83 29 34 -27 84 30 34 -28 86 30 34 -29 87 30 34 -30 88 31 35 -31 89 31 35 -32 91 31 35 -33 92 32 35 -34 93 32 35 -35 94 32 35 -36 96 33 36 -37 97 33 36 -38 98 33 36 -39 99 34 36 -40 101 34 36 -41 102 34 36 -42 103 35 36 -43 104 35 36 -44 106 36 36 -45 107 36 36 -46 108 36 36 -47 109 37 36 -48 111 37 36 -49 112 37 36 -50 113 38 36 -51 114 38 36 -52 116 38 36 -53 117 39 36 -54 118 39 36 -55 119 40 36 -56 120 40 35 -57 122 41 35 -58 123 41 35 -59 124 42 35 -60 125 42 35 -61 126 43 34 -62 127 43 34 -63 129 44 34 -64 130 44 34 -65 131 45 33 -66 132 45 33 -67 133 46 33 -68 134 46 33 -69 135 47 32 -70 136 48 32 -71 137 48 32 -72 138 49 31 -73 139 50 31 -74 140 50 31 -75 141 51 31 -76 142 52 30 -77 143 52 30 -78 144 53 30 -79 145 54 29 -80 146 55 29 -81 147 56 29 -82 148 56 28 -83 149 57 28 -84 150 58 28 -85 151 59 27 -86 151 60 27 -87 152 60 27 -88 153 61 27 -89 154 62 26 -90 155 63 26 -91 156 64 26 -92 156 65 25 -93 157 66 25 -94 158 67 25 -95 159 67 25 -96 160 68 24 -97 160 69 24 -98 161 70 24 -99 162 71 23 -100 163 72 23 -101 164 73 23 -102 164 74 23 -103 165 75 23 -104 166 76 22 -105 166 77 22 -106 167 78 22 -107 168 79 22 -108 169 80 21 -109 169 81 21 -110 170 82 21 -111 171 83 21 -112 171 84 21 -113 172 85 20 -114 173 86 20 -115 173 87 20 -116 174 88 20 -117 175 89 20 -118 175 90 20 -119 176 91 20 -120 177 92 19 -121 177 93 19 -122 178 94 19 -123 179 95 19 -124 179 96 19 -125 180 97 19 -126 180 98 19 -127 181 99 19 -128 182 100 19 -129 182 101 19 -130 183 102 19 -131 183 103 18 -132 184 104 18 -133 185 105 18 -134 185 106 18 -135 186 107 18 -136 186 108 18 -137 187 109 18 -138 187 110 18 -139 188 111 19 -140 188 113 19 -141 189 114 19 -142 190 115 19 -143 190 116 19 -144 191 117 19 -145 191 118 19 -146 192 119 19 -147 192 120 19 -148 193 121 19 -149 193 122 20 -150 194 123 20 -151 194 124 20 -152 195 126 20 -153 195 127 20 -154 196 128 20 -155 196 129 21 -156 197 130 21 -157 197 131 21 -158 198 132 21 -159 198 133 22 -160 199 134 22 -161 199 135 22 -162 199 137 22 -163 200 138 23 -164 200 139 23 -165 201 140 23 -166 201 141 24 -167 202 142 24 -168 202 143 24 -169 203 144 25 -170 203 146 25 -171 203 147 25 -172 204 148 26 -173 204 149 26 -174 205 150 27 -175 205 151 27 -176 205 153 27 -177 206 154 28 -178 206 155 28 -179 207 156 29 -180 207 157 29 -181 207 158 30 -182 208 159 30 -183 208 161 31 -184 209 162 31 -185 209 163 32 -186 209 164 32 -187 210 165 32 -188 210 167 33 -189 210 168 33 -190 211 169 34 -191 211 170 34 -192 211 171 35 -193 212 173 36 -194 212 174 36 -195 212 175 37 -196 213 176 37 -197 213 177 38 -198 213 179 38 -199 214 180 39 -200 214 181 39 -201 214 182 40 -202 215 183 40 -203 215 185 41 -204 215 186 42 -205 216 187 42 -206 216 188 43 -207 216 190 43 -208 216 191 44 -209 217 192 44 -210 217 193 45 -211 217 195 46 -212 217 196 46 -213 218 197 47 -214 218 198 47 -215 218 200 48 -216 218 201 49 -217 219 202 49 -218 219 203 50 -219 219 205 51 -220 219 206 51 -221 220 207 52 -222 220 208 52 -223 220 210 53 -224 220 211 54 -225 220 212 54 -226 221 214 55 -227 221 215 56 -228 221 216 56 -229 221 218 57 -230 221 219 58 -231 222 220 58 -232 222 222 59 -233 222 223 60 -234 222 224 60 -235 222 226 61 -236 222 227 61 -237 223 228 62 -238 223 230 63 -239 223 231 63 -240 223 232 64 -241 223 234 65 -242 223 235 65 -243 223 236 66 -244 223 238 67 -245 223 239 68 -246 224 240 68 -247 224 242 69 -248 224 243 70 -249 224 245 70 -250 224 246 71 -251 224 247 72 -252 224 249 72 -253 224 250 73 -254 224 252 74 -255 224 253 74 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Speed.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Speed.lut deleted file mode 100644 index e450ce9eb..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Speed.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 23 35 18 -1 23 35 19 -2 23 36 20 -3 23 37 21 -4 23 38 21 -5 24 39 22 -6 24 40 23 -7 24 41 24 -8 24 42 24 -9 24 43 25 -10 24 44 26 -11 25 45 26 -12 25 46 27 -13 25 47 28 -14 25 48 28 -15 25 49 29 -16 25 50 30 -17 25 51 30 -18 25 52 31 -19 25 52 32 -20 25 53 32 -21 25 54 33 -22 25 55 33 -23 25 56 34 -24 24 57 34 -25 24 58 35 -26 24 59 35 -27 24 60 36 -28 24 61 36 -29 24 62 37 -30 23 63 37 -31 23 64 38 -32 23 65 38 -33 23 66 39 -34 22 67 39 -35 22 68 39 -36 22 69 40 -37 21 70 40 -38 21 71 40 -39 21 72 41 -40 20 73 41 -41 20 74 41 -42 20 74 42 -43 19 75 42 -44 19 76 42 -45 18 77 42 -46 18 78 43 -47 17 79 43 -48 17 80 43 -49 16 81 43 -50 16 82 43 -51 15 83 44 -52 15 84 44 -53 15 85 44 -54 14 86 44 -55 14 87 44 -56 13 88 44 -57 13 89 44 -58 12 90 44 -59 12 91 44 -60 12 92 44 -61 11 92 44 -62 11 93 44 -63 11 94 44 -64 11 95 44 -65 10 96 44 -66 10 97 44 -67 10 98 44 -68 10 99 44 -69 11 100 44 -70 11 101 44 -71 11 102 43 -72 12 103 43 -73 12 103 43 -74 13 104 43 -75 13 105 43 -76 14 106 42 -77 15 107 42 -78 16 108 42 -79 17 109 41 -80 18 110 41 -81 19 111 41 -82 20 111 41 -83 21 112 40 -84 22 113 40 -85 24 114 39 -86 25 115 39 -87 26 116 39 -88 28 117 38 -89 29 117 38 -90 31 118 37 -91 32 119 37 -92 34 120 36 -93 35 121 36 -94 37 121 35 -95 38 122 35 -96 40 123 34 -97 42 124 33 -98 43 125 33 -99 45 125 32 -100 46 126 32 -101 48 127 31 -102 50 128 30 -103 52 129 30 -104 53 129 29 -105 55 130 28 -106 57 131 28 -107 59 131 27 -108 60 132 26 -109 62 133 26 -110 64 134 25 -111 66 134 24 -112 67 135 23 -113 69 136 23 -114 71 137 22 -115 73 137 21 -116 75 138 20 -117 77 139 20 -118 78 139 19 -119 80 140 18 -120 82 141 17 -121 84 141 16 -122 86 142 16 -123 87 143 15 -124 89 143 14 -125 91 144 13 -126 93 145 12 -127 95 145 12 -128 97 146 11 -129 98 146 10 -130 100 147 9 -131 102 148 9 -132 104 148 8 -133 106 149 8 -134 107 150 7 -135 109 150 7 -136 111 151 6 -137 113 151 6 -138 115 152 5 -139 116 153 5 -140 118 153 5 -141 120 154 5 -142 122 155 5 -143 124 155 5 -144 125 156 5 -145 127 156 6 -146 129 157 6 -147 131 158 6 -148 132 158 7 -149 134 159 7 -150 136 159 8 -151 138 160 9 -152 139 161 10 -153 141 161 11 -154 143 162 12 -155 145 162 13 -156 146 163 14 -157 148 164 15 -158 150 164 16 -159 151 165 17 -160 153 165 18 -161 155 166 20 -162 157 167 21 -163 158 167 22 -164 160 168 23 -165 162 168 25 -166 163 169 26 -167 165 170 28 -168 167 170 29 -169 168 171 30 -170 170 171 32 -171 171 172 33 -172 173 173 35 -173 175 173 36 -174 176 174 38 -175 178 175 40 -176 179 175 41 -177 181 176 43 -178 182 177 44 -179 184 177 46 -180 185 178 48 -181 187 179 50 -182 188 179 51 -183 190 180 53 -184 191 181 55 -185 193 181 57 -186 194 182 59 -187 195 183 60 -188 197 183 62 -189 198 184 64 -190 199 185 66 -191 201 186 68 -192 202 186 70 -193 203 187 72 -194 205 188 74 -195 206 189 76 -196 207 190 78 -197 208 190 80 -198 209 191 82 -199 211 192 84 -200 212 193 86 -201 213 194 88 -202 214 195 91 -203 215 196 93 -204 216 196 95 -205 217 197 97 -206 218 198 99 -207 219 199 101 -208 220 200 103 -209 221 201 106 -210 222 202 108 -211 223 203 110 -212 223 204 112 -213 224 205 114 -214 225 206 116 -215 226 207 119 -216 227 208 121 -217 228 209 123 -218 229 210 125 -219 229 211 127 -220 230 212 129 -221 231 213 132 -222 232 214 134 -223 232 215 136 -224 233 216 138 -225 234 217 140 -226 235 219 143 -227 235 220 145 -228 236 221 147 -229 237 222 149 -230 238 223 151 -231 238 224 153 -232 239 225 156 -233 240 226 158 -234 240 227 160 -235 241 229 162 -236 242 230 164 -237 242 231 166 -238 243 232 168 -239 244 233 171 -240 244 234 173 -241 245 236 175 -242 246 237 177 -243 246 238 179 -244 247 239 181 -245 248 240 183 -246 248 241 185 -247 249 243 188 -248 250 244 190 -249 250 245 192 -250 251 246 194 -251 252 247 196 -252 252 249 198 -253 253 250 200 -254 254 251 202 -255 254 252 205 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Tempo.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Tempo.lut deleted file mode 100644 index 8a6985a5e..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Tempo.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 20 29 67 -1 21 30 68 -2 21 31 68 -3 21 32 69 -4 21 33 69 -5 22 34 70 -6 22 35 70 -7 22 36 71 -8 22 37 71 -9 22 38 72 -10 23 39 72 -11 23 40 73 -12 23 41 74 -13 23 42 74 -14 24 43 75 -15 24 44 75 -16 24 45 76 -17 24 46 76 -18 24 47 77 -19 24 47 78 -20 25 48 78 -21 25 49 79 -22 25 50 79 -23 25 51 80 -24 25 52 80 -25 26 53 81 -26 26 54 82 -27 26 55 82 -28 26 56 83 -29 26 57 83 -30 26 58 84 -31 26 59 84 -32 27 60 85 -33 27 60 86 -34 27 61 86 -35 27 62 87 -36 27 63 87 -37 27 64 88 -38 27 65 89 -39 27 66 89 -40 27 67 90 -41 27 68 90 -42 27 69 91 -43 28 70 91 -44 28 70 92 -45 28 71 93 -46 28 72 93 -47 28 73 94 -48 28 74 94 -49 28 75 95 -50 28 76 96 -51 28 77 96 -52 28 78 97 -53 28 79 97 -54 27 80 98 -55 27 81 98 -56 27 81 99 -57 27 82 100 -58 27 83 100 -59 27 84 101 -60 27 85 101 -61 27 86 102 -62 27 87 102 -63 27 88 103 -64 26 89 103 -65 26 90 104 -66 26 91 105 -67 26 92 105 -68 26 93 106 -69 25 94 106 -70 25 94 107 -71 25 95 107 -72 25 96 108 -73 24 97 108 -74 24 98 109 -75 24 99 109 -76 23 100 110 -77 23 101 110 -78 23 102 111 -79 22 103 111 -80 22 104 112 -81 22 105 112 -82 21 106 113 -83 21 107 113 -84 21 108 114 -85 20 109 114 -86 20 109 115 -87 20 110 115 -88 19 111 115 -89 19 112 116 -90 19 113 116 -91 18 114 117 -92 18 115 117 -93 18 116 117 -94 17 117 118 -95 17 118 118 -96 17 119 119 -97 17 120 119 -98 16 121 119 -99 16 122 120 -100 16 123 120 -101 16 124 120 -102 16 125 121 -103 16 125 121 -104 17 126 121 -105 17 127 122 -106 17 128 122 -107 18 129 122 -108 18 130 123 -109 19 131 123 -110 19 132 123 -111 20 133 123 -112 21 134 124 -113 22 135 124 -114 23 136 124 -115 24 137 124 -116 25 137 125 -117 26 138 125 -118 28 139 125 -119 29 140 125 -120 31 141 125 -121 32 142 126 -122 34 143 126 -123 35 144 126 -124 37 144 126 -125 38 145 126 -126 40 146 127 -127 42 147 127 -128 44 148 127 -129 46 149 127 -130 47 149 127 -131 49 150 128 -132 51 151 128 -133 53 152 128 -134 55 153 128 -135 57 153 128 -136 59 154 129 -137 61 155 129 -138 63 156 129 -139 65 157 129 -140 67 157 129 -141 69 158 130 -142 71 159 130 -143 73 160 130 -144 75 160 131 -145 77 161 131 -146 79 162 131 -147 81 162 131 -148 83 163 132 -149 85 164 132 -150 87 165 133 -151 89 165 133 -152 91 166 133 -153 93 167 134 -154 95 167 134 -155 97 168 135 -156 99 169 135 -157 101 169 136 -158 103 170 136 -159 105 171 137 -160 106 171 137 -161 108 172 138 -162 110 173 138 -163 112 174 139 -164 114 174 139 -165 116 175 140 -166 118 176 141 -167 119 176 141 -168 121 177 142 -169 123 178 143 -170 125 178 143 -171 127 179 144 -172 128 180 145 -173 130 180 145 -174 132 181 146 -175 134 182 147 -176 136 182 148 -177 137 183 148 -178 139 184 149 -179 141 184 150 -180 142 185 151 -181 144 186 152 -182 146 186 153 -183 148 187 153 -184 149 188 154 -185 151 188 155 -186 153 189 156 -187 154 190 157 -188 156 191 158 -189 157 191 159 -190 159 192 160 -191 161 193 161 -192 162 193 162 -193 164 194 163 -194 166 195 164 -195 167 195 165 -196 169 196 166 -197 170 197 167 -198 172 198 168 -199 174 198 169 -200 175 199 170 -201 177 200 171 -202 178 201 172 -203 180 201 174 -204 181 202 175 -205 183 203 176 -206 184 204 177 -207 186 204 178 -208 187 205 179 -209 189 206 181 -210 190 207 182 -211 192 207 183 -212 193 208 184 -213 195 209 185 -214 196 210 187 -215 198 210 188 -216 199 211 189 -217 201 212 190 -218 202 213 192 -219 204 214 193 -220 205 214 194 -221 207 215 195 -222 208 216 197 -223 210 217 198 -224 211 218 199 -225 212 218 201 -226 214 219 202 -227 215 220 203 -228 217 221 205 -229 218 222 206 -230 220 223 207 -231 221 223 209 -232 222 224 210 -233 224 225 212 -234 225 226 213 -235 227 227 214 -236 228 228 216 -237 229 229 217 -238 231 229 219 -239 232 230 220 -240 234 231 221 -241 235 232 223 -242 236 233 224 -243 238 234 226 -244 239 235 227 -245 241 236 229 -246 242 237 230 -247 243 238 232 -248 245 238 233 -249 246 239 235 -250 247 240 236 -251 249 241 238 -252 250 242 239 -253 252 243 241 -254 253 244 242 -255 254 245 244 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Thermal.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Thermal.lut deleted file mode 100644 index 63408e96b..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Thermal.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 3 35 51 -1 4 35 53 -2 4 36 55 -3 4 37 57 -4 4 38 58 -5 4 38 60 -6 5 39 62 -7 5 40 64 -8 5 40 66 -9 5 41 68 -10 5 42 70 -11 6 42 73 -12 6 43 75 -13 6 43 77 -14 7 44 79 -15 7 44 81 -16 8 45 83 -17 8 46 86 -18 9 46 88 -19 10 47 90 -20 10 47 93 -21 11 48 95 -22 12 48 98 -23 13 48 100 -24 14 49 103 -25 15 49 105 -26 16 50 108 -27 17 50 110 -28 18 50 113 -29 20 50 116 -30 21 51 118 -31 23 51 121 -32 24 51 124 -33 26 51 126 -34 27 51 129 -35 29 51 132 -36 31 51 134 -37 33 51 137 -38 35 51 140 -39 37 51 142 -40 39 51 144 -41 41 51 147 -42 43 51 149 -43 46 51 151 -44 48 51 153 -45 50 51 154 -46 53 50 156 -47 55 51 157 -48 57 51 157 -49 59 51 158 -50 61 51 159 -51 63 51 159 -52 65 52 159 -53 67 52 159 -54 69 53 159 -55 71 53 159 -56 73 54 159 -57 74 54 158 -58 76 55 158 -59 78 55 158 -60 79 56 157 -61 81 57 157 -62 83 57 156 -63 84 58 156 -64 86 59 156 -65 87 59 155 -66 89 60 155 -67 90 61 154 -68 92 61 154 -69 93 62 153 -70 95 63 153 -71 96 64 152 -72 97 64 152 -73 99 65 151 -74 100 66 151 -75 102 66 150 -76 103 67 150 -77 104 67 149 -78 106 68 149 -79 107 69 148 -80 108 69 148 -81 110 70 148 -82 111 71 147 -83 112 71 147 -84 114 72 146 -85 115 73 146 -86 116 73 146 -87 118 74 145 -88 119 74 145 -89 121 75 144 -90 122 76 144 -91 123 76 144 -92 125 77 143 -93 126 77 143 -94 127 78 143 -95 129 78 142 -96 130 79 142 -97 131 80 142 -98 133 80 142 -99 134 81 141 -100 135 81 141 -101 137 82 141 -102 138 82 140 -103 140 83 140 -104 141 83 140 -105 142 84 139 -106 144 84 139 -107 145 85 139 -108 147 85 138 -109 148 86 138 -110 150 86 138 -111 151 87 137 -112 152 87 137 -113 154 88 136 -114 155 88 136 -115 157 89 136 -116 158 89 135 -117 160 90 135 -118 161 90 134 -119 163 91 134 -120 164 91 133 -121 166 92 133 -122 167 92 133 -123 169 93 132 -124 170 93 131 -125 172 93 131 -126 173 94 130 -127 175 94 130 -128 176 95 129 -129 178 95 129 -130 179 96 128 -131 181 96 127 -132 182 97 127 -133 184 97 126 -134 185 97 125 -135 187 98 124 -136 188 98 124 -137 190 99 123 -138 191 99 122 -139 193 100 121 -140 194 100 120 -141 196 101 120 -142 197 101 119 -143 199 102 118 -144 200 102 117 -145 202 103 116 -146 203 103 115 -147 205 104 114 -148 206 104 113 -149 207 105 112 -150 209 105 111 -151 210 106 110 -152 212 107 109 -153 213 107 108 -154 214 108 107 -155 216 108 105 -156 217 109 104 -157 219 110 103 -158 220 110 102 -159 221 111 101 -160 222 112 100 -161 224 112 98 -162 225 113 97 -163 226 114 96 -164 227 115 95 -165 229 116 93 -166 230 116 92 -167 231 117 91 -168 232 118 90 -169 233 119 88 -170 234 120 87 -171 235 121 86 -172 236 122 85 -173 237 123 83 -174 238 124 82 -175 239 125 81 -176 240 126 80 -177 241 127 79 -178 241 128 78 -179 242 130 76 -180 243 131 75 -181 243 132 74 -182 244 133 73 -183 245 135 72 -184 245 136 71 -185 246 137 70 -186 246 139 69 -187 247 140 69 -188 247 141 68 -189 248 143 67 -190 248 144 66 -191 249 146 65 -192 249 147 65 -193 249 148 64 -194 250 150 63 -195 250 151 63 -196 250 153 62 -197 250 154 62 -198 250 156 62 -199 251 157 61 -200 251 159 61 -201 251 161 61 -202 251 162 60 -203 251 164 60 -204 251 165 60 -205 251 167 60 -206 251 168 60 -207 251 170 60 -208 251 172 60 -209 251 173 60 -210 251 175 60 -211 251 176 60 -212 251 178 60 -213 251 180 60 -214 251 181 61 -215 251 183 61 -216 250 184 61 -217 250 186 62 -218 250 188 62 -219 250 189 62 -220 250 191 63 -221 249 193 63 -222 249 194 64 -223 249 196 64 -224 249 198 65 -225 248 199 65 -226 248 201 66 -227 248 203 67 -228 247 204 67 -229 247 206 68 -230 246 208 69 -231 246 209 69 -232 246 211 70 -233 245 213 71 -234 245 214 72 -235 244 216 72 -236 244 218 73 -237 243 219 74 -238 243 221 75 -239 242 223 76 -240 242 224 77 -241 241 226 77 -242 241 228 78 -243 240 229 79 -244 239 231 80 -245 239 233 81 -246 238 234 82 -247 237 236 83 -248 237 238 84 -249 236 240 85 -250 235 241 86 -251 235 243 86 -252 234 245 87 -253 233 246 88 -254 232 248 89 -255 231 250 90 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Turbid.lut b/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Turbid.lut deleted file mode 100644 index d4454fd08..000000000 --- a/src/main/resources/fiji/plugin/trackmate/gui/displaysettings/luts/Turbid.lut +++ /dev/null @@ -1,257 +0,0 @@ -Index Red Green Blue -0 34 30 27 -1 35 31 27 -2 36 31 28 -3 37 32 28 -4 38 33 29 -5 39 33 29 -6 40 34 30 -7 41 35 30 -8 42 35 31 -9 43 36 31 -10 44 37 31 -11 45 37 32 -12 46 38 32 -13 47 39 33 -14 48 39 33 -15 49 40 34 -16 50 40 34 -17 51 41 35 -18 52 42 35 -19 53 42 36 -20 54 43 36 -21 55 44 36 -22 56 44 37 -23 57 45 37 -24 58 45 38 -25 59 46 38 -26 60 47 39 -27 61 47 39 -28 62 48 39 -29 63 49 40 -30 64 49 40 -31 65 50 41 -32 66 50 41 -33 67 51 41 -34 68 52 42 -35 69 52 42 -36 70 53 42 -37 71 53 43 -38 72 54 43 -39 73 55 44 -40 74 55 44 -41 75 56 44 -42 76 56 45 -43 77 57 45 -44 78 58 45 -45 79 58 46 -46 80 59 46 -47 81 59 46 -48 82 60 47 -49 83 60 47 -50 84 61 47 -51 85 62 48 -52 86 62 48 -53 87 63 48 -54 88 63 49 -55 89 64 49 -56 90 65 49 -57 92 65 49 -58 93 66 50 -59 94 66 50 -60 95 67 50 -61 96 68 51 -62 97 68 51 -63 98 69 51 -64 99 69 51 -65 100 70 52 -66 101 71 52 -67 102 71 52 -68 103 72 52 -69 104 72 53 -70 105 73 53 -71 106 74 53 -72 107 74 53 -73 108 75 53 -74 109 75 54 -75 110 76 54 -76 111 76 54 -77 112 77 54 -78 113 78 54 -79 114 78 55 -80 115 79 55 -81 116 80 55 -82 117 80 55 -83 119 81 55 -84 120 81 55 -85 121 82 55 -86 122 83 56 -87 123 83 56 -88 124 84 56 -89 125 85 56 -90 126 85 56 -91 127 86 56 -92 128 86 56 -93 129 87 56 -94 130 88 57 -95 131 88 57 -96 132 89 57 -97 133 90 57 -98 134 90 57 -99 135 91 57 -100 136 92 57 -101 137 92 57 -102 138 93 57 -103 139 94 57 -104 140 94 57 -105 141 95 57 -106 142 96 57 -107 142 97 58 -108 143 97 58 -109 144 98 58 -110 145 99 58 -111 146 99 58 -112 147 100 58 -113 148 101 58 -114 149 102 58 -115 150 102 58 -116 151 103 58 -117 152 104 58 -118 153 105 58 -119 154 105 58 -120 154 106 58 -121 155 107 58 -122 156 108 58 -123 157 108 58 -124 158 109 58 -125 159 110 59 -126 160 111 59 -127 161 111 59 -128 161 112 59 -129 162 113 59 -130 163 114 59 -131 164 115 59 -132 165 116 59 -133 166 116 59 -134 166 117 59 -135 167 118 59 -136 168 119 60 -137 169 120 60 -138 169 121 60 -139 170 121 60 -140 171 122 60 -141 172 123 60 -142 173 124 60 -143 173 125 61 -144 174 126 61 -145 175 127 61 -146 175 128 61 -147 176 129 61 -148 177 129 62 -149 178 130 62 -150 178 131 62 -151 179 132 62 -152 180 133 63 -153 180 134 63 -154 181 135 63 -155 182 136 64 -156 182 137 64 -157 183 138 64 -158 184 139 65 -159 184 140 65 -160 185 141 66 -161 185 142 66 -162 186 143 66 -163 187 144 67 -164 187 145 67 -165 188 146 68 -166 188 147 68 -167 189 148 69 -168 190 149 69 -169 190 150 70 -170 191 151 71 -171 191 152 71 -172 192 153 72 -173 192 154 73 -174 193 155 73 -175 193 156 74 -176 194 157 75 -177 194 158 75 -178 195 159 76 -179 195 160 77 -180 196 161 78 -181 196 162 78 -182 197 163 79 -183 197 164 80 -184 198 165 81 -185 198 167 82 -186 199 168 83 -187 199 169 84 -188 200 170 84 -189 200 171 85 -190 201 172 86 -191 201 173 87 -192 202 174 88 -193 202 175 89 -194 202 176 90 -195 203 177 91 -196 203 178 92 -197 204 180 94 -198 204 181 95 -199 205 182 96 -200 205 183 97 -201 206 184 98 -202 206 185 99 -203 206 186 100 -204 207 187 101 -205 207 188 102 -206 208 189 104 -207 208 191 105 -208 209 192 106 -209 209 193 107 -210 210 194 108 -211 210 195 110 -212 210 196 111 -213 211 197 112 -214 211 198 113 -215 212 199 115 -216 212 201 116 -217 213 202 117 -218 213 203 119 -219 214 204 120 -220 214 205 121 -221 215 206 122 -222 215 207 124 -223 216 208 125 -224 216 210 126 -225 217 211 128 -226 217 212 129 -227 217 213 130 -228 218 214 132 -229 218 215 133 -230 219 216 135 -231 219 218 136 -232 220 219 137 -233 220 220 139 -234 221 221 140 -235 221 222 142 -236 222 223 143 -237 222 224 144 -238 223 226 146 -239 223 227 147 -240 224 228 149 -241 225 229 150 -242 225 230 152 -243 226 231 153 -244 226 232 155 -245 227 234 156 -246 227 235 157 -247 228 236 159 -248 228 237 160 -249 229 238 162 -250 229 239 163 -251 230 241 165 -252 231 242 166 -253 231 243 168 -254 232 244 169 -255 232 245 171 diff --git a/src/main/resources/fiji/plugin/trackmate/gui/images/TrackMateBVV-logo-16x16.png b/src/main/resources/fiji/plugin/trackmate/gui/images/TrackMateBVV-logo-16x16.png new file mode 100644 index 000000000..2e5b7d5fa Binary files /dev/null and b/src/main/resources/fiji/plugin/trackmate/gui/images/TrackMateBVV-logo-16x16.png differ diff --git a/src/main/resources/fiji/plugin/trackmate/gui/images/bullet_green.png b/src/main/resources/fiji/plugin/trackmate/gui/images/bullet_green.png new file mode 100644 index 000000000..058ad261f Binary files /dev/null and b/src/main/resources/fiji/plugin/trackmate/gui/images/bullet_green.png differ diff --git a/src/main/resources/fiji/plugin/trackmate/gui/images/help.png b/src/main/resources/fiji/plugin/trackmate/gui/images/help.png new file mode 100644 index 000000000..5c870176d Binary files /dev/null and b/src/main/resources/fiji/plugin/trackmate/gui/images/help.png differ diff --git a/src/main/resources/fiji/plugin/trackmate/visualization/bvv/mesh.fp b/src/main/resources/fiji/plugin/trackmate/visualization/bvv/mesh.fp new file mode 100644 index 000000000..fb732ddd4 --- /dev/null +++ b/src/main/resources/fiji/plugin/trackmate/visualization/bvv/mesh.fp @@ -0,0 +1,48 @@ +out vec4 fragColor; + +in vec3 Normal; +in vec3 FragPos; + +uniform vec4 ObjectColor; +uniform float IsSelected; +uniform vec4 SelectionColor; + +const vec3 lightColor1 = 0.5 * vec3(0.9, 0.9, 1); +const vec3 lightDir1 = normalize(vec3(0, -0.2, -1)); + +const vec3 lightColor2 = 0.5 * vec3(0.1, 0.1, 1); +const vec3 lightDir2 = normalize(vec3(1, 1, 0.5)); + +const vec3 ambient = vec3(0.7, 0.7, 0.7); + +const float specularStrength = 5; + +vec3 phong(vec3 norm, vec3 viewDir, vec3 lightDir, vec3 lightColor, float shininess, float specularStrength) +{ + float diff = max(dot(norm, lightDir), 0.0); + vec3 diffuse = diff * lightColor; + + vec3 reflectDir = reflect(-lightDir, norm); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess); + vec3 specular = specularStrength * spec * lightColor; + + return diffuse + specular; +} + +void main() +{ +// fragColor = vec4(ObjectColor, 1); + vec3 norm = normalize(Normal); + vec3 viewDir = normalize(-FragPos); + + vec3 l1 = phong( norm, viewDir, lightDir1, lightColor1, 32, 0.1 ); + vec3 l2 = phong( norm, viewDir, lightDir2, lightColor2, 32, 0.5 ); + + if (IsSelected > 0.5) { + fragColor = vec4( (ambient + l1 + l2), SelectionColor[3]) * SelectionColor; + } else { + float it = dot(norm, viewDir); + fragColor = vec4( it * (ambient + l1 + l2), 1) * ObjectColor + (1-it) * vec4(1,1,1,ObjectColor[3]); + } + +} diff --git a/src/main/resources/fiji/plugin/trackmate/visualization/bvv/mesh.vp b/src/main/resources/fiji/plugin/trackmate/visualization/bvv/mesh.vp new file mode 100644 index 000000000..e86810409 --- /dev/null +++ b/src/main/resources/fiji/plugin/trackmate/visualization/bvv/mesh.vp @@ -0,0 +1,16 @@ +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec3 aNormal; + +out vec3 FragPos; +out vec3 Normal; + +uniform mat4 pvm; +uniform mat4 vm; +uniform mat3 itvm; + +void main() +{ + gl_Position = pvm * vec4( aPos, 1.0 ); + FragPos = vec3(vm * vec4(aPos, 1.0)); + Normal = itvm * aNormal; +} diff --git a/src/test/java/fiji/plugin/trackmate/AssertJTrackMate.java b/src/test/java/fiji/plugin/trackmate/AssertJTrackMate.java new file mode 100644 index 000000000..2096c266b --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/AssertJTrackMate.java @@ -0,0 +1,392 @@ +package fiji.plugin.trackmate; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.assertj.core.presentation.StandardRepresentation; +import org.jgrapht.graph.DefaultWeightedEdge; + +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; + +/** + * Utility class to provide AssertJ assertions for TrackMate objects. + */ +public class AssertJTrackMate +{ + + public static void testDisplaySettingsEquality( final DisplaySettings actual, final DisplaySettings expected ) + { + assertThat( actual ) + .withRepresentation( new StandardRepresentation() + { + @Override + public String toStringOf( final Object obj ) + { + if ( obj instanceof final DisplaySettings ds ) + return String.format( "DisplaySettings[%d]", ds.hashCode() ); + return super.toStringOf( obj ); + } + } ) + .usingRecursiveComparison() + .isEqualTo( expected ); + } + + public static void testSettingsEquality( final Settings actual, final Settings expected ) + { + assertThat( actual ) + .withRepresentation( new StandardRepresentation() + { + @Override + public String toStringOf( final Object obj ) + { + if ( obj instanceof final Settings s ) + return String.format( "Settings[%d]", s.hashCode() ); + return super.toStringOf( obj ); + } + } ) + .usingRecursiveComparison() + .isEqualTo( expected ); + } + + public static void testModelEquality( final Model actual, final Model expected ) + { + testModelEquality( actual, expected, true ); + } + + /** + * Compares two models for equality. + *

    + * If {@code compareTrackIds} is true, tracks are matched by their track IDs. + * If false, tracks are matched by their spot content and names, ignoring + * track ID mismatches (useful after undo/redo operations that may change track IDs). + * + * @param actual the actual Model + * @param expected the expected Model + * @param compareTrackIds if true, compare tracks by ID; if false, compare by spot content and name + */ + public static void testModelEquality( final Model actual, final Model expected, final boolean compareTrackIds ) + { + assertThat( actual ) + .withRepresentation( new StandardRepresentation() + { + @Override + public String toStringOf( final Object obj ) + { + if ( obj instanceof final Model m ) + return String.format( "Model[%d]", m.hashCode() ); + return super.toStringOf( obj ); + } + } ) + .usingRecursiveComparison() + .withEqualsForType( + // Used to pair spots in sets. + ( a, b ) -> a.ID() == b.ID(), Spot.class ) + .ignoringFields( + // Treated separately + "spots", + "featureModel", + "trackModel", + // Listeners + "modelChangeListeners", + // Utils + "logger", + // Transaction state fields + "updateLevel", + "spotsAdded", + "spotsRemoved", + "spotsMoved", + "spotsUpdated", + "eventCache", + // Undo / redo + "undoRedoStack" ) + .isEqualTo( expected ); + + /* + * SpotCollection + */ + + compareSpotCollections( actual.getSpots(), expected.getSpots() ); + + /* + * Feature model + */ + + assertThat( actual.getFeatureModel() ) + .withRepresentation( new StandardRepresentation() + { + @Override + public String toStringOf( final Object obj ) + { + if ( obj instanceof final FeatureModel fm ) + return String.format( "FeatureModel[%d]", fm.hashCode() ); + return super.toStringOf( obj ); + } + } ) + .usingRecursiveComparison() + .ignoringFields( + "model", // Transient + "edgeFeatureValues" ) // Treated separately ) + .isEqualTo( expected.getFeatureModel() ); + + // Compare edge feature values separately, because the keys are + // DefaultWeightedEdge objects + compareEdgeFeatureValues( actual, expected ); + + /* + * Track model + */ + + compareTrackModels( actual.getTrackModel(), expected.getTrackModel(), compareTrackIds ); + } + + private static void compareSpotCollections( final SpotCollection actual, final SpotCollection expected ) + { + // Same frames + assertThat( actual.keySet() ) + .as( "SpotCollection frames" ) + .containsExactlyInAnyOrderElementsOf( expected.keySet() ); + + actual.keySet().forEach( frame -> { + + final Iterable< Spot > actualSpots = actual.iterable( frame, false ); + final Iterable< Spot > expectedSpots = expected.iterable( frame, false ); + + // Same number of spots per frame + assertThat( actualSpots ) + .as( "number of spots in frame %d", frame ) + .hasSameSizeAs( expectedSpots ); + + // Build ID -> Spot lookup for expected + final Map< Integer, Spot > expectedById = new HashMap<>(); + expectedSpots.forEach( s -> expectedById.put( s.ID(), s ) ); + + actualSpots.forEach( actualSpot -> { + + // Spot exists in expected + final Spot expectedSpot = expectedById.get( actualSpot.ID() ); + assertThat( expectedSpot ) + .as( "Spot ID=%d missing in frame %d", actualSpot.ID(), frame ) + .isNotNull(); + + // Delegate to single-spot comparison + assertSpotEquals( actualSpot, expectedSpot ); + } ); + } ); + } + + private static void assertSpotEquals( final Spot actual, final Spot expected ) + { + // 1. Check same implementation type + assertThat( actual.getClass() ) + .as( "Spot ID=%d implementation type", actual.ID() ) + .isEqualTo( expected.getClass() ); + + // 2. Basic identity + assertThat( actual.ID() ) + .as( "Spot ID" ) + .isEqualTo( expected.ID() ); + + assertThat( actual.getName() ) + .as( "Spot ID=%d name", actual.ID() ) + .isEqualTo( expected.getName() ); + + // 3. Feature keys + assertThat( actual.getFeatures().keySet() ) + .as( "Spot ID=%d feature keys", actual.ID() ) + .containsExactlyInAnyOrderElementsOf( + expected.getFeatures().keySet() ); + + // 4. Feature values + assertThat( actual.getFeatures() ) + .as( "Spot ID=%d feature values", actual.ID() ) + .allSatisfy( ( key, value ) -> assertThat( value ) + .as( "Spot ID=%d feature '%s'", actual.ID(), key ) + .isEqualTo( + expected.getFeatures().getOrDefault( key, Double.NaN ) ) ); + + // 5. SpotRoi-specific: polygon coordinates + if ( actual instanceof SpotRoi ) + assertSpotRoiEquals( ( SpotRoi ) actual, ( SpotRoi ) expected ); + } + + private static void assertSpotRoiEquals( final SpotRoi actual, final SpotRoi expected ) + { + // Number of polygon vertices + assertThat( actual.nPoints() ) + .as( "SpotRoi ID=%d number of polygon points", actual.ID() ) + .isEqualTo( expected.nPoints() ); + + // X coordinates of polygon vertices (relative to center) + for ( int i = 0; i < actual.nPoints(); i++ ) + { + final int idx = i; // for lambda capture + assertThat( actual.xr( i ) ) + .as( "SpotRoi ID=%d polygon x[%d]", actual.ID(), idx ) + .isEqualTo( expected.xr( i ) ); + + assertThat( actual.yr( i ) ) + .as( "SpotRoi ID=%d polygon y[%d]", actual.ID(), idx ) + .isEqualTo( expected.yr( i ) ); + } + } + + private static void compareTrackModels( final TrackModel actual, final TrackModel expected, final boolean compareById ) + { + // 1. All spots in the graph (including isolated ones) + assertThat( actual.vertexSet().stream() + .map( s -> s.ID() ) + .collect( Collectors.toSet() ) ) + .as( "TrackModel: spot IDs" ) + .containsExactlyInAnyOrderElementsOf( + expected.vertexSet().stream() + .map( s -> s.ID() ) + .collect( Collectors.toSet() ) ); + + // 2. Edge topology and weights + final Map< String, Double > actualEdges = buildEdgeWeightMap( actual ); + final Map< String, Double > expectedEdges = buildEdgeWeightMap( expected ); + + assertThat( actualEdges.keySet() ) + .as( "TrackModel: edges" ) + .containsExactlyInAnyOrderElementsOf( expectedEdges.keySet() ); + + assertThat( actualEdges ) + .as( "TrackModel: edge weights" ) + .allSatisfy( ( edgeKey, weight ) -> assertThat( weight ) + .as( "weight of edge [%s]", edgeKey ) + .isEqualTo( expectedEdges.get( edgeKey ) ) ); + + // 3. Track structure, visibility and names + compareTracks( actual, expected, compareById ); + } + + /** + * Build "srcID->tgtID" -> weight map, independent of edge object identity. + */ + private static Map< String, Double > buildEdgeWeightMap( final TrackModel model ) + { + final Map< String, Double > map = new LinkedHashMap<>(); + model.edgeSet().forEach( e -> { + final String key = model.getEdgeSource( e ).ID() + "->" + model.getEdgeTarget( e ).ID(); + map.put( key, model.getEdgeWeight( e ) ); + } ); + return map; + } + + /** + * Compares tracks between two TrackModels. + *

    + * If {@code compareById} is true, tracks are matched by their track IDs + * (the default behavior). If false, tracks are matched by their spot content + * and names, ignoring track ID mismatches (useful after undo/redo operations + * that may change track IDs). + * + * @param actual the actual TrackModel + * @param expected the expected TrackModel + * @param compareById if true, compare by track ID; if false, compare by spot content and name + */ + private static void compareTracks( final TrackModel actual, final TrackModel expected, final boolean compareById ) + { + final Set< Integer > actualTrackIDs = actual.trackIDs( false ); + final Set< Integer > expectedTrackIDs = expected.trackIDs( false ); + + if ( compareById ) + { + // Strict comparison: track IDs must match exactly + assertThat( actualTrackIDs ) + .as( "TrackModel: identical tracks IDs" ) + .isEqualTo( expectedTrackIDs ); + + actualTrackIDs.forEach( actualTID -> { + + // Visibility + assertThat( actual.isVisible( actualTID ) ) + .as( "visibility of track ID: [%d]", actualTID ) + .isEqualTo( expected.isVisible( actualTID ) ); + + // Name + assertThat( actual.name( actualTID ) ) + .as( "name of track ID: [%d]", actualTID ) + .isEqualTo( expected.name( actualTID ) ); + } ); + } + else + { + // Flexible comparison: match tracks by spot content and name + assertThat( actualTrackIDs.size() ) + .as( "TrackModel: number of tracks" ) + .isEqualTo( expectedTrackIDs.size() ); + + // Build a map of track spots for matching + final Map< Set< Integer >, Integer > expectedTrackSpotsToId = new HashMap<>(); + for ( final Integer expectedTID : expectedTrackIDs ) + { + final Set< Integer > spotIds = expected.trackSpots( expectedTID ).stream() + .map( Spot::ID ) + .collect( Collectors.toSet() ); + expectedTrackSpotsToId.put( spotIds, expectedTID ); + } + + // For each actual track, find matching expected track by spot content + actualTrackIDs.forEach( actualTID -> { + final Set< Integer > actualSpotIds = actual.trackSpots( actualTID ).stream() + .map( Spot::ID ) + .collect( Collectors.toSet() ); + + final Integer expectedTID = expectedTrackSpotsToId.get( actualSpotIds ); + assertThat( expectedTID ) + .as( "Track with spot IDs %s not found in expected model", actualSpotIds ) + .isNotNull(); + + // Visibility + assertThat( actual.isVisible( actualTID ) ) + .as( "visibility of track with spots %s", actualSpotIds ) + .isEqualTo( expected.isVisible( expectedTID ) ); + + // Name + assertThat( actual.name( actualTID ) ) + .as( "name of track with spots %s", actualSpotIds ) + .isEqualTo( expected.name( expectedTID ) ); + } ); + } + } + + private static void compareEdgeFeatureValues( final Model m1, final Model m2 ) + { + final FeatureModel fm1 = m1.getFeatureModel(); + final FeatureModel fm2 = m2.getFeatureModel(); + + // If so, iterate edges from the track model instead: + m1.getTrackModel().edgeSet().forEach( edge -> { + final int sId = m1.getTrackModel().getEdgeSource( edge ).ID(); + final int tId = m1.getTrackModel().getEdgeTarget( edge ).ID(); + + // Find matching edge in readback model + final DefaultWeightedEdge matchedEdge = m2.getTrackModel().edgeSet().stream() + .filter( e -> m2.getTrackModel().getEdgeSource( e ).ID() == sId + && m2.getTrackModel().getEdgeTarget( e ).ID() == tId ) + .findFirst() + .orElseThrow( () -> new AssertionError( "No matching edge for ID" + sId + " -> ID" + tId ) ); + + // Compare feature by feature + fm1.getEdgeFeatures() + .forEach( featureKey -> { + final Double actual = fm1.getEdgeFeature( edge, featureKey ); + final Double expected = fm2.getEdgeFeature( matchedEdge, featureKey ); + + // Both null is ok. + if ( actual == null && expected == null ) + return; + + assertThat( actual ) + .as( "feature '%s' on edge (ID%d -> ID%d)", featureKey, sId, tId ) + .isEqualTo( expected ); + } ); + } ); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/ModelTest.java b/src/test/java/fiji/plugin/trackmate/ModelTest.java index 6c05a893f..34cac6d7c 100644 --- a/src/test/java/fiji/plugin/trackmate/ModelTest.java +++ b/src/test/java/fiji/plugin/trackmate/ModelTest.java @@ -42,17 +42,17 @@ public class ModelTest { public void testTrackVisibility() { final Model model = new Model(); // Build track 1 with 5 spots - final Spot s1 = new Spot( 0d, 0d, 0d, 1d, -1d, "S1" ); - final Spot s2 = new Spot( 0d, 0d, 0d, 1d, -1d, "S2" ); - final Spot s3 = new Spot( 0d, 0d, 0d, 1d, -1d, "S3" ); - final Spot s4 = new Spot( 0d, 0d, 0d, 1d, -1d, "S4" ); - final Spot s5 = new Spot( 0d, 0d, 0d, 1d, -1d, "S5" ); + final Spot s1 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S1" ); + final Spot s2 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S2" ); + final Spot s3 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S3" ); + final Spot s4 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S4" ); + final Spot s5 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S5" ); // Build track 2 with 2 spots - final Spot s6 = new Spot( 0d, 0d, 0d, 1d, -1d, "S6" ); - final Spot s7 = new Spot( 0d, 0d, 0d, 1d, -1d, "S7" ); + final Spot s6 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S6" ); + final Spot s7 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S7" ); // Build track 3 with 2 spots - final Spot s8 = new Spot( 0d, 0d, 0d, 1d, -1d, "S8" ); - final Spot s9 = new Spot( 0d, 0d, 0d, 1d, -1d, "S9" ); + final Spot s8 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S8" ); + final Spot s9 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S9" ); model.beginUpdate(); try { @@ -161,11 +161,11 @@ public void testTrackNumber() { assertEquals(0, model.getTrackModel().nTracks(false)); // Build track with 5 spots - final Spot s1 = new Spot( 0d, 0d, 0d, 1d, -1d, "S1" ); - final Spot s2 = new Spot( 0d, 0d, 0d, 1d, -1d, "S2" ); - final Spot s3 = new Spot( 0d, 0d, 0d, 1d, -1d, "S3" ); - final Spot s4 = new Spot( 0d, 0d, 0d, 1d, -1d, "S4" ); - final Spot s5 = new Spot( 0d, 0d, 0d, 1d, -1d, "S5" ); + final Spot s1 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S1" ); + final Spot s2 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S2" ); + final Spot s3 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S3" ); + final Spot s4 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S4" ); + final Spot s5 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S5" ); model.beginUpdate(); try { model.addSpotTo(s1, 0); @@ -242,11 +242,11 @@ public void modelChanged(final ModelChangeEvent event) { model.addModelChangeListener(eventLogger); - final Spot s1 = new Spot( 0d, 0d, 0d, 1d, -1d, "S1" ); - final Spot s2 = new Spot( 0d, 0d, 0d, 1d, -1d, "S2" ); - final Spot s3 = new Spot( 0d, 0d, 0d, 1d, -1d, "S3" ); - final Spot s4 = new Spot( 0d, 0d, 0d, 1d, -1d, "S4" ); - final Spot s5 = new Spot( 0d, 0d, 0d, 1d, -1d, "S5" ); + final Spot s1 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S1" ); + final Spot s2 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S2" ); + final Spot s3 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S3" ); + final Spot s4 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S4" ); + final Spot s5 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S5" ); // System.out.println("Create the graph in one update:"); model.beginUpdate(); @@ -381,7 +381,7 @@ public void testRemovingWholeTracksAtOnce() { Spot previous = null; Spot spot = null; for (int j = 0; j < DEPTH; j++) { - spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpotTo(spot, j); if (i == 0) { trackSpots.add(spot); @@ -444,11 +444,11 @@ public void exampleManipulation() { // Add an event listener now model.addModelChangeListener(new EventLogger()); - final Spot s1 = new Spot( 0d, 0d, 0d, 1d, -1d, "S1" ); - final Spot s2 = new Spot( 0d, 0d, 0d, 1d, -1d, "S2" ); - final Spot s3 = new Spot( 0d, 0d, 0d, 1d, -1d, "S3" ); - final Spot s4 = new Spot( 0d, 0d, 0d, 1d, -1d, "S4" ); - final Spot s5 = new Spot( 0d, 0d, 0d, 1d, -1d, "S5" ); + final Spot s1 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S1" ); + final Spot s2 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S2" ); + final Spot s3 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S3" ); + final Spot s4 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S4" ); + final Spot s5 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S5" ); System.out.println("Create the graph in one update:"); model.beginUpdate(); diff --git a/src/test/java/fiji/plugin/trackmate/SpotCollectionTest.java b/src/test/java/fiji/plugin/trackmate/SpotCollectionTest.java index 9985f7b26..c111fbfd2 100644 --- a/src/test/java/fiji/plugin/trackmate/SpotCollectionTest.java +++ b/src/test/java/fiji/plugin/trackmate/SpotCollectionTest.java @@ -70,7 +70,7 @@ public void setUp() throws Exception final HashSet< Spot > spots = new HashSet<>( 100 ); for ( int j = 0; j < N_SPOTS; j++ ) { - final Spot spot = new Spot( j, j, j, 1d, -1d ); + final Spot spot = new SpotBase( j, j, j, 1d, -1d ); spot.putFeature( Spot.POSITION_T, Double.valueOf( i ) ); spot.putFeature( Spot.QUALITY, Double.valueOf( j ) ); spot.putFeature( Spot.RADIUS, Double.valueOf( j / 2 ) ); @@ -101,7 +101,7 @@ public void testAdd() } // Add a spot to target frame final int targetFrame = 1 + 2 * new Random().nextInt( 50 ); - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); sc.add( spot, targetFrame ); // Test for ( final Integer frame : frames ) @@ -221,7 +221,7 @@ public void testGetClosestSpot() final FeatureFilter filter = new FeatureFilter( Spot.QUALITY, 20d, false ); sc.filter( filter ); - final Spot location = new Spot( 50.1, 50.1, 50.1, 1d, -1d ); + final Spot location = new SpotBase( 50.1, 50.1, 50.1, 1d, -1d ); for ( final Integer frame : frames ) { // Closest non-visible spot should be the one with QUALITY = 50 @@ -240,8 +240,8 @@ public void testGetSpotAt() final FeatureFilter filter = new FeatureFilter( Spot.QUALITY, 20d, false ); sc.filter( filter ); - final Spot location1 = new Spot( 50.1, 50.1, 50.1, 1d, -1d ); - final Spot location2 = new Spot( 10.1, 10.1, 10.1, 1d, -1d ); + final Spot location1 = new SpotBase( 50.1, 50.1, 50.1, 1d, -1d ); + final Spot location2 = new SpotBase( 10.1, 10.1, 10.1, 1d, -1d ); for ( final Integer frame : frames ) { // The closest non-visible spot should be the one with QUALITY = 50 @@ -391,7 +391,7 @@ public void testPut() final HashSet< Spot > spots = new HashSet<>( N_SPOTS_TO_ADD ); for ( int i = 0; i < N_SPOTS_TO_ADD; i++ ) { - spots.add( new Spot( -1d, -1d, -1d, 1d, -1d ) ); + spots.add( new SpotBase( -1d, -1d, -1d, 1d, -1d ) ); } // Add it to a new frame int targetFrame = 1000; @@ -435,7 +435,7 @@ public void testFirstKey() final HashSet< Spot > spots = new HashSet<>( N_SPOTS_TO_ADD ); for ( int i = 0; i < N_SPOTS_TO_ADD; i++ ) { - spots.add( new Spot( -1d, -1d, -1d, 1d, -1d ) ); + spots.add( new SpotBase( -1d, -1d, -1d, 1d, -1d ) ); } // Add it to a new frame final int targetFrame = -1; @@ -456,7 +456,7 @@ public void testLastKey() final HashSet< Spot > spots = new HashSet<>( N_SPOTS_TO_ADD ); for ( int i = 0; i < N_SPOTS_TO_ADD; i++ ) { - spots.add( new Spot( -1d, -1d, -1d, 1d, -1d ) ); + spots.add( new SpotBase( -1d, -1d, -1d, 1d, -1d ) ); } // Add it to a new frame final int targetFrame = 1000; diff --git a/src/test/java/fiji/plugin/trackmate/TestCopy.java b/src/test/java/fiji/plugin/trackmate/TestCopy.java index d2cdcde30..adb689c74 100644 --- a/src/test/java/fiji/plugin/trackmate/TestCopy.java +++ b/src/test/java/fiji/plugin/trackmate/TestCopy.java @@ -27,11 +27,9 @@ import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsIO; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.wizard.TrackMateWizardSequence; import fiji.plugin.trackmate.io.TmXmlReader; -import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; import ij.ImageJ; import ij.ImagePlus; @@ -57,13 +55,10 @@ public static void main( final String[] args ) throws ClassNotFoundException, In final ImagePlus imp = reader.readImage(); imp.show(); final Settings settings = reader.readSettings( imp ); + final GuiModel guiModel = new GuiModel( copy, settings ); - final SelectionModel selectionModel = new SelectionModel( copy ); - final DisplaySettings ds = DisplaySettingsIO.readUserDefault(); - final HyperStackDisplayer displayer = new HyperStackDisplayer( copy, selectionModel, imp, ds ); - displayer.render(); - - final TrackMateWizardSequence sequence = new TrackMateWizardSequence( new TrackMate( copy, settings ), selectionModel, ds ); + guiModel.getWindowManager().createHyperStackDisplayer(); + final TrackMateWizardSequence sequence = new TrackMateWizardSequence( guiModel ); sequence.setCurrent( "ConfigureViews" ); final JFrame frame = sequence.run( "Copy model" ); frame.setLocationRelativeTo( imp.getWindow() ); diff --git a/src/test/java/fiji/plugin/trackmate/TestTrackMatePlugin.java b/src/test/java/fiji/plugin/trackmate/TestTrackMatePlugin.java new file mode 100644 index 000000000..826a3dc6f --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/TestTrackMatePlugin.java @@ -0,0 +1,48 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate; + +import org.scijava.Context; + +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.util.TMUtils; +import ij.IJ; +import ij.ImagePlus; + +class TestTrackMatePlugin extends TrackMatePlugIn +{ + + public void setUp() + { + final ImagePlus imp = IJ.createImage( "Test Image", 256, 256, 10, 8 ); + final Settings settings = createSettings( imp ); + final Model model = createModel( imp ); + final DisplaySettings ds = createDisplaySettings(); + new GuiModel( model, settings, ds ); + } + + public Context getLocalContext() + { + return TMUtils.getContext(); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/TrackMatePluginTest.java b/src/test/java/fiji/plugin/trackmate/TrackMatePluginTest.java index c8b86088c..f0a1125fd 100644 --- a/src/test/java/fiji/plugin/trackmate/TrackMatePluginTest.java +++ b/src/test/java/fiji/plugin/trackmate/TrackMatePluginTest.java @@ -22,43 +22,27 @@ package fiji.plugin.trackmate; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; +import java.awt.GraphicsEnvironment; import java.util.List; import org.junit.Test; -import org.scijava.Context; import org.scijava.object.ObjectService; -import fiji.plugin.trackmate.util.TMUtils; -import ij.IJ; -import ij.ImagePlus; - public class TrackMatePluginTest { @Test public void testTrackMateRegistration() { - TestTrackMatePlugin testPlugin = new TestTrackMatePlugin(); + // Skip this test in headless mode - it requires GUI initialization + assumeTrue("Skipping GUI test in headless mode", !GraphicsEnvironment.isHeadless()); + + final TestTrackMatePlugin testPlugin = new TestTrackMatePlugin(); testPlugin.setUp(); - ObjectService objectService = testPlugin.getLocalContext().service(ObjectService.class); - - List trackMateInstances = objectService.getObjects(TrackMate.class); + final ObjectService objectService = testPlugin.getLocalContext().service(ObjectService.class); + + final List trackMateInstances = objectService.getObjects(TrackMate.class); assertTrue(trackMateInstances.size() == 1); assertTrue(trackMateInstances.get(0) instanceof TrackMate); } - - private class TestTrackMatePlugin extends TrackMatePlugIn { - - @SuppressWarnings("unused") - public void setUp() { - ImagePlus imp = IJ.createImage("Test Image", 256, 256, 10, 8); - Settings settings = createSettings(imp); - Model model = createModel(imp); - TrackMate trackMate = createTrackMate(model, settings); - } - - public Context getLocalContext() { - return TMUtils.getContext(); - } - - } } diff --git a/src/test/java/fiji/plugin/trackmate/TrackModelTest.java b/src/test/java/fiji/plugin/trackmate/TrackModelTest.java index 906c3961a..ee24ddf1d 100644 --- a/src/test/java/fiji/plugin/trackmate/TrackModelTest.java +++ b/src/test/java/fiji/plugin/trackmate/TrackModelTest.java @@ -49,7 +49,7 @@ public void testBuildingTracks() Spot previous = null; for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpot( spot ); if ( null != previous ) { @@ -83,7 +83,7 @@ public void testConnectingTracks() Spot spot = null; for ( int j = 0; j < DEPTH; j++ ) { - spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpot( spot ); if ( null != previous ) { @@ -125,7 +125,7 @@ public void testBreakingTracksBySpots() { for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpot( spot ); if ( null != previous ) { @@ -158,14 +158,14 @@ public void testBreakingTracksByEdges() // Build 1 long track final TrackModel model = new TrackModel(); final List< DefaultWeightedEdge > trackBreaks = new ArrayList<>(); - Spot previous = new Spot( 0d, 0d, 0d, 1d, -1d ); + Spot previous = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpot( previous ); for ( int i = 0; i < N_TRACKS; i++ ) { DefaultWeightedEdge edge = null; for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpot( spot ); edge = model.addEdge( previous, spot, 1 ); previous = spot; @@ -198,7 +198,7 @@ public void testVisibility() Spot previous = null; for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpot( spot ); if ( null != previous ) { @@ -245,7 +245,7 @@ public void testVisibilityMerge() Spot previous = null; for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpot( spot ); if ( null != previous ) { diff --git a/src/test/java/fiji/plugin/trackmate/action/CloseGapsByLinearInterpolationActionTest.java b/src/test/java/fiji/plugin/trackmate/action/CloseGapsByLinearInterpolationActionTest.java index f64329a91..b2c83cb07 100644 --- a/src/test/java/fiji/plugin/trackmate/action/CloseGapsByLinearInterpolationActionTest.java +++ b/src/test/java/fiji/plugin/trackmate/action/CloseGapsByLinearInterpolationActionTest.java @@ -29,6 +29,7 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.TrackModel; import fiji.plugin.trackmate.action.closegaps.CloseGapsByLinearInterpolation; @@ -73,7 +74,7 @@ public void testIfGapsInLinearTracksAreClosed() final TrackModel trackModel = model.getTrackModel(); // Check if positions were interpolated in the right way - final GraphIterator< Spot, DefaultWeightedEdge > spots = trackModel.getDepthFirstIterator( spot0, true ); + final GraphIterator< Spot, DefaultWeightedEdge > spots = trackModel.getDirectedDepthFirstIterator( spot0, false ); final double[][] referencePositions = { { 0, 0 }, { 1, 1 }, { 2, 2 }, { 3, 3 }, { 4, 4 }, { 5, 5 } }; @@ -115,7 +116,7 @@ public void testIfGapsInDividingTracksAreClosed() final TrackModel trackModel = model.getTrackModel(); // Check if positions were interpolated in the right way - final GraphIterator< Spot, DefaultWeightedEdge > spots = trackModel.getDepthFirstIterator( spot0, true ); + final GraphIterator< Spot, DefaultWeightedEdge > spots = trackModel.getDirectedDepthFirstIterator( spot0, false ); final double[][] referencePositions = { { 0, 0 }, { 1, 1 }, { 2, 2 }, { 4, 4 }, { 6, 6 }, { 8, 8 }, { 3, 3 }, { 4, 4 }, { 5, 5 } }; @@ -157,7 +158,7 @@ public void testIfGapsInDividingBackwardsTracksAreClosed() final TrackModel trackModel = model.getTrackModel(); // Check if positions were interpolated in the right way - final GraphIterator< Spot, DefaultWeightedEdge > spots = trackModel.getDepthFirstIterator( spot0, false ); + final GraphIterator< Spot, DefaultWeightedEdge > spots = trackModel.getDepthFirstIterator( spot0 ); final double[][] referencePositions = { { 0, 0 }, { 1, 1 }, { 2, 2 }, { 3, 3 }, { 4, 4 }, { 5, 5 }, { 4, 4 }, { 6, 6 }, { 8, 8 } }; @@ -180,8 +181,7 @@ private void checkPositions( final GraphIterator< Spot, DefaultWeightedEdge > sp private Spot createSpot( final double x, final double y, final double z ) { - final Spot newSpot = new Spot( x, y, z, 1.0, 1.0 ); - + final Spot newSpot = new SpotBase( x, y, z, 1.0, 1.0 ); newSpot.getFeatures().put( Spot.POSITION_T, 1.0 ); return newSpot; } diff --git a/src/test/java/fiji/plugin/trackmate/action/SpotGaussianFitterExample.java b/src/test/java/fiji/plugin/trackmate/action/SpotGaussianFitterExample.java index 53f178239..bf7a36bcc 100644 --- a/src/test/java/fiji/plugin/trackmate/action/SpotGaussianFitterExample.java +++ b/src/test/java/fiji/plugin/trackmate/action/SpotGaussianFitterExample.java @@ -31,16 +31,14 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.action.fit.SpotFitterController; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.wizard.TrackMateWizardSequence; import fiji.plugin.trackmate.io.TmXmlReader; -import fiji.plugin.trackmate.visualization.TrackMateModelView; -import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; import ij.ImageJ; import ij.ImagePlus; import net.imglib2.type.numeric.RealType; @@ -59,16 +57,15 @@ public static < T extends RealType< T > > void main( final String[] args ) throw final ImagePlus imp = reader.readImage(); imp.show(); final Settings settings = reader.readSettings( imp ); - final TrackMate trackmate = new TrackMate( model, settings ); + final DisplaySettings displaySettings = reader.getDisplaySettings(); + final GuiModel guiModel = new GuiModel( model, settings, displaySettings ); + final TrackMate trackmate = guiModel.getTrackMate(); trackmate.setNumThreads( 1 ); // Main view. - final SelectionModel selectionModel = new SelectionModel( model ); - final DisplaySettings displaySettings = reader.getDisplaySettings(); - final TrackMateModelView displayer = new HyperStackDisplayer( model, selectionModel, settings.imp, displaySettings ); - displayer.render(); + guiModel.getWindowManager().createHyperStackDisplayer(); - final TrackMateWizardSequence sequence = new TrackMateWizardSequence( trackmate, selectionModel, displaySettings ); + final TrackMateWizardSequence sequence = new TrackMateWizardSequence( guiModel ); sequence.setCurrent( "ConfigureViews" ); final JFrame frame = sequence.run( "Test Gauss-fitting action" ); frame.setIconImage( TRACKMATE_ICON.getImage() ); @@ -76,7 +73,7 @@ public static < T extends RealType< T > > void main( final String[] args ) throw frame.setVisible( true ); // Launch fitting controller. - final SpotFitterController controller = new SpotFitterController( trackmate, selectionModel, Logger.DEFAULT_LOGGER ); + final SpotFitterController controller = new SpotFitterController( guiModel, Logger.DEFAULT_LOGGER ); controller.show(); } } diff --git a/src/test/java/fiji/plugin/trackmate/detection/HessianDetectorTestDrive1.java b/src/test/java/fiji/plugin/trackmate/detection/HessianDetectorTestDrive1.java index 72f9ad84b..5a11fabd9 100644 --- a/src/test/java/fiji/plugin/trackmate/detection/HessianDetectorTestDrive1.java +++ b/src/test/java/fiji/plugin/trackmate/detection/HessianDetectorTestDrive1.java @@ -24,16 +24,16 @@ import java.util.List; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.features.FeatureUtils; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsIO; import fiji.plugin.trackmate.util.TMUtils; -import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; import ij.IJ; import ij.ImageJ; import ij.ImagePlus; @@ -55,7 +55,6 @@ public static < T extends RealType< T > & NativeType< T > > void main( final Str final ImagePlus imp = IJ.openImage( "samples/TSabateCell.tif" ); imp.show(); - @SuppressWarnings( "unchecked" ) final ImgPlus< T > input = TMUtils.rawWraps( imp ); final double[] calibration = TMUtils.getSpatialCalibration( imp ); final double radiusXY = 0.6 / 2.; // um; @@ -91,14 +90,13 @@ public static < T extends RealType< T > & NativeType< T > > void main( final Str final Model model = new Model(); model.setPhysicalUnits( imp.getCalibration().getUnit(), imp.getCalibration().getTimeUnit() ); model.setSpots( sc, false ); - final SelectionModel selectionModel = new SelectionModel( model ); final DisplaySettings ds = DisplaySettingsIO.readUserDefault(); final String feature = Spot.QUALITY; ds.setSpotColorBy( TrackMateObject.SPOTS, feature ); final double[] mm = FeatureUtils.autoMinMax( model, TrackMateObject.SPOTS, feature ); ds.setSpotMinMax( mm[ 0 ], mm[ 1 ] ); - final HyperStackDisplayer displayer = new HyperStackDisplayer( model, selectionModel, imp, ds ); - displayer.render(); - displayer.refresh(); + final Settings settings = new Settings( imp ); + final GuiModel guiModel = new GuiModel( model, settings, ds ); + guiModel.getWindowManager().createHyperStackDisplayer(); } } diff --git a/src/test/java/fiji/plugin/trackmate/features/SpotFeatureComputationBenchmark.java b/src/test/java/fiji/plugin/trackmate/features/SpotFeatureComputationBenchmark.java index c47651532..02e44e3a6 100644 --- a/src/test/java/fiji/plugin/trackmate/features/SpotFeatureComputationBenchmark.java +++ b/src/test/java/fiji/plugin/trackmate/features/SpotFeatureComputationBenchmark.java @@ -32,7 +32,7 @@ import fiji.plugin.trackmate.features.spot.SpotAnalyzerFactoryBase; import fiji.plugin.trackmate.io.TmXmlReader; import fiji.plugin.trackmate.providers.SpotAnalyzerProvider; -import fiji.plugin.trackmate.providers.SpotMorphologyAnalyzerProvider; +import fiji.plugin.trackmate.providers.Spot2DMorphologyAnalyzerProvider; import ij.ImagePlus; import net.imglib2.util.Util; @@ -67,7 +67,7 @@ public static void main( final String[] args ) for ( final String key : provider1.getVisibleKeys() ) factories.add( provider1.getFactory( key ) ); - final SpotMorphologyAnalyzerProvider provider2 = new SpotMorphologyAnalyzerProvider( 1 ); + final Spot2DMorphologyAnalyzerProvider provider2 = new Spot2DMorphologyAnalyzerProvider( 1 ); for ( final String key : provider2.getVisibleKeys() ) factories.add( provider2.getFactory( key ) ); diff --git a/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTargetAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTargetAnalyzerTest.java index a3115f739..e5a149739 100644 --- a/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTargetAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTargetAnalyzerTest.java @@ -23,11 +23,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.ModelChangeEvent; -import fiji.plugin.trackmate.ModelChangeListener; -import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.features.edges.EdgeTargetAnalyzer; import java.util.Collection; import java.util.HashMap; @@ -37,6 +32,13 @@ import org.junit.Before; import org.junit.Test; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; +import fiji.plugin.trackmate.ModelChangeListener; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.features.edges.EdgeTargetAnalyzer; + public class EdgeTargetAnalyzerTest { @@ -73,7 +75,7 @@ public void setUp() for ( int j = 0; j <= DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpotTo( spot, j ); if ( null != previous ) { diff --git a/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTimeAndLocationAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTimeAndLocationAnalyzerTest.java index 5644982d4..10989ac75 100644 --- a/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTimeAndLocationAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTimeAndLocationAnalyzerTest.java @@ -23,12 +23,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import fiji.plugin.trackmate.Dimension; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.ModelChangeEvent; -import fiji.plugin.trackmate.ModelChangeListener; -import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.features.edges.EdgeTimeLocationAnalyzer; import java.util.Collection; import java.util.HashMap; @@ -39,6 +33,14 @@ import org.junit.Before; import org.junit.Test; +import fiji.plugin.trackmate.Dimension; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; +import fiji.plugin.trackmate.ModelChangeListener; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.features.edges.EdgeTimeLocationAnalyzer; + public class EdgeTimeAndLocationAnalyzerTest { @@ -75,7 +77,7 @@ public void setUp() for ( int j = 0; j <= DEPTH; j++ ) { - final Spot spot = new Spot( i + j, i + j, i + j, 1d, -1d ); + final Spot spot = new SpotBase( i + j, i + j, i + j, 1d, -1d ); spot.putFeature( Spot.POSITION_T, Double.valueOf( j ) ); model.addSpotTo( spot, j ); if ( null != previous ) @@ -181,8 +183,8 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { + model.beforeEdit( aspot ); aspot.putFeature( Spot.POSITION_X, -1000d ); - model.updateFeatures( aspot ); } finally { diff --git a/src/test/java/fiji/plugin/trackmate/features/edge/EdgeVelocityAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/edge/EdgeVelocityAnalyzerTest.java index e35c873b0..90398a6aa 100644 --- a/src/test/java/fiji/plugin/trackmate/features/edge/EdgeVelocityAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/edge/EdgeVelocityAnalyzerTest.java @@ -23,11 +23,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.ModelChangeEvent; -import fiji.plugin.trackmate.ModelChangeListener; -import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.features.edges.EdgeSpeedAnalyzer; import java.util.Collection; import java.util.HashMap; @@ -37,6 +32,13 @@ import org.junit.Before; import org.junit.Test; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; +import fiji.plugin.trackmate.ModelChangeListener; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.features.edges.EdgeSpeedAnalyzer; + public class EdgeVelocityAnalyzerTest { @@ -75,10 +77,9 @@ public void setUp() for ( int j = 0; j <= DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); - spot.putFeature( posFeats[ i % 3 ], Double.valueOf( i + j ) ); // rotate - // displacement - // dimension + final Spot spot = new SpotBase( 0., 0., 0., 1., -1. ); + spot.putFeature( posFeats[ i % 3 ], Double.valueOf( i + j ) ); + // rotate displacement dimension spot.putFeature( Spot.POSITION_T, Double.valueOf( 2 * j ) ); model.addSpotTo( spot, j ); if ( null != previous ) @@ -167,8 +168,8 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { + model.beforeEdit( aspot ); aspot.putFeature( Spot.POSITION_X, -1000d ); - model.updateFeatures( aspot ); } finally { diff --git a/src/test/java/fiji/plugin/trackmate/features/spot/SpotIntensityAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/spot/SpotIntensityAnalyzerTest.java index 2689123ff..0fc21667c 100644 --- a/src/test/java/fiji/plugin/trackmate/features/spot/SpotIntensityAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/spot/SpotIntensityAnalyzerTest.java @@ -27,10 +27,11 @@ import org.junit.Test; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.util.SpotNeighborhood; +import fiji.plugin.trackmate.SpotBase; import net.imagej.ImgPlus; import net.imagej.axis.Axes; import net.imagej.axis.AxisType; +import net.imglib2.IterableInterval; import net.imglib2.RandomAccess; import net.imglib2.img.Img; import net.imglib2.img.array.ArrayImgs; @@ -75,7 +76,7 @@ public void setUp() throws Exception } - spot = new Spot( CENTER[ 0 ], CENTER[ 1 ], CENTER[ 2 ], RADIUS, -1d, "1" ); + spot = new SpotBase( CENTER[ 0 ], CENTER[ 1 ], CENTER[ 2 ], RADIUS, -1d, "1" ); } @Test @@ -97,16 +98,12 @@ public static void main( final String[] args ) throws Exception final SpotIntensityAnalyzerTest test = new SpotIntensityAnalyzerTest(); test.setUp(); - final Spot tmpSpot = new Spot( CENTER[ 0 ], CENTER[ 1 ], CENTER[ 2 ], RADIUS, -1d ); - final SpotNeighborhood< UnsignedShortType > disc = new SpotNeighborhood<>( tmpSpot, test.img2D ); + final Spot tmpSpot = new SpotBase( CENTER[ 0 ], CENTER[ 1 ], CENTER[ 2 ], RADIUS, -1d ); + final IterableInterval< UnsignedShortType > disc = tmpSpot.iterable( test.img2D ); for ( final UnsignedShortType pixel : disc ) - { pixel.set( 1500 ); - } ij.ImageJ.main( args ); net.imglib2.img.display.imagej.ImageJFunctions.show( test.img2D ); - } - } diff --git a/src/test/java/fiji/plugin/trackmate/features/track/TrackBranchingAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/track/TrackBranchingAnalyzerTest.java index e81cac2c4..75bc6a444 100644 --- a/src/test/java/fiji/plugin/trackmate/features/track/TrackBranchingAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/track/TrackBranchingAnalyzerTest.java @@ -37,6 +37,7 @@ import fiji.plugin.trackmate.ModelChangeEvent; import fiji.plugin.trackmate.ModelChangeListener; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; public class TrackBranchingAnalyzerTest { @@ -74,7 +75,7 @@ public void setUp() Spot previous = null; for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpotTo( spot, j ); if ( null != previous ) { @@ -93,7 +94,7 @@ public void setUp() { continue; } - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpotTo( spot, j ); if ( null != previous ) { @@ -109,7 +110,7 @@ public void setUp() split = null; // Store the spot at the branch split for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); if ( j == DEPTH / 2 ) { split = spot; @@ -129,7 +130,7 @@ public void setUp() previous = split; for ( int j = DEPTH / 2 + 1; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpotTo( spot, j ); model.addEdge( previous, spot, 1 ); previous = spot; @@ -143,7 +144,7 @@ public void setUp() Spot merge = null; for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); if ( j == DEPTH / 2 ) { merge = spot; @@ -158,7 +159,7 @@ public void setUp() previous = null; for ( int j = 0; j < DEPTH / 2; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpotTo( spot, j ); if ( null != previous ) { @@ -233,8 +234,8 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { - final Spot spot1 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 0 ); - final Spot spot2 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 1 ); + final Spot spot1 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 0 ); + final Spot spot2 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 1 ); model.addEdge( spot1, spot2, 1 ); } @@ -269,7 +270,7 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { - newSpot = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), firstFrame + 1 ); + newSpot = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), firstFrame + 1 ); model.addEdge( lFirstSpot, newSpot, 1 ); } finally diff --git a/src/test/java/fiji/plugin/trackmate/features/track/TrackDurationAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/track/TrackDurationAnalyzerTest.java index 6d2cc1c4e..8f6c231ac 100644 --- a/src/test/java/fiji/plugin/trackmate/features/track/TrackDurationAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/track/TrackDurationAnalyzerTest.java @@ -24,10 +24,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.ModelChangeEvent; -import fiji.plugin.trackmate.ModelChangeListener; -import fiji.plugin.trackmate.Spot; import java.util.Collection; import java.util.HashMap; @@ -39,6 +35,12 @@ import org.junit.Before; import org.junit.Test; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; +import fiji.plugin.trackmate.ModelChangeListener; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; + public class TrackDurationAnalyzerTest { @@ -86,7 +88,7 @@ public void setUp() final HashSet< Spot > track = new HashSet<>(); for ( int j = start; j <= stop; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); spot.putFeature( Spot.POSITION_T, Double.valueOf( j ) ); model.addSpotTo( spot, j ); track.add( spot ); @@ -161,9 +163,9 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { - final Spot spot1 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 0 ); + final Spot spot1 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 0 ); spot1.putFeature( Spot.POSITION_T, 0d ); - final Spot spot2 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 1 ); + final Spot spot2 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 1 ); spot2.putFeature( Spot.POSITION_T, 1d ); model.addEdge( spot1, spot2, 1 ); @@ -201,7 +203,7 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { - newSpot = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), firstFrame + 1 ); + newSpot = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), firstFrame + 1 ); newSpot.putFeature( Spot.POSITION_T, Double.valueOf( firstFrame + 1 ) ); model.addEdge( firstSpot, newSpot, 1 ); } @@ -273,8 +275,8 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { + model.beforeEdit( aspot ); aspot.putFeature( Spot.POSITION_T, aspot.getFeature( Spot.POSITION_T ) + increment ); - model.updateFeatures( aspot ); } finally { diff --git a/src/test/java/fiji/plugin/trackmate/features/track/TrackIndexAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/track/TrackIndexAnalyzerTest.java index 0a6d0fb44..94d509888 100644 --- a/src/test/java/fiji/plugin/trackmate/features/track/TrackIndexAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/track/TrackIndexAnalyzerTest.java @@ -39,6 +39,7 @@ import fiji.plugin.trackmate.ModelChangeEvent; import fiji.plugin.trackmate.ModelChangeListener; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; /** * @author Jean-Yves Tinevez @@ -65,7 +66,7 @@ public void setUp() Spot previous = null; for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpotTo( spot, j ); if ( null != previous ) { @@ -151,7 +152,7 @@ public void modelChanged( final ModelChangeEvent event ) try { final Spot targetSpot = model.getSpots().iterator( 0, true ).next(); - final Spot newSpot = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 1 ); + final Spot newSpot = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 1 ); model.addEdge( targetSpot, newSpot, 1 ); } finally diff --git a/src/test/java/fiji/plugin/trackmate/features/track/TrackLocationAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/track/TrackLocationAnalyzerTest.java index 4d158f5ad..72283edff 100644 --- a/src/test/java/fiji/plugin/trackmate/features/track/TrackLocationAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/track/TrackLocationAnalyzerTest.java @@ -24,10 +24,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.ModelChangeEvent; -import fiji.plugin.trackmate.ModelChangeListener; -import fiji.plugin.trackmate.Spot; import java.util.Collection; import java.util.HashMap; @@ -38,6 +34,12 @@ import org.junit.Before; import org.junit.Test; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; +import fiji.plugin.trackmate.ModelChangeListener; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; + public class TrackLocationAnalyzerTest { @@ -73,7 +75,7 @@ public void setUp() for ( int j = 0; j <= DEPTH; j++ ) { // We use deterministic locations - final Spot spot = new Spot( j + i, j + i, j + i, 1d, -1d ); + final Spot spot = new SpotBase( j + i, j + i, j + i, 1d, -1d ); model.addSpotTo( spot, j ); track.add( spot ); if ( null != previous ) @@ -144,9 +146,9 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { - final Spot spot1 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 0 ); + final Spot spot1 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 0 ); spot1.putFeature( Spot.POSITION_T, 0d ); - final Spot spot2 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 1 ); + final Spot spot2 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 1 ); spot2.putFeature( Spot.POSITION_T, 1d ); model.addEdge( spot1, spot2, 1 ); diff --git a/src/test/java/fiji/plugin/trackmate/features/track/TrackSpeedStatisticsAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/track/TrackSpeedStatisticsAnalyzerTest.java index 4cf4c23dd..d956df98f 100644 --- a/src/test/java/fiji/plugin/trackmate/features/track/TrackSpeedStatisticsAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/track/TrackSpeedStatisticsAnalyzerTest.java @@ -38,6 +38,7 @@ import fiji.plugin.trackmate.ModelChangeEvent; import fiji.plugin.trackmate.ModelChangeListener; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; public class TrackSpeedStatisticsAnalyzerTest { @@ -71,7 +72,7 @@ public void setUp() for ( int j = 0; j <= DEPTH; j++ ) { // We use deterministic locations - final Spot spot = new Spot( j * i, i, i, 1d, -1d ); + final Spot spot = new SpotBase( j * i, i, i, 1d, -1d ); spot.putFeature( Spot.POSITION_T, Double.valueOf( j ) ); model.addSpotTo( spot, j ); track.add( spot ); @@ -127,7 +128,7 @@ public final void testProcess2() for ( int j = 0; j <= DEPTH; j++ ) { // We use deterministic locations - final Spot spot = new Spot( j * j, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( j * j, 0d, 0d, 1d, -1d ); spot.putFeature( Spot.POSITION_T, Double.valueOf( j ) ); model2.addSpotTo( spot, j ); track.add( spot ); @@ -197,9 +198,9 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { - final Spot spot1 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 0 ); + final Spot spot1 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 0 ); spot1.putFeature( Spot.POSITION_T, 0d ); - final Spot spot2 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 1 ); + final Spot spot2 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 1 ); spot2.putFeature( Spot.POSITION_T, 1d ); model.addEdge( spot1, spot2, 1 ); @@ -318,8 +319,8 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { + model.beforeEdit( lastSpot ); lastSpot.putFeature( Spot.POSITION_X, 2 * lastSpot.getFeature( Spot.POSITION_X ) ); - model.updateFeatures( lastSpot ); } finally { diff --git a/src/test/java/fiji/plugin/trackmate/graph/ConvexBranchDecompositionDebug.java b/src/test/java/fiji/plugin/trackmate/graph/ConvexBranchDecompositionDebug.java index ea47c0dd8..7cd3554a7 100644 --- a/src/test/java/fiji/plugin/trackmate/graph/ConvexBranchDecompositionDebug.java +++ b/src/test/java/fiji/plugin/trackmate/graph/ConvexBranchDecompositionDebug.java @@ -22,12 +22,12 @@ package fiji.plugin.trackmate.graph; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.TrackModel; import fiji.plugin.trackmate.graph.ConvexBranchesDecomposition.TrackBranchDecomposition; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.visualization.trackscheme.TrackScheme; public class ConvexBranchDecompositionDebug @@ -35,17 +35,17 @@ public class ConvexBranchDecompositionDebug public static void main( final String[] args ) { - final Spot sa0 = new Spot( 0, 0, 0, 1, -1, "SA_0" ); - final Spot sa1 = new Spot( 0, 0, 0, 1, -1, "SA_1" ); - final Spot sa3 = new Spot( 0, 0, 0, 1, -1, "SA_3" ); - final Spot sa4 = new Spot( 0, 0, 0, 1, -1, "SA_4" ); + final Spot sa0 = new SpotBase( 0, 0, 0, 1, -1, "SA_0" ); + final Spot sa1 = new SpotBase( 0, 0, 0, 1, -1, "SA_1" ); + final Spot sa3 = new SpotBase( 0, 0, 0, 1, -1, "SA_3" ); + final Spot sa4 = new SpotBase( 0, 0, 0, 1, -1, "SA_4" ); - final Spot sb0 = new Spot( 0, 0, 0, 1, -1, "SB_0" ); - final Spot sb1 = new Spot( 0, 0, 0, 1, -1, "SB_1" ); - final Spot sb3 = new Spot( 0, 0, 0, 1, -1, "SB_3" ); - final Spot sb4 = new Spot( 0, 0, 0, 1, -1, "SB_4" ); + final Spot sb0 = new SpotBase( 0, 0, 0, 1, -1, "SB_0" ); + final Spot sb1 = new SpotBase( 0, 0, 0, 1, -1, "SB_1" ); + final Spot sb3 = new SpotBase( 0, 0, 0, 1, -1, "SB_3" ); + final Spot sb4 = new SpotBase( 0, 0, 0, 1, -1, "SB_4" ); - final Spot nexus = new Spot( 0, 0, 0, 1, -1, "NEXUS" ); + final Spot nexus = new SpotBase( 0, 0, 0, 1, -1, "NEXUS" ); final SpotCollection spots = new SpotCollection(); spots.add( sa0, 0 ); @@ -70,8 +70,8 @@ public static void main( final String[] args ) model.addEdge( sa3, sa4, -2 ); model.addEdge( sb3, sb4, -2 ); - final SelectionModel sm = new SelectionModel( model ); - final TrackScheme trackScheme = new TrackScheme( model, sm, DisplaySettings.defaultStyle().copy() ); + final GuiModel guiModel = new GuiModel( model ); + final TrackScheme trackScheme = new TrackScheme( guiModel ); trackScheme.render(); final TrackModel tm = model.getTrackModel(); diff --git a/src/test/java/fiji/plugin/trackmate/graph/SortedDepthFirstIteratorTest.java b/src/test/java/fiji/plugin/trackmate/graph/SortedDepthFirstIteratorTest.java index 406e3f0df..4e5b1708c 100644 --- a/src/test/java/fiji/plugin/trackmate/graph/SortedDepthFirstIteratorTest.java +++ b/src/test/java/fiji/plugin/trackmate/graph/SortedDepthFirstIteratorTest.java @@ -22,8 +22,6 @@ package fiji.plugin.trackmate.graph; import static org.junit.Assert.assertArrayEquals; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.Spot; import java.util.Arrays; import java.util.Comparator; @@ -33,6 +31,10 @@ import org.junit.BeforeClass; import org.junit.Test; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; + public class SortedDepthFirstIteratorTest { @@ -78,7 +80,7 @@ public int compare( final Spot o1, final Spot o2 ) { // Root - root = new Spot( 0d, 0d, 0d, 1d, -1d, "Root" ); + root = new SpotBase( 0d, 0d, 0d, 1d, -1d, "Root" ); model.addSpotTo( root, 0 ); // First level @@ -88,7 +90,7 @@ public int compare( final Spot o1, final Spot o2 ) { names[ i ] = "A"; // randomString( 5 ); - final Spot spotChild = new Spot( 0d, 0d, 0d, 1d, -1d, names[ i ] ); + final Spot spotChild = new SpotBase( 0d, 0d, 0d, 1d, -1d, names[ i ] ); model.addSpotTo( spotChild, 1 ); model.addEdge( root, spotChild, -1 ); spots[ 0 ][ i ] = spotChild; @@ -96,7 +98,7 @@ public int compare( final Spot o1, final Spot o2 ) spots[ 0 ][ i ] = spotChild; for ( int j = 1; j < spots.length; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d, " " + j + "_" + randomString( 3 ) ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d, " " + j + "_" + randomString( 3 ) ); spots[ j ][ i ] = spot; model.addSpotTo( spot, j + 1 ); model.addEdge( spots[ j - 1 ][ i ], spots[ j ][ i ], -1 ); diff --git a/src/test/java/fiji/plugin/trackmate/interactivetests/ConcurrentSpotTestDrive.java b/src/test/java/fiji/plugin/trackmate/interactivetests/ConcurrentSpotTestDrive.java index 155f7eb91..e064f022e 100644 --- a/src/test/java/fiji/plugin/trackmate/interactivetests/ConcurrentSpotTestDrive.java +++ b/src/test/java/fiji/plugin/trackmate/interactivetests/ConcurrentSpotTestDrive.java @@ -68,7 +68,7 @@ public static void main( final String[] args ) trackmate.execDetection(); // Retrieve spots - final SpotCollection spots = trackmate.getModel().getSpots(); + final SpotCollection spots = model.getSpots(); // Parse spots and detect duplicate IDs final int[] IDs = new int[ Spot.IDcounter.get() + 1 ]; diff --git a/src/test/java/fiji/plugin/trackmate/interactivetests/GraphTest.java b/src/test/java/fiji/plugin/trackmate/interactivetests/GraphTest.java index 15df40465..b3886ee55 100644 --- a/src/test/java/fiji/plugin/trackmate/interactivetests/GraphTest.java +++ b/src/test/java/fiji/plugin/trackmate/interactivetests/GraphTest.java @@ -30,6 +30,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.graph.GraphUtils; import fiji.plugin.trackmate.graph.TimeDirectedNeighborIndex; @@ -55,7 +56,7 @@ private static void pickLeavesOfOneTrack( final Model model ) final TreeSet< Spot > spots = new TreeSet<>( Spot.frameComparator ); spots.addAll( model.getTrackModel().vertexSet() ); final Spot first = spots.first(); - final GraphIterator< Spot, DefaultWeightedEdge > iterator = model.getTrackModel().getDepthFirstIterator( first, true ); + final GraphIterator< Spot, DefaultWeightedEdge > iterator = model.getTrackModel().getDirectedDepthFirstIterator( first, false ); while ( iterator.hasNext() ) { @@ -103,25 +104,26 @@ public static final Model getExampleModel() // Create spots - final Spot root = new Spot( 3d, 0d, 0d, 1d, -1d, "Zygote" ); + final Spot root = new SpotBase( 3d, 0d, 0d, 1d, -1d, "Zygote" ); - final Spot AB = new Spot( 0d, 1d, 0d, 1d, -1d, "AB" ); - final Spot P1 = new Spot( 3d, 1d, 0d, 1d, -1d, "P1" ); + final Spot AB = new SpotBase( 0d, 1d, 0d, 1d, -1d, "AB" ); + final Spot P1 = new SpotBase( 3d, 1d, 0d, 1d, -1d, "P1" ); - final Spot P2 = new Spot( 4d, 2d, 0d, 1d, -1d, "P2" ); - final Spot EMS = new Spot( 2d, 2d, 0d, 1d, -1d, "EMS" ); + final Spot P2 = new SpotBase( 4d, 2d, 0d, 1d, -1d, "P2" ); + final Spot EMS = new SpotBase( 2d, 2d, 0d, 1d, -1d, "EMS" ); - final Spot P3 = new Spot( 5d, 3d, 0d, 1d, -1d, "P3" ); - final Spot C = new Spot( 3d, 3d, 0d, 1d, -1d, "C" ); - final Spot E = new Spot( 1d, 3d, 0d, 1d, -1d, "E" ); - final Spot MS = new Spot( 2d, 3d, 0d, 1d, -1d, "MS" ); - final Spot AB3 = new Spot( 0d, 3d, 0d, 1d, -1d, "AB" ); + final Spot P3 = new SpotBase( 5d, 3d, 0d, 1d, -1d, "P3" ); + final Spot C = new SpotBase( 3d, 3d, 0d, 1d, -1d, "C" ); + final Spot E = new SpotBase( 1d, 3d, 0d, 1d, -1d, "E" ); + final Spot MS = new SpotBase( 2d, 3d, 0d, 1d, -1d, "MS" ); + final Spot AB3 = new SpotBase( 0d, 3d, 0d, 1d, -1d, "AB" ); - final Spot D = new Spot( 4d, 4d, 0d, 1d, -1d, "D" ); - final Spot P4 = new Spot( 5d, 4d, 0d, 1d, -1d, "P4" ); + final Spot D = new SpotBase( 4d, 4d, 0d, 1d, -1d, "D" ); + final Spot P4 = new SpotBase( 5d, 4d, 0d, 1d, -1d, "P4" ); // Add them to the graph + model.pauseUndo(); model.beginUpdate(); try { @@ -166,6 +168,7 @@ public static final Model getExampleModel() finally { model.endUpdate(); + model.resumeUndo(); } // Done! @@ -190,13 +193,14 @@ public static final Model getComplicatedExample() } // Update model + model.pauseUndo(); model.beginUpdate(); try { // new spots - final Spot Q1 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d, "Q1" ), 0 ); - final Spot Q2 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d, "Q2" ), 1 ); - final Spot Q3 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d, "Q3" ), 2 ); + final Spot Q1 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d, "Q1" ), 0 ); + final Spot Q2 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d, "Q2" ), 1 ); + final Spot Q3 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d, "Q3" ), 2 ); // new links model.addEdge( Q1, Q2, -1 ); model.addEdge( Q2, Q3, -1 ); @@ -205,6 +209,7 @@ public static final Model getComplicatedExample() finally { model.endUpdate(); + model.resumeUndo(); } return model; diff --git a/src/test/java/fiji/plugin/trackmate/interactivetests/HyperStackDisplayerTestDrive.java b/src/test/java/fiji/plugin/trackmate/interactivetests/HyperStackDisplayerTestDrive.java index a817f5e0c..9f6404b1b 100644 --- a/src/test/java/fiji/plugin/trackmate/interactivetests/HyperStackDisplayerTestDrive.java +++ b/src/test/java/fiji/plugin/trackmate/interactivetests/HyperStackDisplayerTestDrive.java @@ -26,14 +26,13 @@ import org.scijava.util.AppUtils; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.features.ModelFeatureUpdater; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackDisplayMode; import fiji.plugin.trackmate.io.TmXmlReader; -import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; import fiji.plugin.trackmate.visualization.trackscheme.TrackScheme; import ij.ImagePlus; @@ -51,17 +50,15 @@ public static void main( final String[] args ) final Model model = reader.getModel(); final ImagePlus imp = reader.readImage(); final Settings settings = reader.readSettings( imp ); - final DisplaySettings ds = DisplaySettings.defaultStyle().copy(); ds.setSpotShowName( true ); ds.setTrackDisplayMode( TrackDisplayMode.LOCAL_BACKWARD ); + final GuiModel guiModel = new GuiModel( model, settings, ds ); new ModelFeatureUpdater( model, settings ); - final SelectionModel selectionModel = new SelectionModel( model ); - final HyperStackDisplayer displayer = new HyperStackDisplayer( model, selectionModel, imp, ds ); - displayer.render(); + guiModel.getWindowManager().createHyperStackDisplayer(); - final TrackScheme trackScheme = new TrackScheme( model, selectionModel, ds ); + final TrackScheme trackScheme = new TrackScheme( guiModel ); trackScheme.render(); } } diff --git a/src/test/java/fiji/plugin/trackmate/interactivetests/NNTrackerTest.java b/src/test/java/fiji/plugin/trackmate/interactivetests/NNTrackerTest.java index ced3f6dfc..b3b9a28d4 100644 --- a/src/test/java/fiji/plugin/trackmate/interactivetests/NNTrackerTest.java +++ b/src/test/java/fiji/plugin/trackmate/interactivetests/NNTrackerTest.java @@ -31,13 +31,10 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.io.TmXmlReader; import fiji.plugin.trackmate.tracking.kdtree.NearestNeighborTracker; -import fiji.plugin.trackmate.visualization.TrackMateModelView; -import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; import ij.ImagePlus; public class NNTrackerTest @@ -89,7 +86,7 @@ public static void main( final String args[] ) // Load Image ij.ImageJ.main( args ); - final TrackMateModelView sd2d = new HyperStackDisplayer( model, new SelectionModel( model ), imp, DisplaySettings.defaultStyle().copy() ); - sd2d.render(); + final GuiModel guiModel = new GuiModel( model, imp ); + guiModel.getWindowManager().createHyperStackDisplayer(); } } diff --git a/src/test/java/fiji/plugin/trackmate/interactivetests/SpotFeatureGrapherExample.java b/src/test/java/fiji/plugin/trackmate/interactivetests/SpotFeatureGrapherExample.java index a00798d47..f87c7c77b 100644 --- a/src/test/java/fiji/plugin/trackmate/interactivetests/SpotFeatureGrapherExample.java +++ b/src/test/java/fiji/plugin/trackmate/interactivetests/SpotFeatureGrapherExample.java @@ -33,13 +33,13 @@ import org.scijava.util.AppUtils; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.features.SpotFeatureGrapher; import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.io.TmXmlReader; import fiji.plugin.trackmate.visualization.trackscheme.TrackScheme; @@ -48,12 +48,11 @@ public class SpotFeatureGrapherExample public static void main( final String[] args ) { - // Load objects final File file = new File( AppUtils.getBaseDirectory( TrackMate.class ), "samples/FakeTracks.xml" ); final TmXmlReader reader = new TmXmlReader( file ); final Model model = reader.getModel(); - final SelectionModel selectionModel = new SelectionModel( model ); + final GuiModel guiModel = new GuiModel( model ); final List< String > Y = new ArrayList<>( 1 ); Y.add( Spot.POSITION_T ); @@ -62,12 +61,10 @@ public static void main( final String[] args ) spots.add( it.next() ); final SpotFeatureGrapher grapher = new SpotFeatureGrapher( + guiModel, spots, Spot.POSITION_X, Y, - model, - selectionModel, - DisplaySettings.defaultStyle().copy(), true ); final JFrame frame = grapher.render(); frame.setLocationRelativeTo( null ); @@ -76,9 +73,8 @@ public static void main( final String[] args ) final TrackIndexAnalyzer analyzer = new TrackIndexAnalyzer(); analyzer.process( model.getTrackModel().trackIDs( true ), model ); // needed for trackScheme - final TrackScheme trackScheme = new TrackScheme( model, new SelectionModel( model ), DisplaySettings.defaultStyle().copy() ); + final TrackScheme trackScheme = new TrackScheme( guiModel ); trackScheme.render(); - } /** @@ -96,7 +92,7 @@ private static Model getSpiralModel() final double x = 100d + 100 * i / 100. * Math.cos( i / 100. * 5 * 2 * Math.PI ); final double y = 100d + 100 * i / 100. * Math.sin( i / 100. * 5 * 2 * Math.PI ); final double z = 0d; - final Spot spot = new Spot( x, y, z, 2d, -1d ); + final Spot spot = new SpotBase( x, y, z, 2d, -1d ); spot.putFeature( Spot.POSITION_T, Double.valueOf( i ) ); spots.add( spot ); diff --git a/src/test/java/fiji/plugin/trackmate/interactivetests/SpotNeighborhoodTest.java b/src/test/java/fiji/plugin/trackmate/interactivetests/SpotNeighborhoodTest.java index c1d985f3f..8934b5e3b 100644 --- a/src/test/java/fiji/plugin/trackmate/interactivetests/SpotNeighborhoodTest.java +++ b/src/test/java/fiji/plugin/trackmate/interactivetests/SpotNeighborhoodTest.java @@ -22,15 +22,17 @@ package fiji.plugin.trackmate.interactivetests; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.util.SpotNeighborhood; -import fiji.plugin.trackmate.util.SpotNeighborhoodCursor; +import fiji.plugin.trackmate.SpotBase; import ij.ImageJ; import net.imagej.ImgPlus; +import net.imglib2.Cursor; +import net.imglib2.IterableInterval; import net.imglib2.img.array.ArrayImg; import net.imglib2.img.array.ArrayImgs; import net.imglib2.img.basictypeaccess.array.ShortArray; import net.imglib2.img.display.imagej.ImageJFunctions; import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.util.Util; public class SpotNeighborhoodTest { @@ -42,12 +44,13 @@ public static void main( final String[] args ) // 3D final ArrayImg< UnsignedShortType, ShortArray > image = ArrayImgs.unsignedShorts( 100, 100, 100 ); final ImgPlus< UnsignedShortType > img = new ImgPlus<>( image ); - final Spot spot = new Spot( 50d, 50d, 50d, 30d, -1d ); - final SpotNeighborhood< UnsignedShortType > neighborhood = new SpotNeighborhood<>( spot, img ); - final SpotNeighborhoodCursor< UnsignedShortType > cursor = neighborhood.cursor(); + final Spot spot = new SpotBase( 50d, 50d, 50d, 30d, -1d ); + final IterableInterval< UnsignedShortType > neighborhood = spot.iterable( img ); + final Cursor< UnsignedShortType > cursor = neighborhood.cursor(); while ( cursor.hasNext() ) { - cursor.next().set( ( int ) cursor.getDistanceSquared() ); + final double d = Util.distance( spot, cursor ); + cursor.next().set( ( int ) ( d * d ) ); } System.out.println( "Finished" ); ImageJFunctions.wrap( img, "3D" ).show(); @@ -55,12 +58,13 @@ public static void main( final String[] args ) // 2D final ArrayImg< UnsignedShortType, ShortArray > image2 = ArrayImgs.unsignedShorts( 100, 100 ); final ImgPlus< UnsignedShortType > img2 = new ImgPlus<>( image2 ); - final Spot spot2 = new Spot( 50d, 50d, 0d, 30d, -1d ); - final SpotNeighborhood< UnsignedShortType > neighborhood2 = new SpotNeighborhood<>( spot2, img2 ); - final SpotNeighborhoodCursor< UnsignedShortType > cursor2 = neighborhood2.cursor(); + final Spot spot2 = new SpotBase( 50d, 50d, 0d, 30d, -1d ); + final IterableInterval< UnsignedShortType > neighborhood2 = spot2.iterable( img2 ); + final Cursor< UnsignedShortType > cursor2 = neighborhood2.cursor(); while ( cursor2.hasNext() ) { - cursor2.next().set( ( int ) cursor2.getDistanceSquared() ); + final double d = Util.distance( spot2, cursor2 ); + cursor2.next().set( ( int ) ( d * d ) ); } System.out.println( "Finished" ); ImageJFunctions.wrap( img2, "3D" ).show(); diff --git a/src/test/java/fiji/plugin/trackmate/interactivetests/TmXmlReaderTestDrive.java b/src/test/java/fiji/plugin/trackmate/interactivetests/TmXmlReaderTestDrive.java index 66569aa4b..1c8cc48fd 100644 --- a/src/test/java/fiji/plugin/trackmate/interactivetests/TmXmlReaderTestDrive.java +++ b/src/test/java/fiji/plugin/trackmate/interactivetests/TmXmlReaderTestDrive.java @@ -31,8 +31,9 @@ import fiji.plugin.trackmate.io.TmXmlReader; import fiji.plugin.trackmate.providers.DetectorProvider; import fiji.plugin.trackmate.providers.EdgeAnalyzerProvider; +import fiji.plugin.trackmate.providers.Spot2DMorphologyAnalyzerProvider; +import fiji.plugin.trackmate.providers.Spot3DMorphologyAnalyzerProvider; import fiji.plugin.trackmate.providers.SpotAnalyzerProvider; -import fiji.plugin.trackmate.providers.SpotMorphologyAnalyzerProvider; import fiji.plugin.trackmate.providers.TrackAnalyzerProvider; import fiji.plugin.trackmate.providers.TrackerProvider; import ij.ImagePlus; @@ -57,7 +58,8 @@ public static void main( final String args[] ) new SpotAnalyzerProvider( imp.getNChannels() ), new EdgeAnalyzerProvider(), new TrackAnalyzerProvider(), - new SpotMorphologyAnalyzerProvider( imp.getNChannels() ) ); + new Spot2DMorphologyAnalyzerProvider( imp.getNChannels() ), + new Spot3DMorphologyAnalyzerProvider( imp.getNChannels() ) ); System.out.println( settings ); System.out.println( model ); diff --git a/src/test/java/fiji/plugin/trackmate/interactivetests/TrackLayoutTest.java b/src/test/java/fiji/plugin/trackmate/interactivetests/TrackLayoutTest.java index 68294fd63..a83553aa9 100644 --- a/src/test/java/fiji/plugin/trackmate/interactivetests/TrackLayoutTest.java +++ b/src/test/java/fiji/plugin/trackmate/interactivetests/TrackLayoutTest.java @@ -22,8 +22,7 @@ package fiji.plugin.trackmate.interactivetests; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.visualization.trackscheme.TrackScheme; public class TrackLayoutTest @@ -31,10 +30,9 @@ public class TrackLayoutTest public static void main( final String[] args ) { - final Model model = GraphTest.getExampleModel(); - - final TrackScheme trackScheme = new TrackScheme( model, new SelectionModel( model ), DisplaySettings.defaultStyle().copy() ); + final GuiModel guiModel = new GuiModel( model ); + final TrackScheme trackScheme = new TrackScheme( guiModel ); trackScheme.render(); } } diff --git a/src/test/java/fiji/plugin/trackmate/interactivetests/TrackSchemeTestDrive.java b/src/test/java/fiji/plugin/trackmate/interactivetests/TrackSchemeTestDrive.java index d458ae97c..264c2e124 100644 --- a/src/test/java/fiji/plugin/trackmate/interactivetests/TrackSchemeTestDrive.java +++ b/src/test/java/fiji/plugin/trackmate/interactivetests/TrackSchemeTestDrive.java @@ -26,9 +26,9 @@ import org.scijava.util.AppUtils; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.features.edges.EdgeSpeedAnalyzer; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; import fiji.plugin.trackmate.io.TmXmlReader; @@ -54,8 +54,8 @@ public static void main( final String[] args ) ds.setTrackColorBy( TrackMateObject.EDGES, EdgeSpeedAnalyzer.DISPLACEMENT ); // Instantiate displayer - final SelectionModel sm = new SelectionModel( model ); - final TrackScheme trackscheme = new TrackScheme( model, sm, ds ); + final GuiModel guiModel = new GuiModel( model, ds ); + final TrackScheme trackscheme = new TrackScheme( guiModel ); trackscheme.render(); trackscheme.refresh(); } diff --git a/src/test/java/fiji/plugin/trackmate/io/TmGeffWriterTestDrive.java b/src/test/java/fiji/plugin/trackmate/io/TmGeffWriterTestDrive.java new file mode 100644 index 000000000..579a6c8a0 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/io/TmGeffWriterTestDrive.java @@ -0,0 +1,28 @@ +package fiji.plugin.trackmate.io; + +import java.io.File; +import java.io.IOException; + +import fiji.plugin.trackmate.Model; + +public class TmGeffWriterTestDrive +{ + + public static void main( final String[] args ) throws IOException + { + final String path = "samples/FakeTracks.xml"; + final TmXmlReader reader = new TmXmlReader( new File( path ) ); + if ( !reader.isReadingOk() ) + { + System.err.println( reader.getErrorMessage() ); + return; + } + final String savePath = path.replace( ".xml", ".geff" ); + + final Model model = reader.getModel(); + + System.out.println( "Writing to " + savePath ); + TmGeffWriter.write( model, savePath ); + System.out.println( "Done." ); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/DebugZSlicer.java b/src/test/java/fiji/plugin/trackmate/mesh/DebugZSlicer.java new file mode 100644 index 000000000..2ec486e27 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/DebugZSlicer.java @@ -0,0 +1,85 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.mesh; + +import java.io.File; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.io.TmXmlReader; +import fiji.plugin.trackmate.util.TMUtils; +import ij.CompositeImage; +import ij.ImageJ; +import ij.ImagePlus; +import net.imglib2.mesh.alg.zslicer.Contour; +import net.imglib2.mesh.alg.zslicer.Slice; +import net.imglib2.mesh.alg.zslicer.ZSlicer; + +public class DebugZSlicer +{ + public static void main( final String[] args ) + { + try + { + ImageJ.main( args ); + + final String filePath = "samples/CElegans3D-smoothed-mask-orig-t7.xml"; + final TmXmlReader reader = new TmXmlReader( new File( filePath ) ); + if ( !reader.isReadingOk() ) + { + System.err.println( reader.getErrorMessage() ); + return; + } + + final ImagePlus imp = reader.readImage(); + imp.show(); + final double[] calibration = TMUtils.getSpatialCalibration( imp ); + final Settings settings = reader.readSettings( imp ); + + final Model model = reader.getModel(); + final DisplaySettings ds = reader.getDisplaySettings(); + final GuiModel guiModel = new GuiModel( model, settings, ds ); + + guiModel.getWindowManager().createHyperStackDisplayer(); + imp.setDisplayMode( CompositeImage.GRAYSCALE ); + + final Spot spot = model.getSpots().iterable( true ).iterator().next(); + final double z = 21.; + + imp.setZ( ( int ) Math.round( z / calibration[ 2 ] ) + 1 ); + + final Slice contours = ZSlicer.slice( ( ( SpotMesh ) spot ).getMesh(), z, calibration[ 2 ] ); + System.out.println( "Found " + contours.size() + " contours." ); + for ( final Contour contour : contours ) + System.out.println( contour ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + } +} + diff --git a/src/test/java/fiji/plugin/trackmate/mesh/DefaultMesh.java b/src/test/java/fiji/plugin/trackmate/mesh/DefaultMesh.java new file mode 100644 index 000000000..88c67cac5 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/DefaultMesh.java @@ -0,0 +1,102 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.mesh; + +import java.util.List; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.detection.ThresholdDetector; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; +import ij.ImageJ; +import ij.ImagePlus; +import ij.gui.NewImage; +import ij.measure.Calibration; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.mesh.Mesh; +import net.imglib2.type.logic.BitType; + +public class DefaultMesh +{ + + public static void main( final String[] args ) + { + ImageJ.main( args ); + final ImgPlus< BitType > img = Demo3DMesh.loadTestMask2(); + final ImagePlus imp = ImageJFunctions.show( img, "box" ); + imp.setDimensions( + img.dimensionIndex( Axes.CHANNEL ), + img.dimensionIndex( Axes.Z ), + img.dimensionIndex( Axes.TIME ) ); + final double[] calibration = new double[] { 1., 1., 1. }; + + final ThresholdDetector< BitType > detector = new ThresholdDetector< BitType >( img, img, calibration, 0, false, -1. ); + detector.process(); + final List< Spot > spots = detector.getResult(); + + final Model model = new Model(); + for ( final Spot spot : spots ) + { + model.getSpots().add( spot, 0 ); + System.out.println( spot ); + } + + final GuiModel guiModel = new GuiModel( model, new Settings( imp ) ); + guiModel.getWindowManager().createHyperStackDisplayer(); + } + + public static void main2( final String[] args ) + { + ImageJ.main( args ); + final ImagePlus imp = NewImage.createByteImage( "dummy", + 64, 64, 64, NewImage.FILL_RAMP ); + final Calibration cal = imp.getCalibration(); + cal.pixelWidth = 0.5; + cal.pixelHeight = 0.5; + cal.pixelDepth = 0.5; + imp.show(); + + final long[] min = new long[] { 2, 2, 2 }; + final long[] max = new long[] { 20, 20, 20 }; + final Mesh mesh = Demo3DMesh.debugMesh( min, max ); + final Spot spot = new SpotMesh( mesh, 1. ); + + final Model model = new Model(); + model.beginUpdate(); + try + { + model.addSpotTo( spot, 0 ); + } + finally + { + model.endUpdate(); + } + + final HyperStackDisplayer view = new HyperStackDisplayer( new GuiModel( model, imp ) ); + view.render(); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/Demo3DMesh.java b/src/test/java/fiji/plugin/trackmate/mesh/Demo3DMesh.java new file mode 100644 index 000000000..e203d7972 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/Demo3DMesh.java @@ -0,0 +1,274 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.mesh; + +import java.awt.Color; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Iterator; + +import fiji.plugin.trackmate.detection.MaskUtils; +import fiji.plugin.trackmate.util.TMUtils; +import ij.IJ; +import ij.ImageJ; +import ij.ImagePlus; +import ij.gui.Overlay; +import ij.gui.PolygonRoi; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.converter.RealTypeConverters; +import net.imglib2.img.ImgView; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.img.display.imagej.ImgPlusViews; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.Vertices; +import net.imglib2.mesh.alg.zslicer.Contour; +import net.imglib2.mesh.alg.zslicer.Slice; +import net.imglib2.mesh.alg.zslicer.ZSlicer; +import net.imglib2.mesh.impl.naive.NaiveDoubleMesh; +import net.imglib2.mesh.io.ply.PLYMeshIO; +import net.imglib2.mesh.io.stl.STLMeshIO; +import net.imglib2.roi.labeling.ImgLabeling; +import net.imglib2.roi.labeling.LabelRegion; +import net.imglib2.roi.labeling.LabelRegions; +import net.imglib2.type.logic.BitType; +import net.imglib2.type.logic.BoolType; +import net.imglib2.type.numeric.NumericType; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.numeric.integer.IntType; +import net.imglib2.view.IntervalView; +import net.imglib2.view.Views; + +public class Demo3DMesh +{ + + public static void main( final String[] args ) + { + try + { + + ImageJ.main( args ); +// final ImgPlus< BitType > mask = loadTestMask2(); + final ImgPlus< BitType > mask = loadTestMask(); + + // Convert it to labeling. + final ImgLabeling< Integer, IntType > labeling = MaskUtils.toLabeling( mask, 0.5, 1 ); + final ImagePlus out = ImageJFunctions.show( labeling.getIndexImg(), "labeling" ); + out.setDimensions( mask.dimensionIndex( Axes.CHANNEL ), mask.dimensionIndex( Axes.Z ), mask.dimensionIndex( Axes.TIME ) ); + + // Iterate through all components. + final LabelRegions< Integer > regions = new LabelRegions< Integer >( labeling ); + final double[] cal = TMUtils.getSpatialCalibration( mask ); + + // Parse regions to create polygons on boundaries. + final Iterator< LabelRegion< Integer > > iterator = regions.iterator(); + int j = 0; + while ( iterator.hasNext() ) + { + final LabelRegion< Integer > region = iterator.next(); + + // To mesh. + final IntervalView< BoolType > box = Views.zeroMin( region ); + final Mesh mesh = Meshes.marchingCubes( box ); + System.out.println( "Before cleaning: " + mesh.vertices().size() + " vertices and " + mesh.triangles().size() + " faces." ); + final Mesh cleaned = Meshes.removeDuplicateVertices( mesh, 0 ); + System.out.println( "Before simplification: " + cleaned.vertices().size() + " vertices and " + cleaned.triangles().size() + " faces." ); + final Mesh simplified = Meshes.simplify( cleaned, 0.25f, 10 ); + + // Wrap as mesh with edges. + System.out.println( "After simplification: " + simplified.vertices().size() + " vertices and " + simplified.triangles().size() + " faces." ); + System.out.println(); + + // Scale and offset with physical coordinates. + final double[] origin = region.minAsDoubleArray(); + scale( simplified.vertices(), cal, origin ); + + /* + * IO. + */ + testIO( simplified, ++j ); + + /* + * Display. + */ + + // Intersection with a XY plane at a fixed Z position. + final int zslice = 22; // plan + final double z = ( zslice - 1 ) * cal[ 2 ]; // um + + final Slice contours = ZSlicer.slice( simplified, z, cal[ 2 ] ); + toOverlay( contours, out, cal ); + } + System.out.println( "Done." ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + } + + @SuppressWarnings( "unused" ) + static Mesh debugMesh( final long[] min, final long[] max ) + { + final NaiveDoubleMesh mesh = new NaiveDoubleMesh(); + final net.imglib2.mesh.impl.naive.NaiveDoubleMesh.Vertices vertices = mesh.vertices(); + final net.imglib2.mesh.impl.naive.NaiveDoubleMesh.Triangles triangles = mesh.triangles(); + + // Coords as X Y Z + + // Bottom square. + final double[] bnw = new double[] { min[ 0 ], min[ 1 ], min[ 2 ] }; + final double[] bne = new double[] { max[ 0 ], min[ 1 ], min[ 2 ] }; + final double[] bsw = new double[] { min[ 0 ], max[ 1 ], min[ 2 ] }; + final double[] bse = new double[] { max[ 0 ], max[ 1 ], min[ 2 ] }; + + // Top square. + final double[] tnw = new double[] { min[ 0 ], min[ 1 ], max[ 2 ] }; + final double[] tne = new double[] { max[ 0 ], min[ 1 ], max[ 2 ] }; + final double[] tsw = new double[] { min[ 0 ], max[ 1 ], max[ 2 ] }; + final double[] tse = new double[] { max[ 0 ], max[ 1 ], max[ 2 ] }; + + // Add vertices. + final long bnwi = vertices.add( bnw[ 0 ], bnw[ 1 ], bnw[ 2 ] ); + final long bnei = vertices.add( bne[ 0 ], bne[ 1 ], bne[ 2 ] ); + final long bswi = vertices.add( bsw[ 0 ], bsw[ 1 ], bsw[ 2 ] ); + final long bsei = vertices.add( bse[ 0 ], bse[ 1 ], bse[ 2 ] ); + final long tnwi = vertices.add( tnw[ 0 ], tnw[ 1 ], tnw[ 2 ] ); + final long tnei = vertices.add( tne[ 0 ], tne[ 1 ], tne[ 2 ] ); + final long tswi = vertices.add( tsw[ 0 ], tsw[ 1 ], tsw[ 2 ] ); + final long tsei = vertices.add( tse[ 0 ], tse[ 1 ], tse[ 2 ] ); + + // Add triangles for the 6 faces. + + // Bottom. + triangles.add( bnwi, bnei, bswi ); + triangles.add( bnei, bsei, bswi ); + + // Top. + triangles.add( tnwi, tnei, tswi ); + triangles.add( tnei, tsei, tswi ); + + // Front (facing south). + triangles.add( tswi, tsei, bsei ); + triangles.add( tswi, bsei, bswi ); + + // Back (facing north). + triangles.add( tnwi, tnei, bnei ); + triangles.add( tnwi, bnei, bnwi ); + + // Left (facing west). + triangles.add( tnwi, tswi, bswi ); + triangles.add( tnwi, bnwi, bswi ); + + // Right (facing east). + triangles.add( tnei, tsei, bsei ); + triangles.add( tnei, bnei, bsei ); + + return mesh; + } + + private static void toOverlay( final Slice contours, final ImagePlus out, final double[] cal ) + { + Overlay overlay = out.getOverlay(); + if ( overlay == null ) + { + overlay = new Overlay(); + out.setOverlay( overlay ); + } + + for ( final Contour contour : contours ) + { + System.out.println( contour ); // DEBUG + final float[] xRoi = new float[ contour.size() ]; + final float[] yRoi = new float[ contour.size() ]; + for ( int i = 0; i < contour.size(); i++ ) + { + xRoi[ i ] = ( float ) ( contour.x( i ) / cal[ 0 ] + 0.5 ); + yRoi[ i ] = ( float ) ( contour.y( i ) / cal[ 1 ] + 0.5 ); + } + final PolygonRoi roi = new PolygonRoi( xRoi, yRoi, PolygonRoi.POLYGON ); + roi.setStrokeColor( contour.isInterior() ? Color.GREEN : Color.RED ); + overlay.add( roi ); + } + } + + private static void testIO( final Mesh mesh, final int j ) + { + // Serialize to disk. + try + { + STLMeshIO.save( mesh, String.format( "samples/mesh/io/STL_%02d.stl", j ) ); + + PLYMeshIO.save( mesh, String.format( "samples/mesh/io/PLY_%02d.ply", j ) ); + final byte[] bs = PLYMeshIO.writeAscii( mesh ); + final String str = new String( bs ); + try (final FileWriter writer = new FileWriter( + String.format( "samples/mesh/io/PLYTEXT_%02d.txt", j ) )) + { + writer.write( str ); + } + } + catch ( final IOException e ) + { + e.printStackTrace(); + } + } + + private static void scale( final Vertices vertices, final double[] scale, final double[] origin ) + { + final long nv = vertices.size(); + for ( long i = 0; i < nv; i++ ) + { + final double x = ( origin[ 0 ] + vertices.x( i ) ) * scale[ 0 ]; + final double y = ( origin[ 1 ] + vertices.y( i ) ) * scale[ 1 ]; + final double z = ( origin[ 2 ] + vertices.z( i ) ) * scale[ 2 ]; + vertices.set( i, x, y, z ); + } + } + + static < T extends RealType< T > & NumericType< T > > ImgPlus< BitType > loadTestMask() + { + final String filePath = "samples/mesh/CElegansMask3D.tif"; + final ImagePlus imp = IJ.openImage( filePath ); + + // First channel is the mask. + final ImgPlus< T > img = TMUtils.rawWraps( imp ); + final ImgPlus< T > c1 = ImgPlusViews.hyperSlice( img, img.dimensionIndex( Axes.CHANNEL ), 0 ); + + // Take the first time-point + final ImgPlus< T > t1 = ImgPlusViews.hyperSlice( c1, c1.dimensionIndex( Axes.TIME ), 0 ); + // Make it to boolean. + final RandomAccessibleInterval< BitType > mask = RealTypeConverters.convert( t1, new BitType() ); + return new ImgPlus< BitType >( ImgView.wrap( mask ), t1 ); + } + + static < T extends RealType< T > & NumericType< T > > ImgPlus< BitType > loadTestMask2() + { + final String filePath = "samples/mesh/Cube.tif"; + final ImagePlus imp = IJ.openImage( filePath ); + final ImgPlus< T > img = TMUtils.rawWraps( imp ); + final RandomAccessibleInterval< BitType > mask = RealTypeConverters.convert( img, new BitType() ); + return new ImgPlus<>( ImgView.wrap( mask ), img ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/providers/ViewProvider.java b/src/test/java/fiji/plugin/trackmate/mesh/Demo3DMeshTrackMate.java similarity index 58% rename from src/main/java/fiji/plugin/trackmate/providers/ViewProvider.java rename to src/test/java/fiji/plugin/trackmate/mesh/Demo3DMeshTrackMate.java index 31ec8dc10..76ce297d8 100644 --- a/src/main/java/fiji/plugin/trackmate/providers/ViewProvider.java +++ b/src/test/java/fiji/plugin/trackmate/mesh/Demo3DMeshTrackMate.java @@ -2,7 +2,7 @@ * #%L * TrackMate: your buddy for everyday tracking. * %% - * Copyright (C) 2010 - 2026 TrackMate developers. + * Copyright (C) 2010 - 2024 TrackMate developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as @@ -19,21 +19,32 @@ * . * #L% */ -package fiji.plugin.trackmate.providers; +package fiji.plugin.trackmate.mesh; -import fiji.plugin.trackmate.visualization.ViewFactory; +import fiji.plugin.trackmate.TrackMatePlugIn; +import ij.IJ; +import ij.ImageJ; +import ij.ImagePlus; -public class ViewProvider extends AbstractProvider< ViewFactory > +public class Demo3DMeshTrackMate { - public ViewProvider() - { - super( ViewFactory.class ); - } - public static void main( final String[] args ) { - final ViewProvider provider = new ViewProvider(); - System.out.println( provider.echo() ); + try + { + + ImageJ.main( args ); +// final String filePath = "samples/CElegans3D-smoothed-mask-orig-t7.tif"; + final String filePath = "samples/Celegans-5pc-17timepoints.tif"; + final ImagePlus imp = IJ.openImage( filePath ); + imp.show(); + + new TrackMatePlugIn().run( null ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } } } diff --git a/src/test/java/fiji/plugin/trackmate/mesh/DemoContour.java b/src/test/java/fiji/plugin/trackmate/mesh/DemoContour.java new file mode 100644 index 000000000..e9709c226 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/DemoContour.java @@ -0,0 +1,60 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.mesh; + +import java.io.File; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.io.TmXmlReader; +import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; +import ij.ImageJ; +import ij.ImagePlus; +import math.geom2d.polygon.Polygon2D; +import math.geom2d.polygon.Polygons2D; +import math.geom2d.polygon.SimplePolygon2D; + +public class DemoContour +{ + + public static void main3( final String[] args ) + { + final SimplePolygon2D a = new SimplePolygon2D( new double[] { 0, 2, 2 }, new double[] { 0, 0, 2 } ); + final SimplePolygon2D b = new SimplePolygon2D( new double[] { 0, 0, 2 }, new double[] { 0, 2, 0 } ); + final Polygon2D c = Polygons2D.union( a, b ); + System.out.println( c.area() ); // DEBUG + + } + + public static void main( final String[] args ) + { + ImageJ.main( args ); + final String filePath = "samples/mesh/Torus-mask.xml"; + final TmXmlReader reader = new TmXmlReader( new File( filePath ) ); + final Model model = reader.getModel(); + final ImagePlus imp = reader.readImage(); + imp.show(); + + final HyperStackDisplayer view = new HyperStackDisplayer( new GuiModel( model, imp ) ); + view.render(); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/DemoHollowMesh.java b/src/test/java/fiji/plugin/trackmate/mesh/DemoHollowMesh.java new file mode 100644 index 000000000..b75f570e0 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/DemoHollowMesh.java @@ -0,0 +1,83 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.mesh; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.detection.ThresholdDetectorFactory; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.util.TMUtils; +import ij.ImageJ; +import ij.ImagePlus; +import ij.gui.NewImage; +import net.imagej.ImgPlus; +import net.imglib2.RealPoint; +import net.imglib2.type.numeric.integer.UnsignedByteType; + +public class DemoHollowMesh +{ + + public static void main( final String[] args ) + { + ImageJ.main( args ); + final ImagePlus imp = makeImg(); + + final Settings settings = new Settings( imp ); + settings.detectorFactory = new ThresholdDetectorFactory<>(); + settings.detectorSettings = settings.detectorFactory.getDefaultSettings(); + settings.detectorSettings.put( ThresholdDetectorFactory.KEY_INTENSITY_THRESHOLD, 120. ); + settings.detectorSettings.put( ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS, false ); + + final Model model = new Model(); + final GuiModel guiModel = new GuiModel( model, imp ); + final TrackMate trackmate = guiModel.getTrackMate(); + trackmate.execDetection(); + model.getSpots().setVisible( true ); + + guiModel.getWindowManager().createHyperStackDisplayer(); + } + + public static ImagePlus makeImg() + { + final ImagePlus imp = NewImage.createByteImage( "Hollow", 256, 256, 256, NewImage.FILL_BLACK ); + final ImgPlus< UnsignedByteType > img = TMUtils.rawWraps( imp ); + + final RealPoint center = RealPoint.wrap( new double[] { + imp.getWidth() / 2., + imp.getHeight() / 2., + imp.getNSlices() / 2. + } ); + final double r1 = imp.getWidth() / 4.; + final double r2 = imp.getWidth() / 8.; + final double r3 = imp.getWidth() / 16.; + final Spot s1 = new SpotBase( center, r1, 1. ); + final Spot s2 = new SpotBase( center, r2, 1. ); + final Spot s3 = new SpotBase( center, r3, 1. ); + s1.iterable( img ).forEach( p -> p.setReal( 250. ) ); + s2.iterable( img ).forEach( p -> p.setZero() ); + s3.iterable( img ).forEach( p -> p.setReal( 250. ) ); + return imp; + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/DemoPixelIteration.java b/src/test/java/fiji/plugin/trackmate/mesh/DemoPixelIteration.java new file mode 100644 index 000000000..b310e6d87 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/DemoPixelIteration.java @@ -0,0 +1,125 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.mesh; + +import java.awt.Color; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.detection.ThresholdDetectorFactory; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.util.TMUtils; +import ij.IJ; +import ij.ImageJ; +import ij.ImagePlus; +import ij.gui.NewImage; +import ij.process.LUT; +import net.imagej.ImgPlus; +import net.imglib2.Cursor; +import net.imglib2.RandomAccess; +import net.imglib2.type.numeric.RealType; + +public class DemoPixelIteration +{ + + public static < T extends RealType< T > > void main( final String[] args ) + { + try + { + ImageJ.main( args ); + +// final Mesh mesh = Demo3DMesh.debugMesh( new long[] { 4, 4, 4 }, new long[] { 10, 10, 10 } ); +// final Spot s0 = SpotMesh.createSpot( mesh, 1. ); +// final Model model = new Model(); +// model.beginUpdate(); +// try +// { +// model.addSpotTo( s0, 0 ); +// } +// finally +// { +// model.endUpdate(); +// } +// final ImagePlus imp = NewImage.createByteImage( "cube", 16, 16, 16, NewImage.FILL_BLACK ); + + final String imPath = "samples/mesh/CElegansMask3DNoScale-mask-t1.tif"; + final ImagePlus imp = IJ.openImage( imPath ); + + final Settings settings = new Settings( imp ); + settings.detectorFactory = new ThresholdDetectorFactory<>(); + settings.detectorSettings = settings.detectorFactory.getDefaultSettings(); + settings.detectorSettings.put( + ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS, false ); + settings.detectorSettings.put( + ThresholdDetectorFactory.KEY_INTENSITY_THRESHOLD, 100. ); + + final Model model = new Model(); + final GuiModel guiModel = new GuiModel( model, settings ); + final TrackMate trackmate = guiModel.getTrackMate(); + trackmate.setNumThreads( 4 ); + trackmate.execDetection(); + + final SpotCollection spots = model.getSpots(); + spots.setVisible( true ); + + final ImagePlus out = NewImage.createShortImage( "OUT", imp.getWidth(), imp.getHeight(), imp.getNSlices(), NewImage.FILL_BLACK ); + out.show(); + out.resetDisplayRange(); + + imp.show(); + imp.resetDisplayRange(); + + final double[] cal = TMUtils.getSpatialCalibration( imp ); + int i = 0; + for ( final Spot spot : model.getSpots().iterable( true ) ) + { + System.out.println( spot ); + final ImgPlus< T > img = TMUtils.rawWraps( out ); + final Cursor< T > cursor = spot.iterable( img, cal ).localizingCursor(); + final RandomAccess< T > ra = img.randomAccess(); + while ( cursor.hasNext() ) + { + cursor.fwd(); + cursor.get().setReal( 1 + i++ ); + + ra.setPosition( cursor ); + ra.get().setReal( 100 ); + } + } + + guiModel.getWindowManager().createHyperStackDisplayer(); + imp.setSlice( 19 ); + imp.resetDisplayRange(); + imp.setLut( LUT.createLutFromColor( Color.BLUE ) ); + out.setSlice( 19 ); + out.resetDisplayRange(); + System.out.println( "Done." ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/ExportMeshForDemo.java b/src/test/java/fiji/plugin/trackmate/mesh/ExportMeshForDemo.java new file mode 100644 index 000000000..b03d914da --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/ExportMeshForDemo.java @@ -0,0 +1,85 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.mesh; + +import java.io.File; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.detection.MaskDetectorFactory; +import fiji.plugin.trackmate.detection.ThresholdDetectorFactory; +import fiji.plugin.trackmate.gui.GuiModel; +import ij.IJ; +import ij.ImagePlus; +import net.imglib2.mesh.io.stl.STLMeshIO; + +public class ExportMeshForDemo +{ + + public static void main( final String[] args ) + { + try + { + final String filePath = "samples/mesh/CElegansMask3D.tif"; + final ImagePlus imp = IJ.openImage( filePath ); + + final Settings settings = new Settings( imp ); + settings.detectorFactory = new MaskDetectorFactory<>(); + settings.detectorSettings = settings.detectorFactory.getDefaultSettings(); + settings.detectorSettings.put( ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS, false ); + + final Model model = new Model(); + final GuiModel guiModel = new GuiModel( model, settings ); + final TrackMate trackmate = guiModel.getTrackMate(); + trackmate.setNumThreads( 4 ); + trackmate.execDetection(); + final SpotCollection spots = model.getSpots(); + spots.setVisible( true ); + + final String meshDir = "samples/mesh/io"; + for ( final File file : new File( meshDir ).listFiles() ) + if ( !file.isDirectory() ) + file.delete(); + + for ( final Spot spot : spots.iterable( true ) ) + { + final int t = spot.getFeature( Spot.FRAME ).intValue(); + final int id = spot.ID(); + final String savePath = String.format( "%s/mesh_t%2d_id_%04d.stl", meshDir, t, id ); + if ( spot instanceof SpotMesh ) + { + final SpotMesh mesh = ( SpotMesh ) spot; + STLMeshIO.save( mesh.getMesh(), savePath ); + } + } + System.out.println( "Export done." ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/MeshPlayground.java b/src/test/java/fiji/plugin/trackmate/mesh/MeshPlayground.java new file mode 100644 index 000000000..b038f6d5b --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/MeshPlayground.java @@ -0,0 +1,134 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.mesh; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.joml.Matrix4f; +import org.scijava.ui.behaviour.io.InputTriggerConfig; +import org.scijava.ui.behaviour.util.Actions; + +import bvv.core.VolumeViewerPanel; +import bvv.core.util.MatrixMath; +import bvv.vistools.Bvv; +import bvv.vistools.BvvFunctions; +import bvv.vistools.BvvSource; +import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.visualization.bvv.StupidMesh; +import ij.IJ; +import ij.ImagePlus; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imglib2.img.display.imagej.ImgPlusViews; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.impl.naive.NaiveDoubleMesh; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.type.Type; +import net.imglib2.type.numeric.ARGBType; + +public class MeshPlayground +{ + public static < T extends Type< T > > void main( final String[] args ) + { + final String filePath = "samples/mesh/CElegansMask3D.tif"; + final ImagePlus imp = IJ.openImage( filePath ); + + final ImgPlus< T > img = TMUtils.rawWraps( imp ); + final ImgPlus< T > c1 = ImgPlusViews.hyperSlice( img, img.dimensionIndex( Axes.CHANNEL ), 1 ); + final ImgPlus< T > t1 = ImgPlusViews.hyperSlice( c1, c1.dimensionIndex( Axes.TIME ), 0 ); + final double[] cal = TMUtils.getSpatialCalibration( t1 ); + + final BvvSource source = BvvFunctions.show( c1, "t1", + + Bvv.options() + .maxAllowedStepInVoxels( 0 ) + .renderWidth( 1024 ) + .renderHeight( 1024 ) + .preferredSize( 512, 512 ) + .sourceTransform( cal ) ); + + source.setDisplayRangeBounds( 0, 1024 ); + source.setColor( new ARGBType( 0xaaffaa ) ); + + final List< StupidMesh > meshes = new ArrayList<>(); + for ( int j = 1; j <= 3; ++j ) + { + final String fn = String.format( "samples/mesh/CElegansMask3D_%02d.stl", j ); + meshes.add( new StupidMesh( load( fn ) ) ); + } + + final VolumeViewerPanel viewer = source.getBvvHandle().getViewerPanel(); + + final AtomicBoolean showMeshes = new AtomicBoolean( true ); + viewer.setRenderScene( ( gl, data ) -> { + if ( showMeshes.get() ) + { + final Matrix4f pvm = new Matrix4f( data.getPv() ); + final Matrix4f view = MatrixMath.affine( data.getRenderTransformWorldToScreen(), new Matrix4f() ); + final Matrix4f vm = MatrixMath.screen( data.getDCam(), data.getScreenWidth(), data.getScreenHeight(), new Matrix4f() ).mul( view ); + meshes.forEach( mesh -> mesh.draw( gl, pvm, vm, false ) ); + } + } ); + + final Actions actions = new Actions( new InputTriggerConfig() ); + actions.install( source.getBvvHandle().getKeybindings(), "my-new-actions" ); + actions.runnableAction( () -> { + showMeshes.set( !showMeshes.get() ); + viewer.requestRepaint(); + }, "toggle meshes", "G" ); + + viewer.requestRepaint(); + } + + private static BufferMesh load( final String fn ) + { + BufferMesh mesh = null; + try + { + final NaiveDoubleMesh nmesh = new NaiveDoubleMesh(); + net.imglib2.mesh.io.stl.STLMeshIO.read( nmesh, new File( fn ) ); + mesh = calculateNormals( + nmesh +// Meshes.removeDuplicateVertices( nmesh, 5 ) + ); + } + catch ( final IOException e ) + { + e.printStackTrace(); + } + return mesh; + } + + private static BufferMesh calculateNormals( final Mesh mesh ) + { + final int nvertices = mesh.vertices().size(); + final int ntriangles = mesh.triangles().size(); + final BufferMesh bufferMesh = new BufferMesh( nvertices, ntriangles, true ); + Meshes.calculateNormals( mesh, bufferMesh ); + return bufferMesh; + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/TestEllipsoidFit.java b/src/test/java/fiji/plugin/trackmate/mesh/TestEllipsoidFit.java new file mode 100644 index 000000000..07e5b823b --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/TestEllipsoidFit.java @@ -0,0 +1,122 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2024 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.mesh; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.alg.EllipsoidFitter; +import net.imglib2.mesh.alg.EllipsoidFitter.Ellipsoid; +import net.imglib2.mesh.impl.naive.NaiveDoubleMesh; + +public class TestEllipsoidFit +{ + + @Test + public void testSimpleEllipsoids() + { + final double TOLERANCE = 1e-6; + + final double ra = 1.; + final double rb = 2.; + for ( double rc = 3.; rc < 10.; rc++ ) + { + final Mesh mesh = generateEllipsoidMesh( ra, rb, rc, -1, -1 ); + final Ellipsoid fit = EllipsoidFitter.fit( mesh ); + + final double[] arr = new double[ 3 ]; + + // Center on 0. + fit.center.localize( arr ); + assertArrayEquals( "Ellipsoid center should be close to 0.", arr, new double[] { 0., 0., 0. }, TOLERANCE ); + + // Proper radius, ordered by increasing absolute value. + assertEquals( "Smallest radius has unexpected value.", ra, fit.r1, TOLERANCE ); + assertEquals( "Mid radius has unexpected value.", rb, fit.r2, TOLERANCE ); + assertEquals( "Largest radius has unexpected value.", rc, fit.r3, TOLERANCE ); + + // Vectors, aligned with axes. + fit.ev1.localize( arr ); + for ( int d = 0; d < arr.length; d++ ) + arr[ d ] = Math.abs( arr[ d ] ); + assertArrayEquals( "Smallest eigenvector should be aligned with X axis.", arr, new double[] { 1., 0., 0. }, TOLERANCE ); + + fit.ev2.localize( arr ); + for ( int d = 0; d < arr.length; d++ ) + arr[ d ] = Math.abs( arr[ d ] ); + assertArrayEquals( "Mid eigenvector should be aligned with Y axis.", arr, new double[] { 0., 1., 0. }, TOLERANCE ); + + fit.ev3.localize( arr ); + for ( int d = 0; d < arr.length; d++ ) + arr[ d ] = Math.abs( arr[ d ] ); + assertArrayEquals( "Largest eigenvector should be aligned with Z axis.", arr, new double[] { 0., 0., 1. }, TOLERANCE ); + } + } + + private static Mesh generateEllipsoidMesh( final double ra, final double rb, final double rc, int numLongitudes, int numLatitudes ) + { + if ( numLongitudes < 4 ) + numLongitudes = 36; // Number of longitudinal divisions + if ( numLatitudes < 4 ) + numLatitudes = 18; // Number of latitudinal divisions + + final NaiveDoubleMesh mesh = new NaiveDoubleMesh(); + for ( int lat = 0; lat < numLatitudes; lat++ ) + { + final double theta1 = ( double ) lat / numLatitudes * Math.PI; + final double theta2 = ( double ) ( lat + 1 ) / numLatitudes * Math.PI; + + for ( int lon = 0; lon < numLongitudes; lon++ ) + { + final double phi1 = ( double ) lon / numLongitudes * 2 * Math.PI; + final double phi2 = ( double ) ( lon + 1 ) / numLongitudes * 2 * Math.PI; + + // Calculate the vertices of each triangle + final long p1 = addVertex( mesh, ra, rb, rc, theta1, phi1 ); + final long p2 = addVertex( mesh, ra, rb, rc, theta1, phi2 ); + final long p3 = addVertex( mesh, ra, rb, rc, theta2, phi1 ); + final long p4 = addVertex( mesh, ra, rb, rc, theta2, phi2 ); + + // Draw the triangles + addTriangle( mesh, p1, p3, p2 ); + addTriangle( mesh, p2, p3, p4 ); + } + } + return mesh; + } + + private static long addVertex( final Mesh mesh, final double ra, final double rb, final double rc, final double theta, final double phi ) + { + final double x = ra * Math.sin( theta ) * Math.cos( phi ); + final double y = rb * Math.sin( theta ) * Math.sin( phi ); + final double z = rc * Math.cos( theta ); + return mesh.vertices().add( x, y, z ); + } + + private static long addTriangle( final Mesh mesh, final long p1, final long p2, final long p3 ) + { + return mesh.triangles().add( p1, p2, p3 ); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest.java b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest.java index d4572ce3f..2d0b4773e 100644 --- a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest.java +++ b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest.java @@ -30,16 +30,15 @@ import java.util.Random; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; import fiji.plugin.trackmate.tracking.TrackerKeys; import fiji.plugin.trackmate.tracking.jaqaman.SparseLAPFrameToFrameTracker; -import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; -import fiji.plugin.trackmate.visualization.trackscheme.TrackScheme; import ij.ImageJ; public class KalmanTrackerInteractiveTest @@ -155,12 +154,9 @@ private Model testLAP( final SpotCollection spots ) ds.setTrackColorBy( TrackMateObject.TRACKS, TrackIndexAnalyzer.TRACK_INDEX ); ds.setSpotShowName( true ); - final SelectionModel selectionModel = new SelectionModel( model ); - final HyperStackDisplayer view = new HyperStackDisplayer( model, selectionModel, ds ); - view.render(); - - final TrackScheme trackscheme = new TrackScheme( model, selectionModel, ds ); - trackscheme.render(); + final GuiModel guiModel = new GuiModel( model, ds ); + guiModel.getWindowManager().createHyperStackDisplayer(); + guiModel.getWindowManager().createTrackScheme(); return model; } @@ -201,13 +197,9 @@ private Model test( final SpotCollection spots ) final TrackIndexAnalyzer ta = new TrackIndexAnalyzer(); ta.process( model.getTrackModel().trackIDs( true ), model ); - final SelectionModel selectionModel = new SelectionModel( model ); - final HyperStackDisplayer view = new HyperStackDisplayer( model, selectionModel, ds ); - view.render(); - - final TrackScheme trackscheme = new TrackScheme( model, selectionModel, ds ); - trackscheme.render(); - + final GuiModel guiModel = new GuiModel( model, ds ); + guiModel.getWindowManager().createHyperStackDisplayer(); + guiModel.getWindowManager().createTrackScheme(); return model; } @@ -237,7 +229,7 @@ private SpotCollection createParallelLines() { for ( int k = 0; k < y.length; k++ ) { - final Spot spot = new Spot( x[ k ] + ran.nextGaussian() * WIDTH / 100, y[ k ] + ran.nextGaussian() * WIDTH / 100, 0, 2, k, "T_" + k + "_S_" + t ); + final Spot spot = new SpotBase( x[ k ] + ran.nextGaussian() * WIDTH / 100, y[ k ] + ran.nextGaussian() * WIDTH / 100, 0, 2, k, "T_" + k + "_S_" + t ); spots.add( spot, t ); x[ k ] += vx0[ k ]; @@ -274,7 +266,13 @@ private SpotCollection createSpots() { for ( int k = 0; k < y.length; k++ ) { - final Spot spot = new Spot( x[ k ] + ran.nextGaussian() * WIDTH / 200, y[ k ] + ran.nextGaussian() * WIDTH / 200, 0, 2, k, "T_" + k + "_S_" + t ); + final Spot spot = new SpotBase( + x[ k ] + ran.nextGaussian() * WIDTH / 200, + y[ k ] + ran.nextGaussian() * WIDTH / 200, + 0, + 2, + k, + "T_" + k + "_S_" + t ); spots.add( spot, t ); x[ k ] += vx0[ k ]; diff --git a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest2.java b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest2.java index addc73199..c1c20f9f2 100644 --- a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest2.java +++ b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest2.java @@ -28,13 +28,11 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; import fiji.plugin.trackmate.io.TmXmlReader; -import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; import ij.ImagePlus; public class KalmanTrackerInteractiveTest2 @@ -79,12 +77,9 @@ public static void main( final String[] args ) } ij.ImageJ.main( args ); - final SelectionModel selectionModel = new SelectionModel( model ); - final DisplaySettings ds = DisplaySettings.defaultStyle().copy(); - ds.setSpotColorBy( TrackMateObject.SPOTS, Spot.QUALITY ); - - final HyperStackDisplayer view = new HyperStackDisplayer( model, selectionModel, imp, ds ); - view.render(); + final GuiModel guiModel = new GuiModel( model, imp ); + guiModel.getDisplaySettings().setSpotColorBy( TrackMateObject.SPOTS, Spot.QUALITY ); + guiModel.getWindowManager().createHyperStackDisplayer(); } } diff --git a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest3.java b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest3.java index 449152321..f4557da3f 100755 --- a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest3.java +++ b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest3.java @@ -25,14 +25,13 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; -import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; -import fiji.plugin.trackmate.visualization.trackscheme.TrackScheme; import ij.ImageJ; public class KalmanTrackerInteractiveTest3 @@ -86,12 +85,9 @@ private Model test( final SpotCollection spots ) final TrackIndexAnalyzer ta = new TrackIndexAnalyzer(); ta.process( model.getTrackModel().trackIDs( true ), model ); - final SelectionModel selectionModel = new SelectionModel( model ); - final HyperStackDisplayer view = new HyperStackDisplayer( model, selectionModel, ds ); - view.render(); - - final TrackScheme trackscheme = new TrackScheme( model, selectionModel, ds ); - trackscheme.render(); + final GuiModel guiModel = new GuiModel( model ); + guiModel.getWindowManager().createHyperStackDisplayer(); + guiModel.getWindowManager().createTrackScheme(); return model; } @@ -115,7 +111,13 @@ private SpotCollection createSingleLine() double y = y0; for ( int t = 0; t < NFRAMES; t++ ) { - final Spot spot = new Spot( x + ran.nextGaussian() * sigma, y + ran.nextGaussian() * sigma, 0, 2, 1, "S_" + t ); + final Spot spot = new SpotBase( + x + ran.nextGaussian() * sigma, + y + ran.nextGaussian() * sigma, + 0, + 2, + 1, + "S_" + t ); spots.add( spot, t ); x += vx0; diff --git a/src/test/java/fiji/plugin/trackmate/tracking/sparselap/SparseLAPTrackerExample.java b/src/test/java/fiji/plugin/trackmate/tracking/sparselap/SparseLAPTrackerExample.java index 3e7e95a04..206b35883 100755 --- a/src/test/java/fiji/plugin/trackmate/tracking/sparselap/SparseLAPTrackerExample.java +++ b/src/test/java/fiji/plugin/trackmate/tracking/sparselap/SparseLAPTrackerExample.java @@ -53,13 +53,10 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Settings; -import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.io.TmXmlReader; import fiji.plugin.trackmate.tracking.jaqaman.SparseLAPTracker; -import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; -import fiji.plugin.trackmate.visualization.trackscheme.TrackScheme; public class SparseLAPTrackerExample { @@ -103,15 +100,10 @@ public static void main( final String[] args ) model.setTracks( tracker.getResult(), true ); - final DisplaySettings ds = DisplaySettings.defaultStyle().copy(); - ij.ImageJ.main( args ); - final SelectionModel sm = new SelectionModel( model ); - final HyperStackDisplayer view = new HyperStackDisplayer( model, sm, ds ); - view.render(); - final TrackScheme trackScheme = new TrackScheme( model, sm, ds ); - trackScheme.render(); - + final GuiModel guiModel = new GuiModel( model ); + guiModel.getWindowManager().createHyperStackDisplayer(); + guiModel.getWindowManager().createTrackScheme(); } } diff --git a/src/test/java/fiji/plugin/trackmate/undo/MeshesEqualTest.java b/src/test/java/fiji/plugin/trackmate/undo/MeshesEqualTest.java new file mode 100644 index 000000000..299d6f6e4 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/undo/MeshesEqualTest.java @@ -0,0 +1,194 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2026 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.undo; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +import net.imglib2.mesh.impl.nio.BufferMesh; + +/** + * Tests for the meshesEqual helper method in UndoRedoStack. + */ +public class MeshesEqualTest +{ + + @Test + public void testMeshesEqualIdenticalMeshes() + { + final BufferMesh mesh1 = createTetrahedronMesh(); + final BufferMesh mesh2 = createTetrahedronMesh(); + assertThat( meshesEqual( mesh1, mesh2 ) ).isTrue(); + } + + @Test + public void testMeshesEqualSameReference() + { + final BufferMesh mesh = createTetrahedronMesh(); + assertThat( meshesEqual( mesh, mesh ) ).isTrue(); + } + + @Test + public void testMeshesEqualBothNull() + { + assertThat( meshesEqual( null, null ) ).isTrue(); + } + + @Test + public void testMeshesEqualOneNull() + { + final BufferMesh mesh = createTetrahedronMesh(); + assertThat( meshesEqual( mesh, null ) ).isFalse(); + assertThat( meshesEqual( null, mesh ) ).isFalse(); + } + + @Test + public void testMeshesEqualDifferentVertexPosition() + { + final BufferMesh mesh1 = createTetrahedronMesh(); + final BufferMesh mesh2 = createTetrahedronMesh(); + mesh2.vertices().setPositionf( 0, 1.0f, 0.0f, 1.0f ); + assertThat( meshesEqual( mesh1, mesh2 ) ).isFalse(); + } + + @Test + public void testMeshesEqualDifferentTriangle() + { + final BufferMesh mesh1 = createTetrahedronMesh(); + // Create mesh with different triangle connectivity + final BufferMesh mesh2 = new BufferMesh( 4, 4 ); + mesh2.vertices().add( 0.0f, 0.0f, 1.0f ); + mesh2.vertices().add( 0.0f, 0.942809f, -0.333333f ); + mesh2.vertices().add( -0.816497f, -0.471405f, -0.333333f ); + mesh2.vertices().add( 0.816497f, -0.471405f, -0.333333f ); + // Different triangle connectivity (swapped vertex order) + mesh2.triangles().add( 0, 2, 1 ); // Was: 0, 1, 2 + mesh2.triangles().add( 0, 3, 2 ); // Was: 0, 2, 3 + mesh2.triangles().add( 0, 1, 3 ); // Was: 0, 3, 1 + mesh2.triangles().add( 1, 2, 3 ); // Was: 1, 3, 2 + assertThat( meshesEqual( mesh1, mesh2 ) ).isFalse(); + } + + @Test + public void testMeshesEqualDifferentVertexCount() + { + final BufferMesh mesh1 = createTetrahedronMesh(); + final BufferMesh mesh2 = new BufferMesh( 5, 4 ); + mesh2.vertices().add( 0.0f, 0.0f, 1.0f ); + mesh2.vertices().add( 0.0f, 0.942809f, -0.333333f ); + mesh2.vertices().add( -0.816497f, -0.471405f, -0.333333f ); + mesh2.vertices().add( 0.816497f, -0.471405f, -0.333333f ); + mesh2.vertices().add( 1.0f, 1.0f, 1.0f ); // Extra vertex + mesh2.triangles().add( 0, 1, 2 ); + mesh2.triangles().add( 0, 2, 3 ); + mesh2.triangles().add( 0, 3, 1 ); + mesh2.triangles().add( 1, 3, 2 ); + assertThat( meshesEqual( mesh1, mesh2 ) ).isFalse(); + } + + @Test + public void testMeshesEqualDifferentTriangleCount() + { + final BufferMesh mesh1 = createTetrahedronMesh(); + final BufferMesh mesh2 = new BufferMesh( 4, 3 ); + mesh2.vertices().add( 0.0f, 0.0f, 1.0f ); + mesh2.vertices().add( 0.0f, 0.942809f, -0.333333f ); + mesh2.vertices().add( -0.816497f, -0.471405f, -0.333333f ); + mesh2.vertices().add( 0.816497f, -0.471405f, -0.333333f ); + mesh2.triangles().add( 0, 1, 2 ); + mesh2.triangles().add( 0, 2, 3 ); + mesh2.triangles().add( 0, 3, 1 ); + // One less triangle + assertThat( meshesEqual( mesh1, mesh2 ) ).isFalse(); + } + + /** + * Creates a simple tetrahedron mesh for testing. + */ + private static BufferMesh createTetrahedronMesh() + { + final BufferMesh mesh = new BufferMesh( 4, 4 ); + // 4 vertices of a tetrahedron + mesh.vertices().add( 0.0f, 0.0f, 1.0f ); + mesh.vertices().add( 0.0f, 0.942809f, -0.333333f ); + mesh.vertices().add( -0.816497f, -0.471405f, -0.333333f ); + mesh.vertices().add( 0.816497f, -0.471405f, -0.333333f ); + // 4 triangles + mesh.triangles().add( 0, 1, 2 ); + mesh.triangles().add( 0, 2, 3 ); + mesh.triangles().add( 0, 3, 1 ); + mesh.triangles().add( 1, 3, 2 ); + return mesh; + } + + /** + * Copies the meshesEqual logic from UndoRedoStack for testing. + */ + private static boolean meshesEqual( final BufferMesh a, final BufferMesh b ) + { + if ( a == null && b == null ) + return true; + if ( a == null || b == null ) + return false; + + final net.imglib2.mesh.Vertices verticesA = a.vertices(); + final net.imglib2.mesh.Vertices verticesB = b.vertices(); + final long nVerticesA = verticesA.size(); + final long nVerticesB = verticesB.size(); + + if ( nVerticesA != nVerticesB ) + return false; + + // Compare vertex positions + for ( long i = 0; i < nVerticesA; i++ ) + { + if ( Float.compare( verticesA.xf( i ), verticesB.xf( i ) ) != 0 || + Float.compare( verticesA.yf( i ), verticesB.yf( i ) ) != 0 || + Float.compare( verticesA.zf( i ), verticesB.zf( i ) ) != 0 ) + { + return false; + } + } + + // Compare triangles + final net.imglib2.mesh.Triangles trianglesA = a.triangles(); + final net.imglib2.mesh.Triangles trianglesB = b.triangles(); + final long nTrianglesA = trianglesA.size(); + final long nTrianglesB = trianglesB.size(); + + if ( nTrianglesA != nTrianglesB ) + return false; + + for ( long i = 0; i < nTrianglesA; i++ ) + { + if ( trianglesA.vertex0( i ) != trianglesB.vertex0( i ) || + trianglesA.vertex1( i ) != trianglesB.vertex1( i ) || + trianglesA.vertex2( i ) != trianglesB.vertex2( i ) ) + { + return false; + } + } + + return true; + } +} diff --git a/src/test/java/fiji/plugin/trackmate/undo/TrackNameUndoRedoGuiTest.java b/src/test/java/fiji/plugin/trackmate/undo/TrackNameUndoRedoGuiTest.java new file mode 100644 index 000000000..d38220b8e --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/undo/TrackNameUndoRedoGuiTest.java @@ -0,0 +1,86 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2010 - 2026 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * 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 + * . + * #L% + */ +package fiji.plugin.trackmate.undo; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Before; +import org.junit.Test; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.interactivetests.GraphTest; + +/** + * Tests for undo/redo of track name changes as they would occur from GUI actions. + * This verifies that track name changes wrapped in transactions are properly undoable. + */ +public class TrackNameUndoRedoGuiTest +{ + + private Model model; + + @Before + public void setUp() + { + this.model = GraphTest.getExampleModel(); + } + + @Test + public void testUndoTrackRenameWithTransaction() + { + // Simulate GUI-style track rename (wrapped in transaction) + final Integer trackId = model.getTrackModel().trackIDs( false ).iterator().next(); + final String originalName = model.getTrackModel().name( trackId ); + + // GUI components wrap the rename in a transaction + model.beginUpdate(); + try + { + model.setTrackName( trackId, "RenamedFromGUI" ); + } + finally + { + model.endUpdate(); + } + + // Verify name changed + assertThat( model.getTrackModel().name( trackId ) ) + .as( "Track name after rename" ) + .isEqualTo( "RenamedFromGUI" ); + + // Undo + model.undo(); + + // Verify name is restored + assertThat( model.getTrackModel().name( trackId ) ) + .as( "Track name after undo" ) + .isEqualTo( originalName ); + + // Redo + model.redo(); + + // Verify name is changed again + assertThat( model.getTrackModel().name( trackId ) ) + .as( "Track name after redo" ) + .isEqualTo( "RenamedFromGUI" ); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/undo/TrackNameUndoRedoTest.java b/src/test/java/fiji/plugin/trackmate/undo/TrackNameUndoRedoTest.java new file mode 100644 index 000000000..1cfab2003 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/undo/TrackNameUndoRedoTest.java @@ -0,0 +1,151 @@ +package fiji.plugin.trackmate.undo; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.Before; +import org.junit.Test; + +import fiji.plugin.trackmate.AssertJTrackMate; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.interactivetests.GraphTest; + +/** + * Tests for undo/redo of track name changes. + *

    + * Note: Track topology changes (split/merge) are already handled by the existing + * undo mechanism which restores edges. This test focuses on track name preservation + * through undo/redo operations. + */ +public class TrackNameUndoRedoTest +{ + + private Model model; + + private Model original; + + @Before + public void setUp() + { + this.model = GraphTest.getExampleModel(); + this.original = model.copy(); + } + + @Test + public void testUndoTrackRename() + { + // Get the first track ID + final Integer trackId = model.getTrackModel().trackIDs( false ).iterator().next(); + final String originalName = model.getTrackModel().name( trackId ); + + // Rename the track + model.beginUpdate(); + try + { + model.setTrackName( trackId, "MyCustomTrackName" ); + } + finally + { + model.endUpdate(); + } + + // Verify name changed + assertThat( model.getTrackModel().name( trackId ) ) + .as( "Track name after rename" ) + .isEqualTo( "MyCustomTrackName" ); + + // Model should differ from original + assertThatThrownBy( () -> AssertJTrackMate.testModelEquality( model, original ) ) + .isInstanceOf( AssertionError.class ); + + // Undo + model.undo(); + + // Verify name is restored + assertThat( model.getTrackModel().name( trackId ) ) + .as( "Track name after undo" ) + .isEqualTo( originalName ); + + // Model should be back to original + AssertJTrackMate.testModelEquality( model, original ); + + // Redo + model.redo(); + + // Verify name is changed again + assertThat( model.getTrackModel().name( trackId ) ) + .as( "Track name after redo" ) + .isEqualTo( "MyCustomTrackName" ); + } + + @Test + public void testUndoTrackRenameMultipleTracks() + { + // Rename multiple tracks + final Integer trackId1 = model.getTrackModel().trackIDs( false ).iterator().next(); + final String originalName1 = model.getTrackModel().name( trackId1 ); + + model.beginUpdate(); + try + { + model.setTrackName( trackId1, "FirstTrack" ); + } + finally + { + model.endUpdate(); + } + + // Verify + assertThat( model.getTrackModel().name( trackId1 ) ).isEqualTo( "FirstTrack" ); + + // Undo + model.undo(); + assertThat( model.getTrackModel().name( trackId1 ) ).isEqualTo( originalName1 ); + + // Redo + model.redo(); + assertThat( model.getTrackModel().name( trackId1 ) ).isEqualTo( "FirstTrack" ); + } + + @Test + public void testUndoTrackRenameAfterRedo() + { + // Test multiple undo/redo cycles + final Integer trackId = model.getTrackModel().trackIDs( false ).iterator().next(); + final String originalName = model.getTrackModel().name( trackId ); + + // First rename + model.beginUpdate(); + try + { + model.setTrackName( trackId, "FirstRename" ); + } + finally + { + model.endUpdate(); + } + + // Undo back to original + model.undo(); + assertThat( model.getTrackModel().name( trackId ) ).isEqualTo( originalName ); + + // Redo to first rename + model.redo(); + assertThat( model.getTrackModel().name( trackId ) ).isEqualTo( "FirstRename" ); + + // Rename again + model.beginUpdate(); + try + { + model.setTrackName( trackId, "SecondRename" ); + } + finally + { + model.endUpdate(); + } + + // Should undo to first rename, not original + model.undo(); + assertThat( model.getTrackModel().name( trackId ) ).isEqualTo( "FirstRename" ); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/undo/UndoRedoTest.java b/src/test/java/fiji/plugin/trackmate/undo/UndoRedoTest.java new file mode 100644 index 000000000..09014bcc8 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/undo/UndoRedoTest.java @@ -0,0 +1,446 @@ +package fiji.plugin.trackmate.undo; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.within; + +import java.util.Set; + +import org.jgrapht.graph.DefaultWeightedEdge; +import org.junit.Before; +import org.junit.Test; + +import fiji.plugin.trackmate.AssertJTrackMate; +import fiji.plugin.trackmate.FeatureModel; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.SpotRoi; +import fiji.plugin.trackmate.interactivetests.GraphTest; +import net.imglib2.mesh.impl.nio.BufferMesh; + +public class UndoRedoTest +{ + + private Model model; + + private Model original; + + @Before + public void setUp() + { + this.model = GraphTest.getExampleModel(); + this.original = model.copy(); + } + + @Test + public void testUndoAddSpot() + { + testCore( () -> addSpot() ); + } + + @Test + public void testUndoRemoveSpot() + { + testCore( () -> removeSpot() ); + } + + @Test + public void testUndoAddEdge() + { + testCore( () -> addEdge() ); + } + + @Test + public void testUndoRemoveEdge() + { + testCore( () -> removeEdge() ); + } + + @Test + public void testSeveralModifs() + { + final Runnable doModifs = () -> { + addSpot(); + removeSpot(); + addEdge(); + removeEdge(); + }; + testCore( doModifs ); + } + + @Test + public void testUndoChangePosition() + { + final Spot spot = model.getSpots().iterator( 0, true ).next(); + final double originalX = spot.getDoublePosition( 0 ); + + model.beginUpdate(); + try + { + model.beforeEdit( spot ); + spot.setPosition( originalX + 1,0 ); + } + finally + { + model.endUpdate(); + } + assertThat( originalX ).isNotEqualTo( spot.getDoublePosition( 0 ) ); + + // Undo command. + model.undo(); + + // Must succeed: model is back to original state. + assertThat( originalX ).isEqualTo( spot.getDoublePosition( 0 ) ); + } + + @Test + public void testUndoChangeName() + { + final Spot spot = model.getSpots().iterator( 0, true ).next(); + final String originalName = spot.getName(); + + model.beginUpdate(); + try + { + model.beforeEdit( spot ); + spot.setName( originalName + "_New name" ); + } + finally + { + model.endUpdate(); + } + assertThat( originalName ).isNotEqualTo( spot.getName() ); + + // Undo command. + model.undo(); + + // Must succeed: model is back to original state. + assertThat( originalName ).isEqualTo( spot.getName() ); + } + + @Test + public void testUndoChangeSpotFeature() + { + final Spot spot = model.getSpots().iterator( 0, true ).next(); + final String featureKey = "QUALITY"; + final double originalQuality = spot.getFeature( featureKey ); + final double newQuality = originalQuality + 100.0; + + model.beginUpdate(); + try + { + model.beforeEdit( spot ); + spot.putFeature( featureKey, newQuality ); + } + finally + { + model.endUpdate(); + } + + // Verify the feature was changed + assertThat( originalQuality ).isNotEqualTo( spot.getFeature( featureKey ) ); + assertThat( spot.getFeature( featureKey ) ).isEqualTo( newQuality ); + + // Undo command. + model.undo(); + + // Must succeed: model is back to original state. + assertThat( spot.getFeature( featureKey ) ) + .as( "Spot feature %s should be restored after undo", featureKey ) + .isEqualTo( originalQuality ); + } + + @Test + public void testUndoChangeEdgeFeature() + { + // Get an edge from the model + final DefaultWeightedEdge edge = model.getTrackModel().edgeSet().iterator().next(); + final Spot source = model.getTrackModel().getEdgeSource( edge ); + final FeatureModel featureModel = model.getFeatureModel(); + + // Get the first available edge feature key + final String featureKey = featureModel.getEdgeFeatures().iterator().next(); + final Double originalValue = featureModel.getEdgeFeature( edge, featureKey ); + final Double newValue = ( originalValue == null ? 42.0 : originalValue + 100.0 ); + + model.beginUpdate(); + try + { + // Flag the edge for undo by flagging the source spot + // (edge features are captured when touching spots are flagged) + model.beforeEdit( source ); + featureModel.putEdgeFeature( edge, featureKey, newValue ); + } + finally + { + model.endUpdate(); + } + + // Verify the feature was changed + assertThat( featureModel.getEdgeFeature( edge, featureKey ) ).isEqualTo( newValue ); + assertThat( featureModel.getEdgeFeature( edge, featureKey ) ).isNotEqualTo( originalValue ); + + // Undo command. + model.undo(); + + // Must succeed: model is back to original state. + assertThat( featureModel.getEdgeFeature( edge, featureKey ) ) + .as( "Edge feature %s should be restored after undo", featureKey ) + .isEqualTo( originalValue ); + } + + @Test + public void testUndoChangeSpotPolygon() + { + // Create a spot with ROI (a simple triangle) + final double[] xCoords = { -0.5, 0.5, 0.0 }; + final double[] yCoords = { -0.5, -0.5, 0.5 }; + final SpotRoi spotWithRoi = new SpotRoi( 0d, 0d, 0d, 1d, -1d, "TestSpot", xCoords, yCoords ); + + // Capture original polygon coordinates + final int nPoints = spotWithRoi.nPoints(); + final double[] originalXr = new double[ nPoints ]; + final double[] originalYr = new double[ nPoints ]; + for ( int i = 0; i < nPoints; i++ ) + { + originalXr[ i ] = spotWithRoi.xr( i ); + originalYr[ i ] = spotWithRoi.yr( i ); + } + + model.beginUpdate(); + try + { + model.beforeEdit( spotWithRoi ); + // Modify the polygon (change first point) + spotWithRoi.setXr( 0, originalXr[ 0 ] + 1.0 ); + spotWithRoi.setYr( 0, originalYr[ 0 ] + 1.0 ); + } + finally + { + model.endUpdate(); + } + + // Verify the polygon was changed + assertThat( spotWithRoi.xr( 0 ) ) + .as( "Polygon X[0] should be modified" ) + .isEqualTo( originalXr[ 0 ] + 1.0 ); + assertThat( spotWithRoi.yr( 0 ) ) + .as( "Polygon Y[0] should be modified" ) + .isEqualTo( originalYr[ 0 ] + 1.0 ); + + // Undo command. + model.undo(); + + // Must succeed: model is back to original state. + for ( int i = 0; i < nPoints; i++ ) + { + assertThat( spotWithRoi.xr( i ) ) + .as( "Polygon X[%d] should be restored after undo", i ) + .isEqualTo( originalXr[ i ] ); + assertThat( spotWithRoi.yr( i ) ) + .as( "Polygon Y[%d] should be restored after undo", i ) + .isEqualTo( originalYr[ i ] ); + } + } + + @Test + public void testUndoRedoSpotMove() + { + final Spot spot = model.getSpots().iterator( 0, true ).next(); + final double originalX = spot.getDoublePosition( 0 ); + final double originalY = spot.getDoublePosition( 1 ); + final double originalZ = spot.getDoublePosition( 2 ); + final double newX = originalX + 10.0; + final double newY = originalY + 10.0; + final double newZ = originalZ + 10.0; + + model.beginUpdate(); + try + { + model.beforeEdit( spot ); + spot.setPosition( newX, 0 ); + spot.setPosition( newY, 1 ); + spot.setPosition( newZ, 2 ); + } + finally + { + model.endUpdate(); + } + + // Verify the spot was moved + assertThat( spot.getDoublePosition( 0 ) ).isEqualTo( newX ); + assertThat( spot.getDoublePosition( 1 ) ).isEqualTo( newY ); + assertThat( spot.getDoublePosition( 2 ) ).isEqualTo( newZ ); + + // Undo command. + model.undo(); + + // Verify position is restored after undo + assertThat( spot.getDoublePosition( 0 ) ).isEqualTo( originalX ); + assertThat( spot.getDoublePosition( 1 ) ).isEqualTo( originalY ); + assertThat( spot.getDoublePosition( 2 ) ).isEqualTo( originalZ ); + + // Redo command. + model.redo(); + + // Verify position is restored after redo + assertThat( spot.getDoublePosition( 0 ) ).isEqualTo( newX ); + assertThat( spot.getDoublePosition( 1 ) ).isEqualTo( newY ); + assertThat( spot.getDoublePosition( 2 ) ).isEqualTo( newZ ); + } + + @Test + public void testUndoRedoSpotMeshScale() + { + // Create a simple tetrahedron mesh + final BufferMesh originalMesh = createTetrahedronMesh(); + final SpotMesh spotWithMesh = new SpotMesh( originalMesh, -1., "TestSpotMesh" ); + + // Add spot to model + model.beginUpdate(); + try + { + model.addSpotTo( spotWithMesh, 0 ); + } + finally + { + model.endUpdate(); + } + + // Capture original vertex positions and radius + final int nVertices = originalMesh.vertices().size(); + final float[] originalX = new float[ nVertices ]; + final float[] originalY = new float[ nVertices ]; + final float[] originalZ = new float[ nVertices ]; + final double originalRadius = spotWithMesh.getFeature( Spot.RADIUS ); + for ( int i = 0; i < nVertices; i++ ) + { + originalX[ i ] = originalMesh.vertices().xf( i ); + originalY[ i ] = originalMesh.vertices().yf( i ); + originalZ[ i ] = originalMesh.vertices().zf( i ); + } + + // Scale the mesh by factor of 2 + final double scaleFactor = 2.0; + model.beginUpdate(); + try + { + model.beforeEdit( spotWithMesh ); + spotWithMesh.scale( scaleFactor ); + } + finally + { + model.endUpdate(); + } + + // Verify the mesh was scaled + assertThat( spotWithMesh.getFeature( Spot.RADIUS ) ) + .as( "Radius should be scaled" ) + .isCloseTo( originalRadius * scaleFactor, within( 1e-10 ) ); + + // Undo command + model.undo(); + + // Verify radius is restored + assertThat( spotWithMesh.getFeature( Spot.RADIUS ) ) + .as( "Radius should be restored after undo" ) + .isCloseTo( originalRadius, within( 1e-10 ) ); + + // Verify mesh vertices are restored + for ( int i = 0; i < nVertices; i++ ) + { + assertThat( spotWithMesh.getMesh().vertices().xf( i ) ) + .as( "Mesh vertex X[%d] should be restored after undo", i ) + .isCloseTo( originalX[ i ], within( 1e-5f ) ); + assertThat( spotWithMesh.getMesh().vertices().yf( i ) ) + .as( "Mesh vertex Y[%d] should be restored after undo", i ) + .isCloseTo( originalY[ i ], within( 1e-5f ) ); + assertThat( spotWithMesh.getMesh().vertices().zf( i ) ) + .as( "Mesh vertex Z[%d] should be restored after undo", i ) + .isCloseTo( originalZ[ i ], within( 1e-5f ) ); + } + + // Redo command + model.redo(); + + // Verify radius is restored after redo + assertThat( spotWithMesh.getFeature( Spot.RADIUS ) ) + .as( "Radius should be restored after redo" ) + .isCloseTo( originalRadius * scaleFactor, within( 1e-10 ) ); + } + + /** + * Creates a simple tetrahedron mesh for testing. + */ + private static BufferMesh createTetrahedronMesh() + { + final BufferMesh mesh = new BufferMesh( 4, 4 ); + // 4 vertices of a tetrahedron + mesh.vertices().add( 0.0f, 0.0f, 1.0f ); + mesh.vertices().add( 0.0f, 0.942809f, -0.333333f ); + mesh.vertices().add( -0.816497f, -0.471405f, -0.333333f ); + mesh.vertices().add( 0.816497f, -0.471405f, -0.333333f ); + // 4 triangles + mesh.triangles().add( 0, 1, 2 ); + mesh.triangles().add( 0, 2, 3 ); + mesh.triangles().add( 0, 3, 1 ); + mesh.triangles().add( 1, 3, 2 ); + return mesh; + } + + private void testCore( final Runnable doModifs ) + { + // Must succeed: model is not modified yet. + AssertJTrackMate.testModelEquality( model, original, false ); + + // Do modifications. + model.beginUpdate(); + try + { + doModifs.run(); + } + finally + { + model.endUpdate(); + } + + // Must fail: model is modified. + assertThatThrownBy( () -> AssertJTrackMate.testModelEquality( model, original, false ) ) + .isInstanceOf( AssertionError.class ); + + // Undo commands. + model.undo(); + + // Must succeed: model is back to original state. + // Use flexible comparison (ignore track IDs) since track topology changes + // may result in different track IDs even when content is restored. + AssertJTrackMate.testModelEquality( model, original, false ); + } + + private void addSpot() + { + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); + model.addSpotTo( spot, 0 ); + } + + private void removeSpot() + { + final Spot spot = model.getSpots().iterator( 0, true ).next(); + model.removeSpot( spot ); + } + + private void addEdge() + { + final Spot source = model.getSpots().iterator( 0, true ).next(); + final Spot target = model.getSpots().iterator( 3, true ).next(); + model.addEdge( source, target, 0 ); + } + + private void removeEdge() + { + final Set< DefaultWeightedEdge > edges = model.getTrackModel().edgeSet(); + model.removeEdge( edges.iterator().next() ); + } +} \ No newline at end of file diff --git a/src/test/java/fiji/plugin/trackmate/util/SpotRoiIterableTest.java b/src/test/java/fiji/plugin/trackmate/util/SpotRoiIterableTest.java index 6096f0685..66a290125 100644 --- a/src/test/java/fiji/plugin/trackmate/util/SpotRoiIterableTest.java +++ b/src/test/java/fiji/plugin/trackmate/util/SpotRoiIterableTest.java @@ -61,7 +61,7 @@ public void testIterationPolygon() 84, 85 }; final TIntArrayList vals = new TIntArrayList(); - final IterableInterval< UnsignedByteType > iterable = SpotUtil.iterable( spot, new ImgPlus<>( img ) ); + final IterableInterval< UnsignedByteType > iterable = spot.iterable( new ImgPlus<>( img ) ); final Cursor< UnsignedByteType > cursor = iterable.cursor(); while ( cursor.hasNext() ) { @@ -85,7 +85,7 @@ public static void main( final String[] args ) final double[] yp = new double[] { 1.5, 5, 8.8, 5 }; final Spot spot = SpotRoi.createSpot( xp, yp, 1. ); - final IterableInterval< UnsignedByteType > iterable = SpotUtil.iterable( spot, new ImgPlus<>( img ) ); + final IterableInterval< UnsignedByteType > iterable = spot.iterable( new ImgPlus<>( img ) ); final Cursor< UnsignedByteType > cursor = iterable.cursor(); while ( cursor.hasNext() ) { diff --git a/src/test/java/fiji/plugin/trackmate/util/cli/ExampleCommandCLI.java b/src/test/java/fiji/plugin/trackmate/util/cli/ExampleCommandCLI.java deleted file mode 100644 index 18ea2e60b..000000000 --- a/src/test/java/fiji/plugin/trackmate/util/cli/ExampleCommandCLI.java +++ /dev/null @@ -1,240 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2026 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * 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 - * . - * #L% - */ -package fiji.plugin.trackmate.util.cli; - -import java.awt.BorderLayout; -import java.util.List; - -import javax.swing.JButton; -import javax.swing.JFrame; - -import fiji.plugin.trackmate.util.cli.ConfigGuiBuilder.ConfigPanel; - -public class ExampleCommandCLI extends CommandCLIConfigurator -{ - - private final IntArgument nThreads; - - private final DoubleArgument radius; - - private final DoubleArgument time; - - private final DoubleArgument weight; - - public ExampleCommandCLI() - { - /* - * The executable that will be called by the TrackMate module. - * - * CLI configs that inherit from 'CommandCLIConfigurator' are all based - * on an actual executable, that is a file with exec rights somewhere on - * the user computer. They need to set it themselves, so the config part - * only specifies the name, the help and the key of this command. - * - * The help and name will be used in the UI. - */ - executable - .name( "Path to the executable." ) - .help( "Browse to the executable location on your computer." ) - .key( "PATH_TO_EXECUTABLE" ); - - /* - * In this example we will assume that the tool we want to run accepts a - * few arguments. - * - * The first one is the --nThreads argument, that accept - * integer larger than 1 and smaller than 24. - * - * The 'argument()' part must be something the tool can understand. This - * is what is passed to it before the value. - * - * This example argument is not required, but has a default value of 1. - * The default value is used only in the command line. If an argument is - * not required, is not set, but has a default value, then the argument - * will appear in the command line with this default value. - * - * Adding arguments is done via 'adder' methods, that are only visible - * in inhering classes. The 'get()' method of the adder returns the - * created argument. It also adds it to the inner parts of the mother - * class, so that it is handled automatically when creating a GUI or a - * command line. But it is a good idea to expose it in this concrete - * class so that you can expose it to the user and let them set it. - */ - this.nThreads = addIntArgument() - .argument( "--nThreads" ) // arg in the command line - .name( "N threads" ) // convenient name - .help( "Sets the number of threads to use for computation." ) // help - .defaultValue( 1 ) - .min( 1 ) - .max( 24 ) // will be used to create an adequate UI widget - .key( "N_THREADS" ) // use to serialize to Map - .get(); - - /* - * The second argument is a double. It also has a unit, which is only - * used in the UI. - * - * Additionally, the value is stored as a radius, but displayed as a - * diameter, with values being converted on the fly. - * - * Because it does not specify a min or a max, any numerical value can - * be entered in the GUI. The implementation will have to add an extra - * check to verify consistency of values. - * - * With this, the radius will have the default value of 5, and a - * diameter of 10 µm will be displayed in the UI. - */ - this.radius = CommonTrackMateArguments.addDiameter( this, "µm" ); - - /* - * The third argument is a double. It is required, which means that an - * error will be thrown when making a command line from this config if - * the user forgot to set a value. - */ - this.weight = addDoubleArgument() - .argument( "--weight" ) - .name( "Weight" ) - .key( "WEIGHT" ) - .defaultValue( 70. ) - .units( "kg" ) - .required( true ) // required flag - .get(); - - /* - * A double argument that has a min and a max will generate another - * widget in the UI: a slider. - */ - this.time = addDoubleArgument() - .argument( "--time" ) - .name( "Time" ) - .help( "Time to wait after processing." ) - .key( "TIME" ) - .min( 1. ) - .max( 100. ) - .units( "seconds" ) - .get(); - } - - public IntArgument nThreads() - { - return nThreads; - } - - public DoubleArgument radius() - { - return radius; - } - - public DoubleArgument weight() - { - return weight; - } - - public DoubleArgument time() - { - return time; - } - - public static void main( final String[] args ) - { - final ExampleCommandCLI cli = new ExampleCommandCLI(); - - /* - * Configure the CLI. - */ - - cli.getCommandArg().set( "/path/to/my/executable" ); - - /* - * Play with the command line. - */ - - /* - * Will generate an error because 'weight' is required and not set. - * - * The argument 'nThreads' is not set, but it has a default value and is - * not required -> no error, the command line will use the default - * value. - * - * The argument 'time' is not set either, does not have a default, but - * it is not required -> no error, the command line will just miss the - * 'time' argument. - */ - System.out.println( "First attempt with the command line:" ); - try - { - final List< String > cmd = CommandBuilder.build( cli ); - System.out.println( "To run: " + cmd ); // error - } - catch ( final IllegalArgumentException e ) - { - System.err.println( "Could not generate the command line:" ); - System.err.println( e.getMessage() ); - } - - // Set the weight. Now it should be ok. - System.out.println( "\nSecond attempt with the command line, after setting the weight:" ); - cli.weight().set( 50. ); - try - { - final List< String > cmd = CommandBuilder.build( cli ); - System.out.println( "To run: " + cmd ); - } - catch ( final IllegalArgumentException e ) - { - System.err.println( "Could not generate the command line:" ); - System.err.println( e.getMessage() ); - } - - /* - * Make a UI that configures the CLI. - */ - - // The UI cannot be created wit arguments that do not have a value. This - // will generate an error: - System.out.println( "\nFirst attempt with the UI generator:" ); - try - { - ConfigGuiBuilder.build( cli ); - } - catch ( final IllegalArgumentException e ) - { - System.err.println( "Could not generate the UI:" ); - System.err.println( e.getMessage() ); - } - - System.out.println( "\nSecond attempt with the UI generator:" ); - cli.time().set( 5. ); - cli.nThreads().set( 2 ); - // This should be ok now. - final ConfigPanel panel = ConfigGuiBuilder.build( cli ); - final JFrame frame = new JFrame( "Demo CLI tool" ); - frame.getContentPane().add( panel, BorderLayout.CENTER ); - final JButton btn = new JButton( "echo" ); - btn.addActionListener( e -> System.out.println( CommandBuilder.build( cli ) ) ); - frame.getContentPane().add( btn, BorderLayout.SOUTH ); - frame.setLocationRelativeTo( null ); - frame.pack(); - frame.setVisible( true ); - frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); - } -} diff --git a/src/test/java/fiji/plugin/trackmate/visualization/bvv/BVVKeymapErrorDemo.java b/src/test/java/fiji/plugin/trackmate/visualization/bvv/BVVKeymapErrorDemo.java new file mode 100644 index 000000000..2a5e83933 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/visualization/bvv/BVVKeymapErrorDemo.java @@ -0,0 +1,119 @@ +package fiji.plugin.trackmate.visualization.bvv; + +import java.awt.EventQueue; +import java.io.File; + +import org.scijava.ui.behaviour.io.gui.CommandDescriptions; + +import bdv.ui.keymap.Keymap; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.gui.GuiModel; +import fiji.plugin.trackmate.gui.WindowManager; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.io.TmXmlReader; +import ij.ImageJ; +import ij.ImagePlus; + +/** + * Demo class to reproduce the BVV keymap error issue. + */ +public class BVVKeymapErrorDemo +{ + + public static void main( final String[] args ) + { + try + { + fiji.plugin.trackmate.gui.GuiUtils.setSystemLookAndFeel(); + ImageJ.main( args ); + Thread.sleep( 1000 ); + + // Run the demo + EventQueue.invokeLater( () -> runDemo() ); + } + catch ( final Throwable t ) + { + t.printStackTrace(); + } + } + + private static void runDemo() + { + System.out.println( "=== BVV Keymap Error Demo ===" ); + System.out.println( "" ); + System.out.println( "This demo shows the 'Could not assign InputTrigger' errors" ); + System.out.println( "that occur when the BVV keymap is missing or corrupted." ); + System.out.println( "" ); + + // Load a TrackMate file (3D data required for BVV) + final String filePath = "samples/CElegans3D-smoothed-mask-orig-02.xml"; + final TmXmlReader reader = new TmXmlReader( new File( filePath ) ); + if ( !reader.isReadingOk() ) + { + System.err.println( "Error reading TrackMate file: " + reader.getErrorMessage() ); + return; + } + + final Model model = reader.getModel(); + final ImagePlus imp = reader.readImage(); + final Settings settings = reader.readSettings( imp ); + final DisplaySettings ds = reader.getDisplaySettings(); + final GuiModel guiModel = new GuiModel( model, settings, ds ); + final WindowManager wm = guiModel.getWindowManager(); + + System.out.println( "=== Step 1: Check initial BVV keymap state ===" ); + final BVVKeymapManager bvvKeymapManager = guiModel.getBvvKeymapManager(); + System.out.println( "BVV Keymap user styles: " + bvvKeymapManager.getUserStyles().size() ); + System.out.println( "BVV Keymap selected: " + bvvKeymapManager.getForwardSelectedKeymap().getName() ); + + // Check if keymap has BVV bindings + final var config = bvvKeymapManager.getForwardSelectedKeymap().getConfig(); + final var rotateLeftTriggers = config.getInputs( "rotate left", "bvv" ); + System.out.println( "rotate left triggers in bvv context: " + rotateLeftTriggers ); + System.out.println( "" ); + + if ( rotateLeftTriggers.isEmpty() ) + { + System.out.println( ">>> WARNING: BVV keymap is empty! Errors will occur. <<<" ); + System.out.println( "" ); + } + else + { + System.out.println( ">>> BVV keymap has bindings. To see errors, the keymap needs to be corrupted. <<<" ); + System.out.println( "" ); + } + + System.out.println( "=== Step 2: Launch BVV view ===" ); + System.out.println( "Watch for 'Could not assign InputTrigger' errors below:" ); + System.out.println( "---" ); + + wm.createBVV().render(); + + System.out.println( "---" ); + + // Simulate what happens when PreferencesDialog is opened + // This is where the errors actually occur - when the VisualEditorPanel + // tries to populate the table with all commands for the BVV context + System.out.println( "" ); + System.out.println( "=== Step 3: Simulate PreferencesDialog opening ===" ); + System.out.println( "The PreferencesDialog creates a VisualEditorPanel which" ); + System.out.println( "calls InputTriggerConfig.put() for all BVV commands." ); + System.out.println( "" ); + + // Get the BVV keymap config and simulate what the PreferencesDialog does + bvvKeymapManager.getForwardSelectedKeymap().getConfig(); + final CommandDescriptions bvvDescriptions = bvvKeymapManager.getCommandDescriptions(); + + System.out.println( "BVV command descriptions discovered: " + + (bvvDescriptions != null ? bvvDescriptions.toString() : "null") ); + + System.out.println( "=== Changing keymap ===" ); + final BVVKeymapManager keymapManager = guiModel.getBvvKeymapManager(); + final Keymap keymap = keymapManager.getUserStyles().get( 0 ); + keymapManager.setSelectedStyle( keymap ); + System.out.println( "" ); + System.out.println( "=== Demo complete ===" ); + System.out.println( "" ); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/visualization/table/TrackMateTableExample.java b/src/test/java/fiji/plugin/trackmate/visualization/table/TrackMateTableExample.java index a9e3bcdc5..6870b2701 100644 --- a/src/test/java/fiji/plugin/trackmate/visualization/table/TrackMateTableExample.java +++ b/src/test/java/fiji/plugin/trackmate/visualization/table/TrackMateTableExample.java @@ -28,6 +28,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.gui.GuiModel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.io.TmXmlReader; import ij.ImageJ; @@ -47,6 +48,7 @@ public static void main( final String[] args ) throws ClassNotFoundException, In model.getSpots().iterable( 1, true ).forEach( selectionModel::addSpotToSelection ); final String exportFile = System.getProperty( "user.home" ) + File.separator + "test"; - new TrackTableView( model, selectionModel, ds, exportFile ).render(); + final GuiModel guiModel = new GuiModel( model, ds ); + new TrackTableView( guiModel, exportFile ).render(); } }