Class: RailroadDiagrams::MultipleChoice

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

Overview

Draws a choice where one or several branches may be taken.

Examples:

MultipleChoice.new(0, 'any', 'a', 'b')

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of MultipleChoice.

Parameters:

  • default (Integer)
  • type (String)
  • 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
# File 'lib/railroad_diagrams/multiple_choice.rb', line 13

def initialize(default, type, *items, id: nil, cls: nil, attrs: {})
  super('g', items, nil, nil, id: id, cls: cls, data_attrs: attrs)
  raise InvalidArgument, "default must be between 0 and #{items.length - 1}" unless (0...items.length).cover?(default)
  raise InvalidArgument, "type must be 'any' or 'all'" unless %w[any all].include?(type)

  @default = default
  @type = type
  @needs_space = true
  @inner_width = @items.map(&:width).max
  @width = 30 + AR + @inner_width + AR + 20
  @up = @items[0].up
  @down = @items[-1].down
  @height = @items[default].height

  @items.each_with_index do |item, i|
    minimum =
      if [default - 1, default + 1].include?(i)
        10 + AR
      else
        AR
      end

    if i < default
      @up += [minimum, item.height + item.down + VS + @items[i + 1].up].max
    elsif i > default
      @down += [minimum, item.up + VS + @items[i - 1].down + @items[i - 1].height].max
    end
  end

  @down -= @items[default].height # already counted in @height
end

Instance Method Details

#child_nodes ⇒ Object

Returns:

  • (Object)


195
196
197
# File 'lib/railroad_diagrams/multiple_choice.rb', line 195

def child_nodes
  @items.dup
end

#double_enumerate(seq) ⇒ Object

Parameters:

  • seq (Object)

Returns:

  • (Object)


270
271
272
273
# File 'lib/railroad_diagrams/multiple_choice.rb', line 270

def double_enumerate(seq)
  length = seq.length
  seq.each_with_index.map { |item, i| [i, i - length, item] }
end

#format(x, y, width) ⇒ MultipleChoice

Parameters:

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

Returns:



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

def format(x, y, width)
  left_gap, right_gap = determine_gaps(width, @width)

  # Hook up the two sides if self is narrower than its stated 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

  default = @items[@default]

  # Do the elements that curve above
  above = @items[0...@default].reverse
  distance_from_y = 0
  distance_from_y = [10 + AR, default.up + VS + above.first.down + above.first.height].max if above.any?

  double_enumerate(above).each do |i, ni, item|
    Path.new(x + 30, y).up(distance_from_y - AR).arc('wn').add(self)
    item.format(x + 30 + AR, y - distance_from_y, @inner_width).add(self)
    Path.new(x + 30 + AR + @inner_width, y - distance_from_y + item.height)
        .arc('ne')
        .down(distance_from_y - item.height + default.height - AR - 10)
        .add(self)
    distance_from_y += [AR, item.up + VS + above[i + 1].down + above[i + 1].height].max if ni < -1
  end

  # Do the straight-line path.
  Path.new(x + 30, y).right(AR).add(self)
  @items[@default].format(x + 30 + AR, y, @inner_width).add(self)
  Path.new(x + 30 + AR + @inner_width, y + @height).right(AR).add(self)

  # Do the elements that curve below
  below = @items[(@default + 1)..-1] || []
  distance_from_y = [10 + AR, default.height + default.down + VS + below.first.up].max if below.any?

  below.each_with_index do |item, i|
    Path.new(x + 30, y).down(distance_from_y - AR).arc('ws').add(self)
    item.format(x + 30 + AR, y + distance_from_y, @inner_width).add(self)
    Path.new(x + 30 + AR + @inner_width, y + distance_from_y + item.height)
        .arc('se')
        .up(distance_from_y - AR + item.height - default.height - 10)
        .add(self)

    distance_from_y += [AR, item.height + item.down + VS + (below[i + 1]&.up || 0)].max
  end

  text = DiagramItem.new('g', attrs: { 'class' => 'diagram-text' }).add(self)
  DiagramItem.new(
    'title',
    text: @type == 'any' ? 'take one or more branches, once each, in any order' : 'take all branches, once each, in any order'
  ).add(text)

  DiagramItem.new(
    'path',
    attrs: {
      'd' => "M #{x + 30} #{y - 10} h -26 a 4 4 0 0 0 -4 4 v 12 a 4 4 0 0 0 4 4 h 26 z",
      'class' => 'diagram-text'
    }
  ).add(text)

  DiagramItem.new(
    'text',
    text: @type == 'any' ? '1+' : 'all',
    attrs: { 'x' => x + 15, 'y' => y + 4, 'class' => 'diagram-text' }
  ).add(text)

  DiagramItem.new(
    'path',
    attrs: {
      'd' => "M #{x + @width - 20} #{y - 10} h 16 a 4 4 0 0 1 4 4 v 12 a 4 4 0 0 1 -4 4 h -16 z",
      'class' => 'diagram-text'
    }
  ).add(text)

  DiagramItem.new(
    'text',
    text: '↺',
    attrs: { 'x' => x + @width - 10, 'y' => y + 4, 'class' => 'diagram-arrow' }
  ).add(text)

  self
