Module: Trailblazer::Diagram::BPMN

Defined in:
lib/trailblazer/diagram/bpmn.rb

Overview

rubocop:disable Metrics/ModuleLength

Defined Under Namespace

Modules: Representer Classes: Bounds, Definitions, Edge, Plane, Shape, Waypoint

Class Method Summary collapse

Class Method Details

.fromBottom(bounds) ⇒ Object



142
143
144
# File 'lib/trailblazer/diagram/bpmn.rb', line 142

def self.fromBottom(bounds)
  [bounds.x + bounds.width / 2, bounds.y + bounds.height]
end

.fromRight(left) ⇒ Object



134
135
136
# File 'lib/trailblazer/diagram/bpmn.rb', line 134

def self.fromRight(left)
  [left.x + left.width, left.y + left.height / 2]
end

.fromTop(bounds) ⇒ Object



146
147
148
# File 'lib/trailblazer/diagram/bpmn.rb', line 146

def self.fromTop(bounds)
  [bounds.x + bounds.width / 2, bounds.y]
end

.Path(source, target, do_straight_line) ⇒ Object

rubocop:disable Metrics/AbcSize



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/trailblazer/diagram/bpmn.rb', line 118

def self.Path(source, target, do_straight_line) # rubocop:disable Metrics/AbcSize
  if source.y == target.y # --->
    [Waypoint.new(*fromRight(source)), Waypoint.new(*toLeft(target))]
  elsif do_straight_line
    [Waypoint.new(*fromBottom(source)), Waypoint.new(*toLeft(target))]
  elsif target.y > source.y # target below source.
    [
      l = Waypoint.new(*fromBottom(source)),
      r = Waypoint.new(l.x, target.y + target.height / 2),
      Waypoint.new(target.x, r.y)
    ]
  else # target above source.
    [l = Waypoint.new(*fromTop(source)), r = Waypoint.new(l.x, target.y + target.height / 2), Waypoint.new(target.x, r.y)]
  end
end

.to_xml(activity, linear_task_ids = nil) ⇒ Object

FIXME: this should be called “linear layouter or something” Render an ‘Activity`’s circuit to a BPMN 2.0 XML ‘<process>` structure.

Parameters:

  • activity

    Activity

  • linear_task_ids (String) (defaults to: nil)

    A list of task IDs that should be layouted sequentially in the provided order.



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
# File 'lib/trailblazer/diagram/bpmn.rb', line 37

def self.to_xml(activity, linear_task_ids = nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
  model = Trailblazer::Developer::Activity::Graph.to_model(activity.graph)

  linear_task_ids ||= topological_sort(model)

  # this layouter doesn't want End events in the linear part, we arrange them manually.
  linear_task_ids -= model.end_events.map(&:id)
  linear_task_ids -= model.start_events.map(&:id)
  linear_tasks = linear_task_ids.collect do |id|
    model.task.find { |task| task.id == id } || raise("task #{id} is not in model!")
  end

  start_x = 200
  y_right = 200
  y_left  = 300

  event_width = 54

  shape_width  = 81
  shape_height = 54
  shape_to_shape = 45

  current = start_x
  shapes = []

  # add start.
  shapes << Shape.new(
    "Shape_#{model.start_events[0][:id]}",
    model.start_events[0][:id],
    Bounds.new(current, y_right, event_width, event_width)
  )
  current += event_width + shape_to_shape

  # add tasks.
  linear_tasks.each do |task|
    is_right = %i[pass step].include?(task.options[:created_by])

    shapes << Shape.new(
      "Shape_#{task[:id]}",
      task[:id],
      Bounds.new(current, is_right ? y_right : y_left, shape_width, shape_height)
    )
    current += shape_width + shape_to_shape
  end

  # add ends.
  horizontal_end_offset = 90

  defaults = {
    "End.success" => {y: y_right},
    "End.failure" => {y: y_left},
    "End.pass_fast" => {y: y_right - 90},
    "End.fail_fast" => {y: y_left + 90}
  }

  success_end_events = []
  failure_end_events = [] # rubocop:disable Lint/UselessAssignment

  model.end_events.each do |evt|
    id = evt[:id]
    y  = defaults[id] ? defaults[id][:y] : success_end_events.last + horizontal_end_offset

    success_end_events << y

    shapes << Shape.new("Shape_#{id}", id, Bounds.new(current, y, event_width, event_width))
  end

  edges = []
  model.sequence_flow.each do |flow|
    source = shapes.find { |shape| shape.id == "Shape_#{flow.sourceRef}" }.bounds
    target = shapes.find { |shape| shape.id == "Shape_#{flow.targetRef}" }.bounds

    edges << Edge.new("SequenceFlow_#{flow[:id]}", flow[:id], Path(source, target, target.x != current))
  end

  diagram = Struct.new(:plane).new(Plane.new(model.id, shapes, edges))

  # render XML.
  Representer::Definitions.new(Definitions.new(model, diagram)).to_xml
end

.toLeft(bounds) ⇒ Object



138
139
140
# File 'lib/trailblazer/diagram/bpmn.rb', line 138

def self.toLeft(bounds)
  [bounds.x, bounds.y + bounds.height / 2]
end

.topological_sort(model) ⇒ Object

Helps sorting the tasks in a process “topologically”, which is basically what the Sequence does for us, but this works for any kind of process. DISCUSS: should we work on the Model or Graph interface?



19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/trailblazer/diagram/bpmn.rb', line 19

def self.topological_sort(model)
  edges = {}
  model.end_events.each { |task| edges[task.id] = {} }
  model.sequence_flow.each do |edge|
    edges[edge.sourceRef] ||= []
    edges[edge.sourceRef] << edge.targetRef
  end

  # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
  each_node = ->(&b) { edges.each_key(&b) }
  each_child = ->(n, &b) { edges[n].each(&b) }
  TSort.tsort(each_node, each_child).reverse #=> [4, 2, 3, 1]
end