"""
A Ramachandran Plot widget, along with some tools for manipulating
structures/points.
Usage:
Rama(parent)
Schrodinger L.L.C.
"""
# Copyright Schrodinger, LLC. All rights reserved.
import os
import sys
from past.utils import old_div
import numpy
from schrodinger import structure
from schrodinger.infra import mm
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtWidgets
from schrodinger.structutils import analyze
from schrodinger.ui.qt import smatplotlib
from schrodinger.ui.qt import swidgets
try:
    from schrodinger import maestro
except:
    in_maestro = False
else:
    in_maestro = True
subdir = os.path.join(os.path.dirname(__file__), 'rama_dir')
OK = 1
ERROR = 0
#####################################################
[docs]class DihedralSpinBox(QtWidgets.QDoubleSpinBox):
    """
    A DoulbeSpinBox with min/max = -180/180, step=0.1, and takes a layout
    argument that it places itself in, and a command to call when its value
    changes.
    """
[docs]    def __init__(self, layout, command, dtype):
        QtWidgets.QDoubleSpinBox.__init__(self)
        self.setMaximum(180.)
        self.setMinimum(-180.)
        self.setSingleStep(1.0)
        self.setDecimals(1)
        mylayout = swidgets.SHBoxLayout()
        mylabel = swidgets.SLabel(dtype, layout=mylayout)
        mylayout.addWidget(self)
        layout.addLayout(mylayout)
        self.valueChanged.connect(command)  
[docs]class Rama:
    """
    A class showing a matplotlib plot of the Phi and Psi angles in a helix
    """
    DEFAULT_CURSOR = "crosshair"
    DEFAULT_SYMBOL_SIZE = 1
    DEF_COLOR = "black"
    GLY_COLOR = "black"
    PRO_COLOR = "black"
    FAVORABLE_COLOR = "#f20000"
    ALLOWED_COLOR = "#ffff00"
    ZOOMSTEP = 10.0
    SYMBOL_SCALING_FACTOR = 2
    ESC_KEY = 27
[docs]    class backbone_dihedral:
[docs]        def __init__(self):
            CA = 0
            N1 = 0
            C1 = 0
            N2 = 0
            C2 = 0
            phi = 0.0
            psi = 0.0
            original_phi = 0.0
            original_psi = 0.0
            saved_phi = 0.0
            saved_psi = 0.0
            chain = ""
            resname = ""
            resnum = 0
            inscode = ""
            entry = ""
            fill_colour = ""
            outline_colour = ""  
