/* * This file is part of Caliph & Emir. * * Caliph & Emir 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 2 of the License, or * (at your option) any later version. * * Caliph & Emir 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 Caliph & Emir; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Copyright statement: * -------------------- * (c) 2002-2005 by Mathias Lux (mathias@juggle.at) * http://www.juggle.at, http://caliph-emir.sourceforge.net */ package at.lux.fotoretrieval.panels; import at.knowcenter.caliph.objectcatalog.graphics.Arrow; import at.lux.fotoannotation.AnnotationFrame; import at.lux.fotoannotation.IconCache; import at.lux.fotoretrieval.EmirConfiguration; import at.lux.fotoretrieval.RetrievalFrame; import at.lux.fotoretrieval.lucene.Node; import at.lux.fotoretrieval.retrievalengines.LucenePathIndexRetrievalEngine; import at.lux.fotoretrieval.retrievalengines.LuceneRetrievalEngine; import at.lux.fotoretrieval.retrievalengines.RetrievalEngineFactory; import at.lux.graphviz.LabeledEdge; import at.lux.graphviz.LabeledNode; import at.lux.graphviz.SpringEmbedder; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.util.*; /** * Date: 04.01.2005 * Time: 17:32 * * @author Mathias Lux, mathias@juggle.at */ public class GraphConstructionPanel extends JPanel implements MouseListener, MouseMotionListener, ActionListener { private LinkedList<LabeledNode> semanticObjects = new LinkedList<LabeledNode>(); private LinkedList<LabeledEdge> semanticRelations = new LinkedList<LabeledEdge>(); private HashMap<String, LabeledNode> label2Node = new HashMap<String, LabeledNode>(); private SpringEmbedder embedder = new SpringEmbedder(semanticObjects, semanticRelations); private HashMap<Point2D.Double, LabeledNode> point2node = new HashMap<Point2D.Double, LabeledNode>(); private HashMap<Arrow, LabeledEdge> shape2edge = new HashMap<Arrow, LabeledEdge>(); private static Color BACKGROUND_COLOR = new Color(152, 181, 255); public static final Color ARROW_COLOR = new Color(36, 74, 200); public static final Color NODE_COLOR = Color.green; private double OFFSET_X = EmirConfiguration.getInstance().getDouble("GraphConstructionPanel.EdgeOffset.x"); private double OFFSET_Y = EmirConfiguration.getInstance().getDouble("GraphConstructionPanel.EdgeOffset.y"); private String lastClickedLabel = null; private Point2D.Double lastClickedPoint = null; private Line2D.Double lastDraggedLine = null; private EmbedderThread embedderThread = null; private String[] relationArray; private LabeledNode currentNode; private JMenuItem helpMenuItem, removeNodeMenuItem; private HashMap<LabeledNode, Point.Double> nodeLocation; private boolean embedded = false; private LabeledNode currentMoveNode; private IconCache iconCache = IconCache.getInstance(); /** * Creates a new <code>JPanel</code> with a double buffer * and a flow layout. */ public GraphConstructionPanel() { init(); } private void init() { addMouseListener(this); addMouseMotionListener(this); LinkedList<String> relations = new LinkedList<String>(); relations.add("* any relation"); relations.add("membershipFunction"); for (String relation : LuceneRetrievalEngine.relationMapping.keySet()) { String inverse = LuceneRetrievalEngine.relationMapping.get(relation); relations.add(relation); relations.add(inverse); } Collections.sort(relations); relationArray = new String[1]; relationArray = relations.toArray(relationArray); helpMenuItem = new JMenuItem("Help"); helpMenuItem.addActionListener(this); helpMenuItem.setActionCommand("showHelp"); helpMenuItem.setIcon(IconCache.getInstance().getHelpIcon()); removeNodeMenuItem = new JMenuItem("Remove Node"); removeNodeMenuItem.addActionListener(this); removeNodeMenuItem.setActionCommand("removeNode"); removeNodeMenuItem.setIcon(new ImageIcon(AnnotationFrame.class.getResource("data/delete_obj.gif"))); nodeLocation = new HashMap<LabeledNode, Point.Double>(); } public void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // draw background: g2.setColor(BACKGROUND_COLOR); g2.fillRect(0, 0, getWidth(), getHeight()); g2.setColor(Color.black); Font nFont = new Font("Verdana", Font.ITALIC, 9); g2.setFont(nFont); g2.setFont(nFont.deriveFont(Font.PLAIN)); double maxWidth = getWidth() - 2d * OFFSET_X; double maxHeight = getHeight() - 2d * OFFSET_Y; double xMin = 1.0, xMax = 0.0, yMin = 1.0, yMax = 0.0; for (LabeledNode node : semanticObjects) { if (node.getX() < xMin) xMin = node.getX(); if (node.getX() > xMax) xMax = node.getX(); if (node.getY() < yMin) yMin = node.getY(); if (node.getY() > yMax) yMax = node.getY(); } if (!embedded) embedElements(xMin, xMax, maxWidth, yMin, yMax, maxHeight); shape2edge = new HashMap<Arrow, LabeledEdge>(semanticRelations.size()); for (LabeledEdge edge : semanticRelations) { Point.Double src = nodeLocation.get(edge.getStartNode()); Point.Double tgt = nodeLocation.get(edge.getEndNode()); drawEdge(g2, edge, src, tgt); double moveX = tgt.x - src.x; double moveY = tgt.y - src.y; moveX *= .5; moveY *= .5; Point2D.Double lPoint = new Point2D.Double(src.x + moveX, src.y + moveY); drawLabel(g2, edge.getLabel(), lPoint, ARROW_COLOR); } for (LabeledNode labeledNode : nodeLocation.keySet()) { drawNode(g2, labeledNode, nodeLocation.get(labeledNode)); } point2node = new HashMap<Point2D.Double, LabeledNode>(nodeLocation.keySet().size()); for (LabeledNode node : nodeLocation.keySet()) { point2node.put(nodeLocation.get(node), node); } // draw dragged line: if (lastDraggedLine != null) { g2.setColor(Color.gray); g2.draw(lastDraggedLine); } } private void embedElements(double xMin, double xMax, double maxWidth, double yMin, double yMax, double maxHeight) { if (semanticObjects.size() == 1) { LabeledNode node = semanticObjects.getFirst(); Point.Double point = new Point.Double(0, 0); point.x = (getWidth() >> 1); point.y = (getHeight() >> 1); nodeLocation.put(node, point); } else if (semanticObjects.size() == 2) { LabeledNode node = semanticObjects.get(0); Point.Double point = new Point.Double(0, 0); point.x = (OFFSET_X); point.y = (getHeight() / 2); nodeLocation.put(node, point); node = semanticObjects.get(1); point = new Point.Double(0, 0); point.x = (getWidth() - OFFSET_X); point.y = (getHeight() / 2); nodeLocation.put(node, point); } else if (semanticObjects.size() == 3) { LabeledNode node = semanticObjects.get(0); Point.Double point = new Point.Double(0, 0); point.x = (OFFSET_X); point.y = (OFFSET_Y); nodeLocation.put(node, point); node = semanticObjects.get(1); point = new Point.Double(0, 0); point.x = (getWidth() - OFFSET_X); point.y = (OFFSET_Y); nodeLocation.put(node, point); node = semanticObjects.get(2); point = new Point.Double(0, 0); point.x = (getWidth() / 2); point.y = (getHeight() - OFFSET_Y); nodeLocation.put(node, point); } else { for (LabeledNode node : semanticObjects) { double x = (((node.getX() - xMin) / (xMax - xMin)) * maxWidth + OFFSET_X); double y = (((node.getY() - yMin) / (yMax - yMin)) * maxHeight + OFFSET_Y); Point.Double point = new Point.Double(x, y); nodeLocation.put(node, point); } } if (embedderThread == null || (embedderThread != null && !embedderThread.isEmbedderRunning())) { embedded = true; } } private void drawEdge(Graphics2D g2, LabeledEdge edge, Point2D.Double src, Point2D.Double tgt) { g2.setColor(ARROW_COLOR); double moveX = tgt.x - src.x; double moveY = tgt.y - src.y; double length = Math.sqrt(moveX * moveX + moveY * moveY); moveX *= 10 / length; moveY *= 10 / length; Point2D.Double target = new Point2D.Double(tgt.x - moveX, tgt.y - moveY); Line2D line = new Line2D.Double(src, target); Arrow arrow = new Arrow(line, 4d); shape2edge.put(arrow, edge); g2.fill(arrow); g2.setColor(Color.black); } private void drawNode(Graphics2D g2, LabeledNode node, Point.Double point) { int x = (int) (point.x - 10); int y = (int) (point.y - 10); // System.out.println(x + ", " + y); g2.setColor(NODE_COLOR); g2.fillOval(x, y, 20, 20); g2.setColor(ARROW_COLOR); g2.drawOval(x, y, 20, 20); Point2D.Double labelPoint; if ((getHeight() - point.y) > point.y) labelPoint = new Point2D.Double(point.x, point.y - 13); else labelPoint = new Point2D.Double(point.x, point.y + 13 + g2.getFontMetrics().getHeight()); drawLabel(g2, node.getLabel(), labelPoint); } // draws the label inside an alpha blended box ... private static void drawLabel(Graphics2D g2, String label, Point.Double point) { drawLabel(g2, label, point, Color.white); } private static void drawLabel(Graphics2D g2, String label, Point.Double point, Color boxColor) { Composite comp = g2.getComposite(); float alpha = 0.45f; int type = AlphaComposite.SRC_OVER; AlphaComposite composite = AlphaComposite.getInstance(type, alpha); g2.setComposite(composite); g2.setColor(boxColor); Rectangle2D bounds = g2.getFontMetrics().getStringBounds(label, g2); int fillRectX = (int) point.x - (((int) bounds.getWidth()) >> 1) - 4; int fillRectY = (int) point.y - ((int) bounds.getHeight() + 1); int fillRectWidth = (int) bounds.getWidth() + 8; int fillRectHeight = (int) bounds.getHeight() + 2; RoundRectangle2D.Double labelBackGround = new RoundRectangle2D.Double(fillRectX, fillRectY, fillRectWidth, fillRectHeight, 12.0, 12.0); g2.fill(labelBackGround); // g2.fillRect(fillRectX, fillRectY, fillRectWidth, fillRectHeight); g2.setComposite(comp); g2.setColor(Color.black); int x = (int) point.x - ((int) bounds.getWidth() >> 1); g2.drawString(label, x, (int) point.y - 2); } public void addNode(String label) { if (!label2Node.keySet().contains(label)) { try { if (embedderThread != null) { embedderThread.stopEmbedding(); embedderThread.join(); } LabeledNode node = new LabeledNode(Math.random(), Math.random(), label); semanticObjects.add(node); label2Node.put(label, node); embedder = new SpringEmbedder(semanticObjects, semanticRelations); embedGraph(); } catch (InterruptedException e) { e.printStackTrace(); } } } public void removeNode(String label) { LabeledNode node = label2Node.get(label); removeNode(node); } private void removeNode(LabeledNode node) { String label = node.getLabel(); try { if (embedderThread != null) { embedderThread.stopEmbedding(); embedderThread.join(); embedderThread = null; } semanticObjects.remove(node); label2Node.remove(label); nodeLocation.remove(node); LinkedList<LabeledEdge> toRemove = new LinkedList<LabeledEdge>(); for (LabeledEdge edge : semanticRelations) { if (edge.getEndNode().equals(node) || edge.getStartNode().equals(node)) { toRemove.add(edge); } } for (LabeledEdge edge : toRemove) { removeEdge(edge); } embedGraph(); } catch (InterruptedException e) { e.printStackTrace(); } } /** * Use to stop embedder, remove an edge and start embedder :) * * @param edge */ public void removeRelation(LabeledEdge edge) { try { // stop the thread and join it until it dies :) // dirty but synchronized :) if (embedderThread != null) { embedderThread.stopEmbedding(); embedderThread.join(); embedderThread = null; } removeEdge(edge); embedder = new SpringEmbedder(semanticObjects, semanticRelations); embedGraph(); } catch (InterruptedException e) { e.printStackTrace(); } } /** * Use when embedder is stoppeg, eg. from inside removeNodeMenuItem() * * @param edge */ private void removeEdge(LabeledEdge edge) { semanticRelations.remove(edge); // embedder = new SpringEmbedder(semanticObjects, semanticRelations); // embedGraph(); } public void addRelation(String label, LabeledNode src, LabeledNode tgt) { semanticRelations.add(new LabeledEdge(src, tgt, label)); embedder = new SpringEmbedder(semanticObjects, semanticRelations); embedGraph(); } public void addRelation(String label, String src, String tgt) { semanticRelations.add(new LabeledEdge(label2Node.get(src), label2Node.get(tgt), label)); embedder = new SpringEmbedder(semanticObjects, semanticRelations); embedGraph(); } public void embedGraph() { if (embedderThread != null) { embedderThread.stopEmbedding(); try { embedderThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } embedded = false; if (semanticObjects.size() > 3) { embedderThread = new EmbedderThread(embedder, this); embedderThread.start(); } else { embedded = false; repaint(); } } /** * Invoked when the mouse button has been clicked (pressed * and released) on a component. */ public void mouseClicked(MouseEvent e) { if ((e.getButton() == MouseEvent.BUTTON3 && e.isControlDown())) { double x = e.getPoint().x; double y = e.getPoint().y; // check for nodes ... for (Point2D.Double p : point2node.keySet()) { if (Math.abs(p.x - x) <= 10 && Math.abs(p.y - y) <= 10) { removeNode(point2node.get(p).getLabel()); } } // check for relations: LabeledEdge toRemove = null; for (Arrow arrow : shape2edge.keySet()) { if (arrow.contains(e.getPoint())) { toRemove = shape2edge.get(arrow); } } if (toRemove != null) removeRelation(toRemove); } else if (e.getButton() == MouseEvent.BUTTON3) { double x = e.getPoint().x; double y = e.getPoint().y; // check for nodes ... JPopupMenu menu = new JPopupMenu(); for (Point2D.Double p : point2node.keySet()) { if (Math.abs(p.x - x) <= 10 && Math.abs(p.y - y) <= 10) { currentNode = point2node.get(p); java.util.List<Node> nodes = getNodes(currentNode.getLabel()); for (Node node : nodes) { JMenuItem menuItem = createMenuItemFromNode(node); menu.add(menuItem); } if (nodes.size() > 0) menu.add(new JSeparator()); menu.add(removeNodeMenuItem); menu.add(new JSeparator()); } } menu.add(helpMenuItem); menu.show(e.getComponent(), e.getX(), e.getY()); } } private JMenuItem createMenuItemFromNode(Node node) { JMenuItem menuItem = new JMenuItem(); StringBuilder labelBuilder = new StringBuilder(64).append(node.getLabel()); if (node.getType() != null) { if (node.getType().startsWith("Agent")) { menuItem.setIcon(iconCache.getAgentIcon()); } else if (node.getType().contains("Place")) { menuItem.setIcon(iconCache.getPlaceIcon()); } else if (node.getType().contains("Event")) { menuItem.setIcon(iconCache.getEventIcon()); } else if (node.getType().contains("Object")) { menuItem.setIcon(iconCache.getObjectIcon()); } else if (node.getType().contains("Time")) { menuItem.setIcon(iconCache.getTimeIcon()); } else { labelBuilder.append(" (").append(node.getType().replaceAll("Type", "")).append(')'); } } menuItem.setText(labelBuilder.toString()); StringBuilder builder = new StringBuilder(64); builder.append("label:\"").append(node.getLabel()); builder.append("\" id:").append(node.getNodeID()); builder.append(" type:").append(node.getType()); menuItem.addActionListener(this); menuItem.setActionCommand("setLabel|" + builder.toString()); return menuItem; } /** * Invoked when a mouse button has been pressed on a component. */ public void mousePressed(MouseEvent e) { if (embedderThread != null) { embedderThread.stopEmbedding(); try { embedderThread.join(); } catch (InterruptedException e1) { e1.printStackTrace(); } embedderThread = null; } double x = e.getPoint().x; double y = e.getPoint().y; if ((e.getButton() == MouseEvent.BUTTON1)) { for (Point2D.Double p : point2node.keySet()) { if (Math.abs(p.x - x) <= 10 && Math.abs(p.y - y) <= 10) { currentMoveNode = point2node.get(p); } } } else if ((e.getButton() == MouseEvent.BUTTON2) || (e.getButton() == MouseEvent.BUTTON3 && e.isAltDown())) { lastClickedLabel = null; lastClickedPoint = null; for (Point2D.Double p : point2node.keySet()) { if (Math.abs(p.x - x) <= 10 && Math.abs(p.y - y) <= 10) { lastClickedLabel = point2node.get(p).getLabel(); lastClickedPoint = p; } } } } /** * Invoked when a mouse button has been released on a component. */ public void mouseReleased(MouseEvent e) { if (lastClickedLabel != null && lastClickedPoint != null) { double x = e.getPoint().x; double y = e.getPoint().y; String tgtLabel; for (Point2D.Double p : point2node.keySet()) { if (Math.abs(p.x - x) <= 10 && Math.abs(p.y - y) <= 10) { tgtLabel = point2node.get(p).getLabel(); if (!tgtLabel.equals(lastClickedLabel)) { // add new Relation: Object label = JOptionPane.showInputDialog(this, "Please specify relation:", "Add semantic relation", JOptionPane.PLAIN_MESSAGE, null, relationArray, relationArray[0]); addRelation(label.toString(), lastClickedLabel, tgtLabel); } } } lastClickedLabel = null; lastClickedPoint = null; lastDraggedLine = null; repaint(); } else if (currentMoveNode != null) { currentMoveNode = null; repaint(); } } /** * Invoked when the mouse enters a component. */ public void mouseEntered(MouseEvent e) { } /** * Invoked when the mouse exits a component. */ public void mouseExited(MouseEvent e) { } /** * Invoked when a mouse button is pressed on a component and then * dragged. <code>MOUSE_DRAGGED</code> events will continue to be * delivered to the component where the drag originated until the * mouse button is released (regardless of whether the mouse position * is within the bounds of the component). * <p/> * Due to platform-dependent Drag&Drop implementations, * <code>MOUSE_DRAGGED</code> events may not be delivered during a native * Drag&Drop operation. */ public void mouseDragged(MouseEvent e) { lastDraggedLine = null; if (lastClickedLabel != null && lastClickedPoint != null) { lastDraggedLine = new Line2D.Double(e.getPoint(), lastClickedPoint); repaint(); } else if (currentMoveNode != null) { nodeLocation.put(currentMoveNode, new Point2D.Double(e.getPoint().getX(), e.getPoint().getY())); repaint(); } } /** * Invoked when the mouse cursor has been moved onto a component * but no buttons have been pushed. */ public void mouseMoved(MouseEvent e) { } /** * Creates the search string for searching in the graph list. * * @return the search String for searching in the Graph list */ public String getSearchString() { StringBuilder sb = new StringBuilder(64); int count = 1; HashMap<String, Integer> label2position = new HashMap<String, Integer>(semanticObjects.size()); for (LabeledNode node : semanticObjects) { String label = node.getLabel(); // for emir: replace all numbers, which are in brackets :) label = label.replaceAll(" \\x28\\d\\x29", ""); sb.append('['); sb.append(label); sb.append(']'); sb.append(' '); label2position.put(node.getLabel(), count); count++; } for (LabeledEdge edge : semanticRelations) { // sb.append('['); String label = edge.getLabel(); if (label.indexOf('*') > -1) label = "\\w*"; sb.append(label); sb.append(' '); LabeledNode src = (LabeledNode) edge.getStartNode(); sb.append(label2position.get(src.getLabel())); sb.append(' '); LabeledNode tgt = (LabeledNode) edge.getEndNode(); sb.append(label2position.get(tgt.getLabel())); // sb.append(']'); sb.append(' '); } return sb.toString().trim(); } /** * simple method to create a search string for searching within the 2-path index. * * @return string for searching within the 2-path index. */ public String getPathSearchString() { StringBuilder sb = new StringBuilder(256); int count = 1; HashMap<String, Integer> label2position = new HashMap<String, Integer>(semanticObjects.size()); for (LabeledNode node : semanticObjects) { String label = node.getLabel(); if (label.startsWith(GraphSearchPanel.ANONYMOUS_NODE_NAME)) { label = "*"; } // for emir: replace all numbers, which are in brackets :) label = label.replaceAll(" \\x28\\d\\x29", ""); sb.append('['); sb.append(label); sb.append(']'); sb.append(' '); label2position.put(node.getLabel(), count); count++; } for (LabeledEdge edge : semanticRelations) { String label = edge.getLabel(); if (label.indexOf("*") > -1) label = "*"; sb.append(label); sb.append(' '); LabeledNode src = (LabeledNode) edge.getStartNode(); sb.append(label2position.get(src.getLabel())); sb.append(' '); LabeledNode tgt = (LabeledNode) edge.getEndNode(); sb.append(label2position.get(tgt.getLabel())); sb.append(' '); } return sb.toString().trim(); } public static java.util.List<Node> getNodes(String label) { return ((LucenePathIndexRetrievalEngine) RetrievalEngineFactory.getPathIndexRetrievalEngine()).getNodes(label, RetrievalFrame.BASE_DIRECTORY); } /** * Invoked when an action occurs. */ public void actionPerformed(ActionEvent e) { String cmd = e.getActionCommand(); if (cmd.equals("showHelp")) { showHelp(); } else if (cmd.equals("removeNode")) { removeNode(currentNode); currentNode = null; } else if (cmd.startsWith("setLabel|")) { String newLabel = cmd.substring("setLabel|".length()); // remove old node in label table label2Node.remove(currentNode.getLabel()); // add it with new label: label2Node.put(newLabel, currentNode); currentNode.setLabel(newLabel); repaint(); } } /** * Shows basic help for using this panel. */ private void showHelp() { // g2.drawString("Help:", HELP_STRING_OFFSET_X, getHeight() - 28); // g2.drawString("<Alt> + right mouse click on object to remove node or edge.", HELP_STRING_OFFSET_X, getHeight() - 18); // g2.drawString("Left mouse click on nodes and drag to other nodes to create edges.", HELP_STRING_OFFSET_X, getHeight() - 8); String helpString = "<Ctrl> + right mouse click on object to remove node or edge.\n" + "Middle mouse click or <Alt> + right mouse click on nodes and drag to other nodes to create edges."; JOptionPane.showMessageDialog(this, helpString, "Help", JOptionPane.INFORMATION_MESSAGE); } } class EmbedderThread extends Thread { private SpringEmbedder embedder; private boolean embedderRunning = true; private JPanel parent; public EmbedderThread(SpringEmbedder embedder, JPanel parent) { this.embedder = embedder; this.parent = parent; } public void run() { while (embedderRunning && embedder.step() > 0) { parent.repaint(); try { sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } // Thread.currentThread().wait(40); } embedderRunning = false; } public void stopEmbedding() { embedderRunning = false; } public boolean isEmbedderRunning() { return embedderRunning; } }