R/venndir-label-outside-jp.R

#' Position labels outside JamPolygon
#' 
#' Position labels outside JamPolygon
#' 
#' @family JamPolygon
#'
#' @param jp `JamPolygon`
#' @param which_jp `integer` or `NULL`; when `which_jp` contains one
#'    or more `integer` values, they refer to rows in `jp`, and
#'    each will be analyzed in sequence. When `which_jp=NULL` then
#'    all the polygons in `jp` will be analyzed in sequence.
#' @param center `numeric` vector or matrix with two values indicating
#'    the center position. When `center=NULL` then the center is
#'    determined using a method defined by `center_method`.
#' @param distance `numeric` value indicating the absolute coordinate
#'    distance away from the perimiter of `jp` to place labels.
#'    This value is the `buffer` for `buffer_JamPolygon()`.
#' @param center_method `character` string indicating the method to
#'    determine the `center`:
#'    * `"label"` uses the mean x,y coordinate
#'    of all the polygon label positions;
#'    * `"bbox"` uses the mean x,y
#'    coordinate of the bounding box that encompasses the polygons.
#'    
#'    The effect
#'    is to extend outer labels radially around this center point.
#'    Using the mean label position with `center_method="label"`
#'    is helpful because it ensures labels are extended in all directions
#'    even when most labels are in one upper or lower section of
#'    the `sp` polygons.
#' 
#' @export
label_outside_JamPolygon <- function
(jp,
 which_jp=NULL,
 center=NULL,
 buffer=0.1,
 center_method=c("bbox",
    "label"),
 vector_method=c("label",
    "farthest"),
 segment_method=c("nearest",
    "vector"),
 distance=0.1,
 min_degrees=15,
 relative=TRUE,
 seed=123,
 debug=FALSE,
 verbose=FALSE,
 ...)
{
   # validate input
   center_method <- match.arg(center_method);
   if (length(which_jp) == 0) {
      ## use only those entries with polygon coordinates
      # which_jp <- which(sapply(seq_len(nrow(jp@polygons)), function(ijp){
      #    length(jamba::rmNA(unlist(jp@polygons$x[[ijp]]))) > 0
      # }))
      which_jp <- seq_len(length(jp));
      # jamba::printDebug("which_jp: ", which_jp);# debug
   } else {
      # use only those entries with polygon coordinates
      ## 0.0.34.900 - do not subset which_jp so the function can always return
      ## coordinates in the order requested, and for all entries provided,
      ## even when an entry is empty
      # which_jp_sub <- sapply(which_jp, function(ijp){
      #    length(jamba::rmNA(unlist(jp@polygons$x[[ijp]]))) > 0
      # })
      # jamba::printDebug("table(which_jp_sub): ");print(table(which_jp_sub));# debug
      # which_jp <- which_jp[which_jp_sub];
      # jamba::printDebug("which_jp: ", which_jp);# debug
   }
   
   ## check for empty polygons among which_jp
   which_jp_is_empty <- sapply(which_jp, function(ijp){
      length(jamba::rmNA(unlist(jp@polygons$x[[ijp]]))) == 0
   })
   ## Todo: Check for all empty polygons, and return NA, NA if true
   
   # buffer
   if (length(buffer) == 0) {
      buffer <- 0;
   }
   buffer <- rep(buffer,
      length.out=length(which_jp));
   names(buffer) <- names(jp)[which_jp];

   # get bbox for the whole polygon
   jpbox <- bbox_JamPolygon(jp);
   
   # added tiny buffer around polygons before union
   # otherwise tiny slivers of holes remain, apparently
   # due to rounding errors in the polygon coordinates,
   # which cause adjacent polygons to be not-quite-adjacent.
   # Anyway, the tiny holes caused label_segment_JamPolygon()
   # to take 10-15 seconds per line, instead of being instant.
   jpb <- max(apply(jpbox, 1, diff)) / 100;
   jpall <- buffer_JamPolygon(
      union_JamPolygon(
         buffer_JamPolygon(jp,
            buffer=jpb,
            relative=FALSE)),
      buffer=jpb,
      relative=FALSE);
   # plot(jpall);# debug

   # expand the bounding box
   jpbox_ex <- jpbox;
   jpbox_ex[] <- t(apply(jpbox, 1, expand_range, 0.2));

   # distance (as fraction of bbox)
   bbox_max <- max(apply(jpbox, 1, diff));
   distance <- bbox_max * distance;
   distance <- rep(distance,
      length.out=length(which_jp));
   names(distance) <- names(jp)[which_jp];
   
   # get center
   set.seed(seed);
   # calculate plot center
   if ("bbox" %in% center_method || length(which_jp) == 1) {
      new_center <- matrix(ncol=2,
         rowMeans(jpbox)) + (rnorm(2) / 1000);
   } else if ("label" %in% center_method) {
      # use mean of label positions
      if (all(c("label_x", "label_y") %in% colnames(jp))) {
         new_center <- cbind(
            x=mean(range(jp@polygons$label_x[which_jp], na.rm=TRUE)),
            y=mean(range(jp@polygons$label_y[which_jp], na.rm=TRUE)))
      } else {
         label_xy <- labelr_JamPolygon(jp[which_jp, ]);
         # average from range of labels
         # center <- cbind(
         #    x=mean(range(label_xy[,1], na.rm=TRUE)),
         #    y=mean(range(label_xy[,2], na.rm=TRUE)))
         # average from all actual labels
         new_center <- cbind(
            x=mean(label_xy[,1], na.rm=TRUE),
            y=mean(label_xy[,2], na.rm=TRUE))
      }
   }
   if (length(center) == 2) {
      # relative adjustment
      if (TRUE %in% relative) {
         center[1] <- diff(jpbox["x", ]) * center[1];
         center[2] <- diff(jpbox["x", ]) * center[2];
      }
      center <- center + new_center;
   } else {
      center <- new_center
   }
   colnames(center) <- c("x", "y");
   # jamba::printDebug("center:");print(center);# debug
   # jamba::printDebug("which_jp:");print(which_jp);# debug
   # jamba::printDebug("jp:");print(jp);# debug
   
   # reference coordinate for each polygon
   polyref_xy <- jamba::rbindList(lapply(which_jp, function(iwhich){
      # get sub-polygon
      ijp <- jp[iwhich, ];
      # jamba::printDebug("ijp:");print(ijp);# debug
      # jamba::printDebug("ijp@polygons$x:");print(ijp@polygons$x);# debug
      if (length(jamba::rmNA(unlist(ijp@polygons$x))) == 0) {
         # no polygon present
         return(cbind(x=NA_integer_, y=NA_integer_))
      }
      # get point farthest from center
      if ("farthest" %in% vector_method) {
         # get farthest point from xy
         # print(center);
         xymax <- farthest_point_JamPolygon(center, ijp);
      } else if ("label" %in% vector_method) {
         xymax <- labelr_JamPolygon(ijp);
      }
      # Todo: Consider check if xymax == center
      xymax;
   }));
   # jamba::printDebug("polyref_xy:");print(polyref_xy);# debug
   rownames(polyref_xy) <- names(jp)[which_jp];

   # iterate multiple polygons to find angles
   angles1 <- sapply(which_jp, function(iwhich){
      iname <- names(jp)[iwhich];
      xymax <- polyref_xy[iname, , drop=FALSE];
      # get reference position
      x1 <- c(center[1, 1], xymax[1, 1]);
      y1 <- c(center[1, 2], xymax[1, 2]);
      
      # add a small random value to prevent identical angles
      angle <- jamba::rad2deg(
         atan2(y=diff(y1), x=diff(x1)) + 
            rnorm(1) * 1e-6) %% 360;
      return(angle);
   });
   names(angles1) <- names(jp)[which_jp];
   # jamba::printDebug("angles1:");print(angles1);# debug
   
   # logic here to spread out angles too close to each other
   angles <- angles1;
   not_na <- !is.na(angles1);
   angles[not_na] <- spread_degrees(angles1[not_na],
      min_degrees=min_degrees);
   names(angles) <- names(jp)[which_jp];

   if (verbose > 1) {   
      jamba::printDebug("label_outside_JamPolygon(): ",
         "angles:");
      print(head(data.frame(angles1, angles), 20));
   }
   
   # expand to twice the bbox size
   # max_radius <- max(rowDiffs(jpbox)) * 2;
   max_radius <- max(apply(jpbox, 1, diff)) * 2;

   ## for each angle find the line segment
   segmentxy_list <- lapply(which_jp, function(iwhich){
      # get sub-polygon
      if (verbose > 1) {
         jamba::printDebug("label_outside_JamPolygon(): ",
            "iwhich: ", match(iwhich, which_jp), " of ", length(which_jp));
      }
      ijp <- jp[iwhich, ];
      iname <- names(ijp);
      
      # define point outside using angle and max_radius
      idistance <- distance[[iname]];
      angle <- angles[[iname]];
      xedge <- cos(jamba::deg2rad(angle)) * 
         (max_radius + idistance) + center[1,1];
      yedge <- sin(jamba::deg2rad(angle)) * 
         (max_radius + idistance) + center[1,2];
      
      # Find the point at the encompassing polygon outer edge.
      # This point is the outside label position.
      # polygon_label_segment()
      if (verbose > 1) {
         jamba::printDebug("label_outside_JamPolygon(): ",
            "started first label_segment_JamPolygon.")
      }
      plsxy1 <- label_segment_JamPolygon(
         jp=jpall,
         buffer=idistance * 1,
         relative=FALSE,
         x0=xedge,
         y0=yedge,
         x1=center[1, 1],
         y1=center[1, 2],
         verbose=verbose,
         ...);
      if (verbose > 1) {
         jamba::printDebug("label_outside_JamPolygon(): ",
            "completed first label_segment_JamPolygon");
      }
      
      # find the point at the specific polygon outer edge
      if ("vector" %in% segment_method) {
         # use the boundary point on the vector to the inside label position
         # not the closest point on the inside polygon
         x1use <- polyref_xy[iname, 1];
         y1use <- polyref_xy[iname, 2];
      } else {
         # use closest point on the inside polygon
         # not along the vector to the inside label position
         x1use <- plsxy1[1, 1];
         y1use <- plsxy1[1, 2];
      }

      # polygon_label_segment()
      if (verbose > 1) {
         jamba::printDebug("label_segment_JamPolygon call:");
         print(list(
            jp=ijp,
            buffer=buffer[[iname]],
            relative=relative,
            x0=unlist(plsxy1[1, 1]),
            y0=unlist(plsxy1[1, 2]),
            x1=x1use,
            y1=y1use
         ))
      }
      plsxy <- label_segment_JamPolygon(
         jp=ijp,
         buffer=buffer[[iname]],
         relative=relative,
         x0=unlist(plsxy1[1, 1]),
         y0=unlist(plsxy1[1, 2]),
         x1=x1use,
         y1=y1use,
         verbose=verbose,
         ...);
      if (verbose > 1) {
         jamba::printDebug("label_outside_JamPolygon(): ",
            "completed second label_segment_JamPolygon");
      }
      
      # line segment
      segmentxy <- cbind(
         x=unname(c(plsxy[1, 1], plsxy1[1, 1])),
         y=unname(c(plsxy[1, 2], plsxy1[1, 2])),
         degrees=unname(angle));
      rownames(segmentxy) <- c("border", "label");
      adjdf <- degrees_to_adj(angle,
         top=90,
         clockwise=FALSE);
      segmentxy <- cbind(segmentxy,
         adjx=adjdf[, 1])
      segmentxy <- cbind(segmentxy,
         adjy=adjdf[, 2])
      
      return(segmentxy);
   });
   if (verbose > 1) {   
      jamba::printDebug("label_outside_JamPolygon(): ",
         "completed segmentxy_list");
   }
   
   names(segmentxy_list) <- names(jp)[which_jp];
   
   return(invisible(segmentxy_list));
}

