R/functions.R

Defines functions chmTreeGaps verifyNumeric castListAsInteger castAsInteger getDimensions.Seurat getDimensions.umap chmAddAutoMenuItems bindExtraParams getExtraParams readConfigFile ngchmProtoParamCheck chmCreateManagedServer chmCreateServer getBuilderJar testJava getServerVersion ngchmResponseJSON ngchmGetHandleHTTR chmListServers chmAxisType chmAxis chmNewDialog shortnameslist namesdifferror validateAxisOrder validateNewAxisOrder validateCovariateBar validateNewCovariateBar validateNewLayer chmRegisterToolboxFunction chmListTypes chmRegisterTypeSplitter chmRegisterGetMetadataFunction chmListFunctions ngchmGetProtoParam ngchmGetServerProtocol ngchmListServerProtocols ngchmCreateServerProtocol chmRegisterFunction chmRegisterTypeMapper print.ngchm.type.info chmGetTypeInfo chmRegisterType chmRegisterMatrixFunction getAllMatrixTypeFunctions getAllAxisTypeFunctions getAllAxisFunctions getAllAxisTypes getDimensions.prcomp chmAddReducedDim getDimensions.default getDimensions chmAddUWOT chmAddUMAP chmAddPCA chmAddTSNE writeBinLines chmExportToHTML chmExportToPDF chmExportToFile readTile writeTile plot.ngchmVersion2 chmManager chmBrowse utempfile gitSha chmUU chmFixVersion chmFixDatasetVersion chmFixCovariateVersion getuuid chmGetOverview chmWriteCustomJS chmRegisterAxisFunction chmGetFunction chmNewServer chmColors chmColorMap chmLabel chmProperties chmProperty chmNewProperty checkPropertyValue checkLabel chmNewFunction chmAddValueProperty chmNewColorMap chmNewCovariateBar chmCovariateBar ngchmNewBar chmCovariate chmNewCovariate chmNewDataset chmLayer chmNewDataLayer chmAddList chmAddAxis chmRandomColOrder chmOriginalColOrder chmRandomRowOrder chmOriginalRowOrder chmDefaultRowOrder chmUserLabelsToShaid chmUserDendrogramToShaid chmDefaultColOrder cos.dist1 chmNew ngchmGetEnv chmServer ngchmUnregisterServer ngchmRegisterServer classstr isany systemCheck initLogging

Documented in castAsInteger castListAsInteger chmAddPCA chmAddReducedDim chmAddTSNE chmAddUMAP chmAddUWOT chmAxis chmAxisType chmBrowse chmColorMap chmColors chmCovariate chmCovariateBar chmCreateManagedServer chmCreateServer chmDefaultColOrder chmDefaultRowOrder chmExportToFile chmExportToHTML chmExportToPDF chmGetFunction chmGetOverview chmGetTypeInfo chmLabel chmLayer chmListFunctions chmListServers chmListTypes chmManager chmNew chmNewColorMap chmNewCovariate chmNewCovariateBar chmNewDataLayer chmNewDataset chmNewDialog chmNewFunction chmNewProperty chmNewServer chmOriginalColOrder chmOriginalRowOrder chmProperties chmProperty chmRandomColOrder chmRandomRowOrder chmRegisterAxisFunction chmRegisterFunction chmRegisterGetMetadataFunction chmRegisterMatrixFunction chmRegisterToolboxFunction chmRegisterType chmRegisterTypeMapper chmRegisterTypeSplitter chmServer chmTreeGaps chmWriteCustomJS getDimensions getDimensions.default getDimensions.prcomp getDimensions.Seurat getDimensions.umap initLogging ngchmCreateServerProtocol ngchmGetEnv ngchmGetHandleHTTR ngchmGetProtoParam ngchmGetServerProtocol ngchmListServerProtocols ngchmNewBar ngchmProtoParamCheck ngchmRegisterServer ngchmResponseJSON ngchmUnregisterServer plot.ngchmVersion2 print.ngchm.type.info verifyNumeric

#' @import methods
#' @import utils
NULL

#' Initialize Logging
#'
#' This function initializes logging for the application.
#'
#' @param log_level A single character string specifying the log level. This should
#' be one of 'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', or 'FATAL'.
#' @param log_file An optional character string specifying the name of the file where
#' the log will be written. If this is NULL, the log will be written to the console.
#' @export
#' @importFrom logger log_threshold
#' @importFrom logger log_info
#' @importFrom logger log_layout
#' @importFrom logger log_debug
#' @importFrom logger log_error
#' @importFrom logger log_appender
#' @importFrom logger appender_file
#' @importFrom logger appender_console
#' @importFrom logger layout_glue_generator
#' @importFrom logger layout_glue
#' @return None. This function is used for its side effects of initializing the logging.
initLogging <- function(log_level, log_file = NULL) {
  log_threshold(log_level)
  if (!is.null(log_file)) {
    log_appender(appender_file(log_file))
    log_layout(layout_glue_generator(format = paste(
      "{level}",
      "[{format(time, '%Y-%m-%d %H:%M:%S')}]",
      "{fn}",
      "{msg}"
    )))
    log_info("Writing log to file: ", log_file)
  } else {
    log_appender(appender_console)
    log_layout(layout_glue_generator(format = paste(
      "{crayon::bold(colorize_by_log_level(level, levelr))}",
      "[{crayon::italic(format(time, '%Y-%m-%d %H:%M:%S'))}]",
      "{crayon::blue(fn)}",
      "{grayscale_by_log_level(msg, levelr)}"
    )))
  }
  log_debug("Set log level to ", log_level)
}

systemCheck <- function(command, ...) {
  # Execute the specified command and halt execution with an error
  # message if it fails.
  status <- system(command, ...)
  if (status != 0) {
    stop("Error encountered executing system command: ", command)
  }
}

# Returns TRUE if object has any of the specified classes.
#
isany <- function(object, classes) {
  any(vapply(classes, function(cls) is(object, cls), TRUE))
}
# Returns the class(es) of object as a single string.
#
classstr <- function(object) {
  paste(class(object), collapse = ",")
}
#' Register an ngchmServer.
#'
#' This function registers an ngchmServer that can be used when
#' making and installing a Next Generation Clustered Heat Map.
#'
#' @param uuid A string that identifies the server namespace.
#' @param server The ngchmServer to register.
#'
#' @return the server that was registered
#' @export
#'
#' @seealso [chmInstall()]
#' @seealso [chmUninstall()]
#' @seealso [ngchmUnregisterServer()]
#' @seealso [ngchmServer-class]
#'
ngchmRegisterServer <- function(uuid, server) {
  obj <- list(uuid = uuid, name = server@name, server = server)
  matches <- which(vapply(ngchm.env$servers, function(srv) ((srv$uuid == uuid) && (srv$name == server@name)), TRUE))
  if (length(matches) > 0) {
    ngchm.env$servers[[matches]] <- obj
  } else {
    ngchm.env$servers <- append(ngchm.env$servers, list(obj))
  }
  server
}

#' Unregister NG-CHM Server
#'
#' This function unregisters a server for NG-CHM (Next-Generation Clustered Heat
#' Map) by its UUID and optionally by its name.
#'
#' @param uuid A single character string specifying the UUID of the server.
#' @param name The names(s) of the ngchmServer(s) to unregister.
#' If not specified, all ngchmServers in the namespace are unregistered.
#' Defaults to NULL.
#' 
#' @return None. This function is used for its side effects of unregistering the server.
#' 
#' @seealso [ngchmRegisterServer()]
#' @seealso [ngchmServer-class]
#' 
#' @export
ngchmUnregisterServer <- function(uuid, name = NULL) {
  if (length(name) == 0) {
    matches <- vapply(ngchm.env$servers, function(srv) (srv$uuid == uuid), TRUE)
  } else {
    matches <- vapply(ngchm.env$servers, function(srv) ((srv$uuid == uuid) && (srv$name %in% name)), TRUE)
  }
  if (sum(matches) > 0) {
    ngchm.env$servers <- ngchm.env$servers[!matches]
  }
}

#' Get a registered ngchmServer object for use in making and installing NGCHMs
#'
#' This function returns a ngchmServer object that can be used when
#' making and installing a Next Generation Clustered Heat Map.
#'
#' @param name The name of the ngchmServer desired.
#'
#' @return An object of class ngchmServer if found, NULL otherwise.  If multiple servers of the same
#' name have been defined (in different namespaces), the most recently defined is returned.
#'
#' @export
#'
#' @seealso [chmInstall()]
#' @seealso [chmUninstall()]
#' @seealso [ngchmServer-class]
#'
chmServer <- function(name) {
  matches <- which(vapply(ngchm.env$servers, function(srv) (srv$name == name), TRUE))
  if (length(matches) > 0) {
    return(ngchm.env$servers[[matches[length(matches)]]]$server)
  } else {
    return(NULL)
  }
}

#' Get the ngchm environment (for debugging only).
#'
#' Get the library's internal ngchm environment to help debugging.
#'
#' @export
#' 
#' @return A list representing the current environment for NG-CHM.
ngchmGetEnv <- function() {
  return(ngchm.env)
}

#' Create a new NGCHM.
#'
#' This function creates a Next Generation Clustered Heat Map (NGCHM) object in memory.
#' Additional parameters will be added to the new NGCHM (see chmAdd).
#' The bare NGCHM needs at least one data layer added to it before it can be compiled.
#' This function requires **git** to be installed.
#'
#' @param name The name under which the NGCHM will be saved to the NGCHM server.
#' @param ... Zero or more initial objects to include in the NGCHM (see chmAdd).
#' @param rowOrder A vector, dendrogram, or function specifying the CHM row order.
#' @param rowDist Distance method to use by default RowOrder
#' @param rowAgglom Agglomeration method to use by default RowOrder
#' @param colOrder A vector, dendrogram, or function specifying the CHM column order.
#' @param colDist Distance method to use by default ColOrder
#' @param colAgglom Agglomeration method to use by default ColOrder
#' @param rowAxisType The type(s) of the row labels (default: None).
#' @param colAxisType The type(s) of the column labels (default: None).
#' @param rowCovariates Covariate(Bar)(s) to add to the rows (default: None).
#' @param colCovariates Covariate(Bar)(s) to add to the columns (default: None).
#' @param format The format of NGCHM to produce (default: 'original').
#' @param rowGapLocations Locations for row gaps. Specify as a list of integers or [chmTreeGaps()] function.
#' @param colGapLocations Locations for col gaps. Specify as a list of integers or [chmTreeGaps()] function.
#' @param rowGapWidth Width of row gaps (default: 5 rows)
#' @param colGapWidth Width of col gaps (default: 5 cols)
#' @param overview The format(s) of overview image(s) to create (default: None).
#' @param logLevel The level of logs to output
#' @param logFile The file to which logs should be output
#' @importFrom logger log_debug
#' @importFrom logger log_error
#'
#' @return An object of class ngchm
#'
#' @export
#'
#' @examples
#' mychm <- chmNew("test_chm")
#' mychm <- chmNew("test_chm", rowGapLocations = c(3, 5))
#' mychm <- chmNew("test_chm", rowGapLocations = chmTreeGaps(4))
#' mychm <- chmNew("test_chm", rowGapWidth = 3)
#'
#' @seealso [ngchm-class]
#' @seealso [ngchmServer-class]
#' @seealso [chmAdd()]
#' @seealso [chmAddAxisType()]
#' @seealso [chmAddCovariateBar()]
#' @seealso [chmAddProperty()]
#' @seealso [chmAddOverview()]
#' @seealso [chmInstall()]
#' @seealso [chmExportToFile()]
#' @seealso [chmExportToPDF()]
#' @seealso [chmExportToHTML()]

chmNew <- function(
    name, ...,
    rowOrder = chmDefaultRowOrder, rowDist = "correlation", rowAgglom = "ward.D2",
    colOrder = chmDefaultColOrder, colDist = "correlation", colAgglom = "ward.D2",
    rowAxisType = NULL, colAxisType = NULL,
    rowCovariates = NULL, colCovariates = NULL,
    format = "original",
    rowGapLocations = NULL,
    rowGapWidth = 5,
    colGapLocations = NULL,
    colGapWidth = 5,
    overview = c(),
    logLevel = "INFO", logFile = NULL) {
  initLogging(logLevel, logFile)
  chm <- new(
    Class = "ngchmVersion2",
    name = name,
    format = format,
    inpDir = utempfile("ngchm.input"),
    outDir = utempfile("ngchm.output"),
    rowDist = rowDist,
    rowAgglom = rowAgglom,
    colDist = colDist,
    colAgglom = colAgglom,
    rowOrderMethod = "",
    colOrderMethod = "",
    rowCutLocations = rowGapLocations,
    rowCutWidth = rowGapWidth,
    colCutLocations = colGapLocations,
    colCutWidth = colGapWidth
  )
  chmRowOrder(chm) <- rowOrder
  chmColOrder(chm) <- colOrder
  chm@uuid <- getuuid(name)
  chm <- chmAddCSS(chm, "div.overlay { border: 2px solid yellow; }")
  chm <- chmAddList(chm, list(...))
  if (!is.null(rowAxisType)) chm <- chmAddAxisType(chm, "row", rowAxisType)
  if (!is.null(colAxisType)) chm <- chmAddAxisType(chm, "column", colAxisType)
  if (!is.null(rowCovariates)) {
    if (is.list(rowCovariates)) {
      for (cov in rowCovariates) chm <- chmAddCovariateBar(chm, "row", cov)
    } else {
      chm <- chmAddCovariateBar(chm, "row", cov)
    }
  }
  if (!is.null(colCovariates)) {
    if (is.list(colCovariates)) {
      for (cov in colCovariates) chm <- chmAddCovariateBar(chm, "column", cov)
    } else {
      chm <- chmAddCovariateBar(chm, "col", cov)
    }
  }
  for (ov in overview) chm <- chmAddOverview(chm, ov, NULL, NULL)
  chm
}

#' Compute cosine (angular) distance matrix
#'
#' @param data A numeric matrix
#' @noRd
cos.dist1 <- function(data) {
  dmat <- matrix(0.0, nrow = nrow(data), ncol = nrow(data))
  sumrowsqr <- sqrt(apply(data * data, 1, sum))
  inner <- data %*% t(data)
  dmat <- inner / (sumrowsqr %*% t(sumrowsqr))
  # Adjust distance matrix to avoid clustering error with identical rows.
  rrr <- matrix(abs(rnorm(length(dmat), sd = 1e-10)), nrow = nrow(dmat))
  dmat <- (1 - rrr) * dmat
  return(2 / pi * acos(as.dist(dmat)))
}

#' Return default column order of an NGCHM
#'
#' @param chm An NGCHM containing at least one layer
#'
#' @return Shaid of a dendrogram suitable for use as the
#'         chm's column order.
#'
#' @export
chmDefaultColOrder <- function(chm) {
  chm <- chmFixVersion(chm)
  if (length(chm@layers) == 0) stop("chm requires at least one layer")
  shaidyRepo <- ngchm.env$tmpShaidy
  shaid <- chm@layers[[1]]@data

  provid <- shaidyProvenance(shaidyRepo,
    name = "chmDefaultColOrder",
    shaid = shaid@value, dist = chm@colDist,
    agglom = chm@colAgglom
  )
  res <- shaidyRepo$provenanceDB$get("dendrogram", provid)
  if (length(res) == 0) {
    mat <- ngchmLoadDatasetBlob(shaidyRepo, shaid)$mat
    if (chm@colDist == "correlation") {
      dd <- as.dist(1 - cor(mat, use = "pairwise"))
    } else if (chm@colDist == "cosine") {
      dd <- cos.dist1(t(mat))
    } else {
      dd <- dist(t(mat), method = chm@colDist)
    }
    ddg <- stats::as.dendrogram(stats::hclust(dd, method = chm@colAgglom))
    res <- list(ngchmSaveAsDendrogramBlob(shaidyRepo, ddg))
    shaidyRepo$provenanceDB$insert(provid, res[[1]])
  }
  res[[1]]
}

#' Convert a user specified dendrogram to a shaid
#'
#' @param ddg The dendrogram to convert.
#' @noRd
chmUserDendrogramToShaid <- function(ddg) {
  shaidyRepo <- ngchm.env$tmpShaidy
  ngchmSaveAsDendrogramBlob(shaidyRepo, ddg)
}

#' Convert user specified labels to a shaid
#'
#' @param labels A string vector containing axis labels.
#' @noRd
chmUserLabelsToShaid <- function(labels) {
  shaidyRepo <- ngchm.env$tmpShaidy
  ngchmSaveLabelsAsBlob(shaidyRepo, labels)
}

#' Return default row order of an NGCHM
#'
#' @param chm An NGCHM containing at least one layer
#'
#' @return Shaid of a dendrogram suitable for use as the
#'         chm's row order.
#'
#' @export
chmDefaultRowOrder <- function(chm) {
  chm <- chmFixVersion(chm)
  if (length(chm@layers) == 0) stop("chm requires at least one layer")
  shaidyRepo <- ngchm.env$tmpShaidy
  shaid <- chm@layers[[1]]@data

  provid <- shaidyProvenance(shaidyRepo,
    name = "chmDefaultRowOrder",
    shaid = shaid@value, dist = chm@rowDist,
    agglom = chm@rowAgglom
  )
  res <- shaidyRepo$provenanceDB$get("dendrogram", provid)
  if (length(res) == 0) {
    mat <- ngchmLoadDatasetBlob(shaidyRepo, shaid)$mat
    if (chm@rowDist == "correlation") {
      dd <- as.dist(1 - cor(t(mat), use = "pairwise"))
    } else if (chm@rowDist == "cosine") {
      dd <- cos.dist1(mat)
    } else {
      dd <- dist(mat, method = chm@rowDist)
    }
    ddg <- stats::as.dendrogram(stats::hclust(dd, method = chm@rowAgglom))
    res <- list(ngchmSaveAsDendrogramBlob(shaidyRepo, ddg))
    shaidyRepo$provenanceDB$insert(provid, res[[1]])
  }
  res[[1]]
}

#' Return original row order of an NGCHM
#'
#' @param chm An NGCHM containing at least one layer
#'
#' @return Shaid of a label order suitable for use as the
#'         chm's row order.
#'
#' @export
chmOriginalRowOrder <- function(chm) {
  chm <- chmFixVersion(chm)
  if (length(chm@layers) == 0) stop("chm requires at least one layer")
  shaidyRepo <- ngchm.env$tmpShaidy
  shaid <- chm@layers[[1]]@data

  provid <- shaidyProvenance(shaidyRepo,
    name = "chmOriginalRowOrder",
    shaid = shaid@value
  )
  res <- shaidyRepo$provenanceDB$get("label", provid)
  if (length(res) == 0) {
    mat <- ngchmLoadDatasetBlob(shaidyRepo, shaid)$mat
    labels <- rownames(mat)
    labels.shaid <- ngchmSaveLabelsAsBlob(shaidyRepo, labels)
    shaidyRepo$provenanceDB$insert(provid, labels.shaid)
    res <- list(labels.shaid)
  }
  res[[1]]
}

#' Return random row order of an NGCHM
#'
#' @param chm An NGCHM containing at least one layer
#'
#' @return Shaid of a label order suitable for use as the
#'         chm's row order.
#'
#' @export
chmRandomRowOrder <- function(chm) {
  chm <- chmFixVersion(chm)
  if (length(chm@layers) == 0) stop("chm requires at least one layer")
  shaidyRepo <- ngchm.env$tmpShaidy
  shaid <- chm@layers[[1]]@data

  provid <- shaidyProvenance(shaidyRepo,
    name = "chmRandomRowOrder",
    shaid = shaid@value
  )
  res <- shaidyRepo$provenanceDB$get("label", provid)
  if (length(res) == 0) {
    mat <- ngchmLoadDatasetBlob(shaidyRepo, shaid)$mat
    labels <- rownames(mat)[sample.int(nrow(mat), nrow(mat))]
    labels.shaid <- ngchmSaveLabelsAsBlob(shaidyRepo, labels)
    shaidyRepo$provenanceDB$insert(provid, labels.shaid)
    res <- list(labels.shaid)
  }
  res[[1]]
}

#' Return original column order of an NGCHM
#'
#' @param chm An NGCHM containing at least one layer
#'
#' @return Shaid of a label order suitable for use as the
#'         chm's column order.
#'
#' @export
chmOriginalColOrder <- function(chm) {
  chm <- chmFixVersion(chm)
  if (length(chm@layers) == 0) stop("chm requires at least one layer")
  shaidyRepo <- ngchm.env$tmpShaidy
  shaid <- chm@layers[[1]]@data

  provid <- shaidyProvenance(shaidyRepo,
    name = "chmOriginalColOrder",
    shaid = shaid@value
  )
  res <- shaidyRepo$provenanceDB$get("label", provid)
  if (length(res) == 0) {
    mat <- ngchmLoadDatasetBlob(shaidyRepo, shaid)$mat
    labels <- colnames(mat)
    labels.shaid <- ngchmSaveLabelsAsBlob(shaidyRepo, labels)
    shaidyRepo$provenanceDB$insert(provid, labels.shaid)
    res <- list(labels.shaid)
  }
  res[[1]]
}

#' Return random column order of an NGCHM
#'
#' @param chm An NGCHM containing at least one layer
#'
#' @return Shaid of a label order suitable for use as the
#'         chm's column order.
#'
#' @export
chmRandomColOrder <- function(chm) {
  chm <- chmFixVersion(chm)
  if (length(chm@layers) == 0) stop("chm requires at least one layer")
  shaidyRepo <- ngchm.env$tmpShaidy
  shaid <- chm@layers[[1]]@data

  provid <- shaidyProvenance(shaidyRepo,
    name = "chmRandomColOrder",
    shaid = shaid@value
  )
  res <- shaidyRepo$provenanceDB$get("label", provid)
  if (length(res) == 0) {
    mat <- ngchmLoadDatasetBlob(shaidyRepo, shaid)$mat
    labels <- colnames(mat)[sample.int(ncol(mat), ncol(mat))]
    labels.shaid <- ngchmSaveLabelsAsBlob(shaidyRepo, labels)
    shaidyRepo$provenanceDB$insert(provid, labels.shaid)
    res <- list(labels.shaid)
  }
  res[[1]]
}

chmAddAxis <- function(chm, axis) {
  stopifnot(is(chm, "ngchm") && is(axis, "ngchmAxis"))
  where <- match.arg(axis@axis, c("row", "column", "both"))
  chm <- chmFixVersion(chm)
  for (item in axis@objects) {
    if (is(item, "ngchmAxisType")) {
      item@where <- where
      chm@axisTypes <- append(chm@axisTypes, item)
      chm <- chmAddProperty(chm, paste("axistype", where, sep = "."), item@type)
    } else if (isany(item, c("ngchmBar", "ngchmCovariate"))) {
      chm <- chmAddCovariateBar(chm, where, item)
    } else {
      stop(sprintf("Unable to add item of class '%s' to ngchm axis\n", classstr(item)))
    }
  }
  chm
}

# Function used by chmNew and chmAdd:
chmAddList <- function(chm, args) {
  for (item in args) {
    if (is(item, "ngchmLayer")) {
      chm <- chmAddLayer(chm, item)
    } else if (is(item, "matrix") && (mode(item) == "numeric")) {
      chm <- chmAddLayer(chm, item)
    } else if (is(item, "ngchmDataset")) {
      chm <- chmAddDataset(chm, item)
    } else if (is(item, "ngchmColormap")) {
      chm <- chmAddColormap(chm, item)
    } else if (is(item, "ngchmDialog")) {
      chm <- chmAddDialog(chm, item)
    } else if (is(item, "ngchmProperty")) {
      chmProperty(chm, item@label) <- item
    } else if (is(item, "ngchmAxis")) {
      chm <- chmAddAxis(chm, item)
    } else if (is(item, "list")) {
      chm <- chmAddList(chm, item)
    } else {
      stop(sprintf("Unable to add item of class '%s' to ngchm\n", classstr(item)))
    }
  }
  chm
}

