Beispiel #1
0
    def test_timer(self):
        self.callbackcomplete = False
        self.timer = Timer()
        self.timer.callAfter(0.1, self.timercallback)
        ma = MathApp()
        ma.run()

        for i in range(10):
            time.sleep(1 / 60)
            ma._animate(1)

        self.assertEquals(self.callbackcomplete, True)

        self.timer.destroy()
Beispiel #2
0
    def test_timer(self):
        self.callbackcomplete = False
        self.timer = Timer()
        self.timer.callAfter(0.1, self.timercallback)
        ma = MathApp()
        ma.run()

        for i in range(10):
            time.sleep(1 / 60)
            ma._animate(1)

        self.assertEquals(self.callbackcomplete, True)

        self.timer.destroy()
Beispiel #3
0
class Rocket(ImagePoint):
    """
    Rocket is a class for simulating the motion of a projectile through space,
    acted upon by arbitrary forces (thrust) and by gravitaitonal
    attraction to a single planetary object.

    Required parameters:

    :param Planet planet:  Reference to a :class:`Planet` object.

    Optional keyword parameters are supported:

    :param str bitmap:  Url of a suitable bitmap image for the rocket (png
        recommended), default is `images/rocket.png`
    :param float bitmapscale:  Scale factor for bitmap. Default is 0.1
    :param float velocity:  Initial rocket speed. Default is zero.
    :param float directiond:  Initial rocket direction in degrees. Default is zero.
    :param float direction:  Initial rocket direction in radians. Default is zero.
    :param float tanomalyd:  Initial rocket true anomaly in degrees. Default is 90.
    :param float tanomaly:  Initial rocket true anomaly in radians. Default is pi/2.
    :param float altitude:  Initial rocket altitude in meters. Default is zero.
    :param bool showstatus:  Boolean displays flight parameters on screen. Default
        is True.
    :param (int, int) statuspos:  Tuple with screen x,y coordinates of flight
        parameters. Default is upper left.
    :param list[str] statuslist: List of status names to include in flight parameters.
      Default is all, consisting of: "velocity", "acceleration", "course",
      "altitude", "thrust", "mass", "trueanomaly", "scale", "timezoom",
      "shiptime"
    :param str leftkey: A :class:`ggame.event.KeyEvent` key identifier that will serve
        as the "rotate left" key while controlling the ship. Default is 'left arrow'.
    :param str rightkey: A :class:`ggame.event.KeyEvent` key identifier that will serve
        as the "rotate right" key while controlling the ship. Default is 'right arrow'.

    Following parameters may be set as a constant value, or pass in the
    name of a function that will return the value dynamically or the
    name of a `ggmath` UI control that will return the value.

    :param function or float timezoom:  Scale factor for time zoom. Factor =
        10^timezoom
    :param function or float heading:  Direction to point the rocket in (must be
        radians)
    :param fucntion or float mass: Mass of the rocket (must be kg)
    :param function or float thrust: Thrust of the rocket (must be N)

    Animation related parameters may be ignored if no sprite animation:

    :param Frame bitmapframe:  ((x1,y1),(x2,y2)) tuple defines a region in the bitmap
    :param int bitmapqty: Number of bitmaps -- used for animation effects
    :param str bitmapdir: "horizontal" or "vertical" use with animation effects. Default
        is "horizontal"
    :param int bitmapmargin: Pixels between successive animation frames
    :param float tickrate: Frequency of spacecraft dynamics calculations (Hz)

    Example:

    .. literalinclude:: ../examples/astrorocket.py

    """
    def __init__(self, planet, **kwargs):
        self._xy = (1000000, 1000000)
        self.planet = planet
        """Reference to an app object of :class:`Planet` class"""
        self._bmurl = kwargs.get("bitmap", self.getImagePath("rocket.png"))
        """URL of a bitmap image to use for the rocket"""
        self._bitmapframe = kwargs.get("bitmapframe", None)
        """:class:`ggame.asset.Frame` that specifies location of displayed
            image in bitmap image
        """
        self._bitmapqty = kwargs.get("bitmapqty", 1)
        """Number of sub-images in the bitmap image"""
        self._bitmapdir = kwargs.get("bitmapdir", "horizontal")
        """Orientation of the sub-images for animation (one of "horizontal" or
            "vertical")
        """
        self._bitmapmargin = kwargs.get("bitmapmargin", 0)
        """Margin between the sub-images for animation."""
        self.tickrate = kwargs.get("tickrate", 30)
        """Target animation frames per second."""
        # status display
        statusfuncs = [
            self._velocityText,
            self._accelerationText,
            self._courseDegreesText,
            self._altitudeText,
            self._thrustText,
            self._massText,
            self._trueAnomalyDegreesText,
            self._scaleText,
            self._timeZoomText,
            self._shipTimeText,
        ]
        statuslist = [
            "velocity",
            "acceleration",
            "course",
            "altitude",
            "thrust",
            "mass",
            "trueanomaly",
            "scale",
            "timezoom",
            "shiptime",
        ]

        self._showstatus = kwargs.get("showstatus", True)
        """Boolean: True to show stats on screen, else False."""
        self._statuspos = kwargs.get("statuspos", [10, 10])
        """Location on screen for showing the stats."""
        self._statusselect = kwargs.get("statuslist", statuslist)
        """Reference to a list of statuses to display."""
        self.localheading = 0
        """Heading (angle) of travel"""
        # dynamic parameters
        self.timezoom = self.eval(kwargs.get("timezoom", 0))
        """Reference to a function that will dynamically determine the
        time zoom factor. An integer: 1,2,3 faster, -1, slower.
        """
        self.heading = self.eval(kwargs.get("heading", self._getheading))
        """Reference to a function that will dynamically calculate spaceship
        orientation or heading (in radians)
        """
        self.mass = self.eval(kwargs.get("mass", 1))
        """Reference to a function that will dynamically calculate the
        spaceship mass (in kg).
        """
        self.thrust = self.eval(kwargs.get("thrust", 0))
        """Reference to a funtion that will dynamically calculate the rocket
        thrust (in N).
        """
        # end dynamic
        super().__init__(
            self._bmurl,
            self._getposition,
            frame=self._bitmapframe,
            qty=self._bitmapqty,
            direction=self._bitmapdir,
            margin=self._bitmapmargin,
        )
        self.scale = kwargs.get("bitmapscale", 0.1)  # small
        initvel = kwargs.get("velocity", 0)  # initial velocity
        initdird = kwargs.get("directiond", 0)  # initial direction, degrees
        initdir = kwargs.get("direction", radians(initdird))
        tanomaly = kwargs.get("tanomaly", pi / 2)  # position angle
        tanomaly = radians(kwargs.get("tanomalyd", degrees(tanomaly)))
        altitude = kwargs.get("altitude", 0)  #
        r = altitude + self.planet.radius
        self._xy = (r * cos(tanomaly), r * sin(tanomaly))
        # default heading control if none provided by user
        leftkey = kwargs.get("leftkey", "left arrow")
        rightkey = kwargs.get("rightkey", "right arrow")
        if self.heading == self._getheading:  # pylint: disable=comparison-with-callable
            Planet.listenKeyEvent("keydown", leftkey, self._turn)
            Planet.listenKeyEvent("keydown", rightkey, self._turn)
        self._timer = Timer()
        self.shiptime = 0  # track time on shipboard
        self._timer.callEvery(1 / self.tickrate, self._dynamics)
        self._lasttime = self._timer.time
        self._v_vect = [initvel * cos(initdir), initvel * sin(initdir)]
        self._a_vect = [0, 0]
        # keep track of on-screen resources
        self._labels = []
        # set up status display
        if self._showstatus:
            self.addStatusReport(statuslist, statusfuncs, self._statusselect)

    @staticmethod
    def _vadd(v1, v2):
        """
        Vector add utility.
        """
        return [v1[i] + v2[i] for i in (0, 1)]

    @staticmethod
    def _vmul(s, v):
        """
        Scalar vector multiplication utility.
        """
        return [s * v[i] for i in (0, 1)]

    @staticmethod
    def _vmag(v):
        """
        Vector magnitude function.
        """
        return sqrt(v[0]**2 + v[1]**2)

    def _turn(self, event):
        """


        Respond to left/right turning key events.
        """
        increment = pi / 50 * (1 if event.key == "left arrow" else -1)
        self.localheading += increment

    def _getposition(self):
        return self._xy

    # override or define externally!
    def _getheading(self):
        """
        User override function for dynamically determining the heading.

        :returns: float
        """
        return self.localheading

    # functions available for reporting flight parameters to UI
    def _velocityText(self):
        """
        Report the velocity in m/s as a text string.
        """
        return "Velocity:     {0:8.1f} m/s".format(self.velocity)

    def _accelerationText(self):
        """
        Report the acceleration in m/s as a text string.
        """
        return "Acceleration: {0:8.1f} m/s²".format(self.acceleration)

    def _courseDegreesText(self):
        """
        Report the heading in degrees (zero to the right) as a text string.
        """
        return "Course:       {0:8.1f}°".format(
            degrees(atan2(self._v_vect[1], self._v_vect[0])))

    def _thrustText(self):
        """
        Report the thrust level in Newtons as a text string.
        """
        return "Thrust:       {0:8.1f} N".format(self.thrust())

    def _massText(self):
        """
        Report the spacecraft mass in kilograms as a text string.
        """
        return "Mass:         {0:8.1f} kg".format(self.mass())

    def _trueAnomalyDegreesText(self):
        """
        Report the true anomaly in degrees as a text string.
        """
        return "True Anomaly: {0:8.1f}°".format(self.tanomalyd)

    def _trueAnomalyRadiansText(self):
        """
        Report the true anomaly in radians as a text string.
        """
        return "True Anomaly: {0:8.4f}".format(self.tanomaly)

    def _altitudeText(self):
        """
        Report the altitude in meters as a text string.
        """
        return "Altitude:     {0:8.1f} m".format(self.altitude)

    def _radiusText(self):
        """
        Report the radius (distance to planet center) in meters as a text string.
        """
        return "Radius:       {0:8.1f} m".format(self.r)

    def _scaleText(self):
        """
        Report the view scale (pixels/meter) as a text string.
        """
        return "View Scale:   {0:8.6f} px/m".format(self.planet.scale)

    def _timeZoomText(self):
        """
        Report the time acceleration as a text string.
        """
        return "Time Zoom:    {0:8.1f}".format(float(self.timezoom()))

    def _shipTimeText(self):
        """
        Report the elapsed time as a text string.
        """
        return "Elapsed Time: {0:8.1f} s".format(float(self.shiptime))

    def _dynamics(self, timer):
        """
        Perform one iteration of the simulation using runge-kutta 4th order method.
        """
        # set time duration equal to time since last execution
        tick = 10**self.timezoom() * (timer.time - self._lasttime)
        self.shiptime = self.shiptime + tick
        self._lasttime = timer.time
        # 4th order runge-kutta method
        # (https://sites.temple.edu/math5061/files/2016/12/final_project.pdf)
        # and http://spiff.rit.edu/richmond/nbody/OrbitRungeKutta4.pdf  (succinct,
        # but with a typo)
        self._a_vect = k1v = self._ar(self._xy)
        k1r = self._v_vect
        k2v = self._ar(self._vadd(self._xy, self._vmul(tick / 2, k1r)))
        k2r = self._vadd(self._v_vect, self._vmul(tick / 2, k1v))
        k3v = self._ar(self._vadd(self._xy, self._vmul(tick / 2, k2r)))
        k3r = self._vadd(self._v_vect, self._vmul(tick / 2, k2v))
        k4v = self._ar(self._vadd(self._xy, self._vmul(tick, k3r)))
        k4r = self._vadd(self._v_vect, self._vmul(tick, k3v))
        self._v_vect = [
            self._v_vect[i] + tick / 6 *
            (k1v[i] + 2 * k2v[i] + 2 * k3v[i] + k4v[i]) for i in (0, 1)
        ]
        self._xy = [
            self._xy[i] + tick / 6 *
            (k1r[i] + 2 * k2r[i] + 2 * k3r[i] + k4r[i]) for i in (0, 1)
        ]
        if self.altitude < 0:
            self._v_vect = [0, 0]
            self._a_vect = [0, 0]
            self.altitude = 0

    # generic force as a function of position
    def _fr(self, pos):
        """
        Compute the net force vector on the rocket, as a function of the
        position vector.
        """
        self.rotation = self.heading()
        t = self.thrust()
        g = 6.674e-11
        r = Planet.distance((0, 0), pos)
        uvec = (-pos[0] / r, -pos[1] / r)
        fg = g * self.mass() * self.planet.mass / r**2
        vf = [x * fg for x in uvec]
        return [vf[0] + t * cos(self.rotation), vf[1] + t * sin(self.rotation)]

    # geric acceleration as a function of position
    def _ar(self, pos):
        """
        Compute the acceleration vector of the rocket, as a function of the
        position vector.
        """
        m = self.mass()
        vf = self._fr(pos)
        return [vf[i] / m for i in (0, 1)]

    # add a status reporting function to status display
    def addStatusReport(self, statuslist, statusfuncs, statusselect):
        """
        Accept list of all status names, all status text functions, and
        the list of status names that have been selected for display.

        :param list[str] statuslist: List of status names to include in flight
            parameters. Default is all, consisting of: "velocity", "acceleration",
            "course", "altitude", "thrust", "mass", "trueanomaly", "scale", "timezoom",
            and "shiptime".
        :param list[func] statusfuncs: List of function references corresponding
            to statuslist parameter.
        :param list[str] statusselect: List of names chosen from statuslist to display.

        :returns: None
        """
        statusdict = {n: f for n, f in zip(statuslist, statusfuncs)}
        for name in statusselect:
            if name in statusdict:
                self._labels.append(
                    Label(
                        self._statuspos[:],
                        statusdict[name],
                        size=15,
                        positioning="physical",
                        width=250,
                    ))
                self._statuspos[1] += 25

    @property
    def xyposition(self):
        """
        Report the x,y tuple for logical position of the spaceship.
        """
        return self._xy

    @xyposition.setter
    def xyposition(self, pos):
        self._xy = pos
        # self._touchAsset()

    @property
    def tanomalyd(self):
        """
        Report/set the spaceship position as a direction relative to central body
        (degrees).
        """
        return degrees(self.tanomaly)

    @tanomalyd.setter
    def tanomalyd(self, angle):
        self.tanomaly = radians(angle)

    @property
    def altitude(self):
        """
        Report/set the spaceship altitude of planet surface in meters.
        """
        alt = Planet.distance(self._xy, (0, 0)) - self.planet.radius
        return alt

    @altitude.setter
    def altitude(self, alt):
        r = alt + self.planet.radius
        self._xy = (r * cos(self.tanomaly), r * sin(self.tanomaly))

    @property
    def velocity(self):
        """
        Report the spaceship velocity scalar in m/s.
        """
        return self._vmag(self._v_vect)

    @property
    def acceleration(self):
        """
        Report the spaceship acceleration scalar in m/s.
        """
        return self._vmag(self._a_vect)

    @property
    def tanomaly(self):
        """
        Report/set the spaceship position as a direction relative to central body
        (radians).
        """
        # pos = self._pos()
        return atan2(self._xy[1], self._xy[0])

    @tanomaly.setter
    def tanomaly(self, angle):
        r = self.r
        self._xy = (r * cos(angle), r * sin(angle))
        self.touchAsset()

    @property
    def r(self):
        """
        Report the spaceship distance (radius) from central body center of mass.
        """
        return self.altitude + self.planet.radius
