"""
spiro.py

Program pythona symulujący spirograf.

Autor: Mahesh Venkitachalam
Strona WWW: electronut.in
"""

import sys, random, argparse
import numpy as np
import math
import turtle
import random
from PIL import Image
from datetime import datetime    
from fractions import gcd

# klasa, która rysuje krzywe spiro
class Spiro:
    # konstruktor
    def __init__(self, xc, yc, col, R, r, l):

        # tworzenie obiektu turtle
        self.t = turtle.Turtle()
        # ustawianie kształtu kursora
        self.t.shape('turtle')
        # ustawianie kroku w stopniach
        self.step = 5
        # ustawianie flagi zakończenia rysowania
        self.drawingComplete = False

        # ustawianie parametrów
        self.setparams(xc, yc, col, R, r, l)

        # inicjowanie rysowania
        self.restart()

    # ustawianie parametrów
    def setparams(self, xc, yc, col, R, r, l):
        # parametry spirografu
        self.xc = xc
        self.yc = yc
        self.R = int(R)
        self.r = int(r)
        self.l = l
        self.col = col
        # zredukowanie r/R do najmniejszej postaci poprzez podzielenie przez NWD
        gcdVal = gcd(self.r, self.R)
        self.nRot = self.r//gcdVal
        # uzyskanie stosunku promieni
        self.k = r/float(R)
        # ustawienie koloru
        self.t.color(*col)
        # bieżący kąt
        self.a = 0

    # zrestartowanie rysowania
    def restart(self):
        # ustawienie flagi
        self.drawingComplete = False
        # pokazanie żółwia
        self.t.showturtle()
        # przejście do pierwsego punktu
        self.t.up()
        R, k, l = self.R, self.k, self.l
        a = 0.0
        x = R*((1-k)*math.cos(a) + l*k*math.cos((1-k)*a/k))
        y = R*((1-k)*math.sin(a) - l*k*math.sin((1-k)*a/k))
        self.t.setpos(self.xc + x, self.yc + y)
        self.t.down()

    # rysowanie całości
    def draw(self):
        # rysowanie reszty punktów
        R, k, l = self.R, self.k, self.l
        for i in range(0, 360*self.nRot + 1, self.step):
            a = math.radians(i)
            x = R*((1-k)*math.cos(a) + l*k*math.cos((1-k)*a/k))
            y = R*((1-k)*math.sin(a) - l*k*math.sin((1-k)*a/k))
            self.t.setpos(self.xc + x, self.yc + y)
        # zrobione - ukrycie żółwia
        self.t.hideturtle()
    
    # aktualizowanie o jeden krok
    def update(self):
        # pomiń, jeśli zrobione
        if self.drawingComplete:
            return
        # inkrementacja kąta
        self.a += self.step
        # rysowanie kroku
        R, k, l = self.R, self.k, self.l
        # ustawianie kąta
        a = math.radians(self.a)
        x = self.R*((1-k)*math.cos(a) + l*k*math.cos((1-k)*a/k))
        y = self.R*((1-k)*math.sin(a) - l*k*math.sin((1-k)*a/k))
        self.t.setpos(self.xc + x, self.yc + y)
        # sprawdzanie, czy rysowanie jest zakończone i ustawianie flagi
        if self.a >= 360*self.nRot:
            self.drawingComplete = True
            # zrobione - ukrycie żółwia
            self.t.hideturtle()

    # czyszczenie wszystkiego
    def clear(self):
        self.t.clear()