[docs]    def __init__(self,
                 widget,
                 layout,
                 size=450,
                 ticks=30,
                 show_status=True,
                 show_counters=False,
                 zoom_on_pick=True,
                 dpi=100,
                 multiselect=False):
        """
        Create a Rama object
        :type widget: QWidget
        :param widget: the PyQt widget that 'owns' this plot
        :type layout: QLayout
        :param layout: the layout to place the plot into
        :type size: int
        :param size: size of square plot in pixels, (default=450).
        :type dpi: int
        :param dpi: dots per inch resolution of plot (default=100).
        :type ticks: int
        :param size: unused, kept for backward compatibility
        :type show_status: bool
        :param show_status: If True, cursor location feedback is given in the
            matplotlib toolbar
        :type show_counters: bool
        :param show_counters: If True, show controls for changing the Phi and
            Psi values of the selected dihedral.  Only one of multiselect and
            show_counters can be True.
        :type zoom_on_pick: bool
        :param zoom_on_pick:  If True, zoom the Maestro workspace to the
            dihedral picked
        :type multiselect: bool
        :param multiselect: If True, the user can pick multiple points
            with shift/cntl-click.  If False (default), only one point can be picked
            at a time.  Only one of multiselect and show_counters can be True.
        """
        self.owner = widget
        if multiselect and show_counters:
            raise RuntimeError('Only one of multiselect and show_counters can '
                               'be True')
        self.multiselect = multiselect
        if multiselect:
            self.multiselect_keys = set(['shift', 'control'])
        else:
            self.multiselect_keys = []
        self.picking_callback = None
        self.preadjust_callback = None
        self.postadjust_callback = None
        self.last_found_index = -1
        self.show_status = show_status
        self.zoom_on_pick = zoom_on_pick
        self.dihedral_list = []
        # self.current_pick contains the index of the selected dihedral that was
        # most recently picked.  If no dihedrals are currently selected, it is
        # None.  For historical reasons, this is tracked separately from the
        # list of all selected points.
        self.current_pick = None
        # self.selected_points is the set of all currently selected points.
        self.selected_points = set()
        self.weights = []
        self.styles = []
        self.plot_lines = []
        # Some incarnations of the Rama plot do not plot all dihedrals, so this
        # helps to translate between them
        self.dihedral_to_plotline = {}
        self.set_symbol_size(self.DEFAULT_SYMBOL_SIZE)
        self.size = size
        self.ticks = ticks
        self.viewport = [0.0, 0.0, 0.0, 0.0]  # [ x0, y0, x1, y1 ]
        self.zooming = False
        self.zoombox = [0.0, 0.0, 0.0, 0.0]  # [ x0, y0, x1, y1 ]
        self.scrolling = False
        self.scrollpoint = [0.0, 0.0]
        self.transform_direction = 'C'
        side = old_div(float(size), dpi)
        self.graph = RamaFigure(width=side,
                                height=side,
                                dpi=dpi,
                                layout=layout,
                                coordinates=False)
        self.nav_toolbar = self.graph.toolbar
        self.label = QtWidgets.QLabel(" \n ")
        if self.show_status:
            self.nav_toolbar.addWidget(self.label)
        self.nav_toolbar.update()
        self.graph.fig.clf()
        self.graph.sub = self.graph.fig.add_subplot(111)
        self.first_update = True
        # Labelling
        self.graph.mpl_connect("motion_notify_event", self.drag)
        # Picking
        self.graph.mpl_connect("button_release_event", self.pick_by_point)
        self.show_counters = show_counters
        if show_counters:
            counter_layout = swidgets.SHBoxLayout()
            counter_layout.addStretch()
            spin_layout = swidgets.SVBoxLayout()
            self.phispin = DihedralSpinBox(spin_layout, self.adjust_phi, 'Phi')
            self.psispin = DihedralSpinBox(spin_layout, self.adjust_psi, 'Psi')
            counter_layout.addLayout(spin_layout)
            rb_frame = QtWidgets.QFrame()
            rb_frame.setFrameShape(rb_frame.StyledPanel)
            rb_frame.setFrameShadow(rb_frame.Sunken)
            rb_layout = swidgets.SVBoxLayout(rb_frame)
            self.rb_group = swidgets.SRadioButtonGroup(
                labels=['Transform N-terminus', 'Transform C-terminus'],
                layout=rb_layout)
            counter_layout.addWidget(rb_frame)
            counter_layout.addStretch()
            layout.addLayout(counter_layout) 
