diff --git a/CLAUDE.md b/CLAUDE.md index 28889e415..aaf1bb47c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,6 +104,14 @@ image: the `@Parameter( type = ItemIO.OUTPUT )` on the `dataset` field only hands it to the caller, and a script that captures it decides what to do with it. Being plain text rather than a chooser, it accepts `http(s):` too, but *not* `s3:`. +A second one is dropping onto the image area of a BDV window this plugin opened: `OmeZarrBdvDropListener.install` +(called by `OmeZarrOpenActions.showInBdv` for a new window, and by `OpenInBDVCommand`) puts an AWT `DropTarget` on the +viewer's image area, whose `OmeZarrBdvDropListener` adds the dropped OME-Zarr to *that* window via +`OmeZarr.showInBdv( BdvHandle )`, ignoring the configured opener — where it was dropped already says what the user wants. It validates with `isZarr` like every other route (so no `s3:`) and +reads with the persisted backend and preferred width, loaded at drop time. It lives in `fiji-ui` because those settings +do, so a script calling `OmeZarr.showInBdv()` directly gets a window without it. BDV windows opened by other tools get +no drop target: see issue #127 and bigdataviewer/bigdataviewer-core#227. + `OmeZarr.showInImageJ()`/`showInBdv()` return what they showed (`null` on failure or when the user declined) and `OmeZarrOpenActions` mirrors that; the plugin's own call sites want only the side effect, hence the `@SuppressWarnings( "UnusedReturnValue" )` — the values exist for API and script users. @@ -240,8 +248,8 @@ i.e. a SciJava plugin anyway. what a persisted setting means (`effectiveOpenerName`). - Shipped openers – `imagej-preferred-resolution`, `imagej-highest-resolution`, `bdv-multi-resolution`, - `n5-importer-dialog`, `n5-viewer-dialog`, `script-editor` – are one class each, next to the `OmeZarrOpenActions` they - drive. Only the extension point itself sits in `ome-zarr-fiji`: that is the artifact a downstream opener compiles + `bdv-add-to-active`, `n5-importer-dialog`, `n5-viewer-dialog`, `script-editor` – are one class each, next to the + `OmeZarrOpenActions` they drive. Only the extension point itself sits in `ome-zarr-fiji`: that is the artifact a downstream opener compiles against, and it stays free of concrete openers. Help is a plain button, not an opener. The selection dialog (`OmeZarrOpenActionChooser`, one icon button per offered opener) is **not** an opener: it opens @@ -265,9 +273,17 @@ it is not worth the code. A 0.7-and-earlier preference therefore reads as an unk `info` log, nothing thrown – and the next visit to `OpeningBehaviorSettings` overwrites it. `getOpenerName()` returns `null` for "never configured", which is what lets the priority rule above apply. -**Active-window tracking:** `PyramidalService` (a SciJava service) tracks the most-recently-focused `Pyramidal` window -(BDV or ImageJ) via AWT focus listening; `PyramidalPreprocessor` auto-fills any `Pyramidal`-typed command parameter -with the currently active one. +**Active-window tracking:** `ActiveViewerService` is the only AWT focus listener: it tracks the most recently focused +image viewer, an ImageJ `ImageWindow` or a BDV `ViewerPanel`, whoever opened it. `PyramidalService` follows it to track +the active `Pyramidal`, which `PyramidalPreprocessor` auto-fills into any `Pyramidal`-typed command parameter. +"BigDataViewer (add to active window)" asks it for the active viewer and `BdvUtils.bdvHandleOf` for its handle. **Keep +it one listener**: "an ImageJ window takes over, the toolbar and dialogs don't" is a precedence rule both consumers need, +and two trackers drifting apart is what caused the "sticky BDV" bug (`ed658c70`). It listens to `"focusOwner"`, not +`"activeWindow"`, so that two viewers in one window (BigWarp) stay apart. Focus in a BDV side panel lies outside the +`ViewerPanel`, hence the "only viewer in the window" fallback. Focus events arrive after `show()` returns, so +`OmeZarr.showInBdv` and `OmeZarr.showAsDataset` report the new viewer themselves. `bdvHandleOf` reads a client property +`BdvUtils.show` puts on the panel, mirroring the `BdvHandle.of( ViewerPanel )` proposed upstream in +bigdataviewer/bigdataviewer-core#227 — once that exists, delegate to it and BDVs opened by other tools work too. Note: `BdvHandleService` is test/example-only now (`ome-zarr-fiji-ui/src/test/java/ome/zarr/examples/demo/`), not part of the shipped plugin. @@ -329,7 +345,7 @@ registered) would be the alternative. Java package names stay `ome.zarr.*` throu pyramids). - **`ome-zarr-fiji-ui`** – `ome.zarr.fijiui` (+`.open`, `.open.openers`, `.open.options`, `.plugin`, `.plugin.command.*`, `.dialog`, `.util`); the OME-Zarr `IOPlugin` (drag-and-drop and `fiji://` links) in `.plugin`, - Swing dialogs in `.dialog`, the six built-in `OmeZarrOpener`s in `.open.openers`. The SciJava commands sit in three + Swing dialogs in `.dialog`, the seven built-in `OmeZarrOpener`s in `.open.openers`. The SciJava commands sit in three sibling packages under `.plugin.command`, one per menu location: `.fileimport` for the two `File > Import` entries plus their shared `FileImportHelper` (package-private, so its tests live there too), `.tools` for the `Plugins > OME-Zarr` entries, and `.settings` for `OpeningBehaviorSettings`, `S3Settings` and `UserScriptSettings`. diff --git a/README.md b/README.md index 57fcfed01..cdb5c243f 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,9 @@ If the dropped / pasted / linked target is not recognized as a **OME-Zarr v0.3 - Drop a local OME-Zarr folder or an OME-Zarr URI onto Fiji. What happens next is set in the [Opening Behavior Settings](#opening-behavior-settings). +Dropping onto the image area of a BigDataViewer window opened by this plugin instead adds the OME-Zarr to that +window, e.g., to overlay a segmentation on the raw data. This does not depend on the opening behavior settings. + ### Copy & Paste of OME-Zarr URIs (local folder, http, https, s3) * Supports local paths, http(s) URLs, and `s3://` URIs @@ -162,9 +165,13 @@ What Fiji does with a dropped / pasted / linked OME-Zarr. The options shipped he * **ImageJ (matching resolution)** (initial default): opens the highest single-resolution level that is not wider than the [maximum width](#maximum-width). This avoids loading excessively large images. * **ImageJ (highest resolution)**: opens the highest-resolution level in ImageJ. -* **BigDataViewer**: opens all resolution levels as a multi-resolution source. This is useful for large OME-Zarrs. +* **BigDataViewer (new window)**: opens all resolution levels as a multi-resolution source in a new BigDataViewer + window. This is useful for large OME-Zarrs. Channel names, colors, contrast limits, and the time point are [taken from the OME-Zarr metadata](#read-channel-information-from-ome-zarr-metadata), if available. +* **BigDataViewer (add to active window)**: adds the image as a multi-resolution source to the BigDataViewer + window that had focus most recently, e.g., to overlay a segmentation on the raw data. Opens a new BigDataViewer + window if none is open, or if an ImageJ image window had focus after it. * **N5 importer dialog**: opens the N5 import dialog at the dropped OME-Zarr. It lists the resolution levels, lets you choose one, possibly crop it, and opens it in ImageJ. * **N5 viewer dialog**: opens the N5 viewer dialog at the dropped OME-Zarr. It lists the resolution levels, lets you diff --git a/doc/DEVELOPERS.md b/doc/DEVELOPERS.md index 8954a856b..9c2943e2f 100644 --- a/doc/DEVELOPERS.md +++ b/doc/DEVELOPERS.md @@ -45,7 +45,7 @@ graph TD | `ome-zarr-n5` | `ome.zarr.n5` | N5-universe backed reader, OME-Zarr v0.3 – v0.5. | `ome-zarr-imglib2`, the N5 stack | | `ome-zarr-zarrjava` | `ome.zarr.zarrjava` | zarr-java backed reader (the default), Zarr v2/v3 i.e. OME-Zarr v0.4 + v0.5. The only backend that reads `.ozx` archives. | `ome-zarr-imglib2`, `dev.zarr:zarr-java` | | `ome-zarr-fiji` | `ome.zarr.fiji` `.read` `.open` `.plugins` `.util` | Opens a pyramid in ImageJ or BigDataViewer, tracks the active window, and defines the `OmeZarrOpener` extension point. No dialogs, no backend dependency — pick one at runtime. | `ome-zarr-imglib2`, bigdataviewer-core, spim_data, imagej-common, ij, scijava-common | -| `ome-zarr-fiji-ui` | `ome.zarr.fijiui` `.open` `.plugin` `.dialog` `.util` | The shipped plugin: `IOPlugin`, clipboard, Swing dialogs, SciJava commands, persisted settings, the six built-in openers. | the four above, scijava-common, script-editor, n5-viewer_fiji | +| `ome-zarr-fiji-ui` | `ome.zarr.fijiui` `.open` `.plugin` `.dialog` `.util` | The shipped plugin: `IOPlugin`, clipboard, Swing dialogs, SciJava commands, persisted settings, the seven built-in openers. | the four above, scijava-common, script-editor, n5-viewer_fiji | Pick the smallest one you need: @@ -248,7 +248,7 @@ jar for free, and the icon is resolved out of the *contributing* jar. | Annotation element | What it does | |------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `name` | Identifies the opener in the persisted settings, so **keep it stable across releases**. The shipped ones are `imagej-preferred-resolution`, `imagej-highest-resolution`, `bdv-multi-resolution`, `n5-importer-dialog`, `n5-viewer-dialog` and `script-editor`. | +| `name` | Identifies the opener in the persisted settings, so **keep it stable across releases**. The shipped ones are `imagej-preferred-resolution`, `imagej-highest-resolution`, `bdv-multi-resolution`, `bdv-add-to-active`, `n5-importer-dialog`, `n5-viewer-dialog` and `script-editor`. | | `label`, `description` | What the user sees in the settings and as the button tooltip. | | `iconPath` | Classpath path to the button icon in your jar. | | `priority` | Orders the openers, and decides which one a user who never configured a choice gets. The shipped openers sit at `Priority.HIGH` and below, so declare `Priority.VERY_HIGH` to be the out-of-the-box default. | diff --git a/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/open/OmeZarrBdvDropListener.java b/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/open/OmeZarrBdvDropListener.java new file mode 100644 index 000000000..5d1642507 --- /dev/null +++ b/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/open/OmeZarrBdvDropListener.java @@ -0,0 +1,169 @@ +/*- + * #%L + * OME-Zarr extras for Fiji + * %% + * Copyright (C) 2022 - 2026 SciJava 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 ome.zarr.fijiui.open; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTarget; +import java.awt.dnd.DropTargetAdapter; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.scijava.Context; +import org.scijava.prefs.PrefService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import bdv.util.BdvHandle; +import ij.IJ; +import ome.zarr.fiji.read.OmeZarr; +import ome.zarr.fijiui.open.options.OmeZarrOpeningSettings; +import ome.zarr.fijiui.util.ClipboardUtils; +import ome.zarr.imglib2.util.ZarrUtils; + +/** + * Lets OME-Zarrs be dropped onto the image area of a BigDataViewer window, which adds them to + * that window via {@link OmeZarr#showInBdv(BdvHandle)}. Accepts files and folders, e.g., from + * Finder or Explorer, and URLs, e.g., from a browser. + *

