from PIL import ImageTk
from PIL import Image
import tkinter as Tk
import math

# ouvre l'environnement TK avec une fenetre
fenetre = Tk.Tk()
# donne un titre à la fenetre
fenetre.title('Stage seconde - cinematique inverse - Serpent 1 DLL')
# creation d'un canvas (zone de dessin)
canvas = Tk.Canvas(fenetre,width=1000,height=600,bg='white')
canvas.pack()

class Pos2D:
    # classe qui gere une position ou un vecteur en 2D (x,y)
    def __init__(self,x,y):
        # construction d'une position/vecteur 2D
        self.x = x
        self.y = y

    def __add__(self,p2D):
        # addition avec un autre Pos2D en parametre (somme des composantes)
        return Pos2D (self.x+p2D.x,self.y+p2D.y)

    def __sub__(self,p2D):
        # soustraction avec un autre Pos2D en parametre (soustraction des composantes)
        return Pos2D (self.x-p2D.x,self.y-p2D.y)

    def __mul__(self,scalaire):
        # multiplication avec un scalaire
        return Pos2D (self.x*scalaire,self.y*scalaire)

    def __neg__(self):
        # négation (opposé) du Pos2D
        return Pos2D (-self.x,-self.y)

    def __str__(self):
        # transformation en chaine de caracteres pour l'affichage
        return '('+str(self.x)+' '+str(self.y)+')'

    def distance(self,p2D):
        # calcule et retourne la distance (Euclidienne) avec un autre pos2D en paramètre
        return math.sqrt((self.x-p2D.x)*(self.x-p2D.x)+(self.y-p2D.y)*(self.y-p2D.y))

    def makeInt(self):
        # arrondi les coordonnees a l'entier inferieur
        self.x = math.floor(self.x)
        self.y = math.floor(self.y)

    def normeVecteur(self):
        # calcule et retourne la norme du pos2D (vecteur)
        return self.distance(Pos2D(0,0))

    def normalize(self):
        # normalise le Pos2D (vecteur)
        norme = self.normeVecteur()
        if norme != 0:
            self.x /= norme
            self.y /= norme

    def normalize2(self):
        # retourne le vecteur normalise
        norme = self.normeVecteur()
        vecteurnormalise = Pos2D(self.x,self.y)
        if norme != 0:
            vecteurnormalise.x /= norme
            vecteurnormalise.y /= norme
        return vecteurnormalise

    def produitScalaire(self,p2D):
        # calcule et retourne le produit scalaire avec un autre Pos2D en parametre (vecteur)
        return self.x*p2D.x + self.y*p2D.y

    def angleVecteur(self,p2D):
        # calcule et retourne l'angle avec un autre Pos2D (vecteur)
        normalise = self.normalize2()
        p2Dnormalise = p2D.normalize2()
        pS = normalise.produitScalaire(p2Dnormalise)
        if pS < -1 :
            pS = -1
        if pS > 1:
            pS = 1
        return math.acos(pS)

    def produitVectoriel(self,p2D):
        # calcule et retourne la composante z du produit vectoriel avec un autre Pos2D (vecteur)
        return self.x*p2D.y - self.y*p2D.x

    def produitVectorielZ(self):
        # calcule et retourne le Pos2D (vecteur) qui est le produit vectoriel du vecteur avec l'axe z (équivalent à une rotation de 90 degrees)
        return Pos2D(self.y,-self.x)

    def angleOrienteVecteur(self,p2D):
        # calcule et retourne l'angle orienté avec un autre Pos2D (vecteur)
        angle = self.angleVecteur(p2D)
        prodVect = self.produitVectoriel(p2D)
        if prodVect >= 0:
            return -angle
        else:
            return angle

    def translation(self,p2D,distance):
        # retourne le Pos2D (point) à la distance donnée dans la direction de p2D
        direction = p2D.normalize2()
        translation = direction * distance
        res = self + translation
        return res