#' Create a new Data Layer for a NGCHM.
#'
#' This function creates a new Data Layer suitable for adding to a Next Generation Clustered Heat Map.
#'
#' @param label The name under which the data layer will be displayed to the user.
#' @param data A matrix containing the data to display. Must have rownames and colnames.
#' @param colors A color map specifying how the data should be rendered.  If omitted or NULL,
#' a default green-black-red color map will be estimated from the data.
#' @param summarizationMethod The method to use when summarizing multiple data points per pixel.  Possible values are average (default), sample, and mode.
#' @param cuts_color color of cuts
#'
#' @return An object of class ngchmLayer
#'
#' @export
#'
#' @examples
#' noise <- matrix(runif(1000) + runif(1000 * 1000), nrow = 1000)
#' rownames(noise) <- sprintf("Row%d", 1:nrow(noise))
#' colnames(noise) <- sprintf("Col%d", 1:ncol(noise))
#' noise.colors <- chmNewColorMap(c(0, 1, 2),
#'   c("green", "black", "red"),
#'   missing.color = "yellow"
#' )
#' layer <- chmNewDataLayer("Noisy Data", noise, noise.colors)
#'
#' @seealso [ngchmLayer-class]
#' @seealso [chmNewColorMap()]
#' @seealso [chmAddLayer()]
#'
chmNewDataLayer <- function(label, data, colors, summarizationMethod, cuts_color) {
  if (typeof(label) != "character") {
    stop(sprintf("Parameter 'label' must have type 'character', not '%s'", typeof(label)))
  }
  if (length(label) != 1) {
    stop(sprintf("Parameter 'label' must have a single value, not %d", length(label)))
  }
  if (nchar(label) == 0) {
    stop("Parameter 'label' cannot be the empty string")
  }
  if (missing(colors)) colors <- NULL
  if (missing(summarizationMethod)) summarizationMethod <- "average"
  if (missing(cuts_color)) cuts_color <- "#4c4c4c"
  if (typeof(summarizationMethod) != "character") {
    stop(sprintf("Parameter 'summarizationMethod' must have type 'character', not '%s'", typeof(summarizationMethod)))
  }
  if (length(summarizationMethod) != 1) {
    stop(sprintf("Parameter 'summarizationMethod' must have a single value, not %d", length(summarizationMethod)))
  }
  if (typeof(cuts_color) != "character") {
    stop(sprintf("Parameter 'cuts_color' must have type 'character', not '%s'", typeof(cuts_color)))
  }
  if (length(cuts_color) != 1) {
    stop(sprintf("Parameter 'cuts_color' must have a single value, not %d", length(cuts_color)))
  }
  summarizationMethod <- match.arg(summarizationMethod, c("average", "sample", "mode"))
  data <- ngchmSaveAsDatasetBlob(ngchm.env$tmpShaidy, "tsv", data)
  if (length(colors) == 0) {
    colors <- chmNewColorMap(data, c("#0010a0", "#f0f0f0", "#a01000"), missing.color = "#ff00ff")
  } # Blue, Off-white, Red. Missing=bright magenta.
  new(Class = "ngchmLayer", name = label, data = data, colors = colors, summarizationMethod = summarizationMethod, cuts_color = cuts_color)
}
#' Get a specified Data Layer from an NG-CHM.
#'
#' This function returns a Data Layer contained in a Next Generation Clustered Heat Map.
#'
#' @param hm The NG-CHM object to get the data layer from.
#' @param label The name or index of the data layer to get.  If a name, return the layer with
#'        that name.  If no layer with that name exists or if the index is out of range,
#'        return NULL.
#'
#' @return An object of class ngchmLayer or NULL.
#'
#' @export
#'
#' @examples
#' # Examples using `chmNew()` require git to be installed and available.
#' \dontrun{
#'  # If the NGCHMDemoData package is installed, use it to create an example usage
#'  if (requireNamespace("NGCHMDemoData", quietly = TRUE)) {
#'     # Create example NGCHM
#'     data(TCGA.GBM.Demo, package = "NGCHMDemoData")
#'     matrix <- TCGA.GBM.ExpressionData[1:50, 1:50]
#'     hm <- chmNew("New Heat Map") + chmNewDataLayer("my layer", matrix)
#'     layer <- chmLayer(hm, "my layer")
#'     same_layer <- chmLayer(hm, 1)
#'   }
#'   # Small example not requiring NGCHMDemoData
#'   matrix <- matrix(rnorm(100),
#'     nrow = 10, ncol = 10,
#'     dimnames = list(paste0("r", 1:10), paste0("c", 1:10))
#'   )
#'   hm <- chmNew("New Heat Map") + chmNewDataLayer("my layer", matrix)
#'   layer <- chmLayer(hm, "my layer")
#'   same_layer <- chmLayer(hm, 1)
#'}
#' 
#' @seealso [ngchmLayer-class]
#'
chmLayer <- function(hm, label) {
  stopifnot(!missing(hm), is(hm, "ngchm"))
  stopifnot(missing(label) || length(label) == 1)
  if (length(hm@layers) == 0) {
    return(NULL)
  }
  # Determine numeric layeridx from specified label
  if (missing(label)) {
    layeridx <- 1
  } else if (mode(label) == "character") {
    layernames <- vapply(hm@layers, function(x) x@name, "")
    layeridx <- which(label == layernames)
    if (length(layeridx) == 0) {
      return(NULL)
    }
    if (length(layeridx) > 1) stop("NGCHM contains multiple layers with that name")
  } else if (mode(label) == "numeric") {
    if ((label < 1) || (label > length(hm@layers))) {
      return(NULL)
    }
    layeridx <- label
  } else {
    stop("mode of label must be character or numeric")
  }
  hm@layers[[layeridx]]
}
#' Set (or append) a specified Data Layer in an NG-CHM.
#'
#' This function sets a Data Layer in a Next Generation Clustered Heat Map.
#'
#' @param x The NG-CHM object to set the data layer of
#' @param label The name or index of the data layer to set.  If a name, replace the layer with
#'        that name.  Append a new layer if no layer with that name exists.  If an index,
#' 	  replace the specified layer.  If zero (0), prepend the new layer.  If minus one (-1)
#'        or N+1 (for an NG-CHM with N layers), appends a new layer.
#' @param colors A colormap for the new layer.  If missing, defaults to the color map of the
#'        layer being replaced, or to the default new layer color map for a new layer.
#' @param summarizationMethod The summarization method for the new layer.  If
#'        missing, defaults to the summarization method of the layer being
#'        replaced, or to the default new layer summarization method for a new layer.
#' @param cuts_color The cuts color for the new layer.  If
#'        missing, defaults to the cuts color of the layer being
#'        replaced, or to the default cuts color for a new layer.
#' @param value Either a matrix or a data layer to set in the NG-CHM.  If value is a matrix,
#'        the other data layer parameters (label, colors, summarizationMethod,
#'        and cuts_color) are set from the parameters if specified, from the old
#'        data layer (if any), or the defaults for a new data layer (see chmNewDataLayer).
#'        If value is a data layer, any other data layer parameters specified will
#'        override those in the replacement layer.
#'
#' @return An object of class ngchm.
#'
#' @name chmLayer<-
#' @export
#'
#' @examples
#' # If the NGCHMDemoData package is installed, use demo usage
#' if (requireNamespace("NGCHMDemoData", quietly = TRUE)) {
#'   data(TCGA.GBM.Demo, package = "NGCHMDemoData")
#'   matrix <- TCGA.GBM.ExpressionData[1:50, 1:50]
#'   hm <- chmNew("New Heat Map")
#'   chmLayer(hm, "Layer 1") <- matrix
#'   chmLayer(hm, 1, cuts_color = "#fefefe") <- chmNewDataLayer("New data layer", matrix + 1)
#' }
#' # Small example not requiring NGCHMDemoData
#' matrix <- matrix(rnorm(100),
#'   nrow = 10, ncol = 10,
#'   dimnames = list(paste0("r", 1:10), paste0("c", 1:10))
#' )
#' hm <- chmNew("New Heat Map")
#' chmLayer(hm, "Layer 1") <- matrix
#' chmLayer(hm, 1, cuts_color = "#fefefe") <- chmNewDataLayer("New data layer", matrix + 1)
#'
#' @seealso [ngchmLayer-class]
#' @seealso chmNewDataLayer
#'
assign("chmLayer<-", function(x, label, colors, summarizationMethod, cuts_color, value) {
  stopifnot(!missing(x), is(x, "ngchm"))
  stopifnot(missing(label) || length(label) == 1)
  stopifnot(!missing(value), isany(value, c("matrix", "ngchmLayer")))
  hm <- chmFixVersion(x)
  # Convert label into a numeric layeridx and a character label
  if (missing(label)) {
    layeridx <- length(hm@layers) + 1
    label <- sprintf("Layer %d", layeridx)
  } else if (mode(label) == "character") {
    layernames <- vapply(hm@layers, function(x) x@name, "")
    layeridx <- which(label == layernames)
    if (length(layeridx) > 1) stop("NGCHM contains multiple layers with that name")
    if (length(layeridx) == 0) layeridx <- length(hm@layers) + 1
  } else if (mode(label) == "numeric") {
    if ((label < -1) || (label > length(hm@layers) + 1)) stop("Invalid label")
    if (label < 0) {
      layeridx <- length(hm@layers) + 1
    } else {
      layeridx <- label
    }
    if (label == 0) {
      label <- "Layer 0"
    } else if (label > length(hm@layers)) {
      label <- sprintf("Layer %d", length(hm@layers) + 1)
    } else {
      label <- hm@layers[[layeridx]]@name
    }
  } else {
    stop("mode of label must be either character or numeric")
  }
  if (is(value, "matrix")) {
    if (layeridx <= length(hm@layers)) {
      tmp <- hm@layers[[layeridx]]
      if (missing(colors)) colors <- tmp@colors
      if (missing(summarizationMethod)) summarizationMethod <- tmp@summarizationMethod
      if (missing(cuts_color)) cuts_color <- tmp@cuts_color
    }
    newlayer <- chmNewDataLayer(label, data = value, colors = colors, summarizationMethod = summarizationMethod, cuts_color = cuts_color)
  } else if (is(value, "ngchmLayer")) {
    newlayer <- value
    if (!missing(colors)) newlayer@colors <- colors
    if (!missing(summarizationMethod)) newlayer@summarizationMethod <- summarizationMethod
    if (!missing(cuts_color)) newlayer@cuts_color <- cuts_color
  }
  validateNewLayer(hm, newlayer)
  if (layeridx == 0) {
    hm@layers <- c(newlayer, hm@layers)
  } else {
    hm@layers[[layeridx]] <- newlayer
  }
  chmAddColormap(hm, newlayer@colors)
})
#' Create a new Dataset for a NGCHM.
#'
#' This function creates a new Dataset suitable for attaching to a Next Generation Clustered Heat Map.
#'
#' @param name The filename prefix under which the dataset will be saved to the ngchm.
#' @param description A description of the dataset.
#' @param data A matrix containing the data in the dataset. Must have rownames and colnames.
#' @param row.covariates An optional list of row covariates.
#' @param column.covariates An optional list of column covariates.
#' @param row.type The type, if any, of the dataset rows.
#' @param column.type The type, if any, of the dataset columns.
#'
#' @return An object of class ngchmDataset
#'
#' @export
#'
#' @seealso [ngchmDataset-class]
#' @seealso [ngchmCovariate-class]
#' @seealso [chmAddDataset()]
#'
chmNewDataset <- function(name, description, data,
                          row.type = NULL,
                          column.type = NULL,
                          row.covariates = NULL,
                          column.covariates = NULL) {
  if (typeof(name) != "character") {
    stop(sprintf("Parameter 'name' must have type 'character', not '%s'", typeof(name)))
  }
  if (length(name) != 1) {
    stop(sprintf("Parameter 'name' must have a single value, not %d", length(name)))
  }
  if (nchar(name) == 0) {
    stop("Parameter 'name' cannot be the empty string")
  }
  if (typeof(description) != "character") {
    stop(sprintf("Parameter 'description' must have type 'character', not '%s'", typeof(description)))
  }
  if (length(description) != 1) {
    stop(sprintf("Parameter 'description' must have a single value, not %d", length(description)))
  }
  if (nchar(description) == 0) {
    warning("Parameter 'description' should not be the empty string")
  }
  data <- ngchmSaveAsDatasetBlob(ngchm.env$tmpShaidy, "tsv", data)
  if (length(row.covariates) > 0) {
    if (is(row.covariates, "ngchmCovariate")) {
      row.covariates <- list(row.covariates)
    } else if (is(row.covariates, "list")) {
      stopifnot(all(vapply(row.covariates, function(cov) is(cov, "ngchmCovariate"), TRUE)))
    } else {
      stop(sprintf(
        "Parameter 'row.covariates' for dataset '%s' must be either a covariate or list of covariates, not a '%s'",
        name, classstr(row.covariates)
      ))
    }
  }
  if (length(column.covariates) > 0) {
    if (is(column.covariates, "ngchmCovariate")) {
      column.covariates <- list(column.covariates)
    } else if (is(column.covariates, "list")) {
      stopifnot(all(vapply(column.covariates, function(cov) is(cov, "ngchmCovariate"), TRUE)))
    } else {
      stop(sprintf(
        "Parameter 'column.covariates' for dataset '%s' must be either a covariate or list of covariates, not a '%s'",
        name, classstr(column.covariates)
      ))
    }
  }
  new(
    Class = "ngchmDataset", name = name, description = description, data = data,
    row.type = row.type,
    column.type = column.type,
    row.covariates = row.covariates,
    column.covariates = column.covariates
  )
}

#' Create a new Covariate for adding to an NGCHM auxilary dataset.
#'
#' This function creates a new Covariate suitable for a covariate bar or attaching to an NGCHM auxilary dataset.
#'
#' @param fullname The full (human readable) name of the covariate.
#' @param values A named vector of values (character, logical, or numeric).
#' @param value.properties An ngchmColormap mapping values to properties.
#' @param type The string "discrete" or the string "continuous".  (Defaults to continuous for numeric values, to discrete otherwise.)
#' @param covabbv The short R-compatible identifier used to identify the covariate (derived from fullname if not specified).
#'
#' @return An object of class ngchmCovariate.
#'
#' @export
#'
#' @seealso [ngchmCovariate-class]
#' @seealso [chmAddCovariate()]
#' @seealso [chmNewColorMap()]
#'
chmNewCovariate <- function(fullname, values, value.properties = NULL, type = NULL, covabbv = NULL) {
  # Validate basic properties of 'fullname'.
  if (typeof(fullname) != "character") {
    stop(sprintf("Parameter 'fullname' must have type 'character', not '%s'", typeof(fullname)))
  }
  if (length(fullname) != 1) {
    stop(sprintf("Parameter 'fullname' must have a single value, not %d", length(fullname)))
  }
  if (nchar(fullname) == 0) {
    stop(sprintf("Parameter 'fullname' cannot be the empty string"))
  }
  # Validate basic properties of 'type'.
  if (length(type) == 0) {
    if (mode(values) == "numeric") {
      type <- "continuous"
    } else {
      type <- "discrete"
    }
  } else {
    if (typeof(type) != "character") {
      stop(sprintf("Parameter 'type' for covariate '%s' must have type 'character', not '%s'", fullname, typeof(type)))
    }
    if (length(type) != 1) {
      stop(sprintf("Parameter 'type' for covariate '%s' must have a single value, not %d", fullname, length(type)))
    }
    if (!(type %in% c("discrete", "continuous"))) {
      stop(sprintf("Parameter 'type' for covariate '%s' must be either 'discrete' or 'continuous', not '%s'", fullname, type))
    }
  }

  if ((length(value.properties) > 0) && (!is(value.properties, "ngchmColormap"))) {
    stop(sprintf(
      "value.properties for covariate '%s' must have class ngchmColormap, not '%s'",
      fullname, classstr(value.properties)
    ))
  }

  if (type == "continuous") {
    if (mode(values) != "numeric") {
      stop(sprintf("Covariate '%s' has type continuous, so values must have mode numeric, not '%s'", fullname, mode(values)))
    }
    if (length(value.properties) == 0) {
      value.properties <- chmNewColorMap(as.matrix(values, ncol = 1))
    }
  } else if (length(value.properties) == 0) {
    if (mode(values) == "numeric") {
      value.properties <- chmNewColorMap(sort(unique(values)))
    } else if (mode(values) == "logical") {
      value.properties <- chmNewColorMap(c(FALSE, TRUE))
    } else {
      # We start with human-readable values
      # We convert these into machine-readable codes
      unames <- sort(unique(values))
      uvals <- make.names(unames)
      # The properties map uses codes, but maps to the original values.
      value.properties <- chmNewColorMap(uvals, names = unames)
      # Replace values with the corresponding codes.
      values <- values[!is.na(values)]
      idx <- vapply(values, function(vv) which(unames == vv), 1)
      values[] <- uvals[idx]
    }
  }
  # Validate type of 'values'
  if (mode(values) %in% c("numeric", "logical")) {
    mode(values) <- "character"
  }

  if (length(covabbv) == 0) {
    covabbv <- make.names(fullname)
  } else {
    if (typeof(covabbv) != "character") {
      stop(sprintf("Parameter 'covabbv' must have type 'character', not '%s'", typeof(covabbv)))
    }
    if (length(covabbv) != 1) {
      stop(sprintf("Parameter 'covabbv' must have a single value, not %d", length(covabbv)))
    }
    if (nchar(covabbv) == 0) {
      stop("Parameter 'covabbv' cannot be the empty string")
    }
  }
  values <- values[order(names(values))]
  mat <- matrix(values, ncol = 1, dimnames = list(names(values), "Value"))
  shaid <- ngchmSaveAsDatasetBlob(ngchm.env$tmpShaidy, "tsv", mat)
  new(
    Class = "ngchmCovariate", label = covabbv, fullname = fullname, type = type,
    label.series = shaid, series.properties = value.properties
  )
}
#' Get a covariate attached to an NG-CHM dataset.
#'
#' @param dataset The NG-CHM dataset to get the covariate from.
#' @param fullname The full name of the covariate to get.
#'        If no covariate with that name exists, return NULL.
#' @param where The axis or axes on which to look for the covariate  Can be "row", "column", or "both" (default).
#'
#' @return A ngchmCovariate or NULL.
#'
#' @export
#'
#' @examples
#' # If the NGCHMDemoData package is installed, use it to create demo usage
#' if (requireNamespace("NGCHMDemoData", quietly = TRUE)) {
#'   data(TCGA.GBM.Demo, package = "NGCHMDemoData")
#'   dataset <- chmNewDataset("gbmexpr", "TCGA GBM Expression Data", TCGA.GBM.ExpressionData)
#'   dataset <- chmAddCovariate(
#'     dataset, "column",
#'     chmNewCovariate("TP53 Mutation", TCGA.GBM.TP53MutationData)
#'   )
#'   tp53_mutation <- chmCovariate(dataset, "TP53 Mutation")
#' }
#' # Small example not requiring NGCHMDemoData
#' matrix <- matrix(rnorm(100),
#'   nrow = 10, ncol = 10,
#'   dimnames = list(paste0("r", 1:10), paste0("c", 1:10))
#' )
#' dataset <- chmNewDataset("Demo", "Random Demo Dataset", matrix)
#' covariate <- setNames(rnorm(10), colnames(matrix))
#' dataset <- chmAddCovariate(dataset, "column", chmNewCovariate("Random Covariate", covariate))
#' random_covariate <- chmCovariate(dataset, "Random Covariate")
#'
#' @seealso [ngchmCovariate-class]
#' @seealso chmNewCovariate
#' @seealso chmCovariateBar
#'
chmCovariate <- function(dataset, fullname, where) {
  stopifnot(is(dataset, "ngchmDataset"))
  checkLabel(fullname, "fullname")
  where <- if (missing(where)) "both" else match.arg(where, c("row", "column", "both"))
  if (where %in% c("row", "both")) {
    names <- lapply(dataset@row.covariates, function(cv) cv@fullname)
    idx <- which(names == fullname)
    if (length(idx) > 0) {
      stopifnot(length(idx) == 1)
      return(dataset@row.covariates[[idx]])
    }
    if (where == "row") {
      return(NULL)
    }
  }
  # where must be either "column" or "both".
  names <- lapply(dataset@column.covariates, function(cv) cv@fullname)
  idx <- which(names == fullname)
  if (length(idx) > 0) {
    stopifnot(length(idx) == 1)
    return(dataset@column.covariates[[idx]])
  }
  return(NULL)
}
#' Create a new Classification Bar for a NGCHM
#'
#' This function is deprecated and will be removed in a future version.  Please use chmNewCovariateBar.
#' This function creates a new Classification Bar suitable for adding to a Next Generation Clustered Heat Map.
#'
#' @param label The name by which the classification bar will be known.
#' @param type The string "discrete" or the string "continuous".
#' @param data A vector of the data to be displayed in the classification bar. names(data) must be defined.
#' @param colors A color map specifying how the data should be rendered.
#' @param display Whether the classification bar will be "hidden" or "visible" (default).
#' @param thickness The thickness of the classification bar in pixels. (Default 10).
#' @param merge Algorithm for merging classifications when necessary ("average", "peakColor", "specialColor", or "mostCommon").
#' @param barType Type of covariate bar ("color_plot", "scatter_plot", "bar_plot"). Default "color_plot".
#' @param loBound Low bound for bar and scatter plots. Default minimum data value.
#' @param hiBound High bound for bar and scatter plots.  Default maximum data value.
#' @param fgColor Foreground color for bar and scatter plots.  Default black.
#' @param bgColor Background color for bar and scatter plots.  Default white.
#'
#' @return An object of class ngchmBar
#'
#' @seealso [ngchmBar-class]
#' @seealso [chmNewColorMap()]
#' @seealso [chmNewCovariateBar()]
#' @seealso [chmAddCovariateBar()]
#'
ngchmNewBar <- function(label, type, data, colors = NULL, display = "visible", thickness = as.integer(10), merge, barType,
                        loBound, hiBound, fgColor, bgColor) {
  if (typeof(label) != "character") {
    stop(sprintf("Parameter 'label' must have type 'character', not '%s'", typeof(label)))
  }
  if (length(label) != 1) {
    stop(sprintf("Parameter 'label' must have a single value, not %d", length(label)))
  }
  if (nchar(label) == 0) {
    stop("Parameter 'label' cannot be the empty string")
  }
  if (typeof(type) != "character") {
    stop(sprintf("Parameter 'type' for covariate bar '%s' must have type 'character', not '%s'", label, typeof(type)))
  }
  if (length(type) != 1) {
    stop(sprintf("Parameter 'type' for covariate bar '%s' must have a single value, not %d", label, length(type)))
  }
  if (!(type %in% c("discrete", "continuous"))) {
    stop(sprintf("Parameter 'type' for covariate bar '%s' must be either 'discrete' or 'continuous', not '%s'", label, type))
  }
  if (typeof(display) != "character") {
    stop(sprintf("Parameter 'display' for covariate bar '%s' must have type 'character', not '%s'", label, typeof(display)))
  }
  if (length(display) != 1) {
    stop(sprintf("Parameter 'display' for covariate bar '%s' must have a single value, not %d", label, length(display)))
  }
  if (!(display %in% c("visible", "hidden"))) {
    stop(sprintf("Parameter 'display' for covariate bar '%s' must be either 'visible' or 'hidden', not '%s'", label, display))
  }
  if (missing(barType)) {
    barType <- "color_plot"
  }
  if (typeof(barType) != "character") {
    stop(sprintf("Parameter 'barType' for covariate bar '%s' must have type 'character', not '%s'", label, typeof(barType)))
  }
  if (length(barType) != 1) {
    stop(sprintf("Parameter 'barType' for covariate bar '%s' must have a single value, not %d", label, length(barType)))
  }
  if (!(barType %in% c("color_plot", "scatter_plot", "bar_plot"))) {
    stop(sprintf("Parameter 'barType' for covariate bar '%s' must be either 'visible' or 'hidden', not '%s'", label, barType))
  }

  if (is(data, "shaid")) {
    shaid <- data
    repo <- ngchmFindRepo(shaid)
    data <- ngchmLoadDatasetBlob(repo, shaid, if (type == "discrete") "" else 0.0)$mat[, "Value"]
  } else {
    shaid <- NULL
  }
  if ((display == "visible") && (length(colors) == 0)) {
    if (type == "discrete") {
      qq <- sort(unique(data))
    } else {
      qq <- quantile(data)
    }
    colors <- grDevices::rainbow(length(qq), start = 2 / 6, end = 0)
    colors <- vapply(colors, function(cc) substr(cc, 1, 7), "")
    colors <- chmNewColorMap(qq, colors)
  }
  if (missing(merge)) {
    merge <- "average"
  }
  if ((length(merge) != 1) || !(merge %in% c("average", "peakColor", "specialColor", "mostCommon"))) {
    stop(sprintf("Unknown covariate bar merge value '%s'. Must be 'average', 'peakColor', 'specialColor' or 'mostCommon'", merge))
  }
  if (length(shaid) == 0) {
    if (length(names(data)) == 0) {
      stop(sprintf("Parameter 'data' for covariate bar '%s' must have defined names that match those of the CHM axis to which it will be attached", label))
    }
    if (anyDuplicated(names(data)) != 0) {
      dups <- unique(names(data)[which(duplicated(names(data)))])
      stop(sprintf("Parameter 'data' for covariate bar '%s' has multiple entries for label(s) %s", label, paste(dups, collapse = ", ")))
    }
    data <- data[order(names(data))]
    mat <- matrix(data, ncol = 1, dimnames = list(names(data), "Value"))
    shaid <- ngchmSaveAsDatasetBlob(ngchm.env$tmpShaidy, "tsv", mat)
  }
  if (missing(loBound)) {
    loBound <- if (type == "continuous") min(data, na.rm = TRUE) else 0
  }
  if (missing(hiBound)) {
    hiBound <- if (type == "continuous") max(data, na.rm = TRUE) else 1
  }
  if (missing(fgColor)) {
    fgColor <- "#000000"
  }
  if (missing(bgColor)) {
    bgColor <- "#ffffff"
  }
  new(Class = "ngchmBar", label = label, type = type, data = shaid, thickness = thickness, colors = colors, display = display, merge = merge, barType = barType, loBound = loBound, hiBound = hiBound, fgColor = fgColor, bgColor = bgColor)
}
#' Get a covariate bar attached to an NG-CHM.
#'
#' @param hm The NG-CHM to get the covariate bar from.
#' @param fullname The full name of the covariate bar to get.
#'        If no covariate bar with that name exists, return NULL.
#' @param where The axis or axes on which to look for the covariate bar  Can be "row", "column", or "both" (default).
#'
#' @return An ngchmBar or NULL.
#'
#' @export
#'
#' @examples
#' # Examples using `chmNew()` require git to be installed and available.
#' \dontrun{
#'   # If the NGCHMDemoData package is installed, use it to demo usage
#'   if (requireNamespace("NGCHMDemoData", quietly = TRUE)) {
#'     # Create example NGCHM with covariate bar
#'     data(TCGA.GBM.Demo, package = "NGCHMDemoData")
#'     hm <- chmNew("gbmexpr", TCGA.GBM.ExpressionData[1:50, 1:50])
#'     hm <- chmAddCovariateBar(
#'       hm, "column",
#'       chmNewCovariate("TP53 Mutation", TCGA.GBM.TP53MutationData[1:50])
#'     )
#'     # Get covariate bar by name
#'     tp53_covariate_bar <- chmCovariateBar(hm, "TP53 Mutation")
#'   }
#'   # Small example not requiring NGCHMDemoData
#'   matrix <- matrix(rnorm(100),
#'     nrow = 10, ncol = 10,
#'     dimnames = list(paste0("r", 1:10), paste0("c", 1:10))
#'   )
#'   hm <- chmNew("Demo", matrix)
#'   covariate <- setNames(rnorm(10), colnames(matrix))
#'   hm <- chmAddCovariateBar(hm, "column", chmNewCovariate("my covariate", covariate))
#'   my_covariate_bar <- chmCovariateBar(hm, "my covariate")
#' }
#' @seealso [ngchmBar-class]
#' @seealso chmNewCovariateBar
#' @seealso chmCovariate
#'
chmCovariateBar <- function(hm, fullname, where) {
  stopifnot(is(hm, "ngchm"))
  checkLabel(fullname, "fullname")
  where <- if (missing(where)) "both" else match.arg(where, c("row", "column", "both"))
  if (where %in% c("row", "both")) {
    names <- lapply(hm@rowCovariateBars, function(cvb) cvb@label)
    idx <- which(names == fullname)
    if (length(idx) > 0) {
      stopifnot(length(idx) == 1)
      return(hm@rowCovariateBars[[idx]])
    }
    if (where == "row") {
      return(NULL)
    }
  }
  # where must be either "column" or "both".
  names <- lapply(hm@colCovariateBars, function(cvb) cvb@label)
  idx <- which(names == fullname)
  if (length(idx) > 0) {
    stopifnot(length(idx) == 1)
    return(hm@colCovariateBars[[idx]])
  }
  return(NULL)
}
#' Create a new covariate Bar for a NGCHM
#'
#' This function creates a new covariate bar suitable for adding to a Next Generation Clustered Heat Map.
#'
#' @param covar The covariate to be displayed in the bar.
#' @param display Whether the covariate bar will be "hidden" or "visible" (default).
#' @param thickness The thickness of the covariate bar in pixels. (Default 10).
#' @param merge Algorithm for merging covariates when necessary ("average", "peakColor", "specialColor", or "mostCommon").
#' @param barType Type of covariate bar ("color_plot", "scatter_plot", "bar_plot"). Default "color_plot".
#' @param loBound Low bound for bar and scatter plots. Default minimum data value.
#' @param hiBound High bound for bar and scatter plots.  Default maximum data value.
#' @param fgColor Foreground color for bar and scatter plots.  Default black.
#' @param bgColor Background color for bar and scatter plots.  Default white.
#'
#' @return An object of class ngchmBar
#'
#' @export
#'
#' @examples
#' bar.data <- ifelse(rnorm(1000) < 0, "negative", "non-negative")
#' names(bar.data) <- sprintf("Sample%d", 1:length(bar.data))
#' bar.colors <- chmNewColorMap(c("negative", "non-negative"),
#'   c("white", "black"),
#'   missing.color = "red"
#' )
#' covar <- chmNewCovariate("Group", bar.data, bar.colors, "discrete")
#' bar <- chmNewCovariateBar(covar)
#'
#' @seealso [ngchmBar-class]
#' @seealso [chmNewColorMap()]
#' @seealso [chmAddCovariateBar()]
#'
chmNewCovariateBar <- function(covar, display = "visible", thickness = as.integer(10), merge, barType,
                               loBound, hiBound, fgColor, bgColor) {
  ngchmNewBar(covar@fullname, covar@type, covar@label.series, covar@series.properties,
    display = display, thickness = thickness, merge = merge, barType = barType,
    loBound = loBound, hiBound = hiBound, fgColor = fgColor, bgColor = bgColor
  )
}

