inst/shiny/mapApp/app.R

# vim:textwidth=128:expandtab:shiftwidth=4:softtabstop=4

plotCounter <- 1L

# Set debug to FALSE to turn off these program-flow messages (although some are also
# controlled by 'debug' passed as an argument to functions).
debug <- 1L
dmsg <- function(...)
{
    if (debug)
        message(...)
}

viewDefaults <- list("core", "deep", "bgc")
colDefaults <- list(core="#F5C710", bgc="#05f076", deep="#CD0BBC")

# Default start and end times. We must extend present time because
# it gets stored as a date, so otherwise we miss all profiles taken "today".
dayStart <- function(t)
{
    t <- as.POSIXlt(t, tz="UTC")
    t$hour <- 0L
    t$min <- 0L
    t$sec <- 0.0
    t
}
dayEnd <- function(t)
{
    t <- as.POSIXlt(as.POSIXct(t, tz="UTC") + 86400, tz="UTC")
    t$hour <- 0L
    t$min <- 0L
    t$sec <- 0.0
    t
}
secondsPerDay <- 86400
now <- Sys.time()
endTime <- dayEnd(now)
startTime <- dayStart(now - 10 * secondsPerDay)
#checking dput(now)
#checking dput(startTime)
#checking dput(endTime)
#checking endTime-startTime

stateStack <- list()
pushState <- function(state, debug=0L)
{
    argoFloatsDebug(debug, "pushState() { \n", style="bold", unindent=1)
    nss <- sizeState()
    if (nss == 0L || !identical(state, stateStack[[nss]])) {
        stateStack[[nss + 1L]] <<- state
        argoFloatsDebug(debug, "pushed state, yielding a new stack length of", nss+1, "\n")
        if (debug > 0L)
            printState()
    } else {
        argoFloatsDebug(debug, "ignoring duplicate state, retaining old stack of length", nss, "\n")
    }
    argoFloatsDebug(debug, "} # pushState()\n", style="bold", unindent=1)
}

popState <- function(debug=0L)
{
    argoFloatsDebug(debug, "popState() {\n", style="bold", unindent=1)
    nss <- sizeState()
    if (nss > 1L) {
        stateStack[[nss]] <<- NULL
        argoFloatsDebug(debug, "reduced stack length to", nss-1L, "\n")
    } else {
        argoFloatsDebug(debug, "leaving stack alone, because it is already empty\n")
    }
    #printState()
    argoFloatsDebug(debug, "} # popState()\n", style="bold", unindent=1)
}

topState <- function()
{
    argoFloatsDebug(debug, "topState() {\n", style="bold", unindent=1)
    argoFloatsDebug(debug, "stack length=", length(stateStack), "\n", sep="")
    nss <- sizeState()
    argoFloatsDebug(debug, "} # topState()\n\n", style="bold", unindent=1)
    if (nss > 0L) stateStack[[nss]] else NULL
}
sizeState <- function()
{
    length(stateStack)
}
printState <- function(debug=0L)
{
    argoFloatsDebug(debug, "printState() {\n\n", style="bold", unindent=1)
    if (debug) {
        nss <- sizeState()
        if (nss > 0L) {
            argoFloatsDebug(debug , "stateStack holds", nss, "elements\n")
            if (FALSE) {
                argoFloatsDebug(debug, "top element:\n")
                topOfStack <- stateStack[[nss]]
                for (name in sort(names(topOfStack)))
                    argoFloatsDebug(debug, name, ": ",format(paste(topOfStack[[name]], collapse=" ")))
            }
        } else {
            argoFloatsDebug(debug, "stateStack is empty\n")
        }
    } else {
        argoFloatsDebug(debug, "skipping printing since debug=", debug, "\n")
    }
    argoFloatsDebug(debug, "} # printState()\n\n", style="bold", unindent=1)
}


pi180 <- pi / 180                      # degree/radian conversion factor

keyPressHelp <- "<ul>
<li> '<b>d</b>': toggle <b>d</b>ebugging flag</li>
<li> '<b>h</b>': hold active hover message (press <b>h</b> again to undo)</li>
<li> '<b>r</b>': <b>r</b>eset to initial state</li>
<li> '<b>q</b>': indicate when finished polygon</li>
<li> '<b>u</b>': <b>u</b>ndo previous actions</li>
<li> '<b>0</b>': unzoom an area</li>
<li> '<b>?</b>': display this message</li>
</ul>"

overallHelp <- "<p>mapApp() responds to keystroke actions and GUI actions.</p><p>The permitted <u>keystroke actions</u> will be shown in a pop-up window if the <b>?</b> key is pressed. There are keys for zooming in and out, for moving the focus region through space and time, for controlling updates to an information box that displays mouse location and aspects of a nearby float, undoing previous actions, and turning on a developer mode in which information about processing is printed to the R console.</p><p>The <u>GUI actions</u> are reasonably self-explanatory. On the <i>Map tab</i>, users may enter values in the \"Start\" and \"End\" boxes to set the time range of the display, or empty either box to use the data range. The checkboxes of the \"View\" grouping may be used to choose whether to show 'Core', 'Deep' or 'BGC' data, whether to draw a high-resolution coastline, whether to draw connecting line segments to indicate the path of individual floats, and whether to indicate water depth using contour lines. If a path is displayed, there are options to highlight its start and end points of the path in the selected region, or to hide all points. The focus region may be selected by pressing the mouse at one location, sliding it to a new location, and then releasing it. Double-clicking on a particular float location creates a pop-up window that provides information on that profile. There is a way to focus on an individual float, to the exclusion of others.  Experimenting with the interface will reveal other capabilities; for example, it is worth exploring the <i>Settings tab</i>, which provides control over several aesthetic properties.<p>A text box above the plot shows the mouse position in longitude and latitude as well as information about the nearest profile, if it is within 100km of the mouse location (typing <b>h</b> toggles a setting that causes this information to track the mouse).</p><p>The \"Code\" button brings up a window showing R code that will approximate the view shown in the app, and that hints at some other operations that might be useful in analysis.</p><p>For more details, type <tt>?argoFloats::mapApp</tt> in an R console.</p>"


uiMapApp <- shiny::fluidPage(
    # Header Panel
    shiny::headerPanel(title="", windowTitle="argoFloats mapApp"),
    shiny::tags$script('$(document).on("keypress", function (e) { Shiny.onInputChange("keypress", e.which); Shiny.onInputChange("keypressTrigger", Math.random()); });'),
    #style="text-indent:1em; line-height:1.2; background:#e6f3ff; .btn{ padding: 2px 9px; }; .form-group { margin-top: 0; margin-bottom: 0 }",
    # margin-top works, but not sure if either pre{} or formgroup{} work.
    style="text-indent:1em; line-height:1.2; background:#e6f3ff; margin-top: -2ex; pre { line-height: 0.5; }; .form-group { margin-top: 3px; margin-bottom: 3px;};",
    shiny::fluidRow(
        shiny::uiOutput(outputId="UIwidget1")),
    shiny::fluidRow(
        shiny::uiOutput(outputId="UIwidget2")),
    shiny::fluidRow(
        shiny::column(9, shiny::uiOutput(outputId="UIview"))),
    shiny::fluidRow(
        shiny::uiOutput(outputId="UItrajectory")),
    shiny::fluidRow(
        shiny::uiOutput(outputId="UIinfo")),

    # Main Panel
    shiny::mainPanel(shiny::tabsetPanel(type="tab",
            shiny::tabPanel("Map", value=1),
            shiny::tabPanel("Settings", value=2,
                shiny::tabsetPanel(shiny::tabPanel("Core", value=3, selected=TRUE),
                    shiny::tabPanel("BGC", value=4),
                    shiny::tabPanel("Deep", value=5),
                    id="settab")),
            id="tabselected"),
        shiny::uiOutput(outputId="UIcoreTab"),
        shiny::uiOutput(outputId="UIbgcTab"),
        shiny::uiOutput(outputId="UIdeepTab")),

    shiny::conditionalPanel("input.tabselected!=2",
        shiny::fluidRow(shiny::plotOutput("plotMap",
                hover=shiny::hoverOpts("hover"),
                dblclick=shiny::dblclickOpts("dblclick"),
                click=shiny::clickOpts("click"),
                brush=shiny::brushOpts("brush", delay=2000, resetOnNew=TRUE)))))