# Klasa do animowania spirografów
class SpiroAnimator:
    # konstruktor
    def __init__(self, N):
        # wartość licznika w milisekundach
        self.deltaT = 10
        # uzyskanie rozmiarów okna
        self.width = turtle.window_width()
        self.height = turtle.window_height()
        # tworzenie obiektow spiro
        self.spiros = []
        for i in range(N):
            # generowanie losowych parametrów
            rparams = self.genRandomParams()
            # ustawianie parametrów spiro
            spiro = Spiro(*rparams)
            self.spiros.append(spiro)
        # wywołanie licznika
        turtle.ontimer(self.update, self.deltaT)
    
    # restartowanie rysowania spiro
    def restart(self):
        for spiro in self.spiros:
            # czyszczenie
            spiro.clear()
            # generowanie losowych parametrów
            rparams = self.genRandomParams()
            # ustwianie parametrów spiro
            spiro.setparams(*rparams)
            # zrestartowanie rysowania
            spiro.restart()

    # generowanie losowych parametrów
    def genRandomParams(self):
        width, height = self.width, self.height
        R = random.randint(50, min(width, height)//2)
        r = random.randint(10, 9*R//10)
        l = random.uniform(0.1, 0.9)
        xc = random.randint(-width//2, width//2)
        yc = random.randint(-height//2, height//2)
        col = (random.random(),
               random.random(),
               random.random())
        return (xc, yc, col, R, r, l)

    def update(self):
        # aktualizowanie wszystkich krzywych spiro
        nComplete = 0
        for spiro in self.spiros:
            # aktualizowanie
            spiro.update()
            # zliczanie ukończonych krzywych
            if spiro.drawingComplete:
                nComplete+= 1
        # jeśli wszystkie krzywe spiro są ukończone, restartowanie
        if nComplete == len(self.spiros):
            self.restart()
        # wywołanie timera
        turtle.ontimer(self.update, self.deltaT)

    # włączanie i wyłączanie żółwia
    def toggleTurtles(self):
        for spiro in self.spiros:
            if spiro.t.isvisible():
                spiro.t.hideturtle()
            else:
                spiro.t.showturtle()
            
# zapisywanie krzywych spiro w pliku obrazu
def saveDrawing():
    # ukrycie żółwia
    turtle.hideturtle()
    # generowanie unikatowej nazwy pliku
    dateStr = (datetime.now()).strftime("%d%b%Y-%H%M%S")
    fileName = 'spiro-' + dateStr 
    print('zapisywanie rysunku w pliku %s.eps/png' % fileName)
    # uzyskanie obektu canvas modułu tkinter
    canvas = turtle.getcanvas()
    # zapisywanie obrazu postscript
    canvas.postscript(file = fileName + '.eps')
    # użycie PIL do konwersji na PNG
    img = Image.open(fileName + '.eps')
    img.save(fileName + '.png', 'png')
    # pokazanie żółwia
    turtle.showturtle()

# funkcja main()
def main():
    # użycie sys.argv w razie potrzeby
    print('generowanie spirografu...')
    # tworzenie parsera
    descStr = """Ten program  rysuje krzywe spiro używając modułu turtle. 
    Program uruchomiony bez żadnych argumentów rysuje losowe krzywe spiro.
    
    Terminologia:

    R: promień zewnętrznego okręgu.
    r: promień wewnętrznego okręgu.
    l: stosunek odcinka poprowadzonego ze środka mniejszego okręgu do punktu umieszczenia końcówki pióra do promienia r.
    """
    parser = argparse.ArgumentParser(description=descStr)
  
    # dodanie oczekiwanych argumentów
    parser.add_argument('--sparams', nargs=3, dest='sparams', required=False, 
                        help="Trzy argumenty w sparams: R, r, l.")
                        

    # parsowanie args
    args = parser.parse_args()

    # ustawienie szerokości okna rysowania na 80% szerokości ekranu
    turtle.setup(width=0.8)

    # ustawienie kształtu kursora
    turtle.shape('turtle')

    # ustawienie tytułu
    turtle.title("Spirografy!")
    # dodanie procedury obsługi przycisku dla zapisywania rysunków
    turtle.onkey(saveDrawing, "s")
    # rozpoczęcie nasłuchiwania
    turtle.listen()

    # ukrycie kursora turtle funkcji main
    turtle.hideturtle()

    # sprawdzenie args i rysowanie
    if args.sparams:
        params = [float(x) for x in args.sparams]
        # rysowanie spiro z danymi parametrami
        # domyślnie kolor czarny
        col = (0.0, 0.0, 0.0)
        spiro = Spiro(0, 0, col, *params)
        spiro.draw()
    else:
        # tworzenie obiektu animatora
        spiroAnim = SpiroAnimator(4)
        # dodanie procedury obsługi przycisku dla włączania i wyłączania kursora żółwia
        turtle.onkey(spiroAnim.toggleTurtles, "t")
        # dodanie procedury obsługi dla restartowania animacji
        turtle.onkey(spiroAnim.restart, "space")

    # rozpoczęcie głównej pętli turtle
    turtle.mainloop()

# wywołanie main
if __name__ == '__main__':
    main()
