# Filename: A07_air_track_hollow_cars.py
# Written by: James D. Miller
# 7:34 PM Wed June 19, 2019

# Python
import sys, os
import pygame
import datetime

# PyGame Constants
from pygame.locals import *
from pygame.color import THECOLORS

# gui from Phil's PyGame Utilities
from pgu import gui

#=====================================================================
# Classes
#=====================================================================

class TrackGuiControls( gui.Table):
    def __init__(self, **params):
        gui.Table.__init__(self, **params)

        text_color = THECOLORS["yellow"]  #(0, 0, 255)
        
        # Make a table row in the gui (like a row in HTML).
        self.tr()
        
        # Color transfer.
        self.td( gui.Label(" Color Transfer (c): ", color=text_color), align=1)
        self.td( gui.Switch(value=False, name='colorTransfer'))
        
        # Stickiness.
        self.td( gui.Label("     Fix stickiness (s): ", color=text_color), align=1)
        self.td( gui.Switch(value=True, name='fix_Stickiness'))
        
        # Gravity.
        self.td( gui.Label("     Gravity (g): ", color=text_color))
        self.td( gui.HSlider(0,-3,3, size=20, width=100, height=16, name='gravity_factor'))
        
        # Freeze the cars.
        self.td( gui.Label("     Freeze (f): ", color=text_color))
        # Form element (freeze_button).
        freeze_button = gui.Button("v=0")
        # Note: must invoke the method to be called WITHOUT parentheses.
        freeze_button.connect( gui.CLICK, self.stop_cars)
        self.td( freeze_button)
        
        # Just a help tip for starting a new demo.
        #self.td( gui.Label("       New demo (0-9)", color=THECOLORS["green"]))
    
    # The method that's called by the button must be defined here in this class.
    def stop_cars(self):
        air_track.stop_the_cars()
    
    # Set air_track attributes based on the values returned from the gui.
    def queryIt(self):
        # Color transfer.
        air_track.color_transfer = gui_form['colorTransfer'].value
        
        # Stickiness
        air_track.fix_wall_stickiness = gui_form['fix_Stickiness'].value
        air_track.fix_car_stickiness = air_track.fix_wall_stickiness            
        
        # Gravity: modify the base value by the form scaling factor.
        air_track.g_mps2 = air_track.gbase_mps2 * (gui_form['gravity_factor'].value/2.0)
        
        
class NumberReport:
    def __init__(self, mode):
        self.enabled = False
        if mode=='counter':
            self.mode = 'counter'
            self.font = pygame.font.SysFont("Arial", 14)    
    
    def update(self, numeric_value):
        if self.enabled:
            if self.mode=='counter':
                top_px = 2
                height_px = 18
                width_px = 40
                left_px = game_window.width_px - width_px
                pygame.draw.rect( game_window.surface, THECOLORS["blue"], pygame.Rect(left_px, top_px, width_px, height_px))
                cnt_string = "%.0f" % numeric_value
                txt_surface = self.font.render( cnt_string, True, THECOLORS["white"])
                game_window.surface.blit( txt_surface, [left_px+2, top_px+1])
        
        
class Client:
    def __init__(self, cursorString_color):
        self.cursor_location_px = (0,0)   # x_px, y_px
        self.mouse_button = 1             # 1, 2, or 3
        self.buttonIsStillDown = False        
        
        self.cursorString_color = cursorString_color
        
        self.selected_car = None
        
        # Define the nature of the cursor strings, one for each mouse button.
        self.mouse_strings = {'string1':{'c_drag':   2.0, 'k_Npm':   60.0},
                              'string2':{'c_drag':   0.2, 'k_Npm':    2.0},
                              'string3':{'c_drag':  20.0, 'k_Npm': 1000.0}}
                                        
    def calc_tether_forces_on_cars(self):
        # Calculated the string forces on the selected car and add to the aggregate
        # that is stored in the car object.
        
        # Only check for a selected car if one isn't already selected. This keeps
        # the car from unselecting if cursor is dragged off the car!
        if (self.selected_car == None):
            if self.buttonIsStillDown:
                self.selected_car = air_track.checkForCarAtThisPosition(self.cursor_location_px)        
        
        # If a car is selected
        else:
            if not self.buttonIsStillDown:
                # Unselect the car and bomb out of here.
                self.selected_car.selected = False
                self.selected_car = None
                return None
            
            # If button is down, calculate the forces on the car.
            else:
                # Use dx difference to calculate the hooks law force being applied by the tether line. 
                # If you release the mouse button after a drag it will fling the car.
                # This tether force will diminish as the car gets closer to the mouse point.
                dx_m = env.m_from_px( self.cursor_location_px[0]) - self.selected_car.center_m
                
                stringName = "string" + str(self.mouse_button)
                self.selected_car.cursorString_spring_force_N  += dx_m * self.mouse_strings[stringName]['k_Npm']
                self.selected_car.cursorString_carDrag_force_N += (self.selected_car.v_mps * 
                                                                   (-1) * self.mouse_strings[stringName]['c_drag'])
            
    def draw_cursor_string(self):
        car_center_xy_px = (env.px_from_m(self.selected_car.center_m), self.selected_car.center_y_px)        
        pygame.draw.line(game_window.surface, self.cursorString_color, car_center_xy_px, self.cursor_location_px, 1)

            