class ImageIK:
    # classe qui gere les attributs d'une image
    def __init__(self,nomImage):
        # creation de l'image et de tous ces attributs
        self.nomFichierImage = nomImage+'.png' # nom du fichier
        self.PILImage = Image.open(self.nomFichierImage) # image PIL
        self.TkPhotoImages = Tk.PhotoImage(file=self.nomFichierImage,master=fenetre) # image Tk
        self.itemImage = canvas.create_image(0,0,anchor=Tk.NW,image=self.TkPhotoImages) # item sur canvas
        self.parent = None # l'image parent

        self.globalOrientation = 0 # orientation globale (angle deg, tient compte des parents)
        self.localOrientation = 0 # orientation (angle deg) par rapport au parent
        self.toUpdate = False # indicateur de besoin de mettre a jour

        self.upleftGlobalPosition = Pos2D(0,0) # position du coin en haut à gauche dans la fenetre
        self.initialupleftGlobalPosition = Pos2D(0,0) # position globale du coin en haut à gauche dans la fenetre à l'initialisation
        self.globalRotationCenterPosition = Pos2D(0,0) # position globale du centre de rotation dans la fenetre
        self.localRotationCenterPosition = Pos2D(0,0) # position locale du centre de rotation par rapport au coin en haut à gauche
        self.localInitialRotationCenterPosition = Pos2D(0,0) # position locale du centre de rotation par rapport au coin en haut à gauche à l'initialisation
        self.globalInitialRotationCenterPosition = Pos2D(0,0) # position globale du centre de rotation par rapport au coin en haut à gauche à l'initialisation
        self.localNextRotationCenterPosition = Pos2D(0,0) # position locale du prochain centre de rotation
        self.localInitialNextRotationCenterPosition = Pos2D(0,0) # position locale du prochain centre de rotation à l'initialisation
        self.globalNextRotationCenterPosition = Pos2D(0,0) # position globale du prochain centre de rotation

    def close(self):
        # ferme l'image PIL et supprime le PhotoImage
        self.PILImage.close()
        del self.TkPhotoImages

    def setInitialPosition(self,posx,posy):
        # definition de la position globale initiale du coin (gauche,haut) de l'image
        # fait que 1 fois à l'initialisation
        self.initialupleftGlobalPosition = Pos2D(posx,posy)
        self.setGlobalPosition(posx,posy)

    def setInitialRotationCenterPosition(self,posx,posy):
        # definition de la position locale du centre de rotation par rapport au coin en haut à gauche
        self.localInitialRotationCenterPosition = Pos2D(posx,posy)
        self.globalInitialRotationCenterPosition = self.initialupleftGlobalPosition + self.localInitialRotationCenterPosition

    def setInitialNextRotationCenterPosition(self,posx,posy):
        # definition de la position locale du prochain centre de rotation par rapport au coin en haut à gauche
        self.localInitialNextRotationCenterPosition = Pos2D(posx,posy)

    def getCenterPosition(self):
        # retoune la position globale du centre de l'image
        return Pos2D(self.upleftGlobalPosition.x+self.PILImage.width/2,self.upleftGlobalPosition.y+self.PILImage.height/2)

    def setCenterPosition(self,posx,posy):
        # change la position globale du centre de l'image
        self.upleftGlobalPosition.x = posx-self.PILImage.width/2
        self.upleftGlobalPosition.y = posy-self.PILImage.height/2
        canvas.coords(self.itemImage,self.upleftGlobalPosition.x,self.upleftGlobalPosition.y)

    def setLocalOrientation(self,angle):
        # change l'orientation de l'image
        self.localOrientation = angle
        self.toUpdate = True

    def update(self):
        # met a jour l'orientation et la position globale de l'image
        if self.parent != None:
            self.globalOrientation = self.localOrientation + self.parent.globalOrientation
        else:
            self.globalOrientation = self.localOrientation
        # mise à jour positions
        image_rotated = self.PILImage.rotate(self.globalOrientation,expand=True) # l'image tournee
        (gauche,haut,droite,bas) = image_rotated.getbbox() # calcul de la axis aligned bounding box
        centerBBox = Pos2D((droite-gauche)/2+gauche,(bas-haut)/2+haut) # calcul du centre = centre rotate
        centerBBox.makeInt()
        angle_rad = math.radians(self.globalOrientation) # angle en radians
        # vecteur centreBB -> centre rot
        BBRot = Pos2D(-math.cos(angle_rad)*(self.PILImage.width/2), math.sin(angle_rad)*(self.PILImage.width/2))
        BBRot.makeInt()
        self.localRotationCenterPosition = centerBBox + BBRot
        # vecteur centreBB -> centre rot next
        BBRotNext = Pos2D(math.cos(angle_rad)*(self.PILImage.width/2), -math.sin(angle_rad)*(self.PILImage.width/2))
        BBRotNext.makeInt()
        self.localNextRotationCenterPosition = centerBBox + BBRotNext
        # position globale centre rot
        if self.parent != None:
            self.globalRotationCenterPosition = self.parent.globalNextRotationCenterPosition
        else:
            self.globalRotationCenterPosition = self.globalInitialRotationCenterPosition
        # position globale image
        self.upleftGlobalPosition = self.globalRotationCenterPosition -self.localRotationCenterPosition
        # position globale next center
        self.globalNextRotationCenterPosition = self.upleftGlobalPosition + self.localNextRotationCenterPosition
        # mise a jour Tkphotoimage et item
        self.TkPhotoImages = ImageTk.PhotoImage(image_rotated,master=fenetre)
        canvas.itemconfigure(self.itemImage,image=self.TkPhotoImages)
        canvas.coords(self.itemImage,self.upleftGlobalPosition.x,self.upleftGlobalPosition.y)
        # mis a jour fenetre
        fenetre.update()

    def setGlobalPosition(self,posx,posy):
        # met à jour la position globale
        self.upleftGlobalPosition = Pos2D(posx,posy)
        canvas.coords(self.itemImage,posx,posy)

