#***************************************************************************
#*                                                                         *
#*   Copyright (c) 2018 kbwbe                                              *
#*                                                                         *
#*   Portions of code based on hamish's assembly 2                         *
#*                                                                         *
#*   This program is free software; you can redistribute it and/or modify  *
#*   it under the terms of the GNU Lesser General Public License (LGPL)    *
#*   as published by the Free Software Foundation; either version 2 of     *
#*   the License, or (at your option) any later version.                   *
#*   for detail see the LICENCE text file.                                 *
#*                                                                         *
#*   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 Library General Public License for more details.                  *
#*                                                                         *
#*   You should have received a copy of the GNU Library General Public     *
#*   License along with this program; if not, write to the Free Software   *
#*   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
#*   USA                                                                   *
#*                                                                         *
#***************************************************************************

import FreeCAD
import FreeCADGui
from FreeCAD import Base
import  Part
from PySide import QtGui
from PySide import QtCore
import os
import sys
import copy
import platform
import numpy

PYVERSION =  sys.version_info[0]

preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/A2plus")

USE_PROJECTFILE = preferences.GetBool('useProjectFolder', False)
PARTIAL_PROCESSING_ENABLED = preferences.GetBool('usePartialSolver', True)
AUTOSOLVE_ENABLED = preferences.GetBool('autoSolve', True)
RELATIVE_PATHES_ENABLED = preferences.GetBool('useRelativePathes',True)
FORCE_FIXED_POSITION = preferences.GetBool('forceFixedPosition',True)
SHOW_CONSTRAINTS_ON_TOOLBAR= preferences.GetBool('showConstraintsOnToolbar',True)
RECURSIVE_UPDATE_ENABLED = preferences.GetBool('enableRecursiveUpdate',False)
USE_SOLID_UNION = preferences.GetBool('useSolidUnion',True)

# if SIMULATION_STATE == True assemblies are solved with less accuracy
SIMULATION_STATE = False

SAVED_TRANSPARENCY = []

DEBUGPROGRAM = 1

path_a2p = os.path.dirname(__file__)
path_a2p_resources = os.path.join( path_a2p, 'GuiA2p', 'Resources', 'resources.rcc')
resourcesLoaded = QtCore.QResource.registerResource(path_a2p_resources)
assert resourcesLoaded



wb_globals = {}

RED = (1.0,0.0,0.0)
GREEN = (0.0,1.0,0.0)
BLUE = (0.0,0.0,1.0)
YELLOW = (1.0,1.0,0.0)
WHITE = (1.0,1.0,1.0)
BLACK = (0.0,0.0,0.0)

# DEFINE DEBUG LEVELS FOR CONSOLE OUTPUT
A2P_DEBUG_NONE      = 0
A2P_DEBUG_1         = 1
A2P_DEBUG_2         = 2
A2P_DEBUG_3         = 3

A2P_DEBUG_LEVEL = A2P_DEBUG_NONE

PARTIAL_SOLVE_STAGE1 = 1    #solve all rigid fully constrained to tempfixed rigid, enable only involved dep, then set them as tempfixed
CONSTRAINT_DIALOG_REF = None
CONSTRAINT_EDITOR__REF = None
CONSTRAINT_VIEWMODE = False


# This Icon map is necessary to show correct icons within very old assemblies
A2P_CONSTRAINTS_ICON_MAP = {
    # constraintType:       iconPath
    'pointIdentity':        ':/icons/a2p_PointIdentity.svg',
    'pointOnLine':          ':/icons/a2p_PointOnLineConstraint.svg',
    'pointOnPlane':         ':/icons/a2p_PointOnPlaneConstraint.svg',
    'circularEdge':         ':/icons/a2p_CircularEdgeConstraint.svg',
    'axial':                ':/icons/a2p_AxialConstraint.svg',
    'axisParallel':         ':/icons/a2p_AxisParallelConstraint.svg',
    'axisPlaneParallel':    ':/icons/a2p_AxisPlaneParallelConstraint.svg',
    'axisPlaneNormal':      ':/icons/a2p_AxisPlaneNormalConstraint.svg',
    'axisPlaneAngle':       ':/icons/a2p_AxisPlaneAngleConstraint.svg',
    'planesParallel':       ':/icons/a2p_PlanesParallelConstraint.svg',
    'plane':                ':/icons/a2p_PlaneCoincidentConstraint.svg',
    'angledPlanes':         ':/icons/a2p_AngleConstraint.svg',
    'sphereCenterIdent':    ':/icons/a2p_SphericalSurfaceConstraint.svg',
    'CenterOfMass':         ':/icons/a2p_CenterOfMassConstraint.svg'
}


#------------------------------------------------------------------------------
# Detect the operating system...
#------------------------------------------------------------------------------
tmp = platform.system()
tmp = tmp.upper()
tmp = tmp.split(' ')

OPERATING_SYSTEM = 'UNKNOWN'
if "WINDOWS" in tmp:
    OPERATING_SYSTEM = "WINDOWS"
elif "LINUX" in tmp:
    OPERATING_SYSTEM = "LINUX"
else:
    OPERATING_SYSTEM = "OTHER"



#------------------------------------------------------------------------------
def to_bytes(tx):
    if PYVERSION > 2:
        if isinstance(tx, str):
            value = tx.encode("utf-8")
        else:
            value = tx
    else:
        if isinstance(tx,unicode):
            value = tx.encode("utf-8")
        else:
            value = tx
    return value # Instance of bytes
