Skip to content

Javascript dynamism

This tutorial should follow the first one on Javascript. We will continue the leaflet app by adding new features which requires 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.

Resetting the view

In this section, we will learn how to send things 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. There is two ways to communicate with Javascript :

  • Using rpgm.executeInJavascript(code): this allows to directly executes JS code. It is not recommended, as this limits how much data you can send to JS and does not make a clear distinction between your R and JS code.
  • Using rpgm.sendToJavascript(message, data): this sends a "packet" to Javascript with some data that is automatically translated to JS. This is the recommended way.

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

setDefaultView <- function(){
    rpgm.sendToJavascript('resetView', list()); # resetView doesn't need data, so we use an empty list
}

resetView is just a name we chose so that we will know what to do 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 didReceivedJavascriptMessage 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 set the view to its default location. Now we have to call this setDefaultView function when JS receives the resetView message:

RPGM.on('didReceiveJavascriptMessage', (message, data)=>{
    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 didReceiveJavascriptMessage, 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.sendToLanguage('r', 'mapClick', {latlng: e.latlng.toString()});
    });

This code call the function rpgm.sendToLanguage(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's 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 a complete control of the server.

We now have to catch this message in R. For that, RPGM will call a function named onRPGMJavascript(message, data) in R when it receives a Javascript message. So, let's create a function in main.R:

onRPGMJavascript <- function(message, data){
    if(message == 'mapClick'){
        gui.setValue('this', 'lastClick', data$latlng);
    }
}

The function check 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', list()); # resetView doesn't not need data, so we use an empty list
}

onRPGMJavascript <- function(message, data){
    if(message == 'mapClick'){
        gui.setValue('this', 'lastClick', data$latlng);
    }
}

And the Javascript code:

window.mapInstance = null;

function initializeMap(){
    // Stop if the user is not in the correct GUI
    if(RPGM.getCurrentStepId() !== '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.sendToLanguage('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('didReceiveJavascriptMessage', (message, data)=>{
    if(message === 'resetView'){
        setDefaultView();
    }
});
initializeMap();

Advanced topics

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