"""
Module for placing markers in the Workspace.
"""
import _pymaecxx
import schrodinger
from schrodinger.structutils import color
maestro = schrodinger.get_maestro()
[docs]class Icons(object):
    """
    Constants for marker icons
    """
    NONE = _pymaecxx.MM_GRAPHICS_ICON_NONE
    LOCK = _pymaecxx.MM_GRAPHICS_ICON_LOCK
    SPRING = _pymaecxx.MM_GRAPHICS_ICON_SPRING
    EYE = _pymaecxx.MM_GRAPHICS_ICON_EYE
    EQUALS = _pymaecxx.MM_GRAPHICS_ICON_EQUALS
    FRAGMARK = _pymaecxx.MM_GRAPHICS_ICON_FRAGMARK
    CHECKMARK = _pymaecxx.MM_GRAPHICS_ICON_CHECKMARK
    DIAMOND = _pymaecxx.MM_GRAPHICS_ICON_DIAMOND
    RSI = _pymaecxx.MM_GRAPHICS_ICON_RSI
    TRANSROT = _pymaecxx.MM_GRAPHICS_ICON_TRANSROT
    TORSIONROTATE = _pymaecxx.MM_GRAPHICS_ICON_TORSIONROTATE
    CHECK = _pymaecxx.MM_GRAPHICS_ICON_CHECK
    SCISSORS = _pymaecxx.MM_GRAPHICS_ICON_SCISSORS
    BREAK = _pymaecxx.MM_GRAPHICS_ICON_BREAK
    DDRIVE = _pymaecxx.MM_GRAPHICS_ICON_DDRIVE
    LONEPAIR = _pymaecxx.MM_GRAPHICS_ICON_LONEPAIR
    SMAGGLASS = _pymaecxx.MM_GRAPHICS_ICON_SMAGGLASS
    GMAGGLASS = _pymaecxx.MM_GRAPHICS_ICON_GMAGGLASS
    MAGGLASS = _pymaecxx.MM_GRAPHICS_ICON_MAGGLASS
    WMAGGLASS = _pymaecxx.MM_GRAPHICS_ICON_WMAGGLASS
    IMAGGLASS = _pymaecxx.MM_GRAPHICS_ICON_IMAGGLASS
    VMAGGLASS = _pymaecxx.MM_GRAPHICS_ICON_VMAGGLASS
    EMAGGLASS = _pymaecxx.MM_GRAPHICS_ICON_EMAGGLASS
    ARROWDOWN = _pymaecxx.MM_GRAPHICS_ICON_ARROWDOWN
    SCANATOM = _pymaecxx.MM_GRAPHICS_ICON_SCANATOM
    SCANDIST = _pymaecxx.MM_GRAPHICS_ICON_SCANDIST
    SCANANGLE = _pymaecxx.MM_GRAPHICS_ICON_SCANANGLE 
[docs]class Marker(object):
    """
    Class to manipulate a set of markers in the Workspace by ASLs. The only
    parameter that can be controlled is the color of the marker.
    This marker is useful for showing a picked ligand or residue, for example.
    """
    # A count of how many names we have generated.  Used to ensure that the
    # generated names are unique.
    _name_count = 1
[docs]    def __init__(self, name=None, asl="NOT all", color=(1, 1, 1)):
        """
        Create a Marker object
        :type name: str
        :param name: The name of the marker group. Used for unique
                identification of the marker within Maestro. By default
                a unique string identifier is generated.
        :type asl: str
        :param asl: The ASL defining which atoms should be marked. By default
                no atoms are marked.
        :type color: tuple of 3 floats
        :param red: The amount of red, green, and blue to use, each ranging
                from 0.0 to 1.0. Default is white (1, 1, 1).
        """
        if name is None:
            # Generate a unique identifier for this Marker instance.  Pad the
            # count with zeroes to reduce the chance of the user picking the
            # same name for their own marker.
            name = "Marker-%08i" % self._name_count
            self.__class__._name_count += 1
        self._name = name
        self.setColor(color)
        if maestro:
            maestro.command('markers %s %s %s' % (self._color, self._name, asl)) 
    @property
    def name(self):
        """
        :return: Marker name
        :rtype: str
        """
        return self._name
