/* * 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.graph; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.EventObject; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import org.sosy_lab.ccvisu.Options.Verbosity; import org.sosy_lab.ccvisu.graph.GraphVertex.Shape; import org.sosy_lab.ccvisu.graph.Group.GroupKind; import org.sosy_lab.ccvisu.graph.interfaces.GraphEventListener; import org.sosy_lab.ccvisu.measuring.ClusterQuality; import org.sosy_lab.util.Colors; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; /** * Contains the representation of a graph. * This class is a collection of the data structures needed * for layout and transformation processing. */ public class GraphData { /** Maps a vertex id to a GraphVertex.*/ private List<GraphVertex> vertices; /** Maps a vertex name to a GraphVertex. */ private Map<String, GraphVertex> nameToVertex; /** Edges of type GraphEdgeInt. Only used if (inFormat < LAY). */ private List<GraphEdge> edges; /** Raw RSF data as extracted from input. */ private Relation tuples; /** A cache data structure for getAdjacent. */ private Map<String, Map<GraphVertex, Set<GraphEdge>>> adjacentCache; /** A cache data structure for getAdjacentUndirected. */ private Map<String, Map<GraphVertex, Set<GraphEdge>>> adjacentCacheUndirected; private float degreeMax; private float degreeOutMax; // List of groups. private List<Group> groups; private Group defaultGroup; /** Notify these observers, when changes occur in the graph */ protected Collection<GraphEventListener> onGraphChangeListeners = new ArrayList<GraphEventListener>(); public GraphData() { reset(); } public void reset() { vertices = new ArrayList<GraphVertex>(); nameToVertex = new HashMap<String, GraphVertex>(); edges = new ArrayList<GraphEdge>(); tuples = new Relation(); adjacentCache = null; adjacentCacheUndirected = null; degreeMax = 0; degreeOutMax = 0; groups = new ArrayList<Group>(); defaultGroup = null; notifyAboutGroupsChange(new EventObject(this)); notifyAboutLayoutChange(new EventObject(this)); } public void setVertices(List<GraphVertex> vertices) { this.vertices = vertices; // Create the reverse lookup (nameToVertex). for (int i = 0; i < vertices.size(); ++i) { GraphVertex graphVertex = vertices.get(i); graphVertex.setId(i); // Add vertex-to-number entry for vertex. if (nameToVertex.containsKey(graphVertex.getName())) { System.err.println("Input error: Vertex '" + graphVertex.getName() + "' exists twice in given input."); } nameToVertex.put(graphVertex.getName(), graphVertex); } // Initialize groups. initGroups(); } public List<GraphVertex> getVertices() { return Collections.unmodifiableList(vertices); } public GraphVertex getVertexByName(String vertexName) { return nameToVertex.get(vertexName); } private void insertEdge(GraphEdge edge) { GraphVertex source = edge.getSource(); GraphVertex target = edge.getTarget(); source.setDegree(source.getDegree() + edge.getWeight()); target.setDegree(target.getDegree() + edge.getWeight()); source.setDegreeOut(source.getDegreeOut() + edge.getWeight()); edge.setId(edges.size()); degreeMax = Math.max(degreeMax, source.getDegree()); degreeOutMax = Math.max(degreeOutMax, source.getDegreeOut()); edges.add(edge); } /** * @return the edges */ public List<GraphEdge> getEdges() { return ImmutableList.copyOf(edges); } /** * @return the Relation associated with this graph. */ public Relation getRelation() { return tuples; } public void applyRelation(Relation relation, Verbosity verbosity) { reset(); tuples = relation; List<GraphVertex> vertices = new ArrayList<GraphVertex>(); // Maps a vertex name to a GraphVertex. Map<String, GraphVertex> nameToVertex = new HashMap<String, GraphVertex>(); int lineNr = 0; for (Tuple tuple : relation) { lineNr++; try { List<String> adTuples = tuple.getTupleElements(); if (adTuples.size() < 2 && verbosity.isAtLeast(Verbosity.WARNING)) { System.err.println("Runtime error: Exception while reading " + "a graph edge, at line " + lineNr + ":"); System.err.println("Input graph file needs to follow the RSF format"); System.err.println("'<graph-name> <source-vertex> <target-vertex> [<weight>]'."); } // Relation name. String edgeRelName = tuple.getRelationName(); // Source vertex. String edgeSource = adTuples.get(0); // Target vertex. String edgeTarget = adTuples.get(1); // Edge weight. String edgeWeight = "1.0"; if (adTuples.size() > 2) { edgeWeight = adTuples.get(2); } GraphVertex source = createOrReturnVertex(vertices, nameToVertex, edgeSource); source.setIsSource(true); GraphVertex target = createOrReturnVertex(vertices, nameToVertex, edgeTarget); float weight = 1.0f; try { weight = Math.abs(Float.parseFloat(edgeWeight)); } catch (Exception e) { if (verbosity.isAtLeast(Verbosity.WARNINGLIGHT)) { System.err.println("RSF warning (if input is a weighted graph): " + "Float expected for relation '" + edgeRelName + "' at '" + edgeWeight + "', at line " + lineNr + "."); } weight = 1.0f; } // Insert edge to graph. // (Detection of reflexive edges is done by CCVisu.computeLayout().) GraphEdge edge = new GraphEdge(edgeRelName, source, target, weight); insertEdge(edge); if (edge.getWeight() < 0) { System.err.println("Invalid graph: edge {" + source.getName() + "," + target.getName() + "} " + "has weight: " + edge.getWeight() + "."); } } catch (Exception e) { if (verbosity.isAtLeast(Verbosity.WARNING)) { System.err.println(e.toString()); } } } setVertices(vertices); } private GraphVertex createOrReturnVertex(List<GraphVertex> vertices, Map<String, GraphVertex> nameToVertex, String vertexName) { if (nameToVertex.containsKey(vertexName)) { // Vertex already exists return nameToVertex.get(vertexName); } else{ // Insert vertex into graph. GraphVertex source = new GraphVertex(vertexName, vertices.size()); vertices.add(source); nameToVertex.put(vertexName, source); return source; } } public GraphEdge getEdgeById(int id) { for (GraphEdge edge : edges) { if (id == edge.getId()) { return edge; } } // found no edge with this id return null; } public float getMaxOutdegree() { return degreeOutMax; } public float getMaxDegree() { return degreeMax; } public void setNodesToRandomPositions(Collection<GraphVertex> nodes, int dimensions) { // Initialize with random positions. for (GraphVertex currVertex : nodes) { Position newPosition = new Position(); newPosition.x = 2 * (float) Math.random() - 1; if (dimensions >= 2) { newPosition.y = 2 * (float) Math.random() - 1; } else { newPosition.y = 0; } if (dimensions == 3) { newPosition.z = 2 * (float) Math.random() - 1; } else { newPosition.z = 0; } currVertex.setPosition(newPosition); } } public GraphVertex getVertexById(int id) { for (GraphVertex vertex : vertices) { if (id == vertex.getId()) { return vertex; } } // found no vertex with this id return null; } /** * Returns the set of adjacent edges * for a given vertex and a given relation. */ public Collection<GraphEdge> getAdjacent(GraphVertex graphVertex, String relationName) { // Initialize the map. if (adjacentCache == null) { adjacentCache = new TreeMap<String, Map<GraphVertex, Set<GraphEdge>>>(); } // Look-up the relation name. Map<GraphVertex, Set<GraphEdge>> vertexMap = adjacentCache.get(relationName); if (vertexMap == null) { vertexMap = new HashMap<GraphVertex, Set<GraphEdge>>(); adjacentCache.put(relationName, vertexMap); } // Look-up the vertex. Set<GraphEdge> edgeSet = vertexMap.get(graphVertex); if (edgeSet == null) { edgeSet = new TreeSet<GraphEdge>(); vertexMap.put(graphVertex, edgeSet); for (GraphEdge graphEdge : edges) { try { if (graphEdge.getSource() == graphVertex && graphEdge.getRelName().matches(relationName)) { edgeSet.add(graphEdge); } } catch (Exception pException) { System.err.println("Exception in RegExp match: " + pException.getMessage()); } } } return edgeSet; } /** * Returns the set of adjacent edges (incoming and outgoing) * for a given vertex and a given relation. */ public Collection<GraphEdge> getAdjacentUndirected(GraphVertex graphVertex, String relationName) { // Initialize the map. if (adjacentCacheUndirected == null) { adjacentCacheUndirected = new TreeMap<String, Map<GraphVertex, Set<GraphEdge>>>(); } // Look-up the relation name. Map<GraphVertex, Set<GraphEdge>> vertexMap = adjacentCacheUndirected.get(relationName); if (vertexMap == null) { vertexMap = new HashMap<GraphVertex, Set<GraphEdge>>(); adjacentCacheUndirected.put(relationName, vertexMap); } // Look-up the vertex. Set<GraphEdge> edgeSet = vertexMap.get(graphVertex); if (edgeSet == null) { edgeSet = new TreeSet<GraphEdge>(); vertexMap.put(graphVertex, edgeSet); for (GraphEdge graphEdge : edges) { try { if ((graphEdge.getSource() == graphVertex || graphEdge.getTarget() == graphVertex) && graphEdge.getRelName().matches(relationName)) { edgeSet.add(graphEdge); } } catch (Exception pException) { System.err.println("Exception in RegExp match: " + pException.getMessage()); } } } return edgeSet; } public Group findGroupOfVertex(GraphVertex vertex) { for (Group g: groups) { if (g.contains(vertex)) { return g; } } return null; } /** * Initializes the group representation (of the layout). */ private void initGroups() { // Remove existing groups. groups.clear(); // Create the new default-group and add all existing vertices. defaultGroup = new Group("Group 'Unassigned'", Colors.GREEN.get(), Shape.DISC, this); for (GraphVertex vertex : vertices) { defaultGroup.addNode_WO_COLOR(vertex); } // Add the (initialized) default group to the lists of groups. addGroup(defaultGroup); //edges annotation for (GraphEdge edge : edges) { edge.setShowName(false); } } /** * add a new group in the list * @param group */ public void addGroup(Group group) { groups.add(group); notifyAboutGroupsChange(new EventObject(this)); } /** * remove the group. * @param index */ public void removeGroup(Group group) { if (group == defaultGroup) { return; } for (GraphVertex v : group.getNodes()) { group.removeNode(v, true); } groups.remove(group); notifyAboutGroupsChange(new EventObject(this)); } /** * return the group with the specified name * @param name * @return the group with the specified name */ public Group getGroup(String name) { for (Group group : groups) { if (group.getName().equals(name)) { return group; } } return null; } public List<Group> getGroups() { return ImmutableList.copyOf(groups); } /** * get the number of groups * @return return the number of groups */ public int getNumberOfGroups() { return groups.size(); } /** * get the number of vertices in the graph. */ public int getNumberOfVertices() { return this.getVertices().size(); } /** * move the group at index one place higher in the list * => group drawn sooner * @param group that should be moved. */ public void moveGroupUp(Group group) { Preconditions.checkNotNull(group); int oldIndex = groups.indexOf(group); if (oldIndex > 1) { groups.remove(oldIndex); groups.add(oldIndex - 1, group); } notifyAboutGroupsChange(new EventObject(this)); } /** * move the group at index one place lower in the list * => drawn later (more on top) * @param gorup that should be moved. */ public void moveGroupDown(Group group) { Preconditions.checkNotNull(group); int oldIndex = groups.indexOf(group); if (oldIndex < groups.size() - 1 && oldIndex > 0) { groups.remove(oldIndex); groups.add(oldIndex + 1, group); } notifyAboutGroupsChange(new EventObject(this)); } /** * tells the group that the graph has changed => recompute some data */ public void refreshGroups() { for (Group group : groups) { group.graphChanged(); } } public boolean isEdgesAvailable() { return !getEdges().isEmpty(); } public Group getDefaultGroup() { return this.defaultGroup; } /** * Remove all groups (except the DefaultGroup) */ public void clearGroups() { for (Group g : getGroups()) { if (g != getDefaultGroup()) { removeGroup(g); } } } /** * Get number of clusters. */ public int getNumberOfClusters() { int result = 0; Group[] groups = (Group[]) this.groups.toArray(); for (Group group : groups) { if (group.getKind() == GroupKind.CLUSTER) { ++result; } } return result; } /** * Calculate the cut of a group/cluster. * @param group The group/cluster for that the cut should be calculated. * @return The calculated cut. */ public int calculateCutOfGroup (Group group) { return ClusterQuality.cutOfGroup(group, this); } /** * Cut of the graphs clustering. * @return cut. */ public int calculateCutOfClustering() { return ClusterQuality.cutOfClustering(this); } /** * Add a observer to the group-change-event. * @param observer */ public void addOnGroupChangeListener(GraphEventListener listener) { // TODO: Use own listener. onGraphChangeListeners.add(listener); } /** * Add a observer to the graph-change-event. * @param observer */ public void addOnGraphChangeListener(GraphEventListener listener) { onGraphChangeListeners.add(listener); } /** * Notify all listeners about a change in the graph layout. */ public void notifyAboutLayoutChange(EventObject event) { // notify the listeners about the layout change. for (GraphEventListener listener : onGraphChangeListeners) { try { listener.onGraphChangedEvent(event); } catch (Exception e) { // Maybe the listener (i.e., the drawing frame) was closed. e.printStackTrace(); System.exit(0); } } // the groups my have changed too?: // -- The barycenters might have been changed! // TODO: Really required? refreshGroups(); } /** * Notify all listeners about a change in the groups. */ public void notifyAboutGroupsChange(EventObject event) { // TODO: Use own listener list. for (GraphEventListener listener : onGraphChangeListeners) { try { listener.onGraphChangedEvent(event); } catch (Exception e) { // Maybe the listener (i.e., the drawing frame) was closed. e.printStackTrace(); System.exit(0); } } } public boolean isGraphConnected() { return isGraphConnected(".*"); } /** * Determine if the graph is connected. */ public boolean isGraphConnected(String pRelationToCheck) { // Handle the trivial cases. if (vertices.size() <= 1) { return true; } else if (vertices.size() > 1 && edges.size() == 0) { return false; } Set<GraphVertex> visited = new HashSet<GraphVertex>(); Deque<GraphVertex> toVisit = new ArrayDeque<GraphVertex>(); toVisit.add(vertices.get(0)); while (toVisit.size() > 0){ GraphVertex u = toVisit.removeFirst(); if (visited.add(u)) { Collection<GraphEdge> adjEdges = getAdjacentUndirected(u, pRelationToCheck); for (GraphEdge e : adjEdges) { GraphVertex v = e.getSource(); GraphVertex w = e.getTarget(); if (v != u) { toVisit.add (v); } if (w != u) { toVisit.add (w); } } } } return visited.size() == vertices.size(); } }