diff --git a/.github/build.sh b/.github/build.sh index 523abeb87..9be68958a 100755 --- a/.github/build.sh +++ b/.github/build.sh @@ -1,3 +1,5 @@ #!/bin/sh +export DISPLAY=:99 +sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/main/ci-build.sh sh ci-build.sh diff --git a/pom.xml b/pom.xml index 17b826134..c5cdca84c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.scijava pom-scijava - 37.0.0 + 38.0.1 org.mastodon @@ -63,7 +63,7 @@ cisd jhdf5 - + org.scijava @@ -77,7 +77,33 @@ net.imglib2 imglib2-roi - + + + + org.janelia.saalfeldlab + n5-universe + + + org.janelia.saalfeldlab + n5-google-cloud + + + org.janelia.saalfeldlab + n5-aws-s3 + + + org.janelia.saalfeldlab + n5-viewer_fiji + + + org.janelia.saalfeldlab + n5-ij + + + org.janelia.saalfeldlab + n5-zarr + + com.opencsv @@ -86,7 +112,12 @@ io.humble humble-video-all - + + + + org.jfree + jfreesvg + @@ -94,6 +125,12 @@ junit test + + uk.org.webcompere + system-stubs-junit4 + 2.1.3 + test + sc.fiji TrackMate @@ -104,7 +141,6 @@ MaMuT test - @@ -116,7 +152,7 @@ maven-surefire-plugin 2.5 - -Xms256m -Xmx1024m + -Xms256m -Xmx1536m @@ -202,6 +238,13 @@ https://github.com/xulman xulman + + Stefan Hahmann + https://github.com/stefanhahmann + + stefanhahmann + + diff --git a/src/main/java/org/mastodon/adapter/SelectionModelAdapter.java b/src/main/java/org/mastodon/adapter/SelectionModelAdapter.java index 75d67df50..c259d3f26 100644 --- a/src/main/java/org/mastodon/adapter/SelectionModelAdapter.java +++ b/src/main/java/org/mastodon/adapter/SelectionModelAdapter.java @@ -162,4 +162,10 @@ public void pauseListeners() { selection.pauseListeners(); } + + @Override + public boolean areListenersPaused() + { + return selection.areListenersPaused(); + } } diff --git a/src/main/java/org/mastodon/app/MastodonIcons.java b/src/main/java/org/mastodon/app/MastodonIcons.java index bc0a11f2d..fd3db5534 100644 --- a/src/main/java/org/mastodon/app/MastodonIcons.java +++ b/src/main/java/org/mastodon/app/MastodonIcons.java @@ -203,6 +203,9 @@ public class MastodonIcons public static final List< Image > BDV_VIEW_ICON = Arrays .asList( new Image[] { BDV_ICON_SMALL.getImage(), BDV_ICON_MEDIUM.getImage(), BDV_ICON_LARGE.getImage() } ); + public static final List< Image > BVV_VIEW_ICON = Arrays + .asList( new Image[] { BVV_ICON_SMALL.getImage(), BVV_ICON_MEDIUM.getImage(), BVV_ICON_LARGE.getImage() } ); + public static final List< Image > TRACKSCHEME_VIEW_ICON = Arrays.asList( new Image[] { TRACKSCHEME_ICON_SMALL.getImage(), TRACKSCHEME_ICON_MEDIUM.getImage(), TRACKSCHEME_ICON_LARGE.getImage() } ); diff --git a/src/main/java/org/mastodon/mamut/MainWindow.java b/src/main/java/org/mastodon/mamut/MainWindow.java index f5d5ada30..1f5d48572 100644 --- a/src/main/java/org/mastodon/mamut/MainWindow.java +++ b/src/main/java/org/mastodon/mamut/MainWindow.java @@ -38,6 +38,7 @@ import static org.mastodon.app.MastodonIcons.TAGS_ICON_MEDIUM; import static org.mastodon.app.MastodonIcons.TRACKSCHEME_ICON_MEDIUM; 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.fileMenu; @@ -61,6 +62,7 @@ import javax.swing.JPanel; import javax.swing.JSeparator; import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import org.mastodon.app.MastodonIcons; @@ -73,8 +75,11 @@ import org.mastodon.mamut.views.table.MamutViewTableFactory; import org.mastodon.mamut.views.trackscheme.MamutBranchViewTrackSchemeFactory; import org.mastodon.mamut.views.trackscheme.MamutViewTrackSchemeFactory; +import org.mastodon.ui.commandfinder.CommandFinder; import org.mastodon.ui.keymap.KeyConfigContexts; import org.mastodon.util.RunnableActionPair; +import org.scijava.ui.behaviour.util.Actions; +import org.scijava.ui.behaviour.util.InputActionBindings; import bdv.ui.keymap.Keymap; import net.miginfocom.swing.MigLayout; @@ -97,7 +102,8 @@ public MainWindow( final ProjectModel appModel ) setLocationByPlatform( true ); setLocationRelativeTo( null ); - // Re-register save actions, this time using this frame as parent component. + // Re-register save actions, this time using this frame as parent + // component. ProjectActions.installAppActions( appModel.getProjectActions(), appModel, this ); // Views: @@ -113,7 +119,7 @@ public MainWindow( final ProjectModel appModel ) prepareButton( tableButton, "table", TABLE_ICON_MEDIUM ); buttonsPanel.add( tableButton, "grow" ); - final JButton bdvButton = new JButton( new RunnableActionPair( MamutViewBdvFactory.NEW_BDV_VIEW, + final JButton bdvButton = new JButton( new RunnableActionPair( MamutViewBdvFactory.NEW_BDV_VIEW, () -> projectActionMap.get( MamutViewBdvFactory.NEW_BDV_VIEW ).actionPerformed( null ), () -> projectActionMap.get( MamutBranchViewBdvFactory.NEW_BRANCH_BDV_VIEW ).actionPerformed( null ) ) ); prepareButton( bdvButton, "bdv", BDV_ICON_MEDIUM ); @@ -123,9 +129,9 @@ public MainWindow( final ProjectModel appModel ) prepareButton( selectionTableButton, "selection table", TABLE_ICON_MEDIUM ); buttonsPanel.add( selectionTableButton, "grow" ); - final JButton trackschemeButton = new JButton( new RunnableActionPair( MamutViewTrackSchemeFactory.NEW_TRACKSCHEME_VIEW, - () -> projectActionMap.get( MamutViewTrackSchemeFactory.NEW_TRACKSCHEME_VIEW ).actionPerformed( null ), - () -> projectActionMap.get( MamutBranchViewTrackSchemeFactory.NEW_BRANCH_TRACKSCHEME_VIEW ).actionPerformed( null ) ) ); + final JButton trackschemeButton = new JButton( new RunnableActionPair( MamutViewTrackSchemeFactory.NEW_TRACKSCHEME_VIEW, + () -> projectActionMap.get( MamutViewTrackSchemeFactory.NEW_TRACKSCHEME_VIEW ).actionPerformed( null ), + () -> projectActionMap.get( MamutBranchViewTrackSchemeFactory.NEW_BRANCH_TRACKSCHEME_VIEW ).actionPerformed( null ) ) ); prepareButton( trackschemeButton, "trackscheme", TRACKSCHEME_ICON_MEDIUM ); buttonsPanel.add( trackschemeButton, "grow, wrap" ); @@ -209,6 +215,25 @@ public void windowClosing( final WindowEvent e ) // Register to when the project model is closed. appModel.projectClosedListeners().add( () -> dispose() ); + + // Command finder. + final InputActionBindings keybindings = new InputActionBindings(); + SwingUtilities.replaceUIActionMap( content, keybindings.getConcatenatedActionMap() ); + SwingUtilities.replaceUIInputMap( content, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, + keybindings.getConcatenatedInputMap() ); + final Actions mwActions = new Actions( keymap.getConfig(), KeyConfigContexts.MASTODON ); + mwActions.install( keybindings, "main" ); + CommandFinder.build() + .context( appModel.getContext() ) + .inputTriggerConfig( appModel.getKeymap().getConfig() ) + .descriptionProvider( appModel.getWindowManager().getViewFactories().getCommandDescriptions() ) + .keyConfigContext( KeyConfigContexts.MASTODON ) + .register( appModel.getModelActions() ) + .register( appModel.getProjectActions() ) + .register( appModel.getPlugins().getPluginActions() ) + .modificationListeners( appModel.getKeymap().updateListeners() ) + .parent( this ) + .installOn( mwActions ); } /** @@ -320,11 +345,16 @@ public static void addMenus( final ViewMenu menu, final ActionMap actionMap ) item( ProjectActions.SAVE_PROJECT ), item( ProjectActions.SAVE_PROJECT_AS ), separator(), + menu( "Import" ), + menu( "Export" ), + separator(), + item( ProjectActions.FIX_DATASET_PATH ), + separator(), // item( ProjectActions.IMPORT_TGMM ), // item( ProjectActions.IMPORT_SIMI ), // item( ProjectActions.IMPORT_MAMUT ), // item( ProjectActions.EXPORT_MAMUT ), - // separator(), + // separator(), item( WindowManager.PREFERENCES_DIALOG ), separator(), item( WindowManager.OPEN_ONLINE_DOCUMENTATION ) ) ); diff --git a/src/main/java/org/mastodon/mamut/MamutMenuBuilder.java b/src/main/java/org/mastodon/mamut/MamutMenuBuilder.java index 7ed8063f1..be2d461ac 100644 --- a/src/main/java/org/mastodon/mamut/MamutMenuBuilder.java +++ b/src/main/java/org/mastodon/mamut/MamutMenuBuilder.java @@ -59,6 +59,7 @@ public class MamutMenuBuilder extends ViewMenuBuilder menuTexts.put( ProjectActions.LOAD_PROJECT, "Load Project" ); menuTexts.put( ProjectActions.SAVE_PROJECT, "Save Project" ); menuTexts.put( ProjectActions.SAVE_PROJECT_AS, "Save Project As..." ); + menuTexts.put( ProjectActions.FIX_DATASET_PATH, "Fix Image Path" ); menuTexts.put( ProjectActions.IMPORT_TGMM, "Import TGMM tracks" ); menuTexts.put( ProjectActions.IMPORT_SIMI, "Import Simi BioCell tracks" ); menuTexts.put( ProjectActions.IMPORT_MAMUT, "Import MaMuT project" ); diff --git a/src/main/java/org/mastodon/mamut/MamutViews.java b/src/main/java/org/mastodon/mamut/MamutViews.java index 62f526a90..d44f21a3d 100644 --- a/src/main/java/org/mastodon/mamut/MamutViews.java +++ b/src/main/java/org/mastodon/mamut/MamutViews.java @@ -102,7 +102,7 @@ public < T extends MamutViewI > MamutViewFactory< T > getFactory( final Class< T return ( MamutViewFactory< T > ) factories.get( klass ); } - CommandDescriptionProvider getCommandDescriptions() + public CommandDescriptionProvider getCommandDescriptions() { return new CommandDescriptionProvider( KeyConfigScopes.MAMUT, KeyConfigContexts.MASTODON ) { diff --git a/src/main/java/org/mastodon/mamut/WindowManager.java b/src/main/java/org/mastodon/mamut/WindowManager.java index c455cc69d..c5cdb7c7c 100644 --- a/src/main/java/org/mastodon/mamut/WindowManager.java +++ b/src/main/java/org/mastodon/mamut/WindowManager.java @@ -62,7 +62,16 @@ import org.mastodon.mamut.model.Spot; import org.mastodon.mamut.views.MamutViewFactory; import org.mastodon.mamut.views.MamutViewI; +import org.mastodon.mamut.views.bdv.MamutBranchViewBdv; +import org.mastodon.mamut.views.bdv.MamutViewBdv; +import org.mastodon.mamut.views.grapher.MamutViewGrapher; +import org.mastodon.mamut.views.table.MamutViewSelectionTable; +import org.mastodon.mamut.views.table.MamutViewTable; +import org.mastodon.mamut.views.trackscheme.MamutBranchViewTrackScheme; +import org.mastodon.mamut.views.trackscheme.MamutBranchViewTrackSchemeHierarchy; +import org.mastodon.mamut.views.trackscheme.MamutViewTrackScheme; import org.mastodon.model.tag.ui.TagSetDialog; +import org.mastodon.ui.coloring.ColorBarOverlay.Position; import org.mastodon.ui.coloring.TrackGraphColorGenerator; import org.mastodon.ui.coloring.feature.FeatureColorModeManager; import org.mastodon.ui.keymap.KeyConfigContexts; @@ -71,6 +80,7 @@ import org.mastodon.views.context.ContextProvider; import org.mastodon.views.context.HasContextChooser; import org.mastodon.views.context.HasContextProvider; +import org.mastodon.views.trackscheme.ScreenTransform; import org.scijava.Context; import org.scijava.listeners.Listeners; import org.scijava.plugin.Plugin; @@ -85,6 +95,8 @@ import bdv.ui.keymap.KeymapManager; import bdv.ui.settings.SettingsPage; import bdv.util.InvokeOnEDT; +import bdv.viewer.ViewerPanel; +import net.imglib2.realtransform.AffineTransform3D; /** * Main GUI class for the Mastodon Mamut application. @@ -100,13 +112,19 @@ public class WindowManager { public static final String PREFERENCES_DIALOG = "Preferences"; + public static final String TAGSETS_DIALOG = "edit tag sets"; + public static final String COMPUTE_FEATURE_DIALOG = "compute features"; + public static final String OPEN_ONLINE_DOCUMENTATION = "open online documentation"; static final String[] PREFERENCES_DIALOG_KEYS = new String[] { "meta COMMA", "ctrl COMMA" }; + static final String[] TAGSETS_DIALOG_KEYS = new String[] { "not mapped" }; + static final String[] COMPUTE_FEATURE_DIALOG_KEYS = new String[] { "not mapped" }; + static final String[] OPEN_ONLINE_DOCUMENTATION_KEYS = new String[] { "not mapped" }; public static final String DOCUMENTATION_URL = "https://mastodon.readthedocs.io/en/latest/"; @@ -359,9 +377,321 @@ public synchronized < T extends MamutViewI > T createView( final Class< T > klas } /** - * Adds the create view sub-menu 'Window' to the specified menu, using the specified action-map. - * @param menu the menu to add to. - * @param actionMap the action map of the frame where the menu is. + * Creates and displays a new BDV view, with default display settings. + * + * @return the new BDV view. + */ + public MamutViewBdv createBigDataViewer() + { + return createBigDataViewer( new HashMap<>() ); + } + + /** + * Creates and displays a new BDV 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: + *

