# This is an enhanced version of the chain (catenary curve) example
# discussed in Chapter 5 of the book Object-Oriented Programming in Python
# Authors: Jason Fritts, Michael Goldwasser

from cs1graphics import *
from math import sqrt

#*************************
#  Define constants
#*************************

# example chain length, number of links, link length, etc.
displaySpeed        = 1.0           # larger for faster display, and vice versa
numLinks            = 50            # number of chain links
restingLength       = 21.0          # initial length of each link (in pixels)
totalSeparation     = 630.0         # distance between chain ends (in pixels)

gravityConstant     = ( 1.0 / 32.2 ) * (totalSeparation / 600)
elasticityConstant  = 1.0 / totalSeparation
speed               = displaySpeed * (numLinks / 20)
epsilon             = sqrt(speed) / max(totalSeparation,400)


#*************************
#  Define functions
#*************************

#### convenient function for adding up to three (x,y) tuples
def combine(A, B, C=(0,0) ):
    return (A[0] + B[0] + C[0], A[1] + B[1] + C[1])

#### function returns a tuple representing the force being
#### exerted upon point A due to the link joining A to B.
def calcForce(A, B):
    dX = (B[0] - A[0])
    dY = (B[1] - A[1])
    distance = sqrt(dX * dX + dY * dY)

    forceFactor = 0
    if distance > restingLength:                  # link being stretched
        stretch = distance - restingLength
        forceFactor = stretch * elasticityConstant

    return (forceFactor * dX, forceFactor * dY)   # returning a tuple

#### function to alter the graphical path and refresh canvas
def drawChain(chainData, chainPath, theCanvas):
    for k in range(len(chainData)):
        chainPath.setPoint(Point(chainData[k][0], chainData[k][1]), k)
    theCanvas.refresh()

#*************************
#  Main program
#*************************

# initialize the chain; one end at (0,0) other at (totalSeparation,0)
chain = []
for k in range(numLinks + 1):
    X = totalSeparation * k / numLinks
    chain.append( (X, 0.0) )        # add new position

# initialize the graphics
paper = Canvas(totalSeparation, totalSeparation)
paper.setAutoRefresh(False)

curve = Path()
for p in chain:
    curve.addPoint(Point(p[0], p[1]))
paper.add(curve)

graphicsCounter = int(25 * speed)   # we will only draw some iterations

# as long as forces are not (sufficiently) in equilibrium, adjust
#    chain link positions and re-draw
somethingMoved = True               # force loop to start
while somethingMoved:
    somethingMoved = False          # default for new iteration
    oldChain = list(chain)          # record a copy of the data

    # examine forces being applied to a chain link, and
    #    adjust link position if not at equilibrium
    for k in range(1, numLinks):
        gravForce = (0, gravityConstant)                    # downward force
        leftForce = calcForce(oldChain[k], oldChain[k-1])
        rightForce = calcForce(oldChain[k], oldChain[k+1])
        adjust = combine(gravForce, leftForce, rightForce)
        chain[k] = combine(oldChain[k], adjust)
    
        if abs(adjust[0]) > epsilon or abs(adjust[1]) > epsilon:
            somethingMoved = True

    # only draw intermittent results, otherwise display takes too long
    graphicsCounter -= 1
    if graphicsCounter == 0:
        drawChain(chain, curve, paper)
        graphicsCounter = int(25 * speed)

# display final result with line emphasized (bold)
curve.setBorderWidth(2)
drawChain(chain, curve, paper)
