class Roby::GUI::PlanDotLayout

This class uses Graphviz (i.e. the “dot” tool) to compute a layout for a given plan

Constants

DOT_TO_QT_SCALE_FACTOR_X
DOT_TO_QT_SCALE_FACTOR_Y
FLOAT_VALUE

Attributes

bounding_rects[R]
display[R]
dot_input[R]
object_pos[R]
plan[R]

Public Class Methods

parse_dot_layout(dot_layout, options = Hash.new) click to toggle source
# File lib/roby/gui/plan_dot_layout.rb, line 256
def self.parse_dot_layout(dot_layout, options = Hash.new)
    options = Kernel.validate_options options,
        scale_x: DOT_TO_QT_SCALE_FACTOR_X,
        scale_y: DOT_TO_QT_SCALE_FACTOR_Y
    scale_x = options[:scale_x]
    scale_y = options[:scale_y]

    current_graph_id = nil
    bounding_rects = Hash.new
    object_pos     = Hash.new
    full_line = ""
    dot_layout.each do |line|
        line.chomp!
        full_line << line.strip
        if line[-1] == ?\\ or line[-1] == ?,
            full_line.chomp!
            next
        end

        case full_line
        when /(\w+).*\[.*pos="(#{FLOAT_VALUE}),(#{FLOAT_VALUE})"/
            object_pos[$1] = Qt::PointF.new(Float($2) * scale_x, Float($3) * scale_y)
        when /subgraph cluster_(\w+)/
            current_graph_id = $1
        when /bb="(#{FLOAT_VALUE}),(#{FLOAT_VALUE}),(#{FLOAT_VALUE}),(#{FLOAT_VALUE})"/
            bb = [$1, $2, $3, $4].map { |c| Float(c) }
            bb[0] *= scale_x
            bb[2] *= scale_x
            bb[1] *= scale_x
            bb[3] *= scale_x
            bounding_rects[current_graph_id] = Qt::RectF.new(bb[0], bb[1], bb[2] - bb[0], bb[3] - bb[1])
        end
        full_line = ""
    end

    graph_bb = bounding_rects.delete(nil)
    if !graph_bb
        raise "Graphviz failed to generate a layout for this plan"
    end
    bounding_rects.each_value do |bb|
        bb.x -= graph_bb.x
        bb.y  = graph_bb.y - bb.y - bb.height
    end
    object_pos.each do |id, pos|
        pos.x -= graph_bb.x
        pos.y = graph_bb.y - pos.y
    end

    return bounding_rects, object_pos
end

Public Instance Methods

<<(string) click to toggle source

Add a string to the resulting Dot input file

# File lib/roby/gui/plan_dot_layout.rb, line 250
def <<(string); dot_input << string end
apply() click to toggle source
# File lib/roby/gui/plan_dot_layout.rb, line 422
def apply
    plan.apply_layout(bounding_rects, object_pos, display)
end
layout(display, plan, options = Hash.new) click to toggle source

Generates a layout internal for each task, allowing to place the events according to the propagations

# File lib/roby/gui/plan_dot_layout.rb, line 337
def layout(display, plan, options = Hash.new)
    @display         = display
    options = Kernel.validate_options options,
        scale_x: DOT_TO_QT_SCALE_FACTOR_X, scale_y: DOT_TO_QT_SCALE_FACTOR_Y

    # We first layout only the tasks separately. This allows to find
    # how to layout the events within the task, and know the overall
    # task sizes
    all_tasks = Set.new
    bounding_boxes, positions = run_dot(graph_type: 'graph', layout_method: 'fdp', scale_x: 1.0 / 100, scale_y: 1.0 / 100) do
        display.plans.each do |p|
            p_tasks = p.tasks | p.finalized_tasks
            p_tasks.each do |task|
                task.to_dot_events(display, self)
            end
            all_tasks.merge(p_tasks)
            p.propagated_events.each do |_, sources, to, _|
                sources.each do |from|
                    if from.respond_to?(:task) && to.respond_to?(:task) && from.task == to.task
                        from_id, to_id = from.dot_id, to.dot_id
                        if from_id && to_id
                            self << "  #{from.dot_id} -- #{to.dot_id}\n"
                        end
                    end
                end
            end
        end
    end

    # Ignore graphviz-generated BBs, recompute from the event
    # positions and then make their positions relative
    event_positions = Hash.new
    all_tasks.each do |t|
        next if !display.displayed?(t)
        bb = Qt::RectF.new
        if p = positions[t.dot_id]
            bb |= Qt::RectF.new(p, p)
        end
        t.each_event do |ev|
            next if !display.displayed?(ev)
            p = positions[ev.dot_id]
            bb |= Qt::RectF.new(p, p)
        end
        t.each_event do |ev|
            next if !display.displayed?(ev)
            event_positions[ev.dot_id] = positions[ev.dot_id] - bb.topLeft
        end
        graphics = display.graphics[t]
        graphics.rect = Qt::RectF.new(0, 0, bb.width, bb.height)
    end
    
    @bounding_rects, @object_pos = run_dot(scale_x: 1.0 / 50, scale_y: 1.0 / 15) do
        # Finally, generate the whole plan
        plan.to_dot(display, self, 0)

        # Take the signalling into account for the layout. At this stage,
        # task events are represented by their tasks
        display.plans.each do |p|
            p.propagated_events.each do |_, sources, to, _|
                to_id =
                    if to.respond_to?(:task) then to.task.dot_id
                    else to.dot_id
                    end

                sources.each do |from|
                    from_id =
                        if from.respond_to?(:task)
                            from.task.dot_id
                        else
                            from.dot_id
                        end

                    if from_id && to_id
                        self << "  #{from.dot_id} -> #{to.dot_id}\n"
                    end
                end
            end
        end
    end
    object_pos.merge!(event_positions)

    @plan            = plan
end
run_dot(options = Hash.new) { |dot_input| ... } click to toggle source
# File lib/roby/gui/plan_dot_layout.rb, line 307
def run_dot(options = Hash.new)
    options, parsing_options = Kernel.filter_options options,
        graph_type: 'digraph', layout_method: display.layout_method

    @@index ||= 0
    @@index += 1

    # Dot input file
    @dot_input  = Tempfile.new("roby_dot")
    # Dot output file
    dot_output = Tempfile.new("roby_layout")

    dot_input << "#{options[:graph_type]} relations {\n"
    yield(dot_input)
    dot_input << "}\n"

    dot_input.flush

    # Make sure the GUI keeps being updated while dot is processing
    FileUtils.cp dot_input.path, "/tmp/dot-input-#{@@index}.dot"
    system("#{options[:layout_method]} #{dot_input.path} > #{dot_output.path}")
    FileUtils.cp dot_output.path, "/tmp/dot-output-#{@@index}.dot"

    # Load only task bounding boxes from dot, update arrows later
    lines = File.open(dot_output.path) { |io| io.readlines  }
    PlanDotLayout.parse_dot_layout(lines, parsing_options)
end