Class: RailroadDiagrams::Choice

Inherits:
DiagramMultiContainer
  • Object
show all
Defined in:
lib/railroad_diagrams/choice.rb,
sig/generated/railroad_diagrams/choice.rbs

Overview

rubocop:disable-next Metrics/ClassLength Draws alternative paths, with one default path.

Examples:

Choice.new(0, 'yes', 'no')

Direct Known Subclasses

Optional

Instance Method Summary collapse

Constructor Details

#initialize(default, *items, id: nil, cls: nil, attrs: {}) ⇒ Choice

Returns a new instance of Choice.

Parameters:

  • default (Integer)
  • items (DiagramItem, String)
  • id: (Object) (defaults to: nil)
  • cls: (Object) (defaults to: nil)
  • attrs: (Object) (defaults to: {})


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
# File 'lib/railroad_diagrams/choice.rb', line 13

def initialize(default, *items, id: nil, cls: nil, attrs: {})
  super('g', items, nil, nil, id: id, cls: cls, data_attrs: attrs)
  unless default.is_a?(Integer) && (0...items.size).cover?(default)
    raise InvalidArgument, "default index out of range: #{default.inspect} (0...#{items.size})"
  end

  @default = default
  @width = (AR * 4) + @items.map(&:width).max

  # The calcs are non-trivial and need to be done both here
  # and in .format(), so no reason to do it twice.
  @separators = Array.new(items.size - 1, VS)

  # If the entry or exit lines would be too close together
  # to accommodate the arcs,
  # bump up the vertical separation to compensate.
  @up = 0
  (default - 1).downto(0) do |i|
    arcs =
      if i == default - 1
        AR * 2
      else
        AR
      end

    item = @items[i]
    lower_item = @items[i + 1]

    entry_delta = lower_item.up + VS + item.down + item.height
    exit_delta = lower_item.height + lower_item.up + VS + item.down

    separator = VS
    separator += [arcs - entry_delta, arcs - exit_delta].max if entry_delta < arcs || exit_delta < arcs
    @separators[i] = separator

    @up += lower_item.up + separator + item.down + item.height
  end
  @up += @items[0].up

  @height = @items[default].height
  (default + 1...@items.size).each do |i|
    arcs =
      if i == default + 1
        AR * 2
      else
        AR
      end

    item = @items[i]
    upper_item = @items[i - 1]

    entry_delta = upper_item.height + upper_item.down + VS + item.up
    exit_delta = upper_item.down + VS + item.up + item.height

    separator = VS
    separator += [arcs - entry_delta, arcs - exit_delta].max if entry_delta < arcs || exit_delta < arcs
    @separators[i - 1] = separator

    @down += upper_item.down + separator + item.up + item.height
  end
  @down += @items[-1].down
  @needs_space = false
end

Instance Method Details

#add_downward_path(x, y, distance) ⇒ void

This method returns an undefined value.

Parameters:

  • x (Numeric)
  • y (Numeric)
  • distance (Numeric)


436
437
438
439
440
441
442
# File 'lib/railroad_diagrams/choice.rb', line 436

def add_downward_path(x, y, distance)
  Path.new(x, y)
      .arc('ne')
      .down(distance - (AR * 2))
      .arc('ws')
      .add(self)
end

#add_downward_return_path(x, y, distance, item, default) ⇒ void

This method returns an undefined value.

Parameters:



424
425
426
427
428
429
430
# File 'lib/railroad_diagrams/choice.rb', line 424

def add_downward_return_path(x, y, distance, item, default)
  Path.new(x, y - distance + item.height)
      .arc('ne')
      .down(distance - item.height + default.height - (AR * 2))
      .arc('ws')
      .add(self)
end

#add_upward_path(x, y, distance) ⇒ void

This method returns an undefined value.

Parameters:

  • x (Numeric)
  • y (Numeric)
  • distance (Numeric)


410
411
412
413
414
415
416
# File 'lib/railroad_diagrams/choice.rb', line 410

def add_upward_path(x, y, distance)
  Path.new(x, y)
      .arc('se')
      .up(distance - (AR * 2))
      .arc('wn')
      .add(self)
end

#add_upward_return_path(x, y, distance, item, default) ⇒ void

This method returns an undefined value.

Parameters:



450
451
452
453
454
455
456
# File 'lib/railroad_diagrams/choice.rb', line 450

def add_upward_return_path(x, y, distance, item, default)
  Path.new(x, y + distance + item.height)
      .arc('se')
      .up(distance - (AR * 2) + item.height - default.height)
      .arc('wn')
      .add(self)
end

#child_nodes ⇒ Array[DiagramItem]

Returns:



294
295
296
# File 'lib/railroad_diagrams/choice.rb', line 294

def child_nodes
  @items.dup
end

#choice_layout(context) ⇒ Object

Parameters:

  • context (Object)

