Skip to content

Commit aeacef5

Browse files
committed
n5/zarr loading: add fallback to DatasetSelectorDialog
1 parent fd5b0a3 commit aeacef5

2 files changed

Lines changed: 120 additions & 4 deletions

File tree

‎src/main/java/sc/fiji/snt/gui/cmds/BvvCmd.java‎

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,16 @@
2828
import com.jogamp.opengl.GLProfile;
2929
import net.imagej.ImgPlus;
3030
import mpicbg.spim.data.generic.AbstractSpimData;
31+
import org.janelia.saalfeldlab.n5.bdv.N5ViewerTreeCellRenderer;
32+
import org.janelia.saalfeldlab.n5.ij.N5Importer;
33+
import org.janelia.saalfeldlab.n5.ui.DatasetSelectorDialog;
34+
import org.jspecify.annotations.NonNull;
3135
import org.scijava.command.Command;
3236
import org.scijava.command.ContextCommand;
3337
import org.scijava.plugin.Parameter;
3438
import org.scijava.plugin.Plugin;
39+
import org.scijava.widget.FileWidget;
40+
import sc.fiji.snt.SNTPrefs;
3541
import sc.fiji.snt.SNTUtils;
3642
import sc.fiji.snt.gui.GuiUtils;
3743
import sc.fiji.snt.gui.ScriptInstaller;
@@ -41,12 +47,18 @@
4147
import sc.fiji.snt.viewer.Bdv;
4248
import sc.fiji.snt.viewer.Bvv;
4349

50+
import javax.swing.*;
4451
import java.io.File;
4552
import java.util.ArrayList;
4653
import java.util.List;
4754
import java.util.Objects;
55+
import java.util.concurrent.ExecutorService;
56+
import java.util.concurrent.Executors;
4857
import java.util.stream.Stream;
4958