#------------------------------------------------------------------------------
def to_str(tx):
    if PYVERSION > 2:
        if isinstance(tx, bytes):
            value = tx.decode("utf-8")
        else:
            value = tx
    else:
        if isinstance(tx, unicode):
            value = tx
        else:
            value = tx.decode("utf-8")
    return value # Instance of unicode string
#------------------------------------------------------------------------------
def setSimulationState(boolVal):
    global SIMULATION_STATE
    SIMULATION_STATE = boolVal
#------------------------------------------------------------------------------
def doNotImportInvisibleShapes():
    preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/A2plus")
    return preferences.GetBool('doNotImportInvisibleShapes',True)
#------------------------------------------------------------------------------
def getPerFaceTransparency():
    preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/A2plus")
    return preferences.GetBool('usePerFaceTransparency',False)
#------------------------------------------------------------------------------
def getNativeFileManagerUsage():
    preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/A2plus")
    return preferences.GetBool('useNativeFileManager',False)
#------------------------------------------------------------------------------
def getRecalculateImportedParts():
    preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/A2plus")
    return preferences.GetBool('recalculateImportedParts',False)
#------------------------------------------------------------------------------
def getRecursiveUpdateEnabled():
    preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/A2plus")
    return preferences.GetBool('enableRecursiveUpdate',False)
#------------------------------------------------------------------------------
def getForceFixedPosition():
    preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/A2plus")
    return preferences.GetBool('forceFixedPosition',False)
#------------------------------------------------------------------------------
def getUseSolidUnion():
    preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/A2plus")
    return preferences.GetBool('useSolidUnion',False)
#------------------------------------------------------------------------------
def getConstraintEditorRef():
    global CONSTRAINT_EDITOR__REF
    return CONSTRAINT_EDITOR__REF
#------------------------------------------------------------------------------
def setConstraintEditorRef(ref):
    global CONSTRAINT_EDITOR__REF
    CONSTRAINT_EDITOR__REF = ref
#------------------------------------------------------------------------------
def setConstraintViewMode(active):
    global CONSTRAINT_VIEWMODE 
    CONSTRAINT_VIEWMODE = active
#------------------------------------------------------------------------------
def getConstraintViewMode():
    global CONSTRAINT_VIEWMODE 
    return CONSTRAINT_VIEWMODE
#------------------------------------------------------------------------------
def getConstraintDialogRef():
    global CONSTRAINT_DIALOG_REF
    return CONSTRAINT_DIALOG_REF
#------------------------------------------------------------------------------
def setConstraintDialogRef(ref):
    global CONSTRAINT_DIALOG_REF
    CONSTRAINT_DIALOG_REF = ref
#------------------------------------------------------------------------------
def getUseTopoNaming():
    preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/A2plus")
    return preferences.GetBool('useTopoNaming',False)
#------------------------------------------------------------------------------
def getRelativePathesEnabled():
    global RELATIVE_PATHES_ENABLED
    return RELATIVE_PATHES_ENABLED
#------------------------------------------------------------------------------
def setAutoSolve(enabled):
    global AUTOSOLVE_ENABLED
    AUTOSOLVE_ENABLED = enabled
#------------------------------------------------------------------------------
def getAutoSolveState():
    return AUTOSOLVE_ENABLED
#------------------------------------------------------------------------------
def setPartialProcessing(enabled):
    global PARTIAL_PROCESSING_ENABLED
    PARTIAL_PROCESSING_ENABLED = enabled
#------------------------------------------------------------------------------
def isPartialProcessing():
    return PARTIAL_PROCESSING_ENABLED
#------------------------------------------------------------------------------
def filterShapeObs(_list, allowSketches=False):
    lst = []
    for ob in _list:
        if allowSketches == True:
            if ob.Name.startswith("Sketch"):
                lst.append(ob)
                continue
        if (
            #Following object now have App::GeoFeatureGroupExtension in FC0.19
            #prevent them from beeing filtered out.
            ob.Name.startswith("Boolean") or 
            ob.Name.startswith("Body")
            ):
            pass
        elif ob.hasExtension('App::GeoFeatureGroupExtension'):
            #Part Containers within FC0.19.18405 seem to have a shape property..
            #filter it out
            continue
        elif ob.Name.startswith("Group"):
            #Group Containers within FC0.19 (Release >= 2020/03/31) seem to have a shape property..
            #filter it out
            continue
        if hasattr(ob,"Shape") and ob.Shape is not None and ob.Shape != 'None': #str 'None': TechDraw Balloons...
            if len(ob.Shape.Faces) > 0 and len(ob.Shape.Vertexes) > 0:
                lst.append(ob)
    S = set(lst)
    lst = []
    lst.extend(S)
    return lst
#------------------------------------------------------------------------------
def setTransparency():
    global SAVED_TRANSPARENCY
    # Save Transparency of Objects and make all transparent
    doc = FreeCAD.ActiveDocument

    if len(SAVED_TRANSPARENCY) > 0:
        # Transparency is already saved, no need to set transparency again
        return

    shapedObs = filterShapeObs(doc.Objects) # filter out partlist, spreadsheets etc..
    sel = FreeCADGui.Selection

    for obj in shapedObs:
        if hasattr(obj,'ViewObject'):                                # save "all-in" *MK
            if hasattr(obj.ViewObject,'DiffuseColor'):
                SAVED_TRANSPARENCY.append(
                    (obj.Name, obj.ViewObject.Transparency, obj.ViewObject.ShapeColor, obj.ViewObject.DiffuseColor)
                )
            else:
                SAVED_TRANSPARENCY.append(
                    (obj.Name, obj.ViewObject.Transparency, obj.ViewObject.ShapeColor, None)
                )

        obj.ViewObject.Transparency = 80
        sel.addSelection(obj) # Transparency workaround. Transparency is taken when once been selected
        sel.clearSelection()