[docs]    def draw_regions(self, filename='rama500-general.data'):
        # Regions
        try:
            with open(os.path.join(subdir, filename), 'r') as ramafile:
                self._draw_regions(ramafile)
        except IOError:
            QtWidgets.QMessageBox.warning(
                self.owner, "File Invalid",
                "Region data file %s not found." % (filename))
            return 
    def _draw_regions(self, ramafile):
        datax = []
        datay = []
        dataz = []
        for line in ramafile:
            if not line.strip() or line.startswith('#'):
                continue
            parts = line.split()
            datax.append(float(parts[0]))
            datay.append(float(parts[1]))
            dataz.append(float(parts[2]))
        datax = numpy.array(datax).reshape((180, 180))
        datay = numpy.array(datay).reshape((180, 180))
        dataz = numpy.array(dataz).reshape((180, 180))
        self.graph.sub.contour(datax,
                               datay,
                               dataz,
                               levels=[0.0005, 0.02],
                               colors='black')
        self.graph.sub.contourf(datax,
                                datay,
                                dataz,
                                levels=[0.0005, 0.02, 1],
                                colors=(self.ALLOWED_COLOR,
                                        self.FAVORABLE_COLOR))
        xaxis = self.graph.sub.get_xaxis()
        xaxis.set_ticks_position('bottom')
        yaxis = self.graph.sub.get_yaxis()
        yaxis.set_ticks_position('left')
        self.graph.sub.set_xlim(-180, 180)
        self.graph.sub.set_xticks(list(range(-180, 190, 30)))
        self.graph.sub.set_xlabel("Phi (degrees)", size="xx-small")
        labels = [str(r) for r in range(-180, 190, 30)]
        self.graph.sub.set_xticklabels(labels, size="xx-small")
        self.graph.sub.set_ylim(-180, 180)
        self.graph.sub.set_yticks(list(range(-180, 190, 30)))
        self.graph.sub.set_ylabel("Psi (degrees)", size="xx-small")
        self.graph.sub.set_yticklabels(labels, size="xx-small")
        # Cross hairs
        self.graph.sub.plot([0.0, 0.0], [180.0, -180.0], color="black")
        self.graph.sub.plot([-180.0, 180.0], [0.0, 0.0], color="black")
        return
[docs]    def find_connected_atom(self, ct, iatom, atname):
        """
        Return the atom index with PDB atom type atname attached to
        iatom in ct, or -1 if no connection is found.
        """
        num_bonds = mm.mmct_atom_get_bond_total(ct, iatom)
        for ibond in range(1, num_bonds + 1):
            conn_atom = mm.mmct_atom_get_bond_atom(ct, iatom, ibond)
            if (mm.mmct_atom_get_pdbname(ct, conn_atom) == atname):
                return conn_atom
        return -1 
[docs]    def get_connected_pair(self, ct, iatom, at1name, at2name):
        """
        Return a pair of atom indexes, where at1name is attached to
        iatom and at2name is attached to at1name.
        """
        at1 = self.find_connected_atom(ct, iatom, at1name)
        if (at1 > 0):
            at2 = self.find_connected_atom(ct, at1, at2name)
        else:
            at2 = -1
        return (at1, at2) 
[docs]    def get_weight_symbol(self, weight):
        symbol = ""
        if weight <= 1.0:
            # Default
            symbol = 'ko'
        elif weight <= 2.0:
            # gly
            symbol = 'k^'
        elif weight <= 3.0:
            # pro
            symbol = 'ks'
        elif weight >= 10.0 and weight <= 11.0:
            # def-p
            symbol = 'wo'
        elif weight > 11.0 and weight <= 12.0:
            # gly-p
            symbol = 'w^'
        elif weight > 12.0 and weight <= 13.0:
            # pro-p
            symbol = 'ws'
        else:
            symbol = 'r*'
        return symbol 
[docs]    def update_point_faces(self, indexes):
        """
        Update the face colors for points on the plot - faster than doing a
        complete replot.
        :type indexes: list
        :param indexes: List of indexes to recolor.  Should be the index of the
            point in the self.dihedral_list list
        """
        for index in indexes:
            symbol = self.get_weight_symbol(self.weights[index])
            color = symbol[0]
            line_index = self.dihedral_to_plotline[index]
            self.plot_lines[line_index].set_markerfacecolor(color)
        self.graph.draw() 
[docs]    def update_point_xval(self, index, xval):
        """
        Update the x-value of a point on the plot
        :type index: int
        :param index: The index of the point in the self.dihedral_list list
        """
        line_index = self.dihedral_to_plotline[index]
        self.plot_lines[line_index].set_xdata([xval])
        self.graph.draw() 
[docs]    def update_point_yval(self, index, yval):
        """
        Update the y-value of a point
        :type index: int
        :param index: The index of the point in the self.dihedral_list list
        """
        line_index = self.dihedral_to_plotline[index]
        self.plot_lines[line_index].set_ydata([yval])
        self.graph.draw() 