Beispiel #4
0
    def __init__(self, planet, **kwargs):
        self._xy = (1000000, 1000000)
        self.planet = planet
        """Reference to an app object of :class:`Planet` class"""
        self._bmurl = kwargs.get("bitmap", self.getImagePath("rocket.png"))
        """URL of a bitmap image to use for the rocket"""
        self._bitmapframe = kwargs.get("bitmapframe", None)
        """:class:`ggame.asset.Frame` that specifies location of displayed
            image in bitmap image
        """
        self._bitmapqty = kwargs.get("bitmapqty", 1)
        """Number of sub-images in the bitmap image"""
        self._bitmapdir = kwargs.get("bitmapdir", "horizontal")
        """Orientation of the sub-images for animation (one of "horizontal" or
            "vertical")
        """
        self._bitmapmargin = kwargs.get("bitmapmargin", 0)
        """Margin between the sub-images for animation."""
        self.tickrate = kwargs.get("tickrate", 30)
        """Target animation frames per second."""
        # status display
        statusfuncs = [
            self._velocityText,
            self._accelerationText,
            self._courseDegreesText,
            self._altitudeText,
            self._thrustText,
            self._massText,
            self._trueAnomalyDegreesText,
            self._scaleText,
            self._timeZoomText,
            self._shipTimeText,
        ]
        statuslist = [
            "velocity",
            "acceleration",
            "course",
            "altitude",
            "thrust",
            "mass",
            "trueanomaly",
            "scale",
            "timezoom",
            "shiptime",
        ]

        self._showstatus = kwargs.get("showstatus", True)
        """Boolean: True to show stats on screen, else False."""
        self._statuspos = kwargs.get("statuspos", [10, 10])
        """Location on screen for showing the stats."""
        self._statusselect = kwargs.get("statuslist", statuslist)
        """Reference to a list of statuses to display."""
        self.localheading = 0
        """Heading (angle) of travel"""
        # dynamic parameters
        self.timezoom = self.eval(kwargs.get("timezoom", 0))
        """Reference to a function that will dynamically determine the
        time zoom factor. An integer: 1,2,3 faster, -1, slower.
        """
        self.heading = self.eval(kwargs.get("heading", self._getheading))
        """Reference to a function that will dynamically calculate spaceship
        orientation or heading (in radians)
        """
        self.mass = self.eval(kwargs.get("mass", 1))
        """Reference to a function that will dynamically calculate the
        spaceship mass (in kg).
        """
        self.thrust = self.eval(kwargs.get("thrust", 0))
        """Reference to a funtion that will dynamically calculate the rocket
        thrust (in N).
        """
        # end dynamic
        super().__init__(
            self._bmurl,
            self._getposition,
            frame=self._bitmapframe,
            qty=self._bitmapqty,
            direction=self._bitmapdir,
            margin=self._bitmapmargin,
        )
        self.scale = kwargs.get("bitmapscale", 0.1)  # small
        initvel = kwargs.get("velocity", 0)  # initial velocity
        initdird = kwargs.get("directiond", 0)  # initial direction, degrees
        initdir = kwargs.get("direction", radians(initdird))
        tanomaly = kwargs.get("tanomaly", pi / 2)  # position angle
        tanomaly = radians(kwargs.get("tanomalyd", degrees(tanomaly)))
        altitude = kwargs.get("altitude", 0)  #
        r = altitude + self.planet.radius
        self._xy = (r * cos(tanomaly), r * sin(tanomaly))
        # default heading control if none provided by user
        leftkey = kwargs.get("leftkey", "left arrow")
        rightkey = kwargs.get("rightkey", "right arrow")
        if self.heading == self._getheading:  # pylint: disable=comparison-with-callable
            Planet.listenKeyEvent("keydown", leftkey, self._turn)
            Planet.listenKeyEvent("keydown", rightkey, self._turn)
        self._timer = Timer()
        self.shiptime = 0  # track time on shipboard
        self._timer.callEvery(1 / self.tickrate, self._dynamics)
        self._lasttime = self._timer.time
        self._v_vect = [initvel * cos(initdir), initvel * sin(initdir)]
        self._a_vect = [0, 0]
        # keep track of on-screen resources
        self._labels = []
        # set up status display
        if self._showstatus:
            self.addStatusReport(statuslist, statusfuncs, self._statusselect)
