-
Notifications
You must be signed in to change notification settings - Fork 108
/
io.clj
156 lines (147 loc) · 5.63 KB
/
io.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
(ns ^{:doc "Output and view graphs in various formats"
:author "Justin Kramer"}
loom.io
(:require [loom.graph :refer [directed? weighted? nodes edges weight src dest]]
[loom.alg :refer [distinct-edges loners]]
[loom.attr :refer [attr? attr attrs]]
[clojure.string :refer [escape]]
[clojure.java.io :refer [file]]
[clojure.java.shell :refer [sh]])
(:import (java.io FileWriter
FileOutputStream)))
(defn- dot-esc
[s]
(escape s {\" "\\\"" \newline "\\n"}))
(defn- dot-attrs
[attrs]
(when (seq attrs)
(let [sb (StringBuilder. "[")]
(doseq [[k v] attrs]
(when (pos? (.length (str v)))
(when (< 1 (.length sb))
(.append sb \,))
(doto sb
(.append \")
(.append (dot-esc (if (keyword? k) (name k) (str k))))
(.append "\"=\"")
(.append (dot-esc (if (keyword? v) (name v) (str v))))
(.append \"))))
(.append sb "]")
(str sb))))
(defn dot-str
"Renders graph g as a DOT-format string. Calls (node-label node) and
(edge-label n1 n2) to determine what labels to use for nodes and edges,
if any. Weights become edge labels unless a label is specified.
Labels also include attributes when the graph satisfies AttrGraph."
[g & {:keys [graph-name node-label edge-label]
:or {graph-name "graph"} :as opts }]
(let [d? (directed? g)
w? (weighted? g)
a? (attr? g)
node-label (or node-label
(if a?
#(attr g % :label)
(constantly nil)))
edge-label (or edge-label
(cond
a? #(if-let [a (attr g %1 %2 :label)]
a
(if w? (weight g %1 %2)))
w? #(weight g %1 %2)
:else (constantly nil)))
sb (doto (StringBuilder.
(if d? "digraph \"" "graph \""))
(.append (dot-esc graph-name))
(.append "\" {\n"))]
(doseq [k [:graph :node :edge]]
(when (k opts)
(doto sb
(.append (str " " (name k) " "))
(.append (dot-attrs (k opts))))))
(doseq [edge (distinct-edges g)]
(let [n1 (src edge)
n2 (dest edge)
el (edge-label n1 n2)
eattrs (assoc (if a?
(attrs g n1 n2) {})
:label el)]
(doto sb
(.append (str (hash n1)))
(.append (if d? " -> " " -- "))
(.append (str (hash n2))))
(when (or (:label eattrs) (< 1 (count eattrs)))
(.append sb \space)
(.append sb (dot-attrs eattrs)))
(.append sb "\n")))
(doseq [n (nodes g)]
(let [nl (dot-esc (str (or (node-label n) n)))]
(doto sb
(.append (str (hash n) " [label=\"" nl "\"]"))))
(when-let [nattrs (when a?
(dot-attrs (attrs g n)))]
(.append sb \space)
(.append sb nattrs))
(.append sb "\n"))
(str (doto sb (.append "}")))))
(defn dot
"Writes graph g to f (string or File) in DOT format. args passed to dot-str"
[g f & args]
(spit (str (file f)) (apply dot-str g args)))
(defn- os
"Returns :win, :mac, :unix, or nil"
[]
(condp
#(<= 0 (.indexOf ^String %2 ^String %1))
(.toLowerCase (System/getProperty "os.name"))
"win" :win
"mac" :mac
"nix" :unix
"nux" :unix
nil))
(defn- open
"Opens the given file (a string, File, or file URI) in the default
application for the current desktop environment. Returns nil"
[f]
(let [f (file f)]
;; There's an 'open' method in java.awt.Desktop but it hangs on Windows
;; using Clojure Box and turns the process into a GUI process on Max OS X.
;; Maybe it's ok for Linux?
(condp = (os)
:mac (sh "open" (str f))
:win (sh "cmd" (str "/c start " (-> f .toURI .toURL str)))
:unix (sh "xdg-open" (str f)))
nil))
(defn- open-data
"Writes the given data (string or bytes) to a temporary file with the
given extension (string or keyword, with or without the dot) and then open
it in the default application for that extension in the current desktop
environment. Returns nil"
[data ext]
(let [ext (name ext)
ext (if (= \. (first ext)) ext (str \. ext))
tmp (java.io.File/createTempFile (subs ext 1) ext)]
(if (string? data)
(with-open [w (java.io.FileWriter. tmp)]
(.write w ^String data))
(with-open [w (java.io.FileOutputStream. tmp)]
(.write w ^bytes data)))
(.deleteOnExit tmp)
(open tmp)))
(defn render-to-bytes
"Renders the graph g in the image format using GraphViz and returns data
as a byte array.
Requires GraphViz's 'dot' (or a specified algorithm) to be installed in
the shell's path. Possible algorithms include :dot, :neato, :fdp, :sfdp,
:twopi, and :circo. Possible formats include :png, :ps, :pdf, and :svg."
[g & {:keys [alg fmt] :or {alg "dot" fmt :png} :as opts}]
(let [dot (apply dot-str g (apply concat opts))
{:keys [out]} (sh (name alg) (str "-T" (name fmt)) :in dot :out-enc :bytes)]
out))
(defn view
"Converts graph g to a temporary image file using GraphViz and opens it
in the current desktop environment's default viewer for said files.
Requires GraphViz's 'dot' (or a specified algorithm) to be installed in
the shell's path. Possible algorithms include :dot, :neato, :fdp, :sfdp,
:twopi, and :circo. Possible formats include :png, :ps, :pdf, and :svg."
[g & {:keys [fmt] :or {fmt :png} :as opts}]
(open-data (apply render-to-bytes g (apply concat opts)) fmt))