[docs]    def update_display(self):
        if not self.first_update:
            self.nav_toolbar.push_current()
        self.graph.sub.clear()
        self.draw_regions()
        # Point types
        xvalues = [dihedral.phi for dihedral in self.dihedral_list]
        yvalues = [dihedral.psi for dihedral in self.dihedral_list]
        symbols = [self.get_weight_symbol(w) for w in self.weights]
        count = 0
        self.plot_lines = []
        self.dihedral_to_plotline = {}
        for x, y, symbol in zip(xvalues, yvalues, symbols):
            line = self.graph.sub.plot([x], [y],
                                       symbol,
                                       markersize=self.symbol_size)[0]
            # Some incarnations of the Rama plot don't plot all dihedrals, so we
            # have to translate between the two arrays
            line.dihedral_index = count
            self.dihedral_to_plotline[count] = len(self.plot_lines)
            line.set_pickradius(self.symbol_size * self.SYMBOL_SCALING_FACTOR +
                                0.5)
            self.plot_lines.append(line)
            count = count + 1
        if not self.first_update:
            self.nav_toolbar.back()
        self.graph.draw()
        self.first_update = False
        return 
[docs]    def display_structure(self, ct, CA_list=[]):  # noqa: M511
        self.current_pick = None
        self.selected_points = set()
        self.weights = []
        self.dihedral_list = []
        if ct is None:
            self.graph.sub.clear()
            self.draw_regions()
            self.graph.draw()
            return
        if len(CA_list) == 0:
            CA_list = analyze.evaluate_asl(ct, "atom.ptype \" CA \"")
        for CA in CA_list:
            new_dihedral = self.backbone_dihedral()
            new_dihedral.CA = CA
            (new_dihedral.N1,
             new_dihedral.C1) = self.get_connected_pair(ct, new_dihedral.CA,
                                                        " N  ", " C  ")
            (new_dihedral.C2,
             new_dihedral.N2) = self.get_connected_pair(ct, new_dihedral.CA,
                                                        " C  ", " N  ")
            if new_dihedral.N1 < 0 or new_dihedral.C1 < 0 or new_dihedral.C2 < 0 or new_dihedral.N2 < 0:
                continue
            new_dihedral.phi = mm.mmct_atom_get_dihedral_angle_s(
                ct, new_dihedral.C1, ct, new_dihedral.N1, ct, new_dihedral.CA,
                ct, new_dihedral.C2)
            new_dihedral.psi = mm.mmct_atom_get_dihedral_angle_s(
                ct, new_dihedral.N1, ct, new_dihedral.CA, ct, new_dihedral.C2,
                ct, new_dihedral.N2)
            new_dihedral.original_phi = new_dihedral.phi
            new_dihedral.original_psi = new_dihedral.psi
            new_dihedral.saved_phi = new_dihedral.phi
            new_dihedral.saved_psi = new_dihedral.psi
            new_dihedral.chain = mm.mmct_atom_get_chain(ct, new_dihedral.CA)
            new_dihedral.resname = ct.atom[CA].pdbres
            (new_dihedral.resnum,
             new_dihedral.inscode) = mm.mmct_atom_get_resnum(
                 ct, new_dihedral.CA)
            self.dihedral_list.append(new_dihedral)
            if new_dihedral.resname == "GLY ":
                self.weights.append(1.5)
            elif new_dihedral.resname == "PRO ":
                self.weights.append(2.5)
            else:
                self.weights.append(0.5)
        self.update_display()
        return 
[docs]    def save(self):
        """
        Store the current phi & psi values of the current point.
        This method only makes sense with multiselect == False
        """
        if self.multiselect:
            raise RuntimeError('The save method cannot be used with '
                               'multiselect')
        if not self.current_pick:
            return
        dihedral = self.dihedral_list[self.current_pick]
        dihedral.saved_phi = dihedral.phi
        dihedral.saved_psi = dihedral.psi
        return 
