(require 'org-element)
(require 's)
NOTE
(defun trc/wg--make-connection (dependency-id relation-type &rest props)
"Encode a connection to DEPENDENCY-ID node.
RELATION-TYPE is one of the supported relation types.
PROPS are other properties, unspecified as of yet."
(list* :dependency-id dependency-id :relation-type relation-type props))
(defun trc/wg--make-node (id title todo-state tags first-sentence)
"Encode a node representation.
ID is an org-id, TITLE is unformatted headline content,
TODO-STATE is the TODO keyword, TAGS are org mode tags,
FIRST-SENTENCE is the first sentence of the entry."
(list :id id :title title :todo-state todo-state :tags tags :first-sentence first-sentence))
(defun trc/wg--node-eligible-p (headline)
"True if HEADLINE is eligible for graphing."
(not (null (org-element-property :ID headline))))
(defun trc/wg--node-identifier-from-org-headline (headline)
"Compute an identifier to use for the node from its HEADLINE.
That is either org-id or its title."
(or (org-element-property :ID headline) (org-element-property :raw-value headline)))
(defun trc/wg--node-from-org-headline (headline)
"Turn a parsed org mode HEADLINE into a NODE."
(trc/wg--make-node (trc/wg--node-identifier-from-org-headline headline)
(org-element-property :raw-value headline)
(org-element-property :todo-keyword headline)
(org-element-property :tags headline)
nil TODO ))
(defun trc/wg--parse-edna-blockers (blockers)
"Turn BLOCKERS into a list of (ID TYPE).
BLOCKERS are a string the form: id(foo bar baz)."
(when (and blockers
(string-match "ids(\\(.*\\))" blockers))
FIXME (split-string (match-string 1 blockers))))
(defun trc/wg--connections-from-org-headline (headline)
"Compute all immediate connections for a HEADLINE.
Return value is a list of entries, each of the form:
(ID (CONNECTION-DATA)), which indicates a node of
ID is connected to another."
(let ((connections (list)))
(let ((node-id (trc/wg--node-identifier-from-org-headline headline))
(parent-id (trc/wg--node-identifier-from-org-headline (org-element-property :parent headline)))
(edna-blockers (trc/wg--parse-edna-blockers (org-element-property :BLOCKER headline))))
(when (trc/wg--node-eligible-p (org-element-property :parent headline))
(push (list parent-id (trc/wg--make-connection node-id :finish-to-finish))
connections))
(dolist (blocker edna-blockers)
(push (list node-id (trc/wg--make-connection blocker :finish-to-start)) connections)))
(reverse connections)))
(defun trc/wg--compute-node-label (node)
"Return the label to use for the NODE."
(let ((title (plist-get node :title))
(printable-tags (remove "milestone" (plist-get node :tags))))
(if printable-tags
(format "<%s<br/><font point-size=\"9\">%s</font>>" title (s-join ":" printable-tags))
(format "<%s>" title))))
(defun trc/wg--compute-node-attributes (node)
"Return a string with extra attributes to style the NODE."
(let ((color "black")
(fontcolor "black")
(shape "box")
(styles (list)))
(let ((todo-kw (plist-get node :todo-state))
(tags (plist-get node :tags)))
(when (member "milestone" tags)
(setf shape "septagon"))
(setf color (cond ((equalp todo-kw "TODO")
"red")
((equalp todo-kw "DOING")
"orange")
((equalp todo-kw "DONE")
"darkolivegreen3")
(t "black")))
(unless (null todo-kw)
(push "rounded" styles))
(when (equalp todo-kw "DONE")
(push "dashed" styles)
(setf fontcolor "darkslategrey")))
(format "color=\"%s\",fontcolor=\"%s\"shape=\"%s\",style=\"%s\"" color fontcolor shape (s-join "," styles))))
(defun trc/wg--compute-edge-label (connection)
"Compute label to be put on edge of a CONNECTION, if any."
TODO ""
)
(defun trc/wg--compute-edge-attributes (connection)
"Compute additional styling for CONNECTION edge."
(if (eq (plist-get connection :relation-type) :finish-to-finish)
"arrowhead=\"onormal\""
""))
(defun trc/wg--graphviz-encode-node (node)
"Write out NODE definition for graphviz."
(insert (format "\"%s\" [label=%s,%s]\n"
(plist-get node :id)
(trc/wg--compute-node-label node)
(trc/wg--compute-node-attributes node))))
(defun trc/wg--graphviz-encode-connection (from connection)
"Write out graphviz edge.
FROM is the id of the source node, CONNECTION specifies
the target and properties of the edge."
TODO (insert (format "\"%s\" -> \"%s\" [label=\"%s\",%s]\n"
from
(plist-get connection :dependency-id)
(trc/wg--compute-edge-label connection)
(trc/wg--compute-edge-attributes connection))))
(defun trc/compute-org-task-graph ()
"Return a graph for the org document, which is a (cons connectome nodelist)."
(let ((connectome (make-hash-table :test 'equal))
(nodelist (list)))
(org-element-map (org-element-parse-buffer) 'headline
(lambda (item)
(when (trc/wg--node-eligible-p item)
(dolist (connection (trc/wg--connections-from-org-headline item))
(destructuring-bind (source-id link) connection
(puthash source-id (cons link (gethash source-id connectome (list))) connectome)))
(push (trc/wg--node-from-org-headline item) nodelist))
(values)))
(list connectome (reverse nodelist))))
(defun trc/org-task-graph-to-graphviz (connectome node-list)
"Generate a dot graph from CONNECTOME and NODE-LIST."
(with-temp-buffer
(insert "digraph G {\n")
(insert "ranksep=0.5\n")
(insert "nodesep=0.5\n")
(insert "overlap=\"false\"\n")
(insert "node [color=\"black\", fontsize=10, margin=\"0.055\" style=\"rounded\"]\n")
(insert "edge [fontsize=10]\n")
(mapc #'trc/wg--graphviz-encode-node node-list)
(maphash (lambda (k v)
(dolist (conn v)
(trc/wg--graphviz-encode-connection k conn)))
connectome)
(insert "}\n")
(buffer-string)))
(provide 'trc-workgraph)