+ * BDV itself accepts no drops on its image area, so nothing competes with this. Each dropped + * location is parsed like typed or pasted text ({@link ClipboardUtils#stringToUri}) and checked + * with {@link ZarrUtils#isZarr(URI)}, like every other entry route. Parsing, check and reading run + * on a background thread, since each may go over the network or show an error dialog. + */ +public class OmeZarrBdvDropListener extends DropTargetAdapter +{ + private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); + + private final BdvHandle bdvHandle; + + private final Context context; + + private OmeZarrBdvDropListener( final BdvHandle bdvHandle, final Context context ) + { + this.bdvHandle = bdvHandle; + this.context = context; + } + + /** + * Makes the image area of {@code bdvHandle}'s window accept dropped OME-Zarrs. + *

+ * Dropped OME-Zarrs are read with the backend and preferred width the user has configured at + * the time of the drop, and failures are reported via {@code IJ::error}. + * + * @param bdvHandle the BigDataViewer to add dropped OME-Zarrs to + * @param context the SciJava context the settings are read from + */ + public static void install( final BdvHandle bdvHandle, final Context context ) + { + new DropTarget( bdvHandle.getViewerPanel().getDisplayComponent(), DnDConstants.ACTION_COPY, + new OmeZarrBdvDropListener( bdvHandle, context ) ); + } + + @Override + public void dragEnter( final DropTargetDragEvent event ) + { + if ( event.isDataFlavorSupported( DataFlavor.javaFileListFlavor ) + || event.isDataFlavorSupported( DataFlavor.stringFlavor ) ) + event.acceptDrag( DnDConstants.ACTION_COPY ); + else + event.rejectDrag(); + } + + @Override + public void drop( final DropTargetDropEvent event ) + { + event.acceptDrop( DnDConstants.ACTION_COPY ); + final List< String > locations = droppedLocations( event.getTransferable() ); + final boolean transferSucceeded = !locations.isEmpty(); + event.dropComplete( transferSucceeded ); + if ( transferSucceeded ) + new Thread( () -> { + final OmeZarrOpeningSettings settings = + OmeZarrOpeningSettings.loadSettingsFromPreferences( context.getService( PrefService.class ) ); + locations.forEach( location -> add( location, settings ) ); + }, "OME-Zarr drop onto BigDataViewer" ).start(); + } + + private void add( final String location, final OmeZarrOpeningSettings settings ) + { + final URI uri = ClipboardUtils.stringToUri( location, IJ::error ); + if ( uri == null ) + return; + if ( !ZarrUtils.isZarr( uri ) ) + { + IJ.error( "The dropped location does not appear to be an OME-Zarr dataset:\n" + uri + "." ); + return; + } + OmeZarrOpenActions.withSettings( uri, context, settings, IJ::error ).showInBdv( bdvHandle ); + } + + /** + * The locations in a drop, as text: a path per file or folder if the drop carries a file list, + * else one location per line of text, which is how URLs arrive from a browser (and files on some + * Linux desktops, as {@code file:} URIs). They are turned into URIs later, off the event thread, + * since {@link ClipboardUtils#stringToUri} may report an error in a dialog. + */ + static List< String > droppedLocations( final Transferable transferable ) + { + final List< String > locations = new ArrayList<>(); + try + { + if ( transferable.isDataFlavorSupported( DataFlavor.javaFileListFlavor ) ) + { + for ( final Object file : ( List< ? > ) transferable.getTransferData( DataFlavor.javaFileListFlavor ) ) + locations.add( file.toString() ); + } + else if ( transferable.isDataFlavorSupported( DataFlavor.stringFlavor ) ) + { + for ( final String line : ( ( String ) transferable.getTransferData( DataFlavor.stringFlavor ) ).split( "\\R" ) ) + { + final String location = line.trim(); + // text/uri-list allows comment lines starting with '#' + if ( !location.isEmpty() && !location.startsWith( "#" ) ) + locations.add( location ); + } + } + } + catch ( final UnsupportedFlavorException | IOException e ) + { + logger.warn( "Could not read the dropped data: {}", e.getMessage() ); + return Collections.emptyList(); + } + return locations; + } +} diff --git a/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/open/OmeZarrOpenActions.java b/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/open/OmeZarrOpenActions.java index 94c58930f..2ebf6d97b 100644 --- a/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/open/OmeZarrOpenActions.java +++ b/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/open/OmeZarrOpenActions.java @@ -90,7 +90,7 @@ public static void openWithSettings( final URI inputUri, final Context context ) { final PrefService prefService = context.getService( PrefService.class ); final OmeZarrOpeningSettings settings = OmeZarrOpeningSettings.loadSettingsFromPreferences( prefService ); - final OmeZarr omeZarr = omeZarrFor( inputUri, context, settings, IJ::error ); + final OmeZarr omeZarr = withSettings( inputUri, context, settings, IJ::error ); final OmeZarrOpenerService openerService = context.getService( OmeZarrOpenerService.class ); if ( openerService == null ) { @@ -172,7 +172,7 @@ public OmeZarrOpenActions( final URI inputUri, final Context context, final OmeZ OmeZarrOpenActions( final URI inputUri, final Context context, final OmeZarrOpeningSettings settings, final Consumer< String > errorHandler ) { - this( omeZarrFor( inputUri, context, settings, errorHandler ) ); + this( withSettings( inputUri, context, settings, errorHandler ) ); } /** @@ -189,10 +189,17 @@ public OmeZarrOpenActions( final OmeZarr omeZarr ) } /** - * Reader for {@code inputUri} using the backend, AWS profile and preferred - * width from {@code settings}, or the defaults when no settings are given. + * Factory for an {@link OmeZarr} that uses the backend, AWS profile and + * preferred width from {@code settings}, or the defaults when no settings are + * given, like {@link #withDefaultBackend}. + * + * @param inputUri the OME-Zarr location to read + * @param context the SciJava context used for display and services + * @param settings the opening settings, or {@code null} for the defaults + * @param errorHandler receives a user-facing message when opening fails + * @return an {@link OmeZarr} for {@code inputUri} using {@code settings} */ - private static OmeZarr omeZarrFor( final URI inputUri, final Context context, + public static OmeZarr withSettings( final URI inputUri, final Context context, final OmeZarrOpeningSettings settings, final Consumer< String > errorHandler ) { final OmeZarrBackend backend = settings == null ? OmeZarrOpeningSettings.DEFAULT_BACKEND : settings.getBackend(); @@ -262,11 +269,27 @@ public PyramidalDataset showInImageJ( final int resolutionLevel ) * * @return the resulting {@link BdvHandle}, or {@code null} if opening failed */ - // UnusedReturnValue: the dataset is returned for API and script users @SuppressWarnings( "UnusedReturnValue" ) public BdvHandle showInBdv() { - return omeZarr.showInBdv(); + return showInBdv( null ); + } + + /** + * Adds the dataset to the open BigDataViewer of {@code bdvHandle}, or opens it in a new one if + * {@code null}. Delegates to {@link OmeZarr#showInBdv(BdvHandle)} and makes a new window + * {@link OmeZarrBdvDropListener#install accept dropped OME-Zarrs}. + * + * @param bdvHandle the BigDataViewer to add to, or {@code null} to open a new window + * @return the resulting {@link BdvHandle}, or {@code null} if opening failed + */ + @SuppressWarnings( "UnusedReturnValue" ) + public BdvHandle showInBdv( final BdvHandle bdvHandle ) + { + final BdvHandle result = omeZarr.showInBdv( bdvHandle ); + if ( bdvHandle == null && result != null ) // NB: only install the drop target on a new window, not on an existing one + OmeZarrBdvDropListener.install( result, omeZarr.context() ); + return result; } /** diff --git a/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/open/openers/BdvAddToActiveOpener.java b/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/open/openers/BdvAddToActiveOpener.java new file mode 100644 index 000000000..6c6ac7ed2 --- /dev/null +++ b/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/open/openers/BdvAddToActiveOpener.java @@ -0,0 +1,63 @@ +/*- + * #%L + * OME-Zarr integration into FIJI + * %% + * Copyright (C) 2022 - 2026 SciJava 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 ome.zarr.fijiui.open.openers; + +import ome.zarr.fiji.plugins.ActiveViewerService; +import ome.zarr.fiji.read.OmeZarr; +import ome.zarr.fiji.open.OmeZarrOpener; +import ome.zarr.fiji.util.BdvUtils; +import ome.zarr.fijiui.open.OmeZarrOpenActions; + +import org.scijava.Priority; +import org.scijava.plugin.Plugin; + +/** + * Adds the image to the active BigDataViewer window, e.g. to overlay a + * segmentation on its raw data. A BigDataViewer window is active if it is the + * image window focused or opened most recently. Focusing Fiji's main window, + * toolbar or a dialog keeps it active. Focusing an ImageJ image window, or a + * BigDataViewer window another tool opened, does not, and then, as when no + * BigDataViewer window is open, a new one opens. See {@link BdvUtils#bdvHandleOf}. + */ +@Plugin( type = OmeZarrOpener.class, name = BdvAddToActiveOpener.NAME, + label = "BigDataViewer (add to active window)", + description = "Add as a multi-resolution source to the active BigDataViewer window, or open a new one if there is none", + iconPath = "/ome/zarr/fijiui/dialog/bdv_add_icon.png", priority = Priority.NORMAL - 1 ) +public class BdvAddToActiveOpener implements OmeZarrOpener +{ + /** The stable identifier this opener is persisted under. */ + public static final String NAME = "bdv-add-to-active"; + + @Override + public void open( final OmeZarr omeZarr ) + { + final ActiveViewerService activeViewerService = omeZarr.context().getService( ActiveViewerService.class ); + new OmeZarrOpenActions( omeZarr ).showInBdv( BdvUtils.bdvHandleOf( activeViewerService.getActiveViewer() ) ); + } +} diff --git a/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/open/openers/BdvMultiResolutionOpener.java b/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/open/openers/BdvMultiResolutionOpener.java index 05f5a77fc..b8c97fbdb 100644 --- a/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/open/openers/BdvMultiResolutionOpener.java +++ b/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/open/openers/BdvMultiResolutionOpener.java @@ -30,18 +30,18 @@ import ome.zarr.fiji.read.OmeZarr; import ome.zarr.fiji.open.OmeZarrOpener; +import ome.zarr.fijiui.open.OmeZarrOpenActions; import org.scijava.Priority; import org.scijava.plugin.Plugin; /** - * Opens the whole resolution pyramid as a multi-resolution source in - * BigDataViewer, which is the only opener here that does not have to pick a - * single level. + * Opens the whole resolution pyramid as a multi-resolution source in a new + * BigDataViewer window, so it does not have to pick a single level. */ @Plugin( type = OmeZarrOpener.class, name = BdvMultiResolutionOpener.NAME, - label = "BigDataViewer", - description = "Open as a multi-resolution source in BigDataViewer", + label = "BigDataViewer (new window)", + description = "Open as a multi-resolution source in a new BigDataViewer window", iconPath = "/ome/zarr/fijiui/dialog/bdv_icon.png", priority = Priority.NORMAL ) public class BdvMultiResolutionOpener implements OmeZarrOpener { @@ -51,6 +51,6 @@ public class BdvMultiResolutionOpener implements OmeZarrOpener @Override public void open( final OmeZarr omeZarr ) { - omeZarr.showInBdv(); + new OmeZarrOpenActions( omeZarr ).showInBdv(); } } diff --git a/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/plugin/command/tools/OpenInBDVCommand.java b/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/plugin/command/tools/OpenInBDVCommand.java index 6d20ad76e..c2cc2e57a 100644 --- a/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/plugin/command/tools/OpenInBDVCommand.java +++ b/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/plugin/command/tools/OpenInBDVCommand.java @@ -29,6 +29,8 @@ package ome.zarr.fijiui.plugin.command.tools; import java.lang.invoke.MethodHandles; +import java.util.Collections; +import java.util.List; import org.scijava.command.Command; import org.scijava.plugin.Parameter; @@ -38,10 +40,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import bdv.util.BdvHandle; import ome.zarr.fiji.Pyramidal; -import ome.zarr.fiji.PyramidalBdv; import ome.zarr.fiji.plugins.PyramidalService; import ome.zarr.fiji.util.BdvUtils; +import ome.zarr.fiji.util.OmeZarrImagesChooser; +import ome.zarr.fijiui.open.OmeZarrBdvDropListener; +import ome.zarr.imglib2.PyramidContents; @Plugin( type = Command.class, menuPath = "Plugins > OME-Zarr > Open Current OME-Zarr Image in BigDataViewer" ) public class OpenInBDVCommand implements Command @@ -70,7 +75,23 @@ public void run() logger.warn( message ); return; } - final PyramidalBdv< ? > pyramidalBdv = new PyramidalBdv<>( this.pyramidal.getContext(), this.pyramidal.getPyramidContents() ); - BdvUtils.showBdvAndRegisterWindow( pyramidalBdv, pyramidalService ); + final BdvHandle bdvHandle = BdvUtils.showInOneWindow( selectContents(), this.pyramidal.getContext(), pyramidalService ); + if ( bdvHandle != null ) + OmeZarrBdvDropListener.install( bdvHandle, this.pyramidal.getContext() ); + } + + /** + * The OME-Zarrs to show in one BDV window: just the active one, unless more than one + * is open, in ImageJ or BigDataViewer. Each is listed once, however many windows show it. + */ + private List< PyramidContents< ? > > selectContents() + { + final List< PyramidContents< ? > > open = pyramidalService.getPyramidContents(); + if ( open.isEmpty() ) + return Collections.emptyList(); + // for one OME-Zarr, this.pyramidal's contents should be the same as open.get( 0 ) and for headless mode, we don't want to show a chooser dialog, so just return the active one + if ( open.size() == 1 || uiService.isHeadless() ) + return Collections.singletonList( this.pyramidal.getPyramidContents() ); + return OmeZarrImagesChooser.choose( open, this.pyramidal.getPyramidContents() ); // NB: the active one is pre-selected } } diff --git a/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/util/ClipboardUtils.java b/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/util/ClipboardUtils.java index c46de2be0..3525765f3 100644 --- a/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/util/ClipboardUtils.java +++ b/ome-zarr-fiji-ui/src/main/java/ome/zarr/fijiui/util/ClipboardUtils.java @@ -122,7 +122,7 @@ public static URI stringToUri( final String possibleUri, final Consumer< String { if ( possibleUri == null || possibleUri.trim().isEmpty() ) { - errorHandler.accept( "The clipboard does not contain any text." ); + errorHandler.accept( "There is no text to interpret as a URL or path." ); return null; } final String text = possibleUri.trim(); @@ -155,7 +155,7 @@ public static URI stringToUri( final String possibleUri, final Consumer< String } catch ( InvalidPathException e ) { - errorHandler.accept( "Could not interpret the clipboard contents as a URL or path:\n" + text ); + errorHandler.accept( "Could not interpret the text as a URL or path:\n" + text ); return null; } } diff --git a/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/open/OmeZarrBdvDropListenerTest.java b/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/open/OmeZarrBdvDropListenerTest.java new file mode 100644 index 000000000..8c3b249e6 --- /dev/null +++ b/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/open/OmeZarrBdvDropListenerTest.java @@ -0,0 +1,179 @@ +/*- + * #%L + * OME-Zarr integration into FIJI + * %% + * Copyright (C) 2022 - 2026 SciJava 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 ome.zarr.fijiui.open; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.awt.Point; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTarget; +import java.awt.dnd.DropTargetDropEvent; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nonnull; +import javax.swing.SwingUtilities; + +import org.junit.jupiter.api.Test; +import org.scijava.Context; + +import bdv.util.BdvHandle; +import bdv.viewer.ViewerStateChange; +import ome.zarr.ZarrTestUtils; +import ome.zarr.fiji.PyramidalBdv; +import ome.zarr.fiji.plugins.ActiveViewerService; +import ome.zarr.fiji.plugins.PyramidalService; +import ome.zarr.fiji.util.BdvUtils; +import ome.zarr.fijiui.open.openers.BdvAddToActiveOpener; + +class OmeZarrBdvDropListenerTest +{ + private static final String SPOTS = "ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr"; + + private static final String LABELS = "ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr"; + + /** + * Open the spots with "BigDataViewer (add to active window)", then drop + * the labels folder onto the window's image area, and try to find both in its sources. + */ + @Test + void droppingAFolderAddsItToTheWindow() throws Exception + { + try (Context context = new Context()) + { + new BdvAddToActiveOpener() + .open( OmeZarrOpenActions.withDefaultBackend( ZarrTestUtils.resourcePath( SPOTS ).toUri(), context ) ); + assertInstanceOf( PyramidalBdv.class, context.getService( PyramidalService.class ).getActivePyramidal() ); + BdvHandle bdvHandle = BdvUtils.bdvHandleOf( context.getService( ActiveViewerService.class ).getActiveViewer() ); + assertNotNull( bdvHandle ); + assertEquals( 1, bdvHandle.getViewerPanel().state().getSources().size() ); + + DropTarget dropTarget = bdvHandle.getViewerPanel().getDisplayComponent().getDropTarget(); + assertNotNull( dropTarget, "a BDV window opened by this plugin accepts drops" ); + CountDownLatch sourceAdded = new CountDownLatch( 1 ); + bdvHandle.getViewerPanel().state().changeListeners().add( change -> { + if ( change == ViewerStateChange.NUM_SOURCES_CHANGED ) + sourceAdded.countDown(); + } ); + File labels = ZarrTestUtils.resourcePath( LABELS ).toFile(); + SwingUtilities.invokeAndWait( () -> dropTarget.drop( dropEvent( dropTarget, labels ) ) ); + + assertTrue( sourceAdded.await( 30, TimeUnit.SECONDS ), "the drop is read on a background thread" ); + assertEquals( 2, bdvHandle.getViewerPanel().state().getSources().size() ); + + bdvHandle.close(); + SwingUtilities.invokeAndWait( () -> {} ); // wait until all Swing events are processed + } + } + + @Test + void droppedLocationsGivesAPathPerFile() + { + File a = new File( "/data/a.ome.zarr" ), b = new File( "/data/b.ome.zarr" ); + assertEquals( Arrays.asList( a.toString(), b.toString() ), + OmeZarrBdvDropListener.droppedLocations( fileList( Arrays.asList( a, b ) ) ) ); + } + + /** Browsers send URLs as text, one per line, as in {@code text/uri-list}, where '#' starts a comment. */ + @Test + void droppedLocationsGivesALocationPerLineOfText() + { + String uriList = "# dragged from a browser\r\nhttps://example.com/a.ome.zarr\n\n /data/b.ome.zarr \n"; + assertEquals( Arrays.asList( "https://example.com/a.ome.zarr", "/data/b.ome.zarr" ), + OmeZarrBdvDropListener.droppedLocations( new StringSelection( uriList ) ) ); + } + + @Test + void droppedLocationsIsEmptyForNeitherFilesNorText() + { + assertEquals( Collections.emptyList(), OmeZarrBdvDropListener.droppedLocations( mock( Transferable.class ) ) ); + } + + @Test + void droppedLocationsIsEmptyIfTheDataCannotBeRead() throws Exception + { + Transferable unreadable = mock( Transferable.class ); + when( unreadable.isDataFlavorSupported( DataFlavor.stringFlavor ) ).thenReturn( true ); + when( unreadable.getTransferData( DataFlavor.stringFlavor ) ).thenThrow( new IOException( "source app quit" ) ); + assertEquals( Collections.emptyList(), OmeZarrBdvDropListener.droppedLocations( unreadable ) ); + } + + /** {@code files} as Finder or Explorer drag them. */ + private static Transferable fileList( final List< File > files ) + { + return new Transferable() + { + @Override + public DataFlavor[] getTransferDataFlavors() + { + return new DataFlavor[] { DataFlavor.javaFileListFlavor }; + } + + @Override + public boolean isDataFlavorSupported( final DataFlavor flavor ) + { + return DataFlavor.javaFileListFlavor.equals( flavor ); + } + + @Override + public @Nonnull Object getTransferData( final DataFlavor flavor ) + { + return files; + } + }; + } + + /** A drop of {@code file} onto {@code dropTarget}, as Finder or Explorer would make it. */ + private static DropTargetDropEvent dropEvent( final DropTarget dropTarget, final File file ) + { + final Transferable fileList = fileList( Collections.singletonList( file ) ); + return new DropTargetDropEvent( dropTarget.getDropTargetContext(), new Point( 10, 10 ), + DnDConstants.ACTION_COPY, DnDConstants.ACTION_COPY ) + { + @Override + public Transferable getTransferable() + { + return fileList; + } + }; + } +} diff --git a/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/open/OmeZarrOpenActionsTest.java b/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/open/OmeZarrOpenActionsTest.java index 079a4d336..c82e0f4ab 100644 --- a/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/open/OmeZarrOpenActionsTest.java +++ b/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/open/OmeZarrOpenActionsTest.java @@ -183,7 +183,7 @@ void openWithSettingsDispatchesToTheConfiguredOpener() throws URISyntaxException // OmeZarr reads nothing, the omeZarr it hands the reader to does. final List< OmeZarr > readers = readerConstruction.constructed(); assertEquals( 4, readers.size() ); - verify( readers.get( 0 ), times( 1 ) ).showInBdv(); + verify( readers.get( 0 ), times( 1 ) ).showInBdv( null ); verify( readers.get( 1 ), times( 1 ) ).showInImageJ( 0 ); verify( readers.get( 2 ), times( 1 ) ).showInImageJ(); verifyNoInteractions( readers.get( 3 ) ); @@ -916,8 +916,8 @@ void testOpenDifferentResolutionLevels() throws URISyntaxException PyramidalDataset ifLevel1 = assertInstanceOf( PyramidalDataset.class, datasetService.getDatasets().get( 1 ) ); assertFalse( Util.imagesEqual( Cast.unchecked( ijLevel0.getImgPlus().getImg() ), ifLevel1.getImgPlus().getImg() ) ); - long[] expectedDims0 = new long[] { 64, 64, 16, 3, 4 }; - long[] expectedDims1 = new long[] { 32, 32, 8, 3, 4 }; + long[] expectedDims0 = { 64, 64, 16, 3, 4 }; + long[] expectedDims1 = { 32, 32, 8, 3, 4 }; assertArrayEquals( expectedDims0, ijLevel0.getImgPlus().dimensionsAsLongArray(), "Dimensions of level 0 should be [x=64, y=64, z=16, c=3, t=4]" ); diff --git a/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/open/openers/BdvOpenersTest.java b/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/open/openers/BdvOpenersTest.java new file mode 100644 index 000000000..3255566af --- /dev/null +++ b/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/open/openers/BdvOpenersTest.java @@ -0,0 +1,108 @@ +/*- + * #%L + * OME-Zarr integration into FIJI + * %% + * Copyright (C) 2022 - 2026 SciJava 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 ome.zarr.fijiui.open.openers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.swing.SwingUtilities; + +import org.junit.jupiter.api.Test; +import org.scijava.Context; + +import bdv.util.BdvHandle; +import ome.zarr.ZarrTestUtils; +import ome.zarr.fiji.open.OmeZarrOpener; +import ome.zarr.fiji.plugins.ActiveViewerService; +import ome.zarr.fiji.plugins.PyramidalService; +import ome.zarr.fiji.util.BdvUtils; +import ome.zarr.fijiui.open.OmeZarrOpenActions; + +/** + * "BigDataViewer (add to active window)" against "BigDataViewer (new window)", each opening the + * logo spots and then their labels. + */ +class BdvOpenersTest +{ + private static final String SPOTS = "ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr"; + + private static final String LABELS = "ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr"; + + @Test + void addToActiveOpensANewWindowOnlyIfThereIsNone() throws Exception + { + try (Context context = new Context()) + { + List< BdvHandle > windows = openSpotsThenLabels( new BdvAddToActiveOpener(), context ); + + assertSame( windows.get( 0 ), windows.get( 1 ) ); + assertEquals( 2, windows.get( 0 ).getViewerPanel().state().getSources().size() ); + close( windows ); + } + } + + @Test + void newWindowAlwaysOpensANewWindow() throws Exception + { + try (Context context = new Context()) + { + List< BdvHandle > windows = openSpotsThenLabels( new BdvMultiResolutionOpener(), context ); + + assertNotSame( windows.get( 0 ), windows.get( 1 ) ); + assertEquals( 1, windows.get( 0 ).getViewerPanel().state().getSources().size() ); + assertEquals( 1, windows.get( 1 ).getViewerPanel().state().getSources().size() ); + close( windows ); + } + } + + /** The BDV windows {@code opener} showed the spots and the labels in, one per image. */ + private static List< BdvHandle > openSpotsThenLabels( final OmeZarrOpener opener, final Context context ) throws Exception + { + ActiveViewerService activeViewerService = context.getService( ActiveViewerService.class ); + List< BdvHandle > windows = new ArrayList<>(); + for ( String image : Arrays.asList( SPOTS, LABELS ) ) + { + opener.open( OmeZarrOpenActions.withDefaultBackend( ZarrTestUtils.resourcePath( image ).toUri(), context ) ); + windows.add( BdvUtils.bdvHandleOf( activeViewerService.getActiveViewer() ) ); + } + assertEquals( 2, context.getService( PyramidalService.class ).getPyramidals().size() ); + return windows; + } + + private static void close( final List< BdvHandle > windows ) throws Exception + { + windows.stream().distinct().forEach( BdvHandle::close ); + SwingUtilities.invokeAndWait( () -> {} ); // wait until all Swing events are processed + } +} diff --git a/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/plugin/command/tools/OpenInBDVCommandTest.java b/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/plugin/command/tools/OpenInBDVCommandTest.java index 6711d334f..8ec6930dc 100644 --- a/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/plugin/command/tools/OpenInBDVCommandTest.java +++ b/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/plugin/command/tools/OpenInBDVCommandTest.java @@ -37,6 +37,7 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.util.List; +import java.util.stream.Collectors; import java.util.concurrent.ExecutionException; import javax.swing.SwingUtilities; @@ -49,9 +50,11 @@ import org.junit.jupiter.api.Test; import org.scijava.Context; import org.scijava.command.CommandService; +import org.scijava.ui.UIService; import ome.zarr.fijiui.open.OmeZarrOpenActions; import ome.zarr.fiji.Pyramidal; +import ome.zarr.fiji.PyramidalBdv; import ome.zarr.fiji.plugins.PyramidalService; import ome.zarr.ZarrTestUtils; @@ -76,6 +79,10 @@ private static void flushEventQueue() throws InterruptedException } } + private static final String SPOTS = "ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr"; + + private static final String LABELS = "ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr"; + // 2 resolution levels: level 0 = 64x64x16, level 1 = 32x32x8 private static final String PYRAMID_RESOURCE = "ome/zarr/testdata/5d_testing/5d_dataset_v5.ome.zarr"; @@ -146,4 +153,34 @@ void runAfterBdvOpenCreatesSecondBdvDataset() throws URISyntaxException, Executi assertSame( firstBdvPyramidal.getPyramidContents(), secondBdvPyramidal.getPyramidContents() ); } } + + /** + * Headless, there is no dialog: with two OME-Zarrs open, only the active one is shown. + */ + @Test + void runHeadlessWithTwoOpenShowsOnlyTheActiveOne() throws Exception + { + try (Context context = new Context()) + { + new OmeZarrOpenActions( ZarrTestUtils.resourcePath( SPOTS ).toUri(), context ).showInImageJ(); + new OmeZarrOpenActions( ZarrTestUtils.resourcePath( LABELS ).toUri(), context ).showInImageJ(); + flushEventQueue(); + Pyramidal active = context.getService( PyramidalService.class ).getActivePyramidal(); + context.getService( UIService.class ).setHeadless( true ); + + context.getService( CommandService.class ).run( OpenInBDVCommand.class, true ).get(); + + List< PyramidalBdv< ? > > shown = bdvPyramidals( context ); + assertEquals( 1, shown.size() ); + assertSame( active.getPyramidContents(), shown.get( 0 ).getPyramidContents() ); + } + } + + private static List< PyramidalBdv< ? > > bdvPyramidals( final Context context ) + { + return context.getService( PyramidalService.class ).getPyramidals().stream() + .filter( PyramidalBdv.class::isInstance ) + .map( pyramidal -> ( PyramidalBdv< ? > ) pyramidal ) + .collect( Collectors.toList() ); + } } diff --git a/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/util/ClipboardUtilsTest.java b/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/util/ClipboardUtilsTest.java index ed560a453..08846cf7f 100644 --- a/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/util/ClipboardUtilsTest.java +++ b/ome-zarr-fiji-ui/src/test/java/ome/zarr/fijiui/util/ClipboardUtilsTest.java @@ -80,7 +80,7 @@ void clipboardReportsError( String clipboardContents ) { assertNull( ClipboardUtils.stringToUri( clipboardContents, errorHandler ) ); assertEquals( 1, errors.size() ); - assertTrue( errors.get( 0 ).contains( "clipboard" ) ); + assertTrue( errors.get( 0 ).contains( "no text" ) ); } @Test @@ -180,7 +180,7 @@ void emptySystemClipboardReportsError() setClipboard( "" ); assertNull( ClipboardUtils.readClipboardAsUri( errorHandler ) ); assertEquals( 1, errors.size() ); - assertTrue( errors.get( 0 ).contains( "clipboard" ) ); + assertTrue( errors.get( 0 ).contains( "no text" ) ); } @Test @@ -211,7 +211,7 @@ public boolean isDataFlavorSupported( DataFlavor f ) }, null ); assertNull( ClipboardUtils.readClipboardAsUri( errorHandler ) ); assertEquals( 1, errors.size() ); - assertTrue( errors.get( 0 ).contains( "clipboard" ) ); + assertTrue( errors.get( 0 ).contains( "no text" ) ); } // --- InvalidPathException branch in stringToUri() --- diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/plugins/ActiveViewerService.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/plugins/ActiveViewerService.java new file mode 100644 index 000000000..5dcf03111 --- /dev/null +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/plugins/ActiveViewerService.java @@ -0,0 +1,173 @@ +/*- + * #%L + * OME-Zarr extras for Fiji + * %% + * Copyright (C) 2022 - 2026 SciJava 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 ome.zarr.fiji.plugins; + +import java.awt.Component; +import java.awt.Container; +import java.awt.KeyboardFocusManager; +import java.awt.Window; +import java.beans.PropertyChangeListener; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import javax.swing.SwingUtilities; + +import net.imagej.patcher.LegacyInjector; + +import org.scijava.plugin.Plugin; +import org.scijava.service.AbstractService; +import org.scijava.service.SciJavaService; + +import bdv.viewer.ViewerPanel; +import ij.gui.ImageWindow; + +/** + * Tracks the most recently focused image viewer: an ImageJ {@link ImageWindow} or a BigDataViewer + * {@link ViewerPanel}, no matter which tool opened it. + *

+ * Listens to the {@code "focusOwner"} property of the {@link KeyboardFocusManager} rather than to + * {@code "activeWindow"}, so that the viewers of a window holding two of them (e.g. BigWarp) can be + * told apart. Focus in a BDV window's side panel lies outside its {@link ViewerPanel}, so a window + * holding exactly one viewer counts as that viewer wherever its focus is. All other windows (Fiji's + * main window, dialogs) are ignored, so the last viewer stays active while the user navigates menus. + *

+ * The focus event of a newly shown window arrives only after the call that showed it has returned, + * so whoever opens a viewer reports it via {@link #setActiveViewer}. + */ +@Plugin( type = SciJavaService.class ) +public class ActiveViewerService extends AbstractService implements SciJavaService +{ + static + { + // Patch IJ1 before ij.* classes are loaded during Context startup: + // unpatched IJ1 classes make the later patcher run fail with a LinkageError. + LegacyInjector.preinit(); + } + + /** Weak, so that a closed viewer, and the image data it holds, is not kept alive until the next one becomes active. */ + private final AtomicReference< WeakReference< Component > > activeViewer = new AtomicReference<>( new WeakReference<>( null ) ); + + private final List< Consumer< Component > > listeners = new CopyOnWriteArrayList<>(); + + private PropertyChangeListener focusListener; + + @Override + public void initialize() + { + focusListener = event -> { + final Component viewer = viewerOf( ( Component ) event.getNewValue() ); + if ( viewer != null && viewer != activeViewer.get().get() ) + setActiveViewer( viewer ); + }; + KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener( "focusOwner", focusListener ); + } + + @Override + public void dispose() + { + if ( focusListener != null ) + { + KeyboardFocusManager.getCurrentKeyboardFocusManager().removePropertyChangeListener( "focusOwner", focusListener ); + focusListener = null; + } + listeners.clear(); + activeViewer.set( new WeakReference<>( null ) ); + } + + /** + * @return the most recently focused or reported image viewer, an {@link ImageWindow} or a + * {@link ViewerPanel}, or {@code null} if there is none or it has been closed since + */ + public Component getActiveViewer() + { + final Component viewer = activeViewer.get().get(); + return viewer != null && viewer.isShowing() ? viewer : null; + } + + /** + * Makes {@code viewer} the active one, without waiting for its focus event, and tells the + * listeners. + * + * @param viewer an {@link ImageWindow} or a {@link ViewerPanel}, or {@code null} if the viewer + * about to become active is not known yet + */ + public void setActiveViewer( final Component viewer ) + { + activeViewer.set( new WeakReference<>( viewer ) ); + listeners.forEach( listener -> listener.accept( viewer ) ); + } + + /** Calls {@code listener} with each newly active viewer, see {@link #setActiveViewer}. */ + public void addListener( final Consumer< Component > listener ) + { + listeners.add( listener ); + } + + public void removeListener( final Consumer< Component > listener ) + { + listeners.remove( listener ); + } + + /** + * The image viewer {@code focusOwner} belongs to: its {@link ImageWindow}, the + * {@link ViewerPanel} containing it, or else the only one in its window. {@code null} for + * anything else. + */ + static Component viewerOf( final Component focusOwner ) + { + if ( focusOwner == null ) + return null; + final Window window = focusOwner instanceof Window ? ( Window ) focusOwner : SwingUtilities.getWindowAncestor( focusOwner ); + if ( window instanceof ImageWindow ) + return window; + final Component viewerPanel = SwingUtilities.getAncestorOfClass( ViewerPanel.class, focusOwner ); + if ( viewerPanel != null ) + return viewerPanel; + if ( window == null ) + return null; + final List< ViewerPanel > viewerPanels = new ArrayList<>(); + collectViewerPanels( window, viewerPanels ); + return viewerPanels.size() == 1 ? viewerPanels.get( 0 ) : null; + } + + private static void collectViewerPanels( final Container container, final List< ViewerPanel > viewerPanels ) + { + for ( final Component child : container.getComponents() ) + { + if ( child instanceof ViewerPanel ) + viewerPanels.add( ( ViewerPanel ) child ); + else if ( child instanceof Container ) + collectViewerPanels( ( Container ) child, viewerPanels ); + } + } +} diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/plugins/PyramidalService.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/plugins/PyramidalService.java index 2b1be5e68..1016f826d 100644 --- a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/plugins/PyramidalService.java +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/plugins/PyramidalService.java @@ -31,14 +31,16 @@ import ij.ImageListener; import ij.ImagePlus; import ij.gui.ImageWindow; -import java.awt.KeyboardFocusManager; +import java.awt.Component; import java.awt.Window; -import java.beans.PropertyChangeListener; import java.lang.invoke.MethodHandles; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import javax.swing.SwingUtilities; import net.imagej.Dataset; import net.imagej.patcher.LegacyInjector; import org.scijava.convert.ConvertService; @@ -50,15 +52,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ome.zarr.fiji.Pyramidal; +import ome.zarr.imglib2.PyramidContents; /** * Tracks which BDV or ImageJ window holding a {@link Pyramidal} was most recently focused. *

* BDV windows are not ImageJ2 displays, and IJ1 {@link ImageWindow} focus changes never reach - * the SciJava {@code EventService}. This service therefore listens at the AWT level via a - * {@code "activeWindow"} {@link PropertyChangeListener} on the {@link KeyboardFocusManager}: - * BDV windows set the active pyramidal directly; ImageJ windows resolve it from the displayed - * image; all other windows (toolbar, dialogs) are ignored so menu navigation doesn't clear state. + * the SciJava {@code EventService}. This service therefore follows the + * {@link ActiveViewerService}, which listens at the AWT level. A BDV window registered here gives + * its pyramidal. An ImageJ window resolves it from the displayed image. BDV windows opened by other + * tools are ignored, as are all non-viewer windows (toolbar, dialogs), so menu navigation doesn't + * clear state. */ @Plugin( type = SciJavaService.class ) public class PyramidalService extends AbstractService implements SciJavaService @@ -80,7 +84,11 @@ public class PyramidalService extends AbstractService implements SciJavaService @Parameter private ObjectService objectService; - /** Registered BDV windows and the dataset each one displays. */ + @SuppressWarnings( "unused" ) + @Parameter + private ActiveViewerService activeViewerService; + + /** Registered BDV windows, each with the dataset owning its current source. */ private final Map< Window, Pyramidal > bdvWindows = new ConcurrentHashMap<>(); /** ImagePlus instances known to wrap a {@link Pyramidal}, populated on IJ window focus. */ @@ -88,7 +96,7 @@ public class PyramidalService extends AbstractService implements SciJavaService private final AtomicReference< Pyramidal > activePyramidal = new AtomicReference<>(); - private PropertyChangeListener focusListener; + private Consumer< Component > activeViewerListener; private ImageListener imageCloseListener; @@ -96,8 +104,8 @@ public class PyramidalService extends AbstractService implements SciJavaService public void initialize() { activePyramidal.set( null ); - focusListener = event -> onActiveWindowChanged( ( Window ) event.getNewValue() ); - KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener( "activeWindow", focusListener ); + activeViewerListener = this::onActiveViewerChanged; + activeViewerService.addListener( activeViewerListener ); imageCloseListener = new ImageListener() { @Override @@ -120,10 +128,10 @@ public void imageClosed( final ImagePlus imagePlus ) @Override public void dispose() { - if ( focusListener != null ) + if ( activeViewerListener != null ) { - KeyboardFocusManager.getCurrentKeyboardFocusManager().removePropertyChangeListener( "activeWindow", focusListener ); - focusListener = null; + activeViewerService.removeListener( activeViewerListener ); + activeViewerListener = null; } if ( imageCloseListener != null ) @@ -136,23 +144,25 @@ public void dispose() } /** - * Updates focus precedence when the active window changes. A registered BDV window gives - * BDV precedence; an ImageJ {@link ImageWindow} returns precedence to the IJ dataset; any other - * window (toolbar, dialogs) is ignored so the last image-window state persists. + * Updates focus precedence when the active viewer changes. An ImageJ {@link ImageWindow} gives + * precedence to the IJ dataset. A BDV viewer in a registered window gives BDV precedence. Any + * other viewer is ignored so the last state persists. */ - private void onActiveWindowChanged( final Window window ) + private void onActiveViewerChanged( final Component viewer ) { - if ( window == null ) - return; - final Pyramidal dataset = bdvWindows.get( window ); - if ( dataset != null ) - notifyBdvWindowFocused( dataset ); - else if ( window instanceof ImageWindow ) - notifyImageJWindowFocused( ( ImageWindow ) window ); + if ( viewer instanceof ImageWindow ) + notifyImageJWindowFocused( ( ImageWindow ) viewer ); + else if ( viewer != null ) + { + final Pyramidal dataset = bdvWindows.get( SwingUtilities.getWindowAncestor( viewer ) ); + if ( dataset != null ) + notifyBdvWindowFocused( dataset ); + } } /** - * Registers a BDV {@code window} and the {@code dataset} it displays, and makes it the active pyramidal. + * Registers a BDV {@code window} and the {@code dataset} owning its current source, replacing + * the window's previous one, and makes it the active pyramidal. */ public void registerBdvWindow( final Window window, final Pyramidal dataset ) { @@ -241,4 +251,14 @@ public List< Pyramidal > getPyramidals() { return objectService.getObjects( Pyramidal.class ); } + + /** + * Returns the {@link PyramidContents} of all registered {@link Pyramidal}s, each once however + * many of them show it. Distinct means the same read, not the same location: an OME-Zarr opened + * twice is read twice and listed twice. + */ + public List< PyramidContents< ? > > getPyramidContents() + { + return getPyramidals().stream().map( Pyramidal::getPyramidContents ).distinct().collect( Collectors.toList() ); + } } diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/read/OmeZarr.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/read/OmeZarr.java index 99a4b77a6..c850281ce 100644 --- a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/read/OmeZarr.java +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/read/OmeZarr.java @@ -53,6 +53,7 @@ import ome.zarr.imglib2.exceptions.SingleArrayAxesUnknownException; import ome.zarr.fiji.PyramidalBdv; import ome.zarr.fiji.PyramidalDataset; +import ome.zarr.fiji.plugins.ActiveViewerService; import ome.zarr.fiji.plugins.PyramidalService; import ome.zarr.fiji.read.exceptions.NonExistingResolutionLevelException; import ome.zarr.fiji.util.BdvUtils; @@ -386,6 +387,7 @@ private PyramidalDataset showAsDataset( final PyramidContents< ? > contents, fin { final PyramidalDataset dataset = new PyramidalDataset( context, contents, resolutionLevel ); context.getService( UIService.class ).show( dataset ); + context.getService( ActiveViewerService.class ).setActiveViewer( null ); context.getService( PyramidalService.class ).setActivePyramidal( dataset ); logger.info( "Opened dataset at resolution level {} in ImageJ: {}", resolutionLevel, inputUri ); return dataset; @@ -436,8 +438,21 @@ private static boolean confirmWithDialog( final String message ) * @return the resulting {@link BdvHandle}, or {@code null} if opening failed or * the user declined to open the image */ - // NB: the return value is for API and script users public BdvHandle showInBdv() + { + return showInBdv( null ); + } + + /** + * Adds the {@link ome.zarr.fiji.Pyramidal} as a {@link PyramidalBdv} to the already open + * BigDataViewer window of {@code bdvHandle}, and registers it with the {@link PyramidalService} lifecycle. + * Unlike {@link #showInBdv()}, it does not change the viewer's timepoint. + * + * @param bdvHandle the BigDataViewer to add to, or {@code null} to open a new window like {@link #showInBdv()} + * @return the resulting {@link BdvHandle}, or {@code null} if opening failed or + * the user declined to open the image. + */ + public BdvHandle showInBdv( final BdvHandle bdvHandle ) { return openPyramidImage( () -> { @@ -445,8 +460,9 @@ public BdvHandle showInBdv() return null; final PyramidalBdv< ? > pyramidal = new PyramidalBdv<>( context, readContents() ); final PyramidalService pyramidalService = context.getService( PyramidalService.class ); - final BdvHandle result = BdvUtils.showBdvAndRegisterWindow( pyramidal, pyramidalService ); - logger.info( "Opened pyramidal in BigDataViewer: {}", inputUri ); + final BdvHandle result = BdvUtils.show( pyramidal, bdvHandle, pyramidalService ); + context.getService( ActiveViewerService.class ).setActiveViewer( result.getViewerPanel() ); + logger.info( "{} pyramidal in BigDataViewer: {}", bdvHandle == null ? "Opened" : "Added", inputUri ); return result; } ); } diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/BdvUtils.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/BdvUtils.java index 2166aaa30..2ce31bd4e 100644 --- a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/BdvUtils.java +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/BdvUtils.java @@ -29,6 +29,7 @@ package ome.zarr.fiji.util; import java.awt.Color; +import java.awt.Component; import java.awt.Container; import java.awt.Window; import java.awt.event.WindowAdapter; @@ -39,6 +40,7 @@ import net.imglib2.type.numeric.ARGBType; +import org.scijava.Context; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,48 +52,111 @@ import bdv.viewer.ConverterSetupBounds; import bdv.viewer.ConverterSetups; import bdv.viewer.SourceAndConverter; +import bdv.viewer.ViewerPanel; import bdv.viewer.ViewerState; +import bdv.viewer.ViewerStateChange; import ome.zarr.fiji.PyramidalBdv; +import ome.zarr.fiji.plugins.ActiveViewerService; import ome.zarr.fiji.plugins.PyramidalService; +import ome.zarr.imglib2.PyramidContents; import ome.zarr.imglib2.metadata.Omero; public class BdvUtils { private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); + /** The client property of a {@link ViewerPanel} holding its {@link BdvHandle}, see {@link #bdvHandleOf}. */ + private static final String BDV_HANDLE_KEY = BdvHandle.class.getName(); + private BdvUtils() { // prevent instantiation of this class } /** - * Displays the given pyramidal in a BigDataViewer (BDV) window and registers - * it with {@code pyramidalService} for focus tracking.
+ * Displays the given pyramidal in a new BigDataViewer (BDV) window, see + * {@link #show(PyramidalBdv, BdvHandle, PyramidalService)}. + * + * @param pyramidalBdv the input {@link PyramidalBdv} to be displayed in BDV + * @param pyramidalService the service to notify of focus changes, or {@code null} to skip tracking + * @return the {@code BdvHandle} of the new window + */ + public static BdvHandle show( final PyramidalBdv< ? > pyramidalBdv, final PyramidalService pyramidalService ) + { + return show( pyramidalBdv, null, pyramidalService ); + } + + /** + * Displays each of {@code contents} as a new {@link PyramidalBdv}, all in one new BigDataViewer + * (BDV) window, see {@link #show(PyramidalBdv, BdvHandle, PyramidalService)}. + * + * @param contents the OME-Zarrs to display, in the order their sources are added + * @param context the SciJava context of the new pyramidals + * @param pyramidalService the service to notify of focus changes, or {@code null} to skip tracking + * @return the {@code BdvHandle} of the new window, or {@code null} if {@code contents} is empty + */ + public static BdvHandle showInOneWindow( final List< PyramidContents< ? > > contents, final Context context, + final PyramidalService pyramidalService ) + { + BdvHandle bdvHandle = null; + for ( final PyramidContents< ? > pyramidContents : contents ) + bdvHandle = show( new PyramidalBdv<>( context, pyramidContents ), bdvHandle, pyramidalService ); + return bdvHandle; + } + + /** + * Displays the given pyramidal in a new BigDataViewer (BDV) window, or adds it to the open one of + * {@code bdvHandle}, applies its OMERO channel properties and registers it with + * {@code pyramidalService} for focus tracking. {@link #bdvHandleOf} finds its handle.
* Increments the pyramidal's reference count and decrements it when the window closes. - * If {@code pyramidalService} is non-null, the pyramidal is immediately marked as active. - * Later focus changes are picked up centrally by {@link PyramidalService}. + * Only a new window is moved to the timepoint the OMERO metadata names. When adding, the + * viewer's timepoint is left alone, so the images already shown do not jump. * * @param pyramidalBdv the input {@link PyramidalBdv} to be displayed in BDV + * @param bdvHandle the BDV to add it to, or {@code null} to open a new window * @param pyramidalService the service to notify of focus changes, or {@code null} to skip tracking - * @return a {@code BdvHandle} instance representing the BDV window + * @return the {@code BdvHandle} of the window showing the pyramidal */ - public static BdvHandle showBdvAndRegisterWindow( final PyramidalBdv< ? > pyramidalBdv, final PyramidalService pyramidalService ) + public static BdvHandle show( final PyramidalBdv< ? > pyramidalBdv, final BdvHandle bdvHandle, + final PyramidalService pyramidalService ) { - BdvHandle bdvHandle = BdvFunctions.show( pyramidalBdv.asSources(), pyramidalBdv.getPyramidContents().numTimepoints(), - BdvOptions.options().frameTitle( pyramidalBdv.getName() ) ).getBdvHandle(); + final PyramidContents< ? > pyramidContents = pyramidalBdv.getPyramidContents(); + final BdvOptions options = bdvHandle == null + ? BdvOptions.options().frameTitle( pyramidalBdv.getName() ) + : BdvOptions.options().addTo( bdvHandle ); + final BdvHandle shownIn = BdvFunctions.show( pyramidalBdv.asSources(), pyramidContents.numTimepoints(), options ).getBdvHandle(); + shownIn.getViewerPanel().putClientProperty( BDV_HANDLE_KEY, shownIn ); - final Omero omero = pyramidalBdv.getPyramidContents().omero; - final List< Omero.Channel > omeroChannels = omeroChannels( omero, pyramidalBdv.asSources().size() ); - setTimepoint( omero, bdvHandle.getViewerPanel().state() ); - setChannelProperties( omeroChannels, pyramidalBdv.asSources(), bdvHandle.getConverterSetups(), bdvHandle.getViewerPanel().state() ); + if ( bdvHandle == null ) + setTimepoint( pyramidContents.omero, shownIn.getViewerPanel().state() ); + final List< Omero.Channel > omeroChannels = omeroChannels( pyramidContents.omero, pyramidalBdv.asSources().size() ); + setChannelProperties( omeroChannels, pyramidalBdv.asSources(), shownIn.getConverterSetups(), shownIn.getViewerPanel().state() ); - Container topLevelContainer = bdvHandle.getViewerPanel().getRootPane().getParent(); + Container topLevelContainer = shownIn.getViewerPanel().getRootPane().getParent(); if ( topLevelContainer instanceof Window ) { final Window window = ( Window ) topLevelContainer; - registerBdvWindow( pyramidalBdv, window, pyramidalService ); + registerWindow( pyramidalBdv, window, shownIn, pyramidalService ); } - return bdvHandle; + return shownIn; + } + + /** + * The {@link BdvHandle} of {@code viewer} if it is the viewer of a BigDataViewer shown via + * {@link #show(PyramidalBdv, BdvHandle, PyramidalService)}, else {@code null}, e.g., for an + * ImageJ window or a BigDataViewer opened by another tool. + *

+ * Mirrors the {@code BdvHandle.of( ViewerPanel )} proposed in + * bigdataviewer-core#227, + * including its client property key. Once that exists, this can delegate to it and reach every + * BigDataViewer. + * + * @param viewer an image viewer as returned by {@link ActiveViewerService#getActiveViewer()}, may be {@code null} + * @return the handle to add sources to {@code viewer} with, or {@code null} + */ + public static BdvHandle bdvHandleOf( final Component viewer ) + { + return viewer instanceof ViewerPanel ? ( BdvHandle ) ( ( ViewerPanel ) viewer ).getClientProperty( BDV_HANDLE_KEY ) : null; } /** @@ -99,18 +164,33 @@ public static BdvHandle showBdvAndRegisterWindow( final PyramidalBdv< ? > pyrami * registers the BDV window with it for focus tracking. Also installs a listener to decrement the * reference count (and unregister the window) when the BDV window closes. *

+ * A BDV window can show several pyramidals. + * The {@code pyramidalService} maps the BDV Window to the {@code pyramidal} owning BDV's current + * source. + * So {@code pyramidal} is registered now only if its source is current (BDV makes the + * first source added to an empty viewer current). The {@code pyramidal} is updated whenever the user makes one of its + * sources current. + *

* Focus switches themselves are observed centrally by {@link PyramidalService} via the AWT * {@link java.awt.KeyboardFocusManager}, so no per-window focus listener is needed here. * @param pyramidalBdv the {@link PyramidalBdv} the window displays * @param window the top-level window to track + * @param bdvHandle the handle of the BDV in {@code window} * @param pyramidalService the service to register with, or {@code null} to only reference-count */ - public static void registerBdvWindow( final PyramidalBdv< ? > pyramidalBdv, final Window window, - final PyramidalService pyramidalService ) + private static void registerWindow( final PyramidalBdv< ? > pyramidalBdv, final Window window, + final BdvHandle bdvHandle, final PyramidalService pyramidalService ) { pyramidalBdv.incrementReferences(); if ( pyramidalService != null ) - pyramidalService.registerBdvWindow( window, pyramidalBdv ); + { + if ( isCurrentSource( pyramidalBdv, bdvHandle ) ) + pyramidalService.registerBdvWindow( window, pyramidalBdv ); + bdvHandle.getViewerPanel().state().changeListeners().add( change -> { + if ( change == ViewerStateChange.CURRENT_SOURCE_CHANGED && isCurrentSource( pyramidalBdv, bdvHandle ) ) + pyramidalService.registerBdvWindow( window, pyramidalBdv ); + } ); + } window.addWindowListener( new WindowAdapter() { @Override @@ -123,6 +203,15 @@ public void windowClosed( final WindowEvent e ) } ); } + /** + * Whether the current source of {@code bdvHandle}'s viewer is one of {@code pyramidalBdv}'s + * sources. Checked per viewer, since one pyramidal may be shown in several. + */ + private static boolean isCurrentSource( final PyramidalBdv< ? > pyramidalBdv, final BdvHandle bdvHandle ) + { + return pyramidalBdv.asSources().contains( bdvHandle.getViewerPanel().state().getCurrentSource() ); + } + /** * Colors each of {@code sources} the way the dataset's OMERO metadata asks * for, gives it that channel's display range, and shows or hides it. @@ -132,7 +221,7 @@ public void windowClosed( final WindowEvent e ) * @param converterSetups the viewer's converter setups, to look {@code sources} up in * @param state the viewer state that decides which sources are visible */ - public static void setChannelProperties( final List< Omero.Channel > omeroChannels, + private static void setChannelProperties( final List< Omero.Channel > omeroChannels, final List< ? extends SourceAndConverter< ? > > sources, final ConverterSetups converterSetups, final ViewerState state ) { @@ -181,7 +270,7 @@ private static void setDisplayRangeAndBounds( final ConverterSetup converterSetu * @param numChannels the number of display sources to be configured * @return the channels to apply, empty if they cannot be applied */ - public static List< Omero.Channel > omeroChannels( final Omero omero, final int numChannels ) + static List< Omero.Channel > omeroChannels( final Omero omero, final int numChannels ) { if ( omero == null || omero.channels == null || omero.channels.isEmpty() ) return Collections.emptyList(); diff --git a/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/OmeZarrImagesChooser.java b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/OmeZarrImagesChooser.java new file mode 100644 index 000000000..e575b2813 --- /dev/null +++ b/ome-zarr-fiji/src/main/java/ome/zarr/fiji/util/OmeZarrImagesChooser.java @@ -0,0 +1,78 @@ +/*- + * #%L + * OME-Zarr integration into FIJI + * %% + * Copyright (C) 2022 - 2026 SciJava 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 ome.zarr.fiji.util; + +import java.awt.Dimension; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JOptionPane; + +import ome.zarr.imglib2.PyramidContents; + +/** + * Lets the user tick which of several open OME-Zarrs to show in one viewer window, + * as BigDataViewer's "Open Current Image" does for ImageJ images. + */ +public class OmeZarrImagesChooser +{ + private OmeZarrImagesChooser() + { + // static utility + } + + /** + * Shows a modal dialog with one checkbox per OME-Zarr in {@code candidates}, labeled by its name. + * + * @param candidates the OME-Zarrs to choose from + * @param preselected the one to be ticked when the dialog opens, typically the active pyramid, or {@code null} for none + * @return the ticked OME-Zarrs in the order of {@code candidates}, or an empty list if the user cancels + */ + public static List< PyramidContents< ? > > choose( final List< PyramidContents< ? > > candidates, final PyramidContents< ? > preselected ) + { + final List< JCheckBox > checkboxes = candidates.stream() + .map( contents -> new JCheckBox( contents.name, contents == preselected ) ) + .collect( Collectors.toList() ); + final JOptionPane pane = new JOptionPane( checkboxes.toArray(), JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION ); + final JDialog dialog = pane.createDialog( "OME-Zarr Images to open" ); + dialog.setMinimumSize( new Dimension( 280, 0 ) ); + dialog.setVisible( true ); + dialog.dispose(); + if ( !Integer.valueOf( JOptionPane.OK_OPTION ).equals( pane.getValue() ) ) + return Collections.emptyList(); + return IntStream.range( 0, candidates.size() ) + .filter( i -> checkboxes.get( i ).isSelected() ) + .mapToObj( candidates::get ) + .collect( Collectors.toList() ); + } +} diff --git a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/PyramidalBdvTestBase.java b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/PyramidalBdvTestBase.java index fa2bf8cd0..76eb3f8f1 100644 --- a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/PyramidalBdvTestBase.java +++ b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/PyramidalBdvTestBase.java @@ -56,7 +56,6 @@ import ome.zarr.ZarrTestUtils; import ome.zarr.fiji.util.BdvUtils; import ome.zarr.imglib2.PyramidContents; -import ome.zarr.imglib2.metadata.Omero; /** * Shared parameterized tests for the BigDataViewer Fiji wrapper @@ -227,7 +226,7 @@ default void testConverterSetup( final String resource ) throws URISyntaxExcepti { PyramidContents< ? > contents = read( resource, context ); PyramidalBdv< ? > pyramidalBdv = new PyramidalBdv<>( context, contents ); - BdvHandle bdvHandle = BdvUtils.showBdvAndRegisterWindow( pyramidalBdv, null ); + BdvHandle bdvHandle = BdvUtils.show( pyramidalBdv, null ); List< ConverterSetup > converterSetups = bdvHandle.getConverterSetups().getConverterSetups( pyramidalBdv.asSources() ); assertNotNull( converterSetups ); @@ -269,7 +268,7 @@ default void testOmeroDisplaySettings( final String resource ) throws URISyntaxE // NB: neither example turns a channel off, so switch one off to cover both answers. contents.omero.channels.get( 1 ).active = false; PyramidalBdv< ? > pyramidalBdv = new PyramidalBdv<>( context, contents ); - BdvHandle bdvHandle = BdvUtils.showBdvAndRegisterWindow( pyramidalBdv, null ); + BdvHandle bdvHandle = BdvUtils.show( pyramidalBdv, null ); ViewerState state = bdvHandle.getViewerPanel().state(); assertEquals( 1, state.getCurrentTimepoint(), "the timepoint the rendering defaults name" ); List< ? extends SourceAndConverter< ? > > sources = pyramidalBdv.asSources(); @@ -279,21 +278,4 @@ default void testOmeroDisplaySettings( final String resource ) throws URISyntaxE bdvHandle.close(); } } - - /** Omero channels are applied only when there are as many of them as there are sources. */ - @ParameterizedTest - @ValueSource( strings = { DATASET_5D_V4, DATASET_5D_V5 } ) - default void testOmeroChannelCountMismatch( final String resource ) throws URISyntaxException - { - try (Context context = new Context()) - { - final Omero omero = read( resource, context ).omero; - final int numChannels = omero.channels.size(); - assertEquals( numChannels, BdvUtils.omeroChannels( omero, numChannels ).size(), "as many channels as sources" ); - assertTrue( BdvUtils.omeroChannels( omero, numChannels + 1 ).isEmpty(), "fewer channels than sources" ); - assertTrue( BdvUtils.omeroChannels( omero, numChannels - 1 ).isEmpty(), "more channels than sources" ); - // empty, never null, so callers test isEmpty() and never dereference null - assertTrue( BdvUtils.omeroChannels( null, numChannels ).isEmpty(), "no omero metadata at all" ); - } - } } diff --git a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/open/OmeZarrOpenerServiceTest.java b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/open/OmeZarrOpenerServiceTest.java index b1d3386f0..89b738863 100644 --- a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/open/OmeZarrOpenerServiceTest.java +++ b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/open/OmeZarrOpenerServiceTest.java @@ -94,7 +94,7 @@ private static PluginInfo< OmeZarrOpener > register( final Context context, return info; } - private static OmeZarr omeZarrFor( final Context context ) + private static OmeZarr omeZarrWithoutBackend( final Context context ) { return new OmeZarr( URI_UNDER_TEST, context, null, null, message -> {} ); } @@ -165,10 +165,10 @@ void openRunsTheNamedOpenerAndReportsAnUnknownName() register( context, RecordingOpener.class, "recording", Priority.NORMAL ); RecordingOpener.opened = 0; - assertTrue( openerService.open( "recording", omeZarrFor( context ) ) ); + assertTrue( openerService.open( "recording", omeZarrWithoutBackend( context ) ) ); assertEquals( 1, RecordingOpener.opened ); - assertFalse( openerService.open( "uninstalled", omeZarrFor( context ) ), + assertFalse( openerService.open( "uninstalled", omeZarrWithoutBackend( context ) ), "An unknown name opens nothing and says so" ); assertEquals( 1, RecordingOpener.opened ); } @@ -182,7 +182,7 @@ void aFailingOpenerDoesNotTakeThePipelineDown() final OmeZarrOpenerService openerService = context.getService( OmeZarrOpenerService.class ); register( context, FailingOpener.class, "failing", Priority.NORMAL ); - assertDoesNotThrow( () -> openerService.open( "failing", omeZarrFor( context ) ) ); + assertDoesNotThrow( () -> openerService.open( "failing", omeZarrWithoutBackend( context ) ) ); } } @@ -192,7 +192,7 @@ void metadataFallsBackFromDescriptionToLabelToName() try (Context context = new Context()) { final OmeZarrOpenerService openerService = context.getService( OmeZarrOpenerService.class ); - final OmeZarr omeZarr = omeZarrFor( context ); + final OmeZarr omeZarr = omeZarrWithoutBackend( context ); // Neither label nor description given, so both fall back to the name. final PluginInfo< OmeZarrOpener > bare = register( context, RecordingOpener.class, "bare", Priority.NORMAL ); diff --git a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/plugins/ActiveViewerServiceTest.java b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/plugins/ActiveViewerServiceTest.java new file mode 100644 index 000000000..405faa4e0 --- /dev/null +++ b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/plugins/ActiveViewerServiceTest.java @@ -0,0 +1,161 @@ +/*- + * #%L + * OME-Zarr extras for Fiji + * %% + * Copyright (C) 2022 - 2026 SciJava 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 ome.zarr.fiji.plugins; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.awt.GridLayout; +import java.util.Collections; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JTable; + +import net.imagej.patcher.LegacyInjector; + +import org.junit.jupiter.api.Test; + +import bdv.cache.CacheControl; +import bdv.viewer.ViewerPanel; +import ij.IJ; +import ij.ImagePlus; + +/** + * Which image viewer a focus owner belongs to, as seen in Fiji with a focus probe: ImageJ + * focuses the {@code ImageWindow} or its canvas, BDV its display canvas or, in the side panel, + * the source table or group tree, which lie outside the {@link ViewerPanel}. + */ +class ActiveViewerServiceTest +{ + static + { + // Patch IJ1 before ij.* classes are loaded, as ActiveViewerService does. + LegacyInjector.preinit(); + } + + @Test + void focusInsideABdvViewerPanelGivesThatPanel() + { + final ViewerPanel viewerPanel = bdvViewerPanel(); + try + { + assertSame( viewerPanel, ActiveViewerService.viewerOf( viewerPanel.getDisplayComponent() ) ); + } + finally + { + viewerPanel.stop(); + } + } + + @Test + void focusInTheSidePanelGivesTheWindowsOnlyBdvViewerPanel() + { + final ViewerPanel viewerPanel = bdvViewerPanel(); + final JTable sourceTable = new JTable(); + final JFrame frame = frameWith( viewerPanel, sourceTable ); + try + { + assertSame( viewerPanel, ActiveViewerService.viewerOf( sourceTable ) ); + } + finally + { + viewerPanel.stop(); + frame.dispose(); + } + } + + @Test + void focusBesideTwoBdvViewerPanelsGivesNone() + { + final ViewerPanel left = bdvViewerPanel(); + final ViewerPanel right = bdvViewerPanel(); + final JTable sourceTable = new JTable(); + final JFrame frame = frameWith( left, right, sourceTable ); + try + { + assertNull( ActiveViewerService.viewerOf( sourceTable ) ); + assertSame( right, ActiveViewerService.viewerOf( right.getDisplayComponent() ) ); + } + finally + { + left.stop(); + right.stop(); + frame.dispose(); + } + } + + @Test + void focusOutsideAnyImageViewerGivesNone() + { + final JButton button = new JButton(); + final JFrame frame = frameWith( button ); + try + { + assertNull( ActiveViewerService.viewerOf( button ) ); + assertNull( ActiveViewerService.viewerOf( frame ) ); + assertNull( ActiveViewerService.viewerOf( null ) ); + } + finally + { + frame.dispose(); + } + } + + @Test + void focusInAnImageJWindowGivesThatWindow() + { + final ImagePlus imagePlus = IJ.createImage( "Demo", "8-bit ramp", 64, 64, 1 ); + imagePlus.show(); + try + { + assertSame( imagePlus.getWindow(), ActiveViewerService.viewerOf( imagePlus.getWindow() ) ); + assertSame( imagePlus.getWindow(), ActiveViewerService.viewerOf( imagePlus.getCanvas() ) ); + } + finally + { + imagePlus.close(); + } + } + + private static ViewerPanel bdvViewerPanel() + { + return new ViewerPanel( Collections.emptyList(), 1, new CacheControl.Dummy() ); + } + + /** A window holding {@code components}, never shown: {@code viewerOf} only walks the component tree. */ + private static JFrame frameWith( final java.awt.Component... components ) + { + final JFrame frame = new JFrame(); + frame.getContentPane().setLayout( new GridLayout( 1, components.length ) ); + for ( final java.awt.Component component : components ) + frame.getContentPane().add( component ); + return frame; + } +} diff --git a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/plugins/PyramidalServiceTest.java b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/plugins/PyramidalServiceTest.java index fd1a1e9e2..8874a3236 100644 --- a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/plugins/PyramidalServiceTest.java +++ b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/plugins/PyramidalServiceTest.java @@ -50,6 +50,7 @@ import ome.zarr.fiji.Pyramidal; import ome.zarr.fiji.read.OmeZarr; +import ome.zarr.imglib2.PyramidBackend; import ome.zarr.n5.N5PyramidBackend; import ome.zarr.ZarrTestUtils; @@ -172,4 +173,31 @@ void openTwoBdvDatasets_switchFocusAndCloseInReverseOrder() throws URISyntaxExce assertTrue( pyramidalService.getPyramidals().isEmpty() ); } } + + /** + * An OME-Zarr shown twice, from one read, is listed only once in the pyramid contents. + */ + @Test + void getPyramidContents_listsEachReadOnce() throws URISyntaxException, InterruptedException, InvocationTargetException + { + try (Context context = new Context()) + { + PyramidalService pyramidalService = context.getService( PyramidalService.class ); + PyramidBackend pyramidBackend = new N5PyramidBackend(); + OmeZarr omeZarr2d = new OmeZarr( ZarrTestUtils.resourcePath( ZARR_2D ).toUri(), context, pyramidBackend ); + BdvHandle bdvHandle = omeZarr2d.showInBdv(); + omeZarr2d.showInBdv( bdvHandle ); // the cached read, a second time - 2nd pyramidal (but same pyramid contents) + new OmeZarr( ZarrTestUtils.resourcePath( ZARR_3D ).toUri(), context, pyramidBackend ).showInBdv( bdvHandle ); // a second read, a third pyramidal, but only a second pyramid contents + try + { + assertEquals( 3, pyramidalService.getPyramidals().size() ); + assertEquals( 2, pyramidalService.getPyramidContents().size() ); + } + finally + { + bdvHandle.close(); + SwingUtilities.invokeAndWait( () -> {} ); + } + } + } } diff --git a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/util/BdvUtilsTest.java b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/util/BdvUtilsTest.java new file mode 100644 index 000000000..62febe0d1 --- /dev/null +++ b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/util/BdvUtilsTest.java @@ -0,0 +1,102 @@ +/*- + * #%L + * OME-Zarr extras for Fiji + * %% + * Copyright (C) 2022 - 2026 SciJava 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 ome.zarr.fiji.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; + +import javax.swing.SwingUtilities; + +import org.junit.jupiter.api.Test; +import org.scijava.Context; + +import bdv.util.BdvHandle; +import ome.zarr.ZarrTestUtils; +import ome.zarr.fiji.Pyramidal; +import ome.zarr.fiji.PyramidalBdv; +import ome.zarr.fiji.plugins.PyramidalService; +import ome.zarr.imglib2.PyramidContents; +import ome.zarr.imglib2.metadata.Omero; +import ome.zarr.n5.N5PyramidBackend; + +class BdvUtilsTest +{ + private static final String SPOTS = "ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr"; + + private static final String LABELS = "ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr"; + + /** Two OME-Zarrs end up in one BDV window, as one pyramidal each. */ + @Test + void showInOneWindowShowsAllInOneWindow() throws Exception + { + PyramidContents< ? > spots = N5PyramidBackend.readPyramid( ZarrTestUtils.resourcePath( SPOTS ).toUri() ); + PyramidContents< ? > labels = N5PyramidBackend.readPyramid( ZarrTestUtils.resourcePath( LABELS ).toUri() ); + try (Context context = new Context()) + { + PyramidalService pyramidalService = context.getService( PyramidalService.class ); + BdvHandle bdvHandle = BdvUtils.showInOneWindow( Arrays.asList( spots, labels ), context, pyramidalService ); + try + { + assertEquals( 2, bdvHandle.getViewerPanel().state().getSources().size() ); + assertEquals( 2, pyramidalService.getPyramidals().size() ); + for ( Pyramidal pyramidal : pyramidalService.getPyramidals() ) + assertTrue( bdvHandle.getViewerPanel().state().getSources().containsAll( ( ( PyramidalBdv< ? > ) pyramidal ).asSources() ) ); + } + finally + { + bdvHandle.close(); + SwingUtilities.invokeAndWait( () -> {} ); + } + } + } + + @Test + void showInOneWindowOpensNoWindowForNothing() + { + assertNull( BdvUtils.showInOneWindow( Collections.emptyList(), null, null ) ); + } + + /** Omero channels are applied only when there are as many of them as there are sources. */ + @Test + void testOmeroChannelCountMismatch() + { + final Omero omero = new Omero(); + omero.channels = Arrays.asList( new Omero.Channel(), new Omero.Channel(), new Omero.Channel() ); + final int numChannels = omero.channels.size(); + assertEquals( numChannels, BdvUtils.omeroChannels( omero, numChannels ).size(), "as many channels as sources" ); + assertTrue( BdvUtils.omeroChannels( omero, numChannels + 1 ).isEmpty(), "fewer channels than sources" ); + assertTrue( BdvUtils.omeroChannels( omero, numChannels - 1 ).isEmpty(), "more channels than sources" ); + // empty, never null, so callers test isEmpty() and never dereference null + assertTrue( BdvUtils.omeroChannels( null, numChannels ).isEmpty(), "no omero metadata at all" ); + } +} diff --git a/ome-zarr-fiji/src/test/java/ome/zarr/fiji/util/OmeZarrImagesChooserTest.java b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/util/OmeZarrImagesChooserTest.java new file mode 100644 index 000000000..21ac4d9e9 --- /dev/null +++ b/ome-zarr-fiji/src/test/java/ome/zarr/fiji/util/OmeZarrImagesChooserTest.java @@ -0,0 +1,93 @@ +/*- + * #%L + * OME-Zarr extras for Fiji + * %% + * Copyright (C) 2022 - 2026 SciJava 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 ome.zarr.fiji.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.awt.AWTEvent; +import java.awt.Toolkit; +import java.awt.event.AWTEventListener; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; + +import org.junit.jupiter.api.Test; + +import ome.zarr.ZarrTestUtils; +import ome.zarr.imglib2.PyramidContents; +import ome.zarr.n5.N5PyramidBackend; + +class OmeZarrImagesChooserTest +{ + private static final String SPOTS = "ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr"; + + private static final String LABELS = "ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr"; + + /** + * Opens the real dialog and, once it is shown, notes which boxes are ticked, then unticks the + * preselected spots, ticks the labels and presses OK. Asserting happens back on the test thread. + */ + @Test + void chooseGivesTheTickedOmeZarrs() throws Exception + { + PyramidContents< ? > spots = N5PyramidBackend.readPyramid( ZarrTestUtils.resourcePath( SPOTS ).toUri() ); + PyramidContents< ? > labels = N5PyramidBackend.readPyramid( ZarrTestUtils.resourcePath( LABELS ).toUri() ); + + List< Boolean > ticksWhenShown = new ArrayList<>(); + AWTEventListener user = event -> { + if ( event.getID() != WindowEvent.WINDOW_OPENED || !( event.getSource() instanceof JDialog ) ) + return; + JOptionPane pane = ( JOptionPane ) ( ( JDialog ) event.getSource() ).getContentPane().getComponent( 0 ); + JCheckBox spotsBox = ( JCheckBox ) ( ( Object[] ) pane.getMessage() )[ 0 ]; + JCheckBox labelsBox = ( JCheckBox ) ( ( Object[] ) pane.getMessage() )[ 1 ]; + ticksWhenShown.addAll( Arrays.asList( spotsBox.isSelected(), labelsBox.isSelected() ) ); + spotsBox.setSelected( false ); + labelsBox.setSelected( true ); + SwingUtilities.invokeLater( () -> pane.setValue( JOptionPane.OK_OPTION ) ); // press OK + }; + Toolkit.getDefaultToolkit().addAWTEventListener( user, AWTEvent.WINDOW_EVENT_MASK ); + try + { + List< PyramidContents< ? > > chosen = OmeZarrImagesChooser.choose( Arrays.asList( spots, labels ), spots ); + assertEquals( Arrays.asList( true, false ), ticksWhenShown, "the spots are preselected" ); + assertEquals( Collections.singletonList( labels ), chosen ); // the labels are chosen + } + finally + { + Toolkit.getDefaultToolkit().removeAWTEventListener( user ); + } + } +} diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/README.md b/test-shared/resources/ome/zarr/testdata/bdv_testing/README.md new file mode 100644 index 000000000..f39dba6be --- /dev/null +++ b/test-shared/resources/ome/zarr/testdata/bdv_testing/README.md @@ -0,0 +1,45 @@ +# Logo Spots and Spot Segmentation Test OME-Zarr Datasets + +Two related 2D datasets (OME-Zarr v0.5) with identical extents, so that they overlay exactly: + +* `logo_spots.ome.zarr` → intensity image (uint8): 23 Gaussian spots laid out like the cubes of this repository's logo, + next to the Zarr wordmark in the same blurred style, on a noisy background +* `logo_labels.ome.zarr` → segmentation (uint16): one label id per spot (1–23) and per letter of "zarr" + (24–27), 0 is background + +They are deliberately not called "blobs", so they do not get mixed up with Fiji's well-known *Blobs* sample image. + +Each dataset stores: + +* A single 2D image (Y × X), 512 × 512 pixels +* 2 resolution levels (multiscale pyramid, factor 2) + +The second level is made by subsampling, not averaging, so the label ids stay valid. + +Both carry OMERO metadata with a channel color and contrast limits: the spots are green with a window of 20–230, the +labels magenta with a window of 0–1, so every label is shown at full brightness right after opening. + +## Purpose + +These datasets are designed for testing the display of two OME-Zarr images in the same viewer, e.g. adding a +segmentation to an already open BigDataViewer (issue #127). + +## How to Reproduce + +To recreate the datasets, first create a Conda environment using the provided `conda.yml`: + +```bash +conda env create -f conda.yml +conda activate ome-zarr-test +``` + +Then, run the dataset creation script: + +```bash +python create_logo_spots.py +``` + +It reads the Zarr wordmark from `doc/logo/zarr-pink-horizontal.svg` in this repository and generates: + +* `logo_spots.ome.zarr` +* `logo_labels.ome.zarr` diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/conda.yml b/test-shared/resources/ome/zarr/testdata/bdv_testing/conda.yml new file mode 100644 index 000000000..d2a5b5820 --- /dev/null +++ b/test-shared/resources/ome/zarr/testdata/bdv_testing/conda.yml @@ -0,0 +1,11 @@ +name: ome-zarr-test +channels: + - conda-forge +dependencies: + - python=3.11 + - numpy + - zarr + - ome-zarr + - cairosvg + - pillow + - pip diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/create_logo_spots.py b/test-shared/resources/ome/zarr/testdata/bdv_testing/create_logo_spots.py new file mode 100644 index 000000000..3c45e0731 --- /dev/null +++ b/test-shared/resources/ome/zarr/testdata/bdv_testing/create_logo_spots.py @@ -0,0 +1,200 @@ +### +# #%L +# OME-Zarr extras for Fiji +# %% +# Copyright (C) 2022 - 2026 SciJava 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% +### +import io +import pathlib + +import cairosvg +import numpy as np +import zarr +from PIL import Image, ImageDraw +from ome_zarr.writer import write_multiscale +from ome_zarr.io import parse_url +from ome_zarr.format import FormatV05 + +# The Zarr logo the repository logo uses, see doc/logo +ZARR_SVG = pathlib.Path(__file__).resolve().parents[6] / "doc" / "logo" / "zarr-pink-horizontal.svg" + +# Cells of the repository logo's glyph (x right, y up), as in doc/logo/logo.py's "logo-zarr" +# variant: the wordmark replaces the top edge's three rightmost cubes. +GLYPH = [(0, y) for y in range(7)] + [(x, 6) for x in range(1, 4)] \ + + [(2, 0), (3, 0), (4, 0), (4, 1)] \ + + [(x, y) for x in (2, 4, 6) for y in (2, 3, 4)] + + +def create_logo_spots_and_labels(): + """ + Creates two related 2D OME-Zarr v0.5 datasets with identical extents: + - logo_spots.ome.zarr → uint8 intensity image: Gaussian spots laid out like the + repository logo's cubes, the blurred Zarr wordmark, and noise + - logo_labels.ome.zarr → uint16 segmentation, one label id per spot and per letter + Both have 2 multiscale levels (Y, X), so they overlay exactly in a viewer, + and OMERO metadata with a channel color and contrast limits. + """ + + Y, X = 512, 512 + pitch = 60 # px per glyph cell + left, top = 40, 50 # px from the image corner to the top left cell's corner + rng = np.random.default_rng(42) + + yy, xx = np.mgrid[0:Y, 0:X] + intensity = np.zeros((Y, X), dtype=np.float64) + labels = np.zeros((Y, X), dtype=np.uint16) + best = np.zeros((Y, X), dtype=np.float64) + + # ---------------------------------------------------------------------- + # One spot per cube, jittered, stretched and dimmed so the grid is not exact + # ---------------------------------------------------------------------- + for i, (cx, cy) in enumerate(GLYPH, start=1): + x = left + (cx + 0.5) * pitch + rng.normal(0, 6) + y = top + (6 - cy + 0.5) * pitch + rng.normal(0, 6) + sx, sy = rng.uniform(10, 16, size=2) + spot = np.exp(-(xx - x) ** 2 / (2 * sx ** 2) - (yy - y) ** 2 / (2 * sy ** 2)) * rng.uniform(0.7, 1.0) + intensity += spot + # pixel belongs to the spot that is brightest there, if above half its max + mask = (spot > 0.5 * spot.max()) & (spot > best) + labels[mask] = i + best = np.maximum(best, spot) + intensity /= intensity.max() + + # --------------------------------------------------------------------- + # The wordmark right of the top edge, blurred like the spots: each letter + # scaled to its own peak, dimmed on its own, and labelled where above half + # its peak, like a spot. The outlined "z" is filled first, as a blurred + # outline would come out a dim ring. + # --------------------------------------------------------------------- + word = wordmark(width=int(3.2 * pitch)) + cuts = letter_cuts(word > 0.5) + word[:, :cuts[0]] = np.maximum(word[:, :cuts[0]], holes(word[:, :cuts[0]] > 0.5)) + pad = 12 # px around the wordmark for the blur to spread into + word = np.pad(word, pad) + letter = np.searchsorted(cuts, np.arange(word.shape[1]) - pad) # letter index per column + glow = blur(word, sigma=3) + peak = np.array([glow[:, letter == j].max() for j in range(len(cuts) + 1)]) + glow /= peak[letter] + mask = glow > 0.5 + glow *= 0.9 * rng.uniform(0.7, 1.0, size=len(cuts) + 1)[letter] + wx, wy = left + int(4.1 * pitch) - pad, top + (pitch - word.shape[0]) // 2 + intensity[wy:wy + word.shape[0], wx:wx + word.shape[1]] += glow + region = labels[wy:wy + word.shape[0], wx:wx + word.shape[1]] + region[mask] = (len(GLYPH) + 1 + np.broadcast_to(letter, word.shape))[mask] + n_labels = int(labels.max()) + + intensity = intensity * 200 + rng.normal(20, 5, (Y, X)) + image = np.clip(intensity, 0, 255).astype(np.uint8) + + axes = [ + {"name": "y", "type": "space"}, + {"name": "x", "type": "space"}, + ] + + # Raw data in green (GFP-like), from the background level to the brightest spot + write(image, axes, "logo_spots.ome.zarr", "logo_spots", + omero_channel("spots", "00FF00", start=20, end=230, min=0, max=255)) + # Labels in magenta; a window of 0..1 shows every label id >= 1 at full brightness + write(labels, axes, "logo_labels.ome.zarr", "logo_labels", + omero_channel("labels", "FF00FF", start=0, end=1, min=0, max=n_labels)) + + +def wordmark(width): + """The "zarr" of the Zarr logo as coverage in 0..1, `width` px wide, without the + cube mark: rasterized, and cut at the widest run of empty columns.""" + png = cairosvg.svg2png(url=str(ZARR_SVG), output_width=1800) + art = Image.open(io.BytesIO(png)).getchannel("A") + alpha = np.array(art.crop(art.getbbox())) + runs = empty_column_runs(alpha > 0) + _, gap_end = max(runs, key=lambda r: r[1] - r[0]) + word = Image.fromarray(alpha[:, gap_end:]) + word = word.crop(word.getbbox()) + word = word.resize((width, round(word.height * width / word.width)), Image.LANCZOS) + return np.array(word) / 255.0 + + +def empty_column_runs(mask): + """(start, end) of every run of empty columns in `mask`.""" + empty = list(~mask.any(axis=0)) + [False] + runs, start = [], None + for x, blank in enumerate(empty): + if blank and start is None: + start = x + elif not blank and start is not None: + runs.append((start, x)) + start = None + return runs + + +def letter_cuts(mask): + """The columns that split `mask` into letters: the middle of each run of empty columns.""" + return [(start + end) // 2 for start, end in empty_column_runs(mask)] + + +def holes(mask): + """1 where `mask` encloses background, else 0: what a flood fill from outside does not reach.""" + outside = Image.fromarray(np.pad(mask, 1).astype(np.uint8) * 255).copy() # floodfill cannot write to fromarray's buffer + ImageDraw.floodfill(outside, (0, 0), 128) + return (np.array(outside)[1:-1, 1:-1] == 0).astype(np.float64) + + +def blur(a, sigma): + """`a` convolved with a Gaussian of `sigma` px, separably.""" + r = int(3 * sigma) + k = np.exp(-np.arange(-r, r + 1) ** 2 / (2 * sigma ** 2)) + k /= k.sum() + a = np.apply_along_axis(np.convolve, 0, a, k, mode="same") + return np.apply_along_axis(np.convolve, 1, a, k, mode="same") + + +def omero_channel(label, color, start, end, min, max): + return { + "channels": [{ + "label": label, + "color": color, + "active": True, + "window": {"start": start, "end": end, "min": min, "max": max}, + }], + "rdefs": {"model": "color"}, + } + + +def write(data, axes, path, name, omero): + store = parse_url(path, mode="w").store + root = zarr.group(store=store, overwrite=True, zarr_format=3) + write_multiscale( + [data, data[::2, ::2]], + group=root, + axes=axes, + fmt=FormatV05(), + storage_options=dict(chunks=(256, 256)), + name=name, + metadata={"omero": omero} + ) + print(f"OME-Zarr v0.5 written to {path}") + + +if __name__ == "__main__": + create_logo_spots_and_labels() diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/c/0/0 b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/c/0/0 new file mode 100644 index 000000000..ec49dcf3c Binary files /dev/null and b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/c/0/0 differ diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/c/0/1 b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/c/0/1 new file mode 100644 index 000000000..1cffb7abf Binary files /dev/null and b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/c/0/1 differ diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/c/1/0 b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/c/1/0 new file mode 100644 index 000000000..f18ba3b3e Binary files /dev/null and b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/c/1/0 differ diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/c/1/1 b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/c/1/1 new file mode 100644 index 000000000..895299880 Binary files /dev/null and b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/c/1/1 differ diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/zarr.json b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/zarr.json new file mode 100644 index 000000000..4806ee13b --- /dev/null +++ b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s0/zarr.json @@ -0,0 +1,46 @@ +{ + "shape": [ + 512, + 512 + ], + "data_type": "uint16", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 256, + 256 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "dimension_names": [ + "y", + "x" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s1/c/0/0 b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s1/c/0/0 new file mode 100644 index 000000000..dfe6afacd Binary files /dev/null and b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s1/c/0/0 differ diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s1/zarr.json b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s1/zarr.json new file mode 100644 index 000000000..6d7de2547 --- /dev/null +++ b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/s1/zarr.json @@ -0,0 +1,46 @@ +{ + "shape": [ + 256, + 256 + ], + "data_type": "uint16", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 256, + 256 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "dimension_names": [ + "y", + "x" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/zarr.json b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/zarr.json new file mode 100644 index 000000000..c4297f5d4 --- /dev/null +++ b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_labels.ome.zarr/zarr.json @@ -0,0 +1,68 @@ +{ + "attributes": { + "ome": { + "omero": { + "channels": [ + { + "label": "labels", + "color": "FF00FF", + "active": true, + "window": { + "start": 0, + "end": 1, + "min": 0, + "max": 27 + } + } + ], + "rdefs": { + "model": "color" + } + }, + "version": "0.5", + "multiscales": [ + { + "datasets": [ + { + "path": "s0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0 + ] + } + ] + }, + { + "path": "s1", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 2.0, + 2.0 + ] + } + ] + } + ], + "name": "logo_labels", + "axes": [ + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ] + } + ] + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/c/0/0 b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/c/0/0 new file mode 100644 index 000000000..a7dae2055 Binary files /dev/null and b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/c/0/0 differ diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/c/0/1 b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/c/0/1 new file mode 100644 index 000000000..3e7236409 Binary files /dev/null and b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/c/0/1 differ diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/c/1/0 b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/c/1/0 new file mode 100644 index 000000000..04646ac44 Binary files /dev/null and b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/c/1/0 differ diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/c/1/1 b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/c/1/1 new file mode 100644 index 000000000..089cad661 Binary files /dev/null and b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/c/1/1 differ diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/zarr.json b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/zarr.json new file mode 100644 index 000000000..63411b6dd --- /dev/null +++ b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s0/zarr.json @@ -0,0 +1,43 @@ +{ + "shape": [ + 512, + 512 + ], + "data_type": "uint8", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 256, + 256 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes" + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "dimension_names": [ + "y", + "x" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s1/c/0/0 b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s1/c/0/0 new file mode 100644 index 000000000..1de27cb0a Binary files /dev/null and b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s1/c/0/0 differ diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s1/zarr.json b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s1/zarr.json new file mode 100644 index 000000000..9ac35f8bd --- /dev/null +++ b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/s1/zarr.json @@ -0,0 +1,43 @@ +{ + "shape": [ + 256, + 256 + ], + "data_type": "uint8", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 256, + 256 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes" + }, + { + "name": "zstd", + "configuration": { + "level": 0, + "checksum": false + } + } + ], + "attributes": {}, + "dimension_names": [ + "y", + "x" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] +} \ No newline at end of file diff --git a/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/zarr.json b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/zarr.json new file mode 100644 index 000000000..d51c6cde7 --- /dev/null +++ b/test-shared/resources/ome/zarr/testdata/bdv_testing/logo_spots.ome.zarr/zarr.json @@ -0,0 +1,68 @@ +{ + "attributes": { + "ome": { + "omero": { + "channels": [ + { + "label": "spots", + "color": "00FF00", + "active": true, + "window": { + "start": 20, + "end": 230, + "min": 0, + "max": 255 + } + } + ], + "rdefs": { + "model": "color" + } + }, + "version": "0.5", + "multiscales": [ + { + "datasets": [ + { + "path": "s0", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 1.0, + 1.0 + ] + } + ] + }, + { + "path": "s1", + "coordinateTransformations": [ + { + "type": "scale", + "scale": [ + 2.0, + 2.0 + ] + } + ] + } + ], + "name": "logo_spots", + "axes": [ + { + "name": "y", + "type": "space" + }, + { + "name": "x", + "type": "space" + } + ] + } + ] + } + }, + "zarr_format": 3, + "node_type": "group" +} \ No newline at end of file