2828import com .jogamp .opengl .GLProfile ;
2929import net .imagej .ImgPlus ;
3030import 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 ;
3135import org .scijava .command .Command ;
3236import org .scijava .command .ContextCommand ;
3337import org .scijava .plugin .Parameter ;
3438import org .scijava .plugin .Plugin ;
39+ import org .scijava .widget .FileWidget ;
40+ import sc .fiji .snt .SNTPrefs ;
3541import sc .fiji .snt .SNTUtils ;
3642import sc .fiji .snt .gui .GuiUtils ;
3743import sc .fiji .snt .gui .ScriptInstaller ;
4147import sc .fiji .snt .viewer .Bdv ;
4248import sc .fiji .snt .viewer .Bvv ;
4349
50+ import javax .swing .*;
4451import java .io .File ;
4552import java .util .ArrayList ;
4653import java .util .List ;
4754import java .util .Objects ;
55+ import java .util .concurrent .ExecutorService ;
56+ import java .util .concurrent .Executors ;
4857import 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 }
0 commit comments