package ini.trakem2.utils; import ini.trakem2.Project; import ini.trakem2.display.AreaTree; import ini.trakem2.display.Connector; import ini.trakem2.display.Display; import ini.trakem2.display.Display3D; import ini.trakem2.display.Displayable; import ini.trakem2.display.LayerSet; import ini.trakem2.display.Node; import ini.trakem2.display.Tag; import ini.trakem2.display.Tree; import ini.trakem2.display.Treeline; import ini.trakem2.display.ZDisplayable; import ini.trakem2.parallel.TaskFactory; import ini.trakem2.persistence.FSLoader; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import javax.swing.JFrame; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTabbedPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; public class Merger { /** Take two projects and find out what is different among them, * independent of id. */ static public final void compare(final Project p1, final Project p2) { Utils.log("Be warned: only Treeline, AreaTree and Connector are considered at the moment."); final LayerSet ls1 = p1.getRootLayerSet(), ls2 = p2.getRootLayerSet(); final Collection<ZDisplayable> zds1 = ls1.getZDisplayables(), zds2 = ls2.getZDisplayables(); final HashSet<Class<?>> accepted = new HashSet<Class<?>>(); accepted.add(Treeline.class); accepted.add(AreaTree.class); accepted.add(Connector.class); final HashMap<Displayable,List<Change>> matched = new HashMap<Displayable,List<Change>>(); final HashSet<ZDisplayable> empty1 = new HashSet<ZDisplayable>(), empty2 = new HashSet<ZDisplayable>(); final HashSet<ZDisplayable> unmatched1 = new HashSet<ZDisplayable>(), unmatched2 = new HashSet<ZDisplayable>(zds2); // Remove instances of classes not accepted for (final Iterator<ZDisplayable> it = unmatched2.iterator(); it.hasNext(); ) { ZDisplayable zd = it.next(); if (!accepted.contains(zd.getClass())) { it.remove(); continue; } if (zd.isDeletable()) { it.remove(); empty2.add(zd); } } zds2.removeAll(empty2); final AtomicInteger counter = new AtomicInteger(0); // For every Displayable in p1, find a corresponding Displayable in p2 // or at least one or more that are similar in that they have some nodes in common. try { ini.trakem2.parallel.Process.unbound(zds1, new TaskFactory<ZDisplayable, Object>() { @Override public Object process(final ZDisplayable zd1) { Utils.showProgress(counter.getAndIncrement() / (float)zds1.size()); if (!accepted.contains(zd1.getClass())) { Utils.log("Ignoring: [A] " + zd1); return null; } if (zd1.isDeletable()) { synchronized (empty1) { empty1.add(zd1); } return null; } final List<Change> cs = new ArrayList<Change>(); for (final ZDisplayable zd2 : zds2) { // Same class? if (zd1.getClass() != zd2.getClass()) continue; if (zd1 instanceof Tree<?> && zd2 instanceof Tree<?>) { Change c = compareTrees(zd1, zd2); if (c.hasSimilarNodes()) { cs.add(c); if (1 == cs.size()) { synchronized (matched) { matched.put(zd1, cs); } } synchronized (unmatched2) { unmatched2.remove(zd2); } } // debug if (zd1.getId() == zd2.getId()) { Utils.log("zd1 #" + zd1.getId() + " is similar to #" + zd2.getId() + ": " + c.hasSimilarNodes()); } } } if (cs.isEmpty()) { synchronized (unmatched1) { unmatched1.add(zd1); } } return null; } }); } catch (Exception e) { IJError.print(e); } Utils.showProgress(1); // reset Utils.log("matched.size(): " + matched.size()); makeGUI(p1, p2, empty1, empty2, matched, unmatched1, unmatched2); } private static class Change { ZDisplayable d1, d2; /** If the title is different. */ boolean title = false; /** If the AffineTransform is different. */ boolean transform = false; /** If the tree has been rerooted. */ boolean root = false; /** The difference in the number of nodes, in percent */ int diff = 0; /** Number of nodes in d1 found also in d2 (independent of tags). */ int common_nodes = 0; /** Number of nodes present in d1 but not in d2. */ int d1_only = 0; /** Number of nodes present in d2 but not in d1. */ int d2_only = 0; /** Total number of nodes in d1. */ int n_nodes_d1 = 0; /** Total number of nodes in d2. */ int n_nodes_d2 = 0; /** Number of tags in common (node-wise, not overall tags) */ int common_tags = 0; /** Number of tags present in a node of d1 but not in d2. */ int tags_1_only = 0; /** Number of tags present in a node of d1 but not in d2. */ int tags_2_only = 0; Change(ZDisplayable d1, ZDisplayable d2) { this.d1 = d1; this.d2 = d2; } /** WARNING: if both trees have zero nodes, returns true as well. */ boolean identical() { return !title && !transform && !root && 0 == diff && 0 == tags_1_only && 0 == tags_2_only && 0 == d1_only && 0 == d2_only; } boolean hasSimilarNodes() { return common_nodes > 0; } } /** Just so that hashCode will be 0 always and HashMap will be forced to use equals instead, * and also using the affine. */ static private final class WNode { final Node<?> nd; final float x, y; final double z; WNode(Node<?> nd, AffineTransform aff) { this.nd = nd; float[] f = new float[]{nd.getX(), nd.getY()}; aff.transform(f, 0, f, 0, 1); this.x = f[0]; this.y = f[1]; this.z = nd.getLayer().getZ(); } @Override public final int hashCode() { return 0; } /** Compares only the node's world coordinates. */ @Override public final boolean equals(Object ob) { final WNode o = (WNode)ob; return same(x, o.x) && same(y, o.y) && same(z, o.z); } private final boolean same(final float f1, final float f2) { return Math.abs(f1 - f2) < 0.01; } private final boolean same(final double f1, final double f2) { return Math.abs(f1 - f2) < 0.01; } } /* static private final HashSet<WNode> asWNodes(final Collection<Node<?>> nds, final AffineTransform aff) { final HashSet<WNode> col = new HashSet<WNode>(); for (final Node<?> nd : nds) { col.add(new WNode(nd, aff)); } return col; } */ static private final HashSet<WNode> asWNodes(final Tree<?> tree) { final HashSet<WNode> col = new HashSet<WNode>(); for (final Node<?> nd : tree.getRoot().getSubtreeNodes()) { col.add(new WNode(nd, tree.getAffineTransform())); } return col; } /** Returns three lists: the tags in common, the tags in this node but not in the other, * and the tags in the other but not in this node. */ static public List<Set<Tag>> compareTags(final Node<?> nd1, final Node<?> nd2) { final Set<Tag> tags1 = nd1.getTags(), tags2 = nd2.getTags(); if (null == tags1 && null == tags2) { return null; } final HashSet<Tag> common = new HashSet<Tag>(); if (null != tags1 && null != tags2) { common.addAll(tags1); common.retainAll(tags2); } final HashSet<Tag> only1 = new HashSet<Tag>(); if (null != tags1) { only1.addAll(tags1); if (null != tags2) only1.removeAll(tags2); } final HashSet<Tag> only2 = new HashSet<Tag>(); if (null != tags2) { only2.addAll(tags2); if (null != tags1) only2.removeAll(tags1); } final List<Set<Tag>> t = new ArrayList<Set<Tag>>(); t.add(common); t.add(only1); t.add(only2); return t; } private static Change compareTrees(final ZDisplayable zd1, final ZDisplayable zd2) { final Tree<?> t1 = (Tree<?>)zd1, t2 = (Tree<?>)zd2; Change c = new Change(zd1, zd2); // Title: if (!t1.getTitle().equals(t2.getTitle())) { c.title = true; } // Transform: if (!t2.getAffineTransform().equals(t2.getAffineTransform())) { c.transform = true; } // Data final HashSet<WNode> nds1 = asWNodes(t1); final HashMap<WNode,WNode> nds2 = new HashMap<WNode,WNode>(); for (final Node<?> nd : t2.getRoot().getSubtreeNodes()) { WNode nn = new WNode(nd, t2.getAffineTransform()); nds2.put(nn, nn); } // Which nodes are similar? final HashSet<WNode> diff = new HashSet<WNode>(nds1); diff.removeAll(nds2.keySet()); c.common_nodes = nds1.size() - diff.size(); c.n_nodes_d1 = nds1.size(); c.n_nodes_d2 = nds2.size(); c.d1_only = c.n_nodes_d1 - c.common_nodes; c.d2_only = c.n_nodes_d2 - c.common_nodes; // Same amount of nodes? c.diff = nds1.size() - nds2.size(); if (t1.getId() == t2.getId()) { Utils.log2("nds1.size(): " + nds1.size() + ", nds2.size(): " + nds2.size() + ", diff.size(): " + diff.size() + ", c.common_nodes: " + c.common_nodes + ", c.diff: " + c.diff); } // is the root the same? I.e. has it been re-rooted? if (nds1.size() > 0) { c.root = t1.getRoot().equals(t2.getRoot()); } // what about tags? for (final WNode nd1 : nds1) { final WNode nd2 = nds2.get(nd1); if (null == nd2) continue; final List<Set<Tag>> t = compareTags(nd1.nd, nd2.nd); if (null == t) continue; c.common_tags += t.get(0).size(); c.tags_1_only += t.get(1).size(); c.tags_2_only += t.get(2).size(); } return c; } private static class Row { static int COLUMNS = 17; Change c; boolean sent = false; Row(Change c) { this.c = c; } Comparable<?> getColumn(int i) { switch (i) { case 0: return c.d1.getClass().getSimpleName(); case 1: return c.d1.getId(); case 2: return c.d1.getProject().getMeaningfulTitle2(c.d1); case 3: return !c.title; case 4: return !c.transform; case 5: return !c.root; case 6: return c.common_nodes; case 7: return c.d1_only; case 8: return c.d2_only; case 9: return c.diff; case 10: return c.common_tags; case 11: return c.tags_1_only; case 12: return c.tags_2_only; case 13: return c.d2.getId(); case 14: return c.d2.getProject().getMeaningfulTitle2(c.d2); case 15: return c.identical(); case 16: return sent; default: Utils.log("Row.getColumn: Don't know what to do with column " + i); return null; } } Color getColor() { if (c.identical()) return Color.white; if (c.hasSimilarNodes()) { if (c.d1_only > 0 && c.d2_only > 0) { // Mixed changes: problem return Color.magenta; } else if (c.d1_only > 0) { // Changes only in d1 return Color.orange; } else if (c.d2_only > 0) { // Changes only in d2 return Color.pink; } // Same for tags if (c.tags_1_only > 0 && c.tags_2_only > 0) { return Color.magenta; } else if (c.tags_1_only > 0) { return Color.orange; } else if (c.tags_2_only > 0) { return Color.pink; } } return Color.red.brighter(); } public void sent() { sent = true; } static private String getColumnName(int col) { switch (col) { case 0: return "Type"; case 1: return "id 1"; case 2: return "title 1"; case 3: return "=title?"; case 4: return "=affine?"; case 5: return "=root?"; case 6: return "Nodes common"; case 7: return "N 1 only"; case 8: return "N 2 only"; case 9: return "N diff"; case 10: return "Tags common"; case 11: return "Tags 1 only tags"; case 12: return "Tags 2 only tags"; case 13: return "id 2"; case 14: return "title 2"; case 15: return "Identical?"; case 16: return "sent"; default: Utils.log("Row.getColumnName: Don't know what to do with column " + col); return null; } } static private int getSentColumn() { return Row.COLUMNS -1; } } private static void makeGUI(final Project p1, final Project p2, HashSet<ZDisplayable> empty1, HashSet<ZDisplayable> empty2, HashMap<Displayable,List<Change>> matched, HashSet<ZDisplayable> unmatched1, HashSet<ZDisplayable> unmatched2) { final ArrayList<Row> rows = new ArrayList<Row>(); for (Map.Entry<Displayable,List<Change>> e : matched.entrySet()) { for (Change c : e.getValue()) { rows.add(new Row(c)); } if (e.getValue().size() > 1) { Utils.log("More than one assigned to " + e.getKey()); } } JTabbedPane tabs = new JTabbedPane(); final Table table = new Table(); tabs.addTab("Matched", new JScrollPane(table)); JTable tu1 = createTable(unmatched1, "Unmatched 1", p1, p2); JTable tu2 = createTable(unmatched2, "Unmatched 2", p1, p2); JTable tu3 = createTable(empty1, "Empty 1", p1, p2); JTable tu4 = createTable(empty2, "Empty 2", p1, p2); tabs.addTab("Unmatched 1", new JScrollPane(tu1)); tabs.addTab("Unmatched 2", new JScrollPane(tu2)); tabs.addTab("Empty 1", new JScrollPane(tu3)); tabs.addTab("Empty 2", new JScrollPane(tu4)); for (int i=0; i<tabs.getTabCount(); i++) { if (null == tabs.getTabComponentAt(i)) { Utils.log2("null at " + i); continue; } tabs.getTabComponentAt(i).setPreferredSize(new Dimension(1024, 768)); } String xml1 = new File(((FSLoader)p1.getLoader()).getProjectXMLPath()).getName(); String xml2 = new File(((FSLoader)p2.getLoader()).getProjectXMLPath()).getName(); JFrame frame = new JFrame("1: " + xml1 + " || 2: " + xml2); tabs.setPreferredSize(new Dimension(1024, 768)); frame.getContentPane().add(tabs); frame.pack(); frame.setVisible(true); // so the bullshit starts: any other way to set the model fails, because it tries to render it while setting it SwingUtilities.invokeLater(new Runnable() { public void run() { table.setModel(new Model(rows)); CustomCellRenderer cc = new CustomCellRenderer(); for (int i=0; i<Row.COLUMNS; i++) { table.setDefaultRenderer(table.getColumnClass(i), cc); } }}); } static private class TwoColumnModel extends AbstractTableModel { private static final long serialVersionUID = 1L; final List<ZDisplayable> items = new ArrayList<ZDisplayable>(); final boolean[] sent; TwoColumnModel(final HashSet<ZDisplayable> ds, final String title) { items.addAll(ds); sent = new boolean[items.size()]; } @Override public boolean isCellEditable(int row, int col) { return false; } @Override public void setValueAt(Object value, int row, int col) {} @Override public int getColumnCount() { return 2; } @Override public int getRowCount() { return items.size(); } @Override public Object getValueAt(int row, int col) { switch (col) { case 0: return items.get(row); case 1: return sent[row]; default: return null; } } @Override public String getColumnName(int col) { switch (col) { case 0: return "unmatched"; case 1: return "sent"; default: return null; } } } static private JTable createTable(final HashSet<ZDisplayable> hs, final String column_title, final Project p1, final Project p2) { final TwoColumnModel tcm = new TwoColumnModel(hs, column_title); final JTable table = new JTable(tcm); table.setDefaultRenderer(table.getColumnClass(0), new DefaultTableCellRenderer() { private static final long serialVersionUID = 1L; @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { final Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (1 == column && tcm.sent[row]) { c.setBackground(Color.green); c.setForeground(Color.white); } else if (isSelected) { c.setForeground(table.getSelectionForeground()); c.setBackground(table.getSelectionBackground()); } else { c.setBackground(Color.white); c.setForeground(Color.black); } return c; } }); table.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent me) { final JTable src = (JTable)me.getSource(); final TwoColumnModel model = (TwoColumnModel)src.getModel(); final int row = src.rowAtPoint(me.getPoint()), col = src.columnAtPoint(me.getPoint()); if (2 == me.getClickCount()) { Object ob = model.getValueAt(row, col); if (ob instanceof ZDisplayable) { ZDisplayable zd = (ZDisplayable)ob; Display df = Display.getOrCreateFront(zd.getProject()); df.show(zd.getFirstLayer(), zd, true, false); // also select } } else if (me.isPopupTrigger()) { JPopupMenu popup = new JPopupMenu(); final JMenuItem send = new JMenuItem("Send selection"); popup.add(send); send.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent ae) { ArrayList<ZDisplayable> col = new ArrayList<ZDisplayable>(); for (final int i : src.getSelectedRows()) { col.add((ZDisplayable)model.getValueAt(i, 0)); } if (col.isEmpty()) return; Project target = col.get(0).getProject() == p1 ? p2 : p1; // the other LayerSet ls = target.getRootLayerSet(); ArrayList<ZDisplayable> copies = new ArrayList<ZDisplayable>(); for (ZDisplayable zd : col) { copies.add((ZDisplayable) zd.clone(target, false)); model.sent[row] = true; } // 1. To the LayerSet: ls.addAll(copies); // 2. To the ProjectTree: target.getProjectTree().insertSegmentations(copies); // Update: model.fireTableDataChanged(); } }); popup.show(table, me.getX(), me.getY()); } } }); return table; } static private final class Model extends AbstractTableModel { private static final long serialVersionUID = 1L; ArrayList<Row> rows; Model(ArrayList<Row> rows) { this.rows = rows; } public void sortByColumn(final int column, final boolean descending) { final ArrayList<Row> rows = new ArrayList<Row>(this.rows); Collections.sort(rows, new Comparator<Row>() { @SuppressWarnings("unchecked") public final int compare(Row o1, Row o2) { if (descending) { Row tmp = o1; o1 = o2; o2 = tmp; } Comparable<?> val1 = getValueAt(rows.indexOf(o1), column); Comparable<?> val2 = getValueAt(rows.indexOf(o2), column); return ((Comparable)val1).compareTo((Comparable)val2); } }); this.rows = rows; // swap fireTableDataChanged(); fireTableStructureChanged(); } @Override public Comparable<?> getValueAt(int row, int col) { return rows.get(row).getColumn(col); } @Override public int getRowCount() { if (null == rows) return 0; return rows.size(); } @Override public int getColumnCount() { return Row.COLUMNS; } @Override public boolean isCellEditable(int row, int col) { return false; } @Override public void setValueAt(Object value, int row, int col) {} @Override public String getColumnName(int col) { return Row.getColumnName(col); } } static private final class CustomCellRenderer extends DefaultTableCellRenderer { private static final long serialVersionUID = 1L; public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { final Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); final Row r = ((Model)table.getModel()).rows.get(row); if (Row.getSentColumn() == column && r.sent) { c.setForeground(Color.white); c.setBackground(Color.green); return c; } if (isSelected) { setForeground(table.getSelectionForeground()); setBackground(table.getSelectionBackground()); } else { c.setForeground(Color.black); c.setBackground(r.getColor()); } return c; } } static private final class Table extends JTable { private static final long serialVersionUID = 1L; Table() { super(); setSelectionMode(ListSelectionModel.SINGLE_SELECTION); getTableHeader().addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent me) { // mousePressed would fail to repaint due to asynch issues if (2 != me.getClickCount()) return; int viewColumn = getColumnModel().getColumnIndexAtX(me.getX()); int column = convertColumnIndexToModel(viewColumn); if (-1 == column) return; ((Model)getModel()).sortByColumn(column, me.isShiftDown()); } }); this.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent me) { final int row = Table.this.rowAtPoint(me.getPoint()); //final int col = Table.this.columnAtPoint(me.getPoint()); final Row r = ((Model)getModel()).rows.get(row); if (Utils.isPopupTrigger(me)) { JPopupMenu popup = new JPopupMenu(); final JMenuItem replace12 = new JMenuItem("Replace 1 with 2"); popup.add(replace12); final JMenuItem replace21 = new JMenuItem("Replace 2 with 1"); popup.add(replace21); popup.addSeparator(); final JMenuItem sibling12 = new JMenuItem("Add 1 as sibling of 2"); popup.add(sibling12); final JMenuItem sibling21 = new JMenuItem("Add 2 as sibling of 1"); popup.add(sibling21); popup.addSeparator(); final JMenuItem select = new JMenuItem("Select each in its own display"); popup.add(select); final JMenuItem select2 = new JMenuItem("Select and center each in its own display"); popup.add(select2); final JMenuItem show3D = new JMenuItem("Show both in 3D"); popup.add(show3D); ActionListener listener = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (select == e.getSource()) { select(r.c.d1); select(r.c.d2); } else if (select2 == e.getSource()) { select2(r.c.d1); select2(r.c.d2); } else if (show3D == e.getSource()) { show3D(r.c.d1); show3D(r.c.d2); } else if (replace12 == e.getSource()) { // Replace 1 (old) with 2 (new_) if (replace(r.c.d1, r.c.d2)) { r.sent(); } } else if (replace21 == e.getSource()) { // Replace 2 (old) with 1 (new_) if (replace(r.c.d2, r.c.d1)) { r.sent(); } } else if (sibling12 == e.getSource()) { // add 1 (new_) as sibling of 2 (old) if (addAsSibling(r.c.d2, r.c.d1)) { r.sent(); } } else if (sibling21 == e.getSource()) { // add 2 (new_) as sibling of 1 (old) if (addAsSibling(r.c.d1, r.c.d2)) { r.sent(); } } } }; for (final JMenuItem item : new JMenuItem[]{select, select2, show3D, replace12, replace21, sibling12, sibling21}) { item.addActionListener(listener); } popup.show(Table.this, me.getX(), me.getY()); } } }); } private void select(ZDisplayable d) { Display display = Display.getFront(d.getProject()); if (null == display) { Utils.log("No displays open for project " + d.getProject()); } else { display.select(d); } } private void select2(ZDisplayable d) { Display display = Display.getFront(d.getProject()); if (null == display) { Utils.log("No displays open for project " + d.getProject()); } else { Display.showCentered(d.getFirstLayer(), d, true, false); // also select } } private void show3D(Displayable d) { Display3D.show(d.getProject().findProjectThing(d)); } /** Replace old with new in old's project. */ private boolean replace(ZDisplayable old, ZDisplayable new_) { String xml_old = new File(((FSLoader)old.getProject().getLoader()).getProjectXMLPath()).getName(); String xml_new = new File(((FSLoader)new_.getProject().getLoader()).getProjectXMLPath()).getName(); if (!Utils.check("Really replace " + old + " (" + xml_old + ")\n" + "with " + new_ + " (" + xml_new + ") ?")) { return false; } LayerSet ls = old.getLayerSet(); ls.addChangeTreesStep(); // addCopyAsSibling(old, new_); old.getProject().remove(old); // ls.addChangeTreesStep(); Utils.log("Replaced " + old + " (from " + xml_old + ")\n" + " with " + new_ + " (from " + xml_new + ")"); update(); return true; } /** Add @param to_copy as a sibling of @param old. */ private boolean addAsSibling(ZDisplayable old, ZDisplayable to_copy) { LayerSet ls = old.getLayerSet(); ls.addChangeTreesStep(); addCopyAsSibling(old, to_copy); ls.addChangeTreesStep(); update(); return true; } private void addCopyAsSibling(ZDisplayable old, ZDisplayable to_copy) { // Clone the sibling into the old's project ZDisplayable copy = (ZDisplayable) to_copy.clone(old.getProject(), false); old.getLayerSet().add(copy); old.getProject().getProjectTree().addSibling(old, copy); } private void update() { ((Model)getModel()).fireTableDataChanged(); ((Model)getModel()).fireTableStructureChanged(); } } }