.. _scripting:

Scripting
*********

As of version ``2.2.0``, Gaia Sky uses `Py4J <https://www.py4j.org>`__ instead of `Jython <https://www.jython.org/>`__ as a mechanism to run Python scripts.

You can find lots of example scripts in the
`scripts <https://gitlab.com/langurmonkey/gaiasky/tree/master/assets/scripts>`__
folder of the project.


Quick start
===========

Gaia Sky scripts must be run with the system Python interpreter. They connect to a gateway service offered by a running instance of Gaia Sky. As of version ``2.1.8``, Gaia Sky is **not** responsible anymore to run the scripts.

Requirements
------------

In order to connect to the gateway server, you need a Python interpreter and the Py4J package. Find out how to install it `here <https://www.py4j.org/install.html>`__, or simply do, if you use ``pip``:

.. code:: console
    
    $  pip install py4j

Note that you may need to call this with elevated privileges.

You may also use your distribution or operating system package manager to install Py4J. Please, refer to your distribution or operating system documentation for more information.
Find more information on the library at the `Py4J homepage <https://www.py4j.org/>`__.



Running a test script
---------------------

Then, launch Gaia Sky, download `this script <https://gitlab.com/langurmonkey/gaiasky/raw/master/assets/scripts/showcases/asteroids-tour.py?inline=false>`__, open a terminal window (PowerShell in Windows) and run:

.. code:: console

    $  python asteroids-tour.py


The location from which you run the script does not matter. If all goes well, you should see no crashes anywhere, and Gaia Sky should be showing a nice tour of the asteroids in the DR2 catalog.


.. figure:: img/script/yt-asteroids.jpg
	:width: 400px
	:align: center
	:target: https://www.youtube.com/watch?v=LKGxyfg5iBY

	This script should produce results similar to this video


Have a look at the script. All lines which start with `gs.` are API calls which call methods in the Gaia Sky gateway server. What are API calls, you ask? See next section.


The scripting API
=================

The scripting API is a set of methods which may be called from Python scripts to interact with Gaia Sky. The available methods differ depending on the version of Gaia Sky.

API documentation
-----------------

The only up-to-date API documentation for each version is in the interface header files themselves. Below is a list of links to the different APIs.

- `API Gaia Sky master (development branch) <https://gitlab.com/langurmonkey/gaiasky/blob/master/core/src/gaiasky/script/IScriptingInterface.html>`__
- `API Gaia Sky 3.0.2 <https://gaia.ari.uni-heidelberg.de/gaiasky/docs/javadoc/3.0.2/gaiasky/script/IScriptingInterface.html>`__
- `API Gaia Sky 3.0.1 <https://gaia.ari.uni-heidelberg.de/gaiasky/docs/javadoc/3.0.1/gaiasky/script/IScriptingInterface.html>`__
- `API Gaia Sky 3.0.0 <https://gaia.ari.uni-heidelberg.de/gaiasky/docs/javadoc/3.0.0/gaiasky/script/IScriptingInterface.html>`__
- `API Gaia Sky 2.3.0 <https://gaia.ari.uni-heidelberg.de/gaiasky/docs/javadoc/2.3.0/gaiasky/script/IScriptingInterface.html>`__
- `API Gaia Sky 2.2.6 <https://gaia.ari.uni-heidelberg.de/gaiasky/docs/javadoc/2.2.6/gaiasky/script/IScriptingInterface.html>`__
- `2.2.2 <https://gitlab.com/langurmonkey/gaiasky/blob/2.2.2/core/src/gaiasky/script/IScriptingInterface.java>`__, `2.2.1 <https://gitlab.com/langurmonkey/gaiasky/blob/2.2.1/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `2.2.0 <https://gitlab.com/langurmonkey/gaiasky/blob/2.2.0/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `2.1.7 <https://gitlab.com/langurmonkey/gaiasky/blob/2.1.7/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `2.1.6 <https://gitlab.com/langurmonkey/gaiasky/blob/2.1.6/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `2.1.5 <https://gitlab.com/langurmonkey/gaiasky/blob/2.1.5/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `2.1.4 <https://gitlab.com/langurmonkey/gaiasky/blob/2.1.4/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `2.1.3 <https://gitlab.com/langurmonkey/gaiasky/blob/2.1.3/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `2.1.2 <https://gitlab.com/langurmonkey/gaiasky/blob/2.1.2/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `2.1.1 <https://gitlab.com/langurmonkey/gaiasky/blob/2.1.1/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `2.1.0 <https://gitlab.com/langurmonkey/gaiasky/blob/2.1.0/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `2.0.3 <https://gitlab.com/langurmonkey/gaiasky/blob/2.0.3/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `2.0.2 <https://gitlab.com/langurmonkey/gaiasky/blob/2.0.2/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `2.0.1 <https://gitlab.com/langurmonkey/gaiasky/blob/2.0.1/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `2.0.0 <https://gitlab.com/langurmonkey/gaiasky/blob/2.0.0/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `1.5.0 <https://gitlab.com/langurmonkey/gaiasky/blob/1.5.0/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `1.0.4 <https://gitlab.com/langurmonkey/gaiasky/blob/1.0.4/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `1.0.3 <https://gitlab.com/langurmonkey/gaiasky/blob/1.0.3/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `1.0.2 <https://gitlab.com/langurmonkey/gaiasky/blob/1.0.2/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `1.0.1 <https://gitlab.com/langurmonkey/gaiasky/blob/1.0.1/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__, `1.0.0 <https://gitlab.com/langurmonkey/gaiasky/blob/1.0.0/core/src/gaia/cu9/ari/gaiaorbit/script/IScriptingInterface.java>`__ 


