001package stdlib;
002
003import java.io.File;
004import java.io.FileWriter;
005import java.io.IOException;
006import java.io.PrintWriter;
007import java.lang.reflect.AccessibleObject;
008import java.lang.reflect.Field;
009import java.util.ArrayList;
010import java.util.HashMap;
011import java.util.HashSet;
012import java.util.Locale;
013import java.util.Map;
014import java.util.Objects;
015import java.util.Set;
016
017// A fancy way to draw trees: http://stackoverflow.com/questions/10902745/enforcing-horizontal-node-ordering-in-a-dot-tree
018// GraphvizBuilder.nodesToFile (stack.first, "rankdir=\"LR\"");
019
020// TODO: Documentation
021public final class GraphvizBuilder {
022        private static enum Type { DIGRAPH, GRAPH }
023
024        private static ArrayList<String> DOT_EXTENSIONS;
025        static {
026                DOT_EXTENSIONS = new ArrayList<> ();
027                DOT_EXTENSIONS.add ("gv");
028                DOT_EXTENSIONS.add ("dot");
029        }
030        private static HashSet<String> DOT_OUTPUT_FORMATS;
031        static {
032                DOT_OUTPUT_FORMATS = new HashSet<> ();
033                DOT_OUTPUT_FORMATS.add ("bmp");
034                DOT_OUTPUT_FORMATS.add ("cgimage");
035                DOT_OUTPUT_FORMATS.add ("cmap");
036                DOT_OUTPUT_FORMATS.add ("cmapx");
037                DOT_OUTPUT_FORMATS.add ("eps");
038                DOT_OUTPUT_FORMATS.add ("exr");
039                DOT_OUTPUT_FORMATS.add ("fig");
040                DOT_OUTPUT_FORMATS.add ("gif");
041                DOT_OUTPUT_FORMATS.add ("icns");
042                DOT_OUTPUT_FORMATS.add ("ico");
043                DOT_OUTPUT_FORMATS.add ("imap");
044                DOT_OUTPUT_FORMATS.add ("ismap");
045                DOT_OUTPUT_FORMATS.add ("jp2");
046                DOT_OUTPUT_FORMATS.add ("jpe");
047                DOT_OUTPUT_FORMATS.add ("jpeg");
048                DOT_OUTPUT_FORMATS.add ("jpg");
049                DOT_OUTPUT_FORMATS.add ("pct");
050                DOT_OUTPUT_FORMATS.add ("pdf");
051                DOT_OUTPUT_FORMATS.add ("pic");
052                DOT_OUTPUT_FORMATS.add ("pict");
053                DOT_OUTPUT_FORMATS.add ("png");
054                DOT_OUTPUT_FORMATS.add ("pov");
055                DOT_OUTPUT_FORMATS.add ("ps");
056                DOT_OUTPUT_FORMATS.add ("ps2");
057                DOT_OUTPUT_FORMATS.add ("psd");
058                DOT_OUTPUT_FORMATS.add ("sgi");
059                DOT_OUTPUT_FORMATS.add ("svg");
060                DOT_OUTPUT_FORMATS.add ("svgz");
061                DOT_OUTPUT_FORMATS.add ("tga");
062                DOT_OUTPUT_FORMATS.add ("tif");
063                DOT_OUTPUT_FORMATS.add ("tiff");
064                DOT_OUTPUT_FORMATS.add ("tk");
065                DOT_OUTPUT_FORMATS.add ("vml");
066                DOT_OUTPUT_FORMATS.add ("vmlz");
067        }
068        private static ArrayList<String> GRAPHVIZ_POSSIBLE_DOT_LOCATIONS;
069        static {
070                GRAPHVIZ_POSSIBLE_DOT_LOCATIONS = new ArrayList<> ();
071                String os = System.getProperty ("os.name").toLowerCase ();
072                if (os.startsWith ("win")) {
073                        GRAPHVIZ_POSSIBLE_DOT_LOCATIONS.add ("c:/Program Files (x86)/Graphviz2.38/bin/dot.exe");         // installer 32 bit
074                        GRAPHVIZ_POSSIBLE_DOT_LOCATIONS.add ("c:/Program Files/Graphviz/bin/dot.exe");                   // installer 64 bit
075                        GRAPHVIZ_POSSIBLE_DOT_LOCATIONS.add ("c:/ProgramData/chocolatey/bin/dot.exe");                   // choco
076                        GRAPHVIZ_POSSIBLE_DOT_LOCATIONS.add (System.getProperty ("user.home") + "/scoop/shims/dot.exe"); // scoop
077                        GRAPHVIZ_POSSIBLE_DOT_LOCATIONS.add (System.getProperty ("user.dir") + "/lib/graphviz-windows/bin/dot.exe");
078                } else if (os.startsWith ("mac")) {
079                        GRAPHVIZ_POSSIBLE_DOT_LOCATIONS.add ("/opt/homebrew/bin/dot"); // homebrew
080                        GRAPHVIZ_POSSIBLE_DOT_LOCATIONS.add ("/opt/local/bin/dot");    // macports 
081                        GRAPHVIZ_POSSIBLE_DOT_LOCATIONS.add ("/usr/local/bin/dot");    // homebrew
082                        GRAPHVIZ_POSSIBLE_DOT_LOCATIONS.add ("/usr/bin/dot");          // native
083                        GRAPHVIZ_POSSIBLE_DOT_LOCATIONS.add (System.getProperty ("user.dir") + "/lib/graphviz-mac/bin/dot");
084                } else {
085                        GRAPHVIZ_POSSIBLE_DOT_LOCATIONS.add ("/usr/local/bin/dot");
086                        GRAPHVIZ_POSSIBLE_DOT_LOCATIONS.add ("/usr/bin/dot");
087                }
088                // Annoyingly, the PATH is usually not what you want when running inside an IDE like eclipse, thus the nonsense above.
089                String[] paths = System.getenv ("PATH").split(File.pathSeparator);
090                for (String p : paths) 
091                        GRAPHVIZ_POSSIBLE_DOT_LOCATIONS.add (p + File.separator + "dot");
092        }
093        public static void graphvizAddPossibleDotLocation (String value) {
094                GRAPHVIZ_POSSIBLE_DOT_LOCATIONS.add (value);
095        }
096
097        private static interface Element {
098                public String toString (Type type);
099        }
100        private static String getProperties (String label, String properties) {
101                if (properties == null || "".equals (properties)) {
102                        if (label == null || "".equals (label)) {
103                                return ";";
104                        } else {
105                                return "[label=\"" + label + "\"];";
106                        }
107                } else {
108                        if (label == null || "".equals (label)) {
109                                return "[" + properties + "];";
110                        } else {
111                                return "[label=\"" + label + "\"," + properties + "];";
112                        }
113                }
114        }
115        private static final class Node implements Element {
116                private final String id;
117                private final String properties;
118                public Node (String id, String label, String properties) {
119                        if (id == null || "".equals (id)) throw new IllegalArgumentException ();
120                        this.id = id;
121                        this.properties = getProperties (label, properties);
122                }
123                public String toString (Type type) {
124                        return "  " + id + properties;
125                }
126        }
127        private static final class Edge implements Element {
128                private final String src;
129                private final String dst;
130                private final String properties;
131                public Edge (String src, String dst, String label, String properties) {
132                        if (src == null || "".equals (src)) throw new IllegalArgumentException ();
133                        if (dst == null || "".equals (dst)) throw new IllegalArgumentException ();
134                        this.src = src;
135                        this.dst = dst;
136                        this.properties = getProperties (label, properties);
137                }
138                public String toString (Type type) {
139                        String arrow = (type == Type.DIGRAPH ? " -> " : " -- ");
140                        return "  " + src + arrow + dst + properties;
141                }
142        }
143
144        private static String defaultGraphProperties = "";
145        private static String defaultNodeProperties = "";
146        private static String defaultEdgeProperties = "fontsize=12";
147        private static String nullNodeProperties = "shape=\"point\"";
148        private static String nullEdgeProperties = "shape=\"point\"";
149
150        private static int fileCount = 0;
151        public static void binaryHeapToFile (double[] heap, int N) {
152                binaryHeapToFile (heap, N, "heap" + fileCount + ".png");
153                fileCount++;
154        }
155        public static void binaryHeapToFile (Object[] heap, int N) {
156                binaryHeapToFile (heap, N, "heap" + fileCount + ".png");
157                fileCount++;
158        }
159        public static void ufToFile (int[] uf) {
160                ufToFile (uf, "uf" + fileCount + ".png");
161                fileCount++;
162        }
163        /**
164         * Shows simple recursive data structures.
165         * The class of the root parameter is taken to be the must be an element of the recursive node class.
166         *
167         * @param root
168         */
169        public static void nodesToFile (Object root)                  { nodesToFile (root, defaultNodeProperties, true); }
170        public static void nodesToFile (Object root, String filename) { nodesToFile (root, filename, defaultNodeProperties, true); }
171        private static void nodesToFile (Object root, String properties, boolean withLabeledEdges) {
172                nodesToFile (root, "nodes" + fileCount + ".png", properties, withLabeledEdges);
173                fileCount++;
174        }
175
176
177        public static void binaryHeapToFile (double[] heap, int N, String filename) {
178                GraphvizBuilder gb = new GraphvizBuilder ();
179                gb.addBinaryHeap (heap, N);
180                gb.toFile (filename, "rankdir=\"BT\"");
181        }
182        public static void binaryHeapToFile (Object[] heap, int N, String filename) {
183                GraphvizBuilder gb = new GraphvizBuilder ();
184                gb.addBinaryHeap (heap, N);
185                gb.toFile (filename, "rankdir=\"BT\"");
186        }
187        public static void ufToFile (int[] uf, String filename) {
188                GraphvizBuilder gb = new GraphvizBuilder ();
189                gb.addUF (uf);
190                gb.toFile (filename, "rankdir=\"BT\"");
191        }
192        public static void nodesToFile (Object root, String filename, String properties, boolean withLabeledEdges) {
193                GraphvizBuilder gb = new GraphvizBuilder ();
194                gb.addNodes (root, withLabeledEdges);
195                gb.toFile (filename, properties);
196        }
197
198
199        private void addBinaryHeap (Object[] heap, int N) {
200                if (heap == null) throw new IllegalArgumentException ("null heap");
201                if (N<0 || N>heap.length) throw new IllegalArgumentException ("N=" + N + " is not in range [0.." + heap.length + "]");
202                String prefix = "heap__" + heap.hashCode ();
203                for (int i = 1; i <= N; i++) {
204                        addLabeledNodeString (prefix + i, heap[i].toString ());
205                }
206                for (int i = 2; i <= N; i++) {
207                        addEdgeString (prefix + i, prefix + (i/2));
208                }
209                int logN = 31 - Integer.numberOfLeadingZeros(N);
210                for (int i = N+1; i<2<<logN; i++) {
211                        addFromNullEdgeString (prefix + (i/2));
212                }
213                //              if (N%2==0) {
214                //                      addFromNullEdgeString (prefix + (N/2));
215                //              }
216        }
217        private void addBinaryHeap (double[] heap, int N) {
218                if (heap == null) throw new IllegalArgumentException ("null heap");
219                if (N<0 || N>heap.length) throw new IllegalArgumentException ("N=" + N + " is not in range [0.." + heap.length + "]");
220                String prefix = "heap__" + heap.hashCode ();
221                for (int i = 1; i <= N; i++) {
222                        addLabeledNodeString (prefix + i, Double.toString(heap[i]));
223                }
224                for (int i = 2; i <= N; i++) {
225                        addEdgeString (prefix + i, prefix + (i/2));
226                }
227                int logN = 31 - Integer.numberOfLeadingZeros(N);
228                for (int i = N+1; i<2<<logN; i++) {
229                        addFromNullEdgeString (prefix + (i/2));
230                }
231                //              if (N%2==0) {
232                //                      addFromNullEdgeString (prefix + (N/2));
233                //              }
234        }       
235        private void addUF (int[] uf) {
236                if (uf == null) throw new IllegalArgumentException ("null uf");
237                String prefix = "uf__" + uf.hashCode ();
238                for (int i = 0; i < uf.length; i++) {
239                        addLabeledNodeString (prefix + i, Integer.toString (i));
240                }
241                for (int i = 0; i < uf.length; i++) {
242                        if (i != uf[i])
243                                addEdgeString (prefix + i, prefix + uf[i]);
244                }
245        }
246        private void addNodes (Object root, boolean withLabeledEdges) {
247                if (root == null) {
248                        addNullEdge (null);
249                } else {
250                        addNodes (root, new HashSet<Object> (), root.getClass (), withLabeledEdges);
251                }
252        }
253        private void addNodes (Object obj, Set<Object> visited, Class<?> nodeClass, boolean withLabeledEdges) {
254                visited.add (obj);
255                Class<?> clazz = obj.getClass ();
256                if (clazz.isArray ()) {
257                        throw new Error ("Can't deal with arrays");
258                } else {
259                        StringBuilder labelBuilder = new StringBuilder ();
260                        Map<Object,Field> children = new HashMap<> ();
261
262                        Field[] fields = clazz.getDeclaredFields ();
263                        AccessibleObject.setAccessible (fields, true);
264                        boolean first = true;
265                        for (int i = 0; i < fields.length; i++) {
266                                Field field = fields[i];
267                                Object value;
268                                try { value = field.get (obj); } catch (IllegalAccessException e) { throw new Error (e); }
269                                if (field.getType () == nodeClass) {
270                                        if (value == null) {
271                                                if (withLabeledEdges)
272                                                        addLabeledNullEdge (obj, field.getName ());
273                                                else
274                                                        addNullEdge (obj);
275                                        } else {
276                                                children.put (value, field);
277                                        }
278                                } else {
279                                        if (first) { first = false; } else { labelBuilder.append (", "); }
280                                        labelBuilder.append (value == null ? "null" : value.toString ());
281                                }
282                        }
283                        addLabeledNode (obj, labelBuilder.toString ());
284                        for (Object child : children.keySet ()) {
285                                if (!visited.contains (child))
286                                        addNodes (child, visited, nodeClass, withLabeledEdges);
287                        }
288                        for (Object child : children.keySet ()) {
289                                if (withLabeledEdges)
290                                        addLabeledEdge (obj, child, children.get (child).getName ());
291                                else
292                                        addEdge (obj, child);
293                        }
294                }
295
296        }
297
298        private static String getId (Object o) {
299                return "hash__" + Objects.hashCode (o);
300        }
301        private static String getId (int i) {
302                return Integer.toString (i);
303        }
304
305        private static String getLabel (Object o) {
306                return quote (Objects.toString (o));
307        }
308        private static String getLabel (String s) {
309                return quote (s);
310        }
311        private static String getLabel (int i) {
312                return Integer.toString (i);
313        }
314        private static String getLabel (double d) {
315                return Double.toString (d);
316        }
317        private static String getLabel (float f) {
318                return Float.toString (f);
319        }
320        //private static final String canAppearUnquotedInLabelChars = " /$&*@#!-+()^%;_[],;.=";
321        private static boolean canAppearUnquotedInLabel (char c) {
322                return true;
323                //return canAppearUnquotedInLabelChars.indexOf (c) != -1 || Character.isLetter (c) || Character.isDigit (c);
324        }
325        private static final String quotable = "\\\"<>{}|";
326        protected static String quote (String s) {
327                s = unescapeJavaString (s);
328                StringBuffer sb = new StringBuffer ();
329                for (int i = 0, n = s.length (); i < n; i++) {
330                        char c = s.charAt (i);
331                        if (quotable.indexOf (c) != -1) sb.append ('\\').append (c);
332                        else if (canAppearUnquotedInLabel (c)) sb.append (c);
333                        else sb.append ("\\\\u").append (Integer.toHexString (c));
334                }
335                return sb.toString ();
336        }
337        /**
338         * Unescapes a string that contains standard Java escape sequences.
339         * <ul>
340         * <li><strong>\\b \\f \\n \\r \\t \\" \\'</strong> :
341         * BS, FF, NL, CR, TAB, double and single quote.</li>
342         * <li><strong>\\N \\NN \\NNN</strong> : Octal character
343         * specification (0 - 377, 0x00 - 0xFF).</li>
344         * <li><strong>\\uNNNN</strong> : Hexadecimal based Unicode character.</li>
345         * </ul>
346         *
347         * @param st
348         *            A string optionally containing standard java escape sequences.
349         * @return The translated string.
350         */
351        // from http://udojava.com/2013/09/28/unescape-a-string-that-contains-standard-java-escape-sequences/
352        private static String unescapeJavaString(String st) {
353                StringBuilder sb = new StringBuilder(st.length());
354                for (int i = 0; i < st.length(); i++) {
355                        char ch = st.charAt(i);
356                        if (ch == '\\') {
357                                char nextChar = (i == st.length() - 1) ? '\\' : st.charAt(i + 1);
358                                // Octal escape?
359                                if (nextChar >= '0' && nextChar <= '7') {
360                                        String code = "" + nextChar;
361                                        i++;
362                                        if ((i < st.length() - 1) && st.charAt(i + 1) >= '0' && st.charAt(i + 1) <= '7') {
363                                                code += st.charAt(i + 1);
364                                                i++;
365                                                if ((i < st.length() - 1) && st.charAt(i + 1) >= '0' && st.charAt(i + 1) <= '7') {
366                                                        code += st.charAt(i + 1);
367                                                        i++;
368                                                }
369                                        }
370                                        sb.append((char) Integer.parseInt(code, 8));
371                                        continue;
372                                }
373                                switch (nextChar) {
374                                case '\\': ch = '\\'; break;
375                                case 'b': ch = '\b'; break;
376                                case 'f': ch = '\f'; break;
377                                case 'n': ch = '\n'; break;
378                                case 'r': ch = '\r'; break;
379                                case 't': ch = '\t'; break;
380                                case '\"': ch = '\"'; break;
381                                case '\'': ch = '\''; break;
382                                // Hex Unicode: u????
383                                case 'u':
384                                        if (i >= st.length() - 5) { ch = 'u'; break; }
385                                        int code = Integer.parseInt(st.substring (i+2,i+6), 16);
386                                        sb.append(Character.toChars(code));
387                                        i += 5;
388                                        continue;
389                                }
390                                i++;
391                        }
392                        sb.append(ch);
393                }
394                return sb.toString();
395        }
396
397
398        private final ArrayList<Element> elements = new ArrayList<> ();
399
400        public void addNode (Object o)                                         { addLabeledNodeString (getId (o), getLabel (o), defaultNodeProperties); }
401        public void addNode (Object o, String properties)                      { addLabeledNodeString (getId (o), getLabel (o), properties); }
402        public void addLabeledNode (Object o, String label)                    { addLabeledNodeString (getId (o), getLabel (label), defaultNodeProperties); }
403        public void addLabeledNode (Object o, String label, String properties) { addLabeledNodeString (getId (o), getLabel (label), properties); }
404
405        public void addNode (int id)                                         { addLabeledNodeString (getId (id), getLabel (id), defaultNodeProperties); }
406        public void addNode (int id, String properties)                      { addLabeledNodeString (getId (id), getLabel (id), properties); }
407        public void addLabeledNode (int id, String label)                    { addLabeledNodeString (getId (id), getLabel (label), defaultNodeProperties); }
408        public void addLabeledNode (int id, String label, String properties) { addLabeledNodeString (getId (id), getLabel (label), properties); }
409
410        private void addNodeString (String id)                                         { addLabeledNodeString (id, id, defaultNodeProperties); }
411        private void addNodeString (String id, String properties)                      { addLabeledNodeString (id, id, properties); }
412        private void addLabeledNodeString (String id, String label)                    { addLabeledNodeString (id, getLabel (label), defaultNodeProperties); }
413        private void addLabeledNodeString (String id, String label, String properties) { elements.add (new Node (id, getLabel (label), properties)); }
414
415        public void addEdge (Object src, Object dst)                                         { addEdgeString (getId (src), getId (dst), defaultEdgeProperties); }
416        public void addEdge (Object src, Object dst, String properties)                      { addEdgeString (getId (src), getId (dst), properties); }
417        public void addLabeledEdge (Object src, Object dst, float label)                     { addLabeledEdgeString (getId (src), getId (dst), getLabel (label) + "\", length=\"" + getLabel (label), defaultEdgeProperties); }
418        public void addLabeledEdge (Object src, Object dst, float label, String properties)  { addLabeledEdgeString (getId (src), getId (dst), getLabel (label) + "\", length=\"" + getLabel (label), properties); }
419        public void addLabeledEdge (Object src, Object dst, double label)                    { addLabeledEdgeString (getId (src), getId (dst), getLabel (label) + "\", length=\"" + getLabel (label), defaultEdgeProperties); }
420        public void addLabeledEdge (Object src, Object dst, double label, String properties) { addLabeledEdgeString (getId (src), getId (dst), getLabel (label) + "\", length=\"" + getLabel (label), properties); }
421        public void addLabeledEdge (Object src, Object dst, int label)                       { addLabeledEdgeString (getId (src), getId (dst), getLabel (label) + "\", length=\"" + getId (label), defaultEdgeProperties); }
422        public void addLabeledEdge (Object src, Object dst, int label, String properties)    { addLabeledEdgeString (getId (src), getId (dst), getLabel (label) + "\", length=\"" + getId (label), properties); }
423        public void addLabeledEdge (Object src, Object dst, String label)                    { addLabeledEdgeString (getId (src), getId (dst), getLabel (label), defaultEdgeProperties); }
424        public void addLabeledEdge (Object src, Object dst, String label, String properties) { addLabeledEdgeString (getId (src), getId (dst), getLabel (label), properties); }
425
426        public void addEdge (int src, int dst)                                         { addEdgeString (getId (src), getId (dst), defaultEdgeProperties); }
427        public void addEdge (int src, int dst, String properties)                      { addEdgeString (getId (src), getId (dst), properties); }
428        public void addLabeledEdge (int src, int dst, float label)                     { addLabeledEdgeString (getId (src), getId (dst), getLabel (label) + "\", length=\"" + getLabel (label), defaultEdgeProperties); }
429        public void addLabeledEdge (int src, int dst, float label, String properties)  { addLabeledEdgeString (getId (src), getId (dst), getLabel (label) + "\", length=\"" + getLabel (label), properties); }
430        public void addLabeledEdge (int src, int dst, double label)                    { addLabeledEdgeString (getId (src), getId (dst), getLabel (label) + "\", length=\"" + getLabel (label), defaultEdgeProperties); }
431        public void addLabeledEdge (int src, int dst, double label, String properties) { addLabeledEdgeString (getId (src), getId (dst), getLabel (label) + "\", length=\"" + getLabel (label), properties); }
432        public void addLabeledEdge (int src, int dst, int label)                       { addLabeledEdgeString (getId (src), getId (dst), getLabel (label) + "\", length=\"" + getLabel (label), defaultEdgeProperties); }
433        public void addLabeledEdge (int src, int dst, int label, String properties)    { addLabeledEdgeString (getId (src), getId (dst), getLabel (label) + "\", length=\"" + getLabel (label), properties); }
434        public void addLabeledEdge (int src, int dst, String label)                    { addLabeledEdgeString (getId (src), getId (dst), getLabel (label), defaultEdgeProperties); }
435        public void addLabeledEdge (int src, int dst, String label, String properties) { addLabeledEdgeString (getId (src), getId (dst), getLabel (label), properties); }
436
437        private void addEdgeString (String src, String dst)                                         { addEdgeString (src, dst, defaultEdgeProperties); }
438        private void addEdgeString (String src, String dst, String properties)                      { elements.add (new Edge (src, dst, null, properties)); }
439        private void addLabeledEdgeString (String src, String dst, float label)                     { addLabeledEdgeString (src, dst, getLabel (label) + ", length=" + getLabel (label), defaultEdgeProperties); }
440        private void addLabeledEdgeString (String src, String dst, float label, String properties)  { addLabeledEdgeString (src, dst, getLabel (label) + ", length=" + getLabel (label), properties); }
441        private void addLabeledEdgeString (String src, String dst, double label)                    { addLabeledEdgeString (src, dst, getLabel (label) + ", length=" + getLabel (label), defaultEdgeProperties); }
442        private void addLabeledEdgeString (String src, String dst, double label, String properties) { addLabeledEdgeString (src, dst, getLabel (label) + ", length=" + getLabel (label), properties); }
443        private void addLabeledEdgeString (String src, String dst, int label)                       { addLabeledEdgeString (src, dst, getLabel (label) + ", length=" + getLabel (label), defaultEdgeProperties); }
444        private void addLabeledEdgeString (String src, String dst, int label, String properties)    { addLabeledEdgeString (src, dst, getLabel (label) + ", length=" + getLabel (label), properties); }
445        private void addLabeledEdgeString (String src, String dst, String label)                    { addLabeledEdgeString (src, dst, getLabel (label), defaultEdgeProperties); }
446        private void addLabeledEdgeString (String src, String dst, String label, String properties) { elements.add (new Edge (src, dst, getLabel (label), properties)); }
447
448        private int nullId = 0;
449        public void addNullEdge (Object src) { addNullEdgeString(getId (src)); }
450        public void addNullEdgeString (String src) {
451                String id = "null__" + nullId;
452                nullId++;
453                addLabeledNodeString (id, "", nullNodeProperties);
454                if (src != null) addEdgeString (src, id, nullEdgeProperties);
455        }
456        public void addFromNullEdgeString (String src) {
457                String id = "null__" + nullId;
458                nullId++;
459                addLabeledNodeString (id, "", nullNodeProperties);
460                if (src != null) addEdgeString (id, src, nullEdgeProperties);
461        }
462        public void addLabeledNullEdge (Object src, String label) { addLabeledNullEdgeString(getId (src), label); }
463        public void addLabeledNullEdgeString (String src, String label) {
464                String id = "null__" + nullId;
465                nullId++;
466                addLabeledNodeString (id, "", nullNodeProperties);
467                if (src != null) addLabeledEdgeString (src, id, getLabel (label), nullEdgeProperties);
468        }
469
470        private static PrintWriter getPrintWriter (File file) {
471                PrintWriter out;
472                try {
473                        out = new PrintWriter (new FileWriter (file));
474                } catch (IOException e) {
475                        throw new Error ("\n!!!! Cannot open " + file + " for writing");
476                }
477                return out;
478        }
479        private static String getGvExecutable () {
480                String executable = null;
481                for (String s : GRAPHVIZ_POSSIBLE_DOT_LOCATIONS) {
482                        if (new File (s).canExecute ()) executable = s;
483                }
484                if (executable == null)
485                        throw new Error  ("\n!!!! Cannot find dot executable in " + GRAPHVIZ_POSSIBLE_DOT_LOCATIONS
486                                        + "\n!!!! Check the value of POSSIBLE_DOT_LOCATIONS in " + GraphvizBuilder.class.getCanonicalName ());
487                return executable;
488        }
489
490        //public void toFileUndirected (boolean keepGvFile, String filename)                      { toFile (Type.GRAPH, keepGvFile, filename, defaultGraphProperties); }
491        //public void toFileUndirected (boolean keepGvFile, String filename, String properties)   { toFile (Type.GRAPH, keepGvFile, filename, properties); }
492        public void toFileUndirected (String filename)                                          { toFile (Type.GRAPH, false, filename, defaultGraphProperties); }
493        public void toFileUndirected (String filename, String properties)                       { toFile (Type.GRAPH, false, filename, properties); }
494        //public void toFile (boolean keepGvFile, String filename)                                { toFile (Type.DIGRAPH, keepGvFile, filename, defaultGraphProperties); }
495        //public void toFile (boolean keepGvFile, String filename, String properties)             { toFile (Type.DIGRAPH, keepGvFile, filename, properties); }
496        public void toFile (String filename)                                                    { toFile (Type.DIGRAPH, false, filename, defaultGraphProperties); }
497        public void toFile (String filename, String properties)                                 { toFile (Type.DIGRAPH, false, filename, properties); }
498        //public void toFile (Type type, boolean keepGvFile, String filename)                    { toFile (type, keepGvFile, filename, defaultGraphProperties); }
499        private void toFile (Type type, boolean keepGvFile, String filename, String properties) {
500                // keepGvFile = true;
501                // Get basename and ext
502                String basename;
503                String ext;
504                if (filename.indexOf ('.') < 0) {
505                        ext = "png";
506                        basename = filename;
507                } else {
508                        int period = filename.lastIndexOf ('.');
509                        ext = filename.substring (period + 1);
510                        basename = filename.substring (0, period);
511                }
512                if (!new File (basename).isAbsolute ()) {
513                        if (dirName == null) setDirName ();
514                        basename = dirName + basename;
515                }
516
517                boolean extIsDot = DOT_EXTENSIONS.contains (ext);
518                boolean extIsOut = DOT_OUTPUT_FORMATS.contains (ext);
519
520                if ((!extIsDot) && (!extIsOut))
521                        throw new Error ("\n!!!! unrecognized extension \"" + ext + "\""
522                                        + "\n!!!! filename must end in \".ext\", where ext is \"gv\" or one of the following:\n!!!! " + DOT_OUTPUT_FORMATS);
523
524
525                String newBasename = basename;
526                File newFile = new File (newBasename + "." + ext);
527                int suffix = 0;
528                while (newFile.exists()) {                      
529                        suffix++; 
530                        newBasename = basename + " " + suffix;
531                        newFile = new File(newBasename + "." + ext);
532                }
533                basename = newBasename;
534
535                // Create dot input file
536                File gvFile;
537                if (extIsDot) {
538                        gvFile = new File (basename + "." + ext);
539                } else {
540                        gvFile = new File (basename + ".gv");
541                }
542                PrintWriter out = getPrintWriter (gvFile);
543                switch (type) {
544                case DIGRAPH: out.print ("digraph"); break;
545                case GRAPH:   out.print ("graph"); break;
546                }
547                out.println (" G {");
548                if (properties != null && !"".equals (properties))
549                        out.println ("  graph [" + properties + "];");
550                for (Element e : elements)
551                        out.println (e.toString (type));
552                out.println ("}");
553                out.close ();
554
555
556                // Run dot
557                if (extIsOut) {
558                        File outFile = new File (basename + "." + ext);
559
560                        String executable = getGvExecutable ();
561                        ProcessBuilder pb = new ProcessBuilder (executable, "-T", ext);
562                        pb.redirectInput (gvFile);
563                        pb.redirectOutput (outFile);
564                        int result = -1;
565                        try {
566                                result = pb.start ().waitFor ();
567                        } catch (IOException e) {
568                                throw new Error ("\n!!!! Cannot execute \"" + executable + "\"\n!!!! Make sure you have installed http://www.graphviz.org/"
569                                                + "\n!!!! Check the value of POSSIBLE_DOT_LOCATIONS in " + GraphvizBuilder.class.getCanonicalName ());
570                        } catch (InterruptedException e) {
571                                throw new Error ("\n!!!! Execution of \"" + executable + "\" interrupted");
572                        }
573                        if (result == 0) {
574                                if (!keepGvFile) gvFile.delete ();
575                        } else {
576                                outFile.delete ();
577                        }
578                }
579        }
580        /**
581         * Graphics files are saved in directory baseDirName/Graphviz.
582         * If baseDirName is a relative pathname, then it is placed in the users Desktop folder.
583         * baseDirName directory is created if it does not already exist.
584         * If baseDirName/Graphviz exists, then numbers are appended to the directory name:
585         * "baseDirName/Graphviz 1", "baseDirName/Graphviz 2", etc.
586         */
587        public static void setBaseDirName (String baseDirName) {
588                if (baseDirName == null) throw new Error ("\n!!!! no nulls please");            
589                GraphvizBuilder.baseDirName = baseDirName;
590        }
591        private static String baseDirName = "GraphvizOutput";
592
593        private static boolean isWindows () {
594                String osName = System.getProperty("os.name");
595                if (osName == null) {
596                        throw new Error("\n!!! os.name not found");
597                }
598                osName = osName.toLowerCase(Locale.ENGLISH);
599                return osName.contains("windows");
600        }
601        private static String getDesktop () {
602                if (isWindows()) {
603                        // Supposedly this selects the windows desktop folder:
604                        return javax.swing.filechooser.FileSystemView.getFileSystemView().getHomeDirectory().toString();
605                } else {
606                        return System.getProperty ("user.home") + File.separator + "Desktop";
607                }
608        }
609        private static String dirName = null;
610        private static void setDirName () {
611                // create dir
612                File dir = new File (baseDirName);
613                if (!dir.isAbsolute ()) {
614                        baseDirName = getDesktop() + File.separator + baseDirName;
615                        dir = new File (baseDirName);
616                }
617                if (dir.exists ()) {
618                        if (!dir.isDirectory ()) 
619                                throw new Error ("\n!!!! \"" + dir + "\" is not a directory");
620                        if (!dir.canWrite ()) 
621                                throw new Error ("\n!!!! Unable to write directory: \"" + dir + "\"");
622                } else {
623                        dir.mkdirs ();
624                }
625
626                // create newDir
627                String mainClassName = "Graphviz";
628                String prefix = baseDirName + File.separator;
629                File newDir = new File (prefix + mainClassName);
630                int suffix = 0;
631                while (newDir.exists()) { 
632                        suffix++; 
633                        newDir = new File(prefix + mainClassName + " " + suffix);
634                }
635                newDir.mkdir ();
636
637                if (!newDir.isDirectory () || !newDir.canWrite ())
638                        throw new Error ("Failed setOutputDirectory \"" + newDir + "\"");
639                dirName = newDir + File.separator;
640        }
641}