[feature request] select-able table of non-spatial data for editMap
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).
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.
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
sfcould 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 insf. What other options do we have? (ta) I thinkst_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 onlyPOLYGONfeatures, we should also provide emptyPOLYGONs for the rows without added geometries. -
I think we need to have methods to convert drawn leaflet geojson objects to sf class
sfg/XYas inst_point(c(1, 2))which comprise the set of simplest geometry classes in sf (e.g.POINT,LINESTRING,POLYGONand all theMULTI*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 tosf? (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
editAttributesmethod that works for both plain table data as well as sf class data? Maybe some sort ofedit attributesbutton ineditMap/editFeaturesto 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),
@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

Also added some comments inline above
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)

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.
@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?
@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.
~~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
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)
ideally, zoom_to + highlight
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.

@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.
Wow, this is awesome.
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).
@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.
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
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.
@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.
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 (https://github.com/rstudio/DT/pull/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 (https://github.com/r-spatial/mapedit/pull/81/commits/fb2f63af6f507682b8ba8e192811695923f27b2a) 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?