package ini.trakem2.persistence; import ij.IJ; import ij.gui.GenericDialog; import ij.io.OpenDialog; import ini.trakem2.ControlWindow; import ini.trakem2.Project; import ini.trakem2.display.Displayable; import ini.trakem2.display.Patch; import ini.trakem2.display.YesNoDialog; import ini.trakem2.utils.Dispatcher; import ini.trakem2.utils.IJError; import ini.trakem2.utils.Utils; import java.awt.Dimension; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.util.Collection; import java.util.HashSet; import java.util.Hashtable; import java.util.Set; import java.util.Vector; import javax.swing.BoxLayout; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.SwingUtilities; import javax.swing.table.AbstractTableModel; /** A class to manage "file not found" problems. */ public class FilePathRepair { private final Project project; private final PathTableModel data = new PathTableModel(); private final JTable table = new JTable(data); private final JFrame frame; private FilePathRepair(final Project project) { this.project = project; this.frame = ControlWindow.createJFrame("Repair: " + project); } private final Runnable makeGUI() { return new Runnable() { public void run() { JScrollPane jsp = new JScrollPane(table); jsp.setPreferredSize(new Dimension(500, 500)); table.addMouseListener(listener); JLabel label = new JLabel("Double-click any to repair file path:"); JLabel label2 = new JLabel("(Any listed with identical parent folder will be fixed as well.)"); JPanel plabel = new JPanel(); BoxLayout pbl = new BoxLayout(plabel, BoxLayout.Y_AXIS); plabel.setLayout(pbl); //plabel.setBorder(new LineBorder(Color.black, 1, true)); plabel.setMinimumSize(new Dimension(400, 40)); plabel.add(label); plabel.add(label2); JPanel all = new JPanel(); BoxLayout bl = new BoxLayout(all, BoxLayout.Y_AXIS); all.setLayout(bl); all.add(plabel); all.add(jsp); frame.getContentPane().add(all); frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent we) { synchronized (projects) { if (data.vpath.size() > 0 ) { Utils.logAll("WARNING: Some images remain associated to inexistent file paths."); } projects.remove(project); } } }); frame.pack(); ij.gui.GUI.center(frame); frame.setVisible(true); }}; } private static class PathTableModel extends AbstractTableModel { final Vector<Patch> vp = new Vector<Patch>(); final Vector<String> vpath = new Vector<String>(); final HashSet<Patch> set = new HashSet<Patch>(); PathTableModel() {} public final String getColumnName(final int col) { switch (col) { case 0: return "Image"; case 1: return "Nonexistent file path"; default: return ""; } } public final int getRowCount() { return vp.size(); } public final int getColumnCount() { return 2; } public final Object getValueAt(final int row, final int col) { switch (col) { case 0: return vp.get(row); case 1: return vpath.get(row); default: return null; } } public final boolean isCellEditable(final int row, final int col) { return false; } public final void setValueAt(final Object value, final int row, final int col) {} // ignore synchronized public final void add(final Patch patch) { if (set.contains(patch)) return; // already here vp.add(patch); vpath.add(patch.getImageFilePath()); // no slice information if it's a stack set.add(patch); } synchronized public final String remove(final int row) { set.remove(vp.remove(row)); return vpath.remove(row); } synchronized public final String remove(final Patch p) { final int i = vp.indexOf(p); if (-1 == i) return null; set.remove(vp.remove(i)); return vpath.remove(i); } } // Static part static private final Hashtable<Project,FilePathRepair> projects = new Hashtable<Project,FilePathRepair>(); static public void add(final Patch patch) { dispatcher.exec(new Runnable() { public void run() { final Project project = patch.getProject(); FilePathRepair fpr = null; synchronized (projects) { fpr = projects.get(project); if (null == fpr) { fpr = new FilePathRepair(project); projects.put(project, fpr); SwingUtilities.invokeLater(fpr.makeGUI()); } fpr.data.add(patch); if (!fpr.frame.isVisible()) { fpr.frame.setVisible(true); } else { SwingUtilities.invokeLater(new Repainter(fpr)); } } }}); } static private class Repainter implements Runnable { FilePathRepair fpr; Repainter(final FilePathRepair fpr) { this.fpr = fpr; } public void run() { try { fpr.table.updateUI(); fpr.table.repaint(); fpr.frame.pack(); } catch (Exception e) { IJError.print(e); } } } static private final Dispatcher dispatcher = new Dispatcher("File path fixer"); static private MouseAdapter listener = new MouseAdapter() { public void mousePressed(final MouseEvent me) { final JTable table = (JTable) me.getSource(); final PathTableModel data = (PathTableModel) table.getModel(); final int row = table.rowAtPoint(me.getPoint()); if (-1 == row) return; if (2 == me.getClickCount()) { dispatcher.exec(new Runnable() { public void run() { try { table.setEnabled(false); GenericDialog gd = new GenericDialog("Fix paths"); gd.addCheckbox("Fix other listed image files with identical parent directory", true); gd.addCheckbox("Fix all image files in the project with identical parent directory", true); gd.addCheckbox("Update mipmaps for each fixed path", false); gd.showDialog(); if (!gd.wasCanceled()) { fixPath(table, data, row, gd.getNextBoolean(), gd.getNextBoolean(), gd.getNextBoolean()); } } catch (Exception e) { IJError.print(e); } finally { table.setEnabled(true); } }}); } } }; static private void fixPath(final JTable table, final PathTableModel data, final int row, final boolean fix_similar, final boolean fix_all, final boolean update_mipmaps) throws Exception { synchronized (projects) { final Patch patch = data.vp.get(row); if (null == patch) return; final String old_path = patch.getImageFilePath(); final File f = new File(old_path); if (f.exists()) { Utils.log("File exists for " + patch + " at " + f.getAbsolutePath() + "\n --> not updating path."); data.remove(row); return; } // Else, pop up file dialog OpenDialog od = new OpenDialog("Select image file", OpenDialog.getDefaultDirectory(), null); String dir = od.getDirectory(); final String filename = od.getFileName(); if (null == dir) return; // dialog was canceled if (IJ.isWindows()) dir = dir.replace('\\', '/'); if (!dir.endsWith("/")) dir += "/"; // Compare filenames if ( ! filename.equals(f.getName())) { YesNoDialog yn = new YesNoDialog(projects.get(patch.getProject()).frame, "WARNING", "Different file names!\n old: " + f.getName() + "\n new: " + filename + "\nSet to new file name?"); if ( ! yn.yesPressed()) return; // Remove mipmaps: would not be found with the new name and the old ones would remain behind unused if ( ! f.getName().equals(new File(old_path).getName())) { // remove mipmaps: the name wouldn't match otherwise patch.getProject().getLoader().removeMipMaps(patch); } } // String wrong_parent_path = new File(data.vpath.get(row)).getParent(); wrong_parent_path = wrong_parent_path.replace('\\', '/'); if (!wrong_parent_path.endsWith("/")) wrong_parent_path = new StringBuilder(wrong_parent_path).append('/').toString(); // not File.separatorChar, TrakEM2 uses '/' as folder separator final String path = new StringBuilder(dir).append(filename).toString(); // keep track of fixed slices to avoid calling n_slices * n_slices times! final HashSet<Patch> fixed = new HashSet<Patch>(); int n_fixed = 0; if (-1 == patch.getFilePath().lastIndexOf("-----#slice=")) { if (!fixPatchPath(patch, path, update_mipmaps)) { return; } data.remove(patch); fixed.add(patch); n_fixed += 1; } else { int n = fixStack(data, fixed, patch.getStackPatches(), old_path, path, update_mipmaps); if (0 == n) { return; // some error ocurred, no paths fixed } n_fixed += n; // data already cleared of removed patches by fixStack } String good_parent_path = dir; if (!dir.endsWith("/")) good_parent_path = new StringBuilder(good_parent_path).append('/').toString(); // not File.separatorChar, TrakEM2 uses '/' as folder separator // Check for similar parent paths and see if they can be fixed if (fix_similar) { for (int i=data.vp.size() -1; i>-1; i--) { final String wrong_path = data.vpath.get(i); final Patch p = data.vp.get(i); if (wrong_path.startsWith(wrong_parent_path)) { // try to fix as well final File file = new File(new StringBuilder(good_parent_path).append(wrong_path.substring(wrong_parent_path.length())).toString()); if (file.exists()) { if (-1 == p.getFilePath().lastIndexOf("-----#slice=")) { if (!fixed.contains(p) && fixPatchPath(p, file.getAbsolutePath(), update_mipmaps)) { data.remove(p); // not by 'i' but by Patch, since if some fail the order is not the same n_fixed++; fixed.add(p); } } else { if (fixed.contains(p)) continue; n_fixed += fixStack(data, fixed, p.getStackPatches(), wrong_path, file.getAbsolutePath(), update_mipmaps); } } } } } if (fix_all) { // traverse all Patch from the entire project, minus those already fixed for (final Displayable d : patch.getLayerSet().getDisplayables(Patch.class)) { final Patch p = (Patch) d; final String wrong_path = p.getImageFilePath(); if (wrong_path.startsWith(wrong_parent_path)) { File file = new File(new StringBuilder(good_parent_path).append(wrong_path.substring(wrong_parent_path.length())).toString()); if (file.exists()) { if (-1 == p.getFilePath().lastIndexOf("-----#slice=")) { if (!fixed.contains(p) && fixPatchPath(p, file.getAbsolutePath(), update_mipmaps)) { data.remove(p); // not by 'i' but by Patch, since if some fail the order is not the same n_fixed++; fixed.add(p); } } else { if (fixed.contains(p)) continue; n_fixed += fixStack(data, fixed, p.getStackPatches(), wrong_path, file.getAbsolutePath(), update_mipmaps); } } } } } // if table is empty, close if (0 == data.vp.size()) { FilePathRepair fpr = projects.remove(patch.getProject()); fpr.frame.dispose(); } Utils.logAll("Fixed " + n_fixed + " image file path" + (n_fixed > 1 ? "s" : "")); } } static private int fixStack(final PathTableModel data, final Set<Patch> fixed, final Collection<Patch> slices, final String wrong_path, final String new_path, final boolean update_mipmaps) { int n_fixed = 0; Dimension dim = null; Loader loader = null; for (final Patch ps : slices) { if (fixed.contains(ps)) continue; final String slicepath = ps.getFilePath(); final int isl = slicepath.lastIndexOf("-----#slice="); if (-1 == isl) { Utils.log2("Not a stack path: " + slicepath); continue; // someone linked an image... } final String ps_path = slicepath.substring(0, isl); // same: // ps.getImageFilePath(); if (! ps_path.substring(0, isl).equals(wrong_path)) { Utils.log2("Not the same stack path:\n i=" + ps_path + "\n ref=" + wrong_path); continue; // not the same stack! } if (null == dim) { loader = ps.getProject().getLoader(); loader.releaseToFit(Math.max(Loader.MIN_FREE_BYTES, ps.getOWidth() * ps.getOHeight() * 10)); dim = loader.getDimensions(new_path); if (null == dim) { Utils.log(new StringBuilder("ERROR: could not open image at ").append(new_path).toString()); // preserving backslashes return n_fixed; } // Check dimensions if (dim.width != ps.getOWidth() || dim.height != ps.getOHeight()) { Utils.log("ERROR different o_width,o_height for patch " + ps + "\n at new path " + new_path + "\nold o_width,o_height: " + ps.getOWidth() + "," + ps.getOHeight() + "\nnew o_width,o_height: " + dim.width + "," + dim.height); return n_fixed; } } // flag as good fixed.add(ps); loader.removeFromUnloadable(ps); // Assign new image path with slice info appended loader.addedPatchFrom(new_path + slicepath.substring(isl), ps); // submit job to regenerate mipmaps in the background if (update_mipmaps) loader.regenerateMipMaps(ps); // data.remove(ps); n_fixed++; } return n_fixed; } static private boolean fixPatchPath(final Patch patch, final String new_path, final boolean update_mipmaps) { try { // Open the image header to check that dimensions match final Loader loader = patch.getProject().getLoader(); loader.releaseToFit(Math.max(Loader.MIN_FREE_BYTES, patch.getOWidth() * patch.getOHeight() * 10)); final Dimension dim = loader.getDimensions(new_path); if (null == dim) { Utils.log(new StringBuilder("ERROR: could not open image at ").append(new_path).toString()); // preserving backslashes return false; } // Check and set dimensions if (dim.width != patch.getOWidth() || dim.height != patch.getOHeight()) { Utils.log("ERROR different o_width,o_height for patch " + patch + "\n at new path " + new_path + "\nold o_width,o_height: " + patch.getOWidth() + "," + patch.getOHeight() + "\nnew o_width,o_height: " + dim.width + "," + dim.height); return false; } // flag as good loader.removeFromUnloadable(patch); // Assign new image path loader.addedPatchFrom(new_path, patch); // submit job to regenerate mipmaps in the background if (update_mipmaps) loader.regenerateMipMaps(patch); return true; } catch (Exception e) { IJError.print(e); return false; } } }