[docs]    def hide(self):
        """
        Hide the markers
        """
        if maestro:
            maestro.command('hidemarkers %s' % self._name) 
[docs]    def show(self, asl=None):
        """
        Show the markers. If a new ASL is specified, then the previous
        markers get cleared and the new atoms are marked.
        :type asl: str
        :param asl: The ASL defining which atoms should be marked.  This
            replaces any previous ASL marked by this Marker instance.
        """
        if maestro:
            if asl:
                self.hide()
                maestro.command('markers %s %s %s' %
                                (self._color, self._name, asl))
            maestro.command('showmarkers ' + self._name) 
[docs]    def setVisible(self, show):
        """
        Show the marker if <show> is True; hide otherwise. Useful for hooking
        up to a slot of a QCheckBox.
        """
        if show:
            self.show()
        else:
            self.hide() 
[docs]    def setAsl(self, asl):
        """
        Update the ASL of this marker to the given string, without affecting
        the visibility of it.
        """
        maestro.command('markers %s %s %s' % (self._color, self._name, asl)) 
[docs]    def setColor(self, color):
        """
        :param color: The amount of red, green, and blue to use, each ranging
                from 0.0 to 1.0.
        :type color: tuple(float, float, float)
        """
        red, green, blue = color
        if any([x < 0.0 or x > 1.0 for x in color]):
            raise ValueError("Color values must be between 0 and 1")
        self._color = f'r={red:.2f} g={green:.2f} b={blue:.2f}'  
