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:
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:
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:
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:
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: '© <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.