#' Create a new Color Map for use in constructing a NGCHM
#'
#' This function creates a new Color Map suitable for use in constructing Data Layers and Covariates
#' in Next Generation Clustered Heat Maps.  Color maps can be used in both discrete and continuous
#' contents.  In a discrete context, values specifies the properties of series.  In a continuous context,
#' values specifies the break points.
#'
#' If values is a matrix, the function will estimate a suitable sequence of color break points.  For a quantile
#' color map, the matrix data is ignored.  For a linear color map, it will use equispaced values between a low value and
#' a high value. The low value is the median of the minima of each row in the matrix, and the high value is the median
#' of the row maxima. If the low and high values have different signs, the values will be symmetric about zero.
#'
#' @param values A vector specifying the series / break points for which the following colors are defined, or a data matrix.
#' @param colors Either a string vector specifying the color to use for each series / break point, or a single integer.
#' @param names A string vector specifying 'human-readable' names for each series / break point.
#' @param shapes A string vector specifying the shape to use for each series.
#' @param zs A numeric vector specifying the z order to use for each series.
#' @param type The string "linear" (default) or "quantile" (or unique abbreviation thereof).
#' @param missing.color A string specifying the color to use for missing data.
#' @param palette A function(n) that returns a vector of n colors.
#'
#' @return An object of class ngchmColormap
#'
#' @export
#'
#' @examples
#' noise.colors <- chmNewColorMap(c(0, 1, 2),
#'   c("green", "black", "red"),
#'   missing.color = "yellow"
#' )
#' bar.colors <- chmNewColorMap(c("small", "big"),
#'   c("#00FFFF", "#FF00FF"),
#'   type = "quantile"
#' )
#'
#' @seealso [ngchmColormap-class]
#' @seealso [chmNewDataLayer()]
#' @seealso [chmNewCovariateBar()]
#'
chmNewColorMap <- function(values, colors = NULL, names = NULL, shapes = NULL, zs = NULL, type = "linear", missing.color = NULL, palette = NULL) {
  # Validate parameter 'type'
  if (typeof(type) != "character") {
    stop(sprintf("Parameter 'type' must have type 'character', not '%s'", typeof(type)))
  }
  if (length(type) != 1) {
    stop(sprintf("Parameter 'type' must have a single value, not %d", length(type)))
  }
  allowedTypes <- c("linear", "quantile")
  mtype <- pmatch(type, allowedTypes)
  if (is.na(mtype)) {
    stop(sprintf(
      "Unknown color map type '%s'. Allowed types are %s.", type,
      paste("'", allowedTypes, "'", sep = "", collapse = ", ")
    ))
  }
  type <- allowedTypes[mtype]

  # Determine number of colors and whether auto-picking.
  NC <- length(colors)
  if ((NC == 1) && (mode(colors) == "numeric") && (as.integer(colors) == colors)) {
    if (colors < 2) {
      stop(sprintf('"%d" colors requested. At least two are required.', colors))
    }
    NC <- colors
    colors <- NULL
  }

  # Validate 'values'.  Auto-pick values if appropriate.
  if (isany(values, c("matrix", "shaid"))) {
    # User just supplied a data matrix.
    if (NC == 0) NC <- 3
    if (type == "quantile") {
      values <- (1.0 / (NC - 1)) * (0:(NC - 1))
    } else if (type == "linear") {
      if (is(values, "shaid")) {
        # FIXME: redoing calculation is inefficient
        repo <- ngchmFindRepo(values)
        values <- ngchmLoadDatasetBlob(repo, values)$mat
      }
      if ((nrow(values) == 1) || (ncol(values) == 1)) {
        minv <- min(values, na.rm = TRUE)
        maxv <- max(values, na.rm = TRUE)
      } else {
        minv <- median(apply(values, 1, function(v) min(v, na.rm = TRUE)), na.rm = TRUE)
        maxv <- median(apply(values, 1, function(v) max(v, na.rm = TRUE)), na.rm = TRUE)
      }
      if ((minv < 0.0) && (maxv > 0.0)) {
        maxv <- max(maxv, -minv)
        minv <- -maxv
      }
      values <- minv + (maxv - minv) * (1.0 / (NC - 1)) * (0:(NC - 1))
    } else {
      stop(sprintf("chmNewColorMap: unable to derive color map breaks from matrix for map type '%s'", type))
    }
  } else if (isany(values, c("character", "numeric", "integer", "logical"))) {
    # User supplied a vector of values.
    if (anyDuplicated(values) != 0) {
      stop("chmNewColorMap: values contains duplicates")
    }
    if (NC == 0) {
      NC <- length(values)
    }
    if (length(values) != NC) {
      stop(sprintf("chmNewColorMap: number of values (%d) does not equal number of color (%d). It should.", length(values), NC))
    }
    if (isany(values, c("numeric", "integer")) && !all(is.finite(values))) {
      stop("chmNewColorMap: values contains non-finite values")
    }
  } else {
    # Don't know what the user provided.
    stop(sprintf(
      "chmNewColorMap: values vector has unknown class '%s'. It must be either a vector or a matrix.",
      classstr(values)
    ))
  }
  # values is now a length NC vector of cut points
  stopifnot(length(values) == NC)

  # Auto-pick colors if needed.
  if (length(colors) == 0) {
    if (length(palette) > 0) {
      colors <- palette(NC)
    } else {
      colors <- grDevices::rainbow(NC, start = 2 / 6, end = 0)
    }
    colors <- vapply(colors, function(cc) substr(cc, 1, 7), "")
  }
  stopifnot(length(colors) == NC)

  # Validate other parameters.
  if (!is.null(names) && length(names) != NC) {
    stop(sprintf("chmNewColorMap: number of names (%d) does not equal number of colors (%d). It should.", length(names), NC))
  }
  if (!is.null(shapes) && length(shapes) != NC) {
    stop(sprintf("chmNewColorMap: number of shapes (%d) does not equal number of colors (%d). It should.", length(shapes), NC))
  }
  if (!is.null(zs) && length(zs) != NC) {
    stop(sprintf("chmNewColorMap: number of zindices (%d) does not equal number of colors (%d). It should.", length(zs), NC))
  }
  grDevices::col2rgb(missing.color) # error check

  # Construct ValueMap
  pts <- chmAddValueProperty(NULL, value = values, color = colors, name = names, shape = shapes, z = zs)
  new(Class = "ngchmColormap", type = type, missing = missing.color, points = pts)
}

chmAddValueProperty <- function(vps, value, color, name = NULL, shape = NULL, z = NULL) {
  some.shapes <- c("circle", "square", "diamond", "triangle")
  all.shapes <- c(some.shapes, "triangle-down", "line")
  if (is.null(name)) name <- as.character(value)
  if (is.null(shape)) shape <- some.shapes[1 + (0:(length(value) - 1)) %% length(some.shapes)]
  if (is.null(z)) z <- rep(9, length(value))
  if (!all(shape %in% all.shapes)) {
    stop("unknown shape ", shape)
  }
  if (any(z < 0)) {
    stop("z must be non-negative")
  }
  grDevices::col2rgb(color) # error check
  for (ii in 1:length(value)) {
    vps <- append(vps, new(Class = "ngchmValueProp", value = value[ii], color = color[ii], name = name[ii], shape = shape[ii], z = z[ii]))
  }
  vps
}

#' Create a new Javascript function for adding to a NGCHM menu.
#'
#' This function creates a new Javascript function object for adding to
#' a Next Generation Clustered Heat Map menu.
#'
#' @param name The name of the Javascript function
#' @param description A short description of the Javascript function
#' @param implementation A string containing the javascript code required to define the function. When called the function
#'        is passed a list of selected values (e.g. labels).  Additional parameters can be declared before the values
#'        parameter and must be resolved through currying (binding) before the function is used in menus.
#' @param extraParams An optional list of extra parameters. (Default NULL.)
#' @param requires An optional vector of (custom) Javascript function names that this function requires.
#' @param global A logical: TRUE if should be defined globally, not within a customization section. (Default FALSE.)
#'
#' @return An object of class ngchmJS
#'
#' @export
#'
#' @examples
#' alertFn <- chmNewFunction("showAlert", "Display the parameter in an alert box",
#'   "function showAlert(label) { alert(label); }",
#'   global = TRUE
#' )
#' dbLookup <- chmNewFunction(
#'   "dbLookup", "Lookup the parameter in a database",
#'   "function showAlert(database, label) { alert(database[label]); }",
#'   c("database")
#' )
#'
#' @seealso [ngchmJS-class]
#' @seealso [chmAddMenuItem()]
#' @seealso [chmBindFunction()]
#' @seealso [chmRegisterFunction()]
#'
chmNewFunction <- function(name, description, implementation, extraParams = NULL, requires = NULL, global = FALSE) {
  if (typeof(name) != "character") {
    stop(sprintf("Parameter 'name' must have type 'character', not '%s'", typeof(name)))
  }
  if (length(name) != 1) {
    stop(sprintf("Parameter 'name' must have a single value, not %d", length(name)))
  }
  # We declare "" to be a simple value reference
  if (typeof(description) != "character") {
    stop(sprintf("Parameter 'description' for JS function '%s' must have type 'character', not '%s'", name, typeof(description)))
  }
  if (length(description) != 1) {
    stop(sprintf("Parameter 'description' for JS function '%s' must have a single value, not %d", name, length(description)))
  }
  if (nchar(description) == 0) {
    warning(sprintf("Parameter 'description' for JS function '%s' should not be the empty string", name))
  }
  if (typeof(implementation) != "character") {
    stop(sprintf("Parameter 'implementation' for JS function '%s' must have type 'character', not '%s'", name, typeof(implementation)))
  }
  if (length(implementation) != 1) {
    stop(sprintf("Parameter 'implementation' for JS function '%s' must have a single value, not %d", name, length(implementation)))
  }
  # We might have an empty implementation if we are making R aware of a Javascript builtin
  if (length(extraParams) > 0) {
    if (typeof(extraParams) != "character") {
      stop(sprintf("Parameter 'extraParams' for JS function '%s' must have type 'character', not '%s'", name, typeof(extraParams)))
    }
  }
  if (length(requires) > 0) {
    if (typeof(requires) != "character") {
      stop(sprintf("Parameter 'requires' for JS function '%s' must have type 'character', not '%s'", name, typeof(requires)))
    }
  }
  fn <- new(Class = "ngchmJS", name = name, description = description, script = implementation, requires = requires, extraParams = extraParams, global = global)
  chmRegisterFunction(fn)
}


checkLabel <- function(label, paramName = "label") {
  if (typeof(label) != "character") {
    stop(sprintf("Parameter '%s' must have type 'character', not '%s'", paramName, typeof(label)))
  }
  if (length(label) != 1) {
    stop(sprintf("Parameter '%s' must have a single value, not %d", paramName, length(label)))
  }
  if (nchar(label) == 0) {
    stop(sprintf("Parameter '%s' cannot be the empty string", paramName))
  }
}
checkPropertyValue <- function(label, value) {
  if (!typeof(value) %in% c("character", "double", "integer", "logical")) {
    stop(sprintf("Parameter 'value' for property '%s' must have type 'character', 'double', 'integer', or 'logical', not '%s'", label, typeof(value)))
  }
  if (length(value) < 1) {
    stop(sprintf("Parameter 'value' for property '%s' must have at least one value", label))
  }
}
#' Create a new Property for adding to a NGCHM.
#'
#' This function creates a new Property object for adding to
#' a Next Generation Clustered Heat Map.
#'
#' @param label The property label
#' @param value The property value
#'
#' @return An object of class ngchmProperty
#'
#' @export
#'
#' @examples
#' prop <- chmNewProperty(
#'   "chm.info.caption",
#'   "This is a nifty new CHM."
#' )
#'
#' @seealso [ngchm-class]
#' @seealso [chmAddProperty()]
#'
chmNewProperty <- function(label, value) {
  checkLabel(label)
  checkPropertyValue(label, value)
  new(Class = "ngchmProperty", label = label, value = as.character(value))
}

#' Get the value of an NG-CHM property.
#'
#' @param hm The NG-CHM object to get the property value from.
#' @param label The name of the property to get.
#'        If no property with that name exists, return NULL.
#'
#'        Well-known property labels used by the NG-CHM system include:
#'
#'        * "chm.info.caption"  A paragraph describing the NG-CHM's contents (set by user).
#' 	  * "chm.info.built.time"  The date and time the NG-CHM was saved (set by system).
#'
#' @return A property value or NULL.
#'
#' @export
#'
#' @examples
#' hm <- chmNew("Empty")
#' chmProperty(hm, "chm.info.caption")
#'
#' @seealso [ngchm-class]
#'
chmProperty <- function(hm, label) {
  stopifnot(is(hm, "ngchm"))
  checkLabel(label)
  labels <- lapply(hm@properties, function(p) p@label)
  idx <- which(label == labels)
  if (length(idx) == 0) {
    return(NULL)
  }
  stopifnot(length(idx) == 1)
  hm@properties[[idx]]@value
}
#' Set the value of an NG-CHM property.
#'
#' @param x The NG-CHM object on which to set the property.
#' @param label The name of the property to set.
#'        If no property with that name exists, a new property with that name is appended.
#' @param value A non-empty vector of character, logical, or numeric values.
#'
#' @return The modified NG-CHM object.
#'
#' @export
#'
#' @examples
#' hm <- chmNew("Empty")
#' chmProperty(hm, "chm.info.caption") <- "Nothing to see here"
#'
#' @seealso [ngchm-class]
#'
"chmProperty<-" <- function(x, label, value) {
  stopifnot(is(x, "ngchm"))
  checkLabel(label)
  checkPropertyValue(label, value)
  labels <- lapply(x@properties, function(p) p@label)
  idx <- which(label == labels)
  if (length(idx) == 0) {
    x@properties <- append(x@properties, new(Class = "ngchmProperty", label = label, value = as.character(value)))
  } else {
    stopifnot(length(idx) == 1)
    x@properties[[idx]]@value <- as.character(value)
  }
  x
}