class _BaseMarker(object):
    """
    Base class for the marker classes.  This class cannot be instantiated
    directly.
    """
    SETTINGS_VARS = {"color", "icon", "text", "alt_color", "highlight"}
    def __init__(self):
        # The variable declarations here are for reference purposes only (both
        # for human readability and static code analyzers).  This class cannot
        # be instantiated directly.
        self._color = None
        self._icon = None
        self._text = None
        self._alt_color = None
        self._atoms = None
        self._highlight = None
        self._handle = None
        raise NotImplementedError
    def _getEntryAtomNumbers(self, atoms):
        """
        Retrieve entry ids and atom numbers by entry for a list of atoms
        :param atoms: A list of atoms to retrieve data for
        :type atoms: list(schrodinger.structure._StructureAtom)
        :return: A list of tuples, where each tuple contains the entry id and
            number-by-entry of a single atom.
        :rtype: list
        """
        atom_data = []
        for cur_atom in atoms:
            eid = cur_atom.entry_id
            num_by_entry = cur_atom.number_by_entry
            cur_atom_data = (eid, num_by_entry)
            atom_data.append(cur_atom_data)
        return atom_data
    def _createMarker(self):
        """
        Create a marker using the criteria currently set in instance variables
        """
        raise NotImplementedError
    def _getWorkspaceAtomNumbers(self):
        """
        Get the workspace atom numbers for the atoms described in self._atoms
        :return: A list of atom numbers relative to the workspace structure
        :rtype: list
        :raise NotInWorkspaceError: If an atom described in self._atoms is not
            currently in the workspace.
        """
        ws_atom_nums = []
        ws_struc = maestro.workspace_get(copy=False)
        for (eid, num_by_entry) in self._atoms:
            for ws_atom in ws_struc.atom:
                if (ws_atom.entry_id == eid and
                        ws_atom.number_by_entry == num_by_entry):
                    ws_atom_nums.append(ws_atom.index)
                    break
            else:
                err = ("Atom not found in workspace: entry %s, "
                       "number_by_entry %i" % (eid, num_by_entry))
                raise NotInWorkspaceError(err)
        return ws_atom_nums
    def hide(self):
        """
        Hide this marker.
        """
        if self._handle is not None:
            _pymaecxx.mae_remove_object(self._handle)
            self._handle = None
    def show(self):
        """
        Show this marker.
        """
        if self._handle is None:
            self._createMarker()
    def setVisible(self, show):
        """
        If "show" is True, show the marker, otherwise hide it.
        """
        if show:
            self.show()
        else:
            self.hide()
    def _colorToRgb(self, incolor):
        """
        Given a color of any accepted format, return an RGB tuple (range 0-1).
        If None is specified, color white is returned.
        :return: Tuple of RGB values (range 0-1)
        :rtype: tuple
        """
        if isinstance(incolor, (int, str)):
            outcolor = color.Color(incolor).rgb_float
            return tuple(outcolor)
        elif incolor is None:
            return (1.0, 1.0, 1.0)  # white
        elif isinstance(incolor, color.Color):
            return tuple(incolor.rgb_float)
        elif isinstance(incolor, (tuple, list)):
            r, g, b = incolor
            if not (0 <= r <= 1 and 0 <= g <= 1 and 0 <= b <= 1):
                raise ValueError("RGB values must be between 0.0 and 1.0")
            return (r, g, b)
        else:
            raise TypeError("Unrecognized color")
    def _altColorToRgb(self, alt_color):
        """
        Given a color of any accepted format, return an RGB tuple (range 0-1).
        If None is specified, None is returned.
        :return: Tuple of RGB values (range 0-1) or None if `alt_color` is None
        :rtype: tuple or None
        """
        if alt_color is None:
            return None
        else:
            return self._colorToRgb(alt_color)
    def _getAltColor(self):
        """
        Get the alternate color.
        :return: List of RGB values (range 0-1)
        :rtype: list
        """
        if self._alt_color is None:
            return self._color
        else:
            return self._alt_color
    def setColor(self, marker_color):
        """
        Change the marker and icon color
        :param marker_color:  The color of the marker.  May be an RGB tuple,
            color name, color index, or `schrodinger.structutils.color` instance.
            If not given, white will be used.
        :type marker_color: tuple, str, int, or `schrodinger.structutils.color`
        """
        self._color = self._colorToRgb(marker_color)
        self.update()
    def setIcon(self, icon):
        """
        Change the marker icon
        :param icon: The icon to draw next to the marker. Should be one the
            Icons.* constants.  If not given, no icon will be drawn.
        :type icon: int
        """
        self._icon = icon
        self.update()
    def setText(self, text):
        """
        Change the marker text
        :param text: The text to display next to the marker.  If not given, no
            text will be displayed.
        :type text: str
        """
        self._text = text
        self.update()
    def setAltColor(self, alt_color):
        """
        Change the text color
        :param alt_color:  The color of the text.  May be an RGB tuple,
            color name, color index, or `schrodinger.structutils.color` instance.
            If not given, the marker color will be used.
        :type alt_color: tuple, str, int, or `schrodinger.structutils.color`
        """
        self._alt_color = self._altColorToRgb(alt_color)
        self.update()
    def setHighlight(self, highlight):
        """
        Change whether the marker is highlighted.   A highlighted marker is
        indicated with thicker lines and is colored using `alt_color` instead
        of `color`.
        :param highlight: Whether the marker should be highlighted.
        :type highlight: bool
        """
        self._highlight = highlight
        self.update()
    def update(self):
        """
        Update the marker in response to the workspace contents changing.  If
        any marked atom is not in the workspace, the marker will be deleted.
        """
        self.hide()
        try:
            self._createMarker()
        except NotInWorkspaceError:
            self._handle = None
    def getSettings(self):
        """
        Get the current marker settings.
        :return: A dictionary of the marker settings.  The keys of this
            dictionary will be `SETTING_VARS`.
        :rtype: dict
        """
        return {name: getattr(self, "_" + name) for name in self.SETTINGS_VARS}
    def applySettings(self, settings):
        """
        Apply the specified marker settings
        :param settings: A dictionary of marker settings.  The keys of this
            dictionary must appear in `SETTINGS_VARS`.  Note that not all keys need
            to be specified.
        :type settings: dict
        """
        if not self.SETTINGS_VARS.issuperset(list(settings)):
            raise ValueError("Unrecognized marker setting key")
        for name, val in settings.items():
            if name == "color":
                val = self._colorToRgb(val)
            elif name == "alt_color":
                val = self._altColorToRgb(val)
            setattr(self, "_" + name, val)
        self.update()
    @classmethod
    def defaultSettings(cls):
        """
        Get the default marker settings
        :return: A dictionary of marker settings.  The keys of this dictionary
            will be `SETTING_VARS`.
        :rtype: dict
        """
        settings = dict.fromkeys(cls.SETTINGS_VARS)
        settings["icon"] = Icons.NONE
        settings["highlight"] = False
        if "text" in settings:
            settings["text"] = ""
        return settings
