r-spatial / mapedit

Interactive editing of spatial data in R
https://www.r-spatial.org/r/2019/03/31/mapedit_leafpm.html
Other
218 stars 33 forks source link

Can you plot reactive data using editMod? #76

Open m-e-cws opened 6 years ago

m-e-cws commented 6 years ago

Hello,

I am self-taught in R and computer coding, so I apologize if this question is poorly worded or already answered elsewhere.

I am trying to plot a reactive dataset in editMod, while retaining the ability to use the edit (draw) functions provided by editMod. So far I have not found a way to plot reactive data using editMod.

I want a map that allows me to:

Below is a reproducible example using the supplementary "quakes" dataset. In the below map, I can generate summary data based on a reactive dataset selected by the slider bar, but the reactive dataset is not reflected in what is plotted on the map.

library(mapview)
library(leaflet)
library(shiny)
library(ggplot2)
library(mapedit)
library(leaflet.extras)
library(sf)

ui.q <- fluidPage(

  # Application title
  titlePanel("Quake data exploration"),

  sidebarLayout(
    sidebarPanel(
      sliderInput("range", 
                  label = "Range of earthquake magnitude:",
                  min = 4.0, max = 6.4, value = c(4.0, 6.4), step = 0.1)
    ),

    mainPanel(
      fluidRow(

         editModUI("editor", height = 860),

         absolutePanel(id = "controls", class = "panel panel-default", fixed = TRUE, 
                                           draggable = TRUE, top = 120, left = "auto", right = 20, bottom = "auto", 
                                           width = 400, height = "auto",

                                           h4("Summary data"),

                                           plotOutput("plot")))
        )
      )
    )

# Define server logic 
server.q <- function(input, output){

  #create a sf of the quake data.
  quake_mx <- data.matrix(quakes[,2:1])
  quake_mp <- st_multipoint(quake_mx)
  quake_sf <- st_sf(st_cast(st_sfc(quake_mp), "POINT"), quakes, crs=4326)

  #trying to subset the quake_sf data by the range of magnitude inputted by the slider bar. 
  #this creates a reactive dataframe and I can't seem to get it to be reflected in the editMod output.
  #I can get this to work when I use leafletProxy but then we lose the ability to draw.

  datasetMag <- reactive({
    quake_sf = subset(quake_sf, mag %in% c(input$range[1]:input$range[2]))
    })

#generate the leaflet map
  lf.quakes <- leaflet(options = leafletOptions(minZoom = 4, maxZoom = 10)) %>% 
    addTiles() %>%
    addProviderTiles("Esri.OceanBasemap",group="OceanBasemap") %>%
    addProviderTiles("Esri.WorldImagery",group="WorldImagery") %>%

    addCircleMarkers(data = quake_sf, 
                     color = "red",
                     weight = 1, 
                     fillOpacity = 0.7,
                     popup = popupTable(quake_sf, zcol = "mag"),
                     radius = quake_sf$mag) %>%

    addLayersControl(baseGroups = c("OceanBasemap","WorldImagery"),
                                  options = layersControlOptions(collapsed = FALSE))

  #call the editMod function from 'mapedit' to use in the leaflet map. I think this may be the line where I am going wrong

  edits <- callModule(editMod, "editor", leafmap = lf.quakes)

  #generate the reactive dataset based on what is drawn on the leaflet map
  datasetInput <- reactive({
    mag.range = c(input$range[1]:input$range[2])
    req(edits()$finished)
    quake_intersect <- st_intersection(edits()$finished, quake_sf)
    df <- data.frame(quake_intersect)
    df = df[df$mag %in% mag.range,] #make the dataset reactive to the slider input range
  }) 

  #render a histogram with the reactive dataset as the output

  output$plot <- renderPlot({
    hist(datasetInput()$mag, col = "grey", border = "black")
  })
}

shinyApp(ui.q, server.q)  
timelyportfolio commented 6 years ago

@m-e-cws I will try to demonstrate a way of achieving your objective soon. Thanks for the report.

davis3tnpolitics commented 4 years ago

Just to bring this comment back up to top of mind- I can get reactivity to work after loading in the basemap with a leaflet proxy.

I was using isolate to break the reactivity before loading in the basemap, but I can’t get the basemap to change and display the reactive data I need.

Any thoughts on how to accomplish this?

leungi commented 4 years ago

@davis3tnpolitics: I'm running into the same problem.

Here's an example taken from mapedit demo.

The code line of interest is callModule(editMod, "editor", mapview(qk_rv())@map), where qk_rv is intentionally set to a reactive object.

I've tried several suggestion from forums but no luck:

The only way to get it to work is with shiny::isolate() to get a static value for qk_rv. The downside is as @davis3tnpolitics noted - the qk_rv doesn't update as it should.

library(mapedit)
library(mapview)
library(shiny)
library(sf)

# make the coordinates a numeric matrix
qk_mx <- data.matrix(quakes[,2:1])
# convert the coordinates to a multipoint feature
qk_mp <- st_multipoint(qk_mx)
# convert the multipoint feature to sf
qk_sf <- st_sf(st_cast(st_sfc(qk_mp), "POINT"), quakes, crs=4326)

ui <- fluidPage(
  fluidRow(
    # edit module ui
    column(6, editModUI("editor")),
    column(
      6,
      h3("Boxplot of Depth"),
      plotOutput("selectstat")
    )
  )
)
server <- function(input, output, session) {
  qk_rv <- reactive({qk_sf})

  # edit module returns sf
  edits <- callModule(editMod, "editor", mapview(qk_rv())@map)

  output$selectstat <- renderPlot({
    req(edits()$finished)
    qk_intersect <- st_intersection(edits()$finished, qk_sf)
    req(nrow(qk_intersect) > 0) 
    boxplot(
      list(
        all = as.numeric(qk_sf$depth),
        selected = as.numeric(qk_intersect$depth)
      ),
      xlab = "depth"
    )
  })
}
shinyApp(ui, server)
m-e-cws commented 4 years ago

