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.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +364 -0
  3. data/LICENSE +25 -0
  4. data/README.adoc +695 -557
  5. data/exe/elkrb +36 -2
  6. data/lib/elkrb/atomic_destination.rb +53 -0
  7. data/lib/elkrb/best_effort_write.rb +49 -0
  8. data/lib/elkrb/cli/layout_flag_options.rb +54 -0
  9. data/lib/elkrb/cli.rb +155 -82
  10. data/lib/elkrb/command_resolver.rb +186 -0
  11. data/lib/elkrb/commands/batch_command.rb +28 -5
  12. data/lib/elkrb/commands/convert_command.rb +22 -52
  13. data/lib/elkrb/commands/diagram_command.rb +234 -102
  14. data/lib/elkrb/commands/options_command.rb +87 -0
  15. data/lib/elkrb/commands/render_command.rb +6 -1
  16. data/lib/elkrb/commands/validate_command.rb +146 -91
  17. data/lib/elkrb/errors.rb +20 -1
  18. data/lib/elkrb/format_sniffer.rb +362 -0
  19. data/lib/elkrb/geometry/bezier.rb +14 -4
  20. data/lib/elkrb/geometry/point.rb +5 -6
  21. data/lib/elkrb/geometry/rectangle.rb +2 -2
  22. data/lib/elkrb/graph/deep_stringify_keys.rb +30 -0
  23. data/lib/elkrb/graph/edge.rb +89 -3
  24. data/lib/elkrb/graph/graph.rb +43 -15
  25. data/lib/elkrb/graph/label.rb +16 -2
  26. data/lib/elkrb/graph/node.rb +13 -2
  27. data/lib/elkrb/graph/node_constraints.rb +139 -8
  28. data/lib/elkrb/graph/normalize_option_keys.rb +39 -0
  29. data/lib/elkrb/graph/port.rb +29 -21
  30. data/lib/elkrb/graph/read_only_mapping.rb +21 -0
  31. data/lib/elkrb/graphviz_wrapper.rb +152 -44
  32. data/lib/elkrb/layout/algorithm_registry.rb +65 -8
  33. data/lib/elkrb/layout/algorithms/base_algorithm.rb +74 -74
  34. data/lib/elkrb/layout/algorithms/box.rb +47 -20
  35. data/lib/elkrb/layout/algorithms/disco.rb +150 -61
  36. data/lib/elkrb/layout/algorithms/fixed.rb +70 -10
  37. data/lib/elkrb/layout/algorithms/force.rb +145 -86
  38. data/lib/elkrb/layout/algorithms/layered/bk/aligner.rb +104 -0
  39. data/lib/elkrb/layout/algorithms/layered/bk/compactor.rb +87 -0
  40. data/lib/elkrb/layout/algorithms/layered/bk/view.rb +103 -0
  41. data/lib/elkrb/layout/algorithms/layered/bk_node_placer.rb +170 -0
  42. data/lib/elkrb/layout/algorithms/layered/components.rb +170 -0
  43. data/lib/elkrb/layout/algorithms/layered/crossing_minimizer.rb +306 -0
  44. data/lib/elkrb/layout/algorithms/layered/cycle_breaker.rb +118 -49
  45. data/lib/elkrb/layout/algorithms/layered/discovery_order.rb +75 -0
  46. data/lib/elkrb/layout/algorithms/layered/dummy_slot.rb +31 -0
  47. data/lib/elkrb/layout/algorithms/layered/layer_assigner.rb +188 -48
  48. data/lib/elkrb/layout/algorithms/layered/layer_constraints.rb +82 -0
  49. data/lib/elkrb/layout/algorithms/layered/layer_sweep.rb +195 -0
  50. data/lib/elkrb/layout/algorithms/layered/node_placer.rb +214 -36
  51. data/lib/elkrb/layout/algorithms/layered/orthogonal/gap_solver.rb +219 -0
  52. data/lib/elkrb/layout/algorithms/layered/orthogonal/hyper_edge_segment.rb +152 -0
  53. data/lib/elkrb/layout/algorithms/layered/orthogonal/junction_points.rb +58 -0
  54. data/lib/elkrb/layout/algorithms/layered/orthogonal/segment_order.rb +134 -0
  55. data/lib/elkrb/layout/algorithms/layered/orthogonal_router.rb +253 -0
  56. data/lib/elkrb/layout/algorithms/layered/port_order.rb +188 -0
  57. data/lib/elkrb/layout/algorithms/layered/port_spread.rb +42 -0
  58. data/lib/elkrb/layout/algorithms/layered.rb +548 -13
  59. data/lib/elkrb/layout/algorithms/libavoid.rb +951 -187
  60. data/lib/elkrb/layout/algorithms/mrtree.rb +585 -65
  61. data/lib/elkrb/layout/algorithms/radial.rb +188 -29
  62. data/lib/elkrb/layout/algorithms/random.rb +131 -17
  63. data/lib/elkrb/layout/algorithms/rectpacking.rb +28 -40
  64. data/lib/elkrb/layout/algorithms/spore_compaction.rb +57 -100
  65. data/lib/elkrb/layout/algorithms/spore_overlap.rb +78 -84
  66. data/lib/elkrb/layout/algorithms/stress.rb +391 -84
  67. data/lib/elkrb/layout/algorithms/topdown_packing.rb +95 -31
  68. data/lib/elkrb/layout/algorithms/vertiflex.rb +12 -47
  69. data/lib/elkrb/layout/constraints/alignment_constraint.rb +49 -12
  70. data/lib/elkrb/layout/constraints/base_constraint.rb +3 -15
  71. data/lib/elkrb/layout/constraints/constraint_processor.rb +9 -23
  72. data/lib/elkrb/layout/constraints/fixed_position_constraint.rb +22 -13
  73. data/lib/elkrb/layout/constraints/layer_constraint.rb +10 -33
  74. data/lib/elkrb/layout/constraints/relative_position_constraint.rb +94 -50
  75. data/lib/elkrb/layout/edge_router.rb +533 -337
  76. data/lib/elkrb/layout/hierarchical_processor.rb +19 -276
  77. data/lib/elkrb/layout/java_random.rb +59 -0
  78. data/lib/elkrb/layout/label_placer.rb +165 -157
  79. data/lib/elkrb/layout/layout_engine.rb +46 -23
  80. data/lib/elkrb/layout/node_index.rb +149 -0
  81. data/lib/elkrb/layout/polyomino/component_compactor.rb +138 -0
  82. data/lib/elkrb/layout/polyomino/footprint.rb +94 -0
  83. data/lib/elkrb/layout/polyomino/grid.rb +106 -0
  84. data/lib/elkrb/layout/polyomino/packer.rb +95 -0
  85. data/lib/elkrb/layout/polyomino/piece.rb +52 -0
  86. data/lib/elkrb/layout/port_constraint_processor.rb +131 -44
  87. data/lib/elkrb/layout/spore/body.rb +65 -0
  88. data/lib/elkrb/layout/spore/depth_first_compaction.rb +127 -0
  89. data/lib/elkrb/layout/spore/edge_cost.rb +69 -0
  90. data/lib/elkrb/layout/spore/gap.rb +76 -0
  91. data/lib/elkrb/layout/spore/geometry.rb +104 -0
  92. data/lib/elkrb/layout/spore/overlap_sweep.rb +90 -0
  93. data/lib/elkrb/layout/spore/placement.rb +57 -0
  94. data/lib/elkrb/layout/spore/rays.rb +119 -0
  95. data/lib/elkrb/layout/spore/spanning_tree.rb +53 -0
  96. data/lib/elkrb/layout/spore/triangulation.rb +159 -0
  97. data/lib/elkrb/layout_flags.rb +69 -0
  98. data/lib/elkrb/options/decimal.rb +50 -0
  99. data/lib/elkrb/options/element_options.rb +43 -0
  100. data/lib/elkrb/options/elk_padding.rb +20 -14
  101. data/lib/elkrb/options/k_vector.rb +6 -7
  102. data/lib/elkrb/options/k_vector_chain.rb +12 -20
  103. data/lib/elkrb/options/option_map.rb +85 -0
  104. data/lib/elkrb/options/registry.rb +335 -0
  105. data/lib/elkrb/options/resolver.rb +151 -0
  106. data/lib/elkrb/options/spellings.rb +62 -0
  107. data/lib/elkrb/options/unhonoured_report.rb +194 -0
  108. data/lib/elkrb/parsers/elkt/lexer.rb +309 -0
  109. data/lib/elkrb/parsers/elkt/parser.rb +424 -0
  110. data/lib/elkrb/parsers/elkt/resolver.rb +142 -0
  111. data/lib/elkrb/parsers/elkt/token.rb +40 -0
  112. data/lib/elkrb/parsers/elkt_parser.rb +32 -230
  113. data/lib/elkrb/serializers/dot_serializer.rb +342 -54
  114. data/lib/elkrb/serializers/elkt_serializer.rb +386 -155
  115. data/lib/elkrb/version.rb +1 -1
  116. data/lib/elkrb.rb +60 -61
  117. metadata +71 -49
  118. data/.rspec +0 -3
  119. data/.rubocop.yml +0 -11
  120. data/Gemfile +0 -13
  121. data/Rakefile +0 -64
  122. data/benchmarks/README.md +0 -172
  123. data/benchmarks/elkjs_benchmark.js +0 -140
  124. data/benchmarks/elkrb_benchmark.rb +0 -145
  125. data/benchmarks/fixtures/graphs.json +0 -10777
  126. data/benchmarks/generate_report.rb +0 -241
  127. data/benchmarks/generate_test_graphs.rb +0 -154
  128. data/benchmarks/results/elkrb_results.json +0 -280
  129. data/benchmarks/results/elkrb_summary.json +0 -285
  130. data/elkrb.gemspec +0 -39
  131. data/examples/dot_export_demo.rb +0 -133
  132. data/examples/hierarchical_graph.rb +0 -19
  133. data/examples/layout_constraints_demo.rb +0 -272
  134. data/examples/port_constraints_demo.rb +0 -291
  135. data/examples/self_loop_demo.rb +0 -391
  136. data/examples/simple_graph.rb +0 -50
  137. data/examples/spline_routing_demo.rb +0 -235
  138. data/lib/elkrb/graph/layout_options.rb +0 -247
  139. data/sig/elkrb/constraints.rbs +0 -114
  140. data/sig/elkrb/geometry.rbs +0 -61
  141. data/sig/elkrb/graph.rbs +0 -112
  142. data/sig/elkrb/layout.rbs +0 -107
  143. data/sig/elkrb/options.rbs +0 -81
  144. data/sig/elkrb.rbs +0 -32
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "port_order"
4
+ require_relative "port_spread"
5
+ require_relative "bk/view"
6
+ require_relative "bk/aligner"
7
+ require_relative "bk/compactor"
8
+
9
+ module Elkrb
10
+ module Layout
11
+ module Algorithms
12
+ module Layered
13
+ # Brandes-Koepf node placement along the cross axis. Four passes
14
+ # run (each sweep direction packed toward each end). By default the
15
+ # narrowest pass wins, the first of them on a tie, which is what
16
+ # ELK does when no fixed alignment is set. With `place(alignment:
17
+ # :balanced)` the passes are aligned on the narrowest and every node
18
+ # takes the mean of its two middle coordinates.
19
+ class BkNodePlacer
20
+ PASSES = [%i[left down], %i[left up],
21
+ %i[right down], %i[right up]].freeze
22
+
23
+ # @param layers [Array<Array>] nodes and DummySlots per layer
24
+ # @param port_order [PortOrder]
25
+ # @param spacing [#call] gap between two neighbours in a layer,
26
+ # given their ids
27
+ # @param size_of [#call] cross size of a layer item
28
+ # @param port_size_of [#call] cross size of a port
29
+ def initialize(layers, port_order, spacing:, size_of:,
30
+ port_size_of:)
31
+ @layers = layers
32
+ @port_order = port_order
33
+ @spacing = spacing
34
+ @sizes = layers.flatten.to_h do |item|
35
+ [item.id, size_of.call(item)]
36
+ end
37
+ @offsets = PortSpread.offsets(layers, port_order, @sizes,
38
+ port_size_of)
39
+ @marked = type_one_conflicts
40
+ end
41
+
42
+ # @param alignment [Symbol] :smallest or :balanced
43
+ # @return [Hash{String=>Float}] cross coordinate of each item,
44
+ # starting at zero
45
+ def place(alignment: :smallest)
46
+ return {} if @sizes.empty?
47
+
48
+ passes = PASSES.map { |orientation| pass(orientation) }
49
+ tops = alignment == :balanced ? balance(passes) : narrowest(passes)
50
+ low = tops.values.min
51
+ tops.transform_values { |top| top - low }
52
+ end
53
+
54
+ private
55
+
56
+ def pass(orientation)
57
+ view = Bk::View.new(@layers, @port_order, @sizes, @offsets,
58
+ orientation)
59
+ aligner = Bk::Aligner.new(view, @marked)
60
+ starts = Bk::Compactor.new(view, aligner, @spacing).place
61
+ return starts unless orientation.last == :up
62
+
63
+ starts.to_h { |id, start| [id, -(start + @sizes[id])] }
64
+ end
65
+
66
+ def narrowest(passes)
67
+ passes.min_by { |tops| width(span(tops)) }
68
+ end
69
+
70
+ def balance(passes)
71
+ aligned = align_to_narrowest(passes)
72
+ @sizes.keys.to_h { |id| [id, median(aligned.map { |t| t[id] })] }
73
+ end
74
+
75
+ # Shifts every pass so it shares the narrowest pass's start (packed
76
+ # downward) or end (packed upward).
77
+ def align_to_narrowest(passes)
78
+ spans = passes.map { |tops| span(tops) }
79
+ narrow = spans.min_by { |bounds| width(bounds) }
80
+ passes.zip(spans, PASSES).map do |tops, bounds, orientation|
81
+ delta = anchor(narrow, orientation) - anchor(bounds, orientation)
82
+ tops.transform_values { |top| top + delta }
83
+ end
84
+ end
85
+
86
+ def anchor(bounds, orientation)
87
+ orientation.last == :down ? bounds.first : bounds.last
88
+ end
89
+
90
+ def width(bounds)
91
+ bounds.last - bounds.first
92
+ end
93
+
94
+ def span(tops)
95
+ [tops.map { |_, top| top }.min,
96
+ tops.map { |id, top| top + @sizes[id] }.max]
97
+ end
98
+
99
+ def median(values)
100
+ middle = values.sort[1..2]
101
+ middle.sum / 2.0
102
+ end
103
+
104
+ # Type 1 conflicts: a segment crossing an inner segment (both ends
105
+ # dummies) must never join a block, or the long edge would bend.
106
+ def type_one_conflicts
107
+ marked = {}.compare_by_identity
108
+ @layers.each_cons(2) do |left, right|
109
+ mark_pair(left, right, marked)
110
+ end
111
+ marked
112
+ end
113
+
114
+ def mark_pair(left, right, marked)
115
+ positions = left.each_with_index.to_h { |item, i| [item.id, i] }
116
+ low = 0
117
+ scanned = 0
118
+ right.each_with_index do |item, index|
119
+ high = run_end(item, index == right.length - 1, left, positions)
120
+ next unless high
121
+
122
+ mark_run(right[scanned..index], positions, low..high, marked)
123
+ scanned = index + 1
124
+ low = high
125
+ end
126
+ end
127
+
128
+ # Where the run of nodes ending at `item` may attach in the layer
129
+ # before it; nil while the run continues.
130
+ def run_end(item, last, left, positions)
131
+ inner = inner_segment(item)
132
+ return positions.fetch(inner.from_port.item_id) if inner
133
+
134
+ left.length - 1 if last
135
+ end
136
+
137
+ def mark_run(run, positions, allowed, marked)
138
+ run.each do |item|
139
+ segments_toward_west(item).each do |segment|
140
+ above = positions.fetch(segment.from_port.item_id)
141
+ marked[segment] = true unless allowed.cover?(above) ||
142
+ inner?(segment)
143
+ end
144
+ end
145
+ end
146
+
147
+ def segments_toward_west(item)
148
+ @port_order.visual(item.id, :west).flat_map(&:segments)
149
+ end
150
+
151
+ def inner_segment(item)
152
+ return unless dummy?(item)
153
+
154
+ segments_toward_west(item).find { |segment| inner?(segment) }
155
+ end
156
+
157
+ def inner?(segment)
158
+ @dummy_ids ||= @layers.flatten.select { |i| dummy?(i) }.to_set(&:id)
159
+ @dummy_ids.include?(segment.from_port.item_id) &&
160
+ @dummy_ids.include?(segment.to_port.item_id)
161
+ end
162
+
163
+ def dummy?(item)
164
+ item.respond_to?(:dummy?) && item.dummy?
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "crossing_minimizer"
4
+
5
+ module Elkrb
6
+ module Layout
7
+ module Algorithms
8
+ module Layered
9
+ # Connected components of one hierarchy level, and the row packing
10
+ # that puts the laid-out components back side by side.
11
+ module Components
12
+ # One component after its own layout.
13
+ Part = Struct.new(:graph, :layers, :index, :port_order, :slots)
14
+
15
+ # The crossing minimizer of one component. ELK counts every node of
16
+ # the level, not the component's own, against the greedy-switch
17
+ # limit.
18
+ class Minimizer < CrossingMinimizer
19
+ def initialize(graph, layers, index, resolver, level_size)
20
+ super(graph, layers, index, resolver)
21
+ @node_count = level_size
22
+ end
23
+ end
24
+
25
+ # Splits a level into one graph per connected component: nodes in
26
+ # input order, components in order of their first node, each
27
+ # graph-owned edge with the component its endpoints are in.
28
+ class Splitter
29
+ # @param graph [Graph::Graph] the level being laid out
30
+ # @param index [NodeIndex] the level's index
31
+ def initialize(graph, index)
32
+ @graph = graph
33
+ @index = index
34
+ end
35
+
36
+ # @return [Array<Graph::Graph>]
37
+ def graphs
38
+ groups.map do |nodes|
39
+ Graph::Graph.new(
40
+ id: @graph.id, children: nodes, edges: edges_of(nodes),
41
+ layout_options: @graph.layout_options
42
+ )
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def groups
49
+ seen = {}
50
+ @graph.children.filter_map do |node|
51
+ next if seen[node.id]
52
+
53
+ members = reach(node.id, seen)
54
+ @graph.children.select { |other| members.include?(other.id) }
55
+ end
56
+ end
57
+
58
+ def reach(start, seen)
59
+ found = []
60
+ pending = [start]
61
+ until pending.empty?
62
+ id = pending.pop
63
+ next if seen[id]
64
+
65
+ seen[id] = true
66
+ found << id
67
+ pending.concat(neighbours[id])
68
+ end
69
+ found
70
+ end
71
+
72
+ def neighbours
73
+ @neighbours ||= Hash.new { |map, id| map[id] = [] }.tap do |map|
74
+ @index.edges.each do |edge|
75
+ ids = owner_ids(edge)
76
+ ids.each { |id| map[id].concat(ids) }
77
+ end
78
+ end
79
+ end
80
+
81
+ def owner_ids(edge)
82
+ @index.endpoint_owners(edge.sources + edge.targets).map(&:id)
83
+ end
84
+
85
+ def edges_of(nodes)
86
+ ids = nodes.map(&:id)
87
+ @graph.edges.to_a.select do |edge|
88
+ ids.include?(owner_ids(edge).first)
89
+ end
90
+ end
91
+ end
92
+
93
+ # Places boxes in rows the way ELK's simple row placer does: the
94
+ # smallest box first, a new row once the next box would pass the
95
+ # larger of the widest box and sqrt(total area) * aspect ratio.
96
+ class RowPacker
97
+ # The next free spot in the current row.
98
+ Cursor = Struct.new(:x, :y, :tallest) do
99
+ # This cursor, or the start of the next row when a box of
100
+ # `width` would pass the limit.
101
+ def fit(width, limit, spacing)
102
+ return self unless x + width > limit
103
+
104
+ Cursor.new(0.0, y + tallest + spacing, 0.0)
105
+ end
106
+
107
+ def corner
108
+ [x, y]
109
+ end
110
+
111
+ def past(width, height, spacing)
112
+ Cursor.new(x + width + spacing, y, [tallest, height].max)
113
+ end
114
+ end
115
+ private_constant :Cursor
116
+
117
+ # @param sizes [Array<Array(Float, Float)>] width and height of
118
+ # each box, without padding
119
+ # @param spacing [Float] gap between boxes and between rows
120
+ # @param aspect_ratio [Float]
121
+ def initialize(sizes, spacing:, aspect_ratio:)
122
+ @sizes = sizes
123
+ @spacing = spacing
124
+ @aspect_ratio = aspect_ratio
125
+ end
126
+
127
+ # @return [Array<Array(Float, Float)>] the top-left of each box,
128
+ # in the order the sizes were given
129
+ def offsets
130
+ placed = {}
131
+ cursor = Cursor.new(0.0, 0.0, 0.0)
132
+ order.each do |box|
133
+ width, height = @sizes[box]
134
+ cursor = cursor.fit(width, limit, @spacing)
135
+ placed[box] = cursor.corner
136
+ cursor = cursor.past(width, height, @spacing)
137
+ end
138
+ placed.sort.map(&:last)
139
+ end
140
+
141
+ # @param offsets [Array<Array(Float, Float)>] from #offsets
142
+ # @return [Array(Float, Float)] the packed width and height
143
+ def size(offsets)
144
+ boxes = @sizes.zip(offsets)
145
+ [[@spacing, *boxes.map { |(w, _), (x, _)| x + w }].max,
146
+ boxes.map { |(_, h), (_, y)| y + h }.max]
147
+ end
148
+
149
+ private
150
+
151
+ def order
152
+ @sizes.each_index.sort_by { |box| [@sizes[box].inject(:*), box] }
153
+ end
154
+
155
+ # ELK takes the square root in single precision.
156
+ def limit
157
+ @limit ||= row_limit
158
+ end
159
+
160
+ def row_limit
161
+ area = @sizes.sum { |width, height| width * height }
162
+ root = [Math.sqrt(area)].pack("f").unpack1("f")
163
+ [@sizes.map(&:first).max, root * @aspect_ratio].max
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../java_random"
4
+ require_relative "discovery_order"
5
+ require_relative "layer_sweep"
6
+ require_relative "port_order"
7
+
8
+ module Elkrb
9
+ module Layout
10
+ module Algorithms
11
+ module Layered
12
+ # Reorders nodes inside assigned layers with barycenter sweeps, the
13
+ # way ELK's layer sweep does: a forward sweep, then backward sweeps
14
+ # alternating while the crossing count keeps falling.
15
+ #
16
+ # A node's barycenter is the mean rank of the ports its edges meet
17
+ # in the fixed layer, so two edges leaving one node pull their
18
+ # targets apart. Each sweep also reorders the ports of the layer it
19
+ # moves; #port_order carries the result to the node placer.
20
+ class CrossingMinimizer
21
+ STRATEGY = "elk.layered.crossingMinimization.strategy"
22
+ MODEL_ORDER = "elk.layered.considerModelOrder.strategy"
23
+ # ELK only runs its greedy switch on graphs smaller than this.
24
+ GREEDY_SWITCH_NODE_LIMIT = 40
25
+ # ELK's default for elk.randomSeed.
26
+ RANDOM_SEED = 1
27
+
28
+ # @return [PortOrder] the port lists as the last sweep left them
29
+ attr_reader :port_order
30
+
31
+ def initialize(graph, layers, index, resolver)
32
+ @layers = layers
33
+ @strategy = resolver.get(STRATEGY, graph)
34
+ @preserve_input_order =
35
+ resolver.get(MODEL_ORDER, graph) == "NODES_AND_EDGES"
36
+ @input_order = input_order(graph, layers)
37
+ DiscoveryOrder.new(graph, index).sort(layers)
38
+ @port_order = PortOrder.new(layers, index)
39
+ @node_count = (graph.children || []).length
40
+ @random = JavaRandom.new(RANDOM_SEED)
41
+ end
42
+
43
+ def minimize
44
+ unless trivial?
45
+ sweep_layers unless @strategy == "NONE"
46
+ greedy_switch
47
+ end
48
+ @layers
49
+ end
50
+
51
+ private
52
+
53
+ # ELK has nothing to minimize in an empty graph or a lone node.
54
+ def trivial?
55
+ @layers.all?(&:empty?) ||
56
+ (@layers.length == 1 && @layers.first.length == 1)
57
+ end
58
+
59
+ def sweep_layers
60
+ return sweep_until_stable if @preserve_input_order
61
+
62
+ LayerSweep.new(@layers, @port_order, @random,
63
+ -> { count_crossings }).minimize
64
+ end
65
+
66
+ # ELK runs its two-sided greedy switch after whichever minimizer
67
+ # ran, NONE included: adjacent nodes swap while that lowers the
68
+ # crossings against both neighbouring layers, in the port order
69
+ # the sweep left. Its first direction is a draw from the seeded
70
+ # generator, which the sweep may already have advanced.
71
+ def greedy_switch
72
+ return if @node_count >= GREEDY_SWITCH_NODE_LIMIT
73
+
74
+ @random.next_long
75
+ forward = @random.next_bits(1) != 0
76
+ switch_until_unchanged(forward)
77
+ end
78
+
79
+ def switch_until_unchanged(forward)
80
+ loop do
81
+ first, *free = (0...@layers.length).to_a
82
+ first, *free = [first, *free].reverse unless forward
83
+ improved = switch_once?(first)
84
+ free.each { |index| improved |= switch_until_stable(index) }
85
+ forward = !forward
86
+ break unless improved
87
+ end
88
+ end
89
+
90
+ def switch_until_stable(index)
91
+ improved = false
92
+ improved = true while switch_once?(index)
93
+ improved
94
+ end
95
+
96
+ def switch_once?(index)
97
+ layer = @layers[index]
98
+ sides = switch_sides(index)
99
+ Array.new([layer.length - 1, 0].max) do |upper|
100
+ swap_if_pays?(layer, upper, sides)
101
+ end.any?
102
+ end
103
+
104
+ def swap_if_pays?(layer, upper, sides)
105
+ return false unless switch_pays?(layer[upper], layer[upper + 1],
106
+ sides)
107
+
108
+ layer[upper], layer[upper + 1] = layer[upper + 1], layer[upper]
109
+ true
110
+ end
111
+
112
+ def switch_sides(index)
113
+ { west: neighbour_ranks(index - 1, :east),
114
+ east: neighbour_ranks(index + 1, :west) }
115
+ end
116
+
117
+ def neighbour_ranks(index, side)
118
+ return {}.compare_by_identity unless index.between?(
119
+ 0, @layers.length - 1
120
+ )
121
+
122
+ port_ranks(@layers[index], side)
123
+ end
124
+
125
+ def switch_pays?(upper, lower, sides)
126
+ crossings_below(upper, lower, sides) >
127
+ crossings_below(lower, upper, sides)
128
+ end
129
+
130
+ # Crossings between the edges of `above` and `below` when `above`
131
+ # sits on top.
132
+ def crossings_below(above, below, sides)
133
+ sides.sum do |side, ranks|
134
+ far_ranks(above, side, ranks).product(
135
+ far_ranks(below, side, ranks),
136
+ ).count { |high, low| high > low }
137
+ end
138
+ end
139
+
140
+ def far_ranks(item, side, ranks)
141
+ @port_order.visual(item.id, side).flat_map(&:others)
142
+ .map { |other| ranks.fetch(other) }
143
+ end
144
+
145
+ def sweep_until_stable
146
+ forward = true
147
+ crossings = sweep_and_count(forward)
148
+ loop do
149
+ previous = crossings
150
+ forward = !forward
151
+ crossings = sweep_and_count(forward)
152
+ break unless previous > crossings && crossings.positive?
153
+ end
154
+ end
155
+
156
+ def sweep_and_count(forward)
157
+ sweep(forward)
158
+ count_crossings
159
+ end
160
+
161
+ def input_order(graph, layers)
162
+ order = (graph.children || []).each_with_index
163
+ .to_h { |node, position| [node.id, position] }
164
+ layers.flatten.select { |item| item.respond_to?(:edge_order) }
165
+ .each { |slot| order[slot.id] = order.length + slot.edge_order }
166
+ order
167
+ end
168
+
169
+ def sweep(forward)
170
+ if forward
171
+ (1...@layers.length).each { |i| reorder(i, i - 1) }
172
+ else
173
+ (@layers.length - 2).downto(0) { |i| reorder(i, i + 1) }
174
+ end
175
+ end
176
+
177
+ def reorder(layer_index, fixed_index)
178
+ forward = fixed_index < layer_index
179
+ free_side = forward ? :west : :east
180
+ ranks = port_ranks(@layers[fixed_index], forward ? :east : :west)
181
+ layer = @layers[layer_index]
182
+ sort_ports(layer, free_side, ranks)
183
+ layer.replace(sorted(layer, free_side, ranks))
184
+ end
185
+
186
+ def sort_ports(layer, side, ranks)
187
+ layer.each do |item|
188
+ @port_order.sort!(item.id, side) do |port|
189
+ port_key(port, side, ranks)
190
+ end
191
+ end
192
+ end
193
+
194
+ # Model order keeps the WEST ports of a node in the order of the
195
+ # nodes they come from; the stored WEST list runs bottom to top.
196
+ def port_key(port, side, ranks)
197
+ if @preserve_input_order && side == :west
198
+ -port.others.map { |other| @input_order.fetch(other.item_id) }.min
199
+ else
200
+ mean(port.others.map { |other| ranks.fetch(other) })
201
+ end
202
+ end
203
+
204
+ # Rank of every port on one side of a layer, counted top to bottom
205
+ # across the whole layer.
206
+ def port_ranks(layer, side)
207
+ ranks = {}.compare_by_identity
208
+ layer.each do |item|
209
+ @port_order.visual(item.id, side).each do |port|
210
+ ranks[port] = ranks.length
211
+ end
212
+ end
213
+ ranks
214
+ end
215
+
216
+ def mean(values)
217
+ values.sum.to_f / values.length
218
+ end
219
+
220
+ def sorted(layer, side, ranks)
221
+ keys = fill_unknown(layer.map do |item|
222
+ barycenter(item, side, ranks)
223
+ end)
224
+ layer.each_with_index.sort_by do |item, position|
225
+ known, value = keys[position]
226
+ [value, known ? 0 : 1, known ? tie_breaker(item) : position]
227
+ end.map(&:first)
228
+ end
229
+
230
+ def barycenter(item, side, ranks)
231
+ others = @port_order.visual(item.id, side)
232
+ .flat_map(&:others)
233
+ mean(others.map { |other| ranks.fetch(other) }) unless others.empty?
234
+ end
235
+
236
+ # A node with no edge toward the fixed layer takes the barycenter
237
+ # of the node before it, so it keeps its place.
238
+ def fill_unknown(values)
239
+ lead = values.compact.first || 0.0
240
+ last = nil
241
+ values.map do |value|
242
+ last = value if value
243
+ [!value.nil?, value || last || lead]
244
+ end
245
+ end
246
+
247
+ def tie_breaker(node)
248
+ if @preserve_input_order
249
+ @input_order.fetch(node.id)
250
+ else
251
+ node.id.to_s
252
+ end
253
+ end
254
+
255
+ def count_crossings
256
+ positions = layer_positions
257
+ visual = visual_indexes
258
+ gaps = @port_order.segments.group_by do |segment|
259
+ positions.fetch(segment.from_port.item_id).first
260
+ end
261
+ gaps.values.sum do |segments|
262
+ crossings_in(slot_pairs(segments, positions, visual))
263
+ end
264
+ end
265
+
266
+ def slot_pairs(segments, positions, visual)
267
+ segments.map do |segment|
268
+ [slot(segment.from_port, positions, visual),
269
+ slot(segment.to_port, positions, visual)]
270
+ end
271
+ end
272
+
273
+ def crossings_in(pairs)
274
+ pairs.combination(2).count do |(a1, b1), (a2, b2)|
275
+ (a1 <=> a2) * (b1 <=> b2) == -1
276
+ end
277
+ end
278
+
279
+ def slot(port, positions, visual)
280
+ [positions.fetch(port.item_id).last, visual.fetch(port)]
281
+ end
282
+
283
+ def layer_positions
284
+ @layers.each_with_index.with_object({}) do |(layer, number), map|
285
+ layer.each_with_index do |item, position|
286
+ map[item.id] = [number, position]
287
+ end
288
+ end
289
+ end
290
+
291
+ def visual_indexes
292
+ indexes = {}.compare_by_identity
293
+ @layers.flatten.each do |item|
294
+ PortOrder::SIDES.each do |side|
295
+ @port_order.visual(item.id, side).each_with_index do |port, i|
296
+ indexes[port] = i
297
+ end
298
+ end
299
+ end
300
+ indexes
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end