class ImageManager:
    # classe qui gere toutes les images a afficher
    def __init__(self):
        # constructeur
        self.dicoImages = {} # dictionnaire des Images

    def close(self):
        # ferme les images
        for image in self.dicoImages.keys():
            self.dicoImages[image].close()

    def ajouterImage(self,nomImage):
        # ajout d'une image au manager
        self.dicoImages[nomImage] = ImageIK(nomImage)

    def setParentImage(self,nomImageEnfant,nomImageParent):
        # definition du parent d'une image
        self.dicoImages[nomImageEnfant].parent = self.dicoImages[nomImageParent]

    def getParentImage(self,nomImageEnfant):
        # retourne le parent d'une image
        return self.dicoImages[nomImageEnfant].parent

    def setInitialPositionImage(self,nomImage,posx,posy):
        # definition de la position globale initiale de l'image
        self.dicoImages[nomImage].setInitialPosition(posx,posy)

    def setInitialRotationCenterPositionImage(self,nomImage,posx,posy):
        # definition de la position locale initiale du centre de rotation
        self.dicoImages[nomImage].setInitialRotationCenterPosition(posx,posy)

    def setInitialNextRotationCenterPositionImage(self,nomImage,posx,posy):
        # definition de la position locale initiale du prochain centre de rotation
        self.dicoImages[nomImage].setInitialNextRotationCenterPosition(posx,posy)

    def setPositionImage(self,nomImage,posx,posy):
        # changement de position globale de l'image
        self.dicoImages[nomImage].setGlobalPosition(posx,posy)

    def setCenterPositionImage(self,nomImage,posx,posy):
        # changement de position globale du centre de l'image
        self.dicoImages[nomImage].setCenterPosition(posx,posy)

    def getPositionImage(self,nomImage):
        # recuperation de la position globale de l'image
        return self.dicoImages[nomImage].upleftGlobalPosition

    def getCenterPositionImage(self,nomImage):
        # recuperation de la position globale du centre de l'image
        return self.dicoImages[nomImage].getCenterPosition()

    def setLocalOrientationImage(self,nomImage,orientation):
        # changement de l'orientation locale de l'image
        self.dicoImages[nomImage].setLocalOrientation(orientation)

    def getLocalOrientationImage(self,nomImage):
        # recuperation de l'orientation locale de l'image
        return self.dicoImages[nomImage].localOrientation

    def getGlobalCenterPositionImage(self,nomImage):
        # recuperation de la position globale du centre de rotation
        return self.dicoImages[nomImage].globalRotationCenterPosition

    def getGlobalNextCenterPositionImage(self,nomImage):
        # recuperation de la position globale du prochain centre de rotation
        return self.dicoImages[nomImage].globalNextRotationCenterPosition

    def update(self):
        # mis a jour de toutes les orientations et positions des images
        for image in self.dicoImages.keys():
            if self.dicoImages[image].toUpdate:
                self.dicoImages[image].update()