+ * + * @param guiState + * the map of settings. + * @return the new BDV view. + */ + public MamutViewBdv createBigDataViewer( final Map< String, Object > guiState ) + { + return createView( MamutViewBdv.class, guiState ); + } + + /** + * Creates and displays a new TrackScheme view, with default display + * settings. + * + * @return the new trackscheme view. + */ + public MamutViewTrackScheme createTrackScheme() + { + return createTrackScheme( new HashMap<>() ); + } + + /** + * Creates and displays a new BDV 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: + *

+ * + * @param guiState + * the map of settings. + * @return the new trackscheme view. + */ + public MamutViewTrackScheme createTrackScheme( final Map< String, Object > guiState ) + { + return createView( MamutViewTrackScheme.class, guiState ); + } + + /** + * Creates and displays a new Selection Table view. + *

+ * 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: + *

+ * + * @param guiState + * the map of settings. + * @return the new table view. + */ + public MamutViewTable createTable( final Map< String, Object > guiState ) + { + return createView( MamutViewTable.class, guiState ); + } + + /** + * Creates and displays a new Grapher view, with default display settings. + * + * @return the new grapher view. + */ + public MamutViewGrapher createGrapher() + { + return createGrapher( new HashMap<>() ); + } + + /** + * Creates and displays a new Grapher 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: + *

+ * + * @param guiState + * the map of settings. + * @return the new grapher view. + */ + public MamutViewGrapher createGrapher( final Map< String, Object > guiState ) + { + return createView( MamutViewGrapher.class, guiState ); + } + + /** + * Creates and displays a new Branch-BDV view, with default display + * settings. The branch version of this view displays the branch graph. + * + * @return the new branch BDV view. + */ + public MamutBranchViewBdv createBranchBigDataViewer() + { + return createBranchBigDataViewer( new HashMap<>() ); + } + + /** + * Creates and displays a new Branch-BDV view, using a map to specify the + * display settings. + * + * @param guiState + * the settings map. + * @return the new branch BDV view. + * @see #createBigDataViewer(Map) + */ + public MamutBranchViewBdv createBranchBigDataViewer( final Map< String, Object > guiState ) + { + return createView( MamutBranchViewBdv.class, guiState ); + } + + /** + * Creates and displays a new Branch-TrackScheme view, with default display + * settings. The branch version of this view displays the branch graph. + * + * @return the new branch TrackScheme view. + */ + public MamutBranchViewTrackScheme createBranchTrackScheme() + { + return createBranchTrackScheme( new HashMap<>() ); + } + + /** + * Creates and displays a new Branch-TrackScheme view, using a map to + * specify the display settings. + * + * @see #createTrackScheme(Map) + * @return the new branch TrackScheme view. + * @param guiState + * the settings map. + */ + public MamutBranchViewTrackScheme createBranchTrackScheme( final Map< String, Object > guiState ) + { + return createView( MamutBranchViewTrackScheme.class, guiState ); + } + + /** + * Creates and displays a new Hierarchy-TrackScheme view, with default + * display settings. + * + * @return the new hierarchy TrackScheme view. + */ + public MamutBranchViewTrackSchemeHierarchy createHierarchyTrackScheme() + { + return createHierarchyTrackScheme( new HashMap<>() ); + } + + /** + * Creates and displays a new Hierarchy-TrackScheme view, using a map to + * specify the display settings. + * + * @param guiState + * the settings map. + * @return the new hierarchy TrackScheme view. + * @see #createTrackScheme(Map) + */ + public MamutBranchViewTrackSchemeHierarchy createHierarchyTrackScheme( final Map< String, Object > guiState ) + { + return createView( MamutBranchViewTrackSchemeHierarchy.class, guiState ); + } + + /** + * Adds the create view sub-menu 'Window' to the specified menu, using the + * specified action-map. + * + * @param menu + * the menu to add to. + * @param actionMap + * the action map of the frame where the menu is. */ public void addWindowMenu( final ViewMenu menu, final ActionMap actionMap ) { diff --git a/src/main/java/org/mastodon/mamut/feature/SpotPositionFeature.java b/src/main/java/org/mastodon/mamut/feature/SpotPositionFeature.java index b5eff98a0..e559e0110 100644 --- a/src/main/java/org/mastodon/mamut/feature/SpotPositionFeature.java +++ b/src/main/java/org/mastodon/mamut/feature/SpotPositionFeature.java @@ -55,7 +55,7 @@ public class SpotPositionFeature implements Feature< Spot > private final LinkedHashMap< FeatureProjectionKey, FeatureProjection< Spot > > projections; - private static final List< FeatureProjectionSpec > PROJECTION_SPECS = new ArrayList<>( 3 ); + public static final List< FeatureProjectionSpec > PROJECTION_SPECS = new ArrayList<>( 3 ); static { for ( int d = 0; d < 3; d++ ) diff --git a/src/main/java/org/mastodon/mamut/io/DatasetPathDialog.java b/src/main/java/org/mastodon/mamut/io/DatasetPathDialog.java new file mode 100644 index 000000000..a0f496d4d --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/DatasetPathDialog.java @@ -0,0 +1,413 @@ +/*- + * #%L + * mastodon-tomancak + * %% + * Copyright (C) 2018 - 2024 Tobias Pietzsch + * %% + * 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; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Frame; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.ActionMap; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.InputMap; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.SpinnerNumberModel; +import javax.swing.WindowConstants; + +import org.mastodon.mamut.ProjectModel; +import org.mastodon.mamut.io.project.MamutProject; +import org.mastodon.ui.util.ExtensionFileFilter; +import org.mastodon.ui.util.FileChooser; + +public class DatasetPathDialog extends JDialog +{ + + private static final long serialVersionUID = 1L; + + private final Path projectRootWoMastodonFile; + + public DatasetPathDialog( final Frame owner, final ProjectModel appModel ) + { + super( owner, "Edit Dataset Path...", true ); + + final MamutProject project = appModel.getProject(); + final boolean projectInContainerFile = project.getProjectRoot().isFile(); + projectRootWoMastodonFile = projectInContainerFile ? project.getProjectRoot().toPath().getParent() : project.getProjectRoot().toPath(); + + final JPanel content = new JPanel(); + content.setLayout( new GridBagLayout() ); + content.setBorder( BorderFactory.createEmptyBorder( 30, 20, 20, 20 ) ); + + final GridBagConstraints c = new GridBagConstraints(); + c.gridy = 0; + c.gridx = 0; + c.anchor = GridBagConstraints.LINE_START; + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 0.0; + content.add( new JLabel( "Current project path: " ), c ); + + c.gridx = 1; + final JLabel rootPathTextField = new JLabel( tellProjectPath( !project.isDatasetXmlPathRelative() ) ); + content.add( rootPathTextField, c ); + + ++c.gridy; + c.gridx = 0; + content.add( new JLabel( "Current BDV dataset path: " ), c ); + + String initialPathValue = tellXmlFilePath( convertFromWinOrLeaveAsIs( project.getDatasetXmlFile().toPath() ), + !project.isDatasetXmlPathRelative() ); + if ( projectInContainerFile && project.isDatasetXmlPathRelative() && initialPathValue.startsWith( ".." ) ) + { + // this is hacky, it removes the leading "../" or "..\" from the + // (for sure!) relative path, which + // was here to "get out of" the .mastodon container file, and which + // also confuses the Java Path functions... + initialPathValue = initialPathValue.substring( 3 ); + } + final JTextField xmlPathTextField = new JTextField( initialPathValue ); + c.gridx = 1; + c.weightx = 1.0; + content.add( xmlPathTextField, c ); + xmlPathTextField.setColumns( 50 ); + + final JButton browseButton = new JButton( "Browse" ); + c.gridx = 2; + c.weightx = 0.0; + content.add( browseButton, c ); + + ++c.gridy; + c.gridx = 0; + content.add( new JLabel( "Store as absolute path: " ), c ); + final JCheckBox storeAbsoluteCheckBox = new JCheckBox(); + storeAbsoluteCheckBox.setSelected( !project.isDatasetXmlPathRelative() ); + + c.gridx = 1; + content.add( storeAbsoluteCheckBox, c ); + + c.gridx = 2; + final JButton testButton = new JButton( "Test Path" ); + content.add( testButton, c ); + // + final Color normalBgColor = xmlPathTextField.getBackground(); + testButton.addChangeListener( l -> { + if ( testButton.getModel().isPressed() ) + { + final File f = new File( tellXmlFilePath( Paths.get( xmlPathTextField.getText() ), true ) ); + xmlPathTextField.setBackground( f.isFile() ? Color.GREEN : Color.RED ); + } + else + { + xmlPathTextField.setBackground( normalBgColor ); + } + } ); + + final JPanel infoLine = new JPanel(); + infoLine.setLayout( new BoxLayout( infoLine, BoxLayout.LINE_AXIS ) ); + infoLine.add( Box.createHorizontalGlue() ); + infoLine.add( new JLabel( "Save the project eventually to make the changes permanent." ) ); + + final JPanel buttons = new JPanel(); + final JButton dummy = new JButton( "I want dummy image data instead" ); + final JButton cancel = new JButton( "Cancel" ); + final JButton ok = new JButton( "OK" ); + buttons.setLayout( new BoxLayout( buttons, BoxLayout.LINE_AXIS ) ); + buttons.add( dummy ); + buttons.add( Box.createHorizontalGlue() ); + buttons.add( cancel ); + buttons.add( ok ); + + getContentPane().add( content, BorderLayout.NORTH ); + getContentPane().add( infoLine, BorderLayout.CENTER ); + getContentPane().add( buttons, BorderLayout.SOUTH ); + + class Browse implements ActionListener + { + private final JTextField path; + + private final String dialogTitle; + + public Browse( final JTextField path, final String dialogTitle ) + { + this.path = path; + this.dialogTitle = dialogTitle; + } + + @Override + public void actionPerformed( final ActionEvent e ) + { + final File file = FileChooser.chooseFile( + true, + DatasetPathDialog.this, + tellXmlFilePath( Paths.get( path.getText() ), true ), + new ExtensionFileFilter( "xml" ), + dialogTitle, + FileChooser.DialogType.LOAD, + FileChooser.SelectionMode.FILES_ONLY ); + if ( file != null ) + path.setText( tellXmlFilePath( file.toPath(), storeAbsoluteCheckBox.isSelected() ) ); + } + } + browseButton.addActionListener( new Browse( xmlPathTextField, "Select BDV XML file" ) ); + + storeAbsoluteCheckBox.addActionListener( e -> { + rootPathTextField.setText( tellProjectPath( storeAbsoluteCheckBox.isSelected() ) ); + xmlPathTextField.setText( tellXmlFilePath( Paths.get( xmlPathTextField.getText() ), storeAbsoluteCheckBox.isSelected() ) ); + } ); + + ok.addActionListener( e -> { + final String path = xmlPathTextField.getText(); + final boolean relative = !storeAbsoluteCheckBox.isSelected(); + + // always give the absolute path! -- the underlying spim_data + // library "relativyfies" + // the path on its own (provided the set..Xml..Relative() is set to + // true) + final File xmlFilePath = new File( tellXmlFilePath( Paths.get( path ), true ) ); + project.setDatasetXmlFile( xmlFilePath ); + project.setDatasetXmlPathRelative( relative ); + System.out.println( "Storing BDV xml path as " + xmlFilePath + " (should be relative: " + relative + ")" ); + close(); + } ); + + cancel.addActionListener( e -> close() ); + + dummy.addActionListener( e -> { + final DummyImageDataParams params = new DummyImageDataParams( owner, appModel ); + if ( !params.wasOkClosed ) + return; + + rootPathTextField.setText( tellProjectPath( true ) ); + xmlPathTextField.setText( "x=" + params.xSize + + " y=" + params.ySize + " z=" + params.zSize + + " sx=1 sy=1 sz=1 t=" + params.timePoints + ".dummy" ); + storeAbsoluteCheckBox.setSelected( false ); + storeAbsoluteCheckBox.setEnabled( false ); + browseButton.setEnabled( false ); + testButton.setEnabled( false ); + } ); + + addWindowListener( new WindowAdapter() + { + @Override + public void windowClosing( final WindowEvent e ) + { + close(); + } + } ); + + final ActionMap am = getRootPane().getActionMap(); + final InputMap im = getRootPane().getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + final Object hideKey = new Object(); + final Action hideAction = new AbstractAction() + { + @Override + public void actionPerformed( final ActionEvent e ) + { + close(); + } + + private static final long serialVersionUID = 1L; + }; + im.put( KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0 ), hideKey ); + am.put( hideKey, hideAction ); + + pack(); + setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE ); + setLocationRelativeTo( null ); + } + + private void close() + { + setVisible( false ); + dispose(); + } + + private static Path convertFromWinOrLeaveAsIs( final Path relativePath ) + { + return Paths.get( relativePath.toString().replace( "\\", "/" ) ); + } + + private String tellXmlFilePath( final Path xmlFilePath, final boolean tellAsAbsolutePath ) + { + if ( tellAsAbsolutePath ) + { + if ( xmlFilePath.isAbsolute() ) + return xmlFilePath.toString(); + else + return projectRootWoMastodonFile.resolve( xmlFilePath ).toString(); + } + else + { + // should create relative paths + if ( xmlFilePath.isAbsolute() ) + return projectRootWoMastodonFile.relativize( xmlFilePath ).toString(); + else + return xmlFilePath.toString(); + } + } + + private String tellProjectPath( final boolean tellAsAbsolutePath ) + { + return tellAsAbsolutePath ? "(this path is not considered now)" : projectRootWoMastodonFile.toString(); + } + + private static class DummyImageDataParams extends JDialog + { + + private static final long serialVersionUID = 1L; + + public DummyImageDataParams( final Frame owner, final ProjectModel appModel ) + { + super( owner, "Adjust Dummy Dataset Parameters", true ); + + final JPanel content = new JPanel(); + content.setLayout( new GridBagLayout() ); + content.setBorder( BorderFactory.createEmptyBorder( 10, 10, 10, 10 ) ); + + final GridBagConstraints c = new GridBagConstraints(); + c.anchor = GridBagConstraints.LINE_START; + c.fill = GridBagConstraints.HORIZONTAL; + c.gridy = 0; + + final JSpinner xSpinner = new JSpinner( new SpinnerNumberModel( xSize, 10, 10000, 100 ) ); + final JSpinner ySpinner = new JSpinner( new SpinnerNumberModel( ySize, 10, 10000, 100 ) ); + final JSpinner zSpinner = new JSpinner( new SpinnerNumberModel( zSize, 10, 10000, 100 ) ); + final JSpinner tpSpinner = new JSpinner( new SpinnerNumberModel( timePoints, 10, 10000, 100 ) ); + + c.gridx = 0; + content.add( new JLabel( "Size in pixels in X: " ), c ); + c.gridx = 1; + content.add( xSpinner, c ); + + c.gridy++; + c.gridx = 0; + content.add( new JLabel( "Size in pixels in Y: " ), c ); + c.gridx = 1; + content.add( ySpinner, c ); + + c.gridy++; + c.gridx = 0; + content.add( new JLabel( "Size in pixels in Z: " ), c ); + c.gridx = 1; + content.add( zSpinner, c ); + + c.gridy++; + c.gridx = 0; + content.add( new JLabel( "Number of time points: " ), c ); + c.gridx = 1; + content.add( tpSpinner, c ); + + c.gridy++; + c.gridx = 0; + final JButton fromSpots = new JButton( "From spots" ); + if ( appModel == null ) + { + fromSpots.setEnabled( false ); + } + else + { + fromSpots.addActionListener( l -> { + final double[] max = new double[ 3 ]; + final double[] pos = new double[ 3 ]; + appModel.getModel() + .getSpatioTemporalIndex() + .forEach( s -> { + s.localize( pos ); + if ( pos[ 0 ] > max[ 0 ] ) + max[ 0 ] = pos[ 0 ]; + if ( pos[ 1 ] > max[ 1 ] ) + max[ 1 ] = pos[ 1 ]; + if ( pos[ 2 ] > max[ 2 ] ) + max[ 2 ] = pos[ 2 ]; + } ); + xSpinner.setValue( ( int ) Math.floor( 1.1 * max[ 0 ] ) ); + ySpinner.setValue( ( int ) Math.floor( 1.1 * max[ 1 ] ) ); + zSpinner.setValue( ( int ) Math.floor( 1.1 * max[ 2 ] ) ); + tpSpinner.setValue( appModel.getMaxTimepoint() + 1 ); + } ); + } + content.add( fromSpots, c ); + // + c.gridx = 1; + final JButton ok = new JButton( "OK" ); + ok.addActionListener( l -> { + xSize = ( int ) ( xSpinner.getValue() ); + ySize = ( int ) ( ySpinner.getValue() ); + zSize = ( int ) ( zSpinner.getValue() ); + timePoints = ( int ) ( tpSpinner.getValue() ); + wasOkClosed = true; + close(); + } ); + content.add( ok, c ); + + getContentPane().add( content ); + pack(); + setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE ); + setLocationRelativeTo( owner ); + setVisible( true ); + } + + private void close() + { + setVisible( false ); + dispose(); + } + + int xSize = 1000; + + int ySize = 1000; + + int zSize = 1000; + + int timePoints = 1000; + + boolean wasOkClosed = false; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/MamutViewStateXMLSerialization.java b/src/main/java/org/mastodon/mamut/io/MamutViewStateXMLSerialization.java index 363641d17..3f26422d2 100644 --- a/src/main/java/org/mastodon/mamut/io/MamutViewStateXMLSerialization.java +++ b/src/main/java/org/mastodon/mamut/io/MamutViewStateXMLSerialization.java @@ -70,9 +70,9 @@ /** * Utility class that can transform a GUI state - * 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 + *

+ * + * + * @return an {@link EmptyArrayCreator} for {@code A} or null. + */ + default EmptyArrayCreator< A > getEmptyArrayCreator() + { + return null; + } + + /** + * Load cell data into memory. This method blocks until data is successfully + * loaded. If it completes normally, the returned data is always valid. If + * anything goes wrong, an {@link IOException} is thrown. + *

+ * {@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, E extends Edge< V > > RefSet< V > filterRo } /** - * Gets the minimum timepoint in the given {@link Model} at which at least one {@link Spot} exists in the Model. + * Gets the minimum 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 */ 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 @@ + + + . + + + + + 0 + 0 + + + 1 + 1 + + + 2 + 2 + + + 3 + 3 + + + + 0 + Setup0 + 2048 2048 35 + + micrometer + 0.21667 0.21667 0.4 + + + 0 + + + + 1 + Setup1 + 2048 2048 35 + + micrometer + 0.21667 0.21667 0.4 + + + 1 + + + + 2 + Setup2 + 2048 2048 35 + + micrometer + 0.21667 0.21667 0.4 + + + 2 + + + + 3 + Setup3 + 2048 2048 35 + + micrometer + 0.21667 0.21667 0.4 + + + 3 + + + + + 0 + 17 + + + https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.4/idr0101A/13457227.zarr + / + + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + + 0.21667 0.0 0.0 0.0 0.0 0.21667 0.0 0.0 0.0 0.0 + 0.4 0.0 + + + + \ No newline at end of file