Returns:

  • (Object)


300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/railroad_diagrams/choice.rb', line 300

def choice_layout(context)
  children = @items.map { |item| context.metrics(item) }
  arc = context.options.arc_radius
  separation = context.options.vertical_separation
  width = (arc * 4) + children.map(&:width).max
  separators = Array.new(children.size - 1, separation)
  up = 0
  (@default - 1).downto(0) do |index|
    arcs = index == @default - 1 ? arc * 2 : arc
    item = children[index]
    lower = children[index + 1]
    entry_delta = lower.up + separation + item.down + item.height
    exit_delta = lower.height + lower.up + separation + item.down
    separator = separation
    separator += [arcs - entry_delta, arcs - exit_delta].max if entry_delta < arcs || exit_delta < arcs
    separators[index] = separator
    up += lower.up + separator + item.down + item.height
  end
  up += children[0].up

  height = children[@default].height
  down = 0
  (@default + 1...children.size).each do |index|
    arcs = index == @default + 1 ? arc * 2 : arc
    item = children[index]
    upper = children[index - 1]
    entry_delta = upper.height + upper.down + separation + item.up
    exit_delta = upper.down + separation + item.up + item.height
    separator = separation
    separator += [arcs - entry_delta, arcs - exit_delta].max if entry_delta < arcs || exit_delta < arcs
    separators[index - 1] = separator
    down += upper.down + separator + item.up + item.height
  end
  down += children[-1].down
  [Metrics.new(width: width, up: up, height: height, down: down, needs_space: false), separators]
end

#expand_text(diagram, left, right, line) ⇒ Object

Parameters:

  • diagram (Object)
  • left (Object)
  • right (Object)
  • line (Object)

Returns:

  • (Object)


341
342
343
344
345
346
347
# File 'lib/railroad_diagrams/choice.rb', line 341

def expand_text(diagram, left, right, line)
  rows = diagram.lines.each_with_index.map do |text, index|
    "#{index == diagram.entry ? line * left : ' ' * left}#{text}" \
      "#{index == diagram.exit ? line * right : ' ' * right}"
  end
  TextDiagram.new(diagram.entry, diagram.exit, rows)
end

#format(x, y, width) ⇒ Choice

Parameters:

  • x (Numeric)
  • y (Numeric)
  • width (Numeric)

Returns:



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/railroad_diagrams/choice.rb', line 87

def format(x, y, width)
  left_gap, right_gap = determine_gaps(width, @width)
  Path.new(x, y).h(left_gap).add(self)
  Path.new(x + left_gap + @width, y + @height).h(right_gap).add(self)
  x += left_gap

  inner_width = @width - (AR * 4)
  default = @items[@default]

  format_items_above_default(x, y, inner_width, default)
  format_default_item(x, y, inner_width)
  format_items_below_default(x, y, inner_width, default)

  self
end

#format_default_item(x, y, inner_width) ⇒ void

This method returns an undefined value.

Parameters:

  • x (Numeric)
  • y (Numeric)
  • inner_width (Numeric)


382
383
384
385
386
# File 'lib/railroad_diagrams/choice.rb', line 382

def format_default_item(x, y, inner_width)
  Path.new(x, y).right(AR * 2).add(self)
  @items[@default].format(x + (AR * 2), y, inner_width).add(self)
  Path.new(x + (AR * 2) + inner_width, y + @height).right(AR * 2).add(self)
end

#format_items_above_default(x, y, inner_width, default) ⇒ void

This method returns an undefined value.

Parameters:

  • x (Numeric)
  • y (Numeric)
  • inner_width (Numeric)
  • default (DiagramItem)


365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/railroad_diagrams/choice.rb', line 365

def format_items_above_default(x, y, inner_width, default)
  distance_from_y = 0
  (@default - 1).downto(0) do |i|
    item = @items[i]
    lower_item = @items[i + 1]
    distance_from_y += lower_item.up + @separators[i] + item.down + item.height

    add_upward_path(x, y, distance_from_y)
    item.format(x + (AR * 2), y - distance_from_y, inner_width).add(self)
    add_downward_return_path(x + (AR * 2) + inner_width, y, distance_from_y, item, default)
  end
end

#format_items_below_default(x, y, inner_width, default) ⇒ void

This method returns an undefined value.

Parameters:

  • x (Numeric)
  • y (Numeric)
  • inner_width (Numeric)
  • default (DiagramItem)


393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/railroad_diagrams/choice.rb', line 393

def format_items_below_default(x, y, inner_width, default)
  distance_from_y = 0
  (@default + 1...@items.size).each do |i|
    item = @items[i]
    upper_item = @items[i - 1]
    distance_from_y += upper_item.height + upper_item.down + @separators[i - 1] + item.up

    add_downward_path(x, y, distance_from_y)
    item.format(x + (AR * 2), y + distance_from_y, inner_width).add(self)
    add_upward_return_path(x + (AR * 2) + inner_width, y, distance_from_y, item, default)
  end
