/* -*- mode: java; c-basic-offset: 8; indent-tabs-mode: t; tab-width: 8 -*- */ /*- * #%L * Fiji distribution of ImageJ for the life sciences. * %% * Copyright (C) 2010 - 2020 Fiji developers. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-3.0.html>. * #L% */ package tracing; import java.awt.Color; import java.awt.Component; import java.awt.event.KeyListener; import java.io.File; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.scijava.vecmath.Color3f; import org.scijava.vecmath.Point3d; import org.scijava.vecmath.Point3f; import amira.AmiraMeshDecoder; import amira.AmiraParameters; import client.ArchiveClient; import features.ComputeCurvatures; import features.GaussianGenerationCallback; import features.SigmaPalette; import features.TubenessProcessor; import ij.IJ; import ij.ImagePlus; import ij.ImageStack; import ij.Prefs; import ij.gui.ImageRoi; import ij.gui.Overlay; import ij.gui.Roi; import ij.gui.StackWindow; import ij.gui.YesNoCancelDialog; import ij.io.FileInfo; import ij.io.OpenDialog; import ij.plugin.ZProjector; import ij.process.ByteProcessor; import ij.text.TextWindow; import ij3d.Content; import ij3d.Image3DUniverse; import stacks.ThreePanes; /* Note on terminology: "traces" files are made up of "paths". Paths are non-branching sequences of adjacent points (including diagonals) in the image. Branches and joins are supported by attributes of paths that specify that they begin on (or end on) other paths. */ public class SimpleNeuriteTracer extends ThreePanes implements SearchProgressCallback, GaussianGenerationCallback, PathAndFillListener { protected static boolean verbose = false; protected static final int DISPLAY_PATHS_SURFACE = 1; protected static final int DISPLAY_PATHS_LINES = 2; protected static final int DISPLAY_PATHS_LINES_AND_DISCS = 3; protected static final int MIN_SNAP_CURSOR_WINDOW_XY = 2; protected static final int MIN_SNAP_CURSOR_WINDOW_Z = 0; protected static final int MAX_SNAP_CURSOR_WINDOW_XY = 20; protected static final int MAX_SNAP_CURSOR_WINDOW_Z = 8; protected static final String startBallName = "Start point"; protected static final String targetBallName = "Target point"; protected static final int ballRadiusMultiplier = 5; protected PathAndFillManager pathAndFillManager; protected SNTPrefs prefs; protected boolean use3DViewer; protected Image3DUniverse univ; protected Content imageContent; protected boolean useCompressedXML; volatile protected boolean unsavedPaths = false; volatile protected boolean autoCanvasActivation; volatile protected boolean snapCursor; volatile protected int cursorSnapWindowXY; volatile protected int cursorSnapWindowZ; public boolean pathsUnsaved() { return unsavedPaths; } public PathAndFillManager getPathAndFillManager() { return pathAndFillManager; } public InteractiveTracerCanvas getXYCanvas() { return xy_tracer_canvas; } public String stripExtension(final String filename) { final int lastDot = filename.lastIndexOf("."); if (lastDot > 0) return filename.substring(0, lastDot); return null; } /* * Just for convenience, keep casted references to the superclass's * InteractiveTracerCanvas objects: */ protected InteractiveTracerCanvas xy_tracer_canvas; protected InteractiveTracerCanvas xz_tracer_canvas; protected InteractiveTracerCanvas zy_tracer_canvas; public ImagePlus getImagePlus() { return xy; } public double getLargestDimension() { return Math.max(x_spacing * width, Math.max(y_spacing * height, z_spacing * depth)); } public double getStackDiagonalLength() { return Math.sqrt((x_spacing * width) * (x_spacing * width) + (y_spacing * height) * (y_spacing * height) + (z_spacing * depth) * (z_spacing * depth)); } /* This overrides the method in ThreePanes... */ @Override public InteractiveTracerCanvas createCanvas(final ImagePlus imagePlus, final int plane) { return new InteractiveTracerCanvas(imagePlus, this, plane, pathAndFillManager); } public void cancelSearch(final boolean cancelFillToo) { if (currentSearchThread != null) currentSearchThread.requestStop(); if (tubularGeodesicsThread != null) tubularGeodesicsThread.requestStop(); endJoin = null; endJoinPoint = null; if (cancelFillToo && filler != null) filler.requestStop(); } @Override public void threadStatus(final SearchInterface source, final int status) { // Ignore this information. } public void changeUIState(final int newState) { resultsDialog.changeState(newState); } public int getUIState() { return resultsDialog.getCurrentState(); } synchronized public void saveFill() { if (filler != null) { // The filler must be paused while we save to // avoid concurrent modifications... if (verbose) SNT.log("[" + Thread.currentThread() + "] going to lock filler in plugin.saveFill"); synchronized (filler) { if (verbose) SNT.log("[" + Thread.currentThread() + "] acquired it"); if (SearchThread.PAUSED == filler.getThreadStatus()) { // Then we can go ahead and save: pathAndFillManager.addFill(filler.getFill()); // ... and then stop filling: filler.requestStop(); resultsDialog.changeState(NeuriteTracerResultsDialog.WAITING_TO_START_PATH); filler = null; } else { SNT.error("The filler must be paused before saving the fill."); } } if (verbose) SNT.log("[" + Thread.currentThread() + "] left lock on filler"); } } synchronized public void discardFill() { discardFill(true); } synchronized public void discardFill(final boolean updateState) { if (filler != null) { synchronized (filler) { filler.requestStop(); if (updateState) resultsDialog.changeState(NeuriteTracerResultsDialog.WAITING_TO_START_PATH); filler = null; } } } synchronized public void pauseOrRestartFilling() { if (filler != null) { filler.pauseOrUnpause(); } } protected List<SNTListener> listeners = Collections.synchronizedList(new ArrayList<SNTListener>()); public void addListener(final SNTListener listener) { listeners.add(listener); } public void notifyListeners(final SNTEvent event) { for (final SNTListener listener : listeners.toArray(new SNTListener[0])) { listener.onEvent(event); } } public boolean anyListeners() { return listeners.size() > 0; } /* * Now a couple of callback methods, which get information about the * progress of the search. */ @Override public void finished(final SearchInterface source, final boolean success) { /* * This is called by both filler and currentSearchThread, so distinguish * these cases: */ if (source == currentSearchThread || source == tubularGeodesicsThread) { removeSphere(targetBallName); if (success) { final Path result = source.getResult(); if (result == null) { SNT.error("Bug! Succeeded, but null result."); return; } if (endJoin != null) { result.setEndJoin(endJoin, endJoinPoint); } setTemporaryPath(result); resultsDialog.changeState(NeuriteTracerResultsDialog.QUERY_KEEP); } else { resultsDialog.changeState(NeuriteTracerResultsDialog.PARTIAL_PATH); } // Indicate in the dialog that we've finished... if (source == currentSearchThread) { currentSearchThread = null; } } removeThreadToDraw(source); repaintAllPanes(); } @Override public void pointsInSearch(final SearchInterface source, final int inOpen, final int inClosed) { // Just use this signal to repaint the canvas, in case there's // been no mouse movement. repaintAllPanes(); } /* * FIXME, just for synchronization - replace this with synchronization on * the object it protects: */ protected String nonsense = "unused"; /* * These member variables control what we're actually doing - whether that's * tracing, logging points or displaying values of the Hessian at particular * points. Currently we only support tracing, support for the others has * been removed. */ protected boolean setupLog = false; protected boolean setupEv = false; protected boolean setupTrace = false; protected boolean setupPreprocess = false; /* If we're timing out the searches (probably not any longer...) */ volatile protected boolean setupTimeout = false; volatile protected float setupTimeoutValue = 0.0f; /* * For the original file info - needed for loading the corresponding labels * file and checking if a "tubes.tif" file already exists: */ public FileInfo file_info; protected int width, height, depth; public void justDisplayNearSlices(final boolean value, final int eitherSide) { xy_tracer_canvas.just_near_slices = value; if (!single_pane) { xz_tracer_canvas.just_near_slices = value; zy_tracer_canvas.just_near_slices = value; } xy_tracer_canvas.eitherSide = eitherSide; if (!single_pane) { xz_tracer_canvas.eitherSide = eitherSide; zy_tracer_canvas.eitherSide = eitherSide; } repaintAllPanes(); } public void setCrosshair(final double new_x, final double new_y, final double new_z) { xy_tracer_canvas.setCrosshairs(new_x, new_y, new_z, true); if (!single_pane) { xz_tracer_canvas.setCrosshairs(new_x, new_y, new_z, true); zy_tracer_canvas.setCrosshairs(new_x, new_y, new_z, true); } } protected String[] materialList; byte[][] labelData; synchronized public void loadLabelsFile(final String path) { final AmiraMeshDecoder d = new AmiraMeshDecoder(); if (!d.open(path)) { SNT.error("Could not open the labels file '" + path + "'"); return; } final ImageStack stack = d.getStack(); final ImagePlus labels = new ImagePlus("Label file for Tracer", stack); if ((labels.getWidth() != width) || (labels.getHeight() != height) || (labels.getStackSize() != depth)) { SNT.error("The size of that labels file doesn't match the size of the image you're tracing."); return; } // We need to get the AmiraParameters object for that image... final AmiraParameters parameters = d.parameters; materialList = parameters.getMaterialList(); labelData = new byte[depth][]; for (int z = 0; z < depth; ++z) { labelData[z] = (byte[]) stack.getPixels(z + 1); } } synchronized public void loadLabels() { String fileName; String directory; if (file_info != null) { fileName = file_info.fileName; directory = file_info.directory; final File possibleLoadFile = new File(directory, fileName + ".labels"); final String path = possibleLoadFile.getPath(); if (possibleLoadFile.exists()) { final YesNoCancelDialog d = new YesNoCancelDialog(IJ.getInstance(), "Confirm", "Load the default labels file?\n(" + path + ")"); if (d.yesPressed()) { loadLabelsFile(path); return; } else if (d.cancelPressed()) { return; } } } // Presumably "No" was pressed... OpenDialog od; od = new OpenDialog("Select labels file...", null, null); fileName = od.getFileName(); directory = od.getDirectory(); if (fileName != null) { loadLabelsFile(directory + fileName); return; } } volatile boolean loading = false; synchronized public void loadTracings() { loading = true; String fileName = null; String directory = null; if (file_info != null) { fileName = file_info.fileName; directory = file_info.directory; File possibleLoadFile = null; final int dotIndex = fileName.lastIndexOf("."); if (dotIndex >= 0) { possibleLoadFile = new File(directory, fileName.substring(0, dotIndex) + ".traces"); final String path = possibleLoadFile.getPath(); if (possibleLoadFile.exists()) { final YesNoCancelDialog d = new YesNoCancelDialog(IJ.getInstance(), "Confirm", "Load the default traces file?\n(" + path + ")"); if (d.yesPressed()) { if (pathAndFillManager.loadGuessingType(path)) unsavedPaths = false; Prefs.set("tracing.Simple_Neurite_Tracer.lastTracesLoadDirectory", directory); Prefs.savePreferences(); loading = false; return; } else if (d.cancelPressed()) { loading = false; return; } } } } directory = Prefs.get("tracing.Simple_Neurite_Tracer.lastTracesLoadDirectory", null); if (directory == null && file_info != null && file_info.directory != null) directory = file_info.directory; // Presumably "No" was pressed... OpenDialog od; od = new OpenDialog("Select .traces or .(e)swc file...", directory, null); fileName = od.getFileName(); directory = od.getDirectory(); if (fileName != null) { final File chosenFile = new File(directory, fileName); if (!chosenFile.exists()) { SNT.error("The file '" + chosenFile.getAbsolutePath() + "' didn't exist"); loading = false; return; } Prefs.set("tracing.Simple_Neurite_Tracer.lastTracesLoadDirectory", directory); Prefs.savePreferences(); final int guessedType = PathAndFillManager.guessTracesFileType(chosenFile.getAbsolutePath()); switch (guessedType) { case PathAndFillManager.TRACES_FILE_TYPE_SWC: { final SWCImportOptionsDialog swcImportDialog = new SWCImportOptionsDialog( "SWC import options for " + chosenFile.getName()); // FIXME: pop up a dialog to ask about options: // .. and then call the full importSWC.=: if (swcImportDialog.succeeded && pathAndFillManager.importSWC(chosenFile.getAbsolutePath(), swcImportDialog.getIgnoreCalibration(), swcImportDialog.getXOffset(), swcImportDialog.getYOffset(), swcImportDialog.getZOffset(), swcImportDialog.getXScale(), swcImportDialog.getYScale(), swcImportDialog.getZScale(), swcImportDialog.getReplaceExistingPaths())) unsavedPaths = false; break; } case PathAndFillManager.TRACES_FILE_TYPE_COMPRESSED_XML: if (pathAndFillManager.loadCompressedXML(chosenFile.getAbsolutePath())) unsavedPaths = false; break; case PathAndFillManager.TRACES_FILE_TYPE_UNCOMPRESSED_XML: if (pathAndFillManager.loadUncompressedXML(chosenFile.getAbsolutePath())) unsavedPaths = false; break; default: SNT.error("The file '" + chosenFile.getAbsolutePath() + "' was of unknown type (" + guessedType + ")"); break; } } loading = false; } public void mouseMovedTo(final double x_in_pane, final double y_in_pane, final int in_plane, final boolean shift_key_down, final boolean join_modifier_down) { double x, y, z; final double[] pd = new double[3]; findPointInStackPrecise(x_in_pane, y_in_pane, in_plane, pd); x = pd[0]; y = pd[1]; z = pd[2]; if (join_modifier_down && pathAndFillManager.anySelected()) { final PointInImage pointInImage = pathAndFillManager.nearestJoinPointOnSelectedPaths(x, y, z); if (pointInImage != null) { x = pointInImage.x / x_spacing; y = pointInImage.y / y_spacing; z = pointInImage.z / z_spacing; } } final int ix = (int) Math.round(x); final int iy = (int) Math.round(y); final int iz = (int) Math.round(z); final double x_scaled = ix * x_spacing; final double y_scaled = iy * y_spacing; final double z_scaled = iz * z_spacing; if (shift_key_down) setSlicesAllPanes(ix, iy, iz); if ((xy_tracer_canvas != null) && ((xz_tracer_canvas != null) || single_pane) && ((zy_tracer_canvas != null) || single_pane)) { String statusMessage = "world: (" + x_scaled + "," + y_scaled + "," + z_scaled + ") image: (" + ix + "," + iy + "," + iz + ")"; setCrosshair(x, y, z); if (labelData != null) { final byte b = labelData[iz][iy * width + ix]; final int m = b & 0xFF; final String material = materialList[m]; statusMessage += ", material: " + material; } IJ.showStatus(statusMessage); repaintAllPanes(); // Or the crosshair isn't updated.... } if (filler != null) { synchronized (filler) { final float distance = filler.getDistanceAtPoint(ix, iy, iz); resultsDialog.showMouseThreshold(distance); } } } volatile boolean lastStartPointSet = false; int last_start_point_x; int last_start_point_y; int last_start_point_z; Path endJoin; PointInImage endJoinPoint; /* * If we've finished searching for a path, but the user hasn't confirmed * that they want to keep it yet, temporaryPath is non-null and holds the * Path we just searched out. */ // Any method that deals with these two fields should be synchronized. Path temporaryPath = null; Path currentPath = null; // When we set temporaryPath, we also want to update the display: synchronized public void setTemporaryPath(final Path path) { final Path oldTemporaryPath = this.temporaryPath; xy_tracer_canvas.setTemporaryPath(path); if (!single_pane) { zy_tracer_canvas.setTemporaryPath(path); xz_tracer_canvas.setTemporaryPath(path); } temporaryPath = path; if (temporaryPath != null) temporaryPath.setName("Temporary Path"); if (use3DViewer) { if (oldTemporaryPath != null) { oldTemporaryPath.removeFrom3DViewer(univ); } if (temporaryPath != null) temporaryPath.addTo3DViewer(univ, Color.BLUE, null); } } synchronized public void setCurrentPath(final Path path) { final Path oldCurrentPath = this.currentPath; xy_tracer_canvas.setCurrentPath(path); if (!single_pane) { zy_tracer_canvas.setCurrentPath(path); xz_tracer_canvas.setCurrentPath(path); } currentPath = path; if (currentPath != null) currentPath.setName("Current Path"); if (use3DViewer) { if (oldCurrentPath != null) { oldCurrentPath.removeFrom3DViewer(univ); } if (currentPath != null) currentPath.addTo3DViewer(univ, Color.RED, null); } } synchronized public Path getCurrentPath() { return currentPath; } /* * pathUnfinished indicates that we have started to create a path, but not * yet finished it (in the sense of moving on to a new path with a differen * starting point.) FIXME: this may be redundant - check that. */ volatile boolean pathUnfinished = false; public void setPathUnfinished(final boolean unfinished) { this.pathUnfinished = unfinished; xy_tracer_canvas.setPathUnfinished(unfinished); if (!single_pane) { zy_tracer_canvas.setPathUnfinished(unfinished); xz_tracer_canvas.setPathUnfinished(unfinished); } } void addThreadToDraw(final SearchInterface s) { xy_tracer_canvas.addSearchThread(s); if (!single_pane) { zy_tracer_canvas.addSearchThread(s); xz_tracer_canvas.addSearchThread(s); } } void removeThreadToDraw(final SearchInterface s) { xy_tracer_canvas.removeSearchThread(s); if (!single_pane) { zy_tracer_canvas.removeSearchThread(s); xz_tracer_canvas.removeSearchThread(s); } } int[] selectedPaths = null; /* * Create a new 8 bit ImagePlus of the same dimensions as this image, but * with values set to either 255 (if there's a point on a path there) or 0 */ synchronized public ImagePlus makePathVolume(final ArrayList<Path> paths) { final byte[][] snapshot_data = new byte[depth][]; for (int i = 0; i < depth; ++i) snapshot_data[i] = new byte[width * height]; pathAndFillManager.setPathPointsInVolume(paths, snapshot_data, width, height, depth); final ImageStack newStack = new ImageStack(width, height); for (int i = 0; i < depth; ++i) { final ByteProcessor thisSlice = new ByteProcessor(width, height); thisSlice.setPixels(snapshot_data[i]); newStack.addSlice(null, thisSlice); } final ImagePlus newImp = new ImagePlus(xy.getShortTitle() + " Rendered Paths", newStack); newImp.setCalibration(xy.getCalibration()); return newImp; } synchronized public ImagePlus makePathVolume() { return makePathVolume(pathAndFillManager.allPaths); } /* If non-null, holds a reference to the currently searching thread: */ TracerThread currentSearchThread; TubularGeodesicsTracer tubularGeodesicsThread = null; /* Start a search thread looking for the goal in the arguments: */ synchronized void testPathTo(final double world_x, final double world_y, final double world_z, final PointInImage joinPoint) { if (!lastStartPointSet) { IJ.showStatus("No initial start point has been set. Do that with a mouse click." + " (Or a Shift-" + (IJ.isMacintosh() ? "Alt" : "Control") + "-click if the start of the path should join another neurite."); return; } if (temporaryPath != null) { IJ.showStatus("There's already a temporary path; Press 'N' to cancel it or 'Y' to keep it."); return; } double real_x_end, real_y_end, real_z_end; int x_end, y_end, z_end; if (joinPoint == null) { real_x_end = world_x; real_y_end = world_y; real_z_end = world_z; } else { real_x_end = joinPoint.x; real_y_end = joinPoint.y; real_z_end = joinPoint.z; endJoin = joinPoint.onPath; endJoinPoint = joinPoint; } addSphere(targetBallName, real_x_end, real_y_end, real_z_end, Color.BLUE, x_spacing * ballRadiusMultiplier); x_end = (int) Math.round(real_x_end / x_spacing); y_end = (int) Math.round(real_y_end / y_spacing); z_end = (int) Math.round(real_z_end / z_spacing); if (tubularGeodesicsTracingEnabled) { // Then useful values are: // oofFile.getAbsolutePath() - the filename of the OOF file // last_start_point_[xyz] - image coordinates of the start point // [xyz]_end - image coordinates of the end point // [xyz]_spacing tubularGeodesicsThread = new TubularGeodesicsTracer(oofFile, last_start_point_x, last_start_point_y, last_start_point_z, x_end, y_end, z_end, x_spacing, y_spacing, z_spacing, spacing_units); addThreadToDraw(tubularGeodesicsThread); tubularGeodesicsThread.addProgressListener(this); tubularGeodesicsThread.start(); } else { currentSearchThread = new TracerThread(xy, stackMin, stackMax, 0, // timeout // in // seconds 1000, // reportEveryMilliseconds last_start_point_x, last_start_point_y, last_start_point_z, x_end, y_end, z_end, true, // reciprocal singleSlice, (hessianEnabled ? hessian : null), resultsDialog.getMultiplier(), tubeness, hessianEnabled); addThreadToDraw(currentSearchThread); currentSearchThread.setDrawingColors(Color.CYAN, null); currentSearchThread.setDrawingThreshold(-1); currentSearchThread.addProgressListener(this); currentSearchThread.start(); } repaintAllPanes(); } synchronized public void confirmTemporary() { if (temporaryPath == null) // Just ignore the request to confirm a path (there isn't one): return; currentPath.add(temporaryPath); final PointInImage last = currentPath.lastPoint(); last_start_point_x = (int) Math.round(last.x / x_spacing); last_start_point_y = (int) Math.round(last.y / y_spacing); last_start_point_z = (int) Math.round(last.z / z_spacing); if (currentPath.endJoins == null) { setTemporaryPath(null); resultsDialog.changeState(NeuriteTracerResultsDialog.PARTIAL_PATH); repaintAllPanes(); } else { setTemporaryPath(null); // Since joining onto another path for the end must finish the path: finishedPath(); } /* * This has the effect of removing the path from the 3D viewer and * adding it again: */ setCurrentPath(currentPath); } synchronized public void cancelTemporary() { if (!lastStartPointSet) { SNT.error("No initial start point has been set yet. Do that with a mouse click or\na Shift+" + (IJ.isMacintosh() ? "Alt" : "Control") + "-click if the start of the path should join another neurite."); return; } if (temporaryPath == null) { SNT.error("There's no temporary path to cancel!"); return; } removeSphere(targetBallName); if (temporaryPath.endJoins != null) { temporaryPath.unsetEndJoin(); } setTemporaryPath(null); endJoin = null; endJoinPoint = null; resultsDialog.changeState(NeuriteTracerResultsDialog.PARTIAL_PATH); repaintAllPanes(); } synchronized public void cancelPath() { // Is there an unconfirmed path? If so, warn people about it... if (temporaryPath != null) { SNT.error( " There is an unconfirmed path: You need to\nconfirm the last segment before canceling the path."); return; } if (currentPath != null) { if (currentPath.startJoins != null) currentPath.unsetStartJoin(); if (currentPath.endJoins != null) currentPath.unsetEndJoin(); } removeSphere(targetBallName); removeSphere(startBallName); setCurrentPath(null); setTemporaryPath(null); lastStartPointSet = false; setPathUnfinished(false); resultsDialog.changeState(NeuriteTracerResultsDialog.WAITING_TO_START_PATH); repaintAllPanes(); } synchronized public void finishedPath() { // Is there an unconfirmed path? If so, warn people about it... if (temporaryPath != null) { SNT.error( " There is an unconfirmed path: You need to\nconfirm the last segment before finishing the path."); return; } if (currentPath == null || justFirstPoint()) { SNT.error("You can't complete a path with only a start point in it."); return; } removeSphere(startBallName); removeSphere(targetBallName); lastStartPointSet = false; setPathUnfinished(false); final Path savedCurrentPath = currentPath; setCurrentPath(null); pathAndFillManager.addPath(savedCurrentPath, true); unsavedPaths = true; // ... and change the state of the UI resultsDialog.changeState(NeuriteTracerResultsDialog.WAITING_TO_START_PATH); repaintAllPanes(); } synchronized public void clickForTrace(final Point3d p, final boolean join) { final double x_unscaled = p.x / x_spacing; final double y_unscaled = p.y / y_spacing; final double z_unscaled = p.z / z_spacing; setSlicesAllPanes((int) x_unscaled, (int) y_unscaled, (int) z_unscaled); clickForTrace(p.x, p.y, p.z, join); } synchronized public void clickForTrace(final double world_x, final double world_y, final double world_z, final boolean join) { PointInImage joinPoint = null; if (join) { joinPoint = pathAndFillManager.nearestJoinPointOnSelectedPaths(world_x / x_spacing, world_y / y_spacing, world_z / z_spacing); } if (resultsDialog == null) return; // FIXME: in some of the states this doesn't make sense; check for them: if (currentSearchThread != null) return; if (temporaryPath != null) return; if (filler != null) { setFillThresholdFrom(world_x, world_y, world_z); return; } if (pathUnfinished) { /* * Then this is a succeeding point, and we should start a search. */ testPathTo(world_x, world_y, world_z, joinPoint); resultsDialog.changeState(NeuriteTracerResultsDialog.SEARCHING); } else { /* This is an initial point. */ startPath(world_x, world_y, world_z, joinPoint); resultsDialog.changeState(NeuriteTracerResultsDialog.PARTIAL_PATH); } } synchronized public void clickForTrace(final double x_in_pane_precise, final double y_in_pane_precise, final int plane, final boolean join) { final double[] p = new double[3]; findPointInStackPrecise(x_in_pane_precise, y_in_pane_precise, plane, p); final double world_x = p[0] * x_spacing; final double world_y = p[1] * y_spacing; final double world_z = p[2] * z_spacing; clickForTrace(world_x, world_y, world_z, join); } public void setFillThresholdFrom(final double world_x, final double world_y, final double world_z) { final float distance = filler.getDistanceAtPoint(world_x / x_spacing, world_y / y_spacing, world_z / z_spacing); setFillThreshold(distance); } public void setFillThreshold(final double distance) { if (distance > 0) { if (verbose) SNT.log("Setting new threshold of: " + distance); resultsDialog.thresholdChanged(distance); filler.setThreshold(distance); } } synchronized void startPath(final double world_x, final double world_y, final double world_z, final PointInImage joinPoint) { endJoin = null; endJoinPoint = null; if (lastStartPointSet) { IJ.showStatus("The start point has already been set; to finish a path press 'F'"); return; } setPathUnfinished(true); lastStartPointSet = true; final Path path = new Path(x_spacing, y_spacing, z_spacing, spacing_units); path.setName("New Path"); Color ballColor; double real_last_start_x, real_last_start_y, real_last_start_z; if (joinPoint == null) { real_last_start_x = world_x; real_last_start_y = world_y; real_last_start_z = world_z; ballColor = Color.BLUE; } else { real_last_start_x = joinPoint.x; real_last_start_y = joinPoint.y; real_last_start_z = joinPoint.z; path.setStartJoin(joinPoint.onPath, joinPoint); ballColor = Color.GREEN; } last_start_point_x = (int) Math.round(real_last_start_x / x_spacing); last_start_point_y = (int) Math.round(real_last_start_y / y_spacing); last_start_point_z = (int) Math.round(real_last_start_z / z_spacing); addSphere(startBallName, real_last_start_x, real_last_start_y, real_last_start_z, ballColor, x_spacing * ballRadiusMultiplier); setCurrentPath(path); } protected void addSphere(final String name, final double x, final double y, final double z, final Color color, final double radius) { if (use3DViewer) { final List<Point3f> sphere = customnode.MeshMaker.createSphere(x, y, z, radius); univ.addTriangleMesh(sphere, new Color3f(color), name); } } protected void removeSphere(final String name) { if (use3DViewer) univ.removeContent(name); } /* * Return true if we have just started a new path, but have not yet added * any connections to it, otherwise return false. */ public boolean justFirstPoint() { return pathUnfinished && (currentPath.size() == 0); } public static String getStackTrace() { final StringWriter sw = new StringWriter(); new Exception("Dummy Exception for Stack Trace").printStackTrace(new PrintWriter(sw)); return sw.toString(); } protected double x_spacing = 1; protected double y_spacing = 1; protected double z_spacing = 1; protected String spacing_units = ""; public void viewFillIn3D(final boolean asMask) { final ImagePlus imagePlus = filler.fillAsImagePlus(asMask); imagePlus.show(); } public void setPositionAllPanes(final int x, final int y, final int z) { xy.setSlice(z + 1); zy.setSlice(x); xz.setSlice(y); } protected int imageType = -1; protected byte[][] slices_data_b; protected short[][] slices_data_s; protected float[][] slices_data_f; protected NeuriteTracerResultsDialog resultsDialog; volatile protected boolean cancelled = false; protected TextWindow helpTextWindow; protected boolean singleSlice; protected ArchiveClient archiveClient; volatile protected float stackMax = Float.MIN_VALUE; volatile protected float stackMin = Float.MAX_VALUE; public int guessResamplingFactor() { if (width == 0 || height == 0 || depth == 0) throw new RuntimeException("Can't call guessResamplingFactor() before width, height and depth are set..."); /* * This is about right for me, but probably should be related to the * free memory somehow. However, those calls are so notoriously * unreliable on Java that it's probably not worth it. */ final long maxSamplePoints = 500 * 500 * 100; int level = 0; while (true) { final long samplePoints = (long) (width >> level) * (long) (height >> level) * (depth >> level); if (samplePoints < maxSamplePoints) return (1 << level); ++level; } } public boolean isReady() { if (resultsDialog == null) return false; return resultsDialog.isVisible(); } public void launchPaletteAround(final int x, final int y, final int z) { final int either_side = 40; int x_min = x - either_side; int x_max = x + either_side; int y_min = y - either_side; int y_max = y + either_side; int z_min = z - either_side; int z_max = z + either_side; final int originalWidth = xy.getWidth(); final int originalHeight = xy.getHeight(); final int originalDepth = xy.getStackSize(); if (x_min < 0) x_min = 0; if (y_min < 0) y_min = 0; if (z_min < 0) z_min = 0; if (x_max >= originalWidth) x_max = originalWidth - 1; if (y_max >= originalHeight) y_max = originalHeight - 1; if (z_max >= originalDepth) z_max = originalDepth - 1; final double[] sigmas = new double[9]; for (int i = 0; i < sigmas.length; ++i) { sigmas[i] = ((i + 1) * getMinimumSeparation()) / 2; } resultsDialog.changeState(NeuriteTracerResultsDialog.WAITING_FOR_SIGMA_CHOICE); final SigmaPalette sp = new SigmaPalette(); sp.setListener(resultsDialog); sp.makePalette(xy, x_min, x_max, y_min, y_max, z_min, z_max, new TubenessProcessor(true), sigmas, 256 / resultsDialog.getMultiplier(), 3, 3, z); } public void startFillerThread(final FillerThread filler) { this.filler = filler; filler.addProgressListener(this); filler.addProgressListener(resultsDialog.getFillWindow()); addThreadToDraw(filler); filler.start(); resultsDialog.changeState(NeuriteTracerResultsDialog.FILLING_PATHS); } // This should only be assigned to when synchronized on this object // (FIXME: check that that is true) FillerThread filler = null; synchronized public void startFillingPaths(final Set<Path> fromPaths) { // currentlyFilling = true; resultsDialog.getFillWindow().pauseOrRestartFilling.setText("Pause"); filler = new FillerThread(xy, stackMin, stackMax, false, // startPaused true, // reciprocal 0.03f, // Initial threshold to display 5000); // reportEveryMilliseconds addThreadToDraw(filler); filler.addProgressListener(this); filler.addProgressListener(resultsDialog.getFillWindow()); filler.setSourcePaths(fromPaths); resultsDialog.setFillListVisible(true); filler.start(); resultsDialog.changeState(NeuriteTracerResultsDialog.FILLING_PATHS); } public void setFillTransparent(final boolean transparent) { xy_tracer_canvas.setFillTransparent(transparent); if (!single_pane) { xz_tracer_canvas.setFillTransparent(transparent); zy_tracer_canvas.setFillTransparent(transparent); } } public double getMinimumSeparation() { return Math.min(Math.abs(x_spacing), Math.min(Math.abs(y_spacing), Math.abs(z_spacing))); } volatile boolean hessianEnabled = false; ComputeCurvatures hessian = null; /* * This variable just stores the sigma which the current 'hessian' * ComputeCurvatures was / is being calculated (or -1 if 'hessian' is null) * ... */ volatile double hessianSigma = -1; public void startHessian() { if (hessian == null) { resultsDialog.changeState(NeuriteTracerResultsDialog.CALCULATING_GAUSSIAN); hessianSigma = resultsDialog.getSigma(); hessian = new ComputeCurvatures(xy, hessianSigma, this, true); new Thread(hessian).start(); } else { final double newSigma = resultsDialog.getSigma(); if (newSigma != hessianSigma) { resultsDialog.changeState(NeuriteTracerResultsDialog.CALCULATING_GAUSSIAN); hessianSigma = newSigma; hessian = new ComputeCurvatures(xy, hessianSigma, this, true); new Thread(hessian).start(); } } } // Even better, we might have a "tubeness" file already there. // If this is non-null then we found the "tubeness" file // (called foo.tubes.tif) on startup and loaded it // successfully. float[][] tubeness; public boolean oofFileAvailable() { return oofFile != null; } /* * If there appears to be a local file called <image-basename>.oof.nrrd then * we assume that we can use the Tubular Geodesics tracing method. This * variable null if not such file was found. */ protected File oofFile = null; protected boolean tubularGeodesicsTracingEnabled = false; public synchronized void enableTubularGeodesicsTracing(final boolean enable) { tubularGeodesicsTracingEnabled = enable; } public synchronized void enableHessian(final boolean enable) { hessianEnabled = enable; if (enable) { startHessian(); resultsDialog.editSigma.setEnabled(false); resultsDialog.sigmaWizard.setEnabled(false); } else { resultsDialog.editSigma.setEnabled(true); resultsDialog.sigmaWizard.setEnabled(true); } } public synchronized void cancelGaussian() { if (hessian != null) { hessian.cancelGaussianGeneration(); } } // This is the implementation of GaussianGenerationCallback @Override public void proportionDone(final double proportion) { if (proportion < 0) { hessianEnabled = false; hessian = null; hessianSigma = -1; resultsDialog.gaussianCalculated(false); IJ.showProgress(1.0); return; } else if (proportion >= 1.0) { hessianEnabled = true; resultsDialog.gaussianCalculated(true); } IJ.showProgress(proportion); } /* * public void getTracings( boolean mineOnly ) { boolean result = * pathAndFillManager.getTracings( mineOnly, archiveClient ); if( result ) * unsavedPaths = false; } */ /* * public void uploadTracings( ) { boolean result = * pathAndFillManager.uploadTracings( archiveClient ); if( result ) * unsavedPaths = false; } */ public static boolean haveJava3D() { final ClassLoader loader = IJ.getClassLoader(); if (loader == null) throw new RuntimeException("IJ.getClassLoader() failed (!)"); try { final Class<?> c = loader.loadClass("ij3d.ImageWindow3D"); /* * In fact the documentation says that this should throw an * exception and not return null, but just in case: */ return c != null; } catch (final Exception e) { return false; } } public void showCorrespondencesTo(final File tracesFile, final Color c, final double maxDistance) { final PathAndFillManager pafmTraces = new PathAndFillManager(width, height, depth, (float) x_spacing, (float) y_spacing, (float) z_spacing, spacing_units); /* * FIXME: may well want to odd SWC options here, which isn't done with * the "loadGuessingType" method: */ if (!pafmTraces.loadGuessingType(tracesFile.getAbsolutePath())) { SNT.error("Failed to load traces from: " + tracesFile.getAbsolutePath()); return; } final List<Point3f> linePoints = new ArrayList<>(); // Now find corresponding points from the first one, and draw lines to // them: final ArrayList<NearPoint> cp = pathAndFillManager.getCorrespondences(pafmTraces, 2.5); int done = 0; for (final NearPoint np : cp) { if (np != null) { // SNT.log("Drawing:"); // SNT.log(np.toString()); linePoints.add(new Point3f((float) np.nearX, (float) np.nearY, (float) np.nearZ)); linePoints.add(new Point3f((float) np.closestIntersection.x, (float) np.closestIntersection.y, (float) np.closestIntersection.z)); final String ballName = univ.getSafeContentName("ball " + done); final List<Point3f> sphere = customnode.MeshMaker.createSphere(np.nearX, np.nearY, np.nearZ, Math.abs(x_spacing / 2)); univ.addTriangleMesh(sphere, new Color3f(c), ballName); } ++done; } univ.addLineMesh(linePoints, new Color3f(Color.red), "correspondences", false); for (int pi = 0; pi < pafmTraces.size(); ++pi) { final Path p = pafmTraces.getPath(pi); if (p.getUseFitted()) continue; p.addAsLinesTo3DViewer(univ, c, null); } // univ.resetView(); } protected volatile boolean showOnlySelectedPaths; protected void setShowOnlySelectedPaths(final boolean showOnlySelectedPaths, final boolean updateGUI) { this.showOnlySelectedPaths = showOnlySelectedPaths; if (updateGUI) { update3DViewerContents(); repaintAllPanes(); } } public void setShowOnlySelectedPaths(final boolean showOnlySelectedPaths) { setShowOnlySelectedPaths(showOnlySelectedPaths, true); } public void addPathsToOverlay(Overlay overlay, final int plane, final boolean SWTColoring) { if (overlay == null) overlay = new Overlay(); if (pathAndFillManager != null) { for (int i = 0; i < pathAndFillManager.size(); ++i) { final Path p = pathAndFillManager.getPath(i); if (p == null) continue; if (p.fittedVersionOf != null) continue; // If the path suggests using the fitted version, draw that // instead final Path drawPath = (p.useFitted) ? p.fitted : p; if (showOnlySelectedPaths && !pathAndFillManager.isSelected(p)) continue; if (SWTColoring) drawPath.setColorBySWCtype(); drawPath.drawPathAsPoints(overlay, plane); } } } public StackWindow getWindow(final int plane) { switch (plane) { case ThreePanes.XY_PLANE: return xy_window; case ThreePanes.XZ_PLANE: return (single_pane) ? null : xz_window; case ThreePanes.ZY_PLANE: return (single_pane) ? null : zy_window; default: return null; } } public boolean getSinglePane() { return single_pane; } protected void setSinglePane(final boolean single_pane) { this.single_pane = single_pane; } public boolean getShowOnlySelectedPaths() { return showOnlySelectedPaths; } /* * Whatever the state of the paths, update the 3D viewer to make sure that * they're the right colour, the right version (fitted or unfitted) is being * used and whether the path should be displayed at all - it shouldn't if * the "Show only selected paths" option is set. */ public void update3DViewerContents() { pathAndFillManager.update3DViewerContents(); } public Image3DUniverse get3DUniverse() { return univ; } public static final Color DEFAULT_SELECTED_COLOR = Color.GREEN; public static final Color DEFAULT_DESELECTED_COLOR = Color.MAGENTA; public static final Color3f DEFAULT_SELECTED_COLOR3F = new Color3f(Color.GREEN); public static final Color3f DEFAULT_DESELECTED_COLOR3F = new Color3f(Color.MAGENTA); public Color3f selectedColor3f = DEFAULT_SELECTED_COLOR3F; public Color3f deselectedColor3f = DEFAULT_DESELECTED_COLOR3F; public Color selectedColor = DEFAULT_SELECTED_COLOR; public Color deselectedColor = DEFAULT_DESELECTED_COLOR; public boolean displayCustomPathColors = true; public ImagePlus colorImage; public void setSelectedColor(final Color newColor) { selectedColor = newColor; selectedColor3f = new Color3f(newColor); repaintAllPanes(); update3DViewerContents(); } public void setDeselectedColor(final Color newColor) { deselectedColor = newColor; deselectedColor3f = new Color3f(newColor); repaintAllPanes(); update3DViewerContents(); } /* * FIXME: this can be very slow ... Perhaps do it in a separate thread? */ public void setColorImage(final ImagePlus newColorImage) { colorImage = newColorImage; update3DViewerContents(); } private int paths3DDisplay = 1; public void setPaths3DDisplay(final int paths3DDisplay) { this.paths3DDisplay = paths3DDisplay; update3DViewerContents(); } public int getPaths3DDisplay() { return this.paths3DDisplay; } public void selectPath(final Path p, final boolean addToExistingSelection) { final HashSet<Path> pathsToSelect = new HashSet<>(); if (p.isFittedVersionOfAnotherPath()) pathsToSelect.add(p.fittedVersionOf); else pathsToSelect.add(p); if (addToExistingSelection) { pathsToSelect.addAll(resultsDialog.getPathWindow().getSelectedPaths()); } resultsDialog.getPathWindow().setSelectedPaths(pathsToSelect, this); } public Set<Path> getSelectedPaths() { if (resultsDialog.getPathWindow() != null) { return resultsDialog.getPathWindow().getSelectedPaths(); } throw new RuntimeException("getSelectedPaths was called when resultsDialog.pw was null"); } @Override public void setPathList(final String[] newList, final Path justAdded, final boolean expandAll) { } @Override public void setFillList(final String[] newList) { } // Note that rather unexpectedly the p.setSelcted calls make sure that // the colour of the path in the 3D viewer is right... (FIXME) @Override public void setSelectedPaths(final HashSet<Path> selectedPathsSet, final Object source) { if (source == this) return; for (int i = 0; i < pathAndFillManager.size(); ++i) { final Path p = pathAndFillManager.getPath(i); if (selectedPathsSet.contains(p)) { p.setSelected(true); } else { p.setSelected(false); } } } /** * This method will remove the existing keylisteners from the component 'c', * tells 'firstKeyListener' to call those key listeners if it has not dealt * with the key, and then sets 'firstKeyListener' as the key listener for * 'c' */ public static void setAsFirstKeyListener(final Component c, final QueueJumpingKeyListener firstKeyListener) { final KeyListener[] oldKeyListeners = c.getKeyListeners(); for (final KeyListener kl : oldKeyListeners) { c.removeKeyListener(kl); } firstKeyListener.addOtherKeyListeners(oldKeyListeners); c.addKeyListener(firstKeyListener); } public synchronized void findSnappingPointInXYview(final double x_in_pane, final double y_in_pane, final double[] point) { // if (width == 0 || height == 0 || depth == 0) // throw new RuntimeException( // "Can't call findSnappingPointInXYview() before width, height and // depth are set..."); final int[] window_center = new int[3]; findPointInStack((int) Math.round(x_in_pane), (int) Math.round(y_in_pane), ThreePanes.XY_PLANE, window_center); int startx = window_center[0] - cursorSnapWindowXY; if (startx < 0) startx = 0; int starty = window_center[1] - cursorSnapWindowXY; if (starty < 0) starty = 0; int startz = window_center[2] - cursorSnapWindowZ; if (startz < 0) startz = 0; int stopx = window_center[0] + cursorSnapWindowXY; if (stopx > width) stopx = width; int stopy = window_center[1] + cursorSnapWindowXY; if (stopy > height) stopy = height; int stopz = window_center[2] + cursorSnapWindowZ; if (cursorSnapWindowZ == 0) { ++stopz; } else if (stopz > depth) { stopz = depth; } ArrayList<int[]> pointsAtMaximum = new ArrayList<>(); float currentMaximum = -Float.MAX_VALUE; for (int x = startx; x < stopx; ++x) { for (int y = starty; y < stopy; ++y) { for (int z = startz; z < stopz; ++z) { float v = -Float.MAX_VALUE; final int xyIndex = y * width + x; switch (imageType) { case ImagePlus.GRAY8: case ImagePlus.COLOR_256: v = 0xFF & slices_data_b[z][xyIndex]; break; case ImagePlus.GRAY16: v = slices_data_s[z][xyIndex]; break; case ImagePlus.GRAY32: v = slices_data_f[z][xyIndex]; break; default: throw new RuntimeException("Unknow image type: " + imageType); } if (v > currentMaximum) { pointsAtMaximum = new ArrayList<>(); pointsAtMaximum.add(new int[] { x, y, z }); currentMaximum = v; } else if (v == currentMaximum) { pointsAtMaximum.add(new int[] { x, y, z }); } } } } // if (pointsAtMaximum.size() == 0) { // findPointInStackPrecise(x_in_pane, y_in_pane, ThreePanes.XY_PLANE, // point); // if (verbose) // SNT.log("No maxima in snap-to window"); // return; // } final int[] snapped_p = pointsAtMaximum.get(pointsAtMaximum.size() / 2); if (window_center[2] != snapped_p[2]) xy.setSlice(snapped_p[2] + 1); point[0] = snapped_p[0]; point[1] = snapped_p[1]; point[2] = snapped_p[2]; } public void clickAtMaxPoint(final int x_in_pane, final int y_in_pane, final int plane) { final int[][] pointsToConsider = findAllPointsAlongLine(x_in_pane, y_in_pane, plane); ArrayList<int[]> pointsAtMaximum = new ArrayList<>(); float currentMaximum = -Float.MAX_VALUE; for (int i = 0; i < pointsToConsider.length; ++i) { float v = -Float.MAX_VALUE; final int[] p = pointsToConsider[i]; final int xyIndex = p[1] * width + p[0]; switch (imageType) { case ImagePlus.GRAY8: case ImagePlus.COLOR_256: v = 0xFF & slices_data_b[p[2]][xyIndex]; break; case ImagePlus.GRAY16: v = slices_data_s[p[2]][xyIndex]; break; case ImagePlus.GRAY32: v = slices_data_f[p[2]][xyIndex]; break; default: throw new RuntimeException("Unknow image type: " + imageType); } if (v > currentMaximum) { pointsAtMaximum = new ArrayList<>(); pointsAtMaximum.add(p); currentMaximum = v; } else if (v == currentMaximum) { pointsAtMaximum.add(p); } } /* * Take the middle of those points, and pretend that was the point that * was clicked on. */ final int[] p = pointsAtMaximum.get(pointsAtMaximum.size() / 2); clickForTrace(p[0] * x_spacing, p[1] * y_spacing, p[2] * z_spacing, false); } public static final int OVERLAY_OPACITY_PERCENT = 20; private static final String OVERLAY_IDENTIFIER = "SNT-MIP-OVERLAY"; public void showMIPOverlays(final boolean show) { final ArrayList<ImagePlus> allImages = new ArrayList<>(); allImages.add(xy); if (!single_pane) { allImages.add(xz); allImages.add(zy); } for (final ImagePlus imagePlus : allImages) { if (imagePlus == null || imagePlus.getImageStackSize() == 1) continue; Overlay overlayList = imagePlus.getOverlay(); if (show) { // Create a MIP project of the stack: final ZProjector zp = new ZProjector(); zp.setImage(imagePlus); zp.setMethod(ZProjector.MAX_METHOD); zp.doProjection(); final ImagePlus overlay = zp.getProjection(); // Add display it as an overlay. // (This logic is taken from OverlayCommands.) final Roi roi = new ImageRoi(0, 0, overlay.getProcessor()); roi.setName(OVERLAY_IDENTIFIER); ((ImageRoi) roi).setOpacity(OVERLAY_OPACITY_PERCENT / 100.0); if (overlayList == null) overlayList = new Overlay(); overlayList.add(roi); } else { removeMIPfromOverlay(overlayList); } imagePlus.setOverlay(overlayList); } } private void removeMIPfromOverlay(final Overlay overlay) { if (overlay != null && overlay.size() > 0) { for (int i = overlay.size() - 1; i >= 0; i--) { final String roiName = overlay.get(i).getName(); if (roiName != null && roiName.equals(OVERLAY_IDENTIFIER)) { overlay.remove(i); return; } } } } protected void updateViewPathChoice() { if (!resultsDialog.viewPathChoice.isEnabled()) return; resultsDialog.viewPathChoice.setSelectedItem( xy_tracer_canvas.just_near_slices ? resultsDialog.partsNearbyChoice : resultsDialog.projectionChoice); } protected void toogleSnapCursor() { enableSnapCursor(!snapCursor); } public synchronized void enableSnapCursor(final boolean enable) { snapCursor = enable; resultsDialog.useSnapWindow.setSelected(enable); resultsDialog.snapWindowXYsizeSpinner.setEnabled(enable); resultsDialog.snapWindowZsizeSpinner.setEnabled(enable && !singleSlice); } public void enableAutoActivation(final boolean enable) { autoCanvasActivation = enable; } protected boolean drawDiametersXY = Prefs.get("tracing.Simple_Neurite_Tracer.drawDiametersXY", "false") .equals("true"); public void setDrawDiametersXY(final boolean draw) { drawDiametersXY = draw; repaintAllPanes(); } public boolean getDrawDiametersXY() { return drawDiametersXY; } @Override public void closeAndReset() { // Dispose xz/zy images unless the user stored some annotations (ROIs) // on the image overlay or modified them somehow. In that case, restore // them to the user if (!single_pane) { final ImagePlus[] impPanes = { xz, zy }; final StackWindow[] winPanes = { xz_window, zy_window }; for (int i = 0; i < impPanes.length; ++i) { final Overlay overlay = impPanes[i].getOverlay(); removeMIPfromOverlay(overlay); if (!impPanes[i].changes && (overlay == null || impPanes[i].getOverlay().size() == 0)) impPanes[i].close(); else { winPanes[i] = new StackWindow(impPanes[i]); removeMIPfromOverlay(overlay); impPanes[i].setOverlay(overlay); } } } // Restore main view final Overlay overlay = (xy == null) ? null : xy.getOverlay(); if (original_xy_canvas != null && xy != null && xy.getImage() != null) { xy_window = new StackWindow(xy, original_xy_canvas); removeMIPfromOverlay(overlay); xy.setOverlay(overlay); } } }