/* * CCVisu is a tool for visual graph clustering * and general force-directed graph layout. * This file is part of CCVisu. * * Copyright (C) 2005-2012 Dirk Beyer * * CCVisu is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * CCVisu 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with CCVisu; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Please find the GNU Lesser General Public License in file * license_lgpl.txt or http://www.gnu.org/licenses/lgpl.txt * * Dirk Beyer (firstname.lastname@uni-passau.de) * University of Passau, Bavaria, Germany */ package org.sosy_lab.ccvisu.writers; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Stroke; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import java.util.TreeSet; import org.sosy_lab.ccvisu.DisplayCriteria; import org.sosy_lab.ccvisu.NameHandler; import org.sosy_lab.ccvisu.Options; import org.sosy_lab.ccvisu.Options.OptionsEnum; import org.sosy_lab.ccvisu.graph.GraphData; import org.sosy_lab.ccvisu.graph.GraphEdge; import org.sosy_lab.ccvisu.graph.GraphVertex; import org.sosy_lab.ccvisu.graph.GraphVertex.Shape; import org.sosy_lab.ccvisu.graph.GraphicLayoutInfo; import org.sosy_lab.ccvisu.graph.Group; import org.sosy_lab.ccvisu.graph.NameVisibility; import org.sosy_lab.ccvisu.graph.Position; import org.sosy_lab.ccvisu.ui.FrameDisplay; import org.sosy_lab.util.Colors; /** * Writer for displaying the layout on the screen device. */ public class WriterDataLayoutDISP extends WriterDataLayout { private List<Set<String>> xMap; private List<Set<String>> yMap; private List<List<Set<GraphEdge>>> edgeMap; //write-color (computed once and stored) private Color frontColor; // Temporarily associated during callback from ScreenDisplay. private Graphics graphics; private int insetleft; private int insetbottom; private int xSize = 0; private int ySize = 0; // Ashgan added the following data field, which is used to // filter the display of vertices and edges private DisplayCriteria dispFilter = null; private final FrameDisplay display; /** * Constructor. * @param graph Graph representation, contains the positions of the vertices. */ public WriterDataLayoutDISP(PrintWriter out, GraphData graph, Container frame, Options options) { super(out, graph, options); assert options.frame != null; // Adjust frontColor. adjustFrontColor(); display = new FrameDisplay(this, frame); graph.addOnGraphChangeListener(display); if (display == null) { System.err.println("Runtime error: Could not open ScreenDisplay."); System.exit(1); } // Ashgan added the following line dispFilter = new DisplayCriteria(graph); } /** * Nothing to do here. * The constructor initializes the ScreenDisplay (frame and canvas), * and that calls back to the methods below * (writeDISP, writeLAY, toggleVertexNames, getVertexNames). */ @Override public void write() { } /** * Writes the layout on the screen device (DISP output format). * Call-back method, invoked from <code>ScreenDisplay</code>. * @param size Size of the output drawing area. * @param graphics The drawing area of the canvas. * @param xCanvasSize Width of the canvas. * @param yCanvasSize Height of the canvas. * @param insetleft Left inset of the drawing frame. * @param insetbottom Bottom inset of the drawing frame. */ public void writeDISP(Dimension size, Graphics graphics, int xCanvasSize, int yCanvasSize, int insetleft, int insetbottom) { this.graphics = graphics; this.insetbottom = insetbottom; this.insetleft = insetleft; // Maps for getting the vertices at mouse positions. xMap = new ArrayList<Set<String>>(xCanvasSize); yMap = new ArrayList<Set<String>>(yCanvasSize); for (int i = 0; i < xCanvasSize; ++i) { xMap.add(new TreeSet<String>()); } for (int i = 0; i < yCanvasSize; ++i) { yMap.add(new TreeSet<String>()); } // Maps for getting the edges at mouse positions. if (options.getOption(OptionsEnum.showEdges).getBool()) { if (xSize == xCanvasSize && ySize == yCanvasSize) { for (int x = 0; x < xCanvasSize; ++x) { List<Set<GraphEdge>> lVec = edgeMap.get(x); for (int y = 0; y < yCanvasSize; ++y) { lVec.get(y).clear(); } } } else { edgeMap = new ArrayList<List<Set<GraphEdge>>>(xCanvasSize); for (int x = 0; x < xCanvasSize; ++x) { List<Set<GraphEdge>> lVec = new ArrayList<Set<GraphEdge>>(yCanvasSize); edgeMap.add(lVec); for (int y = 0; y < yCanvasSize; ++y) { lVec.add(new TreeSet<GraphEdge>()); } } xSize = xCanvasSize; ySize = yCanvasSize; } } List<GraphVertex> emptyVertices = new ArrayList<GraphVertex>(); List<GraphEdge> emptyEdges = new ArrayList<GraphEdge>(); // Write edges only writeGraphicsLayout(emptyVertices, graph.getEdges(), size); // Draw the vertices, group by group, groups on top of normal vertices. Iterator<Group> it = graph.getGroups().iterator(); while (it.hasNext()) { Group group = it.next(); if (group.isVisible()) { writeGraphicsLayout(group.getNodes(), emptyEdges, size); } } GraphicLayoutInfo graphicLayoutInfo = calculateOffsetAndScale(size); // Draw the group specific information (except default group) for (Group group : graph.getGroups()) { if (group == graph.getDefaultGroup()) { continue; } if (group.isVisible() && group.isDrawInfo()) { Position position = graphicLayoutInfo.mapToOriginal(new Position(group.getX(), group.getY(), 0)); int x = (int) (position.x + insetleft); int y = (int) (position.y - insetbottom); int left = x - 5; int right = x + 5; int top = y - 5; int bottom = y + 5; graphics.setColor(Color.BLACK); graphics.drawLine(left, top, right, bottom); graphics.drawLine(right, top, left, bottom); graphics.setColor(group.getColor()); graphics.drawLine(left, y, right, y); graphics.drawLine(x, top, x, bottom); int radius = (int) (group.getAverageRadius() * graphicLayoutInfo.getScale().x); int diam = (radius + radius); graphics.drawOval(x - radius, y - radius, diam, diam); graphics.setColor(Color.BLACK); } } if (options.writerDataGraphicsDISPListener != null) { options.writerDataGraphicsDISPListener.onUpdateImage(size, graphicLayoutInfo, graphics); } } /** * Writes a vertex on screen. * @param vertex The vertex object, to access vertex attributes. * @param xPos x coordinate of the vertex. * @param yPos y coordinate of the vertex. * @param zPos z coordinate of the vertex. * @param Width Width of the vertex. * @param height Height of the vertex. */ @Override public void writeVertex(GraphVertex vertex, int xPos, int yPos, int zPos, int width, int height, Color color) { assert (vertex.isShowVertex()); assert (!vertex.isAuxiliary()); // Correction for inset.left and inset.bottom xPos = xPos + insetleft; yPos = yPos - insetbottom; int startX = xPos - width; int startY = yPos - height; // Draw the vertex. int radius = width; int diam = 2 * width; graphics.setColor(color); if ((vertex.getShape() == Shape.BOX) || (vertex.getShape() == Shape.FIXED_SIZE_BOX)) { // BOX graphics.fillRect(startX, startY, diam, diam); if (options.getOption(OptionsEnum.blackCircle).getBool()) { graphics.setColor(Colors.get(options.getOption(OptionsEnum.ringColor).getString())); graphics.drawRect(startX, startY, diam, diam); } } else if (vertex.getShape() == Shape.DISC) { // DISC graphics.fillOval(startX, startY, diam, diam); if (options.getOption(OptionsEnum.blackCircle).getBool()) { graphics.setColor(Colors.get(options.getOption(OptionsEnum.ringColor).getString())); graphics.drawOval(startX, startY, diam, diam); } graphics.setColor(frontColor); } else if (vertex.getShape() == Shape.RBOX) { // RBox float d = diam; graphics.fillRoundRect(startX, startY, diam, diam, (int) (3 * d / 4), (int) (3 * d / 4)); // graphics.fillPolygon(xs, ys, 8); if (options.getOption(OptionsEnum.blackCircle).getBool()) { graphics.setColor(Colors.get(options.getOption(OptionsEnum.ringColor).getString())); // graphics.drawPolygon(xs, ys, 8); } } else if (vertex.getShape() == Shape.FILLER_RECT) { // FILLER_RECT graphics.fillRect(startX, startY, height, width); if (options.getOption(OptionsEnum.blackCircle).getBool()) { graphics.setColor(Colors.get(options.getOption(OptionsEnum.ringColor).getString())); graphics.drawRect(startX, startY, height, width); } } else { // UNKNOWN assert (false); } // Draw a little loop if the vertex has a self-loop. if (vertex.hasSelfLoop() && options.getOption(OptionsEnum.showEdges).getBool()) { graphics.drawOval(startX, startY, (int) ((1.8) * diam), (int) ((1.8) * diam + 4)); } if (vertex.isShowName()) { // Draw annotation. // Use inverted background color for the annotation. graphics.setColor(frontColor); String vertexLabel = vertex.getLabel(); if (options.getOption(OptionsEnum.shortNames).getBool()) { vertexLabel = NameHandler.shortenName(vertexLabel); } graphics.drawString(vertexLabel, xPos + radius + 3, yPos + 3); /* * if (options.getOption(OptionsEnum.enableFeatureVisu).getBool()) { * int res = Statistics.calculateProximityDegreeExtended(curVertex, graph); * String resStr = "(proximity degree: " + res + ")"; * int lYPos = yPos + 3 + options.getOption(OptionsEnum.fontSize).getInt(); * mGraphics.drawString(resStr, xPos + radius + 3, lYPos); * } */ } try { // For interactive annotation: Store vertex names at their positions in the maps. int endX = Math.min(xPos + radius, xMap.size() - 1); for (int pos = Math.max(startX, 0); pos <= endX; ++pos) { xMap.get(pos).add(vertex.getName()); } int endY = Math.min(yPos + radius, yMap.size() - 1); for (int pos = Math.max(startY, 0); pos <= endY; ++pos) { yMap.get(pos).add(vertex.getName()); } } catch (Exception e) { // Tolerate exception (it's because of the concurrent thread). } } /** * Writes an edge. * @param edge an edge in graph.edges * @param xPos1 x coordinate of the first point. * @param yPos1 y coordinate of the first point. * @param zPos1 z coordinate of the first point. * @param xPos2 x coordinate of the second point. * @param yPos2 y coordinate of the second point. * @param zPos2 z coordinate of the second point. */ @Override public void writeEdge(GraphEdge edge, int xPos1, int yPos1, int zPos1, int xPos2, int yPos2, int zPos2) { assert (!edge.isAuxiliaryEdge()); // reflexive edges are not allowed by specification if (xPos1 == xPos2 && yPos1 == yPos2) { return; } // Correction for inset.left xPos1 = xPos1 + insetleft; xPos2 = xPos2 + insetleft; // Correction for inset.bottom yPos1 = yPos1 - insetbottom; yPos2 = yPos2 - insetbottom; // Draw graphics.setColor(edge.getColor()); paintArrow(graphics, xPos1, yPos1, xPos2, yPos2, edge.isDashed()); // Draw the annotation if (edge.showName()) { int xPos = (xPos1 + xPos2 + options.getOption(OptionsEnum.fontSize).getInt()) / 2; int yPos = (yPos1 + yPos2 + options.getOption(OptionsEnum.fontSize).getInt()) / 2; graphics.drawString(edge.getRelName(), xPos, yPos); } // For interactive annotation: Store edges at their positions in the maps. // Using Bresenham's line algorithm. boolean steep = Math.abs(yPos2 - yPos1) > Math.abs(xPos2 - xPos1); if (steep) { int tmp = xPos1; xPos1 = yPos1; yPos1 = tmp; tmp = xPos2; xPos2 = yPos2; yPos2 = tmp; } if (xPos1 > xPos2) { int tmp = xPos1; xPos1 = xPos2; xPos2 = tmp; tmp = yPos1; yPos1 = yPos2; yPos2 = tmp; } int deltaX = xPos2 - xPos1; int deltaY = Math.abs(yPos2 - yPos1); float error = 0; float deltaError = ((float) deltaY) / deltaX; int y = yPos1; int ystep; if (yPos1 < yPos2) { ystep = 1; } else { ystep = -1; } for (int x = xPos1; x <= xPos2; x++) { try { if (steep) { if (y >= 0 && y < xSize && x >= 0 && x < ySize) { edgeMap.get(y).get(x).add(edge); } } else { if (x >= 0 && x < xSize && y >= 0 && y < ySize) { edgeMap.get(x).get(y).add(edge); } } error += deltaError; if (error >= 0.5) { y += ystep; error -= 1.0; } } catch (Exception e) { // Tolerate exception (it's because of the concurrent thread). } } } /** * Marks all vertices whose node names match the given regular expression. * Call-back method, invoked from within ScreenDisplay. * @param regEx Regular expression. */ public void markVertices(String regEx) { for (GraphVertex vertex : graph.getVertices()) { if (vertex.getName().matches(regEx)) { vertex.setColor(Color.red); vertex.setShowName(NameVisibility.Priority.FIND, true); } else { vertex.resetColor(); vertex.unsetShowName(NameVisibility.Priority.FIND); } } } /** * Resets all marked vertices. */ public void resetMarkedVertices() { for (GraphVertex vertex : graph.getVertices()) { vertex.resetColor(); vertex.unsetShowName(NameVisibility.Priority.FIND); } } /** * Toggle the showName flag of the vertices and edges at the given position. * Call-back method, invoked from within ScreenDisplay. * @param point coordinates of the vertex. * @return number of names toggled */ public int toggleNames(Point point) { int xPos = (int) point.getX(); int yPos = (int) point.getY(); int nb = 0; Set<String> tmp = new TreeSet<String>(xMap.get(xPos)); tmp.retainAll(yMap.get(yPos)); for (String name : tmp) { nb++; GraphVertex vertex = graph.getVertexByName(name); vertex.setShowName(NameVisibility.Priority.MANUAL, !vertex.isShowName()); } // edges if (options.getOption(OptionsEnum.showEdges).getBool()) { Set<GraphEdge> edgesIndex = edgeMap.get(xPos).get(yPos); for (GraphEdge edge : edgesIndex) { nb++; edge.setShowName(!edge.showName()); } } return nb; } /** * Compute list of names of the vertices and edges at the given position. * Call-back method, invoked from within ScreenDisplay. * @param point coordinates. */ public Set<String> getNames(Point point, Boolean includeEdges) { int xPos = (int) point.getX(); int yPos = (int) point.getY(); Set<String> tmp = new TreeSet<String>(); try { if (xPos >= xMap.size() || yPos >= yMap.size()) { return tmp; } tmp.addAll(xMap.get(xPos)); tmp.retainAll(yMap.get(yPos)); } catch (Exception e) { // Tolerate exception (it's because of the concurrent thread). } // Edges if ((includeEdges) && (options.getOption(OptionsEnum.showEdges).getBool())) { Set<GraphEdge> edgesIndex = edgeMap.get(xPos).get(yPos); for (GraphEdge edge : edgesIndex) { tmp.add(edge.getRelName()); } } return tmp; } /** * Restrict the set of vertices displayed on the screen to * the vertices within the given rectangular (i.e., zoom). * Call-back method, invoked from within ScreenDisplay. * @param topLeft coordinates of the top left corner of the rectangular. * @param bottomRight coordinates of the bottom right corner of the rectangular. */ public void restrictShowedVertices(Point topLeft, Point bottomRight) { Set<String> xNodes = new TreeSet<String>(); int end = (int) Math.min(bottomRight.getX(), xMap.size()); int minX = Math.max(0, (int) topLeft.getX()); for (int i = minX; i < end; i++) { xNodes.addAll(xMap.get(i)); } end = (int) Math.min(bottomRight.getY(), yMap.size()); int minY = Math.max(0, (int) topLeft.getY()); Set<String> yNodes = new TreeSet<String>(); for (int i = minY; i < end; i++) { yNodes.addAll(yMap.get(i)); } Set<String> nodesToKeep = xNodes; nodesToKeep.retainAll(yNodes); for (int i = 0; i < graph.getVertices().size(); i++) { GraphVertex vertex = graph.getVertices().get(i); if (!nodesToKeep.contains(vertex.getName())) { vertex.setShowVertex(false); } } // Ashgan starts // Every time the canvas view changes, the // undo list in the associated display filter instance // should also be updated this.dispFilter.resetUndoList(graph.getVertices()); // Ashgan ends } /** * Reset vertex restriction that was set by restrictShowedVertices. * Call-back method, invoked from within ScreenDisplay. */ public void resetRestriction() { // Handle vertex options. for (GraphVertex vertex : graph.getVertices()) { // hideSource (do not show vertex if it is source of an edge). if (options.getOption(OptionsEnum.hideSource).getBool() && vertex.isSource()) { vertex.setShowVertex(false); } else { vertex.setShowVertex(true); } } // Ashgan starts // Every time the canvas view changes, the // undo list in the associated display filter instance // should also be updated this.dispFilter.resetUndoList(graph.getVertices()); // Ashgan ends } /** * Gets the local graph representation (layout). * Call-back method, invoked from within ScreenDisplay. * @return Graph/layout representation to switch to. */ public GraphData getGraphData() { return graph; } /** * adjust frontColor */ public void adjustFrontColor() { Color backColor = options.backColor.get(); frontColor = new Color(0xffffffff - backColor.getRGB()); //problem when using gray: colors too close => hard to read //ignore alpha if (Math.abs(frontColor.getRed() - backColor.getRed()) < 10 && Math.abs(frontColor.getBlue() - backColor.getBlue()) < 10 && Math.abs(frontColor.getGreen() - backColor.getGreen()) < 10) { frontColor = Color.BLACK; } } /** * the color of the text * @return the color of the text */ public Color getWriteColor() { return frontColor; } /** * Open the name of what is under the cursor as if it is an URL. * @param point Coordinates */ public void openURL(Point point) { // TODO: Simplify! String targets = getNames(point, false).toString(); int lenght = targets.length(); if (lenght <= 2) { return; } StringTokenizer stringTokenizer = new StringTokenizer(targets.substring(1, lenght - 1)); if (stringTokenizer.hasMoreTokens()) { String url = stringTokenizer.nextToken(); if ((url.length() > 0 && url.charAt(0) == '"') && url.endsWith("\"")) { url = url.substring(1, url.length() - 1); } if (options.getOption(OptionsEnum.browser).getString().equals("")) { if (!guessBrowser(url)) { System.err.println("Unable to find browser"); return; } } else { String cmd[] = { options.getOption(OptionsEnum.browser).getString(), url }; System.err.println("opening: " + url); try { Runtime rt = Runtime.getRuntime(); rt.exec(cmd); } catch (Throwable t) { t.printStackTrace(); } } } } private boolean guessBrowser(String url) { String[] possibility = { "firefox", "mozilla", "opera", "safari", "iexplorer", "epiphany", "konqueror" }; for (int i = 0; i < possibility.length; i++) { options.getOption(OptionsEnum.browser).set(possibility[i]); String cmd[] = { options.getOption(OptionsEnum.browser).getString(), url }; try { Runtime rt = Runtime.getRuntime(); rt.exec(cmd); return true; } catch (Throwable t) { } } options.getOption(OptionsEnum.browser).set(""); return false; } /** * Draws an arrow from one 2d-coordinate to another. */ private void paintArrow(Graphics g, int x0, int y0, int x1, int y1, boolean dashed) { if (dashed) { Stroke drawingStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] { 9 }, 0); Graphics2D g2d = (Graphics2D) g; Stroke oldStroke = g2d.getStroke(); g2d.setStroke(drawingStroke); // Draw line. g2d.drawLine(x0, y0, x1, y1); g2d.setStroke(oldStroke); } else { g.drawLine(x0, y0, x1, y1); } // Draw arrow head. int[] aps = paintArrow(x0, y0, x1, y1); g.drawLine(aps[0], aps[1], aps[2], aps[3]); g.drawLine(aps[4], aps[5], aps[6], aps[7]); } /** * @return the display */ public FrameDisplay getDisplay() { return display; } // Ashgan starts /** * @param filterType <br> * 0: Display unchanged vertices<br> * 1: Display removed vertices<br> * 2: Display added vertices */ public void filterVertices(int filterType) { dispFilter.filterVertices(graph, filterType, true); } /** * @param filterType <br> * 0: Display unchanged edges<br> * 1: Display removed edges<br> * 2: Display added edges */ public void filterEdges(int filterType) { dispFilter.filterEdges(graph, filterType, true); } public void filterOff() { dispFilter.resetView(graph.getVertices()); } // Ashgan ends }