Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature request] select-able table of non-spatial data for editMap #56

Open
tiernanmartin opened this issue Jul 19, 2017 · 20 comments
Open

Comments

@tiernanmartin
Copy link

It would be nice if users could control which feature they were editing by selecting a row from a DT table widget in the editMap editor.

Here's my use case: I have a tibble of non-spatial data that I want to convert into an sf object, adding geometries to each row. mapedit seems like the right package for the job! I can convert the tibble to sf by adding a sfc with empty geometries, but then there's no way to select them in the editMap editor. Having a table + map interface in the editor would allow a users to add new geometries to non-spatial data by going row by row, highlighting and drawing each shape/line/point one by one.

Additionally, I could see such an interface being useful for users who want to create MULTI* geometries (e.g., a MULTIPOLYGON containing > 1 polygons).

@timelyportfolio
Copy link
Contributor

This is a very interesting idea, and I very much appreciate the suggestion. It seems like this functionality might align nicely with our yet untouched objective of attribute editing and offer some synergy with existing mapedit capabilities. I will try to assemble a quick poc.

@tim-salabim, would love to hear your thoughts.

@tim-salabim
Copy link
Member

tim-salabim commented Jul 20, 2017

Agreed! A very nice idea. I usually approach spatial data from the geometry side of things, not from he attribute side, so this perspective never occurred to me. But I do find this a very interesting option that I would love to see implemented.
I also agree that this aligns quite nicely with the objective of feature editing. Here's a first few points that pop into my head that (I think) need consideration:

  • do we require the user to have a sf object for this? I.e. will it be up to the user to provide some empty but valid sf geometry column? I would prefer to allow all sorts of tabular data.
    (kr) I think any tabular input as a starting point and would like for a user to not have to specify an empty geometry column. If input is sf could default to the current geometry column.
    (ta) Agreed, this will also align with the RConsortium suggestion to try to be as general as possible.

  • If we allow simple tabular data, how to go about rows with no added/drawn geometries? st_point(c(NA, NA)) as a default?
    (kr) You will know better here the best way to represent a null geometry in sf. What other options do we have?
    (ta) I think st_point(c(NA, NA)) is the easiest for mixed geometries. It would be nice to scan the table for uniqueness. E.g. in case there are only POLYGON features, we should also provide empty POLYGONs for the rows without added geometries.

  • I think we need to have methods to convert drawn leaflet geojson objects to sf class sfg/XY as in st_point(c(1, 2)) which comprise the set of simplest geometry classes in sf (e.g. POINT, LINESTRING, POLYGON and all the MULTI* variants + GEOMETRYCOLLECTION). Which, however could also help to structure the internals of mapedit more concisely.
    (kr) not sure I understand. Do you mean everything should be converted to sf?
    (ta) maybe it is I who doesn't understand. Let me wait for the poc...

  • can we come up with a general way of providing a editAttributes method that works for both plain table data as well as sf class data? Maybe some sort of edit attributes button in editMap/editFeatures to switch to editing-attributes-mode?

  • how do we layout? I.e. can we come up with a useful pane viewer layout to accommodate enough space for both map and table?
    (kr) will be tricky, since ui will hide much of the map. I wonder if the ui should exist to the side of the map.

As I said, these just popped into my head when thinking about this a little.
In any case, a draft poc would be fabulous to have some solid example case to progress from.

I very much like this idea, especially as it is something that standard GIS systems usually don't provide in an easy fashion (if at all),

@timelyportfolio
Copy link
Contributor

timelyportfolio commented Jul 22, 2017

@tim-salabim, I commented inline above. Thanks for listing out your thoughts. I will try to create the quickest poc I can to stimulate additional thought. http://geojson.io will be a good reference point and also

image

@tim-salabim
Copy link
Member

Also added some comments inline above

@timelyportfolio
Copy link
Contributor

timelyportfolio commented Jul 22, 2017

Thanks @tiernanmartin and @tim-salabim. See if this rough poc is headed in the right direction, but I just realized in this implementation if you draw first (likely) then successive edit and/or delete will be ignored. Will make the quick fix if I get positive feedback. Also, in this case, we will most likely want to turn off multiple mode in Leaflet.draw.

