2017-10-21 18:39:23 +00:00
|
|
|
// Package gg implements ginger graph creation, traversal, and (de)serialization
|
|
|
|
package gg
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/md5"
|
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
|
|
|
"hash"
|
|
|
|
)
|
|
|
|
|
2017-11-24 18:05:58 +00:00
|
|
|
// TODO instead of Identifier being public, make it encoding.TextMarshaler
|
|
|
|
|
2017-10-21 18:39:23 +00:00
|
|
|
// Identifier is implemented by any value which can return a unique string for
|
|
|
|
// itself via an Identify method
|
|
|
|
type Identifier interface {
|
|
|
|
Identify(hash.Hash)
|
|
|
|
}
|
|
|
|
|
|
|
|
func identify(i Identifier) string {
|
|
|
|
h := md5.New()
|
|
|
|
i.Identify(h)
|
|
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
|
|
}
|
|
|
|
|
2017-11-24 18:05:58 +00:00
|
|
|
// Str is an Identifier identified by its string value
|
|
|
|
type Str string
|
|
|
|
|
|
|
|
// Identify implements the Identifier interface
|
|
|
|
func (s Str) Identify(h hash.Hash) {
|
|
|
|
fmt.Fprintf(h, "%q", s)
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2017-10-21 18:39:23 +00:00
|
|
|
// VertexType enumerates the different possible vertex types
|
|
|
|
type VertexType string
|
|
|
|
|
|
|
|
const (
|
|
|
|
// Value is a Vertex which contains exactly one value and has at least one
|
|
|
|
// edge (either input or output)
|
|
|
|
Value VertexType = "value"
|
|
|
|
|
|
|
|
// Junction is a Vertex which contains two or more in edges and exactly one
|
|
|
|
// out edge
|
|
|
|
Junction VertexType = "junction"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Edge is a uni-directional connection between two vertices with an attribute
|
|
|
|
// value
|
|
|
|
type Edge struct {
|
|
|
|
From *Vertex
|
|
|
|
Value Identifier
|
|
|
|
To *Vertex
|
|
|
|
}
|
|
|
|
|
|
|
|
// Vertex is a vertex in a Graph. No fields should be modified directly, only
|
|
|
|
// through method calls
|
|
|
|
type Vertex struct {
|
|
|
|
VertexType
|
|
|
|
Value Identifier // Value is valid if-and-only-if VertexType is Value
|
|
|
|
In, Out []Edge
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2017-11-05 16:11:05 +00:00
|
|
|
// OpenEdge is an un-realized Edge which can't be used for anything except
|
2017-10-21 18:39:23 +00:00
|
|
|
// constructing graphs. It has no meaning on its own.
|
2017-11-05 16:11:05 +00:00
|
|
|
type OpenEdge struct {
|
2017-10-21 18:39:23 +00:00
|
|
|
// fromV will be the source vertex as-if the vertex (and any sub-vertices of
|
|
|
|
// it) doesn't already exist in the graph. If it or it's sub-vertices does
|
|
|
|
// already that will need to be taken into account when persisting into the
|
|
|
|
// graph
|
|
|
|
fromV vertex
|
|
|
|
val Identifier
|
|
|
|
}
|
|
|
|
|
|
|
|
// Identify implements the Identifier interface
|
2017-11-05 16:11:05 +00:00
|
|
|
func (oe OpenEdge) Identify(h hash.Hash) {
|
|
|
|
fmt.Fprintln(h, "openEdge")
|
|
|
|
oe.fromV.Identify(h)
|
|
|
|
oe.val.Identify(h)
|
2017-10-21 18:39:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// vertex is a representation of a vertex in the graph. Each Graph contains a
|
|
|
|
// set of all the Value vertex instances it knows about. Each of these contains
|
2017-11-05 16:11:05 +00:00
|
|
|
// all the input OpenEdges which are known for it. So you can think of these
|
|
|
|
// "top-level" Value vertex instances as root nodes in a tree, and each OpenEdge
|
2017-10-21 18:39:23 +00:00
|
|
|
// as a branch.
|
|
|
|
//
|
2017-11-05 16:11:05 +00:00
|
|
|
// If a OpenEdge contains a fromV which is a Value that vertex won't have its in
|
2017-10-21 18:39:23 +00:00
|
|
|
// slice populated no matter what. If fromV is a Junction it will be populated,
|
|
|
|
// with any sub-Value's not being populated and so-on recursively
|
|
|
|
//
|
|
|
|
// When a view is constructed in makeView these Value instances are deduplicated
|
|
|
|
// and the top-level one's in value is used to properly connect it.
|
|
|
|
type vertex struct {
|
|
|
|
VertexType
|
|
|
|
val Identifier
|
2017-11-05 16:11:05 +00:00
|
|
|
in []OpenEdge
|
2017-10-21 18:39:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// A Value vertex is unique by the value it contains
|
|
|
|
// A Junction vertex is unique by its input edges
|
|
|
|
func (v vertex) Identify(h hash.Hash) {
|
|
|
|
switch v.VertexType {
|
|
|
|
case Value:
|
|
|
|
fmt.Fprintln(h, "value")
|
|
|
|
v.val.Identify(h)
|
|
|
|
case Junction:
|
|
|
|
fmt.Fprintf(h, "junction:%d\n", len(v.in))
|
|
|
|
for _, in := range v.in {
|
|
|
|
in.Identify(h)
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
panic(fmt.Sprintf("invalid VertexType:%#v", v))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v vertex) cp() vertex {
|
|
|
|
cp := v
|
2017-11-05 16:11:05 +00:00
|
|
|
cp.in = make([]OpenEdge, len(v.in))
|
2017-10-21 18:39:23 +00:00
|
|
|
copy(cp.in, v.in)
|
|
|
|
return cp
|
|
|
|
}
|
|
|
|
|
2017-11-05 16:11:05 +00:00
|
|
|
func (v vertex) hasOpenEdge(oe OpenEdge) bool {
|
|
|
|
oeID := identify(oe)
|
2017-10-21 18:39:23 +00:00
|
|
|
for _, in := range v.in {
|
2017-11-05 16:11:05 +00:00
|
|
|
if identify(in) == oeID {
|
2017-10-21 18:39:23 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2017-11-05 16:11:05 +00:00
|
|
|
func (v vertex) cpAndDelOpenEdge(oe OpenEdge) (vertex, bool) {
|
|
|
|
oeID := identify(oe)
|
2017-10-22 17:39:32 +00:00
|
|
|
for i, in := range v.in {
|
2017-11-05 16:11:05 +00:00
|
|
|
if identify(in) == oeID {
|
2017-10-22 17:39:32 +00:00
|
|
|
v = v.cp()
|
|
|
|
v.in = append(v.in[:i], v.in[i+1:]...)
|
|
|
|
return v, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return v, false
|
|
|
|
}
|
|
|
|
|
2017-10-21 18:39:23 +00:00
|
|
|
// Graph is a wrapper around a set of connected Vertices
|
|
|
|
type Graph struct {
|
|
|
|
vM map[string]vertex // only contains value vertices
|
|
|
|
view map[string]*Vertex
|
|
|
|
}
|
|
|
|
|
|
|
|
// Null is the root empty graph, and is the base off which all graphs are built
|
|
|
|
var Null = &Graph{
|
|
|
|
vM: map[string]vertex{},
|
|
|
|
view: map[string]*Vertex{},
|
|
|
|
}
|
|
|
|
|
|
|
|
// this does _not_ copy the view, as it's assumed the only reason to copy a
|
|
|
|
// graph is to modify it anyway
|
|
|
|
func (g *Graph) cp() *Graph {
|
|
|
|
cp := &Graph{
|
|
|
|
vM: make(map[string]vertex, len(g.vM)),
|
|
|
|
}
|
|
|
|
for id, v := range g.vM {
|
|
|
|
cp.vM[id] = v
|
|
|
|
}
|
|
|
|
return cp
|
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Graph creation
|
|
|
|
|
2017-11-05 16:11:05 +00:00
|
|
|
// ValueOut creates a OpenEdge which, when used to construct a Graph, represents
|
2017-10-21 18:39:23 +00:00
|
|
|
// an edge (with edgeVal attached to it) leaving the Value Vertex containing
|
|
|
|
// val.
|
|
|
|
//
|
|
|
|
// When constructing Graphs Value vertices are de-duplicated on their value. So
|
2017-11-05 16:11:05 +00:00
|
|
|
// multiple ValueOut OpenEdges constructed with the same val will be leaving the
|
2017-10-21 18:39:23 +00:00
|
|
|
// same Vertex instance in the constructed Graph.
|
2017-11-05 16:11:05 +00:00
|
|
|
func ValueOut(val, edgeVal Identifier) OpenEdge {
|
|
|
|
return OpenEdge{
|
2017-10-21 18:39:23 +00:00
|
|
|
fromV: vertex{
|
|
|
|
VertexType: Value,
|
|
|
|
val: val,
|
|
|
|
},
|
|
|
|
val: edgeVal,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-05 16:11:05 +00:00
|
|
|
// JunctionOut creates a OpenEdge which, when used to construct a Graph,
|
2017-10-21 18:39:23 +00:00
|
|
|
// represents an edge (with edgeVal attached to it) leaving the Junction Vertex
|
|
|
|
// comprised of the given ordered-set of input edges.
|
|
|
|
//
|
|
|
|
// When constructing Graphs Junction vertices are de-duplicated on their input
|
2017-11-05 16:11:05 +00:00
|
|
|
// edges. So multiple Junction OpenEdges constructed with the same set of input
|
2017-10-21 18:39:23 +00:00
|
|
|
// edges will be leaving the same Junction instance in the constructed Graph.
|
2017-11-05 16:11:05 +00:00
|
|
|
func JunctionOut(in []OpenEdge, edgeVal Identifier) OpenEdge {
|
|
|
|
return OpenEdge{
|
2017-10-21 18:39:23 +00:00
|
|
|
fromV: vertex{
|
|
|
|
VertexType: Junction,
|
|
|
|
in: in,
|
|
|
|
},
|
|
|
|
val: edgeVal,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-05 16:11:05 +00:00
|
|
|
// AddValueIn takes a OpenEdge and connects it to the Value Vertex containing
|
2017-10-22 17:39:32 +00:00
|
|
|
// val, returning the new Graph which reflects that connection. Any Vertices
|
2017-11-05 16:11:05 +00:00
|
|
|
// referenced within toe OpenEdge which do not yet exist in the Graph will also
|
2017-10-21 18:39:23 +00:00
|
|
|
// be created in this step.
|
2017-11-05 16:11:05 +00:00
|
|
|
func (g *Graph) AddValueIn(oe OpenEdge, val Identifier) *Graph {
|
2017-10-21 18:39:23 +00:00
|
|
|
to := vertex{
|
|
|
|
VertexType: Value,
|
|
|
|
val: val,
|
|
|
|
}
|
|
|
|
toID := identify(to)
|
|
|
|
|
|
|
|
// if to is already in the graph, pull it out, as it might have existing in
|
|
|
|
// edges we want to keep
|
|
|
|
if exTo, ok := g.vM[toID]; ok {
|
|
|
|
to = exTo
|
|
|
|
}
|
|
|
|
|
|
|
|
// if the incoming edge already exists in to then there's nothing to do
|
2017-11-05 16:11:05 +00:00
|
|
|
if to.hasOpenEdge(oe) {
|
2017-10-21 18:39:23 +00:00
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
|
|
|
to = to.cp()
|
2017-11-05 16:11:05 +00:00
|
|
|
to.in = append(to.in, oe)
|
2017-10-21 18:39:23 +00:00
|
|
|
g = g.cp()
|
|
|
|
|
|
|
|
// starting with to (which we always overwrite) go through vM and
|
|
|
|
// recursively add in any vertices which aren't already there
|
|
|
|
var persist func(vertex)
|
|
|
|
persist = func(v vertex) {
|
|
|
|
vID := identify(v)
|
|
|
|
if v.VertexType == Value {
|
|
|
|
if _, ok := g.vM[vID]; !ok {
|
|
|
|
g.vM[vID] = v
|
|
|
|
}
|
2017-10-22 17:39:32 +00:00
|
|
|
} else {
|
|
|
|
for _, e := range v.in {
|
|
|
|
persist(e.fromV)
|
|
|
|
}
|
2017-10-21 18:39:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
delete(g.vM, toID)
|
|
|
|
persist(to)
|
2017-10-22 17:39:32 +00:00
|
|
|
for _, e := range to.in {
|
|
|
|
persist(e.fromV)
|
|
|
|
}
|
2017-10-21 18:39:23 +00:00
|
|
|
|
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
2017-11-05 16:11:05 +00:00
|
|
|
// DelValueIn takes a OpenEdge and disconnects it from the Value Vertex
|
2017-10-22 17:39:32 +00:00
|
|
|
// containing val, returning the new Graph which reflects the disconnection. If
|
|
|
|
// the Value Vertex doesn't exist within the graph, or it doesn't have the given
|
2017-11-05 16:11:05 +00:00
|
|
|
// OpenEdge, no changes are made. Any vertices referenced by toe OpenEdge for
|
2017-10-22 17:39:32 +00:00
|
|
|
// which that edge is their only outgoing edge will be removed from the Graph.
|
2017-11-05 16:11:05 +00:00
|
|
|
func (g *Graph) DelValueIn(oe OpenEdge, val Identifier) *Graph {
|
2017-10-22 17:39:32 +00:00
|
|
|
to := vertex{
|
|
|
|
VertexType: Value,
|
|
|
|
val: val,
|
|
|
|
}
|
|
|
|
toID := identify(to)
|
|
|
|
|
|
|
|
// pull to out of the graph. if it's not there then bail
|
|
|
|
var ok bool
|
|
|
|
if to, ok = g.vM[toID]; !ok {
|
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
|
|
|
// get new copy of to without the half-edge, or return if the half-edge
|
|
|
|
// wasn't even in to
|
2017-11-05 16:11:05 +00:00
|
|
|
to, ok = to.cpAndDelOpenEdge(oe)
|
2017-10-22 17:39:32 +00:00
|
|
|
if !ok {
|
|
|
|
return g
|
|
|
|
}
|
|
|
|
g = g.cp()
|
|
|
|
g.vM[toID] = to
|
|
|
|
|
|
|
|
// connectedTo returns whether the vertex has any connections with the
|
|
|
|
// vertex of the given id, descending recursively
|
|
|
|
var connectedTo func(string, vertex) bool
|
|
|
|
connectedTo = func(vID string, curr vertex) bool {
|
|
|
|
for _, in := range curr.in {
|
|
|
|
if in.fromV.VertexType == Value && identify(in.fromV) == vID {
|
|
|
|
return true
|
|
|
|
} else if in.fromV.VertexType == Junction && connectedTo(vID, in.fromV) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// isOrphaned returns whether the given vertex has any connections to other
|
|
|
|
// nodes in the graph
|
|
|
|
isOrphaned := func(v vertex) bool {
|
|
|
|
vID := identify(v)
|
|
|
|
if v, ok := g.vM[vID]; ok && len(v.in) > 0 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for vID2, v2 := range g.vM {
|
|
|
|
if vID2 == vID {
|
|
|
|
continue
|
|
|
|
} else if connectedTo(vID, v2) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// if to is orphaned get rid of it
|
|
|
|
if isOrphaned(to) {
|
|
|
|
delete(g.vM, toID)
|
|
|
|
}
|
|
|
|
|
2017-11-05 16:11:05 +00:00
|
|
|
// rmOrphaned descends down the given OpenEdge and removes any Value
|
2017-10-22 17:39:32 +00:00
|
|
|
// Vertices referenced in it which are now orphaned
|
2017-11-05 16:11:05 +00:00
|
|
|
var rmOrphaned func(OpenEdge)
|
|
|
|
rmOrphaned = func(oe OpenEdge) {
|
|
|
|
if oe.fromV.VertexType == Value && isOrphaned(oe.fromV) {
|
|
|
|
delete(g.vM, identify(oe.fromV))
|
|
|
|
} else if oe.fromV.VertexType == Junction {
|
|
|
|
for _, juncHe := range oe.fromV.in {
|
2017-10-22 17:39:32 +00:00
|
|
|
rmOrphaned(juncHe)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-11-05 16:11:05 +00:00
|
|
|
rmOrphaned(oe)
|
2017-10-22 17:39:32 +00:00
|
|
|
|
|
|
|
return g
|
|
|
|
}
|
|
|
|
|
|
|
|
// Union takes in another Graph and returns a new one which is the union of the
|
|
|
|
// two. Value vertices which are shared between the two will be merged so that
|
|
|
|
// the new vertex has the input edges of both.
|
|
|
|
func (g *Graph) Union(g2 *Graph) *Graph {
|
|
|
|
g = g.cp()
|
|
|
|
for vID, v2 := range g2.vM {
|
|
|
|
v, ok := g.vM[vID]
|
|
|
|
if !ok {
|
|
|
|
v = v2
|
|
|
|
} else {
|
|
|
|
for _, v2e := range v2.in {
|
2017-11-05 16:11:05 +00:00
|
|
|
if !v.hasOpenEdge(v2e) {
|
2017-10-22 17:39:32 +00:00
|
|
|
v.in = append(v.in, v2e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
g.vM[vID] = v
|
|
|
|
}
|
|
|
|
return g
|
|
|
|
}
|
2017-10-21 18:39:23 +00:00
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Graph traversal
|
|
|
|
|
|
|
|
func (g *Graph) makeView() {
|
|
|
|
if g.view != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// view only contains value vertices, but we need to keep track of all
|
|
|
|
// vertices while constructing the view
|
|
|
|
g.view = make(map[string]*Vertex, len(g.vM))
|
|
|
|
all := map[string]*Vertex{}
|
|
|
|
|
|
|
|
var getV func(vertex, bool) *Vertex
|
|
|
|
getV = func(v vertex, top bool) *Vertex {
|
|
|
|
vID := identify(v)
|
|
|
|
V, ok := all[vID]
|
|
|
|
if !ok {
|
|
|
|
V = &Vertex{VertexType: v.VertexType, Value: v.val}
|
|
|
|
all[vID] = V
|
|
|
|
}
|
|
|
|
|
|
|
|
// we can be sure all Value vertices will be called with top==true at
|
|
|
|
// some point, so we only need to descend into the input edges if:
|
|
|
|
// * top is true
|
|
|
|
// * this is a junction's first time being gotten
|
|
|
|
if !top && (ok || v.VertexType != Junction) {
|
|
|
|
return V
|
|
|
|
}
|
|
|
|
|
|
|
|
V.In = make([]Edge, 0, len(v.in))
|
|
|
|
for i := range v.in {
|
|
|
|
fromV := getV(v.in[i].fromV, false)
|
|
|
|
e := Edge{From: fromV, Value: v.in[i].val, To: V}
|
|
|
|
fromV.Out = append(fromV.Out, e)
|
|
|
|
V.In = append(V.In, e)
|
|
|
|
}
|
|
|
|
|
|
|
|
if v.VertexType == Value {
|
|
|
|
g.view[identify(v.val)] = V
|
|
|
|
}
|
|
|
|
|
|
|
|
return V
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, v := range g.vM {
|
|
|
|
getV(v, true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Value returns the Value Vertex for the given value. If the Graph doesn't
|
|
|
|
// contain a vertex for the value then nil is returned
|
|
|
|
func (g *Graph) Value(val Identifier) *Vertex {
|
|
|
|
g.makeView()
|
|
|
|
return g.view[identify(val)]
|
|
|
|
}
|
|
|
|
|
2017-10-22 17:39:32 +00:00
|
|
|
// Values returns all Value Vertices in the Graph
|
|
|
|
func (g *Graph) Values() []*Vertex {
|
|
|
|
g.makeView()
|
|
|
|
vv := make([]*Vertex, 0, len(g.view))
|
|
|
|
for _, v := range g.view {
|
|
|
|
vv = append(vv, v)
|
|
|
|
}
|
|
|
|
return vv
|
|
|
|
}
|
|
|
|
|
2017-10-21 18:39:23 +00:00
|
|
|
// Equal returns whether or not the two Graphs are equivalent in value
|
|
|
|
func Equal(g1, g2 *Graph) bool {
|
|
|
|
if len(g1.vM) != len(g2.vM) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for v1ID, v1 := range g1.vM {
|
|
|
|
v2, ok := g2.vM[v1ID]
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// since the vertices are values we must make sure their input sets are
|
|
|
|
// the same (which is tricky since they're unordered, unlike a
|
|
|
|
// junction's)
|
|
|
|
if len(v1.in) != len(v2.in) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, in := range v1.in {
|
2017-11-05 16:11:05 +00:00
|
|
|
if !v2.hasOpenEdge(in) {
|
2017-10-21 18:39:23 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
2017-11-05 16:57:57 +00:00
|
|
|
|
|
|
|
// Walk will traverse the Graph, calling the callback on every Vertex in the
|
|
|
|
// Graph once. If startWith is non-nil then that Vertex will be the first one
|
|
|
|
// passed to the callback and used as the starting point of the traversal. If
|
|
|
|
// the callback returns false the traversal is stopped.
|
|
|
|
func (g *Graph) Walk(startWith *Vertex, callback func(*Vertex) bool) {
|
|
|
|
g.makeView()
|
|
|
|
if len(g.view) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
seen := make(map[*Vertex]bool, len(g.view))
|
|
|
|
var innerWalk func(*Vertex) bool
|
|
|
|
innerWalk = func(v *Vertex) bool {
|
|
|
|
if seen[v] {
|
|
|
|
return true
|
|
|
|
} else if !callback(v) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
seen[v] = true
|
|
|
|
for _, e := range v.In {
|
|
|
|
if !innerWalk(e.From) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if startWith != nil {
|
|
|
|
if !innerWalk(startWith) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, v := range g.view {
|
|
|
|
if !innerWalk(v) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|