"""Implementation of a controller.
"""
# pylint: disable=maybe-no-member
from contextlib import contextmanager
from numpy.core.fromnumeric import shape
import wx
import traceback
from typing import Any, Collection, List, Optional, Set
import rkviewer.iodine as iod
import logging
from rkviewer.iodine import Color, getReactionModifiers
from .utils import gchain, rgba_to_wx_colour
from .events import DidAddCompartmentEvent, DidAddNodeEvent, DidAddReactionEvent, DidChangeCompartmentOfNodesEvent, DidCommitDragEvent, DidRedoEvent, DidUndoEvent, DidNewNetworkEvent, post_event
from .canvas.data import Compartment, Node, Reaction, CompositeShape
from .canvas.geometry import Vec2
from .canvas.utils import get_nodes_by_ident, get_nodes_by_idx
from .mvc import IController, IView, ModelError, ModifierTipStyle
[docs]def iod_setter(controller_iod_setter):
"""Decorator for controller iod_setter methods that catches Errors and auto updates views."""
# If programmatic is True, then do not trigger a C-Event
def ret(self, *args):
retval = controller_iod_setter(self, *args)
if self.group_depth == 0:
self._update_view()
return retval
return ret
[docs]class Controller(IController):
"""A controller class.
This is not strictly adhering to the MVC architecture, since there is not a separate Model
interface. Rather, this controller directly interacts with iodine. The model class should
be implemented if necessary.
"""
view: IView
def __init__(self, view: IView):
self.view = view
iod.reset()
iod.newNetwork('the one')
self.stacklen = 0 # TODO temporary hack to not undo the first newNetwork() operation.
self.group_depth = 0
self.initial_position = wx.Point (0,0)
[docs] @contextmanager
def group_action(self):
try:
self._start_group()
yield
finally:
self._end_group()
def _start_group(self) -> bool:
self.group_depth += 1
# already in a group before; don't start startGroup()
if self.group_depth > 1:
return False
iod.startGroup()
return True
def _end_group(self) -> bool:
assert self.group_depth > 0
self.group_depth -= 1
# still in a group; don't call endGroup()
if self.group_depth > 0:
return False
iod.endGroup()
self._update_view()
return True
[docs] def in_group(self) -> bool:
return self.group_depth > 0
[docs] def undo(self) -> bool:
if self.stacklen == 0:
return False
try:
assert self.group_depth == 0
iod.undo()
post_event(DidUndoEvent())
except iod.StackEmptyError:
logging.getLogger('controller').info('Undo stack is empty')
return False
self.stacklen -= 2 # -2 to correct the +1 in update_view
self._update_view()
return True
[docs] def redo(self) -> bool:
try:
assert self.group_depth == 0
iod.redo()
post_event(DidRedoEvent())
except iod.StackEmptyError:
logging.getLogger('controller').info('Redo stack is empty')
return False
self._update_view()
return True
[docs] @iod_setter
def clear_network(self, neti):
iod.clearNetwork(neti)
[docs] def add_node_g(self, neti: int, node: Node) -> int:
'''
Add node represented by the given Node variable.
The 'g' suffix indicates that this operation creates its own group
'''
with self.group_action():
iod.addNode(neti, node.id, node.position.x, node.position.y, node.size.x, node.size.y, True)
nodei = iod.getNodeIndex(neti, node.id)
# iod.setNodeFillColorAlpha(neti, nodei, node.fill_color.Alpha() / 255)
# iod.setNodeFillColorRGB(neti, nodei, node.fill_color.Red(),
# node.fill_color.Green(), node.fill_color.Blue())
# iod.setNodeBorderColorAlpha(neti, nodei, node.border_color.Alpha() / 255)
# iod.setNodeBorderColorRGB(neti, nodei, node.border_color.Red(),
# node.border_color.Green(), node.border_color.Blue())
# iod.setNodeBorderWidth(neti, nodei, int(node.border_width))
iod.setCompartmentOfNode(neti, nodei, node.comp_idx)
iod.setNodeFloatingStatus(neti, nodei, node.floatingNode)
iod.setNodeShapeIndex(neti, nodei, node.shape_index)
post_event(DidAddNodeEvent(nodei))
return nodei
[docs] def set_application_position(self, pos: wx.Point):
self.initial_position = pos
[docs] def get_application_position(self) -> wx.Point:
return self.initial_position
[docs] def get_composite_shape_list(self, neti: int) -> List[CompositeShape]:
return iod.getListOfCompositeShapes(neti)
[docs] def get_composite_shape_at(self, neti: int, shapei: int) -> CompositeShape:
return iod.getCompositeShapeAt(neti, shapei)
[docs] def get_node_shape(self, neti: int, nodei: int) -> CompositeShape:
return iod.getNodeShape(neti, nodei)
[docs] def get_node_shape_index(self, neti: int, nodei: int) -> int:
return iod.getNodeShapeIndex(neti, nodei)
[docs] @iod_setter
def set_node_shape_index(self, neti: int, nodei: int, shapei: int):
iod.setNodeShapeIndex(neti, nodei, shapei)
[docs] @iod_setter
def set_node_primitive_property(self, neti: int, nodei: int, primitive_index: int, prop_name: str, prop_value):
iod.setNodePrimitiveProperty(neti, nodei, primitive_index, prop_name, prop_value)
[docs] def wx_to_tcolor(self, color: wx.Colour) -> Color:
return Color(color.Red(), color.Green(), color.Blue(), color.Alpha())
[docs] def tcolor_to_wx(self, color: Color) -> wx.Colour:
return wx.Colour(color.r, color.g, color.b, color.a)
[docs] def add_compartment_g(self, neti: int, compartment: Compartment) -> int:
if len(compartment.nodes) != 0:
raise ValueError('The "nodes" list for a newly added compartment should be empty. '
'This is to avoid implicit moving of nodes between compartments.')
with self.group_action():
compi = iod.addCompartment(neti, compartment.id, *compartment.position, *compartment.size)
iod.setCompartmentFillColor(neti, compi, self.wx_to_tcolor(compartment.fill))
iod.setCompartmentOutlineColor(neti, compi, self.wx_to_tcolor(compartment.border))
iod.setCompartmentOutlineThickness(neti, compi, compartment.border_width)
iod.setCompartmentVolume(neti, compi, compartment.volume)
post_event(DidAddCompartmentEvent(compi))
return compi
[docs] @iod_setter
def add_alias_node(self, neti: int, original_idx: int, pos: Vec2, size: Vec2) -> int:
return iod.addAliasNode(neti, original_idx, *pos, *size)
[docs] @iod_setter
def alias_for_reaction(self, neti: int, reai: int, nodei: int, pos: Vec2, size: Vec2):
iod.aliasForReaction(neti, reai, nodei, *pos, *size)
[docs] @iod_setter
def move_node(self, neti: int, nodei: int, pos: Vec2, allowNegativeCoordinates: bool=False):
#assert pos.x >= 0 and pos.y >= 0
iod.setNodeCoordinate(neti, nodei, pos.x, pos.y, allowNegativeCoordinates)
[docs] @iod_setter
def set_node_size(self, neti: int, nodei: int, size: Vec2):
iod.setNodeSize(neti, nodei, size.x, size.y)
[docs] @iod_setter
def rename_node(self, neti: int, nodei: int, new_id: str):
iod.setNodeID(neti, nodei, new_id)
[docs] @iod_setter
def set_node_name(self, neti: int, nodei: int, new_name: str):
iod.setNodeName(neti, nodei, new_name)
[docs] @iod_setter
def set_node_SBO(self, neti: int, nodei: int, new_SBO: str):
iod.setNodeSBO(neti, nodei, new_SBO)
[docs] @iod_setter
def set_node_concentration(self, neti: int, nodei: int, new_conc: float):
iod.setNodeConcentration(neti, nodei, new_conc)
[docs] @iod_setter
def set_node_floating_status(self, neti: int, nodei: int, floatingStatus: bool):
iod.setNodeFloatingStatus (neti, nodei, floatingStatus)
[docs] @iod_setter
def set_node_locked_status(self, neti: int, nodei: int, lockedNode: bool):
iod.setNodeLockedStatus (neti, nodei, lockedNode)
[docs] @iod_setter
def set_node_fill_rgb(self, neti: int, nodei: int, color: wx.Colour):
iod.setNodeFillColorRGB(neti, nodei, color.Red(), color.Green(), color.Blue())
[docs] @iod_setter
def set_node_fill_alpha(self, neti: int, nodei: int, alpha: int):
iod.setNodeFillColorAlpha(neti, nodei, alpha / 255)
[docs] @iod_setter
def set_node_border_rgb(self, neti: int, nodei: int, color: wx.Colour):
iod.setNodeBorderColorRGB(neti, nodei, color.Red(), color.Green(), color.Blue())
[docs] @iod_setter
def set_node_border_alpha(self, neti: int, nodei: int, alpha: int):
iod.setNodeBorderColorAlpha(neti, nodei, alpha / 255)
[docs] @iod_setter
def rename_reaction(self, neti: int, reai: int, new_id: str):
iod.setReactionID(neti, reai, new_id)
[docs] @iod_setter
def set_reaction_line_thickness(self, neti: int, reai: int, thickness: float):
iod.setReactionLineThickness(neti, reai, thickness)
[docs] @iod_setter
def set_reaction_fill_rgb(self, neti: int, reai: int, color: wx.Colour):
iod.setReactionFillColorRGB(neti, reai, color.Red(), color.Green(), color.Blue())
[docs] @iod_setter
def set_reaction_fill_alpha(self, neti: int, reai: int, alpha: int):
iod.setReactionFillColorAlpha(neti, reai, alpha / 255)
[docs] @iod_setter
def set_reaction_bezier_curves(self, neti: int, reai: int, bezierCurves: bool):
iod.setReactionBezierCurves(neti, reai, bezierCurves)
[docs] @iod_setter
def set_node_border_width(self, neti: int, nodei: int, width: float):
iod.setNodeBorderWidth(neti, nodei, width)
[docs] @iod_setter
def set_reaction_modifiers(self, neti: int, reai: int, modifiers: Set[int]):
iod.setReactionModifiers(neti, reai, modifiers)
[docs] def get_reaction_modifiers(self, neti: int, reai: int) -> Set[int]:
return iod.getReactionModifiers(neti, reai)
[docs] @iod_setter
def set_modifier_tip_style(self, neti: int, reai: int, style: ModifierTipStyle):
iod.setModifierTipStyle(neti, reai, style)
[docs] def get_modifier_tip_style(self, neti: int, reai: int) -> ModifierTipStyle:
return iod.getModifierTipStyle(neti, reai)
[docs] @iod_setter
def set_network_parameter(self, neti: int, param_id: str, param_value: float):
iod.setParameter(neti, param_id, param_value)
[docs] @iod_setter
def remove_network_parameter(self, neti: int, param_id: str):
iod.removeParameter(neti, param_id)
[docs] def get_network_parameters(self, neti: int):
return iod.getParameters(neti)
[docs] @iod_setter
def delete_node(self, neti: int, nodei: int) -> bool:
return iod.deleteNode(neti, nodei)
[docs] @iod_setter
def delete_reaction(self, neti: int, reai: int):
iod.deleteReaction(neti, reai)
[docs] @iod_setter
def delete_compartment(self, neti: int, compi: int):
iod.deleteCompartment(neti, compi)
[docs] def add_reaction_g(self, neti: int, reaction: Reaction) -> int:
"""Try create a reaction."""
with self.group_action():
iod.createReaction(neti, reaction.id, reaction.sources, reaction.targets)
reai = iod.getReactionIndex(neti, reaction.id)
for sidx in reaction.sources:
iod.setReactionSrcNodeStoich(neti, reai, sidx, 1.0)
for tidx in reaction.targets:
iod.setReactionDestNodeStoich(neti, reai, tidx, 1.0)
iod.setReactionFillColorRGB(neti, reai,
reaction.fill_color.Red(),
reaction.fill_color.Green(),
reaction.fill_color.Blue())
for (gi, nodei), handle in zip(gchain(reaction.sources, reaction.targets), reaction.handles):
pos = handle.tip
if gi == 0:
iod.setReactionSrcNodeHandlePosition(neti, reai, nodei, pos.x, pos.y)
else:
iod.setReactionDestNodeHandlePosition(neti, reai, nodei, pos.x, pos.y)
cpos = reaction.src_c_handle.tip
iod.setReactionCenterHandlePosition(neti, reai, cpos.x, cpos.y)
post_event(DidAddReactionEvent(reai, reaction.sources, reaction.targets))
return reai
[docs] @iod_setter
def set_reaction_ratelaw(self, neti: int, reai: int, ratelaw: str):
iod.setRateLaw(neti, reai, ratelaw)
[docs] @iod_setter
def set_reaction_center(self, neti: int, reai: int, center_pos: Optional[Vec2]):
iod.setReactionCenterPos(neti, reai, center_pos)
[docs] @iod_setter
def set_src_node_stoich(self, neti: int, reai: int, nodei: int, stoich: float):
iod.setReactionSrcNodeStoich(neti, reai, nodei, stoich)
[docs] @iod_setter
def set_dest_node_stoich(self, neti: int, reai: int, nodei: int, stoich: float):
iod.setReactionDestNodeStoich(neti, reai, nodei, stoich)
[docs] @iod_setter
def set_src_node_handle(self, neti: int, reai: int, nodei: int, pos: Vec2):
iod.setReactionSrcNodeHandlePosition(neti, reai, nodei, pos.x, pos.y)
[docs] @iod_setter
def set_dest_node_handle(self, neti: int, reai: int, nodei: int, pos: Vec2):
iod.setReactionDestNodeHandlePosition(neti, reai, nodei, pos.x, pos.y)
[docs] @iod_setter
def set_center_handle(self, neti: int, reai: int, pos: Vec2):
iod.setReactionCenterHandlePosition(neti, reai, pos.x, pos.y)
[docs] def get_src_node_handle(self, neti: int, reai: int, nodei: int) -> Vec2:
return Vec2(iod.getReactionSrcNodeHandlePosition(neti, reai, nodei))
[docs] def get_dest_node_handle(self, neti: int, reai: int, nodei: int) -> Vec2:
return Vec2(iod.getReactionDestNodeHandlePosition(neti, reai, nodei))
[docs] def get_center_handle(self, neti: int, reai: int) -> Vec2:
return Vec2(iod.getReactionCenterHandlePosition(neti, reai))
[docs] def get_src_node_stoich(self, neti: int, reai: int, nodei: int):
return iod.getReactionSrcNodeStoich(neti, reai, nodei)
[docs] def get_dest_node_stoich(self, neti: int, reai: int, nodei: int):
return iod.getReactionDestNodeStoich(neti, reai, nodei)
[docs] def get_list_of_src_indices(self, neti: int, reai: int):
return iod.getListOfReactionSrcNodes(neti, reai)
[docs] def get_list_of_dest_indices(self, neti: int, reai: int):
return iod.getListOfReactionDestNodes(neti, reai)
[docs] def get_list_of_node_ids(self, neti: int) -> List[str]:
return iod.getListOfNodeIDs(neti)
[docs] def get_reactions_as_reactant(self, neti: int, nodei: int) -> Set[int]:
return iod.getSrcReactions(neti, nodei)
[docs] def get_reactions_as_product(self, neti: int, nodei: int) -> Set[int]:
return iod.getDestReactions(neti, nodei)
[docs] def get_node_indices(self, neti: int) -> Set[int]:
return iod.getListOfNodeIndices(neti)
[docs] def get_reaction_indices(self, neti: int) -> Set[int]:
return iod.getListOfReactionIndices(neti)
[docs] def get_compartment_indices(self, neti: int) -> Set[int]:
return iod.getListOfCompartmentIndices(neti)
[docs] def get_list_of_nodes(self, neti: int) -> List[Node]:
nodes = list()
for nodei in iod.getListOfNodeIndices(neti):
nodes.append(self.get_node_by_index(neti, nodei))
return nodes
[docs] def get_list_of_reactions(self, neti: int) -> List[Reaction]:
reactions = list()
for reai in iod.getListOfReactionIndices(neti):
reactions.append(self.get_reaction_by_index(neti, reai))
return reactions
[docs] def get_list_of_compartments(self, neti: int) -> List[Compartment]:
return [self.get_compartment_by_index(neti, compi)
for compi in iod.getListOfCompartments(neti)]
[docs] @iod_setter
def rename_compartment(self, neti: int, compi: int, new_id: str):
iod.setCompartmentID(neti, compi, new_id)
[docs] @iod_setter
def move_compartment(self, neti: int, compi: int, pos: Vec2):
try:
iod.setCompartmentPosition(neti, compi, *pos)
except:
wx.MessageBox("Can not move the compartment.", "Message", wx.OK | wx.ICON_INFORMATION)
[docs] @iod_setter
def set_compartment_size(self, neti: int, compi: int, size: Vec2):
iod.setCompartmentSize(neti, compi, *size)
[docs] @iod_setter
def set_compartment_fill(self, neti: int, compi: int, fill: wx.Colour):
iod.setCompartmentFillColor(neti, compi, self.wx_to_tcolor(fill))
[docs] @iod_setter
def set_compartment_border(self, neti: int, compi: int, border: wx.Colour):
iod.setCompartmentOutlineColor(neti, compi, self.wx_to_tcolor(border))
[docs] @iod_setter
def set_compartment_border_width(self, neti: int, compi: int, width: float):
iod.setCompartmentOutlineThickness(neti, compi, width)
[docs] @iod_setter
def set_compartment_volume(self, neti: int, compi: int, volume: float):
iod.setCompartmentVolume(neti, compi, volume)
[docs] @iod_setter
def set_compartment_of_node(self, neti: int, nodei: int, compi: int):
iod.setCompartmentOfNode(neti, nodei, compi)
[docs] def get_compartment_of_node(self, neti: int, nodei: int) -> int:
return iod.getCompartmentOfNode(neti, nodei)
[docs] def get_nodes_in_compartment(self, neti: int, compi: int) -> List[int]:
return iod.getNodesInCompartment(neti, compi)
[docs] def get_node_index(self, neti: int, node_id: str) -> int:
return iod.getNodeIndex(neti, node_id)
[docs] def get_node_id(self, neti: int, nodei: int) -> str:
return iod.getNodeID(neti, nodei)
[docs] def get_reaction_index(self, neti: int, rxn_id: str) -> int:
return iod.getReactionIndex(neti, rxn_id)
[docs] def get_node_by_index(self, neti: int, nodei: int) -> Node:
id = iod.getNodeID(neti, nodei)
x, y, w, h = iod.getNodeCoordinateAndSize(neti, nodei)
# fill_alpha = iod.getNodeFillColorAlpha(neti, nodei)
# fill_rgb = iod.getNodeFillColorRGB(neti, nodei)
# fill_color = rgba_to_wx_colour(fill_rgb, fill_alpha)
# border_alpha = iod.getNodeBorderColorAlpha(neti, nodei)
# border_rgb = iod.getNodeBorderColorRGB(neti, nodei)
# border_color = rgba_to_wx_colour(border_rgb, border_alpha)
return Node(
id,
neti,
index=nodei,
pos=Vec2(x, y),
size=Vec2(w, h),
concentration=iod.getNodeConcentration(neti, nodei),
node_name = iod.getNodeName(neti, nodei),
node_SBO = iod.getNodeSBO(neti, nodei),
# fill_color=fill_color,
# border_color=border_color,
# border_width=iod.getNodeBorderWidth(neti, nodei),
comp_idx=iod.getCompartmentOfNode(neti, nodei),
floatingNode=iod.IsFloatingNode(neti, nodei),
lockNode=iod.IsNodeLocked(neti, nodei),
shape_index=iod.getNodeShapeIndex(neti, nodei),
composite_shape=iod.getNodeShape(neti, nodei),
original_index=iod.getOriginalIndex(neti, nodei)
)
[docs] def get_reaction_by_index(self, neti: int, reai: int) -> Reaction:
id = iod.getReactionID(neti, reai)
sindices = iod.getListOfReactionSrcNodes(neti, reai)
tindices = iod.getListOfReactionDestNodes(neti, reai)
fill_rgb = iod.getReactionFillColorRGB(neti, reai)
fill_alpha = iod.getReactionFillColorAlpha(neti, reai)
# Handle positions array
items = list()
items.append(self.get_center_handle(neti, reai))
items += [self.get_src_node_handle(neti, reai, i) for i in sindices]
items += [self.get_dest_node_handle(neti, reai, i) for i in tindices]
return Reaction(id,
neti,
sources=sindices,
targets=tindices,
fill_color=rgba_to_wx_colour(fill_rgb, fill_alpha),
line_thickness=iod.getReactionLineThickness(neti, reai),
index=reai,
rate_law=iod.getReactionRateLaw(neti, reai),
handle_positions=items,
center_pos=iod.getReactionCenterPos(neti, reai),
bezierCurves=iod.bezier_curves(neti, reai),
modifiers=iod.getReactionModifiers(neti, reai),
modifier_tip_style=iod.getModifierTipStyle(neti, reai),
)
[docs] def get_compartment_by_index(self, neti: int, compi: int) -> Compartment:
id = iod.getCompartmentID(neti, compi)
return Compartment(id,
nodes=iod.getNodesInCompartment(neti, compi),
volume=iod.getCompartmentVolume(neti, compi),
position=Vec2(iod.getCompartmentPosition(neti, compi)),
size=Vec2(iod.getCompartmentSize(neti, compi)),
fill=self.tcolor_to_wx(iod.getCompartmentFillColor(neti, compi)),
border=self.tcolor_to_wx(iod.getCompartmentOutlineColor(neti, compi)),
border_width=iod.getCompartmentOutlineThickness(neti, compi),
index=compi,
net_index=neti,
)
[docs] def update_view(self):
"""Immediately update the view with using latest model."""
return self._update_view()
[docs] def dump_network(self, neti: int):
return iod.dumpNetwork(neti)
[docs] def load_network(self, json_obj: Any) -> int:
net_index = iod.loadNetwork(json_obj)
self._update_view()
return net_index
[docs] def new_network(self):
iod.clearNetwork(0)
post_event(DidNewNetworkEvent())
self._update_view()
# get the updated list of nodes from model and update
def _update_view(self):
"""tell the view to update by re-populating its list of nodes."""
self.stacklen += 1 # TODO remove once fixed
neti = 0
self.view.update_all(self.get_list_of_nodes(neti), self.get_list_of_reactions(neti),
self.get_list_of_compartments(neti))