Beispiel #5
0
class TestMathMethods(unittest.TestCase):
    def buttonstatus(self):
        return "True" if self.imgbutton() else "False"

    def labelcoords(self):
        return (100 + self.vslider1(), 175)

    def buttoncoords(self):
        return (300 + self.vslider1(), 175)

    def labelcolor(self):
        colorval = self.vslider1()
        return Color(colorval * 256, 1)

    def pressbutton(self, caller):
        print("button pressed: ", caller)

    def __init__(self, arg):
        super().__init__(arg)

    def test_controls(self):
        self.imgbutton = InputImageButton(
            "ggame/images/button-round.png",
            self.pressbutton,
            (0, 0),
            frame=Frame(0, 0, 100, 100),
            qty=2,
        )
        self.imgbutton.scale = 0.5
        self.vslider1 = Slider(
            (100, 150), 0, 250, 125, positioning="physical", steps=10
        )
        self.label = Label(
            self.labelcoords,
            self.buttonstatus,
            size=15,
            positioning="physical",
            color=self.labelcolor,
        )
        self.button = InputButton(
            self.pressbutton,
            self.buttoncoords,
            "Press Me",
            size=15,
            positioning="physical",
        )
        self.numinput = InputNumeric((300, 275), 3.14, positioning="physical")

        ma = MathApp()
        ma.run()

        self.vslider1.destroy()
        self.label.destroy()
        self.button.destroy()
        self.numinput.destroy()
        self.imgbutton.destroy()

    def test_geometry(self):
        self.ip = ImagePoint("bunny.png", (0, 0))
        self.ip.movable = True
        self.p1 = Point((0, 0), color=Color(0x008000, 1))
        self.p1.movable = True
        self.p2 = Point((0, -1))
        self.p3 = Point((1.2, 0))
        self.l1 = LineSegment(self.p2, self.p3, style=LineStyle(3, Color(0, 1)))
        self.l2 = LineSegment(self.p2, self.p1, style=LineStyle(3, Color(0, 1)))
        self.c2 = Circle((-1, -1), self.p1)

        ma = MathApp()
        ma.run()

        self.ip.destroy()
        self.p1.destroy()
        self.p2.destroy()
        self.p3.destroy()
        self.c2.destroy()
        self.l1.destroy()
        self.l2.destroy()

    def test_fancycontrols(self):
        self.imgbutton = InputImageButton(
            "ggame/images/button-round.png",
            self.pressbutton,
            (0, 0),
            frame=Frame(0, 0, 100, 100),
            qty=2,
        )
        self.imgbutton.scale = 0.5
        self.ii = ImageIndicator(
            "ggame/images/red-led-off-on.png",
            (300, 500),
            self.imgbutton,
            positioning="physical",
            frame=Frame(0, 0, 600, 600),
            qty=2,
        )
        self.ii.scale = 0.1
        self.glassbutton = GlassButton(None, (0, -0.5))
        self.toggle = MetalToggle(0, (0, -1))
        self.Li = LEDIndicator((300, 450), self.glassbutton, positioning="physical")
        self.Lit = LEDIndicator((300, 480), self.toggle, positioning="physical")

        ma = MathApp()
        ma.run()

        self.imgbutton.destroy()
        self.ii.destroy()
        self.glassbutton.destroy()
        self.toggle.destroy()
        self.Li.destroy()
        self.Lit.destroy()

    def timercallback(self, timer):
        self.assertEqual(timer, self.timer)
        self.callbackcomplete = True

    def test_timer(self):
        self.callbackcomplete = False
        self.timer = Timer()
        self.timer.callAfter(0.1, self.timercallback)
        ma = MathApp()
        ma.run()

        for i in range(10):
            time.sleep(1 / 60)
            ma._animate(1)

        self.assertEquals(self.callbackcomplete, True)

        self.timer.destroy()
