Skip to content

Javascript dynamism

This tutorial should follow the first one on Javascript. We will continue the leaflet app by adding new features that require JS to communicate with R, namely:

  • A button to reset the view of leaflet to its default;
  • A label that will display where the user clicked on the map by using R.

Note

Please note that the button resetting the view should be implemented directly in Javascript, as R is not required for that. But for this tutorial, we will go through R to learn how to communicate between the two languages.

How communication works

For security reasons and good practices, communication between R/Python and Javascript are possible through an event system. A packet is sent with:

  • a message, which is a string to indicate which operation to perform;
  • some values in an array or a list, which act as arguments to the message.

When the receiving language gets a packet (message and values), an event is emitted so that the developer can catch the packet and handle it by executing the corresponding code.

You will likely have a function handling a packet, that will call the correct function of your code depending on the message and with the values in the values part.

Resetting the view

In this section, we will learn how to send data from R to JS. First, create a new R script and call it main.R. Add it before the GUI in the sequence file main.pseq:

Added R file to sequence

Now open the main.R file. The main way to communicate with Javascript is the rpgm.sendToJavascript(message, rData) function to send a packet to Javascript with some data that is automatically translated to JS.

Create a setDefaultView() function and call rpgm.sendToJavascript.

setDefaultView <- function(){
    rpgm.sendToJavascript('resetView'); # resetView does not need data, so we did not set the value argument
}

resetView is just a name we chose, we will perform a specific action when receiving a resetView message in Javascript.

Go into main.pgui and add a button calling the R function:

R Button

Now we have to receive the message in Javascript by using the didReceiveMessage event. Edit the main.js file and add a function to reset the default view:

function setDefaultView(){
    // Check the map exists
    if(window.mapInstance){
        window.mapInstance.setView([51.505, -0.09], 13);
        window.mapInstance.invalidateSize(); // This fixes some grey area sometimes showing in leaflet
    }
}

The function simply checks if the map exists and sets the view to its default location. Now we have to call this setDefaultView function when JS receives the resetView message:

RPGM.on('didReceiveMessage', (message, rData)=>{
    if(message === 'resetView'){
        setDefaultView();
    }
});

You can test! Execute your app, drag the map and click on the button: the map will center itself on the initial location.

Detecting clicks in R

We will now learn how to send data from JS to R by detecting clicks on the map and tell R where the click happened. Just like the didReceiveMessage, we will create a special function in R that will be called when data is sent from Javascript.

In main.pgui, create a new label with id lastClick and a default value:

Click label

In the main.js file, add this code at the end of the initializeMap function to listen to the click on the map:

    // Detect clicks
    window.mapInstance.on('click', (e)=>{
        RPGM.sendMessage('r', 'mapClick', {latlng: e.latlng.toString()});
    });

This code calls the function rpgm.sendMessage(language, message, data):

  • The first argument is the language to send the message to. Either "r" or "python";
  • The second argument is the message name that we can choose. Here we chose "mapClick";
  • The third and last argument is some data, here it is a string representation of the longitude and latitude of the click.

Info

Why is there no rpgm.executeInLanguage(language, code) function in Javascript? Because this would open a huge security vulnerability: users could execute any R or Python code on your computer or in RPGM Server, where even a beginner computer science student could have complete control of the server.

We now have to catch this message in R. For that, RPGM will emit an event called didReceiveMessage, like in Javascript. To catch event, we give a function to rpgm.on(message, func). So, let's create a function in main.R:

rpgm.on('didReceiveMessage', function(message, jsData){
    if(message == 'mapClick'){
        gui.setValue('this', 'lastClick', jsData$latlng);
    }
}

The function checks if the message is called lastClick, which is what we have set in the main.js file. If so, set the value of the label to the latlng data from JS which is a string representation of where the user clicked. Now test the app:

Click working

It works!

Final files

Your R code should look like this:

setDefaultView <- function(){
    rpgm.sendToJavascript('resetView'); # resetView doesn't need data, so we did not set the value argument
}

rpgm.on('didReceiveMessage', function(message, jsData){
    if(message == 'mapClick'){
        gui.setValue('this', 'lastClick', jsData$latlng);
    }
}

And the Javascript code:

window.mapInstance = null;

function initializeMap(currentStep){
    // Stop if the user is not in the correct GUI
    if(currentStep !== 'leaflet'){
        return;
    }

    // Stop if already initialized
    if(window.mapInstance !== null){
        return;
    }

    // Initialize map
    window.mapInstance = L.map('map');
    window.mapInstance.setView([51.505, -0.09], 13);
    const tiles = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxZoom: 19,
        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    }).addTo(window.mapInstance);

    // Detect clicks
    window.mapInstance.on('click', (e)=>{
        RPGM.sendMessage('r', 'mapClick', {latlng: e.latlng.toString()});
    });
}

function setDefaultView(){
    // Check the map exists
    if(window.mapInstance){
        window.mapInstance.setView([51.505, -0.09], 13);
        window.mapInstance.invalidateSize(); // This fixes some grey area sometimes showing in leaflet
    }
}

RPGM.on('didEnterStep', initializeMap); // Function called when entered a new step
RPGM.on('didReceiveMessage', (message, rData)=>{
    if(message === 'resetView'){
        setDefaultView();
    }
});

Advanced topics

You can find advanced topics about the Javascript integration in the next tutorial.