Difference between revisions of "Terragen RPC: Time of Day Example Script"

From Terragen Documentation from Planetside Software
Jump to: navigation, search
(Replaced description and example image for deprecated function calls in getSunlightInProject() function. Updated link to new content file.)
m (Fixed typo.)
Line 28: Line 28:
 
This project uses the Python script “sunpos.py” created by John Clark Craig to calculate the sun’s heading and elevation values based on the time of day and a given location.  You’ll need to download the script and save it in your project folder.
 
This project uses the Python script “sunpos.py” created by John Clark Craig to calculate the sun’s heading and elevation values based on the time of day and a given location.  You’ll need to download the script and save it in your project folder.
  
Download the “[https://levelup.gitconnected.com/python-sun-position-for-solar-energy-and-research-7a4ead801777 sunpos.py]” script and save it in your project folder as sunpos.py. <br /n>
+
* Download the “[https://levelup.gitconnected.com/python-sun-position-for-solar-energy-and-research-7a4ead801777 sunpos.py]” script and save it in your project folder as sunpos.py. <br /n>
https://levelup.gitconnected.com/python-sun-position-for-solar-energy-and-research-7a4ead801777
+
* https://levelup.gitconnected.com/python-sun-position-for-solar-energy-and-research-7a4ead801777
  
  

Revision as of 20:48, 17 November 2022

Terragen RPC: Time of Day Example Script

Overview[edit]

Terragen 4.6.30 Professional introduces a new feature for remote procedure calling, or RPC. With a running instance of Terragen acting as a server, other programs can make remote procedure calls to query and modify a Terragen project.

The goal of this tutorial is to walk you through a step-by-step procedure using the Python programming language to create a script that changes the time of day in a Terragen project; in other words, provide the ability to modify a Sunlight node’s heading and elevation parameters outside of the Terragen program.


Part 01: Set up[edit]

For this tutorial you’ll first need to download and install Terragen 4.6.30 Professional as well as the Python programming language source code and installers. Once you’ve installed the new build of Terragen be sure to launch it.

  • LINK to Terragen 4.6.x
  • Python source code

On a Windows computer you can verify that Python is installed by querying the operating system via the Command Prompt window.

Type “command prompt” or “cmd” in the Windows search box and press “enter” to open the Command Prompt window. Type “python” on the command line of the Command Prompt window, and press “enter”. If installed, the version of Python will be displayed.

Command prompt windows displaying the installed version of Python.


Next, create a folder for this project that is accessible to your computer network. We’ll be saving and executing the python script from this location, as well as other files and resources.

This project uses the Python script “sunpos.py” created by John Clark Craig to calculate the sun’s heading and elevation values based on the time of day and a given location. You’ll need to download the script and save it in your project folder.


Part 02: Coding the basic functionality[edit]

In this section our goal is to simply pass the minimum requirements needed by the sunpos.py in order for it to calculate the sun’s heading and elevation; then pass along these results to the default Sun node in Terragen.

We’ll use Python’s Integrated Development and Learning Environment, or IDLE, to write the code for our script.

Type “IDLE” in the Windows search box and press “enter” to open the IDLE Shell window.

IDLE Shell.


While we can type and execute python commands directly in the IDLE shell, we’ll use the built-in file editor to create our script file so we can run it multiple times as we refine it.

Select “New File” from the IDLE shell menu to open the file editor window.

Select New File to open the file editor.


Right now our script is completely blank, and in order for it to do something we first need to instruct Python to load certain modules or libraries when the script is run. Not surprisingly, we need to tell Python to load the Terragen RPC library and the sunpos.py script we previously downloaded. Additionally, you might need to tell Python where the Terragen RPC module was installed, as well as have the ability to query the operating system in order to access the date and time.

Editor window.


We’ll use the “import” keyword to insert a module into our script at run time. For some modules we’ll also use the “as” keyword to provide the module with an alias that we reference within the script.

Type the following lines of code into the file editor window. You can also copy and paste them. Note that your Terragen RPC installation path may vary.


    import sys
    sys.path.insert(0,'P:/PlanetsideSoftware/RPC/terragen_rpc-0.9.0/terragen_rpc-0.9.0')
    import terragen_rpc as tg
    import sunpos as sp


In order for the sunpos module to determine the sun’s heading and elevation, it needs to know a date, time, and a location. Initially we can supply this information via Python’s datetime module, so we’ll load that as well, this time using the “from” keyword instead of the “import” keyword in order to load just the portion of the module we want.


    from datetime import datetime


Our script should look like this:

Import the modules into the script.


With the library modules imported into the script, our next step is to define the values which the sunpos module requires to make its calculations. Variables are used to store information that can be referenced or manipulated within a computer program, such as text and numbers. In Python we can define the variable and its value in the same statement.

Let’s create four variables to hold this information, one for the computer system’s date and time, one for the location on Earth utilizing latitude and longitude coordinates, one for a timezone, and one last variable to format the date, time and timezone information the way that the sunpos module expects it to be.

For this example, we’ll use the timezone, latitude, and longitude for Los Angeles, CA, USA. Notice that the location variable and the lookupTime variable consist of multiple components. This is referred to as a list in Python, and is somewhat similar to an array in other programming languages.


    sysTime = datetime.now()
    timezone = -7
    location = [34.052235, -104.741667]
    lookupTime = [sysTime.year, sysTime.month, sysTime.day, sysTime.hour, sysTime.minute, sysTime.second, timezone]


Our script now looks like this:

Define the variables.


Now that the variables have been defined that will be passed to the sunpos module, we can call the module and save its results. All of this can be done with one Python statement. We’ll define a new variable on the left of the equals sign called “results” to store the sun’s heading and elevation information which will be returned by the call to the sunpos module.

The statement to the right of the equals sign is the call to the sunpos module. The “sp” is the alias we defined at the start of the script to reference the sunpos module, and the “sunpos()” is the function within the module that gets run and calculates the sun’s heading and elevation. All the data needed to perform the calculations are passed to the module by listing the variables between the two parenthesis


    results = sp.sunpos(lookupTime, location, True)


Define a variable for the results from the sunpos module.


The last step is to apply the results to our Terragen project via RPC. We’ll do that by telling Terragen which item we want to act upon, the default Sun node, then setting the new values for its heading and elevation parameters.

The code below creates an instance of the item “Sunlight 01” called “node” which can be manipulated by our script. The “set_param” function then changes the heading and elevation to the values in the results[] list.


    node = tg.node_by_path("Sunlight 01") node.set_param('heading',results[0]) node.set_param('elevation',results[1])


Our script now looks like this:

Call the Terragen RPC module


Save the script in the project folder by selecting “File Save As” from the IDLE main menu and giving the script a name. Then press “F5” or select “Run > Run Module” from the IDLE main menu to run the script. The 3D Preview window in Terragen will update to show the new direction of the sun.

The Sunlight 01 node's heading and elevation changed by the script.


Part 03: Coding a basic GUI[edit]


A very simple GUI in order to manipulate the parameter values easier.


In the previous section we showed how a simple script can be created to manipulate the sun’s heading and position. In this section we’ll make the simple graphical interface, or GUI, seen below in order to manipulate the values of each variable much easier.

To build the GUI for our script we’ll take advantage of the “tk interface” package, or “tkinter”, included in Python. Note that in some cases, tkinter’s syntax may appear slightly different than the coding in the section above, for example in defining a variable.

Just as in the previous section, the first step is to import the tkinter package into our script. When creating a script that includes a GUI these are typically the first lines of code in the script. So we’ll prepend our existing script with these two lines of code.

The first line of code imports all the functions and built-in modules in the tkinter package into the script, while the second line of code imports the ttk package which is responsible for the style of the widgets (buttons, etc.)


    From tkinter import *
    From tkinter import ttk


Import the tkinter package into the script.


Next we’ll build the window for the GUI, starting with its size and title. We’ll insert these lines of code after the import statements and before the variables. It’s also useful to add notes or comments in a script as well. The “#” symbol instructs Python to ignore anything in the line of code that follows it. We’ll make use of this symbol to set reminders and stay organized in our script.

In Python scripts the basic window is often defined as “root”, but for this example we’ll use the word “gui”.


In the “title” attribute we can define text to display in the windows title bar. The “geometry” attribute sets the width and height of the window in pixels.


    gui = Tk()
    gui.title("RPC - Time of day")
    gui.geometry("300x250")


Whenever you make a GUI, you want to include a looping function for that window at the end of the script, which in effect causes the script to wait for user input. This will remain the last line of code in our script. Add the following line of code at the bottom of the script.


    gui.mainloop()


Here's the script so far:

Create the GUI window.


In order to individually adjust each component that makes up the date and time information, we need to define a variable for each part. We’ll create these variables using tkinter functions, such as IntVar(). These functions accept many arguments such as a window name or a value. In our script, “gui” is the window name, and the value comes from the sysTime variable’s attributes we previously defined.

Insert the lines of code below, following the sysTime variable statement.


    year = IntVar(gui,value = sysTime.year)
    month = IntVar(gui,value = sysTime.month)
    day = IntVar(gui,value = sysTime.day)
    hour = IntVar(gui,value = sysTime.hour)
    minute = IntVar(gui,value = sysTime.minute)
    second = IntVar(gui,value = sysTime.second)


We need to rewrite the timezone variable definition in the tkinter syntax too.


    timezone = IntVar(gui,value = -7)


Just as with the date and time information, we need to split the location data into latitude and longitude, which use decimal precision and therefore require a different tkinter function, DoubleVar(). As these variables will replace the existing location variable, we can comment out that line of code so that Python ignores it.

The “#” at the beginning of the line of code defining the location variable, so Python ignores it.


    # location = [34.052235, -104.741667]
    latitude = DoubleVar(gui,value = 34.052235)
    longitude = DoubleVar(gui,value = -104.741667)


Defining the variables for each part of the time, date and location data.


Now that we’ve accounted for all the individual variables we can add them to the GUI. There are several ways to do this in Python, and for this tutorial we’ll use the grid method, which will allow us to align each variable with a corresponding text label to describe it.

Python refers to the GUI components, such as labels and buttons, as widgets. We’ll begin by creating a widget for each label. Each widget needs a unique name, so for the label widgets we’ll use “l_” followed by the name of the data it contains, for example “l_year”. The labels themselves will include information for the window it belongs on, the text to display, its position within the window, and how it is aligned. All the labels will belong to the “gui” window for now, and the text to be displayed is simply their name. The grid method uses rows and columns to position the label, and a “sticky” attribute to align the label.


The last widget allows us to insert a blank line that spans both columns.


    l_year = Label(gui,text = 'Year').grid(row=0,column=0,sticky='w')
    l_month = Label(gui,text = 'Month').grid(row=1, column=0,sticky='w')
    l_day = Label(gui,text = 'Day').grid(row=2, column=0,sticky='w')
    l_hour = Label(gui,text = 'Hour').grid(row=3, column=0,sticky='w')
    l_minute = Label(gui,text = 'Minute').grid(row=4, column=0,sticky='w')
    l_second = Label(gui,text = 'Second').grid(row=5, column=0,sticky='w')
    l_timezone = Label(gui,text = 'Timezone').grid(row=6, column=0,sticky='w')
    l_latitude = Label(gui,text='Latitude (N)').grid(row=7,column=0,sticky='w')
    l_longitude = Label(gui,text='Longitude (W)').grid(row=8,column=0,sticky='w')
    l_null = Label(gui,text=" ").grid(row=9,columnspan=2)


The code for the labels should look like this:

Defining the label widgets.


While the Label widget is used to display text and can not be changed by the user, the Entry widget accepts a starting value and user input. We’ll set each Entry’s “textvariable” to the corresponding initial value we assigned to the variables. Later, the user can change these values by simply entering a different value. To describe the widget for each Entry, we’ll use the prefix “e_” and the name of the Entry, for example “e_year”.


    e_year = Entry(gui, textvariable = year,width=6).grid(row=0,column=2, sticky='e')
    e_month = Entry(gui, textvariable = month,width=6).grid(row=1,column=2, sticky='e')
    e_day = Entry(gui, textvariable = day,width=6).grid(row=2,column=2, sticky='e')
    e_hour = Entry(gui, textvariable = hour,width=6).grid(row=3,column=2, sticky='e')
    e_minute = Entry(gui, textvariable = minute,width=6).grid(row=4,column=2, sticky='e')
    e_second = Entry(gui, textvariable = second,width=6).grid(row=5,column=2, sticky='e')
    e_timezone = Entry(gui, textvariable = timezone,width=6).grid(row=6, column = 2, sticky='e')
    e_latitude = Entry(gui, textvariable = latitude,width=16).grid(row=7, column =2)
    e_longitude = Entry(gui, textvariable = longitude,width=16).grid (row=8,column =2)


The code for the Entry section should look like this:

Defining the entry widgets.


Now that we can change the initial values for each parameter, we need a way to tell the script when to calculate our changes. For this we’ll use a button widget. Like the other widgets, the button can contain several bits of information, such as the text to display on the button. Most importantly it contains a command attribute, telling it what to do when the button has been clicked. In this example, the script will run the function “whenWhere”, which is yet to be defined.


    b_ApplySun = Button(gui,text='Apply to Sun',bg='yellow',command=whenWhere).grid(row=11,column=2)


This is the code for the button section of the script.

Defining the button widget.


To define the whenWhere() function, insert the code below into the script following the variables section and before the labels section. Note that it’s important to indent the lines of code after defining the function.

Using the get() function the “lookupTime” variable is updated with the current date and time values and the “location” variable is updated with the current latitude and longitude values. Note, some of these variables will be defined immediately after this step.

The next statement first passes three values to the sunpos module, then it accepts the returned data in the “results” variable as a list containing the heading and elevation.

Then the function “setTerragenSunHeadingAndElevation” is called, passing along the text “Sunlight 01” and the results variable containing the sun heading and elevation.


    def whenWhere():
    lookupTime = [year.get(), month.get(), day.get(), hour.get(), minute.get(), second.get(),timezone.get()]
    location = [latitude.get(),longitude.get()]
    results = sp.sunpos(lookupTime,location,True)
    setTerragenSunHeadingAndElevation(“Sunlight 01”,results)


This is the code defining the whenWhere() function.

Create the lookupTime function.


Since we’ve included the “lookupTime” variable within the whenWhere() function, and it is updated by the get() functions whenever the whenWhere() function is called, we no longer need it in the variables section.

Delete the line of code in the variables section that defines the “lookupTime” variable.

Also included in the whenWhere() function was the call to a function that performs the actual RPC commands and updates Terragen. The function will accept two arguments, one for the item to modify in Terragen which is the Sunlight 01 node, and the other, a list, containing the heading and elevation values. Let’s code that function directly beneath the previous function in the script.

Once you’ve completed coding the function below you can remove the previous lines of code created in Part 02 of this tutorial that did the same thing.

This function includes error checking commands such as “try”, “except”, and “raise”. Checking for potential errors or exceptions allows the program to handle them before they cause a problem, like crashing the application. For example, if a user entered text data in a numeric field, and the program expected numeric data, then an error would occur and the application might crash. With error handling, the application could display a message indicating the wrong type of data was entered and giving the user an opportunity to change the data. You can read more about the error checking command in the Terragen RPC documentation.


    def setTerragenSunHeadingAndElevation(name,values):
    try:
    node = tg.node_by_path(name)
    try:
    node.set_param('heading',values[0])
    node.set_param('elevation',values[1])
    except AttributeError as err:
    showError("Sunlight not in project. Refresh list.")
    except ConnectionError as e:
    showError("Terragen RPC connection error: " + str(e))
    except TimeoutError as e:
    showError("Terragen RPC timeout error: " + str(e))
    except tg.ReplyError as e:
    showError("Terragen RPC server reply error: " + str(e))
    except tg.ApiError:
    showError("Terragen RPC API error")
    raise


Here is the code for the setTerragenSunHeadingAndElevation function.

The setTerragenSunHeadingAndElevation function.


The last step in this section is to create another function which displays any message passed to it by another part of the script. This function receives one input, the message to be displayed.


    def showError(text):
    errMsg.set(text)


This is the code for the showError() function.

The showError function.


We need to create one last variable to store any messages set by the error checking processes. We can define the new variable at the end of the variables section of the script.


    errMsg = StringVar()


Here is the variable section of the script.

The defined variables.


Save and run the script. Try entering new values, especially for the hour of the day, and applying them to the Terragen sun.


Part 04: Modifying parameters more easily[edit]


The GUI with sliders added.


Now that we have a GUI and can modify the values for the time of day and location, let’s make it easier to make those modifications by adding sliders to the interface for the date and time parameters.

A slider can be created with Tkinter’s Scale widget. We’ll use the prefix “s_” and the name of each parameter to define each slider. Scale widgets can also accept arguments, such as which window to be positioned in, a range of values to slide between, and the orientation of the slider. We’ll set an appropriate range for each slider, for example: 1 - 12, for the Month parameter, and orient the sliders horizontally so they best fit the layout of the GUI we’ve already created.

Insert the following lines of code into the script, between the Labels section and the Entry section.


    s_year = Scale(gui,from_= 1900, to = 2099,variable=year,orient=HORIZONTAL,showvalue=0).grid(row=0,column=1)
    s_month = Scale(gui,from_= 1, to = 12,variable=month,orient=HORIZONTAL,showvalue=0).grid(row=1,column=1)
    s_day = Scale(gui,from_= 1, to =31,variable=day,orient=HORIZONTAL,showvalue=0).grid(row=2,column=1)
    s_hour = Scale(gui,from_= 0, to = 23,variable=hour,orient=HORIZONTAL,showvalue=0).grid(row=3,column=1)
    s_minute = Scale(gui,from_= 0,to = 59,variable=minute,orient=HORIZONTAL,showvalue=0).grid(row=4,column=1)
    s_second = Scale(gui,from_= 0, to = 59,variable=second,orient=HORIZONTAL,showvalue=0).grid(row=5,column=1)
    s_timezone = Scale(gui,from_= -13, to = 13,variable=timezone,orient=HORIZONTAL,showvalue=0).grid(row=6,column=1)


Here’s the sliders section of the script:

Sliders section.


You may have noticed in the lines of code above, that the slider’s grid position is located in column 1, and in the previous step the label’s grid position was located in column 0, while the entry’s grid position was located in column 2. This allows our slider to drop in between the label and the entry columns of the GUI.

Visually, the GUI will look better if the latitude and longitude entry fields align with the sliders because of their width; and that the Apply button is centered within the GUI, so modify all their grid positions to column 1 as well.


    e_latitude = Entry(gui, textvariable = latitude,width=16).grid(row=7, column =1)
    e_longitude = Entry(gui, textvariable = longitude,width=16).grid (row=8,column =1)
    b_ApplySun = Button(gui,text='Apply to Sun',bg='yellow',command=whenWhere).grid(row=11,column=1)


If you save the script and run it, you’ll notice how much easier it is to adjust the time and date parameters with the sliders, while still being able to enter a precise numerical value in the Entry field.


Part 05: Multiple sunlight nodes[edit]


The final GUI for Part 05.


“But, what if there is more than one sunlight node in the Terragen project, or it’s named something else, or it’s missing entirely?” Good questions, and in this section we’ll address them while expanding the functionality of the script and GUI.


So far all of the GUI components have been located in the “gui” window. Tkinter allows you to divide the window into smaller frames. You can then tell each component which frame to be displayed in.

To accommodate the features we’ll be adding to the script we can start by making the GUI larger. Edit the existing line of code that sets the width and height of the GUI as indicated below.


    gui.geometry("900x600")


We define a frame using the LabelFrame widget. Just like the other widgets, it accepts options for which window to be located in, and what text to display, if any. For starters we’ll create two frames. The first frame will be used to select the available Sunlight nodes in the project, and the second frame will contain the parameters we’ve already coded.

To define the frames, enter the following lines of code right below the existing code that defines the window.

The first frame, frame0, will reside in the “gui” window and display the text “Select sunlight node / Click drop-down arrow to refresh list:”. It has 20 pixels of padding on its left and right, and 10 pixels of padding above and below it. No border will be drawn around the frame because “relief” set to FLAT.


    frame0 = LabelFrame(gui,text="Select sunlight node / Click drop-down arrow to refresh list:",relief=FLAT, padx=20,pady=10)


The second frame, frame1, will aslo reside in the “gui” window. It will display the text “Enter time and location:”. It has 10 pixels of padding all around it and will display a border.


    frame1 = LabelFrame(gui,text="Enter time and location: ",padx=10,pady=10)


Then skip a line and enter the following lines of code which define where the frames are positioned within the gui window.


    frame0.grid(row=0,column=0,padx=5,pady=5)
    frame1.grid(row=10,column=0,padx=25,pady=0,sticky='w')


The code should look like this:

Defining the frame.


In order for the frames to have any effect, the display option for the labels, sliders, entry, and buttons needs to be changed from “gui” to the one of the frames. Edit the lines of code for each labels, sliders, entry, and button which are currently coded to “gui” to “frame1”.

The code for the labels, sliders, entry and button now look like this:

The labels, sliders, entry and buttons edited to reflect the new frame location.


So far the design of our script has been to send certain information to Terragen in order to change something in the project, but now we want Terragen to provide some information about the project first. Specifically, we want to know what sunlight type nodes are in the project as soon as the script is run. Our GUI needs to reflect this information and provide us with the ability to select among the sunlight nodes.

Coding the query to Terragen consists of two parts. Part one will be a way in which to store the sunlight node names and IDs returned from Terragen. Part two will be a function to make the request to Terragen for the sunlight node information.

Enter the following line of code into the script, after the functions already defined and before the labels section.

This creates the variable “sunNodes” which will store the returned information from the getSunlightInProject() function.


    sunNodes = getSunlightInProject()


Next, create the new function getSunlightInProject() just below the last function currently in the script by entering the lines of code below. Note, that this function also includes the same kind of error checking as in Part 3; please see that section for more explanation on capturing errors and exceptions. This function initializes the variables “nodeIDs” and “nodeNames” so that they’re empty each time this function is run, then populates them with the current information from the Terragen project.


    def getSunlightInProject():
    try:
    project = tg.root()
    nodeIDs = []
    nodeNames = []
    nodeIDs = project.children_filtered_by_class(‘sunlight’)
    for nodes in nodeIDs:
    nodeNames.append(nodes.name())
    return nodeIDs,nodeNames
    except ConnectionError as e:
    showError("Terragen RPC connection error: " + str(e))
    except TimeoutError as e:
    showError("Terragen RPC timeout error: " + str(e))
    except tg.ReplyError as e:
    showError("Terragen RPC server reply error: " + str(e))
    except tg.ApiError:
    showError("Terragen RPC API error")
    raise


The code for the function call looks like this:

The getSunlightInProject() function.


Now that we have the names of the sunlight nodes in the project, we can add them to our GUI. Currently, the labels, sliders, entries and Apply button reside in the “frame1” portion of the GUI, so we’ll place the names of the sunlight nodes in the “frame0” portion of the GUI and use a widget known as a Combobox to do so. As we’ve seen with other widgets, Comboboxes also accept options such as which part of the window to be displayed in.

Enter the following lines of code right after the statement to call the getSunlightInProject() function and before the Labels section.

The postcommand option is run each time a change is made to the Combobox. In this example, the updateCBList function is run, ensuring that the list of Sunlight nodes is always up to date. The textvariable “num” acts as a pointer to which item in the list is acted upon.


    cb_suns = ttk.Combobox(frame0,textvariable=num,postcommand=updateCbList)


Here's the code for the Combobox:

Combobox.


Next we need to define the new variable and function that we referenced in the above line of code.

In the variables section of our script, add the following to define the pointer variable “num”. By using tkinter’s StringVar() function we can later retrieve and store new values to the variable with the get() and set() functions.


    num = StringVar()


Here is the variables section of the script after adding the num variable:

Add the num variable in the variables section.


Following the last function we defined in our script, add the following lines of code. Note there is error checking in this function because it’s possible that a sunlight node is no longer in the Terragen project or the names have been changed since the last time this query was made.


    def updateCbList():
    updateSuns = getSunlightInProject()
    try:
    updateSunID.set(updateSuns[0])
    updateSunNames.set(updateSuns[1])
    cb_suns["values"] = updateSuns[1]
    showError(" ")
    except TypeError as e:
    showError("Sunlight nodes not found. " + str(e))


The code for this function looks like this:

The updateCbList() function.


We need to create the two variables we just reference in the above function so they can store the sunlight node ID numbers and names, and get updated whenever the updateCbList() function is called. At the end of the variables section in the script add these lines of code:


    updateSunID = StringVar()
    updateSunNames = StringVar()”


The code for the variable section now looks like this:

The variables section.


Return to the script and directly beneath the line of code that creates the Combobox add these lines of code, in order to display the updated list of Sunlight nodes and to catch any errors or exceptions.


    Try:
    cb_suns["values"]=r[1]
    cb_suns.current(0)
    except TypeError as e:
    showError("No sunlight nodes in project or Terragen not running." + str(e))


To position the Combobox within frame0 add this line of code.


    cb_suns.grid(row=0,column=0)


When dealing with multiple sunlight nodes in the project, it would be helpful if there was a way within the script to enable or disable each sunlight node. Since we’ve already created a Combobox to select a sunlight node, we can easily add a bit of code to enable or disable the selected node. We’ll use a button widget for this.

Just after the Combobox code and before the Labels code, add this line of code to create the button to create a button to the right of the Combobox position that will execute the ckboxSun command when clicked.


    b_SunOnOff = Button(frame0,text="Toggle sun on/off",bg='pink',command=ckboxSun).grid(row=0,column=1,padx=10)


The code for the Combobox should look like this:

The Combobox and button.


Now we’ll create the ckboxSun() function that gets called when the button is clicked. Just below the lines of code for the last function we wrote add the following to call the toggleTerragenSun() function, passing it the index number for the currently selected item in the Combobox.


    def ckboxSun():
    toggleTerragenSun(num.get())


You might wonder why we call one function when the button gets clicked and then another function. The reason is to keep the functions themselves as simple as possible. That way they can be used by other functions within the script. Let’s create the toggleTerragenSun() function so we can turn the sun on and off. Just below the last function type the following lines of code, which also contain some error checking.


    def toggleTerragenSun(name):
    try:
    node = tg.node_by_path(name)
    try:
    x = node.get_param_as_int('enable')
    except AttributeError as err:
    showError("Sunlight not in project. Refresh list.")
    else:
    if x == 1:
    node.set_param('enable',0)
    else:
    node.set_param('enable',1)
    except ConnectionError as e:
    showError("Terragen RPC connection error: " + str(e))
    except TimeoutError as e:
    showError("Terragen RPC timeout error: " + str(e))
    except tg.ReplyError as e:
    showError("Terragen RPC server reply error: " + str(e))
    except tg.ApiError:
    showError("Terragen RPC API error")
    raise


Here is the code for the above functions.

Code for the ckboxSun() function and toggleTerragenSun() function.


Save the script and run it. In Terragen, duplicate the Sunlight object a few times, then click in the Combobox and enable or disable the lights, and change their heading and elevation.


Part 06: Presets[edit]


GUI for Part 06.


Let’s add one more section to our GUI which contains presets for selected locations and timezones around the world. This way we can jump from the Los Angeles timezone to any other timezone around the world with a click of a button.

Presets drop down list.


We’ll create an external text file to hold the data so that it can be easily customized in any text editor, such as Notepad, and save the file in the CSV format which stands for “comma separated values”. As the format name implies each piece of data is separated from the next by a comma. Each row in the file should be formatted as follows: Timezone City/Country , longitude, latitude, timezone offset

Copy and paste the following data into a text file and save it as presets_latLong.csv. As you can see from the “Timezone City/Country” data the entries are organized from timezone -11 to timezone +13. This is the order that the data will show up in the GUI.

    -11 Liku/Niue, -169.792327,-19.053680, -11.0
    -10 Honolulu/Hawaii, -157.854995, 21.306647, -10.0
    -9 Adak/Adak Island, -176.660156, 51.862923, -9.0
    -8 Anchorage/Alaska, -149.721679, 61.227957, -8.0
    -7 Los Angeles/USA, -104.741667, 34.052235, -7.0
    -7 Vancouver/Canada, -123.108673, 49.263323,-7.0
    -6 Calgary/Canada, -114.071044, 51.058660, -6.0
    -5 Winnipeg/Canada, -97.141113, 49.866316, -5.0
    -4 Igloolik/Igloolik Island, -81.806259, 69.379587, -4.0
    -4 Ontario/Canada, -84.887696, 49.315573, -4.0
    -3 Salvador/Brazil, -38.480987, -12.973780, -3.0
    -3 Goose Bay/Newfoundland, -60.342407, 53.304621,-3.0
    -2 Nuuk/Greenland, -51.723632, 64.182464, -2.0
    -1 Sao Filipe/Fogo, -24.483718, 14.890709,-1.0
    0 Reykjavik/Iceland, -21.928710,64.144161, 0.0
    +1 Dublin/Ireland, -6.249847, 53.350551, 1.0
    +1 Greenwhich/UK, -0.193634, 51.489293, 1.0
    +2 Brussels/Belgium, 4.390869, 50.824444, 2.0
    +2 Berlin/Germany, 13.405151, 52.511763, 2.0
    +2 Krakow/Poland, 19.929199, 50.050084, 2.0
    +3 Odesa/Ukraine, 30.739746, 46.468132, 3.0
    +3 Novgorodd/Russia, 43.978271, 56.307396, 3.0
    +4 Baku/Azerbaijan, 49.844970, 40.390488, 4.0
    +4 Dubai/UAE, 55.341795, 25.138654, 4.0
    +5 Islamabad/Pakistan, 73.044433, 33.706061, 5.0
    +6 Jahore/Bangladesh, 89.229125, 23.179080, 6.0
    +7 Bangkok/Thailand, 100.546875, 13.496472, 7.0
    +8 Beijing/China, 116.389160, 39.884450, 8.0
    +8 Manilla/Philippines, 120.878899, 14.491408, 8.0
    +9 Sapporo/Japan, 141.344604, 43.056847, 9.0
    +10 Andersen AFB/Guam, 144.924087, 13.580586, 10.0
    +11 Magadan/Russia, 150.805206, 59.568418, 11.0
    +12 Queenstown/New Zealand, 168.618640, -45.104546, 12.0
    +12 McMurdo Station/Antartica, 166.690063, -77.84011, 12.0
    +13 Nomuka/Tonga, -174.802093, -20.251890, 13.0


With the presets file saved, let’s load the data into our script. Immediately following the line of code that retrieves the Sunlight nodes in the Terragen project add the line of code:


    presets()


The statement that calls the presets() function.


Then create the new presets() function with the following lines of code, which will read the contents of the text file in “read only” mode, one line at a time. Each line is split into data each time the comma separator is encountered and these bits of data are appended to new variables for the Timezone City/Country, longitude, latitude and timezone offset.


    def presets():
    file = open('presets_latlong.csv',"r")
    content = file.readlines()
    for line in content:
    x,y,z,a = line.strip().split(',')
    plocation.append(x)
    plat.append(z)
    plon.append(y)
    ptz.append(a)
    file.close()


The code for the presets() function looks like this:

The presets function.


We referenced four new variables in the presets() function above, which store the data from the presets text file. Since these variables are created when the script is first run and their values do not change during the execution of the script, we can define them using standard Python variables, and not tkinter variables such as with the StringVar() function. Add these variables to the variables section of the script.


    plocation = []
    plat = []
    plon = []
    ptz = []


Here's the updated variables section in our script:

The variables section of the script.


We’ll add another frame to our main window and place the GUI components to display the presets there. We’ll need to add another Combobox to display the preset choices, labels for descriptions, and a button in order to update the variables for the timezone, latitude and longitude. Following the last line of code in the script add the lines below:


    cb_presets = ttk.Combobox(frame3,textvariable=pnum)
    cb_presets["values"]=plocation
    cb_presets.current(16)
    cb_presets.grid(row=0,column=1,sticky='w')

    l_presetlocation = Label(frame3,text="Location: ").grid(row=0,column=0,sticky='w')
    l_presetLat = Label(frame3,text="Latitude:").grid(row=1,column=0,sticky='w')
    l_presetLat = Label(frame3,text="Longitude:").grid(row=2,column=0,sticky='w')
    l_presetTz = Label(frame3,text="Timezone:").grid(row=3,column=0,sticky='w')
    l_presetDispLat = Label(frame3,textvariable=displayLat).grid(row=1,column=1,sticky='w')
    l_presetDispLon = Label(frame3,textvariable=displayLon).grid(row=2,column=1,sticky='w')
    l_presetDispTz = Label (frame3, textvariable=displayTz).grid(row=3,column=1,sticky='w')

    b1 = Button(frame3,text="Apply Preset",command=setLatLon).grid(row=5,column=1,pady=5,sticky='w')


Here is the code for the frame3 components:

The Combobox for the Presets.


We referenced a new frame and variable in the above lines of code to define the Combobox, so let’s add both those now.

Append the following line of code to the section of our script where we’ve defined the window title, size and frames. Note, we will define frame2 in the last section of this tutorial.


    frame3 = LabelFrame(gui,text="Location presets: ",padx=10,pady=10)
    frame3.grid(row=15,column=0,padx=25,pady=10,sticky='w')


Here's the updated lines of code for the window section:

Defining frame3 in the GUI window.


Then add the following line of code the the variables section of our script to account for the new variable, which identifies the current item in the preset list.


    pnum=StringVar()


Here's the updated variables section:

Variables section.


The last step to our preset code is to define the following function which will update and display the latitude, longitude and timezone variables to those of the selected preset.


    def setLatLon():
    v = cb_presets.current()
    displayLat.set(plat[v])
    displayLon.set(plon[v])
    displayTz.set(ptz[v])
    latitude.set(plat[v])
    longitude.set(plon[v])
    timezone.set(ptz[v])


Here's the code for the setLatLon() function:

The setLatLon() function.


Three new variables were referenced in the setLatLon() function above, so we need to add them to the variable section, using the tkinter format so they can be updated throughout the script with the get() and set() functions. Add these lines of code following the last entry in the variable section of the script.


    displayLat = StringVar()
    displayLon = StringVar()
    displayTz = StringVar()


Here's the updated variables section of our script:

Variables section.


Part 07 - Displaying previous values, errors & exceptions[edit]


GUI for Part 07.


Several of the functions throughout this script have accounted for errors and exceptions, but up to now we haven’t displayed those messages. In addition, as we modify the Sunlight nodes in Terragen, it would be useful if we could see those values displayed in the GUI. We’ll use frame2, the empty right portion of our window to display any of these kinds of information.


We can begin by defining the frame, which will consist of a border with a description and labels. In the windows section of the script insert the following line of code between frame1 and frame3.


    frame2 = LabelFrame(gui,text="Last values plotted were: ",padx=10,pady=10)
    frame2.grid(row=10,column=1,padx=0,pady=0,sticky='nw')


The code for the GUI window now looks like this:

Defining the frame2 area within the main window.


Since the display of this information is mostly in the form of Labels, let’s add the following code at the end of the Labels section and before the Sliders section of our script to display the last Sunlight node that was modified via our script, and the values used to do so.


    l_lastSun = Label(frame2,text='Node: ').grid(row=12,column=0,sticky='w')
    l_lastSunName = Label(frame2,textvariable=displayLastSun).grid(row=12,column=1,sticky='w')
    l_dateTime = Label(frame2,text='Date & time: ').grid(row=13, column=0,sticky='w')
    l_dateTimeData = Label(frame2,textvariable=displayTimeData).grid(row=13, column=1,sticky='w')
    l_location = Label(frame2,text='Location: ').grid(row=14,column=0,sticky='w')
    l_locationData = Label(frame2,textvariable=displayLocation).grid(row=14,column=1,sticky='w')
    l_azimuth = Label(frame2,text='Azimuth:').grid(row=15,column=0,sticky='w')
    l_azimuthData = Label (frame2,textvariable=azimuth).grid(row=15,column=1,sticky='w')
    l_elevation = Label(frame2,text='Elevation:').grid(row=16,column=0,sticky='w')
    l_elevationData = Label(frame2,textvariable=elevation).grid(row=16,column=1,sticky='w')
    l_errMsg = Label(frame2,textvariable=errMsg,fg='red').grid(row=17,columnspan=2,sticky='w')


The labels section of the script, with frame2 components added.

Labels section of the script.


Now we’ll add the new variables we referenced in the lines of code above, so they can be updated by different functions in the script with the get() and set() functions. Append the following lines of code to the Variables section of the script.


    displayLastSun = StringVar()
    displayTimeData=StringVar()
    displayLocation=StringVar()
    azimuth = StringVar()
    elevation = StringVar()


The updated variables section of the script:

Variables section of the script.


The last step is to actually display the information, whenever the “Apply to Sun” button is clicked. To do this we need to update some of the whenWhere() function we’ve already created. Insert the following lines of code into that function, taking care to position them in the script as indicated in the screen shot.


    displayTimeData.set(lookupTime)
    displayLocation.set(location)
    azimuth.set(results[0])
    elevation.set(results[1])
    displayLastSun.set(num.get())


The updated whenWhere() function now looks like this:

The updated whenWhere() function will now display messages.


In Conclusion[edit]

This tutorial is meant to show you the possibilities of Terragen’s new RPC feature. How might you further modify this script to add even more functionality to it?

  • How about adding the ability to modify the camera’s exposure when the Sunlight node is below the horizon? What if more than one camera existed in the project?
  • How about a button that resets the sun’s heading and elevation to their default settings?
  • How would you add your location and timezone to the presets?
  • How would you turn off all the sunlight nodes at once? Back on again?

Special thanks to John Clark Craig for making his Python script available to the public.

The Terragen RPC Time of Day example script may be downloaded from Planetside Software here.


A single object or device in the node network which generates or modifies data and may accept input data or create output data or both, depending on its function. Nodes usually have their own settings which control the data they create or how they modify data passing through them. Nodes are connected together in a network to perform work in a network-based user interface. In Terragen 2 nodes are connected together to describe a scene.

Graphical User Interface, a general term that refers to the interface of any program running in a modern graphical operating system and which does not operate exclusively from the commandline.

A parameter is an individual setting in a node parameter view which controls some aspect of the node.