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 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
|
;;; cl-typesetting/cl-typegraph copyright 2003-2004 Marc Battyani see license.txt for the details
;;; You can reach me at marc.battyani@fractalconcept.com or marc@battyani.net
;;; The homepage of cl-typesetting is here: http://www.fractalconcept.com/asp/html/cl-typesetting.html
(in-package typeset)
;(defparameter *dot-command* "dot -Tps ~s -o ~s")
(defparameter *dot-command* "dot")
(defparameter *dot-command-args* '("-Tplain-ext"))
(defparameter *graph-file-prefix* "/tmp/")
(defparameter *arrow-width* 2)
(defparameter *arrow-length* 6)
(defparameter *edge-label-font* (pdf:get-font "helvetica"))
(defparameter *edge-label-font-size* 9)
(defparameter *node-label-font* (pdf:get-font "helvetica"))
(defparameter *node-label-font-size* 12)
(defvar *graph-id-counter* 0)
(defun make-graph-node-id ()
(format nil "N~d." (incf *graph-id-counter*)))
(defun make-graph-file-id ()
(format nil "F~d" (incf *graph-id-counter*)))
(defclass graph-node ()
((id :accessor id :initform (make-graph-node-id))
(data :accessor data :initarg :data :initform nil)
(dot-attributes :accessor dot-attributes :initarg :dot-attributes :initform nil)
(background-color :accessor background-color :initarg :background-color :initform '(1.0 1.0 1.0))
(border-color :accessor border-color :initarg :border-color :initform '(0.0 0.0 0.0))
(border-width :accessor border-width :initarg :border-width :initform 1)
(shape :accessor shape :initarg :shape :initform :box)
(x :accessor x :initform 0)
(y :accessor y :initform 0)
(dx :accessor dx :initarg :dx :initform 60)
(dy :accessor dy :initarg :dy :initform 15)))
(defclass graph-edge ()
((label :accessor label :initarg :label :initform nil)
(head :accessor head :initarg :head)
(tail :accessor tail :initarg :tail)
(direction :accessor direction :initarg :direction :initform :forward)
(data :accessor data :initarg :data :initform nil)
(dot-attributes :accessor dot-attributes :initarg :dot-attributes :initform nil)
(color :accessor color :initarg :color :initform '(0.0 0.0 0.0))
(width :accessor width :initarg :width :initform 1)
(label-color :accessor label-color :initarg :color :initform '(0.0 0.0 0.0))
(label-x :accessor label-x)
(label-y :accessor label-y)
(points :accessor points :initform ())))
;;; dot-attributes is a list of (attribute value) pairs ex: ("rankdir" "LR")
(defclass graph ()
((nodes :accessor nodes :initform (make-hash-table :test #'equal))
(edges :accessor edges :initform (make-hash-table :test #'equal))
(dot-attributes :accessor dot-attributes :initarg :dot-attributes :initform nil)
(rank-constraints :accessor rank-constraints :initform nil)
(background-color :accessor background-color :initarg :background-color :initform '(1.0 1.0 1.0))
(border-color :accessor border-color :initarg :border-color :initform '(0.0 0.0 0.0))
(border-width :accessor border-width :initarg :border-width :initform 1)
(landscape-layout :accessor landscape-layout :initarg :landscape-layout :initform nil)
(max-dx :accessor max-dx :initarg :max-dx :initform 400)
(max-dy :accessor max-dy :initarg :max-dy :initform 400)
(scale :accessor scale :initform 1)
(dx :accessor dx)
(dy :accessor dy)))
(defmethod initialize-instance :after ((node graph-node) &rest args
&key fixed-height fixed-width graph &allow-other-keys)
(unless (and fixed-width fixed-height)
(adjust-graph-node-size node (data node) fixed-width fixed-height))
(when graph (add-node graph node)))
(defmethod initialize-instance :after ((edge graph-edge) &rest args &key graph &allow-other-keys)
(when graph (add-edge graph edge)))
(defun add-node (graph node)
(setf (gethash (id node) (nodes graph)) node))
(defun add-edge (graph edge)
(setf (gethash (cons (head edge)(tail edge)) (edges graph)) edge))
(defun add-rank-constraint (graph constraint nodes)
(push (cons constraint nodes) (rank-constraints graph)))
(defmethod adjust-graph-node-size ((node graph-node) data fixed-width fixed-height)
(unless fixed-width
(setf (dx node) (+ (pdf::text-width (format nil "~a" data) *node-label-font* *node-label-font-size*) 4)))
(unless fixed-height
(setf (dy node) (+ *node-label-font-size* 4))))
(defmethod adjust-graph-node-size ((node graph-node) (box box) fixed-width fixed-height)
(unless fixed-height
(setf (dy node) (+ (compute-boxes-natural-size (boxes box) #'dy) 4))))
(defun gen-dot-attributes (s attributes &optional comma)
(loop for (attribute value) in attributes do
(if comma
(write-string ", " s)
(setf comma t))
(write-string attribute s)
(write-char #\= s)
(write-line value s)))
(defmethod gen-graph-dot-data ((graph graph) s)
(format s "digraph G {
size=\"~a,~a\";
edge [fontname=~a,fontsize=~a];
"
(/ (max-dx graph) 72.0)(/ (max-dy graph) 72.0)
(pdf:name *edge-label-font*) *edge-label-font-size*)
(loop for (rank-constraint . nodes) in (rank-constraints graph) do
(format s "{rank = ~a; ~{~s;~^ ~}};~%" rank-constraint (mapcar 'id nodes)))
(format s "graph [")
(gen-dot-attributes s (dot-attributes graph))
(format s "];")
(iter (for (id node) in-hashtable (nodes graph))
(gen-graph-dot-data node s))
(iter (for (id edge) in-hashtable (edges graph))
(gen-graph-dot-data edge s))
(format s "}~%"))
(defmethod gen-graph-dot-data ((node graph-node) s)
(format s "~s [shape=~a, fixedsize=true, width=~a, height=~a"
(id node)(shape node)(/ (dx node) 72.0)(/ (dy node) 72.0))
(gen-dot-attributes s (dot-attributes node) t)
(format s "];~%"))
(defmethod gen-graph-dot-data ((edge graph-edge) s)
(format s "~s -> ~s [label=\"~a\", arrowhead=none"
(id (head edge)) (id (tail edge))
(if (label edge) (label edge) ""))
(gen-dot-attributes s (dot-attributes edge) t)
(format s "];~%"))
(defun read-graph-line-values (string)
(when string
(let ((*package* (find-package :keyword)))
(iter (for position first 0 then end)
(for (values object end) = (read-from-string string nil nil :start position))
(while object)
(collect object)))))
(defun process-graph-line (graph values)
(setf (scale graph) (first values)
(dx graph)(* (second values) (scale graph) 72.0)
(dy graph)(* (third values) (scale graph) 72.0)))
(defun process-graph-node-line (graph values)
(let ((node (gethash (pop values) (nodes graph))))
(setf (x node) (* (pop values) 72.0)
(y node) (* (pop values) 72.0))))
(defun process-graph-edge-line (graph values)
(let* ((head (gethash (pop values) (nodes graph)))
(tail (gethash (pop values) (nodes graph)))
(edge (gethash (cons head tail) (edges graph)))
(nb-points (pop values)))
(setf (points edge) (iter (repeat nb-points)
(collect (* (pop values) 72.0))
(collect (* (pop values) 72.0))))
(when (label edge)
(pop values)
(setf (label-x edge) (* (pop values) 72.0)
(label-y edge) (* (pop values) 72.0)))))
;;; this should be changed to use pipes instead of files and adapted to other Lisp implementations.
(defun compute-graph-layout (graph)
(let* ((file-id (make-graph-file-id))
(dot-file (concatenate 'string *graph-file-prefix* file-id ".dot"))
(result-file (concatenate 'string *graph-file-prefix* file-id ".txt")))
(unwind-protect
(progn
(with-open-file (s dot-file :direction :output :if-exists :supersede)
(gen-graph-dot-data graph s))
#+lispworks (sys:call-system (format nil "~a~{ ~s~} ~s -o ~s" *dot-command* *dot-command-args* dot-file result-file) :wait t)
#+cmu (ext:run-program *dot-command* `(,@*dot-command-args* ,dot-file "-o" ,result-file) :wait t)
#+sbcl (sb-ext:run-program *dot-command* `(,@*dot-command-args* ,dot-file "-o" ,result-file) :wait t)
(with-open-file (s result-file :direction :input)
(iter (for line = (read-line s nil))
(while line)
(for (line-type . values) = (read-graph-line-values line))
(case line-type
(:node (process-graph-node-line graph values))
(:edge (process-graph-edge-line graph values))
(:graph (process-graph-line graph values))
(:stop (finish))))))
(progn (ignore-errors (delete-file dot-file))
(ignore-errors (delete-file result-file))))))
(defun graph-box (graph &rest args)
(let ((dx (dx graph))
(dy (dy graph)))
(when (landscape-layout graph)
(rotatef dx dy))
(add-box (apply 'make-instance 'user-drawn-box
:stroke-fn #'(lambda(box x y)
(if (landscape-layout graph)
(pdf:with-saved-state
(pdf:translate x (- y dy))
(pdf:rotate 90)
(stroke graph 0 0))
(stroke graph x y)))
:inline t :dx dx :dy dy
:allow-other-keys t args))))
(defmethod stroke ((graph graph) x y)
(pdf:with-saved-state
(pdf:set-color-fill (background-color graph))
(when (border-width graph)
(pdf:set-color-stroke (border-color graph))
(pdf:set-line-width (border-width graph))
(pdf:basic-rect x y (dx graph)(- (dy graph)))
(pdf:fill-and-stroke))
(pdf:translate x (- y (dy graph)))
(pdf:scale (scale graph)(scale graph))
(iter (for (id edge) in-hashtable (edges graph))
(stroke-edge edge (data edge)))
(iter (for (id node) in-hashtable (nodes graph))
(stroke-node node (data node)))))
(defmethod stroke-node ((node graph-node) data)
(pdf:with-saved-state
(pdf:set-color-fill (background-color node))
(when (border-width node)
(pdf:set-color-stroke (border-color node))
(pdf:set-line-width (border-width node))
(pdf:basic-rect (- (x node)(* (dx node) 0.5))(+ (y node)(* (dy node) 0.5))(dx node)(- (dy node)))
(pdf:fill-and-stroke)))
(stroke-node-content node data))
(defmethod stroke-node-content ((node graph-node) data)
(when data
(pdf:set-color-fill '(0.0 0.0 0.0))
(pdf:draw-centered-text (x node)(- (y node) (* 0.3 *node-label-font-size*))
(format nil "~a" data)
*node-label-font* *node-label-font-size*)))
(defmethod stroke-node-content ((node graph-node) (box box))
(stroke box (- (x node)(* (dx node) 0.5)) (+ (y node)(* (dy node) 0.5))))
(defmethod stroke-edge ((edge graph-edge) data)
(pdf:with-saved-state
(pdf:set-color-stroke (color edge))
(pdf:set-color-fill (color edge))
(pdf:set-line-width (width edge))
(let ((points (points edge))
x1 y1 x2 y2 x3 y3 prev-x1 prev-y1)
(pdf:move-to (pop points)(pop points))
(iter (while points)
(setf prev-x1 x1 prev-y1 y1)
(setf x1 (pop points) y1 (pop points)
x2 (pop points) y2 (pop points)
x3 (pop points) y3 (pop points))
(pdf:bezier-to x1 y1 x2 y2 x3 y3))
(pdf:stroke)
(when (eq (direction edge) :forward)
(let* ((nx (- x1 x3))
(ny (- y1 y3))
(l (sqrt (+ (* nx nx)(* ny ny))))
x0 y0)
(when (zerop l)
(setf nx (- prev-x1 x3) ny (- prev-y1 y3))
(setf l (sqrt (+ (* nx nx)(* ny ny)))))
(setf nx (/ nx l) ny (/ ny l))
(pdf:move-to x3 y3)
(setf x0 (+ x3 (* nx *arrow-length*)) y0 (+ y3 (* ny *arrow-length*))
nx (* nx *arrow-width*) ny (* ny *arrow-width*))
(pdf:line-to (+ x0 ny)(- y0 nx))
(pdf:line-to (- x0 ny)(+ y0 nx))
(pdf:line-to x3 y3)
(pdf:fill-and-stroke)))
(when (label edge)
(pdf:set-color-fill (label-color edge))
(pdf:draw-centered-text (label-x edge)(label-y edge)(label edge)
*edge-label-font* *edge-label-font-size*)))))
|