module Trailblazer::Diagram::BPMN

Constants

Bounds
Definitions
Edge
Plane
Shape
Waypoint

Public Class Methods

Path(source, target, do_straight_line) click to toggle source
# 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
fromBottom(bounds) click to toggle source
# File lib/trailblazer/diagram/bpmn.rb, line 142
def self.fromBottom(bounds)
  [bounds.x + bounds.width / 2, bounds.y + bounds.height]
end
fromRight(left) click to toggle source
# File lib/trailblazer/diagram/bpmn.rb, line 134
def self.fromRight(left)
  [left.x + left.width, left.y + left.height / 2]
end
fromTop(bounds) click to toggle source
# File lib/trailblazer/diagram/bpmn.rb, line 146
def self.fromTop(bounds)
  [bounds.x + bounds.width / 2, bounds.y]
end
toLeft(bounds) click to toggle source
# File lib/trailblazer/diagram/bpmn.rb, line 138
def self.toLeft(bounds)
  [bounds.x, bounds.y + bounds.height / 2]
end
to_xml(activity, linear_task_ids = nil) click to toggle source

FIXME: this should be called “linear layouter or something” Render an `Activity`'s circuit to a BPMN 2.0 XML `<process>` structure. @param activity Activity @param linear_task_ids [String] A list of task IDs that should be layouted sequentially in the provided order.

# 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
topological_sort(model) click to toggle source

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?

# 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