fixed improper generic parameter use in Tree.duplicateAs > Map, necessary
[trakem2.git] / TrakEM2_ / src / main / java / ini / trakem2 / persistence / FilePathRepair.java
blob807937e3ce768531946aca2f32e0c0d7174c96b3
1 package ini.trakem2.persistence;
3 import ij.IJ;
4 import ij.gui.GenericDialog;
5 import ij.io.OpenDialog;
6 import ini.trakem2.ControlWindow;
7 import ini.trakem2.Project;
8 import ini.trakem2.display.Displayable;
9 import ini.trakem2.display.Patch;
10 import ini.trakem2.display.YesNoDialog;
11 import ini.trakem2.utils.Dispatcher;
12 import ini.trakem2.utils.IJError;
13 import ini.trakem2.utils.Utils;
15 import java.awt.Dimension;
16 import java.awt.event.MouseAdapter;
17 import java.awt.event.MouseEvent;
18 import java.awt.event.WindowAdapter;
19 import java.awt.event.WindowEvent;
20 import java.io.File;
21 import java.util.Collection;
22 import java.util.HashSet;
23 import java.util.Hashtable;
24 import java.util.Set;
25 import java.util.Vector;
27 import javax.swing.BoxLayout;
28 import javax.swing.JFrame;
29 import javax.swing.JLabel;
30 import javax.swing.JPanel;
31 import javax.swing.JScrollPane;
32 import javax.swing.JTable;
33 import javax.swing.SwingUtilities;
34 import javax.swing.table.AbstractTableModel;
36 /** A class to manage "file not found" problems. */
37 public class FilePathRepair {
39 private final Project project;
40 private final PathTableModel data = new PathTableModel();
41 private final JTable table = new JTable(data);
42 private final JFrame frame;
44 private FilePathRepair(final Project project) {
45 this.project = project;
46 this.frame = ControlWindow.createJFrame("Repair: " + project);
49 private final Runnable makeGUI() {
50 return new Runnable() { public void run() {
51 JScrollPane jsp = new JScrollPane(table);
52 jsp.setPreferredSize(new Dimension(500, 500));
53 table.addMouseListener(listener);
54 JLabel label = new JLabel("Double-click any to repair file path:");
55 JLabel label2 = new JLabel("(Any listed with identical parent folder will be fixed as well.)");
56 JPanel plabel = new JPanel();
57 BoxLayout pbl = new BoxLayout(plabel, BoxLayout.Y_AXIS);
58 plabel.setLayout(pbl);
59 //plabel.setBorder(new LineBorder(Color.black, 1, true));
60 plabel.setMinimumSize(new Dimension(400, 40));
61 plabel.add(label);
62 plabel.add(label2);
63 JPanel all = new JPanel();
64 BoxLayout bl = new BoxLayout(all, BoxLayout.Y_AXIS);
65 all.setLayout(bl);
66 all.add(plabel);
67 all.add(jsp);
68 frame.getContentPane().add(all);
69 frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
70 frame.addWindowListener(new WindowAdapter() {
71 public void windowClosing(WindowEvent we) {
72 synchronized (projects) {
73 if (data.vpath.size() > 0 ) {
74 Utils.logAll("WARNING: Some images remain associated to inexistent file paths.");
76 projects.remove(project);
79 });
80 frame.pack();
81 ij.gui.GUI.center(frame);
82 frame.setVisible(true);
83 }};
86 private static class PathTableModel extends AbstractTableModel {
87 final Vector<Patch> vp = new Vector<Patch>();
88 final Vector<String> vpath = new Vector<String>();
89 final HashSet<Patch> set = new HashSet<Patch>();
90 PathTableModel() {}
91 public final String getColumnName(final int col) {
92 switch (col) {
93 case 0: return "Image";
94 case 1: return "Nonexistent file path";
95 default: return "";
98 public final int getRowCount() { return vp.size(); }
99 public final int getColumnCount() { return 2; }
100 public final Object getValueAt(final int row, final int col) {
101 switch (col) {
102 case 0: return vp.get(row);
103 case 1: return vpath.get(row);
104 default: return null;
107 public final boolean isCellEditable(final int row, final int col) {
108 return false;
110 public final void setValueAt(final Object value, final int row, final int col) {} // ignore
112 synchronized public final void add(final Patch patch) {
113 if (set.contains(patch)) return; // already here
114 vp.add(patch);
115 vpath.add(patch.getImageFilePath()); // no slice information if it's a stack
116 set.add(patch);
119 synchronized public final String remove(final int row) {
120 set.remove(vp.remove(row));
121 return vpath.remove(row);
124 synchronized public final String remove(final Patch p) {
125 final int i = vp.indexOf(p);
126 if (-1 == i) return null;
127 set.remove(vp.remove(i));
128 return vpath.remove(i);
132 // Static part
134 static private final Hashtable<Project,FilePathRepair> projects = new Hashtable<Project,FilePathRepair>();
136 static public void add(final Patch patch) {
137 dispatcher.exec(new Runnable() { public void run() {
138 final Project project = patch.getProject();
139 FilePathRepair fpr = null;
140 synchronized (projects) {
141 fpr = projects.get(project);
142 if (null == fpr) {
143 fpr = new FilePathRepair(project);
144 projects.put(project, fpr);
145 SwingUtilities.invokeLater(fpr.makeGUI());
147 fpr.data.add(patch);
148 if (!fpr.frame.isVisible()) {
149 fpr.frame.setVisible(true);
150 } else {
151 SwingUtilities.invokeLater(new Repainter(fpr));
154 }});
157 static private class Repainter implements Runnable {
158 FilePathRepair fpr;
159 Repainter(final FilePathRepair fpr) { this.fpr = fpr; }
160 public void run() {
161 try {
162 fpr.table.updateUI();
163 fpr.table.repaint();
164 fpr.frame.pack();
165 } catch (Exception e) { IJError.print(e); }
169 static private final Dispatcher dispatcher = new Dispatcher("File path fixer");
171 static private MouseAdapter listener = new MouseAdapter() {
172 public void mousePressed(final MouseEvent me) {
173 final JTable table = (JTable) me.getSource();
174 final PathTableModel data = (PathTableModel) table.getModel();
175 final int row = table.rowAtPoint(me.getPoint());
176 if (-1 == row) return;
178 if (2 == me.getClickCount()) {
179 dispatcher.exec(new Runnable() { public void run() {
180 try {
181 table.setEnabled(false);
182 GenericDialog gd = new GenericDialog("Fix paths");
183 gd.addCheckbox("Fix other listed image files with identical parent directory", true);
184 gd.addCheckbox("Fix all image files in the project with identical parent directory", true);
185 gd.addCheckbox("Update mipmaps for each fixed path", false);
186 gd.showDialog();
187 if (!gd.wasCanceled()) {
188 fixPath(table, data, row, gd.getNextBoolean(), gd.getNextBoolean(), gd.getNextBoolean());
190 } catch (Exception e) {
191 IJError.print(e);
192 } finally {
193 table.setEnabled(true);
195 }});
200 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 {
201 synchronized (projects) {
202 final Patch patch = data.vp.get(row);
203 if (null == patch) return;
204 final String old_path = patch.getImageFilePath();
205 final File f = new File(old_path);
206 if (f.exists()) {
207 Utils.log("File exists for " + patch + " at " + f.getAbsolutePath() + "\n --> not updating path.");
208 data.remove(row);
209 return;
211 // Else, pop up file dialog
212 OpenDialog od = new OpenDialog("Select image file", OpenDialog.getDefaultDirectory(), null);
213 String dir = od.getDirectory();
214 final String filename = od.getFileName();
215 if (null == dir) return; // dialog was canceled
216 if (IJ.isWindows()) dir = dir.replace('\\', '/');
217 if (!dir.endsWith("/")) dir += "/";
218 // Compare filenames
219 if ( ! filename.equals(f.getName())) {
220 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?");
221 if ( ! yn.yesPressed()) return;
223 // Remove mipmaps: would not be found with the new name and the old ones would remain behind unused
224 if ( ! f.getName().equals(new File(old_path).getName())) {
225 // remove mipmaps: the name wouldn't match otherwise
226 patch.getProject().getLoader().removeMipMaps(patch);
230 String wrong_parent_path = new File(data.vpath.get(row)).getParent();
231 wrong_parent_path = wrong_parent_path.replace('\\', '/');
232 if (!wrong_parent_path.endsWith("/")) wrong_parent_path = new StringBuilder(wrong_parent_path).append('/').toString(); // not File.separatorChar, TrakEM2 uses '/' as folder separator
234 final String path = new StringBuilder(dir).append(filename).toString();
236 // keep track of fixed slices to avoid calling n_slices * n_slices times!
237 final HashSet<Patch> fixed = new HashSet<Patch>();
239 int n_fixed = 0;
241 if (-1 == patch.getFilePath().lastIndexOf("-----#slice=")) {
242 if (!fixPatchPath(patch, path, update_mipmaps)) {
243 return;
245 data.remove(patch);
246 fixed.add(patch);
247 n_fixed += 1;
248 } else {
249 int n = fixStack(data, fixed, patch.getStackPatches(), old_path, path, update_mipmaps);
250 if (0 == n) {
251 return; // some error ocurred, no paths fixed
253 n_fixed += n;
254 // data already cleared of removed patches by fixStack
257 String good_parent_path = dir;
258 if (!dir.endsWith("/")) good_parent_path = new StringBuilder(good_parent_path).append('/').toString(); // not File.separatorChar, TrakEM2 uses '/' as folder separator
260 // Check for similar parent paths and see if they can be fixed
261 if (fix_similar) {
262 for (int i=data.vp.size() -1; i>-1; i--) {
263 final String wrong_path = data.vpath.get(i);
264 final Patch p = data.vp.get(i);
265 if (wrong_path.startsWith(wrong_parent_path)) {
266 // try to fix as well
267 final File file = new File(new StringBuilder(good_parent_path).append(wrong_path.substring(wrong_parent_path.length())).toString());
268 if (file.exists()) {
269 if (-1 == p.getFilePath().lastIndexOf("-----#slice=")) {
270 if (!fixed.contains(p) && fixPatchPath(p, file.getAbsolutePath(), update_mipmaps)) {
271 data.remove(p); // not by 'i' but by Patch, since if some fail the order is not the same
272 n_fixed++;
273 fixed.add(p);
275 } else {
276 if (fixed.contains(p)) continue;
277 n_fixed += fixStack(data, fixed, p.getStackPatches(), wrong_path, file.getAbsolutePath(), update_mipmaps);
283 if (fix_all) {
284 // traverse all Patch from the entire project, minus those already fixed
285 for (final Displayable d : patch.getLayerSet().getDisplayables(Patch.class)) {
286 final Patch p = (Patch) d;
287 final String wrong_path = p.getImageFilePath();
288 if (wrong_path.startsWith(wrong_parent_path)) {
289 File file = new File(new StringBuilder(good_parent_path).append(wrong_path.substring(wrong_parent_path.length())).toString());
290 if (file.exists()) {
291 if (-1 == p.getFilePath().lastIndexOf("-----#slice=")) {
292 if (!fixed.contains(p) && fixPatchPath(p, file.getAbsolutePath(), update_mipmaps)) {
293 data.remove(p); // not by 'i' but by Patch, since if some fail the order is not the same
294 n_fixed++;
295 fixed.add(p);
297 } else {
298 if (fixed.contains(p)) continue;
299 n_fixed += fixStack(data, fixed, p.getStackPatches(), wrong_path, file.getAbsolutePath(), update_mipmaps);
306 // if table is empty, close
307 if (0 == data.vp.size()) {
308 FilePathRepair fpr = projects.remove(patch.getProject());
309 fpr.frame.dispose();
312 Utils.logAll("Fixed " + n_fixed + " image file path" + (n_fixed > 1 ? "s" : ""));
316 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) {
317 int n_fixed = 0;
318 Dimension dim = null;
319 Loader loader = null;
320 for (final Patch ps : slices) {
321 if (fixed.contains(ps)) continue;
322 final String slicepath = ps.getFilePath();
323 final int isl = slicepath.lastIndexOf("-----#slice=");
324 if (-1 == isl) {
325 Utils.log2("Not a stack path: " + slicepath);
326 continue; // someone linked an image...
328 final String ps_path = slicepath.substring(0, isl); // same: // ps.getImageFilePath();
329 if (! ps_path.substring(0, isl).equals(wrong_path)) {
330 Utils.log2("Not the same stack path:\n i=" + ps_path + "\n ref=" + wrong_path);
331 continue; // not the same stack!
333 if (null == dim) {
334 loader = ps.getProject().getLoader();
335 loader.releaseToFit(Math.max(Loader.MIN_FREE_BYTES, ps.getOWidth() * ps.getOHeight() * 10));
336 dim = loader.getDimensions(new_path);
337 if (null == dim) {
338 Utils.log(new StringBuilder("ERROR: could not open image at ").append(new_path).toString()); // preserving backslashes
339 return n_fixed;
341 // Check dimensions
342 if (dim.width != ps.getOWidth() || dim.height != ps.getOHeight()) {
343 Utils.log("ERROR different o_width,o_height for patch " + ps + "\n at new path " + new_path +
344 "\nold o_width,o_height: " + ps.getOWidth() + "," + ps.getOHeight() +
345 "\nnew o_width,o_height: " + dim.width + "," + dim.height);
346 return n_fixed;
349 // flag as good
350 fixed.add(ps);
351 loader.removeFromUnloadable(ps);
352 // Assign new image path with slice info appended
353 loader.addedPatchFrom(new_path + slicepath.substring(isl), ps);
354 // submit job to regenerate mipmaps in the background
355 if (update_mipmaps) loader.regenerateMipMaps(ps);
357 data.remove(ps);
358 n_fixed++;
360 return n_fixed;
363 static private boolean fixPatchPath(final Patch patch, final String new_path, final boolean update_mipmaps) {
364 try {
365 // Open the image header to check that dimensions match
366 final Loader loader = patch.getProject().getLoader();
367 loader.releaseToFit(Math.max(Loader.MIN_FREE_BYTES, patch.getOWidth() * patch.getOHeight() * 10));
368 final Dimension dim = loader.getDimensions(new_path);
369 if (null == dim) {
370 Utils.log(new StringBuilder("ERROR: could not open image at ").append(new_path).toString()); // preserving backslashes
371 return false;
373 // Check and set dimensions
374 if (dim.width != patch.getOWidth() || dim.height != patch.getOHeight()) {
375 Utils.log("ERROR different o_width,o_height for patch " + patch + "\n at new path " + new_path +
376 "\nold o_width,o_height: " + patch.getOWidth() + "," + patch.getOHeight() +
377 "\nnew o_width,o_height: " + dim.width + "," + dim.height);
378 return false;
380 // flag as good
381 loader.removeFromUnloadable(patch);
382 // Assign new image path
383 loader.addedPatchFrom(new_path, patch);
384 // submit job to regenerate mipmaps in the background
385 if (update_mipmaps) loader.regenerateMipMaps(patch);
386 return true;
387 } catch (Exception e) {
388 IJError.print(e);
389 return false;