[docs]class AtomMarker(_BaseMarker):
    """
    Class for marking a single atom.
    """
    SETTINGS_VARS = _BaseMarker.SETTINGS_VARS - {"text"}
[docs]    def __init__(self,
                 atom,
                 marker_color=None,
                 icon=Icons.NONE,
                 alt_color=None,
                 highlight=False):
        """
        :param atom: The atom to mark
        :type atom: `schrodinger.structure._StructureAtom`
        :param marker_color:  The color of the marker.  May be an RGB tuple,
            color name, color index, or `schrodinger.structutils.color` instance.
            If not given, white will be used.
        :type marker_color: tuple, str, int, or `schrodinger.structutils.color`
        :param icon: The icon to draw next to the marker. Should be one the
            Icons.* constants.  If not given, no icon will be drawn.
        :type icon: int
        :param alt_color: The alternate marker color.  This color is always used
            for text, and is used for the marker and icon when `highlight` is True.
            If not given, `color` will be used.
        :type alt_color: tuple, str, int, or `schrodinger.structutils.color`
        :param highlight: Whether the marker should be highlighted.  A
            highlighted marker is indicated with thicker lines and is colored using
            `alt_color` instead of `color`.
        :type highlight: bool
        """
        self._atoms = self._getEntryAtomNumbers([atom])
        self._color = self._colorToRgb(marker_color)
        self._alt_color = self._altColorToRgb(alt_color)
        self._icon = icon
        self._highlight = highlight
        self._handle = None
        self.update() 
    def _createMarker(self):
        atoms = self._getWorkspaceAtomNumbers()
        r, g, b = self._color
        tr, tg, tb = self._getAltColor()
        self._handle = _pymaecxx.mae_create_atom_marker(atoms[0], r, g, b, tr,
                                                        tg, tb, self._highlight,
                                                        self._icon)
[docs]    def setText(self, text):
        raise RuntimeError("Text cannot be set for atom markers")  
[docs]class PairMarker(_BaseMarker):
    """
    Class for marking two atoms. Useful for marking bonds and distances.
    """
[docs]    def __init__(self,
                 atom1,
                 atom2,
                 marker_color=None,
                 icon=Icons.NONE,
                 text="",
                 alt_color=None,
                 highlight=False):
        """
        :param atom1: The first atom to mark
        :type atom1: `schrodinger.structure._StructureAtom`
        :param atom2: The second atom to mark
        :type atom2: `schrodinger.structure._StructureAtom`
        :param marker_color:  The color of the marker.  May be an RGB tuple,
            color name, color index, or `schrodinger.structutils.color` instance.
            If not given, white will be used.
        :type marker_color: tuple, str, int, or `schrodinger.structutils.color`
        :param icon: The icon to draw next to the marker. Should be one the
            Icons.* constants.  If not given, no icon will be drawn.
        :type icon: int
        :param text: The text to display next to the marker.  If not given, no
            text will be displayed.
        :type text: str
        :param alt_color: The alternate marker color.  This color is always used
            for text, and is used for the marker and icon when `highlight` is True.
            If not given, `color` will be used.
        :type alt_color: tuple, str, int, or `schrodinger.structutils.color`
        :param highlight: Whether the marker should be highlighted.  A
            highlighted marker is indicated with thicker lines and is colored using
            `alt_color` instead of `color`.
        :type highlight: bool
        :note: Either an icon or text may be displayed on a marker, but not
            both.  If both are given, only the text will be shown.
        """
        self._atoms = self._getEntryAtomNumbers([atom1, atom2])
        self._color = self._colorToRgb(marker_color)
        self._icon = icon
        self._text = text
        self._alt_color = self._altColorToRgb(alt_color)
        self._highlight = highlight
        self._handle = None
        self.update() 
    def _createMarker(self):
        atoms = self._getWorkspaceAtomNumbers()
        r, g, b = self._color
        tr, tg, tb = self._getAltColor()
        self._handle = _pymaecxx.mae_create_atom_pair_marker(
            atoms[0], atoms[1], r, g, b, tr, tg, tb, self._highlight,
            self._text, self._icon) 
