elkrb 1.0.2 → 2.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +364 -0
- data/LICENSE +25 -0
- data/README.adoc +695 -557
- data/exe/elkrb +36 -2
- data/lib/elkrb/atomic_destination.rb +53 -0
- data/lib/elkrb/best_effort_write.rb +49 -0
- data/lib/elkrb/cli/layout_flag_options.rb +54 -0
- data/lib/elkrb/cli.rb +155 -82
- data/lib/elkrb/command_resolver.rb +186 -0
- data/lib/elkrb/commands/batch_command.rb +28 -5
- data/lib/elkrb/commands/convert_command.rb +22 -52
- data/lib/elkrb/commands/diagram_command.rb +234 -102
- data/lib/elkrb/commands/options_command.rb +87 -0
- data/lib/elkrb/commands/render_command.rb +6 -1
- data/lib/elkrb/commands/validate_command.rb +146 -91
- data/lib/elkrb/errors.rb +20 -1
- data/lib/elkrb/format_sniffer.rb +362 -0
- data/lib/elkrb/geometry/bezier.rb +14 -4
- data/lib/elkrb/geometry/point.rb +5 -6
- data/lib/elkrb/geometry/rectangle.rb +2 -2
- data/lib/elkrb/graph/deep_stringify_keys.rb +30 -0
- data/lib/elkrb/graph/edge.rb +89 -3
- data/lib/elkrb/graph/graph.rb +43 -15
- data/lib/elkrb/graph/label.rb +16 -2
- data/lib/elkrb/graph/node.rb +13 -2
- data/lib/elkrb/graph/node_constraints.rb +139 -8
- data/lib/elkrb/graph/normalize_option_keys.rb +39 -0
- data/lib/elkrb/graph/port.rb +29 -21
- data/lib/elkrb/graph/read_only_mapping.rb +21 -0
- data/lib/elkrb/graphviz_wrapper.rb +152 -44
- data/lib/elkrb/layout/algorithm_registry.rb +65 -8
- data/lib/elkrb/layout/algorithms/base_algorithm.rb +74 -74
- data/lib/elkrb/layout/algorithms/box.rb +47 -20
- data/lib/elkrb/layout/algorithms/disco.rb +150 -61
- data/lib/elkrb/layout/algorithms/fixed.rb +70 -10
- data/lib/elkrb/layout/algorithms/force.rb +145 -86
- data/lib/elkrb/layout/algorithms/layered/bk/aligner.rb +104 -0
- data/lib/elkrb/layout/algorithms/layered/bk/compactor.rb +87 -0
- data/lib/elkrb/layout/algorithms/layered/bk/view.rb +103 -0
- data/lib/elkrb/layout/algorithms/layered/bk_node_placer.rb +170 -0
- data/lib/elkrb/layout/algorithms/layered/components.rb +170 -0
- data/lib/elkrb/layout/algorithms/layered/crossing_minimizer.rb +306 -0
- data/lib/elkrb/layout/algorithms/layered/cycle_breaker.rb +118 -49
- data/lib/elkrb/layout/algorithms/layered/discovery_order.rb +75 -0
- data/lib/elkrb/layout/algorithms/layered/dummy_slot.rb +31 -0
- data/lib/elkrb/layout/algorithms/layered/layer_assigner.rb +188 -48
- data/lib/elkrb/layout/algorithms/layered/layer_constraints.rb +82 -0
- data/lib/elkrb/layout/algorithms/layered/layer_sweep.rb +195 -0
- data/lib/elkrb/layout/algorithms/layered/node_placer.rb +214 -36
- data/lib/elkrb/layout/algorithms/layered/orthogonal/gap_solver.rb +219 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal/hyper_edge_segment.rb +152 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal/junction_points.rb +58 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal/segment_order.rb +134 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal_router.rb +253 -0
- data/lib/elkrb/layout/algorithms/layered/port_order.rb +188 -0
- data/lib/elkrb/layout/algorithms/layered/port_spread.rb +42 -0
- data/lib/elkrb/layout/algorithms/layered.rb +548 -13
- data/lib/elkrb/layout/algorithms/libavoid.rb +951 -187
- data/lib/elkrb/layout/algorithms/mrtree.rb +585 -65
- data/lib/elkrb/layout/algorithms/radial.rb +188 -29
- data/lib/elkrb/layout/algorithms/random.rb +131 -17
- data/lib/elkrb/layout/algorithms/rectpacking.rb +28 -40
- data/lib/elkrb/layout/algorithms/spore_compaction.rb +57 -100
- data/lib/elkrb/layout/algorithms/spore_overlap.rb +78 -84
- data/lib/elkrb/layout/algorithms/stress.rb +391 -84
- data/lib/elkrb/layout/algorithms/topdown_packing.rb +95 -31
- data/lib/elkrb/layout/algorithms/vertiflex.rb +12 -47
- data/lib/elkrb/layout/constraints/alignment_constraint.rb +49 -12
- data/lib/elkrb/layout/constraints/base_constraint.rb +3 -15
- data/lib/elkrb/layout/constraints/constraint_processor.rb +9 -23
- data/lib/elkrb/layout/constraints/fixed_position_constraint.rb +22 -13
- data/lib/elkrb/layout/constraints/layer_constraint.rb +10 -33
- data/lib/elkrb/layout/constraints/relative_position_constraint.rb +94 -50
- data/lib/elkrb/layout/edge_router.rb +533 -337
- data/lib/elkrb/layout/hierarchical_processor.rb +19 -276
- data/lib/elkrb/layout/java_random.rb +59 -0
- data/lib/elkrb/layout/label_placer.rb +165 -157
- data/lib/elkrb/layout/layout_engine.rb +46 -23
- data/lib/elkrb/layout/node_index.rb +149 -0
- data/lib/elkrb/layout/polyomino/component_compactor.rb +138 -0
- data/lib/elkrb/layout/polyomino/footprint.rb +94 -0
- data/lib/elkrb/layout/polyomino/grid.rb +106 -0
- data/lib/elkrb/layout/polyomino/packer.rb +95 -0
- data/lib/elkrb/layout/polyomino/piece.rb +52 -0
- data/lib/elkrb/layout/port_constraint_processor.rb +131 -44
- data/lib/elkrb/layout/spore/body.rb +65 -0
- data/lib/elkrb/layout/spore/depth_first_compaction.rb +127 -0
- data/lib/elkrb/layout/spore/edge_cost.rb +69 -0
- data/lib/elkrb/layout/spore/gap.rb +76 -0
- data/lib/elkrb/layout/spore/geometry.rb +104 -0
- data/lib/elkrb/layout/spore/overlap_sweep.rb +90 -0
- data/lib/elkrb/layout/spore/placement.rb +57 -0
- data/lib/elkrb/layout/spore/rays.rb +119 -0
- data/lib/elkrb/layout/spore/spanning_tree.rb +53 -0
- data/lib/elkrb/layout/spore/triangulation.rb +159 -0
- data/lib/elkrb/layout_flags.rb +69 -0
- data/lib/elkrb/options/decimal.rb +50 -0
- data/lib/elkrb/options/element_options.rb +43 -0
- data/lib/elkrb/options/elk_padding.rb +20 -14
- data/lib/elkrb/options/k_vector.rb +6 -7
- data/lib/elkrb/options/k_vector_chain.rb +12 -20
- data/lib/elkrb/options/option_map.rb +85 -0
- data/lib/elkrb/options/registry.rb +335 -0
- data/lib/elkrb/options/resolver.rb +151 -0
- data/lib/elkrb/options/spellings.rb +62 -0
- data/lib/elkrb/options/unhonoured_report.rb +194 -0
- data/lib/elkrb/parsers/elkt/lexer.rb +309 -0
- data/lib/elkrb/parsers/elkt/parser.rb +424 -0
- data/lib/elkrb/parsers/elkt/resolver.rb +142 -0
- data/lib/elkrb/parsers/elkt/token.rb +40 -0
- data/lib/elkrb/parsers/elkt_parser.rb +32 -230
- data/lib/elkrb/serializers/dot_serializer.rb +342 -54
- data/lib/elkrb/serializers/elkt_serializer.rb +386 -155
- data/lib/elkrb/version.rb +1 -1
- data/lib/elkrb.rb +60 -61
- metadata +71 -49
- data/.rspec +0 -3
- data/.rubocop.yml +0 -11
- data/Gemfile +0 -13
- data/Rakefile +0 -64
- data/benchmarks/README.md +0 -172
- data/benchmarks/elkjs_benchmark.js +0 -140
- data/benchmarks/elkrb_benchmark.rb +0 -145
- data/benchmarks/fixtures/graphs.json +0 -10777
- data/benchmarks/generate_report.rb +0 -241
- data/benchmarks/generate_test_graphs.rb +0 -154
- data/benchmarks/results/elkrb_results.json +0 -280
- data/benchmarks/results/elkrb_summary.json +0 -285
- data/elkrb.gemspec +0 -39
- data/examples/dot_export_demo.rb +0 -133
- data/examples/hierarchical_graph.rb +0 -19
- data/examples/layout_constraints_demo.rb +0 -272
- data/examples/port_constraints_demo.rb +0 -291
- data/examples/self_loop_demo.rb +0 -391
- data/examples/simple_graph.rb +0 -50
- data/examples/spline_routing_demo.rb +0 -235
- data/lib/elkrb/graph/layout_options.rb +0 -247
- data/sig/elkrb/constraints.rbs +0 -114
- data/sig/elkrb/geometry.rbs +0 -61
- data/sig/elkrb/graph.rbs +0 -112
- data/sig/elkrb/layout.rbs +0 -107
- data/sig/elkrb/options.rbs +0 -81
- data/sig/elkrb.rbs +0 -32
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../options/resolver"
|
|
4
|
+
|
|
3
5
|
module Elkrb
|
|
4
6
|
module Serializers
|
|
5
7
|
# Serializes ELK graphs to Graphviz DOT format
|
|
@@ -23,6 +25,9 @@ module Elkrb
|
|
|
23
25
|
# rankdir: "TB"
|
|
24
26
|
# )
|
|
25
27
|
class DotSerializer
|
|
28
|
+
HtmlValue = Data.define(:text)
|
|
29
|
+
private_constant :HtmlValue
|
|
30
|
+
|
|
26
31
|
# Default indentation width for DOT output
|
|
27
32
|
INDENT_WIDTH = 2
|
|
28
33
|
|
|
@@ -43,14 +48,15 @@ module Elkrb
|
|
|
43
48
|
graph_attrs: {},
|
|
44
49
|
node_attrs: {},
|
|
45
50
|
edge_attrs: {},
|
|
51
|
+
engine: nil,
|
|
46
52
|
}.merge(options)
|
|
47
53
|
|
|
48
54
|
@indent_level = 0
|
|
49
|
-
@node_counter = 0
|
|
50
|
-
@cluster_counter = 0
|
|
51
55
|
|
|
52
56
|
# Convert hash to Graph if needed
|
|
53
57
|
@graph = graph.is_a?(Hash) ? hash_to_graph(graph) : graph
|
|
58
|
+
index_endpoints(@graph)
|
|
59
|
+
@scope_stack = [@endpoint_scopes.fetch(@graph)]
|
|
54
60
|
|
|
55
61
|
lines = []
|
|
56
62
|
lines << graph_declaration
|
|
@@ -97,20 +103,20 @@ module Elkrb
|
|
|
97
103
|
# Generate graph declaration
|
|
98
104
|
def graph_declaration
|
|
99
105
|
type = @options[:directed] ? "digraph" : "graph"
|
|
100
|
-
"#{type} #{@options[:graph_name]}"
|
|
106
|
+
"#{type} #{quote_id(@options[:graph_name])}"
|
|
101
107
|
end
|
|
102
108
|
|
|
103
109
|
# Format graph-level attributes
|
|
104
110
|
def format_graph_attributes(graph)
|
|
105
111
|
attrs = @options[:graph_attrs].dup
|
|
112
|
+
attrs[:compound] = true if @clusters.any?
|
|
106
113
|
|
|
107
114
|
# Add rankdir from options or graph layout options
|
|
108
115
|
if @options[:rankdir]
|
|
109
116
|
attrs[:rankdir] = @options[:rankdir]
|
|
110
|
-
elsif
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
117
|
+
elsif (direction = option_resolver.get("elk.direction", graph,
|
|
118
|
+
default: nil))
|
|
119
|
+
attrs[:rankdir] = elk_direction_to_rankdir(direction)
|
|
114
120
|
end
|
|
115
121
|
|
|
116
122
|
# Add graph size if specified
|
|
@@ -134,48 +140,41 @@ module Elkrb
|
|
|
134
140
|
lines.concat(format_subgraph(node))
|
|
135
141
|
else
|
|
136
142
|
# Simple node
|
|
137
|
-
node_id =
|
|
143
|
+
node_id = quote_id(@node_names.fetch(node))
|
|
138
144
|
attrs = build_node_attributes(node)
|
|
139
145
|
|
|
140
146
|
lines << indent("#{node_id} #{format_attrs(attrs)}")
|
|
141
147
|
end
|
|
142
148
|
|
|
143
|
-
# Process child edges if any
|
|
144
|
-
if node.edges && !node.edges.empty?
|
|
145
|
-
node.edges.each do |edge|
|
|
146
|
-
lines.concat(format_edge(edge))
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
149
|
lines
|
|
151
150
|
end
|
|
152
151
|
|
|
153
152
|
# Format a subgraph (hierarchical node)
|
|
154
153
|
def format_subgraph(node)
|
|
155
154
|
lines = []
|
|
156
|
-
cluster_id =
|
|
157
|
-
@cluster_counter += 1
|
|
155
|
+
cluster_id = @clusters.fetch(node)
|
|
158
156
|
|
|
159
157
|
lines << indent("subgraph #{cluster_id} {")
|
|
160
158
|
@indent_level += 1
|
|
161
159
|
|
|
162
160
|
# Subgraph label
|
|
163
161
|
if node.labels && !node.labels.empty?
|
|
164
|
-
|
|
165
|
-
lines << indent("label=#{quote_value(label_text)}")
|
|
162
|
+
lines << indent("label=#{quote_labels(node.labels.map(&:text))}")
|
|
166
163
|
end
|
|
167
164
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
node.children
|
|
171
|
-
|
|
165
|
+
with_endpoint_scope(node) do
|
|
166
|
+
# Process children
|
|
167
|
+
if node.children && !node.children.empty?
|
|
168
|
+
node.children.each do |child|
|
|
169
|
+
lines.concat(format_node(child, node.id))
|
|
170
|
+
end
|
|
172
171
|
end
|
|
173
|
-
end
|
|
174
172
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
# Process edges within this subgraph
|
|
174
|
+
if node.edges && !node.edges.empty?
|
|
175
|
+
node.edges.each do |edge|
|
|
176
|
+
lines.concat(format_edge(edge))
|
|
177
|
+
end
|
|
179
178
|
end
|
|
180
179
|
end
|
|
181
180
|
|
|
@@ -191,10 +190,9 @@ module Elkrb
|
|
|
191
190
|
|
|
192
191
|
# Label
|
|
193
192
|
if node.labels && !node.labels.empty?
|
|
194
|
-
|
|
195
|
-
attrs[:label] = label_text
|
|
193
|
+
attrs[:label] = node.labels.map(&:text)
|
|
196
194
|
elsif node.id
|
|
197
|
-
attrs[:label] = node.id
|
|
195
|
+
attrs[:label] = [node.id]
|
|
198
196
|
end
|
|
199
197
|
|
|
200
198
|
# Size (DOT uses inches)
|
|
@@ -205,7 +203,7 @@ module Elkrb
|
|
|
205
203
|
end
|
|
206
204
|
|
|
207
205
|
# Position (if laid out)
|
|
208
|
-
if node.x && node.y
|
|
206
|
+
if neato? && node.x && node.y
|
|
209
207
|
# DOT uses center coordinates, ELK uses top-left
|
|
210
208
|
# Also need to account for height since DOT y goes up
|
|
211
209
|
center_x = node.x + ((node.width || 0) / 2.0)
|
|
@@ -221,9 +219,40 @@ module Elkrb
|
|
|
221
219
|
attrs[:shape] = node.properties["dot.shape"]
|
|
222
220
|
end
|
|
223
221
|
|
|
222
|
+
apply_port_label(attrs, node) if declared_ports(node).any?
|
|
223
|
+
|
|
224
224
|
attrs
|
|
225
225
|
end
|
|
226
226
|
|
|
227
|
+
def apply_port_label(attrs, node)
|
|
228
|
+
attrs[:label] = HtmlValue.new(html_port_label(node))
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def html_port_label(node)
|
|
232
|
+
labels = Array(node.labels).map(&:text)
|
|
233
|
+
labels = [node.id] if labels.empty?
|
|
234
|
+
label = labels.map { |text| html_escape(text) }.join("<BR/>")
|
|
235
|
+
ports = declared_ports(node).map do |port_id, name|
|
|
236
|
+
%(<TD PORT="#{name}" TOOLTIP="#{html_escape(port_id)}"></TD>)
|
|
237
|
+
end.join
|
|
238
|
+
"<<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\"" \
|
|
239
|
+
"#{html_table_dimensions(node)}>" \
|
|
240
|
+
"<TR><TD>#{label}</TD></TR><TR>#{ports}</TR></TABLE>>"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def html_table_dimensions(node)
|
|
244
|
+
return "" unless node.width&.positive? && node.height&.positive?
|
|
245
|
+
|
|
246
|
+
width = node.width.ceil
|
|
247
|
+
height = node.height.ceil
|
|
248
|
+
%( WIDTH="#{width}" HEIGHT="#{height}")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def html_escape(value)
|
|
252
|
+
value.to_s.gsub("&", "&").gsub("<", "<")
|
|
253
|
+
.gsub(">", ">").gsub('"', """)
|
|
254
|
+
end
|
|
255
|
+
|
|
227
256
|
# Format an edge
|
|
228
257
|
def format_edge(edge)
|
|
229
258
|
lines = []
|
|
@@ -232,11 +261,13 @@ module Elkrb
|
|
|
232
261
|
!edge.targets || edge.targets.empty?
|
|
233
262
|
|
|
234
263
|
# Get source and target
|
|
235
|
-
source_id = sanitize_id(edge.sources.first)
|
|
236
|
-
target_id = sanitize_id(edge.targets.first)
|
|
237
|
-
|
|
238
264
|
# Build edge attributes
|
|
239
265
|
attrs = build_edge_attributes(edge)
|
|
266
|
+
source = resolved_endpoint(edge.sources.first)
|
|
267
|
+
target = resolved_endpoint(edge.targets.first)
|
|
268
|
+
source_id = edge_endpoint(source)
|
|
269
|
+
target_id = edge_endpoint(target)
|
|
270
|
+
add_cluster_attributes(attrs, source, target)
|
|
240
271
|
|
|
241
272
|
# Edge operator
|
|
242
273
|
op = @options[:directed] ? "->" : "--"
|
|
@@ -252,12 +283,11 @@ module Elkrb
|
|
|
252
283
|
|
|
253
284
|
# Label
|
|
254
285
|
if edge.labels && !edge.labels.empty?
|
|
255
|
-
|
|
256
|
-
attrs[:label] = label_text
|
|
286
|
+
attrs[:label] = edge.labels.map(&:text)
|
|
257
287
|
end
|
|
258
288
|
|
|
259
289
|
# Edge routing points
|
|
260
|
-
if edge.sections && !edge.sections.empty?
|
|
290
|
+
if neato? && edge.sections && !edge.sections.empty?
|
|
261
291
|
section = edge.sections.first
|
|
262
292
|
points = []
|
|
263
293
|
|
|
@@ -282,7 +312,12 @@ module Elkrb
|
|
|
282
312
|
return "" if attrs.empty?
|
|
283
313
|
|
|
284
314
|
attr_strs = attrs.map do |key, value|
|
|
285
|
-
|
|
315
|
+
rendered = if key == :label && value.is_a?(Array)
|
|
316
|
+
quote_labels(value)
|
|
317
|
+
else
|
|
318
|
+
quote_value(value)
|
|
319
|
+
end
|
|
320
|
+
"#{key}=#{rendered}"
|
|
286
321
|
end
|
|
287
322
|
|
|
288
323
|
"[#{attr_strs.join(', ')}]"
|
|
@@ -290,28 +325,281 @@ module Elkrb
|
|
|
290
325
|
|
|
291
326
|
# Quote a value appropriately for DOT
|
|
292
327
|
def quote_value(value)
|
|
328
|
+
return value.text if value.is_a?(HtmlValue)
|
|
329
|
+
|
|
293
330
|
value_str = value.to_s
|
|
294
331
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
332
|
+
return value_str if bare_id?(value_str)
|
|
333
|
+
|
|
334
|
+
quote_string(value_str)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def quote_id(id)
|
|
338
|
+
text = id.to_s
|
|
339
|
+
bare_id?(text) ? text : quote_string(text)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def bare_id?(text)
|
|
343
|
+
identifier = text.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
344
|
+
numeral = text.match?(/\A-?(?:\.\d+|\d+(?:\.\d*)?)\z/)
|
|
345
|
+
(identifier || numeral) && !dot_keyword?(text)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def dot_keyword?(text)
|
|
349
|
+
%w[node edge graph digraph subgraph strict].include?(text.downcase)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def quote_string(text)
|
|
353
|
+
escaped = text.gsub(/[\\"]/) do |char|
|
|
354
|
+
char == "\\" ? "\\\\" : '\\"'
|
|
355
|
+
end
|
|
356
|
+
"\"#{escaped}\""
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def quote_labels(labels)
|
|
360
|
+
escaped = labels.map do |label|
|
|
361
|
+
label.to_s.gsub(/[\\"]/) do |char|
|
|
362
|
+
char == "\\" ? "\\\\" : '\\"'
|
|
363
|
+
end
|
|
302
364
|
end
|
|
365
|
+
"\"#{escaped.join('\\n')}\""
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def resolved_endpoint(id)
|
|
369
|
+
endpoint_entry(id.to_s) || { type: :node, id: id.to_s, clusters: [] }
|
|
303
370
|
end
|
|
304
371
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
sanitized = id.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
372
|
+
def edge_endpoint(endpoint)
|
|
373
|
+
if endpoint.fetch(:type) == :port
|
|
374
|
+
return "#{quote_id(endpoint[:owner])}:#{endpoint[:port]}"
|
|
375
|
+
end
|
|
310
376
|
|
|
311
|
-
|
|
312
|
-
|
|
377
|
+
if endpoint.fetch(:type) == :compound
|
|
378
|
+
return quote_id(endpoint[:representative])
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
quote_id(endpoint[:id])
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def add_cluster_attributes(attrs, source, target)
|
|
385
|
+
source_cluster = source[:cluster]
|
|
386
|
+
target_cluster = target[:cluster]
|
|
387
|
+
if source_cluster && !target.fetch(:clusters).include?(source_cluster)
|
|
388
|
+
attrs[:ltail] = source_cluster
|
|
389
|
+
end
|
|
390
|
+
if target_cluster && !source.fetch(:clusters).include?(target_cluster)
|
|
391
|
+
attrs[:lhead] = target_cluster
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def index_endpoints(graph)
|
|
396
|
+
@endpoint_scopes = {}.compare_by_identity
|
|
397
|
+
@clusters = {}.compare_by_identity
|
|
398
|
+
nodes = descendant_nodes(graph)
|
|
399
|
+
index_node_names(nodes)
|
|
400
|
+
index_clusters(nodes)
|
|
401
|
+
index_cluster_memberships(graph)
|
|
402
|
+
index_all_ports(nodes)
|
|
403
|
+
index_container(graph)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def index_cluster_memberships(graph)
|
|
407
|
+
@cluster_memberships = {}.compare_by_identity
|
|
408
|
+
index_child_memberships(Array(graph.children), [])
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def index_child_memberships(children, clusters)
|
|
412
|
+
children.each do |node|
|
|
413
|
+
own_clusters = node.hierarchical? ? [*clusters, @clusters.fetch(node)] : clusters
|
|
414
|
+
@cluster_memberships[node] = own_clusters
|
|
415
|
+
index_child_memberships(Array(node.children), own_clusters)
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def index_container(container)
|
|
420
|
+
children = Array(container.children)
|
|
421
|
+
@endpoint_scopes[container] = build_endpoint_scope(children)
|
|
422
|
+
children.each { |node| index_container(node) }
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def descendant_nodes(container)
|
|
426
|
+
Array(container.children).flat_map do |node|
|
|
427
|
+
[node, *descendant_nodes(node)]
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def index_node_names(nodes)
|
|
432
|
+
@node_names = {}.compare_by_identity
|
|
433
|
+
reserved = nodes.to_h { |node| [node.id.to_s, true] }
|
|
434
|
+
used = {}
|
|
435
|
+
nodes.each do |node|
|
|
436
|
+
name = node.id.to_s
|
|
437
|
+
name = available_node_name(name, reserved, used) if used.key?(name)
|
|
438
|
+
@node_names[node] = name
|
|
439
|
+
used[name] = true
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def available_node_name(base, reserved, used)
|
|
444
|
+
index = 2
|
|
445
|
+
loop do
|
|
446
|
+
candidate = "#{base}_#{index}"
|
|
447
|
+
return candidate unless reserved.key?(candidate) || used.key?(candidate)
|
|
448
|
+
|
|
449
|
+
index += 1
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def index_clusters(nodes)
|
|
454
|
+
nodes.select(&:hierarchical?).each_with_index do |node, index|
|
|
455
|
+
@clusters[node] = "cluster_#{index}"
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def index_all_ports(nodes)
|
|
460
|
+
@port_names = {}.compare_by_identity
|
|
461
|
+
@declared_ports = {}.compare_by_identity
|
|
462
|
+
ports_by_owner = {}.compare_by_identity
|
|
463
|
+
nodes.each do |node|
|
|
464
|
+
owner = port_representative(node)
|
|
465
|
+
next unless owner
|
|
466
|
+
|
|
467
|
+
Array(node.ports).each do |port|
|
|
468
|
+
(ports_by_owner[owner] ||= []) << [node, port]
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
ports_by_owner.each { |owner, entries| index_owner_ports(owner, entries) }
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def index_owner_ports(owner, entries)
|
|
475
|
+
reserved = entries.filter_map do |_node, port|
|
|
476
|
+
port_id = port.id.to_s
|
|
477
|
+
port_id if bare_id?(port_id)
|
|
478
|
+
end.to_h { |port_id| [port_id, true] }
|
|
479
|
+
used = {}
|
|
480
|
+
@declared_ports[owner] = entries.filter_map.with_index do |(node, port), index|
|
|
481
|
+
names = (@port_names[node] ||= {})
|
|
482
|
+
port_id = port.id.to_s
|
|
483
|
+
next if names.key?(port_id)
|
|
484
|
+
|
|
485
|
+
name = if bare_id?(port_id) && !used.key?(port_id)
|
|
486
|
+
port_id
|
|
487
|
+
else
|
|
488
|
+
available_port_name(index, reserved.merge(used))
|
|
489
|
+
end
|
|
490
|
+
names[port_id] = name
|
|
491
|
+
used[name] = true
|
|
492
|
+
[port.id, name]
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def available_port_name(index, used)
|
|
497
|
+
loop do
|
|
498
|
+
candidate = "port#{index}"
|
|
499
|
+
return candidate unless used.key?(candidate)
|
|
500
|
+
|
|
501
|
+
index += 1
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def build_endpoint_scope(children)
|
|
506
|
+
scope = children.each_with_object({}) do |node, entries|
|
|
507
|
+
entries[node.id.to_s] ||= node_endpoint(node)
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
children.each do |node|
|
|
511
|
+
add_ports_to_scope(scope, node)
|
|
512
|
+
end
|
|
513
|
+
add_descendant_scopes(scope, children)
|
|
514
|
+
|
|
515
|
+
scope
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def add_descendant_scopes(scope, children)
|
|
519
|
+
children.each { |node| add_descendants_to_scope(scope, node) }
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def add_descendants_to_scope(scope, node)
|
|
523
|
+
Array(node.children).each do |descendant|
|
|
524
|
+
scope[descendant.id.to_s] ||= node_endpoint(descendant)
|
|
525
|
+
add_ports_to_scope(scope, descendant)
|
|
526
|
+
add_descendants_to_scope(scope, descendant)
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def add_ports_to_scope(scope, node)
|
|
531
|
+
owner = port_representative(node)
|
|
532
|
+
return unless owner
|
|
533
|
+
|
|
534
|
+
Array(node.ports).each do |port|
|
|
535
|
+
scope[port.id.to_s] ||= port_endpoint(node, owner, port)
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def port_endpoint(node, owner, port)
|
|
540
|
+
{
|
|
541
|
+
type: :port, owner: @node_names.fetch(owner),
|
|
542
|
+
port: @port_names.fetch(node).fetch(port.id.to_s),
|
|
543
|
+
cluster: @clusters[node], clusters: @cluster_memberships.fetch(owner)
|
|
544
|
+
}
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def node_endpoint(node)
|
|
548
|
+
unless node.hierarchical?
|
|
549
|
+
return {
|
|
550
|
+
type: :node, id: @node_names.fetch(node),
|
|
551
|
+
clusters: @cluster_memberships.fetch(node)
|
|
552
|
+
}
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
representative = representative_node(node)
|
|
556
|
+
|
|
557
|
+
{
|
|
558
|
+
type: :compound, id: @node_names.fetch(node),
|
|
559
|
+
cluster: @clusters.fetch(node),
|
|
560
|
+
representative: @node_names.fetch(representative),
|
|
561
|
+
clusters: @cluster_memberships.fetch(representative)
|
|
562
|
+
}
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def endpoint_entry(id)
|
|
566
|
+
@scope_stack.reverse_each do |scope|
|
|
567
|
+
return scope[id] if scope.key?(id)
|
|
568
|
+
end
|
|
569
|
+
nil
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def with_endpoint_scope(container)
|
|
573
|
+
@scope_stack << @endpoint_scopes.fetch(container)
|
|
574
|
+
yield
|
|
575
|
+
ensure
|
|
576
|
+
@scope_stack.pop
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def port_representative(node)
|
|
580
|
+
node.hierarchical? ? representative_node(node) : node
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def representative_node(node)
|
|
584
|
+
Array(node.children).each do |child|
|
|
585
|
+
return child unless child.hierarchical?
|
|
586
|
+
|
|
587
|
+
representative = representative_node(child)
|
|
588
|
+
return representative if representative
|
|
589
|
+
end
|
|
590
|
+
nil
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def declared_ports(node)
|
|
594
|
+
@declared_ports.fetch(node, [])
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def option_resolver
|
|
598
|
+
@option_resolver ||= Elkrb::Options::Resolver.new
|
|
599
|
+
end
|
|
313
600
|
|
|
314
|
-
|
|
601
|
+
def neato?
|
|
602
|
+
@options[:engine].to_s == "neato"
|
|
315
603
|
end
|
|
316
604
|
|
|
317
605
|
# Convert ELK direction to DOT rankdir
|