#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Tool for drawing beautiful DIN-conform dimensioning arrows
(c) 2012 by Johannes B. Rutzmoser, johannes.rutzmoser (at) googlemail (dot) com

Please contact me, if you know a way how the extension module accepts mouse input; this would help to improve the tool

Add this file and the dimensioning.inx file into the following folder to get the feature run:
UNIX:
$HOME/.config/inkscape/extensions/

Mac OS X (when using the binary):
/Applications/Inkscape.app/Contents/Resources/extensions/
or
/Applications/Inkscape.app/Contents/Resources/share/inkscape/extensions

WINDOWS (Filepath may differ, depending where the program was installed):
C:\Program Files\Inkscape\share\extensions

License:
GNU GENERAL PUBLIC LICENSE

'''

import inkex, cubicsuperpath
import simplestyle
import numpy as np
import gettext
_ = gettext.gettext

def norm(a):
    return a/np.sqrt(np.dot(a, a))

def rotate(tangentvec, point):
    if tangentvec[0] == 0:
        angle = - np.pi/2
    else:
        angle = np.arctan(tangentvec[1]/tangentvec[0])
    return 'rotate(' + str(angle/np.pi*180) + ',' + str(point[0]) + ',' + str(point[1]) + ')'


class Dimensioning(inkex.Effect):
    def __init__(self):
        inkex.Effect.__init__(self)
        # the options given in the dialouge
        self.OptionParser.add_option("--orientation",
                        action="store", type="string",
                        dest="orientation", default='horizontal',
                        help="The type of orientation of the dimensioning (horizontal, vertical or parallel)")
        self.OptionParser.add_option("--arrow_orientation",
                        action="store", type="string",
                        dest="arrow_orientation", default='auto',
                        help="The type of orientation of the arrows")
        self.OptionParser.add_option("--line_scale",
                        action="store", type="float",
                        dest="line_scale", default=1.0,
                        help="Scale factor for the line thickness")
        self.OptionParser.add_option("--overlap",
                        action="store", type="float",
                        dest="overlap", default=1.0,
                        help="Overlap of the helpline over the dimensioning line")
        self.OptionParser.add_option("--distance",
                        action="store", type="float",
                        dest="distance", default=1.0,
                        help="Distance of the helpline to the object")
        self.OptionParser.add_option("--position",
                        action="store", type="float",
                        dest="position", default=1.0,
                        help="position of the dimensioning line")
        self.OptionParser.add_option("--flip",
                        action="store", type="inkbool",
                        dest="flip", default=False,
                        help="flip side")
        self.OptionParser.add_option("--scale_factor",
                        action="store", type="float",
                        dest="scale_factor", default=1.0,
                        help="scale factor for the dimensioning text")
        self.OptionParser.add_option("--unit",
                        action="store", type="string",
                        dest="unit", default='px',
                        help="The unit that should be used for the dimensioning")
        self.OptionParser.add_option("--rotate",
                        action="store", type="inkbool",
                        dest="rotate", default=True,
                        help="Rotate the annotation?")
        self.OptionParser.add_option("--digit",
                        action="store", type="int",
                        dest="digit", default=0,
                        help="number of digits after the point")
        self.OptionParser.add_option("--tab",
                        action="store", type="string",
                        dest="tab", default="sampling",
                        help="The selected UI-tab when OK was pressed")
    def create_linestyles(self):
        '''
        Create the line styles for the drawings.
        '''

        self.helpline_style = {
                        'stroke'        : '#000000',
                        'stroke-width'  : '{}px'.format(0.5*self.options.line_scale),
                        'fill'          : 'none'
                        }
        self.dimline_style = {
                        'stroke'        : '#000000',
                        'stroke-width'  : '{}px'.format(0.75*self.options.line_scale),
                        'fill'          : 'none',
                        'marker-start'  : 'url(#ArrowDIN-start)',
                        'marker-end'    : 'url(#ArrowDIN-end)'
                        }
        self.text_style = {
                        'font-size'     : '{}px'.format(12*self.options.line_scale),
                        'font-family'   : 'Sans',
                        'font-style'    : 'normal',
                        'text-anchor'   : 'middle'
                        }
        self.helpline_attribs = {'style' : simplestyle.formatStyle(self.helpline_style),
                                   inkex.addNS('label', 'inkscape') : 'helpline',
                                   'd' : 'm 0,0 100,0'
                                 }
        self.text_attribs = {'style'     : simplestyle.formatStyle(self.text_style),
                             'x'         : '100',
                             'y'         : '100'
                             }
        self.dimline_attribs = {'style'  : simplestyle.formatStyle(self.dimline_style),
                               inkex.addNS('label','inkscape') : 'dimline',
                               'd' : 'm 0,0 200,0'
                               }

    def getUnittouu(self, param):
        try:
            return inkex.unittouu(param)
        except AttributeError:
            return self.unittouu(param)

    def effect(self):
        # will be executed when feature is activated
        self.create_linestyles()
        self.makeGroup()
        self.getPoints()
        self.calcab()
        self.drawHelpline()
        self.drawDimension()
        self.drawText()

    def makeMarkerstyle(self, name, rotate):
        defs = self.xpathSingle('/svg:svg//svg:defs')
        if defs == None:
            defs = inkex.etree.SubElement(self.document.getroot(),inkex.addNS('defs','svg'))
        marker = inkex.etree.SubElement(defs ,inkex.addNS('marker','svg'))
        marker.set('id', name)
        marker.set('orient', 'auto')
        marker.set('refX', '0.0')
        marker.set('refY', '0.0')
        marker.set('style', 'overflow:visible')
        marker.set(inkex.addNS('stockid','inkscape'), name)

        arrow = inkex.etree.Element("path")
        # messy but works; definition of arrows in beautiful DIN-shapes:
        if name.startswith('ArrowDIN-'):
            if rotate:
                arrow.set('d', 'M 8,0 -8,2.11 -8,-2.11 z')
            else:
                arrow.set('d', 'M -8,0 8,-2.11 8,2.11 z')
        if name.startswith('ArrowDINout-'):
            if rotate:
                arrow.set('d', 'M 0,0 16,2.11 16,0.5 26,0.5 26,-0.5 16,-0.5 16,-2.11 z')
            else:
                arrow.set('d', 'M 0,0 -16,2.11 -16,0.5 -26,0.5 -26,-0.5 -16,-0.5 -16,-2.11 z')

        arrow.set('style', 'fill:#000000;stroke:none')
        marker.append(arrow)


    def makeGroup(self):
        '''puts everything of the dimensioning in a group'''
        layer = self.current_layer
        # Group in which the object should be put into
        grp_name = 'dimensioning'
        grp_attributes = {inkex.addNS('label', 'inkscape') :  grp_name}
        self.grp = inkex.etree.SubElement(layer, 'g', grp_attributes)

    def getPoints(self):
        self.p1 = np.array([0.,100.])
        self.p1 = np.array([100.,100.])
        # Get variables of a selected object
        for id, node in self.selected.iteritems():
            # if it is a path:
            if node.tag == inkex.addNS('path', 'svg'):
                d = node.get('d')
                p = cubicsuperpath.parsePath(d)
                # p has all nodes with the anchor points in a list;
                # the rule is [anchorpoint, node, anchorpoint]
                # the points are lists with x and y coordinate
                self.p1 = np.array(p[0][0][1])
                self.p2 = np.array(p[0][-1][1])


    def calcab(self):
        # get p1,p2 ordered for correct dimension direction
        # determine quadrant
        if self.p1[0] <= self.p2[0]:
            if self.p1[1] <= self.p2[1]:
                quad = 1 # p1 is left,up of p2
            else:  quad = 2 # p1 is left,down of p2
        elif self.p1[1] <= self.p2[1]:
            quad = 3 # p1 is right,up of p2
        else: quad = 4 # p1 is right,down of p2
        swap = False if quad ==1 else True
        minp = self.p2 if swap else self.p1
        maxp = self.p1 if swap else self.p2
        # distance between points
        delta = maxp - minp
        # rotation matrix
        rotateMat = np.array([[0,-1],[1,0]])
        # compute the unit vectors e1 and e2 along the cartesian coordinates of the dimension
        if self.options.orientation == 'horizontal':
            if quad == 3: self.e1 = np.array([1.0, 0.0])
            else: self.e1 = np.array([-1.0, 0.0])
        if self.options.orientation == 'vertical':
            if quad == 2:
                self.e1 = np.array([0.0, -1.0])
            else: self.e1 = np.array([0.0, 1.0])
        if self.options.orientation == 'parallel':
            self.e1 = norm(delta)
            #if quad==2 or quad==3: self.e1 *= -1
        self.e2 = np.dot(rotateMat, self.e1)
        if self.options.flip:
            self.e2 *= -1.
        # compute the points a and b, where the dimension line arrow spikes start and end
        dist = self.options.position*self.e2
        if self.options.flip:
            outpt = maxp
            delta *= -1
            if swap:
                self.a = outpt + dist
                self.b = self.a + self.e1*np.dot(self.e1,delta)
            else:
                self.b = outpt + dist
                self.a = self.b + self.e1*np.dot(self.e1,delta)
        else:
            outpt = minp
            if swap:
                self.b = outpt + dist
                self.a = self.b + self.e1*np.dot(self.e1,delta)
            else:
                self.a = outpt + dist
                self.b = self.a + self.e1*np.dot(self.e1,delta)


    def drawHelpline(self):
        # manipulate the start- and endpoints with distance and overlap
        h1_start = self.p1 + norm(self.a - self.p1)*self.options.distance
        h1_end = self.a + norm(self.a - self.p1)*self.options.overlap
        h2_start = self.p2 + norm(self.b - self.p2)*self.options.distance
        h2_end = self.b + norm(self.b - self.p2)*self.options.overlap

        # print the lines
        hline1 = inkex.etree.SubElement(self.grp, inkex.addNS('path', 'svg'), self.helpline_attribs)
        hline1.set('d', 'M %f,%f %f,%f' % (h1_start[0], h1_start[1],h1_end[0],h1_end[1],))

        hline2 = inkex.etree.SubElement(self.grp, inkex.addNS('path', 'svg'), self.helpline_attribs)
        hline2.set('d', 'M %f,%f %f,%f' % (h2_start[0], h2_start[1],h2_end[0],h2_end[1],))

    def setMarker(self, option):
        if option=='inside':
            # inside
            self.arrowlen = 6.0 * self.options.line_scale
            self.dimline_style['marker-start'] = 'url(#ArrowDIN-start)'
            self.dimline_style['marker-end'] = 'url(#ArrowDIN-end)'
            self.makeMarkerstyle('ArrowDIN-start', False)
            self.makeMarkerstyle('ArrowDIN-end', True)
        else:
            # outside
            self.arrowlen = 0
            self.dimline_style['marker-start'] = 'url(#ArrowDINout-start)'
            self.dimline_style['marker-end'] = 'url(#ArrowDINout-end)'
            self.makeMarkerstyle('ArrowDINout-start', False)
            self.makeMarkerstyle('ArrowDINout-end', True)
        self.dimline_attribs['style'] = simplestyle.formatStyle(self.dimline_style)

    def drawDimension(self):
        # critical length, when inside snaps to outside
        critical_length = 35 * self.options.line_scale
        if self.options.arrow_orientation == 'auto':
            if np.abs(np.dot(self.e1, self.b - self.a)) > critical_length:
                self.setMarker('inside')
            else:
                self.setMarker('outside')
        else:
            self.setMarker(self.options.arrow_orientation)
        # start- and endpoint of the dimension line
        dim_start = self.a + self.arrowlen*norm(self.b - self.a)
        dim_end = self.b - self.arrowlen*norm(self.b - self.a)
        # print
        dimline = inkex.etree.SubElement(self.grp, inkex.addNS('path', 'svg'), self.dimline_attribs)
        dimline.set('d', 'M %f,%f %f,%f' % (dim_start[0], dim_start[1], dim_end[0], dim_end[1]))

    def drawText(self):
        # distance of text to the dimension line
        self.textdistance = 5.0 * self.options.line_scale
        if self.e2[1] > 0:
            textpoint = (self.a + self.b)/2 - self.e2*self.textdistance
        elif self.e2[1] == 0:
            textpoint = (self.a + self.b)/2 - np.array([1,0])*self.textdistance
        else:
            textpoint = (self.a + self.b)/2 + self.e2*self.textdistance

        value = np.abs(np.dot(self.e1, self.b - self.a)) / (self.getUnittouu(str(self.options.scale_factor)+self.options.unit))
        string_value = str(round(value, self.options.digit))
        # chop off last characters if digit is zero or negative
        if self.options.digit <=0:
            string_value = string_value[:-2]
        text = inkex.etree.SubElement(self.grp, inkex.addNS('text', 'svg'), self.text_attribs)
        # The alternative for framing with dollars, when LATEX Math export is seeked
        # text.text = '$' + string_value + '$'
        text.text = string_value
        text.set('x', str(textpoint[0]))
        text.set('y', str(textpoint[1]))
        if self.options.rotate:
            text.set('transform', rotate(self.e1, textpoint))

# call the object function
if __name__ == '__main__':
    a = Dimensioning()
    a.affect()
