R/save_map.R

Defines functions save_map

Documented in save_map

#' Save maps to disk
#'
#' Save a map to disk intended to be part of a as a still image sequence of one of four types: networks, tiles, lines, or polygons.
#'
#' @details
#' \code{save_map} takes a specific type of data frame catering to networks, tiles, lines, or polygons.
#' It plots a 3D globe map with \code{ortho=TRUE} (default) or a flat map (\code{ortho=FALSE}).
#' For flat maps, \code{lon}, \code{lat}, \code{n.period}, \code{n.frames}, and \code{rotation.axis} are ignored.
#' For plotting on a globe, \code{lon} and \code{lat} are used to describe the field of view or the visible hemisphere.
#' \code{n.period} relates is eithe the period of rotation of the globe or the length of the non-repeating, arbitrary coordinates sequence.
#' \code{n.frames} is always the explicit number of frames that will make up an animation
#' regardless of the length of the series of data frames \code{data} to be plotted or the length of the rotational period or coordinates sequence.
#'
#' \subsection{Map Type Options}{
#' If you are familiar with \code{ggplot2}, it may help to think of \code{points} as making use of \code{geom_point};
#' \code{maplines} uses \code{geom_path}; \code{polygons} uses \code{geom_polygon} and \code{geom_path};
#' \code{maptiles} uses \code{geom_tile}; \code{density} uses \code{stat_density2d} or \code{stat_contour}; and \code{"network"} combines \code{geom_path} and \code{geom_point}.
#' \code{maptiles} is also a specific case of \code{density}.
#' }
#'
#' \subsection{Location Only Vs. Location Plus Data}{
#' Maps are based on either two variables (location only) or on three variables (longitude, latitude, plus a data variable).
#' The former are generated by the map type options \code{points}, \code{maplines}, and \code{network}.
#' The latter are generated by \code{polygons} and \code{maptiles}. \code{density} may apply to either case.
#'
#' \code{z.name} is relevant only for fill color when drawing tiles, polygons, or densities.
#' \code{z.range} is important for these plot types that use a data variable in addition to longitude and latitude
#' because it is used to ensure colors are mapped to data values consistently across all plots.
#' This is not only for the case of changing data values across a series of plots of different data frames \code{data}.
#' There are also changes in the range of values for a fixed data frame when it is plotted repeatedly as the globe is rotated and different hemispheres of the map
#' (different data subsets) are in view across the image sequence. \code{z.range} will default to the range of the given \code{data} if not provided.
#' }
#'
#' \subsection{Color Specification}{
#' The \code{color} argument is used differently depending on \code{type}.
#' For \code{points} and \code{maplines} it is a single color. Additional colors in a vector are ignored. For other plot types it must be a vector.
#' \code{maptiles} and \code{polygons} require a vector of at least two colors to produce a palette for their color gradient.
#' \code{density} may also require this but only for contour fill color gradient, which is relevant only when a data variable is specified by \code{z.name}.
#'
#' \code{network} is a special case which assumes four colors in the following order: background line, foreground line, background point, foreground point.
#' The four colors are layered in the plot in this order. Semi-transparent colors can work well in this context. Additional colors are ignored.
#' If \code{col=NULL} (default) sensible default colors are provided for each plot type.
#'
#' For contour lines, overlays on density maps are black by default but when layered with points \code{col} applies to both points and contour lines.
#' In the current package version defaul black for density is only overridden by the \code{col} argument when \code{contour="only"}.
#' The first color is used and any additional colors in \code{col} are ignored, unless \code{contour="only"} and \code{type="density"} when \code{z.name} is also provided.
#' In this case, the density map represents a contour map based on all three variables
#' and since the contour is not filled (only contour lines; no overlay on a filled contour) the color palette defined by a \code{col} vector is allowed to apply to the contour lines themselves.
#' }
#'
#' \subsection{Density And Contour}{
#' \code{type="density"} is a special map type setting in that density maps can be based on either position only (longitude and latitude) or on position plus data at position.
#' The name "density" is more appropriate for the former, for example a desnity plot of x and y data. For the latter, using three variables x, y and z,
#' it would be more accurate to refer to the default plot as a filled contour plot.
#' The visual effect is the same so it is not split into two distinct \code{type} selections.
#' Instead, the map is drawn using either \code{ggplot2::stat_density2d} or \code{ggplot2::stat_contour} depending on whether the user provides \code{z.name} or leaves it as \code{NULL}.
#' A separate \code{contour} argument is reserved for the option to overlay explicit contour lines (no color fill).
#'
#' The \code{contour} argument is for optionally displaying contour lines on a map. By default, no contour lines are drawn.
#' \code{contour="overlay"} will overlay contour lines on another plotting layer and \code{contour="only"} will plot only the contour lines in place of the default layer.
#' Contour lines are only available for specific plot types: \code{type="points"} and \code{type="density"}.
#' Since points are based on \code{lon} and \code{lat} data only, contour lines are similarly drawn based on \code{lon} and \code{lat} using \code{ggplot2::stat_density2d}.
#' On the other hand, density maps can be based on either two or three variables.
#' 2D densities are drawn as with \code{type="points"}.
#' When three variables are provided  (\code{lon}, \code{lat}, and \code{z.name}), contour lines are drawn using \code{ggplot2::stat_contour}.
#' \code{contour} is ignored for any other \code{type}.
#'
#' Density maps can be generated using polygons or tiles (rasterized data).
#' \code{density.geom="tile"} is the default because although it may require more processing power and time to generate map outputs,
#' tiles generally provides higher quality output with sufficient spatial resolution of the input data.
#' \code{density.geom="polygon"} may be quicker to render but may provide visually disappointing maps with severe clipping, especially in the orthographic projection, depending on the input data.
#'
#' This is analogous to the trade off between using \code{type="polygons"} at all vs. electing to rasterize the input data up front and use it with \code{type="maptiles"} instead.
#' In fact, \code{type="maptiles"} is a special case of \code{type="density"}; the one which will generally work best, and without the superimposed contour lines.
#' See the introductory vignette for examples: \code{browseVignettes(package=="mapmate")}
#'
#' For these reasons, the \code{polygons} map type and the \code{density.geom="polygon"} option for the \code{density} map type
#' are potentially useful only if the intent is to eventually zoom in on an area of the globe where no artifacts are visible.
#' Otherwise these settings serve largely to illustrate their limitations
#' and it is still best to use \code{type="maptiles"} or the \code{density.geom="tile"} option with \code{type="density"}
#' if the goal is to achieve artifact-free 3D global plots viewed from any arbitrary persepctive.
#' Depending on the input data, this may be necessary even for flat maps.
#'
#' The costs to avoiding these problems are (1) much greater processing time with increasing resolution of the input data in order to make map pixels satisfactorily small
#' and (2) the computing resources necessary to run it as well as have it run satisfactorily quickly.
#'
#' Finally, drawing polygons and contour lines may yield superior visual results when working with three variables
#' because this means there potentially can be data values associated uniformly with every spot on the surface of the globe,
#' whereas density or intensity maps made from only the spatial locations of points themselves may be more likely to reveal clipping issues when projected.
#' }
#'
#' \subsection{Other Details}{
#' The png output directory will be created if it does not exist, recursively if necessary. The default is the working directory.
#' This is ignored if \code{save.plot=FALSE}.
#'
#' \code{type="polygons"} is only recommended for use with flat maps, not the orthographic projection. See the vignette for an example and description of the issue.
#' For globe plots it is best to rasterize polygons and use \code{type="maptiles"} for better results in exchange for increased processing time.
#' }
#'
#' @param data a data frame containing networks, tiles, lines or polygons information.
#' @param z.name character, the column name of the data (\code{z}) variable in \code{data}. Only needed for \code{type="maptiles"} and \code{type="polygons"}
#' @param z.range numeric vector, the full known range for the data values across all \code{data} objects, not just the current one, e.g. \code{c(0, 5)}.
#' @param id character, column name referring to column of \code{data} representing frame sequence integer IDs.
#' @param dir png output directory. Defaults to working directory.
#' @param lon starting longitude for rotation sequence or vector of arbitrary longitude sequence.
#' @param lat fixed latitude or vector of arbitrary latitude sequence.
#' @param n.period intended length of the period.
#' @param n.frames intended number of frames in animation.
#' @param ortho use an orthographic projection for globe plots. Defaults to \code{TRUE}.
#' @param col sensible default colors provided for each \code{type}
#' @param type the type of plot, one of \code{points}, \code{maplines}, \code{polygons}, \code{maptiles}, \code{density}, or \code{"network"}.
#' @param contour character, one of \code{none}, \code{overlay}, or \code{only}. Defaults to \code{none}. See details.
#' @param density.geom character, one of \code{tile} or \code{polygon}. Defaults to \code{tile}. See details.
#' @param xlim numeric vector, defaults to \code{(-180, 180)}. Will crop to range of longitudes in \code{data} if \code{NULL}.
#' @param ylim numeric vector, defaults to \code{(-90, 90)}. Will crop to range of latitudes in \code{data} if \code{NULL}.
#' @param pt.size numeric vector, applies only to \code{type="network"}. Point sizes follow same order as colors for networks. See details.
#' @param rotation.axis the rotation axis used when \code{ortho=TRUE} for globe plots. Defaults to 23.4 degrees.
#' @param file character, output filename pattern preceeding the image sequence numbering and file extension. Defaults to \code{"Rplot"}.
#' @param png.args a list of arguments passed to \code{png}. \code{bg} will still be used to color the plot bakground if \code{return.plot=TRUE}
#' so continue to pass \code{png.args} a background color when \code{bg} is not the default \code{transparent} even if \code{save.plot=FALSE}.
#' @param save.plot save the plot to disk. Defaults to \code{TRUE}. Typically only set to \code{FALSE} for demonstrations and testing.
#' @param return.plot return the ggplot object. Defaults to \code{FALSE}. Only intended for single-plot demonstrations and testing, not for still image sequence automation.
#' @param overwrite logical, overwrite existing files. Defaults to \code{FALSE}. If file exists and \code{return.plot=TRUE} the plot is still returned. Otherwise returns \code{NULL}.
#' This is a frame by frame check on each file. File writing is simply skipped for existing files when \code{overwrite=FALSE}. No error or warning is thrown.
#' @param num.format number of digits including any leading zeros for image sequence frame numbering. Defaults to 4, i.e. \code{0001, 0002, ...}.
#'
#' @return usually returns NULL after writing file to disk. May return a ggplot object with or without the file writing side effect.
#' @export
#'
#' @examples
#' # not run
#' \dontrun{
#' library(dplyr)
#' library(purrr)
#' library(RColorBrewer)
#'
#' data(annualtemps)
#' pal <- rev(brewer.pal(11,"RdYlBu"))
#'
#' temps <- mutate(annualtemps, frameID = Year - min(Year) + 1)
#' temps <- split(temps, temps$frameID)
#' rng <- range(annualtemps$z, na.rm=TRUE)
#' n <- length(unique(annualtemps$Year))
#' filename <- "annual_3D_rotating"
#'
#' # should specify a dir or set working dir for file output
#' # consider running over a smaller subset of frame IDs
#' walk(temps, ~save_map(.x, z.name="z", id="frameID", lon=-70, lat=50,
#'   n.period=30, n.frames=n, col=pal, type="maptiles", file=filename, z.range=rng))
#' }
save_map <- function(data, z.name=NULL, z.range=NULL, id, dir=".", lon=0, lat=0, # nolint start
                     n.period=360, n.frames=n.period, ortho=TRUE, col=NULL, type,
                     contour="none", density.geom="tile", xlim=c(-180, 180),
                     ylim=c(-90, 90), pt.size=c(1, 0.5, 1, 0.5), rotation.axis=23.4, file="Rplot",
                     png.args=list(width=1920, height=1080, res=300, bg="transparent"),
                     save.plot=TRUE, return.plot=FALSE, overwrite=FALSE, num.format=4){

  if(n.frames >= eval(parse(text=paste0("1e", num.format))))
    warning("'num.format' may be too small for sequential file numbering given the total number of files suggested by 'n.frames'.")
  if(missing(id)) stop("'id' column is missing.")
  if(!id %in% names(data)) stop("'id' must refer to a column name.")
  if(!overwrite & file.exists(file) & !return.plot) return(NULL)
  i <- data[[id]][1]
  lonlat <- get_lonlat_seq(lon, lat, n.period, n.frames)

  if(is.null(col)) col <- switch(type,
                                 points="black",
                                 network=c("#1E90FF25", "#FFFFFF25", "#FFFFFF", "#1E90FF50"),
                                 maptiles=c("black", "white"),
                                 maplines="black",
                                 polygons=c("royalblue", "purple", "orange", "yellow"),
                                 density=c("royalblue", "purple", "orange", "yellow"))

  z.types <- c("maptiles", "polygons")
  if(type %in% z.types && is.null(z.name)) stop("Must provide 'z.name'.")
  if(type %in% z.types && !z.name %in% names(data)) stop("'z' must refer to a column name.")
  if(type %in% z.types && is.null(z.range)) z.range <- range(data[[z.name]], na.rm=TRUE)

  if(type=="maptiles"){

    .colorStop(col, "map tiles")
    g <- ggplot2::ggplot(data, ggplot2::aes_string("lon", "lat", fill=z.name)) +
      ggplot2::geom_tile() + ggplot2::scale_fill_gradientn(colors=col, limits=z.range)

  } else if(type=="polygons"){

    .colorStop(col, "polygon fill")
    g <- ggplot2::ggplot(data, ggplot2::aes_string("lon", "lat", group="group", fill=z.name)) +
      ggplot2::geom_polygon() + ggplot2::geom_path(color="white") +
      ggplot2::scale_fill_gradientn(colours=col, limits=z.range)

  } else if(type=="points"){

    col <- col[1]
    if(!is.null(z.name)) warning("'z.name' variable is ignored for point data (type='points').")
    g <- ggplot2::ggplot(data, ggplot2::aes_string("lon", "lat"))
    if(contour=="only") g <- g + ggplot2::stat_density2d(colour=col)
    if(contour=="overlay") g <- g + ggplot2::stat_density2d(colour=col)
    if(contour=="none" | contour=="overlay") g <- g + ggplot2::geom_point(colour=col, size=1)

  } else if(type=="density"){

    if(is.null(z.name)){
      g <- ggplot2::ggplot(data, ggplot2::aes_string("lon", "lat"))
      if(contour=="none" | contour=="overlay"){
        .colorStop(col, paste("(lon,lat) point density"))
        g <- g + ggplot2::scale_fill_gradientn(colours=col)
        if(density.geom=="polygon")
          g <- g + ggplot2::stat_density2d(geom="polygon", ggplot2::aes(fill = ..level..))
        if(density.geom=="tile")
          g <- g + ggplot2::stat_density2d(geom="tile", ggplot2::aes(fill = ..density..), contour = FALSE)
      }
      if(contour=="overlay") g <- g + ggplot2::geom_density2d(colour="black")
      if(contour=="only" & length(col)==1) g <- g + ggplot2::geom_density2d(colour=col)
      if(contour=="only" & length(col) > 1)
        g <- g + ggplot2::geom_density2d(ggplot2::aes(colour = ..level..)) +
          ggplot2::scale_colour_gradientn(colours=col)
    } else {
      if(!z.name %in% names(data)) stop("'z' must refer to a column name.")
      if(is.null(z.range)) z.range <- range(data[[z.name]], na.rm=TRUE)
      g <- ggplot2::ggplot(data, ggplot2::aes_string("lon", "lat", z=z.name))

      if(contour=="none" | contour=="overlay"){
        .colorStop(col, paste(z.name, "data density"))
        g <- g + ggplot2::scale_fill_gradientn(colours=col, limits=z.range)
        if(density.geom=="polygon")
          g <- g + ggplot2::stat_contour(geom="polygon", ggplot2::aes(fill = ..level..))
        if(density.geom=="tile") g <- g + ggplot2::geom_tile(ggplot2::aes_string(fill=z.name))

      }
      if(contour=="overlay"){
        g <- g + ggplot2::stat_contour(colour="black")
      }
      if(contour=="only" & length(col)==1) g <- g + ggplot2::stat_contour(colour=col)
      if(contour=="only" & length(col) > 1){
        g <- g + ggplot2::stat_contour(ggplot2::aes(colour = ..level..)) +
          ggplot2::scale_colour_gradientn(colours=col, limits=z.range)
      }
    }

  } else {

    g <- ggplot2::ggplot(data, ggplot2::aes_string("lon", "lat", group="group"))
    if(type=="maplines"){
      if(!is.null(z.name))
        warning("'z.name' variable is ignored for path lines (type='maplines').")
      g <- g + ggplot2::geom_path(colour=col[1])
    }
    if(type=="network"){
      if(!is.null(z.name))
        warning("'z.name' variable is ignored for moving line segments/great circle arcs (type='network').")
      data.lead <- dplyr::group_by(data, group) %>% dplyr::slice(n())
      size <- rep(pt.size, length=4)
      g <- g + ggplot2::geom_path(colour=col[1], size=size[1]) +
        ggplot2::geom_path(colour=col[2], size=size[2]) +
        ggplot2::geom_point(data=data.lead, colour=col[3], size=size[3]) +
        ggplot2::geom_point(data=data.lead, colour=col[4], size=size[4])
    }

  }

  if(is.null(png.args$bg)) png.args$bg <- "transparent"
  if(is.null(png.args$res)) png.args$res <- 300
  g <- g + .theme_blank(bg=png.args$bg)
  g <- g + ggplot2::scale_x_continuous(limits=xlim, expand=c(0, 0)) +
    ggplot2::scale_y_continuous(limits=ylim, expand=c(0, 0))
  if(ortho) g <- g + ggplot2::coord_map("ortho", orientation=c(lonlat$lat[i], lonlat$lon[i], rotation.axis))
  if(save.plot){
    dir.create(dir, recursive=TRUE, showWarnings=FALSE)
    file <- sprintf(paste0(dir, "/", file, "_%0", num.format, "d.png"), i)
    if(!overwrite & file.exists(file) & return.plot) return(g)
    do.call(png, c(filename=file, png.args))
    print(g)
    dev.off()
  }
  if(return.plot) return(g)
  NULL
}
# nolint end
leonawicz/mapmate documentation built on May 21, 2019, 5:09 a.m.