class GameWindow:
    def __init__(self, screen_tuple_px):
        self.width_px = screen_tuple_px[0]
        self.height_px = screen_tuple_px[1]

        # Create a reference to display's surface object. This object is a pygame "surface".
        # Screen dimensions in pixels (tuple)
        self.surface = pygame.display.set_mode(screen_tuple_px)
        
        # Define the physics-world boundaries of the window.
        self.left_m = 0.0
        self.right_m = env.m_from_px(self.width_px)
        
        # Paint screen black.
        self.erase_and_update()
        
    def update_caption(self, title):
        pygame.display.set_caption(title)
        self.caption = title
        
    def erase_and_update(self):
        # Useful for shifting between the various demos.
        self.surface.fill(THECOLORS["black"])
        pygame.display.flip()
        

class Detroit:
    def __init__(self, color=THECOLORS["white"], left_px=10, width_px=26, height_px=98, hollow=True, m_kg=None, v_mps=1, density_kgpm2=600.0):
        
        self.color = color
        self.hollow = hollow
        
        self.height_px = height_px        
        self.top_px    = game_window.height_px - self.height_px
        self.width_px  = width_px
        
        # Use y midpoint for drawing the cursor line.
        self.center_y_px = int(round( float(game_window.height_px - self.height_px) + float(self.height_px)/2.0) )
        # For use with cursor-tethers selection.
        self.selected = False
        
        self.width_m = env.m_from_px( width_px)
        self.halfwidth_m = self.width_m/2.0
        
        self.height_m = env.m_from_px( height_px)
        
        # Initialize the position and velocity of the car. These are affected by the
        # physics calcs in the Track.
        self.center_m = env.m_from_px(left_px) + self.halfwidth_m
        self.v_mps = v_mps
        
        # Aggregate type forces acting on car.
        self.cursorString_spring_force_N = 0
        self.cursorString_carDrag_force_N = 0

        # Calculate the mass of the car.
        if self.hollow:
            self.m_kg = m_kg
        else:
            self.density_kgpm2 = density_kgpm2
            self.m_kg = self.height_m * self.width_m * self.density_kgpm2
        
        # Increment the car count. The air_track object has global scope.
        air_track.carCount += 1
        # Name this car based on this air_track attribute.
        self.name = air_track.carCount
        
        # Update the list of masses and recalculate the maximum.
        air_track.mass_list.append(self.m_kg)
        air_track.max_m_kg = max(air_track.mass_list)
        
        # Create a rectangle object based on these dimensions
        # Left: distance from the left edge of the screen in px.
        # Top:  distance from the top  edge of the screen in px.
        self.rect = pygame.Rect(left_px, self.top_px, self.width_px, self.height_px)
        
        # Calculate the hole characteristics (shrink values).
        if self.hollow:
            if (self.m_kg == air_track.max_m_kg):
                # If you're the top dog (heaviest), everyone else will have to re-calculate their shrink
                # values accordingly.
                    for eachcar in air_track.cars:
                        eachcar.calc_hole_shrink()
            else:
                # Oh well, not the top dog. Then I'm the only one that needs to calculate
                # shrink values.
                self.calc_hole_shrink()
    
    def calc_hole_shrink(self):
        # Calculate a special density in kg per pixel area. Use this only for the 
        # hole calculation. Notice the reference to the air_track's max_m_kg which is
        # the mass of the heaviest car.
        
        self.density_kgppx2 = float(air_track.max_m_kg)/float(self.width_px * self.height_px)
        
        # Keep the hole width consistent for all the cars.
        hole_width_pxi = self.width_px - 2
        
        # Calculate the hole height based on the difference in mass it represents.
        hole_height_pxf = (air_track.max_m_kg - self.m_kg)/(self.density_kgppx2 * hole_width_pxi)
        hole_height_pxi = int(round(hole_height_pxf))
        
        # These shrink values will be used (relative to the main car rectangle) when time comes 
        # to draw the rectangle that represents the hole.
        self.shrink_x_px = self.width_px - hole_width_pxi
        self.shrink_y_px = self.height_px - hole_height_pxi
    
    def draw_car(self):
        # Update the pixel position of the car's rectangle object to match the value
        # controlled by the physics calculations.
        self.rect.centerx = env.px_from_m( self.center_m)
        
        # Draw the main rectangle.
        pygame.draw.rect(game_window.surface, self.color, self.rect)
        
        if self.hollow and (self.m_kg <> air_track.max_m_kg):
        
            # Draw a subrectangle (a hole) to illustrate the mass of this car relative
            # to the mass of the most massive car. The closer we are to the most massive car
            # the more shrinking of the hole. So heavier cars look more
            # solid.
            
            # Make a hole by shrinking the main rectangle.
            hole_rect = self.rect.inflate(-self.shrink_x_px, -self.shrink_y_px)  # x,y
            
            # Draw the hole.
            pygame.draw.rect(game_window.surface, THECOLORS["black"], hole_rect)        

        