[docs]    def revert_to_original(self):
        """
        Set the current point to its original phi & psi values
        This method only makes sense with multiselect == False
        """
        if self.multiselect:
            raise RuntimeError('The revert_to_original method cannot be used '
                               'with multiselect')
        if not self.current_pick:
            return
        dihedral = self.dihedral_list[self.current_pick]
        dihedral.phi = dihedral.original_phi
        dihedral.psi = dihedral.original_psi
        self.change_current_pick(self.current_pick)
        self.apply_dihedral(self.current_pick)
        return 
[docs]    def revert_to_saved(self):
        """
        Set the current point to its previously saved phi & psi values
        This method only makes sense with multiselect == False
        """
        if self.multiselect:
            raise RuntimeError('The revert_to_saved method cannot be used '
                               'with multiselect')
        if not self.current_pick:
            return
        dihedral = self.dihedral_list[self.current_pick]
        dihedral.phi = dihedral.saved_phi
        dihedral.psi = dihedral.saved_psi
        self.change_current_pick(self.current_pick)
        self.apply_dihedral(self.current_pick)
        return 
[docs]    def display_residue(self, which='all'):
        """
        Select residues in the workspace corresponding to the given points
        :type which: int, None or 'all'
        :param which: If None, all residues are deselected.  If an integer,
            that integer is taken as the index in the dihedral list to select.  If
            'all', residues for all selected points are selected.
        """
        if which is None or (which == 'all' and not self.selected_points):
            # No point to display
            self.label.setText(" \n ")
            if in_maestro:
                maestro.command("workspaceselectionclear")
        else:
            # Create the toolbar label for the desired point, which is
            # the most recently selected point if multiple points are picked
            if which == 'all':
                index = self.current_pick
            else:
                index = which
            residue = self.dihedral_list[index]
            text = "Residue: %1s:%3s%4d%1s\nphi=%4.1f psi=%4.1f" \
                                  
% (residue.chain, residue.resname,
                                     residue.resnum, residue.inscode,
                                     residue.phi, residue.psi)
            self.label.setText(text)
            # Select the residue for all selected points
            if in_maestro:
                if which == 'all':
                    residue_asls = []
                    for apoint in self.selected_points:
                        res = self.dihedral_list[apoint]
                        asl = (
                            '(chain.name "%s" and res.num %d and res.i "%s")' %
                            (res.chain, res.resnum, res.inscode))
                        residue_asls.append(asl)
                    if residue_asls:
                        select_asl = ' OR '.join(residue_asls)
                    else:
                        select_asl = ""
                else:
                    select_asl = (
                        '(chain.name "%s" and res.num %d and res.i "%s")' %
                        (residue.chain, residue.resnum, residue.inscode))
                if select_asl:
                    maestro.command("workspaceselectionreplace %s " %
                                    select_asl) 
[docs]    def adjust_phi(self, value):
        """
        React to changed value of the Phi spinbox
        The method only makes sense with multiselect == False
        :type value: float
        :param value: new value of the spinbox
        """
        if self.multiselect:
            raise RuntimeError('The adjust_phi method cannot be used '
                               'with multiselect')
        try:
            fval = float(value)
        except:
            return ERROR
        if self.current_pick is not None:
            # If the new value is different from the old value, adjust the
            # angle.  The new and old values may be the same if this call is
            # generated by the widget updating when a new residue is picked.
            if abs(fval - self.dihedral_list[self.current_pick].phi) > 0.049:
                self.dihedral_list[self.current_pick].phi = fval
                self.apply_dihedral(self.current_pick)
                self.update_point_xval(self.current_pick, fval)
        return OK 
[docs]    def adjust_psi(self, value):
        """
        React to changed value of the Psi spinbox
        The method only makes sense with multiselect == False
        :type value: float
        :param value: new value of the spinbox
        """
        if self.multiselect:
            raise RuntimeError('The adjust_psi method cannot be used '
                               'with multiselect')
        try:
            fval = float(value)
        except:
            return ERROR
        if self.current_pick is not None:
            # If the new value is different from the old value, adjust the
            # angle.  The new and old values may be the same if this call is
            # generated by the widget updating when a new residue is picked.
            if abs(fval - self.dihedral_list[self.current_pick].psi) > 0.049:
                self.dihedral_list[self.current_pick].psi = fval
                self.apply_dihedral(self.current_pick)
                self.update_point_yval(self.current_pick, fval)
        return OK 
