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.

Getting started

To create a new web application:

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

setwd("my_app")
gp_build() # the front end and create minified js files.

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.

Basic mapping function

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)
geoplumber | output
## [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:

## [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:

# we must change dir into a gp path
ow <- setwd(path)
r <- gp_plumb(run = FALSE)
## 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'"

serve data

This is the last lines of the R/plumber.R file, as stated above, makes up the backend:

ow <- setwd(path)
tail(readLines("R/plumber.R"))
## [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:

ow <- setwd(path)
tail(readLines("R/plumber.R"))
## [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:

ow <- setwd(path)
tail(readLines("src/Welcome.js"))
## [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

Adding colour and linewidth to a remote geojson data.

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)
If you now run the 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.
World's rivers coloured and random linewidth assigned using code in above chunk

World’s rivers coloured and random linewidth assigned using code in above chunk

Analysing an sf object

Adding front end React components

Underlying stack

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.

End-point explained

#' 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.

  1. Just a comment using conventional R style.

  2. 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.

  3. 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.

  4. R package geojsonsf lightening fast converts our dataframe object traffic into a single string geojson object.

  5. 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.