class AirTrack:
    def __init__(self):
        self.clean()    
        self.gui_menu = True
        
        self.clack = pygame.mixer.Sound('clack_long.wav')
        
    def clean(self):
        # Initialize the list of cars.
        self.cars = []
        self.carCount = 0
        self.mass_list = []
        self.max_m_kg = 0
        
        # Coefficients of restitution.
        self.coef_rest_base = 0.90  # Useful for reseting things.
        self.coef_rest_car = self.coef_rest_base
        self.coef_rest_wall = self.coef_rest_base
        
        # Component of gravity along the length of the track.
        self.g_toggle = False
        self.gbase_mps2 = 9.8/40.0 # one 40th of g.
        
        self.color_transfer = False
        
        self.collision_count = 0
        
        #self.fix_wall_stickiness = True
        #self.fix_car_stickiness = True
        gui_form['fix_Stickiness'].value = True
        
        self.pi_collisions = False
        self.piCalc_wallCollision = False
    
    def checkForCarAtThisPosition(self, cursor_location_xy):
        x_px = cursor_location_xy[0]
        y_px = cursor_location_xy[1]
        x_m = env.m_from_px(x_px)
        for car in self.cars:
            if (((x_m > car.center_m - car.halfwidth_m) and (x_m < car.center_m + car.halfwidth_m)) and
                    (y_px > game_window.height_px - car.height_px)):
                car.selected = True
                return car
        return None
    
    def update_SpeedandPosition(self, car, dt_s):
        # Add up all the forces on the car.
        car_forces_N = (car.m_kg * self.g_mps2) + (car.cursorString_spring_force_N + 
                                                   car.cursorString_carDrag_force_N )
        
        # Calculate the acceleration based on the forces and Newton's law.
        car_acc_mps2 = car_forces_N / car.m_kg
        
        # Calculate the velocity at the end of this time step.
        v_end_mps = car.v_mps + (car_acc_mps2 * dt_s)
        
        # Calculate the average velocity during this timestep.
        v_avg_mps = (car.v_mps + v_end_mps)/2.0
        
        # Use the average velocity to calculate the new position of the car.
        # Physics note: v_avg*t is equivalent to (v*t + (1/2)*acc*t^2)
        car.center_m = car.center_m + (v_avg_mps * dt_s)
        
        # Assign the final velocity to the car.
        car.v_mps = v_end_mps
        
        # Reset the aggregate forces.
        car.cursorString_spring_force_N = 0
        car.cursorString_carDrag_force_N = 0
        
    def check_for_PI_collisions(self):
        # Car-car collisions
        # Check if right edge of car 0 is beyond the left edge of car 1.
        if ((self.cars[0].center_m + self.cars[0].width_m/2.0) > (self.cars[1].center_m - self.cars[1].width_m/2.0) and self.piCalc_wallCollision):
            self.collision_count += 1
            self.clack.play()
            self.piCalc_wallCollision = False

            if self.color_transfer:
                (self.cars[0].color, self.cars[1].color) = (self.cars[1].color, self.cars[0].color)
                
            # Prevent sticking to other cars.
            if self.fix_car_stickiness:
                self.correct_car_penetrations(self.cars[0], self.cars[1])
                        
            # Calculate the new post-collision velocities.
            (self.cars[0].v_mps, self.cars[1].v_mps) = self.car_and_ocar_vel_AFTER_collision( self.cars[0], self.cars[1])
                    
        # Collisions with walls.
        # Check car 0 for collisions with the left wall.
        # If left-edge of the car is less than the left boundary. 
        if (((self.cars[0].center_m - self.cars[0].width_m/2.0) < game_window.left_m) and (not self.piCalc_wallCollision)):
            self.collision_count += 1
            self.clack.play()
            self.piCalc_wallCollision = True
            
            if self.fix_wall_stickiness:
                self.correct_wall_penetrations( self.cars[0])
            
            self.cars[0].v_mps = -self.cars[0].v_mps * self.coef_rest_wall   
            
        
    def check_for_collisions(self):
        # Collisions with walls.
        # Enumerate so can efficiently check car-car collisions below.
        
        for i, car in enumerate(self.cars):
            
            # Collisions with Left and Right wall.
            #   If left-edge of the car is less than...                OR  If right-edge of car is greater than...
            if ((car.center_m - car.width_m/2.0) < game_window.left_m) or ((car.center_m + car.width_m/2.0) > game_window.right_m):
                self.collision_count += 1
                
                if self.fix_wall_stickiness:
                    self.correct_wall_penetrations(car)
            
                car.v_mps = -car.v_mps * self.coef_rest_wall                
            
            # This makes use of the "enumerate"d for loop above. 
            # In doing so, it avoids checking the self-self case and avoids checking pairs twice
            # like (2 with 3) and (3 with 2).
            # Example checks: (1 with 2,3,4,5), (2 with 3,4,5), (3 with 4,5), (4 with 5) etc...
            for ocar in self.cars[i+1:]:
                # Check for overlap with other rectangle.
                if (abs(car.center_m - ocar.center_m) < (car.halfwidth_m + ocar.halfwidth_m)):
                    self.collision_count += 1

                    if self.color_transfer:
                        (car.color, ocar.color) = (ocar.color, car.color)
                    
                    # Prevent sticking to other cars.
                    if self.fix_car_stickiness:
                        self.correct_car_penetrations(car, ocar)
                    
                    # Calculate the new post-collision velocities.
                    (car.v_mps, ocar.v_mps) = self.car_and_ocar_vel_AFTER_collision( car, ocar)

    def car_and_ocar_vel_AFTER_collision(self, car, ocar, CR=None):
        # If no override CR is provided, use the car's value.
        if (CR == None):
            CR = self.coef_rest_car
            
        # Calculate the AFTER velocities.
        car_vel_AFTER_mps =  ( (CR * ocar.m_kg * (ocar.v_mps - car.v_mps) + car.m_kg*car.v_mps + ocar.m_kg*ocar.v_mps)/
                               (car.m_kg + ocar.m_kg) )
        ocar_vel_AFTER_mps = ( (CR * car.m_kg *  (car.v_mps - ocar.v_mps) + car.m_kg*car.v_mps + ocar.m_kg*ocar.v_mps)/
                               (car.m_kg + ocar.m_kg) )

        return (car_vel_AFTER_mps, ocar_vel_AFTER_mps)
        
    def correct_wall_penetrations(self, car):
        penetration_left_x_m = game_window.left_m - (car.center_m - car.halfwidth_m)
        if penetration_left_x_m > 0:
            car.center_m += 2 * penetration_left_x_m
        
        penetration_right_x_m = (car.center_m + car.halfwidth_m) - game_window.right_m
        if penetration_right_x_m > 0:
            car.center_m -= 2 * penetration_right_x_m
    
    def correct_car_penetrations(self, car, ocar):
        relative_spd_mps = abs(car.v_mps - ocar.v_mps)
        penetration_m = (car.halfwidth_m + ocar.halfwidth_m) - abs(car.center_m - ocar.center_m)
        
        if (relative_spd_mps > 0.0):
            penetration_time_s = penetration_m / relative_spd_mps
            
            # First, back up the two cars, to their collision point, along their incoming trajectory paths.
            # Use BEFORE collision velocities here!
            car.center_m  -= car.v_mps  * penetration_time_s
            ocar.center_m -= ocar.v_mps * penetration_time_s
            
            # Calculate the velocities along the normal AFTER the collision. Use a CR (coefficient of restitution)
            # of 1 here to better avoid stickiness.
            (car_vel_AFTER_mps, ocar_vel_AFTER_mps) = self.car_and_ocar_vel_AFTER_collision( car, ocar, CR=1.0)

            # Finally, travel another penetration time worth of distance using these AFTER-collision velocities.
            # This will put the cars where they should have been at the time of collision detection.
            car.center_m  += car_vel_AFTER_mps  * penetration_time_s
            ocar.center_m += ocar_vel_AFTER_mps * penetration_time_s
        else:
            pass
    
    def stop_the_cars(self):
        for car in self.cars:
            car.v_mps = 0
    
    def make_some_cars(self, nmode):
        # Update the caption at the top of the pygame window frame.
        game_window.update_caption("Air Track (hollow cars): Demo #" + str(nmode)) 
        
        # Scrub off the old cars and reset some stuff.
        air_track.clean()
        
        # For the pi calculation...
        env.counterDisplay.enabled = False
        air_track.pi_collisions = False
        
        if nmode == '1p':
            gui_form['gravity_factor'].value = 0.0
            gui_form['colorTransfer'].value = False
            
            self.cars.append( Detroit(color=THECOLORS["yellow" ], left_px = 240, width_px=20, hollow=False, v_mps=  0.0))
            self.cars.append( Detroit(color=THECOLORS["orange"],  left_px = 340, width_px=30, hollow=False, v_mps= -0.0))
        
        elif nmode == '2p':
            gui_form['gravity_factor'].value = 2.0
            gui_form['colorTransfer'].value = False
            
            self.cars.append( Detroit(color=THECOLORS["yellow" ], left_px = 240, width_px=20, hollow=False, v_mps= -0.1))
            self.cars.append( Detroit(color=THECOLORS["orange"],  left_px = 440, width_px=60, hollow=False, v_mps= -0.2))
        
        elif nmode == '3p':
            gui_form['gravity_factor'].value = -1.0
            gui_form['colorTransfer'].value = True
            
            self.cars.append( Detroit(color=THECOLORS["yellow" ], left_px = 240, width_px=20, hollow=False, v_mps= -0.1))
            self.cars.append( Detroit(color=THECOLORS["orange"],  left_px = 440, width_px=80, hollow=False, v_mps= -0.2))
            
        elif nmode == '4p':
            env.counterDisplay.enabled = True
            air_track.pi_collisions = True
            air_track.piCalc_wallCollision = True
            gui_form['gravity_factor'].value = 0
            gui_form['colorTransfer'].value = False
            gui_form['fix_Stickiness'].value = False
                        
            self.coef_rest_car  = 1.0
            self.coef_rest_wall = 1.0
            
            self.cars.append( Detroit(color=THECOLORS["white"], hollow=False,
                              left_px=200,                              
                              height_px=10,   width_px=20,
                              density_kgpm2=6.0,
                              v_mps=0.00))
            self.cars.append( Detroit(color=THECOLORS["yellow"], hollow=False,
                              left_px=300, 
                              height_px=100, width_px=200,
                              density_kgpm2=6.0 * 100**3,
                              v_mps=-0.03)) 
            
        elif nmode == 0:
            gui_form['gravity_factor'].value = 0
            gui_form['colorTransfer'].value = False
            
            self.coef_rest_car  = 1.00
            self.coef_rest_wall = 1.00    
            x_steps = Next_x( 100, 29)
            cars_v_mps = 0.0 #.15 #m/s
            cars_m_kg =  0.3 #kg
            color_list = ["yellow","red","green","blue","pink"]
            k_color = 0
            for j in range(9):
                if (k_color > (len(color_list) - 1)):
                    k_color = 0
                else:
                    k_color += 0
                self.cars.append( Detroit(color=THECOLORS[color_list[k_color]], left_px=x_steps.step(), 
                                  v_mps=cars_v_mps, m_kg=cars_m_kg))
                    
            cars_v_mps = -0.3 #.15 #m/s
            x_steps = Next_x( 750, 45)
            k_color = 0
            for j in range(3):
                if (k_color > (len(color_list) - 1)):
                    k_color = 2
                else:
                    k_color += 1
                self.cars.append( Detroit(color=THECOLORS[color_list[k_color]], left_px=x_steps.step(), 
                                  v_mps=cars_v_mps, m_kg=cars_m_kg))        
        
        elif nmode == 1:
            gui_form['gravity_factor'].value = 2
            gui_form['colorTransfer'].value = True
            
            self.coef_rest_car  = 0.95
            self.coef_rest_wall = 0.95    
            
            color_list = ["yellow","red","green","blue","pink"]
            x_steps = Next_x( 050, 30)
            cars_v_mps = 0.0 #.15 #m/s
            cars_m_kg =  0.3 #kg
            k_color = 0
            for j in range(4):
                if (k_color > (len(color_list) - 1)):
                    k_color = 0
                self.cars.append( Detroit(color=THECOLORS[color_list[k_color]], left_px=x_steps.step(), 
                                  v_mps=cars_v_mps, m_kg=cars_m_kg))
                k_color += 0
            k_color += 1
            self.cars.append( Detroit(color=THECOLORS[color_list[k_color]], left_px=x_steps.step(), 
                              v_mps=cars_v_mps, m_kg=cars_m_kg))
            
        elif nmode == 2:
            gui_form['gravity_factor'].value = 3
            gui_form['colorTransfer'].value = False
            
            x_steps = Next_x( 450, 30)
            cars_v_mps = 0.0 #.15 #m/s
            cars_m_kg =  0.3 #kg
            
            self.cars.append( Detroit(color=THECOLORS["white"], left_px=x_steps.step(), v_mps=cars_v_mps, m_kg=cars_m_kg*6))
            self.cars.append( Detroit(color=THECOLORS["white"], left_px=x_steps.step(), v_mps=cars_v_mps, m_kg=cars_m_kg*8))    
        
        elif nmode == 3:            
            gui_form['gravity_factor'].value = 3
            gui_form['colorTransfer'].value = False
            
            x_steps = Next_x( 450, 30)
            cars_v_mps = 0.0 #.15 #m/s
            cars_m_kg =  0.3 #kg
            
            self.cars.append( Detroit(color=THECOLORS["white"], left_px=x_steps.step(), v_mps=cars_v_mps, m_kg=cars_m_kg*1))
            self.cars.append( Detroit(color=THECOLORS["white"], left_px=x_steps.step(), v_mps=cars_v_mps, m_kg=cars_m_kg*8))                
        
        elif nmode == 4:
            gui_form['colorTransfer'].value = False
            
            self.coef_rest_car  = 0.0
            self.coef_rest_wall = 1.0
            
            gui_form['gravity_factor'].value = 0
            
            cars_v_mps = 0.1 #.15 #m/s
            cars_m_kg =  0.3 #kg
            
            self.cars.append( Detroit(color=THECOLORS["white"], left_px= 30, v_mps=+7.0*cars_v_mps, m_kg=2*cars_m_kg))
            self.cars.append( Detroit(color=THECOLORS["white"], left_px=500, v_mps=+2.0*cars_v_mps, m_kg=1*cars_m_kg))  
            self.cars.append( Detroit(color=THECOLORS["white"], left_px=600, v_mps=-2.0*cars_v_mps, m_kg=2*cars_m_kg))  
            self.cars.append( Detroit(color=THECOLORS["white"], left_px=700, v_mps=-1.0*cars_v_mps, m_kg=1*cars_m_kg))  
            self.cars.append( Detroit(color=THECOLORS["white"], left_px=900, v_mps=-5.5*cars_v_mps, m_kg=2*cars_m_kg))  
            
        elif nmode == 5:
            self.coef_rest_car  = 1
            self.coef_rest_wall = 1          
            
            gui_form['gravity_factor'].value = 0
            gui_form['colorTransfer'].value = True
            
            x_steps = Next_x(450, 35)
            cars_v_mps = 0.0 #.15 #m/s
            cars_m_kg =  0.3 #kg
            
            for j in range(10):
                self.cars.append( Detroit(color=THECOLORS["yellow"], 
                                             left_px=x_steps.step(),  
                                             v_mps=cars_v_mps, 
                                             m_kg=cars_m_kg))
            self.cars.append( Detroit(color=THECOLORS["red"], left_px=x_steps.step(), v_mps=0.5, m_kg=cars_m_kg)) 
        
        elif nmode == 6:
            self.coef_rest_car  = 1
            self.coef_rest_wall = 1          
            
            gui_form['gravity_factor'].value = -1
            gui_form['colorTransfer'].value = True
            
            x_steps = Next_x(450, 35)
            cars_v_mps = 0.1
            cars_m_kg =  0.3 #kg
            
            for j in range(10):
                self.cars.append( Detroit(color=THECOLORS["yellow"], 
                                             left_px=x_steps.step(),  
                                             v_mps=cars_v_mps, 
                                             m_kg=cars_m_kg))
            self.cars.append( Detroit(color=THECOLORS["red"], left_px=x_steps.step(), v_mps=0.5, m_kg=cars_m_kg)) 
        
        elif nmode == 7:
            self.coef_rest_car  = 1 #0.99
            self.coef_rest_wall = 1 #0.99          
            
            gui_form['gravity_factor'].value = 0
            
            gui_form['colorTransfer'].value = True
            
            x_steps = Next_x(450, 35)
            cars_v_mps = 0.0 #.15 #m/s
            cars_m_kg =  0.3 #kg
            
            for j in range(10):
                cars_m_kg += .1
                self.cars.append( Detroit(color=THECOLORS["yellow"], 
                                             left_px=x_steps.step(),  
                                             v_mps=cars_v_mps, 
                                             m_kg=cars_m_kg))
            cars_m_kg += .1
            self.cars.append( Detroit(color=THECOLORS["red"], left_px=x_steps.step(), v_mps=0.5, m_kg=cars_m_kg)) 
            
        elif nmode == 8:
            gui_form['colorTransfer'].value = False
            
            self.coef_rest_car  = 1.000
            self.coef_rest_wall = 1.000 
            
            gui_form['gravity_factor'].value = 0

            cars_v_mps = 0.0 #.15 #m/s
            cars_m_kg =  0.3 #kg
            
            # This does interesting COMPLETE energy transfers between the cars when the first car is
            # 3,4,5,6... times the mass of the other car, and the lighter car is initially stationary.
            # If the heavy car is initially stationary, then only 3x works.
            self.cars.append( Detroit(color=THECOLORS["yellow"], left_px=200, v_mps=0, m_kg=3*cars_m_kg))
            self.cars.append( Detroit(color=THECOLORS["red"],    left_px=500, v_mps=1, m_kg=1*cars_m_kg))  
            
        elif nmode == 9:
            #self.gui_menu = True #False
            gui_form['gravity_factor'].value = 0
            gui_form['colorTransfer'].value = True
            
            self.coef_rest_car  = 1.00
            self.coef_rest_wall = 1.00    
            
            cars_v_mps = 0.0 #.15 #m/s
            cars_m_kg =  0.3 #kg
            color_list = ["yellow","red","green","blue","pink","white","darkgrey"]

            self.cars.append( Detroit(color=THECOLORS[color_list[1]], left_px= 20, v_mps= 0.05, m_kg=cars_m_kg))        
            x_steps = Next_x( 50, 29)
            for j in range(12):
                self.cars.append( Detroit(color=THECOLORS[color_list[0]], left_px=x_steps.step(), v_mps=cars_v_mps, m_kg=cars_m_kg))
            self.cars.append( Detroit(color=THECOLORS[color_list[5]], left_px= 430, v_mps=-0.05, m_kg=cars_m_kg))        
                
            self.cars.append( Detroit(color=THECOLORS[color_list[3]], left_px= 460, v_mps= 0.20, m_kg=cars_m_kg))        
            x_steps = Next_x( 500, 29)
            for j in range(12):
                self.cars.append( Detroit(color=THECOLORS[color_list[0]], left_px=x_steps.step(), v_mps=cars_v_mps, m_kg=cars_m_kg))
            self.cars.append( Detroit(color=THECOLORS[color_list[6]], left_px=890, v_mps=-0.05, m_kg=cars_m_kg))        
            
        else:
            print "nothing set up for this key"
            
            