Writing scripts for Gaia Sky
============================

Gaia Sky uses the `single-threaded model <https://www.py4j.org/py4j_client_server.html>`__ of Py4J.
In order to connect to Gaia Sky from Python, import ``ClientServer`` and ``JavaParameters``, and then
create a gateway and get its entry point. The entry point is the object you can use to call 
API methods on. Since Gaia Sky uses a server per script, the gateway must be shut down at the end of the script
so that the Python program can terminate correctly and Gaia Sky can create a new server to deal with further
scripts listening to the Py4J port.

.. code:: python
    
    from py4j.clientserver import ClientServer, JavaParameters

    gateway = ClientServer(java_parameters=JavaParameters(auto_convert=True))
    gs = gateway.entry_point

    [...]

    gateway.shutdown()


The ``JavaParameters(auto_convert=True)`` is not strictly necessary, but if you don't use it you need
to convert Python lists to Java arrays yourself before calling the API.

Now, we can start calling API methods on ``gs``.

.. code:: python

    # Disable input
    gs.disableInput()
    gs.cameraStop()
    gs.minimizeInterfaceWindow()

    # Welcome
    gs.setHeadlineMessage("Welcome to the Gaia Sky")
    gs.setSubheadMessage("Explore Gaia, the Solar System and the whole Galaxy!")
    [...]


Find lots of example scripts `here <https://gitlab.com/langurmonkey/gaiasky/tree/master/assets/scripts>`__.

Logging to Gaia Sky and Python
------------------------------

When printing messages, you can either log to Gaia Sky or print to the standard 
output of the terminal where Python runs:

.. code:: python
    
    gs.print("This goes to the Gaia Sky log")
    print("This goes to the Python output")


In order to log messages to both outputs, you can define a function which
takes a string and prints it out to both sides:

.. code:: python

    def pprint(text):
        gs.print(text)
        print(text)

    pprint("Hey, this is printed in both Gaia Sky AND Python!")



Method and attribute access
---------------------------

Py4J allows accessing public class methods but not public attrbiutes. In case
you get objects from Gaia Sky, you can't directly call public attributes, but need
to access them via public methods:

.. code:: python

    # Get the Mars model object
    body = gs.getObject("Mars")
    # Get spherical coordinates
    radec = body.getPosSph()

    # DO NOT do this, it crashes!
    gs.print("RA/DEC: %f / %f" % (radec.x, radec.y))

    # DO THIS instead
    gs.print("RA/DEC: %f / %f" % (radec.x(), radec.y()))



Strict parameter types
----------------------

Please, be strict with the parameter types. Use floats when the method signature has floats
and integers when it has integers. The scripting interface still tries to perform conversions under
the hood but it is better to do it right from the beginning. For example, for the API method:

.. code:: java

    double[] galacticToInternalCartesian(double l, double b, double r);


may not work if called like this from Python:

.. code:: python

    gs.galacticToInternalCartesian(10, 43.5, 2)


Note that the first and third parameters are integers rather than floating-point numbers. Call it
like this instead:

.. code:: python

    gs.galacticToInternalCartesian(10.0, 43.5, 2.0)


.. _scripting-datasets:

Loading datasets from scripts
-----------------------------

Gaia Sky supports data loading from scripts using the :ref:`STIL data provider <stil-data-provider>`. 
It is really easy to load a ``VOTable`` file from a script:

.. code:: python

    
    from py4j.clientserver import ClientServer, JavaParameters
    gateway = ClientServer(java_parameters=JavaParameters(auto_convert=True))
    gs = gateway.entry_point

    # Load dataset
    gs.loadDataset("dataset-name", "/path/to/dataset.vot")
    # Async insertion, let's make sure the data is available
    gs.sleep(2)

    # Now we can play around with it
    gs.hideDataset("dataset-name")

    # Show it again
    gs.showDataset("dataset-name")

    # Shutdown
    gateway.shutdown()


Find an example of data loading in a script `here <https://gitlab.com/langurmonkey/gaiasky/tree/master/assets/scripts/tests/load-catalog-test.py>`__.
    

Synchronizing with the main loop
--------------------------------

Sometimes, when updating animations or creating camera paths, it is necessary to 
sync the execution of scripts with the thread which runs the main loop (main thread). 
However, the scripting engine runs scripts in separate threads asynchronously, 
making it a non-obvious task to achieve this synchronization.
In order to fix this, a new mechanism has been added in Gaia Sky ``2.0.3``. Now, runnables
can be parked so that they run at the end of the update-render processing of each loop
cycle. A runnable is a class which extends ``java.lang.Runnable``, and implements 
a very simple ``public void run()`` method.

Runnables can be **posted**, meaning that they are run only once at the end fo the current
cycle, or **parked**, meaning that they run until they stop or they are unparked. Parked
runnables must provide a name identifier in order to be later accessed and unparked.

Let's see an example of how to implement a frame counter in Python using ``py4j``:

.. code:: python

    from py4j.clientserver import ClientServer, JavaParameters, PythonParameters
    
    class FrameCounterRunnable(object):
        def __init__(self):
            self.n = 0
            
        def run(self):
            self.n = self.n + 1
            if self.n % 30 == 0:
                gs.print("Number of frames: %d" % self.n)

        class Java:
            implements = ["java.lang.Runnable"]


    gateway = ClientServer(java_parameters=JavaParameters(auto_convert=True),
                          python_parameters=PythonParameters())
    gs = gateway.entry_point

    # We park a runnable which counts the frames and prints the current number 
    # of frames every 30 of them
    gs.parkRunnable("frame_counter", FrameCounterRunnable())

    gs.sleep(15.0)

    # We unpark the frame counter
    gs.unparkRunnable("frame_counter")

    gateway.shutdown()


In this example, we park a runnable which counts frames for 15 seconds. Note that here we need
to pass a ``PythonParameters`` instance to the ``ClientServer`` constructor.

A more useful example can be found `here <https://gitlab.com/langurmonkey/gaiasky/blob/master/assets/scripts/showcases/line-objects-update.py>`__. In this one, a polyline is created between the Earth and the Moon. Then, a
parked runnable is used to update the line points with the new postions of the bodies. Finally,
time is started so that the bodies start moving and the line positions are updated correctly and in
synch with the main thread.

More examples
-------------

As we said, you can find more examples in the ``scripts``
`folder <https://gitlab.com/langurmonkey/gaiasky/tree/master/assets/scripts>`__ in the
repository.

Running and debugging scripts
=============================

In order to run scripts, you need a Python interpreter with the
``python-py4j`` module installed in your system. 

Load up Gaia Sky, open a new terminal window and run your script:

.. code:: console
    
    $  python script.py



Please, note that Gaia Sky needs to be running before the script is started 
for the connection to succeed.

To debug a script in the terminal using ``pudb`` run this:

.. code:: console

    $  python -m pudb script.py