#' Create NG-CHM Properties
#'
#' This function creates one or more NG-CHM (Next-Generation Clustered Heat Map) properties.
#'
#' @param ... Named arguments representing the properties to be created.
#' Each argument should be a single value of type character, double, integer, or logical.
#'
#' @return A list of properties. Each property is represented as a list with two elements: 'label' and 'value'.
#'
#' @seealso [chmAdd()]
#'
#' @examples
#' # Create three properties: 'prop1', 'prop2', and 'prop3'.
#' props <- chmProperties(prop1 = "value1", prop2 = 2, prop3 = TRUE)
#' @export
chmProperties <- function(...) {
  props <- list(...)
  if (length(props) > 0) {
    if (is.null(names(props)) || any(names(props) == "")) {
      stop("All properties must be named")
    }
    props <- lapply(1:length(props), function(idx) {
      label <- names(props)[idx]
      value <- props[[idx]]
      if (!typeof(value) %in% c("character", "double", "integer", "logical")) {
        stop(sprintf("Property %s has invalid type %s", label, typeof(value)))
      }
      if (length(value) != 1) {
        stop(sprintf("Property %s has length %d", label, length(value)))
      }
      chmNewProperty(label, as.character(value))
    })
  }
  props
}
#' Get the label/name of an NG-CHM object.
#'
#' @param x The NG-CHM object to get the label/name of.  Can be:
#'
#' * An object of class ngchm
#' * An object of class ngchmLayer
#' * An object of class ngchmDataset
#' * An object of class ngchmBar
#' * An object of class ngchmCovariate
#' * An object of class ngchmColormap
#'
#' @return A character string (or a vector of strings for an ngchmColormap)
#'
#' @export
#'
#' @examples
#' chmLabel(chmNew("New CHM"))
#'
#' @seealso [ngchm-class]
#'
chmLabel <- function(x) {
  if (isany(x, c("ngchm", "ngchmLayer", "ngchmDataset"))) {
    return(x@name)
  }
  if (isany(x, c("ngchmBar", "ngchmCovariate"))) {
    return(x@label)
  }
  if (is(x, "ngchmColormap")) {
    return(vapply(x@points, function(p) p@name, ""))
  }
  stop("Unknown object class")
}
#' Set the label/name of an NG-CHM object
#'
#' @param x The NG-CHM object on which to set the label/name.
#' @param value The new name (a single character string).
#'
#' @return The modified NG-CHM object.
#'
#' @export
#'
#' @examples
#' hm <- chmNew("Old name")
#' chmLabel(hm) <- "A new name"
#'
#' @seealso [chmLabel]
#'
"chmLabel<-" <- function(x, value) {
  stopifnot(
    isany(value, c("character", "numeric", "logical")),
    length(value) == 1 || is(x, "ngchmColormap")
  )
  value <- as.character(value)
  if (isany(x, c("ngchm", "ngchmLayer", "ngchmDataset"))) {
    x@name <- value
  } else if (isany(x, c("ngchmBar", "ngchmCovariate"))) {
    x@label <- value
  } else if (is(x, "ngchmColormap")) {
    stopifnot(length(x@points) == length(value))
    for (idx in 1:length(x@points)) x@points[[idx]]@name <- value[idx]
  } else {
    stop("unknown object class")
  }
  x
}
#' Get the color map of an NG-CHM object.
#'
#' @param x The NG-CHM object to get the color map of.  Can be:
#'
#' * An object of class ngchmLayer
#' * An object of class ngchmBar
#' * An object of class ngchmCovariate
#'
#' @return An ngchmColormap
#'
#' @export
#'
#' @examples
#' # If the NGCHMDemoData package is installed, use it to demo usage
#' if (requireNamespace("NGCHMDemoData", quietly = TRUE)) {
#'   data(TCGA.GBM.EXPR, package = "NGCHMDemoData")
#'   colormap <- chmColorMap(chmNewDataLayer("New layer", TCGA.GBM.EXPR[1:3, 1:3]))
#' }
#' matrix <- matrix(rnorm(100),
#'   nrow = 10, ncol = 10,
#'   dimnames = list(paste0("r", 1:10), paste0("c", 1:10))
#' )
#' colormap <- chmColorMap(chmNewDataLayer("New layer", matrix))
#'
#' @seealso [chmNewColorMap]
#'
chmColorMap <- function(x) {
  if (isany(x, c("ngchmLayer", "ngchmBar"))) {
    return(x@colors)
  }
  if (is(x, "ngchmCovariate")) {
    return(x@series.properties)
  }
  stop("Unknown object class")
}
#' Set the color map of an NG-CHM object
#'
#' @param x The NG-CHM object on which to set the color map.
#' @param value The ngchmColormap value to set.
#'
#' @return The modified NG-CHM object.
#'
#' @export
#'
#' @examples
#' # If the NGCHMDemoData package is installed, use it to demo usage
#' if (requireNamespace("NGCHMDemoData", quietly = TRUE)) {
#'   data(TCGA.GBM.EXPR, package = "NGCHMDemoData")
#'   dataLayer <- chmNewDataLayer("GBM layer", TCGA.GBM.EXPR[1:30, 1:30])
#'   chmColorMap(dataLayer) <- chmNewColorMap(c(2, 14))
#' }
#' # Small example not requiring NGCHMDemoData
#' matrix <- matrix(rnorm(100),
#'   nrow = 10, ncol = 10,
#'   dimnames = list(paste0("r", 1:10), paste0("c", 1:10))
#' )
#' dataLayer <- chmNewDataLayer("my layer", matrix)
#' chmColorMap(dataLayer) <- chmNewColorMap(c(2, 14))
#'
#' @seealso [chmColorMap]
#'
"chmColorMap<-" <- function(x, value) {
  stopifnot(is(value, "ngchmColormap"))
  if (isany(x, c("ngchmLayer", "ngchmBar"))) {
    x@colors <- value
  } else if (is(x, "ngchmCovariate")) {
    x@series.properties <- value
  } else {
    stop("Unknown object class")
  }
  x
}
#' Get the colors of an ngchmColormap, ngchmLayer, ngchmBar, or ngchmCovariate.
#'
#' @param x The object to get the colors of.
#'
#' @return A character string vector of the map colors.
#'
#' @export
#'
#' @examples
#' # If the NGCHMDemoData package is installed, use it to demo usage
#' if (requireNamespace("NGCHMDemoData", quietly = TRUE)) {
#'   data(TCGA.GBM.EXPR, package = "NGCHMDemoData")
#'   colors <- chmColors(chmNewDataLayer("GBM Expression", TCGA.GBM.EXPR[1:50, 1:50]))
#' }
#' # Small example not requiring NGCHMDemoData
#' matrix <- matrix(rnorm(100),
#'   nrow = 10, ncol = 10,
#'   dimnames = list(paste0("r", 1:10), paste0("c", 1:10))
#' )
#' colors <- chmColors(chmNewDataLayer("my layer", matrix))
#'
#' @seealso [ngchm-class]
#'
chmColors <- function(x) {
  if (!is(x, "ngchmColormap")) x <- chmColorMap(x)
  vapply(x@points, function(p) p@color, "")
}
#' Set the colors of an ngchmColormap, ngchmLayer, ngchmBar, or ngchmCovariate.
#'
#' @param x The NG-CHM object on which to set the colors.
#' @param value A character string vector of colors.  The vector length must equal
#'        the number of data points in the color map.
#'
#' @return The modified NG-CHM object.
#'
#' @export
#'
#' @examples
#' # If the NGCHMDemoData package is installed, use it to demo usage
#' if (requireNamespace("NGCHMDemoData", quietly = TRUE)) {
#'   data(TCGA.GBM.EXPR, package = "NGCHMDemoData")
#'   layer <- chmNewDataLayer("GBM Layer", TCGA.GBM.EXPR[1:50, 1:50])
#'   chmColors(layer) <- c("blue", "white", "red")
#' }
#' # Small example not requiring NGCHMDemoData
#' matrix <- matrix(rnorm(100),
#'   nrow = 10, ncol = 10,
#'   dimnames = list(paste0("r", 1:10), paste0("c", 1:10))
#' )
#' layer <- chmNewDataLayer("my layer", matrix)
#' chmColors(layer) <- c("blue", "white", "red")
#'
#' @seealso [chmColors]
#'
"chmColors<-" <- function(x, value) {
  stopifnot(is(value, "character"))
  cm <- if (is(x, "ngchmColormap")) x else chmColorMap(x)
  stopifnot(length(cm@points) == length(value))
  for (idx in 1:length(cm@points)) cm@points[[idx]]@color <- value[idx]
  if (is(x, "ngchmColormap")) {
    return(cm)
  } else {
    chmColorMap(x) <- cm
    x
  }
}
#' Create a new object representing a NGCHM server.
#'
#' This function creates a new object that represents a NGCHM server.
#'
#' @param serverName The DNS name of the NGCHM server.
#' @param serverPort The port on which the server is listening.
#' @param deployServer The DNS name to use when deploying a NGCHM (defaults to serverName).
#' @param protoOpts A list of protocol-specific parameters
#' @param jarFile The location of the heatmap build jar file to use when making a NGCHM (defaults to jar file on serverURL WS).
#' @param serverURL The URL used to access the NGCHM server (defaults to serverName:serverPort/chm).
#'
#' @return An object of class ngchmServer
#'
#' @export
#'
#' @examples
#' cloudServ <- chmNewServer("dnsname.domain")
#'
#' @seealso [ngchmServer-class]
#' @seealso [chmInstall()]
#' @seealso [chmUninstall()]
#'
chmNewServer <- function(serverName, serverPort = 8080, deployServer = NULL, protoOpts = NULL,
                         jarFile = NULL, serverURL = NULL) {
  if (is.null(deployServer)) deployServer <- serverName
  if (is.null(serverURL)) serverURL <- paste("http://", serverName, ":", serverPort, "/chm", sep = "")
  new(
    Class = "ngchmServer",
    deployServer = deployServer,
    protoOpts = protoOpts,
    jarFile = jarFile,
    serverURL = serverURL
  )
}

#############################################################################################
#
# Javascript Customization.
#

#' Get a predefined Javascript function for use in NGCHM menus
#'
#' This function returns a predefined Javascript function that can be used when
#' building a Next Generation Clustered Heat Map.
#'
#' @param name The name of the predefined Javascript function desired.
#'
#' @return An object of class ngchmFunction if found, NULL otherwise.
#'
#' @export
#'
#' @seealso [chmAddMenuItem()]
#' @seealso [chmNewFunction()]
#' @seealso [ngchmAxisFunction-class]
#' @seealso [ngchmMatrixFunction-class]
#'
chmGetFunction <- function(name) {
  if (typeof(name) != "character") {
    stop(sprintf("Parameter 'name' must have type 'character', not '%s'", typeof(name)))
  }
  if (length(name) != 1) {
    stop(sprintf("Parameter 'name' must have a single value, not %d", length(name)))
  }
  if (is.list(ngchm.env$scripts)) {
    for (ii in 1:length(ngchm.env$scripts)) {
      if (ngchm.env$scripts[[ii]]@name == name) {
        return(ngchm.env$scripts[[ii]])
      }
    }
  }
  return(NULL)
}

#' Register a predefined Javascript function for use in NGCHM Axis menus.
#'
#' This function registers a Javascript function that will be automatically
#' added to the appropriate axis menu(s) when building a Next Generation
#' Clustered Heat Map for axes that match the function's axis type.
#' This function is intended for use by NGCHM system developers.
#'
#' @param type The axis type required by this function.
#' @param label The name of the axis menu entry to be used for this function.
#' @param fn The Javascript function to register.
#'
#' @export
#'
#' @seealso [chmAddAxisType()]
#' @seealso [chmRegisterMatrixFunction()]
#' @seealso [chmRegisterTypeMapper()]
#' @seealso [chmNewFunction()]
#'
#' @return None. This function is used for its side effects of registering a new axis function.
chmRegisterAxisFunction <- function(type, label, fn) {
  if (typeof(label) != "character") {
    stop(sprintf("Parameter 'label' must have type 'character', not '%s'", typeof(label)))
  }
  if (length(label) != 1) {
    stop(sprintf("Parameter 'label' must have a single value, not %d", length(label)))
  }
  if (nchar(label) == 0) {
    stop("Parameter 'label' cannot be the empty string")
  }
  if (typeof(type) != "character") {
    stop(sprintf("Parameter 'type' for axis function '%s' must have type 'character', not '%s'", label, typeof(type)))
  }
  if (length(type) < 1) {
    stop(sprintf("Parameter 'type' for axis function '%s' must have at least one value", label))
  }
  for (ii in 1:length(type)) {
    if (nchar(type[ii]) == 0) {
      stop(sprintf("Parameter 'type[%d]' for axis function '%s' cannot be the empty string", ii, label))
    }
  }
  if (!is(type, "character")) {
    stop(sprintf("chmRegisterAxisFunction: error registering axis function '%s'. Specified type is '%', not string.", label, classstr(type)))
  }
  if (!is(label, "character")) {
    stop(sprintf("chmRegisterAxisFunction: error registering axis function. Type of specified label is '%', not string.", classstr(type)))
  }
  if (is(fn, "character")) {
    fn <- chmGetFunction(fn)
  }
  af <- new("ngchmAxisFunction", type = type, label = label, func = fn)
  matches <- which(vapply(ngchm.env$axisFunctions, function(af) (af@label == label) && (af@type == type), TRUE))
  if (length(matches) > 0) {
    ngchm.env$axisFunctions[[matches]] <- af
  } else {
    ngchm.env$axisFunctions <- append(ngchm.env$axisFunctions, af)
  }
  NULL
}

getAllAxisTypes <- function(chm, where) {
  w <- vapply(chm@axisTypes, function(at) at@where, "")
  locs <- (w == where) | (w == "both")
  # types is a list of character vectors (since an axistype can have > 1 type)
  types <- lapply(chm@axisTypes[locs], function(at) at@type)
  uu <- !duplicated(types)
  types <- types[uu]
  builders <- chm@axisTypes[locs][uu]

  # fromtypes is a list of character vectors (since a mapper can have > 1 fromtype)
  fromtypes <- lapply(ngchm.env$typeMappers, function(mm) mm@fromtype)
  # totypes is a character vector (since each mapper has exactly 1 totype)
  totypes <- vapply(ngchm.env$typeMappers, function(mm) mm@totype, "")
  ii <- 0
  while (ii < length(types)) {
    ii <- ii + 1
    # Find those fromtypes that match the current type.
    extra <- vapply(fromtypes, function(ft) any(vapply(types[[ii]], function(tt) any(tt == ft), TRUE)), TRUE)
    # Append types of matching mappers, but exclude any duplicates.
    # FIXME: only excludes exact duplicate type lists. Does not exclude a mapper if all its types can
    # be obtained from a collection of the current types.
    types <- append(types, totypes[extra])
    uu <- !duplicated(types)
    types <- types[uu]
    builders <- append(builders, ngchm.env$typeMappers[extra])[uu]
  }
  return(list(types = types, builders = builders))
}

getAllAxisFunctions <- function(chm, where) {
  w <- vapply(chm@axisTypes, function(at) at@where, "")
  locs <- (w == where) | (w == "both")
  fns <- lapply(chm@axisTypes[locs], function(at) at@func)
  return(fns)
}

# axistypes is a list of character vectors (since an axis type can have > 1 type)
getAllAxisTypeFunctions <- function(chmSpecificFns, axistypes) {
  fnList <- append(chmSpecificFns, ngchm.env$axisFunctions)
  matches <- vapply(
    fnList,
    function(af) any(vapply(axistypes, function(at) any(at == af@type), TRUE)),
    TRUE
  )
  return(fnList[matches])
}

getAllMatrixTypeFunctions <- function(chm, rowtypes, columntypes) {
  matches <- vapply(
    ngchm.env$matrixFunctions,
    function(mf) {
      rowm <- any(vapply(rowtypes, function(at) any(at == mf@rowtype), TRUE))
      colm <- any(vapply(columntypes, function(at) any(at == mf@columntype), TRUE))
      message("getAllMatrixTypeFunctions for ", mf@label, ":")
      message("    row matches: ", rowm)
      message("    col matches: ", colm)
      rowm && colm
    },
    TRUE
  )
  message("getAllMatrixTypeFunctions: found ", sum(matches), " matches.")
  return(ngchm.env$matrixFunctions[matches])
}

#' Register a predefined Javascript function for use in NGCHM Matrix menus.
#'
#' This function registers a Javascript function that will be automatically
#' added to the matrix menu when building a Next Generation
#' Clustered Heat Map for matrices whose rows and columns match then function's
#' axes types.
#' This function is intended for use by NGCHM system developers.
#'
#' @param rowtype A character vector specifying the row type(s) of the matrix function.
#' @param columntype A character vector specifying the column type(s) of the
#' matrix function.
#' @param label A single character string specifying the label of the matrix function.
#' @param fn The function to be registered. This can be either a function or a
#' character string representing the name of a function.
#' @export
#' @seealso [chmAddAxisType()]
#' @seealso [chmRegisterAxisFunction()]
#' @seealso [chmRegisterTypeMapper()]
#' @seealso [chmNewFunction()]
#' @return None. This function is used for its side effects of registering a new
#' function in the NGCHM matrix menues.
chmRegisterMatrixFunction <- function(rowtype, columntype, label, fn) {
  if (typeof(label) != "character") {
    stop(sprintf("Parameter 'label' must have type 'character', not '%s'", typeof(label)))
  }
  if (length(label) != 1) {
    stop(sprintf("Parameter 'label' must have a single value, not %d", length(label)))
  }
  if (nchar(label) == 0) {
    stop("Parameter 'label' cannot be the empty string")
  }
  if (typeof(rowtype) != "character") {
    stop(sprintf("Parameter 'rowtype' for matrix function '%s' must have type 'character', not '%s'", label, typeof(rowtype)))
  }
  if (length(rowtype) < 1) {
    stop(sprintf("Parameter 'rowtype' for matrix function '%s' must have at least one value", label))
  }
  if (typeof(columntype) != "character") {
    stop(sprintf("Parameter 'columntype' for matrix function '%s' must have type 'character', not '%s'", label, typeof(columntype)))
  }
  if (length(columntype) < 1) {
    stop(sprintf("Parameter 'columntype' for matrix function '%s' must have at least one value", label))
  }
  if (is(fn, "character")) {
    fn <- chmGetFunction(fn)
  }
  newmf <- new("ngchmMatrixFunction", rowtype = rowtype, columntype = columntype, label = label, func = fn)
  matches <- which(vapply(ngchm.env$matrixFunctions, function(mf) (mf@label == label) && (mf@rowtype == rowtype) && (mf@columntype == columntype), TRUE))
  if (length(matches) > 0) {
    ngchm.env$matrixFunctions[[matches]] <- newmf
  } else {
    ngchm.env$matrixFunctions <- append(ngchm.env$matrixFunctions, newmf)
  }
  NULL
}

#' Register a type name.
#'
#' This function registers a type name used for determining row and column
#' linkouts.
#' This function is intended to be used by NGCHM system developers to record
#' basic information about the semantic interpretation of a type name.  Registration
#' of a typename is (currently) not required in order to use it.
#'
#' @param typename A character vector specifying the name(s) of the type(s) to be
#' registered.
#' @param description A single character string specifying the description of the
#' type(s).
#'
#' @return None. This function is used for its side effects of registering a new type.
#' @export
#'
#' @seealso [chmListTypes()]
#' @seealso [chmGetTypeInfo()]
#' @seealso [chmRegisterTypeMapper()]
chmRegisterType <- function(typename, description) {
  if (typeof(typename) != "character") {
    stop(sprintf("Parameter 'typename' must have type 'character', not '%s'", typeof(typename)))
  }
  if (typeof(description) != "character") {
    stop(sprintf("Parameter 'description' must have type 'character', not '%s'", typeof(description)))
  }
  if (length(typename) < 1) {
    stop("chmRegisterType: at least one typename must be specified.")
  }
  if (length(description) != 1) {
    stop(sprintf("chmRegisterType: description must be exactly a single string, not %d.", length(description)))
  }
  for (ii in 1:length(typename)) {
    tt <- data.frame(name = typename[ii], description = description, stringsAsFactors = FALSE)
    if (typename[ii] %in% ngchm.env$typeInfo$name) {
      warning(sprintf('Replacing definition of typename "%s"', typename[ii]))
      idx <- which(ngchm.env$typeInfo$name == typename[ii])
      ngchm.env$typeInfo[idx, ] <- tt
    } else {
      ngchm.env$typeInfo <- rbind(ngchm.env$typeInfo, tt)
    }
  }
  NULL
}

#' Get information about a type name.
#'
#' This function gets any registered information about a type name used for determining row and column
#' linkouts.
#' Registration of a typename is (currently) not required in order to use it, so it's possible for
#' valid type name not to have any registered information.
#'
#' @param typename The name of the type.
#'
#' @return Object of class "ngchm.type.info" containing basic information about the type.
#'
#' @export
#'
#' @seealso [chmListTypes()]
#' @seealso [chmRegisterType()]
chmGetTypeInfo <- function(typename) {
  if (typeof(typename) != "character") {
    stop(sprintf("Parameter 'typename' must have type 'character', not '%s'", typeof(typename)))
  }
  if (length(typename) != 1) {
    stop("chmGetTypeInfo: exactly one typename must be specified.")
  }
  if (!(typename %in% ngchm.env$typeInfo$name)) {
    typeinfo <- list(name = typename, description = NULL)
  } else {
    idx <- which(ngchm.env$typeInfo$name == typename)
    typeinfo <- as.list(ngchm.env$typeInfo[idx, ])
  }

  # Find any axis functions that match
  matches <- which(vapply(ngchm.env$axisFunctions, function(af) any(af@type == typename), TRUE))
  if (length(matches) > 0) {
    typeinfo$axisFunctions <- ngchm.env$axisFunctions[matches]
  } else {
    typeinfo$axisFunctions <- NULL
  }

  # Find any matrix functions that match
  matches <- which(vapply(ngchm.env$matrixFunctions, function(af) any(af@rowtype == typename) || any(af@columntype == typename), TRUE))
  if (length(matches) > 0) {
    typeinfo$matrixFunctions <- ngchm.env$matrixFunctions[matches]
  } else {
    typeinfo$matrixFunctions <- NULL
  }

  # Find any type mappers that match
  matches <- which(vapply(ngchm.env$typeMappers, function(af) any(af@fromtype == typename), TRUE))
  if (length(matches) > 0) {
    typeinfo$typeMappers <- ngchm.env$typeMappers[matches]
  } else {
    typeinfo$typeMappers <- NULL
  }

  class(typeinfo) <- "ngchm.type.info"
  typeinfo
}

#' Pretty Print NGCHM Type Information
#'
#' This function takes an object of class 'ngchm.type.info' and returns a formatted string
#' that provides a detailed description of the NGCHM type.
#'
#' @param x An object of class 'ngchm.type.info' as returned by chmGetTypeInfo.
#' @param ... Additional arguments (not used).
#'
#' @return A string that provides a detailed description of the NGCHM type.
#'
#' @export
#'
#' @seealso [chmGetTypeInfo()]
print.ngchm.type.info <- function(x, ...) {
  output <- c()
  output <- c(output, sprintf("NGCHM type %s: %s", x$name, x$description))
  if (length(x$axisFunctions) > 0) {
    fns <- paste(vapply(x$axisFunctions, function(af) af@label, ""), collapse = ", ")
    output <- c(output, sprintf("matches axis functions %s.", fns))
  }
  if (length(x$matrixFunctions) > 0) {
    fns <- paste(vapply(x$matrixFunctions, function(af) af@label, ""), collapse = ", ")
    output <- c(output, sprintf("matches matrix functions %s.", fns))
  }
  if (length(x$typeMappers) > 0) {
    types <- paste(vapply(x$typeMappers, function(af) af@totype, ""), collapse = ", ")
    output <- c(output, sprintf("maps to types %s.", types))
  }
  return(paste(output, collapse = "\n"))
}