#------------------------------------------------------------------------------
def restoreTransparency():
    global SAVED_TRANSPARENCY
    # restore transparency of objects...
    doc = FreeCAD.ActiveDocument

    sel = FreeCADGui.Selection

    for setting in SAVED_TRANSPARENCY:
        obj = doc.getObject(setting[0])
        if obj is not None:                                          # restore "all-in" *MK
            obj.ViewObject.Transparency = setting[1]
            obj.ViewObject.ShapeColor = setting[2]
            obj.ViewObject.DiffuseColor = setting[3]                 # diffuse always at last
            sel.addSelection(obj)
            sel.clearSelection()

    SAVED_TRANSPARENCY = []
#------------------------------------------------------------------------------
def isTransparencyEnabled():
    global SAVED_TRANSPARENCY
    return (len(SAVED_TRANSPARENCY) > 0)
#------------------------------------------------------------------------------
def getSelectedConstraint():
    # Check that constraint is selected
    selection = [s for s in FreeCADGui.Selection.getSelection() if s.Document == FreeCAD.ActiveDocument ]
    if len(selection) == 0: return None
    connectionToView = selection[0]

    if not 'ConstraintInfo' in connectionToView.Content and not 'ConstraintNfo' in connectionToView.Content:
        return None

    return connectionToView
#------------------------------------------------------------------------------
def appVersionStr():
    version = int(FreeCAD.Version()[0])
    subVersion = int(float(FreeCAD.Version()[1]))
    return "%03d.%03d" %(version,subVersion)
#------------------------------------------------------------------------------
def numpyVecToFC(nv):
    assert len(nv) == 3
    return Base.Vector(nv[0],nv[1],nv[2])
#------------------------------------------------------------------------------
def fit_rotation_axis_to_surface1( surface, n_u=3, n_v=3 ):
    'should work for cylinders and possibly cones (depending on the u,v mapping)'
    uv = sum( [ [ (u,v) for u in numpy.linspace(0,1,n_u)] for v in numpy.linspace(0,1,n_v) ], [] )
    P = [ numpy.array(surface.value(u,v)) for u,v in uv ] #positions at u,v points
    N = [ numpy.cross( *surface.tangent(u,v) ) for u,v in uv ] 
    intersections = []
    for i in range(len(N)-1):
        for j in range(i+1,len(N)):
            # based on the distance_between_axes( p1, u1, p2, u2) function,
            if 1 - abs(numpy.dot( N[i], N[j])) < 10**-6:
                continue #ignore parrallel case
            p1_x, p1_y, p1_z = P[i]
            u1_x, u1_y, u1_z = N[i]
            p2_x, p2_y, p2_z = P[j]
            u2_x, u2_y, u2_z = N[j]
            t1_t1_coef = u1_x**2 + u1_y**2 + u1_z**2 #should equal 1
            t1_t2_coef = -2*u1_x*u2_x - 2*u1_y*u2_y - 2*u1_z*u2_z # collect( expand(d_sqrd), [t1*t2] )
            t2_t2_coef = u2_x**2 + u2_y**2 + u2_z**2 #should equal 1 too
            t1_coef    = 2*p1_x*u1_x + 2*p1_y*u1_y + 2*p1_z*u1_z - 2*p2_x*u1_x - 2*p2_y*u1_y - 2*p2_z*u1_z
            t2_coef    =-2*p1_x*u2_x - 2*p1_y*u2_y - 2*p1_z*u2_z + 2*p2_x*u2_x + 2*p2_y*u2_y + 2*p2_z*u2_z
            A = numpy.array([ [ 2*t1_t1_coef , t1_t2_coef ] , [ t1_t2_coef, 2*t2_t2_coef ] ])
            b = numpy.array([ t1_coef, t2_coef])
            try:
                t1, t2 = numpy.linalg.solve(A,-b)
            except numpy.linalg.LinAlgError:
                continue #print('distance_between_axes, failed to solve problem due to LinAlgError, using numerical solver instead')
            pos_t1 = P[i] + numpy.array(N[i])*t1
            pos_t2 = P[j] + N[j]*t2
            intersections.append( pos_t1 )
            intersections.append( pos_t2 )
    if len(intersections) < 2:
        error = numpy.inf
        return None, None, error
    else: #fit vector to intersection points; http://mathforum.org/library/drmath/view/69103.html
        X = numpy.array(intersections)
        centroid = numpy.mean(X,axis=0)
        M = numpy.array([i - centroid for i in intersections ])
        A = numpy.dot(M.transpose(), M)
        U,s,V = numpy.linalg.svd(A)    #numpy docs: s : (..., K) The singular values for every matrix, sorted in descending order.
        axis_pos = centroid
        axis_dir = V[0]
        error = s[1] #dont know if this will work
        return numpyVecToFC(axis_dir), numpyVecToFC(axis_pos), error
