rgl 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/ChangeLog +19 -10
- data/Gemfile +3 -0
- data/{README → README.rdoc} +70 -98
- data/Rakefile +44 -150
- data/examples/canvas.rb +63 -64
- data/examples/examples.rb +42 -42
- data/examples/graph.dot +46 -0
- data/examples/images/example.jpg +0 -0
- data/examples/images/module_graph.jpg +0 -0
- data/examples/images/rgl_modules.png +0 -0
- data/examples/{insel-der-tausend-gefahren.rb → insel_der_tausend_gefahren.rb} +18 -19
- data/examples/north.rb +2 -2
- data/examples/north2.rb +11 -11
- data/examples/rdep-rgl.rb +218 -222
- data/lib/rgl/adjacency.rb +78 -74
- data/lib/rgl/base.rb +160 -78
- data/lib/rgl/bellman_ford.rb +115 -0
- data/lib/rgl/bidirectional.rb +17 -10
- data/lib/rgl/bipartite.rb +87 -0
- data/lib/rgl/condensation.rb +13 -4
- data/lib/rgl/connected_components.rb +38 -30
- data/lib/rgl/dijkstra.rb +158 -0
- data/lib/rgl/dijkstra_visitor.rb +42 -0
- data/lib/rgl/dot.rb +40 -32
- data/lib/rgl/edge_properties_map.rb +55 -0
- data/lib/rgl/edmonds_karp.rb +136 -0
- data/lib/rgl/enumerable_ext.rb +4 -1
- data/lib/rgl/graph_iterator.rb +15 -0
- data/lib/rgl/graph_visitor.rb +138 -0
- data/lib/rgl/graph_wrapper.rb +15 -0
- data/lib/rgl/graphxml.rb +20 -10
- data/lib/rgl/implicit.rb +68 -66
- data/lib/rgl/mutable.rb +37 -31
- data/lib/rgl/path_builder.rb +40 -0
- data/lib/rgl/prim.rb +52 -0
- data/lib/rgl/rdot.rb +411 -374
- data/lib/rgl/topsort.rb +23 -16
- data/lib/rgl/transitivity.rb +29 -27
- data/lib/rgl/traversal.rb +67 -205
- data/rakelib/dep_graph.rake +4 -3
- data/test/bellman_ford_test.rb +187 -0
- data/test/bipartite_test.rb +47 -0
- data/test/components_test.rb +80 -0
- data/test/cycles_test.rb +60 -0
- data/test/dijkstra_test.rb +148 -0
- data/test/directed_graph_test.rb +118 -0
- data/test/dot_test.rb +26 -0
- data/test/edge_properties_map_test.rb +63 -0
- data/test/edge_test.rb +35 -0
- data/test/edmonds_karp_test.rb +105 -0
- data/{tests/TestGraph.rb → test/graph_test.rb} +6 -6
- data/test/graph_xml_test.rb +57 -0
- data/test/implicit_test.rb +53 -0
- data/test/prim_test.rb +98 -0
- data/{tests/TestRdot.rb → test/rdot_test.rb} +309 -308
- data/{tests → test}/test_helper.rb +4 -1
- data/{tests/TestTransitivity.rb → test/transitivity_test.rb} +43 -43
- data/test/traversal_test.rb +221 -0
- data/test/undirected_graph_test.rb +103 -0
- metadata +226 -145
- data/examples/example.jpg +0 -0
- data/examples/module_graph.jpg +0 -0
- data/install.rb +0 -49
- data/tests/TestComponents.rb +0 -65
- data/tests/TestCycles.rb +0 -61
- data/tests/TestDirectedGraph.rb +0 -125
- data/tests/TestDot.rb +0 -18
- data/tests/TestEdge.rb +0 -34
- data/tests/TestGraphXML.rb +0 -57
- data/tests/TestImplicit.rb +0 -52
- data/tests/TestTraversal.rb +0 -220
- data/tests/TestUnDirectedGraph.rb +0 -102
data/lib/rgl/mutable.rb
CHANGED
@@ -5,15 +5,16 @@ require 'rgl/base'
|
|
5
5
|
module RGL
|
6
6
|
|
7
7
|
# A MutableGraph can be changed via the addition or removal of edges and
|
8
|
-
# vertices.
|
8
|
+
# vertices.
|
9
|
+
#
|
9
10
|
module MutableGraph
|
10
11
|
|
11
12
|
include Graph
|
12
13
|
|
13
|
-
# Add a new vertex _v_ to the graph.
|
14
|
+
# Add a new vertex _v_ to the graph. If the vertex is already in the
|
14
15
|
# graph (tested via eql?), the method does nothing.
|
15
|
-
|
16
|
-
def add_vertex
|
16
|
+
#
|
17
|
+
def add_vertex(v)
|
17
18
|
raise NotImplementedError
|
18
19
|
end
|
19
20
|
|
@@ -22,82 +23,87 @@ module RGL
|
|
22
23
|
# Note that for undirected graphs, (u,v) is the same edge as (v,u), so
|
23
24
|
# after a call to the function add_edge(), this implies that edge (u,v)
|
24
25
|
# will appear in the out-edges of u and (u,v) (or equivalently (v,u))
|
25
|
-
# will appear in the out-edges of v.
|
26
|
-
# to u and u will be adjacent to v.
|
27
|
-
|
28
|
-
def add_edge
|
26
|
+
# will appear in the out-edges of v. Put another way, v will be adjacent
|
27
|
+
# to u and u will be adjacent to v.
|
28
|
+
#
|
29
|
+
def add_edge(u, v)
|
29
30
|
raise NotImplementedError
|
30
31
|
end
|
31
32
|
|
32
33
|
# Add all objects in _a_ to the vertex set.
|
33
|
-
|
34
|
-
def add_vertices
|
34
|
+
#
|
35
|
+
def add_vertices(*a)
|
35
36
|
a.each { |v| add_vertex v }
|
36
37
|
end
|
37
38
|
|
38
|
-
# Add all edges in the _edges_ array to the edge set.
|
39
|
+
# Add all edges in the _edges_ array to the edge set. Elements of the
|
39
40
|
# array can be both two-element arrays or instances of DirectedEdge or
|
40
|
-
# UnDirectedEdge.
|
41
|
-
|
42
|
-
def add_edges
|
41
|
+
# UnDirectedEdge.
|
42
|
+
#
|
43
|
+
def add_edges(*edges)
|
43
44
|
edges.each { |edge| add_edge(edge[0], edge[1]) }
|
44
45
|
end
|
45
46
|
|
46
|
-
# Remove u from the vertex set of the graph.
|
47
|
+
# Remove u from the vertex set of the graph. All edges whose target is
|
47
48
|
# _v_ are also removed from the edge set of the graph.
|
48
49
|
#
|
49
50
|
# Postcondition: num_vertices is one less, _v_ no longer appears in the
|
50
51
|
# vertex set of the graph, and there no edge with source or target _v_.
|
51
|
-
|
52
|
-
def remove_vertex
|
52
|
+
#
|
53
|
+
def remove_vertex(v)
|
53
54
|
raise NotImplementedError
|
54
55
|
end
|
55
56
|
|
56
|
-
# Remove the edge (u,v) from the graph.
|
57
|
+
# Remove the edge (u,v) from the graph. If the graph allows parallel
|
57
58
|
# edges, this removes all occurrences of (u,v).
|
58
|
-
#
|
59
|
+
#
|
59
60
|
# Precondition: u and v are vertices in the graph.
|
60
61
|
# Postcondition: (u,v) is no longer in the edge set for g.
|
61
|
-
|
62
|
-
def remove_edge
|
62
|
+
#
|
63
|
+
def remove_edge(u, v)
|
63
64
|
raise NotImplementedError
|
64
65
|
end
|
65
66
|
|
66
67
|
# Remove all vertices specified by the array a from the graph by calling
|
67
68
|
# remove_vertex.
|
68
|
-
|
69
|
-
def remove_vertices
|
69
|
+
#
|
70
|
+
def remove_vertices(*a)
|
70
71
|
a.each { |v| remove_vertex v }
|
71
72
|
end
|
72
73
|
|
73
|
-
# Returns all minimum cycles that pass through a give vertex.
|
74
|
+
# Returns all minimum cycles that pass through a give vertex.
|
74
75
|
# The format is an Array of cycles, with each cycle being an Array
|
75
76
|
# of vertices in the cycle.
|
77
|
+
#
|
76
78
|
def cycles_with_vertex(vertex)
|
77
79
|
cycles_with_vertex_helper(vertex, vertex, [])
|
78
80
|
end
|
79
81
|
|
80
|
-
|
82
|
+
protected
|
83
|
+
|
81
84
|
def cycles_with_vertex_helper(vertex, start, visited) #:nodoc:
|
82
|
-
adjacent_vertices(start).reject {|x| visited.include?(x)}.inject([]) do |acc, adj|
|
85
|
+
adjacent_vertices(start).reject { |x| visited.include?(x) }.inject([]) do |acc, adj|
|
83
86
|
local_visited = Array.new(visited) << adj
|
84
87
|
acc << local_visited if (adj==vertex)
|
85
|
-
acc = acc + cycles_with_vertex_helper(vertex,adj,local_visited)
|
88
|
+
acc = acc + cycles_with_vertex_helper(vertex, adj, local_visited)
|
86
89
|
end
|
87
90
|
end
|
88
91
|
|
89
|
-
|
92
|
+
public
|
93
|
+
|
90
94
|
# Returns an array of all minimum cycles in a graph
|
91
95
|
#
|
92
96
|
# This is not an efficient implementation O(n^4) and could
|
93
97
|
# be done using Minimum Spanning Trees. Hint. Hint.
|
98
|
+
#
|
94
99
|
def cycles
|
95
100
|
g = self.clone
|
96
|
-
self.inject([]) do |acc, v|
|
101
|
+
self.inject([]) do |acc, v|
|
97
102
|
acc = acc.concat(g.cycles_with_vertex(v))
|
98
103
|
g.remove_vertex(v); acc
|
99
104
|
end
|
100
105
|
end
|
101
106
|
|
102
|
-
end
|
103
|
-
|
107
|
+
end # module MutableGraph
|
108
|
+
|
109
|
+
end # module RGL
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module RGL
|
2
|
+
|
3
|
+
class PathBuilder # :nodoc:
|
4
|
+
|
5
|
+
def initialize(source, parents_map)
|
6
|
+
@source = source
|
7
|
+
@parents_map = parents_map
|
8
|
+
@paths = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def path(target)
|
12
|
+
if @paths.has_key?(target)
|
13
|
+
@paths[target]
|
14
|
+
else
|
15
|
+
@paths[target] = restore_path(target)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def paths(targets)
|
20
|
+
paths_map = {}
|
21
|
+
|
22
|
+
targets.each do |target|
|
23
|
+
paths_map[target] = path(target)
|
24
|
+
end
|
25
|
+
|
26
|
+
paths_map
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def restore_path(target)
|
32
|
+
return [@source] if target == @source
|
33
|
+
|
34
|
+
parent = @parents_map[target]
|
35
|
+
path(parent) + [target] if parent
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end # RGL
|
data/lib/rgl/prim.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rgl/dijkstra'
|
2
|
+
require 'rgl/adjacency'
|
3
|
+
|
4
|
+
module RGL
|
5
|
+
|
6
|
+
class PrimAlgorithm
|
7
|
+
|
8
|
+
# Replacement for default distance combinator that is used in Dijkstra's algorithm. While building a minimum
|
9
|
+
# spanning tree (MST) we're interested not in the distance from the source (the vertex that is added first to the
|
10
|
+
# MST) to a vertex, but rather in the distance between already completed part of the MST (that includes all examined
|
11
|
+
# vertices) and the vertex. Therefore, when we examine an edge (u, v), where _u_ is already in the MST and _v_ is
|
12
|
+
# not, the distance from the MST to the vertex _v_ is the weight of the edge (u, v).
|
13
|
+
DISTANCE_COMBINATOR = lambda { |_, edge_weight| edge_weight }
|
14
|
+
|
15
|
+
# Initializes Prim's algorithm for a _graph_ with provided edges weights map.
|
16
|
+
#
|
17
|
+
def initialize(graph, edge_weights_map, visitor)
|
18
|
+
@graph = graph
|
19
|
+
@edge_weights_map = EdgePropertiesMap.new(edge_weights_map, @graph.directed?)
|
20
|
+
@visitor = visitor
|
21
|
+
@dijkstra = DijkstraAlgorithm.new(@graph, @edge_weights_map, @visitor, DISTANCE_COMBINATOR)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns minimum spanning tree for the _graph_. If the graph is disconnected, Prim's algorithm will find the minimum
|
25
|
+
# spanning tree only for one of the connectivity components. If _start_vertex_ is given, Dijkstra's search will be
|
26
|
+
# started in this vertex and the algorithm will return the minimum spanning tree for the component it belongs to.
|
27
|
+
#
|
28
|
+
def minimum_spanning_tree(start_vertex = nil)
|
29
|
+
@dijkstra.find_shortest_paths(start_vertex || @graph.vertices.first)
|
30
|
+
AdjacencyGraph[*@visitor.parents_map.to_a.flatten]
|
31
|
+
end
|
32
|
+
|
33
|
+
end # class PrimAlgorithm
|
34
|
+
|
35
|
+
module Graph
|
36
|
+
|
37
|
+
# Finds the minimum spanning tree of the graph.
|
38
|
+
#
|
39
|
+
# Returns an AdjacencyGraph that represents the minimum spanning tree of the graph's connectivity component that
|
40
|
+
# contains the starting vertex. The algorithm starts from an arbitrary vertex if the _start_vertex_ is not given.
|
41
|
+
# Since the implementation relies on the Dijkstra's algorithm, Prim's algorithm uses the same visitor class and emits
|
42
|
+
# the same events.
|
43
|
+
#
|
44
|
+
# Raises ArgumentError if edge weight is undefined.
|
45
|
+
#
|
46
|
+
def prim_minimum_spanning_tree(edge_weights_map, start_vertex = nil, visitor = DijkstraVisitor.new(self))
|
47
|
+
PrimAlgorithm.new(self, edge_weights_map, visitor).minimum_spanning_tree(start_vertex)
|
48
|
+
end
|
49
|
+
|
50
|
+
end # module Graph
|
51
|
+
|
52
|
+
end # module RGL
|
data/lib/rgl/rdot.rb
CHANGED
@@ -1,157 +1,164 @@
|
|
1
|
-
# This is a modified version of dot.rb from Dave Thomas's rdoc project.
|
1
|
+
# This is a modified version of dot.rb from Dave Thomas's rdoc project. I
|
2
2
|
# renamed it to rdot.rb to avoid collision with an installed rdoc/dot.
|
3
3
|
#
|
4
4
|
# It also supports undirected edges.
|
5
5
|
|
6
|
-
module RGL
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
6
|
+
module RGL
|
7
|
+
|
8
|
+
module DOT
|
9
|
+
|
10
|
+
# options for node declaration
|
11
|
+
|
12
|
+
NODE_OPTS = [
|
13
|
+
# attributes due to
|
14
|
+
# http://www.graphviz.org/Documentation/dotguide.pdf
|
15
|
+
# February 23, 2008
|
16
|
+
'color', # default: black; node shape color
|
17
|
+
'comment', # any string (format-dependent)
|
18
|
+
'distortion', # default: 0.0; node distortion for shape=polygon
|
19
|
+
'fillcolor', # default: lightgrey/black; node fill color
|
20
|
+
'fixedsize', # default: false; label text has no affect on node size
|
21
|
+
'fontcolor', # default: black; type face color
|
22
|
+
'fontname', # default: Times-Roman; font family
|
23
|
+
'fontsize', #default: 14; point size of label
|
24
|
+
'group', # name of node's group
|
25
|
+
'height', # default: .5; height in inches
|
26
|
+
'label', # default: node name; any string
|
27
|
+
'layer', # default: overlay range; all, id or id:id
|
28
|
+
'orientation', # dafault: 0.0; node rotation angle
|
29
|
+
'peripheries', # shape-dependent number of node boundaries
|
30
|
+
'regular', # default: false; force polygon to be regular
|
31
|
+
'shape', # default: ellipse; node shape; see Section 2.1 and Appendix E
|
32
|
+
'shapefile', # external EPSF or SVG custom shape file
|
33
|
+
'sides', # default: 4; number of sides for shape=polygon
|
34
|
+
'skew' , # default: 0.0; skewing of node for shape=polygon
|
35
|
+
'style', # graphics options, e.g. bold, dotted, filled; cf. Section 2.3
|
36
|
+
'URL', # URL associated with node (format-dependent)
|
37
|
+
'width', # default: .75; width in inches
|
38
|
+
'z', #default: 0.0; z coordinate for VRML output
|
39
|
+
|
40
|
+
# maintained for backward compatibility or rdot internal
|
41
|
+
'bottomlabel', # auxiliary label for nodes of shape M*
|
42
|
+
'bgcolor',
|
43
|
+
'rank',
|
44
|
+
'toplabel' # auxiliary label for nodes of shape M*
|
45
|
+
]
|
46
|
+
|
47
|
+
# options for edge declaration
|
48
|
+
|
49
|
+
EDGE_OPTS = [
|
50
|
+
'arrowhead', # default: normal; style of arrowhead at head end
|
51
|
+
'arrowsize', # default: 1.0; scaling factor for arrowheads
|
52
|
+
'arrowtail', # default: normal; style of arrowhead at tail end
|
53
|
+
'color', # default: black; edge stroke color
|
54
|
+
'comment', # any string (format-dependent)
|
55
|
+
'constraint', # default: true use edge to affect node ranking
|
56
|
+
'decorate', # if set, draws a line connecting labels with their edges
|
57
|
+
'dir', # default: forward; forward, back, both, or none
|
58
|
+
'fontcolor', # default: black type face color
|
59
|
+
'fontname', # default: Times-Roman; font family
|
60
|
+
'fontsize', # default: 14; point size of label
|
61
|
+
'headlabel', # label placed near head of edge
|
62
|
+
'headport', # n,ne,e,se,s,sw,w,nw
|
63
|
+
'headURL', # URL attached to head label if output format is ismap
|
64
|
+
'label', # edge label
|
65
|
+
'labelangle', # default: -25.0; angle in degrees which head or tail label is rotated off edge
|
66
|
+
'labeldistance', # default: 1.0; scaling factor for distance of head or tail label from node
|
67
|
+
'labelfloat', # default: false; lessen constraints on edge label placement
|
68
|
+
'labelfontcolor', # default: black; type face color for head and tail labels
|
69
|
+
'labelfontname', # default: Times-Roman; font family for head and tail labels
|
70
|
+
'labelfontsize', # default: 14 point size for head and tail labels
|
71
|
+
'layer', # default: overlay range; all, id or id:id
|
72
|
+
'lhead', # name of cluster to use as head of edge
|
73
|
+
'ltail', # name of cluster to use as tail of edge
|
74
|
+
'minlen', # default: 1 minimum rank distance between head and tail
|
75
|
+
'samehead', # tag for head node; edge heads with the same tag are merged onto the same port
|
76
|
+
'sametail', # tag for tail node; edge tails with the same tag are merged onto the same port
|
77
|
+
'style', # graphics options, e.g. bold, dotted, filled; cf. Section 2.3
|
78
|
+
'taillabel', # label placed near tail of edge
|
79
|
+
'tailport', # n,ne,e,se,s,sw,w,nw
|
80
|
+
'tailURL', # URL attached to tail label if output format is ismap
|
81
|
+
'weight', # default: 1; integer cost of stretching an edge
|
82
|
+
|
83
|
+
# maintained for backward compatibility or rdot internal
|
84
|
+
'id'
|
85
|
+
]
|
86
|
+
|
87
|
+
# options for graph declaration
|
88
|
+
|
89
|
+
GRAPH_OPTS = [
|
90
|
+
'bgcolor', # background color for drawing, plus initial fill color
|
91
|
+
'center', # default: false; center draing on page
|
92
|
+
'clusterrank', # default: local; may be "global" or "none"
|
93
|
+
'color', # default: black; for clusters, outline color, and fill color if
|
94
|
+
# fillcolor not defined
|
95
|
+
'comment', # any string (format-dependent)
|
96
|
+
'compound', # default: false; allow edges between clusters
|
97
|
+
'concentrate', # default: false; enables edge concentrators
|
98
|
+
'fillcolor', # default: black; cluster fill color
|
99
|
+
'fontcolor', # default: black; type face color
|
100
|
+
'fontname', # default: Times-Roman; font family
|
101
|
+
'fontpath', # list of directories to search for fonts
|
102
|
+
'fontsize', # default: 14; point size of label
|
103
|
+
'label', # any string
|
104
|
+
'labeljust', # default: centered; "l" and "r" for left- and right-justified
|
105
|
+
# cluster labels, respectively
|
106
|
+
'labelloc', # default: top; "t" and "b" for top- and bottom-justified
|
107
|
+
# cluster labels, respectively
|
108
|
+
'layers', # id:id:id...
|
109
|
+
'margin', # default: .5; margin included in page, inches
|
110
|
+
'mclimit', # default: 1.0; scale factor for mincross iterations
|
111
|
+
'nodesep', # default: .25; separation between nodes, in inches.
|
112
|
+
'nslimit', # if set to "f", bounds network simplex iterations by
|
113
|
+
# (f)(number of nodes) when setting x-coordinates
|
114
|
+
'nslimit1', # if set to "f", bounds network simplex iterations by
|
115
|
+
# (f)(number of nodes) when ranking nodes
|
116
|
+
'ordering', # if "out" out edge order is preserved
|
117
|
+
'orientation', # default: portrait; if "rotate" is not used and the value is
|
118
|
+
# "landscape", use landscape orientation
|
119
|
+
'page', # unit of pagination, e.g. "8.5,11"
|
120
|
+
'rank', # "same", "min", "max", "source", or "sink"
|
121
|
+
'rankdir', # default: TB; "LR" (left to right) or "TB" (top to bottom)
|
122
|
+
'ranksep', # default: .75; separation between ranks, in inches.
|
123
|
+
'ratio', # approximate aspect ratio desired, "fill" or "auto"
|
124
|
+
'samplepoints', # default: 8; number of points used to represent ellipses
|
125
|
+
# and circles on output
|
126
|
+
'searchsize', # default: 30; maximum edges with negative cut values to check
|
127
|
+
# when looking for a minimum one during network simplex
|
128
|
+
'size', # maximum drawing size, in inches
|
129
|
+
'style', # graphics options, e.g. "filled" for clusters
|
130
|
+
'URL', # URL associated with graph (format-dependent)
|
131
|
+
|
132
|
+
# maintained for backward compatibility or rdot internal
|
133
|
+
'layerseq'
|
134
|
+
]
|
135
|
+
|
136
|
+
# Ancestor of Edge, Node, and Graph.
|
137
|
+
#
|
138
|
+
class Element
|
139
|
+
|
140
|
+
attr_accessor :name, :options
|
141
|
+
|
142
|
+
def initialize(params = {}, option_list = []) # :nodoc:
|
143
|
+
@name = params['name'] ? params['name'] : nil
|
144
|
+
@options = {}
|
145
|
+
|
146
|
+
option_list.each do |i|
|
147
|
+
@options[i] = params[i] if params[i]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
145
152
|
|
146
|
-
private
|
147
153
|
# Returns the string given in _id_ within quotes if necessary. Special
|
148
154
|
# characters are escaped as necessary.
|
155
|
+
#
|
149
156
|
def quote_ID(id)
|
150
157
|
# Ensure that the ID is a string.
|
151
158
|
id = id.to_s
|
152
159
|
|
153
160
|
# Return the ID verbatim if it looks like a name, a number, or HTML.
|
154
|
-
return id if id =~ /\A([[:alpha:]_][[:alnum:]_]*|-?(\.[[:digit:]]+|[[:digit:]]+(\.[[:digit:]]*)?)|<.*>)\Z/m
|
161
|
+
return id if id =~ /\A([[:alpha:]_][[:alnum:]_]*|-?(\.[[:digit:]]+|[[:digit:]]+(\.[[:digit:]]*)?)|<.*>)\Z/m && id[-1] != ?\n
|
155
162
|
|
156
163
|
# Return a quoted version of the ID otherwise.
|
157
164
|
'"' + id.gsub('\\', '\\\\\\\\').gsub('"', '\\\\"') + '"'
|
@@ -161,285 +168,315 @@ module RGL; module DOT
|
|
161
168
|
# characters are escaped as necessary. Labels get special treatment in
|
162
169
|
# order to handle embedded *\n*, *\r*, and *\l* sequences which are copied
|
163
170
|
# into the new string verbatim.
|
171
|
+
#
|
164
172
|
def quote_label(label)
|
165
173
|
# Ensure that the label is a string.
|
166
174
|
label = label.to_s
|
167
175
|
|
168
176
|
# Return the label verbatim if it looks like a name, a number, or HTML.
|
169
|
-
return label if label =~ /\A([[:alpha:]_][[:alnum:]_]*|-?(\.[[:digit:]]+|[[:digit:]]+(\.[[:digit:]]*)?)|<.*>)\Z/m
|
177
|
+
return label if label =~ /\A([[:alpha:]_][[:alnum:]_]*|-?(\.[[:digit:]]+|[[:digit:]]+(\.[[:digit:]]*)?)|<.*>)\Z/m && label[-1] != ?\n
|
170
178
|
|
171
179
|
# Return a quoted version of the label otherwise.
|
172
180
|
'"' + label.split(/(\\n|\\r|\\l)/).collect do |part|
|
173
181
|
case part
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
182
|
+
when "\\n", "\\r", "\\l"
|
183
|
+
part
|
184
|
+
else
|
185
|
+
part.gsub('\\', '\\\\\\\\').gsub('"', '\\\\"').gsub("\n", '\\n')
|
178
186
|
end
|
179
187
|
end.join + '"'
|
180
188
|
end
|
181
|
-
|
182
|
-
|
189
|
+
end
|
183
190
|
|
184
|
-
# Ports are used when a Node instance has its `shape' option set to
|
185
|
-
# _record_ or _Mrecord_. Ports can be nested.
|
186
|
-
class Port
|
187
|
-
attr_accessor :name, :label, :ports
|
188
191
|
|
189
|
-
#
|
190
|
-
#
|
192
|
+
# Ports are used when a Node instance has its `shape' option set to
|
193
|
+
# _record_ or _Mrecord_. Ports can be nested.
|
191
194
|
#
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
195
|
+
class Port
|
196
|
+
attr_accessor :name, :label, :ports
|
197
|
+
|
198
|
+
# Create a new port with either an optional name and label or a set of
|
199
|
+
# nested ports.
|
200
|
+
#
|
201
|
+
# :call-seq:
|
202
|
+
# new(name = nil, label = nil)
|
203
|
+
# new(ports)
|
204
|
+
#
|
205
|
+
# A +nil+ value for +name+ is valid; otherwise, it must be a String or it
|
206
|
+
# will be interpreted as +ports+.
|
207
|
+
#
|
208
|
+
def initialize(name_or_ports = nil, label = nil)
|
209
|
+
if name_or_ports.nil? || name_or_ports.kind_of?(String)
|
210
|
+
@name = name_or_ports
|
211
|
+
@label = label
|
212
|
+
@ports = nil
|
213
|
+
else
|
214
|
+
@ports = name_or_ports
|
215
|
+
@name = nil
|
216
|
+
@label = nil
|
217
|
+
end
|
207
218
|
end
|
208
|
-
end
|
209
219
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
220
|
+
# Returns a string representation of this port. If ports is a non-empty
|
221
|
+
# Enumerable, a nested ports representation is returned; otherwise, a
|
222
|
+
# name-label representation is returned.
|
223
|
+
#
|
224
|
+
def to_s
|
225
|
+
if @ports.nil? || @ports.empty?
|
226
|
+
n = (name.nil? || name.empty?) ? '' : "<#{name}>"
|
227
|
+
n + ((n.empty? || label.nil? || label.empty?) ? '' : ' ') + label.to_s
|
228
|
+
else
|
229
|
+
'{' + @ports.collect { |p| p.to_s }.join(' | ') + '}'
|
230
|
+
end
|
219
231
|
end
|
220
232
|
end
|
221
|
-
end
|
222
|
-
|
223
|
-
# A node representation. Edges are drawn between nodes. The rendering of a
|
224
|
-
# node depends upon the options set for it.
|
225
|
-
class Node < Element
|
226
|
-
attr_accessor :ports
|
227
|
-
|
228
|
-
# Creates a new Node with the _params_ Hash providing settings for all
|
229
|
-
# node options. The _option_list_ parameter restricts those options to the
|
230
|
-
# list of valid names it contains. The exception to this is the _ports_
|
231
|
-
# option which, if specified, must be an Enumerable containing a list of
|
232
|
-
# ports.
|
233
|
-
def initialize (params = {}, option_list = NODE_OPTS)
|
234
|
-
super(params, option_list)
|
235
|
-
@ports = params['ports'] ? params['ports'] : []
|
236
|
-
end
|
237
233
|
|
238
|
-
#
|
239
|
-
#
|
240
|
-
#
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
234
|
+
# A node representation. Edges are drawn between nodes. The rendering of a
|
235
|
+
# node depends upon the options set for it.
|
236
|
+
#
|
237
|
+
class Node < Element
|
238
|
+
|
239
|
+
attr_accessor :ports
|
240
|
+
|
241
|
+
# Creates a new Node with the _params_ Hash providing settings for all
|
242
|
+
# node options. The _option_list_ parameter restricts those options to the
|
243
|
+
# list of valid names it contains. The exception to this is the _ports_
|
244
|
+
# option which, if specified, must be an Enumerable containing a list of
|
245
|
+
# ports.
|
246
|
+
#
|
247
|
+
def initialize(params = {}, option_list = NODE_OPTS)
|
248
|
+
super(params, option_list)
|
249
|
+
@ports = params['ports'] ? params['ports'] : []
|
251
250
|
end
|
252
251
|
|
253
|
-
#
|
254
|
-
#
|
255
|
-
#
|
256
|
-
|
257
|
-
|
258
|
-
|
252
|
+
# Returns a string representation of this node which is consumable by the
|
253
|
+
# graphviz tools +dot+ and +neato+. The _leader_ parameter is used to indent
|
254
|
+
# every line of the returned string, and the _indent_ parameter is used to
|
255
|
+
# additionally indent nested items.
|
256
|
+
#
|
257
|
+
def to_s(leader = '', indent = ' ')
|
258
|
+
label_option = nil
|
259
|
+
|
260
|
+
if @options['shape'] =~ /^M?record$/ && !@ports.empty?
|
261
|
+
# Ignore the given label option in this case since the ports should each
|
262
|
+
# provide their own name/label.
|
263
|
+
label_option = leader + indent + "#{quote_ID('label')} = #{quote_ID(@ports.collect { |port| port.to_s }.join(" | "))}"
|
264
|
+
elsif @options['label']
|
265
|
+
# Otherwise, use the label when given one.
|
266
|
+
label_option = leader + indent + "#{quote_ID('label')} = #{quote_label(@options['label'])}"
|
267
|
+
end
|
268
|
+
|
269
|
+
# Convert all the options except `label' and options with nil values
|
270
|
+
# straight into name = value pairs. Then toss out any resulting nil
|
271
|
+
# entries in the final array.
|
272
|
+
stringified_options = @options.collect do |name, val|
|
273
|
+
unless name == 'label' || val.nil?
|
274
|
+
leader + indent + "#{quote_ID(name)} = #{quote_ID(val)}"
|
275
|
+
end
|
276
|
+
end.compact
|
277
|
+
|
278
|
+
# Append the specially computed label option.
|
279
|
+
stringified_options.push(label_option) unless label_option.nil?
|
280
|
+
|
281
|
+
# Join them all together.
|
282
|
+
stringified_options = stringified_options.join(",\n")
|
283
|
+
|
284
|
+
# Put it all together into a single string with indentation and return the
|
285
|
+
# result.
|
286
|
+
if stringified_options.empty?
|
287
|
+
leader + quote_ID(@name) unless @name.nil?
|
288
|
+
else
|
289
|
+
leader + (@name.nil? ? '' : quote_ID(@name) + " ") + "[\n" +
|
290
|
+
stringified_options + "\n" +
|
291
|
+
leader + "]"
|
259
292
|
end
|
260
|
-
end.compact
|
261
|
-
# Append the specially computed label option.
|
262
|
-
stringified_options.push(label_option) unless label_option.nil?
|
263
|
-
# Join them all together.
|
264
|
-
stringified_options = stringified_options.join(",\n")
|
265
|
-
|
266
|
-
# Put it all together into a single string with indentation and return the
|
267
|
-
# result.
|
268
|
-
if stringified_options.empty? then
|
269
|
-
return leader + quote_ID(@name) unless @name.nil?
|
270
|
-
return nil
|
271
|
-
else
|
272
|
-
return leader + (@name.nil? ? '' : quote_ID(@name) + " ") + "[\n" +
|
273
|
-
stringified_options + "\n" +
|
274
|
-
leader + "]"
|
275
293
|
end
|
276
|
-
end
|
277
294
|
|
278
|
-
|
279
|
-
|
280
|
-
# A graph representation. Whether or not it is rendered as directed or
|
281
|
-
# undirected depends on which of the programs *dot* or *neato* is used to
|
282
|
-
# process and render the graph.
|
283
|
-
class Graph < Element
|
284
|
-
|
285
|
-
# Creates a new Graph with the _params_ Hash providing settings for all
|
286
|
-
# graph options. The _option_list_ parameter restricts those options to the
|
287
|
-
# list of valid names it contains. The exception to this is the _elements_
|
288
|
-
# option which, if specified, must be an Enumerable containing a list of
|
289
|
-
# nodes, edges, and/or subgraphs.
|
290
|
-
def initialize (params = {}, option_list = GRAPH_OPTS)
|
291
|
-
super(params, option_list)
|
292
|
-
@elements = params['elements'] ? params['elements'] : []
|
293
|
-
@dot_string = 'graph'
|
294
|
-
end
|
295
|
+
end # class Node
|
295
296
|
|
296
|
-
#
|
297
|
-
#
|
297
|
+
# A graph representation. Whether or not it is rendered as directed or
|
298
|
+
# undirected depends on which of the programs *dot* or *neato* is used to
|
299
|
+
# process and render the graph.
|
298
300
|
#
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
301
|
+
class Graph < Element
|
302
|
+
|
303
|
+
# Creates a new Graph with the _params_ Hash providing settings for all
|
304
|
+
# graph options. The _option_list_ parameter restricts those options to the
|
305
|
+
# list of valid names it contains. The exception to this is the _elements_
|
306
|
+
# option which, if specified, must be an Enumerable containing a list of
|
307
|
+
# nodes, edges, and/or subgraphs.
|
308
|
+
#
|
309
|
+
def initialize(params = {}, option_list = GRAPH_OPTS)
|
310
|
+
super(params, option_list)
|
311
|
+
@elements = params['elements'] ? params['elements'] : []
|
312
|
+
@dot_string = 'graph'
|
313
|
+
end
|
306
314
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
315
|
+
# Calls _block_ once for each node, edge, or subgraph contained by this
|
316
|
+
# graph, passing the node, edge, or subgraph to the block.
|
317
|
+
#
|
318
|
+
# :call-seq:
|
319
|
+
# graph.each_element {|element| block} -> graph
|
320
|
+
#
|
321
|
+
def each_element(&block)
|
322
|
+
@elements.each(&block)
|
323
|
+
self
|
324
|
+
end
|
325
|
+
|
326
|
+
# Adds a new node, edge, or subgraph to this graph.
|
327
|
+
#
|
328
|
+
# :call-seq:
|
329
|
+
# graph << element -> graph
|
330
|
+
#
|
331
|
+
def << (element)
|
332
|
+
@elements << element
|
333
|
+
self
|
334
|
+
end
|
335
|
+
|
336
|
+
alias push <<
|
337
|
+
|
338
|
+
# Removes the most recently added node, edge, or subgraph from this graph
|
339
|
+
# and returns it.
|
340
|
+
#
|
341
|
+
# :call-seq:
|
342
|
+
# graph.pop -> element
|
343
|
+
#
|
344
|
+
def pop
|
345
|
+
@elements.pop
|
346
|
+
end
|
347
|
+
|
348
|
+
# Returns a string representation of this graph which is consumable by the
|
349
|
+
# graphviz tools +dot+ and +neato+. The _leader_ parameter is used to indent
|
350
|
+
# every line of the returned string, and the _indent_ parameter is used to
|
351
|
+
# additionally indent nested items.
|
352
|
+
#
|
353
|
+
def to_s(leader = '', indent = ' ')
|
354
|
+
hdr = leader + @dot_string + (@name.nil? ? '' : ' ' + quote_ID(@name)) + " {\n"
|
355
|
+
|
356
|
+
options = @options.to_a.collect do |name, val|
|
357
|
+
unless val.nil?
|
358
|
+
if name == 'label'
|
359
|
+
leader + indent + "#{quote_ID(name)} = #{quote_label(val)}"
|
360
|
+
else
|
361
|
+
leader + indent + "#{quote_ID(name)} = #{quote_ID(val)}"
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end.compact.join("\n")
|
365
|
+
|
366
|
+
elements = @elements.collect do |element|
|
367
|
+
element.to_s(leader + indent, indent)
|
368
|
+
end.join("\n\n")
|
369
|
+
|
370
|
+
hdr + (options.empty? ? '' : options + "\n\n") +
|
371
|
+
(elements.empty? ? '' : elements + "\n") + leader + "}"
|
372
|
+
end
|
373
|
+
|
374
|
+
end # class Graph
|
375
|
+
|
376
|
+
# A digraph is a directed graph representation which is the same as a Graph
|
377
|
+
# except that its header in dot notation has an identifier of _digraph_
|
378
|
+
# instead of _graph_.
|
311
379
|
#
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
380
|
+
class Digraph < Graph
|
381
|
+
|
382
|
+
# Creates a new Digraph with the _params_ Hash providing settings for all
|
383
|
+
# graph options. The _option_list_ parameter restricts those options to the
|
384
|
+
# list of valid names it contains. The exception to this is the _elements_
|
385
|
+
# option which, if specified, must be an Enumerable containing a list of
|
386
|
+
# nodes, edges, and/or subgraphs.
|
387
|
+
#
|
388
|
+
def initialize(params = {}, option_list = GRAPH_OPTS)
|
389
|
+
super(params, option_list)
|
390
|
+
@dot_string = 'digraph'
|
391
|
+
end
|
317
392
|
|
318
|
-
#
|
319
|
-
|
393
|
+
end # class Digraph
|
394
|
+
|
395
|
+
# A subgraph is a nested graph element and is the same as a Graph except
|
396
|
+
# that its header in dot notation has an identifier of _subgraph_ instead of
|
397
|
+
# _graph_.
|
320
398
|
#
|
321
|
-
|
322
|
-
|
399
|
+
class Subgraph < Graph
|
400
|
+
|
401
|
+
# Creates a new Subgraph with the _params_ Hash providing settings for
|
402
|
+
# all graph options. The _option_list_ parameter restricts those options to
|
403
|
+
# list of valid names it contains. The exception to this is the _elements_
|
404
|
+
# option which, if specified, must be an Enumerable containing a list of
|
405
|
+
# nodes, edges, and/or subgraphs.
|
406
|
+
#
|
407
|
+
def initialize(params = {}, option_list = GRAPH_OPTS)
|
408
|
+
super(params, option_list)
|
409
|
+
@dot_string = 'subgraph'
|
410
|
+
end
|
411
|
+
|
412
|
+
end # class Subgraph
|
413
|
+
|
414
|
+
# This is an undirected edge representation.
|
323
415
|
#
|
324
|
-
|
325
|
-
|
326
|
-
|
416
|
+
class Edge < Element
|
417
|
+
|
418
|
+
# A node or subgraph reference or instance to be used as the starting point
|
419
|
+
# for an edge.
|
420
|
+
attr_accessor :from
|
421
|
+
|
422
|
+
# A node or subgraph reference or instance to be used as the ending point
|
423
|
+
# for an edge.
|
424
|
+
attr_accessor :to
|
425
|
+
|
426
|
+
# Creates a new Edge with the _params_ Hash providing settings for all
|
427
|
+
# edge options. The _option_list_ parameter restricts those options to the
|
428
|
+
# list of valid names it contains.
|
429
|
+
#
|
430
|
+
def initialize(params = {}, option_list = EDGE_OPTS)
|
431
|
+
super(params, option_list)
|
432
|
+
@from = params['from'] ? params['from'] : nil
|
433
|
+
@to = params['to'] ? params['to'] : nil
|
434
|
+
end
|
327
435
|
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
unless val.nil? then
|
337
|
-
if name == 'label' then
|
338
|
-
leader + indent + "#{quote_ID(name)} = #{quote_label(val)}"
|
339
|
-
else
|
436
|
+
# Returns a string representation of this edge which is consumable by the
|
437
|
+
# graphviz tools +dot+ and +neato+. The _leader_ parameter is used to indent
|
438
|
+
# every line of the returned string, and the _indent_ parameter is used to
|
439
|
+
# additionally indent nested items.
|
440
|
+
#
|
441
|
+
def to_s(leader = '', indent = ' ')
|
442
|
+
stringified_options = @options.collect do |name, val|
|
443
|
+
unless val.nil?
|
340
444
|
leader + indent + "#{quote_ID(name)} = #{quote_ID(val)}"
|
341
445
|
end
|
342
|
-
end
|
343
|
-
end.compact.join( "\n" )
|
344
|
-
|
345
|
-
elements = @elements.collect do |element|
|
346
|
-
element.to_s(leader + indent, indent)
|
347
|
-
end.join("\n\n")
|
348
|
-
hdr + (options.empty? ? '' : options + "\n\n") +
|
349
|
-
(elements.empty? ? '' : elements + "\n") + leader + "}"
|
350
|
-
end
|
446
|
+
end.compact.join(",\n")
|
351
447
|
|
352
|
-
|
353
|
-
|
354
|
-
# A digraph is a directed graph representation which is the same as a Graph
|
355
|
-
# except that its header in dot notation has an identifier of _digraph_
|
356
|
-
# instead of _graph_.
|
357
|
-
class Digraph < Graph
|
358
|
-
|
359
|
-
# Creates a new Digraph with the _params_ Hash providing settings for all
|
360
|
-
# graph options. The _option_list_ parameter restricts those options to the
|
361
|
-
# list of valid names it contains. The exception to this is the _elements_
|
362
|
-
# option which, if specified, must be an Enumerable containing a list of
|
363
|
-
# nodes, edges, and/or subgraphs.
|
364
|
-
def initialize (params = {}, option_list = GRAPH_OPTS)
|
365
|
-
super(params, option_list)
|
366
|
-
@dot_string = 'digraph'
|
367
|
-
end
|
368
|
-
|
369
|
-
end # class Digraph
|
370
|
-
|
371
|
-
# A subgraph is a nested graph element and is the same as a Graph except
|
372
|
-
# that its header in dot notation has an identifier of _subgraph_ instead of
|
373
|
-
# _graph_.
|
374
|
-
class Subgraph < Graph
|
375
|
-
|
376
|
-
# Creates a new Subgraph with the _params_ Hash providing settings for
|
377
|
-
# all graph options. The _option_list_ parameter restricts those options to
|
378
|
-
# list of valid names it contains. The exception to this is the _elements_
|
379
|
-
# option which, if specified, must be an Enumerable containing a list of
|
380
|
-
# nodes, edges, and/or subgraphs.
|
381
|
-
def initialize (params = {}, option_list = GRAPH_OPTS)
|
382
|
-
super(params, option_list)
|
383
|
-
@dot_string = 'subgraph'
|
384
|
-
end
|
448
|
+
f_s = @from || ''
|
449
|
+
t_s = @to || ''
|
385
450
|
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
# for an edge.
|
393
|
-
attr_accessor :from
|
394
|
-
# A node or subgraph reference or instance to be used as the ending point
|
395
|
-
# for an edge.
|
396
|
-
attr_accessor :to
|
397
|
-
|
398
|
-
# Creates a new Edge with the _params_ Hash providing settings for all
|
399
|
-
# edge options. The _option_list_ parameter restricts those options to the
|
400
|
-
# list of valid names it contains.
|
401
|
-
def initialize (params = {}, option_list = EDGE_OPTS)
|
402
|
-
super(params, option_list)
|
403
|
-
@from = params['from'] ? params['from'] : nil
|
404
|
-
@to = params['to'] ? params['to'] : nil
|
405
|
-
end
|
406
|
-
|
407
|
-
# Returns a string representation of this edge which is consumable by the
|
408
|
-
# graphviz tools +dot+ and +neato+. The _leader_ parameter is used to indent
|
409
|
-
# every line of the returned string, and the _indent_ parameter is used to
|
410
|
-
# additionally indent nested items.
|
411
|
-
def to_s (leader = '', indent = ' ')
|
412
|
-
stringified_options = @options.collect do |name, val|
|
413
|
-
unless val.nil? then
|
414
|
-
leader + indent + "#{quote_ID(name)} = #{quote_ID(val)}"
|
451
|
+
if stringified_options.empty?
|
452
|
+
leader + quote_ID(f_s) + ' ' + edge_link + ' ' + quote_ID(t_s)
|
453
|
+
else
|
454
|
+
leader + quote_ID(f_s) + ' ' + edge_link + ' ' + quote_ID(t_s) + " [\n" +
|
455
|
+
stringified_options + "\n" +
|
456
|
+
leader + "]"
|
415
457
|
end
|
416
|
-
end.compact.join( ",\n" )
|
417
|
-
|
418
|
-
f_s = @from || ''
|
419
|
-
t_s = @to || ''
|
420
|
-
if stringified_options.empty? then
|
421
|
-
leader + quote_ID(f_s) + ' ' + edge_link + ' ' + quote_ID(t_s)
|
422
|
-
else
|
423
|
-
leader + quote_ID(f_s) + ' ' + edge_link + ' ' + quote_ID(t_s) + " [\n" +
|
424
|
-
stringified_options + "\n" +
|
425
|
-
leader + "]"
|
426
458
|
end
|
427
|
-
end
|
428
459
|
|
429
|
-
|
460
|
+
private
|
461
|
+
|
430
462
|
def edge_link
|
431
463
|
'--'
|
432
464
|
end
|
433
465
|
|
434
|
-
|
466
|
+
end # class Edge
|
435
467
|
|
436
|
-
|
437
|
-
|
468
|
+
# A directed edge representation otherwise identical to Edge.
|
469
|
+
#
|
470
|
+
class DirectedEdge < Edge
|
471
|
+
|
472
|
+
private
|
438
473
|
|
439
|
-
private
|
440
474
|
def edge_link
|
441
475
|
'->'
|
442
476
|
end
|
443
477
|
|
444
|
-
|
445
|
-
|
478
|
+
end # class DirectedEdge
|
479
|
+
|
480
|
+
end # module DOT
|
481
|
+
|
482
|
+
end # module RGL
|