[docs]class TripleMarker(_BaseMarker):
    """
    Class for marking three atoms. Useful for marking bond angles.
    """
[docs]    def __init__(self,
                 atom1,
                 atom2,
                 atom3,
                 marker_color=None,
                 icon=Icons.NONE,
                 text="",
                 alt_color=None,
                 highlight=False):
        """
        :param atom1: The first atom to mark
        :type atom1: `schrodinger.structure._StructureAtom`
        :param atom2: The second atom to mark
        :type atom2: `schrodinger.structure._StructureAtom`
        :param atom3: The third atom to mark
        :type atom3: `schrodinger.structure._StructureAtom`
        :param marker_color:  The color of the marker.  May be an RGB tuple,
            color name, color index, or `schrodinger.structutils.color` instance.
            If not given, white will be used.
        :type marker_color: tuple, str, int, or `schrodinger.structutils.color`
        :param icon: The icon to draw next to the marker. Should be one the
            Icons.* constants.  If not given, no icon will be drawn.
        :type icon: int
        :param text: The text to display next to the marker.  If not given, no
            text will be displayed.
        :type text: str
        :param alt_color: The alternate marker color.  This color is always used
            for text, and is used for the marker and icon when `highlight` is True.
            If not given, `color` will be used.
        :type alt_color: tuple, str, int, or `schrodinger.structutils.color`
        :param highlight: Whether the marker should be highlighted.  A
            highlighted marker is indicated with thicker lines and is colored using
            `alt_color` instead of `color`.
        :type highlight: bool
        :note: Either an icon or text may be displayed on a marker, but not
            both.  If both are given, only the text will be shown.
        """
        self._atoms = self._getEntryAtomNumbers([atom1, atom2, atom3])
        self._color = self._colorToRgb(marker_color)
        self._icon = icon
        self._text = text
        self._alt_color = self._altColorToRgb(alt_color)
        self._highlight = highlight
        self._handle = None
        self.update() 
    def _createMarker(self):
        atoms = self._getWorkspaceAtomNumbers()
        r, g, b = self._color
        tr, tg, tb = self._getAltColor()
        self._handle = _pymaecxx.mae_create_atom_triple_marker(
            atoms[0], atoms[1], atoms[2], r, g, b, tr, tg, tb, self._highlight,
            self._text, self._icon) 
[docs]class QuadMarker(_BaseMarker):
    """
    Class for marking four atoms. Useful for marking dihedral angles.
    """
