geoplumber
depends only on plumber
R package to enable building web applications using R and currently ReactJS. It uses npm
but this is by no means a preference. It is a rapid development available skill utilisation choice.
Currently there is no database at the backend, this is simply because we are at the beginning of this process and there will be need to use one or more. Therefore, as things stand it is a two tier architecture and prefers the flux
one. That is data flow should be one directional and follow the “React” paradigm.
In other words, in future geoplumber
could be a lightweight geographic data server, like a barebones version of GeoServer but with a smaller footprint (< 5MB rather than > 50MB download size) and easier installation, especially if you are already an R user. There is internal discussions on the choice of a spatial database (PostgreSQL, MongoDB etc) but the package is just too young for this.
In terms of npm packages, it relies on Facebook’s “create-react-app” (CRA) npm
package to create the basic structure of the front end. But there is basic code to replace this and assemble a React package as it will be demonstrated in this vignette.
In terms of web mapping, the landscape is not huge. Being an open source pacakge, we chose the simplicity of Leaflet
. Thanks to react-leaflet
this was straightforward to integrate into a ReactJS app.
To create a new web application:
library(geoplumber)
gp_create("my_app")
This will create a my_app
folder at your current working directory. Suppose you started an R session from a folder with path /Users/ruser/
, you will have /Users/ruser/my_app
on your machine.
You can then build the new project
Note: gp_build()
produces a production ready minifed front end. It does not have to be used everytime a little change is done to the front end, as the package is still very young, it does not have the proper development “serve” function which would use gp_plumb_front()
but would have to also use gp_plumb()
to serve the backend.
At this point, if you created an app using the above examples or set your working directory to a geoplumber app. You can then serve all endpoints and front end with: gp_plumb()
# provide custom port if you wish, default is 8000
Please note that the folder should be either non-existent (it will then be created by gp_create()
) or empty. If your working directory is an empty directory, you can create a geoplumber app using: geoplumber::gp_create(".")
.
After running gp_create()
you might want to use gp_rstudio("project_name")
to create an RStudio project from R’s command line. You could also use RStudio’s default way of creating a project within an existing directory – or just don’t create an RStudio project.
You can also give geoplumber a path including one ending with a new directory. Currently, geoplumber does not do any checks on this but the underlying CRA does.
geoplumber
is not built to do mapping or more low level rendering, that should happen on the front end. The function is there to showcase how that is possible using modern packages such as React.
Given some dataset such as the traffic
included in this package let us get a web application up and running where we can see some geojson
data on a map.
library(geoplumber)
html_file = gp_map(traffic, browse_map = FALSE, dest_path = getwd(), height = "260px")
htmltools::includeHTML(html_file)
## [1] TRUE
Let us assemble an app using gp_create
:
path = file.path(tempdir(), "new_app")
# make sure that exists
dir.create(path)
gp_create(path = path)
## To build/run app, set working directory to: /tmp/RtmpgwBKsz/new_app
## Standard output from create-react-app works.
## You can run gp_ functions from directory: /tmp/RtmpgwBKsz/new_app
## To build the front end run: gp_build()
## To run the geoplumber app: gp_plumb()
## Happy coding.
# check if that is the case
gp_is_wd_geoplumber(path = path)
## [1] TRUE
Now that path
is a valid geoplumber
application, quick look into it:
list.files(path)
## [1] "package.json" "public" "R" "src"
list.files(file.path(path, "R"))
## [1] "plumber.R"
Quickly, the package.json
file is standard yarn
or npm
node package file where various settings live for Node to manager our package, as the directory is now a valid node package. The public
is the static “public” directory for css
, custom js
and conventionally the entry point index.html
to live and in future possibly for our deployed front-end to live. Finally, the src
is where all React components and our geoplumber
front-end will be built from. Again, this could all change as geoplumber
flourishes.
Quick peek:
list.files(file.path(path, "src"))
## [1] "App.css" "App.js"
## [3] "App.test.js" "components"
## [5] "img" "index.js"
## [7] "logo.svg" "registerServiceWorker.js"
## [9] "utils.js" "Welcome.js"
The R/plumber.R
is the only backend script that would be executed in a fresh geoplumber app. This could change, based on the documentations in plumber
and also as geoplumber
grows. Using the underlying plumber
package, we can inspect what is in the R/plumber.R
file:
## WARNING:
## Looks like geoplumber was not built, serveing API only.
## To serve the front end run gp_build() first.
length(r$endpoints[[1]]) # it should be 5
## [1] 5
There is a default endpoint where a message is echoed by the server, we can test this using:
make_req <- function(verb, path, qs="", body=""){
req <- new.env()
req$REQUEST_METHOD <- toupper(verb)
req$PATH_INFO <- path
req$QUERY_STRING <- qs
req$rook.input <- list(read_lines = function(){ body },
read = function(){ charToRaw(body) },
rewind = function(){ length(charToRaw(body)) })
req
}
response <- r$endpoints[[1]][[1]]$exec(make_req("GET", "/"), "")
response$msg # it should be: "The message is: 'nothing given'"
## [1] "The message is: 'nothing given'"
This is the last lines of the R/plumber.R
file, as stated above, makes up the backend:
## [1] "#' Tell plumber where our public facing directory is to SERVE."
## [2] "#' No need to map / to the build or public index.html. This will do."
## [3] "#' plumber1.0 working directory is current file's parent."
## [4] "#'"
## [5] "#' @assets ../build /"
## [6] "list()"
We can add the following to the end of the file and rerun gp_plumb
:
#' Serve geoplumber::traffic from /api/data
#' @get /api/data
get_traffic <- function(res) {
geojson <- geojsonsf::sf_geojson(geoplumber::traffic)
res$body <- geojson
res
}
For a break down of the above chunk go to the section (End-point explained) explaining it.
Let us use a helper function from geoplumber
called gp_endpoint_from_clip
:
ow <- setwd(path)
old_clip <- clipr::read_clip()
# adding below to clipboard
clipr::write_clip(c(
"#' vignette added endpoint",
"#'",
"#' Serve geoplumber::traffic from /api/data",
"#' @get /api/data",
"get_traffic <- function(res) {",
" geojson <- geojsonsf::sf_geojson(geoplumber::traffic)",
" res$body <- geojson",
" res",
"}"
))
gp_endpoint_from_clip()
clipr::write_clip(old_clip)
check if that worked:
## [1] "#' @get /api/data"
## [2] "get_traffic <- function(res) {"
## [3] " geojson <- geojsonsf::sf_geojson(geoplumber::traffic)"
## [4] " res$body <- geojson"
## [5] " res"
## [6] "}"
That did work. Now, we should have an extra endpoint:
# we must change dir into a gp path
ow <- setwd(path)
r <- gp_plumb(run = FALSE)
length(r$endpoints[[1]]) # it should now be 6
## [1] 6
Now, we cannot simulate the last endpoint by executing it as we have done above. But if you would like to run gp_plumb
, you should now have geojson
object returned to you if you visit localhost:8000/api/data
URL on your browser or curl
it.
What about the front end? Well, we need to consume that URL somehow. There is few functions to assist with composing React components. One of them is called gp_add_geojson
which takes a URL and can “render” the data from it:
ow <- setwd(path)
gp_add_geojson(endpoint = "/api/data") # which is the default
## Remember to rebuild frontend: gp_build()
## Success.
A peek into the main React component called “Welcome” to see if we have a component called GeoJSONComponent
:
## [1] " <GeoJSONComponent style={{color:'#3388ff'}} fetchURL='http://localhost:8000/api/data' map={ this.state.map } />"
## [2] " </Map>"
## [3] " );"
## [4] " }"
## [5] "}"
## [6] ""
Yep, that looks good. We are now ready to build our front end and consume the data served from the R backend:
ow <- setwd(path)
gp_build()
## Running: npm run build
## Looks like first run, installing npm packages...
## Running: gp_npm_install()
## Now trying to build: npm run build
## Standard output from create-react-app above works.
## To run the geoplumber app: gp_plumb()
Now, we have installed the required dependencies of the npm package and we can run the application using:
gp_plumb()
# visit localhost:8000 to see the application
There is a geojson file which includes world’s rivers. Please run this example remembering to have colourvalues
R package.
We can read it into a dataframe, add a “colour” column, then add another “lwd” column and tell geoplumber
to use them to colour and draw them accordingly.
path = file.path(tempdir(), "rivers") # new geoplumber app
dir.create(path)
gp_is_wd_geoplumber(path)
#> [1] FALSE
gp_create(path = path)
gp_is_wd_geoplumber(path)
#> [1] TRUE
od = setwd(path)
# adding the necessary code to the plumber.R file.
write(c("#' Rivers example",
"rivers <- geojsonsf::geojson_sf('https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_rivers_lake_centerlines.geojson')",
"rivers <- rivers[, c('name', 'geometry')]",
"n <- nrow(rivers)",
"m <- grDevices::colorRamp(c('red', 'green'))( (1:n)/n )",
"rivers$col <- colourvalues::colour_values(rivers$name, palette = m, include_alpha = F)",
"rivers$lwd <- seq(5, 8, 0.5)",
"#' now serve `rivers` object from /api/rivers",
"#' @get /api/rivers",
"get_rivers <- function(res) {",
"geojson <- geojsonsf::sf_geojson(rivers, factors_as_string = TRUE)",
"res$body <- geojson",
"res",
"}"), file = "R/plumber.R", append = TRUE)
tail(readLines("R/plumber.R"))
#> [1] "#' @get /api/rivers"
#> [2] "get_rivers <- function(res) {"
#> [3] "geojson <- geojsonsf::sf_geojson(rivers, factors_as_string = TRUE)"
#> [4] "res$body <- geojson"
#> [5] "res"
#> [6] "}"
gp_add_geojson(endpoint = "/api/rivers",
color = "col", # matching above rivers$col
line_weight = "lwd",
properties = TRUE)
#> Remember to rebuild frontend: gp_build()
#> Success.
gp_build()
#> Running: npm run build
#> Looks like first run, installing npm packages...
#> Running: gp_npm_install()
#> Now trying to build: npm run build
#> Standard output from create-react-app above works.
#> To run the geoplumber app: gp_plumb()
# gp_plumb()
setwd(od)
gp_plumb
function you should see something similar to the following image in your browser. The reason, you could guess, is that the front end JavaScript code takes into account a colour value and also passes the lwd
to the appropriate styling to the underlying Leaflet API.
As geoplumber uses both R and Node, currently R v3.4 is the minimum, we will do all we can to make it backward compatible both in R and node. As for node, whatever the needs of Facebook’s create-react-app
is. For instructions on installing node on your OS please refer to the NodeJS official docs.
#' Serve geoplumber::traffic from /api/data (1)
#' @get /api/data
get_traffic <- function(res) { # (3)
geojson <- geojsonsf::sf_geojson(geoplumber::traffic)
res$body <- geojson # (5)
res
}
Let us go through this, though you would need a better understanding of pulmber
to be able to understand exactly what is going on.
Just a comment using conventional R style.
plumber
markup, @get
is translated into a http GET request, the space allows the parser to recognise what is coming next which is the URL or the local path to the root URL. In this case /api/data
. Current version of plumber
is sensitive to the ending slash.
Conventional R function definition but has res
parameter in it. This is passed down by plumber
and stands for the http response
to be sent back to the client.
R package geojsonsf
lightening fast converts our dataframe
object traffic
into a single string geojson
object.
We implicitly make the response a json
response by populating the body of the response with the object from (4). Finally, we return the response so that plumber
can return it to the client.