[docs]    def apply_dihedral(self, idihedral):
        """
        Change the value of a dihedral
        :type idihedral: int
        :param idihedral: The index of the dihedral to change
        """
        if self.preadjust_callback:
            self.preadjust_callback()
        if in_maestro:
            dangle = self.dihedral_list[idihedral]
            phi_atoms = [dangle.C2, dangle.CA, dangle.N1, dangle.C1]
            psi_atoms = [dangle.N2, dangle.C2, dangle.CA, dangle.N1]
            if self.rb_group.isChecked(id=1):
                # Chang atom order so the other terminus will be adjusted
                phi_atoms.reverse()
                psi_atoms.reverse()
            phiats = ' '.join([str(x) for x in phi_atoms])
            psiats = ' '.join([str(x) for x in psi_atoms])
            phi = str(dangle.phi)
            psi = str(dangle.psi)
            maestro.command('adjustdihedral dihedral=%s %s' % (phi, phiats))
            maestro.command('adjustdihedral dihedral=%s %s' % (psi, psiats))
        if self.postadjust_callback:
            self.postadjust_callback()
        return 
[docs]    def minus(self, event):
        self.zoombox[0] = self.viewport[0] - self.ZOOMSTEP
        self.zoombox[1] = self.viewport[1] - self.ZOOMSTEP
        self.zoombox[2] = self.viewport[2] + self.ZOOMSTEP
        self.zoombox[3] = self.viewport[3] + self.ZOOMSTEP
        self.set_viewport(min_x=self.zoombox[0],
                          min_y=self.zoombox[1],
                          max_x=self.zoombox[2],
                          max_y=self.zoombox[3])
        return 
[docs]    def plus(self, event):
        self.zoombox[0] = self.viewport[0] + self.ZOOMSTEP
        self.zoombox[1] = self.viewport[1] + self.ZOOMSTEP
        self.zoombox[2] = self.viewport[2] - self.ZOOMSTEP
        self.zoombox[3] = self.viewport[3] - self.ZOOMSTEP
        self.set_viewport(min_x=self.zoombox[0],
                          min_y=self.zoombox[1],
                          max_x=self.zoombox[2],
                          max_y=self.zoombox[3])
        return 
[docs]    def clear_selection(self, event):
        """
        Clear the selected points
        :type event: event or None
        :param event: The event that generated this call, or None if not being
            called as a response to an event.  This parameter is unused.
        """
        for point in self.selected_points:
            self.weights[point] -= 10.0
        self.update_point_faces(self.selected_points)
        self.selected_points = set()
        self.current_pick = None
        self.display_residue(which=None)
        if self.picking_callback:
            self.picking_callback()
        return 
[docs]    def drag(self, event):
        """
        As the user moves the mouse over a point, display information for that
        point in the toolbar and select the corresponding residue in the
        workspace.
        :type event: matplotlib MouseEvent
        :param event: The event that generated the call to this method.
        """
        if event.xdata is None or event.ydata is None:
            return
        #found_index = self.find_closest_point(event.xdata, event.ydata)
        found_index = self.find_closest_point(event)
        # Certain conditions (EV 119761) can cause this callback to be called
        # multiple times for each actual event, which eventually causes a
        # recursion error.  Bail out quickly and don't modify the workspace if
        # the workspace isn't actually going to change.
        if found_index == self.last_found_index or \
           
(found_index is None and
            self.last_found_index in self.selected_points):
            # Don't update workspace if there will be no change
            return
        if found_index is None:
            # Cursor not on any point, go back to displaying selected residues
            self.last_found_index = self.current_pick
            self.display_residue()
        else:
            # Display the residue the cursor is over
            self.last_found_index = found_index
            self.display_residue(which=found_index) 
