;;; -*- mode: Lisp; Syntax: Common-Lisp; -*-
;;;
;;; Copyright (c) 2009 by the authors.
;;;
;;; See LICENCE for details.
(in-package :hu.dwim.presentation)
;;;;;;
;;; graph/widget
(def (component e) graph/widget (standard/widget cl-graph:dot-graph)
((x :type number)
(y :type number)
(width :type number)
(height :type number)
(scale 1 :type number)
(max-width 400 :type number)
(max-height 400 :type number)
(border-width nil :type number))
(:default-initargs
:vertex-class 'vertex/widget
:directed-edge-class 'directed-edge/widget
:undirected-edge-class 'edge/widget))
(def (macro e) graph/widget ((&rest args &key &allow-other-keys) &body vertices-and-edges)
`(aprog1 (make-instance 'graph/widget ,@args)
(add-vertices-and-edges it (list ,@vertices-and-edges))))
(def refresh-component graph/widget
"Layouts the graph using graphviz through CFFI. The graph's coordinate system origin is the bottom left, width and height are increasing right and up."
(bind (((:slots x y width height max-width max-height scale) -self-))
(cl-graph::iterate-nodes -self-
(lambda (node)
;; TODO: handle node shapes
;; (setf (getf (dot-attributes node) :shape) (shape-of node))
(setf (getf (cl-graph:dot-attributes node) :shape) :box)
(setf (getf (cl-graph:dot-attributes node) :fixedsize) t)
(compute-vertex-size node (content-of node))))
(cl-graph:iterate-edges -self- (lambda (edge)
(setf (getf (cl-graph:dot-attributes edge) :label) (label-of edge))
(awhen (head-arrow-of edge)
(setf (getf (cl-graph:dot-attributes edge) :arrowhead) (shape-of it)))
(awhen (tail-arrow-of edge)
(setf (getf (cl-graph:dot-attributes edge) :arrowtail) (shape-of it)))))
(cl-graph:layout-graph-with-graphviz -self-)
;; store graph coordinates
(bind ((((blx bly) (urx ury)) (cl-graph:dot-attribute-value :bb -self-)))
(setf x blx
y bly
width (- urx blx)
height (- ury bly)))
;; store edge coordinates
(cl-graph:iterate-edges -self-
(lambda (edge)
(bind (((&optional xc yc) (cl-graph:dot-attribute-value :lp edge)))
(setf (points-of edge) (mapcar [list (first !1) (second !1)]
(cl-graph:dot-attribute-value :pos edge))
(label-x-of edge) xc
(label-y-of edge) yc))))
;; store vertex coordinates
(cl-graph::iterate-nodes -self-
(lambda (vertex)
(bind (((xc yc) (cl-graph:dot-attribute-value :pos vertex))
(width (coerce (cl-graph:width-in-pixels vertex) 'float))
(height (coerce (cl-graph:height-in-pixels vertex) 'float)))
(setf (x-of vertex) (- xc (/ width 2.0))
(y-of vertex) (- yc (/ height 2.0))
(width-of vertex) width
(height-of vertex) height))))
;; make sure the graph will fit
(when (> (* scale width) max-width)
(setf scale (/ max-width width)))
(when (> (* scale height) max-height)
(setf scale (/ max-height height)))))
(def render-text graph/widget
(render-component "Graph omitted from text output."))
(def render-xhtml graph/widget ()
(bind (((:read-only-slots width height style-class custom-style id) -self-))
>))
(marker "normal-arrow-start" :path "M 10 0 L 0 5 L 10 10 z" :refX 8 :refY 5)
(marker "normal-arrow-end" :path "M 0 0 L 10 5 L 0 10 z" :refX 0 :refY 5)
(marker "empty-arrow-start" :path "M 10 0 L 0 5 L 10 10 z" :refX 8 :refY 5 :stroke-width 1 :stroke "black" :fill "white")
(marker "empty-arrow-end" :path "M 0 0 L 10 5 L 0 10 z" :refX 0 :refY 5 :stroke-width 1 :stroke "black" :fill "white")
(marker "reverse-arrow-with-line-start" :path "M 10 5 L 0 0 L 0 10 z M 10 5 L 0 5" :refX 8 :refY 5 :stroke "black" :stroke-width 1 :fill "white")
(marker "reverse-arrow-with-line-end" :path "M 0 5 L 10 0 L 10 10 z M 0 5 L 10 5" :refX 0 :refY 5 :stroke "black" :stroke-width 1 :fill "white")
(marker "filled-diamond-start" :path "M 5 0 L 0 5 L 5 10 L 10 5 z" :refX 8 :refY 5)
(marker "filled-diamond-end" :path "M 5 0 L 10 5 L 5 10 L 0 5 z" :refX 0 :refY 5))>
,(bind ((%graph-height% height))
(declare (special %graph-height%))
(cl-graph::iterate-edges -self- #'render-component)
(values))>))))>
>))
(def render-odt graph/widget
)
(def function add-vertices-and-edges (graph vertices-and-edges)
(dolist (vertice-or-edge vertices-and-edges)
(etypecase vertice-or-edge
(vertex/widget
(cl-graph:add-vertex graph vertice-or-edge))
(edge/widget
(bind ((vertices (collect-if (of-type 'vertex/widget) vertices-and-edges)))
(setf (slot-value vertice-or-edge 'cl-graph:vertex-1) (find (cl-graph:vertex-1 vertice-or-edge) vertices :key #'cl-graph:vertex-id))
(setf (slot-value vertice-or-edge 'cl-graph:vertex-2) (find (cl-graph:vertex-2 vertice-or-edge) vertices :key #'cl-graph:vertex-id))
(cl-graph:add-edge graph vertice-or-edge))))))
;;;;;;
;;; vertex/widget
(def (component e) vertex/widget (standard/widget cl-graph:dot-vertex)
((x :type number)
(y :type number)
(width nil :type number)
(height nil :type number)
(shape :box :type (member :box))
(border-width 1 :type number)
(content nil :type component)))
(def constructor vertex/widget
(setf (slot-value -self- 'cl-graph:element) (cl-graph:vertex-id -self-)))
(def (macro e) vertex/widget ((&rest args &key &allow-other-keys) &body content)
`(make-instance 'vertex/widget ,@args :content ,(the-only-element content)))
(def method component-style-class ((self vertex/widget))
(string+ "content-border " (call-next-method)))
(def render-xhtml vertex/widget
(bind (((:read-only-slots x y width height content style-class custom-style id) -self-))
))
(def generic compute-vertex-size (vertex content)
(:method (vertex (content (eql nil)))
(values))
(:method (vertex content)
(setf (getf (cl-graph:dot-attributes vertex) :width) (or (width-of vertex) 1))
(setf (getf (cl-graph:dot-attributes vertex) :height) (or (height-of vertex) 1))))
;;;;;;
;;; edge/widget
(def (component e) edge/widget (standard/widget cl-graph:dot-edge)
((points :type list)
(width 1 :type number)
(label nil :type component)
(label-x :type number)
(label-y :type number)
(head-arrow nil :type arrow/widget)
(tail-arrow nil :type arrow/widget)))
(def (macro e) edge/widget ((&rest args &key &allow-other-keys) &body label)
`(make-instance 'edge/widget ,@args :label ,(the-only-element label)))
(def render-xhtml edge/widget
(flet ((p->string (p)
(string+ (princ-to-string (first p)) "," (princ-to-string (svg-y (second p)))))
(arrow-marker (arrow type)
(when arrow
(string+ "url(#" (string-downcase (symbol-name (shape-of arrow))) "-arrow-" type ")"))))
(bind (((:read-only-slots points head-arrow tail-arrow label label-x label-y style-class custom-style id) -self-)
(bezier-points points)
(points-length (length bezier-points)))
(unless (zerop points-length)
(iter (for p1 first (car bezier-points) then p4)
(for (p2 p3 p4 more) on (cdr bezier-points) by #'cdddr)
(for i :from 1)
string p1)
" C" (p->string p2)
" " (p->string p3)
" " (p->string p4))
:marker-end ,(when (= i (/ (1- points-length) 3))
(arrow-marker head-arrow "end"))
:marker-start ,(when (first-iteration-p)
(arrow-marker tail-arrow "start"))
:fill "none" :stroke "brown" :stroke-width 2)>))
(when (and label-x label-y)
))))
;;;;;;
;;; directed-edge/widget
(def (component e) directed-edge/widget (edge/widget cl-graph:dot-directed-edge)
())
;;;;;;
;;; arrow/widget
(def (component e) arrow/widget (standard/widget)
((shape :type symbol)))
(def (macro e) arrow/widget (&rest args &key &allow-other-keys)
`(make-instance 'arrow/widget ,@args))
;;;;;;
;;; Util
;; converts to physical SVG coordinates
(def function svg-y (y)
(declare (special %graph-height%))
(- %graph-height% y))