Beispiel #6
0
class Rocket(ImagePoint):
    """
    Rocket is a class for simulating the motion of a projectile through space,
    acted upon by arbitrary forces (thrust) and by gravitaitonal
    attraction to a single planetary object.

    Required parameters:

    :param Planet planet:  Reference to a :class:`Planet` object.

    Optional keyword parameters are supported:

    :param str bitmap:  Url of a suitable bitmap image for the rocket (png
        recommended), default is `ggame/images/rocket.png`
    :param float bitmapscale:  Scale factor for bitmap. Default is 0.1
    :param float velocity:  Initial rocket speed. Default is zero.
    :param float directiond:  Initial rocket direction in degrees. Default is zero.
    :param float direction:  Initial rocket direction in radians. Default is zero.
    :param float tanomalyd:  Initial rocket true anomaly in degrees. Default is 90.
    :param float tanomaly:  Initial rocket true anomaly in radians. Default is pi/2.
    :param float altitude:  Initial rocket altitude in meters. Default is zero.
    :param bool showstatus:  Boolean displays flight parameters on screen. Default
        is True.
    :param (int, int) statuspos:  Tuple with screen x,y coordinates of flight
        parameters. Default is upper left.
    :param list[str] statuslist: List of status names to include in flight parameters.
      Default is all, consisting of: "velocity", "acceleration", "course",
      "altitude", "thrust", "mass", "trueanomaly", "scale", "timezoom",
      "shiptime"
    :param str leftkey: A :class:`ggame.event.KeyEvent` key identifier that will serve
        as the "rotate left" key while controlling the ship. Default is 'left arrow'.
    :param str rightkey: A :class:`ggame.event.KeyEvent` key identifier that will serve
        as the "rotate right" key while controlling the ship. Default is 'right arrow'.

    Following parameters may be set as a constant value, or pass in the
    name of a function that will return the value dynamically or the
    name of a `ggmath` UI control that will return the value.

    :param function or float timezoom:  Scale factor for time zoom. Factor =
        10^timezoom
    :param function or float heading:  Direction to point the rocket in (must be
        radians)
    :param fucntion or float mass: Mass of the rocket (must be kg)
    :param function or float thrust: Thrust of the rocket (must be N)

    Animation related parameters may be ignored if no sprite animation:

    :param Frame bitmapframe:  ((x1,y1),(x2,y2)) tuple defines a region in the bitmap
    :param int bitmapqty: Number of bitmaps -- used for animation effects
    :param str bitmapdir: "horizontal" or "vertical" use with animation effects. Default
        is "horizontal"
    :param int bitmapmargin: Pixels between successive animation frames
    :param float tickrate: Frequency of spacecraft dynamics calculations (Hz)

    Example:

    .. literalinclude:: ../examples/astrorocket.py

    """

    def __init__(self, planet, **kwargs):
        self._xy = (1000000, 1000000)
        self.planet = planet
        """Reference to an app object of :class:`Planet` class"""
        self._bmurl = kwargs.get("bitmap", self.getImagePath("rocket.png"))
        """URL of a bitmap image to use for the rocket"""
        self._bitmapframe = kwargs.get("bitmapframe", None)
        """:class:`ggame.asset.Frame` that specifies location of displayed
            image in bitmap image
        """
        self._bitmapqty = kwargs.get("bitmapqty", 1)
        """Number of sub-images in the bitmap image"""
        self._bitmapdir = kwargs.get("bitmapdir", "horizontal")
        """Orientation of the sub-images for animation (one of "horizontal" or
            "vertical")
        """
        self._bitmapmargin = kwargs.get("bitmapmargin", 0)
        """Margin between the sub-images for animation."""
        self.tickrate = kwargs.get("tickrate", 30)
        """Target animation frames per second."""
        # status display
        statusfuncs = [
            self._velocityText,
            self._accelerationText,
            self._courseDegreesText,
            self._altitudeText,
            self._thrustText,
            self._massText,
            self._trueAnomalyDegreesText,
            self._scaleText,
            self._timeZoomText,
            self._shipTimeText,
        ]
        statuslist = [
            "velocity",
            "acceleration",
            "course",
            "altitude",
            "thrust",
            "mass",
            "trueanomaly",
            "scale",
            "timezoom",
            "shiptime",
        ]

        self._showstatus = kwargs.get("showstatus", True)
        """Boolean: True to show stats on screen, else False."""
        self._statuspos = kwargs.get("statuspos", [10, 10])
        """Location on screen for showing the stats."""
        self._statusselect = kwargs.get("statuslist", statuslist)
        """Reference to a list of statuses to display."""
        self.localheading = 0
        """Heading (angle) of travel"""
        # dynamic parameters
        self.timezoom = self.eval(kwargs.get("timezoom", 0))
        """Reference to a function that will dynamically determine the
        time zoom factor. An integer: 1,2,3 faster, -1, slower.
        """
        self.heading = self.eval(kwargs.get("heading", self._getheading))
        """Reference to a function that will dynamically calculate spaceship
        orientation or heading (in radians)
        """
        self.mass = self.eval(kwargs.get("mass", 1))
        """Reference to a function that will dynamically calculate the
        spaceship mass (in kg).
        """
        self.thrust = self.eval(kwargs.get("thrust", 0))
        """Reference to a funtion that will dynamically calculate the rocket
        thrust (in N).
        """
        # end dynamic
        super().__init__(
            self._bmurl,
            self._getposition,
            frame=self._bitmapframe,
            qty=self._bitmapqty,
            direction=self._bitmapdir,
            margin=self._bitmapmargin,
        )
        self.scale = kwargs.get("bitmapscale", 0.1)  # small
        initvel = kwargs.get("velocity", 0)  # initial velocity
        initdird = kwargs.get("directiond", 0)  # initial direction, degrees
        initdir = kwargs.get("direction", radians(initdird))
        tanomaly = kwargs.get("tanomaly", pi / 2)  # position angle
        tanomaly = radians(kwargs.get("tanomalyd", degrees(tanomaly)))
        altitude = kwargs.get("altitude", 0)  #
        r = altitude + self.planet.radius
        self._xy = (r * cos(tanomaly), r * sin(tanomaly))
        # default heading control if none provided by user
        leftkey = kwargs.get("leftkey", "left arrow")
        rightkey = kwargs.get("rightkey", "right arrow")
        if self.heading == self._getheading:  # pylint: disable=comparison-with-callable
            Planet.listenKeyEvent("keydown", leftkey, self._turn)
            Planet.listenKeyEvent("keydown", rightkey, self._turn)
        self._timer = Timer()
        self.shiptime = 0  # track time on shipboard
        self._timer.callEvery(1 / self.tickrate, self._dynamics)
        self._lasttime = self._timer.time
        self._v_vect = [initvel * cos(initdir), initvel * sin(initdir)]
        self._a_vect = [0, 0]
        # keep track of on-screen resources
        self._labels = []
        # set up status display
        if self._showstatus:
            self.addStatusReport(statuslist, statusfuncs, self._statusselect)

    @staticmethod
    def _vadd(v1, v2):
        """
        Vector add utility.
        """
        return [v1[i] + v2[i] for i in (0, 1)]

    @staticmethod
    def _vmul(s, v):
        """
        Scalar vector multiplication utility.
        """
        return [s * v[i] for i in (0, 1)]

    @staticmethod
    def _vmag(v):
        """
        Vector magnitude function.
        """
        return sqrt(v[0] ** 2 + v[1] ** 2)

    def _turn(self, event):
        """

        Respond to left/right turning key events.
        """
        increment = pi / 50 * (1 if event.key == "left arrow" else -1)
        self.localheading += increment

    def _getposition(self):
        return self._xy

    # override or define externally!
    def _getheading(self):
        """
        User override function for dynamically determining the heading.

        :returns: float
        """
        return self.localheading

    # functions available for reporting flight parameters to UI
    def _velocityText(self):
        """
        Report the velocity in m/s as a text string.
        """
        return "Velocity:     {0:8.1f} m/s".format(self.velocity)

    def _accelerationText(self):
        """
        Report the acceleration in m/s as a text string.
        """
        return "Acceleration: {0:8.1f} m/s²".format(self.acceleration)

    def _courseDegreesText(self):
        """
        Report the heading in degrees (zero to the right) as a text string.
        """
        return "Course:       {0:8.1f}°".format(
            degrees(atan2(self._v_vect[1], self._v_vect[0]))
        )

    def _thrustText(self):
        """
        Report the thrust level in Newtons as a text string.
        """
        return "Thrust:       {0:8.1f} N".format(self.thrust())

    def _massText(self):
        """
        Report the spacecraft mass in kilograms as a text string.
        """
        return "Mass:         {0:8.1f} kg".format(self.mass())

    def _trueAnomalyDegreesText(self):
        """
        Report the true anomaly in degrees as a text string.
        """
        return "True Anomaly: {0:8.1f}°".format(self.tanomalyd)

    def _trueAnomalyRadiansText(self):
        """
        Report the true anomaly in radians as a text string.
        """
        return "True Anomaly: {0:8.4f}".format(self.tanomaly)

    def _altitudeText(self):
        """
        Report the altitude in meters as a text string.
        """
        return "Altitude:     {0:8.1f} m".format(self.altitude)

    def _radiusText(self):
        """
        Report the radius (distance to planet center) in meters as a text string.
        """
        return "Radius:       {0:8.1f} m".format(self.r)

    def _scaleText(self):
        """
        Report the view scale (pixels/meter) as a text string.
        """
        return "View Scale:   {0:8.6f} px/m".format(self.planet.scale)

    def _timeZoomText(self):
        """
        Report the time acceleration as a text string.
        """
        return "Time Zoom:    {0:8.1f}".format(float(self.timezoom()))

    def _shipTimeText(self):
        """
        Report the elapsed time as a text string.
        """
        return "Elapsed Time: {0:8.1f} s".format(float(self.shiptime))

    def _dynamics(self, timer):
        """
        Perform one iteration of the simulation using runge-kutta 4th order method.
        """
        # set time duration equal to time since last execution
        tick = 10 ** self.timezoom() * (timer.time - self._lasttime)
        self.shiptime = self.shiptime + tick
        self._lasttime = timer.time
        # 4th order runge-kutta method
        # (https://sites.temple.edu/math5061/files/2016/12/final_project.pdf)
        # and http://spiff.rit.edu/richmond/nbody/OrbitRungeKutta4.pdf  (succinct,
        # but with a typo)
        self._a_vect = k1v = self._ar(self._xy)
        k1r = self._v_vect
        k2v = self._ar(self._vadd(self._xy, self._vmul(tick / 2, k1r)))
        k2r = self._vadd(self._v_vect, self._vmul(tick / 2, k1v))
        k3v = self._ar(self._vadd(self._xy, self._vmul(tick / 2, k2r)))
        k3r = self._vadd(self._v_vect, self._vmul(tick / 2, k2v))
        k4v = self._ar(self._vadd(self._xy, self._vmul(tick, k3r)))
        k4r = self._vadd(self._v_vect, self._vmul(tick, k3v))
        self._v_vect = [
            self._v_vect[i] + tick / 6 * (k1v[i] + 2 * k2v[i] + 2 * k3v[i] + k4v[i])
            for i in (0, 1)
        ]
        self._xy = [
            self._xy[i] + tick / 6 * (k1r[i] + 2 * k2r[i] + 2 * k3r[i] + k4r[i])
            for i in (0, 1)
        ]
        if self.altitude < 0:
            self._v_vect = [0, 0]
            self._a_vect = [0, 0]
            self.altitude = 0

    # generic force as a function of position
    def _fr(self, pos):
        """
        Compute the net force vector on the rocket, as a function of the
        position vector.
        """
        self.rotation = self.heading()
        t = self.thrust()
        g = 6.674e-11
        r = Planet.distance((0, 0), pos)
        uvec = (-pos[0] / r, -pos[1] / r)
        fg = g * self.mass() * self.planet.mass / r ** 2
        vf = [x * fg for x in uvec]
        return [vf[0] + t * cos(self.rotation), vf[1] + t * sin(self.rotation)]

    # geric acceleration as a function of position
    def _ar(self, pos):
        """
        Compute the acceleration vector of the rocket, as a function of the
        position vector.
        """
        m = self.mass()
        vf = self._fr(pos)
        return [vf[i] / m for i in (0, 1)]

    # add a status reporting function to status display
    def addStatusReport(self, statuslist, statusfuncs, statusselect):
        """
        Accept list of all status names, all status text functions, and
        the list of status names that have been selected for display.

        :param list[str] statuslist: List of status names to include in flight
            parameters. Default is all, consisting of: "velocity", "acceleration",
            "course", "altitude", "thrust", "mass", "trueanomaly", "scale", "timezoom",
            and "shiptime".
        :param list[func] statusfuncs: List of function references corresponding
            to statuslist parameter.
        :param list[str] statusselect: List of names chosen from statuslist to display.

        :returns: None
        """
        statusdict = {n: f for n, f in zip(statuslist, statusfuncs)}
        for name in statusselect:
            if name in statusdict:
                self._labels.append(
                    Label(
                        self._statuspos[:],
                        statusdict[name],
                        size=15,
                        positioning="physical",
                        width=250,
                    )
                )
                self._statuspos[1] += 25

    @property
    def xyposition(self):
        """
        Report the x,y tuple for logical position of the spaceship.
        """
        return self._xy

    @xyposition.setter
    def xyposition(self, pos):
        self._xy = pos
        # self._touchAsset()

    @property
    def tanomalyd(self):
        """
        Report/set the spaceship position as a direction relative to central body
        (degrees).
        """
        return degrees(self.tanomaly)

    @tanomalyd.setter
    def tanomalyd(self, angle):
        self.tanomaly = radians(angle)

    @property
    def altitude(self):
        """
        Report/set the spaceship altitude of planet surface in meters.
        """
        alt = Planet.distance(self._xy, (0, 0)) - self.planet.radius
        return alt

    @altitude.setter
    def altitude(self, alt):
        r = alt + self.planet.radius
        self._xy = (r * cos(self.tanomaly), r * sin(self.tanomaly))

    @property
    def velocity(self):
        """
        Report the spaceship velocity scalar in m/s.
        """
        return self._vmag(self._v_vect)

    @property
    def acceleration(self):
        """
        Report the spaceship acceleration scalar in m/s.
        """
        return self._vmag(self._a_vect)

    @property
    def tanomaly(self):
        """
        Report/set the spaceship position as a direction relative to central body
        (radians).
        """
        # pos = self._pos()
        return atan2(self._xy[1], self._xy[0])

    @tanomaly.setter
    def tanomaly(self, angle):
        r = self.r
        self._xy = (r * cos(angle), r * sin(angle))
        self.touchAsset()

    @property
    def r(self):
        """
        Report the spaceship distance (radius) from central body center of mass.
        """
        return self.altitude + self.planet.radius