class Next_x:
    # Initialize the positions of the cars.
    def __init__(self, x_start, x_increment):
        self.x = x_start
        self.dx = x_increment
    def step(self):
        self.x += self.dx
        return self.x
        

class Environment:
    def __init__(self, length_px, length_m):
        self.px_to_m = length_m/float(length_px)
        self.m_to_px = (float(length_px)/length_m)
        
        # Add a local (non-network) client to the client dictionary.
        self.clients = {'local':Client(THECOLORS["green"])}
        self.gui_controls = None
    
    # Convert from meters to pixels
    def px_from_m(self, dx_m):
        return int(round(dx_m * self.m_to_px))
    
    # Convert from pixels to meters
    def m_from_px(self, dx_px):
        return float(dx_px) * self.px_to_m
        
    def shift_key_down(self):
        keys = pygame.key.get_pressed()
        if (keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]):
            return True
        
    def get_local_user_input(self):
        # Get all the events since the last call to get().
        for event in pygame.event.get():
            if (event.type == pygame.QUIT): 
                return 'quit'
            elif (event.type == pygame.KEYDOWN):
                if (event.key == K_ESCAPE):
                    return 'quit'
                elif (event.key==K_KP1):            
                    return "1p"
                elif (event.key==K_KP2):            
                    return "2p"
                elif (event.key==K_KP3):            
                    return "3p"
                elif (event.key==K_KP4):            
                    return "4p"
                elif (event.key==K_1):
                    if self.shift_key_down():
                        return "1p"
                    else:
                        return 1           
                elif (event.key==K_2):
                    if self.shift_key_down():
                        return "2p"
                    else:
                        return 2
                elif (event.key==K_3):
                    if self.shift_key_down():
                        return "3p"
                    else:
                        return 3           
                elif (event.key==K_4):
                    if self.shift_key_down():
                        return "4p"
                    else:
                        return 4           
                elif (event.key==K_5):
                    return 5
                elif (event.key==K_6):
                    return 6
                elif (event.key==K_7):
                    return 7
                elif (event.key==K_8):
                    return 8
                elif (event.key==K_9):
                    return 9
                elif (event.key==K_0):
                    return 0
                
                elif (event.key==K_s):
                    gui_form['fix_Stickiness'].value = not gui_form['fix_Stickiness'].value
                
                elif (event.key==K_c):
                    gui_form['colorTransfer'].value = not gui_form['colorTransfer'].value
                
                elif (event.key==K_f):
                    air_track.stop_the_cars()
                    
                elif (event.key==K_g):
                    air_track.g_toggle = not air_track.g_toggle
                    if air_track.g_toggle:
                        gui_form['gravity_factor'].value = -2
                    else:
                        gui_form['gravity_factor'].value = 0
                
                elif (event.key==K_F2):
                    # Turn the gui menu on/off
                    air_track.gui_menu = not air_track.gui_menu
                    
                elif (event.key==K_n):
                    print "collision count reset to 0 from", air_track.collision_count
                    air_track.collision_count = 0
                
                elif (event.key==K_LSHIFT):
                    pass
                
                else:
                    return "Nothing set up for this key."
            
            elif (event.type == pygame.KEYUP):
                pass
            
            elif (event.type == pygame.MOUSEBUTTONDOWN):
                self.clients['local'].buttonIsStillDown = True
            
                (button1, button2, button3) = pygame.mouse.get_pressed()
                if button1:
                    self.clients['local'].mouse_button = 1
                elif button2:
                    self.clients['local'].mouse_button = 2
                elif button3:
                    self.clients['local'].mouse_button = 3
                else:
                    self.clients['local'].mouse_button = 0
            
            elif event.type == pygame.MOUSEBUTTONUP:
                self.clients['local'].buttonIsStillDown = False
                self.clients['local'].mouse_button = 0
            
            # In all cases, pass the "event" to the Gui application.
            gui_application.event( event)
            
        if self.clients['local'].buttonIsStillDown:
            # If it is down, get the cursor position.
            self.clients['local'].cursor_location_px = (mouseX, mouseY) = pygame.mouse.get_pos()
                
        
