#!/usr/bin/python2

# GladeVcp Widget - DRO widget, showing all 3 reference types
# This widgets displays linuxcnc axis position information.
#
# Copyright (c) 2013 Norbert Schechner
# Based on the drowidget from Chris Morley
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.


import gtk
import gobject
import os
import sys
import pango
import math
import linuxcnc

# constants
_INCH = 0
_MM = 1
_AXISLETTERS = ["X", "Y", "Z", "A", "B", "C", "U", "V", "W"]


# we put this in a try so there is no error in the glade editor
# linuxcnc is probably not running then
try:
    INIPATH = os.environ['INI_FILE_NAME']
except:
#    INIPATH = '/home/emcmesa/linuxcnc-dev/configs/sim/gmoccapy/gmoccapy.ini'
    pass

# This is the main class
class Combi_DRO(gtk.VBox):
    '''
    Combi_DRO will display an linuxcnc DRO with all three types at ones

    Combi_DRO = Combi_DRO(joint_number)
    joint_number is an integer in the range from 0 to 8
    where 0 = X, 1 = Y, 2 = Z, etc.
    '''

    __gtype_name__ = 'Combi_DRO'
    __gproperties__ = {
        'joint_number' : (gobject.TYPE_INT, 'Joint Number', '0:X  1:Y  2:Z  etc',
                    0, 8, 0, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
        'actual' : (gobject.TYPE_BOOLEAN, 'Actual Position', 'Display Actual or Commanded Position',
                    True, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
        'metric_units' : (gobject.TYPE_BOOLEAN, 'Display in metric units', 'Display in metric or not',
                    True, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
        'auto_units' : (gobject.TYPE_BOOLEAN, 'Change units according gcode', 'Units will toggle between metric and imperial according to gcode.',
                    True, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
        'diameter' : (gobject.TYPE_BOOLEAN, 'Diameter Adjustment', 'Display Position As Diameter',
                    False, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
        'mm_text_template' : (gobject.TYPE_STRING, 'Text template for Metric Units',
                'Text template to display. Python formatting may be used for one variable',
                "%10.3f", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
        'imperial_text_template' : (gobject.TYPE_STRING, 'Text template for Imperial Units',
                'Text template to display. Python formatting may be used for one variable',
                "%9.4f", gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
        'homed_color' : (gtk.gdk.Color.__gtype__, 'homed color', 'Sets the color of the display when the axis is homed',
                        gobject.PARAM_READWRITE),
        'unhomed_color' : (gtk.gdk.Color.__gtype__, 'unhomed color', 'Sets the color of the display when the axis is not homed',
                        gobject.PARAM_READWRITE),
        'abs_color' : (gtk.gdk.Color.__gtype__, 'Absolute color', 'Sets the color of the display when absolute coordinates are used',
                        gobject.PARAM_READWRITE),
        'rel_color' : (gtk.gdk.Color.__gtype__, 'Relative color', 'Sets the color of the display when relative coordinates are used',
                        gobject.PARAM_READWRITE),
        'dtg_color' : (gtk.gdk.Color.__gtype__, 'DTG color', 'Sets the color of the display when dtg coordinates are used',
                        gobject.PARAM_READWRITE),
        'font_size' : (gobject.TYPE_INT, 'Font Size', 'The font size of the big numbers, the small ones will be 2.5 times smaler',
                    8, 96, 25, gobject.PARAM_READWRITE | gobject.PARAM_CONSTRUCT),
    }
    __gproperties = __gproperties__

    __gsignals__ = {
                    'clicked': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gobject.TYPE_PYOBJECT)),
                    'units_changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN,)),
                    'system_changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)),
                    'exit': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
                   }

    # Init the class
    def __init__(self, joint_number = 0):
        super(Combi_DRO, self).__init__()

        # get the necesarry connextions to linuxcnc
        self.joint_number = joint_number
        self.linuxcnc = linuxcnc
        self.status = linuxcnc.stat()

        # set some default values'
        self._ORDER = ["Rel", "Abs", "DTG"]
        self.system = "Rel"
        self.homed = False
        self.homed_color = gtk.gdk.Color("green")
        self.unhomed_color = gtk.gdk.Color("red")
        self.abs_color = gtk.gdk.Color("blue")
        self.rel_color = gtk.gdk.Color("black")
        self.dtg_color = gtk.gdk.Color("yellow")
        self.mm_text_template = "%10.3f"
        self.imperial_text_template = "%9.4f"
        self.font_size = 25
        self.metric_units = True
        self.machine_units = _MM
        self.unit_convert = 1
        self._auto_units = True

        # Make the GUI and connect signals
        self.eventbox = gtk.EventBox()
        self.eventbox.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("black"))
        self.add(self.eventbox)
        vbox_main = gtk.VBox(False, 0)
        self.eventbox.add(vbox_main)
        hbox_up = gtk.HBox(False, 0)
        vbox_main.pack_start(hbox_up)
        attr = self._set_attributes((0, 0, 0), (65535, 0, 0), (self.font_size * 1000, 0, -1), (600, 0, -1))
        self.lbl_axisletter = gtk.Label(_AXISLETTERS[self.joint_number])
        self.lbl_axisletter.set_attributes(attr)
        hbox_up.pack_start(self.lbl_axisletter, False, False)
        vbox_ref_type = gtk.VBox(False, 0)
        hbox_up.pack_start(vbox_ref_type, False, False)
        lbl_space = gtk.Label("")
        vbox_ref_type.pack_start(lbl_space)
        attr = self._set_attributes((0, 0, 0), (65535, 0, 0), (int(self.font_size * 1000 / 2.5), 0, -1), (600, 0, -1))
        self.lbl_sys_main = gtk.Label(self.system)
        vbox_ref_type.pack_start(self.lbl_sys_main, False, False)
        self.lbl_sys_main.set_attributes(attr)
        self.main_dro = gtk.Label("9999.999")
        hbox_up.pack_start(self.main_dro)
        self.main_dro.set_alignment(1.0, 0.5)
        attr = self._set_attributes((0, 0, 0), (65535, 0, 0), (self.font_size, 0, -1), (600, 0, -1))
        self.main_dro.set_attributes(attr)
        hbox_down = gtk.HBox(False, 5)
        vbox_main.pack_start(hbox_down)
        self.lbl_sys_left = gtk.Label("Abs")
        hbox_down.pack_start(self.lbl_sys_left)
        attr = self._set_attributes((0, 0, 0), (65535, 0, 0), (int(self.font_size * 1000 / 2.5), 0, -1), (600, 0, -1))
        self.lbl_sys_left.set_attributes(attr)
        self.dro_left = gtk.Label("-11.111")
        hbox_down.pack_start(self.dro_left)
        self.dro_left.set_alignment(1.0, 0.5)
        self.dro_left.set_attributes(attr)
        self.lbl_sys_right = gtk.Label("DTG")
        hbox_down.pack_start(self.lbl_sys_right)
        self.lbl_sys_right.set_attributes(attr)
        self.dro_right = gtk.Label("22.222")
        hbox_down.pack_start(self.dro_right)
        self.dro_right.set_alignment(1.0, 0.5)
        self.dro_right.set_attributes(attr)

        self.eventbox.connect("button_press_event", self._on_eventbox_clicked)

        self.show_all()

        # add the timer at a period of 100 ms
        gobject.timeout_add(100, self._periodic)

        # This try is only needed because while working with glade
        # linuxcnc may not be working
        try:
            self.inifile = self.linuxcnc.ini(INIPATH)
            # check the ini file if UNITS are set to mm"
            # first check the global settings
            units = self.inifile.find("TRAJ", "LINEAR_UNITS")
            if units == None:
                # else then the X axis units
                units = self.inifile.find("AXIS_0", "UNITS")
        except:
            units = "inch"

        if units == "mm" or units == "metric" or units == "1.0":
            self.machine_units = _MM
        else:
            self.machine_units = _INCH

    # make an pango attribute to be used with several labels
    def _set_attributes(self, bgcolor, fgcolor, size, weight):
        attr = pango.AttrList()
        bg_color = pango.AttrBackground(bgcolor[0], bgcolor[1], bgcolor[2], 0, -1)
        attr.insert(bg_color)
        size_attr = pango.AttrSize(size[0], size[1], size[2])
        attr.insert(size_attr)
        weight_attr = pango.AttrWeight(weight[0], weight[1], weight[2])
        attr.insert(weight_attr)
        fg_color = pango.AttrForeground(fgcolor[0], fgcolor[1], fgcolor[2], 0, 11)
        attr.insert(fg_color)
        return attr

    # if the eventbox has been clicked, we like to toggle the DRO's
    def _on_eventbox_clicked(self, widget, event):
        self.toogle_readout()

    # Get propertys
    def do_get_property(self, property):
        name = property.name.replace('-', '_')
        if name in self.__gproperties.keys():
            return getattr(self, name)
        else:
            raise AttributeError('unknown property %s' % property.name)

    # Set propertys
    def do_set_property(self, property, value):
        try:
            name = property.name.replace('-', '_')
            if name in self.__gproperties.keys():
                setattr(self, name, value)
                self.queue_draw()
                if name in ('mm_text_template', 'imperial_text_template'):
                    try:
                        v = value % 0.0
                    except Exception, e:
                        print "Invalid format string '%s': %s" % (value, e)
                        return False
                if name == "homed_color":
                    self.homed_color = value
                    self._set_labels()
                if name == "unhomed_color":
                    self.unhomed_color = value
                    self._set_labels()
                if name == "abs_color":
                    self.abs_color = value
                    self._set_labels()
                if name == "rel_color":
                    self.rel_color = value
                    self._set_labels()
                if name == "dtg_color":
                    self.dtg_color = value
                    self._set_labels()
                if name == "auto_units":
                    self._auto_units = value
                    self._set_labels()
                if name == "joint_number":
                    self.joint_number = value
                    self.change_axisletter(_AXISLETTERS[self.joint_number])
                if name == "font_size":
                    self.font_size = value
                    self._set_labels()
                if name in ('metric_units', 'actual', 'diameter'):
                    setattr(self, name, value)
                    self.queue_draw()
            else:
                raise AttributeError('unknown property %s' % property.name)
        except:
            pass

    # get the actual coordinate system to display it on the DRO
    def _get_current_system(self):
            gcode = self.status.gcodes[1:]
            for code in gcode:
                if code >= 540 and code <= 590:
                    return "G%s" % (code / 10)
                elif code > 590 and code <= 593:
                    return "G%s" % (code / 10.0)
            return "Rel"

    # Get the units used according to gcode
    def _get_current_units(self):
            gcode = self.status.gcodes[1:]
            for code in gcode:
                if code >= 200 and code <= 210:
                    return (code / 10)
            return False

    # update the labels
    def _set_labels(self):
        if self._ORDER[0] == "Rel":
            self.lbl_sys_main.set_text(self._get_current_system())
        else:
            self.lbl_sys_main.set_text(self._ORDER[0])
        if self._ORDER[1] == "Rel":
            self.lbl_sys_left.set_text(self._get_current_system())
        else:
            self.lbl_sys_left.set_text(self._ORDER[1])
        if self._ORDER[2] == "Rel":
            self.lbl_sys_right.set_text(self._get_current_system())
        else:
            self.lbl_sys_right.set_text(self._ORDER[2])

        if self._ORDER[0] == "Abs":
            bg_color = self.abs_color
        elif self._ORDER[0] == "DTG":
            bg_color = self.dtg_color
        else:
            bg_color = self.rel_color
        self.eventbox.modify_bg(gtk.STATE_NORMAL, bg_color)
        bg_color = self._convert_to_rgb(bg_color)
        if self.homed:
            fg_color = self.homed_color
        else:
            fg_color = self.unhomed_color
        fg_color = self._convert_to_rgb(fg_color)
        attr = self._set_attributes(bg_color, fg_color, (int(self.font_size * 1000 / 2.5), 0, -1), (600, 0, -1))
        self.lbl_sys_main.set_attributes(attr)
        self.lbl_sys_left.set_attributes(attr)
        self.lbl_sys_right.set_attributes(attr)
        self.dro_left.set_attributes(attr)
        self.dro_right.set_attributes(attr)
        attr = self._set_attributes(bg_color, fg_color, (self.font_size * 1000, 0, -1), (600, 0, -1))
        self.main_dro.set_attributes(attr)
        self.lbl_axisletter.set_attributes(attr)

        self.system = self._get_current_system()

    # returns the separate RGB color numbers from the color widget
    def _convert_to_rgb(self, spec):
        color = spec.to_string()
        temp = color.strip("#")
        r = temp[0:4]
        g = temp[4:8]
        b = temp[8:]
        return (int(r, 16), int(g, 16), int(b, 16))

    # periodic call to update the positions, every 100 ms
    def _periodic(self):
        try:
            self.status.poll()
            main, left, right = self._position()
            if self.system != self._get_current_system():
                self._set_labels()
                self.emit("system_changed", self._get_current_system())
            if self.homed != self.status.homed[self.joint_number]:
                self.homed = self.status.homed[self.joint_number]
                self._set_labels()
            if (self._get_current_units() == 20 and self.metric_units) or (self._get_current_units() == 21 and not self.metric_units):
                if self._auto_units:
                    self.metric_units = not self.metric_units
                self.emit("units_changed", self.metric_units)
        except:
            sys = 0
            main = 9999.999
            left = 10.123
            right = 0.000

        if self.metric_units:
            tmpl = lambda s: self.mm_text_template % s
        else:
            tmpl = lambda s: self.imperial_text_template % s

        if self.diameter:
            scale = 2.0
        else:
            scale = 1.0
        main_dro = tmpl(main * scale)
        left_dro = tmpl(left * scale)
        right_dro = tmpl(right * scale)
        self.main_dro.set_text(main_dro)
        self.dro_left.set_text(left_dro)
        self.dro_right.set_text(right_dro)
        return True

    # calculate the positions to display
    def _position(self):
        if self.actual:
            p = self.status.actual_position
        else:
            p = self.status.position
        dtg = self.status.dtg[self.joint_number]

        abs_pos = p[self.joint_number]

        rel_pos = p[self.joint_number] - self.status.g5x_offset[self.joint_number] - self.status.tool_offset[self.joint_number]

        if self.status.rotation_xy != 0:
            t = math.radians(-self.status.rotation_xy)
            x = p[0] - self.status.g5x_offset[0] - self.status.tool_offset[0]
            y = p[1] - self.status.g5x_offset[1] - self.status.tool_offset[1]
            if self.joint_number == 0:
                rel_pos = x * math.cos(t) - y * math.sin(t)
            if self.joint_number == 1:
                rel_pos = x * math.sin(t) + y * math.cos(t)

        rel_pos -= self.status.g92_offset[self.joint_number]

        if self.metric_units and self.machine_units == _INCH:
            if self.joint_number not in (3, 4, 5):
                abs_pos = abs_pos * 25.4
                rel_pos = rel_pos * 25.4
                dtg = dtg * 25.4

        if not self.metric_units and self.machine_units == _MM:
            if self.joint_number not in (3, 4, 5):
                abs_pos = abs_pos / 25.4
                rel_pos = rel_pos / 25.4
                dtg = dtg / 25.4

        if self._ORDER == ["Rel", "Abs", "DTG"]:
            return rel_pos, abs_pos, dtg
        if self._ORDER == ["DTG", "Rel", "Abs"]:
            return dtg, rel_pos, abs_pos
        if self._ORDER == ["Abs", "DTG", "Rel"]:
            return abs_pos, dtg, rel_pos

    # sets the DRO explicity to inch or mm
    # attentions auto_units takes also effekt on that!
    def set_to_inch(self, state):
        '''
        sets the DRO to show imperial units

        Combi_DRO.set_to_inch(state)

        state = boolean (true or False)
        '''
        if state:
            self.metric_units = False
        else:
            self.metric_units = True

    # If auto_units is true, the DRO will change according to the
    # active gcode (G20 / G21)
    def set_auto_units(self, state):
        '''
        if True the DRO will change units according to active gcode (G20 / G21)

        Combi_DRO.set_auto_units(state)

        state = boolean (true or False)
        '''
        self._auto_units = state

    # Set the axis to diameter mode, the DRO value will be
    # multiplied by 2
    def set_to_diameter(self, state):
        '''
        if True the DRO will show the diameter not the radius, specialy needed for lathes
        the DRO value will be multiplied by 2

        Combi_DRO.set_to_diameter(state)

        state = boolean (true or False)

        '''
        self.diameter = state

    # this will toggle the DRO around, mainly used to mantain all DRO
    # at the same state, because a click on one will only change that DRO
    # This can be used to change also the others
    def toogle_readout(self):
        '''
        toggles the order of the DRO in the widget

        Combi_DRO.toggle_readout()

        '''
        self._ORDER = [self._ORDER[2], self._ORDER[0], self._ORDER[1]]
        self._set_labels()
        self.emit("clicked", self.joint_number, self._ORDER)

    # You can change the automatic given axisletter using this funktion
    # i.e. to use an axis as R or D insteadt of X on a lathe
    def change_axisletter(self, letter):
        '''
        changes the automaticaly given axisletter
        very usefull to change an lathe DRO from X to R or D

        Combi_DRO.change_axisletter(letter)

        letter = string

        '''
        self.lbl_axisletter.set_text(letter)

    # returns the order of the DRO, mainly used to mantain them consistent
    # the order will also be transmitted with the clicked signal
    def get_order(self):
        '''
        returns the order of the DRO in the widget mainly used to mantain them consistent
        the order will also be transmitted with the clicked signal

        Combi_DRO.get_order()

        returns a list containing the order
        '''
        return self._ORDER

    # sets the order of the DRO, mainly used to mantain them consistent
    def set_order(self, order):
        '''
        sets the order of the DRO, mainly used to mantain them consistent

        Combi_DRO.set_order(order)

        order = list object, must be one of
                ["Rel", "Abs", "DTG"]
                ["DTG", "Rel", "Abs"]
                ["Abs", "DTG", "Rel"]
        '''
        self._ORDER = order
        self._set_labels()

    # This will return the position information of all three DRO
    # it will be in the order Abs, Rel, DTG
    def get_position(self):
        '''
        returns the positions of the DRO

        Combi_DRO.get_position()

        returns the position of the DRO as a list of floats
        the order is independent of the order shown on the DRO
        and will be givven as [Absolute , relative , DTG]

        Absolute = the machine coordinates, depends on the actual property
                   will give actual or commanded position
        Relative = will be the coordinates of the actual coordinate system
        DTG = the distance to go, will mosltly be 0, as this function should not be used
              while the machine is moving, because of time delays
        '''
        positions = self._position()
        if self._ORDER == ["Rel", "Abs", "DTG"]:
            return positions[1], positions[0], positions[2]
        if self._ORDER == ["DTG", "Rel", "Abs"]:
            return positions[2], positions[1], positions[0]
        if self._ORDER == ["Abs", "DTG", "Rel"]:
            return positions[0], positions[2], positions[1]

# for testing without glade editor:
# to show some behavior and setting options
def main():
    window = gtk.Window(gtk.WINDOW_TOPLEVEL)

    vbox = gtk.VBox(False, 5)
    MDRO_X = Combi_DRO(0)
    MDRO_Y = Combi_DRO(1)
    MDRO_Z = Combi_DRO(2)
    MDRO_C = Combi_DRO(5)

    vbox.add(MDRO_X)
    vbox.add(MDRO_Y)
    vbox.add(MDRO_Z)
    vbox.add(MDRO_C)
    window.add(vbox)

    window.connect("destroy", gtk.main_quit)
    MDRO_X.connect("clicked", clicked)
    MDRO_Y.connect("clicked", clicked)
    MDRO_Y.set_auto_units(False)
    MDRO_Y.set_to_inch(True)
#    MDRO_Y.set_to_diameter(True)
#    MDRO_Y.set_property('joint_number',0)
#    MDRO_Y.change_axisletter("D")
    MDRO_Z.connect("clicked", clicked)
    MDRO_C.connect("clicked", clicked)
    MDRO_C.set_property('mm_text_template', '%10.2f')
    MDRO_C.set_property('imperial_text_template', '%10.2f')
    window.show_all()
    gtk.main()

def clicked(self, axis_number, order):
    '''
    This signal will be emitted if the user clicked on the DRO

    axis_number = the joint number of the widget
    order = the actual order of the DRO in the widget
    '''
    print("Klick recieved from ", axis_number)
    print("Order = ", order)
    print(self.get_position())
#    self.set_property("joint_number", 0)
    # other_widget.set_order(order)
    # so they may be mantained consistent

if __name__ == "__main__":
    main()