#' Get the farthest polygon point from a reference point
#'
#' @family JamPolygon
#'
#' @returns `numeric` matrix with columns `"x"` and `"y"`
#' 
#' @param x `numeric` matrix with columns `"x"` and `"y"`
#' @param jp `JamPolygon`
#' @param ... additional arguments are ignored.
#'
#' @export
farthest_point_JamPolygon <- function
(x,
 jp,
 ...)
{
   # get polygon points
   jpx <- unlist(jp@polygons[, "x"])
   jpy <- unlist(jp@polygons[, "y"])
   jpxy <- cbind(x=jpx, y=jpy);

   # validate x
   if (!all(c("x", "y") %in% colnames(x)) || !is.numeric(x) || length(x) == 0) {
      stop("x must be a numeric matrix with colnames 'x' and 'y'");
   }
   xy <- x[, c("x", "y"), drop=FALSE];

   xydist <- as.matrix(dist(rbind(xy, jpxy)))[-1, 1];
   xymax <- jpxy[which.max(xydist), , drop=FALSE];
   colnames(xymax) <- c("x", "y");
   return(xymax);
}

#' Get the nearest polygon point to a reference point
#' 
#' @family JamPolygon
#'
#' @returns `numeric` matrix with columns `"x"` and `"y"`
#' 
#' @param x `numeric` matrix with columns `"x"` and `"y"`
#' @param jp `JamPolygon`
#' @param ... additional arguments are ignored.
#'
#' @export
nearest_point_JamPolygon <- function
(x,
 jp,
 ...)
{
   # get polygon points
   jpx <- unlist(jp@polygons[, "x"])
   jpy <- unlist(jp@polygons[, "y"])
   jpxy <- cbind(x=jpx, y=jpy);
   
   # validate x
   if (!all(c("x", "y") %in% colnames(x)) || !is.numeric(x) || length(x) == 0) {
      stop("x must be a numeric matrix with colnames 'x' and 'y'");
   }
   xy <- x[, c("x", "y"), drop=FALSE];
   
   xydist <- as.matrix(dist(rbind(xy, jpxy)))[-1, 1];
   xymax <- jpxy[which.min(xydist), , drop=FALSE];
   colnames(xymax) <- c("x", "y");
   return(xymax);
}

