# Author: Takanori Nakane
# License: BSD-2 Clause
# Version: 0.3.20120830

'''
Dynamic Mesh

This script was tested on PyMOL 1.2 and 1.5.

Example:

run dynamic_mesh.py
fetch 1hwk, async=0
fetch 1hwk, 1hwk_map, type=2fofc, async=0
dynamic_mesh 1hwk_map, sym_source=1hwk
show sticks, resn 117
show ribbon
zoom chain A and resn 117

Note: On PyMOL <= 1.4, you have to download the electron density
map from the Uppsala Electron Density Server manually.
'''

from __future__ import print_function
from pymol.callback import Callback
from pymol import cmd
from chempy import cpv


class DynamicMesh(Callback):

    def __init__(self, map_name, level, radius, name, sym_source):
        self.level = level
        self.radius = radius
        self.map_name = map_name
        self.name = name
        self.center_name = cmd.get_unused_name('_center')
        self.callback_name = cmd.get_unused_name('_cb')

        cmd.set("auto_zoom", 0)
        cmd.pseudoatom(self.center_name)
        cmd.hide("everything", self.center_name)

        symmetry = cmd.get_symmetry(sym_source or map_name)
        if symmetry:
            cmd.set("map_auto_expand_sym", 1)
            cmd.set_symmetry(self.center_name, *symmetry)

        cmd.set_key("pgup", self.contour_plus)
        cmd.set_key("pgdn", self.contour_minus)

        self.update()

    def load(self):
        cmd.load_callback(self, self.callback_name)

    def contour_plus(self, d=0.1):
        self.level += d
        print("Map level: " + str(self.level))
        self.update()

    def contour_minus(self):
        if self.level < 0.15:
            return
        self.contour_plus(-0.1)

    def update(self):
        self.center = cmd.get_position()
        cmd.alter_state(0, self.center_name, "(x, y, z) = p", space={'p': self.center})
        cmd.isomesh(self.name, self.map_name, self.level, self.center_name, carve=self.radius)

    def __call__(self):
        if self.name not in cmd.get_names('objects'):
            cmd.delete(self.callback_name)
            cmd.set_key("pgup", lambda: None)
            cmd.set_key("pgdn", lambda: None)
            return

        tmp = cmd.get_position()
        r = cpv.distance_sq(self.center, tmp)
        if (r > 0.3):  # increase this number if it is too slow
            self.update()

    def get_extent(self):
        tmp = cmd.get_position()
        return [[i - self.radius for i in tmp], [i + self.radius for i in tmp]]


def dynamic_mesh(map_name, level=1.0, radius=8, name='dynamic_mesh', sym_source=None):
    '''
DESCRIPTION

    Make 'dynamic' mesh from volumetric data such as electron density map.
    The mesh will dynamically follow the center of the view.
    Contour level of isomesh can be changed by PageDown and PageUp keys.

    NOTE: Crystallographic operations can be applied to the map.

USAGE

    dynamic_mesh map_name [, level [, radius [, name [, sym_source ]]]]

ARGUMENTS

    map_name = string: name of volumetric object(map) to display

    level = float: contour level of isomesh {default: 1.0}

    radius = float: radius of isomesh around the center of the view {default: 8}

    name = string: name of mesh object {default: dynamic_mesh}

    sym_source = string: name of object from which symmetry
                         information is derived {default: map_name}

EXAMPLE

    fetch 1hwk, async=0
    fetch 1hwk, 1hwk_map, type=2fofc, async=0
    dynamic_mesh 1hwk_map

SEE ALSO

    isomesh
    '''
    cmd.delete(name)
    callback = DynamicMesh(map_name, float(level), float(radius), name, sym_source)
    callback.load()

cmd.extend('dynamic_mesh', dynamic_mesh)
cmd.auto_arg[0]['dynamic_mesh'] = cmd.auto_arg[1]['isomesh']