Beispiel #7
0
    def __init__(self, planet, **kwargs):
        self._xy = (1000000, 1000000)
        self.planet = planet
        """Reference to an app object of :class:`Planet` class"""
        self._bmurl = kwargs.get("bitmap", self.getImagePath("rocket.png"))
        """URL of a bitmap image to use for the rocket"""
        self._bitmapframe = kwargs.get("bitmapframe", None)
        """:class:`ggame.asset.Frame` that specifies location of displayed
            image in bitmap image
        """
        self._bitmapqty = kwargs.get("bitmapqty", 1)
        """Number of sub-images in the bitmap image"""
        self._bitmapdir = kwargs.get("bitmapdir", "horizontal")
        """Orientation of the sub-images for animation (one of "horizontal" or
            "vertical")
        """
        self._bitmapmargin = kwargs.get("bitmapmargin", 0)
        """Margin between the sub-images for animation."""
        self.tickrate = kwargs.get("tickrate", 30)
        """Target animation frames per second."""
        # status display
        statusfuncs = [
            self._velocityText,
            self._accelerationText,
            self._courseDegreesText,
            self._altitudeText,
            self._thrustText,
            self._massText,
            self._trueAnomalyDegreesText,
            self._scaleText,
            self._timeZoomText,
            self._shipTimeText,
        ]
        statuslist = [
            "velocity",
            "acceleration",
            "course",
            "altitude",
            "thrust",
            "mass",
            "trueanomaly",
            "scale",
            "timezoom",
            "shiptime",
        ]

        self._showstatus = kwargs.get("showstatus", True)
        """Boolean: True to show stats on screen, else False."""
        self._statuspos = kwargs.get("statuspos", [10, 10])
        """Location on screen for showing the stats."""
        self._statusselect = kwargs.get("statuslist", statuslist)
        """Reference to a list of statuses to display."""
        self.localheading = 0
        """Heading (angle) of travel"""
        # dynamic parameters
        self.timezoom = self.eval(kwargs.get("timezoom", 0))
        """Reference to a function that will dynamically determine the
        time zoom factor. An integer: 1,2,3 faster, -1, slower.
        """
        self.heading = self.eval(kwargs.get("heading", self._getheading))
        """Reference to a function that will dynamically calculate spaceship
        orientation or heading (in radians)
        """
        self.mass = self.eval(kwargs.get("mass", 1))
        """Reference to a function that will dynamically calculate the
        spaceship mass (in kg).
        """
        self.thrust = self.eval(kwargs.get("thrust", 0))
        """Reference to a funtion that will dynamically calculate the rocket
        thrust (in N).
        """
        # end dynamic
        super().__init__(
            self._bmurl,
            self._getposition,
            frame=self._bitmapframe,
            qty=self._bitmapqty,
            direction=self._bitmapdir,
            margin=self._bitmapmargin,
        )
        self.scale = kwargs.get("bitmapscale", 0.1)  # small
        initvel = kwargs.get("velocity", 0)  # initial velocity
        initdird = kwargs.get("directiond", 0)  # initial direction, degrees
        initdir = kwargs.get("direction", radians(initdird))
        tanomaly = kwargs.get("tanomaly", pi / 2)  # position angle
        tanomaly = radians(kwargs.get("tanomalyd", degrees(tanomaly)))
        altitude = kwargs.get("altitude", 0)  #
        r = altitude + self.planet.radius
        self._xy = (r * cos(tanomaly), r * sin(tanomaly))
        # default heading control if none provided by user
        leftkey = kwargs.get("leftkey", "left arrow")
        rightkey = kwargs.get("rightkey", "right arrow")
        if self.heading == self._getheading:  # pylint: disable=comparison-with-callable
            Planet.listenKeyEvent("keydown", leftkey, self._turn)
            Planet.listenKeyEvent("keydown", rightkey, self._turn)
        self._timer = Timer()
        self.shiptime = 0  # track time on shipboard
        self._timer.callEvery(1 / self.tickrate, self._dynamics)
        self._lasttime = self._timer.time
        self._v_vect = [initvel * cos(initdir), initvel * sin(initdir)]
        self._a_vect = [0, 0]
        # keep track of on-screen resources
        self._labels = []
        # set up status display
        if self._showstatus:
            self.addStatusReport(statuslist, statusfuncs, self._statusselect)