library(leaflet)
library(mapview)
library(mapedit)
library(sf)
library(DT)
library(shiny)

make_an_sf <- function(dat) {
  ui <- fluidPage(
    fluidRow(
      column(6,DT::dataTableOutput("tbl",width="100%", height="400px")),
      column(6,editModUI("map"))
    ),
    fluidRow(actionButton("donebtn", "Done"))
  )
  
  server <- function(input, output, session) {
    data_copy <- st_as_sf(
      dat,
      geometry = st_sfc(lapply(seq_len(nrow(dat)),function(i){st_point()}))
    )
    
    edits <- callModule(
      editMod,
      leafmap = mapview()@map,
      id = "map"
    )
    output$tbl <- DT::renderDataTable({
      DT::datatable(
        dat,
        options = list(scrollY="400px"),
        # could support multi but do single for now
        selection = "single"
      )
    })
    
    # unfortunately I did not implement last functionality
    #  for editMap, so do it the hard way
    # last seems useful, so I might circle back and add that
    EVT_DRAW <- "map_draw_new_feature"
    EVT_EDIT <- "map_draw_edited_features"
    EVT_DELETE <- "map_draw_deleted_features"
    
    nsm <- function(event="", id="map") {
      paste0(session$ns(id), "-", event)
    }
    
    observe({
      possible <- list(
        draw = input[[nsm(EVT_DRAW)]],
        edit = input[[nsm(EVT_EDIT)]],
        delete = input[[nsm(EVT_DELETE)]]
      )
      
      # get last event
      last <- Filter(Negate(is.null), possible)
      # really dislike that we need to do in R
      pos <- Position(Negate(is.null), possible)
      
      # get selected row
      selected <- isolate(input$tbl_rows_selected)
      
      skip = FALSE
      # ignore if selected is null
      #  not great but good enough for poc
      if(is.null(selected)) {skip = TRUE}
      
      # ignore if no event
      if(length(last) == 0) {skip = TRUE}
  
      # replace if draw or edit
      if(skip==FALSE && (names(possible)[pos] %in% c("edit","draw"))) {
        
        sf::st_geometry(data_copy[selected,]) <<- sf::st_geometry(
          mapedit:::st_as_sfc.geo_list(unname(last)[[1]])
        )
      }
      
      # remove if delete
      if(skip==FALSE && (names(possible)[pos] %in% c("delete"))) {
        sf::st_geometry(data_copy[selected,]) <<- sf::st_sfc(st_point())
      }
    })
    
    # provide mechanism to return after all done
    observeEvent(input$donebtn, {
      # convert to sf
      
      stopApp(st_sf(data_copy,crs=4326))
    })
  }
  
  return(runApp(shinyApp(ui,server)))
}


# let's act like breweries does not have geometries
brewsub <- breweries[,1:4,drop=TRUE]

brewpub <- make_an_sf(brewsub)

mapview(brewpub)

mapedit_poc

@timelyportfolio
Copy link
Contributor

timelyportfolio commented Jul 22, 2017

One other idea would be we could use selectMap to join existing features to a data.frame. So select feature and select row to join. Return object would be sf with joined data and features.

@tiernanmartin
Copy link
Author

@timelyportfolio It's hard for me to imagine a more perfect poc - your gif illustrates the exact workflow I had in mind! Really nice stuff 👍

It seems that this implementation doesn't allow users to create MULTI*/GEOMETRY COLLECTION geometries - is that correct?

@timelyportfolio
Copy link
Contributor

timelyportfolio commented Jul 22, 2017

@tiernanmartin oh good! I did not test with MULTI so not surpised if not working. I will play a little to see if I can get it to work, but the issue here is Leaflet.draw does not accumulate draw events, so each completed feature will be sent. To achieve multi, we would need to add some other UI to know when grouping should occur. edit and delete though are accumulated and sent as collection. I will also make change so edit and delete will work.

@timelyportfolio
Copy link
Contributor

timelyportfolio commented Jul 22, 2017

actually I am wrong. In multiple mode, the feature is returned as a collection, so might be relatively easy I think to add multi to draw. actually wrong each draw is separate