#------------------------------------------------------------------------------
def fit_plane_to_surface1( surface, n_u=3, n_v=3 ):
    uv = sum( [ [ (u,v) for u in numpy.linspace(0,1,n_u)] for v in numpy.linspace(0,1,n_v) ], [] )
    P = [ surface.value(u,v) for u,v in uv ] #positions at u,v points
    N = [ numpy.cross( *surface.tangent(u,v) ) for u,v in uv ] 
    plane_norm = sum(N) / len(N) #plane's normal, averaging done to reduce error
    plane_pos = P[0]
    error = sum([ abs( numpy.dot(p - plane_pos, plane_norm) ) for p in P ])
    return numpyVecToFC(plane_norm), numpyVecToFC(plane_pos), error
#------------------------------------------------------------------------------
def isLine(param):
    if hasattr(Part,"LineSegment"):
        return isinstance(param,(Part.Line,Part.LineSegment))
    else:
        return isinstance(param,Part.Line)
#------------------------------------------------------------------------------
def getObjectFaceFromName( obj, faceName ):
    assert faceName.startswith('Face')
    ind = int( faceName[4:]) -1
    return obj.Shape.Faces[ind]
#------------------------------------------------------------------------------
def getProjectFolder():
    '''
    #------------------------------------------------------------------------------------
    # A new Parameter is required: projectFolder...
    # All Parts will be searched below this projectFolder-Value...
    #------------------------------------------------------------------------------------
    '''
    preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/A2plus")
    if not preferences.GetBool('useProjectFolder', False): return ""
    return preferences.GetString('projectFolder', '~')

#------------------------------------------------------------------------------
def pathToOS(path):
    if path is None: return None
    p = to_str(path)
    if OPERATING_SYSTEM == 'WINDOWS':
        p = p.replace(u'/',u'\\')
    else:
        p = p.replace(u'\\',u'/')
    return p # unicode string

#------------------------------------------------------------------------------
def findFile(_name, _path):
    '''
    Searches a file within a directory and it's subdirectories
    '''
    name = to_str(_name)
    path = to_str(_path)
    for root, dirs, files in os.walk(path):
        if name in files:
            return os.path.join(root, name)
    return None
            
#------------------------------------------------------------------------------
def findSourceFileInProject(_pathImportPart, _assemblyPath):
    '''
    #------------------------------------------------------------------------------------
    # interpret the sourcefile name of imported part
    # if working with preference "useProjectFolder:
    # - path of sourcefile is ignored
    # - filename is looked up beneath projectFolder
    #
    # if not working with preference "useProjectFolder":
    # - path of sourcefile is checked for being relative to assembly or absolute
    # - path is interpreted in appropriate way
    #------------------------------------------------------------------------------------
    '''
    pathImportPart = _pathImportPart
    assemblyPath = _assemblyPath
    
    pathImportPart = to_bytes(pathImportPart)
    assemblyPath = to_bytes(assemblyPath)
    
    preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/A2plus")
    if not preferences.GetBool('useProjectFolder', False): 
        # not working with useProjectFolder preference,
        # check whether path is relative or absolute...
        if (
            pathImportPart.startswith(b'../') or
            pathImportPart.startswith(b'..\\') or
            pathImportPart.startswith(b'./') or
            pathImportPart.startswith(b'.\\')
            ):
            # relative path
            # calculate the absolute path
            p1 = to_str(assemblyPath)
            p2 = to_str(pathImportPart)
            joinedPath = os.path.join(p1,p2)
            
            absolutePath = os.path.normpath(joinedPath)
            absolutePath = pathToOS(absolutePath)
            return to_str(absolutePath)
        else:
            pathImportPart = pathToOS(pathImportPart)
            return to_str(pathImportPart)

    projectFolder = os.path.abspath(getProjectFolder()) # get normalized path
    fileName = os.path.basename(pathImportPart)
    retval = findFile(fileName,projectFolder)
    retval = pathToOS(retval)
    if retval:
        return to_str(retval)
    else:
        return None
#------------------------------------------------------------------------------
def checkFileIsInProjectFolder(path):
    preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/A2plus")
    if not preferences.GetBool('useProjectFolder', False): return True

    projectFolder = os.path.abspath(getProjectFolder()) # get normalized path
    fileName = os.path.basename(path)
    nameInProject = findFile(fileName,projectFolder)
    
    if nameInProject == path:
        return True
    else:
        return False

#------------------------------------------------------------------------------
def pathOfModule():
    return os.path.dirname(__file__)

#------------------------------------------------------------------------------
def Msg(tx):
    FreeCAD.Console.PrintMessage(tx)

#------------------------------------------------------------------------------
def DebugMsg(level, tx):
    if A2P_DEBUG_LEVEL >= level:
        FreeCAD.Console.PrintMessage(tx)

#------------------------------------------------------------------------------
def drawSphere(center, color):
    doc = FreeCAD.ActiveDocument
    s = Part.makeSphere(2.0,center)
    sphere = doc.addObject("Part::Feature","Sphere")
    sphere.Shape = s
    sphere.ViewObject.ShapeColor = color
    doc.recompute()
