# Copyright (C) 2019 DLR
#
# All rights reserved. This program and the accompanying materials are made
# available under the terms of the 3-Clause BSD License which accompanies this
# distribution, and is available at
# https://opensource.org/licenses/BSD-3-Clause
#
# Contributors:
# Christoph Suerig <christoph.suerig@dlr.de>
# Don't connect with the Copyright comment above!
# Version 12.07.1019
import math
import time
from gaphas.solver import Variable
from rafcon.gui.models.state_machine import StateMachineModel
from rafcon.gui.singleton import state_machine_manager_model
from rafcon.gui.utils import constants
from rafcon.utils import log
logger = log.get_logger(__name__)
[docs]class StateMachineLayouter:
"""
StateMachineLayouter gets a state machine, and layouts it in a particular way.
"""
[docs] def layout_state_machine(self, state_machine, target_state, fixed_size, state_order):
"""
This function will format the state machine in a merlon like format.
:param state_machine: A state machine to layout.
:param target_state: The "root state" all content in the state will be formated, it needs to be tube like.
:param fixed_size: True if the size of the root state is fixed.
:param state_order: The order of the states in the machine.
:return: void
"""
start_time = time.time()
logger.info("Layouting state machine...")
# the state machine model.
state_machine_m = None
if state_machine.state_machine_id in state_machine_manager_model.state_machines:
state_machine_m = state_machine_manager_model.state_machines[state_machine.state_machine_id]
else:
state_machine_m = StateMachineModel(state_machine)
target_state_m = state_machine_m.get_state_model_by_path(target_state.get_path())
# number of rows of states.
num_states = len(target_state_m.states)
row_count = 0
column_count = 0
x_gap = 25 # a gap between the state columns
y_gap = 25 # a gap between the state rows _ _
# the sm will be layouted column by column, in merlon shape. Like: |_| |_| |_|
# the width of a state in the sm
state_width = 100.
# the height of a state in the sm
state_height = 100.
# format root state
canvas_height = 0
canvas_width = 0
r_width = 0
r_height = 0
border_size = 0
if fixed_size:
r_width, r_height = target_state_m.meta['gui']['editor_gaphas']['size']
border_size = Variable(min(r_width, r_height) / constants.BORDER_WIDTH_STATE_SIZE_FACTOR)
canvas_height = r_height - 2 * border_size
canvas_width = r_width - 2 * border_size
row_count = self.__get_num_rows(num_states, canvas_width, canvas_height)
column_count = math.ceil(num_states / row_count)
state_width, state_height, x_gap, y_gap = self.__get_state_dimensions(canvas_width, canvas_height,
column_count + 1, row_count)
else:
row_count = self.__get_num_rows(num_states)
column_count = math.ceil(num_states / row_count)
canvas_width = (column_count + 1) * (x_gap + state_width) + x_gap
canvas_height = row_count * (y_gap + state_height)
# root state width, height, and root state border size.
r_width, r_height, border_size = self.__get_target_state_dimensions(canvas_width, canvas_height)
logger.debug("Root state size: height: {} width: {}".format(r_height, r_width))
# set root state size
target_state_m.meta['gui']['editor_gaphas']['size'] = (r_width, r_height)
# set root state in / out come position
target_state_m.income.set_meta_data_editor('rel_pos', (0., border_size + y_gap + state_height / 4.))
out_come = [oc for oc in target_state_m.outcomes if oc.outcome.outcome_id == 0].pop()
out_come.meta['gui']['editor_gaphas']['rel_pos'] = (r_width, border_size + y_gap + state_height / 4.)
# positions where an income or an outcome can occure
up_pos = (state_width / 2., 0.)
down_pos = (state_width / 2., state_height)
left_pos = (0., state_height / 4.)
right_pos = (state_width, state_height / 4.)
current_row = 0
current_column = 0
# increment_row is true if formatting digs down a row, and false if it climbs the next row up again.
increment_row = True
# format states
for c_state_id in state_order: # state_machine_m.root_state.states.values():
# gui model of state
state_m = target_state_m.states[c_state_id]
# decide position of income and outcome
income_pos = up_pos if increment_row else down_pos
outcome_pos = down_pos if increment_row else up_pos
# special cases e.g. the corners of the merlon structure.
if current_row == 0 and current_row + 1 >= row_count: # special case, if row_count = 1
income_pos = left_pos
outcome_pos = right_pos
elif current_row == 0 and increment_row: # upper left corner
income_pos = left_pos
outcome_pos = down_pos
elif current_row == 0 and not increment_row: # upper right corner
income_pos = down_pos
outcome_pos = right_pos
elif current_row + 1 >= row_count and increment_row: # lower left corner
income_pos = up_pos
outcome_pos = right_pos
elif current_row + 1 >= row_count and not increment_row: # lower right corner
income_pos = left_pos
outcome_pos = up_pos
# set state size
state_m.meta['gui']['editor_gaphas']['size'] = (state_width, state_height)
# set position of income and outcome
state_m.income.set_meta_data_editor('rel_pos', income_pos)
out_come = [oc for oc in state_m.outcomes if oc.outcome.outcome_id >= 0].pop()
out_come.meta['gui']['editor_gaphas']['rel_pos'] = outcome_pos
# set position of state
current_x = current_column * (x_gap + state_width) + x_gap + border_size
current_y = current_row * (y_gap + state_height) + y_gap + border_size
state_m.meta['gui']['editor_gaphas']['rel_pos'] = (current_x, current_y)
# logger.debug("x: {} y: {}".format(current_x, current_y))
# loop trailer, in / decrement rhow and column counter, decide if to increment row next.
if current_row <= 0 and not increment_row:
increment_row = True
current_column += 1
elif current_row + 1 >= row_count and increment_row:
increment_row = False
current_column += 1
else:
current_row = current_row + 1 if increment_row else current_row - 1
# last state is a special case, its outcome should always be right.
out_come = [oc for oc in target_state_m.states[state_order[-1]].outcomes if oc.outcome.outcome_id == 0].pop()
out_come.meta['gui']['editor_gaphas']['rel_pos'] = right_pos
# store the meta data.
if state_machine.file_system_path:
state_machine_m.store_meta_data() # TODO find solution, if state machine was never saved bevore.
logger.info("State machine layouting took {0:.4f} seconds.".format(time.time() - start_time))
def __get_num_rows(self, num_states, width=16., height=9.):
"""
Get num rows, receives the number of states, a width and a height. it uses the width and the height to calculate
a ratio, to be able to calculate the number of rows to use the given space optimal.
:param num_states: The number of the states used.
:param width: The width of the available space. if this or height <= 0 automatically set to 16.
:param height: The height of the available space if this or heigt <= 0 automatically set to 9.
:return: double: The number of rows optimal in the state machine.
"""
if width <= 0 or height <= 0:
width = 16.
height = 9.
ratio = round(width / height, 2)
logger.debug('Width / Height Ratio: {}:1'.format(ratio))
row_count = math.sqrt(
num_states / ratio) # claculates the hight for approximatly ratio of 16:9, which is appr. 1.78:1
row_count = round(row_count) if row_count > 1 else 1
return row_count
def __get_target_state_dimensions(self, canvas_width, canvas_height):
"""
get_target_state_dimensions receives a desired canvas width and height, and returns the overall rootstate size,
and the border width.
:param canvas_width: The width of the canvas.
:param canvas_height: The height of the canvas.
:return: (double, double, double): (width, height, border_width).
"""
border_width = Variable(min(canvas_width, canvas_height) / constants.BORDER_WIDTH_STATE_SIZE_FACTOR)
r_width = canvas_width + 2 * border_width
r_height = canvas_height + 2 * border_width
border_width = Variable(min(r_width, r_height) / constants.BORDER_WIDTH_STATE_SIZE_FACTOR)
while (r_width - 2 * border_width) < canvas_width and (r_height - 2 * border_width) < canvas_height:
r_width = r_width + 2 * border_width
r_height = r_height + 2 * border_width
border_width = Variable(min(r_width, r_height) / constants.BORDER_WIDTH_STATE_SIZE_FACTOR)
return (r_width, r_height, border_width)
def __get_state_dimensions(self, canvas_width, canvas_height, col_count, row_count):
"""
get_state_dimensions reveives a fixed canvas width and height, a col and a row count and calcualtes a possible
state size. adds an additional x_gap.
:param canvas_width:
:param canvas_height:
:return: (double,double,double,double). (state_width, state_height, x_gap, y_gap)
"""
num_xgap = 1 + col_count
num_ygap = 1 + row_count
state_width = max(canvas_width / (0.25 * num_xgap + col_count), 1)
state_height = max(canvas_height / (0.25 * num_ygap + row_count), 1)
state_width = min(state_width, state_height)
state_height = min(state_width, state_height)
x_gap = 0.25 * state_width
y_gap = 0.25 * state_height
return state_width, state_height, x_gap, y_gap