mirror of
https://github.com/gonum/gonum.git
synced 2025-10-05 15:16:59 +08:00
238 lines
6.6 KiB
Go
238 lines
6.6 KiB
Go
// Copyright ©2017 The gonum Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package dot
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"gonum.org/v1/gonum/graph"
|
|
"gonum.org/v1/gonum/graph/formats/dot"
|
|
"gonum.org/v1/gonum/graph/formats/dot/ast"
|
|
"golang.org/x/tools/container/intsets"
|
|
)
|
|
|
|
// Builder is a graph that can have user-defined nodes and edges added.
|
|
type Builder interface {
|
|
graph.Graph
|
|
graph.Builder
|
|
// NewNode adds a new node with a unique node ID to the graph.
|
|
NewNode() graph.Node
|
|
// NewEdge adds a new edge from the source to the destination node to the
|
|
// graph, or returns the existing edge if already present.
|
|
NewEdge(from, to graph.Node) graph.Edge
|
|
}
|
|
|
|
// UnmarshalerAttr is the interface implemented by objects that can unmarshal a
|
|
// DOT attribute description of themselves.
|
|
type UnmarshalerAttr interface {
|
|
// UnmarshalDOTAttr decodes a single DOT attribute.
|
|
UnmarshalDOTAttr(attr Attribute) error
|
|
}
|
|
|
|
// Unmarshal parses the Graphviz DOT-encoded data and stores the result in dst.
|
|
func Unmarshal(data []byte, dst Builder) 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 copyGraph(dst, file.Graphs[0])
|
|
}
|
|
|
|
// copyGraph copies the nodes and edges from the Graphviz AST source graph to
|
|
// the destination graph. Edge direction is maintained if present.
|
|
func copyGraph(dst Builder, src *ast.Graph) (err error) {
|
|
defer func() {
|
|
switch e := recover().(type) {
|
|
case nil:
|
|
case error:
|
|
err = e
|
|
default:
|
|
panic(e)
|
|
}
|
|
}()
|
|
gen := &generator{
|
|
directed: src.Directed,
|
|
ids: make(map[string]graph.Node),
|
|
}
|
|
for _, stmt := range src.Stmts {
|
|
gen.addStmt(dst, stmt)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// A generator keeps track of the information required for generating a gonum
|
|
// graph from a dot AST graph.
|
|
type generator struct {
|
|
// Directed graph.
|
|
directed bool
|
|
// Map from dot AST node ID to gonum node.
|
|
ids map[string]graph.Node
|
|
// Nodes processed within the context of a subgraph, that is to be used as a
|
|
// vertex of an edge.
|
|
subNodes []graph.Node
|
|
// Stack of start indices into the subgraph node slice. The top element
|
|
// corresponds to the start index of the active (or inner-most) subgraph.
|
|
subStart []int
|
|
}
|
|
|
|
// node returns the gonum node corresponding to the given dot AST node ID,
|
|
// generating a new such node if none exist.
|
|
func (gen *generator) node(dst Builder, id string) graph.Node {
|
|
if n, ok := gen.ids[id]; ok {
|
|
return n
|
|
}
|
|
n := dst.NewNode()
|
|
gen.ids[id] = n
|
|
// Check if within the context of a subgraph, that is to be used as a vertex
|
|
// of an edge.
|
|
if gen.isInSubgraph() {
|
|
// Append node processed within the context of a subgraph, that is to be
|
|
// used as a vertex of an edge
|
|
gen.appendSubgraphNode(n)
|
|
}
|
|
return n
|
|
}
|
|
|
|
// addStmt adds the given statement to the graph.
|
|
func (gen *generator) addStmt(dst Builder, stmt ast.Stmt) {
|
|
switch stmt := stmt.(type) {
|
|
case *ast.NodeStmt:
|
|
n := gen.node(dst, stmt.Node.ID)
|
|
if n, ok := n.(UnmarshalerAttr); ok {
|
|
for _, attr := range stmt.Attrs {
|
|
a := Attribute{
|
|
Key: attr.Key,
|
|
Value: attr.Val,
|
|
}
|
|
if err := n.UnmarshalDOTAttr(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:
|
|
// ignore.
|
|
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 graph.
|
|
func (gen *generator) addEdgeStmt(dst Builder, e *ast.EdgeStmt) {
|
|
fs := gen.addVertex(dst, e.From)
|
|
ts := gen.addEdge(dst, e.To)
|
|
for _, f := range fs {
|
|
for _, t := range ts {
|
|
edge := dst.NewEdge(f, t)
|
|
if edge, ok := edge.(UnmarshalerAttr); ok {
|
|
for _, attr := range e.Attrs {
|
|
a := Attribute{
|
|
Key: attr.Key,
|
|
Value: attr.Val,
|
|
}
|
|
if err := edge.UnmarshalDOTAttr(a); err != nil {
|
|
panic(fmt.Errorf("unable to unmarshal edge DOT attribute (%s=%s)", a.Key, a.Value))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// addVertex adds the given vertex to the graph, and returns its set of nodes.
|
|
func (gen *generator) addVertex(dst Builder, 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))
|
|
}
|
|
}
|
|
|
|
// addEdge adds the given edge to the graph, and returns its set of nodes.
|
|
func (gen *generator) addEdge(dst Builder, to *ast.Edge) []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.addEdge(dst, to.To)
|
|
for _, f := range fs {
|
|
for _, t := range ts {
|
|
dst.NewEdge(f, t)
|
|
}
|
|
}
|
|
}
|
|
return fs
|
|
}
|
|
|
|
// pushSubgraph pushes the node start index of the active subgraph onto the
|
|
// stack.
|
|
func (gen *generator) pushSubgraph() {
|
|
gen.subStart = append(gen.subStart, len(gen.subNodes))
|
|
}
|
|
|
|
// popSubgraph pops the node start index of the active subgraph from the stack,
|
|
// and returns the nodes processed since.
|
|
func (gen *generator) popSubgraph() []graph.Node {
|
|
// Get nodes processed since the subgraph became active.
|
|
start := gen.subStart[len(gen.subStart)-1]
|
|
// TODO: Figure out a better way to store subgraph nodes, so that duplicates
|
|
// may not occur.
|
|
nodes := unique(gen.subNodes[start:])
|
|
// Remove subgraph from stack.
|
|
gen.subStart = gen.subStart[:len(gen.subStart)-1]
|
|
if len(gen.subStart) == 0 {
|
|
// Remove subgraph nodes when the bottom-most subgraph has been processed.
|
|
gen.subNodes = gen.subNodes[:0]
|
|
}
|
|
return nodes
|
|
}
|
|
|
|
// unique returns the set of unique nodes contained within ns.
|
|
func unique(ns []graph.Node) []graph.Node {
|
|
var nodes []graph.Node
|
|
var set intsets.Sparse
|
|
for _, n := range ns {
|
|
id := n.ID()
|
|
if set.Has(id) {
|
|
// skip duplicate node
|
|
continue
|
|
}
|
|
set.Insert(id)
|
|
nodes = append(nodes, n)
|
|
}
|
|
return nodes
|
|
}
|
|
|
|
// isInSubgraph reports whether the active context is within a subgraph, that is
|
|
// to be used as a vertex of an edge.
|
|
func (gen *generator) isInSubgraph() bool {
|
|
return len(gen.subStart) > 0
|
|
}
|
|
|
|
// appendSubgraphNode appends the given node to the slice of nodes processed
|
|
// within the context of a subgraph.
|
|
func (gen *generator) appendSubgraphNode(n graph.Node) {
|
|
gen.subNodes = append(gen.subNodes, n)
|
|
}
|