59+
import static org.janelia.saalfeldlab.n5.bdv.N5ViewerCreator.n5vGroupParsers;
60+
import static org.janelia.saalfeldlab.n5.bdv.N5ViewerCreator.n5vParsers;
61+
5062
/**
5163
* Convenience command for starting a standalone Bdv/Bvv instance.
5264
*
@@ -68,10 +80,12 @@ and big data formats with lazy loading (N5, Zarr, HDF5, OME-TIFF,
6880
private static final String CONVERT = "Show me how to convert to multi-resolution pyramid image";
6981

7082

71-
@Parameter(label = "Main volume", description = "Primary image volume.\n"+ TOOLTIP)
83+
@Parameter(label = "Main volume", style = FileWidget.FILE_AND_DIRECTORY_STYLE,
84+
description = "Primary image volume.\n"+ TOOLTIP)
7285
File img1File;
7386

74-
@Parameter(required = false, label = "Secondary volume (optional)", description = "Optional image volume.\n"+ TOOLTIP)
87+
@Parameter(required = false, style = FileWidget.FILE_AND_DIRECTORY_STYLE,
88+
label = "Secondary volume (optional)", description = "Optional image volume.\n"+ TOOLTIP)
7589
File img2File;
7690

7791
@Parameter(required = false, label = "Reconstruction files (optional)",
@@ -116,13 +130,77 @@ public void run() {
116130
}
117131
}
118132

133+
/** True if path is an existing .n5 or .zarr directory. */
134+
private static boolean isN5OrZarrDir(final String path) {
135+
final File f = new File(path);
136+
final String lower = f.getName().toLowerCase();
137+
return f.isDirectory() && (lower.endsWith(".n5") || lower.endsWith(".zarr"));
138+
}
139+
140+
/**
141+
* Fallback UI for when {@link SpimDataUtils#resolvePathToSource(String)}
142+
* cannot auto-discover a dataset in an N5/Zarr container on its own (e.g.
143+
* an ambiguous or unusually structured container). Lets the user pick a
144+
* dataset interactively.
145+
* <p>
146+
* Non-blocking: {@code DatasetSelectorDialog} shows a plain (non-modal)
147+
* {@code JFrame}, so this method returns immediately, before the user has
148+
* interacted with it. The resolved source is only added to {@code viewer}
149+
* later, from the dialog's OK callback, once the user makes a selection.
150+
*
151+
* @param n5ZarrDir the {@code .n5} or {@code .zarr} directory
152+
* @param viewer the already-created {@link Bvv} or {@link Bdv} to add
153+
* the user's eventual selection to
154+
*/
155+
private void datasetDialog(final String n5ZarrDir, final AbstractBigViewer viewer) {
156+
final ExecutorService exec = Executors.newFixedThreadPool(SNTPrefs.getThreads());
157+
final DatasetSelectorDialog datasetDialog = getDatasetSelectorDialog(n5ZarrDir, exec);
158+
SwingUtilities.invokeLater(() -> {
159+
datasetDialog.run(selection -> {
160+
try {
161+
final SpimDataUtils.N5Sources n5Sources =
162+
SpimDataUtils.resolveN5Selection(selection, new File(n5ZarrDir).getName());
163+
if (viewer instanceof Bvv bvv) bvv.show(n5Sources);
164+
else if (viewer instanceof Bdv bdv) bdv.show(n5Sources);
165+
} catch (final Exception e) {
166+
GuiUtils.errorPrompt("Could not open '" + n5ZarrDir + "': " + e.getMessage());
167+
} finally {
168+
exec.shutdown();
169+
}
170+
});
171+
datasetDialog.openContainer(n5ZarrDir); // run() calls buildDialog() synchronously before returning
172+
});
173+
}
174+
175+
private static DatasetSelectorDialog getDatasetSelectorDialog(final String n5ZarrDir, final ExecutorService exec) {
176+
final DatasetSelectorDialog datasetDialog = new DatasetSelectorDialog(
177+
new N5Importer.N5ViewerReaderFun(), new N5Importer.N5BasePathFun(), n5ZarrDir,
178+
n5vGroupParsers, n5vParsers);
179+
datasetDialog.setLoaderExecutor(exec);
180+
datasetDialog.setTreeRenderer(new N5ViewerTreeCellRenderer(false));
181+
datasetDialog.setContainerPathUpdateCallback(path -> {}); // required; NPEs otherwise
182+
return datasetDialog;
183+
}
184+
119185
/** Resolves sources, enforces GPU texture limits, then opens BVV. */
120186
private void runBvv(final String[] filePaths) {
121187
final int maxTexSize = queryMaxTexture3DSize();
122188
SNTUtils.log("BVV: GL_MAX_3D_TEXTURE_SIZE = " + maxTexSize);
123189
final List<Object> sources = new ArrayList<>();
190+
final List<String> deferredPaths = new ArrayList<>(); // need the interactive dialog
124191
for (final String path : filePaths) {
125-
final Object source = SpimDataUtils.resolvePathToSource(path);
192+
final Object source;
193+
try {
194+
source = SpimDataUtils.resolvePathToSource(path);
195+
} catch (final IllegalArgumentException e) {
196+
if (isN5OrZarrDir(path)) {
197+
SNTUtils.log("BVV: headless N5/Zarr discovery failed for '" + path + "' (" + e.getMessage()
198+
+ "); will prompt for dataset selection");
199+
deferredPaths.add(path);
200+
continue;
201+
}
202+
throw e;
203+
}
126204
if (source instanceof ImgPlus<?> img && ImgUtils.exceedsDimension(img, maxTexSize)) {
127205
SNTUtils.setIsLoading(false);
128206
final Object handled = handleOversizedImage(img, maxTexSize, path);
@@ -143,15 +221,29 @@ private void runBvv(final String[] filePaths) {
143221
bvv.show((ImgPlus) img);
144222
}
145223
}
224+
for (final String path : deferredPaths)
225+
datasetDialog(path, bvv);
146226
loadReconstructions(bvv);
147227
loadMarkers(bvv);
148228
}
149229

150230
/** Resolves sources (no texture-size constraint) then opens BDV. */
151231
private void runBdv(final String[] filePaths) {
152232
final Bdv bdv = new Bdv();
233+
final List<String> deferredPaths = new ArrayList<>(); // need the interactive dialog
153234
for (final String path : filePaths) {
154-
final Object source = SpimDataUtils.resolvePathToSource(path);
235+
final Object source;
236+
try {
237+
source = SpimDataUtils.resolvePathToSource(path);
238+
} catch (final IllegalArgumentException e) {
239+
if (isN5OrZarrDir(path)) {
240+
SNTUtils.log("BDV: headless N5/Zarr discovery failed for '" + path + "' (" + e.getMessage()
241+
+ "); will prompt for dataset selection");
242+
deferredPaths.add(path);
243+
continue;
244+
}
245+
throw e;
246+
}
155247
if (source instanceof AbstractSpimData<?> spim) {
156248
bdv.show(spim, path); // path-aware overload populates spimDataFilePaths
157249
} else if (source instanceof SpimDataUtils.N5Sources n5) {
@@ -161,6 +253,8 @@ private void runBdv(final String[] filePaths) {
161253
bdv.show((ImgPlus) img);
162254
}
163255
}
256+
for (final String path : deferredPaths)
257+
datasetDialog(path, bdv);
164258
loadReconstructions(bdv);
165259
loadMarkers(bdv);
166260
}

‎src/main/java/sc/fiji/snt/io/SpimDataUtils.java‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,28 @@ private static N5Sources resolveN5ToSources(final File dir) {
298298
}
299299
}
300300

301+
/**
302+
* Builds {@link N5Sources} from a user-made {@link DataSelection} (e.g.
303+
* from an interactive {@code DatasetSelectorDialog}), reusing the same
304+
* source-building logic as the automatic discovery in
305+
* {@link #resolvePathToSource(String)}. Intended as a fallback UI for
306+
* containers whose structure {@link #resolvePathToSource(String)} could
307+
* not resolve on its own.
308+
*
309+
* @param selection the user's dataset selection
310+
* @param name display name for the resulting {@link N5Sources}
311+
* @return the resolved sources
312+
* @throws IllegalArgumentException if no displayable sources are found
313+
*/
314+
public static N5Sources resolveN5Selection(final DataSelection selection, final String name) {
315+
try {
316+
final List<N5Metadata> selected = N5Viewer.unwrapMultichannelSelections(selection);
317+
return buildN5Sources(selection.n5, selected, name);
318+
} catch (final IOException e) {
319+
throw new IllegalArgumentException("Could not open N5/Zarr selection: " + e.getMessage(), e);
320+
}
321+
}
322+
301323
/**
302324
* Recursively collects the metadata of every node in the tree that has any, without descending into a matched
303325
* node's own children (e.g. resolution-level subfolders inside a multiscale group, which are not separate datasets)

0 commit comments

Comments
 (0)