Source code for rkviewer.controller

"""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, 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) # True = floating species 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_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 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)) 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): iod.setCompartmentPosition(neti, compi, *pos)
[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), # 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) 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))