5858import java .io .File ;
5959import java .util .ArrayList ;
6060import java .util .Collection ;
61+ import java .util .LinkedHashMap ;
6162import java .util .List ;
63+ import java .util .Map ;
6264import java .util .Objects ;
6365import java .util .concurrent .ExecutorService ;
6466import java .util .concurrent .Executors ;
@@ -83,6 +85,7 @@ and big data formats with lazy loading (N5, Zarr, HDF5, OME-TIFF,
8385 the entire file into memory.""" ;
8486
8587 private static final int GL_MAX_3D_TEXTURE_SIZE = 0x8073 ; // OpenGL constant
88+ private static final int REACHABILITY_TIMEOUT_MS = 4000 ;
8689 private static final String ABORT = "Abort" ;
8790 private static final String DOWNSAMPLE = "Downsample to fit" ;
8891 private static final String CONVERT = "Show me how to convert to multi-resolution pyramid image" ;
@@ -147,7 +150,7 @@ private void loadDemo() {
147150 // fine to declare URL as file even though File collapses "//" -> "/": restoreUrlScheme() already handles that
148151 img1File = new File ("https://ome-zarr-scivis.s3.us-east-1.amazonaws.com/v0.4/96x0/marmoset_neurons.ome.zarr" );
149152 img2File = null ;
150- recFiles = new File ("https://raw.githubusercontent.com/morphonets/misc/680ac2a9b2cb1dfe85c0b64f17fed816e3da1647/dataset-demos/marmoset_neurons/autotracings.traces" );;
153+ recFiles = new File ("https://raw.githubusercontent.com/morphonets/misc/680ac2a9b2cb1dfe85c0b64f17fed816e3da1647/dataset-demos/marmoset_neurons/autotracings.traces" );
151154 markerFile = new File ("https://raw.githubusercontent.com/morphonets/misc/680ac2a9b2cb1dfe85c0b64f17fed816e3da1647/dataset-demos/marmoset_neurons/soma_detections.csv" );
152155 viewerType = "Big Data Viewer (BDV): Interactive reslicing" ;
153156 tracingEnabled = true ;
@@ -198,7 +201,6 @@ private void putOrRemove(final String key, final File file) {
198201
199202 @ Override
200203 public void run () {
201- SNTUtils .setDebugMode (true );
202204 if (img1File == null ) {
203205 error ("Main volume is required." );
204206 return ;
@@ -212,6 +214,7 @@ public void run() {
212214 error ("No volume files specified." );
213215 return ;
214216 }
217+ if (!checkReachable (filePaths )) return ; // single friendly dialog if any remote field is unreachable
215218 final boolean threeD = viewerType != null && viewerType .toLowerCase ().contains ("bvv" );
216219 final boolean tracer = tracingEnabled ;
217220
@@ -220,20 +223,30 @@ public void run() {
220223 return ;
221224 }
222225
226+ AbstractBigViewer viewer = null ;
223227 try {
224228 SNTUtils .setIsLoading (true );
225229 if (tracer && threeD )
226- runBvvWithTracing (filePaths );
230+ viewer = runBvvWithTracing (filePaths );
227231 else if (tracer )
228- runBdvWithTracing (filePaths );
232+ viewer = runBdvWithTracing (filePaths );
229233 else if (threeD )
230- runBvv (filePaths );
234+ viewer = runBvv (filePaths );
231235 else
232- runBdv (filePaths );
233- SNTUtils .setIsLoading (false );
236+ viewer = runBdv (filePaths );
234237 } catch (final Exception e ) {
235238 SNTUtils .setIsLoading (false ); // hide splashscreen behind error dialog
236- error ("An error occurred: " + e .getMessage ());
239+ error ("An error occurred: " + GuiUtils .friendlyErrorMessage (e ));
240+ } finally {
241+ SNTUtils .setIsLoading (false );
242+ if (viewer != null && viewer .getViewerFrame () != null && BoundingBox .UNSET_SPACING_UNIT .equals (viewer .getPhysicalUnit ())) {
243+ AbstractBigViewer finalViewer = viewer ;
244+ SwingUtilities .invokeLater (() -> {
245+ new GuiUtils (finalViewer .getViewerFrame ()).warning (
246+ "<HTML>Spatial calibration values appear to be invalid.<br>" +
247+ "To adjust them, right-click the scale bar button in <i>Scene Controls</i>." );
248+ });
249+ }
237250 }
238251 }
239252
@@ -252,6 +265,29 @@ private static String toPathString(final File f) {
252265 : f .getAbsolutePath ();
253266 }
254267
268+ /**
269+ * @return true if every remote field is reachable (or none are remote); false if the run was
270+ * aborted because at least one was not
271+ */
272+ private boolean checkReachable (final String [] filePaths ) {
273+ final LinkedHashMap <String , String > byLabel = new LinkedHashMap <>();
274+ if (filePaths .length > 0 ) byLabel .put ("Main volume" , filePaths [0 ]);
275+ if (filePaths .length > 1 ) byLabel .put ("Secondary volume" , filePaths [1 ]);
276+ if (recFiles != null ) byLabel .put ("Reconstruction(s)" , toPathString (recFiles ));
277+ if (markerFile != null ) byLabel .put ("Markers" , toPathString (markerFile ));
278+ final List <String > unreachable = new ArrayList <>();
279+ for (final Map .Entry <String , String > entry : byLabel .entrySet ()) {
280+ final String path = entry .getValue ();
281+ if (SpimDataUtils .isRemoteUrl (path ) && !SNTUtils .isReachable (path , REACHABILITY_TIMEOUT_MS ))
282+ unreachable .add (entry .getKey () + ": " + path );
283+ }
284+ if (unreachable .isEmpty ()) return true ;
285+ error ("<HTML>No internet access. Could not reach:<br> "
286+ + String .join ("<br> " , unreachable )
287+ + "<br>Please check your connection and try again." );
288+ return false ;
289+ }
290+
255291 /** True if path is an existing .n5/.zarr directory, or a remote URL to one. */
256292 private static boolean isN5OrZarrDir (final String path ) {
257293 if (SpimDataUtils .isRemoteUrl (path )) {
@@ -311,15 +347,16 @@ private static DatasetSelectorDialog getDatasetSelectorDialog(final String n5Zar
311347 }
312348
313349 /** Resolves sources, enforces GPU texture limits, then opens BVV. */
314- private void runBvv (final String [] filePaths ) {
350+ private AbstractBigViewer runBvv (final String [] filePaths ) {
315351 final int maxTexSize = queryMaxTexture3DSize ();
316352 SNTUtils .log ("BVV: GL_MAX_3D_TEXTURE_SIZE = " + maxTexSize );
317353 final ResolvedSources resolved = resolveBvvSources (filePaths , maxTexSize );
318- if (resolved == null ) return ; // user chose Abort (oversized image or non-pyramidal dataset)
354+ if (resolved == null ) return null ; // user chose Abort (oversized image or non-pyramidal dataset)
319355 final Bvv bvv = new Bvv ();
320356 addSourcesToBvv (bvv , resolved );
321357 loadReconstructions (bvv );
322358 loadMarkers (bvv );
359+ return bvv ;
323360 }
324361
325362 /**
@@ -333,7 +370,7 @@ private void runBvv(final String[] filePaths) {
333370 * manual tracing still works (segment tracing only needs spacing, which defaults to 1 regardless
334371 * of a loaded image), but A* has nothing real to search until an image is loaded via the SNTUI.
335372 */
336- private void runBvvWithTracing (final String [] filePaths ) {
373+ private AbstractBigViewer runBvvWithTracing (final String [] filePaths ) {
337374 final int maxTexSize = queryMaxTexture3DSize ();
338375 SNTUtils .log ("BVV: GL_MAX_3D_TEXTURE_SIZE = " + maxTexSize );
339376 // Resolve the primary volume (img1File, i.e. filePaths[0]) exactly once: startTracingSNT() needs
@@ -342,11 +379,12 @@ private void runBvvWithTracing(final String[] filePaths) {
342379 // discovery a second time for an unchanged path
343380 final TracingSetup setup = startTracingSNT (img1File );
344381 final ResolvedSources resolved = resolveBvvSources (filePaths , maxTexSize , setup .primarySource ());
345- if (resolved == null ) return ; // user chose Abort (oversized image or non-pyramidal dataset)
382+ if (resolved == null ) return null ; // user chose Abort (oversized image or non-pyramidal dataset)
346383 final Bvv bvv = new Bvv (setup .snt ());
347384 addSourcesToBvv (bvv , resolved );
348385 loadReconstructions (bvv );
349386 loadMarkers (bvv );
387+ return bvv ;
350388 }
351389
352390 /** Holds the outcome of {@link #resolveBvvSources(String[], int, Object)}. */
@@ -591,7 +629,7 @@ private static void applyWorldOriginOffset(final SNT snt, final bdv.viewer.Sourc
591629 }
592630
593631 /** Resolves sources (no texture-size constraint) then opens BDV. */
594- private void runBdv (final String [] filePaths ) {
632+ private AbstractBigViewer runBdv (final String [] filePaths ) {
595633 final Bdv bdv = new Bdv ();
596634 final List <String > deferredPaths = new ArrayList <>(); // need the interactive dialog
597635 for (final String path : filePaths ) {
@@ -620,10 +658,11 @@ private void runBdv(final String[] filePaths) {
620658 datasetDialog (path , bdv );
621659 loadReconstructions (bdv );
622660 loadMarkers (bdv );
661+ return bdv ;
623662 }
624663
625664 /** BDV counterpart to runBvvWithTracing(final String[] filePaths); */
626- private void runBdvWithTracing (final String [] filePaths ) {
665+ private AbstractBigViewer runBdvWithTracing (final String [] filePaths ) {
627666 final TracingSetup setup = startTracingSNT (img1File );
628667 final Bdv bdv = new Bdv (setup .snt ());
629668 final List <String > deferredPaths = new ArrayList <>(); // need the interactive dialog
@@ -657,6 +696,7 @@ private void runBdvWithTracing(final String[] filePaths) {
657696 datasetDialog (path , bdv );
658697 loadReconstructions (bdv );
659698 loadMarkers (bdv );
699+ return bdv ;
660700 }
661701
662702 private void loadMarkers (final AbstractBigViewer viewer ) {
@@ -698,11 +738,11 @@ private void loadReconstructions(final AbstractBigViewer viewer) {
698738 if (SpimDataUtils .isRemoteUrl (path )) {
699739 // fileAvailable()/getReconstructionFiles() below are local-filesystem-only checks. Tree.listFromFile()
700740 // already knows how to handle a remote URL directly (a single reconstruction file is streamed, a .zip is
701- // downloaded/extracted and treated as a irectory ), so route straight through it instead for this case
741+ // downloaded/extracted and treated as a directory ), so route straight through it instead for this case
702742 try {
703743 trees .addAll (Tree .listFromFile (path ));
704744 } catch (final IllegalArgumentException e ) {
705- error ("Could not load reconstructions from " + path + ": " + e . getMessage ( ));
745+ error ("Could not load reconstructions from " + path + ": " + GuiUtils . friendlyErrorMessage ( e ));
706746 return ;
707747 }
708748 if (trees .isEmpty ()) {
0 commit comments