Skip to content

Commit 08dbbb5

Browse files
committed
BigDataLoaderCmd: Improve network detection and misc improvements
1 parent 875f674 commit 08dbbb5

10 files changed

Lines changed: 144 additions & 79 deletions

File tree

‎src/main/java/sc/fiji/snt/BookmarkManager.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,7 +1362,7 @@ private void loadBookmarksFromFile(final File file) {
13621362
try {
13631363
model.populateFromFile(file);
13641364
} catch (final Exception ex) {
1365-
guiUtils.error(ex.getMessage() + ".");
1365+
guiUtils.error(GuiUtils.friendlyErrorMessage(ex) + ".");
13661366
SNTUtils.error("loadBookmarksFromFile() failure", ex);
13671367
}
13681368
}
@@ -1371,7 +1371,7 @@ private void loadBookmarksFromFile(final String filePathOrURL) {
13711371
try {
13721372
model.populateFromFile(filePathOrURL);
13731373
} catch (final Exception ex) {
1374-
guiUtils.error(ex.getMessage() + ".");
1374+
guiUtils.error(GuiUtils.friendlyErrorMessage(ex) + ".");
13751375
SNTUtils.error("loadBookmarksFromFile() failure", ex);
13761376
}
13771377
}

‎src/main/java/sc/fiji/snt/SNTUtils.java‎

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
import org.scijava.ui.swing.script.LanguageSupportService;
5252
import org.scijava.util.FileUtils;
5353
import org.scijava.util.VersionUtils;
54-
import sc.fiji.snt.analysis.sholl.ShollUtils;
5554
import sc.fiji.snt.gui.GuiUtils;
5655
import sc.fiji.snt.gui.cmds.BigDataLoaderCmd;
5756
import sc.fiji.snt.util.BoundingBox;
@@ -60,6 +59,8 @@
6059
import java.awt.*;
6160
import java.io.*;
6261
import java.lang.reflect.InvocationTargetException;
62+
import java.net.InetSocketAddress;
63+
import java.net.Socket;
6364
import java.net.URI;
6465
import java.net.URISyntaxException;
6566
import java.net.URL;
@@ -74,8 +75,6 @@
7475
import java.util.concurrent.Future;
7576
import java.util.concurrent.TimeUnit;
7677
import java.util.concurrent.TimeoutException;
77-
import java.util.jar.Attributes;
78-
import java.util.jar.Manifest;
7978
import java.util.regex.Matcher;
8079
import java.util.regex.Pattern;
8180

@@ -212,7 +211,7 @@ private static String stringForCSV(final String s) {
212211
if (s.indexOf(',') >= 0 || s.indexOf(' ') >= 0) quote = true;
213212
if (s.indexOf('"') >= 0) {
214213
quote = true;
215-
result = s.replaceAll("\"", "\"\"");
214+
result = s.replace("\"", "\"\"");
216215
}
217216
if (quote) return "\"" + result + "\"";
218217
else return result;
@@ -340,6 +339,35 @@ protected static boolean isValidURL(final String url) {
340339
}
341340
}
342341

342+
/**
343+
* Checks for whether {@code url}'s host can be reached, meant to be called before a real download/stream attempt
344+
* (e.g., {@link #downloadToTempFile}) so that a missing network connection surfaces as one clear message.
345+
* est-effort: only http(s) URLs are actually probed; any other scheme (e.g. a bare host-less URI) is assumed
346+
* reachable, deferring to the real caller
347+
*
348+
* @param url the URL to check
349+
* @param timeoutMs connect timeout, in milliseconds
350+
* @return true if the host could be reached (or {@code url} is not a plain http(s) URL); false if
351+
* a connection could not be established within {@code timeoutMs}
352+
*/
353+
public static boolean isReachable(final String url, final int timeoutMs) {
354+
try {
355+
final URI uri = new URI(url);
356+
final String scheme = uri.getScheme();
357+
if (scheme == null || !(scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")))
358+
return true; // not a plain HTTP(S) URL (e.g. s3://, gs://); nothing simple to probe here
359+
final String host = uri.getHost();
360+
if (host == null) return true; // no host to probe; let the real caller deal with it
361+
final int port = uri.getPort() != -1 ? uri.getPort() : ("https".equalsIgnoreCase(scheme) ? 443 : 80);
362+
try (final Socket socket = new Socket()) {
363+
socket.connect(new InetSocketAddress(host, port), timeoutMs);
364+
return true;
365+
}
366+
} catch (final Exception e) {
367+
return false;
368+
}
369+
}
370+
343371
public static String formatDouble(final double value, final int digits) {
344372
return (Double.isNaN(value)) ? "NaN" : getDecimalFormat(value, digits).format(value);
345373
}
@@ -465,29 +493,6 @@ else if (time < 90000)
465493
- TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(time)));
466494
}
467495