Beispiel #8
0
class TestMathMethods(unittest.TestCase):
    def buttonstatus(self):
        return "True" if self.imgbutton() else "False"

    def labelcoords(self):
        return (100 + self.vslider1(), 175)

    def buttoncoords(self):
        return (300 + self.vslider1(), 175)

    def labelcolor(self):
        colorval = self.vslider1()
        return Color(colorval * 256, 1)

    def pressbutton(self, caller):
        print("button pressed: ", caller)

    def __init__(self, arg):
        super().__init__(arg)

    def test_controls(self):
        self.imgbutton = InputImageButton(
            "images/button-round.png",
            self.pressbutton,
            (0, 0),
            frame=Frame(0, 0, 100, 100),
            qty=2,
        )
        self.imgbutton.scale = 0.5
        self.vslider1 = Slider((100, 150),
                               0,
                               250,
                               125,
                               positioning="physical",
                               steps=10)
        self.label = Label(
            self.labelcoords,
            self.buttonstatus,
            size=15,
            positioning="physical",
            color=self.labelcolor,
        )
        self.button = InputButton(
            self.pressbutton,
            self.buttoncoords,
            "Press Me",
            size=15,
            positioning="physical",
        )
        self.numinput = InputNumeric((300, 275), 3.14, positioning="physical")

        ma = MathApp()
        ma.run()

        self.vslider1.destroy()
        self.label.destroy()
        self.button.destroy()
        self.numinput.destroy()
        self.imgbutton.destroy()

    def test_geometry(self):
        self.ip = ImagePoint("bunny.png", (0, 0))
        self.ip.movable = True
        self.p1 = Point((0, 0), color=Color(0x008000, 1))
        self.p1.movable = True
        self.p2 = Point((0, -1))
        self.p3 = Point((1.2, 0))
        self.l1 = LineSegment(self.p2,
                              self.p3,
                              style=LineStyle(3, Color(0, 1)))
        self.l2 = LineSegment(self.p2,
                              self.p1,
                              style=LineStyle(3, Color(0, 1)))
        self.c2 = Circle((-1, -1), self.p1)

        ma = MathApp()
        ma.run()

        self.ip.destroy()
        self.p1.destroy()
        self.p2.destroy()
        self.p3.destroy()
        self.c2.destroy()
        self.l1.destroy()
        self.l2.destroy()

    def test_fancycontrols(self):
        self.imgbutton = InputImageButton(
            "images/button-round.png",
            self.pressbutton,
            (0, 0),
            frame=Frame(0, 0, 100, 100),
            qty=2,
        )
        self.imgbutton.scale = 0.5
        self.ii = ImageIndicator(
            "images/red-led-off-on.png",
            (300, 500),
            self.imgbutton,
            positioning="physical",
            frame=Frame(0, 0, 600, 600),
            qty=2,
        )
        self.ii.scale = 0.1
        self.glassbutton = GlassButton(None, (0, -0.5))
        self.toggle = MetalToggle(0, (0, -1))
        self.Li = LEDIndicator((300, 450),
                               self.glassbutton,
                               positioning="physical")
        self.Lit = LEDIndicator((300, 480),
                                self.toggle,
                                positioning="physical")

        ma = MathApp()
        ma.run()

        self.imgbutton.destroy()
        self.ii.destroy()
        self.glassbutton.destroy()
        self.toggle.destroy()
        self.Li.destroy()
        self.Lit.destroy()

    def timercallback(self, timer):
        self.assertEqual(timer, self.timer)
        self.callbackcomplete = True

    def test_timer(self):
        self.callbackcomplete = False
        self.timer = Timer()
        self.timer.callAfter(0.1, self.timercallback)
        ma = MathApp()
        ma.run()

        for i in range(10):
            time.sleep(1 / 60)
            ma._animate(1)

        self.assertEquals(self.callbackcomplete, True)

        self.timer.destroy()