Hi @davis3tnpolitics @leungi ,

Thank you both for bringing this issue back to my attention. I had originally posted this almost two years ago, and since then I have learned a lot about reactive datasets and how they interact with leaflet maps and shiny apps. I have managed to solve my own problem, and I should have posted my own solution sooner. Below is the code which allows to subset data in an interactive leaflet map by both a slider bar, and by drawing a shape over points.

Feel free to check out a map I made which includes some extra tools for subsetting and selecting data in a leaflet map: https://englishm.shinyapps.io/Plastics/

library(mapview)
library(leaflet)
library(shiny)
library(ggplot2)
library(mapedit)
library(leaflet.extras)
library(sf)

ui.q <- fluidPage(

  #Application title
  titlePanel("Quake data exploration"),

  sidebarLayout(
    sidebarPanel(
      sliderInput("range", 
                  label = "Range of earthquake magnitude:",
                  min = 4.0, max = 6.4, value = c(4.0, 6.4), step = 0.1)
    ),

    mainPanel(
      fluidRow(

        editModUI("editor", height = 860),

        absolutePanel(id = "controls", class = "panel panel-default", fixed = TRUE, 
                      draggable = TRUE, top = 120, left = "auto", right = 20, bottom = "auto", 
                      width = 400, height = "auto",

                      h4("Summary data"),

                      plotOutput("plot")))
    )
  )
)

#Define server logic 
server.q <- function(input, output){

  #create a sf of the quake data.
  quake_mx <- data.matrix(quakes[,2:1])
  quake_mp <- st_multipoint(quake_mx)
  quake_sf <- st_sf(st_cast(st_sfc(quake_mp), "POINT"), quakes, crs=4326)

  #subset the quake_sf data by the range of magnitude inputted by the slider bar. 
  #this creates a reactive dataframe.

  datasetMag <- reactive({
    quake_sf[quake_sf$mag >= input$range[1] & quake_sf$mag <= input$range[2],]
  })

  #generate the leaflet map

  #set the namespace for the map
  ns <- shiny::NS("editor")

  lf.quakes <- leaflet(options = leafletOptions(minZoom = 4, maxZoom = 10)) %>% 
    addTiles() %>%
    addProviderTiles("Esri.OceanBasemap",group="OceanBasemap") %>%
    addProviderTiles("Esri.WorldImagery",group="WorldImagery") %>%

    addCircleMarkers(data = quake_sf,  #use the non-reactive dataset
                     color = "red",
                     weight = 1, 
                     fillOpacity = 0.7,
                     popup = popupTable(quake_sf, zcol = "mag"),
                     radius = quake_sf$mag) %>%

    addLayersControl(baseGroups = c("OceanBasemap","WorldImagery"),
                     options = layersControlOptions(collapsed = FALSE))

  #call the editMod function from 'mapedit' to use in the leaflet map.

  edits <- callModule(editMod, "editor", leafmap = lf.quakes)

  #now we need to create an observer so the leaflet map can 'observe' the reactive dataset and be redrawn based on the input

  observeEvent(c(input$range),{  
    proxy.lf <- leafletProxy(ns("map"))
    req(input$range)
    req(nrow(datasetMag())>0)

    proxy.lf %>%
      clearMarkers() %>%
      addCircleMarkers(data = datasetMag(),  #use the reactive dataset generated above
                       color = "red",
                       weight = 1, 
                       fillOpacity = 0.7,
                       popup = popupTable(quake_sf, zcol = "mag"),
                       radius = quake_sf$mag) 
  })

  #generate the reactive dataset based on what is drawn on the leaflet map
  datasetInput <- reactive({
    req(edits()$finished)
    quake_intersect <- st_intersection(edits()$finished, datasetMag()) #intersection between what is drawn and the reactive dataframe
    df <- data.frame(quake_intersect)
  })

  #render a histogram with the reactive dataset as the output

  output$plot <- renderPlot({
    hist(datasetInput()$mag, col = "grey", border = "black")
  })
}

shinyApp(ui.q, server.q)
leungi commented 4 years ago

Thanks prompt response and sharing @m-e-cws!

The commenting really helps with my understanding 🙏

leungi commented 4 years ago

@m-e-cws: have you encountered a case for your solution above where the CRS of the initial map (i.e. lf.quakes) is different than that of the edits() object?

For such a case, even after setting the edits() object's CRS to match the initial map, st_intersection() somehow returns empty.

I posted this issue with sf.

tim-salabim commented 4 years ago

Just to be clear, setting a crs won't change the coordinates. You need to st_transform your object

leungi commented 4 years ago

Thanks for tip @tim-salabim!

This is one of those rare moments where I have to deal with objects with NA_crs_.

I tried your suggestion, but got the following error, which is logical given st_transform() expects a EPSG, or proj4string code.

# edits() is objected returned from mapedit selection
selected <- edits()$finished

selected_trf <- selected %>% st_transform(crs = NA_crs_)

#> Error in st_transform.sfc: sfc object should have crs set
JavierMorales2 commented 1 year ago

Hello guys!

Any update about the reactive data using editMod? I'm trying to make something similar but can´t add a reactive variable or leaflet without an error on CallModule(editMod).

I want that one can upload a Shapefile, and it could be edited there

image