|
|
|
@ -1,98 +1,49 @@ |
|
|
|
|
package main |
|
|
|
|
// Package view implements rendering a graph to a terminal.
|
|
|
|
|
package view |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
"sort" |
|
|
|
|
|
|
|
|
|
"github.com/mediocregopher/ginger/gg" |
|
|
|
|
"github.com/mediocregopher/ginger/gim/constraint" |
|
|
|
|
"github.com/mediocregopher/ginger/gim/geo" |
|
|
|
|
"github.com/mediocregopher/ginger/gim/terminal" |
|
|
|
|
"github.com/mediocregopher/ginger/gim/view/constraint" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
// "Solves" vertex position by detemining relative positions of vertices in
|
|
|
|
|
// primary and secondary directions (independently), with relative positions
|
|
|
|
|
// being described by "levels", where multiple vertices can occupy one level.
|
|
|
|
|
//
|
|
|
|
|
// Primary determines relative position in the primary direction by trying
|
|
|
|
|
// to place vertices before their outs and after their ins.
|
|
|
|
|
//
|
|
|
|
|
// Secondary determines relative position in the secondary direction by
|
|
|
|
|
// trying to place vertices relative to vertices they share an edge with in
|
|
|
|
|
// the order that the edges appear on the shared node.
|
|
|
|
|
func posSolve(g *gg.Graph) ([][]*gg.Vertex, map[string]int, map[string]int) { |
|
|
|
|
primEng := constraint.NewEngine() |
|
|
|
|
secEng := constraint.NewEngine() |
|
|
|
|
|
|
|
|
|
strM := g.ByID() |
|
|
|
|
for _, v := range strM { |
|
|
|
|
var prevIn *gg.Vertex |
|
|
|
|
for _, e := range v.In { |
|
|
|
|
primEng.AddConstraint(constraint.Constraint{ |
|
|
|
|
Elem: e.From.ID, |
|
|
|
|
LT: v.ID, |
|
|
|
|
}) |
|
|
|
|
if prevIn != nil { |
|
|
|
|
secEng.AddConstraint(constraint.Constraint{ |
|
|
|
|
Elem: prevIn.ID, |
|
|
|
|
LT: e.From.ID, |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
prevIn = e.From |
|
|
|
|
} |
|
|
|
|
// View wraps a single Graph instance and a set of display options for it, and
|
|
|
|
|
// generates renderable terminal output for it.
|
|
|
|
|
type View struct { |
|
|
|
|
g *gg.Graph |
|
|
|
|
start gg.Value |
|
|
|
|
|
|
|
|
|
var prevOut *gg.Vertex |
|
|
|
|
for _, e := range v.Out { |
|
|
|
|
if prevOut == nil { |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
secEng.AddConstraint(constraint.Constraint{ |
|
|
|
|
Elem: prevOut.ID, |
|
|
|
|
LT: e.To.ID, |
|
|
|
|
}) |
|
|
|
|
prevOut = e.To |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
prim := primEng.Solve() |
|
|
|
|
sec := secEng.Solve() |
|
|
|
|
|
|
|
|
|
// determine maximum primary level
|
|
|
|
|
var maxPrim int |
|
|
|
|
for _, lvl := range prim { |
|
|
|
|
if lvl > maxPrim { |
|
|
|
|
maxPrim = lvl |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
outStr := make([][]string, maxPrim+1) |
|
|
|
|
for v, lvl := range prim { |
|
|
|
|
outStr[lvl] = append(outStr[lvl], v) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// sort each primary level
|
|
|
|
|
for _, vv := range outStr { |
|
|
|
|
sort.Slice(vv, func(i, j int) bool { |
|
|
|
|
return sec[vv[i]] < sec[vv[j]] |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// convert to vertices
|
|
|
|
|
out := make([][]*gg.Vertex, len(outStr)) |
|
|
|
|
for i, vv := range outStr { |
|
|
|
|
out[i] = make([]*gg.Vertex, len(outStr[i])) |
|
|
|
|
for j, v := range vv { |
|
|
|
|
out[i][j] = strM[v] |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return out, prim, sec |
|
|
|
|
primFlowDir, secFlowDir geo.XY |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type view struct { |
|
|
|
|
g *gg.Graph |
|
|
|
|
primFlowDir, secFlowDir geo.XY |
|
|
|
|
start gg.Value |
|
|
|
|
// New instantiates and returns a view around the given Graph instance, with
|
|
|
|
|
// start indicating the value vertex to consider the "root" of the graph.
|
|
|
|
|
//
|
|
|
|
|
// Drawing is done by aligning the vertices into rows and columns in such a way
|
|
|
|
|
// as to reduce edge crossings. primaryDir indicates the direction edges will
|
|
|
|
|
// primarily be pointed in. For example, if it is geo.Down then adjacent
|
|
|
|
|
// vertices will be arranged into columns.
|
|
|
|
|
//
|
|
|
|
|
// secondaryDir indicates the direction vertices should be arranged when they
|
|
|
|
|
// end up in the same "rank" (e.g. when primaryDir is geo.Down, all vertices on
|
|
|
|
|
// the same row will be the same "rank").
|
|
|
|
|
//
|
|
|
|
|
// A primaryDir/secondaryDir of either geo.Down/geo.Right or geo.Right/geo.Down
|
|
|
|
|
// are recommended, but any combination of perpendicular directions is allowed.
|
|
|
|
|
func New(g *gg.Graph, start gg.Value, primaryDir, secondaryDir geo.XY) *View { |
|
|
|
|
return &View{ |
|
|
|
|
g: g, |
|
|
|
|
start: start, |
|
|
|
|
primFlowDir: primaryDir, |
|
|
|
|
secFlowDir: secondaryDir, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (view *view) draw(buf *terminal.Buffer) { |
|
|
|
|
// Draw renders and draws the View's Graph to the Buffer.
|
|
|
|
|
func (view *View) Draw(buf *terminal.Buffer) { |
|
|
|
|
relPos, _, secSol := posSolve(view.g) |
|
|
|
|
|
|
|
|
|
// create boxes
|
|
|
|
@ -183,3 +134,80 @@ func (view *view) draw(buf *terminal.Buffer) { |
|
|
|
|
line.draw(buf, view.primFlowDir, view.secFlowDir) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// "Solves" vertex position by detemining relative positions of vertices in
|
|
|
|
|
// primary and secondary directions (independently), with relative positions
|
|
|
|
|
// being described by "levels", where multiple vertices can occupy one level.
|
|
|
|
|
//
|
|
|
|
|
// Primary determines relative position in the primary direction by trying
|
|
|
|
|
// to place vertices before their outs and after their ins.
|
|
|
|
|
//
|
|
|
|
|
// Secondary determines relative position in the secondary direction by
|
|
|
|
|
// trying to place vertices relative to vertices they share an edge with in
|
|
|
|
|
// the order that the edges appear on the shared node.
|
|
|
|
|
func posSolve(g *gg.Graph) ([][]*gg.Vertex, map[string]int, map[string]int) { |
|
|
|
|
primEng := constraint.NewEngine() |
|
|
|
|
secEng := constraint.NewEngine() |
|
|
|
|
|
|
|
|
|
strM := g.ByID() |
|
|
|
|
for _, v := range strM { |
|
|
|
|
var prevIn *gg.Vertex |
|
|
|
|
for _, e := range v.In { |
|
|
|
|
primEng.AddConstraint(constraint.Constraint{ |
|
|
|
|
Elem: e.From.ID, |
|
|
|
|
LT: v.ID, |
|
|
|
|
}) |
|
|
|
|
if prevIn != nil { |
|
|
|
|
secEng.AddConstraint(constraint.Constraint{ |
|
|
|
|
Elem: prevIn.ID, |
|
|
|
|
LT: e.From.ID, |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
prevIn = e.From |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var prevOut *gg.Vertex |
|
|
|
|
for _, e := range v.Out { |
|
|
|
|
if prevOut == nil { |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
secEng.AddConstraint(constraint.Constraint{ |
|
|
|
|
Elem: prevOut.ID, |
|
|
|
|
LT: e.To.ID, |
|
|
|
|
}) |
|
|
|
|
prevOut = e.To |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
prim := primEng.Solve() |
|
|
|
|
sec := secEng.Solve() |
|
|
|
|
|
|
|
|
|
// determine maximum primary level
|
|
|
|
|
var maxPrim int |
|
|
|
|
for _, lvl := range prim { |
|
|
|
|
if lvl > maxPrim { |
|
|
|
|
maxPrim = lvl |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
outStr := make([][]string, maxPrim+1) |
|
|
|
|
for v, lvl := range prim { |
|
|
|
|
outStr[lvl] = append(outStr[lvl], v) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// sort each primary level
|
|
|
|
|
for _, vv := range outStr { |
|
|
|
|
sort.Slice(vv, func(i, j int) bool { |
|
|
|
|
return sec[vv[i]] < sec[vv[j]] |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// convert to vertices
|
|
|
|
|
out := make([][]*gg.Vertex, len(outStr)) |
|
|
|
|
for i, vv := range outStr { |
|
|
|
|
out[i] = make([]*gg.Vertex, len(outStr[i])) |
|
|
|
|
for j, v := range vv { |
|
|
|
|
out[i][j] = strM[v] |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return out, prim, sec |
|
|
|
|
} |