+ * The display settings are specified as a map of strings to objects. The + * accepted key and value types are: + *
'FramePosition'
→ an int[]
array of 4
+ * elements: x, y, width and height.
+ * 'LockGroupId'
→ an integer that specifies the lock
+ * group id.
+ * 'SettingsPanelVisible'
→ a boolean that specifies
+ * whether the settings panel is visible on this view.
+ * 'BdvState'
→ a XML Element that specifies the BDV
+ * window state. See {@link ViewerPanel#stateToXml()} and
+ * {@link ViewerPanel#stateFromXml(org.jdom2.Element)} for more information.
+ * 'BdvTransform'
→ an {@link AffineTransform3D} that
+ * specifies the view point.
+ * 'NoColoring'
→ a boolean; if true
, the
+ * feature or tag coloring will be ignored.
+ * 'TagSet'
→ a string specifying the name of the
+ * tag-set to use for coloring. If not null
, the coloring will
+ * be done using the tag-set.
+ * 'FeatureColorMode'
→ a String specifying the name
+ * of the feature color mode to use for coloring. If not null
,
+ * the coloring will be done using the feature color mode.
+ * 'ColorbarVisible'
→ a boolean specifying whether
+ * the colorbar is visible for tag-set and feature-based coloring.
+ * 'ColorbarPosition'
→ a {@link Position} specifying
+ * the position of the colorbar.
+ * + * The display settings are specified as a map of strings to objects. The + * accepted key and value types are: + *
'FramePosition'
→ an int[]
array of 4
+ * elements: x, y, width and height.
+ * 'LockGroupId'
→ an integer that specifies the lock
+ * group id.
+ * 'SettingsPanelVisible'
→ a boolean that specifies
+ * whether the settings panel is visible on this view.
+ * 'TrackSchemeTransform'
→ a {@link ScreenTransform}
+ * that defines the starting view zone in TrackScheme.
+ * 'NoColoring'
→ a boolean; if true
, the
+ * feature or tag coloring will be ignored.
+ * 'TagSet'
→ a string specifying the name of the
+ * tag-set to use for coloring. If not null
, the coloring will
+ * be done using the tag-set.
+ * 'FeatureColorMode'
→ a @link String specifying the
+ * name of the feature color mode to use for coloring. If not
+ * null
, the coloring will be done using the feature color
+ * mode.
+ * 'ColorbarVisible'
→ a boolean specifying whether
+ * the colorbar is visible for tag-set and feature-based coloring.
+ * 'ColorbarPosition'
→ a {@link Position} specifying
+ * the position of the colorbar.
+ *
+ * The table will only display the current content of the selection, and
+ * will listen to its changes. If false
, the table will display
+ * the full graph content, listen to its changes, and will be able to edit
+ * the selection.
+ *
+ * @return the new selection table view.
+ */
+ public MamutViewSelectionTable createSelectionTable()
+ {
+ return createSelectionTable( new HashMap<>() );
+ }
+
+ /**
+ * Creates and displays a new Selection Table view, using a map to specify
+ * the display settings.
+ *
+ * The table will only display the current content of the selection, and
+ * will listen to its changes. If false
, the table will display
+ * the full graph content, listen to its changes, and will be able to edit
+ * the selection.
+ *
+ * @param guiState
+ * the map of settings.
+ * @return the new selection table view.
+ * @see #createTable(Map)
+ */
+ public MamutViewSelectionTable createSelectionTable( final Map< String, Object > guiState )
+ {
+ return createView( MamutViewSelectionTable.class, guiState );
+ }
+
+ /**
+ * Creates and displays a new Table view, using a map to specify the display
+ * settings.
+ *
+ * @return the new table view.
+ */
+ public MamutViewTable createTable()
+ {
+ return createTable( new HashMap<>() );
+ }
+
+ /**
+ * Creates and displays a new Table view, using a map to specify the display
+ * settings.
+ *
+ * The display settings are specified as a map of strings to objects. The + * accepted key and value types are: + *
'FramePosition'
→ an int[]
array of 4
+ * elements: x, y, width and height.
+ * 'LockGroupId'
→ an integer that specifies the lock
+ * group id.
+ * 'SettingsPanelVisible'
→ a boolean that specifies
+ * whether the settings panel is visible on this view.
+ * 'NoColoring'
→ a boolean; if true
, the
+ * feature or tag coloring will be ignored.
+ * 'TagSet'
→ a string specifying the name of the
+ * tag-set to use for coloring. If not null
, the coloring will
+ * be done using the tag-set.
+ * 'FeatureColorMode'
→ a @link String specifying the
+ * name of the feature color mode to use for coloring. If not
+ * null
, the coloring will be done using the feature color
+ * mode.
+ * 'ColorbarVisible'
→ a boolean specifying whether
+ * the colorbar is visible for tag-set and feature-based coloring.
+ * 'ColorbarPosition'
→ a {@link Position} specifying
+ * the position of the colorbar.
+ * + * The display settings are specified as a map of strings to objects. The + * accepted key and value types are: + *
'FramePosition'
→ an int[]
array of 4
+ * elements: x, y, width and height.
+ * 'LockGroupId'
→ an integer that specifies the lock
+ * group id.
+ * 'SettingsPanelVisible'
→ a boolean that specifies
+ * whether the settings panel is visible on this view.
+ * 'NoColoring'
→ a boolean; if true
, the
+ * feature or tag coloring will be ignored.
+ * 'TagSet'
→ a string specifying the name of the
+ * tag-set to use for coloring. If not null
, the coloring will
+ * be done using the tag-set.
+ * 'FeatureColorMode'
→ a @link String specifying the
+ * name of the feature color mode to use for coloring. If not
+ * null
, the coloring will be done using the feature color
+ * mode.
+ * 'ColorbarVisible'
→ a boolean specifying whether
+ * the colorbar is visible for tag-set and feature-based coloring.
+ * 'ColorbarPosition'
→ a {@link Position} specifying
+ * the position of the colorbar.
+ * 'GrapherTransform'
→ a
+ * {@link org.mastodon.views.grapher.datagraph.ScreenTransform} specifying
+ * the region to initially zoom on the XY plot.
+ * Map< String, Object >
to XML and vice versa.
+ * Map< String, Object >
to XML and vice versa.
*/
-class MamutViewStateXMLSerialization
+public class MamutViewStateXMLSerialization
{
private static final String WINDOW_TAG = "Window";
@@ -88,7 +88,7 @@ class MamutViewStateXMLSerialization
*/
private static final String CHOSEN_CONTEXT_PROVIDER_KEY = "ContextProvider";
- static Element toXml( final Map< String, Object > guiState )
+ public static Element toXml( final Map< String, Object > guiState )
{
final Element element = new Element( WINDOW_TAG );
toXml( guiState, element );
@@ -191,7 +191,7 @@ else if ( value instanceof List )
* @param windowManager
* the application {@link WindowManager}.
*/
- static void fromXml( final Element windowsEl, final WindowManager windowManager )
+ public static void fromXml( final Element windowsEl, final WindowManager windowManager )
{
final MamutViews viewFactories = windowManager.getViewFactories();
final Collection< Class< ? extends MamutViewI > > classes = viewFactories.getKeys();
diff --git a/src/main/java/org/mastodon/mamut/io/ProjectActions.java b/src/main/java/org/mastodon/mamut/io/ProjectActions.java
index 97e923328..b60035e6a 100644
--- a/src/main/java/org/mastodon/mamut/io/ProjectActions.java
+++ b/src/main/java/org/mastodon/mamut/io/ProjectActions.java
@@ -34,8 +34,10 @@
import javax.swing.JOptionPane;
+import org.mastodon.app.MastodonIcons;
import org.mastodon.mamut.KeyConfigScopes;
import org.mastodon.mamut.ProjectModel;
+import org.mastodon.mamut.io.project.MamutImagePlusProject;
import org.mastodon.mamut.launcher.LauncherUtil;
import org.mastodon.ui.keymap.KeyConfigContexts;
import org.scijava.Context;
@@ -60,6 +62,7 @@ public class ProjectActions
public static final String IMPORT_SIMI = "import simi";
public static final String IMPORT_MAMUT = "import mamut";
public static final String EXPORT_MAMUT = "export mamut";
+ public static final String FIX_DATASET_PATH = "fix project image path";
static final String[] CREATE_PROJECT_KEYS = new String[] { "not mapped" };
static final String[] CREATE_PROJECT_FROM_URL_KEYS = new String[] { "not mapped" };
@@ -70,6 +73,7 @@ public class ProjectActions
static final String[] IMPORT_SIMI_KEYS = new String[] { "not mapped" };
static final String[] IMPORT_MAMUT_KEYS = new String[] { "not mapped" };
static final String[] EXPORT_MAMUT_KEYS = new String[] { "not mapped" };
+ static final String[] FIX_DATASET_PATH_KEYS = { "not mapped" };
/**
* Install the global actions for creating, loading or importing a new
@@ -115,15 +119,34 @@ public static void installAppActions( final Actions actions, final ProjectModel
final RunnableAction importTgmmAction = new RunnableAction( IMPORT_TGMM, () -> ProjectImporter.importTgmmDataWithDialog( appModel, parentComponent ) );
final RunnableAction importSimiAction = new RunnableAction( IMPORT_TGMM, () -> ProjectImporter.importSimiDataWithDialog( appModel, parentComponent ) );
final RunnableAction exportMamutAction = new RunnableAction( EXPORT_MAMUT, () -> ProjectExporter.exportMamut( appModel, parentComponent ) );
+ final RunnableAction fixDatasetPathAction = new RunnableAction( FIX_DATASET_PATH, () -> tweakDatasetPath( appModel, parentComponent ) );
actions.namedAction( saveProjectAction, SAVE_PROJECT_KEYS );
actions.namedAction( saveProjectAsAction, SAVE_PROJECT_AS_KEYS );
actions.namedAction( importTgmmAction, IMPORT_TGMM_KEYS );
actions.namedAction( importSimiAction, IMPORT_SIMI_KEYS );
actions.namedAction( exportMamutAction, EXPORT_MAMUT_KEYS );
+ actions.namedAction( fixDatasetPathAction, FIX_DATASET_PATH_KEYS );
}
- private static Runnable runInNewThread( Runnable o )
+ private static void tweakDatasetPath( final ProjectModel appModel, final Frame parentComponent )
+ {
+ if ( appModel.getProject() instanceof MamutImagePlusProject )
+ {
+ JOptionPane.showMessageDialog(
+ null,
+ "The current project is based on an \n"
+ + "ImagePlus as image data source. \n"
+ + "Its dataset path cannot be edited.",
+ "Cannot edit dataset path",
+ JOptionPane.WARNING_MESSAGE,
+ MastodonIcons.MASTODON_ICON_MEDIUM );
+ return;
+ }
+ new DatasetPathDialog( parentComponent, appModel ).setVisible( true );
+ }
+
+ private static Runnable runInNewThread( final Runnable o )
{
return () -> {
new Thread( () -> {
@@ -131,7 +154,7 @@ private static Runnable runInNewThread( Runnable o )
{
o.run();
}
- catch ( Throwable t )
+ catch ( final Throwable t )
{
t.printStackTrace();
}
@@ -164,6 +187,8 @@ public void getCommandDescriptions( final CommandDescriptions descriptions )
"Import tracks from a Simi Biocell .sbd into the current project." );
descriptions.add( IMPORT_MAMUT, IMPORT_MAMUT_KEYS, "Import a MaMuT project." );
descriptions.add( EXPORT_MAMUT, EXPORT_MAMUT_KEYS, "Export current project as a MaMuT project." );
+ descriptions.add( FIX_DATASET_PATH, FIX_DATASET_PATH_KEYS,
+ "Shows a dialog to modify a new path to the image data and whether it is relative or absolute." );
}
}
diff --git a/src/main/java/org/mastodon/mamut/io/exporter/mamut/MamutExporterPlugin.java b/src/main/java/org/mastodon/mamut/io/exporter/mamut/MamutExporterPlugin.java
index 4ac3edf67..efa1d8ca5 100644
--- a/src/main/java/org/mastodon/mamut/io/exporter/mamut/MamutExporterPlugin.java
+++ b/src/main/java/org/mastodon/mamut/io/exporter/mamut/MamutExporterPlugin.java
@@ -35,7 +35,7 @@
import java.awt.Image;
import java.io.File;
import java.io.IOException;
-import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -45,6 +45,7 @@
import org.mastodon.app.MastodonIcons;
import org.mastodon.app.ui.ViewMenuBuilder;
import org.mastodon.mamut.KeyConfigScopes;
+import org.mastodon.mamut.MamutMenuBuilder;
import org.mastodon.mamut.ProjectModel;
import org.mastodon.mamut.io.importer.trackmate.MamutExporter;
import org.mastodon.mamut.plugin.MamutPlugin;
@@ -92,8 +93,8 @@ public Map< String, String > getMenuTexts()
@Override
public List< ViewMenuBuilder.MenuItem > getMenuItems()
{
- return Arrays.asList(
- menu( "Plugins", menu( "Exports", item( EXPORT_MAMUT ) ) ) );
+ return Collections.singletonList(
+ MamutMenuBuilder.fileMenu( menu( "Export", item( EXPORT_MAMUT ) ) ) );
}
@Override
diff --git a/src/main/java/org/mastodon/mamut/io/img/cache/MastodonSimpleCacheArrayLoader.java b/src/main/java/org/mastodon/mamut/io/img/cache/MastodonSimpleCacheArrayLoader.java
new file mode 100644
index 000000000..8c018fe54
--- /dev/null
+++ b/src/main/java/org/mastodon/mamut/io/img/cache/MastodonSimpleCacheArrayLoader.java
@@ -0,0 +1,118 @@
+/*-
+ * #%L
+ * Mastodon
+ * %%
+ * Copyright (C) 2014 - 2024 Mastodon developers
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package org.mastodon.mamut.io.img.cache;
+
+import java.io.IOException;
+
+import bdv.img.cache.CacheArrayLoader;
+import bdv.img.cache.CreateInvalidVolatileCell;
+import bdv.img.cache.DefaultEmptyArrayCreator;
+import bdv.img.cache.EmptyArrayCreator;
+import bdv.img.cache.SimpleCacheArrayLoader;
+import net.imglib2.img.basictypeaccess.DataAccess;
+import net.imglib2.img.basictypeaccess.volatiles.VolatileArrayDataAccess;
+import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileByteArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileCharArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileDoubleArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileFloatArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileIntArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileLongArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.DirtyVolatileShortArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileByteArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileCharArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileDoubleArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileFloatArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileIntArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileLongArray;
+import net.imglib2.img.basictypeaccess.volatiles.array.VolatileShortArray;
+import net.imglib2.img.cell.CellGrid;
+import net.imglib2.type.NativeType;
+
+public interface MastodonSimpleCacheArrayLoader< A extends DataAccess > extends SimpleCacheArrayLoader< A >
+{
+
+ /**
+ * Implementing classes must override this if {@code A} is not a standard
+ * {@link VolatileArrayDataAccess} type. The default implementation returns
+ * {@code null}, which will let
+ * {@link CreateInvalidVolatileCell#get(CellGrid, NativeType, boolean)
+ * CreateInvalidVolatileCell.get(...)} try to figure out the appropriate
+ * {@link DefaultEmptyArrayCreator}.
+ * + * Default access types are + *
+ *+ * {@code SimpleCacheArrayLoader} is supposed to load data one specific + * image. {@code loadArray()} will not get information about which + * timepoint, resolution level, etc a requested block belongs to. + *
+ * This is in contrast to + * {@link CacheArrayLoader#loadArray(int, int, int, int[], long[])}, where + * all information to identify a particular block in a whole dataset is + * provided. + * + * @param gridPosition + * the coordinate of the cell in the cell grid. + * @param cellDimensions + * the dimensions of the cell. + * @param timepoint + * the timepoint. + * + * @return loaded cell data. + */ + A loadArrayAtTimepoint( long[] gridPosition, int[] cellDimensions, int timepoint ) throws IOException; +} diff --git a/src/main/java/org/mastodon/mamut/io/img/cache/MastodonVolatileGlobalCellCache.java b/src/main/java/org/mastodon/mamut/io/img/cache/MastodonVolatileGlobalCellCache.java new file mode 100644 index 000000000..cd5b3d0b1 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/img/cache/MastodonVolatileGlobalCellCache.java @@ -0,0 +1,301 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2023 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.img.cache; + +import bdv.cache.SharedQueue; +import bdv.img.cache.CacheArrayLoader; +import bdv.img.cache.CreateInvalidVolatileCell; +import bdv.img.cache.EmptyArrayCreator; +import bdv.img.cache.SimpleCacheArrayLoader; +import bdv.img.cache.VolatileCachedCellImg; +import bdv.img.cache.VolatileGlobalCellCache; +import java.util.concurrent.Callable; + +import bdv.cache.CacheControl; +import net.imglib2.cache.Cache; +import net.imglib2.cache.CacheLoader; +import net.imglib2.cache.LoaderCache; +import net.imglib2.cache.queue.BlockingFetchQueues; +import net.imglib2.cache.ref.SoftRefLoaderCache; +import net.imglib2.cache.ref.WeakRefVolatileCache; +import net.imglib2.cache.util.KeyBimap; +import net.imglib2.cache.volatiles.CacheHints; +import net.imglib2.cache.volatiles.VolatileCache; +import net.imglib2.img.basictypeaccess.DataAccess; +import net.imglib2.img.cell.Cell; +import net.imglib2.img.cell.CellGrid; +import net.imglib2.type.NativeType; + +public class MastodonVolatileGlobalCellCache implements CacheControl +{ + /** + * Key for a cell identified by timepoint, setup, level, and index + * (flattened spatial coordinate). + */ + public static class Key + { + private final int timepoint; + + private final int setup; + + private final int level; + + private final long index; + + /** + * Create a Key for the specified cell. + * + * @param timepoint + * timepoint coordinate of the cell + * @param setup + * setup coordinate of the cell + * @param level + * level coordinate of the cell + * @param index + * index of the cell (flattened spatial coordinate of the + * cell) + */ + public Key( final int timepoint, final int setup, final int level, final long index ) + { + this.timepoint = timepoint; + this.setup = setup; + this.level = level; + this.index = index; + + int value = Long.hashCode( index ); + value = 31 * value + level; + value = 31 * value + setup; + value = 31 * value + timepoint; + hashcode = value; + } + + @Override + public boolean equals( final Object other ) + { + if ( this == other ) + return true; + if ( !( other instanceof VolatileGlobalCellCache.Key ) ) + return false; + final Key that = ( Key ) other; + return ( this.index == that.index ) && ( this.timepoint == that.timepoint ) && ( this.setup == that.setup ) + && ( this.level == that.level ); + } + + final int hashcode; + + @Override + public int hashCode() + { + return hashcode; + } + } + + private final BlockingFetchQueues< Callable< ? > > queue; + + protected final LoaderCache< Key, Cell< ? > > backingCache; + + /** + * Create a new global cache with a new fetch queue served by the specified + * number of fetcher threads. + * + * @param maxNumLevels + * the highest occurring mipmap level plus 1. + * @param numFetcherThreads + * how many threads should be created to load data. + */ + public MastodonVolatileGlobalCellCache( final int maxNumLevels, final int numFetcherThreads ) + { + queue = new SharedQueue( numFetcherThreads, maxNumLevels ); + backingCache = new SoftRefLoaderCache<>(); + } + + /** + * Create a new global cache with the specified fetch queue. (It is the + * callers responsibility to create fetcher threads that serve the queue.) + * + * @param queue + * queue to which asynchronous data loading jobs are submitted + */ + public MastodonVolatileGlobalCellCache( final BlockingFetchQueues< Callable< ? > > queue ) + { + this.queue = queue; + backingCache = new SoftRefLoaderCache<>(); + } + + /** + * Prepare the cache for providing data for the "next frame", + * by moving pending cell request to the prefetch queue + * ({@link BlockingFetchQueues#clearToPrefetch()}). + */ + @Override + public void prepareNextFrame() + { + queue.clearToPrefetch(); + } + + /** + * Remove all references to loaded data. + *
+ * Note that there may be pending cell requests which will re-populate the cache + * unless the fetch queue is cleared as well. + */ + public void clearCache() + { + backingCache.invalidateAll(); + } + + /** + * Create a {@link VolatileCachedCellImg} backed by this {@link VolatileGlobalCellCache}, + * using the provided {@link CacheArrayLoader} to load data. + * + * @param grid + * @param timepoint + * @param setup + * @param level + * @param cacheHints + * @param cacheArrayLoader + * @param type + * @return + */ + public < T extends NativeType< T >, A extends DataAccess > VolatileCachedCellImg< T, A > createImg( + final CellGrid grid, + final int timepoint, + final int setup, + final int level, + final CacheHints cacheHints, + final CacheArrayLoader< A > cacheArrayLoader, + final T type ) + { + final CacheLoader< Long, Cell< ? > > loader = key -> { + final int n = grid.numDimensions(); + final long[] cellMin = new long[ n ]; + final int[] cellDims = new int[ n ]; + grid.getCellDimensions( key, cellMin, cellDims ); + return new Cell<>( + cellDims, + cellMin, + cacheArrayLoader.loadArray( timepoint, setup, level, cellDims, cellMin ) ); + }; + return createImg( grid, timepoint, setup, level, cacheHints, loader, cacheArrayLoader.getEmptyArrayCreator(), type ); + } + + /** + * Create a {@link VolatileCachedCellImg} backed by this {@link VolatileGlobalCellCache}, + * using the provided {@link SimpleCacheArrayLoader} to load data. + * + * @param grid + * @param timepoint + * @param setup + * @param level + * @param cacheHints + * @param cacheArrayLoader + * @param type + * @return + */ + public < T extends NativeType< T >, A extends DataAccess > VolatileCachedCellImg< T, A > createImg( + final CellGrid grid, + final int timepoint, + final int setup, + final int level, + final CacheHints cacheHints, + final SimpleCacheArrayLoader< A > cacheArrayLoader, + final T type ) + { + final CacheLoader< Long, Cell< ? > > loader = key -> { + final int n = grid.numDimensions(); + final long[] cellMin = new long[ n ]; + final int[] cellDims = new int[ n ]; + final long[] cellGridPosition = new long[ n ]; + grid.getCellDimensions( key, cellMin, cellDims ); + grid.getCellGridPositionFlat( key, cellGridPosition ); + return new Cell<>( cellDims, cellMin, cacheArrayLoader.loadArray( cellGridPosition, cellDims ) ); + }; + return createImg( grid, timepoint, setup, level, cacheHints, loader, cacheArrayLoader.getEmptyArrayCreator(), type ); + } + + public < T extends NativeType< T >, A extends DataAccess > VolatileCachedCellImg< T, A > createImg( + final CellGrid grid, + final int timepoint, + final int setup, + final int level, + final CacheHints cacheHints, + final CacheLoader< Long, Cell< ? > > loader, + final EmptyArrayCreator< A > emptyArrayCreator, // optional, can be null + final T type ) + { + final KeyBimap< Long, Key > bimap = KeyBimap.build( + index -> new Key( timepoint, setup, level, index ), + key -> ( key.timepoint == timepoint && key.setup == setup && key.level == level ) + ? key.index + : null ); + + final Cache< Long, Cell< ? > > cache = backingCache + .mapKeys( bimap ) + .withLoader( loader ); + + final CreateInvalidVolatileCell< ? > createInvalid = ( emptyArrayCreator == null ) + ? CreateInvalidVolatileCell.get( grid, type, false ) + : new CreateInvalidVolatileCell<>( grid, type.getEntitiesPerPixel(), emptyArrayCreator ); + + final VolatileCache< Long, Cell< ? > > vcache = new WeakRefVolatileCache<>( cache, queue, createInvalid ); + + @SuppressWarnings( "unchecked" ) + final VolatileCachedCellImg< T, A > img = new VolatileCachedCellImg<>( grid, type, cacheHints, ( VolatileCache ) vcache ); + + return img; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/importer/graphml/GraphMLImporterPlugin.java b/src/main/java/org/mastodon/mamut/io/importer/graphml/GraphMLImporterPlugin.java index 900a1314c..96c2d8b83 100644 --- a/src/main/java/org/mastodon/mamut/io/importer/graphml/GraphMLImporterPlugin.java +++ b/src/main/java/org/mastodon/mamut/io/importer/graphml/GraphMLImporterPlugin.java @@ -33,7 +33,7 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -43,6 +43,7 @@ import org.mastodon.app.MastodonIcons; import org.mastodon.app.ui.ViewMenuBuilder; import org.mastodon.mamut.KeyConfigScopes; +import org.mastodon.mamut.MamutMenuBuilder; import org.mastodon.mamut.ProjectModel; import org.mastodon.mamut.plugin.MamutPlugin; import org.mastodon.ui.keymap.KeyConfigContexts; @@ -91,8 +92,8 @@ public Map< String, String > getMenuTexts() @Override public List< ViewMenuBuilder.MenuItem > getMenuItems() { - return Arrays.asList( - menu( "Plugins", menu( "Imports", item( IMPORT_GRAPHML ) ) ) ); + return Collections.singletonList( + MamutMenuBuilder.fileMenu( menu( "Import", item( IMPORT_GRAPHML ) ) ) ); } @Override diff --git a/src/main/java/org/mastodon/mamut/io/importer/trackmate/TrackMateImporter.java b/src/main/java/org/mastodon/mamut/io/importer/trackmate/TrackMateImporter.java index c69f5fd51..80d792dd6 100644 --- a/src/main/java/org/mastodon/mamut/io/importer/trackmate/TrackMateImporter.java +++ b/src/main/java/org/mastodon/mamut/io/importer/trackmate/TrackMateImporter.java @@ -424,6 +424,18 @@ private final class Import extends ModelImporter final int targetID = Integer.parseInt( edgeEl.getAttributeValue( EDGE_TARGET_ATTRIBUTE ) ); final Spot target = idToSpotIDmap.get( targetID, targetRef ); + if ( source == null ) + { + // TODO Echo warnings in a logger instead of sysout. + System.out.println( "Could not find spot with ID " + sourceID + " - skipping edge " + sourceID + " → " + targetID ); + continue; + } + if ( target == null ) + { + System.out.println( "Could not find spot with ID " + targetID + " - skipping edge " + sourceID + " → " + targetID ); + continue; + } + // Protect against link time inversion. final Link link; if ( source.getTimepoint() < target.getTimepoint() ) diff --git a/src/main/java/org/mastodon/mamut/io/loader/N5UniverseImgLoader.java b/src/main/java/org/mastodon/mamut/io/loader/N5UniverseImgLoader.java new file mode 100644 index 000000000..3fbbbec1b --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/N5UniverseImgLoader.java @@ -0,0 +1,499 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/* + * Original code from bigdataviewer-core: + * https://github.com/bigdataviewer/bigdataviewer-core/blob/c47e2370ae9b9e281444f127cb36cd9ef1497336/src/main/java/bdv/img/n5/N5ImageLoader.java + * LICENSE is shown below: + * + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2023 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.janelia.saalfeldlab.n5.DataType; +import org.janelia.saalfeldlab.n5.DatasetAttributes; +import org.janelia.saalfeldlab.n5.N5Exception; +import org.janelia.saalfeldlab.n5.N5KeyValueReader; +import org.janelia.saalfeldlab.n5.N5Reader; +import org.janelia.saalfeldlab.n5.hdf5.N5HDF5Reader; +import org.janelia.saalfeldlab.n5.universe.N5Factory; +import org.janelia.saalfeldlab.n5.zarr.ZarrCompressor; +import org.janelia.saalfeldlab.n5.zarr.ZarrKeyValueReader; +import org.mastodon.mamut.io.img.cache.MastodonSimpleCacheArrayLoader; +import org.mastodon.mamut.io.img.cache.MastodonVolatileGlobalCellCache; +import org.mastodon.mamut.io.loader.adapter.N5HDF5ReaderToViewerImgLoaderAdapter; +import org.mastodon.mamut.io.loader.adapter.N5KeyValueReaderToViewerImgLoaderAdapter; +import org.mastodon.mamut.io.loader.adapter.ZarrKeyValueReaderToViewerImgLoaderAdapter; +import org.mastodon.mamut.io.loader.adapter.N5ReaderToViewerImgLoaderAdapter; +import org.mastodon.mamut.io.loader.util.mobie.OmeZarrMultiscales; +import org.mastodon.mamut.io.loader.util.mobie.OmeZarrMultiscalesAdapter; +import org.mastodon.mamut.io.loader.util.mobie.ZarrAxes; +import org.mastodon.mamut.io.loader.util.mobie.ZarrAxesAdapter; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.google.gson.GsonBuilder; + +import bdv.AbstractViewerSetupImgLoader; +import bdv.ViewerImgLoader; +import bdv.cache.CacheControl; +import bdv.cache.SharedQueue; +import bdv.img.cache.SimpleCacheArrayLoader; +import bdv.img.n5.DataTypeProperties; +import bdv.util.ConstantRandomAccessible; +import bdv.util.MipmapTransforms; +import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription; +import mpicbg.spim.data.generic.sequence.BasicViewSetup; +import mpicbg.spim.data.generic.sequence.ImgLoaderHint; +import mpicbg.spim.data.sequence.MultiResolutionImgLoader; +import mpicbg.spim.data.sequence.MultiResolutionSetupImgLoader; +import mpicbg.spim.data.sequence.VoxelDimensions; +import net.imglib2.Dimensions; +import net.imglib2.FinalDimensions; +import net.imglib2.FinalInterval; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.Volatile; +import net.imglib2.cache.CacheLoader; +import net.imglib2.cache.volatiles.CacheHints; +import net.imglib2.cache.volatiles.LoadingStrategy; +import net.imglib2.img.basictypeaccess.DataAccess; +import net.imglib2.img.cell.Cell; +import net.imglib2.img.cell.CellGrid; +import net.imglib2.img.cell.CellImg; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.NativeType; +import net.imglib2.util.Cast; +import net.imglib2.view.Views; + +/** + * {@link ViewerImgLoader} for N5/OME-Zarr/HDF5 image data using {@link N5Reader}. + */ +public class N5UniverseImgLoader implements ViewerImgLoader, MultiResolutionImgLoader +{ + private N5Reader n5; + + private N5Factory factory; + + private final String url; + + public String getUrl() + { + return url; + } + + private final String dataset; + + public String getDataset() + { + return dataset; + } + + private N5ReaderToViewerImgLoaderAdapter< ? > adapter; + + // TODO: it would be good if this would not be needed + // find available setups from the n5 + private final AbstractSequenceDescription< ?, ?, ? > seq; + + /** + * Maps setup id to {@link SetupImgLoader}. + */ + private final Map< Integer, SetupImgLoader< ?, ? > > setupImgLoaders = new HashMap<>(); + + /** + * Create a new {@link N5UniverseImgLoader} for the given URI and dataset. + * @param uri + * @param dataset + * @param sequenceDescription + * @param s3Credentials use the {@link DefaultAWSCredentialsProviderChain} if null + */ + public N5UniverseImgLoader( final String uri, final String dataset, final AbstractSequenceDescription< ?, ?, ? > sequenceDescription, + final AWSCredentials s3Credentials ) + { + this( uri, dataset, sequenceDescription ); + if ( s3Credentials == null ) + { + this.factory = this.factory.s3UseCredentials(); + } + else + { + this.factory = this.factory.s3UseCredentials( s3Credentials ); + } + } + + public N5UniverseImgLoader( final String uri, final String dataset, final AbstractSequenceDescription< ?, ?, ? > sequenceDescription ) + { + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter( ZarrCompressor.class, ZarrCompressor.jsonAdapter ); + gsonBuilder.registerTypeAdapter( ZarrAxes.class, new ZarrAxesAdapter() ); + gsonBuilder.registerTypeAdapter( OmeZarrMultiscales.class, new OmeZarrMultiscalesAdapter() ); + this.factory = new N5Factory() + .cacheAttributes( true ) + .hdf5DefaultBlockSize( 64 ) + .zarrDimensionSeparator( "/" ) + .zarrMapN5Attributes( true ) + .gsonBuilder( gsonBuilder ); + this.url = uri; + this.dataset = dataset.endsWith( "/" ) ? dataset : dataset + "/"; + this.seq = sequenceDescription; + } + + public N5Reader getN5Reader() + { + return n5; + } + + private volatile boolean isOpen = false; + + private SharedQueue createdSharedQueue; + + private MastodonVolatileGlobalCellCache cache; + + private int requestedNumFetcherThreads = -1; + + private SharedQueue requestedSharedQueue; + + @Override + public synchronized void setNumFetcherThreads( final int n ) + { + requestedNumFetcherThreads = n; + } + + @Override + public void setCreatedSharedQueue( final SharedQueue createdSharedQueue ) + { + requestedSharedQueue = createdSharedQueue; + } + + public static N5ReaderToViewerImgLoaderAdapter< ? extends N5Reader > getAdapter( final N5Reader n5, final String dataset, + final AbstractSequenceDescription< ?, ?, ? > seq ) + { + if ( n5 instanceof N5HDF5Reader ) + { + return new N5HDF5ReaderToViewerImgLoaderAdapter( ( N5HDF5Reader ) n5, dataset ); + } + else if ( n5 instanceof ZarrKeyValueReader ) + { + return new ZarrKeyValueReaderToViewerImgLoaderAdapter( ( ZarrKeyValueReader ) n5, dataset ); + } + else if ( n5 instanceof N5KeyValueReader ) + { + return new N5KeyValueReaderToViewerImgLoaderAdapter( ( N5KeyValueReader ) n5, dataset ); + } + else + { + throw new UnsupportedOperationException( "Unsupported format'" ); + } + } + + /** + * Validate if the n5 is readable. + */ + public boolean validate() + { + try + { + open(); + return true; + } + catch ( Throwable e ) + { + return false; + } + } + + private void open() + { + if ( !isOpen ) + { + synchronized ( this ) + { + if ( isOpen ) + return; + + try + { + this.n5 = factory.openReader( url ); + this.adapter = getAdapter( n5, dataset, seq ); + int maxNumLevels = 0; + final List< ? extends BasicViewSetup > setups = seq.getViewSetupsOrdered(); + for ( final BasicViewSetup setup : setups ) + { + final int setupId = setup.getId(); + final SetupImgLoader< ?, ? > setupImgLoader = createSetupImgLoader( setupId ); + setupImgLoaders.put( setupId, setupImgLoader ); + maxNumLevels = Math.max( maxNumLevels, setupImgLoader.numMipmapLevels() ); + } + + final int numFetcherThreads = requestedNumFetcherThreads >= 0 + ? requestedNumFetcherThreads + : Math.max( 1, Runtime.getRuntime().availableProcessors() ); + final SharedQueue queue = requestedSharedQueue != null + ? requestedSharedQueue + : ( createdSharedQueue = new SharedQueue( numFetcherThreads, maxNumLevels ) ); + cache = new MastodonVolatileGlobalCellCache( queue ); + } + catch ( final IOException e ) + { + throw new RuntimeException( e ); + } + + isOpen = true; + } + } + } + + /** + * Clear the cache. Images that were obtained from + * this loader before {@link #close()} will stop working. Requesting images + * after {@link #close()} will cause the n5 to be reopened (with a + * new cache). + */ + public void close() + { + if ( isOpen ) + { + synchronized ( this ) + { + if ( !isOpen ) + return; + + if ( createdSharedQueue != null ) + createdSharedQueue.shutdown(); + cache.clearCache(); + + createdSharedQueue = null; + isOpen = false; + } + } + } + + @Override + public SetupImgLoader< ?, ? > getSetupImgLoader( final int setupId ) + { + open(); + return setupImgLoaders.get( setupId ); + } + + private < T extends NativeType< T >, V extends Volatile< T > & NativeType< V > > SetupImgLoader< T, V > + createSetupImgLoader( final int setupId ) throws IOException + { + DataType dataType = adapter.getSetupDataType( setupId ); + return new SetupImgLoader<>( setupId, Cast.unchecked( DataTypeProperties.of( dataType ) ) ); + } + + @Override + public CacheControl getCacheControl() + { + open(); + return cache; + } + + public class SetupImgLoader< T extends NativeType< T >, V extends Volatile< T > & NativeType< V > > + extends AbstractViewerSetupImgLoader< T, V > + implements MultiResolutionSetupImgLoader< T > + { + private final int setupId; + + private final double[][] mipmapResolutions; + + private final AffineTransform3D[] mipmapTransforms; + + public SetupImgLoader( final int setupId, final DataTypeProperties< T, V, ?, ? > props ) throws IOException + { + this( setupId, props.type(), props.volatileType() ); + } + + public SetupImgLoader( final int setupId, final T type, final V volatileType ) throws IOException + { + super( type, volatileType ); + this.setupId = setupId; + mipmapResolutions = adapter.getMipmapResolutions( setupId ); + mipmapTransforms = new AffineTransform3D[ mipmapResolutions.length ]; + for ( int level = 0; level < mipmapResolutions.length; level++ ) + mipmapTransforms[ level ] = MipmapTransforms.getMipmapTransformDefault( mipmapResolutions[ level ] ); + } + + @Override + public RandomAccessibleInterval< V > getVolatileImage( final int timepointId, final int level, final ImgLoaderHint... hints ) + { + return prepareCachedImage( timepointId, level, LoadingStrategy.BUDGETED, volatileType ); + } + + @Override + public RandomAccessibleInterval< T > getImage( final int timepointId, final int level, final ImgLoaderHint... hints ) + { + return prepareCachedImage( timepointId, level, LoadingStrategy.BLOCKING, type ); + } + + @Override + public Dimensions getImageSize( final int timepointId, final int level ) + { + try + { + final String pathName = getFullPathName( adapter.getPathNameFromSetupTimepointLevel( setupId, timepointId, level ) ); + final DatasetAttributes attributes = n5.getDatasetAttributes( pathName ); + return new FinalDimensions( attributes.getDimensions() ); + } + catch ( final RuntimeException e ) + { + return null; + } + } + + @Override + public double[][] getMipmapResolutions() + { + return mipmapResolutions; + } + + @Override + public AffineTransform3D[] getMipmapTransforms() + { + return mipmapTransforms; + } + + @Override + public int numMipmapLevels() + { + return mipmapResolutions.length; + } + + @Override + public VoxelDimensions getVoxelSize( final int timepointId ) + { + return null; + } + + /** + * Create a {@link CellImg} backed by the cache. + */ + private < K extends NativeType< K > > RandomAccessibleInterval< K > prepareCachedImage( final int timepointId, final int level, + final LoadingStrategy loadingStrategy, final K type ) + { + try + { + final long[] dimensions = adapter.getDimensions( setupId, timepointId, level ); + final int[] cellDimensions = adapter.getCellDimensions( setupId, timepointId, level ); + final CellGrid grid = new CellGrid( dimensions, cellDimensions ); + final int n = grid.numDimensions(); + + final int priority = numMipmapLevels() - 1 - level; + final CacheHints cacheHints = new CacheHints( loadingStrategy, priority, false ); + + final SimpleCacheArrayLoader< ? > cacheArrayLoader = + adapter.createCacheArrayLoader( setupId, timepointId, level, grid ); + final CacheLoader< Long, Cell< ? > > loader = key -> { + final long[] cellGridMin = new long[ n ]; + final int[] cellGridDims = new int[ n ]; + final long[] cellGridPosition = new long[ n ]; + grid.getCellDimensions( key, cellGridMin, cellGridDims ); + grid.getCellGridPositionFlat( key, cellGridPosition ); + final long[] cellMin = Arrays.copyOf( cellGridMin, 3 ); + final int[] cellDims = Arrays.copyOf( cellGridDims, 3 ); + if ( n < 3 ) + { + Arrays.fill( cellDims, n, 3, 1 ); + } + DataAccess dataAccess = null; + if ( adapter instanceof N5ReaderToViewerImgLoaderAdapter && n == 4 ) + { + dataAccess = ( ( MastodonSimpleCacheArrayLoader< ? > ) cacheArrayLoader ).loadArrayAtTimepoint( cellGridPosition, + cellDims, timepointId ); + } + else + { + dataAccess = cacheArrayLoader.loadArray( cellGridPosition, cellDims ); + } + return new Cell<>( cellDims, cellMin, dataAccess ); + }; + final long[] imgDimensions = Arrays.copyOf( dimensions, 3 ); + if ( n < 3 ) + { + Arrays.fill( imgDimensions, n, 3, 1 ); + } + final int[] imgCellDimensions = Arrays.copyOf( cellDimensions, 3 ); + if ( n < 3 ) + { + Arrays.fill( imgCellDimensions, n, 3, 1 ); + } + final CellGrid imgGrid = new CellGrid( imgDimensions, imgCellDimensions ); + return cache.createImg( imgGrid, timepointId, setupId, level, cacheHints, loader, cacheArrayLoader.getEmptyArrayCreator(), + type ); + } + catch ( final IOException | N5Exception e ) + { + System.err.println( String.format( + "image data for timepoint %d setup %d level %d could not be found.", + timepointId, setupId, level ) ); + return Views.interval( + new ConstantRandomAccessible<>( type.createVariable(), 3 ), + new FinalInterval( 1, 1, 1 ) ); + } + } + } + + private String getFullPathName( final String pathName ) + { + String fullPathName = dataset + pathName; + while ( fullPathName.contains( "//" ) ) + { + fullPathName = fullPathName.replace( "//", "/" ); + } + return fullPathName; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/XmlIoN5UniverseImgLoader.java b/src/main/java/org/mastodon/mamut/io/loader/XmlIoN5UniverseImgLoader.java new file mode 100644 index 000000000..50c04b0c8 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/XmlIoN5UniverseImgLoader.java @@ -0,0 +1,203 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/* + * Original code from bigdataviewer-core: + * https://github.com/bigdataviewer/bigdataviewer-core/blob/c47e2370ae9b9e281444f127cb36cd9ef1497336/src/main/java/bdv/img/n5/N5ImageLoader.java + * LICENSE is shown below: + * + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2023 BigDataViewer developers. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import org.janelia.saalfeldlab.n5.N5URI; +import org.jdom2.Element; +import org.mastodon.mamut.io.loader.util.credentials.AWSCredentialsManager; +import org.mastodon.mamut.io.loader.util.credentials.AWSCredentialsTools; + +import mpicbg.spim.data.XmlHelpers; +import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription; +import mpicbg.spim.data.generic.sequence.ImgLoaderIo; +import mpicbg.spim.data.generic.sequence.XmlIoBasicImgLoader; + +import static mpicbg.spim.data.XmlKeys.IMGLOADER_FORMAT_ATTRIBUTE_NAME; + +@ImgLoaderIo( format = "bdv.n5.universe", type = N5UniverseImgLoader.class ) +public class XmlIoN5UniverseImgLoader implements XmlIoBasicImgLoader< N5UniverseImgLoader > +{ + public static final String URL = "Url"; + + public static final String DATASET = "Dataset"; + + @Override + public Element toXml( N5UniverseImgLoader imgLoader, File basePath ) + { + final Element elem = new Element( "ImageLoader" ); + elem.setAttribute( IMGLOADER_FORMAT_ATTRIBUTE_NAME, "bdv.n5.universe" ); + elem.setAttribute( "version", "1.0" ); + String url = imgLoader.getUrl(); + String scheme = getScheme( url ); + if ( scheme == null ) + { + // url is a path + elem.addContent( XmlHelpers.pathElement( URL, new File( url ), basePath ) ); + } + else if ( scheme.equals( "file" ) ) + { + // remove "file:" prefix + elem.addContent( XmlHelpers.pathElement( URL, new File( url.substring( "file:".length() ) ), basePath ) ); + } + else + { + elem.addContent( XmlHelpers.textElement( URL, imgLoader.getUrl() ) ); + } + elem.addContent( XmlHelpers.textElement( DATASET, imgLoader.getDataset() ) ); + return elem; + } + + @Override + public N5UniverseImgLoader fromXml( Element elem, File basePath, AbstractSequenceDescription< ?, ?, ? > sequenceDescription ) + { + String url = XmlHelpers.getText( elem, URL ); + if ( getScheme( url ) == null ) + { + url = XmlHelpers.loadPath( elem, URL, basePath ).toString(); + } + final String dataset = XmlHelpers.getText( elem, DATASET ); + try + { + N5UniverseImgLoader imgLoader; + if ( AWSCredentialsManager.getInstance().getCredentials() != null ) + { + try + { + // use stored BasicAWSCredentials + imgLoader = new N5UniverseImgLoader( url, dataset, sequenceDescription, + AWSCredentialsManager.getInstance().getCredentials() ); + if ( imgLoader.validate() ) + { + return imgLoader; + } + } + catch ( final Throwable e ) + {} + AWSCredentialsManager.getInstance().setCredentials( null ); + } + // try anonymous access + imgLoader = new N5UniverseImgLoader( url, dataset, sequenceDescription ); + if ( imgLoader.validate() ) + { + return imgLoader; + } + // use DefaultAWSCredentialsProviderChain + imgLoader = new N5UniverseImgLoader( url, dataset, sequenceDescription, null ); + if ( imgLoader.validate() ) + { + return imgLoader; + } + else + { + // use BasicAWSCredentials + if ( AWSCredentialsManager.getInstance().getCredentials() == null ) + AWSCredentialsManager.getInstance().setCredentials( AWSCredentialsTools.getBasicAWSCredentials() ); + if ( AWSCredentialsManager.getInstance().getCredentials() == null ) + throw new RuntimeException( "No credentials provided" ); + imgLoader = + new N5UniverseImgLoader( url, dataset, sequenceDescription, AWSCredentialsManager.getInstance().getCredentials() ); + if ( imgLoader.validate() ) + { + return imgLoader; + } + else + { + // for some reason, credentials are not valid + AWSCredentialsManager.getInstance().setCredentials( AWSCredentialsTools.getBasicAWSCredentials() ); + if ( AWSCredentialsManager.getInstance().getCredentials() == null ) + throw new RuntimeException( "No credentials provided" ); + imgLoader = new N5UniverseImgLoader( url, dataset, sequenceDescription, + AWSCredentialsManager.getInstance().getCredentials() ); + if ( imgLoader.validate() ) + { + return imgLoader; + } + else + { + throw new RuntimeException( "Could not create N5UniverseImgLoader with credentials" ); + } + } + } + } + catch ( final Throwable e ) + { + AWSCredentialsManager.getInstance().setCredentials( null ); + throw new RuntimeException( e ); + } + } + + private static String getScheme( final String url ) + { + String scheme = null; + try + { + final URI encodedUri = N5URI.encodeAsUri( url ); + scheme = encodedUri.getScheme(); + } + catch ( URISyntaxException ignored ) + {} + return scheme; + } + +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/adapter/N5HDF5ReaderToViewerImgLoaderAdapter.java b/src/main/java/org/mastodon/mamut/io/loader/adapter/N5HDF5ReaderToViewerImgLoaderAdapter.java new file mode 100644 index 000000000..91dadd276 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/adapter/N5HDF5ReaderToViewerImgLoaderAdapter.java @@ -0,0 +1,165 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.adapter; + +import org.janelia.saalfeldlab.n5.hdf5.N5HDF5Reader; +import org.janelia.saalfeldlab.n5.universe.metadata.SpatialMultiscaleMetadata; +import org.mastodon.mamut.io.img.cache.MastodonSimpleCacheArrayLoader; +import org.mastodon.mamut.io.loader.util.mobie.N5CacheArrayLoader; + +import bdv.img.n5.BdvN5Format; +import bdv.img.n5.DataTypeProperties; + +import java.io.IOException; + +import org.janelia.saalfeldlab.n5.DataType; +import org.janelia.saalfeldlab.n5.DatasetAttributes; +import org.janelia.saalfeldlab.n5.N5Exception; +import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; +import ch.systemsx.cisd.hdf5.HDF5Factory; +import ch.systemsx.cisd.hdf5.IHDF5Reader; +import hdf.hdf5lib.exceptions.HDF5Exception; +import net.imglib2.img.cell.CellGrid; + +public class N5HDF5ReaderToViewerImgLoaderAdapter implements N5ReaderToViewerImgLoaderAdapter< N5HDF5Reader > +{ + + private final N5HDF5Reader n5; + + private final String dataset; + + private static IHDF5Reader openHdf5Reader( String hdf5Path ) + { + try + { + return HDF5Factory.openForReading( hdf5Path ); + } + catch ( HDF5Exception e ) + { + throw new N5IOException( "Cannot open HDF5 Reader", new IOException( e ) ); + } + } + + public N5HDF5ReaderToViewerImgLoaderAdapter( final N5HDF5Reader n5, final String dataset ) + { + this.n5 = n5; + this.dataset = dataset; + } + + @Override + public String getDataset() + { + return dataset; + } + + @Override + public N5HDF5Reader getN5Reader() + { + return n5; + } + + @Override + public DataType getSetupDataType( int setupId ) throws IOException + { + DataType dataType = null; + try + { + final String pathName = getFullPathName( getPathNameFromSetup( setupId ) ); + dataType = n5.getAttribute( pathName, BdvN5Format.DATA_TYPE_KEY, DataType.class ); + } + catch ( final N5Exception e ) + { + dataType = null; + } + return dataType == null ? DataType.UINT16 : dataType; + } + + @Override + public double[][] getMipmapResolutions( int setupId ) throws IOException + { + final String pathName = getFullPathName( getPathNameFromSetup( setupId ) ) + "/resolutions"; + final IHDF5Reader reader = openHdf5Reader( ( ( N5HDF5Reader ) n5 ).getFilename().toString() ); + final double[][] mipmapResolutions = reader.readDoubleMatrix( pathName ); + return mipmapResolutions; + } + + @Override + public long[] getDimensions( int setupId, int timepointId, int level ) + { + final String pathName = getFullPathName( getPathNameFromSetupTimepointLevel( setupId, timepointId, level ) ); + final DatasetAttributes attributes = n5.getDatasetAttributes( pathName ); + return attributes.getDimensions(); + } + + @Override + public int[] getCellDimensions( int setupId, int timepointId, int level ) + { + final String pathName = getFullPathName( getPathNameFromSetupTimepointLevel( setupId, timepointId, level ) ); + final DatasetAttributes attributes = n5.getDatasetAttributes( pathName ); + return attributes.getBlockSize(); + } + + @Override + public MastodonSimpleCacheArrayLoader< ? > createCacheArrayLoader( int setupId, int timepointId, int level, CellGrid grid ) + throws IOException + { + final String pathName = getFullPathName( getPathNameFromSetupTimepointLevel( setupId, timepointId, level ) ); + final DatasetAttributes attributes; + try + { + attributes = n5.getDatasetAttributes( pathName ); + } + catch ( final N5Exception e ) + { + throw new IOException( e ); + } + return new N5CacheArrayLoader<>( n5, pathName, attributes, DataTypeProperties.of( attributes.getDataType() ) ); + } + + public String getPathNameFromSetup( int setupId ) + { + return String.format( "s%02d", setupId ); + } + + public String getPathNameFromSetupTimepoint( int setupId, int timepointId ) + { + return String.format( "t%05d/s%02d", timepointId, setupId ); + } + + public String getPathNameFromSetupTimepointLevel( int setupId, int timepointId, int level ) + { + return String.format( "t%05d/s%02d/%d/cells", timepointId, setupId, level ); + } + + @Override + public SpatialMultiscaleMetadata< ? > getMetadata() + { + throw new UnsupportedOperationException( "Not supported" ); + } +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/adapter/N5KeyValueReaderToViewerImgLoaderAdapter.java b/src/main/java/org/mastodon/mamut/io/loader/adapter/N5KeyValueReaderToViewerImgLoaderAdapter.java new file mode 100644 index 000000000..cca04bd9a --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/adapter/N5KeyValueReaderToViewerImgLoaderAdapter.java @@ -0,0 +1,211 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.adapter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +import org.janelia.saalfeldlab.n5.DataType; +import org.janelia.saalfeldlab.n5.DatasetAttributes; +import org.janelia.saalfeldlab.n5.N5Exception; +import org.janelia.saalfeldlab.n5.N5KeyValueReader; +import org.janelia.saalfeldlab.n5.N5Reader; +import org.janelia.saalfeldlab.n5.bdv.N5ViewerCreator; +import org.janelia.saalfeldlab.n5.metadata.N5ViewerMultichannelMetadata; +import org.janelia.saalfeldlab.n5.metadata.imagej.N5ImagePlusMetadata; +import org.janelia.saalfeldlab.n5.universe.N5DatasetDiscoverer; +import org.janelia.saalfeldlab.n5.universe.N5TreeNode; +import org.janelia.saalfeldlab.n5.universe.metadata.MultiscaleMetadata; +import org.janelia.saalfeldlab.n5.universe.metadata.N5CosemMetadata; +import org.janelia.saalfeldlab.n5.universe.metadata.N5Metadata; +import org.mastodon.mamut.io.loader.util.mobie.N5CacheArrayLoader; + +import bdv.img.cache.SimpleCacheArrayLoader; +import bdv.img.n5.DataTypeProperties; +import net.imglib2.img.cell.CellGrid; + +public class N5KeyValueReaderToViewerImgLoaderAdapter implements N5ReaderToViewerImgLoaderAdapter< N5KeyValueReader > +{ + + private final N5KeyValueReader n5; + + private final String dataset; + + private final N5ViewerMultichannelMetadata metadata; + + private final Map< Integer, List< DatasetAttributes > > setupToAttributesList = new HashMap<>(); + + public N5KeyValueReaderToViewerImgLoaderAdapter( final N5KeyValueReader n5, final String dataset ) + { + this.n5 = n5; + this.dataset = dataset; + this.metadata = getMetadata( n5 ); + for ( int i = 0; i < metadata.getChildrenMetadata().length; i++ ) + { + MultiscaleMetadata< ? > setupMetadata = metadata.getChildrenMetadata()[ i ]; + final List< DatasetAttributes > attributesList = Arrays.stream( setupMetadata.getChildrenMetadata() ) + .map( levelMetadata -> levelMetadata.getAttributes() ) + .collect( Collectors.toList() ); + setupToAttributesList.put( i, attributesList ); + } + } + + @Override + public String getDataset() + { + return dataset; + } + + @Override + public N5KeyValueReader getN5Reader() + { + return n5; + } + + @Override + public DataType getSetupDataType( int setupId ) throws IOException + { + return setupToAttributesList.get( setupId ).get( 0 ).getDataType(); + } + + @Override + public double[][] getMipmapResolutions( int setupId ) throws IOException + { + final List< DatasetAttributes > attributesList = setupToAttributesList.get( setupId ); + if ( attributesList == null || attributesList.isEmpty() ) + return null; + double[][] mipmapResolutions = new double[ attributesList.size() ][]; + long[] dimensionsOfLevel0 = attributesList.get( 0 ).getDimensions(); + for ( int level = 0; level < attributesList.size(); level++ ) + { + long[] dimensions = attributesList.get( level ).getDimensions(); + mipmapResolutions[ level ] = new double[ 3 ]; + for ( int d = 0; d < 2; d++ ) + { + mipmapResolutions[ level ][ d ] = Math.round( 1.0 * dimensionsOfLevel0[ d ] / dimensions[ d ] ); + } + mipmapResolutions[ level ][ 2 ] = + attributesList.get( level ).getNumDimensions() == 3 ? Math.round( 1.0 * dimensionsOfLevel0[ 2 ] / dimensions[ 2 ] ) + : 1.0; + } + return mipmapResolutions; + } + + @Override + public long[] getDimensions( int setupId, int timepointId, int level ) + { + final DatasetAttributes attributes = setupToAttributesList.get( setupId ).get( level ); + return attributes.getDimensions(); + } + + @Override + public int[] getCellDimensions( int setupId, int timepointId, int level ) + { + final DatasetAttributes attributes = setupToAttributesList.get( setupId ).get( level ); + return attributes.getBlockSize(); + } + + @Override + public String getPathNameFromSetupTimepointLevel( int setupId, int timepointId, int level ) + { + return String.format( "c%d/s%d", setupId, level ); + } + + @Override + public SimpleCacheArrayLoader< ? > createCacheArrayLoader( int setupId, int timepointId, int level, CellGrid grid ) + throws IOException + { + String pathName = getFullPathName( String.format( "c%d/s%d", setupId, level ) ); + DatasetAttributes attributes = setupToAttributesList.get( setupId ).get( level ); + return new N5CacheArrayLoader<>( n5, pathName, attributes, DataTypeProperties.of( attributes.getDataType() ) ); + } + + private static boolean isSupportedMetadata( final N5Metadata meta ) + { + if ( meta instanceof N5ImagePlusMetadata || + meta instanceof N5CosemMetadata || + meta instanceof N5ViewerMultichannelMetadata ) + return true; + return false; + } + + private static N5Metadata getMetadataRecursively( final N5TreeNode node ) + { + N5Metadata meta = node.getMetadata(); + if ( isSupportedMetadata( meta ) ) + { + return meta; + } + for ( final N5TreeNode child : node.childrenList() ) + { + meta = getMetadataRecursively( child ); + if ( meta != null ) + { + return meta; + } + } + return null; + } + + private static N5ViewerMultichannelMetadata getMetadata( final N5Reader n5 ) + { + N5TreeNode node = null; + final N5DatasetDiscoverer discoverer = new N5DatasetDiscoverer( + n5, + Executors.newCachedThreadPool(), + Arrays.asList( N5ViewerCreator.n5vParsers ), + Arrays.asList( N5ViewerCreator.n5vGroupParsers ) + ); + try + { + node = discoverer.discoverAndParseRecursive( "" ); + } + catch ( final IOException e ) + {} + if ( node == null ) + return null; + N5Metadata meta = getMetadataRecursively( node ); + if ( isSupportedMetadata( meta ) ) + return ( N5ViewerMultichannelMetadata ) meta; + else + throw new N5Exception( "No N5ViewerMultichannelMetadata found" ); + } + + @Override + public N5ViewerMultichannelMetadata getMetadata() + { + return metadata; + } + +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/adapter/N5ReaderToViewerImgLoaderAdapter.java b/src/main/java/org/mastodon/mamut/io/loader/adapter/N5ReaderToViewerImgLoaderAdapter.java new file mode 100644 index 000000000..150befeab --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/adapter/N5ReaderToViewerImgLoaderAdapter.java @@ -0,0 +1,83 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.adapter; + +import java.io.IOException; +import org.janelia.saalfeldlab.n5.DataType; +import org.janelia.saalfeldlab.n5.N5Reader; +import org.janelia.saalfeldlab.n5.universe.metadata.N5Metadata; +import bdv.img.cache.SimpleCacheArrayLoader; +import net.imglib2.img.cell.CellGrid; + +public interface N5ReaderToViewerImgLoaderAdapter< T extends N5Reader > +{ + + String getDataset(); + + T getN5Reader(); + + DataType getSetupDataType( final int setupId ) throws IOException; + + double[][] getMipmapResolutions( final int setupId ) throws IOException; + + long[] getDimensions( int setupId, int timepointId, int level ); + + int[] getCellDimensions( int setupId, int timepointId, int level ); + + String getPathNameFromSetupTimepointLevel( int setupId, int timepointId, int level ); + + SimpleCacheArrayLoader< ? > createCacheArrayLoader( int setupId, int timepointId, int level, CellGrid grid ) + throws IOException; + + N5Metadata getMetadata(); + + default int getNumSetups() + { + int numSetups = 0; + while ( true ) + { + try + { + if ( getMipmapResolutions( numSetups ) == null ) + break; + } + catch ( IOException e ) + { + break; + } + numSetups++; + } + return numSetups; + } + + default String getFullPathName( final String pathName ) + { + return ( getDataset() + pathName ).replaceAll( "/+", "/" ); + } +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/adapter/ZarrKeyValueReaderToViewerImgLoaderAdapter.java b/src/main/java/org/mastodon/mamut/io/loader/adapter/ZarrKeyValueReaderToViewerImgLoaderAdapter.java new file mode 100644 index 000000000..3472becb0 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/adapter/ZarrKeyValueReaderToViewerImgLoaderAdapter.java @@ -0,0 +1,308 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.adapter; + +import static org.mastodon.mamut.io.loader.util.mobie.OmeZarrMultiscales.MULTI_SCALE_KEY; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executors; + +import org.janelia.saalfeldlab.n5.DataType; +import org.janelia.saalfeldlab.n5.DatasetAttributes; +import org.janelia.saalfeldlab.n5.N5Exception; +import org.janelia.saalfeldlab.n5.universe.N5DatasetDiscoverer; +import org.janelia.saalfeldlab.n5.universe.N5TreeNode; +import org.janelia.saalfeldlab.n5.universe.metadata.N5GenericSingleScaleMetadataParser; +import org.janelia.saalfeldlab.n5.universe.metadata.N5Metadata; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMetadata; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMetadataParser; +import org.janelia.saalfeldlab.n5.zarr.ZarrKeyValueReader; +import org.mastodon.mamut.io.loader.util.mobie.N5OMEZarrCacheArrayLoader; +import org.mastodon.mamut.io.loader.util.mobie.OmeZarrMultiscales; +import org.mastodon.mamut.io.loader.util.mobie.ZarrAxes; + +import bdv.img.cache.SimpleCacheArrayLoader; +import bdv.img.n5.BdvN5Format; +import net.imglib2.Dimensions; +import net.imglib2.FinalDimensions; +import net.imglib2.img.cell.CellGrid; + +public class ZarrKeyValueReaderToViewerImgLoaderAdapter implements N5ReaderToViewerImgLoaderAdapter< ZarrKeyValueReader > +{ + + private final ZarrKeyValueReader n5; + + private final String dataset; + + // Used for ZarrKeyValueReader + private final Map< Integer, String > setupToPathname = new HashMap<>(); + + private final Map< Integer, OmeZarrMultiscales > setupToMultiscale = new HashMap<>(); + + private final Map< Integer, DatasetAttributes > setupToAttributes = new HashMap<>(); + + private final Map< Integer, Integer > setupToChannel = new HashMap<>(); + + /** + * https://github.com/mobie/mobie-io/blob/0216a25b2fa6f4e3fd7f9f6976de278b8eaa1b76/src/main/java/org/embl/mobie/io/ome/zarr/loaders/N5OMEZarrImageLoader.java#L422 + * @param setupId + * @param attributes + * @return + */ + private Dimensions getSpatialDimensions( int setupId, DatasetAttributes attributes ) + { + final long[] spatialDimensions = new long[ 3 ]; + long[] attributeDimensions = attributes.getDimensions(); + Arrays.fill( spatialDimensions, 1 ); + ZarrAxes zarrAxes = setupToMultiscale.get( setupId ).axes; + final Map< Integer, Integer > spatialToZarr = zarrAxes.spatialToZarr(); + for ( Map.Entry< Integer, Integer > entry : spatialToZarr.entrySet() ) + { + spatialDimensions[ entry.getKey() ] = attributeDimensions[ entry.getValue() ]; + } + return new FinalDimensions( spatialDimensions ); + } + + /** + * https://github.com/mobie/mobie-io/blob/0216a25b2fa6f4e3fd7f9f6976de278b8eaa1b76/src/main/java/org/embl/mobie/io/ome/zarr/loaders/N5OMEZarrImageLoader.java#L529 + * @param attributes + * @return + */ + private int[] fillBlockSize( DatasetAttributes attributes ) + { + int[] tmp = new int[ 3 ]; + tmp[ 0 ] = Arrays.stream( attributes.getBlockSize() ).toArray()[ 0 ]; + tmp[ 1 ] = Arrays.stream( attributes.getBlockSize() ).toArray()[ 1 ]; + tmp[ 2 ] = 1; + return tmp; + } + + /** + * https://github.com/mobie/mobie-io/blob/0216a25b2fa6f4e3fd7f9f6976de278b8eaa1b76/src/main/java/org/embl/mobie/io/ome/zarr/loaders/N5OMEZarrImageLoader.java#L520 + * @param setupId + * @param attributes + * @return + */ + private int[] getBlockSize( int setupId, DatasetAttributes attributes ) + { + ZarrAxes zarrAxes = setupToMultiscale.get( setupId ).axes; + if ( !zarrAxes.hasZAxis() ) + { + return fillBlockSize( attributes ); + } + else + { + return Arrays.stream( attributes.getBlockSize() ).limit( 3 ).toArray(); + } + } + + /** + * @param setupId + * @param level + * @return + */ + private String getPathNameLevel( int setupId, int level ) + { + return setupToPathname.get( setupId ) + "/" + setupToMultiscale.get( setupId ).datasets[ level ].path; + } + + public ZarrKeyValueReaderToViewerImgLoaderAdapter( final ZarrKeyValueReader n5, final String dataset ) + { + this.n5 = n5; + this.dataset = dataset; + int setupId = -1; + final OmeZarrMultiscales[] multiscales = n5.getAttribute( dataset, MULTI_SCALE_KEY, OmeZarrMultiscales[].class ); + for ( OmeZarrMultiscales multiscale : multiscales ) + { + final DatasetAttributes attributes = n5.getDatasetAttributes( dataset + multiscale.datasets[ 0 ].path ); + long nC = 1; + if ( multiscale.axes.hasChannels() ) + { + nC = attributes.getDimensions()[ multiscale.axes.channelIndex() ]; + } + + for ( int c = 0; c < nC; c++ ) + { + // each channel is one setup + setupId++; + setupToChannel.put( setupId, c ); + + // all channels have the same multiscale and attributes + setupToMultiscale.put( setupId, multiscale ); + setupToAttributes.put( setupId, attributes ); + setupToPathname.put( setupId, "" ); + } + } + } + + @Override + public String getDataset() + { + return dataset; + } + + @Override + public ZarrKeyValueReader getN5Reader() + { + return n5; + } + + @Override + public DataType getSetupDataType( int setupId ) throws IOException + { + DataType dataType = null; + try + { + final String pathName = getFullPathName( setupToPathname.get( setupId ) ); + dataType = n5.getAttribute( pathName, BdvN5Format.DATA_TYPE_KEY, DataType.class ); + } + catch ( final N5Exception e ) + { + throw new IOException( e ); + } + return dataType == null ? DataType.UINT16 : dataType; + } + + @Override + public double[][] getMipmapResolutions( int setupId ) throws IOException + { + double[][] mipmapResolutions = null; + try + { + OmeZarrMultiscales multiscale = setupToMultiscale.get( setupId ); + if ( multiscale == null ) + { + throw new IOException( "Multiscale not found for setup " + setupId ); + } + mipmapResolutions = new double[ multiscale.datasets.length ][]; + + long[] dimensionsOfLevel0 = setupToAttributes.get( setupId ).getDimensions(); + mipmapResolutions[ 0 ] = new double[] { 1.0, 1.0, 1.0 }; + + for ( int level = 1; level < mipmapResolutions.length; level++ ) + { + long[] dimensions = n5.getDatasetAttributes( getFullPathName( getPathNameLevel( setupId, level ) ) ).getDimensions(); + mipmapResolutions[ level ] = new double[ 3 ]; + for ( int d = 0; d < 2; d++ ) + { + mipmapResolutions[ level ][ d ] = Math.round( 1.0 * dimensionsOfLevel0[ d ] / dimensions[ d ] ); + } + mipmapResolutions[ level ][ 2 ] = + multiscale.axes.hasZAxis() ? Math.round( 1.0 * dimensionsOfLevel0[ 2 ] / dimensions[ 2 ] ) : 1.0; + } + } + catch ( final N5Exception e ) + { + throw new IOException( e ); + } + return mipmapResolutions; + } + + private DatasetAttributes getDatasetAttributes( int setupId, int level ) throws N5Exception + { + return n5.getDatasetAttributes( getFullPathName( getPathNameLevel( setupId, level ) ) ); + } + + @Override + public long[] getDimensions( int setupId, int timepointId, int level ) + { + final DatasetAttributes attributes = getDatasetAttributes( setupId, level ); + return getSpatialDimensions( setupId, attributes ).dimensionsAsLongArray(); + } + + @Override + public int[] getCellDimensions( int setupId, int timepointId, int level ) + { + final DatasetAttributes attributes = getDatasetAttributes( setupId, level ); + return getBlockSize( setupId, attributes ); + } + + @Override + public SimpleCacheArrayLoader< ? > createCacheArrayLoader( int setupId, int timepointId, int level, CellGrid grid ) + throws IOException + { + final DatasetAttributes attributes; + try + { + attributes = getDatasetAttributes( setupId, level ); + } + catch ( final N5Exception e ) + { + throw new IOException( e ); + } + final String pathName = getFullPathName( getPathNameLevel( setupId, level ) ); + return new N5OMEZarrCacheArrayLoader<>( n5, pathName, setupToChannel.get( setupId ), timepointId, attributes, grid, + setupToMultiscale.get( setupId ).axes ); + } + + @Override + public String getPathNameFromSetupTimepointLevel( int setupId, int timepointId, int level ) + { + return getPathNameLevel( setupId, level ); + } + + @Override + public OmeNgffMetadata getMetadata() + { + final N5DatasetDiscoverer discoverer = new N5DatasetDiscoverer( n5, + Executors.newCachedThreadPool(), + Collections.singletonList( new N5GenericSingleScaleMetadataParser() ), + Collections.singletonList( new OmeNgffMetadataParser() ) ); + N5TreeNode root = null; + try + { + root = discoverer.discoverAndParseRecursive( "" ); + } + catch ( IOException e ) + { + e.printStackTrace(); + } + + final N5TreeNode metaNode = root.getDescendant( dataset ) + .filter( node -> { + return node.getMetadata() != null; + } ) + .get(); + if ( metaNode == null ) + { + return null; + } + final N5Metadata meta = metaNode.getMetadata(); + if ( meta instanceof OmeNgffMetadata ) + { + return ( OmeNgffMetadata ) meta; + } + return null; + } + +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/util/credentials/AWSCredentialsManager.java b/src/main/java/org/mastodon/mamut/io/loader/util/credentials/AWSCredentialsManager.java new file mode 100644 index 000000000..3ae1974c6 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/util/credentials/AWSCredentialsManager.java @@ -0,0 +1,54 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.util.credentials; + +import com.amazonaws.auth.AWSCredentials; + +public enum AWSCredentialsManager +{ + INSTANCE; + + private AWSCredentials credentials; + + public static AWSCredentialsManager getInstance() + { + return INSTANCE; + } + + public void setCredentials( final AWSCredentials credentials ) + { + this.credentials = credentials; + } + + public AWSCredentials getCredentials() + { + return credentials; + } + +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/util/credentials/AWSCredentialsTools.java b/src/main/java/org/mastodon/mamut/io/loader/util/credentials/AWSCredentialsTools.java new file mode 100644 index 000000000..866e96bd5 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/util/credentials/AWSCredentialsTools.java @@ -0,0 +1,70 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.util.credentials; + +import java.util.Arrays; + +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPasswordField; +import javax.swing.JTextField; + +import com.amazonaws.auth.BasicAWSCredentials; + +public class AWSCredentialsTools +{ + public static BasicAWSCredentials getBasicAWSCredentials() + { + final JLabel lblUsername = new JLabel( "Username" ); + final JTextField textFieldUsername = new JTextField(); + final JLabel lblPassword = new JLabel( "Password" ); + final JPasswordField passwordField = new JPasswordField(); + final Object[] ob = { lblUsername, textFieldUsername, lblPassword, passwordField }; + final int result = JOptionPane.showConfirmDialog( null, ob, "Please input credentials", JOptionPane.OK_CANCEL_OPTION ); + + if ( result == JOptionPane.OK_OPTION ) + { + final String username = textFieldUsername.getText(); + final char[] password = passwordField.getPassword(); + try + { + return new BasicAWSCredentials( username, String.valueOf( password ) ); + } + finally + { + Arrays.fill( password, '0' ); + } + } + else + { + return null; + } + } + +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ArrayCreator.java b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ArrayCreator.java new file mode 100644 index 000000000..2e205ac5b --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ArrayCreator.java @@ -0,0 +1,162 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/*- + * #%L + * Readers and writers for image data in MoBIE projects + * %% + * Copyright (C) 2021 - 2023 EMBL + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.util.mobie; + +import java.util.function.BiConsumer; + +import org.janelia.saalfeldlab.n5.DataBlock; +import org.janelia.saalfeldlab.n5.DataType; +import org.janelia.saalfeldlab.n5.imglib2.N5CellLoader; +import org.jetbrains.annotations.NotNull; + +import net.imglib2.img.array.ArrayImg; +import net.imglib2.img.array.ArrayImgs; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileByteArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileDoubleArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileFloatArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileIntArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileLongArray; +import net.imglib2.img.basictypeaccess.volatiles.array.VolatileShortArray; +import net.imglib2.img.cell.CellGrid; +import net.imglib2.type.NativeType; +import net.imglib2.util.Cast; + +public abstract class ArrayCreator< A, T extends NativeType< T > > +{ + protected final CellGrid cellGrid; + + protected final DataType dataType; + + protected final BiConsumer< ArrayImg< T, ? >, DataBlock< ? > > copyFromBlock; + + public ArrayCreator( CellGrid cellGrid, DataType dataType ) + { + this.cellGrid = cellGrid; + this.dataType = dataType; + this.copyFromBlock = N5CellLoader.createCopy( dataType ); + } + + @NotNull + public A VolatileDoubleArray( DataBlock< ? > dataBlock, long[] cellDims, int n ) + { + switch ( dataType ) + { + case UINT8: + case INT8: + byte[] bytes = new byte[ n ]; + copyFromBlock.accept( Cast.unchecked( ArrayImgs.bytes( bytes, cellDims ) ), dataBlock ); + return ( A ) new VolatileByteArray( bytes, true ); + case UINT16: + case INT16: + short[] shorts = new short[ n ]; + copyFromBlock.accept( Cast.unchecked( ArrayImgs.shorts( shorts, cellDims ) ), dataBlock ); + return ( A ) new VolatileShortArray( shorts, true ); + case UINT32: + case INT32: + int[] ints = new int[ n ]; + copyFromBlock.accept( Cast.unchecked( ArrayImgs.ints( ints, cellDims ) ), dataBlock ); + return ( A ) new VolatileIntArray( ints, true ); + case UINT64: + case INT64: + long[] longs = new long[ n ]; + copyFromBlock.accept( Cast.unchecked( ArrayImgs.longs( longs, cellDims ) ), dataBlock ); + return ( A ) new VolatileLongArray( longs, true ); + case FLOAT32: + float[] floats = new float[ n ]; + copyFromBlock.accept( Cast.unchecked( ArrayImgs.floats( floats, cellDims ) ), dataBlock ); + return ( A ) new VolatileFloatArray( floats, true ); + case FLOAT64: + double[] doubles = new double[ n ]; + copyFromBlock.accept( Cast.unchecked( ArrayImgs.doubles( doubles, cellDims ) ), dataBlock ); + return ( A ) new VolatileDoubleArray( doubles, true ); + default: + throw new IllegalArgumentException(); + } + } + + public A createEmptyArray( long[] gridPosition ) + { + long[] cellDims = getCellDims( gridPosition ); + int n = ( int ) ( cellDims[ 0 ] * cellDims[ 1 ] * cellDims[ 2 ] ); + switch ( dataType ) + { + case UINT8: + case INT8: + return Cast.unchecked( new VolatileByteArray( new byte[ n ], true ) ); + case UINT16: + case INT16: + return Cast.unchecked( new VolatileShortArray( new short[ n ], true ) ); + case UINT32: + case INT32: + return Cast.unchecked( new VolatileIntArray( new int[ n ], true ) ); + case UINT64: + case INT64: + return Cast.unchecked( new VolatileLongArray( new long[ n ], true ) ); + case FLOAT32: + return Cast.unchecked( new VolatileFloatArray( new float[ n ], true ) ); + case FLOAT64: + return Cast.unchecked( new VolatileDoubleArray( new double[ n ], true ) ); + default: + throw new IllegalArgumentException(); + } + } + + public long[] getCellDims( long[] gridPosition ) + { + return null; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/util/mobie/AxesTypes.java b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/AxesTypes.java new file mode 100644 index 000000000..f1cef5c82 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/AxesTypes.java @@ -0,0 +1,108 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/*- + * #%L + * Readers and writers for image data in MoBIE projects + * %% + * Copyright (C) 2021 - 2023 EMBL + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.util.mobie; + +public enum AxesTypes +{ + TIME( "time" ), + CHANNEL( "channel" ), + SPACE( "space" ); + + private final String typeName; + + AxesTypes( String typeName ) + { + this.typeName = typeName; + } + + public static boolean contains( String test ) + { + for ( AxesTypes c : AxesTypes.values() ) + { + if ( c.typeName.equals( test ) ) + { + return true; + } + } + return false; + } + + public static AxesTypes getAxisType( String axisString ) + { + if ( axisString.equals( "x" ) || axisString.equals( "y" ) || axisString.equals( "z" ) ) + { + return AxesTypes.SPACE; + } + else if ( axisString.equals( "t" ) ) + { + return AxesTypes.TIME; + } + else if ( axisString.equals( "c" ) ) + { + return AxesTypes.CHANNEL; + } + else + { + return null; + } + } + + public String getTypeName() + { + return typeName; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/util/mobie/N5CacheArrayLoader.java b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/N5CacheArrayLoader.java new file mode 100644 index 000000000..1e543fe03 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/N5CacheArrayLoader.java @@ -0,0 +1,219 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/*- + * #%L + * Readers and writers for image data in MoBIE projects + * %% + * Copyright (C) 2021 - 2023 EMBL + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.util.mobie; + +import java.io.IOException; +import java.util.Arrays; +import java.util.function.Function; +import java.util.function.IntFunction; + +import org.janelia.saalfeldlab.n5.DataBlock; +import org.janelia.saalfeldlab.n5.DatasetAttributes; +import org.janelia.saalfeldlab.n5.N5Exception; +import org.janelia.saalfeldlab.n5.N5Reader; +import org.mastodon.mamut.io.img.cache.MastodonSimpleCacheArrayLoader; + +import bdv.img.n5.DataTypeProperties; +import net.imglib2.img.basictypeaccess.DataAccess; +import net.imglib2.util.Cast; +import net.imglib2.util.Intervals; + +public class N5CacheArrayLoader< T, A extends DataAccess > implements MastodonSimpleCacheArrayLoader< A > +{ + private final N5Reader n5; + + private final String pathName; + + private final DatasetAttributes attributes; + + private final IntFunction< T > createPrimitiveArray; + + private final Function< T, A > createVolatileArrayAccess; + + public N5CacheArrayLoader( final N5Reader n5, final String pathName, final DatasetAttributes attributes, + final DataTypeProperties< ?, ?, T, A > dataTypeProperties ) + { + this( n5, pathName, attributes, dataTypeProperties.createPrimitiveArray(), dataTypeProperties.createVolatileArrayAccess() ); + } + + public N5CacheArrayLoader( final N5Reader n5, final String pathName, final DatasetAttributes attributes, + final IntFunction< T > createPrimitiveArray, + final Function< T, A > createVolatileArrayAccess ) + { + this.n5 = n5; + this.pathName = pathName; + this.attributes = attributes; + this.createPrimitiveArray = createPrimitiveArray; + this.createVolatileArrayAccess = createVolatileArrayAccess; + } + + @Override + public A loadArray( final long[] gridPosition, final int[] cellDimensions ) throws IOException + { + return loadArrayAtTimepoint( gridPosition, cellDimensions, -1 ); + } + + @Override + public A loadArrayAtTimepoint( final long[] gridPosition, final int[] cellDimensions, int timepoint ) throws IOException + { + final DataBlock< T > dataBlock; + try + { + dataBlock = Cast.unchecked( n5.readBlock( pathName, attributes, gridPosition ) ); + } + catch ( final N5Exception e ) + { + throw new IOException( e ); + } + if ( dataBlock != null && Arrays.equals( dataBlock.getSize(), cellDimensions ) ) + { + return createVolatileArrayAccess.apply( dataBlock.getData() ); + } + else + { + final T data = createPrimitiveArray.apply( ( int ) Intervals.numElements( cellDimensions ) ); + if ( dataBlock != null ) + { + final T src = dataBlock.getData(); + final int[] srcDims = dataBlock.getSize(); + final int[] dstDims = Arrays.copyOf( cellDimensions, srcDims.length ); + if ( cellDimensions.length < dstDims.length ) + { + Arrays.fill( dstDims, cellDimensions.length, dstDims.length, 1 ); + } + final int[] srcPos = new int[ srcDims.length ]; + final int[] dstPos = new int[ srcDims.length ]; + final int[] size = new int[ srcDims.length ]; + if ( srcDims.length == 4 && timepoint >= 0 ) + { + srcPos[ srcDims.length - 1 ] = timepoint % srcDims[ srcDims.length - 1 ]; + } + Arrays.setAll( size, d -> Math.min( srcDims[ d ], dstDims[ d ] ) ); + ndArrayCopy( src, srcDims, srcPos, data, dstDims, dstPos, size ); + } + return createVolatileArrayAccess.apply( data ); + } + } + + /** + * Like `System.arrayCopy()` but for flattened nD arrays. + * + * @param src + * the (flattened) source array. + * @param srcSize + * dimensions of the source array. + * @param srcPos + * starting position in the source array. + * @param dest + * the (flattened destination array. + * @param destSize + * dimensions of the source array. + * @param destPos + * starting position in the destination data. + * @param size + * the number of array elements to be copied. + */ + // TODO: This will be moved to a new imglib2-blk artifact later. Re-use it from there when that happens. + private static < T > void ndArrayCopy( + final T src, final int[] srcSize, final int[] srcPos, + final T dest, final int[] destSize, final int[] destPos, + final int[] size ) + { + final int n = srcSize.length; + int srcStride = 1; + int destStride = 1; + int srcOffset = 0; + int destOffset = 0; + for ( int d = 0; d < n; ++d ) + { + srcOffset += srcStride * srcPos[ d ]; + srcStride *= srcSize[ d ]; + destOffset += destStride * destPos[ d ]; + destStride *= destSize[ d ]; + } + ndArrayCopy( n - 1, src, srcSize, srcOffset, dest, destSize, destOffset, size ); + } + + private static < T > void ndArrayCopy( + final int d, + final T src, final int[] srcSize, final int srcPos, + final T dest, final int[] destSize, final int destPos, + final int[] size ) + { + if ( d == 0 ) + System.arraycopy( src, srcPos, dest, destPos, size[ d ] ); + else + { + int srcStride = 1; + int destStride = 1; + for ( int dd = 0; dd < d; ++dd ) + { + srcStride *= srcSize[ dd ]; + destStride *= destSize[ dd ]; + } + + final int w = size[ d ]; + for ( int x = 0; x < w; ++x ) + { + ndArrayCopy( d - 1, + src, srcSize, srcPos + x * srcStride, + dest, destSize, destPos + x * destStride, + size ); + } + } + } +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/util/mobie/N5OMEZarrCacheArrayLoader.java b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/N5OMEZarrCacheArrayLoader.java new file mode 100644 index 000000000..5a681438c --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/N5OMEZarrCacheArrayLoader.java @@ -0,0 +1,146 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/*- + * #%L + * Readers and writers for image data in MoBIE projects + * %% + * Copyright (C) 2021 - 2023 EMBL + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.util.mobie; + +import java.io.IOException; +import java.util.Map; + +import org.janelia.saalfeldlab.n5.DataBlock; +import org.janelia.saalfeldlab.n5.DataType; +import org.janelia.saalfeldlab.n5.DatasetAttributes; +import org.janelia.saalfeldlab.n5.N5Reader; + +import com.amazonaws.SdkClientException; + +import bdv.img.cache.SimpleCacheArrayLoader; +import net.imglib2.img.basictypeaccess.DataAccess; +import net.imglib2.img.cell.CellGrid; + +public class N5OMEZarrCacheArrayLoader< A extends DataAccess > implements SimpleCacheArrayLoader< A > +{ + private final N5Reader n5; + + private final String pathName; + + private final int channel; + + private final int timepoint; + + private final DatasetAttributes attributes; + + private final ZarrArrayCreator< A, ? > zarrArrayCreator; + + private final ZarrAxes zarrAxes; + + public N5OMEZarrCacheArrayLoader( final N5Reader n5, final String pathName, final int channel, final int timepoint, + final DatasetAttributes attributes, CellGrid grid, ZarrAxes zarrAxes ) + { + this.n5 = n5; + this.pathName = pathName; // includes the level + this.channel = channel; + this.timepoint = timepoint; + this.attributes = attributes; + final DataType dataType = attributes.getDataType(); + this.zarrArrayCreator = new ZarrArrayCreator<>( grid, dataType, zarrAxes ); + this.zarrAxes = zarrAxes; + } + + @Override + public A loadArray( final long[] gridPosition, int[] cellDimensions ) throws IOException + { + DataBlock< ? > block = null; + + long[] dataBlockIndices = toZarrChunkIndices( gridPosition ); + + try + { + block = n5.readBlock( pathName, attributes, dataBlockIndices ); + } + catch ( SdkClientException e ) + { + System.err.println( e.getMessage() ); // this happens sometimes, not sure yet why... + } + + if ( block == null ) + { + return ( A ) zarrArrayCreator.createEmptyArray( gridPosition ); + } + else + { + return zarrArrayCreator.createArray( block, gridPosition ); + } + } + + private long[] toZarrChunkIndices( long[] gridPosition ) + { + + long[] chunkInZarr = new long[ zarrAxes.getNumDimension() ]; + + // fill in the spatial dimensions + final Map< Integer, Integer > spatialToZarr = zarrAxes.spatialToZarr(); + for ( Map.Entry< Integer, Integer > entry : spatialToZarr.entrySet() ) + chunkInZarr[ entry.getValue() ] = gridPosition[ entry.getKey() ]; + + if ( zarrAxes.hasChannels() ) + chunkInZarr[ zarrAxes.channelIndex() ] = channel; + + if ( zarrAxes.hasTimepoints() ) + chunkInZarr[ zarrAxes.timeIndex() ] = timepoint; + + return chunkInZarr; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/util/mobie/OmeZarrMultiscales.java b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/OmeZarrMultiscales.java new file mode 100644 index 000000000..2f8ad29c5 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/OmeZarrMultiscales.java @@ -0,0 +1,157 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/*- + * #%L + * Readers and writers for image data in MoBIE projects + * %% + * Copyright (C) 2021 - 2023 EMBL + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.util.mobie; + +import java.util.List; + +import mpicbg.spim.data.sequence.VoxelDimensions; + +public class OmeZarrMultiscales +{ + + // key in json for multiscales + public static final String MULTI_SCALE_KEY = "multiscales"; + + public transient ZarrAxes axes; + + public List< ZarrAxis > zarrAxisList; + + public Dataset[] datasets; + + public String name; + + public String type; + + public String version; + + public CoordinateTransformations[] coordinateTransformations; + + public OmeZarrMultiscales() + {} + + public OmeZarrMultiscales( ZarrAxes axes, String name, String type, String version, + VoxelDimensions voxelDimensions, double[][] resolutions, String timeUnit, + double frameInterval ) + { + this.version = version; + this.name = name; + this.type = type; + this.axes = axes; + this.zarrAxisList = axes.toAxesList( voxelDimensions.unit(), timeUnit ); + generateDatasets( voxelDimensions, frameInterval, resolutions ); + } + + private void generateDatasets( VoxelDimensions voxelDimensions, double frameInterval, double[][] resolutions ) + { + + Dataset[] datasets = new Dataset[ resolutions.length ]; + for ( int i = 0; i < resolutions.length; i++ ) + { + Dataset dataset = new Dataset(); + + CoordinateTransformations coordinateTransformations = new CoordinateTransformations(); + coordinateTransformations.scale = getScale( voxelDimensions, frameInterval, resolutions[ i ] ); + coordinateTransformations.type = "scale"; + + dataset.path = "s" + i; + dataset.coordinateTransformations = new CoordinateTransformations[] { coordinateTransformations }; + datasets[ i ] = dataset; + } + this.datasets = datasets; + } + + private double[] getScale( VoxelDimensions voxelDimensions, double frameInterval, double[] xyzScale ) + { + int nDimensions = zarrAxisList.size(); + double[] scale = new double[ nDimensions ]; + if ( axes.timeIndex() != -1 ) + { + scale[ axes.timeIndex() ] = frameInterval; + } + + if ( axes.channelIndex() != -1 ) + { + scale[ axes.channelIndex() ] = 1; + } + + for ( int i = 0; i < 3; i++ ) + { + double dimension = voxelDimensions.dimension( i ) * xyzScale[ i ]; + scale[ nDimensions - ( i + 1 ) ] = dimension; + } + + return scale; + } + + public static class Dataset + { + public String path; + + public CoordinateTransformations[] coordinateTransformations; + } + + public static class CoordinateTransformations + { + public String type; + + public double[] scale; + + public double[] translation; + + public String path; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/util/mobie/OmeZarrMultiscalesAdapter.java b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/OmeZarrMultiscalesAdapter.java new file mode 100644 index 000000000..c41a7c1a0 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/OmeZarrMultiscalesAdapter.java @@ -0,0 +1,108 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/*- + * #%L + * Readers and writers for image data in MoBIE projects + * %% + * Copyright (C) 2021 - 2023 EMBL + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.util.mobie; + +import java.lang.reflect.Type; +import java.util.List; + +import org.mastodon.mamut.io.loader.util.mobie.OmeZarrMultiscales.Dataset; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.common.reflect.TypeToken; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +public class OmeZarrMultiscalesAdapter implements JsonDeserializer< OmeZarrMultiscales >, JsonSerializer< OmeZarrMultiscales > +{ + + @Override + public OmeZarrMultiscales deserialize( JsonElement json, Type typeOfT, JsonDeserializationContext context ) throws JsonParseException + { + JsonObject jsonObject = json.getAsJsonObject(); + Type zarrAxisListType = new TypeToken< List< ZarrAxis > >() + {}.getType(); + List< ZarrAxis > zarrAxisList = context.deserialize( jsonObject.get( "axes" ), zarrAxisListType ); + ZarrAxes axes = context.deserialize( jsonObject.get( "axes" ), ZarrAxes.class ); + Type datasetsType = new TypeToken< Dataset[] >() + {}.getType(); + Dataset[] datasets = context.deserialize( jsonObject.get( "datasets" ), datasetsType ); + String version = jsonObject.get( "version" ).getAsString(); + OmeZarrMultiscales multiscales = new OmeZarrMultiscales(); + multiscales.axes = axes; + multiscales.zarrAxisList = zarrAxisList; + multiscales.datasets = datasets; + multiscales.version = version; + return multiscales; + } + + @Override + public JsonElement serialize( OmeZarrMultiscales src, Type typeOfSrc, JsonSerializationContext context ) + { + JsonObject obj = new JsonObject(); + obj.add( "axes", context.serialize( src.zarrAxisList ) ); + obj.add( "datasets", context.serialize( src.datasets ) ); + obj.add( "name", context.serialize( src.name ) ); + obj.add( "type", context.serialize( src.type ) ); + obj.add( "version", context.serialize( src.version ) ); + obj.add( "coordinateTransformations", context.serialize( src.coordinateTransformations ) ); + return obj; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/util/mobie/UnitTypes.java b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/UnitTypes.java new file mode 100644 index 000000000..24a5e19b0 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/UnitTypes.java @@ -0,0 +1,172 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/*- + * #%L + * Readers and writers for image data in MoBIE projects + * %% + * Copyright (C) 2021 - 2023 EMBL + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.util.mobie; + +import ucar.units.PrefixDBException; +import ucar.units.SpecificationException; +import ucar.units.Unit; +import ucar.units.UnitDBException; +import ucar.units.UnitFormat; +import ucar.units.UnitFormatManager; +import ucar.units.UnitSystemException; + +public enum UnitTypes +{ + ANGSTROM( "angstrom" ), + ATTOMETER( "attometer" ), + CENTIMETER( "centimeter" ), + DECIMETER( "decimeter" ), + EXAMETER( "exameter" ), + FEMTOMETER( "femtometer" ), + FOOT( "foot" ), + GIGAMETER( "gigameter" ), + HECTOMETER( "hectometer" ), + INCH( "inch" ), + KILOMETER( "kilometer" ), + MEGAMETER( "megameter" ), + METER( "meter" ), + MICROMETER( "micrometer" ), + MILE( "mile" ), + MILLIMETER( "millimeter" ), + NANOMETER( "nanometer" ), + PARSEC( "parsec" ), + PETAMETER( "petameter" ), + PICOMETER( "picometer" ), + TERAMETER( "terameter" ), + YARD( "yard" ), + YOCTOMETER( "yoctometer" ), + YOTTAMETER( "yottameter" ), + ZEPTOMETER( "zeptometer" ), + ZETTAMETER( "zettameter" ), + + ATTOSECOND( "attosecond" ), + CENTISECOND( "centisecond" ), + DAY( "day" ), + DECISECOND( "decisecond" ), + EXASECOND( "exasecond" ), + FEMTOSECOND( "femtosecond" ), + GIGASECOND( "gigasecond" ), + HECTOSECOND( "hectosecond" ), + HOUR( "hour" ), + KILOSECOND( "kilosecond" ), + MEGASECOND( "megasecond" ), + MICROSECOND( "microsecond" ), + MILLISECOND( "millisecond" ), + MINUTE( "minute" ), + NANOSECOND( "nanosecond" ), + PETASECOND( "petasecond" ), + PICOSECOND( "picosecond" ), + SECOND( "second" ), + TERASECOND( "terasecond" ), + YOCTOSECOND( "yoctosecond" ), + YOTTASECOND( "yottasecond" ), + ZEPTOSECOND( "zeptosecond" ), + ZETTASECOND( "zettasecond" ); + + private final String typeName; + + UnitTypes( String typeName ) + { + this.typeName = typeName; + } + + public static boolean contains( String test ) + { + for ( UnitTypes c : UnitTypes.values() ) + { + if ( c.typeName.equals( test ) ) + { + return true; + } + } + return false; + } + + public static UnitTypes convertUnit( String unit ) + { + // Convert the mu symbol into "u". + String unitString = unit.replace( "\u00B5", "u" ); + + try + { + UnitFormat unitFormatter = UnitFormatManager.instance(); + Unit inputUnit = unitFormatter.parse( unitString ); + + for ( UnitTypes unitType : UnitTypes.values() ) + { + Unit zarrUnit = unitFormatter.parse( unitType.typeName ); + if ( zarrUnit.getCanonicalString().equals( inputUnit.getCanonicalString() ) ) + { + System.out.println( "Converted unit: " + unit + " to recommended ome-zarr unit: " + unitType.getTypeName() ); + return unitType; + } + } + } + catch ( SpecificationException | UnitDBException | PrefixDBException | UnitSystemException e ) + { + e.printStackTrace(); + } + + System.out.println( unit + " is not one of the recommended units for ome-zarr" ); + return null; + } + + public String getTypeName() + { + return typeName; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ZarrArrayCreator.java b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ZarrArrayCreator.java new file mode 100644 index 000000000..e1f281c76 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ZarrArrayCreator.java @@ -0,0 +1,106 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/*- + * #%L + * Readers and writers for image data in MoBIE projects + * %% + * Copyright (C) 2021 - 2023 EMBL + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.util.mobie; + +import java.util.Arrays; + +import org.janelia.saalfeldlab.n5.DataBlock; +import org.janelia.saalfeldlab.n5.DataType; + +import net.imglib2.img.cell.CellGrid; +import net.imglib2.type.NativeType; + +public class ZarrArrayCreator< A, T extends NativeType< T > > extends ArrayCreator< A, T > +{ + private final ZarrAxes zarrAxes; + + public ZarrArrayCreator( CellGrid cellGrid, DataType dataType, ZarrAxes zarrAxes ) + { + super( cellGrid, dataType ); + this.zarrAxes = zarrAxes; + } + + public A createArray( DataBlock< ? > dataBlock, long[] gridPosition ) + { + long[] cellDims = getCellDims( gridPosition ); + int n = ( int ) ( cellDims[ 0 ] * cellDims[ 1 ] * cellDims[ 2 ] ); + + if ( zarrAxes.getNumDimension() == 2 ) + cellDims = Arrays.stream( cellDims ).limit( 2 ).toArray(); + + return ( A ) VolatileDoubleArray( dataBlock, cellDims, n ); + } + + @Override + public long[] getCellDims( long[] gridPosition ) + { + long[] cellMin = new long[ Math.max( zarrAxes.getNumDimension(), 3 ) ]; + int[] cellDims = new int[ Math.max( zarrAxes.getNumDimension(), 3 ) ]; + + if ( zarrAxes.hasChannels() ) + { + cellDims[ zarrAxes.channelIndex() ] = 1; + } + if ( zarrAxes.hasTimepoints() ) + { + cellDims[ zarrAxes.timeIndex() ] = 1; + } + + cellGrid.getCellDimensions( gridPosition, cellMin, cellDims ); + return Arrays.stream( cellDims ).mapToLong( i -> i ).toArray(); // casting to long for creating ArrayImgs.* + } +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ZarrAxes.java b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ZarrAxes.java new file mode 100644 index 000000000..27d48e22a --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ZarrAxes.java @@ -0,0 +1,214 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/*- + * #%L + * Readers and writers for image data in MoBIE projects + * %% + * Copyright (C) 2021 - 2023 EMBL + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.util.mobie; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.google.common.collect.Lists; + +@JsonFormat( shape = JsonFormat.Shape.ARRAY ) +public enum ZarrAxes +{ + YX( "[\"y\",\"x\"]" ), + CYX( "[\"c\",\"y\",\"x\"]" ), + TYX( "[\"t\",\"y\",\"x\"]" ), + ZYX( "[\"z\",\"y\",\"x\"]" ), + CZYX( "[\"c\",\"z\",\"y\",\"x\"]" ), + TZYX( "[\"t\",\"z\",\"y\",\"x\"]" ), + TCYX( "[\"t\",\"c\",\"y\",\"x\"]" ), + TCZYX( "[\"t\",\"c\",\"z\",\"y\",\"x\"]" ); // v0.2 + + private final String axes; + + ZarrAxes( String axes ) + { + this.axes = axes; + } + + @JsonCreator + public static ZarrAxes decode( final String axes ) + { + return Stream.of( ZarrAxes.values() ).filter( targetEnum -> targetEnum.axes.equals( axes ) ).findFirst().orElse( TCZYX ); + } + + public List< String > getAxesList() + { + String pattern = "([a-z])"; + List< String > allMatches = new ArrayList<>(); + Matcher m = Pattern.compile( pattern ) + .matcher( axes ); + while ( m.find() ) + { + allMatches.add( m.group() ); + } + return allMatches; + } + + public List< ZarrAxis > toAxesList( String spaceUnit, String timeUnit ) + { + List< ZarrAxis > zarrAxesList = new ArrayList<>(); + List< String > zarrAxesStrings = getAxesList(); + + String[] units = new String[] { spaceUnit, timeUnit }; + + // convert to valid ome-zarr units, if possible, otherwise just go ahead with + // given unit + for ( int i = 0; i < units.length; i++ ) + { + String unit = units[ i ]; + if ( !UnitTypes.contains( unit ) ) + { + UnitTypes unitType = UnitTypes.convertUnit( unit ); + if ( unitType != null ) + { + units[ i ] = unitType.getTypeName(); + } + } + } + + for ( int i = 0; i < zarrAxesStrings.size(); i++ ) + { + String axisString = zarrAxesStrings.get( i ); + AxesTypes axisType = AxesTypes.getAxisType( axisString ); + + String unit; + if ( axisType == AxesTypes.SPACE ) + { + unit = units[ 0 ]; + } + else if ( axisType == AxesTypes.TIME ) + { + unit = units[ 1 ]; + } + else + { + unit = null; + } + + zarrAxesList.add( new ZarrAxis( i, axisString, axisType.getTypeName(), unit ) ); + } + + return zarrAxesList; + } + + public boolean hasTimepoints() + { + return this.axes.equals( TCYX.axes ) || this.axes.equals( TZYX.axes ) || this.axes.equals( TYX.axes ) + || this.axes.equals( TCZYX.axes ); + } + + public boolean hasChannels() + { + return this.axes.equals( CZYX.axes ) || this.axes.equals( CYX.axes ) || this.axes.equals( TCYX.axes ) + || this.axes.equals( TCZYX.axes ); + } + + // the flag reverseAxes determines whether the index will be given w.r.t. + // reversedAxes=true corresponds to the java/bdv axis convention + // reversedAxes=false corresponds to the zarr axis convention + public int axisIndex( String axisName, boolean reverseAxes ) + { + if ( reverseAxes ) + { + List< String > reverseAxesList = Lists.reverse( getAxesList() ); + return reverseAxesList.indexOf( axisName ); + } + return getAxesList().indexOf( axisName ); + } + + public int timeIndex() + { + return axisIndex( "t", true ); + } + + public int channelIndex() + { + return axisIndex( "c", true ); + } + + // spatial: 0,1,2 (x,y,z) + public Map< Integer, Integer > spatialToZarr() + { + final HashMap< Integer, Integer > map = new HashMap<>(); + map.put( 0, 0 ); + map.put( 1, 1 ); + if ( hasZAxis() ) + { + map.put( 2, 2 ); + } + return map; + } + + public boolean hasZAxis() + { + return this.axes.equals( TCZYX.axes ) || this.axes.equals( CZYX.axes ) || this.axes.equals( TZYX.axes ) + || this.axes.equals( ZYX.axes ); + } + + public int getNumDimension() + { + return getAxesList().size(); + } +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ZarrAxesAdapter.java b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ZarrAxesAdapter.java new file mode 100644 index 000000000..cbcfe281f --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ZarrAxesAdapter.java @@ -0,0 +1,128 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/*- + * #%L + * Readers and writers for image data in MoBIE projects + * %% + * Copyright (C) 2021 - 2023 EMBL + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.util.mobie; + +import java.lang.reflect.Type; +import java.util.List; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +public class ZarrAxesAdapter implements JsonDeserializer< ZarrAxes >, JsonSerializer< ZarrAxes > +{ + + @Override + public ZarrAxes deserialize( JsonElement json, Type typeOfT, JsonDeserializationContext context ) throws JsonParseException + { + JsonArray array = json.getAsJsonArray(); + if ( array.size() > 0 ) + { + StringBuilder axisString = new StringBuilder( "[" ); + for ( int i = 0; i < array.size(); i++ ) + { + String element; + try + { + element = array.get( i ).getAsString(); + } + catch ( UnsupportedOperationException e ) + { + try + { + JsonElement jj = array.get( i ); + element = jj.getAsJsonObject().get( "name" ).getAsString(); + } + catch ( Exception exception ) + { + throw new JsonParseException( "" + e ); + } + } + if ( i != 0 ) + { + axisString.append( "," ); + } + axisString.append( "\"" ); + axisString.append( element ); + axisString.append( "\"" ); + + } + axisString.append( "]" ); + return ZarrAxes.decode( axisString.toString() ); + } + else + { + return null; + } + } + + @Override + public JsonElement serialize( ZarrAxes axes, Type typeOfSrc, JsonSerializationContext context ) + { + List< String > axisList = axes.getAxesList(); + JsonArray jsonArray = new JsonArray(); + for ( String axis : axisList ) + { + jsonArray.add( axis ); + } + return jsonArray; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ZarrAxis.java b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ZarrAxis.java new file mode 100644 index 000000000..7d2e3adc6 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/loader/util/mobie/ZarrAxis.java @@ -0,0 +1,130 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Mastodon developers + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/*- + * #%L + * Readers and writers for image data in MoBIE projects + * %% + * Copyright (C) 2021 - 2023 EMBL + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.io.loader.util.mobie; + +import java.util.List; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; + +public class ZarrAxis +{ + private transient final int index; + + private final String name; + + private final String type; + + private String unit; + + public ZarrAxis( int index, String name, String type, String unit ) + { + this.index = index; + this.name = name; + this.type = type; + this.unit = unit; + } + + public ZarrAxis( int index, String name, String type ) + { + this.index = index; + this.name = name; + this.type = type; + } + + public static JsonElement convertToJson( List< ZarrAxis > zarrAxes ) + { + StringBuilder axes = new StringBuilder(); + axes.append( "[" ); + for ( ZarrAxis axis : zarrAxes ) + { + axes.append( "\"" ).append( axis.getName() ).append( "\"" ); + if ( axis.getIndex() < zarrAxes.size() - 1 ) + { + axes.append( "," ); + } + } + axes.append( "]" ); + Gson gson = new Gson(); + return gson.fromJson( axes.toString(), JsonElement.class ); + } + + public int getIndex() + { + return index; + } + + public String getName() + { + return name; + } + + public String getType() + { + return type; + } + + public String getUnit() + { + return unit; + } + + public void setUnit( String unit ) + { + this.unit = unit; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/project/MamutProject.java b/src/main/java/org/mastodon/mamut/io/project/MamutProject.java index 94bbe13ab..f6455107f 100644 --- a/src/main/java/org/mastodon/mamut/io/project/MamutProject.java +++ b/src/main/java/org/mastodon/mamut/io/project/MamutProject.java @@ -28,6 +28,8 @@ */ package org.mastodon.mamut.io.project; +import org.apache.commons.io.FileUtils; + import java.io.Closeable; import java.io.File; import java.io.FileInputStream; @@ -36,6 +38,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -333,29 +337,37 @@ public void close() throws IOException private class WriteToDirectory implements ProjectWriter { + private final File pendingDirectory; + + private WriteToDirectory() + { + pendingDirectory = new File( projectRoot.getParent(), projectRoot.getName() + "_pending" ); + if ( !pendingDirectory.exists() ) + pendingDirectory.mkdir(); + } @Override public OutputStream getProjectXmlOutputStream() throws FileNotFoundException { - return new FileOutputStream( new File( projectRoot, PROJECT_FILE_NAME ) ); + return new FileOutputStream( new File( pendingDirectory, PROJECT_FILE_NAME ) ); } @Override public OutputStream getRawModelOutputStream() throws FileNotFoundException { - return new FileOutputStream( new File( projectRoot, RAW_MODEL_FILE_NAME ) ); + return new FileOutputStream( new File( pendingDirectory, RAW_MODEL_FILE_NAME ) ); } @Override public OutputStream getRawTagsOutputStream() throws FileNotFoundException { - return new FileOutputStream( new File( projectRoot, RAW_TAGS_FILE_NAME ) ); + return new FileOutputStream( new File( pendingDirectory, RAW_TAGS_FILE_NAME ) ); } @Override public OutputStream getFeatureOutputStream( final String featureKey ) throws IOException { - final File featureFolder = new File( projectRoot, FEATURE_FOLDER_NAME ); + final File featureFolder = new File( pendingDirectory, FEATURE_FOLDER_NAME ); if ( !featureFolder.exists() ) featureFolder.mkdir(); return new FileOutputStream( new File( featureFolder, featureKey + ".raw" ) ); @@ -364,27 +376,34 @@ public OutputStream getFeatureOutputStream( final String featureKey ) throws IOE @Override public OutputStream getGuiOutputStream() throws IOException { - return new FileOutputStream( new File( projectRoot, GUI_FILE_NAME ) ); + return new FileOutputStream( new File( pendingDirectory, GUI_FILE_NAME ) ); } @Override public OutputStream getBackupDatasetXmlOutputStream() throws IOException { - return new FileOutputStream( new File( projectRoot, BACKUP_DATASET_XML_FILE_NAME ) ); + return new FileOutputStream( new File( pendingDirectory, BACKUP_DATASET_XML_FILE_NAME ) ); } @Override public void close() throws IOException - {} + { + FileUtils.deleteDirectory( projectRoot ); + Files.move( pendingDirectory.toPath(), projectRoot.toPath(), StandardCopyOption.REPLACE_EXISTING ); + } } private class WriteToZip implements ProjectWriter { private final WriteZip zip; - WriteToZip() throws IOException + private final File pendingFile; + + private WriteToZip() throws IOException { - zip = new WriteZip( projectRoot ); + String suffix = "_pending"; + pendingFile = new File( projectRoot.getParent(), projectRoot.getName() + suffix ); + zip = new WriteZip( pendingFile ); } @Override @@ -427,6 +446,7 @@ public OutputStream getBackupDatasetXmlOutputStream() throws IOException public void close() throws IOException { zip.close(); + Files.move( pendingFile.toPath(), projectRoot.toPath(), StandardCopyOption.REPLACE_EXISTING ); } } } diff --git a/src/main/java/org/mastodon/mamut/launcher/LauncherGUI.java b/src/main/java/org/mastodon/mamut/launcher/LauncherGUI.java index 699e39703..d5bea035d 100644 --- a/src/main/java/org/mastodon/mamut/launcher/LauncherGUI.java +++ b/src/main/java/org/mastodon/mamut/launcher/LauncherGUI.java @@ -102,6 +102,8 @@ class LauncherGUI extends JPanel final NewMastodonProjectPanel newMastodonProjectPanel; + final NewFromUrlPanel newFromUrlPanel; + final LoggerPanel logger; final ImportTGMMPanel importTGMMPanel; @@ -208,6 +210,9 @@ public LauncherGUI( final Consumer< String > projectOpener ) newMastodonProjectPanel = new NewMastodonProjectPanel( "New Mastodon project", "create" ); centralPanel.add( newMastodonProjectPanel, NEW_MASTODON_PROJECT_KEY ); + newFromUrlPanel = new NewFromUrlPanel( "New From URL", "create" ); + centralPanel.add( newFromUrlPanel, NEW_FROM_URL_KEY ); + recentProjectsPanel = new RecentProjectsPanel( projectOpener ); centralPanel.add( recentProjectsPanel, RECENT_PROJECTS_KEY ); @@ -350,6 +355,7 @@ public void log( final String string ) { log( string, NORMAL_COLOR ); } + public void log( final String message, final Color color ) { final StyleContext sc = StyleContext.getDefaultStyleContext(); diff --git a/src/main/java/org/mastodon/mamut/launcher/MastodonLauncher.java b/src/main/java/org/mastodon/mamut/launcher/MastodonLauncher.java index cc758a782..37815d195 100644 --- a/src/main/java/org/mastodon/mamut/launcher/MastodonLauncher.java +++ b/src/main/java/org/mastodon/mamut/launcher/MastodonLauncher.java @@ -113,6 +113,7 @@ public MastodonLauncher( final Context context ) gui.btnHelp.addActionListener( l -> showHelpPanel() ); gui.newMastodonProjectPanel.btnCreate.addActionListener( l -> createNewProject() ); + gui.newFromUrlPanel.btnCreate.addActionListener( l -> createNewProjectFromURL() ); gui.importTGMMPanel.btnImport.addActionListener( l -> importTgmm() ); gui.importSimiBioCellPanel.btnImport.addActionListener( l -> importSimi() ); @@ -318,6 +319,32 @@ else if ( entries.length == 9 ) return null; } + private void createNewProjectFromURL() + { + gui.clearLog(); + + /* + * Open from a URL. + */ + + final EverythingDisablerAndReenabler disabler = + new EverythingDisablerAndReenabler( gui, new Class[] { JLabel.class } ); + disabler.disable(); + new Thread( () -> { + try + { + final ProjectModel appModel = + LauncherUtil.createProjectFromBdvFileWithDialog( gui.newFromUrlPanel.xmlFile, context, gui, gui::error ); + new MainWindow( appModel ).setVisible( true ); + dispose(); + } + finally + { + disabler.reenable(); + } + } ).start(); + } + private void createNewProject() { gui.clearLog(); @@ -571,7 +598,8 @@ private void loadMastodonProject( final String projectPath ) } catch ( final IOException e ) { - gui.error( "Invalid Mastodon file.\nMaybe it is not a Mastodon file?\n\n" + LauncherUtil.getProblemDescription( null, e ) ); + gui.error( "Invalid Mastodon file.\nMaybe it is not a Mastodon file?\n\n" + + LauncherUtil.getProblemDescription( null, e ) ); } } finally @@ -592,7 +620,8 @@ public synchronized void drop( final DropTargetDropEvent dropTargetDropEvent ) { dropTargetDropEvent.acceptDrop( DnDConstants.ACTION_COPY ); @SuppressWarnings( "unchecked" ) - final List< File > droppedFiles = ( List< File > ) dropTargetDropEvent.getTransferable().getTransferData( DataFlavor.javaFileListFlavor ); + final List< File > droppedFiles = + ( List< File > ) dropTargetDropEvent.getTransferable().getTransferData( DataFlavor.javaFileListFlavor ); for ( final File file : droppedFiles ) { // process files diff --git a/src/main/java/org/mastodon/mamut/launcher/MastodonLauncherCommand.java b/src/main/java/org/mastodon/mamut/launcher/MastodonLauncherCommand.java index a6a0850df..73f30192d 100644 --- a/src/main/java/org/mastodon/mamut/launcher/MastodonLauncherCommand.java +++ b/src/main/java/org/mastodon/mamut/launcher/MastodonLauncherCommand.java @@ -33,7 +33,7 @@ import org.scijava.command.ContextCommand; import org.scijava.plugin.Plugin; -@Plugin( type = Command.class, menuPath = "Plugins>Mastodon" ) +@Plugin( type = Command.class, menuPath = "Plugins>Tracking>Mastodon>Mastodon Launcher" ) public class MastodonLauncherCommand extends ContextCommand { diff --git a/src/main/java/org/mastodon/mamut/launcher/NewFromUrlPanel.java b/src/main/java/org/mastodon/mamut/launcher/NewFromUrlPanel.java new file mode 100644 index 000000000..b124b8726 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/launcher/NewFromUrlPanel.java @@ -0,0 +1,408 @@ +/*- + * #%L + * Mastodon + * %% + * Copyright (C) 2014 - 2024 Tobias Pietzsch, Jean-Yves Tinevez + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.mamut.launcher; + +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.filechooser.FileNameExtensionFilter; + +import org.janelia.saalfeldlab.n5.N5Exception; +import org.janelia.saalfeldlab.n5.N5Reader; +import org.janelia.saalfeldlab.n5.N5URI; +import org.janelia.saalfeldlab.n5.bdv.N5ViewerCreator; +import org.janelia.saalfeldlab.n5.bdv.N5ViewerTreeCellRenderer; +import org.janelia.saalfeldlab.n5.ij.N5Importer; +import org.janelia.saalfeldlab.n5.metadata.N5ViewerMultichannelMetadata; +import org.janelia.saalfeldlab.n5.ui.DatasetSelectorDialog; +import org.janelia.saalfeldlab.n5.universe.N5Factory; +import org.janelia.saalfeldlab.n5.universe.N5Factory.StorageFormat; +import org.janelia.saalfeldlab.n5.universe.metadata.MultiscaleMetadata; +import org.janelia.saalfeldlab.n5.universe.metadata.N5Metadata; +import org.janelia.saalfeldlab.n5.universe.metadata.N5SingleScaleMetadata; +import org.janelia.saalfeldlab.n5.universe.metadata.axes.AxisUtils; +import org.janelia.saalfeldlab.n5.universe.metadata.axes.DefaultAxisMetadata; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.NgffSingleScaleAxesMetadata; +import org.janelia.saalfeldlab.n5.universe.metadata.ome.ngff.v04.OmeNgffMetadata; +import org.mastodon.mamut.io.loader.N5UniverseImgLoader; +import org.mastodon.mamut.io.loader.util.credentials.AWSCredentialsManager; +import org.mastodon.mamut.io.loader.util.credentials.AWSCredentialsTools; +import org.mastodon.ui.util.EverythingDisablerAndReenabler; + +import bdv.spimdata.SequenceDescriptionMinimal; +import bdv.spimdata.SpimDataMinimal; +import bdv.spimdata.XmlIoSpimDataMinimal; +import ij.IJ; +import mpicbg.spim.data.SpimDataException; +import mpicbg.spim.data.generic.sequence.BasicImgLoader; +import mpicbg.spim.data.generic.sequence.BasicViewSetup; +import mpicbg.spim.data.registration.ViewRegistration; +import mpicbg.spim.data.registration.ViewRegistrations; +import mpicbg.spim.data.sequence.FinalVoxelDimensions; +import mpicbg.spim.data.sequence.TimePoint; +import mpicbg.spim.data.sequence.TimePoints; +import mpicbg.spim.data.sequence.VoxelDimensions; +import net.imglib2.Dimensions; +import net.imglib2.FinalDimensions; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.util.Pair; + +class NewFromUrlPanel extends JPanel +{ + + private static final long serialVersionUID = 1L; + + final JButton btnCreate; + + final JLabel labelInfo; + + File xmlFile; + + private String lastOpenedContainer = ""; + + public NewFromUrlPanel( final String panelTitle, final String buttonTitle ) + { + final GridBagLayout gblNewMastodonProjectPanel = new GridBagLayout(); + gblNewMastodonProjectPanel.columnWidths = new int[] { 0, 0 }; + gblNewMastodonProjectPanel.rowHeights = new int[] { 35, 70, 65, 0, 25, 45, 0, 0, 25, 0, 0, 0 }; + gblNewMastodonProjectPanel.columnWeights = new double[] { 1.0, Double.MIN_VALUE }; + gblNewMastodonProjectPanel.rowWeights = + new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, Double.MIN_VALUE }; + setLayout( gblNewMastodonProjectPanel ); + + int row = 0; + + final JLabel lblNewMastodonProject = new JLabel( panelTitle ); + lblNewMastodonProject.setFont( + lblNewMastodonProject.getFont().deriveFont( lblNewMastodonProject.getFont().getStyle() | Font.BOLD ) ); + final GridBagConstraints gbcLblNewMastodonProject = new GridBagConstraints(); + gbcLblNewMastodonProject.insets = new Insets( 5, 5, 5, 5 ); + gbcLblNewMastodonProject.gridx = 0; + gbcLblNewMastodonProject.gridy = row++; + add( lblNewMastodonProject, gbcLblNewMastodonProject ); + + final JLabel lblFetchFromURL = new JLabel( "Browse URL:" ); + final GridBagConstraints gbcLblFetchFromURL = new GridBagConstraints(); + gbcLblFetchFromURL.insets = new Insets( 5, 5, 5, 5 ); + gbcLblFetchFromURL.anchor = GridBagConstraints.SOUTHWEST; + gbcLblFetchFromURL.gridx = 0; + gbcLblFetchFromURL.gridy = row++; + add( lblFetchFromURL, gbcLblFetchFromURL ); + + final JButton btnBrowse = new JButton( "browse" ); + final GridBagConstraints gbcBtnBrowse = new GridBagConstraints(); + gbcBtnBrowse.insets = new Insets( 5, 5, 5, 5 ); + gbcBtnBrowse.anchor = GridBagConstraints.EAST; + gbcBtnBrowse.gridx = 0; + gbcBtnBrowse.gridy = row++; + add( btnBrowse, gbcBtnBrowse ); + + /* + * Wire listeners. + */ + btnBrowse.addActionListener( e -> openBrowserWindow() ); + + labelInfo = new JLabel( "" ); + final GridBagConstraints gbcLabelInfo = new GridBagConstraints(); + gbcLabelInfo.insets = new Insets( 5, 5, 5, 5 ); + gbcLabelInfo.fill = GridBagConstraints.BOTH; + gbcLabelInfo.gridx = 0; + gbcLabelInfo.gridy = row++; + add( labelInfo, gbcLabelInfo ); + + btnCreate = new JButton( buttonTitle ); + btnCreate.setEnabled( false ); + final GridBagConstraints gbcBtnCreate = new GridBagConstraints(); + gbcBtnCreate.anchor = GridBagConstraints.EAST; + gbcBtnCreate.gridx = 0; + gbcBtnCreate.gridy = row++; + add( btnCreate, gbcBtnCreate ); + } + + private void openBrowserWindow() + { + xmlFile = null; + final ExecutorService exec = Executors.newFixedThreadPool( ij.Prefs.getThreads() ); + final DatasetSelectorDialog dialog = new DatasetSelectorDialog( + new N5ViewerReaderFun(), + new N5Importer.N5BasePathFun(), + lastOpenedContainer, + N5ViewerCreator.n5vGroupParsers, + N5ViewerCreator.n5vParsers ); + + dialog.setLoaderExecutor( exec ); + dialog.setContainerPathUpdateCallback( x -> lastOpenedContainer = x ); + dialog.setTreeRenderer( new N5ViewerTreeCellRenderer( false ) ); + + btnCreate.setEnabled( false ); + labelInfo.setText( "" ); + + dialog.run( selection -> { + + // Disable UI when clicking ok. + final EverythingDisablerAndReenabler disabler = new EverythingDisablerAndReenabler( SwingUtilities.getWindowAncestor( dialog.getJTree() ), null ); + disabler.disable(); + + final N5Metadata metadata = selection.metadata.get( 0 ); + long[] dimensions = null; + double[] scales = null; + String[] axisLabels = null; + String unit = null; + AffineTransform3D calib = null; + int x = 1; + int y = 1; + int z = 1; + int c = 1; + int t = 1; + double sx = 1.0; + double sy = 1.0; + double sz = 1.0; + if ( metadata instanceof OmeNgffMetadata ) + { + final NgffSingleScaleAxesMetadata setup0Level0Metadata = + ( ( OmeNgffMetadata ) metadata ).multiscales[ 0 ].getChildrenMetadata()[ 0 ]; + dimensions = setup0Level0Metadata.getAttributes().getDimensions(); + scales = setup0Level0Metadata.getScale(); + axisLabels = setup0Level0Metadata.getAxisLabels(); + unit = setup0Level0Metadata.unit(); + if ( unit == null || unit.isEmpty() ) + unit = "pixel"; + calib = setup0Level0Metadata.spatialTransform3d(); + } + else if ( metadata instanceof N5ViewerMultichannelMetadata ) + { + c = ( ( N5ViewerMultichannelMetadata ) metadata ).getChildrenMetadata().length; + final MultiscaleMetadata< ? > setup0Metadata = + ( ( N5ViewerMultichannelMetadata ) metadata ).getChildrenMetadata()[ 0 ]; + final N5SingleScaleMetadata setup0Level0Metadata = ( N5SingleScaleMetadata ) setup0Metadata.getChildrenMetadata()[ 0 ]; + dimensions = setup0Level0Metadata.getAttributes().getDimensions(); + scales = setup0Level0Metadata.getPixelResolution(); + final DefaultAxisMetadata axes = AxisUtils.defaultN5ViewerAxes( ( setup0Level0Metadata ) ); + axisLabels = axes.getAxisLabels(); + unit = setup0Level0Metadata.unit(); + if ( unit == null || unit.isEmpty() ) + unit = "pixel"; + calib = setup0Level0Metadata.spatialTransform3d(); + } + else + { + SwingUtilities.invokeLater( + () -> IJ.error( "Metadata not supported", "The metadata is not supported: " + metadata.getName() ) ); + return; + } + final AffineTransform3D calibFinal = calib.copy(); + + for ( int i = 0; i < axisLabels.length; i++ ) + { + if ( axisLabels[ i ].toLowerCase().equals( "x" ) ) + { + x = ( int ) dimensions[ i ]; + sx = scales[ i ]; + } + else if ( axisLabels[ i ].toLowerCase().equals( "y" ) ) + { + y = ( int ) dimensions[ i ]; + sy = scales[ i ]; + } + else if ( axisLabels[ i ].toLowerCase().equals( "z" ) ) + { + z = ( int ) dimensions[ i ]; + sz = scales[ i ]; + } + else if ( axisLabels[ i ].toLowerCase().equals( "c" ) ) + c = ( int ) dimensions[ i ]; + else if ( axisLabels[ i ].toLowerCase().equals( "t" ) ) + t = ( int ) dimensions[ i ]; + } + final Dimensions imageSize = new FinalDimensions( x, y, z ); + final TimePoints timepoints = new TimePoints( + IntStream.range( 0, t ).mapToObj( TimePoint::new ).collect( Collectors.toList() ) ); + final Map< Integer, BasicViewSetup > setups = new HashMap<>(); + final VoxelDimensions voxelDimensions = new FinalVoxelDimensions( unit, sx, sy, sz ); + for ( int i = 0; i < c; i++ ) + setups.put( i, new BasicViewSetup( i, String.format( "channel %d", i ), imageSize, voxelDimensions ) ); + final BasicImgLoader imgLoader = + new N5UniverseImgLoader( selection.n5.getURI().toString(), metadata.getPath(), null ); + final SequenceDescriptionMinimal sequenceDescription = + new SequenceDescriptionMinimal( timepoints, setups, imgLoader, null ); + final ViewRegistrations viewRegistrations = new ViewRegistrations( + IntStream.range( 0, t ) + .boxed() + .flatMap( tp -> IntStream.range( 0, setups.size() ) + .mapToObj( setup -> new ViewRegistration( tp, setup, calibFinal ) ) ) + .collect( Collectors.toList() ) ); + + final JFileChooser fileChooser = new JFileChooser(); + fileChooser.setDialogTitle( "Save BigDataViewer XML File" ); + final FileNameExtensionFilter xmlFilter = new FileNameExtensionFilter( "XML Files", "xml" ); + fileChooser.setFileFilter( xmlFilter ); + final String currDir = IJ.getDirectory( "current" ); + if ( currDir != null ) + fileChooser.setCurrentDirectory( new File( currDir ) ); + final int userSelection = fileChooser.showSaveDialog( null ); + if ( userSelection == JFileChooser.APPROVE_OPTION ) + { + File fileToSave = fileChooser.getSelectedFile(); + + // Ensure the file has the .xml extension + if ( !fileToSave.getAbsolutePath().endsWith( ".xml" ) ) + { + fileToSave = new File( fileToSave.getAbsolutePath() + ".xml" ); + } + + final SpimDataMinimal spimData = + new SpimDataMinimal( fileToSave.getParentFile(), sequenceDescription, viewRegistrations ); + + try + { + new XmlIoSpimDataMinimal().save( spimData, fileToSave.getAbsolutePath() ); + } + catch ( final SpimDataException e ) + { + e.printStackTrace(); + } + + if ( checkBDVFile( fileToSave ) ) + { + xmlFile = fileToSave; + btnCreate.setEnabled( true ); + } + } + } ); + SwingUtilities.getWindowAncestor( dialog.getJTree() ).setLocationRelativeTo( this ); + } + + boolean checkBDVFile( final File file ) + { + try + { + final SpimDataMinimal spimData = new XmlIoSpimDataMinimal().load( file.getAbsolutePath() ); + final String str = LauncherUtil.buildInfoString( spimData ); + labelInfo.setText( str ); + return true; + } + catch ( final SpimDataException | RuntimeException e ) + { + labelInfo.setText( "Invalid BDV xml file.
" + e.getMessage() + "" );
+ return false;
+ }
+ }
+
+ public static class N5ViewerReaderFun implements Function< String, N5Reader >
+ {
+
+ public String message;
+
+ @Override
+ public N5Reader apply( final String n5UriOrPath )
+ {
+ if ( n5UriOrPath == null || n5UriOrPath.isEmpty() )
+ return null;
+
+ String rootPath = null;
+ if ( n5UriOrPath.contains( "?" ) )
+ {
+ try
+ {
+ // need to strip off storage format for n5uri to correctly
+ // remove query;
+ final Pair< StorageFormat, URI > fmtUri = N5Factory.StorageFormat.parseUri( n5UriOrPath );
+ final StorageFormat format = fmtUri.getA();
+
+ final N5URI n5uri = new N5URI( URI.create( fmtUri.getB().toString() ) );
+ // add the format prefix back if it was present
+ rootPath = format == null ? n5uri.getContainerPath() : format.toString().toLowerCase() + ":" + n5uri.getContainerPath();
+ }
+ catch ( final URISyntaxException e )
+ {
+ SwingUtilities.invokeLater(
+ () -> IJ.error( "Invalid URI", "The URI is not valid or credentials are missing: " + n5UriOrPath ) );
+ }
+ }
+
+ if ( rootPath == null )
+ rootPath = upToLastExtension( n5UriOrPath );
+
+ N5Factory factory = new N5Factory().cacheAttributes( true );
+ try
+ {
+ return factory.openReader( rootPath );
+ }
+ catch ( final Exception e )
+ {}
+ // Use credentials
+ if ( AWSCredentialsManager.getInstance().getCredentials() == null )
+ AWSCredentialsManager.getInstance().setCredentials( AWSCredentialsTools.getBasicAWSCredentials() );
+ factory = factory.s3UseCredentials( AWSCredentialsManager.getInstance().getCredentials() );
+ try
+ {
+ return factory.openReader( rootPath );
+ }
+ catch ( final N5Exception e )
+ {
+ AWSCredentialsManager.getInstance().setCredentials( null );
+ IJ.handleException( e );
+ }
+ return null;
+ }
+ }
+
+ private static String upToLastExtension( final String path )
+ {
+
+ final int i = path.lastIndexOf( '.' );
+ if ( i >= 0 )
+ {
+ final int j = path.substring( i ).indexOf( '/' );
+ if ( j >= 0 )
+ return path.substring( 0, i + j );
+ else
+ return path;
+ }
+ else
+ return path;
+ }
+}
diff --git a/src/main/java/org/mastodon/mamut/launcher/NewMastodonProjectPanel.java b/src/main/java/org/mastodon/mamut/launcher/NewMastodonProjectPanel.java
index b4b4655dc..70598e5a6 100644
--- a/src/main/java/org/mastodon/mamut/launcher/NewMastodonProjectPanel.java
+++ b/src/main/java/org/mastodon/mamut/launcher/NewMastodonProjectPanel.java
@@ -82,7 +82,7 @@ public NewMastodonProjectPanel( final String panelTitle, final String buttonTitl
gbcLblNewMastodonProject.gridy = 0;
add( lblNewMastodonProject, gbcLblNewMastodonProject );
- rdbtBrowseToBDV = new JRadioButton( "Browse to a BDV file (xml/h5 pair):" );
+ rdbtBrowseToBDV = new JRadioButton( "Browse to a BDV file (xml + N5/OME-Zarr/HDF5 pair):" );
final GridBagConstraints gbc_rdbtBrowseToBDV = new GridBagConstraints();
gbc_rdbtBrowseToBDV.anchor = GridBagConstraints.SOUTHWEST;
gbc_rdbtBrowseToBDV.insets = new Insets( 5, 5, 5, 5 );
diff --git a/src/main/java/org/mastodon/mamut/model/ModelGraph.java b/src/main/java/org/mastodon/mamut/model/ModelGraph.java
index 256cb0ca9..5ac432ed9 100644
--- a/src/main/java/org/mastodon/mamut/model/ModelGraph.java
+++ b/src/main/java/org/mastodon/mamut/model/ModelGraph.java
@@ -76,6 +76,20 @@ public boolean addVertexLabelListener( final PropertyChangeListener< Spot > list
{
return vertexPool.label.propertyChangeListeners().add( listener );
}
+
+ /**
+ * Register a {@link PropertyChangeListener} that will be notified when
+ * a {@code Spot}s covariance is changed.
+ *
+ * @param listener
+ * the listener to register.
+ * @return {@code true} if the listener was successfully registered.
+ * {@code false} if it was already registered.
+ */
+ public boolean addVertexCovarianceListener( final PropertyChangeListener< Spot > listener )
+ {
+ return vertexPool.covariance.propertyChangeListeners().add( listener );
+ }
/**
* Removes the specified {@link PropertyChangeListener} from the set of
@@ -90,4 +104,19 @@ public boolean removeVertexLabelListener( final PropertyChangeListener< Spot > l
{
return vertexPool.label.propertyChangeListeners().remove( listener );
}
+
+ /**
+ * Removes the specified {@link PropertyChangeListener} from the set of
+ * vertex covariance listeners.
+ *
+ * @param listener
+ * the listener to remove.
+ * @return {@code true} if the listener was present in the listeners of this
+ * model and was successfully removed.
+ */
+ public boolean removeVertexCovarianceListener( final PropertyChangeListener< Spot > listener )
+ {
+ return vertexPool.covariance.propertyChangeListeners().remove( listener );
+ }
}
+
diff --git a/src/main/java/org/mastodon/mamut/model/ModelSerializer.java b/src/main/java/org/mastodon/mamut/model/ModelSerializer.java
index e2e16c856..afab35997 100644
--- a/src/main/java/org/mastodon/mamut/model/ModelSerializer.java
+++ b/src/main/java/org/mastodon/mamut/model/ModelSerializer.java
@@ -33,7 +33,7 @@
import org.mastodon.graph.ref.AbstractVertexPool;
import org.mastodon.pool.PoolObjectAttributeSerializer;
-class ModelSerializer implements GraphSerializer< Spot, Link >
+public class ModelSerializer implements GraphSerializer< Spot, Link >
{
private ModelSerializer()
{}
diff --git a/src/main/java/org/mastodon/mamut/model/Spot.java b/src/main/java/org/mastodon/mamut/model/Spot.java
index 694a3e146..ce3a212ba 100644
--- a/src/main/java/org/mastodon/mamut/model/Spot.java
+++ b/src/main/java/org/mastodon/mamut/model/Spot.java
@@ -29,6 +29,7 @@
package org.mastodon.mamut.model;
import org.mastodon.model.AbstractSpot;
+import org.mastodon.model.HasCovariance;
import org.mastodon.model.HasLabel;
import org.mastodon.pool.ByteMappedElement;
import org.mastodon.views.bdv.overlay.util.JamaEigenvalueDecomposition;
@@ -39,7 +40,7 @@
*
* @author Tobias Pietzsch
*/
-public final class Spot extends AbstractSpot< Spot, Link, SpotPool, ByteMappedElement, ModelGraph > implements HasLabel
+public final class Spot extends AbstractSpot< Spot, Link, SpotPool, ByteMappedElement, ModelGraph > implements HasLabel, HasCovariance
{
private final JamaEigenvalueDecomposition eig = new JamaEigenvalueDecomposition( 3 );
@@ -145,11 +146,13 @@ public Spot init( final int timepointId, final double[] pos, final double[][] co
return this;
}
+ @Override
public void getCovariance( final double[][] cov )
{
getCovarianceInternal( cov );
}
+ @Override
public void setCovariance( final double[][] cov )
{
pool.covariance.notifyBeforePropertyChange( this );
@@ -167,7 +170,7 @@ public double getBoundingSphereRadiusSquared()
@Override
public String getLabel()
{
- if ( pool.label.isSet( this ) )
+ if ( isLabelSet() )
return pool.label.get( this );
else
return Integer.toString( getInternalPoolIndex() );
@@ -179,6 +182,11 @@ public void setLabel( final String label )
pool.label.set( this, label );
}
+ public boolean isLabelSet()
+ {
+ return pool.label.isSet( this );
+ }
+
@Override
public String toString()
{
diff --git a/src/main/java/org/mastodon/mamut/model/branch/BranchGraphSynchronizer.java b/src/main/java/org/mastodon/mamut/model/branch/BranchGraphSynchronizer.java
index ba17b3996..f76e97651 100644
--- a/src/main/java/org/mastodon/mamut/model/branch/BranchGraphSynchronizer.java
+++ b/src/main/java/org/mastodon/mamut/model/branch/BranchGraphSynchronizer.java
@@ -55,10 +55,13 @@ public BranchGraphSynchronizer( final GraphListener< ?, ? > bg, final ReadLock r
this.bg = bg;
this.lock = readLock;
this.listeners = new Listeners.SynchronizedList<>();
+ this.uptodate = true;
}
public void sync()
{
+ if ( uptodate )
+ return;
lock.lock();
try
{
diff --git a/src/main/java/org/mastodon/mamut/views/AbstractMamutViewFactory.java b/src/main/java/org/mastodon/mamut/views/AbstractMamutViewFactory.java
index 4ed35466b..a8f491f77 100644
--- a/src/main/java/org/mastodon/mamut/views/AbstractMamutViewFactory.java
+++ b/src/main/java/org/mastodon/mamut/views/AbstractMamutViewFactory.java
@@ -240,11 +240,6 @@ private static void restoreFramePosition( final Window frame, final Map< String,
final int[] pos = ( int[] ) guiState.get( FRAME_POSITION_KEY );
if ( null != pos )
frame.setBounds( pos[ 0 ], pos[ 1 ], pos[ 2 ], pos[ 3 ] );
- else
- {
- frame.setSize( 650, 400 );
- frame.setLocationRelativeTo( null );
- }
}
private static void restoreGroupHandle( final GroupHandle groupHandle, final Map< String, Object > guiState )
diff --git a/src/main/java/org/mastodon/mamut/views/bdv/MamutViewBdv.java b/src/main/java/org/mastodon/mamut/views/bdv/MamutViewBdv.java
index 661eb1078..4eeeb12e5 100644
--- a/src/main/java/org/mastodon/mamut/views/bdv/MamutViewBdv.java
+++ b/src/main/java/org/mastodon/mamut/views/bdv/MamutViewBdv.java
@@ -63,6 +63,7 @@
import org.mastodon.model.HighlightModel;
import org.mastodon.model.NavigationHandler;
import org.mastodon.model.SelectionModel;
+import org.mastodon.ui.ExportViewActions;
import org.mastodon.ui.FocusActions;
import org.mastodon.ui.HighlightBehaviours;
import org.mastodon.ui.SelectionActions;
@@ -72,6 +73,7 @@
import org.mastodon.ui.coloring.GraphColorGeneratorAdapter;
import org.mastodon.ui.coloring.HasColorBarOverlay;
import org.mastodon.ui.coloring.HasColoringModel;
+import org.mastodon.ui.commandfinder.CommandFinder;
import org.mastodon.ui.keymap.KeyConfigContexts;
import org.mastodon.views.bdv.BdvContextProvider;
import org.mastodon.views.bdv.BigDataViewerActionsMamut;
@@ -187,6 +189,7 @@ public MamutViewBdv( final ProjectModel appModel )
selectionModel,
coloring );
+ viewer.getDisplay().overlays().add( colorBarOverlay );
viewer.getDisplay().overlays().add( tracksOverlay );
viewer.renderTransformListeners().add( tracksOverlay );
viewer.timePointListeners().add( tracksOverlay );
@@ -259,6 +262,8 @@ public MamutViewBdv( final ProjectModel appModel )
viewer.timePointListeners().add( timePointIndex -> timepointModel.setTimepoint( timePointIndex ) );
timepointModel.listeners().add( () -> viewer.setTimepoint( timepointModel.getTimepoint() ) );
+ ExportViewActions.install( viewActions, frame.getViewerPanel().getDisplayComponent(), frame, "BDV" );
+
final RenderSettingsManager renderSettingsManager = appModel.getWindowManager().getManager( RenderSettingsManager.class );
final RenderSettings renderSettings = renderSettingsManager.getForwardDefaultStyle();
tracksOverlay.setRenderSettings( renderSettings );
@@ -275,6 +280,21 @@ public MamutViewBdv( final ProjectModel appModel )
// Notifies context provider that context changes when visibility mode changes.
tracksOverlay.getVisibilities().getVisibilityListeners().add( contextProvider::notifyContextChanged );
+ // Command finder.
+ final CommandFinder cf = CommandFinder.build()
+ .context( appModel.getContext() )
+ .inputTriggerConfig( appModel.getKeymap().getConfig() )
+ .keyConfigContexts( keyConfigContexts )
+ .descriptionProvider( appModel.getWindowManager().getViewFactories().getCommandDescriptions() )
+ .register( viewActions )
+ .register( appModel.getModelActions() )
+ .register( appModel.getProjectActions() )
+ .register( appModel.getPlugins().getPluginActions() )
+ .modificationListeners( appModel.getKeymap().updateListeners() )
+ .parent( frame )
+ .installOn( viewActions );
+ cf.getDialog().setTitle( cf.getDialog().getTitle() + " - " + frame.getTitle() );
+
MainWindow.addMenus( menu, actionMap );
appModel.getWindowManager().addWindowMenu( menu, actionMap );
MamutMenuBuilder.build( menu, actionMap,
@@ -284,7 +304,10 @@ public MamutViewBdv( final ProjectModel appModel )
item( BigDataViewerActions.SAVE_SETTINGS ),
separator(),
item( RecordMovieDialog.RECORD_MOVIE_DIALOG ),
- item( RecordMaxProjectionMovieDialog.RECORD_MIP_MOVIE_DIALOG ) ),
+ item( RecordMaxProjectionMovieDialog.RECORD_MIP_MOVIE_DIALOG ),
+ separator(),
+ item( ExportViewActions.EXPORT_VIEW_TO_SVG ),
+ item( ExportViewActions.EXPORT_VIEW_TO_PNG ) ),
viewMenu(
separator(),
item( MastodonFrameViewActions.TOGGLE_SETTINGS_PANEL ) ),
diff --git a/src/main/java/org/mastodon/mamut/views/bdv/MamutViewBdvFactory.java b/src/main/java/org/mastodon/mamut/views/bdv/MamutViewBdvFactory.java
index c5c31d85a..8cbf3f9aa 100644
--- a/src/main/java/org/mastodon/mamut/views/bdv/MamutViewBdvFactory.java
+++ b/src/main/java/org/mastodon/mamut/views/bdv/MamutViewBdvFactory.java
@@ -39,8 +39,11 @@
import org.scijava.plugin.Plugin;
import bdv.tools.InitializeViewerState;
+import bdv.viewer.AbstractViewerPanel;
+import bdv.viewer.SynchronizedViewerState;
import bdv.viewer.ViewerPanel;
import bdv.viewer.ViewerState;
+import bdv.viewer.state.XmlIoViewerState;
import net.imglib2.realtransform.AffineTransform3D;
/**
@@ -115,10 +118,12 @@ public void restoreGuiState( final MamutViewBdv view, final Map< String, Object
restoreBdvGuiState( view.getViewerPanelMamut(), guiState );
}
- static void getBdvGuiState( final ViewerPanel viewerPanel, final Map< String, Object > guiState )
+ public static void getBdvGuiState( final AbstractViewerPanel viewerPanel, final Map< String, Object > guiState )
{
// Viewer state.
- final Element stateEl = viewerPanel.stateToXml();
+ @SuppressWarnings( "deprecation" )
+ final bdv.viewer.state.ViewerState deprecatedState = new bdv.viewer.state.ViewerState( ( SynchronizedViewerState ) viewerPanel.state() );
+ final Element stateEl = new XmlIoViewerState().toXml( deprecatedState );
guiState.put( BDV_STATE_KEY, stateEl );
// Transform.
final AffineTransform3D t = new AffineTransform3D();
@@ -126,13 +131,18 @@ static void getBdvGuiState( final ViewerPanel viewerPanel, final Map< String, Ob
guiState.put( BDV_TRANSFORM_KEY, t );
}
- static void restoreBdvGuiState( final ViewerPanel viewerPanel, final Map< String, Object > guiState )
+ public static void restoreBdvGuiState( final AbstractViewerPanel viewerPanel, final Map< String, Object > guiState )
{
// Restore BDV state.
final Element stateEl = ( Element ) guiState.get( BDV_STATE_KEY );
if ( null != stateEl )
- viewerPanel.stateFromXml( stateEl );
+ {
+ final XmlIoViewerState io = new XmlIoViewerState();
+ @SuppressWarnings( "deprecation" )
+ final bdv.viewer.state.ViewerState deprecatedState = new bdv.viewer.state.ViewerState( ( SynchronizedViewerState ) viewerPanel.state() );
+ io.restoreFromXml( stateEl.getChild( io.getTagName() ), deprecatedState );
+ }
// Restore transform.
final AffineTransform3D tLoaded = ( AffineTransform3D ) guiState.get( BDV_TRANSFORM_KEY );
if ( null == tLoaded )
diff --git a/src/main/java/org/mastodon/mamut/views/grapher/GrapherInitializer.java b/src/main/java/org/mastodon/mamut/views/grapher/GrapherInitializer.java
new file mode 100644
index 000000000..5f6ef4f98
--- /dev/null
+++ b/src/main/java/org/mastodon/mamut/views/grapher/GrapherInitializer.java
@@ -0,0 +1,287 @@
+package org.mastodon.mamut.views.grapher;
+
+import net.imglib2.loops.LoopBuilder;
+import org.apache.commons.lang3.function.TriFunction;
+import org.mastodon.Ref;
+import org.mastodon.app.ui.MastodonFrameViewActions;
+import org.mastodon.app.ui.SearchVertexLabel;
+import org.mastodon.app.ui.ViewMenu;
+import org.mastodon.app.ui.ViewMenuBuilder;
+import org.mastodon.graph.Edge;
+import org.mastodon.graph.Vertex;
+import org.mastodon.grouping.GroupHandle;
+import org.mastodon.mamut.MainWindow;
+import org.mastodon.mamut.MamutMenuBuilder;
+import org.mastodon.mamut.ProjectModel;
+import org.mastodon.mamut.UndoActions;
+import org.mastodon.mamut.model.Model;
+import org.mastodon.mamut.views.MamutViewI;
+import org.mastodon.model.AutoNavigateFocusModel;
+import org.mastodon.model.FocusModel;
+import org.mastodon.model.HasLabel;
+import org.mastodon.model.HighlightModel;
+import org.mastodon.model.NavigationHandler;
+import org.mastodon.model.SelectionModel;
+import org.mastodon.spatial.HasTimepoint;
+import org.mastodon.ui.EditTagActions;
+import org.mastodon.ui.ExportViewActions;
+import org.mastodon.ui.FocusActions;
+import org.mastodon.ui.SelectionActions;
+import org.mastodon.ui.coloring.ColorBarOverlay;
+import org.mastodon.ui.coloring.ColoringModel;
+import org.mastodon.ui.coloring.GraphColorGeneratorAdapter;
+import org.mastodon.views.context.ContextChooser;
+import org.mastodon.views.grapher.datagraph.DataContextListener;
+import org.mastodon.views.grapher.datagraph.DataEdge;
+import org.mastodon.views.grapher.datagraph.DataGraph;
+import org.mastodon.views.grapher.datagraph.DataGraphLayout;
+import org.mastodon.views.grapher.datagraph.DataVertex;
+import org.mastodon.views.grapher.display.DataDisplayFrame;
+import org.mastodon.views.grapher.display.DataDisplayOptions;
+import org.mastodon.views.grapher.display.DataDisplayPanel;
+import org.mastodon.views.grapher.display.DataDisplayZoom;
+import org.mastodon.views.grapher.display.FeatureGraphConfig;
+import org.mastodon.views.grapher.display.OffsetAxes;
+import org.mastodon.views.grapher.display.style.DataDisplayStyle;
+import org.mastodon.views.grapher.display.style.DataDisplayStyleManager;
+import org.mastodon.views.trackscheme.display.TrackSchemeNavigationActions;
+import org.scijava.ui.behaviour.util.Actions;
+import org.scijava.ui.behaviour.util.Behaviours;
+
+import javax.swing.ActionMap;
+import javax.swing.JPanel;
+import java.util.function.BiConsumer;
+
+import static org.mastodon.app.ui.ViewMenuBuilder.item;
+import static org.mastodon.app.ui.ViewMenuBuilder.separator;
+import static org.mastodon.mamut.MamutMenuBuilder.colorMenu;
+import static org.mastodon.mamut.MamutMenuBuilder.colorbarMenu;
+import static org.mastodon.mamut.MamutMenuBuilder.editMenu;
+import static org.mastodon.mamut.MamutMenuBuilder.fileMenu;
+import static org.mastodon.mamut.MamutMenuBuilder.tagSetMenu;
+import static org.mastodon.mamut.MamutMenuBuilder.viewMenu;
+
+public class GrapherInitializer< V extends Vertex< E > & HasTimepoint & HasLabel & Ref< V >, E extends Edge< V > & Ref< E > >
+{
+ private final DataGraph< V, E > viewGraph;
+
+ private final ProjectModel appModel;
+
+ private final Model model;
+
+ private final DataDisplayFrame< V, E > frame;
+
+ private final DataDisplayPanel< V, E > panel;
+
+ private final DataGraphLayout< V, E > layout;
+
+ private final DataDisplayStyle forwardDefaultStyle;
+
+ private final DataDisplayStyle.UpdateListener styleUpdateListener;
+
+ private final ContextChooser< V > contextChooser;
+
+ private ColoringModel coloringModel;
+
+ private ColorBarOverlay colorBarOverlay;
+
+ private final GraphColorGeneratorAdapter< V, E, DataVertex, DataEdge > coloringAdapter;
+
+ private final AutoNavigateFocusModel< DataVertex, DataEdge > navigateFocusModel;
+
+ private final SelectionModel< DataVertex, DataEdge > selectionModel;
+
+ private final NavigationHandler< DataVertex, DataEdge > navigationHandler;
+
+ private final FocusModel< DataVertex > focusModel;
+
+ GrapherInitializer( final DataGraph< V, E > graph, final ProjectModel appModel,
+ final SelectionModel< DataVertex, DataEdge > selectionModel, final NavigationHandler< DataVertex, DataEdge > navigationHandler,
+ final FocusModel< DataVertex > focusModel, final HighlightModel< DataVertex, DataEdge > highlightModel,
+ final GroupHandle groupHandle
+ )
+ {
+ this.viewGraph = graph;
+ this.appModel = appModel;
+ this.model = appModel.getModel();
+ this.selectionModel = selectionModel;
+ this.navigationHandler = navigationHandler;
+ this.focusModel = focusModel;
+
+ // The layout.
+ layout = new DataGraphLayout<>( viewGraph, selectionModel );
+
+ // ContextChooser
+ final DataContextListener< V > contextListener = new DataContextListener<>( viewGraph );
+ contextChooser = new ContextChooser<>( contextListener );
+
+ // Style
+ final DataDisplayStyleManager dataDisplayStyleManager = appModel.getWindowManager().getManager( DataDisplayStyleManager.class );
+ forwardDefaultStyle = dataDisplayStyleManager.getForwardDefaultStyle();
+
+ // A reference on the {@code GraphColorGeneratorAdapter} created and registered with this instance/window
+ coloringAdapter = new GraphColorGeneratorAdapter<>( viewGraph.getVertexMap(), viewGraph.getEdgeMap() );
+
+ // Options
+ final DataDisplayOptions< DataVertex, DataEdge > options = DataDisplayOptions.options();
+ options.shareKeyPressedEvents( appModel.getKeyPressedManager() ).style( forwardDefaultStyle )
+ .graphColorGenerator( coloringAdapter );
+
+ // Navigation
+ navigateFocusModel = new AutoNavigateFocusModel<>( focusModel, navigationHandler );
+
+ // Frame
+ this.frame = new DataDisplayFrame<>(
+ viewGraph,
+ appModel.getModel().getFeatureModel(),
+ appModel.getSharedBdvData().getSources().size(),
+ layout,
+ highlightModel,
+ navigateFocusModel,
+ selectionModel,
+ navigationHandler,
+ model,
+ groupHandle,
+ contextChooser,
+ options );
+
+ // Panel
+ this.panel = frame.getDataDisplayPanel();
+ contextListener.setContextListener( panel );
+
+ // Style listener
+ styleUpdateListener = panel::repaint;
+ forwardDefaultStyle.updateListeners().add( styleUpdateListener );
+
+ // Label listener
+ appModel.getModel().getGraph().addVertexLabelListener( vertex -> panel.entitiesAttributesChanged() );
+ }
+
+ void setOnClose( final MamutViewI view )
+ {
+ view.onClose( () -> forwardDefaultStyle.updateListeners().remove( styleUpdateListener ) );
+ }
+
+ void initFeatureConfig( final FeatureGraphConfig featureGraphConfig )
+ {
+ frame.getVertexSidePanel().setGraphConfig( featureGraphConfig );
+ panel.graphChanged();
+ }
+
+ void addMenusAndRegisterColors(
+ final TriFunction< ViewMenuBuilder.JMenuHandle, GraphColorGeneratorAdapter< V, E, DataVertex, DataEdge >,
+ DataDisplayPanel< V, E >, ColoringModel > colorModelRegistration,
+ final LoopBuilder.TriConsumer< ColorBarOverlay, ViewMenuBuilder.JMenuHandle,
+ DataDisplayPanel< V, E > > colorBarRegistration,
+ final BiConsumer< ViewMenuBuilder.JMenuHandle, DataDisplayPanel< V, E > > tagSetMenuRegistration,
+ final String[] keyConfigContexts )
+ {
+ final ViewMenu viewMenu = new ViewMenu( frame.getJMenuBar(), appModel.getKeymap(), keyConfigContexts );
+ final ActionMap actionMap = frame.getKeybindings().getConcatenatedActionMap();
+
+ final ViewMenuBuilder.JMenuHandle coloringMenuHandle = new ViewMenuBuilder.JMenuHandle();
+ final ViewMenuBuilder.JMenuHandle colorbarMenuHandle = new ViewMenuBuilder.JMenuHandle();
+ final ViewMenuBuilder.JMenuHandle tagSetMenuHandle = new ViewMenuBuilder.JMenuHandle();
+
+ MainWindow.addMenus( viewMenu, actionMap );
+ appModel.getWindowManager().addWindowMenu( viewMenu, actionMap );
+ MamutMenuBuilder.build( viewMenu, actionMap,
+ fileMenu(
+ separator(),
+ item( ExportViewActions.EXPORT_VIEW_TO_SVG ),
+ item( ExportViewActions.EXPORT_VIEW_TO_PNG ) ),
+ viewMenu(
+ colorMenu( coloringMenuHandle ),
+ colorbarMenu( colorbarMenuHandle ),
+ separator(),
+ item( MastodonFrameViewActions.TOGGLE_SETTINGS_PANEL ) ),
+ editMenu(
+ item( UndoActions.UNDO ),
+ item( UndoActions.REDO ),
+ separator(),
+ item( SelectionActions.DELETE_SELECTION ),
+ item( SelectionActions.SELECT_WHOLE_TRACK ),
+ item( SelectionActions.SELECT_TRACK_DOWNWARD ),
+ item( SelectionActions.SELECT_TRACK_UPWARD ),
+ separator(),
+ tagSetMenu( tagSetMenuHandle ) ) );
+ appModel.getPlugins().addMenus( viewMenu );
+
+ registerColoring( colorModelRegistration, coloringMenuHandle );
+ registerColorBar( colorBarRegistration, colorbarMenuHandle );
+ registerTagSetMenu( tagSetMenuRegistration, tagSetMenuHandle );
+ }
+
+ void layout()
+ {
+ layout.layout();
+ panel.repaint();
+ panel.getDisplay().requestFocusInWindow();
+ }
+
+ void installActions( final Actions viewActions, final Behaviours viewBehaviours )
+ {
+ MastodonFrameViewActions.install( viewActions, () -> frame );
+ FocusActions.install( viewActions, viewGraph, viewGraph.getLock(), navigateFocusModel, selectionModel );
+ EditTagActions.install( viewActions, frame.getKeybindings(), frame.getTriggerbindings(), model.getTagSetModel(),
+ appModel.getSelectionModel(), viewGraph.getLock(), panel, panel.getDisplay(), model );
+ DataDisplayZoom.install( viewBehaviours, panel );
+ ExportViewActions.install( viewActions, panel.getDisplay(), frame, frame.getTitle() );
+
+ panel.getNavigationActions().install( viewActions, TrackSchemeNavigationActions.NavigatorEtiquette.FINDER_LIKE );
+ panel.getNavigationBehaviours().install( viewBehaviours );
+ panel.getTransformEventHandler().install( viewBehaviours );
+ }
+
+ void addSearchPanel( final Actions viewActions )
+ {
+ final JPanel searchPanel =
+ SearchVertexLabel.install( viewActions, viewGraph, navigationHandler, selectionModel, focusModel, panel );
+ frame.getSettingsPanel().add( searchPanel );
+ }
+
+ private void
+ registerColoring(
+ final TriFunction< ViewMenuBuilder.JMenuHandle, GraphColorGeneratorAdapter< V, E, DataVertex, DataEdge >,
+ DataDisplayPanel< V, E >, ColoringModel > colorModelRegistration,
+ final ViewMenuBuilder.JMenuHandle coloringMenuHandle )
+ {
+ coloringModel = colorModelRegistration.apply( coloringMenuHandle, coloringAdapter, panel );
+ }
+
+ private void registerColorBar( final LoopBuilder.TriConsumer< ColorBarOverlay, ViewMenuBuilder.JMenuHandle,
+ DataDisplayPanel< V, E > > colorBarRegistration, final ViewMenuBuilder.JMenuHandle colorbarMenuHandle )
+ {
+ colorBarOverlay = new ColorBarOverlay( coloringModel, panel::getBackground );
+ final OffsetAxes offset = panel.getOffsetAxes();
+ offset.listeners().add( ( w, h ) -> colorBarOverlay.setInsets( 15, w + 15, h + 15, 15 ) );
+ colorBarRegistration.accept( colorBarOverlay, colorbarMenuHandle, panel );
+ panel.getDisplay().overlays().add( colorBarOverlay );
+ }
+
+ private void registerTagSetMenu( final BiConsumer< ViewMenuBuilder.JMenuHandle, DataDisplayPanel< V, E > > tagSetMenuRegistration,
+ final ViewMenuBuilder.JMenuHandle tagSetMenuHandle )
+ {
+ tagSetMenuRegistration.accept( tagSetMenuHandle, panel );
+ }
+
+ ContextChooser< V > getContextChooser()
+ {
+ return contextChooser;
+ }
+
+ DataDisplayFrame< V, E > getFrame()
+ {
+ return frame;
+ }
+
+ ColoringModel getColoringModel()
+ {
+ return coloringModel;
+ }
+
+ ColorBarOverlay getColorBarOverlay()
+ {
+ return colorBarOverlay;
+ }
+}
diff --git a/src/main/java/org/mastodon/mamut/views/grapher/MamutBranchViewGrapher.java b/src/main/java/org/mastodon/mamut/views/grapher/MamutBranchViewGrapher.java
new file mode 100644
index 000000000..55763f057
--- /dev/null
+++ b/src/main/java/org/mastodon/mamut/views/grapher/MamutBranchViewGrapher.java
@@ -0,0 +1,139 @@
+/*-
+ * #%L
+ * Mastodon
+ * %%
+ * Copyright (C) 2014 - 2024 Tobias Pietzsch, Jean-Yves Tinevez
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package org.mastodon.mamut.views.grapher;
+
+import net.imglib2.loops.LoopBuilder;
+import org.apache.commons.lang3.function.TriFunction;
+import org.mastodon.app.ui.ViewMenuBuilder.JMenuHandle;
+import org.mastodon.feature.FeatureProjectionSpec;
+import org.mastodon.mamut.ProjectModel;
+import org.mastodon.mamut.feature.branch.BranchDisplacementDurationFeature;
+import org.mastodon.mamut.model.branch.BranchLink;
+import org.mastodon.mamut.model.branch.BranchSpot;
+import org.mastodon.mamut.views.MamutBranchView;
+import org.mastodon.ui.coloring.ColorBarOverlay;
+import org.mastodon.ui.coloring.ColoringModel;
+import org.mastodon.ui.coloring.GraphColorGeneratorAdapter;
+import org.mastodon.ui.coloring.HasColorBarOverlay;
+import org.mastodon.ui.coloring.HasColoringModel;
+import org.mastodon.ui.keymap.KeyConfigContexts;
+import org.mastodon.views.context.ContextChooser;
+import org.mastodon.views.context.HasContextChooser;
+import org.mastodon.views.grapher.datagraph.DataEdge;
+import org.mastodon.views.grapher.datagraph.DataGraph;
+import org.mastodon.views.grapher.datagraph.DataVertex;
+import org.mastodon.views.grapher.display.DataDisplayFrame;
+import org.mastodon.views.grapher.display.DataDisplayPanel;
+import org.mastodon.views.grapher.display.FeatureGraphConfig;
+import org.mastodon.views.grapher.display.FeatureGraphConfig.GraphDataItemsSource;
+import org.mastodon.views.grapher.display.FeatureSpecPair;
+import org.mastodon.views.grapher.display.InertialScreenTransformEventHandler;
+
+import java.util.Iterator;
+import java.util.function.BiConsumer;
+
+public class MamutBranchViewGrapher extends MamutBranchView< DataGraph< BranchSpot, BranchLink >, DataVertex, DataEdge >
+ implements HasContextChooser< BranchSpot >, HasColoringModel, HasColorBarOverlay
+{
+
+ private final GrapherInitializer< BranchSpot, BranchLink > grapherInitializer;
+
+ MamutBranchViewGrapher( final ProjectModel appModel )
+ {
+ super( appModel,
+ new DataGraph<>(
+ appModel.getModel().getBranchGraph(),
+ appModel.getModel().getBranchGraphIdBimap(),
+ appModel.getModel().getGraph().getLock() ),
+ new String[] { KeyConfigContexts.GRAPHER } );
+
+ grapherInitializer = new GrapherInitializer<>( viewGraph, appModel, selectionModel, navigationHandler, focusModel, highlightModel,
+ getGroupHandle() );
+ grapherInitializer.getFrame().setTitle( "Grapher Branch" );
+ InertialScreenTransformEventHandler handler = grapherInitializer.getFrame().getDataDisplayPanel().getTransformEventHandler();
+ handler.setMinScaleX( 0.1d );
+ handler.setMinScaleY( 0.1d );
+ grapherInitializer.setOnClose( this );
+ grapherInitializer.initFeatureConfig( getFeatureGraphConfig() );
+ setFrame( grapherInitializer.getFrame() ); // this creates viewActions and viewBehaviours thus must be called before installActions
+ grapherInitializer.installActions( viewActions, viewBehaviours );
+ grapherInitializer.addSearchPanel( viewActions );
+
+ TriFunction< JMenuHandle, GraphColorGeneratorAdapter< BranchSpot, BranchLink, DataVertex, DataEdge >,
+ DataDisplayPanel< BranchSpot, BranchLink >, ColoringModel > colorModelRegistration =
+ ( menuHandle, coloringAdaptor, panel ) -> registerBranchColoring( coloringAdaptor, menuHandle,
+ panel::entitiesAttributesChanged );
+ LoopBuilder.TriConsumer< ColorBarOverlay, JMenuHandle, DataDisplayPanel< BranchSpot, BranchLink > > colorBarRegistration =
+ ( overlay, menuHandle, panel ) -> registerColorbarOverlay( overlay, menuHandle, panel::repaint );
+ BiConsumer< JMenuHandle, DataDisplayPanel< BranchSpot, BranchLink > > tagSetMenuRegistration =
+ ( menuHandle, panel ) -> registerTagSetMenu( menuHandle, panel::entitiesAttributesChanged );
+
+ grapherInitializer.addMenusAndRegisterColors( colorModelRegistration, colorBarRegistration, tagSetMenuRegistration,
+ keyConfigContexts );
+ grapherInitializer.layout();
+ }
+
+ private static FeatureGraphConfig getFeatureGraphConfig()
+ {
+ // If they are available, set some sensible defaults for the feature.
+ Iterator< FeatureProjectionSpec > projections = BranchDisplacementDurationFeature.SPEC.getProjectionSpecs().iterator();
+ FeatureProjectionSpec displacementSpec = projections.next();
+ FeatureProjectionSpec durationSpec = projections.next();
+ final FeatureSpecPair featureSpecX =
+ new FeatureSpecPair( BranchDisplacementDurationFeature.SPEC, displacementSpec, false, false );
+ final FeatureSpecPair featureSpecY =
+ new FeatureSpecPair( BranchDisplacementDurationFeature.SPEC, durationSpec, false, false );
+ return new FeatureGraphConfig( featureSpecX, featureSpecY, GraphDataItemsSource.TRACK_OF_SELECTION, true );
+ }
+
+ @SuppressWarnings( "unchecked" )
+ @Override
+ public DataDisplayFrame< BranchSpot, BranchLink > getFrame()
+ {
+ return ( DataDisplayFrame< BranchSpot, BranchLink > ) super.getFrame();
+ }
+
+ @Override
+ public ContextChooser< BranchSpot > getContextChooser()
+ {
+ return grapherInitializer.getContextChooser();
+ }
+
+ @Override
+ public ColoringModel getColoringModel()
+ {
+ return grapherInitializer.getColoringModel();
+ }
+
+ @Override
+ public ColorBarOverlay getColorBarOverlay()
+ {
+ return grapherInitializer.getColorBarOverlay();
+ }
+}
diff --git a/src/main/java/org/mastodon/mamut/views/grapher/MamutBranchViewGrapherFactory.java b/src/main/java/org/mastodon/mamut/views/grapher/MamutBranchViewGrapherFactory.java
new file mode 100644
index 000000000..844765eb6
--- /dev/null
+++ b/src/main/java/org/mastodon/mamut/views/grapher/MamutBranchViewGrapherFactory.java
@@ -0,0 +1,43 @@
+package org.mastodon.mamut.views.grapher;
+
+import org.mastodon.mamut.ProjectModel;
+import org.mastodon.mamut.views.AbstractMamutViewFactory;
+import org.mastodon.mamut.views.MamutViewFactory;
+import org.scijava.Priority;
+import org.scijava.plugin.Plugin;
+
+@Plugin( type = MamutViewFactory.class, priority = Priority.NORMAL - 8 )
+public class MamutBranchViewGrapherFactory extends AbstractMamutViewFactory< MamutBranchViewGrapher >
+{
+ public static final String NEW_GRAPHER_BRANCH_VIEW = "new grapher branch view";
+
+ @Override
+ public MamutBranchViewGrapher create( final ProjectModel projectModel )
+ {
+ return new MamutBranchViewGrapher( projectModel );
+ }
+
+ @Override
+ public String getCommandName()
+ {
+ return NEW_GRAPHER_BRANCH_VIEW;
+ }
+
+ @Override
+ public String getCommandDescription()
+ {
+ return "Open a new Grapher Branch view.";
+ }
+
+ @Override
+ public String getCommandMenuText()
+ {
+ return "New Grapher Branch View";
+ }
+
+ @Override
+ public Class< MamutBranchViewGrapher > getViewClass()
+ {
+ return MamutBranchViewGrapher.class;
+ }
+}
diff --git a/src/main/java/org/mastodon/mamut/views/grapher/MamutViewGrapher.java b/src/main/java/org/mastodon/mamut/views/grapher/MamutViewGrapher.java
index 77551e287..e48c091c5 100644
--- a/src/main/java/org/mastodon/mamut/views/grapher/MamutViewGrapher.java
+++ b/src/main/java/org/mastodon/mamut/views/grapher/MamutViewGrapher.java
@@ -1,244 +1,94 @@
-/*-
- * #%L
- * Mastodon
- * %%
- * Copyright (C) 2014 - 2024 Tobias Pietzsch, Jean-Yves Tinevez
- * %%
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- * #L%
- */
package org.mastodon.mamut.views.grapher;
-import static org.mastodon.app.ui.ViewMenuBuilder.item;
-import static org.mastodon.app.ui.ViewMenuBuilder.separator;
-import static org.mastodon.mamut.MamutMenuBuilder.colorMenu;
-import static org.mastodon.mamut.MamutMenuBuilder.colorbarMenu;
-import static org.mastodon.mamut.MamutMenuBuilder.editMenu;
-import static org.mastodon.mamut.MamutMenuBuilder.tagSetMenu;
-import static org.mastodon.mamut.MamutMenuBuilder.viewMenu;
+import java.util.function.BiConsumer;
-import javax.swing.ActionMap;
-import javax.swing.JPanel;
-
-import org.mastodon.app.ui.MastodonFrameViewActions;
-import org.mastodon.app.ui.SearchVertexLabel;
-import org.mastodon.app.ui.ViewMenu;
-import org.mastodon.app.ui.ViewMenuBuilder.JMenuHandle;
-import org.mastodon.mamut.MainWindow;
-import org.mastodon.mamut.MamutMenuBuilder;
+import org.apache.commons.lang3.function.TriFunction;
+import org.mastodon.app.ui.ViewMenuBuilder;
import org.mastodon.mamut.ProjectModel;
-import org.mastodon.mamut.UndoActions;
import org.mastodon.mamut.feature.SpotFrameFeature;
import org.mastodon.mamut.feature.SpotQuickMeanIntensityFeature;
import org.mastodon.mamut.model.Link;
-import org.mastodon.mamut.model.Model;
import org.mastodon.mamut.model.Spot;
-import org.mastodon.mamut.model.branch.BranchLink;
-import org.mastodon.mamut.model.branch.BranchSpot;
import org.mastodon.mamut.views.MamutView;
-import org.mastodon.model.AutoNavigateFocusModel;
-import org.mastodon.ui.EditTagActions;
-import org.mastodon.ui.FocusActions;
-import org.mastodon.ui.SelectionActions;
import org.mastodon.ui.coloring.ColorBarOverlay;
-import org.mastodon.ui.coloring.ColoringModelMain;
+import org.mastodon.ui.coloring.ColoringModel;
import org.mastodon.ui.coloring.GraphColorGeneratorAdapter;
import org.mastodon.ui.coloring.HasColorBarOverlay;
import org.mastodon.ui.coloring.HasColoringModel;
+import org.mastodon.ui.commandfinder.CommandFinder;
import org.mastodon.ui.keymap.KeyConfigContexts;
import org.mastodon.views.context.ContextChooser;
import org.mastodon.views.context.HasContextChooser;
-import org.mastodon.views.grapher.datagraph.DataContextListener;
import org.mastodon.views.grapher.datagraph.DataEdge;
import org.mastodon.views.grapher.datagraph.DataGraph;
-import org.mastodon.views.grapher.datagraph.DataGraphLayout;
import org.mastodon.views.grapher.datagraph.DataVertex;
import org.mastodon.views.grapher.display.DataDisplayFrame;
-import org.mastodon.views.grapher.display.DataDisplayOptions;
import org.mastodon.views.grapher.display.DataDisplayPanel;
-import org.mastodon.views.grapher.display.DataDisplayZoom;
import org.mastodon.views.grapher.display.FeatureGraphConfig;
-import org.mastodon.views.grapher.display.FeatureGraphConfig.GraphDataItemsSource;
import org.mastodon.views.grapher.display.FeatureSpecPair;
-import org.mastodon.views.grapher.display.OffsetAxes;
-import org.mastodon.views.grapher.display.style.DataDisplayStyle;
-import org.mastodon.views.grapher.display.style.DataDisplayStyleManager;
-import org.mastodon.views.trackscheme.display.TrackSchemeNavigationActions;
-import org.scijava.ui.behaviour.KeyPressedManager;
-
-public class MamutViewGrapher extends MamutView< DataGraph< Spot, Link >, DataVertex, DataEdge > implements HasContextChooser< Spot >, HasColoringModel, HasColorBarOverlay
-{
- private final ContextChooser< Spot > contextChooser;
+import net.imglib2.loops.LoopBuilder;
- /**
- * a reference on the {@code GraphColorGeneratorAdapter} created and
- * registered with this instance/window
- */
- private final GraphColorGeneratorAdapter< Spot, Link, DataVertex, DataEdge > coloringAdapter;
-
- /**
- * a reference on a supervising instance of the {@code ColoringModel} that
- * is bound to this instance/window
- */
- private final ColoringModelMain< Spot, Link, BranchSpot, BranchLink > coloringModel;
+public class MamutViewGrapher extends MamutView< DataGraph< Spot, Link >, DataVertex, DataEdge >
+ implements HasContextChooser< Spot >, HasColoringModel, HasColorBarOverlay
+{
- private final ColorBarOverlay colorbarOverlay;
+ private final GrapherInitializer< Spot, Link > grapherInitializer;
- public MamutViewGrapher( final ProjectModel appModel )
+ MamutViewGrapher( final ProjectModel appModel )
{
super( appModel,
- new DataGraph< Spot, Link >(
+ new DataGraph<>(
appModel.getModel().getGraph(),
appModel.getModel().getGraphIdBimap(),
- appModel.getModel().getGraph().getLock() ),
+ appModel.getModel().getGraph().getLock()
+ ),
new String[] { KeyConfigContexts.GRAPHER } );
- final KeyPressedManager keyPressedManager = appModel.getKeyPressedManager();
- final Model model = appModel.getModel();
-
- /*
- * The layout.
- */
- final DataGraphLayout< Spot, Link > layout = new DataGraphLayout<>( viewGraph, selectionModel );
- /*
- * ContextChooser
- */
- final DataContextListener< Spot > contextListener = new DataContextListener<>( viewGraph );
- contextChooser = new ContextChooser<>( contextListener );
-
- /*
- * Show the frame
- */
-
- final DataDisplayStyleManager dataDisplayStyleManager = appModel.getWindowManager().getManager( DataDisplayStyleManager.class );
- final DataDisplayStyle forwardDefaultStyle = dataDisplayStyleManager.getForwardDefaultStyle();
- coloringAdapter = new GraphColorGeneratorAdapter<>( viewGraph.getVertexMap(), viewGraph.getEdgeMap() );
- final DataDisplayOptions options = DataDisplayOptions.options()
- .shareKeyPressedEvents( keyPressedManager )
- .style( forwardDefaultStyle )
- .graphColorGenerator( coloringAdapter );
- final AutoNavigateFocusModel< DataVertex, DataEdge > navigateFocusModel =
- new AutoNavigateFocusModel<>( focusModel, navigationHandler );
-
- final DataDisplayFrame< Spot, Link > frame = new DataDisplayFrame< Spot, Link >(
- viewGraph,
- appModel.getModel().getFeatureModel(),
- appModel.getSharedBdvData().getSources().size(),
- layout,
- highlightModel,
- navigateFocusModel,
- selectionModel,
- navigationHandler,
- model,
- groupHandle,
- contextChooser,
- options );
- final DataDisplayPanel< Spot, Link > dataDisplayPanel = frame.getDataDisplayPanel();
+ grapherInitializer = new GrapherInitializer<>( viewGraph, appModel, selectionModel, navigationHandler, focusModel, highlightModel,
+ getGroupHandle() );
+ grapherInitializer.setOnClose( this );
+ grapherInitializer.initFeatureConfig( getFeatureGraphConfig() );
+ setFrame( grapherInitializer.getFrame() ); // this creates viewActions and viewBehaviours thus must be called before installActions
+ grapherInitializer.installActions( viewActions, viewBehaviours );
+ grapherInitializer.addSearchPanel( viewActions );
+
+ final TriFunction< ViewMenuBuilder.JMenuHandle, GraphColorGeneratorAdapter< Spot, Link, DataVertex, DataEdge >,
+ DataDisplayPanel< Spot, Link >, ColoringModel > colorModelRegistration = ( menuHandle, coloringAdaptor,
+ panel ) -> registerColoring( coloringAdaptor, menuHandle, panel::entitiesAttributesChanged );
+ final LoopBuilder.TriConsumer< ColorBarOverlay, ViewMenuBuilder.JMenuHandle, DataDisplayPanel< Spot, Link > > colorBarRegistration =
+ ( overlay, menuHandle, panel ) -> registerColorbarOverlay( overlay, menuHandle, panel::repaint );
+ final BiConsumer< ViewMenuBuilder.JMenuHandle, DataDisplayPanel< Spot, Link > > tagSetMenuRegistration =
+ ( menuHandle, panel ) -> registerTagSetMenu( menuHandle, panel::entitiesAttributesChanged );
+
+ grapherInitializer.addMenusAndRegisterColors( colorModelRegistration, colorBarRegistration, tagSetMenuRegistration,
+ keyConfigContexts );
+ grapherInitializer.layout();
+
+ final CommandFinder cf = CommandFinder.build()
+ .context( appModel.getContext() )
+ .inputTriggerConfig( appModel.getKeymap().getConfig() )
+ .keyConfigContexts( keyConfigContexts )
+ .descriptionProvider( appModel.getWindowManager().getViewFactories().getCommandDescriptions() )
+ .register( viewActions )
+ .register( appModel.getModelActions() )
+ .register( appModel.getProjectActions() )
+ .register( appModel.getPlugins().getPluginActions() )
+ .modificationListeners( appModel.getKeymap().updateListeners() )
+ .parent( frame )
+ .installOn( viewActions );
+ cf.getDialog().setTitle( cf.getDialog().getTitle() + " - " + frame.getTitle() );
+ }
+ private static FeatureGraphConfig getFeatureGraphConfig()
+ {
// If they are available, set some sensible defaults for the feature.
final FeatureSpecPair spvx = new FeatureSpecPair( SpotFrameFeature.SPEC,
SpotFrameFeature.SPEC.getProjectionSpecs().iterator().next(), false, false );
final FeatureSpecPair spvy = new FeatureSpecPair( SpotQuickMeanIntensityFeature.SPEC,
SpotQuickMeanIntensityFeature.PROJECTION_SPEC, 0, false, false );
- final FeatureGraphConfig gcv =
- new FeatureGraphConfig( spvx, spvy, GraphDataItemsSource.TRACK_OF_SELECTION, true );
- frame.getVertexSidePanel().setGraphConfig( gcv );
-
- dataDisplayPanel.graphChanged();
- contextListener.setContextListener( dataDisplayPanel );
-
- final DataDisplayStyle.UpdateListener updateListener = () -> dataDisplayPanel.repaint();
- forwardDefaultStyle.updateListeners().add( updateListener );
- onClose( () -> forwardDefaultStyle.updateListeners().remove( updateListener ) );
-
- setFrame( frame );
-
- MastodonFrameViewActions.install( viewActions, this );
- FocusActions.install( viewActions, viewGraph, viewGraph.getLock(), navigateFocusModel, selectionModel );
- EditTagActions.install( viewActions, frame.getKeybindings(), frame.getTriggerbindings(), model.getTagSetModel(),
- appModel.getSelectionModel(), viewGraph.getLock(), dataDisplayPanel, dataDisplayPanel.getDisplay(),
- model );
- DataDisplayZoom.install( viewBehaviours, dataDisplayPanel );
-
- final JPanel searchPanel = SearchVertexLabel.install( viewActions, viewGraph, navigationHandler, selectionModel,
- focusModel, dataDisplayPanel );
- frame.getSettingsPanel().add( searchPanel );
-
- dataDisplayPanel.getNavigationActions().install( viewActions,
- TrackSchemeNavigationActions.NavigatorEtiquette.FINDER_LIKE );
- dataDisplayPanel.getNavigationBehaviours().install( viewBehaviours );
- dataDisplayPanel.getTransformEventHandler().install( viewBehaviours );
-
- /*
- * Menus
- */
- final ViewMenu menu = new ViewMenu( this );
- final ActionMap actionMap = frame.getKeybindings().getConcatenatedActionMap();
-
- final JMenuHandle coloringMenuHandle = new JMenuHandle();
- final JMenuHandle tagSetMenuHandle = new JMenuHandle();
- final JMenuHandle colorbarMenuHandle = new JMenuHandle();
-
- MainWindow.addMenus( menu, actionMap );
- appModel.getWindowManager().addWindowMenu( menu, actionMap );
- MamutMenuBuilder.build( menu, actionMap,
- viewMenu(
- colorMenu( coloringMenuHandle ),
- colorbarMenu( colorbarMenuHandle ),
- separator(),
- item( MastodonFrameViewActions.TOGGLE_SETTINGS_PANEL ) ),
- editMenu(
- item( UndoActions.UNDO ),
- item( UndoActions.REDO ),
- separator(),
- item( SelectionActions.DELETE_SELECTION ),
- item( SelectionActions.SELECT_WHOLE_TRACK ),
- item( SelectionActions.SELECT_TRACK_DOWNWARD ),
- item( SelectionActions.SELECT_TRACK_UPWARD ),
- separator(),
- tagSetMenu( tagSetMenuHandle ) ) );
- appModel.getPlugins().addMenus( menu );
-
- /*
- * Coloring & colobar.
- */
- coloringModel = registerColoring( coloringAdapter, coloringMenuHandle,
- () -> dataDisplayPanel.entitiesAttributesChanged() );
- registerTagSetMenu( tagSetMenuHandle,
- () -> dataDisplayPanel.entitiesAttributesChanged() );
- colorbarOverlay = new ColorBarOverlay( coloringModel, () -> dataDisplayPanel.getBackground() );
- final OffsetAxes offset = dataDisplayPanel.getOffsetAxes();
- offset.listeners().add( ( w, h ) -> colorbarOverlay.setInsets( 15, w + 15, h + 15, 15 ) );
- registerColorbarOverlay( colorbarOverlay, colorbarMenuHandle, () -> dataDisplayPanel.repaint() );
- dataDisplayPanel.getDisplay().overlays().add( colorbarOverlay );
-
- // Listen to label changes.
- model.getGraph().addVertexLabelListener( v -> dataDisplayPanel.entitiesAttributesChanged() );
-
- layout.layout();
- dataDisplayPanel.repaint();
- dataDisplayPanel.getDisplay().requestFocusInWindow();
+ return new FeatureGraphConfig( spvx, spvy, FeatureGraphConfig.GraphDataItemsSource.TRACK_OF_SELECTION, true );
}
@SuppressWarnings( "unchecked" )
@@ -251,18 +101,18 @@ public DataDisplayFrame< Spot, Link > getFrame()
@Override
public ContextChooser< Spot > getContextChooser()
{
- return contextChooser;
+ return grapherInitializer.getContextChooser();
}
@Override
- public ColoringModelMain< Spot, Link, BranchSpot, BranchLink > getColoringModel()
+ public ColoringModel getColoringModel()
{
- return coloringModel;
+ return grapherInitializer.getColoringModel();
}
@Override
public ColorBarOverlay getColorBarOverlay()
{
- return colorbarOverlay;
+ return grapherInitializer.getColorBarOverlay();
}
}
diff --git a/src/main/java/org/mastodon/mamut/views/table/MamutViewTable.java b/src/main/java/org/mastodon/mamut/views/table/MamutViewTable.java
index 2c82ca6f7..f1e2e53f2 100644
--- a/src/main/java/org/mastodon/mamut/views/table/MamutViewTable.java
+++ b/src/main/java/org/mastodon/mamut/views/table/MamutViewTable.java
@@ -30,6 +30,7 @@
import static org.mastodon.app.MastodonIcons.TABLE_VIEW_ICON;
import static org.mastodon.app.ui.ViewMenuBuilder.item;
+import static org.mastodon.app.ui.ViewMenuBuilder.menu;
import static org.mastodon.app.ui.ViewMenuBuilder.separator;
import static org.mastodon.mamut.MamutMenuBuilder.editMenu;
import static org.mastodon.mamut.MamutMenuBuilder.fileMenu;
@@ -87,6 +88,7 @@
import org.mastodon.ui.coloring.TagSetGraphColorGenerator;
import org.mastodon.ui.coloring.TrackGraphColorGenerator;
import org.mastodon.ui.coloring.feature.FeatureColorModeManager;
+import org.mastodon.ui.commandfinder.CommandFinder;
import org.mastodon.ui.keymap.KeyConfigContexts;
import org.mastodon.views.context.ContextChooser;
import org.mastodon.views.context.HasContextChooser;
@@ -100,6 +102,10 @@ public class MamutViewTable extends MamutView< ViewGraph< Spot, Link, Spot, Link
implements HasContextChooser< Spot >, HasColoringModel
{
+ private static final int DEFAULT_WIDTH = 500;
+
+ private static final int DEFAULT_HEIGHT = 300;
+
public static String csvExportPath = null;
private static final String[] CONTEXTS = new String[] { KeyConfigContexts.TABLE };
@@ -163,6 +169,10 @@ protected MamutViewTable( final ProjectModel projectModel, final boolean selecti
.navigationHandler( branchGraphNavigation( projectModel, navigationHandler ) )
.done()
.title( selectionTable ? "Selection table" : "Data table" )
+ .x( -1 )
+ .y( -1 )
+ .width( DEFAULT_WIDTH )
+ .height( DEFAULT_HEIGHT )
.get();
setFrame( frame );
frame.setIconImages( TABLE_VIEW_ICON );
@@ -175,6 +185,19 @@ protected MamutViewTable( final ProjectModel projectModel, final boolean selecti
// Table actions.
MastodonFrameViewActions.install( viewActions, this );
TableViewActions.install( viewActions, frame );
+ final CommandFinder cf = CommandFinder.build()
+ .context( appModel.getContext() )
+ .inputTriggerConfig( appModel.getKeymap().getConfig() )
+ .keyConfigContexts( keyConfigContexts )
+ .descriptionProvider( appModel.getWindowManager().getViewFactories().getCommandDescriptions() )
+ .register( viewActions )
+ .register( appModel.getModelActions() )
+ .register( appModel.getProjectActions() )
+ .register( appModel.getPlugins().getPluginActions() )
+ .modificationListeners( appModel.getKeymap().updateListeners() )
+ .parent( frame )
+ .installOn( viewActions );
+ cf.getDialog().setTitle( cf.getDialog().getTitle() + " - " + frame.getTitle() );
// Menus
final ViewMenu menu = new ViewMenu( frame.getJMenuBar(), projectModel.getKeymap(), CONTEXTS );
@@ -186,11 +209,11 @@ protected MamutViewTable( final ProjectModel projectModel, final boolean selecti
appModel.getWindowManager().addWindowMenu( menu, actionMap );
MamutMenuBuilder.build( menu, actionMap,
fileMenu(
- separator(),
- item( TableViewActions.EXPORT_TO_CSV ) ),
+ menu( "Export",
+ item( TableViewActions.EXPORT_TO_CSV ) ) ),
viewMenu(
MamutMenuBuilder.colorMenu( colorMenuHandle ),
- ViewMenuBuilder.menu( "Branch coloring", colorBranchMenuHandle ),
+ menu( "Branch coloring", colorBranchMenuHandle ),
separator(),
item( MastodonFrameViewActions.TOGGLE_SETTINGS_PANEL ) ),
editMenu(
@@ -206,7 +229,7 @@ protected MamutViewTable( final ProjectModel projectModel, final boolean selecti
separator(),
item( TableViewActions.EDIT_LABEL ),
item( TableViewActions.TOGGLE_TAG ) ),
- ViewMenuBuilder.menu( "Settings",
+ menu( "Settings",
item( BigDataViewerActions.BRIGHTNESS_SETTINGS ),
item( BigDataViewerActions.VISIBILITY_AND_GROUPING ) ) );
projectModel.getPlugins().addMenus( menu );
diff --git a/src/main/java/org/mastodon/mamut/views/trackscheme/MamutBranchViewTrackScheme.java b/src/main/java/org/mastodon/mamut/views/trackscheme/MamutBranchViewTrackScheme.java
index 0967f4830..4b7bbd7d6 100644
--- a/src/main/java/org/mastodon/mamut/views/trackscheme/MamutBranchViewTrackScheme.java
+++ b/src/main/java/org/mastodon/mamut/views/trackscheme/MamutBranchViewTrackScheme.java
@@ -33,6 +33,7 @@
import static org.mastodon.mamut.MamutMenuBuilder.branchColorMenu;
import static org.mastodon.mamut.MamutMenuBuilder.colorbarMenu;
import static org.mastodon.mamut.MamutMenuBuilder.editMenu;
+import static org.mastodon.mamut.MamutMenuBuilder.fileMenu;
import static org.mastodon.mamut.MamutMenuBuilder.tagSetMenu;
import static org.mastodon.mamut.MamutMenuBuilder.viewMenu;
@@ -66,6 +67,7 @@
import org.mastodon.model.RootsModel;
import org.mastodon.model.TimepointModel;
import org.mastodon.ui.EditTagActions;
+import org.mastodon.ui.ExportViewActions;
import org.mastodon.ui.FocusActions;
import org.mastodon.ui.SelectionActions;
import org.mastodon.ui.coloring.ColorBarOverlay;
@@ -73,6 +75,7 @@
import org.mastodon.ui.coloring.GraphColorGeneratorAdapter;
import org.mastodon.ui.coloring.HasColorBarOverlay;
import org.mastodon.ui.coloring.HasColoringModel;
+import org.mastodon.ui.commandfinder.CommandFinder;
import org.mastodon.ui.keymap.KeyConfigContexts;
import org.mastodon.views.trackscheme.LineageTreeLayout;
import org.mastodon.views.trackscheme.LongEdgesLineageTreeLayout;
@@ -120,7 +123,7 @@ protected MamutBranchViewTrackScheme(
final TimepointModel timepointModel )
{
super( appModel, trackSchemeGraphFactory.createViewGraph( appModel ),
- new String[] { KeyConfigContexts.TRACKSCHEME } );
+ new String[] { KeyConfigContexts.TRACKSCHEME, KeyConfigContexts.MASTODON } );
// TrackScheme options.
final GraphColorGeneratorAdapter< BranchSpot, BranchLink, TrackSchemeVertex, TrackSchemeEdge > coloringAdapter = new GraphColorGeneratorAdapter<>( viewGraph.getVertexMap(), viewGraph.getEdgeMap() );
@@ -187,6 +190,7 @@ protected MamutBranchViewTrackScheme(
frame.getTrackschemePanel().getDisplay(), model );
ShowSelectedTracksActions.install( viewActions, viewGraph, selectionModel, rootsModel,
frame.getTrackschemePanel() );
+ ExportViewActions.install( viewActions, frame.getTrackschemePanel().getDisplay(), frame, "TrackScheme Branch" );
frame.getTrackschemePanel().getNavigationActions().install( viewActions,
TrackSchemeNavigationActions.NavigatorEtiquette.FINDER_LIKE );
@@ -208,6 +212,21 @@ protected MamutBranchViewTrackScheme(
frame.getTrackschemePanel().graphChanged();
} );
+ // Command finder.
+ final CommandFinder cf = CommandFinder.build()
+ .context( appModel.getContext() )
+ .inputTriggerConfig( appModel.getKeymap().getConfig() )
+ .descriptionProvider( appModel.getWindowManager().getViewFactories().getCommandDescriptions() )
+ .keyConfigContexts( keyConfigContexts )
+ .register( viewActions )
+ .register( appModel.getModelActions() )
+ .register( appModel.getProjectActions() )
+ .register( appModel.getPlugins().getPluginActions() )
+ .modificationListeners( appModel.getKeymap().updateListeners() )
+ .parent( frame )
+ .installOn( viewActions );
+ cf.getDialog().setTitle( cf.getDialog().getTitle() + " - " + frame.getTitle() );
+
// Menus.
final ViewMenu menu = new ViewMenu( frame.getJMenuBar(), appModel.getKeymap(), keyConfigContexts );
final ActionMap actionMap = frame.getKeybindings().getConcatenatedActionMap();
@@ -219,6 +238,10 @@ protected MamutBranchViewTrackScheme(
MainWindow.addMenus( menu, actionMap );
appModel.getWindowManager().addWindowMenu( menu, actionMap );
MamutMenuBuilder.build( menu, actionMap,
+ fileMenu(
+ separator(),
+ item( ExportViewActions.EXPORT_VIEW_TO_SVG ),
+ item( ExportViewActions.EXPORT_VIEW_TO_PNG ) ),
viewMenu(
branchColorMenu( coloringMenuHandle ),
colorbarMenu( colorbarMenuHandle ),
diff --git a/src/main/java/org/mastodon/mamut/views/trackscheme/MamutViewTrackScheme.java b/src/main/java/org/mastodon/mamut/views/trackscheme/MamutViewTrackScheme.java
index bddb46317..318072f40 100644
--- a/src/main/java/org/mastodon/mamut/views/trackscheme/MamutViewTrackScheme.java
+++ b/src/main/java/org/mastodon/mamut/views/trackscheme/MamutViewTrackScheme.java
@@ -33,6 +33,7 @@
import static org.mastodon.mamut.MamutMenuBuilder.colorMenu;
import static org.mastodon.mamut.MamutMenuBuilder.colorbarMenu;
import static org.mastodon.mamut.MamutMenuBuilder.editMenu;
+import static org.mastodon.mamut.MamutMenuBuilder.fileMenu;
import static org.mastodon.mamut.MamutMenuBuilder.tagSetMenu;
import static org.mastodon.mamut.MamutMenuBuilder.viewMenu;
@@ -63,6 +64,7 @@
import org.mastodon.model.DefaultRootsModel;
import org.mastodon.model.RootsModel;
import org.mastodon.ui.EditTagActions;
+import org.mastodon.ui.ExportViewActions;
import org.mastodon.ui.FocusActions;
import org.mastodon.ui.HighlightBehaviours;
import org.mastodon.ui.SelectionActions;
@@ -71,6 +73,7 @@
import org.mastodon.ui.coloring.GraphColorGeneratorAdapter;
import org.mastodon.ui.coloring.HasColorBarOverlay;
import org.mastodon.ui.coloring.HasColoringModel;
+import org.mastodon.ui.commandfinder.CommandFinder;
import org.mastodon.ui.keymap.KeyConfigContexts;
import org.mastodon.views.context.ContextChooser;
import org.mastodon.views.context.HasContextChooser;
@@ -144,6 +147,7 @@ public MamutViewTrackScheme( final ProjectModel appModel )
new AutoNavigateFocusModel<>( focusModel, navigationHandler, timepointModel );
final RootsModel< TrackSchemeVertex > rootsModel = new DefaultRootsModel<>( model.getGraph(), viewGraph );
+ onClose( () -> rootsModel.close() );
final FadingModelAdapter< Spot, Link, TrackSchemeVertex, TrackSchemeEdge > fadingModelAdapter =
new FadingModelAdapter<>( null, viewGraph.getVertexMap(), viewGraph.getEdgeMap() );
@@ -182,6 +186,7 @@ public MamutViewTrackScheme( final ProjectModel appModel )
appModel.getSelectionModel(), viewGraph.getLock(), frame.getTrackschemePanel(),
frame.getTrackschemePanel().getDisplay(), model );
ShowSelectedTracksActions.install( viewActions, viewGraph, selectionModel, rootsModel, frame.getTrackschemePanel() );
+ ExportViewActions.install( viewActions, frame.getTrackschemePanel().getDisplay(), frame, "TrackScheme" );
// Timepoint and number of spots.
final TimepointAndNumberOfSpotsPanel timepointAndNumberOfSpotsPanel = new TimepointAndNumberOfSpotsPanel( timepointModel, model );
@@ -197,6 +202,21 @@ public MamutViewTrackScheme( final ProjectModel appModel )
frame.getTrackschemePanel().getNavigationBehaviours().install( viewBehaviours );
frame.getTrackschemePanel().getTransformEventHandler().install( viewBehaviours );
+ // Command finder.
+ final CommandFinder cf = CommandFinder.build()
+ .context( appModel.getContext() )
+ .inputTriggerConfig( appModel.getKeymap().getConfig() )
+ .keyConfigContexts( keyConfigContexts )
+ .descriptionProvider( appModel.getWindowManager().getViewFactories().getCommandDescriptions() )
+ .register( viewActions )
+ .register( appModel.getModelActions() )
+ .register( appModel.getProjectActions() )
+ .register( appModel.getPlugins().getPluginActions() )
+ .modificationListeners( appModel.getKeymap().updateListeners() )
+ .parent( frame )
+ .installOn( viewActions );
+ cf.getDialog().setTitle( cf.getDialog().getTitle() + " - " + frame.getTitle() );
+
final ViewMenu menu = new ViewMenu( this );
final ActionMap actionMap = frame.getKeybindings().getConcatenatedActionMap();
@@ -207,6 +227,10 @@ public MamutViewTrackScheme( final ProjectModel appModel )
MainWindow.addMenus( menu, actionMap );
appModel.getWindowManager().addWindowMenu( menu, actionMap );
MamutMenuBuilder.build( menu, actionMap,
+ fileMenu(
+ separator(),
+ item( ExportViewActions.EXPORT_VIEW_TO_SVG ),
+ item( ExportViewActions.EXPORT_VIEW_TO_PNG ) ),
viewMenu(
colorMenu( coloringMenuHandle ),
colorbarMenu( colorbarMenuHandle ),
diff --git a/src/main/java/org/mastodon/model/BranchTrackSchemeRootsModel.java b/src/main/java/org/mastodon/model/BranchTrackSchemeRootsModel.java
index 8eaecfa5b..577c3e0e5 100644
--- a/src/main/java/org/mastodon/model/BranchTrackSchemeRootsModel.java
+++ b/src/main/java/org/mastodon/model/BranchTrackSchemeRootsModel.java
@@ -31,6 +31,8 @@
import org.mastodon.adapter.RefBimap;
import org.mastodon.collection.RefList;
import org.mastodon.collection.ref.RefArrayList;
+import org.mastodon.graph.GraphListener;
+import org.mastodon.mamut.model.Link;
import org.mastodon.mamut.model.ModelGraph;
import org.mastodon.mamut.model.Spot;
import org.mastodon.mamut.model.branch.BranchLink;
@@ -42,7 +44,7 @@
import java.util.List;
public class BranchTrackSchemeRootsModel
- implements RootsModel< TrackSchemeVertex >
+ implements RootsModel< TrackSchemeVertex >, GraphListener< Spot, Link >
{
private final ModelGraph modelGraph;
@@ -59,6 +61,17 @@ public BranchTrackSchemeRootsModel( ModelGraph graph, ModelBranchGraph branchGra
this.branchGraph = branchGraph;
this.viewGraph = viewGraph;
this.list = new RefArrayList<>( graph.vertices().getRefPool() );
+ this.modelGraph.addGraphListener( this );
+ }
+
+ /**
+ * This method should be called when the model is no longer needed.
+ * It removes listeners to allow garbage collection.
+ */
+ @Override
+ public void close()
+ {
+ modelGraph.removeGraphListener( this );
}
@Override
@@ -93,4 +106,34 @@ public RefList< TrackSchemeVertex > getRoots()
branchGraph.releaseRef( branchSpotRef );
return roots;
}
+
+ @Override
+ public void graphRebuilt()
+ {
+ list.clear();
+ }
+
+ @Override
+ public void vertexAdded( Spot vertex )
+ {
+
+ }
+
+ @Override
+ public void vertexRemoved( Spot vertex )
+ {
+ list.remove( vertex );
+ }
+
+ @Override
+ public void edgeAdded( Link edge )
+ {
+
+ }
+
+ @Override
+ public void edgeRemoved( Link edge )
+ {
+
+ }
}
diff --git a/src/main/java/org/mastodon/model/DefaultRootsModel.java b/src/main/java/org/mastodon/model/DefaultRootsModel.java
index c1f9122ea..d426ae838 100644
--- a/src/main/java/org/mastodon/model/DefaultRootsModel.java
+++ b/src/main/java/org/mastodon/model/DefaultRootsModel.java
@@ -32,7 +32,8 @@
import org.mastodon.collection.RefList;
import org.mastodon.collection.ref.RefArrayList;
import org.mastodon.graph.Edge;
-import org.mastodon.graph.ReadOnlyGraph;
+import org.mastodon.graph.GraphListener;
+import org.mastodon.graph.ListenableReadOnlyGraph;
import org.mastodon.graph.Vertex;
import org.mastodon.views.trackscheme.TrackSchemeGraph;
import org.mastodon.views.trackscheme.TrackSchemeVertex;
@@ -40,18 +41,32 @@
import java.util.List;
public class DefaultRootsModel< Spot extends Vertex< Link >, Link extends Edge< Spot > >
- implements RootsModel< TrackSchemeVertex >
+ implements RootsModel< TrackSchemeVertex >, GraphListener< Spot, Link >
{
private final TrackSchemeGraph< Spot, Link > viewGraph;
private final RefList< Spot > modelRoots;
- public DefaultRootsModel( ReadOnlyGraph< Spot, Link > modelGraph, TrackSchemeGraph< Spot, Link > viewGraph )
+ private final ListenableReadOnlyGraph< Spot, Link > modelGraph;
+
+ public DefaultRootsModel( ListenableReadOnlyGraph< Spot, Link > modelGraph, TrackSchemeGraph< Spot, Link > viewGraph )
{
this.viewGraph = viewGraph;
+ this.modelGraph = modelGraph;
+ this.modelGraph.addGraphListener( this );
this.modelRoots = RefCollections.createRefList( modelGraph.vertices() );
}
+ /**
+ * This method should be called when the model is no longer needed.
+ * It removes listeners to allow garbage collection.
+ */
+ @Override
+ public void close()
+ {
+ modelGraph.removeGraphListener( this );
+ }
+
@Override
public void setRoots( List< TrackSchemeVertex > viewRoots )
{
@@ -72,4 +87,34 @@ public RefList< TrackSchemeVertex > getRoots()
viewGraph.releaseRef( ref );
return viewRoots;
}
+
+ @Override
+ public void graphRebuilt()
+ {
+ modelRoots.clear();
+ }
+
+ @Override
+ public void vertexAdded( Spot vertex )
+ {
+
+ }
+
+ @Override
+ public void vertexRemoved( Spot vertex )
+ {
+ modelRoots.remove( vertex );
+ }
+
+ @Override
+ public void edgeAdded( Link edge )
+ {
+
+ }
+
+ @Override
+ public void edgeRemoved( Link edge )
+ {
+
+ }
}
diff --git a/src/main/java/org/mastodon/model/DefaultSelectionModel.java b/src/main/java/org/mastodon/model/DefaultSelectionModel.java
index 08cf2dfe7..7d1a151ae 100644
--- a/src/main/java/org/mastodon/model/DefaultSelectionModel.java
+++ b/src/main/java/org/mastodon/model/DefaultSelectionModel.java
@@ -389,4 +389,10 @@ public void pauseListeners()
{
emitEvents = false;
}
+
+ @Override
+ public boolean areListenersPaused()
+ {
+ return !emitEvents;
+ }
}
diff --git a/src/main/java/org/mastodon/model/HasCovariance.java b/src/main/java/org/mastodon/model/HasCovariance.java
new file mode 100644
index 000000000..b8077770a
--- /dev/null
+++ b/src/main/java/org/mastodon/model/HasCovariance.java
@@ -0,0 +1,30 @@
+package org.mastodon.model;
+
+/**
+ * Interface for objects that have a 3x3 covariance matrix, that can be set and
+ * accessed.
+ */
+public interface HasCovariance
+{
+
+ /**
+ * Stores the covariance matrix in the specified double[][]
+ * arrays.
+ *
+ * @param mat
+ * a (at least) 3x3 double[][]
array in which the
+ * covariance matrix will be written.
+ */
+ public void getCovariance( final double[][] mat );
+
+ /**
+ * Sets the covariance matrix of this objects, reading values from the
+ * specified double[][]
arrays.
+ *
+ * @param mat
+ * a (at least) 3x3 double[][]
array from which
+ * covariance values will be read.
+ */
+ public void setCovariance( final double[][] mat );
+
+}
diff --git a/src/main/java/org/mastodon/model/RootsModel.java b/src/main/java/org/mastodon/model/RootsModel.java
index 961c7f55f..90346540d 100644
--- a/src/main/java/org/mastodon/model/RootsModel.java
+++ b/src/main/java/org/mastodon/model/RootsModel.java
@@ -37,4 +37,6 @@ public interface RootsModel< V >
void setRoots( List< V > roots );
RefList< V > getRoots();
+
+ void close();
}
diff --git a/src/main/java/org/mastodon/model/SelectionModel.java b/src/main/java/org/mastodon/model/SelectionModel.java
index 91a5a50bf..3ccc37c24 100644
--- a/src/main/java/org/mastodon/model/SelectionModel.java
+++ b/src/main/java/org/mastodon/model/SelectionModel.java
@@ -163,7 +163,33 @@ public interface SelectionModel< V extends Vertex< E >, E extends Edge< V > >
*/
public Listeners< SelectionListener > listeners();
- public void resumeListeners();
+ /**
+ * Pauses the selection listeners. While paused, no listener that are contained in {@link #listeners()} will be notified
+ * of selection changes.
+ *
+ * However, this {@link SelectionModel} may record changes in selection state, and listeners may be notified of such a change, when the listeners are resumed with {@link #resumeListeners()}.
+ *
+ * Changes the state of {@link #areListenersPaused()} to {@code true}.
+ *
+ * Call {@link #resumeListeners()} to resume the listeners.
+ */
+ void pauseListeners();
+
+ /**
+ * Unpauses the selection listeners. All listeners that are contained in {@link #listeners()} will be notified of selection changes again.
+ *
+ * If this {@link SelectionModel} has recorded changes in selection state while the listeners were paused, listeners will be notified of these changes.
+ *
+ * Changes the state of {@link #areListenersPaused()} to {@code false}.
+ */
+ void resumeListeners();
- public void pauseListeners();
+ /**
+ * Checks if the selection listeners are paused.
+ *
+ * When paused, no listener that are contained in {@link #listeners()} will be notified of selection changes.
+ *
+ * @return {@code true} if the listeners are paused, {@code false} otherwise.
+ */
+ boolean areListenersPaused();
}
diff --git a/src/main/java/org/mastodon/model/branch/BranchGraphSelectionAdapter.java b/src/main/java/org/mastodon/model/branch/BranchGraphSelectionAdapter.java
index 1660aa244..5a29bf02a 100644
--- a/src/main/java/org/mastodon/model/branch/BranchGraphSelectionAdapter.java
+++ b/src/main/java/org/mastodon/model/branch/BranchGraphSelectionAdapter.java
@@ -75,6 +75,12 @@ public void pauseListeners()
selection.pauseListeners();
}
+ @Override
+ public boolean areListenersPaused()
+ {
+ return selection.areListenersPaused();
+ }
+
@Override
public boolean isSelected( final BV vertex )
{
@@ -123,9 +129,15 @@ public boolean isSelected( final BE edge )
@Override
public void setSelected( final BV vertex, final boolean selected )
{
- selection.pauseListeners();
- setVertexSelected( vertex, selected );
- selection.resumeListeners();
+ boolean areListenersPaused = selection.areListenersPaused();
+ if ( areListenersPaused )
+ setVertexSelected( vertex, selected );
+ else
+ {
+ selection.pauseListeners();
+ setVertexSelected( vertex, selected );
+ selection.resumeListeners();
+ }
}
private boolean setVertexSelected( final BV branchVertex, final boolean selected )
diff --git a/src/main/java/org/mastodon/ui/ExportViewActions.java b/src/main/java/org/mastodon/ui/ExportViewActions.java
new file mode 100644
index 000000000..d87495562
--- /dev/null
+++ b/src/main/java/org/mastodon/ui/ExportViewActions.java
@@ -0,0 +1,73 @@
+package org.mastodon.ui;
+
+import java.awt.Component;
+
+import javax.swing.JFrame;
+
+import org.mastodon.ui.keymap.KeyConfigContexts;
+import org.mastodon.ui.keymap.KeyConfigScopes;
+import org.mastodon.ui.util.ExportUtils;
+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 org.scijava.ui.behaviour.util.RunnableAction;
+
+public class ExportViewActions
+{
+ public static final String EXPORT_VIEW_TO_SVG = "Export current view to SVG";
+
+ protected static final String[] EXPORT_VIEW_TO_SVG_KEYS = new String[] { "ctrl P" };
+
+ public static final String EXPORT_VIEW_TO_PNG = "Export current view to PNG";
+
+ protected static final String[] EXPORT_VIEW_TO_PNG_KEYS = new String[] { "ctrl shift P" };
+
+ private final RunnableAction exportToSvgAction;
+
+ private final RunnableAction exportToPngAction;
+
+ public static void install( final Actions actions, final Component comp, final JFrame frame,
+ final String name )
+ {
+ new ExportViewActions( comp, frame, name ).install( actions );
+ }
+
+ private ExportViewActions( final Component comp, final JFrame frame, final String name )
+ {
+ exportToSvgAction = new RunnableAction( EXPORT_VIEW_TO_SVG, () -> ExportUtils.chooseFileAndExport(
+ ExportUtils.SVG_EXTENSION, file -> ExportUtils.exportSvg( file, comp ), name, frame ) );
+ exportToPngAction = new RunnableAction( EXPORT_VIEW_TO_PNG, () -> ExportUtils.chooseFileAndExport(
+ ExportUtils.PNG_EXTENSION, file -> ExportUtils.exportPng( file, comp ), name, frame ) );
+ }
+
+ private void install( final Actions actions )
+ {
+ actions.namedAction( exportToSvgAction, EXPORT_VIEW_TO_SVG_KEYS );
+ actions.namedAction( exportToPngAction, EXPORT_VIEW_TO_PNG_KEYS );
+ }
+
+ /*
+ * Command descriptions for all provided commands
+ */
+ @Plugin( type = CommandDescriptionProvider.class )
+ public static class Descriptions extends CommandDescriptionProvider
+ {
+ public Descriptions()
+ {
+ super( KeyConfigScopes.MASTODON,
+ KeyConfigContexts.BIGDATAVIEWER,
+ KeyConfigContexts.TRACKSCHEME,
+ KeyConfigContexts.GRAPHER );
+ }
+
+ @Override
+ public void getCommandDescriptions( final CommandDescriptions descriptions )
+ {
+ descriptions.add( EXPORT_VIEW_TO_SVG, EXPORT_VIEW_TO_SVG_KEYS,
+ "Capture the current view and export it to a SVG image file." );
+ descriptions.add( EXPORT_VIEW_TO_PNG, EXPORT_VIEW_TO_PNG_KEYS,
+ "Capture the current view and export it to a PNG image file." );
+ }
+ }
+}
diff --git a/src/main/java/org/mastodon/ui/commandfinder/CommandFinder.java b/src/main/java/org/mastodon/ui/commandfinder/CommandFinder.java
new file mode 100644
index 000000000..8a50d0ee7
--- /dev/null
+++ b/src/main/java/org/mastodon/ui/commandfinder/CommandFinder.java
@@ -0,0 +1,311 @@
+package org.mastodon.ui.commandfinder;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import javax.swing.Action;
+import javax.swing.ActionMap;
+import javax.swing.InputMap;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+
+import org.mastodon.app.ui.CloseWindowActions;
+import org.mastodon.ui.keymap.KeyConfigContexts;
+import org.mastodon.ui.keymap.KeyConfigScopes;
+import org.scijava.Context;
+import org.scijava.listeners.Listeners;
+import org.scijava.plugin.Plugin;
+import org.scijava.ui.behaviour.io.InputTriggerConfig;
+import org.scijava.ui.behaviour.io.gui.Command;
+import org.scijava.ui.behaviour.io.gui.CommandDescriptionProvider;
+import org.scijava.ui.behaviour.io.gui.CommandDescriptions;
+import org.scijava.ui.behaviour.io.gui.CommandDescriptionsBuilder;
+import org.scijava.ui.behaviour.util.Actions;
+
+import bdv.tools.ToggleDialogAction;
+import bdv.ui.keymap.Keymap.UpdateListener;
+
+public class CommandFinder
+{
+
+ public static final String SHOW_COMMAND_FINDER = "show command finder";
+
+ private static final String[] SHOW_COMMAND_FINDER_KEYS = new String[] { "ctrl shift F", "meta shift F" };
+
+ private final JDialog dialog;
+
+ private final List< Actions > acs;
+
+ private final CommandFinderPanel gui;
+
+ public static Builder build()
+ {
+ return new Builder();
+ }
+
+ private CommandFinder(
+ final List< Actions > acs,
+ final InputTriggerConfig config,
+ final Map< Command, String > commandMap,
+ final String[] keyConfigContexts,
+ final JFrame parent )
+ {
+ this.acs = acs;
+ this.dialog = new JDialog( parent, "Command finder" );
+
+ final Consumer< String > runner = ( actionName ) -> run( actionName );
+ this.gui = new CommandFinderPanel( runner, commandMap, config );
+
+ dialog.getContentPane().add( gui );
+ dialog.pack();
+ dialog.setLocationByPlatform( true );
+ dialog.setLocationRelativeTo( null );
+ // Give focus to text after being made visible.
+ dialog.addComponentListener( new ComponentAdapter()
+ {
+ @Override
+ public void componentShown( final ComponentEvent e )
+ {
+ gui.textFieldFilter.requestFocusInWindow();
+ }
+ } );
+ // Close with escape.
+ final ActionMap am = dialog.getRootPane().getActionMap();
+ final InputMap im = dialog.getRootPane().getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT );
+ final Actions actionsDialog = new Actions( im, am, null, keyConfigContexts );
+ CloseWindowActions.install( actionsDialog, dialog );
+ }
+
+ public CommandFinderPanel getGui()
+ {
+ return gui;
+ }
+
+ public JDialog getDialog()
+ {
+ return dialog;
+ }
+
+ /**
+ * Runs the 'first' action of behavior we can find with the specified name.
+ *
+ * @param actionName
+ * the action or behavior name.
+ */
+ private void run( final String actionName )
+ {
+ for ( final Actions ac : acs )
+ {
+ final Action action = ac.getActionMap().get( actionName );
+ if ( action != null )
+ {
+ final ActionEvent event = new ActionEvent( dialog, 0, actionName );
+ action.actionPerformed( event );
+ return;
+ }
+ }
+ }
+
+ /*
+ * Command descriptions for all provided commands
+ */
+ @Plugin( type = CommandDescriptionProvider.class )
+ public static class Descriptions extends CommandDescriptionProvider
+ {
+ public Descriptions()
+ {
+ super( KeyConfigScopes.MASTODON, KeyConfigContexts.MASTODON );
+ }
+
+ @Override
+ public void getCommandDescriptions( final CommandDescriptions descriptions )
+ {
+ descriptions.add( SHOW_COMMAND_FINDER, SHOW_COMMAND_FINDER_KEYS, "Show the command finder dialog." );
+ }
+ }
+
+ public static class Builder
+ {
+
+ private final List< Actions > acs = new ArrayList<>();
+
+ private final List< CommandDescriptionProvider > descriptionProviders = new ArrayList<>();
+
+ private JFrame parent;
+
+ private final Set< String > keyConfigContexts = new HashSet<>();
+
+ private InputTriggerConfig config;
+
+ private Context context;
+
+ private Listeners< UpdateListener > updateListeners;
+
+
+ public Builder context( final Context context )
+ {
+ this.context = context;
+ return this;
+ }
+
+ public Builder inputTriggerConfig( final InputTriggerConfig config )
+ {
+ this.config = config;
+ return this;
+ }
+
+ public Builder register( final Actions actions )
+ {
+ this.acs.add( actions );
+ return this;
+ }
+
+ public Builder parent( final JFrame parent )
+ {
+ this.parent = parent;
+ return this;
+ }
+
+ public Builder keyConfigContext( final String kcc )
+ {
+ this.keyConfigContexts.add( kcc );
+ return this;
+ }
+
+ public Builder keyConfigContexts( final String[] keyConfigContexts )
+ {
+ for ( final String kcc : keyConfigContexts )
+ this.keyConfigContexts.add( kcc );
+ return this;
+ }
+
+ public CommandFinder get()
+ {
+ if ( config == null )
+ throw new IllegalArgumentException( "The InputTriggerConfig cannot be null." );
+ if ( context == null )
+ throw new IllegalArgumentException( "The Context cannot be null." );
+
+ final Map< Command, String > commandDescriptions = buildCommandDescriptions();
+ final CommandFinder cf = new CommandFinder(
+ acs,
+ config,
+ commandDescriptions,
+ keyConfigContexts.toArray( new String[] {} ),
+ parent );
+ // Refresh the command list it it changes.
+ if ( updateListeners != null )
+ {
+ updateListeners.add( () -> {
+ final Map< Command, String > cds = buildCommandDescriptions();
+ cf.getGui().setCommandDescriptions( cds );
+ } );
+ }
+ return cf;
+ }
+
+ public Builder modificationListeners( final Listeners< UpdateListener > updateListeners )
+ {
+ this.updateListeners = updateListeners;
+ return this;
+ }
+
+ public Builder descriptionProvider( final CommandDescriptionProvider descriptionsProvider )
+ {
+ this.descriptionProviders.add( descriptionsProvider );
+ return this;
+ }
+
+ public CommandFinder installOn( final Actions installOn )
+ {
+ final CommandFinder cf = get();
+ installOn.namedAction( new ToggleDialogAction( SHOW_COMMAND_FINDER, cf.dialog ), SHOW_COMMAND_FINDER_KEYS );
+ register( installOn );
+ cf.getGui().setCommandDescriptions( buildCommandDescriptions() );
+ return cf;
+ }
+
+ /**
+ * Discovers and build the command vs description map.
+ *
+ * Important: in this implementation of a command finder, derived from
+ * the keymap editor, that commands are listed and filtered from the
+ * command descriptions. Which means that only the commands that have a
+ * description, provided with a CommandDescriptionProvider, will appear
+ * in the command list.
+ *
+ * @param actionMap
+ * the command descriptions will be filtered to only include
+ * the commands present in the specified action map.
+ * @param keyConfigContexts
+ * the command descriptions will be filtered to only include
+ * those with the context in the specified array.
+ * @return the command descriptions map as a new map.
+ */
+ private Map< Command, String > buildCommandDescriptions()
+ {
+ // Copy and sort key contexts.
+ final String[] contexts = Arrays.copyOf( keyConfigContexts.toArray( new String[] {} ), keyConfigContexts.size() );
+ Arrays.sort( contexts );
+
+ final CommandDescriptionsBuilder builder = new CommandDescriptionsBuilder();
+ context.inject( builder );
+ builder.discoverProviders();
+ for ( final CommandDescriptionProvider cdp : descriptionProviders )
+ builder.addManually( cdp, contexts );
+ final CommandDescriptions cd = builder.build();
+ final Map< Command, String > map = cd.createCommandDescriptionsMap();
+
+
+ // Create new command map, filtered.
+ final Map< Command, String > filteredMap = new HashMap<>();
+
+ /*
+ * Build array of keys (command name) that are in one of the actions
+ * or behaviors we were build with.
+ */
+
+ // Loop over actions.
+ final Set< String > allKeySet = new HashSet<>();
+ for ( final Actions ac : acs )
+ {
+ // Build list of commands in the action map.
+ final ActionMap actionMap = ac.getActionMap();
+ final Object[] objs = actionMap.allKeys();
+ if ( objs != null )
+ for ( int i = 0; i < objs.length; i++ )
+ allKeySet.add( ( String ) objs[ i ] );
+ }
+ final String[] allKeys = allKeySet.toArray( new String[] {} );
+ Arrays.sort( allKeys );
+
+ /*
+ * Loop over all command descriptions, and retain those 1/ which
+ * have a context that we manage 2/ which are in one of the action
+ * map of behavior map that was provided to us.
+ */
+ for ( final Command command : map.keySet() )
+ {
+ if ( Arrays.binarySearch( contexts, command.getContext() ) < 0 )
+ continue; // not in our contexts.
+
+ if ( Arrays.binarySearch( allKeys, command.getName() ) < 0 )
+ continue; // not a command we know.
+
+ // Add it.
+ filteredMap.put( command, map.get( command ) );
+ }
+ return filteredMap;
+ }
+ }
+}
diff --git a/src/main/java/org/mastodon/ui/commandfinder/CommandFinderPanel.java b/src/main/java/org/mastodon/ui/commandfinder/CommandFinderPanel.java
new file mode 100644
index 000000000..e5d5d80a3
--- /dev/null
+++ b/src/main/java/org/mastodon/ui/commandfinder/CommandFinderPanel.java
@@ -0,0 +1,777 @@
+package org.mastodon.ui.commandfinder;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.FlowLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.KeyboardFocusManager;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+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.JTable;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+import javax.swing.KeyStroke;
+import javax.swing.ListSelectionModel;
+import javax.swing.RowFilter;
+import javax.swing.ScrollPaneConstants;
+import javax.swing.SwingUtilities;
+import javax.swing.border.EmptyBorder;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.AbstractTableModel;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableRowSorter;
+
+import org.apache.commons.lang3.StringUtils;
+import org.scijava.listeners.Listeners;
+import org.scijava.ui.behaviour.InputTrigger;
+import org.scijava.ui.behaviour.io.InputTriggerConfig;
+import org.scijava.ui.behaviour.io.gui.Command;
+import org.scijava.ui.behaviour.io.gui.TagPanelEditor;
+import org.scijava.ui.behaviour.io.gui.VisualEditorPanel.ConfigChangeListener;
+import org.scijava.ui.behaviour.util.RunnableAction;
+
+public class CommandFinderPanel extends JPanel
+{
+
+ private static final long serialVersionUID = 1L;
+
+ private final Consumer< String > runAction;
+
+ private final InputTriggerConfig config;
+
+ private final Map< Command, String > actionDescriptions;
+
+ private final Set< Command > commands;
+
+ private final Listeners.List< ConfigChangeListener > modelChangedListeners;
+
+ final JTextField textFieldFilter;
+
+ private final JPanel panelEditor;
+
+ private final JLabel labelCommandName;
+
+ private final JTextArea textAreaDescription;
+
+ private final JButton btnApply;
+
+ private final JTable tableBindings;
+
+ private TableRowSorter< MyTableModel > tableRowSorter;
+
+ private MyTableModel tableModel;
+
+ private JLabel keybindingEditor;
+
+ public CommandFinderPanel(
+ final Consumer< String > runAction,
+ final Map< Command, String > commandMap,
+ final InputTriggerConfig config
+ )
+ {
+ this.runAction = runAction;
+ this.config = config;
+ this.actionDescriptions = new HashMap<>( commandMap );
+ this.commands = new HashSet<>( actionDescriptions.keySet() );
+ this.modelChangedListeners = new Listeners.SynchronizedList<>();
+
+ /*
+ * GUI
+ */
+
+ setLayout( new BorderLayout( 0, 0 ) );
+
+ final JPanel panelFilter = new JPanel();
+ add( panelFilter, BorderLayout.NORTH );
+ panelFilter.setLayout( new BoxLayout( panelFilter, BoxLayout.X_AXIS ) );
+
+ final Component horizontalStrut = Box.createHorizontalStrut( 5 );
+ panelFilter.add( horizontalStrut );
+
+ final JLabel lblFilter = new JLabel( "Filter:" );
+ lblFilter.setToolTipText( "Filter on command names. Accept regular expressions." );
+ lblFilter.setAlignmentX( Component.CENTER_ALIGNMENT );
+ panelFilter.add( lblFilter );
+
+ final Component horizontalStrut_1 = Box.createHorizontalStrut( 5 );
+ panelFilter.add( horizontalStrut_1 );
+
+ textFieldFilter = new JTextField();
+ panelFilter.add( textFieldFilter );
+ textFieldFilter.setColumns( 10 );
+ // Run action when the user presses enter.
+ textFieldFilter.addActionListener( e -> runFromFilter() );
+ // Filter when a key is pressed.
+ textFieldFilter.getDocument().addDocumentListener( new DocumentListener()
+ {
+
+ @Override
+ public void removeUpdate( final DocumentEvent e )
+ {
+ filterRows();
+ }
+
+ @Override
+ public void insertUpdate( final DocumentEvent e )
+ {
+ filterRows();
+ }
+
+ @Override
+ public void changedUpdate( final DocumentEvent e )
+ {
+ filterRows();
+ }
+ } );
+ // Move to the list with UP and DOWN array key.
+ textFieldFilter.addKeyListener( new KeyAdapter()
+ {
+ @Override
+ public void keyPressed( final KeyEvent e )
+ {
+ final int keyCode = e.getKeyCode();
+ if ( keyCode == KeyEvent.VK_UP )
+ goToLastInList();
+ else if ( keyCode == KeyEvent.VK_DOWN )
+ goToFirstInList();
+ }
+ } );
+
+ panelEditor = new JPanel();
+ add( panelEditor, BorderLayout.SOUTH );
+ panelEditor.setLayout( new BorderLayout( 0, 0 ) );
+
+ final JPanel panelCommandEditor = new JPanel();
+ panelEditor.add( panelCommandEditor, BorderLayout.CENTER );
+ final GridBagLayout gbl_panelCommandEditor = new GridBagLayout();
+ gbl_panelCommandEditor.rowHeights = new int[] { 0, 0, 0, 0, 60 };
+ gbl_panelCommandEditor.columnWidths = new int[] { 30, 100 };
+ gbl_panelCommandEditor.columnWeights = new double[] { 0.0, 1.0 };
+ gbl_panelCommandEditor.rowWeights = new double[] { 0.0, 0.0, 0.0, 0.0, 0.0 };
+ panelCommandEditor.setLayout( gbl_panelCommandEditor );
+
+ final JLabel lblName = new JLabel( "Name:" );
+ final GridBagConstraints gbc_lblName = new GridBagConstraints();
+ gbc_lblName.insets = new Insets( 5, 5, 5, 5 );
+ gbc_lblName.anchor = GridBagConstraints.WEST;
+ gbc_lblName.gridx = 0;
+ gbc_lblName.gridy = 0;
+ panelCommandEditor.add( lblName, gbc_lblName );
+
+ this.labelCommandName = new JLabel();
+ final GridBagConstraints gbc_labelActionName = new GridBagConstraints();
+ gbc_labelActionName.anchor = GridBagConstraints.WEST;
+ gbc_labelActionName.insets = new Insets( 5, 5, 5, 0 );
+ gbc_labelActionName.gridx = 1;
+ gbc_labelActionName.gridy = 0;
+ panelCommandEditor.add( labelCommandName, gbc_labelActionName );
+
+ final JLabel lblBinding = new JLabel( "Binding:" );
+ final GridBagConstraints gbc_lblBinding = new GridBagConstraints();
+ gbc_lblBinding.anchor = GridBagConstraints.WEST;
+ gbc_lblBinding.insets = new Insets( 5, 5, 5, 5 );
+ gbc_lblBinding.gridx = 0;
+ gbc_lblBinding.gridy = 1;
+ panelCommandEditor.add( lblBinding, gbc_lblBinding );
+
+ this.keybindingEditor = new JLabel();
+ final GridBagConstraints gbc_textFieldBinding = new GridBagConstraints();
+ gbc_textFieldBinding.insets = new Insets( 5, 5, 5, 5 );
+ gbc_textFieldBinding.fill = GridBagConstraints.HORIZONTAL;
+ gbc_textFieldBinding.gridx = 1;
+ gbc_textFieldBinding.gridy = 1;
+ panelCommandEditor.add( keybindingEditor, gbc_textFieldBinding );
+
+ final JLabel lblDescription = new JLabel( "Description:" );
+ final GridBagConstraints gbc_lblDescription = new GridBagConstraints();
+ gbc_lblDescription.insets = new Insets( 5, 5, 5, 5 );
+ gbc_lblDescription.anchor = GridBagConstraints.NORTHWEST;
+ gbc_lblDescription.gridx = 0;
+ gbc_lblDescription.gridy = 4;
+ panelCommandEditor.add( lblDescription, gbc_lblDescription );
+
+ final JScrollPane scrollPaneDescription = new JScrollPane();
+ scrollPaneDescription.setOpaque( false );
+ scrollPaneDescription.setHorizontalScrollBarPolicy( ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER );
+ final GridBagConstraints gbc_scrollPaneDescription = new GridBagConstraints();
+ gbc_scrollPaneDescription.insets = new Insets( 5, 5, 5, 5 );
+ gbc_scrollPaneDescription.fill = GridBagConstraints.BOTH;
+ gbc_scrollPaneDescription.gridx = 1;
+ gbc_scrollPaneDescription.gridy = 4;
+ panelCommandEditor.add( scrollPaneDescription, gbc_scrollPaneDescription );
+
+ textAreaDescription = new JTextArea();
+ textAreaDescription.setRows( 3 );
+ textAreaDescription.setFont( getFont().deriveFont( getFont().getSize2D() - 1f ) );
+ textAreaDescription.setOpaque( false );
+ textAreaDescription.setWrapStyleWord( true );
+ textAreaDescription.setEditable( false );
+ textAreaDescription.setLineWrap( true );
+ textAreaDescription.setFocusable( false );
+ scrollPaneDescription.setViewportView( textAreaDescription );
+
+ final JPanel panelButtons = new JPanel();
+ panelEditor.add( panelButtons, BorderLayout.SOUTH );
+ final FlowLayout flowLayout = ( FlowLayout ) panelButtons.getLayout();
+ flowLayout.setAlignment( FlowLayout.TRAILING );
+
+ this.btnApply = new JButton( "Run" );
+ btnApply.setToolTipText( "Run the selected command." );
+ panelButtons.add( btnApply );
+
+ final JScrollPane scrollPane = new JScrollPane();
+ scrollPane.setVerticalScrollBarPolicy( ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS );
+ scrollPane.setHorizontalScrollBarPolicy( ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER );
+ add( scrollPane, BorderLayout.CENTER );
+
+ tableBindings = new JTable()
+ {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void updateUI()
+ {
+ super.updateUI();
+ setRowHeight( ( int ) ( getFontMetrics( getFont() ).getHeight() * 1.5 ) );
+ }
+ };
+ tableBindings.setSelectionMode( ListSelectionModel.SINGLE_SELECTION );
+ tableBindings.setFillsViewportHeight( true );
+ tableBindings.setAutoResizeMode( JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS );
+ tableBindings.getSelectionModel().addListSelectionListener( new ListSelectionListener()
+ {
+ @Override
+ public void valueChanged( final ListSelectionEvent e )
+ {
+ if ( e.getValueIsAdjusting() )
+ return;
+ updateEditors();
+ }
+ } );
+ tableBindings.setFocusTraversalKeys( KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, null );
+ tableBindings.setFocusTraversalKeys( KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, null );
+
+ // Run when the user press enter and a row is selected
+ final KeyStroke enter = KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 );
+ final String solve = "Run command";
+ tableBindings.getInputMap( JTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( enter, solve );
+ tableBindings.getActionMap().put( solve, new RunnableAction( solve, this::runSelection ) );
+
+ // Run when the user double-clicks on a row
+ tableBindings.addMouseListener( new MouseAdapter()
+ {
+ @Override
+ public void mouseClicked( final MouseEvent e )
+ {
+ if ( e.getClickCount() == 2 )
+ runSelection();
+ }
+ } );
+
+ // Run when the user presses the button
+ btnApply.addActionListener( ( e ) -> runSelection() );
+
+ configToModel();
+ tableBindings.getRowSorter().toggleSortOrder( 0 );
+ if ( tableBindings.getRowCount() > 0 )
+ tableBindings.getSelectionModel().setSelectionInterval( 0, 0 );
+
+ scrollPane.setViewportView( tableBindings );
+ }
+
+ private void goToFirstInList()
+ {
+ selectAndShowViewRow( 0 );
+ }
+
+ private void goToLastInList()
+ {
+ final int nFiltered = tableBindings.getRowSorter().getViewRowCount();
+ selectAndShowViewRow( nFiltered - 1 );
+ }
+
+ private void selectAndShowViewRow( final int row )
+ {
+ tableBindings.getSelectionModel().setSelectionInterval( row, row );
+ tableBindings.requestFocusInWindow();
+ tableBindings.scrollRectToVisible( tableBindings.getCellRect( row, row, true ) );
+ }
+
+ private void runFromFilter()
+ {
+ final int nFiltered = tableBindings.getRowSorter().getViewRowCount();
+ if ( nFiltered != 1 )
+ return;
+
+ runRow( 0 );
+ }
+
+ private void runSelection()
+ {
+ final int viewRow = tableBindings.getSelectedRow();
+ if ( viewRow < 0 )
+ {
+ SwingUtilities.getWindowAncestor( this ).setVisible( false );
+ return;
+ }
+ runRow( viewRow );
+ }
+
+ private void runRow( final int viewRow )
+ {
+ final int modelRow = tableBindings.convertRowIndexToModel( viewRow );
+ final MyTableRow row = tableModel.rows.get( modelRow );
+ final String actionName = row.getName();
+
+ runAction.accept( actionName );
+ SwingUtilities.getWindowAncestor( this ).setVisible( false );
+ }
+
+ private void updateEditors()
+ {
+ final int viewRow = tableBindings.getSelectedRow();
+ if ( viewRow < 0 )
+ {
+ labelCommandName.setText( "" );
+ textAreaDescription.setText( "" );
+ keybindingEditor.setText( InputTrigger.NOT_MAPPED.toString() );
+ return;
+ }
+
+ final int modelRow = tableBindings.convertRowIndexToModel( viewRow );
+ final MyTableRow row = tableModel.rows.get( modelRow );
+ final String action = row.getName();
+ final List< InputTrigger > triggers = row.getTrigger();
+ final List< String > contexts = row.getContexts();
+ final String description = actionDescriptions.get( new Command( action, contexts.get( 0 ) ) );
+ labelCommandName.setText( action );
+ keybindingEditor.setText( StringUtils.join( triggers, ", " ) );
+ textAreaDescription.setText( description );
+ textAreaDescription.setCaretPosition( 0 );
+ }
+
+ public void setCommandDescriptions( final Map< Command, String > cds )
+ {
+ actionDescriptions.clear();
+ commands.clear();
+ actionDescriptions.putAll( cds );
+ commands.addAll( actionDescriptions.keySet() );
+ configToModel();
+ }
+
+ private void configToModel()
+ {
+ tableModel = new MyTableModel( commands, config );
+ tableBindings.setModel( tableModel );
+
+ tableRowSorter = new TableRowSorter<>( tableModel );
+ tableRowSorter.setComparator( 1, InputTriggersComparator );
+ tableBindings.setRowSorter( tableRowSorter );
+ filterRows();
+
+ // Renderers.
+ tableBindings.getColumnModel().getColumn( 1 ).setCellRenderer( new MyBindingsRenderer() );
+ tableBindings.getColumnModel().getColumn( 2 ).setCellRenderer( new MyContextsRenderer( Collections.emptyList() ) );
+
+ // Notify listeners.
+ notifyListeners();
+ }
+
+ private void notifyListeners()
+ {
+ modelChangedListeners.list.forEach( ConfigChangeListener::configChanged );
+ }
+
+ private void filterRows()
+ {
+ final String regex = textFieldFilter.getText();
+ final Pattern pattern = Pattern.compile( regex, Pattern.CASE_INSENSITIVE );
+ final Matcher matcher = pattern.matcher( "" );
+ final RowFilter< MyTableModel, Integer > rf = new RowFilter< MyTableModel, Integer >()
+ {
+
+ @Override
+ public boolean include( final Entry< ? extends MyTableModel, ? extends Integer > entry )
+ {
+ int count = entry.getValueCount();
+ while ( --count >= 0 )
+ {
+ matcher.reset( entry.getStringValue( count ) );
+ if ( matcher.find() )
+ { return true; }
+ }
+ return false;
+ }
+ };
+ tableRowSorter.setRowFilter( rf );
+ }
+
+ private static class MyTableModel extends AbstractTableModel
+ {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final String[] TABLE_HEADERS = new String[] { "Command", "Binding", "Contexts" };
+
+ private final List< MyTableRow > rows;
+
+ private final Set< Command > allCommands;
+
+ public MyTableModel( final Set< Command > commands, final InputTriggerConfig config )
+ {
+ rows = new ArrayList<>();
+ allCommands = commands;
+ for ( final Command command : commands )
+ {
+ final Set< InputTrigger > inputs = config.getInputs( command.getName(), command.getContext() );
+ for ( final InputTrigger input : inputs )
+ rows.add( new MyTableRow( command.getName(), input, command.getContext() ) );
+ }
+ addMissingRows();
+ }
+
+ /**
+ * In the given list of {@code rows}, find and merge rows with the same
+ * action name and trigger, but different contexts.
+ *
+ * @param rows
+ * list of rows to modify.
+ */
+ private void mergeRows( final List< MyTableRow > rows )
+ {
+ final List< MyTableRow > rowsUnmerged = new ArrayList<>( rows );
+ rows.clear();
+
+ rowsUnmerged.sort( MyTableRowComparator );
+
+ for ( int i = 0; i < rowsUnmerged.size(); )
+ {
+ final MyTableRow rowA = rowsUnmerged.get( i );
+ int j = i + 1;
+ while ( j < rowsUnmerged.size() && MyTableRowComparator.compare( rowsUnmerged.get( j ), rowA ) == 0 )
+ ++j;
+
+ // Merge contexts.
+ final Set< String > contexts = new HashSet<>();
+ for ( int k = i; k < j; ++k )
+ contexts.addAll( rowsUnmerged.get( k ).getContexts() );
+ final List< String > contextsList = new ArrayList<>( contexts );
+ contextsList.sort( null );
+
+ // Merge triggers.
+ final Set< InputTrigger > triggers = new HashSet<>();
+ for ( int k = i; k < j; ++k )
+ triggers.addAll( rowsUnmerged.get( k ).getTrigger() );
+ final List< InputTrigger > triggersList = new ArrayList<>( triggers );
+ triggersList.sort( InputTriggerComparator );
+
+ rows.add( new MyTableRow( rowA.getName(), triggersList, contextsList ) );
+
+ i = j;
+ }
+ }
+
+ /**
+ * Add {@code NOT_MAPPED} rows for (name, context) pairs in
+ * {@link #allCommands} that are not otherwise covered. Then
+ * {@link #mergeRows()}.
+ *
+ * If any changes are made, {@code fireTableDataChanged} is fired.
+ *
+ * @return true, if changes were made.
+ */
+ private boolean addMissingRows()
+ {
+ final ArrayList< MyTableRow > copy = new ArrayList<>( rows );
+ addMissingRows( rows );
+ if ( !copy.equals( rows ) )
+ {
+ this.fireTableDataChanged();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * In the given list of {@code rows}, add {@code NOT_MAPPED} rows for
+ * (name, context) pairs in {@link #allCommands} that are not otherwise
+ * covered. Then {@link #mergeRows(List)}.
+ *
+ * @param rows
+ * list of rows to modify.
+ */
+ private void addMissingRows( final List< MyTableRow > rows )
+ {
+ final ArrayList< Command > missingCommands = new ArrayList<>();
+ for ( final Command command : allCommands )
+ {
+ boolean found = false;
+ for ( final MyTableRow row : rows )
+ {
+ if ( row.getName().equals( command.getName() ) && row.getContexts().contains( command.getContext() ) )
+ {
+ found = true;
+ break;
+ }
+ }
+ if ( !found )
+ missingCommands.add( command );
+ }
+
+ for ( final Command command : missingCommands )
+ rows.add( new MyTableRow( command.getName(), InputTrigger.NOT_MAPPED, command.getContext() ) );
+
+ mergeRows( rows );
+ }
+
+ @Override
+ public int getRowCount()
+ {
+ return rows.size();
+ }
+
+ @Override
+ public int getColumnCount()
+ {
+ return 3;
+ }
+
+ @Override
+ public Object getValueAt( final int rowIndex, final int columnIndex )
+ {
+ switch ( columnIndex )
+ {
+ case 0:
+ return rows.get( rowIndex ).getName();
+ case 1:
+ return rows.get( rowIndex ).getTrigger();
+ case 2:
+ return rows.get( rowIndex ).getContexts();
+ default:
+ throw new NoSuchElementException( "Cannot access column " + columnIndex + " in this model." );
+ }
+ }
+
+ @Override
+ public String getColumnName( final int column )
+ {
+ return TABLE_HEADERS[ column ];
+ }
+ }
+
+ private static class MyTableRow
+ {
+ private final String name;
+
+ private final List< InputTrigger > triggers;
+
+ private final List< String > contexts;
+
+ public MyTableRow( final String name, final InputTrigger trigger, final String context )
+ {
+ this( name, Collections.singletonList( trigger ), Collections.singletonList( context ) );
+ }
+
+ public MyTableRow( final String name, final List< InputTrigger > triggers, final Collection< String > contexts )
+ {
+ this.name = name;
+ this.triggers = triggers;
+ this.contexts = new ArrayList<>( contexts );
+ }
+
+ public String getName()
+ {
+ return name;
+ }
+
+ public List< InputTrigger > getTrigger()
+ {
+ return triggers;
+ }
+
+ public List< String > getContexts()
+ {
+ return contexts;
+ }
+
+ @Override
+ public boolean equals( final Object o )
+ {
+ if ( this == o )
+ return true;
+ if ( o == null || getClass() != o.getClass() )
+ return false;
+
+ final MyTableRow that = ( MyTableRow ) o;
+
+ if ( !name.equals( that.name ) )
+ return false;
+ if ( !triggers.equals( that.triggers ) )
+ return false;
+ return contexts.equals( that.contexts );
+ }
+
+ @Override
+ public int hashCode()
+ {
+ int result = name.hashCode();
+ result = 31 * result + triggers.hashCode();
+ result = 31 * result + contexts.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "MyTableRow{" +
+ "name='" + name + '\'' +
+ ", trigger=" + triggers +
+ ", contexts=" + contexts +
+ '}';
+ }
+ }
+
+ private static final Comparator< MyTableRow > MyTableRowComparator = new Comparator< MyTableRow >()
+ {
+ @Override
+ public int compare( final MyTableRow o1, final MyTableRow o2 )
+ {
+ final int cn = o1.name.compareTo( o2.name );
+ if ( cn != 0 )
+ return cn;
+
+ return 0;
+ }
+ };
+
+ private static final Comparator< InputTrigger > InputTriggerComparator = new Comparator< InputTrigger >()
+ {
+ @Override
+ public int compare( final InputTrigger o1, final InputTrigger o2 )
+ {
+ if ( o1 == InputTrigger.NOT_MAPPED )
+ return 1;
+ if ( o2 == InputTrigger.NOT_MAPPED )
+ return -1;
+ return o1.toString().compareTo( o2.toString() );
+ }
+ };
+
+ private static final Comparator< List< InputTrigger > > InputTriggersComparator = new Comparator< List< InputTrigger > >()
+ {
+ @Override
+ public int compare( final List< InputTrigger > o1, final List< InputTrigger > o2 )
+ {
+ if ( o1.equals( Collections.singletonList( InputTrigger.NOT_MAPPED ) ) )
+ return 1;
+ if ( o2.equals( Collections.singletonList( InputTrigger.NOT_MAPPED ) ) )
+ return -1;
+
+ return StringUtils.join( o1, ", " ).compareTo( StringUtils.join( o2, ", " ) );
+ }
+ };
+
+ private static final class MyBindingsRenderer extends MultiInputTriggerPanelEditor implements TableCellRenderer
+ {
+
+ private static final long serialVersionUID = 1L;
+
+ public MyBindingsRenderer()
+ {
+ super();
+ }
+
+ @Override
+ public void updateUI()
+ {
+ super.updateUI();
+ setBorder( new EmptyBorder( 0, 0, 0, 0 ) );
+ }
+
+ @Override
+ public Component getTableCellRendererComponent( final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column )
+ {
+ setForeground( isSelected ? table.getSelectionForeground() : table.getForeground() );
+ setBackground( isSelected ? table.getSelectionBackground() : table.getBackground() );
+
+ @SuppressWarnings( "unchecked" )
+ final List< InputTrigger > inputs = ( List< InputTrigger > ) value;
+ if ( null != inputs )
+ {
+ setInputTriggers( inputs );
+ final String val = StringUtils.join( inputs, ", " );
+ setToolTipText( val );
+ }
+ else
+ {
+ setInputTriggers( Collections.singletonList( InputTrigger.NOT_MAPPED ) );
+ setToolTipText( "No binding" );
+ }
+ return this;
+ }
+ }
+
+ private final class MyContextsRenderer extends TagPanelEditor implements TableCellRenderer
+ {
+
+ private static final long serialVersionUID = 1L;
+
+ public MyContextsRenderer( final Collection< String > tags )
+ {
+ super( tags, false );
+ }
+
+ @Override
+ public void updateUI()
+ {
+ super.updateUI();
+ setBorder( new EmptyBorder( 0, 0, 0, 0 ) );
+ }
+
+ @Override
+ public Component getTableCellRendererComponent( final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column )
+ {
+ setForeground( isSelected ? table.getSelectionForeground() : table.getForeground() );
+ setBackground( isSelected ? table.getSelectionBackground() : table.getBackground() );
+
+ @SuppressWarnings( "unchecked" )
+ final List< String > contexts = value != null
+ ? ( List< String > ) value
+ : Collections.emptyList();
+ if ( contexts.isEmpty() )
+ setBackground( Color.PINK );
+ setAcceptableTags( contexts );
+ setTags( contexts );
+ setToolTipText( contexts.toString() );
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/org/mastodon/ui/commandfinder/MultiInputTriggerPanelEditor.java b/src/main/java/org/mastodon/ui/commandfinder/MultiInputTriggerPanelEditor.java
new file mode 100644
index 000000000..d63fcfe3a
--- /dev/null
+++ b/src/main/java/org/mastodon/ui/commandfinder/MultiInputTriggerPanelEditor.java
@@ -0,0 +1,420 @@
+/*-
+ * #%L
+ * Configurable key and mouse event handling
+ * %%
+ * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology
+ * and Genetics.
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package org.mastodon.ui.commandfinder;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.UIManager;
+import javax.swing.border.EmptyBorder;
+
+import org.scijava.ui.behaviour.InputTrigger;
+import org.scijava.ui.behaviour.io.gui.RoundBorder;
+
+public class MultiInputTriggerPanelEditor extends JPanel
+{
+
+ @FunctionalInterface
+ public static interface InputTriggerChangeListener
+ {
+ public void inputTriggerChanged();
+ }
+
+ private static final long serialVersionUID = 1L;
+
+ private final List< KeyItem > keyItems;
+
+ private final JTextField textField;
+
+ private List< InputTrigger > triggers = Collections.singletonList( InputTrigger.NOT_MAPPED );
+
+ public MultiInputTriggerPanelEditor()
+ {
+ this.keyItems = new ArrayList<>();
+
+ setPreferredSize( new Dimension( 400, 26 ) );
+ setMinimumSize( new Dimension( 26, 26 ) );
+ setLayout( new BoxLayout( this, BoxLayout.LINE_AXIS ) );
+
+ this.textField = new JTextField();
+ textField.setColumns( 10 );
+ textField.setBorder( new EmptyBorder( 0, 0, 0, 0 ) );
+ textField.setOpaque( false );
+ textField.setEditable( false );
+
+ add( textField );
+ add( Box.createHorizontalGlue() );
+ }
+
+ @Override
+ public void updateUI()
+ {
+ super.updateUI();
+ setBorder( UIManager.getBorder( "TextField.border" ) );
+ setBackground( UIManager.getColor( "TextField.background" ) );
+ }
+
+ public void setInputTriggers( final List< InputTrigger > triggers )
+ {
+ this.triggers = triggers;
+ regenKeyPanels();
+ }
+
+ public List< InputTrigger > getInputTrigger()
+ {
+ return triggers;
+ }
+
+ private void regenKeyPanels()
+ {
+ // Clear
+ for ( final KeyItem keyItem : keyItems )
+ remove( keyItem );
+ keyItems.clear();
+
+ final List< String[] > tokenList = new ArrayList<>( triggers.size() );
+ for ( final InputTrigger trigger : triggers )
+ {
+ if ( trigger.equals( InputTrigger.NOT_MAPPED ) )
+ tokenList.add( null );
+ else
+ tokenList.add( trigger.toString().split( " " ) );
+ }
+
+ int n = 0;
+ for ( int i = 0; i < tokenList.size(); i++ )
+ {
+ final String[] tokens = tokenList.get( i );
+ if ( tokens == null )
+ continue;
+ if ( n > 0 )
+ {
+ // Separator.
+ final KeyItem tagp = new SeparatorKeyItem();
+ keyItems.add( tagp );
+ add( tagp, getComponentCount() - 2 );
+ }
+
+ sortTokens( tokens );
+ for ( final String key : tokens )
+ {
+ final KeyItem tagp = new KeyItem( key, true );
+ keyItems.add( tagp );
+ add( tagp, getComponentCount() - 2 );
+ }
+ n++;
+ }
+ revalidate();
+ repaint();
+ }
+
+ @Override
+ public boolean requestFocusInWindow()
+ {
+ return textField.requestFocusInWindow();
+ }
+
+ /*
+ * INNER CLASSES
+ */
+
+ private class SeparatorKeyItem extends KeyItem
+ {
+
+ private static final long serialVersionUID = 1L;
+
+ public SeparatorKeyItem()
+ {
+ super( "or", true );
+ }
+
+ @Override
+ protected void updateTxtLook()
+ {
+ if ( txt != null )
+ {
+ txt.setOpaque( false );
+ Font font = UIManager.getFont( "Label.font" );
+ font = font.deriveFont( font.getSize2D() - 4f );
+ txt.setFont( font );
+ }
+ }
+ }
+
+ private class KeyItem extends JPanel
+ {
+ private static final long serialVersionUID = 1L;
+
+ private final boolean valid;
+
+ protected final JLabel txt;
+
+ public KeyItem( final String tag, final boolean valid )
+ {
+ this.valid = valid;
+ final String str = TRIGGER_SYMBOLS.containsKey( tag ) ? ( " " + TRIGGER_SYMBOLS.get( tag ) + " " ) : ( " " + tag + " " );
+ txt = new JLabel( str );
+ txt.setOpaque( true );
+ updateTxtLook();
+
+ setLayout( new BoxLayout( this, BoxLayout.LINE_AXIS ) );
+ add( Box.createHorizontalStrut( 1 ) );
+ add( txt );
+ add( Box.createHorizontalStrut( 1 ) );
+ setOpaque( false );
+ }
+
+ @Override
+ public void updateUI()
+ {
+ super.updateUI();
+ updateTxtLook();
+ }
+
+ protected void updateTxtLook()
+ {
+ if ( txt != null )
+ {
+ final Color tfg = UIManager.getColor( "TextField.foreground" );
+ final Color tbg = UIManager.getColor( "TextField.background" );
+ final Color bg = valid ? mix( tbg, tfg, 0.95 ) : mix( tbg, Color.red, 0.5 );
+ final Color borderColor = mix( bg, tfg, 0.8 );
+ txt.setBackground( bg );
+ txt.setBorder( new RoundBorder( borderColor, MultiInputTriggerPanelEditor.this, 1 ) );
+
+ Font font = UIManager.getFont( "Label.font" );
+ font = font.deriveFont( font.getSize2D() - 2f );
+ txt.setFont( font );
+ }
+ }
+ }
+
+ /** Contains the tags in the order we want them to appear in the panel. */
+ private static final List< String > INPUT_TRIGGER_SYNTAX_TAGS = new ArrayList<>();
+
+ /**
+ * Contains the tags sorted so that they can be searched by the autocomplete
+ * process.
+ */
+ private static final List< String > INPUT_TRIGGER_SYNTAX_TAGS_SORTED = new ArrayList<>();
+
+ /** Small-caps version of INPUT_TRIGGER_SYNTAX_TAGS_SORTED. */
+ private static final List< String > INPUT_TRIGGER_SYNTAX_TAGS_SMALL_CAPS;
+
+ /** Visual replacement for some tags. */
+ private static final Map< String, String > TRIGGER_SYMBOLS = new HashMap<>();
+
+ private static final Map< String, String > INPUT_TRIGGER_SYNTAX_TAG_REMAP = new HashMap<>();
+
+ static
+ {
+ INPUT_TRIGGER_SYNTAX_TAGS.addAll(
+ Arrays.asList(
+ "all",
+ "ctrl",
+ "alt",
+ "altGraph",
+ "shift",
+ "meta",
+ "command",
+ "cmd",
+ "win",
+ "ENTER",
+ "BACK_SPACE",
+ "TAB",
+ "CANCEL",
+ "CLEAR",
+ "COMPOSE",
+ "PAUSE",
+ "CAPS_LOCK",
+ "ESCAPE",
+ "SPACE",
+ "PAGE_UP",
+ "PAGE_DOWN",
+ "END",
+ "HOME",
+ "BEGIN",
+ "COMMA",
+ "PERIOD",
+ "SLASH",
+ "SEMICOLON",
+ "EQUALS",
+ "OPEN_BRACKET",
+ "BACK_SLASH",
+ "CLOSE_BRACKET",
+ "LEFT",
+ "UP",
+ "RIGHT",
+ "DOWN",
+ "NUMPAD0",
+ "NUMPAD1",
+ "NUMPAD2",
+ "NUMPAD3",
+ "NUMPAD4",
+ "NUMPAD5",
+ "NUMPAD6",
+ "NUMPAD7",
+ "NUMPAD8",
+ "NUMPAD9",
+ "MULTIPLY",
+ "ADD",
+ "SEPARATOR",
+ "SUBTRACT",
+ "DECIMAL",
+ "DIVIDE",
+ "DELETE",
+ "NUM_LOCK",
+ "SCROLL_LOCK",
+ "double-click",
+ "button1",
+ "button2",
+ "button3",
+ "scroll",
+ "|" ) );
+ for ( int i = 0; i < 26; i++ )
+ INPUT_TRIGGER_SYNTAX_TAGS.add( String.valueOf( ( char ) ( 'A' + i ) ) );
+ for ( int i = 0; i < 10; i++ )
+ INPUT_TRIGGER_SYNTAX_TAGS.add( "" + i );
+ for ( int i = 1; i <= 24; i++ )
+ INPUT_TRIGGER_SYNTAX_TAGS.add( "F" + i );
+
+ INPUT_TRIGGER_SYNTAX_TAGS_SORTED.addAll( INPUT_TRIGGER_SYNTAX_TAGS );
+ INPUT_TRIGGER_SYNTAX_TAGS_SORTED.sort( String.CASE_INSENSITIVE_ORDER );
+ INPUT_TRIGGER_SYNTAX_TAGS_SMALL_CAPS = new ArrayList<>( INPUT_TRIGGER_SYNTAX_TAGS_SORTED.size() );
+ for ( final String tag : INPUT_TRIGGER_SYNTAX_TAGS_SORTED )
+ INPUT_TRIGGER_SYNTAX_TAGS_SMALL_CAPS.add( tag.toLowerCase() );
+
+ INPUT_TRIGGER_SYNTAX_TAG_REMAP.put( "cmd", "meta" );
+ INPUT_TRIGGER_SYNTAX_TAG_REMAP.put( "command", "meta" );
+ INPUT_TRIGGER_SYNTAX_TAG_REMAP.put( "windows", "win" );
+
+ TRIGGER_SYMBOLS.put( "ENTER", "\u23CE" );
+ TRIGGER_SYMBOLS.put( "BACK_SPACE", "\u232B" );
+ TRIGGER_SYMBOLS.put( "DELETE", "\u2326" );
+ TRIGGER_SYMBOLS.put( "TAB", "\u21E5" );
+ TRIGGER_SYMBOLS.put( "PAUSE", "||" );
+ TRIGGER_SYMBOLS.put( "CAPS_LOCK", "\u21EA" );
+ TRIGGER_SYMBOLS.put( "PAGE_UP", "\u21DE" );
+ TRIGGER_SYMBOLS.put( "PAGE_DOWN", "\u21DF" );
+ TRIGGER_SYMBOLS.put( "END", "\u2198" );
+ TRIGGER_SYMBOLS.put( "HOME", "\u2196" );
+ TRIGGER_SYMBOLS.put( "ESCAPE", "\u238b" );
+ TRIGGER_SYMBOLS.put( "LEFT", "\u2190" );
+ TRIGGER_SYMBOLS.put( "UP", "\u2191" );
+ TRIGGER_SYMBOLS.put( "RIGHT", "\u2192" );
+ TRIGGER_SYMBOLS.put( "DOWN", "\u2193" );
+ TRIGGER_SYMBOLS.put( "NUMPAD0", "\u24ea" );
+ TRIGGER_SYMBOLS.put( "NUMPAD1", "\u2460" );
+ TRIGGER_SYMBOLS.put( "NUMPAD2", "\u2461" );
+ TRIGGER_SYMBOLS.put( "NUMPAD3", "\u2462" );
+ TRIGGER_SYMBOLS.put( "NUMPAD4", "\u2463" );
+ TRIGGER_SYMBOLS.put( "NUMPAD5", "\u2464" );
+ TRIGGER_SYMBOLS.put( "NUMPAD6", "\u2465" );
+ TRIGGER_SYMBOLS.put( "NUMPAD7", "\u2466" );
+ TRIGGER_SYMBOLS.put( "NUMPAD8", "\u2467" );
+ TRIGGER_SYMBOLS.put( "NUMPAD9", "\u2468" );
+ TRIGGER_SYMBOLS.put( "MULTIPLY", "\u00d7" );
+ TRIGGER_SYMBOLS.put( "DIVIDE", "\u00f7" );
+ TRIGGER_SYMBOLS.put( "ADD", "+" );
+ TRIGGER_SYMBOLS.put( "SUBTRACT", "-" );
+ TRIGGER_SYMBOLS.put( "COMMA", "," );
+ TRIGGER_SYMBOLS.put( "PERIOD", "." );
+ TRIGGER_SYMBOLS.put( "SLASH", "/" );
+ TRIGGER_SYMBOLS.put( "SEMICOLON", ";" );
+ TRIGGER_SYMBOLS.put( "EQUALS", "=" );
+ TRIGGER_SYMBOLS.put( "OPEN_BRACKET", "[" );
+ TRIGGER_SYMBOLS.put( "BACK_SLASH", "\\" );
+ TRIGGER_SYMBOLS.put( "CLOSE_BRACKET", "]" );
+ TRIGGER_SYMBOLS.put( "ctrl", "\u2303" );
+ TRIGGER_SYMBOLS.put( "alt", "\u2387" );
+ TRIGGER_SYMBOLS.put( "shift", "\u21e7" );
+ TRIGGER_SYMBOLS.put( "meta", isMac() ? "\u2318" : "\u25c6" );
+ TRIGGER_SYMBOLS.put( "win", "\u2756" );
+ // Vertical bar is special
+ TRIGGER_SYMBOLS.put( "|", " | " );
+ }
+
+ /**
+ * Sort tokens in a visually pleasing way. Makes sure we do not mess with
+ * the '|' syntax.
+ */
+ private static final void sortTokens( final String[] tokens )
+ {
+ int vbarIndex = -1;
+ for ( int i = 0; i < tokens.length; i++ )
+ {
+ if ( tokens[ i ].equals( "|" ) )
+ {
+ vbarIndex = i;
+ break;
+ }
+ }
+ if ( vbarIndex >= 0 )
+ {
+ Arrays.sort( tokens, 0, vbarIndex, Comparator.comparingInt( INPUT_TRIGGER_SYNTAX_TAGS::indexOf ) );
+ Arrays.sort( tokens, vbarIndex + 1, tokens.length, Comparator.comparingInt( INPUT_TRIGGER_SYNTAX_TAGS::indexOf ) );
+ }
+ else
+ Arrays.sort( tokens, Comparator.comparingInt( INPUT_TRIGGER_SYNTAX_TAGS::indexOf ) );
+ }
+
+ private static boolean isMac()
+ {
+ final String OS = System.getProperty( "os.name", "generic" ).toLowerCase( Locale.ENGLISH );
+ return ( OS.indexOf( "mac" ) >= 0 ) || ( OS.indexOf( "darwin" ) >= 0 );
+ }
+
+ /**
+ * Mix colors {@code c1} and {@code c2} by ratios {@code c1Weight} and
+ * {@code (1-c1Weight)}, respectively.
+ */
+ static Color mix( final Color c1, final Color c2, final double c1Weight )
+ {
+ final double c2Weight = 1.0 - c1Weight;
+ return new Color(
+ ( int ) ( c1.getRed() * c1Weight + c2.getRed() * c2Weight ),
+ ( int ) ( c1.getGreen() * c1Weight + c2.getGreen() * c2Weight ),
+ ( int ) ( c1.getBlue() * c1Weight + c2.getBlue() * c2Weight ) );
+ }
+}
diff --git a/src/main/java/org/mastodon/ui/util/CombinedFileFilter.java b/src/main/java/org/mastodon/ui/util/CombinedFileFilter.java
new file mode 100644
index 000000000..a4f57aae0
--- /dev/null
+++ b/src/main/java/org/mastodon/ui/util/CombinedFileFilter.java
@@ -0,0 +1,44 @@
+package org.mastodon.ui.util;
+
+import java.io.File;
+import javax.swing.filechooser.FileFilter;
+
+public class CombinedFileFilter extends FileFilter
+{
+
+ private FileFilter[] fileFilters;
+
+ public CombinedFileFilter( FileFilter... fileFilters )
+ {
+ this.fileFilters = fileFilters;
+ }
+
+ @Override
+ public boolean accept( File f )
+ {
+ for ( FileFilter fileFilter : fileFilters )
+ {
+ if ( fileFilter.accept( f ) )
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String getDescription()
+ {
+ StringBuilder sb = new StringBuilder();
+ for ( FileFilter fileFilter : fileFilters )
+ {
+ if ( sb.length() > 0 )
+ {
+ sb.append( " or " );
+ }
+ sb.append( fileFilter.getDescription() );
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/src/main/java/org/mastodon/ui/util/ExportUtils.java b/src/main/java/org/mastodon/ui/util/ExportUtils.java
new file mode 100644
index 000000000..f795061eb
--- /dev/null
+++ b/src/main/java/org/mastodon/ui/util/ExportUtils.java
@@ -0,0 +1,119 @@
+package org.mastodon.ui.util;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Desktop;
+import java.awt.Graphics2D;
+import java.awt.Toolkit;
+import java.awt.geom.AffineTransform;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.function.Consumer;
+
+import javax.imageio.ImageIO;
+
+import org.jfree.graphics2d.svg.SVGGraphics2D;
+import org.jfree.graphics2d.svg.SVGUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility methods for exporting JComponents as PNG or SVG files.
+ */
+public class ExportUtils
+{
+ private ExportUtils()
+ {
+ // prevent instantiation
+ }
+
+ private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() );
+
+ public static final String PNG_EXTENSION = "png";
+
+ public static final String SVG_EXTENSION = "svg";
+
+ private static final int PRINT_RESOLUTION = 600;
+
+ /**
+ * Export the given Component as PNG to the specified file.
+ *
+ * @param file
+ * the file to export to
+ * @param paintComponent
+ * the component to export
+ */
+ public static void exportPng( final File file, final Component paintComponent )
+ {
+ final int screenResolution = Toolkit.getDefaultToolkit().getScreenResolution();
+ final double scale = PRINT_RESOLUTION / ( double ) screenResolution;
+ final BufferedImage image = new BufferedImage( ( int ) ( paintComponent.getWidth() * scale ),
+ ( int ) ( paintComponent.getHeight() * scale ), BufferedImage.TYPE_INT_RGB );
+ final Graphics2D g = image.createGraphics();
+ g.setTransform( AffineTransform.getScaleInstance( scale, scale ) );
+ paintComponent.paint( g );
+ try
+ {
+ ImageIO.write( image, PNG_EXTENSION, file );
+ }
+ catch ( final IOException e )
+ {
+ logger.error( "Could not export trackscheme as PNG to File: {}.", file.getAbsolutePath(), e );
+ }
+ }
+
+ /**
+ * Export the given Component as SVG to the specified file.
+ *
+ * @param file
+ * the file to export to
+ * @param paintComponent
+ * the component to export
+ */
+ public static void exportSvg( final File file, final Component paintComponent )
+ {
+ final SVGGraphics2D g2 = new SVGGraphics2D( paintComponent.getWidth(), paintComponent.getHeight() );
+ paintComponent.paint( g2 );
+ try
+ {
+ SVGUtils.writeToSVG( file, g2.getSVGElement() );
+ }
+ catch ( final IOException e )
+ {
+ logger.error( "Could not export trackscheme as SVG to File: {}.", file.getAbsolutePath(), e );
+ }
+ }
+
+ /**
+ * Open a file chooser dialog to choose a file to export to, then export the given JComponent to that file using the given export function.
+ * @param extension the file extension to use. Supported extensions are {@link #SVG_EXTENSION} and {@link #PNG_EXTENSION}.
+ * @param exportFunction the function to export the JComponent to a file
+ * @param name the name in the file chooser dialog
+ * @param parentComponent the parent component of the file chooser dialog
+ */
+ public static void chooseFileAndExport( final String extension, final Consumer< File > exportFunction, final String name,
+ final Container parentComponent )
+ {
+ final File chosenFile = FileChooser.chooseFile( parentComponent, name + "." + extension, new ExtensionFileFilter( extension ),
+ "Save " + name + " to " + extension, FileChooser.DialogType.SAVE );
+ if ( chosenFile != null )
+ {
+ exportFunction.accept( chosenFile );
+ openFile( chosenFile );
+ }
+ }
+
+ private static void openFile( final File chosenFile )
+ {
+ try
+ {
+ Desktop.getDesktop().open( chosenFile );
+ }
+ catch ( final IOException e )
+ {
+ logger.error( "Could not open file: {}", chosenFile.getAbsolutePath(), e );
+ }
+ }
+}
diff --git a/src/main/java/org/mastodon/ui/util/HDF5FileFilter.java b/src/main/java/org/mastodon/ui/util/HDF5FileFilter.java
new file mode 100644
index 000000000..ecb2f6392
--- /dev/null
+++ b/src/main/java/org/mastodon/ui/util/HDF5FileFilter.java
@@ -0,0 +1,37 @@
+/*-
+ * #%L
+ * Mastodon
+ * %%
+ * Copyright (C) 2014 - 2022 Tobias Pietzsch, Jean-Yves Tinevez
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+package org.mastodon.ui.util;
+
+public class HDF5FileFilter extends CombinedFileFilter
+{
+ public HDF5FileFilter()
+ {
+ super( new ExtensionFileFilter( "h5" ), new ExtensionFileFilter( "hdf5" ) );
+ }
+}
diff --git a/src/main/java/org/mastodon/util/TagSetUtils.java b/src/main/java/org/mastodon/util/TagSetUtils.java
index 1952be552..0afd96d3e 100644
--- a/src/main/java/org/mastodon/util/TagSetUtils.java
+++ b/src/main/java/org/mastodon/util/TagSetUtils.java
@@ -28,7 +28,9 @@
*/
package org.mastodon.util;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
@@ -36,6 +38,7 @@
import org.mastodon.mamut.model.Model;
import org.mastodon.mamut.model.ModelGraph;
import org.mastodon.mamut.model.Spot;
+import org.mastodon.mamut.model.branch.BranchSpot;
import org.mastodon.model.tag.ObjTagMap;
import org.mastodon.model.tag.TagSetModel;
import org.mastodon.model.tag.TagSetStructure;
@@ -351,4 +354,32 @@ public static TagSetStructure.Tag findTag( final TagSetStructure.TagSet tagSet,
return tag;
throw new NoSuchElementException( "Did not find a tag with the given label: " + tagLabel );
}
+
+ /**
+ * Returns the names of all tag sets in the model.
+ * @param model the model to get the tag-set model from.
+ * @return the names of all tag sets in the model.
+ */
+ public static List< String > getTagSetNames( final Model model )
+ {
+ List< String > tagSetNames = new ArrayList<>();
+ model.getTagSetModel().getTagSetStructure().getTagSets().forEach( tagSet -> tagSetNames.add( tagSet.getName() ) );
+ return tagSetNames;
+ }
+
+ /**
+ * Gets the tag label of the first spot in the given branchSpot within the given tagSet.
+ * @param model the model to which the branch belongs
+ * @param branchSpot the branch spot
+ * @param tagSet the tag set
+ * @return the tag label
+ */
+ public static String getTagLabel( final Model model, final BranchSpot branchSpot, final TagSetStructure.TagSet tagSet, final Spot ref )
+ {
+ if ( model == null || branchSpot == null || tagSet == null )
+ return null;
+ Spot first = TreeUtils.getFirstSpot( model, branchSpot, ref );
+ TagSetStructure.Tag tag = TagSetUtils.getBranchTag( model, tagSet, first );
+ return tag == null ? null : tag.label();
+ }
}
diff --git a/src/main/java/org/mastodon/util/TreeUtils.java b/src/main/java/org/mastodon/util/TreeUtils.java
index 0a8734354..f046320f6 100644
--- a/src/main/java/org/mastodon/util/TreeUtils.java
+++ b/src/main/java/org/mastodon/util/TreeUtils.java
@@ -39,6 +39,7 @@
import org.mastodon.graph.Vertex;
import org.mastodon.mamut.model.Model;
import org.mastodon.mamut.model.Spot;
+import org.mastodon.mamut.model.branch.BranchSpot;
public class TreeUtils
{
@@ -231,12 +232,16 @@ private static < V extends Vertex
+ * If the model is empty, returns 0.
* @param model the {@link Model}
* @return the timepoint
*/
public static int getMinTimepoint( final Model model )
{
+ if ( model.getGraph().vertices().isEmpty() )
+ return 0;
int minTimepoint = Integer.MAX_VALUE;
for ( final Spot spot : model.getGraph().vertices() )
minTimepoint = Math.min( minTimepoint, spot.getTimepoint() );
@@ -244,7 +249,9 @@ public static int getMinTimepoint( final Model model )
}
/**
- * Gets the maximum timepoint in the given {@link Model} at which at least one {@link Spot} exists in the Model.
+ * Gets the maximum time point in the given {@link Model} at which at least one {@link Spot} exists in the Model.
+ *
+ * If the model is empty, returns 0.
* @param model the {@link Model}
* @return the timepoint
*/
@@ -255,4 +262,15 @@ public static int getMaxTimepoint( final Model model )
max = Math.max( max, spot.getTimepoint() );
return max;
}
+
+ /**
+ * Gets the first {@link Spot} within the given {@link BranchSpot}.
+ * @param model the {@link Model} to which the {@link BranchSpot} belongs
+ * @param branchSpot the {@link BranchSpot} to query
+ * @return the first {@link Spot}
+ */
+ public static Spot getFirstSpot( final Model model, final BranchSpot branchSpot, final Spot ref )
+ {
+ return model.getBranchGraph().getFirstLinkedVertex( branchSpot, ref );
+ }
}
diff --git a/src/main/java/org/mastodon/views/bdv/SharedBigDataViewerData.java b/src/main/java/org/mastodon/views/bdv/SharedBigDataViewerData.java
index f38d1c208..19a74a677 100644
--- a/src/main/java/org/mastodon/views/bdv/SharedBigDataViewerData.java
+++ b/src/main/java/org/mastodon/views/bdv/SharedBigDataViewerData.java
@@ -126,7 +126,9 @@ private SharedBigDataViewerData(
this.setups = setups;
this.cache = cache;
- final ViewerOptions lvo = new ViewerOptions();
+ final ViewerOptions lvo = new ViewerOptions()
+ .width( 650 )
+ .height( 400 );
this.inputTriggerConfig = ( lvo.values.getInputTriggerConfig() != null )
? lvo.values.getInputTriggerConfig()
: new InputTriggerConfig();
@@ -265,7 +267,7 @@ public Element toXmlSetupAssignments()
return elem;
}
- private void restoreFromXmlSetupAssignments( final Element parent )
+ public void restoreFromXmlSetupAssignments( final Element parent )
{
final Element elemSetupAssignments = parent.getChild( "SetupAssignments" );
if ( elemSetupAssignments == null )
@@ -471,7 +473,8 @@ private static SharedBigDataViewerData fromSpimData(
if ( !sbdv.tryLoadSettings( spimDataXmlFilename ) )
{
final BasicViewerState state = new BasicViewerState();
- state.addSource( sources.get( 0 ) );
+ for ( int i = 0; i < sources.size(); ++i )
+ state.addSource( sources.get( i ) );
state.setCurrentSource( sources.get( 0 ) );
InitializeViewerState.initBrightness( 0.001, 0.999, state, setups );
}
diff --git a/src/main/java/org/mastodon/views/bdv/overlay/OverlayNavigation.java b/src/main/java/org/mastodon/views/bdv/overlay/OverlayNavigation.java
index 25e143742..0742d72d8 100644
--- a/src/main/java/org/mastodon/views/bdv/overlay/OverlayNavigation.java
+++ b/src/main/java/org/mastodon/views/bdv/overlay/OverlayNavigation.java
@@ -31,14 +31,14 @@
import org.mastodon.model.NavigationListener;
import org.mastodon.ui.NavigationEtiquette;
-import bdv.viewer.ViewerPanel;
+import bdv.viewer.AbstractViewerPanel;
import bdv.viewer.animate.TranslationAnimator;
import net.imglib2.realtransform.AffineTransform3D;
public class OverlayNavigation< V extends OverlayVertex< V, E >, E extends OverlayEdge< E, V > >
implements NavigationListener< V, E >
{
- private final ViewerPanel panel;
+ private final AbstractViewerPanel panel;
private final OverlayGraph< V, E > graph;
@@ -47,7 +47,7 @@ public class OverlayNavigation< V extends OverlayVertex< V, E >, E extends Overl
private NavigationBehaviour< V, E > navigationBehaviour;
public OverlayNavigation(
- final ViewerPanel panel,
+ final AbstractViewerPanel panel,
final OverlayGraph< V, E > graph )
{
this.panel = panel;
@@ -84,7 +84,7 @@ public void navigateToVertex( final V vertex )
{
// Always move in T.
final int tp = vertex.getTimepoint();
- panel.setTimepoint( tp );
+ panel.state().setCurrentTimepoint( tp );
final AffineTransform3D currentTransform = panel.state().getViewerTransform();
final double[] target = navigationBehaviour.navigateToVertex( vertex, currentTransform );
@@ -105,7 +105,7 @@ public void navigateToEdge( final E edge )
final V ref = graph.vertexRef();
final int tp = edge.getTarget( ref ).getTimepoint();
graph.releaseRef( ref );
- panel.setTimepoint( tp );
+ panel.state().setCurrentTimepoint( tp );
final AffineTransform3D currentTransform = panel.state().getViewerTransform();
final double[] target = navigationBehaviour.navigateToEdge( edge, currentTransform );
diff --git a/src/main/java/org/mastodon/views/bdv/overlay/OverlayVertex.java b/src/main/java/org/mastodon/views/bdv/overlay/OverlayVertex.java
index e9447f173..e9a3339d1 100644
--- a/src/main/java/org/mastodon/views/bdv/overlay/OverlayVertex.java
+++ b/src/main/java/org/mastodon/views/bdv/overlay/OverlayVertex.java
@@ -30,6 +30,7 @@
import org.mastodon.Ref;
import org.mastodon.graph.Vertex;
+import org.mastodon.model.HasCovariance;
import org.mastodon.model.HasLabel;
import org.mastodon.spatial.HasTimepoint;
@@ -37,11 +38,8 @@
import net.imglib2.RealPositionable;
public interface OverlayVertex< O extends OverlayVertex< O, E >, E extends OverlayEdge< E, ? > >
- extends Vertex< E >, Ref< O >, RealLocalizable, RealPositionable, HasTimepoint, HasLabel
+ extends Vertex< E >, Ref< O >, RealLocalizable, RealPositionable, HasTimepoint, HasLabel, HasCovariance
{
- public void getCovariance( final double[][] mat );
-
- public void setCovariance( final double[][] mat );
public double getBoundingSphereRadiusSquared();
diff --git a/src/main/java/org/mastodon/views/grapher/display/DataDisplayFrame.java b/src/main/java/org/mastodon/views/grapher/display/DataDisplayFrame.java
index b1dad7f92..aea86d8fd 100644
--- a/src/main/java/org/mastodon/views/grapher/display/DataDisplayFrame.java
+++ b/src/main/java/org/mastodon/views/grapher/display/DataDisplayFrame.java
@@ -82,7 +82,7 @@ public DataDisplayFrame(
final UndoPointMarker undoPointMarker,
final GroupHandle groupHandle,
final ContextChooser< V > contextChooser,
- final DataDisplayOptions optional )
+ final DataDisplayOptions< DataVertex, DataEdge > optional )
{
super( "Grapher" );
@@ -112,7 +112,7 @@ public DataDisplayFrame(
*/
sidePanel = new GrapherSidePanel( nSources, contextChooser );
- sidePanel.btnPlot.addActionListener( e -> dataDisplayPanel.plot( sidePanel.getGraphConfig(), featureModel ) );
+ sidePanel.getBtnPlot().addActionListener( e -> dataDisplayPanel.plot( sidePanel.getGraphConfig(), featureModel ) );
final FeatureModelListener featureModelListener = () -> sidePanel.setFeatures(
FeatureUtils.collectFeatureMap( featureModel, vertexClass ),
@@ -163,8 +163,14 @@ public void windowClosing( final WindowEvent e )
mouseAndKeyHandler.setBehaviourMap( triggerbindings.getConcatenatedBehaviourMap() );
mouseAndKeyHandler.setKeypressManager( optional.values.getKeyPressedManager(), dataDisplayPanel.getDisplay() );
dataDisplayPanel.getDisplay().addHandler( mouseAndKeyHandler );
- setLocation( optional.values.getX(), optional.values.getY() );
setIconImages( FEATURES_ICON );
+
+ final int x = optional.values.getX();
+ final int y = optional.values.getY();
+ if ( x <= 0 && y <= 0 )
+ setLocationRelativeTo( null );
+ else
+ setLocation( x, y );
}
public DataDisplayPanel< V, E > getDataDisplayPanel()
diff --git a/src/main/java/org/mastodon/views/grapher/display/DataDisplayOptions.java b/src/main/java/org/mastodon/views/grapher/display/DataDisplayOptions.java
index ce248e583..a2f53a938 100644
--- a/src/main/java/org/mastodon/views/grapher/display/DataDisplayOptions.java
+++ b/src/main/java/org/mastodon/views/grapher/display/DataDisplayOptions.java
@@ -30,24 +30,24 @@
import java.awt.event.KeyListener;
+import org.mastodon.graph.Edge;
+import org.mastodon.graph.Vertex;
import org.mastodon.ui.NavigationEtiquette;
import org.mastodon.ui.coloring.DefaultGraphColorGenerator;
import org.mastodon.ui.coloring.GraphColorGenerator;
-import org.mastodon.views.grapher.datagraph.DataEdge;
-import org.mastodon.views.grapher.datagraph.DataVertex;
import org.mastodon.views.grapher.display.DataDisplayOverlay.DataDisplayOverlayFactory;
import org.mastodon.views.grapher.display.style.DataDisplayStyle;
import org.mastodon.views.trackscheme.display.TrackSchemeFrame;
import org.mastodon.views.trackscheme.display.TrackSchemePanel;
import org.scijava.ui.behaviour.KeyPressedManager;
-public class DataDisplayOptions
+public class DataDisplayOptions< V extends Vertex< E >, E extends Edge< V > >
{
- public final Values values = new Values();
+ public final Values< V, E > values = new Values<>();
- public static DataDisplayOptions options()
+ public static < V extends Vertex< E >, E extends Edge< V > > DataDisplayOptions< V, E > options()
{
- return new DataDisplayOptions();
+ return new DataDisplayOptions<>();
}
/**
@@ -58,7 +58,7 @@ public static DataDisplayOptions options()
* the X position.
* @return this instance.
*/
- public DataDisplayOptions x( final int x )
+ public DataDisplayOptions< V, E > x( final int x )
{
values.x = x;
return this;
@@ -72,7 +72,7 @@ public DataDisplayOptions x( final int x )
* the Y position.
* @return this instance.
*/
- public DataDisplayOptions y( final int y )
+ public DataDisplayOptions< V, E > y( final int y )
{
values.y = y;
return this;
@@ -85,7 +85,7 @@ public DataDisplayOptions y( final int y )
* the width.
* @return this instance.
*/
- public DataDisplayOptions width( final int w )
+ public DataDisplayOptions< V, E > width( final int w )
{
values.width = w;
return this;
@@ -98,7 +98,7 @@ public DataDisplayOptions width( final int w )
* the height.
* @return this instance.
*/
- public DataDisplayOptions height( final int h )
+ public DataDisplayOptions< V, E > height( final int h )
{
values.height = h;
return this;
@@ -111,7 +111,7 @@ public DataDisplayOptions height( final int h )
* the animation time in milliseconds.
* @return this instance.
*/
- public DataDisplayOptions animationDurationMillis( final long ms )
+ public DataDisplayOptions< V, E > animationDurationMillis( final long ms )
{
values.animationDurationMillis = ms;
return this;
@@ -131,7 +131,7 @@ public DataDisplayOptions animationDurationMillis( final long ms )
* the key-pressed manager.
* @return this instance.
*/
- public DataDisplayOptions shareKeyPressedEvents( final KeyPressedManager manager )
+ public DataDisplayOptions< V, E > shareKeyPressedEvents( final KeyPressedManager manager )
{
values.keyPressedManager = manager;
return this;
@@ -144,7 +144,7 @@ public DataDisplayOptions shareKeyPressedEvents( final KeyPressedManager manager
* the navigation etiquette.
* @return this instance.
*/
- public DataDisplayOptions navigationEtiquette( final NavigationEtiquette navigationEtiquette )
+ public DataDisplayOptions< V, E > navigationEtiquette( final NavigationEtiquette navigationEtiquette )
{
values.navigationEtiquette = navigationEtiquette;
return this;
@@ -157,7 +157,7 @@ public DataDisplayOptions navigationEtiquette( final NavigationEtiquette navigat
* the style.
* @return this instance.
*/
- public DataDisplayOptions style( final DataDisplayStyle style )
+ public DataDisplayOptions< V, E > style( final DataDisplayStyle style )
{
values.style = style;
return this;
@@ -170,7 +170,7 @@ public DataDisplayOptions style( final DataDisplayStyle style )
* the factory.
* @return this instance.
*/
- public DataDisplayOptions dataDisplayOverlayFactory( final DataDisplayOverlayFactory factory )
+ public DataDisplayOptions< V, E > dataDisplayOverlayFactory( final DataDisplayOverlayFactory factory )
{
values.dataDisplayOverlayFactory = factory;
return this;
@@ -184,7 +184,7 @@ public DataDisplayOptions dataDisplayOverlayFactory( final DataDisplayOverlayFac
* the color generator.
* @return this instance.
*/
- public DataDisplayOptions graphColorGenerator( final GraphColorGenerator< DataVertex, DataEdge > generator )
+ public DataDisplayOptions< V, E > graphColorGenerator( final GraphColorGenerator< V, E > generator )
{
values.graphColorGenerator = generator;
return this;
@@ -193,15 +193,15 @@ public DataDisplayOptions graphColorGenerator( final GraphColorGenerator< DataVe
/**
* Read-only {@link DataDisplayOptions} values.
*/
- public static class Values
+ public static class Values< V extends Vertex< E >, E extends Edge< V > >
{
private int x = 0;
private int y = 0;
- private int width = 700;
+ private int width = 400;
- private int height = 450;
+ private int height = 400;
private long animationDurationMillis = 500;
@@ -213,13 +213,19 @@ public static class Values
private DataDisplayOverlayFactory dataDisplayOverlayFactory = new DataDisplayOverlayFactory();
- private GraphColorGenerator< DataVertex, DataEdge > graphColorGenerator = new DefaultGraphColorGenerator<>();
+ private GraphColorGenerator< V, E > graphColorGenerator = new DefaultGraphColorGenerator<>();
- public DataDisplayOptions optionsFromValues()
+ public DataDisplayOptions< V, E > optionsFromValues()
{
- return new DataDisplayOptions().x( x ).y( y ).width( width ).height( height )
- .animationDurationMillis( animationDurationMillis ).navigationEtiquette( navigationEtiquette )
- .style( style ).dataDisplayOverlayFactory( dataDisplayOverlayFactory )
+ return new DataDisplayOptions< V, E >()
+ .x( x )
+ .y( y )
+ .width( width )
+ .height( height )
+ .animationDurationMillis( animationDurationMillis )
+ .navigationEtiquette( navigationEtiquette )
+ .style( style )
+ .dataDisplayOverlayFactory( dataDisplayOverlayFactory )
.graphColorGenerator( graphColorGenerator );
}
@@ -268,7 +274,7 @@ public DataDisplayOverlayFactory getDataDisplayOverlayFactory()
return dataDisplayOverlayFactory;
}
- public GraphColorGenerator< DataVertex, DataEdge > getGraphColorGenerator()
+ public GraphColorGenerator< V, E > getGraphColorGenerator()
{
return graphColorGenerator;
}
diff --git a/src/main/java/org/mastodon/views/grapher/display/DataDisplayOverlay.java b/src/main/java/org/mastodon/views/grapher/display/DataDisplayOverlay.java
index 134ecdbf0..486150786 100644
--- a/src/main/java/org/mastodon/views/grapher/display/DataDisplayOverlay.java
+++ b/src/main/java/org/mastodon/views/grapher/display/DataDisplayOverlay.java
@@ -118,7 +118,7 @@ public DataDisplayOverlay(
final FocusModel< DataVertex > focus,
final PaintDecorations paintDecorations,
final PaintGraph paintGraph,
- final DataDisplayOptions options )
+ final DataDisplayOptions< DataVertex, DataEdge > options )
{
this.graph = graph;
this.highlight = highlight;
@@ -380,7 +380,7 @@ public DataDisplayOverlay create(
final DataGraph< ?, ? > graph,
final HighlightModel< DataVertex, DataEdge > highlight,
final FocusModel< DataVertex > focus,
- final DataDisplayOptions options )
+ final DataDisplayOptions< DataVertex, DataEdge > options )
{
return new DataDisplayOverlay(
graph,
diff --git a/src/main/java/org/mastodon/views/grapher/display/DataDisplayPanel.java b/src/main/java/org/mastodon/views/grapher/display/DataDisplayPanel.java
index 1d555704a..ef3fb4429 100644
--- a/src/main/java/org/mastodon/views/grapher/display/DataDisplayPanel.java
+++ b/src/main/java/org/mastodon/views/grapher/display/DataDisplayPanel.java
@@ -205,14 +205,14 @@ public DataDisplayPanel(
final FocusModel< DataVertex > focus,
final SelectionModel< DataVertex, DataEdge > selection,
final NavigationHandler< DataVertex, DataEdge > navigation,
- final DataDisplayOptions optional )
+ final DataDisplayOptions< DataVertex, DataEdge > optional )
{
super( new BorderLayout(), false );
this.graph = graph;
this.layout = layout;
this.selection = selection;
- final Values options = optional.values;
+ final Values< DataVertex, DataEdge > options = optional.values;
animationMilleseconds = options.getAnimationDurationMillis();
/*
@@ -225,6 +225,8 @@ public DataDisplayPanel(
screenTransform.listeners().add( this );
transformEventHandler = new InertialScreenTransformEventHandler( screenTransform );
+ setPreferredSize( new Dimension( w, h ) );
+
/*
* Make this instance listen to data graph and UI objects.
*/
diff --git a/src/main/java/org/mastodon/views/grapher/display/DataDisplayZoom.java b/src/main/java/org/mastodon/views/grapher/display/DataDisplayZoom.java
index 37c01fcb7..5792ad9f0 100644
--- a/src/main/java/org/mastodon/views/grapher/display/DataDisplayZoom.java
+++ b/src/main/java/org/mastodon/views/grapher/display/DataDisplayZoom.java
@@ -32,13 +32,14 @@
import java.awt.Graphics;
import javax.swing.ImageIcon;
+import javax.swing.JPanel;
import org.mastodon.graph.Edge;
import org.mastodon.graph.Vertex;
-import org.mastodon.ui.keymap.KeyConfigScopes;
import org.mastodon.model.HasLabel;
import org.mastodon.spatial.HasTimepoint;
import org.mastodon.ui.keymap.KeyConfigContexts;
+import org.mastodon.ui.keymap.KeyConfigScopes;
import org.mastodon.views.grapher.datagraph.ScreenTransform;
import org.mastodon.views.grapher.display.OffsetAxes.OffsetAxesListener;
import org.mastodon.views.trackscheme.display.TrackSchemeZoom;
@@ -65,9 +66,10 @@ public class DataDisplayZoom< V extends Vertex< E > & HasTimepoint & HasLabel, E
extends AbstractNamedBehaviour
implements DragBehaviour, OffsetAxesListener, TransformListener< ScreenTransform >
{
- private static final String TOGGLE_ZOOM = "box zoom";
- private static final String[] TOGGLE_ZOOM_KEYS = new String[] { "Z" };
+ public static final String TOGGLE_ZOOM = "box zoom";
+
+ public static final String[] TOGGLE_ZOOM_KEYS = new String[] { "Z" };
/*
* Command descriptions for all provided commands
@@ -90,7 +92,7 @@ public void getCommandDescriptions( final CommandDescriptions descriptions )
public static < V extends Vertex< E > & HasTimepoint & HasLabel, E extends Edge< V > > void
install( final Behaviours behaviours, final DataDisplayPanel< V, E > panel )
{
- final DataDisplayZoom< V, E > zoom = new DataDisplayZoom<>( panel );
+ final DataDisplayZoom< V, E > zoom = new DataDisplayZoom<>( panel, panel.getTransformEventHandler() );
// Create and register overlay.
zoom.transformChanged( panel.getScreenTransform().get() );
@@ -107,7 +109,7 @@ public void getCommandDescriptions( final CommandDescriptions descriptions )
public static final Color ZOOM_GRAPH_OVERLAY_COLOR = Color.BLUE.darker();
- private final DataDisplayPanel< V, E > panel;
+ private final JPanel panel;
private final InertialScreenTransformEventHandler transformEventHandler;
@@ -119,11 +121,11 @@ public void getCommandDescriptions( final CommandDescriptions descriptions )
private final ZoomOverlay overlay;
- private DataDisplayZoom( final DataDisplayPanel< V, E > panel )
+ private DataDisplayZoom( final JPanel panel, final InertialScreenTransformEventHandler transformEventHandler )
{
super( TOGGLE_ZOOM );
this.panel = panel;
- this.transformEventHandler = panel.getTransformEventHandler();
+ this.transformEventHandler = transformEventHandler;
dragging = false;
screenTransform = new ScreenTransform();
diff --git a/src/main/java/org/mastodon/views/grapher/display/GrapherSidePanel.java b/src/main/java/org/mastodon/views/grapher/display/GrapherSidePanel.java
index db610bc85..c5ae2e7b7 100644
--- a/src/main/java/org/mastodon/views/grapher/display/GrapherSidePanel.java
+++ b/src/main/java/org/mastodon/views/grapher/display/GrapherSidePanel.java
@@ -100,7 +100,7 @@ public class GrapherSidePanel extends JPanel
private final JRadioButton rdbtnContext;
- final JButton btnPlot;
+ private final JButton btnPlot;
public GrapherSidePanel( final int nSources, final ContextChooser< ? > contextChooser )
{
@@ -254,6 +254,16 @@ public GrapherSidePanel( final int nSources, final ContextChooser< ? > contextCh
rdbtnContext.addChangeListener( e -> contextEnabler.setEnabled( rdbtnContext.isSelected() ) );
}
+ /**
+ * Exposes the plot button of this panel.
+ *
+ * @return the plot button.
+ */
+ public JButton getBtnPlot()
+ {
+ return btnPlot;
+ }
+
public < V, E > void setFeatures(
final Map< FeatureSpec< ?, V >, Feature< V > > vertexFeatures,
final Map< FeatureSpec< ?, E >, Feature< E > > edgeFeatures )
diff --git a/src/main/java/org/mastodon/views/grapher/display/InertialScreenTransformEventHandler.java b/src/main/java/org/mastodon/views/grapher/display/InertialScreenTransformEventHandler.java
index 725fbdacc..b4aad0929 100644
--- a/src/main/java/org/mastodon/views/grapher/display/InertialScreenTransformEventHandler.java
+++ b/src/main/java/org/mastodon/views/grapher/display/InertialScreenTransformEventHandler.java
@@ -31,8 +31,8 @@
import java.util.Timer;
import java.util.TimerTask;
-import org.mastodon.ui.keymap.KeyConfigScopes;
import org.mastodon.ui.keymap.KeyConfigContexts;
+import org.mastodon.ui.keymap.KeyConfigScopes;
import org.mastodon.views.grapher.datagraph.DataGraphLayout;
import org.mastodon.views.grapher.datagraph.DataGraphLayout.LayoutListener;
import org.mastodon.views.grapher.datagraph.ScreenTransform;
@@ -126,12 +126,12 @@ public void getCommandDescriptions( final CommandDescriptions descriptions )
/**
* Sets the maximal zoom level in X.
*/
- private static final double MIN_SIBLINGS_ON_CANVAS = 4;
+ private static final double DEFAULT_MIN_SCALE_X = 4;
/**
* Sets the maximal zoom level in Y.
*/
- private static final double MIN_TIMEPOINTS_ON_CANVAS = 4;
+ private static final double DEFAULT_MIN_SCALE_Y = 4;
private static final double EPSILON = 0.0000000001;
@@ -214,6 +214,10 @@ public void getCommandDescriptions( final CommandDescriptions descriptions )
*/
private boolean stayFullyZoomedOut;
+ private double minScaleX;
+
+ private double minScaleY;
+
/**
* Timer that runs {@link #currentTimerTask}.
*/
@@ -240,6 +244,8 @@ public InertialScreenTransformEventHandler( final ScreenTransformState transform
zoomScrollBehaviourX = new ZoomScrollBehaviour( ZOOM_X, ScrollAxis.X );
zoomScrollBehaviourY = new ZoomScrollBehaviour( ZOOM_Y, ScrollAxis.Y );
zoomScrollBehaviourXY = new ZoomScrollBehaviour( ZOOM_XY, ScrollAxis.XY );
+ minScaleX = DEFAULT_MIN_SCALE_X;
+ minScaleY = DEFAULT_MIN_SCALE_Y;
}
@Override
@@ -268,6 +274,16 @@ public synchronized void updateAxesSize( final int width, final int height )
updateTransformScreenSize();
}
+ public void setMinScaleY( final double minScaleY )
+ {
+ this.minScaleY = minScaleY;
+ }
+
+ public void setMinScaleX( final double minScaleX )
+ {
+ this.minScaleX = minScaleX;
+ }
+
private void updateTransformScreenSize()
{
final ScreenTransform transform = transformState.get();
@@ -282,25 +298,50 @@ private void updateTransformScreenSize()
@Override
public synchronized void layoutChanged( final DataGraphLayout< ?, ? > layout )
{
- final ScreenTransform transform = transformState.get();
+ final double minX = layout.getCurrentLayoutMinX();
+ final double maxX = layout.getCurrentLayoutMaxX();
+ final double minY = layout.getCurrentLayoutMinY();
+ final double maxY = layout.getCurrentLayoutMaxY();
+ layoutChanged( minX, maxX, minY, maxY );
+ }
- boundXMin = layout.getCurrentLayoutMinX() - boundXLayoutBorder;
- boundXMax = layout.getCurrentLayoutMaxX() + boundXLayoutBorder;
- boundYMin = layout.getCurrentLayoutMinY() - boundYLayoutBorder;
- boundYMax = layout.getCurrentLayoutMaxY() + boundYLayoutBorder;
+ /**
+ * Updates the handler so that it accommodates data contained in the
+ * specified bounds.
+ *
+ * @param minX
+ * min X bound.
+ * @param maxX
+ * max X bound.
+ * @param minY
+ * min Y bound.
+ * @param maxY
+ * max Y bound.
+ */
+ protected synchronized void layoutChanged(
+ double minX,
+ double maxX,
+ double minY,
+ double maxY )
+ {
+ this.boundXMin = minX - boundXLayoutBorder;
+ this.boundXMax = maxX + boundXLayoutBorder;
+ this.boundYMin = minY - boundYLayoutBorder;
+ this.boundYMax = maxY + boundYLayoutBorder;
- if ( boundXMax - boundXMin < MIN_SIBLINGS_ON_CANVAS )
+ final ScreenTransform transform = transformState.get();
+ if ( maxX - minX < minScaleX )
{
- final double c = ( boundXMax + boundXMin ) / 2;
- boundXMin = c - MIN_SIBLINGS_ON_CANVAS / 2;
- boundXMax = c + MIN_SIBLINGS_ON_CANVAS / 2;
+ final double c = ( maxX + minX ) / 2;
+ minX = c - minScaleX / 2;
+ maxX = c + minScaleX / 2;
}
updateMaxSizeX( transform.getScreenWidth() );
- if ( boundYMax - boundYMin < MIN_SIBLINGS_ON_CANVAS )
+ if ( maxY - minY < minScaleX )
{
- final double c = ( boundYMax + boundYMin ) / 2;
- boundYMin = c - MIN_SIBLINGS_ON_CANVAS / 2;
- boundYMax = c + MIN_SIBLINGS_ON_CANVAS / 2;
+ final double c = ( maxY + minY ) / 2;
+ minY = c - minScaleX / 2;
+ maxY = c + minScaleX / 2;
}
updateMaxSizeY( transform.getScreenHeight() );
@@ -331,11 +372,11 @@ public void setLayoutRangeY( final double layoutMinY, final double layoutMaxY )
boundYMin = layoutMinY - boundYLayoutBorder;
boundYMax = layoutMaxY + boundYLayoutBorder;
- if ( boundYMax - boundYMin < MIN_TIMEPOINTS_ON_CANVAS )
+ if ( boundYMax - boundYMin < minScaleY )
{
final double c = ( boundYMax + boundYMin ) / 2;
- boundYMin = c - MIN_TIMEPOINTS_ON_CANVAS / 2;
- boundYMax = c + MIN_TIMEPOINTS_ON_CANVAS / 2;
+ boundYMin = c - minScaleY / 2;
+ boundYMax = c + minScaleY / 2;
}
updateMaxSizeY( transformState.get().getScreenHeight() );
@@ -345,7 +386,7 @@ private void constrainTransform( final ScreenTransform transform )
{
ConstrainScreenTransform.constrainTransform(
transform,
- MIN_SIBLINGS_ON_CANVAS, MIN_TIMEPOINTS_ON_CANVAS,
+ minScaleX, minScaleY,
maxSizeX, maxSizeY,
boundXMin, boundXMax, boundYMin, boundYMax,
borderRatioX, borderRatioY,
@@ -374,12 +415,12 @@ private void zoomOutFullyY( final ScreenTransform transform )
private boolean hasMinSizeX( final ScreenTransform transform )
{
- return ConstrainScreenTransform.hasMinSizeX( transform, MIN_SIBLINGS_ON_CANVAS + EPSILON );
+ return ConstrainScreenTransform.hasMinSizeX( transform, minScaleX + EPSILON );
}
private boolean hasMinSizeY( final ScreenTransform transform )
{
- return ConstrainScreenTransform.hasMinSizeY( transform, MIN_TIMEPOINTS_ON_CANVAS + EPSILON );
+ return ConstrainScreenTransform.hasMinSizeY( transform, minScaleY + EPSILON );
}
private boolean hasMaxSizeX( final ScreenTransform transform )
@@ -507,7 +548,7 @@ public void scroll( final double wheelRotation, final boolean isHorizontal, fina
{
if ( zoomIn )
{
- if ( !hasMinSizeY( transform ) )
+ if ( !hasMinSizeY( transform ) || true )
transform.zoomY( dScale, y );
}
else
diff --git a/src/main/java/org/mastodon/views/grapher/display/style/DataDisplayStyleEditorPanel.java b/src/main/java/org/mastodon/views/grapher/display/style/DataDisplayStyleEditorPanel.java
index d77170cec..1b265b059 100644
--- a/src/main/java/org/mastodon/views/grapher/display/style/DataDisplayStyleEditorPanel.java
+++ b/src/main/java/org/mastodon/views/grapher/display/style/DataDisplayStyleEditorPanel.java
@@ -135,7 +135,8 @@ public DataDisplayStyleEditorPanel( final DataDisplayStyle style )
final SelectionModel< DataVertex, DataEdge > selection =
new SelectionModelAdapter<>( ex.getSelectionModel(), vertexMap, edgeMap );
final NavigationHandler< DataVertex, DataEdge > navigation = new DefaultNavigationHandler<>();
- final DataDisplayOptions options = DataDisplayOptions.options().style( style );
+ final DataDisplayOptions< DataVertex, DataEdge > options = DataDisplayOptions.options();
+ options.style( style );
// Layout.
diff --git a/src/main/java/org/mastodon/views/table/TableViewFrameBuilder.java b/src/main/java/org/mastodon/views/table/TableViewFrameBuilder.java
index a1838c5f1..969a92acd 100644
--- a/src/main/java/org/mastodon/views/table/TableViewFrameBuilder.java
+++ b/src/main/java/org/mastodon/views/table/TableViewFrameBuilder.java
@@ -97,6 +97,14 @@ public class TableViewFrameBuilder
private final ArrayList< Runnable > runOnClose = new ArrayList<>();
+ private int x = 0;
+
+ private int y = 0;
+
+ private int width = 500;
+
+ private int height = 300;
+
public TableViewFrameBuilder title( final String title )
{
this.title = title;
@@ -123,6 +131,30 @@ public TableViewFrameBuilder undo( final UndoPointMarker undo )
return this;
}
+ public TableViewFrameBuilder x( final int x )
+ {
+ this.x = x;
+ return this;
+ }
+
+ public TableViewFrameBuilder y( final int y )
+ {
+ this.y = y;
+ return this;
+ }
+
+ public TableViewFrameBuilder width( final int width )
+ {
+ this.width = width;
+ return this;
+ }
+
+ public TableViewFrameBuilder height( final int height )
+ {
+ this.height = height;
+ return this;
+ }
+
public MyTableViewFrame get()
{
final MyTableViewFrame frame = new MyTableViewFrame( title );
@@ -164,6 +196,16 @@ public void windowClosing( final WindowEvent e )
frame.contextChoosers.add( bundle.contextChooser );
}
+ /*
+ * Size and position.
+ */
+
+ frame.setSize( width, height );
+ if ( x <= 0 && y <= 0 )
+ frame.setLocationRelativeTo( null );
+ else
+ frame.setLocation( x, y );
+
return frame;
}
diff --git a/src/main/java/org/mastodon/views/trackscheme/display/TrackSchemeFrame.java b/src/main/java/org/mastodon/views/trackscheme/display/TrackSchemeFrame.java
index 67e6c3de2..3529ce87e 100644
--- a/src/main/java/org/mastodon/views/trackscheme/display/TrackSchemeFrame.java
+++ b/src/main/java/org/mastodon/views/trackscheme/display/TrackSchemeFrame.java
@@ -123,8 +123,14 @@ public void windowClosing( final WindowEvent e )
mouseAndKeyHandler.setBehaviourMap( triggerbindings.getConcatenatedBehaviourMap() );
mouseAndKeyHandler.setKeypressManager( optional.values.getKeyPressedManager(), trackschemePanel.getDisplay() );
trackschemePanel.getDisplay().addHandler( mouseAndKeyHandler );
- setLocation( optional.values.getX(), optional.values.getY() );
setIconImages( TRACKSCHEME_VIEW_ICON );
+
+ final int x = optional.values.getX();
+ final int y = optional.values.getY();
+ if ( x <= 0 && y <= 0 )
+ setLocationRelativeTo( null );
+ else
+ setLocation( x, y );
}
public TrackSchemePanel getTrackschemePanel()
diff --git a/src/test/java/org/mastodon/StartMastodonLauncher.java b/src/test/java/org/mastodon/StartMastodonLauncher.java
index 6514ff5ac..61004adb2 100644
--- a/src/test/java/org/mastodon/StartMastodonLauncher.java
+++ b/src/test/java/org/mastodon/StartMastodonLauncher.java
@@ -28,11 +28,14 @@
*/
package org.mastodon;
+import org.mastodon.mamut.io.loader.XmlIoN5UniverseImgLoader;
import org.mastodon.mamut.launcher.MastodonLauncherCommand;
import org.scijava.Context;
import org.scijava.command.CommandService;
import org.scijava.ui.UIService;
+import mpicbg.spim.data.generic.sequence.ImgLoaders;
+
/**
* Shows the ImageJ main window and Mastodon launcher.
*
@@ -46,6 +49,7 @@ public static void main( final String... args )
@SuppressWarnings( "resource" )
final Context context = new Context();
final UIService uiService = context.service( UIService.class );
+ ImgLoaders.registerManually( XmlIoN5UniverseImgLoader.class );
uiService.showUI();
final CommandService commandService = context.service( CommandService.class );
commandService.run( MastodonLauncherCommand.class, true );
diff --git a/src/test/java/org/mastodon/mamut/ProjectModelTestUtils.java b/src/test/java/org/mastodon/mamut/ProjectModelTestUtils.java
new file mode 100644
index 000000000..6fb423039
--- /dev/null
+++ b/src/test/java/org/mastodon/mamut/ProjectModelTestUtils.java
@@ -0,0 +1,37 @@
+package org.mastodon.mamut;
+
+import ij.ImagePlus;
+import net.imagej.ImgPlus;
+import net.imagej.axis.Axes;
+import net.imagej.axis.AxisType;
+import net.imglib2.img.Img;
+import net.imglib2.img.display.imagej.ImgToVirtualStack;
+import net.imglib2.type.numeric.real.FloatType;
+import org.mastodon.mamut.io.project.MamutProject;
+import org.mastodon.mamut.model.Model;
+import org.mastodon.views.bdv.SharedBigDataViewerData;
+import org.scijava.Context;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Objects;
+
+public class ProjectModelTestUtils
+{
+ public static ProjectModel wrapAsAppModel( final Img< FloatType > image, final Model model, final Context context, final File file )
+ throws IOException
+ {
+ final SharedBigDataViewerData sharedBigDataViewerData = asSharedBdvDataXyz( image );
+ MamutProject mamutProject = new MamutProject( file );
+ File datasetXmlFile = File.createTempFile( "test", ".xml" );
+ mamutProject.setDatasetXmlFile( datasetXmlFile );
+ return ProjectModel.create( context, model, sharedBigDataViewerData, mamutProject );
+ }
+
+ public static SharedBigDataViewerData asSharedBdvDataXyz( final Img< FloatType > image1 )
+ {
+ final ImagePlus image =
+ ImgToVirtualStack.wrap( new ImgPlus<>( image1, "image", new AxisType[] { Axes.X, Axes.Y, Axes.Z, Axes.TIME } ) );
+ return Objects.requireNonNull( SharedBigDataViewerData.fromImagePlus( image ) );
+ }
+}
diff --git a/src/test/java/org/mastodon/mamut/feature/FeatureComputerTestUtils.java b/src/test/java/org/mastodon/mamut/feature/FeatureComputerTestUtils.java
index 9c5a26b8b..fcf79f1f4 100644
--- a/src/test/java/org/mastodon/mamut/feature/FeatureComputerTestUtils.java
+++ b/src/test/java/org/mastodon/mamut/feature/FeatureComputerTestUtils.java
@@ -43,7 +43,9 @@ public class FeatureComputerTestUtils
public static < T > Feature< T > getFeature( Context context, Model model, FeatureSpec< ? extends Feature< T >, T > spec )
{
final MamutFeatureComputerService featureComputerService = getMamutFeatureComputerService( context, model );
- return Cast.unchecked( featureComputerService.compute( true, spec ).get( spec ) );
+ Feature< ? > feature = featureComputerService.compute( true, spec ).get( spec );
+ model.getFeatureModel().declareFeature( feature );
+ return Cast.unchecked( feature );
}
public static < T > FeatureProjection< T > getFeatureProjection( Context context, Model model,
diff --git a/src/test/java/org/mastodon/mamut/feature/FeatureSerializerTestUtils.java b/src/test/java/org/mastodon/mamut/feature/FeatureSerializerTestUtils.java
index c01e4ae5d..a4db2fd37 100644
--- a/src/test/java/org/mastodon/mamut/feature/FeatureSerializerTestUtils.java
+++ b/src/test/java/org/mastodon/mamut/feature/FeatureSerializerTestUtils.java
@@ -105,7 +105,7 @@ public static < T > boolean checkFeatureProjectionEquality( Feature< T > feature
Map< FeatureProjectionKey, FeatureProjection< T > > projections1 = feature1.projections().stream()
.collect( Collectors.toMap( FeatureProjection::getKey, x -> x ) );
- Map< FeatureProjectionKey, FeatureProjection< T > > projections2 = feature1.projections().stream()
+ Map< FeatureProjectionKey, FeatureProjection< T > > projections2 = feature2.projections().stream()
.collect( Collectors.toMap( FeatureProjection::getKey, x -> x ) );
if ( !projections1.keySet().equals( projections2.keySet() ) )
diff --git a/src/test/java/org/mastodon/mamut/io/ProjectLoaderTest.java b/src/test/java/org/mastodon/mamut/io/ProjectLoaderTest.java
new file mode 100644
index 000000000..7066744f1
--- /dev/null
+++ b/src/test/java/org/mastodon/mamut/io/ProjectLoaderTest.java
@@ -0,0 +1,95 @@
+package org.mastodon.mamut.io;
+
+import mpicbg.spim.data.SpimDataException;
+import net.imglib2.img.Img;
+import net.imglib2.img.array.ArrayImgs;
+import net.imglib2.type.numeric.real.FloatType;
+import net.imglib2.util.Cast;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mastodon.feature.FeatureProjection;
+import org.mastodon.feature.FeatureProjectionKey;
+import org.mastodon.mamut.ProjectModel;
+import org.mastodon.mamut.ProjectModelTestUtils;
+import org.mastodon.mamut.feature.FeatureComputerTestUtils;
+import org.mastodon.mamut.feature.MamutFeatureComputerService;
+import org.mastodon.mamut.feature.branch.BranchDisplacementDurationFeature;
+import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph1;
+import org.mastodon.mamut.model.Model;
+import org.mastodon.mamut.model.branch.BranchSpot;
+import org.scijava.Context;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * This unit test can be used to test if the ProjectLoader class can properly load and close a project file multiple times without causing memory leaks.
+ */
+public class ProjectLoaderTest
+{
+ @Ignore( "The run time of this test is too long for a unit test that is run on every build." )
+ @Test
+ public void testLoadAndCloseProjectGarbageCollection() throws IOException, SpimDataException
+ {
+ Model model = new Model();
+ Img< FloatType > image = ArrayImgs.floats( 1, 1, 1 );
+ File mastodonFile = File.createTempFile( "test", ".mastodon" );
+ try (Context context = new Context())
+ {
+ ProjectModel appModel = ProjectModelTestUtils.wrapAsAppModel( image, model, context, mastodonFile );
+ ProjectSaver.saveProject( mastodonFile, appModel );
+ }
+ for ( int i = 0; i < 100; i++ )
+ loadAndCloseProjectModel( mastodonFile );
+ assertTrue( true );
+ }
+
+ @Test
+ public void testBranchFeaturesAfterSaveAndReload() throws IOException, SpimDataException
+ {
+ ExampleGraph1 graph = new ExampleGraph1();
+ Model model = graph.getModel();
+ try (Context context = new Context())
+ {
+ File mastodonFile = File.createTempFile( "test", ".mastodon" );
+ Img< FloatType > image = ArrayImgs.floats( 1, 1, 1 );
+ ProjectModel projectModel = ProjectModelTestUtils.wrapAsAppModel( image, model, context, mastodonFile );
+ final MamutFeatureComputerService computerService = MamutFeatureComputerService.newInstance( context );
+ computerService.setModel( model );
+ FeatureProjection< BranchSpot > durationProjection = FeatureComputerTestUtils.getFeatureProjection( context, model,
+ BranchDisplacementDurationFeature.SPEC, BranchDisplacementDurationFeature.DURATION_PROJECTION_SPEC );
+ double durationBeforeSave = durationProjection.value( graph.branchSpotA );
+ ProjectModel reloadedProjectModel = saveAndReloadProject( projectModel, mastodonFile, context );
+ FeatureProjection< BranchSpot > reloadedDurationProjection = getDurationProjectionFromModel( reloadedProjectModel );
+ BranchSpot branchSpot = reloadedProjectModel.getModel().getBranchGraph().vertices().iterator().next(); // NB: the model only has one branch spot
+ double durationAfterSave = reloadedDurationProjection.value( branchSpot );
+ assertEquals( durationBeforeSave, durationAfterSave, 0 );
+ }
+ }
+
+ private void loadAndCloseProjectModel( final File mastodonFile ) throws SpimDataException, IOException
+ {
+ try (Context context = new Context())
+ {
+ ProjectModel projectModel = ProjectLoader.open( mastodonFile.getAbsolutePath(), context, false, true );
+ projectModel.close();
+ }
+ }
+
+ private static FeatureProjection< BranchSpot > getDurationProjectionFromModel( final ProjectModel reloadedProjectModel )
+ {
+ BranchDisplacementDurationFeature reloadedFeature = Cast
+ .unchecked( reloadedProjectModel.getModel().getFeatureModel().getFeature( BranchDisplacementDurationFeature.SPEC ) );
+ return reloadedFeature.project( FeatureProjectionKey.key( BranchDisplacementDurationFeature.DURATION_PROJECTION_SPEC ) );
+ }
+
+ private static ProjectModel saveAndReloadProject( final ProjectModel projectModel, final File mastodonFile, final Context context )
+ throws IOException, SpimDataException
+ {
+ ProjectSaver.saveProject( mastodonFile, projectModel );
+ return ProjectLoader.open( mastodonFile.getAbsolutePath(), context, false, true );
+ }
+}
diff --git a/src/test/java/org/mastodon/mamut/io/ProjectSaverTest.java b/src/test/java/org/mastodon/mamut/io/ProjectSaverTest.java
new file mode 100644
index 000000000..28dd94d7b
--- /dev/null
+++ b/src/test/java/org/mastodon/mamut/io/ProjectSaverTest.java
@@ -0,0 +1,97 @@
+package org.mastodon.mamut.io;
+
+import ij.ImagePlus;
+import net.imagej.ImgPlus;
+import net.imagej.axis.Axes;
+import net.imagej.axis.AxisType;
+import net.imglib2.img.Img;
+import net.imglib2.img.array.ArrayImgs;
+import net.imglib2.img.display.imagej.ImgToVirtualStack;
+import net.imglib2.type.numeric.real.FloatType;
+import org.junit.Test;
+import org.mastodon.mamut.ProjectModel;
+import org.mastodon.mamut.io.project.MamutProject;
+import org.mastodon.mamut.model.Model;
+import org.mastodon.views.bdv.SharedBigDataViewerData;
+import org.scijava.Context;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Objects;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class ProjectSaverTest
+{
+ @Test
+ public void testSaveProjectToZip() throws IOException
+ {
+ try (Context context = new Context())
+ {
+ Model model = new Model();
+ Img< FloatType > image = ArrayImgs.floats( 1, 1, 1 );
+ File mastodonFile = File.createTempFile( "test", ".mastodon" );
+ ProjectModel appModel = wrapAsAppModel( image, model, context, mastodonFile );
+ ProjectSaver.saveProject( mastodonFile, appModel );
+ assertNotNull( mastodonFile );
+ assertTrue( mastodonFile.exists() );
+ assertTrue( mastodonFile.isFile() );
+ assertTrue( mastodonFile.length() > 0 );
+ assertEquals( mastodonFile, appModel.getProject().getProjectRoot() );
+
+ ProjectSaver.saveProject( mastodonFile, appModel ); // Overwrite again
+ assertTrue( mastodonFile.exists() );
+ assertTrue( mastodonFile.isFile() );
+ assertTrue( mastodonFile.length() > 0 );
+ assertEquals( mastodonFile, appModel.getProject().getProjectRoot() );
+ }
+ }
+
+ @Test
+ public void testSaveProjectToDirectory() throws IOException
+ {
+ try (Context context = new Context())
+ {
+ Model model = new Model();
+ Img< FloatType > image = ArrayImgs.floats( 1, 1, 1 );
+ File mastodonDirectory = Files.createTempDirectory( "test" ).toFile();
+ ProjectModel appModel = wrapAsAppModel( image, model, context, mastodonDirectory );
+ ProjectSaver.saveProject( mastodonDirectory, appModel );
+ assertNotNull( mastodonDirectory );
+ assertTrue( mastodonDirectory.exists() );
+ assertTrue( mastodonDirectory.isDirectory() );
+ assertEquals( mastodonDirectory, appModel.getProject().getProjectRoot() );
+ File[] files = mastodonDirectory.listFiles();
+ assertNotNull( files );
+ assertEquals( 6, files.length );
+
+ ProjectSaver.saveProject( mastodonDirectory, appModel ); // Overwrite again
+ assertTrue( mastodonDirectory.exists() );
+ assertTrue( mastodonDirectory.isDirectory() );
+ assertEquals( mastodonDirectory, appModel.getProject().getProjectRoot() );
+ files = mastodonDirectory.listFiles();
+ assertNotNull( files );
+ assertEquals( 6, files.length );
+ }
+ }
+
+ private static ProjectModel wrapAsAppModel( final Img< FloatType > image, final Model model, final Context context, final File file )
+ throws IOException
+ {
+ final SharedBigDataViewerData sharedBigDataViewerData = asSharedBdvDataXyz( image );
+ MamutProject mamutProject = new MamutProject( file );
+ File datasetXmlFile = File.createTempFile( "test", ".xml" );
+ mamutProject.setDatasetXmlFile( datasetXmlFile );
+ return ProjectModel.create( context, model, sharedBigDataViewerData, mamutProject );
+ }
+
+ private static SharedBigDataViewerData asSharedBdvDataXyz( final Img< FloatType > image1 )
+ {
+ final ImagePlus image =
+ ImgToVirtualStack.wrap( new ImgPlus<>( image1, "image", new AxisType[] { Axes.X, Axes.Y, Axes.Z, Axes.TIME } ) );
+ return Objects.requireNonNull( SharedBigDataViewerData.fromImagePlus( image ) );
+ }
+}
diff --git a/src/test/java/org/mastodon/mamut/model/ModelPropertyListenerExample.java b/src/test/java/org/mastodon/mamut/model/ModelPropertyListenerExample.java
new file mode 100644
index 000000000..45894c1a0
--- /dev/null
+++ b/src/test/java/org/mastodon/mamut/model/ModelPropertyListenerExample.java
@@ -0,0 +1,34 @@
+package org.mastodon.mamut.model;
+
+import org.mastodon.properties.PropertyChangeListener;
+
+/**
+ * Example that demonstrates how to listen to spot property changes.
+ */
+public class ModelPropertyListenerExample
+{
+
+ public static void main( final String[] args )
+ {
+ final Model model = new Model();
+ final ModelGraph graph = model.getGraph();
+
+ final double radius = 3.;
+ final Spot spot = graph.addVertex().init( 0, new double[] { 0., 0., 0. }, radius );
+
+ final PropertyChangeListener< Spot > listener = s -> System.out.println( "The covariance of " + s + " changed!" );
+ graph.getVertexPool().covariance.propertyChangeListeners().add( listener );
+
+ final double[][] cov = new double[ 3 ][ 3 ];
+ final double newRadius = 10. * 10.;
+ covarianceFromRadiusSquared( newRadius, cov );
+ spot.setCovariance( cov );
+ }
+
+ private static void covarianceFromRadiusSquared( final double rsqu, final double[][] cov )
+ {
+ for ( int row = 0; row < 3; ++row )
+ for ( int col = 0; col < 3; ++col )
+ cov[ row ][ col ] = ( row == col ) ? rsqu : 0;
+ }
+}
diff --git a/src/test/java/org/mastodon/mamut/model/branch/BranchGraphSynchronizerTest.java b/src/test/java/org/mastodon/mamut/model/branch/BranchGraphSynchronizerTest.java
new file mode 100644
index 000000000..523b58f75
--- /dev/null
+++ b/src/test/java/org/mastodon/mamut/model/branch/BranchGraphSynchronizerTest.java
@@ -0,0 +1,74 @@
+package org.mastodon.mamut.model.branch;
+
+import net.imglib2.img.Img;
+import net.imglib2.img.array.ArrayImgs;
+import net.imglib2.type.numeric.real.FloatType;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mastodon.feature.FeatureProjection;
+import org.mastodon.mamut.ProjectModel;
+import org.mastodon.mamut.ProjectModelTestUtils;
+import org.mastodon.mamut.feature.FeatureComputerTestUtils;
+import org.mastodon.mamut.feature.MamutFeatureComputerService;
+import org.mastodon.mamut.feature.branch.BranchDisplacementDurationFeature;
+import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph1;
+import org.mastodon.mamut.model.Model;
+import org.scijava.Context;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+public class BranchGraphSynchronizerTest
+{
+
+ @Test
+ public void testKeepBranchFeaturesAfterSyncWithoutChanges() throws IOException
+ {
+ ExampleGraph1 graph = new ExampleGraph1();
+ Model model = graph.getModel();
+ try (Context context = new Context())
+ {
+ File mastodonFile = File.createTempFile( "test", ".mastodon" );
+ Img< FloatType > image = ArrayImgs.floats( 1, 1, 1 );
+ ProjectModel projectModel = ProjectModelTestUtils.wrapAsAppModel( image, model, context, mastodonFile );
+ final MamutFeatureComputerService computerService = MamutFeatureComputerService.newInstance( context );
+ computerService.setModel( model );
+ FeatureProjection< BranchSpot > durationProjection =
+ FeatureComputerTestUtils.getFeatureProjection( context, model, BranchDisplacementDurationFeature.SPEC,
+ BranchDisplacementDurationFeature.DURATION_PROJECTION_SPEC );
+ BranchGraphSynchronizer branchGraphSynchronizer = projectModel.getBranchGraphSync();
+ double durationBeforeSync = durationProjection.value( graph.branchSpotA );
+ branchGraphSynchronizer.sync();
+ double durationAfterSync = durationProjection.value( graph.branchSpotA );
+ assertEquals( durationBeforeSync, durationAfterSync, 0 );
+ }
+ }
+
+ @Ignore( "This is a known issue. The test is ignored until the issue is fixed." )
+ @Test
+ public void testKeepBranchFeaturesAfterSyncWithChanges() throws IOException
+ {
+ ExampleGraph1 graph = new ExampleGraph1();
+ Model model = graph.getModel();
+ try (Context context = new Context())
+ {
+ File mastodonFile = File.createTempFile( "test", ".mastodon" );
+ Img< FloatType > image = ArrayImgs.floats( 1, 1, 1 );
+ ProjectModel projectModel = ProjectModelTestUtils.wrapAsAppModel( image, model, context, mastodonFile );
+ final MamutFeatureComputerService computerService = MamutFeatureComputerService.newInstance( context );
+ computerService.setModel( model );
+ FeatureProjection< BranchSpot > durationProjection =
+ FeatureComputerTestUtils.getFeatureProjection( context, model, BranchDisplacementDurationFeature.SPEC,
+ BranchDisplacementDurationFeature.DURATION_PROJECTION_SPEC );
+ BranchGraphSynchronizer branchGraphSynchronizer = projectModel.getBranchGraphSync();
+ double durationBeforeSync = durationProjection.value( graph.branchSpotA );
+ model.getGraph().addVertex().init( 0, new double[] { 0, 0, 0 }, 1 );
+ model.getGraph().notifyGraphChanged();
+ branchGraphSynchronizer.sync();
+ double durationAfterSync = durationProjection.value( graph.branchSpotA );
+ assertEquals( durationBeforeSync, durationAfterSync, 0 );
+ }
+ }
+}
diff --git a/src/test/java/org/mastodon/model/BranchTrackSchemeRootsModelTest.java b/src/test/java/org/mastodon/model/BranchTrackSchemeRootsModelTest.java
new file mode 100644
index 000000000..c516cf85f
--- /dev/null
+++ b/src/test/java/org/mastodon/model/BranchTrackSchemeRootsModelTest.java
@@ -0,0 +1,65 @@
+package org.mastodon.model;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Collections;
+
+import org.junit.Test;
+import org.mastodon.mamut.model.Model;
+import org.mastodon.mamut.model.ModelGraph;
+import org.mastodon.mamut.model.Spot;
+import org.mastodon.mamut.model.branch.BranchLink;
+import org.mastodon.mamut.model.branch.BranchSpot;
+import org.mastodon.mamut.model.branch.ModelBranchGraph;
+import org.mastodon.views.trackscheme.TrackSchemeGraph;
+import org.mastodon.views.trackscheme.TrackSchemeVertex;
+import org.mastodon.views.trackscheme.wrap.DefaultModelGraphProperties;
+import org.mastodon.views.trackscheme.wrap.ModelGraphProperties;
+
+/**
+ * Tests for {@link BranchTrackSchemeRootsModel}.
+ */
+public class BranchTrackSchemeRootsModelTest
+{
+ @Test
+ public void testUpdateWhenRootIsRemoved()
+ {
+ // setup
+ final Model model = new Model();
+ final ModelGraph modelGraph = model.getGraph();
+ final Spot spotA = modelGraph.addVertex().init( 0, new double[ 3 ], 1 );
+ final ModelBranchGraph branchGraph = model.getBranchGraph();
+ branchGraph.graphRebuilt();
+ final TrackSchemeGraph< BranchSpot, BranchLink > viewGraph = createTrackSchemeGraph( branchGraph );
+ final TrackSchemeVertex vertexA = viewGraph.getRoots().iterator().next();
+
+ // run
+ final RootsModel< TrackSchemeVertex > rootsModel = new BranchTrackSchemeRootsModel( modelGraph, branchGraph, viewGraph );
+ rootsModel.setRoots( Collections.singletonList( vertexA ) );
+ modelGraph.remove( spotA );
+
+ // test
+ assertEquals( 0, rootsModel.getRoots().size() );
+ }
+
+ private static TrackSchemeGraph< BranchSpot, BranchLink > createTrackSchemeGraph( ModelBranchGraph branchGraph )
+ {
+ final ModelGraphProperties< BranchSpot, BranchLink > properties =
+ new DefaultModelGraphProperties< BranchSpot, BranchLink >()
+ {
+
+ @Override
+ public String getFirstLabel( final BranchSpot branchSpot )
+ {
+ return branchSpot.getFirstLabel();
+ }
+
+ @Override
+ public int getFirstTimePoint( final BranchSpot branchSpot )
+ {
+ return branchSpot.getFirstTimePoint();
+ }
+ };
+ return new TrackSchemeGraph<>( branchGraph, branchGraph.getGraphIdBimap(), properties );
+ }
+}
diff --git a/src/test/java/org/mastodon/model/DefaultRootsModelTest.java b/src/test/java/org/mastodon/model/DefaultRootsModelTest.java
new file mode 100644
index 000000000..116da40e6
--- /dev/null
+++ b/src/test/java/org/mastodon/model/DefaultRootsModelTest.java
@@ -0,0 +1,37 @@
+package org.mastodon.model;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Collections;
+
+import org.junit.Test;
+import org.mastodon.mamut.model.Link;
+import org.mastodon.mamut.model.ModelGraph;
+import org.mastodon.mamut.model.ModelGraphTrackSchemeProperties;
+import org.mastodon.mamut.model.Spot;
+import org.mastodon.views.trackscheme.TrackSchemeGraph;
+import org.mastodon.views.trackscheme.TrackSchemeVertex;
+
+/**
+ * Tests for {@link DefaultRootsModel}.
+ */
+public class DefaultRootsModelTest
+{
+ @Test
+ public void testUpdateWhenRootIsRemoved()
+ {
+ // setup
+ final ModelGraph graph = new ModelGraph();
+ final Spot spotA = graph.addVertex().init( 0, new double[ 3 ], 1 );
+ final TrackSchemeGraph< Spot, Link > viewGraph = new TrackSchemeGraph<>( graph, graph.getGraphIdBimap(), new ModelGraphTrackSchemeProperties( graph ) );
+ final TrackSchemeVertex vertexA = viewGraph.getRoots().iterator().next();
+
+ // run
+ final RootsModel< TrackSchemeVertex > rootsModel = new DefaultRootsModel<>( graph, viewGraph );
+ rootsModel.setRoots( Collections.singletonList( vertexA ) );
+ graph.remove( spotA );
+
+ // test
+ assertEquals( 0, rootsModel.getRoots().size() );
+ }
+}
diff --git a/src/test/java/org/mastodon/util/TagSetUtilsTest.java b/src/test/java/org/mastodon/util/TagSetUtilsTest.java
index 48f0a1421..34c30b7b6 100644
--- a/src/test/java/org/mastodon/util/TagSetUtilsTest.java
+++ b/src/test/java/org/mastodon/util/TagSetUtilsTest.java
@@ -38,9 +38,13 @@
import java.awt.Color;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class TagSetUtilsTest
@@ -110,4 +114,38 @@ public void tagSpotAndOutgoingEdges()
// 3 links are tagged: spot0 -> spot1, spot2 -> spot3, spot2 -> spot11
assertEquals( 3, model.getTagSetModel().getEdgeTags().getTaggedWith( tag0 ).size() );
}
+
+ @Test
+ public void testGetTagSetNames()
+ {
+ ExampleGraph1 exampleGraph1 = new ExampleGraph1();
+ String tagSetName1 = "TagSet1";
+ String tagSetName2 = "TagSet2";
+ String tagSetName3 = "TagSet2";
+ Collection< Pair< String, Integer > > emptyTagsAndColors = Collections.emptyList();
+ TagSetUtils.addNewTagSetToModel( exampleGraph1.getModel(), tagSetName1, emptyTagsAndColors );
+ TagSetUtils.addNewTagSetToModel( exampleGraph1.getModel(), tagSetName2, emptyTagsAndColors );
+ TagSetUtils.addNewTagSetToModel( exampleGraph1.getModel(), tagSetName3, emptyTagsAndColors );
+ Collection< String > tagSetNames = TagSetUtils.getTagSetNames( exampleGraph1.getModel() );
+ List< String > expected = Arrays.asList( tagSetName1, tagSetName2, tagSetName3 );
+ assertEquals( expected, tagSetNames );
+ }
+
+ @Test
+ public void testGetTagLabel()
+ {
+ ExampleGraph2 exampleGraph2 = new ExampleGraph2();
+ String tagSetName = "TagSet";
+ Pair< String, Integer > tag0 = Pair.of( "Tag", 0 );
+ Collection< Pair< String, Integer > > tagAndColor = Collections.singletonList( tag0 );
+ TagSetStructure.TagSet tagSet = TagSetUtils.addNewTagSetToModel( exampleGraph2.getModel(), tagSetName, tagAndColor );
+ TagSetStructure.Tag tag = tagSet.getTags().get( 0 );
+ TagSetUtils.tagBranch( exampleGraph2.getModel(), tagSet, tag, exampleGraph2.spot5 );
+ Spot ref = exampleGraph2.getModel().getGraph().vertexRef();
+ assertEquals( tag.label(), TagSetUtils.getTagLabel( exampleGraph2.getModel(), exampleGraph2.branchSpotD, tagSet, ref ) );
+ assertNull( TagSetUtils.getTagLabel( null, exampleGraph2.branchSpotD, tagSet, ref ) );
+ assertNull( TagSetUtils.getTagLabel( exampleGraph2.getModel(), null, tagSet, ref ) );
+ assertNull( TagSetUtils.getTagLabel( exampleGraph2.getModel(), exampleGraph2.branchSpotD, null, ref ) );
+ exampleGraph2.getModel().getGraph().releaseRef( ref );
+ }
}
diff --git a/src/test/java/org/mastodon/util/TreeUtilsTest.java b/src/test/java/org/mastodon/util/TreeUtilsTest.java
index 803f991f7..b3784ae0e 100644
--- a/src/test/java/org/mastodon/util/TreeUtilsTest.java
+++ b/src/test/java/org/mastodon/util/TreeUtilsTest.java
@@ -41,6 +41,7 @@
import org.mastodon.collection.RefSet;
import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph1;
import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph2;
+import org.mastodon.mamut.model.Model;
import org.mastodon.mamut.model.ModelGraph;
import org.mastodon.mamut.model.Spot;
@@ -182,7 +183,7 @@ public void testGetMinTimepoint()
{
assertEquals( 0, TreeUtils.getMinTimepoint( new ExampleGraph1().getModel() ) );
assertEquals( 0, TreeUtils.getMinTimepoint( new ExampleGraph2().getModel() ) );
-
+ assertEquals( 0, TreeUtils.getMinTimepoint( new Model() ) );
}
@Test
@@ -190,5 +191,20 @@ public void testGetMaxTimepoint()
{
assertEquals( 3, TreeUtils.getMaxTimepoint( new ExampleGraph1().getModel() ) );
assertEquals( 7, TreeUtils.getMaxTimepoint( new ExampleGraph2().getModel() ) );
+ assertEquals( 0, TreeUtils.getMaxTimepoint( new Model() ) );
+ }
+
+ @Test
+ public void testGetFirstSpot()
+ {
+ ExampleGraph1 exampleGraph1 = new ExampleGraph1();
+ Spot ref = exampleGraph1.getModel().getGraph().vertexRef();
+ assertEquals( exampleGraph1.spot0, TreeUtils.getFirstSpot( exampleGraph1.getModel(), exampleGraph1.branchSpotA, ref ) );
+ exampleGraph1.getModel().getGraph().releaseRef( ref );
+
+ ExampleGraph2 exampleGraph2 = new ExampleGraph2();
+ ref = exampleGraph2.getModel().getGraph().vertexRef();
+ assertEquals( exampleGraph2.spot5, TreeUtils.getFirstSpot( exampleGraph2.getModel(), exampleGraph2.branchSpotD, ref ) );
+ exampleGraph2.getModel().getGraph().releaseRef( ref );
}
}
diff --git a/src/test/java/org/mastodon/views/trackscheme/MamutBranchViewTrackSchemeTest.java b/src/test/java/org/mastodon/views/trackscheme/MamutBranchViewTrackSchemeTest.java
new file mode 100644
index 000000000..a17f0c401
--- /dev/null
+++ b/src/test/java/org/mastodon/views/trackscheme/MamutBranchViewTrackSchemeTest.java
@@ -0,0 +1,53 @@
+package org.mastodon.views.trackscheme;
+
+import net.imglib2.img.Img;
+import net.imglib2.img.array.ArrayImgs;
+import net.imglib2.type.numeric.real.FloatType;
+import org.junit.Test;
+import org.mastodon.feature.FeatureProjection;
+import org.mastodon.mamut.ProjectModel;
+import org.mastodon.mamut.ProjectModelTestUtils;
+import org.mastodon.mamut.feature.FeatureComputerTestUtils;
+import org.mastodon.mamut.feature.MamutFeatureComputerService;
+import org.mastodon.mamut.feature.branch.BranchDisplacementDurationFeature;
+import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph1;
+import org.mastodon.mamut.model.Model;
+import org.mastodon.mamut.model.branch.BranchSpot;
+import org.mastodon.mamut.views.trackscheme.MamutBranchViewTrackScheme;
+import org.mastodon.mamut.views.trackscheme.MamutBranchViewTrackSchemeHierarchy;
+import org.scijava.Context;
+
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+public class MamutBranchViewTrackSchemeTest
+{
+ @Test
+ public void testBranchFeaturesAfterOpeningBranchView() throws IOException, InterruptedException
+ {
+ ExampleGraph1 graph = new ExampleGraph1();
+ Model model = graph.getModel();
+ try (Context context = new Context())
+ {
+ File mastodonFile = File.createTempFile( "test", ".mastodon" );
+ Img< FloatType > image = ArrayImgs.floats( 1, 1, 1 );
+ ProjectModel projectModel = ProjectModelTestUtils.wrapAsAppModel( image, model, context, mastodonFile );
+ final MamutFeatureComputerService computerService = MamutFeatureComputerService.newInstance( context );
+ computerService.setModel( model );
+ FeatureProjection< BranchSpot > durationProjection =
+ FeatureComputerTestUtils.getFeatureProjection( context, model, BranchDisplacementDurationFeature.SPEC,
+ BranchDisplacementDurationFeature.DURATION_PROJECTION_SPEC );
+ double duration = durationProjection.value( graph.branchSpotA );
+ new MamutBranchViewTrackScheme( projectModel );
+ Thread.sleep( 1_000 );
+ double durationAfterCreatingTrackSchemeBranch = durationProjection.value( graph.branchSpotA );
+ assertEquals( duration, durationAfterCreatingTrackSchemeBranch, 0 );
+ new MamutBranchViewTrackSchemeHierarchy( projectModel );
+ Thread.sleep( 1_000 );
+ double durationAfterCreatingTrackSchemeHierarchyBranch = durationProjection.value( graph.branchSpotA );
+ assertEquals( duration, durationAfterCreatingTrackSchemeHierarchyBranch, 0 );
+ }
+ }
+}
diff --git a/src/test/resources/org/mastodon/mamut/io/loader/13457227_s3_omezarr_tczyx.xml b/src/test/resources/org/mastodon/mamut/io/loader/13457227_s3_omezarr_tczyx.xml
new file mode 100644
index 000000000..6bc54e2af
--- /dev/null
+++ b/src/test/resources/org/mastodon/mamut/io/loader/13457227_s3_omezarr_tczyx.xml
@@ -0,0 +1,543 @@
+
+