end

#measure(context) ⇒ Object

Parameters:

  • context (Object)

Returns:

  • (Object)


147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/railroad_diagrams/multiple_choice.rb', line 147

def measure(context)
  items = @items.map { |item| context.metrics(item) }
  arc = context.options.arc_radius
  separation = context.options.vertical_separation
  width = 30 + arc + items.map(&:width).max + arc + 20
  up = items.first.up
  down = items.last.down
  height = items[@default].height

  items.each_with_index do |item, index|
    minimum = [@default - 1, @default + 1].include?(index) ? 10 + arc : arc
    if index < @default
      up += [minimum, item.height + item.down + separation + items[index + 1].up].max
    elsif index > @default
      down += [minimum, item.up + separation + items[index - 1].down + items[index - 1].height].max
    end
  end
  down -= items[@default].height
  Metrics.new(width: width, up: up, height: height, down: down, needs_space: true)
end

#render_svg(context, x, y, width) ⇒ Object

Parameters:

  • context (Object)
  • x (Object)
  • y (Object)
  • width (Object)

Returns:

  • (Object)


168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/railroad_diagrams/multiple_choice.rb', line 168

def render_svg(context, x, y, width)
  metrics = context.metrics(self)
  items = @items.map { |item| context.metrics(item) }
  arc = context.options.arc_radius
  inner_width = items.map(&:width).max
  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

  render_svg_above(context, group, x, y, inner_width)
  group << svg_path(Svg::PathData.new(x + 30, y).h(arc))
  group << context.render_svg(@items[@default], x + 30 + arc, y, inner_width)
  group << svg_path(Svg::PathData.new(x + 30 + arc + inner_width, y + metrics.height).h(arc))
  render_svg_below(context, group, x, y, inner_width)
  group << render_svg_annotation(context, x, y, metrics.width)
  group
end

#render_svg_above(context, group, x, y, inner_width) ⇒ Object

Parameters:

  • context (Object)
  • group (Object)
  • x (Object)
  • y (Object)
  • inner_width (Object)

Returns:

  • (Object)


205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/railroad_diagrams/multiple_choice.rb', line 205

def render_svg_above(context, group, x, y, inner_width)
  above = @items[0...@default].reverse
  return if above.empty?

  arc = context.options.arc_radius
  separation = context.options.vertical_separation
  default = context.metrics(@items[@default])
  first = context.metrics(above.first)
  distance = [10 + arc, default.up + separation + first.down + first.height].max
  double_enumerate(above).each do |index, negative_index, item|
    child = context.metrics(item)
    group << svg_path(Svg::PathData.new(x + 30, y, arc_radius: arc).v(-[0, distance - arc].max).arc('wn'))
    group << context.render_svg(item, x + 30 + arc, y - distance, inner_width)
    return_path = Svg::PathData.new(x + 30 + arc + inner_width, y - distance + child.height, arc_radius: arc)
                               .arc('ne').v([0, distance - child.height + default.height - arc - 10].max)
    group << svg_path(return_path)
    if negative_index < -1
      following = context.metrics(above[index + 1])
      distance += [arc, child.up + separation + following.down + following.height].max
    end
  end
end

#render_svg_annotation(context, x, y, width) ⇒ Object

Parameters:

  • context (Object)
  • x (Object)
  • y (Object)
  • width (Object)