#------------------------------------------------------------------------------
def drawVector(fromPoint,toPoint, color):
    if fromPoint == toPoint: return
    doc = FreeCAD.ActiveDocument

    l = Part.LineSegment()
    l.StartPoint = fromPoint
    l.EndPoint = toPoint
    line = doc.addObject("Part::Feature","Line")
    line.Shape = l.toShape()
    line.ViewObject.LineColor = color
    line.ViewObject.LineWidth = 1

    
    c = Part.makeCone(0,1,4)
    cone = doc.addObject("Part::Feature","ArrowHead")
    cone.Shape = c
    cone.ViewObject.ShapeColor = color
    #
    mov = Base.Vector(0,0,0)
    zAxis = Base.Vector(0,0,-1)
    rot = FreeCAD.Rotation(zAxis,toPoint.sub(fromPoint))
    cent = Base.Vector(0,0,0)
    conePlacement = FreeCAD.Placement(mov,rot,cent)
    cone.Placement = conePlacement.multiply(cone.Placement)
    cone.Placement.move(toPoint)
    doc.recompute()
#------------------------------------------------------------------------------
def findUnusedObjectName(base, counterStart=1, fmt='%03i', document=None):
    if document is None:
        document = FreeCAD.ActiveDocument
    i = counterStart
    usedNames = [ obj.Name for obj in document.Objects ]

    if base[-4:-3] == '_':
        base2 = base[:-4]
    else:
        base2 = base
    base2 = base2 + '_'

    objName = '%s%s' % (base2, fmt%i)
    while objName in usedNames:
        i += 1
        objName = '%s%s' % (base2, fmt%i)
    return objName
#------------------------------------------------------------------------------
def findUnusedObjectLabel(base, counterStart=1, fmt='%03i', document=None, extension=None):
    if document is None:
        document = FreeCAD.ActiveDocument
    i = counterStart
    usedLabels = [ obj.Label for obj in document.Objects ]

    if base[-4:-3] == '_':
        base2 = base[:-4]
    else:
        base2 = base
    base2 = base2 + '_'
    
    if extension is None:
        base3 = base2
    else:
        base3 = base2+extension+'_'

    objLabel = '%s%s' % (base3, fmt%i)
    while objLabel in usedLabels:
        i += 1
        objLabel = '%s%s' % (base3, fmt%i)
    return objLabel
#------------------------------------------------------------------------------
class ConstraintSelectionObserver:

    def __init__(self, selectionGate, parseSelectionFunction,
                  taskDialog_title, taskDialog_iconPath, taskDialog_text,
                  secondSelectionGate=None):
        self.selections = []
        self.parseSelectionFunction = parseSelectionFunction
        self.secondSelectionGate = secondSelectionGate
        FreeCADGui.Selection.addObserver(self)
        FreeCADGui.Selection.removeSelectionGate()
        FreeCADGui.Selection.addSelectionGate( selectionGate )
        wb_globals['selectionObserver'] = self
        self.taskDialog = SelectionTaskDialog(taskDialog_title, taskDialog_iconPath, taskDialog_text)
        FreeCADGui.Control.showDialog( self.taskDialog )

    def addSelection( self, docName, objName, sub, pnt ):
        self.selections.append( SelectionRecord( docName, objName, sub ))
        if len(self.selections) == 2:
            self.stopSelectionObservation()
            self.parseSelectionFunction( self.selections)
        elif self.secondSelectionGate != None and len(self.selections) == 1:
            FreeCADGui.Selection.removeSelectionGate()
            FreeCADGui.Selection.addSelectionGate( self.secondSelectionGate )

    def stopSelectionObservation(self):
        FreeCADGui.Selection.removeObserver(self)
        del wb_globals['selectionObserver']
        FreeCADGui.Selection.removeSelectionGate()
        FreeCADGui.Control.closeDialog()

#------------------------------------------------------------------------------
class SelectionRecord:
    def __init__(self, docName, objName, sub):
        self.Document = FreeCAD.getDocument(docName)
        self.ObjectName = objName
        self.Object = self.Document.getObject(objName)
        self.SubElementNames = [sub]

#------------------------------------------------------------------------------
class SelectionTaskDialog:

    def __init__(self, title, iconPath, textLines ):
        self.form = SelectionTaskDialogForm( textLines )
        self.form.setWindowTitle( title )
        if iconPath != None:
            self.form.setWindowIcon( QtGui.QIcon( iconPath ) )

    def reject(self):
        wb_globals['selectionObserver'].stopSelectionObservation()

    def getStandardButtons(self): #http://forum.freecadweb.org/viewtopic.php?f=10&t=11801
        return 0x00400000 #cancel button
#------------------------------------------------------------------------------
class SelectionTaskDialogForm(QtGui.QWidget):

    def __init__(self, textLines ):
        super(SelectionTaskDialogForm, self).__init__()
        self.textLines = textLines
        self.initUI()

    def initUI(self):
        vbox = QtGui.QVBoxLayout()
        for line in self.textLines.split('\n'):
            vbox.addWidget( QtGui.QLabel(line) )
        self.setLayout(vbox)

#------------------------------------------------------------------------------
class SelectionExObject:
    'allows for selection gate functions to interface with classification functions below'
    def __init__(self, doc, Object, subElementName):
        self.doc = doc
        self.Object = Object
        self.ObjectName = Object.Name
        self.SubElementNames = [subElementName]
#------------------------------------------------------------------------------
def getObjectEdgeFromName( obj, name ):
    assert name.startswith('Edge')
    ind = int( name[4:]) -1
    return obj.Shape.Edges[ind]