468-
/**
469-
* Retrieves Sholl Analysis implementation date
470-
*
471-
* @return the implementation date or an empty strong if date could not be
472-
* retrieved.
473-
*/
474-
public static String buildDate() {
475-
String BUILD_DATE;
476-
final Class<ShollUtils> clazz = ShollUtils.class;
477-
final String className = clazz.getSimpleName() + ".class";
478-
try {
479-
final String classPath = clazz.getResource(className).toString();
480-
final String manifestPath = classPath.substring(0, classPath.lastIndexOf("!") + 1) + "/META-INF/MANIFEST.MF";
481-
final Manifest manifest = new Manifest(new URI(manifestPath).toURL().openStream());
482-
final Attributes attr = manifest.getMainAttributes();
483-
BUILD_DATE = attr.getValue("Implementation-Date");
484-
BUILD_DATE = BUILD_DATE.substring(0, BUILD_DATE.lastIndexOf("T"));
485-
} catch (final Exception ignored) {
486-
BUILD_DATE = "";
487-
}
488-
return BUILD_DATE;
489-
}
490-
491496
/**
492497
* Retrieves a list of reconstruction files stored in a common directory
493498
* matching the specified criteria.

‎src/main/java/sc/fiji/snt/gui/GuiUtils.java‎

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ public void error(final String msg, final String title) {
143143
centeredDialog(msg, title, JOptionPane.ERROR_MESSAGE);
144144
}
145145

146+
public void warning(final String msg) {
147+
centeredDialog(msg, "SNT v" + SNTUtils.VERSION, JOptionPane.WARNING_MESSAGE);
148+
}
149+
146150
/**
147151
* Returns the Last-Modified timestamp from a HEAD request, or -1 on failure.
148152
*/
@@ -2508,6 +2512,26 @@ public static void errorPrompt(final String msg, final boolean centeredOnScreen)
25082512
guiUtils.error(msg, "SNT v" + SNTUtils.VERSION);
25092513
}
25102514

2515+
/**
2516+
* Rewrites a common network-connectivity exception (unknown host, refused/timed-out connection)
2517+
* into a short, user-facing message, instead of the exception's own message, which for e.g.
2518+
* {@link java.net.UnknownHostException} is just the bare, unexplained hostname. Falls back to {@code
2519+
* t.getMessage()} for anything else.
2520+
*
2521+
* @param t the exception to inspect (its cause chain is also walked)
2522+
* @return a friendly message describing {@code t}
2523+
*/
2524+
public static String friendlyErrorMessage(final Throwable t) {
2525+
for (Throwable cause = t; cause != null; cause = cause.getCause()) {
2526+
if (cause instanceof java.net.UnknownHostException)
2527+
return "Could not reach '" + cause.getMessage() + "' - check your internet connection";
2528+
if (cause instanceof java.net.ConnectException || cause instanceof java.net.SocketTimeoutException)
2529+
return "Could not connect to the remote server - check your internet connection";
2530+
}
2531+
assert t != null;
2532+
return t.getMessage();
2533+
}
2534+
25112535
public static String[] availableLookAndFeels() {
25122536
return new String[] { LAF_LIGHT, LAF_LIGHT_INTJ, LAF_DARK, LAF_DARCULA };
25132537
}
@@ -3847,19 +3871,18 @@ public static JMenuItem openHelpURL(final String label, final String URL) {
38473871
}
38483872

38493873
public static JMenuItem showHelpOnCountingSpines(final boolean streamMode, final Action postDialogAction) {
3850-
38513874
final JMenuItem mi = new JMenuItem("Spine/Varicosity Analysis Help", IconFactory.menuIcon(GLYPH.QUESTION));
38523875
mi.addActionListener(e -> {
38533876
if (postDialogAction == null) {
38543877
GuiUtils.showHTMLDialog(spineHelpMsg(streamMode), "Annotating Spines/Varicosities");
3855-
} else if (new GuiUtils().yesNoHTMLDialog(spineHelpMsg(streamMode), "Annotating Spines/Varicosities",
3856-
"Pause SNT & Start Manual Annotation", "Dismiss")) {
3878+
} else {
38573879
String actionName = (String) postDialogAction.getValue(Action.NAME);
3858-
if (actionName == null) {
3859-
actionName = "Proceed";
3880+
if (actionName == null) actionName = "Proceed";
3881+
if (new GuiUtils().yesNoHTMLDialog(spineHelpMsg(streamMode), "Annotating Spines/Varicosities",
3882+
actionName, "Dismiss")) {
3883+
final ActionEvent event = new ActionEvent(GuiUtils.class, ActionEvent.ACTION_PERFORMED, actionName);
3884+
postDialogAction.actionPerformed(event);
38603885
}
3861-
final ActionEvent event = new ActionEvent(GuiUtils.class, ActionEvent.ACTION_PERFORMED, actionName);
3862-
postDialogAction.actionPerformed(event);
38633886
}
38643887
});
38653888
return mi;

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

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@
5858
import java.io.File;
5959
import java.util.ArrayList;
6060
import java.util.Collection;
61+
import java.util.LinkedHashMap;
6162
import java.util.List;
63+
import java.util.Map;
6264
import java.util.Objects;
6365
import java.util.concurrent.ExecutorService;
6466
import 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>&nbsp;&nbsp;"
286+
+ String.join("<br>&nbsp;&nbsp;", 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()) {

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,7 @@ private void tracesFileChanged() {
236236
public void run() {
237237

238238
SNTUtils.setIsLoading(true);
239-
final boolean noImg = IMAGE_NONE.equals(imageChoice) || (IMAGE_FILE.equals(
240-
imageChoice) && imageFile == null);
239+
final boolean noImg = IMAGE_NONE.equals(imageChoice) || (IMAGE_FILE.equals(imageChoice) && imageFile == null);
241240

242241
if (noImg) {
243242
final PathAndFillManager pathAndFillManager = new PathAndFillManager();

0 commit comments

Comments
 (0)