serverMapApp <- function(input, output, session)
{
    lastHoverMessage <- "" # used with 'h' keystroke
    age <- shiny::getShinyOption("age")
    destdir <- shiny::getShinyOption("destdir")
    argoServer <- shiny::getShinyOption("argoServer")
    colLand <- shiny::getShinyOption("colLand")
    debug <<- shiny::getShinyOption("debug")
    if (!requireNamespace("shiny", quietly=TRUE))
        stop("must install.packages('shiny') for mapApp() to work")
    if (!requireNamespace("colourpicker", quietly=TRUE))
        stop("must install.packages('colourpicker') for mapApp() to work")
    # State: reactive
    state <- shiny::reactiveValues(
        begin=TRUE,
        xlim=c(-180, 180),
        ylim=c(-90, 90),
        polyDone=FALSE,
        polygon=FALSE,
        data=NULL,
        startTime=startTime,
        endTime=endTime,
        action=NULL,
        focusID=NULL,
        view=viewDefaults,
        Ccolour=colDefaults$core,
        Csymbol=21,
        Csize=0.9,
        Cborder="black",
        CPcolour=colDefaults$core,
        CPwidth=1.4,
        Bcolour=colDefaults$bgc,
        Bsymbol=21,
        Bsize=0.9,
        Bborder="black",
        BPcolour=colDefaults$bgc,
        BPwidth=1.4,
        Dcolour=colDefaults$deep,
        Dsymbol=21,
        Dsize=0.9,
        Dborder="black",
        DPcolour=colDefaults$deep,
        DPwidth=1.4,
        hoverIsPasted=FALSE)
    pushState(isolate(reactiveValuesToList(state)))
    # Depending on whether 'hires' selected, 'coastline' will be one of the following two version:
    data("coastlineWorld", package="oce", envir=environment())
    coastlineWorld <- get("coastlineWorld")
    data("coastlineWorldFine", package="ocedata", envir=environment())
    coastlineWorldFine <- get("coastlineWorldFine")
    # Depending on whether 'hires' selected, 'topo' wil be one of the following two version:
    data("topoWorld", package="oce", envir=environment())
    topoWorld <- get("topoWorld")
    if (file.exists("topoWorldFine.rda")) {
        load("topoWorldFine.rda")
    } else {
        topoWorldFine <- topoWorld
    }
    argoFloatsDebug(debug, "mapApp() is about to check the cache\n")
    if (argoFloatsIsCached("argo", debug=debug-1L)) {
        notificationId <- shiny::showNotification("Using argo data that were cached temporarily in R-session memory\n")
        argo <- argoFloats::argoFloatsGetFromCache("argo", debug=debug-1L)
    } else {
        # Get core and BGC data.
        notificationId <- shiny::showNotification("Step 1/3: Getting \"core\" Argo index, either by downloading new data or using data in \"destdir\".  This may take a minute or two.", type="message", duration=NULL)
        i <<- argoFloats::getIndex(age=age, destdir=destdir, server=argoServer, debug=debug)
        argoFloatsDebug(debug, "getIndex() returned", if (is.null(i)) "NULL" else "not NULL", "\n")
        shiny::removeNotification(notificationId)
        notificationId <- shiny::showNotification("Step 2/3: Getting \"BGC\" Argo index, either by downloading new data or using cached data.  This may take a minute or two.", type="message", duration=NULL)
        iBGC <<- argoFloats::getIndex("bgc", age=age, destdir=destdir, server=argoServer, debug=debug)
        m <<- merge(i, iBGC)
        shiny::removeNotification(notificationId)
        # Combine core and BGC data.  This relies on the fact that every BGC ID is
        # also a core ID.  Here is sample code that proves it:
        #
        # library(argoFloats)
        # i <- getIndex()
        # b <- getIndex("bgc")
        # iid <- i[["ID"]]
        # bid <- b[["ID"]]
        # stopifnot(sum(bid %in% iid) == length(bid))
        #
        # (The above works without signalling an error.  FYI, there were 231513 entries
        # in bid.)

        notificationId <- shiny::showNotification("Step 3/3: Computing \"Deep\" Argo index. This may take a minute or two.", type="message", duration=NULL)
        ID <- i[["ID"]]
        cycle <- i[["cycle"]]
        lon <- i[["longitude"]]
        lat <- i[["latitude"]]
        profilerType <- i[["profiler_type"]]
        institution <- i[["institution"]]
        file <- i[["file"]]
        idBGC <- unique(iBGC[["ID"]])
        n <- length(ID)
        type <- rep("core", n)
        type[ID %in% idBGC] <- "bgc"
        #> ## This method took 0.44s (DE 2021-01-11), whereas the next one
        #> ## took 0.027s.
        #> type[grepl("849|862|864", i@data$index$profiler_type)] <- "deep"
        type[("849" == i@data$index$profiler_type)] <- "deep"
        type[("862" == i@data$index$profiler_type)] <- "deep"
        type[("864" == i@data$index$profiler_type)] <- "deep"
        argo <- data.frame(time=i[["date"]], ID=ID, cycle=cycle, longitude=lon, latitude=lat, type=type, profilerType=profilerType, institution=institution, file=file)
        argo$longitude <- ifelse(argo$longitude > 180, argo$longitude - 360, argo$longitude)
        ok <- is.finite(argo$time)
        argo <- argo[ok, ]
        argoFloatsStoreInCache("argo", argo, debug=debug-1L)
    }

    ok <- is.finite(argo$longitude)
    argo <- argo[ok, ]
    ok <- is.finite(argo$latitude)
    argo <- argo[ok, ]
    visible <- rep(TRUE, length(argo$lon)) # vector indicating whether to keep any given cycle.

    # Functions used to prevent off-world points
    pinlat <- function(lat)
        ifelse(lat < -90, -90, ifelse(90 < lat, 90, lat))
    pinlon <- function(lon)
        ifelse(lon < -180, -180, ifelse(180 < lon, 180, lon))

    output$UIview <- shiny::renderUI({
        if (argoFloatsIsCached("argo", debug=debug-1L) && input$tabselected %in% c(1)) {
            shiny::removeNotification(notificationId)
            shiny::checkboxGroupInput("view",
                label="View",
                choiceNames=list(
                    shiny::tags$span("Core",style=paste0('font-weight:bold; color:#',
                            paste(as.raw(as.vector(col2rgb(ifelse(state$Ccolour == "default",
                                                colDefaults$core,
                                                state$Ccolour)))),
                                collapse=""))),
                    shiny::tags$span("Deep",
                        style=paste0('font-weight:bold; color:#',
                            paste(as.raw(as.vector(col2rgb(ifelse(state$Dcolour == "default",
                                                colDefaults$deep,
                                                state$Dcolour)))),
                                collapse=""))),
                    shiny::tags$span("BGC",
                        style=paste0('font-weight:bold; color:#',
                            paste(as.raw(as.vector(col2rgb(ifelse(state$Bcolour == "default",
                                                colDefaults$bgc,
                                                state$Bcolour)))),
                                collapse=""))),
                    shiny::tags$span("HiRes", style="color: black;"),
                    shiny::tags$span("Topo", style="color: black;"),
                    shiny::tags$span("Contour", style="color:black;"),
                    shiny::tags$span("Path", style="color:black;")),
                choiceValues=list("core", "deep", "bgc", "hires", "topo", "contour", "path"),
                selected=state$view,
                #? selected=viewDefaults,
                inline=TRUE)
        }
    })

    output$UIcoreTab <- shiny::renderUI({
        if (input$tabselected %in% c(2) && input$settab %in% c(3)) {
            shiny::mainPanel(
                shiny::fluidRow(
                    shiny::div(style="color:black; font-weight:bold; margin-bottom: 10px;",
                        hr("Symbol Properties"))),
                shiny::fluidRow(
                    shiny::column(3,
                        shiny::div(style="margin-bottom: 10px;",
                            shiny::actionButton("CsymbolGallery", "Symbol Gallery")))),
                shiny::fluidRow(
                    shiny::column(3,
                        colourpicker::colourInput("Ccolour", "Colour", state$Ccolour)),
                    shiny::column(2, shiny::numericInput("Csymbol", "Type", value=state$Csymbol, min=0, max=25)),
                    shiny::column(3, shiny::sliderInput("Csize", "Size", min=0, max=1, value=state$Csize, step=0.05))),
                shiny::fluidRow(
                    if (state$Csymbol == 21) {
                        shiny::column(3,
                            colourpicker::colourInput("Cborder", "Border Colour", value=state$Cborder))}),
                shiny::fluidRow(
                    shiny::div(style="color:black; font-weight:bold; margin-bottom: 10px;",
                        hr("Path Properties"))),
                shiny::fluidRow(
                    shiny::column(3,
                        colourpicker::colourInput("CPcolour", "Colour", state$CPcolour)),
                    shiny::column(3, shiny::sliderInput("CPwidth", "Width", min=0.5, max=6, value=state$CPwidth, step=0.5))))
        }
    })

    output$UIbgcTab <- shiny::renderUI({
        if (input$tabselected %in% c(2) && input$settab %in% c(4)) {
            shiny::mainPanel(
                shiny::fluidRow(
                    shiny::div(style="color:black; font-weight:bold; margin-bottom: 10px;",
                        hr("Symbol Properties"))),
                shiny::fluidRow(
                    shiny::column(3,
                        shiny::div(style="margin-bottom: 10px;",
                            shiny::actionButton("BsymbolGallery", "Symbol Gallery")))),
                shiny::fluidRow(
                    shiny::column(3,
                        colourpicker::colourInput("Bcolour", "Colour", state$Bcolour)),
                    shiny::column(2, shiny::numericInput("Bsymbol", "Type", value=state$Bsymbol, min=0, max=25)),
                    shiny::column(3, shiny::sliderInput("Bsize", "Size", min=0, max=1, value=state$Bsize, step=0.05))),
                shiny::fluidRow(
                    if (state$Bsymbol == 21) {
                        shiny::column(3,
                            colourpicker::colourInput("Bborder", "Border Colour", value=state$Bborder))}),
                    shiny::fluidRow(
                    shiny::div(style="color:black; font-weight:bold; margin-bottom: 10px;",
                        hr("Path Properties"))),
                shiny::fluidRow(
                    shiny::column(3,
                        colourpicker::colourInput("BPcolour", "Colour", state$BPcolour)),
                    shiny::column(3, shiny::sliderInput("BPwidth", "Width", min=0.5, max=6, value=state$BPwidth, step=0.5))))
        }
    })

    output$UIdeepTab <- shiny::renderUI({
        if (input$tabselected %in% c(2) && input$settab %in% c(5)) {
        shiny::mainPanel(
            shiny::fluidRow(
                shiny::div(style="color:black; font-weight:bold; margin-bottom: 10px;",
                    hr("Symbol Properties"))),
            shiny::fluidRow(
                shiny::column(3,
                    shiny::div(style="margin-bottom: 10px;",
                        shiny::actionButton("DsymbolGallery", "Symbol Gallery")))),
            shiny::fluidRow(
                shiny::column(3,
                    colourpicker::colourInput("Dcolour", "Colour", state$Dcolour)),
                shiny::column(2, shiny::numericInput("Dsymbol", "Type", value=state$Dsymbol, min=0, max=25)),
                shiny::column(3, shiny::sliderInput("Dsize", "Size", min=0, max=1, value=state$Dsize, step=0.05))),
            shiny::fluidRow(
                if (state$Dsymbol == 21) {
                    shiny::column(3,
                        colourpicker::colourInput("Dborder", "Border Colour", value=state$Dborder))
                }),
            shiny::fluidRow(
                shiny::div(style="color:black; font-weight:bold; margin-bottom: 10px;",
                    hr("Path Properties"))),
            shiny::fluidRow(
                shiny::column(3,
                    colourpicker::colourInput("DPcolour", "Colour", state$DPcolour)),
                shiny::column(3, shiny::sliderInput("DPwidth", "Width", min=0.5, max=6, value=state$DPwidth, step=0.5))))
        }
    })

    output$UIwidget1 <- shiny::renderUI({
        if (argoFloatsIsCached("argo", debug=debug-1L) && input$tabselected %in% c(1)) {
            shiny::fluidRow(
                shiny::actionButton("help", "Help"),
                shiny::actionButton("undo", "Undo"),
                shiny::actionButton("code", "Code"),
                shiny::actionButton("goW", shiny::HTML("&larr;")),
                shiny::actionButton("goN", shiny::HTML("&uarr;")),
                shiny::actionButton("goS", shiny::HTML("&darr;")),
                shiny::actionButton("goE", shiny::HTML("&rarr;")),
                shiny::actionButton("zoomIn", "+"),
                shiny::actionButton("zoomOut", "-"),
                shinyBS::bsButton("polygon", shiny::HTML("&#x2B21;")),
                shinyBS::bsTooltip(id="polygon",title="To subset by polygon 1) Click this button 2) Click at least 3 points in the map 3) Click q to indicate done.", trigger="hover"),
                style="margin-left:0.5em;")
        }
    })
    output$UIwidget2 <- shiny::renderUI({
        if (argoFloatsIsCached("argo", debug=debug-1L) && input$tabselected %in% c(1)) {
            shiny::fluidRow(
                shiny::div(style="display: inline-block; vertical-align:center; width: 8em; margin: 0; padding-left:0px;",
                    shiny::dateInput(inputId="start", label="Start", value=state$startTime)),
                shiny::div(style="display: inline-block;vertical-align:top; width: 8em;",
                    shiny::dateInput(inputId="end", label="End", value=state$endTime)),
                shiny::div(style="display: inline-block;vertical-align:top; width: 8em;",
                    shiny::textInput("ID", "Float ID", value=state$focusID, width="8em")),
                style="margin-left:0.5em;")
        }
    })
    output$UItrajectory <- shiny::renderUI({
        if (argoFloatsIsCached("argo", debug=debug-1L) && input$tabselected %in% c(1) && "path" %in% state$view) {
            shiny::fluidRow(shiny::column(6,
                    style="text-indent:1em;",
                    shiny::checkboxGroupInput("action",
                        label="Path Properties",
                        choiceNames=list(shiny::tags$span("Start", style="color: black;"),
                            shiny::tags$span("End", style="color: black;"),
                            shiny::tags$span("Without Profiles", style="color: black;")),
                        choiceValues=list( "start", "end", "lines"), selected=state$action,
                        inline=TRUE)))
        }
    })
    output$UIinfo <- shiny::renderUI({
        if (argoFloatsIsCached("argo", debug=debug-1L)) {
            shiny::fluidRow(shiny::verbatimTextOutput("info"),
                if (state$hoverIsPasted) {
                    tags$head(tags$style("#info{color: red}"))
                } else {
                    tags$head(tags$style("#info{color: black}"))
                }
            )
        }
    })

    output$info <- shiny::renderText({
        # show location.  If lat range is under 90deg, also show nearest float within 100km
        if (input$tabselected == 1) {
            if (state$hoverIsPasted)
                return(paste("[HOLD]",lastHoverMessage))
        x <- input$hover$x
        y <- input$hover$y
        if (is.null(x) && input$tabselected == 1)
            return("Hover to see location/cycle; brush to select region; double-click to restrict ID.")
        rval <- ""
        if (diff(range(state$ylim)) < 90 && sum(visible)) {
            fac <- cos(pi180 * y)      # account for meridional convergence
            dist2 <- ifelse(visible, (fac * (x - argo$longitude))^2 + (y - argo$latitude)^2, 1000)
            i <- which.min(dist2)
            holdLatitude <<- argo$latitude[i]
            holdLongitude <<- argo$longitude[i]
            dist <- sqrt(dist2[i]) * 111 # 1deg lat approx 111km
            lonstring <- ifelse(x < 0, sprintf("%.2fW", abs(argo$longitude[i])), sprintf("%.2fE", x))
            latstring <- ifelse(y < 0, sprintf("%.2fS", abs(argo$latitude[i])), sprintf("%.2fN", y))
            if (length(dist) && dist < 100) {
                highlight <<- TRUE
                rval <- sprintf("%s Float %s cycle %s (type %s from %s) at %s %s, %s",
                    switch(argo$type[i], "core"="Core", "bgc"="BGC", "deep"="Deep"),
                    argo$ID[i],
                    argo$cycle[i],
                    argo$profilerType[i],
                    argo$institution[i],
                    lonstring,
                    latstring,
                    format(argo$time[i], "%Y-%m-%d %H:%M"))
            } else {
                rval <- sprintf("%s %s", lonstring, latstring)
                highlight <<- FALSE
            }
        } else {
            lonstring <- ifelse(x < 0, sprintf("%.2fW", x), sprintf("%.2fE", x))
            latstring <- ifelse(y < 0, sprintf("%.2fS", y), sprintf("%.2fN", y))
            rval <- sprintf("%s %s", lonstring, latstring)
        }
        lastHoverMessage <<- rval
        rval
    }
    })

    shiny::observeEvent(input$brush,
        {
            if (state$polygon == FALSE) {
                state$data <- NULL
                state$polyDone <- FALSE
                argoFloatsDebug(debug,  "observeEvent(input$brush) {\n", style="bold", showTime=FALSE, unindent=1)
                state$xlim <<- c(input$brush$xmin, input$brush$xmax)
                state$ylim <<- c(input$brush$ymin, input$brush$ymax)
                argoFloatsDebug(debug, "set state$xlim=c(",paste(state$xlim,collapse=",),"),
                    " and state$ylim=c(", paste(state$ylim, collapse=","), ")\n")
                argoFloatsDebug(debug,  "} # observeEvent(input$ID)\n", style="bold", showTime=FALSE, unindent=1)
            }
        })

    shiny::observeEvent(input$Ccolour,
        {
            argoFloatsDebug(debug,  "observeEvent(input$Ccolour) {\n", style="bold", showTime=FALSE, unindent=1)
            state$Ccolour <<- input$Ccolour
            argoFloatsDebug(debug, "input$Ccolour= ", input$Ccolour, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$Ccolour)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$Csymbol,
        {
            argoFloatsDebug(debug,  "observeEvent(input$Csymbol) {\n", style="bold", showTime=FALSE, unindent=1)
            state$Csymbol <<- input$Csymbol
            argoFloatsDebug(debug, "input$Csymbol= ", input$Csymbol, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$Csymbol)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$Csize,
        {
            argoFloatsDebug(debug,  "observeEvent(input$Csize) {\n", style="bold", showTime=FALSE, unindent=1)
            state$Csize <<- input$Csize
            argoFloatsDebug(debug, "input$Csize= ", input$Csize, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$Csize)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$Cborder,
        {
            argoFloatsDebug(debug,  "observeEvent(input$Cborder) {\n", style="bold", showTime=FALSE, unindent=1)
            state$Cborder <<- input$Cborder
            argoFloatsDebug(debug, "input$Cborder= ", input$Cborder, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$Cborder)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$CPcolour,
        {
            argoFloatsDebug(debug,  "observeEvent(input$CPcolour) {\n", style="bold", showTime=FALSE, unindent=1)
            state$CPcolour <<- input$CPcolour
            argoFloatsDebug(debug, "input$CPcolour= ", input$CPcolour, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$CPcolour)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$CPwidth,
        {
            argoFloatsDebug(debug,  "observeEvent(input$CPwidth) {\n", style="bold", showTime=FALSE, unindent=1)
            state$CPwidth <<- input$CPwidth
            argoFloatsDebug(debug, "input$CPwidth= ", input$CPwidth, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$CPwidth)\n", style="bold", showTime=FALSE, unindent=1)
        })


 shiny::observeEvent(input$Bcolour,
        {
            argoFloatsDebug(debug,  "observeEvent(input$Bcolour) {\n", style="bold", showTime=FALSE, unindent=1)
            state$Bcolour <<- input$Bcolour
            argoFloatsDebug(debug, "input$Bcolour= ", input$Bcolour, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$Bcolour)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$Bsymbol,
        {
            argoFloatsDebug(debug,  "observeEvent(input$Bsymbol) {\n", style="bold", showTime=FALSE, unindent=1)
            state$Bsymbol <<- input$Bsymbol
            argoFloatsDebug(debug, "input$Bsymbol= ", input$Bsymbol, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$Bsymbol)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$Bsize,
        {
            argoFloatsDebug(debug,  "observeEvent(input$Bsize) {\n", style="bold", showTime=FALSE, unindent=1)
            state$Bsize <<- input$Bsize
            argoFloatsDebug(debug, "input$=Bsize ", input$Bsize, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$Bsize)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$Bborder,
        {
            argoFloatsDebug(debug,  "observeEvent(input$Bborder) {\n", style="bold", showTime=FALSE, unindent=1)
            state$Bborder <<- input$Bborder
            argoFloatsDebug(debug, "input$=Bborder ", input$Bborder, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$Bborder)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$BPcolour,
        {
            argoFloatsDebug(debug,  "observeEvent(input$BPcolour) {\n", style="bold", showTime=FALSE, unindent=1)
            state$BPcolour <<- input$BPcolour
            argoFloatsDebug(debug, "input$=BPcolour ", input$BPcolour, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$BPcolour)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$BPwidth,
        {
            argoFloatsDebug(debug,  "observeEvent(input$BPwidth) {\n", style="bold", showTime=FALSE, unindent=1)
            state$BPwidth <<- input$BPwidth
            argoFloatsDebug(debug, "input$=BPwidth ", input$BPwidth, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$BPwidth)\n", style="bold", showTime=FALSE, unindent=1)
        })

 shiny::observeEvent(input$Dcolour,
        {
            argoFloatsDebug(debug,  "observeEvent(input$Dcolour) {\n", style="bold", showTime=FALSE, unindent=1)
            state$Dcolour <<- input$Dcolour
            argoFloatsDebug(debug, "input$=Dcolour ", input$Dcolour, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$Dcolour)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$Dsymbol,
        {
            argoFloatsDebug(debug,  "observeEvent(input$Dsymbol) {\n", style="bold", showTime=FALSE, unindent=1)
            state$Dsymbol <<- input$Dsymbol
            argoFloatsDebug(debug, "input$=Dsymbol ", input$Dsymbol, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$Dsymbol)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$Dsize,
        {
            argoFloatsDebug(debug,  "observeEvent(input$Dsize) {\n", style="bold", showTime=FALSE, unindent=1)
            state$Dsize <<- input$Dsize
            argoFloatsDebug(debug, "input$=Dsize ", input$Dsize, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$Dsize)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$Dborder,
        {
            argoFloatsDebug(debug,  "observeEvent(input$Dborder) {\n", style="bold", showTime=FALSE, unindent=1)
            state$Dborder <<- input$Dborder
            argoFloatsDebug(debug, "input$=Dborder ", input$Dborder, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$Dborder)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$DPcolour,
        {
            argoFloatsDebug(debug,  "observeEvent(input$DPcolour) {\n", style="bold", showTime=FALSE, unindent=1)
            state$DPcolour <<- input$DPcolour
            argoFloatsDebug(debug, "input$=DPcolour ", input$DPcolour, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$DPcolour)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$DPwidth,
        {
            argoFloatsDebug(debug,  "observeEvent(input$DPwidth) {\n", style="bold", showTime=FALSE, unindent=1)
            state$DPwidth <<- input$DPwidth
            argoFloatsDebug(debug, "input$=DPwidth ", input$DPwidth, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$DPwidth)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$view,
        {
            state$view <<- input$view
        }, ignoreNULL=FALSE)

    shiny::observeEvent(input$action,
        {
            state$action <<- input$action
        }, ignoreNULL=FALSE)

    shiny::observeEvent(input$goE,
        {
            argoFloatsDebug(debug,  "observeEvent(input$goE) {\n", style="bold", showTime=FALSE, unindent=1)
            dx <- diff(state$xlim) # present longitude span
            state$xlim <<- pinlon(state$xlim + dx / 4)
            argoFloatsDebug(debug, "state$xlim ", state$xlim, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$goE)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$goW,
        {
            argoFloatsDebug(debug,  "observeEvent(input$goW) {\n", style="bold", showTime=FALSE, unindent=1)
            dx <- diff(state$xlim) # present longitude span
            state$xlim <<- pinlon(state$xlim - dx / 4)
            argoFloatsDebug(debug, "state$xlim ", state$xlim, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$goW)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$goS,
        {
            argoFloatsDebug(debug,  "observeEvent(input$goS) {\n", style="bold", showTime=FALSE, unindent=1)
            dy <- diff(state$ylim) # present latitude span
            state$ylim <<- pinlat(state$ylim - dy / 4)
            argoFloatsDebug(debug, "state$ylim ", state$ylim, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$goS)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$goN,
        {
            argoFloatsDebug(debug,  "observeEvent(input$goN) {\n", style="bold", showTime=FALSE, unindent=1)
            dy <- diff(state$ylim) # present latitude span
            state$ylim <<- pinlat(state$ylim + dy / 4)
            argoFloatsDebug(debug, "state$ylim ", state$ylim, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$goN)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$zoomIn,
        {
            argoFloatsDebug(debug,  "observeEvent(input$zoomIn) {\n", style="bold", showTime=FALSE, unindent=1)
            state$xlim <<- pinlon(mean(state$xlim)) + c(-0.5, 0.5) / 1.3 * diff(state$xlim)
            state$ylim <<- pinlat(mean(state$ylim)) + c(-0.5, 0.5) / 1.3 * diff(state$ylim)
            argoFloatsDebug(debug, "state$xlim ", state$xlim, " and state$ylim=",state$ylim, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$zoomIn)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$zoomOut,
        {
            argoFloatsDebug(debug,  "observeEvent(input$zoomOut) {\n", style="bold", showTime=FALSE, unindent=1)
            state$xlim <<- pinlon(mean(state$xlim) + c(-0.5, 0.5) * 1.3 * diff(state$xlim))
            state$ylim <<- pinlat(mean(state$ylim) + c(-0.5, 0.5) * 1.3 * diff(state$ylim))
            argoFloatsDebug(debug, "state$xlim ", state$xlim, " and state$ylim=",state$ylim, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$zoomOut)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$code,
        {
            argoFloatsDebug(debug,  "observeEvent(input$code) {\n", style="bold", showTime=FALSE, unindent=1)
            msg <- "library(argoFloats)<br>"
            msg <- paste(msg, "# Download (or use cached) index from one of two international servers.<br>")
            if ("core" %in% state$view && "bgc" %in% state$view && !("deep" %in% state$view)) {
                msg <- paste(msg, "ai <- getIndex()<br>")
                msg <- paste(msg, "bai <- getIndex(filename=\"bgc\")<br>")
                msg <- paste(msg, "mai <- merge(ai,bai)<br>")
                msg <- paste(msg, "# Subset to remove deep profiles.<br>")
                msg <- paste(msg, "index <- subset(mai, deep=FALSE)<br>")
            } else if ("core" %in% state$view && "deep" %in% state$view && !("bgc" %in% state$view)) {
                msg <- paste(msg, "ai <- getIndex()<br>")
                msg <- paste(msg, "bai <- getIndex(filename=\"bgc\")<br>")
                msg <- paste(msg, "# Subset deep profiles.<br>")
                msg <- paste(msg, "deep1 <- subset(ai, deep=TRUE)<br>")
                msg <- paste(msg, "deep2 <- subset(bai, deep=TRUE)<br>")
                msg <- paste(msg, "deep <- merge(deep1,deep2)<br>")
                msg <- paste(msg, "index <- merge(ai,deep)<br>")
            } else if ("bgc" %in% state$view && "deep" %in% state$view && !("core" %in% state$view)) {
                msg <- paste(msg, "ai <- getIndex()<br>")
                msg <- paste(msg, "bai <- getIndex(filename=\"bgc\")<br>")
                msg <- paste(msg, "# Subset deep profiles.<br>")
                msg <- paste(msg, "deep1 <- subset(ai, deep=TRUE)<br>")
                msg <- paste(msg, "deep2 <- subset(bai, deep=TRUE)<br>")
                msg <- paste(msg, "deep <- merge(deep1,deep2)<br>")
                msg <- paste(msg, "index <- merge(bai,deep)<br>")
            } else if ("bgc" %in% state$view && "deep" %in% state$view && "core" %in% state$view) {
                msg <- paste(msg, "ai <- getIndex()<br>")
                msg <- paste(msg, "bai <- getIndex(filename=\"bgc\")<br>")
                msg <- paste(msg, "index <- merge(ai,bai)<br>")
            } else if ("core" %in% state$view && !("bgc" %in% state$view) && !("deep" %in% state$view)) {
                msg <- paste(msg, "index <- getIndex()<br>")
            } else if ("bgc" %in% state$view && !("core" %in% state$view) && !("deep" %in% state$view)) {
                msg <- paste(msg, "index <- getIndex(filename=\"bgc\")<br>")
            } else if ("deep" %in% state$view && !("core" %in% state$view) && !("bgc" %in% state$view)) {
                msg <- paste(msg, "bai <- getIndex(filename=\"bgc\")<br>")
                msg <- paste(msg, "ai <- getIndex()<br>")
                msg <- paste(msg, "# Subset deep profiles.<br>")
                msg <- paste(msg, "deep1 <- subset(ai, deep=TRUE)<br>")
                msg <- paste(msg, "deep2 <- subset(bai, deep=TRUE)<br>")
                msg <- paste(msg, "index <- merge(deep1,deep2)<br>")
            }
            msg <- paste(msg, "# Subset by time.<br>")
            msg <- paste(msg, "from <- as.POSIXct(\"", format(state$startTime, "%Y-%m-%d", tz="UTC"), "\", tz=\"UTC\")<br>")
            msg <- paste(msg, "to <- as.POSIXct(\"", format(state$endTime, "%Y-%m-%d", tz="UTC"), "\", tz=\"UTC\")<br>", sep="")
            msg <- paste(msg, "subset1 <- subset(index, time=list(from=from, to=to))<br>")
            msg <- paste(msg, "# Subset by space.<br>")
            if (state$polyDone) {
                latp <- paste(round(latpoly,3), collapse=",")
                lonp <- paste(round(lonpoly,3), collapse=",")
                msg <- paste(msg, "subset2 <- subset(subset1, polygon=list(longitude=c(",lonp,"),latitude=c(",latp,")))<br>")
            } else {
                lonRect <- state$xlim
                latRect <- state$ylim
                msg <- paste(msg, sprintf("rect <- list(longitude=c(%.4f,%.4f), latitude=c(%.4f,%.4f))<br>",
                        lonRect[1], lonRect[2], latRect[1], latRect[2]))
                msg <- paste(msg, "subset2 <- subset(subset1, rectangle=rect)<br>")
            }
            if (!is.null(state$focusID)) {
                msg <- paste0(msg, sprintf("subset2 <- subset(subset2, ID=%2s)<br>", state$focusID))
            }
            msg <- paste(msg, "# Plot a map (with different formatting than used here).<br>")
            if ("topo" %in% state$view) {
                if ("path" %in% state$view) {
                    if ("lines" %in% state$action) {
                        msg <- paste(msg, "plot(subset2, which=\"map\", type=\"l\")<br>")
                    } else  {
                        msg <- paste(msg, "plot(subset2, which=\"map\", type=\"o\")<br>")
                    }
                } else {
                    msg <- paste(msg, "plot(subset2, which=\"map\")<br>")
                }
            } else {
                if ("path" %in% state$view) {
                    if ("lines" %in% state$action) {
                        msg <- paste(msg, "plot(subset2, which=\"map\", bathymetry=FALSE, type=\"l\")<br>")
                    } else {
                        msg <- paste(msg, "plot(subset2, which=\"map\", bathymetry=FALSE, type=\"o\")<br>")
                    }
                } else {
                    msg <- paste(msg, "plot(subset2, which=\"map\", bathymetry=FALSE)<br>")
                }
            }

            if ("start" %in% state$action) {
                argoFloatsDebug(debug, "state$action= ", state$action, "\n")
                msg <- paste(msg, "o <- order(subset2[['time']])<br>")
                msg <- paste(msg, "lat <- subset2[['latitude']]<br>")
                msg <- paste(msg, "lon <- subset2[['longitude']]<br>")
                msg <- paste(msg, "points(lon[1],lat[1], pch=2, cex=2, lwd=1.4)<br>")
            }

            if ("end" %in% state$action) {
                msg <- paste(msg, "o <- order(subset2[['time']])<br>")
                msg <- paste(msg, "lat <- subset2[['latitude']]<br>")
                msg <- paste(msg, "lon <- subset2[['longitude']]<br>")
                msg <- paste(msg, "no <- length(o)<br>")
                msg <- paste(msg, "points(lon[no],lat[no], pch=0, cex=2, lwd=1.4)<br>")
            }

            if ("contour" %in% state$view) {
                msg <- paste(msg, "# Adding contour. <br>")
                msg <- paste(msg, "data(topoWorld)<br>")
                msg <- paste(msg, "contour(topoWorld[[\"longitude\"]], topoWorld[[\"latitude\"]], topoWorld[[\"z\"]], levels=-1000*(1:10), drawlabels=FALSE, add=TRUE)<br>")
            }
            msg <- paste(msg, "# The following shows how to make a TS plot for these profiles. This<br>")
            msg <- paste(msg, "# involves downloading, which will be slow for a large number of<br>")
            msg <- paste(msg, "# profiles, so the steps are placed in an unexecuted block.<br>")
            msg <- paste(msg, "if (FALSE) {<br>")
            msg <- paste(msg, "&nbsp;&nbsp; profiles <- getProfiles(subset2)<br>")
            msg <- paste(msg, "&nbsp;&nbsp; argos <- readProfiles(profiles)<br>")
            msg <- paste( msg, "&nbsp;&nbsp; plot(applyQC(argos), which=\"TS\", TSControl=list(colByCycle=1:8))<br>")
            msg <- paste(msg, "}<br>")
            argoFloatsDebug(debug, "state$view= ", state$view, "\n")
            argoFloatsDebug(debug, "state$focusID= ", state$focusID, "\n")
            argoFloatsDebug(debug, "state$action= ", state$action, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$code)\n", style="bold", showTime=FALSE, unindent=1)
            shiny::showModal(shiny::modalDialog(shiny::HTML(msg), title="R code hints", size="l"))
        })

    shiny::observeEvent(input$ID,
        {
            argoFloatsDebug(debug,  "observeEvent(input$ID) {\n", style="bold", showTime=FALSE, unindent=1)
            if (0 == nchar(input$ID)) {
                argoFloatsDebug(debug, "input$ID is empty, so setting state$focusID to NULL and doing nothing else\n")
                state$focusID <<- NULL
            } else {
                k <- which(argo$ID == input$ID)
                argoFloatsDebug(debug, " have ", length(k), " cycles for input$ID='", input$ID, "'\n", sep="")
                if (length(k) > 0L) {
                    state$focusID <<- input$ID
                    state$xlim <<- pinlon(extendrange(argo$lon[k], f=0.15))
                    state$ylim <<- pinlat(extendrange(argo$lat[k], f=0.15))
                    state$startTime <<- dayStart(min(argo$time[k]))
                    state$endTime <<- dayEnd(max(argo$time[k]))
                    argoFloatsDebug(debug, "set xlim:      ", state$xlim[1], " to ", state$xlim[2], "\n")
                    argoFloatsDebug(debug, "set ylim:      ", state$ylim[1], " to ", state$ylim[2], "\n")
                    argoFloatsDebug(debug, "set startTime: ", format(state$startTime, "%Y-%m-%d %H:%M:%S"), "\n")
                    argoFloatsDebug(debug, "set endTime:   ", format(state$endTime, "%Y-%m-%d %H:%M:%S"), "\n")
                } else {
                    # Float IDs usually (always?) have 7 digits
                    if (nchar(input$ID) > 7L)
                        shiny::showNotification("Float IDs cannot have more than 7 characters", type="error")
                    else if (nchar(input$ID) > 6L)
                        shiny::showNotification(paste0("There is no float with ID ", input$ID, "."), type="error")
                }
            }
            argoFloatsDebug(debug,  "} # observeEvent(input$ID)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$dblclick,
        {
            argoFloatsDebug(debug,  "observeEvent(input$dblclick) {\n", style="bold", showTime=FALSE, unindent=1)
            x <- input$dblclick$x
            y <- input$dblclick$y
            state$data <- NULL
            state$polyDone <- FALSE
            state$polygon <- FALSE
            shinyBS::updateButton(session, "polygon", style="default")
            fac2 <- 1.0/cos(pi180*y)^2 # for deltaLon^2 compared with deltaLat^2
            if (!is.null(state$focusID)) {
                argoFloatsDebug(debug, "state$focusID is NULL\n")
                keep <- argo$ID == state$focusID
            } else {
                # Restrict search to the present time window
                argoFloatsDebug(debug, "state$focusID='", state$focusID, "'\n", sep="")
                keep <- state$startTime <= argo$time & argo$time <= state$endTime
            }
            i <- which.min(ifelse(keep, fac2*(x-argo$longitude)^2+(y-argo$latitude)^2, 1000))
            if (argo$type[i] %in% state$view) {
                state$focusID <<- argo$ID[i]
                k <- which(argo$ID == argo$ID[i])
                argoFloatsDebug(debug, "have ", length(k), " cycles for inferred ID='", argo$ID[i], "'\n", sep="")
                state$xlim <<- pinlon(extendrange(argo$lon[k], f=0.15))
                state$ylim <<- pinlat(extendrange(argo$lat[k], f=0.15))
                state$startTime <<- dayStart(min(argo$time[k]))
                state$endTime <<- dayEnd(max(argo$time[k]))
                argoFloatsDebug(debug, "set xlim:      ", state$xlim[1], " to ", state$xlim[2], "\n")
                argoFloatsDebug(debug, "set ylim:      ", state$ylim[1], " to ", state$ylim[2], "\n")
                argoFloatsDebug(debug, "set startTime: ", format(state$startTime, "%Y-%m-%d %H:%M:%S"), "\n")
                argoFloatsDebug(debug, "set endTime:   ", format(state$endTime, "%Y-%m-%d %H:%M:%S"), "\n")
            } else {
                argoFloatsDebug(debug, "float ID=\"", state$focusID, "\" is NOT in view\n", sep="")
            }
            argoFloatsDebug(debug,  "} # observeEvent(input$dblclick)\n", style="bold", showTime=FALSE, unindent=1)
        })
    var1 <- list()
    var2 <- list()
    data <- cbind(var1, var2)
    shiny::observeEvent(input$click,
        {
            argoFloatsDebug(debug,  "observeEvent(input$click) {\n", style="bold", showTime=FALSE, unindent=1)
            argoFloatsDebug(debug, "state$polygon= ", state$polygon, "and state$polyDone = ", state$polyDone, "\n")
            if (input$polygon) {
                argoFloatsDebug(debug, "state$polygon= ", state$polygon, "and state$polyDone = ", state$polyDone, "\n")
                state$click$x <<- input$click$x
                state$click$y <<- input$click$y
                state$data <- rbind(state$data, cbind(input$click$x, input$click$y))
                lonpoly <<- unlist(state$data[,1])
                latpoly <<- unlist(state$data[,2])
            }
            argoFloatsDebug(debug,  "} # observeEvent(input$click)\n", style="bold", showTime=FALSE, unindent=1)
        })
    shiny::observeEvent(input$polygon,
        {
            argoFloatsDebug(debug,  "observeEvent(input$polygon) {\n", style="bold", showTime=FALSE, unindent=1)
            state$polygon <<- input$polygon
            shinyBS::updateButton(session, "polygon", style="danger")
            if (state$polygon > 1) {
                state$data <- NULL
            }
            argoFloatsDebug(debug, "state$polygon= ", state$polygon, "and state$polyDone = ", state$polyDone, "\n")
            argoFloatsDebug(debug,  "} # observeEvent(input$click)\n", style="bold", showTime=FALSE, unindent=1)
        })
    shiny::observeEvent(input$start,
        {
            argoFloatsDebug(debug,  "observeEvent(input$start) {\n", style="bold", showTime=FALSE, unindent=1)
            if (0 == nchar(input$start)) {
                state$startTime <<- min(argo$time, na.rm=TRUE)
            } else {
                t <- try(as.POSIXct(format(input$start, "%Y-%m-%d 00:00:00"), tz="UTC"), silent=TRUE)
                if (inherits(t, "try-error")) {
                    shiny::showNotification(paste0("Start time \"",
                            input$start,
                            "\" is not in yyyy-mm-dd format, or is otherwise invalid."), type="error")
                } else {
                    state$startTime <<- t
                    argoFloatsDebug(debug, "user selected start time ", format(t, "%Y-%m-%d %H:%M:%S %z"), "\n")
                }
            }
            argoFloatsDebug(debug,  "} # observeEvent(input$start)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$end,
        {
            argoFloatsDebug(debug,  "observeEvent(input$end) {\n", style="bold", showTime=FALSE, unindent=1)
            if (0 == nchar(input$end)) {
                state$endTime <<- max(argo$time, na.rm = TRUE)
            } else {
                t <- try(as.POSIXct(format(input$end, "%Y-%m-%d 00:00:00"), tz="UTC"), silent=TRUE)
                if (inherits(t, "try-error")) {
                    shiny::showNotification(paste0( "End time \"", input$end, "\" is not in yyyy-mm-dd format, or is otherwise invalid."), type = "error")
                } else {
                    state$endTime <<- t
                    argoFloatsDebug(debug, "user selected end time ", format(t, "%Y-%m-%d %H:%M:%S %z"), "\n")
                }
            }
            argoFloatsDebug(debug,  "} # observeEvent(input$end)\n", style="bold", showTime=FALSE, unindent=1)
        })

    shiny::observeEvent(input$help,
        {
            msg <- shiny::HTML(overallHelp)
            shiny::showModal(shiny::modalDialog(shiny::HTML(msg), title="Using mapApp()", size="l"))
        })


    shiny::observeEvent(input$undo,
        {
            if (sizeState() > 2L) {
                popState()
                previousState <- topState()
                colourpicker::updateColourInput(session, inputId="Ccolour", value=previousState$Ccolour)
                colourpicker::updateColourInput(session, inputId="Cborder", value=previousState$Cborder)
                colourpicker::updateColourInput(session, inputId="CPcolour", value=previousState$CPcolour)
                colourpicker::updateColourInput(session, inputId="Bcolour", value=previousState$Bcolour)
                colourpicker::updateColourInput(session, inputId="Bborder", value=previousState$Bborder)
                colourpicker::updateColourInput(session, inputId="BPcolour", value=previousState$BPcolour)
                colourpicker::updateColourInput(session, inputId="Dcolour", value=previousState$Dcolour)
                colourpicker::updateColourInput(session, inputId="Dborder", value=previousState$Dborder)
                colourpicker::updateColourInput(session, inputId="DPcolour", value=previousState$DPcolour)
                shiny::updateNumericInput(session, inputId="Csymbol", value=previousState$Csymbol)
                shiny::updateSliderInput(session, inputId="Csize", value=previousState$Csize)
                shiny::updateSliderInput(session, inputId="CPwidth", value=previousState$CPwidth)
                shiny::updateNumericInput(session, inputId="Bsymbol", value=previousState$Bsymbol)
                shiny::updateSliderInput(session, inputId="Bsize", value=previousState$Bsize)
                shiny::updateSliderInput(session, inputId="BPwidth", value=previousState$BPwidth)
                shiny::updateNumericInput(session, inputId="Dsymbol", value=previousState$Dsymbol)
                shiny::updateSliderInput(session, inputId="Dsize", value=previousState$Dsize)
                shiny::updateSliderInput(session, inputId="DPwidth", value=previousState$DPwidth)
                for (name in names(previousState))
                    state[[name]] <- previousState[[name]]
            }
        })

    shiny::observeEvent(input$CsymbolGallery,
        {
            shiny::showModal(shiny::modalDialog(shiny::img(src="/pch_gallery.png"),
                    title="Symbol Gallery", size="s", easyClose=TRUE))
        })

    shiny::observeEvent(input$BsymbolGallery,
        {
            shiny::showModal(shiny::modalDialog(shiny::img(src="/pch_gallery.png"),
                    title="Symbol Gallery", size="s", easyClose=TRUE))
        })

    shiny::observeEvent(input$DsymbolGallery,
        {
            shiny::showModal(shiny::modalDialog(shiny::img(src="/pch_gallery.png"),
                    title="Symbol Gallery", size="s", easyClose=TRUE))
        })

    shiny::observeEvent(input$keypressTrigger,
        {
            argoFloatsDebug(debug,  "observeEvent(input$keypressTrigger) {\n", style="bold", showTime=FALSE, unindent=1)
            key <- intToUtf8(input$keypress)
            #message(input$keypress)
            #message(key)
            if (key == "d") { # toggle debug
                debug <<- !debug
                message("switched debug to ", debug)
            #} else if (key == "n") { # go north
            #    dy <- diff(state$ylim)
            #    state$ylim <<- pinlat(state$ylim + dy / 4)
            #} else if (key == "s") { # go south
            #    dy <- diff(state$ylim)
            #    state$ylim <<- pinlat(state$ylim - dy / 4)
            #} else if (key == "e") { # go east
            #    dx <- diff(state$xlim)
            #    state$xlim <<- pinlon(state$xlim + dx / 4)
            #} else if (key == "w") { # go west
            #    dx <- diff(state$xlim)
            #    state$xlim <<- pinlon(state$xlim - dx / 4)
            #} else if (key == "f") { # forward in time
            #    interval <- as.numeric(state$endTime) - as.numeric(state$startTime)
            #    state$startTime <<- state$startTime + interval
            #    state$endTime <<- state$endTime + interval
            #    shiny::updateTextInput(session, "start", value=format(state$startTime, "%Y-%m-%d"))
            #    shiny::updateTextInput(session, "end", value=format(state$endTime, "%Y-%m-%d"))
            #} else if (key == "b") { # backward in time
            #    interval <- as.numeric(state$endTime) - as.numeric(state$startTime)
            #    state$startTime <<- state$startTime - interval
            #    state$endTime <<- state$endTime - interval
            #    shiny::updateTextInput(session, "start", value=format(state$startTime, "%Y-%m-%d"))
            #    shiny::updateTextInput(session, "end", value=format(state$endTime, "%Y-%m-%d"))
            #} else if (key == "i") { # zoom in
            #    state$xlim <<- pinlon(mean(state$xlim)) + c(-0.5, 0.5) / 1.3 * diff(state$xlim)
            #    state$ylim <<- pinlat(mean(state$ylim)) + c(-0.5, 0.5) / 1.3 * diff(state$ylim)
            #} else if (key == "o") { # zoom out
            #    state$xlim <<- pinlon(mean(state$xlim) + c(-0.5, 0.5) * 1.3 * diff(state$xlim))
            #    state$ylim <<- pinlat(mean(state$ylim) + c(-0.5, 0.5) * 1.3 * diff(state$ylim))
            } else if (key == "h") { # paste hover message
                argoFloatsDebug(debug, "Key h pressed, so setting state$hoverIsPasted to", !state$hoverIsPasted, "\n")
                state$hoverIsPasted <<- !state$hoverIsPasted
                pushState(isolate(reactiveValuesToList(state)))
            } else if (key == "u") {
                if (sizeState() > 2L) {
                    popState()
                    previousState <- topState()
                    colourpicker::updateColourInput(session, inputId="Ccolour", value=previousState$Ccolour)
                    colourpicker::updateColourInput(session, inputId="Cborder", value=previousState$Cborder)
                    colourpicker::updateColourInput(session, inputId="CPcolour", value=previousState$CPcolour)
                    colourpicker::updateColourInput(session, inputId="Bcolour", value=previousState$Bcolour)
                    colourpicker::updateColourInput(session, inputId="Bborder", value=previousState$Bborder)
                    colourpicker::updateColourInput(session, inputId="BPcolour", value=previousState$BPcolour)
                    colourpicker::updateColourInput(session, inputId="Dcolour", value=previousState$Dcolour)
                    colourpicker::updateColourInput(session, inputId="Dborder", value=previousState$Dborder)
                    colourpicker::updateColourInput(session, inputId="DPcolour", value=previousState$DPcolour)
                    shiny::updateNumericInput(session, inputId="Csymbol", value=previousState$Csymbol)
                    shiny::updateSliderInput(session, inputId="Csize", value=previousState$Csize)
                    shiny::updateSliderInput(session, inputId="CPwidth", value=previousState$CPwidth)
                    shiny::updateNumericInput(session, inputId="Bsymbol", value=previousState$Bsymbol)
                    shiny::updateSliderInput(session, inputId="Bsize", value=previousState$Bsize)
                    shiny::updateSliderInput(session, inputId="BPwidth", value=previousState$BPwidth)
                    shiny::updateNumericInput(session, inputId="Dsymbol", value=previousState$Dsymbol)
                    shiny::updateSliderInput(session, inputId="Dsize", value=previousState$Dsize)
                    shiny::updateSliderInput(session, inputId="DPwidth", value=previousState$DPwidth)
                    for (name in names(previousState))
                        state[[name]] <- previousState[[name]]
                }

            } else if (key == "r") { # reset to start
                argoFloatsDebug(debug, "Key r pressed \n")
                shinyBS::updateButton(session, "polygon", style="default")
                state$xlim <<- c(-180, 180)
                state$ylim <<- c(-90, 90)
                state$startTime <<- startTime
                state$endTime <<- endTime
                state$focusID <<- NULL
                state$hoverIsPasted <<- FALSE
                state$polyDone <- FALSE
                state$data <<- NULL
                shiny::updateCheckboxGroupInput(session, "show", selected=character(0))
                shiny::updateCheckboxGroupInput(session, "view", selected=c("core", "deep", "bgc"))
                shiny::updateSelectInput(session, "action", selected=NULL)
                shiny::updateDateInput(session, inputId="start", label="Start", value=startTime)
                shiny::updateDateInput(session, inputId="end", label="End", value=endTime)
                state$view <<- c("core", "deep", "bgc")
                state$action <<- NULL
                # core
                colourpicker::updateColourInput(session, inputId="Ccolour", value=colDefaults$core)
                colourpicker::updateColourInput(session, inputId="Cborder", value="black")
                colourpicker::updateColourInput(session, inputId="CPcolour", value=colDefaults$core)
                shiny::updateNumericInput(session, inputId="Csymbol", value=21)
                shiny::updateSliderInput(session, inputId="Csize", value=0.9)
                shiny::updateSliderInput(session, inputId="CPwidth", value=1.4)
                # bgc
                colourpicker::updateColourInput(session, inputId="Bcolour", value=colDefaults$bgc)
                colourpicker::updateColourInput(session, inputId="Bborder", value="black")
                colourpicker::updateColourInput(session, inputId="BPcolour", value=colDefaults$bgc)
                shiny::updateNumericInput(session, inputId="Bsymbol", value=21)
                shiny::updateSliderInput(session, inputId="Bsize", value=0.9)
                shiny::updateSliderInput(session, inputId="BPwidth", value=1.4)
                # deep
                colourpicker::updateColourInput(session, inputId="Dcolour", value=colDefaults$deep)
                colourpicker::updateColourInput(session, inputId="Dborder", value="black")
                colourpicker::updateColourInput(session, inputId="DPcolour", value=colDefaults$deep)
                shiny::updateNumericInput(session, inputId="Dsymbol", value=21)
                shiny::updateSliderInput(session, inputId="Dsize", value=0.9)
                shiny::updateSliderInput(session, inputId="DPwidth", value=1.4)
            } else if (key == "0") { # Unzoom an area and keep same time scale
                argoFloatsDebug(debug, "Key 0 pressed \n")
                state$xlim <<- c(-180, 180)
                state$ylim <<- c(-90, 90)
                shiny::updateCheckboxGroupInput(session, "show", selected=character(0))
            } else if (key == "?") { # show help on keystrokes
                argoFloatsDebug(debug, "Key ? pressed \n")
                shiny::showModal(shiny::modalDialog(title="Key-stroke commands",
                        shiny::HTML(keyPressHelp), easyClose=TRUE))
            } else if (key == "q") {
                argoFloatsDebug(debug, "Key q pressed \n")
                if (state$polygon) {
                    if (length(lonpoly) < 3) {
                        shiny::showNotification("Must choose at least 3 points for polygon.", type="message", duration=5)
                    } else {
                        state$polyDone <- TRUE
                        state$polygon <- FALSE
                        POLY <- subset(m, polygon=list(longitude=lonpoly, latitude=latpoly), silent=TRUE)
                        polykeep <<- (argo[["file"]] %in% POLY[["file"]])
                        state$xlim <<- c(min(lonpoly), max(lonpoly))
                        state$ylim <<- c(min(latpoly), max(latpoly))
                        shinyBS::updateButton(session, "polygon", style="default")
                    }
                }
            }
            argoFloatsDebug(debug,  "} # observeEvent(input$keypressTrigger)\n", style="bold", showTime=FALSE, unindent=1)
        })                                  # keypressTrigger

    output$plotMap <- shiny::renderPlot({
        argoFloatsDebug(debug, "plotMap() {\n", style="bold", unindent=1)
        argoFloatsDebug(debug, "plotCounter=", plotCounter, "\n", sep="")
        argoFloatsDebug(debug, "lastHoverMessage=", lastHoverMessage, "\n", sep="")
        argoFloatsDebug(debug, "state$hoverIsPasted=", state$hoverIsPasted, "\n", sep="")
        plotCounter <<- plotCounter + 1L
        #> message("in output$plotMap with state$begin=", state$begin)
        if (state$begin)
            state$view <<- viewDefaults
        state$begin <<- FALSE
        validate(need(state$startTime < state$endTime,
                "The Start time must precede the End time."))
        omar <- par("mar")
        omgp <- par("mgp")
        par(mar=c(2.0, 2.0, 1.0, 1.0), mgp=c(2, 0.75, 0))
        on.exit(par(mar=omar, mgp=omgp))
        topo <- if ("hires" %in% state$view) topoWorldFine else topoWorld
        argoFloatsDebug(debug, "calling plot() to set up scales\n")
        if (plotCounter < 4) {
            argoFloatsDebug(debug, "} # plotMap() early return since plotCounter=", plotCounter, "\n", sep="", style="bold", unindent=1)
            return()
        }
        plot(state$xlim, state$ylim, xlab="", ylab="", axes=FALSE, type="n", asp=1 / cos(pi180 * mean(state$ylim)))
        if (debug > 0) {
            mtext(plotCounter, adj=1, col=2)
        }
        if ("topo" %in% state$view) {
            argoFloatsDebug(debug, "drawing topo as an image\n")
            image(topo[["longitude"]], topo[["latitude"]], topo[["z"]], add=TRUE, breaks=seq(-8000, 0, 100), col=oce::oceColorsGebco(80))
        }
        usr <- par("usr")
        usr[1] <- pinlon(usr[1])
        usr[2] <- pinlon(usr[2])
        usr[3] <- pinlat(usr[3])
        usr[4] <- pinlat(usr[4])
        at <- pretty(usr[1:2], 10)
        at <- at[usr[1] < at & at < usr[2]]
        labels <- paste(abs(at), ifelse(at < 0, "W", ifelse(at > 0, "E", "")), sep="")
        axis(1, pos=pinlat(usr[3]), at=at, labels=labels, lwd=1, cex.axis=0.8)
        at <- pretty(usr[3:4], 10)
        at <- at[usr[3] < at & at < usr[4]]
        labels <- paste(abs(at), ifelse(at < 0, "S", ifelse(at > 0, "N", "")), sep="")
        axis(2, pos=pinlon(usr[1]), at=at, labels=labels, lwd=1, cex.axis=0.8)
        coastline <- if ("hires" %in% state$view) coastlineWorldFine else coastlineWorld
        argoFloatsDebug(debug, "drawing coastline as a polygon\n")
        polygon(coastline[["longitude"]], coastline[["latitude"]], col=colLand)
        rect(usr[1], usr[3], usr[2], usr[4], lwd = 1)
        # For focusID mode, we do not trim by time or space
        keep <- if (!is.null(state$focusID)) {
            argo$ID == state$focusID
        }  else {
            rep(TRUE, length(argo$ID))
        }
        argoFloatsDebug(debug, "subset from", format(state$startTime, "%Y-%m-%d %H:%M:%S %z"), "to", format(state$endTime, "%Y-%m-%d %H:%M:%S %z"), "\n")
        keep <- keep & (state$startTime <= argo$time & argo$time <= state$endTime)
        keep <- keep & (state$xlim[1] <= argo$longitude & argo$longitude <= state$xlim[2])
        keep <- keep & (state$ylim[1] <= argo$latitude & argo$latitude <= state$ylim[2])
        if (state$polyDone && is.null(state$focusID)) {
            keep <- keep & polykeep
        }
        argoFloatsDebug(debug, "subsetting for time and space leaves", sum(keep), "profiles, in categories:\n")
        cex <- 0.9
        counts <- ""
        if (sum(c("core", "deep", "bgc") %in% state$view) > 0) {
            # Draw points, optionally connecting paths (and indicating start points)
            visible <<- rep(FALSE, length(argo$lon))
            for (view in c("core", "deep", "bgc")) {
                if (view %in% state$view) {
                    k <- keep & argo$type == view
                    visible <<- visible | k
                    lonlat <- argo[k,]
                    counts <- paste0(counts, view, ":", sum(k), " ")
                    colSettings <- list(core=if (state$Ccolour == "default") colDefaults$core else state$Ccolour,
                        bgc=if (state$Bcolour == "default") colDefaults$bgc else state$Bcolour,
                        deep=if (state$Dcolour == "default") colDefaults$deep else state$Dcolour)
                    symbSettings <- list(core=state$Csymbol, bgc=state$Bsymbol, deep=state$Dsymbol)
                    borderSettings <- list(core=state$Cborder, bgc=state$Bborder, deep=state$Dborder)
                    sizeSettings <- list(core=state$Csize, bgc=state$Bsize, deep=state$Dsize)
                    if ("lines" %in% state$action && !"path" %in% state$view) {
                        state$action <- NULL
                    }
                    if (!"lines" %in% state$action) {
                        if (symbSettings[[view]] == 21) {
                            points(lonlat$lon, lonlat$lat, pch=symbSettings[[view]], cex=sizeSettings[[view]], bg=colSettings[[view]], col=borderSettings[[view]], lwd=0.5)
                        } else {
                            points(lonlat$lon, lonlat$lat, pch=symbSettings[[view]], cex=sizeSettings[[view]], col=colSettings[[view]], bg=colSettings[[view]], lwd=0.5)
                        }
                    }
                    if (state$hoverIsPasted && highlight == TRUE) {
                        points(holdLongitude, holdLatitude, pch=21, col="red", bg="red")
                    }
                    if (!(state$polygon %in% FALSE)) {
                        points(unlist(state$data[,1]), unlist(state$data[,2]), pch=20, col="red", type="o", lwd=2)
                    }
                    if (state$polyDone && state$polygon == FALSE) {
                        polygon(unlist(state$data[,1]), unlist(state$data[,2]), border="gray", col=NA, lwd=2)
                    }
                    if ("path" %in% state$view) {
                        for (ID in unique(lonlat$ID)) {
                            LONLAT <- lonlat[lonlat$ID==ID,] # will be redefined in this loop
                            # Sort by time instead of relying on the order in the repository
                            o <- order(LONLAT$time)
                            LONLAT <- LONLAT[o, ]
                            no <- length(o)
                            startCycle <- list(longitude=head(LONLAT$lon, 1),
                                latitude=head(LONLAT$lat, 1),
                                cycle=head(LONLAT$cycle, 1))
                            endCycle <- list(longitude=tail(LONLAT$lon, 1),
                                latitude=tail(LONLAT$lat, 1),
                                cycle=tail(LONLAT$cycle, 1))
                            if (no > 1) {
                                pathColour <- list(core=if (state$CPcolour == "default") colDefaults$core else state$CPcolour,
                                    bgc=if (state$BPcolour == "default") colDefaults$bgc else state$BPcolour,
                                    deep=if (state$DPcolour == "default") colDefaults$deep else state$DPcolour)
                                pathWidth <- list(core=state$CPwidth, bgc=state$BPwidth, deep=state$DPwidth)
                                #message(pathColour[[view]], " is the path color")
                                # Chop data at the dateline
                                # https://github.com/ArgoCanada/argoFloats/issues/503
                                LONLAT <- sf::st_wrap_dateline(
                                    sf::st_sfc(
                                        sf::st_linestring(cbind(LONLAT$lon, LONLAT$lat)),
                                        crs="OGC:CRS84"))[[1]]
                                # message("class(lonlatSegments): ", paste(class(lonlatSegments), collapse=" "))
                                # Examination with the above indicates two choices: LINESTRING and MULTILINESTRING
                                if (inherits(LONLAT, "LINESTRING")) {
                                    lines(LONLAT[,1], LONLAT[,2],
                                        col=pathColour[[view]], lwd=pathWidth[[view]])
                                    if ("start" %in% state$action)
                                        points(startCycle$longitude, startCycle$latitude, pch=2, cex=1, lwd=1.4)
                                    if ("end" %in% state$action)
                                        points(endCycle$longitude, endCycle$latitude, pch=23, cex=1, lwd=1.4)
                                } else if (inherits(LONLAT, "MULTILINESTRING")) {
                                    for (seg in LONLAT) {
                                        lines(seg[,1], seg[,2], col=pathColour[[view]], lwd=1.4)
                                        if ("start" %in% state$action)
                                            points(startCycle$longitude, startCycle$latitude, pch=2, cex=1, lwd=1.4)
                                        if ("end" %in% state$action)
                                            points(endCycle$longitude, endCycle$latitude, pch=23, cex=1, lwd=1.4)
                                    }
                                }
                            }
                        }
                    }
                }
            }
            # Draw the inspection rectangle as a thick gray line, but only if zoomed
            if (-180 < state$xlim[1] || state$xlim[2] < 180 || -90 < state$ylim[1] || state$ylim[2] < 90)
                if (!(state$polyDone)) {
                    rect(state$xlim[1], state$ylim[1], state$xlim[2], state$ylim[2], border="darkgray", lwd=4)
                }
            # Write a margin comment
            argoFloatsDebug(debug, counts, "\n")
            if (!is.null(state$focusID)) {
                argoFloatsDebug(debug, "single-ID with", sum(visible), "profiles\n")
                mtext(sprintf("Float %s: %s to %s",
                        state$focusID,
                        format(state$startTime, "%Y-%m-%d", tz="UTC"),
                        format(state$endTime, "%Y-%m-%d", tz="UTC")),
                    side=3, cex=0.8 * par("cex"), line=0)
            } else {
                argoFloatsDebug(debug, "multi-ID with", sum(visible), "profiles\n")
                mtext(sprintf("%s to %s: %d Argo profiles",
                        format(state$startTime, "%Y-%m-%d", tz="UTC"),
                        format(state$endTime, "%Y-%m-%d", tz="UTC"),
                        sum(visible)),
                    side=3, line=0, cex=0.8 * par("cex"))
            }
        }                          # if (sum(c("core", "deep", "bgc") %in% state$view) > 0)
        # Add depth contours, if requested.
        if ("contour" %in% state$view)
            contour(topo[["longitude"]], topo[["latitude"]], topo[["z"]],
                levels=-1000*(1:10), drawlabels=FALSE, add=TRUE)
        # Add a scalebar, if we are zoomed in sufficiently. This whites-out below, so
        # we must do this as the last step of drawing.
        if (diff(range(state$ylim)) < 90 && sum(visible)) {
        argoFloatsDebug(debug, "drawing a scale bar because range(state$ylim)= ", range(state$ylim), "\n")
            oce::mapScalebar(x="topright", cex=0.8) }
        pushState(isolate(reactiveValuesToList(state)), debug=debug-1L)
        argoFloatsDebug(debug, "} # plotMap()\n", style="bold", unindent=1)
    }, execOnResize=TRUE, pointsize=18) # plotMap
}                                      # serverMapApp

shiny::shinyApp(ui=uiMapApp, server=serverMapApp)

Try the argoFloats package in your browser

Any scripts or data that you put into this service are public.

argoFloats documentation built on Oct. 18, 2023, 1:06 a.m.