#' Define a label segment for JamPolygon
#' 
#' @family JamPolygon
#' 
#' @export
label_segment_JamPolygon <- function
(x0,
 x1,
 y0,
 y1,
 jp,
 return_class=c("point",
    "matrix"),
 buffer=0,
 plot_debug=FALSE,
 relative=TRUE,
 verbose=FALSE,
 ...)
{
   return_class <- match.arg(return_class);
   
   if (length(x0) > 1) {
      if (verbose) {
         jamba::printDebug("label_segment_JamPolygon(): ",
            "iterating ", length(x0), " input entries.");
      }
      i <- seq_along(x0);
      x1 <- rep(x1, length.out=length(x0));
      y0 <- rep(y0, length.out=length(x0));
      y1 <- rep(y1, length.out=length(x0));
      buffer <- rep(buffer, length.out=length(x0));
      jpi <- ((i - 1) %% length(jp)) + 1;
      # jamba::printDebug("i:", i);# debug
      # jamba::printDebug("jpi:", jpi);# debug
      listout <- lapply(i, function(j){
         # jamba::printDebug("j:", j);# debug
         # jamba::printDebug("class(jp):", class(jp));# debug
         jp_use <- jp[[jpi[j]]];
         lsjp <- label_segment_JamPolygon(x0=x0[j],
            x1=x1[j],
            y0=y0[j],
            y1=y1[j],
            jp=jp_use,
            buffer=buffer[[j]],
            return_class=return_class,
            plot_debug=plot_debug,
            verbose=verbose,
            add=(j > 1),
            ...);
         # jamba::printDebug("lsjp:");print(lsjp);@ debug
         lsjp;
      });
      if ("point" %in% return_class) {
         listout <- jamba::rbindList(listout);
      }
      return(listout);
   }

   lxy <- cbind(x=c(x0, x1),
      y=c(y0, y1));

   # check for empty jp
   if ("list" %in% class(jp)) {
      if (length(jp) == 1) {
         jp <- jp[[1]];
      } else {
         jp <- do.call(rbind2, jp);
      }
      # jamba::printDebug("label_segment_JamPolygon(): ", "use_jp:");print(use_jp);# debug
      if (length(jamba::rmNA(unlist(jp@polygons$x))) == 0) {
         # jamba::printDebug("label_segment_JamPolygon(): ", "empty jp");# debug
         return(lxy);
      }
   } else {
      # jamba::printDebug("label_segment_JamPolygon(): ", "jp@polygons:");print(jp@polygons);# debug
      if (length(jamba::rmNA(unlist(jp@polygons$x))) == 0) {
         return(lxy);
      }
   }
   
   # optional polygon buffer
   buffer <- head(buffer, 1);

   # grow polygon in size using buffer
   if (!0 %in% buffer && !"list" %in% class(jp)) {
      jp <- buffer_JamPolygon(jp,
         buffer=buffer,
         relative=relative,
         ...);
   }
   
   # check line (points) are inside polygon
   P <- list(x=lxy[, 1], y=lxy[, 2]);
   points_inside <- has_point_in_JamPolygon(x=P, jp=jp);

   # jamba::printDebug("lxy (input):");print(lxy);# debug
   if (all(points_inside)) {
      # return if sl is fully contained inside sp
      lxy[2, ] <- c(NA, NA);
   } else if (!any(points_inside)) {
      # the line does not intersect the polygon
      # use nearest point on the polygon boundary
      nearestP <- nearest_point_JamPolygon(do.call(cbind, P)[1, , drop=FALSE], jp);
      # jamba::printDebug("nearestP:");print(nearestP);# debug
      lxy[2, c("x", "y")] <- c(nearestP[1, "x"], nearestP[1, "y"]);
   } else {
      # subtract the polygon from the line
      line_jp <- new("JamPolygon",
         polygons=data.frame(name="line",
            x=I(list(lxy[, 1])),
            y=I(list(lxy[, 2]))))
      # plot(rbind2(line_jp, jp))
      line_diff <- minus_JamPolygon(rbind2(line_jp, jp), closed=FALSE);
      # check for multiple line segments, take only the first
      # plot(rbind2(line_diff, jp))
      line_x <- line_diff@polygons$x[[1]];
      line_y <- line_diff@polygons$y[[1]];
      if (is.list(line_x)) {
         line_x <- line_x[[1]];
         line_y <- line_y[[1]];
      }
      line_diff@polygons$x <- I(list(line_x));
      line_diff@polygons$y <- I(list(line_y));
      lxy <- cbind(x=line_x, y=line_y)
   }

   # jamba::printDebug("lxy (after):");print(lxy);# debug
   # optional convenience step, add text adj
   if (any(is.na(lxy[, 1]))) {
      adjx <- 0.5;
      adjy <- 0.5;
   } else {
      degrees <- jamba::rad2deg(
         atan2(y=diff(-lxy[, 2]),
            x=diff(-lxy[, 1])) + 
            rnorm(1) * 1e-6) %% 360;
      adjdf <- degrees_to_adj(degrees,
         top=90,
         clockwise=FALSE);
      adjx <- adjdf[, "adjx"];
      adjy <- adjdf[, "adjy"];
   }
   lxy <- cbind(lxy, adjx=adjx, adjy=adjy);
   if ("matrix" %in% return_class) {
      return(lxy);
   } else if ("point" %in% return_class) {
      return(lxy[2, , drop=FALSE]);
   }
   #
}
jmw86069/venndir documentation built on Nov. 14, 2024, 10:12 a.m.