#' Register a predefined Javascript function for converting values from
#' one type to another.
#'
#' This function registers a Javascript function that will be automatically
#' added to a Next Generation Clustered Heat Map as required for converting
#' values from one type into another more basic type.
#' This function is intended for use by NGCHM system developers.
#'
#' @param fromtype The type of values the function expects as input.
#' @param totype The type of values the function will produce.  The length of
#'        totype must be shorter than fromtype.
#' @param op The operation code for performing the conversion
#' @param ... Additional parameters required for specifying the conversion (op specific)
#'
#' @return NULL
#'
#' op can have the following values:
#' + 'field' Split source into fields separated by 'separator' and select field 'num' (0 origin)
#' + 'expr' Compute string expression 'expr'. 'return' value is a vector or a scalar (default)
#' + 'javascript' Evaluate javascript function 'fn' (deprecated)
#'
#' @export
#'
#' @seealso [chmAddAxisType()]
#' @seealso [chmRegisterAxisFunction()]
#' @seealso [chmRegisterMatrixFunction()]
#' @seealso [chmNewFunction()]
chmRegisterTypeMapper <- function(fromtype, totype, op, ...) {
  if (typeof(fromtype) != "character") {
    stop(sprintf("Parameter 'fromtype' must have type 'character', not '%s'", typeof(fromtype)))
  }
  if (typeof(totype) != "character") {
    stop(sprintf("Parameter 'totype' must have type 'character', not '%s'", typeof(totype)))
  }
  if (length(fromtype) < 1) {
    stop("chmRegisterTypeMapper: at least one fromtype must be specified.")
  }
  if (length(totype) != 1) {
    stop(sprintf("chmRegisterTypeMapper: totype must be exactly a single string, not %d.", length(totype)))
  }
  for (ii in 1:length(fromtype)) {
    if (nchar(totype) >= nchar(fromtype[ii])) {
      stop(sprintf("chmRegisterTypeMapper: totype ('%s') must be shorter than fromtype ('%s').", totype, fromtype[ii]))
    }
  }
  stopifnot(is(op, "character") && length(op) == 1)

  pp <- list(...)
  if (op == "field") {
    stopifnot("separator" %in% names(pp))
    stopifnot(is(pp$separator, "character") && length(pp$separator) == 1)
    if (!"num" %in% names(pp)) pp$num <- 0
    params <- list(separator = pp$separator, num = pp$num)
  } else if (op == "expr") {
    stopifnot("expr" %in% names(pp))
    stopifnot(is(pp$expr, "character") && length(pp$expr) == 1)
    if (!"return" %in% names(pp)) pp$return <- "scalar"
    params <- list(expr = pp$expr, return = pp$return)
  } else if (op == "javascript") {
    stopifnot("fn" %in% names(pp))
    if (is(pp$fn, "character")) {
      pp$fn <- chmGetFunction(pp$fn)
    }
    params <- list(fn = pp$fn)
  } else {
    stop("unknown type mapper op")
  }
  newtm <- new("ngchmTypeMapper", fromtype = fromtype, totype = totype, op = op, params = params)
  matches <- which(vapply(ngchm.env$typeMappers, function(tm) (length(intersect(tm@fromtype, fromtype)) > 0) && (tm@totype == totype), TRUE))
  if (length(matches) > 0) {
    matches <- sort(matches, decreasing = TRUE)
    for (idx in matches) {
      ftype <- ngchm.env$typeMappers[[idx]]@fromtype
      common <- intersect(ftype, fromtype)
      if (length(common) == length(ftype)) {
        # All fromtypes are replaced: remove mapping.
        ngchm.env$typeMappers <- ngchm.env$typeMappers[-idx]
      } else {
        ngchm.env$typeMappers[[idx]]@fromtype <- setdiff(ftype, common)
      }
    }
  }
  ngchm.env$typeMappers <- append(ngchm.env$typeMappers, newtm)
  NULL
}

#' Register a predefined Javascript function for use in NGCHM menus.
#'
#' This function registers a Javascript function that can be used when
#' building a Next Generation Clustered Heat Map.  This function is
#' intended for use by NGCHM system developers.
#'
#' @param fn The function to be registered. This should be an object of class 'ngchmJS'.
#'
#' @return The registered function.
#'
#' @export
#'
#' @seealso [chmAddMenuItem()]
#' @seealso [chmNewFunction()]
#' @seealso [ngchmAxisFunction-class]
#' @seealso [ngchmMatrixFunction-class]
chmRegisterFunction <- function(fn) {
  if (!is(fn, "ngchmJS")) {
    stop(sprintf("Parameter 'fn' must have type 'ngchmJS', not '%s'", classstr(fn)))
  }
  matches <- which(vapply(ngchm.env$scripts, function(ss) (ss@name == fn@name), TRUE))
  if (length(matches) > 0) {
    ngchm.env$scripts[[matches]] <- fn
  } else {
    ngchm.env$scripts <- append(ngchm.env$scripts, fn)
  }
  fn
}

#' Create and register an NGCHM server protocol implementation.
#'
#' This function creates and registers a protocol implementation for manipulating
#' an NGCHM server.
#'
#' @param protocolName A single character string specifying the name of the protocol.
#' @param chmFormat A single character string specifying the format of the heat map.
#' Defaults to "original".
#' @param requiredParams A character vector specifying the required parameters for the
#' protocol, if any.
#' @param optionalParams A character vector specifying the optional parameters for the
#' protocol, if any.
#' @param paramValidator A function(list) for validating the parameters specified
#' for a new server.
#' @param findCollection A function(server,collection,path) that finds a collection
#' on the server.
#' @param createCollection A function(server,collection,name) that creates a collection
#' on the server.
#' @param installMethod A function(server,chm) that installs a heat map on the server.
#' @param uninstallMethod A function(server,chmname) that uninstalls a heat map 
#' from the server.
#' @param makePrivate A function(server,chmname) that makes a heat map private
#' on the server.
#' @param makePublic A function(server,chmname) that makes a heat map public on the
#' server.
#' @param setCredentials A function(server,credentialstring) that sets the credentials
#' for the server.
#' @export
#' @return An object of class 'ngchmServerProtocol' representing the new server protocol.
ngchmCreateServerProtocol <- function(protocolName, chmFormat,
                                      requiredParams, optionalParams,
                                      paramValidator,
                                      findCollection, createCollection,
                                      installMethod, uninstallMethod,
                                      makePrivate, makePublic,
                                      setCredentials) {
  if (missing(chmFormat)) chmFormat <- "original"
  if (typeof(chmFormat) != "character") {
    stop(sprintf("Parameter 'chmFormat' must have type 'character', not '%s'", typeof(chmFormat)))
  }
  if (length(chmFormat) != 1) {
    stop(sprintf("Parameter 'chmFormat' must have a single value, not %d", length(chmFormat)))
  }
  if (nchar(chmFormat) == 0) {
    stop("Parameter 'chmFormat' cannot be the empty string")
  }
  if (typeof(protocolName) != "character") {
    stop(sprintf("Parameter 'protocolName' must have type 'character', not '%s'", typeof(protocolName)))
  }
  if (length(protocolName) != 1) {
    stop(sprintf("Parameter 'protocolName' must have a single value, not %d", length(protocolName)))
  }
  if (nchar(protocolName) == 0) {
    stop("Parameter 'protocolName' cannot be the empty string")
  }
  if (missing(requiredParams)) {
    requiredParams <- NULL
  }
  if (!is.null(requiredParams) && typeof(requiredParams) != "character") {
    stop(sprintf("Parameter 'requiredParams' must have type 'character', not '%s'", typeof(requiredParams)))
  }
  if (missing(optionalParams)) {
    optionalParams <- NULL
  }
  if (!is.null(optionalParams) && typeof(optionalParams) != "character") {
    stop(sprintf("Parameter 'optionalParams' must have type 'character', not '%s'", typeof(optionalParams)))
  }
  if (missing(paramValidator)) {
    paramValidator <- function(params) {
    }
  }
  if (missing(findCollection)) {
    findCollection <- function(server, collectionId, path) {
      return(NULL)
    }
  }
  if (missing(createCollection)) {
    createCollection <- function(server, collectionId, name) {
      return(NULL)
    }
  }
  if (missing(installMethod)) {
    installMethod <- function(server, chm) {
      stop("NGCHMs cannot be automatically installed on this server. Please obtain installation instructions from the server administrator.")
    }
  }
  if (missing(uninstallMethod)) {
    uninstallMethod <- function(server, chmname) {
      stop("NGCHMs cannot be automatically uninstalled from this server. Please obtain instructions from the server administrator.")
    }
  }
  if (missing(makePrivate)) {
    makePrivate <- function(server, chmname) {
      stop("NGCHMs cannot be automatically made private on this server. Please obtain instructions from the server administrator.")
    }
  }
  if (missing(makePublic)) {
    makePublic <- function(server, chmname) {
      stop("NGCHMs cannot be automatically made public on this server. Please obtain instructions from the server administrator.")
    }
  }
  if (missing(setCredentials)) {
    setCredentials <- function(server, credentialstring) {
      stop("You cannot set credentials for this server. Please obtain instructions from the server administrator.")
    }
  }
  dm <- new(
    Class = "ngchmServerProtocol", protocolName = protocolName, chmFormat = chmFormat,
    requiredParams = requiredParams,
    optionalParams = optionalParams,
    paramValidator = paramValidator,
    setCredentials = setCredentials,
    findCollection = findCollection, createCollection = createCollection,
    installMethod = installMethod, uninstallMethod = uninstallMethod,
    makePrivate = makePrivate, makePublic = makePublic
  )
  matches <- which(vapply(ngchm.env$serverProtocols, function(ss) (ss@protocolName == protocolName), TRUE))
  if (length(matches) > 0) {
    ngchm.env$serverProtocols[[matches]] <- dm
  } else {
    ngchm.env$serverProtocols <- append(ngchm.env$serverProtocols, dm)
  }
  dm
}

#' List defined server protocols
#'
#' @export
#'
#' @return A character vector

ngchmListServerProtocols <- function() {
  vapply(ngchm.env$serverProtocols, function(sp) sp@protocolName, "")
}

#' Get Server Protocol for NG-CHM
#'
#' This function gets a server protocol for NG-CHM (Next-Generation Clustered Heat Map)
#' by its name.
#'
#' @param protocolName A single character string specifying the name of the protocol.
#'
#' @return An object of class 'ngchmServerProtocol' representing the server protocol.
#' @export
ngchmGetServerProtocol <- function(protocolName) {
  if (typeof(protocolName) != "character") {
    stop(sprintf("Parameter 'protocolName' must have type 'character', not '%s'", typeof(protocolName)))
  }
  if (length(protocolName) != 1) {
    stop(sprintf("Parameter 'protocolName' must have a single value, not %d", length(protocolName)))
  }
  if (nchar(protocolName) == 0) {
    stop("Parameter 'protocolName' cannot be the empty string")
  }
  matches <- which(vapply(ngchm.env$serverProtocols, function(sp) (sp@protocolName == protocolName), TRUE))
  if (length(matches) == 0) {
    stop(sprintf("No server protocol found with name '%s'", protocolName))
  } else {
    return(ngchm.env$serverProtocols[[matches]])
  }
}

#' Get Protocol Parameter for NG-CHM Server
#'
#' This function gets a protocol parameter for a specified NG-CHM
#' (Next-Generation Clustered Heat Map) server.
#'
#' @param server An object of class 'ngchmServer' representing the server.
#' @param option A single character string specifying the name of the protocol parameter.
#' @param default An optional default value to return if the protocol parameter is
#' not found. Defaults to NULL.
#' @export
#' @return The value of the protocol parameter if it is found, otherwise the
#' specified default value.
ngchmGetProtoParam <- function(server, option, default = NULL) {
  stopifnot(is(server, "ngchmServer"))
  stopifnot(is(option, "character"))
  stopifnot(length(option) == 1)
  if (is.null(server@protoOpts)) {
    return(default)
  }
  if (option %in% names(server@protoOpts)) {
    return(server@protoOpts[[option]])
  }
  return(default)
}

#' List the predefined Javascript functions available for use in NGCHM menus.
#'
#' This function lists the predefined Javascript functions available for use in NGCHM menus.
#'
#' @param re The regular expression to match. This should be a single character string.
#' Default is ".*", which matches all functions.
#'
#' @export
#'
#' @seealso [chmAddMenuItem()]
#' @seealso [chmGetFunction()]
#' @seealso [chmRegisterFunction()]
#' @seealso [grep()]
#'
#' @return A string containing the names and descriptions of the matching functions.
#'
#' @examples
#' chmListFunctions() # List all functions.
#' chmListFunctions('^chm') # List all functions whose names start with 'chm'.
chmListFunctions <- function(re = ".*") {
  if (typeof(re) != "character") {
    stop(sprintf("Parameter 're' must have type 'character', not '%s'", typeof(re)))
  }
  output <- c()
  for (ii in 1:length(ngchm.env$scripts)) {
    fn <- ngchm.env$scripts[[ii]]
    if (length(grep(re, fn@name)) > 0) {
      output <- c(output, sprintf("%s\t%s", fn@name, fn@description))
    }
  }
  return(paste(output, collapse = "\n"))
}

#' Define and register a Javascript function for obtaining a specific metadata value.
#'
#' This function defines and registers a Javascript function for obtaining a specific
#' metadata value and returning it as a javascript list.  The function is suitable for
#' use as an axis type accessor function.
#'
#' @param functionName A single character string specifying the name of the
#' function to be registered.
#' @param metadataColumnName A single character string specifying the name
#' of the metadata column to be retrieved by the function.
#' @export
#' @return The registered function.
#' @seealso [chmAddAxisType()]
#' @seealso [chmGetFunction()]
#' @seealso [chmListFunctions()]
chmRegisterGetMetadataFunction <- function(functionName, metadataColumnName) {
  if (typeof(functionName) != "character") {
    stop(sprintf("Parameter 'functionName' must have type 'character', not '%s'", typeof(functionName)))
  }
  if (length(functionName) != 1) {
    stop(sprintf("Parameter 'functionName' must have a single value, not %d", length(functionName)))
  }
  if (nchar(functionName) == 0) {
    stop("Parameter 'functionName' cannot be the empty string")
  }
  if (typeof(metadataColumnName) != "character") {
    stop(sprintf("Parameter 'metadataColumnName' must have type 'character', not '%s'", typeof(metadataColumnName)))
  }
  if (length(metadataColumnName) != 1) {
    stop(sprintf("Parameter 'metadataColumnName' must have a single value, not %d", length(metadataColumnName)))
  }
  if (nchar(metadataColumnName) == 0) {
    stop("Parameter 'metadataColumnName' cannot be the empty string")
  }
  chmNewFunction(
    functionName,
    "This returns the label at the specified index as a list of values.  Can be used whenever the label itself is of the correct type.",
    paste(sprintf("function %s (axis, idx) {", functionName),
      sprintf("    return [axis.labels.getMetaData (idx).%s];", metadataColumnName),
      "};",
      sep = "\n"
    )
  )
}

#' Define and register a Javascript function for converting a lists of type values into single values.
#'
#' This function defines and registers a Javascript function for converting a list of type values
#' separated by the specified separator into the single values, and registers it as a type mapper.
#'
#' @param functionName A single character string specifying the name of the function
#' to be registered.
#' @param listtype A single character string specifying the type of the list to be split.
#' @param itemtype A single character string specifying the type of the items in the
#' list after splitting.
#' @param separator A single character string specifying the separator to be used for
#' splitting.

#' @export
#' @return None. This function is used for its side effects of registering a new type
#' splitter.
#' @seealso [chmGetFunction()]
#' @seealso [chmListFunctions()]
#' @seealso [chmRegisterTypeMapper()]
chmRegisterTypeSplitter <- function(functionName, listtype, itemtype, separator) {
  if (typeof(functionName) != "character") {
    stop(sprintf("Parameter 'functionName' must have type 'character', not '%s'", typeof(functionName)))
  }
  if (length(functionName) != 1) {
    stop(sprintf("Parameter 'functionName' must have a single value, not %d", length(functionName)))
  }
  if (nchar(functionName) == 0) {
    stop("Parameter 'functionName' cannot be the empty string")
  }
  if (typeof(separator) != "character") {
    stop(sprintf("Parameter 'separator' for typesplitter '%s' must have type 'character', not '%s'", functionName, typeof(separator)))
  }
  if (length(separator) != 1) {
    stop(sprintf("Parameter 'separator' for typesplitter '%s' must have a single value, not %d", functionName, length(separator)))
  }
  if (nchar(separator) == 0) {
    stop(sprintf("Parameter 'separator' for typesplitter '%s' cannot be the empty string", functionName))
  }
  fn <- chmNewFunction(
    functionName,
    sprintf("Convert %s to %s by splitting on '%s'", listtype, itemtype, separator),
    paste(sprintf("function %s (names) {", functionName),
      sprintf("    return names.map(function(nm){return nm.split('%s');}).reduce(function(a,b){return a.concat(b);});", separator),
      "}",
      sep = "\n"
    )
  )
  chmRegisterTypeMapper(listtype, itemtype, "javascript", fn = fn)
}

#' List known axis types.
#'
#' This function returns a list of the axis types for which axis- or matrix- menu entries may be defined.
#'
#' @param re Only types with names matching re are returned (default ".*")
#'
#' @return a character vector of axis type names
#'
#' @export
#'
#' @seealso [chmAddAxisType()]
chmListTypes <- function(re = ".*") {
  if (typeof(re) != "character") {
    stop(sprintf("Parameter 're' must have type 'character', not '%s'", typeof(re)))
  }
  if (length(re) != 1) {
    stop(sprintf("Parameter 're' must have a single value, not %d", length(re)))
  }
  at <- lapply(ngchm.env$axisFunctions, function(x) x@type)
  rt <- lapply(ngchm.env$matrixFunctions, function(x) x@rowtype)
  ct <- lapply(ngchm.env$matrixFunctions, function(x) x@columntype)
  tt <- lapply(ngchm.env$typeMappers, function(x) x@fromtype)
  allt <- c(at, rt, ct, tt, recursive = TRUE)
  allt <- allt[!duplicated(allt)]
  mm <- vapply(allt, function(x) length(grep(re, x)) > 0, TRUE)
  return(allt[mm])
}

#' Register a Javascript function for use in the NGCHM toolbox.
#'
#' This function registers a Javascript function that can included in
#' the toolbox of an NGCHM.  This function is
#' intended for use by NGCHM system developers.
#'
#' @param tbtype A single character string specifying the type of the toolbox function.
#' @param menulabel A single character string specifying the menu label of the
#' toolbox function.
#' @param jsfn The function to be registered. This should be an object of class 'ngchmJS'.
#' @export
#' @seealso [chmNewFunction()]
#' @seealso [ngchmAxisFunction-class]
#' @seealso [ngchmMatrixFunction-class]
#' @return None. This function is used for its side effects of registering a new
#' toolbox function.
chmRegisterToolboxFunction <- function(tbtype, menulabel, jsfn) {
  if (typeof(menulabel) != "character") {
    stop(sprintf("Parameter 'menulabel' must have type 'character', not '%s'", typeof(menulabel)))
  }
  if (length(menulabel) != 1) {
    stop(sprintf("Parameter 'menulabel' must have a single value, not %d", length(menulabel)))
  }
  if (nchar(menulabel) == 0) {
    stop("Parameter 'menulabel' cannot be the empty string")
  }
  if (typeof(tbtype) != "character") {
    stop(sprintf("Parameter 'tbtype' of toolbox function '%s' must have type 'character', not '%s'", menulabel, typeof(tbtype)))
  }
  if (length(tbtype) != 1) {
    stop(sprintf("Parameter 'tbtype' of toolbox function '%s' must have a single value, not %d", menulabel, length(tbtype)))
  }
  if (nchar(tbtype) == 0) {
    stop(sprintf("Parameter 'tbtype' of toolbox function '%s' cannot be the empty string", menulabel))
  }
  if (!is(jsfn, "ngchmJS")) {
    stop(sprintf(
      "a toolbox function must be of class ngchmJS (see chmNewFunction) not '%s'",
      classstr(jsfn)
    ))
  }
  if (length(jsfn@extraParams) != 1) {
    stop(sprintf(
      "a toolbox function requires exactly 1 extra parameter (called 'dataset'), not %d",
      length(jsfn@extraParams)
    ))
  }
  if (jsfn@extraParams != "dataset") {
    stop(sprintf(
      "a toolbox function requires exactly 1 extra parameter called 'dataset', not '%s'",
      jsfn@extraParams
    ))
  }
  matches <- NULL
  if (length(ngchm.env$toolbox) > 0) {
    matches <- which(apply(ngchm.env$toolbox, 1, function(tbf) (tbf$label == menulabel) && (tbf$type == tbtype)))
  }
  if (length(matches) > 0) {
    ngchm.env$toolbox[[matches, "fn"]] <- jsfn
  } else {
    ngchm.env$toolbox <- rbind(ngchm.env$toolbox, list(type = tbtype, label = menulabel, fn = jsfn))
  }
  NULL
}

#' Check that the specified layer can be added to the specificed NGCHM.
#'
#' Stops with an error if the specified layer cannot be added to the specified NGCHM.
#'
#' @param chm The CHM to which the new layer is being added.
#' @param layer The new layer is being added.
#'
#' @return NULL
#' @noRd
validateNewLayer <- function(chm, layer) {
  newnames <- dimnames(layer)
  if (length(chm@layers) > 0) {
    # Check new layer is compatible with first layer.
    oldnames <- dimnames(chm@layers[[1]])
    layer1 <- chm@layers[[1]]
    if (any(vapply(newnames, length, 0) != vapply(oldnames, length, 0))) {
      stop(sprintf(
        'Dimensions of new layer "%s" (%dx%d) for CHM "%s" differ from those of existing layers (%dx%d)',
        layer@name, nrow(layer), ncol(layer), chm@name, nrow(layer1), ncol(layer1)
      ))
    }
    nm <- newnames[[1]]
    nm1 <- oldnames[[1]]
    if (!setequal(nm, nm1)) {
      m <- sprintf(
        'Row names of new layer "%s" for CHM "%s" differ from those of existing layers',
        layer@name, chm@name
      )
      errs <- namesdifferror("new layer", nm, "existing layers", nm1)
      stop(paste(c(m, errs), collapse = "\n"))
    }
    nm <- newnames[[2]]
    nm1 <- oldnames[[2]]
    if (!setequal(nm, nm1)) {
      m <- sprintf(
        'Column names of new layer "%s" for CHM "%s" differ from those of existing layers',
        layer@name, chm@name
      )
      errs <- namesdifferror("new layer", nm, "existing layers", nm1)
      stop(paste(c(m, errs), collapse = "\n"))
    }
  } else {
    # First layer.  Check names are compatible with class bars, if any.
    layername <- sprintf('new layer "%s"', layer@name)
    if (length(chm@rowCovariateBars) > 0) {
      for (ii in 1:length(chm@rowCovariateBars)) {
        validateCovariateBar(chm, "Row", layername, newnames[[1]], chm@rowCovariateBars[[ii]])
      }
    }
    if (length(chm@colCovariateBars) > 0) {
      for (ii in 1:length(chm@colCovariateBars)) {
        validateCovariateBar(chm, "Column", layername, newnames[[2]], chm@colCovariateBars[[ii]])
      }
    }
    # Check names are compatible with row/column orders, if any.
    if (!is(chm@rowOrder, "function")) {
      validateAxisOrder(chm, "Row", layername, newnames[[1]], chm@rowOrder)
    }
    if (!is(chm@colOrder, "function")) {
      validateAxisOrder(chm, "Column", layername, newnames[[2]], chm@colOrder)
    }
  }
}

validateNewCovariateBar <- function(chm, where, bar) {
  if (length(chm@layers) > 0) {
    layer <- chm@layers[[1]]
    layername <- sprintf('layer "%s"', layer@name)
    if (where %in% c("row", "both")) {
      labels <- ngchmGetLabelsStr(layer@data, "row")
      validateCovariateBar(chm, "Row", layername, labels, bar)
    }
    if (where %in% c("column", "both")) {
      labels <- ngchmGetLabelsStr(layer@data, "column")
      validateCovariateBar(chm, "Column", layername, labels, bar)
    }
  }
}
validateCovariateBar <- function(chm, where, layername, labels, bar) {
  repo <- ngchmFindRepo(bar@data)
  barData <- ngchmLoadDatasetBlob(repo, bar@data, if (bar@type == "discrete") "" else 0.0)$mat
  if (length(intersect(labels, rownames(barData))) == 0) {
    m <- sprintf(
      '%s names of %s for CHM "%s" are completely different from those of covariate bar "%s"',
      where, layername, chm@name, bar@label
    )
    errs <- namesdifferror(layername, labels, sprintf('covariate bar "%s"', bar@label), rownames(barData))
    stop(paste(c(m, errs), collapse = "\n"))
  }
}
validateNewAxisOrder <- function(chm, where, order) {
  if (length(chm@layers) > 0) {
    layer <- chm@layers[[1]]
    layername <- sprintf('layer "%s"', layer@name)
    if (where %in% c("row", "both")) {
      labels <- ngchmGetLabelsStr(layer@data, "row")
      validateAxisOrder(chm, "Row", layername, labels, order)
    }
    if (where %in% c("column", "both")) {
      labels <- ngchmGetLabelsStr(layer@data, "column")
      validateAxisOrder(chm, "Column", layername, labels, order)
    }
  }
}