Beispiel #9
0
    def __init__(self, planet, **kwargs):
        self._xy = (1000000, 1000000)
        self.planet = planet
        self.bmurl = kwargs.get('bitmap',
                                'ggimages/rocket.png')  # default rocket png
        self.bitmapframe = kwargs.get('bitmapframe', None)  #
        self.bitmapqty = kwargs.get('bitmapqty',
                                    1)  # Number of images in bitmap
        self.bitmapdir = kwargs.get('bitmapdir',
                                    'horizontal')  # animation orientation
        self.bitmapmargin = kwargs.get('bitmapmargin', 0)  # bitmap spacing
        self.tickrate = kwargs.get('tickrate', 30)  # dynamics calcs per sec
        # status display
        statusfuncs = [
            self.velocityText, self.accelerationText, self.courseDegreesText,
            self.altitudeText, self.thrustText, self.massText,
            self.trueAnomalyDegreesText, self.scaleText, self.timeZoomText,
            self.shipTimeText
        ]
        statuslist = [
            "velocity", "acceleration", "course", "altitude", "thrust", "mass",
            "trueanomaly", "scale", "timezoom", "shiptime"
        ]

        self.showstatus = kwargs.get('showstatus', True)  # show stats
        self.statuspos = kwargs.get('statuspos', [10, 10])  # position of stats
        self.statusselect = kwargs.get('statuslist', statuslist)
        self.localheading = 0
        # dynamic parameters
        self.timezoom = self.Eval(kwargs.get(
            'timezoom', self.gettimezoom))  # 1,2,3 faster, -1, slower
        self.heading = self.Eval(kwargs.get(
            'heading', self.getheading))  # must be radians
        self.mass = self.Eval(kwargs.get('mass', self.getmass))  # kg
        self.thrust = self.Eval(kwargs.get('thrust', self.getthrust))  # N
        # end dynamic
        super().__init__(self.bmurl,
                         self._getposition,
                         frame=self.bitmapframe,
                         qty=self.bitmapqty,
                         direction=self.bitmapdir,
                         margin=self.bitmapmargin)
        self.scale = kwargs.get('bitmapscale', 0.1)  # small
        initvel = kwargs.get('velocity', 0)  # initial velocity
        initdird = kwargs.get('directiond', 0)  # initial direction, degrees
        initdir = kwargs.get('direction', radians(initdird))
        tanomaly = kwargs.get('tanomaly', pi / 2)  # position angle
        tanomaly = radians(kwargs.get('tanomalyd', degrees(tanomaly)))
        altitude = kwargs.get('altitude', 0)  #
        r = altitude + self.planet.radius
        self._xy = (r * cos(tanomaly), r * sin(tanomaly))
        # default heading control if none provided by user
        leftkey = kwargs.get('leftkey', 'left arrow')
        rightkey = kwargs.get('rightkey', 'right arrow')
        if self.heading == self.getheading:
            Planet.listenKeyEvent('keydown', leftkey, self.turn)
            Planet.listenKeyEvent('keydown', rightkey, self.turn)
        self.timer = Timer()
        self.shiptime = 0  # track time on shipboard
        self.timer.callEvery(1 / self.tickrate, self.dynamics)
        self.lasttime = self.timer.time
        self.V = [initvel * cos(initdir), initvel * sin(initdir)]
        self.A = [0, 0]
        # keep track of on-screen resources
        self._labels = []
        # set up status display
        if self.showstatus:
            self.addStatusReport(statuslist, statusfuncs, self.statusselect)