Returns:

  • (Object)


248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/railroad_diagrams/multiple_choice.rb', line 248

def render_svg_annotation(context, x, y, width)
  group = Svg::Element.new('g', { 'class' => 'diagram-text' })
  key = @type == 'any' ? :multiple_choice_any_tooltip : :multiple_choice_all_tooltip
  title = I18n.t(key, locale: context.options.locale)
  group << Svg::Element.new('title', {}, [Svg::TextNode.new(title)])
  left_path = {
    'd' => "M #{x + 30} #{y - 10} h -26 a 4 4 0 0 0 -4 4 v 12 a 4 4 0 0 0 4 4 h 26 z",
    'class' => 'diagram-text'
  }
  group << Svg::Element.new('path', left_path)
  group << Svg::Element.new('text', { 'x' => x + 15, 'y' => y + 4, 'class' => 'diagram-text' },
                            [Svg::TextNode.new(@type == 'any' ? '1+' : 'all')])
  right_path = {
    'd' => "M #{x + width - 20} #{y - 10} h 16 a 4 4 0 0 1 4 4 v 12 a 4 4 0 0 1 -4 4 h -16 z",
    'class' => 'diagram-text'
  }
  group << Svg::Element.new('path', right_path)
  group << Svg::Element.new('text', { 'x' => x + width - 10, 'y' => y + 4, 'class' => 'diagram-arrow' },
                            [Svg::TextNode.new('↺')])
  group
end

#render_svg_below(context, group, x, y, inner_width) ⇒ Object

Parameters:

  • context (Object)
  • group (Object)
  • x (Object)
  • y (Object)
  • inner_width (Object)

Returns:

  • (Object)


228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/railroad_diagrams/multiple_choice.rb', line 228

def render_svg_below(context, group, x, y, inner_width)
  below = @items[(@default + 1)..-1] || []
  return if below.empty?

  arc = context.options.arc_radius
  separation = context.options.vertical_separation
  default = context.metrics(@items[@default])
  distance = [10 + arc, default.height + default.down + separation + context.metrics(below.first).up].max
  below.each_with_index do |item, index|
    child = context.metrics(item)
    group << svg_path(Svg::PathData.new(x + 30, y, arc_radius: arc).v([0, distance - arc].max).arc('ws'))
    group << context.render_svg(item, x + 30 + arc, y + distance, inner_width)
    return_path = Svg::PathData.new(x + 30 + arc + inner_width, y + distance + child.height, arc_radius: arc)
                               .arc('se').v(-[0, distance - arc + child.height - default.height - 10].max)
    group << svg_path(return_path)
    following = below[index + 1]
    distance += [arc, child.height + child.down + separation + (following ? context.metrics(following).up : 0)].max
  end
end

#render_text(context) ⇒ Object

Parameters:

  • context (Object)

Returns:

  • (Object)


188
189
190
191
192
193
# File 'lib/railroad_diagrams/multiple_choice.rb', line 188

def render_text(context)
  any_all = TextDiagram.rect(@type == 'any' ? '1+' : 'all', parts: context.parts)
  diagram = Choice.new(@default, *@items).render_text(context)
  repeat = TextDiagram.rect(context.parts.fetch('multi_repeat'), parts: context.parts)
  any_all.append_right(diagram, '').append_right(repeat, '')
end

#svg_path(data) ⇒ Object

Parameters:

  • data (Object)

Returns:

  • (Object)


201
202
203
# File 'lib/railroad_diagrams/multiple_choice.rb', line 201

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

#text_diagram ⇒ TextDiagram

Returns:

  • (TextDiagram)


138
139
140
141
142
143
144
145
# File 'lib/railroad_diagrams/multiple_choice.rb', line 138

def text_diagram
  multi_repeat = TextDiagram.get_parts(['multi_repeat']).first
  any_all = TextDiagram.rect(@type == 'any' ? '1+' : 'all')
  diagram_td = Choice.new(@default, *@items).text_diagram
  repeat_td = TextDiagram.rect(multi_repeat)
  diagram_td = any_all.append_right(diagram_td, '')
  diagram_td.append_right(repeat_td, '')
end

#to_s ⇒ String

Returns:

  • (String)


46
47
48
49
# File 'lib/railroad_diagrams/multiple_choice.rb', line 46

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