@timelyportfolio
Copy link
Contributor

timelyportfolio commented Jul 22, 2017

Ok, this allows draw, edit, delete, but I don't know a good way to easily handle multiple edit, deletes. It can be done but requires much more complicated logic. Also need to add zoom_to functionality on datatable select if a feature exists on the selected row, but that also requires more work :).

library(leaflet)
library(mapview)
library(mapedit)
library(sf)
library(DT)
library(shiny)

make_an_sf <- function(dat) {
  ui <- fluidPage(
    fluidRow(
      column(6,DT::dataTableOutput("tbl",width="100%", height="400px")),
      column(6,editModUI("map"))
    ),
    fluidRow(actionButton("donebtn", "Done"))
  )
  
  server <- function(input, output, session) {
    data_copy <- st_as_sf(
      dat,
      geometry = st_sfc(lapply(seq_len(nrow(dat)),function(i){st_point()}))
    )
    
    edits <- callModule(
      editMod,
      leafmap = mapview()@map,
      id = "map"
    )
    output$tbl <- DT::renderDataTable({
      DT::datatable(
        dat,
        options = list(scrollY="400px"),
        # could support multi but do single for now
        selection = "single"
      )
    })
    
    # unfortunately I did not implement last functionality
    #  for editMap, so do it the hard way
    # last seems useful, so I might circle back and add that
    EVT_DRAW <- "map_draw_new_feature"
    EVT_EDIT <- "map_draw_edited_features"
    EVT_DELETE <- "map_draw_deleted_features"
    
    nsm <- function(event="", id="map") {
      paste0(session$ns(id), "-", event)
    }
    
    addObserve <- function(event) {
      observeEvent(
        input[[nsm(event)]],
        {
          evt <- input[[nsm(event)]]
          # for now if edit, just consider, first feature
          #   of the FeatureCollection
          if(event == EVT_EDIT) {
            evt <- evt$features[[1]]
          }
          
          # get selected row
          selected <- isolate(input$tbl_rows_selected)
          
          skip = FALSE
          # ignore if selected is null
          #  not great but good enough for poc
          if(is.null(selected)) {skip = TRUE}
          
          # ignore if no event
          #if(length(evt) == 0) {skip = TRUE}
print(evt)      
          # replace if draw or edit
          if(skip==FALSE) {
            sf::st_geometry(data_copy[selected,]) <<- sf::st_geometry(
              mapedit:::st_as_sfc.geo_list(evt)
            )
          }
      })
    }
    
    addObserve(EVT_DRAW)
    addObserve(EVT_EDIT)
    
    observeEvent(
      input[[nsm(EVT_DELETE)]],
      {
        evt <- input[[nsm(EVT_DELETE)]]
        # get selected row
        selected <- isolate(input$tbl_rows_selected)
        
        skip = FALSE
        # ignore if selected is null
        #  not great but good enough for poc
        if(is.null(selected)) {skip = TRUE}
        
        # ignore if no event
        #if(length(last) == 0) {skip = TRUE}

        # remove if delete
        if(skip==FALSE) {
          sf::st_geometry(data_copy[selected,]) <<- st_geometry(sf::st_sfc(st_point()))
        }
      }
    )
    
    # provide mechanism to return after all done
    observeEvent(input$donebtn, {
      # convert to sf
      
      stopApp(st_sf(data_copy,crs=4326))
    })
  }
  
  return(runApp(shinyApp(ui,server)))
}


# let's act like breweries does not have geometries
brewsub <- breweries[,1:4,drop=TRUE]

brewpub <- make_an_sf(brewsub)

mapview(brewpub)

@tim-salabim
Copy link
Member

ideally, zoom_to + highlight

timelyportfolio added a commit to timelyportfolio/mapedit that referenced this issue Jul 23, 2017
@timelyportfolio
Copy link
Contributor

timelyportfolio commented Jul 23, 2017

Rather than copy/pasting with each iteration, I saved the code here so we can track changes. I was able to add zoom on datatable selected row.

Also, I think I have an idea for MULTI, but I need to do some testing.

@tim-salabim, I think lessons learned in this exercise will be very useful, but I am not sure how far we shoudl push this poc. Zoom/highlight will likely be necessary for edit attributes functionality.