#------------------------------------------------------------------------------
def CircularEdgeSelected( selection ):
    if len( selection.SubElementNames ) == 1:
        subElement = selection.SubElementNames[0]
        if subElement.startswith('Edge'):
            edge = getObjectEdgeFromName( selection.Object, subElement)
            if not hasattr(edge, 'Curve'): #issue 39
                return False
            if isLine(edge.Curve):
                return False
            if hasattr( edge.Curve, 'Radius' ):
                return True
            
            # the following section fails for linear edges, protect it
            # by try/except block
            try:
                BSpline = edge.Curve.toBSpline()
                arcs = BSpline.toBiArcs(10**-6)
                if all( hasattr(a,'Center') for a in arcs ):
                    centers = numpy.array([a.Center for a in arcs])
                    sigma = numpy.std( centers, axis=0 )
                    if max(sigma) < 10**-6: #then circular curve
                        return True
            except:
                pass
            
    return False
#------------------------------------------------------------------------------
def ClosedEdgeSelected( selection ):
    if len( selection.SubElementNames ) == 1:
        subElement = selection.SubElementNames[0]
        if subElement.startswith('Edge'):
            edge = getObjectEdgeFromName( selection.Object, subElement)
            if edge.isClosed():
                return True
            else:
                return False
    return False
#------------------------------------------------------------------------------
def AxisOfPlaneSelected( selection ): #adding Planes/Faces selection for Axial constraints
    if len( selection.SubElementNames ) == 1:
        subElement = selection.SubElementNames[0]
        if subElement.startswith('Face'):
            face = getObjectFaceFromName( selection.Object, subElement)
            if str(face.Surface) == '<Plane object>':
                return True
            else:
                axis, center, error = fit_rotation_axis_to_surface1(face.Surface)
                error_normalized = error / face.BoundBox.DiagonalLength
                if error_normalized < 10**-6:
                    return True
    return False
#------------------------------------------------------------------------------
def printSelection(selection):
    entries = []
    for s in selection:
        for e in s.SubElementNames:
            entries.append(' - %s:%s' % (s.ObjectName, e))
            if e.startswith('Face'):
                ind = int( e[4:]) -1
                face = s.Object.Shape.Faces[ind]
                entries[-1] = entries[-1] + ' %s' % str(face.Surface)
    return '\n'.join( entries[:5] )
#------------------------------------------------------------------------------
def updateObjectProperties( c ):
    return
#------------------------------------------------------------------------------
def planeSelected( selection ):
    if len( selection.SubElementNames ) == 1:
        subElement = selection.SubElementNames[0]
        if subElement.startswith('Face'):
            face = getObjectFaceFromName( selection.Object, subElement)
            if str(face.Surface) == '<Plane object>':
                return True
            elif str(face.Surface) == '<BSplineSurface object>':
                normal,pos,error = fit_plane_to_surface1(face.Surface)
                if abs(error) < 1e-9:
                    return True
    return False
#------------------------------------------------------------------------------
def vertexSelected( selection ):
    if len( selection.SubElementNames ) == 1:
        return selection.SubElementNames[0].startswith('Vertex')
    return False
#------------------------------------------------------------------------------
def cylindricalFaceSelected( selection ):
    if len( selection.SubElementNames ) == 1:
        subElement = selection.SubElementNames[0]
        if subElement.startswith('Face'):
            face = getObjectFaceFromName( selection.Object, subElement)
            if hasattr(face.Surface,'Radius'):
                return True
            elif str(face.Surface).startswith('<SurfaceOfRevolution'):
                return True
            else:
                axis, center, error = fit_rotation_axis_to_surface1(face.Surface)
                error_normalized = error / face.BoundBox.DiagonalLength
                if error_normalized < 10**-6:
                    return True
    return False
#------------------------------------------------------------------------------
def LinearEdgeSelected( selection ):
    if len( selection.SubElementNames ) == 1:
        subElement = selection.SubElementNames[0]
        if subElement.startswith('Edge'):
            edge = getObjectEdgeFromName( selection.Object, subElement)
            if not hasattr(edge, 'Curve'): #issue 39
                return False
            if isLine(edge.Curve):
                return True

            BSpline = edge.Curve.toBSpline()
            arcs = BSpline.toBiArcs(10**-6)
            if all(isLine(a) for a in arcs):
                lines = arcs
                D = numpy.array([L.tangent(0)[0] for L in lines]) #D(irections)
                if numpy.std( D, axis=0 ).max() < 10**-9: #then linear curve
                    return True
    return False
#------------------------------------------------------------------------------
def sphericalSurfaceSelected( selection ):
    if len( selection.SubElementNames ) == 1:
        subElement = selection.SubElementNames[0]
        if subElement.startswith('Face'):
            face = getObjectFaceFromName( selection.Object, subElement)
            return str( face.Surface ).startswith('Sphere ')
    return False
#------------------------------------------------------------------------------
def getObjectVertexFromName( obj, name ):
    assert name.startswith('Vertex')
    ind = int( name[6:]) -1
    return obj.Shape.Vertexes[ind]
#------------------------------------------------------------------------------
def removeConstraint( constraint ):
    'required as constraint.Proxy.onDelete only called when deleted through GUI'
    doc = constraint.Document
    if constraint.ViewObject != None:
        constraint.ViewObject.Proxy.onDelete( constraint.ViewObject, None ) # also removes mirror...
    doc.removeObject( constraint.Name )
