Class: Roby::GUI::PlanDotLayout

Inherits:
Object
  • Object
show all
Defined in:
lib/roby/gui/plan_dot_layout.rb

Overview

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

Constant Summary collapse

FLOAT_VALUE =
"\\d+(?:\\.\\d+)?(?:e[+-]\\d+)?"
DOT_TO_QT_SCALE_FACTOR_X =
1.0 / 55
DOT_TO_QT_SCALE_FACTOR_Y =
1.0 / 55

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#bounding_rectsObject (readonly)

Returns the value of attribute bounding_rects.



273
274
275
# File 'lib/roby/gui/plan_dot_layout.rb', line 273

def bounding_rects
  @bounding_rects
end

#displayObject (readonly)

Returns the value of attribute display.



273
274
275
# File 'lib/roby/gui/plan_dot_layout.rb', line 273

def display
  @display
end

#dot_inputObject (readonly)

Returns the value of attribute dot_input.



273
274
275
# File 'lib/roby/gui/plan_dot_layout.rb', line 273

def dot_input
  @dot_input
end

#object_posObject (readonly)

Returns the value of attribute object_pos.



273
274
275
# File 'lib/roby/gui/plan_dot_layout.rb', line 273

def object_pos
  @object_pos
end

#planObject (readonly)

Returns the value of attribute plan.



273
274
275
# File 'lib/roby/gui/plan_dot_layout.rb', line 273

def plan
  @plan
end

Class Method Details

.parse_dot_layout(dot_layout, options = {}) ⇒ Object



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
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/roby/gui/plan_dot_layout.rb', line 284

def self.parse_dot_layout(dot_layout, options = {})
    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 = {}
    object_pos = {}
    full_line = String.new
    dot_layout.each do |line|
        line.chomp!
        full_line << line.strip
        if line[-1] == "\\" || 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 = String.new
    end

    graph_bb = bounding_rects.delete(nil)
    unless 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 |(_, pos)|
        pos.x -= graph_bb.x
        pos.y = graph_bb.y - pos.y
    end

    [bounding_rects, object_pos]
end

Instance Method Details

#<<(string) ⇒ Object

Add a string to the resulting Dot input file



276
277
278
# File 'lib/roby/gui/plan_dot_layout.rb', line 276

def <<(string)
    dot_input << string
end

#applyObject



455
456
457
# File 'lib/roby/gui/plan_dot_layout.rb', line 455

def apply
    plan.apply_layout(bounding_rects, object_pos, display)
end

#layout(display, plan, options = {}) ⇒ Object

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



367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/roby/gui/plan_dot_layout.rb', line 367

def layout(display, plan, options = {})
    @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 = {}
    all_tasks.each do |t|
        next unless 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 unless display.displayed?(ev)

            p = positions[ev.dot_id]
            bb |= Qt::RectF.new(p, p)
        end
        t.each_event do |ev| # rubocop:disable Style/CombinableLoops
            next unless 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 = {}) {|dot_input| ... } ⇒ Object

Yields:



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/roby/gui/plan_dot_layout.rb', line 337

def run_dot(options = {})
    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, &:readlines)
    PlanDotLayout.parse_dot_layout(lines, parsing_options)
end