validateAxisOrder <- function(chm, where, layername, labels, order) {
  # NULL order allowed.
  if (length(order) == 0) {
    return()
  }
  # Convert other order types in a label vector.
  if (is(order, "dendrogram")) {
    order <- labels(order)
  } else if (is(order, "hclust")) {
    order <- labels(as.dendrogram(order))
  } else {
    stopifnot(mode(order) == "character")
  }

  # Order must be a permutation of labels.
  # so lengths must match
  if (length(labels) != length(order)) {
    m <- sprintf(
      'Number of %s names of %s for CHM "%s" (%d) differs from number of labels in order (%d)',
      where, layername, chm@name, length(labels), length(order)
    )
    errs <- namesdifferror(layername, labels, "order", order)
    stop(paste(c(m, errs), collapse = "\n"))
  }
  # and intersection must have the same length as well.
  if (length(intersect(labels, order)) != length(labels)) {
    m <- sprintf(
      '%s names of %s for CHM "%s" differ from those of the specified order',
      where, layername, chm@name
    )
    errs <- namesdifferror(layername, labels, "order", order)
    stop(paste(c(m, errs), collapse = "\n"))
  }
}
namesdifferror <- function(desc1, names1, desc2, names2) {
  msg <- c()
  # msg <- c(msg,sprintf ("Names in %s: %s", desc1, shortnameslist (names1)));
  # msg <- c(msg,sprintf ("Names in %s: %s", desc2, shortnameslist (names2)));
  only1 <- setdiff(names1, names2)
  if (length(only1) == 0) {
    msg <- c(msg, sprintf("No names in %s but not %s", desc1, desc2))
  } else {
    msg <- c(msg, sprintf("%d name(s) in %s but not %s: %s", length(only1), desc1, desc2, shortnameslist(only1)))
  }
  only2 <- setdiff(names2, names1)
  if (length(only2) == 0) {
    msg <- c(msg, sprintf("No names in %s but not %s", desc2, desc1))
  } else {
    msg <- c(msg, sprintf("%d name(s) in %s but not %s: %s", length(only2), desc2, desc1, shortnameslist(only2)))
  }
  msg
}

shortnameslist <- function(names, maxnames = 5) {
  if (length(names) > maxnames) {
    qnames <- c(sprintf('"%s"', names[1:maxnames]), "...")
  } else {
    qnames <- sprintf('"%s"', names)
  }
  paste(qnames, collapse = ", ")
}

#' Create a new Dialog for a NGCHM.
#'
#' This function creates a new Dialog suitable for adding to a Next Generation Clustered Heat Map.
#'
#' @param id The html id for the dialog.
#' @param title The dialog title / menu entry name.
#' @param fn The javascript function for customizing the dialog's contents.
#'
#' @return An object of class ngchmDialog
#'
#' @export
#'
#' @seealso [chmAdd()]
#' @seealso [chmAddDialog()]
#'
chmNewDialog <- function(id, title, fn) {
  if (is(fn, "character")) {
    fn <- chmGetFunction(fn)
  }
  dialog <- new(Class = "ngchmDialog", id = id, title = title, fn = fn)
  dialog
}
#' Create a new Axis for adding to an NG-CHM.
#'
#' This function creates a new Axis for adding to a Next Generation Clustered Heat Map.  You can specify any
#' axis name here, but chmAdd only accepts row, column, and both.
#'
#' @param axis The name of the axis
#' @param ... Objects to add to the axis
#'
#' @return An object of class 'ngchmAxis' representing the newly created axis.
#' @examples
#' x_axis <- chmAxis('row')
#' y_axis <- chmAxis('col')
#'
#' @export
#'
#' @seealso [chmAdd()]
chmAxis <- function(axis, ...) {
  new("ngchmAxis", axis = axis, objects = list(...))
}

#' Create a new AxisType for adding to an ngchmAxis.
#'
#' This function creates a new AxisType for adding to an ngchmAxis.
#'
#' @param typename The name of the axis type to be created. This should be a single character string.
#' @param func The function to be used for getting label values. If not provided, the default 'getLabelValue' function is used. 
#'             If a character string is provided, it is assumed to be the name of a function and is retrieved using 'chmGetFunction'.
#'             If a function is provided, it is checked to be of class 'ngchmJS'.
#'
#' @return An object of class 'ngchmAxisType' representing the newly created axis type.
#' @export
#'
#' @seealso [chmAxis()]
chmAxisType <- function(typename, func) {
  stopifnot(typeof(typename) == "character" && length(typename) == 1)
  if (missing(func)) {
    func <- chmGetFunction("getLabelValue")
  } else if (typeof(func) == "character" && length(func) == 1) {
    func <- chmGetFunction(func)
  } else {
    stopifnot(is(func, "ngchmJS"))
  }
  new(Class = "ngchmAxisType", where = "", type = typename, func = func)
}

#' List NG-CHM Servers
#'
#' This function lists all NG-CHM (Next-Generation Clustered Heat Map) servers that are currently available.
#'
#' @return A character vector containing the names of all available servers.
#'
#' @examples
#' servers <- chmListServers() # Get a list of all available servers.
#'
#' @export
chmListServers <- function() {
  vapply(ngchm.env$servers, function(svr) svr$name, "")
}

#' Get a HTTR handle for the server's view/WS URL
#'
#' This function returns a 'handle' suitable for use with the server's view/WS URL
#'
#' @param server An object of class ngchmServer
#'
#' @return An HTTR handle
#'
#' @import httr
#' @import jsonlite
#'
#' @export
#'
ngchmGetHandleHTTR <- function(server) {
  ws <- if (is(server, "ngchmServer")) server@serverURL else server
  if (!exists(ws, ngchm.env$handledb)) {
    assign(ws, httr::handle(ws), ngchm.env$handledb)
  }
  get(ws, ngchm.env$handledb)
}

#' Return response content interpreted as JSON
#'
#' @param httrResponse The httr response object
#'
#' @return The response parsed as JSON and returned as an R object
#'
#' @export
ngchmResponseJSON <- function(httrResponse) {
  stopifnot(httr::status_code(httrResponse) >= 200 && httr::status_code(httrResponse) < 300)
  jsonlite::fromJSON(content(httrResponse, "text", encoding = "UTF-8"), simplifyVector = FALSE)
}

getServerVersion <- function(server) {
  url <- if (is(server, "ngchmServer")) server@serverURL else server
  ws <- sprintf("%s/gdacws/servermetadata", url)
  res <- httr::GET(ws, handle = ngchmGetHandleHTTR(server))
  as.numeric(ngchmResponseJSON(res)$Build_Number)
}


testJava <- function(jarfile) {
  res <- NULL
  suppressWarnings(try(
    {
      res <- system2("java", c("-jar", jarfile), stdout = TRUE, stderr = TRUE)
    },
    silent = TRUE
  ))
  if (is.null(res)) stop("Unable to execute Java")
  if (length(res) == 0 || res[length(res)] != "WRONG_NUMBER_OF_PARAMETERS_ERROR") stop("Bad Java version")
}

getBuilderJar <- function(server) {
  if (is.null(server@jarFile)) {
    vvv <- sprintf("version%d", getServerVersion(server))
    if (!exists(vvv, ngchm.env$jarCache)) {
      ws <- sprintf("%s/resources/heatmappipeline.jar", server@serverURL)
      res <- httr::GET(ws, handle = ngchmGetHandleHTTR(server))
      tmpJarFile <- utempfile("hmt", fileext = ".jar")
      writeBin(res$content, tmpJarFile)
      testJava(tmpJarFile)
      assign(vvv, tmpJarFile, ngchm.env$jarCache)
    }
    return(get(vvv, ngchm.env$jarCache))
  }
  if (!exists(server@jarFile, ngchm.env$jarCache)) {
    # Load server@jarFile into jarCache
    if (length(grep("^scp://", server@jarFile)) > 0) {
      tmpJarFile <- utempfile("hmt", fileext = ".jar")
      parts <- URLparts(server@jarFile)
      if (parts[3] == "") {
        systemCheck(sprintf(
          "scp %s:%s %s",
          shQuote(parts[2]), shQuote(parts[4]), tmpJarFile
        ))
      } else {
        systemCheck(sprintf(
          "scp -P %s %s:%s %s",
          shQuote(parts[3]), shQuote(parts[2]), shQuote(parts[4]), tmpJarFile
        ))
      }
      ngchm.env$jarCache[[server@jarFile]] <- tmpJarFile
    } else if (length(grep("^http://", server@jarFile)) > 0) {
      tmpJarFile <- utempfile("hmt", fileext = ".jar")
      download.file(server@jarFile, tmpJarFile)
      ngchm.env$jarCache[[server@jarFile]] <- tmpJarFile
    } else if (length(grep("^https://", server@jarFile)) > 0) {
      tmpJarFile <- utempfile("hmt", fileext = ".jar")
      download.file(server@jarFile, tmpJarFile)
      ngchm.env$jarCache[[server@jarFile]] <- tmpJarFile
    } else if (length(grep("^file://", server@jarFile)) > 0) {
      ngchm.env$jarCache[[server@jarFile]] <- substring(server@jarFile, 8)
    } else {
      stop(sprintf("Unknown retrieval method for jarFile '%s' for NGCHM server '%s'", server@jarFile, server@name))
    }
    testJava(ngchm.env$jarCache[[server@jarFile]])
  }
  ngchm.env$jarCache[[server@jarFile]]
}

#' Create an ngchmServer object from a specification.
#'
#' Create an ngchmServer object called 'serverName' from the specification 'serverSpec' (see details).
#' serverOptions override those in the specification files option by option.
#' The new ngchmServer object is returned and registered so that it can be
#' referenced by name, including retrieval using chmServer.
#'
#' @param serverName The name of the new server object.
#' @param serverSpec The specification for the server (defaults to servername).
#' @param serverOptions A named list of server options.
#'
#' @return The created (and registered) ngchmServer object.
#' @export
#'
#' @details serverSpec can be any of:
#' \describe{
#'   \item{A configuration directory path. }{The specification will be read from a file 'config.txt' in that directory.}
#'   \item{An NGCHM server URL (ending in '/chm' or '/Viewer' for instance). }{A minimal specification will be inferred.
#'         Known methods for uploading NGCHMs to the server will be autoprobed unless specified manually.}
#'   \item{A URL referencing a configuration file (must end in '/config.txt'). }{The specification will be read from the
#'         specified URL.}
#' }
#' serverOptions can include both protocol-specific options and the following generic options:
#' \describe{
#'  \item{'serverURL'. }{The URL for the NGCHM server.}
#'  \item{'serverProtocol'. }{The protocol to be used for uploading etc. NGCHMs to the server.}
#'  \item{'jarFile'. }{The jarFile used to build NGCHMs.}
#'  \item{'traceLevel'. }{The amount of trace to output. Defaults to "PROGRESS".}
#' }
#'
#' @seealso [chmServer()]
#' @seealso [ngchmServer-class]
#' @seealso [ngchmGetServerProtocol()]
#' @seealso [ngchmServerProtocol-class]


chmCreateServer <- function(serverName,
                            serverSpec = NULL,
                            serverOptions = NULL) {
  defaultOptions <- list(traceLevel = "PROGRESS", serverProtocol = "manual", jarFile = NULL)
  if (is.null(serverSpec)) serverSpec <- serverName

  cfg <- new.env()

  if (substr(serverSpec, 1, 2) == "//") {
    serverSpec <- paste("http:", serverSpec, sep = "")
  }

  hasProto <- grepl("^[^/]*:", serverSpec)
  if (!hasProto) {
    # No protocol, so try to interpret as a server configuration directory
    if (file.exists(file.path(serverSpec, "config.txt"))) {
      readConfigFile(cfg, file.path(serverSpec, "config.txt"), "=")
    } else {
      stop(sprintf("'%s' is not a server configuration directory", serverSpec))
    }
  } else {
    proto <- sub(":.*", "", serverSpec)
    if (!(proto %in% c("http", "https", "scp", "file"))) {
      stop(sprintf("Unknown protocol in serverSpec '%s'", serverSpec))
    }
    if (proto == "file") {
      serverSpec <- sub("file:", "", serverSpec)
      if (file.exists(file.path(serverSpec, "config.txt"))) {
        readConfigFile(cfg, file.path(serverSpec, "config.txt"), "=")
      } else {
        stop(sprintf("'%s' is not a server configuration directory", serverSpec))
      }
    } else if (proto %in% c("http", "https")) {
      if (substring(serverSpec, nchar(serverSpec) - 10) == "/config.txt") {
        readConfigFile(cfg, serverSpec, "=")
      } else {
        # Assume a URL that refers to an NGCHM server.
        cfg$serverURL <- serverSpec
        # If protocol not specified, probe to see if API available.
        if (!"serverProtocol" %in% names(serverOptions)) {
          foundAPI <- FALSE
          # try manager API
          ws <- sprintf("%s/manager/rest/chmservers", serverSpec)
          res <- httr::GET(ws, handle = ngchmGetHandleHTTR(serverSpec))
          if (res$status_code >= 200 && res$status_code < 300) {
            content <- try(ngchmResponseJSON(res), silent = TRUE)
            if ((!is(content, "try-error")) && (length(content) > 0)) {
              cfg$serverProtocol <- "manager"
              cfg$deployServer <- sprintf("%s/manager/rest", serverSpec)
              cfg$serviceName <- names(content)[1]
              foundAPI <- TRUE
            }
          }
          if (!foundAPI) {
            # try shaidy API
            ws <- sprintf("%s/api/", serverSpec)
            res <- httr::GET(ws, handle = ngchmGetHandleHTTR(serverSpec))
            if (res$status_code >= 200 && res$status_code < 300) {
              content <- try(ngchmResponseJSON(res), silent = TRUE)
              if ((!is(content, "try-error")) && (length(content) > 0)) {
                cfg$serverProtocol <- "shaidy"
                cfg$basePath <- sprintf("%s/api", serverSpec)
                cfg$viewServer <- sprintf("%s/NGCHM", serverSpec)
                cfg$accessMethod <- "api"
                foundAPI <- TRUE
              }
            }
          }
        }
      }
    } else if (proto == "scp") {
      defaultOptions$serverProtocol <- "mds2"
      spec <- sub("^[^/]*:", "", serverSpec)
      if (substr(spec, 1, 2) != "//") stop(sprintf("Malformed server spec '%s'", serverSpec))
      spec <- substring(spec, 3)
      cfgServer <- sub("/.*", "", spec)
      cfgDir <- sub("^[^/]*", "", spec)
      if (nchar(defaultOptions$deployServer) == 0) stop(sprintf("Malformed server spec '%s'", serverSpec))
      if (nchar(defaultOptions$deployDir) == 0) stop(sprintf("Malformed server spec '%s'", serverSpec))
      defaultOptions$deployServer <- cfgServer
      defaultOptions$deployDir <- cfgDir

      cfgFile <- utempfile("cfg", fileext = ".txt")
      rcfg <- new.env()
      if (system2("scp", args = c(sprintf("%s:%s/%s", cfgServer, cfgDir, "config.txt"), cfgFile)) == 0) {
        readConfigFile(rcfg, cfgFile, "=")
      }
      rcfg <- as.list(rcfg)
      if (!("jarFile" %in% names(serverOptions))) {
        # No user specified jarFile, so let's see what the remote config says.
        if (length(rcfg$jarFile) == 0) {
          # No jarFile specified in remote config, so peek to see if there's a remote jarFile in .mds subdirectory.
          jarFile <- paste(cfgDir, ".mds", "heatmappipeline.jar", sep = "")
          found <- system2("ssh", args = c(cfgServer, sprintf("[ -r %s ]", jarFile)))
          if (found == 0) {
            # Shell command above succeeded.
            rcfg$jarFile <- paste("scp://", cfgServer, jarFile, sep = "")
          }
        } else if (substr(rcfg$jarFile, 1, 7) == "file://") {
          # Remote specified jarFile using file://, so convert to an scp://
          jarFile <- substr(rcfg$jarFile, 8, nchar(rcfg$jarFile))
          if (substr(jarFile, 1, 1) != "/") {
            jarFile <- paste("", cfgDir, jarFile, sep = "/")
          }
          found <- system2("ssh", args = c(cfgServer, sprintf("[ -r %s ]", jarFile)))
          if (found == 0) {
            rcfg$jarFile <- paste("scp://", cfgServer, jarFile, sep = "")
          } else {
            stop(sprintf("Unable to access remote mds2 jarfile scp://%s%s", cfgServer, jarFile))
          }
        } else {
          # We assume network based addresses will work equally well from the current machine.
        }
      }
      for (field in names(rcfg)) {
        cfg[[field]] <- rcfg[[field]]
      }
    }
  }
  fileOptions <- as.list(cfg)

  if ("jarFile" %in% names(serverOptions)) {
    # the user explicitly specified the jarFile to use.  Use it or fail.
    if (file.exists(serverOptions$jarFile)) {
      serverOptions$jarFile <- paste("file://", serverOptions$jarFile, sep = "")
    } else {
      stop(sprintf("Specified jarFile '%s' does not exist", serverOptions$jarFile))
    }
  }

  classFields <- c("traceLevel", "serverProtocol", "deployServer", "jarFile", "serverURL", "viewServer")

  # defaultOptions, overridden by file options, overridden by explicit serverOptions.
  cfg <- defaultOptions
  for (field in names(fileOptions)) {
    cfg[[field]] <- fileOptions[[field]]
  }
  for (field in names(serverOptions)) {
    cfg[[field]] <- serverOptions[[field]]
  }

  stopifnot("serverProtocol" %in% names(cfg))
  protocol <- ngchmGetServerProtocol(cfg$serverProtocol)
  protoOpts <- cfg[setdiff(names(cfg), classFields)]
  ngchmProtoParamCheck(protoOpts, protocol@requiredParams, protocol@optionalParams)
  protocol@paramValidator(protoOpts)

  ngchmRegisterServer("base", new(
    Class = "ngchmServer",
    name = serverName,
    serverURL = cfg$serverURL,
    serverProtocol = protocol,
    traceLevel = cfg$traceLevel,
    jarFile = cfg$jarFile,
    deployServer = cfg$deployServer,
    viewServer = cfg$viewServer,
    protoOpts = protoOpts
  ))
}

#' Create an ngchmServer object for a managed NG-CHM server
#'
#' Create an ngchmServer object called 'serverName' (see details).
#' The new ngchmServer object is returned and registered so that it can be
#' referenced by name, including retrieval using chmServer.
#' This library will communicate with the NG-CHM using the private address.
#' Returned URLs for viewing NG-CHMs will use the public address.
#'
#' @param serverName The name of the new server object.
#' @param privateAddr Private IP name/address of the server.
#' @param publicAddr Public IP name/address of the server.
#' @param chmPort Port on which the chm viewer is listening.
#' @param managerPort Port on which the chm manager is listening.
#' @param serviceName Name of the chmManager service
#' @param ... Additional options passed to chmCreateServer
#'
#' @return The created (and registered) ngchmServer object.
#' @export
#'
#' @seealso [chmServer()]
#' @seealso [chmCreateServer()]

chmCreateManagedServer <- function(serverName, privateAddr, publicAddr = NULL, chmPort = 80, managerPort = 18080, serviceName = "default", ...) {
  if (is.null(publicAddr)) publicAddr <- privateAddr
  chmCreateServer(
    serverName,
    sprintf("http://%s:%d/chm", privateAddr, chmPort),
    list(
      serverProtocol = "manager",
      deployServer = sprintf("http://%s:%d/chm/manager/rest", privateAddr, managerPort),
      serviceName = serviceName,
      viewServer = sprintf("http://%s:%d/chm", publicAddr, chmPort),
      ...
    )
  )
}

#' Check Protocol Parameters for NG-CHM
#'
#' Check that all required parameters are specified, and all specified parameters are
#' either required or optional.
#'
#' @param params A list of parameters to be checked.
#' @param required A character vector specifying the required parameters.
#' @param optional A character vector specifying the optional parameters.
#' @export
#' @return None. This function is used for its side effects of checking the
#' parameters and potentially stopping execution with an error message.
ngchmProtoParamCheck <- function(params, required, optional) {
  nm <- names(params)
  need <- setdiff(required, nm)
  if (length(need) != 0) stop(sprintf("Required protocol parameter(s) %s not specified", paste(need, collapse = ",")))
  unknown <- setdiff(nm, c(required, optional))
  if (length(unknown) != 0) stop(sprintf("Unknown protocol parameter(s) %s specified", paste(unknown, collapse = ",")))
}

readConfigFile <- function(cfg, filename, sep = ":") {
  lines <- NULL
  try(suppressWarnings(lines <- readLines(filename)), silent = TRUE)
  if (length(lines) > 0) {
    for (line in strsplit(lines, sep)) {
      if (length(line) != 2) stop(sprintf('Malformed configuration line "%s" in %s: should be option%svalue', paste(line, sep = sep), filename, sep))
      cfg[[line[1]]] <- line[2]
    }
  }
}

getExtraParams <- function(props) {
  # Collect extraparams.
  pat <- "^!extraparam:"
  idx <- which(vapply(props, function(x) regexpr(pat, x@label) != -1, TRUE))
  params <- new.env()
  for (pp in props[idx]) {
    match <- regexpr(pat, pp@label)
    paramname <- substring(pp@label, attr(match, "match.length") + 1)
    params[[paramname]] <- pp@value
  }
  params
}

bindExtraParams <- function(funcs, params, props) {
  hasep <- vapply(funcs, function(fn) length(fn@func@extraParams) > 0, TRUE)
  epfuncs <- funcs[which(hasep)]
  funcs <- funcs[which(!hasep)]
  for (fn in epfuncs) {
    extra <- fn@func@extraParams
    if (all(vapply(extra, function(p) exists(p, params), TRUE))) {
      vals <- vapply(extra, function(p) params[[p]], "")
      names(vals) <- extra
      tmpname <- paste(c("f", sample(c(letters, toupper(letters), as.character(0:9)), 15, replace = TRUE)), collapse = "")
      chmBindFunction(tmpname, fn@func@name, as.list(vals))
      if (is(fn, "ngchmAxisFunction")) {
        newfn <- new(class(fn), type = fn@type, label = fn@label, func = chmGetFunction(tmpname))
      } else if (is(fn, "ngchmMatrixFunction")) {
        newfn <- new(class(fn), rowtype = fn@rowtype, coltype = fn@coltype, label = fn@label, func = chmGetFunction(tmpname))
      } else {
        stop(sprintf("Unknown function class '%s'", classstr(fn)))
      }
      funcs <- append(funcs, newfn)
    }
  }
  funcs
}