mapedit_poc2

@tim-salabim
Copy link
Member

@timelyportfolio this is coming along nicely I think! The zoom doesn't seem to work for points as, I guess, map.fitBounds(map._layers[layerid].getBounds()); doesn't find any bounds for 0 dimensional data.
Additionally, I am wondering whether a vertically split layout with 2 3rds map and 1 3rd DT would be better? Given that we work one row at a time in this setup, I feel that the table doesn't need so much space.

@mdsumner
Copy link
Member

Wow, this is awesome.

@lbusett
Copy link
Contributor

lbusett commented Nov 6, 2017

Hi. I just wish to add that this would be great functionality for the remote sensing community also. For example, it would allow selecting Areas of Interest over a satellite image on zones with different characteristics (e.g., different land use), to be used for example for plotting purposes (e.g., plotting time series), or as training / testing data for raster classification. These are common tasks, which are currently accomplished using QGIS, ENVI, or other RS software.

Any plans to develop it further? (sorry I can't help but I am not fluent (yet) with leaflet/shiny).

@tim-salabim
Copy link
Member

@lbusett good point! This will be developed further, no doubt. We are currently not pushing mapedit development too much as we would like to wait until leaflet has been upgraded to use leafletjs 1.x (there's a lot of changes and a lot of new goodies). Nonetheless, I think it would be useful to have a working basic implementation of this functionality for users to test and report back so we can get a feel of what the expectations for such a tool are in real life.
Also note, that direct querying of raster data will likely not happen until package stars has been developed further so that we can query using sf (which is the infrastructure used by mapedit). Until then you will be restricted to use the raster imagery as a background map and do the extract yourself after digitizing.

@lbusett
Copy link
Contributor

lbusett commented Nov 6, 2017

Thanks for the reply. Looking forward for this!

PS: I'm aware of the issue about not being able to "query" the raster data. Indeed, I recently developed some routines for "efficient" raster extraction starting from sf objects within a "spatial processing wrappers package" I'm currently developing.
Though the package is not yet publicly released - and is intended mostly as a "facilitator" for R-based analysis within my group, thus often replicating functionality of other packages with just a modified syntax - , maybe they can be of interest (at least until "stars" comes around):

https://lbusett.github.io/sprawl/reference/extract_rast.html
https://lbusett.github.io/sprawl/articles/articles/extract_rast_example.html

@lbusett
Copy link
Contributor

lbusett commented Nov 20, 2017

Somehow related to this, a very useful feature could also to be able to edit the attribute table of a vector when editing it using "editFeature".

For example: consider I want to "split" an existing polygon into multiple polygons, thus creating new geometries, and that I have an "ID" column in the original vector's attributes table. It would be nice to be able to interactively assing a new "ID" to the newly created polygons before returning the updated object to "R".

I recon this may be complex, and maybe calls for a dedicated shiny/lealflet app, but I think it's a "use case" worth considering.

@timelyportfolio
Copy link
Contributor

@lbusett, thanks, and yes we plan to add attribute editing soon. As of now, we are waiting on Leaflet > 1. Hopefully, we will be back to work on this soon.

@mrjoh3
Copy link
Contributor

mrjoh3 commented Oct 11, 2018

I am coming to this discussion a bit late, but it is exactly the workflow I have been looking for.

Lately I have also been integrating editable DT (rstudio/DT#480) in some of my other shiny apps. I think integrating an editable DT with the app example does make the attributes editable.

I have created a pull request (fb2f63a) for the example but basically you just create a dataTableProxy take the value edits and apply them to the underlying data.frame.

    # update table with entered notes
    proxy = dataTableProxy('tbl')

    observeEvent(input$tbl_cell_edit, {

      info = input$tbl_cell_edit

      str(info)

      i = info$row
      j = info$col
      v = info$value

      info$value <- as.character(info$value)

      data_copy[i, j] <<- DT::coerceValue(v, data_copy[i, j])
      replaceData(proxy, data_copy, resetPaging = FALSE)  # important

    })

@lbusett and @timelyportfolio I am curious if this is the functionality you were after and if this experiment had progressed any further?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants