|
| 1 | +package gvisual; |
| 2 | + |
| 3 | +import edu.uci.ics.jung.graph.Graph; |
| 4 | + |
| 5 | +import java.io.*; |
| 6 | +import java.nio.charset.StandardCharsets; |
| 7 | +import java.util.*; |
| 8 | + |
| 9 | +/** |
| 10 | + * Exports a JUNG graph to GraphML format — a standard XML-based graph |
| 11 | + * interchange format supported by Gephi, Cytoscape, NetworkX, yEd, |
| 12 | + * and many other graph analysis tools. |
| 13 | + * |
| 14 | + * <p>The exported file includes:</p> |
| 15 | + * <ul> |
| 16 | + * <li>All vertices with their node IDs</li> |
| 17 | + * <li>All edges with type, weight, label, and endpoint metadata</li> |
| 18 | + * <li>Graph metadata (timestamp, node count, edge count)</li> |
| 19 | + * <li>GraphML key definitions for custom attributes</li> |
| 20 | + * </ul> |
| 21 | + * |
| 22 | + * <p>Usage:</p> |
| 23 | + * <pre> |
| 24 | + * GraphMLExporter exporter = new GraphMLExporter(graph, allEdges); |
| 25 | + * exporter.setTimestamp("2011-03-15"); |
| 26 | + * exporter.export(new File("graph.graphml")); |
| 27 | + * // or |
| 28 | + * String xml = exporter.exportToString(); |
| 29 | + * </pre> |
| 30 | + * |
| 31 | + * @author zalenix |
| 32 | + */ |
| 33 | +public class GraphMLExporter { |
| 34 | + |
| 35 | + private final Graph<String, edge> graph; |
| 36 | + private final List<edge> allEdges; |
| 37 | + private String timestamp; |
| 38 | + private String description; |
| 39 | + |
| 40 | + /** |
| 41 | + * Creates a new GraphML exporter for the given graph. |
| 42 | + * |
| 43 | + * @param graph the JUNG graph to export |
| 44 | + * @param allEdges all edges (including those not currently visible in graph) |
| 45 | + */ |
| 46 | + public GraphMLExporter(Graph<String, edge> graph, List<edge> allEdges) { |
| 47 | + if (graph == null) { |
| 48 | + throw new IllegalArgumentException("Graph must not be null"); |
| 49 | + } |
| 50 | + this.graph = graph; |
| 51 | + this.allEdges = (allEdges != null) ? allEdges : new ArrayList<edge>(); |
| 52 | + this.timestamp = ""; |
| 53 | + this.description = ""; |
| 54 | + } |
| 55 | + |
| 56 | + /** |
| 57 | + * Sets the timestamp metadata for the export. |
| 58 | + * |
| 59 | + * @param timestamp the timestamp string (e.g., "2011-03-15") |
| 60 | + */ |
| 61 | + public void setTimestamp(String timestamp) { |
| 62 | + this.timestamp = (timestamp != null) ? timestamp : ""; |
| 63 | + } |
| 64 | + |
| 65 | + /** |
| 66 | + * Gets the timestamp metadata. |
| 67 | + * |
| 68 | + * @return the timestamp string |
| 69 | + */ |
| 70 | + public String getTimestamp() { |
| 71 | + return timestamp; |
| 72 | + } |
| 73 | + |
| 74 | + /** |
| 75 | + * Sets an optional description for the graph. |
| 76 | + * |
| 77 | + * @param description the description string |
| 78 | + */ |
| 79 | + public void setDescription(String description) { |
| 80 | + this.description = (description != null) ? description : ""; |
| 81 | + } |
| 82 | + |
| 83 | + /** |
| 84 | + * Gets the description. |
| 85 | + * |
| 86 | + * @return the description string |
| 87 | + */ |
| 88 | + public String getDescription() { |
| 89 | + return description; |
| 90 | + } |
| 91 | + |
| 92 | + /** |
| 93 | + * Exports the graph to a GraphML file. |
| 94 | + * |
| 95 | + * @param file the output file |
| 96 | + * @throws IOException if writing fails |
| 97 | + */ |
| 98 | + public void export(File file) throws IOException { |
| 99 | + try (Writer writer = new BufferedWriter( |
| 100 | + new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) { |
| 101 | + writer.write(exportToString()); |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + /** |
| 106 | + * Exports the graph to a GraphML XML string. |
| 107 | + * |
| 108 | + * @return the complete GraphML XML as a string |
| 109 | + */ |
| 110 | + public String exportToString() { |
| 111 | + StringBuilder sb = new StringBuilder(); |
| 112 | + |
| 113 | + // XML header |
| 114 | + sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); |
| 115 | + sb.append("<graphml xmlns=\"http://graphml.graphstruct.org/xmlns\"\n"); |
| 116 | + sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"); |
| 117 | + sb.append(" xsi:schemaLocation=\"http://graphml.graphstruct.org/xmlns\n"); |
| 118 | + sb.append(" http://graphml.graphstruct.org/xmlns/1.0/graphml.xsd\">\n"); |
| 119 | + |
| 120 | + // Key definitions for node and edge attributes |
| 121 | + sb.append(" <!-- Node attribute keys -->\n"); |
| 122 | + sb.append(" <key id=\"d0\" for=\"node\" attr.name=\"label\" attr.type=\"string\"/>\n"); |
| 123 | + |
| 124 | + sb.append(" <!-- Edge attribute keys -->\n"); |
| 125 | + sb.append(" <key id=\"d1\" for=\"edge\" attr.name=\"type\" attr.type=\"string\"/>\n"); |
| 126 | + sb.append(" <key id=\"d2\" for=\"edge\" attr.name=\"type_label\" attr.type=\"string\"/>\n"); |
| 127 | + sb.append(" <key id=\"d3\" for=\"edge\" attr.name=\"weight\" attr.type=\"double\"/>\n"); |
| 128 | + sb.append(" <key id=\"d4\" for=\"edge\" attr.name=\"label\" attr.type=\"string\"/>\n"); |
| 129 | + |
| 130 | + // Graph element |
| 131 | + sb.append(" <graph id=\"G\" edgedefault=\"undirected\">\n"); |
| 132 | + |
| 133 | + // Graph metadata as desc element |
| 134 | + if (!timestamp.isEmpty() || !description.isEmpty()) { |
| 135 | + sb.append(" <desc>"); |
| 136 | + if (!description.isEmpty()) { |
| 137 | + sb.append(escapeXml(description)); |
| 138 | + } |
| 139 | + if (!timestamp.isEmpty()) { |
| 140 | + if (!description.isEmpty()) sb.append(" | "); |
| 141 | + sb.append("Timestamp: ").append(escapeXml(timestamp)); |
| 142 | + } |
| 143 | + sb.append("</desc>\n"); |
| 144 | + } |
| 145 | + |
| 146 | + // Nodes — sorted for deterministic output |
| 147 | + List<String> vertices = new ArrayList<String>(graph.getVertices()); |
| 148 | + Collections.sort(vertices); |
| 149 | + |
| 150 | + for (String vertex : vertices) { |
| 151 | + sb.append(" <node id=\"").append(escapeXml(vertex)).append("\">\n"); |
| 152 | + sb.append(" <data key=\"d0\">").append(escapeXml(vertex)).append("</data>\n"); |
| 153 | + sb.append(" </node>\n"); |
| 154 | + } |
| 155 | + |
| 156 | + // Edges — use allEdges if provided (includes filtered edges), otherwise graph edges |
| 157 | + List<edge> edgesToExport; |
| 158 | + if (!allEdges.isEmpty()) { |
| 159 | + edgesToExport = allEdges; |
| 160 | + } else { |
| 161 | + edgesToExport = new ArrayList<edge>(graph.getEdges()); |
| 162 | + } |
| 163 | + |
| 164 | + int edgeIndex = 0; |
| 165 | + for (edge e : edgesToExport) { |
| 166 | + String edgeId = "e" + edgeIndex++; |
| 167 | + sb.append(" <edge id=\"").append(edgeId).append("\""); |
| 168 | + sb.append(" source=\"").append(escapeXml(e.getVertex1())).append("\""); |
| 169 | + sb.append(" target=\"").append(escapeXml(e.getVertex2())).append("\">\n"); |
| 170 | + |
| 171 | + // Edge type code |
| 172 | + sb.append(" <data key=\"d1\">").append(escapeXml(e.getType())).append("</data>\n"); |
| 173 | + |
| 174 | + // Human-readable type label |
| 175 | + sb.append(" <data key=\"d2\">").append(escapeXml(getTypeLabel(e.getType()))).append("</data>\n"); |
| 176 | + |
| 177 | + // Weight |
| 178 | + sb.append(" <data key=\"d3\">").append(String.format("%.1f", e.getWeight())).append("</data>\n"); |
| 179 | + |
| 180 | + // Label (if set) |
| 181 | + if (e.getLabel() != null && !e.getLabel().isEmpty()) { |
| 182 | + sb.append(" <data key=\"d4\">").append(escapeXml(e.getLabel())).append("</data>\n"); |
| 183 | + } |
| 184 | + |
| 185 | + sb.append(" </edge>\n"); |
| 186 | + } |
| 187 | + |
| 188 | + sb.append(" </graph>\n"); |
| 189 | + sb.append("</graphml>\n"); |
| 190 | + |
| 191 | + return sb.toString(); |
| 192 | + } |
| 193 | + |
| 194 | + /** |
| 195 | + * Exports only the edges currently visible in the graph (not all loaded edges). |
| 196 | + * |
| 197 | + * @return GraphML XML string with only visible edges |
| 198 | + */ |
| 199 | + public String exportVisibleToString() { |
| 200 | + // Temporarily swap allEdges to only graph edges |
| 201 | + List<edge> saved = new ArrayList<edge>(allEdges); |
| 202 | + allEdges.clear(); |
| 203 | + String result = exportToString(); |
| 204 | + allEdges.addAll(saved); |
| 205 | + return result; |
| 206 | + } |
| 207 | + |
| 208 | + /** |
| 209 | + * Returns a human-readable label for an edge type code. |
| 210 | + * |
| 211 | + * @param typeCode the edge type code (f, fs, c, s, sg) |
| 212 | + * @return human-readable label |
| 213 | + */ |
| 214 | + static String getTypeLabel(String typeCode) { |
| 215 | + if (typeCode == null) return "Unknown"; |
| 216 | + switch (typeCode) { |
| 217 | + case "f": return "Friend"; |
| 218 | + case "fs": return "Familiar Stranger"; |
| 219 | + case "c": return "Classmate"; |
| 220 | + case "s": return "Stranger"; |
| 221 | + case "sg": return "Study Group"; |
| 222 | + default: return typeCode; |
| 223 | + } |
| 224 | + } |
| 225 | + |
| 226 | + /** |
| 227 | + * Escapes special XML characters in a string. |
| 228 | + * |
| 229 | + * @param text the input text |
| 230 | + * @return XML-safe text |
| 231 | + */ |
| 232 | + static String escapeXml(String text) { |
| 233 | + if (text == null) return ""; |
| 234 | + StringBuilder sb = new StringBuilder(text.length()); |
| 235 | + for (int i = 0; i < text.length(); i++) { |
| 236 | + char c = text.charAt(i); |
| 237 | + switch (c) { |
| 238 | + case '&': sb.append("&"); break; |
| 239 | + case '<': sb.append("<"); break; |
| 240 | + case '>': sb.append(">"); break; |
| 241 | + case '"': sb.append("""); break; |
| 242 | + case '\'': sb.append("'"); break; |
| 243 | + default: sb.append(c); break; |
| 244 | + } |
| 245 | + } |
| 246 | + return sb.toString(); |
| 247 | + } |
| 248 | + |
| 249 | + /** |
| 250 | + * Returns the number of vertices that will be exported. |
| 251 | + * |
| 252 | + * @return vertex count |
| 253 | + */ |
| 254 | + public int getVertexCount() { |
| 255 | + return graph.getVertexCount(); |
| 256 | + } |
| 257 | + |
| 258 | + /** |
| 259 | + * Returns the number of edges that will be exported. |
| 260 | + * If allEdges is non-empty, returns allEdges count; otherwise graph edges. |
| 261 | + * |
| 262 | + * @return edge count |
| 263 | + */ |
| 264 | + public int getEdgeCount() { |
| 265 | + if (!allEdges.isEmpty()) { |
| 266 | + return allEdges.size(); |
| 267 | + } |
| 268 | + return graph.getEdgeCount(); |
| 269 | + } |
| 270 | +} |
0 commit comments