end

#measure(context) ⇒ Metrics

Parameters:

  • context (Context)

Returns:

  • (Metrics)


178
179
180
# File 'lib/railroad_diagrams/choice.rb', line 178

def measure(context)
  choice_layout(context).first
end

#render_svg(context, x, y, width) ⇒ Svg::Element

Parameters:

  • context (Context)
  • x (Numeric)
  • y (Numeric)
  • width (Numeric)

Returns:

  • (Svg::Element)


187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/railroad_diagrams/choice.rb', line 187

def render_svg(context, x, y, width)
  metrics = context.metrics(self)
  separators = choice_layout(context).last
  arc = context.options.arc_radius
  left_gap, right_gap = context.gaps(width, metrics.width)
  group = Svg::Element.new('g', @attrs.dup)
  group << svg_path(Svg::PathData.new(x, y).h(left_gap))
  group << svg_path(Svg::PathData.new(x + left_gap + metrics.width, y + metrics.height).h(right_gap))
  x += left_gap
  inner_width = metrics.width - (arc * 4)
  default = context.metrics(@items[@default])

  distance = 0
  (@default - 1).downto(0) do |index|
    item = @items[index]
    child = context.metrics(item)
    lower = context.metrics(@items[index + 1])
    distance += lower.up + separators[index] + child.down + child.height
    path = Svg::PathData.new(x, y, arc_radius: arc).arc('se').v(-[0, distance - (arc * 2)].max).arc('wn')
    group << svg_path(path)
    group << context.render_svg(item, x + (arc * 2), y - distance, inner_width)
    path = Svg::PathData.new(x + (arc * 2) + inner_width, y - distance + child.height,
                             arc_radius: arc).arc('ne')
    path.v([0, distance - child.height + default.height - (arc * 2)].max).arc('ws')
    group << svg_path(path)
  end

  group << svg_path(Svg::PathData.new(x, y).h([0, arc * 2].max))
  group << context.render_svg(@items[@default], x + (arc * 2), y, inner_width)
  group << svg_path(Svg::PathData.new(x + (arc * 2) + inner_width, y + metrics.height).h([0, arc * 2].max))

  distance = 0
  (@default + 1...@items.size).each do |index|
    item = @items[index]
    child = context.metrics(item)
    upper = context.metrics(@items[index - 1])
    distance += upper.height + upper.down + separators[index - 1] + child.up
    path = Svg::PathData.new(x, y, arc_radius: arc).arc('ne').v([0, distance - (arc * 2)].max).arc('ws')
    group << svg_path(path)
    group << context.render_svg(item, x + (arc * 2), y + distance, inner_width)
    path = Svg::PathData.new(x + (arc * 2) + inner_width, y + distance + child.height,
                             arc_radius: arc).arc('se')
    path.v(-[0, distance - (arc * 2) + child.height - default.height].max).arc('wn')
    group << svg_path(path)
  end
  group
end

#render_text(context) ⇒ TextDiagram

Parameters:

  • context (Context)

Returns:

  • (TextDiagram)


237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/railroad_diagrams/choice.rb', line 237

def render_text(context)
  cross, line, vertical, bottom_left, bottom_right, top_left, top_right =
    context.parts.values_at('cross', 'line', 'line_vertical', 'roundcorner_bot_left',
                            'roundcorner_bot_right', 'roundcorner_top_left', 'roundcorner_top_right')
  items = @items.map { |item| expand_text(item.render_text(context), 1, 1, line) }
  max_width = items.map(&:width).max
  diagram = TextDiagram.new(0, 0, [])
  items.each_with_index do |item, index|
    left_pad, right_pad = text_gaps(context, max_width, item.width)
    item = expand_text(item, left_pad, right_pad, line)
    left_lines = [vertical] * item.height
    right_lines = [vertical] * item.height
    separator = index.positive?
    move_entry = false
    move_exit = false
    if index <= @default
      left_lines[item.entry] = top_left
      right_lines[item.exit] = top_right
      if index.zero?
        (0...item.entry).each { |row| left_lines[row] = ' ' }
        (0...item.exit).each { |row| right_lines[row] = ' ' }
      end
    end
    if index >= @default
      left_lines[item.entry] = bottom_left
      right_lines[item.exit] = bottom_right
      if index == @items.size - 1
        (item.entry + 1...item.height).each { |row| left_lines[row] = ' ' }
        (item.exit + 1...item.height).each { |row| right_lines[row] = ' ' }
      end
    end
    if index == @default
      left_lines[item.entry] = cross
      right_lines[item.exit] = cross
      move_entry = true
      move_exit = true
      if index.zero? && index == @items.size - 1
        left_lines[item.entry] = line
        right_lines[item.exit] = line
      elsif index.zero?
        left_lines[item.entry] = top_right
        right_lines[item.exit] = top_left
      elsif index == @items.size - 1
        left_lines[item.entry] = bottom_right
        right_lines[item.exit] = bottom_left
      end
    end
    left_join = TextDiagram.new(item.entry, item.entry, left_lines)
    right_join = TextDiagram.new(item.exit, item.exit, right_lines)
    item = left_join.append_right(item, '').append_right(right_join, '')
    between = separator ? [vertical + (' ' * (TextDiagram.max_width(diagram, item) - 2)) + vertical] : []
    diagram = diagram.append_below(item, between, move_entry: move_entry, move_exit: move_exit)
  end
  diagram