[docs]    def change_current_pick(self, new_pick, multi=False):
        points_to_update = []
        if new_pick in self.selected_points and multi:
            # Deselecting a currently selected point - remove it from the
            # selected points and deselect it
            self.weights[new_pick] -= 10.0
            self.selected_points.remove(new_pick)
            points_to_update.append(new_pick)
            try:
                # Just grab any selected point as the "most recent" point
                self.current_pick = self.selected_points.pop()
                self.selected_points.add(self.current_pick)
            except KeyError:
                self.current_pick = None
            new_pick = None
        elif not multi:
            # Making a single point the only selected point
            for apoint in self.selected_points:
                self.weights[apoint] -= 10.0
            points_to_update = list(self.selected_points)
            self.selected_points = set()
            self.current_pick = new_pick
        else:
            # Selecting another point
            self.current_pick = new_pick
        if new_pick is not None:
            # Select a new point
            self.weights[self.current_pick] += 10.0
            self.selected_points.add(self.current_pick)
            points_to_update.append(self.current_pick)
        self.update_point_faces(points_to_update)
        # Update the Phi and Psi counters if they exist
        if self.show_counters:
            if self.current_pick is not None:
                self.phispin.setValue(self.dihedral_list[self.current_pick].phi)
                self.psispin.setValue(self.dihedral_list[self.current_pick].psi)
            else:
                self.phispin.setValue(0.0)
                self.psispin.setValue(0.0)
        # Select the proper residue(s) in the workspace
        if multi and self.current_pick is not None:
            self.display_residue(which='all')
        else:
            self.display_residue(which=self.current_pick)
        # Zoom in on the residues
        if in_maestro and self.zoom_on_pick:
            if self.multiselect and len(self.selected_points) > 1:
                # Multiple residues selected
                maestro.command('fit atom.selected')
            elif self.current_pick is None:
                maestro.command("fit")
            else:
                # Only one residue selected
                maestro.command("fit")
                maestro.command("spotcenter %d" %
                                self.dihedral_list[self.current_pick].CA)
                maestro.command("zoom factor=70 in")
        if self.picking_callback:
            self.picking_callback() 
[docs]    def find_closest_point(self, event):
        """
        Find the plot point that is closest to the event
        :type event: MouseEvent
        :param event: The matplotlib mouse event that generates this call
        """
        # Find the closest item
        found_index = None
        found_distance = None
        found_lines = []
        for index, line in enumerate(self.plot_lines):
            inpoint, points = line.contains(event)
            if inpoint:
                found_lines.append(line.dihedral_index)
        if not found_lines:
            # No points close to cursor
            return None
        elif len(found_lines) == 1:
            # Only one point close to cursor
            return found_lines[0]
        else:
            # More than one point found, find the closes point.  The indexes of
            # self.plot_lines and self.dihedral_lines are the same.
            x = event.xdata
            y = event.ydata
            epsilon = 9  # This is a squared distance
            for index in found_lines:
                xdelta = abs(x - self.dihedral_list[index].phi)
                ydelta = abs(y - self.dihedral_list[index].psi)
                # We use a squared distance to save a call to sqrt
                distance = xdelta * xdelta + ydelta * ydelta
                if distance <= epsilon:
                    if found_distance is None or distance < found_distance:
                        found_index = index
                        found_distance = distance
            return found_index 
[docs]    def pick_by_point(self, event):
        if event.button == 1 and event.inaxes and self.nav_toolbar.mode == '':
            index = self.find_closest_point(event)
            if index is not None:
                self.change_current_pick(index,
                                         multi=event.key
                                         in self.multiselect_keys)
        return 
[docs]    def pick_by_atom(self, iatom):
        ct = maestro.workspace_get()
        chain = ct.atom[iatom].chain
        resnum = ct.atom[iatom].resnum
        inscode = ct.atom[iatom].inscode
        self.clear_selection(None)
        for idihedral in range(len(self.dihedral_list)):
            dihedral = self.dihedral_list[idihedral]
            if dihedral.chain == chain and dihedral.resnum == resnum and dihedral.inscode == inscode:
                self.change_current_pick(idihedral)
                break
        return 