#============================================================
# Main procedural functions.
#============================================================

def main():

    # A few globals.
    global env, game_window, air_track, gui_form, gui_application
    
    # Initiate pygame
    pygame.mixer.pre_init(44100, -16, 2, 2048)
    pygame.mixer.init()
    pygame.init()

    # Tuple to define window dimensions
    window_size_px = window_width_px, window_height_px = 950, 120

    # Instantiate an Environment object for converting back and forth from pixels and meters.
    # The also creates the local client.
    env = Environment(window_width_px, 1.5)

    # Instantiate the window.
    game_window = GameWindow(window_size_px)
    
    # Initialize gui...
    gui_form = gui.Form()
    env.gui_controls = TrackGuiControls()
    env.counterDisplay = NumberReport('counter')
    
    gui_container = gui.Container(align=-1, valign=-1)
    gui_container.add(env.gui_controls, 0, 0)
    
    gui_application = gui.App()
    gui_application.init( gui_container)

    # Instantiate an air track (this adds an empty car list to the track).
    air_track = AirTrack()

    # Make some cars (run demo #1).
    air_track.make_some_cars(1)

    # Instantiate clock to help control the framerate.
    myclock = pygame.time.Clock()
        
    # Control the framerate.
    framerate_limit = 400

    time_s = 0.0
    user_done = False
    
    while not user_done:
    
        # Erase everything.
        game_window.surface.fill(THECOLORS["black"])

        # Get the delta t for one frame (this changes depending on system load).
        dt_s = float(myclock.tick(framerate_limit) * 1e-3)
        
        # This check avoids problem when dragging the game window.
        if (dt_s < 0.10):
        
            # Check for user initiated stop or demo change.
            resetmode = env.get_local_user_input()
            
            if (resetmode in ["1p","2p","3p","4p",1,2,3,4,5,6,7,8,9,0]):
                print "demo mode =", resetmode
                
                # This should remove all references to the cars and effectively deletes them.
                #air_track.cars = []
                
                # Now just black everything out and update the screen.
                game_window.erase_and_update()
                
                # Build new set of cars based on the reset mode.
                air_track.make_some_cars( resetmode)
            
            elif (resetmode == 'quit'):
                user_done = True
                
            elif (resetmode != None):
                print resetmode
            
            # Set object attributes based on the values returned from a query of the Gui.
            # Important to do this AFTER all initialization and car building is over, AFTER
            # any user input from the pygame event queue, and BEFORE the updates to the speed
            # and position.
            env.gui_controls.queryIt()
            
            # Calculate client related forces.
            for client_name in env.clients:
                env.clients[client_name].calc_tether_forces_on_cars()
            
            if (air_track.pi_collisions):
                nFinerTimeStepFactor = 1000 # 1000 works well for up to 5 digits of pi.
                for j in range( nFinerTimeStepFactor):
                    for car in air_track.cars:
                        air_track.update_SpeedandPosition(car, dt_s/float( nFinerTimeStepFactor))
                    air_track.check_for_PI_collisions()
            else:
                # Update velocity and x position of each car based on the dt_s for this frame.
                for car in air_track.cars:
                    air_track.update_SpeedandPosition(car, dt_s)
                    
                # Check for collisions and apply collision physics to determine resulting
                # velocities.
                air_track.check_for_collisions()
            
            # Draw the car at the new position.
            for car in air_track.cars:
                car.draw_car()
                
            # Draw cursor strings.
            for client_name in env.clients:
                if (env.clients[client_name].selected_car != None):
                    env.clients[client_name].draw_cursor_string()
                        
            # Update the total time since starting.
            time_s += dt_s
            
            # Paint the gui. (F2 toggles gui on/off)
            if air_track.gui_menu:
                gui_application.paint()
            
            # This is will only display if env.counterDisplay.enabled is True.
            env.counterDisplay.update( air_track.collision_count)
                    
            # Make this update visible on the screen.
            pygame.display.flip()
            
#============================================================
# Run the main program.    
#============================================================

main()