end

#svg_path(data) ⇒ Object

Parameters:

  • data (Object)

Returns:

  • (Object)


337
338
339
# File 'lib/railroad_diagrams/choice.rb', line 337

def svg_path(data)
  Svg::Element.new('path', { 'd' => data }, self_closing: true)
end

#text_diagram ⇒ TextDiagram

Returns:

  • (TextDiagram)


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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/railroad_diagrams/choice.rb', line 104

def text_diagram
  cross, line, line_vertical, roundcorner_bot_left, roundcorner_bot_right, roundcorner_top_left, roundcorner_top_right =
    TextDiagram.get_parts(
      %w[
        cross line line_vertical roundcorner_bot_left roundcorner_bot_right roundcorner_top_left roundcorner_top_right
      ]
    )

  item_tds = @items.map { |item| item.text_diagram.expand(1, 1, 0, 0) }
  max_item_width = item_tds.map(&:width).max
  diagram_td = TextDiagram.new(0, 0, [])
  item_tds.each_with_index do |item_td, i|
    left_pad, right_pad = TextDiagram.gaps(max_item_width, item_td.width)
    item_td = item_td.expand(left_pad, right_pad, 0, 0)
    has_separator = true
    left_lines = [line_vertical] * item_td.height
    right_lines = [line_vertical] * item_td.height
    move_entry = false
    move_exit = false
    if i <= @default
      left_lines[item_td.entry] = roundcorner_top_left
      right_lines[item_td.exit] = roundcorner_top_right
      if i.zero?
        has_separator = false
        (0...item_td.entry).each { |j| left_lines[j] = ' ' }
        (0...item_td.exit).each { |j| right_lines[j] = ' ' }
      end
    end
    if i >= @default
      left_lines[item_td.entry] = roundcorner_bot_left
      right_lines[item_td.exit] = roundcorner_bot_right
      if i.zero?
        has_separator = false
      end
      if i == @items.size - 1
        (item_td.entry + 1...item_td.height).each { |j| left_lines[j] = ' ' }
        (item_td.exit + 1...item_td.height).each { |j| right_lines[j] = ' ' }
      end
    end
    if i == @default
      left_lines[item_td.entry] = cross
      right_lines[item_td.exit] = cross
      move_entry = true
      move_exit = true
      if i.zero? && i == @items.size - 1
        left_lines[item_td.entry] = line
        right_lines[item_td.exit] = line
      elsif i.zero?
        left_lines[item_td.entry] = roundcorner_top_right
        right_lines[item_td.exit] = roundcorner_top_left
      elsif i == @items.size - 1
        left_lines[item_td.entry] = roundcorner_bot_right
        right_lines[item_td.exit] = roundcorner_bot_left
      end
    end
    left_join_td = TextDiagram.new(item_td.entry, item_td.entry, left_lines)
    right_join_td = TextDiagram.new(item_td.exit, item_td.exit, right_lines)
    item_td = left_join_td.append_right(item_td, '').append_right(right_join_td, '')
    separator =
      if has_separator
        [
          line_vertical +
            (' ' * (TextDiagram.max_width(diagram_td, item_td) - 2)) + line_vertical
        ]
      else
        []
      end
    diagram_td = diagram_td.append_below(item_td, separator, move_entry: move_entry, move_exit: move_exit)
  end
  diagram_td
end

#text_gaps(context, outer, inner) ⇒ Object

Parameters:

  • context (Object)
  • outer (Object)
  • inner (Object)

Returns:

  • (Object)


349
350
351
352
353
354
355
356
357
358
# File 'lib/railroad_diagrams/choice.rb', line 349

def text_gaps(context, outer, inner)
  difference = outer - inner
  case context.options.internal_alignment
  when :left then [0, difference]
  when :right then [difference, 0]
  else
    left = difference / 2
    [left, difference - left]
  end
end

#to_s ⇒ String

Returns:

  • (String)


78
79
80
81
# File 'lib/railroad_diagrams/choice.rb', line 78

def to_s
  items_str = @items.map(&:to_s).join(', ')
  "Choice(#{@default}, #{items_str})"
end