#------------------------------------------------------------------------------
def getPos(obj, subElementName):
    pos = None
    
    if subElementName.startswith('Face'):
        face = getObjectFaceFromName(obj, subElementName)
        surface = face.Surface
        if str(surface) == '<Plane object>':
            pos = getObjectFaceFromName(obj, subElementName).Faces[0].BoundBox.Center
            # axial constraint for Planes
            # pos = surface.Position
        elif str(surface) == "<Cylinder object>":
            pos = surface.Center
        elif all( hasattr(surface,a) for a in ['Axis','Center','Radius'] ):
            pos = surface.Center
        elif str(surface).startswith('<SurfaceOfRevolution'):
            pos = getObjectFaceFromName(obj, subElementName).Edges[0].Curve.Center
        elif str(surface).startswith('<BSplineSurface'):
            axis,pos1,error = fit_plane_to_surface1(surface)
            error_normalized = error / face.BoundBox.DiagonalLength
            if error_normalized < 10**-6: #then good plane fit
                pos = pos1
            axis, center, error = fit_rotation_axis_to_surface1(face.Surface)
            if axis != None:
                error_normalized = error / face.BoundBox.DiagonalLength
                if error_normalized < 10**-6: #then good rotation_axis fix
                    pos = center
            
    elif subElementName.startswith('Edge'):
        edge = getObjectEdgeFromName(obj, subElementName)
        if isLine(edge.Curve):
            if appVersionStr() <= "000.016":
                pos = edge.Curve.StartPoint
            else:
                pos = edge.firstVertex(True).Point
        elif hasattr( edge.Curve, 'Center'): #circular curve
            pos = edge.Curve.Center
        else:
            BSpline = edge.Curve.toBSpline()
            arcs = BSpline.toBiArcs(10**-6)
            if all( hasattr(a,'Center') for a in arcs ):
                centers = numpy.array([a.Center for a in arcs])
                sigma = numpy.std( centers, axis=0 )
                if max(sigma) < 10**-6: #then circular curce
                    pos = numpyVecToFC(centers[0])
            if all(isLine(a) for a in arcs):
                lines = arcs
                D = numpy.array([L.tangent(0)[0] for L in lines]) #D(irections)
                if numpy.std( D, axis=0 ).max() < 10**-9: #then linear curve
                    pos = lines[0].value(0)
            
            
    elif subElementName.startswith('Vertex'):
        pos = getObjectVertexFromName(obj, subElementName).Point
    
    return pos # maybe none !!
#------------------------------------------------------------------------------
def getPlaneNormal(surface):
    axis = None
    if hasattr(surface,'Axis'):
        axis = surface.Axis
    elif str(surface).startswith('<BSplineSurface'):
        axis,pos,error = fit_plane_to_surface1(surface)
    return axis # may be none!
#------------------------------------------------------------------------------
def getAxis(obj, subElementName):
    axis = None
    if subElementName.startswith('Face'):
        face = getObjectFaceFromName(obj, subElementName)
        surface = face.Surface
        if hasattr(surface,'Axis'):
            axis = surface.Axis
        elif str(surface).startswith('<SurfaceOfRevolution'):
            axis = face.Edges[0].Curve.Axis
        elif str(surface).startswith('<BSplineSurface'):
            axis1,pos,error = fit_plane_to_surface1(surface)
            error_normalized = error / face.BoundBox.DiagonalLength
            if error_normalized < 10**-6: #then good plane fit
                axis = axis1
            axis_fitted, center, error = fit_rotation_axis_to_surface1(face.Surface)
            if axis_fitted is not None:
                error_normalized = error / face.BoundBox.DiagonalLength
                if error_normalized < 10**-6: #then good rotation_axis fix
                    axis = axis_fitted
            
    elif subElementName.startswith('Edge'):
        edge = getObjectEdgeFromName(obj, subElementName)
        if isLine(edge.Curve):
            axis = edge.Curve.tangent(0)[0]
        elif hasattr( edge.Curve, 'Axis'): #circular curve
            axis =  edge.Curve.Axis
        else:
            BSpline = edge.Curve.toBSpline()
            arcs = BSpline.toBiArcs(10**-6)
            if all( hasattr(a,'Center') for a in arcs ):
                centers = numpy.array([a.Center for a in arcs])
                sigma = numpy.std( centers, axis=0 )
                if max(sigma) < 10**-6: #then circular curce
                    axis = a.Axis
            if all(isLine(a) for a in arcs):
                lines = arcs
                D = numpy.array([L.tangent(0)[0] for L in lines]) #D(irections)
                if numpy.std( D, axis=0 ).max() < 10**-9: #then linear curve
                    axis = numpyVecToFC(D[0])
            
    return axis # may be none!
#------------------------------------------------------------------------------
def unTouchA2pObjects():
    doc = FreeCAD.activeDocument()
    for obj in doc.Objects:
        # leave A2pSketches touched (for recomputing dependent shapes)
        if isA2pSketch(obj): continue
        if isA2pObject(obj):
            obj.purgeTouched();
#------------------------------------------------------------------------------
def isA2pSketch(obj):
    result = False
    if isA2pPart(obj):
        if len(obj.Shape.Faces) == 0:
            result = True
    return result
#------------------------------------------------------------------------------
def isA2pPart(obj):
    result = False
    if hasattr(obj,"Content"):
        if 'importPart' in obj.Content:
            result = True
    elif hasattr(obj,"a2p_Version"):          # keep old assembly item identification in,
        result = True                         #  -> otherwise toggle transparency won't work
    elif hasattr(obj,"subassemblyImport"):    # another possible assembly item
        result = True
    elif hasattr(obj,"assembly2Version"):    # another possible assembly item (very old a2p versions)
        result = True
    return result