chmAddAutoMenuItems <- function(chm) {
  params <- getExtraParams(chm@properties)

  # Add row menu items for types we know about.
  genSpecFeedback(10, "adding row menu items")
  rowtypes <- getAllAxisTypes(chm, "row")
  rowTypeFnsReqd <- rep(FALSE, length(rowtypes))
  rowfns <- getAllAxisTypeFunctions(chm@rowTypeFunctions, rowtypes$types)
  rowfns <- bindExtraParams(rowfns, params, chm@properties)
  if (length(rowfns) > 0) {
    for (ii in 1:length(rowfns)) {
      fn <- rowfns[[ii]]
      rowTypeFnsReqd[getFnsRqrd(rowtypes, fn@type)] <- TRUE
      entry <- new(
        Class = "ngchmMenuItem", label = fn@label, description = fn@func@description,
        fun = sprintf("function(s,a,e){%s(%s);}", fn@func@name, getValueExpr(rowtypes, fn@type, "axis"))
      )
      chm@rowMenu <- append(chm@rowMenu, entry)
    }
  }

  # Add column menu items for types we know about.
  genSpecFeedback(20, "adding column menu items")
  coltypes <- getAllAxisTypes(chm, "column")
  colTypeFnsReqd <- rep(FALSE, length(coltypes))
  colfns <- getAllAxisTypeFunctions(chm@colTypeFunctions, coltypes$types)
  colfns <- bindExtraParams(colfns, params, chm@properties)
  if (length(colfns) > 0) {
    for (ii in 1:length(colfns)) {
      fn <- colfns[[ii]]
      colTypeFnsReqd[getFnsRqrd(coltypes, fn@type)] <- TRUE
      entry <- new(
        Class = "ngchmMenuItem", label = fn@label, description = fn@func@description,
        fun = sprintf("function(s,a,e){%s(%s);}", fn@func@name, getValueExpr(coltypes, fn@type, "axis"))
      )
      chm@colMenu <- append(chm@colMenu, entry)
    }
  }

  # Add matrix element menu items for types we know about.
  genSpecFeedback(30, "adding matrix menu items")
  matfns <- getAllMatrixTypeFunctions(chm, rowtypes$types, coltypes$types)
  matfns <- bindExtraParams(matfns, params, chm@properties)
  if (length(matfns) > 0) {
    for (ii in 1:length(matfns)) {
      fn <- matfns[[ii]]
      rowTypeFnsReqd[getFnsRqrd(rowtypes, fn@rowtype)] <- TRUE
      colTypeFnsReqd[getFnsRqrd(coltypes, fn@columntype)] <- TRUE
      entry <- new(
        Class = "ngchmMenuItem", label = fn@label, description = fn@func@description,
        fun = sprintf(
          "function(rs,cs,e){%s(%s,%s);}", fn@func@name,
          getValueExpr(rowtypes, fn@rowtype, "row"), getValueExpr(coltypes, fn@columntype, "column")
        )
      )
      chm@elementMenu <- append(chm@elementMenu, entry)
      message("chmMake: added elementMenu entry ", entry@label)
    }
  }

  # Add functions for getting type values from selections.
  genSpecFeedback(40, "adding type selector functions")
  message("chmMake: matfns contains ", length(matfns), " entries")
  fns <- append(matfns, unique(append(rowfns, colfns)))
  if (length(fns) > 0) {
    for (ii in 1:length(fns)) {
      if (length(fns[[ii]]) == 0) {
        message("chmMake: axis/mat fn entry is NULL")
      } else if (is(fns[[ii]], "ngchmAxisType")) {
        chm <- chmAddMenuItem(chm, "nowhere", "unused", fns[[ii]]@func)
      } else if (is(fns[[ii]], "ngchmAxisFunction")) {
        chm <- chmAddMenuItem(chm, "nowhere", "unused", fns[[ii]]@func)
      } else if (is(fns[[ii]], "ngchmMatrixFunction")) {
        chm <- chmAddMenuItem(chm, "nowhere", "unused", fns[[ii]]@func)
      } else if (fns[[ii]]@op != "javascript") {
        message("chmMake: axis/mat fn op is not javascript")
      } else {
        chm <- chmAddMenuItem(chm, "nowhere", "unused", fns[[ii]]@params$func)
      }
    }
  }

  fns <- unique(append(rowtypes$builders[rowTypeFnsReqd], coltypes$builders[colTypeFnsReqd]))
  if (length(fns) > 0) {
    for (ii in 1:length(fns)) {
      if (length(fns[[ii]]) == 0) {
        message("chmMake: builders fn entry is NULL")
      } else if (is(fns[[ii]], "ngchmAxisType")) {
        chm <- chmAddMenuItem(chm, "nowhere", "unused", fns[[ii]]@func)
      } else if (is(fns[[ii]], "ngchmAxisFunction")) {
        chm <- chmAddMenuItem(chm, "nowhere", "unused", fns[[ii]]@func)
      } else if (fns[[ii]]@op != "javascript") {
        message("chmMake: builders fn op is not javascript")
      } else {
        chm <- chmAddMenuItem(chm, "nowhere", "unused", fns[[ii]]@params$func)
      }
    }
  }

  chmUU(chm)
}

#' Output Javascript code required to customize an NGCHM.
#'
#' This function outputs the Javascript required to customize an NGCHM.
#'
#' @param chm An object of class 'chm' representing the heat map.
#' @param filename A single character string specifying the name of the file
#' where the JavaScript will be written.
#'
#' @export
#' @return None. This function is used for its side effects of writing the JavaScript to a file.
chmWriteCustomJS <- function(chm, filename) {
  chm <- chmFixVersion(chm)
  if (length(chm@datasets) > 0) {
    # genSpecFeedback (8, "adding toolbox(es)");
    chm <- addToolBoxes(chm)
  }
  chm <- chmAddAutoMenuItems(chm)
  writeCustomJS(chm, filename)
}

#' Get the file path to the specified overview file.
#'
#' This function returns the file path to the specified overview image of the CHM.  The CHM must be
#' made before the file can be accessed.
#' If idx is specified, format if given must equal that of the overview image, and the path to that overview image is returned.
#' If idx is not specified, the file path to the first overview of the given format (default 'png') is returned.
#'
#' @param chm The CHM for which the overview is to be retrieved.
#' @param format The format of overview image desired (defaults to 'png' if idx is not specified).
#' @param idx The index of the overview image desired (defaults to first image of the specified format).
#'
#' @export
#'
#' @return The path to the retrieved overview.
chmGetOverview <- function(chm, format = NULL, idx = NULL) {
  chm <- chmFixVersion(chm)
  if (is.null(idx)) {
    if (is.null(format)) format <- "png"
    idx <- which(vapply(chm@overviews, function(ov) ov@format, "") == format)
    if (is.null(idx)) stop(sprintf('CHM "%s" has no overview with format "%s"', chm@name, format))
    idx <- idx[1]
  } else {
    if ((idx < 1) || (idx > length(chm@overviews))) {
      stop(sprintf('Invalid index (%d) for overview of CHM "%s"', idx, chm@name))
    }
    if (is.null(format)) {
      format <- chm@overviews[[idx]]@format
    } else if (format != chm@overviews[[idx]]@format) {
      stop(sprintf('Overview #%d of CHM "%s" does not have format "%s"', idx, chm@name, format))
    }
  }
  file.path(chm@outDir, chm@name, "overview", sprintf("overview%d.%s", idx, format))
}

#' @import digest
getuuid <- function(prev = "") {
  digest::digest(paste(c(Sys.info(), Sys.time(), rnorm(1), prev, collapse = ""), recursive = TRUE), algo = "sha256")
}

chmFixCovariateVersion <- function(covariate) {
  if (!is(covariate@label.series, "shaid")) {
    values <- covariate@label.series
    stopifnot(is(values, "character"))
    values <- values[order(names(values))]
    mat <- matrix(values, ncol = 1, dimnames = list(names(values), "Value"))
    covariate@label.series <- ngchmSaveAsDatasetBlob(ngchm.env$tmpShaidy, "tsv", mat)
  }
  covariate
}

chmFixDatasetVersion <- function(dataset) {
  dataset@row.covariates <- lapply(dataset@row.covariates, chmFixCovariateVersion)
  dataset@column.covariates <- lapply(dataset@column.covariates, chmFixCovariateVersion)
  dataset
}

chmFixVersion <- function(chm) {
  if (chm@version < 2) {
    warning("Upgrading chm ", chm@name, " from version ", chm@version, " to version 2")
    v2 <- new("ngchmVersion2")
    for (name in slotNames("ngchmVersion1")) {
      if (!name %in% c("version", "rowClassbars", "colClassbars")) {
        slot(v2, name) <- slot(chm, name)
      }
    }
    v2@rowCovariateBars <- chm@rowClassbars
    v2@colCovariateBars <- chm@colClassbars
    for (ii in 1:length(chm@layers)) {
      if (is(chm@layers[[ii]]@data, "matrix")) {
        v2@layers[[ii]]@data <- ngchmSaveAsDatasetBlob(ngchm.env$tmpShaidy, "tsv", v2@layers[[ii]]@data)
      }
    }
    chm <- v2
  }
  if (chm@version == 2) {
    if (length(chm@layers) > 0) {
      # Early version2 did not have summarizationMethod slot
      for (ii in 1:length(chm@layers)) {
        if (!.hasSlot(chm@layers[[ii]], "summarizationMethod")) {
          chm@layers[[ii]]@summarizationMethod <- "average"
        }
      }
    }
    if (length(chm@templates) > 0) {
      # Early version2 did not have dest.blob slot
      for (ii in 1:length(chm@templates)) {
        if (!.hasSlot(chm@templates[[ii]], "dest.blob")) {
          t <- chm@templates[[ii]]
          # FIXME: will not work if source.path is a file that is either no longer available or different
          # It might be possible to find original template where CHM came from???
          chm@templates[[ii]]@dest.blob <-
            ngchmSaveTemplateAsBlob(ngchm.env$tmpShaidy, t@source.path, t@dest.path, t@substitutions)
        }
      }
    }
    chm@datasets <- lapply(chm@datasets, chmFixDatasetVersion)
  }
  if (length(chm@rowOrder) > 0 && is(chm@rowOrder, "function")) {
    if (!identical(chm@rowOrder, chmDefaultRowOrder) &&
      !identical(chm@rowOrder, chmOriginalRowOrder) &&
      !identical(chm@rowOrder, chmRandomRowOrder)) {
      if (chm@rowOrderMethod == "Random") {
        chm@rowOrder <- chmRandomRowOrder
      } else if (chm@rowOrderMethod == "Original") {
        chm@rowOrder <- chmOriginalRowOrder
      } else {
        chm@rowOrder <- chmDefaultRowOrder
        chm@rowOrderMethod <- "Hierarchical"
      }
    }
  }
  if (length(chm@colOrder) > 0 && is(chm@colOrder, "function")) {
    if (!identical(chm@colOrder, chmDefaultColOrder) &&
      !identical(chm@colOrder, chmOriginalColOrder) &&
      !identical(chm@colOrder, chmRandomColOrder)) {
      if (chm@colOrderMethod == "Random") {
        chm@colOrder <- chmRandomColOrder
      } else if (chm@colOrderMethod == "Original") {
        chm@colOrder <- chmOriginalColOrder
      } else {
        chm@colOrder <- chmDefaultColOrder
        chm@colOrderMethod <- "Hierarchical"
      }
    }
  }
  chm
}

chmUU <- function(chm) {
  chm@uuid <- getuuid(chm@uuid)
  chm
}

#' @import digest
gitSha <- function(data) {
  stopifnot(typeof(data) %in% c("character", "raw"))
  if (typeof(data) == "character") {
    data <- charToRaw(paste(data, collapse = ""))
  }
  head <- sprintf("blob %d", length(data))
  digest::digest(c(charToRaw(head), raw(1), data), "sha1", serialize = FALSE)
}

utempfile <- function(...) {
  # On Windows, file.path separates files by / , tempfile (and tempdir) separate them by backslashes
  # We need consistency, so convert to canonical separator / here, rather than complicate all other uses
  filename <- tempfile(...)
  if (Sys.info()[["sysname"]] == "Windows") {
    filename <- gsub("\\\\", "/", filename)
  }
  filename
}

#' Browse the NGCHMs on the specified server in the viewer.
#'
#' Opens the NG-CHM browser page in the viewer.
#'
#' @param server The NG-CHM server to be browsed. If NULL, the function will use the first server in the list of available servers.
#' @param viewer The function to be used to open the web browser. If NULL, the function will use the 'browseURL' function.
#' @export
#' @return None. This function is used for its side effects of opening a web browser to view the NG-CHM server.
#'
#' @seealso [utils::browseURL()]
chmBrowse <- function(server = NULL, viewer = NULL) {
  if (is.null(server)) server <- getOption("NGCHM.Server", chmListServers()[1])
  if (!is(server, "ngchmServer")) server <- chmServer(server)
  if (is.null(viewer)) viewer <- getOption("viewer", browseURL)
  viewer(server@viewServer)
}

#' Open the NG-CHM Manager
#'
#' This function opens a web browser to view the NG-CHM (Next-Generation Clustered Heat Map) Manager on the specified server.
#'
#' @seealso [utils::browseURL()]
#' @param server The NG-CHM server to be browsed. If NULL, the function will use the first server
#' in the list of available servers.
#' @param viewer The function to be used to open the web browser. If NULL, the function will use the 'browseURL' function.
#'
#' @return None. This function is used for its side effects of opening a web browser to view the NG-CHM Manager.
#'
#' @export
chmManager <- function(server = NULL, viewer = NULL) {
  if (is.null(server)) server <- getOption("NGCHM.Server", chmListServers()[1])
  if (!is(server, "ngchmServer")) server <- chmServer(server)
  stopifnot(server@serverProtocol@protocolName == "manager")
  if (is.null(viewer)) viewer <- getOption("viewer", browseURL)
  viewer(paste(server@viewServer, "manager", sep = "/"))
}

#' Open the NG-CHM on the specified server in the viewer.
#'
#' @param x The NGCHM to view.
#' @param server The server containing the NG-CHM.  Defaults to option "NGCHM.Server" or the first server.
#' @param viewer The viewer to use. Defaults to option "viewer" or browseURL.
#' @param ... Ignored.
#' @return No return value. The function is called for its side effect of plotting the specified NG-CHM.
#' @export
plot.ngchmVersion2 <- function(x, server = NULL, viewer = NULL, ...) {
  if (is.null(server)) server <- getOption("NGCHM.Server", chmListServers()[1])
  if (!is(server, "ngchmServer")) server <- chmServer(server)
  if (is.null(viewer)) viewer <- getOption("viewer", browseURL)
  viewer(chmGetURL(x, server = server))
}

#' Write a matrix as a binary viewer tile
#'
#' Write the matrix as a vector of 32-bit little-endian floats to the specified file.
#'
#' @param mat The matrix to output
#' @param filename The name of the file to write the tile to
#' @noRd
writeTile <- function(mat, filename) {
  vec <- t(mat)
  dim(vec) <- NULL
  writeBin(vec, filename, size = 4, endian = "little")
}

#' Read a binary viewer tile as a matrix
#'
#' Read a vector of little-endian 32-bit floats from the specified file and return
#' as a matrix with nrow rows and ncol columns.
#'
#' @param filename The name of the file to read the tile from
#' @param nrow Number of rows in the tile
#' @param ncol Number of columns in the tile
#' @noRd
readTile <- function(filename, nrow, ncol) {
  vec <- readBin(filename, "double", nrow * ncol, size = 4, endian = "little")
  return(matrix(vec, nrow = nrow, byrow = TRUE))
}

#' Export a standalone NGCHM to a file.
#'
#' Create a standalone viewer for the NGCHM in the specified file.
#' This function requires **Java 11** and the
#' **[NGCHMSupportFiles](https://github.com/MD-Anderson-Bioinformatics/NGCHMSupportFiles)** package.
#'
#' The NGCHMSupportFiles package can be installed from the R-universe repository: \cr\cr
#' \code{install.packages('NGCHMDemoData', } \cr
#' \code{repos = c('https://md-anderson-bioinformatics.r-universe.dev',} \cr
#' \code{'https://cloud.r-project.org'))}
#'
#' @export
#' @rdname chmExportToFile-method
#'
#' @param chm The NGCHM to export
#' @param filename The file in which to save the rendered NGCHM
#' @param overwrite Overwrite file iff true (default false)
#' @param shaidyMapGen Path to shaidyMapGen jar file (default to value of environment variable SHAIDYMAPGEN)
#' @param shaidyMapGenJava Path to java executable with which to run shaidyMapGen (default to value of environment variable SHAIDYMAPGENJAVA or java)
#' @param shaidyMapGenArgs Additional arguments to pass to java when running shaidyMapGen (default to value of environment variable SHAIDYMAPGENARGS)
#'
#' @return the rendered NGCHM
chmExportToFile <- function(chm, filename, overwrite = FALSE, shaidyMapGen, shaidyMapGenJava, shaidyMapGenArgs) {
  if (!overwrite && file.exists(filename)) stop("'filename' already exists")
  if (missing(shaidyMapGen)) shaidyMapGen <- Sys.getenv("SHAIDYMAPGEN")
  if (missing(shaidyMapGenJava)) shaidyMapGenJava <- Sys.getenv("SHAIDYMAPGENJAVA")
  if (shaidyMapGenJava == "") shaidyMapGenJava <- "java"
  if (!checkForJavaVersion(shaidyMapGenJava)) {
    stop("Missing required java version.")
  }
  if (missing(shaidyMapGenArgs)) shaidyMapGenArgs <- strsplit(Sys.getenv("SHAIDYMAPGENARGS"), ",")[[1]]
  if (shaidyMapGen == "") {
    checkForNGCHMSupportFiles()
    stop("Missing required path to ShaidyMapGen.jar file.")
  }
  chm@format <- "shaidy"
  chm <- chmAddProperty(chm, "chm.info.build.time", format(Sys.time(), "%F %H:%M:%S"))
  chm <- chmMake(chm)

  shaidyRepo <- ngchm.env$tmpShaidy
  shaid <- shaidyGetShaid(chm)
  status <- system2(shaidyMapGenJava,
    c(shaidyMapGenArgs, "-jar", shaidyMapGen, shaidyRepo$basepath, shaid@value, shaid@value, "NO_PDF"),
    env = c("DISPLAY=''")) # Set DISPLAY to empty string to prevent X11 errors in Rstudio Docker container
  if (status != 0) stop("export to ngchm failed")
  if (!file.copy(shaidyRepo$blob.path("viewer", shaid@value, chm@name, paste(chm@name, "ngchm", sep = ".")), filename, TRUE)) {
    stop("export to ngchm failed")
  }
  chm
}

#' Export a PDF of the NGCHM to a file.
#'
#' Create a PDF of the NGCHM in the specified file.
#' This function requires **Java 11** and the
#' **[NGCHMSupportFiles](https://github.com/MD-Anderson-Bioinformatics/NGCHMSupportFiles)** package.
#'
#' The NGCHMSupportFiles package can be installed from the R-universe repository: \cr\cr
#' \code{install.packages('NGCHMDemoData', } \cr
#' \code{repos = c('https://md-anderson-bioinformatics.r-universe.dev',} \cr
#' \code{'https://cloud.r-project.org'))}
#'
#' @export
#' @rdname chmExportToPDF-method
#'
#' @param chm The NGCHM to generate the PDF for
#' @param filename The file in which to save the PDF
#' @param overwrite Overwrite file iff true (default false)
#' @param shaidyMapGen Path to shaidyMapGen jar file (default to value of environment variable SHAIDYMAPGEN)
#' @param shaidyMapGenJava Path to java executable with which to run shaidyMapGen (default to value of environment variable SHAIDYMAPGENJAVA or java)
#' @param shaidyMapGenArgs Additional arguments to pass to java when running shaidyMapGen (default to value of environment variable SHAIDYMAPGENARGS)
#'
#' @return filename
chmExportToPDF <- function(chm, filename, overwrite = FALSE, shaidyMapGen, shaidyMapGenJava, shaidyMapGenArgs) {
  if (!overwrite && file.exists(filename)) stop("'filename' already exists")
  if (missing(shaidyMapGen)) shaidyMapGen <- Sys.getenv("SHAIDYMAPGEN")
  if (shaidyMapGen == "") {
    checkForNGCHMSupportFiles()
    stop("Missing required path to ShaidyMapGen.jar file.")
  }
  if (missing(shaidyMapGenJava)) shaidyMapGenJava <- Sys.getenv("SHAIDYMAPGENJAVA")
  if (shaidyMapGenJava == "") shaidyMapGenJava <- "java"
  if (!checkForJavaVersion(shaidyMapGenJava)) {
    stop("Missing required java version.")
  }
  if (missing(shaidyMapGenArgs)) shaidyMapGenArgs <- strsplit(Sys.getenv("SHAIDYMAPGENARGS"), ",")[[1]]

  if (length(chmProperty(chm, "chm.info.build.time")) == 0) {
    chm@format <- "shaidy"
    chmProperty(chm, "chm.info.build.time") <- format(Sys.time(), "%F %H:%M:%S")
    chm <- chmMake(chm)
  }

  shaidyRepo <- ngchm.env$tmpShaidy
  shaid <- shaidyGetShaid(chm)

  pdfpath <- shaidyRepo$blob.path("viewer", shaid@value, chm@name, paste(chm@name, ".pdf", sep = ""))
  if (!file.exists(pdfpath)) {
    status <- system2(shaidyMapGenJava,
      c(shaidyMapGenArgs, "-jar", shaidyMapGen, shaidyRepo$basepath, shaid@value, shaid@value),
      env = c("DISPLAY=''")) # Set DISPLAY to empty string to prevent X11 errors in Rstudio Docker container
    if (status != 0 || !file.exists(pdfpath)) stop("export to pdf failed")
  }

  if (!file.copy(pdfpath, filename, TRUE)) {
    stop("export to pdf failed")
  }
  filename
}
#' Export a standalone HTML containing the NGCHM to a file.
#'
#' Create a standalone HTML containing the NGCHM in the specified file.
#' This function requires **Java 11** and the
#' **[NGCHMSupportFiles](https://github.com/MD-Anderson-Bioinformatics/NGCHMSupportFiles)** package.
#'
#' The NGCHMSupportFiles package can be installed from the R-universe repository: \cr\cr
#' \code{install.packages('NGCHMDemoData', } \cr
#' \code{repos = c('https://md-anderson-bioinformatics.r-universe.dev',} \cr
#' \code{'https://cloud.r-project.org'))}
#'
#' @export
#' @rdname chmExportToHTML-method
#'
#' @param chm The NGCHM to generate the PDF for
#' @param filename The file in which to save the PDF
#' @param overwrite Overwrite file iff true (default false)
#' @param shaidyMapGen Path to shaidyMapGen jar file (default to value of environment variable SHAIDYMAPGEN)
#' @param shaidyMapGenJava Path to java executable with which to run shaidyMapGen (default to value of environment variable SHAIDYMAPGENJAVA or java)
#' @param shaidyMapGenArgs Additional arguments to pass to java when running shaidyMapGen (default to value of environment variable SHAIDYMAPGENARGS)
#' @param ngchmWidgetPath Path to location of ngchm Widget (ngchmWidget-min.js). Defaults to environment variable NGCHMWIDGETPATH.
#'
#' @return filename
chmExportToHTML <- function(chm, filename, overwrite = FALSE, shaidyMapGen, shaidyMapGenJava, shaidyMapGenArgs, ngchmWidgetPath) {
  if (!overwrite && file.exists(filename)) stop("'filename' already exists")
  if (missing(shaidyMapGen)) shaidyMapGen <- Sys.getenv("SHAIDYMAPGEN")
  if (shaidyMapGen == "") {
    checkForNGCHMSupportFiles()
    stop("Missing required path to ShaidyMapGen.jar file.")
  }
  if (missing(shaidyMapGenJava)) shaidyMapGenJava <- Sys.getenv("SHAIDYMAPGENJAVA")
  if (shaidyMapGenJava == "") shaidyMapGenJava <- "java"
  if (!checkForJavaVersion(shaidyMapGenJava)) {
    stop("Missing required java version.")
  }
  if (missing(shaidyMapGenArgs)) shaidyMapGenArgs <- strsplit(Sys.getenv("SHAIDYMAPGENARGS"), ",")[[1]]
  if (missing(ngchmWidgetPath)) ngchmWidgetPath <- Sys.getenv("NGCHMWIDGETPATH")
  if (ngchmWidgetPath == "") {
    checkForNGCHMSupportFiles()
    stop("Missing required path to ngchmWidget-min.js file.")
  }

  if (length(chmProperty(chm, "chm.info.build.time")) == 0) {
    chm@format <- "shaidy"
    chmProperty(chm, "chm.info.build.time") <- format(Sys.time(), "%F %H:%M:%S")
    chm <- chmMake(chm)
  }

  shaidyRepo <- ngchm.env$tmpShaidy
  shaid <- shaidyGetShaid(chm)

  htmlpath <- shaidyRepo$blob.path("viewer", shaid@value, chm@name, paste(chm@name, ".html", sep = ""))
  if (!file.exists(htmlpath)) {
    status <- system2(shaidyMapGenJava,
      c(shaidyMapGenArgs, "-jar", shaidyMapGen, shaidyRepo$basepath, shaid@value, shaid@value, "NO_PDF", "-HTML"),
      env = c("DISPLAY=''")) # Set DISPLAY to empty string to prevent X11 errors in Rstudio Docker container
    if (status != 0 || !file.exists(htmlpath)) stop("export to html failed")
  }

  if (!file.copy(htmlpath, filename, TRUE)) {
    stop("export to html failed")
  }
  filename
}
writeBinLines <- function(text, con) {
  openit <- is.character(con)
  if (openit) con <- file(con, "wb")
  writeLines(text, con, "\n")
  if (openit) close(con)
}

