R/shiny_video_sync_server.R

Defines functions ov_shiny_video_sync_server

## not exported
ov_shiny_video_sync_server <- function(app_data) {
    function(input, output, session) {
        auto_playlist_updates <- TRUE ## temporary while testing - if TRUE allow the playslist table to update automatically (i.e. normal reactive behaviour, which gives slightly simpler code). If FALSE then control these updates manually, which might help avoid unnecessary redraws
        reactive_scrolling <- FALSE ## testing, not sure it helps. In principle if multiple scroll requests get lined up before the first has actually been initiated, then it'll skip to just the last
        styling <- list(h_court_colour = "#bfefff", ## lightblue1
                        h_court_highlight = "darkblue",
                        v_court_colour = "#bcee68", ## darkolivegreen2
                        v_court_highlight = "darkgreen")

        rdata <- reactiveValues(dvw = app_data$dvw)
        tag_data <- reactiveValues(events = tibble(tag_video_time = numeric(), tag = character()))
        editing <- reactiveValues(active = NULL)
        video_state <- reactiveValues(paused = FALSE)
        dv_read_args <- app_data$dv_read_args
        done_first_playlist_render <- FALSE
        running_locally <- !nzchar(Sys.getenv("SHINY_PORT"))
        debug <- 0L
        plays_cols_to_show <- c("error_icon", "clock_time", "video_time", "set_number", "code", "home_setter_position", "visiting_setter_position", "phase_type", "Score", "is_skill")
        plays_col_renames <- c(Set = "set_number", hs = "home_setter_position", as = "visiting_setter_position")
        is_skill <- function(z) !is.na(z) & (!z %in% c("Timeout", "Technical timeout", "Substitution"))
        no_set_attacks <- c("PR", "PP", "P2") ## attacks that don't need a set inserted before them
        default_set_evaluation <- "+" ## for inserted sets
        code_bits_tbl <- dplyr::tribble(~bit, ~width,
                                        "team", 1,
                                        "number", 2,
                                        "skill", 1,
                                        "type", 1,
                                        "eval", 1,
                                        "combo", 2,
                                        "target", 1,
                                        "start_zone", 1,
                                        "end_zone", 1,
                                        "end_subzone", 1,
                                        "skill_type", 1,
                                        "num_players", 1,
                                        "special", 1,
                                        "custom", 5)

        lineup_bits_tbl <- dplyr::tribble(~bit, ~width,
                                        "P1", 2,
                                        "P2", 2,
                                        "P3", 2,
                                        "P4", 2,
                                        "P5", 2,
                                        "P6", 2)

        sub_bits_tbl <- dplyr::tribble(~bit, ~width,
                                       "OUT", 2,
                                       "IN", 2)

        ## some local markup to make the helper entries easier here
        ## | = <br />
        ## {thing} = <strong>thing</strong>
        ## [thing] = <span class=\"clet\">thing</span>
        ## (thing) = <em>thing</em>
        ## --- = <br /><hr />
        gsubf <- function(...) gsub(..., fixed = TRUE)
        mu2html <- function(z) gsubf("[", "<span class=\"clet\">", gsubf("]", "</span>", gsubf("{", "<strong>", gsubf("}", "</strong>", gsubf("|", "<br />", gsubf("(", "<em>", gsubf(")", "</em>", gsubf("---", "<br /><hr />", z))))))))
        paste0_noNA <- function(...) do.call(paste0, Filter(Negate(is.na), list(...)))
        special_helper <- function(skill, evaln) {
            htext <- NA_character_
            if (!is.null(skill) && !is.null(evaln)) {
                htext <- case_when(skill %eq% "A" & evaln %eq% "#" ~ "(Attk kill)|Blk out [S]ide|Blk out l[O]ng|Blk on [F]loor|[X] Direct|on floor",
                                   skill %eq% "A" & evaln %eq% "=" ~ "(Attk err)|Out [S]ide|Out l[O]ng|In [N]et|[I] net contct|[A]ntenna|[Z] ref call",
                                   skill %eq% "A" ~ "(Attk)|blk [C]ontrol|[N] let",
                                   skill %eq% "B" & evaln %in% c("=", "/") ~ "(Blk err)|Out [S]ide|Out l[O]ng|Ball on [F]lr|[X] between|hands|[N] net touch|[A]ntenna|[P] no jump|[T] pos error|[Z] ref call",
                                   skill %eq% "R" ~ "(Rcv)|[U]nplayable|[X] body err|[P]os err|No [E]ffort|[Z] ref call",
                                   skill %eq% "S" & evaln %eq% "#" ~ "(Srv ace)|[N] let",
                                   skill %eq% "S" & evaln %eq% "=" ~ "(Srv err)|Out l[O]ng|Out [L]eft|Out [R]ight|In [N]et|[Z] ref call",
                                   skill %eq% "S" ~ "(Srv)|[N] let",
                                   skill %eq% "E" & evaln %eq% "=" ~ "(Set err)|[U]nhittable|[I] net tch|[Z] ref call",
                                   skill %eq% "Dig" & evaln %eq% "=" ~ "(Dig err)|[U]nplayable|[X] body err|[P]os err|[Z] Ref call|Ball on [F]lr|Ball [O]ut|No [E]ffort",
                                   skill %eq% "Freeball" & evaln %eq% "=" ~ "(Fr err)|[U]nplayable|[X] body err|[P]os err|[Z] Ref call")
            }
            mu2html(paste0_noNA("{Special}---", htext))
        }
        skill_type_helper <- function(skill, evaln) {
            htext <- NA_character_
            if (!is.null(skill)) {
                htext <- case_when(skill %eq% "A" ~ "(Attk)|[H]ard|[P] soft|[T]ip",
                                   skill %eq% "R" ~ "(Rec)|[L]eft|[R]ight|lo[W]|[O]vrhnd|[M]idline",
                                   skill %eq% "E" ~ "(Set)|[1] hand|[2] hands|[3] bump|[4] othr|[5] uhand",
                                   skill %eq% "D" ~ "(Dig)|[S] on spk|[C] spk|cover|[B] aftr|block|[E] emerg")
            }
            mu2html(paste0_noNA("{Skill|type}---", htext))
        }
        num_players_helper <- function(skill, evaln) {
            htext <- NA_character_
            if (!is.null(skill)) {
                htext <- case_when(skill %in% c("A", "B") ~ "(Attk|Blk)|[0]..[3]|[4] hole|block",
                                   skill %eq% "R" ~ "(Rcv)|[1] 2p,L|[2] 2p,R|[3] 3p,L|[4] 3p,M|[5] 3p,R|[6] 4p,L|[7] 4p,LC|[8] 4p,RC|[9] 4p,R")
            }
            mu2html(paste0_noNA("{Num|plyrs}---", htext))
        }
        end_zone_helper <- function(skill, evaln) {
            if (!is.null(skill) && skill %eq% "A" && rdata$dvw$meta$match$zones_or_cones %eq% "C") {
                mu2html("{End cone}|(Attk)|[1..8]")
            } else {
                mu2html("{End zone}---[1..9]")
            }
        }
        get_src_type <- function(src) {
            type <- "local"
            if (is_youtube_url(src)) {
                type <- "youtube"
            } else if (!is_url(src)) {
                src <- file.path(app_data$dvw$meta$video$file)
            }
            list(src = src, type = type)
        }
        code_bits_tbl$helper <- c(mu2html("{Team}---[*]&nbsp;H|[a]&nbsp;V"), ## team
                                  mu2html("{Plyr|num}"), ## number
                                  mu2html("{Skill}---[S]rv|[R]ec|[A]ttk|[B]lk|[D]ig|s[E]t|[F]reeb"), ## skill
                                  mu2html("{Tempo}---[H]igh|[M]ed|[Q]uick|[T]ense|s[U]per|[N] fast|[O]ther"), ## type
                                  mu2html("{Eval}---[#|+|!|-|/|=]"), ## eval
                                  mu2html("{Combo}---(Atk code)|[X.]|[C.]|etc||(Set call)|[K.]"), ## combo
                                  mu2html("{Target}---[F]ront|[C]ntr|[B]ack|[P]ipe|[S]etr"), ## target
                                  mu2html("{Start|zone}---(Attk)|[1..9]||(Srv)|[57691]"), ##start_zone
                                  end_zone_helper, ##end_zone
                                  mu2html("{End|subzn}---[ABCD]"), ##end_subzone
                                  skill_type_helper, ##skill_type
                                  num_players_helper, ##players
                                  special_helper, ##special
                                  mu2html("{Custom}---")) ##custom
        ## note that if any other helpers are turned into functions, they need extra code added below to handle them (see ADD HANDLERS HERE)
        code_bits_tbl$start <- cumsum(lag(code_bits_tbl$width, default = 0))+1L
        code_bits_tbl$end <- code_bits_tbl$start+code_bits_tbl$width-1L

        output$vtdp_ui <- renderUI({
            if (input$video_time_decimal_places > 0) {
                tags$div(class = "alert alert-danger", "Note: files with non-integer video times may not be openable in DataVolley")
            } else {
                NULL
            }
        })

        ## court inset showing rotation and team lists
        court_inset <- callModule(mod_courtrot, id = "courtrot", rdata = rdata, rowidx = reactive(playslist_current_row()), styling = styling)
        rotateTeams <- reactive(court_inset$rt)
        accept_ball_coords <- court_inset$accept_ball_coords ## the "accept" button

        observe({
            if (nrow(court_inset$click_points$queue) > 1 && !is.null(playslist_current_row()) && !is.na(playslist_current_row())) {
                js_show2("courtrot-validate_ball_coords")
                js_show2("courtrot-cancel_ball_coords")
            } else {
                js_hide2("courtrot-validate_ball_coords")
                js_hide2("courtrot-cancel_ball_coords")
            }
        })

        observe({
            rtn <- rotateTeams()
            if (rtn$home > 0) {
                home_force_rotate()
                rtn$home <- 0L
            }
            if (rtn$visiting > 0) {
                visiting_force_rotate()
                rtn$visiting <- 0L
            }
        })

        observeEvent(accept_ball_coords(), {
            if (accept_ball_coords() > 0) { ## ignore the initial triggering of this on app startup
                ridx <- playslist_current_row()
                do_reparse = FALSE
                if (!is.null(ridx) && !is.na(ridx)) {
                    if (nrow(court_inset$click_points$queue) >= 2) {
                        ## if we have at least two points, then we can add coordinates
                        do_reparse <- TRUE
                        ## start coordinate
                        thisxy <- court_inset$click_points$queue[1, ]
                        rdata$dvw$plays$start_coordinate_x[ridx] <- thisxy$x
                        rdata$dvw$plays$start_coordinate_y[ridx] <- thisxy$y
                        if (is.na(thisxy$x)) {
                            rdata$dvw$plays$start_coordinate[ridx] <- NA
                        } else {
                            rdata$dvw$plays$start_coordinate[ridx] <- datavolley::dv_xy2index(thisxy)
                        }
                        ## mid coordinate, which will be missing if we only clicked two points
                        thisxy <- if (nrow(court_inset$click_points$queue) > 2) court_inset$click_points$queue[2, ] else data.frame(x = NA_real_, y = NA_real_)
                        rdata$dvw$plays$mid_coordinate_x[ridx] <- thisxy$x
                        rdata$dvw$plays$mid_coordinate_y[ridx] <- thisxy$y
                        if (is.na(thisxy$x)) {
                            rdata$dvw$plays$mid_coordinate[ridx] <- NA
                        } else {
                            rdata$dvw$plays$mid_coordinate[ridx] <- datavolley::dv_xy2index(thisxy)
                        }
                        ## end coordinate
                        thisxy <- if (nrow(court_inset$click_points$queue) > 2) court_inset$click_points$queue[3, ] else court_inset$click_points$queue[2, ]
                        rdata$dvw$plays$end_coordinate_x[ridx] <- thisxy$x
                        rdata$dvw$plays$end_coordinate_y[ridx] <- thisxy$y
                        if (is.na(thisxy$x)) {
                            rdata$dvw$plays$end_coordinate[ridx] <- NA
                        } else {
                            rdata$dvw$plays$end_coordinate[ridx] <- datavolley::dv_xy2index(thisxy)
                        }
                    }
                }
                if (do_reparse) {
                    playslist_needs_scroll(TRUE)
                    if (!auto_playlist_updates) replace_playlist_data()
                }
                ## and clear the clicked coordinates queue
                court_inset$clear_click_queue()
                editing$active <- NULL
            }
        })

        observeEvent(input$show_shortcuts, {
            showModal(modalDialog(title = "Keyboard shortcuts", easyClose = TRUE, size = "l",
                                  if (app_data$with_video) tagList(tags$p(tags$strong("Video controls")), tags$ul(tags$li("[l or 6] forward 2s, [; or ^] forward 10s, [m or 3] forwards 0.1s, [, or 9] forwards 1 frame"), tags$li("[j or 4] backward 2s, [h or $] backward 10s, [n or 1] backwards 0.1s, [b or 7] backwards 1 frame"), tags$li("[q or 0] pause video"), tags$li("[g or #] go to currently-selected event"))),
                                  fluidRow(column(6, tags$strong("Keyboard controls"),
                                           tags$ul(tags$li("[r or 5] sync selected event video time"),
                                                   tags$li("[i or 8] move to previous skill row"),
                                                   tags$li("[k or 2] move to next skill row"),
                                                   tags$li("[e or E] edit current code"),
                                                   tags$li("[del] delete current code"),
                                                   tags$li("[ins] insert new code above current"),
                                                   tags$li("[Shift-ins] insert new code below current"),
                                                   tags$li("[F1] home team rotate +1"),
                                                   tags$li("[F2] insert setting codes before every attack"),
                                                   tags$li("[F4] delete all setting codes (except errors)"),
                                                   tags$li("[F6] insert digging codes after every attack"),
                                                   tags$li("[F8] delete all digging codes"),
                                                   tags$li("[F10] visiting team rotate +1"),
                                                   )),
                                           column(6, if (app_data$with_video) tagList(tags$strong("Tagging"), tags$ul(tags$li("[left-click the court inset then press 't'] add a tag with the clicked court location. Alternatively, the location can be entered by left-clicking the video, if the court reference data has been provided"),
                                                                                     tags$li("[T] open the tag manager (download or clear tag data)"))),
                                                  tags$strong("Ball coordinates"), tags$ul(tags$li("[left-click the court inset] register the start/mid/end ball positions"),
                                                                                           tags$li("[accept ball coordinates] to add coordinates to the currently selected item"))))
                                  ))
        })

        observeEvent(input$all_video_from_clock, {
            current_video_time <- selected_event()$video_time
            current_clock_time <- selected_event()$time
            all_clock_times <- rdata$dvw$plays$time
            current_is_no_good <- is.null(current_video_time) || is.na(current_video_time) || is.null(current_clock_time) || is.na(current_clock_time)
            showModal(modalDialog(
                title = "Video times from clock times",
                easyClose = TRUE, size = "l",
                if (all(is.na(all_clock_times))) {
                    tags$div(class = "alert alert-danger", "Your file has no clock times, so this tool can't do anything.")
                } else {
                    tags$div(tags$h4("Options:"),
                             fluidRow(column(8, tags$strong("Set missing video times"), "of events based on their clock times, and the video and clock time of the currently-selected event."),
                                      column(4, if (current_is_no_good) tags$div(class = "alert alert-danger", "The currently-selected event needs to have a video time AND clock time set before using this option.") else actionButton("infer_missing_video_from_current", label = tags$span("Infer MISSING video times", tags$br(), "relative to the current event")))),
                             tags$hr(),
                             fluidRow(column(8, tags$strong("Set the video times of ALL events"), "based on their clock times, and the video and clock time of the currently-selected event. This applies to ALL events, whether they are missing their video time or not."),
                                      column(4, if (current_is_no_good) tags$div(class = "alert alert-danger", "The currently-selected event needs to have a video time AND clock time set before using this option.") else actionButton("infer_all_video_from_current", label = tags$span("Infer ALL video times", tags$br(), "relative to the current event")))),
                             tags$hr(),
                             fluidRow(column(8, tags$strong("Set missing video times"), "of events based on their clock times, and the video and clock time of surrounding events."),
                                      column(4, "Not implemented yet.")##actionButton("infer_missing_video_from_surrounding", label = tags$span("Infer MISSING video times", tags$br(), "relative to surrounding events")))
                                      ),
                             tags$hr(),
                             fluidRow(column(8, tags$strong("Set the clock time"), "of the currently-selected event."),
                                      column(4, actionButton("set_selected_clock_time", label = tags$span("Set clock time of", tags$br(), "selected event.")))),
                             tags$hr(),
                             fluidRow(column(8, tags$strong("Set missing clock times"), "of events based on their video times, and the video and clock time of the currently-selected event."),
                                      column(4, if (current_is_no_good) tags$div(class = "alert alert-danger", "The currently-selected event needs to have a video time AND clock time set before using this option.") else actionButton("infer_missing_clock_from_current", label = tags$span("Infer MISSING clock times", tags$br(), "relative to the current event")))),
                             tags$hr(),
                             fluidRow(column(8, tags$strong("Set the clock times of ALL events"), "based on their video times, and the video and clock time of the currently-selected event. This applies to ALL events, whether they are missing their clock time or not."),
                                      column(4, if (current_is_no_good) tags$div(class = "alert alert-danger", "The currently-selected event needs to have a video time AND clock time set before using this option.") else actionButton("infer_all_clock_from_current", label = tags$span("Infer ALL clock times", tags$br(), "relative to the current event"))))
                             )
                }
            ))
        })

        ## video/clock time sync functions
        observe({
            if (isTruthy(input$infer_all_video_from_current) || isTruthy(input$infer_missing_video_from_current)) {
                isolate({
                    removeModal()
                    this_clock_time <- selected_event()$time
                    this_video_time <- selected_event()$video_time
                    if (is.null(this_clock_time) || is.na(this_clock_time) || is.null(this_video_time) || is.na(this_video_time)) {
                        stop("selected event is missing video or clock time")
                    }
                    clock_time_diff <- difftime(rdata$dvw$plays$time, this_clock_time, units = "secs")
                    midx <- if (isTruthy(input$infer_all_video_from_current)) rep(TRUE, nrow(rdata$dvw$plays)) else is.na(rdata$dvw$plays$video_time)
                    new_video_time <- this_video_time + clock_time_diff[midx]
                    ## Only update video time of events happening after current event
                    cidx <- which(clock_time_diff[midx] >= 0)
                    rdata$dvw$plays$video_time[midx][cidx] <- round(new_video_time[cidx], digits = input$video_time_decimal_places)
                    playslist_needs_scroll(TRUE)
                    if (!auto_playlist_updates) replace_playlist_data()
                })
            }
        })
        observe({
            if (isTruthy(input$infer_all_clock_from_current) || isTruthy(input$infer_missing_clock_from_current)) {
                isolate({
                    removeModal()
                    this_time <- selected_event()$time
                    this_video_time <- selected_event()$video_time
                    if (is.null(this_time) || is.na(this_time) || is.null(this_video_time) || is.na(this_video_time)) {
                        stop("selected event is missing video or clock time")
                    }
                    video_time_diff <- rdata$dvw$plays$video_time - this_video_time
                    midx <- if (isTruthy(input$infer_all_clock_from_current)) rep(TRUE, nrow(rdata$dvw$plays)) else is.na(rdata$dvw$plays$time)
                    new_clock_time <- this_time + video_time_diff[midx]
                    # Only update clock time of events happening after current event
                    cidx <- which(video_time_diff[midx] >= 0)
                    rdata$dvw$plays$time[midx][cidx] <- new_clock_time[cidx]
                    playslist_needs_scroll(TRUE)
                    if (!auto_playlist_updates) replace_playlist_data()
                })
            }
        })

        observeEvent(input$set_selected_clock_time, {
            removeModal()
            if (is.null(selected_event())) {
                showModal(modalDialog(title = "Error", tags$div(class = "alert alert-danger", "No event selected.")))
            } else {
                showModal(modalDialog(
                    title = "Set clock time of selected event",
                    easyClose = TRUE, size = "l",
                    tags$div(shinyTime::timeInput("selected_clocktime", label = "Time:", value = if (!is.na(selected_event()$time)) selected_event()$time else NULL), actionButton("do_set_clocktime", "Set time"))
                ))
            }
        })
        observeEvent(input$do_set_clocktime, {
            removeModal()
            ridx <- playslist_current_row()
            if (!is.null(ridx) && !is.na(ridx)) {
                ##cat("x time: "); cat(str(rdata$dvw$plays$time))
                tm <- input$selected_clocktime
                ##cat("time:"); cat(str(tm))
                ##cat("Original time:"); cat(str(rdata$dvw$plays$time[ridx]))
                tm = as.POSIXct(paste(format(rdata$dvw$plays$time[ridx], "%Y-%m-%d"), format(tm, "%H:%M:%S")))
                if (inherits(rdata$dvw$plays$time[ridx], "POSIXct")) tm <- as.POSIXct(tm, tz = lubridate::tz(rdata$dvw$plays$time[ridx]))
                ##cat("time cast:"); cat(str(tm))
                rdata$dvw$plays$time[ridx] <- tm
                ##cat("rdata time:"); cat(str(rdata$dvw$plays$time[ridx]))
                playslist_needs_scroll(TRUE)
                if (!auto_playlist_updates) replace_playlist_data()
            }
        })

        ## sync the selected event to the current video time
        sync_single_video_time <- function() {
            ridx <- playslist_current_row()
            if (!is.null(ridx)) {
                do_video("set_current_video_time", ridx)
            }
        }

        observeEvent(input$set_current_video_time, {
            temp <- strsplit(input$set_current_video_time, split = "&", fixed = TRUE)[[1]]
            ridx <- as.integer(temp[2])
            tm <- as.numeric(temp[1])
            if (!is.null(ridx) && !is.na(ridx) && ridx > 0 && ridx <= nrow(rdata$dvw$plays)) {
                tm <- if (input$video_time_decimal_places < 1) round(tm) else round(tm, digits = input$video_time_decimal_places)
                rdata$dvw$plays$video_time[ridx] <- tm
                rdata$dvw <- preprocess_dvw(rdata$dvw)
                skip <- 1
                if (ridx < nrow(rdata$dvw$plays) && ((rdata$dvw$plays$skill[ridx] %eq% "Attack" && rdata$dvw$plays$skill[ridx+1] %eq% "Block") || (rdata$dvw$plays$skill[ridx] %eq% "Serve" && rdata$dvw$plays$skill[ridx+1] %eq% "Reception"))) {
                    ## give the block the same time as the attack / reception the same time as the serve
                    rdata$dvw$plays$video_time[ridx+1] <- tm
                    skip <- 2
                } else if (rdata$dvw$plays$skill[ridx] %eq% "Attack" && ridx < nrow(rdata$dvw$plays) && rdata$dvw$plays$skill[ridx+1] %eq% "Dig") {
                    ## give the dig a +1s time
                    rdata$dvw$plays$video_time[ridx+1] <- tm+1
                    skip <- 2
                }
                ## advance to the next skill row
                if (ridx < nrow(rdata$dvw$plays)) {
                    next_skill_row <- find_next_skill_row(ridx, step = skip)
                    if (length(next_skill_row) > 0) {
                        ## update the current row
                        playslist_current_row(next_skill_row)
                    }
                }
                ## update the table data, and it will automatically trigger the scroll to the current row once it has finished drawing
                playslist_needs_scroll(TRUE)
                replace_playlist_data() ## always do this here, otherwise if a row is synced with the same existing time that it already has, it won't scroll (?)
            }
        })

        selected_event <- reactive({
            if (!is.null(playslist_current_row())) {
                rdata$dvw$plays[playslist_current_row(), ]
            } else {
                NULL
            }
        })

        output$current_event <- renderUI({
            tags$div(id = "currentevent", style = if (!is.null(input$vo_voffset) && as.numeric(input$vo_voffset) > 0) paste0("margin-top: ", input$vo_offset - 50, "px") else "", tags$strong("Current: "), selected_event()$code)
        })

        observe({
            ## parse on reload (without "error_message" in column names)
            if (!is.null(rdata$dvw) && nrow(rdata$dvw$plays) > 0 && !"error_message" %in% names(rdata$dvw$plays)) {
                rdata$dvw <- preprocess_dvw(rdata$dvw)
            }
        })

        plays_do_rename <- function(z) names_first_to_capital(dplyr::rename(z, plays_col_renames))
        ## the plays display in the RHS table
        output$playslist <- DT::renderDataTable({
            isolate(mydat <- rdata$dvw$plays) ## render once, then isolate from further renders - will be done by replaceData below
            if (!is.null(input$window_height) && !is.na(input$window_height)) {
                plh <- input$window_height*0.6
            } else {
                plh <- 200
            }
            if (!is.null(mydat)) {
                isolate({
                    first_skill_row <- find_next_skill_row(-1)
                    sel <- list(mode = "single")
                    if (length(first_skill_row) > 0) {
                        sel$target <- "row"
                        sel$selected <- first_skill_row
                    }
                })
                mydat$is_skill <- is_skill(mydat$skill)
                mydat$set_number <- as.factor(mydat$set_number)
                mydat$phase_type <- case_when(mydat$phase %eq% "Serve" ~ "S",
                                              mydat$phase %eq% "Reception" ~ "R",
                                              mydat$phase %eq% "Transition" ~ "T")
                mydat$Score <- paste(mydat$home_team_score, mydat$visiting_team_score, sep = "-")
                cols_to_hide <- which(plays_cols_to_show %in% c("is_skill"))-1 ## 0-based because no row names
                cnames <- names(plays_do_rename(mydat[1, plays_cols_to_show, drop = FALSE]))
                cnames[plays_cols_to_show == "error_icon"] <- ""
                out <- DT::datatable(mydat[, plays_cols_to_show, drop = FALSE], rownames = FALSE, colnames = cnames,
                                     extensions = "Scroller",
                                     escape = FALSE, filter = "top",
                                     selection = sel, options = list(scroller = TRUE,
                                                                     lengthChange = FALSE, sDom = '<"top">t<"bottom">rlp', paging = TRUE, "scrollY" = paste0(plh, "px"), ordering = FALSE, ##autoWidth = TRUE,
                                                                     columnDefs = list(list(targets = cols_to_hide, visible = FALSE)),
                                                                     drawCallback = DT::JS("function(settings) { Shiny.setInputValue('playlist_redrawn', new Date().getTime()); }")
                                                                     ##list(targets = 0, width = "20px")) ## does nothing
                                                                     ))
                out <- DT::formatStyle(out, "is_skill", target = "row", backgroundColor = DT::styleEqual(c(FALSE, TRUE), c("#f0f0e0", "lightgreen"))) ## colour skill rows green
                out <- DT::formatStyle(out, "error_icon", color = "red")
                out
            } else {
                NULL
            }
        }, server = TRUE)
        playslist_proxy <- DT::dataTableProxy("playslist")
        playslist_needs_scroll <- reactiveVal(FALSE)
        playslist_scroll_target <- reactiveVal(-99L)
        observeEvent(input$playlist_redrawn, {
            ## when the table has finished being drawn, scroll it if necessary
            if (playslist_needs_scroll()) {
                playslist_needs_scroll(FALSE)
                if (reactive_scrolling) playslist_scroll_target(playslist_current_row()) else scroll_playlist(playslist_current_row())
            }
            ## and mark current row as selected in the table, but don't re-scroll to it
            playslist_select_row(playslist_current_row(), scroll = FALSE)
        })

        ## keep track of selected playslist row as a reactiveVal
        ##   when updating e.g. video time, set this reactiveVal, then wait for DT to redraw THEN scroll
        playslist_current_row <- reactiveVal(NULL)
        ## the playslist_select_row function just changes the visible selection in the table, and optionally scrolls to it, but does not change playslist_current_row() value
        playslist_select_row <- function(rw, scroll = TRUE) {
            DT::selectRows(playslist_proxy, rw)
            if (isTRUE(scroll)) {
                if (reactive_scrolling) playslist_scroll_target(rw) else scroll_playlist(rw)
            }
        }
        ## when the user changes the selected row, update playslist_current_row
        observeEvent(input$playslist_rows_selected, playslist_current_row(input$playslist_rows_selected))

        observe({
            if (reactive_scrolling && !is.null(playslist_scroll_target()) && !is.na(playslist_scroll_target()) && playslist_scroll_target() > 0) {
                scroll_playlist(playslist_scroll_target())
            }
        })

        scroll_playlist <- function(rw) {
            if (!is.null(rw)) {
                ## scrolling works on the VISIBLE row index, so it depends on any column filters that might have been applied
                visible_rowidx <- which(input$playslist_rows_all == rw)
                scrollto <- max(visible_rowidx-1-5, 0) ## -1 for zero indexing, -5 to keep the selected row 5 from the top
                ##dojs(paste0("$('#playslist').find('.dataTable').DataTable().scroller.toPosition(", scrollto, ");")) ## with anim, laggy
                dojs(paste0("$('#playslist').find('.dataTable').DataTable().scroller.toPosition(", scrollto, ", false);")) ## no anim, faster

                ## using the jquery scrollTo extension, enable in UI
                ##stid <- paste0("$('#DataTables_Table_1 > tbody tr:nth-child(", scrollto+1, ")')")
                ##dojs(paste0("$('.dataTables_scrollBody').scrollTo(", stid, ");"))

                ## not tried yet https://github.com/rstudio/DT/issues/519
                ## table.DataTable().row([row_id]).scrollTo(); ## (without scroller?)

                ## other attempts
                ##dojs(sprintf("$('#playslist').find('.dataTable').DataTable().row(%s).node().scrollIntoView();", max(0, rdata$plays_row_to_select-1)))
                ##dojs(sprintf("console.dir($('#playslist').find('.dataTable').DataTable().row(%s).node())", max(0, rdata$plays_row_to_select-1)))
                ##dojs(sprintf("$('#playslist').find('.dataTables_scroll').animate({ scrollTop: $('#playslist').find('.dataTable').DataTable().row(%s).node().offsetTop }, 2000);", max(0, rdata$plays_row_to_select-1)))
            }
        }

        ## if auto_playlist_updates is TRUE
        observe({
            blah <- rdata$dvw
            if (auto_playlist_updates) replace_playlist_data()
        })
        ## replace_playlist_data is used if auto_playlist_updates is FALSE
        replace_playlist_data <- function() {
            mydat <- rdata$dvw$plays
            mydat$is_skill <- is_skill(mydat$skill)
            mydat$set_number <- as.factor(mydat$set_number)
            mydat$phase_type <- case_when(mydat$phase %eq% "Serve" ~ "S",
                                          mydat$phase %eq% "Reception" ~ "R",
                                          mydat$phase %eq% "Transition" ~ "T")
            mydat$Score <- paste(mydat$home_team_score, mydat$visiting_team_score, sep = "-")
            DT::replaceData(playslist_proxy, data = mydat[, plays_cols_to_show, drop = FALSE], rownames = FALSE, clearSelection = "none")
        }

        find_next_skill_row <- function(current_row_idx = NULL, step = 1, respect_filters = TRUE) {
            ## if respect_filters is TRUE, find the next row that is shown in the table (i.e. passing through any column filters that have been applied)
            ## if FALSE, just find the next skill row in the data, ignoring table filters
            if (is.null(current_row_idx)) current_row_idx <- playslist_current_row()
            skill_rows <- which(is_skill(rdata$dvw$plays$skill))
            if (respect_filters) skill_rows <- intersect(skill_rows, input$playslist_rows_all)
            next_skill_row <- skill_rows[skill_rows > current_row_idx]
            next_skill_row[min(step, length(next_skill_row))]
        }

        find_prev_skill_row <- function(current_row_idx = NULL, step = 1, respect_filters = TRUE) {
            ## if respect_filters is TRUE, find the previous row that is shown in the table (i.e. passing through any column filters that have been applied)
            ## if FALSE, just find the previous skill row in the data, ignoring table filters
            if (is.null(current_row_idx)) current_row_idx <- playslist_current_row()
            skill_rows <- which(is_skill(rdata$dvw$plays$skill))
            if (respect_filters) skill_rows <- intersect(skill_rows, input$playslist_rows_all)
            prev_skill_row <- rev(skill_rows[skill_rows < current_row_idx])
            prev_skill_row[min(step, length(prev_skill_row))]
        }

        output$error_message <- renderUI({
            if (is.null(selected_event()) || is.na(selected_event()$error_message)) {
                NULL
            } else {
                tags$div(class = "alert alert-danger", HTML(selected_event()$error_message))
            }
        })

        observeEvent(input$playback_rate, {
            if (!is.null(input$playback_rate)) do_video("playback_rate", input$playback_rate)
        })

        observeEvent(input$cmd, {
            if (!is.null(input$cmd)) {
                temp <- strsplit(input$cmd, "@")[[1]]
                ## elements are keyid element_class element_id cursor_position field_length time
                mycmd <- temp[1]
                myclass <- temp[2]
                if (!is.null(myclass) && nzchar(myclass) && myclass %in% c("form-control")) {
                    ## don't process these - they are e.g. key events in DT filter boxes
                    mycmd <- NULL
                }
                if (!is.null(mycmd)) {
                    ## mycmd comes in as a character representation of the ascii code like "65" or "32"
                    mykey <- intToUtf8(as.numeric(mycmd))
                    ## note that if cmdbox is an INPUT and focus is cmdbox then the document$keypress event doesn't get fired, because it gets grabbed by the cmdbox event handler
                    ignore_keys <- NULL ## placeholder for keys handled elsewhere in code (e.g. 37, 39 might not trigger here, may depend on browser)
                    if (debug > 1) cat("input: ", mycmd, "\n")
                    if (mycmd %in% ignore_keys) {
                        if (debug > 1) cat(" (ignored)")
                    } else if (!is.null(editing$active)) {
                        ## if editing is in progress, don't process the usual navigation etc keys
                        if (mycmd %eq% "13") {
                            ## if editing/tagging/inserting, treat as update
                            if (!editing$active %eq% "teams") code_make_change()
                            ## but not for team editing, because pressing enter in the DT fires this too
                        } else if (mycmd %eq% "27") {
                            ## not sure if this will be detected by keypress, maybe only keydown (may be browser specific)
                            ## esc
                            if (!editing$active %eq% "teams") {
                                editing$active <- NULL
                                removeModal()
                            }
                        }
                    } else {
                        ## editing not active
                        if (mycmd %in% utf8ToInt("eE")) {
                            ## open code editing dialog
                            edit_current_code()
                        } else if (mycmd %eq% "45") {
                            ## handled via input$controlkey
##                            ## insert new row below current
##                            insert_data_row()
                        } else if(mycmd %eq% "83") {
                            insert_sub()
                        } else if (mycmd %eq% "8") {
                            ## backspace
                        } else if (mycmd %eq% "46") {
                            ## delete key, handled via input$controlkey
                        } else if (mycmd %in% utf8ToInt("i8")) {
                            ## prev skill row
                            psr <- find_prev_skill_row()
                            if (length(psr) > 0) playslist_select_row(psr)
                        } else if (mycmd %in% utf8ToInt("k2")) {
                            ## next skill row
                            nsr <- find_next_skill_row()
                            if (length(nsr) > 0) playslist_select_row(nsr)
                        } else if (mycmd %in% utf8ToInt("qQ0")) { ## video navigation
                            do_video("toggle_pause")
                        } else if (mycmd %in% utf8ToInt("gG#")) {
                            ## video go to currently-selected event
                            ev <- selected_event()
                            if (!is.null(ev)) do_video("set_time", ev$video_time)
                        } else if (mycmd %in% utf8ToInt("nm13jhl;46$^b,79")) {
                            ## video forward/backward nav
                            vidcmd <- if (tolower(mykey) %in% c("1", "n", "h", "j", "4", "$", "b", "7")) "rew" else "ff"
                            dur <- if (tolower(mykey) %in% c("h", "$", ";", "^")) 10 else if (tolower(mykey) %in% c("n", "m", "1", "3")) 0.1 else if (tolower(mykey) %in% c("b", "7", ",", "9")) 1/30 else 2
                            do_video(vidcmd, dur)
                        } else if (mykey %in% c("r", "R", "5")) {
                            ## set the video time of the current event
                            if (app_data$with_video) sync_single_video_time()
                        } else if (mykey %eq% "t") {
                            ## tag event at current time
                            if (app_data$with_video) add_tagged_event()
                        } else if (mykey %eq% "T") {
                            ## pop up the tag manager dialog
                            tag_manager()
                        }
                    }
                    if (debug > 1) cat("\n")
                }
            }
        })
        observeEvent(input$controlkey, {
            ## keys that might not get detected by keypress but do by keydown?
            if (!is.null(input$controlkey)) {
                temp <- strsplit(input$controlkey, "@")[[1]]
                ## elements are modifiers_and_key element_class element_id cursor_position field_length time
                mycmd <- temp[1]
                myclass <- temp[2]
                myid <- temp[3]
                suppressWarnings({
                    curpos <- as.integer(temp[4])
                    fieldlen <- as.integer(temp[5])
                })
                if (!is.null(myclass) && nzchar(myclass) && myclass %in% c("form-control")) {
                    ## don't process these - they are e.g. key events in DT filter boxes
                    mycmd <- NULL
                }
                if (!is.null(mycmd)) {
                    if (debug > 1) cat("control key: ", mycmd, "\n")
                    mycmd <- strsplit(mycmd, "|", fixed = TRUE)[[1]] ## e.ctrlKey + '|' + e.altKey + '|' + e.shiftKey + '|' + e.metaKey + '|' + e.which
                    if (length(mycmd) == 5) {
                        ky <- mycmd[5]
                        if (ky %eq% "27") {
                            ## esc
                            if (is.null(editing$active) || !editing$active %eq% "teams") {
                                editing$active <- NULL
                                removeModal()
                            }
                        } else if (ky %eq% "45" && is.null(editing$active)) {
                            ## insert new row above/below current
                            where <- if (mycmd[3] == "true") "below" else "above" ## shift-insert is below, otherwise insert above
                            insert_data_row(where)
                        } else if (ky %eq% "46" && is.null(editing$active)) {
                            ## delete current row
                            delete_data_row()
                        } else if (ky %eq% "83" && is.null(editing$active)) {
                            insert_sub()
                        } else if (ky %eq% "113") {
                            ## insert new setting actions
                            insert_setting_data_row()
                        # }  else if(ky %eq% "112") {
                        #     home_force_rotate()
                        # } else if(ky %eq% "121") {
                        #     visiting_force_rotate()
                        } else if (ky %eq% "115") {
                            ## delete all setting actions
                            delete_setting_data_row()
                        }  else if (ky %eq% "117") {
                            ## insert new digging actions
                            insert_dig_data_row()
                        }  else if (ky %eq% "119") {
                            ## delete all digging actions
                            delete_dig_data_row()
                        } else if (ky %eq% "37") {
                            ## 37 (left arrow)
                            if (curpos %eq% 0L && grepl("shiny-bound-input", myclass, fixed = TRUE)) {
                                ## find prev code_entry_* element
                                myid <- sub("^code_entry_", "", myid)
                                prevel <- which(code_bits_tbl$bit %eq% myid)-1
                                if (length(prevel) == 1 && prevel >= 1) focus_to_element(paste0("code_entry_", code_bits_tbl$bit[prevel]))
                            }
                        } else if (ky %eq% "38") {
                            ## 38 (up arrow)
                        } else if (ky %eq% "39") {
                            ## 39 (right arrow)
                            if (curpos %eq% fieldlen && grepl("shiny-bound-input", myclass, fixed = TRUE)) {
                                ## find next code_entry_* element
                                myid <- sub("^code_entry_", "", myid)
                                nextel <- which(code_bits_tbl$bit %eq% myid)+1
                                if (length(nextel) == 1 && nextel <= nrow(code_bits_tbl)) focus_to_element(paste0("code_entry_", code_bits_tbl$bit[nextel]))
                            }
                        } else if (ky %eq% "40") {
                            ## 40 (down arrow)
                        }
                    }
                }
            }
        })

        ### IF EDITING CODE, DISPLAY A VIDEO REVIEW LOOP (ATTEMPT)
        
        have_second_video <- !is.null(app_data$video_src2)
        current_video_src <- reactiveVal(1L) ## start with video 1
        preview_video_src <- reactiveVal(1L)
        
        review_pane_active <- reactiveVal(FALSE)
        show_review_pane <- function() {
            ## use the current video time from the main video
            ## construct the playlist js by hand, because we need to inject the current video time
            revsrc <- get_src_type(if (current_video_src() == 1L) app_data$dvw$meta$video$file else app_data$video_src2)
            dojs(paste0("var start_t=vidplayer.currentTime()-2; revpl.set_playlist_and_play([{'video_src':'", revsrc$src, "','start_time':start_t,'duration':4,'type':'", revsrc$type, "'}], 'review_player', '", revsrc$type, "', true); revpl.set_playback_rate(1.4);"))
            js_show2("review_pane")
            dojs("Shiny.setInputValue('rv_height', $('#review_player').innerHeight());")
            review_pane_active(TRUE)
        }
        observeEvent(input$rv_height, {
            ##cat("rv_height: ", cstr(input$rv_height), "\n")
            if ((length(input$rv_height) < 1 || is.na(input$rv_height) || input$rv_height <= 0) && review_pane_active()) {
                dojs("Shiny.setInputValue('rv_height', $('#review_player').innerHeight());")
            }
        })
        observe({
            output$review_overlay <- renderPlot({
                opar <- par(mar = c(0, 0, 0, 0), oma = c(0, 0, 0, 0))
                plot(c(0, 1), c(0, 1), xlim = c(0, 1), ylim = c(0, 1), type = "n", xlab = NA, ylab = NA, axes = FALSE, xaxs = "i", yaxs = "i")
                if (FALSE) {##isTRUE(prefs$show_courtref)) {
                    oxy <- overlay_court_lines()
                    ## account for aspect ratios
                    ## ?? oxy$image_x <- ar_fix_x(oxy$image_x)
                    ## ?? oxy$xend <- ar_fix_x(oxy$xend)
                    ## ?? oxy$image_y <- ar_fix_y(oxy$image_y)
                    ## ?? oxy$yend <- ar_fix_y(oxy$yend)
                    segments(x0 = oxy$image_x, y0 = oxy$image_y, x1 = oxy$xend, y1 = oxy$yend, col = app_data$styling$court_lines_colour)
                }
                if (!is.null(overlay_points()) && nrow(overlay_points()) > 0) {
                    ixy <- setNames(crt_to_vid(overlay_points(), arfix = FALSE), c("x", "y"))
                    ## points as blue, invalid points as red
                    points(ixy$x[overlay_points()$valid], ixy$y[overlay_points()$valid], bg = "dodgerblue", pch = 21, col = "white", cex = 2.5)
                    points(ixy$x[!overlay_points()$valid], ixy$y[!overlay_points()$valid], bg = "firebrick", pch = 21, col = "white", cex = 2.5)
                }
                par(opar)
            }, bg = "transparent", height = input$rv_height)
        })
        hide_review_pane <- function() {
            js_hide2("review_pane")
            dojs("revpl.video_stop();")
            review_pane_active(FALSE)
        }
        
        overlay_points <- reactiveVal(NULL)
        overlay_court_lines <- reactive({
            # if (!is.null(detection_ref()$court_ref)) {
            #     oxy <- ovideo::ov_overlay_data(zones = FALSE, serve_zones = FALSE, space = "image", court_ref = detection_ref()$court_ref, crop = TRUE)$courtxy
            #     dplyr::rename(oxy, image_x = "x", image_y = "y")
            # } else {
                NULL
            # }
        })
        
        ####
        
        edit_current_code <- function() {
            ridx <- playslist_current_row()
            if (!is.null(ridx)) {
                thiscode <- rdata$dvw$plays$code[ridx]
                editing$active <- "edit"
                showModal(modalDialog(title = "Edit code", size = "l", 
                                      footer = tags$div(actionButton("edit_commit", label = "Update code (or press Enter)"), actionButton("edit_cancel", label = "Cancel (or press Esc)")),
                                      withTags({
                                          fluidRow(
                                              column(12, "Edit code either in the top text box or in the individual boxes (but not both)",
                                                     textInput("code_entry", label = "Code:", value = thiscode),
                                                     "or",
                                                     build_code_entry_guide("edit", rdata$dvw$plays[ridx, ])))
                                      })
                ))
                
                if (!is_skill(rdata$dvw$plays$skill[ridx])) {
                    ## if it's a non-skill code then focus into the code_entry textbox with cursor at end of input
                    focus_in_code_entry("code_entry")
                } else {
                    ## otherwise focus into the appropriate code_entry_guide sub-box
                    this_skill <- rdata$dvw$plays$skill[ridx]
                    if (this_skill %in% c("Serve", "Reception")) {
                        if (is.na(rdata$dvw$plays$start_zone[ridx])) {
                            focus_in_code_entry("code_entry_start_zone")
                        } else {
                            focus_in_code_entry("code_entry_end_zone")
                        }
                    } else if (this_skill %in% c("Attack")) {
                        focus_in_code_entry("code_entry_end_zone")
                    } else if (this_skill %in% c("Dig")) {
                        focus_in_code_entry("code_entry_eval")
                    } else {
                        focus_in_code_entry("code_entry_skill")
                    }
                }
                show_review_pane()
            }
        }
        focus_in_code_entry <- function(id, highlight_all = TRUE) {
            ## function to set the cursor focus to a particular entry box
            if (!highlight_all) {
                dojs(paste0("$(\"#shiny-modal\").on('shown.bs.modal', function (e) { var el = document.getElementById('", id, "'); el.selectionStart = el.selectionEnd = el.value.length; el.focus(); });"))
            } else {
                dojs(paste0("$(\"#shiny-modal\").on('shown.bs.modal', function (e) { var el = document.getElementById('", id, "'); el.selectionStart = 0; el.selectionEnd = el.value.length; el.focus(); });"))
            }
        }
        focus_in_sub_entry <- function(id, highlight_all = TRUE) {
            ## function to set the cursor focus to a particular entry box
            if (!highlight_all) {
                dojs(paste0("$(\"#shiny-modal\").on('shown.bs.modal', function (e) { var el = document.getElementById('", id, "'); el.selectionStart = el.selectionEnd = el.value.length; el.focus(); });"))
            } else {
                dojs(paste0("$(\"#shiny-modal\").on('shown.bs.modal', function (e) { var el = document.getElementById('", id, "'); el.selectionStart = 0; el.selectionEnd = el.value.length; el.focus(); });"))
            }
        }
        focus_to_element <- function(id, highlight_all = TRUE) {
            ## function to set the cursor focus to a particular entry box
            if (!highlight_all) {
                dojs(paste0("var el = document.getElementById('", id, "'); el.selectionStart = el.selectionEnd = el.value.length; el.focus();"))
            } else {
                dojs(paste0("var el = document.getElementById('", id, "'); el.selectionStart = 0; el.selectionEnd = el.value.length; el.focus();"))
            }
        }
        observeEvent(input$edit_cancel, {
            if (!is.null(editing$active) && editing$active %in% "teams") {
                htdata_edit(NULL)
                vtdata_edit(NULL)
            }
            editing$active <- NULL
            removeModal()
            hide_review_pane()
        })
        observeEvent(input$edit_commit, {
            if (!is.null(editing$active)) code_make_change()
        })
        code_make_change <- function() {
            removeModal()
            hide_review_pane()
            do_reparse <- FALSE
            if (is.null(editing$active)) {
                ## not triggered from current editing activity, huh?
                warning("code_make_change entered but editing not active")
            } else if (editing$active %eq% "tagging") {
                ## add tag
                txt <- if (is.null(input$tag_text)) "" else input$tag_text
                do_video("tag_current_video_time", if (nzchar(txt)) base64enc::base64encode(charToRaw(txt)) else "")
            } else if (editing$active %eq% "teams") {
                ## update from all the input$ht_edit_name/id/coach/assistant inputs
                htidx <- which(rdata$dvw$meta$teams$home_away_team %eq% "*") ## should always be 1
                rdata$dvw$meta$teams$team[htidx] <- input$ht_edit_name
                rdata$dvw$meta$teams$team_id[htidx] <- input$ht_edit_id
                rdata$dvw$meta$teams$coach[htidx] <- input$ht_edit_coach
                rdata$dvw$meta$teams$assistant[htidx] <- input$ht_edit_assistant
                if (!is.null(htdata_edit())) {
                    rdata$dvw$meta$players_h <- htdata_edit()
                    rdata$dvw$meta$players_h$name <- paste(rdata$dvw$meta$players_h$firstname, rdata$dvw$meta$players_h$lastname)
                }
                ## and visiting team
                vtidx <- which(rdata$dvw$meta$teams$home_away_team %eq% "a") ## should always be 2
                rdata$dvw$meta$teams$team[vtidx] <- input$vt_edit_name
                rdata$dvw$meta$teams$team_id[vtidx] <- input$vt_edit_id
                rdata$dvw$meta$teams$coach[vtidx] <- input$vt_edit_coach
                rdata$dvw$meta$teams$assistant[vtidx] <- input$vt_edit_assistant
                if (!is.null(vtdata_edit())) {
                    rdata$dvw$meta$players_v <- vtdata_edit()
                    rdata$dvw$meta$players_v$name <- paste(rdata$dvw$meta$players_v$firstname, rdata$dvw$meta$players_v$lastname)
                }
                do_reparse <- TRUE
            } else if (editing$active %eq% "match_data") {
                rdata$dvw$meta$match$date <- input$match_edit_date
                rdata$dvw$meta$match$time <- tryCatch(lubridate::hms(input$match_edit_time), error = function(e) lubridate::as.period(NA))
                rdata$dvw$meta$match$season <- input$match_edit_season
                rdata$dvw$meta$match$league <- input$match_edit_league
                rdata$dvw$meta$match$phase <- input$match_edit_phase
                rdata$dvw$meta$match$home_away <- input$match_edit_home_away
                rdata$dvw$meta$match$day_number <- input$match_edit_day_number
                rdata$dvw$meta$match$match_number <- input$match_edit_match_number
                ## currently disabled rdata$dvw$meta$match$regulation <- input$match_edit_regulation
                rdata$dvw$meta$match$zones_or_cones <- input$match_edit_zones_or_cones
                do_reparse <- TRUE
            } else if (editing$active %eq% "change starting lineup") {
                if(input$ht_set_number != "" && input$ht_P1  != ""  && input$ht_P2 != "" &&
                   input$ht_P3 != "" &&  input$ht_P4 != "" && input$ht_P5 != "" &&
                   input$ht_P6 != "" && input$ht_setter != ""){
                    team = datavolley::home_team(rdata$dvw)
                    setnumber = input$ht_set_number
                    new_setter = input$ht_setter
                    new_rotation = c(input$ht_P1,input$ht_P2,input$ht_P3,input$ht_P4,input$ht_P5,input$ht_P6)
                    new_rotation_id = rdata$dvw$meta$players_h$player_id[match(new_rotation, rdata$dvw$meta$players_h$number)]
                    # Change meta data in terms of starting rotation
                    rdata$dvw$meta$players_h[,paste0("starting_position_set", setnumber)] <- as.character(match(rdata$dvw$meta$players_h$player_id, new_rotation_id))
                    ## Change libero to "*" in meta
                    ## BR not sure if this is needed, it was commented out
                    ##rdata$dvw$meta$players_h[rdata$dvw$meta$players_h$number %eq% input$ht_libero,paste0("starting_position_set", setnumber)] <- "*"
                    # Change in play rotation 
                    rdata$dvw <- dv_change_startinglineup(rdata$dvw, team, setnumber, new_rotation, new_rotation_id, new_setter)
                }
                if(input$vt_set_number != "" && input$vt_P1  != ""  && input$vt_P2 != "" &&
                   input$vt_P3 != "" &&  input$vt_P4 != "" && input$vt_P5 != "" && 
                   input$vt_P6 != "" && input$vt_setter != ""){
                    team = datavolley::visiting_team(rdata$dvw)
                    setnumber = input$vt_set_number
                    new_setter = input$vt_setter
                    new_rotation = c(input$vt_P1,input$vt_P2,input$vt_P3,input$vt_P4,input$vt_P5,input$vt_P6)
                    new_rotation_id = rdata$dvw$meta$players_v$player_id[match(new_rotation, rdata$dvw$meta$players_v$number)]
                    # Change meta data in terms of starting rotation
                    rdata$dvw$meta$players_v[,paste0("starting_position_set", setnumber)] <- as.character(match(rdata$dvw$meta$players_v$player_id, new_rotation_id))
                    ## Change libero to "*" in meta
                    ## BR not sure if this is needed, it was commented out
                    ##rdata$dvw$meta$players_v[rdata$dvw$meta$players_v$number %eq% input$vt_libero,paste0("starting_position_set", setnumber)] <- "*"
                    # Change in play rotation 
                    rdata$dvw <- dv_change_startinglineup(rdata$dvw, team, setnumber, new_rotation, new_rotation_id, new_setter)
                }
                do_reparse <- TRUE
            } else if (editing$active %eq% "delete all setting actions") {
                ridx <- dplyr::filter(mutate(rdata$dvw$plays, rowN = row_number()), .data$skill %eq% "Set" & !.data$evaluation %eq% "Error")$rowN
                if (length(ridx) > 0) {
                    if (is.logical(ridx)) ridx <- which(ridx)
                    rdata$dvw$plays <- rdata$dvw$plays[-ridx, ]
                    do_reparse <- TRUE
                }
            } else if (editing$active %eq% "insert setting actions") {
                ridx_set <- dv_insert_sets_check(rdata$dvw, no_set_attacks = no_set_attacks)
                if (length(ridx_set) > 0) {
                    rdata$dvw <- dv_insert_sets(rdata$dvw, no_set_attacks = no_set_attacks, default_set_evaluation = default_set_evaluation, ridx = ridx_set)
                    do_reparse <- TRUE
                }
            } else if (editing$active %eq% "delete all digging actions") {
                ridx <- dplyr::filter(mutate(rdata$dvw$plays, rowN = row_number()), .data$skill %eq% "Dig")$rowN
                if (length(ridx) > 0) {
                    if (is.logical(ridx)) ridx <- which(ridx)
                    rdata$dvw$plays <- rdata$dvw$plays[-ridx, ]
                    do_reparse <- TRUE
                }
            } else if (editing$active %eq% "insert digging actions") {
                ## find attacks that remained in play, and which were not followed by a dig, nor followed by a block then a dig
                ridx_dig <- dv_insert_digs_check(rdata$dvw)
                if (length(ridx_dig) > 0) {
                    rdata$dvw <- dv_insert_digs(rdata$dvw, ridx = ridx_dig)
                    do_reparse <- TRUE
                }
            } else {
                ridx <- playslist_current_row()
                if (!is.null(ridx)) {
                    if (editing$active %in% c("edit", "insert above", "insert below")) {
                        current_code <- rdata$dvw$plays$code[ridx]
                        ## user has changed EITHER input$code_entry or used the code_entry_guide
                        ## infer code from code_entry_guide elements
                        newcode1 <- lapply(seq_len(nrow(code_bits_tbl)), function(bi) {
                            val <- input[[paste0("code_entry_", code_bits_tbl$bit[bi])]]
                            if (is.null(val)) val <- ""
                            wid <- code_bits_tbl$width[bi]
                            if (nchar(val) < wid) val <- str_pad(val, wid, side = "right", pad = "~")
                            val
                        })
                        newcode1 <- sub("~+$", "", paste(newcode1, collapse = ""))## trim trailing ~'s
                        newcode2 <- input$code_entry
                        changed1 <- (!newcode1 %eq% current_code) && nzchar(newcode1)
                        changed2 <- (!newcode2 %eq% current_code) && nzchar(newcode2)
                        if (!changed1 && changed2) {
                            newcode <- newcode2
                            ## if we entered via the text box, then run this through the code parser
                            newcode <- sub("~+$", "", ov_code_interpret(newcode))
                        } else if (!changed2 && changed1) {
                            newcode <- newcode1
                        } else if (!changed1 && !changed2) {
                            ## neither changed, nothing to do
                            newcode <- NULL
                        } else {
                            ## both changed?
                            newcode <- NULL
                            warning("BOTH CHANGED: to do")
                        }
                    }
                    if (editing$active %eq% "edit" && !is.null(newcode)) {
                        if (length(newcode) == 1) {
                            ## update the code in the current row
                            rdata$dvw$plays$code[ridx] <- newcode
                        } else if (length(newcode) == 2) {
                            ## hmm, have we entered a compound code?
                            ## can't handle that yet, is it even sensible to support?
                            warning("compound codes can't be used when editing an existing code")
                        }
                    } else if (editing$active %eq% "insert above" && !is.null(newcode)) {
                        ## insert new line above current
                        if (is.logical(ridx)) ridx <- which(ridx)
                        newline <- rdata$dvw$plays[rep(ridx, length(newcode)), ]
                        newline$code <- newcode
                        rdata$dvw$plays <- bind_rows(if (ridx > 1) rdata$dvw$plays[seq(1, ridx-1L, by = 1), ], newline, rdata$dvw$plays[seq(ridx, nrow(rdata$dvw$plays), by = 1), ])
                        ## set the newly-inserted line as the active row
                        ##nsr <- find_next_skill_row()
                        ##if (length(nsr) > 0) playslist_select_row(nsr)
                    } else if (editing$active %eq% "insert below" && !is.null(newcode)) {
                        ## insert new line below current
                        if (is.logical(ridx)) ridx <- which(ridx)
                        newline <- rdata$dvw$plays[rep(ridx, length(newcode)), ]
                        newline$code <- newcode
                        rdata$dvw$plays <- bind_rows(rdata$dvw$plays[seq(1, ridx, by = 1), ], newline, rdata$dvw$plays[seq(ridx + 1L, nrow(rdata$dvw$plays), by = 1), ])
                    } else if (editing$active %eq% "delete") {
                        if (is.logical(ridx)) ridx <- which(ridx)
                        rdata$dvw$plays <- rdata$dvw$plays[-ridx, ]
                    } else if (editing$active %eq% "substitution"){
                        if(input$ht_inplayer != "" && input$ht_outplayer != ""){
                            teamSelect = datavolley::home_team(rdata$dvw)
                            rdata$dvw <- dv_create_substitution(rdata$dvw, ridx, team = teamSelect, in_player = input$ht_inplayer, out_player = input$ht_outplayer, 
                                                                new_setter = input$ht_new_setter)
                        }
                        if(input$vt_inplayer != "" && input$vt_outplayer != ""){
                            teamSelect = datavolley::visiting_team(rdata$dvw)
                            rdata$dvw <- dv_create_substitution(rdata$dvw, ridx, team = teamSelect, in_player = input$vt_inplayer, out_player = input$vt_outplayer,
                                                                new_setter = input$vt_new_setter)
                        }
                    } else if (editing$active %eq% "home_force_rotation"){
                        if (is.logical(ridx)) ridx <- which(ridx)
                        rdata$dvw <- dv_force_rotation(rdata$dvw, team = datavolley::home_team(rdata$dvw), ridx, direction = 1)
                    } else if (editing$active %eq% "visiting_force_rotation"){
                        if (is.logical(ridx)) ridx <- which(ridx)
                        rdata$dvw <- dv_force_rotation(rdata$dvw, team = datavolley::visiting_team(rdata$dvw), ridx, direction = 1)
                    }
                    do_reparse <- TRUE
                }
            }
            if (do_reparse) {
                ## reparse the dvw
                rdata$dvw <- reparse_dvw(rdata$dvw, dv_read_args = dv_read_args)
                playslist_needs_scroll(TRUE)
                if (!auto_playlist_updates) replace_playlist_data()
            }
            editing$active <- NULL
        }
        insert_data_row <- function(where) {
            if (missing(where)) where <- "above"
            where <- tolower(where)
            if (!where %in% c("above", "below")) where <- "above" ## default
            ridx <- playslist_current_row()
            if (!is.null(ridx)) {
                if (where == "above" && ridx > 1) ridx <- ridx-1L ## we are inserting above the selected row, so use the previous row to populate this one
                ## otherwise (if inserting below) use the current row (ridx) as the template
                editing$active <- paste0("insert ", where)
                showModal(modalDialog(title = paste0("Insert new code ", where, " current row"), size = "l", footer = tags$div(actionButton("edit_commit", label = "Insert code (or press Enter)"), actionButton("edit_cancel", label = "Cancel (or press Esc)")),
                                      "Enter new code either in the top text box or in the individual boxes (but not both)",
                                      textInput("code_entry", label = "Code:", value = ""),
                                      "or",
                                      build_code_entry_guide("insert", rdata$dvw$plays[ridx, ])
                                      ))
                focus_in_code_entry("code_entry")
            }
        }
        delete_data_row <- function() {
            ridx <- playslist_current_row()
            if (!is.null(ridx)) {
                thiscode <- rdata$dvw$plays$code[ridx]
                editing$active <- "delete"
                showModal(modalDialog(title = "Delete code", size = "l", footer = actionButton("edit_cancel", label = "Cancel (or press Esc)"),
                                      actionButton("edit_commit", label = "Confirm delete code (or press Enter)")))
            }
        }

        home_force_rotate <- function() {
            ridx <- input$playslist_rows_selected
            if (!is.null(ridx)) {
                editing$active <- "home_force_rotation"
                code_make_change()
            }
        }
        visiting_force_rotate <- function() {
            ridx <- input$playslist_rows_selected
            if (!is.null(ridx)) {
                editing$active <- "visiting_force_rotation"
                code_make_change()
            }
        }

        ## Create a substitution
        insert_sub_old <- function() {
            ridx <- playslist_current_row()
            if (!is.null(ridx)) {
                if (ridx > 1) ridx <- ridx-1L ## we are inserting above the selected row, so use the previous row to populate this one
                editing$active <- "substitution"
                showModal(modalDialog(title = "Substitution", size = "l", footer = tags$div(actionButton("edit_commit", label = "Validate substitution"), actionButton("edit_cancel", label = "Cancel")),
                                      tabsetPanel(
                                          tabPanel("Home team",
                                                   tags$style("#ht_display_team {border: 2px solid #bfefff;}"),
                                                   DT::dataTableOutput("ht_display_team"),
                                                   wellPanel(
                                                       fluidRow(
                                                           column(1,
                                                                  tags$style("#ht_outplayer {border: 2px solid #dd4b39;}"),
                                                                  textInput("ht_outplayer", label = "OUT", placeholder = "OUT")),
                                                           column(1,
                                                                  tags$style("#ht_inplayer {border: 2px solid #168a52;}"),
                                                                  textInput("ht_inplayer", label = "IN", placeholder = "IN")),
                                                           column(2,
                                                                  tags$style("#ht_new_setter {border: 2px solid #f5ed0c;}"),
                                                                  textInput("ht_new_setter", label = "New Setter", placeholder = "NS"))
                                                           ),
                                                       style = "background: #bfefff"
                                                   )
                                          ),
                                          tabPanel("Visiting team",
                                                   tags$style("#vt_display_team {border: 2px solid #bcee68;}"),
                                                   DT::dataTableOutput("vt_display_team"),
                                                   wellPanel(
                                                       fluidRow(column(1, 
                                                                       tags$style("#vt_outplayer {border: 2px solid #dd4b39;}"),
                                                                       textInput("vt_outplayer", label = "OUT", placeholder = "OUT")),
                                                                column(1, 
                                                                       tags$style("#vt_inplayer {border: 2px solid #168a52;}"),
                                                                       textInput("vt_inplayer", label = "IN", placeholder = "IN")),
                                                                column(2,
                                                                       tags$style("#vt_new_setter {border: 2px solid #f5ed0c;}"),
                                                                       textInput("vt_new_setter", label = "New Setter", placeholder = "NS"))
                                                                ),
                                                       style = "background: #bcee68"
                                                   )
                                          )
                                      )
                ))
            }
        }
        
        insert_sub <- function() {
            ridx <- playslist_current_row()
            if (!is.null(ridx)) {
                if (ridx > 1) ridx <- ridx-1L ## we are inserting above the selected row, so use the previous row to populate this one
                editing$active <- "substitution"
                showModal(modalDialog(title = "Substitution", size = "l", footer = tags$div(actionButton("edit_commit", label = "Validate substitution"), actionButton("edit_cancel", label = "Cancel")),
                                      wellPanel(
                                          fluidRow(class = "ht_roster_court_vt_roster",
                                              column(3,id = "hroster", uiOutput("htroster")),
                                              column(6,plotOutput("court_inset", height = "200px")),
                                              column(3, id = "vroster", uiOutput("vtroster"))),
                                          fluidRow(class = "ht_sub_vt_sub",
                                              column(1,id = "hroster",
                                                     tags$style("#ht_outplayer {border: 2px solid #dd4b39;}"),
                                                     textInput("ht_outplayer", label = "OUT", placeholder = "OUT")),
                                              column(1,id = "hroster",
                                                     tags$style("#ht_inplayer {border: 2px solid #168a52;}"),
                                                     textInput("ht_inplayer", label = "IN", placeholder = "IN")),
                                              column(2,id = "hroster",
                                                     tags$style("#ht_new_setter {border: 2px solid #f5ed0c;}"),
                                                     textInput("ht_new_setter", label = "New Setter", placeholder = "NS")),
                                              column(4),
                                              column(2,id = "vroster",
                                                     tags$style("#vt_new_setter {border: 2px solid #f5ed0c;}"),
                                                     textInput("vt_new_setter", label = "New Setter", placeholder = "NS")),
                                              column(1, id = "vroster",
                                                     tags$style("#vt_inplayer {border: 2px solid #168a52;}"),
                                                     textInput("vt_inplayer", label = "IN", placeholder = "IN")),
                                              column(1, id = "vroster",
                                                     tags$style("#vt_outplayer {border: 2px solid #dd4b39;}"),
                                                     textInput("vt_outplayer", label = "OUT", placeholder = "OUT")))
                                      )
                )
                )
            }
        }
        
        output$htroster <- renderUI({
            re <- names2roster(rdata$dvw$meta$players_h)
            do.call(tags$div, c(list(tags$strong("Home team"), tags$br()), lapply(re, function(z) tagList(tags$span(z), tags$br()))))
        })
        output$vtroster <- renderUI({
            re <- names2roster(rdata$dvw$meta$players_v)
            do.call(tags$div, c(list(tags$strong("Visiting team"), tags$br()), lapply(re, function(z) tagList(tags$span(z), tags$br()))))
        })
        output$court_inset <- renderPlot({
            p <- ggplot(data = data.frame(x = c(-0.25, 4.25, 4.25, -0.25), y = c(-0.25, -0.25, 7.25, 7.25)), mapping = aes_string("x", "y")) +
                geom_polygon(data = data.frame(x = c(0.5, 3.5, 3.5, 0.5), y = c(0.5, 0.5, 3.5, 3.5)), fill = styling$h_court_colour) +
                geom_polygon(data = data.frame(x = c(0.5, 3.5, 3.5, 0.5), y = 3 + c(0.5, 0.5, 3.5, 3.5)), fill = styling$v_court_colour) +
                ggcourt(labels = NULL, show_zones = FALSE, show_zone_lines = TRUE, court_colour = "indoor")
            ridx <- playslist_current_row()
            if (!is.null(ridx)) {
                this_pn <- rdata$dvw$plays$player_number[ridx] ## player in the selected row
                htrot <- tibble(player_id = as.character(rdata$dvw$plays[ridx, paste0("home_player_id", 1:6)]), team_id = rdata$dvw$plays$home_team_id[ridx])
                htrot <- dplyr::left_join(htrot, rdata$dvw$meta$players_h[, c("player_id", "number", "lastname", "firstname", "name")], by = "player_id")
                vtrot <- tibble(player_id = as.character(rdata$dvw$plays[ridx, paste0("visiting_player_id", 1:6)]), team_id = rdata$dvw$plays$visiting_team_id[ridx])
                vtrot <- dplyr::left_join(vtrot, rdata$dvw$meta$players_v[, c("player_id", "number", "lastname", "firstname", "name")], by = "player_id")
                plxy <- cbind(dv_xy(1:6, end = "lower"), htrot)
                plxy$court_num <- unlist(rdata$dvw$plays[ridx, paste0("home_p", 1:6)]) ## the on-court player numbers in the play-by-play data
                ## player names and circles
                ## home team
                p <- p + geom_polygon(data = court_circle(cz = 1:6, end = "lower"), aes_string(group = "id"), fill = styling$h_court_colour, colour = styling$h_court_highlight)
                ## highlighted player
                if (rdata$dvw$plays$team[ridx] %eq% rdata$dvw$plays$home_team[ridx] && sum(this_pn %eq% plxy$court_num) == 1) {
                    p <- p + geom_polygon(data = court_circle(cz = which(this_pn %eq% plxy$court_num), end = "lower"), fill = "yellow", colour = "black")
                }
                p <- p + geom_text(data = plxy, aes_string("x", "y", label = "court_num"), size = 6, fontface = "bold", vjust = 0) +
                    geom_text(data = plxy, aes_string("x", "y", label = "lastname"), size = 3, vjust = 1.5)
                ## visiting team
                plxy <- cbind(dv_xy(1:6, end = "upper"), vtrot)
                plxy$court_num <- unlist(rdata$dvw$plays[ridx, paste0("visiting_p", 1:6)]) ## the on-court player numbers in the play-by-play data
                p <- p + geom_polygon(data = court_circle(cz = 1:6, end = "upper"), aes_string(group = "id"), fill = styling$v_court_colour, colour = styling$v_court_highlight)
                if (rdata$dvw$plays$team[ridx] %eq% rdata$dvw$plays$visiting_team[ridx] && sum(this_pn %eq% plxy$court_num) == 1) {
                    p <- p + geom_polygon(data = court_circle(cz = which(this_pn %eq% plxy$court_num), end = "upper"), fill = "yellow", colour = "black")
                }
                p <- p + geom_text(data = plxy, aes_string("x", "y", label = "court_num"), size = 6, fontface = "bold", vjust = 0) +
                    geom_text(data = plxy, aes_string("x", "y", label = "lastname"), size = 3, vjust = 1.5)  + coord_flip()
            }
            p
        })
        # Insert setting
        
        insert_setting_data_row <- function() {
            ridx_set <- dv_insert_sets_check(rdata$dvw, no_set_attacks = no_set_attacks)
            if (length(ridx_set) > 0) {
                editing$active <- "insert setting actions"
                showModal(modalDialog(title = "Insert setting codes", size = "l", footer = actionButton("edit_cancel", label = "Cancel (or press Esc)"),
                                      actionButton("edit_commit", label = paste0("Confirm insert ", length(ridx_set), " setting codes (or press Enter)"))))
            } else {
                showModal(modalDialog(title = "Insert setting codes", size = "l", footer = actionButton("edit_cancel", label = "Cancel (or press Esc)"),
                                      "No setting codes to insert."))
            }
        }

        delete_setting_data_row <- function() {
            ridx <- dplyr::filter(mutate(rdata$dvw$plays, rowN = row_number()), .data$skill %eq% "Set" & !.data$evaluation %eq% "Error")$rowN
            if (length(ridx) > 0) {
                thiscode <- rdata$dvw$plays$code[ridx]
                editing$active <- "delete all setting actions"
                showModal(modalDialog(title = "Delete all non-error setting codes", size = "l", footer = actionButton("edit_cancel", label = "Cancel (or press Esc)"),
                                      actionButton("edit_commit", label = paste0("Confirm delete (", length(ridx), ") setting codes (or press Enter)"))))
            }
        }
        insert_dig_data_row <- function() {
            ridx_dig <- dv_insert_digs_check(rdata$dvw)
            if (length(ridx_dig) > 0) {
                editing$active <- "insert digging actions"
                showModal(modalDialog(title = "Insert dig codes", size = "l", footer = actionButton("edit_cancel", label = "Cancel (or press Esc)"),
                                      actionButton("edit_commit", label = paste0("Confirm insert ", length(ridx_dig), " dig codes (or press Enter)"))))
            } else {
                showModal(modalDialog(title = "Insert dig codes", size = "l", footer = actionButton("edit_cancel", label = "Cancel (or press Esc)"),
                                      "No dig codes to insert."))
            }
        }
        delete_dig_data_row <- function() {
            ridx <- dplyr::filter(mutate(rdata$dvw$plays, rowN = row_number()), .data$skill %eq% "Dig")$rowN
            if (length(ridx) > 0) {
                thiscode <- rdata$dvw$plays$code[ridx]
                editing$active <- "delete all digging actions"
                showModal(modalDialog(title = "Delete all digging codes", size = "l", footer = actionButton("edit_cancel", label = "Cancel (or press Esc)"),
                                      actionButton("edit_commit", label = paste0("Confirm delete all (", length(ridx), ") digging codes (or press Enter)"))))
            }
        }

        ## tagging
        add_tagged_event <- function() {
            editing$active <- "tagging"
            showModal(modalDialog(title = "Add tag at current video time", size = "l", footer = actionButton("tagging_cancel", label = "Cancel (or press Esc)"),
                                  tags$div(textInput("tag_text", "Tag text:"), actionButton("do_add_tag", "Add tag (or press Enter)"))
            ))
            ## focus
            dojs("$(\"#shiny-modal\").on('shown.bs.modal', function (e) { var el = document.getElementById(\"tag_text\"); el.selectionStart = el.selectionEnd = el.value.length; el.focus(); });")
        }
        observeEvent(input$tagging_cancel, {
            editing$active <- NULL
            removeModal()
        })
        observeEvent(input$do_add_tag, {
            code_make_change()
        })
        observeEvent(input$tag_current_video_time, {
            temp <- strsplit(input$tag_current_video_time, split = "&", fixed = TRUE)[[1]]
            tagtxt <- if (length(temp) >= 2) rawToChar(base64enc::base64decode(temp[2])) else ""
            tm <- as.numeric(temp[1])
            extra <- selected_event()
            ## add match_id regardless of whether there is a selected event
            this_match_id <- rdata$dvw$meta$match_id
            if (!is.null(extra)) extra <- extra[, setdiff(names(extra), "match_id")]
            thisxy <- data.frame(x = NA_real_, y = NA_real_)
            if (nrow(court_inset$click_points$queue) > 0) {
                thisxy <- tail(court_inset$click_points$queue, 1)
                thisxy <- cbind(thisxy, crt_to_vid(thisxy))
            }
            tag_data$events <- bind_rows(tag_data$events, bind_cols(tibble(match_id = this_match_id, tag_video_time = tm, tag = tagtxt), thisxy, extra))
            ## clear the ball coords data
            court_inset$clear_click_queue()
        })
        remember_include_all_pbp <- reactiveVal(FALSE)
        tag_manager <- function() {
            editing$active <- "tagging"
            showModal(modalDialog(
                title = "Tag manager",
                size = "l", footer = actionButton("tagging_cancel", label = "Cancel (or press Esc)"),
                ##    ##tags$div(textInput("tag_text", "Tag text:"), actionButton("do_add_tag", "Add tag (or press Enter)"))
                fluidRow(column(4, downloadButton("download_tags"), checkboxInput("tags_include_all_pbp", "Include all play-by-play data columns?", value = remember_include_all_pbp())), column(4, actionButton("clear_tags", "Clear tag data")))
            ))
#### focus
            ##dojs("$(\"#shiny-modal\").on('shown.bs.modal', function (e) { var el = document.getElementById(\"tag_text\"); el.selectionStart = el.selectionEnd = el.value.length; el.focus(); });")
        }
        observeEvent(input$tags_include_all_pbp, remember_include_all_pbp(input$tags_include_all_pbp))
        observeEvent(input$clear_tags, {
            tag_data$events <- tibble(tag_video_time = numeric(), tag = character())
            editing$active <- NULL
            removeModal()
        })
        output$download_tags <- downloadHandler(
            filename = function() "tags.csv",
            content = function(file) {
                editing$active <- NULL
                this <- tag_data$events
                this <- this[, setdiff(names(this), c("error_message", "error_icon"))] ## don't export these
                if (!isTRUE(remember_include_all_pbp())) {
                    ## include just some key columns
                    ## note that if we tagged without a selected row, there are no additional data for that row
                    this <- this[, intersect(names(this), c("match_id", "set_number", "file_line_number", "video_time", "tag", "tag_video_time", "image_x", "image_y", "x", "y"))]
                }
                write.csv(this, file, row.names = FALSE, na = "")
                removeModal()
            }
        )

        ## video functions
        do_video <- function(what, ..., id = "main_video") {
            if (!app_data$with_video) return(NULL)
            getel <- paste0("document.getElementById('", id, "')")
            myargs <- list(...)
            if (what == "pause") {
                if (video_state$paused) {
                    dojs(paste0(getel, ".play();"))
                    video_state$paused <- FALSE
                } else {
                    dojs(paste0(getel, ".pause();"))
                    video_state$paused <- TRUE
                }
                NULL
            } else if (what == "toggle_pause") {
                dojs(paste0("if (", getel, ".paused == true) { ", getel, ".play(); } else { ", getel, ".pause(); }"))
            } else if (what == "get_time") {
                dojs(paste0("Shiny.onInputChange('video_time', ", getel, ".currentTime)"))
            } else if (what == "get_time_fid") {
                dojs(paste0("Shiny.onInputChange('video_time', ", getel, ".currentTime + '&", myargs[[1]], "')"))
            } else if (what == "set_time") {
                dojs(paste0(getel, ".currentTime='", myargs[[1]], "';"))
            } else if (what == "set_current_video_time") {
                dojs(paste0("Shiny.onInputChange('set_current_video_time', ", getel, ".currentTime + '&", myargs[1], "&' + new Date().getTime())"))
            } else if (what == "tag_current_video_time") {
                dojs(paste0("Shiny.onInputChange('tag_current_video_time', ", getel, ".currentTime + '&", myargs[1], "')"))
            } else if (what == "rew") {
                dojs(paste0(getel, ".currentTime=", getel, ".currentTime - ", myargs[[1]], ";"))
            } else if (what == "ff") {
                dojs(paste0(getel, ".currentTime=", getel, ".currentTime + ", myargs[[1]], ";"))
            } else if (what == "playback_rate") {
                dojs(paste0(getel, ".playbackRate=", myargs[[1]], ";"))
            } else {
                NULL
            }
        }

        ## save file
        output$save_file_ui <- renderUI({
            if (is.null(rdata$dvw)) {
                NULL
            } else {
                downloadButton("save_file_button", "Save file")
            }
        })
        output$save_file_button <- downloadHandler(
            filename = reactive(
                if (!is.null(rdata$dvw$meta$filename) && !is.na(rdata$dvw$meta$filename)) basename(rdata$dvw$meta$filename) else "myfile.dvw"
            ),
            content = function(file) {
                tryCatch(dv_write(rdata$dvw, file = file),
                         error = function(e) {
                             rds_ok <- FALSE
                             if (running_locally) {
                                 ## this only makes sense if running locally, not deployed on a remote server
                                 tf <- tempfile(fileext = ".rds")
                                 try({
                                     saveRDS(rdata$dvw, file = tf)
                                     rds_ok <- file.exists(tf) && file.size(tf) > 0
                                 }, silent = TRUE)
                             }
                             showModal(modalDialog(title = "Save error",
                                                   tags$div(class = "alert alert-danger", "Sorry, the save failed. The error message was:", tags$br(), tags$pre(conditionMessage(e)), tags$br(), if (rds_ok) paste0("The edited datavolley object has been saved to ", tf, ". You might be able to recover your edited information from that (contact the package authors for assistance)."))))
                             NULL
                         })
            }
        )
        build_code_entry_guide <- function(mode, thisrow) {
            mode <- match.arg(mode, c("edit", "insert"))
            bitstbl <- code_bits_tbl
            if (mode %eq% "edit" && is_skill(thisrow$skill)) {
                ## only with skill, not timeout/sub/etc
                thiscode <- thisrow$code
                bitstbl$value <- vapply(seq_len(nrow(bitstbl)), function(z) substr(thiscode, bitstbl$start[z], bitstbl$end[z]), FUN.VALUE = "", USE.NAMES = FALSE)
            } else {
                bitstbl$value <- ""
            }
            bitstbl$value <- gsub("~", "", bitstbl$value)
            cbitInput <- function (bitname, value = "", width = 2, helper = "") {
                tags$div(style = paste0("display:inline-block; vertical-align:top;"), tags$input(id = paste0("code_entry_", bitname), type = "text", value = value, size = width, maxlength = width, class = "input-small"),
                         ##HTML(paste0("<input id=\"code_entry_", bitname, "\" type=\"text\" value=\"", value, "\" size=\"", width, "\" maxlength=\"", width, "\" class=\"input-small\"", if (bitname == "end_zone") " autofocus=\"autofocus\"", " />")),
                         tags$div(class = "code_entry_guide", helper))
            }
            tags$div(style = "padding: 8px;", do.call(shiny::fixedRow, lapply(seq_len(nrow(bitstbl)), function(z) {
                this_skill <- bitstbl$value[bitstbl$bit %eq% "skill"]
                this_ev <- bitstbl$value[bitstbl$bit %eq% "eval"]
                cbitInput(bitstbl$bit[z], value = bitstbl$value[z], width = bitstbl$width[z], helper = if (is.function(bitstbl$helper[[z]])) uiOutput(paste0("code_entry_helper_", bitstbl$bit[z], "_ui")) else HTML(bitstbl$helper[[z]]))
            })))
        }

        ## the helpers that are defined as functions in code_bits_tbl are dynamic, they depend on skill/evaluation
        ## ADD HANDLERS HERE
        output$code_entry_helper_skill_type_ui <- renderUI({
            HTML(skill_type_helper(input$code_entry_skill, input$code_entry_eval))
        })
        output$code_entry_helper_num_players_ui <- renderUI({
            HTML(num_players_helper(input$code_entry_skill, input$code_entry_eval))
        })
        output$code_entry_helper_special_ui <- renderUI({
            HTML(special_helper(input$code_entry_skill, input$code_entry_eval))
        })
        output$code_entry_helper_end_zone_ui <- renderUI({
            HTML(end_zone_helper(input$code_entry_skill, input$code_entry_eval))
        })

        ## match data editing
        observeEvent(input$edit_match_data_button, {
            editing$active <- "match_data"
            match_time <- if (!is.na(rdata$dvw$meta$match$time)) {
                              as.POSIXct(rdata$dvw$meta$match$time, origin = "1970-01-01")
                          } else {
                              NULL
                          }
            showModal(modalDialog(title = "Edit match data", size = "l", footer = tags$div(actionButton("edit_commit", label = "Update match data (or press Enter)"), actionButton("edit_cancel", label = "Cancel (or press Esc)")),
                                  tags$div(
                                           fluidRow(column(4, shiny::dateInput("match_edit_date", label = "Match date:", value = rdata$dvw$meta$match$date)),
                                                    column(4, textInput("match_edit_time", label = "Start time:", value = match_time, placeholder = "HH:MM:SS")),
                                                    column(4, textInput("match_edit_season", label = "Season:", value = rdata$dvw$meta$match$season))),
                                           fluidRow(column(4, textInput("match_edit_league", label = "League:", value = rdata$dvw$meta$match$league)),
                                                    column(4, textInput("match_edit_phase", label = "Phase:", value = rdata$dvw$meta$match$phase)),
                                                    column(4, shiny::selectInput("match_edit_home_away", label = "Home/away:", choices = c("", "Home", "Away"), selected = rdata$dvw$meta$match$home_away))),
                                           fluidRow(column(4, textInput("match_edit_day_number", "Day number:", value = rdata$dvw$meta$match$day_number)),
                                                    column(4, textInput("match_edit_match_number", "Match number:", value = rdata$dvw$meta$match$match_number)),
                                                    ##column(2, shiny::selectInput("match_edit_regulation", "Regulation:", choices = c("indoor sideout", "indoor rally point", "beach rally point"), selected = rdata$dvw$meta$match$regulation)),
                                                    column(4, shiny::selectInput("match_edit_zones_or_cones", "Zones or cones:", choices = c("C", "Z"), selected = rdata$dvw$meta$match$zones_or_cones), tags$span(style = "font-size:small", "Note: changing cones/zones here will only change the indicator in the file header, it will not convert a file recorded with zones into one recorded with cones, or vice-versa. Don't change this unless you know what you are doing!")))
                                       )
                                  ))
        })

        ## team data editing
        observeEvent(input$edit_teams_button, {
            editing$active <- "teams"
            htidx <- which(rdata$dvw$meta$teams$home_away_team %eq% "*") ## should always be 1
            vtidx <- which(rdata$dvw$meta$teams$home_away_team %eq% "a") ## should always be 2
            showModal(modalDialog(title = "Edit teams", size = "l", footer = tags$div(actionButton("edit_commit", label = "Update teams data"), actionButton("edit_cancel", label = "Cancel")),
                                  tabsetPanel(
                                      tabPanel("Home team",
                                               fluidRow(column(4, textInput("ht_edit_name", label = "Team name:", value = rdata$dvw$meta$teams$team[htidx])),
                                                        column(4, textInput("ht_edit_id", label = "Team ID:", value = rdata$dvw$meta$teams$team_id[htidx])),
                                                        column(4, textInput("ht_edit_coach", label = "Coach:", value = rdata$dvw$meta$teams$coach[htidx])),
                                                        column(4, textInput("ht_edit_assistant", label = "Assistant:", value = rdata$dvw$meta$teams$assistant[htidx]))),
                                               DT::dataTableOutput("ht_edit_team"),
                                               wellPanel(
                                                   fluidRow(column(2, textInput("ht_new_id", label = "ID:", placeholder = "ID")),
                                                            column(1, textInput("ht_new_number", label = "Number:", placeholder = "Number")),
                                                            column(3, textInput("ht_new_lastname", label = "Last name:", placeholder = "Last name")),
                                                            column(3, textInput("ht_new_firstname", label = "First name:", placeholder = "First name")),
                                                            column(2, selectInput("ht_new_role", label = "Role", choices = c("", "libero", "outside", "opposite", "middle", "setter", "unknown"))),
                                                            column(1, selectInput("ht_new_special", label = "Special", choices = c("", "L", "C")))),
                                                   fluidRow(column(3, offset = 9, actionButton("ht_add_player_button", "Add player")))
                                               ),
                                               uiOutput("ht_delete_player_ui")
                                               ),
                                      tabPanel("Visiting team",
                                               fluidRow(column(4, textInput("vt_edit_name", label = "Team name:", value = rdata$dvw$meta$teams$team[vtidx])),
                                                        column(4, textInput("vt_edit_id", label = "Team ID:", value = rdata$dvw$meta$teams$team_id[vtidx])),
                                                        column(4, textInput("vt_edit_coach", label = "Coach:", value = rdata$dvw$meta$teams$coach[vtidx])),
                                                        column(4, textInput("vt_edit_assistant", label = "Assistant:", value = rdata$dvw$meta$teams$assistant[vtidx]))),
                                               DT::dataTableOutput("vt_edit_team"),
                                               wellPanel(
                                                   fluidRow(column(2, textInput("vt_new_id", label = "ID:", placeholder = "ID")),
                                                            column(1, textInput("vt_new_number", label = "Number:", placeholder = "Number")),
                                                            column(3, textInput("vt_new_lastname", label = "Last name:", placeholder = "Last name")),
                                                            column(3, textInput("vt_new_firstname", label = "First name:", placeholder = "First name")),
                                                            column(2, selectInput("vt_new_role", label = "Role", choices = c("", "libero", "outside", "opposite", "middle", "setter", "unknown"))),
                                                            column(1, selectInput("vt_new_special", label = "Special", choices = c("", "L", "C")))),
                                                   fluidRow(column(3, offset = 9, actionButton("vt_add_player_button", "Add player")))
                                               ),
                                               uiOutput("vt_delete_player_ui")
                                               )
                                  )
                                  ))
        })
        htdata_edit <- reactiveVal(NULL)
        output$ht_edit_team <- DT::renderDataTable({
            if (is.null(htdata_edit())) htdata_edit(rdata$dvw$meta$players_h)
            if (!is.null(htdata_edit())) {
                cols_to_hide <- which(!names(htdata_edit()) %in% c("player_id", "number", "lastname", "firstname", "role", "special_role"))-1L ## 0-based because no row names
                cnames <- names(names_first_to_capital(htdata_edit()))
                DT::datatable(htdata_edit(), rownames = FALSE, colnames = cnames, selection = "single", editable = TRUE, options = list(lengthChange = FALSE, sDom = '<"top">t<"bottom">rlp', paging = FALSE, ordering = FALSE, columnDefs = list(list(targets = cols_to_hide, visible = FALSE))))
            } else {
                NULL
            }
        }, server = TRUE)
        ht_edit_team_proxy <- DT::dataTableProxy("ht_edit_team")
        htdata_display <- reactiveVal(NULL)
        output$ht_display_team <- DT::renderDataTable({
            if (is.null(htdata_display())) htdata_display(rdata$dvw$meta$players_h)
            if (!is.null(htdata_display())) {
                cols_to_hide <- which(!names(htdata_display()) %in% c("player_id", "number", "lastname", "firstname", "role", "special_role"))-1L ## 0-based because no row names
                cnames <- names(names_first_to_capital(htdata_display()))
                DT::datatable(htdata_display(), rownames = FALSE, colnames = cnames, selection = "single", editable = FALSE, options = list(lengthChange = FALSE, sDom = '<"top">t<"bottom">rlp', paging = FALSE, ordering = FALSE, columnDefs = list(list(targets = cols_to_hide, visible = FALSE))))
            } else {
                NULL
            }
        }, server = TRUE)
        ht_display_team_proxy <- DT::dataTableProxy("ht_display_team")
        observeEvent(input$ht_edit_team_cell_edit, {
            info <- input$ht_edit_team_cell_edit
            isolate(temp <- htdata_edit())
            temp[info$row, info$col+1L] <- DT::coerceValue(info$value, temp[[info$row, info$col+1L]]) ## no row names so +1 on col indices
            DT::replaceData(ht_edit_team_proxy, temp, resetPaging = FALSE, rownames = FALSE)
            htdata_edit(temp)
        })
        output$ht_delete_player_ui <- renderUI({
            if (!is.null(input$ht_edit_team_rows_selected)) {
                actionButton("ht_delete_player_button", "Delete selected player")
            } else {
                NULL
            }
        })
        observeEvent(input$ht_delete_player_button, {
            ridx <- input$ht_edit_team_rows_selected
            if (!is.null(ridx)) {
                temp <- htdata_edit()
                temp <- temp[-ridx, ]
                DT::replaceData(ht_edit_team_proxy, temp, resetPaging = FALSE, rownames = FALSE)
                htdata_edit(temp)
            }
        })
        observeEvent(input$ht_add_player_button, {
            chk <- list(input$ht_new_id, input$ht_new_number, input$ht_new_lastname, input$ht_new_firstname)
            if (!any(vapply(chk, is_nnn, FUN.VALUE = TRUE))) {
                try({
                    newrow <- tibble(number = as.numeric(input$ht_new_number), player_id = input$ht_new_id, lastname = input$ht_new_lastname, firstname = input$ht_new_firstname, role = if (nzchar(input$ht_new_role)) input$ht_new_role else NA_character_, special_role = if (nzchar(input$ht_new_special)) input$ht_new_special else NA_character_)
                    newrow$name <- paste(newrow$firstname, newrow$lastname)
                    temp <- bind_rows(htdata_edit(), newrow)
                    temp <- dplyr::arrange(temp, .data$number)
                    DT::replaceData(ht_edit_team_proxy, temp, resetPaging = FALSE, rownames = FALSE)
                    htdata_edit(temp)
                    ## clear inputs
                    updateTextInput(session, "ht_new_number", value = "")
                    updateTextInput(session, "ht_new_id", value = "")
                    updateTextInput(session, "ht_new_lastname", value = "")
                    updateTextInput(session, "ht_new_firstname", value = "")
                    updateSelectInput(session, "ht_new_role", selected = "")
                    updateSelectInput(session, "ht_new_special", selected = "")
                })
            }
        })
        vtdata_edit <- reactiveVal(NULL)
        output$vt_edit_team <- DT::renderDataTable({
            if (is.null(vtdata_edit())) vtdata_edit(rdata$dvw$meta$players_v)
            if (!is.null(vtdata_edit())) {
                cols_to_hide <- which(!names(vtdata_edit()) %in% c("player_id", "number", "lastname", "firstname", "role", "special_role"))-1L ## 0-based because no row names
                cnames <- names(names_first_to_capital(vtdata_edit()))
                DT::datatable(vtdata_edit(), rownames = FALSE, colnames = cnames, selection = "single", editable = TRUE, options = list(lengthChange = FALSE, sDom = '<"top">t<"bottom">rlp', paging = FALSE, ordering = FALSE, columnDefs = list(list(targets = cols_to_hide, visible = FALSE))))
            } else {
                NULL
            }
        }, server = TRUE)
        vt_edit_team_proxy <- DT::dataTableProxy("vt_edit_team")
        vtdata_display <- reactiveVal(NULL)
        output$vt_display_team <- DT::renderDataTable({
            if (is.null(vtdata_display())) vtdata_display(rdata$dvw$meta$players_v)
            if (!is.null(vtdata_display())) {
                cols_to_hide <- which(!names(vtdata_display()) %in% c("player_id", "number", "lastname", "firstname", "role", "special_role"))-1L ## 0-based because no row names
                cnames <- names(names_first_to_capital(vtdata_display()))
                DT::datatable(vtdata_display(), rownames = FALSE, colnames = cnames, selection = "single", editable = FALSE, options = list(lengthChange = FALSE, sDom = '<"top">t<"bottom">rlp', paging = FALSE, ordering = FALSE, columnDefs = list(list(targets = cols_to_hide, visible = FALSE))))
            } else {
                NULL
            }
        }, server = TRUE)
        vt_display_team_proxy <- DT::dataTableProxy("vt_display_team")
        observeEvent(input$vt_edit_team_cell_edit, {
            info <- input$vt_edit_team_cell_edit
            isolate(temp <- vtdata_edit())
            temp[info$row, info$col+1L] <- DT::coerceValue(info$value, temp[[info$row, info$col+1L]]) ## no row names so +1 on col indices
            DT::replaceData(vt_edit_team_proxy, temp, resetPaging = FALSE, rownames = FALSE)
            vtdata_edit(temp)
        })
        output$vt_delete_player_ui <- renderUI({
            if (!is.null(input$vt_edit_team_rows_selected)) {
                actionButton("vt_delete_player_button", "Delete selected player")
            } else {
                NULL
            }
        })
        observeEvent(input$vt_delete_player_button, {
            ridx <- input$vt_edit_team_rows_selected
            if (!is.null(ridx)) {
                temp <- vtdata_edit()
                temp <- temp[-ridx, ]
                DT::replaceData(vt_edit_team_proxy, temp, resetPaging = FALSE, rownames = FALSE)
                vtdata_edit(temp)
            }
        })
        observeEvent(input$vt_add_player_button, {
            chk <- list(input$vt_new_id, input$vt_new_number, input$vt_new_lastname, input$vt_new_firstname)
            if (!any(vapply(chk, is_nnn, FUN.VALUE = TRUE))) {
                try({
                    newrow <- tibble(number = as.numeric(input$vt_new_number), player_id = input$vt_new_id, lastname = input$vt_new_lastname, firstname = input$vt_new_firstname, role = if (nzchar(input$vt_new_role)) input$vt_new_role else NA_character_, special_role = if (nzchar(input$vt_new_special)) input$vt_new_special else NA_character_)
                    newrow$name <- paste(newrow$firstname, newrow$lastname)
                    temp <- bind_rows(vtdata_edit(), newrow)
                    temp <- dplyr::arrange(temp, .data$number)
                    DT::replaceData(vt_edit_team_proxy, temp, resetPaging = FALSE, rownames = FALSE)
                    vtdata_edit(temp)
                    ## clear inputs
                    updateTextInput(session, "vt_new_number", value = "")
                    updateTextInput(session, "vt_new_id", value = "")
                    updateTextInput(session, "vt_new_lastname", value = "")
                    updateTextInput(session, "vt_new_firstname", value = "")
                    updateSelectInput(session, "vt_new_role", selected = "")
                    updateSelectInput(session, "vt_new_special", selected = "")
                })
            }
        })
        
        
        ## starting line up editing
        observeEvent(input$edit_lineup_button, {
            editing$active <- "change starting lineup"
            htidx <- which(rdata$dvw$meta$teams$home_away_team %eq% "*") ## should always be 1
            vtidx <- which(rdata$dvw$meta$teams$home_away_team %eq% "a") ## should always be 2
            showModal(modalDialog(title = "Edit starting line up", size = "l", footer = tags$div(actionButton("edit_commit", label = "Update teams lineups"), actionButton("edit_cancel", label = "Cancel")),
                                  tabsetPanel(
                                      tabPanel("Home team",
                                               tags$style("#ht_display_team {border: 2px solid #bfefff;}"),
                                               DT::dataTableOutput("ht_display_team"),
                                               wellPanel(
                                                   fluidRow(
                                                       column(1, textInput("ht_set_number", label = "Set", placeholder = "Set number")),
                                                       column(1, textInput("ht_P1", label = "P1", placeholder = "P1")),
                                                       column(1, textInput("ht_P2", label = "P2", placeholder = "P2")),
                                                       column(1, textInput("ht_P3", label = "P3", placeholder = "P3")),
                                                       column(1, textInput("ht_P4", label = "P4", placeholder = "P4")),
                                                       column(1, textInput("ht_P5", label = "P5", placeholder = "P5")),
                                                       column(1, textInput("ht_P6", label = "P6", placeholder = "P6"))),
                                                   fluidRow(
                                                       column(1, textInput("ht_setter", label = "Setter", placeholder = "Setter")),
                                                       column(1, textInput("ht_libero", label = "Libero", placeholder = "Libero"))
                                                       ),
                                                   style = "background: #bfefff"
                                               ),
                                               uiOutput("ht_delete_player_ui")
                                      ),
                                      tabPanel("Visiting team",
                                               tags$style("#vt_display_team {border: 2px solid #bcee68;}"),
                                               DT::dataTableOutput("vt_display_team"),
                                               wellPanel(
                                                   fluidRow(
                                                       column(1, textInput("vt_set_number", label = "Set", placeholder = "Set number")),
                                                       column(1, textInput("vt_P1", label = "P1", placeholder = "P1")),
                                                       column(1, textInput("vt_P2", label = "P2", placeholder = "P2")),
                                                       column(1, textInput("vt_P3", label = "P3", placeholder = "P3")),
                                                       column(1, textInput("vt_P4", label = "P4", placeholder = "P4")),
                                                       column(1, textInput("vt_P5", label = "P5", placeholder = "P5")),
                                                       column(1, textInput("vt_P6", label = "P6", placeholder = "P6"))),
                                                   fluidRow(
                                                       column(1, textInput("vt_setter", label = "Setter", placeholder = "Setter")),
                                                       column(1, textInput("vt_libero", label = "Libero", placeholder = "Libero"))
                                                   ),
                                                   style = "background: #bcee68"
                                               ),
                                               uiOutput("vt_delete_player_ui")
                                      )
                                  )
            ))
        })

        ## General help
        observeEvent(input$general_help, introjs(session, options = list("nextLabel"="Next", "prevLabel"="Previous", "skipLabel"="Skip")))

        ## height of the video player element
        vo_height <- reactiveVal("auto")
        observe({
            if (app_data$with_video) {
                if (!is.null(input$dv_height) && as.numeric(input$dv_height) > 0) {
                    this <- as.numeric(input$dv_height)
                    vo_height(this)
                    dojs(paste0("document.getElementById('video_overlay').style.height = '", this, "px';"))
                    dojs(paste0("document.getElementById('video_overlay_img').style.height = '", this, "px';"))
                } else {
                    vo_height("auto")
                    dojs(paste0("document.getElementById('video_overlay').style.height = '400px';"))
                    dojs(paste0("document.getElementById('video_overlay_img').style.height = '400px';"))
                }
            }
        })
        ## width of the video player element
        vo_width <- reactiveVal("auto")
        observe({
            if (app_data$with_video) {
                if (!is.null(input$dv_width) && as.numeric(input$dv_width) > 0) {
                    this <- as.numeric(input$dv_width)
                    vo_width(this)
                    dojs(paste0("document.getElementById('video_overlay').style.width = '", this, "px';"))
                    dojs(paste0("document.getElementById('video_overlay_img').style.width = '", this, "px';"))
                } else {
                    vo_width("auto")
                    dojs(paste0("document.getElementById('video_overlay').style.width = '600px';"))
                    dojs(paste0("document.getElementById('video_overlay_img').style.width = '600px';"))
                }
            }
        })
        ## height of the video player container, use as negative vertical offset on the overlay element
        observe({
            if (app_data$with_video) {
                if (!is.null(input$vo_voffset) && as.numeric(input$vo_voffset) > 0) {
                    dojs(paste0("document.getElementById('currentevent').style.marginTop = '-", input$vo_voffset - 50, "px';"))
                    dojs(paste0("document.getElementById('video_overlay').style.marginTop = '-", input$vo_voffset, "px';"))
                    dojs(paste0("document.getElementById('video_overlay_img').style.marginTop = '-", input$vo_voffset, "px';"))
                } else {
                    dojs("document.getElementById('currentevent').style.marginTop = '-50px';")
                    dojs("document.getElementById('video_overlay').style.marginTop = '0px';")
                    dojs("document.getElementById('video_overlay_img').style.marginTop = '0px';")
                }
            }
        })
        ## video overlay
        output$show_overlay_ui <- renderUI(if (!is.null(app_data$court_ref)) checkboxInput("show_overlay", "Show court overlay?", value = FALSE) else NULL)

        gg_tight <- list(theme(legend.position = "none", panel.background = element_rect(fill = "transparent", colour = NA), plot.background = element_rect(fill = "transparent", color = NA), panel.grid.major = element_blank(), panel.grid.minor = element_blank(), panel.spacing = unit(0, "null"), plot.margin = rep(unit(0, "null"), 4), axis.ticks = element_blank(), axis.ticks.length = unit(0, "null"), axis.text.x = element_blank(), axis.text.y = element_blank(), axis.title.x = element_blank(), axis.title.y = element_blank()), scale_x_continuous(limits = c(0, 1), expand = c(0, 0)), scale_y_continuous(limits = c(0, 1), expand = c(0, 0)))
        overlay_images <- reactiveVal(list(zones = NULL, cones_L = NULL, cones_M = NULL, cones_R = NULL, image_dir = NULL))
        ## generate static overlay images
        observe({
            blah <- list(input$dv_height, input$dv_width) ## reactive to these ## previously also included rdata$dvw
            isolate(img_dir <- overlay_images()$image_dir)
            if (is.null(img_dir)) {
                img_dir <- tempfile()
                dir.create(img_dir)
                shiny::addResourcePath(prefix = "courtimg", img_dir)
            } else {
            }
            if (isTRUE(input$show_overlay) && !is.null(app_data$court_ref) && !is.null(input$dv_width) && as.numeric(input$dv_width) > 0 && !is.null(input$dv_height) && as.numeric(input$dv_height) > 0) {
                overlay_images(list(zones = gen_overlay_img(polytype = "zones", width = input$dv_width, height = input$dv_height, outdir = img_dir),
                                    cones_L = gen_overlay_img(polytype = "cones", sz = "L", width = input$dv_width, height = input$dv_height, outdir = img_dir),
                                    cones_M = gen_overlay_img(polytype = "cones", sz = "M", width = input$dv_width, height = input$dv_height, outdir = img_dir),
                                    cones_R = gen_overlay_img(polytype = "cones", sz = "R", width = input$dv_width, height = input$dv_height, outdir = img_dir),
                                    image_dir = img_dir))
            } else {
                overlay_images(list(zones = NULL, cones_L = NULL, cones_M = NULL, cones_R = NULL, image_dir = img_dir))
            }
        })
        gen_overlay_img <- function(polytype, sz, width, height, outdir) {
            ## outer lines
            cxy <- data.frame(x = c(rep(0.5, 3), 0.5, 3.5), xend = c(rep(3.5, 3), 0.5, 3.5),
                              y = c(0.5, 3.5, 6.5, 0.5, 0.5),
                              yend = c(0.5, 3.5, 6.5, 6.5, 6.5),
                              width = 1.0)
            ## serve zones
            szlen <- 0.25 ## length of serve zone lines - make this at least 1 once clipping is implemented
            sxy <- data.frame(x = c(0.5, 1.1, 1.7, 2.3, 2.9, 3.5),
                              xend = c(0.5, 1.1, 1.7, 2.3, 2.9, 3.5),
                              y = rep(0.5, 6), yend = rep(0.5-szlen, 6), width = 0.75)
            cxy <- bind_rows(cxy, sxy)
            sxy[, c("x", "y")] <- dv_flip_xy(sxy[, c("x", "y")])
            sxy[, c("xend", "yend")] <- dv_flip_xy(sxy[, c("xend", "yend")])
            cxy <- bind_rows(cxy, sxy)
            if (polytype %eq% "cones") {
                sznum <- if (sz == "L") 4L else if (sz == "M") 3L else if (sz == "R") 2L else NA_integer_
                polyxy <- dv_cone_polygons(zone = sz, end = "upper")
                Nc <- max(polyxy$cone_number)
                polyxy$cone_number <- paste0(polyxy$cone_number, "U")
                polyxy <- bind_rows(polyxy, mutate(dv_cone_polygons(zone = sz, end = "lower"), cone_number = paste0(.data$cone_number, "L")))
                ## labels
                labxy <- mutate(dv_cone2xy(sznum, end_cones = seq_len(Nc), end = "upper", xynames = c("x", "y")), label = row_number())
                labxy <- bind_rows(labxy, mutate(dv_cone2xy(sznum, end_cones = seq_len(Nc), end = "lower", xynames = c("x", "y")), label = row_number()))
            } else {
                ## 3m and other zone lines
                cxy <- bind_rows(cxy, data.frame(x = c(0.5, 0.5, 0.5, 0.5, 1.5, 2.5), xend = c(3.5, 3.5, 3.5, 3.5, 1.5, 2.5),
                                                 y = c(2.5, 4.5, 1.5, 5.5, 0.5, 0.5), yend = c(2.5, 4.5, 1.5, 5.5, 6.5, 6.5),
                                                 width = 0.75))
                polyxy <- NULL
                labxy <- data.frame(x = rep(c(1, 2, 3), 6), y = as.numeric(matrix(1:6, nrow = 3, ncol = 6, byrow = TRUE)),
                                    label = c(5, 6, 1, 7, 8, 9, 4, 3, 2, 2, 3, 4, 9, 8, 7, 1, 6, 5))
            }
            cxy[, c("x", "y")] <- ovideo::ov_transform_points(cxy[, c("x", "y")], ref = app_data$court_ref, direction = "to_image")
            cxy[, c("xend", "yend")] <- setNames(ovideo::ov_transform_points(cxy[, c("xend", "yend")], ref = app_data$court_ref, direction = "to_image"), c("xend", "yend"))
            labxy[, c("x", "y")] <- ovideo::ov_transform_points(labxy[, c("x", "y")], ref = app_data$court_ref, direction = "to_image")
            if (!is.null(polyxy)) {
                polyxy[, c("x", "y")] <- ovideo::ov_transform_points(polyxy[, c("x", "y")], ref = app_data$court_ref, direction = "to_image")
            }
            p <- ggplot(cxy, aes_string("x", "y", xend = "xend", yend = "yend", size = "width")) + geom_segment(color = "blue") + gg_tight + scale_size_continuous(range = c(0.5, 1.0))
            if (!is.null(polyxy)) p <- p + geom_polygon(data = polyxy, aes_string(x = "x", y = "y", group = "cone_number"), inherit.aes = FALSE, color = "blue", fill = NA)
            p + geom_label(data = labxy, aes_string(x = "x", y = "y", label = "label"), inherit.aes = FALSE, color = "blue", hjust = 0.5, vjust = 0.5)#, size = 1.5)
            fname <- tempfile(tmpdir = outdir, fileext = ".png")
            ggplot2::ggsave(fname, p, device = "png", width = width/100, height = height/100, dpi = 100, units = "in", bg = "transparent")
            ##message(fname)
            basename(fname)
        }

        observe({
            if (!app_data$with_video || !isTRUE(input$show_overlay)) {
                dojs(paste0("document.getElementById('video_overlay_img').setAttribute('src', '');"))
            } else {
                ridx <- playslist_current_row()
                polytype <- "zones"
                if (!is.null(ridx) && !is.na(ridx)) {
                    try({
                        if (rdata$dvw$plays$skill[ridx] %eq% "Attack" && rdata$dvw$meta$match$zones_or_cones %eq% "C" && rdata$dvw$plays$start_zone[ridx] %in% 1:9) polytype <- "cones"
                    })
                }
                if (polytype %eq% "cones") {
                    sz <- if (rdata$dvw$plays$start_zone[ridx] %in% c(4, 7, 5)) "L" else if (rdata$dvw$plays$start_zone[ridx] %in% c(3, 8, 6)) "M" else "R"
                    polytype <- paste0("cones_", sz)
                }
                if (!is.null(overlay_images()[[polytype]])) {
                    dojs(paste0("document.getElementById('video_overlay_img').setAttribute('src', '/courtimg/", overlay_images()[[polytype]], "');"))
                } else {
                    dojs(paste0("document.getElementById('video_overlay_img').setAttribute('src', '');"))
                }
            }
        })

        ## other overlay plotting can be done here?
        observe({
            output$video_overlay <- renderPlot({
                if (!app_data$with_video) return(NULL)
                ## test - red diagonal line across the overlay plot
                ##ggplot(data.frame(x = c(0, 1), y = c(0, 1)), aes_string("x", "y")) + geom_path(color = "red") + gg_tight
                ## for tagging, need to plot SOMETHING else we don't get correct coordinates back
                this <- selected_event()
                ok <- FALSE
                try({
                    if (court_inset$ball_coords() && !is.null(this) && nrow(this) == 1 && !is.null(app_data$court_ref) && !is.na(this$start_coordinate_x) && !is.na(this$end_coordinate_x)) {
                        thisxy <- data.frame(x = as.numeric(this[, c("start_coordinate_x", "mid_coordinate_x", "end_coordinate_x")]),
                                             y = as.numeric(this[, c("start_coordinate_y", "mid_coordinate_y", "end_coordinate_y")]))
                        thisxy <- setNames(ovideo::ov_transform_points(thisxy, ref = app_data$court_ref, direction = "to_image"), c("image_x", "image_y"))
                        p <- ggplot(mapping = aes_string("image_x", "image_y")) + geom_point(data = thisxy[1, ], shape = 16, col = "green", size = 5) +
                            geom_point(data = thisxy[3, ], shape = 16, col = "red", size = 5) +
                            geom_path(data = na.omit(thisxy), arrow = arrow(length = unit(0.01, "npc"), ends = "last"))
                        ok <- TRUE
                    }
                }, silent = TRUE)
                if (!ok) {
                    p <- ggplot(data.frame(x = c(0, 1), y = c(0, 1)), aes_string("x", "y"))
                }
                p + gg_tight
                ##NULL
            }, bg = "transparent", width = vo_width(), height = vo_height())
        })

        vid_to_crt <- function(obj) {
            courtxy <- data.frame(x = NA_real_, y = NA_real_)
            if (!is.null(app_data$court_ref)) {
                vxy <- c(obj$x, obj$y)
                if (length(vxy) == 2 && !any(is.na(vxy))) {
                    courtxy <- ovideo::ov_transform_points(vxy[1], vxy[2], ref = app_data$court_ref, direction = "to_court")
                }
            }
            courtxy
        }
        crt_to_vid <- function(obj) {
            imagexy <- data.frame(image_x = NA_real_, image_y = NA_real_)
            if (!is.null(app_data$court_ref)) {
                vxy <- c(obj$x, obj$y)
                if (length(vxy) == 2 && !any(is.na(vxy))) {
                    imagexy <- setNames(ovideo::ov_transform_points(vxy[1], vxy[2], ref = app_data$court_ref, direction = "to_image"), c("image_x", "image_y"))
                }
            }
            imagexy
        }

        ## single click the video to register a tag location, or starting ball coordinates
        observeEvent(input$video_click, {
            if (app_data$with_video) {
                courtxy <- vid_to_crt(input$video_click)
                court_inset$add_to_click_queue(courtxy)
            }
        })
    }
}
openvolley/ovscout documentation built on June 4, 2023, 2:07 a.m.