#------------------------------------------------------------------------------
def isEditableA2pPart(obj):
    if not isA2pPart(obj): return False
    if hasattr(obj,"sourceFile"):
        if obj.sourceFile == "converted": return False
        if obj.sourceFile == "": return False
    return True
#------------------------------------------------------------------------------
def isA2pConstraint(obj): 
    result = False
    if hasattr(obj,"Content"):
        if ('ConstraintInfo' in obj.Content) or ('ConstraintNfo'in obj.Content):
            result = True
    return result
#------------------------------------------------------------------------------
def isA2pObject(obj):
    result = False
    if isA2pPart(obj) or isA2pConstraint(obj):
        result = True
    return result
#------------------------------------------------------------------------------
def isFastenerObject(obj):
    '''
    recognize an object created by the fasteners WB
    '''
    if hasattr(obj,'Proxy'):
        if str(obj.Proxy).startswith('<FastenersCmd.FSScrewObject'): return True
        if str(obj.Proxy).startswith('<FastenersCmd.FSWasherObject'): return True
        if str(obj.Proxy).startswith('<FastenersCmd.FSScrewRodObject'): return True
    return False
#------------------------------------------------------------------------------
def makeDiffuseElement(color,trans):
    elem = (color[0],color[1],color[2],trans/100.0)
    return elem
#------------------------------------------------------------------------------
def copyObjectColors(ob1,ob2):
    '''
    copies colors from ob2 to ob1
    Transparency of updated object is not touched until
    user activates perFaceTransparency within preferences.
    '''
    if ob1.updateColors != True:
        ob1.ViewObject.DiffuseColor = [ob1.ViewObject.ShapeColor] # set syncron
        return
    
    # obj1.updateColors == True from now
    newDiffuseColor = copy.copy(ob2.ViewObject.DiffuseColor)
    ob1.ViewObject.ShapeColor = ob2.ViewObject.ShapeColor
    ob1.ViewObject.DiffuseColor = newDiffuseColor # set diffuse last

    if not getPerFaceTransparency():
        # touch transparency one to trigger update of 3D View
        # per face transparency probably gets lost
        if ob1.ViewObject.Transparency > 0:
            t = ob1.ViewObject.Transparency
            ob1.ViewObject.Transparency = 0
            ob1.ViewObject.Transparency = t
        else:
            ob1.ViewObject.Transparency = 1
            ob1.ViewObject.Transparency = 0

    # select/deselect object once to trigger update of 3D View
    FreeCADGui.Selection.addSelection(ob1)
    FreeCADGui.Selection.removeSelection(ob1)
#------------------------------------------------------------------------------
def isConstrainedPart(doc,obj):
    if not isA2pPart(obj): return False
    constraints = [ ob for ob in doc.Objects if 'ConstraintInfo' in ob.Content]
    for c in constraints:
        if c.Object1 == obj.Name:
            return True
        if c.Object2 == obj.Name:
            return True
    return False
#------------------------------------------------------------------------------
def objectExists(name):
    doc = FreeCAD.activeDocument()
    try:
        ob = doc.getObject(name)
        if ob != None: return True
    except:
        pass
    return False
#------------------------------------------------------------------------------
def deleteConstraintsOfDeletedObjects():
    doc = FreeCAD.activeDocument()
    deleteList = []
    missingObjects = []
    for c in doc.Objects:
        if 'ConstraintInfo' in c.Content:
            if not objectExists(c.Object1):
                deleteList.append(c)
                missingObjects.append(c.Object1)
                continue
            if not objectExists(c.Object2):
                deleteList.append(c)
                missingObjects.append(c.Object2)
    if len(deleteList) != 0:
        for c in deleteList:
            removeConstraint(c)

        missingObjects = set(missingObjects)
    
        msg = u"Not existing part(s):\n  - {}".format(
            u'\n  - '.join( objName for objName in missingObjects)
            )
        QtGui.QMessageBox.information(
            QtGui.QApplication.activeWindow(), 
            u"Constraints of missing parts removed!", 
            msg 
            )
#------------------------------------------------------------------------------
def a2p_repairTreeView():
    doc = FreeCAD.activeDocument()
    if doc is None: return
    
    deleteConstraintsOfDeletedObjects()

    constraints = [ obj for obj in doc.Objects if 'ConstraintInfo' in obj.Content]
    for c in constraints:
        if c.Proxy != None:
            c.Proxy.disable_onChanged = True
        if not hasattr(c,"ParentTreeObject"):
            c.addProperty("App::PropertyLink","ParentTreeObject","ConstraintInfo")
            c.setEditorMode("ParentTreeObject", 1)
        parent = doc.getObject(c.Object1)
        c.ParentTreeObject = parent
        if parent is not None: parent.touch()
        if c.Proxy != None:
            c.Proxy.disable_onChanged = False
    #
    mirrors = [ obj for obj in doc.Objects if 'ConstraintNfo' in obj.Content]
    for m in mirrors:
        if m.Proxy != None:
            m.Proxy.disable_onChanged = True
        if not hasattr(m,"ParentTreeObject"):
            m.addProperty("App::PropertyLink","ParentTreeObject","ConstraintNfo")
            m.setEditorMode("ParentTreeObject", 1)
        parent = doc.getObject(m.Object2)
        m.ParentTreeObject = parent
        if parent is not None: parent.touch()
        if m.Proxy != None:
            m.Proxy.disable_onChanged = False
            
    unTouchA2pObjects()
#------------------------------------------------------------------------------