#' Add TSNE coordinates to an NG-CHM.
#'
#' Add TSNE coordinates as hidden covariate bars to an axis of an NG-CHM.  One hidden
#' covariate bar is added for each TSNE coordinate.  Coordinates have names 'BASENAME.coordinate.N',
#' where BASENAME is specified by the parameter basename (default TSNE) and N ranges from 1 to the number of
#' added covariate bars.
#'
#' pointIds is required because [Rtsne::Rtsne()](https://CRAN.R-project.org/package=Rtsne) does not preserve the rownames of the data matrix it was applied to.
#' Their values must match those on that axis of the NGCHM, but their order must match those in the data
#' matrix passed to [Rtsne::Rtsne()](https://CRAN.R-project.org/package=Rtsne).
#'
#' @examples
#' # Examples using  `chmNew()` require git to be installed.
#' \dontrun{
#'   # If the NGCHMDemoData package is installed, use it to demo usage
#'   if (requireNamespace("NGCHMDemoData", quietly = TRUE)) {
#'     data(TCGA.GBM.EXPR, package = "NGCHMDemoData")
#'     mat <- TCGA.GBM.EXPR[1:10, 1:10]
#'     rtc <- Rtsne::Rtsne(t(mat), check_duplicates = FALSE, perplexity = 3)
#'     hm <- chmNew("gbm", mat)
#'     hm <- chmAddTSNE(hm, "column", rtc, colnames(mat))
#'   }
#'   # Small example not requiring NGCHMDemoData
#'   matrix <- matrix(rnorm(100),
#'     nrow = 10, ncol = 10,
#'     dimnames = list(paste0("r", 1:10), paste0("c", 1:10))
#'   )
#'   rtc <- Rtsne::Rtsne(t(matrix), check_duplicates = FALSE, perplexity = 3)
#'   hm <- chmNew("Demo TSNE", matrix)
#'   hm <- chmAddTSNE(hm, "column", rtc, colnames(matrix))
#' }
#' 
#' @export
#'
#' @param hm The NGCHM to add the coordinates to
#' @param axis The NGCHM axis ("row" or "column") to add the coordinates to
#' @param tsne TSNE coordinates (output of [Rtsne::Rtsne()](https://CRAN.R-project.org/package=Rtsne)) for the specified NGCHM axis
#' @param pointIds The NGCHM names for the data points in tsne
#' @param basename The prefix to use for the coordinate names.
#'
#' @return The NGCHM with added coordinates.
#' @seealso [chmAddPCA()]
#' @seealso [chmAddUMAP()]
#' @seealso [chmAddUWOT()]
#' @seealso [chmAddReducedDim()]

chmAddTSNE <- function(hm, axis, tsne, pointIds, basename = "TSNE") {
  stopifnot(is(hm, "ngchmVersion2"))
  stopifnot(mode(axis) == "character" && length(axis) == 1)
  stopifnot(axis == "row" || axis == "column")
  stopifnot(mode(pointIds) == "character" && length(pointIds) == nrow(tsne$Y))
  stopifnot(mode(basename) == "character" && length(basename) == 1)

  for (idx in 1:ncol(tsne$Y)) {
    coordname <- sprintf("%s.coordinate.%d", basename, idx)
    vals <- tsne$Y[, idx]
    names(vals) <- pointIds
    minv <- min(vals, na.rm = TRUE)
    maxv <- max(vals, na.rm = TRUE)
    midv <- if (minv * maxv < 0) 0.0 else (minv + maxv) / 2.0
    cmap <- chmNewColorMap(c(minv, midv, maxv), colors = c("#00007f", "#d0d0d0", "#7f0000"))
    cv <- chmNewCovariate(coordname, vals, cmap)
    hm <- chmAddCovariateBar(hm, axis, cv, display = "hidden")
  }
  return(hm)
}

#' Add PCA coordinates to an NG-CHM.
#'
#' Add PCA coordinates as hidden covariate bars to an axis of an NG-CHM.
#' One hidden covariate bar is added for each PCA coordinate (up to ndim coordinates).
#' Coordinates are given names 'BASENAME.coordinate.N', where BASENAME is specified by the
#' parameter basename (default "PC") and N ranges from 1 to the number of added covariate bars.
#'
#' @examples
#' # Examples using `chmNew()` require git to be installed.
#' \dontrun{
#'   # If the NGCHMDemoData package is installed, use it to demo usage
#'   if (requireNamespace("NGCHMDemoData", quietly = TRUE)) {
#'    data(TCGA.GBM.EXPR, package = "NGCHMDemoData")
#'     prc <- prcomp(TCGA.GBM.EXPR[1:50, 1:50])
#'     hm <- chmNew("gbm", TCGA.GBM.EXPR[1:50, 1:50])
#'     hm <- chmAddPCA(hm, "column", prc)
#'   }
#'   # Small example not requiring NGCHMDemoData
#'   matrix <- matrix(rnorm(100),
#'     nrow = 10, ncol = 10,
#'     dimnames = list(paste0("r", 1:10), paste0("c", 1:10))
#'   )
#'   prc <- prcomp(matrix)
#'   hm <- chmNew("Demo PCA", matrix)
#'   hm <- chmAddPCA(hm, "column", prc)
#' }
#' @export
#'
#' @param hm The NGCHM to add the coordinates to.
#' @param axis The NGCHM axis ("row" or "column") to add the coordinates to.
#' @param prc Principal component coordinates (output of [stats::prcomp()]) for the specified NGCHM axis.
#' @param basename The prefix to use for the coordinate names.
#' @param ndim The maximum number of coordinates to add.
#'
#' @return The NGCHM with added coordinates.
#' @seealso [chmAddTSNE()]
#' @seealso [chmAddUMAP()]
#' @seealso [chmAddUWOT()]
#' @seealso [chmAddReducedDim()]

chmAddPCA <- function(hm, axis, prc, basename = "PC", ndim = 2) {
  stopifnot(is(hm, "ngchmVersion2"))
  stopifnot(mode(axis) == "character" && length(axis) == 1)
  stopifnot(axis == "row" || axis == "column")
  stopifnot(mode(basename) == "character" && length(basename) == 1)
  stopifnot(mode(ndim) == "numeric" && length(basename) == 1)

  pointIds <- rownames(prc$rotation)
  for (idx in 1:min(ncol(prc$rotation), ndim)) {
    coordname <- sprintf("%s.coordinate.%d", basename, idx)
    vals <- prc$rotation[, idx]
    names(vals) <- pointIds
    minv <- min(vals, na.rm = TRUE)
    maxv <- max(vals, na.rm = TRUE)
    midv <- if (minv * maxv < 0) 0.0 else (minv + maxv) / 2.0
    cmap <- chmNewColorMap(c(minv, midv, maxv), colors = c("#00007f", "#d0d0d0", "#7f0000"))
    cv <- chmNewCovariate(coordname, vals, cmap)
    hm <- chmAddCovariateBar(hm, axis, cv, display = "hidden")
  }
  return(hm)
}

#' Add UMAP coordinates to an NG-CHM.
#'
#' Add UMAP coordinates as hidden covariate bars to an axis of an NG-CHM.  One hidden
#' covariate bar is added for each UMAP coordinate.  Coordinates have names 'BASENAME.coordinate.N',
#' where BASENAME is specified by the parameter basename (default UMAP) and N ranges from 1 to the number of
#' added covariate bars.
#'
#' @examples
#' # Examples using `chmNew()` require git to be installed.
#' \dontrun{
#'   # If the NGCHMDemoData package is installed, use it to demo usage
#'   if (requireNamespace("NGCHMDemoData", quietly = TRUE)) {
#'     data(TCGA.GBM.EXPR, package = "NGCHMDemoData")
#'     mat <- TCGA.GBM.EXPR[1:50, 1:50]
#'     umc <- umap::umap(t(mat))
#'     hm <- chmNew("gbm", mat)
#'     hm <- chmAddUMAP(hm, "column", umc)
#'   }
#'   # Small example not requiring NGCHMDemoData
#'   matrix <- matrix(rnorm(100),
#'     nrow = 10, ncol = 10,
#'     dimnames = list(paste0("r", 1:10), paste0("c", 1:10))
#'   )
#'   umc <- umap::umap(t(matrix), n_neighbors = 8)
#'   hm <- chmNew("Demo UMAP", matrix)
#'   hm <- chmAddUMAP(hm, "column", umc)
#' }
#' 
#' @export
#'
#' @param hm The NGCHM to add the coordinates to.
#' @param axis The NGCHM axis ("row" or "column") to add the coordinates to.
#' @param umap TSNE coordinates (output of [umap::umap()](https://CRAN.R-project.org/package=umap)) for the specified NGCHM axis.
#' @param basename The prefix to use for the coordinate names.
#'
#' @return The NGCHM with added coordinates.
#' @seealso [chmAddPCA()]
#' @seealso [chmAddTSNE()]
#' @seealso [chmAddUWOT()]
#' @seealso [chmAddReducedDim()]

chmAddUMAP <- function(hm, axis, umap, basename = "UMAP") {
  stopifnot(is(hm, "ngchmVersion2"))
  stopifnot(mode(axis) == "character" && length(axis) == 1)
  stopifnot(axis == "row" || axis == "column")
  stopifnot(mode(basename) == "character" && length(basename) == 1)

  pointIds <- rownames(umap$layout)
  for (idx in 1:ncol(umap$layout)) {
    coordname <- sprintf("%s.coordinate.%d", basename, idx)
    vals <- umap$layout[, idx]
    names(vals) <- pointIds
    minv <- min(vals, na.rm = TRUE)
    maxv <- max(vals, na.rm = TRUE)
    midv <- if (minv * maxv < 0) 0.0 else (minv + maxv) / 2.0
    cmap <- chmNewColorMap(c(minv, midv, maxv), colors = c("#00007f", "#d0d0d0", "#7f0000"))
    cv <- chmNewCovariate(coordname, vals, cmap)
    hm <- chmAddCovariateBar(hm, axis, cv, display = "hidden")
  }
  return(hm)
}

#' Add UWOT::UMAP coordinates to an NG-CHM.
#'
#' Add UWOT::UMAP coordinates as hidden covariate bars to an axis of an NG-CHM.  One hidden
#' covariate bar is added for each UMAP coordinate.  Coordinates have names 'BASENAME.coordinate.N',
#' where BASENAME is specified by the parameter basename (default UMAP) and N ranges from 1 to the number of
#' added covariate bars.
#'
#' pointIds is required because [uwot::umap()](https://CRAN.R-project.org/package=uwot) does not preserve the rownames of the data matrix it was applied to.
#' Their values must match those on that axis of the NGCHM, but their order must match those in the data
#' matrix passed to [uwot::umap()](https://CRAN.R-project.org/package=uwot).
#'
#' @examples
#' # Examples using `chmNew()` require git to be installed.
#' \dontrun{
#'   # If the NGCHMDemoData package is installed, use it to demo usage
#'   if (requireNamespace("NGCHMDemoData", quietly = TRUE)) {
#'     data(TCGA.GBM.EXPR, package = "NGCHMDemoData")
#'     umc <- uwot::umap(t(TCGA.GBM.EXPR[1:50, 1:50]))
#'     hm <- chmNew("gbm", TCGA.GBM.EXPR[1:50, 1:50])
#'     hm <- chmAddUWOT(hm, "column", umc, colnames(TCGA.GBM.EXPR[1:50, 1:50]))
#'   }
#'   # Small example not requiring NGCHMDemoData
#'   matrix <- matrix(rnorm(100),
#'     nrow = 10, ncol = 10,
#'     dimnames = list(paste0("r", 1:10), paste0("c", 1:10))
#'   )
#'   umc <- uwot::umap(t(matrix), n_neighbors = 8)
#'   hm <- chmNew("Demo UMAP", matrix)
#'   hm <- chmAddUWOT(hm, "column", umc, colnames(matrix))
#' }
#' 
#' @export
#'
#' @param hm The NGCHM to add the coordinates to.
#' @param axis The NGCHM axis ("row" or "column") to add the coordinates to.
#' @param uwot UMAP coordinates (output of [uwot::umap()](https://CRAN.R-project.org/package=uwot)) for the specified NGCHM axis.
#' @param pointIds The NGCHM names for the data points in uwot
#' @param basename The prefix to use for the coordinate names.
#'
#' @return The NGCHM with added coordinates.
#' @seealso [chmAddPCA()]
#' @seealso [chmAddTSNE()]
#' @seealso [chmAddUMAP()]
#' @seealso [chmAddReducedDim()]

chmAddUWOT <- function(hm, axis, uwot, pointIds, basename = "UMAP") {
  stopifnot(is(hm, "ngchmVersion2"))
  stopifnot(mode(axis) == "character" && length(axis) == 1)
  stopifnot(axis == "row" || axis == "column")
  stopifnot(mode(pointIds) == "character" && length(pointIds) == nrow(uwot))
  stopifnot(mode(basename) == "character" && length(basename) == 1)

  for (idx in 1:ncol(uwot)) {
    coordname <- sprintf("%s.coordinate.%d", basename, idx)
    vals <- uwot[, idx]
    names(vals) <- pointIds
    minv <- min(vals, na.rm = TRUE)
    maxv <- max(vals, na.rm = TRUE)
    midv <- if (minv * maxv < 0) 0.0 else (minv + maxv) / 2.0
    cmap <- chmNewColorMap(c(minv, midv, maxv), colors = c("#00007f", "#d0d0d0", "#7f0000"))
    cv <- chmNewCovariate(coordname, vals, cmap)
    hm <- chmAddCovariateBar(hm, axis, cv, display = "hidden")
  }
  return(hm)
}
#' Generic method to get a dimensions matrix from obj.
#'
#' The return value must be NULL or a numeric matrix, each column of which is a (reduced) dimension.
#' The rows of the returned matrix must be named.
#'
#' @name getDimensions
#' @rdname getDimensions-method
#' @param obj The object from which to obtain the dimension(s).
#' @param ... Additional class-specific parameters for specifying the desired dimension.
#' @return A matrix with one dimension per column and one named row per observation in obj.
#' @export
#'
#' @seealso [chmAddReducedDim()]
#'
getDimensions <- function(obj, ...) {
  UseMethod("getDimensions", obj)
}
#' @rdname getDimensions-method
#' @export
getDimensions.default <- function(obj, ...) {
  return(NULL)
}
#' Add reduced dimension coordinates to an NG-CHM.
#'
#' Add (reduced) dimension coordinates from an object obj
#' as hidden covariate bars to an axis of an NG-CHM.  Depending on the object type, dimName and dimAxis
#' can be used to specify the name of the dimension of interest in obj.
#'
#' One hidden covariate bar is added for each coordinate obtained from `obj`.
#' If specified, maxDim limits the maximum number of covariate bars added to the chm.
#'
#' Coordinates have names 'BASENAME.coordinate.N', where BASENAME is specified by the parameter
#' basename (defaults to dimName if omitted) and N ranges from 1 to the number of added covariate bars.
#'
#' `obj` can be a numeric matrix, each column of which is a (reduced) dimension.  In this case, dimName and dimAxis
#' are not used for obtaining the reduced dimension.  The number of rows of the matrix must equal the size of the specified
#' NGCHM axis and each row of the matrix must be uniquely named using the names from that axis of the NG-CHM.
#'
#' `obj` can also be an instance of class className if there exists an S3 method getDimensions.className.
#' The method takes the object as its first parameter and up to two optional parameters, dimName and dimAxis,
#' that can be used to specify the desired dimension.  The method's return value is a matrix similar to
#' the one described in the preceding paragraph.  This package defines methods for classes `prcomp` and `umap`.
#'
#' @examples
#' # Examples using `chmNew()` require git to be installed.
#' \dontrun{
#'   if (requireNamespace("NGCHMDemoData", quietly = TRUE)) {
#'     data(TCGA.GBM.EXPR, package = "NGCHMDemoData")
#'     mat <- TCGA.GBM.EXPR[1:10, 1:10]
#'     prc <- prcomp(mat)
#'     hm <- chmNew("Demo reduced dimension coordinates", mat)
#'     hm <- chmAddReducedDim(hm, "column", prc, "PCA", 3, "PC")
#'     umc <- umap::umap(t(mat), n_neighbors = 8)
#'     hm <- chmAddReducedDim(hm, "column", umc, "UMAP")
#'   }
#'   # Small example not requiring NGCHMDemoData
#'   matrix <- matrix(rnorm(100),
#'     nrow = 10, ncol = 10,
#'     dimnames = list(paste0("r", 1:10), paste0("c", 1:10))
#'   )
#'   prc <- prcomp(matrix)
#'   hm <- chmNew("Demo reduced dimension coordinates", matrix)
#'   hm <- chmAddReducedDim(hm, "column", prc, "PCA", 3, "PC")
#'   umc <- umap::umap(t(matrix), n_neighbors = 8)
#'   hm <- chmAddReducedDim(hm, "column", umc, "UMAP")
#' }
#'
#' @export
#'
#' @param hm The NGCHM to add the coordinates to.
#' @param axis The NGCHM axis ("row" or "column") to add the coordinates to.
#' @param obj An object containing the (reduced) dimension.
#' @param dimName The name of the (reduced) dimension to create covariate bars for.
#' @param maxDim The maximum number of coordinates to add (default all).
#' @param basename The prefix to use for the coordinate names (defaults to dimName).
#' @param dimAxis The axis on obj containing the named dimension, if applicable.
#'
#' @return The NGCHM with added coordinates.
#' @seealso [chmAddPCA()]
#' @seealso [chmAddTSNE()]
#' @seealso [chmAddUMAP()]
#' @seealso [chmAddUWOT()]
#' @seealso [getDimensions()]

chmAddReducedDim <- function(hm, axis, obj, dimName, maxDim, basename, dimAxis) {
  stopifnot(is(hm, "ngchmVersion2"))
  stopifnot(is(axis, "character") && length(axis) == 1)

  # Initially try to use generic getDimension method.
  layout <- getDimensions(obj, dimName, dimAxis)
  # Try hard to find an applicable reducedDim method
  if (is.null(layout)) {
    pkg <- attr(class(obj), "package")
    if (length(pkg) != 0) {
      ns <- tryCatch(loadNamespace(pkg), error = function(e) e)
      if (!is(ns, "error") && exists("reducedDim", ns)) {
        if (missing(dimAxis)) {
          layout <- get("reducedDim", ns)(obj, dimName)
        } else {
          layout <- get("reducedDim", ns)(obj, dimName, axis = dimAxis)
        }
      }
    }
  }
  if (is.null(layout)) {
    # Last chance: try .GlobalEnv
    if (exists("reducedDim")) {
      if (missing(dimAxis)) {
        layout <- get("reducedDim")(obj, dimName)
      } else {
        layout <- get("reducedDim")(obj, dimName, axis = dimAxis)
      }
    } else if (is(obj, "matrix")) {
      layout <- obj
    } else {
      stop("Unable to get reduced dimension from object")
    }
  }
  stopifnot(is(layout, "matrix"))
  if (missing(maxDim)) {
    maxDim <- ncol(layout)
  } else {
    stopifnot(maxDim <= ncol(layout))
  }
  if (missing(basename)) basename <- dimName
  for (idx in 1:maxDim) {
    coordname <- sprintf("%s.coordinate.%d", basename, idx)
    vals <- layout[, idx]
    minv <- min(vals, na.rm = TRUE)
    maxv <- max(vals, na.rm = TRUE)
    midv <- if (minv * maxv < 0) 0 else (minv + maxv) / 2.0
    cmap <- chmNewColorMap(c(minv, midv, maxv), colors = c("#00007f", "#d0d0d0", "#7f0000"))
    cv <- chmNewCovariate(coordname, vals, cmap)
    hm <- chmAddCovariateBar(hm, axis, cv, display = "hidden")
  }
  return(hm)
}
#' @rdname getDimensions-method
#' @aliases getDimensions,prcomp
#' @export
getDimensions.prcomp <- function(obj, ...) {
  return(obj$rotation)
}
#' @rdname getDimensions-method
#' @aliases getDimensions,umap
#' @export
getDimensions.umap <- function(obj, ...) {
  return(obj$layout)
}
#' @rdname getDimensions-method
#' @aliases getDimensions,Seurat
#' @param dimName The name of the dimension matrix to obtain.
#' @export
getDimensions.Seurat <- function(obj, dimName, ...) {
  return(obj@reductions[[dimName]]@cell.embeddings)
}
#' Helper function to cast variables as integers.
#'
#' If variable value is far from integer, print error message and stop.
#'
#' @param variableToCast Variable to cast as integer
#' @return integer value of variableToCast
castAsInteger <- function(variableToCast) {
  roundTolerance <- 0.01
  if (abs(round(variableToCast) - variableToCast) > roundTolerance) {
    log_error("Variable '", deparse(substitute(variableToCast)), "' must be integer")
    stop("Variable '", deparse(substitute(variableToCast)), "' must be integer")
  } else {
    return(as.integer(round(variableToCast)))
  }
}

#' Helper function to cast list as integer
#'
#' If variable value is far from integer, print error message and stop.
#'
#' @param listToCast List to cast as integer
#' @return list with members cast to integers
castListAsInteger <- function(listToCast) {
  roundTolerance <- 0.01
  lapply(listToCast, function(elem) {
    if ((abs(round(elem) - elem)) > roundTolerance) {
      log_error("Entries of '", deparse(substitute(listToCast)), "' must be integer")
      stop("Entries of '", deparse(substitute(listToCast)), "' must be integer")
    }
  })
  return(as.integer(round(listToCast)))
}

#' Helper function to verify if variable is numeric.
#'
#' If not numeric, print error message and stop.
#'
#' @param variableToCheck The variable to check for being numeric.
#' @return TRUE
verifyNumeric <- function(variableToCheck) {
  if (!is.numeric(variableToCheck)) {
    log_error("Variable '", deparse(substitute(variableToCheck)), "' must be numeric.")
    stop("Variable '", deparse(substitute(variableToCheck)), "' must be numeric.")
  } else {
    return(TRUE)
  }
}

#' Creates new treeCuts object
#'
#' This function was designed to facilitate setting rowGapLocations and colGapLocations in the
#' [chmNew()] function. See examples section.
#'
#' @param numberOfCuts Number of tree cuts
#' @return [treeCuts-class] object with specified number of tree cuts
#' @export
#'
#' @examples
#' mychm <- chmNew("test_chm", rowGapLocations = chmTreeGaps(5))
chmTreeGaps <- function(numberOfCuts) {
  return(new(Class = "treeCuts", numberOfCuts = as.integer(numberOfCuts)))
}
MD-Anderson-Bioinformatics/NGCHM-R documentation built on April 1, 2024, 12:34 p.m.