[docs]    def get_picked(self):
        """
        Get the backbone_dihedral object for the selected dihedral that was
        most recently selected.
        :rtype: `backbone_dihedral` object or None
        :return: The `backbone_dihedral` object for the selected dihedral that
            was most recently selected, or None if there are no selected dihedrals.
        """
        if self.current_pick:
            return self.dihedral_list[self.current_pick]
        else:
            return None 
[docs]    def set_viewport(self,
                     min_x=-180.0,
                     max_x=180.0,
                     min_y=-180.0,
                     max_y=180.0):
        # Swap coordinates if the box was drawn "backwards"
        if min_x > max_x:
            min_x, max_x = max_x, min_x
        if min_y > max_y:
            min_y, max_y = max_y, min_y
        # Make sure the view is square and within the plot region
        if (max_x - min_x) > (max_y - min_y):  # x is the longest side
            if (max_x - min_x
               ) > 360.0:  # bigger than the plot, so just set to full size
                min_x = -180.0
                max_x = 180.0
            else:
                if min_x < -180.0:  # off to the left
                    max_x += -180.0 - min_x
                    min_x = -180.0
                elif max_x > 180.0:  # off to the right
                    min_x -= max_x - 180.0
                    max_x = 180.0
            addition = 0.5 * ((max_x - min_x) -
                              (max_y - min_y))  # enlarge y to make it square
            min_y -= addition
            max_y += addition
            if min_y < -180.0:  # off the bottom
                max_y += -180.0 - min_y
                min_y = -180.0
            elif max_y > 180.0:  # off the top
                min_y -= max_y - 180.0
                max_y = 180.0
        else:  # y is the longest side
            if (max_y - min_y
               ) > 360.0:  # bigger than the plot, so just set to full size
                min_y = -180.0
                max_y = 180.0
            else:
                if min_y < -180.0:  # off the bottom
                    max_y += -180.0 - min_y
                    min_y = -180.0
                elif max_y > 180.0:  # off the top
                    min_y -= max_y - 180.0
                    max_y = 180.0
            addition = 0.5 * ((max_y - min_y) -
                              (max_x - min_x))  # enlarge x to make it square
            min_x -= addition
            max_x += addition
            if min_x < -180.0:  # off to the left
                max_x += -180.0 - min_x
                min_x = -180.0
            elif max_x > 180.0:  # off to the right
                min_x -= max_x - 180.0
                max_x = 180.0
        # Don't do anything if the box was too small
        if (max_x - min_x) > 5 and (max_y - min_y) > 5:
            self.viewport = [min_x, min_y, max_x, max_y]
        return 
[docs]    def write_file(self, filename):
        if filename:
            try:
                self.graph.fig.savefig(filename, format="png")
            except:
                print('Failure writing file ' + filename)
        return 
[docs]    def set_symbol_size(self, value):
        self.symbol_size = self.SYMBOL_SCALING_FACTOR * int(value) 
[docs]    def adjust_symbol_size(self, value):
        self.set_symbol_size(value)
        self.update_display()
        return 
[docs]    def options_quit(self):
        self.options_top.destroy() 
[docs]    def clearGraph(self):
        """ Gets called when project is closed """
        # FIXME: Maybe call when workspace changes?
        self.dihedral_list = []
        self.update_display()
        return  
#############################################################
if __name__ == '__main__':
    import schrodinger.ui.qt.appframework as appframework
    mypanel = appframework.AppFramework(buttons={'Close': {
        'command': quit
    }},
                                        title='Ramachandran Plot')
    #rama = Rama( mypanel, mypanel.interior_layout, show_status=True, size=600,
    #            show_counters=True, dpi=200)
    rama = Rama(mypanel,
                mypanel.interior_layout,
                show_status=True,
                size=600,
                show_counters=False,
                dpi=200,
                multiselect=True)
    ct = structure.StructureReader.read(sys.argv[1])
    CAs = analyze.evaluate_asl(ct, "atom.ptype \" CA \"")
    rama.display_structure(ct, CA_list=CAs)
    mypanel.show()
    mypanel.exec_()