[docs]    def __init__(self,
                 atom1,
                 atom2,
                 atom3,
                 atom4,
                 marker_color=None,
                 icon=Icons.NONE,
                 text="",
                 alt_color=None,
                 highlight=False):
        """
        :param atom1: The first atom to mark
        :type atom1: `schrodinger.structure._StructureAtom`
        :param atom2: The second atom to mark
        :type atom2: `schrodinger.structure._StructureAtom`
        :param atom3: The third atom to mark
        :type atom3: `schrodinger.structure._StructureAtom`
        :param atom3: The fourth atom to mark
        :type atom3: `schrodinger.structure._StructureAtom`
        :param marker_color:  The color of the marker.  May be an RGB tuple,
            color name, color index, or `schrodinger.structutils.color` instance.
            If not given, white will be used.
        :type marker_color: tuple, str, int, or `schrodinger.structutils.color`
        :param icon: The icon to draw next to the marker. Should be one the
            Icons.* constants.  If not given, no icon will be drawn.
        :type icon: int
        :param text: The text to display next to the marker.  If not given, no
            text will be displayed.
        :type text: str
        :param alt_color: The alternate marker color.  This color is always used
            for text, and is used for the marker and icon when `highlight` is True.
            If not given, `color` will be used.
        :type alt_color: tuple, str, int, or `schrodinger.structutils.color`
        :param highlight: Whether the marker should be highlighted.  A
            highlighted marker is indicated with thicker lines and is colored using
            `alt_color` instead of `color`.
        :type highlight: bool
        :note: Either an icon or text may be displayed on a marker, but not
            both.  If both are given, only the text will be shown.
        """
        self._atoms = self._getEntryAtomNumbers([atom1, atom2, atom3, atom4])
        self._color = self._colorToRgb(marker_color)
        self._icon = icon
        self._text = text
        self._alt_color = self._altColorToRgb(alt_color)
        self._highlight = highlight
        self._handle = None
        self.update() 
    def _createMarker(self):
        atoms = self._getWorkspaceAtomNumbers()
        r, g, b = self._color
        tr, tg, tb = self._getAltColor()
        self._handle = _pymaecxx.mae_create_atom_quad_marker(
            atoms[0], atoms[1], atoms[2], atoms[3], r, g, b, tr, tg, tb,
            self._highlight, self._text, self._icon) 
[docs]class LineMarker(_BaseMarker):
    """
    Class for marking two arbitrary points. Useful for marking lines between
    two graphics3d objects.
    """
[docs]    def __init__(self,
                 coord1,
                 entry_id1,
                 coord2,
                 entry_id2,
                 marker_color=None,
                 alt_color=None,
                 line_width=1):
        """
        :param coord1: The first coordinate to mark
        :type coord1: List of int
        :param coord1: The second coordinate to mark
        :type coord1: List of int
        :param entry_id1: Entry ID the first coordinate is associated with.
                          This is to simplify when the line marker should be
                          shown and hidden.
        :type entry_id1: int
        :param entry_id2: Entry ID the second coordinate is associated with.
        :type entry_id2: int
        :param marker_color:  The color of the marker.  May be an RGB tuple,
            color name, color index, or `schrodinger.structutils.color` instance.
            If not given, white will be used.
        :type marker_color: tuple, str, int, or `schrodinger.structutils.color`
        :param alt_color: The alternate marker color.  This color is always used
            for text, and is used for the marker and icon when `highlight` is True.
            If not given, `color` will be used.
        :type alt_color: tuple, str, int, or `schrodinger.structutils.color`
        :param line_width: Width of the line
        :type line_width: int
        """
        self._color = self._colorToRgb(marker_color)
        self._alt_color = self._altColorToRgb(alt_color)
        self._line_width = line_width
        self._coord1 = coord1
        self._coord2 = coord2
        self._entry_ids = {str(entry_id1), str(entry_id2)}
        self._handle = None
        self.update() 
    def _createMarker(self):
        """
        Create the line marker according to given options.
        """
        r, g, b = self._color
        tr, tg, tb = self._getAltColor()
        x1, y1, z1 = self._coord1
        x2, y2, z2 = self._coord2
        self._handle = _pymaecxx.mae_create_line_marker(x1, y1, z1, x2, y2, z2,
                                                        r, g, b, tr, tg, tb,
                                                        self._line_width)
[docs]    def update(self):
        """
        See parent class documentation.
        Update the marker in response to the workspace contents changing. If
        either one of the entries the marker vertices belong to is no longer
        included in workspace, the marker will be deleted.
        """
        self.hide()
        workspace_eids = maestro.get_included_entry_ids()
        if workspace_eids.issuperset(self._entry_ids):
            self._createMarker()
        else:
            self._handle = None  
[docs]class NotInWorkspaceError(Exception):
    """
    An exception when trying to display a marker covering atoms that are not
    currently in the workspace.
    """ 
    # This class intentionally left blank