mirror of
https://github.com/gonum/gonum.git
synced 2025-10-16 20:20:41 +08:00
graph/encoding/dot: add multigraph serialization and deserialization support
This commit is contained in:
@@ -50,6 +50,19 @@ func Unmarshal(data []byte, dst encoding.Builder) error {
|
|||||||
return copyGraph(dst, file.Graphs[0])
|
return copyGraph(dst, file.Graphs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalMulti parses the Graphviz DOT-encoded data as a multigraph and
|
||||||
|
// stores the result in dst.
|
||||||
|
func UnmarshalMulti(data []byte, dst encoding.MultiBuilder) error {
|
||||||
|
file, err := dot.ParseBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(file.Graphs) != 1 {
|
||||||
|
return fmt.Errorf("invalid number of graphs; expected 1, got %d", len(file.Graphs))
|
||||||
|
}
|
||||||
|
return copyMultigraph(dst, file.Graphs[0])
|
||||||
|
}
|
||||||
|
|
||||||
// copyGraph copies the nodes and edges from the Graphviz AST source graph to
|
// copyGraph copies the nodes and edges from the Graphviz AST source graph to
|
||||||
// the destination graph. Edge direction is maintained if present.
|
// the destination graph. Edge direction is maintained if present.
|
||||||
func copyGraph(dst encoding.Builder, src *ast.Graph) (err error) {
|
func copyGraph(dst encoding.Builder, src *ast.Graph) (err error) {
|
||||||
@@ -62,9 +75,41 @@ func copyGraph(dst encoding.Builder, src *ast.Graph) (err error) {
|
|||||||
panic(e)
|
panic(e)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
gen := &generator{
|
gen := &simpleGraph{
|
||||||
directed: src.Directed,
|
generator: generator{
|
||||||
ids: make(map[string]graph.Node),
|
directed: src.Directed,
|
||||||
|
ids: make(map[string]graph.Node),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if dst, ok := dst.(DOTIDSetter); ok {
|
||||||
|
dst.SetDOTID(src.ID)
|
||||||
|
}
|
||||||
|
if a, ok := dst.(AttributeSetters); ok {
|
||||||
|
gen.graphAttr, gen.nodeAttr, gen.edgeAttr = a.DOTAttributeSetters()
|
||||||
|
}
|
||||||
|
for _, stmt := range src.Stmts {
|
||||||
|
gen.addStmt(dst, stmt)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyMultigraph copies the nodes and edges from the Graphviz AST source graph to
|
||||||
|
// the destination graph. Edge direction is maintained if present.
|
||||||
|
func copyMultigraph(dst encoding.MultiBuilder, src *ast.Graph) (err error) {
|
||||||
|
defer func() {
|
||||||
|
switch e := recover().(type) {
|
||||||
|
case nil:
|
||||||
|
case error:
|
||||||
|
err = e
|
||||||
|
default:
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
gen := &multiGraph{
|
||||||
|
generator: generator{
|
||||||
|
directed: src.Directed,
|
||||||
|
ids: make(map[string]graph.Node),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if dst, ok := dst.(DOTIDSetter); ok {
|
if dst, ok := dst.(DOTIDSetter); ok {
|
||||||
dst.SetDOTID(src.ID)
|
dst.SetDOTID(src.ID)
|
||||||
@@ -97,7 +142,7 @@ type generator struct {
|
|||||||
|
|
||||||
// node returns the gonum node corresponding to the given dot AST node ID,
|
// node returns the gonum node corresponding to the given dot AST node ID,
|
||||||
// generating a new such node if none exist.
|
// generating a new such node if none exist.
|
||||||
func (gen *generator) node(dst encoding.Builder, id string) graph.Node {
|
func (gen *generator) node(dst graph.NodeAdder, id string) graph.Node {
|
||||||
if n, ok := gen.ids[id]; ok {
|
if n, ok := gen.ids[id]; ok {
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
@@ -117,8 +162,10 @@ func (gen *generator) node(dst encoding.Builder, id string) graph.Node {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type simpleGraph struct{ generator }
|
||||||
|
|
||||||
// addStmt adds the given statement to the graph.
|
// addStmt adds the given statement to the graph.
|
||||||
func (gen *generator) addStmt(dst encoding.Builder, stmt ast.Stmt) {
|
func (gen *simpleGraph) addStmt(dst encoding.Builder, stmt ast.Stmt) {
|
||||||
switch stmt := stmt.(type) {
|
switch stmt := stmt.(type) {
|
||||||
case *ast.NodeStmt:
|
case *ast.NodeStmt:
|
||||||
n, ok := gen.node(dst, stmt.Node.ID).(encoding.AttributeSetter)
|
n, ok := gen.node(dst, stmt.Node.ID).(encoding.AttributeSetter)
|
||||||
@@ -206,7 +253,7 @@ func applyPortsToEdge(from ast.Vertex, to *ast.Edge, edge graph.Edge) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addEdgeStmt adds the given edge statement to the graph.
|
// addEdgeStmt adds the given edge statement to the graph.
|
||||||
func (gen *generator) addEdgeStmt(dst encoding.Builder, stmt *ast.EdgeStmt) {
|
func (gen *simpleGraph) addEdgeStmt(dst encoding.Builder, stmt *ast.EdgeStmt) {
|
||||||
fs := gen.addVertex(dst, stmt.From)
|
fs := gen.addVertex(dst, stmt.From)
|
||||||
ts := gen.addEdge(dst, stmt.To, stmt.Attrs)
|
ts := gen.addEdge(dst, stmt.To, stmt.Attrs)
|
||||||
for _, f := range fs {
|
for _, f := range fs {
|
||||||
@@ -220,7 +267,7 @@ func (gen *generator) addEdgeStmt(dst encoding.Builder, stmt *ast.EdgeStmt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addVertex adds the given vertex to the graph, and returns its set of nodes.
|
// addVertex adds the given vertex to the graph, and returns its set of nodes.
|
||||||
func (gen *generator) addVertex(dst encoding.Builder, v ast.Vertex) []graph.Node {
|
func (gen *simpleGraph) addVertex(dst encoding.Builder, v ast.Vertex) []graph.Node {
|
||||||
switch v := v.(type) {
|
switch v := v.(type) {
|
||||||
case *ast.Node:
|
case *ast.Node:
|
||||||
n := gen.node(dst, v.ID)
|
n := gen.node(dst, v.ID)
|
||||||
@@ -237,7 +284,7 @@ func (gen *generator) addVertex(dst encoding.Builder, v ast.Vertex) []graph.Node
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addEdge adds the given edge to the graph, and returns its set of nodes.
|
// addEdge adds the given edge to the graph, and returns its set of nodes.
|
||||||
func (gen *generator) addEdge(dst encoding.Builder, to *ast.Edge, attrs []*ast.Attr) []graph.Node {
|
func (gen *simpleGraph) addEdge(dst encoding.Builder, to *ast.Edge, attrs []*ast.Attr) []graph.Node {
|
||||||
if !gen.directed && to.Directed {
|
if !gen.directed && to.Directed {
|
||||||
panic(fmt.Errorf("directed edge to %v in undirected graph", to.Vertex))
|
panic(fmt.Errorf("directed edge to %v in undirected graph", to.Vertex))
|
||||||
}
|
}
|
||||||
@@ -307,6 +354,123 @@ func (gen *generator) appendSubgraphNode(n graph.Node) {
|
|||||||
gen.subNodes = append(gen.subNodes, n)
|
gen.subNodes = append(gen.subNodes, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type multiGraph struct{ generator }
|
||||||
|
|
||||||
|
// addStmt adds the given statement to the multigraph.
|
||||||
|
func (gen *multiGraph) addStmt(dst encoding.MultiBuilder, stmt ast.Stmt) {
|
||||||
|
switch stmt := stmt.(type) {
|
||||||
|
case *ast.NodeStmt:
|
||||||
|
n, ok := gen.node(dst, stmt.Node.ID).(encoding.AttributeSetter)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, attr := range stmt.Attrs {
|
||||||
|
a := encoding.Attribute{
|
||||||
|
Key: attr.Key,
|
||||||
|
Value: attr.Val,
|
||||||
|
}
|
||||||
|
if err := n.SetAttribute(a); err != nil {
|
||||||
|
panic(fmt.Errorf("unable to unmarshal node DOT attribute (%s=%s)", a.Key, a.Value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *ast.EdgeStmt:
|
||||||
|
gen.addEdgeStmt(dst, stmt)
|
||||||
|
case *ast.AttrStmt:
|
||||||
|
var n encoding.AttributeSetter
|
||||||
|
var dst string
|
||||||
|
switch stmt.Kind {
|
||||||
|
case ast.GraphKind:
|
||||||
|
if gen.graphAttr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n = gen.graphAttr
|
||||||
|
dst = "graph"
|
||||||
|
case ast.NodeKind:
|
||||||
|
if gen.nodeAttr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n = gen.nodeAttr
|
||||||
|
dst = "node"
|
||||||
|
case ast.EdgeKind:
|
||||||
|
if gen.edgeAttr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n = gen.edgeAttr
|
||||||
|
dst = "edge"
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
for _, attr := range stmt.Attrs {
|
||||||
|
a := encoding.Attribute{
|
||||||
|
Key: attr.Key,
|
||||||
|
Value: attr.Val,
|
||||||
|
}
|
||||||
|
if err := n.SetAttribute(a); err != nil {
|
||||||
|
panic(fmt.Errorf("unable to unmarshal global %s DOT attribute (%s=%s)", dst, a.Key, a.Value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *ast.Attr:
|
||||||
|
// ignore.
|
||||||
|
case *ast.Subgraph:
|
||||||
|
for _, stmt := range stmt.Stmts {
|
||||||
|
gen.addStmt(dst, stmt)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unknown statement type %T", stmt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addEdgeStmt adds the given edge statement to the multigraph.
|
||||||
|
func (gen *multiGraph) addEdgeStmt(dst encoding.MultiBuilder, stmt *ast.EdgeStmt) {
|
||||||
|
fs := gen.addVertex(dst, stmt.From)
|
||||||
|
ts := gen.addLine(dst, stmt.To, stmt.Attrs)
|
||||||
|
for _, f := range fs {
|
||||||
|
for _, t := range ts {
|
||||||
|
edge := dst.NewLine(f, t)
|
||||||
|
dst.SetLine(edge)
|
||||||
|
applyPortsToEdge(stmt.From, stmt.To, edge)
|
||||||
|
addEdgeAttrs(edge, stmt.Attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addVertex adds the given vertex to the multigraph, and returns its set of nodes.
|
||||||
|
func (gen *multiGraph) addVertex(dst encoding.MultiBuilder, v ast.Vertex) []graph.Node {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case *ast.Node:
|
||||||
|
n := gen.node(dst, v.ID)
|
||||||
|
return []graph.Node{n}
|
||||||
|
case *ast.Subgraph:
|
||||||
|
gen.pushSubgraph()
|
||||||
|
for _, stmt := range v.Stmts {
|
||||||
|
gen.addStmt(dst, stmt)
|
||||||
|
}
|
||||||
|
return gen.popSubgraph()
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unknown vertex type %T", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addLine adds the given edge to the multigraph, and returns its set of nodes.
|
||||||
|
func (gen *multiGraph) addLine(dst encoding.MultiBuilder, to *ast.Edge, attrs []*ast.Attr) []graph.Node {
|
||||||
|
if !gen.directed && to.Directed {
|
||||||
|
panic(fmt.Errorf("directed edge to %v in undirected graph", to.Vertex))
|
||||||
|
}
|
||||||
|
fs := gen.addVertex(dst, to.Vertex)
|
||||||
|
if to.To != nil {
|
||||||
|
ts := gen.addLine(dst, to.To, attrs)
|
||||||
|
for _, f := range fs {
|
||||||
|
for _, t := range ts {
|
||||||
|
edge := dst.NewLine(f, t)
|
||||||
|
dst.SetLine(edge)
|
||||||
|
applyPortsToEdge(to.Vertex, to.To, edge)
|
||||||
|
addEdgeAttrs(edge, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
// addEdgeAttrs adds the attributes to the given edge.
|
// addEdgeAttrs adds the attributes to the given edge.
|
||||||
func addEdgeAttrs(edge graph.Edge, attrs []*ast.Attr) {
|
func addEdgeAttrs(edge graph.Edge, attrs []*ast.Attr) {
|
||||||
e, ok := edge.(encoding.AttributeSetter)
|
e, ok := edge.(encoding.AttributeSetter)
|
||||||
|
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"gonum.org/v1/gonum/graph"
|
"gonum.org/v1/gonum/graph"
|
||||||
"gonum.org/v1/gonum/graph/encoding"
|
"gonum.org/v1/gonum/graph/encoding"
|
||||||
|
"gonum.org/v1/gonum/graph/multi"
|
||||||
"gonum.org/v1/gonum/graph/simple"
|
"gonum.org/v1/gonum/graph/simple"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -293,6 +294,107 @@ const undirectedNonchained = `strict graph {
|
|||||||
B -- C [label="baz 2"];
|
B -- C [label="baz 2"];
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
func TestMultigraphDecoding(t *testing.T) {
|
||||||
|
for i, test := range []struct {
|
||||||
|
directed bool
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
directed: true,
|
||||||
|
input: directedMultigraph,
|
||||||
|
expected: directedMultigraph,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
directed: false,
|
||||||
|
input: undirectedMultigraph,
|
||||||
|
expected: undirectedMultigraph,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
directed: true,
|
||||||
|
input: directedSelfLoopMultigraph,
|
||||||
|
expected: directedSelfLoopMultigraph,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
directed: false,
|
||||||
|
input: undirectedSelfLoopMultigraph,
|
||||||
|
expected: undirectedSelfLoopMultigraph,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
var dst encoding.MultiBuilder
|
||||||
|
if test.directed {
|
||||||
|
dst = multi.NewDirectedGraph()
|
||||||
|
} else {
|
||||||
|
dst = multi.NewUndirectedGraph()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := UnmarshalMulti([]byte(test.input), dst); err != nil {
|
||||||
|
t.Errorf("i=%d: unable to unmarshal DOT graph; %v", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buf, err := MarshalMulti(dst, "", "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("i=%d: unable to marshal graph; %v", i, dst)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
actual := string(buf)
|
||||||
|
if actual != test.expected {
|
||||||
|
t.Errorf("i=%d: graph content mismatch; want:\n%s\n\nactual:\n%s", i, test.expected, actual)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const directedMultigraph = `digraph {
|
||||||
|
// Node definitions.
|
||||||
|
0;
|
||||||
|
1;
|
||||||
|
2;
|
||||||
|
|
||||||
|
// Edge definitions.
|
||||||
|
0 -> 1;
|
||||||
|
0 -> 1;
|
||||||
|
0 -> 2;
|
||||||
|
2 -> 0;
|
||||||
|
}`
|
||||||
|
|
||||||
|
const undirectedMultigraph = `graph {
|
||||||
|
// Node definitions.
|
||||||
|
0;
|
||||||
|
1;
|
||||||
|
2;
|
||||||
|
|
||||||
|
// Edge definitions.
|
||||||
|
0 -- 1;
|
||||||
|
0 -- 1;
|
||||||
|
0 -- 2;
|
||||||
|
0 -- 2;
|
||||||
|
}`
|
||||||
|
|
||||||
|
const directedSelfLoopMultigraph = `digraph {
|
||||||
|
// Node definitions.
|
||||||
|
0;
|
||||||
|
1;
|
||||||
|
|
||||||
|
// Edge definitions.
|
||||||
|
0 -> 0;
|
||||||
|
0 -> 0;
|
||||||
|
1 -> 1;
|
||||||
|
1 -> 1;
|
||||||
|
}`
|
||||||
|
|
||||||
|
const undirectedSelfLoopMultigraph = `graph {
|
||||||
|
// Node definitions.
|
||||||
|
0;
|
||||||
|
1;
|
||||||
|
|
||||||
|
// Edge definitions.
|
||||||
|
0 -- 0;
|
||||||
|
0 -- 0;
|
||||||
|
1 -- 1;
|
||||||
|
1 -- 1;
|
||||||
|
}`
|
||||||
|
|
||||||
// Below follows a minimal implementation of a graph capable of validating the
|
// Below follows a minimal implementation of a graph capable of validating the
|
||||||
// round-trip encoding and decoding of DOT graphs with nodes and edges
|
// round-trip encoding and decoding of DOT graphs with nodes and edges
|
||||||
// containing DOT attributes.
|
// containing DOT attributes.
|
||||||
|
@@ -55,17 +55,33 @@ type Structurer interface {
|
|||||||
Structure() []Graph
|
Structure() []Graph
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MultiStructurer represents a graph.Multigraph that can define subgraphs.
|
||||||
|
type MultiStructurer interface {
|
||||||
|
Structure() []Multigraph
|
||||||
|
}
|
||||||
|
|
||||||
// Graph wraps named graph.Graph values.
|
// Graph wraps named graph.Graph values.
|
||||||
type Graph interface {
|
type Graph interface {
|
||||||
graph.Graph
|
graph.Graph
|
||||||
DOTID() string
|
DOTID() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multigraph wraps named graph.Multigraph values.
|
||||||
|
type Multigraph interface {
|
||||||
|
graph.Multigraph
|
||||||
|
DOTID() string
|
||||||
|
}
|
||||||
|
|
||||||
// Subgrapher wraps graph.Node values that represent subgraphs.
|
// Subgrapher wraps graph.Node values that represent subgraphs.
|
||||||
type Subgrapher interface {
|
type Subgrapher interface {
|
||||||
Subgraph() graph.Graph
|
Subgraph() graph.Graph
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MultiSubgrapher wraps graph.Node values that represent subgraphs.
|
||||||
|
type MultiSubgrapher interface {
|
||||||
|
Subgraph() graph.Multigraph
|
||||||
|
}
|
||||||
|
|
||||||
// Marshal returns the DOT encoding for the graph g, applying the prefix
|
// Marshal returns the DOT encoding for the graph g, applying the prefix
|
||||||
// and indent to the encoding. Name is used to specify the graph name. If
|
// and indent to the encoding. Name is used to specify the graph name. If
|
||||||
// name is empty and g implements Graph, the returned string from DOTID
|
// name is empty and g implements Graph, the returned string from DOTID
|
||||||
@@ -76,7 +92,7 @@ type Subgrapher interface {
|
|||||||
// implementation of the Node, Attributer, Porter, Attributers, Structurer,
|
// implementation of the Node, Attributer, Porter, Attributers, Structurer,
|
||||||
// Subgrapher and Graph interfaces.
|
// Subgrapher and Graph interfaces.
|
||||||
func Marshal(g graph.Graph, name, prefix, indent string) ([]byte, error) {
|
func Marshal(g graph.Graph, name, prefix, indent string) ([]byte, error) {
|
||||||
var p printer
|
var p simpleGraphPrinter
|
||||||
p.indent = indent
|
p.indent = indent
|
||||||
p.prefix = prefix
|
p.prefix = prefix
|
||||||
p.visited = make(map[edge]bool)
|
p.visited = make(map[edge]bool)
|
||||||
@@ -87,6 +103,28 @@ func Marshal(g graph.Graph, name, prefix, indent string) ([]byte, error) {
|
|||||||
return p.buf.Bytes(), nil
|
return p.buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalMulti returns the DOT encoding for the multigraph g, applying the
|
||||||
|
// prefix and indent to the encoding. Name is used to specify the graph name. If
|
||||||
|
// name is empty and g implements Graph, the returned string from DOTID
|
||||||
|
// will be used. If strict is true the output bytes will be prefixed with
|
||||||
|
// the DOT "strict" keyword.
|
||||||
|
//
|
||||||
|
// Graph serialization will work for a graph.Multigraph without modification,
|
||||||
|
// however, advanced GraphViz DOT features provided by Marshal depend on
|
||||||
|
// implementation of the Node, Attributer, Porter, Attributers, Structurer,
|
||||||
|
// MultiSubgrapher and Multigraph interfaces.
|
||||||
|
func MarshalMulti(g graph.Multigraph, name, prefix, indent string) ([]byte, error) {
|
||||||
|
var p multiGraphPrinter
|
||||||
|
p.indent = indent
|
||||||
|
p.prefix = prefix
|
||||||
|
p.visited = make(map[line]bool)
|
||||||
|
err := p.print(g, name, false, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
type printer struct {
|
type printer struct {
|
||||||
buf bytes.Buffer
|
buf bytes.Buffer
|
||||||
|
|
||||||
@@ -94,8 +132,6 @@ type printer struct {
|
|||||||
indent string
|
indent string
|
||||||
depth int
|
depth int
|
||||||
|
|
||||||
visited map[edge]bool
|
|
||||||
|
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,38 +140,17 @@ type edge struct {
|
|||||||
from, to int64
|
from, to int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *printer) print(g graph.Graph, name string, needsIndent, isSubgraph bool) error {
|
|
||||||
nodes := graph.NodesOf(g.Nodes())
|
|
||||||
sort.Sort(ordered.ByID(nodes))
|
|
||||||
|
|
||||||
p.buf.WriteString(p.prefix)
|
|
||||||
if needsIndent {
|
|
||||||
for i := 0; i < p.depth; i++ {
|
|
||||||
p.buf.WriteString(p.indent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !isSubgraph {
|
|
||||||
p.buf.WriteString("strict ")
|
|
||||||
}
|
|
||||||
_, isDirected := g.(graph.Directed)
|
|
||||||
if isSubgraph {
|
|
||||||
p.buf.WriteString("sub")
|
|
||||||
} else if isDirected {
|
|
||||||
p.buf.WriteString("di")
|
|
||||||
}
|
|
||||||
p.buf.WriteString("graph")
|
|
||||||
|
|
||||||
|
func (p *simpleGraphPrinter) print(g graph.Graph, name string, needsIndent, isSubgraph bool) error {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
if g, ok := g.(Graph); ok {
|
if g, ok := g.(Graph); ok {
|
||||||
name = g.DOTID()
|
name = g.DOTID()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if name != "" {
|
|
||||||
p.buf.WriteByte(' ')
|
|
||||||
p.buf.WriteString(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.openBlock(" {")
|
_, isDirected := g.(graph.Directed)
|
||||||
|
p.printFrontMatter(name, needsIndent, isSubgraph, isDirected, true)
|
||||||
|
|
||||||
if a, ok := g.(Attributers); ok {
|
if a, ok := g.(Attributers); ok {
|
||||||
p.writeAttributeComplex(a)
|
p.writeAttributeComplex(a)
|
||||||
}
|
}
|
||||||
@@ -150,6 +165,9 @@ func (p *printer) print(g graph.Graph, name string, needsIndent, isSubgraph bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nodes := graph.NodesOf(g.Nodes())
|
||||||
|
sort.Sort(ordered.ByID(nodes))
|
||||||
|
|
||||||
havePrintedNodeHeader := false
|
havePrintedNodeHeader := false
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
if s, ok := n.(Subgrapher); ok {
|
if s, ok := n.(Subgrapher); ok {
|
||||||
@@ -264,11 +282,40 @@ func (p *printer) print(g graph.Graph, name string, needsIndent, isSubgraph bool
|
|||||||
p.buf.WriteByte(';')
|
p.buf.WriteByte(';')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p.closeBlock("}")
|
p.closeBlock("}")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *printer) printFrontMatter(name string, needsIndent, isSubgraph, isDirected, isStrict bool) error {
|
||||||
|
p.buf.WriteString(p.prefix)
|
||||||
|
if needsIndent {
|
||||||
|
for i := 0; i < p.depth; i++ {
|
||||||
|
p.buf.WriteString(p.indent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isSubgraph && isStrict {
|
||||||
|
p.buf.WriteString("strict ")
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSubgraph {
|
||||||
|
p.buf.WriteString("sub")
|
||||||
|
} else if isDirected {
|
||||||
|
p.buf.WriteString("di")
|
||||||
|
}
|
||||||
|
p.buf.WriteString("graph")
|
||||||
|
|
||||||
|
if name != "" {
|
||||||
|
p.buf.WriteByte(' ')
|
||||||
|
p.buf.WriteString(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.openBlock(" {")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *printer) writeNode(n graph.Node) {
|
func (p *printer) writeNode(n graph.Node) {
|
||||||
p.buf.WriteString(nodeID(n))
|
p.buf.WriteString(nodeID(n))
|
||||||
}
|
}
|
||||||
@@ -293,7 +340,7 @@ func nodeID(n graph.Node) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func graphID(g graph.Graph, n graph.Node) string {
|
func graphID(g interface{}, n graph.Node) string {
|
||||||
switch g := g.(type) {
|
switch g := g.(type) {
|
||||||
case Node:
|
case Node:
|
||||||
return g.DOTID()
|
return g.DOTID()
|
||||||
@@ -372,3 +419,165 @@ func (p *printer) closeBlock(b string) {
|
|||||||
p.newline()
|
p.newline()
|
||||||
p.buf.WriteString(b)
|
p.buf.WriteString(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type simpleGraphPrinter struct {
|
||||||
|
printer
|
||||||
|
visited map[edge]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type multiGraphPrinter struct {
|
||||||
|
printer
|
||||||
|
visited map[line]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type line struct {
|
||||||
|
inGraph string
|
||||||
|
id int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *multiGraphPrinter) print(g graph.Multigraph, name string, needsIndent, isSubgraph bool) error {
|
||||||
|
if name == "" {
|
||||||
|
if g, ok := g.(Multigraph); ok {
|
||||||
|
name = g.DOTID()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, isDirected := g.(graph.Directed)
|
||||||
|
p.printFrontMatter(name, needsIndent, isSubgraph, isDirected, false)
|
||||||
|
|
||||||
|
if a, ok := g.(Attributers); ok {
|
||||||
|
p.writeAttributeComplex(a)
|
||||||
|
}
|
||||||
|
if s, ok := g.(MultiStructurer); ok {
|
||||||
|
for _, g := range s.Structure() {
|
||||||
|
_, subIsDirected := g.(graph.Directed)
|
||||||
|
if subIsDirected != isDirected {
|
||||||
|
return errors.New("dot: mismatched graph type")
|
||||||
|
}
|
||||||
|
p.buf.WriteByte('\n')
|
||||||
|
p.print(g, g.DOTID(), true, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes := graph.NodesOf(g.Nodes())
|
||||||
|
sort.Sort(ordered.ByID(nodes))
|
||||||
|
|
||||||
|
havePrintedNodeHeader := false
|
||||||
|
for _, n := range nodes {
|
||||||
|
if s, ok := n.(MultiSubgrapher); ok {
|
||||||
|
// If the node is not linked to any other node
|
||||||
|
// the graph needs to be written now.
|
||||||
|
if g.From(n.ID()).Len() == 0 {
|
||||||
|
g := s.Subgraph()
|
||||||
|
_, subIsDirected := g.(graph.Directed)
|
||||||
|
if subIsDirected != isDirected {
|
||||||
|
return errors.New("dot: mismatched graph type")
|
||||||
|
}
|
||||||
|
if !havePrintedNodeHeader {
|
||||||
|
p.newline()
|
||||||
|
p.buf.WriteString("// Node definitions.")
|
||||||
|
havePrintedNodeHeader = true
|
||||||
|
}
|
||||||
|
p.newline()
|
||||||
|
p.print(g, graphID(g, n), false, true)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !havePrintedNodeHeader {
|
||||||
|
p.newline()
|
||||||
|
p.buf.WriteString("// Node definitions.")
|
||||||
|
havePrintedNodeHeader = true
|
||||||
|
}
|
||||||
|
p.newline()
|
||||||
|
p.writeNode(n)
|
||||||
|
if a, ok := n.(encoding.Attributer); ok {
|
||||||
|
p.writeAttributeList(a)
|
||||||
|
}
|
||||||
|
p.buf.WriteByte(';')
|
||||||
|
}
|
||||||
|
|
||||||
|
havePrintedEdgeHeader := false
|
||||||
|
for _, n := range nodes {
|
||||||
|
nid := n.ID()
|
||||||
|
to := graph.NodesOf(g.From(nid))
|
||||||
|
sort.Sort(ordered.ByID(to))
|
||||||
|
|
||||||
|
for _, t := range to {
|
||||||
|
tid := t.ID()
|
||||||
|
|
||||||
|
lines := graph.LinesOf(g.Lines(nid, tid))
|
||||||
|
sort.Sort(ordered.LinesByIDs(lines))
|
||||||
|
|
||||||
|
for _, l := range lines {
|
||||||
|
lid := l.ID()
|
||||||
|
if p.visited[line{inGraph: name, id: lid}] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.visited[line{inGraph: name, id: lid}] = true
|
||||||
|
|
||||||
|
if !havePrintedEdgeHeader {
|
||||||
|
p.buf.WriteByte('\n')
|
||||||
|
p.buf.WriteString(strings.TrimRight(p.prefix, " \t\n")) // Trim whitespace suffix.
|
||||||
|
p.newline()
|
||||||
|
p.buf.WriteString("// Edge definitions.")
|
||||||
|
havePrintedEdgeHeader = true
|
||||||
|
}
|
||||||
|
p.newline()
|
||||||
|
|
||||||
|
if s, ok := n.(MultiSubgrapher); ok {
|
||||||
|
g := s.Subgraph()
|
||||||
|
_, subIsDirected := g.(graph.Directed)
|
||||||
|
if subIsDirected != isDirected {
|
||||||
|
return errors.New("dot: mismatched graph type")
|
||||||
|
}
|
||||||
|
p.print(g, graphID(g, n), false, true)
|
||||||
|
} else {
|
||||||
|
p.writeNode(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
porter, edgeIsPorter := l.(Porter)
|
||||||
|
if edgeIsPorter {
|
||||||
|
if l.From().ID() == nid {
|
||||||
|
p.writePorts(porter.FromPort())
|
||||||
|
} else {
|
||||||
|
p.writePorts(porter.ToPort())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDirected {
|
||||||
|
p.buf.WriteString(" -> ")
|
||||||
|
} else {
|
||||||
|
p.buf.WriteString(" -- ")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s, ok := t.(MultiSubgrapher); ok {
|
||||||
|
g := s.Subgraph()
|
||||||
|
_, subIsDirected := g.(graph.Directed)
|
||||||
|
if subIsDirected != isDirected {
|
||||||
|
return errors.New("dot: mismatched graph type")
|
||||||
|
}
|
||||||
|
p.print(g, graphID(g, t), false, true)
|
||||||
|
} else {
|
||||||
|
p.writeNode(t)
|
||||||
|
}
|
||||||
|
if edgeIsPorter {
|
||||||
|
if l.From().ID() == nid {
|
||||||
|
p.writePorts(porter.ToPort())
|
||||||
|
} else {
|
||||||
|
p.writePorts(porter.FromPort())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a, ok := l.(encoding.Attributer); ok {
|
||||||
|
p.writeAttributeList(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.buf.WriteByte(';')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.closeBlock("}")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"gonum.org/v1/gonum/graph"
|
"gonum.org/v1/gonum/graph"
|
||||||
"gonum.org/v1/gonum/graph/encoding"
|
"gonum.org/v1/gonum/graph/encoding"
|
||||||
|
"gonum.org/v1/gonum/graph/multi"
|
||||||
"gonum.org/v1/gonum/graph/simple"
|
"gonum.org/v1/gonum/graph/simple"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1417,3 +1418,258 @@ func TestEncode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type intlist []int64
|
||||||
|
|
||||||
|
func createMultigraph(g []intlist) graph.Multigraph {
|
||||||
|
dg := multi.NewUndirectedGraph()
|
||||||
|
for u, e := range g {
|
||||||
|
u := int64(u)
|
||||||
|
nu := multi.Node(u)
|
||||||
|
for _, v := range e {
|
||||||
|
nv := multi.Node(v)
|
||||||
|
dg.SetLine(dg.NewLine(nu, nv))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dg
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNamedMultigraph(g []intlist) graph.Multigraph {
|
||||||
|
dg := multi.NewUndirectedGraph()
|
||||||
|
for u, e := range g {
|
||||||
|
u := int64(u)
|
||||||
|
nu := namedNode{id: u, name: alpha[u : u+1]}
|
||||||
|
for _, v := range e {
|
||||||
|
nv := namedNode{id: v, name: alpha[v : v+1]}
|
||||||
|
dg.SetLine(dg.NewLine(nu, nv))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dg
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDirectedMultigraph(g []intlist) graph.Multigraph {
|
||||||
|
dg := multi.NewDirectedGraph()
|
||||||
|
for u, e := range g {
|
||||||
|
u := int64(u)
|
||||||
|
nu := multi.Node(u)
|
||||||
|
for _, v := range e {
|
||||||
|
nv := multi.Node(v)
|
||||||
|
dg.SetLine(dg.NewLine(nu, nv))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dg
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNamedDirectedMultigraph(g []intlist) graph.Multigraph {
|
||||||
|
dg := multi.NewDirectedGraph()
|
||||||
|
for u, e := range g {
|
||||||
|
u := int64(u)
|
||||||
|
nu := namedNode{id: u, name: alpha[u : u+1]}
|
||||||
|
for _, v := range e {
|
||||||
|
nv := namedNode{id: v, name: alpha[v : v+1]}
|
||||||
|
dg.SetLine(dg.NewLine(nu, nv))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dg
|
||||||
|
}
|
||||||
|
|
||||||
|
var encodeMultiTests = []struct {
|
||||||
|
name string
|
||||||
|
g graph.Multigraph
|
||||||
|
|
||||||
|
prefix string
|
||||||
|
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
g: createMultigraph([]intlist{}),
|
||||||
|
want: `graph {
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: createMultigraph([]intlist{
|
||||||
|
0: {1},
|
||||||
|
1: {0, 2},
|
||||||
|
2: {},
|
||||||
|
}),
|
||||||
|
want: `graph {
|
||||||
|
// Node definitions.
|
||||||
|
0;
|
||||||
|
1;
|
||||||
|
2;
|
||||||
|
|
||||||
|
// Edge definitions.
|
||||||
|
0 -- 1;
|
||||||
|
0 -- 1;
|
||||||
|
1 -- 2;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: createMultigraph([]intlist{
|
||||||
|
0: {1},
|
||||||
|
1: {2, 2},
|
||||||
|
2: {0, 0, 0},
|
||||||
|
}),
|
||||||
|
want: `graph {
|
||||||
|
// Node definitions.
|
||||||
|
0;
|
||||||
|
1;
|
||||||
|
2;
|
||||||
|
|
||||||
|
// Edge definitions.
|
||||||
|
0 -- 1;
|
||||||
|
0 -- 2;
|
||||||
|
0 -- 2;
|
||||||
|
0 -- 2;
|
||||||
|
1 -- 2;
|
||||||
|
1 -- 2;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: createNamedMultigraph([]intlist{
|
||||||
|
0: {1},
|
||||||
|
1: {2, 2},
|
||||||
|
2: {0, 0, 0},
|
||||||
|
}),
|
||||||
|
want: `graph {
|
||||||
|
// Node definitions.
|
||||||
|
A;
|
||||||
|
B;
|
||||||
|
C;
|
||||||
|
|
||||||
|
// Edge definitions.
|
||||||
|
A -- B;
|
||||||
|
A -- C;
|
||||||
|
A -- C;
|
||||||
|
A -- C;
|
||||||
|
B -- C;
|
||||||
|
B -- C;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: createMultigraph([]intlist{
|
||||||
|
0: {2, 1, 0},
|
||||||
|
1: {2, 1, 0},
|
||||||
|
2: {2, 1, 0},
|
||||||
|
}),
|
||||||
|
want: `graph {
|
||||||
|
// Node definitions.
|
||||||
|
0;
|
||||||
|
1;
|
||||||
|
2;
|
||||||
|
|
||||||
|
// Edge definitions.
|
||||||
|
0 -- 0;
|
||||||
|
0 -- 1;
|
||||||
|
0 -- 1;
|
||||||
|
0 -- 2;
|
||||||
|
0 -- 2;
|
||||||
|
1 -- 1;
|
||||||
|
1 -- 2;
|
||||||
|
1 -- 2;
|
||||||
|
2 -- 2;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: createDirectedMultigraph([]intlist{}),
|
||||||
|
want: `digraph {
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: createDirectedMultigraph([]intlist{
|
||||||
|
0: {1},
|
||||||
|
1: {0, 2},
|
||||||
|
2: {},
|
||||||
|
}),
|
||||||
|
want: `digraph {
|
||||||
|
// Node definitions.
|
||||||
|
0;
|
||||||
|
1;
|
||||||
|
2;
|
||||||
|
|
||||||
|
// Edge definitions.
|
||||||
|
0 -> 1;
|
||||||
|
1 -> 0;
|
||||||
|
1 -> 2;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: createDirectedMultigraph([]intlist{
|
||||||
|
0: {1},
|
||||||
|
1: {2, 2},
|
||||||
|
2: {0, 0, 0},
|
||||||
|
}),
|
||||||
|
want: `digraph {
|
||||||
|
// Node definitions.
|
||||||
|
0;
|
||||||
|
1;
|
||||||
|
2;
|
||||||
|
|
||||||
|
// Edge definitions.
|
||||||
|
0 -> 1;
|
||||||
|
1 -> 2;
|
||||||
|
1 -> 2;
|
||||||
|
2 -> 0;
|
||||||
|
2 -> 0;
|
||||||
|
2 -> 0;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: createNamedDirectedMultigraph([]intlist{
|
||||||
|
0: {1},
|
||||||
|
1: {2, 2},
|
||||||
|
2: {0, 0, 0},
|
||||||
|
}),
|
||||||
|
want: `digraph {
|
||||||
|
// Node definitions.
|
||||||
|
A;
|
||||||
|
B;
|
||||||
|
C;
|
||||||
|
|
||||||
|
// Edge definitions.
|
||||||
|
A -> B;
|
||||||
|
B -> C;
|
||||||
|
B -> C;
|
||||||
|
C -> A;
|
||||||
|
C -> A;
|
||||||
|
C -> A;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
g: createDirectedMultigraph([]intlist{
|
||||||
|
0: {2, 1, 0},
|
||||||
|
1: {2, 1, 0},
|
||||||
|
2: {2, 1, 0},
|
||||||
|
}),
|
||||||
|
want: `digraph {
|
||||||
|
// Node definitions.
|
||||||
|
0;
|
||||||
|
1;
|
||||||
|
2;
|
||||||
|
|
||||||
|
// Edge definitions.
|
||||||
|
0 -> 0;
|
||||||
|
0 -> 1;
|
||||||
|
0 -> 2;
|
||||||
|
1 -> 0;
|
||||||
|
1 -> 1;
|
||||||
|
1 -> 2;
|
||||||
|
2 -> 0;
|
||||||
|
2 -> 1;
|
||||||
|
2 -> 2;
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeMulti(t *testing.T) {
|
||||||
|
for i, test := range encodeMultiTests {
|
||||||
|
got, err := MarshalMulti(test.g, test.name, test.prefix, "\t")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if string(got) != test.want {
|
||||||
|
t.Errorf("unexpected DOT result for test %d:\ngot: %s\nwant:%s", i, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -12,6 +12,12 @@ type Builder interface {
|
|||||||
graph.Builder
|
graph.Builder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MultiBuilder is a graph that can have user-defined nodes and edges added.
|
||||||
|
type MultiBuilder interface {
|
||||||
|
graph.Multigraph
|
||||||
|
graph.MultigraphBuilder
|
||||||
|
}
|
||||||
|
|
||||||
// AttributeSetter is implemented by types that can set an encoded graph
|
// AttributeSetter is implemented by types that can set an encoded graph
|
||||||
// attribute.
|
// attribute.
|
||||||
type AttributeSetter interface {
|
type AttributeSetter interface {
|
||||||
|
@@ -74,3 +74,20 @@ func Reverse(nodes []graph.Node) {
|
|||||||
nodes[i], nodes[j] = nodes[j], nodes[i]
|
nodes[i], nodes[j] = nodes[j], nodes[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LinesByIDs implements the sort.Interface sorting a slice of graph.LinesByIDs
|
||||||
|
// lexically by the From IDs, then by the To IDs, finally by the Line IDs.
|
||||||
|
type LinesByIDs []graph.Line
|
||||||
|
|
||||||
|
func (n LinesByIDs) Len() int { return len(n) }
|
||||||
|
func (n LinesByIDs) Less(i, j int) bool {
|
||||||
|
a, b := n[i], n[j]
|
||||||
|
if a.From().ID() != b.From().ID() {
|
||||||
|
return a.From().ID() < b.From().ID()
|
||||||
|
}
|
||||||
|
if a.To().ID() != b.To().ID() {
|
||||||
|
return a.To().ID() < b.To().ID()
|
||||||
|
}
|
||||||
|
return n[i].ID() < n[j].ID()
|
||||||
|
}
|
||||||
|
func (n LinesByIDs) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
||||||
|
Reference in New Issue
Block a user