Beispiel #10
0
class Rocket(ImagePoint):
    """
    Rocket is a class for simulating the motion of a projectile through space, 
    acted upon by arbitrary forces (thrust) and by gravitaitonal 
    attraction to a single planetary object.

    Initialize the Rocket object. 
    
    Example:
    
        rocket1 = Rocket(earth, altitude=400000, velocity=7670, timezoom=2)
   
    Required parameters:
    
    * **planet**:  Reference to a `Planet` object.
    
    Optional keyword parameters are supported:
    
    * **bitmap**:  url of a suitable bitmap image for the rocket (png recommended)
      default is `ggimages/rocket.png`
    * **bitmapscale**:  scale factor for bitmap. Default is 0.1
    * **velocity**:  initial rocket speed. default is zero.
    * **directiond**:  initial rocket direction in degrees. Default is zero.
    * **direction**:  initial rocket direction in radians. Default is zero.
    * **tanomalyd**:  initial rocket true anomaly in degrees. Default is 90.
    * **tanomaly**:  initial rocket true anomaly in radians. Default is pi/2.
    * **altitude**:  initial rocket altitude in meters. Default is zero.
    * **showstatus**:  boolean displays flight parameters on screen. Default
      is True.
    * **statuspos**:  tuple with x,y coordinates of flight parameters. 
      Default is upper left.
    * **statuslist**: list of status names to include in flight parameters. 
      Default is all, consisting of: "velocity", "acceleration", "course",
      "altitude", "thrust", "mass", "trueanomaly", "scale", "timezoom",
      "shiptime"
    * **leftkey**: a `ggame` key identifier that will serve as the 
      "rotate left" key while controlling the ship. Default is 'left arrow'.
    * **rightkey**: a `ggame` key identifier that will serve as the 
      "rotate right" key while controlling the ship. Default is 'right arrow'.
    
    Following parameters may be set as a constant value, or pass in the
    name of a function that will return the value dynamically or the
    name of a `ggmath` UI control that will return the value.
    
    * **timezoom**  scale factor for time zoom. Factor = 10^timezoom
    * **heading**  direction to point the rocket in (must be radians)
    * **mass**  mass of the rocket (must be kg)
    * **thrust**  thrust of the rocket (must be N)

    Animation related parameters may be ignored if no sprite animation:
    
    * **bitmapframe**  ((x1,y1),(x2,y2)) tuple defines a region in the bitmap
    * **bitmapqty**  number of bitmaps -- used for animation effects
    * **bitmapdir**  "horizontal" or "vertical" use with animation effects
    * **bitmapmargin**  pixels between successive animation frames
    * **tickrate**  frequency of spacecraft dynamics calculations (Hz)
    
    """
    def __init__(self, planet, **kwargs):
        self._xy = (1000000, 1000000)
        self.planet = planet
        self.bmurl = kwargs.get('bitmap',
                                'ggimages/rocket.png')  # default rocket png
        self.bitmapframe = kwargs.get('bitmapframe', None)  #
        self.bitmapqty = kwargs.get('bitmapqty',
                                    1)  # Number of images in bitmap
        self.bitmapdir = kwargs.get('bitmapdir',
                                    'horizontal')  # animation orientation
        self.bitmapmargin = kwargs.get('bitmapmargin', 0)  # bitmap spacing
        self.tickrate = kwargs.get('tickrate', 30)  # dynamics calcs per sec
        # status display
        statusfuncs = [
            self.velocityText, self.accelerationText, self.courseDegreesText,
            self.altitudeText, self.thrustText, self.massText,
            self.trueAnomalyDegreesText, self.scaleText, self.timeZoomText,
            self.shipTimeText
        ]
        statuslist = [
            "velocity", "acceleration", "course", "altitude", "thrust", "mass",
            "trueanomaly", "scale", "timezoom", "shiptime"
        ]

        self.showstatus = kwargs.get('showstatus', True)  # show stats
        self.statuspos = kwargs.get('statuspos', [10, 10])  # position of stats
        self.statusselect = kwargs.get('statuslist', statuslist)
        self.localheading = 0
        # dynamic parameters
        self.timezoom = self.Eval(kwargs.get(
            'timezoom', self.gettimezoom))  # 1,2,3 faster, -1, slower
        self.heading = self.Eval(kwargs.get(
            'heading', self.getheading))  # must be radians
        self.mass = self.Eval(kwargs.get('mass', self.getmass))  # kg
        self.thrust = self.Eval(kwargs.get('thrust', self.getthrust))  # N
        # end dynamic
        super().__init__(self.bmurl,
                         self._getposition,
                         frame=self.bitmapframe,
                         qty=self.bitmapqty,
                         direction=self.bitmapdir,
                         margin=self.bitmapmargin)
        self.scale = kwargs.get('bitmapscale', 0.1)  # small
        initvel = kwargs.get('velocity', 0)  # initial velocity
        initdird = kwargs.get('directiond', 0)  # initial direction, degrees
        initdir = kwargs.get('direction', radians(initdird))
        tanomaly = kwargs.get('tanomaly', pi / 2)  # position angle
        tanomaly = radians(kwargs.get('tanomalyd', degrees(tanomaly)))
        altitude = kwargs.get('altitude', 0)  #
        r = altitude + self.planet.radius
        self._xy = (r * cos(tanomaly), r * sin(tanomaly))
        # default heading control if none provided by user
        leftkey = kwargs.get('leftkey', 'left arrow')
        rightkey = kwargs.get('rightkey', 'right arrow')
        if self.heading == self.getheading:
            Planet.listenKeyEvent('keydown', leftkey, self.turn)
            Planet.listenKeyEvent('keydown', rightkey, self.turn)
        self.timer = Timer()
        self.shiptime = 0  # track time on shipboard
        self.timer.callEvery(1 / self.tickrate, self.dynamics)
        self.lasttime = self.timer.time
        self.V = [initvel * cos(initdir), initvel * sin(initdir)]
        self.A = [0, 0]
        # keep track of on-screen resources
        self._labels = []
        # set up status display
        if self.showstatus:
            self.addStatusReport(statuslist, statusfuncs, self.statusselect)

    # override or define externally!
    def getthrust(self):
        """
        User override function for dynamically determining thrust force.
        """
        return 0

    # override or define externally!
    def getmass(self):
        """
        User override function for dynamically determining rocket mass.
        """
        return 1

    # override or define externally!
    def getheading(self):
        """
        User override function for dynamically determining the heading.
        """
        return self.localheading

    # override or define externally!
    def gettimezoom(self):
        """
        User override function for dynamically determining the timezoom.
        """
        return 0

    # add a status reporting function to status display
    def addStatusReport(self, statuslist, statusfuncs, statusselect):
        """
        Accept list of all status names, all status text functions, and
        the list of status names that have been selected for display.
        """
        statusdict = {n: f for n, f in zip(statuslist, statusfuncs)}
        for name in statusselect:
            if name in statusdict:
                self._labels.append(
                    Label(self.statuspos[:],
                          statusdict[name],
                          size=15,
                          positioning='physical',
                          width=250))
                self.statuspos[1] += 25

    # functions available for reporting flight parameters to UI
    def velocityText(self):
        """
        Report the velocity in m/s as a text string.
        """
        return "Velocity:     {0:8.1f} m/s".format(self.velocity)

    def accelerationText(self):
        """
        Report the acceleration in m/s as a text string.
        """
        return "Acceleration: {0:8.1f} m/s²".format(self.acceleration)

    def courseDegreesText(self):
        """
        Report the heading in degrees (zero to the right) as a text string.
        """
        return "Course:       {0:8.1f}°".format(
            degrees(atan2(self.V[1], self.V[0])))

    def thrustText(self):
        """
        Report the thrust level in Newtons as a text string.
        """
        return "Thrust:       {0:8.1f} N".format(self.thrust())

    def massText(self):
        """
        Report the spacecraft mass in kilograms as a text string.
        """
        return "Mass:         {0:8.1f} kg".format(self.mass())

    def trueAnomalyDegreesText(self):
        """
        Report the true anomaly in degrees as a text string.
        """
        return "True Anomaly: {0:8.1f}°".format(self.tanomalyd)

    def trueAnomalyRadiansText(self):
        """
        Report the true anomaly in radians as a text string.
        """
        return "True Anomaly: {0:8.4f}".format(self.tanomaly)

    def altitudeText(self):
        """
        Report the altitude in meters as a text string.
        """
        return "Altitude:     {0:8.1f} m".format(self.altitude)

    def radiusText(self):
        """
        Report the radius (distance to planet center) in meters as a text string.
        """
        return "Radius:       {0:8.1f} m".format(self.r)

    def scaleText(self):
        """
        Report the view scale (pixels/meter) as a text string.
        """
        return "View Scale:   {0:8.6f} px/m".format(self.planet.scale)

    def timeZoomText(self):
        """
        Report the time acceleration as a text string.
        """
        return "Time Zoom:    {0:8.1f}".format(float(self.timezoom()))

    def shipTimeText(self):
        """
        Report the elapsed time as a text string.
        """
        return "Elapsed Time: {0:8.1f} s".format(float(self.shiptime))

    def dynamics(self, timer):
        """
        Perform one iteration of the simulation using runge-kutta 4th order method.
        """
        # set time duration equal to time since last execution
        tick = 10**self.timezoom() * (timer.time - self.lasttime)
        self.shiptime = self.shiptime + tick
        self.lasttime = timer.time
        # 4th order runge-kutta method (https://sites.temple.edu/math5061/files/2016/12/final_project.pdf)
        # and http://spiff.rit.edu/richmond/nbody/OrbitRungeKutta4.pdf  (succinct, but with a typo)
        self.A = k1v = self.ar(self._xy)
        k1r = self.V
        k2v = self.ar(self.vadd(self._xy, self.vmul(tick / 2, k1r)))
        k2r = self.vadd(self.V, self.vmul(tick / 2, k1v))
        k3v = self.ar(self.vadd(self._xy, self.vmul(tick / 2, k2r)))
        k3r = self.vadd(self.V, self.vmul(tick / 2, k2v))
        k4v = self.ar(self.vadd(self._xy, self.vmul(tick, k3r)))
        k4r = self.vadd(self.V, self.vmul(tick, k3v))
        self.V = [
            self.V[i] + tick / 6 * (k1v[i] + 2 * k2v[i] + 2 * k3v[i] + k4v[i])
            for i in (0, 1)
        ]
        self._xy = [
            self._xy[i] + tick / 6 *
            (k1r[i] + 2 * k2r[i] + 2 * k3r[i] + k4r[i]) for i in (0, 1)
        ]
        if self.altitude < 0:
            self.V = [0, 0]
            self.A = [0, 0]
            self.altitude = 0

    # generic force as a function of position
    def fr(self, pos):
        """
        Compute the net force vector on the rocket, as a function of the 
        position vector.
        """
        self.rotation = self.heading()
        t = self.thrust()
        G = 6.674E-11
        r = Planet.distance((0, 0), pos)
        uvec = (-pos[0] / r, -pos[1] / r)
        fg = G * self.mass() * self.planet.mass / r**2
        F = [x * fg for x in uvec]
        return [F[0] + t * cos(self.rotation), F[1] + t * sin(self.rotation)]

    # geric acceleration as a function of position
    def ar(self, pos):
        """
        Compute the acceleration vector of the rocket, as a function of the 
        position vector.
        """
        m = self.mass()
        F = self.fr(pos)
        return [F[i] / m for i in (0, 1)]

    def vadd(self, v1, v2):
        """
        Vector add utility.
        """
        return [v1[i] + v2[i] for i in (0, 1)]

    def vmul(self, s, v):
        """
        Scalar vector multiplication utility.
        """
        return [s * v[i] for i in (0, 1)]

    def vmag(self, v):
        """
        Vector magnitude function.
        """
        return sqrt(v[0]**2 + v[1]**2)

    def fgrav(self):
        """
        Vector force due to gravity, at current position.
        """
        G = 6.674E-11
        r = self.r
        uvec = (-self._xy[0] / r, -self._xy[1] / r)
        F = G * self.mass() * self.planet.mass / r**2
        return [x * F for x in uvec]

    def turn(self, event):
        """
        Respond to left/right turning key events.
        """
        increment = pi / 50 * (1 if event.key == "left arrow" else -1)
        self.localheading += increment

    def _getposition(self):
        return self._xy

    @property
    def xyposition(self):
        return self._xy

    @xyposition.setter
    def xyposition(self, pos):
        self._xy = pos
        #self._touchAsset()

    @property
    def tanomalyd(self):
        return degrees(self.tanomaly)

    @tanomalyd.setter
    def tanomalyd(self, angle):
        self.tanomaly = radians(angle)

    @property
    def altitude(self):
        alt = Planet.distance(self._xy, (0, 0)) - self.planet.radius
        return alt

    @altitude.setter
    def altitude(self, alt):
        r = alt + self.planet.radius
        self._xy = (r * cos(self.tanomaly), r * sin(self.tanomaly))

    @property
    def velocity(self):
        return self.vmag(self.V)

    @property
    def acceleration(self):
        return self.vmag(self.A)

    @property
    def tanomaly(self):
        #pos = self._pos()
        return atan2(self._xy[1], self._xy[0])

    @tanomaly.setter
    def tanomaly(self, angle):
        r = self.r
        self._xy = (r * cos(angle), r * sin(angle))
        self._touchAsset()

    @property
    def r(self):
        return self.altitude + self.planet.radius
Beispiel #11
0
"""
Example of using the Timer class.
"""

from ggame.timer import Timer
from ggame.mathapp import MathApp


def timercallback(t):
    """
    Callback function to receive notification of timer expiration.
    """
    print("time's up at", t.time, "seconds!")


TIMER = Timer()
# Execute timercallback after 5 seconds
TIMER.callAfter(5, timercallback)

MathApp().run()